yangerd: Add support for getting ARP neighbors

This commit is contained in:
Mattias Walström
2026-06-27 08:39:48 +02:00
parent 76de3427ff
commit bf850f59d7
3 changed files with 262 additions and 47 deletions
+123 -22
View File
@@ -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" {
+101 -22
View File
@@ -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))
}
+38 -3
View File
@@ -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 {