* 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.
10 KiB
Mihomo
Native mihomo core + metacubexd dashboard, replacing the Clash Verge GUI. Enable per host with
modules.networking.mihomo.enable = true (ai, shoukei).
Usage
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
sudo systemctl restart mihomo.service # dashboard: http://127.0.0.1:9090/ui
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.
Files
| File | Role |
|---|---|
default.nix |
the service: services.mihomo + TUN + metacubexd |
policy.yaml |
portable routing policy: ads, CN services, local ranges, tail |
generate.nu |
renders config.yaml from sources.yaml + policy.yaml |
gateway-config.nu |
the same output plus the gateway deltas |
sources.example.yaml |
schema for sources.yaml |
Rule order: private_domains, your rules, imported rules, then policy.yaml, ending in MATCH.
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.
Why generate
proxy-provider imports proxies only — a subscription's rules and groups are dropped by the core.
The portable policy is therefore written once in policy.yaml as GEOSITE categories. Provider-bound
policy is opt-in: rules_from: (a decoded snapshot) or file: carry groups, rules and
rule-providers over. The generator drops and reports malformed rules, misplaced MATCH and orphaned
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.private_domains: each entry becomes a DIRECT rule + afake-ip-filterentry.- WeChat/QQ media go over bare CDN IPs, hence
multimedia.nt.qq.com.cninfake-ip-filter. If media still stalls:tun_exclude_process: [wechat, WeChatAppEx, qq](Linux names; the fix that won clash-verge-rev#1762). find-process-mode: offunlessPROCESS-*rules ortun_exclude_processneed it.tun_exclude_address: a destination that can never be a proxy target should not pay a fake-ip round trip -- RFC1918, RFC 6598's100.64.0.0/10(where Tailscale addresses come from), link-local, multicast, reserved and documentation prefixes,fc00::/7,fe80::/10. Never exclude198.18.0.0/15, the fake-ip range itself;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.yamlpins the CM hosts,steamcontent.comandIP-ASN,32590DIRECT and keepssteamcommunity.comon the proxy -- Steam dials cached CM addresses without a lookup, so that rule is not decoration. NotGEOSITE,steam: that category includes the community. Sources and how to verify: "Steam download region" below. - CDN selection depends on launcher probes, not the patch download route. On this host,
launcher-webstatic.hoyoverse.comwent through a US proxy node whileautopatchcn.bhsr.comwas DIRECT; the download rose from ~0.5 to 102 MB/s after the proxy group was switched to DIRECT.policy.yamlbypasses proxy only for that observed probe, not game CDNs or broad vendor domains. Verify future probes in/connectionsand add only confirmed hosts. Reference: HoyoPlay API endpoints. - Rule payloads are bare domains:
DOMAIN-SUFFIX,https://qlogo.cnis invalid. - On a systemd-resolved host, sing-tun normally points resolved at the TUN DNS with
resolvectl. The nixpkgsDynamicUsersandbox cannot make those privileged D-Bus changes, so resolved's own upstream queries bypassdns-hijack any:53and leave via the NIC. Tie a resolver takeover to the service lifetime (resolvectl dns/revertinExecStartPost/ExecStopPost, seehosts/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.
Steam download region
Steam hands the client a content-server list keyed by a cell_id, and the cell follows the public
IP Steam sees on the login (CM) connection. A proxied login means a foreign cell, which means
foreign download caches. Nothing in the Steam client overrides that. What the Steam rules in
policy.yaml rest on, and how far to trust each source:
| Source | Establishes | Trust |
|---|---|---|
| Valve's content-server directory API | the list is keyed by cell_id; cell_id=0 means "use geolocation". Ask for any cell with ?cell_id=<n>&max_servers=2000 |
Valve, live, authoritative |
~/.local/share/Steam/logs/content_log.txt |
what this host actually got: 2024 (CellID 201) came from mainland CDN partners, 2025-2026 (CellID 171/177) from the tyo3/sgp1/hkg1/lax1 Valve caches |
your own client, the strongest evidence here |
| 羽翼城 / Dogfight360 | the trick itself: pin *.cm.steampowered.com, *.steamserver.net and Valve's ranges direct; 2022 article, still maintained (2026-05) |
the origin every other recipe copies; single-operator, but the mechanism checks out against the API and the local log |
| femoon, teapotium | the same rules for Clash/mihomo, and the CM-IP-cached-without-DNS detail | derivative reports; their prefix lists are stale, which is why the config matches on the ASN |
| RIPE stat: AS32590 announced prefixes | which ranges Valve actually owns today | registry data; cross-checks what the ASN match covers |
| meta-rules-dat | geosite category names: steam and category-games exist, category-games@cn (in several guides) does not |
upstream data, checked against GeoSite.dat (2026-10) |
Two things that look like levers and are not: the Download Region dropdown -- since 2024-10-04 it
selects no CDN at all
-- and CellIDServerOverride in config.vdf, pinned to 35 (Singapore) on this host while the log
shows downloads arriving from Tokyo. The IP Steam sees is the only lever.
Check the result with steam://open/console then user_info (IPCountry must read CN), and by
listing the cache groups the client was actually served:
grep -oE 'cache[0-9]+-[a-z]{3}[0-9]+' ~/.local/share/Steam/logs/content_log.txt |
sed -E 's/^cache[0-9]+-//' | sort -u
References
- clash-verge-rev#1762 — WeChat media under TUN: fake-ip-filter, qlogo/qpic, the exclude-process fix.
- WeChat-under-TUN retrospective — the IPv6 black hole logic; JD and Taobao break the same way.
- mihomo wiki, proxy providers — why subscription rules are dropped.
- meta-rules-dat — the GEOSITE/GEOIP data.
- metacubexd — the dashboard.
- nixpkgs mihomo module
—
DynamicUser,LoadCredential, onlyCAP_NET_ADMINundertunMode.