Files

336 lines
11 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.
# ekDNSHelper
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) 2026 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 <ip>[:<port>]`: 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 <repo-url>
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-<os>-<arch>[.exe]`.
### Release packages
```powershell
# Build release binaries and create distributable archives
.\build.ps1 -Release -CrossCompile -Package
```
Archives are written to `releases\ekdns-<version>-<os>-<arch>.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`, `<ip>`, or `<ip>:<port>` |
| `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 <ip>[:<port>]`)
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 <internal-dns-ip>
```
### 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 <ip> 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.