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/rlimit"
"github.com/hashicorp/go-multierror"
"github.com/safing/portbase/log"
"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
@ -67,6 +68,8 @@ type Event struct {
Comm string `json:"comm"`
}
// Tracer is the exec tracer itself.
// It must be closed after use.
type Tracer struct {
objs bpfObjects
tp link.Link

View file

@ -1,81 +1,45 @@
package process
import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/safing/portbase/api"
"github.com/safing/portmaster/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,
Name: "Get Process Tag Metadata",
Description: "Get information about process tags.",
}); err != nil {
return err
}
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",
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{
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",
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
}
@ -101,3 +65,35 @@ func handleProcessTagMetadata(ar *api.Request) (i interface{}, err error) {
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 (
"context"
"fmt"
"slices"
"strings"
"sync"
"time"
processInfo "github.com/shirou/gopsutil/process"
"github.com/tevino/abool"
"golang.org/x/exp/maps"
"github.com/safing/portbase/database"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/profile"
)
const processDatabaseNamespace = "network:tree"
@ -48,37 +50,33 @@ func All() map[int]*Process {
return all
}
func FindProcessesByProfile(ctx context.Context, scopedID string) []*Process {
all := 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))
pids := make([]int, 0, len(all))
log.Infof("[DEBUG] searchin processes belonging to %s", scopedID)
for _, p := range all {
p.Lock()
if p.profile != nil && p.profile.LocalProfile().ScopedID() == scopedID {
pids = append(pids, p.Pid)
// 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)
}
}
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 {
if _, ok := m[pid]; ok {
continue
}
process, err := GetProcessGroupLeader(ctx, pid)
if err != nil {
continue
}
m[process.Pid] = process
}
return maps.Values(m)
return procs
}
// 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
}
// 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)

View file

@ -30,21 +30,26 @@ type Process struct {
// Process attributes.
// Don't change; safe for concurrent access.
Name string
UserID int
UserName string
UserHome string
Pid int
Pgid int // linux only
CreatedAt int64
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
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
@ -92,6 +97,16 @@ func (p *Process) Profile() *profile.LayeredProfile {
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 {
@ -213,12 +228,9 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
return process, nil
}
pgid, _ := GetProcessGroupID(ctx, int(pInfo.Pid))
// Create new a process object.
process = &Process{
Pid: int(pInfo.Pid),
Pgid: pgid,
FirstSeen: time.Now().Unix(),
processKey: key,
}
@ -250,7 +262,7 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
// TODO: User Home
// new.UserHome, err =
// Parent process id
// 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)
@ -267,6 +279,19 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
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 {

View file

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

View file

@ -2,83 +2,95 @@ package process
import (
"context"
"fmt"
"syscall"
"github.com/safing/portbase/log"
)
// SystemProcessID is the PID of the System/Kernel itself.
const SystemProcessID = 0
const (
// SystemProcessID is the PID of the System/Kernel itself.
SystemProcessID = 0
func GetProcessGroupLeader(ctx context.Context, pid int) (*Process, error) {
pgid, err := GetProcessGroupID(ctx, pid)
if err != nil {
return nil, err
// 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
}
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 {
log.Infof("[DBUG] found leader pid=%d pgid=%d", leader.Pid, leader.Pgid)
return leader, 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
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 {
next, err := GetOrFindProcess(ctx, iter.ParentPid)
// Get next parent.
parent, err := GetOrFindProcess(ctx, nextParentPid)
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
// the pgid than iter is the first child of the process
// group
if next.Pgid != pgid {
return iter, nil
// 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
}
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) {
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
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
}