Files
visor/pkg/tcpip/nftables/nftinterp_test.go
Parth SarthiandgVisor bot 7420a60c60 Nftables: Added updateReferences.
- After `DeepCopy` updates pointers to the correct objects.
- Allows us to use `Chain` ptrs instead of `name` in Jmp/Goto.
- Saves map search on every Jmp/Goto.

PiperOrigin-RevId: 955682253
2026-07-29 10:27:01 -07:00

1145 lines
49 KiB
Go

// Copyright 2024 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package nftables
import (
"fmt"
"slices"
"testing"
"gvisor.dev/gvisor/pkg/abi/linux"
)
type interpretOperationTestAction struct {
tname string
opStr string
expected operation // will be nil if an error is expected
}
// 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("%s", err.Error())
}
}
// TestInterpretImmediateOps tests 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, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
},
{
tname: "verdict register with drop verdict",
opStr: "[ immediate reg 0 drop ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
},
{
tname: "verdict register with continue verdict",
opStr: "[ immediate reg 0 continue ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_CONTINUE)}),
},
{
tname: "verdict register with return verdict",
opStr: "[ immediate reg 0 return ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, 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, nil, Verdict{Code: VC(linux.NFT_JUMP), Chain: &Chain{name: "next_chain"}}),
},
{
tname: "verdict register with goto verdict",
opStr: "[ immediate reg 0 goto -> next_chain ]",
expected: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_GOTO), Chain: &Chain{name: "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 1 0x0201a8c0 ]",
expected: mustCreateImmediate(t, linux.NFT_REG_1, []byte{0xc0, 0xa8, 0x01, 0x02}, Verdict{}),
},
{
tname: "16-byte register with 8-byte data",
opStr: "[ immediate reg 2 0xb80d0120 0x00000050 ]",
expected: mustCreateImmediate(t, linux.NFT_REG_2, []byte{0x20, 0x01, 0x0d, 0xb8, 0x50, 0x00, 0x00, 0x00}, Verdict{}),
},
{
tname: "16-byte register with 12-byte data",
opStr: "[ immediate reg 3 0xb80d0120 0x00000050 0xb80d0120 ]",
expected: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x20, 0x01, 0x0d, 0xb8, 0x50, 0x00, 0x00, 0x00, 0x20, 0x01, 0x0d, 0xb8}, Verdict{}),
},
{
tname: "16-byte register with 16-byte data",
opStr: "[ immediate reg 4 0xb80d0120 0x00000000 0x00000000 0x02000000 ]",
expected: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, Verdict{}),
},
{
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,
},
} {
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 %T", tname, actual)
}
if imm.dregIdx != expectedImm.dregIdx {
return fmt.Errorf("expected register to be %d for %s, got %d", expectedImm.dregIdx, tname, imm.dregIdx)
}
if !slices.Equal(imm.data, expectedImm.data) {
return fmt.Errorf("expected data to be %v for %s, got %v", expectedImm.data, tname, imm.data)
}
return nil
}
// TestInterpretComparisonOps tests 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: "verdict register with 8-byte data comparison",
opStr: "[ cmp lt reg 0 0xb80d0120 0x02000000 ]",
expected: nil,
},
{
tname: "verdict register with 12-byte data comparison",
opStr: "[ cmp gte reg 0 0xb80d0120 0x18305290 0x02000000 ]",
expected: nil,
},
{
tname: "verdict register with 16-byte data comparison",
opStr: "[ cmp neq reg 0 0xb80d0120 0x18305290 0x18305290 0x02000000 ]",
expected: nil,
},
{
tname: "4-byte register == 4-byte data",
opStr: "[ cmp eq reg 8 0x0302010a ]",
expected: mustCreateComparison(t, linux.NFT_REG32_00, linux.NFT_CMP_EQ, []byte{0x0a, 0x01, 0x02, 0x03}),
},
{
tname: "4-byte register != 4-byte data",
opStr: "[ cmp neq reg 9 0x00000064 ]",
expected: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_NEQ, []byte{0x64, 0x00, 0x00, 0x00}),
},
{
tname: "4-byte register < 4-byte data",
opStr: "[ cmp lt reg 10 0x00000000 ]",
expected: mustCreateComparison(t, linux.NFT_REG32_02, linux.NFT_CMP_LT, []byte{0x00, 0x00, 0x00, 0x00}),
},
{
tname: "4-byte register <= 4-byte data",
opStr: "[ cmp lte reg 11 0x00000164 ]",
expected: mustCreateComparison(t, linux.NFT_REG32_03, linux.NFT_CMP_LTE, []byte{0x64, 0x01, 0x00, 0x00}),
},
{
tname: "4-byte register > 4-byte data",
opStr: "[ cmp gt reg 12 0xe8030000 ]",
expected: mustCreateComparison(t, linux.NFT_REG32_04, linux.NFT_CMP_GT, []byte{0x00, 0x00, 0x03, 0xe8}),
},
{
tname: "4-byte register >= 4-byte data",
opStr: "[ cmp gte reg 13 0xc02b0000 ]",
expected: mustCreateComparison(t, linux.NFT_REG32_05, linux.NFT_CMP_GTE, []byte{0x00, 0x00, 0x2b, 0xc0}),
},
{
tname: "4-byte register with 8-byte data comparison",
opStr: "[ cmp eq reg 23 0xb80d0120 0x02000000 ]",
expected: nil,
},
{
tname: "4-byte register with 12-byte data comparison",
opStr: "[ cmp lte reg 23 0xb80d0120 0x18305290 0x02000000 ]",
expected: nil,
},
{
tname: "4-byte register with 16-byte data comparison",
opStr: "[ cmp gt reg 23 0x0302010a 0x00000000 0x00000000 0x02000001 ]",
expected: nil,
},
{
tname: "16-byte register == 4-byte data",
opStr: "[ cmp eq reg 1 0x0302010a ]",
expected: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, []byte{0x0a, 0x01, 0x02, 0x03}),
},
{
tname: "16-byte register != 4-byte data",
opStr: "[ cmp neq reg 2 0x00000064 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_NEQ, []byte{0x64, 0x00, 0x00, 0x00}),
},
{
tname: "16-byte register < 4-byte data",
opStr: "[ cmp lt reg 3 0x00000000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, []byte{0x00, 0x00, 0x00, 0x00}),
},
{
tname: "16-byte register <= 4-byte data",
opStr: "[ cmp lte reg 4 0x00000164 ]",
expected: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, []byte{0x64, 0x01, 0x00, 0x00}),
},
{
tname: "16-byte register > 4-byte data",
opStr: "[ cmp gt reg 1 0xe8030000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_GT, []byte{0x00, 0x00, 0x03, 0xe8}),
},
{
tname: "16-byte register >= 4-byte data",
opStr: "[ cmp gte reg 2 0xc02b0000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GTE, []byte{0x00, 0x00, 0x2b, 0xc0}),
},
{
tname: "16-byte register == 8-byte data",
opStr: "[ cmp eq reg 1 0x0302010a 0x12345678 ]",
expected: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, []byte{0x0a, 0x01, 0x02, 0x03, 0x78, 0x56, 0x34, 0x12}),
},
{
tname: "16-byte register != 8-byte data",
opStr: "[ cmp neq reg 2 0x00000064 0x00000020 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_NEQ, []byte{0x64, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00}),
},
{
tname: "16-byte register < 8-byte data",
opStr: "[ cmp lt reg 3 0x00000000 0x00000000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}),
},
{
tname: "16-byte register <= 8-byte data",
opStr: "[ cmp lte reg 4 0x00000164 0x00000164 ]",
expected: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, []byte{0x64, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00}),
},
{
tname: "16-byte register > 8-byte data",
opStr: "[ cmp gt reg 2 0xe8030000 0x00000f13 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, []byte{0x00, 0x00, 0x03, 0xe8, 0x13, 0x0f, 0x00, 0x00}),
},
{
tname: "16-byte register >= 8-byte data",
opStr: "[ cmp gte reg 3 0x0a000120 0xc0090000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, []byte{0x20, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x09, 0xc0}),
},
{
tname: "16-byte register == 12-byte data",
opStr: "[ cmp eq reg 1 0x0302010a 0x00000000 0x12345678 ]",
expected: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, []byte{0x0a, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x34, 0x12}),
},
{
tname: "16-byte register != 12-byte data",
opStr: "[ cmp neq reg 2 0x00000064 0x00000000 0x00000020 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_NEQ, []byte{0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00}),
},
{
tname: "16-byte register < 12-byte data",
opStr: "[ cmp lt reg 3 0x00000000 0x00000000 0x00000000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}),
},
{
tname: "16-byte register <= 12-byte data",
opStr: "[ cmp lte reg 4 0x00000164 0x00000164 0x00000164 ]",
expected: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, []byte{0x64, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00}),
},
{
tname: "16-byte register > 12-byte data",
opStr: "[ cmp gt reg 2 0xe8030000 0x00000f13 0xc0090000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, []byte{0x00, 0x00, 0x03, 0xe8, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0}),
},
{
tname: "16-byte register >= 12-byte data",
opStr: "[ cmp gte reg 3 0x0a000120 0x00000f13 0xc0090000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, []byte{0x20, 0x01, 0x00, 0x0a, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0}),
},
{
tname: "16-byte register == 16-byte data",
opStr: "[ cmp eq reg 1 0x0302010a 0x00000000 0x00000000 0x02000002 ]",
expected: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, []byte{0x0a, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02}),
},
{
tname: "16-byte register != 16-byte data",
opStr: "[ cmp neq reg 2 0x00000064 0x00000000 0x00000000 0x02000000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_NEQ, []byte{0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}),
},
{
tname: "16-byte register < 16-byte data",
opStr: "[ cmp lt reg 3 0x00000000 0x00000000 0x00000000 0x00000000 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}),
},
{
tname: "16-byte register <= 16-byte data",
opStr: "[ cmp lte reg 4 0x00000164 0x00000164 0x00000164 0x00000164 ]",
expected: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, []byte{0x64, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00}),
},
{
tname: "16-byte register > 16-byte data",
opStr: "[ cmp gt reg 2 0xe8030000 0x00000f13 0xc0090000 0x0b136a87 ]",
expected: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, []byte{0x00, 0x00, 0x03, 0xe8, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0, 0x87, 0x6a, 0x13, 0x0b}),
},
{
tname: "16-byte register >= 16-byte data",
opStr: "[ cmp gte reg 3 0x0a000120 0x00000f13 0xc0090000 0x0b136a87 ]",
expected: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, []byte{0x20, 0x01, 0x00, 0x0a, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0, 0x87, 0x6a, 0x13, 0x0b}),
},
} {
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.sregIdx != expectedCmp.sregIdx {
return fmt.Errorf("expected register to be %d for %s, got %d", expectedCmp.sregIdx, tname, cmp.sregIdx)
}
if cmp.cop != expectedCmp.cop {
return fmt.Errorf("expected comparison operator to be %v for %s, got %v", expectedCmp.cop, tname, cmp.cop)
}
if !slices.Equal(cmp.data, expectedCmp.data) {
return fmt.Errorf("expected data to be %v for %s, got %v", expectedCmp.data, tname, cmp.data)
}
return nil
}
// TestInterpretPayloadLoadOps tests interpretation of payload load operations.
// Most operations are direct output of nft binary commands. All stated commands
// should be preceded by nft --debug=netlink to generate matching operations.
func TestInterpretPayloadLoadOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
{
tname: "load bytes into verdict register",
opStr: "[ payload load 2b @ transport header + 0 => reg 0 ]",
expected: nil,
},
// cmd: add rule ip tab ch tcp flags syn counter accept
{
tname: "load 1 byte into 4-byte register",
opStr: "[ payload load 1b @ transport header + 13 => reg 9 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 13, 1, linux.NFT_REG32_01),
},
{
tname: "load 1 byte into 16-byte register",
opStr: "[ payload load 1b @ transport header + 13 => reg 1 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 13, 1, linux.NFT_REG_1),
},
// cmd: add rule ip tab ch tcp sport 80 counter accept
{
tname: "load 2 bytes into 4-byte register no offset",
opStr: "[ payload load 2b @ transport header + 0 => reg 8 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 0, 2, linux.NFT_REG32_00),
},
{
tname: "load 2 bytes into 16-byte register no offset",
opStr: "[ payload load 2b @ transport header + 0 => reg 1 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 0, 2, linux.NFT_REG_1),
},
// cmd: add rule ip tab ch tcp dport 12345 counter accept
{
tname: "load 2 bytes into 4-byte register with offset",
opStr: "[ payload load 2b @ transport header + 2 => reg 9 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 2, 2, linux.NFT_REG32_01),
},
{
tname: "load 2 bytes into 16-byte register with offset",
opStr: "[ payload load 2b @ transport header + 2 => reg 1 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 2, 2, linux.NFT_REG_1),
},
// cmd: add rule ip tab ch @th,24,24 0xabcdef counter accept
{
tname: "load 3 bytes into 4-byte register",
opStr: "[ payload load 3b @ transport header + 3 => reg 10 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 3, 3, linux.NFT_REG32_02),
},
{
tname: "load 3 bytes into 16-byte register",
opStr: "[ payload load 3b @ transport header + 3 => reg 2 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 3, 3, linux.NFT_REG_2),
},
// cmd: add rule ip tab ch ip daddr 192.168.1.1 counter accept
{
tname: "load 4 bytes into 4-byte register",
opStr: "[ payload load 4b @ network header + 16 => reg 12 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 16, 4, linux.NFT_REG32_04),
},
{
tname: "load 4 bytes into 16-byte register",
opStr: "[ payload load 4b @ network header + 16 => reg 1 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 16, 4, linux.NFT_REG_1),
},
// cmd: add rule ip tab ch ether saddr 01:23:45:67:89:ab counter drop
{
tname: "load 6 bytes into 4-byte register",
opStr: "[ payload load 6b @ link header + 6 => reg 13 ]",
expected: nil,
},
{
tname: "load 6 bytes into 16-byte register",
opStr: "[ payload load 6b @ link header + 6 => reg 3 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_LL_HEADER, 6, 6, linux.NFT_REG_3),
},
// cmd: add rule ip6 tab ch ip6 saddr 2001:db8::2 counter accept
{
tname: "load 16 bytes into 4-byte register",
opStr: "[ payload load 16b @ network header + 8 => reg 10 ]",
expected: nil,
},
{
tname: "load 16 bytes into 16-byte register",
opStr: "[ payload load 16b @ network header + 8 => reg 1 ]",
expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 8, 16, linux.NFT_REG_1),
},
{
tname: "load >16 bytes into 16-byte register",
opStr: "[ payload load 20b @ network header + 16 => reg 1 ]",
expected: nil,
},
} {
t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkPayloadLoadOp) })
}
}
// checkPayloadLoadOp checks that the given operation is a payload load
// operation and that it matches the expected payload load operation.
func checkPayloadLoadOp(tname string, expected operation, actual operation) error {
expectedPdLoad := expected.(*payloadLoad)
pdload, ok := actual.(*payloadLoad)
if !ok {
return fmt.Errorf("expected operation type to be PayloadLoad for %s, got %T", tname, actual)
}
if pdload.base != expectedPdLoad.base {
return fmt.Errorf("expected payload base to be %v for %s, got %v", expectedPdLoad.base, tname, pdload.base)
}
if pdload.offset != expectedPdLoad.offset {
return fmt.Errorf("expected offset to be %d for %s, got %d", expectedPdLoad.offset, tname, pdload.offset)
}
if pdload.blen != expectedPdLoad.blen {
return fmt.Errorf("expected length to be %d for %s, got %d", expectedPdLoad.blen, tname, pdload.blen)
}
if pdload.dregIdx != expectedPdLoad.dregIdx {
return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedPdLoad.dregIdx, tname, pdload.dregIdx)
}
return nil
}
// TestInterpretPayloadSetOps tests interpretation of payload set operations.
// Most operations are direct output of nft binary commands. All stated commands
// should be preceded by nft --debug=netlink to generate matching operations.
func TestInterpretPayloadSetOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
// Simple checksum type tests.
{
tname: "set checksum type, none",
opStr: "[ payload write reg 1 => 6b @ link header + 0 csum_type 0 csum_off 0 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, 0, 6, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
{
tname: "set checksum type, inet",
opStr: "[ payload write reg 1 => 6b @ link header + 0 csum_type 1 csum_off 0 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, 0, 6, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 0, 0x0),
},
{
tname: "set checksum type, sctp", // not supported
opStr: "[ payload write reg 1 => 6b @ link header + 0 csum_type 2 csum_off 0 csum_flags 0x0 ]",
expected: nil,
},
{
tname: "set out of range checksum type",
opStr: "[ payload write reg 1 => 6b @ link header + 0 csum_type 3 csum_off 0 csum_flags 0x0 ]",
expected: nil,
},
// Simple checksum offset tests.
{
tname: "set valid offset",
opStr: "[ payload write reg 1 => 6b @ link header + 0 csum_type 0 csum_off 100 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, 0, 6, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 100, 0x0),
},
{
tname: "set negative checksum offset",
opStr: "[ payload write reg 1 => 6b @ link header + 100 csum_type 1 csum_off -1 csum_flags 0x0 ]",
expected: nil,
},
// Simple checksum flags tests.
{
tname: "set checksum flags, L4 with psuedoheader flag",
opStr: "[ payload write reg 1 => 6b @ link header + 0 csum_type 0 csum_off 0 csum_flags 0x1 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, 0, 6, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{
tname: "set invalid checksum flags",
opStr: "[ payload write reg 1 => 6b @ link header + 0 csum_type 0 csum_off 0 csum_flags 0x2 ]",
expected: nil,
},
// Invalid register tests.
{
tname: "set from verdict register",
opStr: "[ payload write reg 0 => 4b @ link header + 0 csum_type 0 csum_off 0 csum_flags 0x0 ]",
expected: nil,
},
{
tname: "set >4 bytes from 4-byte register",
opStr: "[ payload write reg 9 => 6b @ link header + 0 csum_type 0 csum_off 0 csum_flags 0x0 ]",
expected: nil,
},
{
tname: "set >16 bytes from 16-byte register",
opStr: "[ payload write reg 2 => 20b @ link header + 0 csum_type 0 csum_off 0 csum_flags 0x0 ]",
expected: nil,
},
// Valid tests.
// Note: It doesn't seem like the nft binary ever outputs payload set ops
// that have an odd offset or length and checksumming on. This makes sense
// because the offset and length are specified in bytes, but the checksum is
// calculated in half-words (2-bytes), which means the checksum calculation
// is only valid if the offset and length are even. However, the linux
// kernel does not specifically enforce this, so on linux it's technically
// possible to declare payload set operations that undoubtedly result in
// invalid checksums. Since the nft binary is what generates our input, we
// do not test these edge cases either.
// cmd: add rule ip tab ch @nh,24,8 set 0xab
{
tname: "set 1 byte from 4-byte register with csum NONE and no flags",
opStr: "[ payload write reg 8 => 1b @ network header + 3 csum_type 0 csum_off 0 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 3, 1, linux.NFT_REG32_00, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
{
tname: "set 1 byte from 16-byte register with csum NONE and no flags",
opStr: "[ payload write reg 1 => 1b @ network header + 4 csum_type 0 csum_off 0 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 4, 1, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
// cmd: add rule ip tab ch tcp sport set 80
{
tname: "set 2 bytes from 4-byte register with csum INET and no flags",
opStr: "[ payload write reg 9 => 2b @ transport header + 0 csum_type 1 csum_off 16 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 0, 2, linux.NFT_REG32_01, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{
tname: "set 2 bytes from 16-byte register with csum INET and no flags",
opStr: "[ payload write reg 2 => 2b @ transport header + 0 csum_type 1 csum_off 16 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 0, 2, linux.NFT_REG_2, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
// cmd: add rule ip tab ch @ll,24,24 set 0xabcdef
{
tname: "set 3 bytes from 4-byte register with csum NONE and no flags",
opStr: "[ payload write reg 10 => 3b @ link header + 3 csum_type 0 csum_off 0 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, 3, 3, linux.NFT_REG32_02, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
{
tname: "set 3 bytes from 16-byte register with csum NONE and no flags",
opStr: "[ payload write reg 2 => 3b @ link header + 3 csum_type 0 csum_off 0 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, 3, 3, linux.NFT_REG_2, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
// cmd: add rule ip tab ch ip daddr set 192.168.1.1
{
tname: "set 4 bytes from 4-byte register with csum INET and pseudoheader flag",
opStr: "[ payload write reg 11 => 4b @ network header + 16 csum_type 1 csum_off 10 csum_flags 0x1 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 16, 4, linux.NFT_REG32_03, linux.NFT_PAYLOAD_CSUM_INET, 10, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{
tname: "set 4 bytes from 4-byte register with csum INET and pseudoheader flag",
opStr: "[ payload write reg 3 => 4b @ network header + 16 csum_type 1 csum_off 10 csum_flags 0x1 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 16, 4, linux.NFT_REG_3, linux.NFT_PAYLOAD_CSUM_INET, 10, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
// cmd: add rule ip tab ch ether saddr set 01:23:45:67:89:ab
{
tname: "set 6 bytes from 16-byte register with csum NONE and no flags",
opStr: "[ payload write reg 4 => 6b @ link header + 6 csum_type 0 csum_off 0 csum_flags 0x0 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, 6, 6, linux.NFT_REG_4, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
// cmd: add rule ip6 tab ch ip6 saddr set 2001:db8::2
{
tname: "set 16 bytes from 16-byte register with csum NONE and psuedoheader flag",
opStr: "[ payload write reg 1 => 16b @ network header + 8 csum_type 0 csum_off 0 csum_flags 0x1 ]",
expected: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 8, 16, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
} {
t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkPayloadSetOp) })
}
}
// checkPayloadSetOp checks that the given operation is a payload set
// operation and that it matches the expected payload set operation.
func checkPayloadSetOp(tname string, expected operation, actual operation) error {
expectedPdSet := expected.(*payloadSet)
pdset, ok := actual.(*payloadSet)
if !ok {
return fmt.Errorf("expected operation type to be PayloadLoad for %s, got %T", tname, actual)
}
if pdset.base != expectedPdSet.base {
return fmt.Errorf("expected payload base to be %v for %s, got %v", expectedPdSet.base, tname, pdset.base)
}
if pdset.offset != expectedPdSet.offset {
return fmt.Errorf("expected offset to be %d for %s, got %d", expectedPdSet.offset, tname, pdset.offset)
}
if pdset.blen != expectedPdSet.blen {
return fmt.Errorf("expected length to be %d for %s, got %d", expectedPdSet.blen, tname, pdset.blen)
}
if pdset.sregIdx != expectedPdSet.sregIdx {
return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedPdSet.sregIdx, tname, pdset.sregIdx)
}
if pdset.csumType != expectedPdSet.csumType {
return fmt.Errorf("expected checksum type to be %d for %s, got %d", expectedPdSet.csumType, tname, pdset.csumType)
}
if pdset.csumOffset != expectedPdSet.csumOffset {
return fmt.Errorf("expected checksum offset to be %d for %s, got %d", expectedPdSet.csumOffset, tname, pdset.csumOffset)
}
if pdset.csumFlags != expectedPdSet.csumFlags {
return fmt.Errorf("expected checksum flags to be %b for %s, got %b", expectedPdSet.csumFlags, tname, pdset.csumFlags)
}
return nil
}
// TestInterpretBitwiseOps tests interpretation of bitwise operations.
// Note: Only tests bitwise bool operations for now because interpretation of
// non-boolean operations is not supported from the nft binary debug output.
func TestInterpretBitwiseOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
// Invalid interpretations.
{
tname: "verdict register with bitwise bool",
opStr: "[ bitwise reg 0 = ( reg 1 & 0x000003ff ) ^ 0x0000b000 ]",
expected: nil,
},
{
tname: "4-byte register with > 4-byte data for bitwise bool",
opStr: "[ bitwise reg 1 = ( reg 9 & 0x000003ff 0x09040302 ) ^ 0x0000b000 0x11ff11ff ]",
expected: nil,
},
{
tname: "mismatch mask and xor lengths for bitwise bool",
opStr: "[ bitwise reg 1 = ( reg 1 & 0x000003ff ) ^ 0x0000b000 0x11ff11ff ]",
expected: nil,
},
// cmd: add rule ip filter input ip dscp set 0x2c
{
tname: "same 4-byte register with 4-byte data for bitwise bool",
opStr: "[ bitwise reg 10 = ( reg 10 & 0x000003ff ) ^ 0x0000b000 ]",
expected: mustCreateBitwiseBool(t, linux.NFT_REG32_02, linux.NFT_REG32_02, []byte{0xff, 0x03, 0x00, 0x00}, []byte{0x00, 0xb0, 0x00, 0x00}),
},
{
tname: "dif 4-byte registers with 4-byte data for bitwise bool",
opStr: "[ bitwise reg 8 = ( reg 9 & 0x000003ff ) ^ 0x0000b000 ]",
expected: mustCreateBitwiseBool(t, linux.NFT_REG32_01, linux.NFT_REG32_00, []byte{0xff, 0x03, 0x00, 0x00}, []byte{0x00, 0xb0, 0x00, 0x00}),
},
// cmd: add rule ip filter input ip saddr and 55 or 0xffff0000 == 34
{
tname: "same 16-byte register with 4-byte data for bitwise bool",
opStr: "[ bitwise reg 1 = ( reg 1 & 0x37000000 ) ^ 0x0000ffff ]",
expected: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_1, []byte{0x00, 0x00, 0x00, 0x37}, []byte{0xff, 0xff, 0x00, 0x00}),
},
{
tname: "dif 16-byte registers with 4-byte data for bitwise bool",
opStr: "[ bitwise reg 4 = ( reg 3 & 0x37000000 ) ^ 0x0000ffff ]",
expected: mustCreateBitwiseBool(t, linux.NFT_REG_3, linux.NFT_REG_4, []byte{0x00, 0x00, 0x00, 0x37}, []byte{0xff, 0xff, 0x00, 0x00}),
},
// cmd: add rule ip filter input ip saddr and 0xff0230ff == 5
{
tname: "4- and 16-byte registers with 4-byte data for bitwise bool",
opStr: "[ bitwise reg 4 = ( reg 14 & 0xff3002ff ) ^ 0x00000000 ]",
expected: mustCreateBitwiseBool(t, linux.NFT_REG32_06, linux.NFT_REG_4, []byte{0xff, 0x02, 0x30, 0xff}, []byte{0x00, 0x00, 0x00, 0x00}),
},
{
tname: "16- and 4-byte registers with 4-byte data for bitwise bool",
opStr: "[ bitwise reg 14 = ( reg 1 & 0xff3002ff ) ^ 0x00000000 ]",
expected: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG32_06, []byte{0xff, 0x02, 0x30, 0xff}, []byte{0x00, 0x00, 0x00, 0x00}),
},
// More than 4 bytes of data.
{
tname: "8-byte data for bitwise bool",
opStr: "[ bitwise reg 1 = ( reg 1 & 0x00000000 0x00000000 ) ^ 0x00000164 0x00000164 ]",
expected: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_1, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, []byte{0x64, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00}),
},
{
tname: "12-byte data for bitwise bool",
opStr: "[ bitwise reg 4 = ( reg 2 & 0x0302010a 0x00000000 0x12345678 ) ^ 0x0a000120 0x00000f13 0xc0090000 ]",
expected: mustCreateBitwiseBool(t, linux.NFT_REG_2, linux.NFT_REG_4, []byte{0x0a, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x34, 0x12}, []byte{0x20, 0x01, 0x00, 0x0a, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0}),
},
{
tname: "16-byte data for bitwise bool",
opStr: "[ bitwise reg 1 = ( reg 3 & 0xe8030000 0x00000f13 0xc0090000 0x0b136a87 ) ^ 0x0a000120 0x00000f13 0xc0090000 0x00000000 ]",
expected: mustCreateBitwiseBool(t, linux.NFT_REG_3, linux.NFT_REG_1, []byte{0x00, 0x00, 0x03, 0xe8, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0, 0x87, 0x6a, 0x13, 0x0b}, []byte{0x20, 0x01, 0x00, 0x0a, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x09, 0xc0, 0x00, 0x00, 0x00, 0x00}),
},
} {
t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkBitwiseOp) })
}
}
// checkBitwiseOp checks that the given operation is a bitwise operation and
// that it matches the expected bitwise operation.
func checkBitwiseOp(tname string, expected operation, actual operation) error {
expectedBit := expected.(*bitwise)
bit, ok := actual.(*bitwise)
if !ok {
return fmt.Errorf("expected operation type to be BitwiseBool for %s, got %T", tname, actual)
}
if bit.sregIdx != expectedBit.sregIdx {
return fmt.Errorf("expected source register to be %d for %s, got %d", expectedBit.sregIdx, tname, bit.sregIdx)
}
if bit.dregIdx != expectedBit.dregIdx {
return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedBit.dregIdx, tname, bit.dregIdx)
}
if bit.bop != expectedBit.bop {
return fmt.Errorf("expected bitwise operation to be %d for %s, got %d", expectedBit.bop, tname, bit.bop)
}
if bit.blen != expectedBit.blen {
return fmt.Errorf("expected bitwise length to be %d for %s, got %d", expectedBit.blen, tname, bit.blen)
}
if !slices.Equal(bit.mask, expectedBit.mask) {
return fmt.Errorf("expected bitwise mask to be %v for %s, got %v", expectedBit.mask, tname, bit.mask)
}
if !slices.Equal(bit.xor, expectedBit.xor) {
return fmt.Errorf("expected bitwise xor to be %v for %s, got %v", expectedBit.xor, tname, bit.xor)
}
if bit.shift != expectedBit.shift {
return fmt.Errorf("expected bitwise shift to be %d for %s, got %d", expectedBit.shift, tname, bit.shift)
}
return nil
}
// TestInterpretCounterOps tests interpretation of counter operations.
// Note: test cases are pretty simple because the counter operation is
// essentially always called with 0 initial bytes and packets.
func TestInterpretCounterOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
{ // cmd: add rule ip tab ch counter
tname: "counter with 0 initial bytes and packets",
opStr: "[ counter pkts 0 bytes 0 ]",
expected: newCounter(0, 0),
},
{
tname: "counter with non-zero initial bytes and packets",
opStr: "[ counter pkts 4561 bytes 39 ]",
expected: newCounter(4561, 39),
},
} {
t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkCounterOp) })
}
}
// checkCounterOp checks that the given operation is a counter operation and
// that it matches the expected counter operation.
func checkCounterOp(tname string, expected operation, actual operation) error {
expectedCntr := expected.(*counter)
cntr, ok := actual.(*counter)
if !ok {
return fmt.Errorf("expected operation type to be Counter for %s, got %T", tname, actual)
}
if bytes, expectedBytes := cntr.bytes.Load(), expectedCntr.bytes.Load(); bytes != expectedBytes {
return fmt.Errorf("expected bytes counter to be %d for %s, got %d", expectedBytes, tname, bytes)
}
if pkts, expectedPkts := cntr.packets.Load(), expectedCntr.packets.Load(); pkts != expectedPkts {
return fmt.Errorf("expected packets counter to be %d for %s, got %d", expectedPkts, tname, pkts)
}
return nil
}
// TestInterpretRouteOps tests interpretation of route operations.
func TestInterpretRouteOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
{ // cmd: add rule ip filter output rt nexthop 192.168.1.1
tname: "load nexthop4 key to 4-byte register",
opStr: "[ rt load nexthop4 => reg 14 ]",
expected: mustCreateRoute(t, linux.NFT_RT_NEXTHOP4, linux.NFT_REG32_06),
},
{ // cmd: add rule ip filter output rt nexthop 192.168.1.9
tname: "load nexthop4 key to 4-byte register",
opStr: "[ rt load nexthop4 => reg 3 ]",
expected: mustCreateRoute(t, linux.NFT_RT_NEXTHOP4, linux.NFT_REG_3),
},
{ // cmd: add rule ip6 filter output rt nexthop 2001:db8:85a3::aa
tname: "load nexthop6 key to 16-byte register",
opStr: "[ rt load nexthop6 => reg 1 ]",
expected: mustCreateRoute(t, linux.NFT_RT_NEXTHOP6, linux.NFT_REG_1),
},
{ // cmd: add rule ip filter output rt mtu 1500
tname: "load tcpmss key to 4-byte register",
opStr: "[ rt load tcpmss => reg 8 ]",
expected: mustCreateRoute(t, linux.NFT_RT_TCPMSS, linux.NFT_REG32_00),
},
{ // cmd: add rule ip filter output rt mtu 0x0102
tname: "load tcpmss key to 16-byte register",
opStr: "[ rt load tcpmss => reg 4 ]",
expected: mustCreateRoute(t, linux.NFT_RT_TCPMSS, linux.NFT_REG_4),
},
// Result in errors.
{ // cmd: add rule ip filter output rt classid 0x05
tname: "unsupported route key classid",
opStr: "[ rt load classid => reg 10 ]",
expected: nil,
},
{ // cmd: add rule ip filter output rt ipsec exists
tname: "unsupported route key ipsec",
opStr: "[ rt load ipsec => reg 1 ]",
expected: nil,
},
{
tname: "invalid route key keyword",
opStr: "[ rt load xrfm => reg 1 ]",
expected: nil,
},
{
tname: "too few tokens for route operation",
opStr: "[ rt nexthop6 => reg 1 ]",
expected: nil,
},
{
tname: "too many tokens for route operation",
opStr: "[ rt load tcpmss => reg 4 -> reg 5 ]",
expected: nil,
},
} {
t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkRouteOp) })
}
}
// checkRouteOp checks that the given operation is a route operation and
// that it matches the expected route operation.
func checkRouteOp(tname string, expected operation, actual operation) error {
expectedRt := expected.(*route)
rt, ok := actual.(*route)
if !ok {
return fmt.Errorf("expected operation type to be Route for %s, got %T", tname, actual)
}
if rt.key != expectedRt.key {
return fmt.Errorf("expected route key to be %v for %s, got %v", expectedRt.key, tname, rt.key)
}
if rt.dregIdx != expectedRt.dregIdx {
return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedRt.dregIdx, tname, rt.dregIdx)
}
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.sregIdx != expectedOrder.sregIdx {
return fmt.Errorf("expected source register to be %d for %s, got %d", expectedOrder.sregIdx, tname, order.sregIdx)
}
if order.dregIdx != expectedOrder.dregIdx {
return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedOrder.dregIdx, tname, order.dregIdx)
}
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
}
// TestInterpretMetaLoadOps tests interpretation of meta load operations.
func TestInterpretMetaLoadOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
{ // cmd: add rule ip tab ch meta length 0x01020304
tname: "meta load len test",
opStr: "[ meta load len => reg 2 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_LEN, linux.NFT_REG_2),
},
{ // cmd: add rule inet tab ch meta protocol 0x0102
tname: "meta load protocol test",
opStr: "[ meta load protocol => reg 3 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_PROTOCOL, linux.NFT_REG_3),
},
{ // cmd: add rule inet tab ch meta nfproto 253
tname: "meta load nfproto test",
opStr: "[ meta load nfproto => reg 4 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_NFPROTO, linux.NFT_REG_4),
},
{ // cmd: add rule inet tab ch meta l4proto 0x17
tname: "meta load l4proto test",
opStr: "[ meta load l4proto => reg 8 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_L4PROTO, linux.NFT_REG32_00),
},
{ // cmd: add rule inet tab ch skuid 0x09080706
tname: "meta load skuid test",
opStr: "[ meta load skuid => reg 10 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_SKUID, linux.NFT_REG32_02),
},
{ // cmd: add rule inet tab ch meta skgid 0x09080706
tname: "meta load skgid test",
opStr: "[ meta load skgid => reg 11 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_SKGID, linux.NFT_REG32_03),
},
{ // cmd: add rule inet tab ch rtclassid 0x01020304
tname: "meta load rtclassid test",
opStr: "[ meta load rtclassid => reg 12 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_RTCLASSID, linux.NFT_REG32_04),
},
{ // cmd: add rule inet tab ch pkttype 0x59
tname: "meta load pkttype test",
opStr: "[ meta load pkttype => reg 13 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_PKTTYPE, linux.NFT_REG32_05),
},
{ // cmd: add rule inet tab ch meta random 0x02040608
tname: "meta load prandom test",
opStr: "[ meta load prandom => reg 9 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_PRANDOM, linux.NFT_REG32_01),
},
{ // cmd: add rule inet tab ch time "2020-06-06 17:00"
tname: "meta load arbitrary time test",
opStr: "[ meta load time => reg 4 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_NS, linux.NFT_REG_4),
},
{ // cmd: add rule inet tab ch time "1970-01-01 00:00:01"
tname: "meta load time 1 sec after unix epoch test",
opStr: "[ meta load time => reg 3 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_NS, linux.NFT_REG_3),
},
{ // cmd: add rule inet tab ch time "1969-01-01 00:00:00"
tname: "meta load time 1 year before unix epoch test",
opStr: "[ meta load time => reg 2 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_NS, linux.NFT_REG_2),
},
{ // cmd: add rule inet tab ch day Monday
tname: "meta load day test",
opStr: "[ meta load day => reg 23 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_DAY, linux.NFT_REG32_15),
},
{ // cmd: add rule inet tab ch hour 0x01020304
tname: "meta load hour test",
opStr: "[ meta load hour => reg 22 ]",
expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_HOUR, linux.NFT_REG32_14),
},
} {
t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkMetaLoadOp) })
}
}
// checkMetaLoadOp checks that the given operation is a meta load operation and
// that it matches the expected meta load operation.
func checkMetaLoadOp(tname string, expected operation, actual operation) error {
expectedMtLoad := expected.(*metaLoad)
mtLoad, ok := actual.(*metaLoad)
if !ok {
return fmt.Errorf("expected operation type to be MetaLoad for %s, got %T", tname, actual)
}
if mtLoad.key != expectedMtLoad.key {
return fmt.Errorf("expected meta key to be %v for %s, got %v", expectedMtLoad.key, tname, mtLoad.key)
}
if mtLoad.dregIdx != expectedMtLoad.dregIdx {
return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedMtLoad.dregIdx, tname, mtLoad.dregIdx)
}
return nil
}
// TestInterpretMetaSetOps tests interpretation of meta set operations.
func TestInterpretMetaSetOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
// cmd: nft --debug=netlink add rule ip tab ch meta pkttype set 34
{
tname: "meta set pkttype 4-byte reg test",
opStr: "[ meta set pkttype with reg 14 ]",
expected: mustCreateMetaSet(t, linux.NFT_META_PKTTYPE, linux.NFT_REG32_06),
},
{
tname: "meta set pkttype 16-byte reg test",
opStr: "[ meta set pkttype with reg 3 ]",
expected: mustCreateMetaSet(t, linux.NFT_META_PKTTYPE, linux.NFT_REG_3),
},
} {
t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkMetaSetOp) })
}
}
// checkMetaSetOp checks that the given operation is a meta set operation and
// that it matches the expected meta set operation.
func checkMetaSetOp(tname string, expected operation, actual operation) error {
expectedMtSet := expected.(*metaSet)
mtSet, ok := actual.(*metaSet)
if !ok {
return fmt.Errorf("expected operation type to be MetaLoad for %s, got %T", tname, actual)
}
if mtSet.key != expectedMtSet.key {
return fmt.Errorf("expected meta key to be %v for %s, got %v", expectedMtSet.key, tname, mtSet.key)
}
if mtSet.sregIdx != expectedMtSet.sregIdx {
return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedMtSet.sregIdx, tname, mtSet.sregIdx)
}
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
expected *Rule // will be nil if an error is expected
}{
{
tname: "empty ruleset",
ruleStr: ``,
expected: &Rule{},
},
{
tname: "empty ruleset with excess whitespace",
ruleStr: `
`,
expected: &Rule{},
},
} {
t.Run(test.tname, func(t *testing.T) {
rule, err := InterpretRule(test.ruleStr)
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.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.expected.ops[i]
switch testOp.(type) {
case *immediate:
if err := checkImmediateOp(test.tname, testOp, op); err != nil {
t.Fatalf("%s", err.Error())
}
case *comparison:
if err := checkComparisonOp(test.tname, testOp, op); err != nil {
t.Fatalf("%s", err.Error())
}
// TODO(b/345684870): cases will be added here as more types are supported.
default:
t.Fatalf("unexpected operation type for %s: %T", test.tname, testOp)
}
}
})
}
}