适用环境

  • Arch Linux,用 NetworkManager 开 Wi-Fi 热点(ipv4.method shared);
  • Clash Verge Rev,内核为 Mihomo(verge-mihomo),已开启 TUN 模式与 auto-route;
  • 热点客户端能连上、能拿到 IP,但打不开任何网站;
  • 关掉 TUN 后客户端立刻恢复。

客户端能连上但打不开网站这一条不成立时,先看「现象对照」确认不是别的问题。

先说结论

热点客户端打不开网页,但主机自己上网正常,绝大多数情况是这一件事:

客户端拿到的 DNS 答案是 198.18.x.x 这类假地址,只有 Mihomo 能解释它;而客户端的流量被配置成不进 Mihomo,于是这些假地址无人认领,连接直接超时。

所以不是 NAT 或防火墙坏了,是”域名解析的结果”和”流量实际走的路”对不上。

先做两件事确定自己属于哪种情况,都在主机上运行。

第一,确认热点 DNS 返回的确实是假地址。192.168.155.1 是占位网关,用 ip -br addr 里热点接口上的地址替换:

sudo pacman -S bind           # 只为拿到 dig,已装可跳过
dig @192.168.155.1 not-exist-abc123.example.com A +short

有输出(且落在 198.18.0.0/15)就说明是 Fake-IP 问题。真实解析器对这个不存在的域名只会返回空。

第二,看 dnsmasq 的上游服务器是谁:

journalctl -b -u NetworkManager --no-pager | grep -E 'dnsmasq.*nameserver'
输出中的上游服务器 你的情况 做法
路由器地址,例如 192.168.50.1 常见情况,Mihomo 直接接管了 dnsmasq 的查询 「修复一」
100.100.100.100、127.0.0.1、127.0.0.53 等本机地址 dnsmasq 的上游是 Tailscale、AdGuard Home、systemd-resolved 这类本机解析器,换成哪个 UID 都会被 TUN 接管 「修复二」,并保留「修复一」

多数人执行「修复一」就够了。「修复二」解决的是:”两条排除规则都写了,dig 还是返回 198.18.x.x“。

三个概念,够用就行

用户只需要理解下面三件事,其余细节可以跳过。

一、TUN 是一个”假的网卡”

开启 TUN 后,Mihomo 会创建一张名为 Mihomo 的虚拟网卡。它同时做两件事:

  1. 给系统加一条路由规则:符合条件的流量交给这张假网卡;
  2. 在假网卡上接住这些流量,按代理规则转发出去。

所以”流量进了 TUN”的意思是:这段流量经过 Mihomo 处理,而不是从物理网卡直接出去。

二、Fake-IP 是一个”代收点编号”

Mihomo 默认用 Fake-IP 方式回答 DNS 查询。你问 example.com 的地址,它不回真实公网 IP,而回一个保留地址,例如 198.18.0.119。

这个地址不是真实服务器地址,路由器也不认识它。它的作用只是”编号”:当流量连到 198.18.0.119 时,Mihomo 从 TUN 中接住,查表得知这是 example.com,再按规则转发。

因此有一条硬性对应关系:

Fake-IP 只有经 Mihomo 才能用。流量绕过 Mihomo,Fake-IP 就是一个死地址。

三、两条排除规则各管一半

tun: 配置里两项排除,作用对象完全不同:

配置 排除的对象 相当于
exclude-interface: wlp2s0 从热点网卡进来的转发流量 客户端的网页、游戏、下载
exclude-uid: 65534 以该 UID 运行的本机进程发出的流量 热点用的 dnsmasq 去问上游 DNS

只写第一条时,客户端的数据不走 TUN,但 dnsmasq 的 DNS 查询仍可能进 TUN,于是客户端从 dnsmasq 那里拿到 Fake-IP 假地址。这就是经典的”能 ping 通 IP,打不开网页”。

现象对照

符合以下全部条件,就是本文的问题:

  1. 主机自己上网正常;
  2. 热点客户端能连上 Wi-Fi,并通过 DHCP 拿到 192.168.155.x 地址;
  3. 客户端 ping 1.1.1.1 成功,但浏览器打不开任何网站;
  4. 关掉 Clash Verge 的 TUN 模式,客户端立刻恢复正常。

第 3 条是区分点:

  • 能 ping IP,不能打开域名 → 是本文的 DNS 与路径不一致问题;
  • IP 也 ping 不通 → 是转发或 NAT 的问题,看「排查步骤」第 3 步。

为什么会出现

