diff --git a/src/dns.mjs b/src/dns.mjs index 0a61e9e..82bc4bf 100644 --- a/src/dns.mjs +++ b/src/dns.mjs @@ -3294,6 +3294,7 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) { // Moshpit names and cannot verify them, which is bad; a machine whose DNS // was refused because an optional component would not start is worse. let proxyUnitPath = null; + let ours = false; if (!rest.includes("--no-proxy") && platform === "linux") { if (!proxyWrapper()) { out(" -- no pinned-TLS proxy installed — https:// on a name will not verify"); @@ -3306,6 +3307,10 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) { out(` ${step.ok ? "ok " : "-- "} ${step.step}${step.error ? ` — ${step.error}` : ""}`); } if (!ensured.ok) out(` -- the proxy is not serving (${ensured.reason}) — https:// will not verify`); + // Started by this run, and confirmed holding the port. That is the + // strongest evidence available that the proxy on 443 is ours, and it is + // strictly better than the handshake below. + ours = ensured.ok; } } @@ -3363,6 +3368,26 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) { // proxy and retracting it two lines later is worse than not looking. out(" -- the bridge already running was not started by this run, so it keeps its own"); out(" mode — to pick up proxy mode: moshcode dns disable && moshcode dns enable"); + } else if (ours) { + // No handshake needed, and none that would work. + // + // The probe asks the proxy for a certificate under some name. The proxy + // will not present one for a name with no key published in the registry — + // deliberately, so a browser gets a TLS failure it can explain rather than + // a reset mid-request — and this command has no way to invent a name that + // someone has registered. There is no endpoint that lists names, only + // endings, so `a.` is the best it could ever do and that is + // exactly the name the proxy refuses. + // + // Which made the check impossible to pass on a correctly configured + // machine: `proxy holds 127.0.0.1:443` and `no pinned-TLS proxy on this + // machine`, three lines apart, both true as written. + // + // So when this run installed the unit, started it, and watched it take the + // port, that is the answer. The probe below stays for a proxy this run did + // not start, where the question is genuinely open. + proxyAddress = { v4: DEFAULT_HOST, v6: null }; + out(` ok pinned-TLS proxy on ${DEFAULT_HOST}:${PROXY_PORT} — started by this run, so every live name will answer there`); } else if (!rest.includes("--no-proxy")) { const probeName = moshpitProbe || ""; if (!probeName) { diff --git a/test/dns-enable-rollback.test.mjs b/test/dns-enable-rollback.test.mjs index 8e16e14..b436547 100644 --- a/test/dns-enable-rollback.test.mjs +++ b/test/dns-enable-rollback.test.mjs @@ -472,3 +472,69 @@ function noSystem() { uid: 0, }; } + +/* --------------------------- a proxy this run started is a proxy it can trust */ + +// Detection used to ask the proxy for a certificate under `a.`. The +// proxy will not present one for a name with no key published in the registry — +// deliberately, so a browser gets a TLS failure it can explain — and nothing +// here can invent a name someone has registered, because the registry lists +// endings and not names. +// +// So on a correctly configured machine the check could not pass. Three lines +// apart, both true as written: +// +// ok proxy holds 127.0.0.1:443 +// -- no pinned-TLS proxy on this machine +// +// and the bridge was then started without proxy mode, leaving every name to +// answer its origin with a certificate no CA had signed. + +test("a proxy started by this run turns proxy mode on without a handshake", async () => { + const lines = []; + let startedWith = null; + const code = await dnsCommand(["enable"], (l) => lines.push(String(l)), { + ...noSystem(), + proxyWrapper: () => "/home/x/.local/bin/moshpit-proxy", + ensureProxy: async () => ({ ok: true, steps: [{ step: "proxy holds 127.0.0.1:443", ok: true }] }), + applyWith: async () => ({ saved: { ok: true }, applied: { ok: true, results: [] }, verified: { ok: true, checks: [] }, rolledBack: null, backups: [] }), + // If this is consulted at all the fix has not worked: the whole point is + // that no name is invented and no handshake is attempted. + findLocalProxyImpl: async () => { throw new Error("the probe must not run for a proxy we started"); }, + startBridge: async (opts) => { startedWith = opts; return { started: true, pid: 1, alreadyRunning: false }; }, + }); + + assert.equal(code, 0); + assert.match(lines.join("\n"), /started by this run/); + assert.equal(startedWith?.proxy, "127.0.0.1", "the bridge has to be told, or names still answer their origin"); +}); + +test("a proxy this run did not start is still probed", async () => { + // The question is genuinely open there, so the handshake stays. + const lines = []; + let probed = false; + await dnsCommand(["enable"], (l) => lines.push(String(l)), { + ...noSystem(), + proxyWrapper: () => "/home/x/.local/bin/moshpit-proxy", + ensureProxy: async () => ({ ok: false, reason: "not-listening", steps: [] }), + applyWith: async () => ({ saved: { ok: true }, applied: { ok: true, results: [] }, verified: { ok: true, checks: [] }, rolledBack: null, backups: [] }), + findLocalProxyImpl: async () => { probed = true; return { found: false, why: null, address: { v4: null, v6: null } }; }, + }); + + assert.equal(probed, true); +}); + +test("with no proxy installed, nothing is claimed and DNS still comes up", async () => { + const lines = []; + let startedWith = null; + const code = await dnsCommand(["enable"], (l) => lines.push(String(l)), { + ...noSystem(), + proxyWrapper: () => null, + applyWith: async () => ({ saved: { ok: true }, applied: { ok: true, results: [] }, verified: { ok: true, checks: [] }, rolledBack: null, backups: [] }), + startBridge: async (opts) => { startedWith = opts; return { started: true, pid: 1, alreadyRunning: false }; }, + }); + + assert.equal(code, 0, "a missing optional component must never refuse the machine its DNS"); + assert.equal(startedWith?.proxy, null); + assert.match(lines.join("\n"), /no pinned-TLS proxy installed/); +});