Skip to content

fix(server): SSH device-host tunnels no longer inherit ControlMaster - #17440

Open
kcazin wants to merge 1 commit into
pingdotgg:mainfrom
kcazin:fix/device-host-ssh-controlmaster
Open

kcazin wants to merge 1 commit into
pingdotgg:mainfrom
kcazin:fix/device-host-ssh-controlmaster

Conversation

@kcazin

@kcazin kcazin commented Oct 9, 2026 •

Copy link
Copy Markdown

Problem

With ControlMaster auto / ControlPersist in ~/.ssh/config, the SSH device-host tunnel spawned by SshDeviceHost.connectOnce becomes a mux client. It hands its -L forwards to the control master and exits 0 at once. The reconnect loop then treats each cycle as a healthy reconnect, about every 1.5 s, and every pass leaks one or two more loopback listeners into the master (the hub forward, plus the daemon forward when agent tools are enabled). Left running, this fills the whole IPv4 ephemeral range (32768–60999), and every outbound connect() on the machine fails with EADDRNOTAVAIL, not just T3's. Full report, trace evidence and environment: #17437.

Change

The device-host tunnel spawn now passes -o ControlMaster=no -o ControlPath=none -o ControlPersist=no, the same options startSshTunnel in packages/ssh/src/tunnel.ts has used since #4347. The tunnel then owns its own connection and its forwards, and they close when the tunnel exits.

The options are added to the long-lived -N spawn only, not to baseSshArgs. Short bootstrap commands keep multiplexing if the user has configured it.

Scope and approval

Fixes #17437, which maintainers have triaged (bug, plus the maintainer-bot analysis on the issue). This is that analysis's first suggested direction. It is a very small fix for an obvious bug: the device-host tunnel (#10856) missed the multiplexing opt-out that #4347 added to the environment tunnel for the same failure (#3533).

Out of scope, for separate PRs if wanted:

  • Backing off when a tunnel exits soon after ready.
  • A shared "managed forward" argument helper.

Verification

Focused test. SshDeviceHost.test.ts now records the args of every spawned ssh command. It asserts that each tunnel spawn (-N) passes all three options as -o values, and that bootstrap commands pass none of them.

  • Before the fix: pnpm exec vp test run apps/server/src/device/SshDeviceHost.test.ts fails with AssertionError: expected -1 to be greater than 0 at the ControlMaster=no lookup.
  • With the fix: Test Files 1 passed (1) · Tests 1 passed (1).
  • vp check on both files: no lint or format issues. vp run --filter t3 typecheck: 0 errors.

Live OpenSSH check. OpenSSH 10.5p1 on Linux, against a macOS device host. A throwaway -F config forces ControlMaster auto / ControlPersist 60 ahead of the user's own config. A bootstrap-style ssh <host> true first creates the master, then each tunnel argv runs against it:

== BEFORE: current tunnel argv (no Control* overrides)
tunnel ssh exit=0 after 0.11 s
listener 127.0.0.1:47111 owned by: pid=2545762 (master pid=2545762)
master fds: 6                                  # was 5 before the tunnel ran

== AFTER: patched tunnel argv (ControlMaster=no ControlPath=none ControlPersist=no)
tunnel ssh pid=2545780 still running after 4 s
listener 127.0.0.1:47112 owned by: pid=2545780
master fds: 6                                  # unchanged
after killing tunnel pid=2545780: port 47112 freed

Not checked: I did not run a full dev server end to end against a device host, and I did not test on macOS or Windows server hosts. The change only adds ssh -o options, and they are the same ones the environment tunnel already ships on every platform.

Model and harness: Claude Opus 5.5 (claude-opus-5-5) in Claude Code, via t3 triage.

🤖 Generated with Claude Code

The device-host tunnel inherited ControlMaster/ControlPersist from the
user's ssh config, so it became a mux client that handed its forwards
to the control master and exited. The reconnect loop then leaked two
listening ports into the master every ~1.5 s until the ephemeral range
ran out. Opt the tunnel out of multiplexing, as the environment tunnel
already does (pingdotgg#4347).

Fixes pingdotgg#17437

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Oct 9, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at a05c419

Macroscope's review found this PR approvable — This is a narrowly scoped SSH tunnel bug fix that prevents device-host forwards from being handed to a persistent ControlMaster, while leaving bootstrap commands unchanged. The production change is isolated and accompanied by focused argument-level tests.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 843c60bb-6869-4c98-8b77-7640009ab935

📥 Commits

Reviewing files that changed from the base of the PR and between 43f8a8d and a05c419.


📒 Files selected for processing (2)
  • apps/server/src/device/SshDeviceHost.test.ts
  • apps/server/src/device/SshDeviceHost.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.



📝 Walkthrough

Walkthrough

The SSH device-host tunnel command now disables connection multiplexing and persistence. Tests verify these options are used for tunnel commands and omitted from ordinary SSH commands.

Changes

SSH device-host tunnel

Layer / File(s) Summary
Tunnel options and regression checks
apps/server/src/device/SshDeviceHost.ts, apps/server/src/device/SshDeviceHost.test.ts
The tunnel command adds ControlMaster=no, ControlPath=none, and ControlPersist=no. Tests verify these options appear on tunnel commands and not on ordinary SSH commands.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: juliusmarminge


Merge Risk

Merge Risk: ⚪ Minimal · up to a05c4

The change limits multiplexing options to device-host tunnels and checks that ordinary SSH commands remain unaffected. No merge-blocking issue is identified.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to a05c4

The change restores direct ownership of tunnel connections and listeners without expanding SSH privileges or network exposure. Ordinary SSH commands retain their existing behavior. No material security risk introduced or worsened by this change was identified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed authority remains bounded to the configured SSH target and its existing account access. Forwarding exposure remains local loopback access to the same remote hub and optional daemon; the diff adds no credentials, privileged remote commands, or broader listener bindings.

Trust Boundaries and Controls

  • observed — The tunnel retains its configured target, identity arguments, batch-mode setting, forwarding-failure control, and keepalives. The new options prevent shared-control-socket reuse for this invocation only; ordinary SSH command argument composition remains separate.



🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed For directly linked issue [#17437], SshDeviceHost.connectOnce adds ControlMaster=no, ControlPath=none, and ControlPersist=no to the long-lived -N tunnel spawn. The options are not added to `…
Out of Scope Changes check Passed The reviewed changes are limited to the device-host tunnel arguments and focused test coverage for issue [#17437]. The tunnel options support connection ownership and the test verifies the required se…
Title check Passed The title clearly and concisely describes the main change: preventing SSH device-host tunnels from inheriting ControlMaster settings.
Description check Passed The description includes complete Problem, Change, Scope and approval, and Verification sections. It explains the failure mode, implementation scope, linked issue, focused tests, live validation, limi…


✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

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

Labels

size:XS 0-9 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSH device-host tunnel inherits ControlMaster from ~/.ssh/config and exhausts the ephemeral port range

1 participant