From 684a1c866521e3cb48714d7145019e61cf79ec44 Mon Sep 17 00:00:00 2001 From: Jayden Nyamiaka Date: Thu, 12 Sep 2024 16:00:50 -0700 Subject: [PATCH] Implement Route operation (parsing, interpretation, evaluation, tests). Out of the 5 route keys, we currently support all but IPsec/XRFM key. However, classid key is only supported for IPv6 (needs support for IPv4 too). Also removes repetition in validatePayloadBase & String for payloadBase. PiperOrigin-RevId: 674044789 --- pkg/tcpip/nftables/nftables.go | 142 ++++++++++++++++++++++++--- pkg/tcpip/nftables/nftables_test.go | 120 ++++++++++++++++++++++ pkg/tcpip/nftables/nftinterp.go | 86 ++++++++++++++++ pkg/tcpip/nftables/nftinterp_test.go | 76 ++++++++++++++ 4 files changed, 411 insertions(+), 13 deletions(-) diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index 63971a264..dcb1cd6d6 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -592,6 +592,7 @@ var ( _ operation = (*bitwise)(nil) _ operation = (*counter)(nil) _ operation = (*last)(nil) + _ operation = (*route)(nil) ) // immediate is an operation that sets the data in a register. @@ -660,7 +661,7 @@ func validateComparisonOp(cop cmpOp) error { // newComparison creates a new Comparison operation. func newComparison(sreg uint8, op int, data []byte) (*comparison, error) { - if sreg == linux.NFT_REG_VERDICT { + if isVerdictRegister(sreg) { return nil, fmt.Errorf("comparison operation cannot use verdict register as source") } bytesData := newBytesData(data) @@ -754,7 +755,7 @@ func validateRangeOp(rop rngOp) error { // newRanged creates a new Ranged operation. func newRanged(sreg uint8, op int, low, high []byte) (*ranged, error) { - if sreg == linux.NFT_REG_VERDICT { + if isVerdictRegister(sreg) { return nil, fmt.Errorf("comparison operation cannot use verdict register as source") } if len(low) != len(high) { @@ -810,9 +811,13 @@ type payloadLoad struct { // 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. +// String for payloadBase returns the string representation of the payload base. func (base payloadBase) String() string { + // Uses errors from validation to handle unsupported payload bases. + if err := validatePayloadBase(base); err != nil { + panic(err) + } + // Cases for supported payload bases. switch base { case linux.NFT_PAYLOAD_LL_HEADER: return "Link Layer Header" @@ -820,20 +825,18 @@ func (base payloadBase) String() string { 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))) + return fmt.Sprintf("Unknown Supported Payload Base: %d", int(base)) } } // validatePayloadBase ensures the payload base is valid. func validatePayloadBase(base payloadBase) error { switch base { + // Supported payload bases. case linux.NFT_PAYLOAD_LL_HEADER, linux.NFT_PAYLOAD_NETWORK_HEADER, linux.NFT_PAYLOAD_TRANSPORT_HEADER: return nil + // Unsupported payload bases. case linux.NFT_PAYLOAD_INNER_HEADER: return fmt.Errorf("inner header not supported") case linux.NFT_PAYLOAD_TUN_HEADER: @@ -871,7 +874,7 @@ func getPayloadBuffer(pkt *stack.PacketBuffer, base payloadBase) []byte { // newPayloadLoad creates a new PayloadLoad operation. func newPayloadLoad(base payloadBase, offset, blen, dreg uint8) (*payloadLoad, error) { - if dreg == linux.NFT_REG_VERDICT { + if isVerdictRegister(dreg) { return nil, fmt.Errorf("payload load operation cannot use verdict register as destination") } if blen > 16 || (blen > 4 && is4ByteRegister(dreg)) { @@ -944,7 +947,7 @@ func validateChecksumType(csumType uint8) error { // newPayloadSet creates a new PayloadSet operation. func newPayloadSet(base payloadBase, offset, blen, sreg, csumType, csumOffset, csumFlags uint8) (*payloadSet, error) { - if sreg == linux.NFT_REG_VERDICT { + if isVerdictRegister(sreg) { return nil, fmt.Errorf("payload set operation cannot use verdict register as destination") } if blen > 16 || (blen > 4 && is4ByteRegister(sreg)) { @@ -1100,7 +1103,7 @@ type bitwise struct { // newBitwiseBool creates a new bitwise boolean operation. func newBitwiseBool(sreg, dreg uint8, mask, xor []byte) (*bitwise, error) { - if sreg == linux.NFT_REG_VERDICT || dreg == linux.NFT_REG_VERDICT { + if isVerdictRegister(sreg) || isVerdictRegister(dreg) { return nil, fmt.Errorf("bitwise operation cannot use verdict register as source or destination") } blen := len(mask) @@ -1115,7 +1118,7 @@ func newBitwiseBool(sreg, dreg uint8, mask, xor []byte) (*bitwise, error) { // newBitwiseShift creates a new bitwise shift operation. func newBitwiseShift(sreg, dreg, blen uint8, shift uint32, right bool) (*bitwise, error) { - if sreg == linux.NFT_REG_VERDICT || dreg == linux.NFT_REG_VERDICT { + if isVerdictRegister(sreg) || isVerdictRegister(dreg) { return nil, fmt.Errorf("bitwise operation cannot use verdict register as source or destination") } if blen > 16 || (blen > 4 && (is4ByteRegister(sreg) || is4ByteRegister(dreg))) { @@ -1273,6 +1276,119 @@ func (op *last) evaluate(regs *registerSet, pkt *stack.PacketBuffer, rule *Rule) op.set.CompareAndSwap(false, true) } +// route is an operation that loads specific route data into a register. +// Note: route operations are not supported for the verdict register. +type route struct { + key routeKey // Route key specifying what data to retrieve. + dreg uint8 // Number of the destination register. + + // Route information is stored AS IS. If the data is a field stored by the + // kernel, it is stored in host endian. If the data is from the packet, 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 some kernel data == 123 vs + // payload data == 123, the nft binary passes host endian register data for + // the former and big endian register data for the latter. +} + +// routeKey is the key that determines the specific route data to retrieve. +// Note: corresponds to enum nft_rt_keys from +// include/uapi/linux/netfilter/nf_tables.h and uses the same constants. +type routeKey int + +// String for routeKey returns the string representation of the route key. +func (key routeKey) String() string { + // Uses errors from validation to handle unsupported route keys. + if err := validateRouteKey(key); err != nil { + panic(err) + } + // Cases for supported route keys. + switch key { + case linux.NFT_RT_NEXTHOP4: + return "Next Hop IPv4" + case linux.NFT_RT_NEXTHOP6: + return "Next Hop IPv6" + case linux.NFT_RT_TCPMSS: + return "TCP Maximum Segment Size (TCPMSS)" + default: + return fmt.Sprintf("Unknown Supported Route Key: %d", int(key)) + } +} + +// validateRouteKey ensures the route key is valid. +func validateRouteKey(key routeKey) error { + switch key { + // Supported route keys. + case linux.NFT_RT_NEXTHOP4, linux.NFT_RT_NEXTHOP6, linux.NFT_RT_TCPMSS: + return nil + // Unsupported route keys. + case linux.NFT_RT_CLASSID: + // Note: We can trivially support Traffic Class ID for IPv6, but we need to + // do more work to support it for IPv4. For safety, we mark it as + // unsupported since we don't know what packet type we're working with until + // the time of evaluation. In the worst case, we don't want the user to + // initialize a route with this key and then have it silently break and + // yield a difficult-to-debug error. + return fmt.Errorf("traffic class id not supported") + case linux.NFT_RT_XFRM: + return fmt.Errorf("xfrm transformation not supported") + default: + return fmt.Errorf("invalid route key: %d", int(key)) + } +} + +// newRoute creates a new route operation. +func newRoute(key routeKey, dreg uint8) (*route, error) { + if isVerdictRegister(dreg) { + return nil, fmt.Errorf("route operation cannot use verdict register as destination") + } + if err := validateRouteKey(key); err != nil { + return nil, err + } + + return &route{key: key, dreg: dreg}, nil +} + +// evaluate for Route loads specific routing data into the destination register. +func (op route) evaluate(regs *registerSet, pkt *stack.PacketBuffer, rule *Rule) { + // Gets the target data to be stored in the destination register. + var target []byte + switch op.key { + + // Retrieves next hop IPv4 address (restricted to IPv4). + // Stores data in big endian network order. + case linux.NFT_RT_NEXTHOP4: + if pkt.NetworkProtocolNumber != header.IPv4ProtocolNumber { + break + } + target = pkt.EgressRoute.NextHop.AsSlice() + + // Retrieves next hop IPv6 address (restricted to IPv6). + // Stores data in big endian network order. + case linux.NFT_RT_NEXTHOP6: + if pkt.NetworkProtocolNumber != header.IPv6ProtocolNumber { + break + } + target = pkt.EgressRoute.NextHop.AsSlice() + + // Retrieves the TCP Maximum Segment Size (TCPMSS). + // Stores data in host endian. + case linux.NFT_RT_TCPMSS: + tcpmss := pkt.GSOOptions.MSS + target = binary.NativeEndian.AppendUint16(nil, tcpmss) + } + + // Breaks if could not retrieve target data. + if target == nil { + regs.verdict = Verdict{Code: VC(linux.NFT_BREAK)} + return + } + + // Stores the target data in the destination register. + data := newBytesData(target) + data.storeData(regs, op.dreg) +} + // // Register and Register-Related Implementations. // Note: Registers are represented by type uint8 for the register number. diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index 417b3ba47..0394e0d39 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -2459,6 +2459,117 @@ func TestEvaluateLast(t *testing.T) { }) } +// TestEvaluateRoute tests that the Route operation correctly loads the specific +// route data into into the destination register. +// The nft binary commands used to generate these are stated above each test. +// Also note that all these commands mirror the ones in TestInterpretRouteOps. +// All commands should be preceded by nft --debug=netlink. +// Note: Relies on expected behavior of the Comparison operation. +func TestEvaluateRoute(t *testing.T) { + for _, test := range []struct { + tname string + pkt *stack.PacketBuffer + op1 operation // Route operation to test. + op2 operation // Comparison operation to check resulting data in register, + }{ + // IPv4 Next Hop Commands + { // cmd: add rule ip filter output rt nexthop 192.168.1.1 + tname: "load nexthop4 key to 4-byte register", + pkt: func() *stack.PacketBuffer { + pkt := makeIPv4Packet(header.IPv6MinimumSize, arbitraryIPv4Fields()) + pkt.EgressRoute.NextHop = tcpip.AddrFrom4(arbitraryIPv4AddrB) + return pkt + }(), + op1: mustCreateRoute(t, linux.NFT_RT_NEXTHOP4, linux.NFT_REG32_06), + op2: mustCreateComparison(t, linux.NFT_REG32_06, linux.NFT_CMP_EQ, arbitraryIPv4AddrB[:]), + }, + { // cmd: add rule ip filter output rt nexthop 192.168.1.9 + tname: "load nexthop4 key to 16-byte register", + pkt: func() *stack.PacketBuffer { + pkt := makeIPv4Packet(header.IPv6MinimumSize, arbitraryIPv4Fields()) + pkt.EgressRoute.NextHop = tcpip.AddrFrom4(arbitraryIPv4AddrB2) + return pkt + }(), + op1: mustCreateRoute(t, linux.NFT_RT_NEXTHOP4, linux.NFT_REG_3), + op2: mustCreateComparison(t, linux.NFT_REG_3, linux.NFT_CMP_EQ, arbitraryIPv4AddrB2[:]), + }, + // IPv6 Next Hop Commands + { // cmd: add rule ip filter output rt nexthop 2001:db8:85a3::aa + tname: "load nexthop6 key to 16-byte register", + pkt: func() *stack.PacketBuffer { + pkt := makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields()) + pkt.EgressRoute.NextHop = tcpip.AddrFrom16(arbitraryIPv6AddrB) + return pkt + }(), + op1: mustCreateRoute(t, linux.NFT_RT_NEXTHOP6, linux.NFT_REG_1), + op2: mustCreateComparison(t, linux.NFT_REG_1, linux.NFT_CMP_EQ, arbitraryIPv6AddrB[:]), + }, + // TCP Maximum Segment Size Commands + { // cmd: add rule ip filter output rt mtu 1500 + tname: "load tcpmss key to 4-byte register", + pkt: func() *stack.PacketBuffer { + pkt := makeIPv4Packet(header.IPv6MinimumSize, arbitraryIPv4Fields()) + pkt.GSOOptions.MSS = 1500 + return pkt + }(), + op1: mustCreateRoute(t, linux.NFT_RT_TCPMSS, linux.NFT_REG32_00), + op2: mustCreateComparison(t, linux.NFT_REG32_00, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint16(nil, 1500)), + }, + { // cmd: add rule ip filter output rt mtu 0x0102 + tname: "load tcpmss key to 16-byte register", + pkt: func() *stack.PacketBuffer { + pkt := makeIPv6Packet(header.IPv6MinimumSize, arbitraryIPv6Fields()) + pkt.GSOOptions.MSS = 0x0102 + return pkt + }(), + op1: mustCreateRoute(t, linux.NFT_RT_TCPMSS, linux.NFT_REG_4), + op2: mustCreateComparison(t, linux.NFT_REG_4, linux.NFT_CMP_EQ, binary.NativeEndian.AppendUint16(nil, 0x0102)), + }, + } { + t.Run(test.tname, func(t *testing.T) { + // Sets up an NFTables object with a single table, chain, and rule. + nf := newNFTablesStd() + tab, err := nf.AddTable(arbitraryFamily, "test", "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 (should be Drop if comparisons are true). + if v.Code != VC(linux.NF_DROP) { + t.Fatalf("expected verdict Drop for true comparison, got %v", v) + } + }) + } +} + // TestLoopCheckOnRegisterAndUnregister tests the loop checking and accompanying // logic on registering and unregistering rules. func TestLoopCheckOnRegisterAndUnregister(t *testing.T) { @@ -3122,3 +3233,12 @@ func mustCreateBitwiseShift(t *testing.T, sreg, dreg, blen uint8, shift uint32, } return bit } + +// mustCreateRoute wraps the newRoute function for brevity. +func mustCreateRoute(t *testing.T, key routeKey, dreg uint8) *route { + rt, err := newRoute(key, dreg) + if err != nil { + t.Fatalf("failed to create route: %v", err) + } + return rt +} diff --git a/pkg/tcpip/nftables/nftinterp.go b/pkg/tcpip/nftables/nftinterp.go index 2169c98fb..8be3882c8 100644 --- a/pkg/tcpip/nftables/nftinterp.go +++ b/pkg/tcpip/nftables/nftinterp.go @@ -153,6 +153,8 @@ func InterpretOperation(line string, lnIdx int) (operation, error) { return InterpretBitwiseBool(line, lnIdx) case "counter": return InterpretCounter(line, lnIdx) + case "rt": + return InterpretRoute(line, lnIdx) default: return nil, &SyntaxError{lnIdx, 1, fmt.Sprintf("unrecognized operation type: %s", tokens[1])} } @@ -668,6 +670,69 @@ func InterpretCounter(line string, lnIdx int) (operation, error) { return cntr, nil } +// InterpretRoute creates a new Route operation from the given string. +func InterpretRoute(line string, lnIdx int) (operation, error) { + tokens := strings.Fields(line) + + // Requires exactly 8 tokens: + // "[", "rt", "load", route key, "=>", "reg", register index, "]". + if len(tokens) != 8 { + return nil, &SyntaxError{lnIdx, 0, fmt.Sprintf("incorrect number of tokens for route 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 "rt". + if err := consumeToken("rt", 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 route key. + key, err := parseRouteKey(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. + rt, err := newRoute(key, reg) + if err != nil { + return nil, &LogicError{lnIdx, tkIdx, err} + } + + return rt, nil +} + // // Interpreter Helper Functions. // @@ -864,6 +929,27 @@ func parsePayloadBase(baseString string, lnIdx int, tkIdx int) (payloadBase, err } } +// parseRouteKey parses the route key from the given string. +func parseRouteKey(keyString string, lnIdx int, tkIdx int) (routeKey, error) { + switch keyString { + // Fully supported route keys. + case "nexthop4": + return linux.NFT_RT_NEXTHOP4, nil + case "nexthop6": + return linux.NFT_RT_NEXTHOP6, nil + case "tcpmss": + return linux.NFT_RT_TCPMSS, nil + // Keys supported for interpretation but not yet for logic/evaluation. + // Note: Will result in logic error during operation construction. + case "classid": + return linux.NFT_RT_CLASSID, nil + case "ipsec": + return linux.NFT_RT_XFRM, nil + default: + return 0, &SyntaxError{lnIdx, tkIdx, fmt.Sprintf("invalid route key keyword: '%s'", keyString)} + } +} + // 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 d476caf28..98253576b 100644 --- a/pkg/tcpip/nftables/nftinterp_test.go +++ b/pkg/tcpip/nftables/nftinterp_test.go @@ -808,6 +808,82 @@ func checkCounterOp(tname string, expected operation, actual operation) error { return nil } +// TestInterpretRouteOps tests interpretation of route operations. +func TestInterpretRouteOps(t *testing.T) { + for _, test := range []interpretOperationTestAction{ + { // cmd: add rule ip filter output rt nexthop 192.168.1.1 + tname: "load nexthop4 key to 4-byte register", + opStr: "[ rt load nexthop4 => reg 14 ]", + expected: mustCreateRoute(t, linux.NFT_RT_NEXTHOP4, linux.NFT_REG32_06), + }, + { // cmd: add rule ip filter output rt nexthop 192.168.1.9 + tname: "load nexthop4 key to 4-byte register", + opStr: "[ rt load nexthop4 => reg 3 ]", + expected: mustCreateRoute(t, linux.NFT_RT_NEXTHOP4, linux.NFT_REG_3), + }, + { // cmd: add rule ip6 filter output rt nexthop 2001:db8:85a3::aa + tname: "load nexthop6 key to 16-byte register", + opStr: "[ rt load nexthop6 => reg 1 ]", + expected: mustCreateRoute(t, linux.NFT_RT_NEXTHOP6, linux.NFT_REG_1), + }, + { // cmd: add rule ip filter output rt mtu 1500 + tname: "load tcpmss key to 4-byte register", + opStr: "[ rt load tcpmss => reg 8 ]", + expected: mustCreateRoute(t, linux.NFT_RT_TCPMSS, linux.NFT_REG32_00), + }, + { // cmd: add rule ip filter output rt mtu 0x0102 + tname: "load tcpmss key to 16-byte register", + opStr: "[ rt load tcpmss => reg 4 ]", + expected: mustCreateRoute(t, linux.NFT_RT_TCPMSS, linux.NFT_REG_4), + }, + // Result in errors. + { // cmd: add rule ip filter output rt classid 0x05 + tname: "unsupported route key classid", + opStr: "[ rt load classid => reg 10 ]", + expected: nil, + }, + { // cmd: add rule ip filter output rt ipsec exists + tname: "unsupported route key ipsec", + opStr: "[ rt load ipsec => reg 1 ]", + expected: nil, + }, + { + tname: "invalid route key keyword", + opStr: "[ rt load xrfm => reg 1 ]", + expected: nil, + }, + { + tname: "too few tokens for route operation", + opStr: "[ rt nexthop6 => reg 1 ]", + expected: nil, + }, + { + tname: "too many tokens for route operation", + opStr: "[ rt load tcpmss => reg 4 -> reg 5 ]", + expected: nil, + }, + } { + t.Run(test.tname, func(t *testing.T) { checkOp(t, test, checkRouteOp) }) + } +} + +// checkRouteOp checks that the given operation is a route operation and +// that it matches the expected route operation. +func checkRouteOp(tname string, expected operation, actual operation) error { + expectedRt := expected.(*route) + rt, ok := actual.(*route) + if !ok { + return fmt.Errorf("expected operation type to be Route for %s, got %T", tname, actual) + } + if rt.key != expectedRt.key { + return fmt.Errorf("expected route key to be %v for %s, got %v", expectedRt.key, tname, rt.key) + } + if rt.dreg != expectedRt.dreg { + return fmt.Errorf("expected destination register to be %d for %s, got %d", expectedRt.dreg, tname, rt.dreg) + } + return nil +} + // TestInterpretRule tests the interpretation of basic and general rules as a // list of operations. func TestInterpretRule(t *testing.T) {