Linux 上的 WireGuard 网络分析(一)
阅读此文章需要前置知识:Linux 网络基础知识、iptables、conntrack
本文内容部分采用了 Copilot 提示内容,也有部分内容用了 ChatGPT 免费版进行分析,确实都比较有帮助。
最近因为工作需要研究了一波 WireGuard 协议,在这篇文章中简单记录下心得。
WireGuard 是什么
WireGuard 是极简主义思想下的 VPN 实现,解决了很多现存 VPN 协议存在的问题。它于 2015 年由 Jason A. Donenfeld 设计实现,因其代码实现简洁易懂、配置简单、性能高、安全强度高而受到广泛关注。
WireGuard 在 2020 年初进入 Linux 主线分支,随后成为 Linux 5.6 的一个内核模块,这之后很快就涌现出许多基于 WireGuard 的开源项目与相关企业,各大老牌 VPN 服务商也逐渐开始支持 WireGuard 协议,很多企业也使用它来组建企业 VPN 网络。
基于 WireGuard 的明星开源项目举例:
- tailscale: 一套简单易用的 WireGuard VPN 私有网络解决方案,强烈推荐!
- headscale: tailscale 控制服务器的开源实现,使你可以自建 tailscale 服务。
- kilo: 基于 WireGuard 的 Kubernetes 多云网络解决方案。
- …
- 除了上面这些,还有很多其他 WireGuard 项目,有兴趣可以去awesome-wireguard 仓库看看。
WireGuard 本身只是一个点对点隧道协议,只提供点对点通信的能力(这也是其极简主义思想的体现)。而其他网络路由、NAT 穿越、DNS 解析、防火墙策略等功能都是基于 Linux 系统的现有工具来实现的。
在这篇文章里,我将搭建一个简单的单服务器 + 单客户端 WireGuard 网络,然后分析它如何使用 Linux 系统现有的工具,在 WireGuard 隧道上搭建出一个安全可靠的虚拟网络。
文章测试用到的服务器与客户端均为虚拟机,使用 Ubuntu 20.04 系统,内核版本为 5.15,也就是说都包含了 wireguard 内核模块。
WireGuard 服务端网络分析
简单起见,这里使用 docker-compose 启动一个 WireGuard 服务端,使用的镜像是linuxserver/docker-wireguard。
配置文件如下,内容完全参考自此镜像的官方 README:
|
|
将上面的配置文件保存为 docker-compose.yml
,然后通过如下命令后台启动 WireGuard 服务端:
|
|
WireGuard 服务端启动好了,现在查看下服务端容器的日志(我加了详细注释说明):
|
|
通过日志能看到,程序首先创建了 WireGuard 设备 wg0 并绑定了地址 10.13.13.1
。作为
WireGuard 网络中的服务端,它所创建的这个 wg0 的任务是成为整个 WireGuard 虚拟网络的默认网关,处理来自虚拟网络内的其他 peers 的流量,构成一个星型网络。
然后服务端为它所生成的 peer1 添加了一个路由,使得 peer1 的流量能够被正确路由到 wg0 设备上。
最后为了让 WireGuard 虚拟网络内的其他 peers 的流量能够通过 wg0 设备访问外部网络或者互相访问,服务端为 wg0 设备添加了如下的 iptables 规则:
iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT;
:允许进出 wg0 设备的数据包通过 netfilter 的 FORWARD 链(默认规则是 DROP,即默认是不允许通过的)iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE
:在 eth+ 网卡上添加 MASQUERADE 规则,即将数据包的源地址伪装成 eth+ 网卡的地址,目的是为了允许 wireguard 的数据包通过 NAT 访问外部网络。- 而回来的流量会被 NAT 的 conntrack 链接追踪规则自动允许通过,不过 conntrack 表有自动清理机制,长时间没流量的话会被从 conntrack 表中移除。这就是前面
docker-compose.yml
中的PERSISTENTKEEPALIVE_PEERS=all
参数解决的问题通过定期发送心跳包来保持 conntrack 表中的连接信息。 - 这里还涉及到了 NAT 穿越相关内容,就不多展开了,感兴趣的可以自行了解。
- 而回来的流量会被 NAT 的 conntrack 链接追踪规则自动允许通过,不过 conntrack 表有自动清理机制,长时间没流量的话会被从 conntrack 表中移除。这就是前面
WireGuard 的实现中还有一个比较重要的概念叫做 AllowedIPs
,它是一个 IP 地址列表,表示允许哪些 IP 地址的流量通过 WireGuard 虚拟网络。为了详细说明这一点,我们先看下服务端配置文件夹中 wg0 的配置:
|
|
AllowedIPs
实际就是每个 peer 在服务端路由表中的 ip 地址,它既可以是 ip 也可以是网段,而且能设置多个,这使所有 peer 都可以负责一个甚至多个 ip 段的转发,也就是充当局域网的路由器——VPN 子路由。
WireGuard 本身只是一个点对点隧道协议,它非常通用。通过 AllowedIPs
参数,我们就能在每个
peer 上添加各 peers 的配置与不同的路由规则,构建出各种复杂的网络拓扑,比如星型、环型、树型等等。
WireGuard 客户端网络分析
现在换台虚拟机跑 WireGuard 客户端,首先需要安装 wireguard 命令行工具:
|
|
第二步是从服务端的配置文件夹中找到 peer1/peer1.conf
,它是服务端容器根据参数 PEERS=1
自动生成的客户端配置文件,先确认下它的内容:
|
|
插入下,这个 Endpoint 的地址也很值得一说,能看到服务端 wg0.conf 的配置中,peer1 并未被设置任何 Endpoint,这实质是表示这个 peer1 的 Endpoint 是动态的,也就是说每次 peer1 发送数据到服务端 wg0 时,服务端通过认证加密技术认证了数据后,就会以数据包的来源 IP 地址作为 peer1 的 Endpoint,这样 peer1 就可以随意更换自己的 IP 地址(Roaming),而 WireGuard 隧道仍然能正常工作(IP 频繁更换的一个典型场景就是手机的网络漫游与 WiFi 切换)。这使 WireGuard 具备了比较明显的无连接特性,也就是说 WireGuard 隧道不需要保持一个什么连接,切换网络也不需要重连,只要数据包能够到达服务端,就能够正常工作。
因为我这里是内网环境测试,配置文件中的 Peer
- Endpoint
的 IP 地址直接用服务端的内网 IP
地址就行,也就是 192.168.5.198
。
如果你的服务端有公网 IP 地址(比如是云服务器,或者通过端口映射用家庭宽带的动态公网 IP),这个 Endpoint 地址也可以使用该公网 IP 地址,效果是一样的。
配置文件确认无误后,将该配置文件保存到客户端的 /etc/wireguard/peer1.conf
这个路径下,然后使用如下命令启动 WireGuard 客户端:
|
|
上述命令会自动在 /etc/wireguard/
目录下找到名为 peer1.conf
的配置文件,然后根据其内容启动一个名为 peer1
的 WireGuard 设备并完成对应配置。
我启动时的日志如下,wg-quick 打印出了它执行的所有网络相关指令(我添加了详细的注释):
|
|
跑完后我们现在确认下状态,应该是能正常走 WireGuard 访问相关网络了,可以 WireShark 抓个包确认下。
如果网络不通,那肯定是中间哪一步配置有问题,可以根据上面的日志一步步排查网络接口、路由表、路由策略、iptables/nftables 的配置,必要时可以通过 WireShark 抓包定位。
现在再检查下系统的网络状态,首先检查下路由表,会发现路由表没任何变化:
|
|
但是我们的 WireGuard 隧道已经生效了,这就说明现在我们的流量已经不是直接走上面这个默认路由表了,还有其他配置在起作用。往回看看前面的客户端启动日志,其中显示 wg-quick 创建了一个名为 51820 的路由表,我们来检查下这个表:
|
|
能看到这个表确实是将所有流量都转发到了 WireGuard 的 peer1 接口,基本能确认现在流量都走了这个路由表。那么问题来了,系统的流量是如何被转发到这个路由表的呢?为什么默认的路由表现在不生效了?
要理清这个问题,需要补充点知识——Linux 从 2.2 开始支持了多路由表,并通过路由策略数据库来为每个数据包选择正确的路由表,这个路由策略数据库可以通过 ip rule
命令来查看、修改。
前置知识补充完毕,现在来看下系统当前的路由策略,同样我已经补充好了注释:
|
|
结合注释看完上面的路由策略,现在你应该理清楚 WireGuard 的路由规则了,它加了条比默认路由策略 32766
优先级更高的路由策略 32765
,将所有普通流量都通过它的自定义路由表路由到 peer1
接口。另一方面 peer1 接口在前面已经被打了 fwmark 标记 51820
也就是 16 进制的 0xca6c,所以 peer1 出站到服务端的流量不会被 32765
匹配到,所以会走优先级更低的 32766
策略,也就是走了 main 路由表。
另外 32764
这条路由策略有点特殊,这里也简单解释下,此策略在前面注释中已经做了解释——是让所有非默认路由的流量都走 main 路由表,而 main 路由表中的非默认路由一般都是其他程序自动管理添加的,或者是我们手动添加的,所以这条规则其实就是确保这些路由策略仍然有效,避免 WireGuard
策略把它们覆盖掉而导致问题。
前面都分析完了,现在还剩下 wg-quick 日志的最后一行 nft -f /dev/fd/63
,它到底做了什么呢?
nft 是 nftables 的命令行工具名称,所以它实际是设置了一些 nftables 规则,我们查看下它的规则内容:
注意:nftables 的这些 chain 名称是完全自定义的,没啥特殊意义
|
|
可以看到这里是创建了一个 wg-quick-peer1
表,通过该表在 netfilter 上设置了如下规则:
preraw
链:此链用于防止恶意数据包进入网络。- type 开头的一行是规则的类型,这里是
filter
,仅匹配了raw
链的prerouting
表。 - 它丢弃掉所有来源接口不是 peer1、目的地址是 10.13.13.2、且源地址不是本地地址的数据包。
- 总结下就是只允许本地地址或者 peer1 直接访问 10.13.13.2 这个地址。
- type 开头的一行是规则的类型,这里是
premangle
链:此链用于确保所有 UDP 数据包都能被正确从 WireGuard 接口入站。- 它将所有 UDP 数据包的标记设置为连接跟踪标记(没搞懂这个标记是如何生效的….)。
postmangle
链:此链用于确保所有 UDP 数据包都能被正确从 WireGuard 接口出站。- 它将所有 UDP 数据包的标记设置为 0xca6c(51820 的 16 进制格式)(同样没理解这个标记是如何生效的…)。
最后看下 WireGuard 的状态,它是前面 wg setconf peer1 /dev/fd/63
设置的:
|
|
分析完毕,现在关闭掉 WireGuard 客户端,将客户端主机的网络恢复到正常状态。
|
|
结语
一通分析,你是否感觉到了 wg-quick 的实现十分巧妙,通过简单几行 iptables/nftables 与
iproute2 命令就在 WireGuard 隧道上实现了一个 VPN 网络,更妙的是只要把新增的这些
iptables/nftables 与 iproute2 规则删除,就能恢复到 WireGuard 未启动的状态,相当于整个工作是完全可逆的(显然前面的 sudo wg-quick down peer1
就是这么干的)。
总之这篇文章简单分析了 wireguard 虚拟网络在 Linux 上的实现,希望对你有所帮助。
下一篇文章(如果有的话…),我会带来更多的 WireGuard 实现细节,敬请期待。
参考
- wireguard protocol: 官方文档还有官方的白皮书,都写得很清晰易懂。
- WireGuard到底好在哪?: 比较深入浅出的随想,值得一读。
- Understanding modern Linux routing (and wg-quick): 对 WireGuard 客户端用到的多路由表与路由策略技术做了详细的介绍。
相关内容
如果你觉得这篇文章对你有所帮助,欢迎评论、分享、打赏~
赞赏