简体中文 | English
在 Linux 内核中按规则选择流量,再以对称 IP 地址对 hash 分配到独立虚拟 TAP 接口。 用户态程序通过 TAP FD 读写完整以太网帧,虚拟接口支持 tcpdump 和 ingress 处理。
项目原名 ditchvtap。当前实现是实验性的 out-of-tree 内核模块,已在 Linux 6.1 / 6.8、x86_64 上完成功能验证。
steervtap 使用 macvtap 同类的内核共享
CONFIG_TAP层。它不是/dev/net/tun创建的标准 TAP:read(FD)接收下层网卡分流的报文,write(FD)从下层网卡发送。命中的原包不再交给宿主机 TCP/IP。
| 能力 | 当前实现 |
|---|---|
| 绑定下层网卡 | 一个 Ethernet 下层设备对应一个共享组,最多 64 个 TAP 成员 |
| 流量选择 | 最多 64 条有序规则,支持 pass/hash,默认放行 |
| 匹配字段 | IPv4/IPv6 CIDR、IP 协议、源/目标端口范围、最外层 VLAN、双向匹配 |
| 分流 | 对称 IP 地址对软件 hash,256 桶 RSS 间接表 |
| 配置更新 | rtnetlink 下发;规则与整个 RSS 表通过 RCU 一起原子替换 |
| 用户态接收 | 每个接口提供独立 TAP 字符设备,读取完整以太网帧 |
| 用户态发送 | 写入同一个 TAP FD,经绑定的下层网卡发出 |
| 网卡接收路径 | 支持 AF_PACKET 抓包和 ingress hooks,已验证 tc ingress 丢包 |
| 统计 | ip -s link、ethtool -S,按接口统计收发和驱动丢包 |
适合自定义 UDP 接入、用户态协议栈、报文处理与网络实验。示例 tap-worker 只负责读包、计数;认证、业务协议、路由、代理和连接状态由上层程序实现。
当前不提供连接状态识别、五元组 hash、无损迁移、被动镜像或完整交换机能力。Suricata 等程序可以沿 AF_PACKET 路径接入,但仓库尚未完成 Suricata、gVisor、tun2socks 集成验证。
用户态配置工具
steervtapctl.py replace/show/clear
│
NETLINK_ROUTE
│
eth0 对应的共享配置组
┌──────────────────────┐
│ 有序规则 + RSS 256 桶 │
│ 一个 RCU 快照发布 │
└───────────┬──────────┘
│
外部网络 → eth0 原有驱动 → Linux 接收流程
│
steer_receive()
│
规则匹配
┌─────────────┴─────────────┐
│ │
pass / 未命中 命中 hash
│ │
宿主机正常接收路径 对称 IP 对 hash
(SSH、ARP、socket 等) │
RSS 桶查表
┌───────────────┴──────────────┐
▼ ▼
steervtap0 steervtap1
│ │
标准网卡接收流程 标准网卡接收流程
│ │
AF_PACKET / tcpdump AF_PACKET / tcpdump
│ │
ingress hooks ingress hooks
│ │
共享 TAP handler 共享 TAP handler
│ │
TAP 队列 TAP 队列
│ read(FD) │ read(FD)
▼ ▼
worker 0 worker 1
首个成员创建时,驱动在下层网卡注册 RX handler。选中的单播修改 skb->dev,返回 RX_HANDLER_ANOTHER,由网络核心按目标虚拟网卡重新走接收流程。共享 TAP handler 在抓包和 ingress 之后接管原包,返回 RX_HANDLER_CONSUMED,不再继续上交宿主机 ARP/IP/TCP/UDP。
命中的广播/组播是另一条路径:复制到所有 UP 成员,并通过 netif_rx() 进入各成员的接收流程;即使成员不在 RSS 表里,也会收到副本。命中的 PAUSE 帧直接消费,不做 fanout。
worker 0:write(tap0_fd, Ethernet_frame) ──┐
├─→ steervtap ndo_start_xmit
worker 1:write(tap1_fd, Ethernet_frame) ──┘ │
steer_xmit()
│
skb->dev = eth0
│
dev_queue_xmit()
│
eth0 发送路径 / 驱动
│
外部网络
TX 不查入站规则或 RSS 表。驱动不做地址转换、路由、MAC 改写或 TCP 处理,调用者必须提供正确的完整以太网帧。成功 write 或 TX 计数增加表示发送路径接受了报文,不保证对端已经收到。
| 操作 | 标准 /dev/net/tun TAP |
steervtap |
|---|---|---|
用户程序 read(FD) |
读取内核向 TAP 网卡发送的帧 | 读取下层网卡分流来的帧 |
用户程序 write(FD) |
把帧注入 TAP RX,进入宿主机接收流程 | 把帧交给下层网卡 TX |
| 是否直接绑定 eth0 | 不会自动绑定 | 创建时指定 link eth0 |
| 配置 IP | 可用于宿主机协议栈通信 | 可以配置,但命中帧仍由 TAP handler 消费,不能据此认为普通 socket 可接收 |
steervtap 的 TAP 字符设备队列与 AF_PACKET 抓包入口是两套接口。AF_PACKET 可观察副本,但不会排空 TAP 队列,也不会自动转发原包。
- Linux,匹配当前目标内核的 headers/build tree,C 编译器、make、kmod、iproute2、Python 3。
- 内核启用
CONFIG_TAP;运行隔离测试还需要 veth、network namespace、IPv4/IPv6 和 tc ingress 相关支持。 - 模块加载、设备创建和配置通常需要 root;worker 需要访问对应字符设备。
.ko与目标内核版本、配置和 CPU 架构相关。升级内核或更换架构通常需要重编译;当前没有 DKMS 自动构建。- macOS/Windows 不能直接运行此模块,需要 Linux 虚拟机。Python 控制工具也依赖 Linux netlink。
Ubuntu/Debian 示例:
sudo apt-get install build-essential linux-headers-$(uname -r) kmod iproute2 python3 ethtool tcpdump
git clone https://github.com/singchia/steervtap.git
cd steervtap
make
make tools指定其他构建目录:
make KDIR=/path/to/target/kernel/build仅更改 KDIR 不会自动完成跨 CPU 架构编译,仍需正确的内核构建参数和工具链。不要把在 Docker 内编译的模块直接加载到不同版本的宿主机内核。
下面使用 eth1,创建两个 TAP,捕获来自 192.0.2.0/24、目标 UDP 9000 的报文。请将网卡和源网段替换成实际值。
共享业务网卡上应精确匹配目标流量。全量接管可能包含 SSH;仅做观察也不能把 hash 当成镜像。
sudo modprobe tap
sudo insmod steervtap.ko
sudo ip link add link eth1 name steervtap0 type steervtap
sudo ip link add link eth1 name steervtap1 type steervtap
sudo ip link set eth1 up
sudo ip link set steervtap0 up
sudo ip link set steervtap1 up新组的规则为空,报文默认放行。创建命令不支持 src、dport 等自定义 iproute2 参数,也不支持在创建时直接下发组配置。
分别在两个终端运行:
sudo ./tools/tap-worker steervtap0sudo ./tools/tap-worker steervtap1程序启动时显示设备路径,随后持续读取;按 Ctrl+C 退出时显示 packets、bytes。它不会逐包打印,也不会默认回包。
仓库提供 examples/udp-service.json:
{
"rules": [
{"action": "hash", "src": "192.0.2.0/24", "protocol": "udp", "dst_port": 9000}
],
"rss": ["steervtap0", "steervtap1"]
}修改源网段后执行:
sudo python3 tools/steervtapctl.py replace steervtap0 examples/udp-service.json
python3 tools/steervtapctl.py show steervtap1配置属于整个下层网卡组,因此可以通过任意成员更新或读取。RSS 表按名字解析到本组成员,两个 TAP 交替占据 256 个桶。
从规则允许的外部源发送 UDP 到服务器可达地址的 9000 端口。抓包应在发流量前启动:
sudo tcpdump -ni steervtap0 'udp dst port 9000'
# 另一个终端可抓 steervtap1ip -s link show steervtap0
ip -s link show steervtap1
sudo ethtool -S steervtap0同一源/目标 IP 对的所有端口共享一个 hash,少量同源同目标测试包通常只进入一个 TAP,这是预期行为。
先清规则,再停止 worker,避免命中后没有 reader:
sudo python3 tools/steervtapctl.py clear steervtap0
# 在两个 worker 终端按 Ctrl+C
sudo ip link del steervtap0
sudo ip link del steervtap1
sudo rmmod steervtap这些配置仅针对当前运行时,仓库没有默认安装开机加载服务。
可组合多个字段,并用靠前的 pass 规则排除流量:
{
"rules": [
{"action": "pass", "protocol": "tcp", "dst_port": 22},
{"action": "hash", "src": "192.0.2.0/24", "protocol": "udp",
"dst_port": "9000-9099", "bidirectional": true}
],
"rss": ["steervtap0", "steervtap1", "steervtap1"]
}- 第一条匹配规则决定结果,未匹配默认 pass;省略字段表示该字段不限制。
- 支持
family、src、dst、protocol、src_port、dst_port、vlan、bidirectional。 - 端口匹配要求显式指定 TCP 或 UDP。双向匹配同时交换源/目标地址和端口。
- RSS 输入为 1–256 个本组接口名字或 ifindex,工具循环填充 256 桶;重复成员可表达近似权重。
- 所有 hash 规则共享一张表,不能给每条规则指定不同 TAP 组。
- 源/目标地址排序后参与软件
jhash,VLAN 信息参与 hash;非 IP 使用对称 MAC 地址对。 - 随机种子在组存在期间固定;硬件 RSS hash 不参与。重建组、主动更换表都可能改变归属。
- 配置先整体校验再通过 RCU 发布,失败保留旧配置;每个包的规则判断和选桶使用同一个快照。
- 更新 ACK 不是队列排空屏障,在途包仍可能按旧配置完成处理。
完整字段、解析边界、4360 字节版本化 netlink 二进制布局见 配置协议。
最小 C 示例是 tools/tap-worker.c。接入步骤:
- 从
/sys/class/net/INTERFACE/steervtap*/dev读取字符设备 major/minor。 - 打开
/dev/steervtap<minor>,使用非阻塞方式并以 poll 等待数据。 - 用
TUNSETIFF协商IFF_TAP | IFF_NO_PI,禁用默认 virtio 网络头,使用原始以太网帧。 read获取帧,业务程序处理后可write完整帧回下层网卡。- 正常关闭时停止轮询并关闭 FD,再删除网卡。
/dev/steervtap1 的后缀是 minor,不是 ifindex,也不一定对应 steervtap1。devtmpfs/udev 通常负责节点创建;root 测试程序会按 sysfs 信息创建临时节点。
默认每个接口使用一个 reader。额外打开可能触发共享 TAP 的多队列选择,不应把它当作广播订阅;当前验证范围是每接口一个队列。Linux 6.1 共享 TAP 在设备删除时不保证唤醒已阻塞的 read,因此示例使用非阻塞 FD 和可取消轮询。
tap-worker --echo 会将收到的帧原样写出,并不交换 MAC、IP 或端口,不是一个正确的 TCP/UDP echo 服务。只应在隔离链路测试中使用。
| 现象 | 含义 / 检查方向 |
|---|---|
| 接口 UP 但没流量 | 检查组配置是否为空、规则和下层到包情况、RSS 是否选中了另一个成员 |
| 所有包落一个 TAP | IP 对相同通常如此;不同端口不改变 hash |
| tcpdump 有包但 reader 没包 | 抓包在 ingress/TAP handler 前,检查 tc、reader、队列和丢包计数 |
| 宿主机 UDP socket 等不到包 | 命中的原包由 TAP 接管,不再交宿主机 socket |
rx_no_reader 增长 |
对应 TAP 没有附着 reader;AF_PACKET 程序不能替代 FD reader |
rx_target_down 增长 |
RSS 目标 DOWN,但还有其他组成员 UP,组仍在接管流量 |
rx_tap_dropped 增长 |
TAP 队列/转换或 multicast 接收 backlog 丢包,不全是队列满 |
rx_allocation_dropped 增长 |
skb 分配或克隆失败 |
| TX 计数为 0 | 只有写 TAP FD 或向虚拟接口发送才走其 TX;宿主机直接从 eth0 发包不计入 |
| 加载模块失败 | 检查目标内核版本、架构、CONFIG_TAP、模块签名要求和内核日志 |
RX packets/bytes 在报文转入虚拟接口时计数,先于 ingress 和用户态读取。rx_dropped 汇总驱动计数,tc 丢包应通过 tc -s filter show dev steervtap0 ingress 查看。TX 计数表示下层发送提交结果,不是远端确认。
- 新建组为空规则;添加成员不改变已发布的 RSS 表。
- 删除任何成员都清空整个组的配置,恢复默认放行,随后需显式重新配置。
- 目标 DOWN、没有 reader、队列满时丢包,不自动换目标或回退宿主机;全部成员 DOWN 时整个组放行。
- 任何成员 UP 时,下层保持一个混杂模式引用,包括空规则状态;最后一个成员 DOWN 时释放。
- MTU 不能超过下层设备;下层 MTU 减小到成员 MTU 以下会被拒绝。
- 不支持嵌套 steervtap、跨 namespace 创建或接口迁移;下层注销时联动删除成员。
- 所有分片,包括首片和 IPv6 atomic fragment,都不匹配端口规则;地址规则可以捕获分片。
- 最多解析四层 inline VLAN、八个受支持的 IPv6 扩展头;畸形/截断报文、超出解析范围和 IPv6 zero-payload/jumbogram 放行。该分类器不能当成完整防火墙校验器。
- 虚拟 MAC 不用于选择 RX 目标,也不改写用户发送的帧。ARP/NDP、回程路由和二层可达性需要业务方案明确处理。
- 没有主动声明 checksum/GSO 加速能力,没有硬件 RSS、零拷贝或吞吐量保证。
模块已加载时运行:
sudo make test测试使用临时 namespace 和 veth,验证规则、原子配置、IPv4/IPv6/VLAN、双向及分片归属、广播、AF_PACKET、tc ingress、所有 TAP 的 TX、worker、丢包、失败回滚和并发删除。
对当前内核执行构建及三轮加载/测试/卸载:
sudo make test-nativenative runner 拒绝替换已加载的 steervtap 模块;请先清理自己的试用接口和 worker。测试记录保存在 gitignored 的 test-results/。
已有验证:
| 环境 | 结果 |
|---|---|
| Linux 6.1.0-50-amd64,QEMU TCG | 三轮完整回归通过,包含 AF_PACKET 与 tc ingress |
| Linux 6.8.0-101-generic,Ubuntu x86_64 | 三轮原生完整回归通过,包含 AF_PACKET 与 tc ingress |
| 云服务器 eth0,virtio_net | 精确 DNS 源 IP/端口分流、TAP 外发及外部回包、虚拟接口 tcpdump 实测通过 |
这些是功能验证,不等于 KASAN/lockdep、真实物理网卡 offload 或吞吐量测试。QEMU 复现方法及记录见 测试说明。tests/live_eth0.py 是显式运行的 eth0 实测脚本,不由 make test 自动触发。
steervtap_main.c 网卡/字符设备生命周期、RX/TX、hash、统计、RCU 配置发布
steervtap_config.c 报文解析、规则匹配、netlink 数据校验及序列化
steervtap_config.h 配置结构、规则字段和 ABI 常量
Makefile 内核模块、C worker、测试入口
tools/steervtapctl.py Python 标准库实现的 rtnetlink 控制工具
tools/tap-worker.c 原始以太网 FD 消费/发送示例
examples/udp-service.json 示例规则与 RSS 配置
tests/ namespace、原生内核、QEMU 和显式 eth0 测试
docs/ 配置协议和测试说明
可继续扩展:五元组与分片关联、连接状态分流、多 RSS 组、镜像模式、流状态迁移、per-CPU 统计、DKMS、用户态协议栈集成。上述项目当前未实现。
内核 C 源码、C worker 和控制工具声明 GPL-2.0-or-later。仓库根目录保留了历史版本的 Apache-2.0 LICENSE,与当前源文件声明存在历史不一致;本次名称和文档更新没有重新授权已有代码。正式发布前需要统一许可文件及声明。