333 lines
9.3 KiB
Go
333 lines
9.3 KiB
Go
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 <hostnames> [-server <mode>] [-timeout <seconds>] [-verbose]")
|
|
fmt.Println(" ekdns delete -host <hostnames>")
|
|
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(" <ip> Query this IP on port 53")
|
|
fmt.Println(" <ip>:<port> 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.")
|
|
}
|