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 <bradfitz@tailscale.com>
This commit is contained in:
		
				
					committed by
					
						
						Brad Fitzpatrick
					
				
			
			
				
	
			
			
			
						parent
						
							6d02a48d8d
						
					
				
				
					commit
					1b87e025e9
				
			@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										156
									
								
								ssh/tailssh/tailssh.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								ssh/tailssh/tailssh.go
									
									
									
									
									
										Normal file
									
								
							@ -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})))
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user