872 lines
25 KiB
Go
Executable File
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
|
|
} |