package navigator import ( "bytes" "errors" "fmt" "math" "net/http" "sort" "strconv" "strings" "sync" "text/tabwriter" "time" "github.com/awalterschulze/gographviz" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/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, 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, 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, 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, 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, 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, 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, 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 += "
`, "`", 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 }