# Feature Specification: Smart DNS Server Resolution Modes

**Feature Branch**: `002-server-resolution-modes`  
**Created**: 2026-03-03  
**Updated**: 2026-03-04  
**Status**: Draft  
**Input**: User description: "Extend the -server flag to support multiple resolution modes (local, gateway, default smart resolution) with parallel authoritative NS discovery, NXDOMAIN short-circuit, and smart error messages for internal hostnames."

## User Scenarios & Testing *(mandatory)*

### User Story 1 - Smart Default Resolution (Priority: P1)

As a network engineer deploying services on client networks, I need the tool to resolve hostnames intelligently without requiring me to specify a DNS server. The tool should automatically find the best authoritative answer — whether the hostname is a public internet address or an internal network resource — by querying local and public resolvers in parallel to discover the authoritative nameservers, then querying the authority directly for the freshest answer.

When no `-server` flag is provided, the tool builds a resolver pool from locally configured DNS resolvers plus a hardcoded set of reliable public resolvers. It performs a parallel NS fan-out at every label level of the hostname across all resolvers in the pool simultaneously. From the results, it picks the most-specific nameserver delegation and queries the authoritative NS directly. If no NS records are found at any level (common for internal hosts without zone delegation), it falls back to a parallel A query against all resolvers and takes the first successful answer.

**Why this priority**: This is the core behavior of the tool. Most users should never need to think about which resolver to use. The tool should just work — for public domains it gets a fresh authoritative answer, and for internal domains it gets the answer from whichever resolver knows about it.

**Independent Test**: Can be fully tested by running `dns-helper add -host www.example.com` (no `-server` flag) and verifying the tool discovers the authoritative NS for `example.com`, queries it directly, and adds the resulting IP to the hosts file.

**Acceptance Scenarios**:

1. **Given** a public hostname `www.example.com` and both local and public resolvers are available, **When** the user runs `dns-helper add -host www.example.com`, **Then** the tool performs parallel NS lookups for `www.example.com` and `example.com` across all resolvers in the pool (local + public), identifies the most-specific NS, resolves one of those NS hostnames to an IP, queries that NS directly for the A record with recursion disabled, and adds the result to the hosts file.

2. **Given** a hostname with subdomain delegation `host1.sub.domain.example.com` where `sub.domain.example.com` has its own NS records, **When** the user runs add without `-server`, **Then** the tool discovers the subdomain delegation (more specific than `example.com`) and queries those delegated nameservers for the final A record.

3. **Given** an internal hostname `myservice.client.local` where no NS records exist at any label level, **When** the user runs add without `-server`, **Then** the NS fan-out returns no NS records from any resolver, so the tool falls back to a parallel A query across all resolvers in the pool. The local resolver returns the IP while public resolvers return NXDOMAIN. The tool takes the successful answer from the local resolver.

4. **Given** a hostname that resolves to a CNAME pointing to a different domain, **When** the user runs add without `-server`, **Then** the tool follows the CNAME chain, restarts the authoritative NS discovery for the CNAME target domain, and returns the freshest A record from the target's authority.

5. **Given** local DNS is completely down (all local resolvers timeout), **When** resolving a public hostname, **Then** the public resolvers in the pool still return NS records and the tool proceeds with authoritative resolution normally — no delay waiting for local resolvers since all queries run in parallel.

6. **Given** all resolvers in the pool return NXDOMAIN at every label level and the hostname uses a well-known private TLD (e.g., `.local`, `.internal`, `.lan`), **When** the fan-out completes, **Then** the tool provides a context-aware error message indicating the hostname appears to be internal but no resolver could resolve it, and suggests using `-server <ip>` to specify a known working internal resolver.

7. **Given** a hostname `app.acme.com` where the public authoritative NS returns `203.0.113.50` but a local resolver returns a different IP `10.0.5.100` (split-horizon / internal override), **When** the user runs `dns-helper add -host app.acme.com` (no `-server`), **Then** the tool detects the conflict between the authoritative answer and the local answer, does NOT write either IP to the hosts file, and reports an error listing both IPs with a suggestion to use `-server local` to trust internal DNS or `-server <ip>` to choose explicitly.

