XDP to TC : TUN eBPF NAT

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_table BPF 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:1234510.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_keystruct 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:1234510.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. 总结与心得

  1. XDP 不适用于 TUN/TAP

    XDP 需要网卡驱动支持,而 TUN 是纯软件设备,数据包不经过驱动层。虽然可以加载,但不会处理任何包。这是本次实验最大的教训。

  2. TC 是虚拟设备的理想选择

    TC 工作在协议栈的入口/出口点,对所有网络设备(包括虚拟设备)都有效。通过 clsact qdisc 挂载 BPF 程序,可以灵活处理进出 TUN 的流量。

  3. 工具链的重要性
    bpftooltctcpdumpsocat 是实验成功的关键。bpftool 提供的 map 查看、更新功能极大简化了调试。

  4. 网络配置细节不容忽视

    • rp_filter 必须关闭,否则内核会因非对称路由丢弃回包。
    • 路由必须精确,确保目标 IP 的下一跳是 TUN 设备。
    • 使用 curl --interface--local-port 强制指定源信息,可以避免路由选择干扰。
  5. BPF 程序的校验和更新

    修改 IP 和 TCP 头后,必须正确更新校验和。代码中使用 bpf_l3_csum_replacebpf_l4_csum_replace,注意 BPF_F_PSEUDO_HDR 标志的使用。

  6. 逐步迭代,善用打印

    在开发过程中,通过 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 地址和端口均为实验用途,请根据实际环境替换。如遇问题,欢迎留言交流。

相关推荐
花开莫与流年错_2 小时前
ZeroMQ基本示例使用
c++·消息队列·mq·示例·zeromq
qq_416018723 小时前
C++中的模板方法模式
开发语言·c++·算法
jyyyx的算法博客3 小时前
KMP 算法
c++·kmp
Emberone4 小时前
从C到C++:一脚踹开面向对象的大门
开发语言·c++
DDzqss4 小时前
3.25打卡day45
c++·算法
JMchen1235 小时前
Android NDK开发从入门到实战:解锁应用性能的终极武器
android·开发语言·c++·python·c#·android studio·ndk开发
程序猿编码6 小时前
隐匿注入型ELF加壳器:原理、设计与实现深度解析(C/C++ 代码实现)
c语言·网络·c++·elf·代码注入
m0_734998017 小时前
Day 26
数据结构·c++·算法
Summer_Uncle8 小时前
【QT学习】Qt界面布局的生命周期和加载时机
c++·qt