Integrate SPN route manipulation settings

This commit is contained in:
Daniel 2022-02-18 14:14:22 +01:00
parent 0bb5f2901e
commit 7d315e92be
6 changed files with 188 additions and 69 deletions

View file

@ -25,9 +25,6 @@ import (
"github.com/safing/portmaster/network" "github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
"github.com/safing/spn/captain"
"github.com/safing/spn/crew"
"github.com/safing/spn/sluice"
) )
var ( var (
@ -335,9 +332,6 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
conn.Accept("connection by Portmaster", noReasonOptionKey) conn.Accept("connection by Portmaster", noReasonOptionKey)
conn.Internal = true conn.Internal = true
// Set tunnel options.
setCustomTunnelOptionsForPortmaster(conn)
// Redirect outbound DNS packests, // Redirect outbound DNS packests,
case pkt.IsOutbound() && case pkt.IsOutbound() &&
pkt.Info().DstPort == 53 && pkt.Info().DstPort == 53 &&
@ -368,41 +362,6 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
conn.Accept("privacy filter disabled", noReasonOptionKey) 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. // TODO: Enable inspection framework again.
conn.Inspecting = false conn.Inspecting = false
@ -419,6 +378,9 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
conn.Encrypted = true conn.Encrypted = true
} }
// Check if connection should be tunneled.
checkTunneling(pkt.Ctx(), conn, pkt)
switch { switch {
case conn.Inspecting: case conn.Inspecting:
log.Tracer(pkt.Ctx()).Trace("filter: start inspecting") log.Tracer(pkt.Ctx()).Trace("filter: start inspecting")

View file

@ -140,10 +140,6 @@ func checkPortmasterConnection(ctx context.Context, conn *network.Connection, _
log.Tracer(ctx).Infof("filter: granting own connection %s", conn) log.Tracer(ctx).Infof("filter: granting own connection %s", conn)
conn.Accept("connection by Portmaster", noReasonOptionKey) conn.Accept("connection by Portmaster", noReasonOptionKey)
conn.Internal = true conn.Internal = true
// Set tunnel options.
setCustomTunnelOptionsForPortmaster(conn)
return true return true
} }

View file

@ -1,23 +1,114 @@
package firewall package firewall
import ( 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/network"
"github.com/safing/portmaster/resolver" "github.com/safing/portmaster/resolver"
"github.com/safing/spn/captain"
"github.com/safing/spn/crew"
"github.com/safing/spn/navigator" "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 { switch {
case !tunnelEnabled(): case !tunnelEnabled():
// Ignore when tunneling is not enabled. // Tunneling is disabled.
return return
case !conn.Entity.IPScope.IsGlobal(): case !conn.Entity.IPScope.IsGlobal():
// Ignore if destination is not in global address space. // Can't tunnel Local/LAN connections.
return
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 return
case resolver.IsResolverAddress(conn.Entity.IP, conn.Entity.Port):
// Set custom tunnel options for DNS servers.
conn.TunnelOpts = &navigator.Options{
RoutingProfile: navigator.RoutingProfileHomeName,
} }
} }
// 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
}
} }

View file

@ -18,6 +18,8 @@ var (
cfgDefaultAction uint8 cfgDefaultAction uint8
cfgEndpoints endpoints.Endpoints cfgEndpoints endpoints.Endpoints
cfgServiceEndpoints endpoints.Endpoints cfgServiceEndpoints endpoints.Endpoints
cfgSPNUsagePolicy endpoints.Endpoints
cfgSPNExitHubPolicy endpoints.Endpoints
cfgFilterLists []string cfgFilterLists []string
) )
@ -75,6 +77,20 @@ func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error {
lastErr = err 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 // build global profile for reference
profile := New(SourceSpecial, "global-config", "", nil) profile := New(SourceSpecial, "global-config", "", nil)
profile.Name = "Global Configuration" profile.Name = "Global Configuration"

View file

@ -48,6 +48,7 @@ type LayeredProfile struct {
PreventBypassing config.BoolOption `json:"-"` PreventBypassing config.BoolOption `json:"-"`
DomainHeuristics config.BoolOption `json:"-"` DomainHeuristics config.BoolOption `json:"-"`
UseSPN config.BoolOption `json:"-"` UseSPN config.BoolOption `json:"-"`
SPNRoutingAlgorithm config.StringOption `json:"-"`
} }
// NewLayeredProfile returns a new layered profile based on the given local profile. // NewLayeredProfile returns a new layered profile based on the given local profile.
@ -115,6 +116,10 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
CfgOptionUseSPNKey, CfgOptionUseSPNKey,
cfgOptionUseSPN, cfgOptionUseSPN,
) )
lp.SPNRoutingAlgorithm = lp.wrapStringOption(
CfgOptionRoutingAlgorithmKey,
cfgOptionRoutingAlgorithm,
)
lp.LayerIDs = append(lp.LayerIDs, localProfile.ScopedID()) lp.LayerIDs = append(lp.LayerIDs, localProfile.ScopedID())
lp.layers = append(lp.layers, localProfile) lp.layers = append(lp.layers, localProfile)
@ -334,6 +339,39 @@ func (lp *LayeredProfile) MatchServiceEndpoint(ctx context.Context, entity *inte
return cfgServiceEndpoints.Match(ctx, entity) 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 // MatchFilterLists matches the entity against the set of filter
// lists. This functions requires the layered profile to be read locked. // 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) { 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 "" return ""
} }
/*
For later:
func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config.StringOption) config.StringOption { func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config.StringOption) config.StringOption {
var revCnt uint64 = 0 var revCnt uint64 = 0
var value string var value string
@ -485,7 +520,6 @@ func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config
return value return value
} }
} }
*/
func max(a, b uint8) uint8 { func max(a, b uint8) uint8 {
if a > b { if a > b {

View file

@ -128,6 +128,8 @@ type Profile struct { //nolint:maligned // not worth the effort
serviceEndpoints endpoints.Endpoints serviceEndpoints endpoints.Endpoints
filterListsSet bool filterListsSet bool
filterListIDs []string filterListIDs []string
spnUsagePolicy endpoints.Endpoints
spnExitHubPolicy endpoints.Endpoints
// Lifecycle Management // Lifecycle Management
outdated *abool.AtomicBool 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 return lastErr
} }