Implement PayloadLoad operation (parsing, interpretation, evaluation, tests).

Tests payload load evaluation for all basic fields of IP, IPv6, & TCP headers.
These are the main headers that are encountered for gVisor, so their
implementation was prioritized. Loading for other packet headers should work
as expected since raw/general payload loading works, but it has not been
explicitly tested. These tests should be added later and a TODO has been left
noting this.

PiperOrigin-RevId: 668202252
This commit is contained in:
Jayden Nyamiaka
2024-08-27 17:24:28 -07:00
committed by gVisor bot
parent ac417d1200
commit 33dc9383dc
5 changed files with 913 additions and 33 deletions
+3
View File
@@ -13,6 +13,7 @@ go_library(
],
deps = [
"//pkg/abi/linux",
"//pkg/tcpip/header",
"//pkg/tcpip/stack",
],
)
@@ -27,6 +28,8 @@ go_test(
deps = [
"//pkg/abi/linux",
"//pkg/buffer",
"//pkg/tcpip",
"//pkg/tcpip/header",
"//pkg/tcpip/stack",
],
)
+113 -6
View File
@@ -33,6 +33,8 @@
// returns the verdict issued by the ruleset and the packet modified by the
// ruleset (if the verdict is not Drop).
//
// Inner Headers and Tunneling Headers are not supported.
//
// Finally, note that error checking for parameters/inputs is only guaranteed
// for public functions. Most private functions are assumed to have
// valid/prechecked inputs.
@@ -43,6 +45,7 @@ import (
"slices"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
@@ -563,6 +566,7 @@ type operation interface {
var (
_ operation = (*immediate)(nil)
_ operation = (*comparison)(nil)
_ operation = (*payloadLoad)(nil)
)
// immediate is an operation that sets the data in a register.
@@ -654,7 +658,7 @@ func (op comparison) evaluate(regs *registerSet, pkt *stack.PacketBuffer) {
panic("comparison operation data is not BytesData")
}
// Gets the data from the source register.
regBuf := bytesData.getRegisterBuffer(regs, op.sreg)
regBuf := getRegisterBuffer(regs, op.sreg)
// Compares bytes from left to right for all bytes in the comparison data.
dif := 0
@@ -688,6 +692,104 @@ func (op comparison) evaluate(regs *registerSet, pkt *stack.PacketBuffer) {
}
}
// payloadLoad is an operation that loads data from the packet payload into a
// register.
// Note: payload operations are not supported for the verdict register.
type payloadLoad struct {
base payloadBase // Payload base to access data from.
offset uint8 // Number of bytes to skip after the base.
blen uint8 // Number of bytes to load.
dreg uint8 // Number of the destination register.
}
// payloadBase is the header that determines the location of the packet data.
// Note: corresponds to enum nft_payload_bases from
// include/uapi/linux/netfilter/nf_tables.h and uses the same constants.
type payloadBase int
// String for NftPayloadBase returns the string representation of the payload
// base.
func (base payloadBase) String() string {
switch base {
case linux.NFT_PAYLOAD_LL_HEADER:
return "Link Layer Header"
case linux.NFT_PAYLOAD_NETWORK_HEADER:
return "Network Header"
case linux.NFT_PAYLOAD_TRANSPORT_HEADER:
return "Transport Header"
case linux.NFT_PAYLOAD_INNER_HEADER:
panic("inner header not supported")
case linux.NFT_PAYLOAD_TUN_HEADER:
panic("tunneling header not supported")
default:
panic(fmt.Sprintf("invalid payload base: %d", int(base)))
}
}
// validatePayloadBase ensures the payload base is valid.
func validatePayloadBase(base payloadBase) error {
switch base {
case linux.NFT_PAYLOAD_LL_HEADER, linux.NFT_PAYLOAD_NETWORK_HEADER, linux.NFT_PAYLOAD_TRANSPORT_HEADER:
return nil
case linux.NFT_PAYLOAD_INNER_HEADER:
return fmt.Errorf("inner header not supported")
case linux.NFT_PAYLOAD_TUN_HEADER:
return fmt.Errorf("tunneling header not supported")
default:
return fmt.Errorf("invalid payload base: %d", int(base))
}
}
// newPayloadLoad creates a new PayloadLoad operation.
func newPayloadLoad(base payloadBase, offset, blen, dreg uint8) (*payloadLoad, error) {
if dreg == linux.NFT_REG_VERDICT {
return nil, fmt.Errorf("payload load operation cannot use verdict register as destination")
}
if blen > 16 || (blen > 4 && is4ByteRegister(dreg)) {
return nil, fmt.Errorf("payload length %d is too long for destination register %d", blen, dreg)
}
if err := validatePayloadBase(base); err != nil {
return nil, err
}
return &payloadLoad{base: base, offset: offset, blen: blen, dreg: dreg}, nil
}
// evaluate for PayloadLoad loads data from the packet payload into the
// destination register.
func (op payloadLoad) evaluate(regs *registerSet, pkt *stack.PacketBuffer) {
// Gets the data from the packet payload.
var payload []byte = nil
switch op.base {
case linux.NFT_PAYLOAD_LL_HEADER:
// Note: Assumes Mac Header is present and valid for necessary use cases.
// Also, doesn't check VLAN tag because VLAN isn't supported by gVisor.
payload = pkt.LinkHeader().Slice()
case linux.NFT_PAYLOAD_NETWORK_HEADER:
// No checks done in linux kernel.
payload = pkt.NetworkHeader().Slice()
case linux.NFT_PAYLOAD_TRANSPORT_HEADER:
// Note: Assumes L4 protocol is present and valid for necessary use cases.
// Errors if the packet is fragmented for IPv4 only.
if net := pkt.NetworkHeader().Slice(); len(net) > 0 && pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber {
if h := header.IPv4(net); h.More() || h.FragmentOffset() != 0 {
break // packet is fragmented
}
}
payload = pkt.TransportHeader().Slice()
}
// Breaks if could not retrieve packet data.
if payload == nil || len(payload) < int(op.offset+op.blen) {
regs.verdict = Verdict{Code: VC(linux.NFT_BREAK)}
return
}
// Copies payload data into the specified register.
data := newBytesData(payload[op.offset : op.offset+op.blen])
data.storeData(regs, op.dreg)
}
//
// Register and Register-Related Implementations.
// Note: Registers are represented by type uint8 for the register number.
@@ -815,11 +917,12 @@ func (rd bytesData) validateRegister(reg uint8) error {
return nil
}
// getRegisterBuffer is a helper function that gets the appropriate slice of
// register data from the register set.
// getRegisterBuffer is a helper function that gets the appropriate slice of the
// register from the register set. The number of bytes returned is rounded up to
// the nearest 4-byte multiple.
// Note: does not support verdict data and assumes the register is valid for the
// given data type.
func (rd bytesData) getRegisterBuffer(regs *registerSet, reg uint8) []byte {
func getRegisterBuffer(regs *registerSet, reg uint8) []byte {
// Returns the entire 4-byte register
if is4ByteRegister(reg) {
start := (reg - linux.NFT_REG32_00) * linux.NFT_REG32_SIZE
@@ -835,7 +938,7 @@ func (rd bytesData) storeData(regs *registerSet, reg uint8) {
if err := rd.validateRegister(reg); err != nil {
panic(err)
}
copy(rd.getRegisterBuffer(regs, reg), rd.data)
copy(getRegisterBuffer(regs, reg), rd.data)
}
// registerSet represents the set of registers supported by the kernel.
@@ -846,7 +949,7 @@ type registerSet struct {
data [registersByteSize]byte // 4 16-byte registers or 16 4-byte registers
}
// newRegisterSet creates a new RegisterSet with the Continue Verdict and all
// newRegisterSet creates a new registerSet with the Continue Verdict and all
// registers set to 0.
func newRegisterSet() registerSet {
return registerSet{
@@ -860,6 +963,10 @@ func (regs *registerSet) Verdict() Verdict {
return regs.verdict
}
func (regs *registerSet) String() string {
return fmt.Sprintf("verdict: %v, data: %x", regs.verdict, regs.data)
}
//
// Verdict Implementation.
// There are two types of verdicts:
File diff suppressed because it is too large Load Diff
+146 -9
View File
@@ -17,7 +17,6 @@ package nftables
import (
"encoding/hex"
"fmt"
"math"
"regexp"
"slices"
"strconv"
@@ -124,7 +123,7 @@ func InterpretRule(ruleString string) (*Rule, error) {
return r, nil
}
// InterpretOperation creates a new Operation from the given operation string,
// InterpretOperation creates a new operation from the given operation string,
// assumed to be a single line of text surrounded in square brackets.
// Note: the operation string should be generated as output from the official nft
// binary (can be accomplished by using flag --debug=netlink).
@@ -140,6 +139,8 @@ func InterpretOperation(line string, lnIdx int) (operation, error) {
return InterpretImmediate(line, lnIdx)
case "cmp":
return InterpretComparison(line, lnIdx)
case "payload":
return InterpretPayloadLoad(line, lnIdx)
default:
return nil, &SyntaxError{lnIdx, 1, fmt.Sprintf("unrecognized operation type: %s", tokens[1])}
}
@@ -264,6 +265,102 @@ func InterpretComparison(line string, lnIdx int) (operation, error) {
return cmp, nil
}
// InterpretPayloadLoad creates a new PayloadLoad operation from the given
// string.
func InterpretPayloadLoad(line string, lnIdx int) (operation, error) {
tokens := strings.Fields(line)
// Requires exactly 13 tokens:
// "[", "payload", "load", len+"b", "@", payload base, "header", "+", offset, "=>", "reg", register index, "]".
if len(tokens) != 13 {
return nil, &SyntaxError{lnIdx, 0, fmt.Sprintf("incorrect number of tokens for payload load operation, should be exactly 13, got %d", len(tokens))}
}
if err := checkOperationBrackets(tokens, lnIdx); err != nil {
return nil, err
}
tkIdx := 1
// First token should be "payload".
if err := consumeToken("payload", tokens, lnIdx, tkIdx); err != nil {
return nil, err
}
tkIdx++
// Second token should be "load".
if err := consumeToken("load", tokens, lnIdx, tkIdx); err != nil {
return nil, err
}
tkIdx++
// Third token should be the length (in bytes) of the payload followed by 'b'.
len, err := parsePayloadLength(tokens[tkIdx], lnIdx, tkIdx)
if err != nil {
return nil, err
}
tkIdx++
// Fourth token should be "@".
if err := consumeToken("@", tokens, lnIdx, tkIdx); err != nil {
return nil, err
}
tkIdx++
// Fifth token should be the payload base header.
base, err := parsePayloadBase(tokens[tkIdx], lnIdx, tkIdx)
if err != nil {
return nil, err
}
tkIdx++
// Sixth token should be "header".
if err := consumeToken("header", tokens, lnIdx, tkIdx); err != nil {
return nil, err
}
tkIdx++
// Seventh token should be "+".
if err := consumeToken("+", tokens, lnIdx, tkIdx); err != nil {
return nil, err
}
tkIdx++
// Eighth token should be the uint8 representing the offset.
offset, err := parseUint8(tokens[tkIdx], "offset", lnIdx, tkIdx)
if err != nil {
return nil, err
}
tkIdx++
// Ninth token should be "=>".
if err := consumeToken("=>", tokens, lnIdx, tkIdx); err != nil {
return nil, err
}
tkIdx++
// Tenth token should be "reg".
if err := consumeToken("reg", tokens, lnIdx, tkIdx); err != nil {
return nil, err
}
tkIdx++
// Eleventh token should be the uint8 representing the register index.
reg, err := parseRegister(tokens[tkIdx], lnIdx, tkIdx)
if err != nil {
return nil, err
}
tkIdx++
// Create the operation with the specified arguments.
pdload, err := newPayloadLoad(base, offset, len, reg)
if err != nil {
return nil, &LogicError{lnIdx, tkIdx, err}
}
return pdload, nil
}
//
// Interpreter Helper Functions.
//
@@ -280,18 +377,25 @@ func checkOperationBrackets(tokens []string, lnIdx int) error {
return nil
}
// parseUint8 parses the uint8 which should be supposed from the given string.
func parseUint8(regString string, supposed string, lnIdx int, tkIdx int) (uint8, error) {
v64, err := strconv.ParseUint(regString, 10, 8)
if err != nil {
return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("could not parse uint8 %s: '%s'", supposed, regString)}
}
return uint8(v64), nil
}
// parseRegister parses the register index from the given string.
func parseRegister(regString string, lnIdx int, tkIdx int) (uint8, error) {
reg64, err := strconv.ParseUint(regString, 10, 8)
reg, err := parseUint8(regString, "register index", lnIdx, tkIdx)
if err != nil {
return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("could not parse uint8 register index: '%s'", regString)}
return 0, err
}
if reg64 > math.MaxUint8 || !isRegister(uint8(reg64)) {
return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid register index: %d", reg64)}
if !isRegister(reg) {
return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid register index: %d", reg)}
}
return uint8(reg64), nil
return reg, nil
}
// parseRegisterData parses the register data from the given token and returns
@@ -414,6 +518,39 @@ func parseCmpOp(copString string, lnIdx int, tkIdx int) (int, error) {
}
}
// parsePayloadLength parses the payload length from the given string
// expecting a unsigned 8-bit integer followed by 'b'.
func parsePayloadLength(lenString string, lnIdx int, tkIdx int) (uint8, error) {
lastChar := lenString[len(lenString)-1]
if lastChar != 'b' {
return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("expected 'b' at the end of payload length, got '%c'", lastChar)}
}
numStr := lenString[:len(lenString)-1]
len, err := strconv.ParseUint(numStr, 10, 8)
if err != nil {
return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("could not parse uint8 payload length: '%s'", numStr)}
}
if len > 16 {
return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("payload length must be <= 16 bytes, got %d", len)}
}
return uint8(len), nil
}
// parsePayloadBase parses the payload base header from the given string.
func parsePayloadBase(baseString string, lnIdx int, tkIdx int) (payloadBase, error) {
switch baseString {
case "link":
return linux.NFT_PAYLOAD_LL_HEADER, nil
case "network":
return linux.NFT_PAYLOAD_NETWORK_HEADER, nil
case "transport":
return linux.NFT_PAYLOAD_TRANSPORT_HEADER, nil
// Inner and Tunnel Headers cannot be specified in payload load operation.
default:
return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid payload base: '%s'", baseString)}
}
}
// 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 {
+122 -2
View File
@@ -52,7 +52,7 @@ func checkOp(t *testing.T, test interpretOperationTestAction, checkFunc func(str
}
}
// TestInterpretImmediateOps tests the interpretation of immediate operations.
// TestInterpretImmediateOps tests interpretation of immediate operations.
func TestInterpretImmediateOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
{
@@ -162,7 +162,7 @@ func checkImmediateOp(tname string, expected operation, actual operation) error
return nil
}
// TestInterpretComparisonOps tests the interpretation of comparison operations.
// TestInterpretComparisonOps tests interpretation of comparison operations.
func TestInterpretComparisonOps(t *testing.T) {
for _, test := range []interpretOperationTestAction{
{
@@ -375,6 +375,126 @@ func checkComparisonOp(tname string, expected operation, actual operation) error
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 ip6 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.dreg != expectedPdLoad.dreg {
return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedPdLoad.dreg, tname, pdload.dreg)
}
return nil
}
// TestInterpretRule tests the interpretation of basic and general rules as a
// list of operations.
func TestInterpretRule(t *testing.T) {