OpenVPN 服务端搭建与家用网络远程接入

给家里的服务器搭 OpenVPN,目标是:外地设备连回来能直接访问家里内网,客户端管理要能批量签发。这篇把最终方案和几个关键决策记录下来。

为什么是 OpenVPN

备选都看了:WireGuard 更轻量但内核模块依赖和兼容性在Debian 上有小坑,IPsec 配置繁琐,Tailscale 方便但流量要过中转。OpenVPN 的优势是纯用户态、配置文件全内联、客户端一个 .ovpn 文件走天下,对”自用 + 偶尔给朋友开个账号”的场景最省心。

整条链路是这样:客户端拿到 tun 地址 → 流量封进 UDP 1194 隧道 → 服务端 tun 拆包按 route 转发 → 出 eth0 时被 MASQUERADE 改写。反向的 iroute 打通见后面的 ccd 小节。

OpenVPN 隧道与内网访问的数据流示意

服务端核心配置

单文件 server.conf,关键决策是所有证书密钥全部内联(<ca></ca>、<cert></cert>、<key></key> 块),不引用外部路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
port 1194
proto udp
dev tun

# 加密套件:AES-256-GCM,性能和安全兼顾
cipher AES-256-GCM
auth SHA256

# 网络规划:tun 网段和服务端虚拟 IP
server 10.8.0.0 255.255.255.0
topology subnet

# 客户端 DNS:推到内网 DNS,解析内网域名
push "dhcp-option DNS 10.8.0.1"
push "dhcp-option DOMAIN lan.example.com"

# 内网路由:告诉客户端 192.168.110.0/24 在 VPN 后面
route 192.168.110.0 255.255.255.0

keepalive 10 120
persist-key
persist-tun
user nobody
group nogroup

几个点说明:

  • dev tun 路由模式:客户端拿到的是三层网络,能直接访问对端内网,比 tap 桥接模式干净;
  • route + push route 分工:route 让服务端认识对端网段,客户端访问该网段的流量才会被送进来;
  • 证书内联:升级服务端时不用同步一堆文件,scp 一个 conf 就完事。

NAT:用 systemd drop-in,不碰 iptables-persistent

VPN 客户端访问家里内网需要 MASQUERADE,最可靠的做法是挂在服务的生命周期上,而不是手动 iptables-save:

1
2
3
4
# /etc/systemd/system/openvpn-server@server.service.d/nat.conf
[Service]
ExecStartPost=/sbin/iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
ExecStopPost=/sbin/iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

服务起来自动加规则,停服务自动撤规则。为什么不装 iptables-persistent:多一层持久化机制就多一个故障点,规则跟着服务走,行为完全可预期,重启服务器也不丢(systemd 会重新拉起)。

客户端签发脚本

自用场景,easy-rsa 的手工流程太啰嗦,封一个脚本:/root/ovpn-addclient.sh <名字>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
set -e
NAME=$1
EASYRSA_DIR="/etc/openvpn/easy-rsa"

cd "$EASYRSA_DIR"
./easyrsa build-client-full "$NAME" nopass

# 拼装 .ovpn:基础配置 + 三个证书块内联
{
cat /etc/openvpn/client-base.conf
echo "<ca>"; cat pki/ca.crt; echo "</ca>"
echo "<cert>"; awk '/BEGIN/,/END/' "pki/issued/$NAME.crt"; echo "</cert>"
echo "<key>"; cat "pki/private/$NAME.key"; echo "</key>"
} > "/root/$NAME.ovpn"

echo "生成完毕: /root/$NAME.ovpn"

客户端拿到 .ovpn 文件,任何平台(Windows/macOS/Android/iOS)的 OpenVPN 客户端导入即用,零额外配置。吊销就用 ./easyrsa revoke <名字> + 更新 CRL,脚本记得把这个也包进去。

ccd:给固定客户端绑内网路由

有一台客户端是”家里的路由器”,需要让 VPN 另一端的设备也能主动访问家里网段——这时单向 route 不够,要双向打通。用客户端配置目录(ccd):

1
2
3
# /etc/openvpn/ccd/home-router
iroute 192.168.110.0 255.255.255.0
ifconfig-push 10.8.0.10 255.255.255.0

iroute 告诉服务端”这个客户端后面挂着 192.168.110.0/24 这个网段”,配合固定的 ifconfig-push 虚拟 IP,双向流量都能正确寻址。前提是对端路由器自己也配了回程路由(把 10.8.0.0/24 的流量指到 OpenVPN 客户端机器),只改一边永远是单向通。

运维备忘

1
2
3
systemctl status openvpn-server@server     # 看状态
journalctl -u openvpn-server@server -f # 看连接日志
grep -c "MULTI: Learn" /var/log/openvpn.log # 粗略统计在线客户端

日志里 Peer Connection Initiated / MULTI: Learn: ... 是客户端上线/学习路由的标志,排查”通了又断”的问题先看这两行。

小结

这套方案的三个关键决策:配置全内联(迁移无脑)、NAT 用 systemd drop-in(行为可预期)、ccd + iroute 打通双向内网(家用场景刚需)。客户端一个 .ovpn 文件走天下,签发吊销全脚本化,自用完全够。