跳转至

通过 VPN 隧道异地组网

版权提示

本教程 YuelaiGroup 版权所有,禁止转载!

目前有两种主流异地组网方式:OpenVPN 以及 WireGuard:

特性 OpenVPN WireGuard
协议 基于 TLS/SSL,支持 TCP/UDP 基于 Noise,只支持 UDP
实现层级 用户态 内核态
加密算法 可选,AES/RSA/ECDSA 等 固定
配置 复杂,需要证书和 PKI 简单,基于公私钥
适用场景 企业 VPN、复杂网络环境 高速传输、云/家庭网络

以下教程服务端环境:Ubuntu 24.04 LTS;客户端环境:macOS 15。

在开始下列内容之前,请先学习计算机网络,否则你的折腾是毫无价值的。没有 Linux 及计算机网络基础请立即退出本教程。

OpenVPN

准备工作

Bash
1
2
3
sudo apt update && sudo apt upgrade
# 安装 openvpn 及 easy-rsa
sudo apt install openvpn easy-rsa
Bash
1
2
3
# 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
1
2
3
# 这里证书文件我命名为 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
1
2
3
4
# 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
1
2
3
4
# 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
1
2
3
sudo systemctl enable openvpn@server
sudo systemctl start openvpn@server
sudo systemctl status openvpn@server

OpenVPN 客户端配置

找到之前生成的客户端证书 ca.crtclient-mac.crtclient-mac.keyta.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
1
2
3
4
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 加入:

Text Only
net.ipv4.ip_forward = 1
Bash
sudo sysctl -p

配置 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
1
2
3
4
5
[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
1
2
3
4
5
[Interface]
# ...
[Peer]
PublicKey = <server_public_key>           # 上一步生成的客户端公钥
AllowedIPs = 10.1.24.0/24, 10.0.1.0/24    # 对端 [Interface] Address 所在网段

启动 WireGuard

Bash
1
2
3
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
1
2
3
4
5
6
7
8
9
[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
1
2
3
4
5
6
7
8
[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'

然后 enablestart 该服务 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
sudo sysctl -p
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 隧道内地址则十分合适。

  1. 作用范围:
    • 仅在本地网络内部可达;
    • 不会在公网(Internet)中被路由;
    • 可用于 VPN、内网服务器、IoT 等场景。
  2. 与 IPv6 Link-Local 不同
    • Link-Local (fe80::/10) 只能在 同一链路 使用;
    • ULA 可以在 整个内网 使用。

所有 ULA 都以 fc00::/7 开头,借助 IPv6 ULA generator 等工具,可以根据当前时间戳加上 MAC 地址,进行 SHA1 哈希,并取低 40 位来生成随机的 ULA:

Text Only
1
2
3
4
5
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 开始分配。