Clean up profiles and move to consolidated configuration system with profile layering

This commit is contained in:
Daniel 2020-03-20 23:05:56 +01:00
parent 543a70422a
commit 36fad7aeec
16 changed files with 937 additions and 1391 deletions

View file

@ -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
View 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
View 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
}

View file

@ -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
}

View file

@ -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,
},
},
}
}

View file

@ -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"
}
}

View file

@ -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())
}
}

View file

@ -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)
}

View file

@ -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)
// }
}

View file

@ -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
View 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
}

View file

@ -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.

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}