diff mbox series

[ovs-dev,v1] Fix load balanced hairpin traffic for fragmented packets.

Message ID 20241105165910.197994-1-numans@ovn.org
State Superseded
Headers show
Series [ovs-dev,v1] Fix load balanced hairpin traffic for fragmented packets. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_ovn-kubernetes success github build: passed

Commit Message

Numan Siddique Nov. 5, 2024, 4:59 p.m. UTC
From: Numan Siddique <numans@ovn.org>

If we have a UDP load balancer - 10.0.0.10:80 = 10.0.0.3:8080, in order to
determine if the load balanced traffic needs to be hairpinned, the
vip - 10.0.0.10 and the vip port - 80 are stored in the registers before
the packet is load balanced using the below logical flow -

table=6 (ls_in_pre_stateful ), priority=120  ,
  match=(reg0[2] == 1 && ip4.dst == 10.0.0.10 && tcp.dst == 80),
  action=(reg1 = 10.0.0.10; reg2[0..15] = 80; ct_lb_mark;)

These registers are used in the later stages to check if the load balanced
packet needs to be hairpinned or not.

However, if the packet is fragmented we may not be able to match on the
L4 fields (tcp, udp or sctp dest port) and this breaks the hairpin
traffic.

This patch addressed this issue by making use of ct_nw_dst/ct_ip6_dst and
ct_tp_dst conntrack fields to determine the hairpin load balanced
traffic.

In order to not break hardware offload on certain smart nics, care is taken
to match on these fields only for fragmented packets.

Reported-at: https://issues.redhat.com/browse/FDP-905
Fixes: 1139b655c996 ("Don't blindly save original dst IP and Port to avoid megaflow unwildcarding.")
CC: Han Zhou <hzhou@ovn.org>
Signed-off-by: Numan Siddique <numans@ovn.org>
---
 controller/lflow.c           |   3 +
 controller/lflow.h           |   4 +
 controller/physical.c        |  37 +++++++++
 include/ovn/actions.h        |  14 +++-
 include/ovn/logical-fields.h |   4 +
 lib/actions.c                | 138 ++++++++++++++++++++++++++++----
 northd/northd.c              |  57 +++++++++++++-
 ovn-sb.xml                   |  27 +++++++
 tests/ovn-macros.at          |   3 +
 tests/ovn-northd.at          |  70 +++++++++-------
 tests/ovn.at                 |  56 ++++++++++++-
 tests/system-ovn-kmod.at     | 149 +++++++++++++++++++++++++++++++++++
 tests/test-ovn.c             |   3 +
 utilities/ovn-trace.c        |   6 ++
 14 files changed, 520 insertions(+), 51 deletions(-)
diff mbox series

Patch

diff --git a/controller/lflow.c b/controller/lflow.c
index 13c3a0d73e..76b4952fd4 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -886,6 +886,9 @@  add_matches_to_flow_table(const struct sbrec_logical_flow *lflow,
         .in_port_sec_ptable = OFTABLE_CHK_IN_PORT_SEC,
         .out_port_sec_ptable = OFTABLE_CHK_OUT_PORT_SEC,
         .mac_cache_use_table = OFTABLE_MAC_CACHE_USE,
+        .ct_nw_dst_load_table = OFTABLE_CT_ORIG_NW_DST_LOAD,
+        .ct_ip6_dst_load_table = OFTABLE_CT_ORIG_IP6_DST_LOAD,
+        .ct_tp_dst_load_table = OFTABLE_CT_ORIG_TP_DST_LOAD,
         .ctrl_meter_id = ctrl_meter_id,
         .common_nat_ct_zone = get_common_nat_zone(ldp),
     };
diff --git a/controller/lflow.h b/controller/lflow.h
index e95a016501..58a12ee0fe 100644
--- a/controller/lflow.h
+++ b/controller/lflow.h
@@ -95,6 +95,10 @@  struct uuid;
 #define OFTABLE_CHK_LB_AFFINITY          78
 #define OFTABLE_MAC_CACHE_USE            79
 #define OFTABLE_CT_ZONE_LOOKUP           80
