wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
555
service/profile/profile.go
Normal file
555
service/profile/profile.go
Normal file
@@ -0,0 +1,555 @@
|
||||
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/portmaster/service/intel/filterlists"
|
||||
"github.com/safing/portmaster/service/profile/binmeta"
|
||||
"github.com/safing/portmaster/service/profile/endpoints"
|
||||
)
|
||||
|
||||
// ProfileSource is the source of the profile.
|
||||
type ProfileSource string //nolint:golint
|
||||
|
||||
// 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 binmeta.IconType
|
||||
// Icons holds a list of icons to represent the application.
|
||||
Icons []binmeta.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
|
||||
// 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 = binmeta.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, md MatchingData) 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 home from ENV.
|
||||
var home string
|
||||
if env := md.Env(); env != nil {
|
||||
home = env["HOME"]
|
||||
}
|
||||
|
||||
// Get binary icon and name.
|
||||
newIcon, newName, err := binmeta.GetIconAndName(ctx, profile.PresentationPath, home)
|
||||
if err != nil {
|
||||
log.Warningf("profile: failed to get binary icon/name for %s: %s", profile.PresentationPath, err)
|
||||
}
|
||||
|
||||
// Apply new data to profile.
|
||||
func() {
|
||||
// Lock profile for applying metadata.
|
||||
profile.Lock()
|
||||
defer profile.Unlock()
|
||||
|
||||
// Apply new name if it changed.
|
||||
if newName != "" && profile.Name != newName {
|
||||
profile.Name = newName
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Apply new icon if found.
|
||||
if newIcon != nil {
|
||||
if len(profile.Icons) == 0 {
|
||||
profile.Icons = []binmeta.Icon{*newIcon}
|
||||
} else {
|
||||
profile.Icons = append(profile.Icons, *newIcon)
|
||||
profile.Icons = binmeta.SortAndCompactIcons(profile.Icons)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user