The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Dave Seddon <dave.seddon.ca@gmail.com>
To: netdev@vger.kernel.org
Cc: "David S . Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@google.com>,
	Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
	Stanislav Fomichev <sdf@fomichev.me>,
	Tom Herbert <tom@herbertland.com>,
	Willem de Bruijn <willemdebruijn.kernel@gmail.com>,
	linux-kernel@vger.kernel.org,
	Dave Seddon <dave.seddon.ca@gmail.com>
Subject: [PATCH net-next v1 02/11] net: flow_dissector: opt-in fast-path for eth + IPv{4,6} + {TCP,UDP}
Date: Wed, 15 Jul 2026 17:43:48 -0700	[thread overview]
Message-ID: <20260716004357.3652679-3-dave.seddon.ca@gmail.com> (raw)
In-Reply-To: <20260716004357.3652679-1-dave.seddon.ca@gmail.com>

A small targeted optimisation to __skb_flow_dissect(). A new
flow_dissect_fast() dispatcher routes the standard flow_keys dissector
calls through per-shape straight-line extractors for the common
eth + IPv4/IPv6 + TCP/UDP packet shapes. On any miss, falls back to the
existing graph walk. Output is byte-identical to the slow path for the
shapes handled.

This is the parent of the fast-path series. Subsequent patches add
per-shape extensions for single VLAN, QinQ, PPPoE, MPLS, IP-in-IP and
GRE. Each patch ships its own per-shape sysctl in the new
``/proc/sys/net/flow_dissector/`` subtree; this patch establishes the
subtree and registers the first entry, ``eth_ip``.

The fast path is invoked strictly after the netns BPF flow dissector
hook: if a BPF program is attached and returns a verdict other than
BPF_FLOW_DISSECTOR_CONTINUE, __skb_flow_dissect() returns before
flow_dissect_fast() is reached. Attached BPF dissectors therefore
always take precedence and observe no behaviour change from this
series.

  - What this does. Adds an opt-in fast-path for eth + IPv4 (IHL=5,
    not fragmented) + TCP/UDP and eth + IPv6 (no extension headers) +
    TCP/UDP. When the per-shape sysctl is enabled and a packet
    matches, the helper fills struct flow_keys byte-identically to the
    slow path and returns. On any miss it defers to the existing graph
    walk. Only the two standard dissectors are eligible
    (flow_keys_dissector and flow_keys_dissector_symmetric); custom
    dissectors and slow-path-only flags defer unconditionally.

  - How it gates. A new
    DEFINE_STATIC_KEY_FALSE(flow_dissector_eth_ip_key), externed in
    <net/flow_dissector.h>, toggled by
    /proc/sys/net/flow_dissector/eth_ip with proc_do_static_key. When
    the sysctl is 0 the dispatcher's per-shape case is a forward not-
    taken JMP — same slow-path cost as today. When 1, the helper runs
    inline.

  - Namespace. The sysctl subtree is /proc/sys/net/flow_dissector/
    (registered by a new late_initcall in net/core/flow_dissector.c
    via register_net_sysctl(&init_net, "net/flow_dissector", tbl) —
    same shape as net/mpls, net/bridge).

  - Forward declarations. flow_dissect_fast_ipv4 / _ipv6 are forward-
    declared at the top of the file (after includes). Subsequent
    patches add flow_dissect_fast_vlan etc. that call these helpers;
    anchoring the declarations at top of file makes the patch insertion
    order robust against kernel-version line-number drift.

  - Cost model. On a miss, dispatcher overhead is one ethertype switch
    (~5 cycles) plus the per-case static_branch test (~not-taken JMP
    when 0). On a hit, the straight-line body saves ~30 ns/skb of
    graph-walk cost on the common shape (Zen 2 / Cortex-A76; see the
    cover letter for the cross-uarch microbench table).

  - Output contract. Byte-identical with the slow path for the
    eligible shape. The IPv6 fast-path defers on any non-zero flow
    label: callers passing FLOW_DISSECTOR_F_STOP_AT_FLOW_LABEL --
    skb_get_hash(), i.e. RPS/RFS, fq, fq_codel, cake -- make the slow
    path stop at a non-zero label, and dissectors requesting the label
    key write it; deferring on the label itself keeps byte-identity
    for every combination without threading flags into the helpers,
    and zero-label IPv6 stays on the fast path. Fragmented IPv4
    packets defer. Custom dissectors defer, as do FLOW_DISSECTOR_F_*
    flags other than PARSE_1ST_FRAG and STOP_AT_FLOW_LABEL. Admitting
    STOP_AT_FLOW_LABEL is what makes the fast path reachable from
    skb_get_hash() at all -- the kernel's main dissection consumer.

