diff --git a/src/dns-service.mjs b/src/dns-service.mjs index ac299b5..224c322 100644 --- a/src/dns-service.mjs +++ b/src/dns-service.mjs @@ -67,6 +67,7 @@ export function serviceUnit({ port, registryBase = null, upstreams = [], + proxy = null, user = process.env.USER || process.env.LOGNAME, } = {}) { if (!entry) throw new Error("serviceUnit needs the entry script to run"); @@ -80,6 +81,13 @@ export function serviceUnit({ // registry. Recorded here, while a working resolver is still around to be // asked, rather than rediscovered at boot when it cannot be. if (upstreams.length) args.push("--upstream", upstreams.join(",")); + // Without this the supervised bridge answers every name with its origin, and + // the origin serves a certificate no CA signed — so `https://` fails on a + // machine where the pinned-TLS proxy is installed, trusted and running. + // `dns enable` has always probed for the proxy and passed it through; + // `dns service` did not, which made a service-managed bridge the one way to + // run Moshpit where HTTPS could never work. + if (proxy) args.push("--proxy", proxy); const exec = [execPath, ...args].map((part) => (/\s/.test(part) ? JSON.stringify(part) : part)).join(" "); const lines = [ diff --git a/src/dns.mjs b/src/dns.mjs index 50f95b9..22374f4 100644 --- a/src/dns.mjs +++ b/src/dns.mjs @@ -2788,7 +2788,33 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) { const upstreams = upstreamsFromArgs(rest).servers.length ? upstreamsFromArgs(rest).servers : await discoverUpstreams(); - const unit = serviceUnit({ system, entry: cliEntry(), port, registryBase, upstreams }); + // Probed the same way `enable` probes it, and for the same reason: a name + // that resolves but cannot complete a TLS handshake reads as broken to the + // person who typed the URL. Asked now, while this command can ask; the unit + // it writes runs at boot and has no way to find out later. + let proxy = null; + if (!rest.includes("--no-proxy")) { + const claimed = await fetchTlds({ registryBase }).catch(() => []); + const probeName = claimed[0] ? `a.${claimed[0]}` : null; + if (probeName) { + const local = await findLocalProxyImpl(probeName).catch(() => ({ found: false })); + if (local.found) proxy = local.address.v4 || local.address.v6; + } + } + + const unit = serviceUnit({ system, entry: cliEntry(), port, registryBase, upstreams, proxy }); + + if (proxy) { + out(`ok pinned-TLS proxy on ${proxy}:${PROXY_PORT} — names will answer there, so https:// verifies`); + out(""); + } else if (!rest.includes("--no-proxy")) { + out("! no pinned-TLS proxy found on this machine"); + out(" names will answer their origin, and a stock client cannot verify those —"); + out(" https:// will fail even though the name resolves. Install it with:"); + out(" curl -fsSL https://raw.githubusercontent.com/profullstack/moshpit-proxy/main/install.sh | sh"); + out(" then re-run this command so the unit points names at it."); + out(""); + } if (!upstreams.length) { out("! no upstream nameservers found, and none given"); diff --git a/test/dns-service.test.mjs b/test/dns-service.test.mjs index 199e1b7..fbb88f2 100644 --- a/test/dns-service.test.mjs +++ b/test/dns-service.test.mjs @@ -99,3 +99,32 @@ test("remove takes the unit away even when it was never enabled", async () => { assert.equal(result.ok, true); assert.equal(existsSync(join(home, ".config/systemd/user", UNIT_NAME)), false); }); + +/* ------------------------------------------------- pointing at the proxy */ + +// A supervised bridge that answers with the origin gives every name a +// certificate no CA signed, so `https://` fails on a machine where the +// pinned-TLS proxy is installed, trusted and running. `dns enable` has always +// probed for the proxy and passed it to the daemon; `dns service` did not, +// which made a service-managed bridge the one way to run Moshpit where HTTPS +// could not work at all. + +test("the unit points names at the local proxy when there is one", () => { + assert.match(unit({ proxy: "127.0.0.1" }), /ExecStart=.* --proxy 127\.0\.0\.1$/m); +}); + +test("a v6 proxy address survives into the unit", () => { + assert.match(unit({ proxy: "::1" }), /--proxy ::1$/m); +}); + +test("no proxy means no flag, not an empty one", () => { + // `--proxy` with nothing after it would make the daemon read the next token + // as an address, and there is no next token. + assert.doesNotMatch(unit({ proxy: null }), /--proxy/); + assert.doesNotMatch(unit(), /--proxy/); +}); + +test("upstreams and proxy coexist without eating each other's values", () => { + const text = unit({ upstreams: ["1.1.1.1", "1.0.0.1"], proxy: "127.0.0.1" }); + assert.match(text, /--upstream 1\.1\.1\.1,1\.0\.0\.1 --proxy 127\.0\.0\.1$/m); +});