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

13 KiB
Raw Blame History

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 localMode: "local", ExplicitAddr: ""
  • -server gatewayMode: "gateway", ExplicitAddr: ""
  • -server 10.0.0.53Mode: "explicit", ExplicitAddr: "10.0.0.53"
  • -server 10.0.0.53:5353Mode: "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 165535.
  • 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 <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