Files
dnshelper/specs/001-safety-reliability-refactor/contracts/packages.md
T

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 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

// 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 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 <backupDir>/hosts.bak.<YYYYMMDD>-<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

// 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.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).