# 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 ```go // 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 `:53`. - The `hostname` is 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 `error` with descriptive message. - On server unreachable/timeout: returns `error` with 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 ```go // 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 ```go // 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 `path` and 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 to `PrefixContent`. - 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 1. Creates backup: copies current hosts file to `/hosts.bak.-<4hex>`. 2. Assembles full file content: prefix + (managed block if non-empty) + postfix. 3. Compares assembled content with original — skips write if identical. 4. Creates temp file in same directory as hosts file: `.dns-helper-tmp-*`. 5. Writes content to temp file. 6. Sets temp file permissions to match original hosts file permissions. 7. Calls `Sync()` on temp file. 8. Closes temp file. 9. Calls `os.Rename(temp, hostsPath)`. 10. On rename success: deletes backup, returns nil. 11. 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 ```go // 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 ```go // 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: `/.dns-helper.lock`. - Created atomically via `os.OpenFile` with `O_CREATE|O_EXCL`. - PID written to lock file for debugging (not used for staleness). - Staleness: lock file with `ModTime` older 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).