commit e81de212db6614b74865a8c2219be2844fb4b9dc Author: ek-dyoder Date: Tue Mar 3 13:58:06 2026 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c97a15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Build outputs +bin/ + +# IDE files +.idea/ \ No newline at end of file diff --git a/dataprep.go b/dataprep.go new file mode 100644 index 0000000..d543ad8 --- /dev/null +++ b/dataprep.go @@ -0,0 +1,133 @@ +package main + +import ( + "errors" + "fmt" + "strings" +) + +var startPattern = "DNSHelper <<-> START CONFIG" +var endPattern = "DNSHelper <->> END CONFIG" + +func extractLinesToEdit(data *workingData) error { + startIndex := 0 + endIndex := 0 + for i, line := range data.DefaultContent { + if strings.Contains(line, startPattern) { + startIndex = i + continue + } + if strings.Contains(line, endPattern) { + endIndex = i + break + } + } + var fileModified bool + var fileCorrupted bool + switch { + case startIndex == 0 && endIndex == 0: + fileModified = false + fileCorrupted = false + case startIndex > 0 && endIndex > startIndex: + fileModified = true + fileCorrupted = false + default: + fileModified = true + fileCorrupted = true + } + + if fileCorrupted == true { + return errors.New("content has been corrupted") + } + + if fileModified == true { + data.PrefixContent = data.DefaultContent[:startIndex] + data.PostfixContent = data.DefaultContent[endIndex+1:] + data.ExistingContent = data.DefaultContent[startIndex+1 : endIndex] + } else { + data.PrefixContent = data.DefaultContent + data.PostfixContent = make([]string, 0) + data.ExistingContent = make([]string, 0) + } + data.NewContent = make([]string, 0) + //fmt.Println("PrefixContent:") + //for _, line := range data.PrefixContent { + // fmt.Println(line) + //} + //fmt.Println("ExistingContent:") + //for _, line := range data.ExistingContent { + // fmt.Println(line) + //} + //fmt.Println("PostfixContent:") + //for _, line := range data.PostfixContent { + // fmt.Println(line) + //} + return nil +} + +func removeHostsFromExistingContent(data *workingData) { + var updatedContent []string + for _, host := range data.Hosts { + for _, line := range data.ExistingContent { + if !strings.Contains(line, host) { + updatedContent = append(updatedContent, line) + } + } + } + data.NewContent = updatedContent +} + +func addHostsToWorkingData(data *workingData, hosts *string) { + hostToAddList := strings.Split(*hosts, ",") + var newHosts []string + for _, host := range hostToAddList { + newHosts = append(newHosts, host) + } + data.Hosts = newHosts +} + +func addResolversToWorkingData(data *workingData, resolvers *string) { + resolverToAddList := strings.Split(*resolvers, ",") + var newResolvers []string + for _, resolver := range resolverToAddList { + newResolvers = append(newResolvers, resolver) + } + data.Resolvers = newResolvers +} + +func addNameToIPMappingsToNewContent(data *workingData) { + for _, host := range data.Hosts { + for _, resolver := range data.Resolvers { + ips := lookupIP(host, resolver) + if len(ips) > 0 { + for _, ip := range ips { + data.NewContent = append(data.NewContent, fmt.Sprintf("%s\t%s", ip, host)) + } + } + } + } +} + +func slicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func dedupeSlice(slice []string) []string { + newSlice := make([]string, 0, len(slice)) + seen := make(map[string]bool) + for _, value := range slice { + if !seen[value] { + seen[value] = true + newSlice = append(newSlice, value) + } + } + return newSlice +} diff --git a/dns.go b/dns.go new file mode 100644 index 0000000..b07f1f3 --- /dev/null +++ b/dns.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "github.com/miekg/dns" +) + +func lookupIP(hostname string, resolver string) []string { + c := dns.Client{} + m := dns.Msg{} + m.SetQuestion(dns.Fqdn(hostname), dns.TypeA) + r, _, err := c.Exchange(&m, resolver+":53") + if err != nil { + return nil + } + if r.Rcode != dns.RcodeSuccess { + fmt.Println("Error: querying " + resolver + " for A record of " + hostname + " failed with code " + dns.RcodeToString[r.Rcode]) + return nil + } + ips := make([]string, 0) + for _, ans := range r.Answer { + if rec, ok := ans.(*dns.A); ok { + ips = append(ips, rec.A.String()) + } + } + + if len(ips) > 0 { + return ips + } + + c = dns.Client{} + m = dns.Msg{} + m.SetQuestion(dns.Fqdn(hostname), dns.TypeCNAME) + r, _, err = c.Exchange(&m, resolver+":53") + if err != nil { + return nil + } + if r.Rcode != dns.RcodeSuccess { + fmt.Println("Error: querying " + resolver + " for CNAME record of " + hostname + " failed with code " + dns.RcodeToString[r.Rcode]) + return nil + } + for _, ans := range r.Answer { + if rec, ok := ans.(*dns.CNAME); ok { + ips = lookupIP(rec.Target, resolver) + for _, ip := range ips { + ips = append(ips, ip) + } + } + } + + if len(ips) > 0 { + return ips + } + + fmt.Println("Error: no IP addresses found for " + hostname) + return nil +} diff --git a/fileoperations.go b/fileoperations.go new file mode 100644 index 0000000..2ba8a95 --- /dev/null +++ b/fileoperations.go @@ -0,0 +1,73 @@ +package main + +import ( + "bufio" + "os" +) + +func fileExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +func getFileContentLines(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return nil, err + } + return lines, nil +} + +func writeOverContentInFile(path string, lines []string) error { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer file.Close() + writer := bufio.NewWriter(file) + for _, line := range lines { + _, err := writer.WriteString(line + "\n") + if err != nil { + return err + } + } + err = writer.Flush() + if err != nil { + return err + } + return nil +} + +func writeHostsFile(data *workingData) error { + var newFileContent []string + newFileContent = append(newFileContent, data.PrefixContent...) + if len(data.NewContent) > 0 { + data.NewContent = dedupeSlice(data.NewContent) + newFileContent = append(newFileContent, "# "+startPattern) + newFileContent = append(newFileContent, data.NewContent...) + newFileContent = append(newFileContent, "# "+endPattern) + } + newFileContent = append(newFileContent, data.PostfixContent...) + if slicesEqual(newFileContent, data.DefaultContent) == false { + err := writeOverContentInFile(data.HostsFileLocation, newFileContent) + if err != nil { + return err + } + } + return nil +} diff --git a/fileoperations_darwin.go b/fileoperations_darwin.go new file mode 100644 index 0000000..5922e75 --- /dev/null +++ b/fileoperations_darwin.go @@ -0,0 +1,18 @@ +//go:build darwin + +package main + +import "errors" + +func getHostsFileLocation() (string, error) { + var hostsFileLocation string + hostsFileLocation = "/etc/hosts" + exists, err := fileExists(hostsFileLocation) + if err != nil { + term(err) + } + if exists == false { + term(errors.New("hosts file does not exist at " + hostsFileLocation)) + } + return hostsFileLocation, nil +} diff --git a/fileoperations_linux.go b/fileoperations_linux.go new file mode 100644 index 0000000..c85b8d4 --- /dev/null +++ b/fileoperations_linux.go @@ -0,0 +1,18 @@ +//go:build linux + +package main + +import "errors" + +func getHostsFileLocation() (string, error) { + var hostsFileLocation string + hostsFileLocation = "/etc/hosts" + exists, err := fileExists(hostsFileLocation) + if err != nil { + term(err) + } + if exists == false { + term(errors.New("hosts file does not exist at " + hostsFileLocation)) + } + return hostsFileLocation, nil +} diff --git a/fileoperations_windows.go b/fileoperations_windows.go new file mode 100644 index 0000000..96dd429 --- /dev/null +++ b/fileoperations_windows.go @@ -0,0 +1,27 @@ +//go:build windows + +package main + +import ( + "errors" + "os" +) + +func getHostsFileLocation() (string, error) { + //return "hosts", nil + var hostsFileLocation string + value, envExists := os.LookupEnv("SystemRoot") + if envExists { + hostsFileLocation = value + "\\System32\\drivers\\etc\\hosts" + } else { + hostsFileLocation = "C:\\Windows\\System32\\drivers\\etc\\hosts" + } + exists, err := fileExists(hostsFileLocation) + if err != nil { + return "", err + } + if exists == false { + return "", errors.New("file does not exist: " + hostsFileLocation) + } + return hostsFileLocation, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fdb2df1 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module dns-helper + +go 1.18 + +require ( + github.com/miekg/dns v1.1.59 // indirect + golang.org/x/mod v0.16.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/tools v0.19.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..31f195d --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= +github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= diff --git a/main.go b/main.go new file mode 100644 index 0000000..440f448 --- /dev/null +++ b/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" +) + +var fileName = filepath.Base(os.Args[0]) + +func init() { + fmt.Println("DNSHelper v1.0") + fmt.Println("Copyright (c) 2024 Emberkom LLC") + fmt.Println("") + if len(os.Args) < 2 { + printUsage() + defer os.Exit(1) + } +} + +func printUsage() { + fmt.Println("This utility will update the local hosts file with DNS entries obtained by the specified DNS server.") + fmt.Println("Usage: " + fileName + " [add|delete] -host hostname [-server dns.example.com]") + fmt.Println("Example:") + fmt.Println(" " + fileName + " add -host xyz.acme.com -server dns.example.com") + fmt.Println(" This will use dns.example.com to find and add all IP addresses for xyz.acme.com to the local hosts file.") + fmt.Println(" " + fileName + " delete -host hostname.example.com") + fmt.Println(" This will delete all entries for hostname.example.com from the local hosts file.") + fmt.Println(" " + fileName + " delete all") + fmt.Println(" This will delete all entries from the local hosts file that were added by this utility.") + fmt.Println("Note: Adding a hostname will first remove all entries in the hosts file that match the same hostname.") + fmt.Println(" This utility will only remove entries from the hosts file that it added.") +} + +func term(err error) { + fmt.Println("Error: " + err.Error()) + defer os.Exit(1) +} + +func main() { + modePing := flag.NewFlagSet("ping", flag.ExitOnError) + modeAdd := flag.NewFlagSet("add", flag.ExitOnError) + hostToAdd := modeAdd.String("host", "", "Comma separated list of hostnames to add") + dnsResolver := modeAdd.String("server", "", "DNS resolver to use") + modeDelete := flag.NewFlagSet("delete", flag.ExitOnError) + hostToDelete := modeDelete.String("host", "", "Comma separated list of hostnames to delete") + + data := new(workingData) + + var err error + data.HostsFileLocation, err = getHostsFileLocation() + if err != nil { + term(err) + } + + data.DefaultContent, err = getFileContentLines(data.HostsFileLocation) + if err != nil { + term(err) + } + + if err = extractLinesToEdit(data); err != nil { + term(err) + } + + switch strings.ToLower(os.Args[1]) { + case "a", "add": + err = modeAdd.Parse(os.Args[2:]) + if err != nil { + term(err) + } + addHostsToWorkingData(data, hostToAdd) + addResolversToWorkingData(data, dnsResolver) + removeHostsFromExistingContent(data) + addNameToIPMappingsToNewContent(data) + case "d", "del", "delete": + err = modeDelete.Parse(os.Args[2:]) + if err != nil { + term(err) + } + if strings.ToLower(*hostToDelete) != "all" && strings.ToLower(*hostToDelete) != "a" { + addHostsToWorkingData(data, hostToDelete) + removeHostsFromExistingContent(data) + } + default: + printUsage() + } + + err = writeHostsFile(data) + if err != nil { + term(err) + } +} diff --git a/structs.go b/structs.go new file mode 100644 index 0000000..ee9713d --- /dev/null +++ b/structs.go @@ -0,0 +1,13 @@ +package main + +type workingData struct { + HostsFileLocation string + IsNew bool + Hosts []string + Resolvers []string + DefaultContent []string + PrefixContent []string + NewContent []string + ExistingContent []string + PostfixContent []string +}