|
| 1 | +--- |
| 2 | +openprd: "0.2" |
| 3 | +id: "0005" |
| 4 | +title: A hosted Moshpit resolver, for the devices that cannot run the bridge |
| 5 | +status: Draft |
| 6 | +authors: |
| 7 | + - anthony@chovy.com |
| 8 | +created: 2026-08-01 |
| 9 | +updated: 2026-08-01 |
| 10 | +repo: https://github.com/moshcoder/moshcode |
| 11 | +discussion: |
| 12 | +implementation: apps/pwa/src/lib/moshpit-resolvers.mjs, apps/pwa/src/routes/moshpit.mjs, src/dns.mjs |
| 13 | +tags: |
| 14 | + - moshpit |
| 15 | + - dns |
| 16 | + - infrastructure |
| 17 | + - privacy |
| 18 | +supersedes: |
| 19 | +superseded-by: |
| 20 | +--- |
| 21 | + |
| 22 | +## Problem |
| 23 | + |
| 24 | +A Moshpit ending is not in the ICANN root, so nothing resolves it until the |
| 25 | +client is told something. Today there is exactly one way to be told: run |
| 26 | +`moshcode dns enable`, which installs a local bridge and points the machine's |
| 27 | +resolver at it. |
| 28 | + |
| 29 | +That works on a laptop or a VPS. It is unavailable on the devices where most |
| 30 | +people would actually meet a Moshpit name: |
| 31 | + |
| 32 | +- a **phone**, which cannot run a daemon or edit `resolved.conf.d` |
| 33 | +- a **router**, where you can set a DNS server and nothing else |
| 34 | +- **someone else's machine** — the person you sent the link to |
| 35 | + |
| 36 | +For all of them the only answer is `pit.moshcode.sh/n/<name>`, which fetches the |
| 37 | +origin server-side and hands back the page. It works, and it is not resolution: |
| 38 | +the URL bar reads `pit.moshcode.sh`, not `chovy.hacker`. The name never becomes |
| 39 | +the visitor's address, which is the entire point of holding one. |
| 40 | + |
| 41 | +The infrastructure for the missing piece is already half-built and unused. |
| 42 | +`MOSHPIT_DNS_RESOLVERS` and `MOSHPIT_DOH_URL` are read, validated as real |
| 43 | +addresses, and rendered on `/pit/dns` — and both are unset in production, so the |
| 44 | +page publishes nothing. The shop window was built before the shop. |
| 45 | + |
| 46 | +This PRD is deliberately **not** a replacement for the local bridge. It is the |
| 47 | +answer for clients that cannot run one, and the difference matters because the |
| 48 | +tradeoff is real and permanent: a hosted resolver sees every DNS query the |
| 49 | +device makes. |
| 50 | + |
| 51 | +## Goals |
| 52 | + |
| 53 | +- A Moshpit name resolves on a device that can only be handed a DNS server. |
| 54 | +- The URL bar keeps the name. `http://chovy.hacker/` stays `chovy.hacker`. |
| 55 | +- Non-Moshpit lookups are answered correctly and are not the product. |
| 56 | +- The privacy cost is stated plainly, in the UI, before anyone opts in. |
| 57 | +- An outage degrades to "Moshpit names stop working", never "the internet stops |
| 58 | + working", for as long as that is within our control. |
| 59 | + |
| 60 | +## Non-Goals |
| 61 | + |
| 62 | +- **Replacing the local bridge.** Where a bridge can run it stays the better |
| 63 | + answer: no third party sees the queries, no round trip, no shared outage. |
| 64 | + `moshcode dns enable` remains the recommended path and the docs say so. |
| 65 | +- **Being a general-purpose public resolver.** We are not competing with |
| 66 | + 1.1.1.1. Forwarding exists so Moshpit names can resolve, not as a service. |
| 67 | +- **Logging queries for analytics.** See Requirements — this is the one place |
| 68 | + where the tempting feature is the one that kills the product. |
| 69 | +- **DNSSEC-signing the Moshpit zone.** Out of scope for v1; the endings are not |
| 70 | + in the root, so there is no chain to anchor to. |
| 71 | + |
| 72 | +## Users |
| 73 | + |
| 74 | +**Someone sent a link.** They open `http://seo.rank/` on a phone. Today: a |
| 75 | +browser error, or a `pit.moshcode.sh` URL. With this: the page, at its own name. |
| 76 | + |
| 77 | +**A household or office.** One DNS setting on the router, and every device on |
| 78 | +the network resolves Moshpit names with nothing installed on any of them. |
| 79 | + |
| 80 | +**A name holder demonstrating one.** The reason to hold `chovy.hacker` is that |
| 81 | +it is an address you can give people. Today giving it away requires asking them |
| 82 | +to install software first. |
| 83 | + |
| 84 | +## Requirements |
| 85 | + |
| 86 | +### R1 — Two protocols, both public |
| 87 | + |
| 88 | +`Do53` (plain UDP/TCP 53) for routers and OS settings, and **DNS-over-HTTPS** |
| 89 | +for phones and browsers, which increasingly will not accept anything else. |
| 90 | +iOS and Android both accept a DoH profile; neither will run our daemon. |
| 91 | + |
| 92 | +DoH is the one that unlocks phones, so it is not optional. |
| 93 | + |
| 94 | +### R2 — Anycast or nothing |
| 95 | + |
| 96 | +A single box is a single point of failure for the DNS of every device pointed |
| 97 | +at it. If the deployment cannot be multi-region behind one address, this ships |
| 98 | +as "best effort, do not set it as your only resolver" and says so in the UI. |
| 99 | + |
| 100 | +### R3 — Forwarding is the dangerous part |
| 101 | + |
| 102 | +The resolver answers Moshpit endings from the registry and forwards everything |
| 103 | +else. That forwarding is what makes it usable as a device's only DNS server — |
| 104 | +and what makes it a privacy problem, a latency tax, and a shared outage. |
| 105 | + |
| 106 | +Three requirements follow, and R3.3 is the one worth arguing about: |
| 107 | + |
| 108 | +- **R3.1** — the claimed-ending gate from `isOurs` applies here identically. |
| 109 | + `google.com` has two labels exactly like `blue.eggs`; only an ending someone |
| 110 | + has actually claimed is ours. An unknown ending set means *not* ours. |
| 111 | +- **R3.2** — forwarded answers are relayed unmodified. No injected records, no |
| 112 | + rewritten NXDOMAIN, no "helpful" search page. The moment a resolver edits |
| 113 | + answers it is spyware with a nice landing page. |
| 114 | +- **R3.3** — **no query logging beyond aggregate counters.** No per-query |
| 115 | + storage of name, source address, or the pair. This is the requirement that |
| 116 | + will be under pressure the first time someone asks "which endings are people |
| 117 | + looking up?" — and the honest answer is that we cannot know without becoming |
| 118 | + the thing the local bridge exists to avoid. Counters (queries/sec, hit rate, |
| 119 | + error rate) are fine. The tuple is not. |
| 120 | + |
| 121 | +### R4 — Say the cost before the click |
| 122 | + |
| 123 | +`/pit/dns` publishes the addresses. It must also state, in the same visual |
| 124 | +weight as the addresses themselves: |
| 125 | + |
| 126 | +> This resolver sees every DNS lookup your device makes, not just Moshpit ones. |
| 127 | +> If you can run `moshcode dns enable`, do that instead — it keeps your lookups |
| 128 | +> on your own machine. |
| 129 | +
|
| 130 | +A page that lists an IP with no context gets pasted into a router by someone who |
| 131 | +has not thought about it. That is the failure mode to design against. |
| 132 | + |
| 133 | +### R5 — Reuse the bridge, do not fork it |
| 134 | + |
| 135 | +The hosted resolver is the same code as `src/dns.mjs` with upstreams |
| 136 | +configured, deployed. The forwarding, the ending gate, the NODATA/NXDOMAIN |
| 137 | +distinction and the AAAA support all already exist and are tested. A second |
| 138 | +implementation would drift from the first, which is the failure this codebase |
| 139 | +has already had five times over with one regex. |
| 140 | + |
| 141 | +### R6 — Never the default |
| 142 | + |
| 143 | +Nothing in `moshcode dns enable`, the CLI, or the extension silently points at |
| 144 | +the hosted resolver. It is a documented address a person chooses to use. A |
| 145 | +resolver that installs itself as your DNS without asking is malware behaviour |
| 146 | +regardless of intent. |
| 147 | + |
| 148 | +## UX Notes |
| 149 | + |
| 150 | +`/pit/dns` already renders `.pit-addrs` cards for published resolvers, so the |
| 151 | +page work is mostly copy plus the DoH URL. The order should be: |
| 152 | + |
| 153 | +1. **`moshcode dns enable`** — recommended, keeps lookups local |
| 154 | +2. **`pit.moshcode.sh/n/<name>`** — works with nothing installed, URL changes |
| 155 | +3. **the hosted resolver** — for devices that cannot do (1), with R4's warning |
| 156 | + |
| 157 | +Today the page has only (1) and publishes nothing for (3). |
| 158 | + |
| 159 | +## Success Metrics |
| 160 | + |
| 161 | +- A stock phone, given only a DoH profile, loads `http://chovy.hacker/` with |
| 162 | + `chovy.hacker` in the URL bar. |
| 163 | +- Resolver p50 latency for a forwarded (non-Moshpit) query stays within ~10ms of |
| 164 | + the device's previous resolver, or the tax is visible enough that people turn |
| 165 | + it off. |
| 166 | +- Zero per-query records in storage, demonstrable from the deployment config |
| 167 | + rather than from a policy document. |
| 168 | + |
| 169 | +## Risks & Open Questions |
| 170 | + |
| 171 | +**The privacy tradeoff is not fixable, only disclosed.** Anyone using this hands |
| 172 | +us their full DNS history. R3.3 and R4 are mitigations, not solutions. If that |
| 173 | +is unacceptable, the honest outcome is to not ship this and leave `/n/` as the |
| 174 | +answer for phones. |
| 175 | + |
| 176 | +**Shared outage.** A device with this as its only DNS server loses the whole |
| 177 | +internet when we go down, not just Moshpit names. R2 exists for this and may |
| 178 | +well be the reason to delay. |
| 179 | + |
| 180 | +**Abuse.** An open forwarding resolver is a DDoS amplifier. Rate limiting and |
| 181 | +response-size limits are required before anything is published, and this is not |
| 182 | +optional or deferrable — an open resolver is found by scanners in hours. |
| 183 | + |
| 184 | +**Does the demand exist?** The cheapest version of this experiment is to publish |
| 185 | +nothing and instead measure how often `/n/` is loaded from mobile user agents. |
| 186 | +If nobody is meeting Moshpit names on phones, this is infrastructure and risk |
| 187 | +bought for an audience that is not there. |
| 188 | + |
| 189 | +**Open:** does the ending gate consult the registry per query, or hold a cached |
| 190 | +ending set refreshed on an interval? Per query is simpler and always current; |
| 191 | +cached is faster and survives a registry outage. The local bridge faces the same |
| 192 | +choice and has not resolved it either. |
0 commit comments