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
next prev 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