在您的 VPS 上自建 WireGuard VPN
在您自己的 Linux VPS 上部署 WireGuard:密钥生成、wg0.conf、IP 转发、NAT、AllowedIPs 语义、DNS,以及那些恼人的握手失败。
您要搭建的东西
在您自己拥有的服务器上搭建一个 WireGuard VPN,大约只需四十行配置:一对密钥、一个接口文件、一条 sysctl 设置、一条 NAT 规则、一个防火墙放行口。安装本身很简单,所以本指南的大部分内容讲的是哪里会出问题:密钥权限、AllowedIPs、转发和 DNS。
WireGuard 是内核中的三层(Layer 3)隧道,自 Linux 5.6 起进入主线内核,因此 Ubuntu 24.04 和 Debian 13 都自带它,无需额外模块。它没有加密算法协商,没有证书颁发机构(CA),也没有用户名/密码环节:一个对端(peer)就是一个公钥,加上这个公钥可以使用的那些 IP 地址。未通过 MAC 校验的数据包会被直接丢弃且不作任何回应,所以端口不会响应扫描。另一面是:不存在认证服务器,因此要收回访问权限,就意味着在这台机器上删除某个对端。
先确认虚拟化类型
WireGuard 需要一个您可以加载模块的内核,在 KVM 类型的 VPS 上它开箱即用。而在与宿主机共享内核的容器型虚拟化(OpenVZ、LXC)上,第一条命令就会失败并报 RTNETLINK answers: Operation not supported,此时的退路是 wireguard-go 这个用户态实现。请先用 sudo modprobe wireguard && echo ok 来确认。
生成密钥且不泄露它们
一个所有人可读的 /etc/wireguard/server.key 和根本没有 VPN 没有区别。常见的那条 umask 077 && wg genkey | sudo tee ... 并不可靠,因为 sudo 会对 tee 创建的文件套用它自己的 umask。请显式设置文件权限。
sudo apt update && sudo apt install -y wireguard nftables
sudo install -d -m 700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey > /etc/wireguard/server.key'
sudo sh -c 'wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub'
sudo chmod 600 /etc/wireguard/server.key用同样的方式生成客户端的密钥对。wg genpsk 可以生成一个可选的预共享密钥(pre-shared key),在每份配置中各占一行。
服务器接口:/etc/wireguard/wg0.conf
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/server.key>
[Peer]
PublicKey = <laptop public key>
PresharedKey = <psk, optional>
AllowedIPs = 10.8.0.2/32对它执行 chmod 600;如果启动时出现文件所有人可访问的警告,说明您漏了这一步。Address 是服务器在隧道内部的地址,携带的是整个 VPN 子网的掩码。请选一个在现实中不太会遇到的网段:192.168.1.0/24 会和您客户端所在的一半家用路由器冲突,届时隧道会悄无声息地输给本地路由。
在服务器一侧,某个对端的 AllowedIPs 是一个 /32,也就是该客户端所拥有的那一个隧道地址。如果给两个对端配置相同的 allowed IP,它会归属到最后配置的那个,而第一个则不再收到流量,且任何地方都不会打印错误。请不要设置 SaveConfig,否则 wg-quick down 会用运行时状态重写这个文件。
把这台机器变成路由器
Linux 服务器会丢弃不是发给它自己的数据包。转发和源 NAT 默认都是缺失的。
printf 'net.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1\n' \
| sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
sysctl net.ipv4.ip_forward单独一条 sysctl -w 在下次重启前有效,之后就会悄悄失效。NAT 需要的是出口接口,也就是能连到互联网的那块网卡,而不是 wg0。不要想当然认为它叫 eth0;请从 ip route show default 里取出您自己的接口名,因为如今的镜像用的是 enp1s0 或 ens3 这样的名字。
防火墙:端口,以及转发路径
一个 nftables 文件就能同时处理过滤和 NAT。写入 /etc/nftables.conf;它会清空(flush)现有的规则集,所以如果这台机器已经由 ufw 或 Docker 管理,请跳过这一步。
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif lo accept
tcp dport 22 accept
udp dport 51820 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
iifname "wg0" oifname "enp1s0" accept
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.8.0.0/24 oifname "enp1s0" masquerade
}
}用 sudo systemctl enable --now nftables 应用它,同时保留另一个已连接的 SSH 会话:policy drop 加上 SSH 规则里的一个笔误,就足以把您自己关在服务器门外。请留意转发链不允许的东西:wg0 到 wg0。各对端能连到互联网,但彼此之间不通;如果要做一个对端互通的 VPN,请加上 iifname "wg0" oifname "wg0" accept。同一条链还决定了对端能访问服务器本身的哪些部分,当这台机器同时兼作 在 tmux 中运行 Claude Code 的远程开发机、而您又不希望把那一面公开暴露时,这一点就很重要。
如果这台机器用的是 ufw:执行 ufw allow 51820/udp,在 /etc/default/ufw 中设置 DEFAULT_FORWARD_POLICY="ACCEPT",并在 /etc/ufw/before.rules 顶部加一条 *nat POSTROUTING MASQUERADE 规则。
用 systemd 启动它
sudo systemctl enable --now wg-quick@wg0
sudo wg showwg-quick 会创建接口、添加地址,并安装根据 AllowedIPs 推导出的路由。enable --now 是关键的一半:手动执行的 wg-quick up wg0 在下次重启后就没了,而内核升级意味着要重启。
客户端配置,以及人人都会搞错的那个设置
[Interface]
PrivateKey = <laptop private key>
Address = 10.8.0.2/32
DNS = 10.8.0.1
[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25AllowedIPs 一次同时承担两种不同的职责,把它们混为一谈正是大多数 WireGuard 困惑的根源。
出站方向,它是一张路由表。 目的地匹配某个对端 AllowedIPs 的数据包会被加密并发往该对端。0.0.0.0/0, ::/0 会把所有流量都送进隧道,也就是全隧道(full tunnel),把服务器作为默认路由。分隧道(split tunnel)则是一份更窄的列表:AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 承载 VPN 流量再加上服务器背后的一个私有网络,其余一切保持本地路由。正是这份窄列表让您可以把服务彻底挡在公网之外:一个绑定到隧道地址的 部署在 VPS 上的私有 Nextcloud 实例,或者运行在同一台机器上的 嵌套虚拟化实验虚拟机,都能被对端访问到,同时对其他所有人隐形。
入站方向,它是一张访问控制列表。 来自某个对端的、经解密后源地址不在该对端 AllowedIPs 之内的数据包会被丢弃。这就是为什么服务器为那台笔记本列出的是 10.8.0.2/32:如果这里写成 0.0.0.0/0,就会让该客户端得以伪造隧道内的任意地址。
PersistentKeepalive 是给位于 NAT 之后的客户端用的,这种情况下路由器只在有数据包流动时才保持 UDP 映射开启。一旦映射过期,服务器就再也无法主动联系到客户端。PersistentKeepalive = 25 会持续保持映射开启:把它设在客户端上,而不是设在拥有公网 IP 的服务器上。
DNS,以及无人察觉的泄露
当 AllowedIPs = 0.0.0.0/0 而没有 DNS = 这一行时,客户端会沿用它从本地网络学到的解析器,也就是那台位于 192.168.1.1 的咖啡馆路由器。这条路由比默认路由更具体,所以 DNS 查询会以明文从本地链路发出,而其余一切都走隧道。流量是私密的,但您访问的域名清单不是。
有两个诚实的选择。把 DNS 指向一个公共解析器(DNS = 9.9.9.9),查询就会走隧道并从您的服务器出去,不过那个解析器仍然看得到它们。或者运行绑定到 10.8.0.1 的 unbound 或 dnsmasq,设置 DNS = 10.8.0.1,并在 input 链里加上 udp dport 53 iifname "wg0" accept;如果加了这一行却忘了配置解析器,那就会什么都解析不了。
在 Linux 客户端上,wg-quick 通过 resolvconf 来应用 DNS;如果没有它,您会看到 resolvconf: command not found。请安装 openresolv,或者在使用 systemd-resolved 的客户端上设置 PostUp = resolvectl dns %i 10.8.0.1。
在不中断隧道的情况下增删对端
为了加一个用户而重启接口,会把所有已连接的人都踢下线。请把 [Peer] 段追加到 wg0.conf,然后就地重新加载对端集合。
sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'wg-quick strip 会打印去掉了仅供 wg-quick 使用的键(Address、DNS、PostUp)之后的配置,而 syncconf 则在保留现有会话的同时应用其中的差异。它只更新对端:改动过的 Address 仍然需要完整地 down/up 一次。用 sudo wg set wg0 peer <public key> remove 来吊销,然后从文件里删掉对应的段,否则它会在下次重新加载时回来。
各种故障形态,以及您会看到的字符串
握手始终无法完成。 wg show 列出了对端但没有 latest handshake,客户端日志则显示:
Handshake for peer 1 (10.0.0.10:51820) did not complete after 5 seconds, retrying (try 2)要么什么都没到达,要么什么都没被接受。按顺序检查:UDP 51820 是否在 VPS 防火墙以及您服务商的网络防火墙上都放行了,后者在大多数控制面板上是一项独立的开关;Endpoint 的地址和端口是否正确;密钥是否搞反了。客户端 [Peer] 段里的密钥必须是服务器的公钥,反之亦然:粘贴成了私钥,或粘贴成了客户端自己的公钥,都会正好导致这个症状。在服务器上运行 sudo tcpdump -ni any udp port 51820 可以看出数据包到底有没有到达。内核模块默认不记录任何日志;只有在您启用动态调试(echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control)之后,WireGuard 的消息才会出现在 dmesg 里,开启之后,密钥不匹配会表现为一次 invalid-MAC 丢包。
握手成功,但没有互联网。 ping 10.8.0.1 通,而 ping 1.1.1.1 超时:说明缺了转发或 NAT。先确认 sysctl net.ipv4.ip_forward 读出来是 1,然后在客户端 ping 的同时用 sudo nft list ruleset 或 sudo iptables -t nat -L POSTROUTING -n -v 观察计数器。masquerade 规则上是零个数据包,说明它的出口接口名写错了;计数器在上升却没有回包,则指向转发链的策略。
互联网通,但域名不通。 ping 1.1.1.1 成功,而 curl https://example.com 返回 Could not resolve host。这是 DNS 那一行缺失了,或者它指向了一个从隧道内部无法访问的解析器。
某些 HTTPS 网站卡住。 SSH 和 ping 都正常,但较大的页面会停住。这是路径 MTU 的问题:隧道增加了额外开销,而中间某段链路把超大的数据包丢弃了,却没有 ICMP 消息返回。请在客户端 [Interface] 里调低 MTU:先试 1420,再试 1380,然后 1280。
接口拒绝启动。 Address already in use 表示有另一个进程占用了 UDP 51820。up 失败之后出现 Cannot find device wg0,通常意味着配置被拒绝了;请查看 journalctl -u wg-quick@wg0 -n 50。
从 Streisand 或 OpenVPN 迁移
Streisand 已无人维护,其代码仓库也已归档,而在被废弃的自动化脚本上运行 VPN 是一个慢性的安全隐患。没有原地升级这回事,OpenVPN 的 PKI 也无法转换:WireGuard 没有证书、没有 CA、也没有有效期,所以每个客户端都要重新生成一对密钥。
请并行迁移:UDP 51820 上的 WireGuard 可以和 1194 上的 OpenVPN 在同一台机器上共存。先把 wg0 搭起来,一次迁一个客户端,然后再停掉旧服务。OpenVPN 的用户名/密码和吊销模型无法沿用过来;如果您需要账户体系或审计记录,请在 WireGuard 之上再叠一层来实现。
备份、升级,以及规模化时的压力点
/etc/wireguard 就是这台服务器。把它备份好(sudo tar czf wg-backup.tgz -C /etc wireguard,权限设为 600,存放在机器之外),您就能在几分钟内于一台全新的 VPS 上重建它。一旦丢失服务器的私钥,每一份客户端配置都必须重新签发,因为客户端固定绑定的是服务器的公钥。升级就是一次普通的 apt upgrade,加上内核更新时的一次重启,而如果您之前启用过 wg-quick@wg0,它会自行回来。
每个对端的状态占用很小,加密又运行在内核里,所以上限取决于您 VPS 的 CPU 和带宽配额,而不是这份配置里的任何东西;请用 iperf3 穿过隧道实测,而不要轻信某个公布的数字。真正在规模化时吃紧的是运维。每个对端都需要一个唯一的隧道 IP,而手工编辑六十个 [Peer] 段正是重复的 AllowedIPs 悄悄混进来的途径:请用脚本来生成这些配置。一台服务器就是一个 UDP 端点,也是一个单点故障,而 WireGuard 没有集群功能:冗余意味着再来一台拥有自己密钥的服务器。密钥轮换仍然是手工的,所以请记录下谁持有哪把密钥、以及您如何吊销其中一把。
所有这一切都需要一台您能掌控的 Linux 机器:一个公网 IP、一个您可以加载模块的内核,以及一道从头到尾归您掌管的防火墙。
FAQ
WireGuard 握手为什么一直无法完成?
wg show 列出对端却没有 latest handshake,意味着数据包没有到达或没有被接受。请检查 UDP 51820 是否在 VPS 防火墙和您服务商那道独立的网络防火墙上都放行,确认 Endpoint 的主机和端口,然后检查密钥有没有搞反:客户端 [Peer] 段里必须放的是服务器的公钥。在服务器上运行 sudo tcpdump -ni any udp port 51820 可以看出数据包究竟有没有到达;只有在您启用动态调试(echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control)之后,dmesg 才会报告 WireGuard 的握手失败,之后密钥不匹配会表现为一次 invalid-MAC 丢包。
隧道连上了但没有互联网,缺了什么?
ping 10.8.0.1 通而 ping 1.1.1.1 超时,指向的是转发或 NAT。请确认 sysctl net.ipv4.ip_forward 读出来是 1,并且它是在 /etc/sysctl.d/ 里设置的,而不只是一条会在重启时失效的 sysctl -w。然后检查 masquerade 规则里写的是您从 ip route show default 得到的真实出口接口,是 enp1s0 或 ens3,很少会是 eth0。
我的客户端配置里需要 DNS = 这一行吗?
在全隧道且没有 DNS = 这一行的情况下,客户端会沿用它从本地网络学到的解析器,于是这些查询会以明文从本地链路发出,而其余一切都走隧道。请把 DNS 指向一个公共解析器,或者运行绑定到 10.8.0.1 的 unbound/dnsmasq 并在 input 链里放行 udp dport 53 iifname "wg0"。
AllowedIPs 到底控制什么?
它承担两种职责。出站方向它是一张路由表:匹配某个对端 AllowedIPs 的流量会被加密并发往该对端。入站方向它是一张访问控制列表:经解密后源地址不在该对端 AllowedIPs 之内的数据包会被丢弃。这就是为什么服务器一侧为每个客户端列的是 /32,而客户端一侧却可以列 0.0.0.0/0。
WireGuard 能在任何 VPS 上运行吗?
在 KVM 类型的 VPS 上,它靠内核内置模块即可工作,无需额外配置。在与宿主机共享内核的容器型虚拟化上,例如 OpenVZ 或 LXC,modprobe wireguard 会失败并报 Operation not supported,退路是 wireguard-go 这个用户态实现。在做任何事情之前,请先运行 sudo modprobe wireguard && echo ok。