【OPS.0x04】使用 Wireguard 进行异地组网

本文最后更新于:2024年11月11日 晚上

龙哥就是龙!惹啊!

0x00. 一切开始之前

最近手上的各种服务器不知道为啥多了起来,但大都没有公网 IP,又都处在不同的局域网里,笔者的肉身连带自己的笔记本也没有办法同时处在这些不同的网络当中,因此笔者迫切需要一个可用且优雅的异地组网方案,从而让笔者能够无论身处何处都能随时访问不同局域网中的任意一台服务器, 当然现在看来这些服务器大部分情况下的用途似乎就是让笔者闲暇时刻运行 neofetch 和 btop 玩玩

局域网(Local Area Network)也就是我们常说的内网,此类网络通常共用一个连接到互联网(公网)的路由器,共用同一公网 IP,利用网络地址转换(Network Address Translation)技术为内网中的设备与公网服务器建立连接(简而言之就是公网 IP:端口 到内网 IP:端口 整了个映射),因此要与内网中的设备进行通信,我们实质上要实现 内网穿透 ,也叫 NAT 穿透 (NAT traversal)

这个需求的实现其实很简单,我们只需要把不同 LAN 当中的设备组到同一个虚拟网络当中就行—— Virtual Private Network 就是专业干这个的,我们可以使用 VPN 技术在设备间建立点对点的加密信道,从而将不同局域网当中的服务器连接起来,而若要为都处于内网的机器进行打洞,则通常需要有一台位于公网的服务器为他们进行中继:

现有的比较成熟的无需用户自行在公网架设服务器的异地组网服务有 tailscale、zerotier 等,但由于其在中国大陆内通常没有相应的中继服务器,从而导致在国内想要通过此类服务进行连接则往往带有一定的延迟(反正笔者是忍受不了)

如何解决这个问题?不难想到我们可以在公网 VPS 上自行搭建 tailscale 的 DERP 节点,或是自建 zerotier 的 Moon 节点,但既然都要自建节点了, 那么我们为什么不直接自建 VPN 呢 —— 诸如 IPSec、OpenVPN 等老牌 VPN 方案可谓是应有尽有

但相比于这些传统方案,笔者更愿意选择被 Linus 青睐甚至已经合并入 Linux kernel mainline 的 WireGuard —— 这是一个高性能的基于 UDP 的现代 VPN 协议,或许也是目前已知的最好的 VPN 解决方案

本篇博客主要讲述如何利用公网服务器搭建 WireGuard VPN 网络

0x01. 公网服务器基本配置

安装与配置 WireGuard

首先我们在公网服务器上安装 WireGuard,笔者这里是 Ubuntu 24.04

参考: WireGuard - Installation

1
2
3
$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install wireguard

完成安装之后我们会获得一个内核模块,以及相应的用户态 CLI 程序 wg

接下来使用 wg 生成一份私钥与对应的公钥,存放在 /etc/wireguard 目录下:

1
$ wg genkey | sudo tee /etc/wireguard/privatekey | wg pubkey | sudo tee /etc/wireguard/publickey

接下来创建 WireGuard 配置文件,这里我们放在 /etc/wireguard/wg0.conf ,写入如下内容:

1
2
3
4
5
6
[Interface]
PrivateKey = 私钥文件内容
ListenPort = 自定义的监听端口
Address = 自定义的内网地址,例如 10.0.0.1/24
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o 你的能访问公网的网卡名 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o 你的能访问公网的网卡名 -j MASQUERADE

这里的 PostUpPostDown 参数分别为接口启动时与关闭时执行的命令,其中 wg0 为我们后面要创建的虚拟网卡名,该命令主要用于流量转发,这里不再赘叙

对于可以访问公网的网卡的名称,可以简单通过如下命令进行获取:

1
$ ip -o -4 route show to default | awk '{print $5}'

接下来使用如下命令启动该接口,其会创建一个虚拟网卡 wg0

1
$ sudo wg-quick up wg0

最后,我们可以为 wg0 接口配置自启动:

1
$ sudo systemctl enable wg-quick@wg0

配置防火墙

为了让 iptables 规则正常工作,我们还需要修改防火墙以允许 IPV4 流量转发,首先编辑 /etc/sysctl.conf 文件,添加如下配置:

1
net.ipv4.ip_forward = 1

也可以在 WireGuard 配置文件中的 [Interface] 添加如下配置:

1
2
PostUp = sysctl -w net.ipv4.ip_forward=1
PostDown = sysctl -w net.ipv4.ip_forward=0

然后重新载入配置:

1
$ sudo sysctl -p

最后开启 wireguard 监听端口上的 UDP 连接:

1
$ sudo ufw allow 你的自定义端口/udp

如果你使用阿里云这样比较专业的云服务器提供商,你可能还需要在相应的控制台页面添加规则:

0x02. 客户端基本配置

Linux 客户端配置

基础配置

首先还是按惯例在本地安装 WireGuard,这里笔者的系统是 openSUSE Tumbleweed

1
$ sudo zypper in wireguard-tools

然后按惯例生成公钥与私钥:

1
$ wg genkey | sudo tee /etc/wireguard/privatekey | wg pubkey | sudo tee /etc/wireguard/publickey

接下来修改服务端配置文件, 添加 客户端配置如下:

每新增一个客户端就添加一份 [Peer] 配置

1
2
3
[Peer]
PublicKey = 客户端生成的公钥
AllowedIPs = 自定义的内网地址,例如 10.0.0.2/32

重启服务端 WireGuard:

1
$ sudo wg-quick down wg0 && sudo wg-quick up wg0

然后创建配置文件 /etc/wireguard/wg0.conf ,写入如下内容:

1
2
3
4
5
6
7
8
9
[Interface]
PrivateKey = 客户端生成的私钥
Address = 上面配置的客户端地址
MTU = 1280

[Peer]
PublicKey = 服务端的公钥
Endpoint = 服务端的公网地址或是域名:服务端的端口
AllowedIPs = 服务端网段,例如 10.0.0.0/24

如果你不止想要组网,还想要让 WireGuard 服务端代理客户端的全部流量,只需要配置客户端的 AllowedIPs = 0.0.0.0/0 即可

最后按惯例启动 WireGuard 即可:

1
$ sudo wg-quick up wg0

如果有需要,可以配置自启动:

1
$ sudo systemctl enable wg-quick@wg0

配置 DDNS