Assisted-by: Claude:claude-fable-5 sparse smatch
Signed-off-by: Dave Seddon <dave.seddon.ca@gmail.com>
---
 Documentation/admin-guide/sysctl/net.rst |  27 ++-
 include/net/flow_dissector.h             |   5 +
 net/core/flow_dissector.c                | 273 ++++++++++++++++++++++-
 3 files changed, 302 insertions(+), 3 deletions(-)

diff --git a/Documentation/admin-guide/sysctl/net.rst b/Documentation/admin-guide/sysctl/net.rst
index e586e17fc7a5..aac82169e019 100644
--- a/Documentation/admin-guide/sysctl/net.rst
+++ b/Documentation/admin-guide/sysctl/net.rst
@@ -460,7 +460,32 @@ historical importance.
 
 Default: 0
 
-2. /proc/sys/net/unix - Parameters for Unix domain sockets
+2. /proc/sys/net/flow_dissector - Flow dissector fast-path per-shape sysctls
+---------------------------------------------------------------------------
+
+The flow dissector is invoked on every packet that needs a flow hash
+(RPS, RFS, ECMP, sch_cake, cls_flower, ...). For common shapes
+(Eth+IPv4/IPv6+TCP/UDP, plus per-shape extensions for VLAN/QinQ,
+PPPoE, MPLS, IP-in-IP and GRE added by this series), an opt-in
+fast-path bypasses the slow-path graph walk and writes
+``struct flow_keys`` directly. Each shape is gated by its own static
+key + sysctl so operators enable only what their deployment uses.
+
+All defaults are 0 (off). Flipping any of these on takes effect
+immediately; no kernel rebuild needed.
+
+eth_ip
+~~~~~~
+
+Eth + IPv4 (IHL=5, no fragmentation) + TCP/UDP, and Eth + IPv6 (no
+extension headers) + TCP/UDP. Output is byte-identical to the slow
+path on the eligible shape; non-matching packets fall through to the
+slow path unchanged. The dispatcher costs one not-taken JMP per call
+when this is 0.
+
+Default: 0
+
+3. /proc/sys/net/unix - Parameters for Unix domain sockets
 ----------------------------------------------------------
 
 There is only one file in this directory.
diff --git a/include/net/flow_dissector.h b/include/net/flow_dissector.h
index ced79dc8e856..7d513a6cbb55 100644
--- a/include/net/flow_dissector.h
+++ b/include/net/flow_dissector.h
@@ -425,6 +425,11 @@ __be32 flow_get_u32_dst(const struct flow_keys *flow);
 extern struct flow_dissector flow_keys_dissector;
 extern struct flow_dissector flow_keys_basic_dissector;
 