+#define OFTABLE_CT_ORIG_NW_DST_LOAD      81
+#define OFTABLE_CT_ORIG_IP6_DST_LOAD     82
+#define OFTABLE_CT_ORIG_TP_DST_LOAD      83
+
 
 struct lflow_ctx_in {
     struct ovsdb_idl_index *sbrec_multicast_group_by_name_datapath;
diff --git a/controller/physical.c b/controller/physical.c
index 2aaa16cbd0..855936ba4e 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -2770,5 +2770,42 @@  physical_run(struct physical_ctx *p_ctx,
      */
     add_default_drop_flow(p_ctx, OFTABLE_LOG_TO_PHY, flow_table);
 
+    /* Table 81, 82 and 83
+     * Match on ct.trk and ct.dnat and store the ct_nw_dst, ct_ip6_dst and
+     * ct_tp_dst in the registers. */
+    uint32_t ct_state = OVS_CS_F_TRACKED | OVS_CS_F_DST_NAT;
+    match_init_catchall(&match);
+    ofpbuf_clear(&ofpacts);
+
+    /* Add the flow:
+     * match = (ct.trk && ct.dnat), action = (reg8 = ct_tp_dst)
+     * table = 83
+     */
+    match_set_ct_state_masked(&match, ct_state, ct_state);
+    put_move(MFF_CT_TP_DST, 0,  MFF_LOG_CT_ORIG_TP_DST_PORT, 0, 16, &ofpacts);
+    ofctrl_add_flow(flow_table, OFTABLE_CT_ORIG_TP_DST_LOAD, 100, 0, &match,
+                    &ofpacts, hc_uuid);
+
+    /* Add the flow:
+     * match = (ct.trk && ct.dnat && ip4), action = (reg4 = ct_nw_dst)
+     * table = 81
+     */
+    ofpbuf_clear(&ofpacts);
+    match_set_dl_type(&match, htons(ETH_TYPE_IP));
+    put_move(MFF_CT_NW_DST, 0,  MFF_LOG_CT_ORIG_NW_DST_ADDR, 0, 32, &ofpacts);
+    ofctrl_add_flow(flow_table, OFTABLE_CT_ORIG_NW_DST_LOAD, 100, 0, &match,
+                    &ofpacts, hc_uuid);
+
+    /* Add the flow:
+     * match = (ct.trk && ct.dnat && ip6), action = (xxreg0 = ct_ip6_dst)
+     * table = 82
+     */
+    ofpbuf_clear(&ofpacts);
+    match_set_dl_type(&match, htons(ETH_TYPE_IPV6));
+    put_move(MFF_CT_IPV6_DST, 0,  MFF_LOG_CT_ORIG_IP6_DST_ADDR, 0,
+             128, &ofpacts);
+    ofctrl_add_flow(flow_table, OFTABLE_CT_ORIG_IP6_DST_LOAD, 100, 0, &match,
+                    &ofpacts, hc_uuid);
+
     ofpbuf_uninit(&ofpacts);
 }
diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 63a12a8821..91a6139b00 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -131,6 +131,9 @@  struct collector_set_ids;
     OVNACT(CHK_LB_AFF,        ovnact_result)          \
     OVNACT(SAMPLE,            ovnact_sample)          \
     OVNACT(MAC_CACHE_USE,     ovnact_null)            \
+    OVNACT(CT_ORIG_NW_DST,    ovnact_result)          \
+    OVNACT(CT_ORIG_IP6_DST,   ovnact_result)          \
+    OVNACT(CT_ORIG_TP_DST,    ovnact_result)          \
 
 /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
 enum OVS_PACKED_ENUM ovnact_type {
@@ -416,10 +419,11 @@  struct ovnact_set_queue {
     uint16_t queue_id;
 };
 
-/* OVNACT_DNS_LOOKUP, OVNACT_CHK_LB_HAIRPIN, OVNACT_CHK_LB_HAIRPIN_REPLY. */
+/* OVNACT_DNS_LOOKUP, OVNACT_CHK_LB_HAIRPIN, OVNACT_CHK_LB_HAIRPIN_REPLY,
+ * OVNACT_CT_ORIG_NW_DST, CT_ORIG_IP6_DST, CT_ORIG_TP_DST */
 struct ovnact_result {
     struct ovnact ovnact;
-    struct expr_field dst;      /* 1-bit destination field. */
+    struct expr_field dst;      /* destination field. */
 };
 
 /* OVNACT_LOG. */
@@ -935,6 +939,12 @@  struct ovnact_encode_params {
                                     this determines which CT zone to use */
     uint32_t mac_cache_use_table; /* OpenFlow table for 'mac_cache_use'
                                    * to resubmit. */
+    uint32_t ct_nw_dst_load_table; /* OpenFlow table for 'ct_nw_dst'
+                                   *  to resubmit. */
+    uint32_t ct_ip6_dst_load_table; /* OpenFlow table for 'ct_ip6_dst'
+                                   *  to resubmit. */
+    uint32_t ct_tp_dst_load_table; /* OpenFlow table for 'ct_tp_dst'
+                                   *  to resubmit. */
 };
 
 void ovnacts_encode(const struct ovnact[], size_t ovnacts_len,
diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
index d6c4a9b6b3..d563e044cb 100644
--- a/include/ovn/logical-fields.h
+++ b/include/ovn/logical-fields.h
@@ -60,6 +60,10 @@  enum ovn_controller_event {
 #define MFF_LOG_LB_AFF_MATCH_LR_IP6_ADDR    MFF_XXREG1
 #define MFF_LOG_LB_AFF_MATCH_PORT           MFF_REG8
 
+#define MFF_LOG_CT_ORIG_NW_DST_ADDR         MFF_REG4
+#define MFF_LOG_CT_ORIG_IP6_DST_ADDR        MFF_XXREG0
+#define MFF_LOG_CT_ORIG_TP_DST_PORT         MFF_REG8
+
 void ovn_init_symtab(struct shash *symtab);
 
 /* MFF_LOG_FLAGS_REG bit assignments */
diff --git a/lib/actions.c b/lib/actions.c
index c5bde996b7..d5fc30b27a 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -3280,9 +3280,10 @@  ovnact_set_queue_free(struct ovnact_set_queue *a OVS_UNUSED)
 }
 
 static void
-parse_ovnact_result(struct action_context *ctx, const char *name,
-                    const char *prereq, const struct expr_field *dst,
-                    struct ovnact_result *res)
+parse_ovnact_result__(struct action_context *ctx, const char *name,
+                      const char *prereq, const struct expr_field *dst,
+                      struct ovnact_result *res,
+                      int n_bits)
 {
     lexer_get(ctx->lexer); /* Skip action name. */
     lexer_get(ctx->lexer); /* Skip '('. */
@@ -3290,8 +3291,8 @@  parse_ovnact_result(struct action_context *ctx, const char *name,
         lexer_error(ctx->lexer, "%s doesn't take any parameters", name);
         return;
     }
-    /* Validate that the destination is a 1-bit, modifiable field. */
-    char *error = expr_type_check(dst, 1, true, ctx->scope);
+    /* Validate that the destination is n_bits, modifiable field. */
+    char *error = expr_type_check(dst, n_bits, true, ctx->scope);
     if (error) {
         lexer_error(ctx->lexer, "%s", error);
         free(error);
@@ -3304,6 +3305,14 @@  parse_ovnact_result(struct action_context *ctx, const char *name,
     }
 }
 
+static void
+parse_ovnact_result(struct action_context *ctx, const char *name,
+                    const char *prereq, const struct expr_field *dst,
+                    struct ovnact_result *res)
+{
+    parse_ovnact_result__(ctx, name, prereq, dst, res, 1);
+}
+
 static void
 parse_dns_lookup(struct action_context *ctx, const struct expr_field *dst,
                  struct ovnact_result *dl)
@@ -4299,22 +4308,40 @@  format_CHK_LB_HAIRPIN_REPLY(const struct ovnact_result *res, struct ds *s)
     ds_put_cstr(s, " = chk_lb_hairpin_reply();");
 }
 
+static void
+encode_result_action___(const struct ovnact_result *res,
+                        uint8_t resubmit_table,
+                        enum mf_field_id dst,
+                        int ofs, int n_bits,
+                        struct ofpbuf *ofpacts)
+{
+    ovs_assert(n_bits <= 128);
+
+    struct mf_subfield res_dst = expr_resolve_field(&res->dst);
+    ovs_assert(res_dst.field);
+
+    put_load(0, dst, ofs, n_bits < 64 ? n_bits : 64, ofpacts);
+    if (n_bits > 64) {
+        put_load(0, dst, ofs + 64, n_bits - 64, ofpacts);
+    }
+
+    emit_resubmit(ofpacts, resubmit_table);
+
+    struct ofpact_reg_move *orm = ofpact_put_REG_MOVE(ofpacts);
+    orm->dst = res_dst;
+    orm->src.field = mf_from_id(dst);
+    orm->src.ofs = ofs;
+    orm->src.n_bits = n_bits;
+}
+
 static void
 encode_result_action__(const struct ovnact_result *res,
                        uint8_t resubmit_table,
                        int log_flags_result_bit,
                        struct ofpbuf *ofpacts)
 {
-    struct mf_subfield dst = expr_resolve_field(&res->dst);
-    ovs_assert(dst.field);
-    put_load(0, MFF_LOG_FLAGS, log_flags_result_bit, 1, ofpacts);
-    emit_resubmit(ofpacts, resubmit_table);
-
-    struct ofpact_reg_move *orm = ofpact_put_REG_MOVE(ofpacts);
-    orm->dst = dst;
-    orm->src.field = mf_from_id(MFF_LOG_FLAGS);
-    orm->src.ofs = log_flags_result_bit;
-    orm->src.n_bits = 1;
+    encode_result_action___(res, resubmit_table, MFF_LOG_FLAGS,
+                            log_flags_result_bit, 1, ofpacts);
 }
 
 static void
@@ -5435,6 +5462,75 @@  encode_MAC_CACHE_USE(const struct ovnact_null *null OVS_UNUSED,
     emit_resubmit(ofpacts, ep->mac_cache_use_table);
 }
 
+static void
+encode_CT_ORIG_NW_DST(const struct ovnact_result *res,
+                      const struct ovnact_encode_params *ep,
+                      struct ofpbuf *ofpacts)
+{
+    encode_result_action___(res, ep->ct_nw_dst_load_table,
+                            MFF_LOG_CT_ORIG_NW_DST_ADDR, 0, 32, ofpacts);
+}
+
+static void
+parse_CT_ORIG_NW_DST(struct action_context *ctx, const struct expr_field *dst,
+                     struct ovnact_result *res)
+{
+    parse_ovnact_result__(ctx, "ct_nw_dst", NULL, dst, res, 32);
+}
+
+static void
+format_CT_ORIG_NW_DST(const struct ovnact_result *res, struct ds *s)
+{
+    expr_field_format(&res->dst, s);
+    ds_put_cstr(s, " = ct_nw_dst();");
+}
+
+static void
+encode_CT_ORIG_IP6_DST(const struct ovnact_result *res,
+                       const struct ovnact_encode_params *ep,
+                       struct ofpbuf *ofpacts)
+{
+    encode_result_action___(res, ep->ct_ip6_dst_load_table,
+                            MFF_LOG_CT_ORIG_IP6_DST_ADDR, 0, 128, ofpacts);
+}
+
+static void
+parse_CT_ORIG_IP6_DST(struct action_context *ctx, const struct expr_field *dst,
+                     struct ovnact_result *res)
+{
+    parse_ovnact_result__(ctx, "ct_ip6_dst", NULL, dst, res, 128);
+}
+
+static void
+format_CT_ORIG_IP6_DST(const struct ovnact_result *res, struct ds *s)
+{
+    expr_field_format(&res->dst, s);
+    ds_put_cstr(s, " = ct_ip6_dst();");
+}
+
+static void
+encode_CT_ORIG_TP_DST(const struct ovnact_result *res,
+                      const struct ovnact_encode_params *ep OVS_UNUSED,
+                      struct ofpbuf *ofpacts)
+{
+    encode_result_action___(res, ep->ct_tp_dst_load_table,
+                            MFF_LOG_CT_ORIG_TP_DST_PORT, 0, 16, ofpacts);
+}
+
+static void
+parse_CT_ORIG_TP_DST(struct action_context *ctx, const struct expr_field *dst,
+                     struct ovnact_result *res)
+{
+    parse_ovnact_result__(ctx, "ct_tp_dst", NULL, dst, res, 16);
+}
+
+static void
+format_CT_ORIG_TP_DST(const struct ovnact_result *res, struct ds *s)
+{
+    expr_field_format(&res->dst, s);
+    ds_put_cstr(s, " = ct_tp_dst();");
+}
+
 /* Parses an assignment or exchange or put_dhcp_opts action. */
 static void
 parse_set_action(struct action_context *ctx)
@@ -5529,6 +5625,18 @@  parse_set_action(struct action_context *ctx)
         } else if (lexer_match_id(ctx->lexer, "dhcp_relay_resp_chk")) {
             parse_dhcp_relay_chk(
                 ctx, &lhs, ovnact_put_DHCPV4_RELAY_RESP_CHK(ctx->ovnacts));
+        } else if (!strcmp(ctx->lexer->token.s, "ct_nw_dst") &&
+                   lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
+            parse_CT_ORIG_NW_DST(ctx, &lhs,
+                                 ovnact_put_CT_ORIG_NW_DST(ctx->ovnacts));
+        } else if (!strcmp(ctx->lexer->token.s, "ct_ip6_dst") &&
+                   lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
+            parse_CT_ORIG_IP6_DST(ctx, &lhs,
+                                  ovnact_put_CT_ORIG_IP6_DST(ctx->ovnacts));
+        } else if (!strcmp(ctx->lexer->token.s, "ct_tp_dst") &&
+                   lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
+            parse_CT_ORIG_TP_DST(ctx, &lhs,
+                                 ovnact_put_CT_ORIG_TP_DST(ctx->ovnacts));
         } else {
             parse_assignment_action(ctx, false, &lhs);
         }
diff --git a/northd/northd.c b/northd/northd.c
index 3037ce0b55..985283a98e 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -124,6 +124,7 @@  static bool vxlan_mode;
 #define REGBIT_ACL_STATELESS      "reg0[16]"
 #define REGBIT_ACL_HINT_ALLOW_REL "reg0[17]"
 #define REGBIT_FROM_ROUTER_PORT   "reg0[18]"
+#define REGBIT_IP_FRAG            "reg0[19]"
 
 #define REG_ORIG_DIP_IPV4         "reg1"
 #define REG_ORIG_DIP_IPV6         "xxreg1"
@@ -6398,6 +6399,13 @@  build_pre_stateful(struct ovn_datapath *od,
 
     /* Note: priority-120 flows are added in build_lb_rules_pre_stateful(). */
 
+    /* If the packet is fragmented, set the REGBIT_IP_FRAG reg bit to 1
+     * as ip.is_frag will not be preserved after conntrack recirculation. */
+    ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_STATEFUL, 115,
+                  REGBIT_CONNTRACK_NAT" == 1 && ip.is_frag",
+                  REGBIT_IP_FRAG" = 1; ct_lb_mark;",
+                  lflow_ref);
+
     ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_STATEFUL, 110,
                   REGBIT_CONNTRACK_NAT" == 1", "ct_lb_mark;",
                   lflow_ref);
@@ -8240,13 +8248,32 @@  build_lb_rules(struct lflow_table *lflows, struct ovn_lb_datapaths *lb_dps,
         struct ovn_lb_vip *lb_vip = &lb->vips[i];
         struct ovn_northd_lb_vip *lb_vip_nb = &lb->vips_nb[i];
         const char *ip_match = NULL;
+
+        ds_clear(action);
+
+        /* Store the original destination IP to be used when generating
+         * hairpin flows.
+         * If the packet is fragmented, then the flow which saves the
+         * original destination IP (and port) in the "ls_in_pre_stateful"
+         * stage will not be hit.
+         */
         if (lb_vip->address_family == AF_INET) {
             ip_match = "ip4";
+            ds_put_format(action, REG_ORIG_DIP_IPV4 " = %s; ",
+                          lb_vip->vip_str);
         } else {
             ip_match = "ip6";
+            ds_put_format(action, REG_ORIG_DIP_IPV6 " = %s; ",
+                          lb_vip->vip_str);
         }
 
-        ds_clear(action);
+        if (lb_vip->port_str) {
+            /* Store the original destination port to be used when generating
+             * hairpin flows.
+             */
+            ds_put_format(action, REG_ORIG_TP_DPORT " = %s; ",
+                          lb_vip->port_str);
+        }
         ds_clear(match);
 
         /* New connections in Ingress table. */
@@ -8378,8 +8405,32 @@  build_lb_hairpin(const struct ls_stateful_record *ls_stateful_rec,
                   lflow_ref);
 
     if (ls_stateful_rec->has_lb_vip) {
-        /* Check if the packet needs to be hairpinned.
-         * Set REGBIT_HAIRPIN in the original direction and
+        /* Check if the packet needs to be hairpinned. */
+
+        /* In order to check if the fragmented packets needs to be
+         * hairpinned we need to save the ct tuple original IPv4/v6
+         * destination and L4 destination port in the registers after
+         * the conntrack recirculation.
+         *
+         * Note: We are assuming that sending the packets to conntrack
+         * will reassemble the packet and L4 fields will be available.
+         * It is a risky assumption as ovs-vswitchd doesn't guarantee it
+         * and userspace datapath doesn't reassemble the fragmented packets
+         * after conntrack.  It is the kernel datapath conntrack behavior.
+         * We need to find a better way to handle the fragmented packets.
+         * */
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_LB, 110,
+                      "ct.trk && !ct.rpl && "REGBIT_IP_FRAG" == 1 && ip4",
+                      REG_ORIG_DIP_IPV4 " = ct_nw_dst(); "
+                      REG_ORIG_TP_DPORT " = ct_tp_dst(); next;",
+                      lflow_ref);
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_LB, 110,
+                      "ct.trk && !ct.rpl && "REGBIT_IP_FRAG" == 1 && ip6",
+                      REG_ORIG_DIP_IPV6 " = ct_ip6_dst(); "
+                      REG_ORIG_TP_DPORT " = ct_tp_dst(); next;",
+                      lflow_ref);
+
+        /* Set REGBIT_HAIRPIN in the original direction and
          * REGBIT_HAIRPIN_REPLY in the reply direction.
          */
         ovn_lflow_add_with_hint(
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 479d3e2851..ea4adc1c34 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -2766,6 +2766,33 @@  tcp.flags = RST;
           </p>
         </dd>
 
+        <dt><code><var>R</var> = ct_nw_dst();</code></dt>
+        <dd>
+          <p>
+            This action checks if the packet is tracked and stores the
+            conntrack original destination IPv4 address in the register
+            <var>R</var> of 32-bit size.
+          </p>
+        </dd>
+
+        <dt><code><var>R</var> = ct_ip6_dst();</code></dt>
+        <dd>
+          <p>
+            This action checks if the packet is tracked and stores the
+            conntrack original destination IPv6 address in the register
+            <var>R</var> of 128-bit size.
+          </p>
+        </dd>
+
+        <dt><code><var>R</var> = ct_tp_dst();</code></dt>
+        <dd>
+          <p>
+            This action checks if the packet is tracked and stores the
+            conntrack original L4 destination port in the register
+            <var>R</var> of 16-bit size.
+          </p>
+        </dd>
+
         <dt><code>sample(probability=<var>packets</var>, ...)</code></dt>
         <dd>
           <p>
diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
index 77d1515f6f..a88556aa99 100644
--- a/tests/ovn-macros.at
+++ b/tests/ovn-macros.at
@@ -1217,5 +1217,8 @@  m4_define([OFTABLE_ECMP_NH], [77])
 m4_define([OFTABLE_CHK_LB_AFFINITY], [78])
 m4_define([OFTABLE_MAC_CACHE_USE], [79])
 m4_define([OFTABLE_CT_ZONE_LOOKUP], [80])
+m4_define([OFTABLE_CT_ORIG_NW_DST_LOAD], [81])
+m4_define([OFTABLE_CT_ORIG_IP6_DST_LOAD], [82])
+m4_define([OFTABLE_CT_ORIG_TP_DST_LOAD], [83])
 
 m4_define([OFTABLE_SAVE_INPORT_HEX], [m4_eval(OFTABLE_SAVE_INPORT, 16)])
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index d1f7f105c0..e1386323a7 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -1415,7 +1415,7 @@  check ovn-nbctl --wait=sb ls-lb-add sw0 lb1
 AT_CAPTURE_FILE([sbflows])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows | grep 'priority=120.*backends' | ovn_strip_lflows], 0, [dnl
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 
 # disabled LSPs should not be a backend of Load Balancer
@@ -1424,7 +1424,7 @@  check ovn-nbctl lsp-set-enabled sw0-p1 disabled
 AT_CAPTURE_FILE([sbflows])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows | grep 'priority=120.*backends' | ovn_strip_lflows], 0, [dnl
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=20.0.0.3:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=20.0.0.3:80);)
 ])
 wait_row_count Service_Monitor 1
 
