Skip to content

sshd: listen on the tsnet interface too#9

Merged
ananthb merged 1 commit into
mainfrom
fix/ssh-listen-on-tsnet
Jul 12, 2026
Merged

sshd: listen on the tsnet interface too#9
ananthb merged 1 commit into
mainfrom
fix/ssh-listen-on-tsnet

Conversation

@ananthb

@ananthb ananthb commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

net.Listen(\":22\") binds only the kernel network stack. tsnet is a userspace WireGuard implementation with its own stack; connections to the tailnet IP land there, not in the kernel. Result: the pivoted sshd from #6 wasn't reachable from tailnet clients — the port was open in the wrong place.

  • StartSSHServer now takes a TailnetListener (interface satisfied by *tsnet.Server). When non-nil, :22 is also bound via tsnet.Server.Listen and served by the same ServerConfig and accept loop as the kernel listener.
  • tsnetauth.PostPivot splits into NewPostPivotServer (creates + Ups the server, returns it) and ServeSSH (the unchanged ListenSSH path). PostPivot kept as a one-call wrapper for callers that don't need the server.
  • postpivot.Run holds the *tsnet.Server so it can pass it to StartSSHServer as the TailnetListener.

Not calling ServeSSH from Run in this PR — tsnet.ListenSSH(\":22\") and our sshd's tsnet.Listen(\":22\") would fight for the same tailnet port. Bringing back Tailscale-native SSH is a separate PR (probably on a different port, or as a mode switch).

Test plan

  • go test ./internal/postpivot/... ./internal/tsnetauth/... ./internal/config/... ./internal/cli/... on Linux (endeavour): all pass.
  • New TestStartSSHServerAddsTailnetListener verifies StartSSHServer binds and cleans up when handed a TailnetListener implementation.
  • nix build .#xmorph clean on endeavour.
  • Real hardware: pivot enterprise again, confirm ssh root@<tailnet-ip> works from a mac App Store Tailscale client.

net.Listen(":22") only binds the kernel network stack. tsnet is a
userspace WireGuard implementation with its own stack — connections to
the tailnet IP land there, not in the kernel, so the pivoted sshd was
unreachable from tailnet.

StartSSHServer now takes a TailnetListener (interface satisfied by
*tsnet.Server). When non-nil, it also binds :22 via tsnet.Listen,
alongside the kernel listener. Both listeners share the same
ServerConfig and accept loop.

tsnetauth splits into NewPostPivotServer (creates + Ups) and ServeSSH
(the ListenSSH path, unchanged). postpivot.Run holds the *tsnet.Server
so it can hand it to StartSSHServer as the TailnetListener.
@ananthb
ananthb merged commit ff4609a into main Jul 12, 2026
2 checks passed
@ananthb
ananthb deleted the fix/ssh-listen-on-tsnet branch July 12, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant