# Package Contracts: dns-helper (002 — Server Resolution Modes)

**Feature Branch**: `002-server-resolution-modes`  
**Date**: 2026-03-04  
**Extends**: [001 Package Contracts](../../001-safety-reliability-refactor/contracts/packages.md)

## Changes from 001

1. **`resolver` package**: New types and functions for multi-mode resolution, parallel fan-out, authoritative queries.
2. **`platform` package**: New `NetworkDiscoverer` interface and cross-platform implementations.
3. **`hostfile` package**: Unchanged.
4. **`lockfile` package**: Unchanged.

---

## Package: `resolver` (extended)

### Existing Interface (unchanged)

```go
// Resolver performs DNS lookups against a specific server.
type Resolver interface {
    LookupIP(hostname, server string) ([]string, error)
}
```

The existing `DNSResolver` and `LookupIP` method are unchanged. They continue to serve the explicit IP mode (`-server <ip>`).

### New Types

```go
// ServerMode represents the resolution strategy.
type ServerMode struct {
    Mode         string // "default", "local", "gateway", "explicit"
    ExplicitAddr string // IP or IP:port when Mode is "explicit"
}

// QueryConfig holds per-invocation DNS query settings.
type QueryConfig struct {
    Timeout time.Duration // Per-query timeout (default 3s)
    Verbose bool          // Emit diagnostic trace to stderr
}

// NSResult holds the outcome of a single NS query during fan-out.
type NSResult struct {
    LabelLevel  string   // Domain queried (e.g., "example.com")
    Resolver    string   // Resolver IP:port that was queried
    NSRecords   []string // NS hostnames returned; nil if no NS found
    CNAMETarget string   // Non-empty if CNAME returned instead of NS
    Err         error    // Non-nil on query failure (timeout, network error)
}

// AuthoritativeNS holds the selected most-specific nameserver delegation.
type AuthoritativeNS struct {
    Zone        string   // Label level (e.g., "sub.example.com")
    Nameservers []string // Deduplicated NS hostnames for this zone
}

// SplitHorizonResult holds the outcome of the Stage 2.5 cross-check.
type SplitHorizonResult struct {
    AuthoritativeIPs    []string // IPs from Stage 2 authoritative query
    AuthoritativeSource string   // NS hostname that provided the authoritative answer
    LocalIPs            []string // IPs from local resolver (nil if NXDOMAIN/timeout/absent)
    LocalSource         string   // Local resolver IP that responded
    HasConflict         bool     // True if LocalIPs non-nil and differs from AuthoritativeIPs
}
```

### New Functions

```go
// ParseServerFlag parses the -server flag value into a ServerMode.
// Returns a validation error for invalid IP:port combinations.
func ParseServerFlag(value string) (ServerMode, error)
```

**Contract**:
- Empty string → `ServerMode{Mode: "default"}`.
- `"local"` (case-insensitive) → `ServerMode{Mode: "local"}`.
- `"gateway"` (case-insensitive) → `ServerMode{Mode: "gateway"}`.
- Valid IP → `ServerMode{Mode: "explicit", ExplicitAddr: "<ip>"}`.
- Valid IP:port → `ServerMode{Mode: "explicit", ExplicitAddr: "<ip>:<port>"}`. Port must be 1–65535.
- Invalid input → error (e.g., `"10.0.0.53:0"` → `"port must be between 1 and 65535"`).

---

```go
// Resolve performs DNS resolution using the specified mode and configuration.
// Returns a list of IPv4 addresses for the hostname.
// discoverer provides local network info (DNS servers, gateway).
func Resolve(hostname string, mode ServerMode, config QueryConfig, discoverer platform.NetworkDiscoverer) ([]string, error)
```

**Contract**:
- **Default mode**: Calls `discoverer.Discover()` to get local resolvers, builds pool with bootstrap set, runs Stage 1 → 2 → 2.5 → 3 pipeline, returns IPs. Stage 2.5 cross-checks the authoritative answer against local resolvers; if they disagree, returns a conflict error with both IP sets.
- **Local mode**: Calls `discoverer.Discover()` for local resolvers only, queries in priority order, returns IPs. Error if no local resolvers found or all fail. No fallback to public. No split-horizon check (user explicitly chose local).
- **Gateway mode**: Calls `discoverer.Discover()` for gateway IP, sends A query to gateway, returns IPs. Error if no gateway or gateway doesn't respond. No split-horizon check.
- **Explicit mode**: Calls existing `LookupIP(hostname, mode.ExplicitAddr)`, returns IPs. Behavior unchanged from 001. No split-horizon check.
- All modes: CNAME chain depth limited to 10. Verbose output to stderr if `config.Verbose` is true.
- All modes: Per-query timeout from `config.Timeout`.
- On failure with private TLD hostname: error message includes hint (FR-025).

