mirror of
https://github.com/safing/portmaster
synced 2025-09-04 19:49:15 +00:00
Merge pull request #1395 from safing/feature/tauri-migration
Add utility apis for processes required for tauri migration
This commit is contained in:
commit
355a483d5e
14 changed files with 874 additions and 18 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -21,7 +21,6 @@ vendor
|
||||||
testing
|
testing
|
||||||
|
|
||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
*.o
|
|
||||||
*.a
|
*.a
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
|
|
119
firewall/interception/ebpf/exec/bpf_bpfeb.go
Normal file
119
firewall/interception/ebpf/exec/bpf_bpfeb.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// Code generated by bpf2go; DO NOT EDIT.
|
||||||
|
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
|
||||||
|
|
||||||
|
package ebpf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/cilium/ebpf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadBpf returns the embedded CollectionSpec for bpf.
|
||||||
|
func loadBpf() (*ebpf.CollectionSpec, error) {
|
||||||
|
reader := bytes.NewReader(_BpfBytes)
|
||||||
|
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't load bpf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadBpfObjects loads bpf and converts it into a struct.
|
||||||
|
//
|
||||||
|
// The following types are suitable as obj argument:
|
||||||
|
//
|
||||||
|
// *bpfObjects
|
||||||
|
// *bpfPrograms
|
||||||
|
// *bpfMaps
|
||||||
|
//
|
||||||
|
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||||
|
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
|
||||||
|
spec, err := loadBpf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec.LoadAndAssign(obj, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfSpecs contains maps and programs before they are loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
|
type bpfSpecs struct {
|
||||||
|
bpfProgramSpecs
|
||||||
|
bpfMapSpecs
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfSpecs contains programs before they are loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
|
type bpfProgramSpecs struct {
|
||||||
|
EnterExecve *ebpf.ProgramSpec `ebpf:"enter_execve"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
|
type bpfMapSpecs struct {
|
||||||
|
PmExecMap *ebpf.MapSpec `ebpf:"pm_exec_map"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfObjects contains all objects after they have been loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
|
type bpfObjects struct {
|
||||||
|
bpfPrograms
|
||||||
|
bpfMaps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *bpfObjects) Close() error {
|
||||||
|
return _BpfClose(
|
||||||
|
&o.bpfPrograms,
|
||||||
|
&o.bpfMaps,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfMaps contains all maps after they have been loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
|
type bpfMaps struct {
|
||||||
|
PmExecMap *ebpf.Map `ebpf:"pm_exec_map"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *bpfMaps) Close() error {
|
||||||
|
return _BpfClose(
|
||||||
|
m.PmExecMap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfPrograms contains all programs after they have been loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
|
type bpfPrograms struct {
|
||||||
|
EnterExecve *ebpf.Program `ebpf:"enter_execve"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *bpfPrograms) Close() error {
|
||||||
|
return _BpfClose(
|
||||||
|
p.EnterExecve,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BpfClose(closers ...io.Closer) error {
|
||||||
|
for _, closer := range closers {
|
||||||
|
if err := closer.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not access this directly.
|
||||||
|
//
|
||||||
|
//go:embed bpf_bpfeb.o
|
||||||
|
var _BpfBytes []byte
|
BIN
firewall/interception/ebpf/exec/bpf_bpfeb.o
Normal file
BIN
firewall/interception/ebpf/exec/bpf_bpfeb.o
Normal file
Binary file not shown.
119
firewall/interception/ebpf/exec/bpf_bpfel.go
Normal file
119
firewall/interception/ebpf/exec/bpf_bpfel.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// Code generated by bpf2go; DO NOT EDIT.
|
||||||
|
//go:build 386 || amd64 || amd64p32 || arm || arm64 || loong64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
|
||||||
|
|
||||||
|
package ebpf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/cilium/ebpf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadBpf returns the embedded CollectionSpec for bpf.
|
||||||
|
func loadBpf() (*ebpf.CollectionSpec, error) {
|
||||||
|
reader := bytes.NewReader(_BpfBytes)
|
||||||
|
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't load bpf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadBpfObjects loads bpf and converts it into a struct.
|
||||||
|
//
|
||||||
|
// The following types are suitable as obj argument:
|
||||||
|
//
|
||||||
|
// *bpfObjects
|
||||||
|
// *bpfPrograms
|
||||||
|
// *bpfMaps
|
||||||
|
//
|
||||||
|
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||||
|
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
|
||||||
|
spec, err := loadBpf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec.LoadAndAssign(obj, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfSpecs contains maps and programs before they are loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
|
type bpfSpecs struct {
|
||||||
|
bpfProgramSpecs
|
||||||
|
bpfMapSpecs
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfSpecs contains programs before they are loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
|
type bpfProgramSpecs struct {
|
||||||
|
EnterExecve *ebpf.ProgramSpec `ebpf:"enter_execve"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
|
type bpfMapSpecs struct {
|
||||||
|
PmExecMap *ebpf.MapSpec `ebpf:"pm_exec_map"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfObjects contains all objects after they have been loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
|
type bpfObjects struct {
|
||||||
|
bpfPrograms
|
||||||
|
bpfMaps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *bpfObjects) Close() error {
|
||||||
|
return _BpfClose(
|
||||||
|
&o.bpfPrograms,
|
||||||
|
&o.bpfMaps,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfMaps contains all maps after they have been loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
|
type bpfMaps struct {
|
||||||
|
PmExecMap *ebpf.Map `ebpf:"pm_exec_map"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *bpfMaps) Close() error {
|
||||||
|
return _BpfClose(
|
||||||
|
m.PmExecMap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpfPrograms contains all programs after they have been loaded into the kernel.
|
||||||
|
//
|
||||||
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
|
type bpfPrograms struct {
|
||||||
|
EnterExecve *ebpf.Program `ebpf:"enter_execve"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *bpfPrograms) Close() error {
|
||||||
|
return _BpfClose(
|
||||||
|
p.EnterExecve,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BpfClose(closers ...io.Closer) error {
|
||||||
|
for _, closer := range closers {
|
||||||
|
if err := closer.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not access this directly.
|
||||||
|
//
|
||||||
|
//go:embed bpf_bpfel.o
|
||||||
|
var _BpfBytes []byte
|
BIN
firewall/interception/ebpf/exec/bpf_bpfel.o
Normal file
BIN
firewall/interception/ebpf/exec/bpf_bpfel.o
Normal file
Binary file not shown.
249
firewall/interception/ebpf/exec/exec.go
Normal file
249
firewall/interception/ebpf/exec/exec.go
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
package ebpf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/cilium/ebpf/link"
|
||||||
|
"github.com/cilium/ebpf/ringbuf"
|
||||||
|
"github.com/cilium/ebpf/rlimit"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"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
|
||||||
|
|
||||||
|
// These constants are defined in `bpf/handler.c` and must be kept in sync.
|
||||||
|
const (
|
||||||
|
arglen = 32
|
||||||
|
argsize = 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
var errTracerClosed = errors.New("tracer is closed")
|
||||||
|
|
||||||
|
// event contains details about each exec call, sent from the eBPF program to
|
||||||
|
// userspace through a perf ring buffer. This type must be kept in sync with
|
||||||
|
// `event_t` in `bpf/handler.c`.
|
||||||
|
type event struct {
|
||||||
|
// Details about the process being launched.
|
||||||
|
Filename [argsize]byte
|
||||||
|
Argv [arglen][argsize]byte
|
||||||
|
Argc uint32
|
||||||
|
UID uint32
|
||||||
|
GID uint32
|
||||||
|
PID uint32
|
||||||
|
|
||||||
|
// Name of the calling process.
|
||||||
|
Comm [argsize]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event contains data about each exec event with many fields for easy
|
||||||
|
// filtering and logging.
|
||||||
|
type Event struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
// Argv contains the raw argv supplied to the process, including argv[0]
|
||||||
|
// (which is equal to `filepath.Base(e.Filename)` in most circumstances).
|
||||||
|
Argv []string `json:"argv"`
|
||||||
|
// Truncated is true if we were unable to read all process arguments into
|
||||||
|
// Argv because there were more than ARGLEN arguments.
|
||||||
|
Truncated bool `json:"truncated"`
|
||||||
|
|
||||||
|
// These values are of the new process. Keep in mind that the exec call may
|
||||||
|
// fail and the PID will be released in such a case.
|
||||||
|
PID uint32 `json:"pid"`
|
||||||
|
UID uint32 `json:"uid"`
|
||||||
|
GID uint32 `json:"gid"`
|
||||||
|
|
||||||
|
// Comm is the "name" of the parent process, usually the filename of the
|
||||||
|
// executable (but not always).
|
||||||
|
Comm string `json:"comm"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracer is the exec tracer itself.
|
||||||
|
// It must be closed after use.
|
||||||
|
type Tracer struct {
|
||||||
|
objs bpfObjects
|
||||||
|
tp link.Link
|
||||||
|
rb *ringbuf.Reader
|
||||||
|
|
||||||
|
closeLock sync.Mutex
|
||||||
|
closed chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New instantiates all of the BPF objects into the running kernel, starts
|
||||||
|
// tracing, and returns the created Tracer. After calling this successfully, the
|
||||||
|
// caller should immediately attach a for loop running `h.Read()`.
|
||||||
|
//
|
||||||
|
// The returned Tracer MUST be closed when not needed anymore otherwise kernel
|
||||||
|
// resources may be leaked.
|
||||||
|
func New() (*Tracer, error) {
|
||||||
|
t := &Tracer{
|
||||||
|
tp: nil,
|
||||||
|
rb: nil,
|
||||||
|
|
||||||
|
closeLock: sync.Mutex{},
|
||||||
|
closed: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := loadBpfObjects(&t.objs, nil); err != nil {
|
||||||
|
return nil, fmt.Errorf("ebpf: failed to load ebpf object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.start(); err != nil {
|
||||||
|
// Best effort.
|
||||||
|
_ = t.Close()
|
||||||
|
return nil, fmt.Errorf("start tracer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// It could be very bad if someone forgot to close this, so we'll try to
|
||||||
|
// detect when it doesn't get closed and log a warning.
|
||||||
|
stack := debug.Stack()
|
||||||
|
runtime.SetFinalizer(t, func(t *Tracer) {
|
||||||
|
err := t.Close()
|
||||||
|
if errors.Is(err, errTracerClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("tracer was finalized but was not closed, created at: %s", stack)
|
||||||
|
log.Infof("tracers must be closed when finished with to avoid leaked kernel resources")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("closing tracer failed: %+v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// start loads the eBPF programs and maps into the kernel and starts them.
|
||||||
|
// You should immediately attach a for loop running `h.Read()` after calling
|
||||||
|
// this successfully.
|
||||||
|
func (t *Tracer) start() error {
|
||||||
|
// If we don't startup successfully, we need to make sure all of the
|
||||||
|
// stuff is cleaned up properly or we'll be leaking kernel resources.
|
||||||
|
ok := false
|
||||||
|
defer func() {
|
||||||
|
if !ok {
|
||||||
|
// Best effort.
|
||||||
|
_ = t.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Allow the current process to lock memory for eBPF resources. This
|
||||||
|
// does nothing on 5.11+ kernels which don't need this.
|
||||||
|
err := rlimit.RemoveMemlock()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove memlock: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the eBPF program to the `sys_enter_execve` tracepoint, which
|
||||||
|
// is triggered at the beginning of each `execve()` syscall.
|
||||||
|
t.tp, err = link.Tracepoint("syscalls", "sys_enter_execve", t.objs.EnterExecve, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open tracepoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the reader for the event ringbuf.
|
||||||
|
t.rb, err = ringbuf.NewReader(t.objs.PmExecMap)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open ringbuf reader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads an event from the eBPF program via the ringbuf, parses it and
|
||||||
|
// returns it. If the *tracer is closed during the blocked call, and error that
|
||||||
|
// wraps io.EOF will be returned.
|
||||||
|
func (t *Tracer) Read() (*Event, error) {
|
||||||
|
rb := t.rb
|
||||||
|
if rb == nil {
|
||||||
|
return nil, errors.New("ringbuf reader is not initialized, tracer may not be open or may have been closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := rb.Read()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ringbuf.ErrClosed) {
|
||||||
|
return nil, fmt.Errorf("tracer closed: %w", io.EOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("read from ringbuf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the ringbuf event entry into an event structure.
|
||||||
|
var rawEvent event
|
||||||
|
err = binary.Read(bytes.NewBuffer(record.RawSample), binary.NativeEndian, &rawEvent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse raw ringbuf entry into event struct: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ev := &Event{
|
||||||
|
Filename: unix.ByteSliceToString(rawEvent.Filename[:]),
|
||||||
|
Argv: []string{}, // populated below
|
||||||
|
Truncated: rawEvent.Argc == arglen+1,
|
||||||
|
PID: rawEvent.PID,
|
||||||
|
UID: rawEvent.UID,
|
||||||
|
GID: rawEvent.GID,
|
||||||
|
Comm: unix.ByteSliceToString(rawEvent.Comm[:]),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy only the args we're allowed to read from the array. If we read more
|
||||||
|
// than rawEvent.Argc, we could be copying non-zeroed memory.
|
||||||
|
argc := int(rawEvent.Argc)
|
||||||
|
if argc > arglen {
|
||||||
|
argc = arglen
|
||||||
|
}
|
||||||
|
for i := 0; i < argc; i++ {
|
||||||
|
str := unix.ByteSliceToString(rawEvent.Argv[i][:])
|
||||||
|
if strings.TrimSpace(str) != "" {
|
||||||
|
ev.Argv = append(ev.Argv, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ev, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close gracefully closes and frees all resources associated with the eBPF
|
||||||
|
// tracepoints, maps and other resources. Any blocked `Read()` operations will
|
||||||
|
// return an error that wraps `io.EOF`.
|
||||||
|
func (t *Tracer) Close() error {
|
||||||
|
t.closeLock.Lock()
|
||||||
|
defer t.closeLock.Unlock()
|
||||||
|
select {
|
||||||
|
case <-t.closed:
|
||||||
|
return errTracerClosed
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
close(t.closed)
|
||||||
|
runtime.SetFinalizer(t, nil)
|
||||||
|
|
||||||
|
// Close everything started in h.Start() in reverse order.
|
||||||
|
var merr error
|
||||||
|
if t.rb != nil {
|
||||||
|
err := t.rb.Close()
|
||||||
|
if err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("close ringbuf reader: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.tp != nil {
|
||||||
|
err := t.tp.Close()
|
||||||
|
if err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("close tracepoint: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := t.objs.Close()
|
||||||
|
if err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("close eBPF objects: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return merr
|
||||||
|
}
|
116
firewall/interception/ebpf/programs/exec.c
Normal file
116
firewall/interception/ebpf/programs/exec.c
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
#include "vmlinux-x86.h"
|
||||||
|
#include "bpf/bpf_helpers.h"
|
||||||
|
#include "bpf/bpf_tracing.h"
|
||||||
|
|
||||||
|
#define ARGLEN 32 // maximum amount of args in argv we'll copy
|
||||||
|
#define ARGSIZE 1024 // maximum byte length of each arg in argv we'll copy
|
||||||
|
|
||||||
|
char __license[] SEC("license") = "GPL";
|
||||||
|
|
||||||
|
// Ring buffer for all connection events
|
||||||
|
struct {
|
||||||
|
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||||
|
__uint(max_entries, 1 << 24);
|
||||||
|
} pm_exec_map SEC(".maps");
|
||||||
|
|
||||||
|
// This struct is defined according to
|
||||||
|
// /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format
|
||||||
|
struct exec_info {
|
||||||
|
u16 common_type; // offset=0, size=2
|
||||||
|
u8 common_flags; // offset=2, size=1
|
||||||
|
u8 common_preempt_count; // offset=3, size=1
|
||||||
|
s32 common_pid; // offset=4, size=4
|
||||||
|
|
||||||
|
s32 syscall_nr; // offset=8, size=4
|
||||||
|
u32 pad; // offset=12, size=4 (pad)
|
||||||
|
const u8 *filename; // offset=16, size=8 (ptr)
|
||||||
|
const u8 *const *argv; // offset=24, size=8 (ptr)
|
||||||
|
const u8 *const *envp; // offset=32, size=8 (ptr)
|
||||||
|
};
|
||||||
|
|
||||||
|
// The event struct. This struct must be kept in sync with the Golang
|
||||||
|
// counterpart.
|
||||||
|
struct event_t {
|
||||||
|
// Details about the process being launched.
|
||||||
|
u8 filename[ARGSIZE];
|
||||||
|
u8 argv[ARGLEN][ARGSIZE];
|
||||||
|
u32 argc; // set to ARGLEN + 1 if there were more than ARGLEN arguments
|
||||||
|
u32 uid;
|
||||||
|
u32 gid;
|
||||||
|
u32 pid;
|
||||||
|
|
||||||
|
// Name of the calling process.
|
||||||
|
u8 comm[ARGSIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tracepoint at the top of execve() syscall.
|
||||||
|
SEC("tracepoint/syscalls/sys_enter_execve")
|
||||||
|
s32 enter_execve(struct exec_info *ctx) {
|
||||||
|
// Reserve memory for our event on the `events` ring buffer defined above.
|
||||||
|
struct event_t *event;
|
||||||
|
event = bpf_ringbuf_reserve(&pm_exec_map, sizeof(struct event_t), 0);
|
||||||
|
if (!event) {
|
||||||
|
bpf_printk("could not reserve ringbuf memory");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store process/calling process details.
|
||||||
|
u64 uidgid = bpf_get_current_uid_gid();
|
||||||
|
u64 pidtgid = bpf_get_current_pid_tgid();
|
||||||
|
event->uid = uidgid; // uid is the first 32 bits
|
||||||
|
event->gid = uidgid >> 32; // gid is the last 32 bits NOLINT(readability-magic-numbers)
|
||||||
|
event->pid = pidtgid; // pid is the first 32 bits
|
||||||
|
s32 ret = bpf_get_current_comm(&event->comm, sizeof(event->comm));
|
||||||
|
if (ret) {
|
||||||
|
bpf_printk("could not get current comm: %d", ret);
|
||||||
|
bpf_ringbuf_discard(event, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the filename in addition to argv[0] because the filename contains
|
||||||
|
// the full path to the file which could be more useful in some situations.
|
||||||
|
ret = bpf_probe_read_user_str(event->filename, sizeof(event->filename), ctx->filename);
|
||||||
|
if (ret < 0) {
|
||||||
|
bpf_printk("could not read filename into event struct: %d", ret);
|
||||||
|
bpf_ringbuf_discard(event, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy everything from ctx->argv to event->argv, incrementing event->argc
|
||||||
|
// as we go.
|
||||||
|
for (s32 i = 0; i < ARGLEN; i++) {
|
||||||
|
if (!(&ctx->argv[i])) {
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copying the arg into it's own variable before copying it into
|
||||||
|
// event->argv[i] prevents memory corruption.
|
||||||
|
const u8 *argp = NULL;
|
||||||
|
ret = bpf_probe_read_user(&argp, sizeof(argp), &ctx->argv[i]);
|
||||||
|
if (ret || !argp) {
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy argp to event->argv[i].
|
||||||
|
ret = bpf_probe_read_user_str(event->argv[i], sizeof(event->argv[i]), argp);
|
||||||
|
if (ret < 0) {
|
||||||
|
bpf_printk("read argv %d: %d", i, ret);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
event->argc++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This won't get hit if we `goto out` in the loop above. This is to signify
|
||||||
|
// to userspace that we couldn't copy all of the arguments because it
|
||||||
|
// exceeded ARGLEN.
|
||||||
|
event->argc++;
|
||||||
|
|
||||||
|
out:
|
||||||
|
// Write the event to the ring buffer and notify userspace. This will cause
|
||||||
|
// the `Read()` call in userspace to return if it was blocked.
|
||||||
|
bpf_ringbuf_submit(event, 0);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -1,17 +1,45 @@
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"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",
|
}); err != nil {
|
||||||
Description: "Get information about process tags.",
|
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 {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -37,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -10,6 +13,7 @@ import (
|
||||||
|
|
||||||
"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"
|
||||||
|
@ -46,6 +50,35 @@ func All() map[int]*Process {
|
||||||
return all
|
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.
|
// Save saves the process to the internal state and pushes an update.
|
||||||
func (p *Process) Save() {
|
func (p *Process) Save() {
|
||||||
p.Lock()
|
p.Lock()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -30,20 +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
|
|
||||||
CreatedAt int64
|
Pid int
|
||||||
|
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
|
||||||
|
@ -91,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 {
|
||||||
|
@ -246,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)
|
||||||
|
@ -263,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 {
|
||||||
|
|
|
@ -3,5 +3,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 = 0
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,96 @@
|
||||||
package process
|
package process
|
||||||
|
|
||||||
// SystemProcessID is the PID of the System/Kernel itself.
|
import (
|
||||||
const SystemProcessID = 0
|
"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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue