301 lines
13 KiB
Markdown
301 lines
13 KiB
Markdown
# 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 <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
|
||
```
|