package main import ( "flag" "fmt" "os" "path/filepath" "strings" "time" "ekdns/hostfile" "ekdns/lockfile" "ekdns/platform" "ekdns/resolver" ) // version and copyrightYear are set at build time via -ldflags. var version = "dev" var copyrightYear = "2024" func main() { // Banner (always printed to stdout). fmt.Printf("ekDNSHelper %s\n", version) fmt.Printf("Copyright (c) %s Emberkom LLC\n", copyrightYear) fmt.Println("") if len(os.Args) < 2 { printUsage() os.Exit(1) } switch strings.ToLower(os.Args[1]) { case "a", "add": os.Exit(runAdd(os.Args[2:])) case "d", "del", "delete": os.Exit(runDelete(os.Args[2:])) default: printUsage() os.Exit(1) } } func runAdd(args []string) int { fs := flag.NewFlagSet("add", flag.ContinueOnError) hostFlag := fs.String("host", "", "Comma separated list of hostnames to add") serverFlag := fs.String("server", "", "Resolution mode: omit for smart default, local, gateway, or IP/IP:port") timeoutFlag := fs.Int("timeout", 3, "Per-query DNS timeout in seconds") verboseFlag := fs.Bool("verbose", false, "Emit per-stage resolution trace to stderr") if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } // Validation (FR-006). if *hostFlag == "" { fmt.Fprintf(os.Stderr, "Error: -host flag is required for the add command\n") return 1 } if *timeoutFlag <= 0 { fmt.Fprintf(os.Stderr, "Error: -timeout must be a positive integer\n") return 1 } mode, err := resolver.ParseServerFlag(*serverFlag) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } config := resolver.QueryConfig{ Timeout: time.Duration(*timeoutFlag) * time.Second, Verbose: *verboseFlag, } discoverer := platform.NewNetworkDiscoverer() hostnames := strings.Split(*hostFlag, ",") // Step 1: DNS resolution before lock acquisition (FR-016). var entries []hostfile.DNSEntry var warnings []string for _, h := range hostnames { h = strings.TrimSpace(h) ips, err := resolver.Resolve(h, mode, config, discoverer) if err != nil { warnings = append(warnings, fmt.Sprintf("Warning: failed to resolve %s: %v", h, err)) continue } for _, ip := range ips { entries = append(entries, hostfile.DNSEntry{IP: ip, Hostname: h}) } } if len(entries) == 0 && len(warnings) > 0 { for _, w := range warnings { fmt.Fprintln(os.Stderr, w) } return 1 } // Step 2: Get hosts file path. hostsPath, err := platform.GetHostsFilePath() if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } // Step 3: Get executable directory for lock and backup storage. exePath, err := os.Executable() if err != nil { fmt.Fprintf(os.Stderr, "Error: determining executable path: %v\n", err) return 1 } exeDir := filepath.Dir(exePath) // Step 4: Acquire lock (FR-016). lock, err := lockfile.Acquire(exeDir) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } defer lock.Release() // Step 5: Read, modify, write. mgr := hostfile.NewManager(hostfile.OSFileSystem{}) hf, err := mgr.Read(hostsPath) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } hf.ManagedContent = hostfile.AddEntries(hf.ManagedContent, entries) if err := mgr.Write(hf, exeDir); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } // Step 6: Output summary (FR-013). entryCounts := make(map[string]int) for _, e := range entries { entryCounts[e.Hostname]++ } for _, h := range hostnames { h = strings.TrimSpace(h) if count, ok := entryCounts[h]; ok { fmt.Printf("Added %d entries for %s\n", count, h) } } for _, w := range warnings { fmt.Fprintln(os.Stderr, w) } if len(warnings) > 0 { return 1 // Partial failure (FR-015). } return 0 } func runDelete(args []string) int { // Check for "all" keyword first — positional argument, not a flag. if len(args) > 0 && (strings.ToLower(args[0]) == "all" || strings.ToLower(args[0]) == "a") { return runDeleteAll() } fs := flag.NewFlagSet("delete", flag.ContinueOnError) hostFlag := fs.String("host", "", "Comma separated list of hostnames to delete") if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } if *hostFlag == "" { fmt.Fprintf(os.Stderr, "Error: -host flag is required for the delete command\n") return 1 } hostnames := strings.Split(*hostFlag, ",") for i, h := range hostnames { hostnames[i] = strings.TrimSpace(h) } // Step 1: Get hosts file path. hostsPath, err := platform.GetHostsFilePath() if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } // Step 2: Get executable directory. exePath, err := os.Executable() if err != nil { fmt.Fprintf(os.Stderr, "Error: determining executable path: %v\n", err) return 1 } exeDir := filepath.Dir(exePath) // Step 3: Acquire lock. lock, err := lockfile.Acquire(exeDir) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } defer lock.Release() // Step 4: Read and parse hosts file. mgr := hostfile.NewManager(hostfile.OSFileSystem{}) hf, err := mgr.Read(hostsPath) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } // Step 5: Remove entries by hostname, counting originals for summary. // Count original managed lines per hostname before removal. originalCounts := make(map[string]int) for _, h := range hostnames { for _, line := range hf.ManagedContent { if strings.Contains(line, h) { originalCounts[h]++ } } } hf.ManagedContent = hostfile.RemoveByHostname(hf.ManagedContent, hostnames) // Step 6: Write updated file. if err := mgr.Write(hf, exeDir); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } // Step 7: Print per-hostname summary. for _, h := range hostnames { if count, ok := originalCounts[h]; ok && count > 0 { fmt.Printf("Removed %d entries for %s\n", count, h) } else { fmt.Printf("No managed entries found for %s\n", h) } } return 0 } func runDeleteAll() int { // Step 1: Get hosts file path. hostsPath, err := platform.GetHostsFilePath() if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } // Step 2: Get executable directory. exePath, err := os.Executable() if err != nil { fmt.Fprintf(os.Stderr, "Error: determining executable path: %v\n", err) return 1 } exeDir := filepath.Dir(exePath) // Step 3: Acquire lock. lock, err := lockfile.Acquire(exeDir) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } defer lock.Release() // Step 4: Read and parse hosts file. mgr := hostfile.NewManager(hostfile.OSFileSystem{}) hf, err := mgr.Read(hostsPath) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } // Step 5: Remove all managed entries. entryCount := len(hf.ManagedContent) hf.ManagedContent = hostfile.RemoveAll() // Step 6: Write updated file. if err := mgr.Write(hf, exeDir); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return 1 } // Step 7: Print summary. if entryCount > 0 { fmt.Printf("Removed all managed entries (%d entries removed)\n", entryCount) } else { fmt.Println("No managed entries found") } return 0 } func printUsage() { fmt.Println("Updates the local hosts file with DNS entries resolved by this tool.") fmt.Println("") fmt.Println("Usage:") fmt.Println(" ekdns add -host [-server ] [-timeout ] [-verbose]") fmt.Println(" ekdns delete -host ") fmt.Println(" ekdns delete all") fmt.Println("") fmt.Println("Flags (add):") fmt.Println(" -host Comma-separated list of hostnames to resolve and add (required)") fmt.Println(" -server Resolution mode (optional):") fmt.Println(" (omitted) Smart default: NS fan-out → authoritative query → parallel A fallback") fmt.Println(" local Query locally configured DNS resolvers only (no public fallback)") fmt.Println(" gateway Query default gateway as DNS server (no fallback)") fmt.Println(" Query this IP on port 53") fmt.Println(" : Query this IP on the given port") fmt.Println(" -timeout Per-query DNS timeout in seconds (default: 3)") fmt.Println(" -verbose Emit per-stage resolution trace to stderr") fmt.Println("") fmt.Println("Examples:") fmt.Println(" ekdns add -host www.example.com") fmt.Println(" Resolve using smart default (NS fan-out + authoritative query).") fmt.Println(" ekdns add -host internal.client.local -server local") fmt.Println(" Resolve using only locally configured DNS resolvers.") fmt.Println(" ekdns add -host www.example.com -server gateway") fmt.Println(" Resolve via the default gateway IP.") fmt.Println(" ekdns add -host www.example.com -server 8.8.8.8") fmt.Println(" Resolve via a specific DNS server.") fmt.Println(" ekdns add -host www.example.com -server 8.8.8.8:5353 -timeout 5 -verbose") fmt.Println(" Resolve via 8.8.8.8:5353 with 5s timeout and verbose trace.") fmt.Println(" ekdns delete -host www.example.com") fmt.Println(" Remove all managed entries for www.example.com from the hosts file.") fmt.Println(" ekdns delete all") fmt.Println(" Remove all managed entries from the hosts file.") fmt.Println("") fmt.Println("Notes:") fmt.Println(" Adding a hostname first removes all existing managed entries for that hostname.") fmt.Println(" Only entries added by this tool are ever removed.") }