在某些场景中,我们可能需要搭建一个链式 VPN 结构,即先让内网设备连接到自建的 WireGuard 服务器(如个人 VPS),再通过该服务器将所有流量转发到另一个 VPN(公司、学校或商业 VPN 服务)后才进入互联网。搭建这种「VPN 套 VPN」的网络架构,有以下明显优势:
-
强大的隐私保护:内网设备的数据流量首先通过自己搭建的 WireGuard 加密隧道传输至个人 VPS,再由 VPS 二次通过商业或公司 VPN 进行转发,最终从该 VPN 的 IP 出口访问互联网。这种方式彻底避免了使用 VPS 自身 IP 地址直接访问目标网络,大大降低了个人 IP 被跟踪或泄露的风险。
-
集中化便捷管理:所有客户端设备统一连接到单个 WireGuard 服务,免去逐一配置每台设备的麻烦。更重要的是,它还能巧妙绕过一些商业 VPN 服务商设定的账号设备数量限制,一次付费,所有设备共享。
-
灵活便捷,轻松切换:由于商业或公司 VPN 配置仅在自建 WireGuard 服务器端修改一次即可生效,下游客户端(手机、电脑等)无需单独调整。这使得更换或重新配置上游 VPN 时更加迅速和便捷,大大提高日常维护的效率。
-
打造专属虚拟局域网,实现跨设备互通:一旦所有设备连接到自建的 WireGuard VPN 服务器后,将自动获得统一的 VPN 虚拟局域网 IP 地址(例如:10.8.0.0/16)。处于该网络中的设备能够直接互相通信,无需额外设置。
这种便捷性意味着无论你身处何处,都能轻松实现:
- 远程 SSH 登录:随时访问家中或办公室的服务器。
- 私有 NAS 媒体访问:在外轻松访问家中 NAS 存储的媒体文件。
- 远程办公:随时随地安全访问公司内部网络资源。
方案
经过本人的探索和实践,我逐步摸索出了自己的一套链式 VPN 方案。总体来看,我的方案完全基于 Docker 部署,所用到的核心组件有:
- gluetun:一个 Docker 镜像,内置 OpenVPN/ WireGuard 客户端,可连接到各大 VPN 服务提供商。本例中我们通过配置在 Docker 网络中提供 VPN 网关功能。
- wg-easy:一个简化的 WireGuard Docker 镜像,内置 Web 界面,便于创建/管理 WireGuard 配置文件。下游设备通过 WireGuard 客户端连接到该服务,所有流量将被转发到 gluetun 容器,由 gluetun 再发给上游 VPN。
该方案的优势在于完全容器化部署,避免污染服务器/VPS 的环境。然后就是比较稳定,个人日常使用未发现明显延迟、卡顿、断流问题,而且性能良好(在个人家用带宽下测试)。另外,该方案现在能够支持 IPv6,即使设备身处 IPv4 的网络环境,也能通过链路实现对 IPv6 资源的代理访问。
在网络拓扑上,整个系统仅需要一个 Docker 网桥(bridge):
- vpn:用于 gluetun 与 wg-easy 之间的“VPN 网络”, gluetun 在其中作为网关,wg-easy 发出的流量都将进入该网络并通过 gluetun 转发到上游 VPN。
[客户端设备]
↓ WireGuard(连入)
[wg-easy 容器] ——→ Docker 网络 vpn ——→ [gluetun 容器] ——→ 上游 VPN 提供商
其中,wg-easy 监听 UDP 51820,客户端(自己的手机电脑)连接后,wg-easy (在容器中)会将流量通过 Docker 网络 vpn 发往 gluetun 容器,gluetun 再使用 WireGuard/OpenVPN 连接上游 VPN。所有下游流量最终从 gluetun 的 tun0 接口出去,达到链式 VPN 的效果。
环境与前置条件
- 一台支持 Docker 与 Docker Compose 的 Linux 服务器/VPS(内核支持 WireGuard 和 iptables/NFT)。建议内核版本 ≥ 5.10。
- 已安装 Docker 和 Docker Compose v2+。
- 拥有一个支持 OpenVPN/WireGuard 的商业 VPN 服务商账号。
- 确保宿主机允许 CAP_NET_ADMIN 与 SYS_MODULE 权限,能加载 WireGuard 内核模块(可通过 lsmod | grep wireguard 验证)。
创建自定义网络
在正式部署前,先在宿主机上创建网桥
docker network create \
--driver bridge \
--subnet 172.32.0.0/16 \
--subnet fd01:beee:beee::/48 \
vpn
说明:如果已有网络与子网冲突,请根据实际情况调整子网段。本示例假设 Docker 网桥支持 IPv6(用于示例 IPv6 转发)。若不使用 IPv6,可省略相关配置或将 disable_ipv6=1。
Docker Compose 配置详解
version: "3"
services:
gluetun:
image: qmcgaw/gluetun
cap_add:
- NET_ADMIN
- SYS_MODULE
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
- VPN_TYPE=${VPN_TYPE}
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_PRESHARED_KEY=${WIREGUARD_PRESHARED_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
- SERVER_REGIONS=${SERVER_REGIONS}
configs:
- source: post-rules.txt
target: /iptables/post-rules.txt
volumes:
- /lib/modules:/lib/modules:ro
- /data/gluetun-ws:/gluetun
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv6.conf.default.forwarding=1
networks:
vpn:
ipv4_address: 172.32.0.4
ipv6_address: "fd01:beee:beee::4"
restart: unless-stopped
wg-easy:
image: ghcr.io/wg-easy/wg-easy:15
ports:
- "51820:51820/udp"
networks:
vpn:
ipv4_address: 172.32.0.8
ipv6_address: "fd01:beee:beee::8"
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv6.conf.default.forwarding=1
volumes:
- /lib/modules:/lib/modules:ro
- /data/wg-easy:/etc/wireguard
restart: unless-stopped
networks:
vpn:
external: true
configs:
post-rules.txt:
content: |
iptables -A FORWARD -i eth0 -o tun0 -j ACCEPT
iptables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
ip6tables -I INPUT -p icmpv6 -j ACCEPT
ip6tables -I OUTPUT -p icmpv6 -j ACCEPT
ip6tables -A FORWARD -i eth0 -o tun0 -j ACCEPT
ip6tables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
ip6tables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
接下来,逐项拆解说明:
image: qmcgaw/gluetun
:Lightweight swiss-army-knife-like VPN client to multiple VPN service providers,能够使用在容器环境提供 VPN 服务,给其他有需要的容器提供 VPN 连接。官方维护的 gluetun 镜像,支持多种 VPN 协议与服务商。cap_add: [NET_ADMIN, SYS_MODULE]
:需要 NET_ADMIN、SYS_MODULE 权限,以及开启 IP 转发等设置(wg-easy 与 gluetun 一致),赋予容器网络管理与加载内核模块的权限,以便创建 TUN 设备及配置路由,且保证 WireGuard 隧道与路由功能正常。devices: ["/dev/net/tun:/dev/net/tun"]
:挂载宿主机的 TUN 设备到容器,使 gluetun 能创建虚拟网卡。image: ghcr.io/wg-easy/wg-easy:15
:wg-easy 的官方镜像(版本号可根据需要调整)。"51820:51820/udp"
:将宿主机 UDP 51820 端口映射到容器,以便下游设备通过宿主机或直接通过 Docker 网络来连接 WireGuard。如果你需要对外暴露 WG-Easy 的 Web 管理界面,那你需要额外加入"51821:51821/tcp"
。
环境变量中的具体参数可参考 gluetun 官方文档。
需要注意的是上面提到的这两个配置,用于固定容器在 Docker 网桥中的 IP 地址,后续配置 WG-Easy 的路由表和防火墙规则的时候会频繁引用。如果需要替换,那么需要注意修改后续配置中的相应值。
networks:
vpn:
ipv4_address: 172.32.0.4
ipv6_address: "fd01:beee:beee::4"
networks:
vpn:
ipv4_address: 172.32.0.8
ipv6_address: "fd01:beee:beee::8"
gluetun 额外启动脚本post-rules.txt
加入的防火墙规则的解释如下:
# 1. IPv4 转发规则 (允许 eth0 <-> tun0 双向转发)
iptables -A FORWARD -i eth0 -o tun0 -j ACCEPT
iptables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# 2. IPv4 NAT 伪装(所有走 tun0 的包都做 SNAT)
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
# 3. IPv6 ICMP 放行(保证 Neighbor Discovery Protocol 等协议正常)
ip6tables -I INPUT -p icmpv6 -j ACCEPT
ip6tables -I OUTPUT -p icmpv6 -j ACCEPT
# 4. IPv6 转发规则 (允许 eth0 <-> tun0 双向转发)
ip6tables -A FORWARD -i eth0 -o tun0 -j ACCEPT
ip6tables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# 5. IPv6 NAT 伪装(所有走 tun0 的 IPv6 包都做 SNAT)
ip6tables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
通过以上规则,下游(来自 wg-easy 的流量)在 gluetun 容器内部可以顺利地被转发到 tun0 接口,进入上游 VPN 通道;上游返回的数据包同样能回到 Docker 网络,并最终到达 wg-easy 再返回客户端。这就实现了一个 VPN 网关的功能。
WG-Easy “Post Up” 脚本说明
除了 gluetun 的网络规则,还需在 wg-easy 里设置“Post Up”和“Post Down”脚本,用于在 WireGuard 隧道建立后动态添加路由与 ip rule,让下游客户端的流量都走 gluetun。本例针对 wg-easy v15 版本,因为在 v15 版本后 wg-easy 才支持 IPv6。如果想知道 v14 版本的配置,可以看我在GitHub 发布的帖子。
在目前 v15.0.0 版本的实际的操作中,需要把我给出的示例转换为单行(用分号分隔每一条命令)填写到截图所示的输入框中。或者,你可以把脚本写成文件,添加到 wg-easy 中的目录中,然后引用。然后点击Save
,耐心等待 wg-easy 弹出配置成功的提示框。必要时,可以在配置后重启 wg-easy 容器。
配置示例
以下为完整示例以及解释(注意需在 wg-easy 的 WEB 管理页面配置):
# WG-Easy Post Up 脚本(示例)
VPN=$(ifconfig | grep -B1 172.32.0.8 | grep -o "^\w*")
# 先默认拒绝所有转发流量
iptables -P FORWARD DROP
ip6tables -P FORWARD DROP
# 开放 WireGuard UDP 端口({{port}} 会被替换为 WireGuard 端口号,一般为 51820)
iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT
ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT
# 允许本地与 WireGuard 隧道之间的转发
iptables -A FORWARD -i wg0 -j ACCEPT
iptables -A FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -s {{ipv4Cidr}} -d {{ipv4Cidr}} -i wg0 -o wg0 -j ACCEPT
ip6tables -A FORWARD -i wg0 -j ACCEPT
ip6tables -A FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT
ip6tables -A FORWARD -s {{ipv6Cidr}} -d {{ipv6Cidr}} -i wg0 -o wg0 -j ACCEPT
# 配置 IPv4 路由表(表 200 用作下游流量专用表)
iptables -t nat -A POSTROUTING -s {{ipv4Cidr}} -o $VPN -j MASQUERADE
# 添加 ip rule,让源自 {{ipv4Cidr}} 的流量走表 200
ip rule add from {{ipv4Cidr}} table 200
ip route add default via 172.32.0.4 dev $VPN table 200
ip route add 172.32.0.0/16 via 172.32.0.1 dev $VPN table 200
ip route add {{ipv4Cidr}} dev wg0 table 200
# 配置 IPv6 路由表(同理,使用表 200)
ip -6 rule add from {{ipv6Cidr}} table 200
ip -6 route add default via fd01:beee:beee::4 dev $VPN table 200
ip -6 route add fd01:beee:beee::/48 via fd01:beee:beee::1 dev $VPN table 200
ip -6 route add {{ipv6Cidr}} dev wg0 table 200
下面三个变量是 wg-easy 的模板变量,会由 wg-easy 自动填充,但用户要确保参数有效:
{{port}}
:WireGuard 监听的端口,一般与 Docker 映射的端口(51820)一致。{{ipv4Cidr}}
:下游客户端网段,比如10.8.0.0/24
。由 wg-easy 分配给 Peer。{{ipv6Cidr}}
:下游客户端的 IPv6 段,比如fdcc:ad94:bacf:61a4::cafe::/64
。
这里提供用于 PostDown 的脚本(简单将-A
替换为-D
,add
变成del
):
VPN=$(ifconfig | grep -B1 172.32.0.8 | grep -o "^\w*")
iptables -P FORWARD DROP
ip6tables -P FORWARD DROP
iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT
ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT
iptables -D FORWARD -i wg0 -j ACCEPT
iptables -D FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -D FORWARD -s {{ipv4Cidr}} -d {{ipv4Cidr}} -i wg0 -o wg0 -j ACCEPT
ip6tables -D FORWARD -i wg0 -j ACCEPT
ip6tables -D FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT
ip6tables -D FORWARD -s {{ipv6Cidr}} -d {{ipv6Cidr}} -i wg0 -o wg0 -j ACCEPT
iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -o $VPN -j MASQUERADE
ip rule del from {{ipv4Cidr}} table 200
ip route del default via 172.32.0.4 dev $VPN table 200
ip route del 172.32.0.0/16 via 172.32.0.1 dev $VPN table 200
ip route del {{ipv4Cidr}} dev wg0 table 200
ip -6 rule del from {{ipv6Cidr}} table 200
ip -6 route del default via fd01:beee:beee::4 dev $VPN table 200
ip -6 route del fd01:beee:beee::/48 via fd01:beee:beee::1 dev $VPN table 200
ip -6 route del {{ipv6Cidr}} dev wg0 table 200
使用多张Docker网桥
在我的环境中,wg-easy 会连接到多张Docker网桥,也就是除了 vpn 之外,还有其他服务专用的网桥。如果你需要为 wg-easy 再接入新的桥接网络,建议保留上面配置中 $VPN 变量而不是直接wg-easy提供的 {{device}}模板变量。因为通过 $VPN 变量可以自动绑定对应网段对应的网卡,确保路由和防火墙规则只作用在指定的接口上,而不会误伤其他网络服务。对于第二个Docker网桥,你可以在PostUp中添加如下命令:
VPN_IF=$(ifconfig | grep -B1 172.32.0.8 | grep -o "^\w*")
需要注意将172.32.0.8
替换为 对应Docker网桥上的 wg-easy 容器的 IP,这个需要你固定 wg-easy在该Docker 网桥中的IP。或者,你可以调整一下这个正则匹配,让grep筛选网段而非IP地址。这样,$VPN_IF
将解析为对应的网段正在使用的网络接口。然后,请同时更新路由表和防火墙规则,保证 各个服务网络之间的流量隔离与正常转发:
iptables -t nat -A POSTROUTING -s <NETWORK_CIDR> -o <NETWORK_INTERFACE> -j MASQUERADE
ip6tables -t nat -A POSTROUTING -s <NETWORK_CIDR> -o <NETWORK_INTERFACE> -j MASQUERADE # 如果此桥有 IPv6
ip route add <NETWORK_CIDR> via <NETWORK_GATEWAY> dev <NETWORK_INTERFACE> table 200
ip -6 route add <NETWORK_CIDR> via <NETWORK_GATEWAY> dev <NETWORK_INTERFACE> table 200 # 如果此桥有 IPv6
对于需要给多个容器提供VPN服务的但又不希望直接给wg-easy添加多个Docker网桥的读者。这里提供一种更精细可控的做法:将那些需要走 VPN 的其他服务分别绑定到独立的 WireGuard 客户端容器(如 linuxserver/wireguard 或类似 Gluetun 的镜像)中,然后通过wg-easy(或类似管理界面)为每个服务创建单独的 WireGuard 账户。最后将对应的配置导入到 服务对应的WireGuard 客户端容器中。这样做的好处在于:
- 每个服务拥有独立的 WireGuard 配置文件,流量隔离更彻底;
- 可以在 wg-easy 中更直观地监控、限制单个服务的网络访问;
- 不再需要编写诸如
iptables -A FORWARD -i eth+ -o tun0
这类对某个网络接口一概放行的规则,而是仅对特定容器进行信任授权。
这种方式能够为每个容器提供更细粒度的权限控制,例如实时流量监控、随时撤销某个服务的 VPN 权限等等。不过,如果运行多个 WireGuard 客户端容器会带来额外的 CPU 和内存消耗。
部署
创建并加载环境变量
在与 docker-compose.yml 同目录下,创建一个 .env 文件,写入(参考前面给出的 gluetun 文档):
VPN_SERVICE_PROVIDER=XXX
VPN_TYPE=XXXX
WIREGUARD_PRIVATE_KEY=YOUR_PRIVATE_KEY
WIREGUARD_PRESHARED_KEY=YOUR_PSK
WIREGUARD_ADDRESSES=XXX
SERVER_REGIONS=XXX
确认 Docker 网络存在
如前文所述,确保 名为 vpn 的自定义网络已创建。
启动容器
执行下面的命令,观察 wg-easy 与 gluetun-ws 容器启动日志,确认 WireGuard 客户端与服务端都正常建立。
$ docker-compose up -d
添加 WireGuard 客户端
- 通过浏览器访问
http://<WG-Easy容器IP>:51821
(取决于 wg-easy 配置),进入 wg-easy 的 Web 界面。或者你可以在 Docker Compose 中加入对于 51821 端口的 TCP 映射,然后用http://<本机IP>:51821
访问 Web 管理界面。或者用 Nginx 进行反向代理也行。 - 在 Admin Panel 的 Hooks 栏,填写上面给出的的 Post Up 和 Post Down 配置,并保存。这一步特别关键。
- 创建一个新的 Peer(填写 Peer 名称),wg-easy 会自动生成对应的配置文件,带有 AllowedIPs = 0.0.0.0/0, ::/0,并下载 .conf 文件。
- 在本地设备(Windows/Mac/Linux/手机)上导入该配置,启动 WireGuard,进行测试。
验证 VPN2VPN 通道
- 在客户端连上 WireGuard 后,打开 https://ipleak.net 或 https://ifconfig.co,查看出口 IP。
- 若显示的是上游商业 VPN 的 IP,则说明链式 VPN 通道正常。
- 可在 gluetun 容器中执行 curl ifconfig.co,应该能看到商业 VPN 给的公网 IP;在 wg-easy 容器中执行同样操作,也能看到相同的出口 IP(走上游 VPN)。
检查流量路由
- 在 wg-easy 容器中,可执行 ip rule show / ip route show table 200,确认路由表 200 已生效,默认路由指向 172.32.0.4(gluetun)。
- 在 gluetun 容器中,执行 iptables -t nat -L -nv,可看到对应的 MASQUERADE 规则。
- 如需排查,可在两容器内分别使用 tcpdump -i 对应接口进行抓包,确认流量状态。
注意事项
DNS 泄露
本例如果不做额外的 DNS 安全措施,就存在 DNS 泄露的风险:当容器或客户端直接使用宿主机或公共 DNS 时,DNS 查询可能绕过 VPN 通道而直连上游,暴露真实 IP 和访问的网站记录;其实gluetun在自己内部会创建一个DNS服务器,然后强制所有的DNS流量走这个服务,来避免使用本地DNS造成DNS泄露。但 gluetun 默认只在容器内部监听 DNS 访问,外部容器或主机无法访问。这也让wg-easy无法直接使用gluetun内部的DNS服务,所以我们不能简单把wg-easy中的DNS流量直接转发到gluetun。
为了避免这些问题,通常需要部署一个独立的 DNS 服务(如 AdGuard Home 或 Pi-hole),并将所有 DNS 查询都通过该服务处理,通过 VPN 出口进行上游解析,这样既能保证 DNS 查询也走 VPN,又能实现广告拦截和恶意域名过滤。
部署 AdGuard Home 或类似的 DNS 服务
我们可以在同一个 Docker 网络(例如 vpn 网络)中运行一个独立的 AdGuard Home 容器。AdGuard Home 会同时监听 UDP/TCP 53 端口,并且具有更强的拦截广告、恶意域名过滤等功能。举例来说,你可以在 docker-compose.yml 中加入类似以下服务(仅示范):
adguard:
image: adguard/adguardhome
restart: unless-stopped
networks:
vpn:
ipv4_address: 172.32.0.16
ipv6_address: "fd01:beee:beee::16"
ports:
- "53:53/udp"
- "53:53/tcp"
- "3000:3000/tcp" # Web 界面端口
volumes:
- /data/adguard/workdir:/opt/adguardhome/work
- /data/adguard/conf:/opt/adguardhome/conf
这样,AdGuard Home 会暴露在宿主机的 53 端口,同时在 Docker vpn 网络内分配一个 IP(比如 172.32.0.5)。后续我们就能够从 wg-easy、客户机甚至其他容器直接访问这个 AdGuard 容器。你也可以选择不暴露53端口,仅仅通过vpn这个网桥提供DNS服务。
访问 AdGuard 的 Web 界面(通常在 http://<Host-IP>:3000
),确认上游 DNS 设置正确,广告拦截等策略正常。然后你需要进行测试,最好在WG-Easy容器中执行nslookup hostname 172.32.0.16
,测试 wg-easy到AdGuard Home的DNS链路是否是通的。
修改DNS配置
在Adgaurd部署、配置并测试成功后,我们需要调整在 wg-easy 生成的客户端DNS配置,因为 wg-easy默认会把 DNS 服务器设置为1.1.1.1
与2606:4700:4700::111
。这一步需要在 wg-easy的Admin Panel把 DNS 改为 AdGuard 容器的 IP:172.32.0.16
与fd01:beee:beee::16
。
需要注意:已经添加的设备的DNS配置并不会跟随Admin Panel中的DNS配置自动进行修改,需要你手动更新设备中WireGuard里的DNS配置,或者在WG-Easy中对这个设备单独进行调整然后在重新下载并应用配置文件到设备。
最后,为了严格限制WG-Easy对外的DNS访问,杜绝DNS泄露。我们可以在PostUp Hook脚本中补充如下指令:
# 允许向 adgaurd 的 DNS 端口发包
iptables -A OUTPUT -o $VPN -d 172.32.0.16 -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -o $VPN -d 172.32.0.16 -p tcp --dport 53 -j ACCEPT
ip6tables -A OUTPUT -o $VPN -d fd01:beee:beee::16 -p udp --dport 53 -j ACCEPT
ip6tables -A OUTPUT -o $VPN -d fd01:beee:beee::16 -p tcp --dport 53 -j ACCEPT
# 拒绝其它网络栈的 DNS 查询,避免漏出到宿主机
iptables -A OUTPUT -p tcp --dport 53 -j REJECT
iptables -A OUTPUT -p udp --dport 53 -j REJECT
ip6tables -A OUTPUT -p tcp --dport 53 -j REJECT
ip6tables -A OUTPUT -p udp --dport 53 -j REJECT
其对应的PostDown脚本如下:
iptables -D OUTPUT -o $VPN -d 172.32.0.16 -p udp --dport 53 -j ACCEPT
iptables -D OUTPUT -o $VPN -d 172.32.0.16 -p tcp --dport 53 -j ACCEPT
ip6tables -D OUTPUT -o $VPN -d fd01:beee:beee::16 -p udp --dport 53 -j ACCEPT
ip6tables -D OUTPUT -o $VPN -d fd01:beee:beee::16 -p tcp --dport 53 -j ACCEPT
iptables -D OUTPUT -p tcp --dport 53 -j REJECT
iptables -D OUTPUT -p udp --dport 53 -j REJECT
ip6tables -D OUTPUT -p tcp --dport 53 -j REJECT
ip6tables -D OUTPUT -p udp --dport 53 -j REJECT
IPv6 支持
- 如不使用 IPv6,可在 docker-compose.yml 中将 ipv6_address、ip6tables 规则等相关配置移除,并在创建网络时不启用 IPv6。
- 如果使用 IPv6,需要确保宿主机与 VPN 服务商支持 IPv6 隧道。
性能与资源
WireGuard 本身对 CPU 要求较高,特别在满速传输时,需要较好的硬件支持。
Multi-Hop(多跳)
本架构即为双跳(下游 → wg-easy → gluetun → 上游 VPN → 互联网)。如果想再进一步配置多跳,可在上游 VPN 选择不同区域,或再叠加一个 VPN 容器。但需注意性能下降与维护复杂度上升。
防火墙与安全
本例中,wg-easy 容器默认仅开放 UDP 51820 端口,如需在公网访问,请在宿主机防火墙(iptables/nftables)开放对应端口,同时只允许可信 IP 访问,以减少暴露面。
PostDown 脚本
为避免卸载后遗留规则,务必为 wg-easy 的 PostDown 添加相应的 iptables -D、ip rule del、ip route del 等操作。否则重启时可能产生重复路由或冲突。
网络排查示例
当发现客户端无法访问 LAN 或 Internet 时,我在这里提供一些常用步骤快速定位问题的方案。这个方案也是我平时常用的。我相信通过这种由粗到细的思路,就能有效找出大多数网络连接故障的根源。
下面给出测试方案的具体的步骤,但这里仅仅展示在IPv4下的排查指令,对于IPv6,读者可以很容易通过对指令进行微调来排查。
分阶段 Ping 测试(本地)
ping 10.8.0.1
:确认 VPN 隧道是否建立。ping 172.32.0.8
:检查客户端是否能到达 wg-easy 容器(Docker 桥接网络)。ping 172.32.0.4
:验证桥接网络到 gluetun 容器的连通性。ping 8.8.8.8
:测试 gluetun 是否能正常访问外网。
如果某步失败,就有针对性地检查对应的路由或防火墙规则。
DNS 解析验证(本地)
通过上面的测试后,说明IP层面链路没有问题,接下来我们就继续排查DNS的问题。
nslookup example.com
:通过 默认DNS服务器 。nslookup example.com 172.32.0.16
:通过 adgaurd 。nslookup example.com 8.8.8.8
:测试公共 DNS 是否可用。
若无法得到正确响应,说明 DNS 请求没有到达预期的服务器。
进入容器内部排查
如果DNS也没有问题,那可能是一些更深层次的路由表或者防火墙规则的冲突问题。我们可以分别进入wg-easy或者gluetun容器进行调试:
docker exec -it <container-name/id-of-wg-easy> /bin/bash
docker exec -it <container-name/id-of-gluetun> /bin/sh
在对应容器内执行:
ip rule show # 查看路由规则是否正确
ip route show table 200 # 验证路由表 200
ping -c 2 8.8.8.8 # 测试容器出网
ping -c 2 example.com # 测试容器内 DNS
iptables-saves # 检查防火墙规则
ip route # 验证默认路由表
更高阶方法:tcpdump、路由表与防火墙检查
如果上述简单手段仍无法定位,那就说明存在一些更深的路由表或者防火墙配置冲突问题,这个时候可使用 tcpdump -i <接口>
捕获数据包,观察 DNS(UDP/TCP 53)或其他流量是否到达容器或被丢弃。然后,请仔细检查宿主机和容器内的路由表(ip rule show、ip route show)是否存在重叠或冲突项;确认 ip rule 的优先级和表表项是否生效。再次审视 iptables/ip6tables 规则,尤其是 FORWARD、OUTPUT、nat 表,确认是否有误拒绝或重复的规则导致流量被拦截。
更新造成的问题
需要注意,你遇到的问题也很有可能是某一个容器的镜像更新后造成的问题,这个时候需要检查是否最近有自动更新,然后尝试通过版本降级该解决。此时如果遇到的问题消失,那么接下来,在问题最终解决之前,可以先固定旧的镜像版本确保服务可用。