Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user