This commit adds a flow graph parsing API. This is a helper API intended to help ethdev drivers implement rte_flow parsers, as common usages map to graph traversal problem very well.
Features provided by the API: - Flow graph, edge, and node definitions - Graph traversal logic - Declarative validation against common flow item types - Per-node validation and state processing callbacks Signed-off-by: Anatoly Burakov <[email protected]> --- Depends-on: series-39013 ("I40E refactors") doc/guides/prog_guide/ethdev/flow_graph.rst | 748 ++++++++++++++++++++ doc/guides/prog_guide/ethdev/index.rst | 1 + doc/guides/rel_notes/release_26_11.rst | 7 + lib/ethdev/meson.build | 1 + lib/ethdev/rte_flow_graph.h | 532 ++++++++++++++ 5 files changed, 1289 insertions(+) create mode 100644 doc/guides/prog_guide/ethdev/flow_graph.rst create mode 100644 lib/ethdev/rte_flow_graph.h diff --git a/doc/guides/prog_guide/ethdev/flow_graph.rst b/doc/guides/prog_guide/ethdev/flow_graph.rst new file mode 100644 index 0000000000..0b754a2a73 --- /dev/null +++ b/doc/guides/prog_guide/ethdev/flow_graph.rst @@ -0,0 +1,748 @@ +.. SPDX-License-Identifier: BSD-3-Clause + Copyright(c) 2026 Intel Corporation + +Flow Graph Parser +================= + +Introduction +------------ + +The flow graph parser is a helper library for PMD drivers that implements ``rte_flow`` pattern matching. +It lets a driver declare the protocol sequences it supports as a directed graph of nodes and edges. +It then validates and extracts fields from an ``rte_flow_item`` pattern in a single traversal. + +The library is defined in ``rte_flow_graph.h`` and is header-only. + +Scope and Limitations +~~~~~~~~~~~~~~~~~~~~~ + +Because the parser is graph-based, it is well suited for matching *protocol stacks*. +These are sequences of protocol headers such as ``ETH / IPv4 / TCP``. +Any pattern that can be expressed as "from protocol A, transitions to protocol B or C are allowed" fits naturally into the graph model. + +The library is **not** designed to cover every ``rte_flow`` item type. +Items that do not represent a position in a protocol stack do not have a natural place in a protocol graph. +This includes conntrack state, meter color, and other metadata items. +Such items are best handled outside the graph, either before or after the graph parse call. + +Defining a Graph +---------------- + +A graph consists of three parts: + +1. An **enum** that assigns a numeric index to every node. +2. A **node array** (``struct rte_flow_graph_node[]``) indexed by that enum. +3. An **edge array** (``struct rte_flow_graph_edge[]``) also indexed by that enum, describing allowed transitions. + +These parts are bundled together in a ``struct rte_flow_graph``. + +The running example used throughout this guide models the following protocol graph:: + + START -> ETH -> [VLAN] -> (IPv4 | IPv6) -> [(TCP | UDP | SCTP)] -> END + +Brackets ``[...]`` denote optional items. +Parentheses ``(...)`` denote a required choice between alternatives. +The key ideas are: + +* ``ETH`` is required after ``START``. +* After ``ETH``, an optional ``VLAN`` may appear, but the pattern must then see an IP layer. +* After an IP layer, an optional transport layer may appear; it may be TCP, UDP, or SCTP, after which the pattern reaches ``END``. + +Node Enum +~~~~~~~~~ + +Every node needs a stable index. +The first node **must** be at index ``RTE_FLOW_NODE_FIRST`` (which is 0). +This is the *start node*. +It is used only as a traversal anchor and must not carry callbacks. + +.. code-block:: c + + enum example_node_id { + EXAMPLE_NODE_START = RTE_FLOW_NODE_FIRST, + EXAMPLE_NODE_ETH, + EXAMPLE_NODE_VLAN, + EXAMPLE_NODE_IPV4, + EXAMPLE_NODE_IPV6, + EXAMPLE_NODE_TCP, + EXAMPLE_NODE_UDP, + EXAMPLE_NODE_SCTP, + EXAMPLE_NODE_END, + /* keep last */ + EXAMPLE_NODE_MAX, + }; + +Node Definitions +~~~~~~~~~~~~~~~~ + +Each node maps to one ``rte_flow_item_type``. +It can also carry a *validate* callback, a *process* callback, and a set of *constraints*. +The the ``END`` node can also have callbacks to perform end-of-match processing. + +A minimal skeleton (callbacks and constraints are added in later sections): + +.. code-block:: c + + const struct rte_flow_graph example_graph = { + .nodes = (struct rte_flow_graph_node[]){ + [EXAMPLE_NODE_START] = { + .name = "START", + /* Start node: no type, no callbacks */ + }, + [EXAMPLE_NODE_ETH] = { + .name = "ETH", + .type = RTE_FLOW_ITEM_TYPE_ETH, + }, + [EXAMPLE_NODE_VLAN] = { + .name = "VLAN", + .type = RTE_FLOW_ITEM_TYPE_VLAN, + }, + [EXAMPLE_NODE_IPV4] = { + .name = "IPV4", + .type = RTE_FLOW_ITEM_TYPE_IPV4, + }, + [EXAMPLE_NODE_IPV6] = { + .name = "IPV6", + .type = RTE_FLOW_ITEM_TYPE_IPV6, + }, + [EXAMPLE_NODE_TCP] = { + .name = "TCP", + .type = RTE_FLOW_ITEM_TYPE_TCP, + }, + [EXAMPLE_NODE_UDP] = { + .name = "UDP", + .type = RTE_FLOW_ITEM_TYPE_UDP, + }, + [EXAMPLE_NODE_SCTP] = { + .name = "SCTP", + .type = RTE_FLOW_ITEM_TYPE_SCTP, + }, + [EXAMPLE_NODE_END] = { + .name = "END", + .type = RTE_FLOW_ITEM_TYPE_END, + }, + }, + }; + +Edge Definitions +~~~~~~~~~~~~~~~~ + +Edges express which nodes may follow the current one. +Every edge list is terminated by the ``RTE_FLOW_NODE_EDGE_END`` sentinel. +All non-``END`` nodes **must** have an edge list. +The ``END`` node itself does not need one. + +.. code-block:: c + + const struct rte_flow_graph example_graph = { + .nodes = (struct rte_flow_graph_node[]){ + /* ... same nodes as above ... */ + }, + .edges = (struct rte_flow_graph_edge[]){ + [EXAMPLE_NODE_START] = { + .next = (const size_t[]){ + EXAMPLE_NODE_ETH, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_ETH] = { + .next = (const size_t[]){ + EXAMPLE_NODE_VLAN, + EXAMPLE_NODE_IPV4, + EXAMPLE_NODE_IPV6, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_VLAN] = { + .next = (const size_t[]){ + EXAMPLE_NODE_IPV4, + EXAMPLE_NODE_IPV6, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_IPV4] = { + .next = (const size_t[]){ + EXAMPLE_NODE_TCP, + EXAMPLE_NODE_UDP, + EXAMPLE_NODE_SCTP, + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_IPV6] = { + .next = (const size_t[]){ + EXAMPLE_NODE_TCP, + EXAMPLE_NODE_UDP, + EXAMPLE_NODE_SCTP, + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_TCP] = { + .next = (const size_t[]){ + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_UDP] = { + .next = (const size_t[]){ + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_SCTP] = { + .next = (const size_t[]){ + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + }, + }; + +Reading the edges back: + +* From ``START``, the parser can only reach ``ETH``, which makes ``ETH`` required. +* From ``ETH``, the parser can reach ``VLAN``, ``IPV4``, or ``IPV6``, which makes ``VLAN`` optional. +* From ``IPV4`` or ``IPV6``, the parser can reach ``TCP``, ``UDP``, ``SCTP``, or ``END``, which makes the transport layer optional. + +Assembling the Graph +~~~~~~~~~~~~~~~~~~~~ + +With nodes and edges defined inline, assembling the graph is just a matter of +combining the two arrays into a single compound literal: + +.. code-block:: c + + const struct rte_flow_graph example_graph = { + .nodes = (struct rte_flow_graph_node[]){ + [EXAMPLE_NODE_START] = { + .name = "START" + }, + [EXAMPLE_NODE_ETH] = { + .name = "ETH", + .type = RTE_FLOW_ITEM_TYPE_ETH + }, + /* ... remaining nodes ... */ + [EXAMPLE_NODE_END] = { + .name = "END", + .type = RTE_FLOW_ITEM_TYPE_END + }, + }, + .edges = (struct rte_flow_graph_edge[]){ + [EXAMPLE_NODE_START] = { + .next = (const size_t[]){ + EXAMPLE_NODE_ETH, + RTE_FLOW_NODE_EDGE_END + } + }, + /* ... remaining edges ... */ + }, + }; + +Callbacks +--------- + +The graph calls up to two callbacks on every visited node: *validate* and *process*. + +Both callbacks share the same return convention. +On success they must return ``0``. +On failure they must call ``rte_flow_error_set`` to record a descriptive error and return its result. + +Validate Callback +~~~~~~~~~~~~~~~~~ + +.. code-block:: c + + typedef int (*rte_flow_node_validate_fn)( + const void *ctx, + const struct rte_flow_item *item, + struct rte_flow_error *error); + +This callback receives a **read-only** context pointer. +The canonical intent is that it should check whether the item's spec, mask, and last values are acceptable for the driver. +On failure it returns the result of ``rte_flow_error_set`` (see the return convention above). + +It is recommended to use this callback for **all checks that can reject a rule**. +This includes unsupported mask bits, conflicting field combinations, hardware limitations, and other applicable criteria. + +Process Callback +~~~~~~~~~~~~~~~~ + +.. code-block:: c + + typedef int (*rte_flow_node_process_fn)( + void *ctx, + const struct rte_flow_item *item, + struct rte_flow_error *error); + +This callback receives a **mutable** context pointer. +The canonical expectation is that it should extract the fields needed for hardware programming. +It should then store extracted data in the driver's context structure. +On the rare failure path it returns the result of ``rte_flow_error_set`` (see the return convention above). + +It is recommended to use this callback for the **happy path**. +For example, it can copy addresses, ports, and protocol IDs into the driver context so they can be programmed later. + +Defining a Context Structure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The opaque ``ctx`` pointer passed to every callback is driver-defined. +A typical context accumulates the parsed protocol fields: + +.. code-block:: c + + struct example_parsed_flow { + /* L2 */ + struct rte_ether_addr dst_mac; + bool has_vlan; + uint16_t vlan_tci; + + /* L3 */ + bool is_ipv6; + rte_be32_t ipv4_src; + rte_be32_t ipv4_dst; + uint8_t ipv6_src[16]; + uint8_t ipv6_dst[16]; + + /* L4 */ + enum rte_flow_item_type l4_proto; + rte_be16_t src_port; + rte_be16_t dst_port; + }; + +These fields are meant to reflect the structure used by the driver to programming hardware with. + +Callback Example +~~~~~~~~~~~~~~~~ + +Below is a validate/process pair for the IPv4 node. +The validate callback rejects unsupported mask bits. +The process callback copies addresses into the context: + +.. code-block:: c + + static int + example_validate_ipv4(const void *ctx __rte_unused, + const struct rte_flow_item *item, + struct rte_flow_error *error) + { + const struct rte_flow_item_ipv4 *mask = item->mask; + + if (mask->hdr.version_ihl || + mask->hdr.type_of_service || + mask->hdr.total_length || + mask->hdr.packet_id || + mask->hdr.fragment_offset || + mask->hdr.time_to_live || + mask->hdr.next_proto_id || + mask->hdr.hdr_checksum) { + return rte_flow_error_set(error, EINVAL, + RTE_FLOW_ERROR_TYPE_ITEM, item, + "Only src/dst addresses supported"); + } + return 0; + } + + static int + example_process_ipv4(void *ctx, + const struct rte_flow_item *item, + struct rte_flow_error *error __rte_unused) + { + struct example_parsed_flow *parsed = ctx; + const struct rte_flow_item_ipv4 *spec = item->spec; + + parsed->is_ipv6 = false; + if (spec != NULL) { + parsed->ipv4_src = spec->hdr.src_addr; + parsed->ipv4_dst = spec->hdr.dst_addr; + } + return 0; + } + +Add the callbacks to the node definition: + +.. code-block:: c + + [EXAMPLE_NODE_IPV4] = { + .name = "IPV4", + .type = RTE_FLOW_ITEM_TYPE_IPV4, + .validate = example_validate_ipv4, + .process = example_process_ipv4, + }, + +Node Constraints +---------------- + +Many nodes share common requirements about which combination of ``spec``, ``mask``, and ``last`` pointers an item must carry. +Instead of checking these in every validate callback, they can be declared via the ``constraints`` field. +The field uses ``rte_flow_graph_node_expect`` flags. + +Available constraint flags (may be ORed together): + +``RTE_FLOW_NODE_EXPECT_EMPTY`` + The item must have ``spec == NULL``, ``mask == NULL``, and + ``last == NULL``. + +``RTE_FLOW_NODE_EXPECT_SPEC`` + ``spec`` is required; ``mask`` and ``last`` must be NULL. + +``RTE_FLOW_NODE_EXPECT_MASK`` + ``mask`` is required; ``spec`` and ``last`` must be NULL. + +``RTE_FLOW_NODE_EXPECT_SPEC_MASK`` + Both ``spec`` and ``mask`` are required; ``last`` must be NULL. + +``RTE_FLOW_NODE_EXPECT_RANGE`` + All three (``spec``, ``mask``, ``last``) are required. + +``RTE_FLOW_NODE_EXPECT_NOT_RANGE`` + ``last`` must be NULL (``spec`` and ``mask`` are unconstrained). + +Multiple flags can be ORed together. +The item is accepted if **any one** of the flagged constraints is satisfied. + +For example, an IPv4 node that accepts either a mask-only item or a spec+mask item: + +.. code-block:: c + + [EXAMPLE_NODE_IPV4] = { + .name = "IPV4", + .type = RTE_FLOW_ITEM_TYPE_IPV4, + .validate = example_validate_ipv4, + .process = example_process_ipv4, + .constraints = RTE_FLOW_NODE_EXPECT_MASK | + RTE_FLOW_NODE_EXPECT_SPEC_MASK, + }, + +An Ethernet node that may appear empty (no spec/mask) or with spec+mask: + +.. code-block:: c + + [EXAMPLE_NODE_ETH] = { + .name = "ETH", + .type = RTE_FLOW_ITEM_TYPE_ETH, + .validate = example_validate_eth, + .process = example_process_eth, + .constraints = RTE_FLOW_NODE_EXPECT_EMPTY | + RTE_FLOW_NODE_EXPECT_SPEC_MASK, + }, + +Constraints are checked **before** the validate callback is invoked. + +Ignoring Item Types +~~~~~~~~~~~~~~~~~~~ + +The ``ignore_nodes`` field on ``struct rte_flow_graph`` is an optional complement to node constraints. +When the pattern may contain item types that are irrelevant to the driver, list them in ``ignore_nodes``. +For example, metadata items like ``RTE_FLOW_ITEM_TYPE_MARK`` do not represent a protocol header. +The parser skips ignored items silently without advancing the current graph position: + +.. code-block:: c + + const struct rte_flow_graph example_graph = { + /* ... nodes and edges ... */ + .ignore_nodes = (const enum rte_flow_item_type[]){ + RTE_FLOW_ITEM_TYPE_MARK, + RTE_FLOW_ITEM_TYPE_END, + }, + }; + +``RTE_FLOW_ITEM_TYPE_VOID`` is always ignored regardless of this list. +Omit ``ignore_nodes`` entirely when no additional item types need to be skipped. + +Calling the Parser +------------------ + +``rte_flow_graph_parse`` walks the pattern against the graph: + +.. code-block:: c + + int + rte_flow_graph_parse(const struct rte_flow_graph *graph, + const struct rte_flow_item *pattern, + struct rte_flow_error *error, + void *ctx); + +A typical call site looks like this: + +.. code-block:: c + + struct example_parsed_flow parsed; + int ret; + + memset(&parsed, 0, sizeof(parsed)); + + ret = rte_flow_graph_parse(&example_graph, pattern, error, &parsed); + if (ret != 0) + return ret; + + /* 'parsed' now contains the extracted protocol fields */ + +The function returns success or failure, with ``error`` populated. + +Error conditions: + +* **Graph is NULL** — for example, when the graph pointer itself is not provided. +* **Pattern is NULL**. +* **Unsupported transition** — when an item type has no matching edge from the current node. + This is the primary way the graph rejects unsupported protocol sequences. +* **Constraint failure** — when the spec, mask, and last combination does not satisfy the node's declared constraints. +* **Validate callback failure** — when driver-specific validation rejects the item. +* **Process callback failure** — when driver-specific extraction path fails. + +.. warning:: + + Malformed graph tables (for example invalid node indices, missing sentinels, + or otherwise inconsistent driver-defined graph structures) are considered to be a driver implementation bug. + Graphs are trusted by default: driver-owned graph structures are expected to be valid and are not fully validated. + +The traversal processes items in order, skipping ignored types. +After the last non-``END`` item, the parser looks for an ``END`` node reachable from the current position. +It then visits that node and runs its callbacks, if any. +This means drivers can attach a process callback to the ``END`` node for post-traversal finalization. + +Putting It All Together +----------------------- + +The complete graph definition with callbacks and constraints: + +.. code-block:: c + + const struct rte_flow_graph example_graph = { + .nodes = (struct rte_flow_graph_node[]){ + [EXAMPLE_NODE_START] = { + .name = "START", + }, + [EXAMPLE_NODE_ETH] = { + .name = "ETH", + .type = RTE_FLOW_ITEM_TYPE_ETH, + .validate = example_validate_eth, + .process = example_process_eth, + .constraints = RTE_FLOW_NODE_EXPECT_EMPTY + | RTE_FLOW_NODE_EXPECT_SPEC_MASK, + }, + [EXAMPLE_NODE_VLAN] = { + .name = "VLAN", + .type = RTE_FLOW_ITEM_TYPE_VLAN, + .process = example_process_vlan, + .constraints = RTE_FLOW_NODE_EXPECT_SPEC_MASK, + }, + [EXAMPLE_NODE_IPV4] = { + .name = "IPV4", + .type = RTE_FLOW_ITEM_TYPE_IPV4, + .validate = example_validate_ipv4, + .process = example_process_ipv4, + .constraints = RTE_FLOW_NODE_EXPECT_MASK + | RTE_FLOW_NODE_EXPECT_SPEC_MASK, + }, + [EXAMPLE_NODE_IPV6] = { + .name = "IPV6", + .type = RTE_FLOW_ITEM_TYPE_IPV6, + .validate = example_validate_ipv6, + .process = example_process_ipv6, + .constraints = RTE_FLOW_NODE_EXPECT_MASK + | RTE_FLOW_NODE_EXPECT_SPEC_MASK, + }, + [EXAMPLE_NODE_TCP] = { + .name = "TCP", + .type = RTE_FLOW_ITEM_TYPE_TCP, + .validate = example_validate_tcp, + .process = example_process_tcp, + .constraints = RTE_FLOW_NODE_EXPECT_MASK + | RTE_FLOW_NODE_EXPECT_SPEC_MASK, + }, + [EXAMPLE_NODE_UDP] = { + .name = "UDP", + .type = RTE_FLOW_ITEM_TYPE_UDP, + .validate = example_validate_udp, + .process = example_process_udp, + .constraints = RTE_FLOW_NODE_EXPECT_MASK + | RTE_FLOW_NODE_EXPECT_SPEC_MASK, + }, + [EXAMPLE_NODE_SCTP] = { + .name = "SCTP", + .type = RTE_FLOW_ITEM_TYPE_SCTP, + .process = example_process_sctp, + .constraints = RTE_FLOW_NODE_EXPECT_EMPTY + | RTE_FLOW_NODE_EXPECT_MASK + | RTE_FLOW_NODE_EXPECT_SPEC_MASK, + }, + [EXAMPLE_NODE_END] = { + .name = "END", + .type = RTE_FLOW_ITEM_TYPE_END, + }, + }, + .edges = (struct rte_flow_graph_edge[]){ + [EXAMPLE_NODE_START] = { + .next = (const size_t[]){ + EXAMPLE_NODE_ETH, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_ETH] = { + .next = (const size_t[]){ + EXAMPLE_NODE_VLAN, + EXAMPLE_NODE_IPV4, + EXAMPLE_NODE_IPV6, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_VLAN] = { + .next = (const size_t[]){ + EXAMPLE_NODE_IPV4, + EXAMPLE_NODE_IPV6, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_IPV4] = { + .next = (const size_t[]){ + EXAMPLE_NODE_TCP, + EXAMPLE_NODE_UDP, + EXAMPLE_NODE_SCTP, + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_IPV6] = { + .next = (const size_t[]){ + EXAMPLE_NODE_TCP, + EXAMPLE_NODE_UDP, + EXAMPLE_NODE_SCTP, + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_TCP] = { + .next = (const size_t[]){ + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_UDP] = { + .next = (const size_t[]){ + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [EXAMPLE_NODE_SCTP] = { + .next = (const size_t[]){ + EXAMPLE_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + }, + }; + + +Tunnel Graphs and Repeated Item Types +------------------------------------- + +Tunneled patterns often repeat the same ``rte_flow_item_type`` in outer and inner headers. +A simple representative example is a TCP-IPv4 over GTP-U pattern:: + + ETH -> IPV4 -> UDP -> GTPU -> IPV4 -> TCP + +The graph library supports this naturally. +Multiple nodes may use the same ``type`` value, as long as they are distinct nodes in the graph and reached through different edges. + +For tunnel parsing, the recommended style is to model repeated protocol types as separate inner/outer nodes, for example ``OUTER_IPV4`` and ``INNER_IPV4``. +This makes the graph intent explicit, keeps callback logic clear, and avoids unexpected graph paths due to loops. + +.. code-block:: c + + enum tunnel_node_id { + TUNNEL_NODE_START = RTE_FLOW_NODE_FIRST, + TUNNEL_NODE_ETH, + TUNNEL_NODE_OUTER_IPV4, + TUNNEL_NODE_TCP, + TUNNEL_NODE_UDP, + TUNNEL_NODE_GTPU, + TUNNEL_NODE_INNER_IPV4, + TUNNEL_NODE_END, + TUNNEL_NODE_MAX, + }; + + const struct rte_flow_graph tunnel_graph = { + .nodes = (struct rte_flow_graph_node[]){ + /* Minimal topology example: callbacks and constraints are omitted. */ + [TUNNEL_NODE_START] = { .name = "START" }, + [TUNNEL_NODE_ETH] = { + .name = "ETH", + .type = RTE_FLOW_ITEM_TYPE_ETH, + }, + [TUNNEL_NODE_OUTER_IPV4] = { + .name = "OUTER_IPV4", + .type = RTE_FLOW_ITEM_TYPE_IPV4, + }, + [TUNNEL_NODE_UDP] = { + .name = "UDP", + .type = RTE_FLOW_ITEM_TYPE_UDP, + }, + [TUNNEL_NODE_GTPU] = { + .name = "GTPU", + .type = RTE_FLOW_ITEM_TYPE_GTPU, + }, + [TUNNEL_NODE_INNER_IPV4] = { + .name = "INNER_IPV4", + .type = RTE_FLOW_ITEM_TYPE_IPV4, + }, + [TUNNEL_NODE_TCP] = { + .name = "TCP", + .type = RTE_FLOW_ITEM_TYPE_TCP, + }, + [TUNNEL_NODE_END] = { + .name = "END", + .type = RTE_FLOW_ITEM_TYPE_END, + }, + }, + .edges = (struct rte_flow_graph_edge[]){ + [TUNNEL_NODE_START] = { + .next = (const size_t[]){ + TUNNEL_NODE_ETH, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [TUNNEL_NODE_ETH] = { + .next = (const size_t[]){ + TUNNEL_NODE_OUTER_IPV4, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [TUNNEL_NODE_OUTER_IPV4] = { + .next = (const size_t[]){ + TUNNEL_NODE_UDP, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [TUNNEL_NODE_UDP] = { + .next = (const size_t[]){ + TUNNEL_NODE_GTPU, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [TUNNEL_NODE_GTPU] = { + .next = (const size_t[]){ + TUNNEL_NODE_INNER_IPV4, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [TUNNEL_NODE_INNER_IPV4] = { + .next = (const size_t[]){ + TUNNEL_NODE_TCP, + RTE_FLOW_NODE_EDGE_END, + }, + }, + [TUNNEL_NODE_TCP] = { + .next = (const size_t[]){ + TUNNEL_NODE_END, + RTE_FLOW_NODE_EDGE_END, + }, + }, + }, + }; + +In other words, traversal follows graph edges (node-to-node), while node matching is done against each candidate node's ``rte_flow_item_type``. +That combination allows repeated protocol layers to be represented cleanly with separate nodes for different parsing contexts. + +Although arbitrary loops are possible in the graph, tunnel protocol graphs are usually easier to reason about when repeated item types are split into explicit inner/outer nodes. +It is not recommended to create loops in the graph, as these loops will be unbounded. diff --git a/doc/guides/prog_guide/ethdev/index.rst b/doc/guides/prog_guide/ethdev/index.rst index 392ced0a2e..b41fd045bb 100644 --- a/doc/guides/prog_guide/ethdev/index.rst +++ b/doc/guides/prog_guide/ethdev/index.rst @@ -10,6 +10,7 @@ Ethernet Device Library ethdev switch_representation flow_offload + flow_graph traffic_metering_and_policing traffic_management qos_framework diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst index c8cc86295d..a4235d7dac 100644 --- a/doc/guides/rel_notes/release_26_11.rst +++ b/doc/guides/rel_notes/release_26_11.rst @@ -55,6 +55,13 @@ New Features Also, make sure to start the actual text at the margin. ======================================================= +* **Added internal ethdev flow graph parser helper API.** + + Added ``rte_flow_graph`` helper definitions in ``rte_flow_graph.h`` + for PMD drivers to build graph-based pattern parsers. + This internal driver API validates ``rte_flow_item`` protocol sequences + through node and edge traversal, with per-node callbacks and constraints. + Removed Items ------------- diff --git a/lib/ethdev/meson.build b/lib/ethdev/meson.build index 8ba6c708a2..686b64e3c2 100644 --- a/lib/ethdev/meson.build +++ b/lib/ethdev/meson.build @@ -40,6 +40,7 @@ driver_sdk_headers += files( 'ethdev_pci.h', 'ethdev_vdev.h', 'rte_flow_driver.h', + 'rte_flow_graph.h', 'rte_mtr_driver.h', 'rte_tm_driver.h', ) diff --git a/lib/ethdev/rte_flow_graph.h b/lib/ethdev/rte_flow_graph.h new file mode 100644 index 0000000000..e9f898ad68 --- /dev/null +++ b/lib/ethdev/rte_flow_graph.h @@ -0,0 +1,532 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2025 Intel Corporation + */ + +#ifndef _RTE_FLOW_GRAPH_H_ +#define _RTE_FLOW_GRAPH_H_ + +/** + * @file + * RTE Flow Graph Parser (Internal Driver API) + * + * This file provides a graph-based flow pattern parser for PMD drivers. + * It defines structures and functions to validate and process rte_flow + * patterns using a directed graph representation. + * + * @warning + * This is an internal API for PMD drivers only. Applications must not use it. + */ + +#include <rte_flow.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Logging for flow graph parse errors. This is an internal driver API; + * RTE_FLOW_GRAPH_LOG requires RTE_COMPONENT_NAME (set by meson for drivers) + * and the corresponding driver logtype variable to be registered. + */ +#ifdef RTE_COMPONENT_NAME +extern int RTE_CONCAT(RTE_COMPONENT_NAME, _logtype_driver); +#define RTE_FLOW_GRAPH_LOG(level, fmt, ...) \ + rte_log(RTE_LOG_##level, RTE_CONCAT(RTE_COMPONENT_NAME, _logtype_driver), \ + "ETHDEV FLOW GRAPH: %s(): " fmt "\n", __func__, ##__VA_ARGS__) +#else +/* Use ETHDEV log level when included outside driver context */ +#define RTE_FLOW_GRAPH_LOG(level, fmt, ...) \ + rte_log(RTE_LOG_##level, \ + rte_eth_dev_logtype, \ + "ETHDEV FLOW GRAPH: %s(): " fmt "\n", __func__, ##__VA_ARGS__) +#endif + +#define RTE_FLOW_NODE_FIRST (0) +/* Edge array termination sentinel (not a valid node index). */ +#define RTE_FLOW_NODE_EDGE_END ((size_t)~0U) + +static inline const char * +rte_flow_graph_item_type_to_str(enum rte_flow_item_type type) +{ + const char *name = NULL; + int ret; + + /* + * this is a hack of monumental proportions. + * + * currently, chkincs will build each driver SDK header file using three + * build flag combinations: with INTERNAL+EXPERIMENTAL allowed, with + * EXPERIMENTAL allowed, and with neither allowed. That last one will + * fail for this header, because `rte_flow_conv` is defined as an + * experimental API. + * + * Arguably, this is a bug in chkincs because this header is installed + * as driver_sdk only (see lib/ethdev Meson file), meaning that this + * file is driver-internal only and is never exported to the user. + * Drivers themselves are already always built with experimental API + * enabled (see drivers Meson file, specifically default_cflags), so + * in practice chkincs tests a configuration that never exists in real + * life for driver SDK. + * + * So, again, arguably, this should be fixed in chkincs, but I have no + * idea what would be the correct way to do that, so for now I'll just + * avoid calling `rte_flow_conv` whenever experimental API's aren't + * allowed, and hopefully we'll come up with a proper solution in one + * of the next versions of this patchset. + */ +#ifdef ALLOW_EXPERIMENTAL_API + ret = rte_flow_conv(RTE_FLOW_CONV_OP_ITEM_NAME_PTR, + &name, sizeof(name), (const void *)(uintptr_t)type, NULL); +#else + ret = -1; + RTE_SET_USED(type); +#endif + if (ret < 0 || name == NULL) + return "UNKNOWN"; + + return name; +} + +/** + * For a lot of nodes, there are multiple common patterns of validation behavior. This enum allows + * marking nodes as implementing one of these common behaviors without need for expressing that in + * validation code. Can be ORed together to express support for multiple node types. These checks + * are not combined (any one of them being satisfied is sufficient). + */ +enum rte_flow_graph_node_expect { + RTE_FLOW_NODE_EXPECT_NONE = 0, /**< No special constraints. */ + RTE_FLOW_NODE_EXPECT_EMPTY = (1 << 0), /**< spec, mask, last must be NULL. */ + RTE_FLOW_NODE_EXPECT_SPEC = (1 << 1), /**< spec is required, mask and last must be NULL. */ + RTE_FLOW_NODE_EXPECT_MASK = (1 << 2), /**< mask is required, spec and last must be NULL. */ + RTE_FLOW_NODE_EXPECT_SPEC_MASK = (1 << 3), /**< spec and mask required, last must be NULL. */ + RTE_FLOW_NODE_EXPECT_RANGE = (1 << 4), /**< spec, mask, and last are required. */ + RTE_FLOW_NODE_EXPECT_NOT_RANGE = (1 << 5), /**< last must be NULL. */ +}; + +/** + * Node validation callback. + * + * Called when the graph traversal reaches this node. Validates the + * rte_flow_item (spec, mask, last) against driver-specific constraints. + * + * Drivers are suggested to perform all checks in this callback. + * + * @param ctx + * Opaque driver context for accumulating parsed state. + * @param item + * Pointer to the rte_flow_item being validated. + * @param error + * Pointer to rte_flow_error structure for reporting failures. + * @return + * 0 on success, or the value returned by rte_flow_error_set() on failure. + * On failure the callback must report the error with rte_flow_error_set(). + */ +typedef int (*rte_flow_node_validate_fn)( + const void *ctx, + const struct rte_flow_item *item, + struct rte_flow_error *error); + +/** + * Node processing callback. + * + * Called after validation succeeds. Extracts fields from the rte_flow_item + * and stores them in driver-specific state for later hardware programming. + * + * Drivers are suggested to implement "happy path" in this callback. + * + * @param ctx + * Opaque driver context for accumulating parsed state. + * @param item + * Pointer to the rte_flow_item to process. + * @param error + * Pointer to rte_flow_error structure for reporting failures. + * @return + * 0 on success, or the value returned by rte_flow_error_set() on failure. + * On failure the callback must report the error with rte_flow_error_set(). + */ +typedef int (*rte_flow_node_process_fn)( + void *ctx, + const struct rte_flow_item *item, + struct rte_flow_error *error); + +/** + * Graph node definition. + * + * Node validity rules: + * - all nodes must define a name, + * - all non-END nodes must define an edge list, + * - start node must not define validation/processing callbacks. + */ +struct rte_flow_graph_node { + const char *name; /**< Node name. */ + const enum rte_flow_item_type type; /**< Corresponding rte_flow_item_type. */ + const enum rte_flow_graph_node_expect constraints; /**< Common validation constraints (ORed). */ + rte_flow_node_validate_fn validate; /**< Validation callback (NULL if unsupported). */ + rte_flow_node_process_fn process; /**< Processing callback (NULL if no extraction needed). */ +}; + +/** + * Graph edge definition. + * + * Describes allowed transitions from one node to others. The 'next' array + * lists all valid successor node types and is terminated by RTE_FLOW_NODE_EDGE_END. + * Drivers define edges to express their supported protocol sequences. Edges + * must be unique, as split path following is not supported. + */ +struct rte_flow_graph_edge { + const size_t *next; /**< Array of valid successor nodes, terminated by RTE_FLOW_NODE_EDGE_END. */ +}; + +/** + * Flow graph to be implemented by drivers. + * + * Graph contents are expected to be well-formed. This library validates + * traversal semantics for pattern items, but does not attempt to harden + * against arbitrary malformed node/edge table definitions. + */ +struct rte_flow_graph { + struct rte_flow_graph_node *nodes; + struct rte_flow_graph_edge *edges; + const enum rte_flow_item_type *ignore_nodes; /**< Additional node types to ignore, terminated by RTE_FLOW_ITEM_TYPE_END. */ +}; + +static inline bool +__flow_graph_node_check_constraint(enum rte_flow_graph_node_expect c, + bool has_spec, bool has_mask, bool has_last) +{ + bool empty = !has_spec && !has_mask && !has_last; + + if ((c & RTE_FLOW_NODE_EXPECT_EMPTY) && empty) + return true; + if ((c & RTE_FLOW_NODE_EXPECT_NOT_RANGE) && !has_last) + return true; + if ((c & RTE_FLOW_NODE_EXPECT_SPEC) && has_spec && !has_mask && !has_last) + return true; + if ((c & RTE_FLOW_NODE_EXPECT_MASK) && has_mask && !has_spec && !has_last) + return true; + if ((c & RTE_FLOW_NODE_EXPECT_SPEC_MASK) && has_spec && has_mask && !has_last) + return true; + if ((c & RTE_FLOW_NODE_EXPECT_RANGE) && has_mask && has_spec && has_last) + return true; + + return false; +} + +static inline bool +__flow_graph_node_is_expected(const struct rte_flow_graph_node *node, + const struct rte_flow_item *item, struct rte_flow_error *error) +{ + enum rte_flow_graph_node_expect c = node->constraints; + + if (c == RTE_FLOW_NODE_EXPECT_NONE) + return true; + + bool has_spec = (item->spec != NULL); + bool has_mask = (item->mask != NULL); + bool has_last = (item->last != NULL); + + if (__flow_graph_node_check_constraint(c, has_spec, has_mask, has_last)) + return true; + + /* + * In the interest of everyone debugging flow parsing code, we should provide the user with + * meaningful messages about exactly what failed, as no one likes non-descript "node + * constraints not met" errors with no clear indication of where this is even coming from. + * What follows is us building said meaningful error messages. It's a bit ugly, but it is + * for the greater good. + */ + const char *msg; + + /* for empty items, we know exactly what went wrong */ + if (c == RTE_FLOW_NODE_EXPECT_EMPTY) { + if (has_spec) + msg = "Unexpected spec in flow item"; + else if (has_mask) + msg = "Unexpected mask in flow item"; + else /* has_last */ + msg = "Unexpected last in flow item"; + } else { + /* + * for non-empty constraints, we need to figure out the one thing user is missing + * (or has extra) that would've satisfied the constraints. + * We do that by flipping each presence bit in turn and seeing whether that single + * change would have satisfied the node constraints. + */ + + /* check spec first */ + if (!has_spec && __flow_graph_node_check_constraint(c, true, has_mask, has_last)) { + msg = "Missing spec in flow item"; + } else if (has_spec && __flow_graph_node_check_constraint(c, false, has_mask, has_last)) { + msg = "Unexpected spec in flow item"; + } + /* check mask next */ + else if (!has_mask && __flow_graph_node_check_constraint(c, has_spec, true, has_last)) { + msg = "Missing mask in flow item"; + } else if (has_mask && __flow_graph_node_check_constraint(c, has_spec, false, has_last)) { + msg = "Unexpected mask in flow item"; + } + /* finally, check range */ + else if (!has_last && __flow_graph_node_check_constraint(c, has_spec, has_mask, true)) { + msg = "Missing last in flow item"; + } else if (has_last && __flow_graph_node_check_constraint(c, has_spec, has_mask, false)) { + msg = "Unexpected last in flow item"; + /* multiple things are wrong with the constraint, so just output a generic error */ + } else { + msg = "Flow item does not meet node constraints"; + } + } + + rte_flow_error_set(error, EINVAL, RTE_FLOW_ERROR_TYPE_ITEM, item, msg); + + return false; +} + +/** + * Check if a flow item type should be ignored by the graph. + * + * Checks if the item type is in the graph's ignore list. + */ +static inline bool +__flow_graph_node_is_ignored(const struct rte_flow_graph *graph, + enum rte_flow_item_type fi_type) +{ + const enum rte_flow_item_type *ignored; + + /* Always skip VOID items */ + if (fi_type == RTE_FLOW_ITEM_TYPE_VOID) + return true; + + if (graph->ignore_nodes == NULL) + return false; + + for (ignored = graph->ignore_nodes; *ignored != RTE_FLOW_ITEM_TYPE_END; ignored++) { + if (*ignored == fi_type) + return true; + } + + return false; +} + +/** + * Get the index of a node within a graph. + */ +static inline size_t +__flow_graph_get_node_index(const struct rte_flow_graph *graph, const struct rte_flow_graph_node *node) +{ + return (size_t)(node - graph->nodes); +} + +/** + * Check if a graph node is valid. + */ +static inline bool +__flow_graph_node_is_valid(const struct rte_flow_graph *graph, + const struct rte_flow_graph_node *node, + struct rte_flow_error *error) +{ + size_t node_idx; + + if (node == NULL) { + rte_flow_error_set(error, EINVAL, + RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL, + "Flow graph node pointer is NULL"); + return false; + } + + if (node->name == NULL) { + rte_flow_error_set(error, EINVAL, + RTE_FLOW_ERROR_TYPE_UNSPECIFIED, node, + "Flow graph node name is not defined"); + return false; + } + + node_idx = __flow_graph_get_node_index(graph, node); + + /* first node can't have callbacks because there's no item */ + if (node_idx == RTE_FLOW_NODE_FIRST && + (node->validate != NULL || node->process != NULL)) { + rte_flow_error_set(error, EINVAL, + RTE_FLOW_ERROR_TYPE_UNSPECIFIED, node, + "Flow graph start node callbacks are not allowed"); + return false; + } + + /* all non-END nodes must have edges */ + if (node->type != RTE_FLOW_ITEM_TYPE_END && + graph->edges[node_idx].next == NULL) { + rte_flow_error_set(error, EINVAL, + RTE_FLOW_ERROR_TYPE_UNSPECIFIED, node, + "Flow graph edge list is not defined for non-END node"); + return false; + } + + return true; +} + +/** + * Find the next node in the graph matching the given item type. + */ +static inline const struct rte_flow_graph_node * +__flow_graph_find_next_node(const struct rte_flow_graph *graph, + const struct rte_flow_graph_node *cur_node, + enum rte_flow_item_type next_type, + struct rte_flow_error *error) +{ + const size_t *next_nodes; + size_t cur_idx, edge_idx; + + if (!__flow_graph_node_is_valid(graph, cur_node, error)) + return NULL; + + cur_idx = __flow_graph_get_node_index(graph, cur_node); + next_nodes = graph->edges[cur_idx].next; + + for (edge_idx = 0; next_nodes[edge_idx] != RTE_FLOW_NODE_EDGE_END; edge_idx++) { + const struct rte_flow_graph_node *tmp = + &graph->nodes[next_nodes[edge_idx]]; + /* if node is invalid, graph is broken */ + if (!__flow_graph_node_is_valid(graph, tmp, error)) + return NULL; + if (tmp->type == next_type) + return tmp; + } + + return NULL; +} + +/** + * Visit (validate and extract) a node's item. + */ +static inline int +__flow_graph_visit_node(const struct rte_flow_graph_node *node, void *ctx, + const struct rte_flow_item *item, struct rte_flow_error *error) +{ + int ret; + + /* if we expect a certain type of node, check for it */ + if (item != NULL && !__flow_graph_node_is_expected(node, item, error)) + return -EINVAL; + + /* Does this node fit driver's criteria? */ + if (node->validate != NULL) { + ret = node->validate(ctx, item, error); + if (ret != 0) + return ret; + } + + /* Extract data from this item */ + if (node->process != NULL) { + ret = node->process(ctx, item, error); + if (ret != 0) + return ret; + } + + return 0; +} + +/** + * Parse and validate a flow pattern using the flow graph. + * + * Traverses the pattern items and validates them against the driver's graph + * structure. For each item, checks that the transition from the current node + * is allowed, then invokes validation and processing callbacks. + * + * @param graph + * Pointer to the driver's flow graph definition with nodes and edges. + * @param pattern + * Array of rte_flow_item structures to parse, terminated by RTE_FLOW_ITEM_TYPE_END. + * @param error + * Pointer to rte_flow_error structure for reporting failures. + * @param ctx + * Opaque driver context for accumulating parsed state. + * @return + * 0 on success, negative errno on failure (error is set). + */ +static inline int +rte_flow_graph_parse(const struct rte_flow_graph *graph, const struct rte_flow_item *pattern, + struct rte_flow_error *error, void *ctx) +{ + const struct rte_flow_graph_node *cur_node; + const struct rte_flow_item *item; + int ret; + + if (graph == NULL || graph->nodes == NULL || graph->edges == NULL) { + RTE_FLOW_GRAPH_LOG(DEBUG, "flow graph is not defined"); + return rte_flow_error_set(error, ENOTSUP, + RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL, + "Flow graph is not defined"); + } + if (pattern == NULL) { + RTE_FLOW_GRAPH_LOG(DEBUG, "flow pattern is NULL"); + return rte_flow_error_set(error, EINVAL, + RTE_FLOW_ERROR_TYPE_ITEM, NULL, + "Flow pattern is NULL"); + } + + /* use start node as traversal anchor */ + cur_node = &graph->nodes[RTE_FLOW_NODE_FIRST]; + + /* is the node valid? */ + if (!__flow_graph_node_is_valid(graph, cur_node, error)) { + /* error may be NULL */ + if (error != NULL) + RTE_FLOW_GRAPH_LOG(DEBUG, "%s", error->message); + return -EINVAL; + } + + /* Traverse pattern items */ + for (item = pattern; item->type != RTE_FLOW_ITEM_TYPE_END; item++) { + + /* Skip items in the graph's ignore list */ + if (__flow_graph_node_is_ignored(graph, item->type)) { + RTE_FLOW_GRAPH_LOG(DEBUG, "ignored item %s", + rte_flow_graph_item_type_to_str(item->type)); + continue; + } + + /* Find the next graph node for this item type */ + cur_node = __flow_graph_find_next_node(graph, cur_node, + item->type, error); + if (cur_node == NULL) { + RTE_FLOW_GRAPH_LOG(DEBUG, "cannot traverse to item %s", + rte_flow_graph_item_type_to_str(item->type)); + return rte_flow_error_set(error, ENOTSUP, + RTE_FLOW_ERROR_TYPE_ITEM, + item, "Pattern item not supported"); + } + RTE_FLOW_GRAPH_LOG(DEBUG, "processing %s", cur_node->name); + /* Validate and process the current item at this node */ + ret = __flow_graph_visit_node(cur_node, ctx, item, error); + if (ret != 0) { + /* error may be NULL */ + if (error != NULL) + RTE_FLOW_GRAPH_LOG(DEBUG, "%s", error->message); + return ret; + } + } + + /* Pattern items have ended but we still need to process the end */ + cur_node = __flow_graph_find_next_node(graph, cur_node, item->type, error); + if (cur_node == NULL) { + RTE_FLOW_GRAPH_LOG(DEBUG, "cannot traverse to item %s", + rte_flow_graph_item_type_to_str(item->type)); + return rte_flow_error_set(error, ENOTSUP, + RTE_FLOW_ERROR_TYPE_ITEM, + item, "Pattern item not supported"); + } + ret = __flow_graph_visit_node(cur_node, ctx, item, error); + if (ret != 0) { + /* error may be NULL */ + if (error != NULL) + RTE_FLOW_GRAPH_LOG(DEBUG, "%s", error->message); + return ret; + } + + return 0; +} + +#ifdef __cplusplus +} +#endif + +#endif /* _RTE_FLOW_GRAPH_H_ */ -- 2.52.0

