dns-ops
Expert knowledge for Pi-hole and Unbound DNS operations. Use when configuring DNS, troubleshooting resolution issues, modifying adlists, or understanding the DNS data flow.
安装与使用
请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。
查看来源 ↗使用指南技能说明
DNS Operations (Pi-hole + Unbound)
Architecture (HA)
┌─ Pi-hole (pi-k3s:53) ──── Unbound (pi-k3s:5335) ──── Root Servers
User Device ───┤
└─ Pi-hole-secondary (pi5-worker-1:53) ──── Unbound-secondary (pi5-worker-1:5335) ──── Root Servers
Both paths are independent. Clients may use either Pi-hole instance via DHCP-assigned DNS.
Why Unbound? Full recursive resolution directly to authoritative DNS servers. Better privacy (no single upstream sees all queries), no third-party trust required, DNSSEC validation.
Service Details
Pi-hole (Primary + Secondary)
-
Image:
pihole/pihole:latestwithhostNetwork: true -
Primary: Port 53 on
pi-k3s(192.168.1.55) -
Secondary: Port 53 on
pi5-worker-1(192.168.1.56) - Password: Synced from 1Password via ExternalSecret
- Upstream DNS: Respective Unbound ClusterIP service
- Adlists: Firebog curated lists via ConfigMap (~900k domains)
- DNSSEC: Disabled on Pi-hole (handled by Unbound)
- Caching: Pi-hole maintains its own DNS cache
Unbound (Primary + Secondary)
-
Image:
madnuttah/unbound:latest(distroless, minimal — no cat/ls/head) - Port: 5335 (non-privileged) via ClusterIP service
- Type: Recursive resolver with DNSSEC validation
- Node Placement: Pi 5 nodes ONLY via nodeSelector (Pi 3 hardware causes TCP failures)
- Performance: ~21ms uncached, 0-15ms cached
Configuration
Pi-hole v6 API (Critical)
Pi-hole v6 ignores most environment variables. Configuration is done via REST API in postStart hook:
-
POST /api/auth- Get session ID -
PATCH /api/config- Set upstream DNS to Unbound ClusterIP -
POST /api/lists- Add adlists from ConfigMap (batch format) -
POST /api/action/gravity- Update gravity database
Reference: docs/pihole-v6-api.md
madnuttah/unbound Config Quirks (CRITICAL)
The madnuttah/unbound image rewrites the main unbound.conf during its entrypoint. It transforms paths and drops directives it doesn't recognize.
-
Compiled config path:
/usr/local/unbound/unbound.conf(NOT/opt/unbound/etc/unbound/unbound.conf) -
Our mount:
/opt/unbound/etc/unbound/unbound.conf→ entrypoint processes this into the actual config -
Dropped directives:
domain-insecureand other less common server directives are silently dropped -
conf.d directory:
/usr/local/unbound/conf.d/*.conf— mount custom server directives HERE as separate files -
Verify running config:
kubectl exec -n pihole deploy/unbound -- unbound-checkconf -o <directive>
To add custom server directives (e.g., domain-insecure):
- Add the directive to the ConfigMap as a separate data key (e.g.,
dnssec-exceptions.conf) - Mount it at
/usr/local/unbound/conf.d/<name>.confusingsubPath - Do NOT put it in the main
unbound.conf— it will be silently dropped
DNSSEC Validation
Unbound validates DNSSEC via:
-
auto-trust-anchor-file— root trust anchor -
harden-dnssec-stripped: yes— strict mode, rejects responses that strip DNSSEC
When a domain has broken DNSSEC (DS records published but no valid DNSKEY):
- Unbound returns SERVFAIL
- Unbound logs show:
validation failure <domain>: no keys have a DS with algorithm RSASHA256 - Fix: Add
domain-insecure: "domain.com"via conf.d mount (see above)
Caching Behavior (CRITICAL for Diagnostics)
Unbound serve-expired configuration:
-
serve-expired: yeswithserve-expired-ttl: 86400(24 hours) - Unbound serves stale cache immediately while refreshing in background
- This means a broken upstream can be masked by stale cache for up to 24 hours
Pi-hole also caches results independently.
Combined effect: test_dns_query (which runs dig inside Pi-hole) can return a successful cached result even when Unbound is actively returning SERVFAIL. A successful test_dns_query does NOT prove the resolution path is healthy. Always use diagnose_dns for troubleshooting.
DNS Resilience
The Pi node uses static DNS (1.1.1.1, 8.8.8.8) configured via NetworkManager.
- Why: Ensures the Pi can pull images (like Pi-hole itself) even if the cluster DNS is down.
IPv6 Blocking (AT&T Routing Issues)
AT&T Fiber has poor IPv6 routing to some CDNs. We selectively block IPv6 for affected domains.
Current blocked domains: See clusters/pi-k3s/pihole/pihole-custom-dns.yaml
IMPORTANT: Test Before Blocking
When a user reports slow/broken connectivity to a service, DO NOT immediately add it to the IPv6 block list. First verify IPv6 is the cause:
# 1. Check if domain returns AAAA records (if no AAAA, IPv6 isn't the issue)
dig AAAA <domain>
# 2. Compare IPv4 vs IPv6 response times from a device on the network
curl -4 -w "IPv4: %{time_total}s\n" -o /dev/null -s https://<domain>
curl -6 -w "IPv6: %{time_total}s\n" -o /dev/null -s https://<domain>
# 3. If IPv6 is significantly slower (2x+) or times out, add to block list
Only add to the block list after confirming IPv6 is the problem. See docs/known-issues.md for details.
DNS Troubleshooting Runbook (MANDATORY)
When a user reports a domain is unreachable, follow ALL steps in order. Do NOT stop after step 1 even if it shows success. Do NOT blame the client's browser or machine until the entire server path is proven clean.
Step 1: Run diagnose_dns MCP Tool (ALWAYS START HERE)
Use diagnose_dns with the reported domain. This single tool tests:
- Pi-hole resolution (may be cached)
- Unbound primary direct resolution (bypasses cache)
- Unbound secondary direct resolution (bypasses cache)
- DNSSEC validation check (if Unbound fails, retries with +cd)
- Unbound pod logs filtered for the domain
If diagnose_dns is unavailable, manually run all substeps:
test_dns_query → check Unbound logs (BOTH pods) → dig @unbound directly
Step 2: Interpret Results
| Pi-hole | Unbound Primary | Unbound Secondary | Diagnosis |
|---|---|---|---|
| OK | OK | OK | Resolution path is healthy. Issue is client-side. |
| OK | FAIL | FAIL | Stale cache masking upstream failure. Check Unbound logs immediately. |
| OK | OK | FAIL | Secondary Unbound is broken. Client may be using secondary. |
| FAIL | FAIL | FAIL | Complete DNS failure. Check pod health, network connectivity. |
| OK (cached) | FAIL + resolves with +cd | FAIL + resolves with +cd | DNSSEC validation failure. Domain has broken DNSSEC. |
Step 3: Check Unbound Logs (NEVER SKIP)
Use get_pod_logs for BOTH Unbound pods:
-
namespace: pihole, pod: unbound -
namespace: pihole, pod: unbound-secondary
Look for:
-
validation failure— DNSSEC issue -
SERVFAIL— upstream failure -
connection timed out— network issue -
TCP connection failed— TCP upstream issue (common on Pi 3)
Step 4: Apply Fix Based on Root Cause
| Root Cause | Fix |
|---|---|
| DNSSEC validation failure | Add domain-insecure via conf.d ConfigMap mount |
| Unbound timeout/crash | Restart Unbound deployment, check node health |
| Pi-hole not forwarding | Check Pi-hole upstream DNS config via API |
| Network issue | Check node connectivity to root servers |
Common Issues
-
"Refused": Check if Pi-hole is running and
hostNetworkis true. - "ServFail": ALWAYS check Unbound logs. Often DNSSEC failure, upstream timeout, or TCP failure on Pi 3 nodes.
-
Adlists not loading: Check
postStarthook logs in Pi-hole pod. -
test_dns_queryshows success but client fails: Stale cache. Usediagnose_dnsinstead.
Relevant Files
-
clusters/pi-k3s/pihole/pihole-deployment.yaml— Primary Pi-hole -
clusters/pi-k3s/pihole/pihole-secondary-deployment.yaml— Secondary Pi-hole -
clusters/pi-k3s/pihole/unbound-deployment.yaml— Primary Unbound + Service -
clusters/pi-k3s/pihole/unbound-secondary-deployment.yaml— Secondary Unbound + Service -
clusters/pi-k3s/pihole/unbound-configmap.yaml— Unbound config (shared by both, includes conf.d entries) -
clusters/pi-k3s/pihole/pihole-custom-dns.yaml— Local DNS + IPv6 overrides
使用示例
查看来源仓库中的使用示例与配置说明。
查看来源 ↗来源历史
- 收录时间
- 2026-03-11
- 索引更新时间
- 2026-03-11
- Branch
- main
收录与同步日期属于本站记录。源码修改历史请查看来源仓库。
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!