---

```go
// ExtractLabelLevels returns all queryable domain levels from a hostname,
// from most-specific to least-specific, excluding single-label TLDs.
// Example: "www.example.com" → ["www.example.com", "example.com"]
func ExtractLabelLevels(hostname string) []string
```

**Contract**:
- Input is normalized: trailing dot stripped, lowercased.
- Returns nil for single-label hostnames (e.g., `"localhost"`).
- Returns one entry for two-label hostnames (e.g., `"example.com"` → `["example.com"]`).
- No public suffix list — all label levels with ≥2 labels are included.

---

```go
// BuildResolverPool constructs the resolver pool for the given mode.
// For default mode: local resolvers + bootstrap set, deduplicated.
// For local mode: local resolvers only.
// For gateway mode: [gateway IP].
// For explicit mode: [explicit address].
func BuildResolverPool(mode ServerMode, info platform.NetworkInfo) []string
```

**Contract**:
- Default: concatenates `info.DNSServers` + `BootstrapResolvers`, deduplicates preserving order.
- Local: returns `info.DNSServers`. May be empty (caller handles).
- Gateway: returns `[]string{info.Gateway}`. May be `[""]` if gateway empty (caller handles).
- Explicit: returns `[]string{mode.ExplicitAddr}`.

---

```go
// ParallelNSFanOut queries NS records for all label levels across all resolvers
// simultaneously. Returns one NSResult per (resolver × label level) combination.
func ParallelNSFanOut(ctx context.Context, resolvers []string, labelLevels []string, timeout time.Duration) []NSResult
```

**Contract**:
- Launches `len(resolvers) × len(labelLevels)` goroutines.
- Each goroutine sends one NS query over UDP and writes one `NSResult` to a buffered channel.
- Collector reads exactly `N×M` results. No goroutine leak.
- Per-query timeout derived from `ctx` + `timeout` parameter.
- NXDOMAIN responses produce `NSResult{Err: nil, NSRecords: nil}` (not an error).
- Connection failures produce `NSResult{Err: <error>, NSRecords: nil}`.

---

```go
// SelectAuthoritativeNS picks the most-specific NS delegation from fan-out results.
// Returns nil if no NS records were found at any level.
func SelectAuthoritativeNS(results []NSResult) *AuthoritativeNS
```

**Contract**:
- Groups results by `LabelLevel`.
- Merges and deduplicates `NSRecords` from all resolvers for each level.
- Selects the level with the most labels (most specific).
- Returns nil if no level has any NS records.

---

```go
// QueryAuthoritative sends an A query to an authoritative nameserver with
// recursion disabled. Tries each NS in order if one is unreachable.
// Returns IPs, or a CNAME target if the answer is a CNAME.
func QueryAuthoritative(ctx context.Context, ns *AuthoritativeNS, hostname string, resolvers []string, timeout time.Duration) (ips []string, cnameTarget string, err error)
```

**Contract**:
- Resolves the first NS hostname to an IP using the resolver pool.
- Sends A query with `RecursionDesired: false`.
- If A records returned: returns IPs.
- If CNAME returned: returns empty IPs + CNAME target.
- If NS unreachable: tries next NS in `ns.Nameservers`. If all fail: returns error.
- Checks `Header.Authoritative` flag for verbose logging (not a hard requirement).

---

```go
// ParallelAFallback sends A queries for hostname to all resolvers simultaneously.
// Returns the first successful A record result. Ignores NXDOMAIN and failures.
func ParallelAFallback(ctx context.Context, resolvers []string, hostname string, timeout time.Duration) ([]string, error)
```

**Contract**:
- Launches `len(resolvers)` goroutines, each sending an A query.
- Returns IPs from the first successful response.
- NXDOMAIN and connection failures are ignored.
- If all resolvers fail or return NXDOMAIN: returns error.

---

