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
This commit is contained in:
Jayden Nyamiaka
2024-09-12 16:03:41 -07:00
committed by gVisor bot
parent b050c045d1
commit 684a1c8665
4 changed files with 411 additions and 13 deletions
+129 -13
View File
@@ -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.
+120
View File
@@ -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
}
+86
View File
@@ -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 {
+76
View File
@@ -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) {