背景
被封在寝室,一直以来使用 easyconnect-docker 连接校园网(参考好友 @zhangzqs 的文章 # Linux下优雅地通过docker-easyconnect实现内网访问。然而最近总是出现连不上的情况:
1 | sudo ./easyconnect.sh |
由于对 WireGuard 感兴趣,考虑到现有条件:
-
宿舍有一条中国电信商业宽带,带公网 IPv4 地址,已配置好 DDNS
-
校园网内有稳定开着的 Linux 服务器
-
阿里云公网上用 frp 转发流量,在传文件的场景下很贵
便打算使用 WireGuard 从校园网向宿舍建立一条虚拟链路,临时代替学校的 VPN 服务。
什么是 WireGuard
WireGuard 是一种极其简单但快速且现代的 VPN,它利用了最先进的加密技术。它的目标是比 IPsec 更快、更简单、更精简和更有用,同时避免令人头疼的问题。旨在提供比 OpenVPN 更高的性能。WireGuard 被设计为在嵌入式接口和超级计算机等上运行的通用 VPN,适用于许多不同的环境。最初仅支持 Linux 平台,现在可以进行跨平台(Windows、macOS、BSD、iOS、Android)的广泛部署。目前仍然在大力开发中,但已经被认为是业内最安全、最容易使用和最简单的 VPN 解决方案。
这里我们只需要知道它跨平台支持不错,代码精简、已并入 Linux 内核,使用方便、性能好。
安装与配置
ArchLinux 的 Wiki 上有一篇不错的配置文章可以参考:WireGuard (简体中文)
在本机 Manjaro Linux:
1 | yay -S wireguard-tools |
远程 Ubuntu:
1 | sudo apt install wireguard-tools |
生成公私钥:
1 | wg genkey | tee privatekey | wg pubkey > publickey |
两边生成好公私钥后即可开始配置。在这里为了避免与校园网网段冲突,我使用了虚拟网段 192.168.51.0/24
,笔记本的主机地址为 1
, 服务器的主机地址为 2
。
笔记本:
1 | ip link add dev wg0 type wireguard |
这里相较 Archlinux wiki 有改动,去掉了 peer 的 endpoint 参数。因为校园网的出口是随机的,而且我也没有在上面添加 NAT 规则的权力。
服务器:
1 | ip link add dev wg0 type wireguard |
注意,执行完这条命令后, wg 会立刻解析其中的主机名,最终保存到配置文件中的只是宿舍网络对应的 IP 地址。
可以使用 wg
命令查看配置概要,并使用 ping
命令测试两端连通情况。不难看出 WireGuard 配置十分方便。由于其底层基于 UDP 协议,双方仅在需要的时候才有网络通信。
1 | [desktop wireguard]# ping 192.168.51.2 |
可以看出 ping 时的延时很低,效果不错。
持久化
配置可以通过 showconf
来保存:
1 | wg showconf wg0 > /etc/wireguard/wg0.conf |
DDNS
我们可以手动修改服务器上的配置 /etc/wireguard/wg0.conf
,使其 Endpoint
地址对应于域名。然后再启动 WireGuard, 查看配置:
1 | sudo wg showconf wg0 |
喔,又变成了 IP 地址。这里需要用到一个 wireguard-tools
自带的 DNS 重解析脚本 reresolve-dns.sh,我的 Manjaro Linux 下可以在 /usr/share/wireguards-tools/example/
目录找到,但是 Ubuntu 安装时似乎没有带这个脚本。如果找不到,可以从前面的 Github 链接下载。
使用方法,定时执行此脚本即可。 crontab
会比较方便,也有人 使用 systemd 管理:
1 | reresolve-dns |
推测其原理是,每次 wg set ... endpoint 域名
时 wg
都会解析这个域名,从而达到了动态更换地址的效果。
到目前为止,我们已可以使用服务器上的 SOCKS5/HTTP 代理访问校内各项网络服务了。然而既然使用了网络层的 WireGuard,为何不直接在网络层做路由呢?
路由
修改笔记本上的 WireGuard 配置项,使其允许来自 10.0.0.0/8
的数据包通过:
1 | sudo wg |
网络部署示意图如下(如果图片显示异常,请在新标签页内查看):
图中对校园网络的拓扑境况做了一定程度的简化。在实际环境中,安全网关和交换机通过 172.17.0.0/24
网段通信,校园网服务也不止存在于 10.0.0.0/8
。不过这些并不影响我们配置。宿舍的路由器通过 DMZ 将笔记本映射到公网上,同时校内的路由器也通过 DMZ 将服务器映射到校内网络,在一般的理解中一般可以将他们视为一体。
假设笔记本的 wg0
网卡已经和服务器的 wg0
网卡建立了连接,一个从笔记本发出的请求需要经过这样一组顺序:
笔记本(wg0) => 服务器(wg0)=> 服务器(enp2s0)=> 校园网
首先在笔记本上设置路由表:
1 | 笔记本 |
在服务器开启 ip_forward,以开启数据包转发:
1 | echo "1" > /proc/sys/net/ipv4/ip_forward |
修改 /etc/sysctl.conf
,添加行:
1 | net.ipv4.ip_forward=1 |
然后执行 sysctl -p
永久应用该设置。
在服务器上使用 tcpdump
分别在 wg0
和 enp2s0
上抓包:
1 | tcpdump -i enp2s0 -nn icmp |
在笔记本上 ping 另一台位于校园网的服务器 B,发现 wg0
上能收到笔记本发出的 ICMP 请求,但是 enp2s0
上没有。经过研究发现需要在转发表上设置 wg0
的数据包通过:
1 | iptables -t filter -A FORWARD -i wg0 -j ACCEPT |
再次抓包,发现:
-
enp2s0
接口发送了 ICMP 请求,但接收不到响应。tcpdump
结果如下:1
2
3
4
5
6
7
8
9
10
11
12root@kite-agent:~# tcpdump -i enp2s0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enp2s0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
13:04:24.269534 IP 192.168.51.1 > 10.1.160.158: ICMP echo request, id 26, seq 1, length 64
13:04:25.275488 IP 192.168.51.1 > 10.1.160.158: ICMP echo request, id 26, seq 2, length 64
13:04:26.289494 IP 192.168.51.1 > 10.1.160.158: ICMP echo request, id 26, seq 3, length 64
13:04:27.302483 IP 192.168.51.1 > 10.1.160.158: ICMP echo request, id 26, seq 4, length 64
13:04:28.316465 IP 192.168.51.1 > 10.1.160.158: ICMP echo request, id 26, seq 5, length 64
^C
5 packets captured
5 packets received by filter
0 packets dropped by kernel -
服务器 B 能接收到服务器 A 转发的 ICMP 请求,并可以正常 echo. 但是
enp2s0
接口并没有收到。
初步判断是路由器转发了源为 192.168.51.1
的数据包,当数据包返回时路由器不知道 192.168.51.1
是谁。修改 tcpdump
的参数,将过滤器改为 icmp or arp
后发现果然如此:
1 | 14:21:52.295856 IP 192.168.51.1 > 10.1.160.158: ICMP echo request, id 27, seq 1, length 64 |
使用 hping3 工具伪造来源自 192.168.1.190
和 192.168.51.10
的 ICMP 请求测试,也可以得到类似结果。想到了三个解决方案:
-
将服务器外侧的路由器拆除,或将校园网络接口接入该路由器的 LAN 口,使其作为交换机使用。这样服务器A 便可以直接拥有校内网络 IP 地址。(要跑一趟,先不考虑)
-
路由器上设置静态路由表。
-
服务器A 的 enp2s0 在将包转发出去时对包的源地址进行转换。其实这是最常规的方案,但我不理解怎样使数据包转发后,对方响应时,iptables 能正确区分响应包的去向:是转发回
wg0
网卡,还是进入 INPUT 链传给上层应用程序。举个例子:我从虚拟网卡
192.168.51.1
拿到数据,将源地址转换为192.168.1.2
后投递出去。待路由器将目的地址为192.168.1.2
的包传递进来时,网卡enp2s0
怎么知道这个包是虚拟网卡发送的数据的响应,还是本机应用程序发送后收到的返回包?注意这里不考虑 TCP/UDP,假设只是一个简单的 ICMP 包,因此没有端口什么事。
SNAT
我选用通用性最好的方案三。利用 iptables 设置规则,可以在 enp2s0
将数据包发送前,通过 SNAT(Source Network Address Translation,源网络地址转换) 将请求包转换成其他地址。
The SNAT target is used to do Source Network Address Translation, which means that this target will rewrite the Source IP address in the IP header of the packet. This is what we want, for example, when several hosts have to share an Internet connection. We can then turn on ip forwarding in the kernel, and write an SNAT rule which will translate all packets going out from our local network to the source IP of our own Internet connection. Without doing this, the outside world would not know where to send reply packets, since our local networks mostly use the IANA specified IP addresses which are allocated for LAN networks. If we forwarded these packets as is, no one on the Internet would know that they were actually from us. The SNAT target does all the translation needed to do this kind of work, letting all packets leaving our LAN look as if they came from a single host, which would be our firewall.
The SNAT target is only valid within the nat table, within the POSTROUTING chain. This is in other words the only chain in which you may use SNAT. Only the first packet in a connection is mangled by SNAT, and after that all future packets using the same connection will also be SNATted. Furthermore, the initial rules in the POSTROUTING chain will be applied to all the packets in the same stream.
修改源 IP 地址的目的一般都是为了让这个包能再回到自己这里,所以在 iptables 中,SNAT 是在出口,也即
POSTROUTING
链发挥作用。
将来源为 192.168.51.1
的数据包源地址修改为 enp2s0
网卡对应的地址:
1 | sudo iptables -t nat -A POSTROUTING -s 192.168.51.1 -o enp2s0 -j MASQUERADE |
再在笔记本上测试就成功了。
回到谜团
回到前文中留下的问题:待路由器将目的地址为 192.168.1.2
的包传递进来时,网卡 enp2s0
怎么知道这个包是虚拟网卡发送的数据的响应,还是本机应用程序发送后收到的返回包?
与 @liyanfeng 讨论后认为, iptables 应该在转发时记住了 IP 之上的协议的相关信息。对于 TCP、UDP 来说,这一信息是端口。对于 ICMP 来说,协议中的 id
字段可以用来区分发送方。也就是说,iptables 进行的 SNAT 转发只能对特定支持的协议生效。进而联想到诸如 QUIC 协议基于 UDP 的理由之一便是良好的防火墙支持,这便解释通了。
参考资料
-
SNAT和DNAT的区别 - 思否, https://segmentfault.com/q/1010000002389520
-
iptables中DNAT、SNAT和MASQUERADE的原理, iptables中DNAT、SNAT和MASQUERADE的原理_kingbrant的博客-CSDN博客