Implement Range operation (construction, evaluation, tests, NO interpretation).

The linux kernel defines the range operation, but its use has not been observed
in the nft binary debug output; commands use two comparison operations instead.
Thus, no interpretation of range operation via the nft binary debug output, but
it's fully supported. Similar comment has been left under operation definition.

Operation named "ranged"  because "range" is a keyword in golang.
Also includes small change to the comparison operation evaluation; no
change in behavior.

PiperOrigin-RevId: 673932318
This commit is contained in:
Jayden Nyamiaka
2024-09-12 11:35:42 -07:00
committed by gVisor bot
parent 115723cc9f
commit 12fb7f25d2
2 changed files with 333 additions and 11 deletions
+93 -11
View File
@@ -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.
+240
View File
@@ -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)