feat(mihomo): keep unproxiable ranges out of TUN, fix the startup DNS race (#419)

* feat(mihomo): keep ranges that can never be proxied out of TUN

The list lives in policy.yaml as tun_exclude_address; sources.yaml may override
it. 198.18.0.0/15 stays in TUN: that is the fake-ip range itself.

* feat(mihomo): match Valve's content servers by ASN

The 20 hand-copied prefixes had drifted from what AS32590 announces. The rule
type needs GeoLite2-ASN.mmdb, so geox-url.asn points at the same mirror.

* feat(mihomo): render the gateway config from the same sources

The hand-kept copy had drifted: a manual sync carried the desktop's secret onto
the gateway and dropped external-ui-url. --redact yields a shareable template.

* fix(mihomo): open the link DNS takeover only once the core serves it

The core opens :1053 after parsing rules, and rule parsing is where missing
geodata gets fetched. Flipping at fork time made that fetch SERVFAIL, the config
fatal, and the unit unstartable.

* fix(justfile): keep local endpoints out of the agent proxy

With only HTTP_PROXY set, a request to this host's own llama-swap endpoint went
to mihomo. Node matches exact hosts only, so the literals carry it; the CIDRs
serve curl and Go.

* feat(justfile): seed mihomo geodata into the service state dir

So a cold start never needs the download that the DNS takeover made unstartable.
This commit is contained in:
ryan4yin | 二花 authored and GitHub committed 2026-10-09 12:31:59 +08:00
1 parent fe302fa950
commit 63b7183381
7 files changed
+184 -51

No files matched your search

+26 -10
View File
@@ -138,17 +138,33 @@ 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.
# Point this link's DNS at mihomo while it runs, and revert it when it stops.
#
# Why a lifecycle hook: mihomo's sing-tun stack would call resolvectl itself,
# but nixpkgs' DynamicUser+hardened unit closes the D-Bus socket and SetLinkDNS
# needs privileged authorization; without the takeover resolved's upstream
# queries bypass dns-hijack and leave via the NIC. The '+' prefix escapes that
# sandbox -- a failed ExecStartPost would kill a healthy service -- and
# ExecStopPost also runs on crashes, so a dead mihomo leaves the network usable.
#
# Why the probe: the core opens :1053 only after parsing the rules, and rule
# parsing is where missing geodata gets fetched. Flipping at fork time pointed
# that fetch at a port nobody was listening on -- SERVFAIL, a fatal config
# error, a unit that could never start.
systemd.services.mihomo.serviceConfig = {
ExecStartPost = lib.mkDefault "+${pkgs.systemd}/bin/resolvectl dns ${iface} 127.0.0.1:1053";
ExecStartPost = lib.mkDefault "+${pkgs.writeShellScript "mihomo-dns-takeover" ''
probe() { (exec 3<>/dev/tcp/127.0.0.1/1053) 2>/dev/null; }
up=0
for ((i = 0; i < 100; i++)); do
if probe; then up=1; break; fi
sleep 0.1
done
if [ "$up" = 0 ]; then
echo "mihomo never opened 127.0.0.1:1053; leaving ${iface} on its own DNS" >&2
exit 0
fi
exec ${pkgs.systemd}/bin/resolvectl dns ${iface} 127.0.0.1:1053
''}";
ExecStopPost = lib.mkDefault "+${pkgs.systemd}/bin/resolvectl revert ${iface}";
};