From 7d315e92bee57017db9a366d8869ac3953eadfd8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 18 Feb 2022 14:14:22 +0100 Subject: [PATCH] Integrate SPN route manipulation settings --- firewall/interception.go | 44 ++-------------- firewall/master.go | 4 -- firewall/tunnel.go | 105 ++++++++++++++++++++++++++++++++++--- profile/config-update.go | 16 ++++++ profile/profile-layered.go | 68 ++++++++++++++++++------ profile/profile.go | 20 +++++++ 6 files changed, 188 insertions(+), 69 deletions(-) diff --git a/firewall/interception.go b/firewall/interception.go index 858ae8e9..25e0354b 100644 --- a/firewall/interception.go +++ b/firewall/interception.go @@ -25,9 +25,6 @@ import ( "github.com/safing/portmaster/network" "github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/packet" - "github.com/safing/spn/captain" - "github.com/safing/spn/crew" - "github.com/safing/spn/sluice" ) var ( @@ -335,9 +332,6 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) { conn.Accept("connection by Portmaster", noReasonOptionKey) conn.Internal = true - // Set tunnel options. - setCustomTunnelOptionsForPortmaster(conn) - // Redirect outbound DNS packests, case pkt.IsOutbound() && pkt.Info().DstPort == 53 && @@ -368,41 +362,6 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) { conn.Accept("privacy filter disabled", noReasonOptionKey) } - // Tunnel, if enabled. - if pkt.IsOutbound() && conn.Entity.IPScope.IsGlobal() && - tunnelEnabled() && conn.Verdict == network.VerdictAccept && - conn.Process().Profile() != nil && - conn.Process().Profile().UseSPN() { - - switch { - case captain.ClientBootstrapping() && - conn.Process().Pid == ownPID: - // Exclude the Portmaster during SPN bootstrapping. - - case captain.IsExcepted(conn.Entity.IP) && - conn.Process().Pid == ownPID: - // Exclude requests of the SPN itself. - - case captain.ClientReady(): - // Queue request in sluice. - err := sluice.AwaitRequest(conn, crew.HandleSluiceRequest) - if err != nil { - log.Tracer(pkt.Ctx()).Warningf("failed to rqeuest tunneling: %s", err) - conn.Failed("failed to request tunneling", "") - } else { - log.Tracer(pkt.Ctx()).Trace("filter: tunneling requested") - conn.Verdict = network.VerdictRerouteToTunnel - conn.Tunneled = true - } - - default: - // Block connection as SPN is not ready yet. - log.Tracer(pkt.Ctx()).Trace("SPN not ready for tunneling") - conn.Failed("SPN not ready for tunneling", "") - - } - } - // TODO: Enable inspection framework again. conn.Inspecting = false @@ -419,6 +378,9 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) { conn.Encrypted = true } + // Check if connection should be tunneled. + checkTunneling(pkt.Ctx(), conn, pkt) + switch { case conn.Inspecting: log.Tracer(pkt.Ctx()).Trace("filter: start inspecting") diff --git a/firewall/master.go b/firewall/master.go index fd02f68e..16dc2b5f 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -140,10 +140,6 @@ func checkPortmasterConnection(ctx context.Context, conn *network.Connection, _ log.Tracer(ctx).Infof("filter: granting own connection %s", conn) conn.Accept("connection by Portmaster", noReasonOptionKey) conn.Internal = true - - // Set tunnel options. - setCustomTunnelOptionsForPortmaster(conn) - return true } diff --git a/firewall/tunnel.go b/firewall/tunnel.go index 27435466..d4381b4e 100644 --- a/firewall/tunnel.go +++ b/firewall/tunnel.go @@ -1,23 +1,114 @@ package firewall import ( + "context" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/network" "github.com/safing/portmaster/resolver" + "github.com/safing/spn/captain" + "github.com/safing/spn/crew" "github.com/safing/spn/navigator" + "github.com/safing/spn/sluice" ) -func setCustomTunnelOptionsForPortmaster(conn *network.Connection) { +func checkTunneling(ctx context.Context, conn *network.Connection, pkt packet.Packet) { + // Check if the connection should be tunneled at all. switch { case !tunnelEnabled(): - // Ignore when tunneling is not enabled. + // Tunneling is disabled. return case !conn.Entity.IPScope.IsGlobal(): - // Ignore if destination is not in global address space. + // Can't tunnel Local/LAN connections. return - case resolver.IsResolverAddress(conn.Entity.IP, conn.Entity.Port): - // Set custom tunnel options for DNS servers. - conn.TunnelOpts = &navigator.Options{ - RoutingProfile: navigator.RoutingProfileHomeName, + case conn.Inbound: + // Can't tunnel incoming connections. + return + case conn.Verdict != network.VerdictAccept: + // Connection will be blocked. + return + case conn.Process().Pid == ownPID: + // Bypass tunneling for certain own connections. + switch { + case captain.ClientBootstrapping(): + return + case captain.IsExcepted(conn.Entity.IP): + return } } + + // Get profile. + layeredProfile := conn.Process().Profile() + if layeredProfile == nil { + conn.Failed("no profile set", "") + return + } + + // Update profile. + if layeredProfile.NeedsUpdate() { + // Update revision counter in connection. + conn.ProfileRevisionCounter = layeredProfile.Update() + conn.SaveWhenFinished() + } else { + // Check if the revision counter of the connection needs updating. + revCnt := layeredProfile.RevisionCnt() + if conn.ProfileRevisionCounter != revCnt { + conn.ProfileRevisionCounter = revCnt + conn.SaveWhenFinished() + } + } + + // Check if tunneling is enabeld for app at all. + if !layeredProfile.UseSPN() { + return + } + + // Check if tunneling is enabled for entity. + conn.Entity.FetchData(ctx) + result, _ := layeredProfile.MatchSPNUsagePolicy(ctx, conn.Entity) + switch result { + case endpoints.MatchError: + conn.Failed("failed to check SPN rules", profile.CfgOptionSPNUsagePolicyKey) + return + case endpoints.Denied: + return + } + + // Tunnel all the things! + + // Check if ready. + if !captain.ClientReady() { + // Block connection as SPN is not ready yet. + log.Tracer(pkt.Ctx()).Trace("SPN not ready for tunneling") + conn.Failed("SPN not ready for tunneling", "") + return + } + + // Set options. + conn.TunnelOpts = &navigator.Options{ + HubPolicies: layeredProfile.StackedExitHubPolicies(), + CheckHubExitPolicyWith: conn.Entity, + RequireTrustedDestinationHubs: conn.Encrypted, + RoutingProfile: layeredProfile.SPNRoutingAlgorithm(), + } + + // Special handling for the internal DNS resolver. + if conn.Process().Pid == ownPID && resolver.IsResolverAddress(conn.Entity.IP, conn.Entity.Port) { + conn.TunnelOpts.RoutingProfile = navigator.RoutingProfileHomeID + } + + // Queue request in sluice. + err := sluice.AwaitRequest(conn, crew.HandleSluiceRequest) + if err != nil { + log.Tracer(pkt.Ctx()).Warningf("failed to rqeuest tunneling: %s", err) + conn.Failed("failed to request tunneling", "") + } else { + log.Tracer(pkt.Ctx()).Trace("filter: tunneling requested") + conn.Verdict = network.VerdictRerouteToTunnel + conn.Tunneled = true + } } diff --git a/profile/config-update.go b/profile/config-update.go index 2e6a1af6..f8979d3d 100644 --- a/profile/config-update.go +++ b/profile/config-update.go @@ -18,6 +18,8 @@ var ( cfgDefaultAction uint8 cfgEndpoints endpoints.Endpoints cfgServiceEndpoints endpoints.Endpoints + cfgSPNUsagePolicy endpoints.Endpoints + cfgSPNExitHubPolicy endpoints.Endpoints cfgFilterLists []string ) @@ -75,6 +77,20 @@ func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error { lastErr = err } + list = cfgOptionSPNUsagePolicy() + cfgSPNUsagePolicy, err = endpoints.ParseEndpoints(list) + if err != nil { + // TODO: module error? + lastErr = err + } + + list = cfgOptionExitHubPolicy() + cfgSPNExitHubPolicy, err = endpoints.ParseEndpoints(list) + if err != nil { + // TODO: module error? + lastErr = err + } + // build global profile for reference profile := New(SourceSpecial, "global-config", "", nil) profile.Name = "Global Configuration" diff --git a/profile/profile-layered.go b/profile/profile-layered.go index bf965944..ce96c01f 100644 --- a/profile/profile-layered.go +++ b/profile/profile-layered.go @@ -35,19 +35,20 @@ type LayeredProfile struct { // via the API. If we ever switch away from JSON to something else supported // by DSD this WILL BREAK! - DisableAutoPermit config.BoolOption `json:"-"` - BlockScopeLocal config.BoolOption `json:"-"` - BlockScopeLAN config.BoolOption `json:"-"` - BlockScopeInternet config.BoolOption `json:"-"` - BlockP2P config.BoolOption `json:"-"` - BlockInbound config.BoolOption `json:"-"` - RemoveOutOfScopeDNS config.BoolOption `json:"-"` - RemoveBlockedDNS config.BoolOption `json:"-"` - FilterSubDomains config.BoolOption `json:"-"` - FilterCNAMEs config.BoolOption `json:"-"` - PreventBypassing config.BoolOption `json:"-"` - DomainHeuristics config.BoolOption `json:"-"` - UseSPN config.BoolOption `json:"-"` + DisableAutoPermit config.BoolOption `json:"-"` + BlockScopeLocal config.BoolOption `json:"-"` + BlockScopeLAN config.BoolOption `json:"-"` + BlockScopeInternet config.BoolOption `json:"-"` + BlockP2P config.BoolOption `json:"-"` + BlockInbound config.BoolOption `json:"-"` + RemoveOutOfScopeDNS config.BoolOption `json:"-"` + RemoveBlockedDNS config.BoolOption `json:"-"` + FilterSubDomains config.BoolOption `json:"-"` + FilterCNAMEs config.BoolOption `json:"-"` + PreventBypassing config.BoolOption `json:"-"` + DomainHeuristics config.BoolOption `json:"-"` + UseSPN config.BoolOption `json:"-"` + SPNRoutingAlgorithm config.StringOption `json:"-"` } // NewLayeredProfile returns a new layered profile based on the given local profile. @@ -115,6 +116,10 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile { CfgOptionUseSPNKey, cfgOptionUseSPN, ) + lp.SPNRoutingAlgorithm = lp.wrapStringOption( + CfgOptionRoutingAlgorithmKey, + cfgOptionRoutingAlgorithm, + ) lp.LayerIDs = append(lp.LayerIDs, localProfile.ScopedID()) lp.layers = append(lp.layers, localProfile) @@ -334,6 +339,39 @@ func (lp *LayeredProfile) MatchServiceEndpoint(ctx context.Context, entity *inte return cfgServiceEndpoints.Match(ctx, entity) } +// MatchSPNUsagePolicy checks if the given endpoint matches an entry in any of the profiles. This functions requires the layered profile to be read locked. +func (lp *LayeredProfile) MatchSPNUsagePolicy(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) { + for _, layer := range lp.layers { + if layer.spnUsagePolicy.IsSet() { + result, reason := layer.spnUsagePolicy.Match(ctx, entity) + if endpoints.IsDecision(result) { + return result, reason + } + } + } + + cfgLock.RLock() + defer cfgLock.RUnlock() + return cfgSPNUsagePolicy.Match(ctx, entity) +} + +// StackedExitHubPolicies returns all exit hub policies of the layered profile, including the global one. +func (lp *LayeredProfile) StackedExitHubPolicies() []endpoints.Endpoints { + policies := make([]endpoints.Endpoints, 0, len(lp.layers)+3) // +1 for global policy, +2 for intel policies + + for _, layer := range lp.layers { + if layer.spnExitHubPolicy.IsSet() { + policies = append(policies, layer.spnExitHubPolicy) + } + } + + cfgLock.RLock() + defer cfgLock.RUnlock() + policies = append(policies, cfgSPNExitHubPolicy) + + return policies +} + // MatchFilterLists matches the entity against the set of filter // lists. This functions requires the layered profile to be read locked. func (lp *LayeredProfile) MatchFilterLists(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) { @@ -451,9 +489,6 @@ func (lp *LayeredProfile) GetProfileSource(configKey string) string { return "" } -/* -For later: - func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config.StringOption) config.StringOption { var revCnt uint64 = 0 var value string @@ -485,7 +520,6 @@ func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config return value } } -*/ func max(a, b uint8) uint8 { if a > b { diff --git a/profile/profile.go b/profile/profile.go index 1e126714..bf0d3017 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -128,6 +128,8 @@ type Profile struct { //nolint:maligned // not worth the effort serviceEndpoints endpoints.Endpoints filterListsSet bool filterListIDs []string + spnUsagePolicy endpoints.Endpoints + spnExitHubPolicy endpoints.Endpoints // Lifecycle Management outdated *abool.AtomicBool @@ -202,6 +204,24 @@ func (profile *Profile) parseConfig() error { } } + list, ok = profile.configPerspective.GetAsStringArray(CfgOptionSPNUsagePolicyKey) + profile.spnUsagePolicy = nil + if ok { + profile.spnUsagePolicy, err = endpoints.ParseEndpoints(list) + if err != nil { + lastErr = err + } + } + + list, ok = profile.configPerspective.GetAsStringArray(CfgOptionExitHubPolicyKey) + profile.spnExitHubPolicy = nil + if ok { + profile.spnExitHubPolicy, err = endpoints.ParseEndpoints(list) + if err != nil { + lastErr = err + } + } + return lastErr }