pbs-projects/Tech/Sessions/dns-knot-pihole-end-to-end.md

116 lines
4.3 KiB
Markdown

---
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.