diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index e2f7bedc7..d20a7b4ba 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -44,6 +44,7 @@ import ( "bytes" "encoding/binary" "fmt" + "math/rand" "slices" "sync/atomic" "time" @@ -123,6 +124,26 @@ func (f AddressFamily) String() string { } } +// Protocol returns the protocol number for the address family. +func (f AddressFamily) Protocol() uint8 { + switch f { + case IP: + return linux.NFPROTO_INET + case IP6: + return linux.NFPROTO_IPV6 + case Inet: + return linux.NFPROTO_IPV6 + case Arp: + return linux.NFPROTO_ARP + case Bridge: + return linux.NFPROTO_BRIDGE + case Netdev: + return linux.NFPROTO_NETDEV + default: + panic(fmt.Sprintf("invalid address family: %d", int(f))) + } +} + // validateAddressFamily ensures the family address is valid (within bounds). func validateAddressFamily(family AddressFamily) error { if family < 0 || family >= NumAFs { @@ -215,6 +236,7 @@ type NFTables struct { filters [NumAFs]*addressFamilyFilter // Filters for each address family. clock tcpip.Clock // Clock for timing evaluations. startTime time.Time // Time NFTables object was created. + rng *rand.Rand // Random number generator. } // addressFamilyFilter represents the nftables state for a specific address @@ -596,6 +618,7 @@ var ( _ operation = (*last)(nil) _ operation = (*route)(nil) _ operation = (*byteorder)(nil) + _ operation = (*metaLoad)(nil) ) // immediate is an operation that sets the data in a register. @@ -604,7 +627,7 @@ type immediate struct { dreg uint8 // Number of the destination register. } -// newImmediate creates a new Immediate operation. +// newImmediate creates a new immediate operation. func newImmediate(dreg uint8, data registerData) (*immediate, error) { if err := data.validateRegister(dreg); err != nil { return nil, err @@ -662,7 +685,7 @@ func validateComparisonOp(cop cmpOp) error { } } -// newComparison creates a new Comparison operation. +// newComparison creates a new comparison operation. func newComparison(sreg uint8, op int, data []byte) (*comparison, error) { if isVerdictRegister(sreg) { return nil, fmt.Errorf("comparison operation cannot use verdict register as source") @@ -756,7 +779,7 @@ func validateRangeOp(rop rngOp) error { } } -// newRanged creates a new Ranged operation. +// newRanged creates a new ranged operation. func newRanged(sreg uint8, op int, low, high []byte) (*ranged, error) { if isVerdictRegister(sreg) { return nil, fmt.Errorf("comparison operation cannot use verdict register as source") @@ -875,7 +898,7 @@ func getPayloadBuffer(pkt *stack.PacketBuffer, base payloadBase) []byte { return nil } -// newPayloadLoad creates a new PayloadLoad operation. +// newPayloadLoad creates a new payloadLoad operation. func newPayloadLoad(base payloadBase, offset, blen, dreg uint8) (*payloadLoad, error) { if isVerdictRegister(dreg) { return nil, fmt.Errorf("payload load operation cannot use verdict register as destination") @@ -948,7 +971,7 @@ func validateChecksumType(csumType uint8) error { } } -// newPayloadSet creates a new PayloadSet operation. +// newPayloadSet creates a new payloadSet operation. func newPayloadSet(base payloadBase, offset, blen, sreg, csumType, csumOffset, csumFlags uint8) (*payloadSet, error) { if isVerdictRegister(sreg) { return nil, fmt.Errorf("payload set operation cannot use verdict register as destination") @@ -1514,6 +1537,224 @@ func (op byteorder) evaluate(regs *registerSet, pkt *stack.PacketBuffer, rule *R } } +// metaLoad is an operation that loads specific meta data into a register. +// Note: meta operations are not supported for the verdict register. +// TODO(b/345684870): Support retrieving more meta fields for Meta Load. +type metaLoad struct { + key metaKey // Meta key specifying what data to retrieve. + dreg uint8 // Number of the destination register. + + // Note: Similar to route, meta fields are stored AS IS. If the meta data is + // a field stored by the kernel (i.e. length), it is stored in host endian. On + // the contrary, if the meta data is data from the packet (i.e. protocol), it + // is stored in big endian (network order). + // The nft binary handles the necessary endian conversions from user input. + // For example, if the user wants to check if meta len == 123 vs payload + // data == 123, the nft binary passes host endian for the former and big + // endian for the latter. +} + +// metaKey is the key that determines the specific meta data to retrieve. +// Note: corresponds to enum nft_meta_keys from +// include/uapi/linux/netfilter/nf_tables.h and uses the same constants. +type metaKey int + +// metaKeyStrings is a map of meta key to its string representation. +var metaKeyStrings = map[metaKey]string{ + linux.NFT_META_LEN: "NFT_META_LEN", + linux.NFT_META_PROTOCOL: "NFT_META_PROTOCOL", + linux.NFT_META_PRIORITY: "NFT_META_PRIORITY", + linux.NFT_META_MARK: "NFT_META_MARK", + linux.NFT_META_IIF: "NFT_META_IIF", + linux.NFT_META_OIF: "NFT_META_OIF", + linux.NFT_META_IIFNAME: "NFT_META_IIFNAME", + linux.NFT_META_OIFNAME: "NFT_META_OIFNAME", + linux.NFT_META_IIFTYPE: "NFT_META_IIFTYPE", + linux.NFT_META_OIFTYPE: "NFT_META_OIFTYPE", + linux.NFT_META_SKUID: "NFT_META_SKUID", + linux.NFT_META_SKGID: "NFT_META_SKGID", + linux.NFT_META_NFTRACE: "NFT_META_NFTRACE", + linux.NFT_META_RTCLASSID: "NFT_META_RTCLASSID", + linux.NFT_META_SECMARK: "NFT_META_SECMARK", + linux.NFT_META_NFPROTO: "NFT_META_NFPROTO", + linux.NFT_META_L4PROTO: "NFT_META_L4PROTO", + linux.NFT_META_BRI_IIFNAME: "NFT_META_BRI_IIFNAME", + linux.NFT_META_BRI_OIFNAME: "NFT_META_BRI_OIFNAME", + linux.NFT_META_PKTTYPE: "NFT_META_PKTTYPE", + linux.NFT_META_CPU: "NFT_META_CPU", + linux.NFT_META_IIFGROUP: "NFT_META_IIFGROUP", + linux.NFT_META_OIFGROUP: "NFT_META_OIFGROUP", + linux.NFT_META_CGROUP: "NFT_META_CGROUP", + linux.NFT_META_PRANDOM: "NFT_META_PRANDOM", + linux.NFT_META_SECPATH: "NFT_META_SECPATH", + linux.NFT_META_IIFKIND: "NFT_META_IIFKIND", + linux.NFT_META_OIFKIND: "NFT_META_OIFKIND", + linux.NFT_META_BRI_IIFPVID: "NFT_META_BRI_IIFPVID", + linux.NFT_META_BRI_IIFVPROTO: "NFT_META_BRI_IIFVPROTO", + linux.NFT_META_TIME_NS: "NFT_META_TIME_NS", + linux.NFT_META_TIME_DAY: "NFT_META_TIME_DAY", + linux.NFT_META_TIME_HOUR: "NFT_META_TIME_HOUR", + linux.NFT_META_SDIF: "NFT_META_SDIF", + linux.NFT_META_SDIFNAME: "NFT_META_SDIFNAME", + linux.NFT_META_BRI_BROUTE: "NFT_META_BRI_BROUTE", +} + +// String for metaKey returns the string representation of the meta key. This +// supports strings for supported and unsupported meta keys. +func (key metaKey) String() string { + keyStr, ok := metaKeyStrings[key] + if !ok { + return fmt.Sprintf("Unsupported Meta Key: %d", int(key)) + } + return keyStr +} + +// metaDataLengths holds the length in bytes for each supported meta key. +var metaDataLengths = map[metaKey]int{ + linux.NFT_META_LEN: 4, + linux.NFT_META_PROTOCOL: 2, + linux.NFT_META_NFPROTO: 1, + linux.NFT_META_L4PROTO: 1, + linux.NFT_META_SKUID: 4, + linux.NFT_META_SKGID: 4, + linux.NFT_META_RTCLASSID: 4, + linux.NFT_META_PKTTYPE: 1, + linux.NFT_META_PRANDOM: 4, + linux.NFT_META_TIME_NS: 8, + linux.NFT_META_TIME_DAY: 1, + linux.NFT_META_TIME_HOUR: 4, +} + +// validateMetaKey ensures the meta key is valid. +func validateMetaKey(key metaKey) error { + switch key { + case linux.NFT_META_LEN, linux.NFT_META_PROTOCOL, linux.NFT_META_NFPROTO, + linux.NFT_META_L4PROTO, linux.NFT_META_SKUID, linux.NFT_META_SKGID, + linux.NFT_META_RTCLASSID, linux.NFT_META_PKTTYPE, linux.NFT_META_PRANDOM, + linux.NFT_META_TIME_NS, linux.NFT_META_TIME_DAY, linux.NFT_META_TIME_HOUR: + return nil + default: + return fmt.Errorf("invalid meta key: %d", int(key)) + } +} + +// newMetaLoad creates a new metaLoad operation. +func newMetaLoad(key metaKey, dreg uint8) (*metaLoad, error) { + if isVerdictRegister(dreg) { + return nil, fmt.Errorf("meta load operation cannot use verdict register as destination") + } + if err := validateMetaKey(key); err != nil { + return nil, err + } + if metaDataLengths[key] > 4 && !is16ByteRegister(dreg) { + return nil, fmt.Errorf("meta load operation cannot use 4-byte register as destination for key %s", key) + } + + return &metaLoad{key: key, dreg: dreg}, nil +} + +// evaluate for MetaLoad loads specific meta data into the destination register. +func (op metaLoad) evaluate(regs *registerSet, pkt *stack.PacketBuffer, rule *Rule) { + var target []byte + switch op.key { + + // Packet Length, in bytes (32-bit, host order). + case linux.NFT_META_LEN: + target = binary.NativeEndian.AppendUint32(nil, uint32(pkt.Size())) + + // Network EtherType Protocol (16-bit, network order). + case linux.NFT_META_PROTOCOL: + // Only valid if network header is present. + if pkt.NetworkHeader().View() == nil { + break + } + target = binary.BigEndian.AppendUint16(nil, uint16(pkt.NetworkProtocolNumber)) + + // Netfilter (Family) Protocol (8-bit, single byte). + case linux.NFT_META_NFPROTO: + family := rule.chain.GetAddressFamily() + target = []byte{family.Protocol()} + + // L4 Transport Layer Protocol (8-bit, single byte). + case linux.NFT_META_L4PROTO: + // Only valid if non-zero. + if pkt.TransportProtocolNumber == 0 { + break + } + target = []byte{uint8(pkt.TransportProtocolNumber)} + + // Originating Socket UID (32-bit, host order). + case linux.NFT_META_SKUID: + // Only valid if Owner is set (only set for locally generated packets). + if pkt.Owner == nil { + break + } + target = binary.NativeEndian.AppendUint32(nil, pkt.Owner.KUID()) + + // Originating Socket GID (32-bit, host order). + case linux.NFT_META_SKGID: + // Only valid if Owner is set (only set for locally generated packets). + if pkt.Owner == nil { + break + } + target = binary.NativeEndian.AppendUint32(nil, pkt.Owner.KGID()) + + // Route Traffic Class ID, same as Route equivalent (32-bit, host order). + // Currently only implemented for IPv6, but should be for IPv4 as well. + case linux.NFT_META_RTCLASSID: + if pkt.NetworkProtocolNumber != header.IPv6ProtocolNumber { + break + } + if pkt.NetworkHeader().View() != nil { + tcid, _ := pkt.Network().TOS() + target = binary.NativeEndian.AppendUint32(nil, uint32(tcid)) + } + + // Packet Type (8-bit, single byte). + case linux.NFT_META_PKTTYPE: + target = []byte{uint8(pkt.PktType)} + + // Generated Pseudo-Random Number (32-bit, network order). + case linux.NFT_META_PRANDOM: + rng := rule.chain.table.afFilter.nftState.rng + target = binary.BigEndian.AppendUint32(nil, uint32(rng.Uint32())) + + // Unix Time in Nanoseconds (64-bit, host order). + case linux.NFT_META_TIME_NS: + clock := rule.chain.table.afFilter.nftState.clock + target = binary.NativeEndian.AppendUint64(nil, uint64(clock.Now().UnixNano())) + + // Day of Week (0 = Sunday, 6 = Saturday) (8-bit, single byte). + case linux.NFT_META_TIME_DAY: + clock := rule.chain.table.afFilter.nftState.clock + target = []byte{uint8(clock.Now().Weekday())} + + // Hour of Day, in seconds (seconds since start of day) (32-bit, host order). + case linux.NFT_META_TIME_HOUR: + clock := rule.chain.table.afFilter.nftState.clock + now := clock.Now() + secs := now.Hour()*3600 + now.Minute()*60 + now.Second() + target = binary.NativeEndian.AppendUint32(nil, uint32(secs)) + } + + // Breaks if could not retrieve meta data. + if target == nil { + regs.verdict = Verdict{Code: VC(linux.NFT_BREAK)} + return + } + + // Gets the destination register. + dst := getRegisterBuffer(regs, op.dreg) + // Zeroes out excess bytes of the destination register. + // This is done since comparison can be done in multiples of 4 bytes. + blen := metaDataLengths[op.key] + if rem := blen % 4; rem != 0 { + clear(dst[blen : blen+4-rem]) + } + // Copies target data into the destination register. + copy(dst, target) +} + // // Register and Register-Related Implementations. // Note: Registers are represented by type uint8 for the register number. @@ -1912,11 +2153,16 @@ func (r *Rule) evaluate(regs *registerSet, pkt *stack.PacketBuffer) error { // NewNFTables creates a new NFTables state object using the given clock for // timing operations. -func NewNFTables(clock tcpip.Clock) *NFTables { +// Note: Expects random number generator to be initialized with a seed. +// TODO(b/345684870): Use a secure RNG. +func NewNFTables(clock tcpip.Clock, rng *rand.Rand) *NFTables { if clock == nil { panic("nftables state must be initialized with a non-nil clock") } - return &NFTables{clock: clock, startTime: clock.Now()} + if rng == nil { + panic("nftables state must be initialized with a non-nil random number generator") + } + return &NFTables{clock: clock, startTime: clock.Now(), rng: rng} } // Flush clears entire ruleset and all data for all address families. diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index ff2809207..b695c51e4 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -17,6 +17,7 @@ package nftables import ( "encoding/binary" "fmt" + "math/rand" "reflect" "slices" "testing" @@ -125,6 +126,13 @@ const ( tcpChecksumLen = 2 tcpUrgPtrOffset = 18 tcpUrgPtrLen = 2 + + // Arbitrary Socket IDs + arbitrarySKUID = 0x020304 + arbitrarySKGID = 45668 + + // Arbitrary Packet Type + arbitraryPktType = tcpip.PacketOutgoing ) var ( @@ -229,6 +237,9 @@ func makeIPv4Packet(reserved int, ipv4Fields *header.IPv4Fields) *stack.PacketBu // 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) @@ -251,6 +262,9 @@ func makeIPv6Packet(reserved int, ipv6Fields *header.IPv6Fields) *stack.PacketBu // 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) @@ -2401,7 +2415,8 @@ func TestEvaluateLast(t *testing.T) { // Sets up an NFTables object with a base chain and fake manual clock. fakeClock := faketime.NewManualClock() - nf := NewNFTables(fakeClock) + fixedRng := rand.New(rand.NewSource(0)) + nf := NewNFTables(fakeClock, fixedRng) tab, err := nf.AddTable(arbitraryFamily, "test", "test table", false) if err != nil { t.Fatalf("unexpected error for AddTable: %v", err) @@ -2813,6 +2828,196 @@ func TestEvaluateByteorder(t *testing.T) { } } +// 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.New(rand.NewSource(0)) + 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)), + }, + { // cmd: add rule ip6 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{IP6.Protocol(), 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(fakeClock, rand.New(rand.NewSource(0))) + + 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) { @@ -3411,7 +3616,9 @@ func packetResultString(initial, final *stack.PacketBuffer) string { // newNFTablesStd creates a new NFTables object w/ a standard clock for testing. func newNFTablesStd() *NFTables { - return NewNFTables(tcpip.NewStdClock()) + stdClock := tcpip.NewStdClock() + fixedRng := rand.New(rand.NewSource(0)) + return NewNFTables(stdClock, fixedRng) } // mustCreateImmediate wraps the newImmediate function for brevity. @@ -3494,3 +3701,12 @@ func mustCreateByteorder(t *testing.T, sreg, dreg uint8, bop byteorderOp, blen, } 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 +} diff --git a/pkg/tcpip/nftables/nftinterp.go b/pkg/tcpip/nftables/nftinterp.go index 5d5bbc70a..002bff087 100644 --- a/pkg/tcpip/nftables/nftinterp.go +++ b/pkg/tcpip/nftables/nftinterp.go @@ -157,6 +157,8 @@ func InterpretOperation(line string, lnIdx int) (operation, error) { return InterpretRoute(line, lnIdx) case "byteorder": return InterpretByteorder(line, lnIdx) + case "meta": + return InterpretMetaLoad(line, lnIdx) default: return nil, &SyntaxError{lnIdx, 1, fmt.Sprintf("unrecognized operation type: %s", tokens[1])} } @@ -818,6 +820,69 @@ func InterpretByteorder(line string, lnIdx int) (operation, error) { return order, nil } +// InterpretMetaLoad creates a new MetaLoad operation from the given string. +func InterpretMetaLoad(line string, lnIdx int) (operation, error) { + tokens := strings.Fields(line) + + // Requires exactly 8 tokens: + // "[", "meta", "load", meta key, "=>", "reg", register index, "]". + if len(tokens) != 8 { + return nil, &SyntaxError{lnIdx, 0, fmt.Sprintf("incorrect number of tokens for meta operation, should be exactly 8, got %d", len(tokens))} + } + + if err := checkOperationBrackets(tokens, lnIdx); err != nil { + return nil, err + } + + tkIdx := 1 + + // First token should be "meta". + if err := consumeToken("meta", 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 meta key. + key, err := parseMetaKey(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 "reg". + if err := consumeToken("reg", tokens, lnIdx, tkIdx); err != nil { + return nil, err + } + tkIdx++ + + // Sixth 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. + mtLoad, err := newMetaLoad(key, reg) + if err != nil { + return nil, &LogicError{lnIdx, tkIdx, err} + } + + return mtLoad, nil +} + // // Interpreter Helper Functions. // @@ -978,7 +1043,7 @@ func parseCmpOp(copString string, lnIdx int, tkIdx int) (int, error) { case "gte": return linux.NFT_CMP_GTE, nil default: - return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid comparison operator: '%s'", copString)} + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid comparison operator keyword: '%s'", copString)} } } @@ -1008,7 +1073,7 @@ func parsePayloadBase(baseString string, lnIdx int, tkIdx int) (payloadBase, err 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)} + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid payload base keyword: '%s'", baseString)} } } @@ -1033,6 +1098,53 @@ func parseRouteKey(keyString string, lnIdx int, tkIdx int) (routeKey, error) { } } +// metaKeyFromKeyword is a map of meta key keywords to their corresponding enum value. +var metaKeyFromKeyword = map[string]metaKey{ + // Supported meta keys. + "len": linux.NFT_META_LEN, + "protocol": linux.NFT_META_PROTOCOL, + "nfproto": linux.NFT_META_NFPROTO, + "l4proto": linux.NFT_META_L4PROTO, + "skuid": linux.NFT_META_SKUID, + "skgid": linux.NFT_META_SKGID, + "rtclassid": linux.NFT_META_RTCLASSID, + "pkttype": linux.NFT_META_PKTTYPE, + "prandom": linux.NFT_META_PRANDOM, + "time": linux.NFT_META_TIME_NS, + "day": linux.NFT_META_TIME_DAY, + "hour": linux.NFT_META_TIME_HOUR, + // Unsupported meta keys. + "priority": linux.NFT_META_PRIORITY, + "mark": linux.NFT_META_MARK, + "iif": linux.NFT_META_IIF, + "oif": linux.NFT_META_OIF, + "iifname": linux.NFT_META_IIFNAME, + "oifname": linux.NFT_META_OIFNAME, + "iiftype": linux.NFT_META_IIFTYPE, + "oiftype": linux.NFT_META_OIFTYPE, + "iifgroup": linux.NFT_META_IIFGROUP, + "oifgroup": linux.NFT_META_OIFGROUP, + "cgroup": linux.NFT_META_CGROUP, + "iifkind": linux.NFT_META_IIFKIND, + "oifkind": linux.NFT_META_OIFKIND, + "sdif": linux.NFT_META_SDIF, + "sdifname": linux.NFT_META_SDIFNAME, + "nftrace": linux.NFT_META_NFTRACE, + "cpu": linux.NFT_META_CPU, + "secmark": linux.NFT_META_SECMARK, + "secpath": linux.NFT_META_SECPATH, + "broute": linux.NFT_META_BRI_BROUTE, +} + +// parseMetaKey parses the meta key from the given string. +func parseMetaKey(keyString string, lnIdx int, tkIdx int) (metaKey, error) { + key, ok := metaKeyFromKeyword[keyString] + if !ok { + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid meta key keyword: '%s'", keyString)} + } + return key, nil +} + // 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 625b4f92d..f12660df4 100644 --- a/pkg/tcpip/nftables/nftinterp_test.go +++ b/pkg/tcpip/nftables/nftinterp_test.go @@ -957,6 +957,101 @@ func checkByteorderOp(tname string, expected operation, actual operation) error return nil } +// TestInterpretMetaLoadOps tests interpretation of meta load operations. +func TestInterpretMetaLoadOps(t *testing.T) { + for _, test := range []interpretOperationTestAction{ + { // cmd: add rule ip tab ch meta length 0x01020304 + tname: "meta load len test", + opStr: "[ meta load len => reg 2 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_LEN, linux.NFT_REG_2), + }, + { // cmd: add rule inet tab ch meta protocol 0x0102 + tname: "meta load protocol test", + opStr: "[ meta load protocol => reg 3 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_PROTOCOL, linux.NFT_REG_3), + }, + { // cmd: add rule inet tab ch meta nfproto 253 + tname: "meta load nfproto test", + opStr: "[ meta load nfproto => reg 4 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_NFPROTO, linux.NFT_REG_4), + }, + { // cmd: add rule inet tab ch meta l4proto 0x17 + tname: "meta load l4proto test", + opStr: "[ meta load l4proto => reg 8 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_L4PROTO, linux.NFT_REG32_00), + }, + { // cmd: add rule inet tab ch skuid 0x09080706 + tname: "meta load skuid test", + opStr: "[ meta load skuid => reg 10 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_SKUID, linux.NFT_REG32_02), + }, + { // cmd: add rule inet tab ch meta skgid 0x09080706 + tname: "meta load skgid test", + opStr: "[ meta load skgid => reg 11 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_SKGID, linux.NFT_REG32_03), + }, + { // cmd: add rule inet tab ch rtclassid 0x01020304 + tname: "meta load rtclassid test", + opStr: "[ meta load rtclassid => reg 12 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_RTCLASSID, linux.NFT_REG32_04), + }, + { // cmd: add rule inet tab ch pkttype 0x59 + tname: "meta load pkttype test", + opStr: "[ meta load pkttype => reg 13 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_PKTTYPE, linux.NFT_REG32_05), + }, + { // cmd: add rule inet tab ch meta random 0x02040608 + tname: "meta load prandom test", + opStr: "[ meta load prandom => reg 9 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_PRANDOM, linux.NFT_REG32_01), + }, + { // cmd: add rule inet tab ch time "2020-06-06 17:00" + tname: "meta load arbitrary time test", + opStr: "[ meta load time => reg 4 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_NS, linux.NFT_REG_4), + }, + { // cmd: add rule inet tab ch time "1970-01-01 00:00:01" + tname: "meta load time 1 sec after unix epoch test", + opStr: "[ meta load time => reg 3 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_NS, linux.NFT_REG_3), + }, + { // cmd: add rule inet tab ch time "1969-01-01 00:00:00" + tname: "meta load time 1 year before unix epoch test", + opStr: "[ meta load time => reg 2 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_NS, linux.NFT_REG_2), + }, + { // cmd: add rule inet tab ch day Monday + tname: "meta load day test", + opStr: "[ meta load day => reg 23 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_DAY, linux.NFT_REG32_15), + }, + { // cmd: add rule inet tab ch hour 0x01020304 + tname: "meta load hour test", + opStr: "[ meta load hour => reg 22 ]", + expected: mustCreateMetaLoad(t, linux.NFT_META_TIME_HOUR, linux.NFT_REG32_14), + }, + } { + t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkMetaLoadOp) }) + } +} + +// checkMetaLoadOp checks that the given operation is a meta load operation and +// that it matches the expected meta load operation. +func checkMetaLoadOp(tname string, expected operation, actual operation) error { + expectedMtLoad := expected.(*metaLoad) + mtLoad, ok := actual.(*metaLoad) + if !ok { + return fmt.Errorf("expected operation type to be MetaLoad for %s, got %T", tname, actual) + } + if mtLoad.key != expectedMtLoad.key { + return fmt.Errorf("expected meta key to be %v for %s, got %v", expectedMtLoad.key, tname, mtLoad.key) + } + if mtLoad.dreg != expectedMtLoad.dreg { + return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedMtLoad.dreg, tname, mtLoad.dreg) + } + return nil +} + // TestInterpretRule tests the interpretation of basic and general rules as a // list of operations. func TestInterpretRule(t *testing.T) {