所有文章
发布于 2026 年 7 月 4 日· 12 分钟阅读

同时装了 Tailscale 和 Clash?一次 DNS 被悄悄劫持的真实排查

TailscaleClash VergeTUNDNSWireGuard网络排查

起因

事情是这样发现的:我在用 Claude Code 处理一个任务,中途它要查一份维基百科页面,结果直接报了一堆证书错误,连不上。

这让我挺意外的——机器上明明装着 Clash Verge,TUN 模式也一直开着,我一直以为 TUN 模式就该接管系统里所有的网络请求,装了就该"全都通",怎么会有东西漏网?

带着这个疑问查下去,才发现问题比想象的深:真正的罪魁祸首不是 Clash,而是另一个我天天开着、压根没往这上面想过的工具——Tailscale。

症状

Clash Verge 开着 TUN 模式,规则也配好了,某个域名的规则清清楚楚写着走代理——但访问这个网站的时候,行为却像完全没走代理:要么直接卡死,要么拿到一个莫名其妙的连接。

奇怪的是,同一台机器上访问别的网站(比如浏览器里打开的网页)完全正常,走代理也正常。只有命令行工具(curldig,包括 AI 编程工具自己发起的请求)查某些域名,行为不对。

如果你的机器上同时装着 Tailscale,这篇大概率能帮到你。

关键词(可以展开学习): DNS 解析优先级、MagicDNS、TUN 虚拟网卡、最长前缀匹配路由


先搞清楚:DNS 解析在 macOS 上到底怎么走

大部分人对"DNS 出问题"的理解停留在"换个 DNS 服务器地址就行",但 macOS(以及大部分现代系统)的 DNS 解析比这复杂一层:

  1. 系统里有个专门管解析的进程(macOS 上叫 mDNSResponder),几乎所有 App 查域名都要经过它
  2. 它会看当前所有"注册过的解析器",决定该问谁——这里有优先级和作用域的区别:有的解析器是无限定的(所有域名都问它),有的是限定某个域名的(比如"只有查 .corp.internal 才问我")
  3. 决定"该问谁"之后,才轮到路由表决定这个查询包实际从哪张网卡出去

Tailscale 的 MagicDNS 功能,一旦打开(默认就是开的),会把自己注册成无限定的默认解析器——意味着系统所有"不知道该问谁"的查询,第一个就问它。而 Tailscale 自己的解析器地址 100.100.100.100,只能从它自己创建的虚拟网卡出去,跟 Clash 的 TUN 网卡完全是两条路。


为什么 Clash 的 TUN 拦不住

Clash Verge / Mihomo 的 TUN 模式靠一个叫 dns-hijack 的机制生效:把"任何目的地是 53 端口的包"重定向到自己的内置 DNS 服务器。听起来应该能拦住一切,但它依赖一个前提——这个包得先经过"走默认路由"这一步

问题就出在这:100.100.100.100 是 Tailscale 自己的虚拟地址,系统路由表里给它配的是一条很具体的路由,直接指向 Tailscale 自己的网卡。IP 路由有条铁律叫最长前缀匹配——越具体的路由优先级越高,跟谁"看起来更该管"没关系。Clash 的 TUN 模式再怎么配置,也只是抢占"默认路由"这个相对宽泛的位置,遇到别人钉死的具体路由,天然抢不过。

也就是说:不是 Clash 拦截失效了,是这个查询压根没走到它能拦截的那条路上。


怎么确认自己中招了

不用猜,几条命令直接看:

# 1. 查系统默认走哪个解析器(不带 domain 限定的那条,往往排最前)
scutil --dns | grep -A2 "^resolver #1"

# 2. 这个解析器地址实际从哪张网卡出去
route -n get 100.100.100.100

# 3. 查一个域名,看拿到的是真实 IP 还是干净的内部地址
dscacheutil -q host -a name example.com

如果第一条显示 100.100.100.100 且没有 domain: 限定,第二条显示走的是 Tailscale 自己的 utun 网卡(不是 Clash 的那个),基本可以确定就是这个问题。

如果你的 Clash 有开放本地管理面板(external-controller),还可以更直接地验证——查一下它自己记录的连接日志,看目标域名对应的连接里 host 字段是不是空的、命中的规则是不是那条兜底的 Match。空的 host 意味着 Clash 自己都不知道这个连接是查哪个域名产生的——因为它压根没看到过这次 DNS 查询。


这不是配置错误,官方也没打算修

去 Clash Verge Rev 的 GitHub 仓库搜一下就知道,这不是个例:

  • #1626:一模一样的复现步骤,开着 Tailscale 时代理直接失效,维护者关闭为 not planned
  • #6989:用户连"排除 Tailscale 网段"「配置 nameserver-policy」这两个常规办法都试了,依然没用

原因跟上面讲的一样:这是两个都想当"网络守门人"的工具之间的路由优先级冲突,不是哪个配置项没填对。靠 Clash 单方面配置,解决不了。


解法:先松手,再补规则

第一步:关掉 Tailscale 的 DNS 覆盖

tailscale set --accept-dns=false

图形界面等价操作:Tailscale 菜单栏 → 设置里关掉「Override local DNS」/「Use Tailscale DNS settings」。

这个操作只影响"是不是系统默认解析器",不影响 Tailscale 设备之间用短域名互访——MagicDNS 对 .ts.net 域名的解析可以用下面的办法单独保留。

第二步:Clash 配置里补三块

