From a8708aa9fe1095ca7859cb3f102b6a3fa0fb98bd Mon Sep 17 00:00:00 2001 From: Fernando Fernandez Mancera Date: Wed, 19 Aug 2026 12:24:08 +0200 Subject: [PATCH 01/11] netfilter: synproxy: fix reset of ct seqadj when reopening a connection SYNPROXY is resetting conntrack seqadj when a closed connection is re-opened, but it was using nf_ct_seqadj_init() which is a no-op for a zero offset. This patch introduces nf_ct_seqadj_reset() which sets the offset values directly to zero and avoid setting IPS_SEQ_ADJUST_BIT flag, it changes SYNPROXY code to use it when needed. Fixes: 48b1de4c110a ("netfilter: add SYNPROXY core/target") Signed-off-by: Fernando Fernandez Mancera Signed-off-by: Pablo Neira Ayuso --- include/net/netfilter/nf_conntrack_seqadj.h | 1 + net/netfilter/nf_conntrack_seqadj.c | 17 +++++++++++++++++ net/netfilter/nf_synproxy_core.c | 4 ++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/include/net/netfilter/nf_conntrack_seqadj.h b/include/net/netfilter/nf_conntrack_seqadj.h index 883c414b768e..0f5bbb14a25a 100644 --- a/include/net/netfilter/nf_conntrack_seqadj.h +++ b/include/net/netfilter/nf_conntrack_seqadj.h @@ -33,6 +33,7 @@ static inline struct nf_conn_seqadj *nfct_seqadj_ext_add(struct nf_conn *ct) int nf_ct_seqadj_init(struct nf_conn *ct, enum ip_conntrack_info ctinfo, s32 off); +void nf_ct_seqadj_reset(struct nf_conn *ct, enum ip_conntrack_info ctinfo); int nf_ct_seqadj_set(struct nf_conn *ct, enum ip_conntrack_info ctinfo, __be32 seq, s32 off); void nf_ct_tcp_seqadj_set(struct sk_buff *skb, struct nf_conn *ct, diff --git a/net/netfilter/nf_conntrack_seqadj.c b/net/netfilter/nf_conntrack_seqadj.c index d75e8dafb189..b7b166a8ad58 100644 --- a/net/netfilter/nf_conntrack_seqadj.c +++ b/net/netfilter/nf_conntrack_seqadj.c @@ -31,6 +31,23 @@ int nf_ct_seqadj_init(struct nf_conn *ct, enum ip_conntrack_info ctinfo, } EXPORT_SYMBOL_GPL(nf_ct_seqadj_init); +void nf_ct_seqadj_reset(struct nf_conn *ct, enum ip_conntrack_info ctinfo) +{ + struct nf_conn_seqadj *seqadj = nfct_seqadj(ct); + enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo); + struct nf_ct_seqadj *this_way; + + if (unlikely(!seqadj)) + return; + + spin_lock_bh(&ct->lock); + this_way = &seqadj->seq[dir]; + this_way->offset_before = 0; + this_way->offset_after = 0; + spin_unlock_bh(&ct->lock); +} +EXPORT_SYMBOL_GPL(nf_ct_seqadj_reset); + int nf_ct_seqadj_set(struct nf_conn *ct, enum ip_conntrack_info ctinfo, __be32 seq, s32 off) { diff --git a/net/netfilter/nf_synproxy_core.c b/net/netfilter/nf_synproxy_core.c index 9fc959ba2030..401ce5a13f3f 100644 --- a/net/netfilter/nf_synproxy_core.c +++ b/net/netfilter/nf_synproxy_core.c @@ -686,7 +686,7 @@ ipv4_synproxy_hook(void *priv, struct sk_buff *skb, * adjustments, they will get initialized once the connection is * reestablished. */ - nf_ct_seqadj_init(ct, ctinfo, 0); + nf_ct_seqadj_reset(ct, ctinfo); synproxy->tsoff = 0; this_cpu_inc(snet->stats->conn_reopened); fallthrough; @@ -1116,7 +1116,7 @@ ipv6_synproxy_hook(void *priv, struct sk_buff *skb, * adjustments, they will get initialized once the connection is * reestablished. */ - nf_ct_seqadj_init(ct, ctinfo, 0); + nf_ct_seqadj_reset(ct, ctinfo); synproxy->tsoff = 0; this_cpu_inc(snet->stats->conn_reopened); fallthrough; From 409df11c5674e1e6c8a79e8d008f007ad6f9b448 Mon Sep 17 00:00:00 2001 From: Shaojie Sun Date: Thu, 20 Aug 2026 20:57:18 +0800 Subject: [PATCH 02/11] netfilter: conntrack: fix nf_conntrack_expect_max default value in documentation The documentation for nf_conntrack_expect_max incorrectly states that the default value is nf_conntrack_buckets / 256. However, the code in nf_conntrack_expect_init() shows: nf_ct_expect_hsize = nf_conntrack_htable_size / 256; nf_ct_expect_max = nf_ct_expect_hsize * 4; This means the default value is actually nf_conntrack_buckets / 64 (i.e. 4 times the hash table size, which defaults to nf_conntrack_buckets / 256). Fix the documentation to reflect the correct default value and add explanation of the calculation. Signed-off-by: Shaojie Sun Signed-off-by: Pablo Neira Ayuso --- Documentation/networking/nf_conntrack-sysctl.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/networking/nf_conntrack-sysctl.rst b/Documentation/networking/nf_conntrack-sysctl.rst index 35f889259fcd..4426cce2a73f 100644 --- a/Documentation/networking/nf_conntrack-sysctl.rst +++ b/Documentation/networking/nf_conntrack-sysctl.rst @@ -44,7 +44,7 @@ nf_conntrack_events - BOOLEAN nf_conntrack_expect_max - INTEGER Maximum size of expectation table. Default value is - nf_conntrack_buckets / 256. Minimum is 1. + nf_conntrack_buckets / 64. Minimum is 1. nf_conntrack_frag6_high_thresh - INTEGER default 262144 From 32cbf7a8e5560b42a8eac04ac66cc4dafff63e95 Mon Sep 17 00:00:00 2001 From: Linkui Xiao Date: Fri, 21 Aug 2026 17:50:14 +0800 Subject: [PATCH 03/11] netfilter: osf: remove unreachable break in nf_osf_ttl() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In nf_osf_ttl(), the break statement after return in NF_OSF_TTL_TRUE case is unreachable dead‑code. The return statement exits the function immediately, so break will never execute. Remove the useless break, no functional change. Signed-off-by: Linkui Xiao Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nfnetlink_osf.c | 1 - 1 file changed, 1 deletion(-) diff --git a/net/netfilter/nfnetlink_osf.c b/net/netfilter/nfnetlink_osf.c index e4835b0f4bb1..c8cd6af95db4 100644 --- a/net/netfilter/nfnetlink_osf.c +++ b/net/netfilter/nfnetlink_osf.c @@ -36,7 +36,6 @@ static inline int nf_osf_ttl(const struct sk_buff *skb, switch (ttl_check) { case NF_OSF_TTL_TRUE: return ip->ttl == f_ttl; - break; case NF_OSF_TTL_NOCHECK: return 1; case NF_OSF_TTL_LESS: From d8cefdc18124d2cedeb6419028648b681b7dfd1a Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Fri, 4 Sep 2026 17:15:36 +0530 Subject: [PATCH 04/11] netfilter: fix several typos in comments Fix typos reported by scripts/checkpatch.pl using the misspelling list in scripts/spelling.txt. Only touches comments, no code changes. Assisted-by: Cursor:claude-opus-5 Signed-off-by: Hemanth Selam Signed-off-by: Pablo Neira Ayuso --- net/ipv4/netfilter/arp_tables.c | 2 +- net/netfilter/ipset/ip_set_core.c | 2 +- net/netfilter/ipvs/ip_vs_sync.c | 2 +- tools/testing/selftests/net/netfilter/conntrack_icmp_related.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/net/ipv4/netfilter/arp_tables.c b/net/ipv4/netfilter/arp_tables.c index db307fa49f3f..cf747e1e17a3 100644 --- a/net/ipv4/netfilter/arp_tables.c +++ b/net/ipv4/netfilter/arp_tables.c @@ -58,7 +58,7 @@ static inline int arp_devaddr_compare(const struct arpt_devaddr_info *ap, /* * Unfortunately, _b and _mask are not aligned to an int (or long int) * Some arches dont care, unrolling the loop is a win on them. - * For other arches, we only have a 16bit alignement. + * For other arches, we only have a 16bit alignment. */ static unsigned long ifname_compare(const char *_a, const char *_b, const char *_mask) { diff --git a/net/netfilter/ipset/ip_set_core.c b/net/netfilter/ipset/ip_set_core.c index 0a86a170ba90..4aeb767088cf 100644 --- a/net/netfilter/ipset/ip_set_core.c +++ b/net/netfilter/ipset/ip_set_core.c @@ -1426,7 +1426,7 @@ static int ip_set_swap(struct sk_buff *skb, const struct nfnl_info *info, return -IPSET_ERR_EXIST_SETNAME2; /* Features must not change. - * Not an artifical restriction anymore, as we must prevent + * Not an artificial restriction anymore, as we must prevent * possible loops created by swapping in setlist type of sets. */ if (!(from->type->features == to->type->features && diff --git a/net/netfilter/ipvs/ip_vs_sync.c b/net/netfilter/ipvs/ip_vs_sync.c index 5383aeafb0ae..4dacd5af65db 100644 --- a/net/netfilter/ipvs/ip_vs_sync.c +++ b/net/netfilter/ipvs/ip_vs_sync.c @@ -1344,7 +1344,7 @@ static void set_mcast_pmtudisc(struct sock *sk, int val) } /* - * Specifiy default interface for outgoing multicasts + * Specify default interface for outgoing multicasts */ static int set_mcast_if(struct sock *sk, struct net_device *dev) { diff --git a/tools/testing/selftests/net/netfilter/conntrack_icmp_related.sh b/tools/testing/selftests/net/netfilter/conntrack_icmp_related.sh index c63d840ead61..f63b7f12b36a 100755 --- a/tools/testing/selftests/net/netfilter/conntrack_icmp_related.sh +++ b/tools/testing/selftests/net/netfilter/conntrack_icmp_related.sh @@ -171,7 +171,7 @@ table inet filter { } EOF -# make sure NAT core rewrites adress of icmp error if nat is used according to +# make sure NAT core rewrites address of icmp error if nat is used according to # conntrack nat information (icmp error will be directed at nsrouter1 address, # but it needs to be routed to nsclient1 address). ip netns exec "$nsrouter1" nft -f - < Date: Tue, 8 Sep 2026 12:02:53 +0200 Subject: [PATCH 05/11] netfilter: conntrack: Untangle insert_failed counter from others There is not much sense in incrementing multiple conntrack counters for the same situation. Defining insert_failed as a situation where a valid packet's conntrack can't be confirmed due to unresolvable clash sets it apart from 'drop' (ENOMEM situation) and 'chaintoolong'. Signed-off-by: Phil Sutter Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nf_conntrack_core.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/net/netfilter/nf_conntrack_core.c b/net/netfilter/nf_conntrack_core.c index d0d9e5ea84a0..53401e21ad99 100644 --- a/net/netfilter/nf_conntrack_core.c +++ b/net/netfilter/nf_conntrack_core.c @@ -1169,7 +1169,6 @@ nf_ct_resolve_clash(struct sk_buff *skb, struct nf_conntrack_tuple_hash *h, return ret; drop: - NF_CT_STAT_INC(net, drop); NF_CT_STAT_INC(net, insert_failed); return NF_DROP; } @@ -1259,7 +1258,6 @@ __nf_conntrack_confirm(struct sk_buff *skb) if (chainlen++ > max_chainlen) { chaintoolong: NF_CT_STAT_INC(net, chaintoolong); - NF_CT_STAT_INC(net, insert_failed); ret = NF_DROP; goto dying; } From 2ac41d7009c7c60e0b876760d98bd84a1f91020d Mon Sep 17 00:00:00 2001 From: Phil Sutter Date: Tue, 8 Sep 2026 12:02:54 +0200 Subject: [PATCH 06/11] netfilter: conntrack: Untangle drop and invalid counters In nf_conntrack_in, 'drop' counter increments if nf_conntrack_handle_packet returns NF_DROP. This is a special case with TCP packets (added by commit 6b69fe0c73c0 ("netfilter: nf_conntrack_tcp: fix endless loop") and not related to invalid packets which are responsible for all the other <=0 returns. So don't increment 'invalid' ounter in this case. Signed-off-by: Phil Sutter Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nf_conntrack_core.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/net/netfilter/nf_conntrack_core.c b/net/netfilter/nf_conntrack_core.c index 53401e21ad99..f9b8927327ba 100644 --- a/net/netfilter/nf_conntrack_core.c +++ b/net/netfilter/nf_conntrack_core.c @@ -2065,9 +2065,10 @@ repeat: if (ret == -NF_REPEAT) goto repeat; - NF_CT_STAT_INC_ATOMIC(state->net, invalid); if (ret == NF_DROP) NF_CT_STAT_INC_ATOMIC(state->net, drop); + else + NF_CT_STAT_INC_ATOMIC(state->net, invalid); ret = -ret; goto out; From d4548625f38b7958540451f9afed9e4155b4fa22 Mon Sep 17 00:00:00 2001 From: Qingfang Deng Date: Wed, 9 Sep 2026 15:56:05 +0800 Subject: [PATCH 07/11] net/sched: act_ct: set net pointer before publishing flowtable nf_flow_table_init() adds the flowtable to the global flowtables list. However, tcf_ct_flow_table_get() sets the table's network namespace only after that call, leaving a window where the published table has a NULL namespace pointer. Set the namespace before calling nf_flow_table_init() so that flowtable list walkers can rely on it being initialized. This is required by: "netfilter: flowtable: check namespace before iterating flows" Signed-off-by: Qingfang Deng Reviewed-by: Xuanqiang Luo Signed-off-by: Pablo Neira Ayuso --- net/sched/act_ct.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/sched/act_ct.c b/net/sched/act_ct.c index 411e3dd92d07..e26c0d5b2176 100644 --- a/net/sched/act_ct.c +++ b/net/sched/act_ct.c @@ -349,10 +349,10 @@ static int tcf_ct_flow_table_get(struct net *net, struct tcf_ct_params *params) ct_ft->nf_ft.type = &flowtable_ct; ct_ft->nf_ft.flags |= NF_FLOWTABLE_HW_OFFLOAD | NF_FLOWTABLE_COUNTER; + write_pnet(&ct_ft->nf_ft.net, net); err = nf_flow_table_init(&ct_ft->nf_ft); if (err) goto err_init; - write_pnet(&ct_ft->nf_ft.net, net); __module_get(THIS_MODULE); out_unlock: From c48fdf027a7265b86ea6332c4e1f56d86268541b Mon Sep 17 00:00:00 2001 From: Qingfang Deng Date: Wed, 9 Sep 2026 16:17:04 +0800 Subject: [PATCH 08/11] netfilter: flowtable: check namespace before iterating flows nf_flow_table_cleanup() walks every registered flow table, checking the network namespace for each flow in nf_flow_table_do_cleanup(). As a result, tables in other namespaces are still iterated and their cleanup work is flushed. Compare the flow table's namespace with the device's namespace in nf_flow_table_cleanup() and skip nonmatching tables. This avoids unnecessary iteration and work flushing, and leaves the per-flow cleanup callback to check only the interface index. Signed-off-by: Qingfang Deng Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nf_flow_table_core.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/net/netfilter/nf_flow_table_core.c b/net/netfilter/nf_flow_table_core.c index 934c6151f558..bd8f9cf394ff 100644 --- a/net/netfilter/nf_flow_table_core.c +++ b/net/netfilter/nf_flow_table_core.c @@ -737,14 +737,9 @@ static void nf_flow_table_do_cleanup(struct nf_flowtable *flow_table, { struct net_device *dev = data; - if (!dev) { - flow_offload_teardown(flow); - return; - } - - if (net_eq(nf_ct_net(flow->ct), dev_net(dev)) && - (flow->tuplehash[0].tuple.iifidx == dev->ifindex || - flow->tuplehash[1].tuple.iifidx == dev->ifindex)) + if (!dev || + flow->tuplehash[0].tuple.iifidx == dev->ifindex || + flow->tuplehash[1].tuple.iifidx == dev->ifindex) flow_offload_teardown(flow); } @@ -761,8 +756,10 @@ void nf_flow_table_cleanup(struct net_device *dev) struct nf_flowtable *flowtable; mutex_lock(&flowtable_lock); - list_for_each_entry(flowtable, &flowtables, list) - nf_flow_table_gc_cleanup(flowtable, dev); + list_for_each_entry(flowtable, &flowtables, list) { + if (net_eq(read_pnet(&flowtable->net), dev_net(dev))) + nf_flow_table_gc_cleanup(flowtable, dev); + } mutex_unlock(&flowtable_lock); } EXPORT_SYMBOL_GPL(nf_flow_table_cleanup); From d3950004ba90350bcdf414c5f804da277178564a Mon Sep 17 00:00:00 2001 From: Phil Sutter Date: Wed, 9 Sep 2026 11:34:55 +0200 Subject: [PATCH 09/11] netfilter: nfnetlink: Fix for interrupted hook dumps Handling of concurrent hook changes with a dump in progress was problematic in nfnl_hook_dump and entirely broken in nfnl_hook_dump_nat. Address all issues in a single patch to please the review LLM. Introduce sequence numbers in struct netns_nf to replace pointer value-based modification detection which may fail due to memory buffer reuse. This also eliminates the need for most manual cb->seq updates and excessive array index value checks. Since nat hook updates are protected by a different mutex than others, they have to bump their own sequence number. nfnl_hook_dump_nat therefore open-codes what nl_dump_check_consistent does but bumps cb->seq instead of setting NLM_F_DUMP_INTR flag. Dumps of many nat hooks was entirely broken since the array index was not (re)stored. Use cb->args[1] for that and make sure it is reset upon completion as the same dump may loop over multiple arrays of nat hooks. In nfnl_hook_dump, don't signal NLM_F_DUMP_INTR if nfnl_hook_entries_head returns error: This is a permanent condition which does not change during a dump. It is already caught by nfnl_hook_dump_start though, so should not happen anyway. In general, access ops array pointer values using READ_ONCE since they are assigned to using WRITE_ONCE. Avoid setting NLM_F_DUMP_INTR flag in garbage memory by calling nl_dump_check_consistent only for non-empty skbs. If not a single netlink message was created, nfnetlink code will take care of setting the flag. Fixes: e2cf17d3774c ("netfilter: add new hook nfnl subsystem") Fixes: b010e2a4a9ac ("netfilter: nfnetlink_hook: Dump nat type chains") Signed-off-by: Phil Sutter Signed-off-by: Pablo Neira Ayuso --- include/net/netns/netfilter.h | 2 + net/netfilter/core.c | 18 ++++++++- net/netfilter/nf_nat_core.c | 11 +++++ net/netfilter/nfnetlink_hook.c | 74 ++++++++++++++++++++-------------- 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/include/net/netns/netfilter.h b/include/net/netns/netfilter.h index a6a0bf4a247e..7fd78394d1e7 100644 --- a/include/net/netns/netfilter.h +++ b/include/net/netns/netfilter.h @@ -33,5 +33,7 @@ struct netns_nf { #if IS_ENABLED(CONFIG_NF_DEFRAG_IPV6) unsigned int defrag_ipv6_users; #endif + unsigned int hook_base_seq; + unsigned int nat_hook_base_seq; }; #endif diff --git a/net/netfilter/core.c b/net/netfilter/core.c index 675a1034b340..940dea2663e9 100644 --- a/net/netfilter/core.c +++ b/net/netfilter/core.c @@ -386,6 +386,15 @@ static void nf_static_key_dec(const struct nf_hook_ops *reg, int pf) #endif } +static void bump_hook_base_seq(struct net *net) +{ + unsigned int base_seq = READ_ONCE(net->nf.hook_base_seq); + + while (++base_seq == 0) + ; + smp_store_release(&net->nf.hook_base_seq, base_seq); +} + static int __nf_register_net_hook(struct net *net, int pf, const struct nf_hook_ops *reg) { @@ -430,6 +439,7 @@ static int __nf_register_net_hook(struct net *net, int pf, if (!IS_ERR(new_hooks)) { hooks_validate(new_hooks); rcu_assign_pointer(*pp, new_hooks); + bump_hook_base_seq(net); } mutex_unlock(&nf_hook_mutex); @@ -483,6 +493,7 @@ static void __nf_unregister_net_hook(struct net *net, int pf, { struct nf_hook_entries __rcu **pp; struct nf_hook_entries *p; + bool found; pp = nf_hook_entry_head(net, pf, reg->hooknum, reg->dev); if (!pp) @@ -496,7 +507,8 @@ static void __nf_unregister_net_hook(struct net *net, int pf, return; } - if (nf_remove_net_hook(p, reg)) { + found = nf_remove_net_hook(p, reg); + if (found) { #ifdef CONFIG_NETFILTER_INGRESS if (nf_ingress_hook(reg, pf)) net_dec_ingress_queue(); @@ -511,6 +523,8 @@ static void __nf_unregister_net_hook(struct net *net, int pf, } p = __nf_hook_entries_try_shrink(p, pp); + if (found) + bump_hook_base_seq(net); mutex_unlock(&nf_hook_mutex); if (!p) return; @@ -784,6 +798,8 @@ static int __net_init netfilter_net_init(struct net *net) return -ENOMEM; } #endif + net->nf.hook_base_seq = 1; + net->nf.nat_hook_base_seq = 1; return 0; } diff --git a/net/netfilter/nf_nat_core.c b/net/netfilter/nf_nat_core.c index 84f82957e66b..5ddd5fc95b24 100644 --- a/net/netfilter/nf_nat_core.c +++ b/net/netfilter/nf_nat_core.c @@ -1160,6 +1160,15 @@ nfnetlink_parse_nat_setup(struct nf_conn *ct, } #endif +static void bump_nat_hook_base_seq(struct net *net) +{ + unsigned int base_seq = READ_ONCE(net->nf.nat_hook_base_seq); + + while (++base_seq == 0) + ; + smp_store_release(&net->nf.nat_hook_base_seq, base_seq); +} + static struct nf_ct_helper_expectfn follow_master_nat = { .name = "nat-follow-master", .expectfn = nf_nat_follow_master, @@ -1245,6 +1254,7 @@ int nf_nat_register_fn(struct net *net, u8 pf, const struct nf_hook_ops *ops, nat_proto_net->nat_hook_ops = nat_ops; nat_proto_net->users++; + bump_nat_hook_base_seq(net); mutex_unlock(&nf_nat_proto_mutex); @@ -1299,6 +1309,7 @@ void nf_nat_unregister_fn(struct net *net, u8 pf, const struct nf_hook_ops *ops, goto unlock; priv = nat_ops[hooknum].priv; nf_hook_entries_delete_raw(&priv->entries, ops); + bump_nat_hook_base_seq(net); if (nat_proto_net->users == 0) { nf_unregister_net_hooks(net, nat_ops, ops_count); diff --git a/net/netfilter/nfnetlink_hook.c b/net/netfilter/nfnetlink_hook.c index 95005e9a6066..b05fd79397c5 100644 --- a/net/netfilter/nfnetlink_hook.c +++ b/net/netfilter/nfnetlink_hook.c @@ -54,7 +54,6 @@ static int nf_netlink_dump_start_rcu(struct sock *nlsk, struct sk_buff *skb, struct nfnl_dump_hook_data { char devname[IFNAMSIZ]; - unsigned long headv; u8 hook; }; @@ -338,27 +337,47 @@ nfnl_hook_entries_head(u8 pf, unsigned int hook, struct net *net, const char *de } static int nfnl_hook_dump_nat(struct sk_buff *nlskb, - const struct nfnl_dump_hook_data *ctx, - const struct nf_hook_ops *ops, - int family, unsigned int seq) + struct netlink_callback *cb, + const struct nf_hook_ops *ops, int family) { struct nf_nat_lookup_hook_priv *priv = ops->priv; - struct nf_hook_entries *e = rcu_dereference(priv->entries); + struct nfnl_dump_hook_data *ctx = cb->data; + struct net *net = sock_net(nlskb->sk); struct nf_hook_ops **nat_ops; - int i, err; + unsigned int i = cb->args[1]; + struct nf_hook_entries *e; + unsigned int base_seq; + int err = 0; + base_seq = smp_load_acquire(&net->nf.nat_hook_base_seq); + + e = rcu_dereference(priv->entries); if (!e) - return 0; + goto out; nat_ops = nf_hook_entries_get_hook_ops(e); - for (i = 0; i < e->num_hook_entries; i++) { - err = nfnl_hook_dump_one(nlskb, ctx, nat_ops[i], - ops->priority, family, seq); + for (; i < e->num_hook_entries; i++) { + err = nfnl_hook_dump_one(nlskb, ctx, + READ_ONCE(nat_ops[i]), + ops->priority, family, + cb->nlh->nlmsg_seq); if (err) - return err; + break; + } - return 0; +out: + if (!err) + i = 0; + cb->args[1] = i; + + if (cb->args[2] && base_seq != cb->args[2]) { + cb->seq++; + err = -EINTR; + } + cb->args[2] = base_seq; + + return err; } static int nfnl_hook_dump(struct sk_buff *nlskb, @@ -373,35 +392,31 @@ static int nfnl_hook_dump(struct sk_buff *nlskb, unsigned int i = cb->args[0]; rcu_read_lock(); + cb->seq = smp_load_acquire(&net->nf.hook_base_seq); e = nfnl_hook_entries_head(family, ctx->hook, net, ctx->devname); - if (!e) + if (!e || IS_ERR(e)) goto done; - if (IS_ERR(e)) { - cb->seq++; - goto done; - } - - if ((unsigned long)e != ctx->headv || i >= e->num_hook_entries) - cb->seq++; - ops = nf_hook_entries_get_hook_ops(e); for (; i < e->num_hook_entries; i++) { - if (ops[i]->hook_ops_type == NF_HOOK_OP_NAT) - err = nfnl_hook_dump_nat(nlskb, ctx, ops[i], family, - cb->nlh->nlmsg_seq); - else - err = nfnl_hook_dump_one(nlskb, ctx, ops[i], - ops[i]->priority, family, + const struct nf_hook_ops *cur = READ_ONCE(ops[i]); + + if (cur->hook_ops_type == NF_HOOK_OP_NAT) { + err = nfnl_hook_dump_nat(nlskb, cb, cur, family); + } else { + err = nfnl_hook_dump_one(nlskb, ctx, cur, + cur->priority, family, cb->nlh->nlmsg_seq); + } if (err) break; } done: - nl_dump_check_consistent(cb, nlmsg_hdr(nlskb)); + if (nlskb->len > 0) + nl_dump_check_consistent(cb, nlmsg_hdr(nlskb)); rcu_read_unlock(); cb->args[0] = i; return nlskb->len; @@ -442,10 +457,7 @@ static int nfnl_hook_dump_start(struct netlink_callback *cb) return -ENOMEM; strscpy(ctx->devname, name, sizeof(ctx->devname)); - ctx->headv = (unsigned long)head; ctx->hook = hooknum; - - cb->seq = 1; cb->data = ctx; return 0; From 1ae2d094b0200f687923298c0e42579273558988 Mon Sep 17 00:00:00 2001 From: Piotr Kubik Date: Wed, 9 Sep 2026 16:49:29 +0200 Subject: [PATCH 10/11] netfilter: ctnetlink: fix inverted IPv6 address match in dump filter ctnetlink_filter_match_tuple() rejects an entry when !ipv6_addr_cmp(filter, entry) is true. ipv6_addr_cmp() is a memcmp() and returns 0 for equal addresses, so the condition is true exactly when the addresses match: a CTA_FILTER dump with an IPv6 CTA_IP_SRC or CTA_IP_DST skips every entry that matches the requested address and returns all the others. The IPv4 branch compares with != and behaves correctly. Observed with libnetfilter_conntrack NFCT_FILTER_DUMP_TUPLE: a dump filtered on src 2001:db8::10 returned only the unrelated entries, and a dump filtered on ::1 returned every entry except the ::1 ones. conntrack-tools does not use the tuple filter for -L, which is why this went unnoticed. Use ipv6_addr_equal() so the IPv6 branch mirrors the IPv4 one. Fixes: cb8aa9a3affb ("netfilter: ctnetlink: add kernel side filtering for dump") Cc: stable@vger.kernel.org Signed-off-by: Piotr Kubik Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nf_conntrack_netlink.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/net/netfilter/nf_conntrack_netlink.c b/net/netfilter/nf_conntrack_netlink.c index 4e5d7c701436..e8477f792c95 100644 --- a/net/netfilter/nf_conntrack_netlink.c +++ b/net/netfilter/nf_conntrack_netlink.c @@ -1098,13 +1098,13 @@ static int ctnetlink_filter_match_tuple(struct nf_conntrack_tuple *filter_tuple, break; case NFPROTO_IPV6: if ((flags & CTA_FILTER_FLAG(CTA_IP_SRC)) && - !ipv6_addr_cmp(&filter_tuple->src.u3.in6, - &ct_tuple->src.u3.in6)) + !ipv6_addr_equal(&filter_tuple->src.u3.in6, + &ct_tuple->src.u3.in6)) return 0; if ((flags & CTA_FILTER_FLAG(CTA_IP_DST)) && - !ipv6_addr_cmp(&filter_tuple->dst.u3.in6, - &ct_tuple->dst.u3.in6)) + !ipv6_addr_equal(&filter_tuple->dst.u3.in6, + &ct_tuple->dst.u3.in6)) return 0; break; } From 46da6029bf468ce3c426cb8b4abf96976ed9d4c8 Mon Sep 17 00:00:00 2001 From: Ilya Maximets Date: Thu, 10 Sep 2026 12:54:13 +0200 Subject: [PATCH 11/11] netfilter: conntrack: make filtering by zone discoverable Conntrack flush supports filtering by zone using CTA_ZONE, but this attribute is really hard to use from user space applications. The reason is that it is not possible to tell if it's supported or not. Older kernels silently ignore CTA_ZONE. And in that case they just happily flush all the entries from all zones breaking all the existing connections. So, applications have to infer support from the kernel version. While it works in most cases, it's not a particularly reliable or desired way to check kernel capabilities from applications that aim to be portable. There should be a better way to probe or discover features in the kernel. The CTA_FILTER interface on the other hand is simple enough to probe. We can check for NLM_F_DUMP_FILTERED in the dump to see if filtering is supported. And unknown sub-attributes in CTA_FILTER are rejected explicitly since strict validation is in use there. Let's add new CTA_FILTER_ZONE that signals that CTA_ZONE should be filtered on. It is a flag, since everything in the CTA_FILTER is a bit mask, i.e., a form of a flag. If set, it means that CTA_ZONE must be present and be used for filtering. If the flag is not set however, the filtering on CTA_ZONE will still take place to ensure backwards compatibility. So, the flag doesn't really change the filtering behavior, but it allows user space applications to properly discover support for CTA_ZONE filtering without need to rely on kernel version parsing or risk accidental flushes of the entire conntrack table, and also without modifying the kernel state. A new test variant is added to test with and without the new flag. Since the setup code is moved into a shared function, expectations replaced with assertions to bail early if the base setup fails to avoid the cascade of secondary failures that can be misleading. Error return is only for the SKIP cases. Signed-off-by: Ilya Maximets Signed-off-by: Pablo Neira Ayuso --- Documentation/netlink/specs/conntrack.yaml | 6 + .../linux/netfilter/nfnetlink_conntrack.h | 1 + net/netfilter/nf_conntrack_netlink.c | 11 ++ .../net/netfilter/conntrack_dump_flush.c | 145 ++++++++++++------ 4 files changed, 117 insertions(+), 46 deletions(-) diff --git a/Documentation/netlink/specs/conntrack.yaml b/Documentation/netlink/specs/conntrack.yaml index 1d163130241a..56c13705243b 100644 --- a/Documentation/netlink/specs/conntrack.yaml +++ b/Documentation/netlink/specs/conntrack.yaml @@ -393,6 +393,12 @@ attribute-sets: name: reply-flags type: u32 doc: bitmask of tuple fields to filter on, reply direction + - + name: zone + type: flag + doc: >- + Filter on conntrack zone id; requires the top-level zone + (``CTA_ZONE``) attribute. - name: conntrack-attrs attributes: diff --git a/include/uapi/linux/netfilter/nfnetlink_conntrack.h b/include/uapi/linux/netfilter/nfnetlink_conntrack.h index 43233af75b9d..985f9c08d3e9 100644 --- a/include/uapi/linux/netfilter/nfnetlink_conntrack.h +++ b/include/uapi/linux/netfilter/nfnetlink_conntrack.h @@ -285,6 +285,7 @@ enum ctattr_filter { CTA_FILTER_UNSPEC, CTA_FILTER_ORIG_FLAGS, CTA_FILTER_REPLY_FLAGS, + CTA_FILTER_ZONE, __CTA_FILTER_MAX }; #define CTA_FILTER_MAX (__CTA_FILTER_MAX - 1) diff --git a/net/netfilter/nf_conntrack_netlink.c b/net/netfilter/nf_conntrack_netlink.c index e8477f792c95..4b6abe36028e 100644 --- a/net/netfilter/nf_conntrack_netlink.c +++ b/net/netfilter/nf_conntrack_netlink.c @@ -911,6 +911,7 @@ struct ctnetlink_filter { static const struct nla_policy cta_filter_nla_policy[CTA_FILTER_MAX + 1] = { [CTA_FILTER_ORIG_FLAGS] = NLA_POLICY_MASK(NLA_U32, CTA_FILTER_F_ALL), [CTA_FILTER_REPLY_FLAGS] = NLA_POLICY_MASK(NLA_U32, CTA_FILTER_F_ALL), + [CTA_FILTER_ZONE] = { .type = NLA_FLAG }, }; static int ctnetlink_parse_filter(const struct nlattr *attr, @@ -930,6 +931,9 @@ static int ctnetlink_parse_filter(const struct nlattr *attr, if (tb[CTA_FILTER_REPLY_FLAGS]) filter->reply_flags = nla_get_u32(tb[CTA_FILTER_REPLY_FLAGS]); + if (tb[CTA_FILTER_ZONE]) + filter->zone_filter = true; + return 0; } @@ -1006,6 +1010,7 @@ ctnetlink_alloc_filter(const struct nlattr * const cda[], u8 family) if (err) goto err_filter; + /* CTA_ZONE is allowed without CTA_FILTER_ZONE. */ if (cda[CTA_ZONE]) { err = ctnetlink_parse_zone(cda[CTA_ZONE], &filter->zone); if (err < 0) @@ -1020,6 +1025,12 @@ ctnetlink_alloc_filter(const struct nlattr * const cda[], u8 family) if (err < 0) goto err_filter; + /* CTA_FILTER_ZONE cannot be set without CTA_ZONE. */ + if (filter->zone_filter && !cda[CTA_ZONE]) { + err = -EINVAL; + goto err_filter; + } + if (filter->orig_flags) { if (!cda[CTA_TUPLE_ORIG]) { err = -EINVAL; diff --git a/tools/testing/selftests/net/netfilter/conntrack_dump_flush.c b/tools/testing/selftests/net/netfilter/conntrack_dump_flush.c index 31b8250ddc53..0c777323f4ba 100644 --- a/tools/testing/selftests/net/netfilter/conntrack_dump_flush.c +++ b/tools/testing/selftests/net/netfilter/conntrack_dump_flush.c @@ -215,7 +215,22 @@ static int count_entries(const struct nlmsghdr *nlh, void *data) return MNL_CB_OK; } -static int conntrack_count_zone(struct mnl_socket *sock, uint16_t zone) +static void put_zone_attr(struct nlmsghdr *nlh, uint16_t zone, + bool use_cta_filter) +{ + struct nlattr *nest; + + mnl_attr_put_u16(nlh, CTA_ZONE, htons(zone)); + + if (use_cta_filter) { + nest = mnl_attr_nest_start(nlh, CTA_FILTER); + mnl_attr_put(nlh, CTA_FILTER_ZONE, 0, NULL); + mnl_attr_nest_end(nlh, nest); + } +} + +static int conntrack_count_zone(struct mnl_socket *sock, uint16_t zone, + bool use_cta_filter) { char buf[MNL_SOCKET_BUFFER_SIZE]; struct nlmsghdr *nlh; @@ -235,7 +250,7 @@ static int conntrack_count_zone(struct mnl_socket *sock, uint16_t zone) nfh->version = NFNETLINK_V0; nfh->res_id = 0; - mnl_attr_put_u16(nlh, CTA_ZONE, htons(zone)); + put_zone_attr(nlh, zone, use_cta_filter); ret = mnl_socket_sendto(sock, nlh, nlh->nlmsg_len); if (ret < 0) { @@ -261,7 +276,8 @@ static int conntrack_count_zone(struct mnl_socket *sock, uint16_t zone) return reply_counter; } -static int conntrack_flush_zone(struct mnl_socket *sock, uint16_t zone) +static int conntrack_flush_zone(struct mnl_socket *sock, uint16_t zone, + bool use_cta_filter) { char buf[MNL_SOCKET_BUFFER_SIZE]; struct nlmsghdr *nlh; @@ -281,7 +297,7 @@ static int conntrack_flush_zone(struct mnl_socket *sock, uint16_t zone) nfh->version = NFNETLINK_V0; nfh->res_id = 0; - mnl_attr_put_u16(nlh, CTA_ZONE, htons(zone)); + put_zone_attr(nlh, zone, use_cta_filter); ret = mnl_socket_sendto(sock, nlh, nlh->nlmsg_len); if (ret < 0) { @@ -304,43 +320,40 @@ static int conntrack_flush_zone(struct mnl_socket *sock, uint16_t zone) return 0; } -FIXTURE(conntrack_dump_flush) -{ - struct mnl_socket *sock; -}; - -FIXTURE_SETUP(conntrack_dump_flush) +static int conntrack_zone_setup(struct __test_metadata *_metadata, + struct mnl_socket **sock) { struct in6_addr src, dst; int ret; - self->sock = mnl_socket_open(NETLINK_NETFILTER); - if (!self->sock) { + *sock = mnl_socket_open(NETLINK_NETFILTER); + if (!*sock) { perror("mnl_socket_open"); - SKIP(return, "cannot open netlink_netfilter socket"); + SKIP(return -1, "cannot open netlink_netfilter socket"); } - ret = mnl_socket_bind(self->sock, 0, MNL_SOCKET_AUTOPID); - EXPECT_EQ(ret, 0); + ret = mnl_socket_bind(*sock, 0, MNL_SOCKET_AUTOPID); + ASSERT_EQ(ret, 0); - ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_count_zone(*sock, TEST_ZONE_ID, false); if (ret < 0 && errno == EPERM) - SKIP(return, "Needs to be run as root"); + SKIP(return -1, "Needs to be run as root"); else if (ret < 0 && errno == EOPNOTSUPP) - SKIP(return, "Kernel does not seem to support conntrack zones"); + SKIP(return -1, + "Kernel does not seem to support conntrack zones"); - ret = conntrack_data_generate_v4(self->sock, 0xf0f0f0f0, 0xf1f1f1f1, + ret = conntrack_data_generate_v4(*sock, 0xf0f0f0f0, 0xf1f1f1f1, TEST_ZONE_ID); - EXPECT_EQ(ret, 0); - ret = conntrack_data_generate_v4(self->sock, 0xf2f2f2f2, 0xf3f3f3f3, + ASSERT_EQ(ret, 0); + ret = conntrack_data_generate_v4(*sock, 0xf2f2f2f2, 0xf3f3f3f3, TEST_ZONE_ID + 1); - EXPECT_EQ(ret, 0); - ret = conntrack_data_generate_v4(self->sock, 0xf4f4f4f4, 0xf5f5f5f5, + ASSERT_EQ(ret, 0); + ret = conntrack_data_generate_v4(*sock, 0xf4f4f4f4, 0xf5f5f5f5, TEST_ZONE_ID + 2); - EXPECT_EQ(ret, 0); - ret = conntrack_data_generate_v4(self->sock, 0xf6f6f6f6, 0xf7f7f7f7, + ASSERT_EQ(ret, 0); + ret = conntrack_data_generate_v4(*sock, 0xf6f6f6f6, 0xf7f7f7f7, NF_CT_DEFAULT_ZONE_ID); - EXPECT_EQ(ret, 0); + ASSERT_EQ(ret, 0); src = (struct in6_addr) {{ .__u6_addr32 = { @@ -358,9 +371,9 @@ FIXTURE_SETUP(conntrack_dump_flush) 0x02000000 } }}; - ret = conntrack_data_generate_v6(self->sock, src, dst, + ret = conntrack_data_generate_v6(*sock, src, dst, TEST_ZONE_ID); - EXPECT_EQ(ret, 0); + ASSERT_EQ(ret, 0); src = (struct in6_addr) {{ .__u6_addr32 = { 0xb80d0120, @@ -377,9 +390,9 @@ FIXTURE_SETUP(conntrack_dump_flush) 0x04000000 } }}; - ret = conntrack_data_generate_v6(self->sock, src, dst, + ret = conntrack_data_generate_v6(*sock, src, dst, TEST_ZONE_ID + 1); - EXPECT_EQ(ret, 0); + ASSERT_EQ(ret, 0); src = (struct in6_addr) {{ .__u6_addr32 = { 0xb80d0120, @@ -396,9 +409,9 @@ FIXTURE_SETUP(conntrack_dump_flush) 0x06000000 } }}; - ret = conntrack_data_generate_v6(self->sock, src, dst, + ret = conntrack_data_generate_v6(*sock, src, dst, TEST_ZONE_ID + 2); - EXPECT_EQ(ret, 0); + ASSERT_EQ(ret, 0); src = (struct in6_addr) {{ .__u6_addr32 = { @@ -416,14 +429,51 @@ FIXTURE_SETUP(conntrack_dump_flush) 0x08000000 } }}; - ret = conntrack_data_generate_v6(self->sock, src, dst, + ret = conntrack_data_generate_v6(*sock, src, dst, NF_CT_DEFAULT_ZONE_ID); - EXPECT_EQ(ret, 0); + ASSERT_EQ(ret, 0); - ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); + return 0; +} + +FIXTURE(conntrack_dump_flush) +{ + struct mnl_socket *sock; +}; + +FIXTURE_VARIANT(conntrack_dump_flush) +{ + bool use_cta_filter; +}; + +FIXTURE_VARIANT_ADD(conntrack_dump_flush, cta_zone) +{ + .use_cta_filter = false, +}; + +FIXTURE_VARIANT_ADD(conntrack_dump_flush, cta_filter) +{ + .use_cta_filter = true, +}; + +FIXTURE_SETUP(conntrack_dump_flush) +{ + int ret; + + if (conntrack_zone_setup(_metadata, &self->sock)) + return; + + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID, false); EXPECT_GE(ret, 2); if (ret > 2) SKIP(return, "kernel does not support filtering by zone"); + + if (variant->use_cta_filter) { + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID, true); + if (ret < 0 && errno == EINVAL) + SKIP(return, "kernel does not support CTA_FILTER_ZONE"); + ASSERT_GE(ret, 0); + } } FIXTURE_TEARDOWN(conntrack_dump_flush) @@ -434,39 +484,42 @@ TEST_F(conntrack_dump_flush, test_dump_by_zone) { int ret; - ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID, + variant->use_cta_filter); EXPECT_EQ(ret, 2); } TEST_F(conntrack_dump_flush, test_flush_by_zone) { + bool filter = variant->use_cta_filter; int ret; - ret = conntrack_flush_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_flush_zone(self->sock, TEST_ZONE_ID, filter); EXPECT_EQ(ret, 0); - ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID, filter); EXPECT_EQ(ret, 0); - ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 1); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 1, filter); EXPECT_EQ(ret, 2); - ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 2); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 2, filter); EXPECT_EQ(ret, 2); - ret = conntrack_count_zone(self->sock, NF_CT_DEFAULT_ZONE_ID); + ret = conntrack_count_zone(self->sock, NF_CT_DEFAULT_ZONE_ID, filter); EXPECT_EQ(ret, 2); } TEST_F(conntrack_dump_flush, test_flush_by_zone_default) { + bool filter = variant->use_cta_filter; int ret; - ret = conntrack_flush_zone(self->sock, NF_CT_DEFAULT_ZONE_ID); + ret = conntrack_flush_zone(self->sock, NF_CT_DEFAULT_ZONE_ID, filter); EXPECT_EQ(ret, 0); - ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID, filter); EXPECT_EQ(ret, 2); - ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 1); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 1, filter); EXPECT_EQ(ret, 2); - ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 2); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 2, filter); EXPECT_EQ(ret, 2); - ret = conntrack_count_zone(self->sock, NF_CT_DEFAULT_ZONE_ID); + ret = conntrack_count_zone(self->sock, NF_CT_DEFAULT_ZONE_ID, filter); EXPECT_EQ(ret, 0); }