Files

207 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
```