diff --git a/netlink_linux_test.go b/netlink_linux_test.go index f092039..592222d 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,281 @@ 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) + } + }) + } +} + +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") + } + }) +}