From 55587d3156143a5f16f4f1caab208fa3ff9057d8 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 10 Jan 2025 21:12:16 -0800 Subject: [PATCH] lanscaping: remove serve, webclient -rwxr-xr-x@ 1 bradfitz staff 14338354 Jan 10 21:12 /Users/bradfitz/bin/tailscaled.min -rwxr-xr-x@ 1 bradfitz staff 15532184 Jan 10 21:12 /Users/bradfitz/bin/tailscaled.minlinux Change-Id: I075670f96610e55b7adaacb105fe4d749ed6cfd8 Signed-off-by: Brad Fitzpatrick --- Makefile | 2 +- cmd/tailscaled/depaware-minlinux.txt | 33 +- cmd/tailscaled/tailscaled.go | 5 - ipn/ipnlocal/local.go | 249 +------ ipn/ipnlocal/peerapi.go | 60 +- ipn/ipnlocal/serve.go | 937 --------------------------- ipn/ipnlocal/web_client.go | 2 +- ipn/localapi/localapi.go | 55 -- 8 files changed, 17 insertions(+), 1326 deletions(-) delete mode 100644 ipn/ipnlocal/serve.go diff --git a/Makefile b/Makefile index 2e0de8a21..ac67408dc 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ updatedeps: ## Update depaware deps tailscale.com/cmd/k8s-operator \ tailscale.com/cmd/stund -MIN_OMITS ?= ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_netstack,ts_omit_nftables,ts_omit_ssh,ts_omit_tka +MIN_OMITS ?= ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_netstack,ts_omit_nftables,ts_omit_ssh,ts_omit_tka,ts_omit_webclient min: ./tool/go build -o $$HOME/bin/tailscaled.min -ldflags "-w -s" --tags=${MIN_OMITS} ./cmd/tailscaled diff --git a/cmd/tailscaled/depaware-minlinux.txt b/cmd/tailscaled/depaware-minlinux.txt index 7baac80f9..0d5a32e8b 100644 --- a/cmd/tailscaled/depaware-minlinux.txt +++ b/cmd/tailscaled/depaware-minlinux.txt @@ -14,8 +14,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+ L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns github.com/golang/groupcache/lru from tailscale.com/net/dnscache - github.com/gorilla/csrf from tailscale.com/client/web - github.com/gorilla/securecookie from github.com/gorilla/csrf L github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign L 💣 github.com/illarion/gonotify/v2 from tailscale.com/net/dns L github.com/josharian/native from github.com/mdlayher/netlink+ @@ -50,7 +48,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+ L 💣 github.com/tailscale/netlink/nl from github.com/tailscale/netlink github.com/tailscale/peercred from tailscale.com/ipn/ipnauth - github.com/tailscale/web-client-prebuilt from tailscale.com/client/web 💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+ 💣 github.com/tailscale/wireguard-go/device from tailscale.com/net/tstun+ 💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device @@ -65,10 +62,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com from tailscale.com/version tailscale.com/appc from tailscale.com/ipn/ipnlocal tailscale.com/atomicfile from tailscale.com/ipn+ - tailscale.com/client/tailscale from tailscale.com/client/web+ + tailscale.com/client/tailscale from tailscale.com/derp tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+ - tailscale.com/client/web from tailscale.com/ipn/ipnlocal - tailscale.com/clientupdate from tailscale.com/client/web+ + tailscale.com/clientupdate from tailscale.com/ipn/ipnlocal+ L tailscale.com/clientupdate/distsign from tailscale.com/clientupdate tailscale.com/cmd/tailscaled/childproc from tailscale.com/cmd/tailscaled tailscale.com/control/controlbase from tailscale.com/control/controlhttp+ @@ -85,10 +81,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal tailscale.com/drive from tailscale.com/client/tailscale+ tailscale.com/envknob from tailscale.com/client/tailscale+ - tailscale.com/envknob/featureknob from tailscale.com/client/web+ + tailscale.com/envknob/featureknob from tailscale.com/ipn/ipnlocal tailscale.com/health from tailscale.com/cmd/tailscaled+ tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal - tailscale.com/hostinfo from tailscale.com/client/web+ + tailscale.com/hostinfo from tailscale.com/cmd/tailscaled+ tailscale.com/internal/noiseconn from tailscale.com/control/controlclient tailscale.com/ipn from tailscale.com/client/tailscale+ tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+ @@ -101,7 +97,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+ tailscale.com/ipn/store/mem from tailscale.com/ipn/ipnlocal+ tailscale.com/kube/kubetypes from tailscale.com/envknob - tailscale.com/licenses from tailscale.com/client/web tailscale.com/log/filelogger from tailscale.com/logpolicy tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+ @@ -138,7 +133,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L tailscale.com/net/tcpinfo from tailscale.com/derp tailscale.com/net/tlsdial from tailscale.com/control/controlclient+ tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial - tailscale.com/net/tsaddr from tailscale.com/client/web+ + tailscale.com/net/tsaddr from tailscale.com/ipn+ tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+ tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+ tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+ @@ -163,7 +158,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled tailscale.com/types/ipproto from tailscale.com/ipn+ tailscale.com/types/key from tailscale.com/client/tailscale+ - tailscale.com/types/lazy from tailscale.com/ipn/ipnlocal+ + tailscale.com/types/lazy from tailscale.com/util/cloudenv+ tailscale.com/types/logger from tailscale.com/appc+ tailscale.com/types/logid from tailscale.com/cmd/tailscaled+ tailscale.com/types/netlogtype from tailscale.com/net/connstats+ @@ -187,7 +182,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/dnsname from tailscale.com/appc+ tailscale.com/util/execqueue from tailscale.com/appc+ tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal - tailscale.com/util/groupmember from tailscale.com/client/web+ + tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth 💣 tailscale.com/util/hashx from tailscale.com/util/deephash tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+ tailscale.com/util/httpm from tailscale.com/client/tailscale+ @@ -224,8 +219,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/vizerror from tailscale.com/tailcfg+ tailscale.com/util/winutil from tailscale.com/ipn/ipnauth tailscale.com/util/zstdframe from tailscale.com/control/controlclient+ - tailscale.com/version from tailscale.com/client/web+ - tailscale.com/version/distro from tailscale.com/client/web+ + tailscale.com/version from tailscale.com/clientupdate+ + tailscale.com/version/distro from tailscale.com/clientupdate+ tailscale.com/wgengine from tailscale.com/cmd/tailscaled+ tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+ tailscale.com/wgengine/filter from tailscale.com/control/controlclient+ @@ -305,12 +300,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de crypto/x509 from crypto/tls+ crypto/x509/pkix from crypto/x509+ embed from crypto/internal/nistec+ - encoding from encoding/gob+ + encoding from encoding/json+ encoding/asn1 from crypto/x509+ encoding/base32 from github.com/go-json-experiment/json encoding/base64 from encoding/json+ encoding/binary from compress/gzip+ - encoding/gob from github.com/gorilla/securecookie encoding/hex from crypto/x509+ encoding/json from expvar+ encoding/pem from crypto/tls+ @@ -323,8 +317,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de hash/adler32 from tailscale.com/taildrop hash/crc32 from compress/gzip+ hash/maphash from go4.org/mem - html from html/template+ - html/template from github.com/gorilla/csrf + html from net/http/pprof+ io from archive/tar+ io/fs from archive/tar+ io/ioutil from github.com/digitalocean/go-smbios/smbios+ @@ -344,7 +337,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de net/http from expvar+ net/http/httptest from tailscale.com/control/controlclient net/http/httptrace from golang.org/x/net/http2+ - net/http/httputil from tailscale.com/client/web+ + net/http/httputil from tailscale.com/cmd/tailscaled+ net/http/internal from net/http+ net/http/pprof from tailscale.com/cmd/tailscaled+ net/netip from github.com/gaissmai/bart+ @@ -370,8 +363,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de sync/atomic from context+ syscall from archive/tar+ text/tabwriter from runtime/pprof - text/template from html/template - text/template/parse from html/template+ time from archive/tar+ unicode from bytes+ unicode/utf16 from crypto/x509+ diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index d1efe2ecb..2344544f9 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -28,7 +28,6 @@ import ( "syscall" "time" - "tailscale.com/client/tailscale" "tailscale.com/cmd/tailscaled/childproc" "tailscale.com/control/controlclient" "tailscale.com/envknob" @@ -583,10 +582,6 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID if root := lb.TailscaleVarRoot(); root != "" { dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf) } - lb.ConfigureWebClient(&tailscale.LocalClient{ - Socket: args.socketpath, - UseSocketOnly: args.socketpath != paths.DefaultTailscaledSocket(), - }) configureTaildrop(logf, lb) if err := startNetstack(lb); err != nil { log.Fatalf("failed to start netstack: %v", err) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 3ad3975b0..ee2c07064 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -17,7 +17,6 @@ import ( "fmt" "io" "log" - "maps" "math" "math/rand/v2" "net" @@ -37,7 +36,6 @@ import ( "sync/atomic" "time" - "go4.org/mem" "go4.org/netipx" "golang.org/x/net/dns/dnsmessage" "tailscale.com/appc" @@ -314,16 +312,6 @@ type LocalBackend struct { //lint:ignore U1000 only used in Linux and Windows builds in autoupdate.go offlineAutoUpdateCancel func() - // ServeConfig fields. (also guarded by mu) - lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig - serveConfig ipn.ServeConfigView // or !Valid if none - - webClient webClient - webClientListeners map[netip.AddrPort]*localListener // listeners for local web client traffic - - serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic - serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy - // statusLock must be held before calling statusChanged.Wait() or // statusChanged.Broadcast(). statusLock sync.Mutex @@ -987,7 +975,6 @@ func (b *LocalBackend) Shutdown() { b.notifyCancel() } b.mu.Unlock() - b.webClientShutdown() if b.sockstatLogger != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -1567,7 +1554,6 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control // Perform all reconfiguration based on the netmap here. if st.NetMap != nil { b.capTailnetLock = st.NetMap.HasCap(tailcfg.CapabilityTailnetLock) - b.setWebClientAtomicBoolLocked(st.NetMap) // As we stepped outside of the lock, it's possible for b.cc // to now be nil. @@ -2846,9 +2832,6 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A b.goTracker.Go(func() { b.pollRequestEngineStatus(ctx) }) } - // TODO(marwan-at-work): streaming background logs? - defer b.DeleteForegroundSession(sessionID) - sender := &rateLimitingBusSender{fn: fn} defer sender.close() @@ -3331,17 +3314,13 @@ func (b *LocalBackend) setTCPPortsIntercepted(ports []uint16) { // which may be !Valid(). func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) { b.sshAtomicBool.Store(p.Valid() && p.RunSSH() && envknob.CanSSHD()) - b.setExposeRemoteWebClientAtomicBoolLocked(p) if !p.Valid() { b.containsViaIPFuncAtomic.Store(ipset.FalseContainsIPFunc()) b.setTCPPortsIntercepted(nil) - b.lastServeConfJSON = mem.B(nil) - b.serveConfig = ipn.ServeConfigView{} } else { filtered := tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes(), tsaddr.IsViaPrefix) b.containsViaIPFuncAtomic.Store(ipset.NewContainsIPFunc(views.SliceOf(filtered))) - b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p) } } @@ -3650,9 +3629,6 @@ func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error { if err := b.checkExitNodePrefsLocked(p); err != nil { errs = append(errs, err) } - if err := b.checkFunnelEnabledLocked(p); err != nil { - errs = append(errs, err) - } if err := b.checkAutoUpdatePrefsLocked(p); err != nil { errs = append(errs, err) } @@ -3759,13 +3735,6 @@ func (b *LocalBackend) checkExitNodePrefsLocked(p *ipn.Prefs) error { return nil } -func (b *LocalBackend) checkFunnelEnabledLocked(p *ipn.Prefs) error { - if p.ShieldsUp && b.serveConfig.IsFunnelOn() { - return errors.New("Cannot enable shields-up when Funnel is enabled.") - } - return nil -} - func (b *LocalBackend) checkAutoUpdatePrefsLocked(p *ipn.Prefs) error { if p.AutoUpdate.Apply.EqualBool(true) && !clientupdate.CanAutoUpdate() { return errors.New("Auto-updates are not supported on this platform.") @@ -3894,20 +3863,6 @@ func (b *LocalBackend) checkProfileNameLocked(p *ipn.Prefs) error { return nil } -// wantIngressLocked reports whether this node has ingress configured. This bool -// is sent to the coordination server (in Hostinfo.WireIngress) as an -// optimization hint to know primarily which nodes are NOT using ingress, to -// avoid doing work for regular nodes. -// -// Even if the user's ServeConfig.AllowFunnel map was manually edited in raw -// mode and contains map entries with false values, sending true (from Len > 0) -// is still fine. This is only an optimization hint for the control plane and -// doesn't affect security or correctness. And we also don't expect people to -// modify their ServeConfig in raw mode. -func (b *LocalBackend) wantIngressLocked() bool { - return b.serveConfig.Valid() && b.serveConfig.HasAllowFunnel() -} - // setPrefsLockedOnEntry requires b.mu be held to call it, but it // unlocks b.mu when done. newp ownership passes to this function. // It returns a readonly copy of the new prefs. @@ -4926,7 +4881,7 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip // if this is accidentally false, then control may not configure DNS // properly. This exists as an optimization to control to program fewer DNS // records that have ingress enabled but are not actually being used. - hi.WireIngress = b.wantIngressLocked() + hi.WireIngress = false hi.AppConnector.Set(prefs.AppConnector().Advertise) } @@ -5306,44 +5261,6 @@ func (b *LocalBackend) ResetForClientDisconnect() { func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() } -// ShouldRunWebClient reports whether the web client is being run -// within this tailscaled instance. ShouldRunWebClient is safe to -// call regardless of whether b.mu is held or not. -func (b *LocalBackend) ShouldRunWebClient() bool { return b.webClientAtomicBool.Load() } - -// ShouldExposeRemoteWebClient reports whether the web client should -// accept connections via [tailscale IP]:5252 in addition to the default -// behaviour of accepting local connections over 100.100.100.100. -// -// This function checks both the web client user pref via -// exposeRemoteWebClientAtomicBool and the disable-web-client node attr -// via ShouldRunWebClient to determine whether the web client should be -// exposed. -func (b *LocalBackend) ShouldExposeRemoteWebClient() bool { - return b.ShouldRunWebClient() && b.exposeRemoteWebClientAtomicBool.Load() -} - -// setWebClientAtomicBoolLocked sets webClientAtomicBool based on whether -// tailcfg.NodeAttrDisableWebClient has been set in the netmap.NetworkMap. -// -// b.mu must be held. -func (b *LocalBackend) setWebClientAtomicBoolLocked(nm *netmap.NetworkMap) { - shouldRun := !nm.HasCap(tailcfg.NodeAttrDisableWebClient) - wasRunning := b.webClientAtomicBool.Swap(shouldRun) - if wasRunning && !shouldRun { - b.goTracker.Go(b.webClientShutdown) // stop web client - } -} - -// setExposeRemoteWebClientAtomicBoolLocked sets exposeRemoteWebClientAtomicBool -// based on whether the RunWebClient pref is set. -// -// b.mu must be held. -func (b *LocalBackend) setExposeRemoteWebClientAtomicBoolLocked(prefs ipn.PrefsView) { - shouldExpose := prefs.Valid() && prefs.RunWebClient() - b.exposeRemoteWebClientAtomicBool.Store(shouldExpose) -} - // ShouldHandleViaIP reports whether ip is an IPv6 address in the // Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to // by Tailscale. @@ -5532,7 +5449,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { netns.SetBindToInterfaceByRoute(nm.HasCap(tailcfg.CapabilityBindToInterfaceByRoute)) netns.SetDisableBindConnToInterface(nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterface)) - b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) if nm == nil { b.nodeByAddr = nil @@ -5771,139 +5687,6 @@ func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) { } } -// reloadServeConfigLocked reloads the serve config from the store or resets the -// serve config to nil if not logged in. The "changed" parameter, when false, instructs -// the method to only run the reset-logic and not reload the store from memory to ensure -// foreground sessions are not removed if they are not saved on disk. -func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { - if b.netMap == nil || !b.netMap.SelfNode.Valid() || !prefs.Valid() || b.pm.CurrentProfile().ID == "" { - // We're not logged in, so we don't have a profile. - // Don't try to load the serve config. - b.lastServeConfJSON = mem.B(nil) - b.serveConfig = ipn.ServeConfigView{} - return - } - - confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID) - // TODO(maisem,bradfitz): prevent reading the config from disk - // if the profile has not changed. - confj, err := b.store.ReadState(confKey) - if err != nil { - b.lastServeConfJSON = mem.B(nil) - b.serveConfig = ipn.ServeConfigView{} - return - } - if b.lastServeConfJSON.Equal(mem.B(confj)) { - return - } - 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 = ipn.ServeConfigView{} - return - } - - // remove inactive sessions - maps.DeleteFunc(conf.Foreground, func(sessionID string, sc *ipn.ServeConfig) bool { - _, ok := b.notifyWatchers[sessionID] - return !ok - }) - - b.serveConfig = conf.View() -} - -// 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(prefs ipn.PrefsView) { - handlePorts := make([]uint16, 0, 4) - - if prefs.Valid() && prefs.RunSSH() && envknob.CanSSHD() { - handlePorts = append(handlePorts, 22) - } - if b.ShouldExposeRemoteWebClient() { - handlePorts = append(handlePorts, webClientPort) - - // don't listen on netmap addresses if we're in userspace mode - if !b.sys.IsNetstack() { - b.updateWebClientListenersLocked() - } - } - - b.reloadServeConfigLocked(prefs) - if b.serveConfig.Valid() { - servePorts := make([]uint16, 0, 3) - for port := range b.serveConfig.TCPs() { - if port > 0 { - servePorts = append(servePorts, uint16(port)) - } - } - handlePorts = append(handlePorts, servePorts...) - - b.setServeProxyHandlersLocked() - - // don't listen on netmap addresses if we're in userspace mode - if !b.sys.IsNetstack() { - b.updateServeTCPPortNetMapAddrListenersLocked(servePorts) - } - } - // Kick off a Hostinfo update to control if WireIngress changed. - if wire := b.wantIngressLocked(); b.hostinfo != nil && b.hostinfo.WireIngress != wire { - b.logf("Hostinfo.WireIngress changed to %v", wire) - b.hostinfo.WireIngress = wire - b.goTracker.Go(b.doSetHostinfoFilterServices) - } - - b.setTCPPortsIntercepted(handlePorts) -} - -// setServeProxyHandlersLocked ensures there is an http proxy handler for each -// backend specified in serveConfig. It expects serveConfig to be valid and -// up-to-date, so should be called after reloadServeConfigLocked. -func (b *LocalBackend) setServeProxyHandlersLocked() { - if !b.serveConfig.Valid() { - return - } - var backends map[string]bool - for _, conf := range b.serveConfig.Webs() { - for _, h := range conf.Handlers().All() { - backend := h.Proxy() - if backend == "" { - // Only create proxy handlers for servers with a proxy backend. - continue - } - mak.Set(&backends, backend, true) - if _, ok := b.serveProxyHandlers.Load(backend); ok { - continue - } - - b.logf("serve: creating a new proxy handler for %s", backend) - p, err := b.proxyHandlerForBackend(backend) - if err != nil { - // The backend endpoint (h.Proxy) should have been validated by expandProxyTarget - // in the CLI, so just log the error here. - b.logf("[unexpected] could not create proxy for %v: %s", backend, err) - continue - } - b.serveProxyHandlers.Store(backend, p) - } - } - - // Clean up handlers for proxy backends that are no longer present - // in configuration. - b.serveProxyHandlers.Range(func(key, value any) bool { - backend := key.(string) - if !backends[backend] { - b.logf("serve: closing idle connections to %s", backend) - b.serveProxyHandlers.Delete(backend) - value.(*reverseProxy).close() - } - return true - }) -} - // operatorUserName returns the current pref's OperatorUser's name, or the // empty string if none. func (b *LocalBackend) operatorUserName() string { @@ -6674,10 +6457,6 @@ func (b *LocalBackend) SetDevStateStore(key, value string) error { return err } - b.mu.Lock() - defer b.mu.Unlock() - b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) - return nil } @@ -6747,8 +6526,6 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry(unlock unlockOnce) err if err := b.initTKALocked(); err != nil { return err } - b.lastServeConfJSON = mem.B(nil) - b.serveConfig = ipn.ServeConfigView{} b.lastSuggestedExitNode = "" b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu b.health.SetLocalLogConfigHealth(nil) @@ -7529,29 +7306,7 @@ func (b *LocalBackend) vipServiceHash(services []*tailcfg.VIPService) string { } func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcfg.VIPService { - // keyed by service name - var services map[string]*tailcfg.VIPService - if !b.serveConfig.Valid() { - return nil - } - - for svc, config := range b.serveConfig.Services().All() { - mak.Set(&services, svc, &tailcfg.VIPService{ - Name: svc, - Ports: config.ServicePortRange(), - }) - } - - for _, s := range prefs.AdvertiseServices().All() { - if services == nil || services[s] == nil { - mak.Set(&services, s, &tailcfg.VIPService{ - Name: s, - }) - } - services[s].Active = true - } - - return slicesx.MapValues(services) + return nil } var ( diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index aa18c3588..77eacc22f 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -33,7 +33,6 @@ import ( "tailscale.com/envknob" "tailscale.com/health" "tailscale.com/hostinfo" - "tailscale.com/ipn" "tailscale.com/net/netaddr" "tailscale.com/net/netmon" "tailscale.com/net/netutil" @@ -376,64 +375,7 @@ This is my Tailscale device. Your device is %v. } func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Request) { - // http.Errors only useful if hitting endpoint manually - // otherwise rely on log lines when debugging ingress connections - // as connection is hijacked for bidi and is encrypted tls - if !h.canIngress() { - h.logf("ingress: denied; no ingress cap from %v", h.remoteAddr) - http.Error(w, "denied; no ingress cap", http.StatusForbidden) - return - } - logAndError := func(code int, publicMsg string) { - h.logf("ingress: bad request from %v: %s", h.remoteAddr, publicMsg) - http.Error(w, publicMsg, http.StatusMethodNotAllowed) - } - bad := func(publicMsg string) { - logAndError(http.StatusBadRequest, publicMsg) - } - if r.Method != "POST" { - logAndError(http.StatusMethodNotAllowed, "only POST allowed") - return - } - srcAddrStr := r.Header.Get("Tailscale-Ingress-Src") - if srcAddrStr == "" { - bad("Tailscale-Ingress-Src header not set") - return - } - srcAddr, err := netip.ParseAddrPort(srcAddrStr) - if err != nil { - bad("Tailscale-Ingress-Src header invalid; want ip:port") - return - } - target := ipn.HostPort(r.Header.Get("Tailscale-Ingress-Target")) - if target == "" { - bad("Tailscale-Ingress-Target header not set") - return - } - if _, _, err := net.SplitHostPort(string(target)); err != nil { - bad("Tailscale-Ingress-Target header invalid; want host:port") - return - } - - getConnOrReset := func() (net.Conn, bool) { - conn, _, err := w.(http.Hijacker).Hijack() - if err != nil { - h.logf("ingress: failed hijacking conn") - http.Error(w, "failed hijacking conn", http.StatusInternalServerError) - return nil, false - } - io.WriteString(conn, "HTTP/1.1 101 Switching Protocols\r\n\r\n") - return &ipn.FunnelConn{ - Conn: conn, - Src: srcAddr, - Target: target, - }, true - } - sendRST := func() { - http.Error(w, "denied", http.StatusForbidden) - } - - h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConnOrReset, sendRST) + http.Error(w, "not implemented", http.StatusNotImplemented) } func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) { diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go deleted file mode 100644 index c144fa529..000000000 --- a/ipn/ipnlocal/serve.go +++ /dev/null @@ -1,937 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package ipnlocal - -import ( - "context" - "crypto/sha256" - "crypto/tls" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "mime" - "net" - "net/http" - "net/http/httputil" - "net/netip" - "net/url" - "os" - "path" - "slices" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - "unicode/utf8" - - "golang.org/x/net/http2" - "tailscale.com/ipn" - "tailscale.com/logtail/backoff" - "tailscale.com/net/netutil" - "tailscale.com/syncs" - "tailscale.com/tailcfg" - "tailscale.com/types/lazy" - "tailscale.com/types/logger" - "tailscale.com/util/ctxkey" - "tailscale.com/util/mak" - "tailscale.com/version" -) - -const ( - contentTypeHeader = "Content-Type" - grpcBaseContentType = "application/grpc" -) - -// ErrETagMismatch signals that the given -// If-Match header does not match with the -// current etag of a resource. -var ErrETagMismatch = errors.New("etag mismatch") - -var serveHTTPContextKey ctxkey.Key[*serveHTTPContext] - -type serveHTTPContext struct { - SrcAddr netip.AddrPort - DestPort uint16 - - // provides funnel-specific context, nil if not funneled - Funnel *funnelFlow -} - -// funnelFlow represents a funneled connection initiated via IngressPeer -// to Host. -type funnelFlow struct { - Host string - IngressPeer tailcfg.NodeView -} - -// localListener is the state of host-level net.Listen for a specific (Tailscale IP, port) -// combination. If there are two TailscaleIPs (v4 and v6) and three ports being served, -// then there will be six of these active and looping in their Run method. -// -// This is not used in userspace-networking mode. -// -// localListener is used by tailscale serve (TCP only), the built-in web client and Taildrive. -// Most serve traffic and peer traffic for the web client are intercepted by netstack. -// This listener exists purely for connections from the machine itself, as that goes via the kernel, -// so we need to be in the kernel's listening/routing tables. -type localListener struct { - b *LocalBackend - ap netip.AddrPort - ctx context.Context // valid while listener is desired - cancel context.CancelFunc // for ctx, to close listener - logf logger.Logf - bo *backoff.Backoff // for retrying failed Listen calls - - handler func(net.Conn) error // handler for inbound connections - closeListener syncs.AtomicValue[func() error] // Listener's Close method, if any -} - -func (b *LocalBackend) newServeListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *localListener { - ctx, cancel := context.WithCancel(ctx) - return &localListener{ - b: b, - ap: ap, - ctx: ctx, - cancel: cancel, - logf: logf, - - handler: func(conn net.Conn) error { - srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort() - handler := b.tcpHandlerForServe(ap.Port(), srcAddr, nil) - if handler == nil { - b.logf("[unexpected] local-serve: no handler for %v to port %v", srcAddr, ap.Port()) - conn.Close() - return nil - } - return handler(conn) - }, - bo: backoff.NewBackoff("serve-listener", logf, 30*time.Second), - } - -} - -// Close cancels the context and closes the listener, if any. -func (s *localListener) Close() error { - s.cancel() - if close, ok := s.closeListener.LoadOk(); ok { - s.closeListener.Store(nil) - close() - } - return nil -} - -// Run starts a net.Listen for the localListener's address and port. -// If unable to listen, it retries with exponential backoff. -// Listen is retried until the context is canceled. -func (s *localListener) Run() { - for { - ip := s.ap.Addr() - ipStr := ip.String() - - var lc net.ListenConfig - if initListenConfig != nil { - // On macOS, this sets the lc.Control hook to - // setsockopt the interface index to bind to. This is - // required by the network sandbox to allow binding to - // a specific interface. Without this hook, the system - // chooses a default interface to bind to. - if err := initListenConfig(&lc, ip, s.b.prevIfState, s.b.dialer.TUNName()); err != nil { - s.logf("localListener failed to init listen config %v, backing off: %v", s.ap, err) - s.bo.BackOff(s.ctx, err) - continue - } - // On macOS (AppStore or macsys) and if we're binding to a privileged port, - if version.IsSandboxedMacOS() && s.ap.Port() < 1024 { - // On macOS, we need to bind to ""/all-interfaces due to - // the network sandbox. Ideally we would only bind to the - // Tailscale interface, but macOS errors out if we try to - // to listen on privileged ports binding only to a specific - // interface. (#6364) - ipStr = "" - } - } - - tcp4or6 := "tcp4" - if ip.Is6() { - tcp4or6 = "tcp6" - } - - // while we were backing off and trying again, the context got canceled - // so don't bind, just return, because otherwise there will be no way - // to close this listener - if s.ctx.Err() != nil { - s.logf("localListener context closed before binding") - return - } - - ln, err := lc.Listen(s.ctx, tcp4or6, net.JoinHostPort(ipStr, fmt.Sprint(s.ap.Port()))) - if err != nil { - if s.shouldWarnAboutListenError(err) { - s.logf("localListener failed to listen on %v, backing off: %v", s.ap, err) - } - s.bo.BackOff(s.ctx, err) - continue - } - s.closeListener.Store(ln.Close) - - s.logf("listening on %v", s.ap) - err = s.handleListenersAccept(ln) - if s.ctx.Err() != nil { - // context canceled, we're done - return - } - if err != nil { - s.logf("localListener accept error, retrying: %v", err) - } - } -} - -func (s *localListener) shouldWarnAboutListenError(err error) bool { - if !s.b.sys.NetMon.Get().InterfaceState().HasIP(s.ap.Addr()) { - // Machine likely doesn't have IPv6 enabled (or the IP is still being - // assigned). No need to warn. Notably, WSL2 (Issue 6303). - return false - } - // TODO(bradfitz): check errors.Is(err, syscall.EADDRNOTAVAIL) etc? Let's - // see what happens in practice. - return true -} - -// handleListenersAccept accepts connections for the Listener. It calls the -// handler in a new goroutine for each accepted connection. This is used to -// handle local "tailscale serve" and web client traffic originating from the -// machine itself. -func (s *localListener) handleListenersAccept(ln net.Listener) error { - for { - conn, err := ln.Accept() - if err != nil { - return err - } - go s.handler(conn) - } -} - -// updateServeTCPPortNetMapAddrListenersLocked starts a net.Listen for configured -// Serve ports on all the node's addresses. -// Existing Listeners are closed if port no longer in incoming ports list. -// -// b.mu must be held. -func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint16) { - // close existing listeners where port - // is no longer in incoming ports list - for ap, sl := range b.serveListeners { - if !slices.Contains(ports, ap.Port()) { - b.logf("closing listener %v", ap) - sl.Close() - delete(b.serveListeners, ap) - } - } - - nm := b.netMap - if nm == nil { - b.logf("netMap is nil") - return - } - if !nm.SelfNode.Valid() { - b.logf("netMap SelfNode is nil") - return - } - - addrs := nm.GetAddresses() - for _, a := range addrs.All() { - for _, p := range ports { - addrPort := netip.AddrPortFrom(a.Addr(), p) - if _, ok := b.serveListeners[addrPort]; ok { - continue // already listening - } - - sl := b.newServeListener(context.Background(), addrPort, b.logf) - mak.Set(&b.serveListeners, addrPort, sl) - - go sl.Run() - } - } -} - -// SetServeConfig establishes or replaces the current serve config. -// ETag is an optional parameter to enforce Optimistic Concurrency Control. -// If it is an empty string, then the config will be overwritten. -func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig, etag string) error { - b.mu.Lock() - defer b.mu.Unlock() - return b.setServeConfigLocked(config, etag) -} - -func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string) error { - prefs := b.pm.CurrentPrefs() - if config.IsFunnelOn() && prefs.ShieldsUp() { - return errors.New("Unable to turn on Funnel while shields-up is enabled") - } - if b.isConfigLocked_Locked() { - return errors.New("can't reconfigure tailscaled when using a config file; config file is locked") - } - - nm := b.netMap - if nm == nil { - return errors.New("netMap is nil") - } - if !nm.SelfNode.Valid() { - return errors.New("netMap SelfNode is nil") - } - - // If etag is present, check that it has - // not changed from the last config. - prevConfig := b.serveConfig - if etag != "" { - // Note that we marshal b.serveConfig - // and not use b.lastServeConfJSON as that might - // be a Go nil value, which produces a different - // checksum from a JSON "null" value. - prevBytes, err := json.Marshal(prevConfig) - if err != nil { - return fmt.Errorf("error encoding previous config: %w", err) - } - sum := sha256.Sum256(prevBytes) - previousEtag := hex.EncodeToString(sum[:]) - if etag != previousEtag { - return ErrETagMismatch - } - } - - var bs []byte - if config != nil { - j, err := json.Marshal(config) - if err != nil { - return fmt.Errorf("encoding serve config: %w", err) - } - bs = j - } - - profileID := b.pm.CurrentProfile().ID - confKey := ipn.ServeConfigKey(profileID) - if err := b.store.WriteState(confKey, bs); err != nil { - return fmt.Errorf("writing ServeConfig to StateStore: %w", err) - } - - b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) - - // clean up and close all previously open foreground sessions - // if the current ServeConfig has overwritten them. - if prevConfig.Valid() { - has := func(string) bool { return false } - if b.serveConfig.Valid() { - has = b.serveConfig.Foreground().Contains - } - for k := range prevConfig.Foreground().All() { - if !has(k) { - for _, sess := range b.notifyWatchers { - if sess.sessionID == k { - sess.cancel() - } - } - } - } - } - - return nil -} - -// ServeConfig provides a view of the current serve mappings. -// If serving is not configured, the returned view is not Valid. -func (b *LocalBackend) ServeConfig() ipn.ServeConfigView { - b.mu.Lock() - defer b.mu.Unlock() - return b.serveConfig -} - -// DeleteForegroundSession deletes a ServeConfig's foreground session -// in the LocalBackend if it exists. It also ensures check, delete, and -// set operations happen within the same mutex lock to avoid any races. -func (b *LocalBackend) DeleteForegroundSession(sessionID string) error { - b.mu.Lock() - defer b.mu.Unlock() - if !b.serveConfig.Valid() || !b.serveConfig.Foreground().Contains(sessionID) { - return nil - } - sc := b.serveConfig.AsStruct() - delete(sc.Foreground, sessionID) - return b.setServeConfigLocked(sc, "") -} - -// HandleIngressTCPConn handles a TCP connection initiated by the ingressPeer -// proxied to the local node over the PeerAPI. -// Target represents the destination HostPort of the conn. -// srcAddr represents the source AddrPort and not that of the ingressPeer. -// getConnOrReset is a callback to get the connection, or reset if the connection -// is no longer available. -// sendRST is a callback to send a TCP RST to the ingressPeer indicating that -// the connection was not accepted. -func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) { - b.mu.Lock() - sc := b.serveConfig - b.mu.Unlock() - - // TODO(maisem,bradfitz): make this not alloc for every conn. - logf := logger.WithPrefix(b.logf, "handleIngress: ") - - if !sc.Valid() { - logf("got ingress conn w/o serveConfig; rejecting") - sendRST() - return - } - - if !sc.HasFunnelForTarget(target) { - logf("got ingress conn for unconfigured %q; rejecting", target) - sendRST() - return - } - - host, port, err := net.SplitHostPort(string(target)) - if err != nil { - logf("got ingress conn for bad target %q; rejecting", target) - sendRST() - return - } - port16, err := strconv.ParseUint(port, 10, 16) - if err != nil { - logf("got ingress conn for bad target %q; rejecting", target) - sendRST() - return - } - dport := uint16(port16) - if b.getTCPHandlerForFunnelFlow != nil { - handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport) - if handler != nil { - c, ok := getConnOrReset() - if !ok { - logf("getConn didn't complete from %v to port %v", srcAddr, dport) - return - } - handler(c) - return - } - } - handler := b.tcpHandlerForServe(dport, srcAddr, &funnelFlow{ - Host: host, - IngressPeer: ingressPeer, - }) - if handler == nil { - logf("[unexpected] no matching ingress serve handler for %v to port %v", srcAddr, dport) - sendRST() - return - } - c, ok := getConnOrReset() - if !ok { - logf("getConn didn't complete from %v to port %v", srcAddr, dport) - return - } - handler(c) -} - -// tcpHandlerForServe returns a handler for a TCP connection to be served via -// the ipn.ServeConfig. The funnelFlow can be nil if this is not a funneled -// connection. -func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort, f *funnelFlow) (handler func(net.Conn) error) { - b.mu.Lock() - sc := b.serveConfig - b.mu.Unlock() - - if !sc.Valid() { - return nil - } - - tcph, ok := sc.FindTCP(dport) - if !ok { - return nil - } - - if tcph.HTTPS() || tcph.HTTP() { - hs := &http.Server{ - Handler: http.HandlerFunc(b.serveWebHandler), - BaseContext: func(_ net.Listener) context.Context { - return serveHTTPContextKey.WithValue(context.Background(), &serveHTTPContext{ - Funnel: f, - SrcAddr: srcAddr, - DestPort: dport, - }) - }, - } - if tcph.HTTPS() { - hs.TLSConfig = &tls.Config{ - GetCertificate: b.getTLSServeCertForPort(dport), - } - return func(c net.Conn) error { - return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "") - } - } - - return func(c net.Conn) error { - return hs.Serve(netutil.NewOneConnListener(c, nil)) - } - } - - if backDst := tcph.TCPForward(); backDst != "" { - return func(conn net.Conn) error { - defer conn.Close() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst) - cancel() - if err != nil { - b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err) - return nil - } - defer backConn.Close() - if sni := tcph.TerminateTLS(); sni != "" { - conn = tls.Server(conn, &tls.Config{ - GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - pair, err := b.GetCertPEM(ctx, sni) - if err != nil { - return nil, err - } - cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM) - if err != nil { - return nil, err - } - return &cert, nil - }, - }) - } - - // TODO(bradfitz): do the RegisterIPPortIdentity and - // UnregisterIPPortIdentity stuff that netstack does - errc := make(chan error, 1) - go func() { - _, err := io.Copy(backConn, conn) - errc <- err - }() - go func() { - _, err := io.Copy(conn, backConn) - errc <- err - }() - return <-errc - } - } - - return nil -} - -func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) { - var z ipn.HTTPHandlerView // zero value - - hostname := r.Host - if r.TLS == nil { - tcd := "." + b.Status().CurrentTailnet.MagicDNSSuffix - if host, _, err := net.SplitHostPort(hostname); err == nil { - hostname = host - } - if !strings.HasSuffix(hostname, tcd) { - hostname += tcd - } - } else { - hostname = r.TLS.ServerName - } - - sctx, ok := serveHTTPContextKey.ValueOk(r.Context()) - if !ok { - b.logf("[unexpected] localbackend: no serveHTTPContext in request") - return z, "", false - } - wsc, ok := b.webServerConfig(hostname, sctx.DestPort) - if !ok { - return z, "", false - } - - if h, ok := wsc.Handlers().GetOk(r.URL.Path); ok { - return h, r.URL.Path, true - } - pth := path.Clean(r.URL.Path) - for { - withSlash := pth + "/" - if h, ok := wsc.Handlers().GetOk(withSlash); ok { - return h, withSlash, true - } - if h, ok := wsc.Handlers().GetOk(pth); ok { - return h, pth, true - } - if pth == "/" { - return z, "", false - } - pth = path.Dir(pth) - } -} - -// proxyHandlerForBackend creates a new HTTP reverse proxy for a particular backend that -// we serve requests for. `backend` is a HTTPHandler.Proxy string (url, hostport or just port). -func (b *LocalBackend) proxyHandlerForBackend(backend string) (http.Handler, error) { - targetURL, insecure := expandProxyArg(backend) - u, err := url.Parse(targetURL) - if err != nil { - return nil, fmt.Errorf("invalid url %s: %w", targetURL, err) - } - p := &reverseProxy{ - logf: b.logf, - url: u, - insecure: insecure, - backend: backend, - lb: b, - } - return p, nil -} - -// reverseProxy is a proxy that forwards a request to a backend host -// (preconfigured via ipn.ServeConfig). If the host is configured with -// http+insecure prefix, connection between proxy and backend will be over -// insecure TLS. If the backend host has a http prefix and the incoming request -// has application/grpc content type header, the connection will be over h2c. -// Otherwise standard Go http transport will be used. -type reverseProxy struct { - logf logger.Logf - url *url.URL - // insecure tracks whether the connection to an https backend should be - // insecure (i.e because we cannot verify its CA). - insecure bool - backend string - lb *LocalBackend - httpTransport lazy.SyncValue[*http.Transport] // transport for non-h2c backends - h2cTransport lazy.SyncValue[*http2.Transport] // transport for h2c backends - // closed tracks whether proxy is closed/currently closing. - closed atomic.Bool -} - -// close ensures that any open backend connections get closed. -func (rp *reverseProxy) close() { - rp.closed.Store(true) - if h2cT := rp.h2cTransport.Get(func() *http2.Transport { - return nil - }); h2cT != nil { - h2cT.CloseIdleConnections() - } - if httpTransport := rp.httpTransport.Get(func() *http.Transport { - return nil - }); httpTransport != nil { - httpTransport.CloseIdleConnections() - } -} - -func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if closed := rp.closed.Load(); closed { - rp.logf("received a request for a proxy that's being closed or has been closed") - http.Error(w, "proxy is closed", http.StatusServiceUnavailable) - return - } - p := &httputil.ReverseProxy{Rewrite: func(r *httputil.ProxyRequest) { - oldOutPath := r.Out.URL.Path - r.SetURL(rp.url) - - // If mount point matches the request path exactly, the outbound - // request URL was set to empty string in serveWebHandler which - // would have resulted in the outbound path set to - // + '/' in SetURL. In that case, if the proxy path was set, we - // want to send the request to the (without the - // '/') . - if oldOutPath == "" && rp.url.Path != "" { - r.Out.URL.Path = rp.url.Path - r.Out.URL.RawPath = rp.url.RawPath - } - - r.Out.Host = r.In.Host - addProxyForwardedHeaders(r) - rp.lb.addTailscaleIdentityHeaders(r) - }} - - // There is no way to autodetect h2c as per RFC 9113 - // https://datatracker.ietf.org/doc/html/rfc9113#name-starting-http-2. - // However, we assume that http:// proxy prefix in combination with the - // protoccol being HTTP/2 is sufficient to detect h2c for our needs. Only use this for - // gRPC to fix a known problem of plaintext gRPC backends - if rp.shouldProxyViaH2C(r) { - rp.logf("received a proxy request for plaintext gRPC") - p.Transport = rp.getH2CTransport() - } else { - p.Transport = rp.getTransport() - } - p.ServeHTTP(w, r) -} - -// getTransport returns the Transport used for regular (non-GRPC) requests -// to the backend. The Transport gets created lazily, at most once. -func (rp *reverseProxy) getTransport() *http.Transport { - return rp.httpTransport.Get(func() *http.Transport { - return &http.Transport{ - DialContext: rp.lb.dialer.SystemDial, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: rp.insecure, - }, - // Values for the following parameters have been copied from http.DefaultTransport. - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - }) -} - -// getH2CTransport returns the Transport used for GRPC requests to the backend. -// The Transport gets created lazily, at most once. -func (rp *reverseProxy) getH2CTransport() *http2.Transport { - return rp.h2cTransport.Get(func() *http2.Transport { - return &http2.Transport{ - AllowHTTP: true, - DialTLSContext: func(ctx context.Context, network string, addr string, _ *tls.Config) (net.Conn, error) { - return rp.lb.dialer.SystemDial(ctx, "tcp", rp.url.Host) - }, - } - }) -} - -// This is not a generally reliable way how to determine whether a request is -// for a h2c server, but sufficient for our particular use case. -func (rp *reverseProxy) shouldProxyViaH2C(r *http.Request) bool { - contentType := r.Header.Get(contentTypeHeader) - return r.ProtoMajor == 2 && strings.HasPrefix(rp.backend, "http://") && isGRPCContentType(contentType) -} - -// isGRPC accepts an HTTP request's content type header value and determines -// whether this is gRPC content. grpc-go considers a value that equals -// application/grpc or has a prefix of application/grpc+ or application/grpc; a -// valid grpc content type header. -// https://github.com/grpc/grpc-go/blob/v1.60.0-dev/internal/grpcutil/method.go#L41-L78 -func isGRPCContentType(contentType string) bool { - s, ok := strings.CutPrefix(contentType, grpcBaseContentType) - return ok && (len(s) == 0 || s[0] == '+' || s[0] == ';') -} - -func addProxyForwardedHeaders(r *httputil.ProxyRequest) { - r.Out.Header.Set("X-Forwarded-Host", r.In.Host) - if r.In.TLS != nil { - r.Out.Header.Set("X-Forwarded-Proto", "https") - } - if c, ok := serveHTTPContextKey.ValueOk(r.Out.Context()); ok { - r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String()) - } -} - -func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) { - // Clear any incoming values squatting in the headers. - r.Out.Header.Del("Tailscale-User-Login") - r.Out.Header.Del("Tailscale-User-Name") - r.Out.Header.Del("Tailscale-User-Profile-Pic") - r.Out.Header.Del("Tailscale-Funnel-Request") - r.Out.Header.Del("Tailscale-Headers-Info") - - c, ok := serveHTTPContextKey.ValueOk(r.Out.Context()) - if !ok { - return - } - if c.Funnel != nil { - r.Out.Header.Set("Tailscale-Funnel-Request", "?1") - return - } - node, user, ok := b.WhoIs("tcp", c.SrcAddr) - if !ok { - return // traffic from outside of Tailnet (funneled or local machine) - } - if node.IsTagged() { - // 2023-06-14: Not setting identity headers for tagged nodes. - // Only currently set for nodes with user identities. - return - } - r.Out.Header.Set("Tailscale-User-Login", encTailscaleHeaderValue(user.LoginName)) - r.Out.Header.Set("Tailscale-User-Name", encTailscaleHeaderValue(user.DisplayName)) - r.Out.Header.Set("Tailscale-User-Profile-Pic", user.ProfilePicURL) - r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers") -} - -// encTailscaleHeaderValue cleans or encodes as necessary v, to be suitable in -// an HTTP header value. See -// https://github.com/tailscale/tailscale/issues/11603. -// -// If v is not a valid UTF-8 string, it returns an empty string. -// If v is a valid ASCII string, it returns v unmodified. -// If v is a valid UTF-8 string with non-ASCII characters, it returns a -// RFC 2047 Q-encoded string. -func encTailscaleHeaderValue(v string) string { - if !utf8.ValidString(v) { - return "" - } - return mime.QEncoding.Encode("utf-8", v) -} - -// serveWebHandler is an http.HandlerFunc that maps incoming requests to the -// correct *http. -func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) { - h, mountPoint, ok := b.getServeHandler(r) - if !ok { - http.NotFound(w, r) - return - } - if s := h.Text(); s != "" { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - io.WriteString(w, s) - return - } - if v := h.Path(); v != "" { - b.serveFileOrDirectory(w, r, v, mountPoint) - return - } - if v := h.Proxy(); v != "" { - p, ok := b.serveProxyHandlers.Load(v) - if !ok { - http.Error(w, "unknown proxy destination", http.StatusInternalServerError) - return - } - h := p.(http.Handler) - // Trim the mount point from the URL path before proxying. (#6571) - if r.URL.Path != "/" { - h = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), h) - } - h.ServeHTTP(w, r) - return - } - - http.Error(w, "empty handler", 500) -} - -func (b *LocalBackend) serveFileOrDirectory(w http.ResponseWriter, r *http.Request, fileOrDir, mountPoint string) { - fi, err := os.Stat(fileOrDir) - if err != nil { - if os.IsNotExist(err) { - http.NotFound(w, r) - return - } - b.logf("error calling stat on %s: %v", fileOrDir, err) - http.Error(w, "an error occurred reading the file or directory", 500) - return - } - if fi.Mode().IsRegular() { - if mountPoint != r.URL.Path { - http.NotFound(w, r) - return - } - f, err := os.Open(fileOrDir) - if err != nil { - b.logf("error opening %s: %v", fileOrDir, err) - http.Error(w, "an error occurred reading the file or directory", 500) - return - } - defer f.Close() - http.ServeContent(w, r, path.Base(mountPoint), fi.ModTime(), f) - return - } - if !fi.IsDir() { - http.Error(w, "not a file or directory", 500) - return - } - if len(r.URL.Path) < len(mountPoint) && r.URL.Path+"/" == mountPoint { - http.Redirect(w, r, mountPoint, http.StatusFound) - return - } - - var fs http.Handler = http.FileServer(http.Dir(fileOrDir)) - if mountPoint != "/" { - fs = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), fs) - } - fs.ServeHTTP(&fixLocationHeaderResponseWriter{ - ResponseWriter: w, - mountPoint: mountPoint, - }, r) -} - -// fixLocationHeaderResponseWriter is an http.ResponseWriter wrapper that, upon -// flushing HTTP headers, prefixes any Location header with the mount point. -type fixLocationHeaderResponseWriter struct { - http.ResponseWriter - mountPoint string - fixOnce sync.Once // guards call to fix -} - -func (w *fixLocationHeaderResponseWriter) fix() { - h := w.ResponseWriter.Header() - if v := h.Get("Location"); v != "" { - h.Set("Location", w.mountPoint+v) - } -} - -func (w *fixLocationHeaderResponseWriter) WriteHeader(code int) { - w.fixOnce.Do(w.fix) - w.ResponseWriter.WriteHeader(code) -} - -func (w *fixLocationHeaderResponseWriter) Write(p []byte) (int, error) { - w.fixOnce.Do(w.fix) - return w.ResponseWriter.Write(p) -} - -// expandProxyArg returns a URL from s, where s can be of form: -// -// * port number ("8080") -// * host:port ("localhost:8080") -// * full URL ("http://localhost:8080", in which case it's returned unchanged) -// * insecure TLS ("https+insecure://127.0.0.1:4430") -func expandProxyArg(s string) (targetURL string, insecureSkipVerify bool) { - if s == "" { - return "", false - } - if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") { - return s, false - } - if rest, ok := strings.CutPrefix(s, "https+insecure://"); ok { - return "https://" + rest, true - } - if allNumeric(s) { - return "http://127.0.0.1:" + s, false - } - return "http://" + s, false -} - -func allNumeric(s string) bool { - for i := range len(s) { - if s[i] < '0' || s[i] > '9' { - return false - } - } - return s != "" -} - -func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebServerConfigView, ok bool) { - key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port)) - - b.mu.Lock() - defer b.mu.Unlock() - - if !b.serveConfig.Valid() { - return c, false - } - return b.serveConfig.FindWeb(key) -} - -func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { - return func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { - if hi == nil || hi.ServerName == "" { - return nil, errors.New("no SNI ServerName") - } - _, ok := b.webServerConfig(hi.ServerName, port) - if !ok { - return nil, errors.New("no webserver configured for name/port") - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - pair, err := b.GetCertPEM(ctx, hi.ServerName) - if err != nil { - return nil, err - } - cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM) - if err != nil { - return nil, err - } - return &cert, nil - } -} diff --git a/ipn/ipnlocal/web_client.go b/ipn/ipnlocal/web_client.go index 37fc31819..978f20fc7 100644 --- a/ipn/ipnlocal/web_client.go +++ b/ipn/ipnlocal/web_client.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build !ios && !android +//go:build !ios && !android && !ts_omit_webclient package ipnlocal diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index df9993ef0..d1b69fad4 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -8,8 +8,6 @@ import ( "bytes" "cmp" "context" - "crypto/sha256" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -118,7 +116,6 @@ var handler = map[string]localAPIHandler{ "query-feature": (*Handler).serveQueryFeature, "reload-config": (*Handler).reloadConfig, "reset-auth": (*Handler).serveResetAuth, - "serve-config": (*Handler).serveServeConfig, "set-dns": (*Handler).serveSetDNS, "set-expiry-sooner": (*Handler).serveSetExpirySooner, "set-gui-visible": (*Handler).serveSetGUIVisible, @@ -1028,58 +1025,6 @@ func (h *Handler) serveResetAuth(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - if !h.PermitRead { - http.Error(w, "serve config denied", http.StatusForbidden) - return - } - config := h.b.ServeConfig() - bts, err := json.Marshal(config) - if err != nil { - http.Error(w, "error encoding config: "+err.Error(), http.StatusInternalServerError) - return - } - sum := sha256.Sum256(bts) - etag := hex.EncodeToString(sum[:]) - w.Header().Set("Etag", etag) - w.Header().Set("Content-Type", "application/json") - w.Write(bts) - case "POST": - if !h.PermitWrite { - http.Error(w, "serve config denied", http.StatusForbidden) - return - } - configIn := new(ipn.ServeConfig) - if err := json.NewDecoder(r.Body).Decode(configIn); err != nil { - writeErrorJSON(w, fmt.Errorf("decoding config: %w", err)) - return - } - - // require a local admin when setting a path handler - // TODO: roll-up this Windows-specific check into either PermitWrite - // or a global admin escalation check. - if err := authorizeServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h); err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - - etag := r.Header.Get("If-Match") - if err := h.b.SetServeConfig(configIn, etag); err != nil { - if errors.Is(err, ipnlocal.ErrETagMismatch) { - http.Error(w, err.Error(), http.StatusPreconditionFailed) - return - } - writeErrorJSON(w, fmt.Errorf("updating config: %w", err)) - return - } - w.WriteHeader(http.StatusOK) - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - } -} - func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) error { switch goos { case "windows", "linux", "darwin", "illumos", "solaris":