From 1b87e025e92ac25ec9083c6fb8c2b226a8c2dd98 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 15 Feb 2022 11:59:21 -0800 Subject: [PATCH] ssh/tailssh: move SSH code from wgengine/netstack to this new package Still largely incomplete, but in a better home now. Updates #3802 Change-Id: I46c5ffdeb12e306879af801b06266839157bc624 Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/depaware.txt | 7 +- ssh/tailssh/tailssh.go | 156 ++++++++++++++++++++++++++++++++++ wgengine/netstack/netstack.go | 23 +++-- wgengine/netstack/ssh.go | 135 +---------------------------- 4 files changed, 173 insertions(+), 148 deletions(-) create mode 100644 ssh/tailssh/tailssh.go diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 5ef73570c..d6e6e96db 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -61,8 +61,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router - L 💣 github.com/creack/pty from tailscale.com/wgengine/netstack - L github.com/gliderlabs/ssh from tailscale.com/wgengine/netstack + L 💣 github.com/creack/pty from tailscale.com/ssh/tailssh + L github.com/gliderlabs/ssh from tailscale.com/ssh/tailssh W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+ W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns @@ -219,6 +219,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/portlist from tailscale.com/ipn/ipnlocal tailscale.com/safesocket from tailscale.com/client/tailscale+ tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+ + L 💣 tailscale.com/ssh/tailssh from tailscale.com/wgengine/netstack 💣 tailscale.com/syncs from tailscale.com/control/controlknobs+ tailscale.com/tailcfg from tailscale.com/client/tailscale+ W tailscale.com/tsconst from tailscale.com/net/interfaces @@ -261,7 +262,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/wgengine/filter from tailscale.com/control/controlclient+ tailscale.com/wgengine/magicsock from tailscale.com/wgengine+ tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+ - 💣 tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled + tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+ tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+ tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal diff --git a/ssh/tailssh/tailssh.go b/ssh/tailssh/tailssh.go new file mode 100644 index 000000000..de1dcce5e --- /dev/null +++ b/ssh/tailssh/tailssh.go @@ -0,0 +1,156 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux +// +build linux + +// Package tailssh is an SSH server integrated into Tailscale. +package tailssh + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "os/exec" + "syscall" + "unsafe" + + "github.com/creack/pty" + "github.com/gliderlabs/ssh" + gossh "golang.org/x/crypto/ssh" + "inet.af/netaddr" + "tailscale.com/envknob" + "tailscale.com/ipn/ipnlocal" + "tailscale.com/net/tsaddr" + "tailscale.com/types/logger" +) + +// TODO(bradfitz): this is all very temporary as code is temporarily +// being moved around; it will be restructured and documented in +// following commits. + +// Handle handles an SSH connection from c. +func Handle(logf logger.Logf, lb *ipnlocal.LocalBackend, c net.Conn) error { + hostKey, err := ioutil.ReadFile("/etc/ssh/ssh_host_ed25519_key") + if err != nil { + return err + } + signer, err := gossh.ParsePrivateKey(hostKey) + if err != nil { + return err + } + sshd := &server{lb, logf} + srv := &ssh.Server{ + Handler: sshd.handleSSH, + RequestHandlers: map[string]ssh.RequestHandler{}, + SubsystemHandlers: map[string]ssh.SubsystemHandler{}, + ChannelHandlers: map[string]ssh.ChannelHandler{}, + } + for k, v := range ssh.DefaultRequestHandlers { + srv.RequestHandlers[k] = v + } + for k, v := range ssh.DefaultChannelHandlers { + srv.ChannelHandlers[k] = v + } + for k, v := range ssh.DefaultSubsystemHandlers { + srv.SubsystemHandlers[k] = v + } + srv.AddHostKey(signer) + + srv.HandleConn(c) + return nil +} + +type server struct { + lb *ipnlocal.LocalBackend + logf logger.Logf +} + +func (srv *server) handleSSH(s ssh.Session) { + lb := srv.lb + logf := srv.logf + + user := s.User() + addr := s.RemoteAddr() + logf("Handling SSH from %v for user %v", addr, user) + ta, ok := addr.(*net.TCPAddr) + if !ok { + logf("tsshd: rejecting non-TCP addr %T %v", addr, addr) + s.Exit(1) + return + } + tanetaddr, ok := netaddr.FromStdIP(ta.IP) + if !ok { + logf("tsshd: rejecting unparseable addr %v", ta.IP) + s.Exit(1) + return + } + if !tsaddr.IsTailscaleIP(tanetaddr) { + logf("tsshd: rejecting non-Tailscale addr %v", ta.IP) + s.Exit(1) + return + } + + ptyReq, winCh, isPty := s.Pty() + if !isPty { + fmt.Fprintf(s, "TODO scp etc") + s.Exit(1) + return + } + srcIPP := netaddr.IPPortFrom(tanetaddr, uint16(ta.Port)) + node, uprof, ok := lb.WhoIs(srcIPP) + if !ok { + fmt.Fprintf(s, "Hello, %v. I don't know who you are.\n", srcIPP) + s.Exit(0) + return + } + allow := envknob.String("TS_SSH_ALLOW_LOGIN") + if allow == "" || uprof.LoginName != allow { + logf("ssh: access denied for %q (only allowing %q)", uprof.LoginName, allow) + jnode, _ := json.Marshal(node) + jprof, _ := json.Marshal(uprof) + fmt.Fprintf(s, "Access denied.\n\nYou are node: %s\n\nYour profile: %s\n\nYou wanted %+v\n", jnode, jprof, ptyReq) + s.Exit(1) + return + } + + var cmd *exec.Cmd + sshUser := s.User() + if os.Getuid() != 0 || sshUser == "root" { + cmd = exec.Command("/bin/bash") + } else { + cmd = exec.Command("/usr/bin/env", "su", "-", sshUser) + } + cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) + f, err := pty.Start(cmd) + if err != nil { + logf("running shell: %v", err) + s.Exit(1) + return + } + defer f.Close() + go func() { + for win := range winCh { + setWinsize(f, win.Width, win.Height) + } + }() + go func() { + io.Copy(f, s) // stdin + }() + io.Copy(s, f) // stdout + cmd.Process.Kill() + if err := cmd.Wait(); err != nil { + s.Exit(1) + } + s.Exit(0) + return +} + +func setWinsize(f *os.File, w, h int) { + syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), + uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) +} diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 1501f4722..bbfd47512 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -103,9 +103,9 @@ type Impl struct { connsOpenBySubnetIP map[netaddr.IP]int } -// sshDemo is initialized in ssh.go (on Linux only) to register an SSH server +// handleSSH is initialized in ssh.go (on Linux only) to register an SSH server // handler. See https://github.com/tailscale/tailscale/issues/3802. -var sshDemo func(*Impl, net.Conn) error +var handleSSH func(logger.Logf, *ipnlocal.LocalBackend, net.Conn) error const nicID = 1 const mtu = 1500 @@ -638,17 +638,16 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) { // block until the TCP handshake is complete. c := gonet.NewTCPConn(&wq, ep) - if reqDetails.LocalPort == 22 && ns.processSSH() && ns.isLocalIP(dialIP) && sshDemo != nil { - // TODO(bradfitz): un-demo this. - ns.logf("doing ssh demo thing....") - if err := sshDemo(ns, c); err != nil { - ns.logf("ssh demo error: %v", err) - } else { - ns.logf("ssh demo: ok") - } - return - } if ns.lb != nil { + if reqDetails.LocalPort == 22 && ns.processSSH() && ns.isLocalIP(dialIP) && handleSSH != nil { + ns.logf("handling SSH connection....") + if err := handleSSH(ns.logf, ns.lb, c); err != nil { + ns.logf("ssh error: %v", err) + } else { + ns.logf("ssh: ok") + } + return + } if port, ok := ns.lb.GetPeerAPIPort(dialIP); ok { if reqDetails.LocalPort == port && ns.isLocalIP(dialIP) { src := netaddr.IPPortFrom(clientRemoteIP, reqDetails.RemotePort) diff --git a/wgengine/netstack/ssh.go b/wgengine/netstack/ssh.go index 1675c7f9d..b624e17ab 100644 --- a/wgengine/netstack/ssh.go +++ b/wgengine/netstack/ssh.go @@ -7,139 +7,8 @@ package netstack -import ( - "encoding/json" - "fmt" - "io" - "io/ioutil" - "log" - "net" - "os" - "os/exec" - "syscall" - "unsafe" - - "github.com/creack/pty" - "github.com/gliderlabs/ssh" - gossh "golang.org/x/crypto/ssh" - "inet.af/netaddr" - "tailscale.com/envknob" - "tailscale.com/net/tsaddr" -) +import "tailscale.com/ssh/tailssh" func init() { - sshDemo = sshDemoImpl -} - -func sshDemoImpl(ns *Impl, c net.Conn) error { - hostKey, err := ioutil.ReadFile("/etc/ssh/ssh_host_ed25519_key") - if err != nil { - return err - } - signer, err := gossh.ParsePrivateKey(hostKey) - if err != nil { - return err - } - srv := &ssh.Server{ - Handler: ns.handleSSH, - RequestHandlers: map[string]ssh.RequestHandler{}, - SubsystemHandlers: map[string]ssh.SubsystemHandler{}, - ChannelHandlers: map[string]ssh.ChannelHandler{}, - } - for k, v := range ssh.DefaultRequestHandlers { - srv.RequestHandlers[k] = v - } - for k, v := range ssh.DefaultChannelHandlers { - srv.ChannelHandlers[k] = v - } - for k, v := range ssh.DefaultSubsystemHandlers { - srv.SubsystemHandlers[k] = v - } - srv.AddHostKey(signer) - - srv.HandleConn(c) - return nil -} - -func (ns *Impl) handleSSH(s ssh.Session) { - lb := ns.lb - user := s.User() - addr := s.RemoteAddr() - log.Printf("Handling SSH from %v for user %v", addr, user) - ta, ok := addr.(*net.TCPAddr) - if !ok { - log.Printf("tsshd: rejecting non-TCP addr %T %v", addr, addr) - s.Exit(1) - return - } - tanetaddr, ok := netaddr.FromStdIP(ta.IP) - if !ok { - log.Printf("tsshd: rejecting unparseable addr %v", ta.IP) - s.Exit(1) - return - } - if !tsaddr.IsTailscaleIP(tanetaddr) { - log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP) - s.Exit(1) - return - } - - ptyReq, winCh, isPty := s.Pty() - if !isPty { - fmt.Fprintf(s, "TODO scp etc") - s.Exit(1) - return - } - srcIPP := netaddr.IPPortFrom(tanetaddr, uint16(ta.Port)) - node, uprof, ok := lb.WhoIs(srcIPP) - if !ok { - fmt.Fprintf(s, "Hello, %v. I don't know who you are.\n", srcIPP) - s.Exit(0) - return - } - allow := envknob.String("TS_SSH_ALLOW_LOGIN") - if allow == "" || uprof.LoginName != allow { - log.Printf("ssh: access denied for %q (only allowing %q)", uprof.LoginName, allow) - jnode, _ := json.Marshal(node) - jprof, _ := json.Marshal(uprof) - fmt.Fprintf(s, "Access denied.\n\nYou are node: %s\n\nYour profile: %s\n\nYou wanted %+v\n", jnode, jprof, ptyReq) - s.Exit(1) - return - } - - var cmd *exec.Cmd - sshUser := s.User() - if os.Getuid() != 0 || sshUser == "root" { - cmd = exec.Command("/bin/bash") - } else { - cmd = exec.Command("/usr/bin/env", "su", "-", sshUser) - } - cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) - f, err := pty.Start(cmd) - if err != nil { - log.Printf("running shell: %v", err) - s.Exit(1) - return - } - defer f.Close() - go func() { - for win := range winCh { - setWinsize(f, win.Width, win.Height) - } - }() - go func() { - io.Copy(f, s) // stdin - }() - io.Copy(s, f) // stdout - cmd.Process.Kill() - if err := cmd.Wait(); err != nil { - s.Exit(1) - } - s.Exit(0) - return -} - -func setWinsize(f *os.File, w, h int) { - syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), - uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) + handleSSH = tailssh.Handle }