mirror of
https://github.com/safing/portmaster
synced 2025-04-18 01:49:09 +00:00
306 lines
8.3 KiB
Go
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
|
|
}
|