From d404f1caedc1ef1597b7f8fe6dc4682896e37816 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 12 Feb 2020 14:46:59 -0800 Subject: [PATCH] cmd/tsshd: add basic SSH server --- cmd/tsshd/tsshd.go | 204 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 4 + go.sum | 8 ++ 3 files changed, 216 insertions(+) create mode 100644 cmd/tsshd/tsshd.go diff --git a/cmd/tsshd/tsshd.go b/cmd/tsshd/tsshd.go new file mode 100644 index 000000000..edd976d6f --- /dev/null +++ b/cmd/tsshd/tsshd.go @@ -0,0 +1,204 @@ +// Copyright (c) 2020 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. + +// The tsshd binary is an SSH server that accepts connections +// from anybody on the same Tailscale network. +// +// It does not use passwords or SSH public key. +// +// Any user name is accepted; users are logged in as whoever is +// running this daemon. +// +// Warning: use at your own risk. This code has had very few eyeballs +// on it. +package main + +import ( + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "strings" + "syscall" + "time" + "unsafe" + + "github.com/gliderlabs/ssh" + "github.com/kr/pty" + gossh "golang.org/x/crypto/ssh" +) + +var ( + port = flag.Int("port", 2200, "port to listen on") + hostKey = flag.String("hostkey", "", "SSH host key") +) + +func main() { + flag.Parse() + if *hostKey == "" { + log.Fatalf("missing required --hostkey") + } + hostKey, err := ioutil.ReadFile(*hostKey) + if err != nil { + log.Fatal(err) + } + signer, err := gossh.ParsePrivateKey(hostKey) + if err != nil { + log.Printf("failed to parse SSH host key: %v; running running SSH server", err) + return + } + + warned := false + for { + addr, iface, err := tailscaleInterface() + if err != nil { + log.Fatalf("listing interfaces: %v", err) + } + if addr == nil { + if !warned { + log.Printf("no tailscale interface found; polling until one is available") + warned = true + } + // TODO: use netlink or other OS-specific mechanism to efficiently + // wait for change in interfaces. Polling every N seconds is good enough + // for now. + time.Sleep(5 * time.Second) + continue + } + warned = false + listen := net.JoinHostPort(addr.String(), fmt.Sprint(*port)) + log.Printf("tailscale ssh server listening on %v, %v", iface.Name, listen) + s := &ssh.Server{ + Addr: listen, + Handler: handleSSH, + } + s.AddHostKey(signer) + + err = s.ListenAndServe() + log.Fatalf("tailscale sshd failed: %v", err) + } + +} + +// tailscaleInterface returns an err on a fatal problem, and all zero values +// if no suitable inteface is found. +func tailscaleInterface() (net.IP, *net.Interface, error) { + ifs, err := net.Interfaces() + if err != nil { + return nil, nil, err + } + for _, iface := range ifs { + if !maybeTailscaleInterfaceName(iface.Name) { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, a := range addrs { + if ipnet, ok := a.(*net.IPNet); ok && isTailscaleIP(ipnet.IP) { + return ipnet.IP, &iface, nil + } + } + } + return nil, nil, nil +} + +// maybeTailscaleInterfaceName reports whether s is an interface +// name that might be used by Tailscale. +func maybeTailscaleInterfaceName(s string) bool { + return strings.HasPrefix(s, "wg") || + strings.HasPrefix(s, "ts") || + strings.HasPrefix(s, "tailscale") +} + +func isTailscaleIP(ip net.IP) bool { + return cgNAT.Contains(ip) +} + +var cgNAT = func() *net.IPNet { + _, ipNet, err := net.ParseCIDR("100.64.0.0/10") + if err != nil { + panic(err) + } + return ipNet +}() + +func handleSSH(s ssh.Session) { + user := s.User() + addr := s.RemoteAddr() + ta, ok := addr.(*net.TCPAddr) + if !ok { + log.Printf("tsshd: rejecting non-TCP addr %T %v", addr, addr) + s.Exit(1) + return + } + if !isTailscaleIP(ta.IP) { + log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP) + s.Exit(1) + return + } + + log.Printf("new session for %q from %v", user, ta) + defer log.Printf("closing session for %q from %v", user, ta) + ptyReq, winCh, isPty := s.Pty() + if !isPty { + fmt.Fprintf(s, "TODO scp etc") + s.Exit(1) + return + } + + userWantsShell := len(s.Command()) == 0 + + if userWantsShell { + shell, err := shellOfUser(s.User()) + if err != nil { + fmt.Fprintf(s, "failed to find shell: %v\n", err) + s.Exit(1) + return + } + cmd := exec.Command(shell) + 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 + } + + fmt.Fprintf(s, "TODO: args\n") + s.Exit(1) + return +} + +func shellOfUser(user string) (string, error) { + // TODO + return "/bin/bash", nil +} + +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/go.mod b/go.mod index c99636c5c..52bdc65c7 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,15 @@ module tailscale.com go 1.13 require ( + github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 + github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/gliderlabs/ssh v0.2.2 github.com/go-ole/go-ole v1.2.4 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e github.com/google/go-cmp v0.4.0 github.com/klauspost/compress v1.9.8 + github.com/kr/pty v1.1.1 github.com/mdlayher/netlink v1.1.0 github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3 diff --git a/go.sum b/go.sum index 4a33de9bd..2ec3f95c4 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,16 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4= github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29/go.mod h1:JYWahgHer+Z2xbsgHPtaDYVWzeHDminu+YIBWkxpCAY= github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab h1:CMGzRRCjnD50RjUFSArBLuCxiDvdp7b8YPAcikBEQ+k= github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab/go.mod h1:nfFtvHn2Hgs9G1u0/J6LHQv//EksNC+7G8vXmd1VTJ8= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= @@ -23,6 +29,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.9.8 h1:VMAMUUOh+gaxKTMk+zqbjsSjsIcUcL/LF4o63i82QyA= github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= @@ -66,6 +73,7 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BG golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=