通过 VPN 隧道异地组网
目前有两种主流异地组网方式:OpenVPN 以及 WireGuard:
特性 |
OpenVPN |
WireGuard |
协议 |
基于 TLS/SSL,支持 TCP/UDP |
基于 Noise,只支持 UDP |
实现层级 |
用户态 |
内核态 |
加密算法 |
可选,AES/RSA/ECDSA 等 |
固定 |
配置 |
复杂,需要证书和 PKI |
简单,基于公私钥 |
适用场景 |
企业 VPN、复杂网络环境 |
高速传输、云/家庭网络 |
以下教程服务端环境:Ubuntu 24.04 LTS;客户端环境:macOS 15。
在开始下列内容之前,请先学习计算机网络,否则你的折腾是毫无价值的。没有 Linux 及计算机网络基础请立即退出本教程。
OpenVPN
准备工作
Bash |
---|
| sudo apt update && sudo apt upgrade
# 安装 openvpn 及 easy-rsa
sudo apt install openvpn easy-rsa
|
Bash |
---|
| # IP 转发
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
|
生成证书文件
首先创建证书目录
Bash |
---|
| make-cadir /etc/openvpn/openvpn-ca
cd /etc/openvpn/openvpn-ca
|
初始化 CA
Bash |
---|
| ./easyrsa init-pki
./easyrsa build-ca nopass
|
这里会要求填写 Common Name(比如 OpenVPN-CA)。
生成服务端证书和密钥
Bash |
---|
| ./easyrsa gen-req server nopass
./easyrsa sign-req server server
|
生成 Diffie-Hellman 参数和 TLS 密钥
Bash |
---|
| ./easyrsa gen-dh
openvpn --genkey secret ta.key
|
生成客户端证书
Bash |
---|
| # 这里证书文件我命名为 client-mac
./easyrsa gen-req client-mac nopass
./easyrsa sign-req client client-mac
|
配置 OpenVPN 服务端
复制之前生成的证书到 OpenVPN 目录
Bash |
---|
| cp pki/ca.crt pki/private/server.key pki/issued/server.crt pki/dh.pem ta.key /etc/openvpn/
|
创建服务端配置文件 /etc/openvpn/server.conf
Bash |
---|
| vim /etc/openvpn/server.conf
|
写入以下内容
注意,这里我没有在隧道内开启 IPv6,一般也没有必要,因为我们在隧道外能够通过 IPv6 进入隧道就可以了,内网终端使用 IPv4 访问往往会更方便。
Text Only |
---|
| # 监听 UDP 协议,支持 IPv4 和 IPv6
# 隧道外使用的协议,udp6/tpc6 均双栈监听
proto udp6
# OpenVPN 监听端口
port 1194
# 使用 TUN 模式
dev tun
topology subnet
# 隧道内虚拟 IPv4 网络配置
# 这里的网段用于分配给 VPN 客户端
server 10.8.0.0 255.255.255.0
# IPv6 网络配置 (略)
# server-ipv6 fd00::1:0:0:0/64
# 证书配置
# 填入刚才生成的所有证书文件
ca /etc/openvpn/ca.crt
cert /etc/openvpn/server.crt
key /etc/openvpn/server.key
dh /etc/openvpn/dh.pem
tls-auth /etc/openvpn/ta.key 0
# up/down 时执行 shell
# 常用于写入 NAT 配置,sh 脚本配置见后续内容
script-security 2
up /etc/openvpn/iptables-add.sh
down /etc/openvpn/iptables-delete.sh
# 下发客户端配置
# 推送内部路由,即外部设备需要访问的内网网段,注意需要做 NAT
push "route 10.0.1.0 255.255.255.0"
# 推送内部 DNS 服务器
push "dhcp-option DNS 10.0.1.2"
# 推送公共 DNS
# push "dhcp-option DNS 223.5.5.5"
# push "dhcp-option DNS 119.29.29.29"
# 客户端默认流量通过 VPN
# 尽量不要开,只推送需要的内网网段即可
# push "redirect-gateway def1 bypass-dhcp"
# IPv6 路由(允许客户端访问 VPN IPv6 子网)
# push "route-ipv6 fd00::/64"
# 允许相同证书多设备连接
duplicate-cn
# 允许客户端之间互相通信
client-to-client
# 连接保持与加密
keepalive 10 120
data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305
--data-ciphers-fallback AES-256-GCM
auth SHA256
# 用户权限与持久化
# user openvpn
# group openvpn
persist-key
persist-tun
# 日志与状态
status /var/log/openvpn-status.log
log /var/log/openvpn.log
verb 3
|
如果需要访问 VPN 服务器所在局域网的其他终端,则需要配置 NAT。配置后,来自 tun0
网卡及源 IP 为 10.8.0.0/24
的报文将使用 NAT,这样内网中的其他终端在响应报文时才会得到正确的源 IP 地址,即 VPN 服务器的 IP 地址,而非隧道内的虚拟 IP 地址 10.8.0.0/24
。
/etc/openvpn/iptables-add.sh |
---|
| # NAT
iptables -A FORWARD -i tun0 -j ACCEPT
iptables -A FORWARD -o tun0 -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eno1 -j MASQUERADE
|
/etc/openvpn/iptables-delete.sh |
---|
| # NAT
iptables -D FORWARD -i tun0 -j ACCEPT
iptables -D FORWARD -o tun0 -j ACCEPT
iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eno1 -j MASQUERADE
|
其中 tun0
为 OpenVPN 创建的 TUN 网卡,10.8.0.0/24
为分发给 VPN 客户端的网段,eno1
为 VPN 服务器出口网卡,均需要根据自己的实际环境修改。
更多的参数请参考官方示例:https://github.com/OpenVPN/openvpn/blob/master/sample/sample-config-files/server.conf
启动 OpenVPN 服务
Bash |
---|
| sudo systemctl enable openvpn@server
sudo systemctl start openvpn@server
sudo systemctl status openvpn@server
|
OpenVPN 客户端配置
找到之前生成的客户端证书 ca.crt
,client-mac.crt
,client-mac.key
,ta.key
填入下面的配置文件。
创建客户端配置文件 client-udp-ipv6.ovpn
Text Only |
---|
| client
dev tun
proto udp6
remote <VPN 服务器 IP 或 DDNS 域名> 1194
resolv-retry infinite
nobind
# 证书和密钥
# ca ca.crt
# cert client-mac.crt
# key client-mac.key
# remote-cert-tls server
# tls-auth ta.key 1
# 嵌入 tls-auth 文件要用下面这种写法
key-direction 1
# 加密和压缩
data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305
auth SHA256
# 其他
remote-cert-tls server
persist-key
persist-tun
verb 3
<ca>
-----BEGIN CERTIFICATE-----
(这里放 ca.crt 内容)
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
(这里放 client1.crt 内容)
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
(这里放 client1.key 内容)
-----END PRIVATE KEY-----
</key>
<tls-auth>
-----BEGIN OpenVPN Static key V1-----
(这里放 ta.key 内容)
-----END OpenVPN Static key V1-----
|
在 macOS 上用 Viscosity / OpenVPN Connect 导入配置文件即可完成连接,这里我推荐 Viscosity,因为后者的 macOS 版本,偶尔连接不上,这是一个未知问题。
WireGuard
小白建议使用这个来进行组网,就别去折腾 OpenVPN,够你喝一壶的。
准备工作
安装 WireGuard
Bash |
---|
| sudo apt update
sudo apt install wireguard
|
生成客户端密钥
Bash |
---|
| cd /etc/wireguard
sudo umask 077
# 生成密钥对
sudo wg genkey | sudo tee server_private.key | wg pubkey | sudo tee server_public.key
|
server_private.key
:服务端私钥
server_public.key
:服务端公钥
开启 IP 转发
在 /etc/sysctl.conf
加入:
配置 WireGuard 服务端
创建配置文件 /etc/wireguard/wg0.conf
Bash |
---|
| vim /etc/wireguard/wg0.conf
|
Text Only |
---|
| [Interface]
Address = 10.1.24.1 # VPN 隧道中的虚拟 IP
ListenPort = 21668 # WireGuard UDP 端口
PrivateKey = <server_private_key> # 上一步生成的客户端私钥
# NAT 转发
# 注意将 iptables 中 -s 10.1.24.0/24 网段更改为 [Peer] AllowedIPs
# 如有多个 AllowedIPs 网段,就写多条 iptables
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -s 10.1.24.0/24 -o eno1 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -s 10.1.24.0/24 -o eno1 -j MASQUERADE
MTU = 1280
[Peer]
PublicKey = <client_public_key> # 后续客户端生成的公钥
AllowedIPs = 10.1.24.0/24 # 客户端 [Interface] Address 所在网段
|
其中,eno1
替换为服务器出口网卡名称(可用 ip addr
查看),因为我没有家庭内部网络访问外地网络的需要,所以 AllowedIPs
就仅填写了 WireGuard 隧道虚拟网段。
我要解释一下 [Interface]
和对端 [Peer]
:其实 WireGuard 没有客户端,服务端的概念,而是 P2P 点对点连接的概念。
比如我现在在外地,需要和家庭内网组网,那么使用 WireGuard 我不仅能够让外地设备能够访问家庭内网,也能让家庭内网访问外部设备所在内网。其拓扑结构如下:
|
外地网络某终端 |
家庭网络 VPN 服务器 |
WireGuard 隧道虚拟 IP |
10.1.24.2 |
10.1.24.1 |
局域网 IP 及其所在子网 |
192.168.0.6(192.168.0.0/24) |
10.0.1.2(10.0.1.0/24) |
公网 IP |
X.X.X.X |
X.X.X.X |
根据以上拓扑结构,那么在 VPN 服务器侧,配置文件对端部分就应该写为:
Text Only |
---|
| [Interface]
# ...
[Peer]
PublicKey = <server_public_key> # 上一步生成的客户端公钥
AllowedIPs = 10.1.24.0/24, 192.168.0.0/24 # 对端 [Interface] Address 所在网段
|
其中,AllowedIPs
允许来自 wg0
网卡的 10.1.24.0/24
即 WireGuard 隧道中对端所在网段,还需要允许对端的局域网网段 10.1.24.0/24
。
在外地某终端侧,配置文件对端部分同理:
Text Only |
---|
| [Interface]
# ...
[Peer]
PublicKey = <server_public_key> # 上一步生成的客户端公钥
AllowedIPs = 10.1.24.0/24, 10.0.1.0/24 # 对端 [Interface] Address 所在网段
|
启动 WireGuard
Bash |
---|
| sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
sudo wg show
|
配置 WireGuard 客户端
同样的,在任意设备执行下列命令生成客户端的密钥对
Bash |
---|
| wg genkey | tee client_private.key | wg pubkey | tee client_public.key
|
client_private.key
:客户端私钥
client_public.key
:客户端公钥
打开 WireGuard 客户端,添加下列配置文件:
Text Only |
---|
| [Interface]
PrivateKey = <client_private_key>
Address = 10.1.24.2
MTU = 1280
[Peer]
PublicKey = <server_public_key>
Endpoint = <server_public_ip>:21668
AllowedIPs = 10.1.24.0/24, 10.0.1.0/24
PersistentKeepalive = 25
|
然后点击启动即可完成组网,GUI 操作很简单。
现在我就可以使用我的外地终端访问家庭内网 10.0.1.0/24
的任何终端了。
DDNS 解析导致连接失败
在 DDNS 场景下,一般主机 IP 会经常变动,但 WG 不会定时重新解析 DNS 以获得最新的主机 IP 地址,所以需要借助定时任务来手动完成 Endpoint 刷新。
官方脚本:https://github.com/WireGuard/wireguard-tools/blob/master/contrib/reresolve-dns/reresolve-dns.sh
借助该脚本,我们可以通过 systemd
定时任务来更新 WG 对端配置,timer
会触发同名的 service
服务。
/etc/systemd/system/wg_reresolve-dns.timer |
---|
| [Unit]
Description=Periodically reresolve DNS of all WireGuard endpoints
[Timer]
# 每 30 秒触发一次
OnCalendar=*:*:0/30
[Install]
WantedBy=timers.target
|
/etc/systemd/system/wg_reresolve-dns.service |
---|
| [Unit]
Description=Reresolve DNS of all WireGuard endpoints
Wants=network-online.target
After=network-online.target nss-lookup.target
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'for i in /etc/wireguard/*.conf; do /usr/share/wireguard-tools/examples/reresolve-dns/reresolve-dns.sh "$i"; done'
|
然后 enable
并 start
该服务 wg_reresolve-dns.timer
。
可以通过下列命令来观察定时任务是否正确执行:
Bash |
---|
| sudo journalctl -u wg_reresolve-dns.service -f
|
扩展配置
开启 Linux BBR 拥塞控制算法
在 Ubuntu 上开启 BBR(Bottleneck Bandwidth and Round-trip propagation time)TCP 拥塞控制可以显著提升网络性能。
编辑 /etc/sysctl.conf
,添加以下内容:
Text Only |
---|
| net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
|
重新加载配置:
Bash |
---|
| # 输出 bbr 则开启成功
cat /proc/sys/net/ipv4/tcp_congestion_control
|
IPv6 ULA 用于 VPN 隧道
IPv6 ULA(Unique Local Address) 是类似于 IPv4 的 私有地址(10.x.x.x、192.168.x.x、172.16-31.x.x) 的一类地址,用于 局域网内部通信,不能在公网路由。所以,将其用于 VPN 隧道内地址则十分合适。
- 作用范围:
- 仅在本地网络内部可达;
- 不会在公网(Internet)中被路由;
- 可用于 VPN、内网服务器、IoT 等场景。
- 与 IPv6 Link-Local 不同
- Link-Local (
fe80::/10
) 只能在 同一链路 使用;
- ULA 可以在 整个内网 使用。
所有 ULA 都以 fc00::/7
开头,借助 IPv6 ULA generator 等工具,可以根据当前时间戳加上 MAC 地址,进行 SHA1
哈希,并取低 40
位来生成随机的 ULA:
Text Only |
---|
| MAC address: 20:37:06:12:34:56
IPv6 ULA: fdf4:e2da:b670::/48
First routable block: fdf4:e2da:b670:0::/64
Last routable block: fdf4:e2da:b670:ffff::/64
|
下面以 OpenVPN 为例,将可路由的 IPv6 ULA 块作为 VPN 隧道的地址池,OpenVPN 服务端会将地址块中第一个地址分配给自己:
Text Only |
---|
| # 其它配置...
# IPv6 网络配置
server-ipv6 fdf4:e2da:b670:0::/64
# 推送 IPv6 路由
push "route-ipv6 fdf4:e2da:b670:0::/64"
# 推送 IPv6 DNS(VPN 服务器地址)
push "dhcp-option DNS6 fdf4:e2da:b670:0::1"
# 其它配置...
|
对于 OpenVPN 客户端,经过测试,地址会从 fdf4:e2da:b670:0::1000
开始分配。