Sáu Tháng Chạy XDP trên Production: Điều Gì Thực Sự Thay Đổi
Khi tôi thay thế bộ lọc DDoS dựa trên iptables bằng một chương trình XDP trên máy chủ edge 10 Gbps, phản ứng đầu tiên của cả nhóm là nghi ngờ. eBPF đã nằm trong danh sách theo dõi của tôi nhiều năm, nhưng XDP có vẻ quá mức cần thiết cho một hệ thống tầm trung. Sáu tháng sau, đây là quyết định hạ tầng tốt nhất tôi từng đưa ra cho stack này — cực kỳ ổn định qua các đợt spike traffic mà trước đây thường kích hoạt rate limiting khẩn cấp hoặc nullrouting từ upstream.
Bài viết này trình bày cách XDP so sánh với xử lý packet truyền thống, ưu điểm thực sự của nó (và những hạn chế), cách dựng môi trường hoạt động, cùng với cài đặt cả packet filter lẫn load balancer Layer 4 đơn giản.
So Sánh Cách Tiếp Cận: XDP vs iptables vs DPDK
Trước khi đụng vào code, cần hiểu vị trí của từng công cụ trong Linux networking stack. Sự khác biệt về kiến trúc chính là lý do giải thích khoảng cách hiệu năng.
iptables / nftables
iptables xử lý packet bên trong framework netfilter của kernel. Khi một packet đến được các rule của bạn, kernel đã cấp phát sk_buff (socket buffer), sao chép packet vào kernel memory, và đi qua nhiều tầng network stack.
Mỗi packet cũng phải qua conntrack, dù bạn có dùng stateful rules hay không. Với 1 triệu packet mỗi giây, chi phí đó tích lũy rất nhanh — CPU usage tăng cao, và bạn có thể kỳ vọng độ trễ từ 60–120 microsecond ngay cả với các rule DROP đơn giản.
DPDK (Data Plane Development Kit)
DPDK bỏ qua hoàn toàn kernel, polling trực tiếp NIC từ userspace. Throughput rất ấn tượng — có thể đạt line rate trên card 100 Gbps. Nhưng cái giá phải trả là: bạn phải dành toàn bộ CPU core cho việc polling, và DPDK chiếm độc quyền NIC đó.
Linux networking stack thông thường sẽ ngừng hoạt động trên interface đó. Viết code DPDK cũng đồng nghĩa với việc tự quản lý memory pool, cấu trúc mbuf và ring buffer. Trong thực tế, điều này đòi hỏi một đội ngũ chuyên biệt, lập kế hoạch capacity cẩn thận, và một management interface riêng chỉ để SSH vào máy.
XDP (eXpress Data Path)
XDP chạy chương trình eBPF tại điểm sớm nhất có thể trong receive path của kernel — ngay bên trong NIC driver, trước khi bất kỳ sk_buff nào được cấp phát. Packet cần DROP sẽ không bao giờ chạm đến phần còn lại của stack. Packet cần redirect hoặc chỉnh sửa được xử lý mà không cần copy.
Khác với DPDK, XDP cùng tồn tại với Linux network stack thông thường. Interface vẫn hiển thị với OS. Bạn vẫn có thể SSH vào máy, và các công cụ như ip, ss, tcpdump vẫn hoạt động bình thường trên traffic không bị intercepted.
Số liệu benchmark từ hệ thống của tôi (Intel X710, Xeon E-2288G, Debian 12):
Công cụ | UDP flood 64-byte | CPU (1 core) | Latency (p99)
----------- | ----------------- | ------------ | -------------
iptables DROP | ~800 Kpps | 95% | 80–120 µs
nftables DROP | ~1.1 Mpps | 88% | 60–90 µs
XDP DROP | ~14 Mpps | 12% | 1–3 µs
Tốc độ DROP 14 triệu packet mỗi giây trên một core duy nhất, với độ trễ p99 dưới 3 microsecond, không phải là con số phòng lab. Những con số đó đến từ xdp-bench drop trong một cuộc tấn công UDP amplification thực tế đạt đỉnh 9 Mpps liên tục.
Ưu và Nhược Điểm
Tại sao XDP xứng đáng để đầu tư
- Tích hợp kernel mà không cần kernel module — chương trình eBPF được verify và load an toàn; không cần recompile kernel.
- Linux networking thông thường vẫn hoạt động — khác với DPDK, SSH session của bạn vẫn sống.
- eBPF maps để cập nhật state realtime — thêm IP vào blocklist, điều chỉnh trọng số backend, đọc per-CPU counter, tất cả từ userspace trong khi chương trình XDP vẫn chạy. Không cần restart.
- Nhiều chế độ gắn kết: native (ở cấp driver, nhanh nhất), offloaded (chính NIC chạy chương trình), hoặc generic (software fallback, hoạt động trên mọi NIC, chậm hơn).
- Có thể kết hợp — chain nhiều chương trình với nhau qua program dispatcher của libxdp.
Những hạn chế của XDP
- Đường cong học tập dốc — eBPF C bị hạn chế: không có vòng lặp không giới hạn, không cấp phát bộ nhớ động, giới hạn stack 512 byte. Verifier từ chối chương trình theo những cách khó hiểu cho đến khi bạn tích lũy được vài giờ đối mặt với lỗi.
- Native mode yêu cầu driver hỗ trợ — NIC mainstream của Intel, Mellanox, Broadcom đều ổn, nhưng phần cứng cũ hoặc giá rẻ sẽ fallback về generic mode, chậm hơn.
- Debug khó hơn iptables —
bpf_trace_printkcó nhưng tốn overhead; debug production dựa vào eBPF map vàbpftool. - Chỉ xử lý ingress, không có conntrack — XDP xử lý packet đến. Để có stateful firewall đầy đủ, bạn vẫn cần nftables, hoặc kết hợp XDP với TC (Traffic Control) eBPF cho egress.
Cấu Hình Đề Xuất
Điểm khởi đầu thực tế cho hầu hết các nhóm:
- Dùng XDP cho lọc ingress — blocklist, rate limiting, giảm thiểu DDoS
- Giữ nftables cho stateful rule, lọc outbound và DNAT
- Dùng XDP redirect + AF_XDP để xử lý packet ở userspace khi cần thiết
- Quản lý chương trình XDP bằng libxdp (từ project xdp-tools) thay vì gọi trực tiếp syscall
bpf()
Phiên bản kernel tối thiểu: 5.10+ để có hỗ trợ XDP ổn định. Kernel 6.1+ (Debian 12, Ubuntu 22.04 HWE) là target tốt hơn — đi kèm hỗ trợ multi-prog và map helper được cải tiến.
Hướng Dẫn Cài Đặt
1. Cài đặt các dependency
# Debian 12 / Ubuntu 22.04
apt install -y clang llvm libelf-dev libbpf-dev linux-headers-$(uname -r) \
bpftool xdp-tools iproute2 gcc make
# Kiểm tra kernel có hỗ trợ BPF không
bpftool feature | grep -E 'xdp|prog_type'
2. Viết XDP packet filter cơ bản bằng eBPF C
Chương trình này DROP toàn bộ UDP packet trên port 53 từ các source IP trong blocklist — biện pháp đối phó trực tiếp với tấn công DNS amplification:
// xdp_filter.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// eBPF map: blocklist các source IP (LPM trie để hỗ trợ CIDR)
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, 10000);
__type(key, struct bpf_lpm_trie_key); // prefixlen + data
__type(value, __u32);
__uint(map_flags, BPF_F_NO_PREALLOC);
} blocklist SEC(".maps");
SEC("xdp")
int xdp_filter_prog(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_UDP)
return XDP_PASS;
// Tra cứu LPM theo source IP
struct {
__u32 prefixlen;
__u32 addr;
} key = { .prefixlen = 32, .addr = ip->saddr };
if (bpf_map_lookup_elem(&blocklist, &key))
return XDP_DROP;
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
3. Biên dịch và load
# Biên dịch sang BPF bytecode
clang -O2 -target bpf -c xdp_filter.c -o xdp_filter.o \
-I/usr/include/$(uname -m)-linux-gnu
# Gắn vào interface (ưu tiên native mode)
ip link set dev eth0 xdpdrv obj xdp_filter.o sec xdp
# Kiểm tra đã load thành công
bpftool net show dev eth0
bpftool map list
4. Quản lý blocklist từ userspace (Python)
Sau khi chương trình đang chạy, bạn có thể thêm hoặc xóa IP mà không cần reload bất cứ thứ gì. Data plane vẫn tiếp tục chạy; bạn chỉ đang cập nhật một map:
#!/usr/bin/env python3
# manage_blocklist.py — dùng bpftool, không cần thêm dependency Python
import socket
import subprocess
BLOCKLIST_MAP = "/sys/fs/bpf/blocklist" # đường dẫn map đã pin
def block_ip(ip_str: str):
"""Thêm một host entry /32 vào XDP blocklist map."""
packed = socket.inet_aton(ip_str).hex()
# key: prefixlen (32, little-endian 4 byte) + addr (4 byte)
key_hex = "20000000" + packed # 0x20 = 32 dạng LE
value_hex = "01000000"
subprocess.run([
"bpftool", "map", "update", "pinned", BLOCKLIST_MAP,
"key", "hex", *[key_hex[i:i+2] for i in range(0, len(key_hex), 2)],
"value", "hex", *[value_hex[i:i+2] for i in range(0, len(value_hex), 2)]
], check=True)
print(f"Đã chặn: {ip_str}")
def unblock_ip(ip_str: str):
packed = socket.inet_aton(ip_str).hex()
key_hex = "20000000" + packed
subprocess.run([
"bpftool", "map", "delete", "pinned", BLOCKLIST_MAP,
"key", "hex", *[key_hex[i:i+2] for i in range(0, len(key_hex), 2)]
], check=True)
print(f"Đã bỏ chặn: {ip_str}")
if __name__ == "__main__":
import sys
if len(sys.argv) == 3 and sys.argv[1] == "block":
block_ip(sys.argv[2])
elif len(sys.argv) == 3 and sys.argv[1] == "unblock":
unblock_ip(sys.argv[2])
# Pin map để userspace tool có thể truy cập qua đường dẫn
bpftool map pin id $(bpftool map list | grep blocklist | awk '{print $1}' | tr -d ':') \
/sys/fs/bpf/blocklist
# Chặn và bỏ chặn IP ngay lập tức — không cần restart service
python3 manage_blocklist.py block 198.51.100.42
python3 manage_blocklist.py unblock 198.51.100.42
5. Load balancer Layer 4 đơn giản dùng XDP_TX
Một L4 LB production xứng đáng có bài viết riêng. Ý tưởng cốt lõi gói gọn trong một câu: parse destination port, chọn backend từ BPF array map dùng hash của 5-tuple, viết lại destination IP và MAC, trả về XDP_TX để truyền lại trên cùng interface. Katran (L4 LB open-source của Meta, xử lý hàng chục triệu packet mỗi giây) và Cilium đều dùng cách tiếp cận này ở quy mô lớn — concept này có thể scale xa hơn mức hầu hết các nhóm sẽ cần đến.
# Xem per-CPU drop/pass counter từ chương trình XDP
bpftool map dump id <stats_map_id>
# Gỡ chương trình XDP một cách sạch sẽ
ip link set dev eth0 xdp off
6. Tự động khởi động qua systemd
# /etc/systemd/system/xdp-filter.service
[Unit]
Description=XDP Packet Filter
After=network.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/ip link set dev eth0 xdpdrv obj /opt/xdp/xdp_filter.o sec xdp
ExecStop=/usr/sbin/ip link set dev eth0 xdp off
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now xdp-filter
Giám Sát và Quan Sát Hệ Thống
Hãy thêm per-CPU counter vào chương trình XDP của bạn ngay từ đầu. Một packet bị DROP mà không có counter đi kèm là một buổi debug bạn không muốn trải qua lúc 2 giờ sáng giữa sự cố.
# Theo dõi XDP drop counter trực tiếp (nếu bạn đã export chúng dưới dạng map)
watch -n 1 'bpftool map dump name xdp_stats'
# Thống kê XDP ở phía kernel theo interface
ip -s link show dev eth0 | grep -A2 'RX\|TX'
# Lệnh một dòng bpftrace: theo dõi mọi quyết định XDP
bpftrace -e 'kprobe:xdp_do_generic_redirect { @[retval] = count(); }'
Suy Nghĩ Cuối
Sau sáu tháng chạy XDP trên các edge node production, quay lại dùng thuần iptables cho ingress filtering là điều tôi không thể nào cân nhắc nữa. Dư địa hiệu năng đồng nghĩa với cùng một máy chủ xử lý được các sự kiện traffic mà trước đây đòi hỏi rate limiting khẩn cấp hoặc nullrouting từ upstream.
Throughput thô chỉ là một phần của câu chuyện. Điều làm XDP hấp dẫn trong công việc hàng ngày là sự kết hợp: hiệu năng cấp kernel với mô hình vận hành của một máy chủ Linux bình thường. Monitoring vẫn hoạt động.
SSH vẫn hoạt động. tcpdump vẫn hoạt động. Giao diện eBPF map cho phép bạn phản ứng với các mối đe dọa theo thời gian thực từ một script Python mà không cần đụng vào chương trình data plane. Đối với các nhóm tự vận hành edge infrastructure, đây là một thế trận vận hành hoàn toàn khác — không chỉ là phiên bản nhanh hơn của những gì bạn đã có.

