* [PATCH nf v2 0/6] netfilter: harden conntrack vs ingress pipeline rewrites
@ 2026-09-17 13:09 Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 1/6] netfilter: nf_conntrack: validate skb->_nfct and packet headers Florian Westphal
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Florian Westphal @ 2026-09-17 13:09 UTC (permalink / raw)
To: netfilter-devel; +Cc: Florian Westphal
v2: address LLM comments. Only changes are in patches 1 and 6.
netfilter is very allergic to packets changing while they are within the
processing pipeline.
- we rely on ip/ipv6 stack to check ip header integrity, later
parts of conntrack, e.g. helpers, rely on conntrack to have
sanity-checked e.g. th->doff.
- parts that bypass inet (e.g. bridge) replicate those sanity
checks on l3 headers.
- Other hardening changes to nf_queue and nft_payload.c have clamped
down on the ability to mangle packet in arbitary ways in-between
hooks.
Another remaining problem is conntrack itself: if nf_conntrack_in finds
the skb already has an nf_conn attached, no further checks are done.
This isn't correct anymore, such nf_conn could have been attached by
output hook (loopback case) or tc conntrack action. In between those
mangling is possible. This patchset aims to add sanity checks for this.
1) Validate skb->_nfct against current L3/L4 headers from nf_conntrack_in().
Drop stale conntrack references and trigger re-lookups if mismatches occur.
2) Refactor nf_confirm() logic into separate functions for protocol offset
determination and helper calls.
3) Verify L4 protocol matches the helper's expected protocol before calling a
conntrack helper. Skip IPv4 fragments and packets without payload. Update
nft_ct to set the L4 protocol in newly allocated helpers.
4) Replace open-coded conntrack helper invocation with nf_ct_call_helper().
Add checks to ensure the helper can process packets. This also reduces
copypaste with tc and ovs.
5) Harden nf_conntrack helper invocation via tuple revalidation. Verify
packet tuples match connection tracking entries. Check for sane TCP
headers in TCP traffic.
6) Validate timeout object protocols against the conntrack tuple protocol
before attachment. Prevent potential out-of-bounds reads caused by
protocol state mismatches.
Earlier attempt to fix offenders instead:
https://lore.kernel.org/netdev/20260819204210.23722-1-fw@strlen.de/
Florian Westphal (6):
netfilter: nf_conntrack: validate skb->_nfct and packet headers
netfilter: nf_conntrack: refactor helper call logic in nf_confirm()
netfilter: nf_conntrack: verify L4 protocol before calling helper
netfilter: conntrack: replace open-coded helper invocation
netfilter: nf_conntrack: harden helper invocation with tuple
revalidation
netfilter: nft_ct: validate timeout object protocol
include/net/netfilter/nf_conntrack_helper.h | 2 +
net/netfilter/nf_conntrack_core.c | 115 ++++++++++++--
net/netfilter/nf_conntrack_ovs.c | 53 +------
net/netfilter/nf_conntrack_proto.c | 166 ++++++++++++++++----
net/netfilter/nft_ct.c | 49 ++++--
5 files changed, 281 insertions(+), 104 deletions(-)
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v2 nf 1/6] netfilter: nf_conntrack: validate skb->_nfct and packet headers
2026-09-17 13:09 [PATCH nf v2 0/6] netfilter: harden conntrack vs ingress pipeline rewrites Florian Westphal
@ 2026-09-17 13:09 ` Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 2/6] netfilter: nf_conntrack: refactor helper call logic in nf_confirm() Florian Westphal
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Florian Westphal @ 2026-09-17 13:09 UTC (permalink / raw)
To: netfilter-devel; +Cc: Florian Westphal
skbs coming from loopback already have skb->_nfct attached.
act_ct may also attach a conntrack to the skb.
Facilities like clsact or pedit can alter headers which may then result
in e.g. UDP packet with TCP nf_conn.
Revalidate that this skb matches the skb l3/l4 header. If not, drop the
stale reference and let nf_conntrack_in perform a re-lookup.
For conntrack helpers, more checks may be required, e.g. tcph->doff
revalidation. This is handled in a followup patch.
Note that the Fixes tag is bogus, back then this was perfectly fine:
namespaces, esp. unprivileged user namespaces, did not exist and all
crash-configs were in the "don't do that, then" department.
Fixes: 9fb9cbb1082d ("[NETFILTER]: Add nf_conntrack subsystem.")
Signed-off-by: Florian Westphal <fw@strlen.de>
---
v2: more complicated checks to handle NAT on loopback.
Test for this has been committed to nftables.git.
net/netfilter/nf_conntrack_core.c | 115 +++++++++++++++++++++++++++---
1 file changed, 106 insertions(+), 9 deletions(-)
diff --git a/net/netfilter/nf_conntrack_core.c b/net/netfilter/nf_conntrack_core.c
index d0d9e5ea84a0..14b49045d758 100644
--- a/net/netfilter/nf_conntrack_core.c
+++ b/net/netfilter/nf_conntrack_core.c
@@ -2000,6 +2000,102 @@ static int nf_conntrack_handle_packet(struct nf_conn *ct,
return generic_packet(ct, skb, ctinfo);
}
+
+static bool nf_ct_check_icmp_inner(const struct sk_buff *skb,
+ const struct nf_hook_state *state,
+ unsigned int dataoff,
+ struct nf_conn *ct,
+ enum ip_conntrack_dir dir)
+{
+ unsigned int nhoff = skb_network_offset(skb);
+ static const unsigned int icmp_hdrsz = 8;
+ struct nf_conntrack_tuple tuple;
+
+ nhoff += dataoff + icmp_hdrsz;
+
+ if (!nf_ct_get_tuplepr(skb, nhoff, state->pf, state->net, &tuple))
+ return false;
+
+ return nf_ct_tuple_equal(&tuple, nf_ct_tuple(ct, !dir));
+}
+
+/**
+ * nf_ct_get_careful - Get connection tracking entry with protocol revalidation
+ *
+ * @skb: Socket buffer whose conntrack entry is to be retrieved
+ * @state: Netfilter hook state
+ * @dataoff: Offset to the layer-4 header within @skb
+ * @protonum: protocol number (e.g., IPPROTO_TCP)
+ * @ctinfo: Pointer to store ip_conntrack_info
+ *
+ * This function retrieves the connection tracking entry associated with @skb.
+ * Performs revalidation of packet header, unlike nf_ct_get().
+ *
+ * If earlier in-kernel mangling (e.g., via TC pedit or clsact) altered packet
+ * contents (e.g. changing TCP to UDP), this function will drop the conntrack
+ * reference and returns NULL. skbs coming in via NF_INET_LOCAL_OUT are
+ * trusted and never revalidated.
+ *
+ * Returns:
+ * %NULL - No valid conntrack entry exists or revalidation failed.
+ * Pointer to &struct nf_conn associated with skb, may be a template.
+ */
+static struct nf_conn *
+nf_ct_get_careful(struct sk_buff *skb,
+ const struct nf_hook_state *state,
+ unsigned int dataoff, u8 protonum,
+ enum ip_conntrack_info *ctinfo)
+{
+ struct nf_conn *tmpl = nf_ct_get(skb, ctinfo);
+ struct nf_conntrack_tuple inverse;
+ struct nf_conntrack_tuple tuple;
+ enum ip_conntrack_dir dir;
+
+ /* LOCAL_OUT is trusted: in case stack sends ICMP error, skb gets
+ * the conntrack assigned via nf_ct_attach().
+ *
+ * Such conntrack may not even be in hashtable yet, so
+ * nf_conntrack_handle_icmp() cannot find a connection matching
+ * the inner header.
+ */
+ if (state->hook == NF_INET_LOCAL_OUT)
+ return tmpl;
+
+ if (!tmpl || nf_ct_is_template(tmpl))
+ return tmpl;
+
+ if (state->pf != nf_ct_l3num(tmpl))
+ goto error;
+
+ if (!nf_ct_get_tuple(skb, skb_network_offset(skb),
+ dataoff, state->pf, protonum, state->net,
+ &tuple))
+ goto error;
+
+ dir = CTINFO2DIR(*ctinfo);
+ if (nf_ct_tuple_equal(&tuple, nf_ct_tuple(tmpl, dir)))
+ return tmpl;
+
+ if (*ctinfo == IP_CT_RELATED || *ctinfo == IP_CT_RELATED_REPLY) {
+ if (state->pf == NFPROTO_IPV4 && protonum == IPPROTO_ICMP &&
+ nf_ct_check_icmp_inner(skb, state, dataoff, tmpl, dir))
+ return tmpl;
+
+ if (state->pf == NFPROTO_IPV6 && protonum == IPPROTO_ICMPV6 &&
+ nf_ct_check_icmp_inner(skb, state, dataoff, tmpl, dir))
+ return tmpl;
+ }
+
+ if (!nf_ct_invert_tuple(&inverse, &tuple))
+ goto error;
+
+ if ((tmpl->status & IPS_NAT_MASK) && nf_ct_tuple_equal(&inverse, nf_ct_tuple(tmpl, !dir)))
+ return tmpl;
+error:
+ nf_reset_ct(skb);
+ return NULL;
+}
+
unsigned int
nf_conntrack_in(struct sk_buff *skb, const struct nf_hook_state *state)
{
@@ -2008,21 +2104,22 @@ nf_conntrack_in(struct sk_buff *skb, const struct nf_hook_state *state)
u_int8_t protonum;
int dataoff, ret;
- tmpl = nf_ct_get(skb, &ctinfo);
+ /* rcu_read_lock()ed by nf_hook_thresh */
+ dataoff = get_l4proto(skb, skb_network_offset(skb), state->pf, &protonum);
+ if (dataoff <= 0) {
+ NF_CT_STAT_INC_ATOMIC(state->net, invalid);
+ nf_reset_ct(skb);
+ return NF_ACCEPT;
+ }
+
+ tmpl = nf_ct_get_careful(skb, state, dataoff, protonum, &ctinfo);
if (tmpl || ctinfo == IP_CT_UNTRACKED) {
/* Previously seen (loopback or untracked)? Ignore. */
if ((tmpl && !nf_ct_is_template(tmpl)) ||
ctinfo == IP_CT_UNTRACKED)
return NF_ACCEPT;
- skb->_nfct = 0;
- }
- /* rcu_read_lock()ed by nf_hook_thresh */
- dataoff = get_l4proto(skb, skb_network_offset(skb), state->pf, &protonum);
- if (dataoff <= 0) {
- NF_CT_STAT_INC_ATOMIC(state->net, invalid);
- ret = NF_ACCEPT;
- goto out;
+ skb->_nfct = 0;
}
if (protonum == IPPROTO_ICMP || protonum == IPPROTO_ICMPV6) {
--
2.55.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v2 nf 2/6] netfilter: nf_conntrack: refactor helper call logic in nf_confirm()
2026-09-17 13:09 [PATCH nf v2 0/6] netfilter: harden conntrack vs ingress pipeline rewrites Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 1/6] netfilter: nf_conntrack: validate skb->_nfct and packet headers Florian Westphal
@ 2026-09-17 13:09 ` Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 3/6] netfilter: nf_conntrack: verify L4 protocol before calling helper Florian Westphal
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Florian Westphal @ 2026-09-17 13:09 UTC (permalink / raw)
To: netfilter-devel; +Cc: Florian Westphal
The current implementation of nf_confirm() handles several distinct
tasks: retrieving the connection, calculating protocol offsets for
different network families, and executing the associated helpers or
sequence adjustments.
Move functionality to extra functions:
1. nf_confirm_get_protoff(): Isolates the logic for determining the
protocol offset (protoff) for IPv4 and IPv6 packets.
2. nf_ct_call_helper(): Encapsulates the decision-making process of
whether to call a connection tracking helper or perform sequence
adjustment, utilizing nf_confirm_get_protoff() to find the correct
offset.
NF_CT_STAT_INC_ATOMIC() is now incremented for any NF_DROP, earlier
this was elided in some cases, IMO that was a bug all along.
Followup patches will extend these new functions with stricter checks.
Fixes: 9fb9cbb1082d ("[NETFILTER]: Add nf_conntrack subsystem.")
Signed-off-by: Florian Westphal <fw@strlen.de>
---
include/net/netfilter/nf_conntrack_helper.h | 2 +
net/netfilter/nf_conntrack_proto.c | 113 +++++++++++++++-----
2 files changed, 87 insertions(+), 28 deletions(-)
diff --git a/include/net/netfilter/nf_conntrack_helper.h b/include/net/netfilter/nf_conntrack_helper.h
index 335b8c43694f..4b5ee758cff3 100644
--- a/include/net/netfilter/nf_conntrack_helper.h
+++ b/include/net/netfilter/nf_conntrack_helper.h
@@ -124,6 +124,8 @@ int __nf_ct_try_assign_helper(struct nf_conn *ct, struct nf_conn *tmpl,
int nf_ct_helper(struct sk_buff *skb, struct nf_conn *ct,
enum ip_conntrack_info ctinfo, u16 proto);
+int nf_ct_call_helper(struct sk_buff *skb, struct nf_conn *ct,
+ enum ip_conntrack_info ctinfo);
int nf_ct_add_helper(struct nf_conn *ct, const char *name, u8 family,
u8 proto, bool nat, struct nf_conntrack_helper **hp);
diff --git a/net/netfilter/nf_conntrack_proto.c b/net/netfilter/nf_conntrack_proto.c
index 7a40e4e0e33e..32f900c155de 100644
--- a/net/netfilter/nf_conntrack_proto.c
+++ b/net/netfilter/nf_conntrack_proto.c
@@ -131,36 +131,16 @@ static bool in_vrf_postrouting(const struct nf_hook_state *state)
return false;
}
-unsigned int nf_confirm(void *priv,
- struct sk_buff *skb,
- const struct nf_hook_state *state)
+static bool nf_confirm_get_protoff(struct sk_buff *skb, struct net *net,
+ struct nf_conn *ct,
+ enum ip_conntrack_info ctinfo,
+ unsigned int *protoffp)
{
- int (*helper_cb)(struct sk_buff *skb, unsigned int protoff,
- struct nf_conn *ct,
- enum ip_conntrack_info conntrackinfo);
- const struct nf_conn_help *help;
- enum ip_conntrack_info ctinfo;
unsigned int protoff;
- struct nf_conn *ct;
- bool seqadj_needed;
__be16 frag_off;
int start;
u8 pnum;
- ct = nf_ct_get(skb, &ctinfo);
- if (!ct || in_vrf_postrouting(state))
- return NF_ACCEPT;
-
- help = nfct_help(ct);
-
- seqadj_needed = test_bit(IPS_SEQ_ADJUST_BIT, &ct->status) && !nf_is_loopback_packet(skb);
- if (!help && !seqadj_needed)
- return nf_conntrack_confirm(skb);
-
- /* helper->help() do not expect ICMP packets */
- if (ctinfo == IP_CT_RELATED_REPLY)
- return nf_conntrack_confirm(skb);
-
switch (nf_ct_l3num(ct)) {
case NFPROTO_IPV4:
protoff = skb_network_offset(skb) + ip_hdrlen(skb);
@@ -169,16 +149,56 @@ unsigned int nf_confirm(void *priv,
pnum = ipv6_hdr(skb)->nexthdr;
start = ipv6_skip_exthdr(skb, sizeof(struct ipv6hdr), &pnum, &frag_off);
if (start < 0 || (frag_off & htons(~0x7)) != 0)
- return nf_conntrack_confirm(skb);
+ return false;
protoff = start;
break;
default:
- return nf_conntrack_confirm(skb);
+ DEBUG_NET_WARN_ONCE(1, "helper invoked on non-IP family!");
+ return false;
}
+ *protoffp = protoff;
+ return true;
+}
+
+/**
+ * nf_ct_call_helper() - Invoke the connection tracking helper for a packet
+ * @skb: The socket buffer containing the packet to be processed.
+ * @ct: The connection tracking entry associated with this packet.
+ * @ctinfo: The current connection track information (state) of the packet.
+ *
+ * This function calls the l4 connection tracking helper (e.g. ftp, sip...) if
+ * one was assigned to the connection.
+ *
+ * Return: verdict (NF_ACCEPT, NF_DROP, ...)
+ */
+int nf_ct_call_helper(struct sk_buff *skb, struct nf_conn *ct,
+ enum ip_conntrack_info ctinfo)
+{
+ struct net *net = nf_ct_net(ct);
+ const struct nf_conn_help *help;
+ bool seqadj_needed;
+ unsigned int protoff;
+
+ help = nfct_help(ct);
+
+ seqadj_needed = test_bit(IPS_SEQ_ADJUST_BIT, &ct->status) && !nf_is_loopback_packet(skb);
+ if (!help && !seqadj_needed)
+ return NF_ACCEPT;
+
+ /* helper->help() do not expect ICMP packets */
+ if (ctinfo == IP_CT_RELATED_REPLY)
+ return NF_ACCEPT;
+
+ if (!nf_confirm_get_protoff(skb, net, ct, ctinfo, &protoff))
+ return NF_ACCEPT;
+
if (help) {
const struct nf_conntrack_helper *helper;
+ int (*helper_cb)(struct sk_buff *skb, unsigned int protoff,
+ struct nf_conn *ct,
+ enum ip_conntrack_info conntrackinfo);
int ret;
/* rcu_read_lock()ed by nf_hook */
@@ -195,9 +215,46 @@ unsigned int nf_confirm(void *priv,
}
if (seqadj_needed &&
- !nf_ct_seq_adjust(skb, ct, ctinfo, protoff)) {
- NF_CT_STAT_INC_ATOMIC(nf_ct_net(ct), drop);
+ !nf_ct_seq_adjust(skb, ct, ctinfo, protoff))
return NF_DROP;
+
+ return NF_ACCEPT;
+}
+
+/**
+ * nf_confirm - Confirm (commit) a connection tracking entry
+ * @priv: Private data from the netfilter hook registration (unused)
+ * @skb: Packet
+ * @state: Netfilter hook state containing context (netns, hooknum, in/out devices)
+ *
+ * This function is invoked as the final step of the connection tracking
+ * processing pipeline. It performs two main operations:
+ *
+ * 1. Invokes the associated conntrack helper (if registered) to allow
+ * layer 7 specific parsing and NAT mangling.
+ *
+ * 2. Commits a new conntrack entry to the conntrack hash table, making it
+ * globally visible and enabling future packet lookups for stateful tracking.
+ *
+ * Returns: A netfilter verdict, e.g. NF_ACCEPT or NF_DROP.
+ */
+unsigned int nf_confirm(void *priv,
+ struct sk_buff *skb,
+ const struct nf_hook_state *state)
+{
+ enum ip_conntrack_info ctinfo;
+ struct nf_conn *ct;
+ int ret;
+
+ ct = nf_ct_get(skb, &ctinfo);
+ if (!ct || in_vrf_postrouting(state))
+ return NF_ACCEPT;
+
+ ret = nf_ct_call_helper(skb, ct, ctinfo);
+ if (unlikely(ret != NF_ACCEPT)) {
+ if (ret == NF_DROP)
+ NF_CT_STAT_INC_ATOMIC(state->net, drop);
+ return ret;
}
/* We've seen it coming out the other side: confirm it */
--
2.55.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v2 nf 3/6] netfilter: nf_conntrack: verify L4 protocol before calling helper
2026-09-17 13:09 [PATCH nf v2 0/6] netfilter: harden conntrack vs ingress pipeline rewrites Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 1/6] netfilter: nf_conntrack: validate skb->_nfct and packet headers Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 2/6] netfilter: nf_conntrack: refactor helper call logic in nf_confirm() Florian Westphal
@ 2026-09-17 13:09 ` Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 4/6] netfilter: conntrack: replace open-coded helper invocation Florian Westphal
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Florian Westphal @ 2026-09-17 13:09 UTC (permalink / raw)
To: netfilter-devel; +Cc: Florian Westphal
This commit ensures that a conntrack helper is only invoked if its
configured L4 protocol matches the actual protocol of the packet.
Currently, nf_ct_call_helper() invokes a helper based on the connection
state, but it does not explicitly verify the protocol number against
the helper's expected protocol. This could potentially lead to helpers
processing packets they were not designed for.
This can happen with related ICMP(v6) errors in the original direction
or when conntrack got attached via TC/act_ct, then was later munged via
act_pipe to alter the ip protocol.
Extend the introduced helper functions: extract the L4 protocol number,
then check it matches helper->l4proto.
Also change ipv4 to explicitly skip frasgments, this shouldn't happen
for normal netfilter-only code path, because conntrack depends on defrag
module, but this may not be the case for other users (ovs, tc).
Also skip the helper invocation if the packet contains no payload
(protoff == skb->len).
nft_ct.c needs to set l4proto in the new helper it allocates to avoid
tripping the new helper->l4proto != pnum guard.
Fixes: 9fb9cbb1082d ("[NETFILTER]: Add nf_conntrack subsystem.")
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/nf_conntrack_proto.c | 23 ++++++++++++++++-------
net/netfilter/nft_ct.c | 1 +
2 files changed, 17 insertions(+), 7 deletions(-)
diff --git a/net/netfilter/nf_conntrack_proto.c b/net/netfilter/nf_conntrack_proto.c
index 32f900c155de..18125aa29e0d 100644
--- a/net/netfilter/nf_conntrack_proto.c
+++ b/net/netfilter/nf_conntrack_proto.c
@@ -134,21 +134,23 @@ static bool in_vrf_postrouting(const struct nf_hook_state *state)
static bool nf_confirm_get_protoff(struct sk_buff *skb, struct net *net,
struct nf_conn *ct,
enum ip_conntrack_info ctinfo,
- unsigned int *protoffp)
+ unsigned int *protoffp, u8 *pnum)
{
unsigned int protoff;
__be16 frag_off;
int start;
- u8 pnum;
switch (nf_ct_l3num(ct)) {
case NFPROTO_IPV4:
+ if (ip_is_fragment(ip_hdr(skb)))
+ return false;
protoff = skb_network_offset(skb) + ip_hdrlen(skb);
+ *pnum = ip_hdr(skb)->protocol;
break;
case NFPROTO_IPV6:
- pnum = ipv6_hdr(skb)->nexthdr;
- start = ipv6_skip_exthdr(skb, sizeof(struct ipv6hdr), &pnum, &frag_off);
- if (start < 0 || (frag_off & htons(~0x7)) != 0)
+ *pnum = ipv6_hdr(skb)->nexthdr;
+ start = ipv6_skip_exthdr(skb, sizeof(struct ipv6hdr), pnum, &frag_off);
+ if (start < 0 || frag_off)
return false;
protoff = start;
@@ -170,6 +172,9 @@ static bool nf_confirm_get_protoff(struct sk_buff *skb, struct net *net,
*
* This function calls the l4 connection tracking helper (e.g. ftp, sip...) if
* one was assigned to the connection.
+ * This re-derives the l4 protocol number: act_ct can associate the skb with
+ * a connection/protocol and later pedit/bpf function can munge the packet
+ * afterwards.
*
* Return: verdict (NF_ACCEPT, NF_DROP, ...)
*/
@@ -180,6 +185,7 @@ int nf_ct_call_helper(struct sk_buff *skb, struct nf_conn *ct,
const struct nf_conn_help *help;
bool seqadj_needed;
unsigned int protoff;
+ u8 pnum = 0;
help = nfct_help(ct);
@@ -191,10 +197,10 @@ int nf_ct_call_helper(struct sk_buff *skb, struct nf_conn *ct,
if (ctinfo == IP_CT_RELATED_REPLY)
return NF_ACCEPT;
- if (!nf_confirm_get_protoff(skb, net, ct, ctinfo, &protoff))
+ if (!nf_confirm_get_protoff(skb, net, ct, ctinfo, &protoff, &pnum))
return NF_ACCEPT;
- if (help) {
+ if (help && protoff < skb->len) {
const struct nf_conntrack_helper *helper;
int (*helper_cb)(struct sk_buff *skb, unsigned int protoff,
struct nf_conn *ct,
@@ -204,6 +210,9 @@ int nf_ct_call_helper(struct sk_buff *skb, struct nf_conn *ct,
/* rcu_read_lock()ed by nf_hook */
helper = rcu_dereference(help->helper);
if (helper) {
+ if (helper->l4proto != pnum)
+ return NF_ACCEPT;
+
helper_cb = rcu_dereference(helper->help);
if (helper_cb) {
ret = helper_cb(skb, protoff,
diff --git a/net/netfilter/nft_ct.c b/net/netfilter/nft_ct.c
index 9dbf127df9c8..a1093311414b 100644
--- a/net/netfilter/nft_ct.c
+++ b/net/netfilter/nft_ct.c
@@ -1306,6 +1306,7 @@ static int nft_ct_expect_helper_alloc(struct nft_ct_expect_obj *priv)
"nft_ct_expect");
ct_expect_helper->me = THIS_MODULE;
ct_expect_helper->expect_policy[NF_CT_EXPECT_CLASS_DEFAULT].max_expected = priv->size;
+ ct_expect_helper->l4proto = priv->l4proto;
rcu_assign_pointer(ct_expect_helper->help, ct_expect_help);
refcount_set(&ct_expect_helper->ct_refcnt, 1);
--
2.55.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v2 nf 4/6] netfilter: conntrack: replace open-coded helper invocation
2026-09-17 13:09 [PATCH nf v2 0/6] netfilter: harden conntrack vs ingress pipeline rewrites Florian Westphal
` (2 preceding siblings ...)
2026-09-17 13:09 ` [PATCH v2 nf 3/6] netfilter: nf_conntrack: verify L4 protocol before calling helper Florian Westphal
@ 2026-09-17 13:09 ` Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 5/6] netfilter: nf_conntrack: harden helper invocation with tuple revalidation Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 6/6] netfilter: nft_ct: validate timeout object protocol Florian Westphal
5 siblings, 0 replies; 7+ messages in thread
From: Florian Westphal @ 2026-09-17 13:09 UTC (permalink / raw)
To: netfilter-devel; +Cc: Florian Westphal
Replace duplicated logic with the new nf_ct_call_helper() helper.
No functional changes intended, except the additional checks to
ensure the helper can process the given packet.
LLM complains about the the existing early !helper-return in
nf_ct_helper(), but I prefer to keep this as-is.
Fixes: 9fb9cbb1082d ("[NETFILTER]: Add nf_conntrack subsystem.")
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/nf_conntrack_ovs.c | 53 +-------------------------------
1 file changed, 1 insertion(+), 52 deletions(-)
diff --git a/net/netfilter/nf_conntrack_ovs.c b/net/netfilter/nf_conntrack_ovs.c
index b4085af3ad1c..fa64ec168e6e 100644
--- a/net/netfilter/nf_conntrack_ovs.c
+++ b/net/netfilter/nf_conntrack_ovs.c
@@ -12,16 +12,8 @@
int nf_ct_helper(struct sk_buff *skb, struct nf_conn *ct,
enum ip_conntrack_info ctinfo, u16 proto)
{
- int (*helper_cb)(struct sk_buff *skb, unsigned int protoff,
- struct nf_conn *ct,
- enum ip_conntrack_info conntrackinfo);
const struct nf_conntrack_helper *helper;
const struct nf_conn_help *help;
- unsigned int protoff;
- int err;
-
- if (ctinfo == IP_CT_RELATED_REPLY)
- return NF_ACCEPT;
help = nfct_help(ct);
if (!help)
@@ -35,50 +27,7 @@ int nf_ct_helper(struct sk_buff *skb, struct nf_conn *ct,
helper->nfproto != proto)
return NF_ACCEPT;
- switch (proto) {
- case NFPROTO_IPV4:
- protoff = ip_hdrlen(skb);
- proto = ip_hdr(skb)->protocol;
- break;
- case NFPROTO_IPV6: {
- u8 nexthdr = ipv6_hdr(skb)->nexthdr;
- __be16 frag_off;
- int ofs;
-
- ofs = ipv6_skip_exthdr(skb, sizeof(struct ipv6hdr), &nexthdr,
- &frag_off);
- if (ofs < 0 || (frag_off & htons(~0x7)) != 0) {
- pr_debug("proto header not found\n");
- return NF_ACCEPT;
- }
- protoff = ofs;
- proto = nexthdr;
- break;
- }
- default:
- WARN_ONCE(1, "helper invoked on non-IP family!");
- return NF_DROP;
- }
-
- if (helper->l4proto != proto)
- return NF_ACCEPT;
-
- helper_cb = rcu_dereference(helper->help);
- if (!helper_cb)
- return NF_ACCEPT;
-
- err = helper_cb(skb, protoff, ct, ctinfo);
- if (err != NF_ACCEPT)
- return err;
-
- /* Adjust seqs after helper. This is needed due to some helpers (e.g.,
- * FTP with NAT) adusting the TCP payload size when mangling IP
- * addresses and/or port numbers in the text-based control connection.
- */
- if (test_bit(IPS_SEQ_ADJUST_BIT, &ct->status) &&
- !nf_ct_seq_adjust(skb, ct, ctinfo, protoff))
- return NF_DROP;
- return NF_ACCEPT;
+ return nf_ct_call_helper(skb, ct, ctinfo);
}
EXPORT_SYMBOL_GPL(nf_ct_helper);
--
2.55.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v2 nf 5/6] netfilter: nf_conntrack: harden helper invocation with tuple revalidation
2026-09-17 13:09 [PATCH nf v2 0/6] netfilter: harden conntrack vs ingress pipeline rewrites Florian Westphal
` (3 preceding siblings ...)
2026-09-17 13:09 ` [PATCH v2 nf 4/6] netfilter: conntrack: replace open-coded helper invocation Florian Westphal
@ 2026-09-17 13:09 ` Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 6/6] netfilter: nft_ct: validate timeout object protocol Florian Westphal
5 siblings, 0 replies; 7+ messages in thread
From: Florian Westphal @ 2026-09-17 13:09 UTC (permalink / raw)
To: netfilter-devel; +Cc: Florian Westphal
Complete hardening of the connection tracking helper invocation sequence
initiated in the previous refactoring commits.
1. Tuple Verification: Extracts the *current* tuple from the packet and
verify it matches the connection tracking entry's tuple.
This prevents helpers from being invoked on packets that may have been
modified after the initial connection lookup, e.g. via act_ct ->
pedit. This also prevents the helper from operating on ICMP(v6) PMTU
errors.
2. TCP Header Sanity: For TCP traffic, the patch introduces a check to
ensure a full and sane TCP header is present.
Fixes: 9fb9cbb1082d ("[NETFILTER]: Add nf_conntrack subsystem.")
Signed-off-by: Florian Westphal <fw@strlen.de>
---
net/netfilter/nf_conntrack_proto.c | 34 ++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/net/netfilter/nf_conntrack_proto.c b/net/netfilter/nf_conntrack_proto.c
index 18125aa29e0d..b4e812268fe6 100644
--- a/net/netfilter/nf_conntrack_proto.c
+++ b/net/netfilter/nf_conntrack_proto.c
@@ -136,6 +136,8 @@ static bool nf_confirm_get_protoff(struct sk_buff *skb, struct net *net,
enum ip_conntrack_info ctinfo,
unsigned int *protoffp, u8 *pnum)
{
+ struct nf_conntrack_tuple tuple, invert;
+ enum ip_conntrack_dir dir;
unsigned int protoff;
__be16 frag_off;
int start;
@@ -160,6 +162,38 @@ static bool nf_confirm_get_protoff(struct sk_buff *skb, struct net *net,
return false;
}
+ if (!nf_ct_get_tuplepr(skb, skb_network_offset(skb), nf_ct_l3num(ct),
+ net, &tuple))
+ return false;
+
+ dir = CTINFO2DIR(ctinfo);
+ nf_ct_invert_tuple(&invert, &tuple);
+
+ /* This is called after L3/L4 headers have been mangled by NAT:
+ * Packet in original direction has been subject to SNAT, i.e.
+ * inverted reply dir.
+ * Packet in reply direction has been subject to DNAT, i.e.
+ * inverted original direction.
+ */
+ if (!nf_ct_tuple_equal(&invert, nf_ct_tuple(ct, !dir)))
+ return false;
+
+ /* Validate that a full, sane TCP header (including options) is
+ * present at protoff before helpers/seqadj are allowed to touch it.
+ */
+ if (tuple.dst.protonum == IPPROTO_TCP) {
+ unsigned int tcplen = skb->len - protoff;
+ const struct tcphdr *th;
+ struct tcphdr _tcph;
+
+ th = skb_header_pointer(skb, protoff, sizeof(_tcph), &_tcph);
+ if (!th)
+ return false;
+
+ if (th->doff * 4 < sizeof(*th) || tcplen < th->doff * 4)
+ return false;
+ }
+
*protoffp = protoff;
return true;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v2 nf 6/6] netfilter: nft_ct: validate timeout object protocol
2026-09-17 13:09 [PATCH nf v2 0/6] netfilter: harden conntrack vs ingress pipeline rewrites Florian Westphal
` (4 preceding siblings ...)
2026-09-17 13:09 ` [PATCH v2 nf 5/6] netfilter: nf_conntrack: harden helper invocation with tuple revalidation Florian Westphal
@ 2026-09-17 13:09 ` Florian Westphal
5 siblings, 0 replies; 7+ messages in thread
From: Florian Westphal @ 2026-09-17 13:09 UTC (permalink / raw)
To: netfilter-devel; +Cc: Florian Westphal, Kyle Zeng
nft_ct_timeout_obj_eval() only compares the timeout object protocol with
packet metadata. A packet header can be changed after conntrack attaches
an entry, so this metadata does not necessarily describe the entry.
Timeout objects contain protocol-specific arrays. Attaching an object for
a protocol with fewer timeout states to an entry for one with more states
lets the conntrack tracker read beyond the object.
Require the object protocol to match the conntrack tuple protocol before
attaching it. This mirrors validation by named timeout policies and
nftables conntrack helper objects.
Based on original patch from Kyle Zheng, who also authored this commit
message.
LLM review complains about _ext_add() races with cloned unconfirmed skbs.
conntrack never supported this; fixing it is hard and out of scope for this
change.
Fixes: 0434ccdcf883 ("netfilter: nf_tables: rework ct timeout set support")
Signed-off-by: Kyle Zeng <kylebot@openai.com>
Signed-off-by: Florian Westphal <fw@strlen.de>
---
v2:
keep nft_ct expectation as is, LLM claims this broke nft expectation support.
changelog: I'm too stupid to fix all bugs at once.
net/netfilter/nft_ct.c | 48 +++++++++++++++++++++++++++++++++---------
1 file changed, 38 insertions(+), 10 deletions(-)
diff --git a/net/netfilter/nft_ct.c b/net/netfilter/nft_ct.c
index a1093311414b..1cecdcae1f4e 100644
--- a/net/netfilter/nft_ct.c
+++ b/net/netfilter/nft_ct.c
@@ -839,6 +839,38 @@ static struct nft_expr_type nft_notrack_type __read_mostly = {
.owner = THIS_MODULE,
};
+/**
+ * nft_ct_get_safe() - Return nf_conn with extra checks
+ * @pkt: nftables packet information structure
+ * @l4proto: The expected Layer 4 protocol
+ * @ctinfop: packet ip_conntrack_info storage
+ *
+ * Returns the conntrack entry only if it is unconfirmed, non-template and
+ * matches the expected L4 protocol.
+ *
+ * Return: Pointer to the &struct nf_conn if all checks pass; NULL otherwise.
+ */
+static struct nf_conn *nft_ct_get_safe(const struct nft_pktinfo *pkt,
+ u8 l4proto, enum ip_conntrack_info *ctinfop)
+{
+ enum ip_conntrack_info ctinfo;
+ struct nf_conn *ct;
+
+ ct = nf_ct_get(pkt->skb, &ctinfo);
+ if (!ct || l4proto != pkt->tprot)
+ return NULL;
+
+ if (l4proto != nf_ct_protonum(ct))
+ return NULL;
+
+ if (READ_ONCE(ct->status) & (IPS_TEMPLATE | IPS_CONFIRMED))
+ return NULL;
+
+ if (ctinfop)
+ *ctinfop = ctinfo;
+ return ct;
+}
+
#ifdef CONFIG_NF_CONNTRACK_TIMEOUT
static int
nft_ct_timeout_parse_policy(void *timeouts,
@@ -878,14 +910,12 @@ static void nft_ct_timeout_obj_eval(struct nft_object *obj,
const struct nft_pktinfo *pkt)
{
const struct nft_ct_timeout_obj *priv = nft_obj_data(obj);
- struct nf_conn *ct = (struct nf_conn *)skb_nfct(pkt->skb);
struct nf_conn_timeout *timeout;
const unsigned int *values;
+ struct nf_conn *ct;
- if (priv->l4proto != pkt->tprot)
- return;
-
- if (!ct || nf_ct_is_template(ct) || nf_ct_is_confirmed(ct))
+ ct = nft_ct_get_safe(pkt, priv->l4proto, NULL);
+ if (!ct)
return;
timeout = nf_ct_timeout_find(ct);
@@ -1114,14 +1144,12 @@ static void nft_ct_helper_obj_eval(struct nft_object *obj,
const struct nft_pktinfo *pkt)
{
const struct nft_ct_helper_obj *priv = nft_obj_data(obj);
- struct nf_conn *ct = (struct nf_conn *)skb_nfct(pkt->skb);
struct nf_conntrack_helper *to_assign = NULL;
struct nf_conn_help *help;
+ struct nf_conn *ct;
- if (!ct ||
- nf_ct_is_confirmed(ct) ||
- nf_ct_is_template(ct) ||
- priv->l4proto != nf_ct_protonum(ct))
+ ct = nft_ct_get_safe(pkt, priv->l4proto, NULL);
+ if (!ct)
return;
switch (nf_ct_l3num(ct)) {
--
2.55.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-17 13:10 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-17 13:09 [PATCH nf v2 0/6] netfilter: harden conntrack vs ingress pipeline rewrites Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 1/6] netfilter: nf_conntrack: validate skb->_nfct and packet headers Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 2/6] netfilter: nf_conntrack: refactor helper call logic in nf_confirm() Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 3/6] netfilter: nf_conntrack: verify L4 protocol before calling helper Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 4/6] netfilter: conntrack: replace open-coded helper invocation Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 5/6] netfilter: nf_conntrack: harden helper invocation with tuple revalidation Florian Westphal
2026-09-17 13:09 ` [PATCH v2 nf 6/6] netfilter: nft_ct: validate timeout object protocol Florian Westphal
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox