Skip to content

fix(dns): use the port holder enable said it would use - #267

Merged
ralyodio merged 1 commit into
mainfrom
fix/dns-port-gate
Aug 3, 2026
Merged

fix(dns): use the port holder enable said it would use#267
ralyodio merged 1 commit into
mainfrom
fix/dns-port-gate

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Found by running sudo moshcode dns enable on a box that already had a bridge on 5354.

  note  127.0.0.1:5354 is held by pid 2471795, which this run did not start — it forwards, so it is being used as-is
  ok    bridge started on 127.0.0.1:5354 (pid 2666577)

Both lines are true, and they contradict each other.

What happens

preflightEnable finds the holder, asks it the same clearnet question catchAllSafety asks, and on a good answer reports it is being used as-is. startDaemon then decides "already running" from our own pidfile — a holder it did not record is invisible to it — so it spawns a second daemon regardless.

Both bind, because the socket is created with reuseAddr: true (src/dns.mjs:816), and the kernel delivers to whichever took the more specific address. On the machine that found this, ours took 127.0.0.1 while the holder had 0.0.0.0, so the bridge the note promised would serve was the one getting nothing.

src/dns.mjs:1379-1398 already documents this hazard in the other direction — ours starved by a holder. This is the same collision with the roles swapped, and the note makes it worse than silence by claiming the opposite.

holderForwards was computed at 1458, used at 1459 to block a holder that does not forward, returned at 1473, printed at 2070 — and never read again. The start at 2315 never saw it.

The change

Gate the start on the preflight's own conclusion. A holder that forwards is reused and said so; everything else starts as before.

Deliberately narrow: a holder that fails the clearnet question stays a blocker, and --force past it still starts ours over it. That is the stale-bridge case, where putting a working bridge in front is the point.

Tests

a holder that forwards is used, not refused was already named for this behaviour and only asserted the note printed — which is exactly why the second daemon went unnoticed. It now asserts startBridge is never called and that the output says so. Reverting src/dns.mjs fails it on the holder is used as-is, so there is nothing to start.

A second test covers the forced non-forwarding path, so the gate cannot be widened into "never start when anything holds the port" without a failure.

Full suite: one failure, TUI /prd still publishes a PRD in a healthy cwd, which times out identically on an unmodified checkout — pre-existing, a TUI test waiting on a pty, unrelated to this.

🤖 Generated with Claude Code

Preflight finds a bridge this run did not start, asks it the clearnet
question, and on a good answer prints that it "is being used as-is".
Then `enable` started a second daemon anyway.

`startDaemon` decides "already running" from our own pidfile, so a
holder it did not record is invisible to it. Both bridges then bind --
the socket is created with reuseAddr -- and the kernel hands the query
to whichever took the more specific address. On the machine that found
this, ours took 127.0.0.1 while the holder had 0.0.0.0, so the bridge
the note promised would serve was the one receiving nothing.

`holderForwards` was computed, returned and printed, and never read
again before the start. This gates the start on it.

The existing test was already named for this -- "a holder that forwards
is used, not refused" -- and only asserted the note was printed, which
is why the second daemon went unnoticed. It now asserts the bridge is
not started, and a new test covers the other half: a holder that fails
the clearnet question is the stale-bridge case, so --force past it must
still start ours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

87 finding(s)

HIGH/CRITICAL: 50 | MEDIUM: 37

