On Tue, Oct 13, 2026 at 12:15 PM Dumitru Ceara <[email protected]> wrote:
> Megaflows matching on these are not offloadable on specific hardware
> NICs. These were originally introduced by 20a96b9d2c3d ("northd: Allow
> fragmented traffic through LB.") in order to address the fact that with
> the userspace datapath fragmented IP packets miss L4 headers in later
> fragments, breaking load balancing. A follow up commit, 8e6f9a8355e2
> ("northd: Fix HW offload problem related to ct_tuple."), tried to
> address the offload issue but missed to add a check to only send the
> first packet of a session (ct_state=+new) to the CT EXTRACT side table
> if and only if it is a fragmented IP packet.
>
> That means that ct_tuple matches would always be present for
> megaflows forwarding unidirectional UDP traffic, even if the IP packets
> are not fragmented. Some hardware NICs may be capable of offloading
> such traffic (see the first Reported-at link) but only if their
> megaflows don't match on ct_tuple fields.
>
> A previous patch attempted to fix a similar issue by limiting switch
> conntrack tuple fields extraction only to traffic destined to load
> balancers that are configured with L4 ports. That approach has some
> downsides and requires an additional logical flow for each load balancer
> VIP.
>
> This patch attempts fixing the issue in a different, a bit more generic
> way: it turns out we don't need to send all ct_state=+new traffic to
> the CT EXTRACT table, we should instead do that only for fragments.
>
> Reported-at:
> https://mail.openvswitch.org/pipermail/ovs-discuss/2026-October/054041.html
> Reported-at:
> https://patchwork.ozlabs.org/project/ovn/patch/[email protected]/
> Reported-at: https://redhat.atlassian.net/browse/FDP-4458
> Fixes: 20a96b9d2c3d ("northd: Allow fragmented traffic through LB.")
> Fixes: 8e6f9a8355e2 ("northd: Fix HW offload problem related to ct_tuple.")
> Assisted-by: Claude Opus 4.6, OpenCode
> Signed-off-by: Dumitru Ceara <[email protected]>
> ---
> northd/northd.c | 34 ++++++++++++++++-
> tests/ovn-northd.at | 62 +++++++++++++++++++++++++++---
> tests/system-ovn.at | 92 +++++++++++++++++++++++++++++++++++++++++++++
> 3 files changed, 180 insertions(+), 8 deletions(-)
>
> diff --git a/northd/northd.c b/northd/northd.c
> index 7c4cb5a62f..84e830428c 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -18413,10 +18413,25 @@ build_lrouter_in_ct_extract_flows(struct
> lflow_table *lflows,
> /* Ingress CT_EXTRACT table: Extract connection tracking fields for
> new
> * connections. */
> ovn_lflow_add(lflows, od, S_ROUTER_IN_CT_EXTRACT, 100,
> - "ct.new && ip",
> + "ct.new && ip && ip.is_frag",
> REG_CT_PROTO " = ct_proto(); "
> REG_CT_TP_DST " = ct_tp_dst(); next;",
> lflow_ref);
> + ovn_lflow_add(lflows, od, S_ROUTER_IN_CT_EXTRACT, 100,
> + "ct.new && ip && tcp && !ip.is_frag",
> + REG_CT_PROTO " = ip.proto; "
> + REG_CT_TP_DST " = tcp.dst; next;",
> + lflow_ref);
> + ovn_lflow_add(lflows, od, S_ROUTER_IN_CT_EXTRACT, 100,
> + "ct.new && ip && udp && !ip.is_frag",
> + REG_CT_PROTO " = ip.proto; "
> + REG_CT_TP_DST " = udp.dst; next;",
> + lflow_ref);
> + ovn_lflow_add(lflows, od, S_ROUTER_IN_CT_EXTRACT, 100,
> + "ct.new && ip && sctp && !ip.is_frag",
> + REG_CT_PROTO " = ip.proto; "
> + REG_CT_TP_DST " = sctp.dst; next;",
> + lflow_ref);
> }
>
> static void
> @@ -19699,10 +19714,25 @@ build_ls_stateful_flows(const struct
> ls_stateful_record *ls_stateful_rec,
> * balancers. */
> if (ls_stateful_rec->has_lb_vip) {
> ovn_lflow_add(lflows, od, S_SWITCH_IN_CT_EXTRACT, 100,
> - "ct.new && ip",
> + "ct.new && ip && ip.is_frag",
> REG_CT_PROTO " = ct_proto(); "
> REG_CT_TP_DST " = ct_tp_dst(); next;",
> ls_stateful_rec->lflow_ref);
> + ovn_lflow_add(lflows, od, S_SWITCH_IN_CT_EXTRACT, 100,
> + "ct.new && ip && tcp && !ip.is_frag",
> + REG_CT_PROTO " = ip.proto; "
> + REG_CT_TP_DST " = tcp.dst; next;",
> + ls_stateful_rec->lflow_ref);
> + ovn_lflow_add(lflows, od, S_SWITCH_IN_CT_EXTRACT, 100,
> + "ct.new && ip && udp && !ip.is_frag",
> + REG_CT_PROTO " = ip.proto; "
> + REG_CT_TP_DST " = udp.dst; next;",
> + ls_stateful_rec->lflow_ref);
> + ovn_lflow_add(lflows, od, S_SWITCH_IN_CT_EXTRACT, 100,
> + "ct.new && ip && sctp && !ip.is_frag",
> + REG_CT_PROTO " = ip.proto; "
> + REG_CT_TP_DST " = sctp.dst; next;",
> + ls_stateful_rec->lflow_ref);
> }
>
> build_lb_hairpin(ls_stateful_rec, od, lflows,
> ls_stateful_rec->lflow_ref);
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 7a0a400c4a..a5622e3fbe 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -4335,8 +4335,6 @@ flow="inport == \"lsp1\" && ${flow_eth} &&
> ${flow_ip} && ${flow_tcp}"
> ovn_trace --ct new --ct new ls "${flow}"
> AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> ct_lb_mark {
> - reg1[[16..23]] = 6;
> - reg1[[0..15]] = 80;
> ct_lb_mark {
> reg0[[6]] = 0;
> reg0[[12]] = 0;
> @@ -4350,6 +4348,20 @@ ct_lb_mark {
> # UDP packets should go to conntrack.
> flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
> AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> +ct_lb_mark {
> + ct_lb_mark {
> + reg0[[6]] = 0;
> + reg0[[12]] = 0;
> + ct_lb_mark /* default (use --ct to customize) */ {
> + output("lsp2");
> + };
> + };
> +};
> +])
> +
> +# Fragmented UDP packets should go to conntrack.
> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp} &&
> ip.is_frag"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> ct_lb_mark {
> reg1[[16..23]] = 17;
> reg1[[0..15]] = 80;
> @@ -4378,6 +4390,20 @@ output("lsp2");
> # UDP packets still go to conntrack.
> flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
> AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> +ct_lb_mark {
> + ct_lb_mark {
> + reg0[[6]] = 0;
> + reg0[[12]] = 0;
> + ct_lb_mark /* default (use --ct to customize) */ {
> + output("lsp2");
> + };
> + };
> +};
> +])
> +
> +# Fragmented UDP packets still go to conntrack.
> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp} &&
> ip.is_frag"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> ct_lb_mark {
> reg1[[16..23]] = 17;
> reg1[[0..15]] = 80;
> @@ -4478,8 +4504,6 @@ check ovn-nbctl --wait=sb sync
> flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_tcp}"
> AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> ct_lb_mark {
> - reg1[[16..23]] = 6;
> - reg1[[0..15]] = 80;
> ct_lb_mark {
> reg0[[6]] = 0;
> reg0[[12]] = 0;
> @@ -4493,6 +4517,20 @@ ct_lb_mark {
> # UDP packets should go to conntrack.
> flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
> AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> +ct_lb_mark {
> + ct_lb_mark {
> + reg0[[6]] = 0;
> + reg0[[12]] = 0;
> + ct_lb_mark /* default (use --ct to customize) */ {
> + output("lsp2");
> + };
> + };
> +};
> +])
> +
> +# Fragmented UDP packets should go to conntrack.
> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp} &&
> ip.is_frag"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> ct_lb_mark {
> reg1[[16..23]] = 17;
> reg1[[0..15]] = 80;
> @@ -4521,6 +4559,20 @@ output("lsp2");
> # UDP packets still go to conntrack.
> flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp}"
> AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> +ct_lb_mark {
> + ct_lb_mark {
> + reg0[[6]] = 0;
> + reg0[[12]] = 0;
> + ct_lb_mark /* default (use --ct to customize) */ {
> + output("lsp2");
> + };
> + };
> +};
> +])
> +
> +# Fragmented UDP packets still go to conntrack.
> +flow="inport == \"lsp1\" && ${flow_eth} && ${flow_ip} && ${flow_udp} &&
> ip.is_frag"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal ls "${flow}"],
> [0], [dnl
> ct_lb_mark {
> reg1[[16..23]] = 17;
> reg1[[0..15]] = 80;
> @@ -8580,8 +8632,6 @@ flow="eth.dst == 00:00:00:00:01:00 && inport ==
> \"rtr-ls\" && ip4.src == 42.42.4
> ovn_trace --ct new "${flow}" --lb-dst 42.42.42.42:4242
> AT_CHECK_UNQUOTED([ovn_trace --ct new --minimal "${flow}" --lb-dst
> 42.42.42.42:4242], [0], [dnl
> ct_dnat /* assuming no un-dnat entry, so no change */ {
> - reg1[[16..23]] = 6;
> - reg1[[0..15]] = 4343;
> ct_lb_mark /* default (use --ct to customize) */ {
> ip.ttl--;
> eth.src = 00:00:00:00:01:00;
> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> index 13e62bf9ba..3239d709b4 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -23978,3 +23978,95 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port
> patch-.*/d
>
> AT_CLEANUP
> ])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([CT extraction limited to LB VIPs])
> +AT_KEYWORDS([ovnlb ct_extract])
> +AT_SKIP_IF([test $HAVE_NCAT = no])
> +AT_SKIP_IF([test $HAVE_TCPDUMP = no])
> +
> +dnl Test that the CT_EXTRACT pipeline stage currently matches doesn't
> +dnl generate ct_tuple4 megaflow matches for non fragments.
> +
> +ovn_start
> +
> +OVS_TRAFFIC_VSWITCHD_START()
> +ADD_BR([br-int])
> +
> +check ovs-vsctl
> \
> + -- set Open_vSwitch . external-ids:system-id=hv1
> \
> + -- set Open_vSwitch .
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> + -- set Open_vSwitch . external-ids:ovn-encap-type=geneve
> \
> + -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1
> \
> + -- set bridge br-int fail-mode=secure
> other-config:disable-in-band=true
> +
> +start_daemon ovn-controller
> +
> +check ovn-nbctl \
> + -- lr-add lr0 \
> + -- lrp-add lr0 lrp aa:bb:cc:dd:ee:03 10.0.0.3/24 \
> + -- ls-add ls0 \
> + -- lsp-add ls0 lsp0 \
> + -- lsp-set-addresses lsp0 "aa:bb:cc:dd:ee:01 10.0.0.1" \
> + -- lsp-add ls0 lsp1 \
> + -- lsp-set-addresses lsp1 "aa:bb:cc:dd:ee:02 10.0.0.2" \
> + -- lsp-add-router-port ls0 ls0-lr0 lrp
> +
> +check ovn-nbctl lb-add lb0 10.0.1.100:5000 10.0.0.2:6000 udp
> +check ovn-nbctl ls-lb-add ls0 lb0
> +
> +ADD_NAMESPACES(lsp0, lsp1)
> +ADD_VETH(lsp0, lsp0, br-int, "10.0.0.1/24", "aa:bb:cc:dd:ee:01",
> "10.0.0.3")
> +ADD_VETH(lsp1, lsp1, br-int, "10.0.0.2/24", "aa:bb:cc:dd:ee:02",
> "10.0.0.3")
> +
> +wait_for_ports_up lsp0 lsp1
> +check ovn-nbctl --wait=hv sync
> +
> +dnl Verify basic connectivity before testing megaflows.
> +NS_CHECK_EXEC([lsp0], [ping -q -c 3 -i 0.3 -w 2 10.0.0.2 | FORMAT_PING], \
> +[0], [dnl
> +3 packets transmitted, 3 received, 0% packet loss, time 0ms
> +])
> +
> +check ovs-appctl revalidator/pause
> +on_exit 'ovs-appctl revalidator/resume'
> +
> +dnl --- Unfragmented unidirectional UDP to non-VIP must not have
> ct_tuple4 ---
> +dnl
> +dnl Start a UDP listener on lsp1 to absorb traffic. Start a continuous
> +dnl unidirectional UDP sender on lsp0 to port 4791 (a non-VIP port).
> +dnl The listener does not send replies, so every packet stays ct.new.
> +dnl There should be no ct_tuple4 matches in the megaflows for this
> traffic.
> +NETNS_DAEMONIZE([lsp1], [ncat -u -l 4791 > /dev/null], [udp-listener.pid])
> +NETNS_DAEMONIZE([lsp0], [bash -c 'while true; do echo test; sleep 0.2;
> done | ncat -u 10.0.0.2 4791'], [udp-sender.pid])
> +
> +dnl Wait until the datapath has a cached flow for the UDP traffic we sent.
> +OVS_WAIT_UNTIL([ovs-appctl dpctl/dump-flows | grep -q 'dst=4791'])
> +
> +ovs-appctl dpctl/dump-flows > dpflows
> +AT_CAPTURE_FILE([dpflows])
> +
> +AT_CHECK([grep 'dst=4791' dpflows | grep -q 'ct_tuple4'], [1])
> +
> +dnl --- UDP to VIP is DNAT'd correctly ---
> +dnl
> +NETNS_DAEMONIZE([lsp1], [ncat -u -l 10.0.0.2 6000], [lsp1_udp.pid])
> +NETNS_START_TCPDUMP([lsp1], [-nn -c 1 -i lsp1 'udp and dst port 6000 and
> dst host 10.0.0.2'], [lb-dnat])
> +
> +NS_CHECK_EXEC([lsp0], [echo test | ncat -u 10.0.1.100 5000], [ignore],
> [ignore], [ignore])
> +
> +OVS_WAIT_UNTIL([
> + total_pkts=$(cat lb-dnat.tcpdump | wc -l)
> + test "${total_pkts}" -ge 1
> +])
> +
> +check ovs-appctl revalidator/resume
> +
> +OVN_CLEANUP_CONTROLLER([hv1])
> +OVN_CLEANUP_NORTHD
> +
> +as
> +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
> +/connection dropped.*/d"])
> +AT_CLEANUP
> +])
> --
> 2.55.0
>
>
Looks good to me, thanks.
Acked-by: Ales Musil <[email protected]>
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev