{"schemaVersion":1,"id":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing","title":"eBPF and XDP: High-Performance Packet Processing and DDoS Mitigation at Line Rate","description":"High-performance packet processing with eBPF and XDP, writing XDP programs in C, AF_XDP sockets, DDoS mitigation at line rate, and peeking inside Cilium's dataplane.","language":"en","datePublished":"2026-02-17","dateModified":null,"contentFormat":"text/markdown","content":"Networking\n\n# eBPF and XDP: High-Performance Packet Processing and DDoS Mitigation at Line Rate\n\n2026-02-179 min read\n\n[Lucio Durán](<https://lucioduran.com/>)\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/bpf-xdp-high-performance-packet-processing>)\n\n## Why XDP Is Different From Everything Else\n\nThe Linux networking stack processes packets through a long chain: NIC hardware → NIC driver → sk\\_buff allocation → netfilter/iptables → socket receive buffer → application. Each step copies data, allocates memory, and burns CPU.\n\nXDP (eXpress Data Path) hooks into the NIC driver, before sk\\_buff allocation. When a packet arrives, the NIC driver calls your XDP program with a raw pointer to the packet data. Your program returns one of five actions:\n\n```c\nXDP_PASS // Continue normal processing (send to kernel stack)\nXDP_DROP // Drop the packet immediately (no allocation, no copy)\nXDP_TX // Bounce the packet back out the same NIC\nXDP_REDIRECT // Send to another NIC, CPU, or AF_XDP socket\nXDP_ABORTED // Error occurred, drop and log\n```\n\nThe critical path for DDoS mitigation is ` XDP_DROP `. The packet never gets an sk\\_buff, never touches iptables, never reaches the network stack. The NIC driver just moves on to the next packet. On a modern NIC, this happens at line rate.\n\n## Writing Your First XDP Program\n\nHere's a minimal XDP program that drops all UDP traffic on port 53 (DNS amplification attacks):\n\n```c\n// xdp_filter.c\n#include <linux/bpf.h>\n#include <linux/if_ether.h>\n#include <linux/ip.h>\n#include <linux/udp.h>\n#include <bpf/bpf_helpers.h>\n\nSEC(\"xdp\")\nint xdp_dns_filter(struct xdp_md *ctx)\n{\n void *data = (void *)(long)ctx->data;\n void *data_end = (void *)(long)ctx->data_end;\n\n // Parse Ethernet header\n struct ethhdr *eth = data;\n if ((void *)(eth + 1) > data_end)\n return XDP_PASS;\n\n if (eth->h_proto != __constant_htons(ETH_P_IP))\n return XDP_PASS;\n\n // Parse IP header\n struct iphdr *ip = (void *)(eth + 1);\n if ((void *)(ip + 1) > data_end)\n return XDP_PASS;\n\n if (ip->protocol != IPPROTO_UDP)\n return XDP_PASS;\n\n // Parse UDP header\n struct udphdr *udp = (void *)ip + (ip->ihl * 4);\n if ((void *)(udp + 1) > data_end)\n return XDP_PASS;\n\n // Drop DNS responses (source port 53), typical amplification attack\n if (udp->source == __constant_htons(53))\n return XDP_DROP;\n\n return XDP_PASS;\n}\n\nchar _license[] SEC(\"license\") = \"GPL\";\n```\n\nEvery bounds check is mandatory. The BPF verifier will reject your program if you access ` ip->protocol ` without first proving that ` (void *)(ip + 1) <= data_end `. This is annoying the first time and reassuring every time after, the verifier is the reason BPF programs can't crash the kernel.\n\nCompile and load:\n\n```bash\n# Compile to BPF bytecode\nclang -O2 -target bpf -c xdp_filter.c -o xdp_filter.o\n\n# Attach to interface (driver mode for performance)\nip link set dev eth0 xdpdrv obj xdp_filter.o sec xdp\n\n# Verify it's loaded\nip link show eth0\n# ... xdp/id:42 ...\n\n# Detach when done\nip link set dev eth0 xdp off\n```\n\n## Real DDoS Mitigation: Rate Limiting With BPF Maps\n\nThe DNS filter above is too crude. Real DDoS mitigation needs rate limiting, allow some traffic from each source, but drop when a source exceeds a threshold. BPF maps provide shared state between the kernel-side XDP program and userspace:\n\n```c\n// Rate limiter with per-source-IP counters\nstruct rate_limit_entry {\n __u64 packet_count;\n __u64 last_reset_ns;\n};\n\nstruct {\n __uint(type, BPF_MAP_TYPE_LRU_HASH);\n __uint(max_entries, 1000000); // Track up to 1M source IPs\n __type(key, __u32); // Source IP (IPv4)\n __type(value, struct rate_limit_entry);\n} rate_limits SEC(\".maps\");\n\n// Configuration map, userspace can update thresholds without reloading\nstruct {\n __uint(type, BPF_MAP_TYPE_ARRAY);\n __uint(max_entries, 1);\n __type(key, __u32);\n __type(value, __u64); // Max packets per second per IP\n} config SEC(\".maps\");\n\nSEC(\"xdp\")\nint xdp_rate_limiter(struct xdp_md *ctx)\n{\n void *data = (void *)(long)ctx->data;\n void *data_end = (void *)(long)ctx->data_end;\n\n struct ethhdr *eth = data;\n if ((void *)(eth + 1) > data_end)\n return XDP_PASS;\n\n if (eth->h_proto != __constant_htons(ETH_P_IP))\n return XDP_PASS;\n\n struct iphdr *ip = (void *)(eth + 1);\n if ((void *)(ip + 1) > data_end)\n return XDP_PASS;\n\n __u32 src_ip = ip->saddr;\n\n // Get rate limit threshold from config\n __u32 config_key = 0;\n __u64 *max_pps = bpf_map_lookup_elem(&config, &config_key);\n __u64 threshold = max_pps ? *max_pps : 10000; // Default: 10k pps\n\n // Get or create rate limit entry for this source IP\n struct rate_limit_entry *entry = bpf_map_lookup_elem(&rate_limits, &src_ip);\n\n __u64 now = bpf_ktime_get_ns();\n\n if (entry) {\n // Reset counter every second\n if (now - entry->last_reset_ns > 1000000000ULL) {\n entry->packet_count = 1;\n entry->last_reset_ns = now;\n return XDP_PASS;\n }\n\n entry->packet_count++;\n\n if (entry->packet_count > threshold) {\n return XDP_DROP;\n }\n } else {\n // First packet from this IP, create entry\n struct rate_limit_entry new_entry = {\n .packet_count = 1,\n .last_reset_ns = now,\n };\n bpf_map_update_elem(&rate_limits, &src_ip, &new_entry, BPF_ANY);\n }\n\n return XDP_PASS;\n}\n```\n\nThe ` BPF_MAP_TYPE_LRU_HASH ` is key here. During a DDoS, you'll see millions of unique source IPs (spoofed). A regular hash map would fill up and reject new entries, letting new attack IPs through untracked. The LRU map evicts the oldest entries, which are typically attack IPs that have already been rate-limited.\n\n## AF\\_XDP: When You Need Userspace Processing\n\nSometimes you need to do more than just drop or pass. AF\\_XDP (Address Family XDP) lets you redirect packets to a userspace application through a zero-copy ring buffer, bypassing the entire kernel network stack:\n\n```c\n// XDP program that redirects specific traffic to userspace via AF_XDP\nstruct {\n __uint(type, BPF_MAP_TYPE_XSKMAP);\n __uint(max_entries, 64); // One per RX queue\n __type(key, __u32);\n __type(value, __u32);\n} xsks_map SEC(\".maps\");\n\nSEC(\"xdp\")\nint xdp_af_xdp_redirect(struct xdp_md *ctx)\n{\n void *data = (void *)(long)ctx->data;\n void *data_end = (void *)(long)ctx->data_end;\n\n struct ethhdr *eth = data;\n if ((void *)(eth + 1) > data_end)\n return XDP_PASS;\n\n if (eth->h_proto != __constant_htons(ETH_P_IP))\n return XDP_PASS;\n\n struct iphdr *ip = (void *)(eth + 1);\n if ((void *)(ip + 1) > data_end)\n return XDP_PASS;\n\n // Redirect TCP traffic on port 8080 to userspace\n if (ip->protocol == IPPROTO_TCP) {\n struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);\n if ((void *)(tcp + 1) > data_end)\n return XDP_PASS;\n\n if (tcp->dest == __constant_htons(8080)) {\n return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_PASS);\n }\n }\n\n return XDP_PASS;\n}\n```\n\nThe userspace side uses ` libxdp ` or raw AF\\_XDP sockets:\n\n```c\n// Simplified AF_XDP userspace receiver\n#include <xdp/xsk.h>\n#include <xdp/libxdp.h>\n\nvoid process_packets(struct xsk_socket *xsk) {\n struct xsk_ring_cons *rx = &xsk->rx;\n struct xsk_ring_prod *fq = &xsk->umem->fq;\n\n unsigned int received = xsk_ring_cons__peek(rx, BATCH_SIZE, &idx);\n if (!received)\n return;\n\n for (int i = 0; i < received; i++) {\n const struct xdp_desc *desc = xsk_ring_cons__rx_desc(rx, idx + i);\n uint8_t *packet = xsk_umem__get_data(xsk->umem->buffer, desc->addr);\n uint32_t len = desc->len;\n\n // Process the packet in userspace, full packet data, zero copy\n handle_packet(packet, len);\n }\n\n xsk_ring_cons__release(rx, received);\n\n // Return frames to the fill queue for reuse\n xsk_ring_prod__submit(fq, received);\n}\n```\n\nAF\\_XDP gives you 8-12 million packets per second per core in userspace. That's about 10x faster than ` recvmsg() ` on a raw socket, because there's no syscall overhead, no sk\\_buff allocation, and no data copying, the packet data stays in the same memory region shared between the NIC and your application.\n\n## Inside Cilium's Dataplane\n\n[Cilium](<https://cilium.io/>) is the most sophisticated production use of BPF for networking. It replaces kube-proxy entirely and implements Kubernetes networking (services, network policies, load balancing) in BPF.\n\nThe architecture (simplified):\n\n```\nPod A → veth → tc/BPF (egress) → tc/BPF (ingress) → veth → Pod B\n ↓ ↓\n [Service LB map] [Network Policy map]\n [CT table map] [Identity map]\n [NAT map] [Endpoint map]\n```\n\nCilium's key BPF maps that power the whole thing:\n\n```c\n// Connection tracking, every established connection\n// Key: 5-tuple (src_ip, dst_ip, src_port, dst_port, proto)\nstruct {\n __uint(type, BPF_MAP_TYPE_HASH);\n __uint(max_entries, 1000000);\n __type(key, struct ct_key);\n __type(value, struct ct_entry);\n} CT_MAP SEC(\".maps\");\n\n// Service map, Kubernetes service ClusterIP → backend pods\nstruct {\n __uint(type, BPF_MAP_TYPE_HASH);\n __uint(max_entries, 65536);\n __type(key, struct service_key); // service IP + port\n __type(value, struct service_value); // backend pod IPs\n} SERVICE_MAP SEC(\".maps\");\n\n// Network policy, allow/deny rules per endpoint identity\nstruct {\n __uint(type, BPF_MAP_TYPE_HASH);\n __uint(max_entries, 65536);\n __type(key, struct policy_key); // identity + port + proto\n __type(value, struct policy_entry);\n} POLICY_MAP SEC(\".maps\");\n```\n\nWhen a packet leaves Pod A destined for a Kubernetes service, Cilium's BPF program:\n\n1. Looks up the destination in ` SERVICE_MAP ` to find a backend pod\n2. Performs DNAT (rewrite destination IP to the backend pod's IP)\n3. Creates a connection tracking entry in ` CT_MAP `\n4. Checks ` POLICY_MAP ` to verify the network policy allows this connection\n5. Forwards the packet directly to the backend pod's veth interface\n\nAll of this happens in the kernel, with no context switches to userspace. Cilium benchmarks show 2-3x better latency and throughput compared to iptables-based kube-proxy, especially under high connection rates.\n\n## Debugging BPF Programs\n\nDebugging is the weak link. ` printf ` doesn't exist in BPF. You have ` bpf_trace_printk() `, which writes to the kernel trace pipe:\n\n```c\n// In your BPF program\nbpf_trace_printk(\"src_ip=%x pkt_count=%llu\\\\n\", src_ip, entry->packet_count);\n```\n\n```bash\n# In a terminal\ncat /sys/kernel/debug/tracing/trace_pipe\n```\n\nFor production, use BPF ring buffers to send structured events to userspace:\n\n```c\nstruct event {\n __u32 src_ip;\n __u32 dst_ip;\n __u16 src_port;\n __u16 dst_port;\n __u32 action; // XDP_PASS, XDP_DROP, etc.\n};\n\nstruct {\n __uint(type, BPF_MAP_TYPE_RINGBUF);\n __uint(max_entries, 256 * 1024);\n} events SEC(\".maps\");\n\n// In the XDP program, after making a decision:\nstruct event *evt = bpf_ringbuf_reserve(&events, sizeof(struct event), 0);\nif (evt) {\n evt->src_ip = ip->saddr;\n evt->dst_ip = ip->daddr;\n evt->action = XDP_DROP;\n bpf_ringbuf_submit(evt, 0);\n}\n```\n\n## Production Numbers\n\nAfter deploying the XDP rate limiter on our edge servers ([DigitalOcean](<https://digitalocean.com/>) Premium CPU droplets with Mellanox NICs):\n\n| Metric | Before (iptables) | After (XDP) |\n| --- | --- | --- |\n| Max DDoS absorption | ~5 Gbps | 40+ Gbps |\n| CPU during 10 Gbps flood | 95% | 8% |\n| Legitimate traffic latency during attack | 200-500ms | \\<5ms |\n| Rule update time | ~50ms (iptables flush) | \\<1ms (map update) |\n| Memory per tracked IP | ~512 bytes (conntrack) | 16 bytes (BPF map) |\n\nThe memory difference matters. During a volumetric DDoS with millions of spoofed source IPs, Linux conntrack would consume gigabytes of RAM and eventually OOM-kill processes. The BPF LRU map uses a fixed 16 MB regardless of attack size.\n\n## When Not to Use XDP\n\nXDP is a surgical tool, not a general solution:\n\n- **Application-layer filtering** (WAF rules, HTTP inspection): Use a reverse proxy. XDP operates on raw packets before reassembly.\n- **Stateful deep packet inspection**: The BPF instruction limit and lack of dynamic memory allocation make complex stateful analysis impractical.\n- **Simple firewalling**: If iptables handles your load, use iptables. XDP adds complexity.\n- **Non-Linux environments**: XDP is Linux-only, kernel 4.8+.\n\nXDP shines for: DDoS mitigation, load balancing, packet steering, telemetry/sampling, and anything where you need to make per-packet decisions at millions of packets per second.\n\nThe learning curve is steep, you need to understand C, the Linux networking stack, BPF verifier constraints, and NIC driver architecture. But once you've written your first XDP program that drops 40 Gbps of attack traffic using 3% of one CPU core, you'll never look at iptables the same way again.\n\n## Frequently asked questions\n\n### How fast can XDP actually process packets?\n\nOn a single core with a modern NIC (Intel E810, Mellanox ConnectX-6), XDP can process 24-26 million packets per second in XDP\\_DROP mode. That's ~25 Gbps of minimum-sized packets on one core. With AF\\_XDP for userspace processing, expect 8-12 Mpps per core depending on packet size and processing complexity.\n\n### Do I need to write C to use XDP?\n\nYou write the XDP program in C (compiled to BPF bytecode via clang), but you can use higher-level frameworks for the userspace side. Cilium uses Go, Katran uses C++, and there are Rust wrappers like Aya that let you write both the BPF program and userspace loader in Rust.\n\n### What happens if my XDP program has a bug?\n\nThe BPF verifier catches most bugs at load time, it statically analyzes every code path, checks bounds on all memory accesses, ensures no infinite loops, and verifies that all map lookups are null-checked. If a bug gets past the verifier (rare), the worst case is dropping or passing packets incorrectly, the kernel itself cannot crash from a BPF program.\n\n## Tips\n\n1. **LuchoTip:** Always use ` __attribute__((always_inline)) ` for helper functions in XDP programs. The BPF verifier counts instructions after inlining, and a non-inlined function call adds overhead that can push you past the 1 million instruction limit on older kernels. On 5.15+ it's less critical but still good practice.\n2. **LuchoTip:** Test your XDP programs with ` XDP_FLAGS_SKB_MODE ` first, then switch to ` XDP_FLAGS_DRV_MODE ` for production. SKB mode runs in the kernel's network stack (slower but always works), driver mode runs in the NIC driver (fast but needs driver support). If your NIC supports it, ` XDP_FLAGS_HW_MODE ` offloads to the NIC itself, zero CPU cost.\n3. **LuchoTip:** Use BPF maps of type ` BPF_MAP_TYPE_LRU_HASH ` instead of ` BPF_MAP_TYPE_HASH ` for connection tracking. Regular hash maps fail silently when full. LRU maps evict the least recently used entry, which for a DDoS filter means evicting the attacker entry that's been quiet the longest, exactly what you want.\n\n- ebpf\n- xdp\n- networking\n- ddos\n- cilium\n- linux-kernel\n- packet-processing\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"eBPF and XDP: High-Performance Packet Processing and DDoS Mitigation at Line Rate","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#title"},{"level":2,"text":"Why XDP Is Different From Everything Else","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#why-xdp-is-different-from-everything-else"},{"level":2,"text":"Writing Your First XDP Program","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#writing-your-first-xdp-program"},{"level":2,"text":"Real DDoS Mitigation: Rate Limiting With BPF Maps","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#real-ddos-mitigation-rate-limiting-with-bpf-maps"},{"level":2,"text":"AF_XDP: When You Need Userspace Processing","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#afxdp-when-you-need-userspace-processing"},{"level":2,"text":"Inside Cilium's Dataplane","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#inside-ciliums-dataplane"},{"level":2,"text":"Debugging BPF Programs","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#debugging-bpf-programs"},{"level":2,"text":"Production Numbers","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#production-numbers"},{"level":2,"text":"When Not to Use XDP","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#when-not-to-use-xdp"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#questions"},{"level":3,"text":"How fast can XDP actually process packets?","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#question-1"},{"level":3,"text":"Do I need to write C to use XDP?","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#question-2"},{"level":3,"text":"What happens if my XDP program has a bug?","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#article-tips"}],"sections":[{"heading":"eBPF and XDP: High-Performance Packet Processing and DDoS Mitigation at Line Rate","level":1,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing","content":"Networking"},{"heading":"eBPF and XDP: High-Performance Packet Processing and DDoS Mitigation at Line Rate","level":1,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#title","content":"2026-02-179 min read\n\n[Lucio Durán](<https://lucioduran.com/>)\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/bpf-xdp-high-performance-packet-processing>)"},{"heading":"Why XDP Is Different From Everything Else","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#why-xdp-is-different-from-everything-else","content":"The Linux networking stack processes packets through a long chain: NIC hardware → NIC driver → sk\\_buff allocation → netfilter/iptables → socket receive buffer → application. Each step copies data, allocates memory, and burns CPU.\n\nXDP (eXpress Data Path) hooks into the NIC driver, before sk\\_buff allocation. When a packet arrives, the NIC driver calls your XDP program with a raw pointer to the packet data. Your program returns one of five actions:\n\n```c\nXDP_PASS // Continue normal processing (send to kernel stack)\nXDP_DROP // Drop the packet immediately (no allocation, no copy)\nXDP_TX // Bounce the packet back out the same NIC\nXDP_REDIRECT // Send to another NIC, CPU, or AF_XDP socket\nXDP_ABORTED // Error occurred, drop and log\n```\n\nThe critical path for DDoS mitigation is ` XDP_DROP `. The packet never gets an sk\\_buff, never touches iptables, never reaches the network stack. The NIC driver just moves on to the next packet. On a modern NIC, this happens at line rate."},{"heading":"Writing Your First XDP Program","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#writing-your-first-xdp-program","content":"Here's a minimal XDP program that drops all UDP traffic on port 53 (DNS amplification attacks):\n\n```c\n// xdp_filter.c\n#include <linux/bpf.h>\n#include <linux/if_ether.h>\n#include <linux/ip.h>\n#include <linux/udp.h>\n#include <bpf/bpf_helpers.h>\n\nSEC(\"xdp\")\nint xdp_dns_filter(struct xdp_md *ctx)\n{\n void *data = (void *)(long)ctx->data;\n void *data_end = (void *)(long)ctx->data_end;\n\n // Parse Ethernet header\n struct ethhdr *eth = data;\n if ((void *)(eth + 1) > data_end)\n return XDP_PASS;\n\n if (eth->h_proto != __constant_htons(ETH_P_IP))\n return XDP_PASS;\n\n // Parse IP header\n struct iphdr *ip = (void *)(eth + 1);\n if ((void *)(ip + 1) > data_end)\n return XDP_PASS;\n\n if (ip->protocol != IPPROTO_UDP)\n return XDP_PASS;\n\n // Parse UDP header\n struct udphdr *udp = (void *)ip + (ip->ihl * 4);\n if ((void *)(udp + 1) > data_end)\n return XDP_PASS;\n\n // Drop DNS responses (source port 53), typical amplification attack\n if (udp->source == __constant_htons(53))\n return XDP_DROP;\n\n return XDP_PASS;\n}\n\nchar _license[] SEC(\"license\") = \"GPL\";\n```\n\nEvery bounds check is mandatory. The BPF verifier will reject your program if you access ` ip->protocol ` without first proving that ` (void *)(ip + 1) <= data_end `. This is annoying the first time and reassuring every time after, the verifier is the reason BPF programs can't crash the kernel.\n\nCompile and load:\n\n```bash\n# Compile to BPF bytecode\nclang -O2 -target bpf -c xdp_filter.c -o xdp_filter.o\n\n# Attach to interface (driver mode for performance)\nip link set dev eth0 xdpdrv obj xdp_filter.o sec xdp\n\n# Verify it's loaded\nip link show eth0\n# ... xdp/id:42 ...\n\n# Detach when done\nip link set dev eth0 xdp off\n```"},{"heading":"Real DDoS Mitigation: Rate Limiting With BPF Maps","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#real-ddos-mitigation-rate-limiting-with-bpf-maps","content":"The DNS filter above is too crude. Real DDoS mitigation needs rate limiting, allow some traffic from each source, but drop when a source exceeds a threshold. BPF maps provide shared state between the kernel-side XDP program and userspace:\n\n```c\n// Rate limiter with per-source-IP counters\nstruct rate_limit_entry {\n __u64 packet_count;\n __u64 last_reset_ns;\n};\n\nstruct {\n __uint(type, BPF_MAP_TYPE_LRU_HASH);\n __uint(max_entries, 1000000); // Track up to 1M source IPs\n __type(key, __u32); // Source IP (IPv4)\n __type(value, struct rate_limit_entry);\n} rate_limits SEC(\".maps\");\n\n// Configuration map, userspace can update thresholds without reloading\nstruct {\n __uint(type, BPF_MAP_TYPE_ARRAY);\n __uint(max_entries, 1);\n __type(key, __u32);\n __type(value, __u64); // Max packets per second per IP\n} config SEC(\".maps\");\n\nSEC(\"xdp\")\nint xdp_rate_limiter(struct xdp_md *ctx)\n{\n void *data = (void *)(long)ctx->data;\n void *data_end = (void *)(long)ctx->data_end;\n\n struct ethhdr *eth = data;\n if ((void *)(eth + 1) > data_end)\n return XDP_PASS;\n\n if (eth->h_proto != __constant_htons(ETH_P_IP))\n return XDP_PASS;\n\n struct iphdr *ip = (void *)(eth + 1);\n if ((void *)(ip + 1) > data_end)\n return XDP_PASS;\n\n __u32 src_ip = ip->saddr;\n\n // Get rate limit threshold from config\n __u32 config_key = 0;\n __u64 *max_pps = bpf_map_lookup_elem(&config, &config_key);\n __u64 threshold = max_pps ? *max_pps : 10000; // Default: 10k pps\n\n // Get or create rate limit entry for this source IP\n struct rate_limit_entry *entry = bpf_map_lookup_elem(&rate_limits, &src_ip);\n\n __u64 now = bpf_ktime_get_ns();\n\n if (entry) {\n // Reset counter every second\n if (now - entry->last_reset_ns > 1000000000ULL) {\n entry->packet_count = 1;\n entry->last_reset_ns = now;\n return XDP_PASS;\n }\n\n entry->packet_count++;\n\n if (entry->packet_count > threshold) {\n return XDP_DROP;\n }\n } else {\n // First packet from this IP, create entry\n struct rate_limit_entry new_entry = {\n .packet_count = 1,\n .last_reset_ns = now,\n };\n bpf_map_update_elem(&rate_limits, &src_ip, &new_entry, BPF_ANY);\n }\n\n return XDP_PASS;\n}\n```\n\nThe ` BPF_MAP_TYPE_LRU_HASH ` is key here. During a DDoS, you'll see millions of unique source IPs (spoofed). A regular hash map would fill up and reject new entries, letting new attack IPs through untracked. The LRU map evicts the oldest entries, which are typically attack IPs that have already been rate-limited."},{"heading":"AF_XDP: When You Need Userspace Processing","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#afxdp-when-you-need-userspace-processing","content":"Sometimes you need to do more than just drop or pass. AF\\_XDP (Address Family XDP) lets you redirect packets to a userspace application through a zero-copy ring buffer, bypassing the entire kernel network stack:\n\n```c\n// XDP program that redirects specific traffic to userspace via AF_XDP\nstruct {\n __uint(type, BPF_MAP_TYPE_XSKMAP);\n __uint(max_entries, 64); // One per RX queue\n __type(key, __u32);\n __type(value, __u32);\n} xsks_map SEC(\".maps\");\n\nSEC(\"xdp\")\nint xdp_af_xdp_redirect(struct xdp_md *ctx)\n{\n void *data = (void *)(long)ctx->data;\n void *data_end = (void *)(long)ctx->data_end;\n\n struct ethhdr *eth = data;\n if ((void *)(eth + 1) > data_end)\n return XDP_PASS;\n\n if (eth->h_proto != __constant_htons(ETH_P_IP))\n return XDP_PASS;\n\n struct iphdr *ip = (void *)(eth + 1);\n if ((void *)(ip + 1) > data_end)\n return XDP_PASS;\n\n // Redirect TCP traffic on port 8080 to userspace\n if (ip->protocol == IPPROTO_TCP) {\n struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);\n if ((void *)(tcp + 1) > data_end)\n return XDP_PASS;\n\n if (tcp->dest == __constant_htons(8080)) {\n return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_PASS);\n }\n }\n\n return XDP_PASS;\n}\n```\n\nThe userspace side uses ` libxdp ` or raw AF\\_XDP sockets:\n\n```c\n// Simplified AF_XDP userspace receiver\n#include <xdp/xsk.h>\n#include <xdp/libxdp.h>\n\nvoid process_packets(struct xsk_socket *xsk) {\n struct xsk_ring_cons *rx = &xsk->rx;\n struct xsk_ring_prod *fq = &xsk->umem->fq;\n\n unsigned int received = xsk_ring_cons__peek(rx, BATCH_SIZE, &idx);\n if (!received)\n return;\n\n for (int i = 0; i < received; i++) {\n const struct xdp_desc *desc = xsk_ring_cons__rx_desc(rx, idx + i);\n uint8_t *packet = xsk_umem__get_data(xsk->umem->buffer, desc->addr);\n uint32_t len = desc->len;\n\n // Process the packet in userspace, full packet data, zero copy\n handle_packet(packet, len);\n }\n\n xsk_ring_cons__release(rx, received);\n\n // Return frames to the fill queue for reuse\n xsk_ring_prod__submit(fq, received);\n}\n```\n\nAF\\_XDP gives you 8-12 million packets per second per core in userspace. That's about 10x faster than ` recvmsg() ` on a raw socket, because there's no syscall overhead, no sk\\_buff allocation, and no data copying, the packet data stays in the same memory region shared between the NIC and your application."},{"heading":"Inside Cilium's Dataplane","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#inside-ciliums-dataplane","content":"[Cilium](<https://cilium.io/>) is the most sophisticated production use of BPF for networking. It replaces kube-proxy entirely and implements Kubernetes networking (services, network policies, load balancing) in BPF.\n\nThe architecture (simplified):\n\n```\nPod A → veth → tc/BPF (egress) → tc/BPF (ingress) → veth → Pod B\n ↓ ↓\n [Service LB map] [Network Policy map]\n [CT table map] [Identity map]\n [NAT map] [Endpoint map]\n```\n\nCilium's key BPF maps that power the whole thing:\n\n```c\n// Connection tracking, every established connection\n// Key: 5-tuple (src_ip, dst_ip, src_port, dst_port, proto)\nstruct {\n __uint(type, BPF_MAP_TYPE_HASH);\n __uint(max_entries, 1000000);\n __type(key, struct ct_key);\n __type(value, struct ct_entry);\n} CT_MAP SEC(\".maps\");\n\n// Service map, Kubernetes service ClusterIP → backend pods\nstruct {\n __uint(type, BPF_MAP_TYPE_HASH);\n __uint(max_entries, 65536);\n __type(key, struct service_key); // service IP + port\n __type(value, struct service_value); // backend pod IPs\n} SERVICE_MAP SEC(\".maps\");\n\n// Network policy, allow/deny rules per endpoint identity\nstruct {\n __uint(type, BPF_MAP_TYPE_HASH);\n __uint(max_entries, 65536);\n __type(key, struct policy_key); // identity + port + proto\n __type(value, struct policy_entry);\n} POLICY_MAP SEC(\".maps\");\n```\n\nWhen a packet leaves Pod A destined for a Kubernetes service, Cilium's BPF program:\n\n1. Looks up the destination in ` SERVICE_MAP ` to find a backend pod\n2. Performs DNAT (rewrite destination IP to the backend pod's IP)\n3. Creates a connection tracking entry in ` CT_MAP `\n4. Checks ` POLICY_MAP ` to verify the network policy allows this connection\n5. Forwards the packet directly to the backend pod's veth interface\n\nAll of this happens in the kernel, with no context switches to userspace. Cilium benchmarks show 2-3x better latency and throughput compared to iptables-based kube-proxy, especially under high connection rates."},{"heading":"Debugging BPF Programs","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#debugging-bpf-programs","content":"Debugging is the weak link. ` printf ` doesn't exist in BPF. You have ` bpf_trace_printk() `, which writes to the kernel trace pipe:\n\n```c\n// In your BPF program\nbpf_trace_printk(\"src_ip=%x pkt_count=%llu\\\\n\", src_ip, entry->packet_count);\n```\n\n```bash\n# In a terminal\ncat /sys/kernel/debug/tracing/trace_pipe\n```\n\nFor production, use BPF ring buffers to send structured events to userspace:\n\n```c\nstruct event {\n __u32 src_ip;\n __u32 dst_ip;\n __u16 src_port;\n __u16 dst_port;\n __u32 action; // XDP_PASS, XDP_DROP, etc.\n};\n\nstruct {\n __uint(type, BPF_MAP_TYPE_RINGBUF);\n __uint(max_entries, 256 * 1024);\n} events SEC(\".maps\");\n\n// In the XDP program, after making a decision:\nstruct event *evt = bpf_ringbuf_reserve(&events, sizeof(struct event), 0);\nif (evt) {\n evt->src_ip = ip->saddr;\n evt->dst_ip = ip->daddr;\n evt->action = XDP_DROP;\n bpf_ringbuf_submit(evt, 0);\n}\n```"},{"heading":"Production Numbers","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#production-numbers","content":"After deploying the XDP rate limiter on our edge servers ([DigitalOcean](<https://digitalocean.com/>) Premium CPU droplets with Mellanox NICs):\n\n| Metric | Before (iptables) | After (XDP) |\n| --- | --- | --- |\n| Max DDoS absorption | ~5 Gbps | 40+ Gbps |\n| CPU during 10 Gbps flood | 95% | 8% |\n| Legitimate traffic latency during attack | 200-500ms | \\<5ms |\n| Rule update time | ~50ms (iptables flush) | \\<1ms (map update) |\n| Memory per tracked IP | ~512 bytes (conntrack) | 16 bytes (BPF map) |\n\nThe memory difference matters. During a volumetric DDoS with millions of spoofed source IPs, Linux conntrack would consume gigabytes of RAM and eventually OOM-kill processes. The BPF LRU map uses a fixed 16 MB regardless of attack size."},{"heading":"When Not to Use XDP","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#when-not-to-use-xdp","content":"XDP is a surgical tool, not a general solution:\n\n- **Application-layer filtering** (WAF rules, HTTP inspection): Use a reverse proxy. XDP operates on raw packets before reassembly.\n- **Stateful deep packet inspection**: The BPF instruction limit and lack of dynamic memory allocation make complex stateful analysis impractical.\n- **Simple firewalling**: If iptables handles your load, use iptables. XDP adds complexity.\n- **Non-Linux environments**: XDP is Linux-only, kernel 4.8+.\n\nXDP shines for: DDoS mitigation, load balancing, packet steering, telemetry/sampling, and anything where you need to make per-packet decisions at millions of packets per second.\n\nThe learning curve is steep, you need to understand C, the Linux networking stack, BPF verifier constraints, and NIC driver architecture. But once you've written your first XDP program that drops 40 Gbps of attack traffic using 3% of one CPU core, you'll never look at iptables the same way again."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#questions","content":""},{"heading":"How fast can XDP actually process packets?","level":3,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#question-1","content":"On a single core with a modern NIC (Intel E810, Mellanox ConnectX-6), XDP can process 24-26 million packets per second in XDP\\_DROP mode. That's ~25 Gbps of minimum-sized packets on one core. With AF\\_XDP for userspace processing, expect 8-12 Mpps per core depending on packet size and processing complexity."},{"heading":"Do I need to write C to use XDP?","level":3,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#question-2","content":"You write the XDP program in C (compiled to BPF bytecode via clang), but you can use higher-level frameworks for the userspace side. Cilium uses Go, Katran uses C++, and there are Rust wrappers like Aya that let you write both the BPF program and userspace loader in Rust."},{"heading":"What happens if my XDP program has a bug?","level":3,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#question-3","content":"The BPF verifier catches most bugs at load time, it statically analyzes every code path, checks bounds on all memory accesses, ensures no infinite loops, and verifies that all map lookups are null-checked. If a bug gets past the verifier (rare), the worst case is dropping or passing packets incorrectly, the kernel itself cannot crash from a BPF program."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#article-tips","content":"1. **LuchoTip:** Always use ` __attribute__((always_inline)) ` for helper functions in XDP programs. The BPF verifier counts instructions after inlining, and a non-inlined function call adds overhead that can push you past the 1 million instruction limit on older kernels. On 5.15+ it's less critical but still good practice.\n2. **LuchoTip:** Test your XDP programs with ` XDP_FLAGS_SKB_MODE ` first, then switch to ` XDP_FLAGS_DRV_MODE ` for production. SKB mode runs in the kernel's network stack (slower but always works), driver mode runs in the NIC driver (fast but needs driver support). If your NIC supports it, ` XDP_FLAGS_HW_MODE ` offloads to the NIC itself, zero CPU cost.\n3. **LuchoTip:** Use BPF maps of type ` BPF_MAP_TYPE_LRU_HASH ` instead of ` BPF_MAP_TYPE_HASH ` for connection tracking. Regular hash maps fail silently when full. LRU maps evict the least recently used entry, which for a DDoS filter means evicting the attacker entry that's been quiet the longest, exactly what you want.\n\n- ebpf\n- xdp\n- networking\n- ddos\n- cilium\n- linux-kernel\n- packet-processing\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Lucio Durán","url":"https://lucioduran.com/"},{"text":"Español","url":"https://lucioduran.com/es/blog/bpf-xdp-high-performance-packet-processing"},{"text":"Cilium","url":"https://cilium.io/"},{"text":"DigitalOcean","url":"https://digitalocean.com/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com/"},{"@type":"WebSite","@id":"https://lucioduran.com/#website","url":"https://lucioduran.com/","name":"Lucio Durán","inLanguage":["en","es"],"publisher":{"@id":"https://lucioduran.com/#person"}},{"@type":"WebPage","@id":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#page","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing","name":"eBPF and XDP: High-Performance Packet Processing and DDoS Mitigation at Line Rate | Lucio Durán","description":"High-performance packet processing with eBPF and XDP, writing XDP programs in C, AF_XDP sockets, DDoS mitigation at line rate, and peeking inside Cilium's dataplane.","inLanguage":"en","author":{"@id":"https://lucioduran.com/#person"},"publisher":{"@id":"https://lucioduran.com/#person"},"isPartOf":{"@id":"https://lucioduran.com/#website"},"isAccessibleForFree":true,"datePublished":"2026-02-17","mainEntity":{"@id":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#article"},"breadcrumb":{"@id":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#breadcrumb"}},{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#article","mainEntityOfPage":{"@id":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#page"},"headline":"eBPF and XDP: High-Performance Packet Processing and DDoS Mitigation at Line Rate","description":"High-performance packet processing with eBPF and XDP, writing XDP programs in C, AF_XDP sockets, DDoS mitigation at line rate, and peeking inside Cilium's dataplane.","datePublished":"2026-02-17","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@id":"https://lucioduran.com/#person"},"url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Networking","keywords":"ebpf, xdp, networking, ddos, cilium, linux-kernel, packet-processing","wordCount":1688,"timeRequired":"PT9M","workTranslation":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/bpf-xdp-high-performance-packet-processing#article","inLanguage":"es","url":"https://lucioduran.com/es/blog/bpf-xdp-high-performance-packet-processing"}],"isAccessibleForFree":true},{"@type":"Blog","@id":"https://lucioduran.com/blog#blog","url":"https://lucioduran.com/blog","name":"Blog","inLanguage":"en","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com/"},"publisher":{"@id":"https://lucioduran.com/#person"},"isPartOf":{"@id":"https://lucioduran.com/#website"}},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"eBPF and XDP: High-Performance Packet Processing and DDoS Mitigation at Line Rate","item":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing"}],"@id":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#breadcrumb"},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"How fast can XDP actually process packets?","acceptedAnswer":{"@type":"Answer","text":"On a single core with a modern NIC (Intel E810, Mellanox ConnectX-6), XDP can process 24-26 million packets per second in XDP_DROP mode. That's ~25 Gbps of minimum-sized packets on one core. With AF_XDP for userspace processing, expect 8-12 Mpps per core depending on packet size and processing complexity."}},{"@type":"Question","name":"Do I need to write C to use XDP?","acceptedAnswer":{"@type":"Answer","text":"You write the XDP program in C (compiled to BPF bytecode via clang), but you can use higher-level frameworks for the userspace side. Cilium uses Go, Katran uses C++, and there are Rust wrappers like Aya that let you write both the BPF program and userspace loader in Rust."}},{"@type":"Question","name":"What happens if my XDP program has a bug?","acceptedAnswer":{"@type":"Answer","text":"The BPF verifier catches most bugs at load time, it statically analyzes every code path, checks bounds on all memory accesses, ensures no infinite loops, and verifies that all map lookups are null-checked. If a bug gets past the verifier (rare), the worst case is dropping or passing packets incorrectly, the kernel itself cannot crash from a BPF program."}}],"@id":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#questions","url":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#questions","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing#page"},"author":{"@id":"https://lucioduran.com/#person"}}]}],"contentHash":"26d80929492654a294306dfdd45fdeff701c5d73c1f7eb3694b6e6fc04add01e","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/bpf-xdp-high-performance-packet-processing","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":72861,"contentBytes":14711}}
