用Docker部署OpenVPN服务端,让几台机器之间能够互相访问,结果在“宿主机访问VPN客户端”这一步卡住了整整一个下午。
容器内能ping通,宿主机却死活不通。traceroute显示数据包到了容器门口就消失了……
今天就把完整的排查过程和解决方案分享出来,希望对遇到类似问题的你有所帮助。
一、我的网络拓扑
机器A :一台云服务器,通过Docker运行OpenVPN服务端
机器B :家里的一台开发机,安装了OpenVPN客户端
机器C :家里的NAS,同样安装了OpenVPN客户端
需求很简单:三台机器要能互相访问,就像在同一个局域网里一样。
B和C连接成功后,各自拿到了VPN虚拟IP:10.8.0.3和10.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段,回包就丢了。
三层问题叠加,难怪不通。
三、解决思路 既然找到了原因,解决方案也就清晰了:
在宿主机上加一条静态路由,指向VPN子网
在容器的iptables中放行宿主机与VPN子网之间的双向转发
对来自宿主机的流量做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" 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 NETWORK_NAME=$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{end}}' "$CONTAINER_NAME " ) 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 " 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 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 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 WatcherAfter =docker.serviceRequires =docker.service[Service] Type =simpleExecStart =/usr/local/bin/openvpn-route-watcher.shRestart =alwaysRestartSec =10 StandardOutput =journalStandardError =journalUser =rootGroup =root[Install] WantedBy =multi-user.target
启动服务并设置开机自启:
1 2 3 sudo systemctl daemon-reloadsudo systemctl enable openvpn-route-watcher.servicesudo 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 $ ip route show | grep 10.8.0.0 $ 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跨网络组网实战指南