mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:36 +01:00
feat(desktop): replace Clash Verge with native mihomo on all NixOS hosts (#379)
This commit is contained in:
1 parent
0a672060a4
commit
bbfe1fa53b
18 files changed
+845
-50
No files matched your search
@@ -9,6 +9,9 @@ ignore-files = true
|
||||
extend-exclude = [
|
||||
"data/",
|
||||
"rime-data/",
|
||||
# small-duck flypy user dictionary: entries are pinyin codes ("delt", "juli"),
|
||||
# not English words, so the spell checker flags every one of them
|
||||
"overlays/fcitx5/flypy_user.txt",
|
||||
]
|
||||
|
||||
[default]
|
||||
|
||||
+6
-5
@@ -46,11 +46,12 @@ recovery procedure, not its removal condition.
|
||||
|
||||
A known limitation with no fix yet.
|
||||
|
||||
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
|
||||
| ------ | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------- | -------------------------------------- | ------ |
|
||||
| WA-012 | `muvm` (x86_64 apps/games via microVM) disabled — `hosts/12kingdoms-shoukei/apple-silicon.nix` | Apple Silicon's 16k page size means x86_64 apps need `muvm`; it is off because it does not build/run on nixos-apple-silicon yet ([nixos-apple-silicon#237](https://github.com/nix-community/nixos-apple-silicon/issues/237)) | Re-enable once `muvm` builds and works | — | on nixos-apple-silicon or muvm updates | active |
|
||||
| WA-015 | Darwin secret ownership — `secrets/darwin.nix` | Blanket `chown /etc/agenix/*` can override declared root ownership through symlinks; source concern, not a verified runtime leak | Verify consumers/runtime ownership, replace blanket ownership with explicit per-file ownership, and recheck target modes | 2026-10-03 | next Darwin activation review | active |
|
||||
| WA-017 | NPU inference for X-ASR — `home/linux/gui/base/vinput/` | OpenVINO and the NPU plugin load and the daemon opens `/dev/accel/accel0`, but no profiling evidence confirms model computation on the NPU; `npu_busy_time_us` did not increase in the sample test | Remove once per-layer NPU profiling confirms execution on `ai` | 2026-10-04 | when model/provider changes | active |
|
||||
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
|
||||
| ------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------- | ------ |
|
||||
| WA-012 | `muvm` (x86_64 apps/games via microVM) disabled — `hosts/12kingdoms-shoukei/apple-silicon.nix` | Apple Silicon's 16k page size means x86_64 apps need `muvm`; it is off because it does not build/run on nixos-apple-silicon yet ([nixos-apple-silicon#237](https://github.com/nix-community/nixos-apple-silicon/issues/237)) | Re-enable once `muvm` builds and works | — | on nixos-apple-silicon or muvm updates | active |
|
||||
| WA-015 | Darwin secret ownership — `secrets/darwin.nix` | Blanket `chown /etc/agenix/*` can override declared root ownership through symlinks; source concern, not a verified runtime leak | Verify consumers/runtime ownership, replace blanket ownership with explicit per-file ownership, and recheck target modes | 2026-10-03 | next Darwin activation review | active |
|
||||
| WA-016 | subscription rule snapshots — `modules/nixos/desktop/networking/mihomo/` (`rules_from:`) | mihomo's `proxy-providers` imports proxies only, so the `rules` and `proxy-groups` a subscription ships with are dropped by the core; the generator re-imports them from a local YAML copy that has to be refreshed by hand when the provider changes its routing policy | Drop the snapshots once mihomo can import rules from a proxy-provider, or the providers publish their rules as rule-providers | 2026-10-04 | when mihomo gains rule import | active |
|
||||
| WA-017 | NPU inference for X-ASR — `home/linux/gui/base/vinput/` | OpenVINO and the NPU plugin load and the daemon opens `/dev/accel/accel0`, but no profiling evidence confirms model computation on the NPU; `npu_busy_time_us` did not increase in the sample test | Remove once per-layer NPU profiling confirms execution on `ai` | 2026-10-04 | when model/provider changes | active |
|
||||
|
||||
## WIP
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
settings = {
|
||||
"github.com" = {
|
||||
# avoid clash fake-IP6 hang, see modules/nixos/desktop/networking/clash-verge.nix
|
||||
# avoid mihomo fake-IP6 hang, see modules/nixos/desktop/networking/mihomo/
|
||||
AddressFamily = "inet";
|
||||
HostName = "ssh.github.com";
|
||||
Port = 443;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
pkgs,
|
||||
pkgs-master,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
@@ -16,8 +14,6 @@
|
||||
"${pkgs.ghostty}/share/applications/com.mitchellh.ghostty.desktop"
|
||||
"${pkgs.kitty}/share/applications/kitty.desktop"
|
||||
|
||||
"${pkgs-master.clash-verge-rev}/share/applications/clash-verge.desktop"
|
||||
|
||||
# nixpaks
|
||||
"${pkgs.nixpaks.firefox}/share/applications/org.mozilla.firefox.desktop"
|
||||
"${pkgs.nixpaks.telegram-desktop}/share/applications/org.telegram.desktop.desktop"
|
||||
|
||||
@@ -118,8 +118,8 @@ in
|
||||
IPv6AcceptRA = true; # for Stateless IPv6 Autoconfiguraton (SLAAC)
|
||||
LinkLocalAddressing = "ipv6";
|
||||
# r8169 drops carrier on S3 resume; without this, networkd tears down the
|
||||
# static address/route and clash's TUN auto-detect briefly loses its
|
||||
# outbound interface, leaving routing broken until clash is restarted.
|
||||
# static address/route and mihomo's TUN auto-detect briefly loses its
|
||||
# outbound interface, leaving routing broken until mihomo is restarted.
|
||||
IgnoreCarrierLoss = "10s";
|
||||
};
|
||||
routes = [
|
||||
|
||||
@@ -321,9 +321,12 @@ in
|
||||
# Misc
|
||||
# ======================================
|
||||
|
||||
# Clash Verge Rev
|
||||
".local/share/io.github.clash-verge-rev.clash-verge-rev"
|
||||
".local/share/clash-verge"
|
||||
# Mihomo (native core): sources spec, generator and config all live here
|
||||
# (contains subscription secrets, hence 0700).
|
||||
{
|
||||
directory = ".config/mihomo";
|
||||
mode = "0700";
|
||||
}
|
||||
|
||||
# Orca Slicer - 3D Printer Slicer
|
||||
".local/share/orca-slicer"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
pkgs-master,
|
||||
myvars,
|
||||
...
|
||||
}:
|
||||
@@ -15,6 +14,8 @@ 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,13 +47,9 @@ in
|
||||
# Browsers keep their profile/preferences in dconf.
|
||||
programs.dconf.enable = true;
|
||||
|
||||
# Clash Verge provides the proxy. Its core is started by the GUI, which is
|
||||
# launched inside the session; service mode + TUN mode come from the module.
|
||||
programs.clash-verge = {
|
||||
enable = true;
|
||||
package = pkgs-master.clash-verge-rev;
|
||||
serviceMode = true;
|
||||
tunMode = 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,23 +0,0 @@
|
||||
{ pkgs-master, ... }:
|
||||
# NOTE: There is a known pitfall with mihomo's fake-IP for IPv6
|
||||
# (fake-ip-range6: fdfe:dcba:9876::/64): TCP connections to IPv6 fake-IPs are
|
||||
# not proxied correctly and simply hang until connect() times out.
|
||||
# Clients without a fast IPv4 fallback stall for ~2 minutes per attempt:
|
||||
# every git remote operation (fetch/pull/push alike, they all go through ssh)
|
||||
# hangs on "Connecting to ssh.github.com [fdfe:dcba:9876::x] port 443"
|
||||
# before falling back to IPv4.
|
||||
# Browsers recover silently thanks to Happy Eyeballs (RFC 8305: when a name
|
||||
# resolves to both IPv4 and IPv6, the client tries one stack first, then races
|
||||
# the other after a ~50ms delay and keeps whichever connects first).
|
||||
# Fix: disable IPv6 in Clash Verge's DNS/TUN settings (confirmed working);
|
||||
# or work around per-client, e.g. `AddressFamily inet` in ssh_config
|
||||
# (see home/base/tui/ssh.nix).
|
||||
{
|
||||
programs.clash-verge = {
|
||||
enable = true;
|
||||
package = pkgs-master.clash-verge-rev;
|
||||
autoStart = false;
|
||||
serviceMode = true;
|
||||
tunMode = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# Mihomo
|
||||
|
||||
Native mihomo core + metacubexd dashboard, replacing the Clash Verge GUI. Enable per host with
|
||||
`modules.networking.mihomo.enable = true` (`ai`, `shoukei`, `ruby`, `kana`).
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
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` |
|
||||
| `sources.example.yaml` | schema for `sources.yaml` |
|
||||
|
||||
Rule order: `private_domains`, your `rules`, imported rules, then `policy.yaml`, ending in `MATCH`.
|
||||
|
||||
## 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 + a `fake-ip-filter` entry.
|
||||
- WeChat/QQ media go over bare CDN IPs, hence `multimedia.nt.qq.com.cn` in `fake-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: off` unless `PROCESS-*` rules or `tun_exclude_process` need it.
|
||||
- Rule payloads are bare domains: `DOMAIN-SUFFIX,https://qlogo.cn` is invalid.
|
||||
|
||||
## References
|
||||
|
||||
- [clash-verge-rev#1762](https://github.com/clash-verge-rev/clash-verge-rev/issues/1762) — WeChat
|
||||
media under TUN: fake-ip-filter, qlogo/qpic, the exclude-process fix.
|
||||
- [WeChat-under-TUN retrospective](https://x.com/realchendahuang/status/2104381806862795161) — the
|
||||
IPv6 black hole logic; JD and Taobao break the same way.
|
||||
- [mihomo wiki](https://wiki.metacubex.one/),
|
||||
[proxy providers](https://wiki.metacubex.one/config/proxy-providers/) — why subscription rules are
|
||||
dropped.
|
||||
- [meta-rules-dat](https://github.com/MetaCubeX/meta-rules-dat) — the GEOSITE/GEOIP data.
|
||||
- [metacubexd](https://github.com/MetaCubeX/metacubexd) — the dashboard.
|
||||
- [nixpkgs mihomo module](https://github.com/NixOS/nixpkgs/blob/nixos-unstable/nixos/modules/services/networking/mihomo.nix)
|
||||
— `DynamicUser`, `LoadCredential`, only `CAP_NET_ADMIN` under `tunMode`.
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
myvars,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# Native mihomo core + metacubexd dashboard, replacing the Clash Verge GUI: the
|
||||
# GUI crashed (SIGFPE) and reopened a fake-IP leak window on every core restart.
|
||||
#
|
||||
# The config stays OUT of the Nix store (it holds subscription URLs and the
|
||||
# controller secret) and reaches the service through systemd's LoadCredential.
|
||||
# This module only wires up the service; generate the file with ./generate.nu.
|
||||
# See README.md for usage and the routing notes, in particular why the config
|
||||
# must keep `ipv6: false`: mihomo does not proxy TCP to an IPv6 fake-IP
|
||||
# (fdfe:dcba:9876::/64), so ssh/git connections hang until they time out
|
||||
# (home/base/tui/ssh.nix keeps `AddressFamily inet` as a per-client fallback).
|
||||
let
|
||||
cfg = config.modules.networking.mihomo;
|
||||
configFile = "${config.users.users.${myvars.username}.home}/.config/mihomo/config.yaml";
|
||||
in
|
||||
{
|
||||
options.modules.networking.mihomo.enable =
|
||||
lib.mkEnableOption "the native Mihomo core with TUN and a local web UI";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.mihomo = {
|
||||
enable = true;
|
||||
package = pkgs.mihomo;
|
||||
inherit configFile;
|
||||
webui = pkgs.metacubexd;
|
||||
tunMode = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
#!/usr/bin/env nu
|
||||
# Generate ~/.config/mihomo/config.yaml from ~/.config/mihomo/sources.yaml:
|
||||
#
|
||||
# nu generate.nu [-s sources.yaml] [-o config.yaml]
|
||||
#
|
||||
# sources.yaml is the only file you edit. It holds secrets, so keep it mode 0600
|
||||
# and never commit it. README.md explains why the config is generated and what
|
||||
# the keys mean; sources.example.yaml is the schema.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const HEALTH_URL = "https://www.gstatic.com/generate_204"
|
||||
|
||||
# The portable tail -- dns fake-ip-filter entries, optional rule-providers and
|
||||
# the rules that close every config -- lives in policy.yaml next to this script.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# small helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def fail [msg: string] {
|
||||
error make { msg: $msg }
|
||||
}
|
||||
|
||||
# drop keys whose value is null or an empty list, to keep the output readable
|
||||
def clean [rec: record] {
|
||||
let drop = (
|
||||
$rec
|
||||
| columns
|
||||
| where { |c|
|
||||
let v = ($rec | get $c)
|
||||
if ($v == null) {
|
||||
true
|
||||
} else if (($v | describe) | str starts-with "list") {
|
||||
($v | length) == 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
)
|
||||
if ($drop | is-empty) {
|
||||
$rec
|
||||
} else {
|
||||
$rec | reject ...$drop
|
||||
}
|
||||
}
|
||||
|
||||
def dupes [names: list<string>] {
|
||||
$names
|
||||
| group-by
|
||||
| items { |k, v| if (($v | length) > 1) { $k } else { null } }
|
||||
| where { |x| $x != null }
|
||||
}
|
||||
|
||||
def node-names [doc: record] {
|
||||
$doc
|
||||
| get -o proxies
|
||||
| default []
|
||||
| each { |p| ($p | get -o name) }
|
||||
| where { |n| $n != null }
|
||||
}
|
||||
|
||||
# mihomo has no wildcard rule type, so a private_domains entry is normalized to
|
||||
# a bare domain: `DOMAIN-SUFFIX` already matches the apex and every label below
|
||||
# it, which is what `*.lab.example.com` means anyway.
|
||||
def private_bare [d: string] {
|
||||
let bare = (($d | str trim) | str replace --all --regex '^\*\.' "")
|
||||
if ($bare | str contains "*") {
|
||||
fail $"private_domains entry '($d)': mihomo has no wildcard rules, use a plain domain or a leading '*.'"
|
||||
}
|
||||
$bare
|
||||
}
|
||||
|
||||
def private-rule [d: string] {
|
||||
let bare = (private_bare $d)
|
||||
if ($bare | is-empty) {
|
||||
return null
|
||||
}
|
||||
$"DOMAIN-SUFFIX,($bare),DIRECT"
|
||||
}
|
||||
|
||||
# fake-ip-filter entry: `+.d` matches the apex and all labels
|
||||
def private-filter [d: string] {
|
||||
let bare = (private_bare $d)
|
||||
if ($bare | is-empty) {
|
||||
return null
|
||||
}
|
||||
$"+.($bare)"
|
||||
}
|
||||
|
||||
# rewrite whole comma-separated fields, so both `...,<group>` and composite
|
||||
# rules such as `AND,(...),<group>` get remapped
|
||||
def rewrite-rule [rule: string, map: record] {
|
||||
$rule
|
||||
| split row ","
|
||||
| each { |part|
|
||||
let t = ($part | str trim)
|
||||
if ($t in $map) { $map | get $t } else { $part }
|
||||
}
|
||||
| str join ","
|
||||
}
|
||||
|
||||
# the group or policy a rule sends traffic to: always the last field, except
|
||||
# that `no-resolve` trails it for IP rules. Composite rules keep the target
|
||||
# last as well, so splitting on commas is safe here.
|
||||
def rule-target [rule: string] {
|
||||
mut parts = ($rule | split row "," | each { |p| $p | str trim })
|
||||
if (($parts | last | default "" | str lowercase) == "no-resolve") {
|
||||
$parts = ($parts | slice 0..(($parts | length) - 2))
|
||||
}
|
||||
$parts | last | default ""
|
||||
}
|
||||
|
||||
# Rules copied out of a browser or a chat log often carry a scheme, e.g.
|
||||
# `DOMAIN-SUFFIX,https://qlogo.cn,DIRECT`; mihomo rejects that. A rule split
|
||||
# across two lines (`DOMAIN-SUFFIX,https://qlogo.cn` then `,DIRECT`) becomes two
|
||||
# invalid entries. Fix the scheme, drop what is still broken, and say so.
|
||||
#
|
||||
# A `MATCH` rule from a source or from `sources.yaml` is dropped: it is a
|
||||
# catch-all, so keeping one there would shadow every policy rule after it.
|
||||
# policy.yaml may keep one, but only as its last rule, which is where the
|
||||
# catch-all belongs.
|
||||
def sanitize-rules [rules: list<any>, who: string, keep_match: bool = false] {
|
||||
mut ok = []
|
||||
mut bad = []
|
||||
let last = (($rules | length) - 1)
|
||||
for e in ($rules | enumerate) {
|
||||
let i = $e.index
|
||||
let raw = ($e.item | into string | str trim)
|
||||
if ($raw | is-empty) {
|
||||
continue
|
||||
}
|
||||
let fixed = ($raw | str replace --all --regex '(?i)\bhttps?://' "")
|
||||
let parts = ($fixed | split row ",")
|
||||
let head = ($parts | first | default "" | str trim | str uppercase)
|
||||
let tail = ($parts | last | default "" | str trim)
|
||||
if ($head == "MATCH") and ($keep_match and ($i == $last)) {
|
||||
$ok = ($ok ++ [$fixed])
|
||||
} else if ($head == "MATCH") {
|
||||
$bad = ($bad ++ [$"($who): catch-all dropped, it belongs last: ($raw)"])
|
||||
} else if (($parts | length) < 3) or ($head | is-empty) or ($tail | is-empty) {
|
||||
$bad = ($bad ++ [$"($who): malformed rule dropped: ($raw)"])
|
||||
} else {
|
||||
$ok = ($ok ++ [$fixed])
|
||||
}
|
||||
}
|
||||
{ ok: $ok, bad: $bad }
|
||||
}
|
||||
|
||||
# PROCESS-* rules and tun.exclude-process both need the core to look up the
|
||||
# owning process, which `find-process-mode: off` disables.
|
||||
def process_mode [rules: list<string>, excluded: list<string>] {
|
||||
let needs = (
|
||||
($rules | any { |r| ($r | str uppercase | str contains "PROCESS-") })
|
||||
or (($excluded | length) > 0)
|
||||
)
|
||||
if $needs {
|
||||
"always"
|
||||
} else {
|
||||
"off"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# importing groups / rules / rule-providers from a local YAML copy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Node names are replaced by a provider reference so that a subscription
|
||||
# refresh (nodes come and go) cannot break the imported groups. A group named
|
||||
# exactly like its provider is dropped and remapped onto the "<name> 节点"
|
||||
# group this generator already builds, because a proxy-provider and a
|
||||
# proxy-group cannot share a name.
|
||||
def import-doc [doc: record, provider: string] {
|
||||
let names = (node-names $doc)
|
||||
let own_group = $"($provider) 节点"
|
||||
# `直连` is the conventional Chinese name for a built-in DIRECT node in these
|
||||
# configs; inline proxy nodes are not imported, so point such references at
|
||||
# the DIRECT policy instead -- unless the document defines its own 直连 group.
|
||||
let declared_groups = ($doc | get -o proxy-groups | default [] | each { |g| ($g | get -o name | default "") | str trim })
|
||||
let map = (
|
||||
if ("直连" in $declared_groups) {
|
||||
{ ($provider): $own_group }
|
||||
} else {
|
||||
{ ($provider): $own_group, "直连": "DIRECT" }
|
||||
}
|
||||
)
|
||||
|
||||
let groups = (
|
||||
$doc
|
||||
| get -o proxy-groups
|
||||
| default []
|
||||
| where { |g| (($g | get -o name | default "") | str trim) != $provider }
|
||||
| each { |g|
|
||||
let declared = ($g | get -o proxies | default [])
|
||||
let kept = (
|
||||
$declared
|
||||
| each { |x|
|
||||
let t = ($x | into string | str trim)
|
||||
if ($t in $map) { $map | get $t } else { $x }
|
||||
}
|
||||
| where { |x| not (($x | into string) in $names) }
|
||||
)
|
||||
let refs = ($declared | where { |x| ($x | into string) in $names })
|
||||
let use_list = (($g | get -o use | default []) ++ (if ($refs | is-empty) { [] } else { [$provider] }))
|
||||
clean ($g | upsert 'proxies' $kept | upsert 'use' $use_list)
|
||||
}
|
||||
| where { |g|
|
||||
let np = (($g | get -o proxies | default []) | length)
|
||||
let n_use = (($g | get -o use | default []) | length)
|
||||
($np > 0) or ($n_use > 0) or (($g | get -o include-all | default false) == true)
|
||||
}
|
||||
)
|
||||
|
||||
let rules = (sanitize-rules ($doc | get -o rules | default []) $"($provider) snapshot")
|
||||
# a rule pointing at a group this generator dropped would make the core refuse
|
||||
# the whole config, so drop the rule and report it instead
|
||||
let kept = ($groups | each { |g| $g.name })
|
||||
let dropped = (
|
||||
$doc
|
||||
| get -o proxy-groups
|
||||
| default []
|
||||
| each { |g| ($g | get -o name | default "") | str trim }
|
||||
# PROXY/AUTO always exist in the output, so rules targeting them are never orphans
|
||||
| where { |n| (not ($n in $kept)) and ($n != $provider) and ($n not-in ["PROXY", "AUTO"]) }
|
||||
)
|
||||
mut final_rules = []
|
||||
mut orphans = []
|
||||
for r in ($rules.ok | each { |x| rewrite-rule $x $map }) {
|
||||
let target = (rule-target $r)
|
||||
if ($target in $dropped) {
|
||||
$orphans = ($orphans ++ [$"($provider) snapshot: rule target has no group, dropped: ($r)"])
|
||||
} else {
|
||||
$final_rules = ($final_rules ++ [$r])
|
||||
}
|
||||
}
|
||||
{
|
||||
groups: $groups
|
||||
rules: $final_rules
|
||||
bad_rules: ($rules.bad ++ $orphans)
|
||||
"rule-providers": ($doc | get -o rule-providers | default {})
|
||||
}
|
||||
}
|
||||
|
||||
def open-doc [path: string, who: string] {
|
||||
let f = ($path | path expand)
|
||||
if not ($f | path exists) {
|
||||
fail $"($who): file not found: ($f)"
|
||||
}
|
||||
let doc = (
|
||||
try {
|
||||
open $f
|
||||
} catch {
|
||||
fail $"($who): ($f) is not readable as YAML/JSON. Save the decoded subscription, the provider's Clash YAML, not a base64 payload."
|
||||
}
|
||||
)
|
||||
if (($doc | describe) | str starts-with "record") {
|
||||
$doc
|
||||
} else {
|
||||
fail $"($who): ($f) has no top-level mapping"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# providers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def health-check [] {
|
||||
{ enable: true, url: $HEALTH_URL, interval: 300 }
|
||||
}
|
||||
|
||||
# -> { name, provider, groups, rules, bad_rules, rule-providers }
|
||||
def build-entry [p: record] {
|
||||
let name = ($p | get -o name | default "" | str trim)
|
||||
if ($name | is-empty) {
|
||||
fail "every provider needs a `name`"
|
||||
}
|
||||
let url = ($p | get -o url)
|
||||
let file = ($p | get -o file)
|
||||
let rules_from = ($p | get -o rules_from)
|
||||
|
||||
if ($url != null) {
|
||||
let provider = {
|
||||
type: "http"
|
||||
url: $url
|
||||
interval: ($p | get -o interval | default 86400)
|
||||
path: $"./providers/($name).yaml"
|
||||
"health-check": (health-check)
|
||||
}
|
||||
let imported = (
|
||||
if ($rules_from != null) {
|
||||
import-doc (open-doc $rules_from $"provider ($name)") $name
|
||||
} else {
|
||||
{ groups: [], rules: [], bad_rules: [], "rule-providers": {} }
|
||||
}
|
||||
)
|
||||
{
|
||||
name: $name
|
||||
provider: $provider
|
||||
groups: $imported.groups
|
||||
rules: $imported.rules
|
||||
bad_rules: $imported.bad_rules
|
||||
"rule-providers": $imported."rule-providers"
|
||||
}
|
||||
} else if ($file != null) {
|
||||
# a local YAML file: inline its proxies (the service user cannot read files
|
||||
# under /home), and carry over its groups, rules and rule-providers too
|
||||
let doc = (open-doc $file $"provider ($name)")
|
||||
let payload = ($doc | get -o proxies)
|
||||
if (($payload == null) or (($payload | describe) | str starts-with "list") and (($payload | length) == 0)) {
|
||||
fail $"provider ($name): ($file) has no top-level `proxies:` list"
|
||||
}
|
||||
let provider = {
|
||||
type: "inline"
|
||||
"health-check": (health-check)
|
||||
payload: $payload
|
||||
}
|
||||
let imported = (import-doc $doc $name)
|
||||
{
|
||||
name: $name
|
||||
provider: $provider
|
||||
groups: $imported.groups
|
||||
rules: $imported.rules
|
||||
bad_rules: $imported.bad_rules
|
||||
"rule-providers": $imported."rule-providers"
|
||||
}
|
||||
} else {
|
||||
fail $"provider ($name): set either `url` or `file`"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main [
|
||||
--sources (-s): path = "~/.config/mihomo/sources.yaml"
|
||||
--out (-o): path = "~/.config/mihomo/config.yaml"
|
||||
--policy (-p): path
|
||||
] {
|
||||
let src = ($sources | path expand)
|
||||
let out = ($out | path expand)
|
||||
let pol = (
|
||||
if $policy == null {
|
||||
($env.FILE_PWD | path join "policy.yaml")
|
||||
} else {
|
||||
$policy | path expand
|
||||
}
|
||||
)
|
||||
if not ($src | path exists) {
|
||||
fail $"sources file not found: ($src) -- copy sources.example.yaml to sources.yaml first"
|
||||
}
|
||||
if not ($pol | path exists) {
|
||||
fail $"policy file not found: ($pol)"
|
||||
}
|
||||
|
||||
# the portable tail: fake-ip-filter entries, optional rule-providers, rules
|
||||
let policy_doc = (open $pol)
|
||||
let policy_filters = ($policy_doc | get -o dns_filters | default [])
|
||||
let policy_rules = (sanitize-rules ($policy_doc | get -o rules | default []) "policy.yaml" true)
|
||||
let policy_rp = ($policy_doc | get -o rule_providers | default {})
|
||||
let node_groups = ($policy_doc | get -o node_groups | default [])
|
||||
|
||||
let spec = (open $src)
|
||||
# an empty secret leaves the dashboard open to anything on the machine
|
||||
if (($spec | get -o secret | default "") | is-empty) {
|
||||
fail "sources.yaml: 'secret' is required -- it protects the dashboard at 127.0.0.1:9090"
|
||||
}
|
||||
let declared = ($spec | get -o providers | default [])
|
||||
if ($declared | is-empty) {
|
||||
print --stderr "warning: no providers defined; everything will go DIRECT"
|
||||
}
|
||||
|
||||
let names = ($declared | each { |p| ($p | get -o name | default "") })
|
||||
let name_dups = (dupes $names)
|
||||
if ($name_dups | is-not-empty) {
|
||||
fail $"duplicate provider names: ($name_dups | str join ', ')"
|
||||
}
|
||||
|
||||
let entries = ($declared | each { |p| build-entry $p })
|
||||
|
||||
let providers_map = (
|
||||
$entries
|
||||
| reduce --fold {} { |it, acc| $acc | insert $it.name ($it.provider) }
|
||||
)
|
||||
|
||||
# groups: entry point, per-region auto+manual, an "everything else" and an
|
||||
# "all nodes" manual select, one select per provider, plus everything the
|
||||
# sources declare. `filter`/`exclude-filter` are Go regexes on the node name;
|
||||
# region auto groups are `lazy` so unselected regions cost no health checks.
|
||||
let per_provider = ($names | each { |n| { name: $"($n) 节点", type: "select", use: [$n] } })
|
||||
let region_groups = (
|
||||
$node_groups
|
||||
| each { |g|
|
||||
let base = ($g | get name)
|
||||
[
|
||||
{ name: $"($base) 自动", type: "url-test", use: $names, filter: ($g | get filter), lazy: true, url: $HEALTH_URL, interval: 300 }
|
||||
{ name: $"($base) 手动", type: "select", use: $names, filter: ($g | get filter) }
|
||||
]
|
||||
}
|
||||
| flatten
|
||||
)
|
||||
let other_groups = (
|
||||
if ($node_groups | is-empty) {
|
||||
[]
|
||||
} else {
|
||||
let joined = ($node_groups | each { |g| ($g | get filter) | str replace --all "(?i)" "" } | str join "|")
|
||||
let excl = "(?i)(" + $joined + ")"
|
||||
[{ name: "🌍 其他地区", type: "select", use: $names, "exclude-filter": $excl }]
|
||||
}
|
||||
)
|
||||
let all_group = [{ name: "🌐 全部地区", type: "select", use: $names }]
|
||||
let generated_groups = ($region_groups ++ $other_groups ++ $all_group)
|
||||
let imported_groups = ($entries | each { |e| $e.groups } | flatten)
|
||||
# an imported group with the same name wins; dropping ours still leaves a
|
||||
# usable config, failing would not
|
||||
let taken = (
|
||||
["PROXY", "AUTO"] ++ ($per_provider | get name) ++ ($imported_groups | each { |g| $g.name })
|
||||
)
|
||||
let region_kept = ($generated_groups | where { |g| $g.name not-in $taken })
|
||||
let region_skipped = ($generated_groups | where { |g| $g.name in $taken } | each { |g| $g.name })
|
||||
let groups = (
|
||||
[
|
||||
{
|
||||
name: "PROXY"
|
||||
type: "select"
|
||||
proxies: (["AUTO"] ++ ($region_kept | get name) ++ ($per_provider | get name) ++ ["DIRECT"])
|
||||
}
|
||||
{ name: "AUTO", type: "url-test", use: $names, url: $HEALTH_URL, interval: 300 }
|
||||
]
|
||||
++ $region_kept
|
||||
++ $per_provider
|
||||
++ $imported_groups
|
||||
)
|
||||
let group_dups = (dupes ($groups | each { |g| $g.name }))
|
||||
if ($group_dups | is-not-empty) {
|
||||
fail $"duplicate proxy-group names: ($group_dups | str join ', ')"
|
||||
}
|
||||
|
||||
# rule-providers: the policy file first, then every source document.
|
||||
# `insert` would abort with an internal nushell trace on a collision, so
|
||||
# detect duplicates across documents first and report the names.
|
||||
let rp_docs = ([$policy_rp] ++ ($entries | each { |e| $e."rule-providers" }))
|
||||
let rp_dups = (dupes ($rp_docs | each { |d| $d | columns } | flatten))
|
||||
if ($rp_dups | is-not-empty) {
|
||||
fail $"duplicate rule-provider names: ($rp_dups | str join ', ')"
|
||||
}
|
||||
let rule_providers = (
|
||||
$rp_docs
|
||||
| reduce --fold {} { |it, acc|
|
||||
$it | columns | reduce --fold $acc { |k, a| $a | insert $k ($it | get $k) }
|
||||
}
|
||||
)
|
||||
|
||||
let priv = ($spec | get -o private_domains | default [])
|
||||
let priv_rules = ($priv | each { |d| private-rule $d } | where { |x| $x != null })
|
||||
let priv_filters = ($priv | each { |d| private-filter $d } | where { |x| $x != null })
|
||||
let user_rules = (sanitize-rules ($spec | get -o rules | default []) "sources.rules")
|
||||
let imported_rules = ($entries | each { |e| $e.rules } | flatten)
|
||||
|
||||
let excluded = ($spec | get -o tun_exclude_process | default [])
|
||||
# evaluation order: private hosts, hand-written rules, provider rules, policy tail
|
||||
let body_rules = ($priv_rules ++ $user_rules.ok ++ $imported_rules ++ $policy_rules.ok)
|
||||
# a RULE-SET pointing at a rule-provider nobody declared, or a target that
|
||||
# is not one of the final groups, makes the core refuse the whole config;
|
||||
# drop the rule and report it. This pass sees every source at once, so it
|
||||
# also catches targets dangling across documents.
|
||||
let rp_names = ($rule_providers | columns)
|
||||
let valid_targets = (($groups | each { |g| $g.name }) ++ ["DIRECT", "PROXY", "REJECT", "PASS"])
|
||||
mut kept_rules = []
|
||||
mut rp_missing = []
|
||||
for r in $body_rules {
|
||||
let parts = ($r | split row ",")
|
||||
let ref = ($parts | get -o 1 | default "")
|
||||
let target = (rule-target $r)
|
||||
if (($parts | first | str uppercase) == "RULE-SET") and ($ref not-in $rp_names) {
|
||||
$rp_missing = ($rp_missing ++ [$"rule set '($ref)' is not declared, dropped: ($r)"])
|
||||
} else if ($target not-in $valid_targets) {
|
||||
$rp_missing = ($rp_missing ++ [$"rule target '($target)' has no group, dropped: ($r)"])
|
||||
} else {
|
||||
$kept_rules = ($kept_rules ++ [$r])
|
||||
}
|
||||
}
|
||||
let bad_rules = (($entries | each { |e| $e.bad_rules } | flatten) ++ $user_rules.bad ++ $policy_rules.bad ++ $rp_missing)
|
||||
# the core needs a catch-all; policy.yaml normally supplies it as its last rule
|
||||
let has_catch_all = ($kept_rules | last | default "" | str uppercase | str starts-with "MATCH")
|
||||
let all_rules = ($kept_rules ++ (if $has_catch_all { [] } else { ["MATCH,PROXY"] }))
|
||||
let fpm = (process_mode $all_rules $excluded)
|
||||
|
||||
let config = {
|
||||
# IPv6 stays off on purpose: most proxy nodes have no IPv6 egress, so an
|
||||
# AAAA record becomes a routing black hole that apps such as WeChat, JD,
|
||||
# Taobao and Feishu sit on until they fall back to IPv4.
|
||||
ipv6: false
|
||||
mode: "rule"
|
||||
"mixed-port": 7897
|
||||
"allow-lan": false
|
||||
"log-level": "warning"
|
||||
"unified-delay": true
|
||||
tcp-concurrent: true
|
||||
"find-process-mode": $fpm
|
||||
"external-controller": "127.0.0.1:9090"
|
||||
secret: ($spec | get -o secret | default "")
|
||||
"external-controller-cors": {
|
||||
"allow-private-network": true
|
||||
"allow-origins": ["http://127.0.0.1:9090", "http://localhost:9090"]
|
||||
}
|
||||
profile: { "store-selected": true, "store-fake-ip": true }
|
||||
tun: (
|
||||
clean ({
|
||||
enable: true
|
||||
stack: "gvisor"
|
||||
"auto-route": true
|
||||
"strict-route": ($spec | get -o tun_strict_route | default true)
|
||||
"auto-detect-interface": true
|
||||
"dns-hijack": ["any:53"]
|
||||
} | upsert 'exclude-process' $excluded)
|
||||
)
|
||||
dns: {
|
||||
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.
|
||||
listen: "127.0.0.1:1053"
|
||||
ipv6: false
|
||||
"enhanced-mode": "fake-ip"
|
||||
"fake-ip-range": "198.18.0.1/16"
|
||||
"fake-ip-filter-mode": "blacklist"
|
||||
"prefer-h3": false
|
||||
"respect-rules": false
|
||||
"use-hosts": false
|
||||
"use-system-hosts": false
|
||||
"default-nameserver": ["223.5.5.5" "119.29.29.29" "223.6.6.6"]
|
||||
nameserver: ["223.5.5.5" "119.29.29.29" "https://dns.alidns.com/dns-query"]
|
||||
"proxy-server-nameserver": ["223.5.5.5" "https://dns.alidns.com/dns-query" "tls://223.5.5.5"]
|
||||
"fake-ip-filter": (($priv_filters | append $policy_filters) | uniq)
|
||||
}
|
||||
"geodata-mode": false
|
||||
"geo-auto-update": true
|
||||
"geo-update-interval": 24
|
||||
"geox-url": {
|
||||
geoip: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat"
|
||||
geosite: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat"
|
||||
mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb"
|
||||
}
|
||||
"proxy-providers": $providers_map
|
||||
"proxy-groups": $groups
|
||||
"rule-providers": $rule_providers
|
||||
rules: ($all_rules | uniq)
|
||||
}
|
||||
|
||||
if not ($out | path dirname | path exists) {
|
||||
mkdir ($out | path dirname)
|
||||
}
|
||||
$config | to yaml | save --force $out
|
||||
^chmod 600 $out
|
||||
|
||||
let breakdown = $"private ($priv_rules | length), user ($user_rules.ok | length), imported ($imported_rules | length), policy ($policy_rules.ok | length)"
|
||||
print $"wrote ($out)"
|
||||
print $" providers: ($names | str join ', ')"
|
||||
print $" proxy-groups: ($groups | length)"
|
||||
print $" rule-providers: ($rule_providers | columns | length)"
|
||||
print $" rules: (($config | get rules) | length) -- ($breakdown)"
|
||||
print $" find-process-mode: ($fpm)"
|
||||
if ($region_skipped | is-not-empty) {
|
||||
print --stderr $"warning: region groups skipped, an imported group already uses: ($region_skipped | str join ', ')"
|
||||
}
|
||||
if ($bad_rules | is-not-empty) {
|
||||
print --stderr $"warning: dropped ($bad_rules | length) rules:"
|
||||
for b in $bad_rules {
|
||||
print --stderr $" ($b)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# Portable routing policy: nothing provider-specific lives here.
|
||||
#
|
||||
# Order matters. The generator appends these last, after the `private_domains`
|
||||
# rules, the rules from `sources.yaml` and the rules imported from your
|
||||
# subscriptions, so this file is the tail that closes every config.
|
||||
#
|
||||
# GEOSITE categories come from meta-rules-dat, which the module keeps current
|
||||
# (`geo-auto-update`). `mihomo -t` validates the names: `taobao`, `alipay` and
|
||||
# `wechat` do not exist in it, hence the hand-written domains below.
|
||||
|
||||
# Region node groups. Each entry becomes two groups offered inside PROXY:
|
||||
# "<name> 自动" (url-test, lazy: only health-checked once selected) and
|
||||
# "<name> 手动" (select), both over the provider nodes whose name matches
|
||||
# `filter` (Go regex, case-insensitive). The generator also adds "🌍 其他地区"
|
||||
# (manual, everything not matched below) and "🌐 全部地区" (manual, all nodes);
|
||||
# AUTO is the all-node auto pick. Edit the regions freely; the single-char
|
||||
# keywords (港/台/日/新/美/韩) deliberately catch naming variants like
|
||||
# 美西, 日服 and 狮城.
|
||||
node_groups:
|
||||
- name: "🇭🇰 香港"
|
||||
filter: "(?i)(港|hk|hong ?kong|🇭🇰)"
|
||||
- name: "🇹🇼 台湾"
|
||||
filter: "(?i)(台|tw|taiwan|🇹🇼)"
|
||||
- name: "🇯🇵 日本"
|
||||
filter: "(?i)(日|jp|japan|东京|🇯🇵)"
|
||||
- name: "🇸🇬 新加坡"
|
||||
filter: "(?i)(新|狮城|sg|singapore|🇸🇬)"
|
||||
- name: "🇰🇷 韩国"
|
||||
filter: "(?i)(韩|kr|korea|🇰🇷)"
|
||||
- name: "🇺🇸 美国"
|
||||
filter: "(?i)(美|usa|us[ -]|united states|america|🇺🇸)"
|
||||
|
||||
# fake-ip-filter: these must resolve to real IPs, never a fake one. Tencent's
|
||||
# multimedia hosts matter because WeChat/QQ fetch images over bare CDN IPs.
|
||||
dns_filters:
|
||||
- "*.lan"
|
||||
- "*.local"
|
||||
- "*.arpa"
|
||||
- localhost.ptlogin2.qq.com
|
||||
- localhost.sec.qq.com
|
||||
- localhost.work.weixin.qq.com
|
||||
- multimedia.nt.qq.com.cn
|
||||
- time.*.com
|
||||
- ntp.*.com
|
||||
- "*.msftncsi.com"
|
||||
- www.msftconnecttest.com
|
||||
- +.market.xiaomi.com
|
||||
- "*.tailscale.com"
|
||||
|
||||
# Optional extra rule-sets, usable by the rules below:
|
||||
# rule_providers:
|
||||
# my_list:
|
||||
# type: http
|
||||
# behavior: domain
|
||||
# format: text
|
||||
# url: "https://example.invalid/list.txt"
|
||||
# path: ./rulesets/my_list.txt
|
||||
# interval: 86400
|
||||
rule_providers: {}
|
||||
|
||||
rules:
|
||||
- GEOSITE,private,DIRECT
|
||||
- GEOSITE,category-ads-all,REJECT
|
||||
|
||||
# CN consumer services: proxied they fail in ways that look like app bugs --
|
||||
# WeChat pictures spin, JD and Taobao product images go blank. Much of it is
|
||||
# served over bare CDN IPs and IPv6, so pin the whole category DIRECT.
|
||||
- GEOSITE,tencent,DIRECT
|
||||
- GEOSITE,bilibili,DIRECT
|
||||
- GEOSITE,jd,DIRECT
|
||||
- DOMAIN-SUFFIX,taobao.com,DIRECT
|
||||
- DOMAIN-SUFFIX,tmall.com,DIRECT
|
||||
- DOMAIN-SUFFIX,alicdn.com,DIRECT
|
||||
- DOMAIN-SUFFIX,360buyimg.com,DIRECT
|
||||
- DOMAIN-SUFFIX,feishu.cn,DIRECT
|
||||
# WeChat/QQ image CDNs spelled out too, since they are fetched by IP
|
||||
- DOMAIN-SUFFIX,qlogo.cn,DIRECT
|
||||
- DOMAIN-SUFFIX,qpic.cn,DIRECT
|
||||
- DOMAIN-SUFFIX,gtimg.com,DIRECT
|
||||
- DOMAIN-SUFFIX,gtimg.cn,DIRECT
|
||||
- DOMAIN-SUFFIX,gtimg.com.cn,DIRECT
|
||||
|
||||
# local ranges, by address as well as by name
|
||||
- DOMAIN-SUFFIX,lan,DIRECT
|
||||
- IP-CIDR,0.0.0.0/8,DIRECT,no-resolve
|
||||
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
|
||||
- IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
|
||||
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
|
||||
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
|
||||
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
|
||||
- IP-CIDR,224.0.0.0/4,DIRECT,no-resolve
|
||||
- IP-CIDR6,fe80::/10,DIRECT,no-resolve
|
||||
|
||||
# everything else that is CN by geolocation, then the catch-all
|
||||
- GEOSITE,cn,DIRECT
|
||||
- GEOIP,CN,DIRECT
|
||||
- MATCH,PROXY
|
||||
@@ -0,0 +1,44 @@
|
||||
# Copy to ~/.config/mihomo/sources.yaml, fill it in, then run `nu generate.nu`.
|
||||
# Keep it mode 0600 and never commit it: subscription URLs, the dashboard
|
||||
# password and private domain names all live here.
|
||||
#
|
||||
# The portable routing policy (ads, CN consumer services, local ranges) is not
|
||||
# here -- it lives in policy.yaml, which is safe to keep in the repository.
|
||||
|
||||
# Password for the dashboard at http://127.0.0.1:9090/ui
|
||||
secret: "replace-me"
|
||||
|
||||
# Domains that must never be proxied or fake-IPed: each becomes a DIRECT rule
|
||||
# plus a fake-ip-filter entry, so it resolves to a real IP. `*.lab.example.com`
|
||||
# is an alias for the plain form; any other `*` placement is rejected.
|
||||
private_domains:
|
||||
# - private.example.com
|
||||
# - "*.lab.example.com"
|
||||
|
||||
# Extra rules, before the ones the sources bring in. Payloads are bare domains,
|
||||
# never `https://…`. A `MATCH` here is dropped: policy.yaml closes the list.
|
||||
rules:
|
||||
# - DOMAIN-SUFFIX,download.example.com,PROXY
|
||||
# - PROCESS-NAME,some-app,DIRECT
|
||||
|
||||
# Keep on unless an app misbehaves under TUN (WeChat and office suites usually).
|
||||
tun_strict_route: true
|
||||
|
||||
# Processes kept off TUN entirely; also re-enables find-process-mode, which
|
||||
# PROCESS-* rules need.
|
||||
tun_exclude_process:
|
||||
# - some-app
|
||||
|
||||
providers:
|
||||
# The core imports a subscription's proxies only; `rules_from` carries its
|
||||
# rules and groups over from a local snapshot copy. Only worth it when the
|
||||
# provider ships policy you actually need -- see README.md.
|
||||
- name: provider-a
|
||||
url: "https://example.invalid/subscribe/replace-me"
|
||||
# interval: 86400
|
||||
# rules_from: "~/.config/mihomo/snapshots/provider-a.yaml"
|
||||
|
||||
# A local Clash YAML file: proxies are inlined (the service user cannot read
|
||||
# /home), and its groups, rules and rule-providers are carried over too.
|
||||
- name: provider-b
|
||||
file: "~/work/example/client-proxy.yaml"
|
||||
@@ -34,6 +34,9 @@ let
|
||||
# not supported yet
|
||||
modules.desktop.gaming.enable = false;
|
||||
modules.desktop.canokey.enable = true;
|
||||
# Native mihomo core + local web dashboard (config is
|
||||
# ~/.config/mihomo/config.yaml, out of the store).
|
||||
modules.networking.mihomo.enable = true;
|
||||
}
|
||||
];
|
||||
home-modules = map mylib.relativeToRoot [
|
||||
|
||||
+3
-1
@@ -190,7 +190,9 @@ in
|
||||
settings = {
|
||||
write = true; # Automatically fix typos
|
||||
configPath = ".typos.toml"; # relative to the flake root
|
||||
exclude = "rime-data/";
|
||||
# git-hooks passes paths on the command line, which makes typos
|
||||
# ignore .typos.toml's extend-exclude, so repeat it here
|
||||
exclude = "rime-data/|overlays/fcitx5/flypy_user.txt";
|
||||
};
|
||||
};
|
||||
prettier = {
|
||||
|
||||
@@ -34,6 +34,9 @@ let
|
||||
modules.secrets.preservation.enable = true;
|
||||
modules.desktop.gaming.enable = true;
|
||||
modules.desktop.canokey.enable = true;
|
||||
# Native mihomo core + local web dashboard instead of the Clash Verge
|
||||
# GUI; its config is ~/.config/mihomo/config.yaml (out of the store).
|
||||
modules.networking.mihomo.enable = true;
|
||||
}
|
||||
];
|
||||
home-modules = map mylib.relativeToRoot [
|
||||
|
||||
@@ -11,7 +11,7 @@ in
|
||||
lib.genAttrs hosts (name: {
|
||||
computerUse = true;
|
||||
atspi = true;
|
||||
clashVerge = true;
|
||||
mihomo = true;
|
||||
timeZone = "America/Los_Angeles";
|
||||
xvfbRestart = "always";
|
||||
i3Restart = "always";
|
||||
|
||||
@@ -19,7 +19,7 @@ lib.genAttrs hosts (
|
||||
{
|
||||
computerUse = system.modules.desktop.computerUse.enable;
|
||||
atspi = system.services.gnome.at-spi2-core.enable;
|
||||
clashVerge = system.programs.clash-verge.enable;
|
||||
mihomo = system.services.mihomo.enable;
|
||||
timeZone = system.time.timeZone;
|
||||
xvfbRestart = home.systemd.user.services.xvfb.Service.Restart;
|
||||
i3Restart = home.systemd.user.services.i3.Service.Restart;
|
||||
|
||||
Reference in new issue
Block a user