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>
This commit is contained in:
Brad Fitzpatrick
2026-05-30 14:17:50 -07:00
committed by Brad Fitzpatrick
parent c9333854fb
commit 3e34e721e8
7 changed files with 618 additions and 0 deletions
+18
View File
@@ -144,6 +144,24 @@ func RegisterNewSSHServer(fn newSSHServerFunc) {
newSSHServer = fn
}
// HookListenSSH is set by the ssh/tailssh package (via feature/ssh) to provide
// an implementation of ListenSSH for use by tsnet.
var HookListenSSH feature.Hook[func(net.Listener, *LocalBackend, logger.Logf) (net.Listener, error)]
// ListenSSH wraps the given listener with an SSH server that authenticates
// connections using Tailscale peer identity. The returned listener's Accept
// yields net.Conn values that are *tailssh.Session.
//
// If the ssh/tailssh package has not been linked (e.g. via
// _ "tailscale.com/feature/ssh"), ListenSSH returns an error.
func (b *LocalBackend) ListenSSH(ln net.Listener, logf logger.Logf) (net.Listener, error) {
fn, ok := HookListenSSH.GetOk()
if !ok {
return nil, errors.New("SSH support not available; import _ \"tailscale.com/feature/ssh\"")
}
return fn(ln, b, logf)
}
// watchSession represents a WatchNotifications channel,
// an [ipnauth.Actor] that owns it (e.g., a connected GUI/CLI),
// and sessionID as required to close targeted buses.