safing-portmaster/process/process.go
2023-08-04 21:49:57 +02:00

352 lines
8.8 KiB
Go

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