From 33dc9383dca568c3d056049ae4586fedd4f8a928 Mon Sep 17 00:00:00 2001 From: Jayden Nyamiaka Date: Tue, 27 Aug 2024 17:21:21 -0700 Subject: [PATCH] Implement PayloadLoad operation (parsing, interpretation, evaluation, tests). Tests payload load evaluation for all basic fields of IP, IPv6, & TCP headers. These are the main headers that are encountered for gVisor, so their implementation was prioritized. Loading for other packet headers should work as expected since raw/general payload loading works, but it has not been explicitly tested. These tests should be added later and a TODO has been left noting this. PiperOrigin-RevId: 668202252 --- pkg/tcpip/nftables/BUILD | 3 + pkg/tcpip/nftables/nftables.go | 119 +++++- pkg/tcpip/nftables/nftables_test.go | 545 ++++++++++++++++++++++++++- pkg/tcpip/nftables/nftinterp.go | 155 +++++++- pkg/tcpip/nftables/nftinterp_test.go | 124 +++++- 5 files changed, 913 insertions(+), 33 deletions(-) diff --git a/pkg/tcpip/nftables/BUILD b/pkg/tcpip/nftables/BUILD index 46dcf7a6b..6ec1ad4d1 100644 --- a/pkg/tcpip/nftables/BUILD +++ b/pkg/tcpip/nftables/BUILD @@ -13,6 +13,7 @@ go_library( ], deps = [ "//pkg/abi/linux", + "//pkg/tcpip/header", "//pkg/tcpip/stack", ], ) @@ -27,6 +28,8 @@ go_test( deps = [ "//pkg/abi/linux", "//pkg/buffer", + "//pkg/tcpip", + "//pkg/tcpip/header", "//pkg/tcpip/stack", ], ) diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index 54cea7a4d..d2d7a1aab 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -33,6 +33,8 @@ // returns the verdict issued by the ruleset and the packet modified by the // ruleset (if the verdict is not Drop). // +// Inner Headers and Tunneling Headers are not supported. +// // Finally, note that error checking for parameters/inputs is only guaranteed // for public functions. Most private functions are assumed to have // valid/prechecked inputs. @@ -43,6 +45,7 @@ import ( "slices" "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/stack" ) @@ -563,6 +566,7 @@ type operation interface { var ( _ operation = (*immediate)(nil) _ operation = (*comparison)(nil) + _ operation = (*payloadLoad)(nil) ) // immediate is an operation that sets the data in a register. @@ -654,7 +658,7 @@ func (op comparison) evaluate(regs *registerSet, pkt *stack.PacketBuffer) { panic("comparison operation data is not BytesData") } // Gets the data from the source register. - regBuf := bytesData.getRegisterBuffer(regs, op.sreg) + regBuf := getRegisterBuffer(regs, op.sreg) // Compares bytes from left to right for all bytes in the comparison data. dif := 0 @@ -688,6 +692,104 @@ func (op comparison) evaluate(regs *registerSet, pkt *stack.PacketBuffer) { } } +// payloadLoad is an operation that loads data from the packet payload into a +// register. +// Note: payload operations are not supported for the verdict register. +type payloadLoad struct { + base payloadBase // Payload base to access data from. + offset uint8 // Number of bytes to skip after the base. + blen uint8 // Number of bytes to load. + dreg uint8 // Number of the destination register. +} + +// payloadBase is the header that determines the location of the packet data. +// Note: corresponds to enum nft_payload_bases from +// include/uapi/linux/netfilter/nf_tables.h and uses the same constants. +type payloadBase int + +// String for NftPayloadBase returns the string representation of the payload +// base. +func (base payloadBase) String() string { + switch base { + case linux.NFT_PAYLOAD_LL_HEADER: + return "Link Layer Header" + case linux.NFT_PAYLOAD_NETWORK_HEADER: + return "Network Header" + case linux.NFT_PAYLOAD_TRANSPORT_HEADER: + return "Transport Header" + case linux.NFT_PAYLOAD_INNER_HEADER: + panic("inner header not supported") + case linux.NFT_PAYLOAD_TUN_HEADER: + panic("tunneling header not supported") + default: + panic(fmt.Sprintf("invalid payload base: %d", int(base))) + } +} + +// validatePayloadBase ensures the payload base is valid. +func validatePayloadBase(base payloadBase) error { + switch base { + case linux.NFT_PAYLOAD_LL_HEADER, linux.NFT_PAYLOAD_NETWORK_HEADER, linux.NFT_PAYLOAD_TRANSPORT_HEADER: + return nil + case linux.NFT_PAYLOAD_INNER_HEADER: + return fmt.Errorf("inner header not supported") + case linux.NFT_PAYLOAD_TUN_HEADER: + return fmt.Errorf("tunneling header not supported") + default: + return fmt.Errorf("invalid payload base: %d", int(base)) + } +} + +// newPayloadLoad creates a new PayloadLoad operation. +func newPayloadLoad(base payloadBase, offset, blen, dreg uint8) (*payloadLoad, error) { + if dreg == linux.NFT_REG_VERDICT { + return nil, fmt.Errorf("payload load operation cannot use verdict register as destination") + } + if blen > 16 || (blen > 4 && is4ByteRegister(dreg)) { + return nil, fmt.Errorf("payload length %d is too long for destination register %d", blen, dreg) + } + if err := validatePayloadBase(base); err != nil { + return nil, err + } + return &payloadLoad{base: base, offset: offset, blen: blen, dreg: dreg}, nil +} + +// evaluate for PayloadLoad loads data from the packet payload into the +// destination register. +func (op payloadLoad) evaluate(regs *registerSet, pkt *stack.PacketBuffer) { + // Gets the data from the packet payload. + var payload []byte = nil + switch op.base { + case linux.NFT_PAYLOAD_LL_HEADER: + // Note: Assumes Mac Header is present and valid for necessary use cases. + // Also, doesn't check VLAN tag because VLAN isn't supported by gVisor. + payload = pkt.LinkHeader().Slice() + case linux.NFT_PAYLOAD_NETWORK_HEADER: + // No checks done in linux kernel. + payload = pkt.NetworkHeader().Slice() + case linux.NFT_PAYLOAD_TRANSPORT_HEADER: + // Note: Assumes L4 protocol is present and valid for necessary use cases. + + // Errors if the packet is fragmented for IPv4 only. + if net := pkt.NetworkHeader().Slice(); len(net) > 0 && pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber { + if h := header.IPv4(net); h.More() || h.FragmentOffset() != 0 { + break // packet is fragmented + } + } + payload = pkt.TransportHeader().Slice() + } + + // Breaks if could not retrieve packet data. + if payload == nil || len(payload) < int(op.offset+op.blen) { + regs.verdict = Verdict{Code: VC(linux.NFT_BREAK)} + return + } + + // Copies payload data into the specified register. + data := newBytesData(payload[op.offset : op.offset+op.blen]) + data.storeData(regs, op.dreg) +} + // // Register and Register-Related Implementations. // Note: Registers are represented by type uint8 for the register number. @@ -815,11 +917,12 @@ func (rd bytesData) validateRegister(reg uint8) error { return nil } -// getRegisterBuffer is a helper function that gets the appropriate slice of -// register data from the register set. +// getRegisterBuffer is a helper function that gets the appropriate slice of the +// register from the register set. The number of bytes returned is rounded up to +// the nearest 4-byte multiple. // Note: does not support verdict data and assumes the register is valid for the // given data type. -func (rd bytesData) getRegisterBuffer(regs *registerSet, reg uint8) []byte { +func getRegisterBuffer(regs *registerSet, reg uint8) []byte { // Returns the entire 4-byte register if is4ByteRegister(reg) { start := (reg - linux.NFT_REG32_00) * linux.NFT_REG32_SIZE @@ -835,7 +938,7 @@ func (rd bytesData) storeData(regs *registerSet, reg uint8) { if err := rd.validateRegister(reg); err != nil { panic(err) } - copy(rd.getRegisterBuffer(regs, reg), rd.data) + copy(getRegisterBuffer(regs, reg), rd.data) } // registerSet represents the set of registers supported by the kernel. @@ -846,7 +949,7 @@ type registerSet struct { data [registersByteSize]byte // 4 16-byte registers or 16 4-byte registers } -// newRegisterSet creates a new RegisterSet with the Continue Verdict and all +// newRegisterSet creates a new registerSet with the Continue Verdict and all // registers set to 0. func newRegisterSet() registerSet { return registerSet{ @@ -860,6 +963,10 @@ func (regs *registerSet) Verdict() Verdict { return regs.verdict } +func (regs *registerSet) String() string { + return fmt.Sprintf("verdict: %v, data: %x", regs.verdict, regs.data) +} + // // Verdict Implementation. // There are two types of verdicts: diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index 890b80805..d1ebeef59 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -15,19 +15,24 @@ package nftables import ( + "encoding/binary" "fmt" "reflect" "testing" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/stack" ) +// Table Constants. const ( - arbitraryTargetChain string = "target_chain" - arbitraryHook Hook = Prerouting - arbitraryFamily AddressFamily = Inet + arbitraryTargetChain string = "target_chain" + arbitraryHook Hook = Prerouting + arbitraryFamily AddressFamily = Inet + arbitraryReservedHeaderBytes int = 16 ) var ( @@ -38,6 +43,7 @@ var ( } return priority }() + arbitraryInfoPolicyAccept *BaseChainInfo = &BaseChainInfo{ BcType: BaseChainTypeFilter, Hook: arbitraryHook, @@ -45,26 +51,250 @@ var ( } ) -// makeTestingPacket creates an arbitrary packet for testing. -func makeTestingPacket() *stack.PacketBuffer { +// Packet Constants. +const ( + transportProtocol = tcpip.TransportProtocolNumber(6) + + arbitraryHeaderID = 3 + arbitraryTimeToLive = 64 + + // 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 +) + +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 + + arbitraryPort = 12345 + arbitraryPort2 = 80 + tcpSeqNum = 32 + tcpAckNum = 165 + tcpWinSize = 65535 + tcpUrgentPointer = 0 + + arbitraryNonZeroFragmentOffset = 16 +) + +// makeArbitraryGeneralPacket creates an arbitrary packet for testing. +func makeArbitraryGeneralPacket(reserved int) *stack.PacketBuffer { return stack.NewPacketBuffer(stack.PacketBufferOptions{ - ReserveHeaderBytes: 50, + ReserveHeaderBytes: reserved, Payload: buffer.MakeWithData([]byte{0, 2, 4, 8, 16, 32, 64, 128}), }) } +// makeArbitraryEtherPacket creates a packet with an arbitrary ethernet header. +func makeArbitraryEtherPacket(reserved int) *stack.PacketBuffer { + eth := make([]byte, header.EthernetMinimumSize) + header.Ethernet(eth).Encode(&header.EthernetFields{ + SrcAddr: arbitraryLinkAddr, + DstAddr: arbitraryLinkAddr2, + Type: arbitraryEthernetType, + }) + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + ReserveHeaderBytes: reserved, + Payload: buffer.MakeWithData(eth), + }) + pkt.LinkHeader().Consume(header.EthernetMinimumSize) + return pkt +} + +// makeArbitraryIPv4Packet creates a packet with an arbitrary IPv4 header. +func makeArbitraryIPv4Packet(reserved int) *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)) + + // Initializes the IPv4 header with fields. + ipv4Hdr.Encode(&header.IPv4Fields{ + TOS: 0, + TotalLength: uint16(ipv4MinTotalLength), + ID: arbitraryHeaderID, + FragmentOffset: 0, + TTL: arbitraryTimeToLive, + Protocol: uint8(transportProtocol), + Checksum: 0, + SrcAddr: tcpip.AddrFrom4(arbitraryIPv4AddrB), + DstAddr: tcpip.AddrFrom4(arbitraryIPv4AddrB2), + Options: nil, + }) + + // Calculates and sets the checksum. + ipv4Hdr.SetChecksum(^ipv4Hdr.CalculateChecksum()) + + // Sets the network protocol number. + pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber + + return pkt +} + +// makeFragmentedIPv4Packet creates a packet with an arbitrary IPv4 header that +// is fragmented (FragmentOffset != 0). +func makeFragmentedIPv4Packet(reserved int) *stack.PacketBuffer { + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + ReserveHeaderBytes: reserved, + }) + ipv4Hdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize)) + ipv4Hdr.Encode(&header.IPv4Fields{ + TOS: 0, + TotalLength: uint16(ipv4MinTotalLength), + ID: arbitraryHeaderID, + FragmentOffset: uint16(arbitraryNonZeroFragmentOffset), + TTL: arbitraryTimeToLive, + Protocol: uint8(transportProtocol), + Checksum: 0, + SrcAddr: tcpip.AddrFrom4(arbitraryIPv4AddrB), + DstAddr: tcpip.AddrFrom4(arbitraryIPv4AddrB2), + Options: nil, + }) + ipv4Hdr.SetChecksum(^ipv4Hdr.CalculateChecksum()) + pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber + return pkt +} + +// makeArbitraryIPv6Packet creates a packet with an arbitrary IPv6 header. +func makeArbitraryIPv6Packet(reserved int) *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)) + + // Initializes the IPv6 header with fields. + ipv6Hdr.Encode(&header.IPv6Fields{ + TrafficClass: 0, + FlowLabel: 0, + PayloadLength: uint16(ipv6MinPayloadLength), + TransportProtocol: transportProtocol, + HopLimit: arbitraryTimeToLive, + SrcAddr: tcpip.AddrFrom16(arbitraryIPv6AddrB), + DstAddr: tcpip.AddrFrom16(arbitraryIPv6AddrB2), + }) + + // Sets the network protocol number. + pkt.NetworkProtocolNumber = header.IPv6ProtocolNumber + + return pkt +} + +// makeArbitraryIPv4TCPPacket creates a packet with an arbitrary IPv4 and TCP +// header. +func makeArbitraryIPv4TCPPacket(reserved int) *stack.PacketBuffer { + pkt := makeArbitraryIPv4Packet(reserved) + + // Prepends the TCP header to the packet buffer. + tcpHdr := header.TCP(pkt.TransportHeader().Push(header.TCPMinimumSize)) + + // Initializes the TCP header with fields. + tcpHdr.Encode(&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), + }) + + // Calculates the TCP checksum using the pseudo-header and set it in the TCP header. + tcpHdr.SetChecksum(tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum( + header.TCPProtocolNumber, + tcpip.AddrFrom4(arbitraryIPv4AddrB), + tcpip.AddrFrom4(arbitraryIPv4AddrB2), + header.TCPMinimumSize, + ))) + + // Sets the transport protocol number. + pkt.TransportProtocolNumber = header.TCPProtocolNumber + + return pkt +} + // 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 := makeArbitraryGeneralPacket(arbitraryReservedHeaderBytes) nf := NewNFTables() for _, unsupportedFamily := range []AddressFamily{AddressFamily(NumAFs), AddressFamily(-1)} { // Note: the Prerouting hook is arbitrary (any hook would work). - pkt := makeTestingPacket() + pkt := makeArbitraryGeneralPacket(arbitraryReservedHeaderBytes) v, err := nf.EvaluateHook(unsupportedFamily, arbitraryHook, pkt) 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(makeTestingPacket(), pkt), err) + v, packetResultString(cmpPkt, pkt), err) } } } @@ -73,11 +303,13 @@ func TestUnsupportedAddressFamily(t *testing.T) { // 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). + cmpPkt := makeArbitraryGeneralPacket(arbitraryReservedHeaderBytes) for _, family := range []AddressFamily{IP, IP6, Inet, Arp, Bridge, Netdev} { t.Run(family.String()+" address family", func(t *testing.T) { nf := NewNFTables() for _, hook := range []Hook{Prerouting, Input, Forward, Output, Postrouting, Ingress, Egress} { - pkt := makeTestingPacket() + pkt := makeArbitraryGeneralPacket(arbitraryReservedHeaderBytes) v, err := nf.EvaluateHook(family, hook, pkt) supported := false @@ -92,13 +324,13 @@ func TestAcceptAllForSupportedHooks(t *testing.T) { 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(makeTestingPacket(), pkt), err) + 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(makeTestingPacket(), pkt), err) + v, packetResultString(cmpPkt, pkt), err) } } } @@ -283,7 +515,7 @@ func TestEvaluateImmediateVerdict(t *testing.T) { } // Runs evaluation and checks verdict. - pkt := makeTestingPacket() + pkt := makeArbitraryGeneralPacket(arbitraryReservedHeaderBytes) v, err := nf.EvaluateHook(arbitraryFamily, arbitraryHook, pkt) if err != nil { @@ -339,7 +571,7 @@ func TestEvaluateImmediateBytesData(t *testing.T) { } } // Runs evaluation and checks for default policy verdict accept - pkt := makeTestingPacket() + pkt := makeArbitraryGeneralPacket(arbitraryReservedHeaderBytes) v, err := nf.EvaluateHook(arbitraryFamily, arbitraryHook, pkt) if err != nil { t.Fatalf("unexpected error for EvaluateHook: %v", err) @@ -824,7 +1056,7 @@ func TestEvaluateComparison(t *testing.T) { } // Runs evaluation and checks verdict. - pkt := makeTestingPacket() + pkt := makeArbitraryGeneralPacket(arbitraryReservedHeaderBytes) v, err := nf.EvaluateHook(arbitraryFamily, arbitraryHook, pkt) if err != nil { t.Fatalf("unexpected error for EvaluateHook: %v", err) @@ -847,6 +1079,266 @@ func TestEvaluateComparison(t *testing.T) { } } +// 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 := makeArbitraryEtherPacket(0) + ipv4Packet := makeArbitraryIPv4Packet(header.IPv4MinimumSize) + ipv6Packet := makeArbitraryIPv6Packet(header.IPv6MinimumSize) + tcpPacket := makeArbitraryIPv4TCPPacket(header.IPv4MinimumSize + header.TCPMinimumSize) + + 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, newBytesData(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, newBytesData(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, newBytesData(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, newBytesData(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, newBytesData(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, newBytesData(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 1 + tname: "load ipv4 header fragment offset non zero for fragmented packet", + pkt: makeFragmentedIPv4Packet(header.IPv4MinimumSize), + 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, newBytesData(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, newBytesData(numToBE(arbitraryTimeToLive, ipv4TTLLen))), + }, + { // cmd: add rule ip tab ch 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, newBytesData(numToBE(int(transportProtocol), 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, newBytesData(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, newBytesData(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, newBytesData(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, newBytesData(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, newBytesData(numToBE(int(transportProtocol), 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, newBytesData(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, newBytesData(arbitraryIPv6AddrB[:])), + }, + { // cmd: add rule ip6 tab ch ip6 saddr 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, newBytesData(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: func() *stack.PacketBuffer { + p := makeFragmentedIPv4Packet(header.IPv4MinimumSize + header.TCPMinimumSize) + tcpHdr := header.TCP(p.TransportHeader().Push(header.TCPMinimumSize)) + tcpHdr.Encode(&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), + }) + tcpHdr.SetChecksum(tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum( + header.TCPProtocolNumber, + tcpip.AddrFrom4(arbitraryIPv4AddrB), + tcpip.AddrFrom4(arbitraryIPv4AddrB2), + header.TCPMinimumSize, + ))) + p.TransportProtocolNumber = header.TCPProtocolNumber + return p + }(), + op1: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, tcpSrcPortOffset, tcpSrcPortLen, 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, newBytesData(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, newBytesData(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, newBytesData(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, newBytesData(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, newBytesData(numToBE(tcpWinSize, tcpWindowLen))), + }, + { // cmd: add rule ip tab ch 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, newBytesData(numToBE(int(header.TCP(tcpPacket.TransportHeader().Slice()).Checksum()), tcpChecksumLen))), + }, + { // cmd: add rule ip tab ch 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, newBytesData(numToBE(tcpUrgentPointer, tcpUrgPtrLen))), + }, + } { + t.Run(test.tname, func(t *testing.T) { + // Sets up an NFTables object with a single table, chain, and rule. + nf := NewNFTables() + tab, err := nf.AddTable(arbitraryFamily, "test", "test table", false) + if err != nil { + t.Fatalf("unexpected error for AddTable: %v", err) + } + bc, err := tab.AddChain("base_chain", nil, "test chain", false) + if err != nil { + t.Fatalf("unexpected error for AddChain: %v", err) + } + bc.SetBaseChainInfo(arbitraryInfoPolicyAccept) + rule := &Rule{} + + // Adds testing operations. + if test.op1 != nil { + rule.addOperation(test.op1) + } + if test.op2 != nil { + rule.addOperation(test.op2) + } + + // Adds drop operation. Will be final verdict if all comparisons are true. + rule.addOperation(mustCreateImmediate(t, linux.NFT_REG_VERDICT, newVerdictData(Verdict{Code: VC(linux.NF_DROP)}))) + + // Registers the rule to the base chain. + if err := bc.RegisterRule(rule, -1); err != nil { + t.Fatalf("unexpected error for RegisterRule: %v", err) + } + + // Runs evaluation. + v, err := nf.EvaluateHook(arbitraryFamily, arbitraryHook, test.pkt) + 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) + } + } + }) + } +} + // TestLoopCheckOnRegisterAndUnregister tests the loop checking and accompanying // logic on registering and unregistering rules. func TestLoopCheckOnRegisterAndUnregister(t *testing.T) { @@ -1298,7 +1790,7 @@ func TestLoopCheckOnRegisterAndUnregister(t *testing.T) { } // Runs evaluation and checks verdict. - pkt := makeTestingPacket() + pkt := makeArbitraryGeneralPacket(arbitraryReservedHeaderBytes) v, err := nf.EvaluateHook(arbitraryFamily, arbitraryHook, pkt) if err != nil { if test.verdict.ChainName != "error" { @@ -1405,7 +1897,7 @@ func TestMaxNestedJumps(t *testing.T) { } // Runs evaluation and checks verdict. - pkt := makeTestingPacket() + pkt := makeArbitraryGeneralPacket(arbitraryReservedHeaderBytes) v, err := nf.EvaluateHook(arbitraryFamily, arbitraryHook, pkt) if err != nil { if test.verdict.ChainName != "error" { @@ -1419,6 +1911,18 @@ func TestMaxNestedJumps(t *testing.T) { } } +// 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 { @@ -1448,3 +1952,12 @@ func mustCreateComparison(t *testing.T, sreg uint8, cop int, data registerData) } return cmp } + +// mustCreatePayloadLoad wraps the NewPayloadLoad function for brevity. +func mustCreatePayloadLoad(t *testing.T, base payloadBase, offset, len, dreg uint8) *payloadLoad { + pdload, err := newPayloadLoad(base, offset, len, dreg) + if err != nil { + t.Fatalf("failed to create payload load: %v", err) + } + return pdload +} diff --git a/pkg/tcpip/nftables/nftinterp.go b/pkg/tcpip/nftables/nftinterp.go index 9abeee06a..61f3f2fe8 100644 --- a/pkg/tcpip/nftables/nftinterp.go +++ b/pkg/tcpip/nftables/nftinterp.go @@ -17,7 +17,6 @@ package nftables import ( "encoding/hex" "fmt" - "math" "regexp" "slices" "strconv" @@ -124,7 +123,7 @@ func InterpretRule(ruleString string) (*Rule, error) { return r, nil } -// InterpretOperation creates a new Operation from the given operation string, +// InterpretOperation creates a new operation from the given operation string, // assumed to be a single line of text surrounded in square brackets. // Note: the operation string should be generated as output from the official nft // binary (can be accomplished by using flag --debug=netlink). @@ -140,6 +139,8 @@ func InterpretOperation(line string, lnIdx int) (operation, error) { return InterpretImmediate(line, lnIdx) case "cmp": return InterpretComparison(line, lnIdx) + case "payload": + return InterpretPayloadLoad(line, lnIdx) default: return nil, &SyntaxError{lnIdx, 1, fmt.Sprintf("unrecognized operation type: %s", tokens[1])} } @@ -264,6 +265,102 @@ func InterpretComparison(line string, lnIdx int) (operation, error) { return cmp, nil } +// InterpretPayloadLoad creates a new PayloadLoad operation from the given +// string. +func InterpretPayloadLoad(line string, lnIdx int) (operation, error) { + tokens := strings.Fields(line) + + // Requires exactly 13 tokens: + // "[", "payload", "load", len+"b", "@", payload base, "header", "+", offset, "=>", "reg", register index, "]". + if len(tokens) != 13 { + return nil, &SyntaxError{lnIdx, 0, fmt.Sprintf("incorrect number of tokens for payload load operation, should be exactly 13, got %d", len(tokens))} + } + + if err := checkOperationBrackets(tokens, lnIdx); err != nil { + return nil, err + } + + tkIdx := 1 + + // First token should be "payload". + if err := consumeToken("payload", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Second token should be "load". + if err := consumeToken("load", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Third token should be the length (in bytes) of the payload followed by 'b'. + len, err := parsePayloadLength(tokens[tkIdx], lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Fourth token should be "@". + if err := consumeToken("@", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Fifth token should be the payload base header. + base, err := parsePayloadBase(tokens[tkIdx], lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Sixth token should be "header". + if err := consumeToken("header", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Seventh token should be "+". + if err := consumeToken("+", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Eighth token should be the uint8 representing the offset. + offset, err := parseUint8(tokens[tkIdx], "offset", lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Ninth token should be "=>". + if err := consumeToken("=>", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Tenth token should be "reg". + if err := consumeToken("reg", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Eleventh token should be the uint8 representing the register index. + reg, err := parseRegister(tokens[tkIdx], lnIdx, tkIdx) + if err != nil { + return nil, err + } + tkIdx++ + + // Create the operation with the specified arguments. + pdload, err := newPayloadLoad(base, offset, len, reg) + if err != nil { + return nil, &LogicError{lnIdx, tkIdx, err} + } + + return pdload, nil +} + // // Interpreter Helper Functions. // @@ -280,18 +377,25 @@ func checkOperationBrackets(tokens []string, lnIdx int) error { return nil } +// parseUint8 parses the uint8 which should be supposed from the given string. +func parseUint8(regString string, supposed string, lnIdx int, tkIdx int) (uint8, error) { + v64, err := strconv.ParseUint(regString, 10, 8) + if err != nil { + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("could not parse uint8 %s: '%s'", supposed, regString)} + } + return uint8(v64), nil +} + // parseRegister parses the register index from the given string. func parseRegister(regString string, lnIdx int, tkIdx int) (uint8, error) { - reg64, err := strconv.ParseUint(regString, 10, 8) + reg, err := parseUint8(regString, "register index", lnIdx, tkIdx) if err != nil { - return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("could not parse uint8 register index: '%s'", regString)} + return 0, err } - - if reg64 > math.MaxUint8 || !isRegister(uint8(reg64)) { - return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid register index: %d", reg64)} + if !isRegister(reg) { + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid register index: %d", reg)} } - - return uint8(reg64), nil + return reg, nil } // parseRegisterData parses the register data from the given token and returns @@ -414,6 +518,39 @@ func parseCmpOp(copString string, lnIdx int, tkIdx int) (int, error) { } } +// parsePayloadLength parses the payload length from the given string +// expecting a unsigned 8-bit integer followed by 'b'. +func parsePayloadLength(lenString string, lnIdx int, tkIdx int) (uint8, error) { + lastChar := lenString[len(lenString)-1] + if lastChar != 'b' { + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("expected 'b' at the end of payload length, got '%c'", lastChar)} + } + numStr := lenString[:len(lenString)-1] + len, err := strconv.ParseUint(numStr, 10, 8) + if err != nil { + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("could not parse uint8 payload length: '%s'", numStr)} + } + if len > 16 { + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("payload length must be <= 16 bytes, got %d", len)} + } + return uint8(len), nil +} + +// parsePayloadBase parses the payload base header from the given string. +func parsePayloadBase(baseString string, lnIdx int, tkIdx int) (payloadBase, error) { + switch baseString { + case "link": + return linux.NFT_PAYLOAD_LL_HEADER, nil + case "network": + return linux.NFT_PAYLOAD_NETWORK_HEADER, nil + case "transport": + return linux.NFT_PAYLOAD_TRANSPORT_HEADER, nil + // Inner and Tunnel Headers cannot be specified in payload load operation. + default: + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid payload base: '%s'", baseString)} + } +} + // consumeToken is a helper function that checks if the token at the given index // matches the expected string, returning a SyntaxError if not. func consumeToken(expected string, tokens []string, lnIdx int, tkIdx int) error { diff --git a/pkg/tcpip/nftables/nftinterp_test.go b/pkg/tcpip/nftables/nftinterp_test.go index cfaa6048a..35d35d0e6 100644 --- a/pkg/tcpip/nftables/nftinterp_test.go +++ b/pkg/tcpip/nftables/nftinterp_test.go @@ -52,7 +52,7 @@ func checkOp(t *testing.T, test interpretOperationTestAction, checkFunc func(str } } -// TestInterpretImmediateOps tests the interpretation of immediate operations. +// TestInterpretImmediateOps tests interpretation of immediate operations. func TestInterpretImmediateOps(t *testing.T) { for _, test := range []interpretOperationTestAction{ { @@ -162,7 +162,7 @@ func checkImmediateOp(tname string, expected operation, actual operation) error return nil } -// TestInterpretComparisonOps tests the interpretation of comparison operations. +// TestInterpretComparisonOps tests interpretation of comparison operations. func TestInterpretComparisonOps(t *testing.T) { for _, test := range []interpretOperationTestAction{ { @@ -375,6 +375,126 @@ func checkComparisonOp(tname string, expected operation, actual operation) error return nil } +// TestInterpretPayloadLoadOps tests interpretation of payload load operations. +// Most operations are direct output of nft binary commands. All stated commands +// should be preceded by nft --debug=netlink to generate matching operations. +func TestInterpretPayloadLoadOps(t *testing.T) { + for _, test := range []interpretOperationTestAction{ + { + tname: "load bytes into verdict register", + opStr: "[ payload load 2b @ transport header + 0 => reg 0 ]", + expected: nil, + }, + // cmd: add rule ip6 ip tab ch tcp flags syn counter accept + { + tname: "load 1 byte into 4-byte register", + opStr: "[ payload load 1b @ transport header + 13 => reg 9 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 13, 1, linux.NFT_REG32_01), + }, + { + tname: "load 1 byte into 16-byte register", + opStr: "[ payload load 1b @ transport header + 13 => reg 1 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 13, 1, linux.NFT_REG_1), + }, + // cmd: add rule ip tab ch tcp sport 80 counter accept + { + tname: "load 2 bytes into 4-byte register no offset", + opStr: "[ payload load 2b @ transport header + 0 => reg 8 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 0, 2, linux.NFT_REG32_00), + }, + { + tname: "load 2 bytes into 16-byte register no offset", + opStr: "[ payload load 2b @ transport header + 0 => reg 1 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 0, 2, linux.NFT_REG_1), + }, + // cmd: add rule ip tab ch tcp dport 12345 counter accept + { + tname: "load 2 bytes into 4-byte register with offset", + opStr: "[ payload load 2b @ transport header + 2 => reg 9 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 2, 2, linux.NFT_REG32_01), + }, + { + tname: "load 2 bytes into 16-byte register with offset", + opStr: "[ payload load 2b @ transport header + 2 => reg 1 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 2, 2, linux.NFT_REG_1), + }, + // cmd: add rule ip tab ch @th,24,24 0xabcdef counter accept + { + tname: "load 3 bytes into 4-byte register", + opStr: "[ payload load 3b @ transport header + 3 => reg 10 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 3, 3, linux.NFT_REG32_02), + }, + { + tname: "load 3 bytes into 16-byte register", + opStr: "[ payload load 3b @ transport header + 3 => reg 2 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_TRANSPORT_HEADER, 3, 3, linux.NFT_REG_2), + }, + // cmd: add rule ip tab ch ip daddr 192.168.1.1 counter accept + { + tname: "load 4 bytes into 4-byte register", + opStr: "[ payload load 4b @ network header + 16 => reg 12 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 16, 4, linux.NFT_REG32_04), + }, + { + tname: "load 4 bytes into 16-byte register", + opStr: "[ payload load 4b @ network header + 16 => reg 1 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 16, 4, linux.NFT_REG_1), + }, + // cmd: add rule ip tab ch ether saddr 01:23:45:67:89:ab counter drop + { + tname: "load 6 bytes into 4-byte register", + opStr: "[ payload load 6b @ link header + 6 => reg 13 ]", + expected: nil, + }, + { + tname: "load 6 bytes into 16-byte register", + opStr: "[ payload load 6b @ link header + 6 => reg 3 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_LL_HEADER, 6, 6, linux.NFT_REG_3), + }, + // cmd: add rule ip6 tab ch ip6 saddr 2001:db8::2 counter accept + { + tname: "load 16 bytes into 4-byte register", + opStr: "[ payload load 16b @ network header + 8 => reg 10 ]", + expected: nil, + }, + { + tname: "load 16 bytes into 16-byte register", + opStr: "[ payload load 16b @ network header + 8 => reg 1 ]", + expected: mustCreatePayloadLoad(t, linux.NFT_PAYLOAD_NETWORK_HEADER, 8, 16, linux.NFT_REG_1), + }, + { + tname: "load >16 bytes into 16-byte register", + opStr: "[ payload load 20b @ network header + 16 => reg 1 ]", + expected: nil, + }, + } { + t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkPayloadLoadOp) }) + } +} + +// checkPayloadLoadOp checks that the given operation is a payload load +// operation and that it matches the expected payload load operation. +func checkPayloadLoadOp(tname string, expected operation, actual operation) error { + expectedPdLoad := expected.(*payloadLoad) + pdload, ok := actual.(*payloadLoad) + if !ok { + return fmt.Errorf("expected operation type to be PayloadLoad for %s, got %T", tname, actual) + } + if pdload.base != expectedPdLoad.base { + return fmt.Errorf("expected payload base to be %v for %s, got %v", expectedPdLoad.base, tname, pdload.base) + } + if pdload.offset != expectedPdLoad.offset { + return fmt.Errorf("expected offset to be %d for %s, got %d", expectedPdLoad.offset, tname, pdload.offset) + } + if pdload.blen != expectedPdLoad.blen { + return fmt.Errorf("expected length to be %d for %s, got %d", expectedPdLoad.blen, tname, pdload.blen) + } + if pdload.dreg != expectedPdLoad.dreg { + return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedPdLoad.dreg, tname, pdload.dreg) + } + return nil +} + // TestInterpretRule tests the interpretation of basic and general rules as a // list of operations. func TestInterpretRule(t *testing.T) {