6.0 KiB
6.0 KiB
Package Contracts: dns-helper
Feature Branch: 001-safety-reliability-refactor
Date: 2026-03-03
Package: resolver
Handles DNS name resolution against a user-specified DNS server.
Interface
// Resolver performs DNS lookups against a specific server.
type Resolver interface {
// LookupIP resolves a hostname to a list of IPv4 addresses using the
// specified DNS server. Follows CNAME chains up to a maximum depth.
// Returns deduplicated IPs. Returns an error if the hostname cannot
// be resolved (NXDOMAIN, timeout, server unreachable, etc.).
LookupIP(hostname, server string) ([]string, error)
}
Behavior Contract
- Queries are sent over UDP to
<server>:53. - The
hostnameis automatically converted to FQDN (trailing dot appended if missing). - An A query is sent first. If A records are returned, they are collected.
- If only CNAME records are returned (no A records in the answer), the CNAME target is resolved recursively.
- CNAME chain depth is limited to 10 levels. Exceeding this returns an error.
- Results are deduplicated before return.
- Timeout: 5 seconds per query.
- On NXDOMAIN: returns
errorwith descriptive message. - On server unreachable/timeout: returns
errorwith descriptive message. - Response ID must match query ID; mismatches are treated as errors.
- Truncated responses (TC flag set) are treated as errors (no TCP fallback for simplicity).
Package: hostfile
Manages reading, parsing, modifying, and atomically writing the hosts file.
Interface
// Manager handles hosts file operations.
type Manager interface {
// Read reads and parses the hosts file at the given path.
// Returns an error if the file cannot be read or the managed block is corrupt.
Read(path string) (*HostsFile, error)
// Write atomically writes the hosts file content to the given path.
// Creates a backup before writing. Uses temp-file-and-rename pattern.
// backupDir is the directory for the backup file (typically the exe directory).
Write(hf *HostsFile, backupDir string) error
}
// HostsFile represents the parsed content of a hosts file.
type HostsFile struct {
Path string
OriginalContent []string
PrefixContent []string
ManagedContent []string
PostfixContent []string
HasManagedBlock bool
}
Sub-package: Managed Block Operations
// AddEntries adds DNS entries to the managed content, removing any
// existing entries for the same hostnames first. Returns updated content.
func AddEntries(existing []string, entries []DNSEntry) []string
// RemoveByHostname removes all managed entries matching any of the
// specified hostnames. Returns updated content.
func RemoveByHostname(existing []string, hostnames []string) []string
// RemoveAll returns an empty slice (clears all managed entries).
func RemoveAll() []string
Behavior Contract — Read
- Opens the file at
pathand reads all lines. - Scans for start marker (
# DNSHelper <<-> START CONFIG) and end marker (# DNSHelper <->> END CONFIG). - If neither marker found:
HasManagedBlock = false, all content goes toPrefixContent. - If both markers found in correct order: splits content into Prefix, Managed, Postfix.
- Corruption detection (returns error):
- Start marker without end marker.
- End marker without start marker.
- End marker before start marker.
- Duplicate start or end markers.
- Uses sentinel values (not zero-index) for marker detection.
Behavior Contract — Write
- Creates backup: copies current hosts file to
<backupDir>/hosts.bak.<YYYYMMDD>-<4hex>. - Assembles full file content: prefix + (managed block if non-empty) + postfix.
- Compares assembled content with original — skips write if identical.
- Creates temp file in same directory as hosts file:
.dns-helper-tmp-*. - Writes content to temp file.
- Sets temp file permissions to match original hosts file permissions.
- Calls
Sync()on temp file. - Closes temp file.
- Calls
os.Rename(temp, hostsPath). - On rename success: deletes backup, returns nil.
- On rename failure (including Windows retry 2-3x with 200ms delay): cleans up temp file, returns error. Backup is cleaned up after confirming original is intact.
Package: platform
Provides platform-specific hosts file location.
Interface
// GetHostsFilePath returns the absolute path to the system hosts file.
// Returns an error if the file does not exist or cannot be accessed.
func GetHostsFilePath() (string, error)
Platform Implementations
| Platform | Path | Detection |
|---|---|---|
| Windows | %SystemRoot%\System32\drivers\etc\hosts (fallback: C:\Windows\...) |
os.LookupEnv("SystemRoot") |
| Linux | /etc/hosts |
Hardcoded |
| macOS | /etc/hosts |
Hardcoded |
Behavior Contract
- Returns error to caller on file-not-found or access error.
- Must NOT call
os.Exit(),log.Fatal(), or any process-terminating function. - Error messages include the path that was checked.
Package: lockfile
Serializes concurrent access to the hosts file.
Interface
// Lock represents an acquired lock.
type Lock interface {
// Release deletes the lock file. Safe to call multiple times.
Release() error
}
// Acquire attempts to acquire a lock file in the specified directory.
// Retries for up to maxWait on contention. Breaks stale locks older
// than staleTimeout. Returns an error if the lock cannot be acquired.
func Acquire(dir string) (Lock, error)
Behavior Contract
- Lock file path:
<dir>/.dns-helper.lock. - Created atomically via
os.OpenFilewithO_CREATE|O_EXCL. - PID written to lock file for debugging (not used for staleness).
- Staleness: lock file with
ModTimeolder than 2 minutes is considered stale and removed. - Retry: up to 2 seconds total, 200ms between attempts.
- Release:
os.Remove(lockPath). Idempotent (no error if already deleted). - Lock acquired after DNS resolution, released after file write (or on error).