@@ -1433,7 +1433,7 @@  check ovn-nbctl lsp-set-enabled sw0-p1 enabled
 AT_CAPTURE_FILE([sbflows])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows | grep 'priority=120.*backends' | ovn_strip_lflows], 0, [dnl
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 wait_row_count Service_Monitor 2
 
@@ -1444,7 +1444,7 @@  wait_row_count Service_Monitor 0
 AT_CAPTURE_FILE([sbflows2])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows2 | grep 'priority=120.*backends' | ovn_strip_lflows], [0],
-[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
+[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 
 AS_BOX([Create the Load_Balancer_Health_Check again.])
@@ -1456,7 +1456,7 @@  check ovn-nbctl --wait=sb sync
 
 ovn-sbctl dump-flows sw0 | grep backends | grep priority=120 > lflows.txt
 AT_CHECK([cat lflows.txt | ovn_strip_lflows], [0], [dnl
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 
 AS_BOX([Get the uuid of both the service_monitor])
@@ -1466,7 +1466,7 @@  sm_sw1_p1=$(fetch_column Service_Monitor _uuid logical_port=sw1-p1)
 AT_CAPTURE_FILE([sbflows3])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows 3 | grep 'priority=120.*backends' | ovn_strip_lflows], [0],
-[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
+[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 
 AS_BOX([Set the service monitor for sw1-p1 to offline])
@@ -1477,7 +1477,7 @@  check ovn-nbctl --wait=sb sync
 AT_CAPTURE_FILE([sbflows4])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows4 | grep 'priority=120.*backends' | ovn_strip_lflows], [0],
-[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80);)
+[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80);)
 ])
 
 AS_BOX([Set the service monitor for sw0-p1 to offline])
