UTUN:极简 VPN 软件

2024-07-07 ⏳5.1分钟(2.0千字)

最近网络环境有所变化,公网 IP 地址没有了,而且也拿不到光猫管理员密码。要想访问内网设备,要么走内网穿透,要么走 VPN 组网。VPN 组网最灵活,不需要关心协议问题。好还我有几台服务器,可以用来组网。但组网的软件没用使用主流的 WireGuard 或者 ZeroTier,而是用了自已开发的 UTUN1。本文就跟大家分享使用 UTUN 的原委。

如果你是同城组网,数据不跨境传输,建议直接使用 WireGuard。无论是性能还是安全方面都无可挑剔。我的服务器都在海外,直接使用 WireGuard 过墙容易被封,所以使用自定制的协议。

另外,本文辅一发布,便收到依云的留言:

wg 已经简化很多了,没必要为了降低安全性而自行设计协议啊。

我的回复如下,供大家参考:

主要是因为 wg 协议跨 gfw 可能会被识别,然后 vps 就有可能被封😂

降低安全性确实争议很大。不过我们可以换个角度考虑🤔

传统 VPN 强调是 Private,内网流量跨公网传输要加密,不能被别人看见。但内网就一定是安全的吗?也不见得。所以有越来越多的网络在内网也使用 HTTPS 等加密方式来通信。如果内网一定安全,那就索性假设它不安全。

基于此,不妨把 VPN 看成 Virtual Public Network😂让他回归网络层或链路层本职,数据加密由传输层和应用层来做。

我在 utun 中使用共享密钥实现「加密」和「鉴权」。因为是共享密钥,理论上大量抓包后可以破解。但没关系,就算是破解了,攻击者也只能看到 IP 地址或者 SNI 等本来就会曝露在公网上的信息。这种方式没有让事情变得更坏。

至于攻击者破解后可以模拟其他设备发送数据包,这确实有风险。但考虑到我们一开始就假设内网也不安全,好像攻击者也做不了太多的事情。

总之,使用 utun 把 nat 之后的设备有限曝露到「公网」上,跟一台有公网 IP 的设备相比,应该不会有更大的安全隐患。这里的「加密」虽然菜鸡,但比没有还是要强一点的。有了这层弱加密,gfw 就无法识别 IP 和 SNI,用来翻墙也足够了😄

我们平时所说的 VPN 有两层功能。一层是 IP 隧道。也就是在两个网络节点之间建立一条虚拟的链路用来连接内部网络。这一层功能 Linux 内核就支持,比如 IPIP、GRE 隧道等都属此类。另一层功能是数据加密。VPN 通常使用公共网络传输数据。为了避免泄漏内部数据,都会将数据先加密再传输。为了实现加密功能,VPN 衍生出各种协议,比如 IPSec、 WireGuard 等等。

那在现在网络环境下有必要将 VPN 搞得这么复杂吗?我个人认为可以简化。因为现在数据基本在应用层或者传输层就加密过了,比如 HTTPS/STARTTLS/SSH 等。明文网络协议正在被逐卡淘汰。VPN 作为网络层,再对数据加密就有点重复了。

但能不能完全使用明文传输 VPN 数据呢?也不太合适。虽然应用数据已经加密,但网络层的 IP 地址、端口,甚至诸如 TLS SNI 等部分应用层信息,还是能被第三方监听。所以我建议使用简单对称算法实现加密。只要密钥足够长,安全性就没有问题。退一万步,就算是被破解了,攻击方能拿到的只有部分 IP 地址信息和 SNI 信息。实际的应用层数据还是加密过的。

基于上述思想,我设计了 UTUN 程序。它利用 Linux 内核的 tun 模块和 UDP 协议实现 VPN 功能。VPN 两端的节点通过设计共享密钥(建议大于 128 位)对收发报文做异或处理。两次异或就能还原数据。但如果不知道密钥,要想还原数据还得费一番功夫。

