BruceFan's Blog

Stay hungry, stay foolish

0%

eBPF XDP负载均衡Demo

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

// client
int client_ip = bpf_htonl(0xac12c05a); // 172.18.192.90
unsigned char client_mac[6] = {0x3c, 0x22, 0xfb, 0x98, 0x15, 0x21};
// backend
int backend_ip = bpf_htonl(0xac12eab2); // 172.18.234.178
unsigned char backend_mac[6] = {0x0, 0xc, 0x29, 0x55, 0xe2, 0xb2};
// load balence
int lb_ip = bpf_htonl(0xac1210a2); // 172.18.16.162
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);
}

// eBPF 程序入口
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;
// 修改客户端发来的数据包,目的IP、MAC地址修改为后端,源IP、MAC地址修改为负载均衡器
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);
// 重新计算 IP 校验和
iph->check = 0;
iph->check = iph_csum(iph);
bpf_printk("Receive TCP packet from client");
return XDP_TX;
}
// 修改后端发来的数据包,目的IP、MAC地址修改为客户端,源IP、MAC地址修改为负载均衡器
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);
// 重新计算 IP 校验和
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/