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 +)