Clash Verge TUN 下让公司内网依赖走内网
在家办公、周末排查问题,或任何不在公司办公网络里的场景,机器经常需要同时连上两套网络:一套公司客户端负责访问内网服务,一套外网代理负责日常上网和公开资料访问。理想状态是内网和外网各走各的路,实际开了 Clash Verge Rev 的 TUN 模式后,公司内网域名可能会先被 Clash 的 DNS 接管,最后落到 198.18.x.x 这样的 fake-ip 上。表面上看,公司客户端已经连上了,真正请求却没有走到公司内网。
这个问题不能只靠一条 DIRECT 规则解决。TUN 模式下,路由规则、fake-ip 过滤、公司域名 DNS 和 TUN 路由排除要一起处理:先让公司域名不走代理节点,再让这些域名不被分配 fake-ip,随后为不同内网服务选择正确的 DNS 或固定 host 映射,最后把解析出来的内网 IP 从 TUN 自动路由里排除。
代理的本质
代理的本质是让一台中间节点替你完成一段连接。区别在于这台中间节点站在哪一边:站在客户端这边,是正向代理;站在服务端这边,是反向代理。
正向代理站在客户端这边。应用把「我要访问哪个目标」交给代理,代理再替应用去连接目标服务。Clash 普通模式里的 HTTP / SOCKS 代理就是这种形态。命中 DIRECT 时,本机 Clash 直接连接目标服务;命中 PROXY 时,本机 Clash 会先把请求交给远端节点服务器,再由节点服务器连接目标服务。
目标服务看到的来源通常是远端节点服务器,而不是应用所在的本机。DIRECT 则少掉远端节点这一段,本机 Clash 会按系统 DNS 和系统路由直接连目标服务。
反向代理站在服务端这边。客户端访问的是业务域名,真正接住请求的是 Nginx、CDN、网关或负载均衡,再由它转发到后端服务。客户端通常不需要知道后面有几台真实机器,也不需要额外配置代理。
客户端
↓ 访问 service.example.com
反向代理,例如 Nginx / CDN / 网关
↓ 转发、负载均衡、TLS 终止、鉴权
后端服务这篇文章里的 Clash 是正向代理,影响的是本机出站流量;公司 VPN / 零信任客户端提供的是公司资源访问路径,影响的是公司域名解析、系统路由和访问网关。问题出在这两套客户端侧网络能力同时接管 DNS 或路由时,谁先处理、谁后处理没有对齐。
先判断公司网络是哪种形态
不同公司的远程办公网络不一定需要同一套处理方式。遇到“内网和外网要同时可用”的问题,先判断公司客户端到底接管了哪一层。
大厂常见的是全隧道模式。客户端接管默认路由和 DNS,所有流量先进入企业网关。公司资源在网关内侧继续走内网、专线、VPC 或应用代理,普通外网再从企业统一出口 NAT 出去。用户看起来是“连上 VPN 以后内外网都能访问”,实际是外网也先进了企业网络。
应用
↓
企业 VPN / 零信任客户端
↓
企业网关
├─ 公司资源:内网 / 专线 / VPC / 应用代理
└─ 普通外网:企业统一出口 NAT这种模式下,企业客户端已经承担了 Clash TUN 的角色。普通开发机不建议再叠一层个人 TUN 代理;如果确实需要外网代理,优先用浏览器或单个工具的应用层代理,避免和企业客户端抢默认路由、DNS 和证书策略。
另一类是分流模式。公司域名、公司 IP 段或受控应用进入 VPN,普通外网仍走本地网络。很多零信任客户端也会落到类似效果:只给受控资源下发 DNS、fake-ip、路由或应用代理,不把整台机器的外网都收走。
公司域名 / 公司 IP
↓
企业 VPN / 零信任客户端
↓
公司资源
普通外网
↓
本地网络 / Clash这篇文章处理的就是分流场景里的冲突:公司客户端负责内网,Clash 负责外网;Clash 一旦开 TUN,就可能抢到本来该交给公司客户端的 DNS 或路由。
还有一种更轻的形态是公网入口加白名单。服务部署在云上,有公网域名,但只允许办公网出口、VPN 出口或企业网关出口访问。这个方案看起来像“卡 IP”,但通常不是唯一防线。成熟一点的企业还会叠身份、设备可信状态、客户端证书、网关鉴权、应用权限和审计。
用户身份 / 设备状态 / 客户端证书
↓
企业 VPN / 零信任网关
↓
固定企业出口 IP
↓
云上服务的白名单 / 安全组 / 应用鉴权所以“连内网是不是靠卡 IP”要分层看。IP 白名单经常是服务端最后一道入口限制;真正决定这台电脑能不能进入公司资源的,通常还有身份、设备、DNS、路由、证书和网关策略。
可以按下面的方式选方案:
| 公司网络形态 | 典型表现 | 更稳的处理方式 |
|---|---|---|
| 全隧道 | 连上公司客户端后,内网和外网都从企业出口走 | 不叠个人 TUN;外网特殊需求优先用应用层代理 |
| 分流 VPN | 只有公司域名或公司 IP 进 VPN,外网仍走本地网络 | Clash 可以负责外网;开 TUN 时排除公司域名、fake-ip 和内网 IP 段 |
| 零信任应用代理 | 只对受控应用下发 DNS、fake-ip、证书或应用代理 | 让公司客户端优先处理公司域名;Clash 只处理非公司流量 |
| 公网入口加白名单 | 服务有公网域名,但非企业出口访问会 403 或超时 | 确认请求从企业出口出去;不要只改 DNS 或本地 hosts |
如果公司客户端已经是全隧道代理或完整企业网关,后面这套 Clash TUN 调整通常不该继续做。它更适合 iOA / SmartVPN 这类只负责公司资源访问路径的场景:公司客户端让内网服务可达,Clash 继续负责普通外网,两边需要避开 DNS 和路由上的互相抢占。
为什么需要 TUN
普通正向代理通常只接管主动配置了 HTTP / SOCKS 代理的应用。浏览器、IDE、命令行、Gradle、npm 各有自己的代理读取方式,任何一层没跟上,流量就可能绕开代理。
不开 TUN 时,Clash 也可以和公司 VPN 一起工作,而且内网访问通常更不容易被 Clash 抢走。顺序取决于应用有没有使用 Clash 的本地代理端口。
应用使用系统代理或手动配置 Clash 代理时,链路是:
应用
↓ 连接 Clash 本地 HTTP / SOCKS 端口
Clash 按域名规则判断
↓ 命中 DIRECT
Clash 作为普通进程发起直连
↓
系统 DNS / 系统路由继续生效
↓
公司域名进入公司 VPN,普通外网进入默认网络应用没有使用代理时,链路更短:
应用
↓
系统 DNS / 系统路由
↓
公司 VPN 或普通网络这种模式能共存的前提是:公司 VPN 已经把公司域名、公司内网 IP 或访问网关路由装好;Clash 只作为应用层代理存在,没有把系统 DNS 和系统路由整体接管。如果系统 DNS 仍被指到 Clash,或者 Clash 的 DNS 增强模式仍在返回 fake-ip,内网域名还是可能被改写,只是问题不再来自 TUN 路由捕获。
TUN 模式换了一个位置拦截。Clash / Mihomo 会创建一块虚拟网卡,再通过系统路由把更多目标地址导向这块网卡。应用不需要知道代理存在,它照常发 DNS 查询、照常连接某个 IP;系统路由把这批 IP 包送进 TUN 网卡后,Clash 再按规则决定走代理节点还是直连。
应用 / 命令行 / 构建工具
↓
DNS 得到目标 IP,可能是真实 IP,也可能是 Clash fake-ip
↓
系统路由表判断这个 IP 走哪块网卡
↓
命中 Clash TUN 路由时,IP 包进入 Clash 虚拟网卡
↓
Clash 根据规则选择 PROXY / DIRECT / REJECT
↓
Clash 自己再发起真正的出站连接DIRECT 只表示 Clash 接到这条连接后不走代理节点,不能让系统跳过 Clash TUN。只要 DNS 或路由已经被 Clash 接管,流量就会先进 Clash,再由 Clash 决定下一跳。
公司 VPN / 零信任客户端也可能在本机创建自己的虚拟网卡和路由。内网服务要走通,关键是公司域名最终解析到公司 VPN 能识别的地址,并且系统路由把这个地址送进公司 VPN 的虚拟网卡,而不是送进 Clash TUN。
期望路径:
公司域名
↓
公司 DNS / VPN DNS
↓
公司 VPN 能识别的 IP
↓
系统路由进入公司 VPN 网卡
↓
公司网关 / 私服
常见错误路径一:
公司域名
↓
Clash DNS 返回 198.18.x.x fake-ip
↓
系统路由进入 Clash TUN
↓
公司 VPN 没有机会接管
常见错误路径二:
公司域名
↓
公司 DNS 返回内网 IP
↓
系统路由仍进入 Clash TUN
↓
请求卡在 Clash 到公司内网这一段所以这里要同时处理两件事:DNS 不能把公司域名改成 Clash fake-ip,路由也不能把公司内网 IP 抓进 Clash TUN。fake-ip-filter 负责前半段,tun.route-exclude-address 负责后半段。
先判断卡在哪一层
macOS / zsh 下先看域名解析和路由。这里把真实私服域名写成占位符,避免把公司内部地址放进公开文章。
dig +short <NEXUS_HOST>
route -n get <NEXUS_HOST>
curl -I --connect-timeout 10 --max-time 18 https://<NEXUS_HOST>/repository/gradle/这三条命令分别看三层问题。dig +short 只看域名最后解析成什么 IP,适合判断当前域名有没有落到 fake-ip,或者有没有拿到公司 DNS 返回的真实内网 IP。route -n get 看系统准备把这个目标 IP 从哪块网卡送出去,适合判断它是走 Clash TUN、普通 Wi-Fi,还是公司 VPN 的 utun 接口。curl -I 发一个只取响应头的 HTTPS 请求,适合判断服务本身能不能握手、能不能返回私服自己的响应。
curl 里的两个超时参数是为了让排障快一点失败。--connect-timeout 10 限制 TCP / TLS 连接阶段最多等 10 秒;--max-time 18 限制整个请求最多等 18 秒。内网路由没通时,不加这两个参数会让命令挂很久,影响判断节奏。
如果 dig 返回 198.18.x.x,通常说明域名被 Clash / Mihomo 的 fake-ip DNS 接管了。Mihomo DNS 文档里的示例也把 fake-ip-range 放在 198.18.0.1/16,这类地址不是公司内网私服的真实 IP。继续看 route,如果网关是 198.18.0.1,接口是 Clash TUN 对应的 utun,就说明请求还在外网代理的 TUN 里打转。
如果加完配置后 dig 不再返回 198.18.x.x,但直接没有结果,问题已经前进了一层:fake-ip 绕开了,内网 DNS 还没有接上。这个状态下 Gradle 常见报错会从 TLS 握手、连接超时,变成更直接的域名解析失败,例如 nodename nor servname provided, or not known。
同一个公司根域名下也可能混着几类私服:有的只在 VPN 内网 DNS 里能解析,有的已经有固定公网入口,有的需要公司 DNS 和 host route 同时配。不要因为一个 Nexus 域名需要公司 DNS,就把整个公司根域名都交给同一个 DNS;这样可能会把本来可访问的 Artifactory 或 GitLab 覆盖掉。如果某个高频域名的 IP 已经稳定,直接写 hosts 往往比每次经 nameserver-policy 查询更稳。
如果 dig 能拿到公司内网 IP,但 curl 仍然连不上,再看路由是否真的进入公司 VPN 对应的接口。拿到 401、403 或私服自己的响应,反而通常说明网络路径已经通了,后面才轮到认证、仓库路径或依赖版本问题。
macOS 上还可以临时绑定接口验证一次,不改系统路由,也不改 Clash 配置:
curl --interface <VPN_INTERFACE> \
--resolve <NEXUS_HOST>:443:<NEXUS_IP> \
-I https://<NEXUS_HOST>/repository/gradle/--interface 会让 curl 这一次请求绑定到指定网卡,例如公司 VPN 对应的 utun4。--resolve 会临时告诉 curl:访问这个 HTTPS 域名时直接连指定 IP,同时仍保留原域名作为 SNI 和 Host。这样可以绕开系统默认 DNS 和默认路由,只验证「这台机器通过公司 VPN 接口能不能访问这个私服」。如果这条能成功,默认请求失败,就能把问题缩小到系统路由或 Clash TUN 捕获。
如果绑定公司 VPN 接口后能拿到私服响应,默认访问却超时,说明私服本身可达,问题在 TUN 路由捕获。这时优先补 tun.route-exclude-address,不要继续在 DNS 上绕。
四段配置缺一不可
Clash Verge Rev 支持用 JavaScript 改写配置。官方 Script Configuration说明入口函数是 main(params),接收 YAML 配置转成的对象,返回修改后的对象。自定义规则要放在原规则前面,因为规则从上到下匹配,遇到 MATCH 后就不会继续往下走;Clash Verge Rev 的 Merge 文档也把这类写法放在 prepend-rules 里。
下面是可复用的 Script 版本。使用前把域名、DNS 和内网 IP 段换成自己公司的值。
function main(config) {
const corpDomain = "corp.example.com";
const fixedHosts = {
[`gitlab.${corpDomain}`]: "10.0.0.10",
[`nexus.${corpDomain}`]: "10.0.0.11",
[`artifactory.${corpDomain}`]: "203.0.113.10",
};
const corpRouteExclude = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"<NEXUS_IP>/32",
];
const corpHosts = [
`nexus.${corpDomain}`,
`artifactory.${corpDomain}`,
`gitlab.${corpDomain}`,
];
const prependRules = [
`DOMAIN-SUFFIX,${corpDomain},DIRECT`,
...corpHosts.map((host) => `DOMAIN,${host},DIRECT`),
];
config.rules = [...prependRules, ...(config.rules || [])];
config.dns = config.dns || {};
const oldFakeIpFilter = config.dns["fake-ip-filter"] || [];
config.dns["fake-ip-filter"] = Array.from(new Set([
...oldFakeIpFilter,
`+.${corpDomain}`,
corpDomain,
...corpHosts,
]));
config.hosts = {
...(config.hosts || {}),
...fixedHosts,
};
config.tun = config.tun || {};
const oldRouteExclude = config.tun["route-exclude-address"] || [];
config.tun["route-exclude-address"] = Array.from(new Set([
...oldRouteExclude,
...corpRouteExclude,
]));
return config;
}这段配置分四层生效。
第一层是 DOMAIN-SUFFIX,...,DIRECT。DIRECT 的意思是不走 Clash 的代理节点;如果流量已经进了 Clash,它仍然会先由 Clash 接住,再按直连方式出站。公司 VPN 正常安装路由时,直连出站才有机会被系统送进公司网络。
第二层是 fake-ip-filter。Mihomo 文档对 fake-ip-filter 的说明是:命中的地址不会下发 fake-ip 映射。公司域名如果还拿到 198.18.x.x,说明 DNS 已经被 Clash 改写;后面即使命中 DIRECT,也只是让 Clash 直连出站,不能保证请求回到公司 VPN 的 DNS 和路由链路。
第三层是 hosts,必要时才用 nameserver-policy。hosts 适合处理已经确认的固定入口,能让 Clash DNS 直接返回结果,不再临时问上游 DNS。nameserver-policy 适合探索或处理确实需要公司 DNS 动态解析的域名;Mihomo 文档里的示例也包含了把内部域名交给内网 DNS 的写法,例如把 +.internal.crop.com 交给 10.0.0.1。这一步最容易误伤:如果公司 DNS 只认识其中一个私服,却把 +.corp.example.com 全部指过去,另一个私服可能从可访问变成解析失败;在 TUN 场景里,浏览器还可能直接表现成 DNS_PROBE_FINISHED_NXDOMAIN。
第四层是 tun.route-exclude-address。Clash Verge Rev 的 Bypass 文档把 TUN 流量排除单独列成一类,Mihomo TUN 文档里也有 route-exclude-address 字段,用于在启用 auto-route 时排除自定义网段。这个字段解决的是另一种半通状态:公司域名已经解析成真实内网 IP,但 route -n get 仍显示流量走 Clash 的 TUN 网卡。此时 Gradle 可能不再报域名不存在,而是卡在 TLS 握手超时或被对端断开。
验证顺序
改完配置后,不要直接跑 Gradle。先用三条命令确认网络路径已经正确。
dig +short <NEXUS_HOST>
route -n get <NEXUS_HOST>
curl -I --connect-timeout 10 --max-time 18 https://<NEXUS_HOST>/repository/gradle/几个状态可以快速判断:
| 现象 | 说明 | 下一步 |
|---|---|---|
dig 还是 198.18.x.x |
fake-ip-filter 没生效,或当前配置没有激活 |
检查 Script 是否启用、规则是否进入最终配置 |
dig 没结果 |
fake-ip 已绕开,但公司 DNS 没接上 | 把 nameserver-policy 里的占位符换成真实公司 DNS |
| 某个私服被公司 DNS 解析失败,但固定 IP 可访问 | 这个私服不应该跟随根域名策略 | 给该私服加精确 hosts |
dig @公司 DNS 正常,浏览器却 NXDOMAIN |
Clash DNS / TUN 里的动态策略不稳定 | 把高频域名改成 hosts 固定映射 |
有真实内网 IP,但 route 仍走 Clash TUN |
域名解析对了,IP 流量还被 TUN 捕获 | 把这个 IP 或公司内网段加入 tun.route-exclude-address |
绑定公司 VPN 接口后 curl 成功,默认访问超时 |
私服可达,但默认流量仍被 Clash TUN 接走 | 补 tun.route-exclude-address,再重启 Clash TUN |
route 显示公司 VPN 接口,但 curl TLS 超时 |
路由方向对了,但私服链路或 TLS 仍未通 | 用同一网络下的浏览器、curl --interface 或公司 VPN 说明继续查证 |
curl 返回 401 / 403 / 私服响应 |
网络路径基本通了 | 回到 Gradle 认证、仓库路径和依赖版本 |
还有一种状态会出现在 Clash Verge 的 UI 配置里:已经在「排除自定义网段」里加了 <NEXUS_IP>/32,但 route -n get <NEXUS_IP> 仍然显示走 Clash TUN。这个时候要先确认最终生成配置里 tun.route-exclude-address 是否真的包含这条记录;如果最终配置已经包含但系统路由还没变,可能需要完整重启 Clash TUN、重新应用配置,或者临时加一条 macOS host route 先把主线走通:
sudo route -n add -host <NEXUS_IP> -interface <VPN_INTERFACE>
route -n get <NEXUS_IP>sudo route -n add -host 会给某个单独 IP 加一条 host route。-host <NEXUS_IP> 表示只影响这个目标 IP,不影响整个网段;-interface <VPN_INTERFACE> 表示这个目标直接从公司 VPN 的接口走。后面的 route -n get 用来确认系统路由表是否真的变了。
如果提示路由已经存在,可以改用:
sudo route -n change -host <NEXUS_IP> -interface <VPN_INTERFACE>change 和 add 的区别是:add 新增一条不存在的路由,change 修改已有路由。Clash TUN 或公司 VPN 已经写过同一个目标时,add 可能提示已存在,这时用 change 把它改到公司 VPN 接口。它们都只是临时兜底,网络、VPN 或系统重启后可能失效。它们适合用来验证「只要流量进公司 VPN,私服就能访问」这个判断,不适合当长期配置。
Gradle 依赖下载只适合放在这些检查之后:
cd <ANDROID_PROJECT>
JAVA_HOME="<ANDROID_STUDIO_JBR>" \
ANDROID_HOME="$HOME/Library/Android/sdk" \
ANDROID_SDK_ROOT="$HOME/Library/Android/sdk" \
./gradlew -I <INIT_SCRIPT> clean <BUILD_TASK> --console=plain --no-daemon这条 Gradle 命令把 Java、Android SDK 和本次构建任务的前提都写在同一处。JAVA_HOME 指向 Android Studio 自带 JBR,避免系统没有默认 Java 或版本不对。ANDROID_HOME / ANDROID_SDK_ROOT 指向本机 Android SDK。-I <INIT_SCRIPT> 加载临时 init script,可以在不改项目源码的情况下补仓库配置、屏蔽本机访问不到的仓库,或者注入只对本机生效的构建兜底。clean <BUILD_TASK> 表示先清理再执行目标构建任务;--console=plain 让日志更适合复制;--no-daemon 避免 Gradle Daemon 缓存上一轮网络或 JVM 状态。
这里的 INIT_SCRIPT 不能替代内网 DNS 和公司 VPN。网络没通时,init script 只会把「插件找不到」推进到「私服域名连不上」。网络通了以后,它才适合处理项目仓库声明不完整、本机临时镜像、或者某个内部仓库在当前网络下不可解析这类构建层问题。
真正验证配置是否正确,最后还是要回到实际构建。一次完整的 Android release 构建能越过依赖解析、Kotlin / Java 编译、KAPT / Hilt、R8 和 APK 打包,说明这套网络配置已经覆盖了构建所需的私服链路。中间出现资源字符串、deprecated API 或 R8 optional dependency 的 warning,不一定代表网络还有问题;要看最终失败点是不是仍然停在域名解析、仓库访问或依赖下载。
一个容易误判的变化
从 198.18.x.x 变成解析失败,看起来像配置变坏了,其实通常是配置修到一半了。前者说明公司域名还在 Clash fake-ip 里,后者说明 fake-ip 已经绕开,但缺真实 DNS。
真正完整的状态应该是:
- 公司域名不再返回
198.18.x.x。 - 公司域名能解析到真实内网 IP。
route显示请求会进入公司 VPN 能到达的路由,而不是 Clash TUN 网卡。curl能拿到私服自己的响应。- Gradle 再继续处理认证、插件仓库、Android SDK 和构建任务。
这类问题最麻烦的地方在于它同时跨了代理、DNS、系统路由和构建工具。把这几层拆开后,排查会轻很多:Clash 只负责外网和规则,TUN 负责接管流量,公司 VPN 负责内网路由,公司 DNS 负责内部域名。哪一层没有接上,命令输出会给出不同形态的失败。
