From 1e6702db2ca4369fdb464ceaf10f8dc5db62daf9 Mon Sep 17 00:00:00 2001 From: Ryan Yin Date: Fri, 9 Oct 2026 17:22:29 +0800 Subject: [PATCH] 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. --- WORKAROUNDS.md | 22 +++++++------ home/base/tui/ssh.nix | 2 +- .../nixos/desktop/networking/mihomo/README.md | 32 +++++++++++++------ .../desktop/networking/mihomo/default.nix | 7 ++-- .../networking/mihomo/gateway-config.nu | 6 ++-- .../desktop/networking/mihomo/generate.nu | 13 +++++--- .../desktop/networking/mihomo/policy.yaml | 10 +++++- 7 files changed, 57 insertions(+), 35 deletions(-) diff --git a/WORKAROUNDS.md b/WORKAROUNDS.md index 41f01871..64221d89 100644 --- a/WORKAROUNDS.md +++ b/WORKAROUNDS.md @@ -33,16 +33,18 @@ 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 | -| WA-011 | Kernel module blacklist (`esp4`, `esp6`, `rxrpc`) — `hardening/kernel-hardening.nix` | attack surface for the Dirty Frag LPE (CVE-2026-43284 / CVE-2026-43500) | Kept as defence in depth; drop only if something needs those modules | 2026-10-03 | rarely | active | -| WA-017 | keyd right-hand modifier passthrough — `modules/nixos/desktop/peripherals.nix` | keyd folds right Ctrl/Shift/Meta into their left keys | Drop once keyd or fcitx5 keeps right-hand modifiers distinct | 2026-10-05 | on keyd bump | active | -| 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 ` 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 | +| 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 | +| WA-011 | Kernel module blacklist (`esp4`, `esp6`, `rxrpc`) — `hardening/kernel-hardening.nix` | attack surface for the Dirty Frag LPE (CVE-2026-43284 / CVE-2026-43500) | Kept as defence in depth; drop only if something needs those modules | 2026-10-03 | rarely | active | +| WA-017 | keyd right-hand modifier passthrough — `modules/nixos/desktop/peripherals.nix` | keyd folds right Ctrl/Shift/Meta into their left keys | Drop once keyd or fcitx5 keeps right-hand modifiers distinct | 2026-10-05 | on keyd bump | active | +| 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 ` 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@`, 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 diff --git a/home/base/tui/ssh.nix b/home/base/tui/ssh.nix index d4abc9dc..0392d404 100644 --- a/home/base/tui/ssh.nix +++ b/home/base/tui/ssh.nix @@ -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; diff --git a/modules/nixos/desktop/networking/mihomo/README.md b/modules/nixos/desktop/networking/mihomo/README.md index ce3f4f6c..5d4bbec6 100644 --- a/modules/nixos/desktop/networking/mihomo/README.md +++ b/modules/nixos/desktop/networking/mihomo/README.md @@ -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. diff --git a/modules/nixos/desktop/networking/mihomo/default.nix b/modules/nixos/desktop/networking/mihomo/default.nix index 75953998..af0bfeb9 100644 --- a/modules/nixos/desktop/networking/mihomo/default.nix +++ b/modules/nixos/desktop/networking/mihomo/default.nix @@ -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"; diff --git a/modules/nixos/desktop/networking/mihomo/gateway-config.nu b/modules/nixos/desktop/networking/mihomo/gateway-config.nu index 5cc05a75..245b2cd0 100644 --- a/modules/nixos/desktop/networking/mihomo/gateway-config.nu +++ b/modules/nixos/desktop/networking/mihomo/gateway-config.nu @@ -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 diff --git a/modules/nixos/desktop/networking/mihomo/generate.nu b/modules/nixos/desktop/networking/mihomo/generate.nu index 25d540b0..80b553c7 100644 --- a/modules/nixos/desktop/networking/mihomo/generate.nu +++ b/modules/nixos/desktop/networking/mihomo/generate.nu @@ -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 diff --git a/modules/nixos/desktop/networking/mihomo/policy.yaml b/modules/nixos/desktop/networking/mihomo/policy.yaml index 61f6d1f2..3e615d2a 100644 --- a/modules/nixos/desktop/networking/mihomo/policy.yaml +++ b/modules/nixos/desktop/networking/mihomo/policy.yaml @@ -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