Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
# CLI Contract: dns-helper (002 — Server Resolution Modes)
|
||||
|
||||
**Feature Branch**: `002-server-resolution-modes`
|
||||
**Date**: 2026-03-04
|
||||
**Extends**: [001 CLI Contract](../../001-safety-reliability-refactor/contracts/cli.md)
|
||||
|
||||
## Changes from 001
|
||||
|
||||
1. The `-server` flag on `add` becomes **optional** (was required).
|
||||
2. The `-server` flag gains keyword values: `local`, `gateway`.
|
||||
3. New flags: `-timeout`, `-verbose` on every command that performs DNS resolution.
|
||||
4. Error messages gain context-aware hints for private TLDs.
|
||||
5. Split-horizon conflict detection: in default mode, cross-checks authoritative answer against local resolvers.
|
||||
|
||||
---
|
||||
|
||||
## Subcommand: `add` (aliases: `a`)
|
||||
|
||||
### Synopsis
|
||||
|
||||
```
|
||||
dns-helper add -host <hostnames> [-server <mode>] [-timeout <seconds>] [-verbose]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Required | Type | Default | Description |
|
||||
|------|----------|------|---------|-------------|
|
||||
| `-host` | Yes | string | — | Comma-separated list of hostnames to resolve and add |
|
||||
| `-server` | No | string | (smart default) | Resolution mode: omit for smart default, `local`, `gateway`, or IP/IP:port |
|
||||
| `-timeout` | No | int | 3 | Per-query DNS timeout in seconds |
|
||||
| `-verbose` | No | bool | false | Emit per-stage resolution trace to stderr |
|
||||
|
||||
### `-server` Flag Values
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| *(omitted)* | Smart default: parallel NS fan-out across local + public resolvers, authoritative query, parallel A fallback. See FR-002. |
|
||||
| `local` | Query only locally configured DNS resolvers in priority order. No public fallback. See FR-003. |
|
||||
| `gateway` | Query the default gateway IP as DNS server. No fallback. See FR-004. |
|
||||
| `<ip>` | Query the specified IP on port 53. Existing behavior, unchanged. See FR-005. |
|
||||
| `<ip>:<port>` | Query the specified IP on the given port. Port must be 1–65535. See FR-005. |
|
||||
|
||||
### `-server` Validation
|
||||
|
||||
- `<ip>:<port>` where port is 0 or >65535: **Immediate usage error** (no DNS queries issued).
|
||||
- `<ip>` where IP is not a valid IPv4 address and not a recognized keyword: **Immediate usage error**.
|
||||
- Recognized keywords are case-insensitive: `local`, `LOCAL`, `Local` all accepted.
|
||||
|
||||
### `-timeout` Validation
|
||||
|
||||
- Must be a positive integer. Zero or negative: **Immediate usage error**.
|
||||
- Applied to every individual DNS query (NS fan-out, authoritative, fallback, local, gateway).
|
||||
|
||||
### Behavior — Smart Default (no `-server`)
|
||||
|
||||
1. Discover local DNS resolvers from OS network configuration.
|
||||
2. Build resolver pool: local resolvers + hardcoded bootstrap set (deduplicated).
|
||||
3. Extract all label levels from hostname (e.g., `www.example.com` → `["www.example.com", "example.com"]`).
|
||||
4. **Stage 1**: Parallel NS fan-out — query NS for every label level across every resolver in the pool simultaneously.
|
||||
5. Select the most-specific NS delegation (longest label level with NS records).
|
||||
6. **Stage 2**: Resolve one of the NS hostnames to an IP, then query that NS directly for the A record with recursion disabled.
|
||||
7. If Stage 2 returns a CNAME, restart from Stage 1 for the CNAME target domain (max 10 hops).
|
||||
8. **Stage 2.5**: If Stage 2 returns A records and local resolvers are available, cross-check by querying local resolvers for the same hostname. If local resolvers return a different IP → conflict error (neither IP written). If same IP, NXDOMAIN, or failure → proceed normally.
|
||||
9. If Stage 1 returns no NS records at any level: **Stage 3** — parallel A query across all resolvers, take first success.
|
||||
10. If all stages fail, report error with private TLD hint if applicable.
|
||||
|
||||
### Behavior — Local Mode (`-server local`)
|
||||
|
||||
1. Discover local DNS resolvers from OS network configuration.
|
||||
2. Query each resolver in priority order with standard A query.
|
||||
3. Return first successful result.
|
||||
4. If all fail: error. **No fallback to public resolvers** (FR-026).
|
||||
|
||||
### Behavior — Gateway Mode (`-server gateway`)
|
||||
|
||||
1. Discover default gateway IP from OS network configuration.
|
||||
2. Send DNS query to gateway.
|
||||
3. If gateway doesn't respond: error. **No fallback** (FR-027).
|
||||
|
||||
### Output — Verbose Mode (stderr only)
|
||||
|
||||
When `-verbose` is provided, emit to **stderr** (FR-030/FR-031):
|
||||
|
||||
```
|
||||
[dns] Resolver pool: [10.26.1.1, 1.1.1.1, 8.8.8.8, 1.0.0.1, 8.8.4.4, 9.9.9.9, 208.67.222.222]
|
||||
[dns] Stage 1: NS fan-out for www.example.com (2 levels × 7 resolvers = 14 queries)
|
||||
[dns] example.com NS: ns1.example.com., ns2.example.com. (via 8.8.8.8, 1.1.1.1)
|
||||
[dns] www.example.com NS: (none)
|
||||
[dns] Selected authority: example.com → ns1.example.com.
|
||||
[dns] Stage 2: Querying ns1.example.com. (93.184.216.34) for www.example.com A (RD=0)
|
||||
[dns] Result: 93.184.216.34
|
||||
```
|
||||
|
||||
Verbose output for fallback:
|
||||
|
||||
```
|
||||
[dns] Stage 1: No NS records found at any level
|
||||
[dns] Stage 3: Parallel A fallback for myservice.client.local (7 resolvers)
|
||||
[dns] 10.26.1.1 → 10.0.5.100
|
||||
[dns] 1.1.1.1 → NXDOMAIN
|
||||
[dns] 8.8.8.8 → NXDOMAIN
|
||||
[dns] Result: 10.0.5.100 (via 10.26.1.1)
|
||||
```
|
||||
|
||||
Verbose output for split-horizon cross-check:
|
||||
|
||||
```
|
||||
[dns] Stage 2.5: Split-horizon cross-check against local resolvers [10.26.1.1]
|
||||
[dns] 10.26.1.1 → 10.0.5.100 (differs from authoritative 203.0.113.50)
|
||||
[dns] CONFLICT: authoritative and local resolvers disagree
|
||||
```
|
||||
|
||||
Verbose output when cross-check passes:
|
||||
|
||||
```
|
||||
[dns] Stage 2.5: Split-horizon cross-check against local resolvers [10.26.1.1]
|
||||
[dns] 10.26.1.1 → 93.184.216.34 (matches authoritative)
|
||||
[dns] No conflict detected
|
||||
```
|
||||
|
||||
### Output — Error Messages
|
||||
|
||||
**Private TLD with total failure** (FR-025):
|
||||
|
||||
```
|
||||
Error: failed to resolve myservice.client.local: no DNS server could resolve this hostname
|
||||
Hint: the hostname uses a private TLD (.local). Try specifying an internal DNS server:
|
||||
dns-helper add -host myservice.client.local -server <internal-dns-ip>
|
||||
```
|
||||
|
||||
**Local mode, all resolvers unreachable** (FR-026):
|
||||
|
||||
```
|
||||
Error: failed to resolve myhost.example.com using local resolvers: all local DNS servers are unreachable
|
||||
Local resolvers tried: 10.26.1.1, 10.26.1.2
|
||||
```
|
||||
|
||||
**Gateway mode, no response** (FR-027):
|
||||
|
||||
```
|
||||
Error: failed to resolve myhost.example.com using gateway: gateway 192.168.1.1 did not respond to DNS query
|
||||
```
|
||||
|
||||
**CNAME loop** (FR-019):
|
||||
|
||||
```
|
||||
Error: CNAME chain depth exceeded for www.example.com (max 10 hops): probable CNAME loop or misconfigured zone
|
||||
```
|
||||
|
||||
**Invalid port**:
|
||||
|
||||
```
|
||||
Error: invalid -server value "10.0.0.53:0": port must be between 1 and 65535
|
||||
```
|
||||
|
||||
**Split-horizon conflict** (FR-034/FR-035):
|
||||
|
||||
```
|
||||
Error: conflicting DNS answers for app.acme.com
|
||||
Authoritative (ns1.acme.com): 203.0.113.50
|
||||
Local resolver (10.26.1.1): 10.0.5.100
|
||||
The hostname resolves to different IPs depending on the DNS source.
|
||||
Use -server local to trust your internal DNS, or -server <ip> to choose explicitly.
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
|
||||
Unchanged from 001:
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | All hostnames resolved and written successfully |
|
||||
| 1 | Any error: validation failure, DNS failure, file write failure, partial resolution |
|
||||
|
||||
---
|
||||
|
||||
## Subcommand: `delete`
|
||||
|
||||
No changes from 001. The `delete` subcommand does not perform DNS resolution and is unaffected by this feature.
|
||||
|
||||
---
|
||||
|
||||
## Usage Help
|
||||
|
||||
Updated to reflect new flag options:
|
||||
|
||||
```
|
||||
This utility will update the local hosts file with DNS entries obtained by the specified DNS server.
|
||||
Usage: dns-helper [add|delete] -host hostname [-server dns.example.com] [-timeout seconds] [-verbose]
|
||||
Example:
|
||||
dns-helper add -host xyz.acme.com -server dns.example.com
|
||||
This will use dns.example.com to find and add all IP addresses for xyz.acme.com to the local hosts file.
|
||||
dns-helper add -host xyz.acme.com
|
||||
This will use smart resolution to find the authoritative IP for xyz.acme.com.
|
||||
dns-helper add -host internal.corp -server local
|
||||
This will use only locally configured DNS servers to resolve internal.corp.
|
||||
dns-helper add -host www.example.com -server gateway
|
||||
This will use the default gateway as the DNS server.
|
||||
dns-helper delete -host hostname.example.com
|
||||
This will delete all entries for hostname.example.com from the local hosts file.
|
||||
dns-helper delete all
|
||||
This will delete all entries from the local hosts file that were added by this utility.
|
||||
Note: Adding a hostname will first remove all entries in the hosts file that match the same hostname.
|
||||
This utility will only remove entries from the hosts file that it added.
|
||||
```
|
||||
@@ -0,0 +1,322 @@
|
||||
# Package Contracts: dns-helper (002 — Server Resolution Modes)
|
||||
|
||||
**Feature Branch**: `002-server-resolution-modes`
|
||||
**Date**: 2026-03-04
|
||||
**Extends**: [001 Package Contracts](../../001-safety-reliability-refactor/contracts/packages.md)
|
||||
|
||||
## Changes from 001
|
||||
|
||||
1. **`resolver` package**: New types and functions for multi-mode resolution, parallel fan-out, authoritative queries.
|
||||
2. **`platform` package**: New `NetworkDiscoverer` interface and cross-platform implementations.
|
||||
3. **`hostfile` package**: Unchanged.
|
||||
4. **`lockfile` package**: Unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Package: `resolver` (extended)
|
||||
|
||||
### Existing Interface (unchanged)
|
||||
|
||||
```go
|
||||
// Resolver performs DNS lookups against a specific server.
|
||||
type Resolver interface {
|
||||
LookupIP(hostname, server string) ([]string, error)
|
||||
}
|
||||
```
|
||||
|
||||
The existing `DNSResolver` and `LookupIP` method are unchanged. They continue to serve the explicit IP mode (`-server <ip>`).
|
||||
|
||||
### New Types
|
||||
|
||||
```go
|
||||
// ServerMode represents the resolution strategy.
|
||||
type ServerMode struct {
|
||||
Mode string // "default", "local", "gateway", "explicit"
|
||||
ExplicitAddr string // IP or IP:port when Mode is "explicit"
|
||||
}
|
||||
|
||||
// QueryConfig holds per-invocation DNS query settings.
|
||||
type QueryConfig struct {
|
||||
Timeout time.Duration // Per-query timeout (default 3s)
|
||||
Verbose bool // Emit diagnostic trace to stderr
|
||||
}
|
||||
|
||||
// NSResult holds the outcome of a single NS query during fan-out.
|
||||
type NSResult struct {
|
||||
LabelLevel string // Domain queried (e.g., "example.com")
|
||||
Resolver string // Resolver IP:port that was queried
|
||||
NSRecords []string // NS hostnames returned; nil if no NS found
|
||||
CNAMETarget string // Non-empty if CNAME returned instead of NS
|
||||
Err error // Non-nil on query failure (timeout, network error)
|
||||
}
|
||||
|
||||
// AuthoritativeNS holds the selected most-specific nameserver delegation.
|
||||
type AuthoritativeNS struct {
|
||||
Zone string // Label level (e.g., "sub.example.com")
|
||||
Nameservers []string // Deduplicated NS hostnames for this zone
|
||||
}
|
||||
|
||||
// SplitHorizonResult holds the outcome of the Stage 2.5 cross-check.
|
||||
type SplitHorizonResult struct {
|
||||
AuthoritativeIPs []string // IPs from Stage 2 authoritative query
|
||||
AuthoritativeSource string // NS hostname that provided the authoritative answer
|
||||
LocalIPs []string // IPs from local resolver (nil if NXDOMAIN/timeout/absent)
|
||||
LocalSource string // Local resolver IP that responded
|
||||
HasConflict bool // True if LocalIPs non-nil and differs from AuthoritativeIPs
|
||||
}
|
||||
```
|
||||
|
||||
### New Functions
|
||||
|
||||
```go
|
||||
// ParseServerFlag parses the -server flag value into a ServerMode.
|
||||
// Returns a validation error for invalid IP:port combinations.
|
||||
func ParseServerFlag(value string) (ServerMode, error)
|
||||
```
|
||||
|
||||
**Contract**:
|
||||
- Empty string → `ServerMode{Mode: "default"}`.
|
||||
- `"local"` (case-insensitive) → `ServerMode{Mode: "local"}`.
|
||||
- `"gateway"` (case-insensitive) → `ServerMode{Mode: "gateway"}`.
|
||||
- Valid IP → `ServerMode{Mode: "explicit", ExplicitAddr: "<ip>"}`.
|
||||
- Valid IP:port → `ServerMode{Mode: "explicit", ExplicitAddr: "<ip>:<port>"}`. Port must be 1–65535.
|
||||
- Invalid input → error (e.g., `"10.0.0.53:0"` → `"port must be between 1 and 65535"`).
|
||||
|
||||
---
|
||||
|
||||
```go
|
||||
// Resolve performs DNS resolution using the specified mode and configuration.
|
||||
// Returns a list of IPv4 addresses for the hostname.
|
||||
// discoverer provides local network info (DNS servers, gateway).
|
||||
func Resolve(hostname string, mode ServerMode, config QueryConfig, discoverer platform.NetworkDiscoverer) ([]string, error)
|
||||
```
|
||||
|
||||
**Contract**:
|
||||
- **Default mode**: Calls `discoverer.Discover()` to get local resolvers, builds pool with bootstrap set, runs Stage 1 → 2 → 2.5 → 3 pipeline, returns IPs. Stage 2.5 cross-checks the authoritative answer against local resolvers; if they disagree, returns a conflict error with both IP sets.
|
||||
- **Local mode**: Calls `discoverer.Discover()` for local resolvers only, queries in priority order, returns IPs. Error if no local resolvers found or all fail. No fallback to public. No split-horizon check (user explicitly chose local).
|
||||
- **Gateway mode**: Calls `discoverer.Discover()` for gateway IP, sends A query to gateway, returns IPs. Error if no gateway or gateway doesn't respond. No split-horizon check.
|
||||
- **Explicit mode**: Calls existing `LookupIP(hostname, mode.ExplicitAddr)`, returns IPs. Behavior unchanged from 001. No split-horizon check.
|
||||
- All modes: CNAME chain depth limited to 10. Verbose output to stderr if `config.Verbose` is true.
|
||||
- All modes: Per-query timeout from `config.Timeout`.
|
||||
- On failure with private TLD hostname: error message includes hint (FR-025).
|
||||
|
||||
---
|
||||
|
||||
```go
|
||||
// ExtractLabelLevels returns all queryable domain levels from a hostname,
|
||||
// from most-specific to least-specific, excluding single-label TLDs.
|
||||
// Example: "www.example.com" → ["www.example.com", "example.com"]
|
||||
func ExtractLabelLevels(hostname string) []string
|
||||
```
|
||||
|
||||
**Contract**:
|
||||
- Input is normalized: trailing dot stripped, lowercased.
|
||||
- Returns nil for single-label hostnames (e.g., `"localhost"`).
|
||||
- Returns one entry for two-label hostnames (e.g., `"example.com"` → `["example.com"]`).
|
||||
- No public suffix list — all label levels with ≥2 labels are included.
|
||||
|
||||
---
|
||||
|
||||
```go
|
||||
// BuildResolverPool constructs the resolver pool for the given mode.
|
||||
// For default mode: local resolvers + bootstrap set, deduplicated.
|
||||
// For local mode: local resolvers only.
|
||||
// For gateway mode: [gateway IP].
|
||||
// For explicit mode: [explicit address].
|
||||
func BuildResolverPool(mode ServerMode, info platform.NetworkInfo) []string
|
||||
```
|
||||
|
||||
**Contract**:
|
||||
- Default: concatenates `info.DNSServers` + `BootstrapResolvers`, deduplicates preserving order.
|
||||
- Local: returns `info.DNSServers`. May be empty (caller handles).
|
||||
- Gateway: returns `[]string{info.Gateway}`. May be `[""]` if gateway empty (caller handles).
|
||||
- Explicit: returns `[]string{mode.ExplicitAddr}`.
|
||||
|
||||
---
|
||||
|
||||
```go
|
||||
// ParallelNSFanOut queries NS records for all label levels across all resolvers
|
||||
// simultaneously. Returns one NSResult per (resolver × label level) combination.
|
||||
func ParallelNSFanOut(ctx context.Context, resolvers []string, labelLevels []string, timeout time.Duration) []NSResult
|
||||
```
|
||||
|
||||
**Contract**:
|
||||
- Launches `len(resolvers) × len(labelLevels)` goroutines.
|
||||
- Each goroutine sends one NS query over UDP and writes one `NSResult` to a buffered channel.
|
||||
- Collector reads exactly `N×M` results. No goroutine leak.
|
||||
- Per-query timeout derived from `ctx` + `timeout` parameter.
|
||||
- NXDOMAIN responses produce `NSResult{Err: nil, NSRecords: nil}` (not an error).
|
||||
- Connection failures produce `NSResult{Err: <error>, NSRecords: nil}`.
|
||||
|
||||
---
|
||||
|
||||
```go
|
||||
// SelectAuthoritativeNS picks the most-specific NS delegation from fan-out results.
|
||||
// Returns nil if no NS records were found at any level.
|
||||
func SelectAuthoritativeNS(results []NSResult) *AuthoritativeNS
|
||||
```
|
||||
|
||||
**Contract**:
|
||||
- Groups results by `LabelLevel`.
|
||||
- Merges and deduplicates `NSRecords` from all resolvers for each level.
|
||||
- Selects the level with the most labels (most specific).
|
||||
- Returns nil if no level has any NS records.
|
||||
|
||||
---
|
||||
|
||||
```go
|
||||
// QueryAuthoritative sends an A query to an authoritative nameserver with
|
||||
// recursion disabled. Tries each NS in order if one is unreachable.
|
||||
// Returns IPs, or a CNAME target if the answer is a CNAME.
|
||||
func QueryAuthoritative(ctx context.Context, ns *AuthoritativeNS, hostname string, resolvers []string, timeout time.Duration) (ips []string, cnameTarget string, err error)
|
||||
```
|
||||
|
||||
**Contract**:
|
||||
- Resolves the first NS hostname to an IP using the resolver pool.
|
||||
- Sends A query with `RecursionDesired: false`.
|
||||
- If A records returned: returns IPs.
|
||||
- If CNAME returned: returns empty IPs + CNAME target.
|
||||
- If NS unreachable: tries next NS in `ns.Nameservers`. If all fail: returns error.
|
||||
- Checks `Header.Authoritative` flag for verbose logging (not a hard requirement).
|
||||
|
||||
---
|
||||
|
||||
```go
|
||||
// ParallelAFallback sends A queries for hostname to all resolvers simultaneously.
|
||||
// Returns the first successful A record result. Ignores NXDOMAIN and failures.
|
||||
func ParallelAFallback(ctx context.Context, resolvers []string, hostname string, timeout time.Duration) ([]string, error)
|
||||
```
|
||||
|
||||
**Contract**:
|
||||
- Launches `len(resolvers)` goroutines, each sending an A query.
|
||||
- Returns IPs from the first successful response.
|
||||
- NXDOMAIN and connection failures are ignored.
|
||||
- If all resolvers fail or return NXDOMAIN: returns error.
|
||||
|
||||
---
|
||||
|
||||
```go
|
||||
// CheckSplitHorizon queries only the local resolvers for the same hostname
|
||||
// and compares the result against the authoritative IPs from Stage 2.
|
||||
// Returns a SplitHorizonResult indicating whether a conflict was detected.
|
||||
// Only called in default mode when local resolvers are available.
|
||||
func CheckSplitHorizon(ctx context.Context, localResolvers []string, hostname string, authoritativeIPs []string, authoritativeSource string, timeout time.Duration) SplitHorizonResult
|
||||
```
|
||||
|
||||
**Contract**:
|
||||
- Queries each local resolver in order with a standard A query for the hostname.
|
||||
- Uses the first successful response from any local resolver.
|
||||
- Compares the local IP set against `authoritativeIPs` (sorted, deduplicated set comparison).
|
||||
- If local returns different IPs: `HasConflict: true`.
|
||||
- If local returns same IPs, NXDOMAIN, or all fail: `HasConflict: false`.
|
||||
- Should be fired in parallel with Stage 2 (or immediately after Stage 1) to avoid adding latency.
|
||||
- Per-query timeout from the `timeout` parameter.
|
||||
|
||||
---
|
||||
|
||||
### Bootstrap Resolver Set
|
||||
|
||||
```go
|
||||
// BootstrapResolvers is the hardcoded set of reliable public DNS resolvers (FR-007).
|
||||
var BootstrapResolvers = []string{
|
||||
"1.1.1.1", // Cloudflare
|
||||
"8.8.8.8", // Google
|
||||
"1.0.0.1", // Cloudflare secondary
|
||||
"8.8.4.4", // Google secondary
|
||||
"9.9.9.9", // Quad9
|
||||
"208.67.222.222", // OpenDNS/Cisco
|
||||
}
|
||||
```
|
||||
|
||||
### Private TLD Set
|
||||
|
||||
```go
|
||||
// PrivateTLDs is the set of well-known private TLDs for error message hints (FR-024).
|
||||
var PrivateTLDs = map[string]bool{
|
||||
"local": true,
|
||||
"internal": true,
|
||||
"lan": true,
|
||||
"home": true,
|
||||
"corp": true,
|
||||
"private": true,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Package: `platform` (extended)
|
||||
|
||||
### Existing Interface (unchanged)
|
||||
|
||||
```go
|
||||
func GetHostsFilePath() (string, error)
|
||||
```
|
||||
|
||||
### New Types
|
||||
|
||||
```go
|
||||
// NetworkInfo holds discovered local network configuration.
|
||||
type NetworkInfo struct {
|
||||
DNSServers []string // Ordered DNS server IPs from default-route adapter
|
||||
Gateway string // Default gateway IP (may be empty)
|
||||
Interface string // Adapter name holding the default route (informational)
|
||||
}
|
||||
```
|
||||
|
||||
### New Interface
|
||||
|
||||
```go
|
||||
// NetworkDiscoverer discovers local network configuration.
|
||||
type NetworkDiscoverer interface {
|
||||
Discover() (NetworkInfo, error)
|
||||
}
|
||||
```
|
||||
|
||||
### Platform Implementations
|
||||
|
||||
Each platform file (`platform_windows.go`, `platform_linux.go`, `platform_darwin.go`) adds a `Discover()` method on a platform-specific struct implementing `NetworkDiscoverer`.
|
||||
|
||||
| Platform | Commands Used | Key Parsing |
|
||||
|----------|--------------|-------------|
|
||||
| Windows | `netsh interface ipv4 show route`, `netsh interface ipv4 show interfaces`, `netsh interface ipv4 show dnsservers name="<name>"` | IPv4 regex on route/DNS output; index→name mapping |
|
||||
| Linux | `ip route show default`, `/etc/resolv.conf`, (conditional) `resolvectl status <iface>` | `via`/`dev` keywords; `nameserver` lines; stub-resolver detection |
|
||||
| macOS | `route -n get default`, `scutil --dns` | `gateway:`/`interface:` lines; resolver block parsing with `if_index` matching |
|
||||
|
||||
### Behavior Contract — Discover()
|
||||
|
||||
- Returns `NetworkInfo` with whatever was discovered. All fields may be empty.
|
||||
- Empty `DNSServers` is **not** an error — it means no local resolvers were found. The caller (resolver pool construction) handles this per FR-009.
|
||||
- Empty `Gateway` is **not** an error — it means no default route exists.
|
||||
- Returns an `error` only for unexpected failures (e.g., OS command execution error). Even then, `NetworkInfo` may have partial results.
|
||||
- **Must NOT** call `os.Exit()` or any process-terminating function.
|
||||
- All DNS server IPs and gateway IPs are validated with `net.ParseIP()` before inclusion.
|
||||
|
||||
### Testability
|
||||
|
||||
A `FakeNetworkDiscoverer` struct is provided for unit tests:
|
||||
|
||||
```go
|
||||
// FakeNetworkDiscoverer returns predetermined NetworkInfo for testing.
|
||||
type FakeNetworkDiscoverer struct {
|
||||
Info NetworkInfo
|
||||
Err error
|
||||
}
|
||||
|
||||
func (f *FakeNetworkDiscoverer) Discover() (NetworkInfo, error) {
|
||||
return f.Info, f.Err
|
||||
}
|
||||
```
|
||||
|
||||
This enables deterministic testing of resolver pool construction and mode dispatch without running real OS commands.
|
||||
|
||||
---
|
||||
|
||||
## Package: `hostfile` (unchanged)
|
||||
|
||||
No changes from 001. The hosts file read/parse/write pipeline is unaffected by this feature.
|
||||
|
||||
---
|
||||
|
||||
## Package: `lockfile` (unchanged)
|
||||
|
||||
No changes from 001.
|
||||
Reference in New Issue
Block a user