wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
234
spn/navigator/findroutes.go
Normal file
234
spn/navigator/findroutes.go
Normal file
@@ -0,0 +1,234 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user