用Docker部署OpenVPN服务端,让几台机器之间能够互相访问,结果在“宿主机访问VPN客户端”这一步卡住了整整一个下午。

容器内能ping通,宿主机却死活不通。traceroute显示数据包到了容器门口就消失了……

今天就把完整的排查过程和解决方案分享出来,希望对遇到类似问题的你有所帮助。

一、我的网络拓扑

  • 机器A:一台云服务器,通过Docker运行OpenVPN服务端
  • 机器B:家里的一台开发机,安装了OpenVPN客户端
  • 机器C:家里的NAS,同样安装了OpenVPN客户端

需求很简单:三台机器要能互相访问,就像在同一个局域网里一样。

B和C连接成功后,各自拿到了VPN虚拟IP:10.8.0.310.8.0.4。从容器内部ping 10.8.0.3是通的,但从宿主机A上ping,却一直超时。

二、问题到底出在哪?

经过一番排查,发现问题出在三个环节:

第一层:宿主机没有到VPN子网的路由
宿主机根本不知道10.8.0.0/24这个网段该往哪儿走。

第二层:容器的防火墙挡住了转发
就算加了路由,数据包到了容器门口,也被iptables的FORWARD链拦截了。

第三层:源地址转换(SNAT)缺失
这个最隐蔽。数据包从宿主机发出时,源IP是宿主机的IP(比如172.20.0.1)。客户端B收到后回复,目标地址是172.20.0.1,但B根本不认识这个IP段,回包就丢了。

三层问题叠加,难怪不通。

三、解决思路

既然找到了原因,解决方案也就清晰了:

  1. 在宿主机上加一条静态路由,指向VPN子网
  2. 在容器的iptables中放行宿主机与VPN子网之间的双向转发
  3. 对来自宿主机的流量做SNAT,让回包能找到回家的路

但还有一个问题:每次容器重启,这些配置都会丢失,这里分享一套自动化的方案:容器启动时,自动完成所有配置。

四、最终方案:自动配置脚本 + systemd监控

核心脚本

我写了一个脚本/usr/local/bin/update-openvpn-route.sh,用于添加静态路由以及设置容器内的网络转发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash

CONTAINER_NAME="openvpn"
ROUTE_NET="10.8.0.0/24"

# 1. 获取容器 IP 和所在网络名称
CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CONTAINER_NAME" 2>/dev/null)
if [ -z "$CONTAINER_IP" ]; then
echo "Container $CONTAINER_NAME not running or no IP found"
exit 1
fi

# 获取容器所在的 Docker 网络名称(例如 bridge, host, 或自定义的 br-xxx)
NETWORK_NAME=$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{end}}' "$CONTAINER_NAME")

# 2. 动态获取该网络的子网段(用于 iptables 的 -s 源地址匹配)
# 例如 172.20.0.0/16
HOST_SUBNET=$(docker network inspect "$NETWORK_NAME" -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}')

if [ -z "$HOST_SUBNET" ]; then
echo "Warning: Could not detect subnet for network $NETWORK_NAME, falling back to 0.0.0.0/0"
HOST_SUBNET="0.0.0.0/0"
fi

echo "Container IP: $CONTAINER_IP, Network: $NETWORK_NAME, Subnet: $HOST_SUBNET"

# 3. 添加宿主机路由(去掉 dev 指定,让内核自动选择网卡)
if ! ip route show "$ROUTE_NET" | grep -q "$CONTAINER_IP"; then
ip route add "$ROUTE_NET" via "$CONTAINER_IP"
echo "Route added: $ROUTE_NET via $CONTAINER_IP"
else
echo "Route already exists"
fi

# 4. 容器内 iptables:放行该网络子网到 VPN 的转发
docker exec "$CONTAINER_NAME" iptables -C FORWARD -s "$HOST_SUBNET" -d "$ROUTE_NET" -j ACCEPT 2>/dev/null
if [ $? -ne 0 ]; then
docker exec "$CONTAINER_NAME" iptables -I FORWARD 1 -s "$HOST_SUBNET" -d "$ROUTE_NET" -j ACCEPT
echo "Added FORWARD: $HOST_SUBNET -> $ROUTE_NET"
fi

