Coding 的痕迹

一位互联网奔跑者的网上日记

0%

使用 WireGuard 连接校园网

背景

被封在寝室,一直以来使用 easyconnect-docker 连接校园网(参考好友 @zhangzqs 的文章 # Linux下优雅地通过docker-easyconnect实现内网访问。然而最近总是出现连不上的情况:

1
2
3
4
5
6
$ sudo ./easyconnect.sh
WARNING: logging deactivated (can't log to stdout when daemonized)
auto login is disabled
auth failed, please check if the vpn address is valid and reachable!
login failed!
svpn stop!

由于对 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
2
3
4
5
6
# ip link add dev wg0 type wireguard
# ip link set dev wg0 mtu 1420
# ip addr add 192.168.51.1/24 dev wg0
# wg set wg0 listen-port 4857 private-key ./privatekey
# wg set wg0 peer [Peer B public key] persistent-keepalive 25 allowed-ips 192.168.51.2/32
# ip link set wg0 up

这里相较 Archlinux wiki 有改动,去掉了 peer 的 endpoint 参数。因为校园网的出口是随机的,而且我也没有在上面添加 NAT 规则的权力。

服务器:

1
2
3
4
5
6
# ip link add dev wg0 type wireguard
# ip link set dev wg0 mtu 1420
# ip addr add 192.168.51.2/24 dev wg0
# wg set wg0 listen-port 3981 private-key ./privatekey
# wg set wg0 peer [Peer A public key] persistent-keepalive 25 allowed-ips 192.168.51.2/32 endpoint dormitory.host.sunnysab.cn:4857
# ip link set wg0 up

注意,执行完这条命令后, wg 会立刻解析其中的主机名,最终保存到配置文件中的只是宿舍网络对应的 IP 地址。

可以使用 wg 命令查看配置概要,并使用 ping 命令测试两端连通情况。不难看出 WireGuard 配置十分方便。由于其底层基于 UDP 协议,双方仅在需要的时候才有网络通信。

1
2
3
4
5
6
7
8
9
10
[desktop wireguard]# ping 192.168.51.2
PING 192.168.51.2 (192.168.51.2) 56(84) 字节的数据。
64 字节,来自 192.168.51.2: icmp_seq=1 ttl=64 时间=6.69 毫秒
64 字节,来自 192.168.51.2: icmp_seq=2 ttl=64 时间=6.35 毫秒
64 字节,来自 192.168.51.2: icmp_seq=3 ttl=64 时间=5.14 毫秒
64 字节,来自 192.168.51.2: icmp_seq=4 ttl=64 时间=4.89 毫秒
^C
--- 192.168.51.2 ping 统计 ---
已发送 4 个包, 已接收 4 个包, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 4.887/5.767/6.690/0.768 ms

可以看出 ping 时的延时很低,效果不错。

持久化

配置可以通过 showconf 来保存:

1
2
# wg showconf wg0 > /etc/wireguard/wg0.conf
# wg setconf wg0 /etc/wireguard/wg0.conf

DDNS

我们可以手动修改服务器上的配置 /etc/wireguard/wg0.conf,使其 Endpoint 地址对应于域名。然后再启动 WireGuard, 查看配置:

1
2
3
4
5
6
7
8
9
10
$ sudo wg showconf wg0
[Interface]
ListenPort = 3981
PrivateKey = <hidden>

[Peer]
PublicKey = iwhxlX/k0VTNZF6RhS7l961UXn/0ssFkCRQXlf3gNAQ=
AllowedIPs = 192.168.51.1/32
Endpoint = 101.94.*.*:4857
PersistentKeepalive = 25

喔,又变成了 IP 地址。这里需要用到一个 wireguard-tools 自带的 DNS 重解析脚本 reresolve-dns.sh,我的 Manjaro Linux 下可以在 /usr/share/wireguards-tools/example/ 目录找到,但是 Ubuntu 安装时似乎没有带这个脚本。如果找不到,可以从前面的 Github 链接下载。

使用方法,定时执行此脚本即可。 crontab 会比较方便,也有人 使用 systemd 管理

1
2
3
4
5
6
7
8
9
reresolve-dns
=============

Run this script from cron every thirty seconds or so, and it will ensure
that if, when using a dynamic DNS service, the DNS entry for a hosts
changes, the kernel will get the update to the DNS entry.

This works by parsing configuration files, and simply running:
$ wg set wg0 peer ... endpoint ...

推测其原理是,每次 wg set ... endpoint 域名wg 都会解析这个域名,从而达到了动态更换地址的效果。

到目前为止,我们已可以使用服务器上的 SOCKS5/HTTP 代理访问校内各项网络服务了。然而既然使用了网络层的 WireGuard,为何不直接在网络层做路由呢?

路由

修改笔记本上的 WireGuard 配置项,使其允许来自 10.0.0.0/8 的数据包通过:

1
2
3
4
5
6
7
8
9
10
11
12
$ sudo wg
interface: sit-wg0
public key: iwhxlX/k0VTNZF6RhS7l961UXn/0ssFkCRQXlf3gNAQ=
private key: (hidden)
listening port: 4857

peer: 9ckAS7RhP2fhWg/lRMcFJ1dWHh+5cZyLnP/e8idH9RY=
endpoint: 116.236.*.*:42790
allowed ips: 10.0.0.0/8, 192.168.51.2/32
latest handshake: 1 minute, 53 seconds ago
transfer: 20.69 MiB received, 4.76 MiB sent
persistent keepalive: every 25 seconds

网络部署示意图如下(如果图片显示异常,请在新标签页内查看):

图中对校园网络的拓扑境况做了一定程度的简化。在实际环境中,安全网关和交换机通过 172.17.0.0/24 网段通信,校园网服务也不止存在于 10.0.0.0/8。不过这些并不影响我们配置。宿舍的路由器通过 DMZ 将笔记本映射到公网上,同时校内的路由器也通过 DMZ 将服务器映射到校内网络,在一般的理解中一般可以将他们视为一体。

假设笔记本的 wg0 网卡已经和服务器的 wg0 网卡建立了连接,一个从笔记本发出的请求需要经过这样一组顺序:

笔记本(wg0) => 服务器(wg0)=> 服务器(enp2s0)=> 校园网

首先在笔记本上设置路由表:

1
2
# 笔记本
sudo ip route add 10.0.0.0/8 via 192.168.51.1 dev wg0

在服务器开启 ip_forward,以开启数据包转发:

1
echo "1" > /proc/sys/net/ipv4/ip_forward

修改 /etc/sysctl.conf,添加行:

1
net.ipv4.ip_forward=1

然后执行 sysctl -p 永久应用该设置。

在服务器上使用 tcpdump 分别在 wg0enp2s0 上抓包:

1
2
tcpdump -i enp2s0 -nn icmp
tcpdump -i wg0 -nn icmp

在笔记本上 ping 另一台位于校园网的服务器 B,发现 wg0 上能收到笔记本发出的 ICMP 请求,但是 enp2s0 上没有。经过研究发现需要在转发表上设置 wg0 的数据包通过:

1
2
iptables -t filter -A FORWARD -i wg0 -j ACCEPT
iptables -t filter -A FORWARD -o wg0 -j ACCEPT

再次抓包,发现:

  • enp2s0 接口发送了 ICMP 请求,但接收不到响应。tcpdump 结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    root@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
2
14:21:52.295856 IP 192.168.51.1 > 10.1.160.158: ICMP echo request, id 27, seq 1, length 64
14:21:52.296367 ARP, Request who-has 192.168.51.1 tell 192.168.1.1, length 46

使用 hping3 工具伪造来源自 192.168.1.190192.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.

iptables-tutorial

修改源 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 的理由之一便是良好的防火墙支持,这便解释通了。

参考资料

  1. SNAT和DNAT的区别 - 思否, https://segmentfault.com/q/1010000002389520

  2. iptables中DNAT、SNAT和MASQUERADE的原理, iptables中DNAT、SNAT和MASQUERADE的原理_kingbrant的博客-CSDN博客

欢迎关注我的其它发布渠道