XDP to TC : TUN eBPF NAT
引言
在虚拟化、容器化以及 VNP 网关等场景中,经常需要对进出 TUN/TAP 设备的流量进行地址转换(NAT)。传统的 iptables 虽然功能强大,但在高吞吐场景下存在性能瓶颈;而 eBPF 的出现为我们提供了在内核中安全、高效地执行自定义数据包处理的能力。
本文记录了我在 WSL2 环境下,从零开始,尝试用 eBPF 为 TUN 设备实现自定义 NAT 的完整过程。期间经历了 iptables/nftables 的基础验证、conntrack 的探索、XDP 方案的尝试与失败 (因为 TUN 设备不支持 XDP),最终转向 TC(Traffic Control) 并成功实现。
文中所有命令行均源自实际实验,重点展示了 XDP 的加载、卸载、map 管理、调试流程 ,以及 TC 方案的完整实现。希望这篇记录能为同样在 eBPF 网络编程路上探索的读者提供一份详实的参考。
1. 环境准备
实验环境:WSL2 (Ubuntu 22.04) ,内核版本 5.15.0-173-generic。
1.1 更新系统并安装基础工具
bash
sudo apt update -y
sudo apt upgrade -y
sudo apt install -y build-essential clang llvm git vim net-tools iproute2 tcpdump curl socat
1.2 安装 eBPF 开发所需库
bash
sudo apt install -y libbpf-dev libelf-dev libz-dev
1.3 安装 linux-tools 并创建 bpftool 软链接
bpftool 是管理 eBPF 程序的关键工具。WSL2 的 linux-tools 包安装后,bpftool 可能不在 PATH 中,需手动链接。
bash
sudo apt install -y linux-tools-common linux-tools-generic
# 查找 bpftool 的实际位置
find / -name "bpftool" 2>/dev/null
# 输出示例:/usr/lib/linux-tools-5.15.0-173/bpftool
# 建立软链接
sudo ln -sf /usr/lib/linux-tools-$(uname -r)/bpftool /usr/sbin/bpftool
1.4 准备项目目录
bash
cd ~
mkdir dd && cd dd
mkdir xdp && cd xdp # 后续所有代码和编译产物均放在此目录
2. 基础探索:iptables 与 conntrack
在正式开始 eBPF 之前,先用传统方法验证网络环境并熟悉 NAT 行为。
bash
# 添加一条 DNAT 规则:将访问本机 20000 端口的流量转发到 7.7.7.7:20000
sudo iptables -t nat -A PREROUTING -p tcp --dport 20000 -j DNAT --to-destination 7.7.7.7:20000
# 查看规则
sudo iptables -t nat -L -n
# 清空规则(后续会用到)
sudo iptables -t nat -F
安装并尝试 libnetfilter_conntrack,编写简单的查询程序(见历史记录中的 query_nat.cpp):
bash
sudo apt install -y libnetfilter-conntrack-dev
g++ -o query_nat query_nat.cpp -lnetfilter_conntrack -lmnl
sudo ./query_nat
虽然 conntrack 能提供连接状态,但无法满足我们对 TUN 设备上流量的精细化控制需求。因此决定转向 eBPF。
3. XDP 方案的尝试与失败
3.1 编写 XDP 程序 xdp_tun_nat.c
在 xdp/ 目录下创建 xdp_tun_nat.c,核心思路是:
- 定义
nat_tableBPF MAP 存储 NAT 规则。 - 在 XDP 入口处解析数据包,匹配规则并修改 IP 地址/端口。
c
// xdp_tun_nat.c(简化示例)
#include <linux/bpf.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
struct nat_key {
__be32 src_ip;
__be32 dst_ip;
__be16 src_port;
__be16 dst_port;
__u16 proto;
__u16 pad;
};
struct nat_value {
__be32 mapped_addr;
__be16 mapped_port;
__u8 is_dnat;
__u8 pad;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, struct nat_key);
__type(value, struct nat_value);
__uint(max_entries, 16384);
} nat_table SEC(".maps");
SEC("xdp")
int xdp_nat_func(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
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 || ip->version != 4)
return XDP_PASS;
if (ip->protocol != IPPROTO_TCP)
return XDP_PASS;
__u32 ip_hdr_len = ip->ihl << 2;
if (ip_hdr_len < sizeof(struct iphdr))
return XDP_PASS;
struct tcphdr *tcp = (void *)ip + ip_hdr_len;
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
struct nat_key key = {
.src_ip = ip->saddr,
.dst_ip = ip->daddr,
.src_port = tcp->source,
.dst_port = tcp->dest,
.proto = IPPROTO_TCP,
};
struct nat_value *val = bpf_map_lookup_elem(&nat_table, &key);
if (!val)
return XDP_PASS;
// 执行 DNAT/SNAT 修改(略)
// 注意:XDP 修改后通常需要重定向或发送,此处省略具体实现
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
3.2 编译 XDP 程序
bash
clang -target bpf -O2 -g -Wall \
-I/usr/include/x86_64-linux-gnu \
-I/usr/src/linux-headers-$(uname -r)/include \
-I/usr/src/linux-headers-$(uname -r)/arch/x86/include \
-I/usr/include/bpf \
-c xdp_tun_nat.c -o xdp_tun_nat.o
3.3 创建 TUN 设备(使用 socat)
由于 WSL2 中直接使用 ip tuntap 创建 TUN 设备存在兼容性问题,改用 socat。
bash
# 创建 TUN 设备,分配 IP 10.0.0.1/24,并设置为 UP
socat TUN:10.0.0.1/24,up,iff-up,iff-running,iff-noarp,iff-pointopoint,up - &
# 确认设备存在
ip link show tun0
ifconfig
# 添加路由:发往 10.0.0.2 的流量走 tun0
sudo ip route add 10.0.0.2/32 dev tun0
# 关闭反向路径过滤
echo 0 | sudo tee /proc/sys/net/ipv4/conf/all/rp_filter
echo 0 | sudo tee /proc/sys/net/ipv4/conf/tun0/rp_filter
3.4 加载 XDP 程序到 TUN 设备
bash
# 尝试加载
sudo ip link set dev tun0 xdp obj xdp_tun_nat.o sec xdp
# 查看加载状态
ip link show tun0 | grep xdp
# 预期输出:xdp
3.5 管理 XDP 程序与 BPF Map
3.5.1 查看已加载的 BPF 程序
bash
# 列出所有 BPF 程序
bpftool prog show | grep xdp_tun_nat
# 示例输出:
# 260: xdp name xdp_nat_func tag 12345678 gpl
3.5.2 挂载 BPF 文件系统(用于操作 Map)
bash
sudo mount -t bpf none /sys/fs/bpf
3.5.3 找到 BPF Map 的 ID 并挂载到文件系统
bash
# 列出所有 BPF Map
bpftool map show | grep nat_table
# 假设 nat_table 的 ID 为 2
sudo bpftool map pin id 2 /sys/fs/bpf/nat_table
# 查看挂载的 map
ls -l /sys/fs/bpf/nat_table
3.5.4 操作 BPF Map:添加、查看、删除规则
添加 DNAT 规则 :将 10.0.0.1:12345 → 10.0.0.2:8080 的流量 DNAT 到 111.63.65.103:80。
bash
# 方式一:使用 bpftool 直接操作(key/value 需为十六进制)
# key: 10.0.0.1(0a000001) + 10.0.0.2(0a000002) + 12345(0x3039) + 8080(0x1f90)
# value: 111.63.65.103(0x6f3f4167) + 80(0x0050) + is_dnat=1
sudo bpftool map update id 2 key hex 0a0000010a00000230391f90 value hex 6f3f4167005001
# 查看 map 内容
sudo bpftool map dump id 2
# 删除规则
sudo bpftool map delete id 2 key hex 0a0000010a00000230391f90
方式二:使用用户态程序 (参考后面 TC 部分的 loader 类似设计,但 XDP 未提供,此处仅展示 bpftool 方式)。
3.5.5 卸载 XDP 程序
bash
sudo ip link set dev tun0 xdp off
3.6 调试 XDP 程序
如果 XDP 程序未按预期工作,可以通过 bpf_printk 添加调试输出。
c
// 在 xdp_tun_nat.c 中添加
#include <bpf/bpf_helpers.h>
// ...
bpf_printk("key: src=%u dst=%u\n", key.src_ip, key.dst_ip);
重新编译加载。然后查看调试输出:
bash
# 挂载 debugfs
sudo mount -t debugfs none /sys/kernel/debug
# 实时查看打印
sudo cat /sys/kernel/debug/tracing/trace_pipe
发送测试流量(如 curl --interface 10.0.0.1 --local-port 12345 http://10.0.0.2:8080),观察打印信息。
3.7 发现 XDP 不适用于 TUN 设备
经过上述测试,虽然 XDP 程序加载成功,但 trace_pipe 中没有任何打印,tcpdump 显示 tun0 上的流量并未被程序处理。原因在于:XDP 需要网卡驱动直接支持,而 TUN 是纯软件虚拟设备,数据包在协议栈内部生成,不经过驱动层,因此 XDP 程序虽然可以附加,但实际不会处理任何数据包。
这是一个重要的经验教训:XDP 不适用于 TUN/TAP 设备 。因此,我们转向 TC(Traffic Control) 方案。
4. TC 方案的成功实践
4.1 编写 TC 程序 tc_redirect.c
TC 程序的结构与 XDP 类似,但挂载点为 classifier,通常用于 egress 方向。以下是最终可用的代码(源自你的历史记录,并已完善):
c
// tc_redirect.c - TC egress NAT(正向DNAT + 反向SNAT)
#include <linux/bpf.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#ifndef IPPROTO_TCP
#define IPPROTO_TCP 6
#endif
#ifndef TC_ACT_OK
#define TC_ACT_OK 0
#endif
struct nat_key {
__be32 src_ip;
__be32 dst_ip;
__be16 src_port;
__be16 dst_port;
__u16 proto;
__u16 pad;
} __attribute__((packed));
struct nat_value {
__be32 mapped_addr;
__be16 mapped_port;
__u8 is_dnat;
__u8 pad;
} __attribute__((packed));
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, struct nat_key);
__type(value, struct nat_value);
__uint(max_entries, 16384);
__uint(map_flags, BPF_F_NO_PREALLOC);
} nat_map SEC(".maps");
SEC("classifier")
int tc_egress_nat(struct __sk_buff* skb) {
void* data_end = (void*)(long)skb->data_end;
void* data = (void*)(long)skb->data;
struct iphdr* ip = data;
if ((void*)(ip + 1) > data_end || ip->version != 4 || ip->protocol != IPPROTO_TCP)
return TC_ACT_OK;
if (ip->frag_off & bpf_htons(0x2000 | 0x1FFF))
return TC_ACT_OK;
__u32 ip_hdr_len = ip->ihl << 2;
if (ip_hdr_len < sizeof(struct iphdr))
return TC_ACT_OK;
struct tcphdr* tcp = (void*)ip + ip_hdr_len;
if ((void*)(tcp + 1) > data_end)
return TC_ACT_OK;
struct nat_key key = {
.src_ip = ip->saddr,
.dst_ip = ip->daddr,
.src_port = tcp->source,
.dst_port = tcp->dest,
.proto = IPPROTO_TCP,
};
struct nat_value* val = bpf_map_lookup_elem(&nat_map, &key);
if (!val)
return TC_ACT_OK;
__u32 ip_csum_off = (char*)&ip->check - (char*)data;
__u32 tcp_csum_off = ip_hdr_len + offsetof(struct tcphdr, check);
if (val->is_dnat) {
__be32 old_daddr = ip->daddr;
__be16 old_dport = tcp->dest;
ip->daddr = val->mapped_addr;
tcp->dest = val->mapped_port;
bpf_l3_csum_replace(skb, ip_csum_off, old_daddr, val->mapped_addr, 4);
bpf_l4_csum_replace(skb, tcp_csum_off, old_daddr, val->mapped_addr, 4 | BPF_F_PSEUDO_HDR);
bpf_l4_csum_replace(skb, tcp_csum_off, old_dport, val->mapped_port, 2);
}
else {
__be32 old_saddr = ip->saddr;
__be16 old_sport = tcp->source;
ip->saddr = val->mapped_addr;
tcp->source = val->mapped_port;
bpf_l3_csum_replace(skb, ip_csum_off, old_saddr, val->mapped_addr, 4);
bpf_l4_csum_replace(skb, tcp_csum_off, old_saddr, val->mapped_addr, 4 | BPF_F_PSEUDO_HDR);
bpf_l4_csum_replace(skb, tcp_csum_off, old_sport, val->mapped_port, 2);
}
return bpf_redirect(skb->ifindex, BPF_F_INGRESS);
}
char _license[] SEC("license") = "GPL";
4.2 编写用户态控制程序 loader.c
该程序负责:
- 将 BPF 程序加载到内核并附加到指定设备的 TC 挂载点。
- 提供
add/del/dump命令来管理 BPF Map 中的 NAT 规则。 - 将 BPF Map 挂载到
/sys/fs/bpf/tun_nat_map,方便后续操作。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <net/if.h>
#include "common.h"
static struct bpf_tc_hook hook = { 0 };
static struct bpf_tc_opts opts = { 0 };
static void usage(const char* name) {
fprintf(stderr, "用法: %s <命令> [参数]\n", name);
fprintf(stderr, "命令:\n");
fprintf(stderr, " attach <网卡名> 加载 TC + pin MAP\n");
fprintf(stderr, " detach <网卡名> 卸载 TC\n");
fprintf(stderr, " add <src_ip> <src_port> <dst_ip> <dst_port> <new_ip> <new_port> <is_dnat>\n");
fprintf(stderr, " del <src_ip> <src_port> <dst_ip> <dst_port>\n");
fprintf(stderr, " dump 查看所有规则\n");
fprintf(stderr, "示例:\n");
fprintf(stderr, " sudo %s attach tun0\n", name);
fprintf(stderr, " sudo %s add 192.168.1.100 12345 8.8.8.8 443 127.0.0.1 8080 1\n", name);
exit(1);
}
int main(int argc, char** argv) {
if (argc < 2) usage(argv[0]);
const char* cmd = argv[1];
// ==================== attach ====================
if (strcmp(cmd, "attach") == 0) {
if (argc != 3) usage(argv[0]);
const char* ifname = argv[2];
__u32 ifindex = if_nametoindex(ifname);
if (ifindex == 0) { perror("if_nametoindex"); return 1; }
struct bpf_object* obj = bpf_object__open_file("tc_redirect.o", NULL);
if (libbpf_get_error(obj)) { fprintf(stderr, "打开 tc_redirect.o 失败\n"); return 1; }
struct bpf_program* prog = bpf_object__find_program_by_name(obj, "tc_egress_nat");
bpf_program__set_type(prog, BPF_PROG_TYPE_SCHED_CLS);
if (bpf_object__load(obj)) { fprintf(stderr, "加载 eBPF 失败\n"); goto cleanup_attach; }
// pin map
struct bpf_map* map = bpf_object__find_map_by_name(obj, "nat_map");
if (map && bpf_map__pin(map, "/sys/fs/bpf/tun_nat_map") && errno != EEXIST)
fprintf(stderr, "警告: MAP pin 失败\n");
else
printf("nat_map 已 pin 到 /sys/fs/bpf/tun_nat_map\n");
hook.ifindex = ifindex;
hook.attach_point = BPF_TC_EGRESS;
int err = bpf_tc_hook_create(&hook);
if (err && err != -EEXIST) { fprintf(stderr, "创建 TC hook 失败\n"); goto cleanup_attach; }
opts.sz = sizeof(opts);
opts.prog_fd = bpf_program__fd(prog);
opts.handle = 1;
opts.priority = 1;
opts.flags = BPF_TC_F_REPLACE;
err = bpf_tc_attach(&hook, &opts);
if (err) { fprintf(stderr, "附加 TC 失败: %d\n", err); goto cleanup_attach; }
printf("TC egress NAT 已成功附加到 %s\n", ifname);
printf(" 现在可以使用 add / del / dump 管理规则\n");
cleanup_attach:
bpf_object__close(obj);
return 0;
}
// ==================== detach ====================
if (strcmp(cmd, "detach") == 0) {
if (argc != 3) usage(argv[0]);
const char* ifname = argv[2];
__u32 ifindex = if_nametoindex(ifname);
if (ifindex == 0) { perror("if_nametoindex"); return 1; }
hook.ifindex = ifindex;
hook.attach_point = BPF_TC_EGRESS;
opts.sz = sizeof(opts);
opts.prog_fd = 0;
opts.handle = 1;
opts.priority = 1;
if (bpf_tc_detach(&hook, &opts) == 0)
printf("TC 已从 %s 卸载\n", ifname);
else
fprintf(stderr, "卸载失败\n");
return 0;
}
// ==================== add ====================
if (strcmp(cmd, "add") == 0) {
if (argc != 9) usage(argv[0]);
struct nat_key key = { 0 };
struct nat_value val = { 0 };
if (inet_pton(AF_INET, argv[2], &key.src_ip) != 1) goto ip_err;
key.src_port = htons(atoi(argv[3]));
if (inet_pton(AF_INET, argv[4], &key.dst_ip) != 1) goto ip_err;
key.dst_port = htons(atoi(argv[5]));
key.proto = IPPROTO_TCP;
if (inet_pton(AF_INET, argv[6], &val.mapped_addr) != 1) goto ip_err;
val.mapped_port = htons(atoi(argv[7]));
val.is_dnat = atoi(argv[8]);
int map_fd = bpf_obj_get("/sys/fs/bpf/tun_nat_map");
if (map_fd < 0) { perror("打开 map 失败(请先执行 attach)"); return 1; }
if (bpf_map_update_elem(map_fd, &key, &val, BPF_ANY) == 0)
printf("规则添加成功\n");
else
perror("添加失败");
close(map_fd);
return 0;
}
// ==================== del ====================
if (strcmp(cmd, "del") == 0) {
if (argc != 6) usage(argv[0]);
struct nat_key key = { 0 };
if (inet_pton(AF_INET, argv[2], &key.src_ip) != 1) goto ip_err;
key.src_port = htons(atoi(argv[3]));
if (inet_pton(AF_INET, argv[4], &key.dst_ip) != 1) goto ip_err;
key.dst_port = htons(atoi(argv[5]));
key.proto = IPPROTO_TCP;
int map_fd = bpf_obj_get("/sys/fs/bpf/tun_nat_map");
if (map_fd < 0) { perror("打开 map 失败"); return 1; }
if (bpf_map_delete_elem(map_fd, &key) == 0)
printf("规则删除成功\n");
else
perror("删除失败");
close(map_fd);
return 0;
}
// ==================== dump ====================
if (strcmp(cmd, "dump") == 0) {
int map_fd = bpf_obj_get("/sys/fs/bpf/tun_nat_map");
if (map_fd < 0) { perror("打开 map 失败(请先 attach)"); return 1; }
struct nat_key key = { 0 }, next_key;
struct nat_value val;
char ip1[INET_ADDRSTRLEN], ip2[INET_ADDRSTRLEN], ip3[INET_ADDRSTRLEN];
printf("当前 NAT 规则:\n");
printf("-----------------------------------------------------------------\n");
printf("源IP:端口 → 目的IP:端口 | 映射后IP:端口 | 类型\n");
printf("-----------------------------------------------------------------\n");
while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
if (bpf_map_lookup_elem(map_fd, &next_key, &val) == 0) {
inet_ntop(AF_INET, &next_key.src_ip, ip1, sizeof(ip1));
inet_ntop(AF_INET, &next_key.dst_ip, ip2, sizeof(ip2));
inet_ntop(AF_INET, &val.mapped_addr, ip3, sizeof(ip3));
printf("%s:%-5d → %s:%-5d | %s:%-5d | %s\n",
ip1, ntohs(next_key.src_port),
ip2, ntohs(next_key.dst_port),
ip3, ntohs(val.mapped_port),
val.is_dnat ? "DNAT" : "SNAT");
}
memcpy(&key, &next_key, sizeof(key));
}
close(map_fd);
return 0;
}
ip_err:
fprintf(stderr, "IP 地址格式错误\n");
return 1;
usage(argv[0]);
return 0;
}
同时需要一个头文件 common.h,定义 struct nat_key 和 struct nat_value(与 BPF 程序一致):
c
// common.h
#ifndef __COMMON_H
#define __COMMON_H
#include <linux/types.h>
struct nat_key {
__be32 src_ip;
__be32 dst_ip;
__be16 src_port;
__be16 dst_port;
__u16 proto;
__u16 pad;
};
struct nat_value {
__be32 mapped_addr;
__be16 mapped_port;
__u8 is_dnat;
__u8 pad;
};
#endif
4.3 编写 Makefile
makefile
CFLAGS = -O2 -g -Wall -Werror
all: loader
loader: loader.c common.h tc_redirect.o
$(CC) $(CFLAGS) -o loader loader.c -lbpf
tc_redirect.o: tc_redirect.c
clang -O2 -target bpf -I/usr/include -I/usr/include/x86_64-linux-gnu -c tc_redirect.c -o tc_redirect.o
clean:
rm -f *.o loader
4.4 编译
bash
make
成功后生成 loader 可执行文件。
4.5 创建 TUN 设备并配置网络
bash
# 启动 socat 创建 tun0(后台运行)
socat TUN:10.0.0.1/24,up,iff-up,iff-running,iff-noarp,iff-pointopoint,up - &
# 确认设备存在
ip link show tun0
# 添加路由:发往 10.0.0.2 的流量走 tun0
sudo ip route add 10.0.0.2/32 dev tun0
# 添加外部路由(例如 NAT 目标 IP 为 111.63.65.103,需通过物理网卡 eth0 可达)
sudo ip route add 111.63.65.103/32 dev eth0
# 关闭反向路径过滤
echo 0 | sudo tee /proc/sys/net/ipv4/conf/all/rp_filter
echo 0 | sudo tee /proc/sys/net/ipv4/conf/tun0/rp_filter
4.6 加载 TC 程序
bash
sudo ./loader attach tun0
输出应类似:
nat_map 已 pin 到 /sys/fs/bpf/tun_nat_map
TC egress NAT 已成功附加到 tun0
现在可以使用 add / del / dump 管理规则
验证附加状态:
bash
sudo tc filter show dev tun0 egress
期望看到类似输出:
filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 tc_redirect.o:[classifier] direct-action not_in_hw
4.7 添加 DNAT 规则并测试
添加规则 :将 10.0.0.1:12345 → 10.0.0.2:8080 的流量 DNAT 到 111.63.65.103:80。
bash
sudo ./loader add 10.0.0.1 12345 10.0.0.2 8080 111.63.65.103 80 1
查看规则:
bash
sudo ./loader dump
发起请求:
bash
curl --interface 10.0.0.1 --local-port 12345 http://10.0.0.2:8080 -v
抓包验证:
bash
# 终端1:监控 tun0
sudo tcpdump -i tun0 -n -v -e
# 终端2:监控物理网卡 eth0,只抓与 NAT 目标 IP 相关的包
sudo tcpdump -i eth0 -n -v host 111.63.65.103
如果 eth0 上出现发往 111.63.65.103:80 的包,则 DNAT 成功。
4.8 调试 TC 程序
若 NAT 未生效,可通过 bpf_printk 查看程序执行流程。在 tc_redirect.c 的关键位置添加:
c
bpf_printk("key: src=%u dst=%u sport=%u dport=%u\n", key.src_ip, key.dst_ip, key.src_port, key.dst_port);
重新编译加载,然后查看调试输出:
bash
sudo cat /sys/kernel/debug/tracing/trace_pipe
发送测试流量,观察打印信息。
5. 清理与卸载
5.1 卸载 TC 程序
bash
sudo ./loader detach tun0
5.2 删除 BPF Map 挂载点
bash
sudo rm -f /sys/fs/bpf/tun_nat_map
5.3 删除路由
bash
sudo ip route del 10.0.0.2/32 dev tun0
sudo ip route del 111.63.65.103/32 dev eth0 # 如果添加过
5.4 关闭 TUN 设备(终止 socat 进程)
bash
sudo pkill socat # 或 ps aux | grep socat 后 kill
5.5 恢复 rp_filter 默认值(可选)
bash
echo 1 | sudo tee /proc/sys/net/ipv4/conf/all/rp_filter
echo 1 | sudo tee /proc/sys/net/ipv4/conf/tun0/rp_filter
6. 总结与心得
-
XDP 不适用于 TUN/TAP
XDP 需要网卡驱动支持,而 TUN 是纯软件设备,数据包不经过驱动层。虽然可以加载,但不会处理任何包。这是本次实验最大的教训。
-
TC 是虚拟设备的理想选择
TC 工作在协议栈的入口/出口点,对所有网络设备(包括虚拟设备)都有效。通过
clsactqdisc 挂载 BPF 程序,可以灵活处理进出 TUN 的流量。 -
工具链的重要性
bpftool、tc、tcpdump、socat是实验成功的关键。bpftool提供的 map 查看、更新功能极大简化了调试。 -
网络配置细节不容忽视
rp_filter必须关闭,否则内核会因非对称路由丢弃回包。- 路由必须精确,确保目标 IP 的下一跳是 TUN 设备。
- 使用
curl --interface和--local-port强制指定源信息,可以避免路由选择干扰。
-
BPF 程序的校验和更新
修改 IP 和 TCP 头后,必须正确更新校验和。代码中使用
bpf_l3_csum_replace和bpf_l4_csum_replace,注意BPF_F_PSEUDO_HDR标志的使用。 -
逐步迭代,善用打印
在开发过程中,通过
bpf_printk输出关键信息,结合trace_pipe观察,可以快速定位问题。
通过这次完整的探索,我从 iptables 入门,经历了 XDP 的挫折,最终成功用 TC + eBPF 实现了 TUN 设备上的自定义 NAT。希望这篇文章能为你的 eBPF 实践提供一份详实的参考。
附录:常用命令速查
| 操作 | 命令 |
|---|---|
| 编译 BPF 程序 | clang -target bpf -O2 -g -I... -c prog.c -o prog.o |
| 加载 XDP | ip link set dev tun0 xdp obj prog.o sec xdp |
| 卸载 XDP | ip link set dev tun0 xdp off |
| 查看 XDP 状态 | `ip link show tun0 |
| 加载 TC 程序 | tc qdisc add dev tun0 clsact tc filter add dev tun0 egress bpf obj prog.o sec classifier |
| 查看 TC 程序 | tc filter show dev tun0 egress |
| 查看 BPF 程序 | bpftool prog show |
| 挂载 BPF Map | bpftool map pin id <id> /sys/fs/bpf/map_name |
| 更新 Map | bpftool map update id <id> key hex ... value hex ... |
| 创建 TUN(socat) | socat TUN:10.0.0.1/24,up,iff-up,iff-running,iff-noarp,iff-pointopoint,up - & |
| 查看 debug 打印 | sudo cat /sys/kernel/debug/tracing/trace_pipe |
注:本文中使用的 IP 地址和端口均为实验用途,请根据实际环境替换。如遇问题,欢迎留言交流。