diff --git a/internal/clientinfo/hostsfile.go b/internal/clientinfo/hostsfile.go index 8c86987..cf7798e 100644 --- a/internal/clientinfo/hostsfile.go +++ b/internal/clientinfo/hostsfile.go @@ -1,8 +1,12 @@ package clientinfo import ( + "bufio" + "bytes" + "io" "net/netip" "os" + "strings" "sync" "github.com/fsnotify/fsnotify" @@ -12,9 +16,10 @@ import ( ) const ( - ipv4LocalhostName = "localhost" - ipv6LocalhostName = "ip6-localhost" - ipv6LoopbackName = "ip6-loopback" + ipv4LocalhostName = "localhost" + ipv6LocalhostName = "ip6-localhost" + ipv6LoopbackName = "ip6-loopback" + hostEntriesConfPath = "/var/unbound/host_entries.conf" ) // hostsFile provides client discovery functionality using system hosts file. @@ -34,14 +39,9 @@ func (hf *hostsFile) init() error { if err := hf.watcher.Add(hostsfile.HostsPath); err != nil { return err } - m, err := hostsfile.ParseHosts(hostsfile.ReadHostsFile()) - if err != nil { - return err - } - hf.mu.Lock() - hf.m = m - hf.mu.Unlock() - return nil + // Conservatively adding hostEntriesConfPath, since it is not available everywhere. + _ = hf.watcher.Add(hostEntriesConfPath) + return hf.refresh() } // refresh reloads hosts file entries. @@ -52,6 +52,14 @@ func (hf *hostsFile) refresh() error { } hf.mu.Lock() hf.m = m + // override hosts file with host_entries.conf content if present. + hem, err := parseHostEntriesConf(hostEntriesConfPath) + if err != nil && !os.IsNotExist(err) { + ctrld.ProxyLogger.Load().Debug().Err(err).Msg("could not read host_entries.conf file") + } + for k, v := range hem { + hf.m[k] = v + } hf.mu.Unlock() return nil } @@ -137,3 +145,46 @@ func isLocalhostName(hostname string) bool { return false } } + +// parseHostEntriesConf parses host_entries.conf file and returns parsed result. +func parseHostEntriesConf(path string) (map[string][]string, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return parseHostEntriesConfFromReader(bytes.NewReader(b)), nil +} + +// parseHostEntriesConfFromReader is like parseHostEntriesConf, but read from an io.Reader instead of file. +func parseHostEntriesConfFromReader(r io.Reader) map[string][]string { + hostsMap := map[string][]string{} + scanner := bufio.NewScanner(r) + + localZone := "" + for scanner.Scan() { + line := scanner.Text() + if after, found := strings.CutPrefix(line, "local-zone:"); found { + after = strings.TrimSpace(after) + fields := strings.Fields(after) + if len(fields) > 1 { + localZone = strings.Trim(fields[0], `""`) + } + continue + } + // Only read "local-data-ptr: ..." line, it has all necessary information. + after, found := strings.CutPrefix(line, "local-data-ptr:") + if !found { + continue + } + after = strings.TrimSpace(after) + after = strings.Trim(after, `"`) + fields := strings.Fields(after) + if len(fields) != 2 { + continue + } + ip := fields[0] + name := strings.TrimSuffix(fields[1], "."+localZone) + hostsMap[ip] = append(hostsMap[ip], name) + } + return hostsMap +} diff --git a/internal/clientinfo/hostsfile_test.go b/internal/clientinfo/hostsfile_test.go index f67fcef..27c8b90 100644 --- a/internal/clientinfo/hostsfile_test.go +++ b/internal/clientinfo/hostsfile_test.go @@ -1,6 +1,7 @@ package clientinfo import ( + "strings" "testing" ) @@ -31,3 +32,46 @@ func Test_hostsFile_LookupHostnameByIP(t *testing.T) { }) } } + +func Test_parseHostEntriesConfFromReader(t *testing.T) { + const content = `local-zone: "localdomain" transparent +local-data-ptr: "127.0.0.1 localhost" +local-data: "localhost A 127.0.0.1" +local-data: "localhost.localdomain A 127.0.0.1" +local-data-ptr: "::1 localhost" +local-data: "localhost AAAA ::1" +local-data: "localhost.localdomain AAAA ::1" +local-data-ptr: "10.0.10.227 OPNsense.localdomain" +local-data: "OPNsense.localdomain A 10.0.10.227" +local-data: "OPNsense A 10.0.10.227" +local-data-ptr: "fe80::5a78:4e29:caa3:f9f7 OPNsense.localdomain" +local-data: "OPNsense.localdomain AAAA fe80::5a78:4e29:caa3:f9f7" +local-data: "OPNsense AAAA fe80::5a78:4e29:caa3:f9f7" +local-data-ptr: "1.1.1.1 banana-party.local.com" +local-data: "banana-party.local.com IN A 1.1.1.1" +local-data-ptr: "1.1.1.1 cheese-land.lan" +local-data: "cheese-land.lan IN A 1.1.1.1" +` + r := strings.NewReader(content) + hostsMap := parseHostEntriesConfFromReader(r) + if len(hostsMap) != 5 { + t.Fatalf("unexpected number of entries, want 5, got: %d", len(hostsMap)) + } + for ip, names := range hostsMap { + switch ip { + case "1.1.1.1": + for _, name := range names { + if name != "banana-party.local.com" && name != "cheese-land.lan" { + t.Fatalf("unexpected names for 1.1.1.1: %v", names) + } + } + case "10.0.10.227": + if len(names) != 1 { + t.Fatalf("unexpected names for 10.0.10.227: %v", names) + } + if names[0] != "OPNsense" { + t.Fatalf("unexpected name: %s", names[0]) + } + } + } +}