2023-01-27 21:37:20 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2021-02-20 06:15:41 +00:00
2021-06-22 22:29:01 +00:00
// Package portmapper is a UDP port mapping client. It currently allows for mapping over
2021-08-03 22:29:53 +00:00
// NAT-PMP, UPnP, and PCP.
2021-02-20 06:15:41 +00:00
package portmapper
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
2021-08-03 05:09:50 +00:00
"net/http"
2022-07-25 03:08:42 +00:00
"net/netip"
2021-02-20 06:15:41 +00:00
"sync"
"time"
2021-08-03 05:09:50 +00:00
"go4.org/mem"
2023-09-11 19:03:39 +00:00
"tailscale.com/control/controlknobs"
2021-02-20 06:15:41 +00:00
"tailscale.com/net/interfaces"
2022-07-25 03:08:42 +00:00
"tailscale.com/net/netaddr"
2021-12-30 19:11:50 +00:00
"tailscale.com/net/neterror"
2023-04-17 23:01:41 +00:00
"tailscale.com/net/netmon"
2021-02-20 06:15:41 +00:00
"tailscale.com/net/netns"
2023-02-03 20:07:58 +00:00
"tailscale.com/net/sockstats"
2021-02-20 06:15:41 +00:00
"tailscale.com/types/logger"
2022-07-25 03:08:42 +00:00
"tailscale.com/types/nettype"
2021-11-23 22:35:55 +00:00
"tailscale.com/util/clientmetric"
2021-02-20 06:15:41 +00:00
)
2023-03-02 23:05:30 +00:00
// 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.
2021-08-03 05:09:50 +00:00
VerboseLogs bool
2021-08-04 23:51:10 +00:00
2023-08-21 20:53:47 +00:00
// 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
2021-08-04 23:51:10 +00:00
// Disable* disables a specific service from mapping.
2021-08-03 05:09:50 +00:00
DisableUPnP bool
DisablePMP bool
DisablePCP bool
2023-03-02 23:05:30 +00:00
}
2021-08-03 05:09:50 +00:00
2021-02-20 06:15:41 +00:00
// 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 {
2021-03-15 20:58:10 +00:00
logf logger . Logf
2023-04-17 23:01:41 +00:00
netMon * netmon . Monitor // optional; nil means interfaces will be looked up on-demand
2023-09-11 19:03:39 +00:00
controlKnobs * controlknobs . Knobs
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
ipAndGateway func ( ) ( gw , ip netip . Addr , ok bool )
2021-07-09 17:01:50 +00:00
onChange func ( ) // or nil
2023-03-02 23:05:30 +00:00
debug DebugKnobs
2021-08-09 19:52:15 +00:00
testPxPPort uint16 // if non-zero, pxpPort to use for tests
testUPnPPort uint16 // if non-zero, uPnPPort to use for tests
2021-02-20 06:15:41 +00:00
mu sync . Mutex // guards following, and all fields thereof
2021-07-09 17:01:50 +00:00
// runningCreate is whether we're currently working on creating
// a port mapping (whether GetCachedMappingOrStartCreatingOne kicked
// off a createMapping goroutine).
runningCreate bool
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
lastMyIP netip . Addr
lastGW netip . Addr
2021-02-20 06:15:41 +00:00
closed bool
lastProbe time . Time
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
pmpPubIP netip . Addr // non-zero if known
2021-02-20 06:15:41 +00:00
pmpPubIPTime time . Time // time pmpPubIP last verified
pmpLastEpoch uint32
2021-08-03 05:09:50 +00:00
pcpSawTime time . Time // time we last saw PCP was available
uPnPSawTime time . Time // time we last saw UPnP was available
uPnPMeta uPnPDiscoResponse // Location header from UPnP UDP discovery response
uPnPHTTPClient * http . Client // netns-configured HTTP client for UPnP; nil until needed
2021-02-20 06:15:41 +00:00
2021-06-22 22:29:01 +00:00
localPort uint16
mapping mapping // non-nil if we have a mapping
}
// 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 )
2023-01-13 00:57:02 +00:00
// GoodUntil will return the lease time that the mapping is valid for.
2021-06-22 22:29:01 +00:00
GoodUntil ( ) time . Time
2023-01-13 00:57:02 +00:00
// RenewAfter returns the earliest time that the mapping should be renewed.
2021-06-22 22:29:01 +00:00
RenewAfter ( ) time . Time
2023-01-13 00:57:02 +00:00
// External indicates what port the mapping can be reached from on the outside.
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
External ( ) netip . AddrPort
2021-02-20 06:15:41 +00:00
}
2021-03-09 23:09:10 +00:00
// HaveMapping reports whether we have a current valid mapping.
func ( c * Client ) HaveMapping ( ) bool {
c . mu . Lock ( )
defer c . mu . Unlock ( )
2021-06-22 22:29:01 +00:00
return c . mapping != nil && c . mapping . GoodUntil ( ) . After ( time . Now ( ) )
2021-03-09 23:09:10 +00:00
}
2021-02-20 06:15:41 +00:00
// pmpMapping is an already-created PMP mapping.
//
// All fields are immutable once created.
type pmpMapping struct {
2021-08-09 19:52:15 +00:00
c * Client
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
gw netip . AddrPort
external netip . AddrPort
internal netip . AddrPort
2021-07-09 17:01:50 +00:00
renewAfter time . Time // the time at which we want to renew the mapping
goodUntil time . Time // the mapping's total lifetime
epoch uint32
2021-02-20 06:15:41 +00:00
}
// externalValid reports whether m.external is valid, with both its IP and Port populated.
func ( m * pmpMapping ) externalValid ( ) bool {
2022-07-25 03:08:42 +00:00
return m . external . Addr ( ) . IsValid ( ) && m . external . Port ( ) != 0
2021-02-20 06:15:41 +00:00
}
2021-06-22 22:29:01 +00:00
func ( p * pmpMapping ) GoodUntil ( ) time . Time { return p . goodUntil }
func ( p * pmpMapping ) RenewAfter ( ) time . Time { return p . renewAfter }
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
func ( p * pmpMapping ) External ( ) netip . AddrPort { return p . external }
2021-06-22 22:29:01 +00:00
// Release does a best effort fire-and-forget release of the PMP mapping m.
func ( m * pmpMapping ) Release ( ctx context . Context ) {
2021-08-09 19:52:15 +00:00
uc , err := m . c . listenPacket ( ctx , "udp4" , ":0" )
2021-02-20 06:15:41 +00:00
if err != nil {
return
}
defer uc . Close ( )
2021-05-15 01:07:28 +00:00
pkt := buildPMPRequestMappingPacket ( m . internal . Port ( ) , m . external . Port ( ) , pmpMapLifetimeDelete )
2022-07-25 03:08:42 +00:00
uc . WriteToUDPAddrPort ( pkt , m . gw )
2021-02-20 06:15:41 +00:00
}
// NewClient returns a new portmapping client.
2021-07-09 17:01:50 +00:00
//
2023-04-17 23:01:41 +00:00
// The netMon parameter is optional; if non-nil it's used to do faster interface
// lookups.
//
2023-03-02 23:05:30 +00:00
// The debug argument allows configuring the behaviour of the portmapper for
// debugging; if nil, a sensible set of defaults will be used.
//
2023-09-11 19:03:39 +00:00
// 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 {
2023-03-02 23:05:30 +00:00
ret := & Client {
2021-03-15 20:58:10 +00:00
logf : logf ,
2023-04-17 23:01:41 +00:00
netMon : netMon ,
2021-03-15 20:58:10 +00:00
ipAndGateway : interfaces . LikelyHomeRouterIP ,
2021-07-09 17:01:50 +00:00
onChange : onChange ,
2023-09-11 19:03:39 +00:00
controlKnobs : controlKnobs ,
2021-02-20 06:15:41 +00:00
}
2023-03-02 23:05:30 +00:00
if debug != nil {
ret . debug = * debug
}
return ret
2021-02-20 06:15:41 +00:00
}
2021-03-15 20:58:10 +00:00
// 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.
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
func ( c * Client ) SetGatewayLookupFunc ( f func ( ) ( gw , myIP netip . Addr , ok bool ) ) {
2021-03-15 20:58:10 +00:00
c . ipAndGateway = f
}
2021-02-20 06:15:41 +00:00
// 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 )
// 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 )
}
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
func ( c * Client ) gatewayAndSelfIP ( ) ( gw , myIP netip . Addr , ok bool ) {
2021-03-15 20:58:10 +00:00
gw , myIP , ok = c . ipAndGateway ( )
2021-02-20 06:15:41 +00:00
if ! ok {
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
gw = netip . Addr { }
myIP = netip . Addr { }
2021-02-20 06:15:41 +00:00
}
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
}
2021-08-09 19:52:15 +00:00
// 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
}
2022-07-25 03:08:42 +00:00
func ( c * Client ) listenPacket ( ctx context . Context , network , addr string ) ( nettype . PacketConn , error ) {
2023-04-13 01:23:22 +00:00
ctx = sockstats . WithSockStats ( ctx , sockstats . LabelPortmapperClient , c . logf )
2023-02-03 20:07:58 +00:00
2021-08-26 03:13:56 +00:00
// 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.
2021-09-08 02:28:45 +00:00
if c . testPxPPort != 0 || c . testUPnPPort != 0 {
2021-08-09 19:52:15 +00:00
var lc net . ListenConfig
2022-07-25 03:08:42 +00:00
pc , err := lc . ListenPacket ( ctx , network , addr )
if err != nil {
return nil , err
}
return pc . ( * net . UDPConn ) , nil
}
2023-04-17 23:01:41 +00:00
pc , err := netns . Listener ( c . logf , c . netMon ) . ListenPacket ( ctx , network , addr )
2022-07-25 03:08:42 +00:00
if err != nil {
return nil , err
2021-08-09 19:52:15 +00:00
}
2022-07-25 03:08:42 +00:00
return pc . ( * net . UDPConn ) , nil
2021-08-09 19:52:15 +00:00
}
2021-02-20 06:15:41 +00:00
func ( c * Client ) invalidateMappingsLocked ( releaseOld bool ) {
2021-06-22 22:29:01 +00:00
if c . mapping != nil {
2021-02-20 06:15:41 +00:00
if releaseOld {
2021-06-22 22:29:01 +00:00
c . mapping . Release ( context . Background ( ) )
2021-02-20 06:15:41 +00:00
}
2021-06-22 22:29:01 +00:00
c . mapping = nil
2021-02-20 06:15:41 +00:00
}
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
c . pmpPubIP = netip . Addr { }
2021-02-20 06:15:41 +00:00
c . pmpPubIPTime = time . Time { }
c . pcpSawTime = time . Time { }
c . uPnPSawTime = time . Time { }
2021-08-03 05:09:50 +00:00
c . uPnPMeta = uPnPDiscoResponse { }
2021-02-20 06:15:41 +00:00
}
func ( c * Client ) sawPMPRecently ( ) bool {
c . mu . Lock ( )
defer c . mu . Unlock ( )
return c . sawPMPRecentlyLocked ( )
}
func ( c * Client ) sawPMPRecentlyLocked ( ) bool {
2022-07-25 03:08:42 +00:00
return c . pmpPubIP . IsValid ( ) && c . pmpPubIPTime . After ( time . Now ( ) . Add ( - trustServiceStillAvailableDuration ) )
2021-02-20 06:15:41 +00:00
}
func ( c * Client ) sawPCPRecently ( ) bool {
c . mu . Lock ( )
defer c . mu . Unlock ( )
2021-08-03 22:29:53 +00:00
return c . sawPCPRecentlyLocked ( )
}
func ( c * Client ) sawPCPRecentlyLocked ( ) bool {
2021-02-20 06:15:41 +00:00
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 ) }
}
2021-07-09 17:01:50 +00:00
// NoMappingError is returned when no NAT mapping could be done.
2021-02-20 06:15:41 +00:00
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
}
var (
ErrNoPortMappingServices = errors . New ( "no port mapping services were found" )
2021-07-27 03:09:44 +00:00
ErrGatewayRange = errors . New ( "skipping portmap; gateway range likely lacks support" )
2022-07-25 03:08:42 +00:00
ErrGatewayIPv6 = errors . New ( "skipping portmap; no IPv6 support for portmapping" )
2021-02-20 06:15:41 +00:00
)
2021-07-09 17:01:50 +00:00
// 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.
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
func ( c * Client ) GetCachedMappingOrStartCreatingOne ( ) ( external netip . AddrPort , ok bool ) {
2021-07-09 17:01:50 +00:00
c . mu . Lock ( )
defer c . mu . Unlock ( )
// Do we have an existing mapping that's valid?
now := time . Now ( )
2021-06-22 22:29:01 +00:00
if m := c . mapping ; m != nil {
if now . Before ( m . GoodUntil ( ) ) {
if now . After ( m . RenewAfter ( ) ) {
2021-07-09 17:01:50 +00:00
c . maybeStartMappingLocked ( )
}
2021-06-22 22:29:01 +00:00
return m . External ( ) , true
2021-07-09 17:01:50 +00:00
}
}
c . maybeStartMappingLocked ( )
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , false
2021-07-09 17:01:50 +00:00
}
// 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
} ( )
if _ , err := c . createOrGetMapping ( ctx ) ; err == nil && c . onChange != nil {
go c . onChange ( )
} else if err != nil && ! IsNoMappingError ( err ) {
c . logf ( "createOrGetMapping: %v" , err )
}
}
2021-08-05 19:33:13 +00:00
// wildcardIP is used when the previous external IP is not known for PCP port mapping.
2022-07-26 03:55:44 +00:00
var wildcardIP = netip . MustParseAddr ( "0.0.0.0" )
2021-08-05 19:33:13 +00:00
2021-07-09 17:01:50 +00:00
// createOrGetMapping either creates a new mapping or returns a cached
2021-02-20 06:15:41 +00:00
// valid one.
//
// If no mapping is available, the error will be of type
// NoMappingError; see IsNoMappingError.
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
func ( c * Client ) createOrGetMapping ( ctx context . Context ) ( external netip . AddrPort , err error ) {
2023-03-02 23:05:30 +00:00
if c . debug . DisableUPnP && c . debug . DisablePCP && c . debug . DisablePMP {
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , NoMappingError { ErrNoPortMappingServices }
2021-08-04 23:51:10 +00:00
}
2021-02-20 06:15:41 +00:00
gw , myIP , ok := c . gatewayAndSelfIP ( )
if ! ok {
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , NoMappingError { ErrGatewayRange }
2021-02-20 06:15:41 +00:00
}
2022-07-25 03:08:42 +00:00
if gw . Is6 ( ) {
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , NoMappingError { ErrGatewayIPv6 }
2022-07-25 03:08:42 +00:00
}
2021-02-20 06:15:41 +00:00
c . mu . Lock ( )
localPort := c . localPort
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
internalAddr := netip . AddrPortFrom ( myIP , localPort )
2021-02-20 06:15:41 +00:00
// 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?
now := time . Now ( )
2021-06-22 22:29:01 +00:00
if m := c . mapping ; m != nil {
if now . Before ( m . RenewAfter ( ) ) {
2021-02-20 06:15:41 +00:00
defer c . mu . Unlock ( )
2021-06-22 22:29:01 +00:00
return m . External ( ) , nil
2021-02-20 06:15:41 +00:00
}
// The mapping might still be valid, so just try to renew it.
2021-06-22 22:29:01 +00:00
prevPort = m . External ( ) . Port ( )
2021-02-20 06:15:41 +00:00
}
2023-03-02 23:05:30 +00:00
if c . debug . DisablePCP && c . debug . DisablePMP {
2021-08-04 23:51:10 +00:00
c . mu . Unlock ( )
if external , ok := c . getUPnPPortMapping ( ctx , gw , internalAddr , prevPort ) ; ok {
return external , nil
}
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , NoMappingError { ErrNoPortMappingServices }
2021-08-04 23:51:10 +00:00
}
2021-02-20 06:15:41 +00:00
// 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 ( )
2021-08-03 22:29:53 +00:00
haveRecentPCP := c . sawPCPRecentlyLocked ( )
2021-08-03 05:09:50 +00:00
2021-08-05 19:33:13 +00:00
// 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.
2021-08-04 23:51:10 +00:00
m := & pmpMapping {
2021-08-09 19:52:15 +00:00
c : c ,
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
gw : netip . AddrPortFrom ( gw , c . pxpPort ( ) ) ,
2021-08-04 23:51:10 +00:00
internal : internalAddr ,
}
2021-02-20 06:15:41 +00:00
if haveRecentPMP {
2022-07-25 03:08:42 +00:00
m . external = netip . AddrPortFrom ( c . pmpPubIP , m . external . Port ( ) )
2021-02-20 06:15:41 +00:00
}
2021-08-03 22:29:53 +00:00
if c . lastProbe . After ( now . Add ( - 5 * time . Second ) ) && ! haveRecentPMP && ! haveRecentPCP {
2021-02-20 06:15:41 +00:00
c . mu . Unlock ( )
2021-06-22 22:29:01 +00:00
// fallback to UPnP portmapping
2021-08-03 22:29:53 +00:00
if external , ok := c . getUPnPPortMapping ( ctx , gw , internalAddr , prevPort ) ; ok {
return external , nil
2021-06-22 22:29:01 +00:00
}
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , NoMappingError { ErrNoPortMappingServices }
2021-02-20 06:15:41 +00:00
}
c . mu . Unlock ( )
2021-08-09 19:52:15 +00:00
uc , err := c . listenPacket ( ctx , "udp4" , ":0" )
2021-02-20 06:15:41 +00:00
if err != nil {
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , err
2021-02-20 06:15:41 +00:00
}
defer uc . Close ( )
uc . SetReadDeadline ( time . Now ( ) . Add ( portMapServiceTimeout ) )
defer closeCloserOnContextDone ( ctx , uc ) ( )
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
pxpAddr := netip . AddrPortFrom ( gw , c . pxpPort ( ) )
2021-08-05 19:33:13 +00:00
2023-03-02 23:05:30 +00:00
preferPCP := ! c . debug . DisablePCP && ( c . debug . DisablePMP || ( ! haveRecentPMP && haveRecentPCP ) )
2021-08-03 22:29:53 +00:00
// Create a mapping, defaulting to PMP unless only PCP was seen recently.
2021-08-05 19:33:13 +00:00
if preferPCP {
// TODO replace wildcardIP here with previous external if known.
2021-08-03 22:29:53 +00:00
// Only do PCP mapping in the case when PMP did not appear to be available recently.
2021-08-05 19:33:13 +00:00
pkt := buildPCPRequestMappingPacket ( myIP , localPort , prevPort , pcpMapLifetimeSec , wildcardIP )
2022-07-25 03:08:42 +00:00
if _ , err := uc . WriteToUDPAddrPort ( pkt , pxpAddr ) ; err != nil {
2021-12-30 19:11:50 +00:00
if neterror . TreatAsLostUDP ( err ) {
err = NoMappingError { ErrNoPortMappingServices }
}
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , err
2021-02-20 06:15:41 +00:00
}
2021-08-03 22:29:53 +00:00
} else {
// Ask for our external address if needed.
2022-07-25 03:08:42 +00:00
if ! m . external . Addr ( ) . IsValid ( ) {
if _ , err := uc . WriteToUDPAddrPort ( pmpReqExternalAddrPacket , pxpAddr ) ; err != nil {
2021-12-30 19:11:50 +00:00
if neterror . TreatAsLostUDP ( err ) {
err = NoMappingError { ErrNoPortMappingServices }
}
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , err
2021-08-03 22:29:53 +00:00
}
}
2021-02-20 06:15:41 +00:00
2021-08-03 22:29:53 +00:00
pkt := buildPMPRequestMappingPacket ( localPort , prevPort , pmpMapLifetimeSec )
2022-07-25 03:08:42 +00:00
if _ , err := uc . WriteToUDPAddrPort ( pkt , pxpAddr ) ; err != nil {
2021-12-30 19:11:50 +00:00
if neterror . TreatAsLostUDP ( err ) {
err = NoMappingError { ErrNoPortMappingServices }
}
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , err
2021-08-03 22:29:53 +00:00
}
2021-02-20 06:15:41 +00:00
}
res := make ( [ ] byte , 1500 )
for {
2023-04-15 20:08:16 +00:00
n , src , err := uc . ReadFromUDPAddrPort ( res )
2021-02-20 06:15:41 +00:00
if err != nil {
if ctx . Err ( ) == context . Canceled {
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , err
2021-02-20 06:15:41 +00:00
}
2021-06-22 22:29:01 +00:00
// fallback to UPnP portmapping
if mapping , ok := c . getUPnPPortMapping ( ctx , gw , internalAddr , prevPort ) ; ok {
return mapping , nil
}
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , NoMappingError { ErrNoPortMappingServices }
2021-02-20 06:15:41 +00:00
}
2023-04-15 20:08:16 +00:00
src = netaddr . Unmap ( src )
2022-08-03 04:48:56 +00:00
if ! src . IsValid ( ) {
2021-02-20 06:15:41 +00:00
continue
}
2021-08-05 19:33:13 +00:00
if src == pxpAddr {
2021-08-03 22:29:53 +00:00
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 {
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , NoMappingError { fmt . Errorf ( "PMP response Op=0x%x,Res=0x%x" , pres . OpCode , pres . ResultCode ) }
2021-08-03 22:29:53 +00:00
}
if pres . OpCode == pmpOpReply | pmpOpMapPublicAddr {
2022-07-25 03:08:42 +00:00
m . external = netip . AddrPortFrom ( pres . PublicAddr , m . external . Port ( ) )
2021-08-03 22:29:53 +00:00
}
if pres . OpCode == pmpOpReply | pmpOpMapUDP {
2022-07-25 03:08:42 +00:00
m . external = netip . AddrPortFrom ( m . external . Addr ( ) , pres . ExternalPort )
2021-08-03 22:29:53 +00:00
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 )
2021-08-04 23:51:10 +00:00
// PCP should only have a single packet response
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , NoMappingError { ErrNoPortMappingServices }
2021-08-03 22:29:53 +00:00
}
2021-08-09 19:52:15 +00:00
pcpMapping . c = c
2021-08-03 22:29:53 +00:00
pcpMapping . internal = m . internal
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
pcpMapping . gw = netip . AddrPortFrom ( gw , c . pxpPort ( ) )
2021-08-03 22:29:53 +00:00
c . mu . Lock ( )
defer c . mu . Unlock ( )
c . mapping = pcpMapping
return pcpMapping . external , nil
default :
c . logf ( "unknown PMP/PCP version number: %d %v" , version , res [ : n ] )
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
return netip . AddrPort { } , NoMappingError { ErrNoPortMappingServices }
2021-02-20 06:15:41 +00:00
}
}
if m . externalValid ( ) {
c . mu . Lock ( )
defer c . mu . Unlock ( )
2021-06-22 22:29:01 +00:00
c . mapping = m
2021-02-20 06:15:41 +00:00
return m . external , nil
}
}
}
2023-01-27 21:36:46 +00:00
//go:generate go run tailscale.com/cmd/addlicense -file pmpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pmpResultCode -trimprefix=pmpCode
2021-11-23 22:43:04 +00:00
2021-02-20 06:15:41 +00:00
type pmpResultCode uint16
// NAT-PMP constants.
const (
2021-08-09 19:52:15 +00:00
pmpDefaultPort = 5351
2021-02-20 06:15:41 +00:00
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
2021-08-03 22:29:53 +00:00
pmpVersion = 0
2021-02-20 06:15:41 +00:00
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:
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
PublicAddr netip . Addr
2021-02-20 06:15:41 +00:00
}
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 ] )
}
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 ) {
gw , myIP , ok := c . gatewayAndSelfIP ( )
if ! ok {
2021-07-27 03:09:44 +00:00
return res , ErrGatewayRange
2021-02-20 06:15:41 +00:00
}
defer func ( ) {
if err == nil {
c . mu . Lock ( )
defer c . mu . Unlock ( )
c . lastProbe = time . Now ( )
}
} ( )
2021-08-09 19:52:15 +00:00
uc , err := c . listenPacket ( context . Background ( ) , "udp4" , ":0" )
2021-02-20 06:15:41 +00:00
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 ) ( )
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
pxpAddr := netip . AddrPortFrom ( gw , c . pxpPort ( ) )
upnpAddr := netip . AddrPortFrom ( gw , c . upnpPort ( ) )
upnpMulticastAddr := netip . AddrPortFrom ( netaddr . IPv4 ( 239 , 255 , 255 , 250 ) , c . upnpPort ( ) )
2021-02-20 06:15:41 +00:00
// 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
2023-03-02 23:05:30 +00:00
} else if ! c . debug . DisablePMP {
2021-11-30 19:42:06 +00:00
metricPMPSent . Add ( 1 )
2022-07-25 03:08:42 +00:00
uc . WriteToUDPAddrPort ( pmpReqExternalAddrPacket , pxpAddr )
2021-02-20 06:15:41 +00:00
}
if c . sawPCPRecently ( ) {
res . PCP = true
2023-03-02 23:05:30 +00:00
} else if ! c . debug . DisablePCP {
2021-11-30 19:42:06 +00:00
metricPCPSent . Add ( 1 )
2022-07-25 03:08:42 +00:00
uc . WriteToUDPAddrPort ( pcpAnnounceRequest ( myIP ) , pxpAddr )
2021-02-20 06:15:41 +00:00
}
2021-08-03 05:09:50 +00:00
if c . sawUPnPRecently ( ) {
res . UPnP = true
2023-03-02 23:05:30 +00:00
} else if ! c . debug . DisableUPnP {
2021-10-27 21:55:59 +00:00
// 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.
2022-06-11 19:02:01 +00:00
//
// 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
2021-11-30 19:41:43 +00:00
metricUPnPSent . Add ( 1 )
2022-07-25 03:08:42 +00:00
uc . WriteToUDPAddrPort ( uPnPPacket , upnpAddr )
uc . WriteToUDPAddrPort ( uPnPPacket , upnpMulticastAddr )
uc . WriteToUDPAddrPort ( uPnPIGDPacket , upnpMulticastAddr )
2021-08-03 05:09:50 +00:00
}
2021-02-20 06:15:41 +00:00
buf := make ( [ ] byte , 1500 )
2021-03-17 22:33:45 +00:00
pcpHeard := false // true when we get any PCP response
2021-02-20 06:15:41 +00:00
for {
2021-08-03 05:09:50 +00:00
if pcpHeard && res . PMP && res . UPnP {
2021-02-20 06:15:41 +00:00
// Nothing more to discover.
return res , nil
}
2023-04-15 20:08:16 +00:00
n , src , err := uc . ReadFromUDPAddrPort ( buf )
2021-02-20 06:15:41 +00:00
if err != nil {
if ctx . Err ( ) == context . DeadlineExceeded {
err = nil
}
return res , err
}
2023-04-15 20:08:16 +00:00
ip := src . Addr ( ) . Unmap ( )
2023-03-04 05:23:26 +00:00
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 ] )
res . UPnP = true
c . mu . Lock ( )
c . uPnPSawTime = time . Now ( )
if c . uPnPMeta != meta {
c . logf ( "UPnP meta changed: %+v" , meta )
c . uPnPMeta = meta
metricUPnPUpdatedMeta . Add ( 1 )
}
c . mu . Unlock ( )
}
2023-04-15 20:08:16 +00:00
port := src . Port ( )
2021-02-20 06:15:41 +00:00
switch port {
2021-08-09 19:52:15 +00:00
case c . upnpPort ( ) :
2023-01-13 00:57:02 +00:00
if mem . Contains ( mem . B ( buf [ : n ] ) , mem . S ( ":InternetGatewayDevice:" ) ) {
2023-03-04 05:23:26 +00:00
handleUPnPResponse ( )
2021-08-03 05:09:50 +00:00
}
2023-03-04 05:23:26 +00:00
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 ( )
}
2021-08-09 19:52:15 +00:00
case c . pxpPort ( ) : // same value for PMP and PCP
2021-11-30 19:42:06 +00:00
metricPXPResponse . Add ( 1 )
2021-02-20 06:15:41 +00:00
if pres , ok := parsePCPResponse ( buf [ : n ] ) ; ok {
2021-03-17 22:33:45 +00:00
if pres . OpCode == pcpOpReply | pcpOpAnnounce {
pcpHeard = true
2021-02-20 06:15:41 +00:00
c . mu . Lock ( )
c . pcpSawTime = time . Now ( )
c . mu . Unlock ( )
2021-03-17 22:33:45 +00:00
switch pres . ResultCode {
case pcpCodeOK :
2021-10-16 13:52:37 +00:00
c . logf ( "[v1] Got PCP response: epoch: %v" , pres . Epoch )
2021-03-17 22:33:45 +00:00
res . PCP = true
2021-11-23 22:35:55 +00:00
metricPCPOK . Add ( 1 )
2021-03-17 22:33:45 +00:00
continue
case pcpCodeNotAuthorized :
// A PCP service is running, but refuses to
// provide port mapping services.
res . PCP = false
2021-11-23 22:35:55 +00:00
metricPCPNotAuthorized . Add ( 1 )
2021-03-17 22:33:45 +00:00
continue
2021-11-23 21:49:47 +00:00
case pcpCodeAddressMismatch :
// A PCP service is running, but it is behind a NAT, so it can't help us.
res . PCP = false
2021-11-23 22:35:55 +00:00
metricPCPAddressMismatch . Add ( 1 )
2021-11-23 21:49:47 +00:00
continue
2021-03-17 22:33:45 +00:00
default :
// Fall through to unexpected log line.
}
2021-02-20 06:15:41 +00:00
}
2021-11-23 22:35:55 +00:00
metricPCPUnhandledResponseCode . Add ( 1 )
2021-02-20 06:15:41 +00:00
c . logf ( "unexpected PCP probe response: %+v" , pres )
}
if pres , ok := parsePMPResponse ( buf [ : n ] ) ; ok {
2021-11-22 18:27:56 +00:00
if pres . OpCode != pmpOpReply | pmpOpMapPublicAddr {
c . logf ( "unexpected PMP probe response opcode: %+v" , pres )
2021-11-23 22:35:55 +00:00
metricPMPUnhandledOpcode . Add ( 1 )
2021-11-22 18:27:56 +00:00
continue
}
switch pres . ResultCode {
case pmpCodeOK :
2021-11-23 22:35:55 +00:00
metricPMPOK . Add ( 1 )
2021-10-16 13:52:37 +00:00
c . logf ( "[v1] Got PMP response; IP: %v, epoch: %v" , pres . PublicAddr , pres . SecondsSinceEpoch )
2021-02-20 06:15:41 +00:00
res . PMP = true
c . mu . Lock ( )
c . pmpPubIP = pres . PublicAddr
c . pmpPubIPTime = time . Now ( )
c . pmpLastEpoch = pres . SecondsSinceEpoch
c . mu . Unlock ( )
continue
2021-11-23 22:35:55 +00:00
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 )
2021-11-22 18:27:56 +00:00
c . logf ( "PMP probe failed due result code: %+v" , pres )
continue
2021-02-20 06:15:41 +00:00
}
2021-11-23 22:35:55 +00:00
metricPMPUnhandledResponseCode . Add ( 1 )
2021-02-20 06:15:41 +00:00
c . logf ( "unexpected PMP probe response: %+v" , pres )
}
}
}
}
2021-08-05 17:32:13 +00:00
var pmpReqExternalAddrPacket = [ ] byte { pmpVersion , pmpOpMapPublicAddr } // 0, 0
2021-08-03 05:09:50 +00:00
const (
2021-08-09 19:52:15 +00:00
upnpDefaultPort = 1900 // for UDP discovery only; TCP port discovered later
2021-08-03 05:09:50 +00:00
)
// 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" )
2022-06-11 19:02:01 +00:00
// 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" )
2021-11-23 22:35:55 +00:00
2021-11-30 19:41:43 +00:00
// PCP/PMP metrics
2021-11-23 22:35:55 +00:00
var (
2021-11-30 19:42:06 +00:00
// 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" )
2021-11-23 22:35:55 +00:00
// 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" )
2021-11-30 19:42:06 +00:00
// metricPMPSent counts the number of times we sent a PMP request.
metricPMPSent = clientmetric . NewCounter ( "portmap_pmp_sent" )
2021-11-23 22:35:55 +00:00
// metricPMPOK counts the number of times
2023-04-17 22:38:24 +00:00
// we received a successful PMP response.
2021-11-23 22:35:55 +00:00
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" )
)
2021-11-30 19:41:43 +00:00
// 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" )
2023-03-04 05:23:26 +00:00
// 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" )
2021-11-30 19:41:43 +00:00
// 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" )
)