8. **Given** a hostname `app.acme.com` where the public authoritative NS returns `93.184.216.34` and the local resolver also returns `93.184.216.34` (no conflict), **When** the user runs add without `-server`, **Then** the tool proceeds normally — the local cross-check confirms agreement and the IP is written to the hosts file.

9. **Given** a hostname `app.acme.com` where the authoritative NS returns an IP but the local resolver returns NXDOMAIN (no local override), **When** the user runs add without `-server`, **Then** the tool proceeds normally — NXDOMAIN from local resolvers is not a conflict, it simply means the hostname is not overridden locally.

---

### User Story 2 - Local Resolver Mode (Priority: P2)

As a user deploying on a client network where internal DNS is the source of truth, I want to explicitly use only the locally configured DNS resolvers to ensure I get answers exclusively from the local DNS infrastructure — without any public resolver involvement.

**Why this priority**: Supports the internal network deployment use case where the user explicitly trusts the local DNS infrastructure and wants to guarantee that only it is consulted. This is distinct from default mode where all resolvers participate.

**Independent Test**: Can be tested by running `dns-helper add -host internal.client.local -server local` and verifying it queries only the locally configured resolvers in priority order.

**Acceptance Scenarios**:

1. **Given** a system with DNS resolvers configured (statically or via DHCP) on the adapter that holds the default route (0.0.0.0), **When** the user runs with `-server local`, **Then** the tool discovers those resolvers and queries them in order (primary, secondary, tertiary) with a standard A query.

2. **Given** the primary local resolver times out, **When** using `-server local`, **Then** the tool tries the secondary resolver, and then the tertiary.

3. **Given** all local resolvers are unreachable, **When** using `-server local`, **Then** the tool displays an error. The user explicitly chose local resolution, so it does NOT fall back to public resolvers.

4. **Given** the local adapter has resolvers configured via DHCP, **When** using `-server local`, **Then** those DHCP-assigned resolvers are discovered and used identically to statically configured ones.

---

### User Story 3 - Gateway Resolver Mode (Priority: P3)

As a user on a SOHO or SMB network where the network gateway also acts as a DNS forwarder, I want to send DNS queries directly to the gateway for resolution.

**Why this priority**: Common in small networks without dedicated DNS infrastructure. The gateway (router) typically provides DNS forwarding, and this is a quick way to use it directly.

**Independent Test**: Can be tested by running `dns-helper add -host www.example.com -server gateway` and verifying the query is sent to the default gateway IP.

**Acceptance Scenarios**:

1. **Given** a system with a default route via a gateway (e.g., 192.168.1.1), **When** the user runs with `-server gateway`, **Then** the tool discovers the gateway IP and sends the DNS query to it.

2. **Given** the gateway does not respond to DNS queries, **When** using `-server gateway`, **Then** the tool displays an appropriate error message without falling back to other resolvers.

---

### User Story 4 - Explicit Server IP (Priority: P4)

As a user who knows exactly which DNS server to query (e.g., a newly deployed internal resolver), I want to specify it directly by IP address.

**Why this priority**: This is existing functionality that must continue to work identically.

**Independent Test**: Already tested by existing test suite.

**Acceptance Scenarios**:

1. **Given** a valid DNS server IP, **When** the user runs with `-server 10.0.0.53`, **Then** the query is sent to that IP (existing behavior, unchanged).

---

### Edge Cases

- **What happens when a CNAME chain exceeds 10 hops?** The tool stops immediately and reports an error indicating a probable CNAME loop or misconfigured zone. No further DNS queries are issued.
- **What happens when `-server` is given as `<ip>:<port>` and the port is invalid (e.g., 0 or >65535)?** The tool MUST reject the value immediately with a clear usage error before performing any DNS queries.
- **What happens when the hostname has only two labels (e.g., `example.com`)?** The NS fan-out queries NS for `example.com` only (one level), since there is no parent domain to check below the TLD.
- **What happens when a local resolver and a public resolver return different NS records for the same zone?** The tool collects results from all resolvers, deduplicates NS records, and picks the most-specific zone. If both return NS for the same zone, the records are merged and deduplicated.
- **What happens when the authoritative NS itself is unreachable?** If the first NS is unreachable, the tool tries other NS records returned for the same zone. If all NS are unreachable, it reports an error indicating the authoritative servers are not responding.
- **What happens when the NS fan-out returns no NS records at any label level?** The tool falls back to a parallel A query across all resolvers in the pool and takes the first successful answer. If all A queries also fail, it reports an error.
- **What happens when a CNAME target is in a different domain with its own delegation?** The tool restarts the authoritative NS discovery process for the CNAME target domain, following the same parallel fan-out workflow.
- **What happens when no local resolvers can be discovered?** The resolver pool consists of only the public bootstrap set. The tool proceeds normally — local resolver discovery failure is not an error for the default mode.
- **What happens when the hostname is internal but uses a public TLD (e.g., `server1.corp.acme.com` where `corp.acme.com` is an internal zone)?** The local resolver knows about the internal zone delegation and returns NS records for it during the fan-out. The tool selects those NS records as the most specific. If local DNS is down, the public resolvers won't know about the internal zone, and the tool will get the public answer (or NXDOMAIN if the zone doesn't exist publicly) — this is the best possible outcome when local DNS is unavailable.
- **What happens when a hostname uses a public TLD but the local network overrides it (split-horizon DNS, e.g., `app.acme.com` resolves to `10.0.5.100` locally but `203.0.113.50` publicly)?** After Stage 2 returns an authoritative answer, the tool performs a split-horizon cross-check: it queries only the local resolvers for the same hostname. If any local resolver returns a different IP than the authoritative answer, the tool reports a conflict and does NOT write either IP to the hosts file. The error message lists both IPs and suggests using `-server local` or `-server <ip>`. If the local resolver returns the same IP, NXDOMAIN, or fails, there is no conflict and the tool proceeds normally. This cross-check only runs in default mode (not `local`, `gateway`, or explicit IP modes) and only when local resolvers are available.
- **What happens when the local resolver returns a different IP but it's a CNAME chain that ultimately resolves to the same IP as the authoritative answer?** The cross-check compares the final A-record IPs, not intermediate CNAME targets. If the IPs match after resolution, there is no conflict.
- **What happens when the cross-check local query times out or fails?** A failed local query is treated as "no conflict" — the tool proceeds with the authoritative answer. Only a successful response with a different IP triggers the conflict error.
- **How does the tool distinguish a connection failure from an NXDOMAIN response?** Connection failures (timeout, refused) mean the resolver was unreachable. NXDOMAIN means the resolver responded authoritatively that the name does not exist. During the parallel fan-out, both are simply collected as results — the selection logic considers only successful NS responses. During the Stage 3 parallel A fallback, NXDOMAIN responses are skipped and only successful A responses are used.

## Requirements *(mandatory)*

### Functional Requirements

#### Server Mode Selection

- **FR-001**: The `-server` flag MUST accept the following values: an IP address (existing), `local`, `gateway`, or be omitted entirely.
- **FR-002**: When `-server` is omitted, the system MUST perform smart default resolution: parallel NS fan-out across all resolvers and all label levels, authoritative NS query, with a parallel A query fallback when no NS records are found.
- **FR-003**: When `-server local` is specified, the system MUST use only the DNS resolvers configured on the network adapter that holds the default route (0.0.0.0), querying each in priority order with standard A queries.
- **FR-004**: When `-server gateway` is specified, the system MUST send the DNS query to the default gateway IP of the adapter that holds the default route.
- **FR-005**: When `-server <ip>` or `-server <ip>:<port>` is specified, the system MUST send the DNS query to that IP address and port. When no port is provided, port 53 MUST be used. This extends existing behavior while remaining backward-compatible.
- **FR-006**: The `-server`, `-timeout`, and `-verbose` flags MUST be available on every command that performs DNS resolution. New commands added in future that perform DNS resolution MUST also expose these flags.

