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 <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-01-10 21:12:16 -08:00
parent 5b4d3207b0
commit 55587d3156
8 changed files with 17 additions and 1326 deletions

View File

@ -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

View File

@ -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+

View File

@ -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)

View File

@ -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 (

View File

@ -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) {

View File

@ -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 <proxy path>
// + '/' in SetURL. In that case, if the proxy path was set, we
// want to send the request to the <proxy path> (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
}
}

View File

@ -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

View File

@ -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":