Clash Verge TUN Mode Breaks All Internet Access: Hysteria2 Traffic Loops Back Into the TUN, and Tailscale Hijacks DNS
Symptoms
Setup: Mac mini, macOS 26.3.1; Clash Verge Rev 2.5.6 with the mihomo v1.19.31 core, running as root through "service mode". The subscription has 7 self-hosted nodes (5 Hysteria2, 2 VLESS-REALITY), global mode, with 美国-DMIT-Hysteria2 (a US Hysteria2 node on DMIT) selected. Tailscale is also running on this machine.
- With only "system proxy" on, the browser and
curl -x http://127.0.0.1:7897work fine. - After turning on "virtual network adapter mode (TUN)" in settings, the switch turns on without any error, but no website loads, not even Baidu.
"Even Chinese sites fail" is the key detail. If DNS poisoning were the only problem, domestic sites would still work. With both domestic and foreign sites down, nothing going through the TUN is getting out.
Debugging tool: talk to the mihomo core's API directly
The Clash Verge GUI only shows switch states, so debugging means asking the core directly. In service mode, the core exposes its control interface on a unix socket that the current user can access (the secret is in config.yaml in the Clash Verge directory):
S=/var/run/clash-verge-service/users/501/verge-mihomo.sock
A="Authorization: Bearer set-your-secret"
curl -s --unix-socket $S -H "$A" http://x/version
# {"meta":true,"version":"v1.19.31"}
# Live logs at debug level; keep this running in another terminal
curl -sN --unix-socket $S -H "$A" "http://x/logs?level=debug"
# Toggle TUN without touching the GUI
curl -s --unix-socket $S -H "$A" -X PATCH http://x/configs -d '{"tun":{"enable":true}}'
Every experiment below uses these three commands: turn TUN on, watch the log, test connectivity with curl --noproxy '*' (bypassing the system proxy so the traffic really goes through the TUN), then turn it off again.
Step 1: is the TUN itself up?
$ ifconfig utun5 | grep inet
inet 198.18.0.1 --> 198.18.0.1 netmask 0xfffffffc
$ netstat -rn -f inet | grep utun5 | head -3
1 198.18.0.1 UGSc utun5
2/7 198.18.0.1 UGSc utun5
4/6 198.18.0.1 UGSc utun5
The interface exists, and auto-route has covered the default route with split routes (1/8, 2/7, 4/6…). The core log looks normal too:
[TUN] default interface changed by monitor, => en1
[TUN] Tun adapter listening at: utun5([198.18.0.1/30],[]), mtu: 9000, auto route: true, auto redir: false, ip stack: gVisor
But every connectivity test fails:
https://www.google.com 000 8.002101s ip=31.13.92.37
https://www.baidu.com 000 8.005689s ip=103.235.47.188
So the TUN itself is fine; the problem is what happens after traffic enters it.
Step 2: a suspicious connection in the log, the proxy proxying itself
Next, the connection log after TUN is turned on:
[UDP] 198.18.0.1:55638 --> 179.253.x.x:443 using GLOBAL
[UDP] 198.18.0.1:56470 --> 163.7.x.x:8488 using GLOBAL
[UDP] 198.18.0.1:54918 --> 36.50.x.x:8488 using GLOBAL
[TCP] 198.18.0.1:57612 --> 103.235.47.188:443 using GLOBAL
The first three destinations are the Hysteria2 nodes themselves (:443 is the DMIT Hysteria2, :8488 the other two). The only thing on this machine that sends UDP to those addresses is mihomo itself. The source address is 198.18.0.1, meaning these packets came in through the TUN adapter.
In other words: mihomo wants to reach a Hysteria2 node → the UDP packet isn't bound to the physical interface en1 → the routing table sends it into utun5 → mihomo receives it from the TUN and, per the GLOBAL rule, hands it to Hysteria2 again → back into the TUN… It's a loop, and no connection through Hysteria2 can ever be established.
Step 3: confirm it's specific to Hysteria2 (UDP)
With TUN on, test the delay of nodes using different protocols:
$ curl -s --unix-socket $S -H "$A" "http://x/proxies/<node name>/delay?url=http://www.gstatic.com/generate_204&timeout=5000"
美国-DMIT-VLESS {"delay":156}
日本-Hysteria2 {"message":"Timeout"}
(The two nodes are a US VLESS node and a Japan Hysteria2 node.) With TUN off, they measure 156 ms and 66 ms, both fine. Comparing logs: every VLESS outbound prints this line, and Hysteria2 never does:
[TUN] Auto detect interface for 179.253.x.x --> en1
auto-detect-interface: true is meant to bind the core's own outbound connections to the physical interface so they don't loop back into the TUN. From what I observed, it works for VLESS (TCP) outbounds and not for Hysteria2 (QUIC/UDP) outbounds. There is a similar report on GitHub (MetaCubeX/mihomo#1222, "after enabling Tun mode, Hysteria and Hysteria2 nodes can't connect") with no official conclusion. I didn't dig into the core's source to confirm, so this describes observed behavior only.
Switching the global node to VLESS and testing again:
TUN+VLESS https://www.baidu.com 200 2.03s ip=103.235.46.96
TUN+VLESS https://www.google.com 000 5.49s ip=31.13.92.37
TUN+VLESS https://www.youtube.com 000 6.03s ip=174.132.167.252
Baidu works, so the loop really is a Hysteria2 problem. But Google and YouTube still fail, and the resolved IPs are wrong: 31.13.92.37 is a Facebook address, a classic DNS-poisoning result. So there's a second problem.
Step 4: why isn't Clash handling DNS?
The TUN config has dns-hijack: [any:53], so every query on port 53 should be intercepted by mihomo. Here's what DNS the system is actually using:
$ scutil --dns | grep "nameserver\[0\]" | sort | uniq -c
3 nameserver[0] : 100.100.100.100
2 nameserver[0] : 240e:306:2085:bd00:42b0:76ff:fec3:f948
$ netstat -rn -f inet | grep -E "100.64|100.100.100.100/32"
100.64/10 link#23 UCS utun4
100.100.100.100/32 link#23 UCS utun4
Tailscale's MagicDNS (100.100.100.100) has taken over system DNS, and that address has a dedicated route to Tailscale's own adapter, utun4. It's more specific than Clash's split routes, so DNS queries go straight into Tailscale and never pass through utun5, which is why dns-hijack never sees them. During the whole investigation, [DNS] hijack appeared in the core log exactly once, triggered by my own manual dig @198.18.0.2.
Tailscale forwards the queries upstream (router / ISP DNS) and gets poisoned IPs back. Inside the TUN, mihomo only sees "connect to 31.13.92.37:443" with no idea of the real domain, so it has the proxy node connect to a Facebook IP while the TLS handshake's SNI says google.com. That can only fail.
Also, Clash Verge changes the system DNS when toggling TUN (the log shows set system dns successfully), but Tailscale's resolver has higher priority in scutil, so that doesn't help either. At the time, the runtime config had no dns section at all ("DNS override" was off and the subscription didn't define one), and no sniffer section.
This site has an earlier piece, Tailscale took over DNS and left the proxy box with no upstream. That time the server side broke; this time it's the client. Both are the same kind of problem: once Tailscale is installed, DNS doesn't go where you think it goes.
The fix
The two problems get separate fixes, and both go into Clash Verge's subscription Merge override (right-click the subscription → "Edit override", or edit that subscription's merge file under profiles/ directly). That way the subscription itself is untouched and updates won't overwrite the changes.
1. Exclude node IPs from the TUN routes
tun:
route-exclude-address:
- 179.253.x.x/32 # one line per node server IP
- 163.7.x.x/32
- 36.50.x.x/32
With this on, the split routes in the routing table skip these addresses (179.253.232/26, 179.253.232.65/32… with exactly the node's IP missing), so packets to the nodes take the default route out of en1 and never enter the TUN. This fix doesn't depend on the core binding the interface correctly; it cuts the loop at the routing layer.
The cost: when a node changes IP, you have to update this list. If a node is configured by domain name, enter its resolved IP.
2. Turn on sniffer to recover domains from SNI
sniffer:
enable: true
parse-pure-ip: true # also sniff connections whose destination is a bare IP
force-dns-mapping: true
override-destination: true # replace the destination IP with the sniffed domain
sniff:
HTTP:
ports: [80, 8080-8880]
TLS:
ports: [443, 8443]
QUIC:
ports: [443, 8443]
It doesn't matter that DNS bypasses Clash: when the browser opens a TLS connection to the poisoned IP, the SNI in its ClientHello still carries the real domain. sniffer reads it, replaces the destination, and lets the proxy node resolve it remotely.
I also tested sniffer combined with DNS override (fake-ip), and everything worked as well. But with Tailscale owning DNS, fake-ip never actually receives the system's queries; sniffer is what does the work. So I kept only the two sections above.
Another approach: stop Tailscale from managing DNS
tailscale set --accept-dns=false returns system DNS to the router, so queries go through the TUN again and get hijacked. The cost is losing MagicDNS hostname resolution. All the SSH configs on this machine use IPs, so in theory I could turn it off, but the sniffer approach leaves Tailscale alone and touches less, so I didn't use this one.
Verification
Using the runtime config Clash Verge regenerated after a restart (clash-verge.yaml now includes both sections), with TUN on, the global node still on Hysteria2, and --noproxy '*' on every request:
https://www.google.com 200 0.65s
https://www.youtube.com 200 1.66s
https://github.com 200 1.48s
https://api.anthropic.com 403 1.10s # the root path returns 403 anyway, so it's reachable
https://www.baidu.com 200 1.70s
exit ip country US AS906 DMIT Cloud Services
I also confirmed Tailscale was unaffected: ssh winmini (a Windows machine on the LAN) returned its hostname normally, and 100.100.100.100:53 was reachable. That's because Tailscale's 100.64/10 route is more specific than the TUN's split routes, so its traffic never enters the TUN in the first place.
Checklist: no internet after turning on Clash TUN
- Do domestic sites load? If they don't either, traffic isn't getting out at all (loop, outbound problem). If only foreign sites fail, suspect DNS first.
- Look for "the proxy connecting to itself" in the core log:
198.18.0.1 --> <node IP>:<node port>in the TUN connection log means a loop. - With TUN on, test node delay per protocol: if only UDP-based protocols (Hysteria2 / TUIC) time out, exclude the node IPs with
route-exclude-address. - Check which DNS the system really uses with
scutil --dns: if it's 100.100.100.100 (Tailscale) or another VPN's address, DNS isn't going through the TUN and you need sniffer. - Check whether the runtime config has
dnsandsniffersections: Clash Verge's runtime file is~/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml. If they're missing, the GUI switches never merged them in.