tstest, cmd/tta: add Tailscale SSH end-to-end VM test

Add TestTailscaleSSH to tstest/natlab/vmtest, exercising the Tailscale
SSH server (tailscale up --ssh, not a system sshd) end to end: an
Ubuntu client node SSHes over the tailnet into an Ubuntu server node
as both root and a non-root user, and into a gokrazy node.

The gokrazy sessions exercise the gokrazy special cases in the SSH
code: util/osuser hard-codes the login shell to serial-busybox ash and
synthesizes a root user when lookup fails (so any username works and
becomes root, unlike Ubuntu where nonexistent users are rejected), and
the incubator's findSU refuses su on gokrazy, handling sessions
in-process.

To support this, testcontrol gains an SSHPolicy field that's sent in
MapResponses along with the CapabilitySSH node capability, tta's /up
handler accepts an ssh=true parameter, and vmtest gains a
TailscaleSSH node option that wires the two together with a
permissive any-principal policy.

Updates tailscale/corp#44813
Updates #13038

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I3f6b9c41a72e05d8c94dd7f6ab1937cf24b81c92
This commit is contained in:
Brad Fitzpatrick
2026-07-13 15:04:51 -07:00
committed by Brad Fitzpatrick
parent 2506ede862
commit c436dec43c
4 changed files with 190 additions and 0 deletions
@@ -66,6 +66,11 @@ type Server struct {
// grants rules.
PeerRelayGrants bool
// SSHPolicy, if non-nil, is sent to every node in MapResponses.
// Each node also gets [tailcfg.CapabilitySSH] added to its capability
// map, permitting "tailscale up --ssh".
SSHPolicy *tailcfg.SSHPolicy
// AllNodesSameUser, if true, makes all created nodes
// belong to the same user.
AllNodesSameUser bool
@@ -1612,10 +1617,14 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
dns = s.DNSConfig.Clone()
}
magicDNSDomain := s.MagicDNSDomain
sshPolicy := s.SSHPolicy.Clone()
s.mu.Unlock()
node.CapMap = nodeCapMap
node.Capabilities = append(node.Capabilities, tailcfg.NodeAttrDisableUPnP)
if sshPolicy != nil {
mak.Set(&node.CapMap, tailcfg.CapabilitySSH, nil)
}
t := time.Date(2020, 8, 3, 0, 0, 0, 1, time.UTC)
if dns != nil && magicDNSDomain != "" {
@@ -1629,6 +1638,7 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
CollectServices: cmp.Or(s.CollectServices, opt.True),
PacketFilter: packetFilterWithIngress(s.PeerRelayGrants),
DNSConfig: dns,
SSHPolicy: sshPolicy,
ControlTime: &t,
}