From c381fa5da684b45f3808a6e1c030afbc306d67be Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 17 Oct 2026 07:05:25 +0000 Subject: [PATCH] =?UTF-8?q?feat(pit/dns):=20lead=20with=20Ways=20to=20conn?= =?UTF-8?q?ect=20=E2=80=94=20browser=20DoH,=20the=20opt-in=20root,=20TronB?= =?UTF-8?q?rowser,=20whole=20machine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The browser route (DNS over HTTPS with dns.moshcode.sh/dns-query) was the fifth bullet under the resolver addresses, which still read "Not published yet". It now opens the page as step one, with Chrome/Edge/Brave and Firefox paths. Step two is new: https:// on a Moshpit name stops warning once the Moshpit Root CA is trusted. The page links /api/moshpit/ca.crt, shows the root's SHA-256 from the same caMaterial() the API uses, gives moshcode dns ca [--user-only] and the by-hand imports, and says plainly that the name scope is a policy rather than a constraint. With no CA configured the step says so and links nothing. "What still breaks" no longer claims the CA is not built. Tests: test/moshpit-dns-page.test.mjs boots the router and checks order, the DoH endpoint, and both CA states; full PWA suite 794/794. Co-Authored-By: Claude Opus 5.5 --- apps/pwa/src/routes/moshpit.mjs | 51 +++++++++--- apps/pwa/test/moshpit-dns-page.test.mjs | 102 ++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 apps/pwa/test/moshpit-dns-page.test.mjs diff --git a/apps/pwa/src/routes/moshpit.mjs b/apps/pwa/src/routes/moshpit.mjs index 8c5962d7..d1c3796d 100644 --- a/apps/pwa/src/routes/moshpit.mjs +++ b/apps/pwa/src/routes/moshpit.mjs @@ -4158,6 +4158,9 @@ moshpitRouter.get("/pit/dns", async (req, res) => { const bal = req.user ? await balance(req.user.id) : 0; const { resolvers, doh, published } = resolverConfig(); const first = resolvers[0]?.address ?? ""; + // The opt-in root, when this registry signs: what makes https:// on a pit + // name stop warning. Absent, the page says so instead of linking a 503. + const ca = await caMaterial().catch(() => null); const addresses = published ? `
    ${resolvers.map((r) => ` @@ -4194,7 +4197,35 @@ moshpitRouter.get("/pit/dns", async (req, res) => {

    ${pitTabs("dns")} -

    The addresses

    +

    Ways to connect

    +

    Pick by how much of the machine you want to change. Most people want the first one.

    +
      +
    1. Just your browser — two minutes, no admin rights. Turn on DNS over HTTPS with + this registry as the provider. Chrome / Edge / Brave: Settings → Privacy and security → Security → + Use secure DNS → With: Custom. Firefox: Settings → Privacy & Security → DNS over HTTPS → + Max Protection → Custom. + ${doh ? `Paste ${esc(doh)}.` : "The endpoint appears here once a resolver is up."} + Moshpit names now open in that browser; every other site resolves as before.
    2. +
    3. https:// without the warning — ${ca + ? `trust the Moshpit Root CA (SHA-256 + ${esc(ca.rootFingerprint)}). Easiest: + moshcode dns ca --user-only (browser store, no prompt) or moshcode dns ca + (every store). By hand: Firefox → Settings → Privacy & Security → Certificates → View + Certificates → Authorities → Import, tick trust this CA to identify websites; Chrome and + Edge use the system store — macOS Keychain Access → System → import, then Always Trust; + Windows: open the file → Install Certificate → Local Machine → Trusted Root Certification + Authorities. Its promise to sign only Moshpit names is a policy, not a technical limit, so trust it + only if you trust this registry.` + : "this registry does not sign certificates yet, so https:// on a Moshpit name will warn; plain http:// works."}
    4. +
    5. Change nothing at all — TronBrowser resolves + the namespace itself over ordinary HTTPS, with no setting to touch.
    6. +
    7. The whole machine, or the whole network — moshcode dns enable sets + up a local resolver, routing and the root in one step (it asks for sudo; it never runs on its own). + Or point the machine's or router's DNS at a resolver below, so curl, git + and every app see the names too.
    8. +
    + +

    The addresses

    ${addresses}

    Set it

    @@ -4208,11 +4239,8 @@ moshpitRouter.get("/pit/dns", async (req, res) => { nameserver line in /etc/resolv.conf.
  • Router — hand it out over DHCP and every device on the network gets the namespace. This is the setup it is really for.
  • -
  • A locked-down machine where DNS is not yours to change — use DNS over HTTPS - in the browser. Firefox: Privacy & Security → DNS over HTTPS → custom provider. Chrome: - Security → Use secure DNS → custom. - ${doh ? `The endpoint is ${esc(doh)}.` : "An endpoint appears here once a resolver is up."} - Or run TronBrowser, which changes no network settings at all.
  • +
  • A locked-down machine where DNS is not yours to change — the browser route in + Ways to connect, or TronBrowser.
  • Or change nothing: TronBrowser

    @@ -4262,11 +4290,12 @@ nslookup anything.moshpit # the Windows spellingWhat still breaks

    - https:// on a Moshpit name will warn on the resolver route. No public - certificate authority will issue for scrambled.eggs, because none of them - recognise a namespace that does not descend from the ICANN root. Plain http:// works, - and so does this site. A certificate authority you opt into is the real answer, and it is not built - yet. + https:// on a Moshpit name will warn on the resolver route until you opt in. + No public certificate authority will issue for scrambled.eggs, because none + of them recognise a namespace that does not descend from the ICANN root. Plain http:// + works, and so does this site. ${ca + ? `The answer is a root you opt into — Ways to connect, step two.` + : "A certificate authority you opt into is the real answer; this registry does not run one yet."}

    TronBrowser sidesteps it rather than solving it: it rewrites the navigation to this registry's own diff --git a/apps/pwa/test/moshpit-dns-page.test.mjs b/apps/pwa/test/moshpit-dns-page.test.mjs new file mode 100644 index 00000000..40a9222c --- /dev/null +++ b/apps/pwa/test/moshpit-dns-page.test.mjs @@ -0,0 +1,102 @@ +// /pit/dns leads with "Ways to connect": the browser route, the opt-in root for +// https, TronBrowser and the whole-machine route, in that order, above the +// resolver addresses. The https step must match the registry's real state: a +// download link and fingerprint when it signs, no link to a 503 when it does not. +// +// Skips cleanly when the PWA dependencies are not installed. +import assert from "node:assert/strict"; +import fs from "node:fs"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { X509Certificate } from "node:crypto"; +import test from "node:test"; + +const require = createRequire(import.meta.url); +let deps = null; +try { + deps = { express: require("express"), cookieParser: require("cookie-parser") }; + require("@peculiar/x509"); +} catch { + deps = null; +} + +const workdir = mkdtempSync(path.join(tmpdir(), "moshcode-dns-page-test-")); +process.env.DATABASE_URL = `file:${path.join(workdir, "test.db")}`; +process.env.SESSION_SECRET = "test-secret"; +process.env.PIT_ORIGIN = "https://pit.moshcode.sh"; +process.env.MOSHPIT_DOH_URL = "https://dns.moshcode.sh/dns-query"; +delete process.env.MOSHPIT_DNS_RESOLVERS; +delete process.env.MOSHPIT_CA_CERT; +delete process.env.MOSHPIT_CA_KEY; +delete process.env.MOSHPIT_CA_ROOT; + +async function boot() { + const { migrate } = await import("../src/migrate.mjs"); + await migrate(); + const { db } = await import("../src/db.mjs"); + const { sessionMiddleware, csrfGuard } = await import("../src/lib/session.mjs"); + const { moshpitRouter } = await import("../src/routes/moshpit.mjs"); + const ca = await import("../src/lib/moshpit-ca.mjs"); + + const app = deps.express(); + app.use(deps.cookieParser()); + app.use(sessionMiddleware); + app.use(csrfGuard); + app.use(moshpitRouter); + const server = await new Promise((resolve) => { + const s = app.listen(0, "127.0.0.1", () => resolve(s)); + }); + const base = `http://127.0.0.1:${server.address().port}`; + const page = () => fetch(`${base}/pit/dns`).then(async (res) => ({ status: res.status, html: await res.text() })); + return { server, db, ca, page }; +} + +let booted = null; +const app = () => (booted ||= boot()); +const skip = !deps && "apps/pwa deps not installed"; + +test.after(() => { + if (!booted) return; + booted.then(({ server, db }) => { server.close(); db.close?.(); }) + .finally(() => { try { fs.rmSync(workdir, { recursive: true, force: true }); } catch { /* noop */ } }); +}); + +test("Ways to connect leads the page, browser route first, above the addresses", { skip }, async () => { + const { ca, page } = await app(); + ca.resetCaForTests(); + const { status, html } = await page(); + assert.equal(status, 200); + const connect = html.indexOf('id="connect"'); + assert.ok(connect > 0, "the section is on the page"); + assert.ok(connect < html.indexOf(">The addresses<"), "and comes before the resolver addresses"); + const section = html.slice(connect, html.indexOf(">The addresses<")); + assert.match(section, /Just your browser/); + assert.match(section, /Use secure DNS/); + assert.match(section, /https:\/\/dns\.moshcode\.sh\/dns-query/); + assert.ok(section.indexOf("Just your browser") < section.indexOf("TronBrowser"), "browser route is listed first"); + assert.match(section, /moshcode dns enable/); +}); + +test("with no CA, the https step says so and links no root", { skip }, async () => { + const { ca, page } = await app(); + ca.resetCaForTests(); + const { html } = await page(); + assert.doesNotMatch(html, /\/api\/moshpit\/ca\.crt/); + assert.match(html, /does not sign certificates yet/); + assert.doesNotMatch(html, /it is not built\s+yet/); +}); + +test("with a CA, the https step links the root and shows its real fingerprint", { skip }, async () => { + const { ca, page } = await app(); + const made = await ca.generateCa({ rootName: "DNS Page Test Root" }); + await ca.configureCa({ cert: made.issuer.cert, key: made.issuer.key, root: made.root.cert }); + const { html } = await page(); + assert.match(html, /href="\/api\/moshpit\/ca\.crt"/); + assert.ok(html.includes(new X509Certificate(made.root.cert).fingerprint256), "the fingerprint shown is the root's"); + assert.match(html, /moshcode dns ca --user-only/); + assert.match(html, /policy, not a technical limit/); + assert.match(html, /href="#connect">Ways to connect<\/a>, step two/); + ca.resetCaForTests(); +});