safing-portmaster/spn/captain/navigation.go
2024-03-27 16:17:58 +01:00

306 lines
8.3 KiB
Go

package captain
import (
"context"
"errors"
"fmt"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/service/intel"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/profile/endpoints"
"github.com/safing/portmaster/spn/access"
"github.com/safing/portmaster/spn/docks"
"github.com/safing/portmaster/spn/hub"
"github.com/safing/portmaster/spn/navigator"
"github.com/safing/portmaster/spn/terminal"
)
const stopCraneAfterBeingUnsuggestedFor = 6 * time.Hour
var (
// ErrAllHomeHubsExcluded is returned when all available home hubs were excluded.
ErrAllHomeHubsExcluded = errors.New("all home hubs are excluded")
// ErrReInitSPNSuggested is returned when no home hub can be found, even without rules.
ErrReInitSPNSuggested = errors.New("SPN re-init suggested")
)
func establishHomeHub(ctx context.Context) error {
// Get own IP.
locations, ok := netenv.GetInternetLocation()
if !ok || len(locations.All) == 0 {
return errors.New("failed to locate own device")
}
log.Debugf(
"spn/captain: looking for new home hub near %s and %s",
locations.BestV4(),
locations.BestV6(),
)
// 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(ctx)
} else if dl := locations.BestV6(); dl != nil && dl.IP != nil {
myEntity = (&intel.Entity{IP: dl.IP}).Init(0)
myEntity.FetchData(ctx)
}
// Get home hub policy for selecting the home hub.
homePolicy, err := getHomeHubPolicy()
if err != nil {
return err
}
// Build navigation options for searching for a home hub.
opts := &navigator.Options{
Home: &navigator.HomeHubOptions{
HubPolicies: []endpoints.Endpoints{homePolicy},
CheckHubPolicyWith: myEntity,
},
}
// Add requirement to only use Safing nodes when not using community nodes.
if !cfgOptionUseCommunityNodes() {
opts.Home.RequireVerifiedOwners = NonCommunityVerifiedOwners
}
// Require a trusted home node when the routing profile requires less than two hops.
routingProfile := navigator.GetRoutingProfile(cfgOptionRoutingAlgorithm())
if routingProfile.MinHops < 2 {
opts.Home.Regard = opts.Home.Regard.Add(navigator.StateTrusted)
}
// Find nearby hubs.
findCandidates:
candidates, err := navigator.Main.FindNearestHubs(
locations.BestV4().LocationOrNil(),
locations.BestV6().LocationOrNil(),
opts, navigator.HomeHub,
)
if err != nil {
switch {
case errors.Is(err, navigator.ErrEmptyMap):
// bootstrap to the network!
err := bootstrapWithUpdates()
if err != nil {
return err
}
goto findCandidates
case errors.Is(err, navigator.ErrAllPinsDisregarded):
if len(homePolicy) > 0 {
return ErrAllHomeHubsExcluded
}
return ErrReInitSPNSuggested
default:
return fmt.Errorf("failed to find nearby hubs: %w", err)
}
}
// Try connecting to a hub.
var tries int
var candidate *hub.Hub
for tries, candidate = range candidates {
err = connectToHomeHub(ctx, candidate)
if err != nil {
// Check if context is canceled.
if ctx.Err() != nil {
return ctx.Err()
}
// Check if the SPN protocol is stopping again.
if errors.Is(err, terminal.ErrStopping) {
return err
}
log.Warningf("spn/captain: failed to connect to %s as new home: %s", candidate, err)
} else {
log.Infof("spn/captain: established connection to %s as new home with %d failed tries", candidate, tries)
return nil
}
}
if err != nil {
return fmt.Errorf("failed to connect to a new home hub - tried %d hubs: %w", tries+1, err)
}
return errors.New("no home hub candidates available")
}
func connectToHomeHub(ctx context.Context, dst *hub.Hub) error {
// Create new context with timeout.
// The maximum timeout is a worst case safeguard.
// Keep in mind that multiple IPs and protocols may be tried in all configurations.
// Some servers will be (possibly on purpose) hard to reach.
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
// Set and clean up exceptions.
setExceptions(dst.Info.IPv4, dst.Info.IPv6)
defer setExceptions(nil, nil)
// Connect to hub.
crane, err := EstablishCrane(ctx, dst)
if err != nil {
return err
}
// Cleanup connection in case of failure.
var success bool
defer func() {
if !success {
crane.Stop(nil)
}
}()
// Query all gossip msgs on first connection.
gossipQuery, tErr := NewGossipQueryOp(crane.Controller)
if tErr != nil {
log.Warningf("spn/captain: failed to start initial gossip query: %s", tErr)
}
// Wait for gossip query to complete.
select {
case <-gossipQuery.ctx.Done():
case <-ctx.Done():
return context.Canceled
}
// Create communication terminal.
homeTerminal, initData, tErr := docks.NewLocalCraneTerminal(crane, nil, terminal.DefaultHomeHubTerminalOpts())
if tErr != nil {
return tErr.Wrap("failed to create home terminal")
}
tErr = crane.EstablishNewTerminal(homeTerminal, initData)
if tErr != nil {
return tErr.Wrap("failed to connect home terminal")
}
if !DisableAccount {
// Authenticate to home hub.
authOp, tErr := access.AuthorizeToTerminal(homeTerminal)
if tErr != nil {
return tErr.Wrap("failed to authorize")
}
select {
case tErr := <-authOp.Result:
if !tErr.Is(terminal.ErrExplicitAck) {
return tErr.Wrap("failed to authenticate to")
}
case <-time.After(3 * time.Second):
return terminal.ErrTimeout.With("waiting for auth to complete")
case <-ctx.Done():
return terminal.ErrStopping
}
}
// Set new home on map.
ok := navigator.Main.SetHome(dst.ID, homeTerminal)
if !ok {
return errors.New("failed to set home hub on map")
}
// Assign crane to home hub in order to query it later.
docks.AssignCrane(crane.ConnectedHub.ID, crane)
success = true
return nil
}
func optimizeNetwork(ctx context.Context, task *modules.Task) error {
if publicIdentity == nil {
return nil
}
optimize:
result, err := navigator.Main.Optimize(nil)
if err != nil {
if errors.Is(err, navigator.ErrEmptyMap) {
// bootstrap to the network!
err := bootstrapWithUpdates()
if err != nil {
return err
}
goto optimize
}
return err
}
// Create any new connections.
var createdConnections int
var attemptedConnections int
for _, connectTo := range result.SuggestedConnections {
// Skip duplicates.
if connectTo.Duplicate {
continue
}
// Check if connection already exists.
crane := docks.GetAssignedCrane(connectTo.Hub.ID)
if crane != nil {
// Update last suggested timestamp.
crane.NetState.UpdateLastSuggestedAt()
// Continue crane if stopping.
if crane.AbortStopping() {
log.Infof("spn/captain: optimization aborted retiring of %s, removed stopping mark", crane)
crane.NotifyUpdate()
}
// Create new connections if we have connects left.
} else if createdConnections < result.MaxConnect {
attemptedConnections++
crane, tErr := EstablishPublicLane(ctx, connectTo.Hub)
if !tErr.IsOK() {
log.Warningf("spn/captain: failed to establish lane to %s: %s", connectTo.Hub, tErr)
} else {
createdConnections++
crane.NetState.UpdateLastSuggestedAt()
log.Infof("spn/captain: established lane to %s", connectTo.Hub)
}
}
}
// Log optimization result.
if attemptedConnections > 0 {
log.Infof(
"spn/captain: created %d/%d new connections for %s optimization",
createdConnections,
attemptedConnections,
result.Purpose)
} else {
log.Infof(
"spn/captain: checked %d connections for %s optimization",
len(result.SuggestedConnections),
result.Purpose,
)
}
// Retire cranes if unsuggested for a while.
if result.StopOthers {
for _, crane := range docks.GetAllAssignedCranes() {
switch {
case crane.Stopped():
// Crane already stopped.
case crane.IsStopping():
// Crane is stopping, forcibly stop if mine and suggested.
if crane.IsMine() && crane.NetState.StopSuggested() {
crane.Stop(nil)
}
case crane.IsMine() && crane.NetState.StoppingSuggested():
// Mark as stopping if mine and suggested.
crane.MarkStopping()
case crane.NetState.RequestStoppingSuggested(stopCraneAfterBeingUnsuggestedFor):
// Mark as stopping requested.
crane.MarkStoppingRequested()
}
}
}
return nil
}