mirror of
https://github.com/safing/portmaster
synced 2025-04-20 10:59:10 +00:00
* Move portbase into monorepo * Add new simple module mgr * [WIP] Switch to new simple module mgr * Add StateMgr and more worker variants * [WIP] Switch more modules * [WIP] Switch more modules * [WIP] swtich more modules * [WIP] switch all SPN modules * [WIP] switch all service modules * [WIP] Convert all workers to the new module system * [WIP] add new task system to module manager * [WIP] Add second take for scheduling workers * [WIP] Add FIXME for bugs in new scheduler * [WIP] Add minor improvements to scheduler * [WIP] Add new worker scheduler * [WIP] Fix more bug related to new module system * [WIP] Fix start handing of the new module system * [WIP] Improve startup process * [WIP] Fix minor issues * [WIP] Fix missing subsystem in settings * [WIP] Initialize managers in constructor * [WIP] Move module event initialization to constrictors * [WIP] Fix setting for enabling and disabling the SPN module * [WIP] Move API registeration into module construction * [WIP] Update states mgr for all modules * [WIP] Add CmdLine operation support * Add state helper methods to module group and instance * Add notification and module status handling to status package * Fix starting issues * Remove pilot widget and update security lock to new status data * Remove debug logs * Improve http server shutdown * Add workaround for cleanly shutting down firewall+netquery * Improve logging * Add syncing states with notifications for new module system * Improve starting, stopping, shutdown; resolve FIXMEs/TODOs * [WIP] Fix most unit tests * Review new module system and fix minor issues * Push shutdown and restart events again via API * Set sleep mode via interface * Update example/template module * [WIP] Fix spn/cabin unit test * Remove deprecated UI elements * Make log output more similar for the logging transition phase * Switch spn hub and observer cmds to new module system * Fix log sources * Make worker mgr less error prone * Fix tests and minor issues * Fix observation hub * Improve shutdown and restart handling * Split up big connection.go source file * Move varint and dsd packages to structures repo * Improve expansion test * Fix linter warnings * Fix interception module on windows * Fix linter errors --------- Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
388 lines
11 KiB
Go
388 lines
11 KiB
Go
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 range max {
|
|
// 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])
|
|
}
|
|
}
|