2023-01-27 21:37:20 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2020-02-25 22:05:17 +00:00
// Package netcheck checks the network conditions from the current host.
package netcheck
import (
2020-06-12 04:37:15 +00:00
"bufio"
2020-02-25 22:05:17 +00:00
"context"
2020-05-30 05:33:08 +00:00
"crypto/tls"
2020-03-09 22:20:33 +00:00
"errors"
2020-03-04 21:40:29 +00:00
"fmt"
2020-02-25 22:05:17 +00:00
"io"
2020-03-09 22:20:33 +00:00
"log"
2022-09-20 19:31:49 +00:00
"math/rand"
2020-02-25 22:05:17 +00:00
"net"
2020-05-11 15:23:09 +00:00
"net/http"
2022-07-25 03:08:42 +00:00
"net/netip"
2021-10-22 16:08:15 +00:00
"runtime"
2020-05-05 06:22:19 +00:00
"sort"
2022-11-06 04:44:33 +00:00
"strings"
2020-02-25 22:05:17 +00:00
"sync"
"time"
2020-05-11 15:23:09 +00:00
"github.com/tcnksm/go-httpstat"
2020-05-29 20:31:08 +00:00
"tailscale.com/derp/derphttp"
2022-01-24 18:52:57 +00:00
"tailscale.com/envknob"
2020-03-10 18:02:30 +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"
2020-05-28 22:27:04 +00:00
"tailscale.com/net/netns"
2022-08-04 21:10:13 +00:00
"tailscale.com/net/ping"
2021-02-20 06:15:41 +00:00
"tailscale.com/net/portmapper"
2023-02-03 20:07:58 +00:00
"tailscale.com/net/sockstats"
2020-05-25 16:15:50 +00:00
"tailscale.com/net/stun"
2020-05-17 16:51:38 +00:00
"tailscale.com/syncs"
"tailscale.com/tailcfg"
2020-02-25 22:05:17 +00:00
"tailscale.com/types/logger"
2022-07-25 03:08:42 +00:00
"tailscale.com/types/nettype"
2020-02-25 22:05:17 +00:00
"tailscale.com/types/opt"
2021-11-16 16:34:25 +00:00
"tailscale.com/util/clientmetric"
2022-08-04 21:10:13 +00:00
"tailscale.com/util/mak"
2020-02-25 22:05:17 +00:00
)
2020-07-25 02:29:27 +00:00
// Debugging and experimentation tweakables.
var (
2022-09-14 19:49:39 +00:00
debugNetcheck = envknob . RegisterBool ( "TS_DEBUG_NETCHECK" )
2020-07-25 02:29:27 +00:00
)
// The various default timeouts for things.
const (
// overallProbeTimeout is the maximum amount of time netcheck will
// spend gathering a single report.
overallProbeTimeout = 5 * time . Second
// stunTimeout is the maximum amount of time netcheck will spend
// probing with STUN packets without getting a reply before
// switching to HTTP probing, on the assumption that outbound UDP
// is blocked.
stunProbeTimeout = 3 * time . Second
2022-08-04 21:10:13 +00:00
// icmpProbeTimeout is the maximum amount of time netcheck will spend
// probing with ICMP packets.
icmpProbeTimeout = 1 * time . Second
2020-07-25 02:29:27 +00:00
// hairpinCheckTimeout is the amount of time we wait for a
// hairpinned packet to come back.
2020-07-25 23:17:09 +00:00
hairpinCheckTimeout = 100 * time . Millisecond
2020-07-25 02:29:27 +00:00
// defaultActiveRetransmitTime is the retransmit interval we use
// for STUN probes when we're in steady state (not in start-up),
// but don't have previous latency information for a DERP
// node. This is a somewhat conservative guess because if we have
// no data, likely the DERP node is very far away and we have no
// data because we timed out the last time we probed it.
defaultActiveRetransmitTime = 200 * time . Millisecond
// defaultInitialRetransmitTime is the retransmit interval used
// when netcheck first runs. We have no past context to work with,
// and we want answers relatively quickly, so it's biased slightly
// more aggressive than defaultActiveRetransmitTime. A few extra
// packets at startup is fine.
defaultInitialRetransmitTime = 100 * time . Millisecond
)
2020-02-25 22:05:17 +00:00
type Report struct {
2021-10-07 00:43:37 +00:00
UDP bool // a UDP STUN round trip completed
IPv6 bool // an IPv6 STUN round trip completed
IPv4 bool // an IPv4 STUN round trip completed
IPv6CanSend bool // an IPv6 packet was able to be sent
IPv4CanSend bool // an IPv4 packet was able to be sent
2022-07-18 23:56:10 +00:00
OSHasIPv6 bool // could bind a socket to ::1
2022-08-04 21:10:13 +00:00
ICMPv4 bool // an ICMPv4 round trip completed
2021-10-07 00:43:37 +00:00
// MappingVariesByDestIP is whether STUN results depend which
// STUN server you're talking to (on IPv4).
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
2020-07-06 20:51:17 +00:00
// UPnP is whether UPnP appears present on the LAN.
// Empty means not checked.
UPnP opt . Bool
// PMP is whether NAT-PMP appears present on the LAN.
// Empty means not checked.
PMP opt . Bool
// PCP is whether PCP appears present on the LAN.
// Empty means not checked.
PCP opt . Bool
PreferredDERP int // or 0 for unknown
RegionLatency map [ int ] time . Duration // keyed by DERP Region ID
RegionV4Latency map [ int ] time . Duration // keyed by DERP Region ID
RegionV6Latency map [ int ] time . Duration // keyed by DERP Region ID
2020-03-02 23:02:34 +00:00
2020-03-09 22:20:33 +00:00
GlobalV4 string // ip:port of global IPv4
2020-05-17 16:51:38 +00:00
GlobalV6 string // [ip]:port of global IPv6
2020-03-09 22:20:33 +00:00
2022-09-20 19:31:49 +00:00
// CaptivePortal is set when we think there's a captive portal that is
// intercepting HTTP traffic.
CaptivePortal opt . Bool
2020-03-02 23:02:34 +00:00
// TODO: update Clone when adding new fields
}
2020-07-06 20:51:17 +00:00
// AnyPortMappingChecked reports whether any of UPnP, PMP, or PCP are non-empty.
func ( r * Report ) AnyPortMappingChecked ( ) bool {
return r . UPnP != "" || r . PMP != "" || r . PCP != ""
}
2020-03-02 23:02:34 +00:00
func ( r * Report ) Clone ( ) * Report {
if r == nil {
return nil
}
r2 := * r
2020-05-17 16:51:38 +00:00
r2 . RegionLatency = cloneDurationMap ( r2 . RegionLatency )
r2 . RegionV4Latency = cloneDurationMap ( r2 . RegionV4Latency )
r2 . RegionV6Latency = cloneDurationMap ( r2 . RegionV6Latency )
2020-03-02 23:02:34 +00:00
return & r2
2020-02-25 22:05:17 +00:00
}
2020-05-17 16:51:38 +00:00
func cloneDurationMap ( m map [ int ] time . Duration ) map [ int ] time . Duration {
if m == nil {
return nil
}
m2 := make ( map [ int ] time . Duration , len ( m ) )
for k , v := range m {
m2 [ k ] = v
}
return m2
}
2020-03-09 22:20:33 +00:00
// Client generates a netcheck Report.
type Client struct {
2020-05-28 16:58:52 +00:00
// Verbose enables verbose logging.
Verbose bool
2020-03-09 22:20:33 +00:00
// Logf optionally specifies where to log to.
2020-05-17 16:51:38 +00:00
// If nil, log.Printf is used.
2020-03-09 22:20:33 +00:00
Logf logger . Logf
2020-03-18 20:04:12 +00:00
// TimeNow, if non-nil, is used instead of time.Now.
TimeNow func ( ) time . Time
2020-05-17 16:51:38 +00:00
// GetSTUNConn4 optionally provides a func to return the
// connection to use for sending & receiving IPv4 packets. If
2022-09-25 18:29:55 +00:00
// nil, an ephemeral one is created as needed.
2020-03-09 22:20:33 +00:00
GetSTUNConn4 func ( ) STUNConn
2020-05-17 16:51:38 +00:00
// GetSTUNConn6 is like GetSTUNConn4, but for IPv6.
2020-03-09 22:20:33 +00:00
GetSTUNConn6 func ( ) STUNConn
2020-03-04 21:40:29 +00:00
2020-10-28 15:23:12 +00:00
// SkipExternalNetwork controls whether the client should not try
// to reach things other than localhost. This is set to true
// in tests to avoid probing the local LAN's router, etc.
SkipExternalNetwork bool
2020-10-28 16:10:35 +00:00
// UDPBindAddr, if non-empty, is the address to listen on for UDP.
// It defaults to ":0".
UDPBindAddr string
2021-02-20 06:15:41 +00:00
// PortMapper, if non-nil, is used for portmap queries.
// If nil, portmap discovery is not done.
PortMapper * portmapper . Client // lazily initialized on first use
2022-09-20 19:31:49 +00:00
// For tests
testEnoughRegions int
testCaptivePortalDelay time . Duration
2020-05-17 16:51:38 +00:00
mu sync . Mutex // guards following
nextFull bool // do a full region scan, even if last != nil
prev map [ time . Time ] * Report // some previous reports
last * Report // most recent report
lastFull time . Time // time of last full (non-incremental) report
curState * reportState // non-nil if we're in a call to GetReportn
2020-03-04 21:40:29 +00:00
}
2020-03-09 22:20:33 +00:00
// STUNConn is the interface required by the netcheck Client when
// reusing an existing UDP connection.
type STUNConn interface {
2022-07-25 03:08:42 +00:00
WriteToUDPAddrPort ( [ ] byte , netip . AddrPort ) ( int , error )
2020-03-09 22:20:33 +00:00
WriteTo ( [ ] byte , net . Addr ) ( int , error )
ReadFrom ( [ ] byte ) ( int , net . Addr , error )
}
2020-03-04 21:40:29 +00:00
2020-08-20 03:47:17 +00:00
func ( c * Client ) enoughRegions ( ) int {
2022-09-20 19:31:49 +00:00
if c . testEnoughRegions > 0 {
return c . testEnoughRegions
}
2020-08-20 03:47:17 +00:00
if c . Verbose {
// Abuse verbose a bit here so netcheck can show all region latencies
// in verbose mode.
return 100
}
return 3
}
2022-09-20 19:31:49 +00:00
func ( c * Client ) captivePortalDelay ( ) time . Duration {
if c . testCaptivePortalDelay > 0 {
return c . testCaptivePortalDelay
}
// Chosen semi-arbitrarily
return 200 * time . Millisecond
}
2022-03-16 23:27:57 +00:00
func ( c * Client ) logf ( format string , a ... any ) {
2020-03-09 22:20:33 +00:00
if c . Logf != nil {
c . Logf ( format , a ... )
} else {
log . Printf ( format , a ... )
}
}
2020-03-04 21:40:29 +00:00
2022-03-16 23:27:57 +00:00
func ( c * Client ) vlogf ( format string , a ... any ) {
2022-09-14 19:49:39 +00:00
if c . Verbose || debugNetcheck ( ) {
2020-05-28 16:58:52 +00:00
c . logf ( format , a ... )
}
}
2020-03-11 04:30:04 +00:00
// handleHairSTUN reports whether pkt (from src) was our magic hairpin
// probe packet that we sent to ourselves.
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 ) handleHairSTUNLocked ( pkt [ ] byte , src netip . AddrPort ) bool {
2020-05-17 16:51:38 +00:00
rs := c . curState
if rs == nil {
return false
}
if tx , err := stun . ParseBindingRequest ( pkt ) ; err == nil && tx == rs . hairTX {
2020-03-11 04:30:04 +00:00
select {
2020-05-17 16:51:38 +00:00
case rs . gotHairSTUN <- src :
2020-03-11 04:30:04 +00:00
default :
}
return true
}
return false
}
2020-05-17 16:51:38 +00:00
// MakeNextReportFull forces the next GetReport call to be a full
// (non-incremental) probe of all DERP regions.
func ( c * Client ) MakeNextReportFull ( ) {
c . mu . Lock ( )
2021-02-20 06:15:41 +00:00
defer c . mu . Unlock ( )
2020-05-17 16:51:38 +00:00
c . nextFull = 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 ) ReceiveSTUNPacket ( pkt [ ] byte , src netip . AddrPort ) {
2020-07-25 02:29:27 +00:00
c . vlogf ( "received STUN packet from %s" , src )
2022-07-25 03:08:42 +00:00
if src . Addr ( ) . Is4 ( ) {
2021-11-16 16:34:25 +00:00
metricSTUNRecv4 . Add ( 1 )
2022-07-25 03:08:42 +00:00
} else if src . Addr ( ) . Is6 ( ) {
2021-11-16 16:34:25 +00:00
metricSTUNRecv6 . Add ( 1 )
}
2020-03-11 22:35:12 +00:00
c . mu . Lock ( )
if c . handleHairSTUNLocked ( pkt , src ) {
c . mu . Unlock ( )
2020-03-11 04:30:04 +00:00
return
}
2020-05-17 16:51:38 +00:00
rs := c . curState
c . mu . Unlock ( )
2020-03-11 22:35:12 +00:00
2020-05-17 16:51:38 +00:00
if rs == nil {
return
2020-03-04 21:40:29 +00:00
}
2020-03-11 22:35:12 +00:00
2022-08-12 19:22:05 +00:00
tx , addrPort , err := stun . ParseResponse ( pkt )
2020-05-17 16:51:38 +00:00
if err != nil {
if _ , err := stun . ParseBindingRequest ( pkt ) ; err == nil {
// This was probably our own netcheck hairpin
// check probe coming in late. Ignore.
return
}
c . logf ( "netcheck: received unexpected STUN message response from %v: %v" , src , err )
return
}
2020-03-11 22:35:12 +00:00
2020-05-17 16:51:38 +00:00
rs . mu . Lock ( )
onDone , ok := rs . inFlight [ tx ]
if ok {
delete ( rs . inFlight , tx )
}
rs . mu . Unlock ( )
if ok {
2022-08-12 19:22:05 +00:00
onDone ( addrPort )
2020-03-09 22:20:33 +00:00
}
}
2020-03-04 21:40:29 +00:00
2020-05-17 16:51:38 +00:00
// probeProto is the protocol used to time a node's latency.
type probeProto uint8
const (
probeIPv4 probeProto = iota // STUN IPv4
probeIPv6 // STUN IPv6
probeHTTPS // HTTPS
)
type probe struct {
// delay is when the probe is started, relative to the time
// that GetReport is called. One probe in each probePlan
// should have a delay of 0. Non-zero values are for retries
// on UDP loss or timeout.
delay time . Duration
// node is the name of the node name. DERP node names are globally
// unique so there's no region ID.
node string
// proto is how the node should be probed.
proto probeProto
// wait is how long to wait until the probe is considered failed.
// 0 means to use a default value.
wait time . Duration
}
// probePlan is a set of node probes to run.
// The map key is a descriptive name, only used for tests.
2020-05-05 06:22:19 +00:00
//
2020-05-17 16:51:38 +00:00
// The values are logically an unordered set of tests to run concurrently.
// In practice there's some order to them based on their delay fields,
// but multiple probes can have the same delay time or be running concurrently
// both within and between sets.
2020-05-05 06:22:19 +00:00
//
2020-05-17 16:51:38 +00:00
// A set of probes is done once either one of the probes completes, or
// the next probe to run wouldn't yield any new information not
// already discovered by any previous probe in any set.
type probePlan map [ string ] [ ] probe
// sortRegions returns the regions of dm first sorted
// from fastest to slowest (based on the 'last' report),
// end in regions that have no data.
func sortRegions ( dm * tailcfg . DERPMap , last * Report ) ( prev [ ] * tailcfg . DERPRegion ) {
prev = make ( [ ] * tailcfg . DERPRegion , 0 , len ( dm . Regions ) )
for _ , reg := range dm . Regions {
2021-03-12 18:34:20 +00:00
if reg . Avoid {
continue
}
2020-05-17 16:51:38 +00:00
prev = append ( prev , reg )
}
sort . Slice ( prev , func ( i , j int ) bool {
da , db := last . RegionLatency [ prev [ i ] . RegionID ] , last . RegionLatency [ prev [ j ] . RegionID ]
if db == 0 && da != 0 {
// Non-zero sorts before zero.
return true
}
if da == 0 {
// Zero can't sort before anything else.
return false
}
return da < db
} )
return prev
}
// numIncrementalRegions is the number of fastest regions to
// periodically re-query during incremental netcheck reports. (During
// a full report, all regions are scanned.)
const numIncrementalRegions = 3
// makeProbePlan generates the probe plan for a DERPMap, given the most
// recent report and whether IPv6 is configured on an interface.
2020-05-28 16:58:52 +00:00
func makeProbePlan ( dm * tailcfg . DERPMap , ifState * interfaces . State , last * Report ) ( plan probePlan ) {
2020-05-17 16:51:38 +00:00
if last == nil || len ( last . RegionLatency ) == 0 {
2020-05-28 16:58:52 +00:00
return makeProbePlanInitial ( dm , ifState )
2020-05-17 16:51:38 +00:00
}
2021-06-18 00:49:15 +00:00
have6if := ifState . HaveV6
2020-05-28 16:58:52 +00:00
have4if := ifState . HaveV4
2020-05-17 16:51:38 +00:00
plan = make ( probePlan )
2020-05-28 16:58:52 +00:00
if ! have4if && ! have6if {
return plan
}
2020-05-17 16:51:38 +00:00
had4 := len ( last . RegionV4Latency ) > 0
had6 := len ( last . RegionV6Latency ) > 0
hadBoth := have6if && had4 && had6
for ri , reg := range sortRegions ( dm , last ) {
if ri == numIncrementalRegions {
break
}
var p4 , p6 [ ] probe
2020-05-28 16:58:52 +00:00
do4 := have4if
2020-05-17 16:51:38 +00:00
do6 := have6if
// By default, each node only gets one STUN packet sent,
// except the fastest two from the previous round.
tries := 1
isFastestTwo := ri < 2
if isFastestTwo {
tries = 2
} else if hadBoth {
// For dual stack machines, make the 3rd & slower nodes alternate
2021-08-24 14:36:48 +00:00
// between.
2020-05-17 16:51:38 +00:00
if ri % 2 == 0 {
do4 , do6 = true , false
} else {
do4 , do6 = false , true
}
}
if ! isFastestTwo && ! had6 {
do6 = false
}
2020-05-05 06:22:19 +00:00
2021-03-12 19:34:49 +00:00
if reg . RegionID == last . PreferredDERP {
// But if we already had a DERP home, try extra hard to
// make sure it's there so we don't flip flop around.
tries = 4
}
2020-05-17 16:51:38 +00:00
for try := 0 ; try < tries ; try ++ {
if len ( reg . Nodes ) == 0 {
// Shouldn't be possible.
continue
}
if try != 0 && ! had6 {
do6 = false
}
n := reg . Nodes [ try % len ( reg . Nodes ) ]
prevLatency := last . RegionLatency [ reg . RegionID ] * 120 / 100
if prevLatency == 0 {
2020-07-25 02:29:27 +00:00
prevLatency = defaultActiveRetransmitTime
2020-05-17 16:51:38 +00:00
}
delay := time . Duration ( try ) * prevLatency
2021-03-12 19:34:49 +00:00
if try > 1 {
delay += time . Duration ( try ) * 50 * time . Millisecond
}
2020-05-17 16:51:38 +00:00
if do4 {
p4 = append ( p4 , probe { delay : delay , node : n . Name , proto : probeIPv4 } )
}
if do6 {
p6 = append ( p6 , probe { delay : delay , node : n . Name , proto : probeIPv6 } )
}
}
if len ( p4 ) > 0 {
plan [ fmt . Sprintf ( "region-%d-v4" , reg . RegionID ) ] = p4
}
if len ( p6 ) > 0 {
plan [ fmt . Sprintf ( "region-%d-v6" , reg . RegionID ) ] = p6
}
}
return plan
}
2020-05-05 06:22:19 +00:00
2020-05-28 16:58:52 +00:00
func makeProbePlanInitial ( dm * tailcfg . DERPMap , ifState * interfaces . State ) ( plan probePlan ) {
2020-05-17 16:51:38 +00:00
plan = make ( probePlan )
for _ , reg := range dm . Regions {
var p4 [ ] probe
var p6 [ ] probe
for try := 0 ; try < 3 ; try ++ {
n := reg . Nodes [ try % len ( reg . Nodes ) ]
2020-07-25 02:29:27 +00:00
delay := time . Duration ( try ) * defaultInitialRetransmitTime
2020-05-28 16:58:52 +00:00
if ifState . HaveV4 && nodeMight4 ( n ) {
2020-05-17 16:51:38 +00:00
p4 = append ( p4 , probe { delay : delay , node : n . Name , proto : probeIPv4 } )
}
2021-06-18 00:49:15 +00:00
if ifState . HaveV6 && nodeMight6 ( n ) {
2020-05-17 16:51:38 +00:00
p6 = append ( p6 , probe { delay : delay , node : n . Name , proto : probeIPv6 } )
2020-05-05 06:22:19 +00:00
}
}
2020-05-17 16:51:38 +00:00
if len ( p4 ) > 0 {
plan [ fmt . Sprintf ( "region-%d-v4" , reg . RegionID ) ] = p4
}
if len ( p6 ) > 0 {
plan [ fmt . Sprintf ( "region-%d-v6" , reg . RegionID ) ] = p6
}
}
return plan
}
// nodeMight6 reports whether n might reply to STUN over IPv6 based on
// its config alone, without DNS lookups. It only returns false if
// it's not explicitly disabled.
func nodeMight6 ( n * tailcfg . DERPNode ) bool {
if n . IPv6 == "" {
return true
}
2022-07-26 03:55:44 +00:00
ip , _ := netip . ParseAddr ( n . IPv6 )
2020-05-17 16:51:38 +00:00
return ip . Is6 ( )
}
// nodeMight4 reports whether n might reply to STUN over IPv4 based on
// its config alone, without DNS lookups. It only returns false if
// it's not explicitly disabled.
func nodeMight4 ( n * tailcfg . DERPNode ) bool {
if n . IPv4 == "" {
return true
2020-05-05 06:22:19 +00:00
}
2022-07-26 03:55:44 +00:00
ip , _ := netip . ParseAddr ( n . IPv4 )
2020-05-17 16:51:38 +00:00
return ip . Is4 ( )
}
2020-05-05 06:22:19 +00:00
2020-05-17 16:51:38 +00:00
// readPackets reads STUN packets from pc until there's an error or ctx is done.
// In either case, it closes pc.
func ( c * Client ) readPackets ( ctx context . Context , pc net . PacketConn ) {
done := make ( chan struct { } )
defer close ( done )
go func ( ) {
select {
case <- ctx . Done ( ) :
case <- done :
}
pc . Close ( )
} ( )
var buf [ 64 << 10 ] byte
for {
n , addr , err := pc . ReadFrom ( buf [ : ] )
if err != nil {
if ctx . Err ( ) != nil {
return
}
c . logf ( "ReadFrom: %v" , err )
2020-05-05 06:22:19 +00:00
return
}
2020-05-17 16:51:38 +00:00
ua , ok := addr . ( * net . UDPAddr )
if ! ok {
c . logf ( "ReadFrom: unexpected addr %T" , addr )
continue
2020-05-05 06:22:19 +00:00
}
2020-05-17 16:51:38 +00:00
pkt := buf [ : n ]
if ! stun . Is ( pkt ) {
continue
}
2022-08-03 04:48:56 +00:00
if ap := netaddr . Unmap ( ua . AddrPort ( ) ) ; ap . IsValid ( ) {
c . ReceiveSTUNPacket ( pkt , ap )
2020-06-30 20:25:13 +00:00
}
2020-05-17 16:51:38 +00:00
}
}
2020-05-05 06:22:19 +00:00
2020-05-17 16:51:38 +00:00
// reportState holds the state for a single invocation of Client.GetReport.
type reportState struct {
c * Client
hairTX stun . TxID
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
gotHairSTUN chan netip . AddrPort
2020-05-17 16:51:38 +00:00
hairTimeout chan struct { } // closed on timeout
pc4 STUNConn
pc6 STUNConn
2022-07-25 03:08:42 +00:00
pc4Hair nettype . PacketConn
2020-05-28 16:58:52 +00:00
incremental bool // doing a lite, follow-up netcheck
stopProbeCh chan struct { }
2020-07-06 20:51:17 +00:00
waitPortMap sync . WaitGroup
2020-05-17 16:51:38 +00:00
mu sync . Mutex
sentHairCheck bool
report * Report // to be returned by GetReport
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
inFlight map [ stun . TxID ] func ( netip . AddrPort ) // called without c.mu held
2020-05-17 16:51:38 +00:00
gotEP4 string
2020-05-28 16:58:52 +00:00
timers [ ] * time . Timer
2020-05-17 16:51:38 +00:00
}
func ( rs * reportState ) anyUDP ( ) bool {
rs . mu . Lock ( )
defer rs . mu . Unlock ( )
return rs . report . UDP
}
func ( rs * reportState ) haveRegionLatency ( regionID int ) bool {
rs . mu . Lock ( )
defer rs . mu . Unlock ( )
_ , ok := rs . report . RegionLatency [ regionID ]
return ok
}
// probeWouldHelp reports whether executing the given probe would
// yield any new information.
// The given node is provided just because the sole caller already has it
// and it saves a lookup.
func ( rs * reportState ) probeWouldHelp ( probe probe , node * tailcfg . DERPNode ) bool {
rs . mu . Lock ( )
defer rs . mu . Unlock ( )
// If the probe is for a region we don't yet know about, that
// would help.
if _ , ok := rs . report . RegionLatency [ node . RegionID ] ; ! ok {
return true
}
// If the probe is for IPv6 and we don't yet have an IPv6
// report, that would help.
if probe . proto == probeIPv6 && len ( rs . report . RegionV6Latency ) == 0 {
return true
}
// For IPv4, we need at least two IPv4 results overall to
// determine whether we're behind a NAT that shows us as
// different source IPs and/or ports depending on who we're
// talking to. If we don't yet have two results yet
// (MappingVariesByDestIP is blank), then another IPv4 probe
// would be good.
if probe . proto == probeIPv4 && rs . report . MappingVariesByDestIP == "" {
return true
}
// Otherwise not interesting.
return false
}
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 ( rs * reportState ) startHairCheckLocked ( dst netip . AddrPort ) {
2020-05-28 16:58:52 +00:00
if rs . sentHairCheck || rs . incremental {
2020-05-17 16:51:38 +00:00
return
}
rs . sentHairCheck = true
2022-07-25 03:08:42 +00:00
rs . pc4Hair . WriteToUDPAddrPort ( stun . Request ( rs . hairTX ) , dst )
rs . c . vlogf ( "sent haircheck to %v" , dst )
2020-07-25 02:29:27 +00:00
time . AfterFunc ( hairpinCheckTimeout , func ( ) { close ( rs . hairTimeout ) } )
2020-05-17 16:51:38 +00:00
}
func ( rs * reportState ) waitHairCheck ( ctx context . Context ) {
rs . mu . Lock ( )
defer rs . mu . Unlock ( )
2020-05-28 16:58:52 +00:00
ret := rs . report
if rs . incremental {
if rs . c . last != nil {
ret . HairPinning = rs . c . last . HairPinning
}
return
}
2020-05-17 16:51:38 +00:00
if ! rs . sentHairCheck {
return
}
select {
case <- rs . gotHairSTUN :
ret . HairPinning . Set ( true )
case <- rs . hairTimeout :
2020-07-25 02:29:27 +00:00
rs . c . vlogf ( "hairCheck timeout" )
2020-05-17 16:51:38 +00:00
ret . HairPinning . Set ( false )
default :
select {
case <- rs . gotHairSTUN :
ret . HairPinning . Set ( true )
case <- rs . hairTimeout :
ret . HairPinning . Set ( false )
case <- ctx . Done ( ) :
}
}
}
2020-05-28 16:58:52 +00:00
func ( rs * reportState ) stopTimers ( ) {
rs . mu . Lock ( )
defer rs . mu . Unlock ( )
for _ , t := range rs . timers {
t . Stop ( )
}
}
2020-05-17 16:51:38 +00:00
// addNodeLatency updates rs to note that node's latency is d. If ipp
// is non-zero (for all but HTTPS replies), it's recorded as our UDP
// IP:port.
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 ( rs * reportState ) addNodeLatency ( node * tailcfg . DERPNode , ipp netip . AddrPort , d time . Duration ) {
2020-05-17 16:51:38 +00:00
var ipPortStr string
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
if ipp != ( netip . AddrPort { } ) {
2022-07-25 03:08:42 +00:00
ipPortStr = net . JoinHostPort ( ipp . Addr ( ) . String ( ) , fmt . Sprint ( ipp . Port ( ) ) )
2020-05-17 16:51:38 +00:00
}
rs . mu . Lock ( )
defer rs . mu . Unlock ( )
ret := rs . report
ret . UDP = true
2020-05-28 07:37:46 +00:00
updateLatency ( ret . RegionLatency , node . RegionID , d )
2020-05-17 16:51:38 +00:00
2020-08-20 03:47:17 +00:00
// Once we've heard from enough regions (3), start a timer to
// give up on the other ones. The timer's duration is a
// function of whether this is our initial full probe or an
// incremental one. For incremental ones, wait for the
// duration of the slowest region. For initial ones, double
// that.
if len ( ret . RegionLatency ) == rs . c . enoughRegions ( ) {
2020-05-28 16:58:52 +00:00
timeout := maxDurationValue ( ret . RegionLatency )
if ! rs . incremental {
timeout *= 2
}
rs . timers = append ( rs . timers , time . AfterFunc ( timeout , rs . stopProbes ) )
}
2020-05-17 16:51:38 +00:00
switch {
2022-07-25 03:08:42 +00:00
case ipp . Addr ( ) . Is6 ( ) :
2020-05-28 07:37:46 +00:00
updateLatency ( ret . RegionV6Latency , node . RegionID , d )
2020-05-17 16:51:38 +00:00
ret . IPv6 = true
ret . GlobalV6 = ipPortStr
// TODO: track MappingVariesByDestIP for IPv6
// too? Would be sad if so, but who knows.
2022-07-25 03:08:42 +00:00
case ipp . Addr ( ) . Is4 ( ) :
2020-05-28 07:37:46 +00:00
updateLatency ( ret . RegionV4Latency , node . RegionID , d )
2020-05-29 19:33:48 +00:00
ret . IPv4 = true
2020-05-17 16:51:38 +00:00
if rs . gotEP4 == "" {
rs . gotEP4 = ipPortStr
ret . GlobalV4 = ipPortStr
rs . startHairCheckLocked ( ipp )
} else {
if rs . gotEP4 != ipPortStr {
ret . MappingVariesByDestIP . Set ( true )
} else if ret . MappingVariesByDestIP == "" {
ret . MappingVariesByDestIP . Set ( false )
}
}
2020-05-05 06:22:19 +00:00
}
}
2020-05-28 16:58:52 +00:00
func ( rs * reportState ) stopProbes ( ) {
select {
case rs . stopProbeCh <- struct { } { } :
default :
}
}
2020-07-06 20:51:17 +00:00
func ( rs * reportState ) setOptBool ( b * opt . Bool , v bool ) {
rs . mu . Lock ( )
defer rs . mu . Unlock ( )
b . Set ( v )
}
func ( rs * reportState ) probePortMapServices ( ) {
defer rs . waitPortMap . Done ( )
rs . setOptBool ( & rs . report . UPnP , false )
rs . setOptBool ( & rs . report . PMP , false )
rs . setOptBool ( & rs . report . PCP , false )
2021-02-20 06:15:41 +00:00
res , err := rs . c . PortMapper . Probe ( context . Background ( ) )
2020-07-06 20:51:17 +00:00
if err != nil {
2021-10-10 15:46:28 +00:00
if ! errors . Is ( err , portmapper . ErrGatewayRange ) {
// "skipping portmap; gateway range likely lacks support"
// is not very useful, and too spammy on cloud systems.
// If there are other errors, we want to log those.
rs . c . logf ( "probePortMapServices: %v" , err )
}
2020-07-06 20:51:17 +00:00
return
}
2020-12-08 23:22:26 +00:00
2021-02-20 06:15:41 +00:00
rs . setOptBool ( & rs . report . UPnP , res . UPnP )
rs . setOptBool ( & rs . report . PMP , res . PMP )
rs . setOptBool ( & rs . report . PCP , res . PCP )
2020-07-06 20:51:17 +00:00
}
2020-05-28 07:37:46 +00:00
func newReport ( ) * Report {
return & Report {
RegionLatency : make ( map [ int ] time . Duration ) ,
RegionV4Latency : make ( map [ int ] time . Duration ) ,
RegionV6Latency : make ( map [ int ] time . Duration ) ,
}
}
2020-10-28 16:10:35 +00:00
func ( c * Client ) udpBindAddr ( ) string {
if v := c . UDPBindAddr ; v != "" {
return v
}
return ":0"
}
2020-03-09 22:20:33 +00:00
// GetReport gets a report.
//
// It may not be called concurrently with itself.
2021-11-16 16:34:25 +00:00
func ( c * Client ) GetReport ( ctx context . Context , dm * tailcfg . DERPMap ) ( _ * Report , reterr error ) {
defer func ( ) {
if reterr != nil {
metricNumGetReportError . Add ( 1 )
}
} ( )
metricNumGetReport . Add ( 1 )
2020-02-28 22:14:02 +00:00
// Mask user context with ours that we guarantee to cancel so
// we can depend on it being closed in goroutines later.
// (User ctx might be context.Background, etc)
2020-07-25 02:29:27 +00:00
ctx , cancel := context . WithTimeout ( ctx , overallProbeTimeout )
2020-02-28 22:14:02 +00:00
defer cancel ( )
2020-03-11 22:35:12 +00:00
2023-03-06 23:35:50 +00:00
ctx = sockstats . WithSockStats ( ctx , sockstats . LabelNetcheckClient )
2023-02-03 20:07:58 +00:00
2020-05-17 16:51:38 +00:00
if dm == nil {
return nil , errors . New ( "netcheck: GetReport: DERP map is nil" )
2020-03-11 22:35:12 +00:00
}
c . mu . Lock ( )
2020-05-17 16:51:38 +00:00
if c . curState != nil {
2020-03-11 22:35:12 +00:00
c . mu . Unlock ( )
return nil , errors . New ( "invalid concurrent call to GetReport" )
}
2020-05-17 16:51:38 +00:00
rs := & reportState {
c : c ,
2020-05-28 07:37:46 +00:00
report : newReport ( ) ,
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
inFlight : map [ stun . TxID ] func ( netip . AddrPort ) { } ,
2020-05-17 16:51:38 +00:00
hairTX : stun . NewTxID ( ) , // random payload
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
gotHairSTUN : make ( chan netip . AddrPort , 1 ) ,
2020-05-17 16:51:38 +00:00
hairTimeout : make ( chan struct { } ) ,
2020-05-28 16:58:52 +00:00
stopProbeCh : make ( chan struct { } , 1 ) ,
2020-05-17 16:51:38 +00:00
}
c . curState = rs
last := c . last
2022-09-20 19:31:49 +00:00
// Even if we're doing a non-incremental update, we may want to try our
// preferred DERP region for captive portal detection. Save that, if we
// have it.
var preferredDERP int
if last != nil {
preferredDERP = last . PreferredDERP
}
2020-05-17 16:51:38 +00:00
now := c . timeNow ( )
2022-09-20 19:31:49 +00:00
doFull := false
2020-05-17 16:51:38 +00:00
if c . nextFull || now . Sub ( c . lastFull ) > 5 * time . Minute {
2022-09-20 19:31:49 +00:00
doFull = true
}
// If the last report had a captive portal and reported no UDP access,
// it's possible that we didn't get a useful netcheck due to the
// captive portal blocking us. If so, make this report a full
// (non-incremental) one.
if ! doFull && last != nil {
doFull = ! last . UDP && last . CaptivePortal . EqualBool ( true )
}
if doFull {
2020-05-17 16:51:38 +00:00
last = nil // causes makeProbePlan below to do a full (initial) plan
c . nextFull = false
c . lastFull = now
2021-11-16 16:34:25 +00:00
metricNumGetReportFull . Add ( 1 )
2020-05-17 16:51:38 +00:00
}
2022-09-20 19:31:49 +00:00
2020-05-28 16:58:52 +00:00
rs . incremental = last != nil
2020-03-11 22:35:12 +00:00
c . mu . Unlock ( )
2020-03-09 22:20:33 +00:00
defer func ( ) {
2020-03-11 22:35:12 +00:00
c . mu . Lock ( )
defer c . mu . Unlock ( )
2020-05-17 16:51:38 +00:00
c . curState = nil
2020-03-09 22:20:33 +00:00
} ( )
2021-10-27 16:37:32 +00:00
if runtime . GOOS == "js" {
if err := c . runHTTPOnlyChecks ( ctx , last , rs , dm ) ; err != nil {
return nil , err
}
return c . finishAndStoreReport ( rs , dm ) , nil
}
2020-05-28 16:58:52 +00:00
ifState , err := interfaces . GetState ( )
2020-02-25 22:05:17 +00:00
if err != nil {
2020-12-21 18:58:06 +00:00
c . logf ( "[v1] interfaces: %v" , err )
2020-05-28 16:58:52 +00:00
return nil , err
2020-02-25 22:05:17 +00:00
}
2020-03-11 04:30:04 +00:00
2022-07-18 23:56:10 +00:00
// See if IPv6 works at all, or if it's been hard disabled at the
// OS level.
2022-07-25 03:08:42 +00:00
v6udp , err := nettype . MakePacketListenerWithNetIP ( netns . Listener ( c . logf ) ) . ListenPacket ( ctx , "udp6" , "[::1]:0" )
2022-07-18 23:56:10 +00:00
if err == nil {
rs . report . OSHasIPv6 = true
v6udp . Close ( )
}
2020-03-11 04:30:04 +00:00
// Create a UDP4 socket used for sending to our discovered IPv4 address.
2022-07-25 03:08:42 +00:00
rs . pc4Hair , err = nettype . MakePacketListenerWithNetIP ( netns . Listener ( c . logf ) ) . ListenPacket ( ctx , "udp4" , ":0" )
2020-03-11 04:30:04 +00:00
if err != nil {
c . logf ( "udp4: %v" , err )
return nil , err
}
2020-05-17 16:51:38 +00:00
defer rs . pc4Hair . Close ( )
2020-02-25 22:05:17 +00:00
2021-02-20 06:15:41 +00:00
if ! c . SkipExternalNetwork && c . PortMapper != nil {
2020-10-28 15:23:12 +00:00
rs . waitPortMap . Add ( 1 )
go rs . probePortMapServices ( )
}
2020-07-06 20:51:17 +00:00
2020-07-06 15:24:22 +00:00
// 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"
2020-07-06 16:55:11 +00:00
rs . pc4Hair . WriteTo ( [ ] byte ( "tailscale netcheck; see https://github.com/tailscale/tailscale/issues/188" ) , & net . UDPAddr { IP : net . ParseIP ( documentationIP ) , Port : 12345 } )
2020-07-06 15:24:22 +00:00
2020-03-09 22:20:33 +00:00
if f := c . GetSTUNConn4 ; f != nil {
2020-05-17 16:51:38 +00:00
rs . pc4 = f ( )
2020-03-09 22:20:33 +00:00
} else {
2022-07-25 03:08:42 +00:00
u4 , err := nettype . MakePacketListenerWithNetIP ( netns . Listener ( c . logf ) ) . ListenPacket ( ctx , "udp4" , c . udpBindAddr ( ) )
2020-03-09 22:20:33 +00:00
if err != nil {
c . logf ( "udp4: %v" , err )
return nil , err
}
2020-05-17 16:51:38 +00:00
rs . pc4 = u4
go c . readPackets ( ctx , u4 )
2020-02-25 22:05:17 +00:00
}
2020-02-28 22:14:02 +00:00
2021-06-18 00:49:15 +00:00
if ifState . HaveV6 {
2020-03-09 22:20:33 +00:00
if f := c . GetSTUNConn6 ; f != nil {
2020-05-17 16:51:38 +00:00
rs . pc6 = f ( )
2020-02-25 22:05:17 +00:00
} else {
2022-07-25 03:08:42 +00:00
u6 , err := nettype . MakePacketListenerWithNetIP ( netns . Listener ( c . logf ) ) . ListenPacket ( ctx , "udp6" , c . udpBindAddr ( ) )
2020-03-09 22:20:33 +00:00
if err != nil {
c . logf ( "udp6: %v" , err )
} else {
2020-05-17 16:51:38 +00:00
rs . pc6 = u6
go c . readPackets ( ctx , u6 )
2020-03-09 22:20:33 +00:00
}
2020-02-25 22:05:17 +00:00
}
}
2020-05-28 16:58:52 +00:00
plan := makeProbePlan ( dm , ifState , last )
2020-03-11 22:35:12 +00:00
2022-09-20 19:31:49 +00:00
// If we're doing a full probe, also check for a captive portal. We
// delay by a bit to wait for UDP STUN to finish, to avoid the probe if
// it's unnecessary.
captivePortalDone := syncs . ClosedChan ( )
captivePortalStop := func ( ) { }
if ! rs . incremental {
// NOTE(andrew): we can't simply add this goroutine to the
// `NewWaitGroupChan` below, since we don't wait for that
// waitgroup to finish when exiting this function and thus get
// a data race.
ch := make ( chan struct { } )
captivePortalDone = ch
tmr := time . AfterFunc ( c . captivePortalDelay ( ) , func ( ) {
defer close ( ch )
found , err := c . checkCaptivePortal ( ctx , dm , preferredDERP )
if err != nil {
c . logf ( "[v1] checkCaptivePortal: %v" , err )
return
}
rs . report . CaptivePortal . Set ( found )
} )
captivePortalStop = func ( ) {
// Don't cancel our captive portal check if we're
// explicitly doing a verbose netcheck.
if c . Verbose {
return
}
if tmr . Stop ( ) {
// Stopped successfully; need to close the
// signal channel ourselves.
close ( ch )
return
}
// Did not stop; do nothing and it'll finish by itself
// and close the signal channel.
}
}
2020-05-17 16:51:38 +00:00
wg := syncs . NewWaitGroupChan ( )
wg . Add ( len ( plan ) )
for _ , probeSet := range plan {
setCtx , cancelSet := context . WithCancel ( ctx )
go func ( probeSet [ ] probe ) {
for _ , probe := range probeSet {
go rs . runProbe ( setCtx , dm , probe , cancelSet )
2020-03-12 21:14:48 +00:00
}
2020-05-17 16:51:38 +00:00
<- setCtx . Done ( )
wg . Decr ( )
} ( probeSet )
2020-02-28 22:14:02 +00:00
}
2020-02-25 22:05:17 +00:00
2020-07-25 02:29:27 +00:00
stunTimer := time . NewTimer ( stunProbeTimeout )
2020-05-29 20:31:08 +00:00
defer stunTimer . Stop ( )
2020-05-17 16:51:38 +00:00
select {
2020-05-29 20:31:08 +00:00
case <- stunTimer . C :
2020-05-17 16:51:38 +00:00
case <- ctx . Done ( ) :
case <- wg . DoneChan ( ) :
2022-09-20 19:31:49 +00:00
// All of our probes finished, so if we have >0 responses, we
// stop our captive portal check.
if rs . anyUDP ( ) {
captivePortalStop ( )
}
2020-05-28 16:58:52 +00:00
case <- rs . stopProbeCh :
// Saw enough regions.
c . vlogf ( "saw enough regions; not waiting for rest" )
2022-09-20 19:31:49 +00:00
// We can stop the captive portal check since we know that we
// got a bunch of STUN responses.
captivePortalStop ( )
2020-02-25 22:05:17 +00:00
}
2020-05-17 16:51:38 +00:00
rs . waitHairCheck ( ctx )
2020-07-25 02:29:27 +00:00
c . vlogf ( "hairCheck done" )
2021-02-20 06:15:41 +00:00
if ! c . SkipExternalNetwork && c . PortMapper != nil {
2020-10-28 15:23:12 +00:00
rs . waitPortMap . Wait ( )
c . vlogf ( "portMap done" )
}
2020-05-28 16:58:52 +00:00
rs . stopTimers ( )
2020-02-25 22:05:17 +00:00
2022-08-04 21:10:13 +00:00
// Try HTTPS and ICMP latency check if all STUN probes failed due to
// UDP presumably being blocked.
2020-05-29 20:31:08 +00:00
// TODO: this should be moved into the probePlan, using probeProto probeHTTPS.
if ! rs . anyUDP ( ) && ctx . Err ( ) == nil {
2020-05-11 15:23:09 +00:00
var wg sync . WaitGroup
2020-05-17 16:51:38 +00:00
var need [ ] * tailcfg . DERPRegion
for rid , reg := range dm . Regions {
if ! rs . haveRegionLatency ( rid ) && regionHasDERPNode ( reg ) {
need = append ( need , reg )
2020-05-11 15:23:09 +00:00
}
2020-05-17 16:51:38 +00:00
}
if len ( need ) > 0 {
2022-08-04 21:10:13 +00:00
// Kick off ICMP in parallel to HTTPS checks; we don't
// reuse the same WaitGroup for those probes because we
// need to close the underlying Pinger after a timeout
// or when all ICMP probes are done, regardless of
// whether the HTTPS probes have finished.
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
if err := c . measureAllICMPLatency ( ctx , rs , need ) ; err != nil {
c . logf ( "[v1] measureAllICMPLatency: %v" , err )
}
} ( )
2020-05-17 16:51:38 +00:00
wg . Add ( len ( need ) )
c . logf ( "netcheck: UDP is blocked, trying HTTPS" )
}
for _ , reg := range need {
go func ( reg * tailcfg . DERPRegion ) {
2020-05-11 15:23:09 +00:00
defer wg . Done ( )
2020-05-29 20:31:08 +00:00
if d , ip , err := c . measureHTTPSLatency ( ctx , reg ) ; err != nil {
2020-12-21 18:58:06 +00:00
c . logf ( "[v1] netcheck: measuring HTTPS latency of %v (%d): %v" , reg . RegionCode , reg . RegionID , err )
2020-05-11 15:23:09 +00:00
} else {
2020-05-17 16:51:38 +00:00
rs . mu . Lock ( )
2022-08-04 21:10:13 +00:00
if l , ok := rs . report . RegionLatency [ reg . RegionID ] ; ! ok {
mak . Set ( & rs . report . RegionLatency , reg . RegionID , d )
} else if l >= d {
rs . report . RegionLatency [ reg . RegionID ] = d
}
2020-05-29 20:31:08 +00:00
// We set these IPv4 and IPv6 but they're not really used
// and we don't necessarily set them both. If UDP is blocked
// and both IPv4 and IPv6 are available over TCP, it's basically
// random which fields end up getting set here.
// Since they're not needed, that's fine for now.
if ip . Is4 ( ) {
rs . report . IPv4 = true
}
if ip . Is6 ( ) {
rs . report . IPv6 = true
}
2020-05-17 16:51:38 +00:00
rs . mu . Unlock ( )
2020-05-11 15:23:09 +00:00
}
2020-05-17 16:51:38 +00:00
} ( reg )
2020-05-11 15:23:09 +00:00
}
wg . Wait ( )
}
2020-03-04 16:20:38 +00:00
2022-09-20 19:31:49 +00:00
// Wait for captive portal check before finishing the report.
<- captivePortalDone
2021-10-27 16:37:32 +00:00
return c . finishAndStoreReport ( rs , dm ) , nil
}
func ( c * Client ) finishAndStoreReport ( rs * reportState , dm * tailcfg . DERPMap ) * Report {
2020-05-17 16:51:38 +00:00
rs . mu . Lock ( )
report := rs . report . Clone ( )
rs . mu . Unlock ( )
2020-03-18 20:04:12 +00:00
c . addReportHistoryAndSetPreferredDERP ( report )
2020-05-17 16:51:38 +00:00
c . logConciseReport ( report , dm )
2020-03-18 20:04:12 +00:00
2021-10-27 16:37:32 +00:00
return report
}
2022-09-20 19:31:49 +00:00
var noRedirectClient = & http . Client {
// No redirects allowed
CheckRedirect : func ( req * http . Request , via [ ] * http . Request ) error {
return http . ErrUseLastResponse
} ,
// Remaining fields are the same as the default client.
Transport : http . DefaultClient . Transport ,
Jar : http . DefaultClient . Jar ,
Timeout : http . DefaultClient . Timeout ,
}
// checkCaptivePortal reports whether or not we think the system is behind a
// captive portal, detected by making a request to a URL that we know should
// return a "204 No Content" response and checking if that's what we get.
//
// The boolean return is whether we think we have a captive portal.
func ( c * Client ) checkCaptivePortal ( ctx context . Context , dm * tailcfg . DERPMap , preferredDERP int ) ( bool , error ) {
defer noRedirectClient . CloseIdleConnections ( )
// If we have a preferred DERP region with more than one node, try
// that; otherwise, pick a random one not marked as "Avoid".
if preferredDERP == 0 || dm . Regions [ preferredDERP ] == nil ||
( preferredDERP != 0 && len ( dm . Regions [ preferredDERP ] . Nodes ) == 0 ) {
rids := make ( [ ] int , 0 , len ( dm . Regions ) )
for id , reg := range dm . Regions {
if reg == nil || reg . Avoid || len ( reg . Nodes ) == 0 {
continue
}
rids = append ( rids , id )
}
2022-10-13 19:55:02 +00:00
if len ( rids ) == 0 {
return false , nil
}
2022-09-20 19:31:49 +00:00
preferredDERP = rids [ rand . Intn ( len ( rids ) ) ]
}
node := dm . Regions [ preferredDERP ] . Nodes [ 0 ]
2022-11-06 04:44:33 +00:00
if strings . HasSuffix ( node . HostName , tailcfg . DotInvalid ) {
// Don't try to connect to invalid hostnames. This occurred in tests:
// https://github.com/tailscale/tailscale/issues/6207
// TODO(bradfitz,andrew-d): how to actually handle this nicely?
return false , nil
}
2022-09-20 19:31:49 +00:00
req , err := http . NewRequestWithContext ( ctx , "GET" , "http://" + node . HostName + "/generate_204" , nil )
if err != nil {
return false , err
}
2022-10-14 16:42:09 +00:00
2022-11-08 21:41:20 +00:00
// Note: the set of valid characters in a challenge and the total
// length is limited; see isChallengeChar in cmd/derper for more
// details.
chal := "ts_" + node . HostName
2022-10-14 16:42:09 +00:00
req . Header . Set ( "X-Tailscale-Challenge" , chal )
2022-09-20 19:31:49 +00:00
r , err := noRedirectClient . Do ( req )
if err != nil {
return false , err
}
2022-10-14 16:42:09 +00:00
defer r . Body . Close ( )
expectedResponse := "response " + chal
validResponse := r . Header . Get ( "X-Tailscale-Response" ) == expectedResponse
2022-09-20 19:31:49 +00:00
2022-10-14 16:42:09 +00:00
c . logf ( "[v2] checkCaptivePortal url=%q status_code=%d valid_response=%v" , req . URL . String ( ) , r . StatusCode , validResponse )
return r . StatusCode != 204 || ! validResponse , nil
2022-09-20 19:31:49 +00:00
}
2021-10-27 16:37:32 +00:00
// runHTTPOnlyChecks is the netcheck done by environments that can
// only do HTTP requests, such as ws/wasm.
func ( c * Client ) runHTTPOnlyChecks ( ctx context . Context , last * Report , rs * reportState , dm * tailcfg . DERPMap ) error {
var regions [ ] * tailcfg . DERPRegion
if rs . incremental && last != nil {
for rid := range last . RegionLatency {
if dr , ok := dm . Regions [ rid ] ; ok {
regions = append ( regions , dr )
}
}
}
if len ( regions ) == 0 {
for _ , dr := range dm . Regions {
regions = append ( regions , dr )
}
}
c . logf ( "running HTTP-only netcheck against %v regions" , len ( regions ) )
var wg sync . WaitGroup
for _ , rg := range regions {
if len ( rg . Nodes ) == 0 {
continue
}
wg . Add ( 1 )
rg := rg
go func ( ) {
defer wg . Done ( )
node := rg . Nodes [ 0 ]
req , _ := http . NewRequestWithContext ( ctx , "HEAD" , "https://" + node . HostName + "/derp/probe" , nil )
// One warm-up one to get HTTP connection set
// up and get a connection from the browser's
// pool.
2022-04-19 18:46:30 +00:00
if r , err := http . DefaultClient . Do ( req ) ; err != nil || r . StatusCode > 299 {
if err != nil {
c . logf ( "probing %s: %v" , node . HostName , err )
} else {
c . logf ( "probing %s: unexpected status %s" , node . HostName , r . Status )
}
2021-10-27 16:37:32 +00:00
return
}
t0 := c . timeNow ( )
2022-04-19 18:46:30 +00:00
if r , err := http . DefaultClient . Do ( req ) ; err != nil || r . StatusCode > 299 {
if err != nil {
c . logf ( "probing %s: %v" , node . HostName , err )
} else {
c . logf ( "probing %s: unexpected status %s" , node . HostName , r . Status )
}
2021-10-27 16:37:32 +00:00
return
}
d := c . timeNow ( ) . Sub ( t0 )
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
rs . addNodeLatency ( node , netip . AddrPort { } , d )
2021-10-27 16:37:32 +00:00
} ( )
}
wg . Wait ( )
return nil
2020-03-18 20:04:12 +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
func ( c * Client ) measureHTTPSLatency ( ctx context . Context , reg * tailcfg . DERPRegion ) ( time . Duration , netip . Addr , error ) {
2021-11-16 16:34:25 +00:00
metricHTTPSend . Add ( 1 )
2020-05-11 15:23:09 +00:00
var result httpstat . Result
2020-07-25 02:29:27 +00:00
ctx , cancel := context . WithTimeout ( httpstat . WithHTTPStat ( ctx , & result ) , overallProbeTimeout )
2020-05-11 15:23:09 +00:00
defer cancel ( )
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
var ip netip . Addr
2020-05-29 20:31:08 +00:00
dc := derphttp . NewNetcheckClient ( c . logf )
2022-09-21 04:27:47 +00:00
defer dc . Close ( )
2022-04-19 18:46:30 +00:00
tlsConn , tcpConn , node , err := dc . DialRegionTLS ( ctx , reg )
2020-05-29 20:31:08 +00:00
if err != nil {
return 0 , ip , err
}
2020-05-30 05:33:08 +00:00
defer tcpConn . Close ( )
2020-05-29 20:31:08 +00:00
2020-05-30 05:33:08 +00:00
if ta , ok := tlsConn . RemoteAddr ( ) . ( * net . TCPAddr ) ; ok {
2022-08-02 20:38:11 +00:00
ip , _ = netip . AddrFromSlice ( ta . IP )
ip = ip . Unmap ( )
2020-05-29 20:31:08 +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
if ip == ( netip . Addr { } ) {
2020-05-30 05:33:08 +00:00
return 0 , ip , fmt . Errorf ( "no unexpected RemoteAddr %#v" , tlsConn . RemoteAddr ( ) )
2020-05-29 20:31:08 +00:00
}
2020-05-30 05:33:08 +00:00
connc := make ( chan * tls . Conn , 1 )
connc <- tlsConn
2020-05-29 20:31:08 +00:00
tr := & http . Transport {
2020-05-30 05:33:08 +00:00
DialContext : func ( ctx context . Context , network , addr string ) ( net . Conn , error ) {
return nil , errors . New ( "unexpected DialContext dial" )
} ,
2020-05-29 20:31:08 +00:00
DialTLSContext : func ( ctx context . Context , network , addr string ) ( net . Conn , error ) {
select {
case nc := <- connc :
return nc , nil
default :
return nil , errors . New ( "only one conn expected" )
}
} ,
}
hc := & http . Client { Transport : tr }
2022-04-29 19:57:52 +00:00
req , err := http . NewRequestWithContext ( ctx , "GET" , "https://" + node . HostName + "/derp/latency-check" , nil )
2020-05-11 15:23:09 +00:00
if err != nil {
2020-05-29 20:31:08 +00:00
return 0 , ip , err
2020-05-11 15:23:09 +00:00
}
2020-05-29 20:31:08 +00:00
resp , err := hc . Do ( req )
2020-05-11 15:23:09 +00:00
if err != nil {
2020-05-29 20:31:08 +00:00
return 0 , ip , err
2020-05-11 15:23:09 +00:00
}
defer resp . Body . Close ( )
2022-04-19 18:46:30 +00:00
// DERPs should give us a nominal status code, so anything else is probably
// an access denied by a MITM proxy (or at the very least a signal not to
// trust this latency check).
if resp . StatusCode > 299 {
return 0 , ip , fmt . Errorf ( "unexpected status code: %d (%s)" , resp . StatusCode , resp . Status )
}
2022-09-15 12:06:59 +00:00
_ , err = io . Copy ( io . Discard , io . LimitReader ( resp . Body , 8 << 10 ) )
2020-05-11 15:23:09 +00:00
if err != nil {
2020-05-29 20:31:08 +00:00
return 0 , ip , err
2020-05-11 15:23:09 +00:00
}
result . End ( c . timeNow ( ) )
// TODO: decide best timing heuristic here.
// Maybe the server should return the tcpinfo_rtt?
2020-05-29 20:31:08 +00:00
return result . ServerProcessing , ip , nil
2020-05-11 15:23:09 +00:00
}
2022-08-04 21:10:13 +00:00
func ( c * Client ) measureAllICMPLatency ( ctx context . Context , rs * reportState , need [ ] * tailcfg . DERPRegion ) error {
if len ( need ) == 0 {
return nil
}
ctx , done := context . WithTimeout ( ctx , icmpProbeTimeout )
defer done ( )
p , err := ping . New ( ctx , c . logf )
if err != nil {
return err
}
defer p . Close ( )
c . logf ( "UDP is blocked, trying ICMP" )
var wg sync . WaitGroup
wg . Add ( len ( need ) )
for _ , reg := range need {
go func ( reg * tailcfg . DERPRegion ) {
defer wg . Done ( )
if d , err := c . measureICMPLatency ( ctx , reg , p ) ; err != nil {
c . logf ( "[v1] measuring ICMP latency of %v (%d): %v" , reg . RegionCode , reg . RegionID , err )
} else {
c . logf ( "[v1] ICMP latency of %v (%d): %v" , reg . RegionCode , reg . RegionID , d )
rs . mu . Lock ( )
if l , ok := rs . report . RegionLatency [ reg . RegionID ] ; ! ok {
mak . Set ( & rs . report . RegionLatency , reg . RegionID , d )
} else if l >= d {
rs . report . RegionLatency [ reg . RegionID ] = d
}
// We only send IPv4 ICMP right now
rs . report . IPv4 = true
rs . report . ICMPv4 = true
rs . mu . Unlock ( )
}
} ( reg )
}
wg . Wait ( )
return nil
}
func ( c * Client ) measureICMPLatency ( ctx context . Context , reg * tailcfg . DERPRegion , p * ping . Pinger ) ( time . Duration , error ) {
if len ( reg . Nodes ) == 0 {
return 0 , fmt . Errorf ( "no nodes for region %d (%v)" , reg . RegionID , reg . RegionCode )
}
// Try pinging the first node in the region
node := reg . Nodes [ 0 ]
// Get the IPAddr by asking for the UDP address that we would use for
// STUN and then using that IP.
//
// TODO(andrew-d): this is a bit ugly
nodeAddr := c . nodeAddr ( ctx , node , probeIPv4 )
if ! nodeAddr . IsValid ( ) {
return 0 , fmt . Errorf ( "no address for node %v" , node . Name )
}
addr := & net . IPAddr {
IP : net . IP ( nodeAddr . Addr ( ) . AsSlice ( ) ) ,
Zone : nodeAddr . Addr ( ) . Zone ( ) ,
}
// Use the unique node.Name field as the packet data to reduce the
// likelihood that we get a mismatched echo response.
return p . Send ( ctx , addr , [ ] byte ( node . Name ) )
}
2020-05-17 16:51:38 +00:00
func ( c * Client ) logConciseReport ( r * Report , dm * tailcfg . DERPMap ) {
2020-12-21 18:58:06 +00:00
c . logf ( "[v1] report: %v" , logger . ArgWriter ( func ( w * bufio . Writer ) {
2020-06-12 04:37:15 +00:00
fmt . Fprintf ( w , "udp=%v" , r . UDP )
if ! r . IPv4 {
fmt . Fprintf ( w , " v4=%v" , r . IPv4 )
}
2022-08-04 21:10:13 +00:00
if ! r . UDP {
fmt . Fprintf ( w , " icmpv4=%v" , r . ICMPv4 )
}
2020-06-12 04:37:15 +00:00
fmt . Fprintf ( w , " v6=%v" , r . IPv6 )
2022-10-21 19:13:49 +00:00
if ! r . IPv6 {
fmt . Fprintf ( w , " v6os=%v" , r . OSHasIPv6 )
}
2020-06-12 04:37:15 +00:00
fmt . Fprintf ( w , " mapvarydest=%v" , r . MappingVariesByDestIP )
fmt . Fprintf ( w , " hair=%v" , r . HairPinning )
2020-07-06 20:51:17 +00:00
if r . AnyPortMappingChecked ( ) {
fmt . Fprintf ( w , " portmap=%v%v%v" , conciseOptBool ( r . UPnP , "U" ) , conciseOptBool ( r . PMP , "M" ) , conciseOptBool ( r . PCP , "C" ) )
} else {
fmt . Fprintf ( w , " portmap=?" )
}
2020-06-12 04:37:15 +00:00
if r . GlobalV4 != "" {
fmt . Fprintf ( w , " v4a=%v" , r . GlobalV4 )
}
if r . GlobalV6 != "" {
fmt . Fprintf ( w , " v6a=%v" , r . GlobalV6 )
}
2022-09-20 19:31:49 +00:00
if r . CaptivePortal != "" {
fmt . Fprintf ( w , " captiveportal=%v" , r . CaptivePortal )
}
2020-06-12 04:37:15 +00:00
fmt . Fprintf ( w , " derp=%v" , r . PreferredDERP )
if r . PreferredDERP != 0 {
fmt . Fprintf ( w , " derpdist=" )
2020-04-09 20:13:05 +00:00
needComma := false
2020-06-12 04:37:15 +00:00
for _ , rid := range dm . RegionIDs ( ) {
if d := r . RegionV4Latency [ rid ] ; d != 0 {
if needComma {
w . WriteByte ( ',' )
}
fmt . Fprintf ( w , "%dv4:%v" , rid , d . Round ( time . Millisecond ) )
needComma = true
}
if d := r . RegionV6Latency [ rid ] ; d != 0 {
if needComma {
w . WriteByte ( ',' )
}
fmt . Fprintf ( w , "%dv6:%v" , rid , d . Round ( time . Millisecond ) )
needComma = true
2020-04-09 20:13:05 +00:00
}
}
}
2020-06-12 04:37:15 +00:00
} ) )
2020-04-09 20:13:05 +00:00
}
2020-03-18 20:04:12 +00:00
func ( c * Client ) timeNow ( ) time . Time {
if c . TimeNow != nil {
return c . TimeNow ( )
}
return time . Now ( )
}
// addReportHistoryAndSetPreferredDERP adds r to the set of recent Reports
// and mutates r.PreferredDERP to contain the best recent one.
func ( c * Client ) addReportHistoryAndSetPreferredDERP ( r * Report ) {
c . mu . Lock ( )
defer c . mu . Unlock ( )
2021-01-11 19:38:49 +00:00
var prevDERP int
if c . last != nil {
prevDERP = c . last . PreferredDERP
}
2020-03-18 20:04:12 +00:00
if c . prev == nil {
c . prev = map [ time . Time ] * Report { }
}
now := c . timeNow ( )
c . prev [ now ] = r
2020-05-05 06:22:19 +00:00
c . last = r
2020-03-18 20:04:12 +00:00
const maxAge = 5 * time . Minute
2020-05-17 16:51:38 +00:00
// region ID => its best recent latency in last maxAge
bestRecent := map [ int ] time . Duration { }
2020-03-18 20:04:12 +00:00
for t , pr := range c . prev {
if now . Sub ( t ) > maxAge {
delete ( c . prev , t )
continue
}
2021-01-11 19:38:49 +00:00
for regionID , d := range pr . RegionLatency {
if bd , ok := bestRecent [ regionID ] ; ! ok || d < bd {
bestRecent [ regionID ] = d
2020-03-18 20:04:12 +00:00
}
}
}
// Then, pick which currently-alive DERP server from the
// current report has the best latency over the past maxAge.
var bestAny time . Duration
2021-01-11 19:38:49 +00:00
var oldRegionCurLatency time . Duration
for regionID , d := range r . RegionLatency {
if regionID == prevDERP {
oldRegionCurLatency = d
}
best := bestRecent [ regionID ]
2020-03-18 20:04:12 +00:00
if r . PreferredDERP == 0 || best < bestAny {
bestAny = best
2021-01-11 19:38:49 +00:00
r . PreferredDERP = regionID
2020-05-17 16:51:38 +00:00
}
}
2021-01-11 19:38:49 +00:00
// If we're changing our preferred DERP but the old one's still
// accessible and the new one's not much better, just stick with
// where we are.
if prevDERP != 0 &&
r . PreferredDERP != prevDERP &&
oldRegionCurLatency != 0 &&
bestAny > oldRegionCurLatency / 3 * 2 {
r . PreferredDERP = prevDERP
}
2020-05-17 16:51:38 +00:00
}
2020-05-28 07:37:46 +00:00
func updateLatency ( m map [ int ] time . Duration , regionID int , d time . Duration ) {
2020-05-17 16:51:38 +00:00
if prev , ok := m [ regionID ] ; ! ok || d < prev {
m [ regionID ] = d
}
}
func namedNode ( dm * tailcfg . DERPMap , nodeName string ) * tailcfg . DERPNode {
if dm == nil {
return nil
}
for _ , r := range dm . Regions {
for _ , n := range r . Nodes {
if n . Name == nodeName {
return n
}
}
}
return nil
}
func ( rs * reportState ) runProbe ( ctx context . Context , dm * tailcfg . DERPMap , probe probe , cancelSet func ( ) ) {
c := rs . c
node := namedNode ( dm , probe . node )
if node == nil {
c . logf ( "netcheck.runProbe: named node %q not found" , probe . node )
return
}
if probe . delay > 0 {
delayTimer := time . NewTimer ( probe . delay )
select {
case <- delayTimer . C :
case <- ctx . Done ( ) :
delayTimer . Stop ( )
return
}
}
if ! rs . probeWouldHelp ( probe , node ) {
cancelSet ( )
return
}
addr := c . nodeAddr ( ctx , node , probe . proto )
2022-07-25 03:08:42 +00:00
if ! addr . IsValid ( ) {
2020-05-17 16:51:38 +00:00
return
}
txID := stun . NewTxID ( )
req := stun . Request ( txID )
sent := time . Now ( ) // after DNS lookup above
rs . mu . Lock ( )
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
rs . inFlight [ txID ] = func ( ipp netip . AddrPort ) {
2020-05-17 16:51:38 +00:00
rs . addNodeLatency ( node , ipp , time . Since ( sent ) )
cancelSet ( ) // abort other nodes in this set
}
rs . mu . Unlock ( )
switch probe . proto {
case probeIPv4 :
2021-11-16 16:34:25 +00:00
metricSTUNSend4 . Add ( 1 )
2022-07-25 03:08:42 +00:00
n , err := rs . pc4 . WriteToUDPAddrPort ( req , addr )
2021-12-30 19:11:50 +00:00
if n == len ( req ) && err == nil || neterror . TreatAsLostUDP ( err ) {
2021-10-07 00:43:37 +00:00
rs . mu . Lock ( )
rs . report . IPv4CanSend = true
rs . mu . Unlock ( )
}
2020-05-17 16:51:38 +00:00
case probeIPv6 :
2021-11-16 16:34:25 +00:00
metricSTUNSend6 . Add ( 1 )
2022-07-25 03:08:42 +00:00
n , err := rs . pc6 . WriteToUDPAddrPort ( req , addr )
2021-12-30 19:11:50 +00:00
if n == len ( req ) && err == nil || neterror . TreatAsLostUDP ( err ) {
2021-10-07 00:43:37 +00:00
rs . mu . Lock ( )
rs . report . IPv6CanSend = true
rs . mu . Unlock ( )
}
2020-05-17 16:51:38 +00:00
default :
panic ( "bad probe proto " + fmt . Sprint ( probe . proto ) )
}
2020-05-28 16:58:52 +00:00
c . vlogf ( "sent to %v" , addr )
2020-05-17 16:51:38 +00:00
}
// proto is 4 or 6
// If it returns nil, the node is skipped.
2022-07-25 03:08:42 +00:00
func ( c * Client ) nodeAddr ( ctx context . Context , n * tailcfg . DERPNode , proto probeProto ) ( ap netip . AddrPort ) {
2020-05-17 16:51:38 +00:00
port := n . STUNPort
if port == 0 {
port = 3478
}
if port < 0 || port > 1 << 16 - 1 {
2022-07-25 03:08:42 +00:00
return
2020-05-17 16:51:38 +00:00
}
2020-07-10 21:26:04 +00:00
if n . STUNTestIP != "" {
2022-07-26 03:55:44 +00:00
ip , err := netip . ParseAddr ( n . STUNTestIP )
2020-07-10 21:26:04 +00:00
if err != nil {
2022-07-25 03:08:42 +00:00
return
2020-07-10 21:26:04 +00:00
}
if proto == probeIPv4 && ip . Is6 ( ) {
2022-07-25 03:08:42 +00:00
return
2020-07-10 21:26:04 +00:00
}
if proto == probeIPv6 && ip . Is4 ( ) {
2022-07-25 03:08:42 +00:00
return
2020-07-10 21:26:04 +00:00
}
2022-07-25 03:08:42 +00:00
return netip . AddrPortFrom ( ip , uint16 ( port ) )
2020-07-10 21:26:04 +00:00
}
2020-05-17 16:51:38 +00:00
switch proto {
case probeIPv4 :
if n . IPv4 != "" {
2022-07-26 03:55:44 +00:00
ip , _ := netip . ParseAddr ( n . IPv4 )
2020-05-17 16:51:38 +00:00
if ! ip . Is4 ( ) {
2022-07-25 03:08:42 +00:00
return
2020-05-17 16:51:38 +00:00
}
2022-07-25 03:08:42 +00:00
return netip . AddrPortFrom ( ip , uint16 ( port ) )
2020-05-17 16:51:38 +00:00
}
case probeIPv6 :
if n . IPv6 != "" {
2022-07-26 03:55:44 +00:00
ip , _ := netip . ParseAddr ( n . IPv6 )
2020-05-17 16:51:38 +00:00
if ! ip . Is6 ( ) {
2022-07-25 03:08:42 +00:00
return
2020-05-17 16:51:38 +00:00
}
2022-07-25 03:08:42 +00:00
return netip . AddrPortFrom ( ip , uint16 ( port ) )
2020-05-17 16:51:38 +00:00
}
default :
2022-07-25 03:08:42 +00:00
return
2020-05-17 16:51:38 +00:00
}
// TODO(bradfitz): add singleflight+dnscache here.
addrs , _ := net . DefaultResolver . LookupIPAddr ( ctx , n . HostName )
for _ , a := range addrs {
if ( a . IP . To4 ( ) != nil ) == ( proto == probeIPv4 ) {
2022-08-02 20:38:11 +00:00
na , _ := netip . AddrFromSlice ( a . IP . To4 ( ) )
return netip . AddrPortFrom ( na . Unmap ( ) , uint16 ( port ) )
2020-03-18 20:04:12 +00:00
}
}
2022-07-25 03:08:42 +00:00
return
2020-02-25 22:05:17 +00:00
}
2020-05-05 06:22:19 +00:00
2020-05-17 16:51:38 +00:00
func regionHasDERPNode ( r * tailcfg . DERPRegion ) bool {
for _ , n := range r . Nodes {
if ! n . STUNOnly {
2020-05-05 06:22:19 +00:00
return true
}
}
return false
}
2020-05-28 16:58:52 +00:00
func maxDurationValue ( m map [ int ] time . Duration ) ( max time . Duration ) {
for _ , v := range m {
if v > max {
max = v
}
}
return max
}
2020-07-06 20:51:17 +00:00
func conciseOptBool ( b opt . Bool , trueVal string ) string {
if b == "" {
return "_"
}
v , ok := b . Get ( )
if ! ok {
return "x"
}
if v {
return trueVal
}
return ""
}
2021-11-16 16:34:25 +00:00
var (
metricNumGetReport = clientmetric . NewCounter ( "netcheck_report" )
metricNumGetReportFull = clientmetric . NewCounter ( "netcheck_report_full" )
metricNumGetReportError = clientmetric . NewCounter ( "netcheck_report_error" )
metricSTUNSend4 = clientmetric . NewCounter ( "netcheck_stun_send_ipv4" )
metricSTUNSend6 = clientmetric . NewCounter ( "netcheck_stun_send_ipv6" )
metricSTUNRecv4 = clientmetric . NewCounter ( "netcheck_stun_recv_ipv4" )
metricSTUNRecv6 = clientmetric . NewCounter ( "netcheck_stun_recv_ipv6" )
metricHTTPSend = clientmetric . NewCounter ( "netcheck_https_measure" )
)