feat(mihomo): answer AAAA and keep the tailnet off the TUN

fake-ip needs one pool per family, and the family a client picks does not pick the
egress family. Exclude the cluster ULA and the Tailscale ranges from the TUN; the ssh
pin and the WA-028 record cover the fake-AAAA stall.
This commit is contained in:
Ryan Yin committed 2026-10-09 18:34:04 +08:00
1 parent 9cf0a92a30
commit 1e6702db2c
7 files changed
+48 -26

No files matched your search

+3 -1
View File
@@ -34,7 +34,7 @@ A version or revision pinned for a reason that can go away.
A setting that works around a bug or a hardware quirk.
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
| ------ | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------ | ------ |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------ | ------ |
| WA-008 | Relaxed NVMe APST (`nvme_core.default_ps_max_latency_us=100000`) — `hosts/idols-ai/hardware-configuration.nix` | the KINGBANK KP260 (DRAM-less QLC) froze on slow APST wake-ups | Drop the override once kernel/firmware fixes or replacement hardware make default APST reliable | 2026-10-03 | watch NVMe timeout/reset/AER in the journal | active |
| WA-009 | USB-SATA bridge mitigations (`usbcore.autosuspend=-1`, `usb-storage.delay_use=10`) — `hosts/12kingdoms-youko/default.nix` | flaky JMicron JMS567 resets with UDMA-CRC and I/O errors | Drop once the bridge/controller is replaced; the signal is smartctl CRC/media errors | 2026-09-21 | on hardware change or CRC errors | active |
| WA-010 | Insta360 Link USB reset on S3 resume — `hosts/idols-ai/default.nix` | the camera stays enumerated with a stalled UVC endpoint after resume | Drop after the Insta360 firmware or the kernel `uvcvideo` fixes it | 2026-08-01 | on kernel or firmware change | active |
@@ -43,6 +43,8 @@ A setting that works around a bug or a hardware quirk.
| WA-018 | user-owned `~/.ssh/config` + disabled systemd ssh-proxy include — `home/base/tui/ssh.nix`, `modules/nixos/base/ssh.nix` | bwrap exposes root-owned store files as `nobody` | Drop once sandboxes preserve store ownership | 2026-10-06 | when sandbox layout changes | active |
| WA-019 | programs.steam.extraCompatPackages = [ pkgs.dwproton-bin pkgs.proton-ge-bin ] — modules/nixos/desktop/gaming.nix | ACE games fail on GE/mainline Proton, work with DW-Proton | Drop once mainline or GE-Proton runs these games (GE-Proton#433) | 2026-10-07 | on Proton/GE bump | active |
| WA-024 | Wi-Fi power saving disabled (`networking.networkmanager.wifi.powersave = false`) — `hosts/12kingdoms-shoukei/apple-silicon.nix` | On this BCM4378/brcmfmac laptop, powersave is a common source of intermittent ASSOC-REJECT / authentication-timeout failures; NetworkManager then mistakes the failure for a bad key, re-prompts, and the client (Noctalia, nmtui, nmcli) saves the answer as a duplicate `<SSID> 1` profile | Drop once the kernel carries the brcmfmac `port_authorized` fix (WA-025) and no association failures remain | 2026-10-07 | watch `journalctl -u wpa_supplicant` for ASSOC-REJECT / authentication timeout | active |
| WA-026 | guest deploys run `installOnHost` then `systemctl restart microvm@<guest>`, not `just microvm-deploy` — `lib/genMicrovmGuestModule.nix` | the sshSwitch step runs `nix-store --load-db` against the guest's read-only virtiofs `/nix/store` and dies on `cannot remount "/nix/store" writable` (nix 2.35) | Drop the manual path once microvm.nix or nix handles a read-only store switch | 2026-10-09 | on microvm.nix or nix bump | active |
| WA-028 | `github.com` keeps `AddressFamily = "inet"` — `home/base/tui/ssh.nix` | measured on 1.19.31 + `tun.stack: gvisor`: TCP to a fake IPv6 is proxied and SSH even authenticates (`[fdfe:dcba:9876::18]:443`), but the session then never closes, while the same run over the fake IPv4 ends cleanly. HTTPS over a fake IPv6 is fine, so only ssh needs the pin | Drop the line once `git ls-remote` with `AddressFamily=any` returns a sha instead of hanging | 2026-10-09 | on a mihomo bump, or if `tun.stack` changes | active |
If WA-008 freezes recur, the fallback is to restore
`nvme_core.default_ps_max_latency_us=0 nvme_core.io_timeout=4294967295 pcie_aspm=off`; this is a
+1 -1
View File
@@ -27,7 +27,7 @@
settings = {
"github.com" = {
# avoid mihomo fake-IP6 hang, see modules/nixos/desktop/networking/mihomo/
# WA-028: pinned to IPv4 until the mihomo v6 fake-ip pool is proven here
AddressFamily = "inet";
HostName = "ssh.github.com";
Port = 443;
@@ -43,17 +43,26 @@ targets instead of writing a config the core rejects.
## Gotchas
- `ipv6: false`: nodes without IPv6 egress + AAAA = routing black hole (WeChat/JD/Taobao images);
mihomo's IPv6 fake-ip also hung ssh/git locally for ~2 minutes, so it stays off.
- `ipv6`, `dns.ipv6`, `fake-ip-range6`: the fake v6 pool is a lookup key, so the family the client
picks never picks the egress family -- a fake-IPv6 client dials an IPv4 proxy target, and CN
services answer identically over `-6` and `-4`. What does break is a fake v6 pool on a line whose
IPv6 is dead: apps that resolve for themselves (WeChat) dial a real AAAA, stall, and do not fall
back. mihomo has no IPv6 reachability probe (mihomo#2233), so the config has to match the line.
SSH over a fake AAAA is the local exception: it connects, authenticates, then never closes --
WA-028's IPv4 pin covers it.
- `private_domains`: each entry becomes a DIRECT rule + a `fake-ip-filter` entry.
- WeChat/QQ media go over bare CDN IPs, hence `multimedia.nt.qq.com.cn` in `fake-ip-filter`. If
media still stalls: `tun_exclude_process: [wechat, WeChatAppEx, qq]` (Linux names; the fix that
won clash-verge-rev#1762).
- WeChat resolves for itself, but its queries still cross the TUN, so `dns-hijack: any:53` catches
them and the fake-ip table hands the name back to the rules: `mmbiz.qpic.cn` measured DIRECT.
`multimedia.nt.qq.com.cn` stays in `fake-ip-filter` for the same reason. The exclude-process fix
that won clash-verge-rev#1762 is not available here: `hardening/bwraps/wechat.nix` sets
`unsharePid = true` and the sandbox reports uid 0, so no process name resolves.
- `find-process-mode: off` unless `PROCESS-*` rules or `tun_exclude_process` need it.
- `tun_exclude_address`: a destination that can never be a proxy target should not pay a fake-ip
round trip -- RFC1918, RFC 6598's `100.64.0.0/10` (where Tailscale addresses come from),
link-local, multicast, reserved and documentation prefixes, `fc00::/7`, `fe80::/10`. Never exclude
`198.18.0.0/15`, the fake-ip range itself; `tun_exclude_address: []` puts everything back in TUN.
link-local, multicast, reserved and documentation prefixes, the cluster ULA `fd05:5::/64` and
Tailscale's `fd7a:115c:a1e0::/48`. Never exclude a range that holds a fake-ip pool: that is
`198.18.0.0/16` and `fdfe:dcba:9876::/64`, which is why the list spells the two ULAs out instead
of taking `fc00::/7`; `tun_exclude_address: []` puts everything back in TUN.
- Steam's download caches follow the public IP Steam sees on the login (CM) connection, so a proxied
Steam downloads from Tokyo/Singapore/HK/Los Angeles. `policy.yaml` pins the CM hosts,
`steamcontent.com` and `IP-ASN,32590` DIRECT and keeps `steamcommunity.com` on the proxy -- Steam
@@ -106,9 +115,12 @@ grep -oE 'cache[0-9]+-[a-z]{3}[0-9]+' ~/.local/share/Steam/logs/content_log.txt
## References
- [clash-verge-rev#1762](https://github.com/clash-verge-rev/clash-verge-rev/issues/1762) — WeChat
media under TUN: fake-ip-filter, qlogo/qpic, the exclude-process fix.
- [WeChat-under-TUN retrospective](https://x.com/realchendahuang/status/2104381806862795161) — the
IPv6 black hole logic; JD and Taobao break the same way.
media under TUN: fake-ip-filter, qlogo/qpic, the exclude-process fix. Its closing comment is the
one that matters: a fake v6 pool works on a line with working IPv6 and hangs on a line without.
- [mihomo#2233](https://github.com/MetaCubeX/mihomo/issues/2233) — open: the core has no IPv6
reachability probe, so `ipv6` cannot stay on for a line that lost IPv6.
- [WeChat-under-TUN retrospective](https://x.com/realchendahuang/status/2104381806862795161) — five
community fixes; "turn IPv6 off" only ever worked where the line's IPv6 was already broken.
- [mihomo wiki](https://wiki.metacubex.one/),
[proxy providers](https://wiki.metacubex.one/config/proxy-providers/) — why subscription rules are
dropped.
@@ -11,10 +11,9 @@
# The config stays OUT of the Nix store (it holds subscription URLs and the
# controller secret) and reaches the service through systemd's LoadCredential.
# This module only wires up the service; generate the file with ./generate.nu.
# See README.md for usage and the routing notes, in particular why the config
# must keep `ipv6: false`: mihomo does not proxy TCP to an IPv6 fake-IP
# (fdfe:dcba:9876::/64), so ssh/git connections hang until they time out
# (home/base/tui/ssh.nix keeps `AddressFamily inet` as a per-client fallback).
# See README.md for usage and the routing notes, in particular the IPv6
# condition: the fake-ip v6 pool needs a line whose IPv6 actually works, and
# mihomo has no reachability probe to notice when the line loses it.
let
cfg = config.modules.networking.mihomo;
configFile = "${config.users.users.${myvars.username}.home}/.config/mihomo/config.yaml";
@@ -31,7 +31,6 @@ def main [
let gw = (open --raw $tmp
| from yaml
| update ipv6 true
| update allow-lan true
| update external-controller $"($address):9090"
| insert tproxy-port 7893
@@ -40,9 +39,8 @@ def main [
| update secret "GATEWAY-SECRET-PLACEHOLDER"
| update tun.stack "mixed"
| update tun.strict-route false
| insert tun.route-address ["0.0.0.0/1", "128.0.0.0/1", "::/1", "8000::/1"]
| update dns.listen ":1053"
| update dns.ipv6 true)
# the desktop's resolver is loopback; a gateway serves the LAN instead
| update dns.listen ":1053")
let gw = (if $redact {
# a file-backed provider (localyaml) has no url to hide
@@ -496,10 +496,10 @@ def main [
let fpm = (process_mode $all_rules $excluded)
let config = {
# IPv6 stays off on purpose: most proxy nodes have no IPv6 egress, so an
# AAAA record becomes a routing black hole that apps such as WeChat, JD,
# Taobao and Feishu sit on until they fall back to IPv4.
ipv6: false
# IPv6 on. fake-ip answers AAAA from a v6 pool, so the family the client
# picks never decides the egress family: mihomo dials the proxy, the proxy
# dials IPv4. Taobao, JD, WeChat and Bilibili answer identically over -6 and -4.
ipv6: true
mode: "rule"
"mixed-port": 7897
"allow-lan": false
@@ -533,9 +533,12 @@ def main [
# answers over loopback (systemd-resolved) need their resolver taken over
# while mihomo runs, or fake-ip is bypassed entirely.
listen: "127.0.0.1:1053"
ipv6: false
ipv6: true
"enhanced-mode": "fake-ip"
"fake-ip-range": "198.18.0.1/16"
# AAAA needs a pool of its own (fake-ip-range6, mihomo >= 1.19.16); one
# prefix per family, no comma list. Keep it out of route-exclude-address.
"fake-ip-range6": "fdfe:dcba:9876::1/64"
"fake-ip-filter-mode": "blacklist"
"prefer-h3": false
"respect-rules": false
@@ -63,7 +63,8 @@ tun_exclude_address:
- 203.0.113.0/24
- 224.0.0.0/4 # multicast: mDNS, SSDP
- 240.0.0.0/4 # reserved, includes limited broadcast
- fc00::/7 # ULA: home network and Tailscale IPv6
- fd05:5::/64 # clusterULA6: the k3s node plane, on-link on the LAN
- fd7a:115c:a1e0::/48 # Tailscale IPv6 CGNAT: stays on tailscale0
- fe80::/10
- 2001:db8::/32 # IPv6 documentation
- 2002::/16 # deprecated 6to4
@@ -126,6 +127,9 @@ rules:
# local ranges, by address as well as by name
- DOMAIN-SUFFIX,lan,DIRECT
# MagicDNS names are dialed by name, so the tailnet CIDR rules below cannot see
# them; a proxied tailnet peer is unreachable either way.
- DOMAIN-SUFFIX,ts.net,DIRECT
- IP-CIDR,0.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
@@ -134,6 +138,10 @@ rules:
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,224.0.0.0/4,DIRECT,no-resolve
- IP-CIDR6,fe80::/10,DIRECT,no-resolve
# IPv6 counterparts of the LAN and tailnet CIDR rules above; a belt for the
# route-exclude-address entries, which keep these off the TUN already.
- IP-CIDR6,fd05:5::/64,DIRECT,no-resolve
- IP-CIDR6,fd7a:115c:a1e0::/48,DIRECT,no-resolve
# everything else that is CN by geolocation, then the catch-all
- GEOSITE,cn,DIRECT