Severity Rule Location
HIGH manifest-typosquat apps/pwa/package.json:19
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
HIGH secret-generic-credential apps/pwa/test/apikey-bearer-scheme.test.mjs:30
HIGH secret-generic-credential apps/pwa/test/apikey-mask.test.mjs:38
HIGH secret-generic-credential apps/pwa/test/apikey-reveal.test.mjs:35
HIGH secret-generic-credential apps/pwa/test/approvals-context.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/approvals-credits.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/approvals-notify.test.mjs:26
HIGH secret-generic-credential apps/pwa/test/approvals-resolve-race.test.mjs:20
HIGH secret-generic-credential apps/pwa/test/auth-form-email.test.mjs:29
HIGH secret-generic-credential apps/pwa/test/auth-form-email.test.mjs:33
HIGH secret-generic-credential apps/pwa/test/auth-page-error.test.mjs:36
HIGH secret-generic-credential apps/pwa/test/cli-device-token.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/cli-pages-balance.test.mjs:32
HIGH secret-generic-credential apps/pwa/test/cli-token.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/credits-pack.test.mjs:51
HIGH secret-generic-credential apps/pwa/test/credits-webhook-event-match.test.mjs:35
HIGH secret-generic-credential apps/pwa/test/credits-webhook.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/csrf-input-escaping.test.mjs:31
HIGH secret-generic-credential apps/pwa/test/csrf-input-escaping.test.mjs:101
HIGH secret-generic-credential apps/pwa/test/logout-csrf.test.mjs:29
HIGH secret-generic-credential apps/pwa/test/moshpit-api-key.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/moshpit-bulk-claim.test.mjs:23
HIGH secret-generic-credential apps/pwa/test/moshpit-claim-full-name.test.mjs:29
HIGH secret-generic-credential apps/pwa/test/moshpit-crawlable.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/moshpit-ending-page.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/moshpit-pins.test.mjs:22
HIGH secret-generic-credential apps/pwa/test/moshpit-pit-page.test.mjs:33
HIGH secret-generic-credential apps/pwa/test/moshpit-records-page.test.mjs:23
HIGH secret-generic-credential apps/pwa/test/moshpit-records.test.mjs:23
HIGH secret-generic-credential apps/pwa/test/moshpit-registry.test.mjs:20
HIGH secret-generic-credential apps/pwa/test/moshpit-related-endings.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/moshpit-sales.test.mjs:16
HIGH secret-generic-credential apps/pwa/test/moshpit-search.test.mjs:74
HIGH secret-generic-credential apps/pwa/test/moshpit-terms.test.mjs:19
HIGH secret-generic-credential apps/pwa/test/moshpit-tlds-pagination.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/passkey-register-duplicate.test.mjs:38
HIGH secret-generic-credential apps/pwa/test/require-auth-next.test.mjs:31
HIGH secret-generic-credential apps/pwa/test/require-auth-next.test.mjs:35
HIGH secret-generic-credential apps/pwa/test/sessions-output-seq.test.mjs:30
HIGH secret-generic-credential apps/pwa/test/sessions-paste.test.mjs:24
HIGH secret-generic-credential apps/pwa/test/sessions-stream-replay.test.mjs:34
HIGH secret-generic-credential apps/pwa/test/sessions.test.mjs:24
HIGH secret-generic-credential apps/pwa/test/signature.test.mjs:6
HIGH secret-generic-credential test/auth.test.mjs:13
HIGH secret-generic-credential test/auth.test.mjs:63
HIGH secret-generic-credential test/console-cookie-malformed.test.mjs:15
HIGH secret-generic-credential test/console.test.mjs:12
HIGH secret-generic-credential test/mirror.test.mjs:37
HIGH secret-generic-credential test/mirror.test.mjs:77

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio marked this pull request as ready for review August 3, 2026 14:41
@ralyodio
ralyodio merged commit 0071c3b into main Aug 3, 2026
4 checks passed
@ralyodio
ralyodio deleted the fix/dns-port-gate branch August 3, 2026 14:41
@ralyodio ralyodio mentioned this pull request Aug 3, 2026
ralyodio added a commit that referenced this pull request Aug 3, 2026
Two resolver fixes, both of which look like working DNS from outside.

#268 — a name pointed at a host answered an authoritative NOERROR with
no records, which a client treats as final. seo.rank, chovy.hacker and
alt.2600 were all unreachable while google.com resolved, so the machine
looked healthy. RECORD_TYPES never consulted a published A or AAAA for
an address question, and targetAddress() returned null for a hostname
with nothing picking up after it.

#267 — `dns enable` reported that a bridge already on the port "is being
used as-is" and then started a second daemon anyway, which bound
alongside it and took the queries the note promised to the first.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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