XDP(eXpress Data Path)可以在网络接口卡(NIC)上处理数据包,使得XDP可以进行超低延迟和高吞吐量的数据包处理,非常适合用于负载均衡、DDoS保护和流量过滤等任务。
使用XDP的项目
Cilium
是一个为云原生环境(如Kubernetes)设计的开源网络工具。它使用XDP高效处理数据包过滤和负载均衡,提升了高流量网络中的性能。
Katran
由Facebook开发,是一个负载均衡器,它使用XDP处理数百万的连接,且CPU使用率低。它高效地将流量分发到服务器,在Facebook内部被用于大规模的网络环境。
Cloudflare
使用XDP来防御DDoS攻击。通过在NIC级别过滤恶意流量,Cloudflare可以在攻击数据包进入内核之前将其丢弃,最大限度地减少对网络的影响。
负载均衡Demo
通常负载均衡器会有多个后端(backend)用来承接流量,这里为了简化只使用了一个后端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h>
#define ETH_P_IP 0x0800
int client_ip = bpf_htonl(0xac12c05a); unsigned char client_mac[6] = {0x3c, 0x22, 0xfb, 0x98, 0x15, 0x21};
int backend_ip = bpf_htonl(0xac12eab2); unsigned char backend_mac[6] = {0x0, 0xc, 0x29, 0x55, 0xe2, 0xb2};
int lb_ip = bpf_htonl(0xac1210a2); unsigned char lb_mac[6] = {0xd8, 0xbb, 0xc1, 0xbb, 0x76, 0x76};
static __always_inline __u16 csum_fold_helper(__u64 csum) { int i; for (i = 0; i < 4; i++) { if (csum >> 16) csum = (csum & 0xffff) + (csum >> 16); } return ~csum; }
static __always_inline __u16 iph_csum(struct iphdr *iph) { iph->check = 0; unsigned long long csum = bpf_csum_diff(0, 0, (unsigned int *)iph, sizeof(struct iphdr), 0); return csum_fold_helper(csum); }
SEC("xdp") int xdp_redirect(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 *iph = data + sizeof(struct ethhdr); if ((void *)(iph + 1) > data_end) return XDP_PASS; if (iph->protocol != IPPROTO_TCP) return XDP_PASS; if (iph->saddr == client_ip) { iph->daddr = backend_ip; __builtin_memcpy(eth->h_dest, backend_mac, 6); iph->saddr = lb_ip; __builtin_memcpy(eth->h_source, lb_mac, 6); iph->check = 0; iph->check = iph_csum(iph); bpf_printk("Receive TCP packet from client"); return XDP_TX; } if (iph->saddr == backend_ip) { iph->daddr = client_ip; __builtin_memcpy(eth->h_dest, client_mac, 6); iph->saddr = lb_ip; __builtin_memcpy(eth->h_source, lb_mac, 6); iph->check = 0; iph->check = iph_csum(iph); bpf_printk("Receive TCP packet from backend"); return XDP_TX; }
return XDP_PASS; }
char _license[] SEC("license") = "GPL";
|
修改数据包的方式非常简单,直接将要改的值重新赋值即可。想要扩展功能,可以设置多个后端,或者从用户空间程序进行配置,如,读取配置文件,将后端信息(IP、MAC地址)通过RINGBUF传递到内核空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <errno.h> #include <unistd.h> #include <sys/resource.h> #include <net/if.h>
#include <bpf/libbpf.h> #include <bpf/bpf.h>
#include "xdp_redirect.skel.h"
static volatile sig_atomic_t exiting = 0;
static void sig_int(int signo) { exiting = 1; }
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { return vfprintf(stderr, format, args); }
int main(int argc, char **argv) { struct xdp_redirect_bpf *skel; int ifindex; int err;
if (argc != 2) { fprintf(stderr, "Usage: %s <ifname>\n", argv[0]); return 1; } libbpf_set_print(libbpf_print_fn);
const char *ifname = argv[1]; ifindex = if_nametoindex(ifname); if (ifindex == 0) { fprintf(stderr, "Invalid interface name %s\n", ifname); return 1; } skel = xdp_redirect_bpf__open(); if (!skel) { fprintf(stderr, "Failed to open BPF skeleton\n"); return 1; } err = xdp_redirect_bpf__load(skel); if (err) { fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err); goto cleanup; } err = xdp_redirect_bpf__attach(skel); if (err) { fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err); goto cleanup; } skel->links.xdp_redirect = bpf_program__attach_xdp(skel->progs.xdp_redirect, ifindex); if (!skel->links.xdp_redirect) { err = -errno; fprintf(stderr, "Failed to attach XDP program: %s", strerror(errno)); goto cleanup; } if (signal(SIGINT, sig_int) == SIG_ERR) { fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); goto cleanup; } printf("Successfully attached XDP program to interface %s\n", ifname);
while (!exiting) { fprintf(stderr, "."); sleep(1); } cleanup: xdp_redirect_bpf__destroy(skel); return -err; }
|
编译运行
编译方法和之前一样,将代码放到libbpf-bootstrap中make。运行:
负载均衡器
1 2 3 4 5 6 7
| $ sudo ./xdp_redirect eno1 ... libbpf: prog 'xdp_redirect': relo #23: patched insn #171 (LDX/ST/STX) off 10 -> 10 libbpf: map 'xdp_redi.data': created successfully, fd=3 libbpf: map 'xdp_redi.rodata': created successfully, fd=4 Successfully attached XDP program to interface eno1 .....
|
后端
1 2
| $ python3 -m http.server Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
|
客户端
1
| $ curl 172.18.16.162:8000
|
我这里运行时,数据包没有正确转发到后端,查了各种原因,最后发现是网卡不支持。。
使用XDP程序调试时,没法在负载均衡器上用wireshark进行抓包,因为XDP在内核栈,数据包还没有到用户空间。可以使用github上开源工具xdp-tools进行抓包进行分析:
1 2 3
| $ ./configure & make $ cd xdp-dump $ sudo ./xdpdump --rx-capture entry,exit --i eno1 -w ./capture.pcap
|
XDP_TX对加载模式的要求
XDP_TX
动作对XDP的加载模式是有一定要求的。具体来说,不同的加载模式决定了XDP程序运行的位置以及是否支持某些特性。
1.在驱动模式(Native)下,XDP_TX是完全支持且推荐使用的。原因如下:
- 驱动模式允许直接访问网卡的传输队列(Tx Queue),XDP_TX 动作可以将数据包直接重新发送回网卡的传输队列。
- 性能最高,因为整个数据包的处理和转发都在驱动层完成。
2.在通用模式(Generic)下,XDP_TX通常是不可用的。这是因为:
- 通用模式运行在内核网络协议栈的早期阶段,而不是直接在驱动层,因此无法直接访问网卡的传输队列。
- XDP_TX动作依赖于直接操作网卡的硬件队列,而通用模式无法提供这种能力。
3.在Offload模式下,XDP_TX是支持的,但有以下要求:
- 硬件(如智能网卡)必须支持将XDP程序加载到硬件,并且支持XDP_TX动作。
- XDP程序的复杂度需要在硬件限制范围内,某些复杂功能可能需要被裁剪(硬件资源有限,如寄存器或内存限制)。
reference
https://eunomia.dev/zh/tutorials/42-xdp-loadbalancer/