mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-19 19:38:40 +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 @@ import (
|
|||||||
qrcode "github.com/skip2/go-qrcode"
|
qrcode "github.com/skip2/go-qrcode"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/safesocket"
|
"tailscale.com/safesocket"
|
||||||
@ -81,6 +82,8 @@ func acceptRouteDefault(goos string) bool {
|
|||||||
|
|
||||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
||||||
|
|
||||||
|
func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||||
|
|
||||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||||
upf := newFlagSet("up")
|
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.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.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")
|
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.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.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")
|
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
|
exitNodeIP string
|
||||||
exitNodeAllowLANAccess bool
|
exitNodeAllowLANAccess bool
|
||||||
shieldsUp bool
|
shieldsUp bool
|
||||||
|
runSSH bool
|
||||||
forceReauth bool
|
forceReauth bool
|
||||||
forceDaemon bool
|
forceDaemon bool
|
||||||
advertiseRoutes string
|
advertiseRoutes string
|
||||||
@ -352,6 +359,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
|||||||
prefs.CorpDNS = upArgs.acceptDNS
|
prefs.CorpDNS = upArgs.acceptDNS
|
||||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||||
prefs.ShieldsUp = upArgs.shieldsUp
|
prefs.ShieldsUp = upArgs.shieldsUp
|
||||||
|
prefs.RunSSH = upArgs.runSSH
|
||||||
prefs.AdvertiseRoutes = routes
|
prefs.AdvertiseRoutes = routes
|
||||||
prefs.AdvertiseTags = tags
|
prefs.AdvertiseTags = tags
|
||||||
prefs.Hostname = upArgs.hostname
|
prefs.Hostname = upArgs.hostname
|
||||||
@ -712,6 +720,7 @@ func init() {
|
|||||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||||
addPrefFlagMapping("operator", "OperatorUser")
|
addPrefFlagMapping("operator", "OperatorUser")
|
||||||
|
addPrefFlagMapping("ssh", "RunSSH")
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||||
@ -902,6 +911,8 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]interfac
|
|||||||
switch f.Name {
|
switch f.Name {
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unhandled flag %q", f.Name))
|
panic(fmt.Sprintf("unhandled flag %q", f.Name))
|
||||||
|
case "ssh":
|
||||||
|
set(prefs.RunSSH)
|
||||||
case "login-server":
|
case "login-server":
|
||||||
set(prefs.ControlURL)
|
set(prefs.ControlURL)
|
||||||
case "accept-routes":
|
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 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/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
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 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 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
|
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/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/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 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
|
||||||
@ -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/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
|
||||||
@ -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/acme from tailscale.com/ipn/localapi
|
||||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
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/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/chacha20poly1305 from crypto/tls+
|
||||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
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/hkdf from crypto/tls
|
||||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
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/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
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/bpf from github.com/mdlayher/netlink+
|
||||||
golang.org/x/net/dns/dnsmessage from net+
|
golang.org/x/net/dns/dnsmessage from net+
|
||||||
golang.org/x/net/http/httpguts from net/http+
|
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/aes from crypto/ecdsa+
|
||||||
crypto/cipher from crypto/aes+
|
crypto/cipher from crypto/aes+
|
||||||
crypto/des from crypto/tls+
|
crypto/des from crypto/tls+
|
||||||
crypto/dsa from crypto/x509
|
crypto/dsa from crypto/x509+
|
||||||
crypto/ecdsa from crypto/tls+
|
crypto/ecdsa from crypto/tls+
|
||||||
crypto/ed25519 from crypto/tls+
|
crypto/ed25519 from crypto/tls+
|
||||||
crypto/elliptic from crypto/ecdsa+
|
crypto/elliptic from crypto/ecdsa+
|
||||||
crypto/hmac from crypto/tls+
|
crypto/hmac from crypto/tls+
|
||||||
crypto/md5 from crypto/tls+
|
crypto/md5 from crypto/tls+
|
||||||
crypto/rand from crypto/ed25519+
|
crypto/rand from crypto/ed25519+
|
||||||
crypto/rc4 from crypto/tls
|
crypto/rc4 from crypto/tls+
|
||||||
crypto/rsa from crypto/tls+
|
crypto/rsa from crypto/tls+
|
||||||
crypto/sha1 from crypto/tls+
|
crypto/sha1 from crypto/tls+
|
||||||
crypto/sha256 from crypto/tls+
|
crypto/sha256 from crypto/tls+
|
||||||
|
@ -329,9 +329,6 @@ func run() error {
|
|||||||
}
|
}
|
||||||
ns.ProcessLocalIPs = useNetstack
|
ns.ProcessLocalIPs = useNetstack
|
||||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||||
if err := ns.Start(); err != nil {
|
|
||||||
return fmt.Errorf("failed to start netstack: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if useNetstack {
|
if useNetstack {
|
||||||
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
|
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
|
||||||
@ -342,7 +339,6 @@ func run() error {
|
|||||||
return ns.DialContextTCP(ctx, dst)
|
return ns.DialContextTCP(ctx, dst)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if socksListener != nil || httpProxyListener != nil {
|
if socksListener != nil || httpProxyListener != nil {
|
||||||
if httpProxyListener != nil {
|
if httpProxyListener != nil {
|
||||||
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
|
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
|
||||||
@ -392,6 +388,10 @@ func run() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ipnserver.New: %w", err)
|
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 {
|
if debugMux != nil {
|
||||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
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)
|
log.Fatalf("invalid environment variable %s value %q: %v", envVar, val, err)
|
||||||
panic("unreachable")
|
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 @@ import (
|
|||||||
"tailscale.com/net/tsdial"
|
"tailscale.com/net/tsdial"
|
||||||
"tailscale.com/paths"
|
"tailscale.com/paths"
|
||||||
"tailscale.com/portlist"
|
"tailscale.com/portlist"
|
||||||
|
"tailscale.com/syncs"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/empty"
|
"tailscale.com/types/empty"
|
||||||
@ -100,6 +101,7 @@ type LocalBackend struct {
|
|||||||
serverURL string // tailcontrol URL
|
serverURL string // tailcontrol URL
|
||||||
newDecompressor func() (controlclient.Decompressor, error)
|
newDecompressor func() (controlclient.Decompressor, error)
|
||||||
varRoot string // or empty if SetVarRoot never called
|
varRoot string // or empty if SetVarRoot never called
|
||||||
|
sshAtomicBool syncs.AtomicBool
|
||||||
|
|
||||||
filterHash deephash.Sum
|
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.logf("backend prefs for %q: %s", key, b.prefs.Pretty())
|
||||||
|
|
||||||
|
b.sshAtomicBool.Set(b.prefs != nil && b.prefs.RunSSH)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1709,6 +1714,8 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
|||||||
netMap := b.netMap
|
netMap := b.netMap
|
||||||
stateKey := b.stateKey
|
stateKey := b.stateKey
|
||||||
|
|
||||||
|
b.sshAtomicBool.Set(newp.RunSSH)
|
||||||
|
|
||||||
oldp := b.prefs
|
oldp := b.prefs
|
||||||
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
||||||
b.prefs = newp
|
b.prefs = newp
|
||||||
@ -2618,8 +2625,11 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
|||||||
b.authURL = ""
|
b.authURL = ""
|
||||||
b.authURLSticky = ""
|
b.authURLSticky = ""
|
||||||
b.activeLogin = ""
|
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
|
// Logout tells the controlclient that we want to log out, and
|
||||||
// transitions the local engine to the logged-out state without
|
// transitions the local engine to the logged-out state without
|
||||||
// waiting for controlclient to be in that state.
|
// 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.
|
// DNS configuration, if it exists.
|
||||||
CorpDNS bool
|
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
|
// WantRunning indicates whether networking should be active on
|
||||||
// this node.
|
// this node.
|
||||||
WantRunning bool
|
WantRunning bool
|
||||||
@ -193,6 +198,7 @@ type MaskedPrefs struct {
|
|||||||
ExitNodeIPSet bool `json:",omitempty"`
|
ExitNodeIPSet bool `json:",omitempty"`
|
||||||
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
|
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
|
||||||
CorpDNSSet bool `json:",omitempty"`
|
CorpDNSSet bool `json:",omitempty"`
|
||||||
|
RunSSHSet bool `json:",omitempty"`
|
||||||
WantRunningSet bool `json:",omitempty"`
|
WantRunningSet bool `json:",omitempty"`
|
||||||
LoggedOutSet bool `json:",omitempty"`
|
LoggedOutSet bool `json:",omitempty"`
|
||||||
ShieldsUpSet bool `json:",omitempty"`
|
ShieldsUpSet bool `json:",omitempty"`
|
||||||
@ -277,6 +283,9 @@ func (p *Prefs) pretty(goos string) string {
|
|||||||
sb.WriteString("mesh=false ")
|
sb.WriteString("mesh=false ")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
|
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
|
||||||
|
if p.RunSSH {
|
||||||
|
sb.WriteString("ssh=true ")
|
||||||
|
}
|
||||||
if p.LoggedOut {
|
if p.LoggedOut {
|
||||||
sb.WriteString("loggedout=true ")
|
sb.WriteString("loggedout=true ")
|
||||||
}
|
}
|
||||||
@ -348,6 +357,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
|||||||
p.ExitNodeIP == p2.ExitNodeIP &&
|
p.ExitNodeIP == p2.ExitNodeIP &&
|
||||||
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
|
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
|
||||||
p.CorpDNS == p2.CorpDNS &&
|
p.CorpDNS == p2.CorpDNS &&
|
||||||
|
p.RunSSH == p2.RunSSH &&
|
||||||
p.WantRunning == p2.WantRunning &&
|
p.WantRunning == p2.WantRunning &&
|
||||||
p.LoggedOut == p2.LoggedOut &&
|
p.LoggedOut == p2.LoggedOut &&
|
||||||
p.NotepadURLs == p2.NotepadURLs &&
|
p.NotepadURLs == p2.NotepadURLs &&
|
||||||
|
@ -40,6 +40,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
|||||||
ExitNodeIP netaddr.IP
|
ExitNodeIP netaddr.IP
|
||||||
ExitNodeAllowLANAccess bool
|
ExitNodeAllowLANAccess bool
|
||||||
CorpDNS bool
|
CorpDNS bool
|
||||||
|
RunSSH bool
|
||||||
WantRunning bool
|
WantRunning bool
|
||||||
LoggedOut bool
|
LoggedOut bool
|
||||||
ShieldsUp bool
|
ShieldsUp bool
|
||||||
|
@ -42,6 +42,7 @@ func TestPrefsEqual(t *testing.T) {
|
|||||||
"ExitNodeIP",
|
"ExitNodeIP",
|
||||||
"ExitNodeAllowLANAccess",
|
"ExitNodeAllowLANAccess",
|
||||||
"CorpDNS",
|
"CorpDNS",
|
||||||
|
"RunSSH",
|
||||||
"WantRunning",
|
"WantRunning",
|
||||||
"LoggedOut",
|
"LoggedOut",
|
||||||
"ShieldsUp",
|
"ShieldsUp",
|
||||||
|
@ -16,7 +16,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -24,6 +23,7 @@ import (
|
|||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnlocal"
|
"tailscale.com/ipn/ipnlocal"
|
||||||
"tailscale.com/ipn/localapi"
|
"tailscale.com/ipn/localapi"
|
||||||
@ -89,7 +89,7 @@ func (s *Server) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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")
|
return errors.New("code disabled without environment variable TAILSCALE_USE_WIP_CODE set true")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,11 +35,13 @@ import (
|
|||||||
"inet.af/netstack/tcpip/transport/udp"
|
"inet.af/netstack/tcpip/transport/udp"
|
||||||
"inet.af/netstack/waiter"
|
"inet.af/netstack/waiter"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
|
"tailscale.com/ipn/ipnlocal"
|
||||||
"tailscale.com/net/packet"
|
"tailscale.com/net/packet"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/net/tsdial"
|
"tailscale.com/net/tsdial"
|
||||||
"tailscale.com/net/tstun"
|
"tailscale.com/net/tstun"
|
||||||
"tailscale.com/syncs"
|
"tailscale.com/syncs"
|
||||||
|
"tailscale.com/types/ipproto"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
"tailscale.com/version/distro"
|
"tailscale.com/version/distro"
|
||||||
@ -82,6 +84,7 @@ type Impl struct {
|
|||||||
dialer *tsdial.Dialer
|
dialer *tsdial.Dialer
|
||||||
ctx context.Context // alive until Close
|
ctx context.Context // alive until Close
|
||||||
ctxCancel context.CancelFunc // called on Close
|
ctxCancel context.CancelFunc // called on Close
|
||||||
|
lb *ipnlocal.LocalBackend
|
||||||
|
|
||||||
// atomicIsLocalIPFunc holds a func that reports whether an IP
|
// atomicIsLocalIPFunc holds a func that reports whether an IP
|
||||||
// is a local (non-subnet) Tailscale IP address of this
|
// is a local (non-subnet) Tailscale IP address of this
|
||||||
@ -97,6 +100,10 @@ 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
|
||||||
|
// handler. See https://github.com/tailscale/tailscale/issues/3802.
|
||||||
|
var sshDemo func(*Impl, net.Conn) error
|
||||||
|
|
||||||
const nicID = 1
|
const nicID = 1
|
||||||
const mtu = 1500
|
const mtu = 1500
|
||||||
|
|
||||||
@ -165,6 +172,12 @@ func (ns *Impl) Close() error {
|
|||||||
return nil
|
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
|
// wrapProtoHandler returns protocol handler h wrapped in a version
|
||||||
// that dynamically reconfigures ns's subnet addresses as needed for
|
// that dynamically reconfigures ns's subnet addresses as needed for
|
||||||
// outbound traffic.
|
// outbound traffic.
|
||||||
@ -252,8 +265,9 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
|
|||||||
ap := protocolAddr.AddressWithPrefix
|
ap := protocolAddr.AddressWithPrefix
|
||||||
ip := netaddrIPFromNetstackIP(ap.Address)
|
ip := netaddrIPFromNetstackIP(ap.Address)
|
||||||
if ip == v4broadcast && ap.PrefixLen == 32 {
|
if ip == v4broadcast && ap.PrefixLen == 32 {
|
||||||
// Don't delete this one later. It seems to be important.
|
// Don't add 255.255.255.255/32 to oldIPs so we don't
|
||||||
// Related to Issue 2642? Likely.
|
// delete it later. We didn't install it, so it's not
|
||||||
|
// ours to delete.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
oldIPs[ap] = true
|
oldIPs[ap] = true
|
||||||
@ -264,10 +278,10 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
|
|||||||
if nm.SelfNode != nil {
|
if nm.SelfNode != nil {
|
||||||
for _, ipp := range nm.SelfNode.Addresses {
|
for _, ipp := range nm.SelfNode.Addresses {
|
||||||
isAddr[ipp] = true
|
isAddr[ipp] = true
|
||||||
|
newIPs[ipPrefixToAddressWithPrefix(ipp)] = true
|
||||||
}
|
}
|
||||||
for _, ipp := range nm.SelfNode.AllowedIPs {
|
for _, ipp := range nm.SelfNode.AllowedIPs {
|
||||||
local := isAddr[ipp]
|
if !isAddr[ipp] && ns.ProcessSubnets {
|
||||||
if local && ns.ProcessLocalIPs || !local && ns.ProcessSubnets {
|
|
||||||
newIPs[ipPrefixToAddressWithPrefix(ipp)] = true
|
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)
|
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
|
// shouldProcessInbound reports whether an inbound packet should be
|
||||||
// handled by netstack.
|
// handled by netstack.
|
||||||
func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
||||||
|
if ns.isInboundTSSH(p) && ns.processSSH() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if !ns.ProcessLocalIPs && !ns.ProcessSubnets {
|
if !ns.ProcessLocalIPs && !ns.ProcessSubnets {
|
||||||
// Fast path for common case (e.g. Linux server in TUN mode) where
|
// 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.
|
// 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 {
|
func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Response {
|
||||||
if !ns.shouldProcessInbound(p, t) {
|
if !ns.shouldProcessInbound(p, t) {
|
||||||
// Let the host network stack (if any) deal with it.
|
// 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.
|
// 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.ForwardTCPIn != nil {
|
if ns.ForwardTCPIn != nil {
|
||||||
ns.ForwardTCPIn(c, reqDetails.LocalPort)
|
ns.ForwardTCPIn(c, reqDetails.LocalPort)
|
||||||
return
|
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…
x
Reference in New Issue
Block a user