mirror of
https://github.com/safing/portmaster
synced 2025-09-02 02:29:12 +00:00
249 lines
6.7 KiB
Go
249 lines
6.7 KiB
Go
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
|
|
}
|