diff --git a/.gitignore b/.gitignore index c1e8b617..56c48eea 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,7 @@ env/ .ruff_cache/ .coverage htmlcov/ + +# mihomo's private input and its subscription snapshots live in ~/.config/mihomo; guard a stray copy +modules/nixos/desktop/networking/mihomo/sources.yaml +modules/nixos/desktop/networking/mihomo/snapshots/ diff --git a/Justfile b/Justfile index 9ee0fb15..739be72f 100644 --- a/Justfile +++ b/Justfile @@ -290,6 +290,25 @@ dsh-web: NO_PROXY: "localhost,127.0.0.1,::1,[::1],192.168.5.100,192.168.5.178,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" } { ^dsh web --no-open } +# Render the mihomo config from this repo and validate it with the live core. +[linux] +[group('services')] +mihomo-gen: + #!/usr/bin/env nu + # ~/.config/mihomo holds only the private sources.yaml and the generated + # config.yaml. The generator and policy.yaml stay in the repository, so the + # live directory can never keep a stale copy of them. + let mod = ("{{ justfile() }}" | path dirname | path join modules nixos desktop networking mihomo) + nu ($mod | path join generate.nu) + # Validate with the core the service actually runs; a nixpkgs one may differ. + let core = (systemctl show mihomo.service -p ExecStart | str trim | split row "path=" | get 1 | split row " " | get 0) + let check = (^$core -t -f ($env.HOME | path join .config mihomo config.yaml) | complete) + if $check.exit_code != 0 { + print --stderr ($check.stderr | str trim) + exit $check.exit_code + } + print "validated -- restart: sudo systemctl restart mihomo.service" + # Seed mihomo's geodata into the service's state dir, so a cold start needs no DNS. [linux] [group('services')] diff --git a/WORKAROUNDS.md b/WORKAROUNDS.md index 41f01871..5a7b83d0 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-029 | mihomo `fake-ip-range6` is `2001:2::/64`, not the shipped ULA default — `modules/nixos/desktop/networking/mihomo/generate.nu` | the default `fdfe:dcba:9876::/64` sits in `fc00::/7`, which Local Network Access calls local, so a public page loaded over the v4 fake pool and pulling a dual-stack subresource prompts for local-network access (Chrome 154, Firefox). The same range also stalled ssh: a session to a fake AAAA authenticated and then never closed, on both `gvisor` and `mips`. `2001:2::/48` is RFC 5180 benchmarking -- the v6 twin of the `198.18.0.0/15` v4 default, absent from that table -- and both symptoms went with the move | Go back to the upstream default once mihomo ships a pool outside the LNA non-public table; the ULA default brings the ssh stall back | 2026-10-09 | on a browser or mihomo bump -- that table already grew to cover `2001:db8::/32` and `198.18.0.0/15` | 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..d50cc56e 100644 --- a/home/base/tui/ssh.nix +++ b/home/base/tui/ssh.nix @@ -27,8 +27,6 @@ settings = { "github.com" = { - # avoid mihomo fake-IP6 hang, see modules/nixos/desktop/networking/mihomo/ - AddressFamily = "inet"; HostName = "ssh.github.com"; Port = 443; User = "git"; diff --git a/lib/genMicrovmGuestModule.nix b/lib/genMicrovmGuestModule.nix index e4d9cbc5..878e990d 100644 --- a/lib/genMicrovmGuestModule.nix +++ b/lib/genMicrovmGuestModule.nix @@ -14,7 +14,7 @@ ... }: let - inherit (networking) proxyGateway proxyGateway6; + inherit (networking) proxyGateway proxyGateway6 clusterULA6; inherit (networking.hostsAddr.${hostName}) ipv4; ipv4WithMask = "${ipv4}/24"; @@ -29,6 +29,9 @@ let if builtins.stringLength h == 1 then "0${h}" else h; mac = "02:00:00:00:${toHex (builtins.elemAt octets 2)}:${toHex (builtins.elemAt octets 3)}"; + # Same octets as the MAC: 192.168.5.111 -> fd05:5::05:6f. Survives ISP prefix changes. + ula = pkgs.lib.toLower "${clusterULA6}${toHex (builtins.elemAt octets 2)}:${toHex (builtins.elemAt octets 3)}/64"; + # tap iface name on the host (IFNAMSIZ is 16, so <= 15 chars). All homelab VMs # are 192.168.5.0/24, so the last octet is unique per host. tapId = "vm${builtins.elemAt octets 3}"; @@ -94,17 +97,18 @@ in systemd.network.networks."10-${hostName}" = { matchConfig.MACAddress = [ mac ]; networkConfig = { - Address = [ ipv4WithMask ]; + Address = [ + ipv4WithMask + ula + ]; DNS = [ proxyGateway ]; - DHCP = "ipv6"; - IPv6AcceptRA = true; + # No SLAAC/DHCPv6: the WAN /64 churns and Cilium keeps probing whatever it + # registered at agent start. The ULA above is the node's IPv6 identity. + DHCP = false; + IPv6AcceptRA = false; IPv6PrivacyExtensions = false; LinkLocalAddressing = "ipv6"; }; - # Keep the static resolver below as the only DNS server; router advertisements and DHCPv6 - # still provide addresses and routes but must not add their own resolver entries. - dhcpV6Config.UseDNS = false; - ipv6AcceptRAConfig.UseDNS = false; routes = [ { Destination = "0.0.0.0/0"; @@ -119,6 +123,13 @@ in linkConfig.RequiredForOnline = "routable"; }; + # No ULA counterpart to the LAN IPv4 accept in + # modules/nixos/base/networking/firewall.nix, so Cilium's node health probe + # (TCP 4240) between nodes is dropped. Trust the node plane like the LAN. + networking.firewall.extraInputRules = pkgs.lib.mkAfter '' + ip6 saddr ${clusterULA6}/64 accept + ''; + # journald on tmpfs -- keep it small services.journald.settings.Journal = { SystemMaxUse = "512M"; diff --git a/modules/nixos/desktop/networking/mihomo/README.md b/modules/nixos/desktop/networking/mihomo/README.md index ce3f4f6c..ae11c35d 100644 --- a/modules/nixos/desktop/networking/mihomo/README.md +++ b/modules/nixos/desktop/networking/mihomo/README.md @@ -8,11 +8,14 @@ Native mihomo core + metacubexd dashboard, replacing the Clash Verge GUI. Enable ```sh cp sources.example.yaml ~/.config/mihomo/sources.yaml && chmod 600 ~/.config/mihomo/sources.yaml $EDITOR ~/.config/mihomo/sources.yaml # fill in: secret, private_domains, providers -nu generate.nu # -> ~/.config/mihomo/config.yaml, mode 0600 -mihomo -t -f ~/.config/mihomo/config.yaml # validate before restarting +just mihomo-gen # -> ~/.config/mihomo/config.yaml, mode 0600 sudo systemctl restart mihomo.service # dashboard: http://127.0.0.1:9090/ui ``` +`~/.config/mihomo` holds only `sources.yaml` and the generated `config.yaml` (plus geodata and the +cache); the generator, `policy.yaml` and `gateway-config.nu` are run from here and never copied +over. `just mihomo-gen` renders the config and validates it with the core the service runs. + `sources.yaml` holds everything private (subscription URLs, secret, private domains) and stays out of the repo and the Nix store; the module loads `config.yaml` via `LoadCredential`. Generate the config before enabling the module, or the service will not start. @@ -31,7 +34,10 @@ Rule order: `private_domains`, your `rules`, imported rules, then `policy.yaml`, The gateway (`suzi`, not a host in this flake) runs the same config with a fixed set of deltas -- `gateway-config.nu` renders it, `--redact` for a shareable template. Its output carries the -subscriptions, so never commit it; the secret is filled in on the box, not here. +subscriptions, so never commit it; the secret is filled in on the box, not here. Its binary is a +static release build under `/usr/local/bin`, not a package: update it by hand, validate the live +config with the new core (`mihomo -t -d /etc/mihomo -f `) before swapping, and keep the +previous binary. The release ships no checksums, so TLS plus `-v` is the whole verification. ## Why generate @@ -43,17 +49,39 @@ 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. The pool is `2001:2::/64`, not the shipped `fdfe:dcba:9876::/64`: + that is ULA (`fc00::/7`), which Local Network Access calls **local**, so a public page pulling a + dual-stack subresource prompts. `2001:2::/48` is RFC 5180 benchmarking, the v6 twin of the + `198.18.0.0/15` v4 default. Never grant that prompt broadly -- it also covers `192.168.5.0/24`, + `fd05:5::/64` and Tailscale. Moving the pool leaves mihomo's own TUN addresses (`198.18.0.1`, + `fdfe:dcba:9876::1`) alone; no client is ever handed those as an answer. +- `tun.stack` stays at upstream's default `mips`; nftables rules out `system` and `mixed`. The + gateway runs `mixed`. +- The gateway sniffs, the desktop does not. LAN clients that use DoH dial the IP they resolved + themselves, and a node may not reach an IP a foreign resolver handed out, so the gateway sniffs + the name out of the handshake (`sniffer`, TLS and QUIC on 443, HTTP on 80) and dials that. The + cost: a bare-IP connection is judged by its SNI, so `IP-CIDR`/`IP-ASN` rules stop applying to it. + On the desktop the fake-ip table already names every flow. - `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 `2001:2::/64`. The two ULAs are spelled out instead of taking all of + `fc00::/7`, which would also exempt any other ULA a client dials; `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 +134,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..983c7d01 100644 --- a/modules/nixos/desktop/networking/mihomo/gateway-config.nu +++ b/modules/nixos/desktop/networking/mihomo/gateway-config.nu @@ -31,18 +31,23 @@ 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 | insert external-ui "ui" | insert external-ui-url $UI_URL | update secret "GATEWAY-SECRET-PLACEHOLDER" - | update tun.stack "mixed" + # the base leaves the stack at upstream's default; this box has run the kernel stack for months + | upsert 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"] + # the desktop's resolver is loopback; a gateway serves the LAN instead | update dns.listen ":1053" - | update dns.ipv6 true) + # LAN clients that use DoH dial the IPs they resolved; sniff so the node dials a name + | insert sniffer { + enable: true, + "parse-pure-ip": true, + sniff: { TLS: { ports: [443] }, HTTP: { ports: [80] }, QUIC: { ports: [443] } } + }) 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..ec81a9c3 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 @@ -517,7 +517,6 @@ def main [ tun: ( clean ({ enable: true - stack: "gvisor" "auto-route": true "strict-route": ($spec | get -o tun_strict_route | default true) "auto-detect-interface": true @@ -533,9 +532,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 its own pool (fake-ip-range6, mihomo >= 1.19.16) and must stay out of + # route-exclude-address. Why this pool and not the ULA default: README.md. + "fake-ip-range6": "2001:2::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..14143c6a 100644 --- a/modules/nixos/desktop/networking/mihomo/policy.yaml +++ b/modules/nixos/desktop/networking/mihomo/policy.yaml @@ -14,8 +14,9 @@ # `filter` (Go regex, case-insensitive). The generator also adds "🌍 其他地区" # (manual, everything not matched below) and "🌐 全部地区" (manual, all nodes); # AUTO is the all-node auto pick. Edit the regions freely; the single-char -# keywords (港/台/日/新/美/韩) deliberately catch naming variants like -# 美西, 日服 and 狮城. +# keywords (港/台/日/美/韩) deliberately catch naming variants like 美西, 日服 +# and 狮城. Spell a country out where one character would also catch a +# neighbour: 新 alone catches 新西兰. node_groups: - name: "🇭🇰 香港" filter: "(?i)(港|hk|hong ?kong|🇭🇰)" @@ -24,7 +25,7 @@ node_groups: - name: "🇯🇵 日本" filter: "(?i)(日|jp|japan|东京|🇯🇵)" - name: "🇸🇬 新加坡" - filter: "(?i)(新|狮城|sg|singapore|🇸🇬)" + filter: "(?i)(新加坡|狮城|sg|singapore|🇸🇬)" - name: "🇰🇷 韩国" filter: "(?i)(韩|kr|korea|🇰🇷)" - name: "🇺🇸 美国" @@ -63,7 +64,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 +128,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 +139,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 diff --git a/modules/nixos/desktop/networking/mihomo/sources.example.yaml b/modules/nixos/desktop/networking/mihomo/sources.example.yaml index f357d902..188ad9ad 100644 --- a/modules/nixos/desktop/networking/mihomo/sources.example.yaml +++ b/modules/nixos/desktop/networking/mihomo/sources.example.yaml @@ -1,4 +1,4 @@ -# Copy to ~/.config/mihomo/sources.yaml, fill it in, then run `nu generate.nu`. +# Copy to ~/.config/mihomo/sources.yaml, fill it in, then run `just mihomo-gen`. # Keep it mode 0600 and never commit it: subscription URLs, the dashboard # password and private domain names all live here. # diff --git a/vars/networking.nix b/vars/networking.nix index 70986e26..cc281402 100644 --- a/vars/networking.nix +++ b/vars/networking.nix @@ -8,6 +8,8 @@ rec { # it's a subrouter with a transparent proxy proxyGateway = "192.168.5.178"; proxyGateway6 = "fe80::8"; + # node identity must not ride the WAN /64: the ISP retires it, Cilium never refreshes it. + clusterULA6 = "fd05:5::"; nameservers = [ # IPv4 "119.29.29.29" # DNSPod https://www.dnspod.cn/Products/publicdns