diff --git a/pkg/abi/linux/nf_tables.go b/pkg/abi/linux/nf_tables.go index 726f1f822c..4e278aeb98 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,117 @@ 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 +) + +// Nf table ct expression keys. +// These correspond to values in include/uapi/linux/netfilter/nf_tables.h. +const ( + NFT_CT_STATE = iota + NFT_CT_DIRECTION + NFT_CT_STATUS + NFT_CT_MARK + NFT_CT_SECMARK + NFT_CT_EXPIRATION + NFT_CT_HELPER + NFT_CT_L3PROTOCOL + NFT_CT_SRC + NFT_CT_DST + NFT_CT_PROTOCOL + NFT_CT_PROTO_SRC + NFT_CT_PROTO_DST + NFT_CT_LABELS + NFT_CT_PKTS + NFT_CT_BYTES + NFT_CT_AVGPKT + NFT_CT_ZONE + NFT_CT_EVENTMASK + NFT_CT_SRC_IP + NFT_CT_DST_IP + NFT_CT_SRC_IP6 + NFT_CT_DST_IP6 + NFT_CT_ID + __NFT_CT_MAX + NFT_CT_MAX = __NFT_CT_MAX - 1 +) + +// Nf table ct expression netlink attributes. +// These correspond to values in include/uapi/linux/netfilter/nf_tables.h. +const ( + NFTA_CT_UNSPEC uint16 = iota + NFTA_CT_DREG + NFTA_CT_KEY + NFTA_CT_DIRECTION + NFTA_CT_SREG + __NFTA_CT_MAX + NFTA_CT_MAX = __NFTA_CT_MAX - 1 +) + +type ConnTrackStateBit int + +const ( + IP_CT_ESTABLISHED ConnTrackStateBit = iota + IP_CT_RELATED + IP_CT_NEW + IP_CT_IS_REPLY + IP_CT_ESTABLISHED_REPLY = IP_CT_ESTABLISHED + IP_CT_IS_REPLY + IP_CT_RELATED_REPLY = IP_CT_RELATED + IP_CT_IS_REPLY + IP_CT_NUMBER = 5 + IP_CT_NEW_REPLY = IP_CT_NUMBER + IP_CT_UNTRACKED = 7 +) + +// Conntrack states. +const ( + NF_CT_STATE_INVALID_BIT = 1 << 0 + NF_CT_STATE_UNTRACKED_BIT = 1 << 6 +) + +// From include/net/netfilter/nf_conntrack_helper.h:NF_CT_HELPER_NAME_LEN +const NF_CT_HELPER_NAME_LEN = 16 + +// From include/net/netfilter/nf_conntrack_labels.h:NF_CT_LABELS_MAX_SIZE +const NF_CT_LABELS_MAX_SIZE = 16 + +// ConnTrackDirection corresponds to values in +// include/uapi/linux/netfilter/nf_conntrack_common.h. +type ConnTrackDir uint8 + +const ( + IP_CT_DIR_ORIGINAL ConnTrackDir = iota + IP_CT_DIR_REPLY + IP_CT_DIR_MAX +) diff --git a/pkg/sentry/socket/netlink/netfilter/protocol.go b/pkg/sentry/socket/netlink/netfilter/protocol.go index fad8e51a52..6631d0876e 100644 --- a/pkg/sentry/socket/netlink/netfilter/protocol.go +++ b/pkg/sentry/socket/netlink/netfilter/protocol.go @@ -846,7 +846,7 @@ func (p *Protocol) newRule(nft *nftables.NFTables, st *stack.Stack, attrs map[ui } for _, exprInfo := range exprInfos { - err = rule.AddOpFromExprInfo(tab, exprInfo) + err = rule.AddOpFromExprInfo(nft, tab, exprInfo) // TODO - b/434244017: Create a copy of nftables structure when modifying the table. // Because we will create a copy of the table, no cleanup is necessary on the error case. // The table will simply be reverted to the original state. diff --git a/pkg/sentry/socket/netlink/nlmsg/message.go b/pkg/sentry/socket/netlink/nlmsg/message.go index 3ccf03e310..43b6263eff 100644 --- a/pkg/sentry/socket/netlink/nlmsg/message.go +++ b/pkg/sentry/socket/netlink/nlmsg/message.go @@ -519,6 +519,11 @@ func HostToNetU64(v uint64) uint64 { return binary.BigEndian.Uint64(b[:]) } +// PutU8 returns a uint8 as a marshal.Marshallable. +func PutU8(val uint8) marshal.Marshallable { + return primitive.AllocateUint8(val) +} + // PutU16 converts a uint16 to network byte order and returns it as a // marshal.Marshallable. func PutU16(val uint16) marshal.Marshallable { 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..00118e66c6 100644 --- a/pkg/tcpip/nftables/BUILD +++ b/pkg/tcpip/nftables/BUILD @@ -20,6 +20,8 @@ go_library( "nft_byteorder.go", "nft_comparison.go", "nft_counter.go", + "nft_ct.go", + "nft_fib.go", "nft_immediate.go", "nft_last.go", "nft_lookup.go", @@ -56,6 +58,7 @@ go_library( "//pkg/tcpip/checksum", "//pkg/tcpip/header", "//pkg/tcpip/stack", + "//pkg/tcpip/transport/tcpconntrack", ], ) 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_ct.go b/pkg/tcpip/nftables/nft_ct.go new file mode 100644 index 0000000000..c7e1377636 --- /dev/null +++ b/pkg/tcpip/nftables/nft_ct.go @@ -0,0 +1,422 @@ +// 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" +) + +// ctKey is the key that determines the data to retrieve. +type ctKey int + +// ctKeyStrings represents the string representation of the key. +var ctKeyStrings = [...]string{ + linux.NFT_CT_STATE: "NFT_CT_STATE", + linux.NFT_CT_DIRECTION: "NFT_CT_DIRECTION", + linux.NFT_CT_STATUS: "NFT_CT_STATUS", + linux.NFT_CT_MARK: "NFT_CT_MARK", + linux.NFT_CT_SECMARK: "NFT_CT_SECMARK", + linux.NFT_CT_EXPIRATION: "NFT_CT_EXPIRATION", + linux.NFT_CT_HELPER: "NFT_CT_HELPER", + linux.NFT_CT_L3PROTOCOL: "NFT_CT_L3PROTOCOL", + linux.NFT_CT_SRC: "NFT_CT_SRC", + linux.NFT_CT_DST: "NFT_CT_DST", + linux.NFT_CT_PROTOCOL: "NFT_CT_PROTOCOL", + linux.NFT_CT_PROTO_SRC: "NFT_CT_PROTO_SRC", + linux.NFT_CT_PROTO_DST: "NFT_CT_PROTO_DST", + linux.NFT_CT_LABELS: "NFT_CT_LABELS", + linux.NFT_CT_PKTS: "NFT_CT_PKTS", + linux.NFT_CT_BYTES: "NFT_CT_BYTES", + linux.NFT_CT_AVGPKT: "NFT_CT_AVGPKT", + linux.NFT_CT_ZONE: "NFT_CT_ZONE", + linux.NFT_CT_EVENTMASK: "NFT_CT_EVENTMASK", + linux.NFT_CT_SRC_IP: "NFT_CT_SRC_IP", + linux.NFT_CT_DST_IP: "NFT_CT_DST_IP", + linux.NFT_CT_SRC_IP6: "NFT_CT_SRC_IP6", + linux.NFT_CT_DST_IP6: "NFT_CT_DST_IP6", + linux.NFT_CT_ID: "NFT_CT_ID", +} + +func (k ctKey) String() string { + if int(k) >= 0 && int(k) < len(ctKeyStrings) { + return ctKeyStrings[k] + } + return "UNKNOWN_CT_KEY" +} + +// ctGet loads conntrack data into a register. +type ctGet struct { + // key is the key to query conntrack for. + key uint32 + // dregIdx is the index of the destination register in registerSet.data. + dregIdx int + // dir is the reply or original direction. + dir linux.ConnTrackDir + // len is the length of data to copy. + len int +} + +// ctSet sets data from a register. +type ctSet struct { + // key is the key to set data for. + key uint32 + // sregIdx is the index of the source register in registerSet.data. + sregIdx int + // dir is the reply or original direction. + dir linux.ConnTrackDir + // len is the length of data to copy. + len int +} + +// ctInfoOpts returns the ConntrackInfoOpts for the current operation. +func (op *ctGet) ctInfoOpts(pkt *stack.PacketBuffer) stack.ConnTrackInfoOpts { + opts := stack.ConnTrackInfoOpts{} + switch op.dir { + case linux.IP_CT_DIR_ORIGINAL: + opts.UseReplyDir = false + case linux.IP_CT_DIR_REPLY: + opts.UseReplyDir = true + default: + opts.UseReplyDir = pkt.IsReplyPacket() + } + switch op.key { + case linux.NFT_CT_ID: + opts.FillPseudoID = true + case linux.NFT_CT_EXPIRATION: + opts.FillExpiration = true + case linux.NFT_CT_STATE: + opts.FillState = true + } + return opts +} + +// netProtoToNFProto converts a tcpip.NetworkProtocolNumber to a Linux NFPROTO constant. +func netProtoToNFProto(proto tcpip.NetworkProtocolNumber) uint8 { + switch proto { + case header.IPv4ProtocolNumber: + return linux.NFPROTO_IPV4 + case header.IPv6ProtocolNumber: + return linux.NFPROTO_IPV6 + default: + return linux.NFPROTO_UNSPEC + } +} + +// connTrackStateBitToState converts a Linux ConnTrackStateBit to a NF_CT_STATE bitmask. +func connTrackStateBitToState(stateBit linux.ConnTrackStateBit) uint32 { + if stateBit < 0 { + return linux.NF_CT_STATE_INVALID_BIT + } + if stateBit == linux.IP_CT_UNTRACKED { + return linux.NF_CT_STATE_UNTRACKED_BIT + } + return (1 << ((stateBit)%linux.IP_CT_IS_REPLY + 1)) +} + +// evaluate implements the operation interface. +// Ref: net/netfilter/nft_ct.c:nft_ct_get_eval() +func (op *ctGet) evaluate(regs *registerSet, evalCtx opEvalCtx) { + pkt := evalCtx.pkt + var ctInfo stack.ConnTrackInfo + ctOk := pkt.FillConnTrackInfo(op.ctInfoOpts(pkt), &ctInfo) + start := op.dregIdx + end := op.dregIdx + op.len + + if !ctOk { + // TODO: b/531808852 - Support notrack conntrack state. + if op.key == linux.NFT_CT_STATE { + // If conntrack is not enabled or failed, state is INVALID. + binary.NativeEndian.PutUint32(regs.data[start:end], uint32(linux.NF_CT_STATE_INVALID_BIT)) + return + } + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + + isIPv4 := ctInfo.NetProto == header.IPv4ProtocolNumber + isIPv6 := ctInfo.NetProto == header.IPv6ProtocolNumber + + // Set entire range to 0 first to handle padding. + clear(regs.data[start:end]) + + switch op.key { + case linux.NFT_CT_STATE: + state := connTrackStateBitToState(ctInfo.StateBit) + binary.NativeEndian.PutUint32(regs.data[start:end], state) + case linux.NFT_CT_DIRECTION: + regs.data[start] = byte(ctInfo.Direction) + case linux.NFT_CT_EXPIRATION: + binary.NativeEndian.PutUint32(regs.data[start:end], uint32(ctInfo.Expiration.Milliseconds())) + case linux.NFT_CT_ID: + binary.NativeEndian.PutUint32(regs.data[start:end], ctInfo.PseudoID) + case linux.NFT_CT_L3PROTOCOL: + regs.data[start] = byte(netProtoToNFProto(ctInfo.NetProto)) + case linux.NFT_CT_PROTOCOL: + regs.data[start] = byte(ctInfo.TransProto) + case linux.NFT_CT_SRC: + copy(regs.data[start:end], ctInfo.SrcAddr.AsSlice()) + case linux.NFT_CT_DST: + copy(regs.data[start:end], ctInfo.DstAddr.AsSlice()) + case linux.NFT_CT_PROTO_SRC: + binary.BigEndian.PutUint16(regs.data[start:start+2], ctInfo.SrcPort) + case linux.NFT_CT_PROTO_DST: + binary.BigEndian.PutUint16(regs.data[start:start+2], ctInfo.DstPort) + case linux.NFT_CT_SRC_IP: + if !isIPv4 { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + copy(regs.data[start:end], ctInfo.SrcAddr.AsSlice()) + case linux.NFT_CT_DST_IP: + if !isIPv4 { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + copy(regs.data[start:end], ctInfo.DstAddr.AsSlice()) + case linux.NFT_CT_SRC_IP6: + if !isIPv6 { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + copy(regs.data[start:end], ctInfo.SrcAddr.AsSlice()) + case linux.NFT_CT_DST_IP6: + if !isIPv6 { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + copy(regs.data[start:end], ctInfo.DstAddr.AsSlice()) + default: + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + } +} + +// GetExprName implements operation's ExprName interface. +func (op *ctGet) GetExprName() string { + return OpTypeCT.String() +} + +// checkCompatibility implements operation's checkCompatibility interface. +func (op *ctGet) checkCompatibility(_ *opCompatCtx) *syserr.AnnotatedError { + return nil +} + +// deepCopy implements operation's deepCopy interface. +func (op *ctGet) deepCopy() operation { + return &ctGet{ + key: op.key, + dregIdx: op.dregIdx, + dir: op.dir, + len: op.len, + } +} + +// Dump implements operation's Dump interface. +func (op *ctGet) Dump() ([]byte, *syserr.AnnotatedError) { + m := &nlmsg.Message{} + m.PutAttr(linux.NFTA_CT_KEY, nlmsg.PutU32(uint32(op.key))) + m.PutAttr(linux.NFTA_CT_DREG, nlmsg.PutU32(formatRegIdxForDump(op.dregIdx))) + if op.dir != linux.IP_CT_DIR_MAX { + m.PutAttr(linux.NFTA_CT_DIRECTION, nlmsg.PutU8(uint8(op.dir))) + } + return m.Buffer(), nil +} + +// Matches Linux net/netfilter/nft_ct.c:nft_ct_set_eval() +func (op *ctSet) evaluate(regs *registerSet, evalCtx opEvalCtx) { + // TODO: b/531808852 - Implement ct set operation. + log.Warningf("ctSet.evaluate is not implemented") + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} +} + +// GetExprName implements operation's ExprName interface. +func (op *ctSet) GetExprName() string { + return OpTypeCT.String() +} + +// Dump implements operation's Dump interface. +func (op *ctSet) Dump() ([]byte, *syserr.AnnotatedError) { + m := &nlmsg.Message{} + m.PutAttr(linux.NFTA_CT_KEY, nlmsg.PutU32(uint32(op.key))) + m.PutAttr(linux.NFTA_CT_SREG, nlmsg.PutU32(formatRegIdxForDump(op.sregIdx))) + if op.dir != linux.IP_CT_DIR_MAX { + m.PutAttr(linux.NFTA_CT_DIRECTION, nlmsg.PutU8(uint8(op.dir))) + } + return m.Buffer(), nil +} + +// checkCompatibility implements operation's checkCompatibility interface. +func (op *ctSet) checkCompatibility(_ *opCompatCtx) *syserr.AnnotatedError { + return nil +} + +// deepCopy implements operation's deepCopy interface. +func (op *ctSet) deepCopy() operation { + return &ctSet{ + key: op.key, + sregIdx: op.sregIdx, + dir: op.dir, + len: op.len, + } +} + +// initCTSet initializes a ct set operation. +func initCTSet(tab *Table, sreg uint8, attrs map[uint16]nlmsg.BytesView) (*ctSet, *syserr.AnnotatedError) { + return nil, syserr.NewAnnotatedError(syserr.ErrNotSupported, "ct set operation is not supported") +} + +// initCTGet initializes a ct get operation. +// Ref: net/netfilter/nft_ct.c:nft_ct_get_init() +func initCTGet(tab *Table, dreg uint8, attrs map[uint16]nlmsg.BytesView) (*ctGet, *syserr.AnnotatedError) { + key, ok := AttrNetToHost[uint32](linux.NFTA_CT_KEY, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse NFTA_CT_KEY attribute") + } + + // TODO: b/531808852 - Support these keys. + switch key { + case linux.NFT_CT_STATUS, linux.NFT_CT_SECMARK, linux.NFT_CT_MARK, linux.NFT_CT_EVENTMASK, + linux.NFT_CT_PKTS, linux.NFT_CT_BYTES, linux.NFT_CT_AVGPKT, linux.NFT_CT_ZONE: + + return nil, syserr.NewAnnotatedError( + syserr.ErrNotSupported, + fmt.Sprintf("ct key %d is not supported", key), + ) + } + + dirU8, dirOK := AttrNetToHost[uint8](linux.NFTA_CT_DIRECTION, attrs) + dir := linux.ConnTrackDir(dirU8) + switch key { + case linux.NFT_CT_DIRECTION, linux.NFT_CT_STATE, linux.NFT_CT_STATUS, linux.NFT_CT_MARK, + linux.NFT_CT_SECMARK, linux.NFT_CT_EXPIRATION, linux.NFT_CT_HELPER, linux.NFT_CT_LABELS, + linux.NFT_CT_ID: + + if dirOK { + return nil, syserr.NewAnnotatedError( + syserr.ErrInvalidArgument, + fmt.Sprintf("direction is not allowed for key %d", key), + ) + } + + case linux.NFT_CT_SRC, linux.NFT_CT_DST, + linux.NFT_CT_SRC_IP, linux.NFT_CT_DST_IP, linux.NFT_CT_SRC_IP6, linux.NFT_CT_DST_IP6, + linux.NFT_CT_PROTO_SRC, linux.NFT_CT_PROTO_DST: + + if !dirOK { + return nil, syserr.NewAnnotatedError( + syserr.ErrInvalidArgument, + fmt.Sprintf("direction is required for key %d", key), + ) + } + } + + if dirOK { + if dir != linux.IP_CT_DIR_ORIGINAL && dir != linux.IP_CT_DIR_REPLY { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "invalid ct direction") + } + } else { + dir = linux.IP_CT_DIR_MAX + } + + ok = false + len := 0 + switch key { + case linux.NFT_CT_DIRECTION, linux.NFT_CT_L3PROTOCOL, linux.NFT_CT_PROTOCOL: + len = 1 + case linux.NFT_CT_STATE, linux.NFT_CT_STATUS, linux.NFT_CT_MARK, linux.NFT_CT_SECMARK, linux.NFT_CT_EXPIRATION: + len = 4 + case linux.NFT_CT_LABELS: + len = linux.NF_CT_LABELS_MAX_SIZE + case linux.NFT_CT_HELPER: + len = linux.NF_CT_HELPER_NAME_LEN + case linux.NFT_CT_SRC, linux.NFT_CT_DST: + switch tab.afFilter.family { + case stack.IP: + len = 4 + case stack.IP6, stack.Inet: + len = 16 + default: + return nil, syserr.NewAnnotatedError( + syserr.ErrNotSupported, + fmt.Sprintf("ct key %v is not supported for family %v", key, tab.afFilter.family), + ) + } + case linux.NFT_CT_SRC_IP, linux.NFT_CT_DST_IP: + len = 4 + case linux.NFT_CT_SRC_IP6, linux.NFT_CT_DST_IP6: + len = 16 // len = sizeof_field(struct nf_conntrack_tuple, src.u.all); + case linux.NFT_CT_PROTO_SRC, linux.NFT_CT_PROTO_DST: + len = 2 + case linux.NFT_CT_BYTES, linux.NFT_CT_PKTS, linux.NFT_CT_AVGPKT, linux.NFT_CT_ZONE: + len = 8 + case linux.NFT_CT_ID: + len = 4 + default: + return nil, syserr.NewAnnotatedError( + syserr.ErrNotSupported, + fmt.Sprintf("ct key %d is not supported", key), + ) + } + + len = (len + 3) & ^3 // round up to nearest multiple of 4. + dregIdx, err := regNumToIdx(dreg, len) + if err != nil { + return nil, err + } + + return &ctGet{key: key, dregIdx: dregIdx, dir: dir, len: len}, nil +} + +// ctAttrPolicy is the policy for parsing ct expression attributes. +// Matches Linux net/netfilter/nft_ct.c:nft_ct_policy +var ctAttrPolicy = []NlaPolicy{ + linux.NFTA_CT_DREG: NlaPolicy{nlaType: linux.NLA_U32}, + linux.NFTA_CT_KEY: NlaPolicy{nlaType: linux.NLA_BE32, validator: AttrMaxValidator[uint32](255)}, + linux.NFTA_CT_DIRECTION: NlaPolicy{nlaType: linux.NLA_U8}, + linux.NFTA_CT_SREG: NlaPolicy{nlaType: linux.NLA_U32}, +} + +// initCT initializes a ct operation (either get or set). +// Ref: net/netfilter/nft_ct.c:nft_ct_init() +func initCT(tab *Table, exprInfo ExprInfo) (operation, *syserr.AnnotatedError) { + attrs, ok := NfParseWithOpts(exprInfo.ExprData, &NfParseOpts{ + Policy: ctAttrPolicy, + }) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "Failed to parse ct expression data") + } + + dreg, dregOK := AttrNetToHost[uint32](linux.NFTA_CT_DREG, attrs) + sreg, sregOK := AttrNetToHost[uint32](linux.NFTA_CT_SREG, attrs) + if dregOK && sregOK { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "ct expression cannot have both DREG and SREG") + } + + if dregOK { + return initCTGet(tab, uint8(dreg), attrs) + } + if sregOK { + return initCTSet(tab, uint8(sreg), attrs) + } + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "ct expression must have either DREG or SREG") +} 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/nft_meta.go b/pkg/tcpip/nftables/nft_meta.go index 6bfa313753..0e54f12f39 100644 --- a/pkg/tcpip/nftables/nft_meta.go +++ b/pkg/tcpip/nftables/nft_meta.go @@ -89,6 +89,8 @@ var metaDataLengths = map[metaKey]int{ linux.NFT_META_TIME_NS: 8, linux.NFT_META_TIME_DAY: 1, linux.NFT_META_TIME_HOUR: 4, + linux.NFT_META_OIFNAME: linux.IFNAMSIZ, + linux.NFT_META_IIFNAME: linux.IFNAMSIZ, } // validateMetaKey ensures the meta key is valid. @@ -97,7 +99,8 @@ func validateMetaKey(key metaKey) *syserr.AnnotatedError { case linux.NFT_META_LEN, linux.NFT_META_PROTOCOL, linux.NFT_META_NFPROTO, linux.NFT_META_L4PROTO, linux.NFT_META_SKUID, linux.NFT_META_SKGID, linux.NFT_META_RTCLASSID, linux.NFT_META_PKTTYPE, linux.NFT_META_PRANDOM, - linux.NFT_META_TIME_NS, linux.NFT_META_TIME_DAY, linux.NFT_META_TIME_HOUR: + linux.NFT_META_TIME_NS, linux.NFT_META_TIME_DAY, linux.NFT_META_TIME_HOUR, + linux.NFT_META_OIFNAME, linux.NFT_META_IIFNAME: return nil default: diff --git a/pkg/tcpip/nftables/nft_metaload.go b/pkg/tcpip/nftables/nft_metaload.go index 5e6cf6c37c..90101e5e6c 100644 --- a/pkg/tcpip/nftables/nft_metaload.go +++ b/pkg/tcpip/nftables/nft_metaload.go @@ -157,6 +157,26 @@ func (op metaLoad) evaluate(regs *registerSet, evalCtx opEvalCtx) { now := clock.Now() secs := now.Hour()*3600 + now.Minute()*60 + now.Second() target = binary.NativeEndian.AppendUint32(nil, uint32(secs)) + + // Output Interface Name (string, host order). + case linux.NFT_META_OIFNAME: + dst := regs.data[op.dregIdx:] + clear(dst[:linux.IFNAMSIZ]) + if evalCtx.route != nil { + nic := evalCtx.route.OutgoingNIC() + name := evalCtx.nftState.stack.FindNICNameFromID(nic) + copy(dst, name) + } + return + + // Input Interface Name (string, host order). + case linux.NFT_META_IIFNAME: + nic := pkt.InputNICID + name := evalCtx.nftState.stack.FindNICNameFromID(nic) + dst := regs.data[op.dregIdx:] + clear(dst[:linux.IFNAMSIZ]) + copy(dst, name) + return } // Breaks if could not retrieve meta data. diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index 363c59c3f9..d0cc992dee 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -195,7 +195,6 @@ func (nf *NFTables) getExtraEvaluators(family stack.AddressFamily, hook stack.NF return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("Nftables: failed to finalize connTrack for packet: %v", pkt)) } } - regs.verdict.Code = VC(linux.NFT_CONTINUE) return nil }}) } @@ -417,14 +416,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. @@ -1367,7 +1366,7 @@ func (r *Rule) addOperation(op operation) *syserr.AnnotatedError { } // AddOpFromExprInfo adds an operation to the rule given the expression information. -func (r *Rule) AddOpFromExprInfo(tab *Table, exprInfo ExprInfo) *syserr.AnnotatedError { +func (r *Rule) AddOpFromExprInfo(nf *NFTables, tab *Table, exprInfo ExprInfo) *syserr.AnnotatedError { // Centralized here so that operations can do their own validation when being created. var op operation var err *syserr.AnnotatedError @@ -1381,6 +1380,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 +1404,15 @@ 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 + } + case OpTypeCT: + if op, err = initCT(tab, exprInfo); err != nil { + return err + } + nf.InitConnTrackOnce() 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..ab354b7252 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,9 @@ var ( _ operation = (*metaSet)(nil) _ operation = (*natOp)(nil) _ operation = (*lookupOp)(nil) + _ operation = (*fib)(nil) + _ operation = (*ctGet)(nil) + _ operation = (*ctSet)(nil) ) // OpType represents the type of operation. @@ -784,6 +788,10 @@ const ( OpTypeNAT // OpTypeLookup is the lookup operation type. OpTypeLookup + // OpTypeFIB is the FIB operation type. + OpTypeFIB + // OpTypeCT is the conntrack operation type. + OpTypeCT // OpTypeUnknown is the unknown operation type. OpTypeUnknown ) @@ -801,6 +809,8 @@ var opTypeStrings = []string{ OpTypeMeta: "meta", OpTypeNAT: "nat", OpTypeLookup: "lookup", + OpTypeFIB: "fib", + OpTypeCT: "ct", OpTypeUnknown: "unknown", } @@ -1382,6 +1392,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/BUILD b/pkg/tcpip/stack/BUILD index 7e45919aad..7114a7e011 100644 --- a/pkg/tcpip/stack/BUILD +++ b/pkg/tcpip/stack/BUILD @@ -260,6 +260,7 @@ go_library( ], visibility = ["//visibility:public"], deps = [ + "//pkg/abi/linux", "//pkg/atomicbitops", "//pkg/buffer", "//pkg/ilist", diff --git a/pkg/tcpip/stack/conntrack.go b/pkg/tcpip/stack/conntrack.go index 667a59c0ed..890619be7a 100644 --- a/pkg/tcpip/stack/conntrack.go +++ b/pkg/tcpip/stack/conntrack.go @@ -21,6 +21,7 @@ import ( "sync" "time" + "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/hash/jenkins" @@ -176,6 +177,27 @@ func (cn *conn) timedOut(now tcpip.MonotonicTime) bool { return now.Sub(cn.lastUsed) > unestablishedTimeout } +// expiresIn returns the duration from now until the connection times out. +func (cn *conn) expiresIn() time.Duration { + var timeout time.Duration + var lastUsed tcpip.MonotonicTime + cn.stateMu.RLock() + state := cn.tcb.State() + lastUsed = cn.lastUsed + cn.stateMu.RUnlock() + if state == tcpconntrack.ResultAlive { + timeout = establishedTimeout + } else { + timeout = unestablishedTimeout + } + now := cn.ct.clock.NowMonotonic() + expires := timeout - now.Sub(lastUsed) + if expires < 0 { + return 0 + } + return expires +} + // update the connection tracking state. func (cn *conn) update(pkt *PacketBuffer, reply bool) { cn.stateMu.Lock() @@ -239,6 +261,11 @@ type ConnTrack struct { // window across save boundaries. seed uint32 + // nftIDSeed is a one-time random value initialized at stack startup + // and is used in the calculation of tuple IDs for nftables. + // It is immutable. + nftIDSeed uint32 + // clock provides timing used to determine conntrack reapings. clock tcpip.Clock // TODO(b/341946753): Restore when netstack is savable. @@ -534,6 +561,86 @@ func (ct *ConnTrack) connForTID(tid tupleID) *tuple { return bkt.connForTID(tid, ct.clock.NowMonotonic()) } +// ConnTrackInfo holds connection tracking information for a packet. +type ConnTrackInfo struct { + StateBit linux.ConnTrackStateBit + Direction linux.ConnTrackDir + SrcAddr tcpip.Address + DstAddr tcpip.Address + SrcPort uint16 + DstPort uint16 + NetProto tcpip.NetworkProtocolNumber + TransProto tcpip.TransportProtocolNumber + Expiration time.Duration + PseudoID uint32 + Bytes uint64 + Packets uint64 +} + +// ConnTrackInfoOpts holds options for GetConnTrackInfo. +type ConnTrackInfoOpts struct { + FillState bool + UseReplyDir bool + FillPseudoID bool + FillExpiration bool +} + +// FillConnTrackInfo fills connection tracking information for the connection. +func (cn *conn) FillConnTrackInfo(opts ConnTrackInfoOpts, info *ConnTrackInfo) bool { + stateBit := linux.ConnTrackStateBit(-1) + if opts.FillState { + cn.stateMu.RLock() + tcbState := cn.tcb.State() + cn.stateMu.RUnlock() + switch tcbState { + case tcpconntrack.ResultConnecting: + stateBit = linux.IP_CT_NEW + + case tcpconntrack.ResultAlive, tcpconntrack.ResultReset, + tcpconntrack.ResultClosedByOriginator, tcpconntrack.ResultClosedByResponder: + + if opts.UseReplyDir { + stateBit = linux.IP_CT_ESTABLISHED_REPLY + } else { + stateBit = linux.IP_CT_ESTABLISHED + } + case tcpconntrack.ResultDrop: + stateBit = -1 + } + } + + dir := linux.IP_CT_DIR_ORIGINAL + t := &cn.original + if opts.UseReplyDir { + t = &cn.reply + dir = linux.IP_CT_DIR_REPLY + } + tID := t.tupleID + + pID := uint32(0) + if opts.FillPseudoID { + // Generate a pseudo-ID similar to Linux nf_ct_get_id + pID = tupleHash(cn.original.tupleID, cn.ct.nftIDSeed) + } + + var expires time.Duration + if opts.FillExpiration { + expires = cn.expiresIn() + } + + info.StateBit = stateBit + info.Direction = dir + info.SrcAddr = tID.srcAddr + info.DstAddr = tID.dstAddr + info.SrcPort = tID.srcPortOrEchoRequestIdent + info.DstPort = tID.dstPortOrEchoReplyIdent + info.NetProto = tID.netProto + info.TransProto = tID.transProto + info.Expiration = expires + info.PseudoID = pID + return true +} + func (bkt *bucket) connForTID(tid tupleID, now tcpip.MonotonicTime) *tuple { bkt.mu.RLock() defer bkt.mu.RUnlock() @@ -627,8 +734,8 @@ func (ct *ConnTrack) bucket(id tupleID) int { return ct.bucketWithTableLength(id, len(ct.buckets)) } -func (ct *ConnTrack) bucketWithTableLength(id tupleID, tableLength int) int { - h := jenkins.Sum32(ct.seed) +func tupleHash(id tupleID, seed uint32) uint32 { + h := jenkins.Sum32(seed) h.Write(id.srcAddr.AsSlice()) h.Write(id.dstAddr.AsSlice()) shortBuf := make([]byte, 2) @@ -640,7 +747,12 @@ func (ct *ConnTrack) bucketWithTableLength(id tupleID, tableLength int) int { h.Write([]byte(shortBuf)) binary.LittleEndian.PutUint16(shortBuf, uint16(id.netProto)) h.Write([]byte(shortBuf)) - return int(h.Sum32()) % tableLength + return h.Sum32() +} + +func (ct *ConnTrack) bucketWithTableLength(id tupleID, tableLength int) int { + h := tupleHash(id, ct.seed) + return int(h) % tableLength } // reapUnused deletes timed out entries from the conntrack map. The rules for @@ -788,9 +900,10 @@ func NewConnTrack(clock tcpip.Clock, rng connTrackRNG, seed *uint32) *ConnTrack seed = &r } ct := &ConnTrack{ - clock: clock, - rng: rng, - seed: *seed, + clock: clock, + rng: rng, + seed: *seed, + nftIDSeed: rng.Uint32(), } ct.init() return ct diff --git a/pkg/tcpip/stack/packet_buffer.go b/pkg/tcpip/stack/packet_buffer.go index 5288802ee0..09af97a798 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 @@ -486,6 +491,24 @@ func (pk *PacketBuffer) IsConnTrackConfigured() bool { return pk.tuple != nil && pk.tuple.conn != nil } +// FillConnTrackInfo fills connection tracking information for the packet. +func (pk *PacketBuffer) FillConnTrackInfo(opts ConnTrackInfoOpts, info *ConnTrackInfo) bool { + t := pk.tuple + if t == nil || t.conn == nil { + return false + } + return t.conn.FillConnTrackInfo(opts, info) +} + +// IsReplyPacket returns whether the packet is a reply packet. +func (pk *PacketBuffer) IsReplyPacket() bool { + t := pk.tuple + if t == nil { + return false + } + return t.reply +} + // IsNATConfigured returns whether NAT is configured for this packet. func (pk *PacketBuffer) IsNATConfigured(nt NATType) bool { if !pk.IsConnTrackConfigured() { diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index b1cae7837e..f2ef9b6329 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -845,7 +845,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, @@ -1783,7 +1783,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/BUILD b/test/nftables/BUILD index f6c54e9fa3..4016096dbf 100644 --- a/test/nftables/BUILD +++ b/test/nftables/BUILD @@ -20,6 +20,7 @@ go_library( deps = [ "//pkg/log", "//test/netutils", + "@org_golang_x_sys//unix:go_default_library", ], ) diff --git a/test/nftables/nftables_validation.go b/test/nftables/nftables_validation.go index 040ed618cb..1ba962f37b 100644 --- a/test/nftables/nftables_validation.go +++ b/test/nftables/nftables_validation.go @@ -20,8 +20,10 @@ import ( "fmt" "net" "strings" + "syscall" "time" + "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/test/netutils" ) @@ -32,6 +34,8 @@ var validationTests = []TestCase{ &tcpDNAT{}, &tcpSNAT{}, &mapTest{}, + &fibTest{}, + &ctTest{}, } func init() { @@ -62,8 +66,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 +704,437 @@ 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, also validate iifname. + {"add", "rule", "inet", "filter", "input", "tcp", "dport", "8995", "meta", "iifname", targetIface.Name, "accept"}, + // Output Chain to validate oifname. + {"add", "chain", "inet", "filter", "output", "{ type filter hook output priority 0; policy drop; }"}, + {"add", "rule", "inet", "filter", "output", "meta", "oifname", "lo", "accept"}, + {"add", "rule", "inet", "filter", "output", "tcp", "sport", "8995", "meta", "oifname", targetIface.Name, "accept"}, + // Try to trigger all the FIB paths and validate iifname. + {"add", "rule", "inet", "filter", "input", "udp", "dport", "9005", + "meta", "iifname", targetIface.Name, + "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 +} + +// ctTest verifies conntrack functionality. +type ctTest struct{ containerCase } + +var _ TestCase = (*ctTest)(nil) + +func (*ctTest) Name() string { + return "ctTest" +} + +// ContainerAction implements TestCase.ContainerAction. +func (t *ctTest) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if ipv6 { + log.Warningf("ctTest is not supported for IPv6 yet.") + return nil + } + + // install_rules + { + cmds := [][]string{ + {"add", "table", "inet", "filter"}, + // Policy set to drop by default; if conntrack 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"}, + // Rule 1: Allow initial packet of a new connection. + {"add", "rule", "inet", "filter", "input", "tcp", "dport", "29008", + "ct", "state", "new", + "ct", "original", "protocol", "tcp", + "ct", "original", "proto-src", "29007", + "ct", "direction", "original", + "accept"}, + // Rule 2: Allow all subsequent packets for connections that have already + // been established or are related to them. + {"add", "rule", "inet", "filter", "input", "ct", "state", "established,related", "accept"}, + // Output Chain + {"add", "chain", "inet", "filter", "output", "{ type filter hook output priority 0; policy drop; }"}, + // Accept LocalAction sync replies. + {"add", "rule", "inet", "filter", "output", "tcp", "sport", "8995", "accept"}, + // Rule 3: Verify Reply packets on Output chain. + {"add", "rule", "inet", "filter", "output", "tcp", "sport", "29008", + "ct", "direction", "reply", + "ct", "reply", "proto-src", "29008", + "ct", "reply", "proto-dst", "29007", + "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("ctTest: NFT rules installed successfully") + } + + // Start listening on 29008. + l, err := net.Listen("tcp", "0.0.0.0:29008") + if err != nil { + return fmt.Errorf("net.Listen failed on 29008: %v", err) + } + defer l.Close() + log.Infof("ctTest: Listening for TCP on port 29008") + + errCh := make(chan error, 1) + go func() { + conn, err := l.Accept() + if err != nil { + errCh <- err + return + } + defer conn.Close() + buf := make([]byte, 1024) + conn.SetReadDeadline(time.Now().Add(30 * time.Second)) + n, err := conn.Read(buf) + if err != nil { + errCh <- err + return + } + if string(buf[:n]) == "remote_test" { + errCh <- nil + } else { + errCh <- fmt.Errorf("unexpected message: %s", string(buf[:n])) + } + }() + + // 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("ctTest: 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("ctTest: 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 TCP + select { + case err := <-errCh: + if err != nil { + return err + } + log.Infof("ctTest: Received remote TCP packet 'remote_test'") + case <-time.After(15 * time.Second): + return fmt.Errorf("timeout waiting for remote client TCP connection on 29008") + } + + return nil +} + +// dialTCPWithReuseAddr dials a TCP connection with the SO_REUSEADDR and SO_LINGER option set. +func dialTCPWithReuseAddr(ctx context.Context, localAddr, remoteAddr net.Addr) (net.Conn, error) { + d := net.Dialer{ + LocalAddr: localAddr, + Control: func(network, address string, c syscall.RawConn) error { + var err error + c.Control(func(fd uintptr) { + err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + }) + return err + }, + } + conn, err := d.DialContext(ctx, "tcp", remoteAddr.String()) + if err != nil { + return nil, err + } + conn.(*net.TCPConn).SetLinger(0) + return conn, nil +} + +// LocalAction implements TestCase.LocalAction. +func (t *ctTest) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if ipv6 { + // ctTest is not supported for IPv6 yet. + return nil + } + + // 1. Sync with ContainerAction setup. + { + log.Infof("ctTest (LocalAction): Waiting for sync...") + var conn net.Conn + var err error + for i := 0; i < 10; i++ { + conn, err = net.DialTimeout("tcp", net.JoinHostPort(ip.String(), "8995"), 2*time.Second) + if err == nil { + conn.Close() + break + } + time.Sleep(1 * time.Second) + } + if err != nil { + return fmt.Errorf("sync dial failed: %v", err) + } + log.Infof("ctTest (LocalAction): Sync successful") + } + + // 2. Connect using source port 29007 (allowed port). + log.Infof("ctTest (LocalAction): Sending positive TCP packets to container (port 29007 -> 29008)") + { + localAddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:29007") + if err != nil { + return fmt.Errorf("resolve local TCP addr failed: %v", err) + } + remoteAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(ip.String(), "29008")) + if err != nil { + return fmt.Errorf("resolve remote TCP addr failed: %v", err) + } + + dialCtx, dialCancel := context.WithTimeout(ctx, 5*time.Second) + defer dialCancel() + conn, err := dialTCPWithReuseAddr(dialCtx, localAddr, remoteAddr) + if err != nil { + return fmt.Errorf("positive TCP dial failed: %v", err) + } + defer conn.Close() + + if _, err := conn.Write([]byte("remote_test")); err != nil { + return fmt.Errorf("failed to write positive TCP payload: %v", err) + } + log.Infof("ctTest (LocalAction): Finished sending positive TCP packets") + } + + // 3. Negative Test: Connecting using ephemeral port should be dropped. + log.Infof("ctTest (LocalAction): Sending negative TCP packets to container (ephemeral port -> 29008)") + { + // Expect this to timeout or fail to connect because it won't match proto-src 29007. + conn, err := net.DialTimeout("tcp", net.JoinHostPort(ip.String(), "29008"), 5*time.Second) + if err == nil { + conn.Close() + return fmt.Errorf("TCP connection succeeded when it should have been dropped") + } + log.Infof("ctTest (LocalAction): Connection failed as expected: %v", err) + } + + return nil +} + +// Timeout implements TestCase.Timeout. +func (*ctTest) Timeout() time.Duration { + return 60 * time.Second +} diff --git a/test/syscalls/linux/socket_netlink_netfilter.cc b/test/syscalls/linux/socket_netlink_netfilter.cc index 94ce9f9cb7..ea3baec11e 100644 --- a/test/syscalls/linux/socket_netlink_netfilter.cc +++ b/test/syscalls/linux/socket_netlink_netfilter.cc @@ -3983,6 +3983,321 @@ 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; + }); + +std::vector GetCTRuleTestParams() { + return { + RuleWithExprTestParams{ + .test_name = "ValidGetState", + .expr_name = "ct", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_CT_DREG, NFT_REG_1) + .U32Attr(NFTA_CT_KEY, NFT_CT_STATE), + .expected_error_no = 0}, + RuleWithExprTestParams{.test_name = "ValidGetSrcWithDirection", + .expr_name = "ct", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_CT_DREG, NFT_REG_1) + .U32Attr(NFTA_CT_KEY, NFT_CT_SRC) + .U8Attr(NFTA_CT_DIRECTION, 0), + .expected_error_no = 0}, + RuleWithExprTestParams{ + .test_name = "MissingDregAndSreg", + .expr_name = "ct", + .expr_attrs = NlNestedAttr().U32Attr(NFTA_CT_KEY, NFT_CT_STATE), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "BothDregAndSreg", + .expr_name = "ct", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_CT_DREG, NFT_REG_1) + .U32Attr(NFTA_CT_SREG, NFT_REG_2) + .U32Attr(NFTA_CT_KEY, NFT_CT_STATE), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "MissingKey", + .expr_name = "ct", + .expr_attrs = NlNestedAttr().U32Attr(NFTA_CT_DREG, NFT_REG_1), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "DirectionNotAllowed", + .expr_name = "ct", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_CT_DREG, NFT_REG_1) + .U32Attr(NFTA_CT_KEY, NFT_CT_STATE) + .U8Attr(NFTA_CT_DIRECTION, 0), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "DirectionRequiredMissing", + .expr_name = "ct", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_CT_DREG, NFT_REG_1) + .U32Attr(NFTA_CT_KEY, NFT_CT_SRC), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{.test_name = "InvalidDirection", + .expr_name = "ct", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_CT_DREG, NFT_REG_1) + .U32Attr(NFTA_CT_KEY, NFT_CT_SRC) + .U8Attr(NFTA_CT_DIRECTION, 2), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "UnsupportedKeyMark", + .expr_name = "ct", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_CT_DREG, NFT_REG_1) + .U32Attr(NFTA_CT_KEY, NFT_CT_MARK), + .expected_error_no = ENOTSUP}, + RuleWithExprTestParams{ + .test_name = "SetOperationUnsupported", + .expr_name = "ct", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_CT_SREG, NFT_REG_1) + .U32Attr(NFTA_CT_KEY, NFT_CT_MARK), + .expected_error_no = ENOTSUP}, + RuleWithExprTestParams{.test_name = "InvalidKeyTooLarge", + .expr_name = "ct", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_CT_DREG, NFT_REG_1) + .U32Attr(NFTA_CT_KEY, 256), + .expected_error_no = EINVAL}, + }; +} + +INSTANTIATE_TEST_SUITE_P(CTRuleTest, AddRuleWithExprTest, + /*param_generator=*/ValuesIn(GetCTRuleTestParams()), + /*param_name_generator=*/ + [](const TestParamInfo& info) { + return info.param.test_name; + }); + } // namespace } // namespace testing