rules 列表最前面加两条,让 Tailscale 自己的进程流量直接放行,不被拦截代理(这个顺带修复了另一个隐藏问题——Tailscale 自己的 WireGuard 打洞流量如果被 TUN 抢走,会导致设备间连接从直连退化成走中转服务器,延迟明显变高):

rules:
  - PROCESS-NAME,io.tailscale.ipn.macsys.network-extension,DIRECT
  - PROCESS-NAME,Tailscale,DIRECT

Linux/Windows 上进程名不同,换成你机器上 tailscaled 的实际进程名即可。

tun.route-exclude-address 追加 Tailscale 的整个网段,不让 TUN 尝试接管:

tun:
  route-exclude-address:
    - 100.64.0.0/10
    - fd7a:115c:a1e0::/48    # 换成你自己 tailnet 的 IPv6 段

dns.nameserver-policy 里把 .ts.net 域名单独指回 Tailscale 的解析器,保住短域名互访:

nameserver-policy:
  "+.ts.net": [100.100.100.100]

验证

dscacheutil -q host -a name example.com   # 应该拿到 198.18.x.x 这种干净的内部地址
curl -sS -o /dev/null -w "%{http_code}\n" https://example.com  # 应该能正常拿到内容

如果 Clash 有管理面板,还能直接看某条规则的命中计数从 0 变成 1——这是最实锤的证据:之前那条规则写得再对,也一次都没真正生效过。


改完之后,流量到底怎么走

加了这么多条例外和策略,容易把自己绕晕——到底哪条流量归谁管?拆开看其实就四条路,互不干扰:

流量类型 举例 走哪条路
普通域名解析 curldig、大部分 App 系统解析器(不再是 Tailscale)→ 被 Clash TUN 拦截 → 拿到 fake-ip → 连接时 Clash 识别出域名 → 按规则代理或直连
浏览器自带 DNS 实现 Chrome、Firefox 浏览器自己解析,不问系统 → 如果走的是明文 UDP:53,一样会被 Clash 拦截识别;如果走加密 DoH,Clash 拿不到域名,只能退化成按 IP 或兜底规则处理(我自己也没有百分百确认某个具体浏览器版本走的是哪种,行为可能因版本而异,抓包能验证)
Tailscale 设备互访 访问 xxx.ts.net、内网短域名 单独配的 nameserver-policy 指回 Tailscale 解析器 → 拿到真实的 100.x.x.x 地址 → 因为这个网段被排除在外,不进 Clash 的 TUN,直接走 Tailscale 自己的网卡
Tailscale 自己的控制流量 tailscaled 进程连它的登录/中转服务器 PROCESS-NAME 规则直接放行,不进代理链

用两个具体例子过一遍,比表格直观:

例一:curl https://example.com

  1. curl 调系统的解析函数,问默认解析器——这时候已经不是 Tailscale 了,问的是普通 DNS(比如 Wi-Fi 配的那个)
  2. 这个查询包刚要发出去,就被 Clash TUN 的 dns-hijack 逮住,重定向给 Clash 自己内置的 DNS 服务器
  3. Clash 回一个 198.18.x.x 这种内部地址(fake-ip),curl 以为这就是 example.com 的真实 IP,其实只是个占位符
  4. curl 拿着这个地址发起连接,这条连接同样会经过 Clash 的 TUN 网卡;这时候 Clash 认出"这个 fake-ip 对应的就是 example.com"(因为是它自己刚发出去的),于是查规则表决定走哪个代理节点、还是直连
  5. 走了代理,最终从代理节点连到 example.com 的真实服务器——curl 全程不知道中间发生了这些,以为自己一直在跟 example.com 说话

例二:ping 另一台 Tailscale 设备

  1. 已经是 100.x.x.x 这种真实 IP,不需要经过上面的 DNS 那一步
  2. 这个网段被明确排除在 Clash TUN 的路由接管范围之外
  3. 系统路由表直接把包送去 Tailscale 自己的网卡,Clash 从头到尾都没看到这个包
  4. 剩下的交给 Tailscale 自己处理:能直连就直连,不能就退化成走它自己的中转服务器

两条路径唯一的交集就是"域名解析"这一步——普通域名交给 Clash,.ts.net 域名单独指回 Tailscale,剩下实际的连接各走各的网卡。Docker 容器内部又是完全独立的一套(跑在自己的虚拟机里,有自己的解析配置),不在这篇讨论范围内,遇到问题得单独排查。


和 Cloudflare Tunnel / 内网互访的关系

这个问题只在同时用 Tailscale 组网 + Clash 类工具做代理这个具体组合下出现。如果你只是想让家里的服务能被访问,用 Tailscale 或者 Cloudflare Tunnel 单独跑,不会碰到这个坑。

→ 见《Tailscale:五分钟组建私有局域网》 → 见《Cloudflare Tunnel:免费内网穿透


常见问题

只用 Surfshark 之类的普通 VPN,会有同样问题吗? 一般不会。普通 VPN 客户端通常不会把自己注册成"无限定的默认 DNS 解析器",只有像 Tailscale 这种带 MagicDNS/内部域名解析功能的组网工具才会主动抢这个位置。

关了 Tailscale 的 DNS 覆盖,会不会哪天升级又被自动打开? 可能。这是 App 层面的设置,跟随版本更新有一定概率被重置,建议出问题时第一步就去检查这个开关的状态。

Windows / Linux 上原理一样吗? 解析优先级和最长前缀匹配路由是操作系统级别的通用规则,思路一致,具体命令和进程名要换成对应平台的写法。