safing-portmaster/spn/navigator/state.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"
}
}