Technical note

PhanTun、udp2raw与wstunnel的UDP隧道选择

有些网络对普通TCP连接没有限制,UDP却很不稳定,甚至完全不可用。WireGuard一类依赖UDP的服务遇到这种环境时,可以在外面再套一层传输。PhanTun、udp2raw和wstunnel都能做这件事,但它们模拟的层次不同,部署代价也不一样。

三种方案解决的是不同层次的问题。真正选择时不必纠结谁“性能最好”,先看中间网络能够识别到哪一层:只有普通NAT和四层防火墙时,FakeTCP已经够用;必须经过HTTP代理或七层网关时,WebSocket才有意义。

三种方案的区别

工具外层流量加密与认证额外要求更适合的情况
PhanTun伪装成TCP的UDP流不负责业务加密TUN、IP转发、NAT规则追求简单和较低封装开销,链路只检查三、四层
udp2rawFakeTCP、UDP或ICMP自带对称加密与认证选项Raw Socket、配套防火墙规则需要多种伪装模式,或维护已有udp2raw部署
wstunnelWebSocket或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和持续丢包情况。

参考资料