diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index de68a7ef6..526da75b5 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -568,5 +568,13 @@ func runDevStoreSet(ctx context.Context, args []string) error { if !devStoreSetArgs.danger { return errors.New("this command is dangerous; use --danger to proceed") } - return localClient.SetDevStoreKeyValue(ctx, args[0], args[1]) + key, val := args[0], args[1] + if val == "-" { + valb, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + val = string(valb) + } + return localClient.SetDevStoreKeyValue(ctx, key, val) } diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index ff43ec4cd..0439e1e78 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -290,7 +290,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/singleflight from tailscale.com/control/controlclient+ tailscale.com/util/strs from tailscale.com/hostinfo+ tailscale.com/util/systemd from tailscale.com/control/controlclient+ - tailscale.com/util/uniq from tailscale.com/wgengine/magicsock + tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+ 💣 tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+ tailscale.com/version from tailscale.com/derp+ tailscale.com/version/distro from tailscale.com/hostinfo+ diff --git a/ipn/ipnlocal/cert_js.go b/ipn/ipnlocal/cert_js.go new file mode 100644 index 000000000..c14d6094d --- /dev/null +++ b/ipn/ipnlocal/cert_js.go @@ -0,0 +1,18 @@ +// Copyright (c) 2022 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. + +package ipnlocal + +import ( + "context" + "errors" +) + +type TLSCertKeyPair struct { + CertPEM, KeyPEM []byte +} + +func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) { + return nil, errors.New("not implemented for js/wasm") +} diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index a9e3e3ef4..518ac3373 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -6,10 +6,13 @@ package ipnlocal import ( "context" + "crypto/tls" "encoding/base64" + "encoding/json" "errors" "fmt" "io" + "math" "net" "net/http" "net/netip" @@ -25,6 +28,7 @@ import ( "sync/atomic" "time" + "go4.org/mem" "go4.org/netipx" "golang.org/x/exp/slices" "tailscale.com/client/tailscale/apitype" @@ -62,6 +66,7 @@ import ( "tailscale.com/util/multierr" "tailscale.com/util/osshare" "tailscale.com/util/systemd" + "tailscale.com/util/uniq" "tailscale.com/version" "tailscale.com/version/distro" "tailscale.com/wgengine" @@ -135,8 +140,9 @@ type LocalBackend struct { sshAtomicBool atomic.Bool shutdownCalled bool // if Shutdown has been called - filterAtomic atomic.Pointer[filter.Filter] - containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool] + filterAtomic atomic.Pointer[filter.Filter] + containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool] + shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool] // The mutex protects the following elements. mu sync.Mutex @@ -192,6 +198,10 @@ type LocalBackend struct { directFileDoFinalRename bool // false on macOS, true on several NAS platforms componentLogUntil map[string]componentLogState + // ServeConfig fields. (also guarded by mu) + lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig + serveConfig ipn.ServeConfig + // statusLock must be held before calling statusChanged.Wait() or // statusChanged.Broadcast(). statusLock sync.Mutex @@ -257,6 +267,8 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale // Default filter blocks everything and logs nothing, until Start() is called. b.setFilter(filter.NewAllowNone(logf, &netipx.IPSet{})) + b.setTCPPortsIntercepted(nil) + b.statusChanged = sync.NewCond(&b.statusLock) b.e.SetStatusCallback(b.setWgengineStatus) @@ -1142,6 +1154,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error { } } b.setAtomicValuesFromPrefs(b.prefs) + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() } wantRunning := b.prefs.WantRunning() @@ -1906,6 +1919,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err // value instead of making up a new one. b.logf("using frontend prefs: %s", prefs.Pretty()) b.prefs = prefs.Clone().View() + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() b.writeServerModeStartState(b.userID, b.prefs) return nil } @@ -1926,6 +1940,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err prefs.WantRunning = false b.logf("using backend prefs; created empty state for %q: %s", key, prefs.Pretty()) b.prefs = prefs.View() + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() return nil case err != nil: return fmt.Errorf("backend prefs: store.ReadState(%q): %v", key, err) @@ -1952,10 +1967,49 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err b.prefs = prefs.View() b.setAtomicValuesFromPrefs(b.prefs) + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() return nil } +// setTCPPortsIntercepted populates b.shouldInterceptTCPPortAtomic with an +// efficient func for ShouldInterceptTCPPort to use, which is called on every +// incoming packet. +func (b *LocalBackend) setTCPPortsIntercepted(ports []uint16) { + slices.Sort(ports) + uniq.ModifySlice(&ports) + b.logf("localbackend: handling TCP ports = %v", ports) + var f func(uint16) bool + switch len(ports) { + case 0: + f = func(uint16) bool { return false } + case 1: + f = func(p uint16) bool { return ports[0] == p } + case 2: + f = func(p uint16) bool { return ports[0] == p || ports[1] == p } + case 3: + f = func(p uint16) bool { return ports[0] == p || ports[1] == p || ports[2] == p } + default: + if len(ports) > 16 { + m := map[uint16]bool{} + for _, p := range ports { + m[p] = true + } + f = func(p uint16) bool { return m[p] } + } else { + f = func(p uint16) bool { + for _, x := range ports { + if p == x { + return true + } + } + return false + } + } + } + b.shouldInterceptTCPPortAtomic.Store(f) +} + // setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic // from the prefs p, which may be nil. func (b *LocalBackend) setAtomicValuesFromPrefs(p ipn.PrefsView) { @@ -1963,6 +2017,7 @@ func (b *LocalBackend) setAtomicValuesFromPrefs(p ipn.PrefsView) { if !p.Valid() { b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil)) + b.setTCPPortsIntercepted(nil) } else { b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(p.AdvertiseRoutes().Filter(tsaddr.IsViaPrefix))) } @@ -2283,8 +2338,8 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn // anyway. No-op if no exit node resolution is needed. findExitNodeIDLocked(newp, netMap) b.prefs = newp.View() - b.setAtomicValuesFromPrefs(b.prefs) + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() b.inServerMode = b.prefs.ForceDaemon() // We do this to avoid holding the lock while doing everything else. @@ -3281,6 +3336,7 @@ func (b *LocalBackend) ResetForClientDisconnect() { b.authURLSticky = "" b.activeLogin = "" b.setAtomicValuesFromPrefs(b.prefs) + b.setTCPPortsIntercepted(nil) } func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() } @@ -3402,6 +3458,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { } b.capFileSharing = fs + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() if nm == nil { b.nodeByAddr = nil return @@ -3436,6 +3493,41 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { } } +// setTCPPortsInterceptedFromNetmapAndPrefsLocked calls setTCPPortsIntercepted with +// the ports that tailscaled should handle as a function of b.netMap and b.prefs. +// +// b.mu must be held. +func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked() { + handlePorts := make([]uint16, 0, 4) + + prefs := b.prefs + if prefs.Valid() && prefs.RunSSH() && envknob.CanSSHD() { + handlePorts = append(handlePorts, 22) + } + + nm := b.netMap + if nm != nil && nm.SelfNode != nil { + profileID := fmt.Sprintf("node-%s", nm.SelfNode.StableID) // TODO(maisem,bradfitz): something else? + confKey := ipn.ServeConfigKey(profileID) + if confj, err := b.store.ReadState(confKey); err == nil { + if !b.lastServeConfJSON.Equal(mem.B(confj)) { + b.lastServeConfJSON = mem.B(confj) + var conf ipn.ServeConfig + if err := json.Unmarshal(confj, &conf); err != nil { + b.logf("invalid ServeConfig %q in StateStore: %v", confKey, err) + } + b.serveConfig = conf + } + for p := range b.serveConfig.TCP { + if p > 0 && p <= math.MaxUint16 { + handlePorts = append(handlePorts, uint16(p)) + } + } + } + } + b.setTCPPortsIntercepted(handlePorts) +} + // operatorUserName returns the current pref's OperatorUser's name, or the // empty string if none. func (b *LocalBackend) operatorUserName() string { @@ -3976,5 +4068,59 @@ func (b *LocalBackend) SetDevStateStore(key, value string) error { } err := b.store.WriteState(ipn.StateKey(key), []byte(value)) b.logf("SetDevStateStore(%q, %q) = %v", key, value, err) - return err + + if err != nil { + return err + } + + b.mu.Lock() + defer b.mu.Unlock() + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() + + return nil +} + +// ShouldInterceptTCPPort reports whether the given TCP port number to a +// Tailscale IP (not a subnet router, service IP, etc) should be intercepted by +// Tailscaled and handled in-process. +func (b *LocalBackend) ShouldInterceptTCPPort(port uint16) bool { + return b.shouldInterceptTCPPortAtomic.Load()(port) +} + +var runDevWebServer = envknob.RegisterBool("TS_DEV_WEBSERVER") + +func (b *LocalBackend) HandleInterceptedTCPConn(c net.Conn) { + if !runDevWebServer() { + b.logf("localbackend: closing TCP conn from %v to %v", c.RemoteAddr(), c.LocalAddr()) + c.Close() + return + } + + // TODO(bradfitz): look up how; sniff SNI if ambiguous + hs := &http.Server{ + TLSConfig: &tls.Config{ + GetCertificate: b.getTLSServeCert, + }, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "