mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 16:22:03 +01:00 
			
		
		
		
	Saves 262 KB so far. I'm sure I missed some places, but shotizam says these were the low hanging fruit. Updates #12614 Change-Id: Ia31c01b454f627e6d0470229aae4e19d615e45e3 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			1361 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1361 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| // Package portmapper is a UDP port mapping client. It currently allows for mapping over
 | |
| // NAT-PMP, UPnP, and PCP.
 | |
| package portmapper
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/binary"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"net/netip"
 | |
| 	"slices"
 | |
| 	"sync"
 | |
| 	"sync/atomic"
 | |
| 	"time"
 | |
| 
 | |
| 	"go4.org/mem"
 | |
| 	"tailscale.com/envknob"
 | |
| 	"tailscale.com/feature/buildfeatures"
 | |
| 	"tailscale.com/net/netaddr"
 | |
| 	"tailscale.com/net/neterror"
 | |
| 	"tailscale.com/net/netmon"
 | |
| 	"tailscale.com/net/netns"
 | |
| 	"tailscale.com/net/portmapper/portmappertype"
 | |
| 	"tailscale.com/net/sockstats"
 | |
| 	"tailscale.com/syncs"
 | |
| 	"tailscale.com/types/logger"
 | |
| 	"tailscale.com/types/nettype"
 | |
| 	"tailscale.com/util/clientmetric"
 | |
| 	"tailscale.com/util/eventbus"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	ErrNoPortMappingServices = portmappertype.ErrNoPortMappingServices
 | |
| 	ErrGatewayRange          = portmappertype.ErrGatewayRange
 | |
| 	ErrGatewayIPv6           = portmappertype.ErrGatewayIPv6
 | |
| 	ErrPortMappingDisabled   = portmappertype.ErrPortMappingDisabled
 | |
| )
 | |
| 
 | |
| var disablePortMapperEnv = envknob.RegisterBool("TS_DISABLE_PORTMAPPER")
 | |
| 
 | |
| // DebugKnobs contains debug configuration that can be provided when creating a
 | |
| // Client. The zero value is valid for use.
 | |
| type DebugKnobs struct {
 | |
| 	// VerboseLogs tells the Client to print additional debug information
 | |
| 	// to its logger.
 | |
| 	VerboseLogs bool
 | |
| 
 | |
| 	// LogHTTP tells the Client to print the raw HTTP logs (from UPnP) to
 | |
| 	// its logger. This is useful when debugging buggy UPnP
 | |
| 	// implementations.
 | |
| 	LogHTTP bool
 | |
| 
 | |
| 	// Disable* disables a specific service from mapping.
 | |
| 	// If the funcs are nil or return false, the service is not disabled.
 | |
| 	// Use the corresponding accessor methods without the "Func" suffix
 | |
| 	// to check whether a service is disabled.
 | |
| 	DisableUPnPFunc func() bool
 | |
| 	DisablePMPFunc  func() bool
 | |
| 	DisablePCPFunc  func() bool
 | |
| 
 | |
| 	// DisableAll, if non-nil, is a func that reports whether all port
 | |
| 	// mapping attempts should be disabled.
 | |
| 	DisableAll func() bool
 | |
| }
 | |
| 
 | |
| // DisableUPnP reports whether UPnP is disabled.
 | |
| func (k *DebugKnobs) DisableUPnP() bool {
 | |
| 	return k != nil && k.DisableUPnPFunc != nil && k.DisableUPnPFunc()
 | |
| }
 | |
| 
 | |
| // DisablePMP reports whether NAT-PMP is disabled.
 | |
| func (k *DebugKnobs) DisablePMP() bool {
 | |
| 	return k != nil && k.DisablePMPFunc != nil && k.DisablePMPFunc()
 | |
| }
 | |
| 
 | |
| // DisablePCP reports whether PCP is disabled.
 | |
| func (k *DebugKnobs) DisablePCP() bool {
 | |
| 	return k != nil && k.DisablePCPFunc != nil && k.DisablePCPFunc()
 | |
| }
 | |
| 
 | |
