TC Hairpin NAT 驱动使用手册(个人版)

TC Hairpin NAT 驱动使用手册(个人版)

说明 :本文档仅供个人回顾使用,记录了基于 TC eBPF 实现 Hairpin NAT 的完整操作流程与设计思路。不包含具体代码实现细节,仅描述原理、配置、调试与维护方法,以保证内核程序的稳定性与安全性。


1. 简介

本驱动实现了一个基于 eBPF 的 TC(Traffic Control)层 hairpin NAT 功能。

它可以在一个网络接口(例如 TUN/TAP 设备、物理网卡)上,根据预定义的 NAT 规则,对 TCP 数据包的源/目的 IP 和端口进行转换,并可选地将包重定向到指定的网络接口(或原接口)。

典型应用场景:

  • 在 VNP 服务端实现端口映射或流量回注。
  • 在容器或虚拟化环境中实现 Service 的 Hairpin NAT(让访问服务的请求经过转换后回到自身)。

核心组件

  • 内核程序:负责数据包匹配、地址端口修改、校验和更新、重定向决策。
  • 用户态控制程序:用于加载/卸载 TC 程序,管理 NAT 规则(增删查)。
  • 编译脚本:简化构建过程。

2. 技术原理

程序工作于 Linux TC 的 egress 方向(发包时)。当数据包从指定网卡发出时,内核会先调用我们的 eBPF 程序。程序解析 IP 头与 TCP 头,提取五元组(源 IP、源端口、目的 IP、目的端口、协议),以该五元组为键查询预先定义的 NAT 映射表。若命中,则按照映射表中的新地址、新端口修改数据包,并重新计算 IP 头与 TCP 头的校验和,保证协议栈能够正确接收。修改完成后,数据包被重定向到指定网卡(默认原网卡)的 ingress 方向继续处理,从而实现流量的"折返"效果。

整个处理过程在内核态完成,避免了用户态与内核态之间的上下文切换,具有极高的处理效率。


3. 环境准备

3.1 系统与内核

  • 推荐 Ubuntu 20.04/22.04 或 WSL2(内核 5.10+)。
  • 确认内核支持 BPF(通常默认开启)。

3.2 安装基础工具链

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

3.3 安装 eBPF 开发库

bash 复制代码
sudo apt install -y libbpf-dev libelf-dev libz-dev

3.4 安装 bpftool(管理 BPF 程序与 map)

bash 复制代码
sudo apt install -y linux-tools-common linux-tools-generic
# 查找 bpftool 实际路径(例如 /usr/lib/linux-tools-5.15.0-173-generic/bpftool)
find /usr -name "bpftool" 2>/dev/null
# 建立软链接
sudo ln -sf /usr/lib/linux-tools-$(uname -r)/bpftool /usr/sbin/bpftool

3.5 挂载 BPF 文件系统(用于持久化 map)

bash 复制代码
sudo mkdir -p /sys/fs/bpf
sudo mount -t bpf none /sys/fs/bpf

4. 编译

4.1 编译内核程序

bash 复制代码
clang -O2 -target bpf -c driver.c -o driver.ko

4.2 编译用户态控制程序

bash 复制代码
gcc loader.c -o loader -lbpf -lelf -lz

说明

  • -O2 是 BPF 程序推荐的优化级别。
  • driver.ko 虽然扩展名是 .ko,但实际是 eBPF 字节码文件,并非内核模块。
  • 编译用户态程序时需链接 libbpflibelflibz

4.3 关于内核常量的手动定义

在编写 BPF 程序时,可能会遇到某些常量未定义的情况(例如 TC_ACT_OK)。这些常量定义在 Linux 内核头文件中。若编译环境缺少这些定义,可在代码开头手动添加(已在源码中处理,无需用户关心)。


5. 网络环境准备

在加载 NAT 程序之前,需要先准备好网络环境。以下以创建一个 TUN 设备 tun0(IP 10.0.0.2)并配置路由为例。

5.1 创建 TUN 设备(可选)

如果你使用 VNP 或虚拟网络设备,这一步可能需要调整。示例使用 socat 创建一个 TUN 设备:

