10 KiB
Implementation Plan: Smart DNS Server Resolution Modes
Branch: 002-server-resolution-modes | Date: 2026-03-04 | Spec: 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 usesos/execto 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/netv0.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.Exitoutsidemain(). 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
-serverflag 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)
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)
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 <ip> 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. |