跳到正文

2026-11-14 7 分钟阅读

在 macOS 上用 Docker 运行 EasyConnect,并用 Clash TUN 做分流

深信服没有提供 Apple Silicon 原生的 EasyConnect,本文用 Docker 跑图形版客户端,再借 Clash Verge Rev 的 TUN 模式和全局扩展脚本,把内网 IP、内网域名和无后缀短名精准分流进 VPN。

macOS 上装 EasyConnect 一直是件别扭事:深信服只提供 x86_64 的安装包,Apple Silicon 机器要么装 Rosetta 2 转译,要么另想办法。我这边的方案是把它扔进 Docker 里跑,宿主机只暴露一个 SOCKS5 端口,再用 Clash Verge Rev 的 TUN 模式配合全局扩展脚本来决定哪些流量走 VPN。

这套东西和 Linux 上的做法完全不同——在 Debian 13 安装 EasyConnect 里是直接修本地依赖库,macOS 上是把整个客户端隔离进容器。

为什么选 Docker

  • 没有原生 arm64 包:官方 macOS 客户端是 x86_64 的,靠 Rosetta 2 转译能跑,但为了一个 VPN 客户端常驻一套转译层不太划算。
  • 沙箱更安心:EasyConnect 会改路由表、接管 DNS、装后台进程,在容器里折腾,退出容器就干净了。
  • 天然支持按需分流:容器登录后自带 1080(SOCKS5)和 8888(HTTP)两个代理端口,宿主机不用把全部流量交给它。
  • 跨平台复用:同一份 compose 在 Linux 上也能直接跑。

代价也说清楚:图形界面得通过 VNC 看,登录要手动点一次;容器重启后 VPN 会话就没了,需要在 VNC 里重新登录。

目录结构

easyconnect/
├── docker-compose.yml
├── .env                  # VNC 密码,不进版本库
├── custom_hosts          # 内网域名 → 内网 IP,挂进容器的 /etc/hosts
├── certs/
│   └── mycert.p12        # 登录用的证书,不进版本库
└── .ecdata/              # 容器 /root 的持久化目录,里面有登录态,不进版本库

docker-compose.yml

services:
  easyconnect:
    image: hagb/docker-easyconnect:7.6.7
    container_name: easyconnect
    restart: unless-stopped
    cap_add:
      - NET_ADMIN                        # VPN 要建 tun 网卡,必需
    devices:
      - /dev/net/tun:/dev/net/tun        # 同上,必需
    environment:
      PASSWORD: "${EC_PASSWORD}"          # VNC 密码,最多 8 位
      URLWIN: "1"                         # VPN 想调浏览器时弹框给出链接
      DISABLE_PKG_VERSION_XML: "1"        # arm64 必加,否则版本校验过不去
      CLIP_TEXT: "https://<vpn-portal>"   # 预置到 VNC 剪贴板,登录时直接粘贴
      PING_ADDR: "10.20.0.10"           # 内网地址,定时 ping 保活
    volumes:
      - ./.ecdata:/root                                        # 登录态持久化
      - ./custom_hosts:/etc/hosts:ro                            # 内网域名映射
      - ./certs/mycert.p12:/root/mycert.p12:ro                  # 数字证书
    ports:
      - "127.0.0.1:5901:5901"             # VNC
      - "127.0.0.1:1080:1080"             # SOCKS5
      - "127.0.0.1:8888:8888"             # HTTP 代理

.env:

EC_PASSWORD=<8 位以内的密码>

几个参数值得说明:

参数 作用
EC_PASSWORD / PASSWORD VNC 密码。VNC 协议限制最多 8 位,写长了会被截断
DISABLE_PKG_VERSION_XML arm64 镜像必需,绕过客户端版本号校验
CLIP_TEXT 把登录地址塞进 VNC 剪贴板,省得在 VNC 窗口里手敲一长串地址(也可用来粘中文用户名)
PING_ADDR 定时 ping 一个只有通过 VPN 才能到达的内网地址,把会话保活;留空则不 ping
ports 一律绑 127.0.0.1 这些端口等于半个内网入口,绝不能对外暴露

Tip

镜像默认走 Docker Hub。国内拉不动可以在 image 前面加加速前缀(我用的是 gh-proxy.org/docker/),拉取成功后再去掉也行。所有 -e 参数写成 ${XX} 形式、密码放进 .env,比直接把密码硬写在 compose 里干净。

custom_hosts:内网域名在容器里解析

公司内网的服务大多是这样三类:

  1. 正经域名,但公网 DNS 查不到,比如 nexus.internal.example.com
  2. Kubernetes 集群内部域名,形如 xxx.stable.svc.cluster.local
  3. 完全没有后缀的短名:ftp、ks、gateway 这种,只有内网 DNS 认识

