# Data Model: Smart DNS Server Resolution Modes **Feature Branch**: `002-server-resolution-modes` **Date**: 2026-03-04 --- ## Entity Overview ``` ┌─────────────┐ selects ┌──────────────────┐ │ ServerMode │─────────────────▸│ ResolverPool │ └─────────────┘ │ (bootstrap + │ │ │ local resolvers) │ │ └────────┬───────────┘ │ │ feeds │ configures ▼ │ ┌──────────────────┐ ▼ │ ParallelNSFanOut │ ┌─────────────┐ │ (Stage 1) │ │ QueryConfig │ └────────┬───────────┘ │ (timeout, │ │ produces │ verbose) │ ▼ └─────────────┘ ┌──────────────────┐ │ NSResult[] │ └────────┬───────────┘ │ selects best ▼ ┌──────────────────┐ │ AuthoritativeNS │──▸ Stage 2: Query with RD=0 └────────┬───────────┘ │ may return ▼ ┌──────────────────┐ │ CNAME redirect │──▸ Restart from Stage 1 └──────────────────┘ │ or ▼ ┌──────────────────┐ │ A record IPs │ └────────┬───────────┘ │ ▼ ┌──────────────────┐ │ SplitHorizon │──▸ Stage 2.5: Cross-check │ CrossCheck │ local resolvers └────────┬───────────┘ │ ┌────────┴───────────┐ │ [IPs match or │──▸ Final result │ no local answer]│ └────────────────────┘ ┌────────────────────┐ │ [IPs differ] │──▸ CONFLICT ERROR └────────────────────┘ (if Stage 1 returns no NS) │ ▼ ┌──────────────────┐ │ ParallelAFallback│──▸ Stage 3: First success wins │ (Stage 3) │ └──────────────────┘ ``` --- ## Entities ### ServerMode Represents the resolution strategy selected by the user via the `-server` flag. | Field | Type | Description | |-------|------|-------------| | Mode | `string` | One of: `"default"`, `"local"`, `"gateway"`, `"explicit"` | | ExplicitAddr | `string` | IP or IP:port when Mode is `"explicit"`. Empty otherwise. | **Derivation rules**: - `-server` omitted → `Mode: "default"`, `ExplicitAddr: ""` - `-server local` → `Mode: "local"`, `ExplicitAddr: ""` - `-server gateway` → `Mode: "gateway"`, `ExplicitAddr: ""` - `-server 10.0.0.53` → `Mode: "explicit"`, `ExplicitAddr: "10.0.0.53"` - `-server 10.0.0.53:5353` → `Mode: "explicit"`, `ExplicitAddr: "10.0.0.53:5353"` **Validation**: - When Mode is `"explicit"`, `ExplicitAddr` must be a valid IP or IP:port. Port (if provided) must be 1–65535. - Invalid `-server` values produce an immediate usage error before any DNS queries. --- ### QueryConfig Global configuration for DNS queries derived from CLI flags. | Field | Type | Default | Description | |-------|------|---------|-------------| | Timeout | `time.Duration` | 3s | Per-query DNS timeout (from `-timeout` flag). Applied to every individual DNS query. | | Verbose | `bool` | false | When true, emit per-stage diagnostic trace to stderr (from `-verbose` flag). | **Validation**: - `Timeout` must be a positive integer (seconds). Zero or negative values produce a usage error. --- ### NetworkInfo Discovered local network configuration from the OS. Returned by platform-specific `Discover()` implementations. | Field | Type | Description | |-------|------|-------------| | DNSServers | `[]string` | Ordered list of DNS server IPs from the default-route adapter. May be empty. | | Gateway | `string` | Default gateway IP. May be empty if no default route exists. | | Interface | `string` | Name of the adapter holding the default route. Informational (used for verbose output). | **Invariants**: - All entries in `DNSServers` are valid IPv4 addresses (validated by `net.ParseIP`). - `Gateway` is either empty or a valid IPv4 address. - Empty `DNSServers` is not an error — FR-009 handles gracefully. --- ### ResolverPool The combined set of resolvers used for parallel fan-out queries in default mode. | Field | Type | Description | |-------|------|-------------| | Resolvers | `[]string` | Ordered list of resolver IPs (local resolvers first, then bootstrap set). | **Construction rules** (by ServerMode): - `"default"`: `Resolvers = NetworkInfo.DNSServers + BootstrapResolvers` (deduplicated, order preserved) - `"local"`: `Resolvers = NetworkInfo.DNSServers` (no bootstrap) - `"gateway"`: `Resolvers = [NetworkInfo.Gateway]` (single entry) - `"explicit"`: `Resolvers = [ServerMode.ExplicitAddr]` (single entry) **Bootstrap Resolver Set** (hardcoded, FR-007): | Order | IP | Provider | |-------|----|----------| | 1 | 1.1.1.1 | Cloudflare | | 2 | 8.8.8.8 | Google | | 3 | 1.0.0.1 | Cloudflare secondary | | 4 | 8.8.4.4 | Google secondary | | 5 | 9.9.9.9 | Quad9 | | 6 | 208.67.222.222 | OpenDNS/Cisco | --- ### NSResult The result of a single NS query during Stage 1 fan-out. One per (resolver × label level) combination. | Field | Type | Description | |-------|------|-------------| | LabelLevel | `string` | The domain queried for NS records (e.g., `"example.com"`) | | Resolver | `string` | The resolver IP:port that was queried | | NSRecords | `[]string` | NS hostnames returned (e.g., `["ns1.example.com.", "ns2.example.com."]`). Nil if query failed or returned NXDOMAIN. | | CNAMETarget | `string` | Non-empty if a CNAME was returned instead of NS records. | | Err | `error` | Non-nil if query failed (timeout, connection refused, parse error). Nil if NXDOMAIN (that's a valid "no NS" response). | **Semantics**: - `NSRecords != nil && Err == nil`: Successful NS response — this label level has delegation. - `NSRecords == nil && Err == nil`: NXDOMAIN or no NS records — not an error, just no delegation at this level. - `Err != nil`: Query failure (timeout, network error). The resolver was unreachable. --- ### AuthoritativeNS The nameserver selected as the most-specific authority for the target hostname. | Field | Type | Description | |-------|------|-------------| | Zone | `string` | The label level with the most-specific NS delegation (e.g., `"sub.example.com"`) | | Nameservers | `[]string` | NS hostnames for this zone (deduplicated across all resolvers that returned them) | **Selection algorithm** (FR-015): 1. Group all successful `NSResult` entries by `LabelLevel`. 2. For each label level, merge and deduplicate `NSRecords` from all resolvers. 3. Select the label level with the longest name (most labels = most specific). 4. If multiple label levels have the same length (shouldn't happen in practice), prefer the one returned by more resolvers. --- ### PrivateTLD Static detection set for context-aware error messages (FR-024). | TLD | Source | |-----|--------| | `.local` | mDNS (RFC 6762) | | `.internal` | Common internal convention | | `.lan` | Consumer router convention | | `.home` | Consumer router convention | | `.corp` | Corporate network convention | | `.private` | Private network convention | **Usage**: When resolution fails completely and the hostname's TLD matches this set, the error message indicates the hostname appears internal and suggests `-server `. --- ### SplitHorizonResult The outcome of the Stage 2.5 cross-check comparing authoritative and local resolver answers. | Field | Type | Description | |-------|------|-------------| | AuthoritativeIPs | `[]string` | IPs returned by the authoritative NS in Stage 2 | | AuthoritativeSource | `string` | The authoritative NS that provided the answer (e.g., `"ns1.example.com."`) | | LocalIPs | `[]string` | IPs returned by the local resolver (nil if NXDOMAIN, timeout, or no local resolvers) | | LocalSource | `string` | The local resolver IP that provided the answer | | HasConflict | `bool` | True if LocalIPs is non-nil and differs from AuthoritativeIPs | **Semantics**: - `LocalIPs == nil`: No local answer (NXDOMAIN, timeout, or no local resolvers). No conflict. - `LocalIPs != nil && sets equal to AuthoritativeIPs`: Local agrees. No conflict. - `LocalIPs != nil && sets differ from AuthoritativeIPs`: **Conflict detected**. Neither IP set is written to the hosts file. **Comparison**: IP sets are compared as sorted, deduplicated string sets. Order does not matter — `["1.2.3.4", "5.6.7.8"]` matches `["5.6.7.8", "1.2.3.4"]`. --- ## State Transitions ### Resolution Flow (Default Mode) ``` START │ ├─▸ Build ResolverPool (local + bootstrap) │ ├─▸ Extract label levels from hostname │ ├─▸ Stage 1: Parallel NS Fan-out │ │ │ ├─▸ [NS records found] ──▸ Stage 2: Authoritative Query │ │ │ │ │ ├─▸ [A records] ──▸ Stage 2.5: Split-Horizon Cross-Check │ │ │ │ │ │ │ ├─▸ [IPs match / no local answer] ──▸ SUCCESS (IPs) │ │ │ └─▸ [IPs differ] ──▸ CONFLICT ERROR │ │ ├─▸ [CNAME] ──▸ Restart from Stage 1 (depth+1) │ │ └─▸ [All NS unreachable] ──▸ ERROR │ │ │ └─▸ [No NS at any level] ──▸ Stage 3: Parallel A Fallback │ │ │ ├─▸ [Any A success] ──▸ SUCCESS (IPs) │ └─▸ [All fail/NXDOMAIN] ──▸ ERROR │ └─▸ ERROR includes private TLD hint if applicable ``` ### Resolution Flow (Local Mode) ``` START │ ├─▸ Discover local resolvers │ │ │ └─▸ [No local resolvers] ──▸ ERROR (no fallback to public) │ ├─▸ Query resolvers in priority order (A query) │ │ │ ├─▸ [Success] ──▸ SUCCESS (IPs) │ ├─▸ [Timeout] ──▸ Try next resolver │ └─▸ [All fail] ──▸ ERROR │ └─▸ No fallback to public resolvers (FR-026) ``` ### Resolution Flow (Gateway Mode) ``` START │ ├─▸ Discover default gateway │ │ │ └─▸ [No gateway] ──▸ ERROR │ ├─▸ Query gateway (A query) │ │ │ ├─▸ [Success] ──▸ SUCCESS (IPs) │ └─▸ [Fail] ──▸ ERROR (no fallback, FR-027) │ └─▸ No fallback ``` ### Resolution Flow (Explicit IP Mode) ``` START │ ├─▸ Query specified server (existing behavior, unchanged) │ │ │ ├─▸ [Success] ──▸ SUCCESS (IPs) │ └─▸ [Fail] ──▸ ERROR │ └─▸ Identical to current dns-helper behavior ```