Update dependencies

This commit is contained in:
bluepython508
2024-11-01 17:33:34 +00:00
parent 033ac0b400
commit 5cdfab398d
3596 changed files with 1033483 additions and 259 deletions

32
vendor/tailscale.com/net/portmapper/disabled_stubs.go generated vendored Normal file
View File

@@ -0,0 +1,32 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build js
package portmapper
import (
"context"
"net/netip"
)
type upnpClient any
type uPnPDiscoResponse struct{}
func parseUPnPDiscoResponse([]byte) (uPnPDiscoResponse, error) {
return uPnPDiscoResponse{}, nil
}
func processUPnPResponses(metas []uPnPDiscoResponse) []uPnPDiscoResponse {
return metas
}
func (c *Client) getUPnPPortMapping(
ctx context.Context,
gw netip.Addr,
internal netip.AddrPort,
prevPort uint16,
) (external netip.AddrPort, ok bool) {
return netip.AddrPort{}, false
}

303
vendor/tailscale.com/net/portmapper/legacy_upnp.go generated vendored Normal file
View File

@@ -0,0 +1,303 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js
// (no raw sockets in JS/WASM)
package portmapper
import (
"context"
"github.com/tailscale/goupnp"
"github.com/tailscale/goupnp/soap"
)
const (
urn_LegacyWANPPPConnection_1 = "urn:dslforum-org:service:WANPPPConnection:1"
urn_LegacyWANIPConnection_1 = "urn:dslforum-org:service:WANIPConnection:1"
)
// legacyWANPPPConnection1 is the same as internetgateway2.WANPPPConnection1,
// except using the old URN that starts with "urn:dslforum-org".
//
// The definition for this can be found in older documentation about UPnP; for
// the purposes of this implementation, we're referring to "DSL Forum TR-064:
// LAN-Side DSL CPE Configuration", which, while deprecated, can be found at:
//
// https://www.broadband-forum.org/wp-content/uploads/2018/11/TR-064_Corrigendum-1.pdf
// https://www.broadband-forum.org/pdfs/tr-064-1-0-1.pdf
type legacyWANPPPConnection1 struct {
goupnp.ServiceClient
}
// AddPortMapping implements upnpClient
func (client *legacyWANPPPConnection1) AddPortMapping(
ctx context.Context,
NewRemoteHost string,
NewExternalPort uint16,
NewProtocol string,
NewInternalPort uint16,
NewInternalClient string,
NewEnabled bool,
NewPortMappingDescription string,
NewLeaseDuration uint32,
) (err error) {
// Request structure.
request := &struct {
NewRemoteHost string
NewExternalPort string
NewProtocol string
NewInternalPort string
NewInternalClient string
NewEnabled string
NewPortMappingDescription string
NewLeaseDuration string
}{}
if request.NewRemoteHost, err = soap.MarshalString(NewRemoteHost); err != nil {
return
}
if request.NewExternalPort, err = soap.MarshalUi2(NewExternalPort); err != nil {
return
}
if request.NewProtocol, err = soap.MarshalString(NewProtocol); err != nil {
return
}
if request.NewInternalPort, err = soap.MarshalUi2(NewInternalPort); err != nil {
return
}
if request.NewInternalClient, err = soap.MarshalString(NewInternalClient); err != nil {
return
}
if request.NewEnabled, err = soap.MarshalBoolean(NewEnabled); err != nil {
return
}
if request.NewPortMappingDescription, err = soap.MarshalString(NewPortMappingDescription); err != nil {
return
}
if request.NewLeaseDuration, err = soap.MarshalUi4(NewLeaseDuration); err != nil {
return
}
// Response structure.
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformAction(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) {
// Request structure.
request := &struct {
NewRemoteHost string
NewExternalPort string
NewProtocol string
}{}
if request.NewRemoteHost, err = soap.MarshalString(NewRemoteHost); err != nil {
return
}
if request.NewExternalPort, err = soap.MarshalUi2(NewExternalPort); err != nil {
return
}
if request.NewProtocol, err = soap.MarshalString(NewProtocol); err != nil {
return
}
// Response structure.
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformAction(ctx, urn_LegacyWANPPPConnection_1, "DeletePortMapping", request, response)
}
// GetExternalIPAddress implements upnpClient
func (client *legacyWANPPPConnection1) GetExternalIPAddress(ctx context.Context) (NewExternalIPAddress string, err error) {
// Request structure.
request := any(nil)
// Response structure.
response := &struct {
NewExternalIPAddress string
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformAction(ctx, urn_LegacyWANPPPConnection_1, "GetExternalIPAddress", request, response); err != nil {
return
}
if NewExternalIPAddress, err = soap.UnmarshalString(response.NewExternalIPAddress); err != nil {
return
}
return
}
// GetStatusInfo implements upnpClient
func (client *legacyWANPPPConnection1) GetStatusInfo(ctx context.Context) (NewConnectionStatus string, NewLastConnectionError string, NewUptime uint32, err error) {
// Request structure.
request := any(nil)
// Response structure.
response := &struct {
NewConnectionStatus string
NewLastConnectionError string
NewUpTime string // NOTE: the "T" is capitalized here, per the spec, though it's lowercase in the newer UPnP spec
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformAction(ctx, urn_LegacyWANPPPConnection_1, "GetStatusInfo", request, response); err != nil {
return
}
if NewConnectionStatus, err = soap.UnmarshalString(response.NewConnectionStatus); err != nil {
return
}
if NewLastConnectionError, err = soap.UnmarshalString(response.NewLastConnectionError); err != nil {
return
}
if NewUptime, err = soap.UnmarshalUi4(response.NewUpTime); err != nil {
return
}
return
}
// legacyWANIPConnection1 is the same as internetgateway2.WANIPConnection1,
// except using the old URN that starts with "urn:dslforum-org".
//
// See legacyWANPPPConnection1 for details on where this is defined.
type legacyWANIPConnection1 struct {
goupnp.ServiceClient
}
// AddPortMapping implements upnpClient
func (client *legacyWANIPConnection1) AddPortMapping(
ctx context.Context,
NewRemoteHost string,
NewExternalPort uint16,
NewProtocol string,
NewInternalPort uint16,
NewInternalClient string,
NewEnabled bool,
NewPortMappingDescription string,
NewLeaseDuration uint32,
) (err error) {
// Request structure.
request := &struct {
NewRemoteHost string
NewExternalPort string
NewProtocol string
NewInternalPort string
NewInternalClient string
NewEnabled string
NewPortMappingDescription string
NewLeaseDuration string
}{}
if request.NewRemoteHost, err = soap.MarshalString(NewRemoteHost); err != nil {
return
}
if request.NewExternalPort, err = soap.MarshalUi2(NewExternalPort); err != nil {
return
}
if request.NewProtocol, err = soap.MarshalString(NewProtocol); err != nil {
return
}
if request.NewInternalPort, err = soap.MarshalUi2(NewInternalPort); err != nil {
return
}
if request.NewInternalClient, err = soap.MarshalString(NewInternalClient); err != nil {
return
}
if request.NewEnabled, err = soap.MarshalBoolean(NewEnabled); err != nil {
return
}
if request.NewPortMappingDescription, err = soap.MarshalString(NewPortMappingDescription); err != nil {
return
}
if request.NewLeaseDuration, err = soap.MarshalUi4(NewLeaseDuration); err != nil {
return
}
// Response structure.
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformAction(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) {
// Request structure.
request := &struct {
NewRemoteHost string
NewExternalPort string
NewProtocol string
}{}
if request.NewRemoteHost, err = soap.MarshalString(NewRemoteHost); err != nil {
return
}
if request.NewExternalPort, err = soap.MarshalUi2(NewExternalPort); err != nil {
return
}
if request.NewProtocol, err = soap.MarshalString(NewProtocol); err != nil {
return
}
// Response structure.
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformAction(ctx, urn_LegacyWANIPConnection_1, "DeletePortMapping", request, response)
}
// GetExternalIPAddress implements upnpClient
func (client *legacyWANIPConnection1) GetExternalIPAddress(ctx context.Context) (NewExternalIPAddress string, err error) {
// Request structure.
request := any(nil)
// Response structure.
response := &struct {
NewExternalIPAddress string
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformAction(ctx, urn_LegacyWANIPConnection_1, "GetExternalIPAddress", request, response); err != nil {
return
}
if NewExternalIPAddress, err = soap.UnmarshalString(response.NewExternalIPAddress); err != nil {
return
}
return
}
// GetStatusInfo implements upnpClient
func (client *legacyWANIPConnection1) GetStatusInfo(ctx context.Context) (NewConnectionStatus string, NewLastConnectionError string, NewUptime uint32, err error) {
// Request structure.
request := any(nil)
// Response structure.
response := &struct {
NewConnectionStatus string
NewLastConnectionError string
NewUpTime string // NOTE: the "T" is capitalized here, per the spec, though it's lowercase in the newer UPnP spec
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformAction(ctx, urn_LegacyWANIPConnection_1, "GetStatusInfo", request, response); err != nil {
return
}
if NewConnectionStatus, err = soap.UnmarshalString(response.NewConnectionStatus); err != nil {
return
}
if NewLastConnectionError, err = soap.UnmarshalString(response.NewLastConnectionError); err != nil {
return
}
if NewUptime, err = soap.UnmarshalUi4(response.NewUpTime); err != nil {
return
}
return
}

175
vendor/tailscale.com/net/portmapper/pcp.go generated vendored Normal file
View File

@@ -0,0 +1,175 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package portmapper
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"net/netip"
"time"
)
// References:
//
// https://www.rfc-editor.org/rfc/pdfrfc/rfc6887.txt.pdf
// https://tools.ietf.org/html/rfc6887
//go:generate go run tailscale.com/cmd/addlicense -file pcpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pcpResultCode -trimprefix=pcpCode
type pcpResultCode uint8
// PCP constants
const (
pcpVersion = 2
pcpDefaultPort = 5351
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
pcpCodeOK pcpResultCode = 0
pcpCodeNotAuthorized pcpResultCode = 2
// From RFC 6887:
// ADDRESS_MISMATCH: The source IP address of the request packet does
// not match the contents of the PCP Client's IP Address field, due
// to an unexpected NAT on the path between the PCP client and the
// PCP-controlled NAT or firewall.
pcpCodeAddressMismatch pcpResultCode = 12
pcpOpReply = 0x80 // OR'd into request's op code on response
pcpOpAnnounce = 0
pcpOpMap = 1
pcpUDPMapping = 17 // portmap UDP
pcpTCPMapping = 6 // portmap TCP
)
type pcpMapping struct {
c *Client
gw netip.AddrPort
internal netip.AddrPort
external netip.AddrPort
renewAfter time.Time
goodUntil time.Time
epoch uint32
}
func (p *pcpMapping) MappingType() string { return "pcp" }
func (p *pcpMapping) GoodUntil() time.Time { return p.goodUntil }
func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter }
func (p *pcpMapping) External() netip.AddrPort { return p.external }
func (p *pcpMapping) MappingDebug() string {
return fmt.Sprintf("pcpMapping{gw:%v, external:%v, internal:%v, renewAfter:%d, goodUntil:%d}",
p.gw, p.external, p.internal,
p.renewAfter.Unix(), p.goodUntil.Unix())
}
func (p *pcpMapping) Release(ctx context.Context) {
uc, err := p.c.listenPacket(ctx, "udp4", ":0")
if err != nil {
return
}
defer uc.Close()
pkt := buildPCPRequestMappingPacket(p.internal.Addr(), p.internal.Port(), p.external.Port(), 0, p.external.Addr())
uc.WriteToUDPAddrPort(pkt, p.gw)
}
// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode.
// To create a packet which deletes a mapping, lifetimeSec should be set to 0.
// If prevPort is not known, it should be set to 0.
// If prevExternalIP is not known, it should be set to 0.0.0.0.
func buildPCPRequestMappingPacket(
myIP netip.Addr,
localPort, prevPort uint16,
lifetimeSec uint32,
prevExternalIP netip.Addr,
) (pkt []byte) {
// 24 byte common PCP header + 36 bytes of MAP-specific fields
pkt = make([]byte, 24+36)
pkt[0] = pcpVersion
pkt[1] = pcpOpMap
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSec)
myIP16 := myIP.As16()
copy(pkt[8:24], myIP16[:])
mapOp := pkt[24:]
rand.Read(mapOp[:12]) // 96 bit mapping nonce
// TODO: should this be a UDP mapping? It looks like it supports "all protocols" with 0, but
// also doesn't support a local port then.
mapOp[12] = pcpUDPMapping
binary.BigEndian.PutUint16(mapOp[16:18], localPort)
binary.BigEndian.PutUint16(mapOp[18:20], prevPort)
prevExternalIP16 := prevExternalIP.As16()
copy(mapOp[20:], prevExternalIP16[:])
return pkt
}
// parsePCPMapResponse parses resp into a partially populated pcpMapping.
// In particular, its Client is not populated.
func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
if len(resp) < 60 {
return nil, fmt.Errorf("Does not appear to be PCP MAP response")
}
res, ok := parsePCPResponse(resp[:24])
if !ok {
return nil, fmt.Errorf("Invalid PCP common header")
}
if res.ResultCode == pcpCodeNotAuthorized {
return nil, fmt.Errorf("PCP is implemented but not enabled in the router")
}
if res.ResultCode != pcpCodeOK {
return nil, fmt.Errorf("PCP response not ok, code %d", res.ResultCode)
}
// TODO: don't ignore the nonce and make sure it's the same?
externalPort := binary.BigEndian.Uint16(resp[42:44])
externalIPBytes := [16]byte{}
copy(externalIPBytes[:], resp[44:])
externalIP := netip.AddrFrom16(externalIPBytes).Unmap()
external := netip.AddrPortFrom(externalIP, externalPort)
lifetime := time.Second * time.Duration(res.Lifetime)
now := time.Now()
mapping := &pcpMapping{
external: external,
renewAfter: now.Add(lifetime / 2),
goodUntil: now.Add(lifetime),
epoch: res.Epoch,
}
return mapping, nil
}
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
func pcpAnnounceRequest(myIP netip.Addr) []byte {
// See https://tools.ietf.org/html/rfc6887#section-7.1
pkt := make([]byte, 24)
pkt[0] = pcpVersion
pkt[1] = pcpOpAnnounce
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
return pkt
}
type pcpResponse struct {
OpCode uint8
ResultCode pcpResultCode
Lifetime uint32
Epoch uint32
}
func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
if len(b) < 24 || b[0] != pcpVersion {
return
}
res.OpCode = b[1]
res.ResultCode = pcpResultCode(b[3])
res.Lifetime = binary.BigEndian.Uint32(b[4:])
res.Epoch = binary.BigEndian.Uint32(b[8:])
return res, true
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by "stringer -type=pcpResultCode -trimprefix=pcpCode"; DO NOT EDIT.
package portmapper
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[pcpCodeOK-0]
_ = x[pcpCodeNotAuthorized-2]
_ = x[pcpCodeAddressMismatch-12]
}
const (
_pcpResultCode_name_0 = "OK"
_pcpResultCode_name_1 = "NotAuthorized"
_pcpResultCode_name_2 = "AddressMismatch"
)
func (i pcpResultCode) String() string {
switch {
case i == 0:
return _pcpResultCode_name_0
case i == 2:
return _pcpResultCode_name_1
case i == 12:
return _pcpResultCode_name_2
default:
return "pcpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by "stringer -type=pmpResultCode -trimprefix=pmpCode"; DO NOT EDIT.
package portmapper
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[pmpCodeOK-0]
_ = x[pmpCodeUnsupportedVersion-1]
_ = x[pmpCodeNotAuthorized-2]
_ = x[pmpCodeNetworkFailure-3]
_ = x[pmpCodeOutOfResources-4]
_ = x[pmpCodeUnsupportedOpcode-5]
}
const _pmpResultCode_name = "OKUnsupportedVersionNotAuthorizedNetworkFailureOutOfResourcesUnsupportedOpcode"
var _pmpResultCode_index = [...]uint8{0, 2, 20, 33, 47, 61, 78}
func (i pmpResultCode) String() string {
if i >= pmpResultCode(len(_pmpResultCode_index)-1) {
return "pmpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _pmpResultCode_name[_pmpResultCode_index[i]:_pmpResultCode_index[i+1]]
}

1289
vendor/tailscale.com/net/portmapper/portmapper.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

795
vendor/tailscale.com/net/portmapper/upnp.go generated vendored Normal file
View File

@@ -0,0 +1,795 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js
// (no raw sockets in JS/WASM)
package portmapper
import (
"bufio"
"bytes"
"cmp"
"context"
"encoding/xml"
"fmt"
"io"
"math/rand/v2"
"net"
"net/http"
"net/netip"
"net/url"
"slices"
"strings"
"sync/atomic"
"time"
"github.com/tailscale/goupnp"
"github.com/tailscale/goupnp/dcps/internetgateway2"
"github.com/tailscale/goupnp/soap"
"tailscale.com/envknob"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
)
// References:
//
// WANIP Connection v2: http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf
// upnpMapping is a port mapping over the upnp protocol. After being created it is immutable,
// but the client field may be shared across mapping instances.
type upnpMapping struct {
gw netip.Addr
external netip.AddrPort
internal netip.AddrPort
goodUntil time.Time
renewAfter time.Time
// rootDev is the UPnP root device, and may be reused across different
// UPnP mappings.
rootDev *goupnp.RootDevice
// loc is the location used to fetch the rootDev
loc *url.URL
// client is the most recent UPnP client used, and should only be used
// to release an existing mapping; new mappings should be selected from
// the rootDev on each attempt.
client upnpClient
}
// upnpProtocolUDP represents the protocol name for UDP, to be used in the UPnP
// <AddPortMapping> message in the <NewProtocol> field.
//
// NOTE: this must be an upper-case string, or certain routers will reject the
// mapping request. Other implementations like miniupnp send an upper-case
// protocol as well. See:
//
// https://github.com/tailscale/tailscale/issues/7377
const upnpProtocolUDP = "UDP"
func (u *upnpMapping) MappingType() string { return "upnp" }
func (u *upnpMapping) GoodUntil() time.Time { return u.goodUntil }
func (u *upnpMapping) RenewAfter() time.Time { return u.renewAfter }
func (u *upnpMapping) External() netip.AddrPort { return u.external }
func (u *upnpMapping) MappingDebug() string {
return fmt.Sprintf("upnpMapping{gw:%v, external:%v, internal:%v, renewAfter:%d, goodUntil:%d, loc:%q}",
u.gw, u.external, u.internal,
u.renewAfter.Unix(), u.goodUntil.Unix(),
u.loc)
}
func (u *upnpMapping) Release(ctx context.Context) {
u.client.DeletePortMapping(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.
type upnpClient interface {
AddPortMapping(
ctx context.Context,
// remoteHost is the remote device sending packets to this device, in the format of x.x.x.x.
// The empty string, "", means any host out on the internet can send packets in.
remoteHost string,
// externalPort is the exposed port of this port mapping. Visible during NAT operations.
// 0 will let the router select the port, but there is an additional call,
// `AddAnyPortMapping`, which is available on 1 of the 3 possible protocols,
// which should be used if available. See `addAnyPortMapping` below, which calls this if
// `AddAnyPortMapping` is not supported.
externalPort uint16,
// protocol is whether this is over TCP or UDP. Either "TCP" or "UDP".
protocol string,
// internalPort is the port that the gateway device forwards the traffic to.
internalPort uint16,
// internalClient is the IP address that packets will be forwarded to for this mapping.
// Internal client is of the form "x.x.x.x".
internalClient string,
// enabled is whether this portmapping should be enabled or disabled.
enabled bool,
// portMappingDescription is a user-readable description of this portmapping.
portMappingDescription string,
// leaseDurationSec is the duration of this portmapping. The value of this argument must be
// greater than 0. From the spec, it appears if it is set to 0, it will switch to using
// 604800 seconds, but not sure why this is desired. The recommended time is 3600 seconds.
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)
}
// tsPortMappingDesc gets sent to UPnP clients as a human-readable label for the portmapping.
// It is not used for anything other than labelling.
const tsPortMappingDesc = "tailscale-portmap"
// addAnyPortMapping abstracts over different UPnP client connections, calling
// the available AddAnyPortMapping call if available for WAN IP connection v2,
// otherwise picking either the previous port (if one is present) or a random
// port and trying to obtain a mapping using AddPortMapping.
//
// It returns the new external port (which may not be identical to the external
// port specified), or an error.
//
// TODO(bradfitz): also returned the actual lease duration obtained. and check it regularly.
func addAnyPortMapping(
ctx context.Context,
upnp upnpClient,
externalPort uint16,
internalPort uint16,
internalClient string,
leaseDuration time.Duration,
) (newPort uint16, err error) {
// Some devices don't let clients add a port mapping for privileged
// ports (ports below 1024). Additionally, per section 2.3.18 of the
// UPnP spec, regarding the ExternalPort field:
//
// If this value is specified as a wildcard (i.e. 0), connection
// request on all external ports (that are not otherwise mapped)
// will be forwarded to InternalClient. In the wildcard case, the
// value(s) of InternalPort on InternalClient are ignored by the IGD
// for those connections that are forwarded to InternalClient.
// Obviously only one such entry can exist in the NAT at any time
// and conflicts are handled with a “first write wins” behavior.
//
// We obviously do not want to open all ports on the user's device to
// the internet, so we want to do this prior to calling either
// AddAnyPortMapping or AddPortMapping.
//
// Pick an external port that's greater than 1024 by getting a random
// number in [0, 65535 - 1024] and then adding 1024 to it, shifting the
// range to [1024, 65535].
if externalPort < 1024 {
externalPort = uint16(rand.N(65535-1024) + 1024)
}
// 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(
ctx,
"",
externalPort,
upnpProtocolUDP,
internalPort,
internalClient,
true,
tsPortMappingDesc,
uint32(leaseDuration.Seconds()),
)
}
// Fall back to using AddPortMapping, which requests a mapping to/from
// a specific external port.
err = upnp.AddPortMapping(
ctx,
"",
externalPort,
upnpProtocolUDP,
internalPort,
internalClient,
true,
tsPortMappingDesc,
uint32(leaseDuration.Seconds()),
)
return externalPort, err
}
// getUPnPRootDevice fetches the UPnP root device given the discovery response,
// ignoring the underlying protocol for now.
// Adapted from https://github.com/huin/goupnp/blob/master/GUIDE.md.
//
// The gw is the detected gateway.
//
// 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 {
return nil, nil, nil
}
if meta.Location == "" {
return nil, nil, nil
}
if debug.VerboseLogs {
logf("fetching %v", meta.Location)
}
u, err := url.Parse(meta.Location)
if err != nil {
return nil, nil, err
}
ipp, err := netip.ParseAddrPort(u.Host)
if err != nil {
return nil, nil, fmt.Errorf("unexpected host %q in %q", u.Host, meta.Location)
}
if ipp.Addr() != gw {
// https://github.com/tailscale/tailscale/issues/5502
logf("UPnP discovered root %q does not match gateway IP %v; repointing at gateway which is assumed to be floating",
meta.Location, gw)
u.Host = net.JoinHostPort(gw.String(), u.Port())
}
// We're fetching a smallish XML document over plain HTTP
// across the local LAN, without using DNS. There should be
// very few round trips and low latency, so one second is a
// long time.
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// This part does a network fetch.
root, err := goupnp.DeviceByURL(ctx, u)
if err != nil {
return nil, nil, err
}
return root, u, nil
}
// selectBestService picks the "best" service from the given UPnP root device
// to use to create a port mapping. It may return (nil, nil) if no supported
// service was found in the provided *goupnp.RootDevice.
//
// 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).
func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootDevice, loc *url.URL) (client upnpClient, err error) {
method := "none"
defer func() {
if client == nil {
return
}
logf("saw UPnP type %v at %v; %v (%v), method=%s",
strings.TrimPrefix(fmt.Sprintf("%T", client), "*internetgateway2."),
loc, root.Device.FriendlyName, root.Device.Manufacturer,
method)
}()
// 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)
var clients []upnpClient
for _, v := range wanIP2 {
clients = append(clients, v)
}
for _, v := range wanIP1 {
clients = append(clients, v)
}
for _, v := range wanPPP {
clients = append(clients, v)
}
// 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)
metricUPnPSelectLegacy.Add(int64(len(legacyClients)))
for _, client := range legacyClients {
clients = append(clients, &legacyWANPPPConnection1{client})
}
legacyClients, _ = goupnp.NewServiceClientsFromRootDevice(ctx, root, loc, urn_LegacyWANIPConnection_1)
metricUPnPSelectLegacy.Add(int64(len(legacyClients)))
for _, client := range legacyClients {
clients = append(clients, &legacyWANIPConnection1{client})
}
// If we have no clients, then return right now; if we only have one,
// just select and return it.
if len(clients) == 0 {
return nil, nil
}
if len(clients) == 1 {
method = "single"
metricUPnPSelectSingle.Add(1)
return clients[0], nil
}
metricUPnPSelectMultiple.Add(1)
// In order to maximize the chances that we find a valid UPnP device
// that can give us a port mapping, we check a few properties:
// 1. Whether the device is "online", as defined by GetStatusInfo
// 2. Whether the device has an external IP address, as defined by
// GetExternalIPAddress
// 3. Whether the device's external IP address is a public address
// or a private one.
//
// We prefer a device where all of the above is true, and fall back if
// none are found.
//
// In order to save on network requests, iterate through all devices
// and determine how many "points" they have based on the above
// criteria, but return immediately if we find one that meets all
// three.
var (
connected = make(map[upnpClient]bool)
externalIPs map[upnpClient]netip.Addr
)
for _, svc := range clients {
isConnected := serviceIsConnected(ctx, logf, svc)
connected[svc] = isConnected
// Don't bother checking for an external IP if the device isn't
// connected; technically this could happen with a misbehaving
// device, but that seems unlikely.
if !isConnected {
continue
}
// Check if the device has an external IP address.
extIP, err := svc.GetExternalIPAddress(ctx)
if err != nil {
continue
}
externalIP, err := netip.ParseAddr(extIP)
if err != nil {
continue
}
mak.Set(&externalIPs, svc, externalIP)
// If we get here, this device has a non-private external IP
// and is up, so we can just return it.
if !externalIP.IsPrivate() {
method = "ext-public"
metricUPnPSelectExternalPublic.Add(1)
return svc, nil
}
}
// Okay, we have no devices that meet all the available options. Fall
// back to first checking for devices that are up and have a private
// external IP (order matters), and then devices that are up, and then
// just anything at all.
//
// try=0 Up + private external IP
// try=1 Up
for try := 0; try <= 1; try++ {
for _, svc := range clients {
if !connected[svc] {
continue
}
_, hasExtIP := externalIPs[svc]
if hasExtIP {
method = "ext-private"
metricUPnPSelectExternalPrivate.Add(1)
return svc, nil
} else if try == 1 {
method = "up"
metricUPnPSelectUp.Add(1)
return svc, nil
}
}
}
// Nothing is up, but we have something (length of clients checked
// above); just return the first one.
metricUPnPSelectNone.Add(1)
return clients[0], nil
}
// 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)
if err != nil {
return false
}
return status == "Connected" || status == "Up"
}
func (c *Client) upnpHTTPClientLocked() *http.Client {
if c.uPnPHTTPClient == nil {
c.uPnPHTTPClient = &http.Client{
Transport: &http.Transport{
DialContext: netns.NewDialer(c.logf, c.netMon).DialContext,
IdleConnTimeout: 2 * time.Second, // LAN is cheap
},
}
if c.debug.LogHTTP {
c.uPnPHTTPClient = requestLogger(c.logf, c.uPnPHTTPClient)
}
}
return c.uPnPHTTPClient
}
var (
disableUPnpEnv = envknob.RegisterBool("TS_DISABLE_UPNP")
)
// getUPnPPortMapping attempts to create a port-mapping over the UPnP protocol. On success,
// it will return the externally exposed IP and port. Otherwise, it will return a zeroed IP and
// port and an error.
func (c *Client) getUPnPPortMapping(
ctx context.Context,
gw netip.Addr,
internal netip.AddrPort,
prevPort uint16,
) (external netip.AddrPort, ok bool) {
if disableUPnpEnv() || c.debug.DisableUPnP || (c.controlKnobs != nil && c.controlKnobs.DisableUPnP.Load()) {
return netip.AddrPort{}, false
}
now := time.Now()
upnp := &upnpMapping{
gw: gw,
internal: internal,
}
// We can have multiple UPnP "meta" values (which correspond to the
// UPnP discovery responses received). We want to try all of them when
// obtaining a mapping, but also prefer any existing mapping's root
// device (if present), since that will allow us to renew an existing
// mapping instead of creating a new one.
// Start by grabbing the list of metas, any existing mapping, and
// creating a HTTP client for use.
c.mu.Lock()
oldMapping, ok := c.mapping.(*upnpMapping)
metas := c.uPnPMetas
ctx = goupnp.WithHTTPClient(ctx, c.upnpHTTPClientLocked())
c.mu.Unlock()
// Wrapper for a uPnPDiscoResponse with an optional existing root
// device + URL (if we've got a previous cached mapping).
type step struct {
rootDev *goupnp.RootDevice // if nil, use 'meta'
loc *url.URL // non-nil if rootDev is non-nil
meta uPnPDiscoResponse
}
var steps []step
// Now, if we have an existing mapping, swap that mapping's entry to
// the first entry in our "metas" list so we try it first.
haveOldMapping := ok && oldMapping != nil
if haveOldMapping && oldMapping.rootDev != nil {
steps = append(steps, step{rootDev: oldMapping.rootDev, loc: oldMapping.loc})
}
// Note: this includes the meta for a previously-cached mapping, in
// case the rootDev changes.
for _, meta := range metas {
steps = append(steps, step{meta: meta})
}
// Now, iterate through every meta that we have trying to get an
// external IP address. If we succeed, we'll return; if we fail, we
// continue this loop.
var errs []error
for _, step := range steps {
var (
rootDev *goupnp.RootDevice
loc *url.URL
err error
)
if step.rootDev != nil {
rootDev = step.rootDev
loc = step.loc
} else {
rootDev, loc, err = getUPnPRootDevice(ctx, c.logf, c.debug, gw, step.meta)
c.vlogf("getUPnPRootDevice: loc=%q err=%v", loc, err)
if err != nil {
errs = append(errs, err)
continue
}
}
if rootDev == nil {
continue
}
// This actually performs the port mapping operation using this
// root device.
//
// TODO(andrew-d): this can successfully perform a portmap and
// return an externalAddrPort that refers to a non-public IP
// address if the first selected RootDevice is a device that is
// connected to another internal network. This is still better
// than randomly flapping between multiple devices, but we
// should probably split this up further to try the best
// service (one with an external IP) first, instead of
// iterating by device.
//
// This is probably sufficiently unlikely that I'm leaving that
// as a follow-up task if it's necessary.
externalAddrPort, client, err := c.tryUPnPPortmapWithDevice(ctx, internal, prevPort, rootDev, loc)
if err != nil {
errs = append(errs, err)
continue
}
// If we get here, we're successful; we can cache this mapping,
// update our local port, and then return.
//
// NOTE: this time might not technically be accurate if we created a
// permanent lease above, but we should still re-check the presence of
// the lease on a regular basis so we use it anyway.
d := time.Duration(pmpMapLifetimeSec) * time.Second
upnp.goodUntil = now.Add(d)
upnp.renewAfter = now.Add(d / 2)
upnp.external = externalAddrPort
upnp.rootDev = rootDev
upnp.loc = loc
upnp.client = client
c.mu.Lock()
defer c.mu.Unlock()
c.mapping = upnp
c.localPort = externalAddrPort.Port()
return upnp.external, true
}
// If we get here, we didn't get anything.
// TODO(andrew-d): use or log errs?
_ = errs
return netip.AddrPort{}, false
}
// tryUPnPPortmapWithDevice attempts to perform a port forward from the given
// UPnP device to the 'internal' address. It tries to re-use the previous port,
// if a non-zero value is provided, and handles retries and errors about
// unsupported features.
//
// It returns the external address and port that was mapped (i.e. the
// address+port that another Tailscale node can use to make a connection to
// this one) and the UPnP client that was used to obtain that mapping.
func (c *Client) tryUPnPPortmapWithDevice(
ctx context.Context,
internal netip.AddrPort,
prevPort uint16,
rootDev *goupnp.RootDevice,
loc *url.URL,
) (netip.AddrPort, upnpClient, error) {
// Select the best mapping service from the given root device. This
// makes network requests, and can vary from mapping to mapping if the
// upstream device's connection status changes.
client, err := selectBestService(ctx, c.logf, rootDev, loc)
if err != nil {
return netip.AddrPort{}, nil, err
}
// If we have no client, we cannot continue; this can happen if we get
// a valid UPnP response that does not contain any of the service types
// that we know how to use.
if client == nil {
// For debugging, print all available services that we aren't
// using because they're not supported; use c.vlogf so we don't
// spam the logs unless verbose debugging is turned on.
rootDev.Device.VisitServices(func(s *goupnp.Service) {
c.vlogf("unsupported UPnP service: Type=%q ID=%q ControlURL=%q", s.ServiceType, s.ServiceId, s.ControlURL.Str)
})
return netip.AddrPort{}, nil, fmt.Errorf("no supported UPnP clients")
}
// Start by trying to make a temporary lease with a duration.
var newPort uint16
newPort, err = addAnyPortMapping(
ctx,
client,
prevPort,
internal.Port(),
internal.Addr().String(),
pmpMapLifetimeSec*time.Second,
)
c.vlogf("addAnyPortMapping: %v, err=%q", newPort, err)
// If this is an error and the code is
// "OnlyPermanentLeasesSupported", then we retry with no lease
// duration; see the following issue for details:
// https://github.com/tailscale/tailscale/issues/9343
if err != nil {
code, ok := getUPnPErrorCode(err)
if ok {
getUPnPErrorsMetric(code).Add(1)
}
// From the UPnP spec: http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf
// 725: OnlyPermanentLeasesSupported
if ok && code == 725 {
newPort, err = addAnyPortMapping(
ctx,
client,
prevPort,
internal.Port(),
internal.Addr().String(),
0, // permanent
)
c.vlogf("addAnyPortMapping: 725 retry %v, err=%q", newPort, err)
}
}
if err != nil {
return netip.AddrPort{}, nil, err
}
// TODO cache this ip somewhere?
extIP, err := client.GetExternalIPAddress(ctx)
c.vlogf("client.GetExternalIPAddress: %v, %v", extIP, err)
if err != nil {
return netip.AddrPort{}, nil, err
}
externalIP, err := netip.ParseAddr(extIP)
if err != nil {
return netip.AddrPort{}, nil, err
}
// Do a bit of validation on the external IP; we've seen cases where
// UPnP devices return the public IP 0.0.0.0, which obviously doesn't
// work as an endpoint.
//
// See: https://github.com/tailscale/corp/issues/23538
if externalIP.IsUnspecified() {
c.logf("UPnP returned unspecified external IP %v", externalIP)
return netip.AddrPort{}, nil, fmt.Errorf("UPnP returned unspecified external IP")
} else if externalIP.IsLoopback() {
c.logf("UPnP returned loopback external IP %v", externalIP)
return netip.AddrPort{}, nil, fmt.Errorf("UPnP returned loopback external IP")
}
return netip.AddrPortFrom(externalIP, newPort), client, nil
}
// processUPnPResponses sorts and deduplicates a list of UPnP discovery
// responses, returning the possibly-reduced list.
//
// It will perform a consistent sort of the provided responses, so if we have
// multiple valid UPnP destinations a consistent option will be picked every
// time.
func processUPnPResponses(metas []uPnPDiscoResponse) []uPnPDiscoResponse {
// Sort and compact all responses to remove duplicates; since
// we send multiple probes, we often get duplicate responses.
slices.SortFunc(metas, func(a, b uPnPDiscoResponse) int {
// Sort the USN in reverse, so that
// "InternetGatewayDevice:2" sorts before
// "InternetGatewayDevice:1".
if ii := cmp.Compare(a.USN, b.USN); ii != 0 {
return -ii
}
if ii := cmp.Compare(a.Location, b.Location); ii != 0 {
return ii
}
return cmp.Compare(a.Server, b.Server)
})
// We can get multiple responses that point to a single Location, since
// we probe for both ssdp:all and InternetGatewayDevice:1 as
// independent packets. Compact by comparing the Location and Server,
// but not the USN (which contains the device being offered).
//
// Since the slices are sorted in reverse above, this means that if we
// get a discovery response for both InternetGatewayDevice:1 and
// InternetGatewayDevice:2, we'll keep the first
// (InternetGatewayDevice:2) response, which is what we want.
metas = slices.CompactFunc(metas, func(a, b uPnPDiscoResponse) bool {
return a.Location == b.Location && a.Server == b.Server
})
return metas
}
// getUPnPErrorCode returns the UPnP error code from the given response, if the
// error is a SOAP error in the proper format, and a boolean indicating whether
// the provided error was actually a UPnP error.
func getUPnPErrorCode(err error) (int, bool) {
soapErr, ok := err.(*soap.SOAPFaultError)
if !ok {
return 0, false
}
var upnpErr struct {
XMLName xml.Name
Code int `xml:"errorCode"`
Description string `xml:"errorDescription"`
}
if err := xml.Unmarshal([]byte(soapErr.Detail.Raw), &upnpErr); err != nil {
return 0, false
}
if upnpErr.XMLName.Local != "UPnPError" {
return 0, false
}
return upnpErr.Code, true
}
type uPnPDiscoResponse struct {
Location string
// Server describes what version the UPnP is, such as MiniUPnPd/2.x.x
Server string
// USN is the serial number of the device, which also contains
// what kind of UPnP service is being offered, i.e. InternetGatewayDevice:2
USN string
}
// parseUPnPDiscoResponse parses a UPnP HTTP-over-UDP discovery response.
func parseUPnPDiscoResponse(body []byte) (uPnPDiscoResponse, error) {
var r uPnPDiscoResponse
res, err := http.ReadResponse(bufio.NewReaderSize(bytes.NewReader(body), 128), nil)
if err != nil {
return r, err
}
r.Location = res.Header.Get("Location")
r.Server = res.Header.Get("Server")
r.USN = res.Header.Get("Usn")
return r, nil
}
type roundTripperFunc func(*http.Request) (*http.Response, error)
func (r roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}
func requestLogger(logf logger.Logf, client *http.Client) *http.Client {
// Clone the HTTP client, and override the Transport to log to the
// provided logger.
ret := *client
oldTransport := ret.Transport
var requestCounter atomic.Uint64
loggingTransport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
ctr := requestCounter.Add(1)
// Read the body and re-set it.
var (
body []byte
err error
)
if req.Body != nil {
body, err = io.ReadAll(req.Body)
req.Body.Close()
if err != nil {
return nil, err
}
req.Body = io.NopCloser(bytes.NewReader(body))
}
logf("request[%d]: %s %q body=%q", ctr, req.Method, req.URL, body)
resp, err := oldTransport.RoundTrip(req)
if err != nil {
logf("response[%d]: err=%v", ctr, err)
return nil, err
}
// Read the response body
if resp.Body != nil {
body, err = io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
logf("response[%d]: %d bodyErr=%v", ctr, resp.StatusCode, err)
return nil, err
}
resp.Body = io.NopCloser(bytes.NewReader(body))
}
logf("response[%d]: %d body=%q", ctr, resp.StatusCode, body)
return resp, nil
})
ret.Transport = loggingTransport
return &ret
}