Technical note
PhanTun、udp2raw与wstunnel的UDP隧道选择
有些网络对普通TCP连接没有限制,UDP却很不稳定,甚至完全不可用。WireGuard一类依赖UDP的服务遇到这种环境时,可以在外面再套一层传输。PhanTun、udp2raw和wstunnel都能做这件事,但它们模拟的层次不同,部署代价也不一样。
三种方案解决的是不同层次的问题。真正选择时不必纠结谁“性能最好”,先看中间网络能够识别到哪一层:只有普通NAT和四层防火墙时,FakeTCP已经够用;必须经过HTTP代理或七层网关时,WebSocket才有意义。
三种方案的区别
| 工具 | 外层流量 | 加密与认证 | 额外要求 | 更适合的情况 |
|---|---|---|---|---|
| PhanTun | 伪装成TCP的UDP流 | 不负责业务加密 | TUN、IP转发、NAT规则 | 追求简单和较低封装开销,链路只检查三、四层 |
| udp2raw | FakeTCP、UDP或ICMP | 自带对称加密与认证选项 | Raw Socket、配套防火墙规则 | 需要多种伪装模式,或维护已有udp2raw部署 |
| wstunnel | WebSocket或HTTP/2,可配TLS | 由TLS保护外层连接 | 证书或反向代理 | 需要穿过HTTP代理、七层网关,或复用HTTPS入口 |
三者都不是VPN。它们只负责把一段UDP流量搬到另一端,内层仍然需要WireGuard或应用自身提供身份认证和数据保护。尤其PhanTun只是流量混淆层,不能因为外观看起来像TCP就把它当作加密隧道。
PhanTun:尽量少改变UDP特性
PhanTun把UDP数据包转换成能够通过常见防火墙和NAT的TCP外观,但不会引入真正TCP的重传和流量控制,因此仍保留UDP乱序到达等特性。当前官方发布版为0.8.1。
它会在客户端和服务端创建TUN设备,需要先打开内核转发:
sudo sysctl -w net.ipv4.ip_forward=1
正式使用时应把配置写入/etc/sysctl.d/,而不是每次启动前临时执行。客户端还需要把TUN流量从物理网卡做源地址转换,服务端则要把监听的TCP端口转到PhanTun的TUN地址。规则应按官方README为当前接口和地址改写,不能直接照搬另一台机器的eth0和TUN网段。
以本机UDP服务监听127.0.0.1:51820、PhanTun服务端对外监听TCP 4567为例,进程本身的参数很简单:
# 服务端
RUST_LOG=info phantun_server \
--local 4567 \
--remote 127.0.0.1:51820
# 客户端
RUST_LOG=info phantun_client \
--local 127.0.0.1:51820 \
--remote tunnel.example.com:4567
PhanTun需要管理TUN设备,但没有必要因此开放Root SSH或让网络进程长期以Root身份运行。官方给出的非Root方式是为二进制授予cap_net_admin:
sudo setcap cap_net_admin=+pe /usr/local/bin/phantun_server
sudo setcap cap_net_admin=+pe /usr/local/bin/phantun_client
如果运行在PVE的非特权LXC里,还要让容器能够访问/dev/net/tun。这属于宿主机配置,容器编号和设备权限应以自己的环境为准,不能为了省事改成特权容器。
PhanTun不做IP分片和重组,并设置DF位。套在WireGuard外面时要给新增的IP、TCP和WireGuard头部留出空间;官方以1500字节链路为例,IPv4建议从1428开始,IPv6从1408开始。实际链路如果还有PPPoE、VLAN或其他封装,需要继续下调并观察丢包。
udp2raw:功能多,但维护痕迹更重
udp2raw默认使用FakeTCP,也能切换为UDP或ICMP模式。它通过Raw Socket工作,并依赖一组防火墙规则阻止内核TCP栈干扰伪造连接。-a可以自动添加规则,-g则只打印所需规则,后者更适合先审阅再纳入自己的防火墙配置。
下面是一组最小结构,密钥必须换成随机生成且只用于这一条隧道的值:
# 服务端:外层监听4096,内层交给本机UDP 51820
udp2raw_amd64 -s \
-l 0.0.0.0:4096 \
-r 127.0.0.1:51820 \
--raw-mode faketcp \
-k '<REPLACE_WITH_RANDOM_SHARED_KEY>' \
-a
# 客户端:本机UDP 51820转到远端4096
udp2raw_amd64 -c \
-l 127.0.0.1:51820 \
-r 192.0.2.10:4096 \
--raw-mode faketcp \
-k '<REPLACE_WITH_RANDOM_SHARED_KEY>' \
-a
客户端和服务端的--raw-mode、密钥、加密模式与认证模式必须一致。命令行里的共享密钥可能被同机用户从进程列表看到,服务文件和日志也可能泄露它,因此至少要限制配置文件权限,并避免把真实值提交到Git。
udp2raw仍然可用,但官方Release页面显示最新正式发布停留在2023年。新部署应优先评估维护更活跃的方案;已有稳定链路则没有必要只为了“换新”而迁移。
wstunnel:需要真正的WebSocket时再用
wstunnel把TCP或UDP流量放进WebSocket或HTTP/2连接,适合中间网络要求标准HTTP握手、必须经过HTTP代理,或者已经有HTTPS入口的情况。它的协议层次更高,部署时要同时考虑TLS证书、域名解析和反向代理超时。
服务端限制只允许转发到本机WireGuard端口,避免把它变成任意目标的开放代理:
wstunnel server \
--restrict-to 127.0.0.1:51820 \
wss://0.0.0.0:443
客户端在本机开一个UDP入口:
wstunnel client \
-L 'udp://127.0.0.1:51820:127.0.0.1:51820?timeout_sec=0' \
wss://tunnel.example.com:443
WireGuard的Endpoint随后指向127.0.0.1:51820。如果WireGuard接管默认路由,还要为wstunnel服务端地址保留一条走物理网关的静态路由,否则会形成“wstunnel连接又被送回WireGuard”的循环。
timeout_sec=0用于关闭UDP映射默认的空闲超时,适合长期保持的WireGuard入口。吞吐异常时先检查MTU,不要一开始就把问题归咎于WebSocket。
动态公网地址的处理
三种客户端都可以把远端写成域名,但常驻进程通常只在启动时解析一次。服务端公网地址变化后,DNS记录即使已经更新,旧进程也可能继续连接原地址。
处理方式是让systemd管理进程,再用一个很小的定时任务比较当前解析结果和上次记录;只有地址变化时才执行systemctl restart。脚本至少要满足这几个条件:
- 使用
getent ahostsv4或明确的A记录查询,失败时不覆盖旧状态。 - 用
flock防止定时任务重叠。 - 状态写入
/run或独立状态目录,不把解析结果混进配置文件。 - 由systemd负责重启和日志,不用
pkill -f模糊匹配整条命令。
如果地址很少变化,手工重启服务反而比维护一百多行监控脚本更可靠。
实际选择
普通家庭网络和四层防火墙之间传输WireGuard时,可以先试PhanTun;它的职责单一,额外开销和配置都比较容易看清。只有中间链路明确要求WebSocket、HTTP代理或HTTPS入口时才换wstunnel。udp2raw更适合维护既有部署,或者确实需要它提供的多种Raw模式。
无论选择哪一种,都应该先用前台命令确认两端参数和MTU,再写成systemd服务。隧道能建立只说明外层连通,最终还要检查内层握手、路由、DNS和持续丢包情况。