From aac8058ab1b78fae37c5035b29096e64cbd81062 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 1 Aug 2026 18:49:48 +0000 Subject: [PATCH] Track the resolver setup that only existed on the box moshpit-dns.service and the systemd-resolved drop-in were configured by hand on a live machine and lived in no repository, so the working DNS setup was one disk failure from being lost and impossible to reproduce. Both are here as deployed, with the reasoning that is easy to get wrong: - the bridge runs as the machine's primary resolver, with no list of Moshpit endings at all. A per-TLD list cannot work -- 5,661 routing domains blow systemd-resolved's ~1090 cap, and even a curated list fails because a routing domain selects a *scope*, and an upstream in that scope answers NXDOMAIN before the bridge is asked. - the drop-in is named 00- so it is read first; resolved uses the first server and rotates only on failure, never on NXDOMAIN, so pre-existing resolvers remain a real fallback instead of shadowing the bridge. - MOSHPIT_DNS_CATCHALL must stay unset: it parks every unresolved name, so github.com resolves to a parking page. Restart=always because it is the resolver now; it replaced an orphaned process that would have stayed dead. --- README.md | 42 ++++++++++++++++++++++++++++++ systemd/00-moshpit.conf | 30 ++++++++++++++++++++++ systemd/moshpit-dns.service | 51 +++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 systemd/00-moshpit.conf create mode 100644 systemd/moshpit-dns.service 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