mirror of
https://github.com/safing/portmaster
synced 2025-04-22 20:09:09 +00:00
426 lines
11 KiB
Go
426 lines
11 KiB
Go
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"
|
|
}
|
|
}
|