作为中继的公网服务器的 IP 在某些需求下不一定是恒定的,虽然说域名通常是可以不变的,但有的时候域名对应的 IP 可能会发生改变,而 WireGuard 不会重复进行解析,仅会使用第一次的结果,从而可能造成组网失败 :(

对于 Linux,WireGuard 官方已经提供了 一份自动重新解析 DNS 的脚本 ,我们只需要设置一个定时执行任务即可,例如使用 crontab

在 Ubuntu 下,该脚本似乎会默认在 /usr/share/doc/wireguard-tools/examples/reresolve-dns/reresolve-dns.sh 安装一份

1
1 * * * * 脚本所在目录/reresolve-dns.sh wg0 # 每小时执行一次

Windows 客户端配置

不用 Windows 👋

首先从官网下载对应架构的安装包并安装,选择新建空隧道:

其会自动生成一份私钥和公钥,我们只需要再按照前面的方式填写配置文件即可:

之后在服务端也添加相应配置并重启 WireGuard 即可

0x03. 利用 NGINX 反代 WireGuard Over Trojan

注:这会让速度大幅下降➕延迟大幅提高(毕竟多了好几层传输和加解密的工作),因此除非 Wireguard 直连无法完成配置,否则笔者 其实不太推荐搭建这样的架构

众所周知随着国际网络局势不断变化,前面忘了中间忘了后面也忘了,总而言之就是跨国或是跨地区的 WireGuard 直接组网因为各种直接或间接的因素而存在一定的小问题导致无法连接成功,因此需要为我们的 WireGuard 隧道套一些防护

基于 UDP 的 WireGuard 协议容易被各种预期外的物理因素识别与阻断,因此我们不难想到的是我们可以再套几层协议以提高其连接稳定性,在 如何在澳洲校园网玩原神 这一篇博客中笔者简要叙述了使用 NGINX + Websocket + TLS + Trojan 的代理方案,在这里我们同样可以利用类似的架构转发 Wireguard 流量完成组网

随手画了个图,可能有点难看,不过大概是这个意思

注:似乎 WireGuard 在这个架构当中是多余的,也可以直接用 Xray 组网?

公网服务器配置

注:可以通过 CloudFlare CDN 代理隐藏公网服务器真实 IP,但需要注意的是在部分网络线路下 CloudFlare 的部分网段是被屏蔽的,从而导致可用性下降

1️⃣ 配置 WireGuard

和前面的流程基本一样,这里不再赘叙

2️⃣ 配置 NGINX 反向代理

首先是安装与启用 NGINX:

1
2
$ sudo apt-get install -y nginx
$ sudo systemctl enable --now nginx.service

接下来在 /etc/nginx/conf.d 目录下创建一个新的配置文件 自己想名字.conf ,写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name 你的域名;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_certificate 你的 .pem 证书路径;
ssl_certificate_key 你的 .key 密钥路径;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

location / {
root /srv/www/你的自定义网页路径;
index index.html;
}

location /和xray配置相同的你的自定义路径 {
if ($http_upgrade != "websocket") {
return 400;
}
proxy_pass http://127.0.0.1:和xray配置相同的你的自定义本地监听端口,例如11451;
proxy_intercept_errors on;
error_page 400 = https://你自己的域名/;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;

proxy_set_header X-Read-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

最后重启 NGINX:

1
$ sudo systemctl restart nginx.service

3️⃣ 配置 XRAY Trojan 代理

直接上 Docker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM debian:latest

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get upgrade
RUN apt-get install -y curl wget unzip procps

RUN wget -P /root/ https://github.com/XTLS/Xray-core/releases/download/v24.11.5/Xray-linux-64.zip
RUN unzip /root/Xray-linux-64.zip -d /root/
RUN chmod +x /root/xray
COPY ./config.json /root/config.json
RUN echo "#!/bin/sh\n/root/xray run -config /root/config.json\nsleep infinity" > /start.sh
RUN chmod +x /start.sh
RUN mkdir -p /var/log/xray
RUN ln -sf ../usr/share/zoneinfo/Australia/Melbourne /etc/localtime

EXPOSE 11451
CMD ["/start.sh"]

配置文件 config.json 这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
"log": {
"loglevel": "warning",
"error": "你的日志地址,例如/var/log/xray/error.log",
"access": "你的日志地址,例如/var/log/xray/access.log"
},
"inbounds": [
{
"listen": "127.0.0.1",
"port": 你的自定义的xray本地监听端口,例如11451,
"protocol": "trojan",
"settings": {
"clients": [
{
"password": "你自己想一个密码"
}
]
},
"streamSettings": {
"network": "ws",
"wsSettings": {
"path": "/自己设定的路径"
}
},
"sniffing": {
"enabled" : true,
"destOverride" : [
"http",
"tls"
]
}
}
],
"routing" : {
"rules" : [
{
"type" : "field",
"protocol" : [
"bittorrent"
],
"outboundTag" : "blocked"
}
]
},
"outbounds": [
{
"sendThrough": "0.0.0.0",
"protocol" : "freedom",
"settings" : {
"redirect": "127.0.0.1:WireGuard的本地监听端口"
}
}, {
"tag" : "blocked",
"protocol" : "blackhole",
"settings" : {}
}
]
}

构建运行,这里直接让容器使用本地网络:

1
2
$ docker build -t wg-xray .
$ docker run -d --name=wg-xray --network=host wg-xray

内网客户端配置

1️⃣ 配置 WireGuard

和前面基本一致,其他不再赘叙,不过配置文件修改如下:

1
2
3
4
5
6
7
8
9
[Interface]
PrivateKey = 客户端生成的私钥
Address = 上面配置的客户端地址
MTU = 1280

[Peer]
PublicKey = 服务端的公钥
Endpoint = 127.0.0.1:xray监听的本地端口
AllowedIPs = 服务端网段,例如 10.0.0.0/24

2️⃣ 配置 xray

直接上 Docker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM debian:latest

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y curl wget unzip procps

RUN wget -P /root/ https://github.com/XTLS/Xray-core/releases/download/v24.11.5/Xray-linux-64.zip
RUN unzip /root/Xray-linux-64.zip -d /root/
RUN chmod +x /root/xray
COPY ./config.json /root/config.json
RUN echo "#!/bin/sh\n/root/xray run -config /root/config.json\nsleep infinity" > /start.sh
RUN chmod +x /start.sh
RUN mkdir -p /var/log/xray
RUN ln -sf ../usr/share/zoneinfo/Australia/Melbourne /etc/localtime

CMD ["/start.sh"]

配置文件 config.json 这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
{
"log": {
"access": "",
"error": "",
"loglevel": "warning"
},
"inbounds": [
{
"tag": "wireguard",
"port": 本地代理端口例如11451,
"listen": "127.0.0.1",
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1",
"port": 本地代理端口例如11451,
"network": "udp",
"followRedirect": false
}
}
],
"outbounds": [
{
"tag": "proxy",
"protocol": "trojan",
"settings": {
"servers": [
{
"address": "你的域名",
"method": "chacha20",
"ota": false,
"password": "你的密码",
"port": 443,
"level": 1
}
]
},
"streamSettings": {
"network": "ws",
"security": "tls",
"tlsSettings": {
"allowInsecure": false,
"serverName": "你的域名",
"fingerprint": "chrome"
},
"wsSettings": {
"path": "/你的子路径",
"headers": {
"Host": "你的域名"
}
}
},
"mux": {
"enabled": false,
"concurrency": -1
}
},
{
"tag": "direct",
"protocol": "freedom",
"settings": {}
},
{
"tag": "block",
"protocol": "blackhole",
"settings": {
"response": {
"type": "http"
}
}
}
],
"dns": {
"hosts": {
"dns.google": "8.8.8.8",
"proxy.example.com": "127.0.0.1"
},
"servers": [
{
"address": "223.5.5.5",
"domains": [
"你的域名"
]
},
{
"address": "223.5.5.5",
"domains": [
"geosite:cn",
"geosite:geolocation-cn"
],
"expectIPs": [
"geoip:cn"
]
},
"1.1.1.1",
"8.8.8.8",
"https://dns.google/dns-query"
]
},
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"port": "443",
"network": "udp",
"outboundTag": "block"
},
{
"type": "field",
"port": "0-65535",
"outboundTag": "proxy"
}
]
}
}

