This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+199 -99
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_serve
@@ -35,11 +35,13 @@ import (
"github.com/pires/go-proxyproto"
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/netmon"
"tailscale.com/net/netutil"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tailcfg/peercap"
"tailscale.com/types/lazy"
"tailscale.com/types/logger"
"tailscale.com/types/views"
@@ -48,6 +50,7 @@ import (
"tailscale.com/util/ctxkey"
"tailscale.com/util/mak"
"tailscale.com/util/slicesx"
"tailscale.com/util/usermetric"
"tailscale.com/version"
)
@@ -91,7 +94,7 @@ type serveHTTPContext struct {
// provides funnel-specific context, nil if not funneled
Funnel *funnelFlow
// AppCapabilities lists all PeerCapabilities that should be forwarded by serve
AppCapabilities views.Slice[tailcfg.PeerCapability]
AppCapabilities views.Slice[peercap.Cap]
}
// funnelFlow represents a funneled connection initiated via IngressPeer
@@ -121,8 +124,22 @@ type localListener struct {
handler func(net.Conn) error // handler for inbound connections
closeListener syncs.AtomicValue[func() error] // Listener's Close method, if any
// allowAllInterfaces, if set, stops the listener from being bound to the
// Tailscale interface, so it answers traffic arriving on any interface.
// Only serve listeners set it, and only when serveAllowAllInterfaces is
// set; the web client listener always stays bound.
allowAllInterfaces bool
}
// serveAllowAllInterfaces, when set, stops serve's kernel listeners from
// being bound to the Tailscale interface, so they answer traffic arriving on
// any interface. It applies only to serve listeners, not the web client
// listener. This re-exposes serve to the local network; it is set only by the
// Kubernetes operator's experimental cluster-traffic ingress feature, which
// forwards cluster traffic to the node's Tailscale IP via another interface.
var serveAllowAllInterfaces = envknob.RegisterBool("TS_SERVE_ALLOW_ALL_INTERFACES")
func (b *LocalBackend) newServeListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *localListener {
ctx, cancel := context.WithCancel(ctx)
return &localListener{
@@ -132,6 +149,8 @@ func (b *LocalBackend) newServeListener(ctx context.Context, ap netip.AddrPort,
cancel: cancel,
logf: logf,
allowAllInterfaces: serveAllowAllInterfaces(),
handler: func(conn net.Conn) error {
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
handler := b.tcpHandlerForServe(ap.Port(), srcAddr, nil)
@@ -166,7 +185,7 @@ func (s *localListener) Run() {
ipStr := ip.String()
var lc net.ListenConfig
if initListenConfig != nil {
if initListenConfig != nil && !s.allowAllInterfaces {
ifIndex, err := netmon.TailscaleInterfaceIndex()
if err != nil {
s.logf("localListener failed to get Tailscale interface index %v, backing off: %v", s.ap, err)
@@ -220,14 +239,13 @@ func (s *localListener) Run() {
s.closeListener.Store(ln.Close)
s.logf("listening on %v", s.ap)
// handleListenersAccept always returns a non-nil error.
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)
}
s.logf("localListener accept error, retrying: %v", err)
}
}
@@ -276,7 +294,7 @@ func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint1
}
}
nm := b.NetMap()
nm := b.NetMapNoPeers()
if nm == nil {
b.logf("netMap is nil")
return
@@ -333,7 +351,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
}
nm := b.NetMap()
nm := b.NetMapNoPeers()
if nm == nil {
return errors.New("netMap is nil")
}
@@ -534,6 +552,56 @@ func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcf
return servicesList
}
type serviceMeteredConn struct {
net.Conn
inbound, outbound *usermetric.MultiLabelMap[serveLabels]
key serveLabels
}
func (c *serviceMeteredConn) Read(p []byte) (int, error) {
n, err := c.Conn.Read(p)
if n > 0 {
c.inbound.Add(c.key, int64(n))
}
return n, err
}
func (c *serviceMeteredConn) Write(p []byte) (int, error) {
n, err := c.Conn.Write(p)
if n > 0 {
c.outbound.Add(c.key, int64(n))
}
return n, err
}
// CloseWrite forwards a write-half close to the underlying conn. We only embed
// the net.Conn interface, which would otherwise hide the underlying conn's
// CloseWrite; net/http's server relies on it (closeWriteAndWait) to send a FIN
// and drain gracefully when a connection won't be reused, avoiding a truncating
// RST on the final response.
func (c *serviceMeteredConn) CloseWrite() error {
if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
// meteredConnForService wraps c to count peer bytes against the per-Service
// Serve counters. The per-Service series is never evicted, so it leaks
// (intentionally) until tailscaled exits.
func (b *LocalBackend) meteredConnForService(c net.Conn, svc tailcfg.ServiceName) net.Conn {
// Plain (non-Service) serve passes an empty svc; don't meter it.
if svc == "" || b.metrics.serveBytesInbound == nil || b.metrics.serveBytesOutbound == nil {
return c
}
return &serviceMeteredConn{
Conn: c,
inbound: b.metrics.serveBytesInbound,
outbound: b.metrics.serveBytesOutbound,
key: serveLabels{Service: svc.String()},
}
}
// tcpHandlerForVIPService returns a handler for a TCP connection to a VIP service
// that is being served via the ipn.ServeConfig. It returns nil if the destination
// address is not a VIP service or if the VIP service does not have a TCP handler set.
@@ -560,68 +628,14 @@ func (b *LocalBackend) tcpHandlerForVIPService(dstAddr, srcAddr netip.AddrPort)
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{
SrcAddr: srcAddr,
ForVIPService: dstSvc,
DestPort: dport,
})
},
}
if tcph.HTTPS() {
// TODO(kevinliang10): just leaving this TLS cert creation as if we don't have other
// hostnames, but for services this getTLSServeCetForPort will need a version that also take
// in the hostname. How to store the TLS cert is still being discussed.
hs.TLSConfig = &tls.Config{
GetCertificate: b.getTLSServeCertForPort(dport, dstSvc),
}
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
},
})
}
return b.forwardTCPWithProxyProtocol(conn, backConn, tcph.ProxyProtocol(), srcAddr, dport, backDst)
}
}
return nil
// TODO(kevinliang10): just leaving this TLS cert creation as if we don't have other
// hostnames, but for services this getTLSServeCetForPort will need a version that also take
// in the hostname. How to store the TLS cert is still being discussed.
return b.tcpHandlerForServeTCP(tcph, dport, srcAddr, &serveHTTPContext{
SrcAddr: srcAddr,
ForVIPService: dstSvc,
DestPort: dport,
}, dstSvc)
}
// tcpHandlerForServe returns a handler for a TCP connection to be served via
@@ -641,27 +655,32 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort,
return nil
}
return b.tcpHandlerForServeTCP(tcph, dport, srcAddr, &serveHTTPContext{
Funnel: f,
SrcAddr: srcAddr,
DestPort: dport,
}, "")
}
func (b *LocalBackend) tcpHandlerForServeTCP(tcph ipn.TCPPortHandlerView, dport uint16, srcAddr netip.AddrPort, httpCtx *serveHTTPContext, forVIPService tailcfg.ServiceName) func(net.Conn) error {
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,
})
c := *httpCtx
return serveHTTPContextKey.WithValue(context.Background(), &c)
},
}
if tcph.HTTPS() {
hs.TLSConfig = &tls.Config{
GetCertificate: b.getTLSServeCertForPort(dport, ""),
}
hs.TLSConfig = b.serveTLSConfig(b.getTLSServeCertForPort(dport, forVIPService), serveTLSNextProtos())
return func(c net.Conn) error {
c = b.meteredConnForService(c, forVIPService)
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
}
}
return func(c net.Conn) error {
c = b.meteredConnForService(c, forVIPService)
return hs.Serve(netutil.NewOneConnListener(c, nil))
}
}
@@ -669,8 +688,16 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort,
if backDst := tcph.TCPForward(); backDst != "" {
return func(conn net.Conn) error {
defer conn.Close()
conn = b.meteredConnForService(conn, forVIPService)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
var backConn net.Conn
var err error
if socketPath, ok := strings.CutPrefix(backDst, "unix:"); ok {
var d net.Dialer
backConn, err = d.DialContext(ctx, "unix", socketPath)
} else {
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)
@@ -678,21 +705,22 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort,
}
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
},
})
conn = tls.Server(conn, b.serveTLSConfig(func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if cert, ok := b.getACMETLSALPNCert(hi); ok {
return cert, nil
}
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
}, nil))
}
// TODO(bradfitz): do the RegisterIPPortIdentity and
@@ -710,7 +738,13 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort,
func (b *LocalBackend) forwardTCPWithProxyProtocol(conn, backConn net.Conn, proxyProtoVer int, srcAddr netip.AddrPort, dport uint16, backDst string) error {
var proxyHeader []byte
if proxyProtoVer > 0 {
backAddr := backConn.RemoteAddr().(*net.TCPAddr)
// PROXY protocol requires a valid TCP destination address.
// For Unix socket backends, RemoteAddr is *net.UnixAddr;
// the CLI rejects this combination, but guard here as well.
backAddr, ok := backConn.RemoteAddr().(*net.TCPAddr)
if !ok {
return fmt.Errorf("PROXY protocol is not supported with non-TCP backend %s", backDst)
}
// We always want to format the PROXY protocol header based on
// the IPv4 or IPv6-ness of the client. The SourceAddr and
@@ -814,6 +848,15 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
return h, r.URL.Path, true
}
pth := path.Clean(r.URL.Path)
// A well-formed origin-form request path is absolute. Malformed request
// targets — "*" (e.g. "GET *") and "" (e.g. "CONNECT" authority-form),
// clean to "*" and "." respectively. Those are path.Dir fixed points that
// never equal "/" and match no mount, so without this guard the loop below
// would spin forever on one CPU core (a remote DoS via serve, or via funnel
// from the internet).
if !strings.HasPrefix(pth, "/") {
return z, "", false
}
for {
withSlash := pth + "/"
if h, ok := wsc.Handlers().GetOk(withSlash); ok {
@@ -825,7 +868,13 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
if pth == "/" {
return z, "", false
}
pth = path.Dir(pth)
// Belt-and-suspenders with the absolute-path check above: stop if
// path.Dir stops shrinking rather than assuming it always reaches "/".
if parent := path.Dir(pth); parent != pth {
pth = parent
} else {
return z, "", false
}
}
}
@@ -835,8 +884,8 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (http.Handler, err
targetURL, insecure := expandProxyArg(backend)
// Handle unix: scheme specially
if strings.HasPrefix(targetURL, "unix:") {
socketPath := strings.TrimPrefix(targetURL, "unix:")
if after, ok := strings.CutPrefix(targetURL, "unix:"); ok {
socketPath := after
if socketPath == "" {
return nil, fmt.Errorf("empty unix socket path")
}
@@ -969,6 +1018,8 @@ func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// to the backend. The Transport gets created lazily, at most once.
func (rp *reverseProxy) getTransport() *http.Transport {
return rp.httpTransport.Get(func() *http.Transport {
// Zero preserves http.Transport's default MaxIdleConnsPerHost value.
maxIdleConnsPerHost, _ := envknob.LookupInt("TS_DEBUG_SERVE_MAX_IDLE_CONNS_PER_HOST")
dial := rp.lb.dialer.SystemDial
if rp.socketPath != "" {
dial = func(ctx context.Context, _, _ string) (net.Conn, error) {
@@ -985,6 +1036,7 @@ func (rp *reverseProxy) getTransport() *http.Transport {
// Values for the following parameters have been copied from http.DefaultTransport.
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
MaxIdleConnsPerHost: maxIdleConnsPerHost,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
@@ -1106,7 +1158,7 @@ func (b *LocalBackend) addAppCapabilitiesHeader(r *httputil.ProxyRequest) error
return nil
}
peerCapsFiltered := make(map[tailcfg.PeerCapability][]tailcfg.RawMessage, acceptCaps.Len())
peerCapsFiltered := make(map[peercap.Cap][]tailcfg.RawMessage, acceptCaps.Len())
for _, cap := range acceptCaps.AsSlice() {
if peerCaps.HasCapability(cap) {
peerCapsFiltered[cap] = peerCaps[cap]
@@ -1311,6 +1363,9 @@ func (b *LocalBackend) getTLSServeCertForPort(port uint16, forVIPService tailcfg
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
if cert, ok := b.getACMETLSALPNCert(hi); ok {
return cert, nil
}
_, ok := b.webServerConfig(hi.ServerName, forVIPService, port)
if !ok {
return nil, errors.New("no webserver configured for name/port")
@@ -1330,6 +1385,48 @@ func (b *LocalBackend) getTLSServeCertForPort(port uint16, forVIPService tailcfg
}
}
// serveTLSConfig returns the TLS configuration used by Serve and TCP-forwarded
// TLS listeners. nextProtos is the ALPN list to advertise for normal
// handshakes; it should be serveTLSNextProtos for HTTPS Serve and nil for
// TLS-terminated TCP forwarding where we don't know the backend protocol.
// During an ACME tls-alpn-01 renewal, GetConfigForClient clones the base config
// and temporarily prepends acme-tls/1, but only for the exact SNI with a pending
// challenge certificate. This keeps ordinary Serve traffic from advertising
// ACME support and lets Go's TLS stack negotiate the challenge protocol before
// GetCertificate is called.
func (b *LocalBackend) serveTLSConfig(getCert func(*tls.ClientHelloInfo) (*tls.Certificate, error), nextProtos []string) *tls.Config {
base := &tls.Config{
GetCertificate: getCert,
NextProtos: nextProtos,
}
base.GetConfigForClient = func(hi *tls.ClientHelloInfo) (*tls.Config, error) {
var nextProtos []string
if proto, ok := b.getACMETLSALPNProto(hi); ok {
b.logf("serve: accepting ACME tls-alpn-01 challenge for %q", hi.ServerName)
nextProtos = append(nextProtos, proto)
}
if len(nextProtos) == 0 {
return nil, nil
}
cfg := base.Clone()
cfg.NextProtos = append(nextProtos, base.NextProtos...)
return cfg, nil
}
return base
}
// HasFunnelForHostPort reports whether the LocalBackend's serve config
// has Funnel enabled for host:port.
func (b *LocalBackend) HasFunnelForHostPort(host string, port uint16) bool {
b.mu.Lock()
defer b.mu.Unlock()
if !b.serveConfig.Valid() {
return false
}
hp := ipn.HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
return b.serveConfig.HasFunnelForTarget(hp)
}
// 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.
@@ -1551,6 +1648,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
if err != nil {
b.lastServeConfJSON = mem.B(nil)
b.serveConfig = ipn.ServeConfigView{}
b.updateCertRefreshLoopLocked()
return
}
if b.lastServeConfJSON.Equal(mem.B(confj)) {
@@ -1561,6 +1659,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
if err := json.Unmarshal(confj, &conf); err != nil {
b.logf("invalid ServeConfig %q in StateStore: %v", confKey, err)
b.serveConfig = ipn.ServeConfigView{}
b.updateCertRefreshLoopLocked()
return
}
@@ -1571,6 +1670,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
})
b.serveConfig = conf.View()
b.updateCertRefreshLoopLocked()
}
func (b *LocalBackend) setVIPServicesTCPPortsInterceptedLocked(svcPorts map[tailcfg.ServiceName][]uint16) {