tunio 是一个使用现代 c++(c++20)与 Boost.Asio 协程支持实现的用户态 TUN 虚拟网络引擎。它将 Linux TUN、macOS utun 与 Windows TAP/Wintun 设备产生的 L3 原始 IP 包处理全面封装于内部,向上层暴露一套与 Boost.Asio 网络编程范式完全对齐的异步接口,可以像使用 asio::ip::tcp::socket 一样直接 co_await,适用于 tun2socks、透明代理与轻量级 VPN 网关等场景。
为什么需要 tunio
使用过 TUN 设备的开发者都清楚,TUN 与普通 socket 是两种完全不同的东西:打开 TUN 设备之后,你拿到的是一堆裸的 L3 IP 包,内核不再为你提供任何传输层服务。TCP 的乱序重排、确认与重传、窗口管理、握手与挥手、NAT 流表…… 全部需要自己处理。这是用户态网络栈的典型痛点:
- 引入
lwIP这类完整协议栈,集成成本高,接口风格与业务代码割裂,反而喧宾夺主。 - 自研”半路转发”方案,只处理 UDP 或者不对 TCP 的可靠性负责,丢包链路上用户体验很差。
tunio 想解决的问题就是:在用户态实现一个足够轻量但又具备基本可靠性的 TCP/UDP 协议引擎,同时把上层 API 设计成开发者最熟悉的 Boost.Asio 风格。这样业务代码关注点只在”如何转发”,而不在”如何实现 TCP”。
主要特性
- 双模设备管理:引擎自主创建并配置 TUN 设备,或接管外部应用已打开的平台原生句柄(文件描述符 /
HANDLE),方便提权前置、多实例共享设备。 - 完全 Asio 风格异步接口:所有公开 API 采用
CompletionToken与async_initiate实现,与use_awaitable、use_future及自定义CompletionToken无缝协作。 - TCP/UDP 对称抽象:TCP 提供
tun_tcp_socket/tun_tcp_acceptor,UDP 提供tun_udp_socket/tun_udp_acceptor,命名与行为对齐Boost.Asio,学习成本极低。 - 轻量协议引擎:接收方向缓存乱序段、按序交付,避免人为乱序触发快速重传;发送方向仅以 RTO 计时器重读用户写缓冲实现重传,无重传队列与拷贝。
- IPv4/IPv6 双栈:双栈报文解析与构造,内置 ICMP/ICMPv6 回显响应,丢弃分片与扩展头报文。
- 生产级健壮性:流数、队列字节、总缓冲等资源上限,空闲超时与半开连接清理,环路与本地地址防护,均可通过
tun_config或编译宏调整。 - 统计接口:
engine_stats原子计数,实时暴露收发包、丢弃、活动连接与会话等指标。
架构与设计
系统采用四层解耦架构:
应用层 (Proxy Logic / SOCKS5 Client)
| co_await / CompletionToken
异步 API 层
tun_tcp_socket / tun_tcp_acceptor tun_udp_socket / tun_udp_acceptor
协议引擎层
TCP Flow Engine UDP Flow Engine
Flow Dispatcher & NAT 表 (串行执行器)
设备抽象层
tun_device (Linux TUN / macOS utun / Windows TAP / Wintun)
└─ async_read_packet / async_write_packet (原始字节包)
└─ async_read_ip / async_write_ip (ip_packet 解析级接口)
所有内部状态变更强制串行执行,这也是无锁设计的前提:单线程模式(默认)直接运行于 io_context 执行器上,省去每包 Strand 派发开销;多线程模式(构造参数 tunio(io, false))运行于单个 Strand 之上,多线程 io_context 下无锁竞争。
TCP 引擎:轻量转发策略
tunio 的 TCP 引擎不是完整协议栈,而是围绕”转发”场景做减法的轻量实现,几个值得说的设计点:
- 握手由应用决定:收到客户端 SYN 后引擎不立即回复 SYN-ACK,而是触发
tun_tcp_acceptor::async_accept。由应用调用accept()(回复 SYN-ACK)或reject()(回复 RST)决定握手结果,首次读写视为隐式批准。这样后端不可达的连接可以在握手阶段就被直接 RST 掉,不浪费任何往返。 - 接收方向:乱序段先进入缓存,缺失段补齐后按序交付并批量确认,避免人为乱序触发对端快速重传与拥塞窗口减半。通告动态接收窗口(
min(固定上限, 剩余接收缓冲),对齐 gVisorselectWindow),剩余缓冲耗尽时通告 0 施加背压,防止对端超发导致队列积压丢包。 - 发送方向:单写模型,同一时刻每条连接至多一个未完成的写操作。写操作以”数据被对端 ACK 确认”作为完成条件(而非写入设备即完成),用户写缓冲在完成回调前保持有效——所以 RTO 重传时直接重读用户缓冲,既无重传队列也无需拷贝。辅以指数退避的 RTO(初始 200ms,上限 60s)与零窗口持久探测,保证丢包链路上的写操作不会永久悬挂。
- FIN 推迟:发送侧关闭时尚有未确认数据时,FIN 推迟到数据全部确认后再发送,避免 FIN 与在途数据段序号重叠导致对端丢弃。
UDP 引擎:会话化数据报
UDP 是无连接的,引擎将其映射为有状态的会话:以客户端三元组(源 IP、源端口、协议)唯一标识一个会话,每个会话与远端是 1 对 N 关系,一次收发对应一个完整数据报。会话老化采用最小堆管理,定时器只等待堆顶超时,避免轮询扫描全表。tun_udp_acceptor::async_accept 在收到新会话数据时交付一个 tun_udp_socket。
设备抽象层可以脱离引擎单独使用
tun_device 与 ip_packet 是独立于协议引擎的:打开真实 TUN 设备(或注入外部句柄)后即可直接读写 IP 报文。除原始字节包接口外,还提供解析级接口 async_read_ip / async_write_ip——一次读取得到一个完整解析的 IP 报文,包含 IP 头、TCP/UDP/ICMP 传输层视图与载荷,且无需拷贝(以下片段假设 dev 为已打开的 tun_device,net 为 boost::asio 命名空间别名):
#include "tunio/tun_device.hpp"
#include "tunio/ip_packet.hpp"
tunio::tun_device dev(io);
boost::system::error_code ec;
if (!dev.open(cfg, ec)) { /* ... */ }
tunio::ip_packet pkt;
size_t n = co_await dev.async_read_ip(pkt, net::use_awaitable);
if (pkt.valid()) {
const net::ip::address src = pkt.source_address();
const net::ip::address dst = pkt.destination_address();
if (pkt.is_tcp()) {
uint16_t sport = pkt.source_port();
const auto *tcp = pkt.tcp(); // 原始 TCP 头视图
const uint8_t *data = pkt.transport_data();
}
}
// 写方向:从字段构造报文,自动计算长度与 IP/TCP/UDP/ICMP 校验和
tunio::ip_packet out;
out.begin_ipv4(src_v4, dst_v4);
out.begin_udp(12345, 53);
out.append_payload(data, len);
out.finalize();
co_await dev.async_write_ip(out, net::use_awaitable);
快速上手
下面是一个最小可运行示例的核心逻辑(完整代码见 examples/tun_echo.cpp):打开 TUN 设备,将虚拟网内的 TCP 连接桥接到本机回环端口的 echo 服务,UDP 数据报则在引擎层直接回显。bidirectional_bridge 是双向数据泵,在虚拟连接与本机 socket 之间互转数据,这里只列出核心的监听与桥接发起逻辑。
// TCP:将虚拟连接桥接到本机 echo 服务 (127.0.0.1:echo_port)
net::awaitable<void> tcp_listener(tunio::tunio &engine, uint16_t echo_port)
{
auto ex = co_await net::this_coro::executor;
tunio::tun_tcp_acceptor acceptor(engine);
for (;;) {
tunio::tun_tcp_socket client(ex);
boost::system::error_code ec;
co_await acceptor.async_accept(
client, net::redirect_error(net::use_awaitable, ec));
if (ec)
co_return;
// 虚拟连接的目标地址,由引擎从 TCP 流中解析出来
const auto dest = client.original_destination();
const net::ip::tcp::endpoint target =
dest.address().is_v6()
? net::ip::tcp::endpoint(net::ip::address_v6::loopback(),
echo_port)
: net::ip::tcp::endpoint(net::ip::address_v4::loopback(),
echo_port);
net::co_spawn(ex, bidirectional_bridge(std::move(client), target),
net::detached);
}
}
// UDP:每个虚拟客户端会话对应一个回显协程
net::awaitable<void> udp_echo_handler(tunio::tun_udp_socket session)
{
std::array<char, 2048> buf;
try {
for (;;) {
net::ip::udp::endpoint sender;
size_t n = co_await session.async_receive_from(
net::buffer(buf), sender, net::use_awaitable);
co_await session.async_send_to(sender, net::buffer(buf, n),
net::use_awaitable);
}
} catch (...) {
session.close();
}
}
net::awaitable<void> udp_listener(tunio::tunio &engine)
{
auto ex = co_await net::this_coro::executor;
tunio::tun_udp_acceptor acceptor(engine);
for (;;) {
tunio::tun_udp_socket session(ex);
boost::system::error_code ec;
co_await acceptor.async_accept(
session, net::redirect_error(net::use_awaitable, ec));
if (ec)
co_return;
net::co_spawn(ex, udp_echo_handler(std::move(session)), net::detached);
}
}
int main()
{
net::io_context io(1);
tunio::tunio engine(io);
tunio::tun_config cfg;
cfg.dev_name = "tun0";
cfg.ipv4_addr = "10.0.0.1";
cfg.netmask = "255.255.255.0";
cfg.mtu = 1500;
boost::system::error_code ec;
if (!engine.open(cfg, ec)) {
std::cerr << "open TUN failed: " << ec.message() << std::endl;
return 1;
}
net::co_spawn(io, tcp_listener(engine, 7), net::detached);
net::co_spawn(io, udp_listener(engine), net::detached);
io.run();
return 0;
}
以 root 运行并配置好路由后,虚拟网内客户端即可访问本机 echo 服务:
sudo ./tun_echo --tun tun0 --ip 10.0.0.1 --netmask 255.255.255.0
sudo ip route add 10.0.0.0/24 dev tun0 # 或由外部策略路由注入流量
平台支持与构建
| 平台 | 设备实现 | 说明 |
|---|---|---|
| Linux | TUN(posix::stream_descriptor) |
需 root 或 CAP_NET_ADMIN,支持 IFF_MULTI_QUEUE 多队列 |
| macOS | utun | 需 root,当前仅支持句柄注入 |
| Windows | TAP(overlapped I/O)或 Wintun | 编译时 USE_WINTUN_DRIVER 切换 Wintun |
依赖 cmake 3.20+、c++20 编译器与 Boost 1.81+(需要 boost::unordered_flat_map 与 net::any_completion_handler)。构建与测试:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failure
仓库自带四个示例程序:tun_echo(上述回显)、tun2socks(基于 SOCKS5 的透明代理,支持 TCP CONNECT 与 UDP ASSOCIATE)、tun_packet(直接使用设备层接口的原始 IP 包中继/打印)、benchmark(异步接口每操作堆分配与吞吐基准)。tun2socks 的业务代码十分精简,你可以对照 examples/tun2socks.cpp 感受一下这种 API 风格给应用层带来的便利。
结语
TUN 设备相关的应用场景很多,而用户态协议引擎的通用化一直是个难题。tunio 的取舍是:协议栈只做转发所必需的部分,把精力花在 API 设计与平台适配的一致性上,让上层业务真正简单起来。详细的架构与协议设计说明见仓库中的 DESIGN.md。
- 仓库:https://github.com/Jackarain/tunio
- 许可证:Boost Software License 1.0
- 欢迎通过 issue / pull request 贡献,提交 PR 时请附带可复现的用例与说明。