diff --git a/ethtool_ntuple_linux.go b/ethtool_ntuple_linux.go new file mode 100644 index 000000000..b39b4de7a --- /dev/null +++ b/ethtool_ntuple_linux.go @@ -0,0 +1,465 @@ +package netlink + +import ( + "fmt" + "net" + "runtime" + "strings" + "unsafe" + + "golang.org/x/sys/unix" +) + +// ethtool RX flow classification (n-tuple) commands. These manage RX flow +// steering rules via the SIOCETHTOOL ioctl; the kernel does not expose them +// over ethtool netlink. See include/uapi/linux/ethtool.h. +const ( + ETHTOOL_GRXCLSRLCNT = 0x0000002e // get RX class rule count + ETHTOOL_GRXCLSRULE = 0x0000002f // get one RX classification rule + ETHTOOL_GRXCLSRLALL = 0x00000030 // get all RX classification rule locations + ETHTOOL_SRXCLSRLDEL = 0x00000031 // delete RX classification rule + ETHTOOL_SRXCLSRLINS = 0x00000032 // insert RX classification rule +) + +// Flow types for ethtoolRxFlowSpec.flowType (subset; enum in ethtool.h). +const ( + TCP_V4_FLOW = 0x01 + UDP_V4_FLOW = 0x02 + TCP_V6_FLOW = 0x05 + UDP_V6_FLOW = 0x06 + ETHER_FLOW = 0x12 +) + +// Special location values for rule insertion. +const ( + RX_CLS_LOC_ANY uint32 = 0xffffffff + RX_CLS_LOC_FIRST uint32 = 0xfffffffe + RX_CLS_LOC_LAST uint32 = 0xfffffffd +) + +// ETHTOOL_RX_FLOW_SPEC_RING masks the queue index out of ring_cookie (the low +// 32 bits); bits 32-39 hold an optional VF id which we leave zero. +const ethtoolRxFlowSpecRing = 0x00000000FFFFFFFF + +// NetDevRxFlow is a typed RX flow steering rule. Exactly one of the typed match +// fields (Ether, TCP4, ...) must be set; it identifies the flow to match and +// supplies both the value (h_u) and mask (m_u) halves of the rule. Queue is the +// target RX queue index that matching packets are delivered to (the "action"). +type NetDevRxFlow struct { + // Match is the flow matcher; its concrete type selects the flow type. + Match NetDevRxFlowMatch + // Queue is the destination RX queue index for matching packets. + Queue uint32 + // Location is the rule slot. Use RX_CLS_LOC_ANY to let the kernel pick; + // on insert the chosen location is returned. + Location uint32 +} + +// NetDevRxFlowMatch is implemented by the typed flow matchers. It produces the +// ethtool flow_type and the 52-byte value/mask union blobs. +type NetDevRxFlowMatch interface { + flowType() uint32 + // serialize returns the value (h_u) and mask (m_u) 52-byte union contents. + serialize() (val [52]byte, mask [52]byte) +} + +// EtherFlow matches on Ethernet header fields (ETHER_FLOW). A zero field in a +// mask means "don't care"; an all-ones mask means "must match exactly". This is +// the matcher used by the KubeVirt AF_XDP example (steer by destination MAC). +type EtherFlow struct { + SrcMAC, SrcMACMask net.HardwareAddr + DstMAC, DstMACMask net.HardwareAddr + EthProto, ProtoMask uint16 // EtherType, host byte order +} + +func (EtherFlow) flowType() uint32 { return ETHER_FLOW } + +func (f EtherFlow) serialize() (val [52]byte, mask [52]byte) { + // struct ethhdr { u8 h_dest[6]; u8 h_source[6]; __be16 h_proto; } + putMAC(val[0:6], f.DstMAC) + putMAC(val[6:12], f.SrcMAC) + // ethhdr.h_proto is __be16 (network order). + networkOrder.PutUint16(val[12:14], f.EthProto) + putMAC(mask[0:6], f.DstMACMask) + putMAC(mask[6:12], f.SrcMACMask) + networkOrder.PutUint16(mask[12:14], f.ProtoMask) + return val, mask +} + +// TCP4Flow and UDP4Flow match on IPv4 TCP/UDP 5-tuple fields. Addresses and +// ports are given in host byte order and serialized in network byte order as the kernel +// (struct ethtool_tcpip4_spec) expects. A zero mask field means "don't care". +type TCP4Flow struct{ TCPIP4Fields } +type UDP4Flow struct{ TCPIP4Fields } + +// TCPIP4Fields holds the IPv4 TCP/UDP match fields shared by TCP4Flow/UDP4Flow. +type TCPIP4Fields struct { + SrcIP, SrcIPMask net.IP // IPv4 + DstIP, DstIPMask net.IP + SrcPort, SrcPortMask uint16 + DstPort, DstPortMask uint16 +} + +func (TCP4Flow) flowType() uint32 { return TCP_V4_FLOW } +func (UDP4Flow) flowType() uint32 { return UDP_V4_FLOW } + +func (f TCP4Flow) serialize() ([52]byte, [52]byte) { return f.TCPIP4Fields.serialize() } +func (f UDP4Flow) serialize() ([52]byte, [52]byte) { return f.TCPIP4Fields.serialize() } + +func (f TCPIP4Fields) serialize() (val [52]byte, mask [52]byte) { + // struct ethtool_tcpip4_spec { __be32 ip4src; __be32 ip4dst; + // __be16 psrc; __be16 pdst; __u8 tos; } + putIP4(val[0:4], f.SrcIP) + putIP4(val[4:8], f.DstIP) + networkOrder.PutUint16(val[8:10], f.SrcPort) + networkOrder.PutUint16(val[10:12], f.DstPort) + putIP4(mask[0:4], f.SrcIPMask) + putIP4(mask[4:8], f.DstIPMask) + networkOrder.PutUint16(mask[8:10], f.SrcPortMask) + networkOrder.PutUint16(mask[10:12], f.DstPortMask) + return val, mask +} + +// ethtoolRxFlowSpec is the logical representation of struct +// ethtool_rx_flow_spec. It is encoded explicitly because the Linux 386 ABI +// aligns uint64 fields to 4 bytes while the other supported Linux ABIs align +// them to 8 bytes. +type ethtoolRxFlowSpec struct { + flowType uint32 + hU [52]byte + hExt [20]byte + mU [52]byte + mExt [20]byte + ringCookie uint64 + location uint32 +} + +// ethtoolRxnfc is the logical representation of struct ethtool_rxnfc. Its Go +// layout is not passed to the kernel; serializeEthtoolRxnfc produces the native +// UAPI byte layout instead. +type ethtoolRxnfc struct { + cmd uint32 + flowType uint32 + data uint64 + fs ethtoolRxFlowSpec + ruleCntOrRssCtx uint32 +} + +type ethtoolRxnfcLayout struct { + size int + ringCookieOffset int + locationOffset int + ruleCntOrRssCtxOffset int + ruleLocsOffset int +} + +const ( + ethtoolRxnfcCmdOffset = 0 + ethtoolRxnfcFlowTypeOffset = 4 + ethtoolRxnfcDataOffset = 8 + ethtoolRxnfcFlowSpecOffset = 16 + + ethtoolRxFlowSpecFlowTypeOffset = 0 + ethtoolRxFlowSpecHUOffset = 4 + ethtoolRxFlowSpecHExtOffset = 56 + ethtoolRxFlowSpecMUOffset = 76 + ethtoolRxFlowSpecMExtOffset = 128 +) + +var ( + // Linux UAPI layout used by all Go-supported Linux architectures except 386. + ethtoolRxnfcLayoutAligned8 = ethtoolRxnfcLayout{ + size: 192, + ringCookieOffset: 152, + locationOffset: 160, + ruleCntOrRssCtxOffset: 184, + ruleLocsOffset: 188, + } + // The i386 ABI gives uint64 fields only 4-byte alignment. + ethtoolRxnfcLayoutAligned4 = ethtoolRxnfcLayout{ + size: 180, + ringCookieOffset: 148, + locationOffset: 156, + ruleCntOrRssCtxOffset: 176, + ruleLocsOffset: 180, + } +) + +func nativeEthtoolRxnfcLayout() ethtoolRxnfcLayout { + if runtime.GOARCH == "386" { + return ethtoolRxnfcLayoutAligned4 + } + return ethtoolRxnfcLayoutAligned8 +} + +func serializeEthtoolRxnfc(nfc *ethtoolRxnfc, layout ethtoolRxnfcLayout, ruleCapacity uint32) ([]byte, error) { + bufLen := uint64(layout.size) + if ruleCapacity != 0 { + locsLen := uint64(layout.ruleLocsOffset) + uint64(ruleCapacity)*4 + if locsLen > bufLen { + bufLen = locsLen + } + } + if bufLen > uint64(^uint(0)>>1) { + return nil, fmt.Errorf("netlink: RX flow rule count %d is too large", ruleCapacity) + } + + buf := make([]byte, int(bufLen)) + native.PutUint32(buf[ethtoolRxnfcCmdOffset:], nfc.cmd) + native.PutUint32(buf[ethtoolRxnfcFlowTypeOffset:], nfc.flowType) + native.PutUint64(buf[ethtoolRxnfcDataOffset:], nfc.data) + + fs := buf[ethtoolRxnfcFlowSpecOffset:] + native.PutUint32(fs[ethtoolRxFlowSpecFlowTypeOffset:], nfc.fs.flowType) + copy(fs[ethtoolRxFlowSpecHUOffset:], nfc.fs.hU[:]) + copy(fs[ethtoolRxFlowSpecHExtOffset:], nfc.fs.hExt[:]) + copy(fs[ethtoolRxFlowSpecMUOffset:], nfc.fs.mU[:]) + copy(fs[ethtoolRxFlowSpecMExtOffset:], nfc.fs.mExt[:]) + native.PutUint64(fs[layout.ringCookieOffset:], nfc.fs.ringCookie) + native.PutUint32(fs[layout.locationOffset:], nfc.fs.location) + native.PutUint32(buf[layout.ruleCntOrRssCtxOffset:], nfc.ruleCntOrRssCtx) + return buf, nil +} + +func deserializeEthtoolRxnfc(nfc *ethtoolRxnfc, buf []byte, layout ethtoolRxnfcLayout) error { + if len(buf) < layout.size { + return fmt.Errorf("netlink: short RX flow classification response") + } + + nfc.cmd = native.Uint32(buf[ethtoolRxnfcCmdOffset:]) + nfc.flowType = native.Uint32(buf[ethtoolRxnfcFlowTypeOffset:]) + nfc.data = native.Uint64(buf[ethtoolRxnfcDataOffset:]) + + fs := buf[ethtoolRxnfcFlowSpecOffset:] + nfc.fs.flowType = native.Uint32(fs[ethtoolRxFlowSpecFlowTypeOffset:]) + copy(nfc.fs.hU[:], fs[ethtoolRxFlowSpecHUOffset:ethtoolRxFlowSpecHExtOffset]) + copy(nfc.fs.hExt[:], fs[ethtoolRxFlowSpecHExtOffset:ethtoolRxFlowSpecMUOffset]) + copy(nfc.fs.mU[:], fs[ethtoolRxFlowSpecMUOffset:ethtoolRxFlowSpecMExtOffset]) + copy(nfc.fs.mExt[:], fs[ethtoolRxFlowSpecMExtOffset:ethtoolRxFlowSpecMExtOffset+len(nfc.fs.mExt)]) + nfc.fs.ringCookie = native.Uint64(fs[layout.ringCookieOffset:]) + nfc.fs.location = native.Uint32(fs[layout.locationOffset:]) + nfc.ruleCntOrRssCtx = native.Uint32(buf[layout.ruleCntOrRssCtxOffset:]) + return nil +} + +func putMAC(dst []byte, mac net.HardwareAddr) { + if len(mac) >= 6 { + copy(dst, mac[:6]) + } +} + +func putIP4(dst []byte, ip net.IP) { + if ip4 := ip.To4(); ip4 != nil { + copy(dst, ip4) + } +} + +func validateHardwareAddr(field string, addr net.HardwareAddr) error { + if len(addr) != 0 && len(addr) != 6 { + return fmt.Errorf("netlink: %s must contain exactly 6 bytes", field) + } + return nil +} + +func validateIPv4(field string, ip net.IP) error { + if len(ip) != 0 && ip.To4() == nil { + return fmt.Errorf("netlink: %s must be an IPv4 address", field) + } + return nil +} + +func validateNetDevRxFlowMatch(match NetDevRxFlowMatch) error { + switch m := match.(type) { + case EtherFlow: + return validateEtherFlow(m) + case *EtherFlow: + if m == nil { + return fmt.Errorf("netlink: NetDevRxFlow.Match must be set") + } + return validateEtherFlow(*m) + case TCP4Flow: + return validateTCPIP4Fields("TCP4Flow", m.TCPIP4Fields) + case *TCP4Flow: + if m == nil { + return fmt.Errorf("netlink: NetDevRxFlow.Match must be set") + } + return validateTCPIP4Fields("TCP4Flow", m.TCPIP4Fields) + case UDP4Flow: + return validateTCPIP4Fields("UDP4Flow", m.TCPIP4Fields) + case *UDP4Flow: + if m == nil { + return fmt.Errorf("netlink: NetDevRxFlow.Match must be set") + } + return validateTCPIP4Fields("UDP4Flow", m.TCPIP4Fields) + default: + return fmt.Errorf("netlink: unsupported NetDevRxFlow.Match type %T", match) + } +} + +func validateEtherFlow(flow EtherFlow) error { + fields := []struct { + name string + addr net.HardwareAddr + }{ + {"EtherFlow.SrcMAC", flow.SrcMAC}, + {"EtherFlow.SrcMACMask", flow.SrcMACMask}, + {"EtherFlow.DstMAC", flow.DstMAC}, + {"EtherFlow.DstMACMask", flow.DstMACMask}, + } + for _, field := range fields { + if err := validateHardwareAddr(field.name, field.addr); err != nil { + return err + } + } + return nil +} + +func validateTCPIP4Fields(flowType string, fields TCPIP4Fields) error { + addresses := []struct { + name string + ip net.IP + }{ + {flowType + ".SrcIP", fields.SrcIP}, + {flowType + ".SrcIPMask", fields.SrcIPMask}, + {flowType + ".DstIP", fields.DstIP}, + {flowType + ".DstIPMask", fields.DstIPMask}, + } + for _, address := range addresses { + if err := validateIPv4(address.name, address.ip); err != nil { + return err + } + } + return nil +} + +// NetDevRxFlowInsert inserts (or updates) an RX flow steering rule on dev, +// directing matching packets to flow.Queue. It returns the rule location the +// kernel assigned. Requires CAP_NET_ADMIN. +// Equivalent to: ethtool --config-ntuple flow-type ... action +func NetDevRxFlowInsert(dev string, flow NetDevRxFlow) (uint32, error) { + if flow.Match == nil { + return 0, fmt.Errorf("netlink: NetDevRxFlow.Match must be set") + } + if err := validateNetDevRxFlowMatch(flow.Match); err != nil { + return 0, err + } + val, mask := flow.Match.serialize() + nfc := ethtoolRxnfc{ + cmd: ETHTOOL_SRXCLSRLINS, + fs: ethtoolRxFlowSpec{ + flowType: flow.Match.flowType(), + hU: val, + mU: mask, + ringCookie: uint64(flow.Queue) & ethtoolRxFlowSpecRing, + location: flow.Location, + }, + } + if err := ethtoolRxnfcIoctl(dev, &nfc); err != nil { + return 0, err + } + // On insert with RX_CLS_LOC_ANY the kernel writes back the chosen location. + return nfc.fs.location, nil +} + +// NetDevRxFlowDelete removes the RX flow steering rule at the given location. +func NetDevRxFlowDelete(dev string, location uint32) error { + nfc := ethtoolRxnfc{ + cmd: ETHTOOL_SRXCLSRLDEL, + fs: ethtoolRxFlowSpec{location: location}, + } + return ethtoolRxnfcIoctl(dev, &nfc) +} + +// NetDevRxFlowList returns the locations of all RX flow steering rules on dev. +func NetDevRxFlowList(dev string) ([]uint32, error) { + // First get the rule count. + cnt := ethtoolRxnfc{cmd: ETHTOOL_GRXCLSRLCNT} + if err := ethtoolRxnfcIoctl(dev, &cnt); err != nil { + return nil, err + } + n := cnt.ruleCntOrRssCtx + if n == 0 { + return nil, nil + } + layout := nativeEthtoolRxnfcLayout() + nfc := ethtoolRxnfc{ + cmd: ETHTOOL_GRXCLSRLALL, + ruleCntOrRssCtx: n, + } + buf, err := serializeEthtoolRxnfc(&nfc, layout, n) + if err != nil { + return nil, err + } + if err := ethtoolIoctl(dev, unsafe.Pointer(&buf[0])); err != nil { + return nil, err + } + return parseNetDevRxFlowLocations(buf, layout, n) +} + +func parseNetDevRxFlowLocations(buf []byte, layout ethtoolRxnfcLayout, capacity uint32) ([]uint32, error) { + if len(buf) < layout.size { + return nil, fmt.Errorf("netlink: short RX flow rule response") + } + n := native.Uint32(buf[layout.ruleCntOrRssCtxOffset:]) + if n > capacity { + return nil, fmt.Errorf("netlink: kernel returned %d RX flow rules, buffer holds %d", n, capacity) + } + locsOff := layout.ruleLocsOffset + if uint64(n) > uint64((len(buf)-locsOff)/4) { + return nil, fmt.Errorf("netlink: short RX flow rule location response") + } + locs := make([]uint32, n) + for i := uint32(0); i < n; i++ { + off := locsOff + int(i)*4 + locs[i] = native.Uint32(buf[off : off+4]) + } + return locs, nil +} + +// ethtoolRxnfcIoctl runs SIOCETHTOOL with a fixed-size ethtool_rxnfc argument. +func ethtoolRxnfcIoctl(dev string, nfc *ethtoolRxnfc) error { + layout := nativeEthtoolRxnfcLayout() + buf, err := serializeEthtoolRxnfc(nfc, layout, 0) + if err != nil { + return err + } + if err := ethtoolIoctl(dev, unsafe.Pointer(&buf[0])); err != nil { + return err + } + return deserializeEthtoolRxnfc(nfc, buf, layout) +} + +// ethtoolIoctl issues SIOCETHTOOL on dev with data pointing at an ethtool +// command struct (whose first u32 is the command). +func ethtoolIoctl(dev string, data unsafe.Pointer) error { + if err := validateNetDevName(dev); err != nil { + return err + } + fd, err := getSocketUDP() + if err != nil { + return err + } + defer unix.Close(fd) + + ifreq := &Ifreq{Data: uintptr(data)} + copy(ifreq.Name[:unix.IFNAMSIZ-1], dev) + _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(SIOCETHTOOL), + uintptr(unsafe.Pointer(ifreq))) + if errno != 0 { + return errno + } + return nil +} + +func validateNetDevName(dev string) error { + switch { + case dev == "": + return fmt.Errorf("netlink: device name must not be empty") + case strings.IndexByte(dev, 0) >= 0: + return fmt.Errorf("netlink: device name %q contains a NUL byte", dev) + case len(dev) >= unix.IFNAMSIZ: + return fmt.Errorf("netlink: device name %q exceeds %d bytes", dev, unix.IFNAMSIZ-1) + default: + return nil + } +} diff --git a/ethtool_ntuple_linux_test.go b/ethtool_ntuple_linux_test.go new file mode 100644 index 000000000..c9ab9b931 --- /dev/null +++ b/ethtool_ntuple_linux_test.go @@ -0,0 +1,331 @@ +package netlink + +import ( + "bytes" + "errors" + "net" + "syscall" + "testing" +) + +var ethtoolRxnfcLayoutTests = []struct { + name string + layout ethtoolRxnfcLayout + size int + ringCookieOffset int + locationOffset int + ruleCntOrRssCtxOffset int + ruleLocsOffset int +}{ + { + name: "8-byte-aligned", + layout: ethtoolRxnfcLayoutAligned8, + size: 192, + ringCookieOffset: 152, + locationOffset: 160, + ruleCntOrRssCtxOffset: 184, + ruleLocsOffset: 188, + }, + { + name: "4-byte-aligned", + layout: ethtoolRxnfcLayoutAligned4, + size: 180, + ringCookieOffset: 148, + locationOffset: 156, + ruleCntOrRssCtxOffset: 176, + ruleLocsOffset: 180, + }, +} + +func TestEthtoolRxnfcLayouts(t *testing.T) { + for _, tt := range ethtoolRxnfcLayoutTests { + t.Run(tt.name, func(t *testing.T) { + if tt.layout.size != tt.size || + tt.layout.ringCookieOffset != tt.ringCookieOffset || + tt.layout.locationOffset != tt.locationOffset || + tt.layout.ruleCntOrRssCtxOffset != tt.ruleCntOrRssCtxOffset || + tt.layout.ruleLocsOffset != tt.ruleLocsOffset { + t.Fatalf("layout = %+v, want size=%d ring_cookie=%d location=%d rule_cnt=%d rule_locs=%d", + tt.layout, tt.size, tt.ringCookieOffset, tt.locationOffset, + tt.ruleCntOrRssCtxOffset, tt.ruleLocsOffset) + } + + nfc := ethtoolRxnfc{ + cmd: ETHTOOL_SRXCLSRLINS, + flowType: UDP_V4_FLOW, + data: 0x0102030405060708, + fs: ethtoolRxFlowSpec{ringCookie: 9, location: RX_CLS_LOC_ANY}, + ruleCntOrRssCtx: 3, + } + buf, err := serializeEthtoolRxnfc(&nfc, tt.layout, 0) + if err != nil { + t.Fatal(err) + } + if len(buf) != tt.size { + t.Fatalf("encoded size = %d, want %d", len(buf), tt.size) + } + if got := native.Uint64(buf[ethtoolRxnfcDataOffset:]); got != nfc.data { + t.Errorf("data = %#x, want %#x", got, nfc.data) + } + fsOff := ethtoolRxnfcFlowSpecOffset + if got := native.Uint64(buf[fsOff+tt.ringCookieOffset:]); got != nfc.fs.ringCookie { + t.Errorf("ring_cookie = %d, want %d", got, nfc.fs.ringCookie) + } + if got := native.Uint32(buf[fsOff+tt.locationOffset:]); got != nfc.fs.location { + t.Errorf("location = %#x, want %#x", got, nfc.fs.location) + } + if got := native.Uint32(buf[tt.ruleCntOrRssCtxOffset:]); got != nfc.ruleCntOrRssCtx { + t.Errorf("rule count = %d, want %d", got, nfc.ruleCntOrRssCtx) + } + + native.PutUint32(buf[fsOff+tt.locationOffset:], 42) + native.PutUint32(buf[tt.ruleCntOrRssCtxOffset:], 2) + var decoded ethtoolRxnfc + if err := deserializeEthtoolRxnfc(&decoded, buf, tt.layout); err != nil { + t.Fatal(err) + } + if decoded.fs.location != 42 || decoded.ruleCntOrRssCtx != 2 { + t.Errorf("decoded location/count = %d/%d, want 42/2", + decoded.fs.location, decoded.ruleCntOrRssCtx) + } + if err := deserializeEthtoolRxnfc(&decoded, buf[:tt.size-1], tt.layout); err == nil { + t.Fatal("accepted a short fixed-size response") + } + }) + } +} + +// TestRxFlowSerializeTCP4 is the golden-bytes encode test (hardware-free). It +// builds the rxnfc for "tcp4, dst 10.0.0.5:80 -> queue 3" and asserts the +// significant bytes land at the exact kernel offsets with the correct (network) +// byte order. This is the primary correctness guard since netdevsim cannot +// exercise rxnfc end to end. +func TestRxFlowSerializeTCP4(t *testing.T) { + flow := NetDevRxFlow{ + Match: TCP4Flow{TCPIP4Fields{ + DstIP: net.IPv4(10, 0, 0, 5), + DstIPMask: net.IPv4(255, 255, 255, 255), + DstPort: 80, + DstPortMask: 0xffff, + }}, + Queue: 3, + Location: RX_CLS_LOC_ANY, + } + val, mask := flow.Match.serialize() + nfc := ethtoolRxnfc{ + cmd: ETHTOOL_SRXCLSRLINS, + fs: ethtoolRxFlowSpec{ + flowType: flow.Match.flowType(), + hU: val, + mU: mask, + ringCookie: uint64(flow.Queue), + location: flow.Location, + }, + } + for _, tt := range ethtoolRxnfcLayoutTests { + t.Run(tt.name, func(t *testing.T) { + b, err := serializeEthtoolRxnfc(&nfc, tt.layout, 0) + if err != nil { + t.Fatal(err) + } + fsOff := ethtoolRxnfcFlowSpecOffset + hUOff := fsOff + ethtoolRxFlowSpecHUOffset + ringCookieOff := fsOff + tt.ringCookieOffset + locationOff := fsOff + tt.locationOffset + + if got := native.Uint32(b[ethtoolRxnfcCmdOffset:]); got != ETHTOOL_SRXCLSRLINS { + t.Errorf("cmd = %#x, want %#x", got, ETHTOOL_SRXCLSRLINS) + } + if got := native.Uint32(b[fsOff:]); got != TCP_V4_FLOW { + t.Errorf("flow_type = %#x, want %#x", got, TCP_V4_FLOW) + } + // ethtool_tcpip4_spec: ip4src(4), ip4dst(4), psrc(2), pdst(2). + if got := b[hUOff+4 : hUOff+8]; !bytes.Equal(got, []byte{10, 0, 0, 5}) { + t.Errorf("ip4dst bytes = %v, want [10 0 0 5]", got) + } + if got := b[hUOff+10 : hUOff+12]; !bytes.Equal(got, []byte{0x00, 0x50}) { + t.Errorf("pdst bytes = %v, want [0 80]", got) + } + if got := native.Uint64(b[ringCookieOff:]); got != 3 { + t.Errorf("ring_cookie = %d, want 3", got) + } + if got := native.Uint32(b[locationOff:]); got != RX_CLS_LOC_ANY { + t.Errorf("location = %#x, want %#x", got, RX_CLS_LOC_ANY) + } + }) + } +} + +// TestRxFlowSerializeEther checks the ETHER_FLOW matcher used by the KubeVirt +// AF_XDP example (steer by destination MAC). +func TestRxFlowSerializeEther(t *testing.T) { + dst, _ := net.ParseMAC("02:00:00:00:00:01") + flow := NetDevRxFlow{ + Match: EtherFlow{ + DstMAC: dst, + DstMACMask: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + Queue: 7, + Location: RX_CLS_LOC_ANY, + } + val, mask := flow.Match.serialize() + nfc := ethtoolRxnfc{ + cmd: ETHTOOL_SRXCLSRLINS, + fs: ethtoolRxFlowSpec{ + flowType: flow.Match.flowType(), + hU: val, + mU: mask, + ringCookie: uint64(flow.Queue), + location: flow.Location, + }, + } + for _, tt := range ethtoolRxnfcLayoutTests { + t.Run(tt.name, func(t *testing.T) { + b, err := serializeEthtoolRxnfc(&nfc, tt.layout, 0) + if err != nil { + t.Fatal(err) + } + fsOff := ethtoolRxnfcFlowSpecOffset + hUOff := fsOff + ethtoolRxFlowSpecHUOffset + mUOff := fsOff + ethtoolRxFlowSpecMUOffset + ringCookieOff := fsOff + tt.ringCookieOffset + + if got := native.Uint32(b[fsOff:]); got != ETHER_FLOW { + t.Errorf("flow_type = %#x, want ETHER_FLOW %#x", got, ETHER_FLOW) + } + if got := b[hUOff : hUOff+6]; !bytes.Equal(got, dst) { + t.Errorf("h_dest = %v, want %v", got, dst) + } + if got := b[mUOff : mUOff+6]; !bytes.Equal(got, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) { + t.Errorf("h_dest mask = %v, want all-ones", got) + } + if got := native.Uint64(b[ringCookieOff:]); got != 7 { + t.Errorf("ring_cookie = %d, want 7", got) + } + }) + } +} + +func TestParseNetDevRxFlowLocations(t *testing.T) { + const capacity = uint32(3) + for _, tt := range ethtoolRxnfcLayoutTests { + t.Run(tt.name, func(t *testing.T) { + nfc := ethtoolRxnfc{ + cmd: ETHTOOL_GRXCLSRLALL, + ruleCntOrRssCtx: 2, + } + buf, err := serializeEthtoolRxnfc(&nfc, tt.layout, capacity) + if err != nil { + t.Fatal(err) + } + native.PutUint32(buf[tt.ruleLocsOffset:], 7) + native.PutUint32(buf[tt.ruleLocsOffset+4:], 9) + native.PutUint32(buf[tt.ruleLocsOffset+8:], 99) + + locs, err := parseNetDevRxFlowLocations(buf, tt.layout, capacity) + if err != nil { + t.Fatal(err) + } + if len(locs) != 2 || locs[0] != 7 || locs[1] != 9 { + t.Fatalf("locations = %v, want [7 9]", locs) + } + + native.PutUint32(buf[tt.ruleCntOrRssCtxOffset:], capacity+1) + if _, err := parseNetDevRxFlowLocations(buf, tt.layout, capacity); err == nil { + t.Fatal("accepted a returned rule count larger than the supplied buffer") + } + if _, err := parseNetDevRxFlowLocations(buf[:tt.size-1], tt.layout, capacity); err == nil { + t.Fatal("accepted a short RX flow rule response") + } + }) + } +} + +func TestValidateNetDevRxFlowMatch(t *testing.T) { + var nilEtherFlow *EtherFlow + tests := []struct { + name string + match NetDevRxFlowMatch + }{ + { + name: "short MAC", + match: EtherFlow{DstMAC: net.HardwareAddr{0x02}}, + }, + { + name: "long MAC", + match: EtherFlow{ + DstMAC: net.HardwareAddr{0x02, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + { + name: "IPv6 value in TCP4 flow", + match: TCP4Flow{TCPIP4Fields{ + DstIP: net.ParseIP("2001:db8::1"), + }}, + }, + { + name: "IPv6 mask in UDP4 flow", + match: UDP4Flow{TCPIP4Fields{ + DstIPMask: net.ParseIP("ffff:ffff:ffff:ffff::"), + }}, + }, + { + name: "typed nil matcher", + match: nilEtherFlow, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateNetDevRxFlowMatch(tt.match); err == nil { + t.Fatal("invalid matcher was accepted") + } + }) + } + + valid := []NetDevRxFlowMatch{EtherFlow{}, TCP4Flow{}, UDP4Flow{}} + for _, match := range valid { + if err := validateNetDevRxFlowMatch(match); err != nil { + t.Errorf("valid matcher %T was rejected: %v", match, err) + } + } +} + +func TestValidateNetDevName(t *testing.T) { + for _, dev := range []string{"", "1234567890123456", "lo\x00ignored"} { + if err := validateNetDevName(dev); err == nil { + t.Errorf("invalid device name %q was accepted", dev) + } + } + if err := validateNetDevName("123456789012345"); err != nil { + t.Errorf("valid 15-byte device name was rejected: %v", err) + } +} + +// TestRxFlowInsertReachesDriver confirms the ioctl is well-formed and dispatched +// to the driver. On a host without rxnfc support (e.g. loopback), the +// kernel returns EOPNOTSUPP - which proves the request reached the driver rather +// than being malformed. A nil error is also accepted and cleaned up. +// netdevsim does NOT implement rxnfc, so a full insert->list->delete round trip +// requires real hardware (bnxt/gve) and is not run here. +func TestRxFlowInsertReachesDriver(t *testing.T) { + t.Cleanup(setUpNetlinkTestWithLoopback(t)) + + location, err := NetDevRxFlowInsert("lo", NetDevRxFlow{ + Match: TCP4Flow{TCPIP4Fields{DstPort: 80, DstPortMask: 0xffff}}, + Queue: 0, + Location: RX_CLS_LOC_ANY, + }) + switch { + case err == nil: + t.Cleanup(func() { + if err := NetDevRxFlowDelete("lo", location); err != nil { + t.Errorf("failed to delete inserted rxnfc rule %d: %v", location, err) + } + }) + t.Logf("rxnfc insert on lo unexpectedly succeeded at location %d", location) + case errors.Is(err, syscall.EOPNOTSUPP), errors.Is(err, syscall.ENOTSUP): + t.Logf("rxnfc insert reached driver and was declined as expected: %v", err) + default: + t.Fatalf("rxnfc insert failed with an unexpected error (possible malformed ioctl): %v", err) + } +}