这个文章对TAP/TUN讲的比较清楚
https://blog.csdn.net/tjcwt2011/article/details/160653673
《深入高可用系统原理与设计》https://www.thebyte.com.cn/network/tuntap.html
一、在用户空间实现自定义网络协议栈
核心思想
内核协议栈是个黑盒------你想改 TCP 拥塞控制算法?想试一种新的传输协议?以前只能改内核源码、重新编译。TUN/TAP 彻底改变了这件事:
把协议栈从内核里"搬"到用户空间,用普通程序实现,而无需修改一行内核代码。
工作原理
你的用户态协议栈 ──write──→ /dev/net/tun (TAP设备)
│
内核协议栈收到一个"以太网帧"
│
当作从真实网卡收到的一样处理
│
路由查找 → 可能转发到物理网卡发出
反过来:
物理网卡收到包 → 内核协议栈 → 路由判定发往 TAP → 你的程序 read() 收到
你的程序既是发送方 也是接收方,完全控制每个字节怎么处理。
典型项目(都是真实可跑的)
| 项目 | 做了什么 | 用的模式 |
|---|---|---|
| microps | 轻量级 TCP/IP 协议栈,专为学习设计 | TAP |
| tapip | 用户态 TCP/IP 实现,GitHub 热门 | TAP |
| level-ip | 完整的用户态协议栈,支持 curl | TAP |
以 microps 为例,实际操作流程:
bash
# 1. 创建 TAP 设备
sudo ip tuntap add dev tap0 mode tap
# 2. 配置 IP(让宿主机能访问这个虚拟网络)
sudo ip addr add 10.0.0.1/24 dev tap0
sudo ip link set dev tap0 up
# 3. 运行你自己写的协议栈
sudo ./app/tcps -i tap0 # 启动一个 TCP 服务器
# 4. 从另一个终端连接测试
telnet 10.0.0.1 8080 # 真的能通!
# 5. Wireshark 抓 tap0,看到的就是你协议栈发出的帧
为什么这对学习/研究有巨大价值
| 传统方式(改内核) | TUN/TAP 方式 |
|---|---|
| 改一行代码 → 重新编译内核 → 重启 → test | 改一行代码 → 重新编译程序 → 重跑 → 立即看到结果 |
| 崩溃 = 内核 panic,机器挂了 | 崩溃 = 程序退出,宿主机 unaffected |
| 无法用 Wireshark 抓"中间过程" | Wireshark 直接抓 TAP 接口,每个包都能看 |
| 只能在自己机器上测 | 可以在任何 Linux 机器上跑,零硬件成本 |
一句话:TUN/TAP 给了你一个完全隔离、可观测、可随意炸掉重建的网络沙盒。
二、流量监控与过滤
核心思想
所有经过 TUN/TAP 的数据包,用户态程序都能原样读到。这意味着你可以:
- 看到每个包的完整内容(不像 iptables 只能看头部)
- 决定放行/丢弃/修改/重定向
- 实现内核做不到的灵活策略
两种部署模式
| 模式 | 原理 | 典型工具 |
|---|---|---|
| 软件 TAP(本文重点) | 创建虚拟接口,把特定流量导过去 | httptap、自研过滤器 |
| 硬件 Tap(物理设备) | 在交换机和设备之间串一个物理 Tap 盒 | 商业网络嗅探方案 |
实战案例:httptap ------ 监控任意程序的 HTTP/HTTPS 流量
这是一个真实项目,用法极其简单:
bash
# 监控 curl 发出的所有 HTTP/HTTPS 请求
httptap -- curl https://example.com
# 指定只看 80 和 443 端口
httptap --http 80,8080 --https 443,8443 -- firefox
# 导出为 HAR 文件,后续用 Chrome DevTools 分析
httptap --dump-har output.har -- your-app
它的内部实现就是:
1. 创建 TUN 设备(工作在 L3,拿到 IP 包)
2. 用户态程序自己实现一个 TCP/IP 栈(gvisor 或 handrolled)
3. 解析出 HTTP 请求/响应
4. 打印 / 存储 / 转发
自定义防火墙策略怎么做?
逻辑非常直观:
c
// 伪代码:一个基于 TUN 的用户态防火墙
while (1) {
packet = read(tun_fd, buf, sizeof(buf)); // 从 TUN 读包
if (packet.dst_ip == 1.2.3.4 && packet.dst_port == 443) {
drop(packet); // 丢包:屏蔽这个 IP
} else if (packet.payload_contains("malware")) {
modify_and_forward(packet); // 修改后转发
} else {
write(tun_fd, packet, len); // 原样放回内核栈
}
}
| 能力 | iptables/nftables | TUN/TAP 用户态 |
|---|---|---|
| 基于 IP/端口过滤 | ✅ | ✅ |
| 基于 payload 内容过滤 | ❌ 做不到 | ✅ 任意正则/字符串匹配 |
| 修改包内容(如重写 Host 头) | ✅ NAT 能做一部分 | ✅ 任意修改 |
| 看到完整包内容(包括 payload) | ❌ | ✅ |
| 性能 | 极高(内核态) | 较低(用户态拷贝) |
性能瓶颈在哪?怎么优化?
| 瓶颈 | 原因 | 优化方案 |
|---|---|---|
| 内核↔用户态拷贝 | 每个包都要 copy_to_user / copy_from_user | 用 TPACKET_V3 + mmap 零拷贝(httptap 就是这么做的,用了 1000 大小的缓冲通道) |
| 用户态协议栈慢 | 自己实现 TCP 比内核慢得多 | 用 gvisor 栈代替手写栈,兼容性和性能都更好 |
| 单线程处理 | 一个 read() 循环扛所有包 | 多线程 + 缓冲通道(httptap 用 chan []byte 做缓冲) |
一句话总结
| 场景 | TUN/TAP 解决了什么 |
|---|---|
| 自定义协议栈 | 不改内核,在用户空间造一个完整的 TCP/IP,可炸可重建可观测 |
| 流量监控过滤 | 内核做不到的深度包检测(DPI),在用户态随便写,代价是性能 |
如果你是想学习协议 ,选 TAP + microps/tapip,零成本开干。如果你是想监控特定应用的流量 ,httptap 这种工具开箱即用。如果你要生产环境的高性能过滤,TUN/TAP 不是最佳选择,但它是最灵活的起点。
microps 例子中,tap0和真实物理网卡没有做桥接,报文是如何从物理网卡发出去的?
靠内核路由表,不需要 bridge。
你的直觉是对的------tap0 和 eth0 之间确实没有二层连接。但它们能通,靠的是三层路由,不是二层桥接。
完整路径长这样
以 microps 发起一个到 8.8.8.8 的包为例:
microps 写包到 tap0
│
▼
内核收到 tap0 的以太网帧(目的 MAC 是网关的 MAC)
│
▼
内核查路由表:8.8.8.8 不在 10.0.0.0/24 → 走默认网关
│
▼
默认网关 = 192.168.1.1 → 下一跳从 eth0 出去
│
▼
eth0 发出,到达物理交换机 → 互联网
反过来也一样:
互联网回包到达 eth0
│
▼
内核查路由表:去 10.0.0.0/24 的路由 → 下一跳是 tap0
│
▼
包从 tap0 送到 microps 的 read()
为什么能通?三个关键
| 条件 | 说明 |
|---|---|
| ✅ tap0 有 IP | ip addr add 10.0.0.1/24 dev tap0,内核认为它是一个真实接口 |
| ✅ 有默认路由 | ip route add default via 192.168.1.1 dev eth0,告诉内核未知目的地走 eth0 |
| ✅ ip_forward 开启 | sysctl net.ipv4.ip_forward=1,允许内核在接口间转发 |
这就够了。内核根本不关心包是从哪个接口进来的,它只查路由表。
和 bridge 方案的本质区别
| microps 方案(tun/tap + 路由) | 虚拟机方案(tap + bridge) | |
|---|---|---|
| 连接方式 | 三层路由 | 二层桥接 |
| tap0 有 IP 吗 | ✅ 有(10.0.0.1) | ❌ 无 |
| eth0 有 IP 吗 | ✅ 有(192.168.1.50) | ❌ 无(IP 移到 br0 上了) |
| 包怎么出去 | 内核路由 → eth0 | bridge 转发 → eth0 |
| 虚拟机能发 ARP 吗 | ❌ 不能(没有二层域) | ✅ 能(bridge 模拟真实交换机) |
一句话
tap0 不需要和 eth0 桥接,因为内核路由表会自动把包从 tap0 导向 eth0。bridge 解决的是二层问题(ARP/广播),microps 这种场景根本不需要二层,三层路由就够了。
这其实就是我最早说的:tun 只能工作在 L3,但 L3 靠 ip_forward 完全可以和物理网卡连通。
microps 方案中,明明创建的是tap,而不是tun,为什么说是三层路由呢
你这个问题问到点子上了。确实不矛盾,但需要把"谁在做二层、谁在做三层"拆清楚。
矛盾在哪?
| 你的理解 | 我之前说的 |
|---|---|
| TAP = 二层设备,收发以太网帧 | 这个方案靠三层路由通的 |
| → 既然是二层,应该靠 bridge 才对 | → 桥接才是二层的事啊 |
看起来矛盾,但其实两句话说的是不同层的事。
拆开看:谁负责哪一层
以 microps 发一个包到 8.8.8.8 为例:
microps 协议栈(用户态)
│
│ 1. 自己实现 ARP,查到网关 MAC = aa:bb:cc:dd:ee:ff
│ 2. 自己构造完整以太网帧:
│ ┌─────────────────────────────────────┐
│ │ eth_hdr: dst=aa:bb:cc:dd:ee:ff │ ← microps 自己填的 MAC 头
│ │ ip_hdr: dst=8.8.8.8 │ ← microps 自己实现的 IP 层
│ │ tcp_hdr: dst=443 │ ← microps 自己实现的 TCP 层
│ └─────────────────────────────────────┘
│ 3. write() 写到 /dev/net/tap → tap0
│
▼
内核协议栈收到 tap0 的以太网帧
│
│ 4. 一看目的 MAC = aa:bb:cc:dd:ee:ff(网关)→ 是给我的
│ 5. 剥掉以太网头,拿到 IP 包,查路由表:
│ 8.8.8.8 不在本地 → 走默认网关,从 eth0 出去
│ 6. 重新封装以太网帧(dst MAC = 真实网关 MAC),eth0 发出
│
▼
物理网卡 → 互联网
| 层级 | 谁在做 | 用什么设备 |
|---|---|---|
| 二层(MAC/ARP) | microps 自己(用户态协议栈) | TAP 只是个管道,透传以太网帧 |
| 三层(IP 路由) | 内核路由表 | 不管包从 tap0 还是 eth0 进来,内核都查同一张路由表 |
所以为什么说"三层路由"?
因为包从 tap0 出来之后,内核的处理方式和从 eth0 收到包完全一样:
剥二层头 → 查三层路由表 → 从另一个接口发出
这就是纯三层路由,和 bridge 没有任何关系。
那为什么 microps 用 TAP 而不用 TUN?
这才是关键。如果用 TUN:
| TAP | TUN | |
|---|---|---|
| 收发的数据 | 完整以太网帧(含 MAC 头) | 纯 IP 包(无 MAC 头) |
| microps 能实现 ARP 吗? | ✅ 能,自己构造 MAC 头 | ❌ 不能,没有 MAC 头可以填 |
| microps 能实现完整 TCP/IP 吗? | ✅ 能,从二层到四层全自己来 | ⚠️ 缺二层,不完整 |
microps 的目的是实现一个完整的 TCP/IP 协议栈用于学习,ARP 是 TCP/IP 的一部分。用 TUN 的话,ARP 没地方放,协议栈就是残的。
所以选 TAP 不是为了桥接,而是因为:
TAP 给了 microps 完整的以太网帧,让它能自己实现 ARP、自己填 MAC 头,从而拥有一个真正完整的协议栈。至于包出去之后怎么路由,那是内核的事,和 TAP 是几层设备无关。
一句话
TAP 是二层设备没错,但 microps 用 TAP 不是为了做二层桥接,而是为了拿到完整以太网帧来实现自己的 ARP。包出了 TAP 之后,内核该查路由查路由,该走三层走三层------设备是二层的,转发路径是三层的,不矛盾。