mirror of
https://github.com/safing/portmaster
synced 2025-04-22 20:09:09 +00:00
441 lines
11 KiB
Go
441 lines
11 KiB
Go
package navigator
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
mrand "math/rand"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/safing/portmaster/service/intel/geoip"
|
|
"github.com/safing/portmaster/spn/hub"
|
|
)
|
|
|
|
const (
|
|
// defaultMaxNearbyMatches defines a default value of how many matches a
|
|
// nearby pin find operation in a map should return.
|
|
defaultMaxNearbyMatches = 100
|
|
|
|
// defaultRandomizeNearbyPinTopPercent defines the top percent of a nearby
|
|
// pins set that should be randomized for balancing purposes.
|
|
// Range: 0-1.
|
|
defaultRandomizeNearbyPinTopPercent = 0.1
|
|
)
|
|
|
|
// nearbyPins is a list of nearby Pins to a certain location.
|
|
type nearbyPins struct {
|
|
pins []*nearbyPin
|
|
minPins int
|
|
maxPins int
|
|
maxCost float32
|
|
cutOffLimit float32
|
|
randomizeTopPercent float32
|
|
|
|
debug *nearbyPinsDebug
|
|
}
|
|
|
|
// nearbyPinsDebug holds additional debugging for nearbyPins.
|
|
type nearbyPinsDebug struct {
|
|
tooExpensive []*nearbyPin
|
|
disregarded []*nearbyDisregardedPin
|
|
}
|
|
|
|
// nearbyDisregardedPin represents a disregarded pin.
|
|
type nearbyDisregardedPin struct {
|
|
pin *Pin
|
|
reason string
|
|
}
|
|
|
|
// nearbyPin represents a Pin and the proximity to a certain location.
|
|
type nearbyPin struct {
|
|
pin *Pin
|
|
cost float32
|
|
}
|
|
|
|
// Len is the number of elements in the collection.
|
|
func (nb *nearbyPins) Len() int {
|
|
return len(nb.pins)
|
|
}
|
|
|
|
// Less reports whether the element with index i should sort before the element
|
|
// with index j.
|
|
func (nb *nearbyPins) Less(i, j int) bool {
|
|
return nb.pins[i].cost < nb.pins[j].cost
|
|
}
|
|
|
|
// Swap swaps the elements with indexes i and j.
|
|
func (nb *nearbyPins) Swap(i, j int) {
|
|
nb.pins[i], nb.pins[j] = nb.pins[j], nb.pins[i]
|
|
}
|
|
|
|
// add potentially adds a Pin to the list of nearby Pins.
|
|
func (nb *nearbyPins) add(pin *Pin, cost float32) {
|
|
if len(nb.pins) > nb.minPins && nb.maxCost > 0 && cost > nb.maxCost {
|
|
// Add debug data if enabled.
|
|
if nb.debug != nil {
|
|
nb.debug.tooExpensive = append(nb.debug.tooExpensive,
|
|
&nearbyPin{
|
|
pin: pin,
|
|
cost: cost,
|
|
},
|
|
)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
nb.pins = append(nb.pins, &nearbyPin{
|
|
pin: pin,
|
|
cost: cost,
|
|
})
|
|
}
|
|
|
|
// contains checks if the collection contains a Pin.
|
|
func (nb *nearbyPins) get(id string) *nearbyPin {
|
|
for _, nbPin := range nb.pins {
|
|
if nbPin.pin.Hub.ID == id {
|
|
return nbPin
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// clean sort and shortens the list to the configured maximum.
|
|
func (nb *nearbyPins) clean() {
|
|
// Sort nearby Pins so that the closest one is on top.
|
|
sort.Sort(nb)
|
|
|
|
// Set maximum cost based on max difference, if we have enough pins.
|
|
if len(nb.pins) >= nb.minPins {
|
|
nb.maxCost = nb.pins[0].cost + nb.cutOffLimit
|
|
}
|
|
|
|
// Remove superfluous Pins from the list.
|
|
if len(nb.pins) > nb.maxPins {
|
|
// Add debug data if enabled.
|
|
if nb.debug != nil {
|
|
nb.debug.tooExpensive = append(nb.debug.tooExpensive, nb.pins[nb.maxPins:]...)
|
|
}
|
|
|
|
nb.pins = nb.pins[:nb.maxPins]
|
|
}
|
|
// Remove Pins that are too costly.
|
|
if len(nb.pins) > nb.minPins {
|
|
// Search for first pin that is too costly.
|
|
okUntil := nb.minPins
|
|
for ; okUntil < len(nb.pins); okUntil++ {
|
|
if nb.pins[okUntil].cost > nb.maxCost {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Add debug data if enabled.
|
|
if nb.debug != nil {
|
|
nb.debug.tooExpensive = append(nb.debug.tooExpensive, nb.pins[okUntil:]...)
|
|
}
|
|
|
|
// Cut off the list at that point.
|
|
nb.pins = nb.pins[:okUntil]
|
|
}
|
|
}
|
|
|
|
// randomizeTop randomized to the top nearest pins for balancing the network.
|
|
func (nb *nearbyPins) randomizeTop() {
|
|
switch {
|
|
case nb.randomizeTopPercent == 0:
|
|
// Check if randomization is enabled.
|
|
return
|
|
case len(nb.pins) < 2:
|
|
// Check if we have enough pins to work with.
|
|
return
|
|
}
|
|
|
|
// Find randomization set.
|
|
randomizeUpTo := len(nb.pins)
|
|
threshold := nb.pins[0].cost * (1 + nb.randomizeTopPercent)
|
|
for i, nb := range nb.pins {
|
|
// Find first value above the threshold to stop.
|
|
if nb.cost > threshold {
|
|
randomizeUpTo = i
|
|
break
|
|
}
|
|
}
|
|
|
|
// Shuffle top set.
|
|
if randomizeUpTo >= 2 {
|
|
mr := mrand.New(mrand.NewSource(time.Now().UnixNano())) //nolint:gosec
|
|
mr.Shuffle(randomizeUpTo, nb.Swap)
|
|
}
|
|
}
|
|
|
|
// FindNearestHubs searches for the nearest Hubs to the given IP address. The returned Hubs must not be modified in any way.
|
|
func (m *Map) FindNearestHubs(locationV4, locationV6 *geoip.Location, opts *Options, matchFor HubType) ([]*hub.Hub, error) {
|
|
m.RLock()
|
|
defer m.RUnlock()
|
|
|
|
// Check if map is populated.
|
|
if m.isEmpty() {
|
|
return nil, ErrEmptyMap
|
|
}
|
|
|
|
// Set default options if unset.
|
|
if opts == nil {
|
|
opts = m.defaultOptions()
|
|
}
|
|
|
|
// Find nearest Pins.
|
|
nearby, err := m.findNearestPins(locationV4, locationV6, opts, matchFor, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert to Hub list and return.
|
|
hubs := make([]*hub.Hub, 0, len(nearby.pins))
|
|
for _, nbPin := range nearby.pins {
|
|
hubs = append(hubs, nbPin.pin.Hub)
|
|
}
|
|
return hubs, nil
|
|
}
|
|
|
|
func (m *Map) findNearestPins(locationV4, locationV6 *geoip.Location, opts *Options, matchFor HubType, debug bool) (*nearbyPins, error) {
|
|
// Fail if no location is provided.
|
|
if locationV4 == nil && locationV6 == nil {
|
|
return nil, errors.New("no location provided")
|
|
}
|
|
|
|
// Raise maxMatches to nearestPinsMinimum.
|
|
maxMatches := defaultMaxNearbyMatches
|
|
if maxMatches < nearestPinsMinimum {
|
|
maxMatches = nearestPinsMinimum
|
|
}
|
|
|
|
// Create nearby Pins list.
|
|
nearby := &nearbyPins{
|
|
minPins: nearestPinsMinimum,
|
|
maxPins: maxMatches,
|
|
cutOffLimit: nearestPinsMaxCostDifference,
|
|
randomizeTopPercent: defaultRandomizeNearbyPinTopPercent,
|
|
}
|
|
if debug {
|
|
nearby.debug = &nearbyPinsDebug{}
|
|
}
|
|
|
|
// Create pin matcher.
|
|
matcher := opts.Matcher(matchFor, m.intel)
|
|
|
|
// Iterate over all Pins in the Map to find the nearest ones.
|
|
for _, pin := range m.all {
|
|
var cost float32
|
|
|
|
// Check if the Pin matches the criteria.
|
|
if !matcher(pin) {
|
|
// Add debug data if enabled.
|
|
if nearby.debug != nil && pin.State.Has(StateActive|StateReachable) {
|
|
nearby.debug.disregarded = append(nearby.debug.disregarded,
|
|
&nearbyDisregardedPin{
|
|
pin: pin,
|
|
reason: "does not match general criteria",
|
|
},
|
|
)
|
|
}
|
|
|
|
// Debugging:
|
|
// log.Tracef("spn/navigator: skipping %s with states %s for finding nearest", pin, pin.State)
|
|
continue
|
|
}
|
|
|
|
// Check if the Hub supports at least one IP version we are looking for.
|
|
switch {
|
|
case locationV4 != nil && pin.LocationV4 != nil:
|
|
// Both have IPv4!
|
|
case locationV6 != nil && pin.LocationV6 != nil:
|
|
// Both have IPv6!
|
|
default:
|
|
// Hub does not support any IP version we need.
|
|
|
|
// Add debug data if enabled.
|
|
if nearby.debug != nil {
|
|
nearby.debug.disregarded = append(nearby.debug.disregarded,
|
|
&nearbyDisregardedPin{
|
|
pin: pin,
|
|
reason: "does not support the required IP version",
|
|
},
|
|
)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
// If finding a home hub and the global routing profile is set to home ("VPN"),
|
|
// check if all local IP versions are available on the Hub.
|
|
if matchFor == HomeHub && cfgOptionRoutingAlgorithm() == RoutingProfileHomeID {
|
|
switch {
|
|
case locationV4 != nil && pin.LocationV4 == nil:
|
|
// Device has IPv4, but Hub does not!
|
|
fallthrough
|
|
case locationV6 != nil && pin.LocationV6 == nil:
|
|
// Device has IPv6, but Hub does not!
|
|
|
|
// Add debug data if enabled.
|
|
if nearby.debug != nil {
|
|
nearby.debug.disregarded = append(nearby.debug.disregarded,
|
|
&nearbyDisregardedPin{
|
|
pin: pin,
|
|
reason: "home hub needs all IP versions of client (when Home/VPN routing)",
|
|
},
|
|
)
|
|
}
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
// 1. Calculate cost based on distance
|
|
|
|
if locationV4 != nil && pin.LocationV4 != nil {
|
|
if locationV4.IsAnycast && m.home != nil {
|
|
// If the destination is anycast, calculate cost though proximity to home hub instead, if possible.
|
|
cost = lessButPositive(cost, CalculateDestinationCost(
|
|
proximityBetweenPins(pin, m.home),
|
|
))
|
|
} else {
|
|
// Regular cost calculation through proximity.
|
|
cost = lessButPositive(cost, CalculateDestinationCost(
|
|
locationV4.EstimateNetworkProximity(pin.LocationV4),
|
|
))
|
|
}
|
|
}
|
|
|
|
if locationV6 != nil && pin.LocationV6 != nil {
|
|
if locationV6.IsAnycast && m.home != nil {
|
|
// If the destination is anycast, calculate cost though proximity to home hub instead, if possible.
|
|
cost = lessButPositive(cost, CalculateDestinationCost(
|
|
proximityBetweenPins(pin, m.home),
|
|
))
|
|
} else {
|
|
// Regular cost calculation through proximity.
|
|
cost = lessButPositive(cost, CalculateDestinationCost(
|
|
locationV6.EstimateNetworkProximity(pin.LocationV6),
|
|
))
|
|
}
|
|
}
|
|
|
|
// If no cost could be calculated, fall back to a default value.
|
|
if cost == 0 {
|
|
cost = CalculateDestinationCost(50) // proximity out of 0-100
|
|
}
|
|
|
|
// Debugging:
|
|
// if matchFor == HomeHub {
|
|
// log.Tracef("spn/navigator: adding %.2f proximity cost to home hub %s", cost, pin.Hub)
|
|
// }
|
|
|
|
// 2. Add cost based on Hub status
|
|
|
|
cost += CalculateHubCost(pin.Hub.Status.Load)
|
|
|
|
// Debugging:
|
|
// if matchFor == HomeHub {
|
|
// log.Tracef("spn/navigator: adding %.2f hub cost to home hub %s", CalculateHubCost(pin.Hub.Status.Load), pin.Hub)
|
|
// }
|
|
|
|
// 3. If matching a home hub, add cost based on capacity/latency performance.
|
|
|
|
if matchFor == HomeHub {
|
|
// Find best capacity/latency values.
|
|
var (
|
|
bestCapacity int
|
|
bestLatency time.Duration
|
|
)
|
|
for _, lane := range pin.Hub.Status.Lanes {
|
|
if lane.Capacity > bestCapacity {
|
|
bestCapacity = lane.Capacity
|
|
}
|
|
if bestLatency == 0 || lane.Latency < bestLatency {
|
|
bestLatency = lane.Latency
|
|
}
|
|
}
|
|
// Add cost of best capacity/latency values.
|
|
cost += CalculateLaneCost(bestLatency, bestCapacity)
|
|
|
|
// Debugging:
|
|
// log.Tracef("spn/navigator: adding %.2f lane cost to home hub %s", CalculateLaneCost(bestLatency, bestCapacity), pin.Hub)
|
|
// log.Debugf("spn/navigator: total cost of %.2f to home hub %s", cost, pin.Hub)
|
|
}
|
|
|
|
nearby.add(pin, cost)
|
|
|
|
// Clean the nearby list if have collected more than two times the max amount.
|
|
if len(nearby.pins) >= nearby.maxPins*2 {
|
|
nearby.clean()
|
|
}
|
|
}
|
|
|
|
// Check if we found any nearby pins
|
|
if nearby.Len() == 0 {
|
|
return nil, ErrAllPinsDisregarded
|
|
}
|
|
|
|
// Clean one last time and return the list.
|
|
nearby.clean()
|
|
|
|
// Randomize top nearest pins for load balancing.
|
|
nearby.randomizeTop()
|
|
|
|
// Debugging:
|
|
// if matchFor == HomeHub {
|
|
// log.Debug("spn/navigator: nearby pins:")
|
|
// for _, nbPin := range nearby.pins {
|
|
// log.Debugf("spn/navigator: nearby pin %s", nbPin)
|
|
// }
|
|
// }
|
|
|
|
return nearby, nil
|
|
}
|
|
|
|
func (nb *nearbyPins) String() string {
|
|
s := make([]string, 0, len(nb.pins))
|
|
for _, nbPin := range nb.pins {
|
|
s = append(s, nbPin.String())
|
|
}
|
|
return strings.Join(s, ", ")
|
|
}
|
|
|
|
func (nb *nearbyPin) String() string {
|
|
return fmt.Sprintf("%s at %.2fc", nb.pin, nb.cost)
|
|
}
|
|
|
|
func proximityBetweenPins(a, b *Pin) float32 {
|
|
var x, y float32
|
|
|
|
// Get IPv4 network proximity.
|
|
if a.LocationV4 != nil && b.LocationV4 != nil {
|
|
x = a.LocationV4.EstimateNetworkProximity(b.LocationV4)
|
|
}
|
|
|
|
// Get IPv6 network proximity.
|
|
if a.LocationV6 != nil && b.LocationV6 != nil {
|
|
y = a.LocationV6.EstimateNetworkProximity(b.LocationV6)
|
|
}
|
|
|
|
// Return higher proximity.
|
|
if x > y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|
|
|
|
func lessButPositive(a, b float32) float32 {
|
|
switch {
|
|
case a == 0:
|
|
return b
|
|
case b == 0:
|
|
return a
|
|
case a < b:
|
|
return a
|
|
default:
|
|
return b
|
|
}
|
|
}
|