From 113ed06da0260728e78585b075ac1c442014d3bf Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 30 Aug 2026 17:38:44 +0000 Subject: [PATCH] fix(dns): enable installs the proxy and the root without help MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs, each of which forced a manual step, and together they were the whole of the manual setup a desktop needed. 1. `proxyWrapperPath()` looked in homedir(). `dns enable` escalates, so that is /root — where moshpit-proxy is definitively not installed. It reported "no pinned-TLS proxy installed" on a machine with one running, and told people to install what they already had. Now the operator's home, via the `operatorHome` this codebase already had for exactly this mistake. Same for the generated unit and MOSHPIT_PROXY_DIR, which were pointing a system service at /root/.moshpit — where the local root is not. 2. `requireNameConstraints` refused any root that did not permit *every* claimed ending. The root is minted by moshpit-proxy for the endings it serves — a handful — while the registry has sold 18224, so this could never pass on a real machine. It refused a working root for not covering 18000 endings nobody was trying to reach, and the advice it gave (regenerate the root) could not help, because the new root is scoped to the same handful. A root that covers some of what you resolve is worth what it covers, so it installs and reports the shortfall. Refused only when it covers none of them — then it is the wrong root, not a partial one. The gate that matters is untouched: a root permitting an ending we do not resolve can vouch outside the namespace, and is still refused. 3. Both refusals printed the full list. At 18224 endings that is several screens, which is not a diagnostic — it is why the line above it goes unread. `summarise` caps it at eight and a count. Together these are what made `dns enable` finish the job: the proxy service installs because the wrapper is found, and the root reaches the system store because a partial root is no longer a refusal — which is what curl needs and what `moshpit-trust` alone never provides. Four tests rewritten around the new policy, including one asserting a refusal stays under 400 characters with 500 endings in play. Suite: 2744, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ThnQwoieWt8VR6N7gtgnhp --- src/dns-service.mjs | 7 ++++--- src/trust.mjs | 46 +++++++++++++++++++++++++++++++++++++++++---- test/trust.test.mjs | 41 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/dns-service.mjs b/src/dns-service.mjs index efca84a0..f928722b 100644 --- a/src/dns-service.mjs +++ b/src/dns-service.mjs @@ -33,6 +33,7 @@ import { mkdir, rm, writeFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; +import { operatorHome } from "./trust.mjs"; export const UNIT_NAME = "moshcode-dns.service"; @@ -217,7 +218,7 @@ export function proxyServicePaths() { export function proxyServiceUnit({ wrapper, nodeDir, - home = homedir(), + home = operatorHome(), user = process.env.SUDO_USER || process.env.USER || process.env.LOGNAME, port = 443, tlds = [], @@ -273,7 +274,7 @@ export function proxyServiceUnit({ } /** Where moshpit-proxy's installer puts its wrapper, if it ran. */ -export function proxyWrapperPath({ home = homedir(), exists = existsSync } = {}) { +export function proxyWrapperPath({ home = operatorHome(), exists = existsSync } = {}) { const candidate = join(home, ".local/bin/moshpit-proxy"); return exists(candidate) ? candidate : null; } @@ -293,7 +294,7 @@ export function proxyWrapperPath({ home = homedir(), exists = existsSync } = {}) * answer, so the wait is generous. */ export async function ensureProxyService({ - home = homedir(), + home = operatorHome(), user = process.env.SUDO_USER || process.env.USER || process.env.LOGNAME, nodeDir = dirname(process.execPath), tlds = [], diff --git a/src/trust.mjs b/src/trust.mjs index 48e8cbc9..843aee5d 100644 --- a/src/trust.mjs +++ b/src/trust.mjs @@ -151,8 +151,24 @@ export function requireNameConstraints(text, { tlds = [] } = {}) { const permits = (tld) => constraints.permitted.some((entry) => bare(entry) === String(tld).toLowerCase()); const missing = tlds.filter((tld) => !permits(tld)); - if (missing.length) { - return { ok: false, kind: "out-of-step", why: `the root does not permit ${missing.join(", ")}` }; + // Not a refusal, and this was the single most damaging line in the file. + // + // The root is minted by moshpit-proxy for the endings it was configured to + // serve — a handful. `tlds` is every ending the registry has sold, which is + // 18224 and climbing. So this could never pass on a real machine: it refused + // to install a perfectly good root because it did not also cover 18000 + // endings nobody on that machine was trying to reach, and printed all of them + // as evidence. The advice it gave — regenerate the root — cannot help, + // because the new root is scoped to the same handful. + // + // A root that covers some of what you resolve is worth exactly what it + // covers. So it goes in, and the shortfall is a count rather than a wall. + if (missing.length === tlds.length) { + return { + ok: false, + kind: "out-of-step", + why: `the root permits none of the endings claimed (${summarise(missing)}) — it is for a different namespace`, + }; } // And nothing beyond them. One `DNS:.com` in the permitted subtree is the // whole hole this gate exists to close, and it would otherwise sail through @@ -163,10 +179,32 @@ export function requireNameConstraints(text, { tlds = [] } = {}) { return { ok: false, kind: "out-of-step", - why: `the root also permits ${foreign.join(", ")}, which is not an ending we resolve — it reaches past Moshpit`, + why: `the root also permits ${summarise(foreign)}, which is not an ending we resolve — it reaches past Moshpit`, }; } - return { ok: true, why: "constrained to Moshpit endings" }; + // Carried, not printed here: the caller decides how loud a partial root is, + // and it is the only place that knows whether the person asked about a name + // the root actually covers. + return missing.length + ? { + ok: true, + why: `constrained to Moshpit endings — covers ${tlds.length - missing.length} of ${tlds.length}`, + missing, + } + : { ok: true, why: "constrained to Moshpit endings" }; +} + +/** + * A list a person can read, rather than one that fills the terminal. + * + * A registry with 18224 endings turns any "these are wrong" message into + * several screens of text, which is not a diagnostic — it is the reason nobody + * reads the line above it. + */ +export function summarise(items, show = 8) { + const list = [...items]; + if (list.length <= show) return list.join(", "); + return `${list.slice(0, show).join(", ")} and ${list.length - show} more`; } /** diff --git a/test/trust.test.mjs b/test/trust.test.mjs index c9feab9c..1ab6919f 100644 --- a/test/trust.test.mjs +++ b/test/trust.test.mjs @@ -161,12 +161,49 @@ test("constraints that are not critical do not count", () => { assert.match(verdict.why, /critical/); }); -test("a root that does not permit the endings we resolve is refused", (t) => { +test("a root that permits none of the endings we resolve is refused, and names them", (t) => { const text = needRoot(t, MOSHPIT_ONLY); if (!text) return; - const verdict = requireNameConstraints(text, { tlds: ["hacker", "eggs"] }); + // The root is for `.hacker` and `.rank`. Asked about a namespace it shares + // nothing with, it is the wrong root, not a partial one. + const verdict = requireNameConstraints(text, { tlds: ["eggs", "2600"] }); assert.equal(verdict.ok, false); assert.match(verdict.why, /eggs/); + assert.match(verdict.why, /2600/); +}); + +test("a root that covers some of what we resolve goes in, and says how much", (t) => { + const text = needRoot(t, MOSHPIT_ONLY); + if (!text) return; + // This is the case a real machine is always in. The root is minted by + // moshpit-proxy for the endings it serves — a handful — while the registry + // has sold 18224. Refusing here meant refusing always, and the refusal + // printed all 18224 as evidence. + const verdict = requireNameConstraints(text, { tlds: ["hacker", "rank", "eggs"] }); + assert.equal(verdict.ok, true, "a root that covers `.hacker` is worth installing for `.hacker`"); + assert.deepEqual(verdict.missing, ["eggs"]); + assert.match(verdict.why, /covers 2 of 3/); +}); + +test("a root reaching past Moshpit is still refused", (t) => { + // The gate that actually matters, and the one this change must not weaken: + // `.rank` is permitted by the root but is not an ending being resolved here, + // so the root can vouch for names outside the namespace it is trusted for. + const text = needRoot(t, MOSHPIT_ONLY); + if (!text) return; + const verdict = requireNameConstraints(text, { tlds: ["hacker"] }); + assert.equal(verdict.ok, false); + assert.match(verdict.why, /reaches past Moshpit/); + assert.match(verdict.why, /rank/); +}); + +test("a refusal names a few endings, never thousands", () => { + // 18224 endings turn any diagnostic into several screens, which is how the + // line explaining the problem stops being read. + const many = Array.from({ length: 500 }, (_, i) => `e${i}`); + const verdict = requireNameConstraints("", { tlds: many }); + assert.equal(verdict.ok, false); + assert.ok(verdict.why.length < 400, `a refusal must stay readable, got ${verdict.why.length} chars`); }); test("nothing usable from openssl is a refusal, not a pass", () => {