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.
|
// Check if the layered profile needs updating.
|
||||||
if layeredProfile.NeedsUpdate() {
|
if layeredProfile.NeedsUpdate() {
|
||||||
// Update revision counter in connection.
|
// Update revision counter in connection.
|
||||||
conn.ProfileRevisionCounter = layeredProfile.Update()
|
conn.ProfileRevisionCounter = layeredProfile.Update(
|
||||||
|
conn.Process().MatchingData(),
|
||||||
|
conn.Process().CreateProfileCallback,
|
||||||
|
)
|
||||||
conn.SaveWhenFinished()
|
conn.SaveWhenFinished()
|
||||||
|
|
||||||
// Reset verdict for connection.
|
// Reset verdict for connection.
|
||||||
|
|||||||
@@ -177,9 +177,11 @@ func createPrompt(ctx context.Context, conn *network.Connection, pkt packet.Pack
|
|||||||
EventData: &promptData{
|
EventData: &promptData{
|
||||||
Entity: entity,
|
Entity: entity,
|
||||||
Profile: promptProfile{
|
Profile: promptProfile{
|
||||||
Source: string(localProfile.Source),
|
Source: string(localProfile.Source),
|
||||||
ID: localProfile.ID,
|
ID: localProfile.ID,
|
||||||
LinkedPath: localProfile.LinkedPath,
|
// 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,
|
Expires: expires,
|
||||||
@@ -259,7 +261,7 @@ func saveResponse(p *profile.Profile, entity *intel.Entity, promptResponse strin
|
|||||||
// Update the profile if necessary.
|
// Update the profile if necessary.
|
||||||
if p.IsOutdated() {
|
if p.IsOutdated() {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,10 @@ func checkTunneling(ctx context.Context, conn *network.Connection) {
|
|||||||
// Update profile.
|
// Update profile.
|
||||||
if layeredProfile.NeedsUpdate() {
|
if layeredProfile.NeedsUpdate() {
|
||||||
// Update revision counter in connection.
|
// Update revision counter in connection.
|
||||||
conn.ProfileRevisionCounter = layeredProfile.Update()
|
conn.ProfileRevisionCounter = layeredProfile.Update(
|
||||||
|
conn.Process().MatchingData(),
|
||||||
|
conn.Process().CreateProfileCallback,
|
||||||
|
)
|
||||||
conn.SaveWhenFinished()
|
conn.SaveWhenFinished()
|
||||||
} else {
|
} else {
|
||||||
// Check if the revision counter of the connection needs updating.
|
// 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/ghodss/yaml v1.0.0
|
||||||
github.com/godbus/dbus/v5 v5.1.0
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/google/gopacket v1.1.19
|
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-multierror v1.1.1
|
||||||
github.com/hashicorp/go-version v1.6.0
|
github.com/hashicorp/go-version v1.6.0
|
||||||
github.com/jackc/puddle/v2 v2.0.0-beta.1
|
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/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 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ func (m *module) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to subscribe to network tree: %w", err)
|
return fmt.Errorf("failed to subscribe to network tree: %w", err)
|
||||||
}
|
}
|
||||||
|
defer close(m.feed)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = sub.Cancel()
|
_ = sub.Cancel()
|
||||||
}()
|
}()
|
||||||
@@ -162,7 +163,5 @@ func (m *module) start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *module) stop() error {
|
func (m *module) stop() error {
|
||||||
close(m.feed)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/safing/portmaster/network/netutils"
|
"github.com/safing/portmaster/network/netutils"
|
||||||
"github.com/safing/portmaster/network/packet"
|
"github.com/safing/portmaster/network/packet"
|
||||||
"github.com/safing/portmaster/process"
|
"github.com/safing/portmaster/process"
|
||||||
|
_ "github.com/safing/portmaster/process/tags"
|
||||||
"github.com/safing/portmaster/resolver"
|
"github.com/safing/portmaster/resolver"
|
||||||
"github.com/safing/spn/navigator"
|
"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
|
func GetNetworkHost(ctx context.Context, remoteIP net.IP) (process *Process, err error) { //nolint:interfacer
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
networkHost := &Process{
|
networkHost := &Process{
|
||||||
Name: fmt.Sprintf("Network Host %s", remoteIP),
|
Name: fmt.Sprintf("Device at %s", remoteIP),
|
||||||
UserName: "Unknown",
|
UserName: "N/A",
|
||||||
UserID: NetworkHostProcessID,
|
UserID: NetworkHostProcessID,
|
||||||
Pid: NetworkHostProcessID,
|
Pid: NetworkHostProcessID,
|
||||||
ParentPid: NetworkHostProcessID,
|
ParentPid: NetworkHostProcessID,
|
||||||
Path: fmt.Sprintf("net:%s", remoteIP),
|
Tags: []profile.Tag{
|
||||||
|
{
|
||||||
|
Key: "ip",
|
||||||
|
Value: remoteIP.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
FirstSeen: now,
|
FirstSeen: now,
|
||||||
LastSeen: now,
|
LastSeen: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the (linked) local profile.
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -84,16 +89,6 @@ func GetNetworkHost(ctx context.Context, remoteIP net.IP) (process *Process, err
|
|||||||
networkHost.PrimaryProfileID = networkHostProfile.ScopedID()
|
networkHost.PrimaryProfileID = networkHostProfile.ScopedID()
|
||||||
networkHost.profile = networkHostProfile.LayeredProfile()
|
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
|
return networkHost, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,5 +26,9 @@ func start() error {
|
|||||||
updatesPath += string(os.PathSeparator)
|
updatesPath += string(os.PathSeparator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := registerAPIEndpoints(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,14 +42,18 @@ type Process struct {
|
|||||||
Cwd string
|
Cwd string
|
||||||
CmdLine string
|
CmdLine string
|
||||||
FirstArg string
|
FirstArg string
|
||||||
|
Env map[string]string
|
||||||
// SpecialDetail holds special information, the meaning of which can change
|
|
||||||
// based on any of the previous attributes.
|
|
||||||
SpecialDetail string
|
|
||||||
|
|
||||||
// Profile attributes.
|
// Profile attributes.
|
||||||
// Once set, these don't change; safe for concurrent access.
|
// 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 holds the scoped ID of the primary profile.
|
||||||
PrimaryProfileID string
|
PrimaryProfileID string
|
||||||
// profile holds the layered profile based on the primary profile.
|
// profile holds the layered profile based on the primary profile.
|
||||||
@@ -64,6 +68,16 @@ type Process struct {
|
|||||||
ExecHashes map[string]string
|
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.
|
// Profile returns the assigned layered profile.
|
||||||
func (p *Process) Profile() *profile.LayeredProfile {
|
func (p *Process) Profile() *profile.LayeredProfile {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
@@ -177,7 +191,7 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get process information from the system.
|
// Get process information from the system.
|
||||||
pInfo, err := processInfo.NewProcess(int32(pid))
|
pInfo, err := processInfo.NewProcessWithContext(ctx, int32(pid))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -186,7 +200,7 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
|||||||
// net yet implemented for windows
|
// net yet implemented for windows
|
||||||
if onLinux {
|
if onLinux {
|
||||||
var uids []int32
|
var uids []int32
|
||||||
uids, err = pInfo.Uids()
|
uids, err = pInfo.UidsWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get UID for p%d: %w", pid, err)
|
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
|
// Username
|
||||||
process.UserName, err = pInfo.Username()
|
process.UserName, err = pInfo.UsernameWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("process: failed to get Username for p%d: %w", pid, err)
|
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 =
|
// new.UserHome, err =
|
||||||
|
|
||||||
// PPID
|
// PPID
|
||||||
ppid, err := pInfo.Ppid()
|
ppid, err := pInfo.PpidWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get PPID for p%d: %w", pid, err)
|
return nil, fmt.Errorf("failed to get PPID for p%d: %w", pid, err)
|
||||||
}
|
}
|
||||||
process.ParentPid = int(ppid)
|
process.ParentPid = int(ppid)
|
||||||
|
|
||||||
// Path
|
// Path
|
||||||
process.Path, err = pInfo.Exe()
|
process.Path, err = pInfo.ExeWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get Path for p%d: %w", pid, err)
|
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)
|
_, process.ExecName = filepath.Split(process.Path)
|
||||||
|
|
||||||
// Current working directory
|
// Current working directory
|
||||||
// net yet implemented for windows
|
// not yet implemented for windows
|
||||||
// new.Cwd, err = pInfo.Cwd()
|
if runtime.GOOS != "windows" {
|
||||||
// if err != nil {
|
process.Cwd, err = pInfo.Cwd()
|
||||||
// log.Warningf("process: failed to get Cwd: %w", err)
|
if err != nil {
|
||||||
// }
|
log.Warningf("process: failed to get Cwd: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Command line arguments
|
// Command line arguments
|
||||||
process.CmdLine, err = pInfo.Cmdline()
|
process.CmdLine, err = pInfo.CmdlineWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get Cmdline for p%d: %w", pid, err)
|
return nil, fmt.Errorf("failed to get Cmdline for p%d: %w", pid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
process.Name, err = pInfo.Name()
|
process.Name, err = pInfo.NameWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get Name for p%d: %w", pid, err)
|
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
|
process.Name = process.ExecName
|
||||||
}
|
}
|
||||||
|
|
||||||
// OS specifics
|
// Get all environment variables
|
||||||
process.specialOSInit()
|
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()
|
process.Save()
|
||||||
return process, nil
|
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
|
package process
|
||||||
|
|
||||||
// SystemProcessID is the PID of the System/Kernel itself.
|
// SystemProcessID is the PID of the System/Kernel itself.
|
||||||
const SystemProcessID = 0
|
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.
|
// SystemProcessID is the PID of the System/Kernel itself.
|
||||||
const SystemProcessID = 0
|
const SystemProcessID = 0
|
||||||
|
|
||||||
// specialOSInit does special OS specific Process initialization.
|
|
||||||
func (p *Process) specialOSInit() {}
|
|
||||||
|
|||||||
@@ -1,28 +1,4 @@
|
|||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
"github.com/safing/portbase/utils/osdetail"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SystemProcessID is the PID of the System/Kernel itself.
|
// SystemProcessID is the PID of the System/Kernel itself.
|
||||||
const SystemProcessID = 4
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,9 +15,6 @@ var ownPID = os.Getpid()
|
|||||||
|
|
||||||
// GetProfile finds and assigns a profile set to the process.
|
// GetProfile finds and assigns a profile set to the process.
|
||||||
func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
||||||
// Update profile metadata outside of *Process lock.
|
|
||||||
defer p.UpdateProfileMetadata()
|
|
||||||
|
|
||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
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.
|
// If not, continue with loading the profile.
|
||||||
log.Tracer(ctx).Trace("process: loading 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.
|
// Check if we need a special profile.
|
||||||
profileID := ""
|
var specialProfileID string
|
||||||
switch p.Pid {
|
switch p.Pid {
|
||||||
case UnidentifiedProcessID:
|
case UnidentifiedProcessID:
|
||||||
profileID = profile.UnidentifiedProfileID
|
specialProfileID = profile.UnidentifiedProfileID
|
||||||
case UnsolicitedProcessID:
|
case UnsolicitedProcessID:
|
||||||
profileID = profile.UnsolicitedProfileID
|
specialProfileID = profile.UnsolicitedProfileID
|
||||||
case SystemProcessID:
|
case SystemProcessID:
|
||||||
profileID = profile.SystemProfileID
|
specialProfileID = profile.SystemProfileID
|
||||||
case ownPID:
|
case ownPID:
|
||||||
profileID = profile.PortmasterProfileID
|
specialProfileID = profile.PortmasterProfileID
|
||||||
default:
|
default:
|
||||||
// Check if this is another Portmaster component.
|
// Check if this is another Portmaster component.
|
||||||
if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) {
|
if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) {
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(p.Path, "portmaster-app"):
|
case strings.Contains(p.Path, "portmaster-app"):
|
||||||
profileID = profile.PortmasterAppProfileID
|
specialProfileID = profile.PortmasterAppProfileID
|
||||||
case strings.Contains(p.Path, "portmaster-notifier"):
|
case strings.Contains(p.Path, "portmaster-notifier"):
|
||||||
profileID = profile.PortmasterNotifierProfileID
|
specialProfileID = profile.PortmasterNotifierProfileID
|
||||||
default:
|
default:
|
||||||
// Unexpected binary from within the Portmaster updates directpry.
|
// Unexpected binary from within the Portmaster updates directpry.
|
||||||
log.Warningf("process: unexpected binary in the updates directory: %s", p.Path)
|
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` ||
|
if (p.Path == `C:\Windows\System32\svchost.exe` ||
|
||||||
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.
|
// 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.
|
// As an alternative in case of failure, we try to match the svchost.exe service parameter.
|
||||||
strings.Contains(p.CmdLine, "-s Dnscache")) {
|
strings.Contains(p.CmdLine, "-s Dnscache")) {
|
||||||
profileID = profile.SystemResolverProfileID
|
specialProfileID = profile.SystemResolverProfileID
|
||||||
}
|
}
|
||||||
case "linux":
|
case "linux":
|
||||||
switch p.Path {
|
switch p.Path {
|
||||||
@@ -77,41 +98,16 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
|||||||
"/usr/sbin/nscd",
|
"/usr/sbin/nscd",
|
||||||
"/usr/bin/dnsmasq",
|
"/usr/bin/dnsmasq",
|
||||||
"/usr/sbin/dnsmasq":
|
"/usr/sbin/dnsmasq":
|
||||||
profileID = profile.SystemResolverProfileID
|
specialProfileID = profile.SystemResolverProfileID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the (linked) local profile.
|
// Check if a special profile should be applied.
|
||||||
localProfile, err := profile.GetProfile(profile.SourceLocal, profileID, p.Path, false)
|
if specialProfileID == "" {
|
||||||
if err != nil {
|
return nil, nil
|
||||||
return false, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign profile to process.
|
// Return special profile.
|
||||||
p.PrimaryProfileID = localProfile.ScopedID()
|
return profile.GetSpecialProfile(specialProfileID, p.Path)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
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.
|
// addActiveProfile registers a active profile.
|
||||||
func addActiveProfile(profile *Profile) {
|
func addActiveProfile(profile *Profile) {
|
||||||
activeProfilesLock.Lock()
|
activeProfilesLock.Lock()
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portbase/config"
|
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
"github.com/safing/portmaster/intel/filterlists"
|
"github.com/safing/portmaster/intel/filterlists"
|
||||||
"github.com/safing/portmaster/profile/endpoints"
|
"github.com/safing/portmaster/profile/endpoints"
|
||||||
@@ -91,11 +90,7 @@ func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error {
|
|||||||
lastErr = err
|
lastErr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
// build global profile for reference
|
// Build config.
|
||||||
profile := New(SourceSpecial, "global-config", "", nil)
|
|
||||||
profile.Name = "Global Configuration"
|
|
||||||
profile.Internal = true
|
|
||||||
|
|
||||||
newConfig := make(map[string]interface{})
|
newConfig := make(map[string]interface{})
|
||||||
// fill profile config options
|
// fill profile config options
|
||||||
for key, value := range cfgStringOptions {
|
for key, value := range cfgStringOptions {
|
||||||
@@ -111,8 +106,14 @@ func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error {
|
|||||||
newConfig[key] = value()
|
newConfig[key] = value()
|
||||||
}
|
}
|
||||||
|
|
||||||
// expand and assign
|
// Build global profile for reference.
|
||||||
profile.Config = config.Expand(newConfig)
|
profile := New(&Profile{
|
||||||
|
ID: "global-config",
|
||||||
|
Source: SourceSpecial,
|
||||||
|
Name: "Global Configuration",
|
||||||
|
Config: newConfig,
|
||||||
|
Internal: true,
|
||||||
|
})
|
||||||
|
|
||||||
// save profile
|
// save profile
|
||||||
err = profile.Save()
|
err = profile.Save()
|
||||||
|
|||||||
@@ -65,41 +65,19 @@ func startProfileUpdateChecker() error {
|
|||||||
continue profileFeed
|
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.
|
// Always increase the revision counter of the layer profile.
|
||||||
// This marks previous connections in the UI as decided with outdated settings.
|
// This marks previous connections in the UI as decided with outdated settings.
|
||||||
if activeProfile.layeredProfile != nil {
|
if activeProfile.layeredProfile != nil {
|
||||||
activeProfile.layeredProfile.increaseRevisionCounter(true)
|
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
|
// If the profile is saved externally (eg. via the API), have the
|
||||||
// next one to use it reload the profile from the database.
|
// next one to use it reload the profile from the database.
|
||||||
receivedProfile, err := EnsureProfile(r)
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/safing/portbase/database"
|
|
||||||
"github.com/safing/portbase/database/query"
|
"github.com/safing/portbase/database/query"
|
||||||
"github.com/safing/portbase/database/record"
|
"github.com/safing/portbase/database/record"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
var getProfileLock sync.Mutex
|
var getProfileLock sync.Mutex
|
||||||
|
|
||||||
// GetProfile fetches a profile. This function ensures that the loaded profile
|
// GetLocalProfile fetches a profile. This function ensures that the loaded profile
|
||||||
// is shared among all callers. You must always supply both the scopedID and
|
// is shared among all callers. Always provide all available data points.
|
||||||
// linkedPath parameters whenever available.
|
// Passing an ID without MatchingData is valid, but could lead to inconsistent
|
||||||
func GetProfile(source profileSource, id, linkedPath string, reset bool) ( //nolint:gocognit
|
// data - use with caution.
|
||||||
|
func GetLocalProfile(id string, md MatchingData, createProfileCallback func() *Profile) ( //nolint:gocognit
|
||||||
profile *Profile,
|
profile *Profile,
|
||||||
err error,
|
err error,
|
||||||
) {
|
) {
|
||||||
@@ -27,99 +31,113 @@ func GetProfile(source profileSource, id, linkedPath string, reset bool) ( //nol
|
|||||||
|
|
||||||
var previousVersion *Profile
|
var previousVersion *Profile
|
||||||
|
|
||||||
// Fetch profile depending on the available information.
|
// Get active profile based on the ID, if available.
|
||||||
switch {
|
if id != "" {
|
||||||
case id != "":
|
// Check if there already is an active profile.
|
||||||
scopedID := makeScopedID(source, id)
|
profile = getActiveProfile(makeScopedID(SourceLocal, id))
|
||||||
|
|
||||||
// Get profile via the scoped ID.
|
|
||||||
// Check if there already is an active and not outdated profile.
|
|
||||||
profile = getActiveProfile(scopedID)
|
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
profile.MarkStillActive()
|
// Mark active and return if not outdated.
|
||||||
|
if profile.outdated.IsNotSet() {
|
||||||
if profile.outdated.IsSet() || reset {
|
profile.MarkStillActive()
|
||||||
previousVersion = profile
|
|
||||||
} else {
|
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Get from database.
|
// If outdated, get from database.
|
||||||
if !reset {
|
previousVersion = profile
|
||||||
profile, err = getProfile(scopedID)
|
profile = nil
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
// In some cases, we might need to get a profile directly, without matching data.
|
||||||
if errors.Is(err, database.ErrNotFound) {
|
// This could lead to inconsistent data - use with caution.
|
||||||
err = nil
|
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, err = getProfile(makeScopedID(SourceLocal, id))
|
||||||
profile = getSpecialProfile(id, linkedPath)
|
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 {
|
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.
|
// Initialize and update profile.
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
// 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.
|
// Process profiles are coming directly from the database or are new.
|
||||||
// As we don't use any caching, these will be new objects.
|
// As we don't use any caching, these will be new objects.
|
||||||
|
|
||||||
// Add a layeredProfile to local and network profiles.
|
// Add a layeredProfile.
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local profiles must have a layered profile, create a new one if it
|
// If we are refetching, assign the layered profile from the previous version.
|
||||||
// does not yet exist.
|
// The internal references will be updated when the layered profile checks for updates.
|
||||||
if profile.layeredProfile == nil {
|
if previousVersion != nil && previousVersion.layeredProfile != nil {
|
||||||
profile.layeredProfile = NewLayeredProfile(profile)
|
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.
|
// 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.
|
// 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
|
// 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.
|
// find one, it will create a new profile for the given linked path.
|
||||||
func findProfile(linkedPath string) (profile *Profile, err error) {
|
func findProfile(source profileSource, md MatchingData) (profile *Profile, err error) {
|
||||||
// Search the database for a matching profile.
|
// TODO: Loading every profile from database and parsing it for every new
|
||||||
it, err := profileDB.Query(
|
// process might be quite expensive. Measure impact and possibly improve.
|
||||||
query.New(makeProfileKey(SourceLocal, "")).Where(
|
|
||||||
query.Where("LinkedPath", query.SameAs, linkedPath),
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only wait for the first result, or until the query ends.
|
// Parse and return fingerprints.
|
||||||
r := <-it.Next
|
return parseFingerprints(profile.Fingerprints, profile.LinkedPath)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepProfile(r record.Record) (*Profile, error) {
|
func loadProfile(r record.Record) (*Profile, error) {
|
||||||
// ensure its a profile
|
// ensure its a profile
|
||||||
profile, err := EnsureProfile(r)
|
profile, err := EnsureProfile(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -192,3 +262,50 @@ func prepProfile(r record.Record) (*Profile, error) {
|
|||||||
// return parsed profile
|
// return parsed profile
|
||||||
return profile, nil
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
|
|
||||||
"github.com/safing/portbase/config"
|
"github.com/safing/portbase/config"
|
||||||
"github.com/safing/portbase/database"
|
"github.com/safing/portbase/database"
|
||||||
"github.com/safing/portbase/database/migration"
|
"github.com/safing/portbase/database/migration"
|
||||||
|
"github.com/safing/portbase/database/query"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portmaster/status"
|
"github.com/safing/portmaster/status"
|
||||||
)
|
)
|
||||||
@@ -19,6 +21,11 @@ func registerMigrations() error {
|
|||||||
Version: "v0.7.19",
|
Version: "v0.7.19",
|
||||||
MigrateFunc: migrateNetworkRatingSystem,
|
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
|
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.
|
// Update profiles if necessary.
|
||||||
if layeredProfile.NeedsUpdate() {
|
// TODO: Cannot update as we have too little information.
|
||||||
layeredProfile.Update()
|
// Just return the current state. Previous code:
|
||||||
}
|
// if layeredProfile.NeedsUpdate() {
|
||||||
|
// layeredProfile.Update()
|
||||||
|
// }
|
||||||
|
|
||||||
return layeredProfile, nil
|
return layeredProfile, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
|
|||||||
|
|
||||||
lp := &LayeredProfile{
|
lp := &LayeredProfile{
|
||||||
localProfile: localProfile,
|
localProfile: localProfile,
|
||||||
layers: make([]*Profile, 0, len(localProfile.LinkedProfiles)+1),
|
layers: make([]*Profile, 0, 1),
|
||||||
LayerIDs: make([]string, 0, len(localProfile.LinkedProfiles)+1),
|
LayerIDs: make([]string, 0, 1),
|
||||||
globalValidityFlag: config.NewValidityFlag(),
|
globalValidityFlag: config.NewValidityFlag(),
|
||||||
RevisionCounter: 1,
|
RevisionCounter: 1,
|
||||||
securityLevel: &securityLevelVal,
|
securityLevel: &securityLevelVal,
|
||||||
@@ -246,17 +246,23 @@ func (lp *LayeredProfile) NeedsUpdate() (outdated bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update checks for and replaces any outdated profiles.
|
// 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()
|
lp.Lock()
|
||||||
defer lp.Unlock()
|
defer lp.Unlock()
|
||||||
|
|
||||||
var changed bool
|
var changed bool
|
||||||
for i, layer := range lp.layers {
|
for i, layer := range lp.layers {
|
||||||
if layer.outdated.IsSet() {
|
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.
|
// Update layer.
|
||||||
newLayer, err := GetProfile(layer.Source, layer.ID, layer.LinkedPath, false)
|
changed = true
|
||||||
|
newLayer, err := GetLocalProfile(layer.ID, md, createProfileCallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("profiles: failed to update profile %s: %s", layer.ScopedID(), err)
|
log.Errorf("profiles: failed to update profile %s: %s", layer.ScopedID(), err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -26,11 +25,8 @@ type profileSource string
|
|||||||
|
|
||||||
// Profile Sources.
|
// Profile Sources.
|
||||||
const (
|
const (
|
||||||
SourceLocal profileSource = "local" // local, editable
|
SourceLocal profileSource = "local" // local, editable
|
||||||
SourceSpecial profileSource = "special" // specials (read-only)
|
SourceSpecial profileSource = "special" // specials (read-only)
|
||||||
SourceNetwork profileSource = "network"
|
|
||||||
SourceCommunity profileSource = "community"
|
|
||||||
SourceEnterprise profileSource = "enterprise"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default Action IDs.
|
// Default Action IDs.
|
||||||
@@ -83,11 +79,21 @@ type Profile struct { //nolint:maligned // not worth the effort
|
|||||||
Icon string
|
Icon string
|
||||||
// IconType describes the type of the Icon property.
|
// IconType describes the type of the Icon property.
|
||||||
IconType iconType
|
IconType iconType
|
||||||
// LinkedPath is a filesystem path to the executable this
|
// Deprecated: LinkedPath used to point to the executableis this
|
||||||
// profile was created for.
|
// profile was created for.
|
||||||
|
// Until removed, it will be added to the Fingerprints as an exact path match.
|
||||||
LinkedPath string // constant
|
LinkedPath string // constant
|
||||||
// LinkedProfiles is a list of other profiles
|
// PresentationPath holds the path of an executable that should be used for
|
||||||
LinkedProfiles []string
|
// 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
|
// SecurityLevel is the mininum security level to apply to
|
||||||
// connections made with this profile.
|
// connections made with this profile.
|
||||||
// Note(ppacher): we may deprecate this one as it can easily
|
// Note(ppacher): we may deprecate this one as it can easily
|
||||||
@@ -143,6 +149,11 @@ func (profile *Profile) prepProfile() {
|
|||||||
// prepare configuration
|
// prepare configuration
|
||||||
profile.outdated = abool.New()
|
profile.outdated = abool.New()
|
||||||
profile.lastActive = new(int64)
|
profile.lastActive = new(int64)
|
||||||
|
|
||||||
|
// Migration of LinkedPath to PresentationPath
|
||||||
|
if profile.PresentationPath == "" && profile.LinkedPath != "" {
|
||||||
|
profile.PresentationPath = profile.LinkedPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (profile *Profile) parseConfig() error {
|
func (profile *Profile) parseConfig() error {
|
||||||
@@ -227,29 +238,25 @@ func (profile *Profile) parseConfig() error {
|
|||||||
|
|
||||||
// New returns a new Profile.
|
// New returns a new Profile.
|
||||||
// Optionally, you may supply custom configuration in the flat (key=value) form.
|
// Optionally, you may supply custom configuration in the flat (key=value) form.
|
||||||
func New(
|
func New(profile *Profile) *Profile {
|
||||||
source profileSource,
|
// Create profile if none is given.
|
||||||
id string,
|
if profile == nil {
|
||||||
linkedPath string,
|
profile = &Profile{}
|
||||||
customConfig map[string]interface{},
|
|
||||||
) *Profile {
|
|
||||||
if customConfig != nil {
|
|
||||||
customConfig = config.Expand(customConfig)
|
|
||||||
} else {
|
|
||||||
customConfig = make(map[string]interface{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
profile := &Profile{
|
// Set default and internal values.
|
||||||
ID: id,
|
profile.Created = time.Now().Unix()
|
||||||
Source: source,
|
profile.savedInternally = true
|
||||||
LinkedPath: linkedPath,
|
|
||||||
Created: time.Now().Unix(),
|
// Expand any given configuration.
|
||||||
Config: customConfig,
|
if profile.Config != nil {
|
||||||
savedInternally: true,
|
profile.Config = config.Expand(profile.Config)
|
||||||
|
} else {
|
||||||
|
profile.Config = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate random ID if none is given.
|
// Generate random ID if none is given.
|
||||||
if id == "" {
|
if profile.ID == "" {
|
||||||
profile.ID = utils.RandomUUID("").String()
|
profile.ID = utils.RandomUUID("").String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,106 +422,78 @@ func EnsureProfile(r record.Record) (*Profile, error) {
|
|||||||
return newProfile, nil
|
return newProfile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateMetadata updates meta data fields on the profile and returns whether
|
// 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
|
// the profile was changed.
|
||||||
// operating system, it will start an async worker to fetch that data and save
|
func (profile *Profile) updateMetadata(binaryPath string) (changed bool) {
|
||||||
// the profile afterwards.
|
|
||||||
func (profile *Profile) UpdateMetadata(binaryPath string) (changed bool) {
|
|
||||||
// Check if this is a local profile, else warn and return.
|
// Check if this is a local profile, else warn and return.
|
||||||
if profile.Source != SourceLocal {
|
if profile.Source != SourceLocal {
|
||||||
log.Warningf("tried to update metadata for non-local profile %s", profile.ScopedID())
|
log.Warningf("tried to update metadata for non-local profile %s", profile.ScopedID())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.Lock()
|
// Set PresentationPath if unset.
|
||||||
defer profile.Unlock()
|
if profile.PresentationPath == "" && binaryPath != "" {
|
||||||
|
profile.PresentationPath = binaryPath
|
||||||
// Update special profile and return if it was one.
|
changed = true
|
||||||
if ok, changed := updateSpecialProfileMetadata(profile, binaryPath); ok {
|
|
||||||
return changed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
// Set Name if unset.
|
||||||
filename := filepath.Base(profile.LinkedPath)
|
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
|
// Migrato to Fingerprints.
|
||||||
// case for older profiles.
|
// TODO: Remove in v1.5
|
||||||
if strings.TrimSpace(profile.Name) == "" || profile.Name == filename {
|
if len(profile.Fingerprints) == 0 && profile.LinkedPath != "" {
|
||||||
// Generate a default profile name if does not exist.
|
profile.Fingerprints = []Fingerprint{
|
||||||
profile.Name = osdetail.GenerateBinaryNameFromPath(profile.LinkedPath)
|
{
|
||||||
if profile.Name == filename {
|
Type: FingerprintTypePathID,
|
||||||
// TODO: Theoretically, the generated name could be identical to the
|
Operation: FingerprintOperationEqualsID,
|
||||||
// filename.
|
Value: profile.LinkedPath,
|
||||||
// As a quick fix, append a space to the name.
|
},
|
||||||
profile.Name += " "
|
|
||||||
}
|
}
|
||||||
changed = true
|
changed = true
|
||||||
needsUpdateFromSystem = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If needed, get more/better data from the operating system.
|
// UI Backward Compatibility:
|
||||||
if needsUpdateFromSystem {
|
// Fill LinkedPath with PresentationPath
|
||||||
module.StartWorker("get profile metadata", profile.updateMetadataFromSystem)
|
// TODO: Remove in v1.1
|
||||||
|
if profile.LinkedPath == "" && profile.PresentationPath != "" {
|
||||||
|
profile.LinkedPath = profile.PresentationPath
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return changed
|
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
|
// updateMetadataFromSystem updates the profile metadata with data from the
|
||||||
// operating system and saves it afterwards.
|
// operating system and saves it afterwards.
|
||||||
func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
||||||
|
var changed bool
|
||||||
|
|
||||||
// This function is only valid for local profiles.
|
// This function is only valid for local profiles.
|
||||||
if profile.Source != SourceLocal || profile.LinkedPath == "" {
|
if profile.Source != SourceLocal || profile.PresentationPath == "" {
|
||||||
return fmt.Errorf("tried to update metadata for non-local / non-linked profile %s", profile.ScopedID())
|
return fmt.Errorf("tried to update metadata for non-local or non-path profile %s", profile.ScopedID())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the profile when finished, if needed.
|
// Get binary name from PresentationPath.
|
||||||
save := false
|
newName, err := osdetail.GetBinaryNameFromSystem(profile.PresentationPath)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, osdetail.ErrNotSupported):
|
case errors.Is(err, osdetail.ErrNotSupported):
|
||||||
case errors.Is(err, osdetail.ErrNotFound):
|
case errors.Is(err, osdetail.ErrNotFound):
|
||||||
case errors.Is(err, osdetail.ErrEmptyOutput):
|
case errors.Is(err, osdetail.ErrEmptyOutput):
|
||||||
default:
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -524,25 +503,26 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get filename of linked path for comparison.
|
// Apply new data to profile.
|
||||||
filename := filepath.Base(profile.LinkedPath)
|
func() {
|
||||||
|
// Lock profile for applying metadata.
|
||||||
|
profile.Lock()
|
||||||
|
defer profile.Unlock()
|
||||||
|
|
||||||
// TODO: Theoretically, the generated name from the system could be identical
|
// Apply new name if it changed.
|
||||||
// to the filename. This would mean that the worker is triggered every time
|
if profile.Name != newName {
|
||||||
// the profile is freshly loaded.
|
profile.Name = newName
|
||||||
if newName == filename {
|
changed = true
|
||||||
// As a quick fix, append a space to the name.
|
}
|
||||||
newName += " "
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
// Lock profile for applying metadata.
|
// If anything changed, save the profile.
|
||||||
profile.Lock()
|
// profile.Lock must not be held!
|
||||||
defer profile.Unlock()
|
if changed {
|
||||||
|
err := profile.Save()
|
||||||
// Apply new name if it changed.
|
if err != nil {
|
||||||
if profile.Name != newName {
|
log.Warningf("profile: failed to save %s after metadata update: %s", profile.ScopedID(), err)
|
||||||
profile.Name = newName
|
}
|
||||||
save = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package profile
|
package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/database"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portmaster/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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.`
|
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.
|
// Get new profile name and check if profile is applicable to special handling.
|
||||||
var newProfileName, newDescription string
|
var newProfileName, newDescription string
|
||||||
switch profile.ID {
|
switch profile.ID {
|
||||||
@@ -100,7 +190,7 @@ func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, chan
|
|||||||
newProfileName = PortmasterNotifierProfileName
|
newProfileName = PortmasterNotifierProfileName
|
||||||
newDescription = PortmasterNotifierProfileDescription
|
newDescription = PortmasterNotifierProfileDescription
|
||||||
default:
|
default:
|
||||||
return false, false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update profile name if needed.
|
// Update profile name if needed.
|
||||||
@@ -115,35 +205,52 @@ func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, chan
|
|||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update LinkedPath to new value.
|
// Update PresentationPath to new value.
|
||||||
if profile.LinkedPath != binaryPath {
|
if profile.PresentationPath != binaryPath {
|
||||||
profile.LinkedPath = binaryPath
|
profile.PresentationPath = binaryPath
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSpecialProfile(profileID, linkedPath string) *Profile {
|
func createSpecialProfile(profileID string, path string) *Profile {
|
||||||
switch profileID {
|
switch profileID {
|
||||||
case UnidentifiedProfileID:
|
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:
|
case SystemProfileID:
|
||||||
return New(SourceLocal, SystemProfileID, linkedPath, nil)
|
return New(&Profile{
|
||||||
|
ID: SystemProfileID,
|
||||||
|
Source: SourceLocal,
|
||||||
|
PresentationPath: path,
|
||||||
|
})
|
||||||
|
|
||||||
case SystemResolverProfileID:
|
case SystemResolverProfileID:
|
||||||
systemResolverProfile := New(
|
return New(&Profile{
|
||||||
SourceLocal,
|
ID: SystemResolverProfileID,
|
||||||
SystemResolverProfileID,
|
Source: SourceLocal,
|
||||||
linkedPath,
|
PresentationPath: path,
|
||||||
map[string]interface{}{
|
Config: map[string]interface{}{
|
||||||
// Explicitly setting the default action to "permit" will improve the
|
// Explicitly setting the default action to "permit" will improve the
|
||||||
// user experience for people who set the global default to "prompt".
|
// user experience for people who set the global default to "prompt".
|
||||||
// Resolved domain from the system resolver are checked again when
|
// Resolved domain from the system resolver are checked again when
|
||||||
// attributed to a connection of a regular process. Otherwise, users
|
// attributed to a connection of a regular process. Otherwise, users
|
||||||
// would see two connection prompts for the same domain.
|
// would see two connection prompts for the same domain.
|
||||||
CfgOptionDefaultActionKey: "permit",
|
CfgOptionDefaultActionKey: "permit",
|
||||||
|
// Explicitly allow incoming connections.
|
||||||
|
CfgOptionBlockInboundKey: status.SecurityLevelOff,
|
||||||
// Explicitly allow localhost and answers to multicast protocols that
|
// Explicitly allow localhost and answers to multicast protocols that
|
||||||
// are commonly used by system resolvers.
|
// are commonly used by system resolvers.
|
||||||
// TODO: When the Portmaster gains the ability to attribute multicast
|
// 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/5353", // Allow inbound mDNS requests and multicast replies.
|
||||||
"+ LAN UDP/5355", // Allow inbound LLMNR requests and multicast replies.
|
"+ LAN UDP/5355", // Allow inbound LLMNR requests and multicast replies.
|
||||||
"+ LAN UDP/1900", // Allow inbound SSDP 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
|
// Explicitly disable all filter lists, as these will be checked later
|
||||||
// with the attributed connection. As this is the system resolver, this
|
// 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
|
// the system resolver is used. Users who want to
|
||||||
CfgOptionFilterListsKey: []string{},
|
CfgOptionFilterListsKey: []string{},
|
||||||
},
|
},
|
||||||
)
|
})
|
||||||
return systemResolverProfile
|
|
||||||
|
|
||||||
case PortmasterProfileID:
|
case PortmasterProfileID:
|
||||||
profile := New(SourceLocal, PortmasterProfileID, linkedPath, nil)
|
return New(&Profile{
|
||||||
profile.Internal = true
|
ID: PortmasterProfileID,
|
||||||
return profile
|
Source: SourceLocal,
|
||||||
|
PresentationPath: path,
|
||||||
|
Internal: true,
|
||||||
|
})
|
||||||
|
|
||||||
case PortmasterAppProfileID:
|
case PortmasterAppProfileID:
|
||||||
profile := New(
|
return New(&Profile{
|
||||||
SourceLocal,
|
ID: PortmasterAppProfileID,
|
||||||
PortmasterAppProfileID,
|
Source: SourceLocal,
|
||||||
linkedPath,
|
PresentationPath: path,
|
||||||
map[string]interface{}{
|
Config: map[string]interface{}{
|
||||||
CfgOptionDefaultActionKey: "block",
|
CfgOptionDefaultActionKey: "block",
|
||||||
CfgOptionEndpointsKey: []string{
|
CfgOptionEndpointsKey: []string{
|
||||||
"+ Localhost",
|
"+ Localhost",
|
||||||
"+ .safing.io",
|
"+ .safing.io",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
Internal: true,
|
||||||
profile.Internal = true
|
})
|
||||||
return profile
|
|
||||||
|
|
||||||
case PortmasterNotifierProfileID:
|
case PortmasterNotifierProfileID:
|
||||||
profile := New(
|
return New(&Profile{
|
||||||
SourceLocal,
|
ID: PortmasterNotifierProfileID,
|
||||||
PortmasterNotifierProfileID,
|
Source: SourceLocal,
|
||||||
linkedPath,
|
PresentationPath: path,
|
||||||
map[string]interface{}{
|
Config: map[string]interface{}{
|
||||||
CfgOptionDefaultActionKey: "block",
|
CfgOptionDefaultActionKey: "block",
|
||||||
CfgOptionEndpointsKey: []string{
|
CfgOptionEndpointsKey: []string{
|
||||||
"+ Localhost",
|
"+ Localhost",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
Internal: true,
|
||||||
profile.Internal = true
|
})
|
||||||
return profile
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
@@ -225,7 +333,7 @@ func specialProfileNeedsReset(profile *Profile) bool {
|
|||||||
|
|
||||||
switch profile.ID {
|
switch profile.ID {
|
||||||
case SystemResolverProfileID:
|
case SystemResolverProfileID:
|
||||||
return canBeUpgraded(profile, "20.11.2021")
|
return canBeUpgraded(profile, "21.10.2022")
|
||||||
case PortmasterAppProfileID:
|
case PortmasterAppProfileID:
|
||||||
return canBeUpgraded(profile, "8.9.2021")
|
return canBeUpgraded(profile, "8.9.2021")
|
||||||
default:
|
default:
|
||||||
|
|||||||
Reference in New Issue
Block a user