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

301 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```