@ -64,9 +64,6 @@ const (
// icmpProbeTimeout is the maximum amount of time netcheck will spend
// icmpProbeTimeout is the maximum amount of time netcheck will spend
// probing with ICMP packets.
// probing with ICMP packets.
icmpProbeTimeout = 1 * time . Second
icmpProbeTimeout = 1 * time . Second
// hairpinCheckTimeout is the amount of time we wait for a
// hairpinned packet to come back.
hairpinCheckTimeout = 100 * time . Millisecond
// defaultActiveRetransmitTime is the retransmit interval we use
// defaultActiveRetransmitTime is the retransmit interval we use
// for STUN probes when we're in steady state (not in start-up),
// for STUN probes when we're in steady state (not in start-up),
// but don't have previous latency information for a DERP
// but don't have previous latency information for a DERP
@ -96,11 +93,6 @@ type Report struct {
// STUN server you're talking to (on IPv4).
// STUN server you're talking to (on IPv4).
MappingVariesByDestIP opt . Bool
MappingVariesByDestIP opt . Bool
// HairPinning is whether the router supports communicating
// between two local devices through the NATted public IP address
// (on IPv4).
HairPinning opt . Bool
// UPnP is whether UPnP appears present on the LAN.
// UPnP is whether UPnP appears present on the LAN.
// Empty means not checked.
// Empty means not checked.
UPnP opt . Bool
UPnP opt . Bool
@ -287,23 +279,6 @@ func (c *Client) vlogf(format string, a ...any) {
}
}
}
}
// handleHairSTUN reports whether pkt (from src) was our magic hairpin
// probe packet that we sent to ourselves.
func ( c * Client ) handleHairSTUNLocked ( pkt [ ] byte , src netip . AddrPort ) bool {
rs := c . curState
if rs == nil {
return false
}
if tx , err := stun . ParseBindingRequest ( pkt ) ; err == nil && tx == rs . hairTX {
select {
case rs . gotHairSTUN <- src :
default :
}
return true
}
return false
}
// MakeNextReportFull forces the next GetReport call to be a full
// MakeNextReportFull forces the next GetReport call to be a full
// (non-incremental) probe of all DERP regions.
// (non-incremental) probe of all DERP regions.
func ( c * Client ) MakeNextReportFull ( ) {
func ( c * Client ) MakeNextReportFull ( ) {
@ -326,10 +301,6 @@ func (c *Client) ReceiveSTUNPacket(pkt []byte, src netip.AddrPort) {
}
}
c . mu . Lock ( )
c . mu . Lock ( )
if c . handleHairSTUNLocked ( pkt , src ) {
c . mu . Unlock ( )
return
}
rs := c . curState
rs := c . curState
c . mu . Unlock ( )
c . mu . Unlock ( )
@ -340,6 +311,8 @@ func (c *Client) ReceiveSTUNPacket(pkt []byte, src netip.AddrPort) {
tx , addrPort , err := stun . ParseResponse ( pkt )
tx , addrPort , err := stun . ParseResponse ( pkt )
if err != nil {
if err != nil {
if _ , err := stun . ParseBindingRequest ( pkt ) ; err == nil {
if _ , err := stun . ParseBindingRequest ( pkt ) ; err == nil {
// We no longer send hairpin checks, but perhaps we might catch a
// stray from earlier versions.
// This was probably our own netcheck hairpin
// This was probably our own netcheck hairpin
// check probe coming in late. Ignore.
// check probe coming in late. Ignore.
return
return
@ -565,20 +538,15 @@ type reportState struct {
c * Client
c * Client
start time . Time
start time . Time
opts * GetReportOpts
opts * GetReportOpts
hairTX stun . TxID
gotHairSTUN chan netip . AddrPort
hairTimeout chan struct { } // closed on timeout
pc4Hair nettype . PacketConn
incremental bool // doing a lite, follow-up netcheck
incremental bool // doing a lite, follow-up netcheck
stopProbeCh chan struct { }
stopProbeCh chan struct { }
waitPortMap sync . WaitGroup
waitPortMap sync . WaitGroup
mu sync . Mutex
mu sync . Mutex
sentHairCheck bool
report * Report // to be returned by GetReport
report * Report // to be returned by GetReport
inFlight map [ stun . TxID ] func ( netip . AddrPort ) // called without c.mu held
inFlight map [ stun . TxID ] func ( netip . AddrPort ) // called without c.mu held
gotEP4 netip . AddrPort
gotEP4 netip . AddrPort
timers [ ] * time . Timer
timers [ ] * time . Timer
}
}
func ( rs * reportState ) anyUDP ( ) bool {
func ( rs * reportState ) anyUDP ( ) bool {
@ -628,50 +596,6 @@ func (rs *reportState) probeWouldHelp(probe probe, node *tailcfg.DERPNode) bool
return false
return false
}
}
func ( rs * reportState ) startHairCheckLocked ( dst netip . AddrPort ) {
if rs . sentHairCheck || rs . incremental {
return
}
rs . sentHairCheck = true
rs . pc4Hair . WriteToUDPAddrPort ( stun . Request ( rs . hairTX ) , dst )
rs . c . vlogf ( "sent haircheck to %v" , dst )
time . AfterFunc ( hairpinCheckTimeout , func ( ) { close ( rs . hairTimeout ) } )
}
func ( rs * reportState ) waitHairCheck ( ctx context . Context ) {
rs . mu . Lock ( )
defer rs . mu . Unlock ( )
ret := rs . report
if rs . incremental {
if rs . c . last != nil {
ret . HairPinning = rs . c . last . HairPinning
}
return
}
if ! rs . sentHairCheck {
return
}
// First, check whether we have a value before we check for timeouts.
select {
case <- rs . gotHairSTUN :
ret . HairPinning . Set ( true )
return
default :
}
// Now, wait for a response or a timeout.
select {
case <- rs . gotHairSTUN :
ret . HairPinning . Set ( true )
case <- rs . hairTimeout :
rs . c . vlogf ( "hairCheck timeout" )
ret . HairPinning . Set ( false )
case <- ctx . Done ( ) :
rs . c . vlogf ( "hairCheck context timeout" )
}
}
func ( rs * reportState ) stopTimers ( ) {
func ( rs * reportState ) stopTimers ( ) {
rs . mu . Lock ( )
rs . mu . Lock ( )
defer rs . mu . Unlock ( )
defer rs . mu . Unlock ( )
@ -720,7 +644,6 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netip.AddrPort
if ! rs . gotEP4 . IsValid ( ) {
if ! rs . gotEP4 . IsValid ( ) {
rs . gotEP4 = ipp
rs . gotEP4 = ipp
ret . GlobalV4 = ipp
ret . GlobalV4 = ipp
rs . startHairCheckLocked ( ipp )
} else {
} else {
if rs . gotEP4 != ipp {
if rs . gotEP4 != ipp {
ret . MappingVariesByDestIP . Set ( true )
ret . MappingVariesByDestIP . Set ( true )
@ -834,9 +757,6 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
opts : opts ,
opts : opts ,
report : newReport ( ) ,
report : newReport ( ) ,
inFlight : map [ stun . TxID ] func ( netip . AddrPort ) { } ,
inFlight : map [ stun . TxID ] func ( netip . AddrPort ) { } ,
hairTX : stun . NewTxID ( ) , // random payload
gotHairSTUN : make ( chan netip . AddrPort , 1 ) ,
hairTimeout : make ( chan struct { } ) ,
stopProbeCh : make ( chan struct { } , 1 ) ,
stopProbeCh : make ( chan struct { } , 1 ) ,
}
}
c . curState = rs
c . curState = rs
@ -894,34 +814,11 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
v6udp . Close ( )
v6udp . Close ( )
}
}
// Create a UDP4 socket used for sending to our discovered IPv4 address.
rs . pc4Hair , err = nettype . MakePacketListenerWithNetIP ( netns . Listener ( c . logf , c . NetMon ) ) . ListenPacket ( ctx , "udp4" , ":0" )
if err != nil {
c . logf ( "udp4: %v" , err )
return nil , err
}
defer rs . pc4Hair . Close ( )
if ! c . SkipExternalNetwork && c . PortMapper != nil {
if ! c . SkipExternalNetwork && c . PortMapper != nil {
rs . waitPortMap . Add ( 1 )
rs . waitPortMap . Add ( 1 )
go rs . probePortMapServices ( )
go rs . probePortMapServices ( )
}
}
// At least the Apple Airport Extreme doesn't allow hairpin
// sends from a private socket until it's seen traffic from
// that src IP:port to something else out on the internet.
//
// See https://github.com/tailscale/tailscale/issues/188#issuecomment-600728643
//
// And it seems that even sending to a likely-filtered RFC 5737
// documentation-only IPv4 range is enough to set up the mapping.
// So do that for now. In the future we might want to classify networks
// that do and don't require this separately. But for now help it.
const documentationIP = "203.0.113.1"
rs . pc4Hair . WriteToUDPAddrPort (
[ ] byte ( "tailscale netcheck; see https://github.com/tailscale/tailscale/issues/188" ) ,
netip . AddrPortFrom ( netip . MustParseAddr ( documentationIP ) , 12345 ) )
plan := makeProbePlan ( dm , ifState , last )
plan := makeProbePlan ( dm , ifState , last )
// If we're doing a full probe, also check for a captive portal. We
// If we're doing a full probe, also check for a captive portal. We
@ -999,8 +896,6 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
captivePortalStop ( )
captivePortalStop ( )
}
}
rs . waitHairCheck ( ctx )
c . vlogf ( "hairCheck done" )
if ! c . SkipExternalNetwork && c . PortMapper != nil {
if ! c . SkipExternalNetwork && c . PortMapper != nil {
rs . waitPortMap . Wait ( )
rs . waitPortMap . Wait ( )
c . vlogf ( "portMap done" )
c . vlogf ( "portMap done" )
@ -1369,7 +1264,6 @@ func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) {
fmt . Fprintf ( w , " v6os=%v" , r . OSHasIPv6 )
fmt . Fprintf ( w , " v6os=%v" , r . OSHasIPv6 )
}
}
fmt . Fprintf ( w , " mapvarydest=%v" , r . MappingVariesByDestIP )
fmt . Fprintf ( w , " mapvarydest=%v" , r . MappingVariesByDestIP )
fmt . Fprintf ( w , " hair=%v" , r . HairPinning )
if r . AnyPortMappingChecked ( ) {
if r . AnyPortMappingChecked ( ) {
fmt . Fprintf ( w , " portmap=%v%v%v" , conciseOptBool ( r . UPnP , "U" ) , conciseOptBool ( r . PMP , "M" ) , conciseOptBool ( r . PCP , "C" ) )
fmt . Fprintf ( w , " portmap=%v%v%v" , conciseOptBool ( r . UPnP , "U" ) , conciseOptBool ( r . PMP , "M" ) , conciseOptBool ( r . PCP , "C" ) )
} else {
} else {