29 KiB
Research: Server Resolution Modes
Feature Branch: 002-server-resolution-modes
Date: 2026-03-04
R-000: Parallel DNS Fan-out Patterns (Go 1.20)
Decision: Goroutines + Buffered Channel
Use goroutines writing to a buffered channel sized to N×M (resolvers × label levels) with a collecting loop that reads exactly N×M results. No mutex, no WaitGroup needed.
Rationale
- Each goroutine writes exactly one result; the collector reads exactly
N×Mvalues. Bounded channel means goroutines never block. - Alternative (WaitGroup + shared slice + Mutex) adds unnecessary synchronization complexity for no benefit.
Per-Query vs Overall Timeout
Use nested context.WithTimeout — parent context sets overall operation deadline (10s), child contexts set per-query deadline (3s from -timeout flag). Parent cancellation propagates to all children automatically. All context APIs available since Go 1.7.
UDP Transport with Context (Go 1.20)
Use net.Dialer.DialContext() (available since Go 1.7) for context-aware dialing. After dialing, use conn.SetDeadline() derived from context deadline. There is no conn.ReadContext() in Go 1.20 — SetDeadline is the standard approach.
NS Query Construction
Identical to existing A query in resolver.go — only Type changes from dnsmessage.TypeA to dnsmessage.TypeNS. Parse responses with parser.NSResource() which returns NSResource{NS: dnsmessage.Name}.
Authoritative Query (RD=false)
Set RecursionDesired: false in dnsmessage.Header. Check Authoritative flag in response for validation/logging. Handle RCodeRefused as "try next NS". Handle empty answers with RCodeSuccess as possible referral — try next NS.
CNAME in NS Responses
Uncommon but possible (zone misconfiguration or CDN aliases). Extend NS response parser to return both NS records and CNAME target. If CNAME received instead of NS, the caller restarts NS discovery for the CNAME target domain. Shared depth counter prevents infinite loops (max 10, matching existing maxCNAMEDepth).
Label-Level Extraction
Split hostname on ., generate all suffixes with ≥2 labels (from most-specific to least-specific). host1.sub.example.com → ["host1.sub.example.com", "sub.example.com", "example.com"]. Single-label hostnames return nil. No public suffix list needed — specificity-based NS selection handles multi-part TLDs naturally.
Go 1.20 Limitations
context.WithTimeoutCause(1.21+): Not available. Usecontext.WithTimeout+ manual error wrapping.slicespackage (1.21+): Not available. Usesort.Slicefor sorting.mapspackage (1.21+): Not available. Manual map iteration.slog(1.21+): Not available. Usefmt.Fprintf(os.Stderr, ...)for verbose output.errors.Join(1.20): Available for aggregating errors from parallel queries.- Generics (1.18): Available in Go 1.20 but not required for this feature.
Alternatives Considered
sync.WaitGroup+ shared slice: Requires mutex, separate done signal. More moving parts for the same result. Rejected.errgroupfromgolang.org/x/sync: Good API but adds an approved-but-unnecessary dependency. The buffered channel pattern is simpler and requires no imports. Rejected.- Manual deadline tracking with
time.After: Error-prone, doesn't propagate cancellation.context.WithTimeoutis superior. Rejected.
R-001: Windows — Local Resolver and Gateway Discovery
Decision
Use a three-command netsh pipeline via os/exec:
netsh interface ipv4 show route→ find the0.0.0.0/0row to extract the interface index (Idx) and gateway IP.netsh interface ipv4 show interfaces→ map the interface index to an interface name.netsh interface ipv4 show dnsservers name="<interface name>"→ extract DNS server IPs.
Rationale
netshis present on every Windows version from Vista through Windows 11/Server 2025. It does not require PowerShell, elevated privileges, or any runtime beyond the base OS.- The numeric data we parse (IP addresses, interface indices,
0.0.0.0/0prefix) is never localized. Column headers and labels are localized on non-English Windows, but we do not parse them — we parse by pattern (IP regex, prefix match) and by column position. - Each command returns rapidly (< 50ms) and produces small output.
Go 1.20 Compatibility
os/exec.Command(),cmd.Output(): available since Go 1.0.strings,regexp,bufio,net: all available in Go 1.20.- No CGo, no WMI bindings, no PowerShell dependency.
Output Formats and Parsing Strategy
Step 1: Default Route — netsh interface ipv4 show route
Verified output (live, 2026-03-04):
Publish Type Met Prefix Idx Gateway/Interface Name
------- -------- --- ------------------------ --- ------------------------
No Manual 0 0.0.0.0/0 4 10.26.1.1
No System 256 10.26.0.0/16 4 vEthernet (Bridge)
No System 256 127.0.0.0/8 1 Loopback Pseudo-Interface 1
Parsing:
- Scan each line for
0.0.0.0/0(literal match; never localized). - When found, extract fields by splitting on whitespace. The row format is:
<Publish> <Type> <Metric> <Prefix> <Idx> <Gateway or Interface Name> - Field 4 (0-indexed) is the interface index. Field 5+ is the gateway IP (an IP address) or the interface name (a string with spaces). We only need the gateway when it's an IP address; for the
0.0.0.0/0route there is always a gateway IP. - Use a regex on the gateway field to match an IPv4 address:
(\d{1,3}\.){3}\d{1,3}. - If multiple
0.0.0.0/0rows exist (e.g., VPN active), take the one with the lowest metric (field 2).
Extracted values: Interface Index (int), Gateway IP (string).
Step 2: Interface Name — netsh interface ipv4 show interfaces
Verified output (live, 2026-03-04):
Idx Met MTU State Name
--- ---------- ---------- ------------ ---------------------------
1 75 4294967295 connected Loopback Pseudo-Interface 1
13 1 1500 disconnected Local Area Connection
4 25 1500 connected vEthernet (Bridge)
Parsing:
- Skip header lines (first two).
- Split each line on whitespace. Field 0 is Idx, field 4+ is the interface name (may contain spaces — take everything from field 4 onward, trimmed).
- Match the row where Idx equals the interface index from Step 1.
Extracted value: Interface Name (string).
Step 3: DNS Servers — netsh interface ipv4 show dnsservers name="<name>"
Verified output (live, 2026-03-04, DHCP-configured):
Configuration for interface "vEthernet (Bridge)"
DNS servers configured through DHCP: 10.26.1.1
Register with which suffix: Primary only
Known format for static configuration with multiple servers:
Configuration for interface "Ethernet"
Statically Configured DNS Servers: 10.0.0.53
8.8.8.8
8.8.4.4
Register with which suffix: Primary only
Parsing:
- Do not parse the label text (it is localized). Instead, scan each line for an IPv4 address regex:
^\s*(?:\S.*:\s+)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*$. - This matches both the first DNS server (after the label) and continuation lines (indented IPs on subsequent lines).
- Validate each match with
net.ParseIP()to reject false positives. - Collect all IPs in order — the first is primary, second is secondary, etc.
Extracted values: Ordered list of DNS server IPs.
Alternatives Considered
-
ipconfig /all: Outputs all adapter info in one shot, but labels ("Default Gateway", "DNS Servers") are fully localized on non-English Windows. Parsing is fragile because there is no fixed column format — values can wrap to multiple lines with inconsistent indentation, and adapter sections are delimited only by blank lines. Rejected for localization and parsing reliability. -
route print 0.0.0.0: Returns the IPv4 route table including the default route. Shows Gateway and Interface (as IP, not name), but the output mixes freeform text (Interface List) with a columnar table, making parsing more complex thannetsh interface ipv4 show route. Also, the interface is shown as an IP address, not an index or name, requiring an additional correlation step. Rejected for added complexity with no benefit. -
PowerShell
Get-DnsClientServerAddress/Get-NetIPConfiguration: Powerful and structured, but requires PowerShell to be present and invocable. On Server Core or minimal installs, PowerShell may not be available. Invoking PowerShell fromos/execincurs a ~500ms startup penalty. The output ofGet-NetIPConfigurationis CIM object formatting (e.g.,MSFT_NetRoute (InstanceID = ...)) that is not human-parseable without-Formatswitches. Rejected for startup cost and deployment fragility. -
Registry (
HKLM\...\Tcpip\Parameters\Interfaces): TheNameServer(static) andDhcpNameServer(DHCP) registry values contain DNS server IPs, andDhcpDefaultGatewaycontains the gateway. This is locale-independent and fast. However, correlating the interface GUID with the interface that holds the default route requires reading the route table anyway (the registry stores routes per-interface-GUID, not in a centralized table). The approach was considered as a backup strategy but rejected for primary use because the GUID→default-route correlation is complex and brittle. It remains a viable fallback ifnetshis ever unavailable. -
WMI/CIM (Go bindings): No maintained Go WMI library works under Go 1.20.
github.com/StackExchange/wmirequiresgo-olewhich has compatibility issues. Out of scope per Go 1.20 constraint.
Edge Cases
| Case | Handling |
|---|---|
| DHCP configuration | Verified: netsh shows DHCP-assigned DNS identically to static — just the label differs. Our IP-regex parsing handles both. |
| Multiple default routes (VPN, dual NIC) | Parse all 0.0.0.0/0 rows, select the one with the lowest metric. This matches Windows routing behavior. |
| No default route | No 0.0.0.0/0 row found. Return empty resolver list and empty gateway. FR-009 handles gracefully. |
| No DNS servers on default-route adapter | Step 3 output contains no IP addresses. Return empty resolver list. |
| IPv6-only adapter | We query ipv4 show route. No IPv4 default route found → return empty. IPv6 DNS discovery is out of scope. |
| Interface name with spaces/parens | Verified: interface names like vEthernet (Bridge) work correctly when quoted in netsh commands. Always quote the name. |
netsh not found (Server Nano/Container) |
exec.Command returns error. Return empty list. FR-009 handles gracefully. |
| Localized Windows | All parsed data (IPs, indices, 0.0.0.0/0) is numeric/standard notation. Label text is ignored. Safe on all locales. |
R-002: Linux — Local Resolver and Gateway Discovery
Decision
Use a two-phase approach:
ip route show default→ extract gateway IP and interface name.- Parse
/etc/resolv.conffornameserverlines → extract DNS server IPs. - If all nameservers are stub-resolver addresses (
127.0.0.53,127.0.0.1,127.0.1.1), attemptresolvectl status <interface>to discover upstream DNS servers.
Rationale
ip routeis part ofiproute2, present on every mainstream Linux distribution (Debian, Ubuntu, RHEL, Fedora, Arch, Alpine, SUSE) since ~2010. It replaced the deprecatedroutecommand./etc/resolv.confis the POSIX-standard DNS configuration file, present on every Linux system.- The
resolvectlfallback handles the systemd-resolved case cleanly without adding complexity for non-systemd distributions.
Go 1.20 Compatibility
os/exec.Command(): available since Go 1.0.os.Open(),bufio.Scanner: available since Go 1.0/1.1.os.Readlink(): available since Go 1.0 (used for symlink detection).- No CGo required.
Output Formats and Parsing Strategy
Step 1: Default Route — ip route show default
Standard output:
default via 192.168.1.1 dev eth0 proto dhcp metric 100
Multiple default routes (rare):
default via 192.168.1.1 dev eth0 proto dhcp metric 100
default via 10.0.0.1 dev wlan0 proto dhcp metric 600
Parsing:
- Split each line on whitespace.
- Look for lines starting with
default. - Extract
via <IP>for gateway anddev <interface>for interface name. - If multiple default routes exist, take the one with the lowest metric (parse
metric <N>). - The
viaanddevkeywords are not localized (they are part of theipcommand's fixed syntax).
Extracted values: Gateway IP (string), Interface Name (string).
Step 2: DNS Servers — /etc/resolv.conf
Standard format (RFC 2136):
# Generated by NetworkManager
nameserver 192.168.1.1
nameserver 8.8.8.8
systemd-resolved stub format:
# This is /run/systemd/resolve/stub-resolv.conf managed by systemd-resolved.
nameserver 127.0.0.53
options edns0 trust-ad
search lan
Parsing:
- Read file line by line.
- Skip lines starting with
#or;(comments). - For lines starting with
nameserver, extract the IP address after the keyword. - Validate with
net.ParseIP(). - Collect in order — the order in
resolv.confrepresents priority.
Extracted values: Ordered list of DNS server IPs.
Step 3 (conditional): systemd-resolved — resolvectl status <interface>
Triggered when all nameservers from Step 2 are loopback addresses (127.0.0.53, 127.0.0.1, 127.0.1.1).
Standard output:
Link 2 (eth0)
Current Scopes: DNS
DefaultRoute setting: yes
LLMNR setting: yes
MulticastDNS setting: no
DNSOverTLS setting: no
DNSSEC setting: no
DNSSEC supported: no
Current DNS Server: 192.168.1.1
DNS Servers: 192.168.1.1
8.8.8.8
DNS Domain: lan
Parsing:
- Look for the
DNS Servers:label (or fall back toCurrent DNS Server:ifDNS Servers:is absent). - Extract the IP after the colon on the
DNS Servers:line. - Continue reading subsequent lines that are indented (continuation IPs).
- Validate each with
net.ParseIP().
Note on localization: resolvectl output is not localized — the keywords are hardcoded in the systemd source. Safe to parse literally.
Alternatives Considered
-
resolvectlas primary approach (skip/etc/resolv.conf): Would miss DNS configuration on non-systemd systems (Alpine, older Debian, container distros, WSL1)./etc/resolv.confis universally present. Rejected as primary; used only as fallback for stub detection. -
nmcli dev show <interface>: NetworkManager-specific. ReturnsIP4.DNS[1]: 192.168.1.1etc. Works well on desktop Linux with NetworkManager but absent on servers, containers, and Alpine. Considered as a tertiary fallback afterresolvectlbut rejected to avoid cascading fallback complexity. Ifresolvectlfails on a stub-resolver system, the stub address itself (127.0.0.53) is still a valid local resolver — it just forwards to upstream. Acceptable for the tool's purposes. -
systemd-resolve --status: Deprecated alias forresolvectl status. Usingresolvectldirectly is forward-compatible. Ifresolvectlis not found,systemd-resolvecould be tried, but this adds complexity for diminishing returns. Rejected. -
Parse
/run/systemd/resolve/resolv.conf(the non-stub version): Contains actual upstream nameservers without the127.0.0.53stub. However, this file's existence and path are implementation details of systemd-resolved, not a stable contract. Rejected in favor ofresolvectlwhich is a proper CLI API. -
route -n(deprecatednet-toolscommand): Not present on minimal installs.ip routefromiproute2is the modern replacement and universally available. Rejected.
The systemd-resolved Question
Q: Is /etc/resolv.conf sufficient on Linux, or does systemd-resolved require special handling?
A: /etc/resolv.conf is sufficient for basic functionality, but systemd-resolved benefits from special handling for optimal results.
The detailed analysis:
| Scenario | /etc/resolv.conf contents |
Behavior if we use it as-is | With resolvectl fallback |
|---|---|---|---|
| No systemd-resolved | Real upstream IPs (e.g., 192.168.1.1) |
Correct. These are the actual DNS servers. | N/A — fallback not triggered. |
| systemd-resolved (stub mode, default) | 127.0.0.53 |
Functional but suboptimal. The tool queries the stub resolver, which forwards to upstream. For -server local mode, the user expects to query the network's DNS directly, not a local forwarder. For the resolver pool in default mode, the stub is fine — it provides access to the same DNS infrastructure. |
Discovers actual upstream servers (e.g., 192.168.1.1). Better fidelity to FR-010 ("resolvers configured on the network adapter"). |
| systemd-resolved (non-stub symlink) | Real upstream IPs | Correct. /etc/resolv.conf → /run/systemd/resolve/resolv.conf which has real IPs. |
N/A — fallback not triggered. |
| dnsmasq local forwarder | 127.0.0.1 |
Similar to systemd-resolved stub. Functional but indirect. | resolvectl won't help here (different subsystem). The 127.0.0.1 address is the best we can do — it's the system's DNS path. |
Recommendation: Implement the resolvectl fallback to handle the common systemd-resolved stub case. For other local forwarders (dnsmasq, unbound), accept the loopback address as the local resolver — it IS the system-configured DNS path and will function correctly.
Edge Cases
| Case | Handling |
|---|---|
| DHCP configuration | ip route shows DHCP routes identically to static. /etc/resolv.conf is updated by DHCP client or NetworkManager. Transparent. |
| No default route | ip route show default returns empty stdout. Return empty list. |
| Multiple default routes | Take the lowest-metric route. |
/etc/resolv.conf missing |
os.Open() returns error. Return empty resolver list. |
/etc/resolv.conf is a broken symlink |
os.Open() returns error. Return empty resolver list. |
| WSL (Windows Subsystem for Linux) | /etc/resolv.conf typically contains the Windows host IP (e.g., 172.x.x.1). ip route show default works. This is the correct local resolver for WSL. |
| Alpine / BusyBox | ip from BusyBox supports ip route show default. /etc/resolv.conf is standard. No systemd-resolved. Works correctly. |
| Container (Docker/Podman) | /etc/resolv.conf is injected by the runtime. ip route show default works. Standard behavior. |
resolvectl not found |
exec.LookPath("resolvectl") fails. Skip fallback, use /etc/resolv.conf entries as-is (the stub 127.0.0.53 is still functional). |
ip command not found |
Extremely rare (would need a broken minimal install). exec.Command returns error. Return empty list. |
R-003: macOS — Local Resolver and Gateway Discovery
Decision
Use a two-command approach:
route -n get default→ extract gateway IP and interface name.scutil --dns→ extract DNS server IPs by matching resolver entries to the default-route interface.
Rationale
routeandscutilare built-in macOS system utilities, present on every macOS version from at least 10.6 (Snow Leopard) through the latest releases.scutil --dnsprovides the most complete DNS configuration view on macOS, including both manually configured and DHCP-assigned DNS servers, with interface correlation.- Neither command requires elevated privileges for reading configuration.
Go 1.20 Compatibility
os/exec.Command(),cmd.Output(): available since Go 1.0.strings,regexp,bufio,net: all available in Go 1.20.- No CGo, no Objective-C runtime, no Swift.
Output Formats and Parsing Strategy
Step 1: Default Route — route -n get default
Standard output:
route to: default
destination: default
mask: default
gateway: 192.168.1.1
interface: en0
flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,AUTOCONF>
recvpipe sendpipe ssthresh rtt,msec rttvar hopcount mtu expire
0 0 0 0 0 0 1500 0
Parsing:
- Scan lines for
gateway:→ extract the IP address after the colon (trimmed). - Scan lines for
interface:→ extract the interface name after the colon (trimmed). - Both keywords are fixed (macOS
routeoutput is not localized). - Validate gateway with
net.ParseIP().
Extracted values: Gateway IP (string), Interface Name (string, e.g., en0).
Step 2: DNS Servers — scutil --dns
Standard output (example with Wi-Fi on en0):
DNS configuration
resolver #1
nameserver[0] : 192.168.1.1
nameserver[1] : 8.8.8.8
if_index : 6 (en0)
flags : Request A records
reach : 0x00020002 (Reachable,Directly Reachable Address)
resolver #2
domain : local
options : mdns
timeout : 5
flags : Request A records
reach : 0x00000000 (Not Reachable)
order : 300000
DNS configuration (for scoped queries)
resolver #1
nameserver[0] : 192.168.1.1
nameserver[1] : 8.8.8.8
if_index : 6 (en0)
flags : Scoped, Request A records
reach : 0x00020002 (Reachable,Directly Reachable Address)
Parsing strategy:
The output is organized into resolver blocks separated by resolver #N headers. The parsing approach:
- Split the output into resolver blocks (delimited by
resolver #<N>lines). - For each block, extract:
- Nameserver IPs: lines matching
nameserver\[\d+\]\s*:\s*(\S+) - Interface: line matching
if_index\s*:\s*\d+\s*\((\w+)\)→ extract the interface name from parentheses
- Nameserver IPs: lines matching
- Primary strategy: Find the resolver block in the top section (before "DNS configuration (for scoped queries)") whose
if_indexmatches the default-route interface from Step 1. Use its nameservers. - Fallback strategy: If no interface match is found, use the nameservers from
resolver #1in the top section — this is the system's primary resolver and is almost always the correct one. - Ignore the "scoped queries" section (below the second
DNS configurationheader) — these are duplicate entries for interface-scoped resolution. - Ignore resolver blocks with
domain : localandoptions : mdns— these are mDNS/Bonjour resolvers, not general-purpose DNS.
Extracted values: Ordered list of DNS server IPs.
Alternatives Considered
-
networksetup -getdnsservers <service>: Returns manually configured DNS servers for a named network service (e.g., "Wi-Fi", "Thunderbolt Ethernet"). Problems: (a) Service names are potentially localized. (b) Only returns manually-set DNS servers — returns "There aren't any DNS Servers set on Wi-Fi." when DNS is DHCP-assigned. (c) Requires a separatenetworksetup -listallhardwareportscall to map the interface device (e.g.,en0) to a service name. Rejected for DHCP blindness and localization risk. -
ipconfig getpacket <interface>: Returns the DHCP packet for an interface, includingdomain_name_serveroption with DNS IPs. Works for DHCP but does not work for statically configured DNS. Would need a separate path for static DNS. Rejected for complexity. -
Parse
/etc/resolv.confon macOS: macOS does have/etc/resolv.conf, but it is managed byconfigdand may not reflect the actual DNS configuration accurately (especially with split DNS, VPN, or multiple interfaces).scutil --dnsis the authoritative source on macOS. Rejected for accuracy concerns. -
dns-sd -G v4 <hostname>: Thedns-sdcommand performs mDNS/DNS-SD lookups, not general DNS configuration discovery. Not relevant. Rejected. -
System Configuration framework via CGo: Could call
SCDynamicStoreCopyValuedirectly forState:/Network/Global/DNS. Would require CGo and Objective-C bridging. Violates theos/execapproach constraint and adds build complexity. Rejected.
Edge Cases
| Case | Handling |
|---|---|
| DHCP configuration | scutil --dns shows DHCP-assigned DNS servers identically to static ones. Transparent. |
| Manually set DNS | scutil --dns shows manually configured DNS servers. Transparent. |
| VPN active | VPN may add resolver blocks and change the default route. route -n get default will return the VPN gateway. scutil --dns will show VPN-specific resolvers. The correct behavior is to follow the default route — if VPN is active, its DNS is the right choice. |
| Wi-Fi + Ethernet both active | route -n get default returns the active path (typically Ethernet due to lower metric). scutil --dns interface matching selects the correct resolver block. |
| No default route | route -n get default outputs an error or no gateway: line. Return empty list. |
| No DNS configured | scutil --dns shows resolver blocks with no nameserver lines. Return empty list. |
| mDNS-only resolver entries | Filtered out by ignoring blocks with domain : local + options : mdns. |
Split DNS (e.g., search domains) |
The tool uses the primary resolver block, not domain-specific blocks. This is correct for general-purpose DNS queries. |
R-004: Gateway IP Discovery (All Platforms)
Decision
Gateway IP is discovered as a byproduct of the default-route discovery command on each platform:
| Platform | Command | Gateway extraction |
|---|---|---|
| Windows | netsh interface ipv4 show route |
Gateway column of the 0.0.0.0/0 row |
| Linux | ip route show default |
The IP after via keyword |
| macOS | route -n get default |
The IP on the gateway: line |
No additional commands needed. The gateway IP is always available from the same command that identifies the default-route interface.
R-005: Implementation Interface Design
Decision
Define a NetworkDiscoverer interface in the platform package for testability. Each platform file implements it. Tests use a fake implementation.
// NetworkInfo holds discovered network configuration.
type NetworkInfo struct {
// DNSServers is the ordered list of DNS server IPs configured on the
// default-route adapter. May be empty if discovery fails.
DNSServers []string
// Gateway is the default gateway IP. May be empty if no default route.
Gateway string
// Interface is the name of the adapter holding the default route.
Interface string
}
// NetworkDiscoverer discovers local network configuration.
type NetworkDiscoverer interface {
Discover() (NetworkInfo, error)
}
The implementations call os/exec internally. For unit tests, a FakeNetworkDiscoverer returns predetermined values, enabling deterministic testing of the resolver pool construction and mode dispatch logic without running real OS commands.
Platform-specific integration tests (build-tagged) can test the actual os/exec parsing against the real system — these run only on CI machines matching the target OS.
Error Handling
- Each
os/execcall wraps errors with context:fmt.Errorf("discovering default route: %w", err). - If the default-route command succeeds but returns no default route, return
NetworkInfo{}with empty fields and anilerror — this is a valid state (FR-009). - If
os/execfails (command not found, permission denied), returnNetworkInfo{}with an error. The caller (resolver pool construction) logs the error in verbose mode and proceeds with only the bootstrap resolver set.
R-006: Summary of Recommended Commands
Quick Reference
| Platform | Default Route + Gateway | DNS Servers | Commands |
|---|---|---|---|
| Windows | netsh interface ipv4 show route → 0.0.0.0/0 row (Idx + Gateway) |
netsh interface ipv4 show interfaces (Idx→Name) then netsh interface ipv4 show dnsservers name="<Name>" (IPs) |
3 commands |
| Linux | ip route show default → via <gw> dev <iface> |
/etc/resolv.conf (nameserver lines); if stub (127.0.0.53), resolvectl status <iface> |
1 command + 1 file read (+ 1 conditional command) |
| macOS | route -n get default → gateway: + interface: lines |
scutil --dns → nameserver lines in matching resolver block |
2 commands |
Confidence Assessment
| Platform | Approach Confidence | Key Risk |
|---|---|---|
| Windows | High — Verified on live system. netsh is the canonical non-PowerShell network CLI. Output format is stable across Windows versions. Numeric data is locale-independent. |
netsh deprecated in future Windows versions (no announced timeline). |
| Linux | High — ip route and /etc/resolv.conf are universal. resolvectl handles the systemd-resolved stub case. |
resolvectl not present on non-systemd distros; mitigated by stub-address fallback being functional. |
| macOS | High — route and scutil are stable system utilities. scutil --dns is the documented way to inspect DNS configuration. |
Apple could change scutil output format in a future macOS version (no precedent for this). |