diff --git a/engine/mac_resolver.go b/engine/mac_resolver.go index d8c1a0e..7561462 100644 --- a/engine/mac_resolver.go +++ b/engine/mac_resolver.go @@ -4,6 +4,7 @@ import ( "bufio" "net" "os" + "os/exec" "strings" "sync" "time" @@ -12,6 +13,7 @@ import ( const ( ifaceCacheTTL = 30 * time.Second arpCacheTTL = 10 * time.Second + ndpCacheTTL = 10 * time.Second ) type sourceMACResolver struct { @@ -22,12 +24,16 @@ type sourceMACResolver struct { lastARPRefresh time.Time arpByIP map[string]net.HardwareAddr + + lastNDPRefresh time.Time + ndpByIP map[string]net.HardwareAddr } func newSourceMACResolver() *sourceMACResolver { return &sourceMACResolver{ ifaceByIP: make(map[string]net.HardwareAddr), arpByIP: make(map[string]net.HardwareAddr), + ndpByIP: make(map[string]net.HardwareAddr), } } @@ -44,6 +50,7 @@ func (r *sourceMACResolver) Resolve(ip net.IP) net.HardwareAddr { r.mu.RLock() ifaceRefreshDue := now.Sub(r.lastIfaceRefresh) > ifaceCacheTTL arpRefreshDue := now.Sub(r.lastARPRefresh) > arpCacheTTL + ndpRefreshDue := now.Sub(r.lastNDPRefresh) > ndpCacheTTL if mac := r.ifaceByIP[ipKey]; len(mac) != 0 { out := append(net.HardwareAddr(nil), mac...) r.mu.RUnlock() @@ -54,6 +61,11 @@ func (r *sourceMACResolver) Resolve(ip net.IP) net.HardwareAddr { r.mu.RUnlock() return out } + if mac := r.ndpByIP[ipKey]; len(mac) != 0 && !ndpRefreshDue { + out := append(net.HardwareAddr(nil), mac...) + r.mu.RUnlock() + return out + } r.mu.RUnlock() if ifaceRefreshDue { @@ -62,6 +74,9 @@ func (r *sourceMACResolver) Resolve(ip net.IP) net.HardwareAddr { if arpRefreshDue { r.refreshARPCache(now) } + if ndpRefreshDue { + r.refreshNDPCache(now) + } r.mu.RLock() defer r.mu.RUnlock() @@ -71,6 +86,9 @@ func (r *sourceMACResolver) Resolve(ip net.IP) net.HardwareAddr { if mac := r.arpByIP[ipKey]; len(mac) != 0 { return append(net.HardwareAddr(nil), mac...) } + if mac := r.ndpByIP[ipKey]; len(mac) != 0 { + return append(net.HardwareAddr(nil), mac...) + } return nil } @@ -143,3 +161,78 @@ func (r *sourceMACResolver) refreshARPCache(now time.Time) { r.lastARPRefresh = now r.mu.Unlock() } + +func (r *sourceMACResolver) refreshNDPCache(now time.Time) { + m, ok := readNeighborCacheFile("/proc/net/ndisc_cache") + if !ok { + // Fallback for environments without /proc/net/ndisc_cache. + m = readIPv6NeighCommand() + } + r.mu.Lock() + r.ndpByIP = m + r.lastNDPRefresh = now + r.mu.Unlock() +} + +func readNeighborCacheFile(path string) (map[string]net.HardwareAddr, bool) { + f, err := os.Open(path) + if err != nil { + return nil, false + } + defer f.Close() + + m := make(map[string]net.HardwareAddr) + scanner := bufio.NewScanner(f) + for scanner.Scan() { + ip, mac, ok := parseNeighborLine(scanner.Text()) + if !ok { + continue + } + m[ip] = mac + } + if err := scanner.Err(); err != nil { + return nil, false + } + return m, true +} + +func readIPv6NeighCommand() map[string]net.HardwareAddr { + m := make(map[string]net.HardwareAddr) + out, err := exec.Command("ip", "-6", "neigh", "show").Output() + if err != nil { + return m + } + for _, line := range strings.Split(string(out), "\n") { + ip, mac, ok := parseNeighborLine(line) + if !ok { + continue + } + m[ip] = mac + } + return m +} + +func parseNeighborLine(line string) (string, net.HardwareAddr, bool) { + fields := strings.Fields(line) + if len(fields) < 2 { + return "", nil, false + } + var ipStr string + var mac net.HardwareAddr + for _, f := range fields { + if ip := net.ParseIP(f); ip != nil { + ipStr = ip.String() + continue + } + if parsedMAC, err := net.ParseMAC(f); err == nil { + // Ignore unresolved/bogus entries. + if parsedMAC.String() != "00:00:00:00:00:00" { + mac = append(net.HardwareAddr(nil), parsedMAC...) + } + } + } + if ipStr == "" || len(mac) == 0 { + return "", nil, false + } + return ipStr, mac, true +}