```go
// CheckSplitHorizon queries only the local resolvers for the same hostname
// and compares the result against the authoritative IPs from Stage 2.
// Returns a SplitHorizonResult indicating whether a conflict was detected.
// Only called in default mode when local resolvers are available.
func CheckSplitHorizon(ctx context.Context, localResolvers []string, hostname string, authoritativeIPs []string, authoritativeSource string, timeout time.Duration) SplitHorizonResult
```

**Contract**:
- Queries each local resolver in order with a standard A query for the hostname.
- Uses the first successful response from any local resolver.
- Compares the local IP set against `authoritativeIPs` (sorted, deduplicated set comparison).
- If local returns different IPs: `HasConflict: true`.
- If local returns same IPs, NXDOMAIN, or all fail: `HasConflict: false`.
- Should be fired in parallel with Stage 2 (or immediately after Stage 1) to avoid adding latency.
- Per-query timeout from the `timeout` parameter.

---

### Bootstrap Resolver Set

```go
// BootstrapResolvers is the hardcoded set of reliable public DNS resolvers (FR-007).
var BootstrapResolvers = []string{
    "1.1.1.1",       // Cloudflare
    "8.8.8.8",       // Google
    "1.0.0.1",       // Cloudflare secondary
    "8.8.4.4",       // Google secondary
    "9.9.9.9",       // Quad9
    "208.67.222.222", // OpenDNS/Cisco
}
```

### Private TLD Set

```go
// PrivateTLDs is the set of well-known private TLDs for error message hints (FR-024).
var PrivateTLDs = map[string]bool{
    "local":    true,
    "internal": true,
    "lan":      true,
    "home":     true,
    "corp":     true,
    "private":  true,
}
```

---

## Package: `platform` (extended)

### Existing Interface (unchanged)

```go
func GetHostsFilePath() (string, error)
```

### New Types

```go
// NetworkInfo holds discovered local network configuration.
type NetworkInfo struct {
    DNSServers []string // Ordered DNS server IPs from default-route adapter
    Gateway    string   // Default gateway IP (may be empty)
    Interface  string   // Adapter name holding the default route (informational)
}
```

### New Interface

```go
// NetworkDiscoverer discovers local network configuration.
type NetworkDiscoverer interface {
    Discover() (NetworkInfo, error)
}
```

### Platform Implementations

Each platform file (`platform_windows.go`, `platform_linux.go`, `platform_darwin.go`) adds a `Discover()` method on a platform-specific struct implementing `NetworkDiscoverer`.

| Platform | Commands Used | Key Parsing |
|----------|--------------|-------------|
| Windows | `netsh interface ipv4 show route`, `netsh interface ipv4 show interfaces`, `netsh interface ipv4 show dnsservers name="<name>"` | IPv4 regex on route/DNS output; index→name mapping |
| Linux | `ip route show default`, `/etc/resolv.conf`, (conditional) `resolvectl status <iface>` | `via`/`dev` keywords; `nameserver` lines; stub-resolver detection |
| macOS | `route -n get default`, `scutil --dns` | `gateway:`/`interface:` lines; resolver block parsing with `if_index` matching |

### Behavior Contract — Discover()

- Returns `NetworkInfo` with whatever was discovered. All fields may be empty.
- Empty `DNSServers` is **not** an error — it means no local resolvers were found. The caller (resolver pool construction) handles this per FR-009.
- Empty `Gateway` is **not** an error — it means no default route exists.
- Returns an `error` only for unexpected failures (e.g., OS command execution error). Even then, `NetworkInfo` may have partial results.
- **Must NOT** call `os.Exit()` or any process-terminating function.
- All DNS server IPs and gateway IPs are validated with `net.ParseIP()` before inclusion.

### Testability

A `FakeNetworkDiscoverer` struct is provided for unit tests:

```go
// FakeNetworkDiscoverer returns predetermined NetworkInfo for testing.
type FakeNetworkDiscoverer struct {
    Info NetworkInfo
    Err  error
}

func (f *FakeNetworkDiscoverer) Discover() (NetworkInfo, error) {
    return f.Info, f.Err
}
```

This enables deterministic testing of resolver pool construction and mode dispatch without running real OS commands.

---

## Package: `hostfile` (unchanged)

No changes from 001. The hosts file read/parse/write pipeline is unaffected by this feature.

---

## Package: `lockfile` (unchanged)

No changes from 001.
