群晖 NAS 异地组网全攻略:OpenVPN + Docker 打通两地内网(从零部署)
一、部署目的
很多朋友有这样的需求:两处办公点或家庭网络需要像在同一局域网一样互相访问。比如 A 地办公室的电脑要直接访问 B 地仓库的打印机、NAS、监控,或者人在外面时用手机连回两地内网。
本教程基于两台群晖 NAS(分别位于 A、B 两地),通过 Docker 运行 OpenVPN 实现:
- 两地局域网互通:A 地与 B 地设备互相访问
- 移动端接入:手机在外网通过 VPN 访问两地内网
- IPv6 公网传输:利用 NAS 的公网 IPv6 建立隧道(无公网 IPv4 也可)
- TCP 协议:降低运营商 QoS 影响
- 流量伪装:非标端口 + tls-crypt,降低被识别封杀风险
- 证书 5 年有效:免去频繁重新签发的麻烦
说明:本文使用经典的
kylemanna/openvpn镜像(OpenVPN 2.4.9)。虽然该镜像多年未更新,但它是目前唯一能同时满足 site-to-site、TCP、IPv6、自定义证书有效期、完全自定义配置等全部需求的成熟方案。若追求最新版本,可参考文末的替代方案。
二、前置准备
2.1 环境要求
- 两台群晖 NAS(A 地、B 地),均已安装 Container Manager(Docker)
- 已开启 SSH:控制面板 → 终端机和 SNMP → 启用 SSH
- 群晖拥有公网 IPv6 地址
- 群晖配置了 IPv6 DDNS
本文约定:
| 项目 | A 地 | B 地 |
|---|---|---|
| 局域网网段 | 192.168.A.0/24 | 192.168.B.0/24 |
| NAS IP | 192.168.A.NAS | 192.168.B.NAS |
| 路由器 | 192.168.A.1 | 192.168.B.1 |
| DDNS 域名 | vpn.yourdomain.com | — |
| 角色 | Server | Client |
请根据你的实际情况替换
A、B、NAS等占位符。
2.2 验证网络环境
① 确认公网 IPv6(不能是 fe80: 开头的链路本地地址):
ip -6 addr show
正常的公网 IPv6 以 2001:、240e:、2409: 等开头。
② 确认 DDNS 解析到 IPv6:
nslookup vpn.yourdomain.com
应看到一条 2409:... 之类的 IPv6 记录。
注意:如果你的 NAS 只拿到
fe80:开头的链路本地地址,那是无法跨公网通信的,必须从路由器确认已下发公网 IPv6 前缀(SLAAC / DHCPv6)。
2.3 拉取镜像(两台都要)
docker pull kylemanna/openvpn
三、核心概念:OpenVPN 与网络层面的关系
在动手前,先理解 OpenVPN 在 site-to-site 组网中扮演的角色,这决定了后面所有配置。
3.1 OpenVPN 是”虚拟路由器”
OpenVPN 隧道建立后,VPN 服务端和客户端可以看成两台路由器。它们各自连接自己的局域网,中间通过加密隧道相连。
A地局域网(192.168.A.0/24) B地局域网(192.168.B.0/24)
│ │
群晖NAS-A(server) 群晖NAS-B(client)
└────────── VPN隧道(192.168.255.0/24) ──────────┘
3.2 要实现互通,需要三层配置
- OpenVPN 本身:隧道建立、证书认证
- 内核 IP 转发:NAS 允许把收到的包转发到另一个接口
- 路由/NAT:两端局域网设备知道”去对端网段”该走哪台 NAS
很多教程只讲 OpenVPN 配置,不讲后两层,导致”隧道通了但访问不了”——这是最常见的坑。
四、A 地 Server 端部署
4.1 创建配置目录
sudo mkdir -p /opt/openvpn
4.2 生成 server 配置(TCP over IPv6)
docker run -v /opt/openvpn:/etc/openvpn --rm \
kylemanna/openvpn ovpn_genconfig \
-u tcp6://vpn.yourdomain.com \
-p "route 192.168.B.0 255.255.255.0"
参数说明:
-u tcp6://...:TCP over IPv6 传输,地址用 DDNS 域名-p "route 192.168.B.0 ...":推送 B 地路由给所有客户端
4.3 初始化 PKI(证书 5 年有效期)
这是最关键的一步,关系到”5 年不出现验证失败”:
docker run -v /opt/openvpn:/etc/openvpn --rm -it \
-e EASYRSA_CERT_EXPIRE=1825 \
-e EASYRSA_CA_EXPIRE=1825 \
-e EASYRSA_CRL_DAYS=1825 \
kylemanna/openvpn ovpn_initpki
注意(证书有效期):要保证 5 年不出验证失败,必须把 CA、Server、Client 证书有效期全部设为 5 年(1825 天)。只设
EASYRSA_CERT_EXPIRE不够,还要EASYRSA_CA_EXPIRE。
sudo openssl x509 -in /opt/openvpn/pki/ca.crt -noout -dates
确认 notAfter 是 5 年后。
注意(证书文件名):生成的 server 证书文件名是以 commonName 命名(如
vpn.yourdomain.com.crt),不是server.crt。这是正常的,镜像会自动识别。
4.4 修改配置文件
编辑 /opt/openvpn/openvpn.conf:
① 非标端口(伪装):
port 54443
② 升级混淆 tls-auth → tls-crypt:
tls-crypt /etc/openvpn/pki/ta.key
③ 推送两条内网路由:
### Push Configurations Below
push "comp-lzo no"
push "route 192.168.A.0 255.255.255.0"
push "route 192.168.B.0 255.255.255.0"
4.5 开启内核 IP 转发
这是很多人漏掉的关键步骤。OpenVPN 服务端必须开启内核转发,否则客户端无法通过服务端访问内网:
# 临时生效
sudo sysctl -w net.ipv4.ip_forward=1
# 永久生效(群晖需通过开机脚本,见下文)
注意(持久化):群晖的
/etc/sysctl.conf在重启后会被覆盖。要让ip_forward=1永久生效,需要用到开机脚本(/usr/local/etc/rc.d/下的自启动脚本),或通过 控制面板 → 任务计划 添加开机自启任务。
4.6 启动 server 容器
docker run -d \
--name openvpn-a \
--network host \
--cap-add NET_ADMIN \
--device /dev/net/tun \
-v /opt/openvpn:/etc/openvpn \
--restart unless-stopped \
kylemanna/openvpn
验证启动:
docker logs openvpn-a
正常日志关键行:
Listening for incoming TCP connection on [AF_INET6][undef]:54443
Initialization Sequence Completed
注意(host 网络模式):必须用
--network host,因为需要 OpenVPN 直接操作宿主机的 tun 接口和路由。同时--cap-add NET_ADMIN和--device /dev/net/tun缺一不可。注意(停止时路由清理报错):容器停止时会出现
RTNETLINK answers: Operation not permitted,这是 host 模式下权限限制,不影响运行,可忽略。
4.7 A 地 NAT 转发
kylemanna/openvpn 镜像默认会自动添加 NAT 规则(将隧道网段 192.168.255.0/24 做 MASQUERADE)。验证:
sudo iptables -t nat -L -n
应能看到:
MASQUERADE all -- 192.168.255.0/24 0.0.0.0/0
五、签发客户端证书
5.1 签发 B 地客户端证书
docker run -v /opt/openvpn:/etc/openvpn --rm -it \
-e EASYRSA_CERT_EXPIRE=1825 \
kylemanna/openvpn easyrsa build-client-full clientB nopass
导出配置:
docker run -v /opt/openvpn:/etc/openvpn --rm \
kylemanna/openvpn ovpn_getclient clientB > /opt/openvpn/clientB.ovpn
5.2 签发移动端证书
docker run -v /opt/openvpn:/etc/openvpn --rm -it \
-e EASYRSA_CERT_EXPIRE=1825 \
kylemanna/openvpn easyrsa build-client-full mobile nopass
导出配置:
docker run -v /opt/openvpn:/etc/openvpn --rm \
kylemanna/openvpn ovpn_getclient mobile > /opt/openvpn/mobile.ovpn
5.3 修正导出的客户端配置(必做)
注意(端口错误):
ovpn_getclient生成的客户端配置端口还是默认 1194,不会同步你改的 54443,必须手动改成 54443。注意(tls-auth 残留):
ovpn_getclient生成的客户端配置仍是tls-auth,不会同步 server 的tls-crypt,必须手动改成tls-crypt。
手动修正每个 .ovpn 文件:
- 端口
1194→54443 - 只保留
remote vpn.yourdomain.com 54443 tcp6一行 <tls-auth>→<tls-crypt>,</tls-auth>→</tls-crypt>- 删除
key-direction 1
注意(tls-crypt 标签格式,最坑):手动改时,开始标签必须是
<tls-crypt>(没有斜杠)。如果误写成</tls-crypt>(带斜杠),客户端会报错:UNKNOWN/unsupported OPTIONS:'', '-----BEGIN', ...因为客户端找不到开始标签,把整块当普通文本逐行解析。同时建议删掉块内的注释行和空行,避免 OpenVPN 3 客户端解析报错。
六、移动端接入与测试
6.1 导入手机
安卓手机安装 OpenVPN Connect,导入修正后的 mobile.ovpn。
6.2 测试
连接成功后,手机拿到隧道 IP(如 192.168.255.6)。
如果”内网能 ping 到手机,但手机 ping 不到内网”,问题出在回包路由:
注意(回包路由,site-to-site 最常见问题):手机访问 A 地内网设备时,A 地设备的回包发往
192.168.255.6(隧道 IP),但 A 地内网设备/路由器不知道这个网段该走哪,导致回包丢失。解决方案:在 A 地路由器加静态路由:
目标网段:192.168.255.0/24 子网掩码:255.255.255.0 下一跳:192.168.A.NAS(A地NAS)
七、B 地 Client 端部署
7.1 传输配置文件
把修正好的 clientB.ovpn 传到 B 地,放到 /opt/openvpn/。
7.2 启动 client 容器
# 把 clientB.ovpn 复制为 openvpn.conf
cp /opt/openvpn/clientB.ovpn /opt/openvpn/openvpn.conf
docker run -d \
--name openvpn-b \
--network host \
--cap-add NET_ADMIN \
--device /dev/net/tun \
-v /opt/openvpn:/etc/openvpn \
--restart unless-stopped \
kylemanna/openvpn
7.3 开启内核转发 + NAT
B 地也要开启 IP 转发,并添加 NAT 让 B 地局域网能访问 A 地:
sudo sysctl -w net.ipv4.ip_forward=1
# 让 B 地局域网(192.168.B.0/24)访问 A 地时,经隧道转发并做 NAT
sudo iptables -t nat -A POSTROUTING -s 192.168.B.0/24 -o tun0 -j MASQUERADE
注意(B 地 NAT 是必须的):B 地局域网设备访问 A 地时,源地址是
192.168.B.x,A 地不知道这个网段。B 地 NAS 必须做 SNAT/MASQUERADE,把源地址伪装成隧道 IP,A 地的回包才能正确返回。
八、双向互通:静态路由配置(核心)
这是实现”两地局域网互访”最关键的一步。OpenVPN 隧道建立后,NAS 相当于路由器,但两地的内网设备/路由器必须知道”去对端网段该走哪台 NAS”。
8.1 A 地路由器静态路由
登录 A 地路由器,添加:
目标网段:192.168.B.0/24
子网掩码:255.255.255.0
下一跳:192.168.A.NAS(A地NAS)
同时(如果移动端要访问 A 地,前面已提过):
目标网段:192.168.255.0/24
子网掩码:255.255.255.0
下一跳:192.168.A.NAS(A地NAS)
8.2 B 地路由器静态路由
登录 B 地路由器,添加:
目标网段:192.168.A.0/24
子网掩码:255.255.255.0
下一跳:192.168.B.NAS(B地NAS)
注意(为什么必须在路由器加路由):如果在每台电脑上加路由,主机一多就非常麻烦。推荐在路由器上配,一次性解决整个网段的访问。配置后,A 地任何设备访问
192.168.B.x时,流量会先到 A 地路由器,路由器根据静态路由转发给 A 地 NAS,NAS 再经隧道转发到 B 地。注意(B 地作为 client,server 端需要知道 B 地网段):如果你的 server 配置里没有推送 B 地网段的路由,或者 B 地有多个网段,可能需要用到 CCD(client-config-dir) 配合
iroute指令。当 B 地 client 连接时,在 A 地 server 的
ccd/clientB文件里写:iroute 192.168.B.0 255.255.255.0这告诉 A 地 server:”clientB 后面挂着 192.168.B.0/24 这个网段”,A 地 NAS 才知道要把发往 B 地的包转发进隧道。
但注意:如果你用
push "route ..."全局推送,且 B 地只有这一个网段,通常不需要 CCD。只有当你需要区分不同客户端访问不同网段时才需要。
九、验证与排错
9.1 验证隧道
A 地 server 日志确认 clientB 已连接:
docker logs openvpn-a | grep clientB
9.2 验证互通
① 从 A 地 NAS ping B 地内网设备:
ping 192.168.B.NAS
② 从 B 地 NAS ping A 地内网设备:
ping 192.168.A.NAS
③ traceroute 确认路径:
traceroute 192.168.B.NAS
正常路径应经过:A地NAS → 隧道 → B地NAS → B地设备。
9.3 常见排错
| 现象 | 可能原因 | 排查 |
|---|---|---|
| 隧道连不上 | 端口/协议不匹配 | 确认客户端端口 54443、tcp6 |
| 连上但访问不了内网 | 内核转发未开 / 静态路由缺失 | cat /proc/sys/net/ipv4/ip_forward 应为 1 |
| 单向通(内网→手机通,反向不通) | 回包路由缺失 | 路由器加静态路由 |
| 两端局域网都不通 | 路由器静态路由缺失 / NAT 缺失 | 检查两地路由器 + NAS 的 NAT |
十、架构总结
A地局域网 192.168.A.0/24 B地局域网 192.168.B.0/24
│ (A地路由器静态路由) │ (B地路由器静态路由)
│ B.0/24 → A.NAS │ A.0/24 → B.NAS
群晖NAS-A (A.NAS, server) 群晖NAS-B (B.NAS, client)
└────────── VPN隧道(192.168.255.0/24) ──────────┘
│
移动端 (client)
十一、安全与运维提醒
- 镜像停更风险:
kylemanna/openvpn停留在 2.4.9(2020 年),OpenSSL 较老。若追求新版本,可考虑:- 自行构建基于 OpenVPN 2.6 的镜像
- 或使用持续维护的社区镜像(需评估其 site-to-site + IPv6 + 自定义证书能力)
- 证书权限:收紧私钥权限:
sudo chmod 600 /opt/openvpn/pki/private/*.key sudo chmod 600 /opt/openvpn/pki/ta.key - IPv6 地址变化:公网 IPv6 可能是动态的,务必保证 DDNS 更新及时,否则客户端通过域名可能连不到。
- 防火墙:如果群晖启用了自带防火墙(控制面板 → 安全性 → 防火墙),需放行 54443 端口(TCP)和 tun0 接口的转发流量。
希望这篇教程能帮到有同样异地组网需求的朋友。如果你在部署中遇到问题,欢迎在评论区留言交流!