构建运行,这里直接让容器使用本地网络:

1
2
$ docker build -t wg-xray .
$ docker run -d --name=wg-xray --network=host wg-xray

成功连接,唯一的问题是延迟可能会相对比较高:

0x04. 可能遇到的问题

客户端能握手但无法连接其他终端,wg show 显示 allowed ips: (none)

在多设备连接到同一网络时可能出现,客户端无法连接到包括服务端在内的其他终端, 但是可能会有一个奇异现象是仅有一个客户端能正常连接

出现这种情况的原因可能是因为 IP 冲突 ,大概原因是客户端之间的 IP 相互 overlap 了,从而只有其中一个能够正常工作

最简单的解决方案是 把客户端的子网掩码 设为 /32 ,下面是一个示例配置文件:

1
2
3
4
5
6
7
8
9
10
[Interface]
PrivateKey = 客户端私钥
Address = 10.191.98.10/32
MTU = 1280

[Peer]
PublicKey = 服务端公钥
Endpoint = 服务端公网IP或域名:服务端配置的端口
AllowedIPs = 10.191.98.0/24
PersistentKeepalive = 25

对应的服务端配置:

1
2
3
4
5
6
7
8
9
10
[Interface]
Address = 10.191.98.1/24
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
ListenPort = 服务端配置的端口
PrivateKey = 服务端私钥

[Peer]
PublicKey = 客户端公钥
AllowedIPs = 10.191.98.10/32

配置 WireGuard 后无法解析域名,但可以通过 IP 进行访问

可能是 DNS 解析出现问题,在客户端配置文件当中你可能会习惯性额外配置如下:

1
2
[Interface]
DNS = 8.8.8.8

这会覆盖原有 DNS 配置,但 DNS 服务器未必是可达的( 原因懂的都懂 ),最简单的解决方案是删除 DNS 配置或是更改成其他的可用的 DNS 服务器(例如 114.114.114.114


【OPS.0x04】使用 Wireguard 进行异地组网
https://arttnba3.github.io/2024/10/23/OPS-0X04-WIREGUARD_VPN/
作者
arttnba3
发布于
2024年10月23日
许可协议