3 Commits
Author SHA1 Message Date
James Tucker 123232260e ssh/tailssh: retain the latest session window size
Drain any stale buffered size before sending the latest window update. The adapter is the only sender to a one-slot channel, so the final resize is retained even if the consumer races with the drain.

Cover late and slow readers over a real SSH session with deterministic synctest synchronization. Keep the final two resizes unread to exercise replacement of the buffered size.

Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
Change-Id: I8c077114a6d4765c4208d42a22f384c82f73f75c
2026-10-26 12:11:07 -07:00
James Tucker a1e51f4942 tsnet: authorize ListenSSH peers, fix PTY emulation and session queue bound
ListenSSH now documents its session trust model and takes an
authorization callback. A session is accepted from any peer that the
tailnet ACL's TCP rules allow and that resolves with WhoIs. The server
performs no SSH client authentication, does not map the peer to a local
user, and does not record the session. The application is responsible
for what each session does. Set Server.SSHAuthCallback to reject
unwanted sessions before they reach Accept. A rejected session is
refused at open with exit status 1 and is never yielded to Accept. The
callback is plumbed through LocalBackend.ListenSSH and HookListenSSH.
With no callback, behavior is unchanged.

The listener's SSH server runs session channels only, with no port
forwarding. Closing the listener closes the accepted connections.

- DisablePTYEmulation runs on every session, so a PTY session passes
  data through unchanged instead of translating "\n" to "\r\n". The
  translation corrupted binary data read through the net.Conn
  interface. Window changes are drained for every session, even when
  the application never calls Pty, because gliderssh writes window
  changes from the session request loop and an undrained channel
  blocks the session's requests. Pty drops window changes when the
  application is not reading them.

- The handshake is bounded by TS_SSH_LOGIN_GRACE_TIME, default 120
  seconds, the same as sshd's LoginGraceTime.

- TS_SSH_IDLE_TIMEOUT bounds connections with no SSH traffic. It is 0
  by default, and zero applies no bound, so quiet but live sessions
  are not closed by default. Both timeout knobs accept a Go duration
  such as "90s" or a bare number of seconds.

- Accepted sessions wait for Accept in a bounded queue. When the queue
  is full, further sessions are refused, so an application that stops
  calling Accept parks only a bounded number of sessions.

Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-25 11:31:39 -07:00
Brad Fitzpatrick 3e34e721e8 tsnet: add opt-in SSH support (Server.ListenSSH)
This adds tsnet.Server.ListenSSH which, if the SSH feature is linked,
returns a net.Listener whose Accept yields *tailssh.Session values (as
net.Conn). This lets tsnet apps accept incoming SSH connections to
implement custom TUI applications.

Basic apps can use net.Conn directly (Read/Write/Close). Rich apps
import ssh/tailssh and type-assert for peer identity, PTY, signals,
etc. If feature/ssh isn't imported, ListenSSH returns an error.

Includes a demo guess-the-number game in tsnet/example/ssh-game.

Updates tailscale/corp#37839

Change-Id: I4e7c3c96afb030cdf4da8f2d8b2253820628129a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-05-30 14:17:50 -07:00