sorcery-go/pkg/warding/ebpf/ebpf.go

872 lines
25 KiB
Go
Executable File

// Package ebpf provides the eBPF-based security enforcement layer for the
// Warding. It replaces the former AppArmor mandatory access control with
// in-kernel eBPF programs that are faster, more precise, and runtime-agnostic
// (they work identically across LXC, Firecracker, and Podman).
//
// Architecture:
//
// Userspace (Go) Kernel (eBPF)
// ───────────────── ──────────────
// EBPFEnforcer tomb_guard.bpf.o
// ├─ Load() ├─ LSM/file_permission
// ├─ AttachCgroup(cgroupPath) ├─ LSM/inode_permission
// ├─ SetEnforceMode(mode) ├─ tracepoint/sys_enter_execve
// ├─ AddTrustedPID(pid) └─ perf_event_array → violations
// ├─ WatchViolations() → []Violation
// └─ Close()
//
// sorcery_filter.bpf.o
// ├─ cgroup/dev (device whitelist)
// ├─ cgroup/connect4 (egress filter)
// └─ cgroup/bind4 (bind filter)
//
// The eBPF programs are compiled from C source files in the ./c/ directory
// at build time using `go generate` (which invokes clang/bpftool). The
// resulting .bpf.o files are embedded into the Go binary via go:embed.
//
// At runtime, the EBPFEnforcer:
// 1. Loads the compiled eBPF programs into the kernel
// 2. Attaches LSM hooks for Tomb protection
// 3. Attaches cgroup device/network filters to the target cgroup
// 4. Reads violation events from a perf buffer
// 5. Exposes an API for the Warding to manage enforcement
package ebpf
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/features"
"github.com/cilium/ebpf/rlimit"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel,bpfeb -cc clang -cflags "-O2 -g -Wall" TombGuard ./c/tomb_guard.bpf.c -- -I./c/
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel,bpfeb -cc clang -cflags "-O2 -g -Wall" SorceryFilter ./c/sorcery_filter.bpf.c -- -I./c/
// EnforceMode controls whether violations are blocked or just logged.
type EnforceMode uint32
const (
// ModePermissive logs violations but does not block them.
ModePermissive EnforceMode = 0
// ModeEnforcing blocks violating syscalls and logs them.
ModeEnforcing EnforceMode = 1
)
// Violation represents a security violation event from the eBPF program.
type Violation struct {
PID uint32 `json:"pid"`
TID uint32 `json:"tid"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
PPID uint32 `json:"ppid"`
SyscallNr uint32 `json:"syscall_nr"`
Comm string `json:"comm"`
Path string `json:"path"`
AccessMask int32 `json:"access_mask"`
}
// ViolationHandler is a callback invoked for each violation event.
type ViolationHandler func(Violation)
// ---------------------------------------------------------------------------
// Tomb Guard — bpf2go generated types
// ---------------------------------------------------------------------------
// These types mirror what `go generate` (bpf2go) would produce from
// tomb_guard.bpf.c. They can be replaced by the actual generated code
// when building on a system with clang + libbpf development headers.
//
// To regenerate from source:
// cd pkg/warding/ebpf && go generate
// tombGuardMaps holds all BPF maps for the Tomb Guard program.
type tombGuardMaps struct {
Violations *ebpf.Map `ebpf:"violations"`
PathConfig *ebpf.Map `ebpf:"path_config"`
EnforceMode *ebpf.Map `ebpf:"enforce_mode"`
TrustedPids *ebpf.Map `ebpf:"trusted_pids"`
}
// tombGuardPrograms holds all BPF programs for the Tomb Guard.
type tombGuardPrograms struct {
TombGuardFilePermission *ebpf.Program `ebpf:"tomb_guard_file_permission"`
TombGuardInodePermission *ebpf.Program `ebpf:"tomb_guard_inode_permission"`
TraceExecve *ebpf.Program `ebpf:"trace_execve"`
}
// TombGuardObjects is the collection of all Tomb Guard BPF objects.
type TombGuardObjects struct {
tombGuardMaps
tombGuardPrograms
}
// TombGuardProgramSpecs is used for loading programs with custom options.
type TombGuardProgramSpecs struct {
tombGuardPrograms
}
// Close releases all resources held by the Tomb Guard objects.
func (o *TombGuardObjects) Close() {
if o.Violations != nil {
o.Violations.Close()
}
if o.PathConfig != nil {
o.PathConfig.Close()
}
if o.EnforceMode != nil {
o.EnforceMode.Close()
}
if o.TrustedPids != nil {
o.TrustedPids.Close()
}
if o.TombGuardFilePermission != nil {
o.TombGuardFilePermission.Close()
}
if o.TombGuardInodePermission != nil {
o.TombGuardInodePermission.Close()
}
if o.TraceExecve != nil {
o.TraceExecve.Close()
}
}
// loadTombGuardObjects loads Tomb Guard programs from embedded .bpf.o files.
// When the bpf2go generated code is available (via go generate), this is
// replaced by the generated function. This version loads the programs
// dynamically using the cilium/ebpf library's collection loader.
func loadTombGuardObjects(objs *TombGuardObjects, opts *ebpf.CollectionOptions) error {
// Look for pre-compiled .bpf.o files (produced by `make ebpf`).
specPath := filepath.Join("pkg", "warding", "ebpf", "c")
bpfelPath := filepath.Join(specPath, "tomb_guard.bpfel.o")
bpfebPath := filepath.Join(specPath, "tomb_guard.bpfeb.o")
// Determine which target to load based on host endianness.
bpfPath := bpfelPath
if binary.NativeEndian == binary.BigEndian {
bpfPath = bpfebPath
}
// If the pre-compiled object exists, load it.
if _, err := os.Stat(bpfPath); err == nil {
return loadBPFCollection(bpfPath, objs, opts)
}
// No pre-compiled object — create maps and programs programmatically.
// This allows the package to compile and initialize even without
// clang/libbpf on the build machine. The LSM hooks won't be active
// but all the Go API surface works correctly.
return loadTombGuardProgrammatic(objs, opts)
}
// loadTombGuardProgrammatic creates the BPF maps and programs without
// compiled eBPF bytecode. The maps are functional (userspace can write
// to them) but the programs are stubs that won't enforce anything in
// the kernel until real .bpf.o files are provided.
func loadTombGuardProgrammatic(objs *TombGuardObjects, opts *ebpf.CollectionOptions) error {
// Create the BPF maps that the userspace API depends on.
pinPath := ""
if opts.Maps.PinPath != "" {
pinPath = opts.Maps.PinPath
os.MkdirAll(pinPath, 0700)
}
// EnforceMode map (ARRAY, 1 entry, key=u32, value=u32)
em, err := ebpf.NewMap(&ebpf.MapSpec{
Name: "enforce_mode",
Type: ebpf.Array,
KeySize: 4,
ValueSize: 4,
MaxEntries: 1,
Pinning: ebpf.PinByName,
})
if err != nil {
return fmt.Errorf("ebpf: create enforce_mode map: %w", err)
}
if pinPath != "" {
_ = em.Pin(filepath.Join(pinPath, "enforce_mode"))
}
objs.EnforceMode = em
// PathConfig map (ARRAY, 4 entries, key=u32, value=char[128])
pc, err := ebpf.NewMap(&ebpf.MapSpec{
Name: "path_config",
Type: ebpf.Array,
KeySize: 4,
ValueSize: 128,
MaxEntries: 4,
Pinning: ebpf.PinByName,
})
if err != nil {
return fmt.Errorf("ebpf: create path_config map: %w", err)
}
if pinPath != "" {
_ = pc.Pin(filepath.Join(pinPath, "path_config"))
}
objs.PathConfig = pc
// TrustedPids map (HASH, 64 entries, key=u32, value=u32)
tp, err := ebpf.NewMap(&ebpf.MapSpec{
Name: "trusted_pids",
Type: ebpf.Hash,
KeySize: 4,
ValueSize: 4,
MaxEntries: 64,
Pinning: ebpf.PinByName,
})
if err != nil {
return fmt.Errorf("ebpf: create trusted_pids map: %w", err)
}
if pinPath != "" {
_ = tp.Pin(filepath.Join(pinPath, "trusted_pids"))
}
objs.TrustedPids = tp
// Violations perf event array.
v, err := ebpf.NewMap(&ebpf.MapSpec{
Name: "violations",
Type: ebpf.PerfEventArray,
KeySize: 4,
ValueSize: 4,
})
if err != nil {
return fmt.Errorf("ebpf: create violations map: %w", err)
}
objs.Violations = v
// Programs are nil — they'll be loaded from .bpf.o when available.
// The enforcer checks for nil programs before attaching LSM hooks.
objs.TombGuardFilePermission = nil
objs.TombGuardInodePermission = nil
objs.TraceExecve = nil
return nil
}
// ---------------------------------------------------------------------------
// Sorcery Filter — bpf2go generated types
// ---------------------------------------------------------------------------
// sorceryFilterMaps holds all BPF maps for the Sorcery Filter program.
type sorceryFilterMaps struct {
DeviceAllowlist *ebpf.Map `ebpf:"device_allowlist"`
NetworkPolicy *ebpf.Map `ebpf:"network_policy"`
}
// sorceryFilterPrograms holds all BPF programs for the Sorcery Filter.
type sorceryFilterPrograms struct {
SorceryDeviceFilter *ebpf.Program `ebpf:"sorcery_device_filter"`
SorceryConnect4Filter *ebpf.Program `ebpf:"sorcery_connect4_filter"`
SorceryBind4Filter *ebpf.Program `ebpf:"sorcery_bind4_filter"`
SorceryConnect6Filter *ebpf.Program `ebpf:"sorcery_connect6_filter"`
}
// SorceryFilterObjects is the collection of all Sorcery Filter BPF objects.
type SorceryFilterObjects struct {
sorceryFilterMaps
sorceryFilterPrograms
}
// SorceryFilterProgramSpecs is used for loading programs with custom options.
type SorceryFilterProgramSpecs struct {
sorceryFilterPrograms
}
// Close releases all resources held by the Sorcery Filter objects.
func (o *SorceryFilterObjects) Close() {
if o.DeviceAllowlist != nil {
o.DeviceAllowlist.Close()
}
if o.NetworkPolicy != nil {
o.NetworkPolicy.Close()
}
if o.SorceryDeviceFilter != nil {
o.SorceryDeviceFilter.Close()
}
if o.SorceryConnect4Filter != nil {
o.SorceryConnect4Filter.Close()
}
if o.SorceryBind4Filter != nil {
o.SorceryBind4Filter.Close()
}
if o.SorceryConnect6Filter != nil {
o.SorceryConnect6Filter.Close()
}
}
// loadSorceryFilterObjects loads Sorcery Filter programs from embedded .bpf.o.
func loadSorceryFilterObjects(objs *SorceryFilterObjects, opts *ebpf.CollectionOptions) error {
specPath := filepath.Join("pkg", "warding", "ebpf", "c")
bpfelPath := filepath.Join(specPath, "sorcery_filter.bpfel.o")
bpfebPath := filepath.Join(specPath, "sorcery_filter.bpfeb.o")
bpfPath := bpfelPath
if binary.NativeEndian == binary.BigEndian {
bpfPath = bpfebPath
}
if _, err := os.Stat(bpfPath); err == nil {
return loadBPFCollection(bpfPath, objs, opts)
}
return loadSorceryFilterProgrammatic(objs, opts)
}
// loadSorceryFilterProgrammatic creates the cgroup filter maps without
// compiled eBPF bytecode. Maps are functional; programs are nil stubs.
func loadSorceryFilterProgrammatic(objs *SorceryFilterObjects, opts *ebpf.CollectionOptions) error {
pinPath := ""
if opts.Maps.PinPath != "" {
pinPath = opts.Maps.PinPath
os.MkdirAll(pinPath, 0700)
}
// DeviceAllowlist (HASH, 128 entries, key=u32, value=u32)
da, err := ebpf.NewMap(&ebpf.MapSpec{
Name: "device_allowlist",
Type: ebpf.Hash,
KeySize: 4,
ValueSize: 4,
MaxEntries: 128,
Pinning: ebpf.PinByName,
})
if err != nil {
return fmt.Errorf("ebpf: create device_allowlist map: %w", err)
}
if pinPath != "" {
_ = da.Pin(filepath.Join(pinPath, "device_allowlist"))
}
objs.DeviceAllowlist = da
// NetworkPolicy (HASH, 64 entries, key=u32, value=network_rule{12 bytes})
np, err := ebpf.NewMap(&ebpf.MapSpec{
Name: "network_policy",
Type: ebpf.Hash,
KeySize: 4,
ValueSize: 12, // u32 ip + u32 mask + u16 port + u16 protocol
MaxEntries: 64,
Pinning: ebpf.PinByName,
})
if err != nil {
return fmt.Errorf("ebpf: create network_policy map: %w", err)
}
if pinPath != "" {
_ = np.Pin(filepath.Join(pinPath, "network_policy"))
}
objs.NetworkPolicy = np
// Programs are nil — loaded from .bpf.o when available.
objs.SorceryDeviceFilter = nil
objs.SorceryConnect4Filter = nil
objs.SorceryBind4Filter = nil
objs.SorceryConnect6Filter = nil
return nil
}
// ---------------------------------------------------------------------------
// Generic BPF collection loader
// ---------------------------------------------------------------------------
// loadBPFCollection loads a .bpf.o file into the kernel using cilium/ebpf's
// CollectionSpec loader. This is used when pre-compiled objects are available.
func loadBPFCollection(bpfPath string, target interface{}, opts *ebpf.CollectionOptions) error {
spec, err := ebpf.LoadCollectionSpec(bpfPath)
if err != nil {
return fmt.Errorf("ebpf: load spec from %s: %w", bpfPath, err)
}
var coll *ebpf.Collection
if opts != nil {
coll, err = ebpf.NewCollectionWithOptions(spec, *opts)
} else {
coll, err = ebpf.NewCollection(spec)
}
if err != nil {
return fmt.Errorf("ebpf: load collection: %w", err)
}
// Map the loaded collection onto the target struct.
switch t := target.(type) {
case *TombGuardObjects:
t.Violations = coll.Maps["violations"]
t.PathConfig = coll.Maps["path_config"]
t.EnforceMode = coll.Maps["enforce_mode"]
t.TrustedPids = coll.Maps["trusted_pids"]
t.TombGuardFilePermission = coll.Programs["tomb_guard_file_permission"]
t.TombGuardInodePermission = coll.Programs["tomb_guard_inode_permission"]
t.TraceExecve = coll.Programs["trace_execve"]
case *SorceryFilterObjects:
t.DeviceAllowlist = coll.Maps["device_allowlist"]
t.NetworkPolicy = coll.Maps["network_policy"]
t.SorceryDeviceFilter = coll.Programs["sorcery_device_filter"]
t.SorceryConnect4Filter = coll.Programs["sorcery_connect4_filter"]
t.SorceryBind4Filter = coll.Programs["sorcery_bind4_filter"]
t.SorceryConnect6Filter = coll.Programs["sorcery_connect6_filter"]
}
return nil
}
// ---------------------------------------------------------------------------
// EBPFEnforcer
// ---------------------------------------------------------------------------
// EBPFEnforcer manages the lifecycle of the eBPF security programs.
type EBPFEnforcer struct {
mu sync.Mutex
// Tomb guard (LSM + tracepoint)
tombGuardObjs TombGuardObjects
tombGuardSpecs *TombGuardPrograms
// Cgroup filter (device + network)
cgroupFilterObjs SorceryFilterObjects
cgroupFilterSpecs *SorceryFilterPrograms
// Attached links (for cleanup)
links []ebpf.Link
cgroupLinks []ebpf.Link
// Perf reader for violation events
violationReader *ebpf.PerfReader
violationHandler ViolationHandler
stopCh chan struct{}
// State
loaded bool
cgroupAttached bool
}
// NewEnforcer creates a new EBPFEnforcer. Call Load() to compile and load
// the programs into the kernel.
func NewEnforcer() *EBPFEnforcer {
return &EBPFEnforcer{
stopCh: make(chan struct{}),
}
}
// Load compiles (if needed) and loads the eBPF programs into the kernel.
// This must be called before AttachCgroup().
func (e *EBPFEnforcer) Load() error {
e.mu.Lock()
defer e.mu.Unlock()
if e.loaded {
return nil
}
// Remove memory lock limit (required for eBPF map creation).
if err := rlimit.RemoveMemlock(); err != nil {
return fmt.Errorf("ebpf: failed to remove memlock: %w", err)
}
rootDir := os.Getenv("SORCERY_GO_ROOT")
if rootDir == "" {
rootDir = "/var/lib/sorcery-go"
}
mapPinPath := filepath.Join(rootDir, "ebpf", "maps")
opts := &ebpf.CollectionOptions{
Maps: ebpf.MapOptions{
PinPath: mapPinPath,
},
}
// Load Tomb Guard programs.
if err := loadTombGuardObjects(&e.tombGuardObjs, opts); err != nil {
return fmt.Errorf("ebpf: failed to load tomb guard: %w", err)
}
// Load Cgroup Filter programs.
if err := loadSorceryFilterObjects(&e.cgroupFilterObjs, opts); err != nil {
// Cgroup filter is optional (not all systems support it).
// Log but don't fail.
fmt.Fprintf(os.Stderr, "ebpf: warning: cgroup filter not available: %v\n", err)
}
e.loaded = true
return nil
}
// AttachCgroup attaches the cgroup device and network filter programs to
// the given cgroup path. This is the primary attachment point for LXC and
// Podman containers. Firecracker manages its own isolation.
func (e *EBPFEnforcer) AttachCgroup(cgroupPath string) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded {
return errors.New("ebpf: not loaded — call Load() first")
}
if _, err := os.Stat(cgroupPath); os.IsNotExist(err) {
return fmt.Errorf("ebpf: cgroup %s does not exist", cgroupPath)
}
// Attach device filter.
if e.cgroupFilterObjs.SorceryDeviceFilter != nil {
link, err := ebpf.AttachCgroup(ebpf.AttachCgroupOpts{
Path: cgroupPath,
Attach: ebpf.AttachCgroupDev,
Program: e.cgroupFilterObjs.SorceryDeviceFilter,
})
if err != nil {
return fmt.Errorf("ebpf: attach device filter: %w", err)
}
e.cgroupLinks = append(e.cgroupLinks, link)
}
// Attach connect4 filter (egress network).
if e.cgroupFilterObjs.SorceryConnect4Filter != nil {
link, err := ebpf.AttachCgroup(ebpf.AttachCgroupOpts{
Path: cgroupPath,
Attach: ebpf.AttachCgroupInet4Connect,
Program: e.cgroupFilterObjs.SorceryConnect4Filter,
})
if err != nil {
return fmt.Errorf("ebpf: attach connect4 filter: %w", err)
}
e.cgroupLinks = append(e.cgroupLinks, link)
}
e.cgroupAttached = true
return nil
}
// SetEnforceMode sets the enforcement mode for the Tomb guard.
// ModePermissive (0) = log only, ModeEnforcing (1) = deny + log.
func (e *EBPFEnforcer) SetEnforceMode(mode EnforceMode) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded {
return errors.New("ebpf: not loaded")
}
key := uint32(0)
val := uint32(mode)
return e.tombGuardObjs.EnforceMode.Update(key, val, ebpf.UpdateAny)
}
// GetEnforceMode returns the current enforcement mode.
func (e *EBPFEnforcer) GetEnforceMode() (EnforceMode, error) {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded {
return ModePermissive, errors.New("ebpf: not loaded")
}
key := uint32(0)
var val uint32
if err := e.tombGuardObjs.EnforceMode.Lookup(key, &val); err != nil {
return ModePermissive, err
}
return EnforceMode(val), nil
}
// SetTombPath updates the protected Tomb path prefix at runtime.
func (e *EBPFEnforcer) SetTombPath(path string) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded {
return errors.New("ebpf: not loaded")
}
if len(path) >= 128 {
return fmt.Errorf("ebpf: path too long (max 127 chars)")
}
buf := make([]byte, 128)
copy(buf, path)
key := uint32(0)
return e.tombGuardObjs.PathConfig.Update(key, buf, ebpf.UpdateAny)
}
// SetStatePath updates the protected State path prefix at runtime.
func (e *EBPFEnforcer) SetStatePath(path string) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded {
return errors.New("ebpf: not loaded")
}
if len(path) >= 128 {
return fmt.Errorf("ebpf: path too long (max 127 chars)")
}
buf := make([]byte, 128)
copy(buf, path)
key := uint32(1)
return e.tombGuardObjs.PathConfig.Update(key, buf, ebpf.UpdateAny)
}
// AddTrustedPID adds a PID to the trusted allowlist. Trusted processes
// bypass all Tomb protection enforcement (they are the sorcery engine itself).
func (e *EBPFEnforcer) AddTrustedPID(pid uint32) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded {
return errors.New("ebpf: not loaded")
}
return e.tombGuardObjs.TrustedPids.Update(pid, uint32(1), ebpf.UpdateAny)
}
// RemoveTrustedPID removes a PID from the trusted allowlist.
func (e *EBPFEnforcer) RemoveTrustedPID(pid uint32) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded {
return errors.New("ebpf: not loaded")
}
return e.tombGuardObjs.TrustedPids.Delete(pid)
}
// AllowDevice adds a device to the cgroup device allowlist.
// major/minor of 0xFFFF means "any" for that field.
func (e *EBPFEnforcer) AllowDevice(major, minor, access uint32) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded || e.cgroupFilterObjs.DeviceAllowlist == nil {
return errors.New("ebpf: not loaded or cgroup filter unavailable")
}
key := (major << 16) | (minor & 0xFFFF)
return e.cgroupFilterObjs.DeviceAllowlist.Update(key, access, ebpf.UpdateAny)
}
// AddNetworkRule adds a network policy rule for egress filtering.
func (e *EBPFEnforcer) AddNetworkRule(index uint32, ip, mask uint32, port uint16, protocol uint16) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded || e.cgroupFilterObjs.NetworkPolicy == nil {
return errors.New("ebpf: not loaded or cgroup filter unavailable")
}
rule := struct {
IP uint32
Mask uint32
Port uint16
Protocol uint16
}{IP: ip, Mask: mask, Port: port, Protocol: protocol}
return e.cgroupFilterObjs.NetworkPolicy.Update(index, rule, ebpf.UpdateAny)
}
// WatchViolations starts reading violation events from the perf buffer.
// The handler callback is invoked for each event. This blocks until
// StopWatching() is called.
func (e *EBPFEnforcer) WatchViolations(handler ViolationHandler) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded {
return errors.New("ebpf: not loaded")
}
reader, err := ebpf.NewPerfReader(ebpf.PerfReaderOptions{
Map: e.tombGuardObjs.Violations,
ReadSize: 4096,
})
if err != nil {
return fmt.Errorf("ebpf: create perf reader: %w", err)
}
e.violationReader = reader
e.violationHandler = handler
go e.readLoop()
return nil
}
// readLoop continuously reads from the perf buffer.
func (e *EBPFEnforcer) readLoop() {
buf := make([]byte, 4096)
for {
select {
case <-e.stopCh:
return
default:
}
n, err := e.violationReader.Read(buf)
if err != nil {
if errors.Is(err, ebpf.ErrClosed) {
return
}
continue
}
if n == 0 {
continue
}
e.parseAndHandle(buf[:n])
}
}
// parseAndHandle parses raw perf event data and invokes the handler.
func (e *EBPFEnforcer) parseAndHandle(data []byte) {
// Perf event format: [header (8 bytes)][payload]
for len(data) >= 8+280 {
payload := data[8:]
r := bytes.NewReader(payload)
var pid, tid, uid, gid, syscallNr, ppid uint32
binary.Read(r, binary.LittleEndian, &pid)
binary.Read(r, binary.LittleEndian, &tid)
binary.Read(r, binary.LittleEndian, &uid)
binary.Read(r, binary.LittleEndian, &gid)
binary.Read(r, binary.LittleEndian, &syscallNr)
binary.Read(r, binary.LittleEndian, &ppid)
var comm [16]byte
r.Read(comm[:])
var path [256]byte
r.Read(path[:])
var accessMask int32
binary.Read(r, binary.LittleEndian, &accessMask)
v := Violation{
PID: pid,
TID: tid,
UID: uid,
GID: gid,
PPID: ppid,
SyscallNr: syscallNr,
Comm: string(bytes.TrimRight(comm[:], "\x00")),
Path: string(bytes.TrimRight(path[:], "\x00")),
AccessMask: accessMask,
}
if e.violationHandler != nil {
e.violationHandler(v)
}
recordSize := 24 + 16 + 256 + 4
aligned := (recordSize + 7) & ^7
if int(aligned) > len(data) {
break
}
data = data[aligned:]
}
}
// StopWatching stops the violation watch loop.
func (e *EBPFEnforcer) StopWatching() {
select {
case <-e.stopCh:
// Already closed.
default:
close(e.stopCh)
}
if e.violationReader != nil {
e.violationReader.Close()
e.violationReader = nil
}
}
// Close detaches all programs and releases all kernel resources.
func (e *EBPFEnforcer) Close() {
e.StopWatching()
for _, link := range e.cgroupLinks {
link.Close()
}
e.cgroupLinks = nil
for _, link := range e.links {
link.Close()
}
e.links = nil
if e.loaded {
e.tombGuardObjs.Close()
e.cgroupFilterObjs.Close()
e.loaded = false
e.cgroupAttached = false
}
}
// IsLoaded reports whether the eBPF programs are loaded into the kernel.
func (e *EBPFEnforcer) IsLoaded() bool {
e.mu.Lock()
defer e.mu.Unlock()
return e.loaded
}
// IsCgroupAttached reports whether the cgroup filters are attached.
func (e *EBPFEnforcer) IsCgroupAttached() bool {
e.mu.Lock()
defer e.mu.Unlock()
return e.cgroupAttached
}
// SelfTrustedPID adds the current process PID to the trusted allowlist.
func (e *EBPFEnforcer) SelfTrustedPID() error {
return e.AddTrustedPID(uint32(os.Getpid()))
}
// Status returns a human-readable summary of the enforcer state.
func (e *EBPFEnforcer) Status() string {
e.mu.Lock()
defer e.mu.Unlock()
if !e.loaded {
return "eBPF: not loaded"
}
mode, _ := e.GetEnforceMode()
modeStr := "permissive (log only)"
if mode == ModeEnforcing {
modeStr = "enforcing (deny + log)"
}
cgroupStr := "not attached"
if e.cgroupAttached {
cgroupStr = "attached"
}
// Detect if programs were loaded from .bpf.o or are programmatic stubs.
programStr := "maps only (no .bpf.o — compile with clang for full enforcement)"
if e.tombGuardObjs.TombGuardFilePermission != nil {
programStr = "LSM + cgroup filters active"
}
return fmt.Sprintf("eBPF: loaded | mode: %s | cgroup filter: %s | programs: %s",
modeStr, cgroupStr, programStr)
}
// ProbeCapabilities checks if the system supports the required eBPF features.
// Returns a list of available features and any errors.
func ProbeCapabilities() (lsm, cgroupDev, cgroupNet bool, err error) {
// Check LSM support.
lsm = features.HaveLSM() == nil
// Check cgroup BPF support (device + network).
cgroupDev = false
cgroupNet = false
if _, err := os.Stat("/sys/fs/cgroup"); err == nil {
cgroupDev = true
cgroupNet = true
}
return lsm, cgroupDev, cgroupNet, nil
}