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