正确状态与故障状态的区别只在一行 DNS:

正常:
热点客户端
  -> 192.168.155.1:53(dnsmasq)
  -> 路由器 192.168.50.1
  -> 真实公网 IP
  -> 客户端用这个真实 IP 经 enp3s0 出去   ✓ 对得上

故障:
热点客户端
  -> 192.168.155.1:53(dnsmasq)
  -> Mihomo TUN
  -> Fake-IP 198.18.1.16
  -> 客户端拿假地址去连,但流量被 exclude-interface 排除,不经 Mihomo
  -> 假地址无人处理                          ✗ 超时

一句话:解析走了一条路,数据走另一条路。两条路必须在同一个地方汇合,否则 Fake-IP 无人认领。

排查步骤

下表中的值全是占位示例,不是本机真实值。用第 1 步的命令查出自己的值再替换,不要照抄:

项目 占位示例
热点连接名称 Hotspot
热点无线接口 wlp2s0
上网接口 enp3s0
热点网段 192.168.155.0/24
热点网关 192.168.155.1
路由器地址 192.168.50.1

热点网段与网关的默认值由 NetworkManager 决定,直接看 ip -br addr show <热点接口> 的输出即可,不需要记任何固定数字。

1. 确认接口名

nmcli device status
ip -br addr

记下热点用的无线接口(ip -br addr 里地址形如 <热点网关>/24 的那张)和实际上网接口(地址形如 192.168.x.x 或 10.x.x.x 的那张)。

2. 检查客户端流量从哪张网卡出去

把 192.168.155.101 换成一个真实在线客户端的地址(ip neigh show dev wlp2s0 可以看到):

ip route get 1.1.1.1 from 192.168.155.101 iif wlp2s0

正确结果类似:

1.1.1.1 from 192.168.155.101 via 192.168.50.1 dev enp3s0

关键是 dev enp3s0。如果显示 dev Mihomo,说明排除接口没生效,或者改的不是当前生效的配置。

3. 检查 NetworkManager 的 NAT 与转发规则

sudo nft list ruleset | grep -A20 nm-shared-wlp2s0

正常时能看到这一表里的两条规则:masquerade(做地址转换)和 filter_forward(允许转发)。有这两条,且第 2 步路由正确,就说明转发和 NAT 没问题,问题在 DNS。

顺带确认转发开关。NetworkManager 共享模式开的是按接口的转发,全局开关可以仍是 0:

sysctl net.ipv4.conf.wlp2s0.forwarding net.ipv4.conf.enp3s0.forwarding
# 两张都要是 1

sysctl net.ipv4.ip_forward
# 0 也正常,不要因为这里为 0 就判定转发被关闭

4. 确认 dnsmasq 的 UID

ps -eo user,uid,pid,args | grep '[d]nsmasq'

输出第二列就是 UID,Arch 上通常是 nobody / 65534。必须用实际输出,不要照抄。

5. 直接问热点 DNS,看它回什么

sudo pacman -S bind        # 只为拿到 dig,已装可跳过
dig @192.168.155.1 example.com A +short
dig @192.168.155.1 not-exist-abc123.example.com A +short

两种结果的含义:

  • 返回 198.18.x.x → 就是本文的问题;
  • 第二条命令(不存在的域名)也返回地址 → 100% 是 Fake-IP,真实解析器对不存在的域名会回空。

6. 判断 dnsmasq 的上游是否被别的程序接管

这一步是排查「两条排除规则都写了,dig 仍然返回 198.18.x.x」时必须做的。

journalctl -b -u NetworkManager --no-pager | grep -E 'dnsmasq.*nameserver'
cat /etc/resolv.conf

如果日志显示:

dnsmasq[12345]: using nameserver 100.100.100.100#53

说明 dnsmasq 上游是 Tailscale 的 MagicDNS,不是路由器。这时 exclude-uid 不起作用,原因见下一节。

为什么有时写了 exclude-uid 也没用

exclude-uid 不是 Mihomo 内部判断,而是写进系统 ip rule 的一条规则。它排在第 9000 位,前面还有别的规则先做决定:

$ ip rule
  5270:  from all lookup 52                        ← Tailscale 自己的路由表
  9000:  from all uidrange 65534-65534 goto 9010   ← exclude-uid,在这里
  9003:  not from all dport 53 lookup main suppress_prefixlength 0
  9004:  not from all iif lo lookup 2022           ← 进 TUN

dnsmasq 的查询发往 100.100.100.100(Tailscale 的解析器)时会先命中 5270:

