This commit is contained in:
2026-02-19 10:07:43 +00:00
parent 007438e372
commit 6e637ecf77
1763 changed files with 60820 additions and 279516 deletions

View File

@@ -10,8 +10,8 @@ package portmapper
import (
"context"
"github.com/tailscale/goupnp"
"github.com/tailscale/goupnp/soap"
"github.com/huin/goupnp"
"github.com/huin/goupnp/soap"
)
const (
@@ -32,8 +32,8 @@ type legacyWANPPPConnection1 struct {
goupnp.ServiceClient
}
// AddPortMapping implements upnpClient
func (client *legacyWANPPPConnection1) AddPortMapping(
// AddPortMappingCtx implements upnpClient
func (client *legacyWANPPPConnection1) AddPortMappingCtx(
ctx context.Context,
NewRemoteHost string,
NewExternalPort uint16,
@@ -85,11 +85,11 @@ func (client *legacyWANPPPConnection1) AddPortMapping(
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformAction(ctx, urn_LegacyWANPPPConnection_1, "AddPortMapping", request, response)
return client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANPPPConnection_1, "AddPortMapping", request, response)
}
// DeletePortMapping implements upnpClient
func (client *legacyWANPPPConnection1) DeletePortMapping(ctx context.Context, NewRemoteHost string, NewExternalPort uint16, NewProtocol string) (err error) {
// DeletePortMappingCtx implements upnpClient
func (client *legacyWANPPPConnection1) DeletePortMappingCtx(ctx context.Context, NewRemoteHost string, NewExternalPort uint16, NewProtocol string) (err error) {
// Request structure.
request := &struct {
NewRemoteHost string
@@ -110,11 +110,11 @@ func (client *legacyWANPPPConnection1) DeletePortMapping(ctx context.Context, Ne
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformAction(ctx, urn_LegacyWANPPPConnection_1, "DeletePortMapping", request, response)
return client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANPPPConnection_1, "DeletePortMapping", request, response)
}
// GetExternalIPAddress implements upnpClient
func (client *legacyWANPPPConnection1) GetExternalIPAddress(ctx context.Context) (NewExternalIPAddress string, err error) {
// GetExternalIPAddressCtx implements upnpClient
func (client *legacyWANPPPConnection1) GetExternalIPAddressCtx(ctx context.Context) (NewExternalIPAddress string, err error) {
// Request structure.
request := any(nil)
@@ -124,7 +124,7 @@ func (client *legacyWANPPPConnection1) GetExternalIPAddress(ctx context.Context)
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformAction(ctx, urn_LegacyWANPPPConnection_1, "GetExternalIPAddress", request, response); err != nil {
if err = client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANPPPConnection_1, "GetExternalIPAddress", request, response); err != nil {
return
}
@@ -134,8 +134,8 @@ func (client *legacyWANPPPConnection1) GetExternalIPAddress(ctx context.Context)
return
}
// GetStatusInfo implements upnpClient
func (client *legacyWANPPPConnection1) GetStatusInfo(ctx context.Context) (NewConnectionStatus string, NewLastConnectionError string, NewUptime uint32, err error) {
// GetStatusInfoCtx implements upnpClient
func (client *legacyWANPPPConnection1) GetStatusInfoCtx(ctx context.Context) (NewConnectionStatus string, NewLastConnectionError string, NewUptime uint32, err error) {
// Request structure.
request := any(nil)
@@ -147,7 +147,7 @@ func (client *legacyWANPPPConnection1) GetStatusInfo(ctx context.Context) (NewCo
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformAction(ctx, urn_LegacyWANPPPConnection_1, "GetStatusInfo", request, response); err != nil {
if err = client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANPPPConnection_1, "GetStatusInfo", request, response); err != nil {
return
}
@@ -171,8 +171,8 @@ type legacyWANIPConnection1 struct {
goupnp.ServiceClient
}
// AddPortMapping implements upnpClient
func (client *legacyWANIPConnection1) AddPortMapping(
// AddPortMappingCtx implements upnpClient
func (client *legacyWANIPConnection1) AddPortMappingCtx(
ctx context.Context,
NewRemoteHost string,
NewExternalPort uint16,
@@ -224,11 +224,11 @@ func (client *legacyWANIPConnection1) AddPortMapping(
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformAction(ctx, urn_LegacyWANIPConnection_1, "AddPortMapping", request, response)
return client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANIPConnection_1, "AddPortMapping", request, response)
}
// DeletePortMapping implements upnpClient
func (client *legacyWANIPConnection1) DeletePortMapping(ctx context.Context, NewRemoteHost string, NewExternalPort uint16, NewProtocol string) (err error) {
// DeletePortMappingCtx implements upnpClient
func (client *legacyWANIPConnection1) DeletePortMappingCtx(ctx context.Context, NewRemoteHost string, NewExternalPort uint16, NewProtocol string) (err error) {
// Request structure.
request := &struct {
NewRemoteHost string
@@ -249,11 +249,11 @@ func (client *legacyWANIPConnection1) DeletePortMapping(ctx context.Context, New
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformAction(ctx, urn_LegacyWANIPConnection_1, "DeletePortMapping", request, response)
return client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANIPConnection_1, "DeletePortMapping", request, response)
}
// GetExternalIPAddress implements upnpClient
func (client *legacyWANIPConnection1) GetExternalIPAddress(ctx context.Context) (NewExternalIPAddress string, err error) {
// GetExternalIPAddressCtx implements upnpClient
func (client *legacyWANIPConnection1) GetExternalIPAddressCtx(ctx context.Context) (NewExternalIPAddress string, err error) {
// Request structure.
request := any(nil)
@@ -263,7 +263,7 @@ func (client *legacyWANIPConnection1) GetExternalIPAddress(ctx context.Context)
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformAction(ctx, urn_LegacyWANIPConnection_1, "GetExternalIPAddress", request, response); err != nil {
if err = client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANIPConnection_1, "GetExternalIPAddress", request, response); err != nil {
return
}
@@ -273,8 +273,8 @@ func (client *legacyWANIPConnection1) GetExternalIPAddress(ctx context.Context)
return
}
// GetStatusInfo implements upnpClient
func (client *legacyWANIPConnection1) GetStatusInfo(ctx context.Context) (NewConnectionStatus string, NewLastConnectionError string, NewUptime uint32, err error) {
// GetStatusInfoCtx implements upnpClient
func (client *legacyWANIPConnection1) GetStatusInfoCtx(ctx context.Context) (NewConnectionStatus string, NewLastConnectionError string, NewUptime uint32, err error) {
// Request structure.
request := any(nil)
@@ -286,7 +286,7 @@ func (client *legacyWANIPConnection1) GetStatusInfo(ctx context.Context) (NewCon
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformAction(ctx, urn_LegacyWANIPConnection_1, "GetStatusInfo", request, response); err != nil {
if err = client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANIPConnection_1, "GetStatusInfo", request, response); err != nil {
return
}

View File

@@ -24,8 +24,9 @@ const _pmpResultCode_name = "OKUnsupportedVersionNotAuthorizedNetworkFailureOutO
var _pmpResultCode_index = [...]uint8{0, 2, 20, 33, 47, 61, 78}
func (i pmpResultCode) String() string {
if i >= pmpResultCode(len(_pmpResultCode_index)-1) {
idx := int(i) - 0
if i < 0 || idx >= len(_pmpResultCode_index)-1 {
return "pmpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _pmpResultCode_name[_pmpResultCode_index[i]:_pmpResultCode_index[i+1]]
return _pmpResultCode_name[_pmpResultCode_index[idx]:_pmpResultCode_index[idx+1]]
}

View File

@@ -8,29 +8,36 @@ package portmapper
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"slices"
"sync"
"sync/atomic"
"time"
"go4.org/mem"
"tailscale.com/control/controlknobs"
"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")
@@ -48,15 +55,33 @@ type DebugKnobs struct {
LogHTTP bool
// Disable* disables a specific service from mapping.
DisableUPnP bool
DisablePMP bool
DisablePCP bool
// 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
@@ -84,16 +109,20 @@ 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
controlKnobs *controlknobs.Knobs
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
mu syncs.Mutex // guards following, and all fields thereof
// runningCreate is whether we're currently working on creating
// a port mapping (whether GetCachedMappingOrStartCreatingOne kicked
@@ -124,6 +153,8 @@ type Client struct {
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...)
@@ -153,7 +184,6 @@ type mapping interface {
MappingDebug() string
}
// HaveMapping reports whether we have a current valid mapping.
func (c *Client) HaveMapping() bool {
c.mu.Lock()
defer c.mu.Unlock()
@@ -201,32 +231,52 @@ func (m *pmpMapping) Release(ctx context.Context) {
uc.WriteToUDPAddrPort(pkt, m.gw)
}
// NewClient returns a new portmapping client.
//
// The netMon parameter is required.
//
// The debug argument allows configuring the behaviour of the portmapper for
// debugging; if nil, a sensible set of defaults will be used.
//
// The controlKnobs, if non-nil, specifies the control knobs from the control
// plane that might disable portmapping.
//
// The optional onChange argument specifies a func to run in a new goroutine
// whenever the port mapping status has changed. If nil, it doesn't make a
// callback.
func NewClient(logf logger.Logf, netMon *netmon.Monitor, debug *DebugKnobs, controlKnobs *controlknobs.Knobs, onChange func()) *Client {
if netMon == nil {
panic("nil netMon")
// 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: logf,
netMon: netMon,
ipAndGateway: netmon.LikelyHomeRouterIP, // TODO(bradfitz): move this to method on netMon
onChange: onChange,
controlKnobs: controlKnobs,
logf: c.Logf,
netMon: c.NetMon,
onChange: c.OnChange,
}
if debug != nil {
ret.debug = *debug
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
}
@@ -256,6 +306,9 @@ func (c *Client) Close() error {
}
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
@@ -417,13 +470,6 @@ func IsNoMappingError(err error) bool {
return ok
}
var (
ErrNoPortMappingServices = errors.New("no port mapping services were found")
ErrGatewayRange = errors.New("skipping portmap; gateway range likely lacks support")
ErrGatewayIPv6 = errors.New("skipping portmap; no IPv6 support for portmapping")
ErrPortMappingDisabled = errors.New("port mapping is disabled")
)
// 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
@@ -467,10 +513,29 @@ func (c *Client) createMapping() {
c.runningCreate = false
}()
if _, err := c.createOrGetMapping(ctx); err == nil && c.onChange != nil {
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()
} else if err != nil && !IsNoMappingError(err) {
c.logf("createOrGetMapping: %v", err)
}
}
@@ -482,19 +547,19 @@ var wildcardIP = netip.MustParseAddr("0.0.0.0")
//
// If no mapping is available, the error will be of type
// NoMappingError; see IsNoMappingError.
func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPort, err error) {
func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, external netip.AddrPort, err error) {
if c.debug.disableAll() {
return netip.AddrPort{}, NoMappingError{ErrPortMappingDisabled}
return nil, netip.AddrPort{}, NoMappingError{ErrPortMappingDisabled}
}
if c.debug.DisableUPnP && c.debug.DisablePCP && c.debug.DisablePMP {
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
if c.debug.DisableUPnP() && c.debug.DisablePCP() && c.debug.DisablePMP() {
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
gw, myIP, ok := c.gatewayAndSelfIP()
if !ok {
return netip.AddrPort{}, NoMappingError{ErrGatewayRange}
return nil, netip.AddrPort{}, NoMappingError{ErrGatewayRange}
}
if gw.Is6() {
return netip.AddrPort{}, NoMappingError{ErrGatewayIPv6}
return nil, netip.AddrPort{}, NoMappingError{ErrGatewayIPv6}
}
now := time.Now()
@@ -523,6 +588,17 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
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",
@@ -548,19 +624,19 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
if now.Before(m.RenewAfter()) {
defer c.mu.Unlock()
reusedExisting = true
return m.External(), nil
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 {
if c.debug.DisablePCP() && c.debug.DisablePMP() {
c.mu.Unlock()
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return external, nil
return nil, external, nil
}
c.vlogf("fallback to UPnP due to PCP and PMP being disabled failed")
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
// If we just did a Probe (e.g. via netchecker) but didn't
@@ -587,16 +663,16 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
c.mu.Unlock()
// fallback to UPnP portmapping
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return external, nil
return nil, external, nil
}
c.vlogf("fallback to UPnP due to no PCP and PMP failed")
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
c.mu.Unlock()
uc, err := c.listenPacket(ctx, "udp4", ":0")
if err != nil {
return netip.AddrPort{}, err
return nil, netip.AddrPort{}, err
}
defer uc.Close()
@@ -605,7 +681,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
pxpAddr := netip.AddrPortFrom(gw, c.pxpPort())
preferPCP := !c.debug.DisablePCP && (c.debug.DisablePMP || (!haveRecentPMP && haveRecentPCP))
preferPCP := !c.debug.DisablePCP() && (c.debug.DisablePMP() || (!haveRecentPMP && haveRecentPCP))
// Create a mapping, defaulting to PMP unless only PCP was seen recently.
if preferPCP {
@@ -616,7 +692,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return netip.AddrPort{}, err
return nil, netip.AddrPort{}, err
}
} else {
// Ask for our external address if needed.
@@ -625,7 +701,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return netip.AddrPort{}, err
return nil, netip.AddrPort{}, err
}
}
@@ -634,7 +710,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return netip.AddrPort{}, err
return nil, netip.AddrPort{}, err
}
}
@@ -643,13 +719,13 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
n, src, err := uc.ReadFromUDPAddrPort(res)
if err != nil {
if ctx.Err() == context.Canceled {
return netip.AddrPort{}, err
return nil, netip.AddrPort{}, err
}
// fallback to UPnP portmapping
if mapping, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return mapping, nil
return nil, mapping, nil
}
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
src = netaddr.Unmap(src)
if !src.IsValid() {
@@ -665,7 +741,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
continue
}
if pres.ResultCode != 0 {
return netip.AddrPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
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())
@@ -683,7 +759,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
if err != nil {
c.logf("failed to get PCP mapping: %v", err)
// PCP should only have a single packet response
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
pcpMapping.c = c
pcpMapping.internal = m.internal
@@ -691,10 +767,10 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
c.mu.Lock()
defer c.mu.Unlock()
c.mapping = pcpMapping
return pcpMapping.external, nil
return pcpMapping, pcpMapping.external, nil
default:
c.logf("unknown PMP/PCP version number: %d %v", version, res[:n])
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
}
@@ -702,7 +778,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPor
c.mu.Lock()
defer c.mu.Unlock()
c.mapping = m
return m.external, nil
return nil, m.external, nil
}
}
}
@@ -790,19 +866,13 @@ func parsePMPResponse(pkt []byte) (res pmpResponse, ok bool) {
return res, true
}
type ProbeResult struct {
PCP bool
PMP bool
UPnP bool
}
// 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 ProbeResult, err error) {
func (c *Client) Probe(ctx context.Context) (res portmappertype.ProbeResult, err error) {
if c.debug.disableAll() {
return res, ErrPortMappingDisabled
}
@@ -837,19 +907,19 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
// https://github.com/tailscale/tailscale/issues/1001
if c.sawPMPRecently() {
res.PMP = true
} else if !c.debug.DisablePMP {
} else if !c.debug.DisablePMP() {
metricPMPSent.Add(1)
uc.WriteToUDPAddrPort(pmpReqExternalAddrPacket, pxpAddr)
}
if c.sawPCPRecently() {
res.PCP = true
} else if !c.debug.DisablePCP {
} else if !c.debug.DisablePCP() {
metricPCPSent.Add(1)
uc.WriteToUDPAddrPort(pcpAnnounceRequest(myIP), pxpAddr)
}
if c.sawUPnPRecently() {
res.UPnP = true
} else if !c.debug.DisableUPnP {
} 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

View File

@@ -0,0 +1,88 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package portmappertype defines the net/portmapper interface, which may or may not be
// linked into the binary.
package portmappertype
import (
"context"
"errors"
"net/netip"
"time"
"tailscale.com/feature"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
// HookNewPortMapper is a hook to install the portmapper creation function.
// It must be set by an init function when buildfeatures.HasPortmapper is true.
var HookNewPortMapper feature.Hook[func(logf logger.Logf,
bus *eventbus.Bus,
netMon *netmon.Monitor,
disableUPnPOrNil,
onlyTCP443OrNil func() bool) Client]
var (
ErrNoPortMappingServices = errors.New("no port mapping services were found")
ErrGatewayRange = errors.New("skipping portmap; gateway range likely lacks support")
ErrGatewayIPv6 = errors.New("skipping portmap; no IPv6 support for portmapping")
ErrPortMappingDisabled = errors.New("port mapping is disabled")
)
// ProbeResult is the result of a portmapper probe, saying
// which port mapping protocols were discovered.
type ProbeResult struct {
PCP bool
PMP bool
UPnP bool
}
// Client is the interface implemented by a portmapper client.
type Client interface {
// 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.
Probe(context.Context) (ProbeResult, error)
// HaveMapping reports whether we have a current valid mapping.
HaveMapping() bool
// 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.
SetGatewayLookupFunc(f func() (gw, myIP netip.Addr, ok bool))
// 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.
NoteNetworkDown()
// 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.
GetCachedMappingOrStartCreatingOne() (external netip.AddrPort, ok bool)
// SetLocalPort updates the local port number to which we want to port
// map UDP traffic
SetLocalPort(localPort uint16)
Close() error
}
// Mapping is an event recording the allocation of a port mapping.
type Mapping struct {
External netip.AddrPort
Type string
GoodUntil time.Time
// TODO(creachadair): Record whether we reused an existing mapping?
}

View File

@@ -25,15 +25,47 @@ import (
"sync/atomic"
"time"
"github.com/tailscale/goupnp"
"github.com/tailscale/goupnp/dcps/internetgateway2"
"github.com/tailscale/goupnp/soap"
"github.com/huin/goupnp"
"github.com/huin/goupnp/dcps/internetgateway2"
"github.com/huin/goupnp/soap"
"tailscale.com/envknob"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/util/ctxkey"
"tailscale.com/util/mak"
)
// upnpHTTPClientKey is a context key for storing an HTTP client to use
// for UPnP requests. This allows us to use a custom HTTP client (with custom
// dialer, timeouts, etc.) while using the upstream goupnp library which only
// supports a global HTTPClientDefault.
var upnpHTTPClientKey = ctxkey.New[*http.Client]("portmapper.upnpHTTPClient", nil)
// delegatingRoundTripper implements http.RoundTripper by delegating to
// the HTTP client stored in the request's context. This allows us to use
// per-request HTTP client configuration with the upstream goupnp library.
type delegatingRoundTripper struct {
inner *http.Client
}
func (d delegatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if c := upnpHTTPClientKey.Value(req.Context()); c != nil {
return c.Transport.RoundTrip(req)
}
return d.inner.Do(req)
}
func init() {
// The upstream goupnp library uses a global HTTP client for all
// requests, while we want to be able to use a per-Client
// [http.Client]. We replace its global HTTP client with one that
// delegates to the HTTP client stored in the request's context.
old := goupnp.HTTPClientDefault
goupnp.HTTPClientDefault = &http.Client{
Transport: delegatingRoundTripper{old},
}
}
// References:
//
// WANIP Connection v2: http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf
@@ -79,14 +111,17 @@ func (u *upnpMapping) MappingDebug() string {
u.loc)
}
func (u *upnpMapping) Release(ctx context.Context) {
u.client.DeletePortMapping(ctx, "", u.external.Port(), upnpProtocolUDP)
u.client.DeletePortMappingCtx(ctx, "", u.external.Port(), upnpProtocolUDP)
}
// upnpClient is an interface over the multiple different clients exported by goupnp,
// exposing the functions we need for portmapping. Those clients are auto-generated from XML-specs,
// which is why they're not very idiomatic.
//
// The method names use the *Ctx suffix to match the upstream goupnp library's convention
// for context-aware methods.
type upnpClient interface {
AddPortMapping(
AddPortMappingCtx(
ctx context.Context,
// remoteHost is the remote device sending packets to this device, in the format of x.x.x.x.
@@ -119,9 +154,9 @@ type upnpClient interface {
leaseDurationSec uint32,
) error
DeletePortMapping(ctx context.Context, remoteHost string, externalPort uint16, protocol string) error
GetExternalIPAddress(ctx context.Context) (externalIPAddress string, err error)
GetStatusInfo(ctx context.Context) (status string, lastConnError string, uptime uint32, err error)
DeletePortMappingCtx(ctx context.Context, remoteHost string, externalPort uint16, protocol string) error
GetExternalIPAddressCtx(ctx context.Context) (externalIPAddress string, err error)
GetStatusInfoCtx(ctx context.Context) (status string, lastConnError string, uptime uint32, err error)
}
// tsPortMappingDesc gets sent to UPnP clients as a human-readable label for the portmapping.
@@ -171,7 +206,7 @@ func addAnyPortMapping(
// First off, try using AddAnyPortMapping; if there's a conflict, the
// router will pick another port and return it.
if upnp, ok := upnp.(*internetgateway2.WANIPConnection2); ok {
return upnp.AddAnyPortMapping(
return upnp.AddAnyPortMappingCtx(
ctx,
"",
externalPort,
@@ -186,7 +221,7 @@ func addAnyPortMapping(
// Fall back to using AddPortMapping, which requests a mapping to/from
// a specific external port.
err = upnp.AddPortMapping(
err = upnp.AddPortMappingCtx(
ctx,
"",
externalPort,
@@ -209,7 +244,7 @@ func addAnyPortMapping(
// The meta is the most recently parsed UDP discovery packet response
// from the Internet Gateway Device.
func getUPnPRootDevice(ctx context.Context, logf logger.Logf, debug DebugKnobs, gw netip.Addr, meta uPnPDiscoResponse) (rootDev *goupnp.RootDevice, loc *url.URL, err error) {
if debug.DisableUPnP {
if debug.DisableUPnP() {
return nil, nil, nil
}
@@ -244,7 +279,7 @@ func getUPnPRootDevice(ctx context.Context, logf logger.Logf, debug DebugKnobs,
defer cancel()
// This part does a network fetch.
root, err := goupnp.DeviceByURL(ctx, u)
root, err := goupnp.DeviceByURLCtx(ctx, u)
if err != nil {
return nil, nil, err
}
@@ -257,8 +292,7 @@ func getUPnPRootDevice(ctx context.Context, logf logger.Logf, debug DebugKnobs,
//
// loc is the parsed location that was used to fetch the given RootDevice.
//
// The provided ctx is not retained in the returned upnpClient, but
// its associated HTTP client is (if set via goupnp.WithHTTPClient).
// The provided ctx is not retained in the returned upnpClient.
func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootDevice, loc *url.URL) (client upnpClient, err error) {
method := "none"
defer func() {
@@ -274,9 +308,9 @@ func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootD
// First, get all available clients from the device, and append to our
// list of possible clients. Order matters here; we want to prefer
// WANIPConnection2 over WANIPConnection1 or WANPPPConnection.
wanIP2, _ := internetgateway2.NewWANIPConnection2ClientsFromRootDevice(ctx, root, loc)
wanIP1, _ := internetgateway2.NewWANIPConnection1ClientsFromRootDevice(ctx, root, loc)
wanPPP, _ := internetgateway2.NewWANPPPConnection1ClientsFromRootDevice(ctx, root, loc)
wanIP2, _ := internetgateway2.NewWANIPConnection2ClientsFromRootDevice(root, loc)
wanIP1, _ := internetgateway2.NewWANIPConnection1ClientsFromRootDevice(root, loc)
wanPPP, _ := internetgateway2.NewWANPPPConnection1ClientsFromRootDevice(root, loc)
var clients []upnpClient
for _, v := range wanIP2 {
@@ -291,12 +325,12 @@ func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootD
// These are legacy services that were deprecated in 2015, but are
// still in use by older devices; try them just in case.
legacyClients, _ := goupnp.NewServiceClientsFromRootDevice(ctx, root, loc, urn_LegacyWANPPPConnection_1)
legacyClients, _ := goupnp.NewServiceClientsFromRootDevice(root, loc, urn_LegacyWANPPPConnection_1)
metricUPnPSelectLegacy.Add(int64(len(legacyClients)))
for _, client := range legacyClients {
clients = append(clients, &legacyWANPPPConnection1{client})
}
legacyClients, _ = goupnp.NewServiceClientsFromRootDevice(ctx, root, loc, urn_LegacyWANIPConnection_1)
legacyClients, _ = goupnp.NewServiceClientsFromRootDevice(root, loc, urn_LegacyWANIPConnection_1)
metricUPnPSelectLegacy.Add(int64(len(legacyClients)))
for _, client := range legacyClients {
clients = append(clients, &legacyWANIPConnection1{client})
@@ -346,7 +380,7 @@ func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootD
}
// Check if the device has an external IP address.
extIP, err := svc.GetExternalIPAddress(ctx)
extIP, err := svc.GetExternalIPAddressCtx(ctx)
if err != nil {
continue
}
@@ -399,7 +433,7 @@ func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootD
// serviceIsConnected returns whether a given UPnP service is connected, based
// on the NewConnectionStatus field returned from GetStatusInfo.
func serviceIsConnected(ctx context.Context, logf logger.Logf, svc upnpClient) bool {
status, _ /* NewLastConnectionError */, _ /* NewUptime */, err := svc.GetStatusInfo(ctx)
status, _ /* NewLastConnectionError */, _ /* NewUptime */, err := svc.GetStatusInfoCtx(ctx)
if err != nil {
return false
}
@@ -434,7 +468,7 @@ func (c *Client) getUPnPPortMapping(
internal netip.AddrPort,
prevPort uint16,
) (external netip.AddrPort, ok bool) {
if disableUPnpEnv() || c.debug.DisableUPnP || (c.controlKnobs != nil && c.controlKnobs.DisableUPnP.Load()) {
if disableUPnpEnv() || c.debug.DisableUPnP() {
return netip.AddrPort{}, false
}
@@ -454,7 +488,7 @@ func (c *Client) getUPnPPortMapping(
c.mu.Lock()
oldMapping, ok := c.mapping.(*upnpMapping)
metas := c.uPnPMetas
ctx = goupnp.WithHTTPClient(ctx, c.upnpHTTPClientLocked())
ctx = upnpHTTPClientKey.WithValue(ctx, c.upnpHTTPClientLocked())
c.mu.Unlock()
// Wrapper for a uPnPDiscoResponse with an optional existing root
@@ -629,7 +663,7 @@ func (c *Client) tryUPnPPortmapWithDevice(
}
// TODO cache this ip somewhere?
extIP, err := client.GetExternalIPAddress(ctx)
extIP, err := client.GetExternalIPAddressCtx(ctx)
c.vlogf("client.GetExternalIPAddress: %v, %v", extIP, err)
if err != nil {
return netip.AddrPort{}, nil, err