diff --git a/internal/clientinfo/mdns.go b/internal/clientinfo/mdns.go index 3f0a311..3fb004e 100644 --- a/internal/clientinfo/mdns.go +++ b/internal/clientinfo/mdns.go @@ -1,11 +1,16 @@ package clientinfo import ( + "bufio" + "bytes" "context" "errors" + "io" "net" "net/netip" "os" + "os/exec" + "strings" "sync" "syscall" "time" @@ -107,6 +112,7 @@ func (m *mdns) init(quitCh chan struct{}) error { go m.probeLoop(v4ConnList, mdnsV4Addr, quitCh) go m.probeLoop(v6ConnList, mdnsV6Addr, quitCh) + go m.getDataFromAvahiDaemonCache() return nil } @@ -212,6 +218,44 @@ func (m *mdns) probe(conns []*net.UDPConn, remoteAddr net.Addr) error { return err } +// getDataFromAvahiDaemonCache reads entries from avahi-daemon cache to update mdns data. +func (m *mdns) getDataFromAvahiDaemonCache() { + if _, err := exec.LookPath("avahi-browse"); err != nil { + ctrld.ProxyLogger.Load().Debug().Err(err).Msg("could not find avahi-browse binary, skipping.") + return + } + // Run avahi-browse to discover services from cache: + // - "-a" -> all services. + // - "-r" -> resolve found services. + // - "-p" -> parseable format. + // - "-c" -> read from cache. + out, err := exec.Command("avahi-browse", "-a", "-r", "-p", "-c").Output() + if err != nil { + ctrld.ProxyLogger.Load().Debug().Err(err).Msg("could not browse services from avahi cache") + return + } + m.storeDataFromAvahiBrowseOutput(bytes.NewReader(out)) +} + +// storeDataFromAvahiBrowseOutput parses avahi-browse output from reader, then updating found data to mdns table. +func (m *mdns) storeDataFromAvahiBrowseOutput(r io.Reader) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + fields := strings.FieldsFunc(scanner.Text(), func(r rune) bool { + return r == ';' + }) + if len(fields) < 8 || fields[0] != "=" { + continue + } + ip := fields[7] + name := normalizeHostname(fields[6]) + // Only using cache value if we don't have existed one. + if _, loaded := m.name.LoadOrStore(ip, name); !loaded { + ctrld.ProxyLogger.Load().Debug().Msgf("found hostname: %q, ip: %q via avahi cache", name, ip) + } + } +} + func multicastInterfaces() ([]net.Interface, error) { ifaces, err := net.Interfaces() if err != nil { diff --git a/internal/clientinfo/mdns_test.go b/internal/clientinfo/mdns_test.go new file mode 100644 index 0000000..e6f8698 --- /dev/null +++ b/internal/clientinfo/mdns_test.go @@ -0,0 +1,27 @@ +package clientinfo + +import ( + "strings" + "testing" +) + +func Test_mdns_storeDataFromAvahiBrowseOutput(t *testing.T) { + const content = `+;wlp0s20f3;IPv6;Foo\032\0402\041;_companion-link._tcp;local ++;wlp0s20f3;IPv4;Foo\032\0402\041;_companion-link._tcp;local +=;wlp0s20f3;IPv6;Foo\032\0402\041;_companion-link._tcp;local;Foo-2.local;192.168.1.123;64842;"rpBA=00:00:00:00:00:01" "rpHI=e6ae2cbbca0e" "rpAD=36566f4d850f" "rpVr=510.71.1" "rpHA=0ddc20fdddc8" "rpFl=0x30000" "rpHN=1d4a03afdefa" "rpMac=0" +=;wlp0s20f3;IPv4;Foo\032\0402\041;_companion-link._tcp;local;Foo-2.local;192.168.1.123;64842;"rpBA=00:00:00:00:00:01" "rpHI=e6ae2cbbca0e" "rpAD=36566f4d850f" "rpVr=510.71.1" "rpHA=0ddc20fdddc8" "rpFl=0x30000" "rpHN=1d4a03afdefa" "rpMac=0" +` + m := &mdns{} + m.storeDataFromAvahiBrowseOutput(strings.NewReader(content)) + ip := "192.168.1.123" + val, loaded := m.name.LoadOrStore(ip, "") + if !loaded { + t.Fatal("missing Foo-2 data from mdns table") + } + + wantHostname := "Foo-2" + hostname := val.(string) + if hostname != wantHostname { + t.Fatalf("unexpected hostname, want: %q, got: %q", wantHostname, hostname) + } +}