$ ip route show table 52
100.100.100.100 dev tailscale0

表 52 里有这条路由,包就直接交给 Tailscale 处理,规则 9000 根本没有机会执行。所以 exclude-uid 拦不住它 —— 不是因为 UID 写错了,而是这个包本来就不是去路由器的。

接下来是真正的故障点:tailscaled 收到查询后,自己再去问上游 192.168.50.1:53。这个包由 tailscaled 发出,UID 是 0,与 65534 无关,于是走到 9004 被送进 TUN。

阶段 发包进程 UID 命中规则 结果
dnsmasq → 100.100.100.100 dnsmasq 65534 5270 表 52 交给 tailscaled,不进 TUN
tailscaled → 192.168.50.1:53 tailscaled 0 9004 表 2022 进 TUN,被劫持成 Fake-IP

验证方法:打开 Clash Verge Rev 的「日志」页(或 GET /logs?level=debug),同时执行一次 dig @192.168.155.1 example.com,日志里会出现:

[DNS] hijack udp:192.168.50.1:53 from 198.18.0.1:42824

192.168.50.1:53 是被劫持的目标,198.18.0.1 是 TUN 自己的地址。来源是 TUN 地址而不是 192.168.155.x 热点网段,说明这个 DNS 请求由主机本地进程发出,不是客户端直接发的。

结论:只要 dnsmasq 的上游是本机解析器(Tailscale、AdGuard Home、systemd-resolved 都算),exclude-uid 就无效,因为最后真正发出查询的是那个解析器进程。正确做法是让热点 dnsmasq 直接问路由器。

修复一:Merge 配置加两条排除规则

适用于「dnsmasq 上游是路由器」的情况。这是最小改动。

1. 在 Clash Verge Rev 里编辑当前订阅的 Merge 配置

打开 Clash Verge Rev → 配置 → 当前正在使用的订阅 → 编辑 Merge / Merge 配置 / 合并配置(不同版本叫法不同)。

不要直接改下面这个文件,它由程序生成,重启内核或切换配置后会被覆盖:

~/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml

在已有的 tun: 块里追加两项。不要新建第二个 tun: 块。

tun:
  exclude-interface:
    - wlp2s0
  exclude-uid:
    - 65534

替换规则:

  • wlp2s0 → 第 1 步查到的热点接口;
  • 65534 → 第 4 步查到的 dnsmasq UID 实际数字。

原有的 enable、auto-route、dns-hijack、stack 等保持不变,这里只是补两项。

2. 重启内核,让客户端重连

保存后在 Clash Verge Rev 里重启 Mihomo 内核,然后让客户端断开 Wi-Fi 再连。重连的目的是清掉客户端自己缓存的旧 DNS 结果。

检查生成的运行配置是否包含这两项:

rg -n -C 3 'exclude-interface|exclude-uid' \
  ~/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml

Flatpak 等非标准安装的路径不同,以应用界面显示的生效配置为准。

修复二:让热点 dnsmasq 直接问路由器

适用于「dnsmasq 上游被 Tailscale / AdGuard Home / systemd-resolved 接管」的情况。修复一的排除规则要保留,两件事一起做才完整:

  • Merge 里的 exclude-interface / exclude-uid 负责让流量不进 TUN;
  • 下面这一项负责让DNS 答案是真实的公网 IP。

NetworkManager 启动热点 dnsmasq 时已经带了 --conf-dir=/etc/NetworkManager/dnsmasq-shared.d,往这个目录放一个文件即可,不需要改 NetworkManager 本身:

sudo mkdir -p /etc/NetworkManager/dnsmasq-shared.d
sudo tee /etc/NetworkManager/dnsmasq-shared.d/hotspot-upstream.conf >/dev/null <<'EOF'
# 热点专用 dnsmasq:忽略 /etc/resolv.conf,直接问路由器
no-resolv
server=192.168.50.1
EOF

把 192.168.50.1 换成你的路由器地址(ip route | grep default 里 via 后面那个)。

再关掉热点的 IPv6 共享。主机没有可分配的公网 IPv6 前缀时,客户端会优先用 IPv6 并拿到一条坏路由:

nmcli connection modify Hotspot ipv6.method disabled
nmcli connection down Hotspot
nmcli connection up Hotspot

新增的 hotspot-upstream.conf 只被热点专用的那个 dnsmasq 读取,不影响主机自己用 Tailscale MagicDNS,也不影响 Mihomo 的 Fake-IP 工作方式;nmcli connection modify 只改 Hotspot 这一个连接。

