Mail reverse DNS check, what Jarl adds to named.conf, lists one per line

jarld checks the reverse DNS of the public zones' MX and SPF addresses as the internet sees it, every 6 hours (Exposure page, jarl exposure mail, jarl_mail_reverse_dns_ok). The Server options page, jarl option generated and GET /options/generated list what Jarl writes into named.conf by itself; stats.rate_limit_log turns off the query-errors logging. Option lists of three or more elements are shown one per line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-12 21:02:11 +01:00
1 parent d33f573782
commit c27a6c10bd
30 files changed
+1095 -16

No files matched your search

+35 -3
View File
@@ -1,8 +1,8 @@
// Internet exposure (docs/exposure.md): which views are reachable from
// the internet, why, and what to harden, with fixes staged like any edit
// and findings that can be accepted on purpose.
import { h, mount, table, toast, badge, modal, input, field, errorBox, select, plural } from "../ui.js";
import { get, change, describeResult, can } from "../api.js";
import { h, mount, table, toast, badge, modal, input, field, errorBox, select, plural, relTime } from "../ui.js";
import { api, get, change, describeResult, can } from "../api.js";
import { rerender } from "../app.js";
const sevKind = { warning: "warn", error: "err", info: "" };
@@ -28,7 +28,11 @@ export async function render(el) {
parts.push(h("div", { class: "panel" }, h("h2", null, "Findings ", badge(plural(all.filter((f) => f.severity === "warning").length, "warning"), all.some((f) => f.severity === "warning") ? "warn" : "ok")),
all.length ? h("div", { class: "findings" }, all.map((f) => findingRow(f, writable))) : h("div", { class: "empty" }, r.scopes.some((s) => s.public) ? "Nothing to harden." : "No view is reachable from the internet, so there is nothing to check.")));
if (r.rate_limiting) parts.push(activity(r.rate_limiting, r.scopes));
if (r.rate_limiting && r.rate_limit_log) parts.push(activity(r.rate_limiting, r.scopes));
else if (r.rate_limiting && r.scopes.some((s) => s.public && s.rate_limit !== "off")) {
parts.push(h("div", { class: "msg info" }, "What rate limiting limits isn't counted: stats.rate_limit_log is off in /etc/jarl/jarl.yaml, so its log lines stay in named's syslog only."));
}
if (r.scopes.some((s) => s.public)) parts.push(mailPanel(r.mail));
mount(el, parts);
}
@@ -118,3 +122,31 @@ function activity(a, scopes) {
table([{ label: "Client network (limited or would be)", render: (e) => h("span", { class: "mono" }, e.key) }, { label: "Responses", render: (e) => String(e.count) }], a.networks || []),
table([{ label: "Name and type", render: (e) => h("span", { class: "mono" }, e.key) }, { label: "Responses", render: (e) => String(e.count) }], a.names || []))]);
}
// mailPanel: the reverse DNS of the addresses that send and receive the
// public zones' mail, as receivers check it (docs/exposure.md#mail).
function mailPanel(m) {
const out = h("div");
const lv = { ok: "ok", info: "", warning: "warn" };
const draw = (x) => {
if (!x) return mount(out, h("div", { class: "muted" }, "Not checked yet: Jarl checks a minute after it starts, then every 6 hours."));
const rows = x.results.length ? table([
{ label: "Address", render: (r) => [h("span", { class: "mono" }, r.address), h("div", { class: "muted small" }, r.zone + ": " + r.roles.join(", "))] },
{ label: "PTR", render: (r) => r.ptr.length ? h("span", { class: "mono" }, r.ptr.join(", ")) : h("span", { class: "muted" }, "none") },
{ label: "Reverse zone run by", render: (r) => h("span", { class: "small" }, r.managed_by || "–") },
{ label: "", render: (r) => [badge(r.ok ? (r.level === "info" ? "works, note" : "works") : "problem", lv[r.level]),
r.level !== "ok" ? h("div", { class: "small" }, r.message) : null,
r.local_zone ? h("div", { class: "muted small" }, "Jarl's reverse zone " + r.local_zone + " only answers your own clients: receivers ask " + (r.managed_by || "the address's owner") + ".") : null] },
], x.results) : h("div", { class: "empty" }, "No public zone has an MX record or SPF addresses.");
mount(out, rows, (x.errors || []).map((e) => h("div", { class: "msg err small" }, e)),
h("div", { class: "muted small" }, "Checked ", relTime(x.checked), "."));
};
draw(m);
const btn = h("button", { class: "small", onclick: async () => {
btn.disabled = true;
try { draw(await api("POST", "/exposure/mail", { body: {} })); } catch (e) { toast(e.message, "err", 8000); } finally { btn.disabled = false; }
} }, "Check now");
return h("div", { class: "panel" }, h("div", { class: "page-head" }, h("h2", { class: "grow" }, "Mail reverse DNS"), h("div", { class: "actions" }, btn)),
h("p", { class: "muted small" }, "Receivers such as Gmail and Outlook reject or mark mail from an address without a PTR record whose name resolves back to it. These records belong to whoever owns the address (your hosting provider or ISP), so Jarl looks them up as the internet sees them: the addresses of your public zones' MX hosts and SPF records."),
out);
}
+19 -1
View File
@@ -159,14 +159,32 @@ export async function render(el) {
const settingsEl = h("div");
const serverEl = h("div");
const topEl = h("div");
const genEl = h("div");
mount(el,
h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Server options"),
h("div", { class: "muted" }, "Global named options, top-level statements such as logging and controls, and Jarl's defaults for new zones."))),
settingsEl, serverEl, topEl);
settingsEl, serverEl, topEl, genEl);
settingsForm(settingsEl, settings);
await optionsEditor(serverEl, { scope: "options", catalogScope: "options", title: "options { }", intro: "These apply to the whole server; views and zones can override many of them." });
await optionsEditor(topEl, { scope: "toplevel", catalogScope: "toplevel", title: "Top-level statements",
intro: "Statements outside options { }, such as logging, controls, statistics-channels, server, remote-servers, tls or include. Values are named.conf syntax, e.g. { channel ... ; category ... ; } for logging." });
generatedPanel(genEl, await get("/options/generated").catch(() => []));
}
// generatedPanel lists what Jarl adds to named.conf by itself: read-only,
// with why and what turns it on or off.
function generatedPanel(el, list) {
if (!list.length) return;
mount(el, h("div", { class: "panel" },
h("h2", null, "Added by Jarl"),
h("p", { class: "muted" }, "Jarl writes these into named.conf itself, for its own features. They aren't options you can edit here: each is controlled by the setting shown."),
h("div", { class: "table-wrap" }, h("table", null,
h("thead", null, h("tr", null, h("th", null, "Where"), h("th", null, "Statement"), h("th", null, "Why"), h("th", null, "Controlled by"))),
h("tbody", null, list.map((g) => h("tr", null,
h("td", { class: "small" }, g.where),
h("td", null, h("pre", { class: "code optval", style: "white-space: pre-wrap; word-break: break-all" }, g.text)),
h("td", { class: "small" }, g.why),
h("td", { class: "small mono" }, g.setting))))))));
}
function settingsForm(el, s) {