Follow-up to #94379 / #94394.
After #94394 landed the caller-scoped ensurePortAvailable(port, host?), the Chrome CDP caller was fixed, but startSshPortForward in src/infra/ssh-tunnel.ts still calls it host-less:
await ensurePortAvailable(localPort);
This re-introduces the exact #94379 flaw on the SSH-tunnel surface: on a dual-stack host the host-less probe binds the IPv6 wildcard :: and misses an IPv4-only occupant, so the preflight reports a busy port as free.
It's also internally inconsistent — every other probe in this module is pinned to IPv4 loopback:
pickEphemeralPort → listen(0, "127.0.0.1")
canConnectLocal → connect({ host: "127.0.0.1" })
- the forward itself →
-L ${localPort}:127.0.0.1:${remotePort}
Fix: pass the interface this caller actually owns, matching #94394's pattern:
await ensurePortAvailable(localPort, "127.0.0.1");
Lower harm than the Chrome path (it only affects SSH-tunnel port selection, and it already falls back to an ephemeral port on EADDRINUSE), but it's the same root cause and a one-line scoped fix. Happy to send a PR.
Follow-up to #94379 / #94394.
After #94394 landed the caller-scoped
ensurePortAvailable(port, host?), the Chrome CDP caller was fixed, butstartSshPortForwardinsrc/infra/ssh-tunnel.tsstill calls it host-less:This re-introduces the exact #94379 flaw on the SSH-tunnel surface: on a dual-stack host the host-less probe binds the IPv6 wildcard
::and misses an IPv4-only occupant, so the preflight reports a busy port as free.It's also internally inconsistent — every other probe in this module is pinned to IPv4 loopback:
pickEphemeralPort→listen(0, "127.0.0.1")canConnectLocal→connect({ host: "127.0.0.1" })-L ${localPort}:127.0.0.1:${remotePort}Fix: pass the interface this caller actually owns, matching #94394's pattern:
Lower harm than the Chrome path (it only affects SSH-tunnel port selection, and it already falls back to an ephemeral port on
EADDRINUSE), but it's the same root cause and a one-line scoped fix. Happy to send a PR.