mux web: two hubs silently share the same port (reuse_address) — refuse like mux d refuses a taken socket #27

Closed
opened 2026-08-20 12:28:02 +00:00 by a73x · 2 comments
a73x commented 2026-08-20 12:28:02 +00:00 (Migrated from git.a73x.sh)

Observed during the 0x0 browser smoke: a stale muxweb and a fresh one both bound 127.0.0.1:7681 (ss showed two LISTEN rows for the port), and browser connections landed on either at random — the page showed the stale hub's wall. muxd refuses a socket path another daemon owns; muxweb should refuse a port another hub owns instead of silently sharing it. Repro: start muxweb twice; both report 'serving http://127.0.0.1:7681'.


Migrated from git-collab issue bd0f8c7d7e4d4f82e1a902649a1387c7d7a41dd2.

Original author: a73x dev@a73x.sh.

Close reason: fixed in 28780537: mux web refuses a port another hub listens on; e2e_06_web taken-port leg pins it on both OSes via os_oracle tcp_listeners (99b86336)

Observed during the 0x0 browser smoke: a stale muxweb and a fresh one both bound 127.0.0.1:7681 (ss showed two LISTEN rows for the port), and browser connections landed on either at random — the page showed the stale hub's wall. muxd refuses a socket path another daemon owns; muxweb should refuse a port another hub owns instead of silently sharing it. Repro: start muxweb twice; both report 'serving http://127.0.0.1:7681'. --- Migrated from git-collab issue `bd0f8c7d7e4d4f82e1a902649a1387c7d7a41dd2`. Original author: a73x <dev@a73x.sh>. Close reason: fixed in 28780537: mux web refuses a port another hub listens on; e2e_06_web taken-port leg pins it on both OSes via os_oracle tcp_listeners (99b86336)
a73x commented 2026-08-29 12:29:55 +00:00 (Migrated from git.a73x.sh)

2026-08-29: still true — addr.listen(.{ .reuse_address = true }) at src/cli/webhub_main.zig:173.

2026-08-29: still true — addr.listen(.{ .reuse_address = true }) at src/cli/webhub_main.zig:173.
forge-4 commented 2026-09-02 10:16:53 +00:00 (Migrated from git.a73x.sh)

[claude 2026-09-02] Picking this up from forge-4. Approach: the sharing is SO_REUSEPORT, not SO_REUSEADDR. Zig's std.net.Address.listen sets BOTH for any non-unix family when reuse_address is true (deps/zig/lib/std/net.zig:279-294), and SO_REUSEPORT is exactly the option that lets several processes hold one addr:port while the kernel load-balances new connections across them -- two LISTEN rows, browsers landing on either. Measured on this box: two plain SO_REUSEADDR binds of 127.0.0.1:P are refused EADDRINUSE, so REUSEPORT is the whole of it. Fix: bind the hub's port by hand in webhub.zig (socket + SO_REUSEADDR only + bind + listen, wrapped in std.net.Server) so REUSEADDR keeps doing the one job it is for -- a hub restarting while its old accepted connections sit in TIME_WAIT still gets its port back -- while a port a live hub owns comes back AddressInUse. webhub_main then answers that with muxd's own one-liner shape ('mux d: a daemon is already running on PATH' -> 'mux web: a hub is already running on 127.0.0.1:PORT', rc 1), which is also what decisions.md already records for muxd's AddressInUse case. Tests: unit test in webhub.zig that the second bind of a port is refused and that the fd carries REUSEADDR without REUSEPORT (asked of the descriptor, not of the call), plus an e2e leg in e2e_06_web.sh starting a second real hub on the live hub's port.

[claude 2026-09-02] Picking this up from forge-4. Approach: the sharing is SO_REUSEPORT, not SO_REUSEADDR. Zig's std.net.Address.listen sets BOTH for any non-unix family when reuse_address is true (deps/zig/lib/std/net.zig:279-294), and SO_REUSEPORT is exactly the option that lets several processes hold one addr:port while the kernel load-balances new connections across them -- two LISTEN rows, browsers landing on either. Measured on this box: two plain SO_REUSEADDR binds of 127.0.0.1:P are refused EADDRINUSE, so REUSEPORT is the whole of it. Fix: bind the hub's port by hand in webhub.zig (socket + SO_REUSEADDR only + bind + listen, wrapped in std.net.Server) so REUSEADDR keeps doing the one job it is for -- a hub restarting while its old accepted connections sit in TIME_WAIT still gets its port back -- while a port a live hub owns comes back AddressInUse. webhub_main then answers that with muxd's own one-liner shape ('mux d: a daemon is already running on PATH' -> 'mux web: a hub is already running on 127.0.0.1:PORT', rc 1), which is also what decisions.md already records for muxd's AddressInUse case. Tests: unit test in webhub.zig that the second bind of a port is refused and that the fd carries REUSEADDR without REUSEPORT (asked of the descriptor, not of the call), plus an e2e leg in e2e_06_web.sh starting a second real hub on the live hub's port.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
a73x/mux#27
No description provided.