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

301 lines
13 KiB
Markdown
Raw Normal View 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 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 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
```