# ekekDNSHelper A cross-platform CLI tool that resolves hostnames against DNS servers and writes the resulting IP-to-hostname mappings into the local hosts file. It manages a clearly delimited block within the hosts file so it can cleanly add and remove only its own entries. By default, ekdns performs **smart resolution**: it discovers the authoritative nameserver for each hostname through a parallel NS fan-out across local and public resolvers, then queries that NS directly for the freshest answer. Internal hostnames (without zone delegation) fall back to a parallel A-record query across all resolvers. Built as a single static binary with no runtime dependencies. **Copyright (c) 2024 Emberkom LLC** ## Features - Smart default resolution: parallel NS fan-out → authoritative query → parallel A fallback (no `-server` flag needed for most use cases) - `-server local`: query only locally configured DNS resolvers (for internal hostnames) - `-server gateway`: query the default gateway as a DNS server - `-server [:]`: query a specific DNS server (existing explicit mode) - Split-horizon conflict detection: warns when the authoritative answer differs from what local resolvers say - CNAME chain following (up to 10 hops) with loop detection - Context-aware error messages with private TLD hints (`.local`, `.corp`, `.internal`, etc.) - Per-query timeout control via `-timeout` - Verbose per-stage resolution trace via `-verbose` - Managed block markers ensure only ekdns's entries are ever modified - Automatic backup of the hosts file before every write - File-based lock prevents concurrent modifications - Cross-platform: Windows, macOS, and Linux ## Requirements - **Go 1.20** toolchain (this project must not use Go 1.21+ features) - Administrator / root privileges (required to modify the system hosts file) ## Installation ### Build from source ```powershell # Clone the repository git clone cd ekdns # Build for the current platform .\build.ps1 # Or build optimized release binaries for all platforms .\build.ps1 -Release -CrossCompile ``` The default build output is `build\ekdns.exe` (Windows). Cross-compiled binaries are named `build\ekdns--[.exe]`. ### Release packages ```powershell # Build release binaries and create distributable archives .\build.ps1 -Release -CrossCompile -Package ``` Archives are written to `releases\ekdns---.zip` (Windows) or `.tar.gz` (macOS/Linux). ## Usage > **Note:** ekdns must be run with elevated privileges (Run as Administrator on Windows, `sudo` on macOS/Linux) because it modifies the system hosts file. ### Add entries Resolve hostnames using smart default resolution and add to hosts file: ```sh ekdns add -host www.example.com ``` Specify a resolution mode: ```sh # Use only local DNS resolvers (no public fallback) ekdns add -host internal.corp -server local # Use the default gateway as DNS ekdns add -host www.example.com -server gateway # Use a specific DNS server ekdns add -host www.example.com -server 8.8.8.8 # Use a specific DNS server on a non-standard port ekdns add -host www.example.com -server 10.0.0.53:5353 ``` Add multiple hostnames at once: ```sh ekdns add -host "host1.example.com,host2.example.com" ``` Use timeout and verbose options: ```sh ekdns add -host www.example.com -timeout 5 -verbose ``` ### Delete entries Remove a specific hostname: ```sh ekdns delete -host hostname.example.com ``` Remove multiple hostnames: ```sh ekdns delete -host "host1.example.com,host2.example.com" ``` Remove **all** managed entries: ```sh ekdns delete all ``` Short aliases: `a` for `add`, `d` / `del` for `delete` ### Command reference | Command | Flag | Required | Default | Description | |---------|------|----------|---------|-------------| | `add` | `-host` | Yes | — | Comma-separated hostnames to resolve and add | | `add` | `-server` | No | smart default | Resolution mode: omit, `local`, `gateway`, ``, or `:` | | `add` | `-timeout` | No | 3 | Per-query DNS timeout in seconds | | `add` | `-verbose` | No | false | Emit per-stage resolution trace to stderr | | `delete` | `-host` | Yes (unless `all`) | — | Comma-separated hostnames to remove | | `delete all` | — | — | — | Removes every entry ekdns has written | ### Exit codes | Code | Meaning | |------|---------| | 0 | Success | | 1 | Error or partial failure (e.g. one hostname failed to resolve) | ## Resolution Modes ### Smart Default (no `-server` flag) When `-server` is omitted, ekdns performs multi-stage authoritative resolution: 1. **Discover** local DNS resolvers and default gateway from OS network configuration. 2. **Build resolver pool**: local resolvers + hardcoded bootstrap set (1.1.1.1, 8.8.8.8, etc.), deduplicated. 3. **Stage 1 — NS Fan-out**: Query NS records for every label level of the hostname across all resolvers simultaneously (e.g., `www.example.com` and `example.com` × all resolvers in parallel). 4. **Select authority**: The most-specific zone with NS records wins. 5. **Stage 2 — Authoritative Query**: Resolve the NS hostname to an IP, then send a non-recursive A query directly to that nameserver. 6. **Stage 2.5 — Split-Horizon Check**: Concurrently with Stage 2, query local resolvers for the same hostname. If local and authoritative answers differ, report a conflict error — neither IP is written. 7. **CNAME Following**: If Stage 2 returns a CNAME, restart from Stage 1 for the target (max 10 hops). 8. **Stage 3 — Parallel A Fallback**: If no NS records were found at any level (internal hosts without zone delegation), send A queries to all resolvers simultaneously and take the first successful response. ```sh # Example: smart default resolution with verbose trace ekdns add -host www.example.com -verbose ``` Verbose output: ``` [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., ns2.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 ``` ### Local Mode (`-server local`) Queries only the locally configured DNS resolvers (from OS network configuration) in priority order. No fallback to public resolvers. ```sh ekdns add -host internal.client.local -server local ``` Fails with an error if all local resolvers are unreachable or no local resolvers are found. ### Gateway Mode (`-server gateway`) Discovers the default gateway IP and uses it as the DNS server. No fallback. ```sh ekdns add -host www.example.com -server gateway ``` ### Explicit Server (`-server [:]`) Queries the specified DNS server directly. Port defaults to 53 if omitted. ```sh ekdns add -host www.example.com -server 8.8.8.8 ekdns add -host www.example.com -server 10.0.0.53:5353 ``` ## Error Messages ### Private TLD hint When a hostname with a private TLD (`.local`, `.corp`, `.internal`, `.lan`, `.home`, `.private`) cannot be resolved: ``` Error: failed to resolve myservice.client.local: ... Hint: the hostname uses a private TLD (.local). Try specifying an internal DNS server: ekdns add -host myservice.client.local -server ``` ### Split-horizon conflict When the authoritative answer differs from the local resolver's answer: ``` 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 to choose explicitly. ``` ### CNAME loop ``` Error: CNAME chain depth exceeded for www.example.com (max 10 hops): probable CNAME loop or misconfigured zone ``` ## How it works ekdns wraps its entries between two marker lines in the hosts file: ``` # ekDNSHelper <<-> START CONFIG 192.0.2.1 hostname.example.com # ekDNSHelper <->> END CONFIG ``` Only lines within this managed block are ever modified or removed. The rest of the hosts file is preserved exactly as-is. Before every write, a backup of the hosts file is saved to the directory containing the ekdns executable. ### Hosts file locations | Platform | Path | |----------|------| | Windows | `%SystemRoot%\System32\drivers\etc\hosts` | | macOS | `/etc/hosts` | | Linux | `/etc/hosts` | ### Network discovery (per platform) When using smart default, local, or gateway modes, ekdns discovers your network configuration via OS utilities: | Platform | Method | |----------|--------| | Windows | `netsh interface ipv4 show route` + `netsh interface ipv4 show dnsservers` | | Linux | `ip route show default` + `/etc/resolv.conf` (with `resolvectl` fallback for systemd-resolved) | | macOS | `route -n get default` + `scutil --dns` | ## Project structure ``` main.go CLI entry point (add / delete commands, flag parsing) build.ps1 Build & packaging script go.mod Module definition (Go 1.20, golang.org/x/net) hostfile/ hostfile.go Hosts file parsing, DNSEntry type, add/remove helpers managed.go Managed block read/write logic filesystem.go FileSystem interface + OSFileSystem implementation hostfile_test.go managed_test.go lockfile/ lockfile.go File-based mutex to prevent concurrent modifications lockfile_test.go platform/ platform.go Package doc — GetHostsFilePath() platform_windows.go Windows: GetHostsFilePath() platform_darwin.go macOS: GetHostsFilePath() platform_linux.go Linux: GetHostsFilePath() network.go NetworkInfo type, NetworkDiscoverer interface, FakeNetworkDiscoverer network_windows.go Windows: WindowsNetworkDiscoverer (netsh) network_darwin.go macOS: DarwinNetworkDiscoverer (route + scutil) network_linux.go Linux: LinuxNetworkDiscoverer (ip route + resolv.conf) discoverer_windows.go Windows: NewNetworkDiscoverer() constructor discoverer_darwin.go macOS: NewNetworkDiscoverer() constructor discoverer_linux.go Linux: NewNetworkDiscoverer() constructor platform_test.go network_test.go resolver/ resolver.go Existing single-server A-record lookup (used internally) parse.go ServerMode, QueryConfig, ParseServerFlag(), ExtractLabelLevels() parse_test.go transport.go Shared UDP query transport (UDPQuery) transport_test.go pool.go BuildResolverPool() — constructs resolver list per mode pool_test.go authority.go ParallelNSFanOut(), SelectAuthoritativeNS(), QueryAuthoritative() authority_test.go fallback.go ParallelAFallback() — Stage 3 parallel A fallback fallback_test.go splithorizon.go CheckSplitHorizon() — Stage 2.5 cross-check splithorizon_test.go modes.go Resolve() dispatcher and mode implementations modes_test.go resolver_test.go ``` ## Development ### Running tests ```sh go1.20 test ./... ``` ### Running tests with coverage ```sh go1.20 test -cover ./... ``` ### Vetting code ```sh go1.20 vet ./... ``` ## Dependencies - [`golang.org/x/net`](https://pkg.go.dev/golang.org/x/net) — DNS message parsing (`golang.org/x/net/dns/dnsmessage`) No other third-party dependencies. Standard library first. ## License Copyright (c) 2024 Emberkom LLC. All rights reserved.