114 lines
10 KiB
Markdown
114 lines
10 KiB
Markdown
# 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 <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. |
|