docker exec "$CONTAINER_NAME" iptables -C FORWARD -s "$ROUTE_NET" -d "$HOST_SUBNET" -j ACCEPT 2>/dev/null
if [ $? -ne 0 ]; then
docker exec "$CONTAINER_NAME" iptables -I FORWARD 2 -s "$ROUTE_NET" -d "$HOST_SUBNET" -j ACCEPT
echo "Added FORWARD: $ROUTE_NET -> $HOST_SUBNET"
fi


# 添加 SNAT 规则(如果不存在)
docker exec "$CONTAINER_NAME" iptables -t nat -C POSTROUTING -s "$HOST_SUBNET" -d "$ROUTE_NET" -j MASQUERADE 2>/dev/null
if [ $? -ne 0 ]; then
docker exec "$CONTAINER_NAME" iptables -t nat -I POSTROUTING 1 -s "$HOST_SUBNET" -d "$ROUTE_NET" -j MASQUERADE
echo "Added MASQUERADE: $HOST_SUBNET -> $ROUTE_NET"
fi

echo "All rules configured successfully."

赋予执行权限:

1
sudo chmod +x /usr/local/bin/update-openvpn-route.sh

如何触发自动执行?

由于脚本需要在容器启动时自动触发,我们编写一个持续监听 Docker 事件的守护进程 /usr/local/bin/openvpn-route-watcher.sh

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
CONTAINER_NAME="openvpn"
ROUTE_SCRIPT="/usr/local/bin/update-openvpn-route.sh"

while true; do
docker events --filter "event=start" --filter "container=$CONTAINER_NAME" --format '{{.Actor.ID}}' | while read id; do
$ROUTE_SCRIPT
done
echo "docker events exited, restarting in 5 seconds..." >&2
sleep 5
done

赋予执行权限:

1
sudo chmod +x /usr/local/bin/openvpn-route-watcher.sh

创建 /etc/systemd/system/openvpn-route-watcher.service,配置systemd服务实现开机自启

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Unit]
Description=OpenVPN Route Update Watcher
After=docker.service
Requires=docker.service

[Service]
Type=simple
ExecStart=/usr/local/bin/openvpn-route-watcher.sh
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
User=root
Group=root

[Install]
WantedBy=multi-user.target

启动服务并设置开机自启:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable openvpn-route-watcher.service
sudo systemctl start openvpn-route-watcher.service

五、最终效果

配置完成后,我做了以下验证,宿主机ping VPN客户端以及端口连通性测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 重启容器
$ docker restart openvpn

# 查看脚本执行日志
$ sudo journalctl -u openvpn-route-watcher.service -f
# 应能看到类似输出:All rules configured successfully.

## 检查宿主机路由
$ ip route show | grep 10.8.0.0
# 输出示例:10.8.0.0/24 via 172.20.0.2 dev br-xxx

# 测试连通性
$ ping 10.8.0.3
64 bytes from 10.8.0.3: icmp_seq=1 ttl=64 time=0.068 ms

$ nc -vz 10.8.0.3 27500
Connection to 10.8.0.3 27500 port [tcp/*] succeeded!

三台机器互访:

  • ✅ A能访问B和C
  • ✅ B能访问A和C
  • ✅ C能访问A和B

六、总结与避坑指南

几个关键点

1. SNAT是必须的,不能省略
很多人只做了路由和FORWARD放行,发现还是不通,就是因为少了这一步。没有SNAT,回包找不到回家的路。

2. 使用-C检查规则,避免重复插入
iptables -C检查规则是否存在,再决定是否插入,这是幂等性的关键。

3. docker events的格式化参数
如果你的Docker版本较老,{{.ID}}可能不生效,需要改为{{.Actor.ID}}

实测命令速查

操作 命令
查看宿主机路由 ip route show
查看FORWARD规则 docker exec openvpn iptables -L FORWARD -n -v
查看NAT规则 docker exec openvpn iptables -t nat -L POSTROUTING -n -v
跟踪数据包路径 traceroute -n 10.8.0.3
测试端口连通 nc -vz 10.8.0.3 27500
容器内抓包 docker exec openvpn tcpdump -i tun0 icmp
查看服务日志 sudo journalctl -u openvpn-route-watcher.service -f

写在最后

折腾了一下午,问题解决了,也学到了一波Docker网络 + iptables的知识。
如果你也遇到了类似的问题,希望这篇文章能帮你少踩一些坑,如果更好的方案,欢迎在评论区交流讨论!

往期文章回顾

Docker部署OpenVPN:企业级VPN服务搭建
只有1个公网IP怎么玩?OpenVPN跨网络组网实战指南