fix(ssh): scope tunnel port preflight to loopback (#94603)#94607
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 19, 2026, 1:55 PM ET / 17:55 UTC. Summary PR surface: Source 0, Tests +123. Total +123 across 2 files. Reproducibility: yes. Current main source still has the hostless SSH tunnel preflight plus errno-only fallback, and the PR body supplies real Review metrics: none identified. Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this focused PR after maintainer review and green required checks, then close or link the canonical SSH-tunnel issue on merge. Do we have a high-confidence way to reproduce the issue? Yes. Current main source still has the hostless SSH tunnel preflight plus errno-only fallback, and the PR body supplies real Is this the best way to solve the issue? Yes. Fixing the SSH tunnel caller to probe and listen on AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 1609365b3e74. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source 0, Tests +123. Total +123 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Quick CI note: the two red checks appear unrelated to this diff, which only touches
Core |
Dependency graph guard clearedThis PR no longer has blocked dependency graph changes. A future dependency graph change requires a fresh
|
startSshPortForward called ensurePortAvailable host-less, binding the IPv6 wildcard on dual-stack hosts and missing IPv4-only occupants — the same address-family flaw as openclaw#94379. Every other probe in this module is pinned to 127.0.0.1, so scope the preflight to match. Closes openclaw#94603 Co-Authored-By: Claude <[email protected]>
The preferred-port preflight calls ensurePortAvailable, which raises the domain PortInUseError (no errno `code`) on a busy port. The catch only matched `isErrno(err) && err.code === "EADDRINUSE"`, so PortInUseError fell through to the rethrow branch and the ephemeral-port fallback never fired. Scoping the probe to 127.0.0.1 made this reachable. Match on the domain error so a busy preferred port falls back as intended. Co-Authored-By: Claude <[email protected]>
|
Landed as 583829a. Verification:
The final patch scopes preferred-port detection to IPv4 loopback, preserves ephemeral fallback for both domain and errno busy-port errors, and pins the SSH forward listener to loopback. Thanks @wangwllu. |
Summary
Follow-up to #94379 / #94394. Two related fixes on the SSH-tunnel port preflight in
src/infra/ssh-tunnel.ts:1. Scope the preflight to IPv4 loopback.
startSshPortForwardcalledensurePortAvailable(localPort)host-less. On a dual-stack host a bare listen binds the IPv6 wildcard::and misses an IPv4-only occupant, so the preflight reports a busy port as free — the exact address-family flaw fixed for the Chrome CDP caller in #94394. Every other probe in this module is already 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}2. Route
PortInUseErrorto the ephemeral-port fallback. Scoping the probe to127.0.0.1made a pre-existing bug reachable:ensurePortAvailableraises the domainPortInUseError(which has no errnocode) on a busy port, but the catch only matchedisErrno(err) && err.code === "EADDRINUSE". So a busy preferred port fell through to the rethrow branch and the intended ephemeral-port fallback never fired. The guard now also matches the domain error (consistent withhandlePortErrorinports.ts):Closes #94603
Real behavior proof
Behavior or issue addressed: (1) the host-less preflight binds the IPv6 wildcard on dual-stack hosts and fails to detect an IPv4-only occupant of the preferred port, reporting a busy port as free (same root cause as #94379); (2) once the probe actually detects a busy port,
ensurePortAvailablethrowsPortInUseError, which the catch guard failed to recognize, so the ephemeral-port fallback was skipped and a hard error was thrown instead.Real environment tested: macOS (darwin arm64, dual-stack), Node v26, run directly against
node:netand the real guard logic to reproduce the OS-level bind behavior and the catch decision.Exact steps or command run after this patch: (a) bound an occupant IPv4-only on
127.0.0.1:<port>, then compared the old host-less preflight against the new127.0.0.1-scoped preflight via realnet.createServer().listen()calls; (b) fed thePortInUseErrorthat a busy port produces through the old vs new catch guard.Evidence after fix:
Observed result after fix: the scoped preflight detects the IPv4-only occupant, and the busy-port
PortInUseErroris now routed topickEphemeralPort()so the tunnel transparently selects a free port instead of failing. A regression test starts a real IPv4 listener (via a mockedsshchild) and asserts the tunnel falls back to a different port that is then used in the-Lforward spec; it fails against the old broken guard.What was not tested: a full live SSH connection (the spawn path is unchanged by this patch); the fix is confined to the preflight host argument and the catch guard.
Test plan
pnpm vitest run src/infra/ssh-tunnel.test.ts src/infra/ports.test.ts— 19/19 pass (ran 3x, stable)pnpm exec oxlint src/infra/ssh-tunnel.ts src/infra/ssh-tunnel.test.ts— cleanpnpm tsgo:core/pnpm tsgo:core:test— exit 0node:netrepro above (both scoping and fallback paths)🤖 Generated with Claude Code