diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index 3d38dd6f9..c4debd2f3 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -42,6 +42,8 @@ import ( "fmt" "slices" + "encoding/binary" + "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/tcpip/stack" ) @@ -554,9 +556,6 @@ type Rule struct { // Operation represents a single operation in a rule. type Operation interface { - // TypeString returns the string representation of the type of the operation. - TypeString() string - // evaluate evaluates the operation on the given packet and register set, // changing the register set and possibly the packet in place. evaluate(regs *RegisterSet, pkt *stack.PacketBuffer) @@ -565,6 +564,7 @@ type Operation interface { // Ensures all operations implement the Operation interface at compile time. var ( _ Operation = (*Immediate)(nil) + _ Operation = (*Comparison)(nil) ) // Immediate is an operation that sets the data in a register. @@ -581,14 +581,119 @@ func NewImmediate(dreg uint8, data RegisterData) (*Immediate, error) { return &Immediate{dreg: dreg, data: data}, nil } -// TypeString for Immediate returns "Immediate" as the string operation type. -func (op *Immediate) TypeString() string { return "Immediate" } - // evaluate for Immediate sets the data in the destination register. func (op Immediate) evaluate(regs *RegisterSet, pkt *stack.PacketBuffer) { op.data.StoreData(regs, op.dreg) } +// Comparison is an operation that compares the data in a register to a given +// value and breaks (by setting the verdict register to NFT_BREAK) from the rule +// if the comparison is false. +// Note: comparison operations are not supported for the verdict register. +type Comparison struct { + data RegisterData // Data to compare the source register to. + sreg uint8 // Number of the source register. + cop cmpOp // Comparison operator. +} + +// cmpOp is the comparison operator for a Comparison operation. +// Note: corresponds to enum nft_cmp_op from +// include/uapi/linux/netfilter/nf_tables.h and uses the same constants. +type cmpOp int + +// String for NftCmpOp returns the string representation of the comparison +// operator. +func (cop cmpOp) String() string { + switch cop { + case linux.NFT_CMP_EQ: + return "==" + case linux.NFT_CMP_NEQ: + return "!=" + case linux.NFT_CMP_LT: + return "<" + case linux.NFT_CMP_LTE: + return "<=" + case linux.NFT_CMP_GT: + return ">" + case linux.NFT_CMP_GTE: + return ">=" + default: + panic(fmt.Sprintf("invalid comparison operator: %d", int(cop))) + } +} + +// validateComparisonOp ensures the comparison operator is valid. +func validateComparisonOp(cop cmpOp) error { + switch cop { + case linux.NFT_CMP_EQ, linux.NFT_CMP_NEQ, linux.NFT_CMP_LT, linux.NFT_CMP_LTE, linux.NFT_CMP_GT, linux.NFT_CMP_GTE: + return nil + default: + return fmt.Errorf("invalid comparison operator: %d", int(cop)) + } +} + +// NewComparison creates a new Comparison operation. +func NewComparison(sreg uint8, op int, data RegisterData) (*Comparison, error) { + if sreg == linux.NFT_REG_VERDICT { + return nil, fmt.Errorf("comparison operation cannot use verdict register as source") + } + if err := data.ValidateRegister(sreg); err != nil { + return nil, err + } + cop := cmpOp(op) + if err := validateComparisonOp(cop); err != nil { + return nil, err + } + return &Comparison{sreg: sreg, cop: cop, data: data}, nil +} + +// evaluate for Comparison compares the data in the source register to the given +// data and breaks from the rule if the comparison is false. +func (op Comparison) evaluate(regs *RegisterSet, pkt *stack.PacketBuffer) { + // Gets the data from the source register. + regBuf := getRegisterData(regs, op.sreg, op.data.Type()) + // Gets the data to compare to. + bytesData, ok := op.data.(BytesData) + if !ok { + panic("comparison operation data is not BytesData") + } + // Compares from left to right in 4-byte chunks starting with the rightmost + // byte of every 4-byte chunk since the data is little endian. + // For example, 16-byte IPv6 address 2001:000a:130f:0000:0000:09c0:876a:130b + // is represented as 0x0a000120 0x00000f13 0xc0090000 0x0b136a87 in operations + // and as [0a|00|01|20|00|00|0f|13|c0|09|00|00|0b|13|6a|87] in the byte slice, + // so we compare right to left in the first 4 bytes and then go to the next 4. + dif := 0 + for i := 0; i < len(bytesData.data) && dif == 0; i += 4 { + regVal := binary.LittleEndian.Uint32(regBuf[i : i+4]) + opVal := binary.LittleEndian.Uint32(bytesData.data[i : i+4]) + if regVal < opVal { + dif = -1 + } else if regVal > opVal { + dif = 1 + } + } + var result bool + switch op.cop { + case linux.NFT_CMP_EQ: + result = dif == 0 + case linux.NFT_CMP_NEQ: + result = dif != 0 + case linux.NFT_CMP_LT: + result = dif < 0 + case linux.NFT_CMP_LTE: + result = dif <= 0 + case linux.NFT_CMP_GT: + result = dif > 0 + case linux.NFT_CMP_GTE: + result = dif >= 0 + } + if !result { + // Comparison is false, so break from the rule. + regs.verdict = Verdict{Code: VC(linux.NFT_BREAK)} + } +} + // // Register and Register-Related Implementations. // Note: Registers are represented by type uint8 for the register number. @@ -735,29 +840,33 @@ func (rd BytesData) ValidateRegister(reg uint8) error { return nil } +// getRegisterData is a helper function that gets the appropriate slice of +// register data from the register set. +// Note: does not support verdict data and assumes the register is valid for the +// given data type. +func getRegisterData(regs *RegisterSet, reg uint8, dataType RegisterDataType) []byte { + // 4-byte data in a 4-byte register. + if is4ByteRegister(reg) { + start := (reg - linux.NFT_REG32_00) * linux.NFT_REG32_SIZE + return regs.data[start : start+linux.NFT_REG32_SIZE] + } + // 16-byte data in a 16-byte register. + if dataType == Data16Bytes { + start := (reg - linux.NFT_REG_1) * linux.NFT_REG_SIZE + return regs.data[start : start+linux.NFT_REG_SIZE] + } + // 4-byte data in a 16-byte register + // Leaves excess space on the left (bc the data is little endian). + start := (reg-linux.NFT_REG_1)*linux.NFT_REG_SIZE + linux.NFT_REG_SIZE - linux.NFT_REG32_SIZE + return regs.data[start : start+linux.NFT_REG32_SIZE] +} + // StoreData sets the data in the destination register to the uint32. func (rd BytesData) StoreData(regs *RegisterSet, reg uint8) { if err := rd.ValidateRegister(reg); err != nil { panic(err) } - var start uint8 - var regBuf []byte - // Stores 4-byte data in a 4-byte register. - if is4ByteRegister(reg) { - start = (reg - linux.NFT_REG32_00) * linux.NFT_REG32_SIZE - regBuf = regs.data[start : start+linux.NFT_REG32_SIZE] - } else { - // Stores 16-byte data in a 16-byte register. - if rd.Type() == Data16Bytes { - start = (reg - linux.NFT_REG_1) * linux.NFT_REG_SIZE - regBuf = regs.data[start : start+linux.NFT_REG_SIZE] - } else { - // Stores 4-byte data in a 16-byte register, leaving excess space on the - // left (bc the data is little endian). - start = (reg-linux.NFT_REG_1)*linux.NFT_REG_SIZE + linux.NFT_REG_SIZE - linux.NFT_REG32_SIZE - regBuf = regs.data[start : start+linux.NFT_REG32_SIZE] - } - } + regBuf := getRegisterData(regs, reg, rd.Type()) copy(regBuf, rd.data) } @@ -806,7 +915,7 @@ type Verdict struct { // String returns a string representation of the verdict. func (v Verdict) String() string { - out := VerdictToString(v.Code) + out := VerdictCodeToString(v.Code) if v.ChainName != "" { out += fmt.Sprintf(" -> %s", v.ChainName) } @@ -818,8 +927,8 @@ func VC(v int32) uint32 { return uint32(v) } -// VerdictToString prints names for the supported verdicts. -func VerdictToString(v uint32) string { +// VerdictCodeToString prints names for the supported verdicts. +func VerdictCodeToString(v uint32) string { switch v { // Netfilter (External) Verdicts: case VC(linux.NF_DROP): @@ -916,7 +1025,7 @@ func (nf *NFTables) EvaluateHook(family AddressFamily, hook Hook, pkt *stack.Pac return Verdict{Code: VC(linux.NF_ACCEPT)}, nil } - panic(fmt.Sprintf("unexpected verdict from hook evaluation: %s", VerdictToString(regs.Verdict().Code))) + panic(fmt.Sprintf("unexpected verdict from hook evaluation: %s", VerdictCodeToString(regs.Verdict().Code))) } // evaluateFromRule is a helper function for Chain.evaluate that evaluates the diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index 918181ba2..d240a5097 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -290,6 +290,284 @@ func TestEvaluateImmediate(t *testing.T) { } } +// TestEvaluateComparison tests that the Comparison operation correctly compares +// the data in the source register to the given data. +// Note: Relies on expected behavior of the Immediate operation. +func TestEvaluateComparison(t *testing.T) { + for _, test := range []struct { + tname string + op1 Operation // will be nil if unused + op2 Operation // will be nil if unused + res bool // should be true if we reach end of the rule (no breaks) + }{ + { + tname: "compare register == 4-byte data, true", + op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{0, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, NewBytesData([]byte{0, 0, 0, 0})), + res: true, + }, + { + tname: "compare register == 4-byte data, false", + op1: mustCreateImmediate(t, linux.NFT_REG32_11, NewBytesData([]byte{1, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG32_11, linux.NFT_CMP_EQ, NewBytesData([]byte{0, 0, 0, 0})), + res: false, + }, + { + tname: "compare register != 4-byte data, true", + op1: mustCreateImmediate(t, linux.NFT_REG32_03, NewBytesData([]byte{1, 7, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG32_03, linux.NFT_CMP_NEQ, NewBytesData([]byte{1, 98, 0, 56})), + res: true, + }, + { + tname: "compare register != 4-byte data, false", + op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{1, 98, 0, 56})), + op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_NEQ, NewBytesData([]byte{1, 98, 0, 56})), + res: false, + }, + { + tname: "compare register < 4-byte data, true", + op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{29, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LT, NewBytesData([]byte{100, 0, 0, 0})), + res: true, + }, + { + tname: "compare register < 4-byte data, false eq", + op1: mustCreateImmediate(t, linux.NFT_REG32_04, NewBytesData([]byte{100, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG32_04, linux.NFT_CMP_LT, NewBytesData([]byte{100, 0, 0, 0})), + res: false, + }, + { + tname: "compare register < 4-byte data, false gt", + op1: mustCreateImmediate(t, linux.NFT_REG32_14, NewBytesData([]byte{200, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG32_14, linux.NFT_CMP_LT, NewBytesData([]byte{100, 0, 0, 0})), + res: false, + }, + { + tname: "compare register > 4-byte data, true", + op1: mustCreateImmediate(t, linux.NFT_REG32_15, NewBytesData([]byte{0, 0, 0, 1})), + op2: mustCreateComparison(t, linux.NFT_REG32_15, linux.NFT_CMP_GT, NewBytesData([]byte{29, 76, 230, 0})), + res: true, + }, + { + tname: "compare register > 4-byte data, false eq", + op1: mustCreateImmediate(t, linux.NFT_REG32_07, NewBytesData([]byte{29, 76, 230, 0})), + op2: mustCreateComparison(t, linux.NFT_REG32_07, linux.NFT_CMP_GT, NewBytesData([]byte{29, 76, 230, 0})), + res: false, + }, + { + tname: "compare register > 4-byte data, false lt", + op1: mustCreateImmediate(t, linux.NFT_REG32_05, NewBytesData([]byte{28, 76, 230, 0})), + op2: mustCreateComparison(t, linux.NFT_REG32_05, linux.NFT_CMP_GT, NewBytesData([]byte{29, 76, 230, 0})), + res: false, + }, + { + tname: "compare register <= 4-byte data, true lt", + op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{29, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LTE, NewBytesData([]byte{100, 0, 0, 0})), + res: true, + }, + { + tname: "compare register <= 4-byte data, true eq", + op1: mustCreateImmediate(t, linux.NFT_REG32_09, NewBytesData([]byte{100, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG32_09, linux.NFT_CMP_LTE, NewBytesData([]byte{100, 0, 0, 0})), + res: true, + }, + { + tname: "compare register <= 4-byte data, false", + op1: mustCreateImmediate(t, linux.NFT_REG32_06, NewBytesData([]byte{200, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_LTE, NewBytesData([]byte{100, 0, 0, 0})), + res: false, + }, + { + tname: "compare register >= 4-byte data, true gt", + op1: mustCreateImmediate(t, linux.NFT_REG32_12, NewBytesData([]byte{0, 0, 0, 1})), + op2: mustCreateComparison(t, linux.NFT_REG32_12, linux.NFT_CMP_GTE, NewBytesData([]byte{29, 76, 230, 0})), + res: true, + }, + { + tname: "compare register >= 4-byte data, true eq", + op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{29, 76, 230, 0})), + op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_GTE, NewBytesData([]byte{29, 76, 230, 0})), + res: true, + }, + { + tname: "compare register >= 4-byte data, false", + op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{28, 76, 230, 0})), + op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, NewBytesData([]byte{29, 76, 230, 0})), + res: false, + }, + { + tname: "compare register == 16-byte data, true", + op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, NewBytesData([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), + res: true, + }, + { + tname: "compare register == 16-byte data, false", + op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), + op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, NewBytesData([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})), + res: false, + }, + { + tname: "compare register != 16-byte data, true", + op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})), + op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_NEQ, NewBytesData([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15})), + res: true, + }, + { + tname: "compare register != 16-byte data, false", + op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})), + op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_NEQ, NewBytesData([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})), + res: false, + }, + { + tname: "compare register < 16-byte data, true", + op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x1f, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0xaa})), + op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: true, + }, + { + tname: "compare register < 16-byte data, false eq", + op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: false, + }, + { + tname: "compare register < 16-byte data, false gt", + op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x21, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0xaa})), + op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: false, + }, + { + tname: "compare register > 16-byte data, true", + op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x21, 0xaa, 0xaa, 0xaa, 0xaa, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0xcc, 0xcc, 0xcc, 0xcc, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: true, + }, + { + tname: "compare register > 16-byte data, false eq", + op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_GT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: false, + }, + { + tname: "compare register > 16-byte data, false lt", + op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x1f, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x90})), + op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: false, + }, + { + tname: "compare register <= 16-byte data, true lt", + op1: mustCreateImmediate(t, linux.NFT_REG_1, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x86})), + op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: true, + }, + { + tname: "compare register <= 16-byte data, true eq", + op1: mustCreateImmediate(t, linux.NFT_REG_2, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: true, + }, + { + tname: "compare register <= 16-byte data, false", + op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0xaa, 0x00, 0x0b, 0x13, 0x6a, 0x88})), + op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: false, + }, + { + tname: "compare register >= 16-byte data, true gt", + op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0xaa, 0xaa, 0xaa, 0x20, 0xaa, 0xaa, 0xaa, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: true, + }, + { + tname: "compare register >= 16-byte data, true eq", + op1: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0xab, 0xbc, 0xcd, 0xde, 0xef, 0x00, 0x01, 0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x90})), + op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, NewBytesData([]byte{0xab, 0xbc, 0xcd, 0xde, 0xef, 0x00, 0x01, 0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x90})), + res: true, + }, + { + tname: "compare register >= 16-byte data, false", + op1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0a, 0x13, 0x6a, 0x85})), + op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + res: false, + }, + { + tname: "compare empty 4-byte register, true", + op1: mustCreateComparison(t, linux.NFT_REG32_10, linux.NFT_CMP_EQ, NewBytesData([]byte{0, 0, 0, 0})), + res: true, + }, + { + tname: "compare empty 4-byte register, false", + op1: mustCreateComparison(t, linux.NFT_REG32_11, linux.NFT_CMP_EQ, NewBytesData([]byte{1, 0, 0, 0})), + res: false, + }, + { + tname: "compare empty 16-byte register, true", + op1: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LT, NewBytesData([]byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), + res: true, + }, + { + tname: "compare empty 16-byte register, false", + op1: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, NewBytesData([]byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), + res: false, + }, + } { + 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) + } + + // Add an operation that drops. This is what the final verdict should be + // if all the comparisons are true (res = 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 := makeTestingPacket() + v, err := nf.EvaluateHook(arbitraryFamily, arbitraryHook, pkt) + if err != nil { + t.Fatalf("unexpected error for EvaluateHook: %v", err) + } + // If all comparisons are true, the packet will get to the end of the rule + // and the last operation above will set the final verdict to oppose the + // base chain policy. If any comparison is false, the comparison operation + // will break from the rule and the final verdict will default to the base + // chain policy. + if test.res { + if v.Code != VC(linux.NF_DROP) { + t.Fatalf("expected verdict Drop for %t result, got %v", test.res, v) + } + } else { + if v.Code != VC(linux.NF_ACCEPT) { + t.Fatalf("expected base chain policy verdict Accept for %t result, got %v", test.res, v) + } + } + }) + } +} + // TestLoopCheckOnRegisterAndUnregister tests the loop checking and accompanying // logic on registering and unregistering rules. func TestLoopCheckOnRegisterAndUnregister(t *testing.T) { @@ -882,3 +1160,12 @@ func mustCreateImmediate(t *testing.T, dreg uint8, data RegisterData) *Immediate } return imm } + +// mustCreateComparison wraps the NewComparison function for brevity. +func mustCreateComparison(t *testing.T, sreg uint8, cop int, data RegisterData) *Comparison { + cmp, err := NewComparison(sreg, cop, data) + if err != nil { + t.Fatalf("failed to create comparison: %v", err) + } + return cmp +} diff --git a/pkg/tcpip/nftables/nftinterp.go b/pkg/tcpip/nftables/nftinterp.go index caa9d6ec3..8e3727659 100644 --- a/pkg/tcpip/nftables/nftinterp.go +++ b/pkg/tcpip/nftables/nftinterp.go @@ -138,6 +138,8 @@ func InterpretOperation(line string, lnIdx int) (Operation, error) { switch tokens[1] { case "immediate": return InterpretImmediate(line, lnIdx) + case "cmp": + return InterpretComparison(line, lnIdx) default: return nil, &SyntaxError{lnIdx, 1, fmt.Sprintf("unrecognized operation type: %s", tokens[1])} } @@ -199,6 +201,69 @@ func InterpretImmediate(line string, lnIdx int) (Operation, error) { return imm, nil } +// InterpretComparison creates a new Comparison operation from the given string. +func InterpretComparison(line string, lnIdx int) (Operation, error) { + tokens := strings.Fields(line) + + // Requires at least 7 tokens: + // "[", "cmp", op, "reg", register index, register value, "]". + if len(tokens) < 7 { + return nil, &SyntaxError{lnIdx, 0, fmt.Sprintf("incorrect number of tokens for cmp operation, should be at least 7, got %d", len(tokens))} + } + + if err := checkOperationBrackets(tokens, lnIdx); err != nil { + return nil, err + } + + tkIdx := 1 + + // First token should be "cmp". + if err := consumeToken("cmp", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Second token should be the comparison operator. + cop, err := parseCmpOp(tokens[tkIdx], lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Third token should be "reg". + if err := consumeToken("reg", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Fourth token should be the uint8 representing the register index. + reg, err := parseRegister(tokens[tkIdx], lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Fifth token should be the value. + nextIdx, data, err := parseRegisterData(reg, 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 comparison operation"} + } + + // Create the operation with the specified arguments. + cmp, err := NewComparison(reg, cop, data) + if err != nil { + return nil, &LogicError{lnIdx, tkIdx, err} + } + + return cmp, nil +} + // // Interpreter Helper Functions. // @@ -326,6 +391,26 @@ func parseHexData(tokens []string, lnIdx int, tkIdx int) (int, RegisterData, err return 0, nil, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("incorrect number of bytes for hexadecimal data, should be 4 or 16, got %d", len(bytes))} } +// parseCmpOp parses the int representing the cmpOp from the given string. +func parseCmpOp(copString string, lnIdx int, tkIdx int) (int, error) { + switch copString { + case "eq": + return linux.NFT_CMP_EQ, nil + case "neq": + return linux.NFT_CMP_NEQ, nil + case "lt": + return linux.NFT_CMP_LT, nil + case "lte": + return linux.NFT_CMP_LTE, nil + case "gt": + return linux.NFT_CMP_GT, nil + case "gte": + return linux.NFT_CMP_GTE, nil + default: + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid comparison operator: '%s'", copString)} + } +} + // consumeToken is a helper function that checks if the token at the given index // matches the expected string, returning a SyntaxError if not. func consumeToken(expected string, tokens []string, lnIdx int, tkIdx int) error { diff --git a/pkg/tcpip/nftables/nftinterp_test.go b/pkg/tcpip/nftables/nftinterp_test.go index 4fce65597..1edeb17c8 100644 --- a/pkg/tcpip/nftables/nftinterp_test.go +++ b/pkg/tcpip/nftables/nftinterp_test.go @@ -21,145 +21,264 @@ import ( "gvisor.dev/gvisor/pkg/abi/linux" ) -func TestInterpretImmediateOps(t *testing.T) { - for _, test := range []struct { - tname string - opStr string - op *Immediate // will be nil if an error is expected - }{ - { - tname: "verdict register with accept verdict", - opStr: "[ immediate reg 0 accept ]", - op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), - }, - { - tname: "verdict register with drop verdict", - opStr: "[ immediate reg 0 drop ]", - op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), - }, - { - tname: "verdict register with continue verdict", - opStr: "[ immediate reg 0 continue ]", - op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})), - }, - { - tname: "verdict register with return verdict", - opStr: "[ immediate reg 0 return ]", - op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_RETURN)})), - }, - { - tname: "verdict register with jump verdict", - opStr: "[ immediate reg 0 jump -> next_chain ]", - op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_JUMP), ChainName: "next_chain"})), - }, - { - tname: "verdict register with goto verdict", - opStr: "[ immediate reg 0 goto -> next_chain ]", - op: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_GOTO), ChainName: "next_chain"})), - }, - { - tname: "verdict register with 4-byte data", - opStr: "[ immediate reg 0 0x0201a8c0 ]", - op: nil, - }, - { - tname: "verdict register with 16-byte data", - opStr: "[ immediate reg 0 0xb80d0120 0x00000000 0x00000000 0x02000000 ]", - op: nil, - }, - { - tname: "16-byte register with verdict data", - opStr: "[ immediate reg 1 accept ]", - op: nil, - }, - { - tname: "16-byte register with verdict data with target", - opStr: "[ immediate reg 2 jump -> next_chain ]", - op: nil, - }, - { - tname: "16-byte register with 4-byte data", - opStr: "[ immediate reg 3 0x0201a8c0 ]", - op: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0x02, 0x01, 0xa8, 0xc0})), - }, - { - tname: "16-byte register with 16-byte data", - opStr: "[ immediate reg 4 0xb80d0120 0x00000000 0x00000000 0x02000000 ]", - op: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0xb8, 0x0d, 0x01, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00})), - }, - { - tname: "16-byte register with 8-byte data", - opStr: "[ immediate reg 4 0xb80d0120 0x00000050 ]", - op: nil, - }, - { - tname: "4-byte register with verdict data", - opStr: "[ immediate reg 8 return ]", - op: nil, - }, - { - tname: "4-byte register with verdict data with target", - opStr: "[ immediate reg 9 goto -> next_chain ]", - op: nil, - }, - { - tname: "4-byte register with 4-byte data", - opStr: "[ immediate reg 10 0x0201a8c0 ]", - op: mustCreateImmediate(t, linux.NFT_REG32_02, NewBytesData([]byte{0x02, 0x01, 0xa8, 0xc0})), - }, - { - tname: "4-byte register with 16-byte data", - opStr: "[ immediate reg 9 0xb80d0120 0x00000000 0x00000000 0x02000000 ]", - op: nil, - }, - } { - t.Run(test.tname, func(t *testing.T) { - rule, err := InterpretRule(test.opStr) - if err != nil { - if test.op == nil { - return - } - t.Fatalf("unexpected interpretation error for %s: %v", test.tname, err) - } +// interpretOperationTestAction is a generic action for testing the +// interpretation of an operation. +type interpretOperationTestAction struct { + tname string + opStr string + expected Operation // will be nil if an error is expected +} - if len(rule.ops) != 1 { - t.Fatalf("expected single operation for %s, got %d", test.tname, len(rule.ops)) - } - op := rule.ops[0] - if err := checkImmediateOp(test.tname, test.op, op); err != nil { - t.Fatalf(err.Error()) - } - }) +// checkOp is a generic operation validation function used for testing that +// the interpretation of an operation matches the expected operation. +func checkOp(t *testing.T, test interpretOperationTestAction, checkFunc func(string, Operation, Operation) error) { + rule, err := InterpretRule(test.opStr) + if test.expected == nil { + if err == nil { + t.Fatalf("unexpected interpretation success for %s", test.tname) + } + return + } + if err != nil { + t.Fatalf("unexpected interpretation error for %s: %v", test.tname, err) + } + if len(rule.ops) != 1 { + t.Fatalf("expected single operation for %s, got %d", test.tname, len(rule.ops)) + } + actual := rule.ops[0] + if actual == nil { + t.Fatalf("expected non-nil operation for %s, got nil", test.tname) + } + if err := checkFunc(test.tname, test.expected, actual); err != nil { + t.Fatalf(err.Error()) } } -func checkImmediateOp(tname string, expected *Immediate, actual Operation) error { - if actual == nil { - return fmt.Errorf("expected non-nil operation for %s, got nil", tname) +// TestInterpretImmediateOps tests the interpretation of immediate operations. +func TestInterpretImmediateOps(t *testing.T) { + for _, test := range []interpretOperationTestAction{ + { + tname: "verdict register with accept verdict", + opStr: "[ immediate reg 0 accept ]", + expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + }, + { + tname: "verdict register with drop verdict", + opStr: "[ immediate reg 0 drop ]", + expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + }, + { + tname: "verdict register with continue verdict", + opStr: "[ immediate reg 0 continue ]", + expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})), + }, + { + tname: "verdict register with return verdict", + opStr: "[ immediate reg 0 return ]", + expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_RETURN)})), + }, + { + tname: "verdict register with jump verdict", + opStr: "[ immediate reg 0 jump -> next_chain ]", + expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_JUMP), ChainName: "next_chain"})), + }, + { + tname: "verdict register with goto verdict", + opStr: "[ immediate reg 0 goto -> next_chain ]", + expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_GOTO), ChainName: "next_chain"})), + }, + { + tname: "verdict register with 4-byte data", + opStr: "[ immediate reg 0 0x0201a8c0 ]", + expected: nil, + }, + { + tname: "verdict register with 16-byte data", + opStr: "[ immediate reg 0 0xb80d0120 0x00000000 0x00000000 0x02000000 ]", + expected: nil, + }, + { + tname: "16-byte register with verdict data", + opStr: "[ immediate reg 1 accept ]", + expected: nil, + }, + { + tname: "16-byte register with verdict data with target", + opStr: "[ immediate reg 2 jump -> next_chain ]", + expected: nil, + }, + { + tname: "16-byte register with 4-byte data", + opStr: "[ immediate reg 3 0x0201a8c0 ]", + expected: mustCreateImmediate(t, linux.NFT_REG_3, NewBytesData([]byte{0x02, 0x01, 0xa8, 0xc0})), + }, + { + tname: "16-byte register with 16-byte data", + opStr: "[ immediate reg 4 0xb80d0120 0x00000000 0x00000000 0x02000000 ]", + expected: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0xb8, 0x0d, 0x01, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00})), + }, + { + tname: "16-byte register with 8-byte data", + opStr: "[ immediate reg 4 0xb80d0120 0x00000050 ]", + expected: nil, + }, + { + tname: "4-byte register with verdict data", + opStr: "[ immediate reg 8 return ]", + expected: nil, + }, + { + tname: "4-byte register with verdict data with target", + opStr: "[ immediate reg 9 goto -> next_chain ]", + expected: nil, + }, + { + tname: "4-byte register with 4-byte data", + opStr: "[ immediate reg 10 0x0201a8c0 ]", + expected: mustCreateImmediate(t, linux.NFT_REG32_02, NewBytesData([]byte{0x02, 0x01, 0xa8, 0xc0})), + }, + { + tname: "4-byte register with 16-byte data", + opStr: "[ immediate reg 9 0xb80d0120 0x00000000 0x00000000 0x02000000 ]", + expected: nil, + }, + } { + t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkImmediateOp) }) } +} + +// checkImmediateOp checks that the given operation is an immediate operation +// and that it matches the expected immediate operation. +func checkImmediateOp(tname string, expected Operation, actual Operation) error { + expectedImm := expected.(*Immediate) imm, ok := actual.(*Immediate) if !ok { - return fmt.Errorf("expected operation type to be Immediate for %s, got %s", tname, actual.TypeString()) + return fmt.Errorf("expected operation type to be Immediate for %s, got %T", tname, actual) } - if imm.dreg != expected.dreg { - return fmt.Errorf("expected register to be %d for %s, got %d", expected.dreg, tname, imm.dreg) + if imm.dreg != expectedImm.dreg { + return fmt.Errorf("expected register to be %d for %s, got %d", expectedImm.dreg, tname, imm.dreg) } - if !imm.data.Equal(expected.data) { - return fmt.Errorf("expected data to be %s for %s, got %s", expected.data.String(), tname, imm.data.String()) + if !imm.data.Equal(expectedImm.data) { + return fmt.Errorf("expected data to be %v for %s, got %v", expectedImm.data, tname, imm.data) } return nil } +// TestInterpretComparisonOps tests the interpretation of comparison operations. +func TestInterpretComparisonOps(t *testing.T) { + for _, test := range []interpretOperationTestAction{ + { + tname: "verdict register with 4-byte data comparison", + opStr: "[ cmp eq reg 0 0x00000002 ]", + expected: nil, + }, + { + tname: "register == 4-byte data", + opStr: "[ cmp eq reg 1 0x0302010a ]", + expected: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, NewBytesData([]byte{0x03, 0x02, 0x01, 0x0a})), + }, + { + tname: "register != 4-byte data", + opStr: "[ cmp neq reg 2 0x00000064 ]", + expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_NEQ, NewBytesData([]byte{0x00, 0x00, 0x00, 0x64})), + }, + { + tname: "register < 4-byte data", + opStr: "[ cmp lt reg 3 0x00000000 ]", + expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, NewBytesData([]byte{0x00, 0x00, 0x00, 0x00})), + }, + { + tname: "register <= 4-byte data", + opStr: "[ cmp lte reg 4 0x00000164 ]", + expected: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, NewBytesData([]byte{0x00, 0x00, 0x01, 0x64})), + }, + { + tname: "register > 4-byte data", + opStr: "[ cmp gt reg 8 0xe8030000 ]", + expected: mustCreateComparison(t, linux.NFT_REG32_00, linux.NFT_CMP_GT, NewBytesData([]byte{0xe8, 0x03, 0x00, 0x00})), + }, + { + tname: "register >= 4-byte data", + opStr: "[ cmp gte reg 9 0xc02b0000 ]", + expected: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_GTE, NewBytesData([]byte{0xc0, 0x2b, 0x00, 0x00})), + }, + { + tname: "verdict register with 16-byte data comparison", + opStr: "[ cmp gt reg 0 0xb80d0120 0x00000000 0x00000000 0x02000000 ]", + expected: nil, + }, + { + tname: "4-byte register with 16-byte data comparison", + opStr: "[ cmp lte reg 8 0x0302010a 0x00000000 0x00000000 0x02000001 ]", + expected: nil, + }, + { + tname: "register == 16-byte data", + opStr: "[ cmp eq reg 1 0x0302010a 0x00000000 0x00000000 0x02000002 ]", + expected: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, NewBytesData([]byte{0x03, 0x02, 0x01, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02})), + }, + { + tname: "register != 16-byte data", + opStr: "[ cmp neq reg 2 0x00000064 0x00000000 0x00000000 0x02000000 ]", + expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_NEQ, NewBytesData([]byte{0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00})), + }, + { + tname: "register < 16-byte data", + opStr: "[ cmp lt reg 3 0x00000000 0x00000000 0x00000000 0x00000000 ]", + expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, NewBytesData([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})), + }, + { + tname: "register <= 16-byte data", + opStr: "[ cmp lte reg 4 0x00000164 0x00000164 0x00000164 0x00000164 ]", + expected: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, NewBytesData([]byte{0x00, 0x00, 0x01, 0x64, 0x00, 0x00, 0x01, 0x64, 0x00, 0x00, 0x01, 0x64, 0x00, 0x00, 0x01, 0x64})), + }, + { + tname: "register > 16-byte data", + opStr: "[ cmp gt reg 2 0xe8030000 0x00000f13 0xc0090000 0x0b136a87 ]", + expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, NewBytesData([]byte{0xe8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + }, + { + tname: "register >= 16-byte data", + opStr: "[ cmp gte reg 3 0x0a000120 0x00000f13 0xc0090000 0x0b136a87 ]", + expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, NewBytesData([]byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87})), + }, + } { + t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkComparisonOp) }) + } +} + +// checkComparisonOp checks that the given operation is an comparison operation +// and that it matches the expected comparison operation. +func checkComparisonOp(tname string, expected Operation, actual Operation) error { + expectedCmp := expected.(*Comparison) + cmp, ok := actual.(*Comparison) + if !ok { + return fmt.Errorf("expected operation type to be Comparison for %s, got %T", tname, actual) + } + if cmp.sreg != expectedCmp.sreg { + return fmt.Errorf("expected register to be %d for %s, got %d", expectedCmp.sreg, tname, cmp.sreg) + } + if cmp.cop != expectedCmp.cop { + return fmt.Errorf("expected comparison operator to be %v for %s, got %v", expectedCmp.cop, tname, cmp.cop) + } + if !cmp.data.Equal(expectedCmp.data) { + return fmt.Errorf("expected data to be %v for %s, got %v", expectedCmp.data, tname, cmp.data) + } + return nil +} + +// TestInterpretRule tests the interpretation of basic and general rules as a +// list of operations. func TestInterpretRule(t *testing.T) { for _, test := range []struct { - tname string - ruleStr string - rule *Rule // will be nil if an error is expected + tname string + ruleStr string + expected *Rule // will be nil if an error is expected }{ { - tname: "empty ruleset", - ruleStr: ``, - rule: &Rule{}, + tname: "empty ruleset", + ruleStr: ``, + expected: &Rule{}, }, { tname: "empty ruleset with excess whitespace", @@ -167,33 +286,40 @@ func TestInterpretRule(t *testing.T) { `, - rule: &Rule{}, + expected: &Rule{}, }, } { t.Run(test.tname, func(t *testing.T) { rule, err := InterpretRule(test.ruleStr) - if err != nil { - if test.rule == nil { - return + if test.expected == nil { + if err == nil { + t.Fatalf("unexpected interpretation success for %s", test.tname) } + return + } + if err != nil { t.Fatalf("unexpected interpretation error for %s: %v", test.tname, err) } - if len(rule.ops) != len(test.rule.ops) { - t.Fatalf("expected %d operations for %s, got %d", len(test.rule.ops), test.tname, len(rule.ops)) + if len(rule.ops) != len(test.expected.ops) { + t.Fatalf("expected %d operations for %s, got %d", len(test.expected.ops), test.tname, len(rule.ops)) } // Checks each operation in the rule with the appropriate check function. for i, op := range rule.ops { - testOp := test.rule.ops[i] + testOp := test.expected.ops[i] switch testOp.(type) { case *Immediate: - if err := checkImmediateOp(test.tname, testOp.(*Immediate), op); err != nil { + if err := checkImmediateOp(test.tname, testOp, op); err != nil { + t.Fatalf(err.Error()) + } + case *Comparison: + if err := checkComparisonOp(test.tname, testOp, op); err != nil { t.Fatalf(err.Error()) } // TODO(b/345684870): cases will be added here as more types are supported. default: - t.Fatalf("unexpected operation type for %s: %s", test.tname, testOp.TypeString()) + t.Fatalf("unexpected operation type for %s: %T", test.tname, testOp) } } })