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>
pull/3951/head
Brad Fitzpatrick 3 years ago 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/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/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/coreos/go-iptables/iptables from tailscale.com/wgengine/router
L 💣 github.com/creack/pty from tailscale.com/wgengine/netstack L 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
L github.com/gliderlabs/ssh from tailscale.com/wgengine/netstack 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 from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns 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/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/client/tailscale+ tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+ 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/syncs from tailscale.com/control/controlknobs+
tailscale.com/tailcfg from tailscale.com/client/tailscale+ tailscale.com/tailcfg from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces 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/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+ tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+ 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/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+ tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal

@ -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 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. // 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 nicID = 1
const mtu = 1500 const mtu = 1500
@ -638,17 +638,16 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
// block until the TCP handshake is complete. // block until the TCP handshake is complete.
c := gonet.NewTCPConn(&wq, ep) 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 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 port, ok := ns.lb.GetPeerAPIPort(dialIP); ok {
if reqDetails.LocalPort == port && ns.isLocalIP(dialIP) { if reqDetails.LocalPort == port && ns.isLocalIP(dialIP) {
src := netaddr.IPPortFrom(clientRemoteIP, reqDetails.RemotePort) src := netaddr.IPPortFrom(clientRemoteIP, reqDetails.RemotePort)

@ -7,139 +7,8 @@
package netstack package netstack
import ( import "tailscale.com/ssh/tailssh"
"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"
)
func init() { func init() {
sshDemo = sshDemoImpl handleSSH = tailssh.Handle
}
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})))
} }

Loading…
Cancel
Save