mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
Clean up profiles and move to consolidated configuration system with profile layering
This commit is contained in:
parent
543a70422a
commit
36fad7aeec
16 changed files with 937 additions and 1391 deletions
|
@ -1,76 +1,44 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
var (
|
||||
activeProfileSets = make(map[string]*Set)
|
||||
activeProfileSetsLock sync.RWMutex
|
||||
// TODO: periodically clean up inactive profiles
|
||||
activeProfiles = make(map[string]*Profile)
|
||||
activeProfilesLock sync.RWMutex
|
||||
)
|
||||
|
||||
func activateProfileSet(ctx context.Context, set *Set) {
|
||||
activeProfileSetsLock.Lock()
|
||||
defer activeProfileSetsLock.Unlock()
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
activeProfileSets[set.id] = set
|
||||
log.Tracer(ctx).Tracef("profile: activated profile set %s", set.id)
|
||||
}
|
||||
// getActiveProfile returns a cached copy of an active profile and nil if it isn't found.
|
||||
func getActiveProfile(scopedID string) *Profile {
|
||||
activeProfilesLock.Lock()
|
||||
defer activeProfilesLock.Unlock()
|
||||
|
||||
// DeactivateProfileSet marks a profile set as not active.
|
||||
func DeactivateProfileSet(set *Set) {
|
||||
activeProfileSetsLock.Lock()
|
||||
defer activeProfileSetsLock.Unlock()
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
delete(activeProfileSets, set.id)
|
||||
log.Tracef("profile: deactivated profile set %s", set.id)
|
||||
}
|
||||
|
||||
func updateActiveProfile(profile *Profile, userProfile bool) {
|
||||
activeProfileSetsLock.RLock()
|
||||
defer activeProfileSetsLock.RUnlock()
|
||||
|
||||
var activeProfile *Profile
|
||||
var profilesUpdated bool
|
||||
|
||||
// iterate all active profile sets
|
||||
for _, activeSet := range activeProfileSets {
|
||||
activeSet.Lock()
|
||||
|
||||
if userProfile {
|
||||
activeProfile = activeSet.profiles[0]
|
||||
} else {
|
||||
activeProfile = activeSet.profiles[2]
|
||||
}
|
||||
|
||||
// check if profile exists (for stamp profiles)
|
||||
if activeProfile != nil {
|
||||
activeProfile.Lock()
|
||||
|
||||
// check if the stamp profile has the same ID
|
||||
if activeProfile.ID == profile.ID {
|
||||
if userProfile {
|
||||
activeSet.profiles[0] = profile
|
||||
log.Infof("profile: updated active user profile %s (%s)", profile.ID, profile.LinkedPath)
|
||||
} else {
|
||||
activeSet.profiles[2] = profile
|
||||
log.Infof("profile: updated active stamp profile %s", profile.ID)
|
||||
}
|
||||
profilesUpdated = true
|
||||
}
|
||||
|
||||
activeProfile.Unlock()
|
||||
}
|
||||
|
||||
activeSet.Unlock()
|
||||
profile, ok := activeProfiles[scopedID]
|
||||
if ok {
|
||||
return profile
|
||||
}
|
||||
|
||||
if profilesUpdated {
|
||||
increaseUpdateVersion()
|
||||
return nil
|
||||
}
|
||||
|
||||
// markProfileActive registers a profile as active.
|
||||
func markProfileActive(profile *Profile) {
|
||||
activeProfilesLock.Lock()
|
||||
defer activeProfilesLock.Unlock()
|
||||
|
||||
activeProfiles[profile.ScopedID()] = profile
|
||||
}
|
||||
|
||||
// markActiveProfileAsOutdated marks an active profile as outdated, so that it will be refetched from the database.
|
||||
func markActiveProfileAsOutdated(scopedID string) {
|
||||
activeProfilesLock.Lock()
|
||||
defer activeProfilesLock.Unlock()
|
||||
|
||||
profile, ok := activeProfiles[scopedID]
|
||||
if ok {
|
||||
profile.oudated.Set()
|
||||
delete(activeProfiles, scopedID)
|
||||
}
|
||||
}
|
||||
|
|
94
profile/config-update.go
Normal file
94
profile/config-update.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portmaster/profile/endpoints"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgLock sync.RWMutex
|
||||
|
||||
cfgDefaultAction uint8
|
||||
cfgEndpoints endpoints.Endpoints
|
||||
cfgServiceEndpoints endpoints.Endpoints
|
||||
)
|
||||
|
||||
func registerConfigUpdater() error {
|
||||
return module.RegisterEventHook(
|
||||
"config",
|
||||
"config changed",
|
||||
"update global config profile",
|
||||
updateGlobalConfigProfile,
|
||||
)
|
||||
}
|
||||
|
||||
func updateGlobalConfigProfile(ctx context.Context, data interface{}) error {
|
||||
cfgLock.Lock()
|
||||
defer cfgLock.Unlock()
|
||||
|
||||
var err error
|
||||
var lastErr error
|
||||
|
||||
action := cfgOptionDefaultAction()
|
||||
switch action {
|
||||
case "permit":
|
||||
cfgDefaultAction = DefaultActionPermit
|
||||
case "ask":
|
||||
cfgDefaultAction = DefaultActionAsk
|
||||
case "block":
|
||||
cfgDefaultAction = DefaultActionBlock
|
||||
default:
|
||||
// TODO: module error?
|
||||
lastErr = fmt.Errorf(`default action "%s" invalid`, action)
|
||||
cfgDefaultAction = DefaultActionBlock // default to block in worst case
|
||||
}
|
||||
|
||||
list := cfgOptionEndpoints()
|
||||
cfgEndpoints, err = endpoints.ParseEndpoints(list)
|
||||
if err != nil {
|
||||
// TODO: module error?
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
list = cfgOptionServiceEndpoints()
|
||||
cfgServiceEndpoints, err = endpoints.ParseEndpoints(list)
|
||||
if err != nil {
|
||||
// TODO: module error?
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
// build global profile for reference
|
||||
profile := &Profile{
|
||||
ID: "config",
|
||||
Source: SourceGlobal,
|
||||
Name: "Global Configuration",
|
||||
Config: make(map[string]interface{}),
|
||||
internalSave: true,
|
||||
}
|
||||
|
||||
// fill profile config options
|
||||
for key, value := range cfgStringOptions {
|
||||
profile.Config[key] = value
|
||||
}
|
||||
for key, value := range cfgStringArrayOptions {
|
||||
profile.Config[key] = value
|
||||
}
|
||||
for key, value := range cfgIntOptions {
|
||||
profile.Config[key] = value
|
||||
}
|
||||
for key, value := range cfgBoolOptions {
|
||||
profile.Config[key] = value
|
||||
}
|
||||
|
||||
// save profile
|
||||
err = profile.Save()
|
||||
if err != nil && lastErr == nil {
|
||||
// other errors are more important
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
228
profile/config.go
Normal file
228
profile/config.go
Normal file
|
@ -0,0 +1,228 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/config"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgStringOptions = make(map[string]config.StringOption)
|
||||
cfgStringArrayOptions = make(map[string]config.StringArrayOption)
|
||||
cfgIntOptions = make(map[string]config.IntOption)
|
||||
cfgBoolOptions = make(map[string]config.BoolOption)
|
||||
|
||||
cfgOptionDefaultActionKey = "filter/mode"
|
||||
cfgOptionDefaultAction config.StringOption
|
||||
|
||||
cfgOptionDisableAutoPermitKey = "filter/disableAutoPermit"
|
||||
cfgOptionDisableAutoPermit config.IntOption // security level option
|
||||
|
||||
cfgOptionEndpointsKey = "filter/endpoints"
|
||||
cfgOptionEndpoints config.StringArrayOption
|
||||
|
||||
cfgOptionServiceEndpointsKey = "filter/serviceEndpoints"
|
||||
cfgOptionServiceEndpoints config.StringArrayOption
|
||||
|
||||
cfgOptionBlockScopeLocalKey = "filter/blockLocal"
|
||||
cfgOptionBlockScopeLocal config.IntOption // security level option
|
||||
|
||||
cfgOptionBlockScopeLANKey = "filter/blockLAN"
|
||||
cfgOptionBlockScopeLAN config.IntOption // security level option
|
||||
|
||||
cfgOptionBlockScopeInternetKey = "filter/blockInternet"
|
||||
cfgOptionBlockScopeInternet config.IntOption // security level option
|
||||
|
||||
cfgOptionBlockP2PKey = "filter/blockP2P"
|
||||
cfgOptionBlockP2P config.IntOption // security level option
|
||||
|
||||
cfgOptionBlockInboundKey = "filter/blockInbound"
|
||||
cfgOptionBlockInbound config.IntOption // security level option
|
||||
|
||||
cfgOptionEnforceSPNKey = "filter/enforceSPN"
|
||||
cfgOptionEnforceSPN config.IntOption // security level option
|
||||
)
|
||||
|
||||
func registerConfiguration() error {
|
||||
// Default Filter Action
|
||||
// permit - blacklist mode: everything is permitted unless blocked
|
||||
// ask - ask mode: if not verdict is found, the user is consulted
|
||||
// block - whitelist mode: everything is blocked unless permitted
|
||||
err := config.Register(&config.Option{
|
||||
Name: "Default Filter Action",
|
||||
Key: cfgOptionDefaultActionKey,
|
||||
Description: `The default filter action when nothing else permits or blocks a connection.`,
|
||||
OptType: config.OptTypeString,
|
||||
DefaultValue: "permit",
|
||||
ValidationRegex: "^(permit|ask|block)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionDefaultAction = config.Concurrent.GetAsString(cfgOptionDefaultActionKey, "permit")
|
||||
cfgStringOptions[cfgOptionDefaultActionKey] = cfgOptionDefaultAction
|
||||
|
||||
// Disable Auto Permit
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Disable Auto Permit",
|
||||
Key: cfgOptionDisableAutoPermitKey,
|
||||
Description: "Auto Permit searches for a relation between an app and the destionation of a connection - if there is a correlation, the connection will be permitted. This setting is negated in order to provide a streamlined user experience, where higher settings are better.",
|
||||
OptType: config.OptTypeInt,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: 4,
|
||||
ValidationRegex: "^(4|6|7)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionDisableAutoPermit = config.Concurrent.GetAsInt(cfgOptionDisableAutoPermitKey, 4)
|
||||
cfgIntOptions[cfgOptionDisableAutoPermitKey] = cfgOptionDisableAutoPermit
|
||||
|
||||
// Endpoint Filter List
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Endpoint Filter List",
|
||||
Key: cfgOptionEndpointsKey,
|
||||
Description: "Filter outgoing connections by matching the destination endpoint. Network Scope restrictions still apply.",
|
||||
Help: `Format:
|
||||
Permission:
|
||||
"+": permit
|
||||
"-": block
|
||||
Host Matching:
|
||||
IP, CIDR, Country Code, ASN, "*" for any
|
||||
Domains:
|
||||
"example.com": exact match
|
||||
".example.com": exact match + subdomains
|
||||
"*xample.com": prefix wildcard
|
||||
"example.*": suffix wildcard
|
||||
"*example*": prefix and suffix wildcard
|
||||
Protocol and Port Matching (optional):
|
||||
<protocol>/<port>
|
||||
|
||||
Examples:
|
||||
+ .example.com */HTTP
|
||||
- .example.com
|
||||
+ 192.168.0.1/24`,
|
||||
OptType: config.OptTypeStringArray,
|
||||
DefaultValue: nil,
|
||||
ExternalOptType: "endpoint list",
|
||||
ValidationRegex: `^(+|-) [A-z0-9\.:-*/]+( [A-z0-9/]+)?$`,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionEndpoints = config.Concurrent.GetAsStringArray(cfgOptionEndpointsKey, nil)
|
||||
cfgStringArrayOptions[cfgOptionEndpointsKey] = cfgOptionEndpoints
|
||||
|
||||
// Service Endpoint Filter List
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Service Endpoint Filter List",
|
||||
Key: cfgOptionServiceEndpointsKey,
|
||||
Description: "Filter incoming connections by matching the source endpoint. Network Scope restrictions and the inbound permission still apply. Also not that the implicit default action of this list is to always block.",
|
||||
OptType: config.OptTypeStringArray,
|
||||
DefaultValue: nil,
|
||||
ExternalOptType: "endpoint list",
|
||||
ValidationRegex: `^(+|-) [A-z0-9\.:-*/]+( [A-z0-9/]+)?$`,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionServiceEndpoints = config.Concurrent.GetAsStringArray(cfgOptionServiceEndpointsKey, nil)
|
||||
cfgStringArrayOptions[cfgOptionServiceEndpointsKey] = cfgOptionServiceEndpoints
|
||||
|
||||
// Block Scope Local
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Block Scope Local",
|
||||
Key: cfgOptionBlockScopeLocalKey,
|
||||
Description: "Block connections to your own device, ie. localhost.",
|
||||
OptType: config.OptTypeInt,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: 0,
|
||||
ValidationRegex: "^(0|4|6|7)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionBlockScopeLocal = config.Concurrent.GetAsInt(cfgOptionBlockScopeLocalKey, 0)
|
||||
cfgIntOptions[cfgOptionBlockScopeLocalKey] = cfgOptionBlockScopeLocal
|
||||
|
||||
// Block Scope LAN
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Block Scope LAN",
|
||||
Key: cfgOptionBlockScopeLANKey,
|
||||
Description: "Block connections to the Local Area Network.",
|
||||
OptType: config.OptTypeInt,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: 0,
|
||||
ValidationRegex: "^(0|4|6|7)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionBlockScopeLAN = config.Concurrent.GetAsInt(cfgOptionBlockScopeLANKey, 0)
|
||||
cfgIntOptions[cfgOptionBlockScopeLANKey] = cfgOptionBlockScopeLAN
|
||||
|
||||
// Block Scope Internet
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Block Scope Internet",
|
||||
Key: cfgOptionBlockScopeInternetKey,
|
||||
Description: "Block connections to the Internet.",
|
||||
OptType: config.OptTypeInt,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: 0,
|
||||
ValidationRegex: "^(0|4|6|7)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionBlockScopeInternet = config.Concurrent.GetAsInt(cfgOptionBlockScopeInternetKey, 0)
|
||||
cfgIntOptions[cfgOptionBlockScopeInternetKey] = cfgOptionBlockScopeInternet
|
||||
|
||||
// Block Peer to Peer Connections
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Block Peer to Peer Connections",
|
||||
Key: cfgOptionBlockP2PKey,
|
||||
Description: "Block peer to peer connections. These are connections that are established directly to an IP address on the Internet without resolving a domain name via DNS first.",
|
||||
OptType: config.OptTypeInt,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: 7,
|
||||
ValidationRegex: "^(4|6|7)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionBlockP2P = config.Concurrent.GetAsInt(cfgOptionBlockP2PKey, 7)
|
||||
cfgIntOptions[cfgOptionBlockP2PKey] = cfgOptionBlockP2P
|
||||
|
||||
// Block Inbound Connections
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Block Inbound Connections",
|
||||
Key: cfgOptionBlockInboundKey,
|
||||
Description: "Block inbound connections to your device. This will usually only be the case if you are running a network service or are using peer to peer software.",
|
||||
OptType: config.OptTypeInt,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: 4,
|
||||
ValidationRegex: "^(4|6|7)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionBlockInbound = config.Concurrent.GetAsInt(cfgOptionBlockInboundKey, 6)
|
||||
cfgIntOptions[cfgOptionBlockInboundKey] = cfgOptionBlockInbound
|
||||
|
||||
// Enforce SPN
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Enforce SPN",
|
||||
Key: cfgOptionEnforceSPNKey,
|
||||
Description: "This setting enforces connections to be routed over the SPN. If this is not possible for any reason, connections will be blocked.",
|
||||
OptType: config.OptTypeInt,
|
||||
ReleaseLevel: config.ReleaseLevelExperimental,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: 0,
|
||||
ValidationRegex: "^(0|4|6|7)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionEnforceSPN = config.Concurrent.GetAsInt(cfgOptionEnforceSPNKey, 0)
|
||||
cfgIntOptions[cfgOptionEnforceSPNKey] = cfgOptionEnforceSPN
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,22 +1,105 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/database/record"
|
||||
)
|
||||
|
||||
// core:profiles/user/12345-1234-125-1234-1235
|
||||
// core:profiles/special/default
|
||||
// /global
|
||||
// core:profiles/stamp/12334-1235-1234-5123-1234
|
||||
// core:profiles/identifier/base64
|
||||
// Database paths:
|
||||
// core:profiles/<scope>/<id>
|
||||
// cache:profiles/index/<identifier>/<value>
|
||||
|
||||
// Namespaces
|
||||
const (
|
||||
UserNamespace = "user"
|
||||
StampNamespace = "stamp"
|
||||
SpecialNamespace = "special"
|
||||
profilesDBPath = "core:profiles/"
|
||||
)
|
||||
|
||||
var (
|
||||
profileDB = database.NewInterface(nil)
|
||||
)
|
||||
|
||||
func makeScopedID(source, id string) string {
|
||||
return source + "/" + id
|
||||
}
|
||||
|
||||
func makeProfileKey(source, id string) string {
|
||||
return profilesDBPath + source + "/" + id
|
||||
}
|
||||
|
||||
func registerValidationDBHook() (err error) {
|
||||
_, err = database.RegisterHook(query.New(profilesDBPath), &databaseHook{})
|
||||
return
|
||||
}
|
||||
|
||||
func startProfileUpdateChecker() error {
|
||||
profilesSub, err := profileDB.Subscribe(query.New(profilesDBPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
module.StartServiceWorker("update active profiles", 0, func(ctx context.Context) (err error) {
|
||||
feedSelect:
|
||||
for {
|
||||
select {
|
||||
case r := <-profilesSub.Feed:
|
||||
// check if nil
|
||||
if r == nil {
|
||||
return errors.New("subscription canceled")
|
||||
}
|
||||
|
||||
// check if internal save
|
||||
if !r.IsWrapped() {
|
||||
profile, ok := r.(*Profile)
|
||||
if ok && profile.internalSave {
|
||||
continue feedSelect
|
||||
}
|
||||
}
|
||||
|
||||
// mark as outdated
|
||||
markActiveProfileAsOutdated(strings.TrimPrefix(r.Key(), profilesDBPath))
|
||||
case <-ctx.Done():
|
||||
return profilesSub.Cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type databaseHook struct {
|
||||
database.HookBase
|
||||
}
|
||||
|
||||
// UsesPrePut implements the Hook interface and returns false.
|
||||
func (h *databaseHook) UsesPrePut() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PrePut implements the Hook interface.
|
||||
func (h *databaseHook) PrePut(r record.Record) (record.Record, error) {
|
||||
// convert
|
||||
profile, err := EnsureProfile(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// prepare config
|
||||
err = profile.prepConfig()
|
||||
if err != nil {
|
||||
// error here, warning when loading
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse config
|
||||
err = profile.parseConfig()
|
||||
if err != nil {
|
||||
// error here, warning when loading
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"github.com/safing/portmaster/status"
|
||||
)
|
||||
|
||||
func makeDefaultGlobalProfile() *Profile {
|
||||
return &Profile{
|
||||
ID: "global",
|
||||
Name: "Global Profile",
|
||||
}
|
||||
}
|
||||
|
||||
func makeDefaultFallbackProfile() *Profile {
|
||||
return &Profile{
|
||||
ID: "fallback",
|
||||
Name: "Fallback Profile",
|
||||
Flags: map[uint8]uint8{
|
||||
// Profile Modes
|
||||
Blacklist: status.SecurityLevelsDynamicAndSecure,
|
||||
Whitelist: status.SecurityLevelFortress,
|
||||
|
||||
// Network Locations
|
||||
Internet: status.SecurityLevelsAll,
|
||||
LAN: status.SecurityLevelDynamic,
|
||||
Localhost: status.SecurityLevelsAll,
|
||||
|
||||
// Specials
|
||||
Related: status.SecurityLevelDynamic,
|
||||
},
|
||||
ServiceEndpoints: []*EndpointPermission{
|
||||
{
|
||||
Type: EptAny,
|
||||
Protocol: 0,
|
||||
StartPort: 0,
|
||||
EndPort: 0,
|
||||
Permit: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,319 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/intel"
|
||||
)
|
||||
|
||||
// Endpoints is a list of permitted or denied endpoints.
|
||||
type Endpoints []*EndpointPermission
|
||||
|
||||
// EndpointPermission holds a decision about an endpoint.
|
||||
type EndpointPermission struct {
|
||||
Value string
|
||||
Type EPType
|
||||
|
||||
Protocol uint8
|
||||
StartPort uint16
|
||||
EndPort uint16
|
||||
|
||||
Permit bool
|
||||
Created int64
|
||||
}
|
||||
|
||||
// EPType represents the type of an EndpointPermission
|
||||
type EPType uint8
|
||||
|
||||
// EPType values
|
||||
const (
|
||||
EptUnknown EPType = 0
|
||||
EptAny EPType = 1
|
||||
EptDomain EPType = 2
|
||||
EptIPv4 EPType = 3
|
||||
EptIPv6 EPType = 4
|
||||
EptIPv4Range EPType = 5
|
||||
EptIPv6Range EPType = 6
|
||||
EptASN EPType = 7
|
||||
EptCountry EPType = 8
|
||||
)
|
||||
|
||||
// EPResult represents the result of a check against an EndpointPermission
|
||||
type EPResult uint8
|
||||
|
||||
// EndpointPermission return values
|
||||
const (
|
||||
NoMatch EPResult = iota
|
||||
Undeterminable
|
||||
Denied
|
||||
Permitted
|
||||
)
|
||||
|
||||
// IsSet returns whether the Endpoints object is "set".
|
||||
func (e Endpoints) IsSet() bool {
|
||||
return len(e) > 0
|
||||
}
|
||||
|
||||
// CheckDomain checks the if the given endpoint matches a EndpointPermission in the list.
|
||||
func (e Endpoints) CheckDomain(domain string) (result EPResult, reason string) {
|
||||
if domain == "" {
|
||||
return Denied, "internal error"
|
||||
}
|
||||
|
||||
for _, entry := range e {
|
||||
if entry != nil {
|
||||
if result, reason = entry.MatchesDomain(domain); result != NoMatch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
// CheckIP checks the if the given endpoint matches a EndpointPermission in the list. If _checkReverseIP_ and no domain is given, the IP will be resolved to a domain, if necessary.
|
||||
func (e Endpoints) CheckIP(domain string, ip net.IP, protocol uint8, port uint16, checkReverseIP bool, securityLevel uint8) (result EPResult, reason string) {
|
||||
if ip == nil {
|
||||
return Denied, "internal error"
|
||||
}
|
||||
|
||||
// ip resolving
|
||||
var cachedGetDomainOfIP func() string
|
||||
if checkReverseIP {
|
||||
var ipResolved bool
|
||||
var ipName string
|
||||
// setup caching wrapper
|
||||
cachedGetDomainOfIP = func() string {
|
||||
if !ipResolved {
|
||||
result, err := intel.ResolveIPAndValidate(context.TODO(), ip.String(), securityLevel)
|
||||
if err != nil {
|
||||
// log.Debug()
|
||||
ipName = result
|
||||
}
|
||||
ipResolved = true
|
||||
}
|
||||
return ipName
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range e {
|
||||
if entry != nil {
|
||||
if result, reason := entry.MatchesIP(domain, ip, protocol, port, cachedGetDomainOfIP); result != NoMatch {
|
||||
return result, reason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
func (ep EndpointPermission) matchesDomainOnly(domain string) (matches bool, reason string) {
|
||||
dotInFront := strings.HasPrefix(ep.Value, ".")
|
||||
wildcardInFront := strings.HasPrefix(ep.Value, "*")
|
||||
wildcardInBack := strings.HasSuffix(ep.Value, "*")
|
||||
|
||||
switch {
|
||||
case dotInFront && !wildcardInFront && !wildcardInBack:
|
||||
// subdomain or domain
|
||||
if strings.HasSuffix(domain, ep.Value) || domain == strings.TrimPrefix(ep.Value, ".") {
|
||||
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
|
||||
}
|
||||
case wildcardInFront && wildcardInBack:
|
||||
if strings.Contains(domain, strings.Trim(ep.Value, "*")) {
|
||||
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
|
||||
}
|
||||
case wildcardInFront:
|
||||
if strings.HasSuffix(domain, strings.TrimLeft(ep.Value, "*")) {
|
||||
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
|
||||
}
|
||||
case wildcardInBack:
|
||||
if strings.HasPrefix(domain, strings.TrimRight(ep.Value, "*")) {
|
||||
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
|
||||
}
|
||||
default:
|
||||
if domain == ep.Value {
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (ep EndpointPermission) matchProtocolAndPortsAndReturn(protocol uint8, port uint16) (result EPResult) {
|
||||
// only check if protocol is defined
|
||||
if ep.Protocol > 0 {
|
||||
// if protocol is unknown, return Undeterminable
|
||||
if protocol == 0 {
|
||||
return Undeterminable
|
||||
}
|
||||
// if protocol does not match, return NoMatch
|
||||
if protocol != ep.Protocol {
|
||||
return NoMatch
|
||||
}
|
||||
}
|
||||
|
||||
// only check if port is defined
|
||||
if ep.StartPort > 0 {
|
||||
// if port is unknown, return Undeterminable
|
||||
if port == 0 {
|
||||
return Undeterminable
|
||||
}
|
||||
// if port does not match, return NoMatch
|
||||
if port < ep.StartPort || port > ep.EndPort {
|
||||
return NoMatch
|
||||
}
|
||||
}
|
||||
|
||||
// protocol and port matched or were defined as any
|
||||
if ep.Permit {
|
||||
return Permitted
|
||||
}
|
||||
return Denied
|
||||
}
|
||||
|
||||
// MatchesDomain checks if the given endpoint matches the EndpointPermission.
|
||||
func (ep EndpointPermission) MatchesDomain(domain string) (result EPResult, reason string) {
|
||||
switch ep.Type {
|
||||
case EptAny:
|
||||
// always matches
|
||||
case EptDomain:
|
||||
var matched bool
|
||||
matched, reason = ep.matchesDomainOnly(domain)
|
||||
if !matched {
|
||||
return NoMatch, ""
|
||||
}
|
||||
case EptIPv4:
|
||||
return Undeterminable, ""
|
||||
case EptIPv6:
|
||||
return Undeterminable, ""
|
||||
case EptIPv4Range:
|
||||
return Undeterminable, ""
|
||||
case EptIPv6Range:
|
||||
return Undeterminable, ""
|
||||
case EptASN:
|
||||
return Undeterminable, ""
|
||||
case EptCountry:
|
||||
return Undeterminable, ""
|
||||
default:
|
||||
return Denied, "encountered unknown enpoint permission type"
|
||||
}
|
||||
|
||||
return ep.matchProtocolAndPortsAndReturn(0, 0), reason
|
||||
}
|
||||
|
||||
// MatchesIP checks if the given endpoint matches the EndpointPermission. _getDomainOfIP_, if given, will be used to get the domain if not given.
|
||||
func (ep EndpointPermission) MatchesIP(domain string, ip net.IP, protocol uint8, port uint16, getDomainOfIP func() string) (result EPResult, reason string) {
|
||||
switch ep.Type {
|
||||
case EptAny:
|
||||
// always matches
|
||||
case EptDomain:
|
||||
if domain == "" {
|
||||
if getDomainOfIP == nil {
|
||||
return NoMatch, ""
|
||||
}
|
||||
domain = getDomainOfIP()
|
||||
}
|
||||
|
||||
var matched bool
|
||||
matched, reason = ep.matchesDomainOnly(domain)
|
||||
if !matched {
|
||||
return NoMatch, ""
|
||||
}
|
||||
case EptIPv4, EptIPv6:
|
||||
if ep.Value != ip.String() {
|
||||
return NoMatch, ""
|
||||
}
|
||||
case EptIPv4Range:
|
||||
return Denied, "endpoint type IP Range not yet implemented"
|
||||
case EptIPv6Range:
|
||||
return Denied, "endpoint type IP Range not yet implemented"
|
||||
case EptASN:
|
||||
return Denied, "endpoint type ASN not yet implemented"
|
||||
case EptCountry:
|
||||
return Denied, "endpoint type country not yet implemented"
|
||||
default:
|
||||
return Denied, "encountered unknown enpoint permission type"
|
||||
}
|
||||
|
||||
return ep.matchProtocolAndPortsAndReturn(protocol, port), reason
|
||||
}
|
||||
|
||||
func (e Endpoints) String() string {
|
||||
s := make([]string, 0, len(e))
|
||||
for _, entry := range e {
|
||||
s = append(s, entry.String())
|
||||
}
|
||||
return fmt.Sprintf("[%s]", strings.Join(s, ", "))
|
||||
}
|
||||
|
||||
func (ept EPType) String() string {
|
||||
switch ept {
|
||||
case EptAny:
|
||||
return "Any"
|
||||
case EptDomain:
|
||||
return "Domain"
|
||||
case EptIPv4:
|
||||
return "IPv4"
|
||||
case EptIPv6:
|
||||
return "IPv6"
|
||||
case EptIPv4Range:
|
||||
return "IPv4-Range"
|
||||
case EptIPv6Range:
|
||||
return "IPv6-Range"
|
||||
case EptASN:
|
||||
return "ASN"
|
||||
case EptCountry:
|
||||
return "Country"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (ep EndpointPermission) String() string {
|
||||
s := ep.Type.String()
|
||||
|
||||
if ep.Type != EptAny {
|
||||
s += ":"
|
||||
s += ep.Value
|
||||
}
|
||||
s += " "
|
||||
|
||||
if ep.Protocol > 0 {
|
||||
s += strconv.Itoa(int(ep.Protocol))
|
||||
} else {
|
||||
s += "*"
|
||||
}
|
||||
|
||||
s += "/"
|
||||
|
||||
if ep.StartPort > 0 {
|
||||
if ep.StartPort == ep.EndPort {
|
||||
s += strconv.Itoa(int(ep.StartPort))
|
||||
} else {
|
||||
s += fmt.Sprintf("%d-%d", ep.StartPort, ep.EndPort)
|
||||
}
|
||||
} else {
|
||||
s += "*"
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (epr EPResult) String() string {
|
||||
switch epr {
|
||||
case NoMatch:
|
||||
return "No Match"
|
||||
case Undeterminable:
|
||||
return "Undeterminable"
|
||||
case Denied:
|
||||
return "Denied"
|
||||
case Permitted:
|
||||
return "Permitted"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testEndpointDomainMatch(t *testing.T, ep *EndpointPermission, domain string, expectedResult EPResult) {
|
||||
var result EPResult
|
||||
result, _ = ep.MatchesDomain(domain)
|
||||
if result != expectedResult {
|
||||
t.Errorf(
|
||||
"line %d: unexpected result for endpoint domain match %s: result=%s, expected=%s",
|
||||
getLineNumberOfCaller(1),
|
||||
domain,
|
||||
result,
|
||||
expectedResult,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testEndpointIPMatch(t *testing.T, ep *EndpointPermission, domain string, ip net.IP, protocol uint8, port uint16, expectedResult EPResult) {
|
||||
var result EPResult
|
||||
result, _ = ep.MatchesIP(domain, ip, protocol, port, nil)
|
||||
if result != expectedResult {
|
||||
t.Errorf(
|
||||
"line %d: unexpected result for endpoint %s/%s/%d/%d: result=%s, expected=%s",
|
||||
getLineNumberOfCaller(1),
|
||||
domain,
|
||||
ip,
|
||||
protocol,
|
||||
port,
|
||||
result,
|
||||
expectedResult,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointMatching(t *testing.T) {
|
||||
ep := &EndpointPermission{
|
||||
Type: EptAny,
|
||||
Protocol: 0,
|
||||
StartPort: 0,
|
||||
EndPort: 0,
|
||||
Permit: true,
|
||||
}
|
||||
|
||||
// ANY
|
||||
|
||||
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
|
||||
// DOMAIN
|
||||
|
||||
// wildcard domains
|
||||
ep.Type = EptDomain
|
||||
ep.Value = "*example.com."
|
||||
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
testEndpointDomainMatch(t, ep, "abc-example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "abc-example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
|
||||
ep.Value = "*.example.com."
|
||||
testEndpointDomainMatch(t, ep, "example.com.", NoMatch)
|
||||
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
|
||||
testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
testEndpointDomainMatch(t, ep, "abc-example.com.", NoMatch)
|
||||
testEndpointIPMatch(t, ep, "abc-example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
|
||||
|
||||
ep.Value = ".example.com."
|
||||
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
testEndpointDomainMatch(t, ep, "abc-example.com.", NoMatch)
|
||||
testEndpointIPMatch(t, ep, "abc-example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
|
||||
|
||||
ep.Value = "example.*"
|
||||
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
testEndpointDomainMatch(t, ep, "abc.example.com.", NoMatch)
|
||||
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
|
||||
|
||||
ep.Value = ".example.*"
|
||||
testEndpointDomainMatch(t, ep, "example.com.", NoMatch)
|
||||
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
|
||||
testEndpointDomainMatch(t, ep, "abc.example.com.", NoMatch)
|
||||
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
|
||||
|
||||
ep.Value = "*.exampl*"
|
||||
testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
|
||||
ep.Value = "*.com."
|
||||
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
|
||||
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
|
||||
|
||||
// edge case
|
||||
ep.Value = ""
|
||||
testEndpointDomainMatch(t, ep, "example.com", NoMatch)
|
||||
|
||||
// edge case
|
||||
ep.Value = "*"
|
||||
testEndpointDomainMatch(t, ep, "example.com", Permitted)
|
||||
|
||||
// edge case
|
||||
ep.Value = "**"
|
||||
testEndpointDomainMatch(t, ep, "example.com", Permitted)
|
||||
|
||||
// edge case
|
||||
ep.Value = "***"
|
||||
testEndpointDomainMatch(t, ep, "example.com", Permitted)
|
||||
|
||||
// protocol
|
||||
ep.Value = "example.com"
|
||||
ep.Protocol = 17
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
|
||||
testEndpointDomainMatch(t, ep, "example.com", Undeterminable)
|
||||
|
||||
// ports
|
||||
ep.StartPort = 442
|
||||
ep.EndPort = 444
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
|
||||
ep.StartPort = 442
|
||||
ep.StartPort = 443
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
|
||||
ep.StartPort = 443
|
||||
ep.EndPort = 444
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
|
||||
ep.StartPort = 443
|
||||
ep.EndPort = 443
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
|
||||
testEndpointDomainMatch(t, ep, "example.com", Undeterminable)
|
||||
|
||||
// IP
|
||||
|
||||
ep.Type = EptIPv4
|
||||
ep.Value = "10.2.3.4"
|
||||
ep.Protocol = 0
|
||||
ep.StartPort = 0
|
||||
ep.EndPort = 0
|
||||
testEndpointIPMatch(t, ep, "", net.ParseIP("10.2.3.4"), 6, 80, Permitted)
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
|
||||
testEndpointIPMatch(t, ep, "", net.ParseIP("10.2.3.5"), 6, 80, NoMatch)
|
||||
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.5"), 17, 443, NoMatch)
|
||||
testEndpointDomainMatch(t, ep, "example.com", Undeterminable)
|
||||
}
|
||||
|
||||
func TestEPString(t *testing.T) {
|
||||
var endpoints Endpoints = []*EndpointPermission{
|
||||
{
|
||||
Type: EptDomain,
|
||||
Value: "example.com",
|
||||
Protocol: 6,
|
||||
Permit: true,
|
||||
},
|
||||
{
|
||||
Type: EptIPv4,
|
||||
Value: "1.1.1.1",
|
||||
Protocol: 17, // TCP
|
||||
StartPort: 53, // DNS
|
||||
EndPort: 53,
|
||||
Permit: false,
|
||||
},
|
||||
{
|
||||
Type: EptDomain,
|
||||
Value: "example.org",
|
||||
Permit: false,
|
||||
},
|
||||
}
|
||||
if endpoints.String() != "[Domain:example.com 6/*, IPv4:1.1.1.1 17/53, Domain:example.org */*]" {
|
||||
t.Errorf("unexpected result: %s", endpoints.String())
|
||||
}
|
||||
|
||||
var noEndpoints Endpoints = []*EndpointPermission{}
|
||||
if noEndpoints.String() != "[]" {
|
||||
t.Errorf("unexpected result: %s", noEndpoints.String())
|
||||
}
|
||||
}
|
130
profile/flags.go
130
profile/flags.go
|
@ -1,130 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/status"
|
||||
)
|
||||
|
||||
// Flags are used to quickly add common attributes to profiles
|
||||
type Flags map[uint8]uint8
|
||||
|
||||
// Profile Flags
|
||||
const (
|
||||
// Profile Modes
|
||||
Prompt uint8 = 0 // Prompt first-seen connections
|
||||
Blacklist uint8 = 1 // Allow everything not explicitly denied
|
||||
Whitelist uint8 = 2 // Only allow everything explicitly allowed
|
||||
|
||||
// Network Locations
|
||||
Internet uint8 = 16 // Allow connections to the Internet
|
||||
LAN uint8 = 17 // Allow connections to the local area network
|
||||
Localhost uint8 = 18 // Allow connections on the local host
|
||||
|
||||
// Specials
|
||||
Related uint8 = 32 // If and before prompting, allow domains that are related to the program
|
||||
PeerToPeer uint8 = 33 // Allow program to directly communicate with peers, without resolving DNS first
|
||||
Service uint8 = 34 // Allow program to accept incoming connections
|
||||
Independent uint8 = 35 // Ignore profile settings coming from the Community
|
||||
RequireGate17 uint8 = 36 // Require all connections to go over Gate17
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFlagsParseFailed is returned if a an invalid flag is encountered while parsing
|
||||
ErrFlagsParseFailed = errors.New("profiles: failed to parse flags")
|
||||
|
||||
sortedFlags = []uint8{
|
||||
Prompt,
|
||||
Blacklist,
|
||||
Whitelist,
|
||||
Internet,
|
||||
LAN,
|
||||
Localhost,
|
||||
Related,
|
||||
PeerToPeer,
|
||||
Service,
|
||||
Independent,
|
||||
RequireGate17,
|
||||
}
|
||||
|
||||
flagIDs = map[string]uint8{
|
||||
"Prompt": Prompt,
|
||||
"Blacklist": Blacklist,
|
||||
"Whitelist": Whitelist,
|
||||
"Internet": Internet,
|
||||
"LAN": LAN,
|
||||
"Localhost": Localhost,
|
||||
"Related": Related,
|
||||
"PeerToPeer": PeerToPeer,
|
||||
"Service": Service,
|
||||
"Independent": Independent,
|
||||
"RequireGate17": RequireGate17,
|
||||
}
|
||||
|
||||
flagNames = map[uint8]string{
|
||||
Prompt: "Prompt",
|
||||
Blacklist: "Blacklist",
|
||||
Whitelist: "Whitelist",
|
||||
Internet: "Internet",
|
||||
LAN: "LAN",
|
||||
Localhost: "Localhost",
|
||||
Related: "Related",
|
||||
PeerToPeer: "PeerToPeer",
|
||||
Service: "Service",
|
||||
Independent: "Independent",
|
||||
RequireGate17: "RequireGate17",
|
||||
}
|
||||
)
|
||||
|
||||
// Check checks if a flag is set at all and if it's active in the given security level.
|
||||
func (flags Flags) Check(flag, level uint8) (active bool, ok bool) {
|
||||
if flags == nil {
|
||||
return false, false
|
||||
}
|
||||
|
||||
setting, ok := flags[flag]
|
||||
if ok {
|
||||
if setting&level > 0 {
|
||||
return true, true
|
||||
}
|
||||
return false, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func getLevelMarker(levels, level uint8) string {
|
||||
if levels&level > 0 {
|
||||
return "+"
|
||||
}
|
||||
return "-"
|
||||
}
|
||||
|
||||
// String return a string representation of Flags
|
||||
func (flags Flags) String() string {
|
||||
var markedFlags []string
|
||||
for _, flag := range sortedFlags {
|
||||
levels, ok := flags[flag]
|
||||
if ok {
|
||||
s := flagNames[flag]
|
||||
if levels != status.SecurityLevelsAll {
|
||||
s += getLevelMarker(levels, status.SecurityLevelDynamic)
|
||||
s += getLevelMarker(levels, status.SecurityLevelSecure)
|
||||
s += getLevelMarker(levels, status.SecurityLevelFortress)
|
||||
}
|
||||
markedFlags = append(markedFlags, s)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("[%s]", strings.Join(markedFlags, ", "))
|
||||
}
|
||||
|
||||
// Add adds a flag to the Flags with the given level.
|
||||
func (flags Flags) Add(flag, levels uint8) {
|
||||
flags[flag] = levels
|
||||
}
|
||||
|
||||
// Remove removes a flag from the Flags.
|
||||
func (flags Flags) Remove(flag uint8) {
|
||||
delete(flags, flag)
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/status"
|
||||
)
|
||||
|
||||
func TestProfileFlags(t *testing.T) {
|
||||
|
||||
// check if all IDs have a name
|
||||
for key, entry := range flagIDs {
|
||||
if _, ok := flagNames[entry]; !ok {
|
||||
t.Errorf("could not find entry for %s in flagNames", key)
|
||||
}
|
||||
}
|
||||
|
||||
// check if all names have an ID
|
||||
for key, entry := range flagNames {
|
||||
if _, ok := flagIDs[entry]; !ok {
|
||||
t.Errorf("could not find entry for %d in flagNames", key)
|
||||
}
|
||||
}
|
||||
|
||||
testFlags := Flags{
|
||||
Prompt: status.SecurityLevelsAll,
|
||||
Internet: status.SecurityLevelsDynamicAndSecure,
|
||||
LAN: status.SecurityLevelsDynamicAndSecure,
|
||||
Localhost: status.SecurityLevelsAll,
|
||||
Related: status.SecurityLevelDynamic,
|
||||
RequireGate17: status.SecurityLevelsSecureAndFortress,
|
||||
}
|
||||
|
||||
if testFlags.String() != "[Prompt, Internet++-, LAN++-, Localhost, Related+--, RequireGate17-++]" {
|
||||
t.Errorf("unexpected output: %s", testFlags.String())
|
||||
}
|
||||
|
||||
// // check Has
|
||||
// emptyFlags := ProfileFlags{}
|
||||
// for flag, name := range flagNames {
|
||||
// if !sortedFlags.Has(flag) {
|
||||
// t.Errorf("sortedFlags should have flag %s (%d)", name, flag)
|
||||
// }
|
||||
// if emptyFlags.Has(flag) {
|
||||
// t.Errorf("emptyFlags should not have flag %s (%d)", name, flag)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // check ProfileFlags creation from strings
|
||||
// var allFlagStrings []string
|
||||
// for _, flag := range *sortedFlags {
|
||||
// allFlagStrings = append(allFlagStrings, flagNames[flag])
|
||||
// }
|
||||
// newFlags, err := FlagsFromNames(allFlagStrings)
|
||||
// if err != nil {
|
||||
// t.Errorf("error while parsing flags: %s", err)
|
||||
// }
|
||||
// if newFlags.String() != sortedFlags.String() {
|
||||
// t.Errorf("parsed flags are not correct (or tests have not been updated to reflect the right number), expected %v, got %v", *sortedFlags, *newFlags)
|
||||
// }
|
||||
//
|
||||
// // check ProfileFlags Stringer
|
||||
// flagString := newFlags.String()
|
||||
// check := strings.Join(allFlagStrings, ",")
|
||||
// if flagString != check {
|
||||
// t.Errorf("flag string is not correct, expected %s, got %s", check, flagString)
|
||||
// }
|
||||
|
||||
}
|
|
@ -8,22 +8,37 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
shutdownSignal = make(chan struct{})
|
||||
module *modules.Module
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register("profile", nil, start, stop, "core")
|
||||
module = modules.Register("profiles", prep, start, nil, "core")
|
||||
}
|
||||
|
||||
func start() error {
|
||||
err := initSpecialProfiles()
|
||||
func prep() error {
|
||||
err := registerConfiguration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = registerConfigUpdater()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func start() error {
|
||||
err := registerValidationDBHook()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = startProfileUpdateChecker()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return initUpdateListener()
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
close(shutdownSignal)
|
||||
return nil
|
||||
}
|
||||
|
|
309
profile/profile-layered.go
Normal file
309
profile/profile-layered.go
Normal file
|
@ -0,0 +1,309 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
|
||||
"github.com/safing/portmaster/status"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portmaster/intel"
|
||||
"github.com/safing/portmaster/profile/endpoints"
|
||||
)
|
||||
|
||||
var (
|
||||
no = abool.NewBool(false)
|
||||
)
|
||||
|
||||
// LayeredProfile combines multiple Profiles.
|
||||
type LayeredProfile struct {
|
||||
lock sync.Mutex
|
||||
|
||||
localProfile *Profile
|
||||
layers []*Profile
|
||||
|
||||
validityFlag *abool.AtomicBool
|
||||
validityFlagLock sync.Mutex
|
||||
globalValidityFlag *config.ValidityFlag
|
||||
|
||||
securityLevel *uint32
|
||||
|
||||
DisableAutoPermit config.BoolOption
|
||||
BlockScopeLocal config.BoolOption
|
||||
BlockScopeLAN config.BoolOption
|
||||
BlockScopeInternet config.BoolOption
|
||||
BlockP2P config.BoolOption
|
||||
BlockInbound config.BoolOption
|
||||
EnforceSPN config.BoolOption
|
||||
}
|
||||
|
||||
// NewLayeredProfile returns a new layered profile based on the given local profile.
|
||||
func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
|
||||
var securityLevelVal uint32
|
||||
|
||||
new := &LayeredProfile{
|
||||
localProfile: localProfile,
|
||||
layers: make([]*Profile, 0, len(localProfile.LinkedProfiles)+1),
|
||||
validityFlag: abool.NewBool(true),
|
||||
globalValidityFlag: config.NewValidityFlag(),
|
||||
securityLevel: &securityLevelVal,
|
||||
}
|
||||
|
||||
new.DisableAutoPermit = new.wrapSecurityLevelOption(
|
||||
cfgOptionDisableAutoPermitKey,
|
||||
cfgOptionDisableAutoPermit,
|
||||
)
|
||||
new.BlockScopeLocal = new.wrapSecurityLevelOption(
|
||||
cfgOptionBlockScopeLocalKey,
|
||||
cfgOptionBlockScopeLocal,
|
||||
)
|
||||
new.BlockScopeLAN = new.wrapSecurityLevelOption(
|
||||
cfgOptionBlockScopeLANKey,
|
||||
cfgOptionBlockScopeLAN,
|
||||
)
|
||||
new.BlockScopeInternet = new.wrapSecurityLevelOption(
|
||||
cfgOptionBlockScopeInternetKey,
|
||||
cfgOptionBlockScopeInternet,
|
||||
)
|
||||
new.BlockP2P = new.wrapSecurityLevelOption(
|
||||
cfgOptionBlockP2PKey,
|
||||
cfgOptionBlockP2P,
|
||||
)
|
||||
new.BlockInbound = new.wrapSecurityLevelOption(
|
||||
cfgOptionBlockInboundKey,
|
||||
cfgOptionBlockInbound,
|
||||
)
|
||||
new.EnforceSPN = new.wrapSecurityLevelOption(
|
||||
cfgOptionEnforceSPNKey,
|
||||
cfgOptionEnforceSPN,
|
||||
)
|
||||
|
||||
// TODO: load referenced profiles
|
||||
|
||||
// FUTURE: load forced company profile
|
||||
new.layers = append(new.layers, localProfile)
|
||||
// FUTURE: load company profile
|
||||
// FUTURE: load community profile
|
||||
|
||||
new.updateCaches()
|
||||
return new
|
||||
}
|
||||
|
||||
func (lp *LayeredProfile) getValidityFlag() *abool.AtomicBool {
|
||||
lp.validityFlagLock.Lock()
|
||||
defer lp.validityFlagLock.Unlock()
|
||||
return lp.validityFlag
|
||||
}
|
||||
|
||||
// Update checks for updated profiles and replaces any outdated profiles.
|
||||
func (lp *LayeredProfile) Update() {
|
||||
lp.lock.Lock()
|
||||
defer lp.lock.Lock()
|
||||
|
||||
var changed bool
|
||||
for i, layer := range lp.layers {
|
||||
if layer.oudated.IsSet() {
|
||||
changed = true
|
||||
// update layer
|
||||
newLayer, err := GetProfile(layer.Source, layer.ID)
|
||||
if err != nil {
|
||||
log.Errorf("profiles: failed to update profile %s", layer.ScopedID())
|
||||
} else {
|
||||
lp.layers[i] = newLayer
|
||||
}
|
||||
}
|
||||
}
|
||||
if !lp.globalValidityFlag.IsValid() {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
// reset validity flag
|
||||
lp.validityFlagLock.Lock()
|
||||
lp.validityFlag.SetTo(false)
|
||||
lp.validityFlag = abool.NewBool(true)
|
||||
lp.validityFlagLock.Unlock()
|
||||
// get global config validity flag
|
||||
lp.globalValidityFlag.Refresh()
|
||||
|
||||
lp.updateCaches()
|
||||
}
|
||||
}
|
||||
|
||||
func (lp *LayeredProfile) updateCaches() {
|
||||
// update security level
|
||||
var newLevel uint8 = 0
|
||||
for _, layer := range lp.layers {
|
||||
if newLevel < layer.SecurityLevel {
|
||||
newLevel = layer.SecurityLevel
|
||||
}
|
||||
}
|
||||
atomic.StoreUint32(lp.securityLevel, uint32(newLevel))
|
||||
|
||||
// TODO: ignore community profiles
|
||||
}
|
||||
|
||||
// SecurityLevel returns the highest security level of all layered profiles.
|
||||
func (lp *LayeredProfile) SecurityLevel() uint8 {
|
||||
return uint8(atomic.LoadUint32(lp.securityLevel))
|
||||
}
|
||||
|
||||
// DefaultAction returns the active default action ID.
|
||||
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.
|
||||
func (lp *LayeredProfile) MatchEndpoint(entity *intel.Entity) (result endpoints.EPResult, reason string) {
|
||||
for _, layer := range lp.layers {
|
||||
if layer.endpoints.IsSet() {
|
||||
result, reason = layer.endpoints.Match(entity)
|
||||
if result != endpoints.NoMatch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfgLock.RLock()
|
||||
defer cfgLock.RUnlock()
|
||||
return cfgEndpoints.Match(entity)
|
||||
}
|
||||
|
||||
// MatchServiceEndpoint checks if the given endpoint of an inbound connection matches an entry in any of the profiles.
|
||||
func (lp *LayeredProfile) MatchServiceEndpoint(entity *intel.Entity) (result endpoints.EPResult, reason string) {
|
||||
entity.EnableReverseResolving()
|
||||
|
||||
for _, layer := range lp.layers {
|
||||
if layer.serviceEndpoints.IsSet() {
|
||||
result, reason = layer.serviceEndpoints.Match(entity)
|
||||
if result != endpoints.NoMatch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfgLock.RLock()
|
||||
defer cfgLock.RUnlock()
|
||||
return cfgServiceEndpoints.Match(entity)
|
||||
}
|
||||
|
||||
/*
|
||||
func (lp *LayeredProfile) wrapSecurityLevelOption(configKey string, globalConfig config.IntOption) config.BoolOption {
|
||||
valid := no
|
||||
var activeAtLevels uint8
|
||||
|
||||
return func() bool {
|
||||
if !valid.IsSet() {
|
||||
valid = lp.getValidityFlag()
|
||||
|
||||
found := false
|
||||
layerLoop:
|
||||
for _, layer := range lp.layers {
|
||||
layerLevel, ok := layer.configPerspective.GetAsInt(configKey)
|
||||
if ok {
|
||||
found = true
|
||||
// TODO: add instead?
|
||||
activeAtLevels = uint8(layerLevel)
|
||||
break layerLoop
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
activeAtLevels = uint8(globalConfig())
|
||||
}
|
||||
}
|
||||
|
||||
return activeAtLevels&max(
|
||||
lp.SecurityLevel(), // layered profile security level
|
||||
status.ActiveSecurityLevel(), // global security level
|
||||
) > 0
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (lp *LayeredProfile) wrapSecurityLevelOption(configKey string, globalConfig config.IntOption) config.BoolOption {
|
||||
activeAtLevels := lp.wrapIntOption(configKey, globalConfig)
|
||||
|
||||
return func() bool {
|
||||
return uint8(activeAtLevels())&max(
|
||||
lp.SecurityLevel(), // layered profile security level
|
||||
status.ActiveSecurityLevel(), // global security level
|
||||
) > 0
|
||||
}
|
||||
}
|
||||
|
||||
func (lp *LayeredProfile) wrapIntOption(configKey string, globalConfig config.IntOption) config.IntOption {
|
||||
valid := no
|
||||
var value int64
|
||||
|
||||
return func() int64 {
|
||||
if !valid.IsSet() {
|
||||
valid = lp.getValidityFlag()
|
||||
|
||||
found := false
|
||||
layerLoop:
|
||||
for _, layer := range lp.layers {
|
||||
layerValue, ok := layer.configPerspective.GetAsInt(configKey)
|
||||
if ok {
|
||||
found = true
|
||||
value = layerValue
|
||||
break layerLoop
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
value = globalConfig()
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
For later:
|
||||
|
||||
func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config.StringOption) config.StringOption {
|
||||
valid := no
|
||||
var value string
|
||||
|
||||
return func() string {
|
||||
if !valid.IsSet() {
|
||||
valid = lp.getValidityFlag()
|
||||
|
||||
found := false
|
||||
layerLoop:
|
||||
for _, layer := range lp.layers {
|
||||
layerValue, ok := layer.configPerspective.GetAsString(configKey)
|
||||
if ok {
|
||||
found = true
|
||||
value = layerValue
|
||||
break layerLoop
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
value = globalConfig()
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func max(a, b uint8) uint8 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
|
@ -1,78 +1,178 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portmaster/status"
|
||||
"github.com/safing/portmaster/profile/endpoints"
|
||||
)
|
||||
|
||||
var (
|
||||
lastUsedUpdateThreshold = 1 * time.Hour
|
||||
)
|
||||
|
||||
// Profile Sources
|
||||
const (
|
||||
SourceLocal string = "local"
|
||||
SourceCommunity string = "community"
|
||||
SourceEnterprise string = "enterprise"
|
||||
SourceGlobal string = "global"
|
||||
)
|
||||
|
||||
// Default Action IDs
|
||||
const (
|
||||
DefaultActionNotSet uint8 = 0
|
||||
DefaultActionBlock uint8 = 1
|
||||
DefaultActionAsk uint8 = 2
|
||||
DefaultActionPermit uint8 = 3
|
||||
)
|
||||
|
||||
// Profile is used to predefine a security profile for applications.
|
||||
type Profile struct {
|
||||
type Profile struct { //nolint:maligned // not worth the effort
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
// Profile Metadata
|
||||
ID string
|
||||
// Identity
|
||||
ID string
|
||||
Source string
|
||||
|
||||
// App Information
|
||||
Name string
|
||||
Description string
|
||||
Homepage string
|
||||
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for a database path or "e:" for the encoded data.
|
||||
Icon string
|
||||
|
||||
// User Profile Only
|
||||
LinkedPath string
|
||||
StampProfileID string
|
||||
StampProfileAssigned int64
|
||||
// References - local profiles only
|
||||
// LinkedPath is a filesystem path to the executable this profile was created for.
|
||||
LinkedPath string
|
||||
// LinkedProfiles is a list of other profiles
|
||||
LinkedProfiles []string
|
||||
|
||||
// Fingerprints
|
||||
Fingerprints []*Fingerprint
|
||||
// TODO: Fingerprints []*Fingerprint
|
||||
|
||||
// Configuration
|
||||
// The mininum security level to apply to connections made with this profile
|
||||
SecurityLevel uint8
|
||||
Flags Flags
|
||||
Endpoints Endpoints
|
||||
ServiceEndpoints Endpoints
|
||||
SecurityLevel uint8
|
||||
Config map[string]interface{}
|
||||
|
||||
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process must be found
|
||||
// Framework *Framework `json:",omitempty bson:",omitempty"`
|
||||
// Interpreted Data
|
||||
configPerspective *config.Perspective
|
||||
dataParsed bool
|
||||
defaultAction uint8
|
||||
endpoints endpoints.Endpoints
|
||||
serviceEndpoints endpoints.Endpoints
|
||||
|
||||
// When this Profile was approximately last used (for performance reasons not every single usage is saved)
|
||||
Created int64
|
||||
// Lifecycle Management
|
||||
oudated *abool.AtomicBool
|
||||
|
||||
// Framework
|
||||
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process/actor must be found
|
||||
// TODO: Framework *Framework
|
||||
|
||||
// When this Profile was approximately last used.
|
||||
// For performance reasons not every single usage is saved.
|
||||
ApproxLastUsed int64
|
||||
Created int64
|
||||
|
||||
internalSave bool
|
||||
}
|
||||
|
||||
func (profile *Profile) prepConfig() (err error) {
|
||||
profile.Lock()
|
||||
defer profile.Unlock()
|
||||
|
||||
// prepare configuration
|
||||
profile.configPerspective, err = config.NewPerspective(profile.Config)
|
||||
return
|
||||
}
|
||||
|
||||
func (profile *Profile) parseConfig() error {
|
||||
profile.Lock()
|
||||
defer profile.Unlock()
|
||||
|
||||
if profile.configPerspective == nil {
|
||||
return errors.New("config not prepared")
|
||||
}
|
||||
|
||||
// check if already parsed
|
||||
if profile.dataParsed {
|
||||
return nil
|
||||
}
|
||||
profile.dataParsed = true
|
||||
|
||||
var err error
|
||||
var lastErr error
|
||||
|
||||
action, ok := profile.configPerspective.GetAsString(cfgOptionDefaultActionKey)
|
||||
if ok {
|
||||
switch action {
|
||||
case "permit":
|
||||
profile.defaultAction = DefaultActionPermit
|
||||
case "ask":
|
||||
profile.defaultAction = DefaultActionAsk
|
||||
case "block":
|
||||
profile.defaultAction = DefaultActionBlock
|
||||
default:
|
||||
lastErr = fmt.Errorf(`default action "%s" invalid`, action)
|
||||
}
|
||||
}
|
||||
|
||||
list, ok := profile.configPerspective.GetAsStringArray(cfgOptionEndpointsKey)
|
||||
if ok {
|
||||
profile.endpoints, err = endpoints.ParseEndpoints(list)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
list, ok = profile.configPerspective.GetAsStringArray(cfgOptionServiceEndpointsKey)
|
||||
if ok {
|
||||
profile.serviceEndpoints, err = endpoints.ParseEndpoints(list)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// New returns a new Profile.
|
||||
func New() *Profile {
|
||||
return &Profile{
|
||||
ID: uuid.NewV4().String(),
|
||||
Source: SourceLocal,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeProfileKey creates the correct key for a profile with the given namespace and ID.
|
||||
func MakeProfileKey(namespace, id string) string {
|
||||
return fmt.Sprintf("core:profiles/%s/%s", namespace, id)
|
||||
// ScopedID returns the scoped ID (Source + ID) of the profile.
|
||||
func (profile *Profile) ScopedID() string {
|
||||
return makeScopedID(profile.Source, profile.ID)
|
||||
}
|
||||
|
||||
// Save saves the profile to the database
|
||||
func (profile *Profile) Save(namespace string) error {
|
||||
func (profile *Profile) Save() error {
|
||||
if profile.ID == "" {
|
||||
profile.ID = uuid.NewV4().String()
|
||||
return errors.New("profile: tried to save profile without ID")
|
||||
}
|
||||
if profile.Source == "" {
|
||||
return fmt.Errorf("profile: profile %s does not specify a source", profile.ID)
|
||||
}
|
||||
|
||||
if !profile.KeyIsSet() {
|
||||
if namespace == "" {
|
||||
return fmt.Errorf("no key or namespace defined for profile %s", profile.String())
|
||||
}
|
||||
profile.SetKey(MakeProfileKey(namespace, profile.ID))
|
||||
profile.SetKey(makeProfileKey(profile.Source, profile.ID))
|
||||
}
|
||||
|
||||
return profileDB.Put(profile)
|
||||
|
@ -92,27 +192,47 @@ func (profile *Profile) String() string {
|
|||
return profile.Name
|
||||
}
|
||||
|
||||
// DetailedString returns a more detailed string representation of theProfile.
|
||||
func (profile *Profile) DetailedString() string {
|
||||
return fmt.Sprintf("%s(SL=%s Flags=%s Endpoints=%s)", profile.Name, status.FmtSecurityLevel(profile.SecurityLevel), profile.Flags.String(), profile.Endpoints.String())
|
||||
// GetProfile loads a profile from the database.
|
||||
func GetProfile(source, id string) (*Profile, error) {
|
||||
return GetProfileByScopedID(makeScopedID(source, id))
|
||||
}
|
||||
|
||||
// GetUserProfile loads a profile from the database.
|
||||
func GetUserProfile(id string) (*Profile, error) {
|
||||
return getProfile(UserNamespace, id)
|
||||
}
|
||||
// GetProfileByScopedID loads a profile from the database using a scoped ID like "local/id" or "community/id".
|
||||
func GetProfileByScopedID(scopedID string) (*Profile, error) {
|
||||
// check cache
|
||||
profile := getActiveProfile(scopedID)
|
||||
if profile != nil {
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// GetStampProfile loads a profile from the database.
|
||||
func GetStampProfile(id string) (*Profile, error) {
|
||||
return getProfile(StampNamespace, id)
|
||||
}
|
||||
|
||||
func getProfile(namespace, id string) (*Profile, error) {
|
||||
r, err := profileDB.Get(MakeProfileKey(namespace, id))
|
||||
// get from database
|
||||
r, err := profileDB.Get(profilesDBPath + scopedID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return EnsureProfile(r)
|
||||
|
||||
// convert
|
||||
profile, err = EnsureProfile(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// prepare config
|
||||
err = profile.prepConfig()
|
||||
if err != nil {
|
||||
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
||||
}
|
||||
|
||||
// parse config
|
||||
err = profile.parseConfig()
|
||||
if err != nil {
|
||||
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
|
||||
}
|
||||
|
||||
// mark active
|
||||
markProfileActive(profile)
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// EnsureProfile ensures that the given record is a *Profile, and returns it.
|
||||
|
|
188
profile/set.go
188
profile/set.go
|
@ -1,188 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portmaster/status"
|
||||
)
|
||||
|
||||
// Set handles Profile chaining.
|
||||
type Set struct {
|
||||
sync.Mutex
|
||||
|
||||
id string
|
||||
profiles [4]*Profile
|
||||
// Application
|
||||
// Global
|
||||
// Stamp
|
||||
// Default
|
||||
|
||||
combinedSecurityLevel uint8
|
||||
independent bool
|
||||
}
|
||||
|
||||
// NewSet returns a new profile set with given the profiles.
|
||||
func NewSet(ctx context.Context, id string, user, stamp *Profile) *Set {
|
||||
new := &Set{
|
||||
id: id,
|
||||
profiles: [4]*Profile{
|
||||
user, // Application
|
||||
nil, // Global
|
||||
stamp, // Stamp
|
||||
nil, // Default
|
||||
},
|
||||
}
|
||||
activateProfileSet(ctx, new)
|
||||
new.Update(status.SecurityLevelFortress)
|
||||
return new
|
||||
}
|
||||
|
||||
// UserProfile returns the user profile.
|
||||
func (set *Set) UserProfile() *Profile {
|
||||
return set.profiles[0]
|
||||
}
|
||||
|
||||
// Update gets the new global and default profile and updates the independence status. It must be called when reusing a profile set for a series of calls.
|
||||
func (set *Set) Update(securityLevel uint8) {
|
||||
set.Lock()
|
||||
|
||||
specialProfileLock.RLock()
|
||||
defer specialProfileLock.RUnlock()
|
||||
|
||||
// update profiles
|
||||
set.profiles[1] = globalProfile
|
||||
set.profiles[3] = fallbackProfile
|
||||
|
||||
// update security level
|
||||
profileSecurityLevel := set.getSecurityLevel()
|
||||
if profileSecurityLevel > securityLevel {
|
||||
set.combinedSecurityLevel = profileSecurityLevel
|
||||
} else {
|
||||
set.combinedSecurityLevel = securityLevel
|
||||
}
|
||||
|
||||
set.Unlock()
|
||||
// update independence
|
||||
if set.CheckFlag(Independent) {
|
||||
set.Lock()
|
||||
set.independent = true
|
||||
set.Unlock()
|
||||
} else {
|
||||
set.Lock()
|
||||
set.independent = false
|
||||
set.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SecurityLevel returns the applicable security level for the profile set.
|
||||
func (set *Set) SecurityLevel() uint8 {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
|
||||
return set.combinedSecurityLevel
|
||||
}
|
||||
|
||||
// GetProfileMode returns the active profile mode.
|
||||
func (set *Set) GetProfileMode() uint8 {
|
||||
switch {
|
||||
case set.CheckFlag(Whitelist):
|
||||
return Whitelist
|
||||
case set.CheckFlag(Prompt):
|
||||
return Prompt
|
||||
case set.CheckFlag(Blacklist):
|
||||
return Blacklist
|
||||
default:
|
||||
return Whitelist
|
||||
}
|
||||
}
|
||||
|
||||
// CheckFlag returns whether a given flag is set.
|
||||
func (set *Set) CheckFlag(flag uint8) (active bool) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
|
||||
for i, profile := range set.profiles {
|
||||
if i == 2 && set.independent {
|
||||
continue
|
||||
}
|
||||
|
||||
if profile != nil {
|
||||
active, ok := profile.Flags.Check(flag, set.combinedSecurityLevel)
|
||||
if ok {
|
||||
return active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckEndpointDomain checks if the given endpoint matches an entry in the corresponding list. This is for outbound communication only.
|
||||
func (set *Set) CheckEndpointDomain(domain string) (result EPResult, reason string) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
|
||||
for i, profile := range set.profiles {
|
||||
if i == 2 && set.independent {
|
||||
continue
|
||||
}
|
||||
|
||||
if profile != nil {
|
||||
if result, reason = profile.Endpoints.CheckDomain(domain); result != NoMatch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
// CheckEndpointIP checks if the given endpoint matches an entry in the corresponding list.
|
||||
func (set *Set) CheckEndpointIP(domain string, ip net.IP, protocol uint8, port uint16, inbound bool) (result EPResult, reason string) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
|
||||
for i, profile := range set.profiles {
|
||||
if i == 2 && set.independent {
|
||||
continue
|
||||
}
|
||||
|
||||
if profile != nil {
|
||||
if inbound {
|
||||
if result, reason = profile.ServiceEndpoints.CheckIP(domain, ip, protocol, port, inbound, set.combinedSecurityLevel); result != NoMatch {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if result, reason = profile.Endpoints.CheckIP(domain, ip, protocol, port, inbound, set.combinedSecurityLevel); result != NoMatch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoMatch, ""
|
||||
}
|
||||
|
||||
// getSecurityLevel returns the highest prioritized security level.
|
||||
func (set *Set) getSecurityLevel() uint8 {
|
||||
if set == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
for i, profile := range set.profiles {
|
||||
if i == 2 {
|
||||
// Stamp profiles do not have the SecurityLevel setting
|
||||
continue
|
||||
}
|
||||
|
||||
if profile != nil {
|
||||
if profile.SecurityLevel > 0 {
|
||||
return profile.SecurityLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
//nolint:unparam
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/status"
|
||||
)
|
||||
|
||||
var (
|
||||
testUserProfile *Profile
|
||||
testStampProfile *Profile
|
||||
)
|
||||
|
||||
func init() {
|
||||
specialProfileLock.Lock()
|
||||
defer specialProfileLock.Unlock()
|
||||
|
||||
globalProfile = makeDefaultGlobalProfile()
|
||||
fallbackProfile = makeDefaultFallbackProfile()
|
||||
|
||||
testUserProfile = &Profile{
|
||||
ID: "unit-test-user",
|
||||
Name: "Unit Test User Profile",
|
||||
SecurityLevel: status.SecurityLevelDynamic,
|
||||
Flags: map[uint8]uint8{
|
||||
Independent: status.SecurityLevelFortress,
|
||||
},
|
||||
Endpoints: []*EndpointPermission{
|
||||
{
|
||||
Type: EptDomain,
|
||||
Value: "good.bad.example.com.",
|
||||
Permit: true,
|
||||
Created: time.Now().Unix(),
|
||||
},
|
||||
{
|
||||
Type: EptDomain,
|
||||
Value: "*bad.example.com.",
|
||||
Permit: false,
|
||||
Created: time.Now().Unix(),
|
||||
},
|
||||
{
|
||||
Type: EptDomain,
|
||||
Value: "example.com.",
|
||||
Permit: true,
|
||||
Created: time.Now().Unix(),
|
||||
},
|
||||
{
|
||||
Type: EptAny,
|
||||
Permit: true,
|
||||
Protocol: 6,
|
||||
StartPort: 22000,
|
||||
EndPort: 22000,
|
||||
Created: time.Now().Unix(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testStampProfile = &Profile{
|
||||
ID: "unit-test-stamp",
|
||||
Name: "Unit Test Stamp Profile",
|
||||
SecurityLevel: status.SecurityLevelFortress,
|
||||
// Flags: map[uint8]uint8{
|
||||
// Internet: status.SecurityLevelsAll,
|
||||
// },
|
||||
Endpoints: []*EndpointPermission{
|
||||
{
|
||||
Type: EptDomain,
|
||||
Value: "*bad2.example.com.",
|
||||
Permit: false,
|
||||
Created: time.Now().Unix(),
|
||||
},
|
||||
{
|
||||
Type: EptAny,
|
||||
Permit: true,
|
||||
Protocol: 6,
|
||||
StartPort: 80,
|
||||
EndPort: 80,
|
||||
Created: time.Now().Unix(),
|
||||
},
|
||||
},
|
||||
ServiceEndpoints: []*EndpointPermission{
|
||||
{
|
||||
Type: EptAny,
|
||||
Permit: true,
|
||||
Protocol: 17,
|
||||
StartPort: 12345,
|
||||
EndPort: 12347,
|
||||
Created: time.Now().Unix(),
|
||||
},
|
||||
{ // default deny
|
||||
Type: EptAny,
|
||||
Permit: false,
|
||||
Created: time.Now().Unix(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testFlag(t *testing.T, set *Set, flag uint8, shouldBeActive bool) {
|
||||
active := set.CheckFlag(flag)
|
||||
if active != shouldBeActive {
|
||||
t.Errorf("unexpected result: flag %s: active=%v, expected=%v", flagNames[flag], active, shouldBeActive)
|
||||
}
|
||||
}
|
||||
|
||||
func testEndpointDomain(t *testing.T, set *Set, domain string, expectedResult EPResult) {
|
||||
var result EPResult
|
||||
result, _ = set.CheckEndpointDomain(domain)
|
||||
if result != expectedResult {
|
||||
t.Errorf(
|
||||
"line %d: unexpected result for endpoint domain %s: result=%s, expected=%s",
|
||||
getLineNumberOfCaller(1),
|
||||
domain,
|
||||
result,
|
||||
expectedResult,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testEndpointIP(t *testing.T, set *Set, domain string, ip net.IP, protocol uint8, port uint16, inbound bool, expectedResult EPResult) {
|
||||
var result EPResult
|
||||
result, _ = set.CheckEndpointIP(domain, ip, protocol, port, inbound)
|
||||
if result != expectedResult {
|
||||
t.Errorf(
|
||||
"line %d: unexpected result for endpoint %s/%s/%d/%d/%v: result=%s, expected=%s",
|
||||
getLineNumberOfCaller(1),
|
||||
domain,
|
||||
ip,
|
||||
protocol,
|
||||
port,
|
||||
inbound,
|
||||
result,
|
||||
expectedResult,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileSet(t *testing.T) {
|
||||
|
||||
set := NewSet(context.Background(), "[pid]-/path/to/bin", testUserProfile, testStampProfile)
|
||||
|
||||
set.Update(status.SecurityLevelDynamic)
|
||||
testFlag(t, set, Whitelist, false)
|
||||
// testFlag(t, set, Internet, true)
|
||||
testEndpointDomain(t, set, "example.com.", Permitted)
|
||||
testEndpointDomain(t, set, "bad.example.com.", Denied)
|
||||
testEndpointDomain(t, set, "other.bad.example.com.", Denied)
|
||||
testEndpointDomain(t, set, "good.bad.example.com.", Permitted)
|
||||
testEndpointDomain(t, set, "bad2.example.com.", Undeterminable)
|
||||
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 22000, false, Permitted)
|
||||
testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 6, 22000, false, Permitted)
|
||||
testEndpointDomain(t, set, "test.local.", Undeterminable)
|
||||
testEndpointDomain(t, set, "other.example.com.", Undeterminable)
|
||||
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 53, false, NoMatch)
|
||||
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 443, false, NoMatch)
|
||||
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 12346, false, NoMatch)
|
||||
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 12345, true, Permitted)
|
||||
testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 17, 12347, true, Permitted)
|
||||
|
||||
set.Update(status.SecurityLevelSecure)
|
||||
// testFlag(t, set, Internet, true)
|
||||
|
||||
set.Update(status.SecurityLevelFortress) // Independent!
|
||||
testFlag(t, set, Whitelist, true)
|
||||
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 12345, true, Denied)
|
||||
testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 17, 12347, true, Denied)
|
||||
testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 80, false, NoMatch)
|
||||
testEndpointDomain(t, set, "bad2.example.com.", Undeterminable)
|
||||
}
|
||||
|
||||
func getLineNumberOfCaller(levels int) int {
|
||||
_, _, line, _ := runtime.Caller(levels + 1) //nolint:dogsled
|
||||
return line
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
)
|
||||
|
||||
var (
|
||||
globalProfile *Profile
|
||||
fallbackProfile *Profile
|
||||
|
||||
specialProfileLock sync.RWMutex
|
||||
)
|
||||
|
||||
func initSpecialProfiles() (err error) {
|
||||
|
||||
specialProfileLock.Lock()
|
||||
defer specialProfileLock.Unlock()
|
||||
|
||||
globalProfile, err = getSpecialProfile("global")
|
||||
if err != nil {
|
||||
if err != database.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
globalProfile = makeDefaultGlobalProfile()
|
||||
_ = globalProfile.Save(SpecialNamespace)
|
||||
}
|
||||
|
||||
fallbackProfile, err = getSpecialProfile("fallback")
|
||||
if err != nil {
|
||||
if err != database.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
fallbackProfile = makeDefaultFallbackProfile()
|
||||
ensureServiceEndpointsDenyAll(fallbackProfile)
|
||||
_ = fallbackProfile.Save(SpecialNamespace)
|
||||
}
|
||||
ensureServiceEndpointsDenyAll(fallbackProfile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSpecialProfile(id string) (*Profile, error) {
|
||||
return getProfile(SpecialNamespace, id)
|
||||
}
|
||||
|
||||
func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) {
|
||||
for _, ep := range p.ServiceEndpoints {
|
||||
if ep != nil {
|
||||
if ep.Type == EptAny &&
|
||||
ep.Protocol == 0 &&
|
||||
ep.StartPort == 0 &&
|
||||
ep.EndPort == 0 &&
|
||||
!ep.Permit {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.ServiceEndpoints = append(p.ServiceEndpoints, &EndpointPermission{
|
||||
Type: EptAny,
|
||||
Protocol: 0,
|
||||
StartPort: 0,
|
||||
EndPort: 0,
|
||||
Permit: false,
|
||||
})
|
||||
return true
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
func initUpdateListener() error {
|
||||
sub, err := profileDB.Subscribe(query.New("core:profiles/"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go updateListener(sub)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateListener(sub *database.Subscription) {
|
||||
for {
|
||||
select {
|
||||
case <-shutdownSignal:
|
||||
return
|
||||
case r := <-sub.Feed:
|
||||
|
||||
if r.Meta().IsDeleted() {
|
||||
continue
|
||||
}
|
||||
|
||||
profile, err := EnsureProfile(r)
|
||||
if err != nil {
|
||||
log.Errorf("profile: received update for profile, but could not read: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("profile: updated %s", profile.ID)
|
||||
|
||||
switch profile.DatabaseKey() {
|
||||
case "profiles/special/global":
|
||||
|
||||
specialProfileLock.Lock()
|
||||
globalProfile = profile
|
||||
specialProfileLock.Unlock()
|
||||
|
||||
case "profiles/special/fallback":
|
||||
|
||||
profile.Lock()
|
||||
profileChanged := ensureServiceEndpointsDenyAll(profile)
|
||||
profile.Unlock()
|
||||
|
||||
if profileChanged {
|
||||
_ = profile.Save(SpecialNamespace)
|
||||
continue
|
||||
}
|
||||
|
||||
specialProfileLock.Lock()
|
||||
fallbackProfile = profile
|
||||
specialProfileLock.Unlock()
|
||||
|
||||
default:
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(profile.Key(), MakeProfileKey(UserNamespace, "")):
|
||||
updateActiveProfile(profile, true /* User Profile */)
|
||||
case strings.HasPrefix(profile.Key(), MakeProfileKey(StampNamespace, "")):
|
||||
updateActiveProfile(profile, false /* Stamp Profile */)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
updateVersion uint32
|
||||
)
|
||||
|
||||
// GetUpdateVersion returns the current profiles internal update version
|
||||
func GetUpdateVersion() uint32 {
|
||||
return atomic.LoadUint32(&updateVersion)
|
||||
}
|
||||
|
||||
func increaseUpdateVersion() {
|
||||
// we intentially want to wrap
|
||||
atomic.AddUint32(&updateVersion, 1)
|
||||
}
|
Loading…
Add table
Reference in a new issue