@@ -1506,7 +1506,7 @@  check ovn-nbctl --wait=sb sync
 AT_CAPTURE_FILE([sbflows7])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows7 | grep backends | grep priority=120 | ovn_strip_lflows], 0,
-[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
+[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 
 AS_BOX([Set the service monitor for sw1-p1 to error])
@@ -1517,7 +1517,7 @@  check ovn-nbctl --wait=sb sync
 ovn-sbctl dump-flows sw0 | grep "ip4.dst == 10.0.0.10 && tcp.dst == 80" \
 | grep priority=120 > lflows.txt
 AT_CHECK([cat lflows.txt | grep ls_in_lb | ovn_strip_lflows], [0], [dnl
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80);)
 ])
 
 AS_BOX([Add one more vip to lb1])
@@ -1543,8 +1543,8 @@  AT_CAPTURE_FILE([sbflows9])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows9 | grep backends | grep priority=120 | ovn_strip_lflows],
   0,
-[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80);)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.40 && tcp.dst == 1000), action=(ct_lb_mark(backends=10.0.0.3:1000);)
+[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.40 && tcp.dst == 1000), action=(reg1 = 10.0.0.40; reg2[[0..15]] = 1000; ct_lb_mark(backends=10.0.0.3:1000);)
 ])
 
 AS_BOX([Set the service monitor for sw1-p1 to online])
