# Implementation Plan: Smart DNS Server Resolution Modes **Branch**: `002-server-resolution-modes` | **Date**: 2026-03-04 | **Spec**: [spec.md](spec.md) **Input**: Feature specification from `/specs/002-server-resolution-modes/spec.md` ## Summary Extend the dns-helper CLI's `-server` flag to support multiple resolution modes: smart default resolution (parallel NS fan-out across local + public resolvers for authoritative answers), `-server local` (local resolvers only), `-server gateway` (default gateway as DNS), and the existing explicit IP mode. Key technical work: implement parallel NS fan-out across all label levels and all resolvers, authoritative NS query with recursion disabled, CNAME-aware re-resolution, parallel A-query fallback for internal hosts without zone delegation, OS-level local resolver and gateway discovery (cross-platform), configurable per-query timeout, verbose diagnostic output to stderr, context-aware error messages for private TLDs, and split-horizon conflict detection (Stage 2.5: cross-check authoritative answer against local resolvers to catch enterprise DNS overrides for public TLD hostnames). ## Technical Context **Language/Version**: Go 1.20 (ceiling per constitution; `go.mod` specifies `go 1.20`) **Primary Dependencies**: Go standard library, `golang.org/x/net/dns/dnsmessage` (approved; already in go.mod v0.35.0) **Storage**: Local filesystem (system hosts file, backup files, lock file) — unchanged from 001 **Testing**: `go test ./...` with TDD (Red-Green-Refactor); fake DNS servers via UDP listeners; interfaces for OS-level network config discovery **Target Platform**: Windows, Linux, macOS (cross-compiled single binary) **Project Type**: CLI tool (single binary, no config files) **Performance Goals**: Parallel NS fan-out must complete within the latency of the slowest single resolver (SC-002); error reporting within 10 seconds (SC-005) **Constraints**: Go 1.20 ceiling; zero unapproved external dependencies; hosts file safety (unchanged); UDP-only DNS (no TCP fallback) **Scale/Scope**: ~800 LOC current across 4 packages; this feature adds ~800-1200 LOC of new resolver logic, platform discovery, and tests ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* ### Principle I: System File Safety - **Status**: PASS — No changes to the hosts file write path. The atomic write (temp+rename), backup, and lock mechanisms from 001 remain intact and unmodified. This feature only changes what IP address is written, not how it is written. ### Principle II: Standard Library First - **Status**: PASS — All new functionality uses Go standard library and `golang.org/x/net/dns/dnsmessage` (approved source, already in go.mod). Parallel operations use goroutines and channels (stdlib). OS-level network config discovery uses `os/exec` to call platform utilities (`ipconfig`, `ip route`, `networksetup`, `scutil`), which is standard library. - **Risk**: Local resolver/gateway discovery requires parsing OS command output. This is fragile but the standard library approach — no approved Go library exists for cross-platform network adapter enumeration under Go 1.20. ### Principle III: Go 1.20 Compatibility - **Status**: PASS — No new language features or stdlib APIs beyond Go 1.20 are needed. `sync.WaitGroup`, channels, goroutines, `context` (available since Go 1.7), `os/exec` — all available in Go 1.20. `golang.org/x/net` v0.35.0 is already pinned and Go 1.20-compatible. ### Principle IV: Idiomatic Error Handling - **Status**: PASS — All new functions (resolver pool construction, NS fan-out, local resolver discovery, gateway discovery) will return errors to callers. No `os.Exit` outside `main()`. Error messages will include context (hostname, resolver IP, stage information). The parallel fan-out collects individual query errors without treating them as fatal — only aggregate failure (no results from any resolver) produces a user-facing error. ### Principle V: Simplicity and Single Purpose - **Status**: PASS — The tool's purpose is unchanged: update the local hosts file with DNS entries. The new resolution modes make the tool smarter about *how* it resolves, but add no new subcommands, no configuration files, no daemons. The `-server` flag gains new keyword values (`local`, `gateway`) and the default behavior becomes smart resolution when omitted. No speculative features added. - **Note**: The parallel NS fan-out adds inherent complexity. This is justified by the core user scenario (US-1) which requires authoritative resolution for both public and internal hostnames without user configuration. ### Principle VI: Test-Driven Development - **Status**: PASS — All new code will follow TDD (Red-Green-Refactor). Platform-specific discovery functions will accept interfaces for testability. The resolver package already has fake DNS server test helpers that will be extended for NS queries. Parallel fan-out logic will be tested with deterministic fake resolver pools. ### Gate Evaluation All six constitution principles are satisfied. No violations exist in the current codebase that this feature introduces or worsens. **GATE: PASS** — proceed to Phase 0. ## Project Structure ### Documentation (this feature) ```text specs/002-server-resolution-modes/ ├── plan.md # This file ├── research.md # Phase 0 output ├── data-model.md # Phase 1 output ├── quickstart.md # Phase 1 output ├── contracts/ # Phase 1 output └── tasks.md # Phase 2 output (created by /speckit.tasks) ``` ### Source Code (repository root) ```text dns-helper/ ├── main.go # Entry point, CLI parsing, os.Exit — updated for new -server modes, -timeout, -verbose ├── resolver/ │ ├── resolver.go # Existing: single-server A-record lookup (kept for -server mode) │ ├── resolver_test.go # Existing tests │ ├── pool.go # NEW: ResolverPool (bootstrap set + local resolvers), parallel fan-out │ ├── pool_test.go # NEW: Tests for pool construction and parallel fan-out │ ├── authority.go # NEW: Authoritative NS discovery (Stage 1), authoritative query (Stage 2), fallback (Stage 3) │ ├── authority_test.go # NEW: Tests for NS fan-out, authoritative query, CNAME re-resolution │ ├── modes.go # NEW: Mode dispatcher (smart default, local, gateway, explicit IP) │ └── modes_test.go # NEW: Tests for mode selection and dispatch ├── platform/ │ ├── platform.go # Package doc — extended with network discovery interface │ ├── platform_windows.go # Existing: GetHostsFilePath + NEW: GetLocalResolvers, GetDefaultGateway │ ├── platform_linux.go # Existing: GetHostsFilePath + NEW: GetLocalResolvers, GetDefaultGateway │ ├── platform_darwin.go # Existing: GetHostsFilePath + NEW: GetLocalResolvers, GetDefaultGateway │ └── platform_test.go # Extended with discovery tests ├── hostfile/ │ ├── hostfile.go # Unchanged │ ├── hostfile_test.go # Unchanged │ ├── managed.go # Unchanged │ ├── managed_test.go # Unchanged │ └── filesystem.go # Unchanged ├── lockfile/ │ ├── lockfile.go # Unchanged │ └── lockfile_test.go # Unchanged ├── go.mod └── go.sum ``` **Structure Decision**: Extend existing package structure. New resolver logic goes into the `resolver/` package in separate files (`pool.go`, `authority.go`, `modes.go`) to maintain single-responsibility per file without introducing new top-level packages. Platform-specific network discovery extends the existing `platform/` package files. The `hostfile/` and `lockfile/` packages are untouched. This follows the established patterns from 001. ## Complexity Tracking > No constitution violations require justification. The parallel NS fan-out is the most complex new component but is justified by the core user scenario (US-1) and explicitly required by FR-013/FR-014. ## Constitution Re-Check (Post Phase 1 Design) *All six principles re-evaluated after design phase. No new violations introduced.* | Principle | Status | Evidence | |-----------|--------|----------| | I. System File Safety | PASS | No changes to the hosts file write path. Atomic write, backup, and lock mechanisms from 001 untouched. Only the DNS resolution layer changes — different IPs may be written, but the write mechanism is identical. | | II. Standard Library First | PASS | All new code uses Go stdlib + `golang.org/x/net/dns/dnsmessage` (approved, already in go.mod). Platform discovery uses `os/exec` to call OS utilities — no new dependencies. Parallel fan-out uses goroutines + channels (stdlib). | | III. Go 1.20 Compatibility | PASS | All APIs verified: `context.WithTimeout` (Go 1.7), `net.Dialer.DialContext` (Go 1.7), `errors.Join` (Go 1.20), `os/exec` (Go 1.0), `dnsmessage.TypeNS`/`parser.NSResource()` (present in x/net v0.35.0). No Go 1.21+ features used. `slices`, `maps`, `slog`, `context.WithTimeoutCause` explicitly avoided. | | IV. Idiomatic Error Handling | PASS | All new functions return errors. `Resolve()` returns errors to `main.go`. `Discover()` returns errors to caller. Parallel fan-out collects per-query errors in `NSResult.Err` without fatal exits. Error messages include hostname, resolver IP, stage context, and private TLD hints. Only `main()` calls `os.Exit()`. | | V. Simplicity and Single Purpose | PASS | Tool purpose unchanged: update hosts file with DNS entries. No new subcommands, config files, daemons, or TUI. The `-server` flag gains keyword values; `-timeout` and `-verbose` are standard CLI patterns. New files follow single-responsibility: `parse.go` (parsing), `pool.go` (pool construction), `authority.go` (NS fan-out), `fallback.go` (A fallback), `modes.go` (dispatch). | | VI. Test-Driven Development | PASS | All new code designed for TDD. `FakeNetworkDiscoverer` enables deterministic pool/mode testing. Existing `startFakeDNS`/`startFakeDNSMulti` test helpers extend to NS queries. `ParseServerFlag` and `ExtractLabelLevels` are pure functions with straightforward table-driven tests. Platform integration tests are build-tagged. |