diff --git a/config.go b/config.go index eef5af0..3d9e222 100644 --- a/config.go +++ b/config.go @@ -178,6 +178,7 @@ type ServiceConfig struct { DiscoverARP *bool `mapstructure:"discover_arp" toml:"discover_dhcp,omitempty"` DiscoverDHCP *bool `mapstructure:"discover_dhcp" toml:"discover_dhcp,omitempty"` DiscoverPtr *bool `mapstructure:"discover_ptr" toml:"discover_ptr,omitempty"` + DiscoverHosts *bool `mapstructure:"discover_hosts" toml:"discover_hosts,omitempty"` Daemon bool `mapstructure:"-" toml:"-"` AllocateIP bool `mapstructure:"-" toml:"-"` } diff --git a/docs/config.md b/docs/config.md index f2b5554..35fbda5 100644 --- a/docs/config.md +++ b/docs/config.md @@ -193,6 +193,13 @@ Perform LAN client discovery using PTR queries. - Required: no - Default: true +### discover_hosts +Perform LAN client discovery using hosts file. + +- Type: boolean +- Required: no +- Default: true + ### dhcp_lease_file_path Relative or absolute path to a custom DHCP leases file location. diff --git a/go.mod b/go.mod index 205a6e7..cd5a22f 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.3 + github.com/txn2/txeh v1.5.3 github.com/vishvananda/netlink v1.2.1-beta.2 go4.org/mem v0.0.0-20220726221520-4f986261bf13 golang.org/x/net v0.10.0 diff --git a/go.sum b/go.sum index 24eea30..2556497 100644 --- a/go.sum +++ b/go.sum @@ -270,6 +270,8 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/txn2/txeh v1.5.3 h1:ZMgc3r+5/AFtE/ayCoICpvxj7xl/CYsZjnIGhozV/Kc= +github.com/txn2/txeh v1.5.3/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4= github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg= github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 9235ca9..a371a19 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -73,6 +73,7 @@ type Table struct { arp *arpDiscover ptr *ptrDiscover mdns *mdns + hf *hostsFile cfg *ctrld.Config quitCh chan struct{} selfIP string @@ -134,6 +135,17 @@ func (t *Table) init() { t.refreshers = append(t.refreshers, t.merlin) } } + if t.discoverHosts() { + t.hf = &hostsFile{} + ctrld.ProxyLogger.Load().Debug().Msg("start hosts file discovery") + if err := t.hf.init(); err != nil { + ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init hosts file discover") + } else { + t.hostnameResolvers = append(t.hostnameResolvers, t.hf) + t.refreshers = append(t.refreshers, t.hf) + } + go t.hf.watchChanges() + } if t.discoverDHCP() { t.dhcp = &dhcp{selfIP: t.selfIP} ctrld.ProxyLogger.Load().Debug().Msg("start dhcp discovery") @@ -328,6 +340,13 @@ func (t *Table) discoverPTR() bool { return *t.cfg.Service.DiscoverPtr } +func (t *Table) discoverHosts() bool { + if t.cfg.Service.DiscoverHosts == nil { + return true + } + return *t.cfg.Service.DiscoverHosts +} + // normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file. func normalizeIP(in string) string { // dnsmasq may put ip with interface index in lease file, strip it here. diff --git a/internal/clientinfo/hostsfile.go b/internal/clientinfo/hostsfile.go new file mode 100644 index 0000000..eafbe69 --- /dev/null +++ b/internal/clientinfo/hostsfile.go @@ -0,0 +1,86 @@ +package clientinfo + +import ( + "os" + + "github.com/fsnotify/fsnotify" + "github.com/txn2/txeh" + + "github.com/Control-D-Inc/ctrld" +) + +// hostsFile provides client discovery functionality using system hosts file. +type hostsFile struct { + h *txeh.Hosts + watcher *fsnotify.Watcher +} + +// init performs initialization works, which is necessary before hostsFile can be fully operated. +func (hf *hostsFile) init() error { + h, err := txeh.NewHostsDefault() + if err != nil { + return err + } + hf.h = h + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + hf.watcher = watcher + if err := hf.watcher.Add(hf.h.ReadFilePath); err != nil { + return err + } + return nil +} + +// refresh reloads hosts file entries. +func (hf *hostsFile) refresh() error { + return hf.h.Reload() +} + +// 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.h.Lock() + defer hf.h.Unlock() + + if names := hf.h.ListHostsByIP(ip); len(names) > 0 { + return names[0] + } + 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" +}