From cb8f5d02e9d79e95aa364f811ccd614a54096338 Mon Sep 17 00:00:00 2001 From: Aaron Campbell Date: Tue, 4 Aug 2026 15:01:55 -0300 Subject: [PATCH] ethtool: add RSS and ring configuration over netlink Add generic netlink support for reading and updating ring parameters, including TCP header/data split and its threshold. Add RSS get and partial set support for existing contexts so callers can safely update the indirection table without disturbing the hash key or function. Unit tests cover attribute encoding, decoding, malformed replies, optional update semantics, and netlink attribute size limits. Signed-off-by: Aaron Campbell --- ethtool_linux.go | 67 ++++++++++++ ethtool_linux_test.go | 63 +++++++++++ ethtool_rings_linux.go | 213 ++++++++++++++++++++++++++++++++++++ ethtool_rings_linux_test.go | 144 ++++++++++++++++++++++++ ethtool_rss_linux.go | 191 ++++++++++++++++++++++++++++++++ ethtool_rss_linux_test.go | 159 +++++++++++++++++++++++++++ nl/ethtool_linux.go | 64 +++++++++++ 7 files changed, 901 insertions(+) create mode 100644 ethtool_linux.go create mode 100644 ethtool_linux_test.go create mode 100644 ethtool_rings_linux.go create mode 100644 ethtool_rings_linux_test.go create mode 100644 ethtool_rss_linux.go create mode 100644 ethtool_rss_linux_test.go create mode 100644 nl/ethtool_linux.go diff --git a/ethtool_linux.go b/ethtool_linux.go new file mode 100644 index 000000000..8d10706f1 --- /dev/null +++ b/ethtool_linux.go @@ -0,0 +1,67 @@ +package netlink + +import ( + "errors" + "fmt" + "syscall" + + "github.com/vishvananda/netlink/nl" + "golang.org/x/sys/unix" +) + +func (h *Handle) ethtoolRequest(command uint8, flags int, attrs []*nl.RtAttr) ([][]syscall.NetlinkRouteAttr, error) { + family, err := h.GenlFamilyGet(nl.ETHTOOL_GENL_NAME) + if err != nil { + return nil, err + } + + req := h.newNetlinkRequest(int(family.ID), flags) + req.AddData(&nl.Genlmsg{ + Command: command, + Version: nl.ETHTOOL_GENL_VERSION, + }) + for _, attr := range attrs { + req.AddData(attr) + } + + msgs, executeErr := req.Execute(unix.NETLINK_GENERIC, 0) + if executeErr != nil && !errors.Is(executeErr, ErrDumpInterrupted) { + return nil, executeErr + } + + parsed := make([][]syscall.NetlinkRouteAttr, 0, len(msgs)) + for _, msg := range msgs { + if len(msg) < nl.SizeofGenlmsg { + return nil, fmt.Errorf("netlink: short ethtool response: got %d bytes, want at least %d", len(msg), nl.SizeofGenlmsg) + } + attrs, err := nl.ParseRouteAttr(msg[nl.SizeofGenlmsg:]) + if err != nil { + return nil, err + } + parsed = append(parsed, attrs) + } + return parsed, executeErr +} + +func newEthtoolHeader(attrType, ifIndex int) (*nl.RtAttr, error) { + if ifIndex <= 0 || uint64(ifIndex) > uint64(^uint32(0)) { + return nil, fmt.Errorf("netlink: invalid interface index %d", ifIndex) + } + header := nl.NewRtAttr(unix.NLA_F_NESTED|attrType, nil) + header.AddRtAttr(nl.ETHTOOL_A_HEADER_DEV_INDEX, nl.Uint32Attr(uint32(ifIndex))) + return header, nil +} + +func readEthtoolUint8(attr syscall.NetlinkRouteAttr) (uint8, error) { + if len(attr.Value) != 1 { + return 0, fmt.Errorf("netlink: ethtool attribute %d has %d bytes, want 1", attr.Attr.Type&nl.NLA_TYPE_MASK, len(attr.Value)) + } + return attr.Value[0], nil +} + +func readEthtoolUint32(attr syscall.NetlinkRouteAttr) (uint32, error) { + if len(attr.Value) != 4 { + return 0, fmt.Errorf("netlink: ethtool attribute %d has %d bytes, want 4", attr.Attr.Type&nl.NLA_TYPE_MASK, len(attr.Value)) + } + return native.Uint32(attr.Value), nil +} diff --git a/ethtool_linux_test.go b/ethtool_linux_test.go new file mode 100644 index 000000000..ddbe43bc9 --- /dev/null +++ b/ethtool_linux_test.go @@ -0,0 +1,63 @@ +package netlink + +import ( + "syscall" + "testing" + + "github.com/vishvananda/netlink/nl" + "golang.org/x/sys/unix" +) + +func routeAttr(attrType int, value []byte) syscall.NetlinkRouteAttr { + return syscall.NetlinkRouteAttr{ + Attr: syscall.RtAttr{Type: uint16(attrType)}, + Value: value, + } +} + +func parseSerializedAttrs(t *testing.T, attrs []*nl.RtAttr) []syscall.NetlinkRouteAttr { + t.Helper() + var data []byte + for _, attr := range attrs { + data = append(data, attr.Serialize()...) + } + parsed, err := nl.ParseRouteAttr(data) + if err != nil { + t.Fatalf("failed to parse serialized attributes: %v", err) + } + return parsed +} + +func attrsByType(attrs []syscall.NetlinkRouteAttr) map[uint16]syscall.NetlinkRouteAttr { + byType := make(map[uint16]syscall.NetlinkRouteAttr, len(attrs)) + for _, attr := range attrs { + byType[attr.Attr.Type&nl.NLA_TYPE_MASK] = attr + } + return byType +} + +func TestNewEthtoolHeader(t *testing.T) { + header, err := newEthtoolHeader(nl.ETHTOOL_A_RINGS_HEADER, 42) + if err != nil { + t.Fatalf("newEthtoolHeader failed: %v", err) + } + if header.Type != unix.NLA_F_NESTED|nl.ETHTOOL_A_RINGS_HEADER { + t.Fatalf("header type = %#x, want %#x", header.Type, unix.NLA_F_NESTED|nl.ETHTOOL_A_RINGS_HEADER) + } + + outer := parseSerializedAttrs(t, []*nl.RtAttr{header}) + inner, err := nl.ParseRouteAttr(outer[0].Value) + if err != nil { + t.Fatalf("failed to parse ethtool header: %v", err) + } + if len(inner) != 1 || inner[0].Attr.Type != nl.ETHTOOL_A_HEADER_DEV_INDEX { + t.Fatalf("header attributes = %#v, want device index", inner) + } + if got := native.Uint32(inner[0].Value); got != 42 { + t.Fatalf("device index = %d, want 42", got) + } + + if _, err := newEthtoolHeader(nl.ETHTOOL_A_RINGS_HEADER, 0); err == nil { + t.Fatal("newEthtoolHeader accepted interface index zero") + } +} diff --git a/ethtool_rings_linux.go b/ethtool_rings_linux.go new file mode 100644 index 000000000..a07cb8c6a --- /dev/null +++ b/ethtool_rings_linux.go @@ -0,0 +1,213 @@ +package netlink + +import ( + "fmt" + "syscall" + + "github.com/vishvananda/netlink/nl" + "golang.org/x/sys/unix" +) + +// NetDevTCPDataSplit describes whether a device places TCP headers and payload +// data in separate receive buffers. +type NetDevTCPDataSplit uint8 + +const ( + NetDevTCPDataSplitUnknown NetDevTCPDataSplit = iota + NetDevTCPDataSplitDisabled + NetDevTCPDataSplitEnabled +) + +// NetDevRings contains the ring parameters reported by a netdevice. +type NetDevRings struct { + RxMax uint32 + RxMiniMax uint32 + RxJumboMax uint32 + TxMax uint32 + Rx uint32 + RxMini uint32 + RxJumbo uint32 + Tx uint32 + + RxBufLen uint32 + TCPDataSplit NetDevTCPDataSplit + CQESize uint32 + TxPush bool + RxPush bool + TxPushBufLen uint32 + TxPushBufLenMax uint32 + HDSThreshold uint32 + HDSThresholdMax uint32 +} + +// NetDevRingsConfig describes a partial ring-parameter update. Nil fields are +// left unchanged. +type NetDevRingsConfig struct { + Rx *uint32 + RxMini *uint32 + RxJumbo *uint32 + Tx *uint32 + RxBufLen *uint32 + + TCPDataSplit *NetDevTCPDataSplit + CQESize *uint32 + TxPush *bool + RxPush *bool + TxPushBufLen *uint32 + HDSThreshold *uint32 +} + +func parseNetDevRings(attrs []syscall.NetlinkRouteAttr) (*NetDevRings, error) { + rings := &NetDevRings{} + for _, attr := range attrs { + typeID := attr.Attr.Type & nl.NLA_TYPE_MASK + switch typeID { + case nl.ETHTOOL_A_RINGS_RX_MAX, + nl.ETHTOOL_A_RINGS_RX_MINI_MAX, + nl.ETHTOOL_A_RINGS_RX_JUMBO_MAX, + nl.ETHTOOL_A_RINGS_TX_MAX, + nl.ETHTOOL_A_RINGS_RX, + nl.ETHTOOL_A_RINGS_RX_MINI, + nl.ETHTOOL_A_RINGS_RX_JUMBO, + nl.ETHTOOL_A_RINGS_TX, + nl.ETHTOOL_A_RINGS_RX_BUF_LEN, + nl.ETHTOOL_A_RINGS_CQE_SIZE, + nl.ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN, + nl.ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN_MAX, + nl.ETHTOOL_A_RINGS_HDS_THRESH, + nl.ETHTOOL_A_RINGS_HDS_THRESH_MAX: + value, err := readEthtoolUint32(attr) + if err != nil { + return nil, err + } + switch typeID { + case nl.ETHTOOL_A_RINGS_RX_MAX: + rings.RxMax = value + case nl.ETHTOOL_A_RINGS_RX_MINI_MAX: + rings.RxMiniMax = value + case nl.ETHTOOL_A_RINGS_RX_JUMBO_MAX: + rings.RxJumboMax = value + case nl.ETHTOOL_A_RINGS_TX_MAX: + rings.TxMax = value + case nl.ETHTOOL_A_RINGS_RX: + rings.Rx = value + case nl.ETHTOOL_A_RINGS_RX_MINI: + rings.RxMini = value + case nl.ETHTOOL_A_RINGS_RX_JUMBO: + rings.RxJumbo = value + case nl.ETHTOOL_A_RINGS_TX: + rings.Tx = value + case nl.ETHTOOL_A_RINGS_RX_BUF_LEN: + rings.RxBufLen = value + case nl.ETHTOOL_A_RINGS_CQE_SIZE: + rings.CQESize = value + case nl.ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN: + rings.TxPushBufLen = value + case nl.ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN_MAX: + rings.TxPushBufLenMax = value + case nl.ETHTOOL_A_RINGS_HDS_THRESH: + rings.HDSThreshold = value + case nl.ETHTOOL_A_RINGS_HDS_THRESH_MAX: + rings.HDSThresholdMax = value + } + case nl.ETHTOOL_A_RINGS_TCP_DATA_SPLIT, + nl.ETHTOOL_A_RINGS_TX_PUSH, + nl.ETHTOOL_A_RINGS_RX_PUSH: + value, err := readEthtoolUint8(attr) + if err != nil { + return nil, err + } + switch typeID { + case nl.ETHTOOL_A_RINGS_TCP_DATA_SPLIT: + rings.TCPDataSplit = NetDevTCPDataSplit(value) + case nl.ETHTOOL_A_RINGS_TX_PUSH: + rings.TxPush = value != 0 + case nl.ETHTOOL_A_RINGS_RX_PUSH: + rings.RxPush = value != 0 + } + } + } + return rings, nil +} + +// NetDevRingsGet returns the ring parameters for ifIndex. +func NetDevRingsGet(ifIndex int) (*NetDevRings, error) { + return pkgHandle.NetDevRingsGet(ifIndex) +} + +// NetDevRingsGet returns the ring parameters for ifIndex. +func (h *Handle) NetDevRingsGet(ifIndex int) (*NetDevRings, error) { + header, err := newEthtoolHeader(nl.ETHTOOL_A_RINGS_HEADER, ifIndex) + if err != nil { + return nil, err + } + msgs, err := h.ethtoolRequest(nl.ETHTOOL_MSG_RINGS_GET, unix.NLM_F_ACK, []*nl.RtAttr{header}) + if err != nil { + return nil, err + } + if len(msgs) != 1 { + return nil, fmt.Errorf("netlink: expected one ethtool rings response, got %d", len(msgs)) + } + return parseNetDevRings(msgs[0]) +} + +func newNetDevRingsSetAttrs(ifIndex int, config NetDevRingsConfig) ([]*nl.RtAttr, error) { + header, err := newEthtoolHeader(nl.ETHTOOL_A_RINGS_HEADER, ifIndex) + if err != nil { + return nil, err + } + attrs := []*nl.RtAttr{header} + addUint32 := func(attrType int, value *uint32) { + if value != nil { + attrs = append(attrs, nl.NewRtAttr(attrType, nl.Uint32Attr(*value))) + } + } + addBool := func(attrType int, value *bool) { + if value != nil { + v := byte(0) + if *value { + v = 1 + } + attrs = append(attrs, nl.NewRtAttr(attrType, []byte{v})) + } + } + + if config.RxBufLen != nil && *config.RxBufLen == 0 { + return nil, fmt.Errorf("netlink: RX buffer length must not be zero") + } + if config.CQESize != nil && *config.CQESize == 0 { + return nil, fmt.Errorf("netlink: CQE size must not be zero") + } + addUint32(nl.ETHTOOL_A_RINGS_RX, config.Rx) + addUint32(nl.ETHTOOL_A_RINGS_RX_MINI, config.RxMini) + addUint32(nl.ETHTOOL_A_RINGS_RX_JUMBO, config.RxJumbo) + addUint32(nl.ETHTOOL_A_RINGS_TX, config.Tx) + addUint32(nl.ETHTOOL_A_RINGS_RX_BUF_LEN, config.RxBufLen) + if config.TCPDataSplit != nil { + if *config.TCPDataSplit > NetDevTCPDataSplitEnabled { + return nil, fmt.Errorf("netlink: invalid TCP data split value %d", *config.TCPDataSplit) + } + attrs = append(attrs, nl.NewRtAttr(nl.ETHTOOL_A_RINGS_TCP_DATA_SPLIT, []byte{byte(*config.TCPDataSplit)})) + } + addUint32(nl.ETHTOOL_A_RINGS_CQE_SIZE, config.CQESize) + addBool(nl.ETHTOOL_A_RINGS_TX_PUSH, config.TxPush) + addBool(nl.ETHTOOL_A_RINGS_RX_PUSH, config.RxPush) + addUint32(nl.ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN, config.TxPushBufLen) + addUint32(nl.ETHTOOL_A_RINGS_HDS_THRESH, config.HDSThreshold) + return attrs, nil +} + +// NetDevRingsSet applies a partial ring-parameter update to ifIndex. +func NetDevRingsSet(ifIndex int, config NetDevRingsConfig) error { + return pkgHandle.NetDevRingsSet(ifIndex, config) +} + +// NetDevRingsSet applies a partial ring-parameter update to ifIndex. +func (h *Handle) NetDevRingsSet(ifIndex int, config NetDevRingsConfig) error { + attrs, err := newNetDevRingsSetAttrs(ifIndex, config) + if err != nil { + return err + } + _, err = h.ethtoolRequest(nl.ETHTOOL_MSG_RINGS_SET, unix.NLM_F_ACK, attrs) + return err +} diff --git a/ethtool_rings_linux_test.go b/ethtool_rings_linux_test.go new file mode 100644 index 000000000..a04f1dca2 --- /dev/null +++ b/ethtool_rings_linux_test.go @@ -0,0 +1,144 @@ +package netlink + +import ( + "reflect" + "syscall" + "testing" + + "github.com/vishvananda/netlink/nl" +) + +func TestParseNetDevRings(t *testing.T) { + attrs := []syscall.NetlinkRouteAttr{ + routeAttr(nl.ETHTOOL_A_RINGS_RX_MAX, nl.Uint32Attr(1)), + routeAttr(nl.ETHTOOL_A_RINGS_RX_MINI_MAX, nl.Uint32Attr(2)), + routeAttr(nl.ETHTOOL_A_RINGS_RX_JUMBO_MAX, nl.Uint32Attr(3)), + routeAttr(nl.ETHTOOL_A_RINGS_TX_MAX, nl.Uint32Attr(4)), + routeAttr(nl.ETHTOOL_A_RINGS_RX, nl.Uint32Attr(5)), + routeAttr(nl.ETHTOOL_A_RINGS_RX_MINI, nl.Uint32Attr(6)), + routeAttr(nl.ETHTOOL_A_RINGS_RX_JUMBO, nl.Uint32Attr(7)), + routeAttr(nl.ETHTOOL_A_RINGS_TX, nl.Uint32Attr(8)), + routeAttr(nl.ETHTOOL_A_RINGS_RX_BUF_LEN, nl.Uint32Attr(9)), + routeAttr(nl.ETHTOOL_A_RINGS_TCP_DATA_SPLIT, []byte{byte(NetDevTCPDataSplitEnabled)}), + routeAttr(nl.ETHTOOL_A_RINGS_CQE_SIZE, nl.Uint32Attr(10)), + routeAttr(nl.ETHTOOL_A_RINGS_TX_PUSH, []byte{1}), + routeAttr(nl.ETHTOOL_A_RINGS_RX_PUSH, []byte{0}), + routeAttr(nl.ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN, nl.Uint32Attr(11)), + routeAttr(nl.ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN_MAX, nl.Uint32Attr(12)), + routeAttr(nl.ETHTOOL_A_RINGS_HDS_THRESH, nl.Uint32Attr(13)), + routeAttr(nl.ETHTOOL_A_RINGS_HDS_THRESH_MAX, nl.Uint32Attr(14)), + } + + got, err := parseNetDevRings(attrs) + if err != nil { + t.Fatalf("parseNetDevRings failed: %v", err) + } + want := &NetDevRings{ + RxMax: 1, + RxMiniMax: 2, + RxJumboMax: 3, + TxMax: 4, + Rx: 5, + RxMini: 6, + RxJumbo: 7, + Tx: 8, + RxBufLen: 9, + TCPDataSplit: NetDevTCPDataSplitEnabled, + CQESize: 10, + TxPush: true, + RxPush: false, + TxPushBufLen: 11, + TxPushBufLenMax: 12, + HDSThreshold: 13, + HDSThresholdMax: 14, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("rings = %#v, want %#v", got, want) + } +} + +func TestParseNetDevRingsRejectsMalformedAttributes(t *testing.T) { + tests := []struct { + name string + attr syscall.NetlinkRouteAttr + }{ + {name: "u32", attr: routeAttr(nl.ETHTOOL_A_RINGS_RX, []byte{1, 2, 3})}, + {name: "u8", attr: routeAttr(nl.ETHTOOL_A_RINGS_TCP_DATA_SPLIT, []byte{1, 2})}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := parseNetDevRings([]syscall.NetlinkRouteAttr{test.attr}); err == nil { + t.Fatal("parseNetDevRings accepted a malformed attribute") + } + }) + } +} + +func TestNewNetDevRingsSetAttrs(t *testing.T) { + rx := uint32(128) + tx := uint32(256) + mode := NetDevTCPDataSplitEnabled + txPush := true + rxPush := false + threshold := uint32(64) + + attrs, err := newNetDevRingsSetAttrs(7, NetDevRingsConfig{ + Rx: &rx, + Tx: &tx, + TCPDataSplit: &mode, + TxPush: &txPush, + RxPush: &rxPush, + HDSThreshold: &threshold, + }) + if err != nil { + t.Fatalf("newNetDevRingsSetAttrs failed: %v", err) + } + byType := attrsByType(parseSerializedAttrs(t, attrs)) + if len(byType) != 7 { + t.Fatalf("encoded %d attribute types, want 7", len(byType)) + } + if got := native.Uint32(byType[nl.ETHTOOL_A_RINGS_RX].Value); got != rx { + t.Errorf("rx = %d, want %d", got, rx) + } + if got := native.Uint32(byType[nl.ETHTOOL_A_RINGS_TX].Value); got != tx { + t.Errorf("tx = %d, want %d", got, tx) + } + if got := byType[nl.ETHTOOL_A_RINGS_TCP_DATA_SPLIT].Value[0]; got != byte(mode) { + t.Errorf("TCP data split = %d, want %d", got, mode) + } + if got := byType[nl.ETHTOOL_A_RINGS_TX_PUSH].Value[0]; got != 1 { + t.Errorf("tx push = %d, want 1", got) + } + if got := byType[nl.ETHTOOL_A_RINGS_RX_PUSH].Value[0]; got != 0 { + t.Errorf("rx push = %d, want 0", got) + } + if got := native.Uint32(byType[nl.ETHTOOL_A_RINGS_HDS_THRESH].Value); got != threshold { + t.Errorf("HDS threshold = %d, want %d", got, threshold) + } +} + +func TestNewNetDevRingsSetAttrsRejectsInvalidDataSplit(t *testing.T) { + mode := NetDevTCPDataSplit(3) + _, err := newNetDevRingsSetAttrs(1, NetDevRingsConfig{TCPDataSplit: &mode}) + if err == nil { + t.Fatal("accepted an invalid TCP data split value") + } +} + +func TestNewNetDevRingsSetAttrsRejectsZeroSizes(t *testing.T) { + zero := uint32(0) + tests := []struct { + name string + config NetDevRingsConfig + }{ + {name: "RX buffer length", config: NetDevRingsConfig{RxBufLen: &zero}}, + {name: "CQE size", config: NetDevRingsConfig{CQESize: &zero}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := newNetDevRingsSetAttrs(1, test.config); err == nil { + t.Fatal("accepted an invalid zero size") + } + }) + } +} diff --git a/ethtool_rss_linux.go b/ethtool_rss_linux.go new file mode 100644 index 000000000..b20774ec7 --- /dev/null +++ b/ethtool_rss_linux.go @@ -0,0 +1,191 @@ +package netlink + +import ( + "fmt" + "syscall" + + "github.com/vishvananda/netlink/nl" + "golang.org/x/sys/unix" +) + +// NetDevRSSHashFunction identifies an RSS hash function. +type NetDevRSSHashFunction uint32 + +const ( + NetDevRSSHashFunctionToeplitz NetDevRSSHashFunction = 1 << iota + NetDevRSSHashFunctionXOR + NetDevRSSHashFunctionCRC32 +) + +// NetDevRSSInputTransformation identifies a transformation applied to RSS +// input fields before hashing. +type NetDevRSSInputTransformation uint32 + +const ( + NetDevRSSInputTransformationNone NetDevRSSInputTransformation = iota + NetDevRSSInputTransformationSymmetricXOR + NetDevRSSInputTransformationSymmetricORXOR +) + +// NetDevRSS contains the RSS configuration for one context. Context zero is +// the main RSS context used for normal receive-side scaling. +type NetDevRSS struct { + Context uint32 + HashFunction NetDevRSSHashFunction + IndirectionTable []uint32 + HashKey []byte + InputTransformation NetDevRSSInputTransformation +} + +// NetDevRSSConfig describes a partial update to an existing RSS context. Nil +// fields are left unchanged. A non-nil, empty IndirectionTable resets the main +// context's table to its default; the kernel does not allow that operation for +// additional contexts. +type NetDevRSSConfig struct { + HashFunction *NetDevRSSHashFunction + IndirectionTable []uint32 + HashKey []byte + InputTransformation *NetDevRSSInputTransformation +} + +const maxEthtoolAttrPayload = int(^uint16(0)) - unix.SizeofRtAttr + +func parseNetDevRSS(attrs []syscall.NetlinkRouteAttr, context uint32) (*NetDevRSS, error) { + rss := &NetDevRSS{Context: context} + for _, attr := range attrs { + typeID := attr.Attr.Type & nl.NLA_TYPE_MASK + switch typeID { + case nl.ETHTOOL_A_RSS_CONTEXT, + nl.ETHTOOL_A_RSS_HFUNC, + nl.ETHTOOL_A_RSS_INPUT_XFRM: + value, err := readEthtoolUint32(attr) + if err != nil { + return nil, err + } + switch typeID { + case nl.ETHTOOL_A_RSS_CONTEXT: + if value != context { + return nil, fmt.Errorf("netlink: ethtool RSS response context is %d, want %d", value, context) + } + case nl.ETHTOOL_A_RSS_HFUNC: + rss.HashFunction = NetDevRSSHashFunction(value) + case nl.ETHTOOL_A_RSS_INPUT_XFRM: + rss.InputTransformation = NetDevRSSInputTransformation(value) + } + case nl.ETHTOOL_A_RSS_INDIR: + if len(attr.Value)%4 != 0 { + return nil, fmt.Errorf("netlink: RSS indirection table has %d bytes, want a multiple of 4", len(attr.Value)) + } + rss.IndirectionTable = make([]uint32, len(attr.Value)/4) + for i := range rss.IndirectionTable { + rss.IndirectionTable[i] = native.Uint32(attr.Value[i*4:]) + } + case nl.ETHTOOL_A_RSS_HKEY: + rss.HashKey = append([]byte(nil), attr.Value...) + } + } + return rss, nil +} + +// NetDevRSSGet returns the RSS configuration for an existing context on +// ifIndex. Context zero selects the main RSS context. +func NetDevRSSGet(ifIndex int, context uint32) (*NetDevRSS, error) { + return pkgHandle.NetDevRSSGet(ifIndex, context) +} + +// NetDevRSSGet returns the RSS configuration for an existing context on +// ifIndex. Context zero selects the main RSS context. +func (h *Handle) NetDevRSSGet(ifIndex int, context uint32) (*NetDevRSS, error) { + header, err := newEthtoolHeader(nl.ETHTOOL_A_RSS_HEADER, ifIndex) + if err != nil { + return nil, err + } + attrs := []*nl.RtAttr{header} + if context != 0 { + attrs = append(attrs, nl.NewRtAttr(nl.ETHTOOL_A_RSS_CONTEXT, nl.Uint32Attr(context))) + } + msgs, err := h.ethtoolRequest(nl.ETHTOOL_MSG_RSS_GET, unix.NLM_F_ACK, attrs) + if err != nil { + return nil, err + } + if len(msgs) != 1 { + return nil, fmt.Errorf("netlink: expected one ethtool RSS response, got %d", len(msgs)) + } + return parseNetDevRSS(msgs[0], context) +} + +func encodeNetDevRSSIndirectionTable(table []uint32) ([]byte, error) { + if len(table) > maxEthtoolAttrPayload/4 { + return nil, fmt.Errorf("netlink: RSS indirection table has %d entries, maximum is %d", len(table), maxEthtoolAttrPayload/4) + } + data := make([]byte, len(table)*4) + for i, queue := range table { + native.PutUint32(data[i*4:], queue) + } + return data, nil +} + +func newNetDevRSSSetAttrs(ifIndex int, context uint32, config NetDevRSSConfig) ([]*nl.RtAttr, error) { + header, err := newEthtoolHeader(nl.ETHTOOL_A_RSS_HEADER, ifIndex) + if err != nil { + return nil, err + } + attrs := []*nl.RtAttr{header} + if context != 0 { + attrs = append(attrs, nl.NewRtAttr(nl.ETHTOOL_A_RSS_CONTEXT, nl.Uint32Attr(context))) + } + if config.HashFunction != nil { + hashFunction := uint32(*config.HashFunction) + switch *config.HashFunction { + case NetDevRSSHashFunctionToeplitz, + NetDevRSSHashFunctionXOR, + NetDevRSSHashFunctionCRC32: + default: + return nil, fmt.Errorf("netlink: invalid RSS hash function %#x", *config.HashFunction) + } + attrs = append(attrs, nl.NewRtAttr(nl.ETHTOOL_A_RSS_HFUNC, nl.Uint32Attr(hashFunction))) + } + if config.IndirectionTable != nil { + if context != 0 && len(config.IndirectionTable) == 0 { + return nil, fmt.Errorf("netlink: cannot reset the indirection table for RSS context %d", context) + } + data, err := encodeNetDevRSSIndirectionTable(config.IndirectionTable) + if err != nil { + return nil, err + } + attrs = append(attrs, nl.NewRtAttr(nl.ETHTOOL_A_RSS_INDIR, data)) + } + if config.HashKey != nil { + if len(config.HashKey) == 0 { + return nil, fmt.Errorf("netlink: RSS hash key must not be empty") + } + if len(config.HashKey) > maxEthtoolAttrPayload { + return nil, fmt.Errorf("netlink: RSS hash key has %d bytes, maximum is %d", len(config.HashKey), maxEthtoolAttrPayload) + } + attrs = append(attrs, nl.NewRtAttr(nl.ETHTOOL_A_RSS_HKEY, append([]byte(nil), config.HashKey...))) + } + if config.InputTransformation != nil { + if *config.InputTransformation > NetDevRSSInputTransformationSymmetricORXOR { + return nil, fmt.Errorf("netlink: invalid RSS input transformation %#x", *config.InputTransformation) + } + attrs = append(attrs, nl.NewRtAttr(nl.ETHTOOL_A_RSS_INPUT_XFRM, nl.Uint32Attr(uint32(*config.InputTransformation)))) + } + return attrs, nil +} + +// NetDevRSSSet applies a partial update to an existing RSS context on ifIndex. +// Context zero selects the main RSS context. +func NetDevRSSSet(ifIndex int, context uint32, config NetDevRSSConfig) error { + return pkgHandle.NetDevRSSSet(ifIndex, context, config) +} + +// NetDevRSSSet applies a partial update to an existing RSS context on ifIndex. +// Context zero selects the main RSS context. +func (h *Handle) NetDevRSSSet(ifIndex int, context uint32, config NetDevRSSConfig) error { + attrs, err := newNetDevRSSSetAttrs(ifIndex, context, config) + if err != nil { + return err + } + _, err = h.ethtoolRequest(nl.ETHTOOL_MSG_RSS_SET, unix.NLM_F_ACK, attrs) + return err +} diff --git a/ethtool_rss_linux_test.go b/ethtool_rss_linux_test.go new file mode 100644 index 000000000..215704e30 --- /dev/null +++ b/ethtool_rss_linux_test.go @@ -0,0 +1,159 @@ +package netlink + +import ( + "reflect" + "syscall" + "testing" + + "github.com/vishvananda/netlink/nl" +) + +func TestParseNetDevRSS(t *testing.T) { + indir := append(nl.Uint32Attr(3), nl.Uint32Attr(1)...) + key := []byte{0xde, 0xad, 0xbe, 0xef} + attrs := []syscall.NetlinkRouteAttr{ + routeAttr(nl.ETHTOOL_A_RSS_CONTEXT, nl.Uint32Attr(4)), + routeAttr(nl.ETHTOOL_A_RSS_HFUNC, nl.Uint32Attr(uint32(NetDevRSSHashFunctionToeplitz))), + routeAttr(nl.ETHTOOL_A_RSS_INDIR, indir), + routeAttr(nl.ETHTOOL_A_RSS_HKEY, key), + routeAttr(nl.ETHTOOL_A_RSS_INPUT_XFRM, nl.Uint32Attr(uint32(NetDevRSSInputTransformationSymmetricXOR))), + } + + got, err := parseNetDevRSS(attrs, 4) + if err != nil { + t.Fatalf("parseNetDevRSS failed: %v", err) + } + want := &NetDevRSS{ + Context: 4, + HashFunction: NetDevRSSHashFunctionToeplitz, + IndirectionTable: []uint32{3, 1}, + HashKey: []byte{0xde, 0xad, 0xbe, 0xef}, + InputTransformation: NetDevRSSInputTransformationSymmetricXOR, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("RSS = %#v, want %#v", got, want) + } + + key[0] = 0 + if got.HashKey[0] != 0xde { + t.Fatal("parsed hash key aliases the netlink response buffer") + } +} + +func TestParseNetDevRSSRejectsMalformedIndirectionTable(t *testing.T) { + attrs := []syscall.NetlinkRouteAttr{ + routeAttr(nl.ETHTOOL_A_RSS_INDIR, []byte{1, 2, 3}), + } + if _, err := parseNetDevRSS(attrs, 0); err == nil { + t.Fatal("parseNetDevRSS accepted a malformed indirection table") + } +} + +func TestParseNetDevRSSContext(t *testing.T) { + rss, err := parseNetDevRSS(nil, 7) + if err != nil { + t.Fatalf("parseNetDevRSS without a context attribute failed: %v", err) + } + if rss.Context != 7 { + t.Fatalf("context = %d, want 7", rss.Context) + } + + attrs := []syscall.NetlinkRouteAttr{ + routeAttr(nl.ETHTOOL_A_RSS_CONTEXT, nl.Uint32Attr(8)), + } + if _, err := parseNetDevRSS(attrs, 7); err == nil { + t.Fatal("parseNetDevRSS accepted a mismatched context") + } +} + +func TestNewNetDevRSSSetAttrs(t *testing.T) { + hashFunction := NetDevRSSHashFunctionToeplitz + inputTransformation := NetDevRSSInputTransformationSymmetricORXOR + attrs, err := newNetDevRSSSetAttrs(9, 12, NetDevRSSConfig{ + HashFunction: &hashFunction, + IndirectionTable: []uint32{2, 0, 1}, + HashKey: []byte{1, 2, 3, 4}, + InputTransformation: &inputTransformation, + }) + if err != nil { + t.Fatalf("newNetDevRSSSetAttrs failed: %v", err) + } + byType := attrsByType(parseSerializedAttrs(t, attrs)) + if len(byType) != 6 { + t.Fatalf("encoded %d attribute types, want 6", len(byType)) + } + if got := native.Uint32(byType[nl.ETHTOOL_A_RSS_CONTEXT].Value); got != 12 { + t.Errorf("context = %d, want 12", got) + } + if got := native.Uint32(byType[nl.ETHTOOL_A_RSS_HFUNC].Value); got != uint32(hashFunction) { + t.Errorf("hash function = %#x, want %#x", got, hashFunction) + } + indir := byType[nl.ETHTOOL_A_RSS_INDIR].Value + if got := []uint32{native.Uint32(indir[0:4]), native.Uint32(indir[4:8]), native.Uint32(indir[8:12])}; !reflect.DeepEqual(got, []uint32{2, 0, 1}) { + t.Errorf("indirection table = %v, want [2 0 1]", got) + } + if got := byType[nl.ETHTOOL_A_RSS_HKEY].Value; !reflect.DeepEqual(got, []byte{1, 2, 3, 4}) { + t.Errorf("hash key = %v, want [1 2 3 4]", got) + } + if got := native.Uint32(byType[nl.ETHTOOL_A_RSS_INPUT_XFRM].Value); got != uint32(inputTransformation) { + t.Errorf("input transformation = %#x, want %#x", got, inputTransformation) + } +} + +func TestNewNetDevRSSSetAttrsIndirectionTablePresence(t *testing.T) { + tests := []struct { + name string + table []uint32 + present bool + }{ + {name: "omitted", table: nil, present: false}, + {name: "reset", table: []uint32{}, present: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + attrs, err := newNetDevRSSSetAttrs(1, 0, NetDevRSSConfig{IndirectionTable: test.table}) + if err != nil { + t.Fatalf("newNetDevRSSSetAttrs failed: %v", err) + } + _, present := attrsByType(parseSerializedAttrs(t, attrs))[nl.ETHTOOL_A_RSS_INDIR] + if present != test.present { + t.Fatalf("indirection table present = %t, want %t", present, test.present) + } + }) + } +} + +func TestNewNetDevRSSSetAttrsRejectsInvalidValues(t *testing.T) { + zeroHash := NetDevRSSHashFunction(0) + combinedHash := NetDevRSSHashFunctionToeplitz | NetDevRSSHashFunctionXOR + largeHash := NetDevRSSHashFunction(0x100) + unknownHash := NetDevRSSHashFunction(0x08) + largeTransformation := NetDevRSSInputTransformation(3) + tests := []struct { + name string + config NetDevRSSConfig + }{ + {name: "zero hash function", config: NetDevRSSConfig{HashFunction: &zeroHash}}, + {name: "combined hash functions", config: NetDevRSSConfig{HashFunction: &combinedHash}}, + {name: "large hash function", config: NetDevRSSConfig{HashFunction: &largeHash}}, + {name: "unknown hash function", config: NetDevRSSConfig{HashFunction: &unknownHash}}, + {name: "empty hash key", config: NetDevRSSConfig{HashKey: []byte{}}}, + {name: "large hash key", config: NetDevRSSConfig{HashKey: make([]byte, maxEthtoolAttrPayload+1)}}, + {name: "large indirection table", config: NetDevRSSConfig{IndirectionTable: make([]uint32, maxEthtoolAttrPayload/4+1)}}, + {name: "input transformation", config: NetDevRSSConfig{InputTransformation: &largeTransformation}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := newNetDevRSSSetAttrs(1, 0, test.config); err == nil { + t.Fatal("accepted an invalid RSS configuration") + } + }) + } +} + +func TestNewNetDevRSSSetAttrsRejectsContextTableReset(t *testing.T) { + _, err := newNetDevRSSSetAttrs(1, 2, NetDevRSSConfig{IndirectionTable: []uint32{}}) + if err == nil { + t.Fatal("accepted an indirection table reset for an additional RSS context") + } +} diff --git a/nl/ethtool_linux.go b/nl/ethtool_linux.go new file mode 100644 index 000000000..9735f257f --- /dev/null +++ b/nl/ethtool_linux.go @@ -0,0 +1,64 @@ +package nl + +// Constants for the "ethtool" generic netlink family, mirroring +// include/uapi/linux/ethtool_netlink_generated.h. Only the subset used for +// ring parameters and RSS configuration is defined here. + +const ( + ETHTOOL_GENL_NAME = "ethtool" + ETHTOOL_GENL_VERSION = 1 +) + +// ethtool message commands. Values are explicit because RSS_SET was appended +// to the UAPI after RSS_GET. +const ( + ETHTOOL_MSG_RINGS_GET = 15 + ETHTOOL_MSG_RINGS_SET = 16 + ETHTOOL_MSG_RSS_GET = 38 + ETHTOOL_MSG_RSS_SET = 48 +) + +// Common request header attributes. +const ( + ETHTOOL_A_HEADER_UNSPEC = iota + ETHTOOL_A_HEADER_DEV_INDEX + ETHTOOL_A_HEADER_DEV_NAME + ETHTOOL_A_HEADER_FLAGS + ETHTOOL_A_HEADER_PHY_INDEX +) + +// Ring parameter attributes. +const ( + ETHTOOL_A_RINGS_UNSPEC = iota + ETHTOOL_A_RINGS_HEADER + ETHTOOL_A_RINGS_RX_MAX + ETHTOOL_A_RINGS_RX_MINI_MAX + ETHTOOL_A_RINGS_RX_JUMBO_MAX + ETHTOOL_A_RINGS_TX_MAX + ETHTOOL_A_RINGS_RX + ETHTOOL_A_RINGS_RX_MINI + ETHTOOL_A_RINGS_RX_JUMBO + ETHTOOL_A_RINGS_TX + ETHTOOL_A_RINGS_RX_BUF_LEN + ETHTOOL_A_RINGS_TCP_DATA_SPLIT + ETHTOOL_A_RINGS_CQE_SIZE + ETHTOOL_A_RINGS_TX_PUSH + ETHTOOL_A_RINGS_RX_PUSH + ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN + ETHTOOL_A_RINGS_TX_PUSH_BUF_LEN_MAX + ETHTOOL_A_RINGS_HDS_THRESH + ETHTOOL_A_RINGS_HDS_THRESH_MAX +) + +// RSS attributes. +const ( + ETHTOOL_A_RSS_UNSPEC = iota + ETHTOOL_A_RSS_HEADER + ETHTOOL_A_RSS_CONTEXT + ETHTOOL_A_RSS_HFUNC + ETHTOOL_A_RSS_INDIR + ETHTOOL_A_RSS_HKEY + ETHTOOL_A_RSS_INPUT_XFRM + ETHTOOL_A_RSS_START_CONTEXT + ETHTOOL_A_RSS_FLOW_HASH +)