bash 复制代码
socat TUN:10.0.0.2/24,up,tun-name=tun0,iff-up,iff-running,iff-noarp,iff-pointopoint,up -

参数解释:

  • TUN:10.0.0.2/24:TUN 设备 IP 为 10.0.0.2,掩码 24。
  • up:设备立即启用。
  • tun-name=tun0:设备名称。
  • iff-up, iff-running 等:设置标志位。

创建后,可用 ip addr show tun0 查看。

5.2 添加路由(使目标 IP 的流量进入 tun0)

假设我们要让访问 10.0.0.8 的流量走 tun0

bash 复制代码
ip route add 10.0.0.8/32 dev tun0

注意:根据实际场景调整路由,确保 NAT 转换前后的 IP 能够正确路由。

5.3 启动后端服务

NAT 转换后的最终目的地址需要有服务监听。例如,我们将在 10.0.0.2:8080 启动一个 HTTP 服务:

bash 复制代码
python3 -m http.server 8080 --bind 10.0.0.2

确保该服务正在运行,否则测试时连接会失败。


6. 加载 BPF 程序

有两种方式:推荐使用我们自带的 loader 工具,也可以手动使用 tc 命令。

6.1 方式一:使用 loader 工具(推荐)

bash 复制代码
# 挂载 BPF 文件系统(若未挂载,只需执行一次)
sudo mount -t bpf none /sys/fs/bpf

# 加载程序到指定网卡(例如 tun0)
sudo ./loader attach tun0

attach 命令会完成以下工作:

  • 打开并加载 driver.ko 字节码。
  • 将规则表持久化到 /sys/fs/bpf/tun_nat_map
  • 在指定网卡的 egress 方向附加 TC 程序(handle 1, priority 1)。

成功输出类似:

复制代码
规则表已 pin 到 /sys/fs/bpf/tun_nat_map
TC egress NAT 已成功附加到 tun0
现在可以使用 add / del / dump 管理规则

6.2 方式二:手动使用 tc 命令

如果你更熟悉 tc,也可以手动附加(前提是已编译出 driver.ko):

bash 复制代码
# 确保网卡已有 clsact qdisc(若没有则创建)
tc qdisc add dev tun0 clsact

# 加载程序,指定节名(驱动程序中已定义)
tc filter add dev tun0 egress bpf da obj driver.ko sec <节名>

# 查看挂载状态
tc filter show dev tun0 egress

此时规则表没有自动 pin,如需管理规则,可以通过 bpftool 查找 map id 后操作(较繁琐,不推荐)。


7. NAT 规则管理

规则通过 loader 程序进行增、删、查。规则存储在 BPF 规则表中,key 是原始五元组(源IP、目的IP、源端口、目的端口、协议),value 是转换后的地址、端口以及重定向接口。

7.1 添加规则

bash 复制代码
sudo ./loader add <src_ip> <src_port> <dst_ip> <dst_port> <new_src_ip> <new_src_port> <new_dst_ip> <new_dst_port> <redirect_ifindex>

参数说明:

  • <src_ip>:原始源 IP(点分十进制)。
  • <src_port>:原始源端口(整数)。
  • <dst_ip>:原始目的 IP。
  • <dst_port>:原始目的端口。
  • <new_src_ip>:转换后的源 IP。
  • <new_src_port>:转换后的源端口。
  • <new_dst_ip>:转换后的目的 IP。
  • <new_dst_port>:转换后的目的端口。
  • <redirect_ifindex>:重定向的目标网卡索引(0 表示使用原接口;可以是数字 ifindex 或网卡名,例如 eth0lo)。

重要:在 hairpin 场景中,通常需要添加两条规则(双向):

  • 去程:将客户端访问服务的请求转换为服务所在后端地址。
  • 回程:将后端响应转换为客户端可识别的地址。

示例

假设:

  • 客户端 IP 10.0.0.2,使用源端口 1111 访问 10.0.0.8:7777
  • 我们想将请求转换为从 10.0.0.1:2222 发往 10.0.0.2:8080(后端服务)。
  • 回程时,将后端响应(10.0.0.2:8080 -> 10.0.0.1:2222)转换为 10.0.0.8:7777 -> 10.0.0.2:1111

添加规则:

