Files
Parth SarthiandgVisor bot 0724b3e842 Nftables: Support xt_NAT compatibility targets.
Added:
- NAT(SNAT/DNAT) targets to perform network address and port translation.
- XTNATTargetV0, XTNATTargetV1, and XTNATTargetV2 ABI support.
- Fixed NFNATRange2(nf_nat_range2) struct and it's size.
- Unit and syscall tests.

Ref: net/netfilter/xt_nat.c
PiperOrigin-RevId: 966381482
2026-08-17 22:44:58 -07:00

6332 lines
239 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 (
"bytes"
"encoding/binary"
"fmt"
"math"
"reflect"
"slices"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/marshal/primitive"
"gvisor.dev/gvisor/pkg/rand"
"gvisor.dev/gvisor/pkg/sentry/socket/netlink/nlmsg"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/syserr"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/faketime"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
// Table Constants.
const (
arbitraryTargetChain string = "target_chain"
arbitraryHook stack.NFHook = stack.NFPrerouting
arbitraryFamily stack.AddressFamily = stack.Inet
arbitraryReservedHeaderBytes int = 16
)
var (
arbitraryPriority Priority = func() Priority {
priority, err := NewStandardPriority("filter", arbitraryFamily, arbitraryHook)
if err != nil {
panic(fmt.Sprintf("unexpected error for NewStandardPriority: %v", err))
}
return priority
}()
arbitraryInfoPolicyAccept *BaseChainInfo = &BaseChainInfo{
BcType: BaseChainTypeFilter,
Hook: arbitraryHook,
Priority: arbitraryPriority,
}
)
// Packet Constants.
const (
tcpTransportProtocol = header.TCPProtocolNumber
arbitraryHeaderID = 3
arbitraryTimeToLive = 64
arbitraryPort = 12345
arbitraryPort2 = 80
tcpSeqNum = 32
tcpAckNum = 165
tcpWinSize = 65535
tcpUrgentPointer = 0
arbitraryNonZeroFragmentOffset = 16
// TODO(b/345684870): Use constants defined in the pkg/tcpip/header package.
// Ethernet Offsets and Lengths.
ethDstAddrOffset = 0
ethDstAddrLen = 6
ethSrcAddrOffset = 6
ethSrcAddrLen = 6
ethTypeOffset = 12
ethTypeLen = 2
// IPv4 Offsets and Lengths.
ipv4LengthOffset = 2
ipv4LengthLen = 2
ipv4IDOffset = 4
ipv4IDLen = 2
ipv4FragOffOffset = 6
ipv4FragOffLen = 2
ipv4TTLOffset = 8
ipv4TTLLen = 1
ipv4ProtocolOffset = 9
ipv4ProtocolLen = 1
ipv4ChecksumOffset = 10
ipv4ChecksumLen = 2
ipv4SrcAddrOffset = 12
ipv4SrcAddrLen = 4
ipv4DstAddrOffset = 16
ipv4DstAddrLen = 4
// IPv6 Offsets and Lengths.
ipv6LengthOffset = 4
ipv6LengthLen = 2
ipv6NextHdrOffset = 6
ipv6NextHdrLen = 1
ipv6HopLimitOffset = 7
ipv6HopLimitLen = 1
ipv6SrcAddrOffset = 8
ipv6SrcAddrLen = 16
ipv6DstAddrOffset = 24
ipv6DstAddrLen = 16
// TCP Offsets and Lengths.
tcpSrcPortOffset = 0
tcpSrcPortLen = 2
tcpDstPortOffset = 2
tcpDstPortLen = 2
tcpSeqNumOffset = 4
tcpSeqNumLen = 4
tcpAckNumOffset = 8
tcpAckNumLen = 4
tcpWindowOffset = 14
tcpWindowLen = 2
tcpChecksumOffset = 16
tcpChecksumLen = 2
tcpUrgPtrOffset = 18
tcpUrgPtrLen = 2
// Arbitrary Socket IDs
arbitrarySKUID = 0x020304
arbitrarySKGID = 45668
// Arbitrary Packet Type
arbitraryPktType = tcpip.PacketOutgoing
)
var (
arbitraryLinkAddr = tcpip.LinkAddress("\x02\x02\x03\x04\x05\x06")
arbitraryLinkAddr2 = tcpip.LinkAddress("\x02\x02\x03\x04\x05\x07")
arbitraryLinkAddrB = [6]byte{0x02, 0x02, 0x03, 0x04, 0x05, 0x06}
arbitraryLinkAddrB2 = [6]byte{0x02, 0x02, 0x03, 0x04, 0x05, 0x07}
arbitraryEthernetType = header.IPv4ProtocolNumber
arbitraryIPv4AddrB = [4]byte{192, 168, 1, 1}
arbitraryIPv4AddrB2 = [4]byte{192, 168, 1, 9}
ipv4MinTotalLength = header.IPv4MinimumSize
arbitraryIPv6AddrB = [16]byte{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa}
arbitraryIPv6AddrB2 = [16]byte{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbb}
ipv6MinPayloadLength = 0
// Note: these are functions to make sure they are not modified by tests and
// so each tests gets a new value.
arbitraryEthernetFields = func() *header.EthernetFields {
return &header.EthernetFields{
SrcAddr: arbitraryLinkAddr,
DstAddr: arbitraryLinkAddr2,
Type: arbitraryEthernetType,
}
}
arbitraryIPv4Fields = func() *header.IPv4Fields {
return &header.IPv4Fields{
TOS: 0,
TotalLength: uint16(ipv4MinTotalLength),
ID: arbitraryHeaderID,
FragmentOffset: 0,
TTL: arbitraryTimeToLive,
Protocol: uint8(tcpTransportProtocol),
Checksum: 0,
SrcAddr: tcpip.AddrFrom4(arbitraryIPv4AddrB),
DstAddr: tcpip.AddrFrom4(arbitraryIPv4AddrB2),
Options: nil,
}
}
fragmentedIPv4Fields = func() *header.IPv4Fields {
fields := arbitraryIPv4Fields()
fields.FragmentOffset = arbitraryNonZeroFragmentOffset
return fields
}
arbitraryIPv6Fields = func() *header.IPv6Fields {
return &header.IPv6Fields{
TrafficClass: 0,
FlowLabel: 0,
PayloadLength: uint16(ipv6MinPayloadLength),
TransportProtocol: tcpTransportProtocol,
HopLimit: arbitraryTimeToLive,
SrcAddr: tcpip.AddrFrom16(arbitraryIPv6AddrB),
DstAddr: tcpip.AddrFrom16(arbitraryIPv6AddrB2),
}
}
arbitraryTCPFields = func() *header.TCPFields {
return &header.TCPFields{
SrcPort: uint16(arbitraryPort),
DstPort: uint16(arbitraryPort2),
SeqNum: uint32(tcpSeqNum),
AckNum: uint32(tcpAckNum),
DataOffset: header.TCPMinimumSize,
WindowSize: uint16(tcpWinSize),
Checksum: 0,
UrgentPointer: uint16(tcpUrgentPointer),
}
}
)
// makeArbitraryPacket creates an arbitrary packet for testing.
func makeArbitraryPacket(reserved int) *stack.PacketBuffer {
return stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: reserved,
Payload: buffer.MakeWithData([]byte{0, 2, 4, 8, 16, 32, 64, 128}),
})
}
// makeEthernetPacket creates a packet with an Ethernet header.
func makeEthernetPacket(reserved int, ethFields *header.EthernetFields) *stack.PacketBuffer {
eth := make([]byte, header.EthernetMinimumSize)
header.Ethernet(eth).Encode(ethFields)
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: reserved,
Payload: buffer.MakeWithData(eth),
})
pkt.LinkHeader().Consume(header.EthernetMinimumSize)
return pkt
}
// makeIPv4Packet creates a packet with an IPv4 header.
func makeIPv4Packet(reserved int, ipv4Fields *header.IPv4Fields) *stack.PacketBuffer {
// Creates a new PacketBuffer with enough space for the IPv4 header.
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: reserved,
})
// Prepends the IPv4 header to the packet buffer.
ipv4Hdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
// Sets the packet type.
pkt.PktType = arbitraryPktType
// Initializes the IPv4 header with fields.
ipv4Hdr.Encode(ipv4Fields)
// Calculates and sets the checksum.
ipv4Hdr.SetChecksum(^ipv4Hdr.CalculateChecksum())
// Sets the network protocol number.
pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber
return pkt
}
func makeArbitraryIPv4Packet() *stack.PacketBuffer {
pkt := makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields())
return pkt
}
// makeIPv6Packet creates a packet with an IPv6 header.
func makeIPv6Packet(reserved int, ipv6Fields *header.IPv6Fields) *stack.PacketBuffer {
// Creates a new PacketBuffer with enough space for the IPv4 header.
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: reserved,
})
// Prepends the IPv6 header to the packet buffer.
ipv6Hdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize))
// Sets the packet type.
pkt.PktType = arbitraryPktType
// Initializes the IPv6 header with fields.
ipv6Hdr.Encode(ipv6Fields)
// No checksum for IPv6 (relies on L4 checksum if extra security is needed).
// Sets the network protocol number.
pkt.NetworkProtocolNumber = header.IPv6ProtocolNumber
return pkt
}
func makeArbitraryIPv6Packet() *stack.PacketBuffer {
pkt := makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields())
return pkt
}
// addTCPHeader adds a TCP header to a packet and returns the header.
// Note: this does not compute the checksum.
func addTCPHeader(pkt *stack.PacketBuffer, tcpFields *header.TCPFields) header.TCP {
// Prepends the TCP header to the packet buffer.
tcpHdr := header.TCP(pkt.TransportHeader().Push(int(tcpFields.DataOffset)))
// Initializes the TCP header with fields.
tcpHdr.Encode(tcpFields)
// Sets the transport protocol number.
pkt.TransportProtocolNumber = header.TCPProtocolNumber
return tcpHdr
}
// makeIPv4TCPPacket creates a packet with an IPv4 and TCP header.
func makeIPv4TCPPacket(reserved int, ipv4Fields *header.IPv4Fields, tcpFields *header.TCPFields) *stack.PacketBuffer {
// Makes a packet with the L3 IPv4 header (this sets the checksum).
pkt := makeIPv4Packet(reserved, ipv4Fields)
// Adds the L4 TCP header.
tcpHdr := addTCPHeader(pkt, tcpFields)
// Calculates the TCP checksum using the pseudo-header and sets it in the TCP header.
tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(
tcpip.TransportProtocolNumber(ipv4Fields.Protocol),
ipv4Fields.SrcAddr,
ipv4Fields.DstAddr,
uint16(tcpFields.DataOffset),
)))
return pkt
}
// makeIPv6TCPPacket creates a packet with an IPv6 and TCP header.
func makeIPv6TCPPacket(reserved int, ipv6Fields *header.IPv6Fields, tcpFields *header.TCPFields) *stack.PacketBuffer {
// Makes a packet with the L3 IPv6 header (this sets the checksum).
pkt := makeIPv6Packet(reserved, ipv6Fields)
// Adds the L4 TCP header.
tcpHdr := addTCPHeader(pkt, tcpFields)
// Calculates the TCP checksum using the pseudo-header and sets it in the TCP header.
tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(
// Next header is supposed to be in pseudo-header calculation for IPv6
// transport protocol checksum, not the transport protocol number according
// to RFC 2460 (https://www.rfc-editor.org/rfc/rfc2460.html#section-8.1).
header.IPv6(pkt.NetworkHeader().Slice()).TransportProtocol(),
ipv6Fields.SrcAddr,
ipv6Fields.DstAddr,
uint16(ipv6Fields.PayloadLength),
)))
return pkt
}
func getAddrFamilyOrDefault(pkt *stack.PacketBuffer, family stack.AddressFamily) stack.AddressFamily {
if len(pkt.NetworkHeader().Slice()) > 0 {
switch pkt.NetworkProtocolNumber {
case header.IPv4ProtocolNumber:
return stack.IP
case header.IPv6ProtocolNumber:
return stack.IP6
}
return family
}
if len(pkt.LinkHeader().Slice()) >= header.EthernetMinimumSize {
ethHdr := header.Ethernet(pkt.LinkHeader().Slice())
switch ethHdr.Type() {
case header.IPv4ProtocolNumber:
return stack.IP
case header.IPv6ProtocolNumber:
return stack.IP6
}
}
return family
}
// TestUnsupportedAddressFamily tests that an empty NFTables object returns an
// error when evaluating a packet for an unsupported address family.
func TestUnsupportedAddressFamily(t *testing.T) {
// Makes arbitrary packet for comparison (to check for no changes).
cmpPkt := makeArbitraryPacket(arbitraryReservedHeaderBytes)
nf := newNFTablesStd()
for _, unsupportedFamily := range []stack.AddressFamily{stack.AddressFamily(stack.NumAFs), stack.AddressFamily(-1)} {
// Note: the Prerouting hook is arbitrary (any hook would work).
pkt := makeArbitraryPacket(arbitraryReservedHeaderBytes)
v, err := nf.EvaluateHook(unsupportedFamily, arbitraryHook, pkt, nil /* route */)
if err == nil {
t.Fatalf("expecting error for EvaluateHook with unsupported address family %d; got %v verdict, %s packet, and error %v",
int(unsupportedFamily),
v, packetResultString(cmpPkt, pkt), err)
}
}
}
// TestAcceptAll tests that an empty NFTables object accepts all packets for
// supported hooks and errors for unsupported hooks for all address families
// when evaluating packets at the hook-level.
func TestAcceptAllForSupportedHooks(t *testing.T) {
// Makes arbitrary packet for comparison (to check for no changes).
for _, family := range []stack.AddressFamily{stack.IP, stack.IP6, stack.Inet, stack.Arp, stack.Bridge, stack.Netdev} {
t.Run(family.String()+" address family", func(t *testing.T) {
nf := newNFTablesStd()
for _, hook := range []stack.NFHook{stack.NFPrerouting, stack.NFInput, stack.NFForward, stack.NFOutput, stack.NFPostrouting, stack.NFIngress, stack.NFEgress} {
var pkt *stack.PacketBuffer
pktFamily := family
switch family {
case stack.IP, stack.Inet:
// Inet address family is not supported for hook evaluation.
pkt = makeArbitraryIPv4Packet()
pktFamily = stack.IP
case stack.IP6:
pkt = makeArbitraryIPv6Packet()
default:
pkt = makeArbitraryPacket(arbitraryReservedHeaderBytes)
}
cmpPkt := pkt.Clone()
v, err := nf.EvaluateHook(pktFamily, hook, pkt, nil /* route */)
supported := supportedHooks[family][hook]
if supported {
if err != nil || v.Code != VC(linux.NF_ACCEPT) {
t.Fatalf("expecting accept verdict for EvaluateHook with supported hook %v for family %v; got %v verdict, %s packet, and error %v",
hook, family,
v, packetResultString(cmpPkt, pkt), err)
}
} else {
if err == nil {
t.Fatalf("expecting error for EvaluateHook with unsupported hook %v for family %v; got %v verdict, %s packet, and error %v",
hook, family,
v, packetResultString(cmpPkt, pkt), err)
}
}
}
})
}
}
// TestEvaluateImmediateVerdict tests that the Immediate operation correctly sets the
// register value and behaves as expected during evaluation.
func TestEvaluateImmediateVerdict(t *testing.T) {
for _, test := range []struct {
tname string
baseOp1 operation // will be nil if unused
baseOp2 operation // will be nil if unused
targetOp operation // will be nil if unused
verdict Verdict
}{
{
tname: "no operations",
verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy
},
{
tname: "immediately accept",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)},
},
{
tname: "immediately drop",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
{
tname: "immediately continue with base chain policy accept",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_CONTINUE)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy
},
{
tname: "immediately return with base chain policy accept",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_RETURN)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy
},
{
tname: "immediately jump to target chain that accepts",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_JUMP)}),
targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)},
},
{
tname: "immediately jump to target chain that drops",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_JUMP)}),
targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
{
tname: "immediately jump to target chain that continues with second rule that accepts",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_JUMP)}),
targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_CONTINUE)}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)},
},
{
tname: "immediately jump to target chain that continues with second rule that drops",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_JUMP)}),
targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_CONTINUE)}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
{
tname: "immediately goto to target chain that accepts",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_GOTO)}),
targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)},
},
{
tname: "immediately goto to target chain that drops",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_GOTO)}),
targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
{
tname: "immediately goto to target chain that continues with second rule that accepts",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_GOTO)}),
targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_CONTINUE)}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy
},
{
tname: "immediately goto to target chain that continues with second rule that drops",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_GOTO)}),
targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_CONTINUE)}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy
},
{
tname: "add data to register then accept",
baseOp1: mustCreateImmediate(t, linux.NFT_REG32_13, []byte{0, 1, 2, 3}, Verdict{}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)},
},
{
tname: "add data to register then drop",
baseOp1: mustCreateImmediate(t, linux.NFT_REG32_15, []byte{0, 1, 2, 3}, Verdict{}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
{
tname: "add data to register then continue",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0, 1, 2, 3}, Verdict{}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_CONTINUE)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy
},
{
tname: "multiple accepts",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)},
},
{
tname: "multiple drops",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
{
tname: "immediately accept then drop",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
verdict: Verdict{Code: VC(linux.NF_ACCEPT)},
},
{
tname: "immediately drop then accept",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
{
tname: "immediate load register",
baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)}),
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a base chain (for 2 rules) and another
// target chain (for 1 rule).
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept /* bcInfo */, "test chain", false /* errorOnDuplicate */, 0 /* chainFlags */, nil /* udata */, linux.NF_ACCEPT /* policy */)
if err != nil {
t.Fatalf("unexpected error for AddChainToTable: %v", err)
}
tc, err := nf.AddChainToTable(tab, arbitraryTargetChain, nil /* bcInfo */, "test chain", false /* errorOnDuplicate */, 0 /* chainFlags */, nil /* udata */, 0 /* policy */)
if err != nil {
t.Fatalf("unexpected error for AddChainToTable: %v", err)
}
// Adds testing rules and operations.
if test.baseOp1 != nil {
setVerdictChainIfJumpGoto(test.baseOp1, tc)
rule1 := &Rule{}
rule1.addOperation(test.baseOp1)
if err := bc.RegisterRule(rule1, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule for the first operation: %v", err)
}
}
if test.baseOp2 != nil {
setVerdictChainIfJumpGoto(test.baseOp2, tc)
rule2 := &Rule{}
rule2.addOperation(test.baseOp2)
if err := bc.RegisterRule(rule2, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule for the second operation: %v", err)
}
}
if test.targetOp != nil {
ruleTarget := &Rule{}
ruleTarget.addOperation(test.targetOp)
if err := tc.RegisterRule(ruleTarget, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule for the target operation: %v", err)
}
}
// Runs evaluation and checks verdict.
pkt := makeArbitraryIPv4Packet()
v, err := nf.EvaluateHook(stack.IP, arbitraryHook, pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
if v.Code != test.verdict.Code {
t.Fatalf("expected verdict %v, got %v", test.verdict, v)
}
})
}
}
// TestEvaluateImmediateVerdict tests that the Immediate operation correctly
// loads bytes data of all lengths into all supported registers.
func TestEvaluateImmediateBytesData(t *testing.T) {
bytes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}
for blen := 1; blen <= len(bytes); blen++ {
for _, registerSize := range []int{linux.NFT_REG32_SIZE, linux.NFT_REG_SIZE} {
if blen > registerSize {
continue
}
tname := fmt.Sprintf("immediately load %d bytes into %d-byte registers", blen, registerSize)
t.Run(tname, func(t *testing.T) {
// Sets up an NFTables object with a base chain with policy accept.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept /* bcInfo */, "test chain", false /* errorOnDuplicate */, 0 /* chainFlags */, nil /* udata */, linux.NF_ACCEPT /* policy */)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
// Adds a rule and immediate operation per register of registerSize.
switch registerSize {
case linux.NFT_REG32_SIZE:
for reg := linux.NFT_REG32_00; reg <= linux.NFT_REG32_15; reg++ {
rule := &Rule{}
rule.addOperation(mustCreateImmediate(t, uint8(reg), bytes[:blen], Verdict{}))
if err := bc.RegisterRule(rule, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule for rule %d: %v", reg-linux.NFT_REG32_00, err)
}
}
case linux.NFT_REG_SIZE:
for reg := linux.NFT_REG_1; reg <= linux.NFT_REG_4; reg++ {
rule := &Rule{}
rule.addOperation(mustCreateImmediate(t, uint8(reg), bytes[:blen], Verdict{}))
if err := bc.RegisterRule(rule, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule for rule %d: %v", reg-linux.NFT_REG_1, err)
}
}
}
// Runs evaluation and checks for default policy verdict accept
pkt := makeArbitraryIPv4Packet()
v, err := nf.EvaluateHook(stack.IP, arbitraryHook, pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
if v.Code != linux.NF_ACCEPT {
t.Fatalf("expected default policy verdict accept, got %v", v)
}
})
}
}
}
// TestEvaluateComparison tests that the Comparison operation correctly compares
// the data in the source register to the given data.
// Note: Relies on expected behavior of the Immediate operation.
func TestEvaluateComparison(t *testing.T) {
for _, test := range []struct {
tname string
op1 operation // will be nil if unused
op2 operation // will be nil if unused
res bool // should be true if we reach end of the rule (no breaks)
}{
// 4-byte data comparisons, alternates between 4-byte and 16-byte registers.
{
tname: "compare register == 4-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, []byte{0, 0, 0, 0}),
res: true,
},
{
tname: "compare register == 4-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG32_11, []byte{1, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_11, linux.NFT_CMP_EQ, []byte{0, 0, 0, 0}),
res: false,
},
{
tname: "compare register != 4-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG32_03, []byte{1, 7, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_03, linux.NFT_CMP_NEQ, []byte{1, 98, 0, 56}),
res: true,
},
{
tname: "compare register != 4-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{1, 98, 0, 56}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_NEQ, []byte{1, 98, 0, 56}),
res: false,
},
{
tname: "compare register < 4-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{29, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LT, []byte{100, 0, 0, 0}),
res: true,
},
{
tname: "compare register < 4-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG32_04, []byte{100, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_04, linux.NFT_CMP_LT, []byte{100, 0, 0, 0}),
res: false,
},
{
tname: "compare register < 4-byte data, false gt",
op1: mustCreateImmediate(t, linux.NFT_REG32_14, []byte{200, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_14, linux.NFT_CMP_LT, []byte{100, 0, 0, 0}),
res: false,
},
{
tname: "compare register > 4-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG32_15, []byte{29, 76, 230, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_15, linux.NFT_CMP_GT, []byte{0, 0, 0, 1}),
res: true,
},
{
tname: "compare register > 4-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG32_07, []byte{29, 76, 230, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_07, linux.NFT_CMP_GT, []byte{29, 76, 230, 0}),
res: false,
},
{
tname: "compare register > 4-byte data, false lt",
op1: mustCreateImmediate(t, linux.NFT_REG32_05, []byte{28, 76, 230, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_05, linux.NFT_CMP_GT, []byte{29, 76, 230, 0}),
res: false,
},
{
tname: "compare register <= 4-byte data, true lt",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{29, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LTE, []byte{100, 0, 0, 0}),
res: true,
},
{
tname: "compare register <= 4-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG32_09, []byte{100, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_09, linux.NFT_CMP_LTE, []byte{100, 0, 0, 0}),
res: true,
},
{
tname: "compare register <= 4-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG32_06, []byte{200, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_LTE, []byte{100, 0, 0, 0}),
res: false,
},
{
tname: "compare register >= 4-byte data, true gt",
op1: mustCreateImmediate(t, linux.NFT_REG32_12, []byte{29, 76, 230, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG32_12, linux.NFT_CMP_GTE, []byte{0, 0, 0, 1}),
res: true,
},
{
tname: "compare register >= 4-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{29, 76, 230, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_GTE, []byte{29, 76, 230, 0}),
res: true,
},
{
tname: "compare register >= 4-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{28, 76, 230, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, []byte{29, 76, 230, 0}),
res: false,
},
// 8-byte data comparisons.
{
tname: "compare register == 8-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{0, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, []byte{0, 0, 0, 0, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register == 8-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{1, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, []byte{0, 0, 0, 0, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare register != 8-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{1, 7, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_NEQ, []byte{1, 98, 0, 56, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register != 8-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{1, 98, 0, 56, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_NEQ, []byte{1, 98, 0, 56, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare register < 8-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{29, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LT, []byte{100, 0, 0, 0, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register < 8-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{100, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LT, []byte{100, 0, 0, 0, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare register < 8-byte data, false gt",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{200, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, []byte{100, 0, 0, 0, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare register > 8-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{29, 76, 230, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GT, []byte{0, 0, 0, 1, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register > 8-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{29, 76, 230, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_GT, []byte{29, 76, 230, 0, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare register > 8-byte data, false lt",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{28, 76, 230, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, []byte{29, 76, 230, 0, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare register <= 8-byte data, true lt",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{29, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LTE, []byte{100, 0, 0, 0, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register <= 8-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{100, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, []byte{100, 0, 0, 0, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register <= 8-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{200, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LTE, []byte{100, 0, 0, 0, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare register >= 8-byte data, true gt",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{30, 0, 0, 1, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GTE, []byte{29, 76, 230, 0, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register >= 8-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{29, 76, 230, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, []byte{29, 76, 230, 0, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register >= 8-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{28, 76, 230, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, []byte{29, 76, 230, 0, 0, 0, 0, 0}),
res: false,
},
// 12-byte data comparisons.
{
tname: "compare register == 12-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register == 12-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare register != 12-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_NEQ, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}),
res: true,
},
{
tname: "compare register != 12-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_NEQ, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}),
res: false,
},
{
tname: "compare register < 12-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{0x0a, 0x00, 0x01, 0x1f, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: true,
},
{
tname: "compare register < 12-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: false,
},
{
tname: "compare register < 12-byte data, false gt",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x0a, 0x00, 0x01, 0x21, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: false,
},
{
tname: "compare register > 12-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0x0a, 0x00, 0x01, 0x21, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: true,
},
{
tname: "compare register > 12-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_GT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: false,
},
{
tname: "compare register > 12-byte data, false lt",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{0x0a, 0x00, 0x01, 0x1f, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: false,
},
{
tname: "compare register <= 12-byte data, true lt",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: true,
},
{
tname: "compare register <= 12-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_LTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: true,
},
{
tname: "compare register <= 12-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{0xaa, 0xaa, 0xaa, 0x20, 0xaa, 0xaa, 0xaa, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: false,
},
{
tname: "compare register >= 12-byte data, true gt",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{0xaa, 0xaa, 0xaa, 0x20, 0xaa, 0xaa, 0xaa, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: true,
},
{
tname: "compare register >= 12-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0xab, 0xbc, 0xcd, 0xde, 0xef, 0x00, 0x01, 0x12, 0x23, 0x34, 0x45, 0x56}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, []byte{0xab, 0xbc, 0xcd, 0xde, 0xef, 0x00, 0x01, 0x12, 0x23, 0x34, 0x45, 0x56}),
res: true,
},
{
tname: "compare register >= 12-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0x0a, 0x00, 0x01, 0x19, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00}),
res: false,
},
// 16-byte data comparisons.
{
tname: "compare register == 16-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare register == 16-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}),
res: false,
},
{
tname: "compare register != 16-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_NEQ, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}),
res: true,
},
{
tname: "compare register != 16-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_NEQ, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}),
res: false,
},
{
tname: "compare register < 16-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{0x0a, 0x00, 0x01, 0x1f, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0xaa}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: true,
},
{
tname: "compare register < 16-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: false,
},
{
tname: "compare register < 16-byte data, false gt",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x0a, 0x00, 0x01, 0x21, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0xaa}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: false,
},
{
tname: "compare register > 16-byte data, true",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0x0a, 0x00, 0x01, 0x21, 0xaa, 0xaa, 0xaa, 0xaa, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GT, []byte{0x0a, 0x00, 0x01, 0x20, 0xcc, 0xcc, 0xcc, 0xcc, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: true,
},
{
tname: "compare register > 16-byte data, false eq",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_GT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: false,
},
{
tname: "compare register > 16-byte data, false lt",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{0x0a, 0x00, 0x01, 0x1f, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x90}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_GT, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: false,
},
{
tname: "compare register <= 16-byte data, true lt",
op1: mustCreateImmediate(t, linux.NFT_REG_1, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x86}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: true,
},
{
tname: "compare register <= 16-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_2, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_LTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: true,
},
{
tname: "compare register <= 16-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0xaa, 0x00, 0x0b, 0x13, 0x6a, 0x88}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_LTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: false,
},
{
tname: "compare register >= 16-byte data, true gt",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0xaa, 0xaa, 0xaa, 0x20, 0xaa, 0xaa, 0xaa, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: true,
},
{
tname: "compare register >= 16-byte data, true eq",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0xab, 0xbc, 0xcd, 0xde, 0xef, 0x00, 0x01, 0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x90}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_GTE, []byte{0xab, 0xbc, 0xcd, 0xde, 0xef, 0x00, 0x01, 0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x90}),
res: true,
},
{
tname: "compare register >= 16-byte data, false",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0a, 0x13, 0x6a, 0x85}, Verdict{}),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, []byte{0x0a, 0x00, 0x01, 0x20, 0x00, 0x00, 0x0f, 0x13, 0xc0, 0x09, 0x00, 0x00, 0x0b, 0x13, 0x6a, 0x87}),
res: false,
},
// Empty register comparisons.
{
tname: "compare empty 4-byte register, true",
op1: mustCreateComparison(t, linux.NFT_REG32_10, linux.NFT_CMP_EQ, []byte{0, 0, 0, 0}),
res: true,
},
{
tname: "compare empty 4-byte register, false",
op1: mustCreateComparison(t, linux.NFT_REG32_11, linux.NFT_CMP_EQ, []byte{1, 0, 0, 0}),
res: false,
},
{
tname: "compare empty 8-byte register, true",
op1: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_NEQ, []byte{1, 1, 1, 1, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare empty 8-byte register, false",
op1: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GT, []byte{1, 1, 1, 1, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare empty 12-byte register, true",
op1: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LTE, []byte{1, 1, 1, 1, 0, 0, 0, 0, 8, 9, 10, 11}),
res: true,
},
{
tname: "compare empty 12-byte register, false",
op1: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_NEQ, []byte{0, 0, 0, 0, 0, 0, 0, 0}),
res: false,
},
{
tname: "compare empty 16-byte register, true",
op1: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_LT, []byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
res: true,
},
{
tname: "compare empty 16-byte register, false",
op1: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_GTE, []byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
res: false,
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a single table, chain, and rule.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept, "test chain", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
rule := &Rule{}
// Adds testing operations.
if test.op1 != nil {
rule.addOperation(test.op1)
}
if test.op2 != nil {
rule.addOperation(test.op2)
}
// Add an operation that drops. This is what the final verdict should be
// if all the comparisons are true (res = true).
rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, 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 := makeArbitraryIPv4Packet()
v, err := nf.EvaluateHook(stack.IP, arbitraryHook, pkt, nil /*route*/)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
// If all comparisons are true, the packet will get to the end of the rule
// and the last operation above will set the final verdict to oppose the
// base chain policy. If any comparison is false, the comparison operation
// will break from the rule and the final verdict will default to the base
// chain policy.
if test.res {
if v.Code != VC(linux.NF_DROP) {
t.Fatalf("expected verdict Drop for %t result, got %v", test.res, v)
}
} else {
if v.Code != VC(linux.NF_ACCEPT) {
t.Fatalf("expected base chain policy verdict Accept for %t result, got %v", test.res, v)
}
}
})
}
}
// 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, numToBE(1, 4), Verdict{}),
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, numToBE(4, 4), Verdict{}),
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, numToBE(1, 4), Verdict{}),
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, numToBE(1, 4), Verdict{}),
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, numToBE(954, 4), Verdict{}),
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, numToBE(954, 4), Verdict{}),
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, numToBE(1, 4), Verdict{}),
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, numToBE(1, 4), Verdict{}),
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, numToBE(100, 4), Verdict{}),
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, numToBE(100, 4), Verdict{}),
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, numToBE(123, 4), Verdict{}),
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, numToBE(123, 4), Verdict{}),
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, numToBE(1, 8), Verdict{}),
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, numToBE(4, 8), Verdict{}),
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, numToBE(1, 8), Verdict{}),
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, numToBE(1, 8), Verdict{}),
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, numToBE(954, 8), Verdict{}),
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, numToBE(954, 8), Verdict{}),
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, numToBE(1, 8), Verdict{}),
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, numToBE(1, 8), Verdict{}),
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, numToBE(100, 8), Verdict{}),
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, numToBE(100, 8), Verdict{}),
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, numToBE(123, 8), Verdict{}),
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, numToBE(123, 8), Verdict{}),
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, []byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
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, []byte{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
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, []byte{0x45, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
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, []byte{0x45, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Verdict{}),
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 := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "test", arbitraryInfoPolicyAccept, "" /* comment */, true /* errorOnDuplicate */, 0 /* chainFlags */, nil /* udata */, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
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, nil, 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 := makeArbitraryIPv4Packet()
v, err := nf.EvaluateHook(stack.IP, arbitraryHook, pkt, nil /* route */)
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.
// All commands should be preceded by nft --debug=netlink.
// Note: Relies on expected behavior of the Comparison operation.
// TODO(b/339691111): Add tests for VLAN, ARP, ICMP, ICMPv6, IGMP, UDP headers.
func TestEvaluatePayloadLoad(t *testing.T) {
// Sets testing packets.
ethernetPacket := makeEthernetPacket(0, arbitraryEthernetFields())
ipv4Packet := makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields())
ipv6Packet := makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields())
tcpPacket := makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields())
for _, test := range []struct {
tname string
pkt *stack.PacketBuffer
op1 operation // Payload Load operation to test.
op2 operation // Comparison operation to check resulting data in register,
// nil if expecting a break during evaluation.
}{
// Ethernet header expression commands.
{ // cmd: add rule ip tab ch ether saddr 02:02:03:04:05:06
tname: "load ethernet header source address",
pkt: ethernetPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_LL_HEADER, ethSrcAddrOffset, ethSrcAddrLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, arbitraryLinkAddrB[:]),
},
{ // cmd: add rule ip tab ch ether daddr 02:02:03:04:05:07
tname: "load ethernet header destination address",
pkt: ethernetPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_LL_HEADER, ethDstAddrOffset, ethDstAddrLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, arbitraryLinkAddrB2[:]),
},
{ // cmd: add rule ip tab ch ether type ip
tname: "load ethernet header type",
pkt: ethernetPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_LL_HEADER, ethTypeOffset, ethTypeLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(int(arbitraryEthernetType), ethTypeLen)),
},
// IPv4 header expression commands.
{ // cmd: add rule ip tab ch ip length 20
tname: "load ipv4 header length",
pkt: ipv4Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4LengthOffset, ipv4LengthLen, linux.NFT_REG32_01),
op2: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, numToBE(header.IPv4MinimumSize, ipv4LengthLen)),
},
{ // cmd: add rule ip tab ch ip id 3
tname: "load ipv4 header ip id",
pkt: ipv4Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4IDOffset, ipv4IDLen, linux.NFT_REG32_01),
op2: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, numToBE(arbitraryHeaderID, ipv4IDLen)),
},
{ // cmd: add rule ip tab ch ip frag-off 0
tname: "load ipv4 header fragment offset",
pkt: ipv4Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4FragOffOffset, ipv4FragOffLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(0, ipv4FragOffLen)),
},
// Though the packet is fragmented, there should be no issue because we are
// changing data within the network header.
{ // cmd: add rule ip tab ch ip frag-off 2 (16 bytes)
tname: "load ipv4 header fragment offset non zero for fragmented packet",
pkt: makeIPv4Packet(header.IPv4MinimumSize, fragmentedIPv4Fields()),
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 6, 2, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(arbitraryNonZeroFragmentOffset/8, ipv4FragOffLen)),
// we divide by 8 because the fragment offset is in units of 8 bytes,
// which is encoded into the packet in IPv4.Encode().
},
{ // cmd: add rule ip tab ch ip ttl 64
tname: "load ipv4 header time to live",
pkt: ipv4Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4TTLOffset, ipv4TTLLen, linux.NFT_REG32_01),
op2: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, numToBE(arbitraryTimeToLive, ipv4TTLLen)),
},
{ // cmd: add rule ip tab ch ip protocol tcp
tname: "load ipv4 header protocol",
pkt: ipv4Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4ProtocolOffset, ipv4ProtocolLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(int(tcpTransportProtocol), ipv4ProtocolLen)),
},
{ // cmd: add rule ip tab ch ip saddr 192.168.1.1
tname: "load ipv4 header source address",
pkt: ipv4Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4SrcAddrOffset, ipv4SrcAddrLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, arbitraryIPv4AddrB[:]),
},
{ // cmd: add rule ip tab ch ip daddr 192.168.1.9
tname: "load ipv4 header destination address",
pkt: ipv4Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4DstAddrOffset, ipv4DstAddrLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, arbitraryIPv4AddrB2[:]),
},
{ // cmd: add rule ip tab ch ip checksum __
tname: "load ipv4 header checksum",
pkt: ipv4Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4ChecksumOffset, ipv4ChecksumLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(int(header.IPv4(ipv4Packet.NetworkHeader().Slice()).Checksum()), ipv4ChecksumLen)),
},
// IPv6 header expression commands.
{ // cmd: add rule ip6 tab ch ip6 length 0
tname: "load ipv6 header length",
pkt: ipv6Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6LengthOffset, ipv6LengthLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(0, ipv6LengthLen)),
},
{ // cmd: add rule ip6 tab ch ip6 nexthdr tcp
tname: "load ipv6 header next header",
pkt: ipv6Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6NextHdrOffset, ipv6NextHdrLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(int(tcpTransportProtocol), ipv6NextHdrLen)),
},
{ // cmd: add rule ip6 tab ch ip6 hoplimit 64
tname: "load ipv6 header hop limit",
pkt: ipv6Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6HopLimitOffset, ipv6HopLimitLen, linux.NFT_REG32_01),
op2: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, numToBE(arbitraryTimeToLive, ipv6HopLimitLen)),
},
{ // cmd: add rule ip6 tab ch ip6 saddr 2001:db8:85a3::aa
tname: "load ipv6 header source address",
pkt: ipv6Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6SrcAddrOffset, ipv6SrcAddrLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, arbitraryIPv6AddrB[:]),
},
{ // cmd: add rule ip6 tab ch ip6 daddr 2001:db8:85a3::bb
tname: "load ipv6 header destination address",
pkt: ipv6Packet,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6DstAddrOffset, ipv6DstAddrLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, arbitraryIPv6AddrB2[:]),
},
// TCP header expression commands.
// Since we are changing data within the transport header with a fragmented
// IPv4 packet, this can be problematic, so the evaluation should break.
{
tname: "load for transport header with a fragmented ipv4 packet",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, fragmentedIPv4Fields(), arbitraryTCPFields()),
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 0, 2, linux.NFT_REG_1),
op2: nil,
},
{ // cmd: add rule ip tab ch tcp sport 12345
tname: "load tcp header source port",
pkt: tcpPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpSrcPortOffset, tcpSrcPortLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(arbitraryPort, tcpSrcPortLen)),
},
{ // cmd: add rule ip tab ch tcp dport 80
tname: "load tcp header destination port",
pkt: tcpPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpDstPortOffset, tcpDstPortLen, linux.NFT_REG32_01),
op2: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, numToBE(arbitraryPort2, tcpDstPortLen)),
},
{
// cmd: add rule ip tab ch tcp sequence 32
tname: "load tcp header sequence number",
pkt: tcpPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpSeqNumOffset, tcpSeqNumLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(tcpSeqNum, tcpSeqNumLen)),
},
{ // cmd: add rule ip tab ch tcp ackseq 165
tname: "load tcp header acknowledgement sequence number",
pkt: tcpPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpAckNumOffset, tcpAckNumLen, linux.NFT_REG32_01),
op2: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, numToBE(tcpAckNum, tcpAckNumLen)),
},
{ // cmd: add rule ip tab ch tcp window 65535
tname: "load tcp header window",
pkt: tcpPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpWindowOffset, tcpWindowLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(tcpWinSize, tcpWindowLen)),
},
{ // cmd: add rule ip tab ch tcp checksum __
tname: "load tcp header checksum",
pkt: tcpPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpChecksumOffset, tcpChecksumLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(int(header.TCP(tcpPacket.TransportHeader().Slice()).Checksum()), tcpChecksumLen)),
},
{ // cmd: add rule ip tab ch tcp urgptr 0
tname: "load tcp header urgent pointer",
pkt: tcpPacket,
op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpUrgPtrOffset, tcpUrgPtrLen, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE(tcpUrgentPointer, tcpUrgPtrLen)),
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a single table, chain, and rule.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept, "" /* comment */, false /* errorOnDuplicate */, 0 /* chainFlags */, nil /* udata */, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
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 all comparisons are true.
rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, 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.
v, err := nf.EvaluateHook(getAddrFamilyOrDefault(test.pkt, arbitraryFamily), arbitraryHook, test.pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
// Checks for final verdict.
if test.op2 == nil {
// If no comparison operation is set, then payload load should break,
// resulting in Accept as the default policy verdict.
if v.Code != VC(linux.NF_ACCEPT) {
t.Fatalf("expected verdict Accept for break during evaluation, got %v", v)
}
} else {
// If a comparison operation is set, both payload load and comparison
// should succeed, resulting in Drop as the final verdict.
if v.Code != VC(linux.NF_DROP) {
t.Fatalf("expected verdict Drop for true comparison, got %v", v)
}
}
})
}
}
// TestEvaluatePayloadSet tests that the Payload Set operation correctly sets
// the payload from the source register and updates the packet checksums.
// The nft binary commands used to generate these are stated above each test.
// All commands should be preceded by nft --debug=netlink.
// TODO(b/339691111): Add tests for VLAN, ARP, ICMP, ICMPv6, IGMP, UDP headers.
func TestEvaluatePayloadSet(t *testing.T) {
for _, test := range []struct {
tname string
pkt *stack.PacketBuffer
outPkt *stack.PacketBuffer // nil if expecting a break during evaluation.
op1 operation // Immediate operation to load source register.
op2 operation // Payload Set operation to test.
}{
// Ethernet header statement commands.
{ // cmd: add rule ip tab ch ether saddr set 02:02:03:04:05:07
tname: "set ethernet header source address",
pkt: makeEthernetPacket(0, arbitraryEthernetFields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryEthernetFields()
fields.SrcAddr = arbitraryLinkAddr2
fields.Type = header.IPv4ProtocolNumber
return makeEthernetPacket(0, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, arbitraryLinkAddrB2[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, ethSrcAddrOffset, ethSrcAddrLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
{ // cmd: add rule ip tab ch ether daddr set 02:02:03:04:05:06
tname: "set ethernet header destination address",
pkt: makeEthernetPacket(0, arbitraryEthernetFields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryEthernetFields()
fields.DstAddr = arbitraryLinkAddr
return makeEthernetPacket(0, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_2, arbitraryLinkAddrB[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, ethDstAddrOffset, ethDstAddrLen, linux.NFT_REG_2, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
{ // cmd: add rule ip tab ch ether type set ip6
tname: "set ethernet header type",
pkt: makeEthernetPacket(0, arbitraryEthernetFields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryEthernetFields()
fields.Type = header.IPv6ProtocolNumber
return makeEthernetPacket(0, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(int(header.IPv6ProtocolNumber), ethTypeLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_LL_HEADER, ethTypeOffset, ethTypeLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
// IPv4 header statement commands.
{ // cmd: add rule ip tab ch ip length set 30
tname: "set ipv4 header length",
pkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv4Fields()
fields.TotalLength = uint16(30)
return makeIPv4Packet(header.IPv4MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(30, ipv4LengthLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4LengthOffset, ipv4LengthLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 10, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{ // cmd: add rule ip tab ch ip id set 12345
tname: "set ipv4 header ip id",
pkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv4Fields()
fields.ID = uint16(12345)
return makeIPv4Packet(header.IPv4MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(12345, ipv4IDLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4IDOffset, ipv4IDLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 10, 0x0),
},
// Note: Fragment offsets are divided by 8 because they are in units of 8
// bytes, which is encoded into the packet in IPv4.Encode().
{ // cmd: add rule ip tab ch ip frag-off set 2 (16 bytes)
tname: "set ipv4 header fragment offset, set fragment on",
pkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
outPkt: makeIPv4Packet(header.IPv4MinimumSize, fragmentedIPv4Fields()),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(arbitraryNonZeroFragmentOffset/8, ipv4FragOffLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4FragOffOffset, ipv4FragOffLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 10, 0x0),
},
{ // cmd: add rule ip tab ch ip frag-off set 0
tname: "set ipv4 header fragment offset, set fragment off for fragmented packet",
pkt: makeIPv4Packet(header.IPv4MinimumSize, fragmentedIPv4Fields()),
outPkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0, ipv4FragOffLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4FragOffOffset, ipv4FragOffLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 10, 0x0),
},
{ // cmd: add rule ip tab ch ip frag-off set 10 (80 bytes)
tname: "set ipv4 header fragment offset, change fragment offset for fragmented packet",
pkt: makeIPv4Packet(header.IPv4MinimumSize, fragmentedIPv4Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv4Fields()
fields.FragmentOffset = uint16(10 * 8)
return makeIPv4Packet(header.IPv4MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(10, ipv4FragOffLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4FragOffOffset, ipv4FragOffLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 10, 0x0),
},
{ // cmd: add rule ip tab ch ip ttl set 128
tname: "set ipv4 time to live",
pkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv4Fields()
fields.TTL = uint8(128)
return makeIPv4Packet(header.IPv4MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG32_01, numToBE(128, ipv4TTLLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4TTLOffset, ipv4TTLLen, linux.NFT_REG32_01, linux.NFT_PAYLOAD_CSUM_INET, 10, 0x0),
},
{ // cmd: add rule ip tab ch ip saddr set 192.168.1.9
tname: "set ipv4 header source address",
pkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv4Fields()
fields.SrcAddr = tcpip.AddrFrom4(arbitraryIPv4AddrB2)
return makeIPv4Packet(header.IPv4MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, arbitraryIPv4AddrB2[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4SrcAddrOffset, ipv4SrcAddrLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 10, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{ // cmd: add rule ip tab ch ip daddr set 192.168.1.1
tname: "set ipv4 header destination address",
pkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv4Fields()
fields.DstAddr = tcpip.AddrFrom4(arbitraryIPv4AddrB)
return makeIPv4Packet(header.IPv4MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_4, arbitraryIPv4AddrB[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4DstAddrOffset, ipv4DstAddrLen, linux.NFT_REG_4, linux.NFT_PAYLOAD_CSUM_INET, 10, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{ // cmd: add rule ip tab ch ip checksum set 6060
tname: "set ipv4 header checksum",
pkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
outPkt: func() *stack.PacketBuffer {
pkt := makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields())
pkt.Network().SetChecksum(6060)
return pkt
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(6060, ipv4ChecksumLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4ChecksumOffset, ipv4ChecksumLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 10, 0x0),
},
// IPv6 header statement commands.
{ // cmd: add rule ip6 tab ch ip6 length set 232
tname: "set ipv6 header length",
pkt: makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv6Fields()
fields.PayloadLength = uint16(232)
return makeIPv6Packet(header.IPv6MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(232, ipv6LengthLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6LengthOffset, ipv6LengthLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{ // cmd: add rule ip6 tab ch ip6 hoplimit set 54
tname: "set ipv6 header hop limit",
pkt: makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv6Fields()
fields.HopLimit = uint8(54)
return makeIPv6Packet(header.IPv6MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(54, ipv6HopLimitLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6HopLimitOffset, ipv6HopLimitLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
{ // cmd: add rule ip6 tab ch ip6 saddr set 2001:db8:85a3::bb
tname: "set ipv6 header source address",
pkt: makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv6Fields()
fields.SrcAddr = tcpip.AddrFrom16(arbitraryIPv6AddrB2)
return makeIPv6Packet(header.IPv6MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, arbitraryIPv6AddrB2[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6SrcAddrOffset, ipv6SrcAddrLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{ // cmd: add rule ip6 tab ch ip6 daddr set 2001:db8:85a3::aa
tname: "set ipv6 header destination address",
pkt: makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv6Fields()
fields.DstAddr = tcpip.AddrFrom16(arbitraryIPv6AddrB)
return makeIPv6Packet(header.IPv6MinimumSize, fields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_3, arbitraryIPv6AddrB[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6DstAddrOffset, ipv6DstAddrLen, linux.NFT_REG_3, linux.NFT_PAYLOAD_CSUM_NONE, 0, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
// TCP with IPv4 header statement commands.
// TCP set commands.
{
// Since we change data within the transport header with a fragmented
// IPv4 packet, this can be problematic, so the evaluation should break.
tname: "set for transport header with a fragmented ipv4 packet",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, fragmentedIPv4Fields(), arbitraryTCPFields()),
outPkt: nil,
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(arbitraryPort, tcpSrcPortLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpSrcPortOffset, tcpSrcPortLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp sport set 80
tname: "set tcp header with ipv4 source port",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.SrcPort = arbitraryPort2
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(arbitraryPort2, tcpSrcPortLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpSrcPortOffset, tcpSrcPortLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp dport set 12345
tname: "set tcp header with ipv4 destination port",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.DstPort = arbitraryPort
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(arbitraryPort, tcpDstPortLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpDstPortOffset, tcpDstPortLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp sequence set 33
tname: "set tcp header with ipv4 sequence number",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.SeqNum = uint32(33)
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(33, tcpSeqNumLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpSeqNumOffset, tcpSeqNumLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp ackseq set 245
tname: "set tcp header with ipv4 acknowledgement sequence number",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.AckNum = uint32(245)
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(245, tcpAckNumLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpAckNumOffset, tcpAckNumLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp window set 91
tname: "set tcp header with ipv4 window",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.WindowSize = 91
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(91, tcpWindowLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpWindowOffset, tcpWindowLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp checksum set 7654
tname: "set tcp header with ipv4 checksum",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
pkt := makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields())
tcpHdr := header.TCP(pkt.TransportHeader().Slice())
tcpHdr.SetChecksum(7654)
return pkt
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(7654, tcpChecksumLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpChecksumOffset, tcpChecksumLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp urgptr set 40
tname: "set tcp header with ipv4 urgent pointer",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.UrgentPointer = 40
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(40, tcpUrgPtrLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpUrgPtrOffset, tcpUrgPtrLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
// IPv4 set commands.
{ // cmd: add rule ip tab ch ip id set 12345
tname: "set ipv4 header with tcp ip id",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
ipFields := arbitraryIPv4Fields()
ipFields.ID = uint16(12345)
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, ipFields, arbitraryTCPFields())
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(12345, ipv4IDLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4IDOffset, ipv4IDLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 10, 0x0),
},
{ // cmd: add rule ip tab ch ip ttl set 128
tname: "set ipv4 time to live",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
ipFields := arbitraryIPv4Fields()
ipFields.TTL = uint8(128)
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, ipFields, arbitraryTCPFields())
}(),
op1: mustCreateImmediate(t, linux.NFT_REG32_01, numToBE(128, ipv4TTLLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4TTLOffset, ipv4TTLLen, linux.NFT_REG32_01, linux.NFT_PAYLOAD_CSUM_INET, 10, 0x0),
},
{ // cmd: add rule ip tab ch ip saddr set 192.168.1.9
tname: "set ipv4 header with tcp source address",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
ipFields := arbitraryIPv4Fields()
ipFields.SrcAddr = tcpip.AddrFrom4(arbitraryIPv4AddrB2)
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, ipFields, arbitraryTCPFields())
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, arbitraryIPv4AddrB2[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4SrcAddrOffset, ipv4SrcAddrLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 10, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{ // cmd: add rule ip tab ch ip daddr set 192.168.1.1
tname: "set ipv4 header with tcp destination address",
pkt: makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, arbitraryIPv4Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
ipFields := arbitraryIPv4Fields()
ipFields.DstAddr = tcpip.AddrFrom4(arbitraryIPv4AddrB)
return makeIPv4TCPPacket(header.IPv4MinimumSize+header.TCPMinimumSize, ipFields, arbitraryTCPFields())
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_4, arbitraryIPv4AddrB[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4DstAddrOffset, ipv4DstAddrLen, linux.NFT_REG_4, linux.NFT_PAYLOAD_CSUM_INET, 10, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
// TCP on IPv6 header statement commands.
// TCP set commands.
{ // cmd: add rule ip tab ch tcp sport set 80
tname: "set tcp header with ipv6 source port",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.SrcPort = arbitraryPort2
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(arbitraryPort2, tcpSrcPortLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpSrcPortOffset, tcpSrcPortLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp dport set 12345
tname: "set tcp header with ipv6 destination port",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.DstPort = arbitraryPort
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(arbitraryPort, tcpDstPortLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpDstPortOffset, tcpDstPortLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp sequence set 33
tname: "set tcp header with ipv6 sequence number",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.SeqNum = uint32(33)
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(33, tcpSeqNumLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpSeqNumOffset, tcpSeqNumLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp ackseq set 245
tname: "set tcp header with ipv6 acknowledgement sequence number",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.AckNum = uint32(245)
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(245, tcpAckNumLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpAckNumOffset, tcpAckNumLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp window set 91
tname: "set tcp header with ipv6 window",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.WindowSize = uint16(91)
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(91, tcpWindowLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpWindowOffset, tcpWindowLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp checksum set 7654
tname: "set tcp header with ipv6 checksum",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
pkt := makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields())
tcpHdr := header.TCP(pkt.TransportHeader().Slice())
tcpHdr.SetChecksum(7654)
return pkt
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(7654, tcpChecksumLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpChecksumOffset, tcpChecksumLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
{ // cmd: add rule ip tab ch tcp urgptr set 40
tname: "set tcp header with ipv6 urgent pointer",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
tcpFields := arbitraryTCPFields()
tcpFields.UrgentPointer = uint16(40)
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), tcpFields)
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(40, tcpUrgPtrLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpUrgPtrOffset, tcpUrgPtrLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_INET, 16, 0x0),
},
// IPv6 set commands.
{ // cmd: add rule ip6 tab ch ip6 length set 232
tname: "set ipv6 header length",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv6Fields()
fields.PayloadLength = uint16(232)
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, fields, arbitraryTCPFields())
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(232, ipv6LengthLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6LengthOffset, ipv6LengthLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{ // cmd: add rule ip6 tab ch ip6 hoplimit set 54
tname: "set ipv6 header hop limit",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv6Fields()
fields.HopLimit = uint8(54)
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, fields, arbitraryTCPFields())
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(54, ipv6HopLimitLen), Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6HopLimitOffset, ipv6HopLimitLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, 0x0),
},
{ // cmd: add rule ip6 tab ch ip6 saddr set 2001:db8:85a3::bb
tname: "set ipv6 header source address",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv6Fields()
fields.SrcAddr = tcpip.AddrFrom16(arbitraryIPv6AddrB2)
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, fields, arbitraryTCPFields())
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_1, arbitraryIPv6AddrB2[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6SrcAddrOffset, ipv6SrcAddrLen, linux.NFT_REG_1, linux.NFT_PAYLOAD_CSUM_NONE, 0, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
{ // cmd: add rule ip6 tab ch ip6 daddr set 2001:db8:85a3::aa
tname: "set ipv6 header destination address",
pkt: makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, arbitraryIPv6Fields(), arbitraryTCPFields()),
outPkt: func() *stack.PacketBuffer {
fields := arbitraryIPv6Fields()
fields.DstAddr = tcpip.AddrFrom16(arbitraryIPv6AddrB)
return makeIPv6TCPPacket(header.IPv6MinimumSize+header.TCPMinimumSize, fields, arbitraryTCPFields())
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_3, arbitraryIPv6AddrB[:], Verdict{}),
op2: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv6DstAddrOffset, ipv6DstAddrLen, linux.NFT_REG_3, linux.NFT_PAYLOAD_CSUM_NONE, 0, linux.NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a single table, chain, and rule.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept /* bcInfo */, "test chain", false, 0 /* chainFlags */, nil /* udata */, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
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 payload set evaluation is
// successful (operation breaks if anything goes wrong).
rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, 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.
v, err := nf.EvaluateHook(getAddrFamilyOrDefault(test.pkt, arbitraryFamily), arbitraryHook, test.pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
// Checks for final verdict.
if test.outPkt == nil {
// If no output packet is expected, then evaluation should break,
// resulting in Accept as the default policy verdict.
if v.Code != VC(linux.NF_ACCEPT) {
t.Fatalf("expected verdict Accept for break during evaluation, got %v", v)
}
return
} else {
// If an output packet is expected, the evaluation should go until end
// of rule (no errors/breaks), resulting in Drop as the final verdict.
if v.Code != VC(linux.NF_DROP) {
t.Fatalf("expected verdict Drop for successful evaluation, got %v", v)
}
}
// Checks if the packet are equal.
checkPacketEquality(t, test.outPkt, test.pkt)
})
}
}
// TestEvaluateBitwise tests that the Bitwise operation correctly performs the
// appropriate bitwise operation on the source register data and stores the
// result in the destination register.
// Note: Relies on expected behavior of the Immediate and Comparison operation.
func TestEvaluateBitwise(t *testing.T) {
for _, test := range []struct {
tname string
op1 operation // Immediate operation to set source register.
op2 operation // Bitwise operation to test.
op3 operation // Comparison operation to validate result.
}{
// Bitwise bool operations.
// cmd: add rule ip filter input ip saddr and _ or _ == 105
{
tname: "same 4-byte register with 4-byte data for bitwise bool",
op1: mustCreateImmediate(t, linux.NFT_REG32_01, numToBE(4783, 4), Verdict{}),
op2: mustCreateBitwiseBool(t, linux.NFT_REG32_01, linux.NFT_REG32_01, numToBE(55, 4), numToBE(78, 4)),
op3: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, numToBE((4783&55)^78, 4)),
},
{
tname: "same 16-byte register with 4-byte data for bitwise bool",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(4783, 4), Verdict{}),
op2: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_1, numToBE(55, 4), numToBE(78, 4)),
op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE((4783&55)^78, 4)),
},
// cmd: add rule ip filter input ip saddr and 0x11111111 == 285217024
{
tname: "dif 4-byte registers with 4-byte data for bitwise bool",
op1: mustCreateImmediate(t, linux.NFT_REG32_01, numToBE(400700800, 4), Verdict{}),
op2: mustCreateBitwiseBool(t, linux.NFT_REG32_01, linux.NFT_REG32_02, numToBE(0x11111111, 4), numToBE(0, 4)),
op3: mustCreateComparison(t, linux.NFT_REG32_02, linux.NFT_CMP_EQ, numToBE(400700800&0x11111111, 4)),
},
{
tname: "dif 16-byte registers with 4-byte data for bitwise bool",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(400700800, 4), Verdict{}),
op2: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_2, numToBE(0x11111111, 4), numToBE(0, 4)),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, numToBE(400700800&0x11111111, 4)),
},
// add rule ip filter input ip saddr or 0xff0230ff == 267583535
{
tname: "4- and 16-byte registers with 4-byte data for bitwise bool",
op1: mustCreateImmediate(t, linux.NFT_REG32_10, numToBE(0, 4), Verdict{}),
op2: mustCreateBitwiseBool(t, linux.NFT_REG32_10, linux.NFT_REG_2, numToBE(0x00cffd00, 4), numToBE(0xff3002ff, 4)),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, numToBE((0&0x00cffd00)^0xff3002ff, 4)),
},
{
tname: "16- and 4-byte registers with 4-byte data for bitwise bool",
op1: mustCreateImmediate(t, linux.NFT_REG_3, numToBE(0, 4), Verdict{}),
op2: mustCreateBitwiseBool(t, linux.NFT_REG_3, linux.NFT_REG32_05, numToBE(0x00cffd00, 4), numToBE(0xff3002ff, 4)),
op3: mustCreateComparison(t, linux.NFT_REG32_05, linux.NFT_CMP_EQ, numToBE((0&0x00cffd00)^0xff3002ff, 4)),
},
{
tname: "8-byte data for bitwise bool",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x12345678, 8), Verdict{}),
op2: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_1, numToBE(0x00cffd00, 8), numToBE(0xff3002ff, 8)),
op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, numToBE((0x12345678&0x00cffd00)^0xff3002ff, 8)),
},
{
tname: "16-byte data for bitwise bool",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, Verdict{}),
op2: mustCreateBitwiseBool(t, linux.NFT_REG_4, linux.NFT_REG_2, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe}),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}),
},
// Bitwise shift operations.
// No nft binary commands were observed that directly used shift operations.
{
tname: "0 shift left for bitwise lshift",
op1: mustCreateImmediate(t, linux.NFT_REG32_01, binary.NativeEndian.AppendUint32(nil, 4783), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG32_01, linux.NFT_REG32_01, 4, 0, false),
op3: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4783)),
},
{
tname: "0 shift right for bitwise rshift",
op1: mustCreateImmediate(t, linux.NFT_REG_1, binary.NativeEndian.AppendUint32(nil, 4783), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG_1, linux.NFT_REG_1, 4, 0, true),
op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4783)),
},
{
tname: "1-bit shift left for bitwise lshift",
op1: mustCreateImmediate(t, linux.NFT_REG_4, binary.NativeEndian.AppendUint32(nil, 4782), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG_4, linux.NFT_REG_4, 4, 1, false),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4782<<1)),
},
{
tname: "1-bit shift right for bitwise rshift",
op1: mustCreateImmediate(t, linux.NFT_REG32_06, binary.NativeEndian.AppendUint32(nil, 4782), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG32_06, linux.NFT_REG32_06, 4, 1, true),
op3: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4782>>1)),
},
{
tname: "8-bit shift left for bitwise lshift",
op1: mustCreateImmediate(t, linux.NFT_REG_4, binary.NativeEndian.AppendUint32(nil, 4782), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG_4, linux.NFT_REG_4, 4, 8, false),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4782<<8)),
},
{
tname: "8-bit shift right for bitwise rshift",
op1: mustCreateImmediate(t, linux.NFT_REG32_06, binary.NativeEndian.AppendUint32(nil, 4782), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG32_06, linux.NFT_REG32_06, 4, 8, true),
op3: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 4782>>8)),
},
{
tname: "16-bit shift left for bitwise lshift",
op1: mustCreateImmediate(t, linux.NFT_REG_4, append(binary.NativeEndian.AppendUint32(nil, 0), binary.NativeEndian.AppendUint32(nil, 0x45678910)...), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG_4, linux.NFT_REG_4, 8, 16, false),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, append(binary.NativeEndian.AppendUint32(nil, 0x00004567), binary.NativeEndian.AppendUint32(nil, 0x89100000)...)),
},
{
tname: "16-bit shift right for bitwise rshift",
op1: mustCreateImmediate(t, linux.NFT_REG32_06, binary.NativeEndian.AppendUint32(nil, 0x45678910), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG32_06, linux.NFT_REG32_06, 4, 16, true),
op3: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 0x45678910>>16)),
},
{
tname: "max-bit shift left for bitwise lshift",
op1: mustCreateImmediate(t, linux.NFT_REG32_03, binary.NativeEndian.AppendUint32(nil, 0x45678910), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG32_03, linux.NFT_REG_2, 4, bitshiftLimit-1, false),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint32(nil, 0)), // 0x45678910 << 31 in 32-bit is 0
},
{
tname: "max-bit shift right for bitwise rshift",
op1: mustCreateImmediate(t, linux.NFT_REG_3, append(binary.NativeEndian.AppendUint32(nil, 0x45678910), binary.NativeEndian.AppendUint32(nil, 0)...), Verdict{}),
op2: mustCreateBitwiseShift(t, linux.NFT_REG_3, linux.NFT_REG_2, 8, bitshiftLimit-1, true),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, append(binary.NativeEndian.AppendUint32(nil, 0), binary.NativeEndian.AppendUint32(nil, 0x8ACF1220)...)),
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a single table, chain, and rule.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept /* bcInfo */, "test chain", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
rule := &Rule{}
// Adds testing operations.
if test.op1 != nil {
rule.addOperation(test.op1)
}
if test.op2 != nil {
rule.addOperation(test.op2)
}
if test.op3 != nil {
rule.addOperation(test.op3)
}
// Adds drop operation. Will be final verdict if comparison is true.
rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, 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 := makeArbitraryIPv4Packet()
v, err := nf.EvaluateHook(stack.IP, arbitraryHook, pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
if v.Code != VC(linux.NF_DROP) {
t.Fatalf("expected verdict Drop for true comparison, got %v", v)
}
})
}
}
// TestEvaluateCounter tests that the Counter operation correctly increments the
// counter for the number of bytes and packets as it encounters packets.
// Note: relies on expected behavior of Comparison and Payload Load operations.
func TestEvaluateCounter(t *testing.T) {
// Creates a counter operation.
counter := newCounter(0, 0)
// Defines the packets to be used in the test.
desiredIpv4Address := tcpip.AddrFrom4(arbitraryIPv4AddrB)
countedIPv4Pkt := func() *stack.PacketBuffer {
fields := arbitraryIPv4Fields()
fields.SrcAddr = desiredIpv4Address
return makeIPv4Packet(header.IPv4MinimumSize, fields)
}
uncountedIPv4Pkt := func() *stack.PacketBuffer {
fields := arbitraryIPv4Fields()
fields.SrcAddr = tcpip.AddrFrom4(arbitraryIPv4AddrB2)
return makeIPv4Packet(header.IPv4MinimumSize, fields)
}
pkts := []*stack.PacketBuffer{countedIPv4Pkt(), uncountedIPv4Pkt(), countedIPv4Pkt(), countedIPv4Pkt(),
uncountedIPv4Pkt(), countedIPv4Pkt(), uncountedIPv4Pkt(), uncountedIPv4Pkt(), uncountedIPv4Pkt(), countedIPv4Pkt()}
t.Run("counter increment tests", func(t *testing.T) {
// Sets up an NFTables object with a base chain with policy accept.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept /* bcInfo */, "test chain", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
// Creates a rule that filters for the desired IPv4 address and adds the
// counter to the end of the rule. So, the counter should only increment for
// packets that satisfy the comparison.
rule := &Rule{}
rule.addOperation(mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, ipv4SrcAddrOffset, ipv4SrcAddrLen, linux.NFT_REG_1))
rule.addOperation(mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, desiredIpv4Address.AsSlice()))
rule.addOperation(counter)
if err := bc.RegisterRule(rule, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule: %v", err)
}
// Runs evaluation for each packet and checks whether the counter has
// incremented correctly.
prevBytes := counter.bytes.Load()
prevPackets := counter.packets.Load()
for i, pkt := range pkts {
_, err := nf.EvaluateHook(getAddrFamilyOrDefault(pkt, arbitraryFamily), arbitraryHook, pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook for packet %d: %v", i, err)
}
// Checks whether the counter should have incremented for the packet.
expectedDBytes, expectedDPackets := uint64(0), uint64(0)
if pkt.Network().SourceAddress() == desiredIpv4Address {
expectedDBytes, expectedDPackets = uint64(pkt.Size()), 1
}
// Checks that the counter incremented correctly.
newBytes := counter.bytes.Load()
newPackets := counter.packets.Load()
if dBytes := newBytes - prevBytes; dBytes != expectedDBytes {
t.Fatalf("counter bytes incremented by %d for packet %d, expected %d", dBytes, i, expectedDBytes)
}
if dPackets := newPackets - prevPackets; dPackets != expectedDPackets {
t.Fatalf("counter packets incremented by %d for packet %d, expected %d", dPackets, i, expectedDPackets)
}
// Updates the previous values for the next packet.
prevBytes = newBytes
prevPackets = newPackets
}
})
}
// TestEvaluateLast tests that the Last operation correctly records the last
// time the operation was evaluated.
func TestEvaluateLast(t *testing.T) {
// Creates last operation and number of elapses (in milliseconds) for testing.
last := &last{}
elapses := []int64{0, 1000, 500, 1000, 100, 200, 300, 20000, 50, 700}
totalElapsed := make([]int64, len(elapses))
copy(totalElapsed, elapses)
t.Run("last timing tests", func(t *testing.T) {
// Makes an arbitrary packet to be used in the test.
pkt := makeArbitraryIPv4Packet()
pktFamily := stack.IP
// Sets up an NFTables object with a base chain and fake manual clock.
fakeClock := faketime.NewManualClock()
fixedRNG := rand.RNGFrom(&fixedReader{})
nf := NewNFTables(nil /* stack */, fakeClock, fixedRNG)
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept /* bcInfo */, "test chain", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
// Registers a single rule with the last operation.
rule := &Rule{}
rule.addOperation(last)
if err := bc.RegisterRule(rule, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule: %v", err)
}
// Sets up a wait group to wait for all AfterFunc goroutines to complete.
var wg sync.WaitGroup
wg.Add(len(elapses))
defer wg.Wait()
// Calls EvaluateHook for each elapse and checks that the last operation
// recorded the correct timestamp and has the set flag set.
startStamp := nf.startTime.UnixMilli()
clock := nf.clock
for i := range elapses {
// Uses totalElapsed slice to avoid race conditions.
if i != 0 {
totalElapsed[i] += totalElapsed[i-1]
}
clock.AfterFunc(time.Duration(totalElapsed[i])*time.Millisecond, func() {
// Decrements wait group counter at end to signal func has completed.
defer wg.Done()
// Evaluates the packet (which should update last's timestamp).
_, err := nf.EvaluateHook(pktFamily, arbitraryHook, pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook for packet %d: %v", i, err)
}
// Checks the set flag and the timestamp via total elapsed time.
if !last.set.Load() {
t.Fatalf("last operation not set for packet %d", i)
}
if dTotal := last.timestampMS.Load() - startStamp; dTotal != totalElapsed[i] {
t.Fatalf("last operation recorded %d milliseconds since start for packet %d, expected %d", dTotal, i, totalElapsed[i])
}
})
}
// Manually advances the clock to trigger the AfterFunc goroutines.
for _, elapse := range elapses {
fakeClock.Advance(time.Duration(elapse) * time.Millisecond)
}
})
}
// TestEvaluateRoute tests that the Route operation correctly loads the specific
// route data into into the destination register.
// The nft binary commands used to generate these are stated above each test.
// Also note that all these commands mirror the ones in TestInterpretRouteOps.
// All commands should be preceded by nft --debug=netlink.
// Note: Relies on expected behavior of the Comparison operation.
func TestEvaluateRoute(t *testing.T) {
for _, test := range []struct {
tname string
pkt *stack.PacketBuffer
op1 operation // Route operation to test.
op2 operation // Comparison operation to check resulting data in register,
}{
// IPv4 Next Hop Commands
{ // cmd: add rule ip filter output rt nexthop 192.168.1.1
tname: "load nexthop4 key to 4-byte register",
pkt: func() *stack.PacketBuffer {
pkt := makeIPv4Packet(header.IPv6MinimumSize, arbitraryIPv4Fields())
pkt.EgressRoute.NextHop = tcpip.AddrFrom4(arbitraryIPv4AddrB)
return pkt
}(),
op1: mustCreateRoute(t, linux.NFT_RT_NEXTHOP4, linux.NFT_REG32_06),
op2: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, arbitraryIPv4AddrB[:]),
},
{ // cmd: add rule ip filter output rt nexthop 192.168.1.9
tname: "load nexthop4 key to 16-byte register",
pkt: func() *stack.PacketBuffer {
pkt := makeIPv4Packet(header.IPv6MinimumSize, arbitraryIPv4Fields())
pkt.EgressRoute.NextHop = tcpip.AddrFrom4(arbitraryIPv4AddrB2)
return pkt
}(),
op1: mustCreateRoute(t, linux.NFT_RT_NEXTHOP4, linux.NFT_REG_3),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_EQ, arbitraryIPv4AddrB2[:]),
},
// IPv6 Next Hop Commands
{ // cmd: add rule ip filter output rt nexthop 2001:db8:85a3::aa
tname: "load nexthop6 key to 16-byte register",
pkt: func() *stack.PacketBuffer {
pkt := makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields())
pkt.EgressRoute.NextHop = tcpip.AddrFrom16(arbitraryIPv6AddrB)
return pkt
}(),
op1: mustCreateRoute(t, linux.NFT_RT_NEXTHOP6, linux.NFT_REG_1),
op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, arbitraryIPv6AddrB[:]),
},
// TCP Maximum Segment Size Commands
{ // cmd: add rule ip filter output rt mtu 1500
tname: "load tcpmss key to 4-byte register",
pkt: func() *stack.PacketBuffer {
pkt := makeIPv4Packet(header.IPv6MinimumSize, arbitraryIPv4Fields())
pkt.GSOOptions.MSS = 1500
return pkt
}(),
op1: mustCreateRoute(t, linux.NFT_RT_TCPMSS, linux.NFT_REG32_00),
op2: mustCreateComparison(t, linux.NFT_REG32_00, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint16(nil, 1500)),
},
{ // cmd: add rule ip filter output rt mtu 0x0102
tname: "load tcpmss key to 16-byte register",
pkt: func() *stack.PacketBuffer {
pkt := makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields())
pkt.GSOOptions.MSS = 0x0102
return pkt
}(),
op1: mustCreateRoute(t, linux.NFT_RT_TCPMSS, linux.NFT_REG_4),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint16(nil, 0x0102)),
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a single table, chain, and rule.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept, "test chain", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
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 all comparisons are true.
rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, 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.
v, err := nf.EvaluateHook(getAddrFamilyOrDefault(test.pkt, arbitraryFamily), arbitraryHook, test.pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
// Checks for final verdict (should be Drop if comparisons are true).
if v.Code != VC(linux.NF_DROP) {
t.Fatalf("expected verdict Drop for true comparison, got %v", v)
}
})
}
}
// TestEvaluateByteorder tests that the Byteorder operation correctly performs
// the appropriate byteorder operation on the source register data and stores
// the result in the destination register.
// Note: Relies on expected behavior of the Immediate and Comparison operation.
func TestEvaluateByteorder(t *testing.T) {
// Given a big endian and little endian byte slice of the same number, returns
// the correct byte slice based on the host endianness.
// Note: Uses enclosure so endianness doesn't need to be passed as an arg or
// rechecked for every call.
chooseOrder := func() func([]byte, []byte) []byte {
hostBytes := binary.NativeEndian.AppendUint16(nil, 0x0102)
isBigEndian := hostBytes[0] == 0x01
return func(big, little []byte) []byte {
if isBigEndian {
return big
}
return little
}
}()
// Like createChooseOrder but takes ints instead of byte slices.
chooseOrderN := func(big, little, size int) []byte {
return chooseOrder(numToBE(big, size), numToBE(little, size))
}
for _, test := range []struct {
tname string
op1 operation // Immediate operation to set source register.
op2 operation // Byteorder operation to test.
op3 operation // Comparison operation to validate result.
}{
// Size 2 tests (Lengths 2, 3, 4, 6, 8, 16)
{
tname: "ntoh size 2 len 2",
op1: mustCreateImmediate(t, linux.NFT_REG32_01, numToBE(0x0102, 2), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG32_01, linux.NFT_REG32_01, linux.NFT_BYTEORDER_NTOH, 2, 2),
op3: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, chooseOrderN(0x0102, 0x0201, 2)),
},
{
tname: "hton size 2 len 2",
op1: mustCreateImmediate(t, linux.NFT_REG32_01, numToBE(0x0102, 2), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG32_01, linux.NFT_REG_1, linux.NFT_BYTEORDER_HTON, 2, 2),
op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, chooseOrderN(0x0102, 0x0201, 2)),
},
{
tname: "ntoh size 2 len 3",
op1: mustCreateImmediate(t, linux.NFT_REG32_01, numToBE(0x010203, 3), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG32_01, linux.NFT_REG_1, linux.NFT_BYTEORDER_NTOH, 3, 2),
op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, chooseOrderN(0x010203, 0x020100, 3)),
},
{
tname: "hton size 2 len 3",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x010203, 3), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG32_01, linux.NFT_BYTEORDER_HTON, 3, 2),
op3: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ, chooseOrderN(0x010203, 0x020100, 3)),
},
{
tname: "ntoh size 2 len 4",
op1: mustCreateImmediate(t, linux.NFT_REG32_10, numToBE(0x01020304, 4), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG32_10, linux.NFT_REG32_05, linux.NFT_BYTEORDER_NTOH, 4, 2),
op3: mustCreateComparison(t, linux.NFT_REG32_05, linux.NFT_CMP_EQ, chooseOrderN(0x01020304, 0x02010403, 4)),
},
{
tname: "hton size 2 len 4",
op1: mustCreateImmediate(t, linux.NFT_REG_4, numToBE(0x01020304, 4), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG32_09, linux.NFT_BYTEORDER_HTON, 4, 2),
op3: mustCreateComparison(t, linux.NFT_REG32_09, linux.NFT_CMP_EQ, chooseOrderN(0x01020304, 0x02010403, 4)),
},
{
tname: "ntoh size 2 len 6",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x010203040506, 6), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_1, linux.NFT_BYTEORDER_NTOH, 6, 2),
op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, chooseOrderN(0x010203040506, 0x020104030605, 6)),
},
{
tname: "hton size 2 len 6",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x010203040506, 6), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_1, linux.NFT_BYTEORDER_HTON, 6, 2),
op3: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, chooseOrderN(0x010203040506, 0x020104030605, 6)),
},
{
tname: "ntoh size 2 len 8",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x0102030405060708, 8), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_NTOH, 8, 2),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0201040306050807, 8)),
},
{
tname: "hton size 2 len 8",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x0102030405060708, 8), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_HTON, 8, 2),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0201040306050807, 8)),
},
{
tname: "ntoh size 2 len 16",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 16, 2),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
[]byte{0x02, 0x01, 0x04, 0x03, 0x06, 0x05, 0x08, 0x07, 0x0a, 0x09, 0x0c, 0x0b, 0x0e, 0x0d, 0x10, 0x0f})),
},
{
tname: "hton size 2 len 16",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_HTON, 16, 2),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
[]byte{0x02, 0x01, 0x04, 0x03, 0x06, 0x05, 0x08, 0x07, 0x0a, 0x09, 0x0c, 0x0b, 0x0e, 0x0d, 0x10, 0x0f})),
},
// Size 4 tests (Lengths 4, 6, 8, 16)
{
tname: "ntoh size 4 len 4",
op1: mustCreateImmediate(t, linux.NFT_REG32_05, numToBE(0x01020304, 4), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG32_05, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 4, 4),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrderN(0x01020304, 0x04030201, 4)),
},
{
tname: "hton size 4 len 4",
op1: mustCreateImmediate(t, linux.NFT_REG_4, numToBE(0x01020304, 4), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG32_09, linux.NFT_BYTEORDER_HTON, 4, 4),
op3: mustCreateComparison(t, linux.NFT_REG32_09, linux.NFT_CMP_EQ, chooseOrderN(0x01020304, 0x04030201, 4)),
},
{
tname: "ntoh size 4 len 6",
op1: mustCreateImmediate(t, linux.NFT_REG_4, numToBE(0x010203040506, 6), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 6, 4),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrderN(0x010203040506, 0x040302010000, 6)),
},
{
tname: "hton size 4 len 6",
op1: mustCreateImmediate(t, linux.NFT_REG_4, numToBE(0x010203040506, 6), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG_2, linux.NFT_BYTEORDER_HTON, 6, 4),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrderN(0x010203040506, 0x040302010000, 6)),
},
{
tname: "ntoh size 4 len 8",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x0102030405060708, 8), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_NTOH, 8, 4),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0403020108070605, 8)),
},
{
tname: "hton size 4 len 8",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x0102030405060708, 8), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_HTON, 8, 4),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0403020108070605, 8)),
},
{
tname: "ntoh size 4 len 16",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 16, 4),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
[]byte{0x04, 0x03, 0x02, 0x01, 0x08, 0x07, 0x06, 0x05, 0x0c, 0x0b, 0x0a, 0x09, 0x10, 0x0f, 0x0e, 0x0d})),
},
{
tname: "hton size 4 len 16",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_HTON, 16, 4),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
[]byte{0x04, 0x03, 0x02, 0x01, 0x08, 0x07, 0x06, 0x05, 0x0c, 0x0b, 0x0a, 0x09, 0x10, 0x0f, 0x0e, 0x0d})),
},
// Size 8 tests (Lengths 8, 12, 16)
{
tname: "ntoh size 8 len 8",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x0102030405060708, 8), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_NTOH, 8, 8),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0807060504030201, 8)),
},
{
tname: "hton size 8 len 8",
op1: mustCreateImmediate(t, linux.NFT_REG_1, numToBE(0x0102030405060708, 8), Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_4, linux.NFT_BYTEORDER_HTON, 8, 8),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrderN(0x0102030405060708, 0x0807060504030201, 8)),
},
{
tname: "ntoh size 8 len 12",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 12, 8),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c},
[]byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00})),
},
{
tname: "hton size 8 len 12",
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c}, Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_3, linux.NFT_REG_2, linux.NFT_BYTEORDER_HTON, 12, 8),
op3: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c},
[]byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00})),
},
{
tname: "ntoh size 8 len 16",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG_4, linux.NFT_BYTEORDER_NTOH, 16, 8),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
[]byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09})),
},
{
tname: "hton size 8 len 16",
op1: mustCreateImmediate(t, linux.NFT_REG_4, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, Verdict{}),
op2: mustCreateByteorder(t, linux.NFT_REG_4, linux.NFT_REG_4, linux.NFT_BYTEORDER_HTON, 16, 8),
op3: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, chooseOrder([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
[]byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09})),
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a single table, chain, and rule.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept /* bcInfo */, "test chain", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
rule := &Rule{}
// Adds testing operations.
if test.op1 != nil {
rule.addOperation(test.op1)
}
if test.op2 != nil {
rule.addOperation(test.op2)
}
if test.op3 != nil {
rule.addOperation(test.op3)
}
// Adds drop operation. Will be final verdict if comparison is true.
rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, 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 := makeArbitraryIPv4Packet()
v, err := nf.EvaluateHook(stack.IP, arbitraryHook, pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
if v.Code != VC(linux.NF_DROP) {
t.Fatalf("expected verdict Drop for true comparison, got %v", v)
}
})
}
}
// mockPacketOwner implements PacketOwner for testing.
type mockPacketOwner struct {
uid uint32
gid uint32
}
// KUID returns the UID of the mock packet owner.
func (m mockPacketOwner) KUID() uint32 {
return m.uid
}
// KGID returns the GID of the mock packet owner.
func (m mockPacketOwner) KGID() uint32 {
return m.gid
}
// TestEvaluateMetaLoad tests that the Meta Load operation correctly loads
// the specific meta data into the destination register.
// The nft binary commands used to generate these are stated above each test.
// All commands should be preceded by nft --debug=netlink.
// Note: Relies on expected behavior of the Comparison operation.
// Note: Does all comparisons in multiples of 4 bytes.
// TODO(b/339691111): Add tests for VLAN, ARP, ICMP, ICMPv6, IGMP, UDP headers.
func TestEvaluateMetaLoad(t *testing.T) {
// Initializes testing packet.
tcid := 0x05
pktSize := header.IPv6MinimumSize + header.TCPMinimumSize
ipv6Fields := arbitraryIPv6Fields()
ipv6Fields.TrafficClass = uint8(tcid)
tcpFields := arbitraryTCPFields()
pkt := makeIPv6TCPPacket(pktSize, ipv6Fields, tcpFields)
pkt.Owner = &mockPacketOwner{arbitrarySKUID, arbitrarySKGID}
// Sets up a fake clock (now = UnixEpoch) and dependent time/random fields.
fakeClock := faketime.NewManualClock()
now := fakeClock.Now()
timeNS := now.UnixNano()
timeDay := now.Weekday()
timeHour := now.Hour()*3600 + now.Minute()*60 + now.Second()
fixedRNG := rand.RNGFrom(&fixedReader{})
seededRandUint32 := fixedRNG.Uint32() // fixes rng
for _, test := range []struct {
tname string
pkt *stack.PacketBuffer
op1 operation // Meta Load operation to test.
op2 operation // Comparison operation to check result data in register.
// Note: op2 should be nil if expecting a break during evaluation.
}{
{ // cmd: add rule ip6 tab ch meta length 60
tname: "meta load len test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_LEN, linux.NFT_REG_2),
op2: mustCreateComparison(t, linux.NFT_REG_2, linux.NFT_CMP_EQ,
binary.NativeEndian.AppendUint32(nil, uint32(pktSize))),
},
{ // cmd: add rule ip6 tab ch meta protocol 0x86dd
tname: "meta load protocol test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_PROTOCOL, linux.NFT_REG_3),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_EQ,
append(numToBE(int(header.IPv6ProtocolNumber), 2), 0, 0)),
},
{ // meta nfproto is only useful in the inet family
// cmd: add rule inet tab ch meta nfproto 0x0a
tname: "meta load nfproto test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_NFPROTO, linux.NFT_REG_4),
op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ,
[]byte{linux.NFPROTO_IPV6, 0, 0, 0}),
},
{ // cmd: add rule ip6 tab ch meta l4proto 0x6
tname: "meta load l4proto test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_L4PROTO, linux.NFT_REG32_00),
op2: mustCreateComparison(t, linux.NFT_REG32_00, linux.NFT_CMP_EQ,
[]byte{uint8(tcpTransportProtocol), 0, 0, 0}),
},
{ // cmd: add rule ip6 tab ch skuid 0x020304
tname: "meta load skuid test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_SKUID, linux.NFT_REG32_02),
op2: mustCreateComparison(t, linux.NFT_REG32_02, linux.NFT_CMP_EQ,
binary.NativeEndian.AppendUint32(nil, arbitrarySKUID)),
},
{ // cmd: add rule ip6 tab ch skgid 45668
tname: "meta load skgid test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_SKGID, linux.NFT_REG32_03),
op2: mustCreateComparison(t, linux.NFT_REG32_03, linux.NFT_CMP_EQ,
binary.NativeEndian.AppendUint32(nil, arbitrarySKGID)),
},
{ // cmd: add rule ip6 tab ch rtclassid 0x05
tname: "meta load rtclassid test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_RTCLASSID, linux.NFT_REG32_04),
op2: mustCreateComparison(t, linux.NFT_REG32_04, linux.NFT_CMP_EQ,
binary.NativeEndian.AppendUint32(nil, uint32(tcid))),
},
{ // cmd: add rule ip6 tab ch pkttype 2
tname: "meta load pkttype test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_PKTTYPE, linux.NFT_REG32_05),
op2: mustCreateComparison(t, linux.NFT_REG32_05, linux.NFT_CMP_EQ,
[]byte{uint8(arbitraryPktType), 0, 0, 0}),
},
{ // cmd: add rule ip6 tab ch meta random 4059586549
tname: "meta load prandom test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_PRANDOM, linux.NFT_REG32_01),
op2: mustCreateComparison(t, linux.NFT_REG32_01, linux.NFT_CMP_EQ,
numToBE(int(seededRandUint32), 4)),
},
{ // cmd: add rule ip6 tab ch time "1970-01-01 00:00:00"
tname: "meta load time at unix epoch test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_TIME_NS, linux.NFT_REG_3),
op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_EQ,
binary.NativeEndian.AppendUint64(nil, uint64(timeNS))),
},
{ // cmd: add rule ip6 tab ch day Thursday
tname: "meta load day test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_TIME_DAY, linux.NFT_REG32_15),
op2: mustCreateComparison(t, linux.NFT_REG32_15, linux.NFT_CMP_EQ,
[]byte{uint8(timeDay), 0, 0, 0}),
},
{ // cmd: add rule inet tab ch hour 0x01020304
tname: "meta load hour test",
pkt: pkt,
op1: mustCreateMetaLoad(t, linux.NFT_META_TIME_HOUR, linux.NFT_REG32_14),
op2: mustCreateComparison(t, linux.NFT_REG32_14, linux.NFT_CMP_EQ,
binary.NativeEndian.AppendUint32(nil, uint32(timeHour))),
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a base chain and fake manual clock.
// Using Manual Clock sets time.Now to Unix Epoch which fixes rng seed!
nf := NewNFTables(nil /* stack */, fakeClock, rand.RNGFrom(&fixedReader{}))
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept /* bcInfo */, "test chain", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
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 all comparisons are true.
rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, 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.
v, err := nf.EvaluateHook(getAddrFamilyOrDefault(test.pkt, arbitraryFamily), arbitraryHook, test.pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
// Checks for final verdict.
if test.op2 == nil {
// If no comparison operation is set, then payload load should break,
// resulting in Accept as the default policy verdict.
if v.Code != VC(linux.NF_ACCEPT) {
t.Fatalf("expected verdict Accept for break during evaluation, got %v", v)
}
} else {
// If a comparison operation is set, both payload load and comparison
// should succeed, resulting in Drop as the final verdict.
if v.Code != VC(linux.NF_DROP) {
t.Fatalf("expected verdict Drop for true comparison, got %v", v)
}
}
})
}
}
// TestEvaluateMetaSet tests that the Meta Set operation correctly sets specific
// packet meta data to the value in the source register.
func TestEvaluateMetaSet(t *testing.T) {
// Packet type to set anc test for.
testPktType := tcpip.PacketMulticast
for _, test := range []struct {
tname string
pkt *stack.PacketBuffer
outPkt *stack.PacketBuffer
op1 operation // Immediate operation to load source register.
op2 operation // Meta set operation to test.
}{
// cmd: nft --debug=netlink add rule ip tab ch meta pkttype set 34
{
tname: "meta set pkttype 4-byte reg test",
pkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
outPkt: func() *stack.PacketBuffer {
pkt := makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields())
pkt.PktType = testPktType
return pkt
}(),
op1: mustCreateImmediate(t, linux.NFT_REG32_06, []byte{uint8(testPktType)}, Verdict{}),
op2: mustCreateMetaSet(t, linux.NFT_META_PKTTYPE, linux.NFT_REG32_06),
},
{
tname: "meta set pkttype 16-byte reg test",
pkt: makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields()),
outPkt: func() *stack.PacketBuffer {
pkt := makeIPv4Packet(header.IPv4MinimumSize, arbitraryIPv4Fields())
pkt.PktType = testPktType
return pkt
}(),
op1: mustCreateImmediate(t, linux.NFT_REG_3, []byte{uint8(testPktType)}, Verdict{}),
op2: mustCreateMetaSet(t, linux.NFT_META_PKTTYPE, linux.NFT_REG_3),
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object with a single table, chain, and rule.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept /* bcInfo */, "test chain", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
rule := &Rule{}
// Adds testing operations.
if test.op1 != nil {
rule.addOperation(test.op1)
}
if test.op2 != nil {
rule.addOperation(test.op2)
}
// Adds drop operation, to be final verdict if evaluation is successful.
rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, 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.
v, err := nf.EvaluateHook(getAddrFamilyOrDefault(test.pkt, arbitraryFamily), arbitraryHook, test.pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
// Evaluation should be successful and result in Drop verdict.
if v.Code != VC(linux.NF_DROP) {
t.Fatalf("expected verdict Drop for successful evaluation, got %v", v)
}
// Checks if the packet are equal.
checkPacketEquality(t, test.outPkt, test.pkt)
})
}
}
// TestLoopCheckOnRegisterAndUnregister tests the loop checking and accompanying
// logic on registering and unregistering rules.
func TestLoopCheckOnRegisterAndUnregister(t *testing.T) {
for _, test := range []struct {
tname string
chains map[string]*Chain
verdict Verdict
shouldErr bool
}{
{
tname: "jump to non-existent chain",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "non_existent_chain")},
}},
},
},
shouldErr: true,
},
{
tname: "goto to non-existent chain",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "non_existent_chain")},
}},
},
},
shouldErr: true,
},
{
tname: "jump to itself",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "base_chain")},
}},
},
},
shouldErr: true,
},
{
tname: "goto to itself",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "base_chain")},
}},
},
},
shouldErr: true,
},
{
tname: "simple 2-chain loop",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain")},
}},
},
"aux_chain": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "base_chain")},
}},
},
},
shouldErr: true,
},
{
tname: "2-chain loop with entry point outside loop",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain")},
}},
},
"aux_chain": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "aux_chain2")},
}},
},
"aux_chain2": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "aux_chain")},
}},
},
},
shouldErr: true,
},
{
tname: "simple 3-chain loop",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain")},
}},
},
"aux_chain": {
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain2")},
}},
},
"aux_chain2": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "base_chain")},
}},
},
},
shouldErr: true,
},
{
tname: "3-chain loop with entry point 2 points outside loop",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain")},
}},
},
"aux_chain": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "aux_chain2")},
}},
},
"aux_chain2": {
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain3")},
}},
},
"aux_chain3": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "aux_chain4")},
}},
},
"aux_chain4": {
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain2")},
}},
},
},
shouldErr: true,
},
{
tname: "simple 4-chain loop",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain")},
}},
},
"aux_chain": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "aux_chain2")},
}},
},
"aux_chain2": {
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain3")},
}},
},
"aux_chain3": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "base_chain")},
}},
},
},
shouldErr: true,
},
{
tname: "simple 5-chain loop",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain")},
}},
},
"aux_chain": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "aux_chain2")},
}},
},
"aux_chain2": {
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain3")},
}},
},
"aux_chain3": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "base_chain")},
}},
},
},
shouldErr: true,
},
{
// 0
// / \
// v v
// 1 <- 2 <-> 3
tname: "complex 2-3 loop",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{{
ops: []operation{
mustCreateJump(t, "aux_chain"),
mustCreateJump(t, "aux_chain2"),
},
}},
},
"aux_chain": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)})},
}},
},
"aux_chain2": {
rules: []*Rule{{
ops: []operation{
mustCreateJump(t, "aux_chain"),
mustCreateJump(t, "aux_chain3"),
},
}},
},
"aux_chain3": {
rules: []*Rule{{
ops: []operation{mustCreateGoto(t, "aux_chain2")},
}},
},
},
shouldErr: true,
},
{
tname: "simple loop amongst other rules and operations",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{
{ops: []operation{mustCreateImmediate(t, linux.NFT_REG_1, []byte{0, 1, 2, 3}, Verdict{})}},
{ops: []operation{mustCreateImmediate(t, linux.NFT_REG32_14, []byte{0, 1, 2, 3}, Verdict{})}},
{ops: []operation{mustCreateJump(t, "aux_chain")}},
},
},
"aux_chain": {
rules: []*Rule{{
ops: []operation{
mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
mustCreateGoto(t, "aux_chain2"),
},
}},
},
"aux_chain2": {
rules: []*Rule{{
ops: []operation{mustCreateJump(t, "aux_chain3")},
}},
},
"aux_chain3": {
rules: []*Rule{
{ops: []operation{mustCreateImmediate(t, linux.NFT_REG_1, []byte{0, 1, 2, 3}, Verdict{})}},
{ops: []operation{mustCreateImmediate(t, linux.NFT_REG32_14, []byte{0, 1, 2, 3}, Verdict{})}},
{ops: []operation{
mustCreateImmediate(t, linux.NFT_REG_4, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Verdict{}),
mustCreateGoto(t, "aux_chain"),
mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}),
}},
},
},
},
shouldErr: true,
},
{
tname: "base chain jump to 3 other chains",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{
{
ops: []operation{
mustCreateJump(t, "aux_chain"),
mustCreateJump(t, "aux_chain2"),
},
},
{ops: []operation{mustCreateJump(t, "aux_chain3")}},
},
},
"aux_chain": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_2, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Verdict{})},
}},
},
"aux_chain2": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_3, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Verdict{})},
}},
},
"aux_chain3": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_4, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Verdict{})},
}},
},
},
verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy
},
{
tname: "base chain jump to 3 other chains with last chain dropping",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{
{
ops: []operation{
mustCreateJump(t, "aux_chain"),
mustCreateJump(t, "aux_chain2"),
},
},
{ops: []operation{mustCreateJump(t, "aux_chain3")}},
},
},
"aux_chain": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_2, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Verdict{})},
}}},
"aux_chain2": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_3, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Verdict{})},
}},
},
"aux_chain3": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)})},
}},
},
},
verdict: Verdict{Code: VC(linux.NF_DROP)}, // from last chain
},
{
tname: "base chain jump to 3 other chains with last rule in base chain dropping",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{
{
ops: []operation{
mustCreateJump(t, "aux_chain"),
mustCreateJump(t, "aux_chain2"),
},
},
{ops: []operation{mustCreateJump(t, "aux_chain3")}},
{ops: []operation{mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)})}},
},
},
"aux_chain": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_2, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Verdict{})},
}}},
"aux_chain2": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_3, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Verdict{})},
}},
},
"aux_chain3": {
comment: "strictly target",
rules: []*Rule{{
ops: []operation{mustCreateImmediate(t, linux.NFT_REG_4, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Verdict{})},
}},
},
},
verdict: Verdict{Code: VC(linux.NF_DROP)}, // from last rule in base chain
},
{
tname: "jump to the same chain",
chains: map[string]*Chain{
"base_chain": {
baseChainInfo: arbitraryInfoPolicyAccept,
rules: []*Rule{
{
ops: []operation{
mustCreateJump(t, "aux_chain"),
mustCreateJump(t, "aux_chain"),
},
},
},
},
"aux_chain": {
comment: "strictly target",
rules: []*Rule{{}},
},
},
verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up an NFTables object based on test struct.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
// Creates all chains in the test struct first. This is necessary so the
// loop checking sees the target chains exist (otherwise it would error).
for chainName, chainInit := range test.chains {
policy := linux.NF_ACCEPT
if chainInit.baseChainInfo != nil && chainInit.baseChainInfo.PolicyDrop {
policy = linux.NF_DROP
}
nf.AddChainToTable(tab, chainName, chainInit.GetBaseChainInfo(), chainInit.GetComment(), false, 0, nil, uint8(policy))
}
if len(test.chains) != tab.ChainCount() {
t.Fatalf("not all chains added to table")
}
// Registers all rules to all chains in the test struct.
for chainName, chainInit := range test.chains {
chain, err := nf.GetChain(tab.GetAddressFamily(), tab.GetName(), chainName)
if err != nil {
t.Fatalf("unexpected error for GetChain: %v", err)
}
for _, rule := range chainInit.rules {
for _, op := range rule.ops {
if imm, ok := op.(*immediate); ok && (imm.verdict.Code == VC(linux.NFT_JUMP) || imm.verdict.Code == VC(linux.NFT_GOTO)) && imm.verdict.Chain != nil {
targetChain, err := nf.GetChain(tab.GetAddressFamily(), tab.GetName(), imm.verdict.Chain.name)
if err == nil {
imm.verdict.Chain = targetChain
} else {
imm.verdict.Chain = nil
}
}
}
// Note: this is where the loop checking is triggered.
if err := chain.RegisterRule(rule, -1); err != nil {
if !test.shouldErr {
t.Fatalf("unexpected error for RegisterRule: %v", err)
}
return
}
// Checks that the chain was assigned to the rule.
if rule.chain == nil {
t.Fatalf("chain is not assigned to rule after RegisterRule")
}
}
if chainInit.RuleCount() != chain.RuleCount() {
t.Fatalf("not all rules added to chain")
}
}
// Runs evaluation and checks verdict.
pkt := makeArbitraryIPv4Packet()
v, err := nf.EvaluateHook(stack.IP, arbitraryHook, pkt, nil /* route */)
if err != nil {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
if v.Code != test.verdict.Code {
t.Fatalf("expected verdict %v, got %v", test.verdict, v)
}
// Unregisters all rules from all chains and checks that the chain is
// unassigned from the rule.
for chainName, chainInit := range test.chains {
chain, err := nf.GetChain(tab.GetAddressFamily(), tab.GetName(), chainName)
if err != nil {
t.Fatalf("unexpected error for GetChain: %v", err)
}
for rIdx := chainInit.RuleCount() - 1; rIdx >= 0; rIdx-- {
rule, err := chain.UnregisterRuleByIndex(rIdx)
if err != nil {
t.Fatalf("unexpected error for UnregisterRule: %v", err)
}
if rule != chainInit.rules[rIdx] {
t.Fatalf("rule returned by UnregisterRule does not match previously registered rule")
}
if rule.chain != nil {
t.Fatalf("chain is not unassigned from rule after UnregisterRule")
}
}
if chain.RuleCount() != 0 {
t.Fatalf("not all rules removed from chain")
}
}
})
}
}
// TestMaxNestedJumps tests the limit on nested jumps (no limit for gotos).
func TestMaxNestedJumps(t *testing.T) {
for _, test := range []struct {
tname string
useJumpOp bool
numberOfJumps int
verdict Verdict
expectErr bool
}{
{
tname: "nested jump limit reached with jumps",
useJumpOp: true,
numberOfJumps: nestedJumpLimit,
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
{
tname: "nested jump limit reached with gotos",
useJumpOp: false,
numberOfJumps: nestedJumpLimit,
verdict: Verdict{Code: VC(linux.NF_DROP)},
},
{
tname: "nested jump limit exceeded with jumps",
useJumpOp: true,
numberOfJumps: nestedJumpLimit + 1,
expectErr: true,
},
{
tname: "nested jump limit exceeded with gotos",
useJumpOp: false,
numberOfJumps: nestedJumpLimit + 1,
verdict: Verdict{Code: VC(linux.NF_DROP)}, // limit only for jumps
},
} {
t.Run(test.tname, func(t *testing.T) {
// Sets up chains of nested jumps or gotos.
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
for i := test.numberOfJumps - 1; i >= 0; i-- {
name := fmt.Sprintf("chain %d", i)
var c *Chain
var err *syserr.AnnotatedError
if i == 0 {
c, err = nf.AddChainToTable(tab, name, arbitraryInfoPolicyAccept, "test chain", false, 0, nil, linux.NF_ACCEPT)
} else {
c, err = nf.AddChainToTable(tab, name, nil /* bcInfo */, "test chain", false, 0, nil, linux.NF_ACCEPT)
}
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
r := &Rule{}
if i == test.numberOfJumps-1 {
err = r.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_DROP)}))
} else {
targetName := fmt.Sprintf("chain %d", i+1)
var targetChain *Chain
if targetChain, err = nf.GetChain(tab.GetAddressFamily(), tab.GetName(), targetName); err != nil {
t.Fatalf("unexpected error getting chain: %v", err)
}
var op *immediate
if test.useJumpOp {
op = mustCreateJump(t, targetName)
} else {
op = mustCreateGoto(t, targetName)
}
op.verdict.Chain = targetChain
err = r.addOperation(op)
}
if err != nil {
t.Fatalf("unexpected error for AddOperation: %v", err)
}
if err := c.RegisterRule(r, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule: %v", err)
}
}
// Runs evaluation and checks verdict.
pkt := makeArbitraryIPv4Packet()
v, err := nf.EvaluateHook(getAddrFamilyOrDefault(pkt, arbitraryFamily), arbitraryHook, pkt, nil /* route */)
if err != nil {
if !test.expectErr {
t.Fatalf("unexpected error for EvaluateHook: %v", err)
}
}
if v.Code != test.verdict.Code {
t.Fatalf("expected verdict %v, got %v", test.verdict, v)
}
})
}
}
// checkPacketEquality checks that the given packets are equal for all fields
// and data relevant to our testing. This is not an exhaustive check.
func checkPacketEquality(t *testing.T, expected, actual *stack.PacketBuffer) {
if expected.PktType != actual.PktType {
t.Fatalf("expected packet type %d for resulting packet, got %d", int(expected.PktType), int(actual.PktType))
}
// Compares checksums first for the expected and actual packet.
if expected.NetworkProtocolNumber != actual.NetworkProtocolNumber {
t.Fatalf("expected network protocol number %d for resulting packet, got %d", expected.NetworkProtocolNumber, actual.NetworkProtocolNumber)
}
if actualHasNetwork, expectedHasNetwork := actual.NetworkHeader().View() != nil, expected.NetworkHeader().View() != nil; actualHasNetwork != expectedHasNetwork {
t.Fatalf("expected network header is present to be %t for resulting packet, got %v", actualHasNetwork, expectedHasNetwork)
}
if actual.NetworkHeader().View() != nil && expected.Network().Checksum() != actual.Network().Checksum() {
t.Fatalf("expected network checksum %d for resulting packet, got %d", expected.Network().Checksum(), actual.Network().Checksum())
}
if actual.TransportProtocolNumber != expected.TransportProtocolNumber {
t.Fatalf("expected transport protocol number %d for resulting packet, got %d", expected.TransportProtocolNumber, actual.TransportProtocolNumber)
}
if actual.TransportProtocolNumber != 0 {
var transport header.Transport
var transportExpected header.Transport
switch tBytes, tOutBytes := actual.TransportHeader().Slice(), expected.TransportHeader().Slice(); actual.TransportProtocolNumber {
case header.TCPProtocolNumber:
transport = header.TCP(tBytes)
transportExpected = header.TCP(tOutBytes)
case header.UDPProtocolNumber:
transport = header.UDP(tBytes)
transportExpected = header.UDP(tOutBytes)
case header.ICMPv4ProtocolNumber:
transport = header.ICMPv4(tBytes)
transportExpected = header.ICMPv4(tOutBytes)
case header.ICMPv6ProtocolNumber:
transport = header.ICMPv6(tBytes)
transportExpected = header.ICMPv6(tOutBytes)
case header.IGMPProtocolNumber:
transport = header.IGMP(tBytes)
transportExpected = header.IGMP(tOutBytes)
}
if transport != nil && transport.Checksum() != transportExpected.Checksum() {
t.Fatalf("expected transport checksum %d for resulting packet, got %d", transport.Checksum(), transportExpected.Checksum())
}
}
// Compares raw packet data in bytes for resulting and expected packet.
actualSlices := actual.AsSlices()
expectedSlices := expected.AsSlices()
if len(actualSlices) != len(expectedSlices) {
t.Fatalf("expected %d slices of data for the resulting packet, got %d", len(expectedSlices), len(actualSlices))
}
for i := range actualSlices {
if !slices.Equal(actualSlices[i], expectedSlices[i]) {
t.Fatalf("packet data does not match expected packet data (for slice %d)", i)
}
}
}
// numToBE converts an n-byte int to Big Endian where n is in [1, 8].
// Assumes the given number can be represented in n bytes.
func numToBE(v int, n int) []byte {
if n > 8 {
panic("cannot support more than 8 bytes")
}
// Gets 8-byte slice Big Endian representation of the number.
be64 := binary.BigEndian.AppendUint64(nil, uint64(v))
// Returns last n bytes as the n-byte Big Endian representation.
return be64[8-n:]
}
// packetResultString compares 2 packets by equality and returns a string
// representation.
func packetResultString(initial, final *stack.PacketBuffer) string {
if final == nil {
return "nil"
}
if reflect.DeepEqual(final, initial) {
return "unmodified"
}
return "modified"
}
// newNFTablesStd creates a new NFTables object w/ a standard clock for testing.
func newNFTablesStd() *NFTables {
stdClock := tcpip.NewStdClock()
fixedRNG := rand.RNGFrom(&fixedReader{})
return NewNFTables(nil /* stack */, stdClock, fixedRNG)
}
func setVerdictChainIfJumpGoto(op operation, chain *Chain) {
imm, ok := op.(*immediate)
if !ok {
return
}
ok, vc := isJumpOrGotoOperation(op)
if !ok || vc != nil {
return
}
imm.verdict.Chain = chain
}
// mustCreateImmediate wraps the newImmediate function for brevity.
func mustCreateImmediate(t *testing.T, dreg uint8, data []byte, verdict Verdict) *immediate {
dataType := immRegToType(uint32(dreg))
imm, err := newImmediate(dreg, dataType, data, verdict)
if err != nil {
t.Fatalf("failed to create immediate: %v", err)
}
return imm
}
// mustCreateJump wraps the newImmediate function for jump operations in tests.
func mustCreateJump(t *testing.T, targetChainName string) *immediate {
return mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{
Code: VC(linux.NFT_JUMP),
Chain: &Chain{name: targetChainName},
})
}
// mustCreateGoto wraps the newImmediate function for goto operations in tests.
func mustCreateGoto(t *testing.T, targetChainName string) *immediate {
return mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{
Code: VC(linux.NFT_GOTO),
Chain: &Chain{name: targetChainName},
})
}
// mustCreateComparison wraps the newComparison function for brevity.
func mustCreateComparison(t *testing.T, sreg uint8, cop int, data []byte) *comparison {
cmp, err := newComparison(sreg, cop, data)
if err != nil {
t.Fatalf("failed to create comparison: %v", err)
}
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, int(len), dreg)
if err != nil {
t.Fatalf("failed to create payload load: %v", err)
}
return pdload
}
// mustCreatePayloadSet wraps the newPayloadSet function for brevity.
func mustCreatePayloadSet(t *testing.T, base payloadBase, offset uint8, len uint8, sreg uint8, csumType uint8, csumOff uint8, csumFlags uint8) *payloadSet {
pdset, err := newPayloadSet(base, offset, len, sreg, csumType, csumOff, csumFlags)
if err != nil {
t.Fatalf("failed to create payload set: %v", err)
}
return pdset
}
// mustCreateBitwiseBool wraps the newBitwiseBool function for brevity.
func mustCreateBitwiseBool(t *testing.T, sreg, dreg uint8, mask, xor []byte) *bitwise {
bit, err := newBitwiseBool(sreg, dreg, mask, xor, len(mask))
if err != nil {
t.Fatalf("failed to create bitwise bool: %v", err)
}
return bit
}
// mustCreateBitwiseShift wraps the newBitwiseShift function for brevity.
func mustCreateBitwiseShift(t *testing.T, sreg, dreg, blen uint8, shift uint32, right bool) *bitwise {
bit, err := newBitwiseShift(sreg, dreg, int(blen), shift, right)
if err != nil {
t.Fatalf("failed to create bitwise shift: %v", err)
}
return bit
}
// mustCreateRoute wraps the newRoute function for brevity.
func mustCreateRoute(t *testing.T, key routeKey, dreg uint8) *route {
rt, err := newRoute(key, dreg)
if err != nil {
t.Fatalf("failed to create route: %v", err)
}
return rt
}
// mustCreateByteorder wraps the newByteorder function for brevity.
func mustCreateByteorder(t *testing.T, sreg, dreg uint8, bop byteorderOp, blen, size uint8) *byteorder {
order, err := newByteorder(sreg, dreg, bop, blen, size)
if err != nil {
t.Fatalf("failed to create byteorder: %v", err)
}
return order
}
// mustCreateMetaLoad wraps the newMetaLoad function for brevity.
func mustCreateMetaLoad(t *testing.T, key metaKey, dreg uint8) *metaLoad {
mtload, err := newMetaLoad(key, dreg)
if err != nil {
t.Fatalf("failed to create meta load: %v", err)
}
return mtload
}
// mustCreateMetaSet wraps the newMetaSet function for brevity.
func mustCreateMetaSet(t *testing.T, key metaKey, sreg uint8) *metaSet {
mtset, err := newMetaSet(key, sreg)
if err != nil {
t.Fatalf("failed to create meta set: %v", err)
}
return mtset
}
// A fixedReader sets all bytes to the same value (1) when Read is called.
//
// It is used to make the RNG deterministic for testing, i.e. it's really
// really bad at being an RNG.
type fixedReader struct{}
// Read implements io.Reader.Read.
func (*fixedReader) Read(buf []byte) (int, error) {
for i := range len(buf) {
buf[i] = 1
}
return len(buf), nil
}
func createEmptyNlMsg() *nlmsg.Message {
m := nlmsg.NewMessage(linux.NetlinkMessageHeader{})
m.Put(&linux.NetFilterGenMsg{})
return m
}
func TestNfAttrParser(t *testing.T) {
tests := []struct {
name string
msg *nlmsg.Message
policy []NlaPolicy
want map[uint16]nlmsg.BytesView
}{
{
name: "validAttrs",
msg: func() *nlmsg.Message {
m := createEmptyNlMsg()
m.PutAttr(linux.NFTA_TABLE_HANDLE, nlmsg.PutU32(123))
m.PutAttr(linux.NFTA_TABLE_USE, nlmsg.PutU64(1))
m.PutAttrString(linux.NFTA_TABLE_NAME, "test_table")
return m
}(),
policy: []NlaPolicy{
linux.NFTA_TABLE_HANDLE: {nlaType: linux.NLA_BE32, validator: AttrMaxValidator[uint32](256)},
linux.NFTA_TABLE_USE: {nlaType: linux.NLA_U64, validator: AttrMaxValidator[uint64](nlmsg.HostToNetU64(1))},
linux.NFTA_TABLE_NAME: {nlaType: linux.NLA_STRING},
},
want: map[uint16]nlmsg.BytesView{
linux.NFTA_TABLE_HANDLE: []byte{0, 0, 0, 123}, // Network byte order.
linux.NFTA_TABLE_USE: []byte{0, 0, 0, 0, 0, 0, 0, 1}, // Network byte order.
// The string is expected to be null terminated.
linux.NFTA_TABLE_NAME: []byte{'t', 'e', 's', 't', '_', 't', 'a', 'b', 'l', 'e', '\x00'},
},
},
{
name: "attrsWithoutPolicy",
msg: func() *nlmsg.Message {
m := createEmptyNlMsg()
m.PutAttr(linux.NFTA_TABLE_HANDLE, nlmsg.PutU32(123))
m.PutAttr(linux.NFTA_TABLE_USE, nlmsg.PutU64(1))
return m
}(),
want: map[uint16]nlmsg.BytesView{
linux.NFTA_TABLE_HANDLE: []byte{0, 0, 0, 123},
linux.NFTA_TABLE_USE: []byte{0, 0, 0, 0, 0, 0, 0, 1},
},
},
{
name: "attrGtThanMax",
msg: func() *nlmsg.Message {
m := createEmptyNlMsg()
m.PutAttr(linux.NFTA_TABLE_HANDLE, nlmsg.PutU32(123))
return m
}(),
policy: []NlaPolicy{
linux.NFTA_TABLE_HANDLE: {nlaType: linux.NLA_BE32, validator: AttrMaxValidator[uint32](1)},
},
},
{
name: "attrWithInvalidType",
msg: func() *nlmsg.Message {
m := createEmptyNlMsg()
m.PutAttr(linux.NFTA_TABLE_HANDLE, nlmsg.PutU16(123))
return m
}(),
policy: []NlaPolicy{
linux.NFTA_TABLE_HANDLE: {nlaType: linux.NLA_U32},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var nfGenMsg linux.NetFilterGenMsg
attr, ok := test.msg.GetData(&nfGenMsg)
if !ok {
t.Fatalf("GetData() failed for msg %v", test.msg)
}
got, gotErr := NfParseWithOpts(attr, &NfParseOpts{Policy: test.policy})
wantErr := test.want == nil
if wantErr && gotErr == nil {
t.Fatalf("NfParseWithOpts() failed, want error, got nil error")
}
if gotErr != nil {
return
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Fatalf("NfParseWithOpts() returned diff (-want +got): %v", diff)
}
})
}
}
func TestNfAttrParserNestedArray(t *testing.T) {
putMsg := func(b []byte) *nlmsg.Message {
m := createEmptyNlMsg()
m.PutNestedAttr(linux.NFTA_RULE_EXPRESSIONS, b)
return m
}
newArray := func(elems ...nlmsg.NestedAttr) nlmsg.NestedAttr {
arr := nlmsg.NestedAttr{}
for _, elem := range elems {
arr.PutAttr(linux.NFTA_LIST_ELEM, primitive.AsByteSlice(elem))
}
return arr
}
tests := []struct {
name string
setup func() (msg *nlmsg.Message, policy []NlaPolicy, want map[uint16]nlmsg.BytesView)
}{
{
name: "validArray",
setup: func() (*nlmsg.Message, []NlaPolicy, map[uint16]nlmsg.BytesView) {
//
// [ NFTA_RULE_EXPRESSIONS (Outer Array Container) ]
// └── [ NFTA_LIST_ELEM (Element #1) ]
// ├── [ NFTA_PAYLOAD_SREG ] (Value: 100)
// └── [ NFTA_PAYLOAD_DREG ] (Value: 123)
elementPolicy := []NlaPolicy{
linux.NFTA_PAYLOAD_SREG: {nlaType: linux.NLA_U32},
linux.NFTA_PAYLOAD_DREG: {nlaType: linux.NLA_U32},
}
elems := [2]nlmsg.NestedAttr{}
elems[0].PutAttr(linux.NFTA_PAYLOAD_SREG, nlmsg.PutU32(100))
elems[0].PutAttr(linux.NFTA_PAYLOAD_DREG, nlmsg.PutU32(123))
elems[1].PutAttr(linux.NFTA_PAYLOAD_SREG, nlmsg.
PutU32(200))
arrayAttr := newArray(elems[:]...)
policy := []NlaPolicy{
linux.NFTA_RULE_EXPRESSIONS: {
nlaType: linux.NLA_NESTED_ARRAY,
validator: AttrArrayValidator(elementPolicy),
},
}
return putMsg(arrayAttr), policy, map[uint16]nlmsg.BytesView{
linux.NFTA_RULE_EXPRESSIONS: nlmsg.BytesView(arrayAttr),
}
},
},
{
name: "validArrayOfArrays",
setup: func() (*nlmsg.Message, []NlaPolicy, map[uint16]nlmsg.BytesView) {
//
// [ NFTA_RULE_EXPRESSIONS (Outer Array Container) ]
// ├── [ NFTA_LIST_ELEM (Outer Element Wrapper #1) ]
// │ └── [ NFTA_EXPR_DATA (Inner Array Container #1) ]
// │ └── [ NFTA_LIST_ELEM (Inner Element Wrapper #1) ]
// │ └── [ NFTA_IMMEDIATE_DREG ] (Value: 100)
// │
// └── [ NFTA_LIST_ELEM (Outer Element Wrapper #2) ]
// └── [ NFTA_EXPR_DATA (Inner Array Container #2) ]
// └── [ NFTA_LIST_ELEM (Inner Element Wrapper #2) ]
// └── [ NFTA_IMMEDIATE_DREG ] (Value: 200)
elementPolicy := []NlaPolicy{
linux.NFTA_IMMEDIATE_DREG: {nlaType: linux.NLA_U32},
}
innerArrayPolicy := []NlaPolicy{
linux.NFTA_EXPR_DATA: {
nlaType: linux.NLA_NESTED_ARRAY,
validator: AttrArrayValidator(elementPolicy),
},
}
innerElems := [2]nlmsg.NestedAttr{}
innerElems[0].PutAttr(linux.NFTA_IMMEDIATE_DREG, nlmsg.PutU32(100))
innerElems[1].PutAttr(linux.NFTA_IMMEDIATE_DREG, nlmsg.PutU32(200))
outerElems := [2]nlmsg.NestedAttr{}
outerElems[0].PutAttr(linux.NFTA_EXPR_DATA, primitive.AsByteSlice(newArray(innerElems[0])))
outerElems[1].PutAttr(linux.NFTA_EXPR_DATA, primitive.AsByteSlice(newArray(innerElems[1])))
outerArray := newArray(outerElems[:]...)
policy := []NlaPolicy{
linux.NFTA_RULE_EXPRESSIONS: {
nlaType: linux.NLA_NESTED_ARRAY,
validator: AttrArrayValidator(innerArrayPolicy),
},
}
return putMsg(outerArray), policy, map[uint16]nlmsg.BytesView{
linux.NFTA_RULE_EXPRESSIONS: nlmsg.BytesView(outerArray),
}
},
},
{
name: "invalidElement",
setup: func() (*nlmsg.Message, []NlaPolicy, map[uint16]nlmsg.BytesView) {
//
// [ NFTA_RULE_EXPRESSIONS (Outer Array Container) ]
// ├── [ NFTA_LIST_ELEM (Element Wrapper #1) ]
// │ └── [ NFTA_PAYLOAD_SREG ] (Value: 2)
// │
// └── [ NFTA_LIST_ELEM (Element Wrapper #2) ]
// └── [ NFTA_PAYLOAD_SREG ] (Value: 10)
elementPolicy := []NlaPolicy{
linux.NFTA_PAYLOAD_SREG: {
nlaType: linux.NLA_U32,
validator: AttrMaxValidator[uint32](8),
},
}
elems := [2]nlmsg.NestedAttr{}
elems[0].PutAttr(linux.NFTA_PAYLOAD_SREG, nlmsg.PutU32(2))
// Invalid value exceeds max value of validator.
elems[1].PutAttr(linux.NFTA_PAYLOAD_SREG, nlmsg.PutU32(10))
arrayAttr := newArray(elems[:]...)
policy := []NlaPolicy{
linux.NFTA_RULE_EXPRESSIONS: {
nlaType: linux.NLA_NESTED_ARRAY,
validator: AttrArrayValidator(elementPolicy),
},
}
return putMsg(arrayAttr), policy, nil
},
},
{
name: "recursionDepthExceeded",
setup: func() (*nlmsg.Message, []NlaPolicy, map[uint16]nlmsg.BytesView) {
//
// [ NFTA_RULE_EXPRESSIONS (Outer Array Container) ]
// └── [ NFTA_EXPR_DATA ] (Nesting Layer #1)
// └── [ NFTA_EXPR_DATA ] (Nesting Layer #2)
// └── ... (Nesting Layer #10)
recursePolicy := []NlaPolicy{
linux.NFTA_EXPR_DATA: {
nlaType: linux.NLA_NESTED_ARRAY,
validator: AttrArrayValidator(nil),
},
}
recursePolicy[linux.NFTA_EXPR_DATA].validator = AttrArrayValidator(recursePolicy)
policy := []NlaPolicy{
linux.NFTA_RULE_EXPRESSIONS: {
nlaType: linux.NLA_NESTED_ARRAY,
validator: AttrArrayValidator(recursePolicy),
},
}
curr := nlmsg.NestedAttr{}
// Build a nested attribute with maxPolicyRecursionDepth + 1 nesting.
for i := 0; i < maxPolicyRecursionDepth; i++ {
upper := nlmsg.NestedAttr{}
upper.PutAttr(linux.NFTA_EXPR_DATA, primitive.AsByteSlice(curr))
curr = upper
}
return putMsg(curr), policy, nil
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
msg, policy, want := test.setup()
var nfGenMsg linux.NetFilterGenMsg
attr, ok := msg.GetData(&nfGenMsg)
if !ok {
t.Fatalf("GetData() failed for msg %v", msg)
}
got, gotErr := NfParseWithOpts(attr, &NfParseOpts{Policy: policy})
wantErr := want == nil
if wantErr && gotErr == nil {
t.Fatalf("NfParseWithOpts() failed, want error, got nil error")
}
if gotErr != nil {
return
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("NfParseWithOpts() returned diff (-want +got): %v", diff)
}
})
}
}
func TestAttrNetToHost(t *testing.T) {
const attr uint16 = 1
tests := []struct {
name string
attrs map[uint16]nlmsg.BytesView
wantOK bool
want any
}{
{
name: "uint8",
attrs: map[uint16]nlmsg.BytesView{attr: nlmsg.BytesView([]byte{0x01})},
wantOK: true,
want: any(uint8(0x01)),
},
{
name: "uint16",
attrs: map[uint16]nlmsg.BytesView{attr: nlmsg.BytesView([]byte{0x01, 0x02})},
wantOK: true,
want: any(uint16(0x0102)),
},
{
name: "uint32",
attrs: map[uint16]nlmsg.BytesView{attr: nlmsg.BytesView([]byte{0x01, 0x02, 0x03, 0x04})},
wantOK: true,
want: any(uint32(0x01020304)),
},
{
name: "uint64",
attrs: map[uint16]nlmsg.BytesView{attr: nlmsg.BytesView([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08})},
wantOK: true,
want: any(uint64(0x0102030405060708)),
},
{
name: "empty",
attrs: map[uint16]nlmsg.BytesView{},
wantOK: false,
},
{
name: "invalidUint8",
attrs: map[uint16]nlmsg.BytesView{attr: nlmsg.BytesView([]byte{})},
wantOK: false,
want: any(uint8(0)),
},
{
name: "invalidUint16",
attrs: map[uint16]nlmsg.BytesView{attr: nlmsg.BytesView([]byte{0x01})},
wantOK: false,
want: any(uint16(0)),
},
{
name: "invalidUint32",
attrs: map[uint16]nlmsg.BytesView{attr: nlmsg.BytesView([]byte{0x01})},
wantOK: false,
want: any(uint32(0)),
},
{
name: "invalidUint64",
attrs: map[uint16]nlmsg.BytesView{attr: nlmsg.BytesView([]byte{0x01})},
wantOK: false,
want: any(uint64(0)),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var got any
var gotOk bool
switch test.want.(type) {
case uint8:
got, gotOk = AttrNetToHost[uint8](attr, test.attrs)
case uint16:
got, gotOk = AttrNetToHost[uint16](attr, test.attrs)
case uint32:
got, gotOk = AttrNetToHost[uint32](attr, test.attrs)
case uint64:
got, gotOk = AttrNetToHost[uint64](attr, test.attrs)
}
if gotOk != test.wantOK {
t.Errorf("AttrNetToHost: got ok %v, want ok %v", gotOk, test.wantOK)
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("AttrNetToHost: got 0x%x, want 0x%x", got, test.want)
}
})
}
}
func TestDumpOperations(t *testing.T) {
tests := []struct {
name string
op operation
validate func([]byte) error
}{
{
name: "immediate",
op: mustCreateImmediate(t, linux.NFT_REG_1 /*dreg*/, []byte{1, 2, 3, 4} /*data*/, Verdict{}),
validate: func(dump []byte) error {
attrs, ok := NfParse(dump)
if !ok {
return fmt.Errorf("failed to parse dumped attributes")
}
reg, ok := AttrNetToHost[uint32](linux.NFTA_IMMEDIATE_DREG, attrs)
if !ok {
return fmt.Errorf("failed to get dreg value")
}
if reg != linux.NFT_REG_1 {
return fmt.Errorf("unexpected dreg value: %d, want %d", reg, linux.NFT_REG_1)
}
dataAttrs, ok := attrs[linux.NFTA_IMMEDIATE_DATA]
if !ok {
return fmt.Errorf("failed to find data attributes")
}
data, ok := NfParse(nlmsg.AttrsView(dataAttrs))
if !ok {
return fmt.Errorf("failed to parse data attributes")
}
dataVal, ok := data[linux.NFTA_DATA_VALUE]
if !ok {
return fmt.Errorf("failed to find data value")
}
if !slices.Equal(dataVal, []byte{1, 2, 3, 4}) {
return fmt.Errorf("unexpected data value: %v, want %v", dataVal, []byte{1, 2, 3, 4})
}
return nil
},
},
{
name: "comparison",
op: mustCreateComparison(t, linux.NFT_REG_2 /*sreg*/, linux.NFT_CMP_LT /*cop*/, []byte{1, 2, 3, 4} /*data*/),
validate: func(dump []byte) error {
attrs, ok := NfParse(dump)
if !ok {
return fmt.Errorf("failed to parse dumped attributes")
}
reg, ok := AttrNetToHost[uint32](linux.NFTA_CMP_SREG, attrs)
if !ok {
return fmt.Errorf("failed to get dreg value")
}
if reg != linux.NFT_REG_2 {
return fmt.Errorf("unexpected dreg value: %d, want %d", reg, linux.NFT_REG_2)
}
cop, ok := AttrNetToHost[uint32](linux.NFTA_CMP_OP, attrs)
if !ok {
return fmt.Errorf("failed to get cop value")
}
if cop != linux.NFT_CMP_LT {
return fmt.Errorf("unexpected cop value: %d, want %d", cop, linux.NFT_CMP_EQ)
}
dataAttrs, ok := attrs[linux.NFTA_CMP_DATA]
if !ok {
return fmt.Errorf("failed to find data attributes")
}
data, ok := NfParse(nlmsg.AttrsView(dataAttrs))
if !ok {
return fmt.Errorf("failed to parse data attributes")
}
dataVal, ok := data[linux.NFTA_DATA_VALUE]
if !ok {
return fmt.Errorf("failed to find data value")
}
if !slices.Equal(dataVal, []byte{1, 2, 3, 4}) {
return fmt.Errorf("unexpected data value: %v, want %v", dataVal, []byte{1, 2, 3, 4})
}
return nil
},
},
{
name: "range",
op: mustCreateRanged(t, linux.NFT_REG_1, linux.NFT_RANGE_EQ, []byte{1}, []byte{2}),
validate: func(dump []byte) error {
// TODO: b/452648112 - Implement validation for ranged operation when dump is implemented.
if dump != nil {
return fmt.Errorf("unexpected dump: %v, want nil", dump)
}
return nil
},
},
{
name: "payloadLoad",
op: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER /*base*/, 4 /*offset*/, 4 /*len*/, linux.NFT_REG_1 /*dreg*/),
validate: func(dump []byte) error {
attrs, ok := NfParse(dump)
if !ok {
return fmt.Errorf("failed to parse dumped attributes")
}
reg, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_DREG, attrs)
if !ok {
return fmt.Errorf("failed to get dreg value")
}
if reg != linux.NFT_REG_1 {
return fmt.Errorf("unexpected dreg value: %d, want %d", reg, linux.NFT_REG_1)
}
base, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_BASE, attrs)
if !ok {
return fmt.Errorf("failed to get base value")
}
if base != linux.NFT_PAYLOAD_NETWORK_HEADER {
return fmt.Errorf("unexpected base value: %d, want %d", base, linux.NFT_PAYLOAD_NETWORK_HEADER)
}
offset, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_OFFSET, attrs)
if !ok {
return fmt.Errorf("failed to get offset value")
}
if offset != 4 {
return fmt.Errorf("unexpected offset value: %d, want %d", offset, 4)
}
len, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_LEN, attrs)
if !ok {
return fmt.Errorf("failed to get len value")
}
if len != 4 {
return fmt.Errorf("unexpected len value: %d, want %d", len, 4)
}
return nil
},
},
{
name: "payloadSet",
op: mustCreatePayloadSet(t, linux.NFT_PAYLOAD_NETWORK_HEADER /*base*/, 0 /*offset*/, 4 /*len*/, linux.NFT_REG_4 /*sreg*/, linux.NFT_PAYLOAD_CSUM_NONE /*csumType*/, 0 /*csumOff*/, 0 /*csumFlags*/),
validate: func(dump []byte) error {
attrs, ok := NfParse(dump)
if !ok {
return fmt.Errorf("failed to parse dumped attributes")
}
reg, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_SREG, attrs)
if !ok {
return fmt.Errorf("failed to get sreg value")
}
if reg != linux.NFT_REG_4 {
return fmt.Errorf("unexpected sreg value: %d, want %d", reg, linux.NFT_REG_4)
}
base, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_BASE, attrs)
if !ok {
return fmt.Errorf("failed to get base value")
}
if base != linux.NFT_PAYLOAD_NETWORK_HEADER {
return fmt.Errorf("unexpected base value: %d, want %d", base, linux.NFT_PAYLOAD_NETWORK_HEADER)
}
offset, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_OFFSET, attrs)
if !ok {
return fmt.Errorf("failed to get offset value")
}
if offset != 0 {
return fmt.Errorf("unexpected offset value: %d, want %d", offset, 0)
}
len, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_LEN, attrs)
if !ok {
return fmt.Errorf("failed to get len value")
}
if len != 4 {
return fmt.Errorf("unexpected len value: %d, want %d", len, 4)
}
csumType, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_CSUM_TYPE, attrs)
if !ok {
return fmt.Errorf("failed to get csum type value")
}
if csumType != linux.NFT_PAYLOAD_CSUM_NONE {
return fmt.Errorf("unexpected csum type value: %d, want %d", csumType, linux.NFT_PAYLOAD_CSUM_NONE)
}
csumOffset, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_CSUM_OFFSET, attrs)
if !ok {
return fmt.Errorf("failed to get csum offset value")
}
if csumOffset != 0 {
return fmt.Errorf("unexpected csum offset value: %d, want %d", csumOffset, 0)
}
csumFlags, ok := AttrNetToHost[uint32](linux.NFTA_PAYLOAD_CSUM_FLAGS, attrs)
if !ok {
return fmt.Errorf("failed to get csum flags value")
}
if csumFlags != 0 {
return fmt.Errorf("unexpected csum flags value: %d, want %d", csumFlags, 0)
}
return nil
},
},
{
name: "bitwise",
op: mustCreateBitwiseBool(t, linux.NFT_REG_1, linux.NFT_REG_2, []byte{0xff}, []byte{0x00}),
validate: func(dump []byte) error {
attrs, ok := NfParse(dump)
if !ok {
return fmt.Errorf("failed to parse dumped attributes")
}
sreg, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_SREG, attrs)
if !ok {
return fmt.Errorf("failed to get sreg")
}
if sreg != linux.NFT_REG_1 {
return fmt.Errorf("unexpected sreg: %d, want %d", sreg, linux.NFT_REG_1)
}
dreg, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_DREG, attrs)
if !ok {
return fmt.Errorf("failed to get dreg")
}
if dreg != linux.NFT_REG_2 {
return fmt.Errorf("unexpected dreg: %d, want %d", dreg, linux.NFT_REG_2)
}
lenAttr, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_LEN, attrs)
if !ok {
return fmt.Errorf("failed to get len")
}
if lenAttr != 1 {
return fmt.Errorf("unexpected len: %d, want %d", lenAttr, 1)
}
bop, ok := AttrNetToHost[uint32](linux.NFTA_BITWISE_OP, attrs)
if !ok {
return fmt.Errorf("failed to get op")
}
if bop != linux.NFT_BITWISE_BOOL {
return fmt.Errorf("unexpected op: %d, want %d", bop, linux.NFT_BITWISE_BOOL)
}
maskAttr, ok := attrs[linux.NFTA_BITWISE_MASK]
if !ok {
return fmt.Errorf("failed to get mask")
}
maskAttrs, ok := NfParse(nlmsg.AttrsView(maskAttr))
if !ok {
return fmt.Errorf("failed to parse mask")
}
maskValue, err := parseDataAttrs(maskAttrs)
if err != nil {
return fmt.Errorf("failed to parse mask data: %v", err)
}
if !bytes.Equal(maskValue, []byte{0xff}) {
return fmt.Errorf("unexpected mask value: %v, want %v", maskValue, []byte{0xff})
}
xorAttr, ok := attrs[linux.NFTA_BITWISE_XOR]
if !ok {
return fmt.Errorf("failed to get xor")
}
xorAttrs, ok := NfParse(nlmsg.AttrsView(xorAttr))
if !ok {
return fmt.Errorf("failed to parse xor")
}
xorValue, err := parseDataAttrs(xorAttrs)
if err != nil {
return fmt.Errorf("failed to parse xor data: %v", err)
}
if !bytes.Equal(xorValue, []byte{0x00}) {
return fmt.Errorf("unexpected xor value: %v, want %v", xorValue, []byte{0x00})
}
return nil
},
},
{
name: "byteorder",
op: mustCreateByteorder(t, linux.NFT_REG_1, linux.NFT_REG_2, linux.NFT_BYTEORDER_NTOH, 4, 4),
validate: func(dump []byte) error {
// TODO: b/452648112 - Implement validation for byteorder operation when dump is implemented.
if dump != nil {
return fmt.Errorf("unexpected dump: %v, want nil", dump)
}
return nil
},
},
{
name: "metaLoad",
op: mustCreateMetaLoad(t, linux.NFT_META_LEN, linux.NFT_REG_1),
validate: func(dump []byte) error {
attrs, ok := NfParse(dump)
if !ok {
return fmt.Errorf("failed to parse dumped attributes")
}
key, ok := AttrNetToHost[uint32](linux.NFTA_META_KEY, attrs)
if !ok {
return fmt.Errorf("failed to get key value")
}
if key != linux.NFT_META_LEN {
return fmt.Errorf("unexpected key value: %d, want %d", key, linux.NFT_META_LEN)
}
reg, ok := AttrNetToHost[uint32](linux.NFTA_META_DREG, attrs)
if !ok {
return fmt.Errorf("failed to get dreg value")
}
if reg != linux.NFT_REG_1 {
return fmt.Errorf("unexpected dreg value: %d, want %d", reg, linux.NFT_REG_1)
}
return nil
},
},
{
name: "metaSet",
op: mustCreateMetaSet(t, linux.NFT_META_PKTTYPE, linux.NFT_REG_1),
validate: func(dump []byte) error {
attrs, ok := NfParse(dump)
if !ok {
return fmt.Errorf("failed to parse dumped attributes")
}
key, ok := AttrNetToHost[uint32](linux.NFTA_META_KEY, attrs)
if !ok {
return fmt.Errorf("failed to get key value")
}
if key != linux.NFT_META_PKTTYPE {
return fmt.Errorf("unexpected key value: %d, want %d", key, linux.NFT_META_PKTTYPE)
}
reg, ok := AttrNetToHost[uint32](linux.NFTA_META_SREG, attrs)
if !ok {
return fmt.Errorf("failed to get sreg value")
}
if reg != linux.NFT_REG_1 {
return fmt.Errorf("unexpected sreg value: %d, want %d", reg, linux.NFT_REG_1)
}
return nil
},
},
{
name: "route",
op: mustCreateRoute(t, linux.NFT_RT_NEXTHOP4, linux.NFT_REG_1),
validate: func(dump []byte) error {
// TODO: b/452648112 - Implement validation for route operation when dump is implemented.
if dump != nil {
return fmt.Errorf("unexpected dump: %v, want nil", dump)
}
return nil
},
},
{
name: "counter",
op: newCounter(10 /*bytes*/, 20 /*packets*/),
validate: func(dump []byte) error {
attrs, ok := NfParse(dump)
if !ok {
return fmt.Errorf("failed to parse dumped attributes")
}
bytes, ok := AttrNetToHost[uint64](linux.NFTA_COUNTER_BYTES, attrs)
if !ok {
return fmt.Errorf("failed to get bytes value")
}
if bytes != 10 {
return fmt.Errorf("unexpected bytes value: %d, want %d", bytes, 10)
}
packets, ok := AttrNetToHost[uint64](linux.NFTA_COUNTER_PACKETS, attrs)
if !ok {
return fmt.Errorf("failed to get packets value")
}
if packets != 20 {
return fmt.Errorf("unexpected packets value: %d, want %d", packets, 20)
}
return nil
},
},
{
name: "lookup",
op: &lookupOp{
set: &nftSet{name: "map_set"},
sregIdx: 0,
invert: true,
fillData: true,
dregIdx: 16,
},
validate: func(dump []byte) error {
attrs, ok := NfParse(dump)
if !ok {
return fmt.Errorf("failed to parse dumped attributes")
}
setName, ok := attrs[linux.NFTA_LOOKUP_SET]
if !ok {
return fmt.Errorf("failed to get set name")
}
if setName.String() != "map_set" {
return fmt.Errorf("unexpected set name: %q, want %q", setName.String(), "map_set")
}
sreg, ok := AttrNetToHost[uint32](linux.NFTA_LOOKUP_SREG, attrs)
if !ok {
return fmt.Errorf("failed to get sreg")
}
if sreg != linux.NFT_REG_1 {
return fmt.Errorf("unexpected sreg: %d, want %d", sreg, linux.NFT_REG_1)
}
dreg, ok := AttrNetToHost[uint32](linux.NFTA_LOOKUP_DREG, attrs)
if !ok {
return fmt.Errorf("failed to get dreg")
}
if dreg != linux.NFT_REG_2 {
return fmt.Errorf("unexpected dreg: %d, want %d", dreg, linux.NFT_REG_2)
}
flags, ok := AttrNetToHost[uint32](linux.NFTA_LOOKUP_FLAGS, attrs)
if !ok {
return fmt.Errorf("failed to get flags")
}
if flags != linux.NFT_LOOKUP_F_INV {
return fmt.Errorf("unexpected flags: %d, want %d", flags, linux.NFT_LOOKUP_F_INV)
}
return nil
},
},
{
name: "last",
op: &last{},
validate: func(dump []byte) error {
// TODO: b/452648112 - Implement validation for last operation when dump is implemented.
if dump != nil {
return fmt.Errorf("unexpected dump: %v, want nil", dump)
}
return nil
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dump, err := test.op.Dump()
if err != nil {
t.Fatalf("unexpected error for Dump(): %v", err)
}
if err := test.validate(dump); err != nil {
t.Errorf("validation failed: %v", err)
}
})
}
}
// TestBaseChainEvalOrder tests that base chains are evaluated in priority order.
func TestBaseChainEvalOrder(t *testing.T) {
nf := newNFTablesStd()
hook := stack.NFOutput
chainNum := 0
createBaseChains := func(t *testing.T, ip stack.AddressFamily, bcType BaseChainType, priority int) *NFTables {
tab, err := nf.AddTable(ip, "test_table_"+stack.AddressFamilyStrings[ip], false)
if err != nil {
t.Fatalf("AddTable failed: %v", err)
}
bcInfo := &BaseChainInfo{
BcType: bcType,
Hook: hook,
Priority: NewIntPriority(priority),
}
_, err = nf.AddChainToTable(tab, fmt.Sprintf("chain_%d", chainNum), bcInfo, "test", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("AddChain failed: %v", err)
}
chainNum++
return nf
}
// Create chains with filter base chains for IP, Inet, and IP6.
nf = createBaseChains(t, stack.IP, BaseChainTypeFilter, 1)
nf = createBaseChains(t, stack.Inet, BaseChainTypeFilter, -100)
nf = createBaseChains(t, stack.IP6, BaseChainTypeFilter, 1)
// Create chains with nat base chains for IP, Inet, and IP6.
nf = createBaseChains(t, stack.IP, BaseChainTypeNat, 10)
nf = createBaseChains(t, stack.Inet, BaseChainTypeNat, -50)
nf = createBaseChains(t, stack.IP6, BaseChainTypeNat, 10)
validateOrder := func(t *testing.T, family stack.AddressFamily, wantNATChains bool, wantLen int) {
baseChains, err := nf.getBaseChainsForEvaluation(family, hook, wantNATChains)
if err != nil {
t.Fatalf("getBaseChainsForEvaluation failed: %v", err)
}
if len(baseChains) != wantLen {
t.Errorf("unexpected number of base chains: %d, want %d", len(baseChains), wantLen)
}
prevPriority := math.MinInt32
for _, bc := range baseChains {
if wantNATChains && bc.baseChainInfo.BcType != BaseChainTypeNat {
t.Fatalf("base chain type: got %v, want nat", bc.baseChainInfo.BcType.String())
} else if !wantNATChains && bc.baseChainInfo.BcType == BaseChainTypeNat {
t.Fatalf("base chain type got nat, want %v", bc.baseChainInfo.BcType.String())
}
p := bc.baseChainInfo.Priority.GetValue()
if p < prevPriority {
t.Fatalf("base chain priority %d is less than previous priority %d", p, prevPriority)
}
prevPriority = p
}
}
wantLen := 2 // ip[4|6] + inet
validateOrder(t, stack.IP, false /*wantNATChains*/, wantLen)
validateOrder(t, stack.IP6, false /*wantNATChains*/, wantLen)
validateOrder(t, stack.IP, true /*wantNATChains*/, wantLen)
validateOrder(t, stack.IP, true /*wantNATChains*/, wantLen)
}
func TestDeepCopyIsolatesOperations(t *testing.T) {
tests := []struct {
name string
op operation
valuesEqual func(op1, op2 operation) bool
}{
{
name: "immediate",
op: &immediate{
data: []byte{1, 2, 3},
},
valuesEqual: func(op1, op2 operation) bool {
// Check that the pointers to the data are different.
if &op1.(*immediate).data[0] == &op2.(*immediate).data[0] {
return false
}
// Check that the data values are equal.
return slices.Equal(op1.(*immediate).data, op2.(*immediate).data)
},
},
{
name: "comparison",
op: &comparison{
data: []byte{1, 2, 3},
},
valuesEqual: func(op1, op2 operation) bool {
// Check that the pointers to the data are different.
if &op1.(*comparison).data[0] == &op2.(*comparison).data[0] {
return false
}
// Check that the data values are equal.
return slices.Equal(op1.(*comparison).data, op2.(*comparison).data)
},
},
{
name: "ranged",
op: &ranged{
low: []byte{1, 2},
high: []byte{3, 4},
},
valuesEqual: func(op1, op2 operation) bool {
// Check that the pointers are different.
if &op1.(*ranged).low[0] == &op2.(*ranged).low[0] ||
&op1.(*ranged).high[0] == &op2.(*ranged).high[0] {
return false
}
// Check that the data values are equal.
return slices.Equal(op1.(*ranged).low, op2.(*ranged).low) &&
slices.Equal(op1.(*ranged).high, op2.(*ranged).high)
},
},
{
name: "bitwise",
op: &bitwise{
mask: []byte{1, 2},
xor: []byte{3, 4},
},
valuesEqual: func(op1, op2 operation) bool {
if &op1.(*bitwise).mask[0] == &op2.(*bitwise).mask[0] ||
&op1.(*bitwise).xor[0] == &op2.(*bitwise).xor[0] {
return false
}
return slices.Equal(op1.(*bitwise).mask, op2.(*bitwise).mask) &&
slices.Equal(op1.(*bitwise).xor, op2.(*bitwise).xor)
},
},
{
name: "counter",
op: newCounter(0, 0),
valuesEqual: func(op1, op2 operation) bool {
return op1.(*counter).packets.Load() == op2.(*counter).packets.Load()
},
},
{
name: "last",
op: &last{},
valuesEqual: func(op1, op2 operation) bool {
return op1.(*last).timestampMS.Load() == op2.(*last).timestampMS.Load()
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
nf := newNFTablesStd()
tab, err := nf.AddTable(stack.IP, "test_table", false)
if err != nil {
t.Fatalf("AddTable failed, err: %v", err)
}
bc, err := nf.AddChainToTable(tab, "test_chain", arbitraryInfoPolicyAccept, "test chain", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("AddChainToTable failed, err: %v", err)
}
rule := &Rule{}
if err := rule.addOperation(test.op); err != nil {
t.Fatalf("addOperation failed, err: %v", err)
}
if err := bc.RegisterRule(rule, -1 /*=index*/); err != nil {
t.Fatalf("RegisterRule failed, err: %v", err)
}
nftCopy := nf.DeepCopy()
origTable := nf.filters[stack.IP].tables["test_table"]
origChain := origTable.chains["test_chain"]
origOp := origChain.rules[0].ops[0]
copyTable := nftCopy.filters[stack.IP].tables["test_table"]
copyChain := copyTable.chains["test_chain"]
copyOp := copyChain.rules[0].ops[0]
if origOp == copyOp {
t.Fatalf("wanted orig != copy, got %p == %p", origOp, copyOp)
}
if !test.valuesEqual(origOp, copyOp) {
t.Fatalf("wanted valuesEqual(orig, copy) == true, got false")
}
})
}
}
func TestGetSet(t *testing.T) {
tabName := "test_table"
setName := "test_set"
tests := []struct {
name string
setAttrs map[uint16]nlmsg.BytesView
getAttrs map[uint16]nlmsg.BytesView
getFlags uint16
expectGetErr *syserr.Error
verifyMsg func(*testing.T, *nlmsg.MessageSet)
}{
{
name: "GetSet",
setAttrs: map[uint16]nlmsg.BytesView{
linux.NFTA_SET_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_NAME: nlmsg.BytesView(setName),
linux.NFTA_SET_ID: nlmsg.BytesView([]byte{0, 0, 0, 1}),
linux.NFTA_SET_KEY_LEN: nlmsg.BytesView([]byte{0, 0, 0, 4}),
linux.NFTA_SET_FLAGS: nlmsg.BytesView([]byte{0, 0, 0, 8}),
linux.NFTA_SET_DATA_TYPE: nlmsg.BytesView([]byte{0xff, 0xff, 0xff, 0x00}),
},
getAttrs: map[uint16]nlmsg.BytesView{
linux.NFTA_SET_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_NAME: nlmsg.BytesView(setName),
},
verifyMsg: func(t *testing.T, ms *nlmsg.MessageSet) {
if ms.Multi {
t.Errorf("Expected Multi to be false for non-DUMP GetSet")
}
if len(ms.Messages) != 1 {
t.Fatalf("Expected 1 message, got %d", len(ms.Messages))
}
attrs, ok := ms.Messages[0].GetData(&linux.NetFilterGenMsg{})
if !ok {
t.Fatalf("Failed to extract NetFilterGenMsg")
}
attrMap, ok := NfParse(attrs)
if !ok {
t.Fatalf("Failed to parse netlink attributes")
}
if string(attrMap[linux.NFTA_SET_TABLE][:len(tabName)]) != tabName {
t.Errorf("Unexpected table name: got %q, want %q", attrMap[linux.NFTA_SET_TABLE], tabName)
}
if string(attrMap[linux.NFTA_SET_NAME][:len(setName)]) != setName {
t.Errorf("Unexpected set name: got %q, want %q", attrMap[linux.NFTA_SET_NAME], setName)
}
},
},
{
name: "GetAllSets",
setAttrs: map[uint16]nlmsg.BytesView{
linux.NFTA_SET_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_NAME: nlmsg.BytesView(setName),
linux.NFTA_SET_ID: nlmsg.BytesView([]byte{0, 0, 0, 1}),
linux.NFTA_SET_KEY_LEN: nlmsg.BytesView([]byte{0, 0, 0, 4}),
linux.NFTA_SET_FLAGS: nlmsg.BytesView([]byte{0, 0, 0, 8}),
linux.NFTA_SET_DATA_TYPE: nlmsg.BytesView([]byte{0xff, 0xff, 0xff, 0x00}),
},
getAttrs: map[uint16]nlmsg.BytesView{},
getFlags: linux.NLM_F_DUMP,
verifyMsg: func(t *testing.T, ms *nlmsg.MessageSet) {
if !ms.Multi {
t.Errorf("Expected Multi to be true for DUMP GetSet")
}
if len(ms.Messages) != 1 {
t.Fatalf("Expected 1 message, got %d", len(ms.Messages))
}
},
},
{
name: "GetNonExistentSet",
setAttrs: map[uint16]nlmsg.BytesView{
linux.NFTA_SET_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_NAME: nlmsg.BytesView(setName),
linux.NFTA_SET_ID: nlmsg.BytesView([]byte{0, 0, 0, 1}),
linux.NFTA_SET_KEY_LEN: nlmsg.BytesView([]byte{0, 0, 0, 4}),
linux.NFTA_SET_FLAGS: nlmsg.BytesView([]byte{0, 0, 0, 8}),
linux.NFTA_SET_DATA_TYPE: nlmsg.BytesView([]byte{0xff, 0xff, 0xff, 0x00}),
},
getAttrs: map[uint16]nlmsg.BytesView{
linux.NFTA_SET_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_NAME: nlmsg.BytesView("BAD_SET"),
},
expectGetErr: syserr.ErrInvalidArgument,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
nf := &NFTables{}
nf.filters[stack.Inet] = &addressFamilyFilter{
family: stack.Inet,
tables: make(map[string]*Table),
}
tab := &Table{
name: tabName,
sets: make(map[string]*nftSet),
setHandles: make(map[uint64]*nftSet),
afFilter: nf.filters[stack.Inet],
}
nf.filters[stack.Inet].tables[tabName] = tab
if tc.setAttrs != nil {
ms := &nlmsg.MessageSet{Multi: false, PortID: 100}
if err := nf.NewSet(tc.setAttrs, stack.Inet, linux.NLM_F_CREATE, ms); err != nil {
t.Fatalf("NewSet failed: %v", err)
}
}
ms := &nlmsg.MessageSet{Multi: false, PortID: 100}
err := nf.GetSet(tc.getAttrs, stack.Inet, tc.getFlags, ms)
if tc.expectGetErr != nil {
if err == nil || err.GetError() != tc.expectGetErr {
t.Fatalf("Expected GetSet error %v, got %v", tc.expectGetErr, err)
}
return
}
if err != nil {
t.Fatalf("GetSet failed: %v", err)
}
if tc.verifyMsg != nil {
tc.verifyMsg(t, ms)
}
})
}
}
type wantSetElem struct {
key []byte
userData []byte
verdict *Verdict
isCatchAll bool
}
func parseDumpedElements(t *testing.T, ms *nlmsg.MessageSet) []wantSetElem {
var parsed []wantSetElem
for _, msg := range ms.Messages {
msgData, ok := msg.GetData(&linux.NetFilterGenMsg{})
if !ok {
continue
}
attrMap, ok := NfParse(msgData)
if !ok {
continue
}
elementsAttr, ok := attrMap[linux.NFTA_SET_ELEM_LIST_ELEMENTS]
if !ok {
continue
}
elementsView := nlmsg.AttrsView(elementsAttr)
for len(elementsView) > 0 {
hdrType, elem, rest, ok := elementsView.ParseFirst()
if !ok {
t.Fatalf("Failed to parse nested element in list")
}
elementsView = rest
if hdrType.Type != linux.NFTA_LIST_ELEM {
continue
}
elemMap, ok := NfParse(nlmsg.AttrsView(elem))
if !ok {
continue
}
var item wantSetElem
flagsAttr, flagsExists := elemMap[linux.NFTA_SET_ELEM_FLAGS]
if flagsExists {
flagVal := binary.BigEndian.Uint32([]byte(flagsAttr))
if uint16(flagVal)&linux.NFT_SET_ELEM_CATCHALL != 0 {
item.isCatchAll = true
}
}
if !item.isCatchAll {
keyAttr, ok := elemMap[linux.NFTA_SET_ELEM_KEY]
if ok {
keyMap, _ := NfParse(nlmsg.AttrsView(keyAttr))
keyData := keyMap[linux.NFTA_DATA_VALUE]
item.key = slices.Clone(keyData)
}
}
userDataAttr, userDataExists := elemMap[linux.NFTA_SET_ELEM_USERDATA]
if userDataExists {
item.userData = slices.Clone(userDataAttr)
}
dataAttr, dataExists := elemMap[linux.NFTA_SET_ELEM_DATA]
if dataExists {
dataMap, _ := NfParse(nlmsg.AttrsView(dataAttr))
verdictData, ok := dataMap[linux.NFTA_DATA_VERDICT]
if ok {
verdictMap, _ := NfParse(nlmsg.AttrsView(verdictData))
verdictCode, ok := AttrNetToHost[uint32](linux.NFTA_VERDICT_CODE, verdictMap)
if ok {
item.verdict = &Verdict{Code: verdictCode}
}
}
}
parsed = append(parsed, item)
}
}
return parsed
}
func TestGetSetElements(t *testing.T) {
tabName := "test_table"
setName := "test_set"
key1 := []byte{192, 168, 1, 1}
keyData1, _ := dumpDataAttr(key1)
verdict1, _ := dumpVerdictDataAttr(Verdict{Code: VC(linux.NF_ACCEPT)})
key2 := []byte{10, 0, 0, 1}
keyData2, _ := dumpDataAttr(key2)
verdict2, _ := dumpVerdictDataAttr(Verdict{Code: VC(linux.NF_DROP)})
userData2 := []byte("my-element-userdata")
// Standard element 1
var qElem1 nlmsg.NestedAttr
qElem1.PutAttr(linux.NFTA_SET_ELEM_KEY, primitive.AsByteSlice(keyData1))
qElem1.PutAttr(linux.NFTA_SET_ELEM_DATA, primitive.AsByteSlice(verdict1))
// Userdata element 2
var qElem2 nlmsg.NestedAttr
qElem2.PutAttr(linux.NFTA_SET_ELEM_KEY, primitive.AsByteSlice(keyData2))
qElem2.PutAttr(linux.NFTA_SET_ELEM_DATA, primitive.AsByteSlice(verdict2))
qElem2.PutAttr(linux.NFTA_SET_ELEM_USERDATA, primitive.AsByteSlice(userData2))
// Catchall element 3
var qElem3 nlmsg.NestedAttr
qElem3.PutAttr(linux.NFTA_SET_ELEM_FLAGS, nlmsg.PutU32(uint32(linux.NFT_SET_ELEM_CATCHALL)))
qElem3.PutAttr(linux.NFTA_SET_ELEM_DATA, primitive.AsByteSlice(verdict1))
tests := []struct {
name string
elemAttrs nlmsg.AttrsView // elements to create via NewSetElements
getAttrs map[uint16]nlmsg.BytesView // elements to get via GetSetElements
getFlags uint16
expectNewElemErr *syserr.Error
expectGetErr *syserr.Error
wanted []wantSetElem
}{
{
name: "GetEmptyElements",
elemAttrs: nil,
getAttrs: map[uint16]nlmsg.BytesView{
linux.NFTA_SET_ELEM_LIST_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_ELEM_LIST_SET: nlmsg.BytesView(setName),
},
getFlags: linux.NLM_F_DUMP,
wanted: nil,
},
{
name: "GetAllElements",
elemAttrs: func() nlmsg.AttrsView {
var list nlmsg.NestedAttr
list.PutAttr(linux.NFTA_LIST_ELEM, primitive.AsByteSlice(qElem1))
list.PutAttr(linux.NFTA_LIST_ELEM, primitive.AsByteSlice(qElem2))
list.PutAttr(linux.NFTA_LIST_ELEM, primitive.AsByteSlice(qElem3))
var attrs nlmsg.NestedAttr
attrs.PutAttrString(linux.NFTA_SET_ELEM_LIST_TABLE, tabName)
attrs.PutAttrString(linux.NFTA_SET_ELEM_LIST_SET, setName)
attrs.PutAttr(linux.NFTA_SET_ELEM_LIST_ELEMENTS, primitive.AsByteSlice(list))
return nlmsg.AttrsView(attrs)
}(),
getAttrs: map[uint16]nlmsg.BytesView{
linux.NFTA_SET_ELEM_LIST_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_ELEM_LIST_SET: nlmsg.BytesView(setName),
},
getFlags: linux.NLM_F_DUMP,
wanted: []wantSetElem{
{
key: key1,
verdict: &Verdict{Code: VC(linux.NF_ACCEPT)},
},
{
key: key2,
userData: userData2,
verdict: &Verdict{Code: VC(linux.NF_DROP)},
},
{
isCatchAll: true,
verdict: &Verdict{Code: VC(linux.NF_ACCEPT)},
},
},
},
{
name: "GetElement",
elemAttrs: func() nlmsg.AttrsView {
var list nlmsg.NestedAttr
list.PutAttr(linux.NFTA_LIST_ELEM, primitive.AsByteSlice(qElem2))
var attrs nlmsg.NestedAttr
attrs.PutAttrString(linux.NFTA_SET_ELEM_LIST_TABLE, tabName)
attrs.PutAttrString(linux.NFTA_SET_ELEM_LIST_SET, setName)
attrs.PutAttr(linux.NFTA_SET_ELEM_LIST_ELEMENTS, primitive.AsByteSlice(list))
return nlmsg.AttrsView(attrs)
}(),
getAttrs: func() map[uint16]nlmsg.BytesView {
var queryElemAttr nlmsg.NestedAttr
queryKeyData, _ := dumpDataAttr(key2)
queryElemAttr.PutAttr(linux.NFTA_SET_ELEM_KEY, primitive.AsByteSlice(queryKeyData))
var queryListAttr nlmsg.NestedAttr
queryListAttr.PutAttr(linux.NFTA_LIST_ELEM, primitive.AsByteSlice(queryElemAttr))
return map[uint16]nlmsg.BytesView{
linux.NFTA_SET_ELEM_LIST_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_ELEM_LIST_SET: nlmsg.BytesView(setName),
linux.NFTA_SET_ELEM_LIST_ELEMENTS: nlmsg.BytesView(queryListAttr),
}
}(),
wanted: []wantSetElem{
{
key: key2,
userData: userData2,
verdict: &Verdict{Code: VC(linux.NF_DROP)},
},
},
},
{
name: "GetElementsFromNonExistentSet",
getAttrs: map[uint16]nlmsg.BytesView{
linux.NFTA_SET_ELEM_LIST_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_ELEM_LIST_SET: nlmsg.BytesView("BAD_SET"),
},
getFlags: linux.NLM_F_DUMP,
expectGetErr: syserr.ErrNoFileOrDir,
},
{
name: "GetNonExistentElement",
getAttrs: func() map[uint16]nlmsg.BytesView {
var queryElemAttr nlmsg.NestedAttr
queryKeyData, _ := dumpDataAttr([]byte{255, 255, 255, 255})
queryElemAttr.PutAttr(linux.NFTA_SET_ELEM_KEY, primitive.AsByteSlice(queryKeyData))
var queryListAttr nlmsg.NestedAttr
queryListAttr.PutAttr(linux.NFTA_LIST_ELEM, primitive.AsByteSlice(queryElemAttr))
return map[uint16]nlmsg.BytesView{
linux.NFTA_SET_ELEM_LIST_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_ELEM_LIST_SET: nlmsg.BytesView(setName),
linux.NFTA_SET_ELEM_LIST_ELEMENTS: nlmsg.BytesView(queryListAttr),
}
}(),
expectGetErr: syserr.ErrNoFileOrDir,
},
{
name: "GetElementsMissingElementsAttr",
getAttrs: map[uint16]nlmsg.BytesView{
linux.NFTA_SET_ELEM_LIST_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_ELEM_LIST_SET: nlmsg.BytesView(setName),
},
expectGetErr: syserr.ErrInvalidArgument,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
nf := &NFTables{}
nf.filters[stack.Inet] = &addressFamilyFilter{
family: stack.Inet,
tables: make(map[string]*Table),
}
tab := &Table{
name: tabName,
sets: make(map[string]*nftSet),
setHandles: make(map[uint64]*nftSet),
afFilter: nf.filters[stack.Inet],
}
nf.filters[stack.Inet].tables[tabName] = tab
// Pre-create the set where elements will be added/queried
newSetAttrs := map[uint16]nlmsg.BytesView{
linux.NFTA_SET_TABLE: nlmsg.BytesView(tabName),
linux.NFTA_SET_NAME: nlmsg.BytesView(setName),
linux.NFTA_SET_ID: nlmsg.BytesView([]byte{0, 0, 0, 1}),
linux.NFTA_SET_KEY_LEN: nlmsg.BytesView([]byte{0, 0, 0, 4}),
linux.NFTA_SET_FLAGS: nlmsg.BytesView([]byte{0, 0, 0, 8}),
linux.NFTA_SET_DATA_TYPE: nlmsg.BytesView([]byte{0xff, 0xff, 0xff, 0x00}),
}
if err := nf.NewSet(newSetAttrs, stack.Inet, linux.NLM_F_CREATE, &nlmsg.MessageSet{}); err != nil {
t.Fatalf("Setup NewSet failed: %v", err)
}
if tc.elemAttrs != nil {
ms := &nlmsg.MessageSet{Multi: false, PortID: 100}
err := nf.NewSetElements(tc.elemAttrs, stack.Inet, 0, ms)
if tc.expectNewElemErr != nil {
if err == nil || err.GetError() != tc.expectNewElemErr {
t.Fatalf("Expected NewSetElements error %v, got %v", tc.expectNewElemErr, err)
}
return
}
if err != nil {
t.Fatalf("NewSetElements failed: %v", err)
}
}
ms := &nlmsg.MessageSet{Multi: false, PortID: 100}
err := nf.GetSetElements(tc.getAttrs, stack.Inet, tc.getFlags, ms)
if tc.expectGetErr != nil {
if err == nil || err.GetError() != tc.expectGetErr {
t.Fatalf("Expected GetSetElements error %v, got %v", tc.expectGetErr, err)
}
return
}
if err != nil {
t.Fatalf("GetSetElements failed: %v", err)
}
// Validate returned elements against expected elements list
parsed := parseDumpedElements(t, ms)
if diff := cmp.Diff(tc.wanted, parsed, cmp.AllowUnexported(wantSetElem{})); diff != "" {
t.Errorf("GetSetElements returned unexpected elements diff (-want +got):\n%s", diff)
}
})
}
}
func TestSetMapRemoveElement(t *testing.T) {
key1 := []byte{192, 168, 1, 1}
key2 := []byte{10, 0, 0, 1}
key3 := []byte{172, 16, 0, 1}
keyNone := []byte{1, 2, 3, 4}
testCases := []struct {
name string
initialElems [][]byte
keyToRemove []byte
wantErr *syserr.Error
wantElems [][]byte
wantBackendMap map[string]int
}{
{
name: "delete first element",
initialElems: [][]byte{key1, key2, key3},
keyToRemove: key1,
wantElems: [][]byte{key3, key2},
wantBackendMap: map[string]int{
string(key3): 0,
string(key2): 1,
},
},
{
name: "delete middle element",
initialElems: [][]byte{key1, key2, key3},
keyToRemove: key2,
wantElems: [][]byte{key1, key3},
wantBackendMap: map[string]int{
string(key1): 0,
string(key3): 1,
},
},
{
name: "delete last element",
initialElems: [][]byte{key1, key2, key3},
keyToRemove: key3,
wantElems: [][]byte{key1, key2},
wantBackendMap: map[string]int{
string(key1): 0,
string(key2): 1,
},
},
{
name: "delete non-existent element",
initialElems: [][]byte{key1, key2, key3},
keyToRemove: keyNone,
wantErr: syserr.ErrNoFileOrDir,
wantElems: [][]byte{key1, key2, key3},
wantBackendMap: map[string]int{
string(key1): 0,
string(key2): 1,
string(key3): 2,
},
},
{
name: "delete from single element set",
initialElems: [][]byte{key1},
keyToRemove: key1,
wantBackendMap: map[string]int{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
set := &nftSet{
name: "test_set",
keyLen: 4,
backend: &setMapBackend{
m: make(map[string]int),
},
}
for i, k := range tc.initialElems {
set.elements = append(set.elements, nftSetElem{startKey: k})
set.backend.(*setMapBackend).m[string(k)] = i
}
err := set.removeElement(tc.keyToRemove, nil)
if tc.wantErr != nil {
if err == nil || err.GetError() != tc.wantErr {
t.Errorf("Expected error %v, got %v", tc.wantErr, err)
}
return
}
if err != nil {
t.Fatalf("removeElement failed: %v", err)
}
var gotElems [][]byte
for _, e := range set.elements {
gotElems = append(gotElems, e.startKey)
}
if diff := cmp.Diff(tc.wantElems, gotElems); diff != "" {
t.Errorf("Elements mismatch (-want +got):\n%s", diff)
}
backend := set.backend.(*setMapBackend)
if diff := cmp.Diff(tc.wantBackendMap, backend.m); diff != "" {
t.Errorf("Backend mapping mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestPruneUnused(t *testing.T) {
nft := newNFTablesStd()
tab, err := nft.AddTable(arbitraryFamily, "test_table", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nft.AddChainToTable(tab, "base_chain", arbitraryInfoPolicyAccept, "base", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
// Create an anonymous binding chain.
bindingChain, err := nft.AddChainToTable(tab, "anon_chain", nil, "anon", false, linux.NFT_CHAIN_BINDING, nil, 0)
if err != nil {
t.Fatalf("unexpected error for AddChain binding: %v", err)
}
// Add rule jumping to bindingChain.
rule := &Rule{}
jumpOp := mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_JUMP), Chain: bindingChain})
if err := rule.addOperation(jumpOp); err != nil {
t.Fatalf("unexpected error for addOperation: %v", err)
}
bindingChain.IncrementChainUse()
if err := bc.RegisterRule(rule, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule: %v", err)
}
if tab.ChainCount() != 2 {
t.Fatalf("expected 2 chains in table, got %d", tab.ChainCount())
}
// Delete rule jumping to bindingChain.
if err := bc.DeleteRule(rule); err != nil {
t.Fatalf("unexpected error for DeleteRule: %v", err)
}
// Sweep unbound binding chains at top level.
nft.PruneUnused()
// Verify that bindingChain was swept from tab.
if tab.ChainCount() != 1 {
t.Fatalf("expected 1 chain after sweep, got %d", tab.ChainCount())
}
if _, err := tab.GetChain("anon_chain"); err == nil {
t.Fatalf("expected anon_chain to be swept and removed from table")
}
}
func TestDestroyTable(t *testing.T) {
nf := newNFTablesStd()
tab, err := nf.AddTable(arbitraryFamily, "test_table", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "base", arbitraryInfoPolicyAccept, "base", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
// Add a target chain and rule jumping to it.
targetChain, err := nf.AddChainToTable(tab, "target", nil, "target", false, 0, nil, 0)
if err != nil {
t.Fatalf("unexpected error for AddChain target: %v", err)
}
rule := &Rule{}
op := mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NFT_JUMP), Chain: targetChain})
if err := rule.addOperation(op); err != nil {
t.Fatalf("unexpected error for addOperation: %v", err)
}
// Add a set with an element jumping to targetChain, and bind a lookupOp to it.
set := &nftSet{
name: "test_set",
keyLen: 4,
backend: &setMapBackend{
m: make(map[string]int),
},
}
if err := nf.addSetToTable(tab, set); err != nil {
t.Fatalf("unexpected error for addSetToTable: %v", err)
}
setElem := nftSetElem{
startKey: []byte{1, 2, 3, 4},
data: dataOrVerdict{
isVerdict: true,
verdict: Verdict{Code: VC(linux.NFT_JUMP), Chain: targetChain},
},
}
set.elements = append(set.elements, setElem)
targetChain.IncrementChainUse()
lookup := &lookupOp{
set: set,
sregIdx: 0,
}
set.bindings = append(set.bindings, lookup)
if err := rule.addOperation(lookup); err != nil {
t.Fatalf("unexpected error for addOperation lookup: %v", err)
}
targetChain.IncrementChainUse()
if err := bc.RegisterRule(rule, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule: %v", err)
}
afFilter := tab.afFilter
tab.destroy()
// 1. Verify the table was cleanly reset back to an empty shell.
if diff := cmp.Diff(&Table{}, tab, cmp.AllowUnexported(Table{}, atomicbitops.Uint64{})); diff != "" {
t.Errorf("Table differs from expected after destroy (-want +got):\n%s", diff)
}
if len(afFilter.hfStacks) != 0 {
t.Errorf("expected afFilter.hfStacks to be empty after destroy")
}
// 2. Verify the base chain was destroyed.
if diff := cmp.Diff(&Chain{}, bc, cmp.AllowUnexported(Chain{})); diff != "" {
t.Errorf("Chain differs from expected after destroy (-want +got):\n%s", diff)
}
// 3. Verify the target chain was destroyed and its chainUse decremented to 0.
if diff := cmp.Diff(&Chain{}, targetChain, cmp.AllowUnexported(Chain{})); diff != "" {
t.Errorf("Target chain differs from expected after destroy (-want +got):\n%s", diff)
}
// 4. Verify the rule was destroyed.
if diff := cmp.Diff(&Rule{}, rule, cmp.AllowUnexported(Rule{})); diff != "" {
t.Errorf("Rule differs from expected after destroy (-want +got):\n%s", diff)
}
// 5. Verify the set was destroyed and its element-level chainUse decremented to 0.
if diff := cmp.Diff(&nftSet{dead: 1}, set, cmp.AllowUnexported(nftSet{}, setMapBackend{})); diff != "" {
t.Errorf("Set differs from expected after destroy (-want +got):\n%s", diff)
}
}
// runWriter keeps updating the NFTables ruleset.
func runWriter(t *testing.T, stk *stack.Stack, writerID, iterations int, wg *sync.WaitGroup) {
defer wg.Done()
for j := 0; j < iterations; j++ {
func() {
stk.LockNFTablesUpdate()
defer stk.UnlockNFTablesUpdate()
current := stk.NFTables().(*NFTables)
copyNFT := current.DeepCopy()
copyTab, err := copyNFT.GetTable(stack.IP, "test_table", 0)
if err != nil {
t.Errorf("writer %d failed to get table: %v", writerID, err)
return
}
copyChain, err := copyTab.GetChain("test_chain")
if err != nil {
t.Errorf("writer %d failed to get chain: %v", writerID, err)
return
}
newRule := &Rule{}
newOp := mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)})
if err := newRule.addOperation(newOp); err != nil {
t.Errorf("writer %d addOperation failed: %v", writerID, err)
return
}
if err := copyChain.RegisterRule(newRule, -1); err != nil {
t.Errorf("writer %d RegisterRule failed: %v", writerID, err)
return
}
stk.SetNFTables(copyNFT)
}()
}
}
// runReader repeatedly performs packet evaluations.
func runReader(t *testing.T, stk *stack.Stack, readIterations int, wg *sync.WaitGroup) {
defer wg.Done()
pkt := makeArbitraryIPv4Packet()
for j := 0; j < readIterations; j++ {
activeNFT := stk.NFTables()
if activeNFT == nil {
t.Errorf("expected non-nil NFTables from stack")
return
}
for jj := 0; jj < 1000; jj++ {
// Evaluate NFTables ruleset.
if !activeNFT.CheckInput(pkt, nil, stack.IP) {
t.Errorf("expected CheckInput to return true (accept)")
return
}
}
}
}
// TestNFTablesConcurrentUpdateAndEvaluate tests the concurrent update and
// packet processing path.
func TestNFTablesConcurrentUpdateAndEvaluate(t *testing.T) {
// Create a new stack and NFTables.
stk := stack.New(stack.Options{})
fakeClock := faketime.NewManualClock()
fixedRNG := rand.RNGFrom(&fixedReader{})
nf := NewNFTables(stk, fakeClock, fixedRNG)
tab, err := nf.AddTable(stack.IP, "test_table", false)
if err != nil {
t.Fatalf("unexpected error for AddTable: %v", err)
}
bc, err := nf.AddChainToTable(tab, "test_chain", arbitraryInfoPolicyAccept, "test", false, 0, nil, linux.NF_ACCEPT)
if err != nil {
t.Fatalf("unexpected error for AddChain: %v", err)
}
// Add an initial immediate rule to the chain.
rule := &Rule{}
op := mustCreateImmediate(t, linux.NFT_REG_VERDICT, nil, Verdict{Code: VC(linux.NF_ACCEPT)})
if err := rule.addOperation(op); err != nil {
t.Fatalf("unexpected error for addOperation: %v", err)
}
if err := bc.RegisterRule(rule, -1); err != nil {
t.Fatalf("unexpected error for RegisterRule: %v", err)
}
stk.SetNFTables(nf)
const numWriters = 2
const numReaders = 4
const iterations = 50
const readIterations = iterations * 4
var wg sync.WaitGroup
// Writers: serialize updates per-stack using LockNFTablesUpdate().
for i := 0; i < numWriters; i++ {
wg.Add(1)
go runWriter(t, stk, i, iterations, &wg)
}
// Readers: concurrently perform packet evaluations.
for i := 0; i < numReaders; i++ {
wg.Add(1)
go runReader(t, stk, readIterations, &wg)
}
wg.Wait()
// Verify that all serialized updates were persisted in the active NFTables object.
finalNFT := stk.NFTables().(*NFTables)
finalTab, err := finalNFT.GetTable(stack.IP, "test_table", 0)
if err != nil {
t.Fatalf("failed to get table from final NFTables: %v", err)
}
finalChain, err := finalTab.GetChain("test_chain")
if err != nil {
t.Fatalf("failed to get chain from final NFTables: %v", err)
}
// Initial rule (1) + numWriters * iterations rules added.
expectedRules := 1 + numWriters*iterations
if len(finalChain.GetRules()) != expectedRules {
t.Fatalf("expected %d rules in final chain, got %d", expectedRules, len(finalChain.GetRules()))
}
}
// Tests compat operation deepCopy functionality.
func TestCompatOperationDeepCopy(t *testing.T) {
for _, tc := range []struct {
name string
op operation
opts cmp.Option
}{
{
name: "conntrack match",
op: &compatCTMatch{
revision: 1,
info: ctMatchInfo{
checkState: true,
stateMask: linux.XT_CONNTRACK_STATE_ESTABLISHED,
},
infoData: []byte{1, 2, 3, 4},
},
opts: cmp.AllowUnexported(compatCTMatch{}, ctMatchInfo{}),
},
{
name: "addrtype match",
op: &compatAddrtypeMatch{
revision: 1,
infoData: []byte{1, 2, 3, 4},
info: addrTypeMatchInfo{
checkSrc: true,
checkDst: true,
invertSrcMatch: true,
invertDstMatch: true,
limitIfaceIn: true,
limitIfaceOut: true,
sourceMask: linux.XT_ADDRTYPE_LOCAL,
destMask: linux.XT_ADDRTYPE_BROADCAST,
},
},
opts: cmp.AllowUnexported(compatAddrtypeMatch{}, addrTypeMatchInfo{}),
},
{
name: "masquerade target",
op: &compatMASQTarget{
revision: 0,
info: masqTargetInfo{
netProto: header.IPv4ProtocolNumber,
hasPortRange: true,
portsOrIdents: stack.PortOrIdentRange{
Start: 1024,
Size: 100,
},
},
infoData: []byte{1, 2, 3},
},
opts: cmp.AllowUnexported(compatMASQTarget{}, masqTargetInfo{}),
},
{
name: "nat target",
op: &compatNATTarget{
name: TargetSNAT,
revision: 1,
info: natTargetInfo{
netProto: header.IPv4ProtocolNumber,
natType: stack.SNAT,
address: tcpip.AddrFrom4([4]byte{192, 168, 1, 1}),
changeAddress: true,
changePort: true,
portsOrIdents: stack.PortOrIdentRange{
Start: 1024,
Size: 100,
},
},
infoData: []byte{1, 2, 3},
},
opts: cmp.AllowUnexported(compatNATTarget{}, natTargetInfo{}),
},
{
name: "noop match",
op: &compatNoopMatch{
name: "tcp",
revision: 0,
infoData: []byte{1, 2, 3, 4},
},
opts: cmp.AllowUnexported(compatNoopMatch{}),
},
} {
t.Run(tc.name, func(t *testing.T) {
copied := tc.op.deepCopy()
if diff := cmp.Diff(tc.op, copied, tc.opts); diff != "" {
t.Fatalf("unexpected diff after copy (-want +got):\n%s", diff)
}
})
}
}
// Tests compat operation checkCompatibility functionality.
func TestCompatOpCompatibility(t *testing.T) {
addrtypeIn := &compatAddrtypeMatch{info: addrTypeMatchInfo{limitIfaceIn: true}}
addrtypeOut := &compatAddrtypeMatch{info: addrTypeMatchInfo{limitIfaceOut: true}}
masq := &compatMASQTarget{}
snat := &compatNATTarget{info: natTargetInfo{natType: stack.SNAT}}
dnat := &compatNATTarget{info: natTargetInfo{natType: stack.DNAT}}
noop := &compatNoopMatch{name: "tcp"}
const nonBaseChainType = BaseChainType(-1)
makeCtx := func(bcType BaseChainType, hook stack.NFHook) *opCompatCtx {
if bcType == nonBaseChainType {
return &opCompatCtx{chain: &Chain{}}
}
return &opCompatCtx{
chain: &Chain{
baseChainInfo: &BaseChainInfo{
BcType: bcType,
Hook: hook,
},
},
}
}
for _, tc := range []struct {
name string
op operation
cCtx *opCompatCtx
wantErr bool
}{
// Addrtype limitIfaceIn
{
name: "addrtype limitIfaceIn valid on PREROUTING",
op: addrtypeIn,
cCtx: makeCtx(BaseChainTypeFilter, stack.NFPrerouting),
},
{
name: "addrtype limitIfaceIn invalid on POSTROUTING",
op: addrtypeIn,
cCtx: makeCtx(BaseChainTypeFilter, stack.NFPostrouting),
wantErr: true,
},
// Addrtype limitIfaceOut
{
name: "addrtype limitIfaceOut valid on POSTROUTING",
op: addrtypeOut,
cCtx: makeCtx(BaseChainTypeFilter, stack.NFPostrouting),
},
{
name: "addrtype limitIfaceOut invalid on PREROUTING",
op: addrtypeOut,
cCtx: makeCtx(BaseChainTypeFilter, stack.NFPrerouting),
wantErr: true,
},
// MASQUERADE
{
name: "MASQUERADE valid on POSTROUTING NAT",
op: masq,
cCtx: makeCtx(BaseChainTypeNat, stack.NFPostrouting),
},
{
name: "MASQUERADE invalid on PREROUTING NAT",
op: masq,
cCtx: makeCtx(BaseChainTypeNat, stack.NFPrerouting),
wantErr: true,
},
{
name: "MASQUERADE invalid on Filter chain",
op: masq,
cCtx: makeCtx(BaseChainTypeFilter, stack.NFPostrouting),
wantErr: true,
},
// SNAT
{
name: "SNAT valid on POSTROUTING NAT",
op: snat,
cCtx: makeCtx(BaseChainTypeNat, stack.NFPostrouting),
},
{
name: "SNAT invalid on PREROUTING NAT",
op: snat,
cCtx: makeCtx(BaseChainTypeNat, stack.NFPrerouting),
wantErr: true,
},
{
name: "SNAT invalid on Filter chain",
op: snat,
cCtx: makeCtx(BaseChainTypeFilter, stack.NFPostrouting),
wantErr: true,
},
// DNAT
{
name: "DNAT valid on PREROUTING NAT",
op: dnat,
cCtx: makeCtx(BaseChainTypeNat, stack.NFPrerouting),
},
{
name: "DNAT invalid on POSTROUTING NAT",
op: dnat,
cCtx: makeCtx(BaseChainTypeNat, stack.NFPostrouting),
wantErr: true,
},
{
name: "DNAT invalid on Filter chain",
op: dnat,
cCtx: makeCtx(BaseChainTypeFilter, stack.NFPrerouting),
wantErr: true,
},
{
name: "noop match",
op: noop,
cCtx: makeCtx(nonBaseChainType, 0),
wantErr: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := tc.op.checkCompatibility(tc.cCtx)
if (err != nil) != tc.wantErr {
t.Fatalf("checkCompatibility() err = %v, wantErr = %v", err, tc.wantErr)
}
})
}
}
func makeUDPv4Packet(srcAddr, dstAddr tcpip.Address, srcPort, dstPort uint16) *stack.PacketBuffer {
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: header.IPv4MinimumSize + header.UDPMinimumSize,
})
pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber
pkt.TransportProtocolNumber = header.UDPProtocolNumber
udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize))
udpHdr.Encode(&header.UDPFields{
SrcPort: srcPort,
DstPort: dstPort,
Length: header.UDPMinimumSize,
})
ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
ipHdr.Encode(&header.IPv4Fields{
TotalLength: header.IPv4MinimumSize + header.UDPMinimumSize,
Protocol: uint8(header.UDPProtocolNumber),
SrcAddr: srcAddr,
DstAddr: dstAddr,
TTL: 64,
})
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
return pkt
}
// TestCompatCTMatchEvaluation tests evaluation of compatCTMatch with tracked and untracked packets.
func TestCompatCTMatchEvaluation(t *testing.T) {
srcAddr := tcpip.AddrFrom4([4]byte{10, 0, 0, 1})
dstAddr := tcpip.AddrFrom4([4]byte{10, 0, 0, 2})
const srcPort = 1234
const dstPort = 5678
for _, tc := range []struct {
name string
matchInfo ctMatchInfo
isReply bool
untracked bool
wantVerdict uint32
}{
// Untracked packet tests
{
name: "untracked match INVALID state",
matchInfo: ctMatchInfo{
checkState: true,
stateMask: linux.XT_CONNTRACK_STATE_INVALID,
},
untracked: true,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "untracked match INVALID state inverted fails",
matchInfo: ctMatchInfo{
checkState: true,
invertStateMatch: true,
stateMask: linux.XT_CONNTRACK_STATE_INVALID,
},
untracked: true,
wantVerdict: VC(linux.NFT_BREAK),
},
{
name: "untracked fail ESTABLISHED state",
matchInfo: ctMatchInfo{
checkState: true,
stateMask: linux.XT_CONNTRACK_STATE_ESTABLISHED,
},
untracked: true,
wantVerdict: VC(linux.NFT_BREAK),
},
{
name: "untracked match inverted ESTABLISHED state",
matchInfo: ctMatchInfo{
checkState: true,
invertStateMatch: true,
stateMask: linux.XT_CONNTRACK_STATE_ESTABLISHED,
},
untracked: true,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "untracked match INVALID state even with checkDirection set",
matchInfo: ctMatchInfo{
checkState: true,
stateMask: linux.XT_CONNTRACK_STATE_INVALID,
checkDirection: true,
},
untracked: true,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "untracked only direction match fails",
matchInfo: ctMatchInfo{
checkState: false,
checkDirection: true,
},
untracked: true,
wantVerdict: VC(linux.NFT_BREAK),
},
{
name: "untracked packet fails NEW state",
matchInfo: ctMatchInfo{
checkState: true,
stateMask: linux.XT_CONNTRACK_STATE_NEW,
},
untracked: true,
wantVerdict: VC(linux.NFT_BREAK),
},
// Tracked packet tests
{
name: "original packet matches NEW state",
matchInfo: ctMatchInfo{
checkState: true,
stateMask: linux.XT_CONNTRACK_STATE_NEW,
},
isReply: false,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "original packet fails ESTABLISHED state",
matchInfo: ctMatchInfo{
checkState: true,
stateMask: linux.XT_CONNTRACK_STATE_ESTABLISHED,
},
isReply: false,
wantVerdict: VC(linux.NFT_BREAK),
},
{
name: "original packet matches ORIGINAL direction",
matchInfo: ctMatchInfo{
checkDirection: true,
invertDirectionMatch: false,
},
isReply: false,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "original packet fails REPLY direction",
matchInfo: ctMatchInfo{
checkDirection: true,
invertDirectionMatch: true,
},
isReply: false,
wantVerdict: VC(linux.NFT_BREAK),
},
{
name: "reply packet matches ESTABLISHED state",
matchInfo: ctMatchInfo{
checkState: true,
stateMask: linux.XT_CONNTRACK_STATE_ESTABLISHED,
},
isReply: true,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "reply packet fails ORIGINAL direction",
matchInfo: ctMatchInfo{
checkDirection: true,
invertDirectionMatch: false,
},
isReply: true,
wantVerdict: VC(linux.NFT_BREAK),
},
{
name: "reply packet matches REPLY direction",
matchInfo: ctMatchInfo{
checkDirection: true,
invertDirectionMatch: true,
},
isReply: true,
wantVerdict: VC(linux.NFT_CONTINUE),
},
} {
t.Run(tc.name, func(t *testing.T) {
op := &compatCTMatch{
info: tc.matchInfo,
}
regs := &registerSet{verdict: Verdict{Code: VC(linux.NFT_CONTINUE)}}
pkt := makeUDPv4Packet(srcAddr, dstAddr, srcPort, dstPort)
defer pkt.DecRef()
var evalPkt *stack.PacketBuffer
if tc.untracked {
evalPkt = pkt
} else {
nf := newNFTablesStd()
nf.InitConnTrackOnce()
ct := nf.GetConnTrack()
// Track initial packet (creates NEW conntrack entry in Original direction).
ct.GetConnAndUpdatePkt(pkt, false /* skipChecksumValidation */)
if !pkt.FinalizeConnTrack() {
t.Fatalf("failed to finalize conntrack for packet")
}
evalPkt = pkt
if tc.isReply {
// Create reply packet with swapped src/dst.
replyPkt := makeUDPv4Packet(dstAddr, srcAddr, dstPort, srcPort)
defer replyPkt.DecRef()
// Track reply packet.
ct.GetConnAndUpdatePkt(replyPkt, false)
evalPkt = replyPkt
}
}
op.evaluate(regs, opEvalCtx{pkt: evalPkt})
if regs.verdict.Code != tc.wantVerdict {
t.Errorf("evaluate verdict = %d, want %d", regs.verdict.Code, tc.wantVerdict)
}
})
}
}
// Tests evaluating compat addrtype match operation.
func TestCompatAddrtypeMatchEvaluation(t *testing.T) {
for _, tc := range []struct {
name string
matchInfo addrTypeMatchInfo
srcAddr tcpip.Address
dstAddr tcpip.Address
hasRoute bool
noNetHeader bool
wantVerdict uint32
}{
{
name: "source broadcast match",
matchInfo: addrTypeMatchInfo{
checkSrc: true,
sourceMask: linux.XT_ADDRTYPE_BROADCAST,
},
srcAddr: header.IPv4Broadcast,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "inverted source local match",
matchInfo: addrTypeMatchInfo{
checkSrc: true,
invertSrcMatch: true,
sourceMask: linux.XT_ADDRTYPE_LOCAL,
},
srcAddr: header.IPv4Broadcast,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "destination broadcast match",
matchInfo: addrTypeMatchInfo{
checkDst: true,
destMask: linux.XT_ADDRTYPE_BROADCAST,
},
dstAddr: header.IPv4Broadcast,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "source multicast match",
matchInfo: addrTypeMatchInfo{
checkSrc: true,
sourceMask: linux.XT_ADDRTYPE_MULTICAST,
},
srcAddr: tcpip.AddrFrom4([4]byte{224, 0, 0, 1}),
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "destination multicast match",
matchInfo: addrTypeMatchInfo{
checkDst: true,
destMask: linux.XT_ADDRTYPE_MULTICAST,
},
dstAddr: tcpip.AddrFrom4([4]byte{224, 0, 0, 1}),
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "source unicast with route",
matchInfo: addrTypeMatchInfo{
checkSrc: true,
sourceMask: linux.XT_ADDRTYPE_UNICAST,
},
srcAddr: tcpip.AddrFrom4([4]byte{10, 0, 0, 1}),
hasRoute: true,
wantVerdict: VC(linux.NFT_CONTINUE),
},
{
name: "source unicast without route fails",
matchInfo: addrTypeMatchInfo{
checkSrc: true,
sourceMask: linux.XT_ADDRTYPE_UNICAST,
},
srcAddr: tcpip.AddrFrom4([4]byte{10, 0, 0, 1}),
hasRoute: false,
wantVerdict: VC(linux.NFT_BREAK),
},
{
name: "destination mis-match",
matchInfo: addrTypeMatchInfo{
checkSrc: true,
sourceMask: linux.XT_ADDRTYPE_BROADCAST,
checkDst: true,
destMask: linux.XT_ADDRTYPE_LOCAL,
},
srcAddr: header.IPv4Broadcast,
dstAddr: tcpip.AddrFrom4([4]byte{224, 0, 0, 1}),
wantVerdict: VC(linux.NFT_BREAK),
},
{
name: "packet without network header breaks",
matchInfo: addrTypeMatchInfo{
checkSrc: true,
sourceMask: linux.XT_ADDRTYPE_BROADCAST,
},
noNetHeader: true,
wantVerdict: VC(linux.NFT_BREAK),
},
} {
t.Run(tc.name, func(t *testing.T) {
op := &compatAddrtypeMatch{
info: tc.matchInfo,
}
regs := &registerSet{verdict: Verdict{Code: VC(linux.NFT_CONTINUE)}}
var pkt *stack.PacketBuffer
if tc.noNetHeader {
pkt = stack.NewPacketBuffer(stack.PacketBufferOptions{})
} else {
fields := arbitraryIPv4Fields()
if tc.srcAddr.Len() > 0 {
fields.SrcAddr = tc.srcAddr
}
if tc.dstAddr.Len() > 0 {
fields.DstAddr = tc.dstAddr
}
pkt = makeIPv4Packet(header.IPv4MinimumSize, fields)
}
defer pkt.DecRef()
stk := stack.New(stack.Options{})
nf := newNFTablesStd()
nf.stack = stk
var route *stack.Route
if tc.hasRoute {
route = &stack.Route{}
}
op.evaluate(regs, opEvalCtx{
pkt: pkt,
route: route,
nftState: nf,
})
if regs.verdict.Code != tc.wantVerdict {
t.Errorf("evaluate verdict = %d, want %d", regs.verdict.Code, tc.wantVerdict)
}
})
}
}
// TestCompatMASQTargetEvaluation tests evaluation of compatMASQTarget,
// verifying that packets whose network protocol doesn't match are passed through.
func TestCompatMASQTargetEvaluation(t *testing.T) {
t.Run("protocol mismatch skips masquerade", func(t *testing.T) {
op := &compatMASQTarget{
info: masqTargetInfo{
netProto: header.IPv6ProtocolNumber,
},
}
regs := &registerSet{verdict: Verdict{Code: VC(linux.NFT_CONTINUE)}}
pkt := makeArbitraryIPv4Packet()
defer pkt.DecRef()
op.evaluate(regs, opEvalCtx{pkt: pkt})
if regs.verdict.Code != VC(linux.NFT_CONTINUE) {
t.Errorf("expected verdict to remain NFT_CONTINUE on protocol mismatch, got %d", regs.verdict.Code)
}
})
}
// TestCompatNATTargetEvaluation tests evaluation of compatNATTarget.
func TestCompatNATTargetEvaluation(t *testing.T) {
t.Run("protocol mismatch skips NAT", func(t *testing.T) {
op := &compatNATTarget{
info: natTargetInfo{
netProto: header.IPv6ProtocolNumber,
natType: stack.SNAT,
},
}
regs := &registerSet{verdict: Verdict{Code: VC(linux.NFT_CONTINUE)}}
pkt := makeArbitraryIPv4Packet()
defer pkt.DecRef()
op.evaluate(regs, opEvalCtx{pkt: pkt})
if regs.verdict.Code != VC(linux.NFT_CONTINUE) {
t.Errorf("expected verdict to remain NFT_CONTINUE on protocol mismatch, got %d", regs.verdict.Code)
}
})
}
// Tests that pass-through no-op compat matches (like tcp/udp) return NFT_CONTINUE.
func TestCompatNoopMatchEvaluation(t *testing.T) {
op := &compatNoopMatch{
name: "tcp",
revision: 0,
infoData: []byte{1, 2, 3, 4},
}
regs := &registerSet{verdict: Verdict{Code: VC(linux.NF_DROP)}}
pkt := makeArbitraryIPv4Packet()
defer pkt.DecRef()
op.evaluate(regs, opEvalCtx{pkt: pkt})
if regs.verdict.Code != VC(linux.NFT_CONTINUE) {
t.Errorf("evaluate verdict = %d, want %d", regs.verdict.Code, VC(linux.NFT_CONTINUE))
}
}