# DNS resolution timing check

## What changed

Added DNS resolution timing as a new diagnostic check. The tool detects the system's configured DNS resolver (via resolvectl, nmcli, or /etc/resolv.conf), measures it directly via dnspython, and compares against public alternatives (Google, Cloudflare, OpenDNS).

### New files
- `yslow/checks/dns.py` — measurement module, all resolvers tested via dnspython UDP queries
- `tests/test_dns_check.py` — unit tests for the check module
- `tests/test_dns_diagnosis.py` — threshold and comparison logic tests

### Modified files
- `pyproject.toml` — added `dnspython>=2.7` dependency
- `yslow/models.py` — added `DnsResult` dataclass (with `is_configured` flag), added `dns` field to `CheckResult`
- `yslow/net.py` — added `get_system_nameservers()` with resolvectl → nmcli → resolv.conf fallback chain, filters out 127.0.0.53 stub
- `yslow/diagnosis.py` — added `_analyze_dns()` with uncached thresholds and configured-vs-public comparison
- `yslow/runner.py` — wired DNS check into `run_once()`
- `yslow/ui/common.py` — renders DNS results per-resolver, configured resolver marked with ✱
- `docs/metrics.md` — documented DNS thresholds and methodology
- `TODO.md` — marked DNS resolution timing as complete

### Measurement approach
- **Configured resolver detected** from resolvectl (systemd-resolved), nmcli (NetworkManager/DHCP), or /etc/resolv.conf. Only the first (primary) is tested.
- **All resolvers tested the same way** — direct UDP queries via dnspython. No getaddrinfo() (that just measures systemd-resolved's local cache, always ~0.3ms, not useful).
- **Cached**: query google.com twice, discard first (warm), keep second
- **Uncached**: query 3 random subdomains of google.com (guaranteed cache miss, NXDOMAIN), take median
- **Deduplication**: if the configured resolver is already in the public list (e.g. user's DHCP gives 8.8.8.8), it's tested once and marked as configured

### Thresholds
Only uncached time is checked — cached time for remote resolvers is just network RTT, not actionable.

| Metric | Warning | Error |
|--------|---------|-------|
| Uncached | > 100 ms | > 250 ms |
| Configured vs best public | > 2× slower (and not already flagged) | — |

## Key decisions
- **Dropped cached thresholds entirely** — originally had 5ms/20ms cached thresholds, but these only make sense for local resolvers. When the configured resolver is remote (common: DHCP gives Google/Cloudflare), 25ms cached is just RTT, not a problem.
- **Dropped getaddrinfo()-based system resolver test** — it measures systemd-resolved's in-memory cache (~0.3ms always), not the actual upstream resolver speed. Testing the upstream directly via dnspython is more useful.
- **Only test first configured nameserver** — multiple nameservers from the same provider (e.g. 8.8.8.8 + 8.8.4.4) are redundant. The primary is what the system uses.
- **Comparison issue suppressed when already flagged** — if the configured resolver is over the absolute uncached threshold, don't also say "slower than public" (redundant).
- **Deferred DoH and DNSSEC** — DoH is already captured implicitly if the system uses it; DNSSEC overhead shows up in resolver response times. Both are separate future features.

## Open questions
- Should we run DNS checks in parallel with ping checks for faster execution?
- If no configured resolver is detected (all sources fail), should we fall back to getaddrinfo()?
