diff --git a/pkg/tcpip/nftables/BUILD b/pkg/tcpip/nftables/BUILD index d537d70de..4bb1a167b 100644 --- a/pkg/tcpip/nftables/BUILD +++ b/pkg/tcpip/nftables/BUILD @@ -19,6 +19,7 @@ go_test( srcs = ["nftables_test.go"], library = ":nftables", deps = [ + "//pkg/abi/linux", "//pkg/buffer", "//pkg/tcpip/stack", ], diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index 54e78cd1c..84695092b 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -53,6 +53,7 @@ import ( const ( defaultBaseChainCapacity = 8 defaultRuleCapacity = 8 + registersByteSize = 64 // 4 16-byte registers or 16 4-byte registers. ) // AddressFamily describes the 6 address families supported by nftables. @@ -277,6 +278,7 @@ type Chain struct { // BaseChainInfo stores hook-related info for attaching a chain to the pipeline. type BaseChainInfo struct { + // BcType is the base chain type of the chain (filter, nat, route). BcType BaseChainType @@ -551,100 +553,273 @@ type Operation interface { Eval() Verdict } -// Verdict represents verdict statements and are issued (returned) from tables, -// chains, and rules when processing packets to alter the control flow -// of the ruleset and determine the actions and modifications to be performed on -// packets. -type Verdict uint32 +// +// Register and Register-Related Implementations. +// Note: Registers are represented by type uint8 for the register number. +// + +func isVerdictRegister(reg uint8) bool { + return reg == linux.NFT_REG_VERDICT +} + +func is16ByteRegister(reg uint8) bool { + return reg >= linux.NFT_REG_1 && reg <= linux.NFT_REG_4 +} + +func is4ByteRegister(reg uint8) bool { + return reg >= linux.NFT_REG32_00 && reg <= linux.NFT_REG32_15 +} + +func isRegister(reg uint8) bool { + return isVerdictRegister(reg) || is16ByteRegister(reg) || is4ByteRegister(reg) +} + +// RegisterDataType is the type of data to be set in a register. +type RegisterDataType int const ( - // - // Absolute verdicts are issued from tables, chains, and rules. These are - // called absolute because they terminate ruleset evaluation immediately. - // - - // NftDrop, terminates evaluation and drops the packet, occurs instantly. - NftDrop Verdict = iota - - // NftAccept, terminates evaluation and accepts the packet; the packet can - // still be dropped later by another hook or another chain within same hook. - NftAccept - - // - // Internal verdicts are issued only for rules and continue/modify evaluation. - // - - // NftQueue, terminates the current evaluation and queues packet to userspace. - // Userspace must provide a Drop or Accept verdict. In the case Accept is - // issued, processing resumes with the next base chain hook, not the rule - // following the Queue verdict. - NftQueue - - // NftContinue, continues evaluation with the next rule. This is the default - // behavior if no verdict is issued. - NftContinue - - // NftReturn, returns from the current chain and continues evaluation with the - // next rule in the previous chain. If issued from a base chain (no previous - // chain), the verdict issued is as specified by the base chain's policy. - NftReturn - - // NftJump, continues evaluation at the first rule in the specified chain and - // is set to continue evaluation back at the next rule in the current chain - // after the specified chain is entirely evaluated or issues a Return (by - // pushing the current position in the ruleset to a call stack). In the case - // an absolute verdict is issued from the specified chain, ruleset evaluation - // terminates immediately as normal. - NftJump // chain required as argument - - // NftGoto, continues evaluation at the first rule in the specified chain - // similar to NftJump but doesn't push the current position to a call stack so - // evaluation doesn't resume at the current chain. - NftGoto // chain required as argument - - // NumVerdicts is the number of verdicts supported by nftables. - NumVerdicts + // DataVerdict represents a verdict to be stored in a register. + DataVerdict RegisterDataType = iota + // Data4Bytes represents 4 bytes of data to be stored in a register. + Data4Bytes + // Data16Bytes represents 16 bytes of data to be stored in a register. + Data16Bytes ) -// String for Verdict prints names for the supported verdicts. +// RegisterData represents the data to be set in a register. +type RegisterData interface { + + // Type returns the register data type. + Type() RegisterDataType + + // String returns a string representation of the register data. + String() string + + // Equal compares the register data to another. + Equal(other RegisterData) bool + + // ValidateRegister ensures the register is compatible with the data type, + // returning an error otherwise. + ValidateRegister(reg uint8) error + + // StoreData sets the data in the destination register, panicking if the + // register is not valid for the data type. + // Note: assumes data is valid for register. This is used primarily during + // operation evaluation and the data type/register compatibility should have + // been checked during the operation init. + StoreData(regs *RegisterSet, reg uint8) +} + +// VerdictData represents a verdict as data to be stored in a register. +type VerdictData struct { + data Verdict +} + +// NewVerdictData creates a RegisterData for a verdict. +func NewVerdictData(verdict Verdict) RegisterData { return VerdictData{data: verdict} } + +// Type returns the DataVerdict register data type for VerdictData. +func (rd VerdictData) Type() RegisterDataType { return DataVerdict } + +// String returns a string representation of the verdict data. +func (rd VerdictData) String() string { + return rd.data.String() +} + +// Equal compares the verdict data to another RegisterData object. +func (rd VerdictData) Equal(other RegisterData) bool { + if other == nil { + return false + } + if other.Type() != DataVerdict { + return false + } + return rd.data == other.(VerdictData).data +} + +// ValidateRegister ensures the register is compatible with VerdictData. +func (rd VerdictData) ValidateRegister(reg uint8) error { + if !isVerdictRegister(reg) { + return fmt.Errorf("verdict data type is only valid for register 0") + } + return nil +} + +// StoreData sets the data in the destination register to the verdict. +func (rd VerdictData) StoreData(regs *RegisterSet, reg uint8) { + if err := rd.ValidateRegister(reg); err != nil { + panic(err) + } + regs.verdict = rd.data +} + +// BytesData represents a 4 or 16 bytes of data to be stored in a register. +type BytesData struct { + data []byte +} + +// NewBytesData creates a RegisterData for 4 or 16 bytes of data. +func NewBytesData(bytes []byte) RegisterData { + if len(bytes) != 4 && len(bytes) != 16 { + panic(fmt.Errorf("invalid byte data length: %d", len(bytes))) + } + return BytesData{data: bytes} +} + +// Type returns the Data4Bytes or Data16Bytes register data type depending +// on the length of the BytesData. +func (rd BytesData) Type() RegisterDataType { + if len(rd.data) == 4 { + return Data4Bytes + } + return Data16Bytes +} + +// String returns a string representation of the bytes data. +func (rd BytesData) String() string { + return fmt.Sprintf("%x", rd.data) +} + +// Equal compares the bytes data to another RegisterData object. +func (rd BytesData) Equal(other RegisterData) bool { + if other == nil { + return false + } + if other.Type() != rd.Type() { + return false + } + return slices.Equal(rd.data, other.(BytesData).data) +} + +// ValidateRegister ensures the register is compatible with Bytes4Data. +func (rd BytesData) ValidateRegister(reg uint8) error { + if rd.Type() == Data4Bytes { + if !is4ByteRegister(reg) && !is16ByteRegister(reg) { + return fmt.Errorf("4-byte data type is only valid for 4-byte and 16-byte registers") + } + } else { + if !is16ByteRegister(reg) { + return fmt.Errorf("16-byte data type is only valid for 16-byte registers") + } + } + return nil +} + +// StoreData sets the data in the destination register to the uint32. +func (rd BytesData) StoreData(regs *RegisterSet, reg uint8) { + if err := rd.ValidateRegister(reg); err != nil { + panic(err) + } + var start uint8 + var regBuf []byte + // Stores 4-byte data in a 4-byte register. + if is4ByteRegister(reg) { + start = (reg - linux.NFT_REG32_00) * linux.NFT_REG32_SIZE + regBuf = regs.data[start : start+linux.NFT_REG32_SIZE] + } else { + // Stores 16-byte data in a 16-byte register. + if rd.Type() == Data16Bytes { + start = (reg - linux.NFT_REG_1) * linux.NFT_REG_SIZE + regBuf = regs.data[start : start+linux.NFT_REG_SIZE] + } else { + // Stores 4-byte data in a 16-byte register, leaving excess space on the + // left (bc the data is little endian). + start = (reg-linux.NFT_REG_1)*linux.NFT_REG_SIZE + linux.NFT_REG_SIZE - linux.NFT_REG32_SIZE + regBuf = regs.data[start : start+linux.NFT_REG32_SIZE] + } + } + copy(regBuf, rd.data) +} + +// RegisterSet represents the set of registers supported by the kernel. +// Use RegisterData.StoreData to set data in the registers. +// Note: Corresponds to nft_regs from include/net/netfilter/nf_tables.h. +type RegisterSet struct { + verdict Verdict // 16-byte verdict register + data [registersByteSize]byte // 4 16-byte registers or 16 4-byte registers +} + +// NewRegisterSet creates a new RegisterSet with the Continue Verdict and all +// registers set to 0. +func NewRegisterSet() RegisterSet { + return RegisterSet{ + verdict: Verdict{Code: VC(linux.NFT_CONTINUE)}, + data: [registersByteSize]byte{0}, + } +} + +// Verdict returns the verdict data. +func (regs *RegisterSet) Verdict() Verdict { + return regs.verdict +} + +// +// Verdict Implementation. +// There are two types of verdicts: +// 1. Netfilter (External) Verdicts: Drop, Accept, Stolen, Queue, Repeat, Stop +// These are terminal verdicts that are returned to the kernel. +// 2. Nftable (Internal) Verdicts:, Continue, Break, Jump, Goto, Return +// These are internal verdicts that only exist within the nftables library. +// Both share the same numeric space (uint32 Verdict Code). +// + +// Verdict represents the result of evaluating a packet against a rule or chain. +type Verdict struct { + // Code is the numeric code that represents the verdict issued. + Code uint32 + + // ChainName is the name of the chain to continue evaluation if the verdict is + // Jump or Goto. + // Note: the chain must be in the same table as the current chain. + ChainName string +} + +// String returns a string representation of the verdict. func (v Verdict) String() string { + out := VerdictToString(v.Code) + if v.ChainName != "" { + out += fmt.Sprintf(" -> %s", v.ChainName) + } + return out +} + +// VC converts a numeric code to a uint32 number representing the verdict. +func VC(v int32) uint32 { + return uint32(v) +} + +// VerdictToString prints names for the supported verdicts. +func VerdictToString(v uint32) string { switch v { - case NftDrop: + // Netfilter (External) Verdicts: + case VC(linux.NF_DROP): return "Drop" - case NftAccept: + case VC(linux.NF_ACCEPT): return "Accept" - case NftQueue: + case VC(linux.NF_STOLEN): + return "Stolen" + case VC(linux.NF_QUEUE): return "Queue" - case NftContinue: + case VC(linux.NF_REPEAT): + return "Repeat" + case VC(linux.NF_STOP): + return "Stop" + // Nftable (Internal) Verdicts: + case VC(linux.NFT_CONTINUE): return "Continue" - case NftReturn: - return "Return" - case NftJump: + case VC(linux.NFT_BREAK): + return "Break" + case VC(linux.NFT_JUMP): return "Jump" - case NftGoto: + case VC(linux.NFT_GOTO): return "Goto" + case VC(linux.NFT_RETURN): + return "Return" default: panic(fmt.Sprintf("invalid verdict: %d", int(v))) } } -// validateVerdict ensures the verdict is valid (can be absolute or internal). -func validateVerdict(verdict Verdict) error { - // Note verdict is unsigned so it can't be less than 0. - if verdict >= NumVerdicts { - return fmt.Errorf("invalid verdict: %d", int(verdict)) - } - return nil -} - -// validateAbsoluteVerdict ensures an absolute verdict (Drop or Accept). -func validateAbsoluteVerdict(verdict Verdict) error { - if verdict != NftDrop && verdict != NftAccept { - return fmt.Errorf("invalid absolute verdict: %d", int(verdict)) - } - return nil -} - // // Top-Level NFTables Functions // Note: Provides wrapper functions for the creation and deletion of tables, @@ -840,17 +1015,17 @@ func (nf *NFTables) EvaluateHook(family AddressFamily, hook Hook, pkt *stack.PacketBuffer) (Verdict, *stack.PacketBuffer, error) { // Ensures address family is valid. if err := validateAddressFamily(family); err != nil { - return NftDrop, nil, err + return Verdict{}, nil, err } // Ensures hook is valid. if err := validateHook(hook, family); err != nil { - return NftDrop, nil, err + return Verdict{}, nil, err } // Immediately accept if there are no base chains for the specified hook. if nf.filters[family] == nil || nf.filters[family].hfStacks[hook] == nil { - return NftAccept, pkt, nil + return Verdict{Code: VC(linux.NF_ACCEPT)}, pkt, nil } // Evaluates packet through all base chains for given hook in priority order. @@ -862,12 +1037,12 @@ func (nf *NFTables) EvaluateHook(family AddressFamily, // Note: chain.evaluate() returns an absolute verdict. newPkt, chainVerdict := chain.evaluate(pkt) // Returns immediately if the verdict is Drop. - if chainVerdict == NftDrop { - return NftDrop, nil, nil + if chainVerdict.Code == VC(linux.NF_DROP) { + return Verdict{Code: VC(linux.NF_DROP)}, nil, nil } pkt = newPkt } - return NftAccept, pkt, nil + return Verdict{Code: VC(linux.NF_ACCEPT)}, pkt, nil } // @@ -1071,7 +1246,7 @@ func (c *Chain) AddRule(rule *Rule) { func (c *Chain) evaluate(pkt *stack.PacketBuffer) (*stack.PacketBuffer, Verdict) { // TODO(b/345684870): Implement this by evaluating all rules in a chain in // sequential order. - return pkt, NftAccept + return pkt, Verdict{Code: VC(linux.NF_ACCEPT)} } // diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index f759ed6a1..99c4d898a 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -15,18 +15,21 @@ package nftables import ( - "fmt" + "reflect" "testing" + "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip/stack" ) -// An arbitrary packet for testing -var pkt *stack.PacketBuffer = stack.NewPacketBuffer(stack.PacketBufferOptions{ - ReserveHeaderBytes: 50, - Payload: buffer.MakeWithData([]byte{0, 2, 4, 8, 16, 32, 64, 128}), -}) +// makeTestingPacket creates an arbitrary packet for testing. +func makeTestingPacket() *stack.PacketBuffer { + return stack.NewPacketBuffer(stack.PacketBufferOptions{ + ReserveHeaderBytes: 50, + Payload: buffer.MakeWithData([]byte{0, 2, 4, 8, 16, 32, 64, 128}), + }) +} // TestUnsupportedAddressFamily tests that an empty NFTables object returns an // error when evaluating a packet for an unsupported address family. @@ -34,12 +37,12 @@ func TestUnsupportedAddressFamily(t *testing.T) { nf := NewNFTables() for _, unsupportedFamily := range []AddressFamily{AddressFamily(NumAFs), AddressFamily(-1)} { // Note: the Prerouting hook is arbitrary (any hook would work). + pkt := makeTestingPacket() v, finalPkt, err := nf.EvaluateHook(unsupportedFamily, Prerouting, pkt) - if v != NftDrop || finalPkt != nil || err == nil { - t.Fatalf("got EvaluateHook(unsupported address family %d, %s, packet) = (%s, %s, %v); want (%s, %s, %s)", - int(unsupportedFamily), Prerouting.String(), - v.String(), packetResultString(finalPkt, pkt), err, - NftDrop.String(), "nilPacket", fmt.Sprintf("Invalid address family: %d", int(unsupportedFamily))) + if err == nil { + t.Fatalf("expecting error for EvaluateHook with unsupported address family %d; got %s verdict, %s packet, and error %v", + int(unsupportedFamily), + v.String(), packetResultString(pkt, finalPkt), err) } } } @@ -52,6 +55,7 @@ func TestAcceptAllForSupportedHooks(t *testing.T) { 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() v, finalPkt, err := nf.EvaluateHook(family, hook, pkt) supported := false @@ -63,20 +67,16 @@ func TestAcceptAllForSupportedHooks(t *testing.T) { } if supported { - if v != NftAccept || finalPkt != pkt || err != nil { - // Should be supported and accept all but an error was returned. - t.Fatalf("got EvaluateHook(%s, %s, packet) = (%s, %s, %v); want (%s, %s, %s)", - family.String(), hook.String(), - v.String(), packetResultString(finalPkt, pkt), err, - NftAccept.String(), "samePacket", "noError") + if err != nil || v.Code != VC(linux.NF_ACCEPT) { + t.Fatalf("expecting accept verdict for EvaluateHook with supported hook %s for family %s; got %s verdict, %s packet, and error %v", + hook.String(), family.String(), + v.String(), packetResultString(pkt, finalPkt), err) } } else { - if v != NftDrop || finalPkt != nil || err == nil { - // Should return an error but the packet was accepted. - t.Fatalf("got EvaluateHook(%s, %s, packet) = (%s, %s, %v); want (%s, %s, hook %s is not valid for address family %s)", - family.String(), hook.String(), - v.String(), packetResultString(finalPkt, pkt), err, - NftDrop.String(), "nilPacket", hook.String(), family.String()) + if err == nil { + t.Fatalf("expecting error for EvaluateHook with unsupported hook %s for family %s; got %s verdict, %s packet, and error %v", + hook.String(), family.String(), + v.String(), packetResultString(pkt, finalPkt), err) } } } @@ -86,14 +86,12 @@ func TestAcceptAllForSupportedHooks(t *testing.T) { // packetResultString compares 2 packets by equality and returns a string // representation. -func packetResultString(final, initial *stack.PacketBuffer) string { - // TODO(b/345684870): Compare packet contents instead of pointers. - switch final { - case nil: - return "nilPacket" - case initial: - return "samePacket" - default: - return "differentPacket" +func packetResultString(initial, final *stack.PacketBuffer) string { + if final == nil { + return "nil" } + if reflect.DeepEqual(final, initial) { + return "unmodified" + } + return "modified" }