Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# dns-helper
|
||||
|
||||
A cross-platform CLI tool that resolves hostnames against a specified DNS server 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.
|
||||
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, dns-helper 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.
|
||||
|
||||
@@ -8,10 +10,17 @@ Built as a single static binary with no runtime dependencies.
|
||||
|
||||
## Features
|
||||
|
||||
- Resolve one or more hostnames via a specified DNS server and add them to the system hosts file
|
||||
- Remove individual hostnames or all managed entries from the hosts file
|
||||
- Managed block markers ensure only dns-helper's entries are modified — the rest of the hosts file is never touched
|
||||
- Automatic backup of the hosts file before every write (stored alongside the executable)
|
||||
- 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 dns-helper'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
|
||||
|
||||
@@ -53,22 +62,38 @@ Archives are written to `releases\dns-helper-<version>-<os>-<arch>.zip` (Windows
|
||||
|
||||
### Add entries
|
||||
|
||||
Resolve hostnames via a DNS server and add the resulting IP mappings to the hosts file:
|
||||
Resolve hostnames using smart default resolution and add to hosts file:
|
||||
|
||||
```sh
|
||||
dns-helper add -host hostname.example.com -server dns.example.com
|
||||
dns-helper add -host www.example.com
|
||||
```
|
||||
|
||||
Add multiple hostnames at once (comma-separated):
|
||||
Specify a resolution mode:
|
||||
|
||||
```sh
|
||||
dns-helper add -host "host1.example.com,host2.example.com" -server dns.example.com
|
||||
# Use only local DNS resolvers (no public fallback)
|
||||
dns-helper add -host internal.corp -server local
|
||||
|
||||
# Use the default gateway as DNS
|
||||
dns-helper add -host www.example.com -server gateway
|
||||
|
||||
# Use a specific DNS server
|
||||
dns-helper add -host www.example.com -server 8.8.8.8
|
||||
|
||||
# Use a specific DNS server on a non-standard port
|
||||
dns-helper add -host www.example.com -server 10.0.0.53:5353
|
||||
```
|
||||
|
||||
Short alias:
|
||||
Add multiple hostnames at once:
|
||||
|
||||
```sh
|
||||
dns-helper a -host hostname.example.com -server dns.example.com
|
||||
dns-helper add -host "host1.example.com,host2.example.com"
|
||||
```
|
||||
|
||||
Use timeout and verbose options:
|
||||
|
||||
```sh
|
||||
dns-helper add -host www.example.com -timeout 5 -verbose
|
||||
```
|
||||
|
||||
### Delete entries
|
||||
@@ -91,16 +116,18 @@ Remove **all** managed entries:
|
||||
dns-helper delete all
|
||||
```
|
||||
|
||||
Short aliases: `d`, `del`
|
||||
Short aliases: `a` for `add`, `d` / `del` for `delete`
|
||||
|
||||
### Command reference
|
||||
|
||||
| Command | Flag | Required | Description |
|
||||
|---------|------|----------|-------------|
|
||||
| `add` | `-host` | Yes | Comma-separated hostnames to resolve and add |
|
||||
| `add` | `-server` | Yes | DNS resolver to query (e.g. `dns.example.com`) |
|
||||
| `delete` | `-host` | Yes (unless `all`) | Comma-separated hostnames to remove |
|
||||
| `delete all` | — | — | Removes every entry dns-helper has written |
|
||||
| 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 dns-helper has written |
|
||||
|
||||
### Exit codes
|
||||
|
||||
@@ -109,6 +136,94 @@ Short aliases: `d`, `del`
|
||||
| 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, dns-helper 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
|
||||
dns-helper 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
|
||||
dns-helper 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
|
||||
dns-helper 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
|
||||
dns-helper add -host www.example.com -server 8.8.8.8
|
||||
dns-helper 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:
|
||||
dns-helper 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
|
||||
|
||||
dns-helper wraps its entries between two marker lines in the hosts file:
|
||||
@@ -131,6 +246,16 @@ Before every write, a backup of the hosts file is saved to the directory contain
|
||||
| macOS | `/etc/hosts` |
|
||||
| Linux | `/etc/hosts` |
|
||||
|
||||
### Network discovery (per platform)
|
||||
|
||||
When using smart default, local, or gateway modes, dns-helper 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
|
||||
|
||||
```
|
||||
@@ -147,13 +272,35 @@ lockfile/
|
||||
lockfile.go File-based mutex to prevent concurrent modifications
|
||||
lockfile_test.go
|
||||
platform/
|
||||
platform.go Platform interface — GetHostsFilePath()
|
||||
platform_windows.go Windows implementation
|
||||
platform_darwin.go macOS implementation
|
||||
platform_linux.go Linux implementation
|
||||
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 DNS resolution via golang.org/x/net/dns/dnsmessage
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user