From bf850f59d71991cb39e5e645fe842697fce2eacd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Fri, 22 May 2026 15:09:22 +0200 Subject: [PATCH] yangerd: Add support for getting ARP neighbors --- src/yangerd/internal/iface/iface.go | 145 +++++++++++++++++++---- src/yangerd/internal/iface/iface_test.go | 123 +++++++++++++++---- src/yangerd/internal/monitor/monitor.go | 41 ++++++- 3 files changed, 262 insertions(+), 47 deletions(-) diff --git a/src/yangerd/internal/iface/iface.go b/src/yangerd/internal/iface/iface.go index b250b515..6f74a16a 100644 --- a/src/yangerd/internal/iface/iface.go +++ b/src/yangerd/internal/iface/iface.go @@ -19,10 +19,15 @@ type FileChecker interface { // `{"interface":[...]}`. The caller (NLMonitor) stores this at tree key // "ietf-interfaces:interfaces"; the IPC server adds the module-qualified // wrapper when responding to clients. -func Transform(linkData, addrData, statsData json.RawMessage, fc FileChecker) json.RawMessage { +// +// neighData is the output of `ip -json neigh show` — an array of objects +// with keys: dst, dev, lladdr, state (array of strings like "REACHABLE", +// "STALE", "PERMANENT", etc.). May be nil if unavailable. +func Transform(linkData, addrData, statsData, neighData json.RawMessage, fc FileChecker) json.RawMessage { links := dedup(decodeObjects(linkData)) addrs := decodeObjects(addrData) stats := decodeObjects(statsData) + neighs := decodeObjects(neighData) addrByName := make(map[string]map[string]any, len(addrs)) for _, addr := range addrs { @@ -42,6 +47,15 @@ func Transform(linkData, addrData, statsData json.RawMessage, fc FileChecker) js statsByName[ifname] = st } + neighByName := make(map[string][]map[string]any) + for _, n := range neighs { + dev := getString(n, "dev") + if dev == "" { + continue + } + neighByName[dev] = append(neighByName[dev], n) + } + interfaces := make([]map[string]any, 0, len(links)) for _, iplink := range links { if skipInterface(iplink) { @@ -60,7 +74,7 @@ func Transform(linkData, addrData, statsData json.RawMessage, fc FileChecker) js } } - iface := interfaceCommon(iplink, ipaddr, fc) + iface := interfaceCommon(iplink, ipaddr, neighByName[ifname], fc) yangType := getString(iface, "type") switch yangType { @@ -172,7 +186,7 @@ func dedup(links []map[string]any) []map[string]any { return out } -func interfaceCommon(iplink, ipaddr map[string]any, fc FileChecker) map[string]any { +func interfaceCommon(iplink, ipaddr map[string]any, neighEntries []map[string]any, fc FileChecker) map[string]any { flags := getStrings(iplink, "flags") iface := map[string]any{ @@ -197,11 +211,11 @@ func interfaceCommon(iplink, ipaddr map[string]any, fc FileChecker) map[string]a iface["statistics"] = stats } - if ipv4 := ipv4Data(ipaddr); len(ipv4) > 0 { + if ipv4 := ipv4Data(ipaddr, neighEntries); len(ipv4) > 0 { iface["ietf-ip:ipv4"] = ipv4 } - if ipv6 := ipv6Data(ipaddr, fc); len(ipv6) > 0 { + if ipv6 := ipv6Data(ipaddr, neighEntries, fc); len(ipv6) > 0 { iface["ietf-ip:ipv6"] = ipv6 } @@ -299,42 +313,54 @@ func statistics(iplink map[string]any) map[string]any { return out } -func ipv4Data(ipaddr map[string]any) map[string]any { - if len(ipaddr) == 0 { +func ipv4Data(ipaddr map[string]any, neighEntries []map[string]any) map[string]any { + if len(ipaddr) == 0 && len(neighEntries) == 0 { return nil } out := map[string]any{} - if mtu, ok := getInt(ipaddr, "mtu"); ok && mtu != 0 && getString(ipaddr, "ifname") != "lo" { - out["mtu"] = mtu + if len(ipaddr) > 0 { + if mtu, ok := getInt(ipaddr, "mtu"); ok && mtu != 0 && getString(ipaddr, "ifname") != "lo" { + out["mtu"] = mtu + } + + if addr := addresses(ipaddr, "inet"); len(addr) > 0 { + out["address"] = addr + } } - if addr := addresses(ipaddr, "inet"); len(addr) > 0 { - out["address"] = addr + if n := neighbors(neighEntries, 4); len(n) > 0 { + out["neighbor"] = n } return out } -func ipv6Data(ipaddr map[string]any, fc FileChecker) map[string]any { - if len(ipaddr) == 0 { +func ipv6Data(ipaddr map[string]any, neighEntries []map[string]any, fc FileChecker) map[string]any { + if len(ipaddr) == 0 && len(neighEntries) == 0 { return nil } out := map[string]any{} - ifname := getString(ipaddr, "ifname") - if ifname != "" && fc != nil { - path := fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/mtu", ifname) - if raw, err := fc.ReadFile(path); err == nil { - trimmed := strings.TrimSpace(raw) - if mtu, err := strconv.Atoi(trimmed); err == nil { - out["mtu"] = mtu + if len(ipaddr) > 0 { + ifname := getString(ipaddr, "ifname") + if ifname != "" && fc != nil { + path := fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/mtu", ifname) + if raw, err := fc.ReadFile(path); err == nil { + trimmed := strings.TrimSpace(raw) + if mtu, err := strconv.Atoi(trimmed); err == nil { + out["mtu"] = mtu + } } } + + if addr := addresses(ipaddr, "inet6"); len(addr) > 0 { + out["address"] = addr + } } - if addr := addresses(ipaddr, "inet6"); len(addr) > 0 { - out["address"] = addr + if n := neighbors(neighEntries, 6); len(n) > 0 { + out["neighbor"] = n } return out @@ -373,6 +399,81 @@ func addresses(ipaddr map[string]any, family string) []map[string]any { return out } +func neighbors(entries []map[string]any, ipVersion int) []map[string]any { + out := make([]map[string]any, 0, len(entries)) + for _, entry := range entries { + dst := getString(entry, "dst") + if dst == "" { + continue + } + + if !neighMatchesFamily(dst, ipVersion) { + continue + } + + lladdr := getString(entry, "lladdr") + if lladdr == "" { + continue + } + + states := getStrings(entry, "state") + origin := "dynamic" + if contains(states, "PERMANENT") { + origin = "static" + } + + neigh := map[string]any{ + "ip": dst, + "link-layer-address": lladdr, + "origin": origin, + } + + if ipVersion == 6 { + if state := neighState(states); state != "" { + neigh["state"] = state + } + if getBool(entry, "router") { + neigh["is-router"] = []any{nil} + } + } + + out = append(out, neigh) + } + + if len(out) == 0 { + return nil + } + return out +} + +func neighMatchesFamily(dst string, ipVersion int) bool { + for i := 0; i < len(dst); i++ { + if dst[i] == '.' { + return ipVersion == 4 + } + if dst[i] == ':' { + return ipVersion == 6 + } + } + return false +} + +func neighState(states []string) string { + xlate := map[string]string{ + "REACHABLE": "reachable", + "STALE": "stale", + "DELAY": "delay", + "PROBE": "probe", + "INCOMPLETE": "incomplete", + } + for _, s := range states { + if v, ok := xlate[s]; ok { + return v + } + } + return "" +} + func inet2yangOrigin(inet map[string]any) string { proto := getString(inet, "protocol") if proto == "kernel_ll" || proto == "kernel_ra" { diff --git a/src/yangerd/internal/iface/iface_test.go b/src/yangerd/internal/iface/iface_test.go index e072abf0..e2b436f4 100644 --- a/src/yangerd/internal/iface/iface_test.go +++ b/src/yangerd/internal/iface/iface_test.go @@ -95,7 +95,7 @@ func TestTransformEmptyInputs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ifaces := mustInterfaces(t, Transform(tt.linkData, tt.addrData, tt.stats, nil)) + ifaces := mustInterfaces(t, Transform(tt.linkData, tt.addrData, tt.stats, nil, nil)) if len(ifaces) != 0 { t.Fatalf("expected empty interface list, got %d", len(ifaces)) } @@ -114,7 +114,7 @@ func TestTransformSingleLoopback(t *testing.T) { "statistics": map[string]any{}, }} - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) if len(ifaces) != 1 { t.Fatalf("expected 1 interface, got %d", len(ifaces)) } @@ -152,7 +152,7 @@ func TestTransformSingleEthernetWithIPv4IPv6(t *testing.T) { fc := &mockFileChecker{files: map[string]string{"/proc/sys/net/ipv6/conf/eth0/mtu": "1400\n"}} - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), mustRaw(t, addr), nil, fc)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), mustRaw(t, addr), nil, nil, fc)) eth0 := mustIfaceByName(t, ifaces, "eth0") if eth0["type"] != "infix-if-type:ethernet" { @@ -203,7 +203,7 @@ func TestTransformStatisticsCountersAsStrings(t *testing.T) { }, }} - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, mustRaw(t, stats), nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, mustRaw(t, stats), nil, nil)) eth1 := mustIfaceByName(t, ifaces, "eth1") st, ok := eth1["statistics"].(map[string]any) if !ok { @@ -232,7 +232,7 @@ func TestTransformVLANAugment(t *testing.T) { }, }} - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) vlan := mustIfaceByName(t, ifaces, "eth0.100") if vlan["type"] != "infix-if-type:vlan" { @@ -258,7 +258,7 @@ func TestTransformVethAugment(t *testing.T) { "linkinfo": map[string]any{"info_kind": "veth"}, }} - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) veth := mustIfaceByName(t, ifaces, "veth0") v, ok := veth["infix-interfaces:veth"].(map[string]any) if !ok || v["peer"] != "veth1" { @@ -291,7 +291,7 @@ func TestTransformGREAndVXLANAugments(t *testing.T) { }, } - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) gre := mustIfaceByName(t, ifaces, "gre1") if gre["type"] != "infix-if-type:gre" { @@ -355,7 +355,7 @@ func TestTransformLAGAugmentModes(t *testing.T) { }, } - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) bond0 := mustIfaceByName(t, ifaces, "bond0") b0 := bond0["infix-interfaces:lag"].(map[string]any) @@ -398,7 +398,7 @@ func TestTransformBridgePortLowerLayer(t *testing.T) { }, }} - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) eth2 := mustIfaceByName(t, ifaces, "eth2") lower := eth2["infix-interfaces:bridge-port"].(map[string]any) if lower["bridge"] != "br0" { @@ -430,7 +430,7 @@ func TestTransformLagPortLowerLayer(t *testing.T) { }, }} - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) eth3 := mustIfaceByName(t, ifaces, "eth3") lower := eth3["infix-interfaces:lag-port"].(map[string]any) if lower["lag"] != "bond0" || lower["state"] != "active" || lower["link-failures"] != float64(5) { @@ -450,7 +450,7 @@ func TestTransformFilteredInterfaces(t *testing.T) { {"ifname": "eth9", "ifindex": 99, "flags": []any{"UP"}, "link_type": "ether", "operstate": "UP"}, } - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) if len(ifaces) != 1 { t.Fatalf("expected only one surviving interface, got %d", len(ifaces)) } @@ -469,7 +469,7 @@ func TestTransformWiFiType(t *testing.T) { }} fc := &mockFileChecker{exists: map[string]bool{"/sys/class/net/wlan0/wireless/": true}} - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, fc)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, fc)) wlan0 := mustIfaceByName(t, ifaces, "wlan0") if wlan0["type"] != "infix-if-type:wifi" { t.Fatalf("wlan0 type = %v", wlan0["type"]) @@ -721,7 +721,7 @@ func TestIPv4Data(t *testing.T) { map[string]any{"family": "inet", "local": "10.0.0.1", "prefixlen": 24, "protocol": "static"}, }, } - out := ipv4Data(in) + out := ipv4Data(in, nil) if out["mtu"] != 1500 { t.Fatalf("unexpected mtu: %#v", out) } @@ -737,7 +737,7 @@ func TestIPv4Data(t *testing.T) { map[string]any{"family": "inet", "local": "10.0.0.2", "prefixlen": 24, "protocol": "static"}, }, } - out := ipv4Data(in) + out := ipv4Data(in, nil) if _, ok := out["mtu"]; ok { t.Fatalf("did not expect mtu in %#v", out) } @@ -745,7 +745,7 @@ func TestIPv4Data(t *testing.T) { t.Run("loopback omits mtu", func(t *testing.T) { in := map[string]any{"ifname": "lo", "mtu": 65536} - out := ipv4Data(in) + out := ipv4Data(in, nil) if _, ok := out["mtu"]; ok { t.Fatalf("loopback must not include mtu: %#v", out) } @@ -761,7 +761,7 @@ func TestIPv6Data(t *testing.T) { }, } fc := &mockFileChecker{files: map[string]string{"/proc/sys/net/ipv6/conf/eth0/mtu": "1280\n"}} - out := ipv6Data(in, fc) + out := ipv6Data(in, nil, fc) if out["mtu"] != 1280 { t.Fatalf("unexpected mtu: %#v", out) } @@ -773,7 +773,7 @@ func TestIPv6Data(t *testing.T) { t.Run("without mtu from filechecker", func(t *testing.T) { in := map[string]any{"ifname": "eth1"} fc := &mockFileChecker{readErr: map[string]error{"/proc/sys/net/ipv6/conf/eth1/mtu": errors.New("no file")}} - out := ipv6Data(in, fc) + out := ipv6Data(in, nil, fc) if _, ok := out["mtu"]; ok { t.Fatalf("did not expect mtu in %#v", out) } @@ -781,20 +781,99 @@ func TestIPv6Data(t *testing.T) { t.Run("without addresses", func(t *testing.T) { in := map[string]any{"ifname": "eth2"} - out := ipv6Data(in, nil) + out := ipv6Data(in, nil, nil) if len(out) != 0 { t.Fatalf("expected empty ipv6 map, got %#v", out) } }) } +func TestNeighbors(t *testing.T) { + t.Run("ipv4 static and dynamic", func(t *testing.T) { + link := []map[string]any{ + {"ifindex": 2, "ifname": "eth0", "flags": []any{"UP"}, "link_type": "ether", "operstate": "UP", "address": "02:00:00:00:00:01"}, + } + neighs := []map[string]any{ + {"dst": "192.168.1.1", "dev": "eth0", "lladdr": "aa:bb:cc:dd:ee:ff", "state": []any{"REACHABLE"}}, + {"dst": "192.168.1.2", "dev": "eth0", "lladdr": "11:22:33:44:55:66", "state": []any{"PERMANENT"}}, + {"dst": "2001:db8::1", "dev": "eth0", "lladdr": "aa:bb:cc:dd:ee:01", "state": []any{"STALE"}}, + } + + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, mustRaw(t, neighs), nil)) + eth0 := mustIfaceByName(t, ifaces, "eth0") + + ipv4, ok := eth0["ietf-ip:ipv4"].(map[string]any) + if !ok { + t.Fatalf("missing ipv4: %#v", eth0) + } + v4neighs, ok := ipv4["neighbor"].([]any) + if !ok || len(v4neighs) != 2 { + t.Fatalf("expected 2 ipv4 neighbors, got %#v", ipv4["neighbor"]) + } + + n0 := v4neighs[0].(map[string]any) + if n0["ip"] != "192.168.1.1" || n0["link-layer-address"] != "aa:bb:cc:dd:ee:ff" || n0["origin"] != "dynamic" { + t.Fatalf("unexpected neighbor[0]: %#v", n0) + } + n1 := v4neighs[1].(map[string]any) + if n1["origin"] != "static" { + t.Fatalf("expected static origin: %#v", n1) + } + }) + + t.Run("ipv6 with state and is-router", func(t *testing.T) { + link := []map[string]any{ + {"ifindex": 2, "ifname": "eth0", "flags": []any{"UP"}, "link_type": "ether", "operstate": "UP", "address": "02:00:00:00:00:01"}, + } + neighs := []map[string]any{ + {"dst": "2001:db8::1", "dev": "eth0", "lladdr": "aa:bb:cc:dd:ee:01", "state": []any{"STALE"}, "router": true}, + } + + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, mustRaw(t, neighs), nil)) + eth0 := mustIfaceByName(t, ifaces, "eth0") + + ipv6, ok := eth0["ietf-ip:ipv6"].(map[string]any) + if !ok { + t.Fatalf("missing ipv6: %#v", eth0) + } + v6neighs, ok := ipv6["neighbor"].([]any) + if !ok || len(v6neighs) != 1 { + t.Fatalf("expected 1 ipv6 neighbor, got %#v", ipv6["neighbor"]) + } + + n := v6neighs[0].(map[string]any) + if n["state"] != "stale" { + t.Fatalf("expected stale state: %#v", n) + } + if _, ok := n["is-router"]; !ok { + t.Fatalf("expected is-router: %#v", n) + } + }) + + t.Run("skips entries without lladdr", func(t *testing.T) { + link := []map[string]any{ + {"ifindex": 2, "ifname": "eth0", "flags": []any{"UP"}, "link_type": "ether", "operstate": "UP", "address": "02:00:00:00:00:01"}, + } + neighs := []map[string]any{ + {"dst": "192.168.1.1", "dev": "eth0", "state": []any{"INCOMPLETE"}}, + } + + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, mustRaw(t, neighs), nil)) + eth0 := mustIfaceByName(t, ifaces, "eth0") + + if _, ok := eth0["ietf-ip:ipv4"]; ok { + t.Fatalf("should not have ipv4 with no valid neighbors: %#v", eth0) + } + }) +} + func TestDedupByIfindex(t *testing.T) { t.Run("keeps UP over DOWN for same ifindex", func(t *testing.T) { link := []map[string]any{ {"ifindex": 2, "ifname": "eth0", "flags": []any{}, "link_type": "ether", "operstate": "DOWN", "address": "02:00:00:00:00:01"}, {"ifindex": 2, "ifname": "e1", "flags": []any{"UP"}, "link_type": "ether", "operstate": "UP", "address": "02:00:00:00:00:01"}, } - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) if len(ifaces) != 1 { t.Fatalf("expected 1 interface after dedup, got %d", len(ifaces)) } @@ -808,7 +887,7 @@ func TestDedupByIfindex(t *testing.T) { {"ifindex": 3, "ifname": "a0", "flags": []any{}, "link_type": "ether", "operstate": "DOWN"}, {"ifindex": 3, "ifname": "a1", "flags": []any{}, "link_type": "ether", "operstate": "DOWN"}, } - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) if len(ifaces) != 1 { t.Fatalf("expected 1 interface after dedup, got %d", len(ifaces)) } @@ -822,7 +901,7 @@ func TestDedupByIfindex(t *testing.T) { {"ifindex": 1, "ifname": "lo", "flags": []any{"LOOPBACK", "UP"}, "link_type": "loopback", "operstate": "UNKNOWN"}, {"ifindex": 2, "ifname": "e1", "flags": []any{"UP"}, "link_type": "ether", "operstate": "UP"}, } - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) if len(ifaces) != 2 { t.Fatalf("expected 2 interfaces, got %d", len(ifaces)) } @@ -833,7 +912,7 @@ func TestDedupByIfindex(t *testing.T) { {"ifname": "x0", "flags": []any{"UP"}, "link_type": "ether", "operstate": "UP"}, {"ifname": "x1", "flags": []any{"UP"}, "link_type": "ether", "operstate": "UP"}, } - ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil)) + ifaces := mustInterfaces(t, Transform(mustRaw(t, link), nil, nil, nil, nil)) if len(ifaces) != 2 { t.Fatalf("expected 2 interfaces (zero ifindex not deduped), got %d", len(ifaces)) } diff --git a/src/yangerd/internal/monitor/monitor.go b/src/yangerd/internal/monitor/monitor.go index fb8cec1c..5b0c8c19 100644 --- a/src/yangerd/internal/monitor/monitor.go +++ b/src/yangerd/internal/monitor/monitor.go @@ -33,6 +33,7 @@ const treeKey = "ietf-interfaces:interfaces" type NLMonitor struct { linkBatch *ipbatch.IPBatch addrBatch *ipbatch.IPBatch + neighBatch *ipbatch.IPBatch brBatch *bridgebatch.BridgeBatch tree *tree.Tree ethRefresh func(string) @@ -55,6 +56,7 @@ type NLMonitor struct { mu sync.Mutex links json.RawMessage // ip -json -s -d link show (includes stats+details) addrs json.RawMessage // ip -json -d addr show (details only, no stats) + neighs json.RawMessage // ip -json neigh show fdb map[string]json.RawMessage mdb map[string]json.RawMessage ethernet map[string]json.RawMessage // ifname → ethtool JSON @@ -67,10 +69,11 @@ type NLMonitor struct { // New creates a netlink monitor backed by ip/bridge batch query workers. // linkBatch should include -s -d flags; addrBatch should include -d only // (no -s, which causes multi-line output for link commands). -func New(linkBatch, addrBatch *ipbatch.IPBatch, brBatch *bridgebatch.BridgeBatch, t *tree.Tree, fc iface.FileChecker, log *slog.Logger) *NLMonitor { +func New(linkBatch, addrBatch, neighBatch *ipbatch.IPBatch, brBatch *bridgebatch.BridgeBatch, t *tree.Tree, fc iface.FileChecker, log *slog.Logger) *NLMonitor { return &NLMonitor{ linkBatch: linkBatch, addrBatch: addrBatch, + neighBatch: neighBatch, brBatch: brBatch, tree: t, fc: fc, @@ -242,13 +245,18 @@ func (m *NLMonitor) initialDump() error { if err != nil { return err } + neighRaw, err := m.queryNeigh("neigh show") + if err != nil { + neighRaw = json.RawMessage(`[]`) + } - m.log.Debug("initialDump", "linkBytes", len(linkRaw), "addrBytes", len(addrRaw)) + m.log.Debug("initialDump", "linkBytes", len(linkRaw), "addrBytes", len(addrRaw), "neighBytes", len(neighRaw)) m.validateAddrData("initialDump", addrRaw) m.mu.Lock() m.links = linkRaw m.addrs = addrRaw + m.neighs = neighRaw for _, name := range interfaceNames(linkRaw) { if st, ok := extractOperStatus(filterByIfName(linkRaw, name)); ok { m.lastOperStatus[name] = st @@ -331,6 +339,19 @@ func (m *NLMonitor) handleNeighUpdate(update netlink.NeighUpdate) { return } + raw, err := m.queryNeigh("neigh show") + if err != nil { + if errors.Is(err, ipbatch.ErrBatchDead) { + m.requestRedump() + } + return + } + + m.mu.Lock() + m.neighs = raw + m.mu.Unlock() + + m.rebuild() } func (m *NLMonitor) handleMDBUpdate() { @@ -410,7 +431,8 @@ func (m *NLMonitor) rebuild() { m.mu.Lock() linksCopy := append(json.RawMessage{}, m.links...) addrsCopy := append(json.RawMessage{}, m.addrs...) - doc := iface.Transform(linksCopy, addrsCopy, linksCopy, m.fc) + neighsCopy := append(json.RawMessage{}, m.neighs...) + doc := iface.Transform(linksCopy, addrsCopy, linksCopy, neighsCopy, m.fc) eth := copyStringMap(m.ethernet) wfi := copyStringMap(m.wifi) fdb := copyStringMap(m.fdb) @@ -630,6 +652,19 @@ func (m *NLMonitor) queryAddr(command string) (json.RawMessage, error) { return raw, nil } +func (m *NLMonitor) queryNeigh(command string) (json.RawMessage, error) { + raw, err := m.neighBatch.Query(command) + if err != nil { + if errors.Is(err, ipbatch.ErrBatchDead) { + m.log.Warn("neigh batch dead", "command", command, "err", err) + return nil, err + } + m.log.Error("neigh batch query failed", "command", command, "err", err) + return nil, err + } + return raw, nil +} + func (m *NLMonitor) queryBridge(command string) (json.RawMessage, error) { raw, err := m.brBatch.Query(command) if err != nil {