@@ -1557,8 +1557,8 @@  AT_CAPTURE_FILE([sbflows10])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows10 | grep backends | grep priority=120 | ovn_strip_lflows],
   0,
-[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.40 && tcp.dst == 1000), action=(ct_lb_mark(backends=10.0.0.3:1000,20.0.0.3:80);)
+[  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.40 && tcp.dst == 1000), action=(reg1 = 10.0.0.40; reg2[[0..15]] = 1000; ct_lb_mark(backends=10.0.0.3:1000,20.0.0.3:80);)
 ])
 
 AS_BOX([Associate lb1 to sw1])
@@ -1567,8 +1567,8 @@  AT_CAPTURE_FILE([sbflows11])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw1 | tee sbflows11 | grep backends | grep priority=120 | ovn_strip_lflows],
   0, [dnl
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.40 && tcp.dst == 1000), action=(ct_lb_mark(backends=10.0.0.3:1000,20.0.0.3:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.40 && tcp.dst == 1000), action=(reg1 = 10.0.0.40; reg2[[0..15]] = 1000; ct_lb_mark(backends=10.0.0.3:1000,20.0.0.3:80);)
 ])
 
 AS_BOX([Now create lb2 same as lb1 but udp protocol.])
@@ -4653,14 +4653,17 @@  check_stateful_flows() {
   table=??(ls_in_pre_stateful ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_stateful ), priority=100  , match=(reg0[[0]] == 1), action=(ct_next;)
   table=??(ls_in_pre_stateful ), priority=110  , match=(reg0[[2]] == 1), action=(ct_lb_mark;)
+  table=??(ls_in_pre_stateful ), priority=115  , match=(reg0[[2]] == 1 && ip.is_frag), action=(reg0[[19]] = 1; ct_lb_mark;)
   table=??(ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark;)
   table=??(ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4.dst == 10.0.0.20 && tcp.dst == 80), action=(reg1 = 10.0.0.20; reg2[[0..15]] = 80; ct_lb_mark;)
 ])
 
     AT_CHECK([grep "ls_in_lb " sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_lb           ), priority=0    , match=(1), action=(next;)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.4:8080);)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.20 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.40:8080);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip4), action=(reg1 = ct_nw_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip6), action=(xxreg1 = ct_ip6_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.4:8080);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.20 && tcp.dst == 80), action=(reg1 = 10.0.0.20; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.40:8080);)
 ])
 
     AT_CHECK([grep "ls_in_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
@@ -4724,6 +4727,7 @@  AT_CHECK([grep "ls_in_pre_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_pre_stateful ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_stateful ), priority=100  , match=(reg0[[0]] == 1), action=(ct_next;)
   table=??(ls_in_pre_stateful ), priority=110  , match=(reg0[[2]] == 1), action=(ct_lb_mark;)
+  table=??(ls_in_pre_stateful ), priority=115  , match=(reg0[[2]] == 1 && ip.is_frag), action=(reg0[[19]] = 1; ct_lb_mark;)
 ])
 
 AT_CHECK([grep "ls_in_lb " sw0flows | ovn_strip_lflows], [0], [dnl
@@ -4764,6 +4768,8 @@  check ovn-nbctl --wait=sb ls-lb-add sw0 lb1
 AT_CHECK([ovn-sbctl dump-flows sw0 | grep "ls_in_lb " | ovn_strip_lflows ], [0], [dnl
   table=??(ls_in_lb           ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 10.0.0.20), action=(drop;)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip4), action=(reg1 = ct_nw_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip6), action=(xxreg1 = ct_ip6_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
 ])
 
 AT_CLEANUP
@@ -7763,7 +7769,9 @@  AT_CHECK([grep -e "ls_in_acl.*eval" -e "ls_in_acl_hint" lsflows | ovn_strip_lflo
 
 AT_CHECK([grep -e "ls_in_lb " lsflows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_lb           ), priority=0    , match=(1), action=(next;)
-  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 10.0.0.2), action=(ct_lb_mark(backends=10.0.0.10);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 10.0.0.2), action=(reg1 = 10.0.0.2; ct_lb_mark(backends=10.0.0.10);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip4), action=(reg1 = ct_nw_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip6), action=(xxreg1 = ct_ip6_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
 ])
 
 AT_CHECK([grep -e "ls_in_stateful" lsflows | ovn_strip_lflows], [0], [dnl
@@ -7818,7 +7826,9 @@  AT_CHECK([grep -e "ls_in_acl.*eval" -e "ls_in_acl_hint" lsflows | ovn_strip_lflo
 
 AT_CHECK([grep -e "ls_in_lb " lsflows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_lb           ), priority=0    , match=(1), action=(next;)
-  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 10.0.0.2), action=(ct_lb_mark(backends=10.0.0.10);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 10.0.0.2), action=(reg1 = 10.0.0.2; ct_lb_mark(backends=10.0.0.10);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip4), action=(reg1 = ct_nw_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip6), action=(xxreg1 = ct_ip6_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
 ])
 
 AT_CHECK([grep -e "ls_in_stateful" lsflows | ovn_strip_lflows], [0], [dnl
@@ -7873,7 +7883,9 @@  AT_CHECK([grep -e "ls_in_acl.*eval" -e "ls_in_acl_hint" lsflows | ovn_strip_lflo
 
 AT_CHECK([grep -e "ls_in_lb " lsflows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_lb           ), priority=0    , match=(1), action=(next;)
-  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 10.0.0.2), action=(ct_lb_mark(backends=10.0.0.10);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 10.0.0.2), action=(reg1 = 10.0.0.2; ct_lb_mark(backends=10.0.0.10);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip4), action=(reg1 = ct_nw_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip6), action=(xxreg1 = ct_ip6_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
 ])
 
 AT_CHECK([grep -e "ls_in_stateful" lsflows | ovn_strip_lflows], [0], [dnl
@@ -9258,13 +9270,13 @@  AT_CAPTURE_FILE([S1flows])
 
 AT_CHECK([grep "ls_in_lb " S0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_lb           ), priority=0    , match=(1), action=(next;)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.2:80);)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.11 && tcp.dst == 8080), action=(ct_lb_mark(backends=10.0.0.2:8080);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.10 && tcp.dst == 80), action=(reg1 = 172.16.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.2:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.11 && tcp.dst == 8080), action=(reg1 = 172.16.0.11; reg2[[0..15]] = 8080; ct_lb_mark(backends=10.0.0.2:8080);)
 ])
 AT_CHECK([grep "ls_in_lb " S1flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_lb           ), priority=0    , match=(1), action=(next;)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.2:80);)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.11 && tcp.dst == 8080), action=(ct_lb_mark(backends=10.0.0.2:8080);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.10 && tcp.dst == 80), action=(reg1 = 172.16.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.2:80);)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.11 && tcp.dst == 8080), action=(reg1 = 172.16.0.11; reg2[[0..15]] = 8080; ct_lb_mark(backends=10.0.0.2:8080);)
 ])
 
 ovn-sbctl get datapath S0 _uuid > dp_uuids
@@ -9394,7 +9406,9 @@  AT_CHECK([grep "ls_in_lb_aff_check" S0flows | ovn_strip_lflows], [0], [dnl
 ])
 AT_CHECK([grep "ls_in_lb " S0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_lb           ), priority=0    , match=(1), action=(next;)
-  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip4), action=(reg1 = ct_nw_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.trk && !ct.rpl && reg0[[19]] == 1 && ip6), action=(xxreg1 = ct_ip6_dst(); reg2[[0..15]] = ct_tp_dst(); next;)
+  table=??(ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 172.16.0.10 && tcp.dst == 80), action=(reg1 = 172.16.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80);)
   table=??(ls_in_lb           ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg1 = 172.16.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.2:80);)
   table=??(ls_in_lb           ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 20.0.0.2 && reg8[[0..15]] == 80), action=(reg1 = 172.16.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=20.0.0.2:80);)
 ])
@@ -13976,7 +13990,7 @@  AT_CHECK([grep "ls_in_pre_stateful" s1flows | ovn_strip_lflows | grep "30.0.0.1"
   table=??(ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4.dst == 30.0.0.1), action=(reg1 = 30.0.0.1; ct_lb_mark;)
 ])
 AT_CHECK([grep "ls_in_lb" s1flows | ovn_strip_lflows | grep "30.0.0.1"], [0], [dnl
-  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 30.0.0.1), action=(ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 30.0.0.1), action=(reg1 = 30.0.0.1; ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
 ])
 
 # Associate load balancer to lr1 with DGP
@@ -14090,7 +14104,7 @@  AT_CHECK([grep "ls_in_pre_stateful" s1flows | ovn_strip_lflows | grep "2001:db8:
   table=??(ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6.dst == 2001:db8:cccc::1), action=(xxreg1 = 2001:db8:cccc::1; ct_lb_mark;)
 ])
 AT_CHECK([grep "ls_in_lb" s1flows | ovn_strip_lflows | grep "2001:db8:cccc::1"], [0], [dnl
-  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip6.dst == 2001:db8:cccc::1), action=(ct_lb_mark(backends=2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip6.dst == 2001:db8:cccc::1), action=(xxreg1 = 2001:db8:cccc::1; ct_lb_mark(backends=2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101);)
 ])
 
 # Associate load balancer to lr1 with DGP
@@ -14201,7 +14215,7 @@  AT_CHECK([grep "ls_in_pre_stateful" s1flows | ovn_strip_lflows | grep "30.0.0.1"
   table=??(ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4.dst == 30.0.0.1), action=(reg1 = 30.0.0.1; ct_lb_mark;)
 ])
 AT_CHECK([grep "ls_in_lb" s1flows | ovn_strip_lflows | grep "30.0.0.1"], [0], [dnl
-  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 30.0.0.1), action=(ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
+  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 30.0.0.1), action=(reg1 = 30.0.0.1; ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
 ])
 
 # Associate load balancer to lr1 with DGP
