mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-10 06:36:14 +02:00
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:
parent
5b4d3207b0
commit
55587d3156
2
Makefile
2
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
|
||||
|
||||
@ -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+
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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":
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user