5 min服务器

用 Docker 部署 UniVPN,让多个 VPN 连接共存

每套 UniVPN 独占一个容器,把网卡和路由隔离,再用不同本地端口访问各自内网。记录 Docker Desktop 下的部署方法。

目录5 节

我用的 Windows UniVPN 客户端一次只能连接一套 VPN。需要同时访问另一套内网时,又不想额外开一台 Windows 虚拟机,于是把另一套 UniVPN 放进 Docker Desktop 的 Linux 容器。

一套 VPN 一个容器,各自维护网卡和路由。 Windows 原有的 VPN 可以继续使用;还需要更多连接,就增加容器,用不同本地端口区分入口。

为什么能共存

普通 bridge 网络下,每个容器都有自己的网络命名空间。UniVPN 创建的 cnem_vnic 网卡和下发的内网路由留在对应容器里,因此两个容器可以使用同样的网卡名,内网 IP 重复也能按入口区分。

这里不要给 VPN 容器加 --network=host,也不要让它们共享另一个容器的网络命名空间,否则就失去了这层隔离。Docker 的 host 网络说明解释了共享网络命名空间的行为。

容器里的 VPN 不会自动接管 Windows 应用。我的做法是把需要访问的内网 SSH 端口转发出来:

映射到容器 2222经 VPN A 转发Windows 本地入口127.0.0.1:22221UniVPN 容器 A独立的 cnem_vnic 与路由内网 A 的 SSH 服务器10.80.0.11:22
映射到容器 2222经 VPN B 转发Windows 本地入口127.0.0.1:22222UniVPN 容器 B独立的 cnem_vnic 与路由内网 B 的 SSH 服务器10.80.0.11:22

两个容器的网卡和路由相互独立;即使内网 IP 相同,也可以通过不同本地端口访问。

准备镜像

前提是 Docker Desktop 已切换到 Linux 容器,且 Linux UniVPN 客户端适配自己的网关。下面使用第三方项目 jesusdf/huawei-vpn 封装的命令行客户端,镜像不是厂商官方发布的。示例针对账号密码认证,短信或动态验证码需要另行调整登录流程。

在一个空目录中新建 Dockerfile:

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 换行:

sh
#!/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.sh

socat 在容器内监听 2222,收到连接后,再沿容器里的 VPN 路由连接目标服务器。它只是转发 TCP,SSH 仍由目标服务器验证身份。

构建镜像,以下单行命令可直接在 PowerShell 中运行:

powershell
docker build -t univpn-forward:local .

启动两套 UniVPN

新建 vpn-a.env,填入第一套 VPN 的信息。以下全部为示例,需要替换:

ini
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 管理权限的人查看。

powershell
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:local

NET_ADMIN 和 /dev/net/tun 用于建立 VPN 网卡。两条命令都使用默认 bridge 网络,容器名、认证文件和宿主机端口分别独立。这里把入口绑定到 127.0.0.1,供本机使用;端口映射规则见 Docker 文档。

如果 Windows 已经连接第一套 VPN,只启动额外需要的那一个容器即可,不必把两套都迁进去。容器隔离也不能绕过 VPN 服务端的账号并发限制,同账号能否多端在线仍取决于网关策略。

Windows 应用怎么访问

SSH 工具直接填写对应入口:

环境地址端口登录身份
VPN A 内的服务器127.0.0.122221目标服务器的账号或密钥
VPN B 内的服务器127.0.0.122222目标服务器的账号或密钥

这一步用的是内网服务器的 SSH 账号,与 VPN 登录账号是两回事。也可以在 Windows 的 ~/.ssh/config 中写两个别名:

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 容器不会自动提供这种代理服务。

日常启停和排查

powershell
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 本身的开销。

评论