mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
So queries originating from host using ::1 as source will be recognized properly, and treated the same as other queries from host itself.
191 lines
4.8 KiB
Go
191 lines
4.8 KiB
Go
package clientinfo
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"io"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/jaytaylor/go-hostsfile"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
const (
|
|
ipv4LocalhostName = "localhost"
|
|
ipv6LocalhostName = "ip6-localhost"
|
|
ipv6LoopbackName = "ip6-loopback"
|
|
hostEntriesConfPath = "/var/unbound/host_entries.conf"
|
|
)
|
|
|
|
// hostsFile provides client discovery functionality using system hosts file.
|
|
type hostsFile struct {
|
|
watcher *fsnotify.Watcher
|
|
mu sync.Mutex
|
|
m map[string][]string
|
|
}
|
|
|
|
// init performs initialization works, which is necessary before hostsFile can be fully operated.
|
|
func (hf *hostsFile) init() error {
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hf.watcher = watcher
|
|
if err := hf.watcher.Add(hostsfile.HostsPath); err != nil {
|
|
return err
|
|
}
|
|
// Conservatively adding hostEntriesConfPath, since it is not available everywhere.
|
|
_ = hf.watcher.Add(hostEntriesConfPath)
|
|
return hf.refresh()
|
|
}
|
|
|
|
// refresh reloads hosts file entries.
|
|
func (hf *hostsFile) refresh() error {
|
|
m, err := hostsfile.ParseHosts(hostsfile.ReadHostsFile())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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
|
|
}
|
|
|
|
// watchChanges watches and updates hosts file data if any changes happens.
|
|
func (hf *hostsFile) watchChanges() {
|
|
if hf.watcher == nil {
|
|
return
|
|
}
|
|
for {
|
|
select {
|
|
case event, ok := <-hf.watcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) || event.Has(fsnotify.Chmod) || event.Has(fsnotify.Remove) {
|
|
if err := hf.refresh(); err != nil && !os.IsNotExist(err) {
|
|
ctrld.ProxyLogger.Load().Err(err).Msg("hosts file changed but failed to update client info")
|
|
}
|
|
}
|
|
case err, ok := <-hf.watcher.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
ctrld.ProxyLogger.Load().Err(err).Msg("could not watch client info file")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// LookupHostnameByIP returns hostname for given IP from current hosts file entries.
|
|
func (hf *hostsFile) LookupHostnameByIP(ip string) string {
|
|
hf.mu.Lock()
|
|
defer hf.mu.Unlock()
|
|
if names := hf.m[ip]; len(names) > 0 {
|
|
isLoopback := ip == ipV4Loopback || ip == ipv6Loopback
|
|
for _, hostname := range names {
|
|
name := normalizeHostname(hostname)
|
|
// Ignoring ipv4/ipv6 loopback entry.
|
|
if isLoopback && isLocalhostName(name) {
|
|
continue
|
|
}
|
|
return name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// LookupHostnameByMac returns hostname for given Mac from current hosts file entries.
|
|
func (hf *hostsFile) LookupHostnameByMac(mac string) string {
|
|
return ""
|
|
}
|
|
|
|
// String returns human-readable format of hostsFile.
|
|
func (hf *hostsFile) String() string {
|
|
return "hosts"
|
|
}
|
|
|
|
func (hf *hostsFile) lookupIPByHostname(name string, v6 bool) string {
|
|
if hf == nil {
|
|
return ""
|
|
}
|
|
hf.mu.Lock()
|
|
defer hf.mu.Unlock()
|
|
for addr, names := range hf.m {
|
|
if ip, err := netip.ParseAddr(addr); err == nil && !ip.IsLoopback() {
|
|
for _, n := range names {
|
|
if n == name && ip.Is6() == v6 {
|
|
return ip.String()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// isLocalhostName reports whether the given hostname represents localhost.
|
|
func isLocalhostName(hostname string) bool {
|
|
switch hostname {
|
|
case ipv4LocalhostName, ipv6LocalhostName, ipv6LoopbackName:
|
|
return true
|
|
default:
|
|
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
|
|
}
|