diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index dcb1cd6d6..e2f7bedc7 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -55,6 +55,8 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/stack" ) +// TODO(b/345684870): Break this file up into multiple files by operation type. +// Each operation should get its own file. // TODO(b/345684870): Make the nftables package thread-safe! Must be done before // the package is used in production. @@ -593,6 +595,7 @@ var ( _ operation = (*counter)(nil) _ operation = (*last)(nil) _ operation = (*route)(nil) + _ operation = (*byteorder)(nil) ) // immediate is an operation that sets the data in a register. @@ -877,7 +880,7 @@ func newPayloadLoad(base payloadBase, offset, blen, dreg uint8) (*payloadLoad, e if isVerdictRegister(dreg) { return nil, fmt.Errorf("payload load operation cannot use verdict register as destination") } - if blen > 16 || (blen > 4 && is4ByteRegister(dreg)) { + if blen > linux.NFT_REG_SIZE || (blen > linux.NFT_REG32_SIZE && is4ByteRegister(dreg)) { return nil, fmt.Errorf("payload length %d is too long for destination register %d", blen, dreg) } if err := validatePayloadBase(base); err != nil { @@ -950,7 +953,7 @@ func newPayloadSet(base payloadBase, offset, blen, sreg, csumType, csumOffset, c if isVerdictRegister(sreg) { return nil, fmt.Errorf("payload set operation cannot use verdict register as destination") } - if blen > 16 || (blen > 4 && is4ByteRegister(sreg)) { + if blen > linux.NFT_REG_SIZE || (blen > linux.NFT_REG32_SIZE && is4ByteRegister(sreg)) { return nil, fmt.Errorf("payload length %d is too long for destination register %d", blen, sreg) } if err := validatePayloadBase(base); err != nil { @@ -1110,7 +1113,7 @@ func newBitwiseBool(sreg, dreg uint8, mask, xor []byte) (*bitwise, error) { 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))) { + if blen > linux.NFT_REG_SIZE || (blen > linux.NFT_REG32_SIZE && (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 @@ -1121,7 +1124,7 @@ func newBitwiseShift(sreg, dreg, blen uint8, shift uint32, right bool) (*bitwise if isVerdictRegister(sreg) || isVerdictRegister(dreg) { return nil, fmt.Errorf("bitwise operation cannot use verdict register as source or destination") } - if blen > 16 || (blen > 4 && (is4ByteRegister(sreg) || is4ByteRegister(dreg))) { + if blen > linux.NFT_REG_SIZE || (blen > linux.NFT_REG32_SIZE && (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 { @@ -1389,6 +1392,128 @@ func (op route) evaluate(regs *registerSet, pkt *stack.PacketBuffer, rule *Rule) data.storeData(regs, op.dreg) } +// byteorder is an operation that performs byte order operations on a register. +// Note: byteorder operations are not supported for the verdict register. +type byteorder struct { + sreg uint8 // Number of the source register. + dreg uint8 // Number of the destination register. + bop byteorderOp // Byte order operation to perform. + blen uint8 // Number of total bytes to operate on. + size uint8 // Granular size in bytes to operate on. +} + +// byteorderOp is the byte order operator for a byteorder operation. +// Note: corresponds to enum nft_byteorder_ops from +// include/uapi/linux/netfilter/nf_tables.h and uses the same constants. +type byteorderOp int + +// String for byteorderOp returns the string representation of the byteorder +// operator. +func (bop byteorderOp) String() string { + switch bop { + case linux.NFT_BYTEORDER_NTOH: + return "network to host" + case linux.NFT_BYTEORDER_HTON: + return "host to network" + default: + panic(fmt.Sprintf("unknown supported byteorder operator: %d", int(bop))) + } +} + +// validateByteorderOp ensures the byteorder operator is valid. +func validateByteorderOp(bop byteorderOp) error { + switch bop { + // Supported operators. + case linux.NFT_BYTEORDER_NTOH, linux.NFT_BYTEORDER_HTON: + return nil + default: + return fmt.Errorf("invalid byteorder operator: %d", int(bop)) + } +} + +// newByteorder creates a new byteorder operation. +func newByteorder(sreg, dreg uint8, bop byteorderOp, blen, size uint8) (*byteorder, error) { + if isVerdictRegister(sreg) || isVerdictRegister(dreg) { + return nil, fmt.Errorf("byteorder operation cannot use verdict register") + } + if err := validateByteorderOp(bop); err != nil { + return nil, err + } + if blen > linux.NFT_REG_SIZE { + return nil, fmt.Errorf("byteorder operation cannot have length greater than the max register size of %d bytes", linux.NFT_REG_SIZE) + } + if (is4ByteRegister(sreg) || is4ByteRegister(dreg)) && blen > linux.NFT_REG32_SIZE { + return nil, fmt.Errorf("byteorder operation cannot have length greater than the max register size of %d bytes", linux.NFT_REG32_SIZE) + } + if size > blen { + return nil, fmt.Errorf("byteorder operation cannot have size greater than length") + } + if size != 2 && size != 4 && size != 8 { + return nil, fmt.Errorf("byteorder operation size must be 2, 4, or 8 bytes") + } + return &byteorder{sreg: sreg, dreg: dreg, bop: bop, blen: blen, size: size}, nil +} + +// evaluate for byteorder performs the byte order operation on the source +// register and stores the result in the destination register. +func (op byteorder) evaluate(regs *registerSet, pkt *stack.PacketBuffer, rule *Rule) { + // Gets the source and destination registers. + src := getRegisterBuffer(regs, op.sreg) + dst := getRegisterBuffer(regs, op.dreg) + + // Performs the byte order operations on the source register and stores the + // result in as many bytes as are available in the destination register. + switch op.size { + case 8: + switch op.bop { + case linux.NFT_BYTEORDER_NTOH: + for i := uint8(0); i < op.blen; i += 8 { + networkNum := binary.BigEndian.Uint64(src[i : i+8]) + binary.NativeEndian.PutUint64(dst[i:], networkNum) + } + case linux.NFT_BYTEORDER_HTON: + for i := uint8(0); i < op.blen; i += 8 { + hostNum := binary.NativeEndian.Uint64(src[i : i+8]) + binary.BigEndian.PutUint64(dst[i:], hostNum) + } + } + + case 4: + switch op.bop { + case linux.NFT_BYTEORDER_NTOH: + for i := uint8(0); i < op.blen; i += 4 { + networkNum := binary.BigEndian.Uint32(src[i : i+4]) + binary.NativeEndian.PutUint32(dst[i:], networkNum) + } + case linux.NFT_BYTEORDER_HTON: + for i := uint8(0); i < op.blen; i += 4 { + hostNum := binary.NativeEndian.Uint32(src[i : i+4]) + binary.BigEndian.PutUint32(dst[i:], hostNum) + } + } + + case 2: + switch op.bop { + case linux.NFT_BYTEORDER_NTOH: + for i := uint8(0); i < op.blen; i += 2 { + networkNum := binary.BigEndian.Uint16(src[i : i+2]) + binary.NativeEndian.PutUint16(dst[i:], networkNum) + } + case linux.NFT_BYTEORDER_HTON: + for i := uint8(0); i < op.blen; i += 2 { + hostNum := binary.NativeEndian.Uint16(src[i : i+2]) + binary.BigEndian.PutUint16(dst[i:], hostNum) + } + } + } + + // Zeroes out excess bytes of the destination register. + // This is done since comparison can be done in multiples of 4 bytes. + if rem := op.blen % 4; rem != 0 { + clear(dst[op.blen : op.blen+4-rem]) + } +} + // // Register and Register-Related Implementations. // Note: Registers are represented by type uint8 for the register number. @@ -1481,8 +1606,8 @@ func newBytesData(bytes []byte) bytesData { if len(bytes) == 0 { panic("bytes data cannot be empty") } - if len(bytes) > 16 { - panic(fmt.Errorf("bytes data cannot be more than 16 bytes: %d", len(bytes))) + if len(bytes) > linux.NFT_REG_SIZE { + panic(fmt.Errorf("bytes data cannot be more than %d bytes: %d", linux.NFT_REG_SIZE, len(bytes))) } return bytesData{data: bytes} } @@ -1509,8 +1634,8 @@ func (rd bytesData) validateRegister(reg uint8) error { if isVerdictRegister(reg) { return fmt.Errorf("data cannot be stored in verdict register") } - if is4ByteRegister(reg) && len(rd.data) > 4 { - return fmt.Errorf("%d-byte data cannot be stored in 4-byte register", len(rd.data)) + if is4ByteRegister(reg) && len(rd.data) > linux.NFT_REG32_SIZE { + return fmt.Errorf("%d-byte data cannot be stored in %d-byte register", len(rd.data), linux.NFT_REG32_SIZE) } // 16-byte register can be used for any data (guaranteed to be <= 16 bytes) return nil diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index 0394e0d39..ff2809207 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -2570,6 +2570,249 @@ func TestEvaluateRoute(t *testing.T) { } } +// TestEvaluateByteorder tests that the Byteorder operation correctly performs +// the appropriate byteorder 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 TestEvaluateByteorder(t *testing.T) { + // Given a big endian and little endian byte slice of the same number, returns + // the correct byte slice based on the host endianness. + // Note: Uses enclosure so endianness doesn't need to be passed as an arg or + // rechecked for every call. + chooseOrder := func() func([]byte, []byte) []byte { + hostBytes := binary.NativeEndian.AppendUint16(nil, 0x0102) + isBigEndian := hostBytes[0] == 0x01 + return func(big, little []byte) []byte { + if isBigEndian { + return big + } + return little + } + }() + // Like createChooseOrder but takes ints instead of byte slices. + chooseOrderN := func(big, little, size int) []byte { + return chooseOrder(numToBE(big, size), numToBE(little, size)) + } + for _, test := range []struct { + tname string + op1 operation // Immediate operation to set source register. + op2 operation // Byteorder operation to test. + op3 operation // Comparison operation to validate result. + }{ + // Size 2 tests (Lengths 2, 3, 4, 6, 8, 16) + { + tname: "ntoh size 2 len 2", + op1: mustCreateImmediate(t, linux.NFT_REG32_01, newBytesData(numToBE(0x0102, 2))), + op2: mustCreateByteorder(t, linux.NFT_REG32_01, linux.NFT_REG32_01, linux.NFT_BYTEORDER_NTOH, 2, 2), + op3: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, chooseOrderN(0x0102, 0x0201, 2)), + }, + { + tname: "hton size 2 len 2", + op1: mustCreateImmediate(t, linux.NFT_REG32_01, newBytesData(numToBE(0x0102, 2))), + op2: mustCreateByteorder(t, linux.NFT_REG32_01, linux.NFT_REG_1, linux.NFT_BYTEORDER_HTON, 2, 2), + op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, chooseOrderN(0x0102, 0x0201, 2)), + }, + { + tname: "ntoh size 2 len 3", + op1: mustCreateImmediate(t, linux.NFT_REG32_01, newBytesData(numToBE(0x010203, 3))), + op2: mustCreateByteorder(t, linux.NFT_REG32_01, linux.NFT_REG_1, linux.NFT_BYTEORDER_NTOH, 3, 2), + op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, chooseOrderN(0x010203, 0x020100, 3)), + }, + { + tname: "hton size 2 len 3", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x010203, 3))), + op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG32_01, linux.NFT_BYTEORDER_HTON, 3, 2), + op3: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, chooseOrderN(0x010203, 0x020100, 3)), + }, + { + tname: "ntoh size 2 len 4", + op1: mustCreateImmediate(t, linux.NFT_REG32_10, newBytesData(numToBE(0x01020304, 4))), + op2: mustCreateByteorder(t, linux.NFT_REG32_10, linux.NFT_REG32_05, linux.NFT_BYTEORDER_NTOH, 4, 2), + op3: mustCreateComparison(t, linux.NFT_REG32_05, linux.NFT_CMP_EQ, chooseOrderN(0x01020304, 0x02010403, 4)), + }, + { + tname: "hton size 2 len 4", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(0x01020304, 4))), + op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG32_09, linux.NFT_BYTEORDER_HTON, 4, 2), + op3: mustCreateComparison(t, linux.NFT_REG32_09, linux.NFT_CMP_EQ, chooseOrderN(0x01020304, 0x02010403, 4)), + }, + { + tname: "ntoh size 2 len 6", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x010203040506, 6))), + op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_1, linux.NFT_BYTEORDER_NTOH, 6, 2), + op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, chooseOrderN(0x010203040506, 0x020104030605, 6)), + }, + { + tname: "hton size 2 len 6", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x010203040506, 6))), + op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_1, linux.NFT_BYTEORDER_HTON, 6, 2), + op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, chooseOrderN(0x010203040506, 0x020104030605, 6)), + }, + { + tname: "ntoh size 2 len 8", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x0102030405060708, 8))), + op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_NTOH, 8, 2), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0201040306050807, 8)), + }, + { + tname: "hton size 2 len 8", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x0102030405060708, 8))), + op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_HTON, 8, 2), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0201040306050807, 8)), + }, + { + tname: "ntoh size 2 len 16", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})), + op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 16, 2), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + []byte{0x02, 0x01, 0x04, 0x03, 0x06, 0x05, 0x08, 0x07, 0x0a, 0x09, 0x0c, 0x0b, 0x0e, 0x0d, 0x10, 0x0f})), + }, + { + tname: "hton size 2 len 16", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})), + op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_HTON, 16, 2), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + []byte{0x02, 0x01, 0x04, 0x03, 0x06, 0x05, 0x08, 0x07, 0x0a, 0x09, 0x0c, 0x0b, 0x0e, 0x0d, 0x10, 0x0f})), + }, + // Size 4 tests (Lengths 4, 6, 8, 16) + { + tname: "ntoh size 4 len 4", + op1: mustCreateImmediate(t, linux.NFT_REG32_05, newBytesData(numToBE(0x01020304, 4))), + op2: mustCreateByteorder(t, linux.NFT_REG32_05, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 4, 4), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrderN(0x01020304, 0x04030201, 4)), + }, + { + tname: "hton size 4 len 4", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(0x01020304, 4))), + op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG32_09, linux.NFT_BYTEORDER_HTON, 4, 4), + op3: mustCreateComparison(t, linux.NFT_REG32_09, linux.NFT_CMP_EQ, chooseOrderN(0x01020304, 0x04030201, 4)), + }, + { + tname: "ntoh size 4 len 6", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(0x010203040506, 6))), + op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 6, 4), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrderN(0x010203040506, 0x040302010000, 6)), + }, + { + tname: "hton size 4 len 6", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(0x010203040506, 6))), + op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG_2, linux.NFT_BYTEORDER_HTON, 6, 4), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrderN(0x010203040506, 0x040302010000, 6)), + }, + { + tname: "ntoh size 4 len 8", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x0102030405060708, 8))), + op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_NTOH, 8, 4), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0403020108070605, 8)), + }, + { + tname: "hton size 4 len 8", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x0102030405060708, 8))), + op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_HTON, 8, 4), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0403020108070605, 8)), + }, + { + tname: "ntoh size 4 len 16", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})), + op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 16, 4), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + []byte{0x04, 0x03, 0x02, 0x01, 0x08, 0x07, 0x06, 0x05, 0x0c, 0x0b, 0x0a, 0x09, 0x10, 0x0f, 0x0e, 0x0d})), + }, + { + tname: "hton size 4 len 16", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})), + op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_HTON, 16, 4), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + []byte{0x04, 0x03, 0x02, 0x01, 0x08, 0x07, 0x06, 0x05, 0x0c, 0x0b, 0x0a, 0x09, 0x10, 0x0f, 0x0e, 0x0d})), + }, + // Size 8 tests (Lengths 8, 12, 16) + { + tname: "ntoh size 8 len 8", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x0102030405060708, 8))), + op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_NTOH, 8, 8), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0807060504030201, 8)), + }, + { + tname: "hton size 8 len 8", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(0x0102030405060708, 8))), + op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_HTON, 8, 8), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0807060504030201, 8)), + }, + { + tname: "ntoh size 8 len 12", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c})), + op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 12, 8), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, + []byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00})), + }, + { + tname: "hton size 8 len 12", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c})), + op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_HTON, 12, 8), + op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, + []byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00})), + }, + { + tname: "ntoh size 8 len 16", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})), + op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG_4, linux.NFT_BYTEORDER_NTOH, 16, 8), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + []byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09})), + }, + { + tname: "hton size 8 len 16", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})), + op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG_4, linux.NFT_BYTEORDER_HTON, 16, 8), + op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + []byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09})), + }, + } { + t.Run(test.tname, func(t *testing.T) { + // Sets up an NFTables object with a single table, chain, and rule. + nf := newNFTablesStd() + 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) { @@ -3242,3 +3485,12 @@ func mustCreateRoute(t *testing.T, key routeKey, dreg uint8) *route { } return rt } + +// mustCreateByteorder wraps the newByteorder function for brevity. +func mustCreateByteorder(t *testing.T, sreg, dreg uint8, bop byteorderOp, blen, size uint8) *byteorder { + order, err := newByteorder(sreg, dreg, bop, blen, size) + if err != nil { + t.Fatalf("failed to create byteorder: %v", err) + } + return order +} diff --git a/pkg/tcpip/nftables/nftinterp.go b/pkg/tcpip/nftables/nftinterp.go index 8be3882c8..5d5bbc70a 100644 --- a/pkg/tcpip/nftables/nftinterp.go +++ b/pkg/tcpip/nftables/nftinterp.go @@ -155,6 +155,8 @@ func InterpretOperation(line string, lnIdx int) (operation, error) { return InterpretCounter(line, lnIdx) case "rt": return InterpretRoute(line, lnIdx) + case "byteorder": + return InterpretByteorder(line, lnIdx) default: return nil, &SyntaxError{lnIdx, 1, fmt.Sprintf("unrecognized operation type: %s", tokens[1])} } @@ -309,7 +311,7 @@ func InterpretPayloadLoad(line string, lnIdx int) (operation, error) { tkIdx++ // Third token should be the length (in bytes) of the payload followed by 'b'. - len, err := parsePayloadLength(tokens[tkIdx], lnIdx, tkIdx) + blen, err := parseUint8PlusChar(tokens[tkIdx], 'b', lnIdx, tkIdx) if err != nil { return nil, err } @@ -367,7 +369,7 @@ func InterpretPayloadLoad(line string, lnIdx int) (operation, error) { tkIdx++ // Create the operation with the specified arguments. - pdload, err := newPayloadLoad(base, offset, len, reg) + pdload, err := newPayloadLoad(base, offset, blen, reg) if err != nil { return nil, &LogicError{lnIdx, tkIdx, err} } @@ -424,7 +426,7 @@ func InterpretPayloadSet(line string, lnIdx int) (operation, error) { tkIdx++ // Sixth token should be the length (in bytes) of the payload followed by 'b'. - len, err := parsePayloadLength(tokens[tkIdx], lnIdx, tkIdx) + blen, err := parseUint8PlusChar(tokens[tkIdx], 'b', lnIdx, tkIdx) if err != nil { return nil, err } @@ -502,7 +504,7 @@ func InterpretPayloadSet(line string, lnIdx int) (operation, error) { tkIdx++ // Create the operation with the specified arguments. - pdset, err := newPayloadSet(base, offset, len, reg, csumType, csumOff, csumFlags) + pdset, err := newPayloadSet(base, offset, blen, reg, csumType, csumOff, csumFlags) if err != nil { return nil, &LogicError{lnIdx, tkIdx, err} } @@ -733,6 +735,89 @@ func InterpretRoute(line string, lnIdx int) (operation, error) { return rt, nil } +// InterpretByteorder creates a new Byteorder operation from the given string. +func InterpretByteorder(line string, lnIdx int) (operation, error) { + tokens := strings.Fields(line) + + // Requires exactly 10 tokens: + // "[", "byteorder", "reg", dreg index, "=", byteorder op+"(reg", sreg index+",", size+",", blen+")", "]". + if len(tokens) != 10 { + return nil, &SyntaxError{lnIdx, 0, fmt.Sprintf("incorrect number of tokens for route operation, should be exactly 10, got %d", len(tokens))} + } + + if err := checkOperationBrackets(tokens, lnIdx); err != nil { + return nil, err + } + + tkIdx := 1 + + // First token should be "byteorder". + if err := consumeToken("byteorder", 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 "ntoh(reg". + var bop byteorderOp + switch tokens[tkIdx] { + case "ntoh(reg": + bop = linux.NFT_BYTEORDER_NTOH + case "hton(reg": + bop = linux.NFT_BYTEORDER_HTON + default: + return nil, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("expected 'ntoh' or 'hton' keyword followed by '(reg' at token %d, got '%s'", tkIdx, tokens[tkIdx])} + } + tkIdx++ + + // Sixth token should be the source register index followed by ','. + sreg, err := parseUint8PlusChar(tokens[tkIdx], ',', lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Seventh token should be the size in bytes followed by ','. + size, err := parseUint8PlusChar(tokens[tkIdx], ',', lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Eighth token should be the length in bytes followed by ')'. + blen, err := parseUint8PlusChar(tokens[tkIdx], ')', lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Create the operation with the specified arguments. + order, err := newByteorder(dreg, sreg, bop, blen, size) + if err != nil { + return nil, &LogicError{lnIdx, tkIdx, err} + } + + return order, nil +} + // // Interpreter Helper Functions. // @@ -870,8 +955,9 @@ func parseHexData(tokens []string, lnIdx int, tkIdx int) (int, []byte, error) { slices.Reverse(bytes4) bytes = append(bytes, bytes4...) } - if len(bytes) > 16 { - return 0, nil, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("cannot have more than 16 bytes of hexadecimal data, got %d", len(bytes))} + if len(bytes) > linux.NFT_REG_SIZE { + return 0, nil, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("cannot have more than %d bytes of hexadecimal data, got %d", + linux.NFT_REG_SIZE, len(bytes))} } return tkIdx, bytes, nil } @@ -896,22 +982,19 @@ func parseCmpOp(copString string, lnIdx int, tkIdx int) (int, error) { } } -// parsePayloadLength parses the payload length from the given string -// expecting a unsigned 8-bit integer followed by 'b'. -func parsePayloadLength(lenString string, lnIdx int, tkIdx int) (uint8, error) { - lastChar := lenString[len(lenString)-1] - if lastChar != 'b' { - return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("expected 'b' at the end of payload length, got '%c'", lastChar)} +// parseUint8PlusChar parses the a uint8 followed by the given character from +// the given string. +func parseUint8PlusChar(numString string, char byte, lnIdx int, tkIdx int) (uint8, error) { + lastChar := numString[len(numString)-1] + if lastChar != char { + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("expected '%c' at the end of the uint8, got '%c'", char, lastChar)} } - numStr := lenString[:len(lenString)-1] - len, err := strconv.ParseUint(numStr, 10, 8) + numStr := numString[:len(numString)-1] + num, err := strconv.ParseUint(numStr, 10, 8) if err != nil { - return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("could not parse uint8 payload length: '%s'", numStr)} + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("could not parse uint8: '%s'", numStr)} } - if len > 16 { - return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("payload length must be <= 16 bytes, got %d", len)} - } - return uint8(len), nil + return uint8(num), nil } // parsePayloadBase parses the payload base header from the given string. diff --git a/pkg/tcpip/nftables/nftinterp_test.go b/pkg/tcpip/nftables/nftinterp_test.go index 98253576b..625b4f92d 100644 --- a/pkg/tcpip/nftables/nftinterp_test.go +++ b/pkg/tcpip/nftables/nftinterp_test.go @@ -884,6 +884,79 @@ func checkRouteOp(tname string, expected operation, actual operation) error { return nil } +// TestInterpretByteorderOps tests interpretation of byteorder operations. +// Note: Most byteorder operations have been revealed in the nft binary +// debug output through bitshifts (which oddly do not use the native bitwise +// operation lshift and rshift operators). Thus, many of following commands are +// simply variations of lshift and rshift commands. +func TestInterpretByteorderOps(t *testing.T) { + for _, test := range []interpretOperationTestAction{ + { // cmd: add rule ip tab ch tcp dport rshift 4 == 0x5678 + tname: "ntoh size 2 len 2", + opStr: "[ byteorder reg 1 = ntoh(reg 1, 2, 2) ]", + expected: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_1, linux.NFT_BYTEORDER_NTOH, 2, 2), + }, + { // cmd: add rule ip tab ch tcp dport rshift 7 == 0x345 + tname: "ntoh size 2 len 2 again", + opStr: "[ byteorder reg 2 = ntoh(reg 11, 2, 2) ]", + expected: mustCreateByteorder(t, linux.NFT_REG_2, linux.NFT_REG32_03, linux.NFT_BYTEORDER_NTOH, 2, 2), + }, + { // cmd: add rule ip filter input @th,24,24 rshift 1 0xabcdef + tname: "ntoh size 2 len 3 again", + opStr: "[ byteorder reg 15 = ntoh(reg 15, 2, 3) ]", + expected: mustCreateByteorder(t, linux.NFT_REG32_07, linux.NFT_REG32_07, linux.NFT_BYTEORDER_NTOH, 3, 2), + }, + { // cmd: add rule ip filter input ether saddr lshift 1 == 01223456 + tname: "ntoh size 2 len 6", + opStr: "[ byteorder reg 4 = ntoh(reg 3, 2, 6) ]", + expected: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG_3, linux.NFT_BYTEORDER_NTOH, 6, 2), + }, + { // cmd: add rule ip tab ch ip daddr rshift 20 99900 + tname: "ntoh size 4 len 4", + opStr: "[ byteorder reg 9 = ntoh(reg 1, 4, 4) ]", + expected: mustCreateByteorder(t, linux.NFT_REG32_01, linux.NFT_REG_1, linux.NFT_BYTEORDER_NTOH, 4, 4), + }, + { // cmd: add rule ip6 tab ch ip6 daddr rshift 90 603 + tname: "ntoh size 8 len 16", + opStr: "[ byteorder reg 1 = ntoh(reg 1, 8, 16) ]", + expected: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_1, linux.NFT_BYTEORDER_NTOH, 16, 8), + }, + { // cmd: add rule ip filter input meta length gt 1000 accept + tname: "hton size 4 len 4", + opStr: "[ byteorder reg 8 = hton(reg 1, 4, 4) ]", + expected: mustCreateByteorder(t, linux.NFT_REG32_00, linux.NFT_REG_1, linux.NFT_BYTEORDER_HTON, 4, 4), + }, + } { + t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkByteorderOp) }) + } +} + +// checkByteorderOp checks that the given operation is a byteorder operation +// and that it matches the expected byteorder operation. +func checkByteorderOp(tname string, expected operation, actual operation) error { + expectedOrder := expected.(*byteorder) + order, ok := actual.(*byteorder) + if !ok { + return fmt.Errorf("expected operation type to be Byteorder for %s, got %T", tname, actual) + } + if order.sreg != expectedOrder.sreg { + return fmt.Errorf("expected source register to be %d for %s, got %d", expectedOrder.sreg, tname, order.sreg) + } + if order.dreg != expectedOrder.dreg { + return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedOrder.dreg, tname, order.dreg) + } + if order.bop != expectedOrder.bop { + return fmt.Errorf("expected byteorder operator to be %v for %s, got %v", expectedOrder.bop, tname, order.bop) + } + if order.blen != expectedOrder.blen { + return fmt.Errorf("expected byteorder length to be %d for %s, got %d", expectedOrder.blen, tname, order.blen) + } + if order.size != expectedOrder.size { + return fmt.Errorf("expected byteorder size to be %d for %s, got %d", expectedOrder.size, tname, order.size) + } + return nil +} + // TestInterpretRule tests the interpretation of basic and general rules as a // list of operations. func TestInterpretRule(t *testing.T) {