改完确认 dnsmasq 换了上游:

journalctl -b -u NetworkManager --no-pager | grep 'using nameserver' | tail -5
# 应只剩 192.168.50.1,不再出现 100.100.100.100

若这里仍显示 100.100.100.100,用 ps -eo args | grep '[d]nsmasq' 确认命令行里确实带 --conf-dir=/etc/NetworkManager/dnsmasq-shared.d。另外注意该目录下所有文件都会被当成配置读取,只有以 . 开头、以 ~ 结尾、或以 # 开头结尾的文件会被跳过;所以这个目录里不要放备份文件或其他内容。

no-resolv 的含义是”不要读 /etc/resolv.conf“,所以热点客户端从此只认 server= 指定的这一台上游。副作用:热点客户端无法解析 Tailscale 的 *.ts.net 名字。要访问 tailnet 内的机器,直接用它的 100.x.y.z 地址即可,这类请求不经过 DNS。

验证

按顺序做,能区分是”转发坏了”还是”DNS 坏了”:

# 1) 客户端上:IP 通不通
ping -c 3 1.1.1.1

# 2) 主机上:热点 DNS 是否还回假地址
dig @192.168.155.1 example.com A +short
#    必须是真实公网 IP,不能在 198.18.0.0/15 内

# 3) 主机上:不存在的域名应该没有答案
dig @192.168.155.1 not-exist-abc123.example.com A +short
#    应为空。有地址说明还是 Fake-IP

# 4) 客户端上:访问 https://example.com

# 5) 主机上:确认流量仍走物理网卡
ip route get 1.1.1.1 from 192.168.155.101 iif wlp2s0
#    应为 dev enp3s0

第 1 步通、第 2 步仍返回 198.18.x.x,按顺序排查三处:

  1. dnsmasq 的配置没生效 —— 重跑修复二里的 using nameserver 检查;
  2. 配置生效了但仍返回假地址 —— 说明 dnsmasq 发出的查询被 TUN 劫持,exclude-uid 没生效,重跑排查步骤第 4 步核对 UID;
  3. server= 后面写的地址不对 —— 用 ip route | grep default 确认网关。

常见异常

现象 原因 处理
路由结果显示 dev Mihomo 接口名写错,或 Merge 未生效 重查 ip -br addr,确认当前订阅,重启内核
dig 仍返回 198.18.x.x dnsmasq UID 与本机不符,或上游被别的解析器接管 重跑第 4、6 步;按修复二写 dnsmasq 配置
能 ping IP,域名全废 DNS 答案仍是 Fake-IP 同上
IP 也 ping 不通 不是本文问题 查 nft 规则、上网接口、sysctl net.ipv4.conf.wlp2s0.forwarding
关掉 TUN 才正常,开着就全废 默认排除配置未合并 确认 Merge 配置里的 tun: 块确实写入了
客户端偶发打不开部分网站 热点开了 IPv6 但上游没有可共享前缀 按修复二关闭热点 IPv6
客户端上装了自己的代理 App 两层代理叠加(客户端 VPN + 主机 TUN),现象互相矛盾 排查时先在客户端退出代理 App

最后一条值得单独说明:手机上开着 Clash 类 App 时,它会建立自己的 VPN 和默认路由。此时客户端”能 ping 通 1.1.1.1”可能只证明主机转发正常,而网页打不开是客户端自己的 Fake-IP 造成的。排查前先把客户端上的代理关掉,否则会在错误的方向上折腾很久。

exclude-uid 的影响范围

exclude-uid: 65534 不只匹配 DNS 端口,它让所有以该 UID 运行的本机进程都绕过 TUN。先确认这个 UID 上还有什么:

ps -eo user=,uid=,comm= | awk '$2 == 65534'

通常只有 dnsmasq 这类低权限服务。若有其他需要走代理的进程也在用这个 UID,需要重新评估。对 NetworkManager 的热点 dnsmasq 而言,这个范围很小,是安全的。

想让热点设备也走代理

本文的方案是让热点设备绕过代理,直连路由器。如果目标是让热点设备的流量也经过 Clash,处理方式相反:

删掉 Merge 里的 exclude-interface 和 exclude-uid,让客户端流量进 TUN。此时 Fake-IP 与数据路径重新一致,不需要改 DNS。

代价与注意事项:

  • 热点设备的全部流量都会消耗代理订阅流量;
  • Mihomo 需要能正确识别热点流量的来源,必要时配合 dns-hijack 与路由标记;
  • 客户端自行设置的代理或 VPN 会与主机 TUN 冲突,应先排除。