+/* Per-shape fast-path static-branch keys, toggled via
+ * /proc/sys/net/flow_dissector/<shape>. All default off.
+ */
+extern struct static_key_false flow_dissector_eth_ip_key;
+
 /* struct flow_keys_digest:
  *
  * This structure is used to hold a digest of the full flow keys. This is a
diff --git a/net/core/flow_dissector.c b/net/core/flow_dissector.c
index b8c59209b460..88b1bd1cc4ca 100644
--- a/net/core/flow_dissector.c
+++ b/net/core/flow_dissector.c
@@ -37,6 +37,33 @@
 #endif
 #include <linux/bpf-netns.h>
 
+/* Per-shape fast-path gates, one static_branch per shape so operators
+ * enable only what their deployment carries. All default off.
+ */
+DEFINE_STATIC_KEY_FALSE(flow_dissector_eth_ip_key);
+EXPORT_SYMBOL(flow_dissector_eth_ip_key);
+
+/* IPv4 version/IHL byte of an option-less header: version 4, IHL 5. */
+#define FLOW_DIS_IPV4_VIHL_NOOPT	0x45
+
+/* Fast-path helper forward declarations. */
+static bool flow_dissect_fast_ipv4(const struct sk_buff *skb,
+				   struct flow_dissector *flow_dissector,
+				   void *target_container,
+				   const void *data,
+				   int nhoff, int hlen);
+static bool flow_dissect_fast_ipv6(const struct sk_buff *skb,
+				   struct flow_dissector *flow_dissector,
+				   void *target_container,
+				   const void *data,
+				   int nhoff, int hlen);
+
+/* One of the two dissectors the fast-path eligibility check admits;
+ * defined here so flow_dissect_fast() below can reference it (its keys
+ * and init live near the bottom of the file).
+ */
+static struct flow_dissector flow_keys_dissector_symmetric __read_mostly;
+
 static void dissector_set_key(struct flow_dissector *flow_dissector,
 			      enum flow_dissector_key_id key_id)
 {
@@ -1043,6 +1070,223 @@ static bool is_pppoe_ses_hdr_valid(const struct pppoe_hdr *hdr)
 	return hdr->ver == 1 && hdr->type == 1 && hdr->code == 0;
 }
 
