Skip to content

Latest commit

Β 

History

History
250 lines (198 loc) Β· 7.16 KB

File metadata and controls

250 lines (198 loc) Β· 7.16 KB

Architecture Overview

Before we build your first eBPF tool, let's understand how eBPF applications are structured. This knowledge will serve as the foundation for everything you'll build.

πŸ—οΈ eBPF Application Architecture

Every eBPF application in this project follows the same architectural pattern:

graph TB
    subgraph "Userspace Application"
        A[Go CLI Application] --> B[eBPF Loader]
        B --> C[Ring Buffer Reader]
        C --> D[Event Processor]
        D --> E[Output Formatter]
    end
    
    subgraph "Kernel Space"
        F[eBPF Program] --> G[eBPF Maps]
        H[Kernel Events] --> F
    end
    
    G --> C
    B --> F
    
    style A fill:#e8f5e8
    style F fill:#f3e5f5
    style G fill:#fff3e0
Loading

🧩 Components Breakdown

1. Kernel Space Components

eBPF Program (bpf/*.c)

  • Purpose: Runs in kernel space, triggered by events
  • Language: C with eBPF-specific extensions
  • Limitations: Limited stack space, no loops, must be verifiable
SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx) {
    // This function runs in kernel space!
    // It has access to kernel data structures
    return 0;
}

eBPF Maps (SEC(".maps"))

  • Purpose: Data exchange between kernel and userspace
  • Types: Ring buffers, hash maps, arrays, etc.
  • Shared: Both kernel and userspace can access
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 1 << 24);  // 16MB buffer
} events SEC(".maps");

2. Userspace Components

Go CLI Application (cmd/*.go)

  • Purpose: User interface and program orchestration
  • Framework: Cobra CLI for command-line interface
  • Responsibilities: Argument parsing, program lifecycle management

eBPF Loader

  • Purpose: Load eBPF programs into the kernel
  • Library: Cilium eBPF
  • Tasks: Program verification, map creation, attachment

Ring Buffer Reader

  • Purpose: Receive events from kernel
  • Mechanism: Zero-copy ring buffer for high performance
  • Processing: Event deserialization and filtering

πŸ“ Project Structure Deep Dive

ebee/
β”œβ”€β”€ bpf/                       # eBPF kernel programs
β”‚   β”œβ”€β”€ execsnoop.c           # Process monitoring eBPF program
β”‚   β”œβ”€β”€ rmdetect.c            # File deletion eBPF program
β”‚   └── headers/              # Kernel headers
β”‚       └── vmlinux.h         # Kernel type definitions
β”œβ”€β”€ cmd/                       # Go userspace applications
β”‚   β”œβ”€β”€ execsnoop.go          # Process monitoring CLI
β”‚   β”œβ”€β”€ rmdetect.go           # File deletion CLI
β”‚   └── root.go               # CLI root command
β”œβ”€β”€ main.go                    # Application entry point
└── Makefile                   # Build automation

File Relationships

graph LR
    A[bpf/execsnoop.c] --> B[execsnoopObjects]
    B --> C[cmd/execsnoop.go]
    C --> D[main.go]
    E[vmlinux.h] --> A
    F[Makefile] --> B
    
    style A fill:#f3e5f5
    style C fill:#e8f5e8
    style E fill:#fff3e0
Loading

πŸ”„ Data Flow

Understanding how data flows through an eBPF application is crucial:

1. Event Trigger

Kernel Event (e.g., process execution) β†’ eBPF Program Triggered

2. Data Capture

// In kernel space (execsnoop.c)
struct data_t *data = bpf_ringbuf_reserve(&events, sizeof(*data), 0);
data->pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
bpf_get_current_comm(&data->comm, sizeof(data->comm));
bpf_ringbuf_submit(data, 0);

3. Data Transfer

Ring Buffer: Kernel Space β†’ Userspace (zero-copy)

4. Data Processing

// In userspace (execsnoop.go)
record, err := rd.Read()
var data exec_data_t
binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &data)

5. Data Display

fmt.Printf("%d\t%s\n", data.Pid, string(data.Comm[:]))

πŸ”— Component Interaction

Compilation Flow

sequenceDiagram
    participant M as Makefile
    participant C as eBPF C Code
    participant G as Go Code
    participant O as Objects
    
    M->>C: bpf2go generates Go bindings
    C->>O: Compiles to eBPF bytecode
    O->>G: Embeds bytecode in Go
    G->>G: Compiles to executable
Loading

Runtime Flow

sequenceDiagram
    participant U as User
    participant G as Go App
    participant K as Kernel
    participant E as eBPF Program
    
    U->>G: ./ebee execsnoop
    G->>K: Load eBPF program
    K->>K: Verify program safety
    G->>K: Attach to tracepoint
    K->>E: Event triggers eBPF
    E->>G: Send data via ring buffer
    G->>U: Display formatted output
Loading

🎯 Design Patterns

1. Event-Driven Architecture

  • eBPF programs are reactive - they respond to kernel events
  • No polling or active monitoring from userspace
  • Efficient and low-overhead

2. Producer-Consumer Pattern

  • Producer: eBPF program generates events in kernel
  • Consumer: Go application processes events in userspace
  • Buffer: Ring buffer decouples producer from consumer

3. Code Generation

  • bpf2go tool generates Go bindings from C code
  • Ensures type safety between C structs and Go structs
  • Embeds eBPF bytecode in Go binary

πŸ› οΈ Build Process

Understanding the build process helps with debugging and customization:

1. Code Generation

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target native execsnoop ../bpf/execsnoop.c

This generates:

  • execsnoop_bpfel.go - eBPF program loader
  • execsnoop_bpfel.o - eBPF bytecode

2. Compilation Steps

make generate  # Generate Go bindings from C
make build     # Compile Go application

3. Generated Code Example

// Auto-generated by bpf2go
type execsnoopObjects struct {
    TraceExec *ebpf.Program `ebpf:"trace_exec"`
    Events    *ebpf.Map     `ebpf:"events"`
}

func loadExecsnoopObjects(obj *execsnoopObjects, opts *ebpf.CollectionOptions) error {
    // Load eBPF program and maps
}

πŸ” Key Concepts to Remember

!!! tip "Architecture Principles" 1. Separation of Concerns: Kernel space handles data collection, userspace handles presentation 2. Type Safety: Shared data structures must match between C and Go 3. Resource Management: Always clean up eBPF resources (programs, maps, links) 4. Error Handling: eBPF operations can fail - handle errors gracefully

!!! warning "Limitations to Consider" 1. eBPF Program Size: Limited to ~1M instructions 2. Stack Space: Only 512 bytes of stack in eBPF programs 3. No Loops: eBPF verifier prevents unbounded loops 4. Helper Functions: Only approved kernel helpers can be called

πŸ“š What's Next?

Now that you understand the architecture, let's build your first tool step by step:

  1. Writing eBPF C Code - Create the kernel component
  2. Go Integration - Build the userspace application
  3. Build and Test - Put it all together

Understanding this architecture will make the following sections much easier to follow. Every tool in this project follows these same patterns - master them once, and you can build any eBPF tool! πŸš€