| func (k *DebugKnobs) disableAll() bool {
 | |
| 	if disablePortMapperEnv() {
 | |
| 		return true
 | |
| 	}
 | |
| 	if k.DisableAll != nil {
 | |
| 		return k.DisableAll()
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // References:
 | |
| //
 | |
| // NAT-PMP: https://tools.ietf.org/html/rfc6886
 | |
| 
 | |
| // portMapServiceTimeout is the time we wait for port mapping
 | |
| // services (UPnP, NAT-PMP, PCP) to respond before we give up and
 | |
| // decide that they're not there. Since these services are on the
 | |
| // same LAN as this machine and a single L3 hop away, we don't
 | |
| // give them much time to respond.
 | |
| const portMapServiceTimeout = 250 * time.Millisecond
 | |
| 
 | |
| // trustServiceStillAvailableDuration is how often we re-verify a port
 | |
| // mapping service is available.
 | |
| const trustServiceStillAvailableDuration = 10 * time.Minute
 | |
| 
 | |
| // Client is a port mapping client.
 | |
| type Client struct {
 | |
| 	// The following two fields must both be non-nil.
 | |
| 	// Both are immutable after construction.
 | |
| 	pubClient *eventbus.Client
 | |
| 	updates   *eventbus.Publisher[portmappertype.Mapping]
 | |
| 
 | |
| 	logf         logger.Logf
 | |
| 	netMon       *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
 | |
| 	ipAndGateway func() (gw, ip netip.Addr, ok bool)
 | |
| 	onChange     func() // or nil
 | |
| 	debug        DebugKnobs
 | |
| 	testPxPPort  uint16 // if non-zero, pxpPort to use for tests
 | |
| 	testUPnPPort uint16 // if non-zero, uPnPPort to use for tests
 | |
| 
 | |
| 	mu sync.Mutex // guards following, and all fields thereof
 | |
| 
 | |
| 	// runningCreate is whether we're currently working on creating
 | |
| 	// a port mapping (whether GetCachedMappingOrStartCreatingOne kicked
 | |
| 	// off a createMapping goroutine).
 | |
| 	runningCreate bool
 | |
| 
 | |
| 	lastMyIP netip.Addr
 | |
| 	lastGW   netip.Addr
 | |
| 	closed   bool
 | |
| 
 | |
| 	lastProbe time.Time
 | |
| 
 | |
| 	// The following PMP fields are populated during Probe
 | |
| 	pmpPubIP     netip.Addr // non-zero if known
 | |
| 	pmpPubIPTime time.Time  // time pmpPubIP last verified
 | |
| 	pmpLastEpoch uint32
 | |
| 
 | |
| 	// The following PCP fields are populated during Probe
 | |
| 	pcpSawTime   time.Time // time we last saw PCP was available
 | |
| 	pcpLastEpoch uint32
 | |
| 
 | |
| 	uPnPSawTime    time.Time           // time we last saw UPnP was available
 | |
| 	uPnPMetas      []uPnPDiscoResponse // UPnP UDP discovery responses
 | |
| 	uPnPHTTPClient *http.Client        // netns-configured HTTP client for UPnP; nil until needed
 | |
| 
 | |
| 	localPort uint16
 | |
| 
 | |
| 	mapping mapping // non-nil if we have a mapping
 | |
| }
 | |
| 
 | |
| var _ portmappertype.Client = (*Client)(nil)
 | |
| 
 | |
| func (c *Client) vlogf(format string, args ...any) {
 | |
| 	if c.debug.VerboseLogs {
 | |
| 		c.logf(format, args...)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // mapping represents a created port-mapping over some protocol.  It specifies a lease duration,
 | |
| // how to release the mapping, and whether the map is still valid.
 | |
| //
 | |
| // After a mapping is created, it should be immutable, and thus reads should be safe across
 | |
| // concurrent goroutines.
 | |
| type mapping interface {
 | |
| 	// Release will attempt to unmap the established port mapping. It will block until completion,
 | |
| 	// but can be called asynchronously. Release should be idempotent, and thus even if called
 | |
| 	// multiple times should not cause additional side-effects.
 | |
| 	Release(context.Context)
 | |
| 	// GoodUntil will return the lease time that the mapping is valid for.
 | |
| 	GoodUntil() time.Time
 | |
| 	// RenewAfter returns the earliest time that the mapping should be renewed.
 | |
| 	RenewAfter() time.Time
 | |
| 	// External indicates what port the mapping can be reached from on the outside.
 | |
| 	External() netip.AddrPort
 | |
| 	// MappingType returns a descriptive string for this type of mapping.
 | |
| 	MappingType() string
 | |
| 	// MappingDebug returns a debug string for this mapping, for use when
 | |
| 	// printing verbose logs.
 | |
| 	MappingDebug() string
 | |
| }
 | |
| 
 | |
| func (c *Client) HaveMapping() bool {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	return c.mapping != nil && c.mapping.GoodUntil().After(time.Now())
 | |
| }
 | |
| 
 | |
| // pmpMapping is an already-created PMP mapping.
 | |
| //
 | |
| // All fields are immutable once created.
 | |
| type pmpMapping struct {
 | |
| 	c          *Client
 | |
| 	gw         netip.AddrPort
 | |
| 	external   netip.AddrPort
 | |
| 	internal   netip.AddrPort
 | |
| 	renewAfter time.Time // the time at which we want to renew the mapping
 | |
| 	goodUntil  time.Time // the mapping's total lifetime
 | |
| 	epoch      uint32
 | |
| }
 | |
| 
 | |
| // externalValid reports whether m.external is valid, with both its IP and Port populated.
 | |
| func (m *pmpMapping) externalValid() bool {
 | |
| 	return m.external.Addr().IsValid() && m.external.Port() != 0
 | |
| }
 | |
| 
 | |
| func (p *pmpMapping) MappingType() string      { return "pmp" }
 | |
| func (p *pmpMapping) GoodUntil() time.Time     { return p.goodUntil }
 | |
| func (p *pmpMapping) RenewAfter() time.Time    { return p.renewAfter }
 | |
| func (p *pmpMapping) External() netip.AddrPort { return p.external }
 | |
| 
 | |
| func (p *pmpMapping) MappingDebug() string {
 | |
| 	return fmt.Sprintf("pmpMapping{gw:%v, external:%v, internal:%v, renewAfter:%d, goodUntil:%d, epoch:%v}",
 | |
| 		p.gw, p.external, p.internal,
 | |
| 		p.renewAfter.Unix(), p.goodUntil.Unix(),
 | |
| 		p.epoch)
 | |
| }
 | |
| 
 | |
| // Release does a best effort fire-and-forget release of the PMP mapping m.
 | |
| func (m *pmpMapping) Release(ctx context.Context) {
 | |
| 	uc, err := m.c.listenPacket(ctx, "udp4", ":0")
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	defer uc.Close()
 | |
| 	pkt := buildPMPRequestMappingPacket(m.internal.Port(), m.external.Port(), pmpMapLifetimeDelete)
 | |
| 	uc.WriteToUDPAddrPort(pkt, m.gw)
 | |
| }
 | |
| 
 | |
| // Config carries the settings for a [Client].
 | |
| type Config struct {
 | |
| 	// EventBus, which must be non-nil, is used for event publication and
 | |
| 	// subscription by portmapper clients created from this config.
 | |
| 	EventBus *eventbus.Bus
 | |
| 
 | |
| 	// Logf is called to generate text logs for the client. If nil, logger.Discard is used.
 | |
| 	Logf logger.Logf
 | |
| 
 | |
| 	// NetMon is the network monitor used by the client. It must be non-nil.
 | |
| 	NetMon *netmon.Monitor
 | |
| 
 | |
| 	// DebugKnobs, if non-nil, configure the behaviour of the portmapper for
 | |
| 	// debugging.  If nil, a sensible set of defaults will be used.
 | |
| 	DebugKnobs *DebugKnobs
 | |
| 
 | |
| 	// OnChange is called to run in a new goroutine whenever the port mapping
 | |
| 	// status has changed. If nil, no callback is issued.
 | |
| 	OnChange func()
 | |
| }
 | |
| 
 | |
| // NewClient constructs a new portmapping [Client] from c. It will panic if any
 | |
| // required parameters are omitted.
 | |
| func NewClient(c Config) *Client {
 | |
| 	switch {
 | |
| 	case c.NetMon == nil:
 | |
| 		panic("nil NetMon")
 | |
| 	case c.EventBus == nil:
 | |
| 		panic("nil EventBus")
 | |
| 	}
 | |
| 	ret := &Client{
 | |
| 		logf:     c.Logf,
 | |
| 		netMon:   c.NetMon,
 | |
| 		onChange: c.OnChange,
 | |
| 	}
 | |
| 	if buildfeatures.HasPortMapper {
 | |
| 		// TODO(bradfitz): move this to method on netMon
 | |
| 		ret.ipAndGateway = netmon.LikelyHomeRouterIP
 | |
| 	}
 | |
| 	ret.pubClient = c.EventBus.Client("portmapper")
 | |
| 	ret.updates = eventbus.Publish[portmappertype.Mapping](ret.pubClient)
 | |
| 	if ret.logf == nil {
 | |
| 		ret.logf = logger.Discard
 | |
| 	}
 | |
| 	if c.DebugKnobs != nil {
 | |
| 		ret.debug = *c.DebugKnobs
 | |
| 	}
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| // SetGatewayLookupFunc set the func that returns the machine's default gateway IP, and
 | |
| // the primary IP address for that gateway. It must be called before the client is used.
 | |
| // If not called, interfaces.LikelyHomeRouterIP is used.
 | |
| func (c *Client) SetGatewayLookupFunc(f func() (gw, myIP netip.Addr, ok bool)) {
 | |
| 	c.ipAndGateway = f
 | |
| }
 | |
| 
 | |
| // NoteNetworkDown should be called when the network has transitioned to a down state.
 | |
| // It's too late to release port mappings at this point (the user might've just turned off
 | |
| // their wifi), but we can make sure we invalidate mappings for later when the network
 | |
| // comes back.
 | |
| func (c *Client) NoteNetworkDown() {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	c.invalidateMappingsLocked(false)
 | |
| }
 | |
| 
 | |
| func (c *Client) Close() error {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	if c.closed {
 | |
| 		return nil
 | |
| 	}
 | |
| 	c.closed = true
 | |
| 	c.invalidateMappingsLocked(true)
 | |
| 	c.updates.Close()
 | |
| 	c.pubClient.Close()
 | |
| 
 | |
| 	// TODO: close some future ever-listening UDP socket(s),
 | |
| 	// waiting for multicast announcements from router.
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // SetLocalPort updates the local port number to which we want to port
 | |
| // map UDP traffic.
 | |
| func (c *Client) SetLocalPort(localPort uint16) {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	if c.localPort == localPort {
 | |
| 		return
 | |
| 	}
 | |
| 	c.localPort = localPort
 | |
| 	c.invalidateMappingsLocked(true)
 | |
| }
 | |
| 
 | |
| func (c *Client) gatewayAndSelfIP() (gw, myIP netip.Addr, ok bool) {
 | |
| 	gw, myIP, ok = c.ipAndGateway()
 | |
| 	if !ok {
 | |
| 		gw = netip.Addr{}
 | |
| 		myIP = netip.Addr{}
 | |
| 	}
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 
 | |
| 	if gw != c.lastGW || myIP != c.lastMyIP || !ok {
 | |
| 		c.lastMyIP = myIP
 | |
| 		c.lastGW = gw
 | |
| 		c.invalidateMappingsLocked(true)
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // pxpPort returns the NAT-PMP and PCP port number.
 | |
| // It returns 5351, except for in tests where it varies by run.
 | |
| func (c *Client) pxpPort() uint16 {
 | |
| 	if c.testPxPPort != 0 {
 | |
| 		return c.testPxPPort
 | |
| 	}
 | |
| 	return pmpDefaultPort
 | |
| }
 | |
| 
 | |
| // upnpPort returns the UPnP discovery port number.
 | |
| // It returns 1900, except for in tests where it varies by run.
 | |
| func (c *Client) upnpPort() uint16 {
 | |
| 	if c.testUPnPPort != 0 {
 | |
| 		return c.testUPnPPort
 | |
| 	}
 | |
| 	return upnpDefaultPort
 | |
| }
 | |
| 
 | |
| func (c *Client) listenPacket(ctx context.Context, network, addr string) (nettype.PacketConn, error) {
 | |
| 	ctx = sockstats.WithSockStats(ctx, sockstats.LabelPortmapperClient, c.logf)
 | |
| 
 | |
| 	// When running under testing conditions, we bind the IGD server
 | |
| 	// to localhost, and may be running in an environment where our
 | |
| 	// netns code would decide that binding the portmapper client
 | |
| 	// socket to the default route interface is the correct way to
 | |
| 	// ensure connectivity. This can result in us trying to send
 | |
| 	// packets for 127.0.0.1 out the machine's LAN interface, which
 | |
| 	// obviously gets dropped on the floor.
 | |
| 	//
 | |
| 	// So, under those testing conditions, do _not_ use netns to
 | |
| 	// create listening sockets. Such sockets are vulnerable to
 | |
| 	// routing loops, but it's tests that don't set up routing loops,
 | |
| 	// so we don't care.
 | |
| 	if c.testPxPPort != 0 || c.testUPnPPort != 0 {
 | |
| 		var lc net.ListenConfig
 | |
| 		pc, err := lc.ListenPacket(ctx, network, addr)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		return pc.(*net.UDPConn), nil
 | |
| 	}
 | |
| 	pc, err := netns.Listener(c.logf, c.netMon).ListenPacket(ctx, network, addr)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return pc.(*net.UDPConn), nil
 | |
| }
 | |
| 
 | |
| func (c *Client) invalidateMappingsLocked(releaseOld bool) {
 | |
| 	if c.mapping != nil {
 | |
| 		if releaseOld {
 | |
| 			c.mapping.Release(context.Background())
 | |
| 		}
 | |
| 		c.mapping = nil
 | |
| 	}
 | |
| 
 | |
| 	c.pmpPubIP = netip.Addr{}
 | |
| 	c.pmpPubIPTime = time.Time{}
 | |
| 	c.pmpLastEpoch = 0
 | |
| 
 | |
| 	c.pcpSawTime = time.Time{}
 | |
| 	c.pcpLastEpoch = 0
 | |
| 
 | |
| 	c.uPnPSawTime = time.Time{}
 | |
| 	c.uPnPMetas = nil
 | |
| }
 | |
| 
 | |
| func (c *Client) sawPMPRecently() bool {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	return c.sawPMPRecentlyLocked()
 | |
| }
 | |
| 
 | |
| func (c *Client) sawPMPRecentlyLocked() bool {
 | |
| 	return c.pmpPubIP.IsValid() && c.pmpPubIPTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
 | |
| }
 | |
| 
 | |
| func (c *Client) sawPCPRecently() bool {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	return c.sawPCPRecentlyLocked()
 | |
| }
 | |
| 
 | |
| func (c *Client) sawPCPRecentlyLocked() bool {
 | |
| 	return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
 | |
| }
 | |
| 
 | |
| func (c *Client) sawUPnPRecently() bool {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	return c.uPnPSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
 | |
| }
 | |
| 
 | |
| // closeCloserOnContextDone starts a new goroutine to call c.Close
 | |
| // if/when ctx becomes done.
 | |
| // To stop the goroutine, call the returned stop func.
 | |
| func closeCloserOnContextDone(ctx context.Context, c io.Closer) (stop func()) {
 | |
| 	// Close uc on ctx being done.
 | |
| 	ctxDone := ctx.Done()
 | |
| 	if ctxDone == nil {
 | |
| 		return func() {}
 | |
| 	}
 | |
| 	stopWaitDone := make(chan struct{})
 | |
| 	go func() {
 | |
| 		select {
 | |
| 		case <-stopWaitDone:
 | |
| 		case <-ctxDone:
 | |
| 			c.Close()
 | |
| 		}
 | |
| 	}()
 | |
| 	return func() { close(stopWaitDone) }
 | |
| }
 | |
| 
 | |
| // NoMappingError is returned when no NAT mapping could be done.
 | |
| type NoMappingError struct {
 | |
| 	err error
 | |
| }
 | |
| 
 | |
| func (nme NoMappingError) Unwrap() error { return nme.err }
 | |
| func (nme NoMappingError) Error() string { return fmt.Sprintf("no NAT mapping available: %v", nme.err) }
 | |
| 
 | |
| // IsNoMappingError reports whether err is of type NoMappingError.
 | |
| func IsNoMappingError(err error) bool {
 | |
| 	_, ok := err.(NoMappingError)
 | |
| 	return ok
 | |
| }
 | |
| 
 | |
| // GetCachedMappingOrStartCreatingOne quickly returns with our current cached portmapping, if any.
 | |
| // If there's not one, it starts up a background goroutine to create one.
 | |
| // If the background goroutine ends up creating one, the onChange hook registered with the
 | |
| // NewClient constructor (if any) will fire.
 | |
| func (c *Client) GetCachedMappingOrStartCreatingOne() (external netip.AddrPort, ok bool) {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 
 | |
| 	// Do we have an existing mapping that's valid?
 | |
| 	now := time.Now()
 | |
| 	if m := c.mapping; m != nil {
 | |
| 		if now.Before(m.GoodUntil()) {
 | |
| 			if now.After(m.RenewAfter()) {
 | |
| 				c.maybeStartMappingLocked()
 | |
| 			}
 | |
| 			return m.External(), true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	c.maybeStartMappingLocked()
 | |
| 	return netip.AddrPort{}, false
 | |
| }
 | |
| 
 | |
| // maybeStartMappingLocked starts a createMapping goroutine up, if one isn't already running.
 | |
| //
 | |
| // c.mu must be held.
 | |
| func (c *Client) maybeStartMappingLocked() {
 | |
| 	if !c.runningCreate {
 | |
| 		c.runningCreate = true
 | |
| 		go c.createMapping()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (c *Client) createMapping() {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	defer func() {
 | |
| 		c.mu.Lock()
 | |
| 		defer c.mu.Unlock()
 | |
| 		c.runningCreate = false
 | |
| 	}()
 | |
| 
 | |
| 	mapping, _, err := c.createOrGetMapping(ctx)
 | |
| 	if err != nil {
 | |
| 		if !IsNoMappingError(err) {
 | |
| 			c.logf("createOrGetMapping: %v", err)
 | |
| 		}
 | |
| 		return
 | |
| 	} else if mapping == nil {
 | |
| 		return
 | |
| 
 | |
| 		// TODO(creachadair): This was already logged in createOrGetMapping.
 | |
| 		// It really should not happen at all, but we will need to untangle
 | |
| 		// the control flow to eliminate that possibility. Meanwhile, this
 | |
| 		// mitigates a panic downstream, cf. #16662.
 | |
| 	}
 | |
| 	c.updates.Publish(portmappertype.Mapping{
 | |
| 		External:  mapping.External(),
 | |
| 		Type:      mapping.MappingType(),
 | |
| 		GoodUntil: mapping.GoodUntil(),
 | |
| 	})
 | |
| 	// TODO(creachadair): Remove this entirely once there are no longer any
 | |
| 	// places where the callback is set.
 | |
| 	if c.onChange != nil {
 | |
| 		go c.onChange()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // wildcardIP is used when the previous external IP is not known for PCP port mapping.
 | |
| var wildcardIP = netip.MustParseAddr("0.0.0.0")
 | |
| 
 | |
| // createOrGetMapping either creates a new mapping or returns a cached
 | |
| // valid one.
 | |
| //
 | |
| // If no mapping is available, the error will be of type
 | |
| // NoMappingError; see IsNoMappingError.
 | |
| func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, external netip.AddrPort, err error) {
 | |
| 	if c.debug.disableAll() {
 | |
| 		return nil, netip.AddrPort{}, NoMappingError{ErrPortMappingDisabled}
 | |
| 	}
 | |
| 	if c.debug.DisableUPnP() && c.debug.DisablePCP() && c.debug.DisablePMP() {
 | |
| 		return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
 | |
| 	}
 | |
| 	gw, myIP, ok := c.gatewayAndSelfIP()
 | |
| 	if !ok {
 | |
| 		return nil, netip.AddrPort{}, NoMappingError{ErrGatewayRange}
 | |
| 	}
 | |
| 	if gw.Is6() {
 | |
| 		return nil, netip.AddrPort{}, NoMappingError{ErrGatewayIPv6}
 | |
| 	}
 | |
| 
 | |
| 	now := time.Now()
 | |
| 
 | |
| 	// Log what kind of portmap we obtained
 | |
| 	reusedExisting := false
 | |
| 	defer func() {
 | |
| 		if err != nil {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		c.mu.Lock()
 | |
| 		defer c.mu.Unlock()
 | |
| 
 | |
| 		portmapType := "none"
 | |
| 		if c.mapping != nil {
 | |
| 			portmapType = c.mapping.MappingType()
 | |
| 		}
 | |
| 		if reusedExisting {
 | |
| 			portmapType = "existing-" + portmapType
 | |
| 		}
 | |
| 
 | |
| 		if c.mapping == nil {
 | |
| 			c.logf("[unexpected] no error but no stored mapping: now=%d external=%v type=%s",
 | |
| 				now.Unix(), external, portmapType)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// TODO(creachadair): This is more subtle than it should be. Ideally we
 | |
| 		// would just return the mapping directly, but there are many different
 | |
| 		// paths through the function with carefully-balanced locks, and not all
 | |
| 		// the paths have a mapping to return. As a workaround, while we're here
 | |
| 		// doing cleanup under the lock, grab the final mapping value and return
 | |
| 		// it, so the caller does not need to grab the lock again and potentially
 | |
| 		// race with a later update. The mapping itself is concurrency-safe.
 | |
| 		//
 | |
| 		// We should restructure this code so the locks are properly scoped.
 | |
| 		mapping = c.mapping
 | |
| 
 | |
| 		// Print the internal details of each mapping if we're being verbose.
 | |
| 		if c.debug.VerboseLogs {
 | |
| 			c.logf("successfully obtained mapping: now=%d external=%v type=%s mapping=%s",
 | |
| 				now.Unix(), external, portmapType, c.mapping.MappingDebug())
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		c.logf("[v1] successfully obtained mapping: now=%d external=%v type=%s goodUntil=%d renewAfter=%d",
 | |
| 			now.Unix(), external, portmapType,
 | |
| 			c.mapping.GoodUntil().Unix(), c.mapping.RenewAfter().Unix())
 | |
| 	}()
 | |
| 
 | |
| 	c.mu.Lock()
 | |
| 	localPort := c.localPort
 | |
| 	internalAddr := netip.AddrPortFrom(myIP, localPort)
 | |
| 
 | |
| 	// prevPort is the port we had most previously, if any. We try
 | |
| 	// to ask for the same port. 0 means to give us any port.
 | |
| 	var prevPort uint16
 | |
| 
 | |
| 	// Do we have an existing mapping that's valid?
 | |
| 	if m := c.mapping; m != nil {
 | |
| 		if now.Before(m.RenewAfter()) {
 | |
| 			defer c.mu.Unlock()
 | |
| 			reusedExisting = true
 | |
| 			return nil, m.External(), nil
 | |
| 		}
 | |
| 		// The mapping might still be valid, so just try to renew it.
 | |
| 		prevPort = m.External().Port()
 | |
| 	}
 | |
| 
 | |
| 	if c.debug.DisablePCP() && c.debug.DisablePMP() {
 | |
| 		c.mu.Unlock()
 | |
| 		if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
 | |
| 			return nil, external, nil
 | |
| 		}
 | |
| 		c.vlogf("fallback to UPnP due to PCP and PMP being disabled failed")
 | |
| 		return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
 | |
| 	}
 | |
| 
 | |
| 	// If we just did a Probe (e.g. via netchecker) but didn't
 | |
| 	// find a PMP service, bail out early rather than probing
 | |
| 	// again. Cuts down latency for most clients.
 | |
| 	haveRecentPMP := c.sawPMPRecentlyLocked()
 | |
| 	haveRecentPCP := c.sawPCPRecentlyLocked()
 | |
| 
 | |
| 	// Since PMP mapping may require multiple calls, and it's not clear from the outset
 | |
| 	// whether we're doing a PCP or PMP call, initialize the PMP mapping here,
 | |
| 	// and only return it once completed.
 | |
| 	//
 | |
| 	// PCP returns all the information necessary for a mapping in a single packet, so we can
 | |
| 	// construct it upon receiving that packet.
 | |
| 	m := &pmpMapping{
 | |
| 		c:        c,
 | |
| 		gw:       netip.AddrPortFrom(gw, c.pxpPort()),
 | |
| 		internal: internalAddr,
 | |
| 	}
 | |
| 	if haveRecentPMP {
 | |
| 		m.external = netip.AddrPortFrom(c.pmpPubIP, m.external.Port())
 | |
| 	}
 | |
| 	if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP && !haveRecentPCP {
 | |
| 		c.mu.Unlock()
 | |
| 		// fallback to UPnP portmapping
 | |
| 		if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
 | |
| 			return nil, external, nil
 | |
| 		}
 | |
| 		c.vlogf("fallback to UPnP due to no PCP and PMP failed")
 | |
| 		return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
 | |
| 	}
 | |
| 	c.mu.Unlock()
 | |
| 
 | |
| 	uc, err := c.listenPacket(ctx, "udp4", ":0")
 | |
| 	if err != nil {
 | |
| 		return nil, netip.AddrPort{}, err
 | |
| 	}
 | |
| 	defer uc.Close()
 | |
| 
 | |
| 	uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
 | |
| 	defer closeCloserOnContextDone(ctx, uc)()
 | |
| 
 | |
| 	pxpAddr := netip.AddrPortFrom(gw, c.pxpPort())
 | |
| 
 | |
| 	preferPCP := !c.debug.DisablePCP() && (c.debug.DisablePMP() || (!haveRecentPMP && haveRecentPCP))
 | |
| 
 | |
| 	// Create a mapping, defaulting to PMP unless only PCP was seen recently.
 | |
| 	if preferPCP {
 | |
| 		// TODO replace wildcardIP here with previous external if known.
 | |
| 		// Only do PCP mapping in the case when PMP did not appear to be available recently.
 | |
| 		pkt := buildPCPRequestMappingPacket(myIP, localPort, prevPort, pcpMapLifetimeSec, wildcardIP)
 | |
| 		if _, err := uc.WriteToUDPAddrPort(pkt, pxpAddr); err != nil {
 | |
| 			if neterror.TreatAsLostUDP(err) {
 | |
| 				err = NoMappingError{ErrNoPortMappingServices}
 | |
| 			}
 | |
| 			return nil, netip.AddrPort{}, err
 | |
| 		}
 | |
| 	} else {
 | |
| 		// Ask for our external address if needed.
 | |
| 		if !m.external.Addr().IsValid() {
 | |
| 			if _, err := uc.WriteToUDPAddrPort(pmpReqExternalAddrPacket, pxpAddr); err != nil {
 | |
| 				if neterror.TreatAsLostUDP(err) {
 | |
| 					err = NoMappingError{ErrNoPortMappingServices}
 | |
| 				}
 | |
| 				return nil, netip.AddrPort{}, err
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		pkt := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
 | |
| 		if _, err := uc.WriteToUDPAddrPort(pkt, pxpAddr); err != nil {
 | |
| 			if neterror.TreatAsLostUDP(err) {
 | |
| 				err = NoMappingError{ErrNoPortMappingServices}
 | |
| 			}
 | |
| 			return nil, netip.AddrPort{}, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	res := make([]byte, 1500)
 | |
| 	for {
 | |
| 		n, src, err := uc.ReadFromUDPAddrPort(res)
 | |
| 		if err != nil {
 | |
| 			if ctx.Err() == context.Canceled {
 | |
| 				return nil, netip.AddrPort{}, err
 | |
| 			}
 | |
| 			// fallback to UPnP portmapping
 | |
| 			if mapping, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
 | |
| 				return nil, mapping, nil
 | |
| 			}
 | |
| 			return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
 | |
| 		}
 | |
| 		src = netaddr.Unmap(src)
 | |
| 		if !src.IsValid() {
 | |
| 			continue
 | |
| 		}
 | |
| 		if src == pxpAddr {
 | |
| 			version := res[0]
 | |
| 			switch version {
 | |
| 			case pmpVersion:
 | |
| 				pres, ok := parsePMPResponse(res[:n])
 | |
| 				if !ok {
 | |
| 					c.logf("unexpected PMP response: % 02x", res[:n])
 | |
| 					continue
 | |
| 				}
 | |
| 				if pres.ResultCode != 0 {
 | |
| 					return nil, netip.AddrPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
 | |
| 				}
 | |
| 				if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
 | |
| 					m.external = netip.AddrPortFrom(pres.PublicAddr, m.external.Port())
 | |
| 				}
 | |
| 				if pres.OpCode == pmpOpReply|pmpOpMapUDP {
 | |
| 					m.external = netip.AddrPortFrom(m.external.Addr(), pres.ExternalPort)
 | |
| 					d := time.Duration(pres.MappingValidSeconds) * time.Second
 | |
| 					now := time.Now()
 | |
| 					m.goodUntil = now.Add(d)
 | |
| 					m.renewAfter = now.Add(d / 2) // renew in half the time
 | |
| 					m.epoch = pres.SecondsSinceEpoch
 | |
| 				}
 | |
| 			case pcpVersion:
 | |
| 				pcpMapping, err := parsePCPMapResponse(res[:n])
 | |
| 				if err != nil {
 | |
| 					c.logf("failed to get PCP mapping: %v", err)
 | |
| 					// PCP should only have a single packet response
 | |
| 					return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
 | |
| 				}
 | |
| 				pcpMapping.c = c
 | |
| 				pcpMapping.internal = m.internal
 | |
| 				pcpMapping.gw = netip.AddrPortFrom(gw, c.pxpPort())
 | |
| 				c.mu.Lock()
 | |
| 				defer c.mu.Unlock()
 | |
| 				c.mapping = pcpMapping
 | |
| 				return pcpMapping, pcpMapping.external, nil
 | |
| 			default:
 | |
| 				c.logf("unknown PMP/PCP version number: %d %v", version, res[:n])
 | |
| 				return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if m.externalValid() {
 | |
| 			c.mu.Lock()
 | |
| 			defer c.mu.Unlock()
 | |
| 			c.mapping = m
 | |
| 			return nil, m.external, nil
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| //go:generate go run tailscale.com/cmd/addlicense -file pmpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pmpResultCode -trimprefix=pmpCode
 | |
| 
 | |
| type pmpResultCode uint16
 | |
| 
 | |
| // NAT-PMP constants.
 | |
| const (
 | |
| 	pmpDefaultPort       = 5351
 | |
| 	pmpMapLifetimeSec    = 7200 // RFC recommended 2 hour map duration
 | |
| 	pmpMapLifetimeDelete = 0    // 0 second lifetime deletes
 | |
| 
 | |
| 	pmpVersion         = 0
 | |
| 	pmpOpMapPublicAddr = 0
 | |
| 	pmpOpMapUDP        = 1
 | |
| 	pmpOpReply         = 0x80 // OR'd into request's op code on response
 | |
| 
 | |
| 	pmpCodeOK                 pmpResultCode = 0
 | |
| 	pmpCodeUnsupportedVersion pmpResultCode = 1
 | |
| 	pmpCodeNotAuthorized      pmpResultCode = 2 // "e.g., box supports mapping, but user has turned feature off"
 | |
| 	pmpCodeNetworkFailure     pmpResultCode = 3 // "e.g., NAT box itself has not obtained a DHCP lease"
 | |
| 	pmpCodeOutOfResources     pmpResultCode = 4
 | |
| 	pmpCodeUnsupportedOpcode  pmpResultCode = 5
 | |
| )
 | |
| 
 | |
| func buildPMPRequestMappingPacket(localPort, prevPort uint16, lifetimeSec uint32) (pkt []byte) {
 | |
| 	pkt = make([]byte, 12)
 | |
| 
 | |
| 	pkt[1] = pmpOpMapUDP
 | |
| 	binary.BigEndian.PutUint16(pkt[4:], localPort)
 | |
| 	binary.BigEndian.PutUint16(pkt[6:], prevPort)
 | |
| 	binary.BigEndian.PutUint32(pkt[8:], lifetimeSec)
 | |
| 
 | |
| 	return pkt
 | |
| }
 | |
| 
 | |
| type pmpResponse struct {
 | |
| 	OpCode            uint8
 | |
| 	ResultCode        pmpResultCode
 | |
| 	SecondsSinceEpoch uint32
 | |
| 
 | |
| 	// For Map ops:
 | |
| 	MappingValidSeconds uint32
 | |
| 	InternalPort        uint16
 | |
| 	ExternalPort        uint16
 | |
| 
 | |
| 	// For public addr ops:
 | |
| 	PublicAddr netip.Addr
 | |
| }
 | |
| 
 | |
| func parsePMPResponse(pkt []byte) (res pmpResponse, ok bool) {
 | |
| 	if len(pkt) < 12 {
 | |
| 		return
 | |
| 	}
 | |
| 	ver := pkt[0]
 | |
| 	if ver != 0 {
 | |
| 		return
 | |
| 	}
 | |
| 	res.OpCode = pkt[1]
 | |
| 	res.ResultCode = pmpResultCode(binary.BigEndian.Uint16(pkt[2:]))
 | |
| 	res.SecondsSinceEpoch = binary.BigEndian.Uint32(pkt[4:])
 | |
| 
 | |
| 	if res.OpCode == pmpOpReply|pmpOpMapUDP {
 | |
| 		if len(pkt) != 16 {
 | |
| 			return res, false
 | |
| 		}
 | |
| 		res.InternalPort = binary.BigEndian.Uint16(pkt[8:])
 | |
| 		res.ExternalPort = binary.BigEndian.Uint16(pkt[10:])
 | |
| 		res.MappingValidSeconds = binary.BigEndian.Uint32(pkt[12:])
 | |
| 	}
 | |
| 
 | |
| 	if res.OpCode == pmpOpReply|pmpOpMapPublicAddr {
 | |
| 		if len(pkt) != 12 {
 | |
| 			return res, false
 | |
| 		}
 | |
| 		res.PublicAddr = netaddr.IPv4(pkt[8], pkt[9], pkt[10], pkt[11])
 | |
| 		if res.PublicAddr.IsUnspecified() {
 | |
| 			// Zero it out so it's not Valid and used accidentally elsewhere.
 | |
| 			res.PublicAddr = netip.Addr{}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return res, true
 | |
| }
 | |
| 
 | |
| // Probe returns a summary of which port mapping services are
 | |
| // available on the network.
 | |
| //
 | |
| // If a probe has run recently and there haven't been any network changes since,
 | |
| // the returned result might be server from the Client's cache, without
 | |
| // sending any network traffic.
 | |
| func (c *Client) Probe(ctx context.Context) (res portmappertype.ProbeResult, err error) {
 | |
| 	if c.debug.disableAll() {
 | |
| 		return res, ErrPortMappingDisabled
 | |
| 	}
 | |
| 	gw, myIP, ok := c.gatewayAndSelfIP()
 | |
| 	if !ok {
 | |
| 		return res, ErrGatewayRange
 | |
| 	}
 | |
| 	defer func() {
 | |
| 		if err == nil {
 | |
| 			c.mu.Lock()
 | |
| 			defer c.mu.Unlock()
 | |
| 			c.lastProbe = time.Now()
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	uc, err := c.listenPacket(context.Background(), "udp4", ":0")
 | |
| 	if err != nil {
 | |
| 		c.logf("ProbePCP: %v", err)
 | |
| 		return res, err
 | |
| 	}
 | |
| 	defer uc.Close()
 | |
| 	ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
 | |
| 	defer cancel()
 | |
| 	defer closeCloserOnContextDone(ctx, uc)()
 | |
| 
 | |
| 	pxpAddr := netip.AddrPortFrom(gw, c.pxpPort())
 | |
| 	upnpAddr := netip.AddrPortFrom(gw, c.upnpPort())
 | |
| 	upnpMulticastAddr := netip.AddrPortFrom(netaddr.IPv4(239, 255, 255, 250), c.upnpPort())
 | |
| 
 | |
| 	// Don't send probes to services that we recently learned (for
 | |
| 	// the same gw/myIP) are available. See
 | |
| 	// https://github.com/tailscale/tailscale/issues/1001
 | |
| 	if c.sawPMPRecently() {
 | |
| 		res.PMP = true
 | |
| 	} else if !c.debug.DisablePMP() {
 | |
| 		metricPMPSent.Add(1)
 | |
| 		uc.WriteToUDPAddrPort(pmpReqExternalAddrPacket, pxpAddr)
 | |
| 	}
 | |
| 	if c.sawPCPRecently() {
 | |
| 		res.PCP = true
 | |
| 	} else if !c.debug.DisablePCP() {
 | |
| 		metricPCPSent.Add(1)
 | |
| 		uc.WriteToUDPAddrPort(pcpAnnounceRequest(myIP), pxpAddr)
 | |
| 	}
 | |
| 	if c.sawUPnPRecently() {
 | |
| 		res.UPnP = true
 | |
| 	} else if !c.debug.DisableUPnP() {
 | |
| 		// Strictly speaking, you discover UPnP services by sending an
 | |
| 		// SSDP query (which uPnPPacket is) to udp/1900 on the SSDP
 | |
| 		// multicast address, and then get a flood of responses back
 | |
| 		// from everything on your network.
 | |
| 		//
 | |
| 		// Empirically, many home routers also respond to SSDP queries
 | |
| 		// directed at udp/1900 on their LAN unicast IP
 | |
| 		// (e.g. 192.168.1.1). This is handy because it means we can
 | |
| 		// probe the router directly and likely get a reply. However,
 | |
| 		// the specs do not _require_ UPnP devices to respond to
 | |
| 		// unicast SSDP queries, so some conformant UPnP
 | |
| 		// implementations only respond to multicast queries.
 | |
| 		//
 | |
| 		// In theory, we could send just the multicast query and get
 | |
| 		// all compliant devices to respond. However, we instead send
 | |
| 		// to both a unicast and a multicast addresses, for a couple
 | |
| 		// of reasons:
 | |
| 		//
 | |
| 		// First, some LANs and OSes have broken multicast in one way
 | |
| 		// or another, so it's possible for the multicast query to be
 | |
| 		// lost while the unicast query gets through. But we still
 | |
| 		// have to send the multicast query to also get a response
 | |
| 		// from strict-UPnP devices on multicast-working networks.
 | |
| 		//
 | |
| 		// Second, SSDP's packet dynamics are a bit weird: you send
 | |
| 		// the SSDP query from your unicast IP to the SSDP multicast
 | |
| 		// IP, but responses are from the UPnP devices's _unicast_ IP
 | |
| 		// to your unicast IP. This can confuse some less-intelligent
 | |
| 		// stateful host firewalls, who might block the responses. To
 | |
| 		// work around this, we send the unicast query first, to teach
 | |
| 		// the firewall to expect a unicast response from the router,
 | |
| 		// and then send our multicast query. That way, even if the
 | |
| 		// device doesn't respond to the unicast query, we've set the
 | |
| 		// stage for the host firewall to accept the response to the
 | |
| 		// multicast query.
 | |
| 		//
 | |
| 		// See https://github.com/tailscale/tailscale/issues/3197 for
 | |
| 		// an example of a device that strictly implements UPnP, and
 | |
| 		// only responds to multicast queries.
 | |
| 		//
 | |
| 		// Then we send a discovery packet looking for
 | |
| 		// urn:schemas-upnp-org:device:InternetGatewayDevice:1 specifically, not
 | |
| 		// just ssdp:all, because there appear to be devices which only send
 | |
| 		// their first descriptor (like urn:schemas-wifialliance-org:device:WFADevice:1)
 | |
| 		// in response to ssdp:all. https://github.com/tailscale/tailscale/issues/3557
 | |
| 		metricUPnPSent.Add(1)
 | |
| 		uc.WriteToUDPAddrPort(uPnPPacket, upnpAddr)
 | |
| 		uc.WriteToUDPAddrPort(uPnPPacket, upnpMulticastAddr)
 | |
| 		uc.WriteToUDPAddrPort(uPnPIGDPacket, upnpMulticastAddr)
 | |
| 	}
 | |
| 
 | |
| 	// We can see multiple UPnP responses from LANs with multiple
 | |
| 	// UPnP-capable routers. Rather than randomly picking whichever arrives
 | |
| 	// first, let's collect all UPnP responses and choose at the end.
 | |
| 	//
 | |
| 	// We do this by starting a 50ms timer from when the first UDP packet
 | |
| 	// is received, and waiting at least that long for more UPnP responses
 | |
| 	// to arrive before returning (as long as the first packet is seen
 | |
| 	// within the first 200ms of the context creation, which is likely in
 | |
| 	// the common case).
 | |
| 	//
 | |
| 	// This 50ms timer is distinct from the context timeout; it is used to
 | |
| 	// delay an early return in the case where we see all three portmapping
 | |
| 	// responses (PCP, PMP, UPnP), whereas the context timeout causes the
 | |
| 	// loop to exit regardless of what portmapping responses we've seen.
 | |
| 	//
 | |
| 	// We use an atomic value to signal that the timer has finished.
 | |
| 	var (
 | |
| 		upnpTimer     *time.Timer
 | |
| 		upnpTimerDone atomic.Bool
 | |
| 	)
 | |
| 	defer func() {
 | |
| 		if upnpTimer != nil {
 | |
| 			upnpTimer.Stop()
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	// Store all returned UPnP responses until we're done, at which point
 | |
| 	// we select from all available options.
 | |
| 	var upnpResponses []uPnPDiscoResponse
 | |
| 	defer func() {
 | |
| 		if !res.UPnP || len(upnpResponses) == 0 {
 | |
| 			// Either we didn't discover any UPnP responses or
 | |
| 			// c.sawUPnPRecently() is true; don't change anything.
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// Deduplicate and sort responses
 | |
| 		upnpResponses = processUPnPResponses(upnpResponses)
 | |
| 
 | |
| 		c.mu.Lock()
 | |
| 		c.uPnPSawTime = time.Now()
 | |
| 		if !slices.Equal(c.uPnPMetas, upnpResponses) {
 | |
| 			c.logf("UPnP meta changed: %+v", upnpResponses)
 | |
| 			c.uPnPMetas = upnpResponses
 | |
| 			metricUPnPUpdatedMeta.Add(1)
 | |
| 		}
 | |
| 		c.mu.Unlock()
 | |
| 	}()
 | |
| 
 | |
| 	// This is the main loop that receives UDP packets and parses them into
 | |
| 	// PCP, PMP, or UPnP responses, updates our ProbeResult, and stores
 | |
| 	// data for use in GetCachedMappingOrStartCreatingOne.
 | |
| 	buf := make([]byte, 1500)
 | |
| 	pcpHeard := false // true when we get any PCP response
 | |
| 	for {
 | |
| 		if pcpHeard && res.PMP && res.UPnP {
 | |
| 			if upnpTimerDone.Load() {
 | |
| 				// Nothing more to discover.
 | |
| 				return res, nil
 | |
| 			}
 | |
| 
 | |
| 			// UPnP timer still running; fall through and keep
 | |
| 			// receiving packets.
 | |
| 		}
 | |
| 		n, src, err := uc.ReadFromUDPAddrPort(buf)
 | |
| 		if err != nil {
 | |
| 			if ctx.Err() == context.DeadlineExceeded {
 | |
| 				err = nil
 | |
| 			}
 | |
| 			return res, err
 | |
| 		}
 | |
| 		// Start timer after we get the first response.
 | |
| 		if upnpTimer == nil {
 | |
| 			upnpTimer = time.AfterFunc(50*time.Millisecond, func() { upnpTimerDone.Store(true) })
 | |
| 		}
 | |
| 
 | |
| 		ip := src.Addr().Unmap()
 | |
| 
 | |
| 		handleUPnPResponse := func() {
 | |
| 			metricUPnPResponse.Add(1)
 | |
| 
 | |
| 			if ip != gw {
 | |
| 				// https://github.com/tailscale/tailscale/issues/5502
 | |
| 				c.logf("UPnP discovery response from %v, but gateway IP is %v", ip, gw)
 | |
| 			}
 | |
| 			meta, err := parseUPnPDiscoResponse(buf[:n])
 | |
| 			if err != nil {
 | |
| 				metricUPnPParseErr.Add(1)
 | |
| 				c.logf("unrecognized UPnP discovery response; ignoring: %v", err)
 | |
| 				return
 | |
| 			}
 | |
| 			metricUPnPOK.Add(1)
 | |
| 			c.logf("[v1] UPnP reply %+v, %q", meta, buf[:n])
 | |
| 
 | |
| 			// Store the UPnP response for later selection
 | |
| 			res.UPnP = true
 | |
| 			if len(upnpResponses) > 10 {
 | |
| 				c.logf("too many UPnP responses: skipping")
 | |
| 			} else {
 | |
| 				upnpResponses = append(upnpResponses, meta)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		port := src.Port()
 | |
| 		switch port {
 | |
| 		case c.upnpPort():
 | |
| 			if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
 | |
| 				handleUPnPResponse()
 | |
| 			}
 | |
| 
 | |
| 		default:
 | |
| 			// https://github.com/tailscale/tailscale/issues/7377
 | |
| 			if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
 | |
| 				c.logf("UPnP discovery response from non-UPnP port %d", port)
 | |
| 				metricUPnPResponseAlternatePort.Add(1)
 | |
| 				handleUPnPResponse()
 | |
| 			}
 | |
| 
 | |
| 		case c.pxpPort(): // same value for PMP and PCP
 | |
| 			metricPXPResponse.Add(1)
 | |
| 			if pres, ok := parsePCPResponse(buf[:n]); ok {
 | |
| 				if pres.OpCode == pcpOpReply|pcpOpAnnounce {
 | |
| 					pcpHeard = true
 | |
| 					c.mu.Lock()
 | |
| 					c.maybeInvalidatePCPMappingLocked(pres.Epoch) // must be before we write to c.pcp*
 | |
| 					c.pcpSawTime = time.Now()
 | |
| 					c.pcpLastEpoch = pres.Epoch
 | |
| 					c.mu.Unlock()
 | |
| 					switch pres.ResultCode {
 | |
| 					case pcpCodeOK:
 | |
| 						c.logf("[v1] Got PCP response: epoch: %v", pres.Epoch)
 | |
| 						res.PCP = true
 | |
| 						metricPCPOK.Add(1)
 | |
| 						continue
 | |
| 					case pcpCodeNotAuthorized:
 | |
| 						// A PCP service is running, but refuses to
 | |
| 						// provide port mapping services.
 | |
| 						res.PCP = false
 | |
| 						metricPCPNotAuthorized.Add(1)
 | |
| 						continue
 | |
| 					case pcpCodeAddressMismatch:
 | |
| 						// A PCP service is running, but it is behind a NAT, so it can't help us.
 | |
| 						res.PCP = false
 | |
| 						metricPCPAddressMismatch.Add(1)
 | |
| 						continue
 | |
| 					default:
 | |
| 						// Fall through to unexpected log line.
 | |
| 					}
 | |
| 				}
 | |
| 				metricPCPUnhandledResponseCode.Add(1)
 | |
| 				c.logf("unexpected PCP probe response: %+v", pres)
 | |
| 			}
 | |
| 			if pres, ok := parsePMPResponse(buf[:n]); ok {
 | |
| 				if pres.OpCode != pmpOpReply|pmpOpMapPublicAddr {
 | |
| 					c.logf("unexpected PMP probe response opcode: %+v", pres)
 | |
| 					metricPMPUnhandledOpcode.Add(1)
 | |
| 					continue
 | |
| 				}
 | |
| 				switch pres.ResultCode {
 | |
| 				case pmpCodeOK:
 | |
| 					metricPMPOK.Add(1)
 | |
| 					c.logf("[v1] Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
 | |
| 					res.PMP = true
 | |
| 					c.mu.Lock()
 | |
| 					c.maybeInvalidatePMPMappingLocked(pres.SecondsSinceEpoch) // must be before we write to c.pmp*
 | |
| 					c.pmpPubIP = pres.PublicAddr
 | |
| 					c.pmpPubIPTime = time.Now()
 | |
| 					c.pmpLastEpoch = pres.SecondsSinceEpoch
 | |
| 					c.mu.Unlock()
 | |
| 					continue
 | |
| 				case pmpCodeNotAuthorized:
 | |
| 					metricPMPNotAuthorized.Add(1)
 | |
| 					c.logf("PMP probe failed due result code: %+v", pres)
 | |
| 					continue
 | |
| 				case pmpCodeNetworkFailure:
 | |
| 					metricPMPNetworkFailure.Add(1)
 | |
| 					c.logf("PMP probe failed due result code: %+v", pres)
 | |
| 					continue
 | |
| 				case pmpCodeOutOfResources:
 | |
| 					metricPMPOutOfResources.Add(1)
 | |
| 					c.logf("PMP probe failed due result code: %+v", pres)
 | |
| 					continue
 | |
| 				}
 | |
| 				metricPMPUnhandledResponseCode.Add(1)
 | |
| 				c.logf("unexpected PMP probe response: %+v", pres)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (c *Client) maybeInvalidatePMPMappingLocked(epoch uint32) {
 | |
| 	if epoch == 0 || c.mapping == nil {
 | |
| 		return
 | |
| 	}
 | |
| 	m, ok := c.mapping.(*pmpMapping)
 | |
| 	if !ok {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if epoch >= m.epoch {
 | |
| 		// Epoch increased, which is fine.
 | |
| 		//
 | |
| 		// TODO: we should more closely follow RFC6887 § 8.5 which also
 | |
| 		// requires us to check the current time and the time that this
 | |
| 		// epoch was received at.
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Epoch decreased, so invalidate the mapping and clear PMP fields.
 | |
| 	c.logf("invalidating PMP mappings since returned epoch %d < stored epoch %d", epoch, m.epoch)
 | |
| 	c.mapping = nil
 | |
| 	c.pmpPubIP = netip.Addr{}
 | |
| 	c.pmpPubIPTime = time.Time{}
 | |
| 	c.pmpLastEpoch = 0
 | |
| }
 | |
| 
 | |
| func (c *Client) maybeInvalidatePCPMappingLocked(epoch uint32) {
 | |
| 	if epoch == 0 || c.mapping == nil {
 | |
| 		return
 | |
| 	}
 | |
| 	m, ok := c.mapping.(*pcpMapping)
 | |
| 	if !ok {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if epoch >= m.epoch {
 | |
| 		// Epoch increased, which is fine.
 | |
| 		//
 | |
| 		// TODO: we should more closely follow RFC6887 § 8.5 which also
 | |
| 		// requires us to check the current time and the time that this
 | |
| 		// epoch was received at.
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Epoch decreased, so invalidate the mapping and clear PCP fields.
 | |
| 	c.logf("invalidating PCP mappings since returned epoch %d < stored epoch %d", epoch, m.epoch)
 | |
| 	c.mapping = nil
 | |
| 	c.pcpSawTime = time.Time{}
 | |
| 	c.pcpLastEpoch = 0
 | |
| }
 | |
| 
 | |
| var pmpReqExternalAddrPacket = []byte{pmpVersion, pmpOpMapPublicAddr} // 0, 0
 | |
| 
 | |
| const (
 | |
| 	upnpDefaultPort = 1900 // for UDP discovery only; TCP port discovered later
 | |
| )
 | |
| 
 | |
| // uPnPPacket is the UPnP UDP discovery packet's request body.
 | |
| var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
 | |
| 	"HOST: 239.255.255.250:1900\r\n" +
 | |
| 	"ST: ssdp:all\r\n" +
 | |
| 	"MAN: \"ssdp:discover\"\r\n" +
 | |
| 	"MX: 2\r\n\r\n")
 | |
| 
 | |
| // Send a discovery frame for InternetGatewayDevice, since some devices respond
 | |
| // to ssdp:all with only their first descriptor (which is often not IGD).
 | |
| // https://github.com/tailscale/tailscale/issues/3557
 | |
| var uPnPIGDPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
 | |
| 	"HOST: 239.255.255.250:1900\r\n" +
 | |
| 	"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" +
 | |
| 	"MAN: \"ssdp:discover\"\r\n" +
 | |
| 	"MX: 2\r\n\r\n")
 | |
| 
 | |
| // PCP/PMP metrics
 | |
| var (
 | |
| 	// metricPXPResponse counts the number of times we received a PMP/PCP response.
 | |
| 	metricPXPResponse = clientmetric.NewCounter("portmap_pxp_response")
 | |
| 
 | |
| 	// metricPCPSent counts the number of times we sent a PCP request.
 | |
| 	metricPCPSent = clientmetric.NewCounter("portmap_pcp_sent")
 | |
| 
 | |
| 	// metricPCPOK counts the number of times
 | |
| 	// we received a successful PCP response.
 | |
| 	metricPCPOK = clientmetric.NewCounter("portmap_pcp_ok")
 | |
| 
 | |
| 	// metricPCPAddressMismatch counts the number of times
 | |
| 	// we received a PCP address mismatch result code.
 | |
| 	metricPCPAddressMismatch = clientmetric.NewCounter("portmap_pcp_address_mismatch")
 | |
| 
 | |
| 	// metricPCPNotAuthorized counts the number of times
 | |
| 	// we received a PCP not authorized result code.
 | |
| 	metricPCPNotAuthorized = clientmetric.NewCounter("portmap_pcp_not_authorized")
 | |
| 
 | |
| 	// metricPCPUnhandledResponseCode counts the number of times
 | |
| 	// we received an (as yet) unhandled PCP result code.
 | |
| 	metricPCPUnhandledResponseCode = clientmetric.NewCounter("portmap_pcp_unhandled_response_code")
 | |
| 
 | |
| 	// metricPMPSent counts the number of times we sent a PMP request.
 | |
| 	metricPMPSent = clientmetric.NewCounter("portmap_pmp_sent")
 | |
| 
 | |
| 	// metricPMPOK counts the number of times
 | |
| 	// we received a successful PMP response.
 | |
| 	metricPMPOK = clientmetric.NewCounter("portmap_pmp_ok")
 | |
| 
 | |
| 	// metricPMPUnhandledOpcode counts the number of times
 | |
| 	// we received an unhandled PMP opcode.
 | |
| 	metricPMPUnhandledOpcode = clientmetric.NewCounter("portmap_pmp_unhandled_opcode")
 | |
| 
 | |
| 	// metricPMPUnhandledResponseCode counts the number of times
 | |
| 	// we received an unhandled PMP result code.
 | |
| 	metricPMPUnhandledResponseCode = clientmetric.NewCounter("portmap_pmp_unhandled_response_code")
 | |
| 
 | |
| 	// metricPMPOutOfResources counts the number of times
 | |
| 	// we received a PCP out of resources result code.
 | |
| 	metricPMPOutOfResources = clientmetric.NewCounter("portmap_pmp_out_of_resources")
 | |
| 
 | |
| 	// metricPMPNetworkFailure counts the number of times
 | |
| 	// we received a PCP network failure result code.
 | |
| 	metricPMPNetworkFailure = clientmetric.NewCounter("portmap_pmp_network_failure")
 | |
| 
 | |
| 	// metricPMPNotAuthorized counts the number of times
 | |
| 	// we received a PCP not authorized result code.
 | |
| 	metricPMPNotAuthorized = clientmetric.NewCounter("portmap_pmp_not_authorized")
 | |
| )
 | |
| 
 | |
| // UPnP metrics
 | |
| var (
 | |
| 	// metricUPnPSent counts the number of times we sent a UPnP request.
 | |
| 	metricUPnPSent = clientmetric.NewCounter("portmap_upnp_sent")
 | |
| 
 | |
| 	// metricUPnPResponse counts the number of times we received a UPnP response.
 | |
| 	metricUPnPResponse = clientmetric.NewCounter("portmap_upnp_response")
 | |
| 
 | |
| 	// metricUPnPResponseAlternatePort counts the number of times we
 | |
| 	// received a UPnP response from a port other than the UPnP port.
 | |
| 	metricUPnPResponseAlternatePort = clientmetric.NewCounter("portmap_upnp_response_alternate_port")
 | |
| 
 | |
| 	// metricUPnPSelectLegacy counts the number of times that a legacy
 | |
| 	// service was found in a UPnP response.
 | |
| 	metricUPnPSelectLegacy = clientmetric.NewCounter("portmap_upnp_select_legacy")
 | |
| 
 | |
| 	// metricUPnPSelectSingle counts the number of times that only a single
 | |
| 	// UPnP device was available in selectBestService.
 | |
| 	metricUPnPSelectSingle = clientmetric.NewCounter("portmap_upnp_select_single")
 | |
| 
 | |
| 	// metricUPnPSelectMultiple counts the number of times that we need to
 | |
| 	// select from among multiple UPnP devices in selectBestService.
 | |
| 	metricUPnPSelectMultiple = clientmetric.NewCounter("portmap_upnp_select_multiple")
 | |
| 
 | |
| 	// metricUPnPSelectExternalPublic counts the number of times that
 | |
| 	// selectBestService picked a UPnP device with an external public IP.
 | |
| 	metricUPnPSelectExternalPublic = clientmetric.NewCounter("portmap_upnp_select_external_public")
 | |
| 
 | |
| 	// metricUPnPSelectExternalPrivate counts the number of times that
 | |
| 	// selectBestService picked a UPnP device with an external private IP.
 | |
| 	metricUPnPSelectExternalPrivate = clientmetric.NewCounter("portmap_upnp_select_external_private")
 | |
| 
 | |
| 	// metricUPnPSelectUp counts the number of times that selectBestService
 | |
| 	// picked a UPnP device that was up but with no external IP.
 | |
| 	metricUPnPSelectUp = clientmetric.NewCounter("portmap_upnp_select_up")
 | |
| 
 | |
| 	// metricUPnPSelectNone counts the number of times that selectBestService
 | |
| 	// picked a UPnP device that is not up.
 | |
| 	metricUPnPSelectNone = clientmetric.NewCounter("portmap_upnp_select_none")
 | |
| 
 | |
| 	// metricUPnPParseErr counts the number of times we failed to parse a UPnP response.
 | |
| 	metricUPnPParseErr = clientmetric.NewCounter("portmap_upnp_parse_err")
 | |
| 
 | |
| 	// metricUPnPOK counts the number of times we received a usable UPnP response.
 | |
| 	metricUPnPOK = clientmetric.NewCounter("portmap_upnp_ok")
 | |
| 
 | |
| 	// metricUPnPUpdatedMeta counts the number of times
 | |
| 	// we received a UPnP response with a new meta.
 | |
| 	metricUPnPUpdatedMeta = clientmetric.NewCounter("portmap_upnp_updated_meta")
 | |
| )
 | |
| 
 | |
| // UPnP error metric that's keyed by code; lazily registered on first read
 | |
| var (
 | |
| 	metricUPnPErrorsByCode syncs.Map[int, *clientmetric.Metric]
 | |
| )
 | |
| 
 | |
| func getUPnPErrorsMetric(code int) *clientmetric.Metric {
 | |
| 	mm, _ := metricUPnPErrorsByCode.LoadOrInit(code, func() *clientmetric.Metric {
 | |
| 		// Metric names cannot contain a hyphen, so we handle negative
 | |
| 		// numbers by prefixing the name with a "minus_".
 | |
| 		var codeStr string
 | |
| 		if code < 0 {
 | |
| 			codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_minus_%d", -code)
 | |
| 		} else {
 | |
| 			codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_%d", code)
 | |
| 		}
 | |
| 
 | |
| 		return clientmetric.NewCounter(codeStr)
 | |
| 	})
 | |
| 	return mm
 | |
| }
 |