diff --git a/README.md b/README.md index 306e6eb..d6d8570 100644 --- a/README.md +++ b/README.md @@ -316,3 +316,45 @@ and Apache-2.0 carries an explicit patent grant where MIT is silent. Implement the pin scheme however you like; the NOTICE covers naming. Security policy and the trust model behind the local CA: [SECURITY.md](SECURITY.md). + +## Resolving Moshpit names on a machine (`systemd/`) + +`systemd/moshpit-dns.service` and `systemd/00-moshpit.conf` are the deployed +resolver setup, kept here because they were previously configured by hand on +each box and existed in no repository. + +```sh +cp systemd/moshpit-dns.service /etc/systemd/system/ +cp systemd/00-moshpit.conf /etc/systemd/resolved.conf.d/ +systemctl daemon-reload && systemctl enable --now moshpit-dns +systemctl restart systemd-resolved +``` + +The bridge runs as this machine's **primary** resolver, with no list of Moshpit +endings anywhere. That is not a shortcut — a per-TLD list cannot be made to work: + +- **It does not scale.** `moshcode dns install --write` emits every claimed + ending as a routing domain — 5,661 of them on one line. systemd-resolved caps + search domains near 1090 and drops the rest alphabetically, logging thousands + of `Failed to add search domain '~zoology': Argument list too long`. Endings + past the cut are configured on disk and absent from the resolver. +- **Curating the list does not fix it either.** A routing domain selects a + *scope*, and a scope tries its servers in order. With an upstream resolver in + the same scope, `.hacker` goes there first, comes back NXDOMAIN, and + systemd-resolved treats that as final — the bridge is never asked. + +Sending every query to the bridge removes both problems. It answers Moshpit +names from the registry and forwards everything else upstream (`mode=clearnet`: +the ordinary internet owns any name it can answer; the registry is a backfill). + +The drop-in is named `00-` so it is read first: systemd-resolved appends `DNS=` +in filename order and uses the first server, rotating only on *failure* — never +on NXDOMAIN. So any pre-existing resolvers stay listed as a genuine fallback for +if the bridge stops, rather than shadowing it. + +**Do not set `MOSHPIT_DNS_CATCHALL=1` here.** It parks every unresolved name on +the registry's parking address, which on a machine's own resolver means +`github.com` resolves to a parking page. Correct startup logs say `mode=clearnet`. +If clearnet names start resolving to a `69.46.46.x` address, check for a stray +`moshcode dns start` bound to `127.0.0.1:5354` — a loopback bind beats the +service's `0.0.0.0` bind and wins every local query. diff --git a/systemd/00-moshpit.conf b/systemd/00-moshpit.conf new file mode 100644 index 0000000..1f52229 --- /dev/null +++ b/systemd/00-moshpit.conf @@ -0,0 +1,30 @@ +# The Moshpit resolver, as this machine's primary DNS. +# +# Named 00- so it is read before DigitalOcean.conf: systemd-resolved appends +# DNS= across drop-ins in filename order, and it uses the first server for +# every query, rotating only on failure. So the bridge answers, and DO's +# resolvers stay listed as a fallback for if it ever stops. +# +# There is deliberately no Domains= line, and that is the whole point. +# +# The old approach listed every claimed Moshpit ending as a routing domain -- +# 5,661 of them, ~60KB on one line. systemd-resolved caps search domains at +# about 1090 and drops the rest alphabetically, logging 8,199 lines of +# "Failed to add search domain '~zoology', ignoring: Argument list too long". +# Endings past the cut (~hacker and ~eggs among them) were configured on disk +# and absent from the resolver, so the bridge answered `dig` and the browser +# still got nothing. It also does not scale: every new ending needs another +# entry, and regenerating the file clobbers any hand-curated version. +# +# It cannot be fixed by curating the list either. A routing domain sends the +# query to a *scope*, and a scope tries its servers in order -- so with DO's +# resolvers in the same global scope, `.hacker` went to 67.207.67.3, came back +# NXDOMAIN, and resolved treated that as final. The bridge was never asked. +# +# Sending everything to the bridge removes all of it. It answers Moshpit names +# from the registry and forwards the rest upstream (mode=clearnet: the ordinary +# internet owns any name it can answer; the registry is only a backfill). +# Nothing to list, no cap to hit, nothing to regenerate. + +[Resolve] +DNS=127.0.0.1:5354 diff --git a/systemd/moshpit-dns.service b/systemd/moshpit-dns.service new file mode 100644 index 0000000..68e9c2a --- /dev/null +++ b/systemd/moshpit-dns.service @@ -0,0 +1,51 @@ +[Unit] +# The Moshpit resolver, running as this machine's primary DNS. +# +# Primary rather than a side-channel on purpose. systemd-resolved cannot do +# split-horizon within one scope: a routing domain sends the query to a scope, +# and the scope tries its servers in order, so an upstream that answers NXDOMAIN +# for `.hacker` ends the lookup before the bridge is ever asked. Listing every +# Moshpit ending in `Domains=` was the workaround, and it does not scale -- +# systemd-resolved caps search domains at ~1090 and silently drops the rest. +# +# So the bridge answers everything and forwards what is not a Moshpit name. +# There is no domain list to maintain, nothing to regenerate, and no cap. +# +# MOSHPIT_DNS_CATCHALL is deliberately NOT set. It parks every unresolved name +# on the registry's parking address, which for a machine's own resolver means +# github.com resolves to a parking page. That mode belongs on the public parking +# responder, not here. +Description=Moshpit DNS bridge (primary resolver) +Documentation=https://github.com/profullstack/moshpit-proxy +After=network-online.target +Wants=network-online.target +Before=systemd-resolved.service + +[Service] +Type=simple +User=anthony +Group=anthony +WorkingDirectory=/home/anthony/moshpit-dns +Environment=MOSHPIT_DNS_PORT=5354 +Environment=MOSHPIT_DNS_HTTP_PORT=8053 +Environment=MOSHPIT_DNS_LOG=queries +ExecStart=/home/anthony/.local/share/mise/installs/bun/latest/bin/bun run scripts/moshpit-dns.ts + +# It is the resolver now. If it dies, name resolution for Moshpit names dies +# with it, so it comes back rather than staying dead like the orphaned process +# this replaced. +Restart=always +RestartSec=2 + +# Unprivileged: it binds 5354, not 53. +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=read-only +ReadWritePaths=/home/anthony/moshpit-dns +ProtectKernelTunables=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX + +[Install] +WantedBy=multi-user.target