Netdev List
 help / color / mirror / Atom feed
* [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen
@ 2026-09-10  9:00 Julius Bairaktaris
  2026-09-10  9:00 ` [PATCH nf-next 1/4] netfilter: conntrack: pick up a TCP flow whose SYN was never answered Julius Bairaktaris
                   ` (4 more replies)
  0 siblings, 5 replies; 9+ messages in thread
From: Julius Bairaktaris @ 2026-09-10  9:00 UTC (permalink / raw)
  To: pablo, netfilter-devel; +Cc: kadlec, fw, coreteam, netdev, geldot

A host that forwards one direction of a TCP connection only sees the
client's SYN and then an ACK continuing from it; the answer took another
path. That ACK has no entry in the transition table, so it and every
packet after it are invalid, the conntrack entry stays in SYN_SENT
[UNREPLIED] with a single packet, and the connection reaches neither the
stateful part of a ruleset nor a flowtable.

Patch 1 takes such a connection over as the mid-stream pickup it is.
Patch 2 withholds the reply direction of a flow offloaded in one
direction until conntrack has seen a reply, and offloads it once
conntrack has. Patch 3 offers a connection whose reply was never seen to
the flowtable in the original direction. Patch 4 adds a selftest arm for
the path; without patches 1 to 3 it fails, with the router forwarding the
SYN of the connection and nothing else.

Verified on an IPQ8074 router with hardware flow offload, iperf3 -P2
between two hosts on different subnets of one bridge with the reply
direction bypassing the router:

                                  CPU port    switch port   throughput
  asymmetric, without the series   81k pps       81k pps    944-947 Mbit/s
  asymmetric, with the series      2-11 pps      81k pps    948-949 Mbit/s
  symmetric, with the series      11-38 pps      81k pps    926-948 Mbit/s

I wrote this series with the help of an AI coding assistant, as the
Assisted-by tags record. I have reviewed and tested it myself.

Gary Dotzler (2):
  netfilter: conntrack: pick up a TCP flow whose SYN was never answered
  netfilter: nft_flow_offload: offload a TCP flow that has no reply

Julius Bairaktaris (2):
  netfilter: flowtable: promote a flow offloaded in one direction only
  selftests: netfilter: cover a TCP flow whose reply is never seen

 include/net/netfilter/nf_conntrack_l4proto.h  |  7 ++
 net/netfilter/nf_conntrack_proto_tcp.c        | 19 +++++
 net/netfilter/nf_flow_table_ip.c              | 26 +++++++
 net/netfilter/nft_flow_offload.c              |  6 +-
 .../selftests/net/netfilter/nft_flowtable.sh  | 73 +++++++++++++++++++
 5 files changed, 129 insertions(+), 2 deletions(-)

-- 
2.53.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH nf-next 1/4] netfilter: conntrack: pick up a TCP flow whose SYN was never answered
  2026-09-10  9:00 [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen Julius Bairaktaris
@ 2026-09-10  9:00 ` Julius Bairaktaris
  2026-09-10  9:00 ` [PATCH nf-next 2/4] netfilter: flowtable: promote a flow offloaded in one direction only Julius Bairaktaris
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 9+ messages in thread
From: Julius Bairaktaris @ 2026-09-10  9:00 UTC (permalink / raw)
  To: pablo, netfilter-devel; +Cc: kadlec, fw, coreteam, netdev, geldot

From: Gary Dotzler <geldot@protonmail.com>

When only one direction of a connection passes the host, the SYN is
seen, the answer to it is not, and the next packet is an ACK continuing
from where the SYN left off. The transition table has no entry for that,
so that ACK and every packet after it are invalid and the entry sits in
SYN_SENT [UNREPLIED] with one packet.

Treat the connection as a mid-stream pickup. Delete the entry and look
the packet up again, so it creates a new entry through the loose path.
That path fills in the unseen direction from the packet and stops
window checking in both directions.

Signed-off-by: Gary Dotzler <geldot@protonmail.com>
Tested-by: Julius Bairaktaris <julius@bairaktaris.de>
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
Assisted-by: Claude:claude-opus-5
---
 net/netfilter/nf_conntrack_proto_tcp.c | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/net/netfilter/nf_conntrack_proto_tcp.c b/net/netfilter/nf_conntrack_proto_tcp.c
index ad6f1986d52a..335b11301a78 100644
--- a/net/netfilter/nf_conntrack_proto_tcp.c
+++ b/net/netfilter/nf_conntrack_proto_tcp.c
@@ -1173,6 +1173,25 @@ int nf_conntrack_tcp_packet(struct nf_conn *ct,
 			return NF_ACCEPT;
 		}
 
+		/* The answer to the SYN never passed the host, as happens
+		 * when the reply direction takes another path, and the client
+		 * continues from where its SYN left off.  Take the connection
+		 * over as a mid-stream pickup: delete the entry so the packet
+		 * creates a new one that seeds the unseen direction from the
+		 * packet itself.
+		 */
+		if (tn->tcp_loose && !nfct_synproxy(ct) &&
+		    old_state == TCP_CONNTRACK_SYN_SENT &&
+		    index == TCP_ACK_SET && dir == IP_CT_DIR_ORIGINAL &&
+		    !test_bit(IPS_SEEN_REPLY_BIT, &ct->status) &&
+		    ntohl(th->seq) == ct->proto.tcp.seen[dir].td_end) {
+			spin_unlock_bh(&ct->lock);
+
+			if (nf_ct_kill(ct))
+				return -NF_REPEAT;
+			return NF_DROP;
+		}
+
 		/* Invalid packet */
 		spin_unlock_bh(&ct->lock);
 		nf_ct_l4proto_log_invalid(skb, ct, state,
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH nf-next 2/4] netfilter: flowtable: promote a flow offloaded in one direction only
  2026-09-10  9:00 [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen Julius Bairaktaris
  2026-09-10  9:00 ` [PATCH nf-next 1/4] netfilter: conntrack: pick up a TCP flow whose SYN was never answered Julius Bairaktaris
@ 2026-09-10  9:00 ` Julius Bairaktaris
  2026-09-10  9:00 ` [PATCH nf-next 3/4] netfilter: nft_flow_offload: offload a TCP flow that has no reply Julius Bairaktaris
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 9+ messages in thread
From: Julius Bairaktaris @ 2026-09-10  9:00 UTC (permalink / raw)
  To: pablo, netfilter-devel; +Cc: kadlec, fw, coreteam, netdev, geldot

A flow offloaded in the original direction alone carries a reply tuple
that conntrack has never seen a packet for. Leave that direction on the
classic path, so conntrack tracks it, and offload it as well once the
connection is assured.

act_ct promotes its unidirectional UDP flows on the same condition.

Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
Assisted-by: Claude:claude-opus-5
---
 net/netfilter/nf_flow_table_ip.c | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c
index c8c29a9a1684..f2cd5c2c5365 100644
--- a/net/netfilter/nf_flow_table_ip.c
+++ b/net/netfilter/nf_flow_table_ip.c
@@ -465,6 +465,26 @@ nf_flow_offload_lookup(struct nf_flowtable_ctx *ctx,
 	return flow_offload_lookup(flow_table, &tuple);
 }
 
+/* The reply direction of a flow offloaded in one direction only stays on the
+ * classic path so that conntrack sees it. Once conntrack has, the flow is
+ * offloaded in both directions.
+ */
+static bool nf_flow_reply_unoffloaded(struct nf_flowtable *flow_table,
+				      struct flow_offload *flow,
+				      enum flow_offload_tuple_dir dir)
+{
+	if (dir != FLOW_OFFLOAD_DIR_REPLY ||
+	    test_bit(NF_FLOW_HW_BIDIRECTIONAL, &flow->flags))
+		return false;
+
+	if (test_bit(IPS_ASSURED_BIT, &flow->ct->status)) {
+		set_bit(NF_FLOW_HW_BIDIRECTIONAL, &flow->flags);
+		flow_offload_refresh(flow_table, flow, true);
+	}
+
+	return true;
+}
+
 static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx,
 				   struct nf_flowtable *flow_table,
 				   struct flow_offload_tuple_rhash *tuplehash,
@@ -478,6 +498,9 @@ static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx,
 	dir = tuplehash->tuple.dir;
 	flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
 
+	if (nf_flow_reply_unoffloaded(flow_table, flow, dir))
+		return 0;
+
 	mtu = flow->tuplehash[dir].tuple.mtu + ctx->offset;
 	if (flow->tuplehash[!dir].tuple.tun_num)
 		mtu -= sizeof(*iph);
@@ -1074,6 +1097,9 @@ static int nf_flow_offload_ipv6_forward(struct nf_flowtable_ctx *ctx,
 	dir = tuplehash->tuple.dir;
 	flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
 
+	if (nf_flow_reply_unoffloaded(flow_table, flow, dir))
+		return 0;
+
 	mtu = flow->tuplehash[dir].tuple.mtu + ctx->offset;
 	if (flow->tuplehash[!dir].tuple.tun_num)
 		mtu -= sizeof(*ip6h);
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH nf-next 3/4] netfilter: nft_flow_offload: offload a TCP flow that has no reply
  2026-09-10  9:00 [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen Julius Bairaktaris
  2026-09-10  9:00 ` [PATCH nf-next 1/4] netfilter: conntrack: pick up a TCP flow whose SYN was never answered Julius Bairaktaris
  2026-09-10  9:00 ` [PATCH nf-next 2/4] netfilter: flowtable: promote a flow offloaded in one direction only Julius Bairaktaris
@ 2026-09-10  9:00 ` Julius Bairaktaris
  2026-09-10  9:00 ` [PATCH nf-next 4/4] selftests: netfilter: cover a TCP flow whose reply is never seen Julius Bairaktaris
  2026-09-11 11:41 ` [PATCH nf-next 0/4] netfilter: offload " Pablo Neira Ayuso
  4 siblings, 0 replies; 9+ messages in thread
From: Julius Bairaktaris @ 2026-09-10  9:00 UTC (permalink / raw)
  To: pablo, netfilter-devel; +Cc: kadlec, fw, coreteam, netdev, geldot

From: Gary Dotzler <geldot@protonmail.com>

A connection whose reply never reaches the host cannot become assured,
and only assured TCP connections are offered to the flowtable, so every
packet of it stays on the classic path.

Offer such a connection to the flowtable in the original direction. The
reply direction follows once conntrack has seen a reply, so a flow that
starts one-directional picks it up if the path turns symmetric.

Signed-off-by: Gary Dotzler <geldot@protonmail.com>
Co-developed-by: Julius Bairaktaris <julius@bairaktaris.de>
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
Assisted-by: Claude:claude-opus-5
---
 include/net/netfilter/nf_conntrack_l4proto.h | 7 +++++++
 net/netfilter/nft_flow_offload.c             | 6 ++++--
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/include/net/netfilter/nf_conntrack_l4proto.h b/include/net/netfilter/nf_conntrack_l4proto.h
index fde2427ceb8f..a4e3d9ed7a64 100644
--- a/include/net/netfilter/nf_conntrack_l4proto.h
+++ b/include/net/netfilter/nf_conntrack_l4proto.h
@@ -208,6 +208,13 @@ static inline bool nf_conntrack_tcp_established(const struct nf_conn *ct)
 	return ct->proto.tcp.state == TCP_CONNTRACK_ESTABLISHED &&
 	       test_bit(IPS_ASSURED_BIT, &ct->status);
 }
+
+/* A flow picked up without its reply cannot become assured. */
+static inline bool nf_conntrack_tcp_unreplied(const struct nf_conn *ct)
+{
+	return ct->proto.tcp.state == TCP_CONNTRACK_ESTABLISHED &&
+	       !test_bit(IPS_SEEN_REPLY_BIT, &ct->status);
+}
 #endif
 
 #ifdef CONFIG_NF_CT_PROTO_SCTP
diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c
index 32b4281038dd..0b6be8ac3d24 100644
--- a/net/netfilter/nft_flow_offload.c
+++ b/net/netfilter/nft_flow_offload.c
@@ -73,7 +73,8 @@ static void nft_flow_offload_eval(const struct nft_expr *expr,
 		tcph = skb_header_pointer(pkt->skb, nft_thoff(pkt),
 					  sizeof(_tcph), &_tcph);
 		if (unlikely(!tcph || tcph->fin || tcph->rst ||
-			     !nf_conntrack_tcp_established(ct)))
+			     (!nf_conntrack_tcp_established(ct) &&
+			      !nf_conntrack_tcp_unreplied(ct))))
 			goto out;
 		break;
 	case IPPROTO_UDP:
@@ -117,7 +118,8 @@ static void nft_flow_offload_eval(const struct nft_expr *expr,
 	if (tcph)
 		flow_offload_ct_tcp(ct);
 
-	__set_bit(NF_FLOW_HW_BIDIRECTIONAL, &flow->flags);
+	if (!tcph || !nf_conntrack_tcp_unreplied(ct))
+		__set_bit(NF_FLOW_HW_BIDIRECTIONAL, &flow->flags);
 	ret = flow_offload_add(flowtable, flow);
 	if (ret < 0)
 		goto err_flow_add;
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH nf-next 4/4] selftests: netfilter: cover a TCP flow whose reply is never seen
  2026-09-10  9:00 [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen Julius Bairaktaris
                   ` (2 preceding siblings ...)
  2026-09-10  9:00 ` [PATCH nf-next 3/4] netfilter: nft_flow_offload: offload a TCP flow that has no reply Julius Bairaktaris
@ 2026-09-10  9:00 ` Julius Bairaktaris
  2026-09-11 11:41 ` [PATCH nf-next 0/4] netfilter: offload " Pablo Neira Ayuso
  4 siblings, 0 replies; 9+ messages in thread
From: Julius Bairaktaris @ 2026-09-10  9:00 UTC (permalink / raw)
  To: pablo, netfilter-devel; +Cc: kadlec, fw, coreteam, netdev, geldot

Add an arm to nft_flowtable.sh in which ns2 answers over a direct link,
so that nsr1 sees the original direction only. The forward hook counter
then has to stay far below the size of the transferred file, which only
happens if the flowtable takes the connection over.

Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
Assisted-by: Claude:claude-opus-5
---
 .../selftests/net/netfilter/nft_flowtable.sh  | 73 +++++++++++++++++++
 1 file changed, 73 insertions(+)

diff --git a/tools/testing/selftests/net/netfilter/nft_flowtable.sh b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
index 449c518bd947..516a544266f1 100755
--- a/tools/testing/selftests/net/netfilter/nft_flowtable.sh
+++ b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
@@ -516,6 +516,79 @@ else
 	ret=1
 fi
 
+# Asymmetric path test:
+# ns2 answers over a direct link, so nsr1 sees the original direction only.
+# Such a connection never becomes assured, but the flowtable is expected to
+# take over the direction that nsr1 does see.
+check_orig_offloaded()
+{
+	local what=$1
+
+	local orig
+	orig=$(ip netns exec "$nsr1" nft reset counter inet filter routed_orig | grep packets)
+	local orig_cnt=${orig#*bytes}
+
+	local fs
+	fs=$(du -sb "$nsin")
+	local max_orig=$(( ${fs%%/*} / 2 ))
+
+	# the flowtable takes over after the first few packets, so the forward
+	# hook must see a small fraction of the transferred file.
+	if [ "$orig_cnt" -gt "$max_orig" ];then
+		echo "FAIL: $what: original counter $orig_cnt exceeds expected value $max_orig" 1>&2
+		ret=1
+		return 1
+	fi
+
+	echo "PASS: $what"
+}
+
+test_asymmetric_path()
+{
+	ip link add name eth1 netns "$ns1" type veth peer name eth1 netns "$ns2"
+	ip -net "$ns1" addr add 10.0.9.99/24 dev eth1
+	ip -net "$ns2" addr add 10.0.9.98/24 dev eth1
+	ip -net "$ns1" addr add dead:9::99/64 dev eth1 nodad
+	ip -net "$ns2" addr add dead:9::98/64 dev eth1 nodad
+	ip -net "$ns1" link set eth1 up
+	ip -net "$ns2" link set eth1 up
+
+	# ns1 keeps sending through nsr1, ns2 answers on the direct link.
+	ip -net "$ns2" route add 10.0.1.99 via 10.0.9.99 dev eth1
+	ip -6 -net "$ns2" route add dead:1::99 via dead:9::99 dev eth1
+
+	ip netns exec "$ns1" sysctl -q net.ipv4.ip_no_pmtu_disc=0
+	ip netns exec "$ns2" sysctl -q net.ipv4.ip_no_pmtu_disc=0
+
+	ip netns exec "$nsr1" nft reset counters table inet filter >/dev/null
+
+	if test_tcp_forwarding "$ns1" "$ns2" 1 4 10.0.2.99 12345; then
+		check_orig_offloaded "flow offloaded for ns1/ns2 without reply"
+	else
+		echo "FAIL: flow offload for ns1/ns2 without reply" 1>&2
+		ip netns exec "$nsr1" nft list ruleset 1>&2
+		ret=1
+	fi
+
+	ip netns exec "$nsr1" nft reset counters table inet filter >/dev/null
+
+	if test_tcp_forwarding "$ns1" "$ns2" 1 6 "[dead:2::99]" 12345; then
+		check_orig_offloaded "IPv6 flow offloaded for ns1/ns2 without reply"
+	else
+		echo "FAIL: IPv6 flow offload for ns1/ns2 without reply" 1>&2
+		ip netns exec "$nsr1" nft list ruleset 1>&2
+		ret=1
+	fi
+
+	ip netns exec "$ns1" sysctl -q net.ipv4.ip_no_pmtu_disc=1
+	ip netns exec "$ns2" sysctl -q net.ipv4.ip_no_pmtu_disc=1
+
+	ip -net "$ns1" link del eth1
+	ip netns exec "$nsr1" nft reset counters table inet filter >/dev/null
+}
+
+test_asymmetric_path
+
 # delete default route, i.e. ns2 won't be able to reach ns1 and
 # will depend on ns1 being masqueraded in nsr1.
 # expect ns1 has nsr1 address.
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen
  2026-09-10  9:00 [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen Julius Bairaktaris
                   ` (3 preceding siblings ...)
  2026-09-10  9:00 ` [PATCH nf-next 4/4] selftests: netfilter: cover a TCP flow whose reply is never seen Julius Bairaktaris
@ 2026-09-11 11:41 ` Pablo Neira Ayuso
  2026-09-11 12:10   ` Julius Bairaktaris
  4 siblings, 1 reply; 9+ messages in thread
From: Pablo Neira Ayuso @ 2026-09-11 11:41 UTC (permalink / raw)
  To: Julius Bairaktaris; +Cc: netfilter-devel, kadlec, fw, coreteam, netdev, geldot

On Thu, Sep 10, 2026 at 11:00:48AM +0200, Julius Bairaktaris wrote:
> A host that forwards one direction of a TCP connection only sees the
> client's SYN and then an ACK continuing from it; the answer took another
> path. That ACK has no entry in the transition table, so it and every
> packet after it are invalid, the conntrack entry stays in SYN_SENT
> [UNREPLIED] with a single packet, and the connection reaches neither the
> stateful part of a ruleset nor a flowtable.

conntrack needs to see packets in both directions, are you assuming a
packet-based load balancer in front of it?

> Patch 1 takes such a connection over as the mid-stream pickup it is.
> Patch 2 withholds the reply direction of a flow offloaded in one
> direction until conntrack has seen a reply, and offloads it once
> conntrack has. Patch 3 offers a connection whose reply was never seen to
> the flowtable in the original direction. Patch 4 adds a selftest arm for
> the path; without patches 1 to 3 it fails, with the router forwarding the
> SYN of the connection and nothing else.
> 
> Verified on an IPQ8074 router with hardware flow offload, iperf3 -P2
> between two hosts on different subnets of one bridge with the reply
> direction bypassing the router:
> 
>                                   CPU port    switch port   throughput
>   asymmetric, without the series   81k pps       81k pps    944-947 Mbit/s
>   asymmetric, with the series      2-11 pps      81k pps    948-949 Mbit/s
>   symmetric, with the series      11-38 pps      81k pps    926-948 Mbit/s
> 
> I wrote this series with the help of an AI coding assistant, as the
> Assisted-by tags record. I have reviewed and tested it myself.
> 
> Gary Dotzler (2):
>   netfilter: conntrack: pick up a TCP flow whose SYN was never answered
>   netfilter: nft_flow_offload: offload a TCP flow that has no reply
> 
> Julius Bairaktaris (2):
>   netfilter: flowtable: promote a flow offloaded in one direction only
>   selftests: netfilter: cover a TCP flow whose reply is never seen
> 
>  include/net/netfilter/nf_conntrack_l4proto.h  |  7 ++
>  net/netfilter/nf_conntrack_proto_tcp.c        | 19 +++++
>  net/netfilter/nf_flow_table_ip.c              | 26 +++++++
>  net/netfilter/nft_flow_offload.c              |  6 +-
>  .../selftests/net/netfilter/nft_flowtable.sh  | 73 +++++++++++++++++++
>  5 files changed, 129 insertions(+), 2 deletions(-)
> 
> -- 
> 2.53.0
> 
> 

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen
  2026-09-11 11:41 ` [PATCH nf-next 0/4] netfilter: offload " Pablo Neira Ayuso
@ 2026-09-11 12:10   ` Julius Bairaktaris
  2026-09-11 14:09     ` Pablo Neira Ayuso
  0 siblings, 1 reply; 9+ messages in thread
From: Julius Bairaktaris @ 2026-09-11 12:10 UTC (permalink / raw)
  To: Pablo Neira Ayuso; +Cc: netfilter-devel, kadlec, fw, coreteam, netdev, geldot

Hi Pablo,

thanks for taking your time to review.

> conntrack needs to see packets in both directions, are you assuming a
> packet-based load balancer in front of it?

No, an asymmetric route is in front of it, with two subnets sharing one
L2 segment and the server answering over that link, so the router only
ever sees one direction.

Conntrack already picks such a connection up when it misses the
handshake entirely, and patch 1 routes the SYN-seen case into that same
path, under the same nf_conntrack_tcp_loose gate.

  net/netfilter/nf_conntrack_proto_tcp.c, tcp_new():

    } else if (tn->tcp_loose == 0) {
        /* Don't try to pick up connections. */
        return false;
    } else {
        ...
        ct->proto.tcp.seen[0].flags =
        ct->proto.tcp.seen[1].flags = IP_CT_TCP_FLAG_SACK_PERM |
                                      IP_CT_TCP_FLAG_BE_LIBERAL;

Julius



Am Fr., 11. Sept. 2026 um 11:42 Uhr schrieb Pablo Neira Ayuso
<pablo@netfilter.org>:

>
> On Thu, Sep 10, 2026 at 11:00:48AM +0200, Julius Bairaktaris wrote:
> > A host that forwards one direction of a TCP connection only sees the
> > client's SYN and then an ACK continuing from it; the answer took another
> > path. That ACK has no entry in the transition table, so it and every
> > packet after it are invalid, the conntrack entry stays in SYN_SENT
> > [UNREPLIED] with a single packet, and the connection reaches neither the
> > stateful part of a ruleset nor a flowtable.
>
> conntrack needs to see packets in both directions, are you assuming a
> packet-based load balancer in front of it?
>
> > Patch 1 takes such a connection over as the mid-stream pickup it is.
> > Patch 2 withholds the reply direction of a flow offloaded in one
> > direction until conntrack has seen a reply, and offloads it once
> > conntrack has. Patch 3 offers a connection whose reply was never seen to
> > the flowtable in the original direction. Patch 4 adds a selftest arm for
> > the path; without patches 1 to 3 it fails, with the router forwarding the
> > SYN of the connection and nothing else.
> >
> > Verified on an IPQ8074 router with hardware flow offload, iperf3 -P2
> > between two hosts on different subnets of one bridge with the reply
> > direction bypassing the router:
> >
> >                                   CPU port    switch port   throughput
> >   asymmetric, without the series   81k pps       81k pps    944-947 Mbit/s
> >   asymmetric, with the series      2-11 pps      81k pps    948-949 Mbit/s
> >   symmetric, with the series      11-38 pps      81k pps    926-948 Mbit/s
> >
> > I wrote this series with the help of an AI coding assistant, as the
> > Assisted-by tags record. I have reviewed and tested it myself.
> >
> > Gary Dotzler (2):
> >   netfilter: conntrack: pick up a TCP flow whose SYN was never answered
> >   netfilter: nft_flow_offload: offload a TCP flow that has no reply
> >
> > Julius Bairaktaris (2):
> >   netfilter: flowtable: promote a flow offloaded in one direction only
> >   selftests: netfilter: cover a TCP flow whose reply is never seen
> >
> >  include/net/netfilter/nf_conntrack_l4proto.h  |  7 ++
> >  net/netfilter/nf_conntrack_proto_tcp.c        | 19 +++++
> >  net/netfilter/nf_flow_table_ip.c              | 26 +++++++
> >  net/netfilter/nft_flow_offload.c              |  6 +-
> >  .../selftests/net/netfilter/nft_flowtable.sh  | 73 +++++++++++++++++++
> >  5 files changed, 129 insertions(+), 2 deletions(-)
> >
> > --
> > 2.53.0
> >
> >

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen
  2026-09-11 12:10   ` Julius Bairaktaris
@ 2026-09-11 14:09     ` Pablo Neira Ayuso
  2026-09-11 14:16       ` Pablo Neira Ayuso
  0 siblings, 1 reply; 9+ messages in thread
From: Pablo Neira Ayuso @ 2026-09-11 14:09 UTC (permalink / raw)
  To: Julius Bairaktaris; +Cc: netfilter-devel, kadlec, fw, coreteam, netdev, geldot

Hi Julius,

On Fri, Sep 11, 2026 at 12:10:04PM +0000, Julius Bairaktaris wrote:
> Hi Pablo,
> 
> thanks for taking your time to review.
> 
> > conntrack needs to see packets in both directions, are you assuming a
> > packet-based load balancer in front of it?
> 
> No, an asymmetric route is in front of it, with two subnets sharing one
> L2 segment and the server answering over that link, so the router only
> ever sees one direction.

I see this requirement to support asymmetric path keeps coming, but
how hard is really to maintain this TCP state machine to deal with all
possible scenarios? ie. invalid transitions, retransmissions, etc.
this all without having access to full TCP connection.  Is it that you
need NAT and the stateless NAT in nftables does not fulfill your
requirements?

> Conntrack already picks such a connection up when it misses the
> handshake entirely, and patch 1 routes the SYN-seen case into that same
> path, under the same nf_conntrack_tcp_loose gate.
> 
>   net/netfilter/nf_conntrack_proto_tcp.c, tcp_new():
> 
>     } else if (tn->tcp_loose == 0) {
>         /* Don't try to pick up connections. */
>         return false;
>     } else {
>         ...
>         ct->proto.tcp.seen[0].flags =
>         ct->proto.tcp.seen[1].flags = IP_CT_TCP_FLAG_SACK_PERM |
>                                       IP_CT_TCP_FLAG_BE_LIBERAL;
> 
> Julius
> 
> 
> 
> Am Fr., 11. Sept. 2026 um 11:42 Uhr schrieb Pablo Neira Ayuso
> <pablo@netfilter.org>:
> 
> >
> > On Thu, Sep 10, 2026 at 11:00:48AM +0200, Julius Bairaktaris wrote:
> > > A host that forwards one direction of a TCP connection only sees the
> > > client's SYN and then an ACK continuing from it; the answer took another
> > > path. That ACK has no entry in the transition table, so it and every
> > > packet after it are invalid, the conntrack entry stays in SYN_SENT
> > > [UNREPLIED] with a single packet, and the connection reaches neither the
> > > stateful part of a ruleset nor a flowtable.
> >
> > conntrack needs to see packets in both directions, are you assuming a
> > packet-based load balancer in front of it?
> >
> > > Patch 1 takes such a connection over as the mid-stream pickup it is.
> > > Patch 2 withholds the reply direction of a flow offloaded in one
> > > direction until conntrack has seen a reply, and offloads it once
> > > conntrack has. Patch 3 offers a connection whose reply was never seen to
> > > the flowtable in the original direction. Patch 4 adds a selftest arm for
> > > the path; without patches 1 to 3 it fails, with the router forwarding the
> > > SYN of the connection and nothing else.
> > >
> > > Verified on an IPQ8074 router with hardware flow offload, iperf3 -P2
> > > between two hosts on different subnets of one bridge with the reply
> > > direction bypassing the router:
> > >
> > >                                   CPU port    switch port   throughput
> > >   asymmetric, without the series   81k pps       81k pps    944-947 Mbit/s
> > >   asymmetric, with the series      2-11 pps      81k pps    948-949 Mbit/s
> > >   symmetric, with the series      11-38 pps      81k pps    926-948 Mbit/s
> > >
> > > I wrote this series with the help of an AI coding assistant, as the
> > > Assisted-by tags record. I have reviewed and tested it myself.
> > >
> > > Gary Dotzler (2):
> > >   netfilter: conntrack: pick up a TCP flow whose SYN was never answered
> > >   netfilter: nft_flow_offload: offload a TCP flow that has no reply
> > >
> > > Julius Bairaktaris (2):
> > >   netfilter: flowtable: promote a flow offloaded in one direction only
> > >   selftests: netfilter: cover a TCP flow whose reply is never seen
> > >
> > >  include/net/netfilter/nf_conntrack_l4proto.h  |  7 ++
> > >  net/netfilter/nf_conntrack_proto_tcp.c        | 19 +++++
> > >  net/netfilter/nf_flow_table_ip.c              | 26 +++++++
> > >  net/netfilter/nft_flow_offload.c              |  6 +-
> > >  .../selftests/net/netfilter/nft_flowtable.sh  | 73 +++++++++++++++++++
> > >  5 files changed, 129 insertions(+), 2 deletions(-)
> > >
> > > --
> > > 2.53.0
> > >
> > >

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen
  2026-09-11 14:09     ` Pablo Neira Ayuso
@ 2026-09-11 14:16       ` Pablo Neira Ayuso
  0 siblings, 0 replies; 9+ messages in thread
From: Pablo Neira Ayuso @ 2026-09-11 14:16 UTC (permalink / raw)
  To: Julius Bairaktaris; +Cc: netfilter-devel, kadlec, fw, coreteam, netdev, geldot

On Fri, Sep 11, 2026 at 04:09:51PM +0200, Pablo Neira Ayuso wrote:
> Hi Julius,
> 
> On Fri, Sep 11, 2026 at 12:10:04PM +0000, Julius Bairaktaris wrote:
> > Hi Pablo,
> > 
> > thanks for taking your time to review.
> > 
> > > conntrack needs to see packets in both directions, are you assuming a
> > > packet-based load balancer in front of it?
> > 
> > No, an asymmetric route is in front of it, with two subnets sharing one
> > L2 segment and the server answering over that link, so the router only
> > ever sees one direction.
> 
> I see this requirement to support asymmetric path keeps coming, but
> how hard is really to maintain this TCP state machine to deal with all
> possible scenarios? ie. invalid transitions, retransmissions, etc.
> this all without having access to full TCP connection.  Is it that you
> need NAT and the stateless NAT in nftables does not fulfill your
> requirements?

I can reply myself, you're targetting at offloading this flow via the
flowtable.

Let me take a look what can be done here for this assymetric case.

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-09-11 14:16 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-10  9:00 [PATCH nf-next 0/4] netfilter: offload a TCP flow whose reply is never seen Julius Bairaktaris
2026-09-10  9:00 ` [PATCH nf-next 1/4] netfilter: conntrack: pick up a TCP flow whose SYN was never answered Julius Bairaktaris
2026-09-10  9:00 ` [PATCH nf-next 2/4] netfilter: flowtable: promote a flow offloaded in one direction only Julius Bairaktaris
2026-09-10  9:00 ` [PATCH nf-next 3/4] netfilter: nft_flow_offload: offload a TCP flow that has no reply Julius Bairaktaris
2026-09-10  9:00 ` [PATCH nf-next 4/4] selftests: netfilter: cover a TCP flow whose reply is never seen Julius Bairaktaris
2026-09-11 11:41 ` [PATCH nf-next 0/4] netfilter: offload " Pablo Neira Ayuso
2026-09-11 12:10   ` Julius Bairaktaris
2026-09-11 14:09     ` Pablo Neira Ayuso
2026-09-11 14:16       ` Pablo Neira Ayuso

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox