mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
wgengine/netstack: add an SSH server experiment
Disabled by default. To use, run tailscaled with: TS_SSH_ALLOW_LOGIN=you@bar.com And enable with: $ TAILSCALE_USE_WIP_CODE=true tailscale up --ssh=true Then ssh [any-user]@[your-tailscale-ip] for a root bash shell. (both the "root" and "bash" part are temporary) Updates #3802 Change-Id: I268f8c3c95c8eed5f3231d712a5dc89615a406f0 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
41fd4eab5c
commit
f3c0023add
@ -24,6 +24,7 @@
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/safesocket"
|
||||
@ -81,6 +82,8 @@ func acceptRouteDefault(goos string) bool {
|
||||
|
||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
||||
|
||||
func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf := newFlagSet("up")
|
||||
|
||||
@ -96,6 +99,9 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
if envknob.UseWIPCode() || inTest() {
|
||||
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
|
||||
}
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
|
||||
upf.StringVar(&upArgs.authKeyOrFile, "authkey", "", `node authorization key; if it begins with "file:", then it's a path to a file containing the authkey`)
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
@ -131,6 +137,7 @@ type upArgsT struct {
|
||||
exitNodeIP string
|
||||
exitNodeAllowLANAccess bool
|
||||
shieldsUp bool
|
||||
runSSH bool
|
||||
forceReauth bool
|
||||
forceDaemon bool
|
||||
advertiseRoutes string
|
||||
@ -352,6 +359,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.RunSSH = upArgs.runSSH
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.Hostname = upArgs.hostname
|
||||
@ -712,6 +720,7 @@ func init() {
|
||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||
addPrefFlagMapping("operator", "OperatorUser")
|
||||
addPrefFlagMapping("ssh", "RunSSH")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
@ -902,6 +911,8 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]interfac
|
||||
switch f.Name {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled flag %q", f.Name))
|
||||
case "ssh":
|
||||
set(prefs.RunSSH)
|
||||
case "login-server":
|
||||
set(prefs.ControlURL)
|
||||
case "accept-routes":
|
||||
|
@ -3,6 +3,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
L github.com/anmitsu/go-shlex from github.com/gliderlabs/ssh
|
||||
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
|
||||
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/aws
|
||||
@ -60,6 +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
|
||||
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
|
||||
@ -256,7 +259,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
|
||||
@ -265,16 +268,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
L golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
L golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
L golang.org/x/crypto/ssh from github.com/gliderlabs/ssh+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
@ -312,14 +318,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
crypto/dsa from crypto/x509
|
||||
crypto/dsa from crypto/x509+
|
||||
crypto/ecdsa from crypto/tls+
|
||||
crypto/ed25519 from crypto/tls+
|
||||
crypto/elliptic from crypto/ecdsa+
|
||||
crypto/hmac from crypto/tls+
|
||||
crypto/md5 from crypto/tls+
|
||||
crypto/rand from crypto/ed25519+
|
||||
crypto/rc4 from crypto/tls
|
||||
crypto/rc4 from crypto/tls+
|
||||
crypto/rsa from crypto/tls+
|
||||
crypto/sha1 from crypto/tls+
|
||||
crypto/sha256 from crypto/tls+
|
||||
|
@ -329,9 +329,6 @@ func run() error {
|
||||
}
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
|
||||
if useNetstack {
|
||||
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
|
||||
@ -342,7 +339,6 @@ func run() error {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
}
|
||||
|
||||
if socksListener != nil || httpProxyListener != nil {
|
||||
if httpProxyListener != nil {
|
||||
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
|
||||
@ -392,6 +388,10 @@ func run() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipnserver.New: %w", err)
|
||||
}
|
||||
ns.SetLocalBackend(srv.LocalBackend())
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
|
||||
if debugMux != nil {
|
||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
||||
|
@ -100,3 +100,7 @@ func LookupInt(envVar string) (v int, ok bool) {
|
||||
log.Fatalf("invalid environment variable %s value %q: %v", envVar, val, err)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// UseWIPCode is whether TAILSCALE_USE_WIP_CODE is set to permit use
|
||||
// of Work-In-Progress code.
|
||||
func UseWIPCode() bool { return Bool("TAILSCALE_USE_WIP_CODE") }
|
||||
|
@ -39,6 +39,7 @@
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/portlist"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/empty"
|
||||
@ -100,6 +101,7 @@ type LocalBackend struct {
|
||||
serverURL string // tailcontrol URL
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
varRoot string // or empty if SetVarRoot never called
|
||||
sshAtomicBool syncs.AtomicBool
|
||||
|
||||
filterHash deephash.Sum
|
||||
|
||||
@ -1536,6 +1538,9 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
|
||||
}
|
||||
|
||||
b.logf("backend prefs for %q: %s", key, b.prefs.Pretty())
|
||||
|
||||
b.sshAtomicBool.Set(b.prefs != nil && b.prefs.RunSSH)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1709,6 +1714,8 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
netMap := b.netMap
|
||||
stateKey := b.stateKey
|
||||
|
||||
b.sshAtomicBool.Set(newp.RunSSH)
|
||||
|
||||
oldp := b.prefs
|
||||
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
||||
b.prefs = newp
|
||||
@ -2618,8 +2625,11 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
b.activeLogin = ""
|
||||
b.sshAtomicBool.Set(false)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Get() }
|
||||
|
||||
// Logout tells the controlclient that we want to log out, and
|
||||
// transitions the local engine to the logged-out state without
|
||||
// waiting for controlclient to be in that state.
|
||||
|
10
ipn/prefs.go
10
ipn/prefs.go
@ -98,6 +98,11 @@ type Prefs struct {
|
||||
// DNS configuration, if it exists.
|
||||
CorpDNS bool
|
||||
|
||||
// RunSSH bool is whether this node should run an SSH
|
||||
// server, permitting access to peers according to the
|
||||
// policies as configured by the Tailnet's admin(s).
|
||||
RunSSH bool
|
||||
|
||||
// WantRunning indicates whether networking should be active on
|
||||
// this node.
|
||||
WantRunning bool
|
||||
@ -193,6 +198,7 @@ type MaskedPrefs struct {
|
||||
ExitNodeIPSet bool `json:",omitempty"`
|
||||
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
|
||||
CorpDNSSet bool `json:",omitempty"`
|
||||
RunSSHSet bool `json:",omitempty"`
|
||||
WantRunningSet bool `json:",omitempty"`
|
||||
LoggedOutSet bool `json:",omitempty"`
|
||||
ShieldsUpSet bool `json:",omitempty"`
|
||||
@ -277,6 +283,9 @@ func (p *Prefs) pretty(goos string) string {
|
||||
sb.WriteString("mesh=false ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
|
||||
if p.RunSSH {
|
||||
sb.WriteString("ssh=true ")
|
||||
}
|
||||
if p.LoggedOut {
|
||||
sb.WriteString("loggedout=true ")
|
||||
}
|
||||
@ -348,6 +357,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.ExitNodeIP == p2.ExitNodeIP &&
|
||||
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
|
||||
p.CorpDNS == p2.CorpDNS &&
|
||||
p.RunSSH == p2.RunSSH &&
|
||||
p.WantRunning == p2.WantRunning &&
|
||||
p.LoggedOut == p2.LoggedOut &&
|
||||
p.NotepadURLs == p2.NotepadURLs &&
|
||||
|
@ -40,6 +40,7 @@ func (src *Prefs) Clone() *Prefs {
|
||||
ExitNodeIP netaddr.IP
|
||||
ExitNodeAllowLANAccess bool
|
||||
CorpDNS bool
|
||||
RunSSH bool
|
||||
WantRunning bool
|
||||
LoggedOut bool
|
||||
ShieldsUp bool
|
||||
|
@ -42,6 +42,7 @@ func TestPrefsEqual(t *testing.T) {
|
||||
"ExitNodeIP",
|
||||
"ExitNodeAllowLANAccess",
|
||||
"CorpDNS",
|
||||
"RunSSH",
|
||||
"WantRunning",
|
||||
"LoggedOut",
|
||||
"ShieldsUp",
|
||||
|
@ -16,7 +16,6 @@
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -24,6 +23,7 @@
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
@ -89,7 +89,7 @@ func (s *Server) Start() error {
|
||||
}
|
||||
|
||||
func (s *Server) start() error {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_USE_WIP_CODE")); !v {
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("code disabled without environment variable TAILSCALE_USE_WIP_CODE set true")
|
||||
}
|
||||
|
||||
|
@ -35,11 +35,13 @@
|
||||
"inet.af/netstack/tcpip/transport/udp"
|
||||
"inet.af/netstack/waiter"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/version/distro"
|
||||
@ -82,6 +84,7 @@ type Impl struct {
|
||||
dialer *tsdial.Dialer
|
||||
ctx context.Context // alive until Close
|
||||
ctxCancel context.CancelFunc // called on Close
|
||||
lb *ipnlocal.LocalBackend
|
||||
|
||||
// atomicIsLocalIPFunc holds a func that reports whether an IP
|
||||
// is a local (non-subnet) Tailscale IP address of this
|
||||
@ -97,6 +100,10 @@ type Impl struct {
|
||||
connsOpenBySubnetIP map[netaddr.IP]int
|
||||
}
|
||||
|
||||
// sshDemo 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
|
||||
|
||||
const nicID = 1
|
||||
const mtu = 1500
|
||||
|
||||
@ -165,6 +172,12 @@ func (ns *Impl) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLocalBackend sets the LocalBackend; it should only be run before
|
||||
// the Start method is called.
|
||||
func (ns *Impl) SetLocalBackend(lb *ipnlocal.LocalBackend) {
|
||||
ns.lb = lb
|
||||
}
|
||||
|
||||
// wrapProtoHandler returns protocol handler h wrapped in a version
|
||||
// that dynamically reconfigures ns's subnet addresses as needed for
|
||||
// outbound traffic.
|
||||
@ -252,8 +265,9 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
|
||||
ap := protocolAddr.AddressWithPrefix
|
||||
ip := netaddrIPFromNetstackIP(ap.Address)
|
||||
if ip == v4broadcast && ap.PrefixLen == 32 {
|
||||
// Don't delete this one later. It seems to be important.
|
||||
// Related to Issue 2642? Likely.
|
||||
// Don't add 255.255.255.255/32 to oldIPs so we don't
|
||||
// delete it later. We didn't install it, so it's not
|
||||
// ours to delete.
|
||||
continue
|
||||
}
|
||||
oldIPs[ap] = true
|
||||
@ -264,10 +278,10 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
|
||||
if nm.SelfNode != nil {
|
||||
for _, ipp := range nm.SelfNode.Addresses {
|
||||
isAddr[ipp] = true
|
||||
newIPs[ipPrefixToAddressWithPrefix(ipp)] = true
|
||||
}
|
||||
for _, ipp := range nm.SelfNode.AllowedIPs {
|
||||
local := isAddr[ipp]
|
||||
if local && ns.ProcessLocalIPs || !local && ns.ProcessSubnets {
|
||||
if !isAddr[ipp] && ns.ProcessSubnets {
|
||||
newIPs[ipPrefixToAddressWithPrefix(ipp)] = true
|
||||
}
|
||||
}
|
||||
@ -390,9 +404,16 @@ func (ns *Impl) isLocalIP(ip netaddr.IP) bool {
|
||||
return ns.atomicIsLocalIPFunc.Load().(func(netaddr.IP) bool)(ip)
|
||||
}
|
||||
|
||||
func (ns *Impl) processSSH() bool {
|
||||
return ns.lb != nil && ns.lb.ShouldRunSSH()
|
||||
}
|
||||
|
||||
// shouldProcessInbound reports whether an inbound packet should be
|
||||
// handled by netstack.
|
||||
func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
||||
if ns.isInboundTSSH(p) && ns.processSSH() {
|
||||
return true
|
||||
}
|
||||
if !ns.ProcessLocalIPs && !ns.ProcessSubnets {
|
||||
// Fast path for common case (e.g. Linux server in TUN mode) where
|
||||
// netstack isn't used at all; don't even do an isLocalIP lookup.
|
||||
@ -484,6 +505,12 @@ func (ns *Impl) userPing(dstIP netaddr.IP, pingResPkt []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *Impl) isInboundTSSH(p *packet.Parsed) bool {
|
||||
return p.IPProto == ipproto.TCP &&
|
||||
p.Dst.Port() == 22 &&
|
||||
ns.isLocalIP(p.Dst.IP())
|
||||
}
|
||||
|
||||
func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Response {
|
||||
if !ns.shouldProcessInbound(p, t) {
|
||||
// Let the host network stack (if any) deal with it.
|
||||
@ -585,6 +612,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.ForwardTCPIn != nil {
|
||||
ns.ForwardTCPIn(c, reqDetails.LocalPort)
|
||||
return
|
||||
|
139
wgengine/netstack/ssh.go
Normal file
139
wgengine/netstack/ssh.go
Normal file
@ -0,0 +1,139 @@
|
||||
// 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 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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
cmd := exec.Command("/bin/bash")
|
||||
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…
Reference in New Issue
Block a user