mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
557 lines
16 KiB
Go
557 lines
16 KiB
Go
package profile
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/tevino/abool"
|
|
|
|
"github.com/safing/portbase/config"
|
|
"github.com/safing/portbase/database/record"
|
|
"github.com/safing/portbase/log"
|
|
"github.com/safing/portbase/utils"
|
|
"github.com/safing/portbase/utils/osdetail"
|
|
"github.com/safing/portmaster/intel/filterlists"
|
|
"github.com/safing/portmaster/profile/endpoints"
|
|
)
|
|
|
|
// ProfileSource is the source of the profile.
|
|
type ProfileSource string
|
|
|
|
// Profile Sources.
|
|
const (
|
|
SourceLocal ProfileSource = "local" // local, editable
|
|
SourceSpecial ProfileSource = "special" // specials (read-only)
|
|
)
|
|
|
|
// 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 { //nolint:maligned // not worth the effort
|
|
record.Base
|
|
sync.RWMutex
|
|
|
|
// ID is a unique identifier for the profile.
|
|
ID string // constant
|
|
// Source describes the source of the profile.
|
|
Source ProfileSource // constant
|
|
// Name is a human readable name of the profile. It
|
|
// defaults to the basename of the application.
|
|
Name string
|
|
// Description may hold an optional description of the
|
|
// profile or the purpose of the application.
|
|
Description string
|
|
// Warning may hold an optional warning about this application.
|
|
// It may be static or be added later on when the Portmaster detected an
|
|
// issue with the application.
|
|
Warning string
|
|
// WarningLastUpdated holds the timestamp when the Warning field was last
|
|
// updated.
|
|
WarningLastUpdated time.Time
|
|
// Homepage may refer to the website of the application
|
|
// vendor.
|
|
Homepage string
|
|
|
|
// Deprecated: Icon holds the icon of the application. The value
|
|
// may either be a filepath, a database key or a blob URL.
|
|
// See IconType for more information.
|
|
Icon string
|
|
// Deprecated: IconType describes the type of the Icon property.
|
|
IconType IconType
|
|
// Icons holds a list of icons to represent the application.
|
|
Icons []Icon
|
|
|
|
// 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
|
|
// 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
|
|
// UsePresentationPath can be used to enable/disable fetching information
|
|
// from the executable at PresentationPath. In some cases, this is not
|
|
// desirable.
|
|
UsePresentationPath bool
|
|
// 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
|
|
// be "simulated" by adjusting the settings
|
|
// directly.
|
|
SecurityLevel uint8
|
|
// Config holds profile specific setttings. It's a nested
|
|
// object with keys defining the settings database path. All keys
|
|
// until the actual settings value (which is everything that is not
|
|
// an object) need to be concatenated for the settings database
|
|
// path.
|
|
Config map[string]interface{}
|
|
|
|
// LastEdited holds the UTC timestamp in seconds when the profile was last
|
|
// edited by the user. This is not set automatically, but has to be manually
|
|
// set by the user interface.
|
|
LastEdited int64
|
|
// Created holds the UTC timestamp in seconds when the
|
|
// profile has been created.
|
|
Created int64
|
|
|
|
// Internal is set to true if the profile is attributed to a
|
|
// Portmaster internal process. Internal is set during profile
|
|
// creation and may be accessed without lock.
|
|
Internal bool
|
|
|
|
// layeredProfile is a link to the layered profile with this profile as the
|
|
// main profile.
|
|
// All processes with the same binary should share the same instance of the
|
|
// local profile and the associated layered profile.
|
|
layeredProfile *LayeredProfile
|
|
|
|
// Interpreted Data
|
|
configPerspective *config.Perspective
|
|
dataParsed bool
|
|
defaultAction uint8
|
|
endpoints endpoints.Endpoints
|
|
serviceEndpoints endpoints.Endpoints
|
|
filterListsSet bool
|
|
filterListIDs []string
|
|
spnUsagePolicy endpoints.Endpoints
|
|
spnTransitHubPolicy endpoints.Endpoints
|
|
spnExitHubPolicy endpoints.Endpoints
|
|
|
|
// Lifecycle Management
|
|
outdated *abool.AtomicBool
|
|
lastActive *int64
|
|
|
|
// savedInternally is set to true for profiles that are saved internally.
|
|
savedInternally bool
|
|
}
|
|
|
|
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 {
|
|
// Check if already parsed.
|
|
if profile.dataParsed {
|
|
return nil
|
|
}
|
|
|
|
// Create new perspective and marked as parsed.
|
|
var err error
|
|
profile.configPerspective, err = config.NewPerspective(profile.Config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create config perspective: %w", err)
|
|
}
|
|
profile.dataParsed = true
|
|
|
|
var lastErr error
|
|
action, ok := profile.configPerspective.GetAsString(CfgOptionDefaultActionKey)
|
|
profile.defaultAction = DefaultActionNotSet
|
|
if ok {
|
|
switch action {
|
|
case DefaultActionPermitValue:
|
|
profile.defaultAction = DefaultActionPermit
|
|
case DefaultActionAskValue:
|
|
profile.defaultAction = DefaultActionAsk
|
|
case DefaultActionBlockValue:
|
|
profile.defaultAction = DefaultActionBlock
|
|
default:
|
|
lastErr = fmt.Errorf(`default action "%s" invalid`, action)
|
|
}
|
|
}
|
|
|
|
list, ok := profile.configPerspective.GetAsStringArray(CfgOptionEndpointsKey)
|
|
profile.endpoints = nil
|
|
if ok {
|
|
profile.endpoints, err = endpoints.ParseEndpoints(list)
|
|
if err != nil {
|
|
lastErr = err
|
|
}
|
|
}
|
|
|
|
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionServiceEndpointsKey)
|
|
profile.serviceEndpoints = nil
|
|
if ok {
|
|
profile.serviceEndpoints, err = endpoints.ParseEndpoints(list)
|
|
if err != nil {
|
|
lastErr = err
|
|
}
|
|
}
|
|
|
|
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionFilterListsKey)
|
|
profile.filterListsSet = false
|
|
if ok {
|
|
profile.filterListIDs, err = filterlists.ResolveListIDs(list)
|
|
if err != nil {
|
|
lastErr = err
|
|
} else {
|
|
profile.filterListsSet = true
|
|
}
|
|
}
|
|
|
|
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionSPNUsagePolicyKey)
|
|
profile.spnUsagePolicy = nil
|
|
if ok {
|
|
profile.spnUsagePolicy, err = endpoints.ParseEndpoints(list)
|
|
if err != nil {
|
|
lastErr = err
|
|
}
|
|
}
|
|
|
|
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionTransitHubPolicyKey)
|
|
profile.spnTransitHubPolicy = nil
|
|
if ok {
|
|
profile.spnTransitHubPolicy, err = endpoints.ParseEndpoints(list)
|
|
if err != nil {
|
|
lastErr = err
|
|
}
|
|
}
|
|
|
|
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionExitHubPolicyKey)
|
|
profile.spnExitHubPolicy = nil
|
|
if ok {
|
|
profile.spnExitHubPolicy, err = endpoints.ParseEndpoints(list)
|
|
if err != nil {
|
|
lastErr = err
|
|
}
|
|
}
|
|
|
|
return lastErr
|
|
}
|
|
|
|
// New returns a new Profile.
|
|
// Optionally, you may supply custom configuration in the flat (key=value) form.
|
|
func New(profile *Profile) *Profile {
|
|
// Create profile if none is given.
|
|
if profile == nil {
|
|
profile = &Profile{}
|
|
}
|
|
|
|
// 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 ID if none is given.
|
|
if profile.ID == "" {
|
|
if len(profile.Fingerprints) > 0 {
|
|
// Derive from fingerprints.
|
|
profile.ID = DeriveProfileID(profile.Fingerprints)
|
|
} else {
|
|
// Generate random ID as fallback.
|
|
log.Warningf("profile: creating new profile without fingerprints to derive ID from")
|
|
profile.ID = utils.RandomUUID("").String()
|
|
}
|
|
}
|
|
|
|
// Make key from ID and source.
|
|
profile.makeKey()
|
|
|
|
// Prepare and parse initial profile config.
|
|
profile.prepProfile()
|
|
if err := profile.parseConfig(); err != nil {
|
|
log.Errorf("profile: failed to parse new profile: %s", err)
|
|
}
|
|
|
|
return profile
|
|
}
|
|
|
|
// ScopedID returns the scoped ID (Source + ID) of the profile.
|
|
func (profile *Profile) ScopedID() string {
|
|
return MakeScopedID(profile.Source, profile.ID)
|
|
}
|
|
|
|
// makeKey derives and sets the record Key from the profile attributes.
|
|
func (profile *Profile) makeKey() {
|
|
profile.SetKey(MakeProfileKey(profile.Source, profile.ID))
|
|
}
|
|
|
|
// Save saves the profile to the database.
|
|
func (profile *Profile) Save() error {
|
|
if profile.ID == "" {
|
|
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)
|
|
}
|
|
|
|
return profileDB.Put(profile)
|
|
}
|
|
|
|
// delete deletes the profile from the database.
|
|
func (profile *Profile) delete() error {
|
|
// Check if a key is set.
|
|
if !profile.KeyIsSet() {
|
|
return errors.New("key is not set")
|
|
}
|
|
|
|
// Delete from database.
|
|
profile.Meta().Delete()
|
|
err := profileDB.Put(profile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Post handling is done by the profile update feed.
|
|
return nil
|
|
}
|
|
|
|
// MarkStillActive marks the profile as still active.
|
|
func (profile *Profile) MarkStillActive() {
|
|
atomic.StoreInt64(profile.lastActive, time.Now().Unix())
|
|
}
|
|
|
|
// LastActive returns the unix timestamp when the profile was last marked as
|
|
// still active.
|
|
func (profile *Profile) LastActive() int64 {
|
|
return atomic.LoadInt64(profile.lastActive)
|
|
}
|
|
|
|
// String returns a string representation of the Profile.
|
|
func (profile *Profile) String() string {
|
|
return fmt.Sprintf("<%s %s/%s>", profile.Name, profile.Source, profile.ID)
|
|
}
|
|
|
|
// IsOutdated returns whether the this instance of the profile is marked as outdated.
|
|
func (profile *Profile) IsOutdated() bool {
|
|
return profile.outdated.IsSet()
|
|
}
|
|
|
|
// GetEndpoints returns the endpoint list of the profile. This functions
|
|
// requires the profile to be read locked.
|
|
func (profile *Profile) GetEndpoints() endpoints.Endpoints {
|
|
return profile.endpoints
|
|
}
|
|
|
|
// GetServiceEndpoints returns the service endpoint list of the profile. This
|
|
// functions requires the profile to be read locked.
|
|
func (profile *Profile) GetServiceEndpoints() endpoints.Endpoints {
|
|
return profile.serviceEndpoints
|
|
}
|
|
|
|
// AddEndpoint adds an endpoint to the endpoint list, saves the profile and reloads the configuration.
|
|
func (profile *Profile) AddEndpoint(newEntry string) {
|
|
profile.addEndpointEntry(CfgOptionEndpointsKey, newEntry)
|
|
}
|
|
|
|
// AddServiceEndpoint adds a service endpoint to the endpoint list, saves the profile and reloads the configuration.
|
|
func (profile *Profile) AddServiceEndpoint(newEntry string) {
|
|
profile.addEndpointEntry(CfgOptionServiceEndpointsKey, newEntry)
|
|
}
|
|
|
|
func (profile *Profile) addEndpointEntry(cfgKey, newEntry string) {
|
|
changed := false
|
|
|
|
// When finished, save the profile.
|
|
defer func() {
|
|
if !changed {
|
|
return
|
|
}
|
|
|
|
err := profile.Save()
|
|
if err != nil {
|
|
log.Warningf("profile: failed to save profile %s after add an endpoint rule: %s", profile.ScopedID(), err)
|
|
}
|
|
}()
|
|
|
|
// Lock the profile for editing.
|
|
profile.Lock()
|
|
defer profile.Unlock()
|
|
|
|
// Get the endpoint list configuration value and add the new entry.
|
|
endpointList, ok := profile.configPerspective.GetAsStringArray(cfgKey)
|
|
if ok {
|
|
// A list already exists, check for duplicates within the same prefix.
|
|
newEntryPrefix := strings.Split(newEntry, " ")[0] + " "
|
|
for _, entry := range endpointList {
|
|
if !strings.HasPrefix(entry, newEntryPrefix) {
|
|
// We found an entry with a different prefix than the new entry.
|
|
// Beyond this entry we cannot possibly know if identical entries will
|
|
// match, so we will have to add the new entry no matter what the rest
|
|
// of the list has.
|
|
break
|
|
}
|
|
|
|
if entry == newEntry {
|
|
// An identical entry is already in the list, abort.
|
|
log.Debugf("profile: ignoring new endpoint rule for %s, as identical is already present: %s", profile, newEntry)
|
|
return
|
|
}
|
|
}
|
|
endpointList = append([]string{newEntry}, endpointList...)
|
|
} else {
|
|
endpointList = []string{newEntry}
|
|
}
|
|
|
|
// Save new value back to profile.
|
|
config.PutValueIntoHierarchicalConfig(profile.Config, cfgKey, endpointList)
|
|
changed = true
|
|
|
|
// Reload the profile manually in order to parse the newly added entry.
|
|
profile.dataParsed = false
|
|
err := profile.parseConfig()
|
|
if err != nil {
|
|
log.Errorf("profile: failed to parse %s config after adding endpoint: %s", profile, err)
|
|
}
|
|
}
|
|
|
|
// LayeredProfile returns the layered profile associated with this profile.
|
|
func (profile *Profile) LayeredProfile() *LayeredProfile {
|
|
profile.Lock()
|
|
defer profile.Unlock()
|
|
|
|
return profile.layeredProfile
|
|
}
|
|
|
|
// EnsureProfile ensures that the given record is a *Profile, and returns it.
|
|
func EnsureProfile(r record.Record) (*Profile, error) {
|
|
// unwrap
|
|
if r.IsWrapped() {
|
|
// only allocate a new struct, if we need it
|
|
newProfile := &Profile{}
|
|
err := record.Unwrap(r, newProfile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newProfile, nil
|
|
}
|
|
|
|
// or adjust type
|
|
newProfile, ok := r.(*Profile)
|
|
if !ok {
|
|
return nil, fmt.Errorf("record not of type *Profile, but %T", r)
|
|
}
|
|
return newProfile, nil
|
|
}
|
|
|
|
// updateMetadata updates meta data fields on the profile and returns whether
|
|
// the profile was changed.
|
|
func (profile *Profile) updateMetadata(binaryPath string) (changed bool) {
|
|
// Check if this is a local profile, else warn and return.
|
|
if profile.Source != SourceLocal {
|
|
log.Warningf("tried to update metadata for non-local profile %s", profile.ScopedID())
|
|
return false
|
|
}
|
|
|
|
// Set PresentationPath if unset.
|
|
if profile.PresentationPath == "" && binaryPath != "" {
|
|
profile.PresentationPath = binaryPath
|
|
changed = true
|
|
}
|
|
|
|
// Migrate LinkedPath to PresentationPath.
|
|
// TODO: Remove in v1.5
|
|
if profile.PresentationPath == "" && profile.LinkedPath != "" {
|
|
profile.PresentationPath = profile.LinkedPath
|
|
changed = true
|
|
}
|
|
|
|
// Set Name if unset.
|
|
if profile.Name == "" && profile.PresentationPath != "" {
|
|
// Generate a default profile name from path.
|
|
profile.Name = osdetail.GenerateBinaryNameFromPath(profile.PresentationPath)
|
|
changed = true
|
|
}
|
|
|
|
// Migrate to Fingerprints.
|
|
// TODO: Remove in v1.5
|
|
if len(profile.Fingerprints) == 0 && profile.LinkedPath != "" {
|
|
profile.Fingerprints = []Fingerprint{
|
|
{
|
|
Type: FingerprintTypePathID,
|
|
Operation: FingerprintOperationEqualsID,
|
|
Value: profile.LinkedPath,
|
|
},
|
|
}
|
|
changed = true
|
|
}
|
|
|
|
// UI Backward Compatibility:
|
|
// Fill LinkedPath with PresentationPath
|
|
// TODO: Remove in v1.1
|
|
if profile.LinkedPath == "" && profile.PresentationPath != "" {
|
|
profile.LinkedPath = profile.PresentationPath
|
|
changed = true
|
|
}
|
|
|
|
return changed
|
|
}
|
|
|
|
// updateMetadataFromSystem updates the profile metadata with data from the
|
|
// operating system and saves it afterwards.
|
|
func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
|
var changed bool
|
|
|
|
// This function is only valid for local profiles.
|
|
if profile.Source != SourceLocal || profile.PresentationPath == "" {
|
|
return fmt.Errorf("tried to update metadata for non-local or non-path profile %s", profile.ScopedID())
|
|
}
|
|
|
|
// Get binary name from PresentationPath.
|
|
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.PresentationPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Check if the new name is valid.
|
|
if strings.TrimSpace(newName) == "" {
|
|
return nil
|
|
}
|
|
|
|
// Apply new data to profile.
|
|
func() {
|
|
// Lock profile for applying metadata.
|
|
profile.Lock()
|
|
defer profile.Unlock()
|
|
|
|
// Apply new name if it changed.
|
|
if profile.Name != newName {
|
|
profile.Name = newName
|
|
changed = true
|
|
}
|
|
}()
|
|
|
|
// If anything changed, save the profile.
|
|
// profile.Lock must not be held!
|
|
if changed {
|
|
err := profile.Save()
|
|
if err != nil {
|
|
log.Warningf("profile: failed to save %s after metadata update: %s", profile.ScopedID(), err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|