--- project: dns-knot-pihole-end-to-end type: session-notes status: completed path: Tech/Sessions tags: - homelab - dns - knot - pihole - openwrt - traefik created: 2026-05-08 updated: 2026-05-08 --- # DNS — Knot + Pi-hole End-to-End ## Outcome Phase 1 of the Traefik project closed. Internal DNS architecture from the homelab DNS decision doc is live: Knot DNS authoritative for `herbylab.dev` on LXC 103, Pi-hole conditionally forwarding the zone, OpenWrt passing answers through. All six initial records resolve cleanly from a normal LAN client through the full chain. ## Architecture (as built) Client → OpenWrt (10.0.11.1) → Pi-hole (10.0.11.50) ├─ herbylab.dev → Knot (10.0.11.10) └─ everything else → Unbound → root ## Records served | Name | IP | VLAN | |---|---|---| | ns1 | 10.0.11.10 | Lab | | immich | 10.0.11.62 | Lab | | pihole | 10.0.11.50 | Lab | | plex | 10.0.11.54 | Lab | | pve | 10.0.21.188 | Private | | traefik | 10.0.21.181 | Private | Migrated from `.lan` to `.dev` to enable LE wildcard via Cloudflare DNS-01 in Phase 2. ## Key Learnings - **`knotc reload` does not re-bind sockets.** It reloads the config file but listening sockets only get re-evaluated on full daemon restart. If `listen:` directives change (or were missing on first start), `systemctl restart knot` is required. Symptom: zone loads, `zone-status` reports correctly, but `dig` returns "connection refused." - **Knot config order matters for references.** `acl` blocks must be defined before `zone` blocks that reference them — YAML validation is single-pass, no forward references. Same pattern likely applies to other ID-based references. - **Fresh PVE LXCs inherit Tailscale resolvers from the host.** `/etc/resolv.conf` gets populated with `100.100.100.100` by default. If the LXC isn't on the tailnet, name resolution silently fails until pointed at a reachable resolver. Permanent fix: configure DNS in the LXC's PVE config or, for a DNS server LXC, point it at itself once it's serving. - **OpenWrt rebind protection silently filters RFC1918 answers.** When dnsmasq has `rebind_protection='1'`, any DNS answer pointing to a private IP gets stripped — query returns NOERROR with ANSWER:0 and EDE 15 (Blocked). The fix is whitelisting the domain via `rebind_domain`. Easy to miss because the block happens *after* the upstream answers correctly, so direct queries to Pi-hole or Knot work fine while client-path queries fail. - **EDE 15 (Blocked) doesn't always mean a blocklist match.** It's a generic "this answer was filtered" code. Pi-hole's blocklists, OpenWrt's rebind protection, and several other filters all use it. Pi-hole's query log saying "Allowed" while `dig` reports EDE 15 is the tell — block is happening downstream of Pi-hole. - **Pi-hole v6 renamed conditional forwarding.** It's now under reverse-server config with format `,,,`. Same plumbing, different UI surface. - **Direct-vs-chain queries are the diagnostic shortcut.** When a DNS chain fails, querying each link directly with `dig @` immediately localizes the broken hop. Saved real time tonight isolating the OpenWrt rebind issue. ## Decisions Locked - Zone file lives at `/var/lib/knot/herbylab.dev.zone` on LXC 103, owned by `knot:knot` - Single NS record (`ns1`) for now; secondary will be added when NAS instance comes up - TTL strategy: 300 during buildout (current), raise to 3600 once stable - SOA serial format: `YYYYMMDDNN` (current: `2026050801`) - ACL allows transfer/notify from localhost only — will expand when secondary is added - Conditional forwarding scoped to `10.0.0.0/8` in Pi-hole — VLAN/firewall layer enforces actual access boundaries ## Open Items - [ ] LXC `/etc/resolv.conf` is a manual edit pointing at Pi-hole — repoint at itself (10.0.11.10) with Pi-hole as fallback, make persistent - [ ] Knot zone file into git as source of truth - [ ] Ansible role for Knot primary deployment - [ ] Knot secondary on the NAS (after primary proven stable) - [ ] Second Pi-hole instance for redundancy - [ ] Pi-hole sync method decision (gravity-sync vs Teleporter) - [ ] Move Traefik VM to Lab VLAN (currently on Private from original bridge setup) - [ ] Raise TTLs to 3600 once buildout settles ## Next Session Phase 2 — wildcard cert via Traefik + Cloudflare DNS-01. DNS layer is done.