不要只删 exclude-interface 而保留 exclude-uid,也不要在没改路由的情况下单独改 DNS,两者都会重新造出”解析与路径不一致”。

改完按同一套命令确认客户端流量确实进了 TUN:

ip route get 1.1.1.1 from 192.168.155.101 iif wlp2s0
# 此时应为 dev Mihomo,而不是 dev enp3s0

dig @192.168.155.1 example.com A +short
# 此时返回 198.18.x.x 是正常的,客户端流量会经 TUN 解释这个地址

为什么 Windows 上可能没这个问题

Windows 的 Internet Connection Sharing、DNS 代理和 TUN 驱动与 Arch 上的 NetworkManager + dnsmasq + Mihomo 不是同一套实现。Windows 的热点 DNS 服务可能根本不经过 TUN,或 TUN 驱动对共享流量的处理不同。

结论:不要照搬其他系统的结论,按本文排查步骤实际看数据走哪张网卡。

附:一次完整排查记录

下面是主机同时使用 Tailscale(接管 /etc/resolv.conf)时的输出,可作为对照。网段、网关、客户端地址、PID 均已替换为占位值,方法结论不受影响。

1. 转发路径正常,客户端流量走物理网卡

$ ip route get 1.1.1.1 from 192.168.155.101 iif wlp2s0
1.1.1.1 from 192.168.155.101 via 192.168.50.1 dev enp3s0

2. 客户端拿到的是 Fake-IP,即使域名根本不存在

$ dig @192.168.155.1 www.example.com A +short
198.18.1.16
$ dig @192.168.155.1 not-exist-abc123.example.com A +short
198.18.1.26

不存在的域名也有答案,说明回答者是 Mihomo,不是路由器。

3. 但客户端流量并不进 TUN,于是假地址无人处理

$ ip route get 198.18.1.26 from 192.168.155.101 iif wlp2s0
198.18.1.26 from 192.168.155.101 via 192.168.50.1 dev enp3s0

4. dnsmasq 的 UID 是 65534,排除规则也写了

$ ps -eo user,uid,pid,args | grep '[d]nsmasq'
nobody  65534  12345  /usr/bin/dnsmasq --conf-file=/dev/null --no-hosts ... --conf-dir=/etc/NetworkManager/dnsmasq-shared.d

5. 真正的原因:dnsmasq 的上游是本机 Tailscale

$ journalctl -b -u NetworkManager | grep -E 'dnsmasq.*nameserver'
dnsmasq[12345]: using nameserver 100.100.100.100#53
dnsmasq[12345]: using nameserver fd7a:115c:a1e0::53#53

/etc/resolv.conf 被 tailscaled 改写,NetworkManager 交给 dnsmasq 的上游就是 Tailscale,不是 192.168.50.1。

6. 路由规则里 Tailscale 的表排在 UID 排除之前

$ ip rule
5270:  from all lookup 52
9000:  from all uidrange 65534-65534 goto 9010
9003:  not from all dport 53 lookup main suppress_prefixlength 0
9004:  not from all iif lo lookup 2022

7. Mihomo 日志确认劫持来自 TUN 自身地址

[DNS] hijack udp:192.168.50.1:53 from 198.18.0.1:42824

来源是 198.18.0.1(TUN 地址),不是 192.168.155.101(客户端),证明这个查询是主机本地进程发出的,且没被 UID 规则放行。

结论与处理:按「修复二」给热点 dnsmasq 指定 server=192.168.50.1,脱离 Tailscale;「修复一」的排除规则保留。修复后第 2 步的第二个命令应返回空。

8. 单独验证 dnsmasq 配置本身可用

在落地前,可以先起一个临时 dnsmasq 实例确认配置文件格式正确:

T=$(mktemp -d)
printf 'no-resolv\nserver=192.168.50.1\n' > $T/hotspot-upstream.conf
dnsmasq --conf-file=/dev/null --no-hosts --listen-address=127.0.0.2 --port=5399 \
        --conf-dir=$T --log-facility=-

启动日志会打出:

dnsmasq[54321]: using nameserver 192.168.50.1#53

说明 no-resolv 生效,上游已切换。此时向它查询仍会得到 198.18.x.x,原因是这个临时实例以当前登录用户(UID 1000)运行,不在 exclude-uid 的保护范围内,它的查询照样被 TUN 劫持。这恰好说明 exclude-uid 不能删。

清理:kill 掉该进程后 rm -rf $T。

参考资料