# 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
```
