diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index b8aa769a1..5567cc03d 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -13,6 +13,8 @@ import ( "io" "log" "maps" + "math" + "math/rand" "net" "net/http" "net/netip" @@ -57,6 +59,7 @@ import ( "tailscale.com/net/dnscache" "tailscale.com/net/dnsfallback" "tailscale.com/net/interfaces" + "tailscale.com/net/netcheck" "tailscale.com/net/netkernelconf" "tailscale.com/net/netmon" "tailscale.com/net/netns" @@ -5990,3 +5993,91 @@ func mayDeref[T any](p *T) (v T) { } return *p } + +// productionRNG is used for randomly picking an exit node in SuggestDERPExitNode. +var productionRNG = rand.New(rand.NewSource(rand.Int63())) + +const derpPrefix = "127.3.3.40:" + +func suggestDERPExitNode(lastReport *netcheck.Report, netMap *netmap.NetworkMap, rng *rand.Rand) (tailcfg.StableNodeID, error) { + peers := netMap.Peers + var preferredExitNodeID tailcfg.StableNodeID + peerRegionMap := make(map[int][]tailcfg.NodeView) + var sortedRegions []int + for r := range netMap.DERPMap.Regions { + sortedRegions = append(sortedRegions, r) + } + derpRegionLat := netMap.DERPMap.Regions[netMap.SelfNode.Hostinfo().NetInfo().PreferredDERP()].Latitude + derpRegionLong := netMap.DERPMap.Regions[netMap.SelfNode.Hostinfo().NetInfo().PreferredDERP()].Longitude + var mullvadCandidates []tailcfg.NodeView + sortedRegions = sortRegions(sortedRegions, lastReport) + + for _, peer := range peers { + if online := peer.Online(); online != nil && !*online { + continue + } + if tsaddr.ContainsExitRoutes(peer.AllowedIPs()) { + if peer.DERP() == "" { + if longLatDistance([]float64{derpRegionLat, derpRegionLong}, []float64{peer.Hostinfo().Location().Latitude, peer.Hostinfo().Location().Longitude}) < 1300000 { + mullvadCandidates = append(mullvadCandidates, peer) + } + } + ipp, _ := netip.ParseAddrPort(peer.DERP()) + if peer.DERP() == "" || !strings.HasPrefix(peer.DERP(), derpPrefix) { + continue + } + regionID := int(ipp.Port()) + peerRegionMap[regionID] = append(peerRegionMap[regionID], peer) + } + } + + for _, r := range sortedRegions { + peers, ok := peerRegionMap[r] + if ok { + preferredExitNodeID = pick(peers, rng).StableID() + break + } + } + if preferredExitNodeID.IsZero() { + return preferredExitNodeID, fmt.Errorf("Unable to choose exit node") + } + return preferredExitNodeID, nil +} + +// pick randomly selects a tailcfg.NodeView given a list of tailcfg.NodeView and rand.Rand. +func pick(candidates []tailcfg.NodeView, rng *rand.Rand) tailcfg.NodeView { + if len(candidates) == 1 { + return candidates[0] + } else { + sort.Slice(candidates, func(i, j int) bool { return candidates[i].ID() < candidates[j].ID() }) + chosen := candidates[rng.Intn(len(candidates))] + return chosen + } +} + +// sortRegions returns a list of sorted regions by ascending latency given a list of region IDs and a netcheck report. +func sortRegions(regions []int, lastReport *netcheck.Report) []int { + sort.Slice(regions, func(i, j int) bool { + iLatency, iOk := lastReport.RegionLatency[regions[i]] + if !iOk || iLatency == 0 { + iLatency = math.MaxInt + } + jLatency, jOk := lastReport.RegionLatency[regions[j]] + if !jOk || jLatency == 0 { + jLatency = math.MaxInt + } + return iLatency < jLatency + }) + return regions +} + +func longLatDistance(firstDistance []float64, secondDistance []float64) float64 { + diffLat := (firstDistance[0] - secondDistance[0]) * math.Pi / 180 + diffLong := (firstDistance[1] - secondDistance[1]) * math.Pi / 180 + latRadians1 := firstDistance[0] * math.Pi / 180 + latRadians2 := secondDistance[0] * math.Pi / 180 + a := math.Pow(math.Sin(diffLat)/2, 2) + math.Pow(math.Sin(diffLong)/2, 2)*math.Cos(latRadians1)*math.Cos(latRadians2) + earthRadius := float64(6371000) // earth radius is 6371000 meters + c := 2 * math.Asin(math.Sqrt(a)) + return earthRadius * c +} diff --git a/tailcfg/derpmap.go b/tailcfg/derpmap.go index d95d26d57..2b64903c2 100644 --- a/tailcfg/derpmap.go +++ b/tailcfg/derpmap.go @@ -105,7 +105,9 @@ type DERPRegion struct { // for a user/network pick the first one (as they should, when // things are healthy), the inter-cluster routing is minimal // to zero. - Nodes []*DERPNode + Nodes []*DERPNode + Latitude float64 + Longitude float64 } // DERPNode describes a DERP packet relay node running within a DERPRegion. diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 5e4d88243..182283769 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -407,6 +407,8 @@ var _DERPRegionCloneNeedsRegeneration = DERPRegion(struct { RegionName string Avoid bool Nodes []*DERPNode + Latitude float64 + Longitude float64 }{}) // Clone makes a deep copy of DERPMap. @@ -576,6 +578,8 @@ var _LocationCloneNeedsRegeneration = Location(struct { City string CityCode string Priority int + Longitude float64 + Latitude float64 }{}) // Clone makes a deep copy of UserProfile. diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index e42688602..b7c7c4749 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -3008,3 +3008,13 @@ func getPeerMTUsProbedMetric(mtu tstun.WireMTU) *clientmetric.Metric { mm, _ := metricRecvDiscoPeerMTUProbesByMTU.LoadOrInit(key, func() *clientmetric.Metric { return clientmetric.NewCounter(key) }) return mm } + +// GetLastNetcheckReport returns the last netcheck report. +func (c *Conn) GetLastNetcheckReport(ctx context.Context) *netcheck.Report { + nr, err := c.updateNetInfo(ctx) + if err != nil { + c.logf("magicsock.Conn.determineEndpoints: updateNetInfo: %v", err) + return nil + } + return nr +}