Improve getting process group leader

This commit is contained in:
Daniel 2023-12-21 13:17:05 +01:00
parent 30fee07a89
commit 425a0bed4c
12 changed files with 217 additions and 156 deletions

View file

@ -15,8 +15,9 @@ import (
"github.com/cilium/ebpf/ringbuf" "github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit" "github.com/cilium/ebpf/rlimit"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/safing/portbase/log"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/safing/portbase/log"
) )
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -Werror" bpf ../programs/exec.c //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -Werror" bpf ../programs/exec.c
@ -67,6 +68,8 @@ type Event struct {
Comm string `json:"comm"` Comm string `json:"comm"`
} }
// Tracer is the exec tracer itself.
// It must be closed after use.
type Tracer struct { type Tracer struct {
objs bpfObjects objs bpfObjects
tp link.Link tp link.Link

View file

@ -1,81 +1,45 @@
package process package process
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/safing/portbase/api" "github.com/safing/portbase/api"
"github.com/safing/portmaster/profile"
) )
func registerAPIEndpoints() error { func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{ if err := api.RegisterEndpoint(api.Endpoint{
Name: "Get Process Tag Metadata",
Description: "Get information about process tags.",
Path: "process/tags", Path: "process/tags",
Read: api.PermitUser, Read: api.PermitUser,
BelongsTo: module, BelongsTo: module,
StructFunc: handleProcessTagMetadata, StructFunc: handleProcessTagMetadata,
Name: "Get Process Tag Metadata",
Description: "Get information about process tags.",
}); err != nil { }); err != nil {
return err return err
} }
if err := api.RegisterEndpoint(api.Endpoint{ if err := api.RegisterEndpoint(api.Endpoint{
Path: "process/by-profile",
Parameters: []api.Parameter{
{
Method: http.MethodGet,
Field: "scopedId",
Value: "",
Description: "The ID of the profile",
},
},
Read: api.PermitUser,
BelongsTo: module,
StructFunc: api.StructFunc(func(ar *api.Request) (any, error) {
id := ar.URL.Query().Get("scopedId")
if id == "" {
return nil, api.ErrorWithStatus(fmt.Errorf("missing profile id"), http.StatusBadRequest)
}
result := FindProcessesByProfile(ar.Context(), id)
return result, nil
}),
Description: "Get all running processes for a given profile",
Name: "Get Processes by Profile", 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 { }); err != nil {
return err return err
} }
if err := api.RegisterEndpoint(api.Endpoint{ if err := api.RegisterEndpoint(api.Endpoint{
Path: "process/by-pid/{pid:[0-9]+}",
Parameters: []api.Parameter{
{
Method: http.MethodGet,
Field: "pid",
Value: "",
Description: "A PID of a process inside the requested process group",
},
},
Read: api.PermitUser,
BelongsTo: module,
StructFunc: api.StructFunc(func(ar *api.Request) (i interface{}, err error) {
pid, err := strconv.ParseInt(ar.URLVars["pid"], 10, 0)
if err != nil {
return nil, api.ErrorWithStatus(err, http.StatusBadRequest)
}
process, err := GetProcessGroupLeader(ar.Context(), int(pid))
if err != nil {
return nil, api.ErrorWithStatus(err, http.StatusInternalServerError)
}
return process, nil
}),
Description: "Load a process group leader by a child PID",
Name: "Get Process Group Leader By PID", 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 { }); err != nil {
return err return err
} }
@ -101,3 +65,35 @@ func handleProcessTagMetadata(ar *api.Request) (i interface{}, err error) {
return resp, nil 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)
}
}

View file

@ -3,15 +3,17 @@ package process
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings"
"sync" "sync"
"time" "time"
processInfo "github.com/shirou/gopsutil/process" processInfo "github.com/shirou/gopsutil/process"
"github.com/tevino/abool" "github.com/tevino/abool"
"golang.org/x/exp/maps"
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portmaster/profile"
) )
const processDatabaseNamespace = "network:tree" const processDatabaseNamespace = "network:tree"
@ -48,37 +50,33 @@ func All() map[int]*Process {
return all return all
} }
func FindProcessesByProfile(ctx context.Context, scopedID string) []*Process { // GetProcessesWithProfile returns all processes that use the given profile.
all := All() // 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))
pids := make([]int, 0, len(all)) // Get all processes that match the given profile.
procs := make([]*Process, 0, 8)
log.Infof("[DEBUG] searchin processes belonging to %s", scopedID) for _, p := range All() {
lp := p.profile.LocalProfile()
for _, p := range all { if lp != nil && lp.Source == profileSource && lp.ID == profileID {
p.Lock() if preferProcessGroupLeader && p.Leader() != nil {
if p.profile != nil && p.profile.LocalProfile().ScopedID() == scopedID { procs = append(procs, p.Leader())
pids = append(pids, p.Pid) } else {
procs = append(procs, p)
}
} }
p.Unlock()
} }
m := make(map[int]*Process) // 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
})
for _, pid := range pids { return procs
if _, ok := m[pid]; ok {
continue
}
process, err := GetProcessGroupLeader(ctx, pid)
if err != nil {
continue
}
m[process.Pid] = process
}
return maps.Values(m)
} }
// Save saves the process to the internal state and pushes an update. // Save saves the process to the internal state and pushes an update.

View file

@ -29,6 +29,13 @@ func GetProcessWithProfile(ctx context.Context, pid int) (process *Process, err
return GetUnidentifiedProcess(ctx), 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) changed, err := process.GetProfile(ctx)
if err != nil { if err != nil {
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err) log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)

View file