宿主机上我不写这些映射——写上去就等于把域名提前解析成 IP,Clash 只能看到 IP 看不到域名,按域名分流就废了。映射只放在容器里:

# 研发/中间件集群
10.20.x.10    nexus.internal.example.com jenkins.internal.example.com
10.20.x.10    git.internal.example.com wiki.internal.example.com
10.20.x.10    gateway.stable gateway-service.test gateway.internal.example.com
10.20.x.20    logs.internal.example.com grafana.internal.example.com
10.20.x.30    ftp ks audit paymentplatform

# 公网映射的特殊地址
203.0.113.7   web.example.com
203.0.113.10  y.example.com

# 固定几个 GitHub raw 的 IP,避免被 VPN 抢走
185.199.108.133 raw.githubusercontent.com
185.199.109.133 raw.githubusercontent.com

Warning

挂载的 /etc/hosts 会整个替换容器自带的 hosts 文件,记得自己补上 127.0.0.1 localhost,否则容器内 localhost 可能解析不出来。

启动并登录

docker compose up -d
docker logs -f easyconnect      # 看启动过程

然后用 macOS 自带的「屏幕共享」连 VNC:

open vnc://127.0.0.1:5901

也可以直接用 Finder → 前往 → 连接服务器,输入 vnc://127.0.0.1:5901,密码填 .env 里那个。

连上后就是一个完整的 EasyConnect 登录窗口:填写 VPN 地址(CLIP_TEXT 已经放到剪贴板里,直接 Cmd+V)、账号密码,需要证书的话选中挂载进来的 .p12。登录成功后终端里会看到路由和 DNS 被改写的日志。

先单独验证 SOCKS5

在把所有东西交给 Clash 之前,先确认容器这条链路本身是通的:

# 域名交给代理端解析(容器内的 /etc/hosts 才会生效)
curl --socks5-hostname 127.0.0.1:1080 -I http://nexus.internal.example.com/