diff --git a/tests/ovn.at b/tests/ovn.at
index 10cd7a79b9..af3133f868 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -2418,6 +2418,54 @@  sample(probability=10, obs_point=ct_label);
 mac_cache_use;
     encodes as resubmit(,OFTABLE_MAC_CACHE_USE)
 
+# ct_nw_dst()
+reg1 = ct_nw_dst();
+    encodes as set_field:0->reg4,resubmit(,OFTABLE_CT_ORIG_NW_DST_LOAD),move:NXM_NX_REG4[[]]->NXM_NX_XXREG0[[64..95]]
+
+xreg1[[3..34]] = ct_nw_dst();
+    encodes as set_field:0->reg4,resubmit(,OFTABLE_CT_ORIG_NW_DST_LOAD),move:NXM_NX_REG4[[]]->NXM_NX_XXREG0[[3..34]]
+
+reg1[[3..34]] = ct_nw_dst();
+    Cannot select bits 3 to 34 of 32-bit field reg1.
+
+reg1[[3..35]] = ct_nw_dst();
+    Cannot select bits 3 to 35 of 32-bit field reg1.
+
+reg1[[1]] = ct_nw_dst();
+    Cannot use 1-bit field reg1[[1..1]] where 32-bit field is required.
+
+ct_nw_dst;
+    Syntax error at `ct_nw_dst' expecting action.
+
+ct_nw_dst();
+    Syntax error at `ct_nw_dst' expecting action.
+
+# ct_ip6_dst()
+xxreg1 = ct_ip6_dst();
+    encodes as set_field:0/0xffffffffffffffff->xxreg0,set_field:0/0xffffffffffffffff0000000000000000->xxreg0,resubmit(,OFTABLE_CT_ORIG_IP6_DST_LOAD),move:NXM_NX_XXREG0[[]]->NXM_NX_XXREG1[[]]
+
+reg1 = ct_ip6_dst();
+    Cannot use 32-bit field reg1[[0..31]] where 128-bit field is required.
+
+ct_ip6_dst;
+    Syntax error at `ct_ip6_dst' expecting action.
+
+ct_ip6_dst();
+    Syntax error at `ct_ip6_dst' expecting action.
+
+# ct_tp_dst()
+reg1[[0..15]] = ct_tp_dst();
+    encodes as set_field:0/0xffff->reg8,resubmit(,OFTABLE_CT_ORIG_TP_DST_LOAD),move:NXM_NX_REG8[[0..15]]->NXM_NX_XXREG0[[64..79]]
+
+reg1 = ct_tp_dst();
+    Cannot use 32-bit field reg1[[0..31]] where 16-bit field is required.
+
+ct_tp_dst;
+    Syntax error at `ct_tp_dst' expecting action.
+
+ct_tp_dst();
+    Syntax error at `ct_tp_dst' expecting action.
+
 # Miscellaneous negative tests.
 ;
     Syntax error at `;'.
@@ -25632,7 +25680,7 @@  OVS_WAIT_FOR_OUTPUT(
    ovn-sbctl dump-flows sw0 | grep ct_lb_mark | grep priority=120 | sed 's/table=..//'], 0,
   [dnl
   (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark;)
-  (ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80; hash_fields="ip_dst,ip_src,tcp_dst,tcp_src");)
+  (ls_in_lb           ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80; hash_fields="ip_dst,ip_src,tcp_dst,tcp_src");)
 ])
 
 AT_CAPTURE_FILE([sbflows2])
@@ -25831,7 +25879,7 @@  OVS_WAIT_FOR_OUTPUT(
    ovn-sbctl dump-flows sw0 | grep ct_lb_mark | grep priority=120 | sed 's/table=..//'], 0,
   [dnl
   (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6.dst == 2001::a && tcp.dst == 80), action=(xxreg1 = 2001::a; reg2[[0..15]] = 80; ct_lb_mark;)
-  (ls_in_lb           ), priority=120  , match=(ct.new && ip6.dst == 2001::a && tcp.dst == 80), action=(ct_lb_mark(backends=[[2001::3]]:80,[[2002::3]]:80; hash_fields="ip_dst,ip_src,tcp_dst,tcp_src");)
+  (ls_in_lb           ), priority=120  , match=(ct.new && ip6.dst == 2001::a && tcp.dst == 80), action=(xxreg1 = 2001::a; reg2[[0..15]] = 80; ct_lb_mark(backends=[[2001::3]]:80,[[2002::3]]:80; hash_fields="ip_dst,ip_src,tcp_dst,tcp_src");)
 ])
 
 AT_CAPTURE_FILE([sbflows2])
@@ -35663,7 +35711,9 @@  check_default_flows() {
     for table in $(grep -oP "table=\K\d*, " oflows | tr -d ',' | sort -n | uniq); do
         # Tables 68 and 70 are part of the chk_lb_hairpin and ct_snat_to_vip actions
         # respectively and it's OK if they don't have a default action.
-        if test ${table} -eq 68 -o ${table} -eq 70; then
+        # Tables 81, 82 and 83 are part of ct_nw_dst(), ct_ip6_dst() and ct_tp_dst()
+        # actions respectively and its OK for them to not have default flows.
+        if test ${table} -eq 68 -o ${table} -eq 70 -o ${table} -eq 81 -o ${table} -eq 82 -o ${table} -eq 83; then
             continue;
         fi
         AT_CHECK([grep -qe "table=$table.* priority=0\(,metadata=0x\w*\)\? actions" oflows], [0], [ignore], [ignore], [echo "Table $table does not contain a default action"])
diff --git a/tests/system-ovn-kmod.at b/tests/system-ovn-kmod.at
index 75ecdadebe..b17ec53f43 100644
--- a/tests/system-ovn-kmod.at
+++ b/tests/system-ovn-kmod.at
@@ -1027,3 +1027,152 @@  OVS_TRAFFIC_VSWITCHD_STOP(["
 "])
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([Load Balancer LS hairpin IPv4 UDP - larger than MTU])
+AT_SKIP_IF([test $HAVE_NC = no])
+AT_SKIP_IF([test $HAVE_TCPDUMP = no])
+AT_KEYWORDS([lb])
+
+ovn_start
+
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+# Set external-ids in br-int needed for ovn-controller
+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 ovn-controller
+start_daemon ovn-controller
+
+# Logical network:
+# One logical switch with IPv4 load balancers that hairpin the traffic.
+ovn-nbctl ls-add sw
+ovn-nbctl lsp-add sw lsp -- lsp-set-addresses lsp 00:00:00:00:00:01
+ovn-nbctl lb-add lb-ipv4-tcp     88.88.88.88:8080 42.42.42.1:4041 tcp
+ovn-nbctl lb-add lb-ipv4-udp     88.88.88.88:4040 42.42.42.1:2021 udp
+ovn-nbctl ls-lb-add sw lb-ipv4-tcp
+ovn-nbctl ls-lb-add sw lb-ipv4-udp
+
+ovn-nbctl lr-add rtr
+ovn-nbctl lrp-add rtr rtr-sw 00:00:00:00:01:00 42.42.42.254/24
+ovn-nbctl lsp-add sw sw-rtr                       \
+    -- lsp-set-type sw-rtr router                 \
+    -- lsp-set-addresses sw-rtr 00:00:00:00:01:00 \
+    -- lsp-set-options sw-rtr router-port=rtr-sw
+
+ADD_NAMESPACES(lsp)
+ADD_VETH(lsp, lsp, br-int, "42.42.42.1/24", "00:00:00:00:00:01", \
+         "42.42.42.254")
+
+ovn-nbctl --wait=hv -t 3 sync
+
+yes 1 | head -n 10000 | tr '\n' ' ' | dd of=datafile bs=7373 count=1
+cat datafile datafile datafile datafile > frag_test.expected
+
+# Start IPv4 UDP server on lsp.
+NETNS_DAEMONIZE([lsp], [nc -l -u 42.42.42.1 2021 -o udp_frag_test.rcvd], [lsp0_udp.pid])
+
+NS_CHECK_EXEC([lsp], [nc -u 88.88.88.88 4040 -p 20000 < datafile], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc -u 88.88.88.88 4040 -p 20000 < datafile], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc -u 88.88.88.88 4040 -p 20000 < datafile], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc -u 88.88.88.88 4040 -p 20000 < datafile], [0], [ignore], [ignore])
+
+AT_CHECK([cmp frag_test.expected udp_frag_test.rcvd], [0], [ignore], [ignore])
+
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+
+as ovn-sb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-nb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as northd
+OVS_APP_EXIT_AND_WAIT([ovn-northd])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([Load Balancer LS hairpin IPv6 UDP - larger than MTU])
+AT_SKIP_IF([test $HAVE_NC = no])
+AT_KEYWORDS([lb])
+
+ovn_start
+
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+# Set external-ids in br-int needed for ovn-controller
+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 ovn-controller
+start_daemon ovn-controller
+
+# Logical network:
+# One logical switch with IPv6 load balancers that hairpin the traffic.
+ovn-nbctl ls-add sw
+ovn-nbctl lsp-add sw lsp -- lsp-set-addresses lsp 00:00:00:00:00:01
+ovn-nbctl lb-add lb-ipv6-tcp     [[8800::0088]]:8080 [[4200::1]]:4041 tcp
+ovn-nbctl lb-add lb-ipv6-udp     [[8800::0088]]:4040 [[4200::1]]:2021 udp
+ovn-nbctl ls-lb-add sw lb-ipv6-tcp
+ovn-nbctl ls-lb-add sw lb-ipv6-udp
+
+ovn-nbctl lr-add rtr
+ovn-nbctl lrp-add rtr rtr-sw 00:00:00:00:01:00 4200::00ff/64
+ovn-nbctl lsp-add sw sw-rtr                       \
+    -- lsp-set-type sw-rtr router                 \
+    -- lsp-set-addresses sw-rtr 00:00:00:00:01:00 \
+    -- lsp-set-options sw-rtr router-port=rtr-sw
+
+ovn-nbctl --wait=hv sync
+
+ADD_NAMESPACES(lsp)
+ADD_VETH(lsp, lsp, br-int, "4200::1/64", "00:00:00:00:00:01", "4200::00ff", "nodad")
+ovn-nbctl --wait=hv -t 3 sync
+
+yes 1 | head -n 10000 | tr '\n' ' ' | dd of=datafile bs=7373 count=1
+cat datafile datafile datafile datafile > frag_test.expected
+
+# Start IPv6 UDP server on lsp.
+NETNS_DAEMONIZE([lsp], [nc -l -u 4200::1 2021 -o udp_frag_test.rcvd], [lsp0_udp.pid])
+
+yes 1 | head -n 10000 | tr '\n' ' ' | dd of=datafile bs=7373 count=1
+NS_CHECK_EXEC([lsp], [nc -6 -u 8800::0088 4040 -p 20001 < datafile], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc -6 -u 8800::0088 4040 -p 20001 < datafile], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc -6 -u 8800::0088 4040 -p 20001 < datafile], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc -6 -u 8800::0088 4040 -p 20001 < datafile], [0], [ignore], [ignore])
+
+cat datafile datafile datafile datafile > udp_frag_test.expected
+AT_CHECK([cmp udp_frag_test.expected udp_frag_test.rcvd], [0], [ignore], [ignore])
+
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+
+as ovn-sb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-nb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as northd
+OVS_APP_EXIT_AND_WAIT([ovn-northd])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+AT_CLEANUP
+])
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index 2ea68f2124..7954bb98ac 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -1376,6 +1376,9 @@  test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
                 .in_port_sec_ptable = OFTABLE_CHK_IN_PORT_SEC,
                 .out_port_sec_ptable = OFTABLE_CHK_OUT_PORT_SEC,
                 .mac_cache_use_table = OFTABLE_MAC_CACHE_USE,
+                .ct_nw_dst_load_table = OFTABLE_CT_ORIG_NW_DST_LOAD,
+                .ct_ip6_dst_load_table = OFTABLE_CT_ORIG_IP6_DST_LOAD,
+                .ct_tp_dst_load_table = OFTABLE_CT_ORIG_TP_DST_LOAD,
                 .lflow_uuid.parts =
                     { 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd},
                 .dp_key = 0xabcdef,
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index f9cc2463b7..806bdf3d9a 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -3447,6 +3447,12 @@  trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             break;
         case OVNACT_MAC_CACHE_USE:
             break;
+        case OVNACT_CT_ORIG_NW_DST:
+            break;
+        case OVNACT_CT_ORIG_IP6_DST:
+            break;
+        case OVNACT_CT_ORIG_TP_DST:
+            break;
         }
     }
     ofpbuf_uninit(&stack);