11From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
22From: singhrohit23 <rohitsingh@arista.com>
33Date: Fri, 18 Apr 2026 10:00:00 +0530
4- Subject: [PATCH 7/7] nfqueue: confirm conntrack on NF_DROP for marked packets
4+ Subject: [PATCH 7/7] nfqueue: confirm conntrack on NF_DROP for marked UDP
55
6- In kernel 6.1, nfqnl_reinject() only calls ct_hook->update()
7- (which confirms conntrack entries) for NF_ACCEPT/REPEAT/STOP
8- verdicts. For NF_DROP, the unconfirmed conntrack entry is
9- destroyed when the skb is freed via kfree_skb().
6+ In kernel 6.1, conntrack entries remain unconfirmed until
7+ nf_conntrack_confirm() runs at POSTROUTING INT_MAX priority.
8+ When NFQUEUE (at lower priority) issues NF_DROP, the packet
9+ never reaches INT_MAX, and the unconfirmed entry is destroyed
10+ when the skb is freed via kfree_skb().
1011
1112This breaks UDP proxy architectures (like Untangle/NGFW) where:
12- 1. NFQUEUE intercepts a forwarded UDP packet
13+ 1. NFQUEUE intercepts a forwarded UDP packet at POSTROUTING
14+ priority 999, after NAT (priority 100)
13152. Userspace reads the data, drops the original (NF_DROP)
14163. Userspace sends its own copy via a separate socket
15174. The server reply needs to match the original conntrack
1618 entry for proper NAT de-translation and forwarding
1719
18- On kernel 5.10, conntrack entries created prior to NFQUEUE were
19- observed to survive NF_DROP in this use case, allowing reply
20- matching. On 6.1, entries remain unconfirmed and are destroyed
21- when the skb is freed.
20+ On kernel 5.10, conntrack entries survived NF_DROP in this use
21+ case, allowing reply matching. On 6.1, entries remain
22+ unconfirmed and are destroyed when the skb is freed.
2223
23- This patch adds a mark-gated opt-in mechanism: when the module
24- parameter nfqueue_confirm_drop_mark is set to a non-zero value,
25- packets whose skb->mark matches that mask will have their
26- conntrack entry confirmed before NF_DROP processing. This
27- preserves the conntrack state needed for reply matching.
24+ Note: ct_hook->update() cannot be used because it calls
25+ nf_confirm_cthelper() which returns early without confirming
26+ when no conntrack helper is attached (common for plain UDP).
27+ Confirmed via kprobes: nf_conntrack_update() returns 0 but
28+ __nf_conntrack_confirm() is never called from that path.
29+
30+ This patch calls __nf_conntrack_confirm() directly with guards:
31+ - Only unconfirmed entries (nf_ct_is_confirmed check)
32+ - Only tracked packets (nf_ct_get check)
33+ - Only UDP protocol (ip_hdr protocol check)
34+ - Only marked packets (nfqueue_confirm_drop_mark mask)
2835
2936Usage:
3037 echo 255 > /sys/module/nfnetlink_queue/parameters/nfqueue_confirm_drop_mark
3138
32- Only packets with the matching mark are affected; all other
33- NF_DROP behavior remains unchanged.
34-
3539Signed-off-by: singhrohit23 <rohitsingh@arista.com>
3640---
37- net/netfilter/nfnetlink_queue.c | 24 ++++++++++++++++++------
38- 1 file changed, 18 insertions(+), 6 deletions(- )
41+ net/netfilter/nfnetlink_queue.c | 25 +++++++++++++++++++++++++
42+ 1 file changed, 25 insertions(+)
3943
4044--- a/net/netfilter/nfnetlink_queue.c 2026-04-18 11:02:50.458929429 +0530
41- +++ b/net/netfilter/nfnetlink_queue.c 2026-04-18 11:03:14.968929279 +0530
42- @@ -36,6 +36,12 @@
45+ +++ b/net/netfilter/nfnetlink_queue.c 2026-04-18 18:02:46.962775641 +0530
46+ @@ -36,6 +36,14 @@
4347
4448 #include <linux/atomic.h>
4549
50+ + #include <linux/ip.h>
51+ +
4652+ static unsigned int nfqueue_confirm_drop_mark __read_mostly;
4753+ module_param(nfqueue_confirm_drop_mark, uint, 0644);
4854+ MODULE_PARM_DESC(nfqueue_confirm_drop_mark,
@@ -52,40 +58,35 @@ Signed-off-by: singhrohit23 <rohitsingh@arista.com>
5258 #if IS_ENABLED(CONFIG_BRIDGE_NETFILTER)
5359 #include "../bridge/br_private.h"
5460 #endif
55- @@ -230,18 +236,25 @@
56- const struct nf_ct_hook *ct_hook;
57- int err;
58-
59- - if (verdict == NF_ACCEPT ||
60- - verdict == NF_REPEAT ||
61- - verdict == NF_STOP) {
62- - rcu_read_lock();
63- - ct_hook = rcu_dereference(nf_ct_hook);
64- - if (ct_hook) {
65- + rcu_read_lock();
66- + ct_hook = rcu_dereference(nf_ct_hook);
67- + if (ct_hook) {
68- + if (verdict == NF_ACCEPT ||
69- + verdict == NF_REPEAT ||
70- + verdict == NF_STOP) {
71- err = ct_hook->update(entry->state.net, entry->skb);
72- if (err < 0)
73- verdict = NF_DROP;
74- + } else if (verdict == NF_DROP &&
75- + nfqueue_confirm_drop_mark &&
76- + (entry->skb->mark & nfqueue_confirm_drop_mark)) {
77- + /* Opt-in: confirm CT for marked packets even on DROP
78- + * to support proxying architectures that re-inject
79- + * packets from userspace. */
80- + ct_hook->update(entry->state.net, entry->skb);
61+ @@ -242,6 +250,28 @@
8162 }
82- - rcu_read_unlock();
63+ rcu_read_unlock();
8364 }
84- + rcu_read_unlock();
65+ + #if IS_ENABLED(CONFIG_NF_CONNTRACK)
66+ + if (verdict == NF_DROP && nfqueue_confirm_drop_mark &&
67+ + (entry->skb->mark & nfqueue_confirm_drop_mark)) {
68+ + struct nf_conn *ct;
69+ + enum ip_conntrack_info ctinfo;
70+ +
71+ + ct = nf_ct_get(entry->skb, &ctinfo);
72+ + if (ct && !nf_ct_is_confirmed(ct) &&
73+ + ip_hdr(entry->skb)->protocol == IPPROTO_UDP) {
74+ + /* Opt-in: confirm CT for marked UDP packets even
75+ + * on DROP to support proxying architectures that
76+ + * re-inject packets from userspace.
77+ + * Call __nf_conntrack_confirm() directly because
78+ + * ct_hook->update() calls nf_confirm_cthelper()
79+ + * which skips confirmation when no conntrack
80+ + * helper is attached (common for plain UDP). */
81+ + int ret = __nf_conntrack_confirm(entry->skb);
82+ + if (ret != NF_ACCEPT)
83+ + pr_debug_ratelimited("nfqueue: ct confirm failed (%d)\n", ret);
84+ + }
85+ + }
86+ + #endif
8587 nf_reinject(entry, verdict);
8688 }
8789
8890
8991- -
90922.34.1
91-
0 commit comments