diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index f495ced8d..43061bdcd 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -41,6 +41,7 @@ package nftables import ( + "bytes" "encoding/binary" "fmt" "slices" @@ -576,6 +577,7 @@ type operation interface { var ( _ operation = (*immediate)(nil) _ operation = (*comparison)(nil) + _ operation = (*ranged)(nil) _ operation = (*payloadLoad)(nil) _ operation = (*payloadSet)(nil) _ operation = (*bitwise)(nil) @@ -615,8 +617,7 @@ type comparison struct { // 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. +// String for cmpOp returns string representation of the comparison operator. func (cop cmpOp) String() string { switch cop { case linux.NFT_CMP_EQ: @@ -667,18 +668,12 @@ func newComparison(sreg uint8, op int, data []byte) (*comparison, error) { func (op comparison) evaluate(regs *registerSet, pkt *stack.PacketBuffer) { // Gets the data to compare to. data := op.data.data + // Gets the data from the source register. - regBuf := getRegisterBuffer(regs, op.sreg) + regBuf := getRegisterBuffer(regs, op.sreg)[:len(data)] // Compares bytes from left to right for all bytes in the comparison data. - dif := 0 - for i := 0; i < len(data) && dif == 0; i++ { - if regBuf[i] < data[i] { - dif = -1 - } else if regBuf[i] > data[i] { - dif = 1 - } - } + dif := bytes.Compare(regBuf, data) // Determines the comparison result depending on the operator. var result bool @@ -702,6 +697,93 @@ func (op comparison) evaluate(regs *registerSet, pkt *stack.PacketBuffer) { } } +// ranged is an operation that checks whether the data in a register is between +// an inclusive range and breaks if the comparison is false. +// Note: ranged operations are not supported for the verdict register. +// Note: named "ranged" because "range" is a reserved keyword in Go. +type ranged struct { + low bytesData // Data to compare the source register to. + high bytesData // Data to compare the source register to. + sreg uint8 // Number of the source register. + rop rngOp // Range operator. + + // Note: The linux kernel defines the range operation, but we have not been + // able to observe it used by the nft binary. For any commands that may use + // range, the nft binary seems to use two comparison operations instead. Thus, + // there is no interpretation of the range operation via the nft binary debug + // output, but the operation is fully supported and implemented. +} + +// rngOp is the range operator for a Ranged operation. +// Note: corresponds to enum nft_range_ops from +// include/uapi/linux/netfilter/nf_tables.h and uses the same constants. +type rngOp int + +// String for rngOp returns string representation of the range operator. +func (rop rngOp) String() string { + switch rop { + case linux.NFT_RANGE_EQ: + return "range ==" + case linux.NFT_CMP_NEQ: + return "range !=" + default: + panic(fmt.Sprintf("invalid range operator: %d", int(rop))) + } +} + +// validateRangeOp ensures the range operator is valid. +func validateRangeOp(rop rngOp) error { + switch rop { + case linux.NFT_RANGE_EQ, linux.NFT_RANGE_NEQ: + return nil + default: + return fmt.Errorf("invalid range operator: %d", int(rop)) + } +} + +// newRanged creates a new Ranged operation. +func newRanged(sreg uint8, op int, low, high []byte) (*ranged, error) { + if sreg == linux.NFT_REG_VERDICT { + return nil, fmt.Errorf("comparison operation cannot use verdict register as source") + } + if len(low) != len(high) { + return nil, fmt.Errorf("upper and lower bounds for ranged operation must be the same length") + } + lowData := newBytesData(low) + if err := lowData.validateRegister(sreg); err != nil { + return nil, err + } + highData := newBytesData(high) + if err := highData.validateRegister(sreg); err != nil { + return nil, err + } + rop := rngOp(op) + if err := validateRangeOp(rop); err != nil { + return nil, err + } + return &ranged{sreg: sreg, rop: rop, low: lowData, high: highData}, nil +} + +// evaluate for Ranged checks whether the source register data is within the +// specified inclusive range and breaks from the rule if comparison is false. +func (op ranged) evaluate(regs *registerSet, pkt *stack.PacketBuffer) { + // Gets the upper and lower bounds as bytesData. + low, high := op.low.data, op.high.data + + // Gets the data from the source register. + regBuf := getRegisterBuffer(regs, op.sreg)[:len(low)] + + // Compares register data to both lower and upper bounds. + d1 := bytes.Compare(regBuf, low) + d2 := bytes.Compare(regBuf, high) + + // Determines the comparison result depending on the operator. + if (d1 >= 0 && d2 <= 0) != (op.rop == linux.NFT_RANGE_EQ) { + // Comparison is false, so break from the rule. + regs.verdict = Verdict{Code: VC(linux.NFT_BREAK)} + } +} + // payloadLoad is an operation that loads data from the packet payload into a // register. // Note: payload operations are not supported for the verdict register. diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index 4ea18691b..ff62d6b96 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -1113,6 +1113,237 @@ func TestEvaluateComparison(t *testing.T) { } } +// TestEvaluateRanged tests that the Ranged operation correctly checks that the +// the data in the source register is within the specified inclusive range. +// Note: Relies on expected behavior of the Immediate operation. +func TestEvaluateRanged(t *testing.T) { + for _, test := range []struct { + tname string + op1 operation // Immediate operation that sets the source register. + op2 operation // Ranged operation to test. + res bool // should be true if we reach end of the rule (no breaks) + }{ + // 4-byte ranges, alternates between 4-byte and 16-byte registers. + { + tname: "4-byte data eq within range", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(1, 4))), + op2: mustCreateRanged(t, linux.NFT_REG_1, linux.NFT_RANGE_EQ, numToBE(0, 4), numToBE(5, 4)), + res: true, + }, + { + tname: "4-byte data neq within range", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(4, 4))), + op2: mustCreateRanged(t, linux.NFT_REG_1, linux.NFT_RANGE_NEQ, numToBE(0, 4), numToBE(5, 4)), + res: false, + }, + { + tname: "4-byte data eq below range", + op1: mustCreateImmediate(t, linux.NFT_REG32_00, newBytesData(numToBE(1, 4))), + op2: mustCreateRanged(t, linux.NFT_REG32_00, linux.NFT_RANGE_EQ, numToBE(3, 4), numToBE(5, 4)), + res: false, + }, + { + tname: "4-byte data neq below range", + op1: mustCreateImmediate(t, linux.NFT_REG32_00, newBytesData(numToBE(1, 4))), + op2: mustCreateRanged(t, linux.NFT_REG32_00, linux.NFT_RANGE_NEQ, numToBE(3, 4), numToBE(5, 4)), + res: true, + }, + { + tname: "4-byte data eq above range", + op1: mustCreateImmediate(t, linux.NFT_REG32_00, newBytesData(numToBE(954, 4))), + op2: mustCreateRanged(t, linux.NFT_REG32_00, linux.NFT_RANGE_EQ, numToBE(3, 4), numToBE(5, 4)), + res: false, + }, + { + tname: "4-byte data neq above range", + op1: mustCreateImmediate(t, linux.NFT_REG32_00, newBytesData(numToBE(954, 4))), + op2: mustCreateRanged(t, linux.NFT_REG32_00, linux.NFT_RANGE_NEQ, numToBE(3, 4), numToBE(5, 4)), + res: true, + }, + { + tname: "4-byte data eq on lower bound", + op1: mustCreateImmediate(t, linux.NFT_REG32_00, newBytesData(numToBE(1, 4))), + op2: mustCreateRanged(t, linux.NFT_REG32_00, linux.NFT_RANGE_EQ, numToBE(1, 4), numToBE(5, 4)), + res: true, + }, + { + tname: "4-byte data neq on lower bound", + op1: mustCreateImmediate(t, linux.NFT_REG32_00, newBytesData(numToBE(1, 4))), + op2: mustCreateRanged(t, linux.NFT_REG32_00, linux.NFT_RANGE_NEQ, numToBE(1, 4), numToBE(5, 4)), + res: false, + }, + { + tname: "4-byte data eq on upper bound", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(100, 4))), + op2: mustCreateRanged(t, linux.NFT_REG_4, linux.NFT_RANGE_EQ, numToBE(4, 4), numToBE(100, 4)), + res: true, + }, + { + tname: "4-byte data neq on upper bound", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(100, 4))), + op2: mustCreateRanged(t, linux.NFT_REG_4, linux.NFT_RANGE_NEQ, numToBE(4, 4), numToBE(100, 4)), + res: false, + }, + { + tname: "4-byte data eq on point range", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(123, 4))), + op2: mustCreateRanged(t, linux.NFT_REG_4, linux.NFT_RANGE_EQ, numToBE(123, 4), numToBE(123, 4)), + res: true, + }, + { + tname: "4-byte data neq on point range", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(123, 4))), + op2: mustCreateRanged(t, linux.NFT_REG_4, linux.NFT_RANGE_NEQ, numToBE(123, 4), numToBE(123, 4)), + res: false, + }, + // 8-byte ranges. + { + tname: "8-byte data eq within range", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(1, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_1, linux.NFT_RANGE_EQ, numToBE(0, 8), numToBE(5, 8)), + res: true, + }, + { + tname: "8-byte data neq within range", + op1: mustCreateImmediate(t, linux.NFT_REG_2, newBytesData(numToBE(4, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_2, linux.NFT_RANGE_NEQ, numToBE(0, 8), numToBE(5, 8)), + res: false, + }, + { + tname: "8-byte data eq below range", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData(numToBE(1, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_3, linux.NFT_RANGE_EQ, numToBE(3, 8), numToBE(5, 8)), + res: false, + }, + { + tname: "8-byte data neq below range", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(1, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_4, linux.NFT_RANGE_NEQ, numToBE(3, 8), numToBE(5, 8)), + res: true, + }, + { + tname: "8-byte data eq above range", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(954, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_1, linux.NFT_RANGE_EQ, numToBE(3, 8), numToBE(5, 8)), + res: false, + }, + { + tname: "8-byte data neq above range", + op1: mustCreateImmediate(t, linux.NFT_REG_2, newBytesData(numToBE(954, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_2, linux.NFT_RANGE_NEQ, numToBE(3, 8), numToBE(5, 8)), + res: true, + }, + { + tname: "8-byte data eq on lower bound", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData(numToBE(1, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_3, linux.NFT_RANGE_EQ, numToBE(1, 8), numToBE(5, 8)), + res: true, + }, + { + tname: "8-byte data neq on lower bound", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(1, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_4, linux.NFT_RANGE_NEQ, numToBE(1, 8), numToBE(5, 8)), + res: false, + }, + { + tname: "8-byte data eq on upper bound", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(100, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_4, linux.NFT_RANGE_EQ, numToBE(4, 8), numToBE(100, 8)), + res: true, + }, + { + tname: "8-byte data neq on upper bound", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData(numToBE(100, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_4, linux.NFT_RANGE_NEQ, numToBE(4, 8), numToBE(100, 8)), + res: false, + }, + { + tname: "8-byte data eq on point range", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData(numToBE(123, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_1, linux.NFT_RANGE_EQ, numToBE(123, 8), numToBE(123, 8)), + res: true, + }, + { + tname: "8-byte data neq on point range", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData(numToBE(123, 8))), + op2: mustCreateRanged(t, linux.NFT_REG_3, linux.NFT_RANGE_NEQ, numToBE(123, 8), numToBE(123, 8)), + res: false, + }, + // simpler 16-byte ranges. + { + tname: "16-byte data eq within range", + op1: mustCreateImmediate(t, linux.NFT_REG_1, newBytesData([]byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), + op2: mustCreateRanged(t, linux.NFT_REG_1, linux.NFT_RANGE_EQ, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}, []byte{5, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}), + res: true, + }, + { + tname: "16-byte data neq within range", + op1: mustCreateImmediate(t, linux.NFT_REG_2, newBytesData([]byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), + op2: mustCreateRanged(t, linux.NFT_REG_2, linux.NFT_RANGE_NEQ, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}, []byte{5, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}), + res: false, + }, + { + tname: "16-byte data eq outside range", + op1: mustCreateImmediate(t, linux.NFT_REG_3, newBytesData([]byte{0x45, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), + op2: mustCreateRanged(t, linux.NFT_REG_3, linux.NFT_RANGE_EQ, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}, []byte{5, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}), + res: false, + }, + { + tname: "16-byte data neq outside range", + op1: mustCreateImmediate(t, linux.NFT_REG_4, newBytesData([]byte{0x45, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})), + op2: mustCreateRanged(t, linux.NFT_REG_4, linux.NFT_RANGE_NEQ, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}, []byte{5, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}), + res: true, + }, + } { + 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) + } + + // 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 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) + } + } + }) + } +} + // TestEvaluatePayloadLoad tests that the Payload Load operation correctly loads // the specified payload into the destination register. // The nft binary commands used to generate these are stated above each test. @@ -2692,6 +2923,15 @@ func mustCreateComparison(t *testing.T, sreg uint8, cop int, data []byte) *compa return cmp } +// mustCreateRanged wraps the newRanged function for brevity. +func mustCreateRanged(t *testing.T, sreg uint8, rop int, low, high []byte) *ranged { + rng, err := newRanged(sreg, rop, low, high) + if err != nil { + t.Fatalf("failed to create ranged: %v", err) + } + return rng +} + // mustCreatePayloadLoad wraps the newPayloadLoad function for brevity. func mustCreatePayloadLoad(t *testing.T, base payloadBase, offset, len, dreg uint8) *payloadLoad { pdload, err := newPayloadLoad(base, offset, len, dreg)