wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
672
spn/navigator/api.go
Normal file
672
spn/navigator/api.go
Normal file
@@ -0,0 +1,672 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/awalterschulze/gographviz"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/spn/docks"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
var (
|
||||
apiMapsLock sync.Mutex
|
||||
apiMaps = make(map[string]*Map)
|
||||
)
|
||||
|
||||
func addMapToAPI(m *Map) {
|
||||
apiMapsLock.Lock()
|
||||
defer apiMapsLock.Unlock()
|
||||
|
||||
apiMaps[m.Name] = m
|
||||
}
|
||||
|
||||
func getMapForAPI(name string) (m *Map, ok bool) {
|
||||
apiMapsLock.Lock()
|
||||
defer apiMapsLock.Unlock()
|
||||
|
||||
m, ok = apiMaps[name]
|
||||
return
|
||||
}
|
||||
|
||||
func removeMapFromAPI(name string) {
|
||||
apiMapsLock.Lock()
|
||||
defer apiMapsLock.Unlock()
|
||||
|
||||
delete(apiMaps, name)
|
||||
}
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/pins`,
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: handleMapPinsRequest,
|
||||
Name: "Get SPN map pins",
|
||||
Description: "Returns a list of pins on the map.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/intel/update`,
|
||||
Write: api.PermitSelf,
|
||||
BelongsTo: module,
|
||||
ActionFunc: handleIntelUpdateRequest,
|
||||
Name: "Update map intelligence.",
|
||||
Description: "Updates the intel data of the map.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/optimization`,
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: handleMapOptimizationRequest,
|
||||
Name: "Get SPN map optimization",
|
||||
Description: "Returns the calculated optimization for the map.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/optimization/table`,
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
DataFunc: handleMapOptimizationTableRequest,
|
||||
Name: "Get SPN map optimization as a table",
|
||||
Description: "Returns the calculated optimization for the map as a table.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/measurements`,
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: handleMapMeasurementsRequest,
|
||||
Name: "Get SPN map measurements",
|
||||
Description: "Returns the measurements of the map.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/measurements/table`,
|
||||
MimeType: api.MimeTypeText,
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
DataFunc: handleMapMeasurementsTableRequest,
|
||||
Name: "Get SPN map measurements as a table",
|
||||
Description: "Returns the measurements of the map as a table.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/graph{format:\.[a-z]{2,4}}`,
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
HandlerFunc: handleMapGraphRequest,
|
||||
Name: "Get SPN map graph",
|
||||
Description: "Returns a graph of the given SPN map.",
|
||||
Parameters: []api.Parameter{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Field: "map (in path)",
|
||||
Value: "name of map",
|
||||
Description: "Specify the map you want to get the map for. The main map is called `main`.",
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Field: "format (in path)",
|
||||
Value: "file type",
|
||||
Description: "Specify the format you want to get the map in. Available values: `dot`, `html`. Please note that the html format is only available in development mode.",
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Register API endpoints from other files.
|
||||
if err := registerRouteAPIEndpoints(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMapPinsRequest(ar *api.Request) (i interface{}, err error) {
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(ar.URLVars["map"])
|
||||
if !ok {
|
||||
return nil, errors.New("map not found")
|
||||
}
|
||||
|
||||
// Export all pins.
|
||||
sortedPins := m.sortedPins(true)
|
||||
exportedPins := make([]*PinExport, len(sortedPins))
|
||||
for key, pin := range sortedPins {
|
||||
exportedPins[key] = pin.Export()
|
||||
}
|
||||
|
||||
return exportedPins, nil
|
||||
}
|
||||
|
||||
func handleIntelUpdateRequest(ar *api.Request) (msg string, err error) {
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(ar.URLVars["map"])
|
||||
if !ok {
|
||||
return "", errors.New("map not found")
|
||||
}
|
||||
|
||||
// Parse new intel data.
|
||||
newIntel, err := hub.ParseIntel(ar.InputData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse intel data: %w", err)
|
||||
}
|
||||
|
||||
// Apply intel data.
|
||||
err = m.UpdateIntel(newIntel, cfgOptionTrustNodeNodes())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to apply intel data: %w", err)
|
||||
}
|
||||
|
||||
return "successfully applied given intel data", nil
|
||||
}
|
||||
|
||||
func handleMapOptimizationRequest(ar *api.Request) (i interface{}, err error) {
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(ar.URLVars["map"])
|
||||
if !ok {
|
||||
return nil, errors.New("map not found")
|
||||
}
|
||||
|
||||
return m.Optimize(nil)
|
||||
}
|
||||
|
||||
func handleMapOptimizationTableRequest(ar *api.Request) (data []byte, err error) {
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(ar.URLVars["map"])
|
||||
if !ok {
|
||||
return nil, errors.New("map not found")
|
||||
}
|
||||
|
||||
// Get optimization result.
|
||||
result, err := m.Optimize(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read lock map, as we access pins.
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
// Get cranes for additional metadata.
|
||||
assignedCranes := docks.GetAllAssignedCranes()
|
||||
|
||||
// Write metadata.
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.WriteString("Optimization:\n")
|
||||
fmt.Fprintf(buf, "Purpose: %s\n", result.Purpose)
|
||||
if len(result.Approach) == 1 {
|
||||
fmt.Fprintf(buf, "Approach: %s\n", result.Approach[0])
|
||||
} else if len(result.Approach) > 1 {
|
||||
buf.WriteString("Approach:\n")
|
||||
for _, approach := range result.Approach {
|
||||
fmt.Fprintf(buf, " - %s\n", approach)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(buf, "MaxConnect: %d\n", result.MaxConnect)
|
||||
fmt.Fprintf(buf, "StopOthers: %v\n", result.StopOthers)
|
||||
|
||||
// Build table of suggested connections.
|
||||
buf.WriteString("\nSuggested Connections:\n")
|
||||
tabWriter := tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0)
|
||||
fmt.Fprint(tabWriter, "Hub Name\tReason\tDuplicate\tCountry\tRegion\tLatency\tCapacity\tCost\tGeo Prox.\tHub ID\tLifetime Usage\tPeriod Usage\tProt\tStatus\n")
|
||||
for _, suggested := range result.SuggestedConnections {
|
||||
var dupe string
|
||||
if suggested.Duplicate {
|
||||
dupe = "yes"
|
||||
} else {
|
||||
// Only lock dupes once.
|
||||
suggested.pin.measurements.Lock()
|
||||
defer suggested.pin.measurements.Unlock()
|
||||
}
|
||||
|
||||
// Add row.
|
||||
fmt.Fprintf(tabWriter,
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t%.2fMbit/s\t%.2fc\t%.2f%%\t%s",
|
||||
suggested.Hub.Info.Name,
|
||||
suggested.Reason,
|
||||
dupe,
|
||||
getPinCountry(suggested.pin),
|
||||
suggested.pin.region.getName(),
|
||||
suggested.pin.measurements.Latency,
|
||||
float64(suggested.pin.measurements.Capacity)/1000000,
|
||||
suggested.pin.measurements.CalculatedCost,
|
||||
suggested.pin.measurements.GeoProximity,
|
||||
suggested.Hub.ID,
|
||||
)
|
||||
|
||||
// Add usage stats.
|
||||
if crane, ok := assignedCranes[suggested.Hub.ID]; ok {
|
||||
addUsageStatsToTable(crane, tabWriter)
|
||||
}
|
||||
|
||||
// Add linebreak.
|
||||
fmt.Fprint(tabWriter, "\n")
|
||||
}
|
||||
_ = tabWriter.Flush()
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// addUsageStatsToTable compiles some usage stats of a lane and addes them to the table.
|
||||
// Table Fields: Lifetime Usage, Period Usage, Prot, Mine.
|
||||
func addUsageStatsToTable(crane *docks.Crane, tabWriter *tabwriter.Writer) {
|
||||
ltIn, ltOut, ltStart, pIn, pOut, pStart := crane.NetState.GetTrafficStats()
|
||||
ltDuration := time.Since(ltStart)
|
||||
pDuration := time.Since(pStart)
|
||||
|
||||
// Build ownership and stopping info.
|
||||
var status string
|
||||
isMine := crane.IsMine()
|
||||
isStopping := crane.IsStopping()
|
||||
stoppingRequested, stoppingRequestedByPeer, markedStoppingAt := crane.NetState.StoppingState()
|
||||
if isMine {
|
||||
status = "mine"
|
||||
}
|
||||
if isStopping || stoppingRequested || stoppingRequestedByPeer {
|
||||
if isMine {
|
||||
status += " - "
|
||||
}
|
||||
status += "stopping "
|
||||
if stoppingRequested {
|
||||
status += "<r"
|
||||
}
|
||||
if isStopping {
|
||||
status += "!"
|
||||
}
|
||||
if stoppingRequestedByPeer {
|
||||
status += "r>"
|
||||
}
|
||||
if isStopping && !markedStoppingAt.IsZero() {
|
||||
status += " since " + markedStoppingAt.Truncate(time.Minute).String()
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(tabWriter,
|
||||
"\t%.2fGB %.2fMbit/s %.2f%%out since %s\t%.2fGB %.2fMbit/s %.2f%%out since %s\t%s\t%s",
|
||||
float64(ltIn+ltOut)/1000000000,
|
||||
(float64(ltIn+ltOut)/1000000/ltDuration.Seconds())*8,
|
||||
float64(ltOut)/float64(ltIn+ltOut)*100,
|
||||
ltDuration.Truncate(time.Second),
|
||||
float64(pIn+pOut)/1000000000,
|
||||
(float64(pIn+pOut)/1000000/pDuration.Seconds())*8,
|
||||
float64(pOut)/float64(pIn+pOut)*100,
|
||||
pDuration.Truncate(time.Second),
|
||||
crane.Transport().Protocol,
|
||||
status,
|
||||
)
|
||||
}
|
||||
|
||||
func handleMapMeasurementsRequest(ar *api.Request) (i interface{}, err error) {
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(ar.URLVars["map"])
|
||||
if !ok {
|
||||
return nil, errors.New("map not found")
|
||||
}
|
||||
|
||||
// Get and sort pins.
|
||||
list := m.pinList(true)
|
||||
sort.Sort(sortByLowestMeasuredCost(list))
|
||||
|
||||
// Copy data and return.
|
||||
measurements := make([]*hub.Measurements, 0, len(list))
|
||||
for _, pin := range list {
|
||||
measurements = append(measurements, pin.measurements.Copy())
|
||||
}
|
||||
return measurements, nil
|
||||
}
|
||||
|
||||
func handleMapMeasurementsTableRequest(ar *api.Request) (data []byte, err error) {
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(ar.URLVars["map"])
|
||||
if !ok {
|
||||
return nil, errors.New("map not found")
|
||||
}
|
||||
matcher := m.DefaultOptions().Transit.Matcher(m.GetIntel())
|
||||
|
||||
// Get and sort pins.
|
||||
list := m.pinList(true)
|
||||
sort.Sort(sortByLowestMeasuredCost(list))
|
||||
|
||||
// Get cranes for usage stats.
|
||||
assignedCranes := docks.GetAllAssignedCranes()
|
||||
|
||||
// Build table and return.
|
||||
buf := bytes.NewBuffer(nil)
|
||||
tabWriter := tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0)
|
||||
fmt.Fprint(tabWriter, "Hub Name\tCountry\tRegion\tLatency\tCapacity\tCost\tGeo Prox.\tHub ID\tLifetime Usage\tPeriod Usage\tProt\tStatus\n")
|
||||
for _, pin := range list {
|
||||
// Only print regarded Hubs.
|
||||
if !matcher(pin) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add row.
|
||||
pin.measurements.Lock()
|
||||
defer pin.measurements.Unlock()
|
||||
fmt.Fprintf(tabWriter,
|
||||
"%s\t%s\t%s\t%s\t%.2fMbit/s\t%.2fc\t%.2f%%\t%s",
|
||||
pin.Hub.Info.Name,
|
||||
getPinCountry(pin),
|
||||
pin.region.getName(),
|
||||
pin.measurements.Latency,
|
||||
float64(pin.measurements.Capacity)/1000000,
|
||||
pin.measurements.CalculatedCost,
|
||||
pin.measurements.GeoProximity,
|
||||
pin.Hub.ID,
|
||||
)
|
||||
|
||||
// Add usage stats.
|
||||
if crane, ok := assignedCranes[pin.Hub.ID]; ok {
|
||||
addUsageStatsToTable(crane, tabWriter)
|
||||
}
|
||||
|
||||
// Add linebreak.
|
||||
fmt.Fprint(tabWriter, "\n")
|
||||
}
|
||||
_ = tabWriter.Flush()
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func getPinCountry(pin *Pin) string {
|
||||
switch {
|
||||
case pin.LocationV4 != nil && pin.LocationV4.Country.Code != "":
|
||||
return pin.LocationV4.Country.Code
|
||||
case pin.LocationV6 != nil && pin.LocationV6.Country.Code != "":
|
||||
return pin.LocationV6.Country.Code
|
||||
case pin.EntityV4 != nil && pin.EntityV4.Country != "":
|
||||
return pin.EntityV4.Country
|
||||
case pin.EntityV6 != nil && pin.EntityV6.Country != "":
|
||||
return pin.EntityV6.Country
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func handleMapGraphRequest(w http.ResponseWriter, hr *http.Request) {
|
||||
r := api.GetAPIRequest(hr)
|
||||
if r == nil {
|
||||
http.Error(w, "API request invalid.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(r.URLVars["map"])
|
||||
if !ok {
|
||||
http.Error(w, "Map not found.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Check format.
|
||||
var format string
|
||||
switch r.URLVars["format"] {
|
||||
case ".dot":
|
||||
format = "dot"
|
||||
case ".html":
|
||||
format = "html"
|
||||
|
||||
// Check if we are in dev mode.
|
||||
if !devMode() {
|
||||
http.Error(w, "Graph html formatting (js rendering) is only available in dev mode.", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.Error(w, "Unsupported format.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Build graph.
|
||||
graph := gographviz.NewGraph()
|
||||
_ = graph.AddAttr("", "overlap", "scale")
|
||||
_ = graph.AddAttr("", "center", "true")
|
||||
_ = graph.AddAttr("", "ratio", "fill")
|
||||
for _, pin := range m.sortedPins(true) {
|
||||
_ = graph.AddNode("", pin.Hub.ID, map[string]string{
|
||||
"label": graphNodeLabel(pin),
|
||||
"tooltip": graphNodeTooltip(pin),
|
||||
"color": graphNodeBorderColor(pin),
|
||||
"fillcolor": graphNodeColor(pin),
|
||||
"shape": "circle",
|
||||
"style": "filled",
|
||||
"fontsize": "20",
|
||||
"penwidth": "4",
|
||||
"margin": "0",
|
||||
})
|
||||
for _, lane := range pin.ConnectedTo {
|
||||
if graph.IsNode(lane.Pin.Hub.ID) && pin.State != StateNone {
|
||||
// Create attributes.
|
||||
edgeOptions := map[string]string{
|
||||
"tooltip": graphEdgeTooltip(pin, lane.Pin, lane),
|
||||
"color": graphEdgeColor(pin, lane.Pin, lane),
|
||||
"len": fmt.Sprintf("%f", lane.Latency.Seconds()*200),
|
||||
"penwidth": fmt.Sprintf("%f", math.Sqrt(float64(lane.Capacity)/1000000)*2),
|
||||
}
|
||||
// Add edge.
|
||||
_ = graph.AddEdge(pin.Hub.ID, lane.Pin.Hub.ID, false, edgeOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mimeType string
|
||||
var responseData []byte
|
||||
switch format {
|
||||
case "dot":
|
||||
mimeType = "text/x-dot"
|
||||
responseData = []byte(graph.String())
|
||||
case "html":
|
||||
mimeType = "text/html"
|
||||
responseData = []byte(fmt.Sprintf(
|
||||
`<!DOCTYPE html><html><meta charset="utf-8"><body style="margin:0;padding:0;">
|
||||
<style>#graph svg {height: 99.5vh; width: 99.5vw;}</style>
|
||||
<div id="graph"></div>
|
||||
<script src="/assets/vendor/js/hpcc-js-wasm-1.13.0/index.min.js"></script>
|
||||
<script src="/assets/vendor/js/d3-7.3.0/d3.min.js"></script>
|
||||
<script src="/assets/vendor/js/d3-graphviz-4.1.0/d3-graphviz.min.js"></script>
|
||||
<script>
|
||||
d3.select("#graph").graphviz(useWorker=false).engine("neato").renderDot(%s%s%s);
|
||||
</script>
|
||||
</body></html>`,
|
||||
"`", graph.String(), "`",
|
||||
))
|
||||
}
|
||||
|
||||
// Write response.
|
||||
w.Header().Set("Content-Type", mimeType+"; charset=utf-8")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(responseData)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := w.Write(responseData)
|
||||
if err != nil {
|
||||
log.Tracer(r.Context()).Warningf("api: failed to write response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func graphNodeLabel(pin *Pin) (s string) {
|
||||
var comment string
|
||||
switch {
|
||||
case pin.State == StateNone:
|
||||
comment = "dead"
|
||||
case pin.State.Has(StateIsHomeHub):
|
||||
comment = "Home"
|
||||
case pin.State.HasAnyOf(StateSummaryDisregard):
|
||||
comment = "disregarded"
|
||||
case !pin.State.Has(StateSummaryRegard):
|
||||
comment = "not regarded"
|
||||
case pin.State.Has(StateTrusted):
|
||||
comment = "trusted"
|
||||
}
|
||||
if comment != "" {
|
||||
comment = fmt.Sprintf("\n(%s)", comment)
|
||||
}
|
||||
|
||||
if pin.Hub.Status.Load >= 80 {
|
||||
comment += fmt.Sprintf("\nHIGH LOAD: %d", pin.Hub.Status.Load)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`"%s%s"`,
|
||||
strings.ReplaceAll(pin.Hub.Name(), " ", "\n"),
|
||||
comment,
|
||||
)
|
||||
}
|
||||
|
||||
func graphNodeTooltip(pin *Pin) string {
|
||||
// Gather IP info.
|
||||
var v4Info, v6Info string
|
||||
if pin.Hub.Info.IPv4 != nil {
|
||||
if pin.LocationV4 != nil {
|
||||
v4Info = fmt.Sprintf(
|
||||
"%s (%s AS%d %s)",
|
||||
pin.Hub.Info.IPv4.String(),
|
||||
pin.LocationV4.Country.Code,
|
||||
pin.LocationV4.AutonomousSystemNumber,
|
||||
pin.LocationV4.AutonomousSystemOrganization,
|
||||
)
|
||||
} else {
|
||||
v4Info = pin.Hub.Info.IPv4.String()
|
||||
}
|
||||
}
|
||||
if pin.Hub.Info.IPv6 != nil {
|
||||
if pin.LocationV6 != nil {
|
||||
v6Info = fmt.Sprintf(
|
||||
"%s (%s AS%d %s)",
|
||||
pin.Hub.Info.IPv6.String(),
|
||||
pin.LocationV6.Country.Code,
|
||||
pin.LocationV6.AutonomousSystemNumber,
|
||||
pin.LocationV6.AutonomousSystemOrganization,
|
||||
)
|
||||
} else {
|
||||
v6Info = pin.Hub.Info.IPv6.String()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`"ID: %s
|
||||
States: %s
|
||||
Version: %s
|
||||
IPv4: %s
|
||||
IPv6: %s
|
||||
Load: %d
|
||||
Cost: %.2f"`,
|
||||
pin.Hub.ID,
|
||||
pin.State,
|
||||
pin.Hub.Status.Version,
|
||||
v4Info,
|
||||
v6Info,
|
||||
pin.Hub.Status.Load,
|
||||
pin.Cost,
|
||||
)
|
||||
}
|
||||
|
||||
func graphEdgeTooltip(from, to *Pin, lane *Lane) string {
|
||||
return fmt.Sprintf(
|
||||
`"%s <> %s
|
||||
Latency: %s
|
||||
Capacity: %.2f Mbit/s
|
||||
Cost: %.2f"`,
|
||||
from.Hub.Info.Name, to.Hub.Info.Name,
|
||||
lane.Latency,
|
||||
float64(lane.Capacity)/1000000,
|
||||
lane.Cost,
|
||||
)
|
||||
}
|
||||
|
||||
// Graphviz colors.
|
||||
// See https://graphviz.org/doc/info/colors.html
|
||||
const (
|
||||
graphColorWarning = "orange2"
|
||||
graphColorError = "red2"
|
||||
graphColorHomeAndConnected = "steelblue2"
|
||||
graphColorDisregard = "tomato2"
|
||||
graphColorNotRegard = "tan2"
|
||||
graphColorTrusted = "seagreen2"
|
||||
graphColorDefaultNode = "seashell2"
|
||||
graphColorDefaultEdge = "black"
|
||||
graphColorNone = "transparent"
|
||||
)
|
||||
|
||||
func graphNodeColor(pin *Pin) string {
|
||||
switch {
|
||||
case pin.State == StateNone:
|
||||
return graphColorNone
|
||||
case pin.Hub.Status.Load >= 95:
|
||||
return graphColorError
|
||||
case pin.Hub.Status.Load >= 80:
|
||||
return graphColorWarning
|
||||
case pin.State.Has(StateIsHomeHub):
|
||||
return graphColorHomeAndConnected
|
||||
case pin.State.HasAnyOf(StateSummaryDisregard):
|
||||
return graphColorDisregard
|
||||
case !pin.State.Has(StateSummaryRegard):
|
||||
return graphColorNotRegard
|
||||
case pin.State.Has(StateTrusted):
|
||||
return graphColorTrusted
|
||||
default:
|
||||
return graphColorDefaultNode
|
||||
}
|
||||
}
|
||||
|
||||
func graphNodeBorderColor(pin *Pin) string {
|
||||
switch {
|
||||
case pin.HasActiveTerminal():
|
||||
return graphColorHomeAndConnected
|
||||
default:
|
||||
return graphColorNone
|
||||
}
|
||||
}
|
||||
|
||||
func graphEdgeColor(from, to *Pin, lane *Lane) string {
|
||||
// Check lane stats.
|
||||
if lane.Capacity == 0 || lane.Latency == 0 {
|
||||
return graphColorWarning
|
||||
}
|
||||
// Alert if capacity is under 10Mbit/s or latency is over 100ms.
|
||||
if lane.Capacity < 10000000 || lane.Latency > 100*time.Millisecond {
|
||||
return graphColorError
|
||||
}
|
||||
|
||||
// Check for active edge forward.
|
||||
if to.HasActiveTerminal() && len(to.Connection.Route.Path) >= 2 {
|
||||
secondLastHopIndex := len(to.Connection.Route.Path) - 2
|
||||
if to.Connection.Route.Path[secondLastHopIndex].HubID == from.Hub.ID {
|
||||
return graphColorHomeAndConnected
|
||||
}
|
||||
}
|
||||
// Check for active edge backward.
|
||||
if from.HasActiveTerminal() && len(from.Connection.Route.Path) >= 2 {
|
||||
secondLastHopIndex := len(from.Connection.Route.Path) - 2
|
||||
if from.Connection.Route.Path[secondLastHopIndex].HubID == to.Hub.ID {
|
||||
return graphColorHomeAndConnected
|
||||
}
|
||||
}
|
||||
|
||||
// Return default color if edge is not active.
|
||||
return graphColorDefaultEdge
|
||||
}
|
||||
396
spn/navigator/api_route.go
Normal file
396
spn/navigator/api_route.go
Normal file
@@ -0,0 +1,396 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/profile/endpoints"
|
||||
)
|
||||
|
||||
func registerRouteAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/route/to/{destination:[a-z0-9_\.:-]{1,255}}`,
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
ActionFunc: handleRouteCalculationRequest,
|
||||
Name: "Calculate Route through SPN",
|
||||
Description: "Returns a textual representation of the routing process.",
|
||||
Parameters: []api.Parameter{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Field: "profile",
|
||||
Value: "<id>|global",
|
||||
Description: "Specify a profile ID to load more settings for simulation.",
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Field: "encrypted",
|
||||
Value: "true",
|
||||
Description: "Specify to signify that the simulated connection should be regarded as encrypted. Only valid with a profile.",
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleRouteCalculationRequest(ar *api.Request) (msg string, err error) { //nolint:maintidx
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(ar.URLVars["map"])
|
||||
if !ok {
|
||||
return "", errors.New("map not found")
|
||||
}
|
||||
// Get profile ID.
|
||||
profileID := ar.Request.URL.Query().Get("profile")
|
||||
|
||||
// Parse destination and prepare options.
|
||||
entity := &intel.Entity{}
|
||||
destination := ar.URLVars["destination"]
|
||||
matchFor := DestinationHub
|
||||
var (
|
||||
introText string
|
||||
locationV4, locationV6 *geoip.Location
|
||||
opts *Options
|
||||
)
|
||||
switch {
|
||||
case destination == "":
|
||||
// Destination is required.
|
||||
return "", errors.New("no destination provided")
|
||||
|
||||
case destination == "home":
|
||||
if profileID != "" {
|
||||
return "", errors.New("cannot apply profile to home hub route")
|
||||
}
|
||||
// Simulate finding home hub.
|
||||
locations, ok := netenv.GetInternetLocation()
|
||||
if !ok || len(locations.All) == 0 {
|
||||
return "", errors.New("failed to locate own device for finding home hub")
|
||||
}
|
||||
introText = fmt.Sprintf("looking for home hub near %s and %s", locations.BestV4(), locations.BestV6())
|
||||
locationV4 = locations.BestV4().LocationOrNil()
|
||||
locationV6 = locations.BestV6().LocationOrNil()
|
||||
matchFor = HomeHub
|
||||
|
||||
// START of copied from captain/navigation.go
|
||||
|
||||
// Get own entity.
|
||||
// Checking the entity against the entry policies is somewhat hit and miss
|
||||
// anyway, as the device location is an approximation.
|
||||
var myEntity *intel.Entity
|
||||
if dl := locations.BestV4(); dl != nil && dl.IP != nil {
|
||||
myEntity = (&intel.Entity{IP: dl.IP}).Init(0)
|
||||
myEntity.FetchData(ar.Context())
|
||||
} else if dl := locations.BestV6(); dl != nil && dl.IP != nil {
|
||||
myEntity = (&intel.Entity{IP: dl.IP}).Init(0)
|
||||
myEntity.FetchData(ar.Context())
|
||||
}
|
||||
|
||||
// Build navigation options for searching for a home hub.
|
||||
homePolicy, err := endpoints.ParseEndpoints(config.GetAsStringArray("spn/homePolicy", []string{})())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse home hub policy: %w", err)
|
||||
}
|
||||
|
||||
opts = &Options{
|
||||
Home: &HomeHubOptions{
|
||||
HubPolicies: []endpoints.Endpoints{homePolicy},
|
||||
CheckHubPolicyWith: myEntity,
|
||||
},
|
||||
}
|
||||
|
||||
// Add requirement to only use Safing nodes when not using community nodes.
|
||||
if !config.GetAsBool("spn/useCommunityNodes", true)() {
|
||||
opts.Home.RequireVerifiedOwners = []string{"Safing"}
|
||||
}
|
||||
|
||||
// Require a trusted home node when the routing profile requires less than two hops.
|
||||
routingProfile := GetRoutingProfile(config.GetAsString(profile.CfgOptionRoutingAlgorithmKey, DefaultRoutingProfileID)())
|
||||
if routingProfile.MinHops < 2 {
|
||||
opts.Home.Regard = opts.Home.Regard.Add(StateTrusted)
|
||||
}
|
||||
|
||||
// END of copied
|
||||
|
||||
case net.ParseIP(destination) != nil:
|
||||
entity.IP = net.ParseIP(destination)
|
||||
|
||||
fallthrough
|
||||
case netutils.IsValidFqdn(destination):
|
||||
fallthrough
|
||||
case netutils.IsValidFqdn(destination + "."):
|
||||
// Resolve domain to IP, if not inherired from a previous case.
|
||||
var ignoredIPs int
|
||||
if entity.IP == nil {
|
||||
entity.Domain = destination
|
||||
|
||||
// Resolve name to IPs.
|
||||
ips, err := net.DefaultResolver.LookupIP(ar.Context(), "ip", destination)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to lookup IP address of %s: %w", destination, err)
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return "", fmt.Errorf("failed to lookup IP address of %s: no result", destination)
|
||||
}
|
||||
|
||||
// Shuffle IPs.
|
||||
if len(ips) >= 2 {
|
||||
mr := mrand.New(mrand.NewSource(time.Now().UnixNano())) //nolint:gosec
|
||||
mr.Shuffle(len(ips), func(i, j int) {
|
||||
ips[i], ips[j] = ips[j], ips[i]
|
||||
})
|
||||
}
|
||||
|
||||
entity.IP = ips[0]
|
||||
ignoredIPs = len(ips) - 1
|
||||
}
|
||||
entity.Init(0)
|
||||
|
||||
// Get location of IP.
|
||||
location, ok := entity.GetLocation(ar.Context())
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to get geoip location for %s: %s", entity.IP, entity.LocationError)
|
||||
}
|
||||
// Assign location to separate variables.
|
||||
if entity.IP.To4() != nil {
|
||||
locationV4 = location
|
||||
} else {
|
||||
locationV6 = location
|
||||
}
|
||||
|
||||
// Set intro text.
|
||||
if entity.Domain != "" {
|
||||
introText = fmt.Sprintf("looking for route to %s at %s\n(ignoring %d additional IPs returned by DNS)", entity.IP, formatLocation(location), ignoredIPs)
|
||||
} else {
|
||||
introText = fmt.Sprintf("looking for route to %s at %s", entity.IP, formatLocation(location))
|
||||
}
|
||||
|
||||
// Get profile.
|
||||
if profileID != "" {
|
||||
var lp *profile.LayeredProfile
|
||||
if profileID == "global" {
|
||||
// Create new empty profile for easy access to global settings.
|
||||
lp = profile.NewLayeredProfile(profile.New(nil))
|
||||
} else {
|
||||
// Get local profile by ID.
|
||||
localProfile, err := profile.GetLocalProfile(profileID, nil, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get profile: %w", err)
|
||||
}
|
||||
lp = localProfile.LayeredProfile()
|
||||
}
|
||||
opts = DeriveTunnelOptions(
|
||||
lp,
|
||||
entity,
|
||||
ar.Request.URL.Query().Has("encrypted"),
|
||||
)
|
||||
} else {
|
||||
opts = m.defaultOptions()
|
||||
}
|
||||
|
||||
default:
|
||||
return "", errors.New("invalid destination provided")
|
||||
}
|
||||
|
||||
// Finalize entity.
|
||||
entity.Init(0)
|
||||
|
||||
// Start formatting output.
|
||||
lines := []string{
|
||||
"Routing simulation: " + introText,
|
||||
"Please note that this routing simulation does match the behavior of regular routing to 100%.",
|
||||
"",
|
||||
}
|
||||
|
||||
// Print options.
|
||||
// ==================
|
||||
|
||||
lines = append(lines, "Routing Options:")
|
||||
lines = append(lines, "Algorithm: "+opts.RoutingProfile)
|
||||
if opts.Home != nil {
|
||||
lines = append(lines, "Home Options:")
|
||||
lines = append(lines, fmt.Sprintf(" Regard: %s", opts.Home.Regard))
|
||||
lines = append(lines, fmt.Sprintf(" Disregard: %s", opts.Home.Disregard))
|
||||
lines = append(lines, fmt.Sprintf(" No Default: %v", opts.Home.NoDefaults))
|
||||
lines = append(lines, fmt.Sprintf(" Hub Policies: %v", opts.Home.HubPolicies))
|
||||
lines = append(lines, fmt.Sprintf(" Require Verified Owners: %v", opts.Home.RequireVerifiedOwners))
|
||||
}
|
||||
if opts.Transit != nil {
|
||||
lines = append(lines, "Transit Options:")
|
||||
lines = append(lines, fmt.Sprintf(" Regard: %s", opts.Transit.Regard))
|
||||
lines = append(lines, fmt.Sprintf(" Disregard: %s", opts.Transit.Disregard))
|
||||
lines = append(lines, fmt.Sprintf(" No Default: %v", opts.Transit.NoDefaults))
|
||||
lines = append(lines, fmt.Sprintf(" Hub Policies: %v", opts.Transit.HubPolicies))
|
||||
lines = append(lines, fmt.Sprintf(" Require Verified Owners: %v", opts.Transit.RequireVerifiedOwners))
|
||||
}
|
||||
if opts.Destination != nil {
|
||||
lines = append(lines, "Destination Options:")
|
||||
lines = append(lines, fmt.Sprintf(" Regard: %s", opts.Destination.Regard))
|
||||
lines = append(lines, fmt.Sprintf(" Disregard: %s", opts.Destination.Disregard))
|
||||
lines = append(lines, fmt.Sprintf(" No Default: %v", opts.Destination.NoDefaults))
|
||||
lines = append(lines, fmt.Sprintf(" Hub Policies: %v", opts.Destination.HubPolicies))
|
||||
lines = append(lines, fmt.Sprintf(" Require Verified Owners: %v", opts.Destination.RequireVerifiedOwners))
|
||||
if opts.Destination.CheckHubPolicyWith != nil {
|
||||
lines = append(lines, " Check Hub Policy With:")
|
||||
if opts.Destination.CheckHubPolicyWith.Domain != "" {
|
||||
lines = append(lines, fmt.Sprintf(" Domain: %v", opts.Destination.CheckHubPolicyWith.Domain))
|
||||
}
|
||||
if opts.Destination.CheckHubPolicyWith.IP != nil {
|
||||
lines = append(lines, fmt.Sprintf(" IP: %v", opts.Destination.CheckHubPolicyWith.IP))
|
||||
}
|
||||
if opts.Destination.CheckHubPolicyWith.Port != 0 {
|
||||
lines = append(lines, fmt.Sprintf(" Port: %v", opts.Destination.CheckHubPolicyWith.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
lines = append(lines, "\n")
|
||||
|
||||
// Find nearest hubs.
|
||||
// ==================
|
||||
|
||||
// Start operating in map.
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
// Check if map is populated.
|
||||
if m.isEmpty() {
|
||||
return "", ErrEmptyMap
|
||||
}
|
||||
|
||||
// Find nearest hubs.
|
||||
nbPins, err := m.findNearestPins(locationV4, locationV6, opts, matchFor, true)
|
||||
if err != nil {
|
||||
lines = append(lines, fmt.Sprintf("FAILED to find any suitable exit hub: %s", err))
|
||||
return strings.Join(lines, "\n"), nil
|
||||
// return "", fmt.Errorf("failed to search for nearby pins: %w", err)
|
||||
}
|
||||
|
||||
// Print found exits to table.
|
||||
lines = append(lines, "Considered Exits (cheapest 10% are shuffled)")
|
||||
buf := bytes.NewBuffer(nil)
|
||||
tabWriter := tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0)
|
||||
fmt.Fprint(tabWriter, "Hub Name\tCost\tLocation\n")
|
||||
for _, nbPin := range nbPins.pins {
|
||||
fmt.Fprintf(tabWriter,
|
||||
"%s\t%.0f\t%s\n",
|
||||
nbPin.pin.Hub.Name(),
|
||||
nbPin.cost,
|
||||
formatMultiLocation(nbPin.pin.LocationV4, nbPin.pin.LocationV6),
|
||||
)
|
||||
}
|
||||
_ = tabWriter.Flush()
|
||||
lines = append(lines, buf.String())
|
||||
|
||||
// Print too expensive exits to table.
|
||||
lines = append(lines, "Too Expensive Exits:")
|
||||
buf = bytes.NewBuffer(nil)
|
||||
tabWriter = tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0)
|
||||
fmt.Fprint(tabWriter, "Hub Name\tCost\tLocation\n")
|
||||
for _, nbPin := range nbPins.debug.tooExpensive {
|
||||
fmt.Fprintf(tabWriter,
|
||||
"%s\t%.0f\t%s\n",
|
||||
nbPin.pin.Hub.Name(),
|
||||
nbPin.cost,
|
||||
formatMultiLocation(nbPin.pin.LocationV4, nbPin.pin.LocationV6),
|
||||
)
|
||||
}
|
||||
_ = tabWriter.Flush()
|
||||
lines = append(lines, buf.String())
|
||||
|
||||
// Print disregarded exits to table.
|
||||
lines = append(lines, "Disregarded Exits:")
|
||||
buf = bytes.NewBuffer(nil)
|
||||
tabWriter = tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0)
|
||||
fmt.Fprint(tabWriter, "Hub Name\tReason\tStates\n")
|
||||
for _, nbPin := range nbPins.debug.disregarded {
|
||||
fmt.Fprintf(tabWriter,
|
||||
"%s\t%s\t%s\n",
|
||||
nbPin.pin.Hub.Name(),
|
||||
nbPin.reason,
|
||||
nbPin.pin.State,
|
||||
)
|
||||
}
|
||||
_ = tabWriter.Flush()
|
||||
lines = append(lines, buf.String())
|
||||
|
||||
// Find routes.
|
||||
// ============
|
||||
|
||||
// Unless we looked for a home node.
|
||||
if destination == "home" {
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
// Find routes.
|
||||
routes, err := m.findRoutes(nbPins, opts)
|
||||
if err != nil {
|
||||
lines = append(lines, fmt.Sprintf("FAILED to find routes: %s", err))
|
||||
return strings.Join(lines, "\n"), nil
|
||||
// return "", fmt.Errorf("failed to find routes: %w", err)
|
||||
}
|
||||
|
||||
// Print found routes to table.
|
||||
lines = append(lines, "Considered Routes (cheapest 10% are shuffled)")
|
||||
buf = bytes.NewBuffer(nil)
|
||||
tabWriter = tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0)
|
||||
fmt.Fprint(tabWriter, "Cost\tPath\n")
|
||||
for _, route := range routes.All {
|
||||
fmt.Fprintf(tabWriter,
|
||||
"%.0f\t%s\n",
|
||||
route.TotalCost,
|
||||
formatRoute(route, entity.IP),
|
||||
)
|
||||
}
|
||||
_ = tabWriter.Flush()
|
||||
lines = append(lines, buf.String())
|
||||
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func formatLocation(loc *geoip.Location) string {
|
||||
return fmt.Sprintf(
|
||||
"%s (%s - AS%d %s)",
|
||||
loc.Country.Name,
|
||||
loc.Country.Code,
|
||||
loc.AutonomousSystemNumber,
|
||||
loc.AutonomousSystemOrganization,
|
||||
)
|
||||
}
|
||||
|
||||
func formatMultiLocation(a, b *geoip.Location) string {
|
||||
switch {
|
||||
case a != nil:
|
||||
return formatLocation(a)
|
||||
case b != nil:
|
||||
return formatLocation(b)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func formatRoute(r *Route, dst net.IP) string {
|
||||
s := make([]string, 0, len(r.Path)+1)
|
||||
for i, hop := range r.Path {
|
||||
if i == 0 {
|
||||
s = append(s, hop.pin.Hub.Name())
|
||||
} else {
|
||||
s = append(s, fmt.Sprintf(">> %.2fc >> %s", hop.Cost, hop.pin.Hub.Name()))
|
||||
}
|
||||
}
|
||||
s = append(s, fmt.Sprintf(">> %.2fc >> %s", r.DstCost, dst))
|
||||
return strings.Join(s, " ")
|
||||
}
|
||||
72
spn/navigator/costs.go
Normal file
72
spn/navigator/costs.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package navigator
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
nearestPinsMaxCostDifference = 5000
|
||||
nearestPinsMinimum = 10
|
||||
)
|
||||
|
||||
// CalculateLaneCost calculates the cost of using a Lane based on the given
|
||||
// Lane latency and capacity.
|
||||
// Ranges from 0 to 10000.
|
||||
func CalculateLaneCost(latency time.Duration, capacity int) (cost float32) {
|
||||
// - One point for every ms in latency (linear)
|
||||
if latency != 0 {
|
||||
cost += float32(latency) / float32(time.Millisecond)
|
||||
} else {
|
||||
// Add cautious default cost if latency is not available.
|
||||
cost += 1000
|
||||
}
|
||||
|
||||
capacityFloat := float32(capacity)
|
||||
switch {
|
||||
case capacityFloat == 0:
|
||||
// Add cautious default cost if capacity is not available.
|
||||
cost += 4000
|
||||
case capacityFloat < cap1Mbit:
|
||||
// - Between 1000 and 10000 points for ranges below 1Mbit/s
|
||||
cost += 1000 + 9000*((cap1Mbit-capacityFloat)/cap1Mbit)
|
||||
case capacityFloat < cap10Mbit:
|
||||
// - Between 100 and 1000 points for ranges below 10Mbit/s
|
||||
cost += 100 + 900*((cap10Mbit-capacityFloat)/cap10Mbit)
|
||||
case capacityFloat < cap100Mbit:
|
||||
// - Between 20 and 100 points for ranges below 100Mbit/s
|
||||
cost += 20 + 80*((cap100Mbit-capacityFloat)/cap100Mbit)
|
||||
case capacityFloat < cap1Gbit:
|
||||
// - Between 5 and 20 points for ranges below 1Gbit/s
|
||||
cost += 5 + 15*((cap1Gbit-capacityFloat)/cap1Gbit)
|
||||
case capacityFloat < cap10Gbit:
|
||||
// - Between 0 and 5 points for ranges below 10Gbit/s
|
||||
cost += 5 * ((cap10Gbit - float32(capacity)) / cap10Gbit)
|
||||
}
|
||||
|
||||
return cost
|
||||
}
|
||||
|
||||
// CalculateHubCost calculates the cost of using a Hub based on the given Hub load.
|
||||
// Ranges from 100 to 10000.
|
||||
func CalculateHubCost(load int) (cost float32) {
|
||||
switch {
|
||||
case load >= 100:
|
||||
return 10000
|
||||
case load >= 95:
|
||||
return 1000
|
||||
case load >= 80:
|
||||
return 500
|
||||
default:
|
||||
return 100
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateDestinationCost calculates the cost of a destination hub to a
|
||||
// destination server based on the given proximity.
|
||||
// Ranges from 0 to 2500.
|
||||
func CalculateDestinationCost(proximity float32) (cost float32) {
|
||||
// Invert from proximity (0-100) to get a distance value.
|
||||
distance := 100 - proximity
|
||||
|
||||
// Take the distance to the power of three and then divide by hundred in order to
|
||||
// make high distances exponentially more expensive.
|
||||
return (distance * distance * distance) / 100
|
||||
}
|
||||
164
spn/navigator/database.go
Normal file
164
spn/navigator/database.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/iterator"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/database/storage"
|
||||
)
|
||||
|
||||
var mapDBController *database.Controller
|
||||
|
||||
// StorageInterface provices a storage.Interface to the
|
||||
// configuration manager.
|
||||
type StorageInterface struct {
|
||||
storage.InjectBase
|
||||
}
|
||||
|
||||
// Database prefixes:
|
||||
// Pins: map:main/<Hub ID>
|
||||
// DNS Requests: network:tree/<PID>/dns/<ID>
|
||||
// IP Connections: network:tree/<PID>/ip/<ID>
|
||||
|
||||
func makeDBKey(mapName, hubID string) string {
|
||||
return fmt.Sprintf("map:%s/%s", mapName, hubID)
|
||||
}
|
||||
|
||||
func parseDBKey(key string) (mapName, hubID string) {
|
||||
// Split into segments.
|
||||
segments := strings.Split(key, "/")
|
||||
|
||||
// Keys have 1 or 2 segments.
|
||||
switch len(segments) {
|
||||
case 1:
|
||||
return segments[0], ""
|
||||
case 2:
|
||||
return segments[0], segments[1]
|
||||
default:
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns a database record.
|
||||
func (s *StorageInterface) Get(key string) (record.Record, error) {
|
||||
// Parse key and check if valid.
|
||||
mapName, hubID := parseDBKey(key)
|
||||
if mapName == "" || hubID == "" {
|
||||
return nil, storage.ErrNotFound
|
||||
}
|
||||
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(mapName)
|
||||
if !ok {
|
||||
return nil, storage.ErrNotFound
|
||||
}
|
||||
|
||||
// Get Pin from map.
|
||||
pin, ok := m.GetPin(hubID)
|
||||
if !ok {
|
||||
return nil, storage.ErrNotFound
|
||||
}
|
||||
return pin.Export(), nil
|
||||
}
|
||||
|
||||
// Query returns a an iterator for the supplied query.
|
||||
func (s *StorageInterface) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) {
|
||||
// Parse key and check if valid.
|
||||
mapName, _ := parseDBKey(q.DatabaseKeyPrefix())
|
||||
if mapName == "" {
|
||||
return nil, storage.ErrNotFound
|
||||
}
|
||||
|
||||
// Get map.
|
||||
m, ok := getMapForAPI(mapName)
|
||||
if !ok {
|
||||
return nil, storage.ErrNotFound
|
||||
}
|
||||
|
||||
// Start query worker.
|
||||
it := iterator.New()
|
||||
module.StartWorker("map query", func(_ context.Context) error {
|
||||
s.processQuery(m, q, it)
|
||||
return nil
|
||||
})
|
||||
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (s *StorageInterface) processQuery(m *Map, q *query.Query, it *iterator.Iterator) {
|
||||
// Return all matching pins.
|
||||
for _, pin := range m.sortedPins(true) {
|
||||
export := pin.Export()
|
||||
if q.Matches(export) {
|
||||
select {
|
||||
case it.Next <- export:
|
||||
case <-it.Done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it.Finish(nil)
|
||||
}
|
||||
|
||||
func registerMapDatabase() error {
|
||||
_, err := database.Register(&database.Database{
|
||||
Name: "map",
|
||||
Description: "SPN Network Maps",
|
||||
StorageType: database.StorageTypeInjected,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
controller, err := database.InjectDatabase("map", &StorageInterface{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mapDBController = controller
|
||||
return nil
|
||||
}
|
||||
|
||||
func withdrawMapDatabase() {
|
||||
mapDBController.Withdraw()
|
||||
}
|
||||
|
||||
// PushPinChanges pushes all changed pins to subscribers.
|
||||
func (m *Map) PushPinChanges() {
|
||||
module.StartWorker("push pin changes", m.pushPinChangesWorker)
|
||||
}
|
||||
|
||||
func (m *Map) pushPinChangesWorker(ctx context.Context) error {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
for _, pin := range m.all {
|
||||
if pin.pushChanges.SetToIf(true, false) {
|
||||
mapDBController.PushUpdate(pin.Export())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushChange pushes changes of the pin, if the pushChanges flag is set.
|
||||
func (pin *Pin) pushChange() {
|
||||
// Check before starting the worker.
|
||||
if pin.pushChanges.IsNotSet() {
|
||||
return
|
||||
}
|
||||
|
||||
// Start worker to push changes.
|
||||
module.StartWorker("push pin change", func(ctx context.Context) error {
|
||||
if pin.pushChanges.SetToIf(true, false) {
|
||||
mapDBController.PushUpdate(pin.Export())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
441
spn/navigator/findnearest.go
Normal file
441
spn/navigator/findnearest.go
Normal file
@@ -0,0 +1,441 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
124
spn/navigator/findnearest_test.go
Normal file
124
spn/navigator/findnearest_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindNearest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create map and lock faking in order to guarantee reproducability of faked data.
|
||||
m := getDefaultTestMap()
|
||||
fakeLock.Lock()
|
||||
defer fakeLock.Unlock()
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
// Create a random destination address
|
||||
ip4, loc4 := createGoodIP(true)
|
||||
|
||||
nbPins, err := m.findNearestPins(loc4, nil, m.DefaultOptions(), DestinationHub, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
t.Logf("Pins near %s: %s", ip4, nbPins)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
// Create a random destination address
|
||||
ip6, loc6 := createGoodIP(true)
|
||||
|
||||
nbPins, err := m.findNearestPins(nil, loc6, m.DefaultOptions(), DestinationHub, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
t.Logf("Pins near %s: %s", ip6, nbPins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Find a way to quickly generate good geoip data on the fly, as we don't want to measure IP address generation, but only finding the nearest pins.
|
||||
|
||||
func BenchmarkFindNearest(b *testing.B) {
|
||||
// Create map and lock faking in order to guarantee reproducability of faked data.
|
||||
m := getDefaultTestMap()
|
||||
fakeLock.Lock()
|
||||
defer fakeLock.Unlock()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Create a random destination address
|
||||
var dstIP net.IP
|
||||
if i%2 == 0 {
|
||||
dstIP = net.ParseIP(gofakeit.IPv4Address())
|
||||
} else {
|
||||
dstIP = net.ParseIP(gofakeit.IPv6Address())
|
||||
}
|
||||
|
||||
_, err := m.findNearestPins(dstIP, m.DefaultOptions(),DestinationHub if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func findFakeHomeHub(m *Map) {
|
||||
// Create fake IP address.
|
||||
_, loc4 := createGoodIP(true)
|
||||
_, loc6 := createGoodIP(false)
|
||||
|
||||
nbPins, err := m.findNearestPins(loc4, loc6, m.defaultOptions(), HomeHub, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(nbPins.pins) == 0 {
|
||||
panic("could not find a Home Hub")
|
||||
}
|
||||
|
||||
// Set Home.
|
||||
m.home = nbPins.pins[0].pin
|
||||
|
||||
// Recalculate reachability.
|
||||
if err := m.recalculateReachableHubs(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearbyPinsCleaning(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCleaning(t, []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}, 3)
|
||||
testCleaning(t, []float32{10, 11, 12, 13, 50, 60, 70, 80, 90, 100}, 4)
|
||||
testCleaning(t, []float32{10, 11, 12, 40, 50, 60, 70, 80, 90, 100}, 3)
|
||||
testCleaning(t, []float32{10, 11, 30, 40, 50, 60, 70, 80, 90, 100}, 3)
|
||||
}
|
||||
|
||||
func testCleaning(t *testing.T, costs []float32, expectedLeftOver int) {
|
||||
t.Helper()
|
||||
|
||||
nb := &nearbyPins{
|
||||
minPins: 3,
|
||||
maxPins: 5,
|
||||
cutOffLimit: 10,
|
||||
}
|
||||
|
||||
// Simulate usage.
|
||||
for _, cost := range costs {
|
||||
// Add to list.
|
||||
nb.add(nil, cost)
|
||||
|
||||
// Clean once in a while.
|
||||
if len(nb.pins) > nb.maxPins {
|
||||
nb.clean()
|
||||
}
|
||||
}
|
||||
// Final clean.
|
||||
nb.clean()
|
||||
|
||||
// Check results.
|
||||
t.Logf("result: %+v", nb.pins)
|
||||
if len(nb.pins) != expectedLeftOver {
|
||||
t.Errorf("unexpected amount of left over pins: %+v", nb.pins)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
54
spn/navigator/findroutes_test.go
Normal file
54
spn/navigator/findroutes_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindRoutes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create map and lock faking in order to guarantee reproducability of faked data.
|
||||
m := getOptimizedDefaultTestMap(t)
|
||||
fakeLock.Lock()
|
||||
defer fakeLock.Unlock()
|
||||
|
||||
for i := 0; i < 1; i++ {
|
||||
// Create a random destination address
|
||||
dstIP, _ := createGoodIP(i%2 == 0)
|
||||
|
||||
routes, err := m.FindRoutes(dstIP, m.DefaultOptions())
|
||||
switch {
|
||||
case err != nil:
|
||||
t.Error(err)
|
||||
case len(routes.All) == 0:
|
||||
t.Logf("No routes for %s", dstIP)
|
||||
default:
|
||||
t.Logf("Best route for %s: %s", dstIP, routes.All[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFindRoutes(b *testing.B) {
|
||||
// Create map and lock faking in order to guarantee reproducability of faked data.
|
||||
m := getOptimizedDefaultTestMap(nil)
|
||||
fakeLock.Lock()
|
||||
defer fakeLock.Unlock()
|
||||
|
||||
// Pre-generate 100 IPs
|
||||
preGenIPs := make([]net.IP, 0, 100)
|
||||
for i := 0; i < cap(preGenIPs); i++ {
|
||||
ip, _ := createGoodIP(i%2 == 0)
|
||||
preGenIPs = append(preGenIPs, ip)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
routes, err := m.FindRoutes(preGenIPs[i%len(preGenIPs)], m.DefaultOptions())
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
} else {
|
||||
b.Logf("Best route for %s: %s", preGenIPs[i%len(preGenIPs)], routes.All[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
222
spn/navigator/intel.go
Normal file
222
spn/navigator/intel.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/service/profile/endpoints"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
// UpdateIntel supplies the map with new intel data. The data is not copied, so
|
||||
// it must not be modified after being supplied. If the map is empty, the
|
||||
// bootstrap hubs will be added to the map.
|
||||
func (m *Map) UpdateIntel(update *hub.Intel, trustNodes []string) error {
|
||||
// Check if intel data is already parsed.
|
||||
if update.Parsed() == nil {
|
||||
return errors.New("intel data is not parsed")
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
// Update the map's reference to the intel data.
|
||||
m.intel = update
|
||||
|
||||
// Update pins with new intel data.
|
||||
for _, pin := range m.all {
|
||||
// Add/Update location data from IP addresses.
|
||||
pin.updateLocationData()
|
||||
|
||||
// Override Pin Data.
|
||||
m.updateInfoOverrides(pin)
|
||||
|
||||
// Update Trust and Advisory Statuses.
|
||||
m.updateIntelStatuses(pin, trustNodes)
|
||||
|
||||
// Push changes.
|
||||
// TODO: Only set when pin changed.
|
||||
pin.pushChanges.Set()
|
||||
}
|
||||
|
||||
// Configure the map's regions.
|
||||
m.updateRegions(m.intel.Regions)
|
||||
|
||||
// Push pin changes.
|
||||
m.PushPinChanges()
|
||||
|
||||
log.Infof("spn/navigator: updated intel on map %s", m.Name)
|
||||
|
||||
// Add bootstrap hubs if map is empty.
|
||||
if m.isEmpty() {
|
||||
return m.addBootstrapHubs(m.intel.BootstrapHubs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIntel returns the map's intel data.
|
||||
func (m *Map) GetIntel() *hub.Intel {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
return m.intel
|
||||
}
|
||||
|
||||
func (m *Map) updateIntelStatuses(pin *Pin, trustNodes []string) {
|
||||
// Reset all related states.
|
||||
pin.removeStates(StateTrusted | StateUsageDiscouraged | StateUsageAsHomeDiscouraged | StateUsageAsDestinationDiscouraged)
|
||||
|
||||
// Check if Intel data is loaded.
|
||||
if m.intel == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check Hub Intel
|
||||
hubIntel, ok := m.intel.Hubs[pin.Hub.ID]
|
||||
if ok {
|
||||
// Apply the verified owner, if any.
|
||||
pin.VerifiedOwner = hubIntel.VerifiedOwner
|
||||
|
||||
// Check if Hub is discontinued.
|
||||
if hubIntel.Discontinued {
|
||||
// Reset state, set offline and return.
|
||||
pin.State = StateNone
|
||||
pin.addStates(StateOffline)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if Hub is trusted.
|
||||
if hubIntel.Trusted {
|
||||
pin.addStates(StateTrusted)
|
||||
}
|
||||
}
|
||||
|
||||
// Check manual trust status.
|
||||
switch {
|
||||
case slices.Contains[[]string, string](trustNodes, pin.VerifiedOwner):
|
||||
pin.addStates(StateTrusted)
|
||||
case slices.Contains[[]string, string](trustNodes, pin.Hub.ID):
|
||||
pin.addStates(StateTrusted)
|
||||
}
|
||||
|
||||
// Check advisories.
|
||||
// Check for UsageDiscouraged.
|
||||
checkStatusList(
|
||||
pin,
|
||||
StateUsageDiscouraged,
|
||||
m.intel.AdviseOnlyTrustedHubs,
|
||||
m.intel.Parsed().HubAdvisory,
|
||||
)
|
||||
// Check for UsageAsHomeDiscouraged.
|
||||
checkStatusList(
|
||||
pin,
|
||||
StateUsageAsHomeDiscouraged,
|
||||
m.intel.AdviseOnlyTrustedHomeHubs,
|
||||
m.intel.Parsed().HomeHubAdvisory,
|
||||
)
|
||||
// Check for UsageAsDestinationDiscouraged.
|
||||
checkStatusList(
|
||||
pin,
|
||||
StateUsageAsDestinationDiscouraged,
|
||||
m.intel.AdviseOnlyTrustedDestinationHubs,
|
||||
m.intel.Parsed().DestinationHubAdvisory,
|
||||
)
|
||||
}
|
||||
|
||||
func checkStatusList(pin *Pin, state PinState, requireTrusted bool, endpointList endpoints.Endpoints) {
|
||||
if requireTrusted && !pin.State.Has(StateTrusted) {
|
||||
pin.addStates(state)
|
||||
return
|
||||
}
|
||||
|
||||
if pin.EntityV4 != nil {
|
||||
result, _ := endpointList.Match(context.TODO(), pin.EntityV4)
|
||||
if result == endpoints.Denied {
|
||||
pin.addStates(state)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if pin.EntityV6 != nil {
|
||||
result, _ := endpointList.Match(context.TODO(), pin.EntityV6)
|
||||
if result == endpoints.Denied {
|
||||
pin.addStates(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) updateInfoOverrides(pin *Pin) {
|
||||
// Check if Intel data is loaded and if there are any overrides.
|
||||
if m.intel == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get overrides for this pin.
|
||||
hubIntel, ok := m.intel.Hubs[pin.Hub.ID]
|
||||
if !ok || hubIntel.Override == nil {
|
||||
return
|
||||
}
|
||||
overrides := hubIntel.Override
|
||||
|
||||
// Apply overrides
|
||||
if overrides.CountryCode != "" {
|
||||
if pin.LocationV4 != nil {
|
||||
pin.LocationV4.Country = geoip.GetCountryInfo(overrides.CountryCode)
|
||||
}
|
||||
if pin.EntityV4 != nil {
|
||||
pin.EntityV4.Country = overrides.CountryCode
|
||||
}
|
||||
if pin.LocationV6 != nil {
|
||||
pin.LocationV6.Country = geoip.GetCountryInfo(overrides.CountryCode)
|
||||
}
|
||||
if pin.EntityV6 != nil {
|
||||
pin.EntityV6.Country = overrides.CountryCode
|
||||
}
|
||||
}
|
||||
if overrides.Coordinates != nil {
|
||||
if pin.LocationV4 != nil {
|
||||
pin.LocationV4.Coordinates = *overrides.Coordinates
|
||||
}
|
||||
if pin.EntityV4 != nil {
|
||||
pin.EntityV4.Coordinates = overrides.Coordinates
|
||||
}
|
||||
if pin.LocationV6 != nil {
|
||||
pin.LocationV6.Coordinates = *overrides.Coordinates
|
||||
}
|
||||
if pin.EntityV6 != nil {
|
||||
pin.EntityV6.Coordinates = overrides.Coordinates
|
||||
}
|
||||
}
|
||||
if overrides.ASN != 0 {
|
||||
if pin.LocationV4 != nil {
|
||||
pin.LocationV4.AutonomousSystemNumber = overrides.ASN
|
||||
}
|
||||
if pin.EntityV4 != nil {
|
||||
pin.EntityV4.ASN = overrides.ASN
|
||||
}
|
||||
if pin.LocationV6 != nil {
|
||||
pin.LocationV6.AutonomousSystemNumber = overrides.ASN
|
||||
}
|
||||
if pin.EntityV6 != nil {
|
||||
pin.EntityV6.ASN = overrides.ASN
|
||||
}
|
||||
}
|
||||
if overrides.ASOrg != "" {
|
||||
if pin.LocationV4 != nil {
|
||||
pin.LocationV4.AutonomousSystemOrganization = overrides.ASOrg
|
||||
}
|
||||
if pin.EntityV4 != nil {
|
||||
pin.EntityV4.ASOrg = overrides.ASOrg
|
||||
}
|
||||
if pin.LocationV6 != nil {
|
||||
pin.LocationV6.AutonomousSystemOrganization = overrides.ASOrg
|
||||
}
|
||||
if pin.EntityV6 != nil {
|
||||
pin.EntityV6.ASOrg = overrides.ASOrg
|
||||
}
|
||||
}
|
||||
}
|
||||
165
spn/navigator/map.go
Normal file
165
spn/navigator/map.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/spn/docks"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
// Map represent a collection of Pins and their relationship and status.
|
||||
type Map struct {
|
||||
sync.RWMutex
|
||||
Name string
|
||||
|
||||
all map[string]*Pin
|
||||
intel *hub.Intel
|
||||
regions []*Region
|
||||
|
||||
home *Pin
|
||||
homeTerminal *docks.CraneTerminal
|
||||
|
||||
measuringEnabled bool
|
||||
hubUpdateHook *database.RegisteredHook
|
||||
|
||||
// analysisLock guards access to all of this map's Pin.analysis,
|
||||
// regardedPins and the lastDesegrationAttempt fields.
|
||||
analysisLock sync.Mutex
|
||||
regardedPins []*Pin
|
||||
lastDesegrationAttempt time.Time
|
||||
}
|
||||
|
||||
// NewMap returns a new and empty Map.
|
||||
func NewMap(name string, enableMeasuring bool) *Map {
|
||||
m := &Map{
|
||||
Name: name,
|
||||
all: make(map[string]*Pin),
|
||||
measuringEnabled: enableMeasuring,
|
||||
}
|
||||
addMapToAPI(m)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Close removes the map's integration, taking it "offline".
|
||||
func (m *Map) Close() {
|
||||
removeMapFromAPI(m.Name)
|
||||
}
|
||||
|
||||
// GetPin returns the Pin of the Hub with the given ID.
|
||||
func (m *Map) GetPin(hubID string) (pin *Pin, ok bool) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
pin, ok = m.all[hubID]
|
||||
return
|
||||
}
|
||||
|
||||
// GetHome returns the current home and it's accompanying terminal.
|
||||
// Both may be nil.
|
||||
func (m *Map) GetHome() (*Pin, *docks.CraneTerminal) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
return m.home, m.homeTerminal
|
||||
}
|
||||
|
||||
// SetHome sets the given hub as the new home. Optionally, a terminal may be
|
||||
// supplied to accompany the home hub.
|
||||
func (m *Map) SetHome(id string, t *docks.CraneTerminal) (ok bool) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
// Get pin from map.
|
||||
newHome, ok := m.all[id]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove home hub state from all pins.
|
||||
for _, pin := range m.all {
|
||||
pin.removeStates(StateIsHomeHub)
|
||||
}
|
||||
|
||||
// Set pin as home.
|
||||
m.home = newHome
|
||||
m.homeTerminal = t
|
||||
m.home.addStates(StateIsHomeHub)
|
||||
|
||||
// Recalculate reachable.
|
||||
err := m.recalculateReachableHubs()
|
||||
if err != nil {
|
||||
log.Warningf("spn/navigator: failed to recalculate reachable hubs: %s", err)
|
||||
}
|
||||
|
||||
m.PushPinChanges()
|
||||
return true
|
||||
}
|
||||
|
||||
// GetAvailableCountries returns a map of countries including their information
|
||||
// where the map has pins suitable for the given type.
|
||||
func (m *Map) GetAvailableCountries(opts *Options, forType HubType) map[string]*geoip.CountryInfo {
|
||||
if opts == nil {
|
||||
opts = m.defaultOptions()
|
||||
}
|
||||
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
matcher := opts.Matcher(forType, m.intel)
|
||||
countries := make(map[string]*geoip.CountryInfo)
|
||||
for _, pin := range m.all {
|
||||
if !matcher(pin) {
|
||||
continue
|
||||
}
|
||||
if pin.LocationV4 != nil && countries[pin.LocationV4.Country.Code] == nil {
|
||||
countries[pin.LocationV4.Country.Code] = &pin.LocationV4.Country
|
||||
}
|
||||
if pin.LocationV6 != nil && countries[pin.LocationV6.Country.Code] == nil {
|
||||
countries[pin.LocationV6.Country.Code] = &pin.LocationV6.Country
|
||||
}
|
||||
}
|
||||
|
||||
return countries
|
||||
}
|
||||
|
||||
// isEmpty returns whether the Map is regarded as empty.
|
||||
func (m *Map) isEmpty() bool {
|
||||
if m.home != nil {
|
||||
// When a home hub is set, we also regard a map with only one entry to be
|
||||
// empty, as this will be the case for Hubs, which will have their own
|
||||
// entry in the Map.
|
||||
return len(m.all) <= 1
|
||||
}
|
||||
|
||||
return len(m.all) == 0
|
||||
}
|
||||
|
||||
func (m *Map) pinList(lockMap bool) []*Pin {
|
||||
if lockMap {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
}
|
||||
|
||||
// Copy into slice.
|
||||
list := make([]*Pin, 0, len(m.all))
|
||||
for _, pin := range m.all {
|
||||
list = append(list, pin)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (m *Map) sortedPins(lockMap bool) []*Pin {
|
||||
// Get list.
|
||||
list := m.pinList(lockMap)
|
||||
|
||||
// Sort list.
|
||||
sort.Sort(sortByPinID(list))
|
||||
return list
|
||||
}
|
||||
85
spn/navigator/map_stats.go
Normal file
85
spn/navigator/map_stats.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MapStats holds generic map statistics.
|
||||
type MapStats struct {
|
||||
Name string
|
||||
States map[PinState]int
|
||||
Lanes map[int]int
|
||||
ActiveTerminals int
|
||||
}
|
||||
|
||||
// Stats collects and returns statistics from the map.
|
||||
func (m *Map) Stats() *MapStats {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
// Create stats struct.
|
||||
stats := &MapStats{
|
||||
Name: m.Name,
|
||||
States: make(map[PinState]int),
|
||||
Lanes: make(map[int]int),
|
||||
}
|
||||
for _, state := range allStates {
|
||||
stats.States[state] = 0
|
||||
}
|
||||
|
||||
// Iterate over all Pins to collect data.
|
||||
for _, pin := range m.all {
|
||||
// Count active terminals.
|
||||
if pin.HasActiveTerminal() {
|
||||
stats.ActiveTerminals++
|
||||
}
|
||||
|
||||
// Check all states.
|
||||
for _, state := range allStates {
|
||||
if pin.State.Has(state) {
|
||||
stats.States[state]++
|
||||
}
|
||||
}
|
||||
|
||||
// Count lanes.
|
||||
laneCnt, ok := stats.Lanes[len(pin.ConnectedTo)]
|
||||
if ok {
|
||||
stats.Lanes[len(pin.ConnectedTo)] = laneCnt + 1
|
||||
} else {
|
||||
stats.Lanes[len(pin.ConnectedTo)] = 1
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
func (ms *MapStats) String() string {
|
||||
var builder strings.Builder
|
||||
|
||||
// Write header.
|
||||
fmt.Fprintf(&builder, "Stats for Map %s:\n", ms.Name)
|
||||
|
||||
// Write State Stats
|
||||
stateSummary := make([]string, 0, len(ms.States))
|
||||
for state, cnt := range ms.States {
|
||||
stateSummary = append(stateSummary, fmt.Sprintf("State %s: %d Hubs", state, cnt))
|
||||
}
|
||||
sort.Strings(stateSummary)
|
||||
for _, stateSum := range stateSummary {
|
||||
fmt.Fprintln(&builder, stateSum)
|
||||
}
|
||||
|
||||
// Write Lane Stats
|
||||
laneStats := make([]string, 0, len(ms.Lanes))
|
||||
for laneCnt, pinCnt := range ms.Lanes {
|
||||
laneStats = append(laneStats, fmt.Sprintf("%d Lanes: %d Hubs", laneCnt, pinCnt))
|
||||
}
|
||||
sort.Strings(laneStats)
|
||||
for _, laneStat := range laneStats {
|
||||
fmt.Fprintln(&builder, laneStat)
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
279
spn/navigator/map_test.go
Normal file
279
spn/navigator/map_test.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit"
|
||||
|
||||
"github.com/safing/jess/lhash"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeLock sync.Mutex
|
||||
|
||||
defaultMapCreate sync.Once
|
||||
defaultMap *Map
|
||||
)
|
||||
|
||||
func getDefaultTestMap() *Map {
|
||||
defaultMapCreate.Do(func() {
|
||||
defaultMap = createRandomTestMap(1, 200)
|
||||
})
|
||||
return defaultMap
|
||||
}
|
||||
|
||||
func TestRandomMapCreation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := getDefaultTestMap()
|
||||
|
||||
fmt.Println("All Pins:")
|
||||
for _, pin := range m.all {
|
||||
fmt.Printf("%s: %s %s\n", pin, pin.Hub.Info.IPv4, pin.Hub.Info.IPv6)
|
||||
}
|
||||
|
||||
// Print stats
|
||||
fmt.Printf("\n%s\n", m.Stats())
|
||||
|
||||
// Print home
|
||||
fmt.Printf("Selected Home Hub: %s\n", m.home)
|
||||
}
|
||||
|
||||
func createRandomTestMap(seed int64, size int) *Map {
|
||||
fakeLock.Lock()
|
||||
defer fakeLock.Unlock()
|
||||
|
||||
// Seed with parameter to make it reproducible.
|
||||
gofakeit.Seed(seed)
|
||||
|
||||
// Enforce minimum size.
|
||||
if size < 10 {
|
||||
size = 10
|
||||
}
|
||||
|
||||
// Create Hub list.
|
||||
var hubs []*hub.Hub
|
||||
|
||||
// Create Intel data structure.
|
||||
mapIntel := &hub.Intel{
|
||||
Hubs: make(map[string]*hub.HubIntel),
|
||||
}
|
||||
|
||||
// Define periodic values.
|
||||
var currentGroup string
|
||||
|
||||
// Create [size] fake Hubs.
|
||||
for i := 0; i < size; i++ {
|
||||
// Change group every 5 Hubs.
|
||||
if i%5 == 0 {
|
||||
currentGroup = gofakeit.Username()
|
||||
}
|
||||
|
||||
// Create new fake Hub and add to the list.
|
||||
h := createFakeHub(currentGroup, true, mapIntel)
|
||||
hubs = append(hubs, h)
|
||||
}
|
||||
|
||||
// Fake three superseeded Hubs.
|
||||
for i := 0; i < 3; i++ {
|
||||
h := hubs[size-1-i]
|
||||
|
||||
// Set FirstSeen in the past and copy an IP address of an existing Hub.
|
||||
h.FirstSeen = time.Now().Add(-1 * time.Hour)
|
||||
if i%2 == 0 {
|
||||
h.Info.IPv4 = hubs[i].Info.IPv4
|
||||
} else {
|
||||
h.Info.IPv6 = hubs[i].Info.IPv6
|
||||
}
|
||||
}
|
||||
|
||||
// Create Lanes between Hubs in order to create the network.
|
||||
totalConnections := size * 10
|
||||
for i := 0; i < totalConnections; i++ {
|
||||
// Get new random indexes.
|
||||
indexA := gofakeit.Number(0, size-1)
|
||||
indexB := gofakeit.Number(0, size-1)
|
||||
if indexA == indexB {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get Hubs and check if they are already connected.
|
||||
hubA := hubs[indexA]
|
||||
hubB := hubs[indexB]
|
||||
if hubA.GetLaneTo(hubB.ID) != nil {
|
||||
// already connected
|
||||
continue
|
||||
}
|
||||
if hubB.GetLaneTo(hubA.ID) != nil {
|
||||
// already connected
|
||||
continue
|
||||
}
|
||||
|
||||
// Create connections.
|
||||
_ = hubA.AddLane(createLane(hubB.ID))
|
||||
// Add the second connection in 99% of cases.
|
||||
// If this is missing, the Pins should not show up as connected.
|
||||
if gofakeit.Number(0, 100) != 0 {
|
||||
_ = hubB.AddLane(createLane(hubA.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse constructed intel data
|
||||
err := mapIntel.ParseAdvisories()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create map and add Pins.
|
||||
m := NewMap(fmt.Sprintf("Test-Map-%d", seed), true)
|
||||
m.intel = mapIntel
|
||||
for _, h := range hubs {
|
||||
m.UpdateHub(h)
|
||||
}
|
||||
|
||||
// Fake communication error with three Hubs.
|
||||
var i int
|
||||
for _, pin := range m.all {
|
||||
pin.MarkAsFailingFor(1 * time.Hour)
|
||||
pin.addStates(StateFailing)
|
||||
|
||||
if i++; i >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Set a Home Hub.
|
||||
findFakeHomeHub(m)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func createFakeHub(group string, randomFailes bool, mapIntel *hub.Intel) *hub.Hub {
|
||||
// Create fake Hub ID.
|
||||
idSrc := gofakeit.Password(true, true, true, true, true, 64)
|
||||
id := lhash.Digest(lhash.BLAKE2b_256, []byte(idSrc)).Base58()
|
||||
ip4, _ := createGoodIP(true)
|
||||
ip6, _ := createGoodIP(false)
|
||||
|
||||
// Create and return new fake Hub.
|
||||
h := &hub.Hub{
|
||||
ID: id,
|
||||
Info: &hub.Announcement{
|
||||
ID: id,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Name: gofakeit.Username(),
|
||||
Group: group,
|
||||
// ContactAddress // TODO
|
||||
// ContactService // TODO
|
||||
// Hosters []string // TODO
|
||||
// Datacenter string // TODO
|
||||
IPv4: ip4,
|
||||
IPv6: ip6,
|
||||
},
|
||||
Status: &hub.Status{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Keys: map[string]*hub.Key{
|
||||
"a": {
|
||||
Expires: time.Now().Add(48 * time.Hour).Unix(),
|
||||
},
|
||||
},
|
||||
Load: gofakeit.Number(10, 100),
|
||||
},
|
||||
Measurements: hub.NewMeasurements(),
|
||||
FirstSeen: time.Now(),
|
||||
}
|
||||
h.Measurements.Latency = createLatency()
|
||||
h.Measurements.Capacity = createCapacity()
|
||||
h.Measurements.CalculatedCost = CalculateLaneCost(
|
||||
h.Measurements.Latency,
|
||||
h.Measurements.Capacity,
|
||||
)
|
||||
|
||||
// Return if not failures of any kind should be simulated.
|
||||
if !randomFailes {
|
||||
return h
|
||||
}
|
||||
|
||||
// Set hub-based states.
|
||||
if gofakeit.Number(0, 100) == 0 {
|
||||
// Fake Info message error.
|
||||
h.InvalidInfo = true
|
||||
}
|
||||
if gofakeit.Number(0, 100) == 0 {
|
||||
// Fake Status message error.
|
||||
h.InvalidStatus = true
|
||||
}
|
||||
if gofakeit.Number(0, 100) == 0 {
|
||||
// Fake expired exchange keys.
|
||||
for _, key := range h.Status.Keys {
|
||||
key.Expires = time.Now().Add(-1 * time.Hour).Unix()
|
||||
}
|
||||
}
|
||||
|
||||
// Return if not failures of any kind should be simulated.
|
||||
if mapIntel == nil {
|
||||
return h
|
||||
}
|
||||
|
||||
// Set advisory-based states.
|
||||
if gofakeit.Number(0, 10) == 0 {
|
||||
// Make Trusted State
|
||||
mapIntel.Hubs[h.ID] = &hub.HubIntel{
|
||||
Trusted: true,
|
||||
}
|
||||
}
|
||||
if gofakeit.Number(0, 100) == 0 {
|
||||
// Discourage any usage.
|
||||
mapIntel.HubAdvisory = append(mapIntel.HubAdvisory, "- "+h.Info.IPv4.String())
|
||||
}
|
||||
if gofakeit.Number(0, 100) == 0 {
|
||||
// Discourage Home Hub usage.
|
||||
mapIntel.HomeHubAdvisory = append(mapIntel.HomeHubAdvisory, "- "+h.Info.IPv4.String())
|
||||
}
|
||||
if gofakeit.Number(0, 100) == 0 {
|
||||
// Discourage Destination Hub usage.
|
||||
mapIntel.DestinationHubAdvisory = append(mapIntel.DestinationHubAdvisory, "- "+h.Info.IPv4.String())
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func createGoodIP(v4 bool) (net.IP, *geoip.Location) {
|
||||
var candidate net.IP
|
||||
for i := 0; i < 100; i++ {
|
||||
if v4 {
|
||||
candidate = net.ParseIP(gofakeit.IPv4Address())
|
||||
} else {
|
||||
candidate = net.ParseIP(gofakeit.IPv6Address())
|
||||
}
|
||||
loc, err := geoip.GetLocation(candidate)
|
||||
if err == nil && loc.Coordinates.Latitude != 0 {
|
||||
return candidate, loc
|
||||
}
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
func createLane(toHubID string) *hub.Lane {
|
||||
return &hub.Lane{
|
||||
ID: toHubID,
|
||||
Latency: createLatency(),
|
||||
Capacity: createCapacity(),
|
||||
}
|
||||
}
|
||||
|
||||
func createLatency() time.Duration {
|
||||
// Return a value between 10ms and 100ms.
|
||||
return time.Duration(gofakeit.Float64Range(10, 100) * float64(time.Millisecond))
|
||||
}
|
||||
|
||||
func createCapacity() int {
|
||||
// Return a value between 10Mbit/s and 1Gbit/s.
|
||||
return gofakeit.Number(10000000, 1000000000)
|
||||
}
|
||||
144
spn/navigator/measurements.go
Normal file
144
spn/navigator/measurements.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/spn/docks"
|
||||
"github.com/safing/portmaster/spn/terminal"
|
||||
)
|
||||
|
||||
// Measurements Configuration.
|
||||
const (
|
||||
NavigatorMeasurementTTLDefault = 4 * time.Hour
|
||||
NavigatorMeasurementTTLByCostBase = 6 * time.Minute
|
||||
NavigatorMeasurementTTLByCostMin = 4 * time.Hour
|
||||
NavigatorMeasurementTTLByCostMax = 50 * time.Hour
|
||||
|
||||
// With a base TTL of 3m, this leads to:
|
||||
// 20c -> 2h -> raised to 4h.
|
||||
// 50c -> 5h
|
||||
// 100c -> 10h
|
||||
// 1000c -> 100h -> capped to 50h.
|
||||
)
|
||||
|
||||
func (m *Map) measureHubs(ctx context.Context, _ *modules.Task) error {
|
||||
if home, _ := m.GetHome(); home == nil {
|
||||
log.Debug("spn/navigator: skipping measuring, no home hub set")
|
||||
return nil
|
||||
}
|
||||
|
||||
var unknownErrCnt int
|
||||
matcher := m.DefaultOptions().Transit.Matcher(m.GetIntel())
|
||||
|
||||
// Get list and sort in order to check near/low-cost hubs earlier.
|
||||
list := m.pinList(true)
|
||||
sort.Sort(sortByLowestMeasuredCost(list))
|
||||
|
||||
// Find first pin where any measurement has expired.
|
||||
for _, pin := range list {
|
||||
// Check if measuring is enabled.
|
||||
if pin.measurements == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if Pin is regarded.
|
||||
if !matcher(pin) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate dynamic TTL.
|
||||
var checkWithTTL time.Duration
|
||||
if pin.HopDistance == 2 { // Hub is directly connected.
|
||||
checkWithTTL = calculateMeasurementTTLByCost(
|
||||
pin.measurements.GetCalculatedCost(),
|
||||
docks.CraneMeasurementTTLByCostBase,
|
||||
docks.CraneMeasurementTTLByCostMin,
|
||||
docks.CraneMeasurementTTLByCostMax,
|
||||
)
|
||||
} else {
|
||||
checkWithTTL = calculateMeasurementTTLByCost(
|
||||
pin.measurements.GetCalculatedCost(),
|
||||
NavigatorMeasurementTTLByCostBase,
|
||||
NavigatorMeasurementTTLByCostMin,
|
||||
NavigatorMeasurementTTLByCostMax,
|
||||
)
|
||||
}
|
||||
|
||||
// Check if we have measured the pin within the TTL.
|
||||
if !pin.measurements.Expired(checkWithTTL) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Measure connection.
|
||||
tErr := docks.MeasureHub(ctx, pin.Hub, checkWithTTL)
|
||||
|
||||
// Independent of outcome, recalculate the cost.
|
||||
latency, _ := pin.measurements.GetLatency()
|
||||
capacity, _ := pin.measurements.GetCapacity()
|
||||
calculatedCost := CalculateLaneCost(latency, capacity)
|
||||
pin.measurements.SetCalculatedCost(calculatedCost)
|
||||
// Log result.
|
||||
log.Infof(
|
||||
"spn/navigator: updated measurements for connection to %s: %s %.2fMbit/s %.2fc",
|
||||
pin.Hub,
|
||||
latency,
|
||||
float64(capacity)/1000000,
|
||||
calculatedCost,
|
||||
)
|
||||
|
||||
switch {
|
||||
case tErr.IsOK():
|
||||
// All good, continue.
|
||||
|
||||
case tErr.Is(terminal.ErrTryAgainLater):
|
||||
if tErr.IsExternal() {
|
||||
// Remote is measuring, just continue with next.
|
||||
log.Debugf("spn/navigator: remote %s is measuring, continuing with next", pin.Hub)
|
||||
} else {
|
||||
// We are measuring, abort and restart measuring again later.
|
||||
log.Debugf("spn/navigator: postponing measuring because we are currently engaged in measuring")
|
||||
return nil
|
||||
}
|
||||
|
||||
default:
|
||||
log.Warningf("spn/navigator: failed to measure connection to %s: %s", pin.Hub, tErr)
|
||||
unknownErrCnt++
|
||||
if unknownErrCnt >= 3 {
|
||||
log.Warningf("spn/navigator: postponing measuring task because of multiple errors")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveMeasuredHubs saves all Hubs that have unsaved measurements.
|
||||
func (m *Map) SaveMeasuredHubs() {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
for _, pin := range m.all {
|
||||
if !pin.measurements.IsPersisted() {
|
||||
if err := pin.Hub.Save(); err != nil {
|
||||
log.Warningf("spn/navigator: failed to save Hub %s to persist measurements: %s", pin.Hub, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func calculateMeasurementTTLByCost(cost float32, base, min, max time.Duration) time.Duration {
|
||||
calculated := time.Duration(cost) * base
|
||||
switch {
|
||||
case calculated < min:
|
||||
return min
|
||||
case calculated > max:
|
||||
return max
|
||||
default:
|
||||
return calculated
|
||||
}
|
||||
}
|
||||
177
spn/navigator/metrics.go
Normal file
177
spn/navigator/metrics.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/metrics"
|
||||
)
|
||||
|
||||
var metricsRegistered = abool.New()
|
||||
|
||||
func registerMetrics() (err error) {
|
||||
// Only register metrics once.
|
||||
if !metricsRegistered.SetToIf(false, true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Map Stats.
|
||||
|
||||
_, err = metrics.NewGauge(
|
||||
"spn/map/main/latency/all/lowest/seconds",
|
||||
nil,
|
||||
getLowestLatency,
|
||||
&metrics.Options{
|
||||
Name: "SPN Map Lowest Latency",
|
||||
Permission: api.PermitUser,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = metrics.NewGauge(
|
||||
"spn/map/main/latency/fas/lowest/seconds",
|
||||
nil,
|
||||
getLowestLatencyFromFas,
|
||||
&metrics.Options{
|
||||
Name: "SPN Map Lowest Latency",
|
||||
Permission: api.PermitUser,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = metrics.NewGauge(
|
||||
"spn/map/main/capacity/all/highest/bytes",
|
||||
nil,
|
||||
getHighestCapacity,
|
||||
&metrics.Options{
|
||||
Name: "SPN Map Lowest Latency",
|
||||
Permission: api.PermitUser,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = metrics.NewGauge(
|
||||
"spn/map/main/capacity/fas/highest/bytes",
|
||||
nil,
|
||||
getHighestCapacityFromFas,
|
||||
&metrics.Options{
|
||||
Name: "SPN Map Lowest Latency",
|
||||
Permission: api.PermitUser,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
mapStats *mapMetrics
|
||||
mapStatsExpires time.Time
|
||||
mapStatsLock sync.Mutex
|
||||
mapStatsTTL = 55 * time.Second
|
||||
)
|
||||
|
||||
type mapMetrics struct {
|
||||
lowestLatency float64
|
||||
lowestForeignASLatency float64
|
||||
highestCapacity float64
|
||||
highestForeignASCapacity float64
|
||||
}
|
||||
|
||||
func getLowestLatency() float64 { return getMapStats().lowestLatency }
|
||||
func getLowestLatencyFromFas() float64 { return getMapStats().lowestForeignASLatency }
|
||||
func getHighestCapacity() float64 { return getMapStats().highestCapacity }
|
||||
func getHighestCapacityFromFas() float64 { return getMapStats().highestForeignASCapacity }
|
||||
|
||||
func getMapStats() *mapMetrics {
|
||||
mapStatsLock.Lock()
|
||||
defer mapStatsLock.Unlock()
|
||||
|
||||
// Return cache if still valid.
|
||||
if time.Now().Before(mapStatsExpires) {
|
||||
return mapStats
|
||||
}
|
||||
|
||||
// Refresh.
|
||||
mapStats = &mapMetrics{}
|
||||
|
||||
// Get all pins and home.
|
||||
list := Main.pinList(true)
|
||||
home, _ := Main.GetHome()
|
||||
|
||||
// Return empty stats if we have incomplete data.
|
||||
if len(list) <= 1 || home == nil {
|
||||
mapStatsExpires = time.Now().Add(mapStatsTTL)
|
||||
return mapStats
|
||||
}
|
||||
|
||||
// Sort by latency.
|
||||
sort.Sort(sortByLowestMeasuredLatency(list))
|
||||
// Get lowest latency.
|
||||
lowestLatency, _ := list[0].measurements.GetLatency()
|
||||
mapStats.lowestLatency = lowestLatency.Seconds()
|
||||
// Find best foreign AS latency.
|
||||
bestForeignASPin := findFirstForeignASStatsPin(home, list)
|
||||
if bestForeignASPin != nil {
|
||||
lowestForeignASLatency, _ := bestForeignASPin.measurements.GetLatency()
|
||||
mapStats.lowestForeignASLatency = lowestForeignASLatency.Seconds()
|
||||
}
|
||||
|
||||
// Sort by capacity.
|
||||
sort.Sort(sortByHighestMeasuredCapacity(list))
|
||||
// Get highest capacity.
|
||||
highestCapacity, _ := list[0].measurements.GetCapacity()
|
||||
mapStats.highestCapacity = float64(highestCapacity) / 8
|
||||
// Find best foreign AS capacity.
|
||||
bestForeignASPin = findFirstForeignASStatsPin(home, list)
|
||||
if bestForeignASPin != nil {
|
||||
highestForeignASCapacity, _ := bestForeignASPin.measurements.GetCapacity()
|
||||
mapStats.highestForeignASCapacity = float64(highestForeignASCapacity) / 8
|
||||
}
|
||||
|
||||
mapStatsExpires = time.Now().Add(mapStatsTTL)
|
||||
return mapStats
|
||||
}
|
||||
|
||||
func findFirstForeignASStatsPin(home *Pin, list []*Pin) *Pin {
|
||||
// Find best foreign AS latency.
|
||||
for _, pin := range list {
|
||||
compared := false
|
||||
|
||||
// Skip if IPv4 AS matches.
|
||||
if home.LocationV4 != nil && pin.LocationV4 != nil {
|
||||
if home.LocationV4.AutonomousSystemNumber == pin.LocationV4.AutonomousSystemNumber {
|
||||
continue
|
||||
}
|
||||
compared = true
|
||||
}
|
||||
|
||||
// Skip if IPv6 AS matches.
|
||||
if home.LocationV6 != nil && pin.LocationV6 != nil {
|
||||
if home.LocationV6.AutonomousSystemNumber == pin.LocationV6.AutonomousSystemNumber {
|
||||
continue
|
||||
}
|
||||
compared = true
|
||||
}
|
||||
|
||||
// Skip if no data was compared
|
||||
if !compared {
|
||||
continue
|
||||
}
|
||||
|
||||
return pin
|
||||
}
|
||||
return nil
|
||||
}
|
||||
129
spn/navigator/module.go
Normal file
129
spn/navigator/module.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/spn/conf"
|
||||
)
|
||||
|
||||
const (
|
||||
// cfgOptionRoutingAlgorithmKey is copied from profile/config.go to avoid import loop.
|
||||
cfgOptionRoutingAlgorithmKey = "spn/routingAlgorithm"
|
||||
|
||||
// cfgOptionRoutingAlgorithmKey is copied from captain/config.go to avoid import loop.
|
||||
cfgOptionTrustNodeNodesKey = "spn/trustNodes"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrHomeHubUnset is returned when the Home Hub is required and not set.
|
||||
ErrHomeHubUnset = errors.New("map has no Home Hub set")
|
||||
|
||||
// ErrEmptyMap is returned when the Map is empty.
|
||||
ErrEmptyMap = errors.New("map is empty")
|
||||
|
||||
// ErrHubNotFound is returned when the Hub was not found on the Map.
|
||||
ErrHubNotFound = errors.New("hub not found")
|
||||
|
||||
// ErrAllPinsDisregarded is returned when all pins have been disregarded.
|
||||
ErrAllPinsDisregarded = errors.New("all pins have been disregarded")
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
|
||||
// Main is the primary map used.
|
||||
Main *Map
|
||||
|
||||
devMode config.BoolOption
|
||||
cfgOptionRoutingAlgorithm config.StringOption
|
||||
cfgOptionTrustNodeNodes config.StringArrayOption
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("navigator", prep, start, stop, "terminal", "geoip", "netenv")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
return registerAPIEndpoints()
|
||||
}
|
||||
|
||||
func start() error {
|
||||
Main = NewMap(conf.MainMapName, true)
|
||||
devMode = config.Concurrent.GetAsBool(config.CfgDevModeKey, false)
|
||||
cfgOptionRoutingAlgorithm = config.Concurrent.GetAsString(cfgOptionRoutingAlgorithmKey, DefaultRoutingProfileID)
|
||||
cfgOptionTrustNodeNodes = config.Concurrent.GetAsStringArray(cfgOptionTrustNodeNodesKey, []string{})
|
||||
|
||||
err := registerMapDatabase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for geoip databases to be ready.
|
||||
// Try again if not yet ready, as this is critical.
|
||||
// The "wait" parameter times out after 1 second.
|
||||
// Allow 30 seconds for both databases to load.
|
||||
geoInitCheck:
|
||||
for i := 0; i < 30; i++ {
|
||||
switch {
|
||||
case !geoip.IsInitialized(false, true): // First, IPv4.
|
||||
case !geoip.IsInitialized(true, true): // Then, IPv6.
|
||||
default:
|
||||
break geoInitCheck
|
||||
}
|
||||
}
|
||||
|
||||
err = Main.InitializeFromDatabase()
|
||||
if err != nil {
|
||||
// Wait for three seconds, then try again.
|
||||
time.Sleep(3 * time.Second)
|
||||
err = Main.InitializeFromDatabase()
|
||||
if err != nil {
|
||||
// Even if the init fails, we can try to start without it and get data along the way.
|
||||
log.Warningf("spn/navigator: %s", err)
|
||||
}
|
||||
}
|
||||
err = Main.RegisterHubUpdateHook()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: delete superseded hubs after x amount of time
|
||||
|
||||
module.NewTask("update states", Main.updateStates).
|
||||
Repeat(1 * time.Hour).
|
||||
Schedule(time.Now().Add(3 * time.Minute))
|
||||
|
||||
module.NewTask("update failing states", Main.updateFailingStates).
|
||||
Repeat(1 * time.Minute).
|
||||
Schedule(time.Now().Add(3 * time.Minute))
|
||||
|
||||
if conf.PublicHub() {
|
||||
// Only measure Hubs on public Hubs.
|
||||
module.NewTask("measure hubs", Main.measureHubs).
|
||||
Repeat(5 * time.Minute).
|
||||
Schedule(time.Now().Add(1 * time.Minute))
|
||||
|
||||
// Only register metrics on Hubs, as they only make sense there.
|
||||
err := registerMetrics()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
withdrawMapDatabase()
|
||||
|
||||
Main.CancelHubUpdateHook()
|
||||
Main.SaveMeasuredHubs()
|
||||
Main.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
13
spn/navigator/module_test.go
Normal file
13
spn/navigator/module_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/core/pmtesting"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log.SetLogLevel(log.DebugLevel)
|
||||
pmtesting.TestMain(m, module)
|
||||
}
|
||||
388
spn/navigator/optimize.go
Normal file
388
spn/navigator/optimize.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/spn/docks"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
const (
|
||||
optimizationLowestCostConnections = 3
|
||||
optimizationHopDistanceTarget = 3
|
||||
waitUntilMeasuredUpToPercent = 0.5
|
||||
|
||||
desegrationAttemptBackoff = time.Hour
|
||||
)
|
||||
|
||||
// Optimization Purposes.
|
||||
const (
|
||||
OptimizePurposeBootstrap = "bootstrap"
|
||||
OptimizePurposeDesegregate = "desegregate"
|
||||
OptimizePurposeWait = "wait"
|
||||
OptimizePurposeTargetStructure = "target-structure"
|
||||
)
|
||||
|
||||
// AnalysisState holds state for analyzing the network for optimizations.
|
||||
type AnalysisState struct { //nolint:maligned
|
||||
// Suggested signifies that a direct connection to this Hub is suggested by
|
||||
// the optimization algorithm.
|
||||
Suggested bool
|
||||
|
||||
// SuggestedHopDistance holds the hop distance to this Hub when only
|
||||
// considering the suggested Hubs as connected.
|
||||
SuggestedHopDistance int
|
||||
|
||||
// SuggestedHopDistanceInRegion holds the hop distance to this Hub in the
|
||||
// same region when only considering the suggested Hubs as connected.
|
||||
SuggestedHopDistanceInRegion int
|
||||
|
||||
// CrossRegionalConnections holds the amount of connections a Pin has from
|
||||
// the current region.
|
||||
CrossRegionalConnections int
|
||||
// CrossRegionalLowestCostLane holds the lowest cost of the counted
|
||||
// connections from the current region.
|
||||
CrossRegionalLowestCostLane float32
|
||||
// CrossRegionalLaneCosts holds all the cross regional lane costs.
|
||||
CrossRegionalLaneCosts []float32
|
||||
// CrossRegionalHighestCostInHubLimit holds to highest cost of the lowest
|
||||
// cost connections within the maximum allowed lanes on a Hub from the
|
||||
// current region.
|
||||
CrossRegionalHighestCostInHubLimit float32
|
||||
}
|
||||
|
||||
// initAnalysis creates all Pin.analysis fields.
|
||||
// The caller needs to hold the map and analysis lock..
|
||||
func (m *Map) initAnalysis(result *OptimizationResult) {
|
||||
// Compile lists of regarded pins.
|
||||
m.regardedPins = make([]*Pin, 0, len(m.all))
|
||||
for _, region := range m.regions {
|
||||
region.regardedPins = make([]*Pin, 0, len(m.all))
|
||||
}
|
||||
// Find all regarded pins.
|
||||
for _, pin := range m.all {
|
||||
if result.matcher(pin) {
|
||||
m.regardedPins = append(m.regardedPins, pin)
|
||||
// Add to region.
|
||||
if pin.region != nil {
|
||||
pin.region.regardedPins = append(pin.region.regardedPins, pin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize analysis state.
|
||||
for _, pin := range m.all {
|
||||
pin.analysis = &AnalysisState{}
|
||||
}
|
||||
}
|
||||
|
||||
// clearAnalysis reset all Pin.analysis fields.
|
||||
// The caller needs to hold the map and analysis lock.
|
||||
func (m *Map) clearAnalysis() {
|
||||
m.regardedPins = nil
|
||||
for _, region := range m.regions {
|
||||
region.regardedPins = nil
|
||||
}
|
||||
for _, pin := range m.all {
|
||||
pin.analysis = nil
|
||||
}
|
||||
}
|
||||
|
||||
// OptimizationResult holds the result of an optimizaion analysis.
|
||||
type OptimizationResult struct {
|
||||
// Purpose holds a semi-human readable constant of the optimization purpose.
|
||||
Purpose string
|
||||
|
||||
// Approach holds human readable descriptions of how the stated purpose
|
||||
// should be achieved.
|
||||
Approach []string
|
||||
|
||||
// SuggestedConnections holds the Hubs to which connections are suggested.
|
||||
SuggestedConnections []*SuggestedConnection
|
||||
|
||||
// MaxConnect specifies how many connections should be created at maximum
|
||||
// based on this optimization.
|
||||
MaxConnect int
|
||||
|
||||
// StopOthers specifies if other connections than the suggested ones may
|
||||
// be stopped.
|
||||
StopOthers bool
|
||||
|
||||
// opts holds the options for matching Hubs in this optimization.
|
||||
opts *HubOptions
|
||||
|
||||
// matcher is the matcher used to create the regarded Pins.
|
||||
// Required for updating suggested hop distance.
|
||||
matcher PinMatcher
|
||||
}
|
||||
|
||||
// SuggestedConnection holds suggestions by the optimization system.
|
||||
type SuggestedConnection struct {
|
||||
// Hub holds the Hub to which a connection is suggested.
|
||||
Hub *hub.Hub
|
||||
// pin holds the Pin of the Hub.
|
||||
pin *Pin
|
||||
// Reason holds a reason why this connection is suggested.
|
||||
Reason string
|
||||
// Duplicate marks duplicate entries. These should be ignored when
|
||||
// connecting, but are helpful for understand the optimization result.
|
||||
Duplicate bool
|
||||
}
|
||||
|
||||
func (or *OptimizationResult) addApproach(description string) {
|
||||
or.Approach = append(or.Approach, description)
|
||||
}
|
||||
|
||||
func (or *OptimizationResult) addSuggested(reason string, pins ...*Pin) {
|
||||
for _, pin := range pins {
|
||||
// Mark as suggested.
|
||||
pin.analysis.Suggested = true
|
||||
|
||||
// Check if this is a duplicate.
|
||||
var duplicate bool
|
||||
for _, sc := range or.SuggestedConnections {
|
||||
if pin.Hub.ID == sc.Hub.ID {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add to suggested connections.
|
||||
or.SuggestedConnections = append(or.SuggestedConnections, &SuggestedConnection{
|
||||
Hub: pin.Hub,
|
||||
pin: pin,
|
||||
Reason: reason,
|
||||
Duplicate: duplicate,
|
||||
})
|
||||
|
||||
// Update hop distances if we have a matcher.
|
||||
if or.matcher != nil {
|
||||
or.markSuggestedReachable(pin, 2)
|
||||
or.markSuggestedReachableInRegion(pin, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (or *OptimizationResult) markSuggestedReachable(suggested *Pin, hopDistance int) {
|
||||
// Don't update if distance is greater or equal than current one.
|
||||
if hopDistance >= suggested.analysis.SuggestedHopDistance {
|
||||
return
|
||||
}
|
||||
|
||||
// Set suggested hop distance.
|
||||
suggested.analysis.SuggestedHopDistance = hopDistance
|
||||
|
||||
// Increase distance and apply to matching Pins.
|
||||
hopDistance++
|
||||
for _, lane := range suggested.ConnectedTo {
|
||||
if or.matcher(lane.Pin) {
|
||||
or.markSuggestedReachable(lane.Pin, hopDistance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize analyzes the map and suggests changes.
|
||||
func (m *Map) Optimize(opts *HubOptions) (result *OptimizationResult, err error) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
// Check if the map is empty.
|
||||
if m.isEmpty() {
|
||||
return nil, ErrEmptyMap
|
||||
}
|
||||
|
||||
// Set default options if unset.
|
||||
if opts == nil {
|
||||
opts = &HubOptions{}
|
||||
}
|
||||
|
||||
return m.optimize(opts)
|
||||
}
|
||||
|
||||
func (m *Map) optimize(opts *HubOptions) (result *OptimizationResult, err error) {
|
||||
if m.home == nil {
|
||||
return nil, ErrHomeHubUnset
|
||||
}
|
||||
|
||||
// Set default options if unset.
|
||||
if opts == nil {
|
||||
opts = &HubOptions{}
|
||||
}
|
||||
|
||||
// Create result.
|
||||
result = &OptimizationResult{
|
||||
opts: opts,
|
||||
matcher: opts.Matcher(TransitHub, m.intel),
|
||||
}
|
||||
|
||||
// Setup analyis.
|
||||
m.analysisLock.Lock()
|
||||
defer m.analysisLock.Unlock()
|
||||
m.initAnalysis(result)
|
||||
defer m.clearAnalysis()
|
||||
|
||||
// Bootstrap to the network and desegregate map.
|
||||
// If there is a result, return it immediately.
|
||||
returnImmediately := m.optimizeForBootstrappingAndDesegregation(result)
|
||||
if returnImmediately {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Check if we have the measurements we need.
|
||||
if m.measuringEnabled {
|
||||
// Cound pins with valid measurements.
|
||||
var validMeasurements float32
|
||||
for _, pin := range m.regardedPins {
|
||||
if pin.measurements.Valid() {
|
||||
validMeasurements++
|
||||
}
|
||||
}
|
||||
|
||||
// If less than the required amount of regarded Pins have valid
|
||||
// measurements, let's wait until we have that.
|
||||
if validMeasurements/float32(len(m.regardedPins)) < waitUntilMeasuredUpToPercent {
|
||||
return &OptimizationResult{
|
||||
Purpose: OptimizePurposeWait,
|
||||
Approach: []string{"Wait for measurements of 80% of regarded nodes for better optimization."},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Set default values for target structure optimization.
|
||||
result.Purpose = OptimizePurposeTargetStructure
|
||||
result.MaxConnect = 3
|
||||
result.StopOthers = true
|
||||
|
||||
// Optimize for lowest cost.
|
||||
m.optimizeForLowestCost(result, optimizationLowestCostConnections)
|
||||
|
||||
// Optimize for lowest cost in region.
|
||||
m.optimizeForLowestCostInRegion(result)
|
||||
|
||||
// Optimize for distance constraint in region.
|
||||
m.optimizeForDistanceConstraintInRegion(result, 3)
|
||||
|
||||
// Optimize for region-to-region connectivity.
|
||||
m.optimizeForRegionConnectivity(result)
|
||||
|
||||
// Optimize for satellite-to-region connectivity.
|
||||
m.optimizeForSatelliteConnectivity(result)
|
||||
|
||||
// Lapse traffic stats after optimizing for good fresh data next time.
|
||||
for _, crane := range docks.GetAllAssignedCranes() {
|
||||
crane.NetState.LapsePeriod()
|
||||
}
|
||||
|
||||
// Clean and return.
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *Map) optimizeForBootstrappingAndDesegregation(result *OptimizationResult) (returnImmediately bool) {
|
||||
// All regarded Pins are reachable.
|
||||
reachable := len(m.regardedPins)
|
||||
|
||||
// Count Pins that may be connectable.
|
||||
connectable := make([]*Pin, 0, len(m.all))
|
||||
// Copy opts as we are going to make changes.
|
||||
opts := result.opts.Copy()
|
||||
opts.NoDefaults = true
|
||||
opts.Regard = StateNone
|
||||
opts.Disregard = StateSummaryDisregard
|
||||
// Collect Pins with matcher.
|
||||
matcher := opts.Matcher(TransitHub, m.intel)
|
||||
for _, pin := range m.all {
|
||||
if matcher(pin) {
|
||||
connectable = append(connectable, pin)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case reachable == 0:
|
||||
|
||||
// Sort by lowest cost.
|
||||
sort.Sort(sortByLowestMeasuredCost(connectable))
|
||||
|
||||
// Return bootstrap optimization.
|
||||
result.Purpose = OptimizePurposeBootstrap
|
||||
result.Approach = []string{"Connect to a near Hub to connect to the network."}
|
||||
result.MaxConnect = 1
|
||||
result.addSuggested("bootstrap", connectable...)
|
||||
return true
|
||||
|
||||
case reachable > len(connectable)/2:
|
||||
// We are part of the majority network, continue with regular optimization.
|
||||
|
||||
case time.Now().Add(-desegrationAttemptBackoff).Before(m.lastDesegrationAttempt):
|
||||
// We tried to desegregate recently, continue with regular optimization.
|
||||
|
||||
default:
|
||||
// We are in a network comprised of less than half of the known nodes.
|
||||
// Attempt to connect to an unconnected one to desegregate the network.
|
||||
|
||||
// Copy opts as we are going to make changes.
|
||||
opts = opts.Copy()
|
||||
opts.NoDefaults = true
|
||||
opts.Regard = StateNone
|
||||
opts.Disregard = StateSummaryDisregard | StateReachable
|
||||
|
||||
// Iterate over all Pins to find any matching Pin.
|
||||
desegregateWith := make([]*Pin, 0, len(m.all)-reachable)
|
||||
matcher := opts.Matcher(TransitHub, m.intel)
|
||||
for _, pin := range m.all {
|
||||
if matcher(pin) {
|
||||
desegregateWith = append(desegregateWith, pin)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by lowest connection cost.
|
||||
sort.Sort(sortByLowestMeasuredCost(desegregateWith))
|
||||
|
||||
// Build desegration optimization.
|
||||
result.Purpose = OptimizePurposeDesegregate
|
||||
result.Approach = []string{"Attempt to desegregate network by connection to an unreachable Hub."}
|
||||
result.MaxConnect = 1
|
||||
result.addSuggested("desegregate", desegregateWith...)
|
||||
|
||||
// Record desegregation attempt.
|
||||
m.lastDesegrationAttempt = time.Now()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map) optimizeForLowestCost(result *OptimizationResult, max int) {
|
||||
// Add approach.
|
||||
result.addApproach(fmt.Sprintf("Connect to best (lowest cost) %d Hubs globally.", max))
|
||||
|
||||
// Sort by lowest cost.
|
||||
sort.Sort(sortByLowestMeasuredCost(m.regardedPins))
|
||||
|
||||
// Add to suggested pins.
|
||||
if len(m.regardedPins) <= max {
|
||||
result.addSuggested("best globally", m.regardedPins...)
|
||||
} else {
|
||||
result.addSuggested("best globally", m.regardedPins[:max]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) optimizeForDistanceConstraint(result *OptimizationResult, max int) { //nolint:unused // TODO: Likely to be used again.
|
||||
// Add approach.
|
||||
result.addApproach(fmt.Sprintf("Satisfy max hop constraint of %d globally.", optimizationHopDistanceTarget))
|
||||
|
||||
for i := 0; i < max; i++ {
|
||||
// Sort by lowest cost.
|
||||
sort.Sort(sortBySuggestedHopDistanceAndLowestMeasuredCost(m.regardedPins))
|
||||
|
||||
// Return when all regarded Pins are within the distance constraint.
|
||||
if m.regardedPins[0].analysis.SuggestedHopDistance <= optimizationHopDistanceTarget {
|
||||
return
|
||||
}
|
||||
|
||||
// If not, suggest a connection to the best match.
|
||||
result.addSuggested("satisfy global hop constraint", m.regardedPins[0])
|
||||
}
|
||||
}
|
||||
224
spn/navigator/optimize_region.go
Normal file
224
spn/navigator/optimize_region.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func (or *OptimizationResult) markSuggestedReachableInRegion(suggested *Pin, hopDistance int) {
|
||||
// Abort if suggested Pin has no region.
|
||||
if suggested.region == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Don't update if distance is greater or equal than current one.
|
||||
if hopDistance >= suggested.analysis.SuggestedHopDistanceInRegion {
|
||||
return
|
||||
}
|
||||
|
||||
// Set suggested hop distance.
|
||||
suggested.analysis.SuggestedHopDistanceInRegion = hopDistance
|
||||
|
||||
// Increase distance and apply to matching Pins.
|
||||
hopDistance++
|
||||
for _, lane := range suggested.ConnectedTo {
|
||||
if lane.Pin.region != nil &&
|
||||
lane.Pin.region.ID == suggested.region.ID &&
|
||||
or.matcher(lane.Pin) {
|
||||
or.markSuggestedReachableInRegion(lane.Pin, hopDistance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) optimizeForLowestCostInRegion(result *OptimizationResult) {
|
||||
if m.home == nil || m.home.region == nil {
|
||||
return
|
||||
}
|
||||
region := m.home.region
|
||||
|
||||
// Add approach.
|
||||
result.addApproach(fmt.Sprintf("Connect to best (lowest cost) %d Hubs within the region.", region.internalMinLanesOnHub))
|
||||
|
||||
// Sort by lowest cost.
|
||||
sort.Sort(sortByLowestMeasuredCost(region.regardedPins))
|
||||
|
||||
// Add to suggested pins.
|
||||
if len(region.regardedPins) <= region.internalMinLanesOnHub {
|
||||
result.addSuggested("best in region", region.regardedPins...)
|
||||
} else {
|
||||
result.addSuggested("best in region", region.regardedPins[:region.internalMinLanesOnHub]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) optimizeForDistanceConstraintInRegion(result *OptimizationResult, max int) {
|
||||
if m.home == nil || m.home.region == nil {
|
||||
return
|
||||
}
|
||||
region := m.home.region
|
||||
|
||||
// Add approach.
|
||||
result.addApproach(fmt.Sprintf("Satisfy max hop constraint of %d within the region.", region.internalMaxHops))
|
||||
|
||||
// Sort by lowest cost.
|
||||
sort.Sort(sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost(region.regardedPins))
|
||||
|
||||
for i := 0; i < max && i < len(region.regardedPins); i++ {
|
||||
// Return when all regarded Pins are within the distance constraint.
|
||||
if region.regardedPins[i].analysis.SuggestedHopDistanceInRegion <= region.internalMaxHops {
|
||||
return
|
||||
}
|
||||
|
||||
// If not, suggest a connection to the best match.
|
||||
result.addSuggested("satisfy regional hop constraint", region.regardedPins[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) optimizeForRegionConnectivity(result *OptimizationResult) {
|
||||
if m.home == nil || m.home.region == nil {
|
||||
return
|
||||
}
|
||||
region := m.home.region
|
||||
|
||||
// Add approach.
|
||||
result.addApproach("Connect region to other regions.")
|
||||
|
||||
// Optimize for every region.
|
||||
checkRegions:
|
||||
for _, otherRegion := range m.regions {
|
||||
// Skip own region.
|
||||
if region.ID == otherRegion.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect data on connections to that region.
|
||||
lanesToRegion, highestCostWithinLaneLimit := m.countConnectionsToRegion(result, region, otherRegion)
|
||||
|
||||
// Sort by lowest cost.
|
||||
sort.Sort(sortByLowestMeasuredCost(otherRegion.regardedPins))
|
||||
|
||||
// Find cheapest connections with a free slot or better values.
|
||||
var lanesSuggested int
|
||||
for _, pin := range otherRegion.regardedPins {
|
||||
myCost := pin.measurements.GetCalculatedCost()
|
||||
|
||||
// Check if we are done or region is satisfied.
|
||||
switch {
|
||||
case lanesSuggested >= region.regionalMaxLanesOnHub:
|
||||
// We hit our max.
|
||||
continue checkRegions
|
||||
case lanesToRegion >= otherRegion.regionalMinLanes && myCost >= highestCostWithinLaneLimit:
|
||||
// Region has enough lanes and we are not better.
|
||||
continue checkRegions
|
||||
}
|
||||
|
||||
// Check if we can contribute on this Pin.
|
||||
switch {
|
||||
case pin.analysis.CrossRegionalConnections < otherRegion.regionalMaxLanesOnHub &&
|
||||
lanesToRegion < otherRegion.regionalMinLanes:
|
||||
// There is a free spot on this Pin and the region needs more connections.
|
||||
result.addSuggested("occupy cross-region lane on pin", pin)
|
||||
lanesSuggested++
|
||||
lanesToRegion++
|
||||
// Because our own Pin is not counted, this should be the default
|
||||
// suggestion for a stable network.
|
||||
|
||||
case myCost < pin.analysis.CrossRegionalHighestCostInHubLimit:
|
||||
// We have a better connection to this Pin than at least one other existing connection (within the limit!).
|
||||
result.addSuggested("replace cross-region lane on pin", pin)
|
||||
lanesSuggested++
|
||||
lanesToRegion++
|
||||
|
||||
case myCost < highestCostWithinLaneLimit &&
|
||||
pin.analysis.CrossRegionalConnections < otherRegion.regionalMaxLanesOnHub:
|
||||
// We have a better connection to this Pin than another existing region-to-region connection.
|
||||
result.addSuggested("replace unrelated cross-region lane", pin)
|
||||
lanesSuggested++
|
||||
lanesToRegion++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// countConnectionsToRegion analyzes existing lanes from this to another
|
||||
// region, with taking lanes from this Hub into account.
|
||||
func (m *Map) countConnectionsToRegion(result *OptimizationResult, region *Region, otherRegion *Region) (lanesToRegion int, highestCostWithinLaneLimit float32) {
|
||||
for _, pin := range region.regardedPins {
|
||||
// Skip self.
|
||||
if m.home.Hub.ID == pin.Hub.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find lanes to other region.
|
||||
for _, lane := range pin.ConnectedTo {
|
||||
if lane.Pin.region != nil &&
|
||||
lane.Pin.region.ID == otherRegion.ID &&
|
||||
result.matcher(lane.Pin) {
|
||||
// This is a lane from this region to a regarded Pin in the other region.
|
||||
lanesToRegion++
|
||||
|
||||
// Count cross region connection.
|
||||
lane.Pin.analysis.CrossRegionalConnections++
|
||||
|
||||
// Collect lane costs.
|
||||
lane.Pin.analysis.CrossRegionalLaneCosts = append(
|
||||
lane.Pin.analysis.CrossRegionalLaneCosts,
|
||||
lane.Cost,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate lane costs from collected lane costs.
|
||||
for _, pin := range otherRegion.regardedPins {
|
||||
sort.Sort(sortCostsByLowest(pin.analysis.CrossRegionalLaneCosts))
|
||||
switch {
|
||||
case len(pin.analysis.CrossRegionalLaneCosts) == 0:
|
||||
// Nothing to do.
|
||||
case len(pin.analysis.CrossRegionalLaneCosts) < otherRegion.regionalMaxLanesOnHub:
|
||||
pin.analysis.CrossRegionalLowestCostLane = pin.analysis.CrossRegionalLaneCosts[0]
|
||||
pin.analysis.CrossRegionalHighestCostInHubLimit = pin.analysis.CrossRegionalLaneCosts[len(pin.analysis.CrossRegionalLaneCosts)-1]
|
||||
default:
|
||||
pin.analysis.CrossRegionalLowestCostLane = pin.analysis.CrossRegionalLaneCosts[0]
|
||||
pin.analysis.CrossRegionalHighestCostInHubLimit = pin.analysis.CrossRegionalLaneCosts[otherRegion.regionalMaxLanesOnHub-1]
|
||||
}
|
||||
|
||||
// Find highest cost within limit.
|
||||
if pin.analysis.CrossRegionalHighestCostInHubLimit > highestCostWithinLaneLimit {
|
||||
highestCostWithinLaneLimit = pin.analysis.CrossRegionalHighestCostInHubLimit
|
||||
}
|
||||
}
|
||||
|
||||
return lanesToRegion, highestCostWithinLaneLimit
|
||||
}
|
||||
|
||||
func (m *Map) optimizeForSatelliteConnectivity(result *OptimizationResult) {
|
||||
if m.home == nil {
|
||||
return
|
||||
}
|
||||
// This is only for Hubs that are not in a region.
|
||||
if m.home.region != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add approach.
|
||||
result.addApproach("Connect satellite to regions.")
|
||||
|
||||
// Optimize for every region.
|
||||
for _, region := range m.regions {
|
||||
// Sort by lowest cost.
|
||||
sort.Sort(sortByLowestMeasuredCost(region.regardedPins))
|
||||
|
||||
// Add to suggested pins.
|
||||
if len(region.regardedPins) <= region.satelliteMinLanes {
|
||||
result.addSuggested(fmt.Sprintf("best to region %s", region.ID), region.regardedPins...)
|
||||
} else {
|
||||
result.addSuggested(fmt.Sprintf("best to region %s", region.ID), region.regardedPins[:region.satelliteMinLanes]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type sortCostsByLowest []float32
|
||||
|
||||
func (a sortCostsByLowest) Len() int { return len(a) }
|
||||
func (a sortCostsByLowest) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortCostsByLowest) Less(i, j int) bool { return a[i] < a[j] }
|
||||
188
spn/navigator/optimize_test.go
Normal file
188
spn/navigator/optimize_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
var (
|
||||
optimizedDefaultMapCreate sync.Once
|
||||
optimizedDefaultMap *Map
|
||||
)
|
||||
|
||||
func getOptimizedDefaultTestMap(t *testing.T) *Map {
|
||||
t.Helper()
|
||||
|
||||
optimizedDefaultMapCreate.Do(func() {
|
||||
optimizedDefaultMap = createRandomTestMap(2, 100)
|
||||
optimizedDefaultMap.optimizeTestMap(t)
|
||||
})
|
||||
return optimizedDefaultMap
|
||||
}
|
||||
|
||||
func (m *Map) optimizeTestMap(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Logf("optimizing test map %s with %d pins", m.Name, len(m.all))
|
||||
|
||||
// Save original Home, as we will be switching around the home for the
|
||||
// optimization.
|
||||
run := 0
|
||||
newLanes := 0
|
||||
originalHome := m.home
|
||||
mcf := newMeasurementCachedFactory()
|
||||
|
||||
for {
|
||||
run++
|
||||
newLanesInRun := 0
|
||||
// Let's check if we have a run without any map changes.
|
||||
lastRun := true
|
||||
|
||||
for _, pin := range m.all {
|
||||
// Set Home to this Pin for this iteration.
|
||||
if !m.SetHome(pin.Hub.ID, nil) {
|
||||
panic("failed to set home")
|
||||
}
|
||||
|
||||
// Update measurements for the new home.
|
||||
updateMeasurements(m, mcf)
|
||||
|
||||
optimizeResult, err := m.optimize(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
lanesCreatedWithResult := 0
|
||||
for _, connectTo := range optimizeResult.SuggestedConnections {
|
||||
// Check if lane to suggested Hub already exists.
|
||||
if m.home.Hub.GetLaneTo(connectTo.Hub.ID) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add lanes to the Hub status.
|
||||
_ = m.home.Hub.AddLane(createLane(connectTo.Hub.ID))
|
||||
_ = connectTo.Hub.AddLane(createLane(m.home.Hub.ID))
|
||||
|
||||
// Update Hubs in map.
|
||||
m.UpdateHub(m.home.Hub)
|
||||
m.UpdateHub(connectTo.Hub)
|
||||
newLanes++
|
||||
newLanesInRun++
|
||||
|
||||
// We are changing the map in this run, so this is not the last.
|
||||
lastRun = false
|
||||
|
||||
// Only create as many lanes as suggested by the result.
|
||||
lanesCreatedWithResult++
|
||||
if lanesCreatedWithResult >= optimizeResult.MaxConnect {
|
||||
break
|
||||
}
|
||||
}
|
||||
if optimizeResult.Purpose != OptimizePurposeTargetStructure {
|
||||
// If we aren't yet building the target structure, we need to keep building.
|
||||
lastRun = false
|
||||
}
|
||||
}
|
||||
|
||||
// Log progress.
|
||||
if t != nil {
|
||||
t.Logf(
|
||||
"optimizing: added %d lanes in run #%d (%d Hubs) - %d new lanes in total",
|
||||
newLanesInRun,
|
||||
run,
|
||||
len(m.all),
|
||||
newLanes,
|
||||
)
|
||||
}
|
||||
|
||||
// End optimization after last run.
|
||||
if lastRun {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Log what was done and set home back to the original value.
|
||||
if t != nil {
|
||||
t.Logf("finished optimizing test map %s: added %d lanes in %d runs", m.Name, newLanes, run)
|
||||
}
|
||||
m.home = originalHome
|
||||
}
|
||||
|
||||
func TestOptimize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := getOptimizedDefaultTestMap(t)
|
||||
matcher := m.defaultOptions().Destination.Matcher(m.intel)
|
||||
originalHome := m.home
|
||||
|
||||
for _, pin := range m.all {
|
||||
// Set Home to this Pin for this iteration.
|
||||
m.home = pin
|
||||
err := m.recalculateReachableHubs()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, peer := range m.all {
|
||||
// Check if the Pin matches the criteria.
|
||||
if !matcher(peer) {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: Adapt test to new regions.
|
||||
if peer.HopDistance > 5 {
|
||||
t.Errorf("Optimization error: %s is %d hops away from %s", peer, peer.HopDistance, pin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print stats
|
||||
t.Logf("optimized map:\n%s\n", m.Stats())
|
||||
|
||||
m.home = originalHome
|
||||
}
|
||||
|
||||
func updateMeasurements(m *Map, mcf *measurementCachedFactory) {
|
||||
for _, pin := range m.all {
|
||||
pin.measurements = mcf.getOrCreate(m.home.Hub.ID, pin.Hub.ID)
|
||||
}
|
||||
}
|
||||
|
||||
type measurementCachedFactory struct {
|
||||
cache map[string]*hub.Measurements
|
||||
}
|
||||
|
||||
func newMeasurementCachedFactory() *measurementCachedFactory {
|
||||
return &measurementCachedFactory{
|
||||
cache: make(map[string]*hub.Measurements),
|
||||
}
|
||||
}
|
||||
|
||||
func (mcf *measurementCachedFactory) getOrCreate(from, to string) *hub.Measurements {
|
||||
var id string
|
||||
comparison := strings.Compare(from, to)
|
||||
switch {
|
||||
case comparison == 0:
|
||||
return nil
|
||||
case comparison > 0:
|
||||
id = from + "-" + to
|
||||
case comparison < 0:
|
||||
id = to + "-" + from
|
||||
}
|
||||
|
||||
m, ok := mcf.cache[id]
|
||||
if ok {
|
||||
return m
|
||||
}
|
||||
|
||||
m = hub.NewMeasurements()
|
||||
m.Latency = createLatency()
|
||||
m.Capacity = createCapacity()
|
||||
m.CalculatedCost = CalculateLaneCost(
|
||||
m.Latency,
|
||||
m.Capacity,
|
||||
)
|
||||
mcf.cache[id] = m
|
||||
return m
|
||||
}
|
||||
330
spn/navigator/options.go
Normal file
330
spn/navigator/options.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/profile/endpoints"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
// HubType is the usage type of a Hub in routing.
|
||||
type HubType uint8
|
||||
|
||||
// Hub Types.
|
||||
const (
|
||||
HomeHub HubType = iota
|
||||
TransitHub
|
||||
DestinationHub
|
||||
)
|
||||
|
||||
// DeriveTunnelOptions derives and returns the tunnel options from the connection and profile.
|
||||
// This function lives in firewall/tunnel.go and is set here to avoid import loops.
|
||||
var DeriveTunnelOptions func(lp *profile.LayeredProfile, destination *intel.Entity, connEncrypted bool) *Options
|
||||
|
||||
// Options holds configuration options for operations with the Map.
|
||||
type Options struct { //nolint:maligned
|
||||
// Home holds the options for Home Hubs.
|
||||
Home *HomeHubOptions
|
||||
|
||||
// Transit holds the options for Transit Hubs.
|
||||
Transit *TransitHubOptions
|
||||
|
||||
// Destination holds the options for Destination Hubs.
|
||||
Destination *DestinationHubOptions
|
||||
|
||||
// RoutingProfile defines the algorithm to use to find a route.
|
||||
RoutingProfile string
|
||||
}
|
||||
|
||||
// HomeHubOptions holds configuration options for Home Hub operations with the Map.
|
||||
type HomeHubOptions HubOptions
|
||||
|
||||
// TransitHubOptions holds configuration options for Transit Hub operations with the Map.
|
||||
type TransitHubOptions HubOptions
|
||||
|
||||
// DestinationHubOptions holds configuration options for Destination Hub operations with the Map.
|
||||
type DestinationHubOptions HubOptions
|
||||
|
||||
// HubOptions holds configuration options for a specific hub type for operations with the Map.
|
||||
type HubOptions struct {
|
||||
// Regard holds required States. Only Hubs where all of these are present
|
||||
// will taken into account for the operation. If NoDefaults is not set, a
|
||||
// basic set of desirable states is added automatically.
|
||||
Regard PinState
|
||||
|
||||
// Disregard holds disqualifying States. Only Hubs where none of these are
|
||||
// present will be taken into account for the operation. If NoDefaults is not
|
||||
// set, a basic set of undesirable states is added automatically.
|
||||
Disregard PinState
|
||||
|
||||
// NoDefaults declares whether default and recommended Regard and Disregard states should not be used.
|
||||
NoDefaults bool
|
||||
|
||||
// HubPolicies is a collection of endpoint lists that Hubs must pass in order
|
||||
// to be taken into account for the operation.
|
||||
HubPolicies []endpoints.Endpoints
|
||||
|
||||
// RequireVerifiedOwners specifies which verified owners are allowed to be used.
|
||||
// If the list is empty, all owners are allowed.
|
||||
RequireVerifiedOwners []string
|
||||
|
||||
// CheckHubPolicyWith provides an entity that must match the Hubs entry or exit
|
||||
// policy (depending on type) in order to be taken into account for the operation.
|
||||
CheckHubPolicyWith *intel.Entity
|
||||
}
|
||||
|
||||
// Copy returns a shallow copy of the Options.
|
||||
func (o *Options) Copy() *Options {
|
||||
copied := &Options{
|
||||
RoutingProfile: o.RoutingProfile,
|
||||
}
|
||||
if o.Home != nil {
|
||||
c := HomeHubOptions(HubOptions(*o.Home).Copy())
|
||||
copied.Home = &c
|
||||
}
|
||||
if o.Transit != nil {
|
||||
c := TransitHubOptions(HubOptions(*o.Transit).Copy())
|
||||
copied.Transit = &c
|
||||
}
|
||||
if o.Destination != nil {
|
||||
c := DestinationHubOptions(HubOptions(*o.Destination).Copy())
|
||||
copied.Destination = &c
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
// Copy returns a shallow copy of the Options.
|
||||
func (o HubOptions) Copy() HubOptions {
|
||||
return HubOptions{
|
||||
Regard: o.Regard,
|
||||
Disregard: o.Disregard,
|
||||
NoDefaults: o.NoDefaults,
|
||||
HubPolicies: o.HubPolicies,
|
||||
RequireVerifiedOwners: o.RequireVerifiedOwners,
|
||||
CheckHubPolicyWith: o.CheckHubPolicyWith,
|
||||
}
|
||||
}
|
||||
|
||||
// PinMatcher is a stateful matching function generated by Options.
|
||||
type PinMatcher func(pin *Pin) bool
|
||||
|
||||
// DefaultOptions returns the default options for this Map.
|
||||
func (m *Map) DefaultOptions() *Options {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.defaultOptions()
|
||||
}
|
||||
|
||||
func (m *Map) defaultOptions() *Options {
|
||||
opts := &Options{
|
||||
RoutingProfile: DefaultRoutingProfileID,
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// HubPoliciesAreSet returns whether any of the given hub policies are set and non-empty.
|
||||
func HubPoliciesAreSet(policies []endpoints.Endpoints) bool {
|
||||
for _, policy := range policies {
|
||||
if policy.IsSet() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var emptyHubOptions = &HubOptions{}
|
||||
|
||||
// Matcher generates a PinMatcher based on the Options.
|
||||
func (o *HomeHubOptions) Matcher(hubIntel *hub.Intel) PinMatcher {
|
||||
if o == nil {
|
||||
return emptyHubOptions.Matcher(HomeHub, hubIntel)
|
||||
}
|
||||
|
||||
// Convert and call base func.
|
||||
ho := HubOptions(*o)
|
||||
return ho.Matcher(HomeHub, hubIntel)
|
||||
}
|
||||
|
||||
// Matcher generates a PinMatcher based on the Options.
|
||||
func (o *TransitHubOptions) Matcher(hubIntel *hub.Intel) PinMatcher {
|
||||
if o == nil {
|
||||
return emptyHubOptions.Matcher(TransitHub, hubIntel)
|
||||
}
|
||||
|
||||
// Convert and call base func.
|
||||
ho := HubOptions(*o)
|
||||
return ho.Matcher(TransitHub, hubIntel)
|
||||
}
|
||||
|
||||
// Matcher generates a PinMatcher based on the Options.
|
||||
func (o *DestinationHubOptions) Matcher(hubIntel *hub.Intel) PinMatcher {
|
||||
if o == nil {
|
||||
return emptyHubOptions.Matcher(DestinationHub, hubIntel)
|
||||
}
|
||||
|
||||
// Convert and call base func.
|
||||
ho := HubOptions(*o)
|
||||
return ho.Matcher(DestinationHub, hubIntel)
|
||||
}
|
||||
|
||||
// Matcher generates a PinMatcher based on the Options.
|
||||
// Always use the Matcher on option structs if you can.
|
||||
func (o *Options) Matcher(hubType HubType, hubIntel *hub.Intel) PinMatcher {
|
||||
switch hubType {
|
||||
case HomeHub:
|
||||
return o.Home.Matcher(hubIntel)
|
||||
case TransitHub:
|
||||
return o.Transit.Matcher(hubIntel)
|
||||
case DestinationHub:
|
||||
return o.Destination.Matcher(hubIntel)
|
||||
default:
|
||||
return nil // This will panic, but should never be used.
|
||||
}
|
||||
}
|
||||
|
||||
// Matcher generates a PinMatcher based on the Options.
|
||||
func (o *HubOptions) Matcher(hubType HubType, hubIntel *hub.Intel) PinMatcher {
|
||||
// Fallback to empty hub options.
|
||||
if o == nil {
|
||||
o = emptyHubOptions
|
||||
}
|
||||
|
||||
// Compile states to regard and disregard.
|
||||
regard := o.Regard
|
||||
disregard := o.Disregard
|
||||
|
||||
// Add default states.
|
||||
if !o.NoDefaults {
|
||||
// Add default States.
|
||||
regard = regard.Add(StateSummaryRegard)
|
||||
disregard = disregard.Add(StateSummaryDisregard)
|
||||
|
||||
// Add type based Advisories.
|
||||
switch hubType {
|
||||
case HomeHub:
|
||||
// Home Hubs don't need to be reachable and don't need keys ready to be used.
|
||||
regard = regard.Remove(StateReachable)
|
||||
regard = regard.Remove(StateActive)
|
||||
// Follow advisory.
|
||||
disregard = disregard.Add(StateUsageAsHomeDiscouraged)
|
||||
// Home Hub may be the current Home Hub.
|
||||
disregard = disregard.Remove(StateIsHomeHub)
|
||||
case TransitHub:
|
||||
// Transit Hubs get no additional states.
|
||||
case DestinationHub:
|
||||
// Follow advisory.
|
||||
disregard = disregard.Add(StateUsageAsDestinationDiscouraged)
|
||||
// Do not use if Hub reports network issues.
|
||||
disregard = disregard.Add(StateConnectivityIssues)
|
||||
}
|
||||
}
|
||||
|
||||
// Add intel policies.
|
||||
hubPolicies := o.HubPolicies
|
||||
if hubIntel != nil && hubIntel.Parsed() != nil {
|
||||
switch hubType {
|
||||
case HomeHub:
|
||||
hubPolicies = append(hubPolicies, hubIntel.Parsed().HubAdvisory, hubIntel.Parsed().HomeHubAdvisory)
|
||||
case TransitHub:
|
||||
hubPolicies = append(hubPolicies, hubIntel.Parsed().HubAdvisory)
|
||||
case DestinationHub:
|
||||
hubPolicies = append(hubPolicies, hubIntel.Parsed().HubAdvisory, hubIntel.Parsed().DestinationHubAdvisory)
|
||||
}
|
||||
}
|
||||
|
||||
// Add entry/exit policiy checks.
|
||||
checkHubPolicyWith := o.CheckHubPolicyWith
|
||||
|
||||
return func(pin *Pin) bool {
|
||||
// Check required Pin States.
|
||||
if !pin.State.Has(regard) || pin.State.HasAnyOf(disregard) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check verified owners.
|
||||
if len(o.RequireVerifiedOwners) > 0 {
|
||||
// Check if Pin has a verified owner at all.
|
||||
if pin.VerifiedOwner == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if verified owner is in the list.
|
||||
inList := false
|
||||
for _, allowed := range o.RequireVerifiedOwners {
|
||||
if pin.VerifiedOwner == allowed {
|
||||
inList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Pin does not have a verified owner from the allowed list.
|
||||
if !inList {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check policies.
|
||||
policyCheck:
|
||||
for _, policy := range hubPolicies {
|
||||
// Check if policy is set.
|
||||
if !policy.IsSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if policy matches.
|
||||
result, reason := policy.MatchMulti(context.TODO(), pin.EntityV4, pin.EntityV6)
|
||||
switch result {
|
||||
case endpoints.NoMatch:
|
||||
// Continue with check.
|
||||
case endpoints.MatchError:
|
||||
log.Warningf("spn/navigator: failed to match policy: %s", reason)
|
||||
// Continue with check for now.
|
||||
// TODO: Rethink how to do this. If eg. the geoip database has a
|
||||
// problem, then no Hub will match. For now, just continue to the
|
||||
// next rule set. Not optimal, but fail safe.
|
||||
case endpoints.Denied:
|
||||
// Explicitly denied, abort immediately.
|
||||
return false
|
||||
case endpoints.Permitted:
|
||||
// Explicitly allowed, abort check and continue.
|
||||
break policyCheck
|
||||
}
|
||||
}
|
||||
|
||||
// Check entry/exit policies.
|
||||
if checkHubPolicyWith != nil {
|
||||
switch hubType {
|
||||
case HomeHub:
|
||||
if endpointListMatch(pin.Hub.Info.EntryPolicy(), checkHubPolicyWith) == endpoints.Denied {
|
||||
// Hub does not allow entry from the given entity.
|
||||
return false
|
||||
}
|
||||
case TransitHub:
|
||||
// Transit Hubs do not have a hub policy.
|
||||
case DestinationHub:
|
||||
if endpointListMatch(pin.Hub.Info.ExitPolicy(), checkHubPolicyWith) == endpoints.Denied {
|
||||
// Hub does not allow exit to the given entity.
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true // All checks have passed.
|
||||
}
|
||||
}
|
||||
|
||||
func endpointListMatch(list endpoints.Endpoints, entity *intel.Entity) endpoints.EPResult {
|
||||
// Check if endpoint list and entity are available.
|
||||
if !list.IsSet() || entity == nil {
|
||||
return endpoints.NoMatch
|
||||
}
|
||||
|
||||
// Match and return result only.
|
||||
result, _ := list.Match(context.TODO(), entity)
|
||||
return result
|
||||
}
|
||||
269
spn/navigator/pin.go
Normal file
269
spn/navigator/pin.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/spn/docks"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
// Pin represents a Hub on a Map.
|
||||
type Pin struct { //nolint:maligned
|
||||
// Hub Information
|
||||
Hub *hub.Hub
|
||||
EntityV4 *intel.Entity
|
||||
EntityV6 *intel.Entity
|
||||
LocationV4 *geoip.Location
|
||||
LocationV6 *geoip.Location
|
||||
|
||||
// Hub Status
|
||||
State PinState
|
||||
// VerifiedOwner holds the name of the verified owner / operator of the Hub.
|
||||
VerifiedOwner string
|
||||
// HopDistance signifies the needed hops to reach this Hub.
|
||||
// HopDistance is measured from the view of a client.
|
||||
// A Hub itself will have itself at distance 1.
|
||||
// Directly connected Hubs have a distance of 2.
|
||||
HopDistance int
|
||||
// Cost is the routing cost of this Hub.
|
||||
Cost float32
|
||||
// ConnectedTo holds validated lanes.
|
||||
ConnectedTo map[string]*Lane // Key is Hub ID.
|
||||
|
||||
// FailingUntil specifies until when this Hub should be regarded as failing.
|
||||
// This is connected to StateFailing.
|
||||
FailingUntil time.Time
|
||||
|
||||
// Connection holds a information about a connection to the Hub of this Pin.
|
||||
Connection *PinConnection
|
||||
|
||||
// Internal
|
||||
|
||||
// pushChanges is set to true if something noteworthy on the Pin changed and
|
||||
// an update needs to be pushed by the database storage interface to whoever
|
||||
// is listening.
|
||||
pushChanges *abool.AtomicBool
|
||||
|
||||
// measurements holds Measurements regarding this Pin.
|
||||
// It must always be set and the reference must not be changed when measuring
|
||||
// is enabled.
|
||||
// Access to fields within are coordinated by itself.
|
||||
measurements *hub.Measurements
|
||||
|
||||
// analysis holds the analysis state.
|
||||
// Should only be set during analysis and be reset at the start and removed at the end of an analysis.
|
||||
analysis *AnalysisState
|
||||
|
||||
// region is the region this Pin belongs to.
|
||||
region *Region
|
||||
}
|
||||
|
||||
// PinConnection represents a connection to a terminal on the Hub.
|
||||
type PinConnection struct {
|
||||
// Terminal holds the active terminal session.
|
||||
Terminal *docks.ExpansionTerminal
|
||||
|
||||
// Route is the route built for this terminal.
|
||||
Route *Route
|
||||
}
|
||||
|
||||
// Lane is a connection to another Hub.
|
||||
type Lane struct {
|
||||
// Pin is the Pin/Hub this Lane connects to.
|
||||
Pin *Pin
|
||||
|
||||
// Capacity designates the available bandwidth between these Hubs.
|
||||
// It is specified in bit/s.
|
||||
Capacity int
|
||||
|
||||
// Lateny designates the latency between these Hubs.
|
||||
// It is specified in nanoseconds.
|
||||
Latency time.Duration
|
||||
|
||||
// Cost is the routing cost of this lane.
|
||||
Cost float32
|
||||
|
||||
// active is a helper flag in order help remove abandoned Lanes.
|
||||
active bool
|
||||
}
|
||||
|
||||
// Lock locks the Pin via the Hub's lock.
|
||||
func (pin *Pin) Lock() {
|
||||
pin.Hub.Lock()
|
||||
}
|
||||
|
||||
// Unlock unlocks the Pin via the Hub's lock.
|
||||
func (pin *Pin) Unlock() {
|
||||
pin.Hub.Unlock()
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the Pin.
|
||||
func (pin *Pin) String() string {
|
||||
return "<Pin " + pin.Hub.Name() + ">"
|
||||
}
|
||||
|
||||
// GetState returns the state of the pin.
|
||||
func (pin *Pin) GetState() PinState {
|
||||
pin.Lock()
|
||||
defer pin.Unlock()
|
||||
|
||||
return pin.State
|
||||
}
|
||||
|
||||
// updateLocationData fetches the necessary location data in order to correctly map out the Pin.
|
||||
func (pin *Pin) updateLocationData() {
|
||||
// TODO: We are currently assigning the Hub ID to the entity domain to
|
||||
// support matching a Hub by its ID. The issue here is that the domain
|
||||
// rules are lower-cased, so we have to lower-case the ID here too.
|
||||
// This is not optimal from a security perspective, but there are still
|
||||
// enough bits left that this cannot be easily exploited.
|
||||
|
||||
if pin.Hub.Info.IPv4 != nil {
|
||||
pin.EntityV4 = (&intel.Entity{
|
||||
IP: pin.Hub.Info.IPv4,
|
||||
Domain: strings.ToLower(pin.Hub.ID) + ".",
|
||||
}).Init(0)
|
||||
|
||||
var ok bool
|
||||
pin.LocationV4, ok = pin.EntityV4.GetLocation(context.TODO())
|
||||
if !ok {
|
||||
log.Warningf("spn/navigator: failed to get location of %s of %s", pin.Hub.Info.IPv4, pin.Hub.StringWithoutLocking())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
pin.EntityV4 = nil
|
||||
pin.LocationV4 = nil
|
||||
}
|
||||
|
||||
if pin.Hub.Info.IPv6 != nil {
|
||||
pin.EntityV6 = (&intel.Entity{
|
||||
IP: pin.Hub.Info.IPv6,
|
||||
Domain: strings.ToLower(pin.Hub.ID) + ".",
|
||||
}).Init(0)
|
||||
|
||||
var ok bool
|
||||
pin.LocationV6, ok = pin.EntityV6.GetLocation(context.TODO())
|
||||
if !ok {
|
||||
log.Warningf("spn/navigator: failed to get location of %s of %s", pin.Hub.Info.IPv6, pin.Hub.StringWithoutLocking())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
pin.EntityV6 = nil
|
||||
pin.LocationV6 = nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetLocation returns the geoip location of the Pin, preferring first the given IP, then IPv4.
|
||||
func (pin *Pin) GetLocation(ip net.IP) *geoip.Location {
|
||||
pin.Lock()
|
||||
defer pin.Unlock()
|
||||
|
||||
switch {
|
||||
case ip != nil && ip.Equal(pin.Hub.Info.IPv4) && pin.LocationV4 != nil:
|
||||
return pin.LocationV4
|
||||
case ip != nil && ip.Equal(pin.Hub.Info.IPv6) && pin.LocationV6 != nil:
|
||||
return pin.LocationV6
|
||||
case pin.LocationV4 != nil:
|
||||
return pin.LocationV4
|
||||
case pin.LocationV6 != nil:
|
||||
return pin.LocationV6
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetActiveTerminal sets an active terminal for the pin.
|
||||
func (pin *Pin) SetActiveTerminal(pc *PinConnection) {
|
||||
pin.Lock()
|
||||
defer pin.Unlock()
|
||||
|
||||
pin.Connection = pc
|
||||
if pin.Connection != nil && pin.Connection.Terminal != nil {
|
||||
pin.Connection.Terminal.SetChangeNotifyFunc(pin.NotifyTerminalChange)
|
||||
}
|
||||
|
||||
pin.pushChanges.Set()
|
||||
}
|
||||
|
||||
// GetActiveTerminal returns the active terminal of the pin.
|
||||
func (pin *Pin) GetActiveTerminal() *docks.ExpansionTerminal {
|
||||
pin.Lock()
|
||||
defer pin.Unlock()
|
||||
|
||||
if !pin.hasActiveTerminal() {
|
||||
return nil
|
||||
}
|
||||
return pin.Connection.Terminal
|
||||
}
|
||||
|
||||
// HasActiveTerminal returns whether the Pin has an active terminal.
|
||||
func (pin *Pin) HasActiveTerminal() bool {
|
||||
pin.Lock()
|
||||
defer pin.Unlock()
|
||||
|
||||
return pin.hasActiveTerminal()
|
||||
}
|
||||
|
||||
func (pin *Pin) hasActiveTerminal() bool {
|
||||
return pin.Connection != nil &&
|
||||
pin.Connection.Terminal.Abandoning.IsNotSet()
|
||||
}
|
||||
|
||||
// NotifyTerminalChange notifies subscribers of the changed terminal.
|
||||
func (pin *Pin) NotifyTerminalChange() {
|
||||
pin.pushChanges.Set()
|
||||
pin.pushChange()
|
||||
}
|
||||
|
||||
// IsFailing returns whether the pin should be treated as failing.
|
||||
// The Pin is locked for this.
|
||||
func (pin *Pin) IsFailing() bool {
|
||||
pin.Lock()
|
||||
defer pin.Unlock()
|
||||
|
||||
return time.Now().Before(pin.FailingUntil)
|
||||
}
|
||||
|
||||
// MarkAsFailingFor marks the pin as failing.
|
||||
// The Pin is locked for this.
|
||||
// Changes are pushed.
|
||||
func (pin *Pin) MarkAsFailingFor(duration time.Duration) {
|
||||
pin.Lock()
|
||||
defer pin.Unlock()
|
||||
|
||||
until := time.Now().Add(duration)
|
||||
// Only ever increase failing until, never reduce.
|
||||
if until.After(pin.FailingUntil) {
|
||||
pin.FailingUntil = until
|
||||
}
|
||||
|
||||
pin.addStates(StateFailing)
|
||||
|
||||
pin.pushChanges.Set()
|
||||
pin.pushChange()
|
||||
}
|
||||
|
||||
// ResetFailingState resets the failing state.
|
||||
// The Pin is locked for this.
|
||||
// Changes are not pushed, but Pins are marked.
|
||||
func (pin *Pin) ResetFailingState() {
|
||||
pin.Lock()
|
||||
defer pin.Unlock()
|
||||
|
||||
if time.Now().Before(pin.FailingUntil) {
|
||||
pin.FailingUntil = time.Now()
|
||||
pin.pushChanges.Set()
|
||||
}
|
||||
if pin.State.Has(StateFailing) {
|
||||
pin.removeStates(StateFailing)
|
||||
pin.pushChanges.Set()
|
||||
}
|
||||
}
|
||||
98
spn/navigator/pin_export.go
Normal file
98
spn/navigator/pin_export.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portmaster/service/intel"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
// PinExport is the exportable version of a Pin.
|
||||
type PinExport struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
ID string
|
||||
Name string
|
||||
Map string
|
||||
FirstSeen time.Time
|
||||
|
||||
EntityV4 *intel.Entity
|
||||
EntityV6 *intel.Entity
|
||||
// TODO: add coords
|
||||
|
||||
States []string // From pin.State
|
||||
VerifiedOwner string
|
||||
HopDistance int
|
||||
|
||||
ConnectedTo map[string]*LaneExport // Key is Hub ID.
|
||||
Route []string // Includes Home Hub and this Pin's ID.
|
||||
SessionActive bool
|
||||
|
||||
Info *hub.Announcement
|
||||
Status *hub.Status
|
||||
}
|
||||
|
||||
// LaneExport is the exportable version of a Lane.
|
||||
type LaneExport struct {
|
||||
HubID string
|
||||
|
||||
// Capacity designates the available bandwidth between these Hubs.
|
||||
// It is specified in bit/s.
|
||||
Capacity int
|
||||
|
||||
// Lateny designates the latency between these Hubs.
|
||||
// It is specified in nanoseconds.
|
||||
Latency time.Duration
|
||||
}
|
||||
|
||||
// Export puts the Pin's information into an exportable format.
|
||||
func (pin *Pin) Export() *PinExport {
|
||||
pin.Lock()
|
||||
defer pin.Unlock()
|
||||
|
||||
// Shallow copy static values.
|
||||
export := &PinExport{
|
||||
ID: pin.Hub.ID,
|
||||
Name: pin.Hub.Info.Name,
|
||||
Map: pin.Hub.Map,
|
||||
FirstSeen: pin.Hub.FirstSeen,
|
||||
EntityV4: pin.EntityV4,
|
||||
EntityV6: pin.EntityV6,
|
||||
States: pin.State.Export(),
|
||||
VerifiedOwner: pin.VerifiedOwner,
|
||||
HopDistance: pin.HopDistance,
|
||||
SessionActive: pin.hasActiveTerminal() || pin.State.Has(StateIsHomeHub),
|
||||
Info: pin.Hub.Info, // Is updated as a whole, no need to copy.
|
||||
Status: pin.Hub.Status, // Is updated as a whole, no need to copy.
|
||||
}
|
||||
|
||||
// Export lanes.
|
||||
export.ConnectedTo = make(map[string]*LaneExport, len(pin.ConnectedTo))
|
||||
for key, lane := range pin.ConnectedTo {
|
||||
export.ConnectedTo[key] = &LaneExport{
|
||||
HubID: lane.Pin.Hub.ID,
|
||||
Capacity: lane.Capacity,
|
||||
Latency: lane.Latency,
|
||||
}
|
||||
}
|
||||
|
||||
// Export route to Pin, if connected.
|
||||
if pin.Connection != nil && pin.Connection.Route != nil {
|
||||
export.Route = make([]string, len(pin.Connection.Route.Path))
|
||||
for key, hop := range pin.Connection.Route.Path {
|
||||
export.Route[key] = hop.HubID
|
||||
}
|
||||
}
|
||||
|
||||
// Create database record metadata.
|
||||
export.SetKey(makeDBKey(export.Map, export.ID))
|
||||
export.SetMeta(&record.Meta{
|
||||
Created: export.FirstSeen.Unix(),
|
||||
Modified: time.Now().Unix(),
|
||||
})
|
||||
|
||||
return export
|
||||
}
|
||||
231
spn/navigator/region.go
Normal file
231
spn/navigator/region.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/profile/endpoints"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRegionalMinLanesPerHub = 0.5
|
||||
defaultRegionalMaxLanesOnHub = 2
|
||||
defaultSatelliteMinLanesPerHub = 0.3
|
||||
defaultInternalMinLanesOnHub = 3
|
||||
defaultInternalMaxHops = 3
|
||||
)
|
||||
|
||||
// Region specifies a group of Hubs for optimization purposes.
|
||||
type Region struct {
|
||||
ID string
|
||||
Name string
|
||||
config *hub.RegionConfig
|
||||
memberPolicy endpoints.Endpoints
|
||||
|
||||
pins []*Pin
|
||||
regardedPins []*Pin
|
||||
|
||||
regionalMinLanes int
|
||||
regionalMaxLanesOnHub int
|
||||
satelliteMinLanes int
|
||||
internalMinLanesOnHub int
|
||||
internalMaxHops int
|
||||
}
|
||||
|
||||
func (region *Region) getName() string {
|
||||
switch {
|
||||
case region == nil:
|
||||
return "-"
|
||||
case region.Name != "":
|
||||
return region.Name
|
||||
default:
|
||||
return region.ID
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) updateRegions(config []*hub.RegionConfig) {
|
||||
// Reset map and pins.
|
||||
m.regions = make([]*Region, 0, len(config))
|
||||
for _, pin := range m.all {
|
||||
pin.region = nil
|
||||
}
|
||||
|
||||
// Stop if not regions are defined.
|
||||
if len(config) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Build regions from config.
|
||||
for _, regionConfig := range config {
|
||||
// Check if region has an ID.
|
||||
if regionConfig.ID == "" {
|
||||
log.Error("spn/navigator: region is missing ID")
|
||||
// Abort adding this region to the map.
|
||||
continue
|
||||
}
|
||||
|
||||
// Create new region.
|
||||
region := &Region{
|
||||
ID: regionConfig.ID,
|
||||
Name: regionConfig.Name,
|
||||
config: regionConfig,
|
||||
}
|
||||
|
||||
// Parse member policy.
|
||||
if len(regionConfig.MemberPolicy) == 0 {
|
||||
log.Errorf("spn/navigator: member policy of region %s is missing", region.ID)
|
||||
// Abort adding this region to the map.
|
||||
continue
|
||||
}
|
||||
memberPolicy, err := endpoints.ParseEndpoints(regionConfig.MemberPolicy)
|
||||
if err != nil {
|
||||
log.Errorf("spn/navigator: failed to parse member policy of region %s: %s", region.ID, err)
|
||||
// Abort adding this region to the map.
|
||||
continue
|
||||
}
|
||||
region.memberPolicy = memberPolicy
|
||||
|
||||
// Recalculate region properties.
|
||||
region.recalculateProperties()
|
||||
|
||||
// Add region to map.
|
||||
m.regions = append(m.regions, region)
|
||||
}
|
||||
|
||||
// Update region in all Pins.
|
||||
for _, pin := range m.all {
|
||||
m.updatePinRegion(pin)
|
||||
}
|
||||
}
|
||||
|
||||
func (region *Region) addPin(pin *Pin) {
|
||||
// Find pin in region.
|
||||
for _, regionPin := range region.pins {
|
||||
if pin.Hub.ID == regionPin.Hub.ID {
|
||||
// Pin is already part of region.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if pin is already part of this region.
|
||||
if pin.region != nil && pin.region.ID == region.ID {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove pin from previous region.
|
||||
if pin.region != nil {
|
||||
pin.region.removePin(pin)
|
||||
}
|
||||
|
||||
// Add new pin to region.
|
||||
region.pins = append(region.pins, pin)
|
||||
pin.region = region
|
||||
|
||||
// Recalculate region properties.
|
||||
region.recalculateProperties()
|
||||
}
|
||||
|
||||
func (region *Region) removePin(pin *Pin) {
|
||||
// Find pin index in region.
|
||||
removeIndex := -1
|
||||
for index, regionPin := range region.pins {
|
||||
if pin.Hub.ID == regionPin.Hub.ID {
|
||||
removeIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if removeIndex < 0 {
|
||||
// Pin is not part of region.
|
||||
return
|
||||
}
|
||||
|
||||
// Remove pin from region.
|
||||
region.pins = append(region.pins[:removeIndex], region.pins[removeIndex+1:]...)
|
||||
|
||||
// Recalculate region properties.
|
||||
region.recalculateProperties()
|
||||
}
|
||||
|
||||
func (region *Region) recalculateProperties() {
|
||||
// Regional properties.
|
||||
region.regionalMinLanes = calculateMinLanes(
|
||||
len(region.pins),
|
||||
region.config.RegionalMinLanes,
|
||||
region.config.RegionalMinLanesPerHub,
|
||||
defaultRegionalMinLanesPerHub,
|
||||
)
|
||||
region.regionalMaxLanesOnHub = region.config.RegionalMaxLanesOnHub
|
||||
if region.regionalMaxLanesOnHub <= 0 {
|
||||
region.regionalMaxLanesOnHub = defaultRegionalMaxLanesOnHub
|
||||
}
|
||||
|
||||
// Satellite properties.
|
||||
region.satelliteMinLanes = calculateMinLanes(
|
||||
len(region.pins),
|
||||
region.config.SatelliteMinLanes,
|
||||
region.config.SatelliteMinLanesPerHub,
|
||||
defaultSatelliteMinLanesPerHub,
|
||||
)
|
||||
|
||||
// Internal properties.
|
||||
region.internalMinLanesOnHub = region.config.InternalMinLanesOnHub
|
||||
if region.internalMinLanesOnHub <= 0 {
|
||||
region.internalMinLanesOnHub = defaultInternalMinLanesOnHub
|
||||
}
|
||||
region.internalMaxHops = region.config.InternalMaxHops
|
||||
if region.internalMaxHops <= 0 {
|
||||
region.internalMaxHops = defaultInternalMaxHops
|
||||
}
|
||||
// Values below 2 do not make any sense for max hops.
|
||||
if region.internalMaxHops < 2 {
|
||||
region.internalMaxHops = 2
|
||||
}
|
||||
}
|
||||
|
||||
func calculateMinLanes(regionHubCount, minLanes int, minLanesPerHub, defaultMinLanesPerHub float64) (minLaneCount int) {
|
||||
// Validate hub count.
|
||||
if regionHubCount <= 0 {
|
||||
// Reset to safe value.
|
||||
regionHubCount = 1
|
||||
}
|
||||
|
||||
// Set to configured minimum lanes.
|
||||
minLaneCount = minLanes
|
||||
|
||||
// Raise to configured minimum lanes per Hub.
|
||||
if minLanesPerHub != 0 {
|
||||
minLanesFromSize := int(math.Ceil(float64(regionHubCount) * minLanesPerHub))
|
||||
if minLanesFromSize > minLaneCount {
|
||||
minLaneCount = minLanesFromSize
|
||||
}
|
||||
}
|
||||
|
||||
// Raise to default minimum lanes per Hub, if still 0.
|
||||
if minLaneCount <= 0 {
|
||||
minLaneCount = int(math.Ceil(float64(regionHubCount) * defaultMinLanesPerHub))
|
||||
}
|
||||
|
||||
return minLaneCount
|
||||
}
|
||||
|
||||
func (m *Map) updatePinRegion(pin *Pin) {
|
||||
for _, region := range m.regions {
|
||||
// Check if pin matches the region's member policy.
|
||||
if pin.EntityV4 != nil {
|
||||
result, _ := region.memberPolicy.Match(context.TODO(), pin.EntityV4)
|
||||
if result == endpoints.Permitted {
|
||||
region.addPin(pin)
|
||||
return
|
||||
}
|
||||
}
|
||||
if pin.EntityV6 != nil {
|
||||
result, _ := region.memberPolicy.Match(context.TODO(), pin.EntityV6)
|
||||
if result == endpoints.Permitted {
|
||||
region.addPin(pin)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
221
spn/navigator/route.go
Normal file
221
spn/navigator/route.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Routes holds a collection of Routes.
|
||||
type Routes struct {
|
||||
All []*Route
|
||||
randomizeTopPercent float32
|
||||
maxCost float32 // automatic
|
||||
maxRoutes int // manual setting
|
||||
}
|
||||
|
||||
// Len is the number of elements in the collection.
|
||||
func (r *Routes) Len() int {
|
||||
return len(r.All)
|
||||
}
|
||||
|
||||
// Less reports whether the element with index i should sort before the element
|
||||
// with index j.
|
||||
func (r *Routes) Less(i, j int) bool {
|
||||
return r.All[i].TotalCost < r.All[j].TotalCost
|
||||
}
|
||||
|
||||
// Swap swaps the elements with indexes i and j.
|
||||
func (r *Routes) Swap(i, j int) {
|
||||
r.All[i], r.All[j] = r.All[j], r.All[i]
|
||||
}
|
||||
|
||||
// isGoodEnough reports whether the route would survive a clean process.
|
||||
func (r *Routes) isGoodEnough(route *Route) bool {
|
||||
if r.maxCost > 0 && route.TotalCost > r.maxCost {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// add adds a Route if it is good enough.
|
||||
func (r *Routes) add(route *Route) {
|
||||
if !r.isGoodEnough(route) {
|
||||
return
|
||||
}
|
||||
r.All = append(r.All, route.CopyUpTo(0))
|
||||
r.clean()
|
||||
}
|
||||
|
||||
// clean sort and shortens the list to the configured maximum.
|
||||
func (r *Routes) clean() {
|
||||
// Sort Routes so that the best ones are on top.
|
||||
sort.Sort(r)
|
||||
// Remove all remaining from the list.
|
||||
if len(r.All) > r.maxRoutes {
|
||||
r.All = r.All[:r.maxRoutes]
|
||||
}
|
||||
// Set new maximum total cost.
|
||||
if len(r.All) >= r.maxRoutes {
|
||||
r.maxCost = r.All[len(r.All)-1].TotalCost
|
||||
}
|
||||
}
|
||||
|
||||
// randomizeTop randomized to the top nearest pins for balancing the network.
|
||||
func (r *Routes) randomizeTop() {
|
||||
switch {
|
||||
case r.randomizeTopPercent == 0:
|
||||
// Check if randomization is enabled.
|
||||
return
|
||||
case len(r.All) < 2:
|
||||
// Check if we have enough pins to work with.
|
||||
return
|
||||
}
|
||||
|
||||
// Find randomization set.
|
||||
randomizeUpTo := len(r.All)
|
||||
threshold := r.All[0].TotalCost * (1 + r.randomizeTopPercent)
|
||||
for i, r := range r.All {
|
||||
// Find first value above the threshold to stop.
|
||||
if r.TotalCost > threshold {
|
||||
randomizeUpTo = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle top set.
|
||||
if randomizeUpTo >= 2 {
|
||||
mr := mrand.New(mrand.NewSource(time.Now().UnixNano())) //nolint:gosec
|
||||
mr.Shuffle(randomizeUpTo, r.Swap)
|
||||
}
|
||||
}
|
||||
|
||||
// Route is a path through the map.
|
||||
type Route struct {
|
||||
// Path is a list of Transit Hubs and the Destination Hub, including the Cost
|
||||
// for each Hop.
|
||||
Path []*Hop
|
||||
|
||||
// DstCost is the calculated cost between the Destination Hub and the destination IP.
|
||||
DstCost float32
|
||||
|
||||
// TotalCost is the sum of all costs of this Route.
|
||||
TotalCost float32
|
||||
|
||||
// Algorithm is the ID of the algorithm used to calculate the route.
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
// Hop is one hop of a route's path.
|
||||
type Hop struct {
|
||||
pin *Pin
|
||||
|
||||
// HubID is the Hub ID.
|
||||
HubID string
|
||||
|
||||
// Cost is the cost for both Lane to this Hub and the Hub itself.
|
||||
Cost float32
|
||||
}
|
||||
|
||||
// addHop adds a hop to the route.
|
||||
func (r *Route) addHop(pin *Pin, cost float32) {
|
||||
r.Path = append(r.Path, &Hop{
|
||||
pin: pin,
|
||||
Cost: cost,
|
||||
})
|
||||
r.recalculateTotalCost()
|
||||
}
|
||||
|
||||
// completeRoute completes the route by adding the destination cost of the
|
||||
// connection between the last hop and the destination IP.
|
||||
func (r *Route) completeRoute(dstCost float32) {
|
||||
r.DstCost = dstCost
|
||||
r.recalculateTotalCost()
|
||||
}
|
||||
|
||||
// removeHop removes the last hop from the Route.
|
||||
func (r *Route) removeHop() {
|
||||
// Reset DstCost, as the route might have been completed.
|
||||
r.DstCost = 0
|
||||
|
||||
if len(r.Path) >= 1 {
|
||||
r.Path = r.Path[:len(r.Path)-1]
|
||||
}
|
||||
r.recalculateTotalCost()
|
||||
}
|
||||
|
||||
// recalculateTotalCost recalculates to total cost of this route.
|
||||
func (r *Route) recalculateTotalCost() {
|
||||
r.TotalCost = r.DstCost
|
||||
for _, hop := range r.Path {
|
||||
if hop.pin.HasActiveTerminal() {
|
||||
// If we have an active connection, only take 80% of the cost.
|
||||
r.TotalCost += hop.Cost * 0.8
|
||||
} else {
|
||||
r.TotalCost += hop.Cost
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CopyUpTo makes a somewhat deep copy of the Route up to the specified amount
|
||||
// and returns it. Hops themselves are not copied, because their data does not
|
||||
// change. Therefore, returned Hops may not be edited.
|
||||
// Specify an amount of 0 to copy all.
|
||||
func (r *Route) CopyUpTo(n int) *Route {
|
||||
// Check amount.
|
||||
if n == 0 || n > len(r.Path) {
|
||||
n = len(r.Path)
|
||||
}
|
||||
|
||||
newRoute := &Route{
|
||||
Path: make([]*Hop, n),
|
||||
DstCost: r.DstCost,
|
||||
TotalCost: r.TotalCost,
|
||||
}
|
||||
copy(newRoute.Path, r.Path)
|
||||
return newRoute
|
||||
}
|
||||
|
||||
// makeExportReady fills in all the missing data fields which are meant for
|
||||
// exporting only.
|
||||
func (r *Routes) makeExportReady(algorithm string) {
|
||||
for _, route := range r.All {
|
||||
route.makeExportReady(algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
// makeExportReady fills in all the missing data fields which are meant for
|
||||
// exporting only.
|
||||
func (r *Route) makeExportReady(algorithm string) {
|
||||
r.Algorithm = algorithm
|
||||
for _, hop := range r.Path {
|
||||
hop.makeExportReady()
|
||||
}
|
||||
}
|
||||
|
||||
// makeExportReady fills in all the missing data fields which are meant for
|
||||
// exporting only.
|
||||
func (hop *Hop) makeExportReady() {
|
||||
hop.HubID = hop.pin.Hub.ID
|
||||
}
|
||||
|
||||
// Pin returns the Pin of the Hop.
|
||||
func (hop *Hop) Pin() *Pin {
|
||||
return hop.pin
|
||||
}
|
||||
|
||||
func (r *Route) String() string {
|
||||
s := make([]string, 0, len(r.Path)+2)
|
||||
s = append(s, fmt.Sprintf("route with %.2fc:", r.TotalCost))
|
||||
for i, hop := range r.Path {
|
||||
if i == 0 {
|
||||
s = append(s, hop.pin.String())
|
||||
} else {
|
||||
s = append(s, fmt.Sprintf("--> %.2fc %s", hop.Cost, hop.pin))
|
||||
}
|
||||
}
|
||||
s = append(s, fmt.Sprintf("--> %.2fc", r.DstCost))
|
||||
return strings.Join(s, " ")
|
||||
}
|
||||
162
spn/navigator/routing-profiles.go
Normal file
162
spn/navigator/routing-profiles.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
)
|
||||
|
||||
// RoutingProfile defines a routing algorithm with some options.
|
||||
type RoutingProfile struct {
|
||||
ID string
|
||||
|
||||
// Name is the human readable name of the profile.
|
||||
Name string
|
||||
|
||||
// MinHops defines how many hops a route must have at minimum. In order to
|
||||
// reduce confusion, the Home Hub is also counted.
|
||||
MinHops int
|
||||
|
||||
// MaxHops defines the limit on how many hops a route may have. In order to
|
||||
// reduce confusion, the Home Hub is also counted.
|
||||
MaxHops int
|
||||
|
||||
// MaxExtraHops sets a limit on how many extra hops are allowed in addition
|
||||
// to the amount of Hops in the currently best route. This is an optimization
|
||||
// option and should not interfere with finding the best route, but might
|
||||
// reduce the amount of routes found.
|
||||
MaxExtraHops int
|
||||
|
||||
// MaxExtraCost sets a limit on the extra cost allowed in addition to the
|
||||
// cost of the currently best route. This is an optimization option and
|
||||
// should not interfere with finding the best route, but might reduce the
|
||||
// amount of routes found.
|
||||
MaxExtraCost float32
|
||||
}
|
||||
|
||||
// Routing Profile Names.
|
||||
const (
|
||||
RoutingProfileHomeID = "home"
|
||||
RoutingProfileSingleHopID = "single-hop"
|
||||
RoutingProfileDoubleHopID = "double-hop"
|
||||
RoutingProfileTripleHopID = "triple-hop"
|
||||
)
|
||||
|
||||
// Routing Profiles.
|
||||
var (
|
||||
DefaultRoutingProfileID = profile.DefaultRoutingProfileID
|
||||
|
||||
RoutingProfileHome = &RoutingProfile{
|
||||
ID: "home",
|
||||
Name: "Plain VPN Mode",
|
||||
MinHops: 1,
|
||||
MaxHops: 1,
|
||||
}
|
||||
RoutingProfileSingleHop = &RoutingProfile{
|
||||
ID: "single-hop",
|
||||
Name: "Speed Focused",
|
||||
MinHops: 1,
|
||||
MaxHops: 3,
|
||||
MaxExtraHops: 1,
|
||||
MaxExtraCost: 10000,
|
||||
}
|
||||
RoutingProfileDoubleHop = &RoutingProfile{
|
||||
ID: "double-hop",
|
||||
Name: "Balanced",
|
||||
MinHops: 2,
|
||||
MaxHops: 4,
|
||||
MaxExtraHops: 2,
|
||||
MaxExtraCost: 10000,
|
||||
}
|
||||
RoutingProfileTripleHop = &RoutingProfile{
|
||||
ID: "triple-hop",
|
||||
Name: "Privacy Focused",
|
||||
MinHops: 3,
|
||||
MaxHops: 5,
|
||||
MaxExtraHops: 3,
|
||||
MaxExtraCost: 10000,
|
||||
}
|
||||
)
|
||||
|
||||
// GetRoutingProfile returns the routing profile with the given ID.
|
||||
func GetRoutingProfile(id string) *RoutingProfile {
|
||||
switch id {
|
||||
case RoutingProfileHomeID:
|
||||
return RoutingProfileHome
|
||||
case RoutingProfileSingleHopID:
|
||||
return RoutingProfileSingleHop
|
||||
case RoutingProfileDoubleHopID:
|
||||
return RoutingProfileDoubleHop
|
||||
case RoutingProfileTripleHopID:
|
||||
return RoutingProfileTripleHop
|
||||
default:
|
||||
return RoutingProfileDoubleHop
|
||||
}
|
||||
}
|
||||
|
||||
type routeCompliance uint8
|
||||
|
||||
const (
|
||||
routeOk routeCompliance = iota // Route is fully compliant and can be used.
|
||||
routeNonCompliant // Route is not compliant, but this might change if more hops are added.
|
||||
routeDisqualified // Route is disqualified and won't be able to become compliant.
|
||||
)
|
||||
|
||||
func (rp *RoutingProfile) checkRouteCompliance(route *Route, foundRoutes *Routes) routeCompliance {
|
||||
switch {
|
||||
case len(route.Path) < rp.MinHops:
|
||||
// Route is shorter than the defined minimum.
|
||||
return routeNonCompliant
|
||||
case len(route.Path) > rp.MaxHops:
|
||||
// Route is longer than the defined maximum.
|
||||
return routeDisqualified
|
||||
}
|
||||
|
||||
// Check for hub re-use.
|
||||
if len(route.Path) >= 2 {
|
||||
lastHop := route.Path[len(route.Path)-1]
|
||||
for _, hop := range route.Path[:len(route.Path)-1] {
|
||||
if lastHop.pin.Hub.ID == hop.pin.Hub.ID {
|
||||
return routeDisqualified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if hub is already in use, if so check if the route matches.
|
||||
if len(route.Path) >= 2 {
|
||||
// Get active connection to the last pin of the current path.
|
||||
lastPinConnection := route.Path[len(route.Path)-1].pin.Connection
|
||||
|
||||
switch {
|
||||
case lastPinConnection == nil:
|
||||
// Last pin is not yet connected.
|
||||
case len(lastPinConnection.Route.Path) < 2:
|
||||
// Path of last pin does not have enough hops.
|
||||
// This is unexpected and should not happen.
|
||||
log.Errorf(
|
||||
"navigator: expected active connection to %s to have 2 hops or more on path, but it had %d",
|
||||
route.Path[len(route.Path)-1].pin.Hub.StringWithoutLocking(),
|
||||
len(lastPinConnection.Route.Path),
|
||||
)
|
||||
case lastPinConnection.Route.Path[len(lastPinConnection.Route.Path)-2].pin.Hub.ID != route.Path[len(route.Path)-2].pin.Hub.ID:
|
||||
// The previous hop of the existing route and the one we are evaluating don't match.
|
||||
// Currently, we only allow one session per Hub.
|
||||
return routeDisqualified
|
||||
}
|
||||
}
|
||||
|
||||
// Abort route exploration when we are outside the optimization boundaries.
|
||||
if len(foundRoutes.All) > 0 {
|
||||
// Get the best found route.
|
||||
best := foundRoutes.All[0]
|
||||
// Abort if current route exceeds max extra costs.
|
||||
if route.TotalCost > best.TotalCost+rp.MaxExtraCost {
|
||||
return routeDisqualified
|
||||
}
|
||||
// Abort if current route exceeds max extra hops.
|
||||
if len(route.Path) > len(best.Path)+rp.MaxExtraHops {
|
||||
return routeDisqualified
|
||||
}
|
||||
}
|
||||
|
||||
return routeOk
|
||||
}
|
||||
141
spn/navigator/sort.go
Normal file
141
spn/navigator/sort.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package navigator
|
||||
|
||||
type sortByPinID []*Pin
|
||||
|
||||
func (a sortByPinID) Len() int { return len(a) }
|
||||
func (a sortByPinID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortByPinID) Less(i, j int) bool { return a[i].Hub.ID < a[j].Hub.ID }
|
||||
|
||||
type sortByLowestMeasuredCost []*Pin
|
||||
|
||||
func (a sortByLowestMeasuredCost) Len() int { return len(a) }
|
||||
func (a sortByLowestMeasuredCost) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortByLowestMeasuredCost) Less(i, j int) bool {
|
||||
x := a[i].measurements.GetCalculatedCost()
|
||||
y := a[j].measurements.GetCalculatedCost()
|
||||
if x != y {
|
||||
return x < y
|
||||
}
|
||||
|
||||
// Fall back to geo proximity.
|
||||
gx := a[i].measurements.GetGeoProximity()
|
||||
gy := a[j].measurements.GetGeoProximity()
|
||||
if gx != gy {
|
||||
return gx > gy
|
||||
}
|
||||
|
||||
// Fall back to Hub ID.
|
||||
return a[i].Hub.ID < a[j].Hub.ID
|
||||
}
|
||||
|
||||
type sortBySuggestedHopDistanceAndLowestMeasuredCost []*Pin
|
||||
|
||||
func (a sortBySuggestedHopDistanceAndLowestMeasuredCost) Len() int { return len(a) }
|
||||
func (a sortBySuggestedHopDistanceAndLowestMeasuredCost) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortBySuggestedHopDistanceAndLowestMeasuredCost) Less(i, j int) bool {
|
||||
// First sort by suggested hop distance.
|
||||
if a[i].analysis.SuggestedHopDistance != a[j].analysis.SuggestedHopDistance {
|
||||
return a[i].analysis.SuggestedHopDistance > a[j].analysis.SuggestedHopDistance
|
||||
}
|
||||
|
||||
// Then by cost.
|
||||
x := a[i].measurements.GetCalculatedCost()
|
||||
y := a[j].measurements.GetCalculatedCost()
|
||||
if x != y {
|
||||
return x < y
|
||||
}
|
||||
|
||||
// Fall back to geo proximity.
|
||||
gx := a[i].measurements.GetGeoProximity()
|
||||
gy := a[j].measurements.GetGeoProximity()
|
||||
if gx != gy {
|
||||
return gx > gy
|
||||
}
|
||||
|
||||
// Fall back to Hub ID.
|
||||
return a[i].Hub.ID < a[j].Hub.ID
|
||||
}
|
||||
|
||||
type sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost []*Pin
|
||||
|
||||
func (a sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost) Len() int { return len(a) }
|
||||
func (a sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost) Less(i, j int) bool {
|
||||
// First sort by suggested hop distance.
|
||||
if a[i].analysis.SuggestedHopDistanceInRegion != a[j].analysis.SuggestedHopDistanceInRegion {
|
||||
return a[i].analysis.SuggestedHopDistanceInRegion > a[j].analysis.SuggestedHopDistanceInRegion
|
||||
}
|
||||
|
||||
// Then by cost.
|
||||
x := a[i].measurements.GetCalculatedCost()
|
||||
y := a[j].measurements.GetCalculatedCost()
|
||||
if x != y {
|
||||
return x < y
|
||||
}
|
||||
|
||||
// Fall back to geo proximity.
|
||||
gx := a[i].measurements.GetGeoProximity()
|
||||
gy := a[j].measurements.GetGeoProximity()
|
||||
if gx != gy {
|
||||
return gx > gy
|
||||
}
|
||||
|
||||
// Fall back to Hub ID.
|
||||
return a[i].Hub.ID < a[j].Hub.ID
|
||||
}
|
||||
|
||||
type sortByLowestMeasuredLatency []*Pin
|
||||
|
||||
func (a sortByLowestMeasuredLatency) Len() int { return len(a) }
|
||||
func (a sortByLowestMeasuredLatency) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortByLowestMeasuredLatency) Less(i, j int) bool {
|
||||
x, _ := a[i].measurements.GetLatency()
|
||||
y, _ := a[j].measurements.GetLatency()
|
||||
switch {
|
||||
case x == y:
|
||||
// Go to fallbacks.
|
||||
case x == 0:
|
||||
// Ignore zero values.
|
||||
return false // j/y is better.
|
||||
case y == 0:
|
||||
// Ignore zero values.
|
||||
return true // i/x is better.
|
||||
default:
|
||||
return x < y
|
||||
}
|
||||
|
||||
// Fall back to geo proximity.
|
||||
gx := a[i].measurements.GetGeoProximity()
|
||||
gy := a[j].measurements.GetGeoProximity()
|
||||
if gx != gy {
|
||||
return gx > gy
|
||||
}
|
||||
|
||||
// Fall back to Hub ID.
|
||||
return a[i].Hub.ID < a[j].Hub.ID
|
||||
}
|
||||
|
||||
type sortByHighestMeasuredCapacity []*Pin
|
||||
|
||||
func (a sortByHighestMeasuredCapacity) Len() int { return len(a) }
|
||||
func (a sortByHighestMeasuredCapacity) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortByHighestMeasuredCapacity) Less(i, j int) bool {
|
||||
x, _ := a[i].measurements.GetCapacity()
|
||||
y, _ := a[j].measurements.GetCapacity()
|
||||
if x != y {
|
||||
return x > y
|
||||
}
|
||||
|
||||
// Fall back to geo proximity.
|
||||
gx := a[i].measurements.GetGeoProximity()
|
||||
gy := a[j].measurements.GetGeoProximity()
|
||||
if gx != gy {
|
||||
return gx > gy
|
||||
}
|
||||
|
||||
// Fall back to Hub ID.
|
||||
return a[i].Hub.ID < a[j].Hub.ID
|
||||
}
|
||||
112
spn/navigator/sort_test.go
Normal file
112
spn/navigator/sort_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
func TestSorting(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
list := []*Pin{
|
||||
{
|
||||
Hub: &hub.Hub{
|
||||
ID: "a",
|
||||
},
|
||||
measurements: &hub.Measurements{
|
||||
Latency: 3,
|
||||
Capacity: 4,
|
||||
CalculatedCost: 5,
|
||||
},
|
||||
analysis: &AnalysisState{
|
||||
SuggestedHopDistance: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
Hub: &hub.Hub{
|
||||
ID: "b",
|
||||
},
|
||||
measurements: &hub.Measurements{
|
||||
Latency: 4,
|
||||
Capacity: 3,
|
||||
CalculatedCost: 1,
|
||||
},
|
||||
analysis: &AnalysisState{
|
||||
SuggestedHopDistance: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
Hub: &hub.Hub{
|
||||
ID: "c",
|
||||
},
|
||||
measurements: &hub.Measurements{
|
||||
Latency: 5,
|
||||
Capacity: 2,
|
||||
CalculatedCost: 2,
|
||||
},
|
||||
analysis: &AnalysisState{
|
||||
SuggestedHopDistance: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
Hub: &hub.Hub{
|
||||
ID: "d",
|
||||
},
|
||||
measurements: &hub.Measurements{
|
||||
Latency: 1,
|
||||
Capacity: 1,
|
||||
CalculatedCost: 3,
|
||||
},
|
||||
analysis: &AnalysisState{
|
||||
SuggestedHopDistance: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
Hub: &hub.Hub{
|
||||
ID: "e",
|
||||
},
|
||||
measurements: &hub.Measurements{
|
||||
Latency: 2,
|
||||
Capacity: 5,
|
||||
CalculatedCost: 4,
|
||||
},
|
||||
analysis: &AnalysisState{
|
||||
SuggestedHopDistance: 4,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sort.Sort(sortByLowestMeasuredCost(list))
|
||||
checkSorting(t, list, "b-c-d-e-a")
|
||||
|
||||
sort.Sort(sortBySuggestedHopDistanceAndLowestMeasuredCost(list))
|
||||
checkSorting(t, list, "c-d-e-a-b")
|
||||
|
||||
sort.Sort(sortByLowestMeasuredLatency(list))
|
||||
checkSorting(t, list, "d-e-a-b-c")
|
||||
|
||||
sort.Sort(sortByHighestMeasuredCapacity(list))
|
||||
checkSorting(t, list, "e-a-b-c-d")
|
||||
|
||||
sort.Sort(sortByPinID(list))
|
||||
checkSorting(t, list, "a-b-c-d-e")
|
||||
}
|
||||
|
||||
func checkSorting(t *testing.T, sortedList []*Pin, expectedOrder string) {
|
||||
t.Helper()
|
||||
|
||||
// Build list ID string.
|
||||
ids := make([]string, 0, len(sortedList))
|
||||
for _, pin := range sortedList {
|
||||
ids = append(ids, pin.Hub.ID)
|
||||
}
|
||||
sortedIDs := strings.Join(ids, "-")
|
||||
|
||||
// Check for matching order.
|
||||
assert.Equal(t, expectedOrder, sortedIDs, "should match")
|
||||
}
|
||||
426
spn/navigator/state.go
Normal file
426
spn/navigator/state.go
Normal file
@@ -0,0 +1,426 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PinState holds a bit-mapped collection of Pin states, or a single state used
|
||||
// for assigment and matching.
|
||||
type PinState uint16
|
||||
|
||||
const (
|
||||
// StateNone represents an empty state.
|
||||
StateNone PinState = 0
|
||||
|
||||
// Negative States.
|
||||
|
||||
// StateInvalid signifies that there was an error while processing or
|
||||
// handling this Hub.
|
||||
StateInvalid PinState = 1 << (iota - 1) // 1 << 0 => 00000001 => 0x01
|
||||
|
||||
// StateSuperseded signifies that this Hub was superseded by another. This is
|
||||
// the case if any other Hub with a matching IP was verified after this one.
|
||||
// Verification timestamp equals Hub.FirstSeen.
|
||||
StateSuperseded // 0x02
|
||||
|
||||
// StateFailing signifies that a recent error was encountered while
|
||||
// communicating with this Hub. Pin.FailingUntil specifies when this state is
|
||||
// re-evaluated at earliest.
|
||||
StateFailing // 0x04
|
||||
|
||||
// StateOffline signifies that the Hub is offline.
|
||||
StateOffline // 0x08
|
||||
|
||||
// Positive States.
|
||||
|
||||
// StateHasRequiredInfo signifies that the Hub announces the minimum required
|
||||
// information about itself.
|
||||
StateHasRequiredInfo // 0x10
|
||||
|
||||
// StateReachable signifies that the Hub is reachable via the network from
|
||||
// the currently connected primary Hub.
|
||||
StateReachable // 0x20
|
||||
|
||||
// StateActive signifies that everything seems fine with the Hub and
|
||||
// connections to it should succeed. This is tested by checking if a valid
|
||||
// semi-ephemeral public key is available.
|
||||
StateActive // 0x40
|
||||
|
||||
_ // 0x80: Reserved
|
||||
|
||||
// Trust and Advisory States.
|
||||
|
||||
// StateTrusted signifies the Hub has the special trusted status.
|
||||
StateTrusted // 0x0100
|
||||
|
||||
// StateUsageDiscouraged signifies that usage of the Hub is discouraged for any task.
|
||||
StateUsageDiscouraged // 0x0200
|
||||
|
||||
// StateUsageAsHomeDiscouraged signifies that usage of the Hub as a Home Hub is discouraged.
|
||||
StateUsageAsHomeDiscouraged // 0x0400
|
||||
|
||||
// StateUsageAsDestinationDiscouraged signifies that usage of the Hub as a Destination Hub is discouraged.
|
||||
StateUsageAsDestinationDiscouraged // 0x0800
|
||||
|
||||
// Special States.
|
||||
|
||||
// StateIsHomeHub signifies that the Hub is the current Home Hub. While not
|
||||
// negative in itself, selecting the Home Hub does not make sense in almost
|
||||
// all cases.
|
||||
StateIsHomeHub // 0x1000
|
||||
|
||||
// StateConnectivityIssues signifies that the Hub reports connectivity issues.
|
||||
// This might impact all connectivity or just some.
|
||||
// This does not invalidate the Hub for all operations and not in all cases.
|
||||
StateConnectivityIssues // 0x2000
|
||||
|
||||
// StateAllowUnencrypted signifies that the Hub is available to handle unencrypted connections.
|
||||
StateAllowUnencrypted // 0x4000
|
||||
|
||||
// State Summaries.
|
||||
|
||||
// StateSummaryRegard summarizes all states that must always be set in order to take a Hub into consideration for any task.
|
||||
// TODO: Add StateHasRequiredInfo when we start enforcing Hub information.
|
||||
StateSummaryRegard = StateReachable | StateActive
|
||||
|
||||
// StateSummaryDisregard summarizes all states that must not be set in order to take a Hub into consideration for any task.
|
||||
StateSummaryDisregard = StateInvalid |
|
||||
StateSuperseded |
|
||||
StateFailing |
|
||||
StateOffline |
|
||||
StateUsageDiscouraged |
|
||||
StateIsHomeHub
|
||||
)
|
||||
|
||||
var allStates = []PinState{
|
||||
StateInvalid,
|
||||
StateSuperseded,
|
||||
StateFailing,
|
||||
StateOffline,
|
||||
StateHasRequiredInfo,
|
||||
StateReachable,
|
||||
StateActive,
|
||||
StateTrusted,
|
||||
StateUsageDiscouraged,
|
||||
StateUsageAsHomeDiscouraged,
|
||||
StateUsageAsDestinationDiscouraged,
|
||||
StateIsHomeHub,
|
||||
StateConnectivityIssues,
|
||||
StateAllowUnencrypted,
|
||||
}
|
||||
|
||||
// Add returns a new PinState with the given states added.
|
||||
func (pinState PinState) Add(states PinState) PinState {
|
||||
// OR:
|
||||
// 0011
|
||||
// | 0101
|
||||
// = 0111
|
||||
return pinState | states
|
||||
}
|
||||
|
||||
// Remove returns a new PinState with the given states removed.
|
||||
func (pinState PinState) Remove(states PinState) PinState {
|
||||
// AND NOT:
|
||||
// 0011
|
||||
// &^ 0101
|
||||
// = 0010
|
||||
return pinState &^ states
|
||||
}
|
||||
|
||||
// Has returns whether the state has all of the given states.
|
||||
func (pinState PinState) Has(states PinState) bool {
|
||||
// AND:
|
||||
// 0011
|
||||
// & 0101
|
||||
// = 0001
|
||||
|
||||
return pinState&states == states
|
||||
}
|
||||
|
||||
// HasAnyOf returns whether the state has any of the given states.
|
||||
func (pinState PinState) HasAnyOf(states PinState) bool {
|
||||
// AND:
|
||||
// 0011
|
||||
// & 0101
|
||||
// = 0001
|
||||
|
||||
return (pinState & states) != 0
|
||||
}
|
||||
|
||||
// HasNoneOf returns whether the state does not have any of the given states.
|
||||
func (pinState PinState) HasNoneOf(states PinState) bool {
|
||||
// AND:
|
||||
// 0011
|
||||
// & 0101
|
||||
// = 0001
|
||||
|
||||
return (pinState & states) == 0
|
||||
}
|
||||
|
||||
// addStates adds the given states on the Pin.
|
||||
func (pin *Pin) addStates(states PinState) {
|
||||
pin.State = pin.State.Add(states)
|
||||
}
|
||||
|
||||
// removeStates removes the given states on the Pin.
|
||||
func (pin *Pin) removeStates(states PinState) {
|
||||
pin.State = pin.State.Remove(states)
|
||||
}
|
||||
|
||||
func (m *Map) updateStateSuperseded(pin *Pin) {
|
||||
pin.removeStates(StateSuperseded)
|
||||
|
||||
// Update StateSuperseded
|
||||
// Iterate over all Pins in order to find a matching IP address.
|
||||
// In order to prevent false positive matching, we have to go through IPv4
|
||||
// and IPv6 separately.
|
||||
// TODO: This will not scale well beyond about 1000 Hubs.
|
||||
|
||||
// IPv4 Loop
|
||||
if pin.Hub.Info.IPv4 != nil {
|
||||
for _, mapPin := range m.all {
|
||||
// Skip Pin itself
|
||||
if mapPin.Hub.ID == pin.Hub.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for a matching IPv4 address.
|
||||
if mapPin.Hub.Info.IPv4 != nil && pin.Hub.Info.IPv4.Equal(mapPin.Hub.Info.IPv4) {
|
||||
continueChecking := checkAndHandleSuperseding(pin, mapPin)
|
||||
if !continueChecking {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IPv6 Loop
|
||||
if pin.Hub.Info.IPv6 != nil {
|
||||
for _, mapPin := range m.all {
|
||||
// Skip Pin itself
|
||||
if mapPin.Hub.ID == pin.Hub.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for a matching IPv6 address.
|
||||
if mapPin.Hub.Info.IPv6 != nil && pin.Hub.Info.IPv6.Equal(mapPin.Hub.Info.IPv6) {
|
||||
continueChecking := checkAndHandleSuperseding(pin, mapPin)
|
||||
if !continueChecking {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkAndHandleSuperseding(newPin, existingPin *Pin) (continueChecking bool) {
|
||||
const (
|
||||
supersedeNone = iota
|
||||
supersedeExisting
|
||||
supersedeNew
|
||||
)
|
||||
var action int
|
||||
|
||||
switch {
|
||||
case newPin.Hub.ID == existingPin.Hub.ID:
|
||||
// Cannot supersede same Hub.
|
||||
// Continue checking.
|
||||
action = supersedeNone
|
||||
|
||||
// Step 1: Check if only one is active.
|
||||
|
||||
case newPin.State.Has(StateActive) && existingPin.State.HasNoneOf(StateActive):
|
||||
// If only the new Hub is active, supersede the existing one.
|
||||
action = supersedeExisting
|
||||
case newPin.State.HasNoneOf(StateActive) && existingPin.State.Has(StateActive):
|
||||
// If only the existing Hub is active, supersede the new one.
|
||||
action = supersedeNew
|
||||
|
||||
// Step 2: Check if only one is reachable.
|
||||
|
||||
case newPin.State.Has(StateReachable) && existingPin.State.HasNoneOf(StateReachable):
|
||||
// If only the new Hub is reachable, supersede the existing one.
|
||||
action = supersedeExisting
|
||||
case newPin.State.HasNoneOf(StateReachable) && existingPin.State.Has(StateReachable):
|
||||
// If only the existing Hub is reachable, supersede the new one.
|
||||
action = supersedeNew
|
||||
|
||||
// Step 3: Check which one has been seen first.
|
||||
|
||||
case newPin.Hub.FirstSeen.After(existingPin.Hub.FirstSeen):
|
||||
// If the new Hub has been first seen later, supersede the existing one.
|
||||
action = supersedeExisting
|
||||
default:
|
||||
// If the existing Hub has been first seen later, supersede the new one.
|
||||
action = supersedeNew
|
||||
}
|
||||
|
||||
switch action {
|
||||
case supersedeExisting:
|
||||
existingPin.addStates(StateSuperseded)
|
||||
existingPin.pushChanges.Set()
|
||||
// Continue checking, as there might be other Hubs to be superseded.
|
||||
return true
|
||||
|
||||
case supersedeNew:
|
||||
newPin.addStates(StateSuperseded)
|
||||
newPin.pushChanges.Set()
|
||||
// If the new pin is superseded, do _not_ continue, as this will lead to an incorrect state.
|
||||
return false
|
||||
|
||||
case supersedeNone:
|
||||
fallthrough
|
||||
default:
|
||||
// Do nothing, continue checking.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (pin *Pin) updateStateHasRequiredInfo() {
|
||||
pin.removeStates(StateHasRequiredInfo)
|
||||
|
||||
// Check for required Hub Information.
|
||||
switch {
|
||||
case len(pin.Hub.Info.Name) == 0:
|
||||
case len(pin.Hub.Info.Group) == 0:
|
||||
case len(pin.Hub.Info.ContactAddress) == 0:
|
||||
case len(pin.Hub.Info.ContactService) == 0:
|
||||
case len(pin.Hub.Info.Hosters) == 0:
|
||||
case len(pin.Hub.Info.Hosters[0]) == 0:
|
||||
case len(pin.Hub.Info.Datacenter) == 0:
|
||||
default:
|
||||
pin.addStates(StateHasRequiredInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) updateActiveHubs() {
|
||||
now := time.Now().Unix()
|
||||
for _, pin := range m.all {
|
||||
pin.updateStateActive(now)
|
||||
}
|
||||
}
|
||||
|
||||
func (pin *Pin) updateStateActive(now int64) {
|
||||
pin.removeStates(StateActive)
|
||||
|
||||
// Check for active key.
|
||||
for _, key := range pin.Hub.Status.Keys {
|
||||
if now < key.Expires {
|
||||
pin.addStates(StateActive)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map) recalculateReachableHubs() error {
|
||||
if m.home == nil {
|
||||
return ErrHomeHubUnset
|
||||
}
|
||||
|
||||
// reset
|
||||
for _, pin := range m.all {
|
||||
pin.removeStates(StateReachable)
|
||||
pin.HopDistance = 0
|
||||
pin.pushChanges.Set()
|
||||
}
|
||||
|
||||
// find all connected Hubs
|
||||
m.home.markReachable(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pin *Pin) markReachable(hopDistance int) {
|
||||
switch {
|
||||
case !pin.State.Has(StateReachable):
|
||||
// Pin wasn't reachable before.
|
||||
case hopDistance < pin.HopDistance:
|
||||
// New path has a shorter distance.
|
||||
case pin.State.HasAnyOf(StateSummaryDisregard): //nolint:staticcheck
|
||||
// Ignore disregarded pins for reachability calculation.
|
||||
return
|
||||
default:
|
||||
// Pin is already reachable at same or better distance.
|
||||
return
|
||||
}
|
||||
|
||||
// Update reachability.
|
||||
pin.addStates(StateReachable)
|
||||
pin.HopDistance = hopDistance
|
||||
pin.pushChanges.Set()
|
||||
|
||||
// Propagate to connected Pins.
|
||||
hopDistance++
|
||||
for _, lane := range pin.ConnectedTo {
|
||||
lane.Pin.markReachable(hopDistance)
|
||||
}
|
||||
}
|
||||
|
||||
// Export returns a list of all state names.
|
||||
func (pinState PinState) Export() []string {
|
||||
// Check if there are no states.
|
||||
if pinState == StateNone {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect state names.
|
||||
var stateNames []string
|
||||
for _, state := range allStates {
|
||||
if pinState.Has(state) {
|
||||
stateNames = append(stateNames, state.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return stateNames
|
||||
}
|
||||
|
||||
// String returns the states as a human readable string.
|
||||
func (pinState PinState) String() string {
|
||||
stateNames := pinState.Export()
|
||||
if len(stateNames) == 0 {
|
||||
return "None"
|
||||
}
|
||||
|
||||
return strings.Join(stateNames, ", ")
|
||||
}
|
||||
|
||||
// Name returns the name of a single state flag.
|
||||
func (pinState PinState) Name() string {
|
||||
switch pinState {
|
||||
case StateNone:
|
||||
return "None"
|
||||
case StateInvalid:
|
||||
return "Invalid"
|
||||
case StateSuperseded:
|
||||
return "Superseded"
|
||||
case StateFailing:
|
||||
return "Failing"
|
||||
case StateOffline:
|
||||
return "Offline"
|
||||
case StateHasRequiredInfo:
|
||||
return "HasRequiredInfo"
|
||||
case StateReachable:
|
||||
return "Reachable"
|
||||
case StateActive:
|
||||
return "Active"
|
||||
case StateTrusted:
|
||||
return "Trusted"
|
||||
case StateUsageDiscouraged:
|
||||
return "UsageDiscouraged"
|
||||
case StateUsageAsHomeDiscouraged:
|
||||
return "UsageAsHomeDiscouraged"
|
||||
case StateUsageAsDestinationDiscouraged:
|
||||
return "UsageAsDestinationDiscouraged"
|
||||
case StateIsHomeHub:
|
||||
return "IsHomeHub"
|
||||
case StateConnectivityIssues:
|
||||
return "ConnectivityIssues"
|
||||
case StateAllowUnencrypted:
|
||||
return "AllowUnencrypted"
|
||||
case StateSummaryRegard, StateSummaryDisregard:
|
||||
// Satisfy exhaustive linter.
|
||||
fallthrough
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
31
spn/navigator/state_test.go
Normal file
31
spn/navigator/state_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := &Pin{}
|
||||
|
||||
p.addStates(StateInvalid | StateFailing | StateSuperseded)
|
||||
assert.Equal(t, StateInvalid|StateFailing|StateSuperseded, p.State)
|
||||
|
||||
p.removeStates(StateFailing | StateSuperseded)
|
||||
assert.Equal(t, StateInvalid, p.State)
|
||||
|
||||
p.addStates(StateTrusted | StateActive)
|
||||
assert.True(t, p.State.Has(StateInvalid|StateTrusted))
|
||||
assert.False(t, p.State.Has(StateInvalid|StateSuperseded))
|
||||
assert.True(t, p.State.HasAnyOf(StateInvalid|StateTrusted))
|
||||
assert.True(t, p.State.HasAnyOf(StateInvalid|StateSuperseded))
|
||||
assert.False(t, p.State.HasAnyOf(StateSuperseded|StateFailing))
|
||||
|
||||
assert.False(t, p.State.Has(StateSummaryRegard))
|
||||
assert.False(t, p.State.Has(StateSummaryDisregard))
|
||||
assert.True(t, p.State.HasAnyOf(StateSummaryRegard))
|
||||
assert.True(t, p.State.HasAnyOf(StateSummaryDisregard))
|
||||
}
|
||||
234
spn/navigator/testdata/main-intel.yml
vendored
Normal file
234
spn/navigator/testdata/main-intel.yml
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
---
|
||||
BootstrapHubs:
|
||||
- tcp://[2a01:4f8:172:3753::2]:17#Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC # fogos [DE]
|
||||
- tcp://[2a01:4f9:2a:d48::2]:17#Zwkwujs345P4ZygNZcEafawTqfZieCBVogQZ3xZPWiu7BU # heleus [FI]
|
||||
- tcp://138.201.140.70:17#Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC # fogos [DE]
|
||||
- tcp://95.216.13.61:17#Zwkwujs345P4ZygNZcEafawTqfZieCBVogQZ3xZPWiu7BU # heleus [FI]
|
||||
|
||||
Hubs:
|
||||
ZwhpYS1jWzXvPYKFhJqh1ZD3bKquLLoSoJ6RjeshmcXoFx: # voria [US]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Override:
|
||||
CountryCode: US
|
||||
Coordinates: # Ashburn, VA
|
||||
Latitude: 39.04
|
||||
Longitude: -77.48
|
||||
AccuracyRadius: 20
|
||||
ZwkAKBoyEd3PkE5RGDNmghahzHiBiTZA7Mg3XH7X3HjS39: # noru [US]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwkapJz5HFWpgd9PHsZLVueBu9PDmTJHKp382Wm9MB2EB7: # lovas [US]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Override:
|
||||
CountryCode: US
|
||||
Coordinates: # Los Angeles, CA
|
||||
Latitude: 34.03
|
||||
Longitude: -118.15
|
||||
AccuracyRadius: 20
|
||||
ZwkLShvVYvQFGmpY1MNhSSPXCktojywMVtv2N86mFbNH4w: # tooina [CA]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Zwkwujs345P4ZygNZcEafawTqfZieCBVogQZ3xZPWiu7BU: # heleus [FI]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Zwm72XieV6aeNKbwtJW8JdPUwT1hopQaLanLXjxcTfV3B9: # mergan [US]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Zwmp5SgUK9FidWBSCDK4d6dyRp3vhz3dQdwma1E4TMfiRw: # grenenia [FR]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Override:
|
||||
CountryCode: FR
|
||||
Coordinates: # Gravelines
|
||||
Latitude: 50.59
|
||||
Longitude: 2.07
|
||||
AccuracyRadius: 20
|
||||
ZwnFd1bSQrBegPZqFkS7DZU29x4PbojpFmTQFUnzQoicKp: # telos [IL]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Zwpg5FoXYVYidzgbdvDyvBBcrArmmHvK9nH3v7KDHiywtt: # melcor [PL]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwpsJpwngWyba54AbVkCawcRQ2HP37RRQAgj5LHNR2svRf: # soalis [AU]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Zwpy5hbrQkKznJwbUmn9WpJwGkpWD9VqE2pi9yfMDQM7PK: # rin9 [FR]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Override:
|
||||
CountryCode: FR
|
||||
Coordinates: # Strasbourg
|
||||
Latitude: 48.35
|
||||
Longitude: 7.45
|
||||
AccuracyRadius: 20
|
||||
ZwqANMrhcyJZb8cRMEd3FdPcXY7ZbvviPPfTUQpLNau12J: # sulkam [GB]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwwBspMhigqcEYv2cryipzJsi4vkHhnBqUmDmkJ2xizGFx: # surn [US]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Override:
|
||||
CountryCode: US
|
||||
Coordinates: # Seattle, WA
|
||||
Latitude: 47.36
|
||||
Longitude: -122.19
|
||||
AccuracyRadius: 20
|
||||
ZwsvsES3SHz1VLnwFPxDbW6DC8Esp1PiEtUHxGnm4BTYHt: # fungvis [DE]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC: # fogos [DS]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwtfvBuq5wkKYRth8rGCuGyp42nMe4doASUDJiDHJ8iucn: # vamalla [AT]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwtjwvdPxG4u7oB2zmNJFvsDy5VDLT9UArDkYDGfC9bkDt: # carros [US]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwvMZt6RcrrRuCdufjApnosxWbzsP8rTPRuHGeHu5KU241: # syniru [SG]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Override:
|
||||
CountryCode: SG
|
||||
Coordinates: # Singapore
|
||||
Latitude: 1.18
|
||||
Longitude: 103.50
|
||||
AccuracyRadius: 20
|
||||
ZwvyDLz8221fcSBw6GKZNDnwEn4YmE9m7JPieLUVe7iGR9: # calla [CA]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Zwvz9S6uyxn4ww1JGqJiisGMDmH2hz6mhwutmJXvTtwQww: # cidai [US]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwxJvZDZH18RUEQ3oFcR5uCqeXJaqkoi9P5Sj1aZ62HPin: # nutis [DE]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwvPQVFkoDbx3J6qThNwfLHZqwvFgUYLYtirCHVd7FfjBz: # perturn [CZ]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Override:
|
||||
CountryCode: CZ
|
||||
Coordinates: # Prague
|
||||
Latitude: 50.05
|
||||
Longitude: 14.25
|
||||
AccuracyRadius: 100
|
||||
Zwj52Q7d5ezvFk7HKB42dBtFu152bC9JasYF7BHB724RfG: # sono [NL]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwmhYMEmw36CzgVUp9sLjoK3gkVDWdMPiupEcekpTAXur8: # ivtos [TR]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
Override:
|
||||
CountryCode: TR
|
||||
Coordinates: # Izmir
|
||||
Latitude: 38.25
|
||||
Longitude: 27.90
|
||||
AccuracyRadius: 20
|
||||
Zwm9JX1hBNUUvSYc3gpMhmw84ay45SuyXE7D2UgETM7XCn: # porcania [PT]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
ZwxE83uRV9LcM8Bm3QjXjjejNRhBBBJAethPf14R6gcZwf: # steepeus [SE]
|
||||
Trusted: true
|
||||
VerifiedOwner: Safing
|
||||
|
||||
InfoOverrides:
|
||||
workaround:
|
||||
for: bug
|
||||
|
||||
AdviseOnlyTrustedHubs: false
|
||||
AdviseOnlyTrustedHomeHubs: true
|
||||
AdviseOnlyTrustedDestinationHubs: false
|
||||
|
||||
HomeHubAdvisory:
|
||||
- "- Zwj52Q7d5ezvFk7HKB42dBtFu152bC9JasYF7BHB724RfG" # sono [NL] is too slow for home hub
|
||||
- "- Zwm9JX1hBNUUvSYc3gpMhmw84ay45SuyXE7D2UgETM7XCn" # porcania [PT] is too slow for home hub
|
||||
- "- ZwmhYMEmw36CzgVUp9sLjoK3gkVDWdMPiupEcekpTAXur8" # ivtos [TR] is too slow for home hub
|
||||
- "- ZwvPQVFkoDbx3J6qThNwfLHZqwvFgUYLYtirCHVd7FfjBz" # perturn [CZ] is too slow for home hub
|
||||
|
||||
Regions:
|
||||
- ID: europe
|
||||
Name: Europe
|
||||
RegionalMinLanes: 5
|
||||
RegionalMinLanesPerHub: 0.7
|
||||
RegionalMaxLanesOnHub: 2
|
||||
SatelliteMinLanes: 2
|
||||
SatelliteMinLanesPerHub: 0.3
|
||||
InternalMinLanesOnHub: 3
|
||||
InternalMaxHops: 3
|
||||
MemberPolicy:
|
||||
- "+ AD"
|
||||
- "+ AL"
|
||||
- "+ AT"
|
||||
- "+ AX"
|
||||
- "+ BA"
|
||||
- "+ BE"
|
||||
- "+ BG"
|
||||
- "+ BY"
|
||||
- "+ CH"
|
||||
- "+ CZ"
|
||||
- "+ DE"
|
||||
- "+ DK"
|
||||
- "+ EE"
|
||||
- "+ ES"
|
||||
- "+ FI"
|
||||
- "+ FO"
|
||||
- "+ FR"
|
||||
- "+ GB"
|
||||
- "+ GG"
|
||||
- "+ GI"
|
||||
- "+ GR"
|
||||
- "+ HR"
|
||||
- "+ HU"
|
||||
- "+ IE"
|
||||
- "+ IM"
|
||||
- "+ IS"
|
||||
- "+ IT"
|
||||
- "+ JE"
|
||||
- "+ LI"
|
||||
- "+ LT"
|
||||
- "+ LU"
|
||||
- "+ LV"
|
||||
- "+ MC"
|
||||
- "+ MD"
|
||||
- "+ ME"
|
||||
- "+ MK"
|
||||
- "+ MT"
|
||||
- "+ NL"
|
||||
- "+ NO"
|
||||
- "+ PL"
|
||||
- "+ PT"
|
||||
- "+ RO"
|
||||
- "+ RS"
|
||||
- "+ RU"
|
||||
- "+ SE"
|
||||
- "+ SI"
|
||||
- "+ SJ"
|
||||
- "+ SK"
|
||||
- "+ SM"
|
||||
- "+ UA"
|
||||
- "+ VA"
|
||||
- ID: north-america
|
||||
Name: "North America"
|
||||
RegionalMinLanes: 5
|
||||
RegionalMinLanesPerHub: 0.7
|
||||
RegionalMaxLanesOnHub: 2
|
||||
SatelliteMinLanes: 2
|
||||
SatelliteMinLanesPerHub: 0.3
|
||||
InternalMinLanesOnHub: 3
|
||||
InternalMaxHops: 3
|
||||
MemberPolicy:
|
||||
- "+ BM"
|
||||
- "+ BZ"
|
||||
- "+ CA"
|
||||
- "+ CR"
|
||||
- "+ GL"
|
||||
- "+ GT"
|
||||
- "+ HN"
|
||||
- "+ MX"
|
||||
- "+ NI"
|
||||
- "+ PA"
|
||||
- "+ PM"
|
||||
- "+ SV"
|
||||
- "+ US"
|
||||
776
spn/navigator/update.go
Normal file
776
spn/navigator/update.go
Normal file
@@ -0,0 +1,776 @@
|
||||
package navigator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
var db = database.NewInterface(&database.Options{
|
||||
Local: true,
|
||||
Internal: true,
|
||||
})
|
||||
|
||||
// InitializeFromDatabase loads all Hubs from the given database prefix and adds them to the Map.
|
||||
func (m *Map) InitializeFromDatabase() error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
// start query for Hubs
|
||||
iter, err := db.Query(query.New(hub.MakeHubDBKey(m.Name, "")))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start query for initialization feed of %s map: %w", m.Name, err)
|
||||
}
|
||||
|
||||
// update navigator
|
||||
var hubCount int
|
||||
log.Tracef("spn/navigator: starting to initialize %s map from database", m.Name)
|
||||
for r := range iter.Next {
|
||||
h, err := hub.EnsureHub(r)
|
||||
if err != nil {
|
||||
log.Warningf("spn/navigator: could not parse hub %q while initializing %s map: %s", r.Key(), m.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
hubCount++
|
||||
m.updateHub(h, false, true)
|
||||
}
|
||||
switch {
|
||||
case iter.Err() != nil:
|
||||
return fmt.Errorf("failed to (fully) initialize %s map: %w", m.Name, iter.Err())
|
||||
case hubCount == 0:
|
||||
log.Warningf("spn/navigator: no hubs available for %s map - this is normal on first start", m.Name)
|
||||
default:
|
||||
log.Infof("spn/navigator: added %d hubs from database to %s map", hubCount, m.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateHook updates the a map from database changes.
|
||||
type UpdateHook struct {
|
||||
database.HookBase
|
||||
m *Map
|
||||
}
|
||||
|
||||
// UsesPrePut implements the Hook interface.
|
||||
func (hook *UpdateHook) UsesPrePut() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PrePut implements the Hook interface.
|
||||
func (hook *UpdateHook) PrePut(r record.Record) (record.Record, error) {
|
||||
// Remove deleted hubs from the map.
|
||||
if r.Meta().IsDeleted() {
|
||||
hook.m.RemoveHub(path.Base(r.Key()))
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Ensure we have a hub and update it in navigation map.
|
||||
h, err := hub.EnsureHub(r)
|
||||
if err != nil {
|
||||
log.Debugf("spn/navigator: record %s is not a hub", r.Key())
|
||||
} else {
|
||||
hook.m.updateHub(h, true, false)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// RegisterHubUpdateHook registers a database pre-put hook that updates all
|
||||
// Hubs saved at the given database prefix.
|
||||
func (m *Map) RegisterHubUpdateHook() (err error) {
|
||||
m.hubUpdateHook, err = database.RegisterHook(
|
||||
query.New(hub.MakeHubDBKey(m.Name, "")),
|
||||
&UpdateHook{m: m},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// CancelHubUpdateHook cancels the map's update hook.
|
||||
func (m *Map) CancelHubUpdateHook() {
|
||||
if m.hubUpdateHook != nil {
|
||||
if err := m.hubUpdateHook.Cancel(); err != nil {
|
||||
log.Warningf("spn/navigator: failed to cancel update hook for map %s: %s", m.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveHub removes a Hub from the Map.
|
||||
func (m *Map) RemoveHub(id string) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
// Get pin and remove it from the map, if it exists.
|
||||
pin, ok := m.all[id]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(m.all, id)
|
||||
|
||||
// Remove lanes from removed Pin.
|
||||
for id := range pin.ConnectedTo {
|
||||
// Remove Lane from peer.
|
||||
peer, ok := m.all[id]
|
||||
if ok {
|
||||
delete(peer.ConnectedTo, pin.Hub.ID)
|
||||
peer.pushChanges.Set()
|
||||
}
|
||||
}
|
||||
|
||||
// Push update to subscriptions.
|
||||
export := pin.Export()
|
||||
export.Meta().Delete()
|
||||
mapDBController.PushUpdate(export)
|
||||
// Push lane changes.
|
||||
m.PushPinChanges()
|
||||
}
|
||||
|
||||
// UpdateHub updates a Hub on the Map.
|
||||
func (m *Map) UpdateHub(h *hub.Hub) {
|
||||
m.updateHub(h, true, true)
|
||||
}
|
||||
|
||||
func (m *Map) updateHub(h *hub.Hub, lockMap, lockHub bool) {
|
||||
if lockMap {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
}
|
||||
if lockHub {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
}
|
||||
|
||||
// Hub requires both Info and Status to be added to the Map.
|
||||
if h.Info == nil || h.Status == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create or update Pin.
|
||||
pin, ok := m.all[h.ID]
|
||||
if ok {
|
||||
pin.Hub = h
|
||||
} else {
|
||||
pin = &Pin{
|
||||
Hub: h,
|
||||
ConnectedTo: make(map[string]*Lane),
|
||||
pushChanges: abool.New(),
|
||||
}
|
||||
m.all[h.ID] = pin
|
||||
}
|
||||
pin.pushChanges.Set()
|
||||
|
||||
// 1. Update Pin Data.
|
||||
|
||||
// Add/Update location data from IP addresses.
|
||||
pin.updateLocationData()
|
||||
|
||||
// Override Pin Data.
|
||||
m.updateInfoOverrides(pin)
|
||||
|
||||
// Update Hub cost.
|
||||
pin.Cost = CalculateHubCost(pin.Hub.Status.Load)
|
||||
|
||||
// Ensure measurements are set when enabled.
|
||||
if m.measuringEnabled && pin.measurements == nil {
|
||||
// Get shared measurements.
|
||||
pin.measurements = pin.Hub.GetMeasurementsWithLockedHub()
|
||||
|
||||
// Update cost calculation.
|
||||
latency, _ := pin.measurements.GetLatency()
|
||||
capacity, _ := pin.measurements.GetCapacity()
|
||||
pin.measurements.SetCalculatedCost(CalculateLaneCost(latency, capacity))
|
||||
|
||||
// Update geo proximity.
|
||||
// Get own location.
|
||||
var myLocation *geoip.Location
|
||||
switch {
|
||||
case m.home != nil && m.home.LocationV4 != nil:
|
||||
myLocation = m.home.LocationV4
|
||||
case m.home != nil && m.home.LocationV6 != nil:
|
||||
myLocation = m.home.LocationV6
|
||||
default:
|
||||
locations, ok := netenv.GetInternetLocation()
|
||||
if ok {
|
||||
myLocation = locations.Best().LocationOrNil()
|
||||
}
|
||||
}
|
||||
// Calculate proximity with available location.
|
||||
if myLocation != nil {
|
||||
switch {
|
||||
case pin.LocationV4 != nil:
|
||||
pin.measurements.SetGeoProximity(
|
||||
myLocation.EstimateNetworkProximity(pin.LocationV4),
|
||||
)
|
||||
case pin.LocationV6 != nil:
|
||||
pin.measurements.SetGeoProximity(
|
||||
myLocation.EstimateNetworkProximity(pin.LocationV6),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Update Pin States.
|
||||
|
||||
// Update the invalid status of the Pin.
|
||||
if pin.Hub.InvalidInfo || pin.Hub.InvalidStatus {
|
||||
pin.addStates(StateInvalid)
|
||||
} else {
|
||||
pin.removeStates(StateInvalid)
|
||||
}
|
||||
|
||||
// Update online status of the Pin.
|
||||
if pin.Hub.HasFlag(hub.FlagOffline) || pin.Hub.Status.Version == hub.VersionOffline {
|
||||
pin.addStates(StateOffline)
|
||||
} else {
|
||||
pin.removeStates(StateOffline)
|
||||
}
|
||||
|
||||
// Update online status of the Pin.
|
||||
if pin.Hub.HasFlag(hub.FlagAllowUnencrypted) {
|
||||
pin.addStates(StateAllowUnencrypted)
|
||||
} else {
|
||||
pin.removeStates(StateAllowUnencrypted)
|
||||
}
|
||||
|
||||
// Update from status flags.
|
||||
if pin.Hub.HasFlag(hub.FlagNetError) {
|
||||
pin.addStates(StateConnectivityIssues)
|
||||
} else {
|
||||
pin.removeStates(StateConnectivityIssues)
|
||||
}
|
||||
|
||||
// Update Trust and Advisory Statuses.
|
||||
m.updateIntelStatuses(pin, cfgOptionTrustNodeNodes())
|
||||
|
||||
// Update Statuses derived from Hub.
|
||||
pin.updateStateHasRequiredInfo()
|
||||
pin.updateStateActive(time.Now().Unix())
|
||||
|
||||
// 3. Update Lanes.
|
||||
|
||||
// Mark all existing Lanes as inactive.
|
||||
for _, lane := range pin.ConnectedTo {
|
||||
lane.active = false
|
||||
}
|
||||
|
||||
// Update Lanes (connections to other Hubs) from the Status.
|
||||
for _, lane := range pin.Hub.Status.Lanes {
|
||||
// Check if this is a Lane to itself.
|
||||
if lane.ID == pin.Hub.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
// First, get the Lane peer.
|
||||
peer, ok := m.all[lane.ID]
|
||||
if !ok {
|
||||
// We need to wait for peer to be added to the Map.
|
||||
continue
|
||||
}
|
||||
|
||||
m.updateHubLane(pin, lane, peer)
|
||||
}
|
||||
|
||||
// Remove all inactive/abandoned Lanes from both Pins.
|
||||
var removedLanes bool
|
||||
for id, lane := range pin.ConnectedTo {
|
||||
if !lane.active {
|
||||
// Remove Lane from this Pin.
|
||||
delete(pin.ConnectedTo, id)
|
||||
pin.pushChanges.Set()
|
||||
removedLanes = true
|
||||
// Remove Lane from peer.
|
||||
peer, ok := m.all[id]
|
||||
if ok {
|
||||
delete(peer.ConnectedTo, pin.Hub.ID)
|
||||
peer.pushChanges.Set()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fully recalculate reachability if any Lanes were removed.
|
||||
if removedLanes {
|
||||
err := m.recalculateReachableHubs()
|
||||
if err != nil {
|
||||
log.Warningf("spn/navigator: failed to recalculate reachable Hubs: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update states that depend on other information.
|
||||
|
||||
// Check if hub is superseded or if it supersedes another hub.
|
||||
m.updateStateSuperseded(pin)
|
||||
|
||||
// Push updates.
|
||||
m.PushPinChanges()
|
||||
}
|
||||
|
||||
const (
|
||||
minUnconfirmedLatency = 10 * time.Millisecond
|
||||
maxUnconfirmedCapacity = 100000000 // 100Mbit/s
|
||||
|
||||
cap1Mbit float32 = 1000000
|
||||
cap10Mbit float32 = 10000000
|
||||
cap100Mbit float32 = 100000000
|
||||
cap1Gbit float32 = 1000000000
|
||||
cap10Gbit float32 = 10000000000
|
||||
)
|
||||
|
||||
// updateHubLane updates a lane between two Hubs on the Map.
|
||||
// pin must already be locked, lane belongs to pin.
|
||||
// peer will be locked by this function.
|
||||
func (m *Map) updateHubLane(pin *Pin, lane *hub.Lane, peer *Pin) {
|
||||
peer.Hub.Lock()
|
||||
defer peer.Hub.Unlock()
|
||||
|
||||
// Then get the corresponding Lane from that peer, if it exists.
|
||||
var peerLane *hub.Lane
|
||||
for _, possiblePeerLane := range peer.Hub.Status.Lanes {
|
||||
if possiblePeerLane.ID == pin.Hub.ID {
|
||||
peerLane = possiblePeerLane
|
||||
// We have found the corresponding peerLane, break the loop.
|
||||
break
|
||||
}
|
||||
}
|
||||
if peerLane == nil {
|
||||
// The peer obviously does not advertise a Lane to this Hub.
|
||||
// Maybe this is a fresh Lane, and the message has not yet reached us.
|
||||
// Alternatively, the Lane could have been recently removed.
|
||||
|
||||
// Abandon this Lane for now.
|
||||
delete(pin.ConnectedTo, peer.Hub.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate combined latency, use the greater value.
|
||||
combinedLatency := lane.Latency
|
||||
if peerLane.Latency > combinedLatency {
|
||||
combinedLatency = peerLane.Latency
|
||||
}
|
||||
// Enforce minimum value if at least one side has no data.
|
||||
if (lane.Latency == 0 || peerLane.Latency == 0) && combinedLatency < minUnconfirmedLatency {
|
||||
combinedLatency = minUnconfirmedLatency
|
||||
}
|
||||
|
||||
// Calculate combined capacity, use the lesser existing value.
|
||||
combinedCapacity := lane.Capacity
|
||||
if combinedCapacity == 0 || (peerLane.Capacity > 0 && peerLane.Capacity < combinedCapacity) {
|
||||
combinedCapacity = peerLane.Capacity
|
||||
}
|
||||
// Enforce maximum value if at least one side has no data.
|
||||
if (lane.Capacity == 0 || peerLane.Capacity == 0) && combinedCapacity > maxUnconfirmedCapacity {
|
||||
combinedCapacity = maxUnconfirmedCapacity
|
||||
}
|
||||
|
||||
// Calculate lane cost.
|
||||
laneCost := CalculateLaneCost(combinedLatency, combinedCapacity)
|
||||
|
||||
// Add Lane to both Pins and override old values in the process.
|
||||
pin.ConnectedTo[peer.Hub.ID] = &Lane{
|
||||
Pin: peer,
|
||||
Capacity: combinedCapacity,
|
||||
Latency: combinedLatency,
|
||||
Cost: laneCost,
|
||||
active: true,
|
||||
}
|
||||
peer.ConnectedTo[pin.Hub.ID] = &Lane{
|
||||
Pin: pin,
|
||||
Capacity: combinedCapacity,
|
||||
Latency: combinedLatency,
|
||||
Cost: laneCost,
|
||||
active: true,
|
||||
}
|
||||
peer.pushChanges.Set()
|
||||
|
||||
// Check for reachability.
|
||||
|
||||
if pin.State.Has(StateReachable) {
|
||||
peer.markReachable(pin.HopDistance + 1)
|
||||
}
|
||||
if peer.State.Has(StateReachable) {
|
||||
pin.markReachable(peer.HopDistance + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// ResetFailingStates resets the failing state on all pins.
|
||||
func (m *Map) ResetFailingStates(ctx context.Context) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
for _, pin := range m.all {
|
||||
pin.ResetFailingState()
|
||||
}
|
||||
|
||||
m.PushPinChanges()
|
||||
}
|
||||
|
||||
func (m *Map) updateFailingStates(ctx context.Context, task *modules.Task) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
for _, pin := range m.all {
|
||||
if pin.State.Has(StateFailing) && !pin.IsFailing() {
|
||||
pin.removeStates(StateFailing)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Map) updateStates(ctx context.Context, task *modules.Task) error {
|
||||
var toDelete []string
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
pinLoop:
|
||||
for _, pin := range m.all {
|
||||
// Check for discontinued Hubs.
|
||||
if m.intel != nil {
|
||||
hubIntel, ok := m.intel.Hubs[pin.Hub.ID]
|
||||
if ok && hubIntel.Discontinued {
|
||||
toDelete = append(toDelete, pin.Hub.ID)
|
||||
log.Infof("spn/navigator: deleting discontinued %s", pin.Hub)
|
||||
continue pinLoop
|
||||
}
|
||||
}
|
||||
// Check for obsoleted Hubs.
|
||||
if pin.State.HasNoneOf(StateActive) && pin.Hub.Obsolete() {
|
||||
toDelete = append(toDelete, pin.Hub.ID)
|
||||
log.Infof("spn/navigator: deleting obsolete %s", pin.Hub)
|
||||
}
|
||||
|
||||
// Delete hubs async, as deleting triggers a couple hooks that lock the map.
|
||||
if len(toDelete) > 0 {
|
||||
module.StartWorker("delete hubs", func(_ context.Context) error {
|
||||
for _, idToDelete := range toDelete {
|
||||
err := hub.RemoveHubAndMsgs(m.Name, idToDelete)
|
||||
if err != nil {
|
||||
log.Warningf("spn/navigator: failed to delete Hub %s: %s", idToDelete, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update StateActive.
|
||||
m.updateActiveHubs()
|
||||
|
||||
// Update StateReachable.
|
||||
return m.recalculateReachableHubs()
|
||||
}
|
||||
|
||||
// AddBootstrapHubs adds the given bootstrap hubs to the map.
|
||||
func (m *Map) AddBootstrapHubs(bootstrapTransports []string) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.addBootstrapHubs(bootstrapTransports)
|
||||
}
|
||||
|
||||
func (m *Map) addBootstrapHubs(bootstrapTransports []string) error {
|
||||
var anyAdded bool
|
||||
var lastErr error
|
||||
var failed int
|
||||
for _, bootstrapTransport := range bootstrapTransports {
|
||||
err := m.addBootstrapHub(bootstrapTransport)
|
||||
if err != nil {
|
||||
log.Warningf("spn/navigator: failed to add bootstrap hub %q to map %s: %s", bootstrapTransport, m.Name, err)
|
||||
lastErr = err
|
||||
failed++
|
||||
} else {
|
||||
anyAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr != nil && !anyAdded {
|
||||
return lastErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Map) addBootstrapHub(bootstrapTransport string) error {
|
||||
// Parse bootstrap hub.
|
||||
transport, hubID, hubIP, err := hub.ParseBootstrapHub(bootstrapTransport)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid bootstrap hub: %w", err)
|
||||
}
|
||||
|
||||
// Check if hub already exists.
|
||||
var h *hub.Hub
|
||||
pin, ok := m.all[hubID]
|
||||
if ok {
|
||||
h = pin.Hub
|
||||
} else {
|
||||
h = &hub.Hub{
|
||||
ID: hubID,
|
||||
Map: m.Name,
|
||||
Info: &hub.Announcement{
|
||||
ID: hubID,
|
||||
},
|
||||
Status: &hub.Status{},
|
||||
FirstSeen: time.Now(), // Do not garbage collect bootstrap hubs.
|
||||
}
|
||||
}
|
||||
|
||||
// Add IP if it does not yet exist.
|
||||
if hubIP4 := hubIP.To4(); hubIP4 != nil {
|
||||
if h.Info.IPv4 == nil {
|
||||
h.Info.IPv4 = hubIP4
|
||||
} else if !h.Info.IPv4.Equal(hubIP4) {
|
||||
return fmt.Errorf("additional bootstrap entry with same ID but mismatching IP address: %s", hubIP)
|
||||
}
|
||||
} else {
|
||||
if h.Info.IPv6 == nil {
|
||||
h.Info.IPv6 = hubIP
|
||||
} else if !h.Info.IPv6.Equal(hubIP) {
|
||||
return fmt.Errorf("additional bootstrap entry with same ID but mismatching IP address: %s", hubIP)
|
||||
}
|
||||
}
|
||||
|
||||
// Add transport if it does not yet exist.
|
||||
t := transport.String()
|
||||
if !utils.StringInSlice(h.Info.Transports, t) {
|
||||
h.Info.Transports = append(h.Info.Transports, t)
|
||||
}
|
||||
|
||||
// Add/update to map for bootstrapping.
|
||||
m.updateHub(h, false, false)
|
||||
log.Infof("spn/navigator: added/updated bootstrap %s to map %s", h, m.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateConfigQuickSettings updates config quick settings with available countries.
|
||||
func (m *Map) UpdateConfigQuickSettings(ctx context.Context) error {
|
||||
ctx, tracer := log.AddTracer(ctx)
|
||||
tracer.Trace("navigator: updating SPN rules country quick settings")
|
||||
defer tracer.Submit()
|
||||
|
||||
opts := m.DefaultOptions()
|
||||
opts.Home = &HomeHubOptions{
|
||||
Regard: StateTrusted,
|
||||
}
|
||||
opts.Destination = &DestinationHubOptions{
|
||||
Regard: StateTrusted,
|
||||
Disregard: StateIsHomeHub,
|
||||
}
|
||||
|
||||
// Home Policy.
|
||||
if err := m.updateQuickSettingExcludeCountryList(ctx, "spn/homePolicy", opts, HomeHub); err != nil {
|
||||
return err
|
||||
}
|
||||
// Transit Policy.
|
||||
if err := m.updateQuickSettingExcludeCountryList(ctx, profile.CfgOptionTransitHubPolicyKey, opts, TransitHub); err != nil {
|
||||
return err
|
||||
}
|
||||
// Exit Policy.
|
||||
if err := m.updateSelectRuleCountryList(ctx, profile.CfgOptionExitHubPolicyKey, opts, DestinationHub); err != nil {
|
||||
return err
|
||||
}
|
||||
// DNS Exit Policy.
|
||||
if err := m.updateSelectRuleCountryList(ctx, "spn/dnsExitPolicy", opts, DestinationHub); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Trust Nodes.
|
||||
if err := m.updateQuickSettingVerifiedOwnerList(ctx, "spn/trustNodes"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tracer.Trace("navigator: finished updating SPN rules country quick settings")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Map) updateQuickSettingExcludeCountryList(ctx context.Context, configKey string, opts *Options, matchFor HubType) error {
|
||||
// Get config option.
|
||||
cfgOption, err := config.GetOption(configKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config option %s: %w", configKey, err)
|
||||
}
|
||||
|
||||
// Get list of countries for this config option.
|
||||
countries := m.GetAvailableCountries(opts, matchFor)
|
||||
// Convert to list.
|
||||
countryList := make([]*geoip.CountryInfo, 0, len(countries))
|
||||
for _, country := range countries {
|
||||
countryList = append(countryList, country)
|
||||
}
|
||||
// Sort list.
|
||||
slices.SortFunc[[]*geoip.CountryInfo, *geoip.CountryInfo](countryList, func(a, b *geoip.CountryInfo) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
// Compile list of quick settings.
|
||||
quickSettings := make([]config.QuickSetting, 0, len(countries))
|
||||
for _, country := range countryList {
|
||||
quickSettings = append(quickSettings, config.QuickSetting{
|
||||
Name: fmt.Sprintf("Exclude %s (%s)", country.Name, country.Code),
|
||||
Value: []string{fmt.Sprintf("- %s", country.Code)},
|
||||
Action: config.QuickMergeTop,
|
||||
})
|
||||
}
|
||||
|
||||
// Lock config option and set new quick settings.
|
||||
cfgOption.Lock()
|
||||
defer cfgOption.Unlock()
|
||||
cfgOption.Annotations[config.QuickSettingsAnnotation] = quickSettings
|
||||
|
||||
log.Tracer(ctx).Debugf("navigator: updated %d countries in quick settings for %s", len(quickSettings), configKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
type selectCountry struct {
|
||||
config.QuickSetting
|
||||
FlagID string
|
||||
}
|
||||
|
||||
func (m *Map) updateSelectRuleCountryList(ctx context.Context, configKey string, opts *Options, matchFor HubType) error {
|
||||
// Get config option.
|
||||
cfgOption, err := config.GetOption(configKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config option %s: %w", configKey, err)
|
||||
}
|
||||
|
||||
// Get list of countries for this config option.
|
||||
countries := m.GetAvailableCountries(opts, matchFor)
|
||||
// Convert to list.
|
||||
countryList := make([]*geoip.CountryInfo, 0, len(countries))
|
||||
for _, country := range countries {
|
||||
countryList = append(countryList, country)
|
||||
}
|
||||
// Sort list.
|
||||
slices.SortFunc[[]*geoip.CountryInfo, *geoip.CountryInfo](countryList, func(a, b *geoip.CountryInfo) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
// Get continents from countries.
|
||||
continents := make(map[string]*geoip.ContinentInfo)
|
||||
for _, country := range countryList {
|
||||
continents[country.Continent.Code] = &country.Continent
|
||||
}
|
||||
// Convert to list.
|
||||
continentList := make([]*geoip.ContinentInfo, 0, len(continents))
|
||||
for _, continent := range continents {
|
||||
continentList = append(continentList, continent)
|
||||
}
|
||||
// Sort list.
|
||||
slices.SortFunc[[]*geoip.ContinentInfo, *geoip.ContinentInfo](continentList, func(a, b *geoip.ContinentInfo) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
// Start compiling all options.
|
||||
selections := make([]selectCountry, 0, len(continents)+len(countries)+2)
|
||||
|
||||
// Add EU as special region.
|
||||
selections = append(selections, selectCountry{
|
||||
QuickSetting: config.QuickSetting{
|
||||
Name: "European Union",
|
||||
Value: []string{"+ AT", "+ BE", "+ BG", "+ CY", "+ CZ", "+ DE", "+ DK", "+ EE", "+ ES", "+ FI", "+ FR", "+ GR", "+ HR", "+ HU", "+ IE", "+ IT", "+ LT", "+ LU", "+ LV", "+ MT", "+ NL", "+ PL", "+ PT", "+ RO", "+ SE", "+ SI", "+ SK", "- *"},
|
||||
Action: config.QuickReplace,
|
||||
},
|
||||
FlagID: "EU",
|
||||
})
|
||||
selections = append(selections, selectCountry{
|
||||
QuickSetting: config.QuickSetting{
|
||||
Name: "US and Canada",
|
||||
Value: []string{"+ US", "+ CA", "- *"},
|
||||
Action: config.QuickReplace,
|
||||
},
|
||||
})
|
||||
|
||||
// Add countries to quick settings.
|
||||
for _, country := range countryList {
|
||||
selections = append(selections, selectCountry{
|
||||
QuickSetting: config.QuickSetting{
|
||||
Name: fmt.Sprintf("%s (%s)", country.Name, country.Code),
|
||||
Value: []string{fmt.Sprintf("+ %s", country.Code), "- *"},
|
||||
Action: config.QuickReplace,
|
||||
},
|
||||
FlagID: country.Code,
|
||||
})
|
||||
}
|
||||
|
||||
// Add continents to quick settings.
|
||||
for _, continent := range continentList {
|
||||
selections = append(selections, selectCountry{
|
||||
QuickSetting: config.QuickSetting{
|
||||
Name: fmt.Sprintf("%s (C:%s)", continent.Name, continent.Code),
|
||||
Value: []string{fmt.Sprintf("+ C:%s", continent.Code), "- *"},
|
||||
Action: config.QuickReplace,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Lock config option and set new quick settings.
|
||||
cfgOption.Lock()
|
||||
defer cfgOption.Unlock()
|
||||
cfgOption.Annotations[config.QuickSettingsAnnotation] = selections
|
||||
|
||||
log.Tracer(ctx).Debugf("navigator: updated %d countries in quick settings for %s", len(selections), configKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Map) updateQuickSettingVerifiedOwnerList(ctx context.Context, configKey string) error {
|
||||
// Get config option.
|
||||
cfgOption, err := config.GetOption(configKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config option %s: %w", configKey, err)
|
||||
}
|
||||
|
||||
pins := m.pinList(true)
|
||||
verifiedOwners := make([]string, 0, len(pins)/5) // Capacity is an estimation.
|
||||
for _, pin := range pins {
|
||||
pin.Lock()
|
||||
vo := pin.VerifiedOwner
|
||||
pin.Unlock()
|
||||
|
||||
// Skip invalid/unneeded values.
|
||||
switch vo {
|
||||
case "", "Safing":
|
||||
continue
|
||||
}
|
||||
|
||||
// Add to list, if not yet in there.
|
||||
if !slices.Contains[[]string, string](verifiedOwners, vo) {
|
||||
verifiedOwners = append(verifiedOwners, vo)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort list.
|
||||
slices.Sort[[]string](verifiedOwners)
|
||||
|
||||
// Compile list of quick settings.
|
||||
quickSettings := make([]config.QuickSetting, 0, len(verifiedOwners))
|
||||
for _, vo := range verifiedOwners {
|
||||
quickSettings = append(quickSettings, config.QuickSetting{
|
||||
Name: fmt.Sprintf("Trust %s", vo),
|
||||
Value: []string{vo},
|
||||
Action: config.QuickMergeBottom,
|
||||
})
|
||||
}
|
||||
|
||||
// Lock config option and set new quick settings.
|
||||
cfgOption.Lock()
|
||||
defer cfgOption.Unlock()
|
||||
cfgOption.Annotations[config.QuickSettingsAnnotation] = quickSettings
|
||||
|
||||
log.Tracer(ctx).Debugf("navigator: updated %d verified owners in quick settings for %s", len(quickSettings), configKey)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user