wip: migrate to mono-repo. SPN has already been moved to spn/

This commit is contained in:
Patrick Pacher
2024-03-15 11:55:13 +01:00
parent b30fd00ccf
commit 8579430db9
577 changed files with 35981 additions and 818 deletions

99
service/process/api.go Normal file
View 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
View 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
View 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
View 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

View 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
View 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
View 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
}

View 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
View 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 }

View 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
}

View 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)
}

View 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
View 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
View 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
View 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
}

View 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
}

View 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
}

View 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]
}

View 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
}

View 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
}

View 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
}

View 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
}