13 KiB
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:
-serveromitted →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",ExplicitAddrmust be a valid IP or IP:port. Port (if provided) must be 1–65535. - Invalid
-servervalues 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:
Timeoutmust 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
DNSServersare valid IPv4 addresses (validated bynet.ParseIP). Gatewayis either empty or a valid IPv4 address.- Empty
DNSServersis 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 | |
| 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):
- Group all successful
NSResultentries byLabelLevel. - For each label level, merge and deduplicate
NSRecordsfrom all resolvers. - Select the label level with the longest name (most labels = most specific).
- 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 <ip>.
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