前段时间,突然想到是否可以用 BPF/XDP 来替代网桥,桥接两对 VETH pair 。经过一番搜索和讨论,我意识到这个功能 Cilium 应该已经实现了,只不过用的是 TC BPF 而不是 XDP。
实现这个功能的第一步是完成 ARP 响应,毕竟在两个二层设备之间转发流量需要 MAC 地址。今天我们先来实现 ARP 应答功能。这个功能非常简单,因为 ARP 协议不需要计算校验和。
先来创建一个实验环境:
ip netns add ns1
type veth peer name ns1-eth0 ip link add ns1-veth
set ns1-eth0 netns ns1 ip link
exec ns1 ip addr add 10.0.1.1/24 dev ns1-eth0 ip netns
exec ns1 ip link set dev ns1-eth0 up ip netns
set ns1-veth up ip link
现在执行如下命令,我们是得不到 ARP 响应的:
exec ns1 ping -I ns1-eth0 -c 1 -w 1 10.0.2.1 ip netns
PING 10.0.2.1 (10.0.2.1) from 10.0.1.1 ns1-eth0: 56(84) bytes of data.
--- 10.0.2.1 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
$ ip netns exec ns1 arp -i ns1-eth0
Address HWtype HWaddress Flags Mask Iface
10.0.2.1 (incomplete) ns1-eth0
我们使用 TC BPF 来实现 ARP 应答,程序的入参是 struct __sk_buff *,我们看到的数据包是这样的:
对于 Ethernet Header,在内核中是用结构体 struct ethhdr 来表示的:
struct ethhdr {
unsigned char h_dest[6];
unsigned char h_source[6];
__be16 h_proto;
};
因为我们在 ns1 中没有配置路由,所以 h_dest 是广播地址,我们将其修改为目标 MAC 地址 12:34:56:78:9a:bc,同时交换 h_dest 和 h_source,作为响应的 Ethernet Header。
对于 ARP Header,内核中使用结构体 struct arphdr 来表示:
struct arphdr {
__be16 ar_hrd;
__be16 ar_pro;
unsigned char ar_hln;
unsigned char ar_pln;
__be16 ar_op;
};
在这里,我们只需要修改 ar_op 字段即可,将其从 ARPOP_REQUEST 修改为 ARPOP_REPLY。
对于 ARP 响应的载荷,我们可以用如下结构体表示:
struct arpdata {
__u8 sha[6];
__u32 sip;
__u8 tha[6];
__u32 tip;
__attribute__((packed));
我们需要将目标 MAC 地址 tha 修改为 12:34:56:78:9a:bc,同时交换 sha 和 tha,sip 和 tip,作为 ARP 应答。
完整的 BPF 代码如下:
union macaddr {
struct {
__u32 p1;
__u16 p2;
};
__u8 addr[6];
};
struct arpdata {
__u8 sha[6];
__u32 sip;
__u8 tha[6];
__u32 tip;
} __attribute__((packed));
void swap_macaddr(union macaddr *saddr, union macaddr *daddr)
{
__u32 p1;
__u16 p2;
p1 = saddr->p1;
p2 = saddr->p2;
saddr->p1 = daddr->p1;
saddr->p2 = daddr->p2;
daddr->p1 = p1;
daddr->p2 = p2;
}
SEC("tc")
int tc_ingress_ns1(struct __sk_buff *ctx)
{
void *data_end = (void *)(__u64)ctx->data_end;
void *data = (void *)(__u64)ctx->data;
struct ethhdr *ethhdr;
struct arphdr *arphdr;
struct arpdata *arpdata;
union macaddr *daddr, *saddr;
__u32 ip;
ethhdr = data;
if ((void *)(ethhdr + 1) > data_end)
return TC_ACT_OK;
if (ethhdr->h_proto != bpf_ntohs(ETH_P_ARP))
return TC_ACT_OK;
arphdr = (struct arphdr *)(ethhdr + 1);
if ((void *)(arphdr + 1) > data_end)
return TC_ACT_OK;
arpdata = (struct arpdata *)(arphdr + 1);
if ((void *)(arpdata + 1) > data_end)
return TC_ACT_OK;
if (arphdr->ar_op != bpf_htons(ARPOP_REQUEST))
return TC_ACT_OK;
/*
* construct APR response, we will always return 12:34:56:78:9a:bc
*/
daddr = (union macaddr *)ethhdr->h_dest;
daddr->addr[0] = 0x12;
daddr->addr[1] = 0x34;
daddr->addr[2] = 0x56;
daddr->addr[3] = 0x78;
daddr->addr[4] = 0x9a;
daddr->addr[5] = 0xbc;
/* update ethernet header */
saddr = (union macaddr *)ethhdr->h_source;
swap_macaddr(saddr, daddr);
/* update ARP header */
arphdr->ar_op = bpf_htons(ARPOP_REPLY);
/* update ARP payload */
ip = arpdata->sip;
arpdata->sip = arpdata->tip;
arpdata->tip = ip;
__builtin_memcpy(arpdata->sha, ethhdr->h_source, sizeof(ethhdr->h_source));
__builtin_memcpy(arpdata->tha, ethhdr->h_dest, sizeof(ethhdr->h_dest));
return bpf_redirect(ctx->ingress_ifindex, 0);
}
char __license[] SEC("license") = "GPL";
执行这段代码之后,我们再测试一下:
ip netns exec ns1 ping -I ns1-eth0 -c 1 -w 1 10.0.2.1
PING 10.0.2.1 (10.0.2.1) from 10.0.1.1 ns1-eth0: 56(84) bytes of data.
--- 10.0.2.1 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
$ ip netns exec ns1 arp -i ns1-eth0
Address HWtype HWaddress Flags Mask Iface
10.0.2.1 ether 12:34:56:78:9a:bc C ns1-eth0
现在依然 PING 不通目的 IP,但是现在我们有了预期的 ARP 条目了。