+/* The IPv4 fast-path assumes a 20-byte IPv4 header (IHL == 5). The
+ * runtime IHL check below is the dynamic invariant; this static
+ * assertion is the compile-time one, in the unlikely case struct
+ * iphdr ever grows.
+ */
+static_assert(sizeof(struct iphdr) == 20);
+
+/* Straight-line eth + IPv4 (IHL=5, not fragmented) + TCP|UDP. Returns
+ * true when @target_container has been filled byte-identically to the
+ * slow path; false on any miss (caller falls back to the slow path).
+ */
+static bool flow_dissect_fast_ipv4(const struct sk_buff *skb,
+				   struct flow_dissector *flow_dissector,
+				   void *target_container,
+				   const void *data,
+				   int nhoff, int hlen)
+{
+	struct flow_dissector_key_control *key_control;
+	struct flow_dissector_key_addrs *key_addrs;
+	struct flow_dissector_key_basic *key_basic;
+	struct flow_dissector_key_ports *key_ports;
+	const struct iphdr *iph;
+	int thoff;
+
+	if (unlikely(hlen - nhoff < (int)sizeof(*iph) + 4))
+		return false;
+
+	iph = (const struct iphdr *)((const u8 *)data + nhoff);
+
+	if (unlikely(*(const u8 *)iph != FLOW_DIS_IPV4_VIHL_NOOPT))
+		return false;
+
+	if (unlikely(iph->frag_off & htons(IP_MF | IP_OFFSET)))
+		return false;
+
+	if (unlikely(iph->protocol != IPPROTO_TCP &&
+		     iph->protocol != IPPROTO_UDP))
+		return false;
+
+	thoff = nhoff + (int)sizeof(*iph);
+
+	if (dissector_uses_key(flow_dissector,
+			       FLOW_DISSECTOR_KEY_CONTROL)) {
+		key_control = skb_flow_dissector_target(flow_dissector,
+							FLOW_DISSECTOR_KEY_CONTROL,
+							target_container);
+		key_control->addr_type = FLOW_DISSECTOR_KEY_IPV4_ADDRS;
+		key_control->thoff = min_t(u16, thoff,
+					   skb ? skb->len : hlen);
+		key_control->flags = 0;
+	}
+	if (dissector_uses_key(flow_dissector,
+			       FLOW_DISSECTOR_KEY_BASIC)) {
+		key_basic = skb_flow_dissector_target(flow_dissector,
+						      FLOW_DISSECTOR_KEY_BASIC,
+						      target_container);
+		key_basic->n_proto = htons(ETH_P_IP);
+		key_basic->ip_proto = iph->protocol;
+	}
+	if (dissector_uses_key(flow_dissector,
+			       FLOW_DISSECTOR_KEY_IPV4_ADDRS)) {
+		key_addrs = skb_flow_dissector_target(flow_dissector,
+						      FLOW_DISSECTOR_KEY_IPV4_ADDRS,
+						      target_container);
+		memcpy(&key_addrs->v4addrs.src, &iph->saddr,
+		       sizeof(key_addrs->v4addrs.src));
+		memcpy(&key_addrs->v4addrs.dst, &iph->daddr,
+		       sizeof(key_addrs->v4addrs.dst));
+	}
+	if (dissector_uses_key(flow_dissector,
+			       FLOW_DISSECTOR_KEY_PORTS)) {
+		const __be32 *ports = (const __be32 *)
+			((const u8 *)data + thoff);
+
+		key_ports = skb_flow_dissector_target(flow_dissector,
+						      FLOW_DISSECTOR_KEY_PORTS,
+						      target_container);
+		key_ports->ports = *ports;
+	}
+
+	return true;
+}
+
+/* Same as flow_dissect_fast_ipv4 but for a fixed 40-byte IPv6 header
+ * (no extension headers). The nexthdr check below holds the run-time
+ * invariant; the static_assert holds the compile-time one.
+ */
+static_assert(sizeof(struct ipv6hdr) == 40);
+
+static bool flow_dissect_fast_ipv6(const struct sk_buff *skb,
+				   struct flow_dissector *flow_dissector,
+				   void *target_container,
+				   const void *data,
+				   int nhoff, int hlen)
+{
+	struct flow_dissector_key_control *key_control;
+	struct flow_dissector_key_addrs *key_addrs;
+	struct flow_dissector_key_basic *key_basic;
+	struct flow_dissector_key_ports *key_ports;
+	const struct ipv6hdr *iph;
+	int thoff;
+
+	if (unlikely(hlen - nhoff < (int)sizeof(*iph) + 4))
+		return false;
+
+	iph = (const struct ipv6hdr *)((const u8 *)data + nhoff);
+
+	if (unlikely((*(const u8 *)iph >> 4) != 6))
+		return false;
+
+	/* Any non-zero flow label defers, checked before everything else
+	 * (including the tunnel descents below). Callers passing
+	 * FLOW_DISSECTOR_F_STOP_AT_FLOW_LABEL (skb_get_hash() -- so
+	 * RPS/RFS, fq, cake) make the slow path stop at a non-zero label
+	 * even on a tunnel outer, and dissectors requesting the label key
+	 * write it; both diverge from the plain parse. Deferring on the
+	 * label itself covers every combination without threading flags
+	 * into the helpers, and zero-label IPv6 (the overwhelmingly
+	 * common case) stays on the fast path.
+	 */
+	if (unlikely((iph->flow_lbl[0] & 0x0f) |
+		     iph->flow_lbl[1] | iph->flow_lbl[2]))
+		return false;
+
+	if (unlikely(iph->nexthdr != IPPROTO_TCP &&
+		     iph->nexthdr != IPPROTO_UDP))
+		return false;
+
+	thoff = nhoff + (int)sizeof(*iph);
+
+	if (dissector_uses_key(flow_dissector,
+			       FLOW_DISSECTOR_KEY_CONTROL)) {
+		key_control = skb_flow_dissector_target(flow_dissector,
+							FLOW_DISSECTOR_KEY_CONTROL,
+							target_container);
+		key_control->addr_type = FLOW_DISSECTOR_KEY_IPV6_ADDRS;
+		key_control->thoff = min_t(u16, thoff,
+					   skb ? skb->len : hlen);
+		key_control->flags = 0;
+	}
+	if (dissector_uses_key(flow_dissector,
+			       FLOW_DISSECTOR_KEY_BASIC)) {
+		key_basic = skb_flow_dissector_target(flow_dissector,
+						      FLOW_DISSECTOR_KEY_BASIC,
+						      target_container);
+		key_basic->n_proto = htons(ETH_P_IPV6);
+		key_basic->ip_proto = iph->nexthdr;
+	}
+	if (dissector_uses_key(flow_dissector,
+			       FLOW_DISSECTOR_KEY_IPV6_ADDRS)) {
+		key_addrs = skb_flow_dissector_target(flow_dissector,
+						      FLOW_DISSECTOR_KEY_IPV6_ADDRS,
+						      target_container);
+		memcpy(&key_addrs->v6addrs.src, &iph->saddr,
+		       sizeof(key_addrs->v6addrs.src));
+		memcpy(&key_addrs->v6addrs.dst, &iph->daddr,
+		       sizeof(key_addrs->v6addrs.dst));
+	}
+	if (dissector_uses_key(flow_dissector,
+			       FLOW_DISSECTOR_KEY_PORTS)) {
+		const __be32 *ports = (const __be32 *)
+			((const u8 *)data + thoff);
+
+		key_ports = skb_flow_dissector_target(flow_dissector,
+						      FLOW_DISSECTOR_KEY_PORTS,
+						      target_container);
+		key_ports->ports = *ports;
+	}
+
+	return true;
+}
+
+/* Top-level dispatcher: eligibility check (only the two standard
+ * dissectors and flag subset) + per-proto switch with per-shape
+ * static_branch gating. Each case's branch is a forward not-taken JMP
+ * when its sysctl is 0 — matches the slow-path cost.
+ */
+static bool flow_dissect_fast(const struct sk_buff *skb,
+			      struct flow_dissector *flow_dissector,
+			      void *target_container,
+			      const void *data,
+			      __be16 proto, int nhoff, int hlen,
+			      unsigned int flags)
+{
+	if (flow_dissector != &flow_keys_dissector &&
+	    flow_dissector != &flow_keys_dissector_symmetric)
+		return false;
+
+	/* skb_get_hash() -- behind RPS/RFS, fq, fq_codel, cake -- passes
+	 * FLOW_DISSECTOR_F_STOP_AT_FLOW_LABEL on every dissect, so the
+	 * flag must be admitted or the fast path never runs for the
+	 * kernel's main consumers. Its only semantic (stop early on a
+	 * non-zero IPv6 flow label) is subsumed by the IPv6 fast path
+	 * deferring on any non-zero label. Any other flag defers.
+	 */
+	if (flags & ~(unsigned int)(FLOW_DISSECTOR_F_PARSE_1ST_FRAG |
+				    FLOW_DISSECTOR_F_STOP_AT_FLOW_LABEL))
+		return false;
+
+	switch (proto) {
+	case htons(ETH_P_IP):
+		if (!static_branch_unlikely(&flow_dissector_eth_ip_key))
+			return false;
+		return flow_dissect_fast_ipv4(skb, flow_dissector,
+					      target_container, data,
+					      nhoff, hlen);
+	case htons(ETH_P_IPV6):
+		if (!static_branch_unlikely(&flow_dissector_eth_ip_key))
+			return false;
+		return flow_dissect_fast_ipv6(skb, flow_dissector,
+					      target_container, data,
+					      nhoff, hlen);
+	default:
+		return false;
+	}
+}
+
 /**
  * __skb_flow_dissect - extract the flow_keys struct and return it
  * @net: associated network namespace, derived from @skb if NULL
@@ -1183,6 +1427,13 @@ bool __skb_flow_dissect(const struct net *net,
 		rcu_read_unlock();
 	}
 
+	/* Opt-in fast-path; flow_dissect_fast() does the eligibility check
+	 * and per-shape gating.
+	 */
+	if (flow_dissect_fast(skb, flow_dissector, target_container,
+			      data, proto, nhoff, hlen, flags))
+		return true;
+
 	if (dissector_uses_key(flow_dissector,
 			       FLOW_DISSECTOR_KEY_ETH_ADDRS)) {
 		struct flow_dissector_key_eth_addrs *key_eth_addrs;
@@ -1879,8 +2130,6 @@ void make_flow_keys_digest(struct flow_keys_digest *digest,
 }
 EXPORT_SYMBOL(make_flow_keys_digest);
 
-static struct flow_dissector flow_keys_dissector_symmetric __read_mostly;
-
 u32 __skb_get_hash_symmetric_net(const struct net *net, const struct sk_buff *skb)
 {
 	struct flow_keys keys;
@@ -2116,3 +2365,23 @@ static int __init init_default_flow_dissectors(void)
 	return 0;
 }
 core_initcall(init_default_flow_dissectors);
+
+/* Per-shape fast-path sysctls under /proc/sys/net/flow_dissector/. */
+static struct ctl_table flow_dissector_sysctl_table[] = {
+	{
+		.procname	= "eth_ip",
+		.data		= &flow_dissector_eth_ip_key.key,
+		.maxlen		= sizeof(flow_dissector_eth_ip_key),
+		.mode		= 0644,
+		.proc_handler	= proc_do_static_key,
+	},
+};
+
+static int __init flow_dissector_sysctl_init(void)
+{
+	if (!register_net_sysctl(&init_net, "net/flow_dissector",
+				 flow_dissector_sysctl_table))
+		return -ENOMEM;
+	return 0;
+}
+late_initcall(flow_dissector_sysctl_init);
-- 
2.54.0


  parent reply	other threads:[~2026-07-16  0:44 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-16  0:43 [PATCH net-next v1 00/11] net: flow_dissector: opt-in byte-identical fast paths for common shapes Dave Seddon
2026-07-16  0:43 ` [PATCH net-next v1 01/11] net: flow_dissector: gate BPF program lookup behind a static key Dave Seddon
2026-07-16  0:43 ` Dave Seddon [this message]
2026-07-16  0:43 ` [PATCH net-next v1 03/11] net: flow_dissector: add fast-path for VLAN and QinQ + IP + TCP/UDP Dave Seddon
2026-07-16  0:43 ` [PATCH net-next v1 04/11] net: flow_dissector: add fast-path for PPPoE session + IPv{4,6} " Dave Seddon
2026-07-16  0:43 ` [PATCH net-next v1 05/11] net: flow_dissector: add fast-path for single MPLS label + IP Dave Seddon
2026-07-16  0:43 ` [PATCH net-next v1 06/11] net: flow_dissector: add fast-path for IP-in-IP family (IPIP / 4in6 / 6in4) Dave Seddon
2026-07-16  0:43 ` [PATCH net-next v1 07/11] net: flow_dissector: add byte-identical fast-path for plain GRE inner Dave Seddon
2026-07-16  0:43 ` [PATCH net-next v1 08/11] net: flow_dissector: per-shape counters + /proc/net/flow_dissector_stats Dave Seddon
2026-07-16  0:43 ` [PATCH net-next v1 09/11] net: flow_dissector: bound fast-path tunnel recursion Dave Seddon
2026-07-16  0:43 ` [PATCH net-next v1 10/11] net: flow_dissector: add KUnit fast/slow path equivalence tests Dave Seddon
2026-07-16  0:43 ` [PATCH net-next v1 11/11] Documentation: networking: add flow_dissector overview and fast-path guide Dave Seddon
2026-07-16  9:50 ` [PATCH net-next v1 00/11] net: flow_dissector: opt-in byte-identical fast paths for common shapes Willem de Bruijn

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260716004357.3652679-3-dave.seddon.ca@gmail.com \
    --to=dave.seddon.ca@gmail.com \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=sdf@fomichev.me \
    --cc=tom@herbertland.com \
    --cc=willemdebruijn.kernel@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox