fix(mihomo): use fake-IP for gateway IPv6 (#442)

This commit is contained in:
ryan4yin | 二花 authored and GitHub committed 2026-10-14 23:50:40 +08:00
1 parent c888dd65e9
commit bb89987640
5 files changed
+26 -30

No files matched your search

-18
View File
@@ -74,24 +74,6 @@ in
useDHCP = false;
};
# suzi's fake-ip DNS hands out AAAA records whose IPv6 path does not work: a
# `curl -6` to a registry returns 502 or times out, while `curl -4` answers in
# 0.1 s. Clients that try the AAAA first (podman, git, pip) then hang on the
# handshake, which is why docker.io, github.com and pypi.org looked blocked
# here. These are RFC 6724's default precedences with the IPv4-mapped row
# raised, so IPv4 sorts first; glibc drops its whole built-in table as soon as
# the file lists any entry, hence spelling all of it out.
#
# The real fix belongs on that gateway: stop answering AAAA for proxied names
# (`dns.ipv6: false` in mihomo; the module README says the same).
networking.getaddrinfo.precedence = {
"::1/128" = 50;
"::/0" = 40;
"2002::/16" = 30;
"::/96" = 20;
"::ffff:0:0/96" = 100;
};
# A `boot.kernel.sysctl` that disabled IPv6 was removed on 2026-10-14: networkd
# re-enables it on the wired link for the `DHCP = "ipv6"`/`IPv6AcceptRA` settings
# below, so it never applied and the AAAA attempt already fails at once (WA-034).
@@ -62,12 +62,19 @@ Verge UI. Verge logs `Unregistered Clash configuration key "ipv6"` for the pin -
still reaches the core.
The gateway (`suzi`, not a host in this flake) runs the same config with a fixed set of deltas --
`gateway-config.nu` renders it from the same `sources.yaml` and `policy.yaml`. Its output carries
the subscriptions and the private domains, 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 <candidate>`) before
swapping, and keep the previous binary. The release ships no checksums, so TLS plus `-v` is the
whole verification.
`gateway-config.nu` renders it from the same `sources.yaml` and `policy.yaml`. The gateway uses
nftables TProxy, disables the desktop TUN auto-route, and uses `fake-ip` DNS with `dns.ipv6: true`:
LAN clients receive stable fake A/AAAA mappings so mihomo retains the domain for routing and
sniffing. Its output carries the subscriptions and the private domains, 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 <candidate>`) before swapping, and keep the previous binary. The release ships no checksums, so TLS
plus `-v` is the whole verification.
Gateway mode contract: the tested working combinations are nftables TProxy with both `tun.enable`
and `tun.auto-route` disabled, using either `redir-host` or `fake-ip` DNS. This generator selects
`fake-ip` because it preserves more domain information. Do not combine LAN TProxy with the desktop
TUN auto-route: in this gateway topology it can recapture fake IPv6 dials and cause timeouts.
## Why generate
@@ -36,9 +36,14 @@ def main [
| insert external-ui "ui"
| insert external-ui-url $UI_URL
| update secret "GATEWAY-SECRET-PLACEHOLDER"
# the base leaves the stack at upstream's default; this box has run the kernel stack for months
| upsert tun.stack "mixed"
# The gateway uses nftables TProxy for LAN traffic; its desktop-style TUN
# auto-route would capture the gateway's own DIRECT IPv6 dials again.
| upsert tun.enable false
| upsert tun.auto-route false
| update tun.strict-route false
# TProxy handles fake IPv4 and IPv6 destinations when the desktop TUN
# auto-route is disabled; fake-IP preserves the domain mapping for LAN flows.
| upsert dns.enhanced-mode "fake-ip"
# the desktop's resolver is loopback; a gateway serves the LAN instead
| update dns.listen ":1053"
# LAN clients that use DoH dial the IPs they resolved; sniff so the node dials a name
@@ -6,7 +6,7 @@
gpuRecoveryEnabled = true;
aspmDisabled = true;
cStatesCapped = true;
prefersIpv4 = true;
nativeIpv6Preference = true;
sourceCheckoutsPersistent = true;
rootlessPodmanEnabled = true;
stableKernel = true;
@@ -25,9 +25,11 @@ in
aspmDisabled = builtins.elem "pcie_aspm=off" cfg.boot.kernelParams;
cStatesCapped = builtins.elem "processor.max_cstate=2" cfg.boot.kernelParams;
# suzi's fake-ip DNS returns AAAA records with no working IPv6 path, so the
# generated gai.conf has to sort IPv4 first (see ./default.nix).
prefersIpv4 = lib.hasInfix "precedence ::ffff:0:0/96 100" cfg.environment.etc."gai.conf".text;
# Aqua uses native IPv6; the gateway returns real AAAA records instead of
# requiring a host-local IPv4 precedence workaround.
nativeIpv6Preference =
!(cfg.environment.etc ? "gai.conf")
|| !lib.hasInfix "precedence ::ffff:0:0/96 100" cfg.environment.etc."gai.conf".text;
# The Strata checkout lives under ~/src, which has to survive the tmpfs root
# like ~/codes does.