How to Implement eBPF Socket Filters (sk_filter) for Kernel-Level Packet Discard

High-performance packet filtering is a foundational requirement for modern Linux systems exposed to the internet. While iptables (Netfilter) and nftables provide extensive filtering capabilities, they process packets relatively late in the network stack. During massive volumetric DDoS attacks, traversing the stack to reach the Netfilter hooks can exhaust CPU resources and cause system lockups. The solution is eBPF (Extended Berkeley Packet Filter).

Specifically, we can attach an eBPF program directly to a raw socket using the SO_ATTACH_BPF socket option (often referred to as an sk_filter). This allows the kernel to discard malicious packets almost immediately upon ingestion, drastically reducing CPU overhead.

The Role of sk_filter in Linux Networking

The sk_filter program type (BPF_PROG_TYPE_SOCKET_FILTER) evaluates network packets passing through a specific socket. Unlike XDP (eXpress Data Path), which operates at the NIC driver level, socket filters operate slightly higher up but are highly targeted to specific applications binding to sockets.

When an eBPF program is attached to a socket, it receives a pointer to the __sk_buff structure containing the packet data. The program returns an integer: returning 0 instructs the kernel to drop the packet, while returning a positive integer dictates how many bytes of the packet should be passed to user space.

Prerequisites for eBPF Compilation

To compile the eBPF bytecode, you need the LLVM/Clang toolchain and the Linux kernel headers installed.

sudo apt update
sudo apt install clang llvm libbpf-dev linux-headers-$(uname -r)

Step 1: Writing the eBPF Socket Filter Program

We will write a C program that inspects incoming UDP packets. If a packet originates from a specific IP address or matches a specific malicious signature (in this example, we’ll simply check the protocol), we will drop it.

Create a file named filter.c:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>

SEC("socket")
int bpf_prog(struct __sk_buff *skb) {
    // Load the protocol byte from the IPv4 header
    // Offset 14 is the start of the IP header (after Ethernet header)
    // Offset 9 inside the IP header is the protocol field
    
    int proto = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));

    // If the packet is UDP (Protocol 17)
    if (proto == IPPROTO_UDP) {
        // Return 0 to drop the packet completely before user space sees it
        return 0;
    }

    // Return the maximum packet size to accept it
    return -1;
}

char _license[] SEC("license") = "GPL";

Note: The load_byte function is an abstraction provided by the BPF helper libraries. It safely reads bytes from the skb without risking kernel panics.

Step 2: Compiling the eBPF Bytecode

Compile the C source file into eBPF bytecode using Clang. The target must be explicitly set to bpf.

clang -O2 -target bpf -c filter.c -o filter.o

This generates an ELF object file (filter.o) containing the compiled instructions.

Step 3: Writing the User Space Loader Program

Unlike XDP or TC programs which can be loaded via the ip or tc command-line tools, socket filters are typically loaded dynamically by the user-space application that opens the socket. We will write a small Python script using the bcc (BPF Compiler Collection) library to load our program and attach it to a raw socket.

Install the BCC Python bindings:

sudo apt install python3-bpfcc

Create a Python script named load_filter.py:

#!/usr/bin/env python3
from bcc import BPF
import socket
import os

# Load the compiled eBPF object file
bpf = BPF(src_file="filter.c")

# Load the function 'bpf_prog' as a SOCKET_FILTER
func = bpf.load_func("bpf_prog", BPF.SOCKET_FILTER)

# Create a raw socket to sniff all incoming packets
# ETH_P_ALL is 0x0003
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(3))
sock.bind(("eth0", 0))

# Attach the BPF program to the socket using the SO_ATTACH_BPF option
# SO_ATTACH_BPF is usually defined as 50
SO_ATTACH_BPF = 50
sock.setsockopt(socket.SOL_SOCKET, SO_ATTACH_BPF, func.fd)

print("eBPF Socket Filter attached to eth0. Dropping UDP packets...")
print("Press Ctrl+C to exit and detach the filter.")

try:
    while True:
        # The socket is now filtering packets in the kernel.
        # Any accepted packets will arrive here in user space.
        packet = sock.recv(2048)
        print(f"Received {len(packet)} bytes (Not UDP)")
except KeyboardInterrupt:
    print("Detaching filter and exiting.")
finally:
    sock.close()

Step 4: Executing the Filter

Run the Python script with root privileges, as creating raw sockets and loading eBPF programs requires administrative capabilities (specifically CAP_SYS_ADMIN and CAP_NET_RAW).

sudo python3 load_filter.py

While the script is running, open a second terminal and attempt to send UDP packets to the server (e.g., using nc -u or dig). You will observe that the kernel silently discards all incoming UDP traffic intended for that interface before it ever reaches traditional socket queues.

Conclusion

By implementing eBPF sk_filter programs, you can shift packet inspection and discard logic from user space or late-stage kernel hooks directly into the early networking stack. This approach provides a mathematically provable, highly performant mechanism to mitigate volumetric attacks without sacrificing the stability or CPU resources of the Linux host.

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.