What is eBPF?
eBPF (extended Berkeley Packet Filter) is a revolutionary Linux kernel technology that allows programs to run in kernel space without requiring kernel modules or modifications. Originally designed for packet filtering, eBPF has evolved into a general-purpose execution engine for safe, efficient kernel instrumentation.
The eBPF Event Interceptor uses eBPF to monitor TCP and UDP network activity with minimal overhead and no kernel modifications.
Why eBPF for Network Monitoring?
Traditional Approaches vs eBPF
Kernel Modules Risks:
Can crash entire system
No safety verification
Complex to maintain
Security vulnerabilities
Benefits:
Full kernel access
Maximum flexibility
eBPF Programs Benefits:
Verified safe before loading
Cannot crash kernel
Sandboxed execution
No recompilation needed
Constraints:
Limited instruction set
Stack size limits
Bounded loops only
Key Advantages
Safety: eBPF verifier ensures programs terminate and don’t crash the kernel
Performance: Runs in kernel context without context switching
Flexibility: Load/unload programs dynamically without rebooting
Observability: Zero application code changes required
Security: Cannot access arbitrary kernel memory
BCC: BPF Compiler Collection
This project uses BCC (BPF Compiler Collection) to simplify eBPF program development.
BCC version is displayed at startup: tcpTracer Ver 1.03e with BCC 0.x.x
UDP Tracer Ver 1.04b with BCC 0.x.x
BCC Components
┌─────────────────────────────────────────────────────────┐
│ Application Code │
│ (event.cc, udpTracer.cc) │
└────────────────────┬────────────────────────────────────┘
│
│ #include "BPF.h"
▼
┌─────────────────────────────────────────────────────────┐
│ BCC Framework (libbcc) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Compiler │ │ Verifier │ │ Perf Buffer │ │
│ │ (Clang) │→ │ Interface │→ │ Manager │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Linux Kernel │
│ ┌──────────────────────────────────────────────────┐ │
│ │ eBPF Virtual Machine │ │
│ │ - Verifier checks program safety │ │
│ │ - JIT compiler optimizes for native execution │ │
│ │ - Runtime executes attached probes │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
BCC API Usage
1. Initialize BPF Program:
ebpf ::BPF bpf;
auto init_res = bpf . init (BPF_PROGRAM);
if ( init_res . code () != 0 ) {
std ::cerr << init_res . msg () << std ::endl;
exit ( 1 );
}
2. Attach Kprobe:
// TCP: Attach to tcp_set_state kernel function
auto attach = bpf . attach_kprobe ( "tcp_set_state" , "kprobe__tcp_set_state" );
// UDP: Attach to multiple functions
bpf . attach_kprobe ( "udp_sendmsg" , "kprobe__udp_sendmsg" );
bpf . attach_kprobe ( "udp_recvmsg" , "kprobe_udp_recvmsg" );
// Return probe captures function return value
bpf . attach_kprobe ( "udp_recvmsg" , "kretprobe__udp_recvmsg" ,
0 , BPF_PROBE_RETURN, 0 );
3. Open Perf Buffer:
auto openResults = bpf . open_perf_buffer ( "tcpEvents" , & handle_output);
if ( openResults . code ()) {
std ::cerr << openResults . msg () << std ::endl;
exit ( 1 );
}
4. Poll for Events:
while ( 1 ) {
bpf . poll_perf_buffer ( "tcpEvents" ); // Blocks until event arrives
}
5. Cleanup:
bpf . free_bcc_memory (); // Free compiler resources
bpf . detach_kprobe ( "tcp_set_state" );
Kprobes: Dynamic Kernel Instrumentation
What are Kprobes?
Kprobes allow eBPF programs to execute when specific kernel functions are called. Two types exist:
kprobe: Executes at function entry (before function runs)
kretprobe: Executes at function return (after function completes)
Kprobes attach to kernel function names, which can change between kernel versions. This implementation targets common functions stable across kernel 4.x-5.x+.
TCP Kprobes
The TCP interceptor attaches to a single critical function:
#define FN_NAME "tcp_set_state"
bpf . attach_kprobe (FN_NAME, "kprobe__tcp_set_state" );
Why tcp_set_state?
This function is called whenever a TCP connection changes state (SYN_SENT → ESTABLISHED → FIN_WAIT1 → etc.). It provides:
Connection establishment events
Connection teardown events
Access to struct sock* containing all connection metadata
TCP State Machine:
CLOSED → LISTEN → SYN_RECV → ESTABLISHED → FIN_WAIT1 →
FIN_WAIT2 → TIME_WAIT → CLOSE
↑ tcp_set_state() called at each transition
UDP Kprobes
UDP is connectionless, so the interceptor tracks operations across multiple functions:
// Connection setup (for connected UDP sockets)
ip4_datagram_connect → kprobe_ip4_datagram_connect
ip6_datagram_connect → kprobe_ip6_datagram_connect
// Data transmission
udp_sendmsg → kprobe__udp_sendmsg (IPv4 send)
udpv6_sendmsg → kprobe__udpv6_sendmsg (IPv6 send)
// Data reception
udp_recvmsg → kprobe_udp_recvmsg (Entry probe)
→ kretprobe__udp_recvmsg (Return probe)
udpv6_recvmsg → kprobe__udpv6_recvmsg (Entry probe)
→ kretprobe__udpv6_recvmsg (Return probe)
// Socket cleanup
udp_destruct_sock → kprobe_udp_destruct_sock (Final stats)
Send Operation: int kprobe__udp_sendmsg ( struct pt_regs * ctx , struct sock * sk ,
struct msghdr * msg , size_t len ) {
struct event_t * eventPtr = otherHash . lookup ( & pointerInt);
if (eventPtr) {
eventPtr -> tx_b += len; // Accumulate bytes sent
eventPtr -> txPkts += 1 ; // Count packets
bpfHelper (eventPtr); // Update timestamp, PID, UID
skHelper (ctx, eventPtr, sk); // Extract socket info
}
return 0 ;
}
Receive Operation (Return Probe): int kretprobe__udp_recvmsg ( struct pt_regs * ctx ) {
int ret = PT_REGS_RC (ctx); // Get return value = bytes received
if (ret > 0 ) {
// Lookup socket pointer saved in entry probe
unsigned long * found = magic . lookup ( & pidTgid);
struct event_t * eventPtr = otherHash . lookup ( & sockPtr);
if (eventPtr) {
eventPtr -> rx_b += ret; // Accumulate bytes received
eventPtr -> rxPkts += 1 ; // Count packets
}
}
return 0 ;
}
Kprobe Attachment Process
From setupBPF() in udpTracer.cc:394-471:
int setupBPF () {
// 1. Initialize BPF subsystem
auto init_res = bpf . init (BPF_PROGRAM);
// 2. Attach each kprobe, store names for cleanup
std ::vector < std ::string > fNamesVector;
auto d = bpf . attach_kprobe ( "ip6_datagram_connect" ,
"kprobe_ip6_datagram_connect" );
if ( d . code ()) {
std ::cerr << d . msg () << std ::endl;
return 1 ;
}
fNamesVector . push_back ( "ip6_datagram_connect" );
// ... repeat for all 9 probes ...
// 3. Display attached probes
for ( auto probeName : fNamesVector) {
std ::cout << "Attached: " << probeName << std ::endl;
}
// 4. Setup perf buffer and start polling
bpf . open_perf_buffer ( "bpfPerfBuffer" , & handle_output);
while ( 1 ) {
bpf . poll_perf_buffer ( "bpfPerfBuffer" );
}
}
eBPF Program Structure
TCP eBPF Program
The TCP eBPF code is loaded from external source (passed to setupBPF()). Key elements:
#include <uapi/linux/ptrace.h>
#include <linux/tcp.h>
#include <net/sock.h>
#include <bcc/proto.h>
// Perf buffer declaration
BPF_PERF_OUTPUT (tcpEvents);
// Main kprobe function
int kprobe__tcp_set_state ( struct pt_regs * ctx , struct sock * sk , int state ) {
// Read socket information
u16 family = sk -> __sk_common . skc_family ;
u16 dport = sk -> __sk_common . skc_dport ;
// Get process context
u64 pid_tgid = bpf_get_current_pid_tgid ();
u32 pid = pid_tgid >> 32 ;
u32 uid = bpf_get_current_uid_gid () & 0x ffffffff ;
// Build event structure
struct event_t event = {};
event . pid = pid;
event . UserId = uid;
event . EventTime = bpf_ktime_get_ns ();
event . family = family;
event . DPT = ntohs (dport);
// Submit to user space
tcpEvents . perf_submit (ctx, & event, sizeof (event));
return 0 ;
}
UDP eBPF Program
The UDP program is embedded directly in udpTracer.cc:58-386:
const char * BPF_PROGRAM = R"(
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
// Data structures
struct event_t {
u16 family;
u32 pid;
u64 EventTime;
u16 SPT, DPT;
unsigned __int128 saddr, daddr;
u64 rx_b, tx_b;
u32 rxPkts, txPkts;
uintptr_t sockPtr;
};
// Perf buffer
BPF_PERF_OUTPUT(bpfPerfBuffer);
// Hash maps for state tracking
BPF_HASH(magic, u64, unsigned long); // pid_tgid → sock*
BPF_HASH(otherHash, uintptr_t, struct event_t); // sock* → event
// Helper to populate event fields
static void bpfHelper(struct event_t *eventPtr) {
if (!eventPtr->pid)
eventPtr->pid = bpf_get_current_pid_tgid() >> 32;
if (!eventPtr->UserId)
eventPtr->UserId = bpf_get_current_uid_gid() & 0xffffffff;
if (!eventPtr->EventTime)
eventPtr->EventTime = bpf_ktime_get_ns();
if (!eventPtr->task[0])
bpf_get_current_comm(eventPtr->task, sizeof(eventPtr->task));
}
// Helper to extract socket information
static void skHelper(struct pt_regs *ctx, struct event_t *eventPtr,
struct sock *sk) {
eventPtr->family = sk->__sk_common.skc_family;
eventPtr->SPT = sk->__sk_common.skc_num;
if (eventPtr->family == AF_INET) {
eventPtr->saddr = sk->__sk_common.skc_rcv_saddr;
eventPtr->daddr = sk->__sk_common.skc_daddr;
} else if (eventPtr->family == AF_INET6) {
bpf_probe_read_kernel(&eventPtr->saddr, sizeof(eventPtr->saddr),
sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
bpf_probe_read_kernel(&eventPtr->daddr, sizeof(eventPtr->daddr),
sk->__sk_common.skc_v6_daddr.in6_u.u6_addr32);
}
u16 dport = sk->__sk_common.skc_dport;
eventPtr->DPT = ntohs(dport);
bpfPerfBuffer.perf_submit(ctx, eventPtr, sizeof(*eventPtr));
}
// ... kprobe implementations ...
)" ;
The UDP program uses BPF hash maps (BPF_HASH) to maintain state across multiple kprobe invocations, tracking per-socket statistics.
eBPF Helper Functions
eBPF programs can only call approved helper functions:
Context Helpers
// Get current process ID (PID) and thread group ID (TGID)
u64 pid_tgid = bpf_get_current_pid_tgid ();
u32 pid = pid_tgid >> 32 ; // Upper 32 bits = PID
u32 tgid = pid_tgid & 0x FFFFFFFF ; // Lower 32 bits = TGID
// Get user ID and group ID
u64 uid_gid = bpf_get_current_uid_gid ();
u32 uid = uid_gid & 0x FFFFFFFF ; // Lower 32 bits = UID
// Get high-resolution timestamp (nanoseconds since boot)
u64 timestamp = bpf_ktime_get_ns ();
// Get process command name (e.g., "nginx", "curl")
char task [ 16 ];
bpf_get_current_comm ( & task , sizeof (task));
Memory Access Helpers
// Safely read kernel memory (required for IPv6 addresses)
int ret = bpf_probe_read_kernel ( & dest , size, & src );
if (ret < 0 ) {
// Read failed - memory not accessible
}
Direct pointer dereferencing is only allowed for kernel structures passed as arguments. Use bpf_probe_read_kernel() for nested structures to avoid verifier errors.
Perf Buffer Helpers
// Submit event to user space
BPF_PERF_OUTPUT (buffer_name);
buffer_name. perf_submit (ctx, & event , sizeof (event));
Map Operations
// Declare hash map
BPF_HASH (map_name, key_type, value_type);
// Lookup value
value_type * val = map_name. lookup ( & key );
if (val) {
// Key exists, use *val
}
// Update/insert
map_name. update ( & key , & value );
// Delete
map_name. delete ( & key );
Safety and Verification
eBPF Verifier Checks
Before loading, the kernel’s eBPF verifier ensures:
Bounded Execution: All loops have maximum iteration limits
Memory Safety: All memory accesses are validated
Null Pointer Checks: Pointers verified before dereferencing
Stack Limits: Stack usage < 512 bytes
Instruction Limits: Program < 1 million instructions
No Infinite Loops: All code paths reach exit
If verification fails, bpf.init() returns an error with the verifier’s detailed analysis.
What eBPF Cannot Do
❌ Modify kernel code or data structures
❌ Call arbitrary kernel functions
❌ Access user space memory directly
❌ Use floating point operations
❌ Create unbounded loops
❌ Allocate dynamic memory
What eBPF Can Do
✅ Read kernel structures (read-only)
✅ Write to maps and perf buffers
✅ Call approved helper functions
✅ Make bounded decisions
✅ Aggregate statistics
✅ Filter events
Overhead Analysis
Kprobe Overhead
Entry: ~1-2μs
Return: ~2-3μs
Per packet cost: minimal
CPU Impact
Idle system: < 0.1%
Busy system: 1-3%
No network slowdown
Memory Usage
eBPF maps: ~100KB
Perf buffers: ~2MB
Queue: ~1MB (1024 events)
JIT Compilation
Modern kernels compile eBPF bytecode to native machine code:
# Check if JIT is enabled
$ cat /proc/sys/net/core/bpf_jit_enable
1 # Enabled
# View JIT-compiled programs
$ bpftool prog dump jited id < prog_i d >
JIT compilation reduces eBPF instruction execution time by 3-5x.
Loading and Attaching eBPF Programs
Complete Initialization Flow
Error Handling
Every BCC operation returns a status object:
auto attach = bpf . attach_kprobe ( "tcp_set_state" , "kprobe__tcp_set_state" );
if ( attach . code () != 0 ) {
// Common errors:
// - Function not found in kernel (kernel version mismatch)
// - Program already attached
// - Insufficient permissions (need CAP_SYS_ADMIN)
std ::cerr << "Error: " << attach . msg () << std ::endl;
return 1 ;
}
eBPF operations require CAP_BPF (kernel 5.8+) or CAP_SYS_ADMIN. Run with sudo or grant capabilities: sudo setcap cap_sys_admin+ep ./tcpEvent.so
Memory Cleanup
Freeing BCC Resources
From event.cc:206-210:
if ( bpf . free_bcc_memory ()) {
std ::cerr << "Failed to free llvm/clang memory" << std ::endl;
exit ( 1 );
}
free_bcc_memory() releases LLVM compiler resources (~50-100MB) after program loading. This is safe because the compiled eBPF bytecode is already in the kernel.
Next Steps
Event Collection Learn about event data structures and collection mechanics
Architecture Review the complete system architecture