This guide covers development setup, code organization, contribution workflow, and extension patterns for AegisBPF.
- Development Environment Setup
- Build System
- Code Organization
- Coding Standards
- Testing
- Adding New Features
- BPF Development
- Debugging
- Performance Profiling
- Release Process
Required:
- Linux kernel 5.8+ with BTF support
- GCC 10+ or Clang 12+ (C++20 support)
- CMake 3.20+
- Ninja build system
- libbpf development headers
- bpftool
- Python 3.8+ (for test scripts)
Optional:
- libsystemd-dev (for journald integration)
- Docker (for containerized builds)
- lcov (for coverage reports)
# Install build dependencies
sudo apt-get update
sudo apt-get install -y \
build-essential \
clang \
llvm \
cmake \
ninja-build \
pkg-config \
libbpf-dev \
bpftool \
libsystemd-dev \
python3 \
python3-jsonschema \
lcov
# Verify kernel requirements
uname -r # Should be 5.8+
cat /sys/kernel/btf/vmlinux | head -c 4 # Should show BTF header
# Verify BPF LSM (for enforce mode development)
cat /sys/kernel/security/lsm | grep bpfsudo dnf install -y \
clang \
llvm \
cmake \
ninja-build \
libbpf-devel \
bpftool \
systemd-devel \
python3 \
python3-jsonschemagit clone https://github.com/your-org/aegisbpf.git
cd aegisbpf
# Configure (Debug build with sanitizers)
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DENABLE_ASAN=ON \
-DENABLE_UBSAN=ON
# Build
cmake --build build
# Run tests
cd build && ctest --output-on-failure| Option | Default | Description |
|---|---|---|
CMAKE_BUILD_TYPE |
Debug | Build type: Debug, Release, RelWithDebInfo |
BUILD_TESTING |
ON | Build test targets |
ENABLE_ASAN |
OFF | Enable AddressSanitizer |
ENABLE_UBSAN |
OFF | Enable UndefinedBehaviorSanitizer |
ENABLE_TSAN |
OFF | Enable ThreadSanitizer |
ENABLE_COVERAGE |
OFF | Enable code coverage |
ENABLE_FUZZING |
OFF | Build fuzzing targets |
SKIP_BPF_BUILD |
OFF | Skip BPF compilation (cross-compile) |
# Debug build (development)
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug
# Release build (production)
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release
# Coverage build
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DENABLE_COVERAGE=ON
# Fuzzing build (requires clang)
CC=clang CXX=clang++ cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DENABLE_FUZZING=ON# Main binary
cmake --build build --target aegisbpf
# Static library (for testing)
cmake --build build --target aegisbpf_lib
# Unit tests
cmake --build build --target aegisbpf_test
# E2E tests (require root)
cmake --build build --target aegisbpf_bypass_test
# Benchmarks
cmake --build build --target aegisbpf_bench
# Fuzzing targets
cmake --build build --target fuzz_policy fuzz_bundle fuzz_networkaegisbpf/
+-- bpf/ # BPF kernel code
| +-- aegis.bpf.c # Main BPF program (includes subsystem headers)
| +-- aegis_common.h # Shared types, constants, and helper functions
| +-- aegis_file.bpf.h # File access LSM hooks (file_open, inode_permission)
| +-- aegis_exec.bpf.h # Exec hooks (execve, bprm_check, file_mmap)
| +-- aegis_net.bpf.h # Network LSM hooks (connect, bind, listen, accept, sendmsg)
| +-- aegis_process.bpf.h # Process lifecycle (fork, exit, process_tree)
+-- src/ # User-space C++ code
| +-- main.cpp # Entry point
| +-- cli_*.cpp/hpp # CLI command handlers
| +-- commands_*.cpp/hpp # Command implementations
| +-- daemon.cpp/hpp # Daemon startup + event loop orchestration
| +-- daemon_*.cpp/hpp # Runtime state, posture, and gating modules
| +-- bpf_ops.cpp/hpp # BPF load/discovery + low-level map ops
| +-- bpf_*.cpp/hpp # Attach, config, integrity, and map helpers
| +-- policy.cpp/hpp # Policy export/write helpers
| +-- policy_*.cpp/hpp # Policy parse + runtime apply/rollback
| +-- network_ops.cpp/hpp # Network rule handling
| +-- crypto.cpp/hpp # Ed25519 signing
| +-- events.cpp/hpp # Event handling
| +-- seccomp.cpp/hpp # Seccomp filter
| +-- sha256.cpp/hpp # SHA256 implementation
| +-- tweetnacl.c/h # TweetNaCl crypto library
| +-- utils.cpp/hpp # Utility functions
| +-- selftest.cpp/hpp # Startup self-tests (map, ringbuf, config validation)
| +-- map_monitor.cpp/hpp # BPF map capacity monitoring and warnings
| +-- proc_scan.cpp/hpp # /proc reconciliation for process_tree
| +-- bpf_signing.cpp/hpp # BPF object hash verification and signing
| +-- binary_hash.cpp/hpp # Binary allow-list hash verification
| +-- rule_engine.cpp/hpp # Hot-loadable JSON detection rules
| +-- plugin.cpp/hpp # Event handler plugin system
| +-- types.hpp # Data structures
| +-- result.hpp # Error handling
| +-- logging.hpp # Logging infrastructure
+-- tests/ # Test code
| +-- test_*.cpp # Unit tests
| +-- e2e/ # End-to-end tests
| +-- fuzz/ # Fuzzing harnesses
| +-- fixtures/ # Test data
+-- docs/ # Documentation
+-- config/ # Configuration templates
+-- packaging/ # Systemd, AppArmor, SELinux
+-- scripts/ # Development and CI scripts
+-- helm/ # Kubernetes Helm chart
+-- .github/workflows/ # CI/CD pipelines
| Pattern | Purpose |
|---|---|
cli_*.cpp |
CLI argument parsing and validation |
commands_*.cpp |
Business logic for CLI commands |
daemon_*.cpp |
Daemon runtime, posture, and enforce-gating helpers |
bpf_*.cpp |
BPF lifecycle helpers (attach, config, integrity, maps) |
policy_*.cpp |
Policy parsing and runtime application helpers |
*_ops.cpp |
Low-level operations still exposed as stable entry points |
*.hpp |
Header files (declarations) |
test_*.cpp |
Unit test files |
fuzz_*.cpp |
Fuzzing harnesses |
types.hpp # Base types (no dependencies)
+-- result.hpp # Error handling (types.hpp)
+-- logging.hpp # Logging (result.hpp)
+-- bpf_ops.hpp # BPF operations
+-- policy.hpp # Policy management
+-- crypto.hpp # Cryptography
+-- ...
- Standard: C++20
- Naming:
- Classes/Structs:
PascalCase - Functions/Methods:
snake_case - Variables:
snake_case - Constants:
kCamelCaseorSCREAMING_SNAKE_CASE - Private members:
member_name_
- Classes/Structs:
- Formatting: Use clang-format with project
.clang-format
Run static analysis locally before opening a PR:
# clang-format
find src tests -name '*.cpp' -o -name '*.hpp' | xargs clang-format --dry-run --Werror
# cppcheck
cppcheck --std=c++20 --enable=all --error-exitcode=1 --inline-suppr \
--suppress=missingIncludeSystem \
--suppress=unmatchedSuppression \
--suppress=syntaxError:tests/test_commands.cpp \
--suppress=syntaxError:tests/test_tracing.cpp \
--suppress=checkersReport \
-I src \
src/ tests/
# clang-tidy (changed C++ files only)
cmake -S . -B build-clang-tidy -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DSKIP_BPF_BUILD=ON
BASE_REF=main BUILD_DIR=build-clang-tidy scripts/run_clang_tidy_changed.sh
# semgrep (changed files only)
BASE_REF=main scripts/run_semgrep_changed.sh
# vendored dependency metadata/audit (TweetNaCl)
scripts/check_vendored_dependencies.sh
# required status-check definitions map to workflow job contexts
python3 scripts/validate_required_checks.py \
--required config/required_checks.txt \
--required config/required_checks_release.txt
# labels referenced by workflows/templates are defined in repo_labels.json
python3 scripts/validate_label_contract.pyAlways use Result<T> for functions that can fail:
// Good: Explicit error handling
Result<InodeId> path_to_inode(const std::string& path) {
struct stat st;
if (stat(path.c_str(), &st) != 0) {
return Error::system(errno, "stat failed for " + path);
}
InodeId id{};
id.ino = st.st_ino;
id.dev = encode_dev(st.st_dev);
return id;
}
// Usage with TRY macro
Result<void> add_block_rule(const std::string& path) {
auto inode = TRY(path_to_inode(path));
TRY(add_to_map(inode));
return {};
}Use structured logging:
// Good: Structured fields
logger().log(SLOG_INFO("Policy applied")
.field("path", path)
.field("rules", static_cast<int64_t>(count)));
// Good: Error with context
logger().log(SLOG_ERROR("BPF load failed")
.field("path", obj_path)
.field("errno", errno)
.error_code(err));
// Bad: Unstructured message
logger().log(SLOG_INFO("Policy " + path + " applied with " + std::to_string(count) + " rules"));For policy lifecycle troubleshooting, you can enable OpenTelemetry-style span logs (start/end with duration and status):
AEGIS_OTEL_SPANS=1 ./build/aegisbpf policy apply config/policy.exampleSpan hierarchy is carried through thread-local trace/span context so nested operations can be correlated in logs:
- CLI spans:
cli.policy_*,cli.network_*,cli.block_*,cli.metrics,cli.health - Daemon spans:
daemon.run,daemon.load_bpf,daemon.attach_programs,daemon.event_loop - BPF spans:
bpf.load,bpf.pin_maps,bpf.attach_all
Each span emits:
otel_span_startwithtrace_id,span_id, optionalparent_span_idotel_span_endwithduration_msandstatus(okorerror)
- Use RAII for all resources
- Prefer
std::string,std::vectorover raw buffers - Use
std::unique_ptrfor owned heap allocations - Never use
new/deletedirectly
// Good: RAII wrapper
class RingBufferGuard {
ring_buffer* rb_;
public:
explicit RingBufferGuard(ring_buffer* rb) : rb_(rb) {}
~RingBufferGuard() { if (rb_) ring_buffer__free(rb_); }
ring_buffer* get() { return rb_; }
};
// Usage
RingBufferGuard rb(ring_buffer__new(fd, callback, nullptr, nullptr));
if (!rb) return Error(...);Located in tests/test_*.cpp. Uses Google Test framework.
#include <gtest/gtest.h>
#include "policy.hpp"
TEST(PolicyTest, ParseValidPolicy) {
PolicyIssues issues;
auto result = parse_policy_file("tests/fixtures/valid.conf", issues);
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->version, 1);
EXPECT_EQ(result->deny_paths.size(), 2);
EXPECT_TRUE(issues.errors.empty());
}
TEST(PolicyTest, RejectInvalidVersion) {
PolicyIssues issues;
auto result = parse_policy_file("tests/fixtures/bad_version.conf", issues);
EXPECT_FALSE(result.ok());
EXPECT_FALSE(issues.errors.empty());
}Run tests:
cd build
ctest --output-on-failure
# Run specific test
./aegisbpf_test --gtest_filter="PolicyTest.*"
# Run with verbose output
./aegisbpf_test --gtest_filter="*" --gtest_print_time=1Require root privileges and real BPF:
sudo ./build/aegisbpf_bypass_testBuild and run fuzzers:
# Build
CC=clang CXX=clang++ cmake -S . -B build -G Ninja -DENABLE_FUZZING=ON
cmake --build build --target fuzz_policy
# Run fuzzer
mkdir -p corpus/policy
./build/fuzz_policy corpus/policy -max_total_time=60Generate coverage report:
cmake -S . -B build -G Ninja -DENABLE_COVERAGE=ON
cmake --build build
cd build && ctest
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report- Create command handler in
src/commands_*.cpp:
// src/commands_myfeature.cpp
#include "commands_myfeature.hpp"
#include "bpf_ops.hpp"
#include "logging.hpp"
namespace aegis {
int cmd_myfeature_do(const std::string& arg) {
TRY(bump_memlock_rlimit());
BpfState state;
TRY(load_bpf(true, false, state));
// Implementation...
logger().log(SLOG_INFO("Feature executed").field("arg", arg));
return 0;
}
} // namespace aegis- Add CLI parser in
src/cli_myfeature.cpp:
// src/cli_myfeature.cpp
#include "cli_myfeature.hpp"
#include "commands_myfeature.hpp"
namespace aegis {
int cli_myfeature(int argc, char** argv) {
if (argc < 2) {
std::cerr << "Usage: aegisbpf myfeature <arg>\n";
return 1;
}
return cmd_myfeature_do(argv[1]);
}
} // namespace aegis- Register in dispatcher (
src/cli_dispatch.cpp):
// Add to dispatch table
{"myfeature", cli_myfeature},- Add to CMakeLists.txt:
set(AEGIS_SOURCES
...
src/cli_myfeature.cpp
src/commands_myfeature.cpp
)- Write tests in
tests/test_myfeature.cpp
- Define in BPF code (
bpf/aegis.bpf.c):
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u64);
__type(value, __u8);
} my_new_map SEC(".maps");- Add pin path (
src/types.hpp):
inline constexpr const char* kMyNewMapPin = "/sys/fs/bpf/aegisbpf/my_new_map";- Add to BpfState (
src/bpf_ops.hpp):
struct BpfState {
// ...
bpf_map* my_new_map = nullptr;
bool my_new_map_reused = false;
};- Load and pin in
load_bpf():
state.my_new_map = bpf_object__find_map_by_name(state.obj, "my_new_map");
// Add to reuse and pin logic...// bpf/aegis.bpf.c
// 1. Include headers
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
// 2. Define types and constants
struct my_event { ... };
// 3. Define maps
struct {
__uint(type, BPF_MAP_TYPE_HASH);
...
} my_map SEC(".maps");
// 4. Helper functions (static __always_inline)
static __always_inline int helper_func(...) { ... }
// 5. BPF programs with SEC() annotations
SEC("lsm/file_open")
int BPF_PROG(handle_file_open, struct file *file) {
...
}
// 6. License
char LICENSE[] SEC("license") = "Dual BSD/GPL";| SEC() | Hook | Can Block |
|---|---|---|
lsm/file_open |
File open | Yes |
lsm/inode_permission |
Inode access | Yes |
lsm/socket_connect |
Outgoing connect | Yes |
lsm/socket_bind |
Socket bind | Yes |
tracepoint/syscalls/sys_enter_openat |
openat syscall | No (audit) |
tracepoint/sched/sched_process_exec |
Process exec | No (audit) |
# View loaded programs
sudo bpftool prog list
# View program details
sudo bpftool prog show id <id>
# Dump program instructions
sudo bpftool prog dump xlated id <id>
# View maps
sudo bpftool map list
sudo bpftool map dump name deny_inode_map
# View pinned objects
ls -la /sys/fs/bpf/aegisbpf/cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DENABLE_ASAN=ON \
-DENABLE_UBSAN=ON# Run under GDB
sudo gdb --args ./build/aegisbpf run --audit
# In GDB:
(gdb) break daemon_run
(gdb) run
(gdb) bt # backtrace on crash# ASAN will report memory errors
ASAN_OPTIONS=detect_leaks=1 sudo ./build/aegisbpf run --audit
# UBSAN reports undefined behavior
UBSAN_OPTIONS=print_stacktrace=1 sudo ./build/aegisbpf run --audit# JSON output shows all structured fields
sudo ./build/aegisbpf run --audit --log-format=json 2>&1 | jq .# Record profile
sudo perf record -g ./build/aegisbpf run --audit &
# ... generate workload ...
sudo perf report
# Flame graph
sudo perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg# Run policy parsing benchmark
./build/aegisbpf_bench --benchmark_filter="BM_Policy*"
# File open latency test
ITERATIONS=1000000 FILE=/etc/hosts scripts/perf_open_bench.sh
# Compare baseline vs. with agent
scripts/perf_compare.sh-
Run all tests:
scripts/dev_check.sh
-
Update version in
CMakeLists.txt -
Update
docs/CHANGELOG.md -
Run release drill:
gh workflow run release-drill.yml
-
Check production readiness:
gh workflow run release-readiness.yml
# Tag the release
git tag -s v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0
# GitHub Actions will:
# 1. Build for all architectures
# 2. Sign with Sigstore
# 3. Generate SBOM
# 4. Create GitHub Release- Major: Breaking API/policy format changes
- Minor: New features, backward compatible
- Patch: Bug fixes only
- eBPF.io - eBPF documentation
- libbpf Documentation
- BPF CO-RE Guide
- Linux BPF LSM
- API_REFERENCE.md - API documentation
- ARCHITECTURE.md - System design
- THREAT_MODEL.md - Security scope and blind spots
- POLICY_SEMANTICS.md - Runtime policy behavior
- QUALITY_GATES.md - CI gate policy and coverage expectations
- CI_EXECUTION_STRATEGY.md - Privileged CI execution model
- TROUBLESHOOTING.md - Common issues
- VENDORED_DEPENDENCIES.md - Vendored dependency tracking