mirror of
https://github.com/safing/portmaster
synced 2025-09-01 10:09:11 +00:00
554 lines
14 KiB
Go
554 lines
14 KiB
Go
package profile
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/safing/portbase/config"
|
|
"github.com/safing/portbase/database/record"
|
|
"github.com/safing/portbase/log"
|
|
"github.com/safing/portbase/runtime"
|
|
"github.com/safing/portmaster/intel"
|
|
"github.com/safing/portmaster/profile/endpoints"
|
|
)
|
|
|
|
// LayeredProfile combines multiple Profiles.
|
|
type LayeredProfile struct {
|
|
record.Base
|
|
sync.RWMutex
|
|
|
|
localProfile *Profile
|
|
layers []*Profile
|
|
|
|
LayerIDs []string
|
|
RevisionCounter uint64
|
|
globalValidityFlag *config.ValidityFlag
|
|
|
|
securityLevel *uint32
|
|
|
|
// These functions give layered access to configuration options and require
|
|
// the layered profile to be read locked.
|
|
|
|
// TODO(ppacher): we need JSON tags here so the layeredProfile can be exposed
|
|
// 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:"-"`
|
|
SPNRoutingAlgorithm config.StringOption `json:"-"`
|
|
EnableHistory config.BoolOption `json:"-"`
|
|
KeepHistory config.IntOption `json:"-"`
|
|
}
|
|
|
|
// NewLayeredProfile returns a new layered profile based on the given local profile.
|
|
func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
|
|
var securityLevelVal uint32
|
|
|
|
lp := &LayeredProfile{
|
|
localProfile: localProfile,
|
|
layers: make([]*Profile, 0, 1),
|
|
LayerIDs: make([]string, 0, 1),
|
|
globalValidityFlag: config.NewValidityFlag(),
|
|
RevisionCounter: 1,
|
|
securityLevel: &securityLevelVal,
|
|
}
|
|
|
|
lp.DisableAutoPermit = lp.wrapBoolOption(
|
|
CfgOptionDisableAutoPermitKey,
|
|
cfgOptionDisableAutoPermit,
|
|
)
|
|
lp.BlockScopeLocal = lp.wrapBoolOption(
|
|
CfgOptionBlockScopeLocalKey,
|
|
cfgOptionBlockScopeLocal,
|
|
)
|
|
lp.BlockScopeLAN = lp.wrapBoolOption(
|
|
CfgOptionBlockScopeLANKey,
|
|
cfgOptionBlockScopeLAN,
|
|
)
|
|
lp.BlockScopeInternet = lp.wrapBoolOption(
|
|
CfgOptionBlockScopeInternetKey,
|
|
cfgOptionBlockScopeInternet,
|
|
)
|
|
lp.BlockP2P = lp.wrapBoolOption(
|
|
CfgOptionBlockP2PKey,
|
|
cfgOptionBlockP2P,
|
|
)
|
|
lp.BlockInbound = lp.wrapBoolOption(
|
|
CfgOptionBlockInboundKey,
|
|
cfgOptionBlockInbound,
|
|
)
|
|
lp.RemoveOutOfScopeDNS = lp.wrapBoolOption(
|
|
CfgOptionRemoveOutOfScopeDNSKey,
|
|
cfgOptionRemoveOutOfScopeDNS,
|
|
)
|
|
lp.RemoveBlockedDNS = lp.wrapBoolOption(
|
|
CfgOptionRemoveBlockedDNSKey,
|
|
cfgOptionRemoveBlockedDNS,
|
|
)
|
|
lp.FilterSubDomains = lp.wrapBoolOption(
|
|
CfgOptionFilterSubDomainsKey,
|
|
cfgOptionFilterSubDomains,
|
|
)
|
|
lp.FilterCNAMEs = lp.wrapBoolOption(
|
|
CfgOptionFilterCNAMEKey,
|
|
cfgOptionFilterCNAME,
|
|
)
|
|
lp.PreventBypassing = lp.wrapBoolOption(
|
|
CfgOptionPreventBypassingKey,
|
|
cfgOptionPreventBypassing,
|
|
)
|
|
lp.DomainHeuristics = lp.wrapBoolOption(
|
|
CfgOptionDomainHeuristicsKey,
|
|
cfgOptionDomainHeuristics,
|
|
)
|
|
lp.UseSPN = lp.wrapBoolOption(
|
|
CfgOptionUseSPNKey,
|
|
cfgOptionUseSPN,
|
|
)
|
|
lp.SPNRoutingAlgorithm = lp.wrapStringOption(
|
|
CfgOptionRoutingAlgorithmKey,
|
|
cfgOptionRoutingAlgorithm,
|
|
)
|
|
lp.EnableHistory = lp.wrapBoolOption(
|
|
CfgOptionEnableHistoryKey,
|
|
cfgOptionEnableHistory,
|
|
)
|
|
lp.KeepHistory = lp.wrapIntOption(
|
|
CfgOptionKeepHistoryKey,
|
|
cfgOptionKeepHistory,
|
|
)
|
|
|
|
lp.LayerIDs = append(lp.LayerIDs, localProfile.ScopedID())
|
|
lp.layers = append(lp.layers, localProfile)
|
|
|
|
// TODO: Load additional profiles.
|
|
|
|
lp.updateCaches()
|
|
|
|
lp.CreateMeta()
|
|
lp.SetKey(runtime.DefaultRegistry.DatabaseName() + ":" + revisionProviderPrefix + localProfile.ScopedID())
|
|
|
|
// Inform database subscribers about the new layered profile.
|
|
lp.Lock()
|
|
defer lp.Unlock()
|
|
|
|
pushLayeredProfile(lp)
|
|
|
|
return lp
|
|
}
|
|
|
|
// LockForUsage locks the layered profile, including all layers individually.
|
|
func (lp *LayeredProfile) LockForUsage() {
|
|
lp.RLock()
|
|
for _, layer := range lp.layers {
|
|
layer.RLock()
|
|
}
|
|
}
|
|
|
|
// UnlockForUsage unlocks the layered profile, including all layers individually.
|
|
func (lp *LayeredProfile) UnlockForUsage() {
|
|
lp.RUnlock()
|
|
for _, layer := range lp.layers {
|
|
layer.RUnlock()
|
|
}
|
|
}
|
|
|
|
// LocalProfile returns the local profile associated with this layered profile.
|
|
func (lp *LayeredProfile) LocalProfile() *Profile {
|
|
if lp == nil {
|
|
return nil
|
|
}
|
|
|
|
lp.RLock()
|
|
defer lp.RUnlock()
|
|
|
|
return lp.localProfile
|
|
}
|
|
|
|
// LocalProfileWithoutLocking returns the local profile associated with this
|
|
// layered profile, but without locking the layered profile.
|
|
// This method my only be used when the caller already has a lock on the layered profile.
|
|
func (lp *LayeredProfile) LocalProfileWithoutLocking() *Profile {
|
|
if lp == nil {
|
|
return nil
|
|
}
|
|
|
|
return lp.localProfile
|
|
}
|
|
|
|
// increaseRevisionCounter increases the revision counter and pushes the
|
|
// layered profile to listeners.
|
|
func (lp *LayeredProfile) increaseRevisionCounter(lock bool) (revisionCounter uint64) { //nolint:unparam // This is documentation.
|
|
if lp == nil {
|
|
return 0
|
|
}
|
|
|
|
if lock {
|
|
lp.Lock()
|
|
defer lp.Unlock()
|
|
}
|
|
|
|
// Increase the revision counter.
|
|
lp.RevisionCounter++
|
|
// Push the increased counter to the UI.
|
|
pushLayeredProfile(lp)
|
|
|
|
return lp.RevisionCounter
|
|
}
|
|
|
|
// RevisionCnt returns the current profile revision counter.
|
|
func (lp *LayeredProfile) RevisionCnt() (revisionCounter uint64) {
|
|
if lp == nil {
|
|
return 0
|
|
}
|
|
|
|
lp.RLock()
|
|
defer lp.RUnlock()
|
|
|
|
return lp.RevisionCounter
|
|
}
|
|
|
|
// MarkStillActive marks all the layers as still active.
|
|
func (lp *LayeredProfile) MarkStillActive() {
|
|
if lp == nil {
|
|
return
|
|
}
|
|
|
|
lp.RLock()
|
|
defer lp.RUnlock()
|
|
|
|
for _, layer := range lp.layers {
|
|
layer.MarkStillActive()
|
|
}
|
|
}
|
|
|
|
// NeedsUpdate checks for outdated profiles.
|
|
func (lp *LayeredProfile) NeedsUpdate() (outdated bool) {
|
|
lp.RLock()
|
|
defer lp.RUnlock()
|
|
|
|
// Check global config state.
|
|
if !lp.globalValidityFlag.IsValid() {
|
|
return true
|
|
}
|
|
|
|
// Check config in layers.
|
|
for _, layer := range lp.layers {
|
|
if layer.outdated.IsSet() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Update checks for and replaces any outdated profiles.
|
|
func (lp *LayeredProfile) Update(md MatchingData, createProfileCallback func() *Profile) (revisionCounter uint64) {
|
|
lp.Lock()
|
|
defer lp.Unlock()
|
|
|
|
var changed bool
|
|
for i, layer := range lp.layers {
|
|
if layer.outdated.IsSet() {
|
|
// Check for unsupported sources.
|
|
if layer.Source != SourceLocal {
|
|
log.Warningf("profile: updating profiles outside of local source is not supported: %s", layer.ScopedID())
|
|
layer.outdated.UnSet()
|
|
continue
|
|
}
|
|
|
|
// Update layer.
|
|
changed = true
|
|
newLayer, err := GetLocalProfile(layer.ID, md, createProfileCallback)
|
|
if err != nil {
|
|
log.Errorf("profiles: failed to update profile %s: %s", layer.ScopedID(), err)
|
|
} else {
|
|
lp.layers[i] = newLayer
|
|
}
|
|
|
|
// Update local profile reference.
|
|
if i == 0 {
|
|
lp.localProfile = newLayer
|
|
}
|
|
}
|
|
}
|
|
if !lp.globalValidityFlag.IsValid() {
|
|
changed = true
|
|
}
|
|
|
|
if changed {
|
|
// get global config validity flag
|
|
lp.globalValidityFlag.Refresh()
|
|
|
|
// update cached data fields
|
|
lp.updateCaches()
|
|
|
|
// bump revision counter
|
|
lp.increaseRevisionCounter(false)
|
|
}
|
|
|
|
return lp.RevisionCounter
|
|
}
|
|
|
|
func (lp *LayeredProfile) updateCaches() {
|
|
// update security level
|
|
var newLevel uint8
|
|
for _, layer := range lp.layers {
|
|
if newLevel < layer.SecurityLevel {
|
|
newLevel = layer.SecurityLevel
|
|
}
|
|
}
|
|
atomic.StoreUint32(lp.securityLevel, uint32(newLevel))
|
|
}
|
|
|
|
// SecurityLevel returns the highest security level of all layered profiles. This function is atomic and does not require any locking.
|
|
func (lp *LayeredProfile) SecurityLevel() uint8 {
|
|
return uint8(atomic.LoadUint32(lp.securityLevel))
|
|
}
|
|
|
|
// DefaultAction returns the active default action ID. This functions requires the layered profile to be read locked.
|
|
func (lp *LayeredProfile) DefaultAction() uint8 {
|
|
for _, layer := range lp.layers {
|
|
if layer.defaultAction > 0 {
|
|
return layer.defaultAction
|
|
}
|
|
}
|
|
|
|
cfgLock.RLock()
|
|
defer cfgLock.RUnlock()
|
|
return cfgDefaultAction
|
|
}
|
|
|
|
// MatchEndpoint 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) MatchEndpoint(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
|
|
for _, layer := range lp.layers {
|
|
if layer.endpoints.IsSet() {
|
|
result, reason := layer.endpoints.Match(ctx, entity)
|
|
if endpoints.IsDecision(result) {
|
|
return result, reason
|
|
}
|
|
}
|
|
}
|
|
|
|
cfgLock.RLock()
|
|
defer cfgLock.RUnlock()
|
|
return cfgEndpoints.Match(ctx, entity)
|
|
}
|
|
|
|
// MatchServiceEndpoint checks if the given endpoint of an inbound connection matches an entry in any of the profiles. This functions requires the layered profile to be read locked.
|
|
func (lp *LayeredProfile) MatchServiceEndpoint(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
|
|
entity.EnableReverseResolving()
|
|
|
|
for _, layer := range lp.layers {
|
|
if layer.serviceEndpoints.IsSet() {
|
|
result, reason := layer.serviceEndpoints.Match(ctx, entity)
|
|
if endpoints.IsDecision(result) {
|
|
return result, reason
|
|
}
|
|
}
|
|
}
|
|
|
|
cfgLock.RLock()
|
|
defer cfgLock.RUnlock()
|
|
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)
|
|
}
|
|
|
|
// StackedTransitHubPolicies returns all transit hub policies of the layered profile, including the global one.
|
|
func (lp *LayeredProfile) StackedTransitHubPolicies() []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.spnTransitHubPolicy.IsSet() {
|
|
policies = append(policies, layer.spnTransitHubPolicy)
|
|
}
|
|
}
|
|
|
|
cfgLock.RLock()
|
|
defer cfgLock.RUnlock()
|
|
policies = append(policies, cfgSPNTransitHubPolicy)
|
|
|
|
return policies
|
|
}
|
|
|
|
// 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) {
|
|
entity.ResolveSubDomainLists(ctx, lp.FilterSubDomains())
|
|
entity.EnableCNAMECheck(ctx, lp.FilterCNAMEs())
|
|
|
|
for _, layer := range lp.layers {
|
|
// Search for the first layer that has filter lists set.
|
|
if layer.filterListsSet {
|
|
if entity.MatchLists(layer.filterListIDs) {
|
|
return endpoints.Denied, entity.ListBlockReason()
|
|
}
|
|
|
|
return endpoints.NoMatch, nil
|
|
}
|
|
}
|
|
|
|
cfgLock.RLock()
|
|
defer cfgLock.RUnlock()
|
|
if len(cfgFilterLists) > 0 {
|
|
if entity.MatchLists(cfgFilterLists) {
|
|
return endpoints.Denied, entity.ListBlockReason()
|
|
}
|
|
}
|
|
|
|
return endpoints.NoMatch, nil
|
|
}
|
|
|
|
func (lp *LayeredProfile) wrapBoolOption(configKey string, globalConfig config.BoolOption) config.BoolOption {
|
|
var revCnt uint64 = 0
|
|
var value bool
|
|
var refreshLock sync.Mutex
|
|
|
|
return func() bool {
|
|
refreshLock.Lock()
|
|
defer refreshLock.Unlock()
|
|
|
|
// Check if we need to refresh the value.
|
|
if revCnt != lp.RevisionCounter {
|
|
revCnt = lp.RevisionCounter
|
|
|
|
// Go through all layers to find an active value.
|
|
found := false
|
|
for _, layer := range lp.layers {
|
|
layerValue, ok := layer.configPerspective.GetAsBool(configKey)
|
|
if ok {
|
|
found = true
|
|
value = layerValue
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
value = globalConfig()
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|
|
}
|
|
|
|
func (lp *LayeredProfile) wrapIntOption(configKey string, globalConfig config.IntOption) config.IntOption {
|
|
var revCnt uint64 = 0
|
|
var value int64
|
|
var refreshLock sync.Mutex
|
|
|
|
return func() int64 {
|
|
refreshLock.Lock()
|
|
defer refreshLock.Unlock()
|
|
|
|
// Check if we need to refresh the value.
|
|
if revCnt != lp.RevisionCounter {
|
|
revCnt = lp.RevisionCounter
|
|
|
|
// Go through all layers to find an active value.
|
|
found := false
|
|
for _, layer := range lp.layers {
|
|
layerValue, ok := layer.configPerspective.GetAsInt(configKey)
|
|
if ok {
|
|
found = true
|
|
value = layerValue
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
value = globalConfig()
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|
|
}
|
|
|
|
// GetProfileSource returns the database key of the first profile in the
|
|
// layers that has the given configuration key set. If it returns an empty
|
|
// string, the global profile can be assumed to have been effective.
|
|
func (lp *LayeredProfile) GetProfileSource(configKey string) string {
|
|
for _, layer := range lp.layers {
|
|
if layer.configPerspective.Has(configKey) {
|
|
return layer.Key()
|
|
}
|
|
}
|
|
|
|
// Global Profile
|
|
return ""
|
|
}
|
|
|
|
func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config.StringOption) config.StringOption {
|
|
var revCnt uint64 = 0
|
|
var value string
|
|
var refreshLock sync.Mutex
|
|
|
|
return func() string {
|
|
refreshLock.Lock()
|
|
defer refreshLock.Unlock()
|
|
|
|
// Check if we need to refresh the value.
|
|
if revCnt != lp.RevisionCounter {
|
|
revCnt = lp.RevisionCounter
|
|
|
|
// Go through all layers to find an active value.
|
|
found := false
|
|
for _, layer := range lp.layers {
|
|
layerValue, ok := layer.configPerspective.GetAsString(configKey)
|
|
if ok {
|
|
found = true
|
|
value = layerValue
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
value = globalConfig()
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|
|
}
|