mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
Move from LinkedPath to Fingerprints and PresentationPath
This commit is contained in:
parent
e1e6a40498
commit
59f776ce2f
9 changed files with 699 additions and 253 deletions
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
323
profile/fingerprint.go
Normal 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
|
||||
}
|
297
profile/get.go
297
profile/get.go
|
@ -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",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue