Blob Blame History Raw
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 8572c24c8..d3747f8d1 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -147,6 +147,7 @@ Fabrizio D'Angelo                  fdangelo@redhat.com
 Flavio Fernandes                   flavio@flaviof.com
 Flavio Leitner                     fbl@redhat.com
 Francesco Fusco                    ffusco@redhat.com
+François Rigault                   frigo@amadeus.com
 Frank Wang                         wangpeihuixyz@126.com
 Frédéric Tobias Christ             fchrist@live.de
 Frode Nordahl                      frode.nordahl@gmail.com
diff --git a/NEWS b/NEWS
index 9f3ce3cf3..15fa545d2 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,7 @@
-OVN v22.03.0 - XX XXX XXXX
+OVN v22.03.1 - xx xxx xxxx
+--------------------------
+
+OVN v22.03.0 - 11 Mar 2022
 --------------------------
   - Refactor CoPP commands introducing a unique name index in CoPP NB table.
     Add following new CoPP commands to manage CoPP table:
diff --git a/configure.ac b/configure.ac
index 283381b4e..70f86e1f5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 AC_PREREQ(2.63)
-AC_INIT(ovn, 22.03.0, bugs@openvswitch.org)
+AC_INIT(ovn, 22.03.1, bugs@openvswitch.org)
 AC_CONFIG_MACRO_DIR([m4])
 AC_CONFIG_AUX_DIR([build-aux])
 AC_CONFIG_HEADERS([config.h])
diff --git a/controller-vtep/binding.c b/controller-vtep/binding.c
index 01d5a16d2..1ee52b592 100644
--- a/controller-vtep/binding.c
+++ b/controller-vtep/binding.c
@@ -109,12 +109,10 @@ update_pb_chassis(const struct sbrec_port_binding *port_binding_rec,
                      port_binding_rec->chassis->name,
                      chassis_rec->name);
         }
-
         sbrec_port_binding_set_chassis(port_binding_rec, chassis_rec);
-        if (port_binding_rec->n_up) {
-            bool up = true;
-            sbrec_port_binding_set_up(port_binding_rec, &up, 1);
-        }
+    } else if (port_binding_rec->n_up) {
+        bool up = true;
+        sbrec_port_binding_set_up(port_binding_rec, &up, 1);
     }
 }
 
diff --git a/controller/binding.c b/controller/binding.c
index 4d62b0858..1259e6b3b 100644
--- a/controller/binding.c
+++ b/controller/binding.c
@@ -481,6 +481,16 @@ remove_related_lport(const struct sbrec_port_binding *pb,
     }
 }
 
+static void
+delete_active_pb_ras_pd(const struct sbrec_port_binding *pb,
+                        struct shash *ras_pd_map)
+{
+    struct pb_ld_binding *ras_pd =
+        shash_find_and_delete(ras_pd_map, pb->logical_port);
+
+    free(ras_pd);
+}
+
 static void
 update_active_pb_ras_pd(const struct sbrec_port_binding *pb,
                         struct hmap *local_datapaths,
@@ -2251,6 +2261,9 @@ binding_handle_port_binding_changes(struct binding_ctx_in *b_ctx_in,
             continue;
         }
 
+        delete_active_pb_ras_pd(pb, b_ctx_out->local_active_ports_ipv6_pd);
+        delete_active_pb_ras_pd(pb, b_ctx_out->local_active_ports_ras);
+
         enum en_lport_type lport_type = get_lport_type(pb);
 
         struct binding_lport *b_lport =
diff --git a/controller/ofctrl.c b/controller/ofctrl.c
index a7c2d2011..3b9d71733 100644
--- a/controller/ofctrl.c
+++ b/controller/ofctrl.c
@@ -943,7 +943,12 @@ link_installed_to_desired(struct installed_flow *i, struct desired_flow *d)
             break;
         }
     }
-    ovs_list_insert(&f->installed_ref_list_node, &d->installed_ref_list_node);
+    if (!f) {
+        ovs_list_insert(&i->desired_refs, &d->installed_ref_list_node);
+    } else {
+        ovs_list_insert(&f->installed_ref_list_node,
+                        &d->installed_ref_list_node);
+    }
     d->installed_flow = i;
     return installed_flow_get_active(i) == d;
 }
@@ -2324,7 +2329,20 @@ deleted_flow_lookup(struct hmap *deleted_flows, struct ovn_flow *target)
             && f->cookie == target->cookie
             && ofpacts_equal(f->ofpacts, f->ofpacts_len, target->ofpacts,
                              target->ofpacts_len)) {
-            return d;
+            /* del_f must have been installed, otherwise it should have
+             * been removed during track_flow_del. */
+            ovs_assert(d->installed_flow);
+
+            /* Now we also need to make sure the desired flow being
+             * added/updated has exact same action and cookie as the installed
+             * flow of d. Otherwise, don't merge them, so that the
+             * installed flow can be updated later. */
+            struct ovn_flow *f_i = &d->installed_flow->flow;
+            if (f_i->cookie == target->cookie
+                && ofpacts_equal(f_i->ofpacts, f_i->ofpacts_len,
+                                 target->ofpacts, target->ofpacts_len)) {
+                return d;
+            }
         }
     }
     return NULL;
@@ -2353,10 +2371,6 @@ merge_tracked_flows(struct ovn_desired_flow_table *flow_table)
                 continue;
             }
 
-            /* del_f must have been installed, otherwise it should have been
-             * removed during track_flow_add_or_modify. */
-            ovs_assert(del_f->installed_flow);
-
             if (!f->installed_flow) {
                 /* f is not installed yet. */
                 replace_installed_to_desired(del_f->installed_flow, del_f, f);
@@ -2665,6 +2679,13 @@ ofctrl_put(struct ovn_desired_flow_table *lflow_table,
     EXTEND_TABLE_FOR_EACH_INSTALLED (m_installed, next_meter, meters) {
         /* Delete the meter. */
         ofctrl_meter_bands_erase(m_installed, &msgs);
+        if (!strncmp(m_installed->name, "__string: ", 10)) {
+            struct ofputil_meter_mod mm = {
+                .command = OFPMC13_DELETE,
+                .meter = { .meter_id = m_installed->table_id },
+            };
+            add_meter_mod(&mm, &msgs);
+        }
         ovn_extend_table_remove_existing(meters, m_installed);
     }
 
diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index 25b37ee88..2f718aca7 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -5523,7 +5523,7 @@ get_localnet_vifs_l3gwports(
             }
             const struct sbrec_port_binding *pb
                 = lport_lookup_by_name(sbrec_port_binding_by_name, iface_id);
-            if (!pb) {
+            if (!pb || pb->chassis != chassis) {
                 continue;
             }
             struct local_datapath *ld
@@ -5554,7 +5554,7 @@ get_localnet_vifs_l3gwports(
         sbrec_port_binding_index_set_datapath(target, ld->datapath);
         SBREC_PORT_BINDING_FOR_EACH_EQUAL (pb, target,
                                            sbrec_port_binding_by_datapath) {
-            if (!strcmp(pb->type, "l3gateway")
+            if ((!strcmp(pb->type, "l3gateway") && pb->chassis == chassis)
                 || !strcmp(pb->type, "patch")) {
                 sset_add(local_l3gw_ports, pb->logical_port);
             }
@@ -5781,7 +5781,8 @@ send_garp_rarp_prepare(struct ovsdb_idl_txn *ovnsb_idl_txn,
         const struct sbrec_port_binding *pb = lport_lookup_by_name(
             sbrec_port_binding_by_name, iface_id);
         if (pb) {
-            send_garp_rarp_update(ovnsb_idl_txn, sbrec_mac_binding_by_lport_ip,
+            send_garp_rarp_update(ovnsb_idl_txn,
+                                  sbrec_mac_binding_by_lport_ip,
                                   local_datapaths, pb, &nat_addresses);
         }
     }
diff --git a/debian/changelog b/debian/changelog
index f1167591b..18a1a042e 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+OVN (22.03.1-1) unstable; urgency=low
+   [ OVN team ]
+   * New upstream version
+
+ -- OVN team <dev@openvswitch.org>  Fri, 11 Mar 2022 13:22:32 -0500
+
 ovn (22.03.0-1) unstable; urgency=low
 
    * New upstream version
diff --git a/lib/actions.c b/lib/actions.c
index 5d3caaf2b..2219c5b37 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -2336,7 +2336,7 @@ validate_empty_lb_backends(struct action_context *ctx,
 
         switch (o->option->code) {
         case EMPTY_LB_VIP:
-            if (!inet_parse_active(c->string, 0, &ss, false)) {
+            if (!inet_parse_active(c->string, 0, &ss, false, NULL)) {
                 lexer_error(ctx->lexer, "Invalid load balancer VIP '%s'",
                             c->string);
                 return;
diff --git a/lib/expr.c b/lib/expr.c
index 47ef6108e..058390a16 100644
--- a/lib/expr.c
+++ b/lib/expr.c
@@ -211,16 +211,17 @@ expr_combine(enum expr_type type, struct expr *a, struct expr *b)
 }
 
 static void
-expr_insert_andor(struct expr *andor, struct expr *before, struct expr *new)
+expr_insert_andor(struct expr *andor, struct ovs_list *before,
+                  struct expr *new)
 {
     if (new->type == andor->type) {
         if (andor->type == EXPR_T_AND) {
             /* Conjunction junction, what's your function? */
         }
-        ovs_list_splice(&before->node, new->andor.next, &new->andor);
+        ovs_list_splice(before, new->andor.next, &new->andor);
         expr_destroy(new);
     } else {
-        ovs_list_insert(&before->node, &new->node);
+        ovs_list_insert(before, &new->node);
     }
 }
 
@@ -2101,7 +2102,8 @@ expr_annotate__(struct expr *expr, const struct shash *symtab,
                 expr_destroy(expr);
                 return NULL;
             }
-            expr_insert_andor(expr, next, new_sub);
+            expr_insert_andor(expr, next ? &next->node : &expr->andor,
+                              new_sub);
         }
         *errorp = NULL;
         return expr;
@@ -2301,7 +2303,7 @@ expr_evaluate_condition(struct expr *expr,
             struct expr *e = expr_evaluate_condition(sub, is_chassis_resident,
                                                      c_aux);
             e = expr_fix(e);
-            expr_insert_andor(expr, next, e);
+            expr_insert_andor(expr, next ? &next->node : &expr->andor, e);
         }
         return expr_fix(expr);
 
@@ -2334,7 +2336,8 @@ expr_simplify(struct expr *expr)
     case EXPR_T_OR:
         LIST_FOR_EACH_SAFE (sub, next, node, &expr->andor) {
             ovs_list_remove(&sub->node);
-            expr_insert_andor(expr, next, expr_simplify(sub));
+            expr_insert_andor(expr, next ? &next->node : &expr->andor,
+                              expr_simplify(sub));
         }
         return expr_fix(expr);
 
@@ -2444,12 +2447,13 @@ crush_and_string(struct expr *expr, const struct expr_symbol *symbol)
      * EXPR_T_OR with EXPR_T_CMP subexpressions. */
     struct expr *sub, *next = NULL;
     LIST_FOR_EACH_SAFE (sub, next, node, &expr->andor) {
+        struct ovs_list *next_list = next ? &next->node : &expr->andor;
         ovs_list_remove(&sub->node);
         struct expr *new = crush_cmps(sub, symbol);
         switch (new->type) {
         case EXPR_T_CMP:
             if (!singleton) {
-                ovs_list_insert(&next->node, &new->node);
+                ovs_list_insert(next_list, &new->node);
                 singleton = new;
             } else {
                 bool match = !strcmp(new->cmp.string, singleton->cmp.string);
@@ -2463,7 +2467,7 @@ crush_and_string(struct expr *expr, const struct expr_symbol *symbol)
         case EXPR_T_AND:
             OVS_NOT_REACHED();
         case EXPR_T_OR:
-            ovs_list_insert(&next->node, &new->node);
+            ovs_list_insert(next_list, &new->node);
             break;
         case EXPR_T_BOOLEAN:
             if (!new->boolean) {
@@ -2559,7 +2563,7 @@ crush_and_numeric(struct expr *expr, const struct expr_symbol *symbol)
         case EXPR_T_AND:
             OVS_NOT_REACHED();
         case EXPR_T_OR:
-            ovs_list_insert(&next->node, &new->node);
+            ovs_list_insert(next ? &next->node : &expr->andor, &new->node);
             break;
         case EXPR_T_BOOLEAN:
             if (!new->boolean) {
@@ -2725,7 +2729,8 @@ crush_or(struct expr *expr, const struct expr_symbol *symbol)
      * is now a disjunction of cmps over the same symbol. */
     LIST_FOR_EACH_SAFE (sub, next, node, &expr->andor) {
         ovs_list_remove(&sub->node);
-        expr_insert_andor(expr, next, crush_cmps(sub, symbol));
+        expr_insert_andor(expr, next ? &next->node : &expr->andor,
+                          crush_cmps(sub, symbol));
     }
     expr = expr_fix(expr);
     if (expr->type != EXPR_T_OR) {
@@ -2886,8 +2891,7 @@ expr_normalize_and(struct expr *expr)
 
     struct expr *a, *b;
     LIST_FOR_EACH_SAFE (a, b, node, &expr->andor) {
-        if (&b->node == &expr->andor
-            || a->type != EXPR_T_CMP || b->type != EXPR_T_CMP
+        if (!b || a->type != EXPR_T_CMP || b->type != EXPR_T_CMP
             || a->cmp.symbol != b->cmp.symbol) {
             continue;
         } else if (a->cmp.symbol->width
@@ -2964,7 +2968,8 @@ expr_normalize_or(struct expr *expr)
                 }
                 expr_destroy(new);
             } else {
-                expr_insert_andor(expr, next, new);
+                expr_insert_andor(expr, next ? &next->node : &expr->andor,
+                                  new);
             }
         } else {
             ovs_assert(sub->type == EXPR_T_CMP ||
diff --git a/lib/inc-proc-eng.c b/lib/inc-proc-eng.c
index 7b4391700..575b774ae 100644
--- a/lib/inc-proc-eng.c
+++ b/lib/inc-proc-eng.c
@@ -354,14 +354,11 @@ engine_recompute(struct engine_node *node, bool allowed,
                  const char *reason_fmt, ...)
 {
     char *reason = NULL;
+    va_list reason_args;
 
-    if (VLOG_IS_DBG_ENABLED()) {
-        va_list reason_args;
-
-        va_start(reason_args, reason_fmt);
-        reason = xvasprintf(reason_fmt, reason_args);
-        va_end(reason_args);
-    }
+    va_start(reason_args, reason_fmt);
+    reason = xvasprintf(reason_fmt, reason_args);
+    va_end(reason_args);
 
     if (!allowed) {
         VLOG_DBG("node: %s, recompute (%s) aborted", node->name, reason);
diff --git a/lib/ovn-parallel-hmap.h b/lib/ovn-parallel-hmap.h
index 897208ef8..0f7d68770 100644
--- a/lib/ovn-parallel-hmap.h
+++ b/lib/ovn-parallel-hmap.h
@@ -58,11 +58,11 @@ extern "C" {
  * ThreadID + step * i as the JOBID parameter.
  */
 
-#define HMAP_FOR_EACH_IN_PARALLEL(NODE, MEMBER, JOBID, HMAP) \
-   for (INIT_CONTAINER(NODE, hmap_first_in_bucket_num(HMAP, JOBID), MEMBER); \
-        (NODE != OBJECT_CONTAINING(NULL, NODE, MEMBER)) \
-       || ((NODE = NULL), false); \
-       ASSIGN_CONTAINER(NODE, hmap_next_in_bucket(&(NODE)->MEMBER), MEMBER))
+#define HMAP_FOR_EACH_IN_PARALLEL(NODE, MEMBER, JOBID, HMAP)                \
+   for (INIT_MULTIVAR(NODE, MEMBER, hmap_first_in_bucket_num(HMAP, JOBID),  \
+                      struct hmap_node);                                    \
+        CONDITION_MULTIVAR(NODE, MEMBER, ITER_VAR(NODE) != NULL);           \
+        UPDATE_MULTIVAR(NODE, hmap_next_in_bucket(ITER_VAR(NODE))))
 
 /* We do not have a SAFE version of the macro, because the hash size is not
  * atomic and hash removal operations would need to be wrapped with
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index a22ae84fe..2f2f7ae39 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -735,7 +735,7 @@ ip_address_and_port_from_lb_key(const char *key, char **ip_address,
                                 uint16_t *port, int *addr_family)
 {
     struct sockaddr_storage ss;
-    if (!inet_parse_active(key, 0, &ss, false)) {
+    if (!inet_parse_active(key, 0, &ss, false, NULL)) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
         VLOG_WARN_RL(&rl, "bad ip address or port for load balancer key %s",
                      key);
diff --git a/northd/northd.c b/northd/northd.c
index b22da67e9..217f180eb 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -763,16 +763,6 @@ init_nat_entries(struct ovn_datapath *od)
         return;
     }
 
-    if (od->n_l3dgw_ports > 1) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-        VLOG_WARN_RL(&rl, "NAT is configured on logical router %s, which has %"
-                     PRIuSIZE" distributed gateway ports. NAT is not supported"
-                     " yet when there is more than one distributed gateway "
-                     "port on the router.",
-                     od->nbr->name, od->n_l3dgw_ports);
-        return;
-    }
-
     od->nat_entries = xmalloc(od->nbr->n_nat * sizeof *od->nat_entries);
 
     for (size_t i = 0; i < od->nbr->n_nat; i++) {
@@ -6418,6 +6408,72 @@ ovn_port_group_destroy(struct hmap *pgs, struct ovn_port_group *pg)
     }
 }
 
+static void
+copy_ra_to_sb(struct ovn_port *op, const char *address_mode);
+
+static void
+ovn_update_ipv6_options(struct hmap *ports)
+{
+    struct ovn_port *op;
+    HMAP_FOR_EACH (op, key_node, ports) {
+        if (!op->nbrp || op->nbrp->peer || !op->peer) {
+            continue;
+        }
+
+        if (!op->lrp_networks.n_ipv6_addrs) {
+            continue;
+        }
+
+        struct smap options;
+        smap_clone(&options, &op->sb->options);
+
+        /* enable IPv6 prefix delegation */
+        bool prefix_delegation = smap_get_bool(&op->nbrp->options,
+                                           "prefix_delegation", false);
+        if (!lrport_is_enabled(op->nbrp)) {
+            prefix_delegation = false;
+        }
+        if (smap_get_bool(&options, "ipv6_prefix_delegation",
+                          false) != prefix_delegation) {
+            smap_add(&options, "ipv6_prefix_delegation",
+                     prefix_delegation ? "true" : "false");
+        }
+
+        bool ipv6_prefix = smap_get_bool(&op->nbrp->options,
+                                     "prefix", false);
+        if (!lrport_is_enabled(op->nbrp)) {
+            ipv6_prefix = false;
+        }
+        if (smap_get_bool(&options, "ipv6_prefix", false) != ipv6_prefix) {
+            smap_add(&options, "ipv6_prefix",
+                     ipv6_prefix ? "true" : "false");
+        }
+        sbrec_port_binding_set_options(op->sb, &options);
+
+        smap_destroy(&options);
+
+        const char *address_mode = smap_get(
+            &op->nbrp->ipv6_ra_configs, "address_mode");
+
+        if (!address_mode) {
+            continue;
+        }
+        if (strcmp(address_mode, "slaac") &&
+            strcmp(address_mode, "dhcpv6_stateful") &&
+            strcmp(address_mode, "dhcpv6_stateless")) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_WARN_RL(&rl, "Invalid address mode [%s] defined",
+                         address_mode);
+            continue;
+        }
+
+        if (smap_get_bool(&op->nbrp->ipv6_ra_configs, "send_periodic",
+                          false)) {
+            copy_ra_to_sb(op, address_mode);
+        }
+    }
+}
+
 static void
 build_port_group_lswitches(struct northd_input *input_data,
                            struct hmap *pgs,
@@ -10759,6 +10815,12 @@ build_neigh_learning_flows_for_lrouter(
                           copp_meter_get(COPP_ARP, od->nbr->copp,
                                          meter_groups));
 
+        ovn_lflow_metered(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 95,
+                          "nd_na && nd.tll == 0",
+                          "put_nd(inport, nd.target, eth.src); next;",
+                          copp_meter_get(COPP_ND_NA, od->nbr->copp,
+                                         meter_groups));
+
         ovn_lflow_metered(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
                           "nd_na", "put_nd(inport, nd.target, nd.tll); next;",
                           copp_meter_get(COPP_ND_NA, od->nbr->copp,
@@ -10851,34 +10913,6 @@ build_ND_RA_flows_for_lrouter_port(
         return;
     }
 
-    struct smap options;
-    smap_clone(&options, &op->sb->options);
-
-    /* enable IPv6 prefix delegation */
-    bool prefix_delegation = smap_get_bool(&op->nbrp->options,
-                                           "prefix_delegation", false);
-    if (!lrport_is_enabled(op->nbrp)) {
-        prefix_delegation = false;
-    }
-    if (smap_get_bool(&options, "ipv6_prefix_delegation",
-                      false) != prefix_delegation) {
-        smap_add(&options, "ipv6_prefix_delegation",
-                 prefix_delegation ? "true" : "false");
-    }
-
-    bool ipv6_prefix = smap_get_bool(&op->nbrp->options,
-                                     "prefix", false);
-    if (!lrport_is_enabled(op->nbrp)) {
-        ipv6_prefix = false;
-    }
-    if (smap_get_bool(&options, "ipv6_prefix", false) != ipv6_prefix) {
-        smap_add(&options, "ipv6_prefix",
-                 ipv6_prefix ? "true" : "false");
-    }
-    sbrec_port_binding_set_options(op->sb, &options);
-
-    smap_destroy(&options);
-
     const char *address_mode = smap_get(
         &op->nbrp->ipv6_ra_configs, "address_mode");
 
@@ -10894,11 +10928,6 @@ build_ND_RA_flows_for_lrouter_port(
         return;
     }
 
-    if (smap_get_bool(&op->nbrp->ipv6_ra_configs, "send_periodic",
-                      false)) {
-        copy_ra_to_sb(op, address_mode);
-    }
-
     ds_clear(match);
     ds_put_format(match, "inport == %s && ip6.dst == ff02::2 && nd_rs",
                           op->json_key);
@@ -12805,6 +12834,7 @@ build_lrouter_out_snat_flow(struct hmap *lflows, struct ovn_datapath *od,
             ds_put_format(actions, "ip%s.src=%s; next;",
                           is_v6 ? "6" : "4", nat->external_ip);
         } else {
+            ds_put_format(match, " && (!ct.trk || !ct.rpl)");
             ds_put_format(actions, "ct_snat(%s", nat->external_ip);
 
             if (nat->external_port_range[0]) {
@@ -13155,6 +13185,18 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
         return;
     }
 
+    /* NAT rules are not currently supported on logical routers with multiple
+     * distributed gateway ports. */
+    if (od->n_l3dgw_ports > 1) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+        VLOG_WARN_RL(&rl, "NAT is configured on logical router %s, which has %"
+                     PRIuSIZE" distributed gateway ports. NAT is not supported"
+                     " yet when there is more than one distributed gateway "
+                     "port on the router.",
+                     od->nbr->name, od->n_l3dgw_ports);
+        return;
+    }
+
     struct sset nat_entries = SSET_INITIALIZER(&nat_entries);
 
     bool dnat_force_snat_ip =
@@ -15048,6 +15090,7 @@ ovnnb_db_run(struct northd_input *input_data,
     build_meter_groups(input_data, &data->meter_groups);
     stopwatch_stop(BUILD_LFLOWS_CTX_STOPWATCH_NAME, time_msec());
     stopwatch_start(CLEAR_LFLOWS_CTX_STOPWATCH_NAME, time_msec());
+    ovn_update_ipv6_options(&data->ports);
     ovn_update_ipv6_prefix(&data->ports);
 
     sync_address_sets(input_data,  ovnsb_txn, &data->datapaths);
diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index 1c9d408af..4cb70e185 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -2301,6 +2301,12 @@ next;
         <code>put_arp(inport, arp.spa, arp.sha); next;</code>
       </li>
 
+      <li>
+        A priority-95 flow with the match <code>nd_na  &amp;&amp;
+        nd.tll == 0</code> and applies the action
+        <code>put_nd(inport, nd.target, eth.src); next;</code>
+      </li>
+
       <li>
         A priority-90 flow with the match <code>nd_na</code> and
         applies the action
@@ -4452,7 +4458,8 @@ nd_ns {
           to change the source IP address of a packet from an IP address of
           <var>A</var> or to change the source IP address of a packet that
           belongs to network <var>A</var> to <var>B</var>, a flow matches
-          <code>ip &amp;&amp; ip4.src == <var>A</var></code> with an action
+          <code>ip &amp;&amp; ip4.src == <var>A</var> &amp;&amp;
+          (!ct.trk || !ct.rpl)</code> with an action
           <code>ct_snat(<var>B</var>);</code>.  The priority of the flow
           is calculated based on the mask of <var>A</var>, with matches
           having larger masks getting higher priorities. If the NAT rule is
diff --git a/rhel/ovn-fedora.spec.in b/rhel/ovn-fedora.spec.in
index 3fb854a37..821eb03cc 100644
--- a/rhel/ovn-fedora.spec.in
+++ b/rhel/ovn-fedora.spec.in
@@ -323,7 +323,7 @@ ln -sf ovn_detrace.py %{_bindir}/ovn-detrace
 %if %{with libcapng}
 if [ $1 -eq 1 ]; then
     sed -i 's:^#OVN_USER_ID=:OVN_USER_ID=:' %{_sysconfdir}/sysconfig/ovn
-    sed -i 's:\(.*su\).*:\1 ovn ovn:' %{_sysconfdir}/logrotate.d/ovn
+    sed -i 's:\(.*su\).*:\1 openvswitch openvswitch:' %{_sysconfdir}/logrotate.d/ovn
 fi
 %endif
 
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 3865003bf..b7dfcd151 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -1030,7 +1030,7 @@ AT_CHECK([grep -e "lr_out_snat" drflows | sed 's/table=../table=??/' | sort], [0
 AT_CHECK([grep -e "lr_out_snat" crflows | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $allowed_range), action=(ct_snat(172.16.1.1);)
+  table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
 ])
 
 
@@ -1062,7 +1062,7 @@ AT_CHECK([grep -e "lr_out_snat" drflows2 | sed 's/table=../table=??/' | sort], [
 AT_CHECK([grep -e "lr_out_snat" crflows2 | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11), action=(ct_snat(172.16.1.1);)
+  table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
   table=??(lr_out_snat        ), priority=35   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $disallowed_range), action=(next;)
 ])
 
@@ -1091,7 +1091,7 @@ AT_CHECK([grep -e "lr_out_snat" drflows3 | sed 's/table=../table=??/' | sort], [
 AT_CHECK([grep -e "lr_out_snat" crflows3 | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $allowed_range), action=(ct_snat(172.16.1.2);)
+  table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
 ])
 
 # Stateful FIP with DISALLOWED_IPs
@@ -1120,7 +1120,7 @@ AT_CHECK([grep -e "lr_out_snat" drflows4 | sed 's/table=../table=??/' | sort], [
 AT_CHECK([grep -e "lr_out_snat" crflows4 | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11), action=(ct_snat(172.16.1.2);)
+  table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
   table=??(lr_out_snat        ), priority=35   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $disallowed_range), action=(next;)
 ])
 
@@ -3447,7 +3447,7 @@ ls_copp_uuid=$(fetch_column nb:Logical_Switch copp)
 AT_CHECK([test "$ls_copp_uuid" = "$copp_uuid"])
 
 check ovn-nbctl --wait=hv copp-add $copp_uuid igmp meter0
-AT_CHECK([ovn-nbctl copp-list copp0], [0], [dnl
+AT_CHECK([ovn-nbctl copp-list copp0 | sort], [0], [dnl
 arp: meter0
 igmp: meter0
 ])
@@ -5140,11 +5140,12 @@ AT_CHECK([grep "lr_out_post_undnat" lr0flows | sed 's/table=./table=?/' | sort],
 AT_CHECK([grep "lr_out_snat" lr0flows | sed 's/table=./table=?/' | sort], [0], [dnl
   table=? (lr_out_snat        ), priority=0    , match=(1), action=(next;)
   table=? (lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=? (lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24), action=(ct_snat(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10), action=(ct_snat(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.3), action=(ct_snat(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.3 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
 ])
 
+
 # Set lb force snat logical router.
 check ovn-nbctl --wait=sb set logical_router lr0 options:lb_force_snat_ip="router_ip"
 check ovn-nbctl --wait=sb sync
@@ -5201,9 +5202,9 @@ AT_CHECK([grep "lr_out_snat" lr0flows | sed 's/table=./table=?/' | sort], [0], [
   table=? (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
   table=? (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
   table=? (lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=? (lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24), action=(ct_snat(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10), action=(ct_snat(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.3), action=(ct_snat(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.3 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
 ])
 
 # Add a LB VIP same as router ip.
@@ -5266,9 +5267,9 @@ AT_CHECK([grep "lr_out_snat" lr0flows | sed 's/table=./table=?/' | sort], [0], [
   table=? (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
   table=? (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
   table=? (lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=? (lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24), action=(ct_snat(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10), action=(ct_snat(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.3), action=(ct_snat(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.3 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
 ])
 
 # Add IPv6 router port and LB.
@@ -5346,9 +5347,9 @@ AT_CHECK([grep "lr_out_snat" lr0flows | sed 's/table=./table=?/' | sort], [0], [
   table=? (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-public"), action=(ct_snat(def0::10);)
   table=? (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-sw0"), action=(ct_snat(aef0::1);)
   table=? (lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=? (lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24), action=(ct_snat(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10), action=(ct_snat(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.3), action=(ct_snat(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.3 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
 ])
 
 check ovn-nbctl lrp-del lr0-sw0
@@ -5804,6 +5805,12 @@ AT_CHECK([grep lr_in_gw_redirect lrflows | grep cr-DR | sed 's/table=../table=??
   table=??(lr_in_gw_redirect  ), priority=50   , match=(outport == "DR-S3"), action=(outport = "cr-DR-S3"; next;)
 ])
 
+# Check that ovn-northd logs a warning when trying to configure NAT
+# on the router with multiple distributed gw ports.  Such configurations are
+# not supported yet.
+check ovn-nbctl lr-nat-add DR dnat_and_snat 42.42.42.1 20.0.0.2
+AT_CHECK([grep -q 'NAT is configured on logical router DR, which has 2 distributed gateway ports. NAT is not supported yet when there is more than one distributed gateway port on the router.' northd/ovn-northd.log], [0])
+
 AT_CLEANUP
 ])
 
@@ -6426,3 +6433,28 @@ AT_CHECK([grep -e "ls_in_stateful" lsflows | sed 's/table=../table=??/' | sort],
 
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([LR neighbor lookup and learning flows])
+ovn_start
+
+# Create logical routers
+ovn-nbctl --wait=sb lr-add lr0
+
+ovn-sbctl dump-flows lr0 > lrflows
+AT_CAPTURE_FILE([lrflows])
+
+AT_CHECK([cat lrflows | grep -e lr_in_lookup_neighbor -e lr_in_learn_neighbor | sort], [0], [dnl
+  table=1 (lr_in_lookup_neighbor), priority=0    , match=(1), action=(reg9[[2]] = 1; next;)
+  table=1 (lr_in_lookup_neighbor), priority=100  , match=(arp.op == 2), action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
+  table=1 (lr_in_lookup_neighbor), priority=100  , match=(nd_na), action=(reg9[[2]] = lookup_nd(inport, nd.target, nd.tll); next;)
+  table=1 (lr_in_lookup_neighbor), priority=100  , match=(nd_ns), action=(reg9[[2]] = lookup_nd(inport, ip6.src, nd.sll); next;)
+  table=2 (lr_in_learn_neighbor), priority=100  , match=(reg9[[2]] == 1), action=(next;)
+  table=2 (lr_in_learn_neighbor), priority=90   , match=(arp), action=(put_arp(inport, arp.spa, arp.sha); next;)
+  table=2 (lr_in_learn_neighbor), priority=90   , match=(nd_na), action=(put_nd(inport, nd.target, nd.tll); next;)
+  table=2 (lr_in_learn_neighbor), priority=90   , match=(nd_ns), action=(put_nd(inport, ip6.src, nd.sll); next;)
+  table=2 (lr_in_learn_neighbor), priority=95   , match=(nd_na && nd.tll == 0), action=(put_nd(inport, nd.target, eth.src); next;)
+])
+
+AT_CLEANUP
+])
diff --git a/tests/ovn.at b/tests/ovn.at
index 4f65d1ecd..4ee9aebc9 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -8588,6 +8588,114 @@ OVN_CLEANUP([hv1])
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([send gratuitous arp for l3gateway only on selected chassis])
+ovn_start
+
+# Create logical switch
+ovn-nbctl ls-add ls0
+# Create gateway router
+ovn-nbctl lr-add lr0
+# Add router port to gateway router
+ovn-nbctl lrp-add lr0 lr0-ls0 f0:00:00:00:00:01 192.168.0.1/24
+ovn-nbctl lsp-add ls0 ls0-lr0 -- set Logical_Switch_Port ls0-lr0 \
+    type=router options:router-port=lr0-ls0 addresses='"f0:00:00:00:00:01"'
+
+# Create a localnet port.
+ovn-nbctl lsp-add ls0 ln_port
+ovn-nbctl lsp-set-addresses ln_port unknown
+ovn-nbctl lsp-set-type ln_port localnet
+ovn-nbctl --wait=hv lsp-set-options ln_port network_name=physnet1
+
+# Prepare packets
+touch empty_expected
+echo "fffffffffffff0000000000108060001080006040001f00000000001c0a80001000000000000c0a80001" > arp_expected
+
+net_add n1
+sim_add hv1
+as hv1
+ovs-vsctl \
+    -- add-br br-phys \
+    -- add-br br-eth0
+
+ovn_attach n1 br-phys 192.168.0.10
+
+AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=physnet1:br-eth0])
+AT_CHECK([ovs-vsctl add-port br-eth0 snoopvif -- set Interface snoopvif options:tx_pcap=hv1/snoopvif-tx.pcap options:rxq_pcap=hv1/snoopvif-rx.pcap])
+
+sim_add hv2
+as hv2
+ovs-vsctl \
+    -- add-br br-phys \
+    -- add-br br-eth0
+
+ovn_attach n1 br-phys 192.168.0.20
+
+AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=physnet1:br-eth0])
+AT_CHECK([ovs-vsctl add-port br-eth0 snoopvif -- set Interface snoopvif options:tx_pcap=hv2/snoopvif-tx.pcap options:rxq_pcap=hv2/snoopvif-rx.pcap])
+
+ovn-sbctl dump-flows > sbflows
+AT_CAPTURE_FILE([sbflows])
+
+# Wait until the patch ports are created in hv1 and hv2 to connect br-int to br-eth0
+AT_CHECK([ovn-nbctl set logical_router lr0 options:chassis=hv1])
+OVS_WAIT_UNTIL([test 1 = `as hv1 ovs-vsctl show | \
+grep "Port patch-br-int-to-ln_port" | wc -l`])
+AT_CHECK([ovn-nbctl set logical_router lr0 options:chassis=hv2])
+OVS_WAIT_UNTIL([test 1 = `as hv2 ovs-vsctl show | \
+grep "Port patch-br-int-to-ln_port" | wc -l`])
+
+# Temporarily remove lr0 chassis
+AT_CHECK([ovn-nbctl remove logical_router lr0 options chassis])
+
+reset_pcap_file() {
+    local hv=$1
+    local iface=$2
+    local pcap_file=$3
+    as $hv
+    ovs-vsctl -- set Interface $iface options:tx_pcap=dummy-tx.pcap \
+options:rxq_pcap=dummy-rx.pcap
+    rm -f ${pcap_file}*.pcap
+    ovs-vsctl -- set Interface $iface options:tx_pcap=${pcap_file}-tx.pcap \
+options:rxq_pcap=${pcap_file}-rx.pcap
+}
+
+reset_pcap_file hv1 snoopvif hv1/snoopvif
+reset_pcap_file hv2 snoopvif hv2/snoopvif
+
+hv1_uuid=$(ovn-sbctl --bare --columns _uuid list chassis hv1)
+AT_CHECK([ovn-nbctl set logical_router lr0 options:chassis=hv1])
+OVS_WAIT_UNTIL([
+    ls0_lr0=$(ovn-sbctl --bare --columns chassis list port_binding ls0-lr0)
+    test "$ls0_lr0" = $hv1_uuid
+])
+
+sleep 2
+OVN_CHECK_PACKETS_CONTAIN([hv1/snoopvif-tx.pcap], [arp_expected])
+OVN_CHECK_PACKETS([hv2/snoopvif-tx.pcap], [empty_expected])
+
+# Temporarily remove lr0 chassis
+AT_CHECK([ovn-nbctl remove logical_router lr0 options chassis])
+
+reset_pcap_file hv1 snoopvif hv1/snoopvif
+reset_pcap_file hv2 snoopvif hv2/snoopvif
+
+hv2_uuid=$(ovn-sbctl --bare --columns _uuid list chassis hv2)
+AT_CHECK([ovn-nbctl set logical_router lr0 options:chassis=hv2])
+OVS_WAIT_UNTIL([
+    ls0_lr0=$(ovn-sbctl --bare --columns chassis list port_binding ls0-lr0)
+    test "$ls0_lr0" = $hv2_uuid
+])
+
+sleep 2
+OVN_CHECK_PACKETS_CONTAIN([hv2/snoopvif-tx.pcap], [arp_expected])
+OVN_CHECK_PACKETS([hv1/snoopvif-tx.pcap], [empty_expected])
+
+OVN_CLEANUP([hv1],[hv2])
+
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([send gratuitous arp with nat-addresses router in localnet])
 ovn_start
@@ -9403,6 +9511,10 @@ check ovn-nbctl --wait=hv qos-add lsw0 to-lport 1002 'inport=="lp2" && is_chassi
 AT_CHECK([as hv ovs-ofctl dump-meters br-int -O OpenFlow13 | grep meter | wc -l], [0], [4
 ])
 
+check ovn-nbctl qos-del lsw0
+AT_CHECK([as hv ovs-ofctl dump-meters br-int -O OpenFlow13 | grep meter | wc -l], [0], [0
+])
+
 OVN_CLEANUP([hv])
 AT_CLEANUP
 ])
@@ -13988,6 +14100,99 @@ OVN_CLEANUP([hv1],[hv2])
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([garps disabled when port no longer bound to chassis])
+ovn_start
+
+net_add n1
+for i in 1 2; do
+    sim_add hv$i
+    as hv$i
+    check ovs-vsctl add-br br-phys
+    ovn_attach n1 br-phys 192.168.0.$i
+    check ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
+done
+
+check ovn-nbctl ls-add ls0
+check ovn-nbctl lsp-add ls0 port
+check ovn-nbctl lsp-set-addresses port "00:00:00:00:00:01 10.0.0.1"
+
+check ovn-nbctl lsp-add ls0 public
+check ovn-nbctl lsp-set-addresses public unknown
+check ovn-nbctl lsp-set-type public localnet
+check ovn-nbctl lsp-set-options public network_name=phys
+
+for hv in hv1 hv2; do
+    as $hv check ovs-vsctl -- add-port br-int port -- \
+        set Interface port external-ids:iface-id=port \
+        options:tx_pcap=$hv/port-tx.pcap \
+        options:rxq_pcap=$hv/port-rx.pcap
+done
+
+reset_pcap_file() {
+    local hv=$1
+    local iface=$2
+    local pcap_file=$3
+    as $hv check ovs-vsctl -- set Interface $iface options:tx_pcap=dummy-tx.pcap \
+                                                   options:rxq_pcap=dummy-rx.pcap
+    check rm -f ${pcap_file}*.pcap
+    as $hv check ovs-vsctl -- set Interface $iface options:tx_pcap=${pcap_file}-tx.pcap \
+                                                   options:rxq_pcap=${pcap_file}-rx.pcap
+}
+
+reset_env() {
+    reset_pcap_file hv1 br-phys_n1 hv1/br-phys_n1
+    reset_pcap_file hv2 br-phys_n1 hv2/br-phys_n1
+
+    for port in hv1/n1 hv2/n1; do
+        : > $port.expected
+    done
+}
+
+for hv in hv1 hv2; do
+    wait_row_count Chassis 1 name=$hv
+done
+hv1_uuid=$(fetch_column Chassis _uuid name=hv1)
+hv2_uuid=$(fetch_column Chassis _uuid name=hv2)
+
+OVN_POPULATE_ARP
+
+# Activate port on each hv giving a chance to each chassis to enable garps
+check ovn-nbctl lsp-set-options port requested-chassis=hv1
+wait_column "$hv1_uuid" Port_Binding chassis logical_port=port
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=port
+wait_for_ports_up
+reset_env
+
+# give chassis some time to generate garps
+sleep 2
+
+expected_garp=ffffffffffff000000000001080600010800060400010000000000010a0000010000000000000a000001
+
+# check hv1 sends garps and hv2 doesn't
+echo $expected_garp >> hv1/n1.expected
+OVN_CHECK_PACKETS_CONTAIN([hv1/br-phys_n1-tx.pcap], [hv1/n1.expected])
+OVN_CHECK_PACKETS([hv2/br-phys_n1-tx.pcap], [hv2/n1.expected])
+
+check ovn-nbctl lsp-set-options port requested-chassis=hv2
+wait_column "$hv2_uuid" Port_Binding chassis logical_port=port
+wait_column "$hv2_uuid" Port_Binding requested_chassis logical_port=port
+wait_for_ports_up
+reset_env
+
+# give chassis some time to generate garps
+sleep 2
+
+# check hv2 sends garps and hv1 doesn't
+echo $expected_garp >> hv2/n1.expected
+OVN_CHECK_PACKETS([hv1/br-phys_n1-tx.pcap], [hv1/n1.expected])
+OVN_CHECK_PACKETS_CONTAIN([hv2/br-phys_n1-tx.pcap], [hv2/n1.expected])
+
+OVN_CLEANUP([hv1],[hv2])
+
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([IPv6 periodic RA disabled for localnet adjacent switch ports])
 ovn_start
@@ -15243,6 +15448,92 @@ OVN_CLEANUP([hv1])
 AT_CLEANUP
 ])
 
+# This test ensures that the incremental flow installation works well when
+# handling update->delete->add/update for the same OVS flow.
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ACL conjunction append and reprocess])
+ovn_start
+
+net_add n1
+sim_add hv1
+as hv1
+check ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+
+# Setup the desired state:
+# - Two ACLs, each matches its own port-group (pg1 & pg2), and matches the same
+#   set of IP addresses.
+# - pg1 includes p1, p2, p3
+# - pg2 includes p4, p5
+check ovn-nbctl ls-add sw
+check ovn-nbctl lsp-add sw p1 -- lsp-set-addresses p1 "00:00:00:00:00:02 192.168.0.2"
+check ovn-nbctl lsp-add sw p2 -- lsp-set-addresses p2 "00:00:00:00:00:03 192.168.0.3"
+check ovn-nbctl lsp-add sw p3 -- lsp-set-addresses p3 "00:00:00:00:00:04 192.168.0.4"
+check ovn-nbctl lsp-add sw p4 -- lsp-set-addresses p4 "00:00:00:00:00:05 192.168.0.5"
+check ovn-nbctl lsp-add sw p5 -- lsp-set-addresses p5 "00:00:00:00:00:06 192.168.0.6"
+check ovn-nbctl pg-add pg1 p1 p2 p3
+check ovn-nbctl pg-add pg2 p4 p5
+check ovs-vsctl add-port br-int p1 -- set Interface p1 external_ids:iface-id=p1
+check ovs-vsctl add-port br-int p2 -- set Interface p2 external_ids:iface-id=p2
+check ovs-vsctl add-port br-int p3 -- set Interface p3 external_ids:iface-id=p3
+check ovs-vsctl add-port br-int p4 -- set Interface p4 external_ids:iface-id=p4
+check ovs-vsctl add-port br-int p5 -- set Interface p5 external_ids:iface-id=p5
+check ovn-nbctl acl-add pg1 to-lport 1000 "outport==@pg1 && ip4 && ip4.src == {10.0.0.1, 10.0.0.2}" allow
+check ovn-nbctl acl-add pg2 to-lport 1000 "outport==@pg2 && ip4 && ip4.src == {10.0.0.1, 10.0.0.2}" allow
+check ovn-nbctl --wait=hv sync
+
+# Now we should have two flows with combined conjunctions.
+OVS_WAIT_UNTIL([test 2 = `as hv1 ovs-ofctl dump-flows br-int | \
+grep conjunction.*conjunction | wc -l`])
+
+
+# Test the scenario 10 times to give enough chance to hit the
+# "update->delete->add/update" scenario, because we can't decide the order of
+# change handling inside ovn-controller.
+for i in $(seq 10); do
+# Unbind the p3 and p5, the combined conjunctions should be gone.
+ovs-vsctl del-port br-int p3
+ovs-vsctl del-port br-int p5
+OVS_WAIT_UNTIL([test 0 = `as hv1 ovs-ofctl dump-flows br-int | \
+grep conjunction.*conjunction | wc -l`])
+
+# Delete and re-add the ACLs, just to bring some randomness in the lflow
+# processing order, so that there is a chance that the order of adding and
+# appending are the same before & after the flow deletion, so that the
+# generated combined conjunctions are the same before & after the flow
+# deletion. (If the order is different, the combined conjunctions order is
+# different and the action comparison would fail, so won't trigger the tracked
+# flow merging. We want to make sure that we test the merging scenario)
+ovn-nbctl acl-del pg1 to-lport 1000 "outport==@pg1 && ip4 && ip4.src == {10.0.0.1, 10.0.0.2}"
+ovn-nbctl acl-del pg2 to-lport 1000 "outport==@pg2 && ip4 && ip4.src == {10.0.0.1, 10.0.0.2}"
+ovn-nbctl acl-add pg1 to-lport 1000 "outport==@pg1 && ip4 && ip4.src == {10.0.0.1, 10.0.0.2}" allow
+ovn-nbctl acl-add pg2 to-lport 1000 "outport==@pg2 && ip4 && ip4.src == {10.0.0.1, 10.0.0.2}" allow
+ovn-nbctl --wait=hv sync
+
+# Now re-bind p3 and p5 in the same transaction, so that pg1 and pg2 update are
+# handled in the same I-P engine run. The order of pg1 and pg2 can be random.
+# If the order is pg2 -> pg1, then it should trigger the OVS flow
+# "update->delete->add/update" scenario:
+# 1) when pg2 update is handled, the ACL-2 would append conjunctions to
+#    the conjunction flows of ACL-1
+# 2) when pg1 update is handled, it would flood remove flows of both ACL-1 and
+#    ACL-2, including the "appended" conjunction flows. And then reprocess
+#    ACL-1 and ACL-2 would re-add and re-append the conjunction flows with
+#    combined conjunctions.
+ovs-vsctl add-port br-int p3 -- set Interface p3 external_ids:iface-id=p3 -- \
+    add-port br-int p5 -- set Interface p5 external_ids:iface-id=p5
+ovn-nbctl --wait=hv sync
+
+# Now making sure we end up with two combined conjunctions.
+OVS_WAIT_UNTIL([test 2 = `as hv1 ovs-ofctl dump-flows br-int | \
+grep conjunction.*conjunction | wc -l`])
+
+done
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([Superseding ACLs with conjunction])
 ovn_start
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index c4a2c39f6..018dcea2a 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -6922,10 +6922,10 @@ p = IP(src="192.168.1.2", dst="192.168.1.1") / UDP(dport = 12345) / Raw(b"X"*64)
 send (p, iface='sw01', loop = 0, verbose = 0, count = 20)
 EOF
 
-# 1pps + 1 burst size
+# 1pps
 OVS_WAIT_UNTIL([
     n_reject=$(grep unreachable reject.pcap | wc -l)
-    test "${n_reject}" = "2"
+    test "${n_reject}" = "1"
 ])
 kill $(pidof tcpdump)
 rm -f reject.pcap
@@ -6938,10 +6938,10 @@ p = IP(src="192.168.1.2", dst="192.168.1.1") / UDP(dport = 12345) / Raw(b"X"*64)
 send (p, iface='sw01', loop = 0, verbose = 0, count = 100)
 EOF
 
-# 10pps + 1 burst size
+# 10pps
 OVS_WAIT_UNTIL([
     n_reject=$(grep unreachable reject.pcap | wc -l)
-    test "${n_reject}" = "20"
+    test "${n_reject}" = "10"
 ])
 
 kill $(pidof tcpdump)
@@ -6974,10 +6974,10 @@ p = IP(src="192.168.1.2", dst="172.16.1.100") / TCP(dport = 80, flags="S") / Raw
 send (p, iface='sw01', loop = 0, verbose = 0, count = 100)
 EOF
 
-# 1pps + 1 burst size
+# 1pps
 OVS_WAIT_UNTIL([
     n_arp=$(grep ARP arp.pcap | wc -l)
-    test "${n_arp}" = "2"
+    test "${n_arp}" = "1"
 ])
 kill $(pidof tcpdump)
 
@@ -6994,10 +6994,10 @@ p = IP(src="192.168.1.2", dst="172.16.1.100", ttl=1) / TCP(dport = 8080, flags="
 send (p, iface='sw01', loop = 0, verbose = 0, count = 100)
 EOF
 
-# 1pps + 1 burst size
+# 1pps
 OVS_WAIT_UNTIL([
     n_icmp=$(grep ICMP icmp.pcap | wc -l)
-    test "${n_icmp}" = "2"
+    test "${n_icmp}" = "1"
 ])
 kill $(pidof tcpdump)
 
@@ -7010,7 +7010,7 @@ bfd: bfd-meter
 
 check ovn-nbctl --bfd lr-route-add R1 240.0.0.0/8 172.16.1.50 rp-public
 printf "%08x" $(ovn-sbctl get bfd . disc) > /tmp/disc
-NS_EXEC([server], [tcpdump -l -n -i s1 udp port 3784 -Q in > bfd.pcap &])
+NS_EXEC([server], [tcpdump -l -nn -i s1 udp port 3784 and ip[[29]]==0x90 -Q in > bfd.pcap &])
 ip netns exec server scapy -H <<-EOF
 import binascii
 f = open("/tmp/disc", "r")
@@ -7023,10 +7023,10 @@ f.close()
 EOF
 rm /tmp/disc
 
-# 1pps + 1 burst size
+# 1pps
 OVS_WAIT_UNTIL([
-    n_tcp_rst=$(grep Final bfd.pcap | wc -l)
-    test "${n_tcp_rst}" = "2"
+    n_bfd=$(grep 3784 bfd.pcap | wc -l)
+    test "${n_bfd}" = "1"
 ])
 kill $(pidof tcpdump)
 
@@ -7992,3 +7992,122 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
 
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([East-West traffic with gateway router if DNAT configured])
+AT_KEYWORDS([ovnnat])
+
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+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 LR - R1  has two switches: sw0 and sw1
+#    sw0 -- R1 -- sw1
+# Logical port 'sw01' in switch 'sw0'.
+# Logical port 'sw11' in switch 'sw1'.
+# nc server running in sw01
+# nc client running on sw11
+
+check ovn-nbctl lr-add R1
+check ovn-nbctl ls-add sw0
+check ovn-nbctl ls-add sw1
+
+check ovn-nbctl lrp-add R1 rp-sw0 00:00:01:01:02:03 192.168.1.1/24
+check ovn-nbctl lrp-add R1 rp-sw1 00:00:03:01:02:03 192.168.2.1/24
+check ovn-nbctl set logical_router R1 options:chassis=hv1
+
+check ovn-nbctl lsp-add sw0 sw0-rp -- set Logical_Switch_Port sw0-rp \
+    type=router options:router-port=rp-sw0 \
+    -- lsp-set-addresses sw0-rp router
+check ovn-nbctl lsp-add sw1 sw1-rp -- set Logical_Switch_Port sw1-rp \
+    type=router options:router-port=rp-sw1 \
+    -- lsp-set-addresses sw1-rp router
+
+ADD_NAMESPACES(sw01)
+ADD_VETH(sw01, sw01, br-int, "192.168.1.2/24", "f0:00:00:01:02:03", \
+       "192.168.1.1")
+check ovn-nbctl lsp-add sw0 sw01 \
+    -- lsp-set-addresses sw01 "f0:00:00:01:02:03 192.168.1.2"
+
+ADD_NAMESPACES(sw11)
+ADD_VETH(sw11, sw11, br-int, "192.168.2.2/24", "f0:00:00:02:02:03", \
+       "192.168.2.1")
+check ovn-nbctl lsp-add sw1 sw11 \
+    -- lsp-set-addresses sw11 "f0:00:00:02:02:03 192.168.2.2"
+
+NETNS_DAEMONIZE([sw01], [nc -k -l 8000], [nc-sw01.pid])
+
+test_ping() {
+    NS_CHECK_EXEC([$1],  [ping -q -c 1 $2 -w 2 | FORMAT_PING], \
+[0], [dnl
+1 packets transmitted, 1 received, 0% packet loss, time 0ms
+])
+}
+
+# Only SNAT
+check ovn-nbctl --wait=hv lr-nat-add R1 snat 172.16.1.21 192.168.2.0/24
+
+echo "foo" > foo
+NS_CHECK_EXEC([sw11], [nc 192.168.1.2 8000 < foo])
+test_ping sw11 192.168.1.2
+
+# Ensure nat has been hit
+OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep -v "n_packets=0" | grep 'nat(src=172.16.1.21)'])
+# Ensure conntrack entry is present
+OVS_WAIT_FOR_OUTPUT([
+    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
+      sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
+tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+AT_CHECK([ovs-appctl dpctl/flush-conntrack])
+
+# SNAT and DNAT. using Logical IP
+ovn-nbctl --wait=hv lr-nat-add R1 dnat_and_snat 172.16.1.2 192.168.1.2
+NS_CHECK_EXEC([sw11], [nc 192.168.1.2 8000 < foo ])
+test_ping sw11 192.168.1.2
+
+# Ensure conntrack entry is present
+OVS_WAIT_FOR_OUTPUT([
+    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
+      sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
+tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+AT_CHECK([ovs-appctl dpctl/flush-conntrack])
+
+# SNAT and DNAT. using floating IP
+NS_CHECK_EXEC([sw11], [nc 172.16.1.2 8000 < foo ])
+test_ping sw11 172.16.1.2
+
+# Ensure conntrack entry is present
+OVS_WAIT_FOR_OUTPUT([
+    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
+      sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmp,orig=(src=192.168.2.2,dst=172.16.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
+tcp,orig=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+AT_CHECK([ovs-appctl dpctl/flush-conntrack])
+
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+
+AT_CLEANUP
+])
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index 7bcc2c66a..7fcfa50d4 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -2856,7 +2856,7 @@ nbctl_lb_add(struct ctl_context *ctx)
     }
 
     struct sockaddr_storage ss_vip;
-    if (!inet_parse_active(lb_vip, 0, &ss_vip, false)) {
+    if (!inet_parse_active(lb_vip, 0, &ss_vip, false, NULL)) {
         ctl_error(ctx, "%s: should be an IP address (or an IP address "
                   "and a port number with : as a separator).", lb_vip);
         return;
@@ -2886,7 +2886,7 @@ nbctl_lb_add(struct ctl_context *ctx)
         struct sockaddr_storage ss_dst;
 
         if (lb_vip_port) {
-            if (!inet_parse_active(token, -1, &ss_dst, false)) {
+            if (!inet_parse_active(token, -1, &ss_dst, false, NULL)) {
                 ctl_error(ctx, "%s: should be an IP address and a port "
                           "number with : as a separator.", token);
                 goto out;
@@ -3032,7 +3032,7 @@ lb_info_add_smap(const struct nbrec_load_balancer *lb,
             const struct smap_node *node = nodes[i];
 
             struct sockaddr_storage ss;
-            if (!inet_parse_active(node->key, 0, &ss, false)) {
+            if (!inet_parse_active(node->key, 0, &ss, false, NULL)) {
                 continue;
             }
 
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index ece5803f2..096d691d5 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -214,7 +214,7 @@ static void
 parse_lb_option(const char *s)
 {
     struct sockaddr_storage ss;
-    if (!inet_parse_active(s, 0, &ss, false)) {
+    if (!inet_parse_active(s, 0, &ss, false, NULL)) {
         ovs_fatal(0, "%s: bad address", s);
     }
 
@@ -1388,7 +1388,8 @@ ovntrace_node_prune_summary(struct ovs_list *nodes)
             sub->type == OVNTRACE_NODE_TABLE) {
             /* Replace 'sub' by its children, if any, */
             ovs_list_remove(&sub->node);
-            ovs_list_splice(&next->node, sub->subs.next, &sub->subs);
+            ovs_list_splice(next ? &next->node : nodes, sub->subs.next,
+                            &sub->subs);
             ovntrace_node_destroy(sub);
         }
     }
@@ -1432,7 +1433,8 @@ ovntrace_node_prune_hard(struct ovs_list *nodes)
             sub->type == OVNTRACE_NODE_OUTPUT) {
             /* Replace 'sub' by its children, if any, */
             ovs_list_remove(&sub->node);
-            ovs_list_splice(&next->node, sub->subs.next, &sub->subs);
+            ovs_list_splice(next ? &next->node : nodes, sub->subs.next,
+                            &sub->subs);
             ovntrace_node_destroy(sub);
         }
     }