# 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 [-server ] [-timeout ] [-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. | | `` | Query the specified IP on port 53. Existing behavior, unchanged. See FR-005. | | `:` | Query the specified IP on the given port. Port must be 1–65535. See FR-005. | ### `-server` Validation - `:` where port is 0 or >65535: **Immediate usage error** (no DNS queries issued). - `` 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 ``` **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 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. ```