diff --git a/firewall/interception/ebpf/bandwidth/bpf_bpfeb.o b/firewall/interception/ebpf/bandwidth/bpf_bpfeb.o index 2b012f73..ad9376c9 100644 Binary files a/firewall/interception/ebpf/bandwidth/bpf_bpfeb.o and b/firewall/interception/ebpf/bandwidth/bpf_bpfeb.o differ diff --git a/firewall/interception/ebpf/bandwidth/bpf_bpfel.o b/firewall/interception/ebpf/bandwidth/bpf_bpfel.o index 1bbbf778..ce361356 100644 Binary files a/firewall/interception/ebpf/bandwidth/bpf_bpfel.o and b/firewall/interception/ebpf/bandwidth/bpf_bpfel.o differ diff --git a/firewall/interception/ebpf/connection_listener/bpf_bpfeb.o b/firewall/interception/ebpf/connection_listener/bpf_bpfeb.o index 7471b9c2..e8e7cdb7 100644 Binary files a/firewall/interception/ebpf/connection_listener/bpf_bpfeb.o and b/firewall/interception/ebpf/connection_listener/bpf_bpfeb.o differ diff --git a/firewall/interception/ebpf/connection_listener/bpf_bpfel.o b/firewall/interception/ebpf/connection_listener/bpf_bpfel.o index 54ba6347..5241f58e 100644 Binary files a/firewall/interception/ebpf/connection_listener/bpf_bpfel.o and b/firewall/interception/ebpf/connection_listener/bpf_bpfel.o differ diff --git a/firewall/interception/ebpf/exec/exec.go b/firewall/interception/ebpf/exec/exec.go index 0d3b839a..f6dbb383 100644 --- a/firewall/interception/ebpf/exec/exec.go +++ b/firewall/interception/ebpf/exec/exec.go @@ -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 diff --git a/process/api.go b/process/api.go index 88197ceb..0f5c43a4 100644 --- a/process/api.go +++ b/process/api.go @@ -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) + } +} diff --git a/process/database.go b/process/database.go index 285b9df2..2041c6cf 100644 --- a/process/database.go +++ b/process/database.go @@ -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. diff --git a/process/find.go b/process/find.go index c0a209e9..be5afdb6 100644 --- a/process/find.go +++ b/process/find.go @@ -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) diff --git a/process/process.go b/process/process.go index f80ab4ba..b7d0cf41 100644 --- a/process/process.go +++ b/process/process.go @@ -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 { diff --git a/process/process_default.go b/process/process_default.go index c0b96f92..e20d481f 100644 --- a/process/process_default.go +++ b/process/process_default.go @@ -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 } diff --git a/process/process_linux.go b/process/process_linux.go index 7401094a..df3fcc94 100644 --- a/process/process_linux.go +++ b/process/process_linux.go @@ -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) - } - }() -} -*/ diff --git a/process/process_windows.go b/process/process_windows.go index e350f5a7..32dfe343 100644 --- a/process/process_windows.go +++ b/process/process_windows.go @@ -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 +}