bash 复制代码
# 去程
sudo ./loader add 10.0.0.2 1111 10.0.0.8 7777 10.0.0.1 2222 10.0.0.2 8080 0

# 回程
sudo ./loader add 10.0.0.2 8080 10.0.0.1 2222 10.0.0.8 7777 10.0.0.2 1111 0

注意

  • 两条规则严格对称,确保转换可逆。
  • redirect_ifindex 设为 0,表示数据包修改后从原接口发出(即还是通过 tun0)。

7.2 查看所有规则

bash 复制代码
sudo ./loader dump

输出示例:

复制代码
当前 NAT 规则:
----------------------------------------------------------------------------------------
源IP:端口          → 目的IP:端口           | 新源IP:新端口        新目的IP:新端口        重定向 ifindex
----------------------------------------------------------------------------------------
10.0.0.2:8080  → 10.0.0.1:2222  | 10.0.0.8:7777  10.0.0.2:1111  0
10.0.0.2:1111  → 10.0.0.8:7777  | 10.0.0.1:2222  10.0.0.2:8080  0

7.3 删除规则

bash 复制代码
sudo ./loader del <src_ip> <src_port> <dst_ip> <dst_port>

例如:

bash 复制代码
sudo ./loader del 10.0.0.2 1111 10.0.0.8 7777
sudo ./loader del 10.0.0.2 8080 10.0.0.1 2222

8. 测试

配置完成后,使用 curl 测试(确保后端服务已启动):

bash 复制代码
curl --local-port 1111 http://10.0.0.8:7777/

--local-port 1111 强制使用源端口 1111,使其匹配去程规则。

如果一切正常,应能收到后端 HTTP 服务的响应。


9. 调试

9.1 查看内核日志(bpf_printk)

驱动中使用调试输出,可以通过 trace_pipe 实时查看:

bash 复制代码
# 确保 debugfs 已挂载(通常系统自动挂载)
sudo mount -t debugfs none /sys/kernel/debug   # 如果未挂载

# 实时输出 BPF 日志
cat /sys/kernel/debug/tracing/trace_pipe | grep NAT

当有数据包匹配时,会看到类似:

复制代码
NAT hit, redirect to 0

9.2 查看 BPF 程序状态

使用 bpftool

bash 复制代码
# 列出所有 BPF 程序,查找 tc_egress
bpftool prog list | grep tc_egress

# 假设程序 id 为 123,查看详细信息
bpftool prog show id 123 --pretty

9.3 查看规则表内容

bash 复制代码
# 列出所有 map,找到规则表对应的 id
bpftool map list

# 导出规则表内容(假设 id 为 42)
bpftool map dump id 42

输出会显示所有规则,便于验证。

9.4 抓包验证

用 tcpdump 抓取双向流量,观察 NAT 转换是否生效:

bash 复制代码
tcpdump -i any -n host 10.0.0.2 or host 10.0.0.8 or host 10.0.0.1

执行 curl 后,应能看到:

  • 原始包:10.0.0.2.1111 > 10.0.0.8.7777
  • 转换后包:10.0.0.1.2222 > 10.0.0.2.8080
  • 回程包:10.0.0.2.8080 > 10.0.0.1.2222
  • 再次转换:10.0.0.8.7777 > 10.0.0.2.1111

10. 清理

10.1 卸载 TC 程序

bash 复制代码
# 使用 loader 卸载
sudo ./loader detach tun0

# 或手动删除 tc 过滤器
tc filter del dev tun0 egress

10.2 删除规则表持久化文件

bash 复制代码
rm -f /sys/fs/bpf/tun_nat_map

10.3 删除 TUN 设备和路由(如果不再需要)

bash 复制代码
ip link del tun0
ip route del 10.0.0.8/32

11. 个人笔记

11.1 为何选择 TC 而非 XDP

XDP 需要网卡驱动直接支持,且工作在驱动层之前。对于 TUN/TAP 这类纯软件虚拟设备,数据包在协议栈内部生成,不经过驱动层,因此 XDP 程序虽然可以附加,但实际上不会处理任何包。而 TC 工作在协议栈的出口点(egress),对包括虚拟设备在内的所有网络设备均有效,是实现 TUN 设备 NAT 的正确选择。

