From 8b330fe8b78886b073368de92456ca9900a2a6a3 Mon Sep 17 00:00:00 2001 From: "G. Paul Ziemba" Date: Wed, 19 Jul 2023 07:49:18 -0700 Subject: [PATCH] pbrd: add vlan filters pcp/vlan-id/vlan-flags; ip-protocol any (doc, tests) Subset: doc and tests doc PBR section updated with new fields and some copy-editing tests pbr_topo1: ensure new vlan fields arrive at zebra Changes by: Josh Werner Eli Baum G. Paul Ziemba Signed-off-by: G. Paul Ziemba --- doc/user/pbr.rst | 203 ++++++++++++-------- tests/topotests/pbr_topo1/test_pbr_topo1.py | 189 ++++++++++++++---- 2 files changed, 272 insertions(+), 120 deletions(-) diff --git a/doc/user/pbr.rst b/doc/user/pbr.rst index 0cdb206dd5..17f3504898 100644 --- a/doc/user/pbr.rst +++ b/doc/user/pbr.rst @@ -4,10 +4,11 @@ PBR *** -:abbr:`PBR` is Policy Based Routing. This implementation supports a very simple -interface to allow admins to influence routing on their router. At this time -you can only match on destination and source prefixes for an incoming interface. -At this point in time, this implementation will only work on Linux. +:abbr:`PBR` is Policy Based Routing, which means forwarding based on +packet fields other than solely the destination IP address. +This implementation currently works only on Linux. Note that some +functionality (VLAN matching, packet mangling) is not supported by +the default Linux kernel dataplane provider. .. _starting-pbr: @@ -17,12 +18,12 @@ Starting PBR Default configuration file for *pbrd* is :file:`pbrd.conf`. The typical location of :file:`pbrd.conf` is |INSTALL_PREFIX_ETC|/pbrd.conf. -If the user is using integrated config, then :file:`pbrd.conf` need not be +If FRR is using integrated config, then :file:`pbrd.conf` need not be present and the :file:`frr.conf` is read instead. .. program:: pbrd -:abbr:`PBR` supports all the common FRR daemon start options which are +:abbr:`PBR` supports all the common FRR daemon start options, which are documented elsewhere. .. _nexthop-groups: @@ -30,9 +31,9 @@ documented elsewhere. Nexthop Groups ============== -Nexthop groups are a way to encapsulate ECMP information together. It's a -listing of ECMP nexthops used to forward packets for when a pbr-map is matched. -For detailed instructions on how to specify a nexthop group on the CLI, see +A nexthop group is a list of ECMP nexthops used to forward packets +when a pbr-map is matched. +For details on specifying a nexthop group in the CLI, see the nexthop-groups section. Showing Nexthop Group Information @@ -42,7 +43,7 @@ Showing Nexthop Group Information Display information on a PBR nexthop-group. If ``NAME`` is omitted, all nexthop groups are shown. Setting ``json`` will provide the same - information in an array of objects which obey the schema below: + information in an array of objects that adhere to the schema below: +-----------+----------------------------+---------+ | Key | Description | Type | @@ -74,118 +75,152 @@ Showing Nexthop Group Information PBR Maps ======== -PBR maps are a way to group policies that we would like to apply to individual -interfaces. These policies when applied are matched against incoming packets. -If matched the nexthop-group or nexthop is used to forward the packets to the -end destination. +PBR maps are a way to specify a set of rules that are applied to +packets received on individual interfaces. +If a received packet matches a rule, the rule's nexthop-group or +nexthop is used to forward it; any other actions +specified in the rule are also applied to the packet. .. clicmd:: pbr-map NAME seq (1-700) - Create a pbr-map with NAME and sequence number specified. This command puts - you into a new submode for pbr-map specification. To exit this mode type - exit or end as per normal conventions for leaving a sub-mode. + Create a pbr-map rule with map NAME and specified sequence number. + This command puts the CLI into a new submode for pbr-map rule specification. + To exit this submode, type ``exit`` or ``end``. .. clicmd:: match src-ip PREFIX - When a incoming packet matches the source prefix specified, take the packet - and forward according to the nexthops specified. This command accepts both - v4 and v6 prefixes. This command is used in conjunction of the - :clicmd:`match dst-ip PREFIX` command for matching. + Match the packet's source IP address. + + This command accepts both v4 and v6 prefixes. .. clicmd:: match dst-ip PREFIX - When a incoming packet matches the destination prefix specified, take the - packet and forward according to the nexthops specified. This command accepts - both v4 and v6 prefixes. This command is used in conjunction of the - :clicmd:`match src-ip PREFIX` command for matching. + Match the packet's destination IP address. + + This command accepts both v4 and v6 prefixes. .. clicmd:: match src-port (1-65535) - When a incoming packet matches the source port specified, take the - packet and forward according to the nexthops specified. + Match the packet's UDP or TCP source port. .. clicmd:: match dst-port (1-65535) - When a incoming packet matches the destination port specified, take the - packet and forward according to the nexthops specified. + Match the packet's UDP or TCP destination port. -.. clicmd:: match ip-protocol [tcp|udp] +.. clicmd:: match ip-protocol PROTOCOL - When a incoming packet matches the specified ip protocol, take the - packet and forward according to the nexthops specified. + Match the packet's IP protocol. + + Protocol names are queried from the protocols database (``/etc/protocols``; + see ``man 5 protocols`` and ``man 3 getprotobyname``). .. clicmd:: match mark (1-4294967295) - Select the mark to match. This is a linux only command and if attempted - on another platform it will be denied. This mark translates to the - underlying `ip rule .... fwmark XXXX` command. + Match the packet's meta-information mark. + The mark value is attached to the packet by the kernel/dataplane and + is platform-specific. + Currently, this field is supported only on linux and corresponds to + the underlying `ip rule .... fwmark XXXX` command. .. clicmd:: match dscp (DSCP|0-63) - Match packets according to the specified differentiated services code point - (DSCP) in the IP header; if this value matches then forward the packet - according to the nexthop(s) specified. The passed DSCP value may also be a - standard name for a differentiated service code point like cs0 or af11. + Match the packet's IP differentiated services code point (DSCP). + The specified DSCP may also be a standard name for a + differentiated service code point such as ``cs0`` or ``af11``. - You may only specify one dscp per route map sequence; to match on multiple - dscp values you will need to create several sequences, one for each value. + You may only specify one dscp per route map rule; to match on multiple + dscp values you will need to create several rules, one for each value. .. clicmd:: match ecn (0-3) - Match packets according to the specified explicit congestion notification - (ECN) field in the IP header; if this value matches then forward the packet - according to the nexthop(s) specified. + Match the packet's IP explicit congestion notification (ECN) field. + +.. clicmd:: match pcp (0-7) + + Match the packet's 802.1Q Priority Code Point. + Zero is the default (nominally, "best effort"). + The Linux kernel dataplane provider does not currently support + matching PCPs, + so this field will be ignored unless other dataplane providers are used. + +.. clicmd:: match vlan (1-4094) + + Match the packet's VLAN (802.1Q) identifier. + Note that VLAN IDs 0 and 4095 are reserved. + The Linux kernel dataplane provider does not currently support + VLAN-matching facilities, + so this field will be ignored unless other dataplane providers are used. + +.. clicmd:: match vlan (tagged|untagged|untagged-or-zero) + Match packets according to whether or not they have a VLAN tag. + Use `untagged-or-zero` to also match packets with either no VLAN tag + or with the reserved VLAN ID of 0 (indicating an untagged frame that + includes other 802.1Q fields). + The Linux kernel dataplane provider does not currently support + VLAN-matching facilities, + so this field will be ignored unless other dataplane providers are used. .. clicmd:: set queue-id (1-65535) - Set the egress port queue identifier for matched packets. The Linux Kernel - provider does not currently support packet mangling, so this field will be - ignored unless another provider is used. + Action: + set the egress port queue identifier. + The Linux Kernel dataplane provider does not currently support + packet mangling, + so this field will be ignored unless another dataplane provider is used. .. clicmd:: set pcp (0-7) - Set the 802.1Q priority code point (PCP) for matched packets. A PCP of zero - is the defaul (nominally, "best effort"). The Linux Kernel provider does not - currently support packet mangling, so this field will be ignored unless - another provider is used. + Action: + set the 802.1Q priority code point (PCP). + A PCP of zero is the default (nominally, "best effort"). + The Linux Kernel dataplane provider does not currently support + packet mangling, + so this field will be ignored unless another dataplane provider is used. .. clicmd:: set vlan (1-4094) - Set the VLAN tag for matched packets. Identifiers 0 and 4095 are reserved. - The Linux Kernel provider does not currently support packet mangling, so - this field will be ignored unless another provider is used. + Action: + set the VLAN tag. Identifiers 0 and 4095 are reserved. + The Linux Kernel dataplane provider does not currently support + packet mangling, + so this field will be ignored unless another dataplane provider is used. .. clicmd:: strip vlan - Strip inner vlan tags from matched packets. The Linux Kernel provider does not currently support packet mangling, so this field will be ignored unless another provider is used. It is invalid to specify both a `strip` and `set - vlan` action. + Action: + strip inner vlan tags. + The Linux Kernel dataplane provider does not currently support + packet mangling, + so this field will be ignored unless another dataplane provider is used. + It is invalid to specify both a `strip` and `set vlan` action. .. clicmd:: set nexthop-group NAME - Use the nexthop-group NAME as the place to forward packets when the match - commands have matched a packet. + Action: + forward the packet using nexthop-group NAME. .. clicmd:: set nexthop [A.B.C.D|X:X::X:XX] [interface] [nexthop-vrf NAME] - Use this individual nexthop as the place to forward packets when the match - commands have matched a packet. + Action: + forward the packet using the specified single nexthop. .. clicmd:: set vrf unchanged|NAME - If unchanged is set, the rule will use the vrf table the interface is in - as its lookup. If NAME is specified, the rule will use that vrf table as - its lookup. + Action: + If set to ``unchanged``, the rule will use the vrf table the interface + is in as its lookup. + If set to NAME, the rule will use that vrf table as its lookup. Not supported with NETNS VRF backend. .. clicmd:: show pbr map [NAME] [detail|json] Display pbr maps either all or by ``NAME``. If ``detail`` is set, it will - give information about the rules unique ID used internally and some extra + give information about each rule's unique internal ID and some extra debugging information about install state for the nexthop/nexthop group. Setting ``json`` will provide the same information in an array of objects - which obey the schema below: + that adher to the schema below: +----------+--------------------------------+---------+ | Key | Description | Type | @@ -197,9 +232,9 @@ end destination. | policies | Rules to match packets against | Array | +----------+--------------------------------+---------+ - Each element of the ``policies`` array is composed of a handful of objects + Each element of the ``policies`` array is composed of a set of objects representing the policies associated with this map. Each policy is - described as below (not all fields are required): + described below (not all fields are required): +-----------------+-------------------------------------------+---------+ | Key | Description | Type | @@ -227,8 +262,8 @@ end destination. | nexthopGroup | This policy's nexthop group (if relevant) | Object | +-----------------+-------------------------------------------+---------+ - Finally, the ``nexthopGroup`` object above cotains information we know - about the configured nexthop for this policy: + Finally, the ``nexthopGroup`` object above contains information FRR + knows about the configured nexthop for this policy: +---------------------+--------------------------------------+---------+ | Key | Description | Type | @@ -239,7 +274,7 @@ end destination. +---------------------+--------------------------------------+---------+ | installed | Is this nexthop group installed? | Boolean | +---------------------+--------------------------------------+---------+ - | installedInternally | Do we think this group is installed? | Integer | + | installedInternally | Does FRR think NHG is installed? | Integer | +---------------------+--------------------------------------+---------+ @@ -251,19 +286,19 @@ end destination. PBR Policy ========== -After you have specified a PBR map, in order for it to be turned on, you must -apply the PBR map to an interface. This policy application to an interface +After you have specified a PBR map, in order for it to be enabled, it must +be applied to an interface. This policy application to an interface causes the policy to be installed into the kernel. .. clicmd:: pbr-policy NAME - This command is available under interface sub-mode. This turns - on the PBR map NAME and allows it to work properly. + This command is available under interface sub-mode. + It enables the PBR map NAME on the interface. .. note:: - This will not dynamically create PBR maps on sub-interfaces (i.e. vlans) - even if one is on the master. Each must have the PBR map explicitly added - to the interface. + This command will not dynamically create PBR maps on sub-interfaces + (i.e. vlans), even if one is on the master. + Each sub-interface must have the PBR map enabled explicitly. .. clicmd:: show pbr interface [NAME] [json] @@ -285,9 +320,9 @@ causes the policy to be installed into the kernel. .. clicmd:: pbr table range (10000-4294966272) (10000-4294966272) - Set or unset the range used to assign numeric table ID's to new + Set or unset the range used to assign numeric table IDs to new nexthop-group tables. Existing tables will not be modified to fit in this - range, so it is recommended to configure this before adding nexthop groups. + range, so this range should be configured before adding nexthop groups. .. seealso:: :ref:`pbr-details` @@ -299,23 +334,23 @@ PBR Debugs .. clicmd:: debug pbr events|map|nht|zebra - Debug pbr in pbrd daemon. You specify what types of debugs to turn on. + Debug pbr in pbrd daemon. You must specify what types of debugs to turn on. .. _pbr-details: PBR Details =========== -Under the covers a PBR map is translated into two separate constructs in the +Internally, a PBR map is translated into two separate constructs in the Linux kernel. -The PBR map specified creates a `ip rule ...` that is inserted into the Linux +The PBR map creates an `ip rule ...` that is inserted into the Linux kernel that points to a table to use for forwarding once the rule matches. -The creation of a nexthop or nexthop-group is translated to a default route in a -table with the nexthops specified as the nexthops for the default route. +The creation of a nexthop or nexthop-group is translated to a +table with a default route having the specified nexthop(s). Sample configuration diff --git a/tests/topotests/pbr_topo1/test_pbr_topo1.py b/tests/topotests/pbr_topo1/test_pbr_topo1.py index a5a29f7e3d..699e4489a3 100644 --- a/tests/topotests/pbr_topo1/test_pbr_topo1.py +++ b/tests/topotests/pbr_topo1/test_pbr_topo1.py @@ -8,6 +8,8 @@ # Cumulus Networks, Inc. # Donald Sharp # +# Copyright (c) 2023 LabN Consulting, L.L.C. +# """ test_pbr_topo1.py: Testing PBR @@ -19,6 +21,7 @@ import sys import pytest import json import platform +import re from functools import partial # Save the Current Working Directory to find configuration files. @@ -109,8 +112,29 @@ def test_converge_protocols(): topotest.sleep(5, "Waiting for PBR convergence") +# +# router: r1 +# tag: "show pbr interface" +# cmd: "show pbr interface json" +# expfile: "{}/{}/pbr-interface.json".format(CWD, router.name) +# +def runit(router, tag, cmd, expfile): + logger.info(expfile) + + # Read expected result from file + expected = json.loads(open(expfile).read()) + + # Actual output from router + test_func = partial(topotest.router_json_cmp, router, cmd, expected) + _, result = topotest.run_and_expect(test_func, None, count=30, wait=1) + assertmsg = '"{}" mismatches on {}'.format(tag, router.name) + if result is not None: + gather_pbr_data_on_error(router) + assert result is None, assertmsg + + def test_pbr_data(): - "Test PBR 'show ip eigrp'" + "Test PBR" tgen = get_topogen() # Don't run this test if we have any failure. @@ -122,55 +146,147 @@ def test_pbr_data(): router_list = tgen.routers().values() for router in router_list: - intf_file = "{}/{}/pbr-interface.json".format(CWD, router.name) - logger.info(intf_file) + runit( + router, + "show pbr interface", + "show pbr interface json", + "{}/{}/pbr-interface.json".format(CWD, router.name), + ) - # Read expected result from file - expected = json.loads(open(intf_file).read()) + runit( + router, + "show pbr map", + "show pbr map json", + "{}/{}/pbr-map.json".format(CWD, router.name), + ) - # Actual output from router - test_func = partial( - topotest.router_json_cmp, router, "show pbr interface json", expected + runit( + router, + "show pbr nexthop-groups", + "show pbr nexthop-groups json", + "{}/{}/pbr-nexthop-groups.json".format(CWD, router.name), ) - _, result = topotest.run_and_expect(test_func, None, count=30, wait=1) - assertmsg = '"show pbr interface" mismatches on {}'.format(router.name) - if result is not None: - gather_pbr_data_on_error(router) - assert result is None, assertmsg - map_file = "{}/{}/pbr-map.json".format(CWD, router.name) - logger.info(map_file) - # Read expected result from file - expected = json.loads(open(map_file).read()) +######################################################################## +# Field test - START +######################################################################## - # Actual output from router - test_func = partial( - topotest.router_json_cmp, router, "show pbr map json", expected - ) - _, result = topotest.run_and_expect(test_func, None, count=30, wait=1) - assertmsg = '"show pbr map" mismatches on {}'.format(router.name) - if result is not None: - gather_pbr_data_on_error(router) - assert result is None, assertmsg +# +# New fields: +# match ip-protocol (was only tcp|udp, now any value in /etc/protocols) +# match pcp (0-7) +# match vlan (1-4094) +# match vlan (tagged|untagged|untagged-or-zero) +# - nexthop_file = "{}/{}/pbr-nexthop-groups.json".format(CWD, router.name) - logger.info(nexthop_file) +# +# tm: must-match pattern +# tN: must Not match pattern +# +# Note we are searching amid a bunch of other rules, so these elements +# should be unique. +# +ftest = [ + {"c": "match ip-protocol icmp", "tm": r"IP protocol Match: 1$"}, + {"c": "no match ip-protocol icmp", "tN": r"IP protocol Match:"}, + {"c": "match pcp 6", "tm": r"PCP Match: 6$"}, + {"c": "match pcp 0", "tm": r"PCP Match: 0$"}, + {"c": "no match pcp 0", "tN": r"PCP Match:"}, + {"c": "match vlan 33", "tm": r"VLAN ID Match: 33$"}, + {"c": "no match vlan 33", "tN": r"VLAN ID Match:"}, + {"c": "match vlan tagged", "tm": r"VLAN Flags Match: tagged$"}, + {"c": "match vlan untagged", "tm": r"VLAN Flags Match: untagged$"}, + {"c": "match vlan untagged-or-zero", "tm": r"VLAN Flags Match: untagged-or-zero$"}, + {"c": "no match vlan tagged", "tN": r"VLAN Flags Match:"}, +] + + +# returns None if command output is correct, otherwise returns output +def rtr_field_cmp(rtr, cmd, pat_mustmatch, pat_mustnotmatch): + outstr = rtr.vtysh_cmd(cmd) + if pat_mustmatch is not None: + logger.info("MUSTMATCH: {}".format(pat_mustmatch)) + m = re.search(pat_mustmatch, outstr, flags=re.M) + if not m: + logger.info('Missing MUSTMATCH "{}"'.format(pat_mustmatch)) + return "MISSING MUSTMATCH: " + outstr + if pat_mustnotmatch is not None: + logger.info("MUSTNOTMATCH: {}".format(pat_mustnotmatch)) + m = re.search(pat_mustnotmatch, outstr, flags=re.M) + if m: + logger.info('Has MUSTNOTMATCH "{}"'.format(pat_mustnotmatch)) + return "HAS MUSTNOTMATCH: " + outstr + return None - # Read expected result from file - expected = json.loads(open(nexthop_file).read()) - # Actual output from router - test_func = partial( - topotest.router_json_cmp, router, "show pbr nexthop-groups json", expected - ) - _, result = topotest.run_and_expect(test_func, None, count=30, wait=1) - assertmsg = '"show pbr nexthop-groups" mismatches on {}'.format(router.name) +# +# This test sets fields in pbrd and looks for them in zebra via "sh pbr map" +# +def test_pbr_fields(): + "Test setting and clearing rule fields" + + tgen = get_topogen() + # Don't run this test if we have any failure. + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + logger.info("Verifying PBR rule fields") + + tag = "field" + + router_list = tgen.routers().values() + for router in router_list: + for t in ftest: + # send field-setting command + # always have a match dst-ip to satisfy rule non-empty check + vcmd = "c t\npbr-map ASAKUSA seq 100\n{}\nmatch dst-ip 9.9.9.9/32\nset nexthop-group A\nend\nend".format( + t["c"] + ) + router.vtysh_multicmd(vcmd) + + # debug + router.vtysh_cmd("sh pbr map") + + match = None + notmatch = None + + if "tm" in t: + match = t["tm"] + logger.info("MUSTMATCH: {}".format(match)) + if "tN" in t: + notmatch = t["tN"] + logger.info("NOTMATCH: {}".format(notmatch)) + + test_func = partial(rtr_field_cmp, router, "sh pbr rule", match, notmatch) + _, result = topotest.run_and_expect(test_func, None, count=10, wait=1) + assertmsg = '"{}" mismatches on {}'.format(tag, router.name) + if result is not None: + gather_pbr_data_on_error(router) + assert result is None, assertmsg + + # + # clean up + # + vcmd = "c t\nno pbr-map ASAKUSA seq 100\nend" + router.vtysh_multicmd(vcmd) + + match = None + notmatch = r"Seq 100\w" + + test_func = partial(rtr_field_cmp, router, "sh pbr rule", match, notmatch) + _, result = topotest.run_and_expect(test_func, None, count=10, wait=1) + assertmsg = '"{}" mismatches on {}'.format(tag, router.name) if result is not None: gather_pbr_data_on_error(router) assert result is None, assertmsg +######################################################################## +# Field test - END +######################################################################## + + def test_pbr_flap(): "Test PBR interface flapping" @@ -244,6 +360,7 @@ if __name__ == "__main__": args = ["-s"] + sys.argv[1:] sys.exit(pytest.main(args)) + # # EXTRA SAUCE # -- 2.39.5