Skip to content

fix(infra): pin ensurePortAvailable to 127.0.0.1 in startSshPortForward#94602

Closed
xzh-icenter wants to merge 1 commit into
openclaw:mainfrom
xzh-icenter:fix/issue-94596
Closed

fix(infra): pin ensurePortAvailable to 127.0.0.1 in startSshPortForward#94602
xzh-icenter wants to merge 1 commit into
openclaw:mainfrom
xzh-icenter:fix/issue-94596

Conversation

@xzh-icenter

@xzh-icenter xzh-icenter commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #94596

Summary

Verification

# Verify the fix pins to 127.0.0.1
node --input-type=module -e "
import { readFileSync } from 'node:fs';
const src = readFileSync('src/infra/ssh-tunnel.ts', 'utf8');
const lines = src.split('\n');
const idx = lines.findIndex(l => l.includes('ensurePortAvailable(localPort'));
const line = lines[idx].trim();
console.log('Fixed line:', line);
console.log('Has 127.0.0.1:', line.includes('127.0.0.1'));
console.log('Still host-less:', line === 'await ensurePortAvailable(localPort);');
"

# Verify consistency with other probes
node --input-type=module -e "
import { readFileSync } from 'node:fs';
const src = readFileSync('src/infra/ssh-tunnel.ts', 'utf8');
const checks = {
  'pickEphemeralPort uses 127.0.0.1': src.includes('listen(0, \"127.0.0.1\")'),
  'canConnectLocal uses 127.0.0.1': src.includes('connect({ host: \"127.0.0.1\"') || src.includes('host: \"127.0.0.1\"'),
  'forward uses 127.0.0.1': src.includes('127.0.0.1:\${opts.remotePort}'),
  'ensurePortAvailable uses 127.0.0.1': src.includes('ensurePortAvailable(localPort, \"127.0.0.1\")'),
};
for (const [k, v] of Object.entries(checks)) console.log(k + ':', v);
"

Real behavior proof

Behavior addressed: SSH tunnel port preflight on dual-stack hosts now correctly detects IPv4-only occupants, preventing false-free reports.

Environment tested: Windows 11 (x64, dual-stack), Node.js v24.16.0.

Steps run after the patch:

# Bind an IPv4-only server, then test old vs new probe behavior
node -e "
import { createServer } from 'node:net';
const server = createServer();
const port = 19876;
await new Promise((resolve, reject) => {
  server.listen(port, '127.0.0.1', () => { console.log('[setup] IPv4-only server on 127.0.0.1:' + port); resolve(); });
  server.on('error', reject);
});
function tryListenOnPort({ port, host }) {
  return new Promise((resolve, reject) => {
    const s = createServer();
    const opts = host ? { port, host } : { port };
    s.listen(opts, () => { const addr = s.address(); s.close(() => resolve(addr)); });
    s.on('error', reject);
  });
}
console.log('\n=== host-less probe (old code) ===');
try { await tryListenOnPort({ port }); console.log('FREE (bug - misses IPv4-only occupant)'); }
catch (err) { console.log('BUSY (' + err.code + ')'); }
console.log('\n=== scoped probe 127.0.0.1 (new code) ===');
try { await tryListenOnPort({ port, host: '127.0.0.1' }); console.log('FREE (unexpected)'); }
catch (err) { console.log('BUSY (' + err.code + ') - fix works!'); }
console.log('\n=== IPv6 probe ===');
try { await tryListenOnPort({ port, host: '::1' }); console.log('FREE on IPv6 (expected - why old code misses)'); }
catch (err) { console.log('BUSY on IPv6 (' + err.code + ')'); }
server.close();
"

Evidence after fix (console output):

image
[setup] IPv4-only server on 127.0.0.1:19876

=== host-less probe (old code) ===
FREE (bug - misses IPv4-only occupant)

=== scoped probe 127.0.0.1 (new code) ===
BUSY (EADDRINUSE) - fix works!

=== IPv6 probe ===
FREE on IPv6 (expected - why old code misses)

Observed result: The host-less probe (old code) reports the port as FREE despite an IPv4-only listener — the exact bug. The scoped probe with 127.0.0.1 (new code) correctly detects EADDRINUSE. The IPv6 probe confirms the root cause: IPv6 wildcard bind does not conflict with IPv4-only listeners on dual-stack hosts.

Not tested: Live SSH tunnel (requires a real SSH server). The fix is a one-line argument addition following the exact pattern from #94394.

Every other probe in ssh-tunnel.ts is pinned to IPv4 loopback
(pickEphemeralPort, canConnectLocal, the -L forward itself),
but ensurePortAvailable was called host-less. On a dual-stack host
the host-less probe binds the IPv6 wildcard :: and misses an
IPv4-only occupant, reporting a busy port as free.

Pass "127.0.0.1" to match the interface this caller actually owns,
consistent with the fix in openclaw#94394.

Fixes openclaw#94596
@openclaw-barnacle openclaw-barnacle Bot added size: XS proof: supplied External PR includes structured after-fix real behavior proof. labels Jun 18, 2026
@clawsweeper

clawsweeper Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded by #94607: the reported SSH tunnel bug is real, but this branch only applies the host-scoped probe while the open sibling also handles the PortInUseError fallback and adds focused regression coverage.

Root-cause cluster
Relationship: superseded
Canonical: #94607
Summary: This PR is a narrow duplicate candidate for the SSH tunnel port-preflight bug, superseded by an open sibling that includes the same production fix plus the missing fallback behavior and tests.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Land one canonical SSH tunnel fix that scopes the preflight to 127.0.0.1, handles PortInUseError in the fallback guard, and carries focused regression coverage; #94607 is the strongest current landing candidate.

So I’m closing this here and keeping the remaining discussion on #94607.

Review details

Best possible solution:

Land one canonical SSH tunnel fix that scopes the preflight to 127.0.0.1, handles PortInUseError in the fallback guard, and carries focused regression coverage; #94607 is the strongest current landing candidate.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows the patched call would surface PortInUseError from ensurePortAvailable, while this branch leaves the SSH tunnel fallback guard recognizing only errno EADDRINUSE; the PR body also supplies useful raw node:net address-family proof.

Is this the best way to solve the issue?

No. Passing 127.0.0.1 is necessary, but the best fix also catches PortInUseError and adds SSH tunnel regression coverage, which the fuller sibling PR already does.

Security review:

Security review cleared: The diff only narrows a local loopback port-probe argument and does not touch dependencies, workflows, secrets, permissions, downloaded artifacts, or other code-execution surfaces.

AGENTS.md: found and applied where relevant.

What I checked:

  • Repository policy applied: Root AGENTS.md was read fully and its PR review guidance requires current source, caller/callee, sibling fix, duplicate, proof, and best-fix checks before verdict. (AGENTS.md:1, 712e69dd7479)
  • Current main still has the SSH hostless probe: On current main, startSshPortForward still calls ensurePortAvailable(localPort) while the same function allocates, waits, and forwards on 127.0.0.1, so the underlying issue remains unimplemented on main. (src/infra/ssh-tunnel.ts:122, 712e69dd7479)
  • Port helper throws a domain error: ensurePortAvailable(port, host?) forwards the optional host to tryListenOnPort, but wraps EADDRINUSE as PortInUseError, which this branch does not catch in the SSH fallback guard. (src/infra/ports.ts:40, 712e69dd7479)
  • This branch is the narrow one-line candidate: The PR diff changes only the host argument at the SSH preflight call and leaves the fallback guard unchanged. (src/infra/ssh-tunnel.ts:122, 5e7d8fff11e5)
  • Canonical sibling includes the missing fallback and tests: The open sibling PR changes the same call, imports PortInUseError, routes that domain error into the fallback branch, and adds focused SSH tunnel tests for the scoped preflight and busy preferred-port fallback. (src/infra/ssh-tunnel.ts:122, 04810b917861)
  • Canonical sibling is viable but still needs normal check handling: Live PR metadata for fix(ssh): scope tunnel port preflight to loopback (#94603) #94607 shows it is open, mergeable, proof-labeled, and ready for maintainer look; its current check rollup is unstable due to unrelated failing checks noted in discussion. (04810b917861)

Likely related people:

  • steipete: git show identifies Peter Steinberger as author of the commit that introduced src/infra/ssh-tunnel.ts, and live PR metadata shows steipete merged the related Browser CDP scoped-probe fix. (role: introduced SSH tunnel module and related merger; confidence: high; commits: d258c68ca1ab, 4ec9d4a2b511; files: src/infra/ssh-tunnel.ts, src/commands/gateway-status/probe-run.ts, src/infra/ports.ts)
  • Pandah97: Authored the merged PR that added the optional host argument pattern to ensurePortAvailable and fixed the Browser CDP caller surface that this SSH tunnel fix follows. (role: recent related helper author; confidence: high; commits: 4ec9d4a2b511; files: src/infra/ports.ts, src/infra/ports.test.ts, extensions/browser/src/browser/chrome.ts)
  • vincentkoc: Live discussion shows vincentkoc twice opted this exact head into automerge, and local shallow blame attributes the current SSH preflight block to a recent broad boundary commit; this is useful routing context but weak code provenance. (role: recent automerge sponsor and shallow current-line provenance; confidence: low; commits: 36bfe77db19c; files: src/infra/ssh-tunnel.ts, src/infra/ports.ts)

Codex review notes: model internal, reasoning high; reviewed against 712e69dd7479.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jun 18, 2026
@xzh-icenter

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 18, 2026
@vincentkoc

Copy link
Copy Markdown
Member

/clownfish automerge

@vincentkoc vincentkoc added the clownfish:automerge Maintainer opted this Clownfish PR into bounded ClawSweeper-reviewed automerge label Jun 19, 2026
@vincentkoc

Copy link
Copy Markdown
Member

Clownfish is on the reef for this PR. 🐠

I tagged clownfish:automerge and sent ClawSweeper over this exact head. If the sweep finds rough coral, failing checks, or needs-human, I will take another bounded repair lap and ask for a fresh review.

A maintainer can call /clownfish stop any time and I will drift this back to human review.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jun 19, 2026
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 19, 2026
@vincentkoc

Copy link
Copy Markdown
Member

/clownfish automerge

@openclaw-clownfish openclaw-clownfish Bot added the clawsweeper:automerge Maintainer opted this PR into bounded ClawSweeper-reviewed automerge label Jun 19, 2026
@openclaw-clownfish

Copy link
Copy Markdown
Contributor

Clownfish is on the reef for this PR. 🐠

I tagged clownfish:automerge and sent ClawSweeper over this exact head. If the sweep finds rough coral, failing checks, or needs-human, I will take another bounded repair lap and ask for a fresh review.

A maintainer can call /clownfish stop any time and I will drift this back to human review.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 🚀 automerge armed This PR is in ClawSweeper's automerge lane. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. clawsweeper:human-review Needs maintainer review before ClawSweeper can continue and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jun 19, 2026
@clawsweeper

clawsweeper Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🦞✅
ClawSweeper is pausing this repair loop for human review.

Source: clawsweeper[bot]
Reason: structured ClawSweeper verdict: needs-human (sha=5e7d8fff11e510fade251d0430ae815806e424a8)

Why human review is needed:
ClawSweeper found a blocker that should be resolved or accepted by a maintainer before the repair or automerge loop continues.

What the maintainer can do as a next step:
If the maintainer accepts the current risk and wants ClawSweeper to continue merge gates, comment @clawsweeper approve. If more work is needed, resolve the blocker first, then comment @clawsweeper automerge to re-review and continue. If automation should stay paused, leave clawsweeper:human-review in place or comment @clawsweeper stop.

I added clawsweeper:human-review and left the final call with a maintainer.

@xzh-icenter

Copy link
Copy Markdown
Contributor Author

Hi @vincentkoc — I see you've triggered clownfish automerge twice on this PR, but ClawSweeper is still in needs-human state. Is there anything I should do on my end, or is this waiting on a maintainer decision?

@steipete

Copy link
Copy Markdown
Contributor

Superseded by #94607, landed as 583829a.

The landed fix includes this loopback-scoped preflight and also handles the domain PortInUseError fallback plus explicitly pins the SSH listener to loopback. Thanks @xzh-xydt for the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clawsweeper:automerge Maintainer opted this PR into bounded ClawSweeper-reviewed automerge clawsweeper:human-review Needs maintainer review before ClawSweeper can continue clownfish:automerge Maintainer opted this Clownfish PR into bounded ClawSweeper-reviewed automerge merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. P2 Normal backlog priority with limited blast radius. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XS status: 🚀 automerge armed This PR is in ClawSweeper's automerge lane.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ensurePortAvailable in startSshPortForward misses IPv4-only occupants (same address-family flaw as #94379)

3 participants