Skip to content

fix(dns): enable rewrites the supervised bridge's unit itself - #483

Merged
ralyodio merged 1 commit into
mainfrom
feat/enable-rewrites-bridge-unit
Aug 31, 2026
Merged

fix(dns): enable rewrites the supervised bridge's unit itself#483
ralyodio merged 1 commit into
mainfrom
feat/enable-rewrites-bridge-unit

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The last step anyone still had to run by hand:

moshcode dns service --proxy-probe <a real name> --write

dns enable could detect the pinned-TLS proxy, install it, start it, watch it take 443 and put its root in the system trust store — and still leave every Moshpit name answering its origin. What a resolver answers with is fixed when it spawns, and the bridge was already running: startDaemon saw a live pidfile, reported "already running", and left it alone. Correct, and the reason proxy mode arrived a reboot late.

Stopping the bridge did not help either. The unit is Restart=always, so systemd put the same ExecStart back within the second.

Why enable could not do it

The only thing that changes a supervised bridge's mind is rewriting its unit and restarting it, and enable was the one command that could not. It escalates, and the bridge is a user unit — so systemctl --user from there addressed root's session, which has no bridge in it and never has. The unit that had to change belongs to a session the escalated half of the command cannot see.

An escalated run now drops back to the invoking user and derives the runtime directory their session bus lives in, rather than inheriting one that points at root's. servicePaths resolves the operator's home for the same reason: escalated, homedir() was /root, so the unit was being written somewhere systemd would never look.

installService also restarts rather than only enable --now, which starts a stopped unit and does nothing whatever to a running one. Without that the change lands on disk and nowhere else.

No name is invented

The probe that needed a real registered name is not on this path. enable knows the proxy address because it started the proxy — which is what v0.89.0 established — so there is nothing to look up and nothing to guess.

Upstreams are read back off the unit's own ExecStart rather than rediscovered. By this point the machine may already be routing every lookup at the bridge from an earlier enable, so asking the resolver for its upstreams can answer this bridge — and a bridge whose upstream is itself resolves nothing at all.

Four things that fell out of it

was now
disable killed the pid, printed "bridge stopped", systemd restarted it within the second stops the unit through systemd; the file stays, since turning resolution off for an afternoon should not delete a unit the operator may have written
closing note told a supervised machine the bridge does not survive a reboot says which unit brings it back
rollback signalled the pid of a bridge it had not started, and announced it had removed it leaves a supervised bridge where it found it
readiness a restart that never answers is a failure: systemctl restart returns as soon as a Type=simple unit forks, so it returns 0 for a bridge that forked and died, and the next thing this run does is point every lookup on the machine at that port

doas publishes a user name and no uid, which would have addressed root's session silently. The uid falls back to the owner of the operator's home.

Test harness

proxyWrapper and applyWith moved into noSystem(). Left to their defaults they reach the real machine — one starts an installed proxy, the other writes /etc/systemd/resolved.conf.d and restarts systemd-resolved — so a test that forgot to stub them either edited the box running the suite or failed on EACCES from somewhere that read as a bug in whatever it was actually testing. Three of the new tests hit exactly that before the stubs moved.

Checks

npm test — 2794 tests, 0 failures (up from 2782 on main; 24 new).

Not verified on a live desktop: this needs an escalated dns enable on a machine with the user unit installed, which is bonita, not the dev box. The dev box has no moshcode-dns.service at all — its bridge is moshpit-proxy's, a different implementation — so running it here would prove nothing and overwrite nothing worth overwriting. The command form was checked against the real /run/user/1000 (present, owned by uid 1000).

🤖 Generated with Claude Code

https://claude.ai/code/session_01ThnQwoieWt8VR6N7gtgnhp

The last step anyone still had to run by hand:

    moshcode dns service --proxy-probe <a real name> --write

`dns enable` could detect the proxy, install it, start it and trust its
root, and still leave every Moshpit name answering its origin — because
what a resolver answers with is fixed when it spawns, and the bridge was
already running. `startDaemon` saw a live pidfile, reported "already
running", and left it alone. Correct, and the reason proxy mode arrived a
reboot late. Stopping the bridge did not help either: the unit is
`Restart=always`, so systemd put the same ExecStart back within the second.

The only thing that changes a supervised bridge's mind is rewriting its
unit and restarting it, and `enable` could not do that. It escalates, and
the bridge is a *user* unit — so `systemctl --user` from there addressed
root's session, which has no bridge in it and never has. The unit that had
to change belongs to a session the escalated half of the command cannot
see.

So an escalated run now drops back to the invoking user and derives the
runtime directory their session bus lives in, rather than inheriting one
that points at root's. Same reason `servicePaths` now resolves the
operator's home instead of `homedir()`: escalated, that was /root, so the
unit was being written somewhere systemd would never look for it.

`installService` also restarts rather than only `enable --now`, which
starts a stopped unit and does nothing whatever to a running one — the
change would have landed on disk and nowhere else.

No name is invented and no handshake is attempted. `enable` knows the
proxy address because it started the proxy, which is why the probe that
needed a real registered name is not on this path at all.

Upstreams are read back off the unit's own ExecStart rather than
rediscovered. By this point the machine may already be routing every
lookup at the bridge from an earlier enable, so asking the resolver for
its upstreams can answer "this bridge" — a bridge whose upstream is itself
resolves nothing at all.

Four things fell out of it:

- `disable` killed the pid and printed "bridge stopped" on a machine where
  systemd restarted it within the second. It now stops the unit through
  systemd and leaves the file, since turning resolution off for an
  afternoon should not delete a unit the operator may have written.
- The closing note told a supervised machine the bridge does not survive a
  reboot. It does now, and advice that is wrong is how advice stops being
  read.
- A rollback signalled the pid of a bridge it had not started and
  announced it had removed it. Both untrue, and the second one louder.
- A restart that never answers is a failure. `systemctl restart` returns
  as soon as a Type=simple unit forks, so it returns 0 for a bridge that
  forked and died — and the next thing this run does is point every lookup
  on the machine at that port.

doas publishes a user name and no uid, which would have addressed root's
session silently; the uid falls back to the owner of the operator's home.

Two stubs moved into the enable test harness. Left to their defaults they
reach the real machine — one starts an installed proxy, the other writes
/etc/systemd/resolved.conf.d and restarts systemd-resolved — so a test
that forgot them either edited the box running the suite or failed on
EACCES from somewhere that read as a bug in whatever it was testing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThnQwoieWt8VR6N7gtgnhp
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

1 finding(s) in the 4 file(s) this pull request changes.

HIGH/CRITICAL: 1

Severity Rule Location
HIGH tls-verification-disabled src/dns.mjs:766
90 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 7 | **MEDIUM**: 73 | **LOW**: 10

Not introduced by this pull request. The full set is in the Security tab.

Severity Rule Location
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
HIGH tls-verification-disabled apps/pwa/src/lib/moshpit-gateway.mjs:299
HIGH sh-remote-script-execution install.sh:86
HIGH sh-remote-script-execution install.sh:90
HIGH sh-remote-script-execution install.sh:202
HIGH sh-remote-script-execution install.sh:213
HIGH sh-remote-script-execution install.sh:219
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:139
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:153
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:179
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:373
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:377
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:422
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:671
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:867
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:869
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:928
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:974
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1044
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1147

…and 70 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit f4b840e into main Aug 31, 2026
6 checks passed
@ralyodio ralyodio mentioned this pull request Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant