diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index 95154a73f..dfd5c3d3e 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -54,11 +54,19 @@ import ( // TODO(b/345684870): Remove unused functions once initial implementation is // complete. -// Defines the default capacity for the slices of hook functions and rules. +// Defines general constants for the nftables interpreter. const ( - registersByteSize = 64 // 4 16-byte registers or 16 4-byte registers. - nestedJumpLimit = 16 // Maximum number of nested jumps allowed, - // corresponding to NFT_JUMP_STACK_SIZE in include/net/netfilter/nf_tables.h. + + // Number of bytes for 4 16-byte registers or 16 4-byte registers. + registersByteSize = 64 + + // Maximum number of nested jumps allowed, corresponding to + // NFT_JUMP_STACK_SIZE in include/net/netfilter/nf_tables.h. + nestedJumpLimit = 16 + + // Limit (exclusive) for number of buts that can be shifted for non-boolean + // bitwise operations. + bitshiftLimit = 32 ) // AddressFamily describes the 6 address families supported by nftables. @@ -570,6 +578,7 @@ var ( _ operation = (*comparison)(nil) _ operation = (*payloadLoad)(nil) _ operation = (*payloadSet)(nil) + _ operation = (*bitwise)(nil) ) // immediate is an operation that sets the data in a register. @@ -958,6 +967,180 @@ func (op payloadSet) evaluate(regs *registerSet, pkt *stack.PacketBuffer) { } } +// bitwiseOp is the bitwise operator for a bitwise operation. +// Note: corresponds to enum nft_bitwise_ops from +// include/uapi/linux/netfilter/nf_tables.h and uses the same constants. +type bitwiseOp int + +// String for bitwiseOp returns the string representation of the bitwise +// operator. +func (bop bitwiseOp) String() string { + switch bop { + case linux.NFT_BITWISE_BOOL: + return "bitwise boolean" + case linux.NFT_BITWISE_LSHIFT: + return "bitwise <<" + case linux.NFT_BITWISE_RSHIFT: + return "bitwise >>" + default: + panic(fmt.Sprintf("invalid bitwise operator: %d", int(bop))) + } +} + +// bitwise is an operation that performs bitwise math operations over data in +// a given register, storing the result in a destination register. +// Note: bitwise operations are not supported for the verdict register. +type bitwise struct { + sreg uint8 // Number of the source register. + dreg uint8 // Number of the destination register. + bop bitwiseOp // Bitwise operator to use. + blen uint8 // Number of bytes to apply bitwise operation to. + mask registerData // Mask to apply bitwise & for boolean operations (before ^). + xor registerData // Xor to apply bitwise ^ for boolean operations (after &). + shift uint32 // Shift to apply bitwise <> for non-boolean operations. + + // Note: Technically, the linux kernel has defined bool, lshift, and rshift + // as the 3 types of bitwise operations. However, we have not been able to + // observe the lshift or rshift operations used by the nft binary. Thus, we + // have no way to test the interpretation of these operations. Maintaining + // consistency with the linux kernel, we have fully implemented lshift and + // rshift, and We will leave the code here in case we are able to observe + // their use in the future (perhaps outside the nft binary debug output). +} + +// newBitwiseBool creates a new bitwise boolean operation. +func newBitwiseBool(sreg, dreg uint8, mask, xor []byte) (*bitwise, error) { + if sreg == linux.NFT_REG_VERDICT || dreg == linux.NFT_REG_VERDICT { + return nil, fmt.Errorf("bitwise operation cannot use verdict register as source or destination") + } + blen := len(mask) + if blen != len(xor) { + return nil, fmt.Errorf("bitwise boolean operation mask and xor must be the same length") + } + if blen > 16 || (blen > 4 && (is4ByteRegister(sreg) || is4ByteRegister(dreg))) { + return nil, fmt.Errorf("bitwise operation length %d is too long for source register %d, destination register %d", blen, sreg, dreg) + } + return &bitwise{sreg: sreg, dreg: dreg, bop: linux.NFT_BITWISE_BOOL, blen: uint8(blen), mask: newBytesData(mask), xor: newBytesData(xor)}, nil +} + +// newBitwiseShift creates a new bitwise shift operation. +func newBitwiseShift(sreg, dreg, blen uint8, shift uint32, right bool) (*bitwise, error) { + if sreg == linux.NFT_REG_VERDICT || dreg == linux.NFT_REG_VERDICT { + return nil, fmt.Errorf("bitwise operation cannot use verdict register as source or destination") + } + if blen > 16 || (blen > 4 && (is4ByteRegister(sreg) || is4ByteRegister(dreg))) { + return nil, fmt.Errorf("bitwise operation length %d is too long for source register %d, destination register %d", blen, sreg, dreg) + } + if shift >= bitshiftLimit { + return nil, fmt.Errorf("bitwise operation shift %d must be less than %d", shift, bitshiftLimit) + } + bop := bitwiseOp(linux.NFT_BITWISE_LSHIFT) + if right { + bop = linux.NFT_BITWISE_RSHIFT + } + return &bitwise{sreg: sreg, dreg: dreg, blen: blen, bop: bop, shift: shift}, nil +} + +// evaluateBitwiseBool performs the bitwise boolean operation on the source register +// data and stores the result in the destination register. +func evaluateBitwiseBool(sregBuf, dregBuf, mask, xor []byte) { + for i := 0; i < len(mask); i++ { + dregBuf[i] = (sregBuf[i] & mask[i]) ^ xor[i] + } +} + +// evaluateBitwiseLshift performs the bitwise left shift operation on source +// register in 4 byte chunks and stores the result in the destination register. +func evaluateBitwiseLshift(sregBuf, dregBuf []byte, shift uint32) { + carry := uint32(0) + + // Rounds down to nearest 4-byte multiple. + for start := (len(sregBuf) - 1) & ^3; start >= 0; start -= 4 { + // 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:]) + } else { + var padded [4]byte + copy(padded[:], sregBuf[start:]) + chunk = binary.BigEndian.Uint32(padded[:]) + } + + // Does left shift, adds the carry, and calculates the new carry. + res := (chunk << shift) | carry + carry = chunk >> (bitshiftLimit - shift) + + // Stores the result in the destination register, using temporary buffer + // if necessary. + if start+4 <= len(dregBuf) { + binary.BigEndian.PutUint32(dregBuf[start:], res) + } else { + var padded [4]byte + binary.BigEndian.PutUint32(padded[:], res) + copy(dregBuf[start:], padded[:]) + } + } +} + +// evaluateBitwiseRshift performs the bitwise right shift operation on source +// register in 4 byte chunks and stores the result in the destination register. +func evaluateBitwiseRshift(sregBuf, dregBuf []byte, shift uint32) { + carry := uint32(0) + + for start := 0; start < len(sregBuf); start += 4 { + // 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:]) + } else { + var padded [4]byte + copy(padded[:], sregBuf[start:]) + chunk = binary.BigEndian.Uint32(padded[:]) + } + + // Does right shift, adds the carry, and calculates the new carry. + res := carry | (chunk >> shift) + carry = chunk << (bitshiftLimit - shift) + + // Stores the result in the destination register, using temporary buffer + // if necessary. + if start+4 <= len(dregBuf) { + binary.BigEndian.PutUint32(dregBuf[start:], res) + } else { + var padded [4]byte + binary.BigEndian.PutUint32(padded[:], res) + copy(dregBuf[start:], padded[:]) + } + } +} + +// evaluate for bitwise performs the bitwise operation on the source register +// data and stores the result in the destination register. +func (op bitwise) evaluate(regs *registerSet, pkt *stack.PacketBuffer) { + // Gets the specified buffers of the source and destination registers. + sregBuf := getRegisterBuffer(regs, op.sreg)[:op.blen] + dregBuf := getRegisterBuffer(regs, op.dreg)[:op.blen] + + if op.bop == linux.NFT_BITWISE_BOOL { + mask, ok := op.mask.(bytesData) + if !ok { + panic("bitwise bool mask data is not BytesData") + } + xor, ok := op.xor.(bytesData) + if !ok { + panic("bitwise bool xor data is not BytesData") + } + evaluateBitwiseBool(sregBuf, dregBuf, mask.data, xor.data) + return + } else { + if op.bop == linux.NFT_BITWISE_LSHIFT { + evaluateBitwiseLshift(sregBuf, dregBuf, op.shift) + } else { + evaluateBitwiseRshift(sregBuf, dregBuf, op.shift) + } + } +} + // // Register and Register-Related Implementations. // Note: Registers are represented by type uint8 for the register number. diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index 157ab1a4f..4ea18691b 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -1906,6 +1906,178 @@ func TestEvaluatePayloadSet(t *testing.T) { } } +// TestEvaluateBitwise tests that the Bitwise operation correctly performs the +// appropriate bitwise operation on the source register data and stores the +// result in the destination register. +// Note: Relies on expected behavior of the Immediate and Comparison operation. +func TestEvaluateBitwise(t *testing.T) { + for _, test := range []struct { + tname string + op1 operation // Immediate operation to set source register. + op2 operation // Bitwise operation to test. + op3 operation // Comparison operation to validate result. + }{ + // Bitwise bool operations. + // cmd: add rule ip filter input ip saddr and _ or _ == 105 + { + tname: "same 4-byte register with 4-byte data for bitwise bool", + op1: mustCreateImmediate(t, linux.NFT_REG32_01, newBytesData(numToBE(4783, 4))), + op2: mustCreateBitwiseBool(t, linux.NFT_REG32_01, linux.NFT_REG32_01, numToBE(55, 4), numToBE(78, 4)), + op3: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, numToBE((4783&55)^78, 4)), + }, + { + tname: "same 16-byte register with 4-byte data for bitwise bool", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(4783, 4))), + op2: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_1, numToBE(55, 4), numToBE(78, 4)), + op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE((4783&55)^78, 4)), + }, + // cmd: add rule ip filter input ip saddr and 0x11111111 == 285217024 + { + tname: "dif 4-byte registers with 4-byte data for bitwise bool", + op1: mustCreateImmediate(t, linux.NFT_REG32_01, newBytesData(numToBE(400700800, 4))), + op2: mustCreateBitwiseBool(t, linux.NFT_REG32_01, linux.NFT_REG32_02, numToBE(0x11111111, 4), numToBE(0, 4)), + op3: mustCreateComparison(t, linux.NFT_REG32_02, linux.NFT_CMP_EQ, numToBE(400700800&0x11111111, 4)), + }, + { + tname: "dif 16-byte registers with 4-byte data for bitwise bool", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(400700800, 4))), + op2: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_2, numToBE(0x11111111, 4), numToBE(0, 4)), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, numToBE(400700800&0x11111111, 4)), + }, + // add rule ip filter input ip saddr or 0xff0230ff == 267583535 + { + tname: "4- and 16-byte registers with 4-byte data for bitwise bool", + op1: mustCreateImmediate(t, linux.NFT_REG32_10, newBytesData(numToBE(0, 4))), + op2: mustCreateBitwiseBool(t, linux.NFT_REG32_10, linux.NFT_REG_2, numToBE(0x00cffd00, 4), numToBE(0xff3002ff, 4)), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, numToBE((0&0x00cffd00)^0xff3002ff, 4)), + }, + { + tname: "16- and 4-byte registers with 4-byte data for bitwise bool", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData(numToBE(0, 4))), + op2: mustCreateBitwiseBool(t, linux.NFT_REG_3, linux.NFT_REG32_05, numToBE(0x00cffd00, 4), numToBE(0xff3002ff, 4)), + op3: mustCreateComparison(t, linux.NFT_REG32_05, linux.NFT_CMP_EQ, numToBE((0&0x00cffd00)^0xff3002ff, 4)), + }, + { + tname: "8-byte data for bitwise bool", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x12345678, 8))), + op2: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_1, numToBE(0x00cffd00, 8), numToBE(0xff3002ff, 8)), + op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE((0x12345678&0x00cffd00)^0xff3002ff, 8)), + }, + { + tname: "16-byte data for bitwise bool", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff})), + op2: mustCreateBitwiseBool(t, linux.NFT_REG_4, linux.NFT_REG_2, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe}), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}), + }, + // Bitwise shift operations. + // 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, newBytesData(numToBE(4783, 4))), + 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)), + }, + { + tname: "0 shift right for bitwise rshift", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(4783, 4))), + 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)), + }, + { + tname: "1-bit shift left for bitwise lshift", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(4782, 4))), + 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)), + }, + { + tname: "1-bit shift right for bitwise rshift", + op1: mustCreateImmediate(t, linux.NFT_REG32_06, newBytesData(numToBE(4782, 4))), + 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)), + }, + { + tname: "8-bit shift left for bitwise lshift", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(4782, 4))), + 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)), + }, + { + tname: "8-bit shift right for bitwise rshift", + op1: mustCreateImmediate(t, linux.NFT_REG32_06, newBytesData(numToBE(4782, 4))), + 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)), + }, + { + tname: "16-bit shift left for bitwise lshift", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(0x45678910, 8))), + 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)), + }, + { + tname: "16-bit shift right for bitwise rshift", + op1: mustCreateImmediate(t, linux.NFT_REG32_06, newBytesData(numToBE(0x45678910, 4))), + 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)), + }, + { + tname: "max-bit shift left for bitwise lshift", + op1: mustCreateImmediate(t, linux.NFT_REG32_03, newBytesData(numToBE(0x45678910, 4))), + 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)), + }, + { + tname: "max-bit shift right for bitwise rshift", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData(numToBE(0x45678910, 8))), + 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)), + }, + } { + t.Run(test.tname, func(t *testing.T) { + // Sets up an NFTables object with a single table, chain, and rule. + nf := NewNFTables() + tab, err := nf.AddTable(arbitraryFamily, "test", "test table", false) + if err != nil { + t.Fatalf("unexpected error for AddTable: %v", err) + } + bc, err := tab.AddChain("base_chain", nil, "test chain", false) + if err != nil { + t.Fatalf("unexpected error for AddChain: %v", err) + } + bc.SetBaseChainInfo(arbitraryInfoPolicyAccept) + rule := &Rule{} + + // Adds testing operations. + if test.op1 != nil { + rule.addOperation(test.op1) + } + if test.op2 != nil { + rule.addOperation(test.op2) + } + if test.op3 != nil { + rule.addOperation(test.op3) + } + + // Adds drop operation. Will be final verdict if comparison is true. + rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, newVerdictData(Verdict{Code: VC(linux.NF_DROP)}))) + + // Registers the rule to the base chain. + if err := bc.RegisterRule(rule, -1); err != nil { + t.Fatalf("unexpected error for RegisterRule: %v", err) + } + + // Runs evaluation and checks verdict. + pkt := makeArbitraryPacket(arbitraryReservedHeaderBytes) + v, err := nf.EvaluateHook(arbitraryFamily, arbitraryHook, pkt) + if err != nil { + t.Fatalf("unexpected error for EvaluateHook: %v", err) + } + if v.Code != VC(linux.NF_DROP) { + t.Fatalf("expected verdict Drop for true comparison, got %v", v) + } + }) + } +} + // TestLoopCheckOnRegisterAndUnregister tests the loop checking and accompanying // logic on registering and unregistering rules. func TestLoopCheckOnRegisterAndUnregister(t *testing.T) { @@ -2537,3 +2709,21 @@ func mustCreatePayloadSet(t *testing.T, base payloadBase, offset uint8, len uint } return pdset } + +// 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) + if err != nil { + t.Fatalf("failed to create bitwise bool: %v", err) + } + return bit +} + +// mustCreateBitwiseShift wraps the newBitwiseShift function for brevity. +func mustCreateBitwiseShift(t *testing.T, sreg, dreg, blen uint8, shift uint32, right bool) *bitwise { + bit, err := newBitwiseShift(sreg, dreg, blen, shift, right) + if err != nil { + t.Fatalf("failed to create bitwise shift: %v", err) + } + return bit +} diff --git a/pkg/tcpip/nftables/nftinterp.go b/pkg/tcpip/nftables/nftinterp.go index 6a2c0026a..31bdc8bad 100644 --- a/pkg/tcpip/nftables/nftinterp.go +++ b/pkg/tcpip/nftables/nftinterp.go @@ -147,6 +147,10 @@ func InterpretOperation(line string, lnIdx int) (operation, error) { return InterpretPayloadSet(line, lnIdx) } return nil, &SyntaxError{lnIdx, 2, fmt.Sprintf("unrecognized operation type: payload %s", tokens[2])} + case "bitwise": + // Assumes the bitwise operation is a boolean because interpretation of + // non-boolean operations is not supported from the nft binary debug output. + return InterpretBitwiseBool(line, lnIdx) default: return nil, &SyntaxError{lnIdx, 1, fmt.Sprintf("unrecognized operation type: %s", tokens[1])} } @@ -502,6 +506,112 @@ func InterpretPayloadSet(line string, lnIdx int) (operation, error) { return pdset, nil } +// InterpretBitwiseBool creates a new Comparison operation from the given string. +func InterpretBitwiseBool(line string, lnIdx int) (operation, error) { + tokens := strings.Fields(line) + + // Requires at least 14 tokens: + // "[", "bitwise", "reg", dreg index, "=", "(", "reg", sreg index, "&", mask value, ")", "^", xor value, "]". + if len(tokens) < 14 { + return nil, &SyntaxError{lnIdx, 0, fmt.Sprintf("incorrect number of tokens for bitwise boolean operation, should be at least 14, got %d", len(tokens))} + } + + if err := checkOperationBrackets(tokens, lnIdx); err != nil { + return nil, err + } + + tkIdx := 1 + + // First token should be "bitwise". + if err := consumeToken("bitwise", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Second token should be "reg". + if err := consumeToken("reg", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Third token should be the uint8 representing destination register index. + dreg, err := parseRegister(tokens[tkIdx], lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Fourth token should be "=". + if err := consumeToken("=", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Fifth token should be "(". + if err := consumeToken("(", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Sixth token should be "reg". + if err := consumeToken("reg", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Seventh token should be the uint8 representing source register index. + sreg, err := parseRegister(tokens[tkIdx], lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Eighth token should be "&". + if err := consumeToken("&", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Ninth token should be the bytesData representing the mask value. + nextIdx, mask, err := parseHexData(tokens, lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx = nextIdx + + // Tenth token should be ")". + if err := consumeToken(")", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Eleventh token should be "^". + if err := consumeToken("^", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Twelfth token should be the bytesData representing the xor value. + nextIdx, xor, err := parseHexData(tokens, lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx = nextIdx + + // Done parsing tokens. + if tkIdx != len(tokens)-1 { + return nil, &SyntaxError{lnIdx, tkIdx, "unexpected token after bitwise boolean operation"} + } + + // Create the operation with the specified arguments. + bitwiseBool, err := newBitwiseBool(sreg, dreg, mask, xor) + if err != nil { + return nil, &LogicError{lnIdx, tkIdx, err} + } + + return bitwiseBool, nil +} + // // Interpreter Helper Functions. // diff --git a/pkg/tcpip/nftables/nftinterp_test.go b/pkg/tcpip/nftables/nftinterp_test.go index 2b36f942d..fbf610d8e 100644 --- a/pkg/tcpip/nftables/nftinterp_test.go +++ b/pkg/tcpip/nftables/nftinterp_test.go @@ -664,6 +664,113 @@ func checkPayloadSetOp(tname string, expected operation, actual operation) error return nil } +// TestInterpretBitwiseOps tests interpretation of bitwise operations. +// Note: Only tests bitwise bool operations for now because interpretation of +// non-boolean operations is not supported from the nft binary debug output. +func TestInterpretBitwiseOps(t *testing.T) { + for _, test := range []interpretOperationTestAction{ + // Invalid interpretations. + { + tname: "verdict register with bitwise bool", + opStr: "[ bitwise reg 0 = ( reg 1 & 0x000003ff ) ^ 0x0000b000 ]", + expected: nil, + }, + { + tname: "4-byte register with > 4-byte data for bitwise bool", + opStr: "[ bitwise reg 1 = ( reg 9 & 0x000003ff 0x09040302 ) ^ 0x0000b000 0x11ff11ff ]", + expected: nil, + }, + { + tname: "mismatch mask and xor lengths for bitwise bool", + opStr: "[ bitwise reg 1 = ( reg 1 & 0x000003ff ) ^ 0x0000b000 0x11ff11ff ]", + expected: nil, + }, + // cmd: add rule ip filter input ip dscp set 0x2c + { + tname: "same 4-byte register with 4-byte data for bitwise bool", + opStr: "[ bitwise reg 10 = ( reg 10 & 0x000003ff ) ^ 0x0000b000 ]", + expected: mustCreateBitwiseBool(t, linux.NFT_REG32_02, linux.NFT_REG32_02, []byte{0xff, 0x03, 0x00, 0x00}, []byte{0x00, 0xb0, 0x00, 0x00}), + }, + { + tname: "dif 4-byte registers with 4-byte data for bitwise bool", + opStr: "[ bitwise reg 8 = ( reg 9 & 0x000003ff ) ^ 0x0000b000 ]", + expected: mustCreateBitwiseBool(t, linux.NFT_REG32_01, linux.NFT_REG32_00, []byte{0xff, 0x03, 0x00, 0x00}, []byte{0x00, 0xb0, 0x00, 0x00}), + }, + // cmd: add rule ip filter input ip saddr and 55 or 0xffff0000 == 34 + { + tname: "same 16-byte register with 4-byte data for bitwise bool", + opStr: "[ bitwise reg 1 = ( reg 1 & 0x37000000 ) ^ 0x0000ffff ]", + expected: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_1, []byte{0x00, 0x00, 0x00, 0x37}, []byte{0xff, 0xff, 0x00, 0x00}), + }, + { + tname: "dif 16-byte registers with 4-byte data for bitwise bool", + opStr: "[ bitwise reg 4 = ( reg 3 & 0x37000000 ) ^ 0x0000ffff ]", + expected: mustCreateBitwiseBool(t, linux.NFT_REG_3, linux.NFT_REG_4, []byte{0x00, 0x00, 0x00, 0x37}, []byte{0xff, 0xff, 0x00, 0x00}), + }, + // cmd: add rule ip filter input ip saddr and 0xff0230ff == 5 + { + tname: "4- and 16-byte registers with 4-byte data for bitwise bool", + opStr: "[ bitwise reg 4 = ( reg 14 & 0xff3002ff ) ^ 0x00000000 ]", + expected: mustCreateBitwiseBool(t, linux.NFT_REG32_06, linux.NFT_REG_4, []byte{0xff, 0x02, 0x30, 0xff}, []byte{0x00, 0x00, 0x00, 0x00}), + }, + { + tname: "16- and 4-byte registers with 4-byte data for bitwise bool", + opStr: "[ bitwise reg 14 = ( reg 1 & 0xff3002ff ) ^ 0x00000000 ]", + expected: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG32_06, []byte{0xff, 0x02, 0x30, 0xff}, []byte{0x00, 0x00, 0x00, 0x00}), + }, + // More than 4 bytes of data. + { + tname: "8-byte data for bitwise bool", + opStr: "[ bitwise reg 1 = ( reg 1 & 0x00000000 0x00000000 ) ^ 0x00000164 0x00000164 ]", + expected: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_1, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, []byte{0x64, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00}), + }, + { + tname: "12-byte data for bitwise bool", + opStr: "[ bitwise reg 4 = ( reg 2 & 0x0302010a 0x00000000 0x12345678 ) ^ 0x0a000120 0x00000f13 0xc0090000 ]", + expected: mustCreateBitwiseBool(t, linux.NFT_REG_2, linux.NFT_REG_4, []byte{0x0a, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x34, 0x12}, []byte{0x20, 0x01, 0x00, 0x0a, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0}), + }, + { + tname: "16-byte data for bitwise bool", + opStr: "[ bitwise reg 1 = ( reg 3 & 0xe8030000 0x00000f13 0xc0090000 0x0b136a87 ) ^ 0x0a000120 0x00000f13 0xc0090000 0x00000000 ]", + expected: mustCreateBitwiseBool(t, linux.NFT_REG_3, linux.NFT_REG_1, []byte{0x00, 0x00, 0x03, 0xe8, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0, 0x87, 0x6a, 0x13, 0x0b}, []byte{0x20, 0x01, 0x00, 0x0a, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0, 0x00, 0x00, 0x00, 0x00}), + }, + } { + t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkBitwiseOp) }) + } +} + +// checkBitwiseOp checks that the given operation is a bitwise operation and +// that it matches the expected bitwise operation. +func checkBitwiseOp(tname string, expected operation, actual operation) error { + expectedBit := expected.(*bitwise) + bit, ok := actual.(*bitwise) + if !ok { + return fmt.Errorf("expected operation type to be BitwiseBool for %s, got %T", tname, actual) + } + if bit.sreg != expectedBit.sreg { + return fmt.Errorf("expected source register to be %d for %s, got %d", expectedBit.sreg, tname, bit.sreg) + } + if bit.dreg != expectedBit.dreg { + return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedBit.dreg, tname, bit.dreg) + } + if bit.bop != expectedBit.bop { + return fmt.Errorf("expected bitwise operation to be %d for %s, got %d", expectedBit.bop, tname, bit.bop) + } + if bit.blen != expectedBit.blen { + return fmt.Errorf("expected bitwise length to be %d for %s, got %d", expectedBit.blen, tname, bit.blen) + } + if !bit.mask.equal(expectedBit.mask) { + return fmt.Errorf("expected bitwise mask to be %v for %s, got %v", expectedBit.mask, tname, bit.mask) + } + if !bit.xor.equal(expectedBit.xor) { + return fmt.Errorf("expected bitwise xor to be %v for %s, got %v", expectedBit.xor, tname, bit.xor) + } + if bit.shift != expectedBit.shift { + return fmt.Errorf("expected bitwise shift to be %d for %s, got %d", expectedBit.shift, tname, bit.shift) + } + return nil +} + // TestInterpretRule tests the interpretation of basic and general rules as a // list of operations. func TestInterpretRule(t *testing.T) {