@ -30,21 +30,26 @@ type Process struct {
// Process attributes. // Process attributes.
// Don't change; safe for concurrent access. // Don't change; safe for concurrent access.
Name string Name string
UserID int UserID int
UserName string UserName string
UserHome string UserHome string
Pid int
Pgid int // linux only Pid int
CreatedAt int64 CreatedAt int64
ParentPid int ParentPid int
ParentCreatedAt int64 ParentCreatedAt int64
Path string
ExecName string LeaderPid int
Cwd string leader *Process
CmdLine string
FirstArg string Path string
Env map[string]string ExecName string
Cwd string
CmdLine string
FirstArg string
Env map[string]string
// unique process identifier ("Pid-CreatedAt") // unique process identifier ("Pid-CreatedAt")
processKey string processKey string
@ -92,6 +97,16 @@ func (p *Process) Profile() *profile.LayeredProfile {
return p.profile 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 // IsIdentified returns whether the process has been identified or if it
// represents some kind of unidentified process. // represents some kind of unidentified process.
func (p *Process) IsIdentified() bool { func (p *Process) IsIdentified() bool {
@ -213,12 +228,9 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
return process, nil return process, nil
} }
pgid, _ := GetProcessGroupID(ctx, int(pInfo.Pid))
// Create new a process object. // Create new a process object.
process = &Process{ process = &Process{
Pid: int(pInfo.Pid), Pid: int(pInfo.Pid),
Pgid: pgid,
FirstSeen: time.Now().Unix(), FirstSeen: time.Now().Unix(),
processKey: key, processKey: key,
} }
@ -250,7 +262,7 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
// TODO: User Home // TODO: User Home
// new.UserHome, err = // new.UserHome, err =
// Parent process id // Parent process ID
ppid, err := pInfo.PpidWithContext(ctx) ppid, err := pInfo.PpidWithContext(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get PPID for p%d: %w", pInfo.Pid, err) return nil, fmt.Errorf("failed to get PPID for p%d: %w", pInfo.Pid, err)
@ -267,6 +279,19 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
process.ParentCreatedAt = parentCreatedAt 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 // Path
process.Path, err = pInfo.ExeWithContext(ctx) process.Path, err = pInfo.ExeWithContext(ctx)
if err != nil { if err != nil {

View file

@ -10,11 +10,14 @@ import (
// SystemProcessID is the PID of the System/Kernel itself. // SystemProcessID is the PID of the System/Kernel itself.
const SystemProcessID = 0 const SystemProcessID = 0
func GetProcessGroupLeader(ctx context.Context, pid int) (*Process, error) { // GetProcessGroupLeader returns the process that leads the process group.
// On systems other than linux we just return the process with PID == pid // Returns nil on unsupported platforms.
return GetOrFindProcess(ctx, pid) 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) { func GetProcessGroupID(ctx context.Context, pid int) (int, error) {
return 0 return UndefinedProcessID, nil
} }

View file

@ -2,83 +2,95 @@ package process
import ( import (
"context" "context"
"fmt"
"syscall" "syscall"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
) )
// SystemProcessID is the PID of the System/Kernel itself. const (
const SystemProcessID = 0 // SystemProcessID is the PID of the System/Kernel itself.
SystemProcessID = 0
func GetProcessGroupLeader(ctx context.Context, pid int) (*Process, error) { // SystemInitID is the PID of the system init process.
pgid, err := GetProcessGroupID(ctx, pid) SystemInitID = 1
if err != nil { )
return nil, err
// 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
} }
leader, err := GetOrFindProcess(ctx, pgid) // 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 { if err == nil {
log.Infof("[DBUG] found leader pid=%d pgid=%d", leader.Pid, leader.Pgid) p.leader = leader
return leader, nil log.Tracer(ctx).Debugf("process: found process leader of %d: pid=%d pgid=%d", p.Pid, leader.Pid, leader.LeaderPid)
} return nil
// this seems like a orphan process group so find the outermost parent
// i.e. the first process in the group
iter, err := GetOrFindProcess(ctx, pid)
if err != nil {
log.Infof("[DBUG] failed to get process for pid %d", pid)
return nil, err
}
// This is already the leader
if iter.Pid == pgid {
log.Infof("[DBUG] iter pid=%d pgid=%d is already leader", pid, pgid)
return iter, 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 { for {
next, err := GetOrFindProcess(ctx, iter.ParentPid) // Get next parent.
parent, err := GetOrFindProcess(ctx, nextParentPid)
if err != nil { if err != nil {
return nil, err p.leader = lastParent
return fmt.Errorf("failed to find parent %d: %w", nextParentPid, err)
} }
// If the parent process group ID of does not match // Check if we are ready to return.
// the pgid than iter is the first child of the process switch {
// group case parent.Pid == p.LeaderPid:
if next.Pgid != pgid { // Found the process group leader!
return iter, nil 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
} }
iter = next // 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) { func GetProcessGroupID(ctx context.Context, pid int) (int, error) {
return syscall.Getpgid(pid) return syscall.Getpgid(pid)
} }
/*
func init() {
tracer, err := ebpf.New()
if err != nil {
panic(err)
}
go func() {
file, _ := os.Create("/tmp/tracer.json")
enc := json.NewEncoder(file)
enc.SetIndent("", " ")
defer tracer.Close()
for {
evt, err := tracer.Read()
if err != nil {
log.Errorf("failed to read from execve tracer: %s", err)
return
}
_ = enc.Encode(evt)
}
}()
}
*/

View file

@ -1,4 +1,21 @@
package process package process
import (
"context"
)
// SystemProcessID is the PID of the System/Kernel itself. // SystemProcessID is the PID of the System/Kernel itself.
const SystemProcessID = 4 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
}