fix(tunnel): bind userspace tunnel listener on an ephemeral port to avoid collisions - #833
Open
danielpaulus wants to merge 2 commits into
Open
fix(tunnel): bind userspace tunnel listener on an ephemeral port to avoid collisions#833danielpaulus wants to merge 2 commits into
danielpaulus wants to merge 2 commits into
Conversation
…void collisions
The userspace TUN local listener port was pre-computed as basePort+portOffset
in the TunnelManager and only the tunnel-info HTTP port was guaranteed free. The
derived TUN listener port had no free-port guarantee, so two per-device tunnel
agents on one host could pick the same port and the second agent could never
bind ("bind: address already in use"), leaving its tunnel permanently down.
Bind the userspace TUN listener on an OS-assigned ephemeral port (localhost:0),
read the actual bound port back via listener.Addr(), and advertise that real
port on the returned Tunnel (UserspaceTUNPort) so the tunnel-info API serves a
reachable port. An explicit non-zero ifacePort is still honored for backward
compatibility. The now-dead basePort/portOffset derivation is removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…port TestTunnelAgentMixed asserted the old contract that each agent's userspace listener port was derived from its tunnel-info port (perPort+1). Now the listener binds an OS-assigned ephemeral port and advertises the actual bound port, so assert that each advertised port is a valid port distinct from the agent's tunnel-info port, and keep the cross-agent no-collision check. The test's real intent (two agents coexist, isolated, each with its own working tunnel) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
The userspace tunnel's local listener port collides when two per-device tunnel
agents run on one host. This started failing in real-device CI after a 2nd
device (iOS 26.5) was added to the office01 macOS runner.
TunnelManagerpre-computed the userspace TUN listener port asd.UserspaceTUNPort = m.basePort + m.portOffset(portOffset starts at 1), andconnectToUserspaceTunnelLockdownbound it vianet.Listen("tcp", "localhost:<port>").Only the agent's tunnel-info HTTP port is guaranteed free (each per-device
agent is started with a free
--tunnel-info-port). The derived userspaceTUN port had no free-port guarantee, so with two concurrent per-device
agents on one host it collided → the second agent could never bind and its
tunnel never came up:
TestTunnelAgent/userspace/<udid>on the second device then timed out after 90s.The fix
Bind the userspace TUN listener on an OS-assigned ephemeral port and
advertise the actual chosen port:
connectToUserspaceTunnelLockdownpassesifacePortthrough tonet.Listen; withifacePort == 0the OS picks a free port. The actual boundport is read back via
listener.Addr().(*net.TCPAddr).Portand set on thereturned
Tunnel.UserspaceTUNPort(withUserspaceTUN = true), so thetunnel-info API (
/tunnel/{udid},/tunnels) serves a real, reachable port.An explicit non-zero
ifacePortis still honored for backward compatibility.TunnelManagerno longer pre-assignsbasePort + portOffset; the now-deadbasePort/portOffsetderivation is removed. The clobbering overwrite inmanualPairingTunnelStart.StartTunnel(
tun.UserspaceTUNPort = device.UserspaceTUNPort) is removed so it can'treset the real port back to a stale value.
The tunnel-info HTTP API port is untouched — only the userspace TUN listener
port becomes ephemeral. The two ports were previously entangled via
basePort(which defaulted to
ios.HttpApiPort()); they are now fully independent.Options considered
actual one. Zero collision surface, no retry loop, no shared coordination
state, and mirrors the existing REST API ephemeral-port approach. The
advertised port is always correct because it's read from the bound listener.
multi-agent hosts would still collide. Rejected.
loop and still races two agents onto the same next candidate; strictly worse
than letting the OS pick. Rejected.
coordinating a base offset across independently-launched processes on one
host — exactly the coordination the ephemeral bind avoids. Rejected.
Verification
go build ./...,go vet ./...,gofmt -lclean;go test ./...passes.🤖 Generated with Claude Code