Merge pull request #918 from safing/feature/variable-profile-matching
Add support for variable profile matching
This commit is contained in:
@@ -71,7 +71,10 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
|
||||
// Check if the layered profile needs updating.
|
||||
if layeredProfile.NeedsUpdate() {
|
||||
// Update revision counter in connection.
|
||||
conn.ProfileRevisionCounter = layeredProfile.Update()
|
||||
conn.ProfileRevisionCounter = layeredProfile.Update(
|
||||
conn.Process().MatchingData(),
|
||||
conn.Process().CreateProfileCallback,
|
||||
)
|
||||
conn.SaveWhenFinished()
|
||||
|
||||
// Reset verdict for connection.
|
||||
|
||||
@@ -177,9 +177,11 @@ func createPrompt(ctx context.Context, conn *network.Connection, pkt packet.Pack
|
||||
EventData: &promptData{
|
||||
Entity: entity,
|
||||
Profile: promptProfile{
|
||||
Source: string(localProfile.Source),
|
||||
ID: localProfile.ID,
|
||||
LinkedPath: localProfile.LinkedPath,
|
||||
Source: string(localProfile.Source),
|
||||
ID: localProfile.ID,
|
||||
// LinkedPath is used to enhance the display of the prompt in the UI.
|
||||
// TODO: Using the process path is a workaround. Find a cleaner solution.
|
||||
LinkedPath: conn.Process().Path,
|
||||
},
|
||||
},
|
||||
Expires: expires,
|
||||
@@ -259,7 +261,7 @@ func saveResponse(p *profile.Profile, entity *intel.Entity, promptResponse strin
|
||||
// Update the profile if necessary.
|
||||
if p.IsOutdated() {
|
||||
var err error
|
||||
p, err = profile.GetProfile(p.Source, p.ID, p.LinkedPath, false)
|
||||
p, err = profile.GetLocalProfile(p.ID, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -70,7 +70,10 @@ func checkTunneling(ctx context.Context, conn *network.Connection) {
|
||||
// Update profile.
|
||||
if layeredProfile.NeedsUpdate() {
|
||||
// Update revision counter in connection.
|
||||
conn.ProfileRevisionCounter = layeredProfile.Update()
|
||||
conn.ProfileRevisionCounter = layeredProfile.Update(
|
||||
conn.Process().MatchingData(),
|
||||
conn.Process().CreateProfileCallback,
|
||||
)
|
||||
conn.SaveWhenFinished()
|
||||
} else {
|
||||
// Check if the revision counter of the connection needs updating.
|
||||
|
||||
1
go.mod
1
go.mod
@@ -10,6 +10,7 @@ require (
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/jackc/puddle/v2 v2.0.0-beta.1
|
||||
|
||||
2
go.sum
2
go.sum
@@ -92,6 +92,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
|
||||
@@ -193,6 +193,20 @@ func (db *Database) ApplyMigrations() error {
|
||||
return fmt.Errorf("failed to create schema: %w", err)
|
||||
}
|
||||
|
||||
// create a few indexes
|
||||
indexes := []string{
|
||||
`CREATE INDEX profile_id_index ON %s (profile)`,
|
||||
`CREATE INDEX started_time_index ON %s (strftime('%%s', started)+0)`,
|
||||
`CREATE INDEX started_ended_time_index ON %s (strftime('%%s', started)+0, strftime('%%s', ended)+0) WHERE ended IS NOT NULL`,
|
||||
}
|
||||
for _, idx := range indexes {
|
||||
stmt := fmt.Sprintf(idx, db.Schema.Name)
|
||||
|
||||
if err := sqlitex.ExecuteTransient(db.writeConn, stmt, nil); err != nil {
|
||||
return fmt.Errorf("failed to create index: %q: %w", idx, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ func (m *module) start() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to subscribe to network tree: %w", err)
|
||||
}
|
||||
defer close(m.feed)
|
||||
defer func() {
|
||||
_ = sub.Cancel()
|
||||
}()
|
||||
@@ -162,7 +163,5 @@ func (m *module) start() error {
|
||||
}
|
||||
|
||||
func (m *module) stop() error {
|
||||
close(m.feed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/safing/portmaster/network/netutils"
|
||||
"github.com/safing/portmaster/network/packet"
|
||||
"github.com/safing/portmaster/process"
|
||||
_ "github.com/safing/portmaster/process/tags"
|
||||
"github.com/safing/portmaster/resolver"
|
||||
"github.com/safing/spn/navigator"
|
||||
)
|
||||
|
||||
39
process/api.go
Normal file
39
process/api.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/api"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "process/tags",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: handleProcessTagMetadata,
|
||||
Name: "Get Process Tag Metadata",
|
||||
Description: "Get information about process tags.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleProcessTagMetadata(ar *api.Request) (i interface{}, err error) {
|
||||
tagRegistryLock.Lock()
|
||||
defer tagRegistryLock.Unlock()
|
||||
|
||||
// Create response struct.
|
||||
resp := struct {
|
||||
Tags []TagDescription
|
||||
}{
|
||||
Tags: make([]TagDescription, 0, len(tagRegistry)*2),
|
||||
}
|
||||
|
||||
// Get all tag descriptions.
|
||||
for _, th := range tagRegistry {
|
||||
resp.Tags = append(resp.Tags, th.TagDescriptions()...)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -64,18 +64,23 @@ func GetProcessByConnection(ctx context.Context, pktInfo *packet.Info) (process
|
||||
func GetNetworkHost(ctx context.Context, remoteIP net.IP) (process *Process, err error) { //nolint:interfacer
|
||||
now := time.Now().Unix()
|
||||
networkHost := &Process{
|
||||
Name: fmt.Sprintf("Network Host %s", remoteIP),
|
||||
UserName: "Unknown",
|
||||
Name: fmt.Sprintf("Device at %s", remoteIP),
|
||||
UserName: "N/A",
|
||||
UserID: NetworkHostProcessID,
|
||||
Pid: NetworkHostProcessID,
|
||||
ParentPid: NetworkHostProcessID,
|
||||
Path: fmt.Sprintf("net:%s", remoteIP),
|
||||
Tags: []profile.Tag{
|
||||
{
|
||||
Key: "ip",
|
||||
Value: remoteIP.String(),
|
||||
},
|
||||
},
|
||||
FirstSeen: now,
|
||||
LastSeen: now,
|
||||
}
|
||||
|
||||
// Get the (linked) local profile.
|
||||
networkHostProfile, err := profile.GetProfile(profile.SourceNetwork, remoteIP.String(), "", false)
|
||||
networkHostProfile, err := profile.GetLocalProfile("", networkHost.MatchingData(), networkHost.CreateProfileCallback)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -84,16 +89,6 @@ func GetNetworkHost(ctx context.Context, remoteIP net.IP) (process *Process, err
|
||||
networkHost.PrimaryProfileID = networkHostProfile.ScopedID()
|
||||
networkHost.profile = networkHostProfile.LayeredProfile()
|
||||
|
||||
if networkHostProfile.Name == "" {
|
||||
// Assign name and save.
|
||||
networkHostProfile.Name = networkHost.Name
|
||||
|
||||
err := networkHostProfile.Save()
|
||||
if err != nil {
|
||||
log.Warningf("process: failed to save profile %s: %s", networkHostProfile.ScopedID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return networkHost, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -26,5 +26,9 @@ func start() error {
|
||||
updatesPath += string(os.PathSeparator)
|
||||
}
|
||||
|
||||
if err := registerAPIEndpoints(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,14 +42,18 @@ type Process struct {
|
||||
Cwd string
|
||||
CmdLine string
|
||||
FirstArg string
|
||||
|
||||
// SpecialDetail holds special information, the meaning of which can change
|
||||
// based on any of the previous attributes.
|
||||
SpecialDetail string
|
||||
Env map[string]string
|
||||
|
||||
// Profile attributes.
|
||||
// Once set, these don't change; safe for concurrent access.
|
||||
|
||||
// Tags holds extended information about the (virtual) process, which is used
|
||||
// to find a profile.
|
||||
Tags []profile.Tag
|
||||
// MatchingPath holds an alternative binary path that can be used to find a
|
||||
// profile.
|
||||
MatchingPath string
|
||||
|
||||
// PrimaryProfileID holds the scoped ID of the primary profile.
|
||||
PrimaryProfileID string
|
||||
// profile holds the layered profile based on the primary profile.
|
||||
@@ -64,6 +68,16 @@ type Process struct {
|
||||
ExecHashes map[string]string
|
||||
}
|
||||
|
||||
// GetTag returns the process tag with the given ID.
|
||||
func (p *Process) GetTag(tagID string) (profile.Tag, bool) {
|
||||
for _, t := range p.Tags {
|
||||
if t.Key == tagID {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return profile.Tag{}, false
|
||||
}
|
||||
|
||||
// Profile returns the assigned layered profile.
|
||||
func (p *Process) Profile() *profile.LayeredProfile {
|
||||
if p == nil {
|
||||
@@ -177,7 +191,7 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
||||
}
|
||||
|
||||
// Get process information from the system.
|
||||
pInfo, err := processInfo.NewProcess(int32(pid))
|
||||
pInfo, err := processInfo.NewProcessWithContext(ctx, int32(pid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -186,7 +200,7 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
||||
// net yet implemented for windows
|
||||
if onLinux {
|
||||
var uids []int32
|
||||
uids, err = pInfo.Uids()
|
||||
uids, err = pInfo.UidsWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get UID for p%d: %w", pid, err)
|
||||
}
|
||||
@@ -194,7 +208,7 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
||||
}
|
||||
|
||||
// Username
|
||||
process.UserName, err = pInfo.Username()
|
||||
process.UserName, err = pInfo.UsernameWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("process: failed to get Username for p%d: %w", pid, err)
|
||||
}
|
||||
@@ -203,14 +217,14 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
||||
// new.UserHome, err =
|
||||
|
||||
// PPID
|
||||
ppid, err := pInfo.Ppid()
|
||||
ppid, err := pInfo.PpidWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get PPID for p%d: %w", pid, err)
|
||||
}
|
||||
process.ParentPid = int(ppid)
|
||||
|
||||
// Path
|
||||
process.Path, err = pInfo.Exe()
|
||||
process.Path, err = pInfo.ExeWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Path for p%d: %w", pid, err)
|
||||
}
|
||||
@@ -222,20 +236,22 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
||||
_, process.ExecName = filepath.Split(process.Path)
|
||||
|
||||
// Current working directory
|
||||
// net yet implemented for windows
|
||||
// new.Cwd, err = pInfo.Cwd()
|
||||
// if err != nil {
|
||||
// log.Warningf("process: failed to get Cwd: %w", err)
|
||||
// }
|
||||
// not yet implemented for windows
|
||||
if runtime.GOOS != "windows" {
|
||||
process.Cwd, err = pInfo.Cwd()
|
||||
if err != nil {
|
||||
log.Warningf("process: failed to get Cwd: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Command line arguments
|
||||
process.CmdLine, err = pInfo.Cmdline()
|
||||
process.CmdLine, err = pInfo.CmdlineWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Cmdline for p%d: %w", pid, err)
|
||||
}
|
||||
|
||||
// Name
|
||||
process.Name, err = pInfo.Name()
|
||||
process.Name, err = pInfo.NameWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Name for p%d: %w", pid, err)
|
||||
}
|
||||
@@ -243,9 +259,51 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
||||
process.Name = process.ExecName
|
||||
}
|
||||
|
||||
// OS specifics
|
||||
process.specialOSInit()
|
||||
// Get all environment variables
|
||||
env, err := pInfo.EnvironWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get the environment for p%d: %w", pid, err)
|
||||
}
|
||||
// Split env variables in key and value.
|
||||
process.Env = make(map[string]string, len(env))
|
||||
for _, entry := range env {
|
||||
splitted := strings.SplitN(entry, "=", 2)
|
||||
if len(splitted) == 2 {
|
||||
process.Env[strings.Trim(splitted[0], `'"`)] = strings.Trim(splitted[1], `'"`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add process tags.
|
||||
process.addTags()
|
||||
if len(process.Tags) > 0 {
|
||||
log.Tracer(ctx).Debugf("profile: added tags: %+v", process.Tags)
|
||||
}
|
||||
|
||||
process.Save()
|
||||
return process, nil
|
||||
}
|
||||
|
||||
// MatchingData returns the matching data for the process.
|
||||
func (p *Process) MatchingData() *MatchingData {
|
||||
return &MatchingData{p}
|
||||
}
|
||||
|
||||
// MatchingData provides a interface compatible view on the process for profile matching.
|
||||
type MatchingData struct {
|
||||
p *Process
|
||||
}
|
||||
|
||||
// Tags returns process.Tags.
|
||||
func (md *MatchingData) Tags() []profile.Tag { return md.p.Tags }
|
||||
|
||||
// Env returns process.Env.
|
||||
func (md *MatchingData) Env() map[string]string { return md.p.Env }
|
||||
|
||||
// Path returns process.Path.
|
||||
func (md *MatchingData) Path() string { return md.p.Path }
|
||||
|
||||
// MatchingPath returns process.MatchingPath.
|
||||
func (md *MatchingData) MatchingPath() string { return md.p.MatchingPath }
|
||||
|
||||
// Cmdline returns the command line of the process.
|
||||
func (md *MatchingData) Cmdline() string { return md.p.CmdLine }
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
//+build !windows,!linux
|
||||
//go:build !windows && !linux
|
||||
// +build !windows,!linux
|
||||
|
||||
package process
|
||||
|
||||
// SystemProcessID is the PID of the System/Kernel itself.
|
||||
const SystemProcessID = 0
|
||||
|
||||
// specialOSInit does special OS specific Process initialization.
|
||||
func (p *Process) specialOSInit() {
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,3 @@ package process
|
||||
|
||||
// SystemProcessID is the PID of the System/Kernel itself.
|
||||
const SystemProcessID = 0
|
||||
|
||||
// specialOSInit does special OS specific Process initialization.
|
||||
func (p *Process) specialOSInit() {}
|
||||
|
||||
@@ -1,28 +1,4 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils/osdetail"
|
||||
)
|
||||
|
||||
// SystemProcessID is the PID of the System/Kernel itself.
|
||||
const SystemProcessID = 4
|
||||
|
||||
// specialOSInit does special OS specific Process initialization.
|
||||
func (p *Process) specialOSInit() {
|
||||
// add svchost.exe service names to Name
|
||||
if p.ExecName == "svchost.exe" {
|
||||
svcNames, err := osdetail.GetServiceNames(int32(p.Pid))
|
||||
switch err {
|
||||
case nil:
|
||||
p.Name += fmt.Sprintf(" (%s)", svcNames)
|
||||
p.SpecialDetail = svcNames
|
||||
case osdetail.ErrServiceNotFound:
|
||||
log.Tracef("process: failed to get service name for svchost.exe (pid %d): %s", p.Pid, err)
|
||||
default:
|
||||
log.Warningf("process: failed to get service name for svchost.exe (pid %d): %s", p.Pid, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -14,9 +15,6 @@ var ownPID = os.Getpid()
|
||||
|
||||
// GetProfile finds and assigns a profile set to the process.
|
||||
func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
||||
// Update profile metadata outside of *Process lock.
|
||||
defer p.UpdateProfileMetadata()
|
||||
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
@@ -29,25 +27,48 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
||||
// If not, continue with loading the profile.
|
||||
log.Tracer(ctx).Trace("process: loading profile")
|
||||
|
||||
// Check if there is a special profile for this process.
|
||||
localProfile, err := p.loadSpecialProfile(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to load special profile: %w", err)
|
||||
}
|
||||
|
||||
// Otherwise, find a regular profile for the process.
|
||||
if localProfile == nil {
|
||||
localProfile, err = profile.GetLocalProfile("", p.MatchingData(), p.CreateProfileCallback)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to find profile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign profile to process.
|
||||
p.PrimaryProfileID = localProfile.ScopedID()
|
||||
p.profile = localProfile.LayeredProfile()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// loadSpecialProfile attempts to load a special profile.
|
||||
func (p *Process) loadSpecialProfile(_ context.Context) (*profile.Profile, error) {
|
||||
// Check if we need a special profile.
|
||||
profileID := ""
|
||||
var specialProfileID string
|
||||
switch p.Pid {
|
||||
case UnidentifiedProcessID:
|
||||
profileID = profile.UnidentifiedProfileID
|
||||
specialProfileID = profile.UnidentifiedProfileID
|
||||
case UnsolicitedProcessID:
|
||||
profileID = profile.UnsolicitedProfileID
|
||||
specialProfileID = profile.UnsolicitedProfileID
|
||||
case SystemProcessID:
|
||||
profileID = profile.SystemProfileID
|
||||
specialProfileID = profile.SystemProfileID
|
||||
case ownPID:
|
||||
profileID = profile.PortmasterProfileID
|
||||
specialProfileID = profile.PortmasterProfileID
|
||||
default:
|
||||
// Check if this is another Portmaster component.
|
||||
if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) {
|
||||
switch {
|
||||
case strings.Contains(p.Path, "portmaster-app"):
|
||||
profileID = profile.PortmasterAppProfileID
|
||||
specialProfileID = profile.PortmasterAppProfileID
|
||||
case strings.Contains(p.Path, "portmaster-notifier"):
|
||||
profileID = profile.PortmasterNotifierProfileID
|
||||
specialProfileID = profile.PortmasterNotifierProfileID
|
||||
default:
|
||||
// Unexpected binary from within the Portmaster updates directpry.
|
||||
log.Warningf("process: unexpected binary in the updates directory: %s", p.Path)
|
||||
@@ -62,10 +83,10 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
||||
if (p.Path == `C:\Windows\System32\svchost.exe` ||
|
||||
p.Path == `C:\Windows\system32\svchost.exe`) &&
|
||||
// This comes from the windows tasklist command and should be pretty consistent.
|
||||
(strings.Contains(p.SpecialDetail, "Dnscache") ||
|
||||
(profile.KeyAndValueInTags(p.Tags, "svchost", "Dnscache") ||
|
||||
// As an alternative in case of failure, we try to match the svchost.exe service parameter.
|
||||
strings.Contains(p.CmdLine, "-s Dnscache")) {
|
||||
profileID = profile.SystemResolverProfileID
|
||||
specialProfileID = profile.SystemResolverProfileID
|
||||
}
|
||||
case "linux":
|
||||
switch p.Path {
|
||||
@@ -77,41 +98,16 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
||||
"/usr/sbin/nscd",
|
||||
"/usr/bin/dnsmasq",
|
||||
"/usr/sbin/dnsmasq":
|
||||
profileID = profile.SystemResolverProfileID
|
||||
specialProfileID = profile.SystemResolverProfileID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the (linked) local profile.
|
||||
localProfile, err := profile.GetProfile(profile.SourceLocal, profileID, p.Path, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
// Check if a special profile should be applied.
|
||||
if specialProfileID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Assign profile to process.
|
||||
p.PrimaryProfileID = localProfile.ScopedID()
|
||||
p.profile = localProfile.LayeredProfile()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// UpdateProfileMetadata updates the metadata of the local profile
|
||||
// as required.
|
||||
func (p *Process) UpdateProfileMetadata() {
|
||||
// Check if there is a profile to work with.
|
||||
localProfile := p.Profile().LocalProfile()
|
||||
if localProfile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Update metadata of profile.
|
||||
metadataUpdated := localProfile.UpdateMetadata(p.Path)
|
||||
|
||||
// Save the profile if we changed something.
|
||||
if metadataUpdated {
|
||||
err := localProfile.Save()
|
||||
if err != nil {
|
||||
log.Warningf("process: failed to save profile %s: %s", localProfile.ScopedID(), err)
|
||||
}
|
||||
}
|
||||
// Return special profile.
|
||||
return profile.GetSpecialProfile(specialProfileID, p.Path)
|
||||
}
|
||||
|
||||
80
process/tags.go
Normal file
80
process/tags.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portmaster/profile"
|
||||
)
|
||||
|
||||
var (
|
||||
tagRegistry []TagHandler
|
||||
tagRegistryLock sync.RWMutex
|
||||
)
|
||||
|
||||
// TagHandler is a collection of process tag related interfaces.
|
||||
type TagHandler interface {
|
||||
// Name returns the tag handler name.
|
||||
Name() string
|
||||
|
||||
// TagDescriptions returns a list of all possible tags and their description
|
||||
// of this handler.
|
||||
TagDescriptions() []TagDescription
|
||||
|
||||
// AddTags adds tags to the given process.
|
||||
AddTags(p *Process)
|
||||
|
||||
// CreateProfile creates a profile based on the tags of the process.
|
||||
// Returns nil to skip.
|
||||
CreateProfile(p *Process) *profile.Profile
|
||||
}
|
||||
|
||||
// TagDescription describes a tag.
|
||||
type TagDescription struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// RegisterTagHandler registers a tag handler.
|
||||
func RegisterTagHandler(th TagHandler) error {
|
||||
tagRegistryLock.Lock()
|
||||
defer tagRegistryLock.Unlock()
|
||||
|
||||
// Check if the handler is already registered.
|
||||
for _, existingTH := range tagRegistry {
|
||||
if th.Name() == existingTH.Name() {
|
||||
return errors.New("already registered")
|
||||
}
|
||||
}
|
||||
|
||||
tagRegistry = append(tagRegistry, th)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Process) addTags() {
|
||||
tagRegistryLock.RLock()
|
||||
defer tagRegistryLock.RUnlock()
|
||||
|
||||
for _, th := range tagRegistry {
|
||||
th.AddTags(p)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateProfileCallback attempts to create a profile on special attributes
|
||||
// of the process.
|
||||
func (p *Process) CreateProfileCallback() *profile.Profile {
|
||||
tagRegistryLock.RLock()
|
||||
defer tagRegistryLock.RUnlock()
|
||||
|
||||
// Go through handlers and see which one wants to create a profile.
|
||||
for _, th := range tagRegistry {
|
||||
newProfile := th.CreateProfile(p)
|
||||
if newProfile != nil {
|
||||
return newProfile
|
||||
}
|
||||
}
|
||||
|
||||
// No handler wanted to create a profile.
|
||||
return nil
|
||||
}
|
||||
89
process/tags/appimage_unix.go
Normal file
89
process/tags/appimage_unix.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/utils/osdetail"
|
||||
"github.com/safing/portmaster/process"
|
||||
"github.com/safing/portmaster/profile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := process.RegisterTagHandler(new(AppImageHandler))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
appImageName = "AppImage"
|
||||
appImagePathTagKey = "app-image-path"
|
||||
)
|
||||
|
||||
// AppImageHandler handles AppImage processes on Unix systems.
|
||||
type AppImageHandler struct{}
|
||||
|
||||
// Name returns the tag handler name.
|
||||
func (h *AppImageHandler) Name() string {
|
||||
return appImageName
|
||||
}
|
||||
|
||||
// TagDescriptions returns a list of all possible tags and their description
|
||||
// of this handler.
|
||||
func (h *AppImageHandler) TagDescriptions() []process.TagDescription {
|
||||
return []process.TagDescription{
|
||||
{
|
||||
ID: appImagePathTagKey,
|
||||
Name: "App Image Path",
|
||||
Description: "Path to the app image file itself.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddTags adds tags to the given process.
|
||||
func (h *AppImageHandler) AddTags(p *process.Process) {
|
||||
// Get and verify AppImage location.
|
||||
appImageLocation, ok := p.Env["APPIMAGE"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
appImageMountDir, ok := p.Env["APPDIR"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// Check if the process path is in the mount dir.
|
||||
if !strings.HasPrefix(p.Path, appImageMountDir) {
|
||||
return
|
||||
}
|
||||
|
||||
// Add matching path for regular profile matching.
|
||||
p.MatchingPath = appImageLocation
|
||||
|
||||
// Add app image tags.
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: appImagePathTagKey,
|
||||
Value: appImageLocation,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateProfile creates a profile based on the tags of the process.
|
||||
// Returns nil to skip.
|
||||
func (h *AppImageHandler) CreateProfile(p *process.Process) *profile.Profile {
|
||||
if tag, ok := p.GetTag(appImagePathTagKey); ok {
|
||||
return profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: osdetail.GenerateBinaryNameFromPath(tag.Value),
|
||||
PresentationPath: p.Path,
|
||||
UsePresentationPath: true,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypePathID,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Value: tag.Value, // Value of appImagePathTagKey.
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
234
process/tags/interpreter_unix.go
Normal file
234
process/tags/interpreter_unix.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/shlex"
|
||||
|
||||
"github.com/safing/portmaster/process"
|
||||
"github.com/safing/portmaster/profile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := process.RegisterTagHandler(new(InterpHandler)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type interpType struct {
|
||||
process.TagDescription
|
||||
|
||||
Regex *regexp.Regexp
|
||||
}
|
||||
|
||||
var knownInterperters = []interpType{
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "python-script",
|
||||
Name: "Python Script",
|
||||
},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/python[23]\.[0-9]+$`),
|
||||
},
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "shell-script",
|
||||
Name: "Shell Script",
|
||||
},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/(ba|k|z|a)?sh$`),
|
||||
},
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "perl-script",
|
||||
Name: "Perl Script",
|
||||
},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/perl$`),
|
||||
},
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "ruby-script",
|
||||
Name: "Ruby Script",
|
||||
},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/ruby$`),
|
||||
},
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "nodejs-script",
|
||||
Name: "NodeJS Script",
|
||||
},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/node(js)?$`),
|
||||
},
|
||||
/*
|
||||
While similar to nodejs, electron is a bit harder as it uses a multiple processes
|
||||
like Chromium and thus a interpreter match on them will but those processes into
|
||||
different groups.
|
||||
|
||||
I'm still not sure how this could work in the future. Maybe processes should try to
|
||||
inherit the profile of the parents if there is no profile that matches the current one....
|
||||
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "electron-app",
|
||||
Name: "Electron App",
|
||||
},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/electron([0-9]+)?$`),
|
||||
},
|
||||
*/
|
||||
}
|
||||
|
||||
func fileMustBeUTF8(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
// read the first chunk of bytes
|
||||
buf := new(bytes.Buffer)
|
||||
size, _ := io.CopyN(buf, f, 128)
|
||||
if size == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
b := buf.Bytes()[:size]
|
||||
for len(b) > 0 {
|
||||
r, runeSize := utf8.DecodeRune(b)
|
||||
if r == utf8.RuneError {
|
||||
return false
|
||||
}
|
||||
|
||||
b = b[runeSize:]
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// InterpHandler supports adding process tags based on well-known interpreter binaries.
|
||||
type InterpHandler struct{}
|
||||
|
||||
// Name returns "Interpreter".
|
||||
func (h *InterpHandler) Name() string {
|
||||
return "Interpreter"
|
||||
}
|
||||
|
||||
// TagDescriptions returns a set of tag descriptions that InterpHandler provides.
|
||||
func (h *InterpHandler) TagDescriptions() []process.TagDescription {
|
||||
l := make([]process.TagDescription, len(knownInterperters))
|
||||
for idx, it := range knownInterperters {
|
||||
l[idx] = it.TagDescription
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// CreateProfile creates a new profile for any process that has a tag created
|
||||
// by InterpHandler.
|
||||
func (h *InterpHandler) CreateProfile(p *process.Process) *profile.Profile {
|
||||
for _, it := range knownInterperters {
|
||||
if tag, ok := p.GetTag(it.ID); ok {
|
||||
// we can safely ignore the error
|
||||
args, err := shlex.Split(p.CmdLine)
|
||||
if err != nil {
|
||||
// this should not happen since we already called shlex.Split()
|
||||
// when adding the tag. Though, make the linter happy and bail out
|
||||
return nil
|
||||
}
|
||||
|
||||
// if arg0 is the interpreter name itself strip it away
|
||||
// and use the next one
|
||||
if it.Regex.MatchString(args[0]) && len(args) > 1 {
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
return profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: fmt.Sprintf("%s: %s", it.Name, args[0]),
|
||||
PresentationPath: tag.Value,
|
||||
UsePresentationPath: true,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Key: it.ID,
|
||||
Value: tag.Value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTags inspects the process p and adds any interpreter tags that InterpHandler
|
||||
// detects.
|
||||
func (h *InterpHandler) AddTags(p *process.Process) {
|
||||
// check if we have a matching interpreter
|
||||
var matched interpType
|
||||
for _, it := range knownInterperters {
|
||||
if it.Regex.MatchString(p.Path) {
|
||||
matched = it
|
||||
}
|
||||
}
|
||||
|
||||
// zero value means we did not find any interpreter matches.
|
||||
if matched.ID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
args, err := shlex.Split(p.CmdLine)
|
||||
if err != nil {
|
||||
// give up if we failed to parse the command line
|
||||
return
|
||||
}
|
||||
|
||||
// if args[0] matches the interpreter name we expect
|
||||
// the second arg to be a file-name
|
||||
if matched.Regex.MatchString(args[0]) {
|
||||
if len(args) == 1 {
|
||||
// there's no argument given, this is likely an interactive
|
||||
// interpreter session
|
||||
return
|
||||
}
|
||||
|
||||
scriptPath := args[1]
|
||||
if !filepath.IsAbs(scriptPath) {
|
||||
scriptPath = filepath.Join(
|
||||
p.Cwd,
|
||||
scriptPath,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(ppacher): there could be some other arguments as well
|
||||
// so it may be better to scan the whole command line for a path to a UTF8
|
||||
// file and use that one.
|
||||
if !fileMustBeUTF8(scriptPath) {
|
||||
return
|
||||
}
|
||||
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: matched.ID,
|
||||
Value: scriptPath,
|
||||
})
|
||||
|
||||
p.MatchingPath = scriptPath
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// we know that this process is interpreted by some known interpreter but args[0]
|
||||
// does not contain the path to the interpreter.
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: matched.ID,
|
||||
Value: args[0],
|
||||
})
|
||||
|
||||
p.MatchingPath = args[0]
|
||||
}
|
||||
65
process/tags/net.go
Normal file
65
process/tags/net.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"github.com/safing/portmaster/process"
|
||||
"github.com/safing/portmaster/profile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := process.RegisterTagHandler(new(NetworkHandler))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
netName = "Network"
|
||||
netIPTagKey = "ip"
|
||||
)
|
||||
|
||||
// NetworkHandler handles AppImage processes on Unix systems.
|
||||
type NetworkHandler struct{}
|
||||
|
||||
// Name returns the tag handler name.
|
||||
func (h *NetworkHandler) Name() string {
|
||||
return netName
|
||||
}
|
||||
|
||||
// TagDescriptions returns a list of all possible tags and their description
|
||||
// of this handler.
|
||||
func (h *NetworkHandler) TagDescriptions() []process.TagDescription {
|
||||
return []process.TagDescription{
|
||||
{
|
||||
ID: netIPTagKey,
|
||||
Name: "IP Address",
|
||||
Description: "The remote IP address of external requests to Portmaster, if enabled.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddTags adds tags to the given process.
|
||||
func (h *NetworkHandler) AddTags(p *process.Process) {
|
||||
// The "net" tag is added directly when creating the virtual process.
|
||||
}
|
||||
|
||||
// CreateProfile creates a profile based on the tags of the process.
|
||||
// Returns nil to skip.
|
||||
func (h *NetworkHandler) CreateProfile(p *process.Process) *profile.Profile {
|
||||
for _, tag := range p.Tags {
|
||||
if tag.Key == netIPTagKey {
|
||||
return profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: p.Name,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Key: tag.Key,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Value: tag.Value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
99
process/tags/svchost_windows.go
Normal file
99
process/tags/svchost_windows.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils/osdetail"
|
||||
"github.com/safing/portmaster/process"
|
||||
"github.com/safing/portmaster/profile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := process.RegisterTagHandler(new(SVCHostTagHandler))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
svchostName = "Service Host"
|
||||
svchostTagKey = "svchost"
|
||||
)
|
||||
|
||||
// SVCHostTagHandler handles svchost processes on Windows.
|
||||
type SVCHostTagHandler struct{}
|
||||
|
||||
// Name returns the tag handler name.
|
||||
func (h *SVCHostTagHandler) Name() string {
|
||||
return svchostName
|
||||
}
|
||||
|
||||
// TagDescriptions returns a list of all possible tags and their description
|
||||
// of this handler.
|
||||
func (h *SVCHostTagHandler) TagDescriptions() []process.TagDescription {
|
||||
return []process.TagDescription{
|
||||
process.TagDescription{
|
||||
ID: svchostTagKey,
|
||||
Name: "SvcHost Service Name",
|
||||
Description: "Name of a service running in svchost.exe as reported by Windows.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TagKeys returns a list of all possible tag keys of this handler.
|
||||
func (h *SVCHostTagHandler) TagKeys() []string {
|
||||
return []string{svchostTagKey}
|
||||
}
|
||||
|
||||
// AddTags adds tags to the given process.
|
||||
func (h *SVCHostTagHandler) AddTags(p *process.Process) {
|
||||
// Check for svchost.exe.
|
||||
if p.ExecName != "svchost.exe" {
|
||||
return
|
||||
}
|
||||
|
||||
// Get services of svchost instance.
|
||||
svcNames, err := osdetail.GetServiceNames(int32(p.Pid))
|
||||
switch err {
|
||||
case nil:
|
||||
// Append service names to process name.
|
||||
p.Name += fmt.Sprintf(" (%s)", strings.Join(svcNames, ", "))
|
||||
// Add services as tags.
|
||||
for _, svcName := range svcNames {
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: svchostTagKey,
|
||||
Value: svcName,
|
||||
})
|
||||
}
|
||||
case osdetail.ErrServiceNotFound:
|
||||
log.Tracef("process/tags: failed to get service name for svchost.exe (pid %d): %s", p.Pid, err)
|
||||
default:
|
||||
log.Warningf("process/tags: failed to get service name for svchost.exe (pid %d): %s", p.Pid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateProfile creates a profile based on the tags of the process.
|
||||
// Returns nil to skip.
|
||||
func (h *SVCHostTagHandler) CreateProfile(p *process.Process) *profile.Profile {
|
||||
if tag, ok := p.GetTag(svchostTagKey); ok {
|
||||
return profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: "Windows Service: " + osdetail.GenerateBinaryNameFromPath(tag.Value),
|
||||
Icon: `C:\Windows\System32\@WLOGO_48x48.png`,
|
||||
IconType: profile.IconTypeFile,
|
||||
UsePresentationPath: false,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
profile.Fingerprint{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Key: tag.Key,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Value: tag.Value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
122
process/tags/winstore_windows.go
Normal file
122
process/tags/winstore_windows.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/utils/osdetail"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
|
||||
"github.com/safing/portbase/utils"
|
||||
|
||||
"github.com/safing/portmaster/process"
|
||||
"github.com/safing/portmaster/profile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := process.RegisterTagHandler(new(WinStoreHandler))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Add custom WindowsApps path.
|
||||
customWinStorePath := os.ExpandEnv(`%ProgramFiles%\WindowsApps\`)
|
||||
if !utils.StringInSlice(winStorePaths, customWinStorePath) {
|
||||
winStorePaths = append(winStorePaths, customWinStorePath)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
winStoreName = "Windows Store"
|
||||
winStoreAppNameTagKey = "winstore-app-name"
|
||||
winStorePublisherIDTagKey = "winstore-publisher-id"
|
||||
)
|
||||
|
||||
var winStorePaths = []string{`C:\Program Files\WindowsApps\`}
|
||||
|
||||
// WinStoreHandler handles AppImage processes on Unix systems.
|
||||
type WinStoreHandler struct{}
|
||||
|
||||
// Name returns the tag handler name.
|
||||
func (h *WinStoreHandler) Name() string {
|
||||
return winStoreName
|
||||
}
|
||||
|
||||
// TagDescriptions returns a list of all possible tags and their description
|
||||
// of this handler.
|
||||
func (h *WinStoreHandler) TagDescriptions() []process.TagDescription {
|
||||
return []process.TagDescription{
|
||||
{
|
||||
ID: winStoreAppNameTagKey,
|
||||
Name: "Windows Store App Name",
|
||||
Description: "Name of the Windows Store App, as found in the executable path.",
|
||||
},
|
||||
{
|
||||
ID: winStorePublisherIDTagKey,
|
||||
Name: "Windows Store Publisher ID",
|
||||
Description: "Publisher ID of a Windows Store App.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddTags adds tags to the given process.
|
||||
func (h *WinStoreHandler) AddTags(p *process.Process) {
|
||||
// Check if the path is in one of the Windows Store Apps paths.
|
||||
var appDir string
|
||||
for _, winStorePath := range winStorePaths {
|
||||
if strings.HasPrefix(p.Path, winStorePath) {
|
||||
appDir = strings.SplitN(strings.TrimPrefix(p.Path, winStorePath), `\`, 2)[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
if appDir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract information from path.
|
||||
// Example: Microsoft.Office.OneNote_17.6769.57631.0_x64__8wekyb3d8bbwe
|
||||
splitted := strings.Split(appDir, "_")
|
||||
if len(splitted) != 5 { // Four fields, one "__".
|
||||
log.Debugf("profile/tags: windows store app has incompatible app dir format: %q", appDir)
|
||||
return
|
||||
}
|
||||
|
||||
name := splitted[0]
|
||||
// version := splitted[1]
|
||||
// platform := splitted[2]
|
||||
publisherID := splitted[4]
|
||||
|
||||
// Add tags.
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: winStoreAppNameTagKey,
|
||||
Value: name,
|
||||
})
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: winStorePublisherIDTagKey,
|
||||
Value: publisherID,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateProfile creates a profile based on the tags of the process.
|
||||
// Returns nil to skip.
|
||||
func (h *WinStoreHandler) CreateProfile(p *process.Process) *profile.Profile {
|
||||
if tag, ok := p.GetTag(winStoreAppNameTagKey); ok {
|
||||
return profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: osdetail.GenerateBinaryNameFromPath(tag.Value),
|
||||
PresentationPath: p.Path,
|
||||
UsePresentationPath: true,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Key: tag.Key,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Value: tag.Value, // Value of appImagePathTagKey.
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
349
profile/fingerprint.go
Normal file
349
profile/fingerprint.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// # Matching and Scores
|
||||
//
|
||||
// There are three levels:
|
||||
//
|
||||
// 1. Type: What matched?
|
||||
// 1. Tag: 50.000 points
|
||||
// 2. Cmdline: 40.000 points
|
||||
// 3. Env: 30.000 points
|
||||
// 4. MatchingPath: 20.000 points
|
||||
// 5. 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: Length of match
|
||||
|
||||
// Fingerprint Type IDs.
|
||||
const (
|
||||
FingerprintTypeTagID = "tag"
|
||||
FingerprintTypeCmdlineID = "cmdline"
|
||||
FingerprintTypeEnvID = "env"
|
||||
FingerprintTypePathID = "path" // Matches both MatchingPath and Path.
|
||||
|
||||
FingerprintOperationEqualsID = "equals"
|
||||
FingerprintOperationPrefixID = "prefix"
|
||||
FingerprintOperationRegexID = "regex"
|
||||
|
||||
tagMatchBaseScore = 50_000
|
||||
cmdlineMatchBaseScore = 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
|
||||
Cmdline() 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) {
|
||||
// Find best match.
|
||||
for _, match := range fp.regex.FindAllString(value, -1) {
|
||||
// Save match length if higher than score.
|
||||
// This will also ignore empty matches.
|
||||
if len(match) > score {
|
||||
score = len(match)
|
||||
}
|
||||
}
|
||||
|
||||
// Add base score and return if anything was found.
|
||||
if score > 0 {
|
||||
return fingerprintRegexBaseScore + checkMatchStrength(score)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
type parsedFingerprints struct {
|
||||
tagPrints []matchingFingerprint
|
||||
envPrints []matchingFingerprint
|
||||
pathPrints []matchingFingerprint
|
||||
cmdlinePrints []matchingFingerprint
|
||||
}
|
||||
|
||||
func parseFingerprints(raw []Fingerprint, deprecatedLinkedPath string) (parsed *parsedFingerprints, firstErr error) {
|
||||
parsed = &parsedFingerprints{}
|
||||
|
||||
// Add deprecated LinkedPath to fingerprints, if they are empty.
|
||||
// TODO: Remove in v1.5
|
||||
if len(raw) == 0 && 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, FingerprintTypeCmdlineID:
|
||||
// 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)
|
||||
case FingerprintTypeCmdlineID:
|
||||
parsed.cmdlinePrints = append(parsed.cmdlinePrints, 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.
|
||||
tags := md.Tags()
|
||||
if len(tags) > 0 {
|
||||
for _, tagPrint := range prints.tagPrints {
|
||||
for _, tag := range 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 cmdline.
|
||||
cmdline := md.Cmdline()
|
||||
if cmdline != "" {
|
||||
for _, cmdlinePrint := range prints.cmdlinePrints {
|
||||
if score := cmdlinePrint.Match(cmdline); score > highestScore {
|
||||
highestScore = score
|
||||
}
|
||||
}
|
||||
if highestScore > 0 {
|
||||
return cmdlineMatchBaseScore + 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
|
||||
}
|
||||
321
profile/get.go
321
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,113 @@ 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.
|
||||
var created bool
|
||||
if profile == nil {
|
||||
created = true
|
||||
|
||||
// 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(),
|
||||
UsePresentationPath: true,
|
||||
Fingerprints: []Fingerprint{
|
||||
{
|
||||
Type: FingerprintTypePathID,
|
||||
Operation: FingerprintOperationEqualsID,
|
||||
Value: fpPath,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If there was a non-recoverable error, return here.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Initialize and update profile.
|
||||
|
||||
// Update metadata.
|
||||
changed := profile.updateMetadata(md.Path())
|
||||
|
||||
// Save if created or changed.
|
||||
if created || changed {
|
||||
// Save profile.
|
||||
err := profile.Save()
|
||||
if err != nil {
|
||||
log.Warningf("profile: failed to save profile %s after creation: %s", profile.ScopedID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger further metadata fetching from system if profile was created.
|
||||
if created && profile.UsePresentationPath {
|
||||
module.StartWorker("get profile metadata", profile.updateMetadataFromSystem)
|
||||
}
|
||||
|
||||
// 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 +155,92 @@ 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 with any returned fingerprints.
|
||||
if prints == nil {
|
||||
continue profileFeed
|
||||
}
|
||||
|
||||
// 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 +262,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",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/migration"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/status"
|
||||
)
|
||||
@@ -19,6 +21,11 @@ func registerMigrations() error {
|
||||
Version: "v0.7.19",
|
||||
MigrateFunc: migrateNetworkRatingSystem,
|
||||
},
|
||||
migration.Migration{
|
||||
Description: "Migrate from LinkedPath to Fingerprints and PresentationPath",
|
||||
Version: "v0.9.9",
|
||||
MigrateFunc: migrateLinkedPath,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,3 +57,43 @@ func migrateNetworkRatingSystem(ctx context.Context, _, to *version.Version, db
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateLinkedPath(ctx context.Context, _, to *version.Version, db *database.Interface) error {
|
||||
// Get iterator over all profiles.
|
||||
it, err := db.Query(query.New(profilesDBPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query profiles: %w", err)
|
||||
}
|
||||
|
||||
// Migrate all profiles.
|
||||
for r := range it.Next {
|
||||
// Parse profile.
|
||||
profile, err := EnsureProfile(r)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Debugf("profiles: failed to parse profile %s for migration: %s", r.Key(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if there is no LinkedPath to migrate from.
|
||||
if profile.LinkedPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update metadata and save if changed.
|
||||
if profile.updateMetadata("") {
|
||||
err = db.Put(profile)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Debugf("profiles: failed to save profile %s after migration: %s", r.Key(), err)
|
||||
} else {
|
||||
log.Tracer(ctx).Tracef("profiles: migrated profile %s to %s", r.Key(), to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there was an error while iterating.
|
||||
if it.Err() != nil {
|
||||
return fmt.Errorf("profiles: failed to iterate over profiles for migration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -26,11 +25,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 +79,21 @@ 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
|
||||
// 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
|
||||
@@ -143,6 +149,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 +238,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()
|
||||
}
|
||||
|
||||
@@ -415,106 +422,78 @@ func EnsureProfile(r record.Record) (*Profile, error) {
|
||||
return newProfile, nil
|
||||
}
|
||||
|
||||
// UpdateMetadata updates meta data fields on the profile and returns whether
|
||||
// the profile was changed. If there is data that needs to be fetched from the
|
||||
// operating system, it will start an async worker to fetch that data and save
|
||||
// the profile afterwards.
|
||||
func (profile *Profile) UpdateMetadata(binaryPath string) (changed bool) {
|
||||
// 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
|
||||
}
|
||||
|
||||
profile.Lock()
|
||||
defer profile.Unlock()
|
||||
|
||||
// Update special profile and return if it was one.
|
||||
if ok, changed := updateSpecialProfileMetadata(profile, binaryPath); ok {
|
||||
return changed
|
||||
// Set PresentationPath if unset.
|
||||
if profile.PresentationPath == "" && binaryPath != "" {
|
||||
profile.PresentationPath = binaryPath
|
||||
changed = true
|
||||
}
|
||||
|
||||
var needsUpdateFromSystem bool
|
||||
// Migrate LinkedPath to PresentationPath.
|
||||
// TODO: Remove in v1.5
|
||||
if profile.PresentationPath == "" && profile.LinkedPath != "" {
|
||||
profile.PresentationPath = profile.LinkedPath
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Check profile name.
|
||||
filename := filepath.Base(profile.LinkedPath)
|
||||
// Set Name if unset.
|
||||
if profile.Name == "" && profile.PresentationPath != "" {
|
||||
// Generate a default profile name from path.
|
||||
profile.Name = osdetail.GenerateBinaryNameFromPath(profile.PresentationPath)
|
||||
changed = true
|
||||
}
|
||||
|
||||
// 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)
|
||||
if profile.Name == filename {
|
||||
// TODO: Theoretically, the generated name could be identical to the
|
||||
// filename.
|
||||
// As a quick fix, append a space to the name.
|
||||
profile.Name += " "
|
||||
// Migrato 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
|
||||
needsUpdateFromSystem = true
|
||||
}
|
||||
|
||||
// If needed, get more/better data from the operating system.
|
||||
if needsUpdateFromSystem {
|
||||
module.StartWorker("get profile metadata", profile.updateMetadataFromSystem)
|
||||
// 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
|
||||
}
|
||||
|
||||
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 {
|
||||
var changed bool
|
||||
|
||||
// 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.
|
||||
save := false
|
||||
defer func() {
|
||||
if save {
|
||||
err := profile.Save()
|
||||
if err != nil {
|
||||
log.Warningf("profile: failed to save %s after metadata update: %s", profile.ScopedID(), err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Get binary name from linked path.
|
||||
newName, err := osdetail.GetBinaryNameFromSystem(profile.LinkedPath)
|
||||
// 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.LinkedPath, err)
|
||||
log.Warningf("profile: error while getting binary name for %s: %s", profile.PresentationPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -524,25 +503,26 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get filename of linked path for comparison.
|
||||
filename := filepath.Base(profile.LinkedPath)
|
||||
// Apply new data to profile.
|
||||
func() {
|
||||
// Lock profile for applying metadata.
|
||||
profile.Lock()
|
||||
defer profile.Unlock()
|
||||
|
||||
// TODO: Theoretically, the generated name from the system could be identical
|
||||
// to the filename. This would mean that the worker is triggered every time
|
||||
// the profile is freshly loaded.
|
||||
if newName == filename {
|
||||
// As a quick fix, append a space to the name.
|
||||
newName += " "
|
||||
}
|
||||
// Apply new name if it changed.
|
||||
if profile.Name != newName {
|
||||
profile.Name = newName
|
||||
changed = true
|
||||
}
|
||||
}()
|
||||
|
||||
// Lock profile for applying metadata.
|
||||
profile.Lock()
|
||||
defer profile.Unlock()
|
||||
|
||||
// Apply new name if it changed.
|
||||
if profile.Name != newName {
|
||||
profile.Name = newName
|
||||
save = 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
|
||||
|
||||
@@ -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,7 +77,94 @@ If you think you might have messed up the settings of the System DNS Client, jus
|
||||
PortmasterNotifierProfileDescription = `This is the Portmaster UI Tray Notifier.`
|
||||
)
|
||||
|
||||
func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, changed bool) {
|
||||
// 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.
|
||||
var created bool
|
||||
profile, err = getProfile(scopedID)
|
||||
switch {
|
||||
case err == nil:
|
||||
// Reset profile if needed.
|
||||
if specialProfileNeedsReset(profile) {
|
||||
profile = createSpecialProfile(id, path)
|
||||
created = true
|
||||
}
|
||||
case !errors.Is(err, database.ErrNotFound):
|
||||
// Warn when fetching from DB fails, and create new profile as fallback.
|
||||
log.Warningf("profile: failed to get special profile %s: %s", id, err)
|
||||
fallthrough
|
||||
default:
|
||||
// Create new profile if it does not exist (or failed to load).
|
||||
profile = createSpecialProfile(id, path)
|
||||
created = true
|
||||
}
|
||||
// Check if creating the special profile was successful.
|
||||
if profile == nil {
|
||||
return nil, errors.New("given ID is not a special profile ID")
|
||||
}
|
||||
|
||||
// Update metadata
|
||||
changed := updateSpecialProfileMetadata(profile, path)
|
||||
|
||||
// Save if created or changed.
|
||||
if created || changed {
|
||||
err := profile.Save()
|
||||
if err != nil {
|
||||
log.Warningf("profile: failed to save special profile %s: %s", scopedID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) (changed bool) {
|
||||
// Get new profile name and check if profile is applicable to special handling.
|
||||
var newProfileName, newDescription string
|
||||
switch profile.ID {
|
||||
@@ -100,7 +190,7 @@ func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, chan
|
||||
newProfileName = PortmasterNotifierProfileName
|
||||
newDescription = PortmasterNotifierProfileDescription
|
||||
default:
|
||||
return false, false
|
||||
return false
|
||||
}
|
||||
|
||||
// Update profile name if needed.
|
||||
@@ -115,35 +205,52 @@ 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
|
||||
return 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 UnsolicitedProfileID:
|
||||
return New(&Profile{
|
||||
ID: UnsolicitedProfileID,
|
||||
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 +261,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 +269,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
|
||||
@@ -225,7 +333,7 @@ func specialProfileNeedsReset(profile *Profile) bool {
|
||||
|
||||
switch profile.ID {
|
||||
case SystemResolverProfileID:
|
||||
return canBeUpgraded(profile, "20.11.2021")
|
||||
return canBeUpgraded(profile, "21.10.2022")
|
||||
case PortmasterAppProfileID:
|
||||
return canBeUpgraded(profile, "8.9.2021")
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user