From 55302f6ba8fd4cc6ab41148224e2c1865dcaed29 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 13 Oct 2026 17:06:30 +0200 Subject: [PATCH 1/2] netlink: add unit tests for IPVS service and destination decoding Add table-driven tests for assembleService and assembleDestination, covering IPv4 and IPv6 addresses and their corresponding netlink attributes. Verify decoding of service persistence engine names, destination weights larger than 16 bits, and persistent connection counts. Also verify that attribute ordering does not affect decoding and that unknown attributes are ignored for forward compatibility. These tests exercise the decoding functions directly, without requiring network namespaces or elevated privileges. Signed-off-by: Sebastiaan van Stijn --- netlink_linux_test.go | 173 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/netlink_linux_test.go b/netlink_linux_test.go index f092039..a292fd7 100644 --- a/netlink_linux_test.go +++ b/netlink_linux_test.go @@ -2,11 +2,19 @@ package ipvs import ( "errors" + "net" "reflect" "syscall" "testing" + + "github.com/vishvananda/netlink/nl" + "golang.org/x/sys/unix" ) +// unknownAttrType represents an unrecognized netlink attribute, +// used to verify that unknown attributes are ignored. +const unknownAttrType = 0x7fff + func Test_getIPFamily(t *testing.T) { testcases := []struct { name string @@ -51,3 +59,168 @@ func Test_getIPFamily(t *testing.T) { }) } } + +func Test_AssembleService(t *testing.T) { + tests := []struct { + name string + attrs []syscall.NetlinkRouteAttr + want Service + }{ + { + name: "IPv4 service", + attrs: []syscall.NetlinkRouteAttr{ + { + Attr: syscall.RtAttr{Type: uint16(ipvsSvcAttrAddress)}, + Value: net.IPv4(192, 0, 2, 1).To4(), + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsSvcAttrAddressFamily)}, + Value: nl.Uint16Attr(unix.AF_INET), + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsSvcAttrProtocol)}, + Value: nl.Uint16Attr(unix.IPPROTO_TCP), + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsSvcAttrPort)}, + Value: []byte{0x00, 0x50}, + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsSvcAttrSchedName)}, + Value: nl.ZeroTerminated("rr"), + }, + { + // Unknown attributes should be ignored for forward compatibility. + Attr: syscall.RtAttr{Type: unknownAttrType}, + Value: []byte{1, 2, 3, 4}, + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsSvcAttrPEName)}, + Value: nl.ZeroTerminated("sip"), + }, + }, + want: Service{ + AddressFamily: unix.AF_INET, + Address: net.IPv4(192, 0, 2, 1).To4(), + Protocol: unix.IPPROTO_TCP, + Port: 80, + SchedName: "rr", + PEName: "sip", + }, + }, + { + name: "IPv6 service", + attrs: []syscall.NetlinkRouteAttr{ + { + Attr: syscall.RtAttr{Type: uint16(ipvsSvcAttrAddressFamily)}, + Value: nl.Uint16Attr(unix.AF_INET6), + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsSvcAttrAddress)}, + Value: net.ParseIP("2001:db8::1").To16(), + }, + }, + want: Service{ + AddressFamily: unix.AF_INET6, + Address: net.ParseIP("2001:db8::1"), + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := assembleService(tc.attrs) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(*got, tc.want) { + t.Errorf("assembleService() = %+v, want %+v", *got, tc.want) + } + }) + } +} + +func Test_AssembleDestination(t *testing.T) { + tests := []struct { + name string + attrs []syscall.NetlinkRouteAttr + want Destination + }{ + { + name: "IPv4 destination", + attrs: []syscall.NetlinkRouteAttr{ + { + Attr: syscall.RtAttr{Type: uint16(ipvsDestAttrAddress)}, + Value: net.IPv4(192, 0, 2, 2).To4(), + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsDestAttrAddressFamily)}, + Value: nl.Uint16Attr(unix.AF_INET), + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsDestAttrPort)}, + Value: []byte{0x1f, 0x90}, + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsDestAttrWeight)}, + Value: nl.Uint32Attr(65536), + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsDestAttrActiveConnections)}, + Value: nl.Uint32Attr(10), + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsDestAttrInactiveConnections)}, + Value: nl.Uint32Attr(20), + }, + { + // Unknown attributes should be ignored for forward compatibility. + Attr: syscall.RtAttr{Type: unknownAttrType}, + Value: []byte{1, 2, 3, 4}, + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsDestAttrPersistentConnections)}, + Value: nl.Uint32Attr(30), + }, + }, + want: Destination{ + AddressFamily: unix.AF_INET, + Address: net.IPv4(192, 0, 2, 2).To4(), + Port: 8080, + Weight: 65536, + ActiveConnections: 10, + InactiveConnections: 20, + PersistentConnections: 30, + }, + }, + { + name: "IPv6 destination", + attrs: []syscall.NetlinkRouteAttr{ + { + Attr: syscall.RtAttr{Type: uint16(ipvsDestAttrAddressFamily)}, + Value: nl.Uint16Attr(unix.AF_INET6), + }, + { + Attr: syscall.RtAttr{Type: uint16(ipvsDestAttrAddress)}, + Value: net.ParseIP("2001:db8::2").To16(), + }, + }, + want: Destination{ + AddressFamily: unix.AF_INET6, + Address: net.ParseIP("2001:db8::2"), + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := assembleDestination(tc.attrs) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(*got, tc.want) { + t.Errorf("assembleDestination() = %+v, want %+v", *got, tc.want) + } + }) + } +} From e14bd325f75b7718e475173e8dc0ed47153d6bc8 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 13 Oct 2026 17:24:22 +0200 Subject: [PATCH 2/2] netlink: add unit tests for IPVS statistics decoding Add table-driven tests for assembleStats, covering all supported statistics attributes, including 32-bit counters and 64-bit byte counts. Verify that unknown attributes are ignored without interrupting decoding, and that malformed netlink attributes return an error. These tests exercise the decoder without requiring network namespaces or elevated privileges, and provide baseline coverage for future STATS64 support. Signed-off-by: Sebastiaan van Stijn --- netlink_linux_test.go | 113 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/netlink_linux_test.go b/netlink_linux_test.go index a292fd7..592222d 100644 --- a/netlink_linux_test.go +++ b/netlink_linux_test.go @@ -224,3 +224,116 @@ func Test_AssembleDestination(t *testing.T) { }) } } + +func Test_AssembleStats(t *testing.T) { + tests := []struct { + name string + attr ipvsStats + value []byte + want SvcStats + }{ + { + name: "connections", + attr: ipvsStatsConns, + value: nl.Uint32Attr(65536), + want: SvcStats{Connections: 65536}, + }, + { + name: "packets in", + attr: ipvsStatsPktsIn, + value: nl.Uint32Attr(123456), + want: SvcStats{PacketsIn: 123456}, + }, + { + name: "packets out", + attr: ipvsStatsPktsOut, + value: nl.Uint32Attr(234567), + want: SvcStats{PacketsOut: 234567}, + }, + { + name: "bytes in", + attr: ipvsStatsBytesIn, + value: nl.Uint64Attr(1 << 40), + want: SvcStats{BytesIn: 1 << 40}, + }, + { + name: "bytes out", + attr: ipvsStatsBytesOut, + value: nl.Uint64Attr(1 << 41), + want: SvcStats{BytesOut: 1 << 41}, + }, + { + name: "connections per second", + attr: ipvsStatsCPS, + value: nl.Uint32Attr(100), + want: SvcStats{CPS: 100}, + }, + { + name: "packets in per second", + attr: ipvsStatsPPSIn, + value: nl.Uint32Attr(200), + want: SvcStats{PPSIn: 200}, + }, + { + name: "packets out per second", + attr: ipvsStatsPPSOut, + value: nl.Uint32Attr(300), + want: SvcStats{PPSOut: 300}, + }, + { + name: "bytes in per second", + attr: ipvsStatsBPSIn, + value: nl.Uint32Attr(400), + want: SvcStats{BPSIn: 400}, + }, + { + name: "bytes out per second", + attr: ipvsStatsBPSOut, + value: nl.Uint32Attr(500), + want: SvcStats{BPSOut: 500}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + attr := nl.NewRtAttr(int(tc.attr), tc.value) + + got, err := assembleStats(attr.Serialize()) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("assembleStats() = %+v, want %+v", got, tc.want) + } + }) + } + + t.Run("unknown attribute", func(t *testing.T) { + // Unknown attributes must not prevent subsequent attributes + // from being decoded. + unknown := nl.NewRtAttr(unknownAttrType, []byte{1, 2, 3, 4}) + known := nl.NewRtAttr(int(ipvsStatsConns), nl.Uint32Attr(123)) + + msg := append(unknown.Serialize(), known.Serialize()...) + got, err := assembleStats(msg) + if err != nil { + t.Fatal(err) + } + want := SvcStats{Connections: 123} + if !reflect.DeepEqual(got, want) { + t.Errorf("assembleStats() = %+v, want %+v", got, want) + } + }) + + t.Run("malformed attribute", func(t *testing.T) { + // The attribute header claims more bytes than are available. + msg := make([]byte, 4) + native.PutUint16(msg[:2], 8) + native.PutUint16(msg[2:], uint16(ipvsStatsConns)) + + _, err := assembleStats(msg) + if err == nil { + t.Fatal("assembleStats() expected an error for malformed attribute") + } + }) +}