Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/dns-service.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 = [],
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 = [],
Expand Down
46 changes: 42 additions & 4 deletions src/trust.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`;
}

/**
Expand Down
41 changes: 39 additions & 2 deletions test/trust.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading