diff --git a/hosts/12kingdoms-shoukei/default.nix b/hosts/12kingdoms-shoukei/default.nix index 4a1f005d..6b8794ef 100644 --- a/hosts/12kingdoms-shoukei/default.nix +++ b/hosts/12kingdoms-shoukei/default.nix @@ -51,6 +51,8 @@ in inherit (myvars.networking) nameservers; }; + modules.networking.mihomo.resolvedIntegration = true; + fileSystems."/btr_pool" = { device = "/dev/disk/by-uuid/c2e8b249-240e-4eef-bf4e-81e7dbbf4887"; fsType = "btrfs"; diff --git a/modules/nixos/desktop/networking/mihomo/README.md b/modules/nixos/desktop/networking/mihomo/README.md index d634f03e..4bd9104b 100644 --- a/modules/nixos/desktop/networking/mihomo/README.md +++ b/modules/nixos/desktop/networking/mihomo/README.md @@ -138,6 +138,17 @@ targets instead of writing a config the core rejects. `hosts/idols-ai/default.nix`), prefixed with `+` to run outside the unit sandbox. Never set link DNS statically to mihomo: a dead mihomo would take DNS down with it, and resolved does not fall back. +- On shoukei, `modules.networking.mihomo.resolvedIntegration` enables resolved and the + NetworkManager resolved backend. Once DNS is listening, the service registers `127.0.0.1:1053` and + `~.` on `Meta`, which NetworkManager leaves unmanaged. Wi-Fi reconnects can update their own DNS + without overwriting this route. Stopping mihomo reverts only `Meta`; removing the TUN also removes + its DNS configuration, leaving the physical links' DNS available. More-specific VPN DNS routes + still win; another VPN claiming `~.` (including Tailscale's full DNS override) would share the + catch-all and must not be used concurrently. This relies on mihomo's default `Meta` device name + and the generator's local DNS port; apply TUN changes with a service restart so the DNS hook runs + for the new interface. After activation, verify normal and reconnected Wi-Fi queries return fake + IPs, and that stopping mihomo restores network DNS. See + [systemd's VPN DNS routing guide](https://github.com/systemd/systemd/blob/main/docs/RESOLVED-VPNS.md). ## Steam download region diff --git a/modules/nixos/desktop/networking/mihomo/default.nix b/modules/nixos/desktop/networking/mihomo/default.nix index f50b2b3d..2b5159cd 100644 --- a/modules/nixos/desktop/networking/mihomo/default.nix +++ b/modules/nixos/desktop/networking/mihomo/default.nix @@ -49,8 +49,10 @@ let ''; in { - options.modules.networking.mihomo.enable = - lib.mkEnableOption "the native Mihomo core with TUN and a local web UI"; + options.modules.networking.mihomo = { + enable = lib.mkEnableOption "the native Mihomo core with TUN and a local web UI"; + resolvedIntegration = lib.mkEnableOption "route systemd-resolved DNS through the mihomo TUN interface"; + }; config = lib.mkIf cfg.enable { services.mihomo = { @@ -67,10 +69,56 @@ in # and fail with "Failed to set up credentials: No such file or directory". systemd.services.mihomo.unitConfig.RequiresMountsFor = builtins.dirOf configFile; + services.resolved.enable = lib.mkIf cfg.resolvedIntegration true; + networking.networkmanager = + lib.mkIf (cfg.resolvedIntegration && config.networking.networkmanager.enable) + { + dns = "systemd-resolved"; + # NetworkManager owns physical links; mihomo owns this virtual link. + unmanaged = [ "interface-name:Meta" ]; + }; + systemd.services.mihomo.after = lib.optional cfg.resolvedIntegration "systemd-resolved.service"; + systemd.services.mihomo.requires = lib.optional cfg.resolvedIntegration "systemd-resolved.service"; + # Force the TUN's v6 address up: left to itself mihomo skips the v6 half when # no NIC has an IPv6 address at startup, yet still installs its strict-route v6 # blackhole, which locks the host out of IPv6. Upstream knob (`inet6-address`, # meta-docs tun.md); the full chain is WA-030. - systemd.services.mihomo.serviceConfig.Environment = [ "SKIP_SYSTEM_IPV6_CHECK=1" ]; + systemd.services.mihomo.serviceConfig = { + Environment = [ "SKIP_SYSTEM_IPV6_CHECK=1" ]; + } + // lib.optionalAttrs cfg.resolvedIntegration { + # Attach the catch-all to the TUN, leaving DHCP and more-specific VPN DNS + # routes untouched. Removing the TUN also removes its resolved settings. + # '+' allows the privileged D-Bus call outside mihomo's service sandbox. + ExecStartPost = "+${pkgs.coreutils}/bin/timeout 30 ${ + lib.getExe ( + pkgs.writeShellApplication { + name = "mihomo-resolved"; + runtimeInputs = [ + pkgs.dig + pkgs.systemd + ]; + text = '' + until (( SECONDS >= 25 )); do + if [[ -e /sys/class/net/Meta ]] \ + && dig +time=1 +tries=1 @127.0.0.1 -p 1053 localhost A >/dev/null \ + && resolvectl dns Meta 127.0.0.1:1053 \ + && resolvectl dnssec Meta no \ + && resolvectl dnsovertls Meta no \ + && resolvectl domain Meta '~.'; then + exit 0 + fi + sleep 0.1 + done + echo "mihomo TUN or DNS did not become ready" >&2 + exit 1 + ''; + } + ) + }"; + # The interface may already be gone after a clean stop or a crash. + ExecStopPost = "-+${pkgs.systemd}/bin/resolvectl revert Meta"; + }; }; } diff --git a/outputs/aarch64-linux/tests/mihomo-dns/expected.nix b/outputs/aarch64-linux/tests/mihomo-dns/expected.nix new file mode 100644 index 00000000..6125ac03 --- /dev/null +++ b/outputs/aarch64-linux/tests/mihomo-dns/expected.nix @@ -0,0 +1,9 @@ +{ + resolved = true; + resolvconf = false; + dnsBackend = "systemd-resolved"; + tunUnmanaged = true; + dispatcherCount = 0; + resolverOrdered = true; + resolverRequired = true; +} diff --git a/outputs/aarch64-linux/tests/mihomo-dns/expr.nix b/outputs/aarch64-linux/tests/mihomo-dns/expr.nix new file mode 100644 index 00000000..35ef1c79 --- /dev/null +++ b/outputs/aarch64-linux/tests/mihomo-dns/expr.nix @@ -0,0 +1,14 @@ +{ outputs, ... }: +let + system = outputs.nixosConfigurations.shoukei-niri.config; + nm = system.networking.networkmanager; +in +{ + resolved = system.services.resolved.enable; + resolvconf = system.networking.resolvconf.enable; + dnsBackend = nm.dns; + tunUnmanaged = builtins.elem "interface-name:Meta" nm.unmanaged; + dispatcherCount = builtins.length nm.dispatcherScripts; + resolverOrdered = builtins.elem "systemd-resolved.service" system.systemd.services.mihomo.after; + resolverRequired = builtins.elem "systemd-resolved.service" system.systemd.services.mihomo.requires; +}