#### Resolver Pool

- **FR-007**: The system MUST maintain a hardcoded bootstrap resolver set consisting of (in order): 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 1.0.0.1 (Cloudflare secondary), 8.8.4.4 (Google secondary), 9.9.9.9 (Quad9), 208.67.222.222 (OpenDNS/Cisco).
- **FR-008**: When using default resolution (no `-server` flag), the system MUST build a resolver pool by combining locally discovered DNS resolvers with the bootstrap resolver set.
- **FR-009**: If local resolver discovery fails (e.g., no default route adapter found), the resolver pool MUST consist of only the bootstrap resolver set, and the tool MUST proceed without error.

#### Local Resolver and Gateway Discovery

- **FR-010**: The system MUST discover DNS resolvers configured on the network adapter that holds the default route (0.0.0.0), regardless of whether the adapter is configured statically or via DHCP.
- **FR-011**: Local resolvers MUST be returned in priority order (primary, secondary, tertiary).
- **FR-012**: The system MUST discover the default gateway IP for the adapter that holds the default route.

#### Default Resolution — Stage 1: Parallel NS Fan-out

- **FR-013**: The system MUST perform parallel NS lookups for every label level of the specified hostname, excluding TLDs and the root. For `host1.sub.domain.example.com`, the system queries NS for: `host1.sub.domain.example.com`, `sub.domain.example.com`, `domain.example.com`, and `example.com`.
- **FR-014**: The parallel NS lookups MUST be sent simultaneously to every resolver in the pool, across every label level. Each combination of (label level × resolver) runs as a concurrent operation.
- **FR-015**: The system MUST select the NS records from the most-specific label level (longest name) that returned valid NS records from any resolver in the pool.
- **FR-016**: Each NS result collected MUST include the queried label level, the resolver that was queried, and the NS records returned (if any), so the selection logic can evaluate specificity.

#### Default Resolution — Stage 2: Authoritative Query

- **FR-017**: After identifying the authoritative NS from Stage 1, the system MUST resolve one of the returned NS hostnames to an IP address using the resolver pool.
- **FR-018**: The system MUST send the final A-record query to the authoritative NS with recursion disabled.
- **FR-019**: If the authoritative answer is a CNAME pointing to a different domain, the system MUST restart the full resolution process (Stage 1) for the CNAME target domain. The system MUST NOT follow more than 10 CNAME hops in a single resolution; if this limit is exceeded, the tool MUST report an error indicating a probable CNAME loop.
- **FR-020**: If the first authoritative NS is unreachable, the system MUST try other NS records from the same zone before reporting failure.

#### Default Resolution — Stage 2.5: Split-Horizon Conflict Detection

- **FR-032**: After Stage 2 returns an authoritative A-record answer, and when local resolvers are available in the resolver pool, the system MUST perform a split-horizon cross-check by querying only the local resolvers for the same hostname with a standard A query.
- **FR-033**: The cross-check query SHOULD run in parallel with Stage 2 (fired at the same time or immediately after Stage 1 completes) so that it adds no additional wall-clock latency to the happy path.
- **FR-034**: If any local resolver returns a successful A-record response with a different set of IPs than the authoritative answer from Stage 2, the system MUST report a conflict error and MUST NOT write either IP to the hosts file.
- **FR-035**: The conflict error message MUST include both the authoritative IP(s) and the local IP(s), identify which resolver provided each, and suggest using `-server local` to trust the internal DNS or `-server <ip>` to choose a specific resolver.
- **FR-036**: If the local cross-check returns the same IP(s) as the authoritative answer, returns NXDOMAIN, or fails (timeout, connection error), the system MUST treat this as "no conflict" and proceed normally with the authoritative answer.
- **FR-037**: The split-horizon cross-check MUST only run in default mode (no `-server` flag). It MUST NOT run in `local`, `gateway`, or explicit IP modes — those modes represent an explicit user choice and should not second-guess the result.
- **FR-038**: The split-horizon cross-check MUST NOT run when no local resolvers were discovered (i.e., the resolver pool contains only bootstrap resolvers). In this case, there is no local DNS to conflict with.

#### Default Resolution — Stage 3: Parallel A Query Fallback

- **FR-021**: If Stage 1 returns no NS records at any label level from any resolver, the system MUST fall back to a parallel A query for the hostname across all resolvers in the pool.
- **FR-022**: The system MUST use the first successful A-record response from the parallel query. NXDOMAIN responses and connection failures are ignored in favor of any successful answer.
- **FR-023**: If all resolvers return NXDOMAIN or fail, the system MUST report an error.

#### Timeout Configuration

- **FR-028**: The `-timeout` flag MUST accept a positive integer value representing the per-query DNS timeout in seconds. The default value is 3 seconds when the flag is omitted.
- **FR-029**: The configured timeout MUST apply uniformly to every DNS query issued by the tool — NS fan-out queries (Stage 1), authoritative NS resolution (Stage 2), parallel A fallback queries (Stage 3), and queries made in `-server local` and `-server gateway` modes.

#### Verbose Output

- **FR-030**: By default the tool MUST produce no diagnostic output — only the final result or an error message is printed. This ensures output is safe for scripting and automation.
- **FR-031**: When the `-verbose` flag is provided, the tool MUST emit a per-stage resolution trace to stderr, including: resolvers in the pool, NS records found at each label level, the authoritative NS selected, whether Stage 3 fallback was triggered, and any CNAME redirects followed.

#### Error Messages and Private TLD Detection

- **FR-024**: The system MUST detect well-known private TLDs (`.local`, `.internal`, `.lan`, `.home`, `.corp`, `.private`) in the target hostname.
- **FR-025**: When resolution fails entirely and the hostname uses a private TLD, the error message MUST indicate the hostname appears to be internal and suggest using `-server <ip>` with a known internal resolver.
- **FR-026**: When using `-server local` and all local resolvers are unreachable, the system MUST NOT fall back to public resolvers and MUST report an error.
- **FR-027**: When using `-server gateway` and the gateway does not respond to DNS queries, the system MUST report an appropriate error without fallback.

### Key Entities

- **Server Mode**: The resolution strategy selected by the user — one of: explicit IP, `local`, `gateway`, or default (omitted). Determines which resolvers are queried and the resolution pattern used.
- **Resolver Pool**: The combined set of all local resolvers and public bootstrap resolvers, built at runtime for default resolution. All members are queried in parallel during fan-out operations.
- **Bootstrap Resolver Set**: An ordered list of six well-known public DNS resolvers across four independent providers (Cloudflare, Google, Quad9, OpenDNS). Always available regardless of local network state.
- **Local Resolver List**: An ordered list of DNS server IPs configured on the network adapter that holds the default route. Discovered at runtime from the operating system. May be empty if discovery fails.
- **NS Fan-out Result**: The response from a single NS query during Stage 1 discovery. Contains the queried label level, the resolver that was queried, and the returned NS records (if any). Collected from parallel operations for evaluation.
- **Authoritative NS**: The nameserver identified as the most-specific authority for the target hostname. The final A-record query is sent here with recursion disabled.
- **Per-Query Timeout**: The maximum time the tool waits for any single DNS query response before treating the resolver as non-responsive. Defaults to 3 seconds; configurable via the `-timeout` flag.
- **Verbose Mode**: An opt-in diagnostic mode activated by the `-verbose` flag. Emits a per-stage resolution trace to stderr without affecting stdout output used by callers.
- **Split-Horizon Conflict**: A state in default mode where the authoritative answer (from Stage 2) differs from the local resolver's answer for the same hostname. Indicates the hostname exists in both public DNS and local DNS with different IPs — typically caused by split-horizon DNS, internal overrides, or conditional forwarding. The tool cannot determine which IP the user intended, so it reports both and asks the user to choose explicitly.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: When no `-server` flag is provided for a public hostname, the resolved IP matches the IP returned by directly querying the hostname's authoritative nameserver, confirming the tool bypasses intermediate caches.
- **SC-002**: The parallel NS fan-out completes within the latency of the slowest single resolver response — wall-clock time does not grow linearly with the number of resolvers or label levels queried.
- **SC-003**: When no `-server` flag is provided, the tool resolves both public hostnames (e.g., `www.google.com`) and internal hostnames (when local DNS is functional) without any additional flags.
- **SC-004**: When local DNS is down and a public hostname is specified without `-server`, the tool still resolves because public resolvers in the pool return NS records and the authoritative path succeeds.
- **SC-005**: When resolution fails for a hostname with a private TLD, the user receives an actionable error message within 10 seconds.
- **SC-006**: The `-server local` mode discovers and uses all configured resolvers (primary, secondary, tertiary) on both statically configured and DHCP-configured adapters.
- **SC-007**: The `-server gateway` mode correctly identifies and queries the default gateway even when the gateway IP changes between sessions.
- **SC-008**: Subdomain delegation is correctly detected — when a subdomain has its own NS records, the default resolution queries those NS servers rather than the parent domain's NS servers.
- **SC-009**: Internal hostnames without NS delegation (Stage 3 fallback) resolve within the same timeout window as hostnames with NS records — the fallback does not add meaningful delay because all queries run in parallel.
- **SC-010**: When a hostname resolves to different IPs via authoritative DNS vs. local DNS (split-horizon), the tool detects the conflict and reports both IPs without writing either to the hosts file.
- **SC-011**: The split-horizon cross-check adds no measurable latency to the happy path (no conflict) because it runs in parallel with Stage 2.

