207 lines
8.2 KiB
Markdown
207 lines
8.2 KiB
Markdown
# 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.
|
||
```
|