Files
dnshelper/specs/002-server-resolution-modes/quickstart.md
T

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 tidy run 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):

  1. resolver/parse.goParseServerFlag() and ExtractLabelLevels() (pure functions, no I/O)
  2. platform/network.goNetworkInfo type, NetworkDiscoverer interface, FakeNetworkDiscoverer
  3. platform/network_windows.go — Windows Discover() implementation
  4. platform/network_linux.go — Linux Discover() implementation
  5. platform/network_darwin.go — macOS Discover() implementation
  6. resolver/pool.goBuildResolverPool() and BootstrapResolvers
  7. resolver/transport.go — Generalized udpQuery() shared transport (extract from existing resolver.go)
  8. resolver/authority.goParallelNSFanOut(), SelectAuthoritativeNS(), QueryAuthoritative()
  9. resolver/splithorizon.goCheckSplitHorizon() (Stage 2.5 cross-check)
  10. resolver/fallback.goParallelAFallback()
  11. resolver/modes.goResolve() mode dispatcher integrating all components
  12. main.go — Update CLI flag parsing, wire new resolver pipeline

Per-Step TDD Cycle

For each item above:

  1. Red: Write the test in *_test.go — it should fail (function doesn't exist yet).
  2. Green: Implement the minimum code to make the test pass.
  3. 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