Every DNS query the name server checks send is now reported from exchangeHostPort as a dig command line with a short answer (addresses, referral, DS, NXDOMAIN) and its round-trip time; the serials check's SOA queries keep their own events with the server's result. The delegation check streams with ?stream=1 like /serials, and the Name servers tab shows its queries as they happen, keeps its result and follows a running check when you come back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d6c28ba69
commit
bb6b4ab64d
6 files changed
+334
-128
No files matched your search
@@ -92,7 +92,7 @@ function nsTable(servers, withSource) {
|
||||
// Name server checks ask other servers over the network, which can take
|
||||
// seconds. Results are kept for a while (per zone, for this page load),
|
||||
// so coming back to the tab shows them at once; "Check again" asks anew.
|
||||
const nsCache = new Map(); // zone id -> {serials: {at, resp, log} | {job}, delegation: {at, resolver, resp}}
|
||||
const nsCache = new Map(); // zone id -> {serials, delegation}: {at, resp, log, resolver} once done, {job, resolver} while running
|
||||
const nsFresh = 10 * 60 * 1000;
|
||||
|
||||
function nsEntry(zone) {
|
||||
@@ -100,32 +100,59 @@ function nsEntry(zone) {
|
||||
return nsCache.get(zone.id);
|
||||
}
|
||||
|
||||
// runSerials starts the serials check, streamed: the log fills with the
|
||||
// queries as they are sent and answered. Views subscribe to it.
|
||||
function runSerials(zone) {
|
||||
const job = { log: [], subs: new Set(), done: null, error: null, started: Date.now() };
|
||||
// runCheck starts a check ("serials" or "delegation"), streamed: its log
|
||||
// fills with the queries as they are sent and answered. Views subscribe
|
||||
// to the job; its result is kept in nsCache.
|
||||
function runCheck(zone, kind, query) {
|
||||
const job = { log: [], subs: new Set(), done: null, error: null };
|
||||
const entry = nsEntry(zone);
|
||||
entry.serials = { job };
|
||||
const resolver = query.resolver || "";
|
||||
entry[kind] = { job, resolver };
|
||||
const notify = () => job.subs.forEach((f) => f());
|
||||
stream("/zones/" + zone.id + "/serials", { stream: 1 }, (ev) => {
|
||||
stream("/zones/" + zone.id + "/" + kind, { ...query, stream: 1 }, (ev) => {
|
||||
if (ev.kind === "start") job.log.push({ id: ev.id, query: ev.query, started: Date.now() });
|
||||
else if (ev.kind === "end") {
|
||||
const q = job.log.find((q) => q.id === ev.id && !q.ended);
|
||||
if (q) Object.assign(q, { ended: Date.now(), result: ev.result, addresses: ev.addresses, error: ev.error });
|
||||
if (q) Object.assign(q, { ended: Date.now(), result: ev.result, answer: ev.answer, error: ev.error, rtt: ev.rtt_ms });
|
||||
} else if (ev.kind === "done") job.done = ev.response;
|
||||
notify();
|
||||
}).then(() => {
|
||||
if (job.done) entry.serials = { at: Date.now(), resp: job.done, log: job.log };
|
||||
if (job.done) entry[kind] = { at: Date.now(), resp: job.done, log: job.log, resolver };
|
||||
else job.error = new Error("the check ended without a result");
|
||||
notify();
|
||||
}).catch((e) => {
|
||||
job.error = e;
|
||||
delete entry.serials;
|
||||
if (entry[kind] && entry[kind].job === job) delete entry[kind];
|
||||
notify();
|
||||
});
|
||||
return job;
|
||||
}
|
||||
|
||||
// followCheck shows a running check in el, drawing progress(job) until
|
||||
// it's done and then result(response, log). It stops following once el
|
||||
// has been shown and is gone (the check goes on, and its result is kept).
|
||||
function followCheck(el, job, progress, result) {
|
||||
let seen = false, timer = null;
|
||||
const stop = () => { job.subs.delete(draw); clearInterval(timer); timer = null; };
|
||||
function draw() {
|
||||
if (el.isConnected) seen = true;
|
||||
else if (seen) { stop(); return; }
|
||||
if (job.error) mount(el, errorBox(job.error));
|
||||
else if (job.done) mount(el, result(job.done, job.log));
|
||||
else mount(el, progress(job));
|
||||
if (job.done || job.error) stop();
|
||||
}
|
||||
job.subs.add(draw);
|
||||
draw();
|
||||
if (!job.done && !job.error) {
|
||||
// Tick the "waiting n s" counters.
|
||||
timer = setInterval(() => {
|
||||
if (seen && !el.isConnected) { stop(); return; }
|
||||
for (const s of el.querySelectorAll(".elapsed")) s.textContent = "waiting " + ((Date.now() - Number(s.dataset.started)) / 1000).toFixed(1) + " s";
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// queryLog lists the queries of a check: what was sent, and the answer or
|
||||
// that it's still waiting.
|
||||
function queryLog(log) {
|
||||
@@ -133,50 +160,64 @@ function queryLog(log) {
|
||||
return h("ul", { class: "query-log" }, log.map((q) => {
|
||||
let outcome;
|
||||
if (!q.ended) outcome = [h("span", { class: "spinner", "aria-hidden": "true" }), h("span", { class: "muted elapsed", dataset: { started: q.started } }, "waiting " + secs(Date.now() - q.started))];
|
||||
else if (q.result) outcome = [nsBadge(q.result), q.result.rtt_ms ? h("span", { class: "muted" }, " " + q.result.rtt_ms + " ms") : h("span", { class: "muted" }, " " + secs(q.ended - q.started))];
|
||||
else if (q.addresses) outcome = h("span", { class: "muted mono" }, "→ " + q.addresses.join(", "));
|
||||
else if (q.error) outcome = h("span", { class: "err-text" }, q.error);
|
||||
else outcome = h("span", { class: "muted" }, "done, " + secs(q.ended - q.started));
|
||||
else {
|
||||
// The server's round-trip time when it has one; else how long the
|
||||
// answer took to arrive here (at least the timeouts, for errors).
|
||||
const ms = q.rtt || q.ended - q.started;
|
||||
const took = h("span", { class: "muted" }, ms < 1 ? "<1 ms" : ms < 1000 ? ms + " ms" : secs(ms));
|
||||
if (q.result) outcome = [nsBadge(q.result), took];
|
||||
else if (q.error) outcome = [h("span", { class: "err-text" }, q.error), took];
|
||||
else outcome = [h("span", { class: "mono" }, "→ " + (q.answer || "done")), took];
|
||||
}
|
||||
return h("li", { class: q.ended ? "" : "pending" }, h("code", null, q.query), " ", outcome);
|
||||
}));
|
||||
}
|
||||
|
||||
// serialsView fills el with the serials check: the cached result, or the
|
||||
// serialsView fills el with the serials check: the kept result, or the
|
||||
// running check's progress until its result is in.
|
||||
function serialsView(el, zone, again) {
|
||||
const entry = nsEntry(zone);
|
||||
const c = entry.serials;
|
||||
const c = nsEntry(zone).serials;
|
||||
if (!again && c && c.resp && Date.now() - c.at < nsFresh) {
|
||||
mount(el, serialsResult(c.resp, c.log, true));
|
||||
return;
|
||||
}
|
||||
const job = !again && c && c.job ? c.job : runSerials(zone);
|
||||
let seen = false, timer = null;
|
||||
const draw = () => {
|
||||
// Stop following the check once the tab is gone (the check goes on
|
||||
// and its result is kept).
|
||||
if (el.isConnected) seen = true;
|
||||
else if (seen) { job.subs.delete(draw); clearInterval(timer); return; }
|
||||
if (job.error) mount(el, errorBox(job.error));
|
||||
else if (job.done) mount(el, serialsResult(job.done, job.log, false));
|
||||
else {
|
||||
const local = job.log.find((q) => q.id === 0);
|
||||
mount(el,
|
||||
h("p", null, "This server: serial ", local && !local.ended ? [h("span", { class: "spinner", "aria-hidden": "true" }), h("span", { class: "muted" }, "asking named")] : h("span", { class: "muted" }, "…")),
|
||||
h("p", { class: "muted small" }, job.log.length ? "Waiting for the servers to answer:" : "Finding the servers to ask…"),
|
||||
queryLog(job.log));
|
||||
}
|
||||
if ((job.done || job.error) && timer) { clearInterval(timer); timer = null; }
|
||||
};
|
||||
job.subs.add(draw);
|
||||
draw();
|
||||
// Tick the "waiting n s" counters.
|
||||
if (!job.done && !job.error) {
|
||||
timer = setInterval(() => {
|
||||
if (seen && !el.isConnected) { clearInterval(timer); job.subs.delete(draw); return; }
|
||||
for (const s of el.querySelectorAll(".elapsed")) s.textContent = "waiting " + ((Date.now() - Number(s.dataset.started)) / 1000).toFixed(1) + " s";
|
||||
}, 200);
|
||||
const job = !again && c && c.job ? c.job : runCheck(zone, "serials", {});
|
||||
followCheck(el, job, (job) => {
|
||||
const local = job.log.find((q) => q.id === 0);
|
||||
return [
|
||||
h("p", null, "This server: serial ", local && !local.ended ? [h("span", { class: "spinner", "aria-hidden": "true" }), h("span", { class: "muted" }, "asking named")] : h("span", { class: "muted" }, "…")),
|
||||
h("p", { class: "muted small" }, job.log.length ? "Waiting for the servers to answer:" : "Finding the servers to ask…"),
|
||||
queryLog(job.log)];
|
||||
}, (resp, log) => serialsResult(resp, log, false));
|
||||
}
|
||||
|
||||
// delegationView fills el with the delegation check, like serialsView.
|
||||
// It starts a check only when asked (run), or follows one that's running.
|
||||
function delegationView(el, zone, resolver, run) {
|
||||
const c = nsEntry(zone).delegation;
|
||||
if (!run) {
|
||||
if (c && c.job) followCheck(el, c.job, delegationProgress, (resp, log) => delegationResult(resp, log, false));
|
||||
else if (c && c.resp && Date.now() - c.at < nsFresh) mount(el, delegationResult(c.resp, c.log, true, c.at));
|
||||
return;
|
||||
}
|
||||
const job = runCheck(zone, "delegation", resolver ? { resolver } : {});
|
||||
followCheck(el, job, delegationProgress, (resp, log) => delegationResult(resp, log, false));
|
||||
}
|
||||
|
||||
function delegationProgress(job) {
|
||||
return [
|
||||
h("p", { class: "muted small" }, job.log.length
|
||||
? "Finding the parent zone, asking its servers for the delegation and the DS records, then each delegated server for the zone:"
|
||||
: [h("span", { class: "spinner", "aria-hidden": "true" }), "Starting…"]),
|
||||
queryLog(job.log)];
|
||||
}
|
||||
|
||||
function delegationResult(r, log, cached, at) {
|
||||
return [
|
||||
delegationReport(r),
|
||||
log && log.length ? h("details", { class: "query-details" }, h("summary", { class: "small muted" }, "Queries sent (" + log.length + ")"), queryLog(log)) : null,
|
||||
h("p", { class: "muted small" }, "Checked ", time(r.checked_at || new Date(at).toISOString()), cached ? " (kept from earlier; Check delegation to ask anew)." : "."),
|
||||
];
|
||||
}
|
||||
|
||||
function serialsResult(r, log, cached) {
|
||||
@@ -205,22 +246,11 @@ async function serversTab(el, zone) {
|
||||
const out = h("div");
|
||||
const entry = nsEntry(zone);
|
||||
const resolver = input({ placeholder: "the server's own", value: entry.delegation ? entry.delegation.resolver : "" });
|
||||
const show = (d, cached) => mount(out, delegationReport(d.resp),
|
||||
h("p", { class: "muted small" }, "Checked ", time(new Date(d.at).toISOString()), cached ? " (kept from earlier; Check delegation to ask anew)." : "."));
|
||||
const run = async (e) => {
|
||||
const run = (e) => {
|
||||
e.preventDefault();
|
||||
const res = resolver.value.trim();
|
||||
mount(out, h("p", { class: "muted" }, h("span", { class: "spinner", "aria-hidden": "true" }),
|
||||
"Finding the parent zone through " + (res || "the server's resolver") + ", then asking its servers for the delegation and each delegated server for the zone…"));
|
||||
try {
|
||||
const d = { at: Date.now(), resolver: res, resp: await get("/zones/" + zone.id + "/delegation", res ? { resolver: res } : {}) };
|
||||
entry.delegation = d;
|
||||
if (out.isConnected) show(d, false);
|
||||
} catch (err) {
|
||||
mount(out, errorBox(err));
|
||||
}
|
||||
delegationView(out, zone, resolver.value.trim(), true);
|
||||
};
|
||||
if (entry.delegation && Date.now() - entry.delegation.at < nsFresh) show(entry.delegation, true);
|
||||
delegationView(out, zone, "", false);
|
||||
parts.push(h("div", { class: "panel" },
|
||||
h("h2", null, "Delegation from the parent zone"),
|
||||
h("p", { class: "muted small" }, "For zones on the internet: checks that the parent zone (for adbraun.lol, \"lol\") points to this zone's name servers with the right glue addresses, and that each of them serves the zone. A zone used only on your own network isn't delegated, and doesn't need this."),
|
||||
|
||||
Reference in new issue
Block a user