This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+157 -79
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package netcheck checks the network conditions from the current host.
@@ -39,12 +39,14 @@ import (
"tailscale.com/net/stun"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/types/bools"
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
"tailscale.com/types/opt"
"tailscale.com/types/views"
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
"tailscale.com/util/testenv"
)
// Debugging and experimentation tweakables.
@@ -110,10 +112,10 @@ type Report struct {
// 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
PreferredDERP tailcfg.DERPRegionID // or 0 for unknown
RegionLatency RegionLatency // keyed by DERP Region ID
RegionV4Latency RegionLatency // keyed by DERP Region ID
RegionV6Latency RegionLatency // keyed by DERP Region ID
GlobalV4Counters map[netip.AddrPort]int // number of times the endpoint was observed
GlobalV6Counters map[netip.AddrPort]int // number of times the endpoint was observed
@@ -183,6 +185,34 @@ func (r *Report) Clone() *Report {
return &r2
}
// RegionLatency is a map of DERP region IDs associated with their measured latencies.
type RegionLatency map[tailcfg.DERPRegionID]time.Duration
// Compare compares the latency of the regions i and j. It returns
//
// -1 if region i is contained in lat, but region j is missing
// +1 if region j is contained in lat, but region i is missing
//
// Otherwise, it returns
//
// -1 if the latency of region i is less than the latency of region j
// +1 if the latency of region i is greater than the latency of region j
//
// If both latency values are equal, then it breaks ties by region ID and returns
//
// -1 if i is less than j
// 0 if i equals j
// +1 if i is greater than j
func (lat RegionLatency) Compare(i, j tailcfg.DERPRegionID) int {
iLat, iOK := lat[i]
jLat, jOK := lat[j]
return cmp.Or(
bools.Compare(!iOK, !jOK), // defined things sort first
cmp.Compare(iLat, jLat), // sort by latency
cmp.Compare(i, j), // break ties by region ID
)
}
// Client generates Reports describing the result of both passive and active
// network configuration probing. It provides two different modes of report, a
// full report (see MakeNextReportFull) and a more lightweight incremental
@@ -229,11 +259,10 @@ type Client struct {
// if non-zero, force this DERP region to be preferred in all reports where
// the DERP is found to be reachable.
ForcePreferredDERP int
ForcePreferredDERP tailcfg.DERPRegionID
// For tests
testEnoughRegions int
testCaptivePortalDelay time.Duration
testEnoughRegions int
mu syncs.Mutex // guards following
nextFull bool // do a full region scan, even if last != nil
@@ -256,14 +285,6 @@ func (c *Client) enoughRegions() int {
return 3
}
func (c *Client) captivePortalDelay() time.Duration {
if c.testCaptivePortalDelay > 0 {
return c.testCaptivePortalDelay
}
// Chosen semi-arbitrarily
return 200 * time.Millisecond
}
func (c *Client) logf(format string, a ...any) {
if c.Logf != nil {
c.Logf(format, a...)
@@ -387,7 +408,7 @@ 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, preferredDERP int) (prev []*tailcfg.DERPRegion) {
func sortRegions(dm *tailcfg.DERPMap, last *Report, preferredDERP tailcfg.DERPRegionID) (prev []*tailcfg.DERPRegion) {
prev = make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
for _, reg := range dm.Regions {
if reg.NoMeasureNoHome {
@@ -431,7 +452,7 @@ const numIncrementalRegions = 3
// TODO(raggi): change from "preferred DERP" from a historical report to "home
// DERP" as in what DERP is the current home connection, this would further
// reduce flap events.
func makeProbePlan(dm *tailcfg.DERPMap, ifState *netmon.State, last *Report, preferredDERP int) (plan probePlan) {
func makeProbePlan(dm *tailcfg.DERPMap, ifState *netmon.State, last *Report, preferredDERP tailcfg.DERPRegionID) (plan probePlan) {
if last == nil || len(last.RegionLatency) == 0 {
return makeProbePlanInitial(dm, ifState)
}
@@ -545,7 +566,7 @@ func makeProbePlanInitial(dm *tailcfg.DERPMap, ifState *netmon.State) (plan prob
var p4 []probe
var p6 []probe
for try := 0; try < 3; try++ {
for try := range 3 {
n := reg.Nodes[try%len(reg.Nodes)]
delay := time.Duration(try) * defaultInitialRetransmitTime
if n.IPv4 != "none" && ((ifState.HaveV4 && nodeMight4(n)) || n.IsTestNode()) {
@@ -610,7 +631,7 @@ func (rs *reportState) anyUDP() bool {
return rs.report.UDP
}
func (rs *reportState) haveRegionLatency(regionID int) bool {
func (rs *reportState) haveRegionLatency(regionID tailcfg.DERPRegionID) bool {
rs.mu.Lock()
defer rs.mu.Unlock()
_, ok := rs.report.RegionLatency[regionID]
@@ -686,6 +707,9 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netip.AddrPort
switch {
case ipp.Addr().Is6():
// Receiving a STUN response proves that the corresponding probe was
// sent, even if the response races SendPacket returning in runProbe.
ret.IPv6CanSend = true
updateLatency(ret.RegionV6Latency, node.RegionID, d)
ret.IPv6 = true
ret.GlobalV6 = ipp
@@ -693,6 +717,7 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netip.AddrPort
// TODO: track MappingVariesByDestIP for IPv6
// too? Would be sad if so, but who knows.
case ipp.Addr().Is4():
ret.IPv4CanSend = true
updateLatency(ret.RegionV4Latency, node.RegionID, d)
ret.IPv4 = true
mak.Set(&ret.GlobalV4Counters, ipp, ret.GlobalV4Counters[ipp]+1)
@@ -747,9 +772,9 @@ func (rs *reportState) probePortMapServices() {
func newReport() *Report {
return &Report{
RegionLatency: make(map[int]time.Duration),
RegionV4Latency: make(map[int]time.Duration),
RegionV6Latency: make(map[int]time.Duration),
RegionLatency: make(map[tailcfg.DERPRegionID]time.Duration),
RegionV4Latency: make(map[tailcfg.DERPRegionID]time.Duration),
RegionV6Latency: make(map[tailcfg.DERPRegionID]time.Duration),
}
}
@@ -764,7 +789,7 @@ type GetReportOpts struct {
//
// If no communication with that region has occurred, or it occurred
// too far in the past, this function should return the zero time.
GetLastDERPActivity func(int) time.Time
GetLastDERPActivity func(tailcfg.DERPRegionID) time.Time
// OnlyTCP443 constrains netcheck reporting to measurements over TCP port
// 443.
OnlyTCP443 bool
@@ -774,20 +799,28 @@ type GetReportOpts struct {
// getLastDERPActivity calls o.GetLastDERPActivity if both o and
// o.GetLastDERPActivity are non-nil; otherwise it returns the zero time.
func (o *GetReportOpts) getLastDERPActivity(region int) time.Time {
func (o *GetReportOpts) getLastDERPActivity(region tailcfg.DERPRegionID) time.Time {
if o == nil || o.GetLastDERPActivity == nil {
return time.Time{}
}
return o.GetLastDERPActivity(region)
}
func (c *Client) SetForcePreferredDERP(region int) {
func (c *Client) SetForcePreferredDERP(region tailcfg.DERPRegionID) {
c.mu.Lock()
defer c.mu.Unlock()
c.ForcePreferredDERP = region
}
var hookStartCaptivePortalDetection feature.Hook[func(ctx context.Context, rs *reportState, dm *tailcfg.DERPMap, preferredDERP int) (<-chan struct{}, func())]
// HookStartCaptivePortalDetection, if set, is called by GetReport to
// asynchronously start captive portal detection during a full (non-incremental)
// netcheck. It is set at init time by the optional
// tailscale.com/feature/captiveportal/netcheckhook package.
//
// The returned done channel is closed when detection has finished (and
// setCaptivePortal has been called with the result, if it ran); the returned
// stop function cancels a detection that has not yet started.
var HookStartCaptivePortalDetection feature.Hook[func(ctx context.Context, c *Client, dm *tailcfg.DERPMap, preferredDERP tailcfg.DERPRegionID, setCaptivePortal func(bool)) (done <-chan struct{}, stop func())]
// GetReport gets a report. The 'opts' argument is optional and can be nil.
// Callers are discouraged from passing a ctx with an arbitrary deadline as this
@@ -846,13 +879,13 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
// in captive portal detection and DERP flapping suppression. Ideally this would
// be the current active home DERP rather than the last report preferred DERP,
// but only the latter is presently available.
var preferredDERP int
var preferredDERP tailcfg.DERPRegionID
if last != nil {
preferredDERP = last.PreferredDERP
}
doFull := false
if c.nextFull || now.Sub(c.lastFull) > 5*time.Minute {
if c.nextFull || now.Sub(c.lastFull) > fullReportInterval {
doFull = true
}
// If the last report had a captive portal and reported no UDP access,
@@ -903,8 +936,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
go rs.probePortMapServices()
}
// Without NAT traversal, we never send STUN: we behave as if UDP were
// blocked and measure DERP latency over HTTPS (and ICMP) instead.
var plan probePlan
if opts == nil || !opts.OnlyTCP443 {
if buildfeatures.HasNATTraversal && (opts == nil || !opts.OnlyTCP443) {
plan = makeProbePlan(dm, ifState, last, preferredDERP)
}
@@ -914,21 +949,26 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
captivePortalDone := syncs.ClosedChan()
captivePortalStop := func() {}
if buildfeatures.HasCaptivePortal && !rs.incremental && !onlySTUN {
start := hookStartCaptivePortalDetection.Get()
captivePortalDone, captivePortalStop = start(ctx, rs, dm, preferredDERP)
if start, ok := HookStartCaptivePortalDetection.GetOk(); ok {
captivePortalDone, captivePortalStop = start(ctx, c, dm, preferredDERP, func(found bool) {
rs.report.CaptivePortal.Set(found)
})
}
}
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)
}
<-setCtx.Done()
wg.Decr()
}(probeSet)
if buildfeatures.HasNATTraversal {
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)
}
<-setCtx.Done()
wg.Decr()
}(probeSet)
}
}
stunTimer := time.NewTimer(stunProbeTimeout)
@@ -964,24 +1004,25 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
var wg sync.WaitGroup
var need []*tailcfg.DERPRegion
for rid, reg := range dm.Regions {
if !rs.haveRegionLatency(rid) && regionHasDERPNode(reg) && !reg.Avoid && !reg.NoMeasureNoHome {
if !rs.haveRegionLatency(tailcfg.DERPRegionID(rid)) && regionHasDERPNode(reg) && !reg.Avoid && !reg.NoMeasureNoHome {
need = append(need, reg)
}
}
if len(need) > 0 {
if opts == nil || !opts.OnlyTCP443 {
// Without UDP to peers, DERP is our only transport and its
// latency is measured over HTTPS alone; ICMP is not worth
// linking in just for that.
if buildfeatures.HasUDPTransport && (opts == nil || !opts.OnlyTCP443) {
// 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()
wg.Go(func() {
if err := c.measureAllICMPLatency(ctx, rs, need); err != nil {
c.logf("[v1] measureAllICMPLatency: %v", err)
}
}()
})
}
wg.Add(len(need))
c.logf("netcheck: UDP is blocked, trying HTTPS")
@@ -1027,7 +1068,7 @@ func (c *Client) finishAndStoreReport(rs *reportState, dm *tailcfg.DERPMap) *Rep
report := rs.report.Clone()
rs.mu.Unlock()
c.addReportHistoryAndSetPreferredDERP(rs, report, dm.View())
c.addReportHistoryAndSetPreferredDERP(rs, report, dm.View(), c.timeNow())
c.logConciseReport(report, dm)
return report
@@ -1072,9 +1113,7 @@ func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *report
if len(rg.Nodes) == 0 {
continue
}
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
node := rg.Nodes[0]
req, _ := http.NewRequestWithContext(ctx, "HEAD", "https://"+node.HostName+"/derp/probe", nil)
// One warm-up one to get HTTP connection set
@@ -1099,7 +1138,7 @@ func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *report
}
d := c.timeNow().Sub(t0)
rs.addNodeLatency(node, netip.AddrPort{}, d)
}()
})
}
wg.Wait()
return nil
@@ -1332,6 +1371,12 @@ func (c *Client) timeNow() time.Time {
}
const (
// fullReportInterval is the maximum time between full netcheck reports.
// Once this long has elapsed since the last full report, the next GetReport
// re-probes every DERP region rather than only the home and fastest regions
// (see GetReport). It also informs retention window for report history
// (c.prev).
fullReportInterval = 5 * time.Minute
// preferredDERPAbsoluteDiff specifies the minimum absolute difference
// in latencies between two DERP regions that would cause a node to
// switch its PreferredDERP ("home DERP"). This ensures that if a node
@@ -1356,44 +1401,45 @@ const (
PreferredDERPKeepAliveTimeout = 2 * derp.KeepAlive
)
// addReportHistoryAndSetPreferredDERP adds r to the set of recent Reports
// and mutates r.PreferredDERP to contain the best recent one.
func (c *Client) addReportHistoryAndSetPreferredDERP(rs *reportState, r *Report, dm tailcfg.DERPMapView) {
c.mu.Lock()
defer c.mu.Unlock()
var prevDERP int
if c.last != nil {
prevDERP = c.last.PreferredDERP
}
// addReportAndPruneExpired adds r to the set of recent Reports, and drops
// reports that are outside of the retention window.
func (c *Client) addReportAndPruneExpired(now time.Time, r *Report) {
if c.prev == nil {
c.prev = map[time.Time]*Report{}
}
now := c.timeNow()
r.Now = now.UTC()
c.prev[now] = r
c.last = r
const maxAge = 5 * time.Minute
// region ID => its best recent latency in last maxAge
bestRecent := map[int]time.Duration{}
for t, pr := range c.prev {
// maxAge is the retention window for report history, based on fullReportInterval
// to make sure that at least one full report is always retained.
const maxAge = fullReportInterval + ReportTimeout
for t := range c.prev {
if now.Sub(t) > maxAge {
delete(c.prev, t)
continue
}
for regionID, d := range pr.RegionLatency {
if bd, ok := bestRecent[regionID]; !ok || d < bd {
bestRecent[regionID] = d
}
}
}
}
// addReportHistoryAndSetPreferredDERP adds r to the set of recent Reports
// and mutates r.PreferredDERP to contain the best recent one.
func (c *Client) addReportHistoryAndSetPreferredDERP(rs *reportState, r *Report, dm tailcfg.DERPMapView, now time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
var prevDERP tailcfg.DERPRegionID
if c.last != nil {
prevDERP = c.last.PreferredDERP
}
// Add report to history, enforce retention window, then take the best (lowest)
// latency seen per region across what remains.
c.addReportAndPruneExpired(now, r)
bestRecent := c.bestRecentLatencyLocked()
// Scale each region's best latency by any provided scores from the
// DERPMap, for use in comparison below.
var scores views.Map[int, float64]
var scores views.Map[tailcfg.DERPRegionID, float64]
if hp := dm.HomeParams(); hp.Valid() {
scores = hp.RegionScore()
}
@@ -1484,7 +1530,39 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(rs *reportState, r *Report,
}
}
func updateLatency(m map[int]time.Duration, regionID int, d time.Duration) {
// bestRecentLatencyLocked returns the lowest latency seen per DERP region across
// the reports currently retained in history (c.prev), keyed by region ID. These
// latencies are used for determining preferred DERP and suggesting an exit node.
func (c *Client) bestRecentLatencyLocked() map[tailcfg.DERPRegionID]time.Duration {
best := make(map[tailcfg.DERPRegionID]time.Duration)
for _, pr := range c.prev {
for regionID, d := range pr.RegionLatency {
if bd, ok := best[regionID]; !ok || d < bd {
best[regionID] = d
}
}
}
return best
}
// RecentRegionLatency returns the lowest latency seen per DERP region over the
// recent history window, keyed by region ID.
func (c *Client) RecentRegionLatency() map[tailcfg.DERPRegionID]time.Duration {
c.mu.Lock()
defer c.mu.Unlock()
return c.bestRecentLatencyLocked()
}
// AddReportHistoryForTest records r in the client's recent-report history
// (prev/last) as if GetReport had produced it at time now, and recomputes
// r.PreferredDERP from that history.
func (c *Client) AddReportHistoryForTest(dm *tailcfg.DERPMap, r *Report, now time.Time) {
testenv.AssertInTest()
rs := &reportState{c: c, start: now}
c.addReportHistoryAndSetPreferredDERP(rs, r, dm.View(), now)
}
func updateLatency(m map[tailcfg.DERPRegionID]time.Duration, regionID tailcfg.DERPRegionID, d time.Duration) {
if prev, ok := m[regionID]; !ok || d < prev {
m[regionID] = d
}
@@ -1684,7 +1762,7 @@ func regionHasDERPNode(r *tailcfg.DERPRegion) bool {
return false
}
func maxDurationValue(m map[int]time.Duration) (max time.Duration) {
func maxDurationValue(m map[tailcfg.DERPRegionID]time.Duration) (max time.Duration) {
for _, v := range m {
if v > max {
max = v