Move from LinkedPath to Fingerprints and PresentationPath

This commit is contained in:
Daniel 2022-10-03 22:10:43 +02:00
parent e1e6a40498
commit 59f776ce2f
9 changed files with 699 additions and 253 deletions

View file

@ -38,21 +38,6 @@ func getAllActiveProfiles() []*Profile {
return result
}
// findActiveProfile searched for an active local profile using the linked path.
func findActiveProfile(linkedPath string) *Profile {
activeProfilesLock.RLock()
defer activeProfilesLock.RUnlock()
for _, activeProfile := range activeProfiles {
if activeProfile.LinkedPath == linkedPath {
activeProfile.MarkStillActive()
return activeProfile
}
}
return nil
}
// addActiveProfile registers a active profile.
func addActiveProfile(profile *Profile) {
activeProfilesLock.Lock()

View file

@ -6,7 +6,6 @@ import (
"sync"
"time"
"github.com/safing/portbase/config"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/intel/filterlists"
"github.com/safing/portmaster/profile/endpoints"
@ -91,11 +90,7 @@ func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error {
lastErr = err
}
// build global profile for reference
profile := New(SourceSpecial, "global-config", "", nil)
profile.Name = "Global Configuration"
profile.Internal = true
// Build config.
newConfig := make(map[string]interface{})
// fill profile config options
for key, value := range cfgStringOptions {
@ -111,8 +106,14 @@ func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error {
newConfig[key] = value()
}
// expand and assign
profile.Config = config.Expand(newConfig)
// Build global profile for reference.
profile := New(&Profile{
ID: "global-config",
Source: SourceSpecial,
Name: "Global Configuration",
Config: newConfig,
Internal: true,
})
// save profile
err = profile.Save()

View file

@ -65,41 +65,19 @@ func startProfileUpdateChecker() error {
continue profileFeed
}
// If the record is being deleted, reset the profile.
// create an empty profile instead.
if r.Meta().IsDeleted() {
newProfile, err := GetProfile(
activeProfile.Source,
activeProfile.ID,
activeProfile.LinkedPath,
true,
)
if err != nil {
log.Errorf("profile: failed to create new profile after reset: %s", err)
} else {
// Copy metadata from the old profile.
newProfile.copyMetadataFrom(activeProfile)
// Save the new profile.
err = newProfile.Save()
if err != nil {
log.Errorf("profile: failed to save new profile after reset: %s", err)
}
}
// If the new profile was successfully created, update layered profile.
activeProfile.outdated.Set()
if err == nil {
newProfile.layeredProfile.Update()
}
module.TriggerEvent(profileConfigChange, nil)
}
// Always increase the revision counter of the layer profile.
// This marks previous connections in the UI as decided with outdated settings.
if activeProfile.layeredProfile != nil {
activeProfile.layeredProfile.increaseRevisionCounter(true)
}
// Always mark as outdated if the record is being deleted.
if r.Meta().IsDeleted() {
activeProfile.outdated.Set()
module.TriggerEvent(profileConfigChange, nil)
continue
}
// If the profile is saved externally (eg. via the API), have the
// next one to use it reload the profile from the database.
receivedProfile, err := EnsureProfile(r)

323
profile/fingerprint.go Normal file
View file

@ -0,0 +1,323 @@
package profile
import (
"fmt"
"regexp"
"strings"
)
// # Matching and Scores
//
// There are three levels:
//
// 1. Type: What matched?
// 1. Tag: 40.000 points
// 2. Env: 30.000 points
// 3. MatchingPath: 20.000 points
// 4. Path: 10.000 points
// 2. Operation: How was it mached?
// 1. Equals: 3.000 points
// 2. Prefix: 2.000 points
// 3. Regex: 1.000 points
// 3. How "strong" was the match?
// 1. Equals: Length of path (irrelevant)
// 2. Prefix: Length of prefix
// 3. Regex: 0 (we are not suicidal)
// ms-store:Microsoft.One.Note
// Path Match /path/to/file
// Tag MS-Store Match value
// Env Regex Key Value
// Fingerprint Type IDs.
const (
FingerprintTypeTagID = "tag"
FingerprintTypeEnvID = "env"
FingerprintTypePathID = "path" // Matches both MatchingPath and Path.
FingerprintOperationEqualsID = "equals"
FingerprintOperationPrefixID = "prefix"
FingerprintOperationRegexID = "regex"
tagMatchBaseScore = 40_000
envMatchBaseScore = 30_000
matchingPathMatchBaseScore = 20_000
pathMatchBaseScore = 10_000
fingerprintEqualsBaseScore = 3_000
fingerprintPrefixBaseScore = 2_000
fingerprintRegexBaseScore = 1_000
maxMatchStrength = 499
)
type (
// Fingerprint defines a way of matching a process.
// The Key is only valid - but required - for some types.
Fingerprint struct {
Type string
Key string // Key must always fully match.
Operation string
Value string
}
// Tag represents a simple key/value kind of tag used in process metadata
// and fingerprints.
Tag struct {
Key string
Value string
}
// MatchingData is an interface to fetching data in the matching process.
MatchingData interface {
Tags() []Tag
Env() map[string]string
Path() string
MatchingPath() string
}
matchingFingerprint interface {
MatchesKey(key string) bool
Match(value string) (score int)
}
)
// MatchesKey returns whether the optional fingerprint key (for some types
// only) matches the given key.
func (fp Fingerprint) MatchesKey(key string) bool {
return key == fp.Key
}
// KeyInTags checks is the given key is in the tags.
func KeyInTags(tags []Tag, key string) bool {
for _, tag := range tags {
if key == tag.Key {
return true
}
}
return false
}
// KeyAndValueInTags checks is the given key/value pair is in the tags.
func KeyAndValueInTags(tags []Tag, key, value string) bool {
for _, tag := range tags {
if key == tag.Key && value == tag.Value {
return true
}
}
return false
}
type fingerprintEquals struct {
Fingerprint
}
func (fp fingerprintEquals) Match(value string) (score int) {
if value == fp.Value {
return fingerprintEqualsBaseScore + checkMatchStrength(len(fp.Value))
}
return 0
}
type fingerprintPrefix struct {
Fingerprint
}
func (fp fingerprintPrefix) Match(value string) (score int) {
if strings.HasPrefix(value, fp.Value) {
return fingerprintPrefixBaseScore + checkMatchStrength(len(fp.Value))
}
return 0
}
type fingerprintRegex struct {
Fingerprint
regex *regexp.Regexp
}
func (fp fingerprintRegex) Match(value string) (score int) {
if fp.regex.MatchString(value) {
// Do not return any deviation from the base score.
// Trying to assign different scores to regex probably won't turn out to
// be a good idea.
return fingerprintRegexBaseScore
}
return 0
}
type parsedFingerprints struct {
tagPrints []matchingFingerprint
envPrints []matchingFingerprint
pathPrints []matchingFingerprint
}
func parseFingerprints(raw []Fingerprint, deprecatedLinkedPath string) (parsed *parsedFingerprints, firstErr error) {
parsed = &parsedFingerprints{}
// Add deprecated linked path to fingerprints.
if deprecatedLinkedPath != "" {
parsed.pathPrints = append(parsed.pathPrints, &fingerprintEquals{
Fingerprint: Fingerprint{
Type: FingerprintTypePathID,
Operation: FingerprintOperationEqualsID,
Value: deprecatedLinkedPath,
},
})
}
// Parse all fingerprints.
// Do not fail when one fails, instead return the first encountered error.
for _, entry := range raw {
// Check type and required key.
switch entry.Type {
case FingerprintTypeTagID, FingerprintTypeEnvID:
if entry.Key == "" {
if firstErr == nil {
firstErr = fmt.Errorf("%s fingerprint is missing key", entry.Type)
}
continue
}
case FingerprintTypePathID:
// Don't need a key.
default:
// Unknown type.
if firstErr == nil {
firstErr = fmt.Errorf("unknown fingerprint type: %q", entry.Type)
}
continue
}
// Create and/or collect operation match functions.
switch entry.Operation {
case FingerprintOperationEqualsID:
parsed.addMatchingFingerprint(entry, fingerprintEquals{entry})
case FingerprintOperationPrefixID:
parsed.addMatchingFingerprint(entry, fingerprintPrefix{entry})
case FingerprintOperationRegexID:
regex, err := regexp.Compile(entry.Value)
if err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to compile regex fingerprint: %s", entry.Value)
}
} else {
parsed.addMatchingFingerprint(entry, fingerprintRegex{
Fingerprint: entry,
regex: regex,
})
}
default:
if firstErr == nil {
firstErr = fmt.Errorf("unknown fingerprint operation: %q", entry.Type)
}
}
}
return parsed, firstErr
}
func (parsed *parsedFingerprints) addMatchingFingerprint(fp Fingerprint, matchingPrint matchingFingerprint) {
switch fp.Type {
case FingerprintTypeTagID:
parsed.tagPrints = append(parsed.tagPrints, matchingPrint)
case FingerprintTypeEnvID:
parsed.envPrints = append(parsed.envPrints, matchingPrint)
case FingerprintTypePathID:
parsed.pathPrints = append(parsed.pathPrints, matchingPrint)
default:
// This should never happen, as the types are checked already.
panic(fmt.Sprintf("unknown fingerprint type: %q", fp.Type))
}
}
// MatchFingerprints returns the highest matching score of the given
// fingerprints and matching data.
func MatchFingerprints(prints *parsedFingerprints, md MatchingData) (highestScore int) {
// Check tags.
for _, tagPrint := range prints.tagPrints {
for _, tag := range md.Tags() {
// Check if tag key matches.
if !tagPrint.MatchesKey(tag.Key) {
continue
}
// Try matching the tag value.
score := tagPrint.Match(tag.Value)
if score > highestScore {
highestScore = score
}
}
}
// If something matched, add base score and return.
if highestScore > 0 {
return tagMatchBaseScore + highestScore
}
// Check env.
for _, envPrint := range prints.envPrints {
for key, value := range md.Env() {
// Check if env key matches.
if !envPrint.MatchesKey(key) {
continue
}
// Try matching the env value.
score := envPrint.Match(value)
if score > highestScore {
highestScore = score
}
}
}
// If something matched, add base score and return.
if highestScore > 0 {
return envMatchBaseScore + highestScore
}
// Check matching path.
matchingPath := md.MatchingPath()
if matchingPath != "" {
for _, pathPrint := range prints.pathPrints {
// Try matching the path value.
score := pathPrint.Match(matchingPath)
if score > highestScore {
highestScore = score
}
}
// If something matched, add base score and return.
if highestScore > 0 {
return matchingPathMatchBaseScore + highestScore
}
}
// Check path.
path := md.Path()
if path != "" {
for _, pathPrint := range prints.pathPrints {
// Try matching the path value.
score := pathPrint.Match(path)
if score > highestScore {
highestScore = score
}
}
// If something matched, add base score and return.
if highestScore > 0 {
return pathMatchBaseScore + highestScore
}
}
// Nothing matched.
return 0
}
func checkMatchStrength(value int) int {
if value > maxMatchStrength {
return maxMatchStrength
}
if value < -maxMatchStrength {
return -maxMatchStrength
}
return value
}

View file

@ -2,20 +2,24 @@ package profile
import (
"errors"
"fmt"
"path"
"strings"
"sync"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
"github.com/safing/portbase/notifications"
)
var getProfileLock sync.Mutex
// GetProfile fetches a profile. This function ensures that the loaded profile
// is shared among all callers. You must always supply both the scopedID and
// linkedPath parameters whenever available.
func GetProfile(source profileSource, id, linkedPath string, reset bool) ( //nolint:gocognit
// GetLocalProfile fetches a profile. This function ensures that the loaded profile
// is shared among all callers. Always provide all available data points.
// Passing an ID without MatchingData is valid, but could lead to inconsistent
// data - use with caution.
func GetLocalProfile(id string, md MatchingData, createProfileCallback func() *Profile) ( //nolint:gocognit
profile *Profile,
err error,
) {
@ -27,99 +31,90 @@ func GetProfile(source profileSource, id, linkedPath string, reset bool) ( //nol
var previousVersion *Profile
// Fetch profile depending on the available information.
switch {
case id != "":
scopedID := makeScopedID(source, id)
// Get profile via the scoped ID.
// Check if there already is an active and not outdated profile.
profile = getActiveProfile(scopedID)
// Get active profile based on the ID, if available.
if id != "" {
// Check if there already is an active profile.
profile = getActiveProfile(makeScopedID(SourceLocal, id))
if profile != nil {
profile.MarkStillActive()
if profile.outdated.IsSet() || reset {
previousVersion = profile
} else {
// Mark active and return if not outdated.
if profile.outdated.IsNotSet() {
profile.MarkStillActive()
return profile, nil
}
}
// Get from database.
if !reset {
profile, err = getProfile(scopedID)
// Check if the profile is special and needs a reset.
if err == nil && specialProfileNeedsReset(profile) {
profile = getSpecialProfile(id, linkedPath)
}
} else {
// Simulate missing profile to create new one.
err = database.ErrNotFound
// If outdated, get from database.
previousVersion = profile
profile = nil
}
case linkedPath != "":
// Search for profile via a linked path.
// Check if there already is an active and not outdated profile for
// the linked path.
profile = findActiveProfile(linkedPath)
if profile != nil {
if profile.outdated.IsSet() || reset {
previousVersion = profile
} else {
return profile, nil
}
}
// Get from database.
if !reset {
profile, err = findProfile(linkedPath)
// Check if the profile is special and needs a reset.
if err == nil && specialProfileNeedsReset(profile) {
profile = getSpecialProfile(id, linkedPath)
}
} else {
// Simulate missing profile to create new one.
err = database.ErrNotFound
}
default:
return nil, errors.New("cannot fetch profile without ID or path")
}
// Create new profile if none was found.
if errors.Is(err, database.ErrNotFound) {
err = nil
// In some cases, we might need to get a profile directly, without matching data.
// This could lead to inconsistent data - use with caution.
if md == nil {
if id == "" {
return nil, errors.New("cannot get local profiles without ID and matching data")
}
// Check if there is a special profile for this ID.
profile = getSpecialProfile(id, linkedPath)
profile, err = getProfile(makeScopedID(SourceLocal, id))
if err != nil {
return nil, fmt.Errorf("failed to load profile %s by ID: %w", makeScopedID(SourceLocal, id), err)
}
}
// If not, create a standard profile.
// If we don't have a profile yet, find profile based on matching data.
if profile == nil {
profile, err = findProfile(SourceLocal, md)
if err != nil {
return nil, fmt.Errorf("failed to search for profile: %w", err)
}
}
// If we still don't have a profile, create a new one.
if profile == nil {
// Try the profile creation callback, if we have one.
if createProfileCallback != nil {
profile = createProfileCallback()
}
// If that did not work, create a standard profile.
if profile == nil {
profile = New(SourceLocal, id, linkedPath, nil)
fpPath := md.MatchingPath()
if fpPath == "" {
fpPath = md.Path()
}
profile = New(&Profile{
ID: id,
Source: SourceLocal,
PresentationPath: md.Path(),
Fingerprints: []Fingerprint{
{
Type: FingerprintTypePathID,
Operation: FingerprintOperationEqualsID,
Value: fpPath,
},
},
})
}
}
// If there was a non-recoverable error, return here.
if err != nil {
return nil, err
}
// Prepare profile for first use.
// Process profiles are coming directly from the database or are new.
// As we don't use any caching, these will be new objects.
// Add a layeredProfile to local and network profiles.
if profile.Source == SourceLocal || profile.Source == SourceNetwork {
// If we are refetching, assign the layered profile from the previous version.
// The internal references will be updated when the layered profile checks for updates.
if previousVersion != nil {
profile.layeredProfile = previousVersion.layeredProfile
}
// Add a layeredProfile.
// Local profiles must have a layered profile, create a new one if it
// does not yet exist.
if profile.layeredProfile == nil {
profile.layeredProfile = NewLayeredProfile(profile)
}
// If we are refetching, assign the layered profile from the previous version.
// The internal references will be updated when the layered profile checks for updates.
if previousVersion != nil && previousVersion.layeredProfile != nil {
profile.layeredProfile = previousVersion.layeredProfile
}
// Profiles must have a layered profile, create a new one if it
// does not yet exist.
if profile.layeredProfile == nil {
profile.layeredProfile = NewLayeredProfile(profile)
}
// Add the profile to the currently active profiles.
@ -137,40 +132,89 @@ func getProfile(scopedID string) (profile *Profile, err error) {
}
// Parse and prepare the profile, return the result.
return prepProfile(r)
return loadProfile(r)
}
// findProfile searches for a profile with the given linked path. If it cannot
// find one, it will create a new profile for the given linked path.
func findProfile(linkedPath string) (profile *Profile, err error) {
// Search the database for a matching profile.
it, err := profileDB.Query(
query.New(makeProfileKey(SourceLocal, "")).Where(
query.Where("LinkedPath", query.SameAs, linkedPath),
),
func findProfile(source profileSource, md MatchingData) (profile *Profile, err error) {
// TODO: Loading every profile from database and parsing it for every new
// process might be quite expensive. Measure impact and possibly improve.
// Get iterator over all profiles.
it, err := profileDB.Query(query.New(profilesDBPath + makeScopedID(source, "")))
if err != nil {
return nil, fmt.Errorf("failed to query for profiles: %w", err)
}
// Find best matching profile.
var (
highestScore int
bestMatch record.Record
)
profileFeed:
for r := range it.Next {
// Parse fingerprints.
prints, err := loadProfileFingerprints(r)
if err != nil {
log.Debugf("profile: failed to load fingerprints of %s: %s", r.Key(), err)
continue
}
// Get matching score and compare.
score := MatchFingerprints(prints, md)
switch {
case score == 0:
// Continue to next.
case score > highestScore:
highestScore = score
bestMatch = r
case score == highestScore:
// Notify user of conflict and abort.
// Use first match - this should be consistent.
notifyConflictingProfiles(bestMatch, r, md)
it.Cancel()
break profileFeed
}
}
// Check if there was an error while iterating.
if it.Err() != nil {
return nil, fmt.Errorf("failed to iterate over profiles: %w", err)
}
// Return nothing if no profile matched.
if bestMatch == nil {
return nil, nil
}
// If we have a match, parse and return the profile.
profile, err = loadProfile(bestMatch)
if err != nil {
return nil, fmt.Errorf("failed to parse selected profile %s: %w", bestMatch.Key(), err)
}
// Check if this profile is already active and return the active version instead.
if activeProfile := getActiveProfile(profile.ScopedID()); activeProfile != nil {
return activeProfile, nil
}
// Return nothing if no profile matched.
return profile, nil
}
func loadProfileFingerprints(r record.Record) (parsed *parsedFingerprints, err error) {
// Ensure it's a profile.
profile, err := EnsureProfile(r)
if err != nil {
return nil, err
}
// Only wait for the first result, or until the query ends.
r := <-it.Next
// Then cancel the query, should it still be running.
it.Cancel()
// Prep and return an existing profile.
if r != nil {
profile, err = prepProfile(r)
return profile, err
}
// If there was no profile in the database, create a new one, and return it.
profile = New(SourceLocal, "", linkedPath, nil)
return profile, nil
// Parse and return fingerprints.
return parseFingerprints(profile.Fingerprints, profile.LinkedPath)
}
func prepProfile(r record.Record) (*Profile, error) {
func loadProfile(r record.Record) (*Profile, error) {
// ensure its a profile
profile, err := EnsureProfile(r)
if err != nil {
@ -192,3 +236,50 @@ func prepProfile(r record.Record) (*Profile, error) {
// return parsed profile
return profile, nil
}
func notifyConflictingProfiles(a, b record.Record, md MatchingData) {
// Get profile names.
var idA, nameA, idB, nameB string
profileA, err := EnsureProfile(a)
if err == nil {
idA = profileA.ScopedID()
nameA = profileA.Name
} else {
idA = strings.TrimPrefix(a.Key(), profilesDBPath)
nameA = path.Base(idA)
}
profileB, err := EnsureProfile(b)
if err == nil {
idB = profileB.ScopedID()
nameB = profileB.Name
} else {
idB = strings.TrimPrefix(b.Key(), profilesDBPath)
nameB = path.Base(idB)
}
// Notify user about conflict.
notifications.NotifyWarn(
fmt.Sprintf("profiles:match-conflict:%s:%s", idA, idB),
"App Settings Match Conflict",
fmt.Sprintf(
"Multiple app settings match the app at %q with the same priority, please change on of them: %q or %q",
md.Path(),
nameA,
nameB,
),
notifications.Action{
Text: "Change (1)",
Type: notifications.ActionTypeOpenProfile,
Payload: idA,
},
notifications.Action{
Text: "Change (2)",
Type: notifications.ActionTypeOpenProfile,
Payload: idB,
},
notifications.Action{
ID: "ack",
Text: "OK",
},
)
}

View file

@ -74,9 +74,11 @@ func getProfileRevision(p *Profile) (*LayeredProfile, error) {
}
// Update profiles if necessary.
if layeredProfile.NeedsUpdate() {
layeredProfile.Update()
}
// TODO: Cannot update as we have too little information.
// Just return the current state. Previous code:
// if layeredProfile.NeedsUpdate() {
// layeredProfile.Update()
// }
return layeredProfile, nil
}

View file

@ -57,8 +57,8 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
lp := &LayeredProfile{
localProfile: localProfile,
layers: make([]*Profile, 0, len(localProfile.LinkedProfiles)+1),
LayerIDs: make([]string, 0, len(localProfile.LinkedProfiles)+1),
layers: make([]*Profile, 0, 1),
LayerIDs: make([]string, 0, 1),
globalValidityFlag: config.NewValidityFlag(),
RevisionCounter: 1,
securityLevel: &securityLevelVal,
@ -246,17 +246,23 @@ func (lp *LayeredProfile) NeedsUpdate() (outdated bool) {
}
// Update checks for and replaces any outdated profiles.
func (lp *LayeredProfile) Update() (revisionCounter uint64) {
func (lp *LayeredProfile) Update(md MatchingData, createProfileCallback func() *Profile) (revisionCounter uint64) {
lp.Lock()
defer lp.Unlock()
var changed bool
for i, layer := range lp.layers {
if layer.outdated.IsSet() {
changed = true
// Check for unsupported sources.
if layer.Source != SourceLocal {
log.Warningf("profile: updating profiles outside of local source is not supported: %s", layer.ScopedID())
layer.outdated.UnSet()
continue
}
// Update layer.
newLayer, err := GetProfile(layer.Source, layer.ID, layer.LinkedPath, false)
changed = true
newLayer, err := GetLocalProfile(layer.ID, md, createProfileCallback)
if err != nil {
log.Errorf("profiles: failed to update profile %s: %s", layer.ScopedID(), err)
} else {

View file

@ -26,11 +26,8 @@ type profileSource string
// Profile Sources.
const (
SourceLocal profileSource = "local" // local, editable
SourceSpecial profileSource = "special" // specials (read-only)
SourceNetwork profileSource = "network"
SourceCommunity profileSource = "community"
SourceEnterprise profileSource = "enterprise"
SourceLocal profileSource = "local" // local, editable
SourceSpecial profileSource = "special" // specials (read-only)
)
// Default Action IDs.
@ -83,11 +80,17 @@ type Profile struct { //nolint:maligned // not worth the effort
Icon string
// IconType describes the type of the Icon property.
IconType iconType
// LinkedPath is a filesystem path to the executable this
// Deprecated: LinkedPath used to point to the executableis this
// profile was created for.
// Until removed, it will be added to the Fingerprints as an exact path match.
LinkedPath string // constant
// LinkedProfiles is a list of other profiles
LinkedProfiles []string
// PresentationPath holds the path of an executable that should be used for
// get representative information from, like the name of the program or the icon.
// Is automatically removed when the path does not exist.
// Is automatically populated with the next match when empty.
PresentationPath string
// Fingerprints holds process matching information.
Fingerprints []Fingerprint
// SecurityLevel is the mininum security level to apply to
// connections made with this profile.
// Note(ppacher): we may deprecate this one as it can easily
@ -143,6 +146,11 @@ func (profile *Profile) prepProfile() {
// prepare configuration
profile.outdated = abool.New()
profile.lastActive = new(int64)
// Migration of LinkedPath to PresentationPath
if profile.PresentationPath == "" && profile.LinkedPath != "" {
profile.PresentationPath = profile.LinkedPath
}
}
func (profile *Profile) parseConfig() error {
@ -227,29 +235,25 @@ func (profile *Profile) parseConfig() error {
// New returns a new Profile.
// Optionally, you may supply custom configuration in the flat (key=value) form.
func New(
source profileSource,
id string,
linkedPath string,
customConfig map[string]interface{},
) *Profile {
if customConfig != nil {
customConfig = config.Expand(customConfig)
} else {
customConfig = make(map[string]interface{})
func New(profile *Profile) *Profile {
// Create profile if none is given.
if profile == nil {
profile = &Profile{}
}
profile := &Profile{
ID: id,
Source: source,
LinkedPath: linkedPath,
Created: time.Now().Unix(),
Config: customConfig,
savedInternally: true,
// Set default and internal values.
profile.Created = time.Now().Unix()
profile.savedInternally = true
// Expand any given configuration.
if profile.Config != nil {
profile.Config = config.Expand(profile.Config)
} else {
profile.Config = make(map[string]interface{})
}
// Generate random ID if none is given.
if id == "" {
if profile.ID == "" {
profile.ID = utils.RandomUUID("").String()
}
@ -437,13 +441,13 @@ func (profile *Profile) UpdateMetadata(binaryPath string) (changed bool) {
var needsUpdateFromSystem bool
// Check profile name.
filename := filepath.Base(profile.LinkedPath)
filename := filepath.Base(profile.PresentationPath)
// Update profile name if it is empty or equals the filename, which is the
// case for older profiles.
if strings.TrimSpace(profile.Name) == "" || profile.Name == filename {
// Generate a default profile name if does not exist.
profile.Name = osdetail.GenerateBinaryNameFromPath(profile.LinkedPath)
profile.Name = osdetail.GenerateBinaryNameFromPath(profile.PresentationPath)
if profile.Name == filename {
// TODO: Theoretically, the generated name could be identical to the
// filename.
@ -462,37 +466,12 @@ func (profile *Profile) UpdateMetadata(binaryPath string) (changed bool) {
return changed
}
func (profile *Profile) copyMetadataFrom(otherProfile *Profile) (changed bool) {
if profile.Name != otherProfile.Name {
profile.Name = otherProfile.Name
changed = true
}
if profile.Description != otherProfile.Description {
profile.Description = otherProfile.Description
changed = true
}
if profile.Homepage != otherProfile.Homepage {
profile.Homepage = otherProfile.Homepage
changed = true
}
if profile.Icon != otherProfile.Icon {
profile.Icon = otherProfile.Icon
changed = true
}
if profile.IconType != otherProfile.IconType {
profile.IconType = otherProfile.IconType
changed = true
}
return
}
// updateMetadataFromSystem updates the profile metadata with data from the
// operating system and saves it afterwards.
func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
// This function is only valid for local profiles.
if profile.Source != SourceLocal || profile.LinkedPath == "" {
return fmt.Errorf("tried to update metadata for non-local / non-linked profile %s", profile.ScopedID())
if profile.Source != SourceLocal || profile.PresentationPath == "" {
return fmt.Errorf("tried to update metadata for non-local or non-path profile %s", profile.ScopedID())
}
// Save the profile when finished, if needed.
@ -507,14 +486,14 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
}()
// Get binary name from linked path.
newName, err := osdetail.GetBinaryNameFromSystem(profile.LinkedPath)
newName, err := osdetail.GetBinaryNameFromSystem(profile.PresentationPath)
if err != nil {
switch {
case errors.Is(err, osdetail.ErrNotSupported):
case errors.Is(err, osdetail.ErrNotFound):
case errors.Is(err, osdetail.ErrEmptyOutput):
default:
log.Warningf("profile: error while getting binary name for %s: %s", profile.LinkedPath, err)
log.Warningf("profile: error while getting binary name for %s: %s", profile.PresentationPath, err)
}
return nil
}
@ -525,7 +504,7 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
}
// Get filename of linked path for comparison.
filename := filepath.Base(profile.LinkedPath)
filename := filepath.Base(profile.PresentationPath)
// TODO: Theoretically, the generated name from the system could be identical
// to the filename. This would mean that the worker is triggered every time

View file

@ -1,9 +1,12 @@
package profile
import (
"errors"
"time"
"github.com/safing/portbase/database"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/status"
)
const (
@ -74,6 +77,73 @@ If you think you might have messed up the settings of the System DNS Client, jus
PortmasterNotifierProfileDescription = `This is the Portmaster UI Tray Notifier.`
)
// GetSpecialProfile fetches a special profile. This function ensures that the loaded profile
// is shared among all callers. Always provide all available data points.
func GetSpecialProfile(id string, path string) ( //nolint:gocognit
profile *Profile,
err error,
) {
// Check if we have an ID.
if id == "" {
return nil, errors.New("cannot get special profile without ID")
}
scopedID := makeScopedID(SourceLocal, id)
// Globally lock getting a profile.
// This does not happen too often, and it ensures we really have integrity
// and no race conditions.
getProfileLock.Lock()
defer getProfileLock.Unlock()
// Check if there already is an active profile.
var previousVersion *Profile
profile = getActiveProfile(scopedID)
if profile != nil {
// Mark active and return if not outdated.
if profile.outdated.IsNotSet() {
profile.MarkStillActive()
return profile, nil
}
// If outdated, get from database.
previousVersion = profile
}
// Get special profile from DB and check if it needs a reset.
profile, err = getProfile(scopedID)
if err != nil {
if !errors.Is(err, database.ErrNotFound) {
log.Warningf("profile: failed to get special profile %s: %s", id, err)
}
profile = createSpecialProfile(id, path)
} else if specialProfileNeedsReset(profile) {
log.Debugf("profile: resetting special profile %s", id)
profile = createSpecialProfile(id, path)
}
if profile == nil {
return nil, errors.New("given ID is not a special profile ID")
}
// Prepare profile for first use.
// If we are refetching, assign the layered profile from the previous version.
// The internal references will be updated when the layered profile checks for updates.
if previousVersion != nil && previousVersion.layeredProfile != nil {
profile.layeredProfile = previousVersion.layeredProfile
}
// Profiles must have a layered profile, create a new one if it
// does not yet exist.
if profile.layeredProfile == nil {
profile.layeredProfile = NewLayeredProfile(profile)
}
// Add the profile to the currently active profiles.
addActiveProfile(profile)
return profile, nil
}
func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, changed bool) {
// Get new profile name and check if profile is applicable to special handling.
var newProfileName, newDescription string
@ -115,35 +185,45 @@ func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, chan
changed = true
}
// Update LinkedPath to new value.
if profile.LinkedPath != binaryPath {
profile.LinkedPath = binaryPath
// Update PresentationPath to new value.
if profile.PresentationPath != binaryPath {
profile.PresentationPath = binaryPath
changed = true
}
return true, changed
}
func getSpecialProfile(profileID, linkedPath string) *Profile {
func createSpecialProfile(profileID string, path string) *Profile {
switch profileID {
case UnidentifiedProfileID:
return New(SourceLocal, UnidentifiedProfileID, linkedPath, nil)
return New(&Profile{
ID: UnidentifiedProfileID,
Source: SourceLocal,
PresentationPath: path,
})
case SystemProfileID:
return New(SourceLocal, SystemProfileID, linkedPath, nil)
return New(&Profile{
ID: SystemProfileID,
Source: SourceLocal,
PresentationPath: path,
})
case SystemResolverProfileID:
systemResolverProfile := New(
SourceLocal,
SystemResolverProfileID,
linkedPath,
map[string]interface{}{
return New(&Profile{
ID: SystemResolverProfileID,
Source: SourceLocal,
PresentationPath: path,
Config: map[string]interface{}{
// Explicitly setting the default action to "permit" will improve the
// user experience for people who set the global default to "prompt".
// Resolved domain from the system resolver are checked again when
// attributed to a connection of a regular process. Otherwise, users
// would see two connection prompts for the same domain.
CfgOptionDefaultActionKey: "permit",
// Explicitly allow incoming connections.
CfgOptionBlockInboundKey: status.SecurityLevelOff,
// Explicitly allow localhost and answers to multicast protocols that
// are commonly used by system resolvers.
// TODO: When the Portmaster gains the ability to attribute multicast
@ -154,6 +234,7 @@ func getSpecialProfile(profileID, linkedPath string) *Profile {
"+ LAN UDP/5353", // Allow inbound mDNS requests and multicast replies.
"+ LAN UDP/5355", // Allow inbound LLMNR requests and multicast replies.
"+ LAN UDP/1900", // Allow inbound SSDP requests and multicast replies.
"- *", // Deny everything else.
},
// Explicitly disable all filter lists, as these will be checked later
// with the attributed connection. As this is the system resolver, this
@ -161,44 +242,44 @@ func getSpecialProfile(profileID, linkedPath string) *Profile {
// the system resolver is used. Users who want to
CfgOptionFilterListsKey: []string{},
},
)
return systemResolverProfile
})
case PortmasterProfileID:
profile := New(SourceLocal, PortmasterProfileID, linkedPath, nil)
profile.Internal = true
return profile
return New(&Profile{
ID: PortmasterProfileID,
Source: SourceLocal,
PresentationPath: path,
Internal: true,
})
case PortmasterAppProfileID:
profile := New(
SourceLocal,
PortmasterAppProfileID,
linkedPath,
map[string]interface{}{
return New(&Profile{
ID: PortmasterAppProfileID,
Source: SourceLocal,
PresentationPath: path,
Config: map[string]interface{}{
CfgOptionDefaultActionKey: "block",
CfgOptionEndpointsKey: []string{
"+ Localhost",
"+ .safing.io",
},
},
)
profile.Internal = true
return profile
Internal: true,
})
case PortmasterNotifierProfileID:
profile := New(
SourceLocal,
PortmasterNotifierProfileID,
linkedPath,
map[string]interface{}{
return New(&Profile{
ID: PortmasterNotifierProfileID,
Source: SourceLocal,
PresentationPath: path,
Config: map[string]interface{}{
CfgOptionDefaultActionKey: "block",
CfgOptionEndpointsKey: []string{
"+ Localhost",
},
},
)
profile.Internal = true
return profile
Internal: true,
})
default:
return nil