diff --git a/.gitignore b/.gitignore index 46610e4..b3577ea 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,14 @@ id_* .ansible/ facts/ +# Local agent-harness / tooling config (not repo content). +.agents/ +.claude/ +.omp/ +.mcp.json +WATCHDOG.yml +skills-lock.json + # Editor and operating-system files. .DS_Store .vscode/ diff --git a/docs/agents/domain.md b/docs/agents/domain.md new file mode 100644 index 0000000..b548c53 --- /dev/null +++ b/docs/agents/domain.md @@ -0,0 +1,51 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root, or +- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. +- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved. + +## File structure + +Single-context repo (most repos): + +``` +/ +├── CONTEXT.md +├── docs/adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +Multi-context repo (presence of `CONTEXT-MAP.md` at the root): + +``` +/ +├── CONTEXT-MAP.md +├── docs/adr/ ← system-wide decisions +└── src/ + ├── ordering/ + │ ├── CONTEXT.md + │ └── docs/adr/ ← context-specific decisions + └── billing/ + ├── CONTEXT.md + └── docs/adr/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ diff --git a/docs/lan-dns-alternatives.md b/docs/lan-dns-alternatives.md new file mode 100644 index 0000000..27dffe3 --- /dev/null +++ b/docs/lan-dns-alternatives.md @@ -0,0 +1,592 @@ +# Home-LAN DNS alternatives for the windy LAN (research, 2026-08) + +**Status: research only. No configuration was changed.** This page evaluates +resolvers/splitters that are genuinely better than — or meaningfully different +from — the current "AdGuard Home (AGH) + idle mosdns" setup on +[`dns.windy.lan`](../hosts/dns.windy.lan.md) (`.36`), for a GFW-constrained +China home LAN. Claims are cited to primary sources (official repos, official +docs, upstream READMEs); anything not verified is flagged as such. + +Scope recap (from [`lan-overview.md`](lan-overview.md), verified 2026-08-06): + +- Clients get DNS via EdgeRouter DHCP option 6 → AGH `192.168.66.36:53`. +- AGH upstreams: `dns.alidns.com` + `doh.pub` DoH (load-balanced), fallback + `https://adg.chans.xyz/dns-query`. **DNSSEC disabled** (known-bad-signature + check failed on the selected path). Rewrites: `hass.local` / `hass.windy.lan`. +- `gfw` OpenWrt (`.1`) runs OpenClash fake-ip + TPROXY; dnsmasq → clash DNS + `127.0.0.1#7874`. A `mosdns` binary listens on `127.0.0.1:6052` but is **not** + on the active path. +- No local authoritative PTR source yet; private reverse DNS is a known gap. + +--- + +## 1. TL;DR / recommendation + +**The current stack is already 80% of the answer.** AGH is a strong LAN DNS +front-end (filtering, rewrites, per-client upstreams, query log, web UI) and its +upstream layer — **per-domain upstreams** plus a **per-domain list loaded from a +file** (`upstream_dns_file`) — is exactly the mechanism the official docs +recommend for accelerating China CDN domains while keeping everything else on a +trusted path. [AGH configuration: upstreams](https://adguard-dns.io/kb/adguard-home/configuration/). + +The genuinely worthwhile changes, in order of value: + +1. **Add geo-split inside AGH** via `upstream_dns_file` fed by a converted + `accelerated-domains.china.conf` ([felixonmars/dnsmasq-china-list](https://github.com/felixonmars/dnsmasq-china-list)): + domestic CDN domains → `dns.alidns.com` / `doh.pub`; everything else → + the trusted foreign path (currently `adg.chans.xyz`). This is a documented + AGH use case, requires **no new daemon**, and removes the need for mosdns. + This is the top recommendation. +2. **Re-enable real DNSSEC** by putting validation behind AGH: AGH's + `enable_dnssec` only sets the DO bit — it does not validate + ([AGH config: DNSSEC](https://adguard-dns.io/kb/adguard-home/configuration/)). + The two realistic ways are (a) point the foreign/trusted default upstream at + a validating resolver ([unbound](https://unbound.docs.nlnetlabs.nl/en/latest/), + [blocky](https://0xerr0r.github.io/blocky/latest/configuration/#dnssec-validation)) + and re-test a known-bad-signature domain; or (b) insert a validating + resolver (blocky is the lightest) between AGH and the upstreams. +3. **Either actually configure mosdns or delete it.** It is idle on the active + path; leaving a half-configured listener is surface area with no benefit. If + kept, be aware its cache strips EDNS0 and it performs no DNSSEC validation + ([mosdns v5 executable plugins](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/ke-zhi-xing-cha-jian.md)). + +Top-3 alternatives worth pursuing (see §3 for detail): + +| Rank | Option | Why | +|------|--------|-----| +| 1 | **AGH with China-list geo-split (`upstream_dns_file`)** | Documented AGH pattern; single box; no new service; keeps filtering/rewrites/UI. | +| 2 | **Blocky as validating backend behind AGH** | The only "new software" option that adds real in-process DNSSEC validation + conditional per-domain upstreams + ECS in one static binary ([blocky README](https://github.com/0xERR0R/blocky), [config](https://0xerr0r.github.io/blocky/latest/configuration/)). | +| 3 | **Unbound as validating recursive resolver** (replaces forwarders for the foreign path, or whole path) | True validation, full recursion (fail-open by nature), private `local-zone`s; heavier ops than AGH's file-driven split. | + +Explicitly **not** recommended as replacements here: smartdns and chinadns-ng +(both excellent *splitters*, but neither validates DNSSEC and both lack AGH's +filtering/UI/query-log layer, so they add a daemon without closing the DNSSEC +gap); mihomo/sing-box DNS as the primary path (couples DNS to the proxy and is +fail-closed; keep for proxy-side concerns only); knot-resolver/dnsdist (overkill +for a single-operator home LAN). + +--- + +## 2. Requirement matrix + +Legend: **●** native/built-in · **◐** possible with config/lists · **○** absent/ +not applicable. "Geo-split" = route domestic vs foreign names to different +upstreams. "Anti-pollution" = a mechanism to avoid/adjudicate poisoned answers +(IP-verdict or trusted-upstream routing). "DNSSEC" = performs validation +in-process (not just forwards DO). + +| Candidate | Geo-split | Anti-pollution | DNSSEC (validate) | Cache | Private names / rewrites | Ops simplicity | License | +|---|---|---|---|---|---|---|---| +| AGH (current) | ◐ per-domain upstreams + list file | ◐ via trusted foreign upstream | ○ (DO bit only) | ● | ● rewrites, per-client, private-PTR | ● Docker + UI | GPL-3.0 | +| mosdns v5 | ● domain/ip list matchers | ◐ forward foreign→trusted | ○ | ● (strips EDNS0) | ● hosts/redirect/reverse_lookup | ◐ single binary, YAML, no UI | GPL-3.0 | +| smartdns | ● nameserver groups + domain lists | ● bogus-nxdomain / blacklist-ip / trusted groups | ○ (no option in config ref) | ● serve-expired | ● address / local-domain / lease file | ◐ single binary, optional WebUI plugin | GPL-3.0 | +| chinadns-ng | ● chnlist/gfwlist + tag:none IP-test | ● IP verdict via chnroute ipset/nftset | ○ | ● cache/stale/verdict | ◐ hosts / dns-rr-ip | ◐ single static binary, config file | AGPL-3.0 | +| dnsmasq-china-list | ◐ (data only) | ◐ (via host resolver) | ◐ via host | ◐ via host | ◐ via host | ◐ feed lists | WTFPL | +| unbound | ◐ forward-zones / RPZ / views | ◐ forward-zones + bogus-nxdomain | ● | ● serve-expired | ● local-zone / local-data | ◐ config daemon, no UI | BSD-style (NLnet) | +| blocky | ◐ conditional per-domain + client groups | ◐ blocking lists + conditional routing | ● | ● prefetch | ● customDNS / rewrite / hosts | ◐ single binary, YAML, REST (no full web UI) | Apache-2.0 | +| Technitium | ◐ conditional-forwarder zones / apps | ◐ blocked lists + forwarding | ● | ● persistent | ● zones, stub, split-horizon | ● .NET + web console | GPL-3.0 | +| sing-box | ● DNS rules (geoip/geosite) | ● rule-based servers + (proxy) sniffing | ○ | ● LRU + optimistic | ● hosts / local server | ◐ single binary, JSON | GPLv3-family (metadata "other") | +| mihomo | ● nameserver-policy + fallback-filter | ● geoip verdict + geosite | ○ | ● (cache-algorithm) | ● hosts; fake-ip-filter for `.lan` | ◐ single binary, YAML | not cleanly verifiable (repo obfuscated) | +| knot-resolver | ◐ policy modules | ◐ policy + RPZ | ● | ● persistent | ◐ hints / local data | ◐ systemd, Lua config | open source (CZ-NIC) | +| dnsdist | ◐ Lua rules (custom) | ◐ custom policies | ○ (balancer, not validator) | ○ (no cache of its own) | ○ | ○ power tool | GPL (PowerDNS) | + +Notes: + +- "Geo-split" for AGH/blocky/unbound/Technitium is real but requires feeding a + China domain list; chinadns-ng/mihomo additionally offer the **IP-verdict** + path for domains not in any list (query both, adopt CN result only if the + answer IP is mainland). +- mosdns v5's `cache` plugin ignores request EDNS0 and strips response EDNS0 + ([cache plugin](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/ke-zhi-xing-cha-jian.md)) — + relevant because AGH in front of it relies on the DO bit for DNSSEC-capable + upstreams. +- License for mihomo/sing-box/knot-resolver marked conservative: GitHub + metadata is "other"/custom or deliberately obfuscated; see §3 caveats. + +--- + +## 3. Per-candidate evaluation + +### 3.1 AdGuard Home — advanced upstream routing / built-ins + +What it is: Go DNS proxy + adblock + DHCP, LAN DNS front-end +([official](https://adguard-dns.io/kb/adguard-home/overview/)). + +Capabilities relevant here (all from the official configuration page): + +- **Per-domain upstreams** dnsmasq-style: `[/domain/]upstream`, wildcards, + `#` = "default upstreams", empty `//` = unqualified names + ([upstreams for domains](https://adguard-dns.io/kb/adguard-home/configuration/#upstreams-for-domains)). +- **List from file** `upstream_dns_file` — the docs *explicitly* call out China + CDN acceleration via dnsmasq lists, with the `server=/0-100.com/114.114.114.114` + → `[/0-100.com/]114.114.114.114` conversion + ([loading upstreams from file](https://adguard-dns.io/kb/adguard-home/configuration/#upstreams-from-file)). +- Upstream modes: `load_balance`, `parallel`, `fastest_addr`; plus `fallback_dns` + used only when primary upstreams fail + ([config file: dns](https://adguard-dns.io/kb/adguard-home/configuration/)). +- Per-client upstreams (`clients.persistent[].upstreams`), rewrites + (`filtering.rewrites`, incl. wildcard), `local_ptr_upstreams` for private PTR, + ECS (`edns_client_subnet` with `use_custom` coarse prefix), optimistic cache + ([same page](https://adguard-dns.io/kb/adguard-home/configuration/)). +- **DNSSEC is DO-bit only**: `enable_dnssec` "defines whether the proxy should + set the DO flag in the upstream requests" — validation must happen upstream + ([same page](https://adguard-dns.io/kb/adguard-home/configuration/)). +- DoH/DoT/DoQ/DoH3 serving, `bind_hosts`/ACL guidance + ([running securely](https://adguard-dns.io/kb/adguard-home/running-securely/)). + +Verdict: **Already installed and capable of the geo-split itself.** The current +setup under-uses it: only a load-balanced CN pair + fallback, no per-domain +routing and no validating upstream. This is the cheapest "better" state — see §5. + +### 3.2 mosdns v5 — installed but idle + +What it is: "一个 DNS 转发器" (a DNS forwarder) — plugin-based, sequence-driven +([README](https://github.com/IrineSistiana/mosdns), GPL-3.0, ~3.7k★). + +What it does (verified from the v5 wiki and source tree): + +- Servers: `udp_server`, `tcp_server` (TLS→DoT), `quic_server`, `http_server` + (DoH); upstreams in `forward` support `udp`, `tcp`, `tls`, `https`, `quic`, + HTTP/3, concurrent racing (`concurrent: n` picks the fastest) and socks5 + ([server plugins](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/fu-wu-qi-cha-jian.md), + [executable plugins](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/ke-zhi-xing-cha-jian.md)). +- Geo-split: v5 data providers are **`domain_set` / `ip_set` (text list files)** + plus `qname`/`resp_ip` matchers — verified from the current source tree + ([plugin/data_provider](https://github.com/IrineSistiana/mosdns/tree/main/plugin/data_provider)) + — and an `ipset`/`nftset` exec plugin to push answer IPs to kernel sets. The + old v4-style `geosite`/`geoip` `.dat` plugins are **not present** in the v5 + tree; the v5 wiki's own matcher page currently states there are no matcher + plugins to document + ([matcher page](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/pi-pei-qi-cha-jian.md)). + Plan on chnlist/gfwlist-style text lists, not `geosite.dat`. +- Cache: yes, incl. optional lazy cache and disk dump; **request EDNS0 is + ignored and response EDNS0 stripped** by the cache plugin + ([cache](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/ke-zhi-xing-cha-jian.md)). +- Private names: `hosts` (domain-rules style, not OS /etc/hosts syntax), + `redirect`, `arbitrary` (zone records), `reverse_lookup` (PTR/HTTP lookup). +- Ops: single binary + YAML; `mosdns service install` ships a systemd/launchd + helper ([v5 overview](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5.md)); + Docker image exists. No web UI of its own. + +Verdict: capable splitter/forwarder, but **adds no DNSSEC and no filtering +layer**, and its cache interferes with EDNS0/DO handling. As a *back-end* splitter +behind AGH it is a legitimate choice only if DNSSEC stays off. Given AGH can do +the same per-domain split natively (3.1), mosdns's marginal value here is +concurrent upstream racing and ipset/nftset integration — neither is needed at +this LAN's scale. Either wire it up properly or remove it. + +### 3.3 smartdns + +What it is: local DNS server that queries multiple upstreams, **speed-tests the +answer IPs and returns the fastest**; DoH/DoT/DoQ/DoH3; GPL-3.0, ~11.2k★ +([README](https://github.com/pymumu/smartdns)). + +Capabilities (from the official config reference and FAQ): + +- Multi upstream + "returns the fastest IP", unlike dnsmasq all-servers + ([README](https://github.com/pymumu/smartdns)). +- Domain groups: `server ... -group ` + `nameserver /domain/group` routing, + per-`bind` port flags (`-group`, `-no-speed-check`…), client rules/MAC/IP + ([config options](https://pymumu.github.io/smartdns/configuration/)). +- Anti-pollution tooling: `bogus-nxdomain` (return NXDOMAIN for poisoned IPs), + `blacklist-ip`, `whitelist-ip`, `ignore-ip`, `ipset`/`nftset` export + ([same page](https://pymumu.github.io/smartdns/configuration/)). +- ECS: global `edns-client-subnet` and per-server `-subnet` + ([same page](https://pymumu.github.io/smartdns/configuration/)). +- Cache: `cache-size`, `serve-expired` (RFC-like stale), `prefetch-domain`, + persistent cache file ([same page](https://pymumu.github.io/smartdns/configuration/)). +- Private names: `address`, `cname`, `local-domain`, `dnsmasq-lease-file` + ([same page](https://pymumu.github.io/smartdns/configuration/)). +- **DNSSEC: no validation option appears anywhere in the official config + reference or FAQ** — its pollution model is blacklist/whitelist + trusted + groups + speed selection, not DNSSEC ([config options](https://pymumu.github.io/smartdns/configuration/), + [FAQ](https://pymumu.github.io/smartdns/faq/)). Flagged: verify on the version + you deploy before relying on it. + +Verdict: the classic China-home "best-IP" resolver; good splitter, no DNSSEC, +speed-test model optimizes for latency rather than anti-pollution correctness. +Not better than AGH+China-list for this LAN; at most a back-end splitter behind +AGH, with the same DNSSEC caveat as mosdns. + +### 3.4 chinadns-ng / chinadns2 / dnsmasq-china-list + +**chinadns-ng** (the requested "china-dns-ng"; actual repo `zfl9/chinadns-ng`, +AGPL-3.0, Zig, ~1.4k★) is the maintained rewrite of shadowsocks/ChinaDNS: + +- Two upstream groups (china / trust) + `chnlist.txt` / `gfwlist.txt` domain + lists; domains are tagged `chn`/`gfw`/`none` + ([README](https://github.com/zfl9/chinadns-ng)). +- `tag:none` names are queried on **both** upstreams and the china answer is + adopted only if its A/AAAA is a mainland IP (tested against a `chnroute` + ipset/nftset loaded into the kernel); verdict caching avoids re-testing and + leaks ([README: 原理/verdict-cache](https://github.com/zfl9/chinadns-ng)). +- Cache with stale + pre-refresh + optional persistence; DoT upstream + (wolfssl build); `hosts` + `dns-rr-ip` local records; `nftset` add for + chn/gfw IPs; **no DoH by design** and **no DNSSEC** — the author's stated + philosophy is "one job, done well" + ([README](https://github.com/zfl9/chinadns-ng)). +- Resource footprint is tiny: ~140 KB baseline, ~2.4 MB with 73k+ chnlist + + 5.7k gfwlist entries ([README](https://github.com/zfl9/chinadns-ng)). + +**chinadns2** (`zfl9/chinadns2`) is the older C predecessor; effectively +superseded by chinadns-ng for new deployments (README not directly fetched — +treat as legacy line). + +**dnsmasq-china-list** (felixonmars, ~6.1k★) is data, not a daemon: +`accelerated-domains.china.conf`, `bogus-nxdomain.china.conf`, +`apple.china.conf`, `google.china.conf`, with generators for **dnsmasq, +unbound, bind, dnscrypt-proxy** +([README](https://github.com/felixonmars/dnsmasq-china-list), WTFPL per repo). + +Verdict: chinadns-ng is the strongest *pure splitter* for GFW networks (IP +verdict beats pure list-based routing for unknown domains), but it cannot +validate DNSSEC and brings no filtering UI. As AGH's backend it duplicates what +AGH's per-domain upstreams already do; its IP-test mode requires shipping +`chnroute` ipset/nftset into the host. dnsmasq-china-list is best used as the +**data feed** for the AGH `upstream_dns_file` recommendation in §5. + +### 3.5 unbound + +What it is: validating, recursive, caching resolver from NLnet Labs +([docs](https://unbound.docs.nlnetlabs.nl/en/latest/)). + +- **Real DNSSEC validation by default** (trust anchor, chain of trust); the + official home-network guide turns it on explicitly + ([home resolver guide](https://unbound.docs.nlnetlabs.nl/en/latest/use-cases/home-resolver.html)). +- Full recursion → does not hard-depend on any upstream or proxy; serve-expired + (RFC 8767), aggressive NSEC, DoH/DoT/DoQ serving and TLS upstreams, + forward-zone/stub-zone/authority-zone, RPZ filtering, views, ECS module + ([docs index](https://unbound.docs.nlnetlabs.nl/en/latest/)). +- Private names: `local-zone`/`local-data` for `*.windy.lan`-style names + ([unbound.conf(5)](https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html)). +- No built-in China split: you assemble it with forward-zones fed by + dnsmasq-china-list (`make unbound` generator) + `bogus-nxdomain`; no UI, no + per-client grouping comparable to AGH. + +Verdict: the gold standard for the **validation** half. Best used as (a) the +validating upstream behind AGH for the foreign/trusted path, or (b) a full +recursive resolver replacing the forwarders if you accept losing AGH-style +filtering/UI on top — keep AGH in front for that. System-package based, heavier +to operate than blocky but battle-tested. + +### 3.6 blocky + +What it is: Go DNS proxy + ad-blocker, "fast and lightweight", single static +binary, stateless, Apache-2.0, ~6.9k★ +([README](https://github.com/0xERR0R/blocky)). + +- **In-process DNSSEC validation**: `dnssec.validate` with DO bit, RRSIG + verification, chain-of-trust, NSEC/NSEC3, custom trust anchors, SERVFAIL on + bogus ([DNSSEC validation docs](https://0xerr0r.github.io/blocky/latest/configuration/#dnssec-validation)). +- Upstreams: `parallel_best` (2 random resolvers, fastest answer), `strict`, + `random`; per-client/per-subnet upstream **groups**; UDP/TCP/DoT/DoH/DoQ/DoH3; + DNS stamps; bootstrap DNS + ([upstreams](https://0xerr0r.github.io/blocky/latest/configuration/#upstreams-configuration)). +- Conditional forwarding + `customDNS` mapping/rewrite (the AGH-rewrite + equivalent), hosts files, per-domain upstream routing + ([custom DNS / conditional](https://0xerr0r.github.io/blocky/latest/configuration/#custom-dns)). +- ECS: `ecs.useAsClient` / `ecs.forward` + ([ECS](https://0xerr0r.github.io/blocky/latest/configuration/#edns-client-subnet-options)). +- Cache with min/max TTL + **prefetching**; optional **Redis** cache/state sync + between instances; query log to SQLite/Postgres/CSV; Prometheus metrics; REST + API ([README](https://github.com/0xERR0R/blocky), + [config](https://0xerr0r.github.io/blocky/latest/configuration/)). +- No full web admin UI (metrics/REST/logs only) — an ops trade-off vs AGH's UI. + +Verdict: the most attractive *new software* option for this LAN **as a backend +behind AGH**: it adds real DNSSEC validation + conditional upstream routing + +ECS with a single binary and YAML. It has no China-IP-verdict split built in — +feed it the China domain list via `conditional.mapping`/upstream groups, which +is fine at this scale. One caveat: no GUI means AGH stays the human-facing +front, so AGH→blocky is strictly additive. + +### 3.7 Technitium DNS Server + +What it is: self-hosted authoritative **and** recursive DNS server, .NET, +web console, GPL-3.0, ~9.5k★ +([README](https://github.com/TechnitiumSoftware/DnsServer)). + +- **DNSSEC validation** for recursive resolution, forwarders, and conditional + forwarders (RSA/ECDSA/EdDSA, NSEC/NSEC3); can also *serve* signed zones + ([README](https://github.com/TechnitiumSoftware/DnsServer)). +- Conditional forwarder zones + bulk conditional forwarding app; blocked-domain + lists with regex support and per-client variants; split-horizon/geolocation + via DNS Apps; ECS; QNAME minimization + ([README](https://github.com/TechnitiumSoftware/DnsServer)). +- Serving side: DoH/DoT/DoQ/DoH3 server, built-in DHCP, persistent cache, + caching with serve-stale/prefetch, clustering, HTTP/SOCKS5 proxy for DNS + (e.g. over Tor) ([README](https://github.com/TechnitiumSoftware/DnsServer)). +- Heavier footprint (needs .NET; Docker image available) and a full web console + with many features this LAN won't use. + +Verdict: capable and genuinely feature-rich (a real AGH alternative in the +"everything in one box" sense — filtering, private zones, validation, DHCP), but +it's more moving parts than this LAN needs, and its geo-split still requires +manual conditional-forwarder lists. Not chosen over the lighter AGH+backend +approach. + +### 3.8 sing-box / mihomo built-in DNS as the split resolver (fake-ip) + +The "third option": let the proxy engine's DNS own resolution, AGH on top. + +**sing-box** DNS object: multiple server types (local, udp, tcp, tls, https, +http3, quic, fakeip, hosts, dhcp, mdns…), rule-based server selection by +geoip/geosite, LRU cache + optimistic serving, per-query timeout, `client_subnet` +(ECS), `reverse_mapping` +([sing-box DNS docs](https://sing-box.sagernet.org/configuration/dns/)). + +**mihomo** (Clash.Meta lineage; docs at +[wiki.metacubex.one](https://wiki.metacubex.one/en/config/dns/)): +`nameserver-policy` (geosite/rule-set/domain keys) routes specific domains to +specific resolvers; `fallback` + `fallback-filter` (geoip=CN, geosite=gfw, +ipcidr, domain) adjudicate pollution — a CN resolver's answer is adopted only if +the IP is mainland, otherwise the overseas fallback's answer is used; +`fake-ip`/`redir-host` enhanced mode, `fake-ip-filter` with e.g. `'*.lan'` to +keep local names on real-IP; per-DNS-server ECS; cache-algorithm +([mihomo DNS config](https://wiki.metacubex.one/en/config/dns/)). + +Assessment for THIS LAN: + +- The **pollution adjudication is strong** (geoip-verdict fallback, geosite + lists), and mihomo already runs on the gateway — so "clash DNS as splitter" is + tempting. +- But the DNS service is **coupled to the proxy**: foreign resolution rides the + proxy path, so when OpenClash/subscription is down, fake-ip mapping and + foreign lookups break (partial fail-open only if `direct-nameserver`/fallback + are carefully set). The LAN requirement says **must not hard-depend on the + proxy (fail-open)**. +- fake-ip adds an indirection layer for anything in front of it (AGH on top + resolves client IPs against fake-ip ranges; leaks/loops need careful rules). +- Neither engine **validates DNSSEC** (no RRSIG verification). +- sing-box repo license shows "other" in GitHub metadata (not cleanly + verifiable); mihomo's repo currently carries **deliberately obfuscated content** + ("Void Terminal" parody) — treat `wiki.metacubex.one` as the authoritative + docs and expect the GitHub surface to change. + +Verdict: keep clash/mihomo DNS exactly where it is (proxy-side, TPROXY/fake-ip), +do **not** make it the LAN resolver of record. If you ever want its IP-verdict +quality outside the proxy, chinadns-ng gives the same idea with zero proxy +dependency. + +### 3.9 knot-resolver / dnsdist — power-resolver options + +**knot-resolver** (CZ-NIC): minimal caching validating resolver, modular/Lua, +full DNSSEC validation, forwarding over TLS, query policies, RPZ, views/ACLs, +DNS64, persistent cache, serve-stale, even XDP fast-path +([docs](https://knot-resolver.readthedocs.io/en/stable/)). As powerful as +unbound but with more configuration surface (Lua); overkill for a one-operator +home LAN, though it would do the validating-resolver role well. + +**dnsdist** (PowerDNS): "highly DNS-, DoS- and abuse-aware loadbalancer" — +routes traffic to backend servers, Lua/YAML config, runtime console, metrics +([overview](https://dnsdist.org/)). It is a **balancer, not a validator/cache** +— it fronts other resolvers. Overkill; only relevant if you wanted a +multi-backend DNS LB, which this LAN does not. + +### 3.10 Emerging / also-considered options + +- **AdGuard Home + dnsmasq-china-list** — covered in §3.1/§5; this is the + "emerging best practice" for China CDN splits on AGH and is officially + documented. +- **pi-hole** — adblock/dashboard equivalent of AGH but no per-domain upstream + routing worth choosing it over AGH here (not deeply verified for this write-up; + AGH already satisfies the role). +- **dnscrypt-proxy** — encrypted forwarder with stamp support; a transport + option, not a splitter/validator (not deeply verified for this write-up). +- **coredns** — plugin-based; geo-split is DIY via plugins; no DNSSEC + validation by default (not deeply verified for this write-up). + +### 3.11 Other popular options (survey supplement, 2026-08-12) + +Follow-up survey of additional popular solutions not covered above, evaluated +against this LAN's constraints (fail-open, keep DNS on `.36`, DNSSEC goal). +None of these change the §4/§5 recommendation. + +**Encrypted-forwarder micro-tools (AGH downstream options, not replacements):** + +- **dnscrypt-proxy** — the classic OpenWrt encrypted forwarder with + China-list support and DNS-stamp routing. No in-process DNSSEC validation and + no filtering UI; overlaps with AGH's own DoH upstream layer, so its marginal + value here is low. +- **dnsproxy** (AdGuardTeam) — lightweight DoH/DoT/DoQ forwarder/server. + Functionally a subset of AGH's upstream layer; only useful if forwarding logic + is deliberately split out of AGH. +- **Stubby** — dnsmasq→stubby→DoT (privacy-community pattern). Pure + forwarding, no split/filter/validation; adopting it alone would be a + downgrade from AGH. + +**Managed / cloud DNS (zero-ops, not self-hosted):** + +- **NextDNS / ControlD / AdGuard DNS / Cloudflare** — hosted filtering, logs, + per-device policies. This LAN already self-hosts AGH + a private + `adg.chans.xyz` fallback, so a cloud service would be a downgrade in control + (data leaves the LAN). Only realistic use: add one as an extra foreign-path + upstream inside AGH's `upstream_dns_file`. + +**Heavier all-in-one resolvers:** + +- **PowerDNS Recursor** — real DNSSEC validation + Lua policy, authoritative + and recursive in one. Capable but overlaps unbound; over-provisioned here. +- **BIND9** — classic authoritative/recursive; can validate DNSSEC and, more + interestingly, serve as a local **authoritative zone** that would close the + private-PTR gap. As a LAN resolver it lacks AGH's filtering/UI and is heavier + to operate; a small dnsmasq authoritative zone is a lighter way to achieve the + PTR goal (still deferred until a local authoritative source exists). +- **hickory-dns / trust-dns** (Rust) — emerging recursive resolver, DNSSEC + friendly, smaller ecosystem/ops track record than unbound/blocky; not yet + worth switching for this LAN. + +**Popular stack patterns (structure, not new software):** + +- **Pi-hole + unbound** — the most common global self-hosted combo + (filtering front-end + validating backend). AGH already occupies the + Pi-hole role here (and does more), so the equivalent is **AGH + unbound/ + blocky** — exactly the report's recommendation #2. +- **dnsmasq + china-list + smartdns** (classic OpenWrt trio) — routes the + China list on the gateway itself. Equivalent to co-locating DNS with the + proxy host (`.1`), which violates the fail-open requirement; not recommended + for this LAN. + +Verdict: the survey adds no better candidate. dnsproxy/dnscrypt-proxy duplicate +AGH's upstream layer, cloud DNS is a control downgrade, and the only genuinely +new capability (a local authoritative source for PTR) is better served by a +small dnsmasq authoritative zone than by replacing the resolver. + +--- + +## 4. Architecture recommendation for this LAN + +### 4.1 Preferred architecture (change is config-only) + +``` +clients (DHCP option 6 = .36) + │ UDP/TCP :53 + ▼ +AGH .36 (filtering, rewrites, query log, per-client upstreams) + │ upstream_dns_file: + │ [/cn-domain-list/] dns.alidns.com doh.pub ← CN CDN domains (China list) + │ default: https://adg.chans.xyz/dns-query … ← trusted/foreign path + └→ validating resolver (unbound OR blocky) for the foreign path (optional phase 2) +``` + +- Front = AGH stays the single LAN DNS box (filtering/rewrites/UI/query log + are its strong suit and are already operating). +- Split = AGH per-domain upstreams fed by a converted dnsmasq-china-list; no + new daemon. This is the documented AGH pattern + ([upstreams from file](https://adguard-dns.io/kb/adguard-home/configuration/#upstreams-from-file)). +- Validation = add a validating resolver behind AGH for the trusted path + (blocky simplest; unbound most battle-tested) and re-run the known-bad-signature + check that failed before; then flip `enable_dnssec`. + +### 4.2 Why not the alternatives as front-ends + +- **smartdns / chinadns-ng as the LAN resolver**: they are pure splitters — + no adblock layer, no query log/UI, no DNSSEC. Replacing AGH with either is a + capability downgrade; behind AGH they duplicate AGH's built-in split while + adding a daemon and losing validation. Only chinadns-ng's IP-verdict mode is + genuinely beyond AGH, and it needs kernel ipset/nftset plumbing. +- **mosdns as the AGH backend**: viable splitter, but no validation and its + cache strips EDNS0/DO ([cache plugin](https://irine-sistiana.gitbook.io/mosdns-wiki/mosdns-v5/ru-he-pei-zhi-mosdns/ke-zhi-xing-cha-jian.md)), + which fights the DNSSEC goal. It is already idle on the box — configure it + deliberately or remove it. +- **mihomo/sing-box DNS as the resolver of record**: fail-closed + proxy-coupled + + no validation. Keep as proxy-side concern (§3.8). +- **knot-resolver / dnsdist / Technitium**: capable but over-provisioned; + Technitium is the only one that would *replace* AGH wholesale, and there's no + benefit worth the migration here. + +### 4.3 Deployment location + +- **Keep DNS on `dns.windy.lan` (.36)**. It is already the DHCP-advertised + resolver; it is a separate VM from the proxy host; DNS therefore stays + independent of OpenClash state (fail-open), which is an explicit requirement. +- **Do not move it to `gfw` (.1)**: the gateway is where OpenClash injects + TPROXY/fake-ip/DNS-hijack rules; co-locating LAN DNS there couples DNS to the + proxy and its restart/update lifecycle. +- A standalone resolver VM adds nothing: both current VMs already sit on the + same PVE hypervisor ([lan-overview.md](lan-overview.md) §Positioning facts), + so a hypervisor outage takes out either placement equally; a second physical + host for HA is out of scope for a home LAN. +- If you ever run a validating resolver + AGH on `.36`, verify outbound from + `.36` to the foreign upstreams is not re-hijacked by OpenClash (loop check + already mandated in the [AGH review](adguard-home-official-review-2026-08.md)). + +### 4.4 Fail-open, DNSSEC, private names — by candidate + +| Concern | How the recommended stack behaves | +|---|---| +| Fail-open when proxy/subscription down | AGH forwards directly to DoH upstreams; `.36`'s outbound is not forced through the proxy in normal ops (no TUN policy routing on `.36` — [dns host facts](../hosts/dns.windy.lan.md)). With unbound/blocky behind, foreign resolution recurses/validates directly, independent of OpenClash. Avoid mihomo-DNS-as-resolver, which is proxy-coupled. | +| DNSSEC validation | Only unbound, blocky, knot-resolver, Technitium validate in-process. AGH sets DO only; mosdns/smartdns/chinadns-ng/mihomo/sing-box do not. Plan: validate behind AGH, or accept "validating public upstream" (confirm with `dig +dnssec`/known-bad test). | +| Private names / rewrites | AGH `rewrites` (already in use for `hass.windy.lan`) + `local_ptr_upstreams` once a local PTR source exists. blocky: `customDNS` mapping/rewrite + hosts. unbound: `local-zone`. All adequate. | +| Query log / visibility | AGH is the best at this of everything evaluated (14-day anonymized log already configured). | + +--- + +## 5. What would make the current AGH + mosdns setup genuinely better + +Concrete, in increasing effort: + +1. **Implement the China-list geo-split in AGH itself** + (`upstream_dns_file` + converted `accelerated-domains.china.conf`, default + upstreams = trusted foreign path, `fallback_dns` kept). Official AGH docs + describe exactly this pattern + ([loading upstreams from file](https://adguard-dns.io/kb/adguard-home/configuration/#upstreams-from-file)); + list source: [dnsmasq-china-list](https://github.com/felixonmars/dnsmasq-china-list). + Wire a refresh path (cron/ansible) so the list stays current. Re-test CDN + resolution and the DNSSEC known-bad domain after. +2. **Put a validating resolver on the trusted path** (unbound or blocky), re-run + the known-bad-signature check, then enable AGH DNSSEC. Without this, AGH's + `enable_dnssec` is only a DO-flag — the exact reason it is currently off + ([AGH DNSSEC semantics](https://adguard-dns.io/kb/adguard-home/configuration/), + [host facts](../hosts/dns.windy.lan.md)). +3. **Either fully configure mosdns (systemd service, sequence, lists) or remove + it.** Leaving an idle `127.0.0.1:6052` listener documented as "not the active + path" is drift. If kept, plan around no-EDNS0 cache + no validation; if + removed, drop the listener and its config to reduce surface. +4. **Close the private-PTR gap**: once a local authoritative source exists (e.g. + dnsmasq on `gw`, or a tiny authoritative zone), point AGH + `local_ptr_upstreams` at it as the AGH review recommends + ([AGH review](adguard-home-official-review-2026-08.md)); + don't set it before that source exists + ([dns host facts](../hosts/dns.windy.lan.md)). +5. **Optional: ECS** for CDN geo-accuracy — AGH `edns_client_subnet.use_custom` + with a coarse fixed prefix (or blocky `ecs.forward`) if measurements show a + benefit; note many CN resolvers ignore ECS + ([AGH ECS](https://adguard-dns.io/kb/adguard-home/configuration/)). + +If the DNS engineering budget is one afternoon, do #1 + #3. If the goal is +"real DNSSEC or nothing", do #1 + #2 + #3. Replacing the stack is only +justified if you want to abandon AGH's UI/filtering entirely — nothing evaluated +here beats it on that axis for this LAN. + +--- + +## Caveats / not verified + +- **Live behavior not tested**: all capability claims are from primary docs + reviewed 2026-08-12; DNSSEC behavior of `dns.alidns.com`/`doh.pub`/the + `adg.chans.xyz` path and mosdns's actual version on `.36` need on-box + `dig +dnssec` verification (per [adguard-home-health](../runbooks/adguard-home-health.md)). +- **smartdns DNSSEC**: the official config reference lists no DNSSEC option; + if a newer version added one, it is not reflected here + ([config options](https://pymumu.github.io/smartdns/configuration/)). +- **mosdns geosite/geoip**: v5 source tree (fetched 2026-08-12) contains only + `domain_set`/`ip_set` data providers; if a `geosite.dat` plugin exists in a + release branch, it is not in `main` + ([plugin/data_provider](https://github.com/IrineSistiana/mosdns/tree/main/plugin/data_provider)). +- **mihomo**: the GitHub repo currently shows deliberately obfuscated metadata + (see §3.8); capabilities cited from + [wiki.metacubex.one](https://wiki.metacubex.one/en/config/dns/). + sing-box/knot-resolver/dnsdist license identifiers via GitHub metadata are + "other"/custom — treat the specific SPDX ids with caution. +- **chinadns2** README was not retrieved (404 on the raw URL); treated as the + legacy predecessor of chinadns-ng and not evaluated in depth. +- Obsidian/personal notes were not consulted; this is upstream-docs-only. + +## Related docs + +- [lan-overview.md](lan-overview.md) — full topology (verified 2026-08-06) +- [hosts/dns.windy.lan.md](../hosts/dns.windy.lan.md) — AGH host facts +- [hosts/gfw.windy.lan.md](../hosts/gfw.windy.lan.md) — OpenClash facts +- [adguard-home-official-review-2026-08.md](adguard-home-official-review-2026-08.md) — prior AGH config review +- [runbooks/adguard-home-health.md](../runbooks/adguard-home-health.md) diff --git a/runbooks/ansible-operations.md b/runbooks/ansible-operations.md index 9bb5ceb..4ac9461 100644 --- a/runbooks/ansible-operations.md +++ b/runbooks/ansible-operations.md @@ -60,6 +60,30 @@ ansible-playbook playbooks/maintenance-preview.yml ansible-playbook playbooks/baseline.yml ``` +## us4 firewalld reconciliation + +The us4 playbook owns only the audited `public` zone allowlist. It fails closed +on unknown services or ports, never reloads/restarts firewalld, and does not +manage Docker-published ports. + +```bash +cd ansible +ansible-galaxy collection install -r requirements.yml + +# Read-only preview +ansible-playbook playbooks/us4-firewalld.yml --limit us4 --check --diff + +# Apply only after testing the provider console and retaining an independent +# SSH rollback session. +ansible-playbook playbooks/us4-firewalld.yml --limit us4 \ + -e '{"us4_firewalld_confirm": true, "us4_console_confirm": true}' +``` + +Apply creates a protected server-local backup and schedules a 15-minute +automatic rollback before changing rules. The rollback is cancelled only after +the playbook verifies fresh SSH/sudo access, public HTTPS routes, SMTP, Docker, +Fail2ban, and WireGuard. Do not bypass either confirmation variable. + ## UniFi SSO login setting (mutating) Reconciles `super_sdn.sso_login_enabled` on the UniFi controller (host `ubnt`, diff --git a/runbooks/pdns-health.md b/runbooks/pdns-health.md index 1313b49..e4dc45d 100644 --- a/runbooks/pdns-health.md +++ b/runbooks/pdns-health.md @@ -18,7 +18,7 @@ Use these only after the Ansible health report needs investigation. ssh -4 windy@hk2.chans.xyz 'cd /opt/pdns && docker compose ps -a' ``` -Expect `auth`, `db`, `poweradmin` healthy; `backup` Up; `pgweb` Up. Ignore stopped orphan `powerdns-admin` unless cleaning orphans. +Expect `auth`, `db`, `poweradmin` healthy; `backup` Up; `pgweb` Up. The legacy PDA orphan `powerdns-admin` was removed 2026-08-12 (W1N-59). ### Version / security poll