11.2 规则表容量

规则表采用哈希表结构,最大条目数设为 131072(2^17) ,即约 13 万条。这一容量远高于绝大多数实际场景的需求,确保即使在大量端口映射或动态连接时也不会出现表满溢出的风险。同时哈希表采用非预分配模式,仅在实际插入条目时才分配内存,避免因预分配大量内存造成内核资源浪费。

11.3 简单至上原则

驱动程序的逻辑被刻意保持简单:

  • 只处理 TCP 协议,避免支持 UDP 等复杂协议带来的校验和更新等问题。
  • 无状态设计,不维护连接跟踪(conntrack),完全依赖规则表匹配。
  • 校验和更新采用增量方式,避免全量重新计算的性能开销。
  • 错误处理极简 ,遇到无法处理的情况直接放行(TC_ACT_OK),宁可丢包也不让内核崩溃。

这种"简单即稳定"的指导思想,确保了内核程序在高压场景下的可靠性与可维护性。任何复杂的特性(如 UDP 支持、动态超时等)都应放在用户态或单独模块中实现,而非塞入这个核心数据路径。


12. 常见问题 (FAQ)

Q1: 加载时提示 libbpf: failed to load program

  • 原因:可能是 BPF 程序验证失败。
  • 解决:查看 dmesg/sys/kernel/debug/tracing/trace_pipe 获取具体错误信息。常见原因包括校验和更新错误、指针越界等。

Q2: curl 无响应,但 tcpdump 显示转换后的包

  • 检查后端服务是否监听在转换后的目的地址端口。
  • 检查回程规则是否正确添加。
  • 检查重定向接口是否配置正确(例如设为 0 时原接口是否能将包送到正确路径)。

Q3: 规则添加成功,但 dump 看不到

  • 可能规则表没有正确 pin,或使用了错误的 map id。确保使用 ./loader attach 挂载程序,它会自动 pin 规则表。
  • 尝试重新 attach 并添加规则。

Q4: 修改了驱动代码后重新 attach 不生效

  • 必须重新编译内核程序,然后先 detach 旧程序,再 attach 新程序。
  • 如果使用 tc 命令,需要先删除旧的过滤器。

Q5: 为什么必须添加双向规则?

  • 驱动基于五元组精确匹配。若不添加回程规则,后端响应的数据包无法匹配任何条目,将原样发出,导致客户端无法识别连接,表现为无响应或重置。双向规则确保往返流量均被转换。

13. 附录:IP 和端口十六进制转换(用于 bpftool 手动操作)

如果你需要绕过 loader 直接用 bpftool 操作规则表,可以使用以下转换:

bash 复制代码
# IP 转十六进制(大端)
printf "0x%02x%02x%02x%02x\n" 192 168 1 100   # 输出 0xc0a80164

# 端口转十六进制(大端)
printf "0x%04x\n" 12345   # 输出 0x3039

然后使用 bpftool map update id <map_id> key hex ... value hex ... 插入规则。

相关推荐
是店小二呀2 小时前
Docker部署EasyNode+内网穿透:轻松实现服务器远程管理
服务器·docker·容器
hweiyu002 小时前
Linux命令:screen
linux·运维·服务器
njidf2 小时前
C++与量子计算模拟
开发语言·c++·算法
Alonse_沃虎电子2 小时前
沃虎工业级RJ45抗震动方案:破解严苛环境下的网络连接难题
网络·产品·电子元器件·电子元器件供应商·网络变压器
Bin努力加餐饭2 小时前
C++(3)TCP
网络·网络协议·tcp/ip
爱学习的程序媛2 小时前
【Web前端】深入解析JavaScript异步编程
开发语言·前端·javascript·ecmascript·web
IAUTOMOBILE2 小时前
两大王者-Laravel vs ThinkPHP:PHP 框架终极对决,谁更适合团队或者个人!
开发语言·php·laravel
Elastic 中国社区官方博客2 小时前
使用 TypeScript 创建 Elasticsearch MCP 服务器
大数据·服务器·数据库·人工智能·elasticsearch·搜索引擎·全文检索
Bert.Cai2 小时前
Python逻辑运算符详解
开发语言·python