5.8 KiB
5.8 KiB
Quickstart: Smart DNS Server Resolution Modes
Feature Branch: 002-server-resolution-modes
Date: 2026-03-04
Prerequisites
- Go 1.20 toolchain installed and on
PATH - Repository cloned and on branch
002-server-resolution-modes go mod tidyrun successfully
Build
# From repository root
go build -o dns-helper.exe .
Cross-compile (existing build.ps1 pattern):
.\build.ps1
Run Tests
go test ./... -v
TDD Workflow
This feature follows the Red-Green-Refactor cycle mandated by Constitution Principle VI.
Order of Implementation
The recommended implementation order (each step follows TDD):
resolver/parse.go—ParseServerFlag()andExtractLabelLevels()(pure functions, no I/O)platform/network.go—NetworkInfotype,NetworkDiscovererinterface,FakeNetworkDiscovererplatform/network_windows.go— WindowsDiscover()implementationplatform/network_linux.go— LinuxDiscover()implementationplatform/network_darwin.go— macOSDiscover()implementationresolver/pool.go—BuildResolverPool()andBootstrapResolversresolver/transport.go— GeneralizedudpQuery()shared transport (extract from existingresolver.go)resolver/authority.go—ParallelNSFanOut(),SelectAuthoritativeNS(),QueryAuthoritative()resolver/splithorizon.go—CheckSplitHorizon()(Stage 2.5 cross-check)resolver/fallback.go—ParallelAFallback()resolver/modes.go—Resolve()mode dispatcher integrating all componentsmain.go— Update CLI flag parsing, wire new resolver pipeline
Per-Step TDD Cycle
For each item above:
- Red: Write the test in
*_test.go— it should fail (function doesn't exist yet). - Green: Implement the minimum code to make the test pass.
- Refactor: Clean up while keeping tests green.
Example for ParseServerFlag:
// resolver/parse_test.go
func TestParseServerFlag_Default(t *testing.T) {
mode, err := resolver.ParseServerFlag("")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mode.Mode != "default" {
t.Errorf("expected mode 'default', got %q", mode.Mode)
}
}
func TestParseServerFlag_Local(t *testing.T) {
mode, err := resolver.ParseServerFlag("local")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mode.Mode != "local" {
t.Errorf("expected mode 'local', got %q", mode.Mode)
}
}
func TestParseServerFlag_InvalidPort(t *testing.T) {
_, err := resolver.ParseServerFlag("10.0.0.53:0")
if err == nil {
t.Fatal("expected error for port 0")
}
}
Testing Parallel Fan-out
Use the existing fake DNS server helpers from resolver/resolver_test.go:
// Start multiple fake DNS servers, each responding to NS queries differently.
// Pass their addresses as the resolver pool to ParallelNSFanOut.
ns1 := startFakeDNS(t, handleNSQuery("example.com", []string{"ns1.example.com."}))
ns2 := startFakeDNS(t, handleNSQueryNXDOMAIN)
results := resolver.ParallelNSFanOut(ctx, []string{ns1, ns2}, []string{"example.com"}, 3*time.Second)
// Assert: 2 results, one with NS records, one with nil
Testing Platform Discovery
Use FakeNetworkDiscoverer for unit tests — never call real OS commands in unit tests:
fake := &platform.FakeNetworkDiscoverer{
Info: platform.NetworkInfo{
DNSServers: []string{"10.0.0.1", "10.0.0.2"},
Gateway: "10.0.0.1",
Interface: "eth0",
},
}
pool := resolver.BuildResolverPool(resolver.ServerMode{Mode: "default"}, fake.Info)
// Assert: pool contains local resolvers + bootstrap set
Platform-specific integration tests (build-tagged) can test real os/exec parsing:
//go:build windows
func TestWindowsDiscover(t *testing.T) {
d := &platform.WindowsNetworkDiscoverer{}
info, err := d.Discover()
// Assert: no error, DNSServers non-empty on a typical system
}
Manual Verification
Smart Default Resolution (no -server)
# Should discover authoritative NS for example.com and query it directly
.\dns-helper.exe add -host www.example.com -verbose
Local Resolver Mode
.\dns-helper.exe add -host internal.corp.local -server local -verbose
Gateway Mode
.\dns-helper.exe add -host www.example.com -server gateway -verbose
Explicit IP (existing, unchanged)
.\dns-helper.exe add -host www.example.com -server 8.8.8.8
Timeout Override
.\dns-helper.exe add -host www.example.com -timeout 1 -verbose
Key Files Changed (Summary)
| File | Change |
|---|---|
main.go |
Updated runAdd(): -server optional, new -timeout/-verbose flags, mode dispatch |
resolver/resolver.go |
Existing code unchanged; generalized UDP transport extracted |
resolver/parse.go |
New: ParseServerFlag(), ExtractLabelLevels() |
resolver/pool.go |
New: BuildResolverPool(), BootstrapResolvers |
resolver/transport.go |
New: shared udpQuery() transport layer |
resolver/authority.go |
New: ParallelNSFanOut(), SelectAuthoritativeNS(), QueryAuthoritative() |
resolver/splithorizon.go |
New: CheckSplitHorizon() — Stage 2.5 cross-check |
resolver/fallback.go |
New: ParallelAFallback() |
resolver/modes.go |
New: Resolve() mode dispatcher |
platform/platform.go |
Extended with NetworkInfo, NetworkDiscoverer interface |
platform/platform_windows.go |
Extended: Discover() via netsh |
platform/platform_linux.go |
Extended: Discover() via ip route + /etc/resolv.conf |
platform/platform_darwin.go |
Extended: Discover() via route + scutil |