bpf_redirect是eBPF提供给XDP程序的一组辅助函数,它允许数据包在网卡驱动收包的最早期阶段被直接转发到另一块网卡或者某个map指定的目的地,全程绕过内核协议栈。这种模式下,数据包不经过IP层的路由查找、不经过netfilter钩子、也不产生skb,开销极小。本文将从原理、代码实现、辅助函数选型三个层面详细讲解如何用它搭建一个高性能转发器。

一、XDP的四种返回动作与重定向原理
要理解bpf_redirect的工作方式,首先要弄清XDP程序挂载后能做什么。XDP程序在网卡驱动的收包回调中执行,执行完毕后通过一个返回值告诉内核如何处置这个包。内核定义了四个基础动作:XDP_PASS表示正常送入协议栈,XDP_DROP表示直接丢弃,XDP_TX表示从收到包的同一块网卡原路弹回,而XDP_REDIRECT则表示把包送去别的地方,可以是另一块网卡、CPU,或者一个AF_XDP socket。
当XDP程序返回XDP_REDIRECT时,内核并不会自己决定去哪里,目的地完全由你在程序里调用bpf_redirect或bpf_redirect_map时指定。以最简单的形式为例,bpf_redirect(ifindex, 0)的第一个参数是目标网卡的接口索引,第二个参数目前必须填0。这个函数调用本身并不立即移动数据包,它只是把目的地记录在per-CPU的临时存储里,真正把包从目标网卡发出的是内核随后执行的ndo_xdp_xmit路径。理解这一点很重要,因为目标网卡必须支持NDK_XDP能力,否则加载时虽然不报错,运行时包会被静默丢弃。
另一个容易忽视的细节是,bpf_redirect不会替你修改数据包内容。从eth0收到的包直接从eth1发出时,以太网头的源MAC仍然是eth0的MAC,目的MAC也还是上游设备的MAC,下游交换机大概率会丢掉这种"来路不明"的帧。所以转发程序必须在重定向前手动交换或改写以太网头部,这是后面代码实现的核心部分。
二、实现一个双向二层转发器
下面用一段完整的代码实现eth0与eth1之间的双向转发。程序逻辑很简单:解析以太网头,判断收到的是IPv4或ARP包,交换源MAC和目的MAC,然后重定向到对端网卡。两个网卡的ifindex通过map注入,这样程序本身不依赖硬编码的接口编号,重新加载时只需更新map而无需重新编译。
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 2);
__type(key, __u32);
__type(value, __u32);
} ifindex_map SEC(".maps");
/* key约定: 0 对应eth1的ifindex, 1 对应eth0的ifindex */
SEC("xdp")
int xdp_redirect_fwd(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;
__u16 proto = bpf_ntohs(eth->h_proto);
/* 只转发IPv4和ARP,其他协议交给协议栈 */
if (proto != ETH_P_IP && proto != ETH_P_ARP)
return XDP_PASS;
/* 交换源MAC和目的MAC,保证下游设备能正确回包 */
unsigned char tmp[ETH_ALEN];
__builtin_memcpy(tmp, eth->h_source, ETH_ALEN);
__builtin_memcpy(eth->h_source, eth->h_dest, ETH_ALEN);
__builtin_memcpy(eth->h_dest, tmp, ETH_ALEN);
/* 根据入口网卡选择出口 */
__u32 key = (ctx->ingress_ifindex == 1) ? 1 : 0;
__u32 *out_if = bpf_map_lookup_elem(&ifindex_map, &key);
if (!out_if)
return XDP_DROP;
return bpf_redirect(*out_if, 0);
}
char _license[] SEC("license") = "GPL";
这段代码有几个值得展开的点。第一,所有对包数据的访问都必须配对data_end边界检查,这是eBPF验证器的要求,即使逻辑上包一定大于14字节的以太网头,验证器也不会放过任何未检查的指针解引用。第二,MAC交换用的是__builtin_memcpy而不是手写循环,固定长度的拷贝会被编译器展开成几条mov指令,验证器处理起来也更友好。第三,判断入口用的是ctx->ingress_ifindex,但更优雅的做法是维护一张方向表,读者可以按需扩展。
加载和注入参数可以用bpftool完成,典型流程如下。假设eth0的ifindex是2,eth1是3:
clang -O2 -target bpf -c fwd.c -o fwd.o # 挂载XDP程序到两块网卡,native模式需要网卡驱动支持 ip link set dev eth0 xdpgeneric obj fwd.o sec xdp ip link set dev eth1 xdpgeneric obj fwd.o sec xdp # 注入对端ifindex bpftool map update pinned /sys/fs/bpf/xdp/ifindex_map key 0 0 0 0 value 3 0 0 0 bpftool map update pinned /sys/fs/bpf/xdp/ifindex_map key 1 0 0 0 value 2 0 0 0
三、bpf_redirect、bpf_redirect_map与bpf_fib_lookup如何选择
内核提供了不止一种重定向手段,选型不当会白白损失性能。直接调用bpf_redirect(ifindex, flags)每次都要在程序里写死或查表拿到ifindex,逻辑直观,适合端口数量固定的场景。而bpf_redirect_map(map, key, flags)把目的地查找下沉到map内部,配合DEVMAP使用时,内核可以直接从map项里取出目标设备的发送队列,省去了一次显式查表和函数调用开销,在端口很多的负载均衡场景下优势明显。
两种重定向还支持BPF_F_BROADCAST和BPF_F_EXCLUDE_INGRESS标志,配合BPF_MAP_TYPE_DEVMAP_HASH可以实现广播语义,这在实现简易的DHCP转发或者组播复制时非常方便。下面是DEVMAP版本的核心差异代码:
struct {
__uint(type, BPF_MAP_TYPE_DEVMAP);
__uint(max_entries, 64);
__type(key, __u32);
__type(value, __u32);
} tx_ports SEC(".maps");
SEC("xdp")
int xdp_fwd_map(struct xdp_md *ctx)
{
/* ...解析并交换MAC的逻辑与前例相同,此处省略... */
/* 用目的IP或者端口哈希作为key,直接从DEVMAP重定向 */
__u32 key = dst_ip & 0x3f;
return bpf_redirect_map(&tx_ports, key, 0);
}
如果目标是实现一个真正的三层转发器,也就是要根据路由表决定下一跳,还需要引入bpf_fib_lookup。这个辅助函数会查询内核FIB,返回下一跳的MAC地址和出口网卡,程序拿到结果后改写以太网头再调用bpf_redirect。它的开销比纯map查表高,但换来的好处是不需要在eBPF侧维护路由镜像,路由变化时内核自动生效。值得一提的是,若bpf_fib_lookup返回BPF_FIB_LKUP_RET_NO_NEIGH,说明邻居表还没有下一跳的MAC,此时应返回XDP_PASS把包交给协议栈触发ARP,这是新手最常踩的坑之一,直接DROP会导致流量黑洞直到邻居表超时。
四、性能与调试要点
工作模式对性能影响巨大。XDP有native、generic、offloaded三种模式:native模式要求驱动支持,包在DMA阶段就被处理,性能最好;generic模式(xdpgeneric)作为软件兜底可以挂在任何网卡上,但执行点在协议栈入口,性能接近于普通eBPF过滤器;offloaded模式把程序下放到智能网卡执行。生产环境务必先确认驱动是否支持native XDP,可以用ethtool -i eth0查看驱动,常见支持的包括ixgbe、i40e、mlx5、virtio等。
调试方面,最实用的工具是bpftool prog tracelog配合内核的trace_pipe,程序里用bpf_printk打点可以快速定位逻辑分支。另一个硬性约束是XDP程序不支持调用bpf_xdp_adjust_meta之外的大部分helper,且指令数上限为100万条,复杂逻辑应尽量拆分到map查表完成。验证器报错信息会精确到指令偏移,遇到"R1 offset is outside of the packet"这类错误时,回头检查data_end边界检查是否覆盖了所有指针运算路径即可。
最后总结一下:bpf_redirect提供的是一条绕过协议栈的最短转发路径,核心工作量在于正确改写二层头部和选对目的地查找方式。固定端口转发用bpf_redirect,多端口分发用DEVMAP加bpf_redirect_map,需要跟随内核路由就用bpf_fib_lookup兜底。结合native XDP模式,单核转发千万级pps并不困难,这也是Katran、Cilium等高吞吐网络组件选择XDP的根本原因。
XDPbpf_redirecteBPF网络编程修改时间:2026-09-03 04:35:00