fix(ssh-tunnel): bind port preflight to IPv4 loopback to avoid dual-stack false-negative#94927
Conversation
…tack false-negative - Pass "127.0.0.1" to ensurePortAvailable in startSshPortForward to match the IPv4-loopback-scoped forward target and the rest of the module's bind/connect calls - Without this, a hostless probe binds :: and misses IPv4-only occupants, reporting a busy port as free (same root cause as openclaw#94379 / openclaw#94471) Fixes openclaw#94596
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Close as superseded: this PR has the right loopback-probe direction, but it leaves the busy-port fallback incomplete and is covered by an open stronger candidate with fallback handling, tests, and sufficient proof. Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Canonical path: Land #94607 as the canonical SSH tunnel fix, then close the duplicate SSH-tunnel candidate PRs and the open canonical issue. So I’m closing this here and keeping the remaining discussion on #94607. Review detailsBest possible solution: Land #94607 as the canonical SSH tunnel fix, then close the duplicate SSH-tunnel candidate PRs and the open canonical issue. Do we have a high-confidence way to reproduce the issue? Yes, source-level: current main probes the preferred SSH tunnel port without a host while the tunnel allocates, waits, and forwards on Is this the best way to solve the issue? No. Passing Security review: Security review cleared: The diff only narrows a local loopback port-probe argument and does not touch dependencies, workflows, secrets, permissions, or downloaded code. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 27f702d68fab. |
|
Closed as superseded by #94607 per ClawSweeper review — the canonical replacement PR has the right loopback-probe direction with complete fallback handling, tests, and sufficient proof. |
Summary
In
startSshPortForward, the port availability preflight callsensurePortAvailable(localPort)without specifying a host. On dual-stack hosts this binds the IPv6 wildcard::, which misses IPv4-only occupants and reports a busy port as free — the same address-family false-negative that was fixed for the Browser CDP path in #94394 and forisPortBusyin #94471.The rest of
ssh-tunnel.tsis consistently IPv4-loopback scoped:pickEphemeralPortbinds127.0.0.1canConnectLocalconnects to127.0.0.1-Lforward targets127.0.0.1The fix is a single-line change: pass
"127.0.0.1"as the host toensurePortAvailable, matching the existing caller-scoped probe pattern from #94394.Fixes #94596
Real behavior proof
Behavior addressed: On dual-stack hosts,
startSshPortForwardreports a busy IPv4-only port as free becauseensurePortAvailable(localPort)without a host binds::(IPv6 wildcard) and misses IPv4-only occupants.Real environment tested: Linux 6.8.0-124-generic, Node.js v22.x, OpenClaw main @ 49f10a1
Exact steps or command run after this patch:
cd ~/workspace/openclaw && npx vitest run src/infra/ssh-tunnel.test.ts src/infra/ports.test.tsAfter-fix evidence: All 17 tests pass (12 ssh-tunnel + 5 ports). Source-level verification:
ensurePortAvailable(localPort, "127.0.0.1")binds the same interface as the SSH forward target,pickEphemeralPort(line 68:listen(0, "127.0.0.1")), andcanConnectLocal(line 83:connect({ host: "127.0.0.1" })), eliminating the dual-stack address-family mismatch.Observed result after the fix: All 17 existing tests pass. The fix is source-verified: the same helper
ensurePortAvailable(port, host?)that was extended with an optional host parameter in #94394 is now called with"127.0.0.1"in the SSH tunnel path, consistent with every other bind/connect call in the module (pickEphemeralPort→listen(0, "127.0.0.1"),canConnectLocal→connect({ host: "127.0.0.1" }),ssh -L ${localPort}:127.0.0.1:${remotePort}).What was not tested: A live dual-stack reproduction was not performed — the fix is source-level verified. The false-negative requires an IPv4-only port occupant on a dual-stack host, which is the reporter's exact scenario but not reproducible in this CI/development environment.
Tests and validation
Unit tests
Type check / Lint
Risk checklist
ensurePortAvailablealready accepts an optionalhostparameter; passing"127.0.0.1"is a stricter, more correct probe that cannot introduce new false-positivesRisk assessment: Low. The fix is a one-line change that makes the port preflight consistent with every other network operation in the same module.
ensurePortAvailablealready supports the optional host parameter (added in #94394). Passing"127.0.0.1"means the probe binds the same interface the SSH forward will use, eliminating the dual-stack address-family mismatch. The existing EADDRINUSE fallback topickEphemeralPortis preserved unchanged.Current review state