如何用TUN模式实现双网卡代理?

TUN模式 / 0人浏览

凌晨三点,老赵的显示器还亮着。屏幕上是一张比特币的K线图,刚刚一根十五分钟级别的阳线穿过布林带中轨,他的网格机器人理应触发一轮加仓。但交易日志里,三条“Order Failed: Connection Timeout”像三根钉子扎在他眼睛里。

他有两张网卡。一张是书房的千兆有线,走电信主干,延迟低但偶尔会抽风;另一张是手机热点的无线网卡,走移动网络,带宽小却稳定得像老狗。他试过用系统自带的“跃点”功能把交易流量绑到有线网卡上,可一旦有线抖动,SSH隧道就断,币安的WebSocket重连要花整整八秒——在合约市场,八秒足够让一个仓位从浮盈变成爆仓。

“你该试试TUN模式。”我在语音里跟他说。

老赵不是程序员,但他懂“代理”这个词。他之前用过某款代理软件,把浏览器流量导到境外VPS上,可那只是HTTP/SOCKS层面的代理,解决不了“双网卡同时工作且流量按需分流”的问题。他需要的是:交易API走有线,行情推送走无线,两者互为备份,且当某条线路延迟超过阈值时,整个TCP会话能无缝迁移到另一张网卡上,而不需要重启任何进程。

这就是TUN模式真正性感的地方。

从“路由表”到“虚拟网卡”:TUN到底在做什么

大多数人第一次接触TUN,是在配置VPN的时候。Linux内核里,TUN是一种虚拟网络设备——它不直接对应任何物理硬件,而是让用户态程序能够像操作真实网卡一样,读写IP数据包。当你创建一个TUN设备(比如tun0),并给它配上一个IP地址,系统就会多出一条虚拟链路。所有路由到tun0的流量,都会被内核交给那个打开/dev/net/tun的进程。

这个进程可以是OpenVPN,可以是WireGuard,也可以是你自己写的一个Python脚本。

关键点在于:TUN模式工作在第三层(网络层),它处理的是IP包,而不是TCP流。这意味着你可以在用户态里对每一个数据包做任意操作——修改源地址、打标签、根据目标端口选择出口网卡、甚至把同一个TCP连接的不同数据包从不同物理网卡发出去。

而传统的SOCKS/HTTP代理工作在第四层或更高,它必须维持一个完整的TCP连接,一旦底层物理链路切换,连接就断了。

对于老赵这种场景,TUN模式让他能实现“双网卡代理”的核心机制:策略路由 + 连接跟踪 + 动态出口选择

实战:把两张网卡塞进同一个TUN管道

老赵的机器是Ubuntu 22.04,两张网卡分别是enp3s0(有线)和wlp2s0(无线)。他想要的效果是:

  • 所有发往币安API(api.binance.com)的流量,优先走有线,当有线RTT超过200ms或丢包率大于5%时,自动切到无线。
  • 所有WebSocket行情推送(stream.binance.com),同时从两张网卡发出,接收端取最先到达的包,实现“冗余传输”。
  • 其他流量(比如更新系统、看YouTube)走无线,避免占用交易通道。

这听起来像是一个SD-WAN的简化版,但用TUN模式,几十行代码就能搞定。

第一步:创建TUN设备并接管默认路由

bash ip tuntap add mode tun dev tun0 ip addr add 10.0.0.1/24 dev tun0 ip link set tun0 up

然后写一个简单的Python程序,打开/dev/net/tun,设置IFF_TUN | IFF_NO_PI标志,开始读取IP包。

python import os, struct, fcntl, socket from fcntl import ioctl

TUNSETIFF = 0x400454ca IFFTUN = 0x0001 IFFNO_PI = 0x1000

tun = open('/dev/net/tun', 'r+b') ifr = struct.pack('16sH', b'tun0', IFFTUN | IFFNO_PI) fcntl.ioctl(tun, TUNSETIFF, ifr)

此时,tun0已经就绪。接下来,你需要把原本走物理网卡的路由,改成走tun0。比如:

bash ip route add default via 10.0.0.1 dev tun0 metric 100

但这样所有流量都会进入你的程序。你的程序需要决定:这个包应该从enp3s0发出去,还是从wlp2s0发出去?

第二步:在用户态实现“双网卡代理”

这里有一个技巧:你不能直接把包从TUN设备“转发”到物理网卡,因为TUN设备是三层设备,而物理网卡需要二层帧。你需要用原始套接字(raw socket)或者AF_PACKET来发送。

更优雅的方式是:在程序里维护两个原始套接字,分别绑定到enp3s0wlp2s0。当从tun0读到一个IP包时,解析它的目标地址和端口,然后根据策略选择出口网卡,再用对应的原始套接字把包发出去。

但这里有个问题:原始套接字发送的包,源IP地址是物理网卡的IP,而不是tun010.0.0.1。这会导致回包无法正确路由回tun0

解决方案是:在发送前做SNAT(源地址转换),把源IP改成物理网卡的IP,并记录一个映射表。当物理网卡收到回包时,再根据映射表把目标IP改回10.0.0.1,然后写入tun0

这就是一个最简化的“双网卡代理”模型。

第三步:用eBPF做动态出口选择

老赵不懂eBPF,但他知道“延迟超过200ms就切换”。这个逻辑可以放在用户态程序里,每隔一秒向两张网卡分别发送ICMP探测包,计算RTT。

但更高效的方式是用eBPF的BPF_PROG_TYPE_SCHED_CLS程序,在数据包离开tun0之前,根据当前网卡的实时延迟,打上一个“出口标记”。用户态程序读取这个标记,决定从哪张网卡发出。