## Assumptions

- The network adapter holding the default route (0.0.0.0) is the correct adapter to inspect for local resolvers and gateway — multi-homed configurations with multiple default routes are out of scope.
- The bootstrap resolver set (Cloudflare, Google, Quad9, OpenDNS) provides sufficient resilience for the vast majority of scenarios; full iterative resolution from root hints is deferred to a future enhancement.
- DNS queries use UDP only (no TCP fallback), consistent with the existing resolver behavior.
- The tool runs with sufficient OS permissions to query adapter configuration (DNS servers, gateway IP) on Windows, Linux, and macOS.
- Private TLD detection uses a static list of well-known private TLDs; custom internal TLDs using public suffixes (e.g., `.com` used internally) are not automatically detected but will still be resolved correctly by local resolvers participating in the pool.
- When no NS records are found during Stage 1, the fallback to parallel A queries (Stage 3) is appropriate — this primarily covers internal hosts in environments without zone delegation (common in small AD or dnsmasq setups).
- The per-query DNS timeout default is 3 seconds. This fits within the 10-second error window stated in SC-005 and aligns with OS resolver defaults. Users may override this with the `-timeout` flag.

## Clarifications

### Session 2026-03-04

- Q: What is the per-query DNS timeout, and should it be user-configurable? → A: 3 seconds default; expose a `-timeout <seconds>` flag so users can override it.
- Q: Should the tool emit diagnostic output during multi-stage resolution? → A: Silent by default; `-verbose` flag emits per-stage trace (resolvers queried, NS selected, fallbacks triggered) to stderr.
- Q: Should `-server <ip>` support a non-standard port? → A: Accept `<ip>` or `<ip>:<port>`; default to port 53 when port is omitted. Invalid port values are rejected with a usage error.
- Q: Should CNAME chain following have a depth limit? → A: Hard limit of 10 CNAME hops; report an error indicating a probable loop if exceeded.
- Q: Which commands does the new resolution behavior (`-server`, `-timeout`, `-verbose`) apply to? → A: All commands that perform DNS resolution, current and future.
- Q: What happens when the local network overrides a public hostname (split-horizon DNS)? → A: After Stage 2 gets an authoritative answer, the tool cross-checks against local resolvers. If local resolvers return a different IP, the tool reports a conflict with both IPs and suggests `-server local` or `-server <ip>`. A ping-based tiebreaker was considered and rejected because ICMP is widely blocked, requires elevated privileges on Linux, and reachability ≠ correctness. The A-record comparison is sufficient — the divergence itself is the signal.
