From 45afcbec756416f7cc76eee2764ada08ced93a8f Mon Sep 17 00:00:00 2001 From: Parth Sarthi Date: Tue, 5 May 2026 00:23:49 -0700 Subject: [PATCH] Nftables: Added support for bitwise op. - Fixed byte order for shift evaluation. - Added dump operation support. - Added/updated integration and unit tests. PiperOrigin-RevId: 910486129 --- pkg/abi/linux/nf_tables.go | 50 ++ pkg/tcpip/network/ipv4/ipv4.go | 4 +- pkg/tcpip/nftables/BUILD | 1 + pkg/tcpip/nftables/nft_bitwise.go | 185 ++++++- pkg/tcpip/nftables/nft_fib.go | 475 ++++++++++++++++++ pkg/tcpip/nftables/nft_lookup.go | 2 +- pkg/tcpip/nftables/nftables.go | 12 +- pkg/tcpip/nftables/nftables_test.go | 116 ++++- pkg/tcpip/nftables/nftables_types.go | 6 + pkg/tcpip/nftables/nftinterp.go | 2 +- pkg/tcpip/stack/packet_buffer.go | 5 + runsc/boot/loader.go | 4 +- test/nftables/nftables_validation.go | 212 +++++++- .../linux/socket_netlink_netfilter.cc | 229 +++++++++ 14 files changed, 1250 insertions(+), 53 deletions(-) create mode 100644 pkg/tcpip/nftables/nft_fib.go diff --git a/pkg/abi/linux/nf_tables.go b/pkg/abi/linux/nf_tables.go index 726f1f822c..7c9f05c670 100644 --- a/pkg/abi/linux/nf_tables.go +++ b/pkg/abi/linux/nf_tables.go @@ -420,6 +420,21 @@ const ( NFT_BITWISE_RSHIFT // right-shift operation ) +// Nf table bitwise expression netlink attributes. +// These correspond to enum values in include/uapi/linux/netfilter/nf_tables.h. +const ( + NFTA_BITWISE_UNSPEC uint16 = iota + NFTA_BITWISE_SREG + NFTA_BITWISE_DREG + NFTA_BITWISE_LEN + NFTA_BITWISE_MASK + NFTA_BITWISE_XOR + NFTA_BITWISE_OP + NFTA_BITWISE_DATA + __NFTA_BITWISE_MAX + NFTA_BITWISE_MAX = __NFTA_BITWISE_MAX - 1 +) + // Nf table route expression keys. // Used by the nft route operation to determine the routing data to retrieve. // These correspond to enum values in include/uapi/linux/netfilter/nf_tables.h. @@ -674,3 +689,38 @@ const ( NFTA_LOOKUP_MAX = __NFTA_LOOKUP_MAX - 1 ) const NFT_LOOKUP_F_INV = uint32(1 << 0) + +// NfTable fib expression netlink attributes. +// These correspond to enum values in include/uapi/linux/netfilter/nf_tables.h. +const ( + NFTA_FIB_UNSPEC uint16 = iota + NFTA_FIB_DREG + NFTA_FIB_RESULT + NFTA_FIB_FLAGS + __NFTA_FIB_MAX +) + +const NFTA_FIB_MAX = __NFTA_FIB_MAX - 1 + +// NfTable fib result types. +// These correspond to enum values in include/uapi/linux/netfilter/nf_tables.h. +const ( + NFT_FIB_RESULT_UNSPEC = iota + NFT_FIB_RESULT_OIF + NFT_FIB_RESULT_OIFNAME + NFT_FIB_RESULT_ADDRTYPE + __NFT_FIB_RESULT_MAX +) + +const NFT_FIB_RESULT_MAX = __NFT_FIB_RESULT_MAX - 1 + +// NfTable fib flags. +// These correspond to enum values in include/uapi/linux/netfilter/nf_tables.h. +const ( + NFTA_FIB_F_SADDR = 1 << 0 + NFTA_FIB_F_DADDR = 1 << 1 + NFTA_FIB_F_MARK = 1 << 2 + NFTA_FIB_F_IIF = 1 << 3 + NFTA_FIB_F_OIF = 1 << 4 + NFTA_FIB_F_PRESENT = 1 << 5 +) diff --git a/pkg/tcpip/network/ipv4/ipv4.go b/pkg/tcpip/network/ipv4/ipv4.go index f357eb48d9..56dcf3a4e5 100644 --- a/pkg/tcpip/network/ipv4/ipv4.go +++ b/pkg/tcpip/network/ipv4/ipv4.go @@ -970,8 +970,10 @@ func (e *endpoint) HandlePacket(pkt *stack.PacketBuffer) { } } + nicID := e.nic.ID() // Loopback traffic skips the prerouting chain. - inNicName := stk.FindNICNameFromID(e.nic.ID()) + inNicName := stk.FindNICNameFromID(nicID) + pkt.InputNICID = nicID if ok := stk.IPTables().CheckPrerouting(pkt, e, inNicName); !ok { // iptables is telling us to drop the packet. stats.IPTablesPreroutingDropped.Increment() diff --git a/pkg/tcpip/nftables/BUILD b/pkg/tcpip/nftables/BUILD index 50557561ae..abee79bb91 100644 --- a/pkg/tcpip/nftables/BUILD +++ b/pkg/tcpip/nftables/BUILD @@ -20,6 +20,7 @@ go_library( "nft_byteorder.go", "nft_comparison.go", "nft_counter.go", + "nft_fib.go", "nft_immediate.go", "nft_last.go", "nft_lookup.go", diff --git a/pkg/tcpip/nftables/nft_bitwise.go b/pkg/tcpip/nftables/nft_bitwise.go index 84d82adbde..d87f368623 100644 --- a/pkg/tcpip/nftables/nft_bitwise.go +++ b/pkg/tcpip/nftables/nft_bitwise.go @@ -17,10 +17,12 @@ package nftables import ( "encoding/binary" "fmt" + "math" "slices" "gvisor.dev/gvisor/pkg/abi/linux" - "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/marshal/primitive" + "gvisor.dev/gvisor/pkg/sentry/socket/netlink/nlmsg" "gvisor.dev/gvisor/pkg/syserr" ) @@ -67,16 +69,19 @@ type bitwise struct { } // newBitwiseBool creates a new bitwise boolean operation. -func newBitwiseBool(sreg, dreg uint8, mask, xor []byte) (*bitwise, *syserr.AnnotatedError) { +func newBitwiseBool(sreg, dreg uint8, mask, xor []byte, blen int) (*bitwise, *syserr.AnnotatedError) { if isVerdictRegister(sreg) || isVerdictRegister(dreg) { return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise operation does not support verdict register as source or destination register") } - blen := len(mask) - if blen != len(xor) { + l := len(mask) + if l != len(xor) { return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise boolean operation mask and xor data lengths must be the same") } + if l != blen { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise boolean operation invalid length") + } if blen > linux.NFT_REG_SIZE || (blen > linux.NFT_REG32_SIZE && (is4ByteRegister(sreg) || is4ByteRegister(dreg))) { - return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("bitwise boolean operation cannot use more than %d bytes", linux.NFT_REG_SIZE)) + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise boolean operation invalid length") } var err *syserr.AnnotatedError var sregIdx, dregIdx int @@ -86,7 +91,7 @@ func newBitwiseBool(sreg, dreg uint8, mask, xor []byte) (*bitwise, *syserr.Annot if dregIdx, err = regNumToIdx(dreg, blen); err != nil { return nil, err } - return &bitwise{sregIdx: sregIdx, dregIdx: dregIdx, bop: linux.NFT_BITWISE_BOOL, blen: blen, mask: mask, xor: xor}, nil + return &bitwise{sregIdx: sregIdx, dregIdx: dregIdx, bop: linux.NFT_BITWISE_BOOL, blen: blen, mask: mask[:blen], xor: xor[:blen]}, nil } // newBitwiseShift creates a new bitwise shift operation. @@ -140,11 +145,11 @@ func evaluateBitwiseLshift(sregBuf, dregBuf []byte, shift uint32) { // Extracts the 4-byte chunk from the source register, padding if necessary. var chunk uint32 if start+4 <= len(sregBuf) { - chunk = binary.BigEndian.Uint32(sregBuf[start:]) + chunk = binary.NativeEndian.Uint32(sregBuf[start:]) } else { var padded [4]byte copy(padded[:], sregBuf[start:]) - chunk = binary.BigEndian.Uint32(padded[:]) + chunk = binary.NativeEndian.Uint32(padded[:]) } // Does left shift, adds the carry, and calculates the new carry. @@ -154,10 +159,10 @@ func evaluateBitwiseLshift(sregBuf, dregBuf []byte, shift uint32) { // Stores the result in the destination register, using temporary buffer // if necessary. if start+4 <= len(dregBuf) { - binary.BigEndian.PutUint32(dregBuf[start:], res) + binary.NativeEndian.PutUint32(dregBuf[start:], res) } else { var padded [4]byte - binary.BigEndian.PutUint32(padded[:], res) + binary.NativeEndian.PutUint32(padded[:], res) copy(dregBuf[start:], padded[:]) } } @@ -172,11 +177,11 @@ func evaluateBitwiseRshift(sregBuf, dregBuf []byte, shift uint32) { // Extracts the 4-byte chunk from the source register, padding if necessary. var chunk uint32 if start+4 <= len(sregBuf) { - chunk = binary.BigEndian.Uint32(sregBuf[start:]) + chunk = binary.NativeEndian.Uint32(sregBuf[start:]) } else { var padded [4]byte copy(padded[:], sregBuf[start:]) - chunk = binary.BigEndian.Uint32(padded[:]) + chunk = binary.NativeEndian.Uint32(padded[:]) } // Does right shift, adds the carry, and calculates the new carry. @@ -186,10 +191,10 @@ func evaluateBitwiseRshift(sregBuf, dregBuf []byte, shift uint32) { // Stores the result in the destination register, using temporary buffer // if necessary. if start+4 <= len(dregBuf) { - binary.BigEndian.PutUint32(dregBuf[start:], res) + binary.NativeEndian.PutUint32(dregBuf[start:], res) } else { var padded [4]byte - binary.BigEndian.PutUint32(padded[:], res) + binary.NativeEndian.PutUint32(padded[:], res) copy(dregBuf[start:], padded[:]) } } @@ -218,13 +223,159 @@ func (op bitwise) GetExprName() string { return OpTypeBitwise.String() } -// TODO: b/452648112 - Implement dump for bitwise operation. func (op bitwise) Dump() ([]byte, *syserr.AnnotatedError) { - log.Warningf("Nftables: Dumping bitwise operation is not implemented") - return nil, nil + m := &nlmsg.Message{} + m.PutAttr(linux.NFTA_BITWISE_SREG, nlmsg.PutU32(formatRegIdxForDump(op.sregIdx))) + m.PutAttr(linux.NFTA_BITWISE_DREG, nlmsg.PutU32(formatRegIdxForDump(op.dregIdx))) + m.PutAttr(linux.NFTA_BITWISE_LEN, nlmsg.PutU32(uint32(op.blen))) + m.PutAttr(linux.NFTA_BITWISE_OP, nlmsg.PutU32(uint32(op.bop))) + + switch op.bop { + case linux.NFT_BITWISE_BOOL: + maskDump, err := dumpDataAttr(op.mask) + if err != nil { + return nil, err + } + m.PutAttr(linux.NFTA_BITWISE_MASK, primitive.AsByteSlice(maskDump)) + + xorDump, err := dumpDataAttr(op.xor) + if err != nil { + return nil, err + } + m.PutAttr(linux.NFTA_BITWISE_XOR, primitive.AsByteSlice(xorDump)) + case linux.NFT_BITWISE_LSHIFT, linux.NFT_BITWISE_RSHIFT: + shiftData := make([]byte, 4) + binary.NativeEndian.PutUint32(shiftData, op.shift) + dataDump, err := dumpDataAttr(shiftData) + if err != nil { + return nil, err + } + m.PutAttr(linux.NFTA_BITWISE_DATA, primitive.AsByteSlice(dataDump)) + } + + return m.Buffer(), nil } // checkCompatibility implements operation.checkCompatibility. func (op bitwise) checkCompatibility(cCtx *opCompatCtx) *syserr.AnnotatedError { return nil } + +// initBitwiseBool initializes a bitwise boolean operation. +// Ref: net/netfilter/nft_bitwise.c:nft_bitwise_init_bool() +func initBitwiseBool(maskAttrBytes, xorAttrBytes nlmsg.BytesView, l uint32, sreg, dreg uint8) (*bitwise, *syserr.AnnotatedError) { + maskAttrs, ok := NfParse(nlmsg.AttrsView(maskAttrBytes)) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse bitwise boolean operation mask attribute") + } + mask, err := parseDataAttrs(maskAttrs) + if err != nil { + return nil, err + } + xorAttrs, ok := NfParse(nlmsg.AttrsView(xorAttrBytes)) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse bitwise boolean operation xor attribute") + } + xor, err := parseDataAttrs(xorAttrs) + if err != nil { + return nil, err + } + + return newBitwiseBool(uint8(sreg), uint8(dreg), mask, xor, int(l)) +} + +// initBitwiseShift initializes a bitwise shift operation. +// Ref: net/netfilter/nft_bitwise.c:nft_bitwise_init_shift() +func initBitwiseShift(dataAttrBytes nlmsg.BytesView, l uint32, sreg, dreg uint8, bitwiseOp uint32) (*bitwise, *syserr.AnnotatedError) { + dataAttrs, ok := NfParse(nlmsg.AttrsView(dataAttrBytes)) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse bitwise shift operation data attribute") + } + shiftData, err := parseDataAttrs(dataAttrs) + if err != nil { + return nil, err + } + if len(shiftData) != 4 { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise shift operation data length must be 4 bytes") + } + shift := binary.NativeEndian.Uint32(shiftData[:4]) + return newBitwiseShift(uint8(sreg), uint8(dreg), int(l), shift, bitwiseOp == linux.NFT_BITWISE_RSHIFT) +} + +// bitwiseAttrPolicy is the policy for parsing the attributes of a bitwise operation. +// Ref: net/netfilter/nft_bitwise.c:nft_bitwise_policy +var bitwiseAttrPolicy = []NlaPolicy{ + linux.NFTA_BITWISE_SREG: {nlaType: linux.NLA_U32}, + linux.NFTA_BITWISE_DREG: {nlaType: linux.NLA_U32}, + linux.NFTA_BITWISE_LEN: {nlaType: linux.NLA_U32}, + linux.NFTA_BITWISE_MASK: {nlaType: linux.NLA_NESTED}, + linux.NFTA_BITWISE_XOR: {nlaType: linux.NLA_NESTED}, + linux.NFTA_BITWISE_OP: NlaPolicy{nlaType: linux.NLA_BE32, validator: AttrMaxValidator[uint32](math.MaxUint8)}, + linux.NFTA_BITWISE_DATA: {nlaType: linux.NLA_NESTED}, +} + +// initBitwise initializes a bitwise operation. +// Ref: net/netfilter/nft_bitwise.c:nft_bitwise_init() +func initBitwise(tab *Table, exprInfo ExprInfo) (*bitwise, *syserr.AnnotatedError) { + attrs, ok := NfParseWithOpts(exprInfo.ExprData, &NfParseOpts{ + Policy: bitwiseAttrPolicy, + }) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse bitwise expression data") + } + + blen, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_LEN, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse bitwise expression data length") + } + + sreg, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_SREG, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse bitwise expression source register") + } + + dreg, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_DREG, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse bitwise expression destination register") + } + + bitwiseOp, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_OP, attrs) + if !ok { + bitwiseOp = uint32(linux.NFT_BITWISE_BOOL) + } else { + switch int(bitwiseOp) { + case linux.NFT_BITWISE_BOOL, linux.NFT_BITWISE_LSHIFT, linux.NFT_BITWISE_RSHIFT: + default: + return nil, syserr.NewAnnotatedError(syserr.ErrNotSupported, "unsupported bitwise operation") + } + } + + data, dataOk := attrs[linux.NFTA_BITWISE_DATA] + mask, maskOk := attrs[linux.NFTA_BITWISE_MASK] + xor, xorOk := attrs[linux.NFTA_BITWISE_XOR] + + if bitwiseOp == linux.NFT_BITWISE_BOOL { + if dataOk { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise boolean operation cannot use data attribute") + } + + if !maskOk { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise boolean operation mask attribute is missing") + } + if !xorOk { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise boolean operation xor attribute is missing") + } + return initBitwiseBool(mask, xor, blen, uint8(sreg), uint8(dreg)) + } + + if bitwiseOp == linux.NFT_BITWISE_LSHIFT || bitwiseOp == linux.NFT_BITWISE_RSHIFT { + if maskOk || xorOk { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise shift operation cannot use mask or xor attribute") + } + if !dataOk { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "bitwise shift operation data attribute is missing") + } + return initBitwiseShift(data, blen, uint8(sreg), uint8(dreg), bitwiseOp) + } + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse bitwise expression operation") +} diff --git a/pkg/tcpip/nftables/nft_fib.go b/pkg/tcpip/nftables/nft_fib.go new file mode 100644 index 0000000000..33a8e1d93e --- /dev/null +++ b/pkg/tcpip/nftables/nft_fib.go @@ -0,0 +1,475 @@ +// Copyright 2026 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nftables + +import ( + "encoding/binary" + "fmt" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/socket/netlink/nlmsg" + "gvisor.dev/gvisor/pkg/syserr" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// fib is an operation that queries the routing table. +type fib struct { + result int + flags uint32 + dregIdx int +} + +// ipv4AddrType determines the address type of the given IPv4 address. +// nicID represents the constraint interface ID. +// rt is the route to 'addr' (if one was found). +func (op *fib) ipv4AddrType(st *stack.Stack, addr tcpip.Address, nicID tcpip.NICID, rt *stack.Route) uint32 { + switch { + case addr == header.IPv4Any || addr == header.IPv4Broadcast: + return uint32(linux.RTN_BROADCAST) + case header.IsV4MulticastAddress(addr): + return uint32(linux.RTN_MULTICAST) + case st.IsSubnetBroadcast(nicID, header.IPv4ProtocolNumber, addr): + return uint32(linux.RTN_BROADCAST) + case st.CheckLocalAddress(0 /*nicID*/, header.IPv4ProtocolNumber, addr) != 0: + // Pass 0 as NICID otherwise the CheckLocalAddress will always return RTN_LOCAL. + return uint32(linux.RTN_LOCAL) + case rt != nil: + return uint32(linux.RTN_UNICAST) + default: + return uint32(linux.RTN_UNREACHABLE) + } +} + +// ipv6AddrType determines the address type of the given IPv6 address. +// nicID represents the constraint interface ID. +// rt is the route to 'addr' (if one was found). +func (op *fib) ipv6AddrType(st *stack.Stack, addr tcpip.Address, nicID tcpip.NICID, rt *stack.Route) uint32 { + switch { + case header.IsV6MulticastAddress(addr): + return uint32(linux.RTN_MULTICAST) + case addr == header.IPv6Any: + // Matches Linux [net/ipv6/netfilter/nft_fib_ipv6.c]:[__nft_fib6_eval_type]() + return uint32(linux.RTN_UNSPEC) + case st.CheckLocalAddress(0 /*nicID*/, header.IPv6ProtocolNumber, addr) != 0: + // Pass 0 as NICID otherwise the CheckLocalAddress will always return RTN_LOCAL. + return uint32(linux.RTN_LOCAL) + case rt != nil: + return uint32(linux.RTN_UNICAST) + default: + return uint32(linux.RTN_UNREACHABLE) + } +} + +func (op *fib) getAddrType(netProto tcpip.NetworkProtocolNumber, st *stack.Stack, addr tcpip.Address, nicID tcpip.NICID, rt *stack.Route) uint32 { + switch netProto { + case header.IPv4ProtocolNumber: + return op.ipv4AddrType(st, addr, nicID, rt) + case header.IPv6ProtocolNumber: + return op.ipv6AddrType(st, addr, nicID, rt) + default: + return uint32(linux.RTN_UNSPEC) + } +} + +// Ref: net/netfilter/nft_fib.c +func (op *fib) storeResult(regs *registerSet, nicID tcpip.NICID, st *stack.Stack) { + // Just set the boolean result if the flag is set. + if op.flags&linux.NFTA_FIB_F_PRESENT != 0 { + if nicID != 0 { + regs.data[op.dregIdx] = uint8(1) + } else { + regs.data[op.dregIdx] = uint8(0) + } + return + } + + switch op.result { + case linux.NFT_FIB_RESULT_OIF: + binary.NativeEndian.PutUint32(regs.data[op.dregIdx:], uint32(nicID)) + case linux.NFT_FIB_RESULT_OIFNAME: + name := st.FindNICNameFromID(nicID) + startIdx, endIdx := op.dregIdx, op.dregIdx+linux.IFNAMSIZ + clear(regs.data[startIdx:endIdx]) + copy(regs.data[startIdx:endIdx], name) + } +} + +func (op *fib) validatePktHeader(pkt *stack.PacketBuffer) bool { + switch pkt.NetworkProtocolNumber { + case header.IPv4ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + if len(hdr) < header.IPv4MinimumSize { + return false + } + case header.IPv6ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + if len(hdr) < header.IPv6MinimumSize { + return false + } + } + return true +} + +func (op *fib) getSrcDstAddr(pkt *stack.PacketBuffer) (tcpip.Address, tcpip.Address, bool) { + switch pkt.NetworkProtocolNumber { + case header.IPv4ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + iph := header.IPv4(hdr) + return iph.SourceAddress(), iph.DestinationAddress(), true + case header.IPv6ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + iph := header.IPv6(hdr) + return iph.SourceAddress(), iph.DestinationAddress(), true + } + return tcpip.Address{}, tcpip.Address{}, false +} + +// getOrFindRoute returns a route from the stack. If the route is found in the +// evalCtx, it is returned directly. Otherwise, the route is found using +// FindRoute. +func (op *fib) getOrFindRoute(evalCtx opEvalCtx, srcAddr, dstAddr tcpip.Address, netProto tcpip.NetworkProtocolNumber, nicID tcpip.NICID, dAddr bool) (rt *stack.Route, release func(), err tcpip.Error) { + if dAddr && evalCtx.route != nil { + return evalCtx.route, func() {}, nil + } + rt, err = evalCtx.nftState.stack.FindRoute(nicID, srcAddr, dstAddr, netProto, false) + if err != nil { + return nil, func() {}, err + } + return rt, rt.Release, nil +} + +// Ref: net/ipv[4|6]/netfilter/nft_fib_ipv[4|6].c]:nft_fib[4|6]_eval() +func (op *fib) evaluateOIF(regs *registerSet, evalCtx opEvalCtx) { + hook := evalCtx.hook + pkt := evalCtx.pkt + st := evalCtx.nftState.stack + if hook == stack.NFPrerouting || hook == stack.NFInput || hook == stack.NFIngress { + nic, err := st.GetNICByID(pkt.InputNICID) + if err == nil && nic.IsLoopback() { + op.storeResult(regs, pkt.InputNICID, st) + return + } + } + + // When: + // 1. nicID == 0, means no OIF/IIF constraint was specified, and FIB will + // just deduce and store the interface from the route lookup. + // 2. nicID != 0, FIB will verify if the calculated route's interface matches + // this constraint, and only store the result if they match. + nicID := tcpip.NICID(0) + if op.flags&linux.NFTA_FIB_F_OIF != 0 { + if evalCtx.route != nil { + nicID = evalCtx.route.OutgoingNIC() + } + } else if op.flags&linux.NFTA_FIB_F_IIF != 0 { + nicID = pkt.InputNICID + } + + if !op.validatePktHeader(evalCtx.pkt) { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + srcAddr, dstAddr, ok := op.getSrcDstAddr(pkt) + if !ok { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + + dAddr := op.flags&linux.NFTA_FIB_F_DADDR != 0 + if !dAddr { + srcAddr, dstAddr = dstAddr, srcAddr + } else { + if evalCtx.hook == stack.NFForward && op.flags&linux.NFTA_FIB_F_IIF != 0 { + log.Warningf("fib FORWARD hook with IIF flag is not fully supported.") + // TODO: b/530282592 - In Linux, if hook is FORWARD and IIF is set, + // the lookup's input interface is set to the output interface. + // However, gVisor's FindRoute does not consider the input interface + // for finding routes. + } + } + addr := dstAddr + + netProto := pkt.NetworkProtocolNumber + switch netProto { + case header.IPv4ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + iph := header.IPv4(hdr) + if op.isV4Zeronet(iph.SourceAddress()) { + if iph.DestinationAddress() == header.IPv4Broadcast || header.IsV4LinkLocalMulticastAddress(iph.DestinationAddress()) { + op.storeResult(regs, pkt.NICID, st) + return + } + } + if srcAddr == header.IPv4Broadcast || header.IsV4MulticastAddress(srcAddr) || op.isV4Zeronet(srcAddr) { + srcAddr = tcpip.Address{} + } + case header.IPv6ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + iph := header.IPv6(hdr) + if op.ipv6SkipICMP(pkt, iph.SourceAddress(), iph.DestinationAddress()) { + op.storeResult(regs, pkt.InputNICID, st) + return + } + } + + // Set the default result to 0 now and only set the regs based on the + // the result of route lookup. + binary.NativeEndian.PutUint32(regs.data[op.dregIdx:], uint32(0)) + + rt, release, err := op.getOrFindRoute(evalCtx, srcAddr, dstAddr, netProto, nicID, dAddr) + defer release() + if err != nil { + return + } + + addrType := op.getAddrType(netProto, st, addr, nicID, rt) + if addrType == linux.RTN_LOCAL { + return + } + + // Just need to set the result to NICID if it is not 0. + if nicID == 0 { + op.storeResult(regs, rt.NICID(), st) + return + } + if rt.OutgoingNIC() == nicID { + op.storeResult(regs, nicID, st) + return + } +} + +// Ref: net/ipv6/netfilter/nft_fib_ipv6.c:nft_fib_v6_skip_icmpv6]() +func (op *fib) ipv6SkipICMP(pkt *stack.PacketBuffer, saddr, daddr tcpip.Address) bool { + if pkt.TransportProtocolNumber != header.ICMPv6ProtocolNumber { + return false + } + if saddr != header.IPv6Any { + return false + } + return header.IsV6LinkLocalUnicastAddress(daddr) || header.IsV6LinkLocalMulticastAddress(daddr) +} + +// Ref: include/linux/in.h:ipv4_is_zeronet() +func (op *fib) isV4Zeronet(addr tcpip.Address) bool { + slice := addr.AsSlice() + return slice[0] == 0 && slice[1] == 0 && slice[2] == 0 && slice[3] == 0 +} + +// Ref: net/ipv4/netfilter/nft_fib_ipv[4|6].c:nft_fib[4|6]_eval_type() +func (op *fib) evaluateAddrType(regs *registerSet, evalCtx opEvalCtx) { + pkt := evalCtx.pkt + if !op.validatePktHeader(pkt) { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + + srcAddr, dstAddr, ok := op.getSrcDstAddr(pkt) + if !ok { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + + dAddr := op.flags&linux.NFTA_FIB_F_DADDR != 0 + if !dAddr { + srcAddr, dstAddr = dstAddr, srcAddr + } + addr := dstAddr + + st := evalCtx.nftState.stack + netProto := pkt.NetworkProtocolNumber + + nicID := tcpip.NICID(0) + if op.flags&linux.NFTA_FIB_F_IIF != 0 { + nicID = pkt.InputNICID + } else if op.flags&linux.NFTA_FIB_F_OIF != 0 { + if evalCtx.route != nil { + nicID = evalCtx.route.OutgoingNIC() + } + } + + // Find route to determine address type. + rt, release, _ := op.getOrFindRoute(evalCtx, srcAddr, dstAddr, netProto, nicID, dAddr) + defer release() + + addrType := op.getAddrType(netProto, st, addr, nicID, rt) + binary.NativeEndian.PutUint32(regs.data[op.dregIdx:], addrType) +} + +// evaluate implements operation.evaluate. +// Ref: net/netfilter/nft_fib.c:nft_fib_eval() +func (op *fib) evaluate(regs *registerSet, evalCtx opEvalCtx) { + switch op.result { + case linux.NFT_FIB_RESULT_ADDRTYPE: + op.evaluateAddrType(regs, evalCtx) + default: + op.evaluateOIF(regs, evalCtx) + } +} + +// GetExprName implements operation's ExprName interface. +func (op *fib) GetExprName() string { + return OpTypeFIB.String() +} + +// Dump implements operation's Dump interface. +func (op *fib) Dump() ([]byte, *syserr.AnnotatedError) { + m := &nlmsg.Message{} + m.PutAttr(linux.NFTA_FIB_DREG, nlmsg.PutU32(formatRegIdxForDump(op.dregIdx))) + m.PutAttr(linux.NFTA_FIB_RESULT, nlmsg.PutU32(uint32(op.result))) + m.PutAttr(linux.NFTA_FIB_FLAGS, nlmsg.PutU32(op.flags)) + return m.Buffer(), nil +} + +// deepCopy implements operation's deepCopy interface. +func (op *fib) deepCopy() operation { + opCopy := &fib{} + opCopy.result = op.result + opCopy.flags = op.flags + opCopy.dregIdx = op.dregIdx + return opCopy +} + +// checkCompatibility implements operation.checkCompatibility. +// Ref: net/netfilter/nft_fib.c:nft_fib_validate() +func (op *fib) checkCompatibility(cCtx *opCompatCtx) *syserr.AnnotatedError { + c := cCtx.chain + if c == nil { + return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "cannot validate fib operation on a rule without a chain") + } + if c.baseChainInfo == nil { + // Accept case. + return nil + } + hook := c.baseChainInfo.Hook + switch op.result { + case linux.NFT_FIB_RESULT_OIF, linux.NFT_FIB_RESULT_OIFNAME: + switch hook { + case stack.NFPrerouting, stack.NFInput, stack.NFForward: + return nil + } + return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib result OIF/OIFNAME only valid in PREROUTING, INPUT, FORWARD") + case linux.NFT_FIB_RESULT_ADDRTYPE: + if op.flags&linux.NFTA_FIB_F_IIF != 0 { + switch hook { + case stack.NFPrerouting, stack.NFInput, stack.NFForward: + return nil + } + return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib result ADDRTYPE with IIF only valid in PREROUTING, INPUT, FORWARD") + } + switch hook { + case stack.NFInput, stack.NFOutput, stack.NFForward, stack.NFPrerouting, stack.NFPostrouting: + return nil + } + return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib result ADDRTYPE without IIF/OIF only valid in INPUT, OUTPUT, FORWARD, PREROUTING, POSTROUTING") + } + return nil +} + +// newFIB creates a new fib operation. +// Ref: net/netfilter/nft_fib.c:nft_fib_init() +func newFIB(result int, flags uint32, dreg uint8) (*fib, *syserr.AnnotatedError) { + if isVerdictRegister(dreg) { + return nil, syserr.NewAnnotatedError( + syserr.ErrInvalidArgument, "fib operation does not support verdict register as destination register", + ) + } + + if flags == 0 { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib flags cannot be zero") + } + + if flags&linux.NFTA_FIB_F_MARK != 0 { + log.Warningf("fib mark flag is not supported for routing lookup in gVisor") + } + + if (flags & (linux.NFTA_FIB_F_SADDR | linux.NFTA_FIB_F_DADDR)) == (linux.NFTA_FIB_F_SADDR | linux.NFTA_FIB_F_DADDR) { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib flags cannot have both SADDR and DADDR") + } + if (flags & (linux.NFTA_FIB_F_IIF | linux.NFTA_FIB_F_OIF)) == (linux.NFTA_FIB_F_IIF | linux.NFTA_FIB_F_OIF) { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib flags cannot have both IIF and OIF") + } + if (flags & (linux.NFTA_FIB_F_SADDR | linux.NFTA_FIB_F_DADDR)) == 0 { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib flags must have at least SADDR or DADDR") + } + + resLen := 0 + switch result { + case linux.NFT_FIB_RESULT_OIF: + if flags&linux.NFTA_FIB_F_OIF != 0 { + return nil, syserr.NewAnnotatedError( + syserr.ErrInvalidArgument, + "fib result OIF/OIFNAME cannot be used with OIF flag", + ) + } + resLen = 4 // size of int + case linux.NFT_FIB_RESULT_OIFNAME: + if flags&linux.NFTA_FIB_F_OIF != 0 { + return nil, syserr.NewAnnotatedError( + syserr.ErrInvalidArgument, + "fib result OIF/OIFNAME cannot be used with OIF flag", + ) + } + resLen = linux.IFNAMSIZ + case linux.NFT_FIB_RESULT_ADDRTYPE: + resLen = 4 // size of uint32 + default: + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("unknown fib result: %d", result)) + } + + dataLen := resLen + dregIdx, err := regNumToIdx(dreg, dataLen) + if err != nil { + return nil, err + } + + return &fib{result: result, flags: flags, dregIdx: dregIdx}, nil +} + +const nftFibFlagsAll = linux.NFTA_FIB_F_SADDR | linux.NFTA_FIB_F_DADDR | + linux.NFTA_FIB_F_MARK | linux.NFTA_FIB_F_IIF | linux.NFTA_FIB_F_OIF | + linux.NFTA_FIB_F_PRESENT + +// Ref: net/netfilter/nft_fib.c:nft_fib_policy +var nftFibPolicy = []NlaPolicy{ + {nlaType: linux.NLA_U32, validator: AttrMaskValidator[uint32](nftFibFlagsAll)}, + {nlaType: linux.NLA_U32}, + {nlaType: linux.NLA_U32}, +} + +// Ref: net/netfilter/nft_fib.c:nft_fib_init() +func initFIB(tab *Table, exprInfo ExprInfo) (operation, *syserr.AnnotatedError) { + attrs, ok := NfParseWithOpts(exprInfo.ExprData, &NfParseOpts{ + Policy: nftFibPolicy, + }) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse fib expression data") + } + dreg, ok := AttrNetToHost[uint32](linux.NFTA_FIB_DREG, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse NFTA_FIB_DREG") + } + result, ok := AttrNetToHost[uint32](linux.NFTA_FIB_RESULT, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse NFTA_FIB_RESULT") + } + flags, ok := AttrNetToHost[uint32](linux.NFTA_FIB_FLAGS, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse NFTA_FIB_FLAGS") + } + return newFIB(int(result), flags, uint8(dreg)) +} diff --git a/pkg/tcpip/nftables/nft_lookup.go b/pkg/tcpip/nftables/nft_lookup.go index d14ae96529..8f67bb2e79 100644 --- a/pkg/tcpip/nftables/nft_lookup.go +++ b/pkg/tcpip/nftables/nft_lookup.go @@ -83,7 +83,7 @@ func (op *lookupOp) evaluate(regs *registerSet, evalCtx opEvalCtx) { } func (op *lookupOp) GetExprName() string { - return "lookup" + return OpTypeLookup.String() } // Dump implements operation.Dump. diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index 363c59c3f9..62c3ed67ac 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -417,14 +417,14 @@ func (r *Rule) evaluate(regs *registerSet, evalCtx opEvalCtx) *syserr.AnnotatedE // NewNFTables creates a new NFTables state object using the given clock for // timing operations. // Note: Expects random number generator to be initialized with a seed. -func NewNFTables(clock tcpip.Clock, rng rand.RNG) *NFTables { +func NewNFTables(stack *stack.Stack, clock tcpip.Clock, rng rand.RNG) *NFTables { if clock == nil { panic("nftables state must be initialized with a non-nil clock") } if rng.Reader == nil { panic("nftables state must be initialized with a non-nil random number generator") } - return &NFTables{clock: clock, startTime: clock.Now(), rng: rng, tableHandleCounter: atomicbitops.Uint64{}, genid: 1} + return &NFTables{stack: stack, clock: clock, startTime: clock.Now(), rng: rng, tableHandleCounter: atomicbitops.Uint64{}, genid: 1} } // GetGenID returns the generation ID for the NFTables object. @@ -1381,6 +1381,10 @@ func (r *Rule) AddOpFromExprInfo(tab *Table, exprInfo ExprInfo) *syserr.Annotate if op, err = initPayload(tab, exprInfo); err != nil { return err } + case OpTypeBitwise: + if op, err = initBitwise(tab, exprInfo); err != nil { + return err + } case OpTypeMeta: if op, err = initMeta(tab, exprInfo); err != nil { return err @@ -1401,6 +1405,10 @@ func (r *Rule) AddOpFromExprInfo(tab *Table, exprInfo ExprInfo) *syserr.Annotate if op, err = initLookup(tab, exprInfo); err != nil { return err } + case OpTypeFIB: + if op, err = initFIB(tab, exprInfo); err != nil { + return err + } default: return syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("Nftables: Unknown expression type not found: %s", exprInfo.ExprName)) diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index bfc9c5483a..9ce7100592 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -15,6 +15,7 @@ package nftables import ( + "bytes" "encoding/binary" "fmt" "math" @@ -2213,63 +2214,63 @@ func TestEvaluateBitwise(t *testing.T) { // No nft binary commands were observed that directly used shift operations. { tname: "0 shift left for bitwise lshift", - op1: mustCreateImmediate(t, linux.NFT_REG32_01, numToBE(4783, 4), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG32_01, binary.NativeEndian.AppendUint32(nil, 4783), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG32_01, linux.NFT_REG32_01, 4, 0, false), - op3: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, numToBE(4783, 4)), + op3: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4783)), }, { tname: "0 shift right for bitwise rshift", - op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(4783, 4), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG_1, binary.NativeEndian.AppendUint32(nil, 4783), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG_1, linux.NFT_REG_1, 4, 0, true), - op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(4783, 4)), + op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4783)), }, { tname: "1-bit shift left for bitwise lshift", - op1: mustCreateImmediate(t, linux.NFT_REG_4, numToBE(4782, 4), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG_4, binary.NativeEndian.AppendUint32(nil, 4782), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG_4, linux.NFT_REG_4, 4, 1, false), - op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, numToBE(4782<<1, 4)), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4782<<1)), }, { tname: "1-bit shift right for bitwise rshift", - op1: mustCreateImmediate(t, linux.NFT_REG32_06, numToBE(4782, 4), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG32_06, binary.NativeEndian.AppendUint32(nil, 4782), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG32_06, linux.NFT_REG32_06, 4, 1, true), - op3: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, numToBE(4782>>1, 4)), + op3: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4782>>1)), }, { tname: "8-bit shift left for bitwise lshift", - op1: mustCreateImmediate(t, linux.NFT_REG_4, numToBE(4782, 4), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG_4, binary.NativeEndian.AppendUint32(nil, 4782), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG_4, linux.NFT_REG_4, 4, 8, false), - op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, numToBE(4782<<8, 4)), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4782<<8)), }, { tname: "8-bit shift right for bitwise rshift", - op1: mustCreateImmediate(t, linux.NFT_REG32_06, numToBE(4782, 4), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG32_06, binary.NativeEndian.AppendUint32(nil, 4782), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG32_06, linux.NFT_REG32_06, 4, 8, true), - op3: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, numToBE(4782>>8, 4)), + op3: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4782>>8)), }, { tname: "16-bit shift left for bitwise lshift", - op1: mustCreateImmediate(t, linux.NFT_REG_4, numToBE(0x45678910, 8), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG_4, append(binary.NativeEndian.AppendUint32(nil, 0), binary.NativeEndian.AppendUint32(nil, 0x45678910)...), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG_4, linux.NFT_REG_4, 8, 16, false), - op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, numToBE(0x45678910<<16, 8)), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, append(binary.NativeEndian.AppendUint32(nil, 0x00004567), binary.NativeEndian.AppendUint32(nil, 0x89100000)...)), }, { tname: "16-bit shift right for bitwise rshift", - op1: mustCreateImmediate(t, linux.NFT_REG32_06, numToBE(0x45678910, 4), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG32_06, binary.NativeEndian.AppendUint32(nil, 0x45678910), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG32_06, linux.NFT_REG32_06, 4, 16, true), - op3: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, numToBE(0x45678910>>16, 4)), + op3: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 0x45678910>>16)), }, { tname: "max-bit shift left for bitwise lshift", - op1: mustCreateImmediate(t, linux.NFT_REG32_03, numToBE(0x45678910, 4), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG32_03, binary.NativeEndian.AppendUint32(nil, 0x45678910), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG32_03, linux.NFT_REG_2, 4, bitshiftLimit-1, false), - op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, numToBE(0x45678910<<(bitshiftLimit-1), 4)), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 0)), // 0x45678910 << 31 in 32-bit is 0 }, { tname: "max-bit shift right for bitwise rshift", - op1: mustCreateImmediate(t, linux.NFT_REG_3, numToBE(0x45678910, 8), stack.NFVerdict{}), + op1: mustCreateImmediate(t, linux.NFT_REG_3, append(binary.NativeEndian.AppendUint32(nil, 0x45678910), binary.NativeEndian.AppendUint32(nil, 0)...), stack.NFVerdict{}), op2: mustCreateBitwiseShift(t, linux.NFT_REG_3, linux.NFT_REG_2, 8, bitshiftLimit-1, true), - op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, numToBE(0x45678910>>(bitshiftLimit-1), 8)), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, append(binary.NativeEndian.AppendUint32(nil, 0), binary.NativeEndian.AppendUint32(nil, 0x8ACF1220)...)), }, } { t.Run(test.tname, func(t *testing.T) { @@ -2407,7 +2408,7 @@ func TestEvaluateLast(t *testing.T) { // Sets up an NFTables object with a base chain and fake manual clock. fakeClock := faketime.NewManualClock() fixedRNG := rand.RNGFrom(&fixedReader{}) - nf := NewNFTables(fakeClock, fixedRNG) + nf := NewNFTables(nil /* stack */, fakeClock, fixedRNG) tab, err := nf.AddTable(arbitraryFamily, "test", false) if err != nil { t.Fatalf("unexpected error for AddTable: %v", err) @@ -2954,7 +2955,7 @@ func TestEvaluateMetaLoad(t *testing.T) { t.Run(test.tname, func(t *testing.T) { // Sets up an NFTables object with a base chain and fake manual clock. // Using Manual Clock sets time.Now to Unix Epoch which fixes rng seed! - nf := NewNFTables(fakeClock, rand.RNGFrom(&fixedReader{})) + nf := NewNFTables(nil /* stack */, fakeClock, rand.RNGFrom(&fixedReader{})) tab, err := nf.AddTable(arbitraryFamily, "test", false) if err != nil { @@ -3753,7 +3754,7 @@ func packetResultString(initial, final *stack.PacketBuffer) string { func newNFTablesStd() *NFTables { stdClock := tcpip.NewStdClock() fixedRNG := rand.RNGFrom(&fixedReader{}) - return NewNFTables(stdClock, fixedRNG) + return NewNFTables(nil /* stack */, stdClock, fixedRNG) } // mustCreateImmediate wraps the newImmediate function for brevity. @@ -3804,7 +3805,7 @@ func mustCreatePayloadSet(t *testing.T, base payloadBase, offset uint8, len uint // mustCreateBitwiseBool wraps the newBitwiseBool function for brevity. func mustCreateBitwiseBool(t *testing.T, sreg, dreg uint8, mask, xor []byte) *bitwise { - bit, err := newBitwiseBool(sreg, dreg, mask, xor) + bit, err := newBitwiseBool(sreg, dreg, mask, xor, len(mask)) if err != nil { t.Fatalf("failed to create bitwise bool: %v", err) } @@ -4433,10 +4434,71 @@ func TestDumpOperations(t *testing.T) { name: "bitwise", op: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_2, []byte{0xff}, []byte{0x00}), validate: func(dump []byte) error { - // TODO: b/452648112 - Implement validation for bitwise operation when dump is implemented. - if dump != nil { - return fmt.Errorf("unexpected dump: %v, want nil", dump) + attrs, ok := NfParse(dump) + if !ok { + return fmt.Errorf("failed to parse dumped attributes") + } + sreg, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_SREG, attrs) + if !ok { + return fmt.Errorf("failed to get sreg") + } + if sreg != linux.NFT_REG_1 { + return fmt.Errorf("unexpected sreg: %d, want %d", sreg, linux.NFT_REG_1) + } + dreg, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_DREG, attrs) + if !ok { + return fmt.Errorf("failed to get dreg") + } + if dreg != linux.NFT_REG_2 { + return fmt.Errorf("unexpected dreg: %d, want %d", dreg, linux.NFT_REG_2) + } + lenAttr, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_LEN, attrs) + if !ok { + return fmt.Errorf("failed to get len") } + if lenAttr != 1 { + return fmt.Errorf("unexpected len: %d, want %d", lenAttr, 1) + } + bop, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_OP, attrs) + if !ok { + return fmt.Errorf("failed to get op") + } + if bop != linux.NFT_BITWISE_BOOL { + return fmt.Errorf("unexpected op: %d, want %d", bop, linux.NFT_BITWISE_BOOL) + } + + maskAttr, ok := attrs[linux.NFTA_BITWISE_MASK] + if !ok { + return fmt.Errorf("failed to get mask") + } + maskAttrs, ok := NfParse(nlmsg.AttrsView(maskAttr)) + if !ok { + return fmt.Errorf("failed to parse mask") + } + maskValue, err := parseDataAttrs(maskAttrs) + if err != nil { + return fmt.Errorf("failed to parse mask data: %v", err) + } + if !bytes.Equal(maskValue, []byte{0xff}) { + return fmt.Errorf("unexpected mask value: %v, want %v", maskValue, []byte{0xff}) + } + + xorAttr, ok := attrs[linux.NFTA_BITWISE_XOR] + if !ok { + return fmt.Errorf("failed to get xor") + } + xorAttrs, ok := NfParse(nlmsg.AttrsView(xorAttr)) + if !ok { + return fmt.Errorf("failed to parse xor") + } + xorValue, err := parseDataAttrs(xorAttrs) + if err != nil { + return fmt.Errorf("failed to parse xor data: %v", err) + } + if !bytes.Equal(xorValue, []byte{0x00}) { + return fmt.Errorf("unexpected xor value: %v, want %v", xorValue, []byte{0x00}) + } + return nil }, }, diff --git a/pkg/tcpip/nftables/nftables_types.go b/pkg/tcpip/nftables/nftables_types.go index 5d85f40e72..8ef2daac86 100644 --- a/pkg/tcpip/nftables/nftables_types.go +++ b/pkg/tcpip/nftables/nftables_types.go @@ -261,6 +261,7 @@ type NFTables struct { connTrack *stack.ConnTrack // Conntrack object for tracking connections. connTrackReaper tcpip.Timer // Reaper timer for reaping timed out connections. natEnabled bool // Whether the nat module is enabled. + stack *stack.Stack // Parent stack object. } // Ensures NFTables implements the NFTablesInterface. @@ -754,6 +755,7 @@ var ( _ operation = (*metaSet)(nil) _ operation = (*natOp)(nil) _ operation = (*lookupOp)(nil) + _ operation = (*fib)(nil) ) // OpType represents the type of operation. @@ -784,6 +786,8 @@ const ( OpTypeNAT // OpTypeLookup is the lookup operation type. OpTypeLookup + // OpTypeFIB is the FIB operation type. + OpTypeFIB // OpTypeUnknown is the unknown operation type. OpTypeUnknown ) @@ -801,6 +805,7 @@ var opTypeStrings = []string{ OpTypeMeta: "meta", OpTypeNAT: "nat", OpTypeLookup: "lookup", + OpTypeFIB: "fib", OpTypeUnknown: "unknown", } @@ -1382,6 +1387,7 @@ func (nf *NFTables) DeepCopy() *NFTables { connTrack: nf.connTrack, connTrackReaper: nf.connTrackReaper, natEnabled: nf.natEnabled, + stack: nf.stack, } nftCopy.tableHandleCounter.Store(nf.tableHandleCounter.Load()) diff --git a/pkg/tcpip/nftables/nftinterp.go b/pkg/tcpip/nftables/nftinterp.go index f7c4ffe1f6..9a596a5549 100644 --- a/pkg/tcpip/nftables/nftinterp.go +++ b/pkg/tcpip/nftables/nftinterp.go @@ -626,7 +626,7 @@ func InterpretBitwiseBool(line string, lnIdx int) (operation, *syserr.AnnotatedE } // Create the operation with the specified arguments. - bitwiseBool, err := newBitwiseBool(sreg, dreg, mask, xor) + bitwiseBool, err := newBitwiseBool(sreg, dreg, mask, xor, len(mask)) if err != nil { return nil, err } diff --git a/pkg/tcpip/stack/packet_buffer.go b/pkg/tcpip/stack/packet_buffer.go index 5288802ee0..de8cc3ebd2 100644 --- a/pkg/tcpip/stack/packet_buffer.go +++ b/pkg/tcpip/stack/packet_buffer.go @@ -158,6 +158,10 @@ type PacketBuffer struct { // NICID is the ID of the last interface the network packet was handled at. NICID tcpip.NICID + // InputNICID is the ID of the interface that the network packet + // was received on. + InputNICID tcpip.NICID + // RXChecksumValidated indicates that checksum verification may be // safely skipped. RXChecksumValidated bool @@ -397,6 +401,7 @@ func (pk *PacketBuffer) Clone() *PacketBuffer { newPk.TransportProtocolNumber = pk.TransportProtocolNumber newPk.PktType = pk.PktType newPk.NICID = pk.NICID + newPk.InputNICID = pk.InputNICID newPk.RXChecksumValidated = pk.RXChecksumValidated newPk.NetworkPacketInfo = pk.NetworkPacketInfo newPk.tuple = pk.tuple diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index c9988d11b2..80507b67f3 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -843,7 +843,7 @@ func (l *Loader) ConfigureNetwork(s inet.Stack) error { eps.Stack.SetIPTables(netfilter.DefaultLinuxTables(eps.Stack.Clock(), eps.Stack.InsecureRNG())) } if nftables.IsNFTablesEnabled() && eps.Stack.NFTables() == nil { - eps.Stack.SetNFTables(nftables.NewNFTables(eps.Stack.Clock(), eps.Stack.SecureRNG())) + eps.Stack.SetNFTables(nftables.NewNFTables(eps.Stack, eps.Stack.Clock(), eps.Stack.SecureRNG())) } n := &Network{ Stack: eps.Stack, @@ -1781,7 +1781,7 @@ func (c *sandboxNetstackCreator) newEmptySandboxNetworkStack() (*netstack.Stack, }), c.uid.UniqueID()) if nftables.IsNFTablesEnabled() { - s.Stack.SetNFTables(nftables.NewNFTables(c.clock, s.Stack.SecureRNG())) + s.Stack.SetNFTables(nftables.NewNFTables(s.Stack, c.clock, s.Stack.SecureRNG())) } // Enable SACK Recovery. diff --git a/test/nftables/nftables_validation.go b/test/nftables/nftables_validation.go index 040ed618cb..b8d1bcda66 100644 --- a/test/nftables/nftables_validation.go +++ b/test/nftables/nftables_validation.go @@ -32,6 +32,7 @@ var validationTests = []TestCase{ &tcpDNAT{}, &tcpSNAT{}, &mapTest{}, + &fibTest{}, } func init() { @@ -62,8 +63,9 @@ func (*JumpAndDropAll) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool {"add", "chain", "inet", tableName, "FORWARDS_TO_DROP_CHAIN", "{ jump DROPS_ALL_CHAIN; }"}, // Create BASE_CHAIN with accept all policy. {"add", "chain", "inet", tableName, "BASE_CHAIN", "{ type filter hook input priority 0; policy accept; }"}, - // Add rule to BASE_CHAIN to jump to FORWARDS_TO_DROP_CHAIN. - {"add", "rule", "inet", tableName, "BASE_CHAIN", "jump", "FORWARDS_TO_DROP_CHAIN"}, + // Add rule to BASE_CHAIN to jump to FORWARDS_TO_DROP_CHAIN + // if dport(0x0961) & 0x0fff == 0x961. + {"add", "rule", "inet", tableName, "BASE_CHAIN", "udp", "dport", "&", "0x0fff", "==", "0x0961", "jump", "FORWARDS_TO_DROP_CHAIN"}, } // Run all the commands. for _, cmd := range cmds { @@ -699,3 +701,209 @@ func (t *mapTest) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { func (*mapTest) Timeout() time.Duration { return 30 * time.Second } + +// fibTest tests installs Nftables rules such that: +// 1. Incoming packets to port 9005 match a FIB lookup. +// 2. If matched, the packet is accepted. Otherwise dropped. +type fibTest struct{ containerCase } + +var _ TestCase = (*fibTest)(nil) + +func (*fibTest) Name() string { + return "fibTest" +} + +// ContainerAction implements TestCase.ContainerAction. +func (t *fibTest) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if ipv6 { + log.Warningf("fibTest is not supported for IPv6 yet.") + return nil + } + + // Find the interface dynamically. + targetIface, ok := netutils.GetNonLoopbackInterface() + if !ok { + return fmt.Errorf("no non-loopback interface found") + } + log.Infof("FIB test using interface: %s (Index: %d)", targetIface.Name, targetIface.Index) + + // install_rules + { + cmds := [][]string{ + {"add", "table", "inet", "filter"}, + // Policy set to drop by default; if FIB fails, packet should be dropped. + {"add", "chain", "inet", "filter", "input", "{ type filter hook input priority 0; policy drop; }"}, + // Accept connections to 8995 to bypass drop policy for the test setup sync + {"add", "rule", "inet", "filter", "input", "tcp", "dport", "8995", "accept"}, + // Try to trigger all the FIB paths. + {"add", "rule", "inet", "filter", "input", "udp", "dport", "9005", + "fib", "saddr", ".", "iif", "oif", fmt.Sprintf("%d", targetIface.Index), + "fib", "saddr", ".", "iif", "oifname", targetIface.Name, + "fib", "saddr", ".", "iif", "oif", "exists", + "fib", "saddr", ".", "iif", "oifname", "exists", + "fib", "saddr", "type", "unicast", + "fib", "daddr", "type", "local", + "accept"}, + } + + for _, cmd := range cmds { + if err := nftCmd(cmd); err != nil { + return fmt.Errorf("nft cmd: %v, failed with error: %v", cmd, err) + } + } + log.Infof("fibTest: NFT rules installed successfully") + } + + // Verify that the UDP packets from outside the container can reach port 9005. + // This verifies that the FIB rules are evaluated correctly. + udpListener, err := net.ListenPacket("udp", "0.0.0.0:9005") + if err != nil { + return fmt.Errorf("UDP net.ListenPacket failed on 9005: %v", err) + } + defer udpListener.Close() + log.Infof("fibTest: Listening for UDP on port 9005") + + udpErrCh := make(chan error, 1) + go func() { + buf := make([]byte, 1024) + for { + udpListener.SetReadDeadline(time.Now().Add(30 * time.Second)) + n, _, err := udpListener.ReadFrom(buf) + if err != nil { + udpErrCh <- fmt.Errorf("failed receiving remote UDP packet: %v", err) + return + } + if string(buf[:n]) == "remote_test" { + break + } + } + udpErrCh <- nil + }() + + // Sync setup with LocalAction. + { + syncListener, err := net.Listen("tcp", "0.0.0.0:8995") + if err != nil { + return fmt.Errorf("net.Listen failed on 8995: %v", err) + } + log.Infof("fibTest: Listening for sync connection on port 8995") + + syncErrCh := make(chan error, 1) + go func() { + defer syncListener.Close() + conn, err := syncListener.Accept() + if err != nil { + syncErrCh <- err + return + } + defer conn.Close() + syncErrCh <- nil + }() + + select { + case err := <-syncErrCh: + if err != nil { + return err + } + log.Infof("fibTest: Sync with LocalAction successful") + case <-time.After(15 * time.Second): + syncListener.Close() + return fmt.Errorf("timeout waiting for client TCP connection on 8995") + } + } + + // Wait for remote UDP + select { + case err := <-udpErrCh: + if err != nil { + return err + } + log.Infof("fibTest: Received remote UDP packet 'remote_test'") + case <-time.After(15 * time.Second): + return fmt.Errorf("timeout waiting for remote client UDP connection on 9005") + } + + // verify UDP negative logic (Local Loopback) + // Packets sent to itself (127.0.0.1) should fail the FIB interface check + // and be dropped by the default policy. + log.Infof("fibTest: Starting negative test (Local Loopback to 127.0.0.1:9005)") + conn, err := net.Dial("udp", "127.0.0.1:9005") + if err != nil { + return fmt.Errorf("local UDP dial failed: %v", err) + } + + if _, err := conn.Write([]byte("local_test")); err != nil { + conn.Close() + return fmt.Errorf("failed to write local UDP packet: %v", err) + } + conn.Close() + + // Read loop to verify local_test doesn't arrive. + buf := make([]byte, 1024) + udpListener.SetReadDeadline(time.Now().Add(1 * time.Second)) + for { + n, _, err := udpListener.ReadFrom(buf) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + log.Infof("fibTest: Success! Local UDP packet was dropped (Timed out waiting for packet)") + break + } + return fmt.Errorf("unexpected error waiting for local UDP: %v", err) + } + if string(buf[:n]) == "local_test" { + return fmt.Errorf("local UDP transmission to 9005 succeeded when it should have been dropped") + } + } + return nil +} + +// LocalAction implements TestCase.LocalAction. +func (t *fibTest) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if ipv6 { + return nil + } + + // 1. Sync with ContainerAction by connecting to port 8995. + log.Infof("fibTest (LocalAction): Attempting to sync with container on port 8995") + { + addr := net.JoinHostPort(ip.String(), "8995") + var err error + var conn net.Conn + for i := 0; i < 10; i++ { + conn, err = net.Dial("tcp", addr) + if err == nil { + conn.Close() + break + } + time.Sleep(1 * time.Second) + } + if err != nil { + return fmt.Errorf("sync dial failed: %v", err) + } + log.Infof("fibTest (LocalAction): Sync successful") + } + + // 2. Validate that remote UDP packets reach Container.. + log.Infof("fibTest (LocalAction): Sending remote UDP packets to container") + { + conn, err := net.Dial("udp", net.JoinHostPort(ip.String(), "9005")) + if err != nil { + return fmt.Errorf("udp dial failed: %v", err) + } + defer conn.Close() + + // Send multiple times to ensure delivery over UDP + for i := 0; i < 10; i++ { + conn.Write([]byte("remote_test")) + time.Sleep(1 * time.Second) + } + log.Infof("fibTest (LocalAction): Finished sending remote UDP packets") + } + + return nil +} + +// Timeout implements TestCase.Timeout. +func (*fibTest) Timeout() time.Duration { + return 90 * time.Second +} diff --git a/test/syscalls/linux/socket_netlink_netfilter.cc b/test/syscalls/linux/socket_netlink_netfilter.cc index 94ce9f9cb7..0d6203cb58 100644 --- a/test/syscalls/linux/socket_netlink_netfilter.cc +++ b/test/syscalls/linux/socket_netlink_netfilter.cc @@ -3983,6 +3983,235 @@ INSTANTIATE_TEST_SUITE_P( return info.param.test_name; }); +std::vector GetFibRuleTestParams() { + return { + RuleWithExprTestParams{ + .test_name = "MissingDreg", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "MissingResult", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "MissingFlags", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "BothSaddrDaddr", + .expr_name = "fib", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR | NFTA_FIB_F_DADDR), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "BothOifIif", + .expr_name = "fib", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF) + .U32Attr(NFTA_FIB_FLAGS, + NFTA_FIB_F_SADDR | NFTA_FIB_F_IIF | NFTA_FIB_F_OIF), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "InvalidResult", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, 999) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "ValidOIF", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR), + .expected_error_no = 0}, + RuleWithExprTestParams{ + .test_name = "ValidOIFNAME", + .expr_name = "fib", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIFNAME) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_DADDR | NFTA_FIB_F_IIF), + .expected_error_no = 0}, + RuleWithExprTestParams{ + .test_name = "ValidADDRTYPE", + .expr_name = "fib", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_ADDRTYPE) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR | NFTA_FIB_F_IIF), + .expected_error_no = 0}, + }; +} + +std::vector GetBitwiseRuleTestParams() { + uint8_t mask_val[4] = {0xaa, 0xbb, 0xcc, 0xdd}; + uint8_t xor_val[4] = {0x11, 0x22, 0x33, 0x44}; + uint8_t data_val[4] = {0x02, 0x00, 0x00, 0x00}; + + std::vector mask_nested = + NlNestedAttr().RawAttr(NFTA_DATA_VALUE, mask_val, 4).Build(); + std::vector xor_nested = + NlNestedAttr().RawAttr(NFTA_DATA_VALUE, xor_val, 4).Build(); + std::vector data_nested = + NlNestedAttr().RawAttr(NFTA_DATA_VALUE, data_val, 4).Build(); + + uint8_t short_mask_val[2] = {0xaa, 0xbb}; + std::vector short_mask_nested = + NlNestedAttr().RawAttr(NFTA_DATA_VALUE, short_mask_val, 2).Build(); + + return { + RuleWithExprTestParams{ + .test_name = "ValidBool", + .expr_name = "bitwise", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_BITWISE_SREG, NFT_REG_1) + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .U32Attr(NFTA_BITWISE_OP, NFT_BITWISE_BOOL) + .RawAttr(NFTA_BITWISE_MASK, mask_nested.data(), + mask_nested.size()) + .RawAttr(NFTA_BITWISE_XOR, xor_nested.data(), + xor_nested.size()), + .expected_error_no = 0}, + RuleWithExprTestParams{ + .test_name = "ValidLshift", + .expr_name = "bitwise", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_BITWISE_SREG, NFT_REG_1) + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .U32Attr(NFTA_BITWISE_OP, NFT_BITWISE_LSHIFT) + .RawAttr(NFTA_BITWISE_DATA, data_nested.data(), + data_nested.size()), + .expected_error_no = 0}, + RuleWithExprTestParams{ + .test_name = "LenMismatchMaskBool", + .expr_name = "bitwise", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_BITWISE_SREG, NFT_REG_1) + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .U32Attr(NFTA_BITWISE_OP, NFT_BITWISE_BOOL) + .RawAttr(NFTA_BITWISE_MASK, short_mask_nested.data(), + short_mask_nested.size()) + .RawAttr(NFTA_BITWISE_XOR, xor_nested.data(), + xor_nested.size()), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "LenMismatchDataShift", + .expr_name = "bitwise", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_BITWISE_SREG, NFT_REG_1) + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .U32Attr(NFTA_BITWISE_OP, NFT_BITWISE_LSHIFT) + .RawAttr(NFTA_BITWISE_DATA, short_mask_nested.data(), + short_mask_nested.size()), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "VerdictRegSreg", + .expr_name = "bitwise", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_BITWISE_SREG, NFT_REG_VERDICT) + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .U32Attr(NFTA_BITWISE_OP, NFT_BITWISE_BOOL) + .RawAttr(NFTA_BITWISE_MASK, mask_nested.data(), + mask_nested.size()) + .RawAttr(NFTA_BITWISE_XOR, xor_nested.data(), + xor_nested.size()), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "MissingSreg", + .expr_name = "bitwise", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .RawAttr(NFTA_BITWISE_MASK, mask_nested.data(), + mask_nested.size()) + .RawAttr(NFTA_BITWISE_XOR, xor_nested.data(), + xor_nested.size()), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "MissingMaskBool", + .expr_name = "bitwise", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_BITWISE_SREG, NFT_REG_1) + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .U32Attr(NFTA_BITWISE_OP, NFT_BITWISE_BOOL) + .RawAttr(NFTA_BITWISE_XOR, xor_nested.data(), + xor_nested.size()), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "HasDataBool", + .expr_name = "bitwise", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_BITWISE_SREG, NFT_REG_1) + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .U32Attr(NFTA_BITWISE_OP, NFT_BITWISE_BOOL) + .RawAttr(NFTA_BITWISE_MASK, mask_nested.data(), + mask_nested.size()) + .RawAttr(NFTA_BITWISE_XOR, xor_nested.data(), + xor_nested.size()) + .RawAttr(NFTA_BITWISE_DATA, data_nested.data(), + data_nested.size()), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "MissingDataShift", + .expr_name = "bitwise", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_BITWISE_SREG, NFT_REG_1) + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .U32Attr(NFTA_BITWISE_OP, NFT_BITWISE_LSHIFT), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "BothMaskAndShift", + .expr_name = "bitwise", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_BITWISE_SREG, NFT_REG_1) + .U32Attr(NFTA_BITWISE_DREG, NFT_REG_2) + .U32Attr(NFTA_BITWISE_LEN, 4) + .U32Attr(NFTA_BITWISE_OP, NFT_BITWISE_LSHIFT) + .RawAttr(NFTA_BITWISE_DATA, data_nested.data(), + data_nested.size()) + .RawAttr(NFTA_BITWISE_MASK, mask_nested.data(), + mask_nested.size()), + .expected_error_no = EINVAL}, + }; +} + +INSTANTIATE_TEST_SUITE_P( + BitwiseRuleTest, AddRuleWithExprTest, + /*param_generator=*/ValuesIn(GetBitwiseRuleTestParams()), + /*param_name_generator=*/ + [](const TestParamInfo& info) { + return info.param.test_name; + }); + } // namespace } // namespace testing