c // eBPF伪代码 if (pkt->dst_port == 443 && is_binance_ip(pkt->dst_ip)) { if (enp3s0_rtt < 200 && enp3s0_loss < 5) pkt->mark = 1; // 走有线 else pkt->mark = 2; // 走无线 }

用户态程序根据mark值,选择对应的原始套接字。

第四步:WebSocket的冗余传输

对于行情推送,老赵希望“双发收先”。这需要在TUN程序里实现一个简单的“复制-发送”逻辑:对于目标端口为9443(币安WebSocket)的包,同时从两张网卡发出,并记录序列号。接收端(也就是币安的服务器)会收到两个相同的包,但TCP协议栈会自动去重。而老赵这边,只需要保证发出的包内容一致即可。

但这里有一个坑:TCP的序列号是端到端的,如果你从两张网卡发出相同的包,但源IP不同,服务器会认为这是两个不同的连接,导致RST。所以,对于TCP流量,你不能简单地“双发”。你需要的是“连接迁移”——当主网卡失效时,把TCP连接的状态(序列号、窗口大小)迁移到备用网卡上,并发送一个带有新源IP的包。

这需要修改内核的TCP栈,或者使用MPTCP(多路径TCP)。但MPTCP需要服务器端支持,币安显然不支持。

所以,对于老赵的场景,更现实的做法是:用TUN模式做“应用层代理”。也就是说,TUN程序不直接转发IP包,而是拦截发往币安的TCP连接,在用户态实现一个TCP代理。这个代理同时维护两条到币安的TCP连接(一条走有线,一条走无线),然后从两条连接里读取数据,取最先到达的,转发给本地的交易程序。

这就是所谓的“双网卡代理”——不是代理IP包,而是代理TCP流。

当虚拟币交易遇上TUN:一场毫秒级的战争

老赵最终用的是这个方案:一个基于TUN的透明TCP代理,用Go语言写的,核心逻辑是:

  1. 创建tun0,把所有去往币安IP段的流量路由到tun0
  2. 用户态程序从tun0读取IP包,解析出TCP SYN。
  3. 对于每个SYN,同时向两张物理网卡发起TCP连接(用SO_BINDTODEVICE绑定)。
  4. 哪个连接先完成三次握手,就用哪个连接作为主通道,另一个作为备用。
  5. 主通道的TCP流被拆成IP包,写入tun0,最终由交易程序接收。
  6. 如果主通道的RTT超过阈值,或者出现重传,立即切换到备用通道,并发送一个“连接迁移”信号。

这个方案的精髓在于:交易程序完全感知不到底层有两张网卡。它看到的只是一个普通的TCP连接,源IP是10.0.0.1。而实际上,这个连接被TUN程序“劫持”了,变成了一个双网卡冗余通道。

上线那天,老赵的网格机器人跑了整整24小时,没有一次“Connection Timeout”。凌晨三点,比特币再次插针,他的机器人成功在最低点加仓,醒来时浮盈12%。

他给我发了一条语音:“这TUN模式,比我想象的野。”

为什么不是简单的“双网卡绑定”

有人会问:为什么不用Linux的bonding或者team,把两张网卡绑成一个逻辑网卡?那样不就能自动冗余了吗?

答案是:bonding工作在二层,它需要两张网卡连接到同一个交换机,或者至少同一个广播域。老赵的有线是电信,无线是移动,根本不在同一个网络里。bonding无法跨运营商工作。

而TUN模式工作在第三层,它不关心底层物理链路是什么,只关心“这个包应该从哪个出口发出去”。这使得它能够跨越异构网络,实现真正的“双网卡代理”。

更重要的是,TUN模式让你能在用户态实现任意复杂的策略。比如:

  • 根据币安API的返回延迟,动态调整两条线路的权重。
  • 在行情剧烈波动时,临时把WebSocket流量切换到延迟更低的网卡。
  • 甚至可以在两条线路上同时发送交易请求,取最先成交的那个,实现“套利级”的冗余。

这些,都是传统代理和bonding做不到的。

最后一点技术细节:性能与坑

TUN模式不是没有代价的。每个IP包都要从内核拷贝到用户态,再从用户态拷贝到内核,这会增加CPU开销。对于高频交易,延迟可能增加几十微秒。但对于老赵这种分钟级的网格,完全够用。

另一个坑是:TUN程序需要处理TCP状态机。如果你只是简单地转发IP包,TCP的重传、拥塞控制都会失效。所以,要么用MPTCP,要么在用户态实现一个完整的TCP代理。后者更复杂,但更可控。

老赵用的是后者。他的Go程序里,有一个简化的TCP状态机,只处理ESTABLISHED状态下的数据转发,以及FIN/RST的清理。对于SYN和ACK,他直接放行,让内核去处理。

还有一个坑是DNS。币安的域名会解析到多个IP,你需要确保所有IP都被路由到tun0。老赵的做法是:在TUN程序里拦截DNS查询,把api.binance.com解析成自己的虚拟IP(比如10.0.0.2),然后把所有去往10.0.0.2的流量都代理到真实的币安IP上。

这样,交易程序只需要连接10.0.0.2,剩下的交给TUN程序。

凌晨五点半,老赵关掉显示器,走到窗前。天还没亮,但他的网格机器人已经在后台安静地运行。两张网卡,一条TUN隧道,把电信和移动的线路拧成一股绳。他知道,下一次插针来临时,他的订单会比别人早几毫秒到达币安的撮合引擎。

在虚拟币的世界里,几毫秒,就是天堂和地狱的距离。而TUN模式,就是那把打开天堂之门的钥匙。

版权声明:

作者: 最新VIVO手机VPN免费节点分享

链接: https://vivovpn.net/tun-mode/dual-nic-proxy-tun-mode.htm

来源: vivovpn.net

文章版权归作者所有,未经允许请勿转载。

最新文章

归档

标签