这里有个很容易踩的坑:--socks5 是本地解析域名再把 IP 交给代理,宿主机根本不认识 nexus.internal.example.com,会直接报解析失败;必须用 --socks5-hostname(或 socks5h://)让域名发到容器里再解析。

接进 Clash Verge Rev

1. TUN 模式

TUN 是这套方案能成立的前提,原因有两条:

  • 只看域名的规则要能命中:内网短名、*.svc.cluster.local 这些名字公网查不到,只有让 Clash 自己接管 DNS 才能拿到域名做匹配;纯系统代理模式下很多程序(终端里的 nc、mysql、redis-cli)压根不读代理设置。
  • 非 HTTP 协议也得走 VPN:内网一堆 MySQL、Redis、Kafka、ES 端口,这些没法用 HTTP 代理,TUN 才能一层层兜住。

配合 fake-ip(默认 198.18.0.0/16)使用:应用访问 gateway.stable 时,Clash 返回一个假 IP 并记住「这个假 IP 对应 gateway.stable」,规则就能按域名把它送去 SOCKS5;真正的解析在容器里由 /etc/hosts 完成。不要在宿主机 /etc/hosts 里写内网映射,会把这条链路掐断。

2. 全局扩展脚本

与其每次改订阅都手写一遍规则,不如用 Clash Verge Rev 的「全局扩展脚本」把所有 profile 统一加工一遍。脚本入口固定是 main(config),返回改写后的配置:

// Define main function (script entry)
function main(config) {
  // 1. 定义 EasyConnect 的 SOCKS5 代理
  const easyConnectProxy = {
    name: "EasyConnect-VPN",
    type: "socks5",
    server: "127.0.0.1",
    port: 1080,
    udp: true,
  };

  if (!config.proxies) config.proxies = [];
  if (!config.proxies.some(p => p.name === easyConnectProxy.name)) {
    config.proxies.push(easyConnectProxy);
  }

  // 2. 内网分流规则
  const internalRules = [
    // 【防误伤】容易撞车的公网域名优先直连
    "DOMAIN-SUFFIX,raw.githubusercontent.com,DIRECT",

    // 【内网 IP 段】注意 /8 的范围,见下面的坑
    "IP-CIDR,10.0.0.0/8,EasyConnect-VPN,no-resolve",
    "IP-CIDR,192.0.2.0/24,EasyConnect-VPN,no-resolve",
    // 排错用:单个 IP 的 /32 路由(已被上面 /8 覆盖,先留着)
    "IP-CIDR,10.20.0.10/32,EasyConnect-VPN,no-resolve",

    // 【公网映射的内网服务】换成你自己的映射地址
    "IP-CIDR,203.0.113.10/32,EasyConnect-VPN,no-resolve",
    "IP-CIDR,198.51.100.20/32,EasyConnect-VPN,no-resolve",

    // 【内网域名后缀】
    "DOMAIN-SUFFIX,example.com,EasyConnect-VPN",
    "DOMAIN-SUFFIX,example-corp.cn,EasyConnect-VPN",
    "DOMAIN-SUFFIX,cluster.local,EasyConnect-VPN",   // K8s 内部域名
    "DOMAIN-SUFFIX,svc,EasyConnect-VPN",             // K8s 简写后缀

    // 【没有后缀的短名】只能精确匹配
    "DOMAIN,ftp,EasyConnect-VPN",
    "DOMAIN,ks,EasyConnect-VPN",
    "DOMAIN,audit,EasyConnect-VPN",
    "DOMAIN,gateway.stable,EasyConnect-VPN",
    "DOMAIN,gateway-service.test,EasyConnect-VPN",
    "DOMAIN,paymentplatform,EasyConnect-VPN",
  ];

  // 3. 清理旧规则再注入,避免反复加载后一堆重复
  let originalRules = config.rules || [];
  originalRules = originalRules.filter(rule => !rule.includes("EasyConnect-VPN"));
  config.rules = [...internalRules, ...originalRules];

  return config;
}

几点设计考虑:

  • 节点由脚本注入:订阅更新会把 proxies 整个换掉,写死在脚本里就不会丢。
  • 规则前置:config.rules 是数组,前面的先匹配,所以内网规则必须排在订阅自带规则之前。
  • no-resolve 不是可选项:IP-CIDR 命中前 Clash 可能要先把域名解析成 IP,加 no-resolve 表示「别为了匹配这条规则去查 DNS」,既快,也避免把内网域名泄漏给公网 DNS。
  • 精确短名用 DOMAIN:gateway.stable 这种名字用 DOMAIN-SUFFIX 容易误伤,精确匹配更稳。
  • 重复加载要幂等:脚本每次重载都会跑,先用 filter 剔掉旧规则再拼,不然规则会越滚越多。

3. 确认分流生效

TUN 打开后,在终端直接访问就行,不需要 --proxy:

curl -I http://gateway.stable/
curl -I https://nexus.internal.example.com/

在 Clash 的「连接」面板里能看到每条连接命中的规则和出口是 EasyConnect-VPN 还是直连——比看日志猜快得多。

踩过的坑

IP-CIDR,10.0.0.0/8 会顺手带走家里的设备。 这个段太宽,NAS、打印机、路由器管理页只要在 10.x 里,就一起被塞进 VPN,然后打不开。要么收窄到公司实际使用的段(比如 10.20.0.0/16、10.30.0.0/16),要么在前面补几条 IP-CIDR,10.x.x.x/32,DIRECT 例外。

.local / svc 这类后缀要显式写规则。 这类名字不会走公网 DNS,Clash 的默认规则里也没有,不写就是直连然后超时。cluster.local、svc 两个后缀基本能覆盖绝大多数 K8s 内部服务,剩下的按需补 DOMAIN,。

容器重启后要重新登录。 restart: unless-stopped 只保证容器起来,VPN 会话不会自动恢复。PING_ADDR 能让会话活得久一点(服务端通常按「无流量超时」断线),但断线后还是得连 VNC 手动登录一次。

不要把 .ecdata、certs、.env 提交到版本库。 .ecdata 里有登录态和会话配置,certs 里是证书私钥,.env 里是密码。.gitignore 三行解决。

VNC 密码只能 8 位。 这是 VNC 协议的限制,不是镜像问题;写长了会被悄悄截断,然后你以为是密码错了。

端口一律绑 127.0.0.1。 1080、8888 相当于一个不用认证的内网入口,0.0.0.0 绑出去等于把公司内网挂到公网上。

小结

这套方案的分工其实很清楚:

  • Docker 容器 负责「连上 VPN」和「解析内网域名」,对外只暴露 127.0.0.1:1080
  • Clash Verge Rev 负责「决定哪些流量走 VPN」,靠 TUN 接管全流量、靠 fake-ip 保留域名、靠全局扩展脚本注入规则
  • 分流规则 负责精准:内网 IP 段、内网域名后缀、无后缀短名,三类分别用 IP-CIDR、DOMAIN-SUFFIX、DOMAIN 收口

好处是不用装 Rosetta、不用把整台机器交给 VPN,坏处是登录要手动点一次,而且规则维护是长期活儿——每多一个内网服务,就多一条规则或者一行 hosts。

分类 标签