mirror of
https://github.com/safing/portmaster
synced 2025-04-24 04:49:10 +00:00
234 lines
5.8 KiB
Go
234 lines
5.8 KiB
Go
package navigator
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
|
|
"github.com/safing/portmaster/service/intel/geoip"
|
|
)
|
|
|
|
const (
|
|
// defaultMaxRouteMatches defines a default value of how many matches a
|
|
// route find operation in a map should return.
|
|
defaultMaxRouteMatches = 10
|
|
|
|
// defaultRandomizeRoutesTopPercent defines the top percent of a routes
|
|
// set that should be randomized for balancing purposes.
|
|
// Range: 0-1.
|
|
defaultRandomizeRoutesTopPercent = 0.1
|
|
)
|
|
|
|
// FindRoutes finds possible routes to the given IP, with the given options.
|
|
func (m *Map) FindRoutes(ip net.IP, opts *Options) (*Routes, error) {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
// Check if map is populated.
|
|
if m.isEmpty() {
|
|
return nil, ErrEmptyMap
|
|
}
|
|
|
|
// Check if home hub is set.
|
|
if m.home == nil {
|
|
return nil, ErrHomeHubUnset
|
|
}
|
|
|
|
// Get the location of the given IP address.
|
|
var locationV4, locationV6 *geoip.Location
|
|
var err error
|
|
// Save whether the given IP address is a IPv4 or IPv6 address.
|
|
if v4 := ip.To4(); v4 != nil {
|
|
locationV4, err = geoip.GetLocation(ip)
|
|
} else {
|
|
locationV6, err = geoip.GetLocation(ip)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get IP location: %w", err)
|
|
}
|
|
|
|
// Set default options if unset.
|
|
if opts == nil {
|
|
opts = m.defaultOptions()
|
|
}
|
|
|
|
// Handle special home routing profile.
|
|
if opts.RoutingProfile == RoutingProfileHomeID {
|
|
switch {
|
|
case locationV4 != nil && m.home.LocationV4 == nil:
|
|
// Destination is IPv4, but Hub has no IPv4!
|
|
// Upgrade routing profile.
|
|
opts.RoutingProfile = RoutingProfileSingleHopID
|
|
|
|
case locationV6 != nil && m.home.LocationV6 == nil:
|
|
// Destination is IPv6, but Hub has no IPv6!
|
|
// Upgrade routing profile.
|
|
opts.RoutingProfile = RoutingProfileSingleHopID
|
|
|
|
default:
|
|
// Return route with only home hub for home hub routing.
|
|
return &Routes{
|
|
All: []*Route{{
|
|
Path: []*Hop{{
|
|
pin: m.home,
|
|
HubID: m.home.Hub.ID,
|
|
}},
|
|
Algorithm: RoutingProfileHomeID,
|
|
}},
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// Find nearest Pins.
|
|
nearby, err := m.findNearestPins(locationV4, locationV6, opts, DestinationHub, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m.findRoutes(nearby, opts)
|
|
}
|
|
|
|
// FindRouteToHub finds possible routes to the given Hub, with the given options.
|
|
func (m *Map) FindRouteToHub(hubID string, opts *Options) (*Routes, error) {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
// Get Pin.
|
|
pin, ok := m.all[hubID]
|
|
if !ok {
|
|
return nil, ErrHubNotFound
|
|
}
|
|
|
|
// Create a nearby with a single Pin.
|
|
nearby := &nearbyPins{
|
|
pins: []*nearbyPin{
|
|
{
|
|
pin: pin,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Find a route to the given Hub.
|
|
return m.findRoutes(nearby, opts)
|
|
}
|
|
|
|
func (m *Map) findRoutes(dsts *nearbyPins, opts *Options) (*Routes, error) {
|
|
if m.home == nil {
|
|
return nil, ErrHomeHubUnset
|
|
}
|
|
|
|
// Initialize matchers.
|
|
var done bool
|
|
transitMatcher := opts.Transit.Matcher(m.intel)
|
|
destinationMatcher := opts.Destination.Matcher(m.intel)
|
|
routingProfile := GetRoutingProfile(opts.RoutingProfile)
|
|
|
|
// Create routes collector.
|
|
routes := &Routes{
|
|
maxRoutes: defaultMaxRouteMatches,
|
|
randomizeTopPercent: defaultRandomizeRoutesTopPercent,
|
|
}
|
|
|
|
// TODO:
|
|
// Start from the destination and use HopDistance to prioritize
|
|
// exploring routes that are in the right direction.
|
|
// How would we handle selecting the destination node based on route to client?
|
|
// Should we just try all destinations?
|
|
|
|
// Create initial route.
|
|
route := &Route{
|
|
// Estimate how much space we will need, else it'll just expand.
|
|
Path: make([]*Hop, 1, routingProfile.MinHops+routingProfile.MaxExtraHops),
|
|
}
|
|
route.Path[0] = &Hop{
|
|
pin: m.home,
|
|
// TODO: add initial cost
|
|
}
|
|
|
|
// exploreHop explores a hop (Lane) to a connected Pin.
|
|
var exploreHop func(route *Route, lane *Lane)
|
|
|
|
// exploreLanes explores all Lanes of a Pin.
|
|
exploreLanes := func(route *Route) {
|
|
for _, lane := range route.Path[len(route.Path)-1].pin.ConnectedTo {
|
|
// Check if we are done and can skip the rest.
|
|
if done {
|
|
return
|
|
}
|
|
|
|
// Explore!
|
|
exploreHop(route, lane)
|
|
}
|
|
}
|
|
|
|
exploreHop = func(route *Route, lane *Lane) {
|
|
// Check if the Pin should be regarded as Transit Hub.
|
|
if !transitMatcher(lane.Pin) {
|
|
return
|
|
}
|
|
|
|
// Add Pin to the current path and remove when done.
|
|
route.addHop(lane.Pin, lane.Cost+lane.Pin.Cost)
|
|
defer route.removeHop()
|
|
|
|
// Check if the route would even make it into the list.
|
|
if !routes.isGoodEnough(route) {
|
|
return
|
|
}
|
|
|
|
// Check route compliance.
|
|
// This also includes some algorithm-based optimizations.
|
|
switch routingProfile.checkRouteCompliance(route, routes) {
|
|
case routeOk:
|
|
// Route would be compliant.
|
|
// Now, check if the last hop qualifies as a Destination Hub.
|
|
if destinationMatcher(lane.Pin) {
|
|
// Get Pin as nearby Pin.
|
|
nbPin := dsts.get(lane.Pin.Hub.ID)
|
|
if nbPin != nil {
|
|
// Pin is listed as selected Destination Hub!
|
|
// Complete route to add destination ("last mile") cost.
|
|
route.completeRoute(nbPin.cost)
|
|
routes.add(route)
|
|
|
|
// We have found a route and have come to an end here.
|
|
return
|
|
}
|
|
}
|
|
|
|
// The Route is compliant, but we haven't found a Destination Hub yet.
|
|
fallthrough
|
|
case routeNonCompliant:
|
|
// Continue exploration.
|
|
exploreLanes(route)
|
|
case routeDisqualified:
|
|
fallthrough
|
|
default:
|
|
// Route is disqualified and we can return without further exploration.
|
|
}
|
|
}
|
|
|
|
// Start the hop exploration tree.
|
|
// This will fork into about a gazillion branches and add all the found valid
|
|
// routes to the list.
|
|
exploreLanes(route)
|
|
|
|
// Check if we found anything.
|
|
if len(routes.All) == 0 {
|
|
return nil, errors.New("failed to find any routes")
|
|
}
|
|
|
|
// Randomize top routes for load balancing.
|
|
routes.randomizeTop()
|
|
|
|
// Copy remaining data to routes.
|
|
routes.makeExportReady(opts.RoutingProfile)
|
|
|
|
// Debugging:
|
|
// log.Debug("spn/navigator: routes:")
|
|
// for _, route := range routes.All {
|
|
// log.Debugf("spn/navigator: %s", route)
|
|
// }
|
|
|
|
return routes, nil
|
|
}
|