鉴权则采用了一种取巧的办法,校验 IP 报文的 checksum 字段。发送方在使用 UDP 转发 IP 报文时会做异或运算。接收方收到之后还原数据,并校验 checksum 值。如果校验值不匹配,则说明发送方的密钥不对,直接丢弃数据。

首先安装 UTUN 可执行文件。当前还没有提供预编译的二进制文件,大家需要通过 go 语言的工具来安装:

go install github.com/taoso/utun/cmd/utun

如果是想为其他平台编译,可以先下载代码再执行交叉编译。比如我需要在 mac 上分别为 AMD64 (服务器) 和 ARM64 (NAS) 两个平台编译二进制文件。

git clone https://github.com/taoso/utun.git
cd utun/cmd/utun
# amd64 平台
GOOS=linux go build .
# arm64 平台
GOOS=linux GOARCH=arm64 go build

建议两边都使用 systemd 自动启动 UTUN。对于服务端,也就是等待传入连接的一端,配置如下:

[Unit]
Description=UDP Tunnel
After=network.target

[Service]
Restart=always
RestartSec=1
ExecStart=/usr/local/bin/utun -listen :4430 -key YOUR-KEY
ExecStartPost=/usr/sbin/ip link set up tun0 mtu 1400
ExecStartPost=/usr/sbin/ip addr add 10.1.1.1 peer 10.1.1.2 dev tun0
ExecStartPost=/usr/sbin/ip route add 192.168.1.0/24 dev tun0
#ExecStartPost=/usr/sbin/iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

[Install]
WantedBy=multi-user.target

ExecStart 指定 utun 的启动参数。-listen 指定监听 UDP 的端口号,-key 指定共享密钥。

UTUN 只会建议最基本的 IP 隧道并转发数据。像是设置 tun 设备 IP 地址、修改系统路由表等操作都需要由外围工具来完成。这里我们利用 ExecStartPost 来指定 UTUN 启动之后要做的事情。

最主要的是为 tun 设备添加 IP 地址:

ip link set up tun0 mtu 1400
ip addr add 10.1.1.1 peer 10.1.1.2 dev tun0

我这里是将 utun 设备为点对点模式,本端地址为 10.1.1.1 对端地址为 10.1.1.2。如果想访问对端的整个网段,则可以添加路由:

ip route add 192.168.1.0/24 dev tun0

这里是说把所有目标为 192.168.1.0/24 网段的流量都交由 tun0 设备转发。

客户端,也就主动创建隧道的一端,配置如下:

[Unit]
Description=UDP Tunnel
After=network.target

[Service]
Restart=always
RestartSec=1
ExecStartPre=/sbin/modprobe tun
ExecStart=/usr/local/bin/utun -connect example.com:4430 -key YOUR-KEY
ExecStartPost=/usr/sbin/ip link set up tun0 mtu 1400
ExecStartPost=/usr/sbin/ip addr add 10.1.1.2 peer 10.1.1.1 dev tun0

[Install]
WantedBy=multi-user.target

整个配置跟服务端类似,不同的是客户端需要使用 -connect 来指定服务端的域名和端口。因为我的 NAS 默认没有加载 tun 模块,所以我使用 ExecStartPre 来预先执行 modprobe 命令加载 tun 模块。

到这里的就配置完成了。但还有一个细节需要注意。因为网络的一方在 NAT 设备之后,所以需要通过心跳来保活。如果长时间不收发数据,NAT设备就会回复对应的端口绑定规则。在此建议处在 NAT 设备的一方定时发心跳包。UTUN 本身不负责保活,可以使用 crontab + ping 实现。比如在服务端每四分钟发一个 ping 包:

*/4 * * * * /usr/bin/ping -c1 10.1.1.2

这里的时间间隔比较重要。原则上间隔越长越好,但又不能超过 NAT 设备的过期时间。大家需要根据自己的网络环境调整时间。我的 NAS 不方便设置定时任务,于是就是服务端 ping 客户端。如果时间间隔超过 NAT 设备的过期时间,就会失联。

到此我们就可以通过服务器来访问 NAT 网络的设备了🍻


  1. https://github.com/taoso/utun/↩︎