wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
99
service/process/api.go
Normal file
99
service/process/api.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Name: "Get Process Tag Metadata",
|
||||
Description: "Get information about process tags.",
|
||||
Path: "process/tags",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: handleProcessTagMetadata,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Name: "Get Processes by Profile",
|
||||
Description: "Get all recently active processes using the given profile",
|
||||
Path: "process/list/by-profile/{source:[a-z]+}/{id:[A-z0-9-]+}",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: handleGetProcessesByProfile,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Name: "Get Process Group Leader By PID",
|
||||
Description: "Load a process group leader by a child PID",
|
||||
Path: "process/group-leader/{pid:[0-9]+}",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: handleGetProcessGroupLeader,
|
||||
}); 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
|
||||
}
|
||||
|
||||
func handleGetProcessesByProfile(ar *api.Request) (any, error) {
|
||||
source := ar.URLVars["source"]
|
||||
id := ar.URLVars["id"]
|
||||
if id == "" || source == "" {
|
||||
return nil, api.ErrorWithStatus(fmt.Errorf("missing profile source/id"), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
result := GetProcessesWithProfile(ar.Context(), profile.ProfileSource(source), id, true)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleGetProcessGroupLeader(ar *api.Request) (any, error) {
|
||||
pid, err := strconv.ParseInt(ar.URLVars["pid"], 10, 0)
|
||||
if err != nil {
|
||||
return nil, api.ErrorWithStatus(err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
process, err := GetOrFindProcess(ar.Context(), int(pid))
|
||||
if err != nil {
|
||||
return nil, api.ErrorWithStatus(err, http.StatusInternalServerError)
|
||||
}
|
||||
err = process.FindProcessGroupLeader(ar.Context())
|
||||
switch {
|
||||
case process.Leader() != nil:
|
||||
return process.Leader(), nil
|
||||
case err != nil:
|
||||
return nil, api.ErrorWithStatus(err, http.StatusInternalServerError)
|
||||
default:
|
||||
return nil, api.ErrorWithStatus(errors.New("leader not found"), http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
35
service/process/config.go
Normal file
35
service/process/config.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/config"
|
||||
)
|
||||
|
||||
// Configuration Keys.
|
||||
var (
|
||||
CfgOptionEnableProcessDetectionKey = "core/enableProcessDetection"
|
||||
|
||||
enableProcessDetection config.BoolOption
|
||||
)
|
||||
|
||||
func registerConfiguration() error {
|
||||
// Enable Process Detection
|
||||
// This should be always enabled. Provided as an option to disable in case there are severe problems on a system, or for debugging.
|
||||
err := config.Register(&config.Option{
|
||||
Name: "Process Detection",
|
||||
Key: CfgOptionEnableProcessDetectionKey,
|
||||
Description: "This option enables the attribution of network traffic to processes. Without it, app settings are effectively disabled.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||
DefaultValue: true,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: 528,
|
||||
config.CategoryAnnotation: "Development",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enableProcessDetection = config.Concurrent.GetAsBool(CfgOptionEnableProcessDetectionKey, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
180
service/process/database.go
Normal file
180
service/process/database.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
processInfo "github.com/shirou/gopsutil/process"
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
)
|
||||
|
||||
const processDatabaseNamespace = "network:tree"
|
||||
|
||||
var (
|
||||
processes = make(map[string]*Process)
|
||||
processesLock sync.RWMutex
|
||||
|
||||
dbController *database.Controller
|
||||
dbControllerFlag = abool.NewBool(false)
|
||||
|
||||
deleteProcessesThreshold = 7 * time.Minute
|
||||
)
|
||||
|
||||
// GetProcessFromStorage returns a process from the internal storage.
|
||||
func GetProcessFromStorage(key string) (*Process, bool) {
|
||||
processesLock.RLock()
|
||||
defer processesLock.RUnlock()
|
||||
|
||||
p, ok := processes[key]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
// All returns a copy of all process objects.
|
||||
func All() map[int]*Process {
|
||||
processesLock.RLock()
|
||||
defer processesLock.RUnlock()
|
||||
|
||||
all := make(map[int]*Process)
|
||||
for _, proc := range processes {
|
||||
all[proc.Pid] = proc
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
// GetProcessesWithProfile returns all processes that use the given profile.
|
||||
// If preferProcessGroupLeader is set, it returns the process group leader instead, if available.
|
||||
func GetProcessesWithProfile(ctx context.Context, profileSource profile.ProfileSource, profileID string, preferProcessGroupLeader bool) []*Process {
|
||||
log.Tracer(ctx).Debugf("process: searching for processes belonging to %s", profile.MakeScopedID(profileSource, profileID))
|
||||
|
||||
// Get all processes that match the given profile.
|
||||
procs := make([]*Process, 0, 8)
|
||||
for _, p := range All() {
|
||||
lp := p.profile.LocalProfile()
|
||||
if lp != nil && lp.Source == profileSource && lp.ID == profileID {
|
||||
if preferProcessGroupLeader && p.Leader() != nil {
|
||||
procs = append(procs, p.Leader())
|
||||
} else {
|
||||
procs = append(procs, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort and compact.
|
||||
slices.SortFunc[[]*Process, *Process](procs, func(a, b *Process) int {
|
||||
return strings.Compare(a.processKey, b.processKey)
|
||||
})
|
||||
slices.CompactFunc[[]*Process, *Process](procs, func(a, b *Process) bool {
|
||||
return a.processKey == b.processKey
|
||||
})
|
||||
|
||||
return procs
|
||||
}
|
||||
|
||||
// Save saves the process to the internal state and pushes an update.
|
||||
func (p *Process) Save() {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
p.UpdateMeta()
|
||||
|
||||
if p.processKey == "" {
|
||||
p.processKey = getProcessKey(int32(p.Pid), p.CreatedAt)
|
||||
}
|
||||
|
||||
if !p.KeyIsSet() {
|
||||
// set key
|
||||
p.SetKey(fmt.Sprintf("%s/%s", processDatabaseNamespace, p.processKey))
|
||||
|
||||
// save
|
||||
processesLock.Lock()
|
||||
processes[p.processKey] = p
|
||||
processesLock.Unlock()
|
||||
}
|
||||
|
||||
if dbControllerFlag.IsSet() {
|
||||
dbController.PushUpdate(p)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deletes a process from the storage and propagates the change.
|
||||
func (p *Process) Delete() {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
// delete from internal storage
|
||||
processesLock.Lock()
|
||||
delete(processes, p.processKey)
|
||||
processesLock.Unlock()
|
||||
|
||||
// propagate delete
|
||||
p.Meta().Delete()
|
||||
if dbControllerFlag.IsSet() {
|
||||
dbController.PushUpdate(p)
|
||||
}
|
||||
|
||||
// TODO: maybe mark the assigned profiles as no longer needed?
|
||||
}
|
||||
|
||||
// CleanProcessStorage cleans the storage from old processes.
|
||||
func CleanProcessStorage(activePIDs map[int]struct{}) {
|
||||
// add system table of processes
|
||||
pids, err := processInfo.Pids()
|
||||
if err != nil {
|
||||
log.Warningf("process: failed to get list of active PIDs: %s", err)
|
||||
} else {
|
||||
for _, pid := range pids {
|
||||
activePIDs[int(pid)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
processesCopy := All()
|
||||
threshold := time.Now().Add(-deleteProcessesThreshold).Unix()
|
||||
|
||||
// clean primary processes
|
||||
for _, p := range processesCopy {
|
||||
// The PID of a process does not change.
|
||||
|
||||
// Check if this is a special process.
|
||||
switch p.Pid {
|
||||
case UnidentifiedProcessID, UnsolicitedProcessID, SystemProcessID:
|
||||
p.profile.MarkStillActive()
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if process is active.
|
||||
_, active := activePIDs[p.Pid]
|
||||
if active {
|
||||
p.profile.MarkStillActive()
|
||||
continue
|
||||
}
|
||||
|
||||
// Process is inactive, start deletion process
|
||||
lastSeen := p.GetLastSeen()
|
||||
switch {
|
||||
case lastSeen == 0:
|
||||
// add last seen timestamp
|
||||
p.SetLastSeen(time.Now().Unix())
|
||||
case lastSeen > threshold:
|
||||
// within keep period
|
||||
default:
|
||||
// delete now
|
||||
p.Delete()
|
||||
log.Tracef("process: cleaned %s", p.DatabaseKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetDBController sets the database controller and allows the package to push database updates on a save. It must be set by the package that registers the "network" database.
|
||||
func SetDBController(controller *database.Controller) {
|
||||
dbController = controller
|
||||
dbControllerFlag.Set()
|
||||
}
|
||||
3
service/process/doc.go
Normal file
3
service/process/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package process fetches process and socket information from the operating system.
|
||||
// It can find the process owning a network connection.
|
||||
package process
|
||||
41
service/process/executable.go
Normal file
41
service/process/executable.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/hex"
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// GetExecHash returns the hash of the executable with the given algorithm.
|
||||
func (p *Process) GetExecHash(algorithm string) (string, error) {
|
||||
sum, ok := p.ExecHashes[algorithm]
|
||||
if ok {
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
var hasher hash.Hash
|
||||
switch algorithm {
|
||||
case "md5":
|
||||
hasher = crypto.MD5.New()
|
||||
case "sha1":
|
||||
hasher = crypto.SHA1.New()
|
||||
case "sha256":
|
||||
hasher = crypto.SHA256.New()
|
||||
}
|
||||
|
||||
file, err := os.Open(p.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = io.Copy(hasher, file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sum = hex.EncodeToString(hasher.Sum(nil))
|
||||
p.ExecHashes[algorithm] = sum
|
||||
return sum, nil
|
||||
}
|
||||
150
service/process/find.go
Normal file
150
service/process/find.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/service/network/state"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
)
|
||||
|
||||
// GetProcessWithProfile returns the process, including the profile.
|
||||
// Always returns valid data.
|
||||
// Errors are logged and returned for information or special handling purposes.
|
||||
func GetProcessWithProfile(ctx context.Context, pid int) (process *Process, err error) {
|
||||
if !enableProcessDetection() {
|
||||
log.Tracer(ctx).Tracef("process: process detection disabled")
|
||||
return GetUnidentifiedProcess(ctx), nil
|
||||
}
|
||||
|
||||
process, err = GetOrFindProcess(ctx, pid)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Debugf("process: failed to find process with PID: %s", err)
|
||||
return GetUnidentifiedProcess(ctx), err
|
||||
}
|
||||
|
||||
// Get process group leader, which is the process "nearest" to the user and
|
||||
// will have more/better information for finding names ans icons, for example.
|
||||
err = process.FindProcessGroupLeader(ctx)
|
||||
if err != nil {
|
||||
log.Warningf("process: failed to get process group leader for %s: %s", process, err)
|
||||
}
|
||||
|
||||
changed, err := process.GetProfile(ctx)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
|
||||
}
|
||||
|
||||
if changed {
|
||||
process.Save()
|
||||
}
|
||||
|
||||
return process, nil
|
||||
}
|
||||
|
||||
// GetPidOfConnection returns the PID of the process that owns the described connection.
|
||||
// Always returns valid data.
|
||||
// Errors are logged and returned for information or special handling purposes.
|
||||
func GetPidOfConnection(ctx context.Context, pktInfo *packet.Info) (pid int, connInbound bool, err error) {
|
||||
if !enableProcessDetection() {
|
||||
return UnidentifiedProcessID, pktInfo.Inbound, nil
|
||||
}
|
||||
|
||||
// Use fast search for inbound packets, as the listening socket should
|
||||
// already be there for a while now.
|
||||
fastSearch := pktInfo.Inbound
|
||||
connInbound = pktInfo.Inbound
|
||||
|
||||
// Check if we need to get the PID.
|
||||
if pktInfo.PID == UndefinedProcessID {
|
||||
log.Tracer(ctx).Tracef("process: getting pid from system network state")
|
||||
pid, connInbound, err = state.Lookup(pktInfo, fastSearch)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to find PID of connection: %w", err)
|
||||
log.Tracer(ctx).Tracef("process: %s", err)
|
||||
pid = UndefinedProcessID
|
||||
}
|
||||
} else {
|
||||
log.Tracer(ctx).Tracef("process: pid already set in packet (by ebpf or kext)")
|
||||
pid = pktInfo.PID
|
||||
}
|
||||
|
||||
// Fallback to special profiles if PID could not be found.
|
||||
if pid == UndefinedProcessID {
|
||||
if connInbound && !netutils.ClassifyIP(pktInfo.Dst).IsLocalhost() {
|
||||
pid = UnsolicitedProcessID
|
||||
} else {
|
||||
pid = UnidentifiedProcessID
|
||||
}
|
||||
}
|
||||
|
||||
return pid, connInbound, err
|
||||
}
|
||||
|
||||
// GetNetworkHost returns a *Process that represents a host on the network.
|
||||
func GetNetworkHost(ctx context.Context, remoteIP net.IP) (process *Process, err error) { //nolint:interfacer
|
||||
now := time.Now().Unix()
|
||||
networkHost := &Process{
|
||||
Name: fmt.Sprintf("Device at %s", remoteIP),
|
||||
UserName: "N/A",
|
||||
UserID: NetworkHostProcessID,
|
||||
Pid: NetworkHostProcessID,
|
||||
ParentPid: NetworkHostProcessID,
|
||||
Tags: []profile.Tag{
|
||||
{
|
||||
Key: "ip",
|
||||
Value: remoteIP.String(),
|
||||
},
|
||||
},
|
||||
FirstSeen: now,
|
||||
LastSeen: now,
|
||||
}
|
||||
|
||||
// Get the (linked) local profile.
|
||||
networkHostProfile, err := profile.GetLocalProfile("", networkHost.MatchingData(), networkHost.CreateProfileCallback)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Assign profile to process.
|
||||
networkHost.PrimaryProfileID = networkHostProfile.ScopedID()
|
||||
networkHost.profile = networkHostProfile.LayeredProfile()
|
||||
|
||||
return networkHost, nil
|
||||
}
|
||||
|
||||
// GetProcessByRequestOrigin returns the process that initiated the API request ar.
|
||||
func GetProcessByRequestOrigin(ar *api.Request) (*Process, error) {
|
||||
// get remote IP/Port
|
||||
remoteIP, remotePort, err := netutils.ParseIPPort(ar.RemoteAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get remote IP/Port: %w", err)
|
||||
}
|
||||
|
||||
pkt := &packet.Info{
|
||||
Inbound: false, // outbound as we are looking for the process of the source address
|
||||
Version: packet.IPv4,
|
||||
Protocol: packet.TCP,
|
||||
Src: remoteIP, // source as in the process we are looking for
|
||||
SrcPort: remotePort, // source as in the process we are looking for
|
||||
PID: UndefinedProcessID,
|
||||
}
|
||||
|
||||
pid, _, err := GetPidOfConnection(ar.Context(), pkt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proc, err := GetProcessWithProfile(ar.Context(), pid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return proc, nil
|
||||
}
|
||||
34
service/process/module.go
Normal file
34
service/process/module.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
updatesPath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("processes", prep, start, nil, "profiles", "updates")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
return registerConfiguration()
|
||||
}
|
||||
|
||||
func start() error {
|
||||
updatesPath = updates.RootPath()
|
||||
if updatesPath != "" {
|
||||
updatesPath += string(os.PathSeparator)
|
||||
}
|
||||
|
||||
if err := registerAPIEndpoints(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
11
service/process/module_test.go
Normal file
11
service/process/module_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/service/core/pmtesting"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pmtesting.TestMain(m, module)
|
||||
}
|
||||
391
service/process/process.go
Normal file
391
service/process/process.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
processInfo "github.com/shirou/gopsutil/process"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
)
|
||||
|
||||
const onLinux = runtime.GOOS == "linux"
|
||||
|
||||
var getProcessSingleInflight singleflight.Group
|
||||
|
||||
// A Process represents a process running on the operating system.
|
||||
type Process struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
// Process attributes.
|
||||
// Don't change; safe for concurrent access.
|
||||
|
||||
Name string
|
||||
UserID int
|
||||
UserName string
|
||||
UserHome string
|
||||
|
||||
Pid int
|
||||
CreatedAt int64
|
||||
|
||||
ParentPid int
|
||||
ParentCreatedAt int64
|
||||
|
||||
LeaderPid int
|
||||
leader *Process
|
||||
|
||||
Path string
|
||||
ExecName string
|
||||
Cwd string
|
||||
CmdLine string
|
||||
FirstArg string
|
||||
Env map[string]string
|
||||
|
||||
// unique process identifier ("Pid-CreatedAt")
|
||||
processKey string
|
||||
|
||||
// Profile attributes.
|
||||
// Once set, these don't change; safe for concurrent access.
|
||||
|
||||
// Tags holds extended information about the (virtual) process, which is used
|
||||
// to find a profile.
|
||||
Tags []profile.Tag
|
||||
// MatchingPath holds an alternative binary path that can be used to find a
|
||||
// profile.
|
||||
MatchingPath string
|
||||
|
||||
// PrimaryProfileID holds the scoped ID of the primary profile.
|
||||
PrimaryProfileID string
|
||||
// profile holds the layered profile based on the primary profile.
|
||||
profile *profile.LayeredProfile
|
||||
|
||||
// Mutable attributes.
|
||||
|
||||
FirstSeen int64
|
||||
LastSeen int64
|
||||
Error string // Cache errors
|
||||
|
||||
ExecHashes map[string]string
|
||||
}
|
||||
|
||||
// GetTag returns the process tag with the given ID.
|
||||
func (p *Process) GetTag(tagID string) (profile.Tag, bool) {
|
||||
for _, t := range p.Tags {
|
||||
if t.Key == tagID {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return profile.Tag{}, false
|
||||
}
|
||||
|
||||
// Profile returns the assigned layered profile.
|
||||
func (p *Process) Profile() *profile.LayeredProfile {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.profile
|
||||
}
|
||||
|
||||
// Leader returns the process group leader that is attached to the process.
|
||||
// This will not trigger a new search for the process group leader, it only
|
||||
// returns existing data.
|
||||
func (p *Process) Leader() *Process {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
return p.leader
|
||||
}
|
||||
|
||||
// IsIdentified returns whether the process has been identified or if it
|
||||
// represents some kind of unidentified process.
|
||||
func (p *Process) IsIdentified() bool {
|
||||
// Check if process exists.
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for special PIDs.
|
||||
switch p.Pid {
|
||||
case UndefinedProcessID:
|
||||
return false
|
||||
case UnidentifiedProcessID:
|
||||
return false
|
||||
case UnsolicitedProcessID:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// HasValidPID returns whether the process has valid PID of an actual process.
|
||||
func (p *Process) HasValidPID() bool {
|
||||
// Check if process exists.
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.Pid >= 0
|
||||
}
|
||||
|
||||
// Equal returns if the two processes are both identified and have the same PID.
|
||||
func (p *Process) Equal(other *Process) bool {
|
||||
return p.IsIdentified() && other.IsIdentified() && p.Pid == other.Pid
|
||||
}
|
||||
|
||||
const systemResolverScopedID = string(profile.SourceLocal) + "/" + profile.SystemResolverProfileID
|
||||
|
||||
// IsSystemResolver is a shortcut to check if the process is or belongs to the
|
||||
// system resolver and needs special handling.
|
||||
func (p *Process) IsSystemResolver() bool {
|
||||
// Check if process exists.
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check ID.
|
||||
return p.PrimaryProfileID == systemResolverScopedID
|
||||
}
|
||||
|
||||
// GetLastSeen returns the unix timestamp when the process was last seen.
|
||||
func (p *Process) GetLastSeen() int64 {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
return p.LastSeen
|
||||
}
|
||||
|
||||
// SetLastSeen sets the unix timestamp when the process was last seen.
|
||||
func (p *Process) SetLastSeen(lastSeen int64) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
p.LastSeen = lastSeen
|
||||
}
|
||||
|
||||
// String returns a string representation of process.
|
||||
func (p *Process) String() string {
|
||||
if p == nil {
|
||||
return "?"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%s:%d", p.UserName, p.Path, p.Pid)
|
||||
}
|
||||
|
||||
// GetOrFindProcess returns the process for the given PID.
|
||||
func GetOrFindProcess(ctx context.Context, pid int) (*Process, error) {
|
||||
log.Tracer(ctx).Tracef("process: getting process for PID %d", pid)
|
||||
|
||||
// Check for special processes
|
||||
switch pid {
|
||||
case UnidentifiedProcessID:
|
||||
return GetUnidentifiedProcess(ctx), nil
|
||||
case UnsolicitedProcessID:
|
||||
return GetUnsolicitedProcess(ctx), nil
|
||||
case SystemProcessID:
|
||||
return GetSystemProcess(ctx), nil
|
||||
}
|
||||
|
||||
// Get pid and creation time for identification.
|
||||
pInfo, err := processInfo.NewProcessWithContext(ctx, int32(pid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createdAt, err := pInfo.CreateTimeWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := getProcessKey(int32(pid), createdAt)
|
||||
|
||||
// Load process and make sure it is only loaded once.
|
||||
p, err, _ := getProcessSingleInflight.Do(key, func() (interface{}, error) {
|
||||
return loadProcess(ctx, key, pInfo)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p == nil {
|
||||
return nil, errors.New("process getter returned nil")
|
||||
}
|
||||
|
||||
return p.(*Process), nil // nolint:forcetypeassert // Can only be a *Process.
|
||||
}
|
||||
|
||||
func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*Process, error) {
|
||||
// Check if we already have the process.
|
||||
process, ok := GetProcessFromStorage(key)
|
||||
if ok {
|
||||
return process, nil
|
||||
}
|
||||
|
||||
// Create new a process object.
|
||||
process = &Process{
|
||||
Pid: int(pInfo.Pid),
|
||||
FirstSeen: time.Now().Unix(),
|
||||
processKey: key,
|
||||
}
|
||||
|
||||
// Get creation time of process. (The value should be cached by the library.)
|
||||
var err error
|
||||
process.CreatedAt, err = pInfo.CreateTimeWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// UID
|
||||
// TODO: implemented for windows
|
||||
if onLinux {
|
||||
var uids []int32
|
||||
uids, err = pInfo.UidsWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get UID for p%d: %w", pInfo.Pid, err)
|
||||
}
|
||||
process.UserID = int(uids[0])
|
||||
}
|
||||
|
||||
// Username
|
||||
process.UserName, err = pInfo.UsernameWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("process: failed to get Username for p%d: %w", pInfo.Pid, err)
|
||||
}
|
||||
|
||||
// TODO: User Home
|
||||
// new.UserHome, err =
|
||||
|
||||
// Parent process ID
|
||||
ppid, err := pInfo.PpidWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get PPID for p%d: %w", pInfo.Pid, err)
|
||||
}
|
||||
process.ParentPid = int(ppid)
|
||||
|
||||
// Parent created time
|
||||
parentPInfo, err := processInfo.NewProcessWithContext(ctx, ppid)
|
||||
if err == nil {
|
||||
parentCreatedAt, err := parentPInfo.CreateTimeWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
process.ParentCreatedAt = parentCreatedAt
|
||||
}
|
||||
|
||||
// Leader process ID
|
||||
// Get process group ID to find group leader, which is the process "nearest"
|
||||
// to the user and will have more/better information for finding names and
|
||||
// icons, for example.
|
||||
leaderPid, err := GetProcessGroupID(ctx, process.Pid)
|
||||
if err != nil {
|
||||
// Fail gracefully.
|
||||
log.Warningf("process: failed to get process group ID for p%d: %s", process.Pid, err)
|
||||
process.LeaderPid = UndefinedProcessID
|
||||
} else {
|
||||
process.LeaderPid = leaderPid
|
||||
}
|
||||
|
||||
// Path
|
||||
process.Path, err = pInfo.ExeWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Path for p%d: %w", pInfo.Pid, err)
|
||||
}
|
||||
// remove linux " (deleted)" suffix for deleted files
|
||||
if onLinux {
|
||||
process.Path = strings.TrimSuffix(process.Path, " (deleted)")
|
||||
}
|
||||
// Executable Name
|
||||
_, process.ExecName = filepath.Split(process.Path)
|
||||
|
||||
// Current working directory
|
||||
// not yet implemented for windows
|
||||
if runtime.GOOS != "windows" {
|
||||
process.Cwd, err = pInfo.CwdWithContext(ctx)
|
||||
if err != nil {
|
||||
log.Warningf("process: failed to get current working dir (PID %d): %s", pInfo.Pid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Command line arguments
|
||||
process.CmdLine, err = pInfo.CmdlineWithContext(ctx)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Warningf("process: failed to get cmdline (PID %d): %s", pInfo.Pid, err)
|
||||
}
|
||||
|
||||
// Name
|
||||
process.Name, err = pInfo.NameWithContext(ctx)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Warningf("process: failed to get process name (PID %d): %s", pInfo.Pid, err)
|
||||
}
|
||||
if process.Name == "" {
|
||||
process.Name = process.ExecName
|
||||
}
|
||||
|
||||
// Get all environment variables
|
||||
env, err := pInfo.EnvironWithContext(ctx)
|
||||
if err == nil {
|
||||
// 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], `'"`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Tracer(ctx).Warningf("process: failed to get the process environment (PID %d): %s", pInfo.Pid, err)
|
||||
}
|
||||
|
||||
// Add process tags.
|
||||
process.addTags()
|
||||
if len(process.Tags) > 0 {
|
||||
log.Tracer(ctx).Debugf("profile: added tags: %+v", process.Tags)
|
||||
}
|
||||
|
||||
process.Save()
|
||||
return process, nil
|
||||
}
|
||||
|
||||
// GetKey returns the key that is used internally to identify the process.
|
||||
// The key consists of the PID and the start time of the process as reported by
|
||||
// the system.
|
||||
func (p *Process) GetKey() string {
|
||||
return p.processKey
|
||||
}
|
||||
|
||||
// Builds a unique identifier for a processes.
|
||||
func getProcessKey(pid int32, createdTime int64) string {
|
||||
return fmt.Sprintf("%d-%d", pid, createdTime)
|
||||
}
|
||||
|
||||
// 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 }
|
||||
23
service/process/process_default.go
Normal file
23
service/process/process_default.go
Normal file
@@ -0,0 +1,23 @@
|
||||
//go:build !windows && !linux
|
||||
// +build !windows,!linux
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// SystemProcessID is the PID of the System/Kernel itself.
|
||||
const SystemProcessID = 0
|
||||
|
||||
// GetProcessGroupLeader returns the process that leads the process group.
|
||||
// Returns nil on unsupported platforms.
|
||||
func (p *Process) FindProcessGroupLeader(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProcessGroupID returns the process group ID of the given PID.
|
||||
// Returns undefined process ID on unsupported platforms.
|
||||
func GetProcessGroupID(ctx context.Context, pid int) (int, error) {
|
||||
return UndefinedProcessID, nil
|
||||
}
|
||||
96
service/process/process_linux.go
Normal file
96
service/process/process_linux.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// SystemProcessID is the PID of the System/Kernel itself.
|
||||
SystemProcessID = 0
|
||||
|
||||
// SystemInitID is the PID of the system init process.
|
||||
SystemInitID = 1
|
||||
)
|
||||
|
||||
// FindProcessGroupLeader returns the process that leads the process group.
|
||||
// Returns nil when process ID is not valid (or virtual).
|
||||
// If the process group leader is found, it is set on the process.
|
||||
// If that process does not exist anymore, then the highest existing parent process is returned.
|
||||
// If an error occurs, the best match is set.
|
||||
func (p *Process) FindProcessGroupLeader(ctx context.Context) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
// Return the leader if we already have it.
|
||||
if p.leader != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if we have the process group leader PID.
|
||||
if p.LeaderPid == UndefinedProcessID {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return nil if we already are the leader.
|
||||
if p.LeaderPid == p.Pid {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get process leader process.
|
||||
leader, err := GetOrFindProcess(ctx, p.LeaderPid)
|
||||
if err == nil {
|
||||
p.leader = leader
|
||||
log.Tracer(ctx).Debugf("process: found process leader of %d: pid=%d pgid=%d", p.Pid, leader.Pid, leader.LeaderPid)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we can't get the process leader process, it has likely already exited.
|
||||
// In that case, find the highest existing parent process within the process group.
|
||||
var (
|
||||
nextParentPid = p.ParentPid
|
||||
lastParent *Process
|
||||
)
|
||||
for {
|
||||
// Get next parent.
|
||||
parent, err := GetOrFindProcess(ctx, nextParentPid)
|
||||
if err != nil {
|
||||
p.leader = lastParent
|
||||
return fmt.Errorf("failed to find parent %d: %w", nextParentPid, err)
|
||||
}
|
||||
|
||||
// Check if we are ready to return.
|
||||
switch {
|
||||
case parent.Pid == p.LeaderPid:
|
||||
// Found the process group leader!
|
||||
p.leader = parent
|
||||
return nil
|
||||
|
||||
case parent.LeaderPid != p.LeaderPid:
|
||||
// We are leaving the process group. Return the previous parent.
|
||||
p.leader = lastParent
|
||||
log.Tracer(ctx).Debugf("process: found process leader (highest parent) of %d: pid=%d pgid=%d", p.Pid, parent.Pid, parent.LeaderPid)
|
||||
return nil
|
||||
|
||||
case parent.ParentPid == SystemProcessID,
|
||||
parent.ParentPid == SystemInitID:
|
||||
// Next parent is system or init.
|
||||
// Use current parent.
|
||||
p.leader = parent
|
||||
log.Tracer(ctx).Debugf("process: found process leader (highest parent) of %d: pid=%d pgid=%d", p.Pid, parent.Pid, parent.LeaderPid)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check next parent.
|
||||
lastParent = parent
|
||||
nextParentPid = parent.ParentPid
|
||||
}
|
||||
}
|
||||
|
||||
// GetProcessGroupID returns the process group ID of the given PID.
|
||||
func GetProcessGroupID(ctx context.Context, pid int) (int, error) {
|
||||
return syscall.Getpgid(pid)
|
||||
}
|
||||
21
service/process/process_windows.go
Normal file
21
service/process/process_windows.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// SystemProcessID is the PID of the System/Kernel itself.
|
||||
const SystemProcessID = 4
|
||||
|
||||
// GetProcessGroupLeader returns the process that leads the process group.
|
||||
// Returns nil on Windows, as it does not have process groups.
|
||||
func (p *Process) FindProcessGroupLeader(ctx context.Context) error {
|
||||
// TODO: Get "main" process of process job object.
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProcessGroupID returns the process group ID of the given PID.
|
||||
// Returns the undefined process ID on Windows, as it does not have process groups.
|
||||
func GetProcessGroupID(ctx context.Context, pid int) (int, error) {
|
||||
return UndefinedProcessID, nil
|
||||
}
|
||||
116
service/process/profile.go
Normal file
116
service/process/profile.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
)
|
||||
|
||||
var ownPID = os.Getpid()
|
||||
|
||||
// GetProfile finds and assigns a profile set to the process.
|
||||
func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
// Check if profile is already loaded.
|
||||
if p.profile != nil {
|
||||
log.Tracer(ctx).Trace("process: profile already loaded")
|
||||
return
|
||||
}
|
||||
|
||||
// If not, continue with loading the profile.
|
||||
log.Tracer(ctx).Trace("process: loading profile")
|
||||
|
||||
// Get special or regular profile.
|
||||
localProfile, err := profile.GetLocalProfile(p.getSpecialProfileID(), 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
|
||||
}
|
||||
|
||||
// RefetchProfile removes the profile and finds and assigns a new profile.
|
||||
func (p *Process) RefetchProfile(ctx context.Context) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
// Get special or regular profile.
|
||||
localProfile, err := profile.GetLocalProfile(p.getSpecialProfileID(), p.MatchingData(), p.CreateProfileCallback)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find profile: %w", err)
|
||||
}
|
||||
|
||||
// Assign profile to process.
|
||||
p.PrimaryProfileID = localProfile.ScopedID()
|
||||
p.profile = localProfile.LayeredProfile()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSpecialProfileID returns the special profile ID for the process, if any.
|
||||
func (p *Process) getSpecialProfileID() (specialProfileID string) {
|
||||
// Check if we need a special profile.
|
||||
switch p.Pid {
|
||||
case UnidentifiedProcessID:
|
||||
specialProfileID = profile.UnidentifiedProfileID
|
||||
case UnsolicitedProcessID:
|
||||
specialProfileID = profile.UnsolicitedProfileID
|
||||
case SystemProcessID:
|
||||
specialProfileID = profile.SystemProfileID
|
||||
case ownPID:
|
||||
specialProfileID = profile.PortmasterProfileID
|
||||
default:
|
||||
// Check if this is another Portmaster component.
|
||||
if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) {
|
||||
switch {
|
||||
case strings.Contains(p.Path, "portmaster-app"):
|
||||
specialProfileID = profile.PortmasterAppProfileID
|
||||
case strings.Contains(p.Path, "portmaster-notifier"):
|
||||
specialProfileID = profile.PortmasterNotifierProfileID
|
||||
default:
|
||||
// Unexpected binary from within the Portmaster updates directpry.
|
||||
log.Warningf("process: unexpected binary in the updates directory: %s", p.Path)
|
||||
// TODO: Assign a fully restricted profile in the future when we are
|
||||
// sure that we won't kill any of our own things.
|
||||
}
|
||||
}
|
||||
// Check if this is the system resolver.
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// Depending on the OS version System32 may be capitalized or not.
|
||||
if (p.Path == `C:\Windows\System32\svchost.exe` ||
|
||||
p.Path == `C:\Windows\system32\svchost.exe`) &&
|
||||
// This comes from the windows tasklist command and should be pretty consistent.
|
||||
(profile.KeyAndValueInTags(p.Tags, "svchost", "Dnscache") ||
|
||||
// As an alternative in case of failure, we try to match the svchost.exe service parameter.
|
||||
strings.Contains(p.CmdLine, "-s Dnscache")) {
|
||||
specialProfileID = profile.SystemResolverProfileID
|
||||
}
|
||||
case "linux":
|
||||
switch p.Path {
|
||||
case "/lib/systemd/systemd-resolved",
|
||||
"/usr/lib/systemd/systemd-resolved",
|
||||
"/lib64/systemd/systemd-resolved",
|
||||
"/usr/lib64/systemd/systemd-resolved",
|
||||
"/usr/bin/nscd",
|
||||
"/usr/sbin/nscd",
|
||||
"/usr/bin/dnsmasq",
|
||||
"/usr/sbin/dnsmasq":
|
||||
specialProfileID = profile.SystemResolverProfileID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return specialProfileID
|
||||
}
|
||||
110
service/process/special.go
Normal file
110
service/process/special.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/network/socket"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
)
|
||||
|
||||
const (
|
||||
// UndefinedProcessID is not used by any (virtual) process and signifies that
|
||||
// the PID is unset.
|
||||
UndefinedProcessID = -1
|
||||
|
||||
// UnidentifiedProcessID is the PID used for outgoing connections that could
|
||||
// not be attributed to a PID for any reason.
|
||||
UnidentifiedProcessID = -2
|
||||
|
||||
// UnsolicitedProcessID is the PID used for incoming connections that could
|
||||
// not be attributed to a PID for any reason.
|
||||
UnsolicitedProcessID = -3
|
||||
|
||||
// NetworkHostProcessID is the PID used for requests served to the network.
|
||||
NetworkHostProcessID = -255
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Check required matching values.
|
||||
if UndefinedProcessID != socket.UndefinedProcessID {
|
||||
panic("UndefinedProcessID does not match socket.UndefinedProcessID")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// unidentifiedProcess is used for non-attributed outgoing connections.
|
||||
unidentifiedProcess = &Process{
|
||||
UserID: UnidentifiedProcessID,
|
||||
UserName: "Unknown",
|
||||
Pid: UnidentifiedProcessID,
|
||||
ParentPid: UnidentifiedProcessID,
|
||||
Name: profile.UnidentifiedProfileName,
|
||||
processKey: getProcessKey(UnidentifiedProcessID, 0),
|
||||
}
|
||||
|
||||
// unsolicitedProcess is used for non-attributed incoming connections.
|
||||
unsolicitedProcess = &Process{
|
||||
UserID: UnsolicitedProcessID,
|
||||
UserName: "Unknown",
|
||||
Pid: UnsolicitedProcessID,
|
||||
ParentPid: UnsolicitedProcessID,
|
||||
Name: profile.UnsolicitedProfileName,
|
||||
processKey: getProcessKey(UnsolicitedProcessID, 0),
|
||||
}
|
||||
|
||||
// systemProcess is used to represent the Kernel.
|
||||
systemProcess = &Process{
|
||||
UserID: SystemProcessID,
|
||||
UserName: "Kernel",
|
||||
Pid: SystemProcessID,
|
||||
ParentPid: SystemProcessID,
|
||||
Name: profile.SystemProfileName,
|
||||
processKey: getProcessKey(SystemProcessID, 0),
|
||||
}
|
||||
|
||||
getSpecialProcessSingleInflight singleflight.Group
|
||||
)
|
||||
|
||||
// GetUnidentifiedProcess returns the special process assigned to non-attributed outgoing connections.
|
||||
func GetUnidentifiedProcess(ctx context.Context) *Process {
|
||||
return getSpecialProcess(ctx, unidentifiedProcess)
|
||||
}
|
||||
|
||||
// GetUnsolicitedProcess returns the special process assigned to non-attributed incoming connections.
|
||||
func GetUnsolicitedProcess(ctx context.Context) *Process {
|
||||
return getSpecialProcess(ctx, unsolicitedProcess)
|
||||
}
|
||||
|
||||
// GetSystemProcess returns the special process used for the Kernel.
|
||||
func GetSystemProcess(ctx context.Context) *Process {
|
||||
return getSpecialProcess(ctx, systemProcess)
|
||||
}
|
||||
|
||||
func getSpecialProcess(ctx context.Context, template *Process) *Process {
|
||||
p, _, _ := getSpecialProcessSingleInflight.Do(template.processKey, func() (interface{}, error) {
|
||||
// Check if we have already loaded the special process.
|
||||
process, ok := GetProcessFromStorage(template.processKey)
|
||||
if ok {
|
||||
return process, nil
|
||||
}
|
||||
|
||||
// Create new process from template
|
||||
process = template
|
||||
process.FirstSeen = time.Now().Unix()
|
||||
|
||||
// Get profile.
|
||||
_, err := process.GetProfile(ctx)
|
||||
if err != nil {
|
||||
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
|
||||
}
|
||||
|
||||
// Save process to storage.
|
||||
process.Save()
|
||||
return process, nil
|
||||
})
|
||||
return p.(*Process) // nolint:forcetypeassert // Can only be a *Process.
|
||||
}
|
||||
80
service/process/tags.go
Normal file
80
service/process/tags.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portmaster/service/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
|
||||
}
|
||||
186
service/process/tags/appimage_unix.go
Normal file
186
service/process/tags/appimage_unix.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/profile/binmeta"
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := process.RegisterTagHandler(new(AppImageHandler))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
appImageName = "AppImage"
|
||||
appImagePathTagKey = "app-image-path"
|
||||
appImageMountIDTagKey = "app-image-mount-id"
|
||||
)
|
||||
|
||||
var (
|
||||
appImageMountDirRegex = regexp.MustCompile(`^/tmp/.mount_[^/]+`)
|
||||
appImageMountNameExtractRegex = regexp.MustCompile(`^[A-Za-z0-9]+`)
|
||||
)
|
||||
|
||||
// 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: "AppImage Path",
|
||||
Description: "Path to the app image file itself.",
|
||||
},
|
||||
{
|
||||
ID: appImageMountIDTagKey,
|
||||
Name: "AppImage Mount ID",
|
||||
Description: "Extracted ID from the AppImage mount name. Use AppImage Path instead, if available.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddTags adds tags to the given process.
|
||||
func (h *AppImageHandler) AddTags(p *process.Process) {
|
||||
// Detect app image path via ENV vars.
|
||||
func() {
|
||||
// Get and verify AppImage location.
|
||||
appImageLocation, ok := p.Env["APPIMAGE"]
|
||||
if !ok || appImageLocation == "" {
|
||||
return
|
||||
}
|
||||
appImageMountDir, ok := p.Env["APPDIR"]
|
||||
if !ok || appImageMountDir == "" {
|
||||
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 tag.
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: appImagePathTagKey,
|
||||
Value: appImageLocation,
|
||||
})
|
||||
}()
|
||||
|
||||
// Detect app image mount point.
|
||||
func() {
|
||||
// Check if binary path matches app image mount pattern.
|
||||
mountDir := appImageMountDirRegex.FindString(p.Path)
|
||||
if mountDir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Get mount name of mount dir.
|
||||
// Also, this confirm this is actually a mounted dir.
|
||||
mountName, err := getAppImageMountName(mountDir)
|
||||
if err != nil {
|
||||
log.Debugf("process/tags: failed to get mount name: %s", err)
|
||||
return
|
||||
}
|
||||
if mountName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract a usable ID from the mount name.
|
||||
mountName, _ = strings.CutPrefix(mountName, "gearlever_")
|
||||
mountName = appImageMountNameExtractRegex.FindString(mountName)
|
||||
if mountName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Add app image tag.
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: appImageMountIDTagKey,
|
||||
Value: mountName,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// 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: binmeta.GenerateBinaryNameFromPath(p.Path),
|
||||
PresentationPath: p.Path,
|
||||
UsePresentationPath: true,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Key: tag.Key,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Value: tag.Value, // Value of appImagePathTagKey.
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if tag, ok := p.GetTag(appImageMountIDTagKey); ok {
|
||||
return profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: binmeta.GenerateBinaryNameFromPath(p.Path),
|
||||
PresentationPath: p.Path,
|
||||
UsePresentationPath: true,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Key: tag.Key,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Value: tag.Value, // Value of appImageMountIDTagKey.
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAppImageMountName(mountPoint string) (mountName string, err error) {
|
||||
// Get mounts.
|
||||
data, err := os.ReadFile("/proc/mounts")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) >= 2 {
|
||||
switch {
|
||||
case fields[1] != mountPoint:
|
||||
case !strings.HasSuffix(strings.ToLower(fields[0]), ".appimage"):
|
||||
default:
|
||||
// Found AppImage mount!
|
||||
return fields[0], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if scanner.Err() != nil {
|
||||
return "", scanner.Err()
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
87
service/process/tags/flatpak_unix.go
Normal file
87
service/process/tags/flatpak_unix.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/profile/binmeta"
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := process.RegisterTagHandler(new(flatpakHandler))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
flatpakName = "Flatpak"
|
||||
flatpakIDTagKey = "flatpak-id"
|
||||
)
|
||||
|
||||
// flatpakHandler handles flatpak processes on Unix systems.
|
||||
type flatpakHandler struct{}
|
||||
|
||||
// Name returns the tag handler name.
|
||||
func (h *flatpakHandler) Name() string {
|
||||
return flatpakName
|
||||
}
|
||||
|
||||
// TagDescriptions returns a list of all possible tags and their description
|
||||
// of this handler.
|
||||
func (h *flatpakHandler) TagDescriptions() []process.TagDescription {
|
||||
return []process.TagDescription{
|
||||
{
|
||||
ID: flatpakIDTagKey,
|
||||
Name: "Flatpak ID",
|
||||
Description: "ID of the flatpak.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddTags adds tags to the given process.
|
||||
func (h *flatpakHandler) AddTags(p *process.Process) {
|
||||
// Check if binary lives in the /app space.
|
||||
if !strings.HasPrefix(p.Path, "/app/") {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the Flatpak ID.
|
||||
flatpakID, ok := p.Env["FLATPAK_ID"]
|
||||
if !ok || flatpakID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Add matching path for regular profile matching.
|
||||
p.MatchingPath = p.Path
|
||||
|
||||
// Add app image tag.
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: flatpakIDTagKey,
|
||||
Value: flatpakID,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateProfile creates a profile based on the tags of the process.
|
||||
// Returns nil to skip.
|
||||
func (h *flatpakHandler) CreateProfile(p *process.Process) *profile.Profile {
|
||||
if tag, ok := p.GetTag(flatpakIDTagKey); ok {
|
||||
return profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: binmeta.GenerateBinaryNameFromPath(p.Path),
|
||||
PresentationPath: p.Path,
|
||||
UsePresentationPath: true,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Key: tag.Key,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Value: tag.Value, // Value of flatpakIDTagKey.
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
249
service/process/tags/interpreter_unix.go
Normal file
249
service/process/tags/interpreter_unix.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/shlex"
|
||||
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/profile/binmeta"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := process.RegisterTagHandler(new(InterpHandler)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type interpType struct {
|
||||
process.TagDescription
|
||||
|
||||
Extensions []string
|
||||
Regex *regexp.Regexp
|
||||
}
|
||||
|
||||
var knownInterperters = []interpType{
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "python-script",
|
||||
Name: "Python Script",
|
||||
},
|
||||
Extensions: []string{".py", ".py2", ".py3"},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/python[23](\.[0-9]+)?$`),
|
||||
},
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "shell-script",
|
||||
Name: "Shell Script",
|
||||
},
|
||||
Extensions: []string{".sh", ".bash", ".ksh", ".zsh", ".ash"},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/(ba|k|z|a)?sh$`),
|
||||
},
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "perl-script",
|
||||
Name: "Perl Script",
|
||||
},
|
||||
Extensions: []string{".pl"},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/perl$`),
|
||||
},
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "ruby-script",
|
||||
Name: "Ruby Script",
|
||||
},
|
||||
Extensions: []string{".rb"},
|
||||
Regex: regexp.MustCompile(`^(/usr)?/bin/ruby$`),
|
||||
},
|
||||
{
|
||||
TagDescription: process.TagDescription{
|
||||
ID: "nodejs-script",
|
||||
Name: "NodeJS Script",
|
||||
},
|
||||
Extensions: []string{".js"},
|
||||
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:]
|
||||
}
|
||||
|
||||
// Create a nice script name from filename.
|
||||
scriptName := filepath.Base(args[0])
|
||||
for _, ext := range it.Extensions {
|
||||
scriptName, _ = strings.CutSuffix(scriptName, ext)
|
||||
}
|
||||
scriptName = binmeta.GenerateBinaryNameFromPath(scriptName)
|
||||
|
||||
return profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: fmt.Sprintf("%s: %s", it.Name, scriptName),
|
||||
PresentationPath: tag.Value,
|
||||
UsePresentationPath: true,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Key: it.ID,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
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
service/process/tags/net.go
Normal file
65
service/process/tags/net.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/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
|
||||
}
|
||||
135
service/process/tags/snap_unix.go
Normal file
135
service/process/tags/snap_unix.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/profile/binmeta"
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := process.RegisterTagHandler(new(SnapHandler))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
snapName = "Snap"
|
||||
snapNameKey = "snap-name"
|
||||
snapVersionKey = "snap-version"
|
||||
|
||||
snapBaseDir = "/snap/"
|
||||
)
|
||||
|
||||
// SnapHandler handles Snap processes on Unix systems.
|
||||
type SnapHandler struct{}
|
||||
|
||||
// Name returns the tag handler name.
|
||||
func (h *SnapHandler) Name() string {
|
||||
return snapName
|
||||
}
|
||||
|
||||
// TagDescriptions returns a list of all possible tags and their description
|
||||
// of this handler.
|
||||
func (h *SnapHandler) TagDescriptions() []process.TagDescription {
|
||||
return []process.TagDescription{
|
||||
{
|
||||
ID: snapNameKey,
|
||||
Name: "Snap Name",
|
||||
Description: "Name of snap package.",
|
||||
},
|
||||
{
|
||||
ID: snapVersionKey,
|
||||
Name: "Snap Version",
|
||||
Description: "Version and revision of the snap package.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddTags adds tags to the given process.
|
||||
func (h *SnapHandler) AddTags(p *process.Process) {
|
||||
// Check for snap env and verify location.
|
||||
snapPkgBaseDir, ok := p.Env["SNAP"]
|
||||
if ok && strings.HasPrefix(p.Path, snapPkgBaseDir) {
|
||||
// Try adding tags from env.
|
||||
added := h.addTagsFromEnv(p)
|
||||
if added {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt adding tags from path instead, if env did not work out.
|
||||
h.addTagsFromPath(p)
|
||||
}
|
||||
|
||||
func (h *SnapHandler) addTagsFromEnv(p *process.Process) (added bool) {
|
||||
// Get and verify snap metadata.
|
||||
snapPkgName, ok := p.Env["SNAP_NAME"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
snapPkgVersion, ok := p.Env["SNAP_VERSION"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Add snap tags.
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: snapNameKey,
|
||||
Value: snapPkgName,
|
||||
})
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: snapVersionKey,
|
||||
Value: snapPkgVersion,
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *SnapHandler) addTagsFromPath(p *process.Process) {
|
||||
// Check if the binary is within the snap base dir.
|
||||
if !strings.HasPrefix(p.Path, snapBaseDir) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get snap package name from path.
|
||||
splitted := strings.SplitN(strings.TrimPrefix(p.Path, snapBaseDir), "/", 2)
|
||||
if len(splitted) < 2 || splitted[0] == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Add snap tags.
|
||||
p.Tags = append(p.Tags, profile.Tag{
|
||||
Key: snapNameKey,
|
||||
Value: splitted[0],
|
||||
})
|
||||
}
|
||||
|
||||
// CreateProfile creates a profile based on the tags of the process.
|
||||
// Returns nil to skip.
|
||||
func (h *SnapHandler) CreateProfile(p *process.Process) *profile.Profile {
|
||||
if tag, ok := p.GetTag(snapNameKey); ok {
|
||||
// Check if we have the snap version.
|
||||
// Only use presentation path if we have it.
|
||||
_, hasVersion := p.GetTag(snapVersionKey)
|
||||
|
||||
return profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: binmeta.GenerateBinaryNameFromPath(tag.Value),
|
||||
PresentationPath: p.Path,
|
||||
UsePresentationPath: hasVersion,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Key: tag.Key,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Value: tag.Value, // Value of snapNameKey.
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
111
service/process/tags/svchost_windows.go
Normal file
111
service/process/tags/svchost_windows.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils/osdetail"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/profile/binmeta"
|
||||
)
|
||||
|
||||
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{
|
||||
{
|
||||
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 {
|
||||
// Remove tags from service names, such as "CDPUserSvc_1bf5729".
|
||||
svcName, _, _ := strings.Cut(svcName, "_")
|
||||
// Add service as tag.
|
||||
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 {
|
||||
// Create new profile based on tag.
|
||||
newProfile := profile.New(&profile.Profile{
|
||||
Source: profile.SourceLocal,
|
||||
Name: "Windows Service: " + binmeta.GenerateBinaryNameFromPath(tag.Value),
|
||||
UsePresentationPath: false,
|
||||
Fingerprints: []profile.Fingerprint{
|
||||
{
|
||||
Type: profile.FingerprintTypeTagID,
|
||||
Key: tag.Key,
|
||||
Operation: profile.FingerprintOperationEqualsID,
|
||||
Value: tag.Value, // Value of svchostTagKey.
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Load default icon for windows service.
|
||||
icon, err := binmeta.LoadAndSaveIcon(context.TODO(), `C:\Windows\System32\@WLOGO_48x48.png`)
|
||||
if err == nil {
|
||||
newProfile.Icons = []binmeta.Icon{*icon}
|
||||
}
|
||||
|
||||
return newProfile
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
119
service/process/tags/winstore_windows.go
Normal file
119
service/process/tags/winstore_windows.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/profile/binmeta"
|
||||
)
|
||||
|
||||
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 Windows Store Apps.
|
||||
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: binmeta.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 winStoreAppNameTagKey.
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user