fix(desktop): tie ai's link DNS takeover to mihomo's lifecycle (#380)

* fix(desktop): tie ai's link DNS takeover to mihomo's lifecycle

sing-tun normally points systemd-resolved at the TUN DNS by running
resolvectl (dns/domain/default-route). Under nixpkgs' hardened
DynamicUser mihomo unit that hand-off fails silently: the unit's
RestrictAddressFamilies blocks resolvectl's D-Bus socket and resolve1
requires privileged authorization, so resolved keeps querying the
physical-link DNS, fake-ip is bypassed, and domain rules degrade to
MATCH.

Do the hand-off as '+' ExecStartPost/ExecStopPost hooks instead: '+'
commands run outside the sandbox (systemd.exec: address-family
restrictions do not apply to them) and ExecStopPost reverts even on
crashes, so a dead mihomo leaves DNS usable.

Signed-off-by: Ryan Yin <xiaoyin_c@qq.com>

* docs(mihomo): record sing-tun's resolvectl hand-off as the leak cause

Signed-off-by: Ryan Yin <xiaoyin_c@qq.com>

* refactor(desktop): keep mihomo only on ai and shoukei

ruby/kana never ran the core (service inactive live), and their
resolved + TUN combo would reintroduce the resolver leak once enabled.
Drop the enable and the explicit module import from computer-use; the
eval test now pins mihomo=false for both guests.

---------

Signed-off-by: Ryan Yin <xiaoyin_c@qq.com>
This commit is contained in:
ryan4yin | 二花 authored and GitHub committed 2026-10-05 00:07:32 +08:00
1 parent bbd230e6fd
commit 8f0961d9c2
7 files changed
+42 -22

No files matched your search

+12 -11
View File
@@ -24,17 +24,18 @@ bump, and the commands that destroy rollback points are in the `nix-config-updat
## 1. Localize by layer
| Layer | Symptom | Look with |
| ------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| eval | `just test` prints `false`, an eval error | `just eval-host <host>` (already passes `--show-trace`) |
| build | build error, hash mismatch, "marked as broken" | `just build-host <host>`, then `nix log <drv>` |
| activation | the deploy fails after building | the deploy output; `journalctl -u home-manager-$USER -b` for Home Manager |
| runtime | a unit is failed or restarting | `just list-failed`, `systemctl status <unit>`, `journalctl -u <unit> -b` |
| boot | errors at boot, wrong kernel | `journalctl -b -p err`; `just history` for what is booted |
| session | desktop or app misbehaves | `journalctl --user -b -p err`; the `nix-config-desktop` skill |
| remote host | anything on a Colmena host | `ssh root@<host> journalctl -b -p err`, same commands over SSH |
| MicroVM guest | guest down or unreachable | on the VM host: `systemctl status microvm@<guest>` and `microvm-tap-interfaces@<guest>`; then `br0` |
| secrets | missing or unreadable `/etc/agenix/*` | the `nix-config-secrets` skill |
| Layer | Symptom | Look with |
| ------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| eval | `just test` prints `false`, an eval error | `just eval-host <host>` (already passes `--show-trace`) |
| build | build error, hash mismatch, "marked as broken" | `just build-host <host>`, then `nix log <drv>` |
| activation | the deploy fails after building | the deploy output; `journalctl -u home-manager-$USER -b` for Home Manager |
| runtime | a unit is failed or restarting | `just list-failed`, `systemctl status <unit>`, `journalctl -u <unit> -b` |
| proxy/DNS | proxied sites time out, node and rules look ok | `dig +short <site>` must return a fake-ip (`198.18.x.x`); a real IP means the system resolver bypasses `dns-hijack` — mihomo README Gotchas |
| boot | errors at boot, wrong kernel | `journalctl -b -p err`; `just history` for what is booted |
| session | desktop or app misbehaves | `journalctl --user -b -p err`; the `nix-config-desktop` skill |
| remote host | anything on a Colmena host | `ssh root@<host> journalctl -b -p err`, same commands over SSH |
| MicroVM guest | guest down or unreachable | on the VM host: `systemctl status microvm@<guest>` and `microvm-tap-interfaces@<guest>`; then `br0` |
| secrets | missing or unreadable `/etc/agenix/*` | the `nix-config-secrets` skill |
## 2. Read the evaluated value
+4 -1
View File
@@ -59,7 +59,10 @@ assume the physical-host deployment is done through Colmena.
unused `args`: haumea passes them lazily and they are still required.
5. `vars/networking.nix` - `hostsAddr.<name> = { iface; ipv4; }` for a LAN host. That entry drives
the static address, the SSH `Host` alias used for remote builds, and `known_hosts`, so a wrong
`iface` takes the host offline at activation. Skip it for a DHCP or mobile host.
`iface` takes the host offline at activation. Skip it for a DHCP or mobile host. If the host
enables `modules.networking.mihomo` and runs systemd-resolved, it needs a DNS takeover tied to
mihomo's lifecycle (`resolvectl dns`/`revert` in `ExecStartPost`/`ExecStopPost`, see
`hosts/idols-ai/default.nix`); a static link DNS would kill DNS when mihomo dies.
6. `hosts/README.md` - add the host to the inventory.
Pin service user and group ids (`service-user-ids.nix`, as on `shoryu`) before the host has state on
+14
View File
@@ -136,6 +136,20 @@ in
linkConfig.RequiredForOnline = "routable";
};
# Dynamic DNS takeover: sing-tun normally asks resolved to use the TUN DNS
# address through resolvectl. That built-in call is not usable from nixpkgs'
# DynamicUser+hardened unit: the D-Bus socket is blocked, and resolve1's
# SetLinkDNS/SetLinkDomains methods require privileged authorization. Without
# it, resolved's upstream queries bypass dns-hijack and leave via the NIC.
# Run the takeover as a privileged lifecycle hook instead. ExecStopPost also
# runs on crashes, so a dead mihomo leaves the network usable.
# The '+' prefix escapes the unit's sandbox; without it, resolvectl's D-Bus
# connect fails and a failed ExecStartPost kills the otherwise healthy service.
systemd.services.mihomo.serviceConfig = {
ExecStartPost = lib.mkDefault "+${pkgs.systemd}/bin/resolvectl dns ${iface} 127.0.0.1:1053";
ExecStopPost = lib.mkDefault "+${pkgs.systemd}/bin/resolvectl revert ${iface}";
};
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. It‘s perfectly fine and recommended to leave
-7
View File
@@ -14,8 +14,6 @@ let
timeZone = if cfg.proxyRegion == "jp" then "Asia/Tokyo" else "America/Los_Angeles";
in
{
imports = [ ./networking/mihomo ]; # also pulled in by the desktop scanPaths; explicit for clarity
options.modules.desktop.computerUse = {
enable = lib.mkEnableOption "headless computer-use environment (X11 + desktop automation)";
proxyRegion = lib.mkOption {
@@ -46,10 +44,5 @@ in
# Browsers keep their profile/preferences in dconf.
programs.dconf.enable = true;
# Native mihomo runs the proxy as a system service (TUN), so the headless
# session does not depend on a GUI starting the core. Its config lives at
# ~/.config/mihomo/config.yaml (out of the Nix store).
modules.networking.mihomo.enable = true;
};
}
@@ -1,7 +1,7 @@
# Mihomo
Native mihomo core + metacubexd dashboard, replacing the Clash Verge GUI. Enable per host with
`modules.networking.mihomo.enable = true` (`ai`, `shoukei`, `ruby`, `kana`).
`modules.networking.mihomo.enable = true` (`ai`, `shoukei`).
## Usage
@@ -46,6 +46,13 @@ targets instead of writing a config the core rejects.
won clash-verge-rev#1762).
- `find-process-mode: off` unless `PROCESS-*` rules or `tun_exclude_process` need it.
- Rule payloads are bare domains: `DOMAIN-SUFFIX,https://qlogo.cn` is invalid.
- On a systemd-resolved host, sing-tun normally points resolved at the TUN DNS with `resolvectl`.
The nixpkgs `DynamicUser` sandbox cannot make those privileged D-Bus changes, so resolved's own
upstream queries bypass `dns-hijack any:53` and leave via the NIC. Tie a resolver takeover to the
service lifetime (`resolvectl dns`/`revert` in `ExecStartPost`/`ExecStopPost`, see
`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.
## References
@@ -518,7 +518,9 @@ def main [
enable: true
# port 53 is unreachable here: the unit runs DynamicUser with only
# CAP_NET_ADMIN, so a privileged bind fails. TUN's dns-hijack covers
# resolution; this listener is a loopback convenience only.
# queries that leave through an interface; hosts whose system resolver
# 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
"enhanced-mode": "fake-ip"
@@ -11,7 +11,7 @@ in
lib.genAttrs hosts (name: {
computerUse = true;
atspi = true;
mihomo = true;
mihomo = false;
timeZone = "America/Los_Angeles";
xvfbRestart = "always";
i3Restart = "always";