Files
dnshelper/specs/002-server-resolution-modes/contracts/cli.md
T

8.2 KiB
Raw Blame History

CLI Contract: dns-helper (002 — Server Resolution Modes)

Feature Branch: 002-server-resolution-modes
Date: 2026-03-04
Extends: 001 CLI Contract

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 165535. 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.