mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-31 13:03:02 +02:00
yangerd: Add support for getting ARP neighbors
This commit is contained in:
@@ -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" {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user