用 Docker 部署 UniVPN,让多个 VPN 连接共存
每套 UniVPN 独占一个容器,把网卡和路由隔离,再用不同本地端口访问各自内网。记录 Docker Desktop 下的部署方法。
我用的 Windows UniVPN 客户端一次只能连接一套 VPN。需要同时访问另一套内网时,又不想额外开一台 Windows 虚拟机,于是把另一套 UniVPN 放进 Docker Desktop 的 Linux 容器。
一套 VPN 一个容器,各自维护网卡和路由。 Windows 原有的 VPN 可以继续使用;还需要更多连接,就增加容器,用不同本地端口区分入口。
为什么能共存
普通 bridge 网络下,每个容器都有自己的网络命名空间。UniVPN 创建的 cnem_vnic 网卡和下发的内网路由留在对应容器里,因此两个容器可以使用同样的网卡名,内网 IP 重复也能按入口区分。
这里不要给 VPN 容器加 --network=host,也不要让它们共享另一个容器的网络命名空间,否则就失去了这层隔离。Docker 的 host 网络说明解释了共享网络命名空间的行为。
容器里的 VPN 不会自动接管 Windows 应用。我的做法是把需要访问的内网 SSH 端口转发出来:
两个容器的网卡和路由相互独立;即使内网 IP 相同,也可以通过不同本地端口访问。
准备镜像
前提是 Docker Desktop 已切换到 Linux 容器,且 Linux UniVPN 客户端适配自己的网关。下面使用第三方项目 jesusdf/huawei-vpn 封装的命令行客户端,镜像不是厂商官方发布的。示例针对账号密码认证,短信或动态验证码需要另行调整登录流程。
在一个空目录中新建 Dockerfile:
FROM jesusdf/huawei-vpn@sha256:f3d98c6229071a2b14d1907a393ecac7743ad1b4d7cc96f38875290d574cd45b
RUN apt-get update \
&& apt-get install -y --no-install-recommends socat \
&& rm -rf /var/lib/apt/lists/* \
&& sed -i 's/-re "Connect Success"/-nocase -re "connect success"/; /^spawn /i log_user 0' /opt/univpn/connect.exp
ENV USER=root
COPY vpn-forward.sh /vpn-forward.sh
ENTRYPOINT ["/bin/sh", "/vpn-forward.sh"]这里固定镜像摘要,并兼容登录成功提示的大小写;同时关闭交互回显,避免账号密码被客户端回显进日志。
再新建 vpn-forward.sh,保存为 LF 换行:
#!/bin/sh
set -eu
: "${FORWARD_TARGET:?请设置内网目标 IP}"
socat TCP4-LISTEN:2222,bind=0.0.0.0,reuseaddr,fork \
"TCP4:${FORWARD_TARGET}:${FORWARD_PORT:-22},connect-timeout=10" &
exec /bin/sh /entrypoint.shsocat 在容器内监听 2222,收到连接后,再沿容器里的 VPN 路由连接目标服务器。它只是转发 TCP,SSH 仍由目标服务器验证身份。
构建镜像,以下单行命令可直接在 PowerShell 中运行:
docker build -t univpn-forward:local .启动两套 UniVPN
新建 vpn-a.env,填入第一套 VPN 的信息。以下全部为示例,需要替换:
VPN_GATEWAY=vpn-a.example.com
VPN_PORT=443
VPN_USERNAME=your-user-a
VPN_PASSWORD=replace-with-your-password
FORWARD_TARGET=10.80.0.11
FORWARD_PORT=22复制为 vpn-b.env,填写第二套的网关、端口、账号、密码和内网目标。两套 VPN 的 FORWARD_TARGET 可以相同,因为它们分别通过自己的隧道访问。网关端口按实际配置填写,不一定是 443。
这两个文件包含密码,只保存在本机,不提交到 Git。环境变量也能被有 Docker 管理权限的人查看。
docker run -d --name univpn-a --restart=no --cap-add=NET_ADMIN --device=/dev/net/tun --env-file vpn-a.env -p 127.0.0.1:22221:2222 univpn-forward:local
docker run -d --name univpn-b --restart=no --cap-add=NET_ADMIN --device=/dev/net/tun --env-file vpn-b.env -p 127.0.0.1:22222:2222 univpn-forward:localNET_ADMIN 和 /dev/net/tun 用于建立 VPN 网卡。两条命令都使用默认 bridge 网络,容器名、认证文件和宿主机端口分别独立。这里把入口绑定到 127.0.0.1,供本机使用;端口映射规则见 Docker 文档。
如果 Windows 已经连接第一套 VPN,只启动额外需要的那一个容器即可,不必把两套都迁进去。容器隔离也不能绕过 VPN 服务端的账号并发限制,同账号能否多端在线仍取决于网关策略。
Windows 应用怎么访问
SSH 工具直接填写对应入口:
| 环境 | 地址 | 端口 | 登录身份 |
|---|---|---|---|
| VPN A 内的服务器 | 127.0.0.1 | 22221 | 目标服务器的账号或密钥 |
| VPN B 内的服务器 | 127.0.0.1 | 22222 | 目标服务器的账号或密钥 |
这一步用的是内网服务器的 SSH 账号,与 VPN 登录账号是两回事。也可以在 Windows 的 ~/.ssh/config 中写两个别名:
Host vpn-a-node
HostName 127.0.0.1
Port 22221
User dev
HostKeyAlias vpn-a-node
Host vpn-b-node
HostName 127.0.0.1
Port 22222
User dev
HostKeyAlias vpn-b-node之后执行 ssh vpn-a-node 或 ssh vpn-b-node。VS Code Remote SSH、SFTP 等工具也能使用对应入口。第一次连接时核对目标主机指纹。
这个例子只转发一个 TCP 服务。需要数据库等其他端口,可以增加对应的 socat 监听和 Docker 端口映射。需要让浏览器按代理访问更多内网地址,则应另配 SOCKS/HTTP 代理;仅启动 UniVPN 容器不会自动提供这种代理服务。
日常启停和排查
docker logs --tail 30 univpn-a
docker exec univpn-a ip addr show cnem_vnic
docker exec univpn-a ip route get 10.80.0.11
docker stop univpn-a univpn-b
docker start univpn-a univpn-b先确认登录成功、网卡存在,再看目标 IP 是否走 VPN 路由,最后测试本地 SSH 入口。端口已经监听不代表 VPN 已连通。如果提示密码错误,先停止容器再核对认证信息;基础镜像内部有重连逻辑,--restart=no 只控制 Docker 的重启策略。
不使用时停止容器。需要限制单个 VPN 容器资源时,可以在创建命令中加 --memory=256m --cpus=0.5,再观察实际占用调整;这个限制不包含 Docker Desktop 和 WSL 本身的开销。
评论