Blob Blame History Raw
diff --git a/.ci/ci.sh b/.ci/ci.sh
index 10f11939c..3f1b41ead 100755
--- a/.ci/ci.sh
+++ b/.ci/ci.sh
@@ -23,7 +23,7 @@ CONTAINER_WORKDIR="/workspace/ovn-tmp"
 IMAGE_NAME=${IMAGE_NAME:-"ovn-org/ovn-tests"}
 
 # Test variables
-ARCH=${ARCH:-$(uname -i)}
+ARCH=${ARCH:-$(uname -m)}
 CC=${CC:-gcc}
 
 
@@ -105,6 +105,16 @@ function run_tests() {
     "
 }
 
+function check_clang_version_ge() {
+    lower=$1
+    version=$(clang --version | head -n1 | cut -d' ' -f3)
+    if ! echo -e "$lower\n$version" | sort -CV; then
+      return 1
+    fi
+
+    return 0
+}
+
 options=$(getopt --options "" \
     --long help,shell,archive-logs,jobs:,ovn-path:,ovs-path:,image-name:\
     -- "${@}")
@@ -149,7 +159,7 @@ while true; do
 done
 
 # Workaround for https://bugzilla.redhat.com/2153359
-if [ "$ARCH" = "aarch64" ]; then
+if [ "$ARCH" = "aarch64" ] && ! check_clang_version_ge "16.0.0"; then
     ASAN_OPTIONS="detect_leaks=0"
 fi
 
diff --git a/.ci/linux-build.sh b/.ci/linux-build.sh
index 5a79a52da..7270b9e60 100755
--- a/.ci/linux-build.sh
+++ b/.ci/linux-build.sh
@@ -80,7 +80,7 @@ function configure_gcc()
             # We should install gcc-multilib for x86 build, we cannot
             # do it directly because gcc-multilib is not available
             # for arm64
-            sudo apt install -y gcc-multilib
+            sudo apt update && sudo apt install -y gcc-multilib
         fi
     fi
 }
diff --git a/.ci/linux-util.sh b/.ci/linux-util.sh
new file mode 100755
index 000000000..920345123
--- /dev/null
+++ b/.ci/linux-util.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+function free_up_disk_space_ubuntu()
+{
+    local pkgs='azure-cli aspnetcore-* dotnet-* ghc-* firefox*
+                google-chrome-stable google-cloud-cli libmono-* llvm-*
+                microsoft-edge-stable mono-* msbuild mysql-server-core-*
+                php-* php7* powershell* temurin-* zulu-*'
+
+    # Use apt patterns to only select real packages that match the names
+    # in the list above.
+    local pkgs=$(echo $pkgs | sed 's/[^ ]* */~n&/g')
+
+    sudo apt update && sudo apt-get --auto-remove -y purge $pkgs
+
+    local paths='/usr/local/lib/android/ /usr/share/dotnet/ /opt/ghc/
+                 /usr/local/share/boost/'
+    sudo rm -rf $paths
+}
diff --git a/.ci/ovn-kubernetes/Dockerfile b/.ci/ovn-kubernetes/Dockerfile
index 12f819017..eda8b6d02 100644
--- a/.ci/ovn-kubernetes/Dockerfile
+++ b/.ci/ovn-kubernetes/Dockerfile
@@ -81,6 +81,7 @@ RUN dnf install -y *.rpm && rm -f *.rpm
 # install ovn-kubernetes binaries built in previous stage
 RUN mkdir -p /usr/libexec/cni/
 COPY --from=ovnkubebuilder /root/ovn-kubernetes/go-controller/_output/go/bin/ovnkube /usr/bin/
+COPY --from=ovnkubebuilder /root/ovn-kubernetes/go-controller/_output/go/bin/ovnkube-identity /usr/bin/
 COPY --from=ovnkubebuilder /root/ovn-kubernetes/go-controller/_output/go/bin/ovn-kube-util /usr/bin/
 COPY --from=ovnkubebuilder /root/ovn-kubernetes/go-controller/_output/go/bin/ovndbchecker /usr/bin/
 COPY --from=ovnkubebuilder /root/ovn-kubernetes/go-controller/_output/go/bin/ovn-k8s-cni-overlay /usr/libexec/cni/ovn-k8s-cni-overlay
diff --git a/.cirrus.yml b/.cirrus.yml
index bd4cd08aa..dd2aa1bfd 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -1,14 +1,33 @@
-arm_unit_tests_task:
+compute_engine_instance:
+  image_project: ubuntu-os-cloud
+  image: family/ubuntu-2304-arm64
+  architecture: arm64
+  platform: linux
+  memory: 4G
+
+# Run separate task for the image build, so it's running only once outside
+# the test matrix.
+build_image_task:
+  install_dependencies_script:
+    - sudo apt update
+    - sudo apt install -y podman make
+
+  build_container_script:
+    - cd utilities/containers
+    - make ubuntu
+    - podman save -o /tmp/image.tar ovn-org/ovn-tests:ubuntu
+
+  upload_image_script:
+    - curl -s -X POST -T /tmp/image.tar http://$CIRRUS_HTTP_CACHE_HOST/${CIRRUS_CHANGE_IN_REPO}
 
-  arm_container:
-    image: ghcr.io/ovn-org/ovn-tests:fedora
-    memory: 4G
-    cpu: 2
+arm_unit_tests_task:
+  depends_on:
+    - build_image
 
   env:
-    ARCH: aarch64
     CIRRUS_CLONE_SUBMODULES: true
     PATH: ${HOME}/bin:${HOME}/.local/bin:${PATH}
+    IMAGE_NAME: ovn-org/ovn-tests:ubuntu
     matrix:
       - CC: gcc
         TESTSUITE: test
@@ -31,5 +50,16 @@ arm_unit_tests_task:
 
   name: ARM64 ${CC} ${TESTSUITE} ${TEST_RANGE}
 
+  install_dependencies_script:
+    - sudo apt update
+    - sudo apt install -y podman
+
+  download_cache_script:
+    - curl http://$CIRRUS_HTTP_CACHE_HOST/${CIRRUS_CHANGE_IN_REPO} -o /tmp/image.tar
+
+  load_image_script:
+    - podman load -i /tmp/image.tar
+    - rm -rf /tmp/image.tar
+
   build_script:
-    - ./.ci/linux-build.sh
+    - ./.ci/ci.sh --archive-logs
diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml
index 57e815ed8..bdd118087 100644
--- a/.github/workflows/containers.yml
+++ b/.github/workflows/containers.yml
@@ -15,7 +15,7 @@ env:
 
 jobs:
   container:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-22.04
     strategy:
       matrix:
         distro: [ fedora, ubuntu ]
diff --git a/.github/workflows/ovn-fake-multinode-tests.yml b/.github/workflows/ovn-fake-multinode-tests.yml
index 75c5ca818..25610df53 100644
--- a/.github/workflows/ovn-fake-multinode-tests.yml
+++ b/.github/workflows/ovn-fake-multinode-tests.yml
@@ -13,7 +13,7 @@ concurrency:
 jobs:
   build:
     name: Build ovn-fake-multinode image
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-22.04
     strategy:
       matrix:
         cfg:
@@ -69,7 +69,7 @@ jobs:
         path: /tmp/_output/ovn_${{ matrix.cfg.branch }}_image.tar
 
   multinode-tests:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-22.04
     timeout-minutes: 15
     needs: [build]
     strategy:
@@ -99,13 +99,18 @@ jobs:
       XDG_RUNTIME_DIR: ''
 
     steps:
+    - name: Check out ovn
+      uses: actions/checkout@v3
+
     - name: install required dependencies
       run:  |
         sudo apt update || true
         sudo apt install -y ${{ env.dependencies }}
 
     - name: Free up disk space
-      run: sudo eatmydata apt-get remove --auto-remove -y aspnetcore-* dotnet-* libmono-* mono-* msbuild php-* php7* ghc-* zulu-*
+      run: |
+        . .ci/linux-util.sh
+        free_up_disk_space_ubuntu
 
     - uses: actions/download-artifact@v3
       with:
@@ -153,7 +158,7 @@ jobs:
     - name: set up python
       uses: actions/setup-python@v4
       with:
-        python-version: '3.x'
+        python-version: '3.12'
 
     - name: Check out ovn
       uses: actions/checkout@v3
diff --git a/.github/workflows/ovn-kubernetes.yml b/.github/workflows/ovn-kubernetes.yml
index 69ab0566d..1689396d6 100644
--- a/.github/workflows/ovn-kubernetes.yml
+++ b/.github/workflows/ovn-kubernetes.yml
@@ -24,7 +24,7 @@ env:
 jobs:
   build:
     name: Build
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-22.04
     steps:
     - name: Enable Docker experimental features
       run: |
@@ -62,7 +62,7 @@ jobs:
   e2e:
     name: e2e
     if: github.event_name != 'schedule'
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-22.04
     timeout-minutes: 220
     strategy:
       fail-fast: false
@@ -95,18 +95,14 @@ jobs:
       KIND_IPV6_SUPPORT: "${{ matrix.ipfamily == 'IPv6' || matrix.ipfamily == 'dualstack' }}"
     steps:
 
-    - name: Free up disk space
-      run: |
-        sudo eatmydata apt-get purge --auto-remove -y \
-          azure-cli aspnetcore-* dotnet-* ghc-* firefox \
-          google-chrome-stable google-cloud-sdk \
-          llvm-* microsoft-edge-stable mono-* \
-          msbuild mysql-server-core-* php-* php7* \
-          powershell temurin-* zulu-*
-
     - name: Check out ovn
       uses: actions/checkout@v3
 
+    - name: Free up disk space
+      run: |
+        . .ci/linux-util.sh
+        free_up_disk_space_ubuntu
+
     - name: Check out ovn-kubernetes
       uses: actions/checkout@v3
       with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fe2a14c40..aa01dc7e9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -80,10 +80,62 @@ jobs:
       if: steps.dpdk_cache.outputs.cache-hit != 'true'
       run: ./.ci/dpdk-build.sh
 
+  prepare-container:
+    # This job has the following matrix, x: Job trigger, y: Branch
+    # (scheduled jobs run only on main):
+    # +-------+-------------------+-------------------+
+    # |       | Push/Pull request |     Scheduled     |
+    # +-------+-------------------+-------------------+
+    # |  main |  ghcr.io - Ubuntu | ghcr.io - Fedora  |
+    # +-------+-------------------+-------------------+
+    # | !main |  Builds - Ubuntu  | xxxxxxxxxxxxxxxxx |
+    # +-------+-------------------+-------------------+
+    env:
+      DEPENDENCIES: podman
+    name: Prepare container
+    runs-on: ubuntu-22.04
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Update APT cache
+        run: sudo apt update
+
+      - name: Install dependencies
+        run: sudo apt install -y ${{ env.DEPENDENCIES }}
+
+      - name: Choose image distro
+        if: github.event_name == 'push' || github.event_name == 'pull_request'
+        run: |
+          echo "IMAGE_DISTRO=ubuntu" >> $GITHUB_ENV
+
+      - name: Choose image distro
+        if: github.event_name == 'schedule'
+        run: |
+          echo "IMAGE_DISTRO=fedora" >> $GITHUB_ENV
+
+      - name: Build container
+        if: github.ref_name != 'main'
+        run: make ${{ env.IMAGE_DISTRO }}
+        working-directory: utilities/containers
+
+      - name: Download container
+        if: github.ref_name == 'main'
+        run: podman pull ghcr.io/ovn-org/ovn-tests:${{ env.IMAGE_DISTRO }}
+
+      - name: Export image
+        run: podman save -o /tmp/image.tar ovn-org/ovn-tests
+
+      - name: Cache image
+        id: image_cache
+        uses: actions/cache@v3
+        with:
+          path: /tmp/image.tar
+          key: ${{ github.sha }}
+
   build-linux:
-    needs: build-dpdk
+    needs: [build-dpdk, prepare-container]
     env:
-      IMAGE_NAME:  ghcr.io/ovn-org/ovn-tests:ubuntu
       ARCH:        ${{ matrix.cfg.arch }}
       CC:          ${{ matrix.cfg.compiler }}
       DPDK:        ${{ matrix.cfg.dpdk }}
@@ -94,7 +146,7 @@ jobs:
       SANITIZERS:  ${{ matrix.cfg.sanitizers }}
 
     name: linux ${{ join(matrix.cfg.*, ' ') }}
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-22.04
 
     strategy:
       fail-fast: false
@@ -157,13 +209,25 @@ jobs:
             sort -V | tail -1)
       working-directory: ovs
 
-    - name: cache
+    - name: cache dpdk
       if: matrix.cfg.dpdk != ''
       uses: actions/cache@v3
       with:
         path: dpdk-dir
         key: ${{ needs.build-dpdk.outputs.dpdk_key }}
 
+    - name: image cache
+      uses: actions/cache@v3
+      with:
+        path: /tmp/image.tar
+        key: ${{ github.sha }}
+
+    - name: load image
+      run: |
+        sudo podman load -i /tmp/image.tar
+        podman load -i /tmp/image.tar
+        rm -rf /tmp/image.tar
+
     - name: build
       if: ${{ startsWith(matrix.cfg.testsuite, 'system-test') }}
       run: sudo -E ./.ci/ci.sh --archive-logs
@@ -225,7 +289,7 @@ jobs:
     - name: set up python
       uses: actions/setup-python@v4
       with:
-        python-version: '3.x'
+        python-version: '3.12'
     - name: prepare
       run:  ./.ci/osx-prepare.sh
     - name: build
@@ -239,8 +303,8 @@ jobs:
 
   build-linux-rpm:
     name: linux rpm fedora
-    runs-on: ubuntu-latest
-    container: fedora:latest
+    runs-on: ubuntu-22.04
+    container: fedora:38
     timeout-minutes: 30
 
     strategy:
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 000000000..8c451663a
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,26 @@
+# .readthedocs.yaml
+# Read the Docs configuration file.
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details.
+
+# Required.
+version: 2
+
+# Set the OS, Python version, etc.
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.12"
+
+# Build documentation in the "Documentation/" directory with Sphinx.
+sphinx:
+  configuration: Documentation/conf.py
+  # Default HTML builder.
+  builder: "html"
+
+# Build all formats: HTML, PDF, ePub.
+formats: all
+
+# Declare the Python requirements.
+python:
+  install:
+  - requirements: Documentation/requirements.txt
diff --git a/Documentation/conf.py b/Documentation/conf.py
index f7eceaec8..f8fc0125f 100644
--- a/Documentation/conf.py
+++ b/Documentation/conf.py
@@ -12,16 +12,14 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
+import importlib
 import string
 import sys
 
-try:
-    import ovs_sphinx_theme
-    use_ovs_theme = True
-except ImportError:
-    print("Cannot find 'ovs-sphinx-theme' package. "
+use_rtd_theme = importlib.util.find_spec('sphinx_rtd_theme') is not None
+if not use_rtd_theme:
+    print("Cannot find 'sphinx_rtd_theme' package. "
           "Falling back to default theme.")
-    use_ovs_theme = False
 
 # -- General configuration ------------------------------------------------
 
@@ -48,7 +46,7 @@ master_doc = 'contents'
 
 # General information about the project.
 project = u'Open Virtual Network (OVN)'
-copyright = u'2020, The Open Virtual Network (OVN) Development Community'
+copyright = u'2020-2023, The Open Virtual Network (OVN) Development Community'
 author = u'The Open Virtual Network (OVN) Development Community'
 
 # The version info for the project you're documenting, acts as replacement for
@@ -89,14 +87,8 @@ linkcheck_anchors = False
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-if use_ovs_theme:
-    html_theme = 'ovs'
-
-# Add any paths that contain custom themes here, relative to this directory.
-if use_ovs_theme:
-    html_theme_path = [ovs_sphinx_theme.get_theme_dir()]
-else:
-    html_theme_path = []
+if use_rtd_theme:
+    html_theme = 'sphinx_rtd_theme'
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
diff --git a/Documentation/internals/documentation.rst b/Documentation/internals/documentation.rst
index 72a120bef..59c18b3a2 100644
--- a/Documentation/internals/documentation.rst
+++ b/Documentation/internals/documentation.rst
@@ -41,17 +41,13 @@ variety of other output formats but also allows for things like
 cross-referencing and indexing. for more information on the two, refer to the
 :doc:`contributing/documentation-style`.
 
-ovs-sphinx-theme
+sphinx_rtd_theme
 ----------------
 
-The documentation uses its own theme, `ovs-sphinx-theme`, which can be found on
-GitHub__ and is published on pypi__. This is shared by Open vSwitch and OVN.
-It is packaged separately to ensure all documentation gets the latest version
-of the theme (assuming there are no major version bumps in that package). If
-building locally and the package is installed, it will be used. If the package
-is not installed, Sphinx will fallback to the default theme.
-
-The package is currently maintained by Stephen Finucane and Russell Bryant.
+The documentation uses `sphinx_rtd_theme`, which can be found on GitHub__ and
+is published on pypi__.  It is also packaged in major distributions.
+If building locally and the package is installed, it will be used.  If the
+package is not installed, Sphinx will fallback to the default theme.
 
 Read the Docs
 -------------
@@ -72,6 +68,6 @@ modifications to this site, refer to the `GitHub project`__.
 
 __ http://docutils.sourceforge.net/rst.html
 __ http://www.sphinx-doc.org/
-__ https://github.com/openvswitch/ovs-sphinx-theme
-__ https://pypi.python.org/pypi/ovs-sphinx-theme
+__ https://github.com/readthedocs/sphinx_rtd_theme
+__ https://pypi.python.org/pypi/sphinx_rtd_theme
 __ https://github.com/ovn-org/ovn-org.github.io
diff --git a/Documentation/internals/release-process.rst b/Documentation/internals/release-process.rst
index ec79fe48c..26d3f8d4d 100644
--- a/Documentation/internals/release-process.rst
+++ b/Documentation/internals/release-process.rst
@@ -64,6 +64,10 @@ Scheduling`_ for the timing of each stage:
    branch.  Features to be added to release branches should be limited in scope
    and risk and discussed on ovs-dev before creating the branch.
 
+   In order to keep the CI stable on the new release branch, the Ubuntu
+   container should be pinned to the current LTS version in the Dockerfile
+   e.g. registry.hub.docker.com/library/ubuntu:22.04.
+
 3. When committers come to rough consensus that the release is ready, they
    release the .0 release on its branch, e.g. 25.09.0 for branch-25.09.  To
    make the actual release, a committer pushes a signed tag named, e.g.
diff --git a/Documentation/intro/install/general.rst b/Documentation/intro/install/general.rst
index 589518846..dd8bf5c2c 100644
--- a/Documentation/intro/install/general.rst
+++ b/Documentation/intro/install/general.rst
@@ -134,10 +134,7 @@ following to obtain better warnings:
 
 - clang, version 3.4 or later
 
-- flake8 along with the hacking flake8 plugin (for Python code). The automatic
-  flake8 check that runs against Python code has some warnings enabled that
-  come from the "hacking" flake8 plugin. If it's not installed, the warnings
-  just won't occur until it's run on a system with "hacking" installed.
+- flake8 (for Python code)
 
 You may find the ovs-dev script found in ``ovs/utilities/ovs-dev.py`` useful.
 
diff --git a/Documentation/requirements.txt b/Documentation/requirements.txt
index 77130c6e0..63a7997f7 100644
--- a/Documentation/requirements.txt
+++ b/Documentation/requirements.txt
@@ -1,2 +1,2 @@
-sphinx>=1.1,<2.0
-ovs_sphinx_theme>=1.0,<1.1
+sphinx>=1.1
+sphinx_rtd_theme>=1.0,<2.0
diff --git a/Makefile.am b/Makefile.am
index b58d4a501..bfc9565e8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -90,6 +90,7 @@ EXTRA_DIST = \
 	.ci/dpdk-build.sh \
 	.ci/dpdk-prepare.sh \
 	.ci/linux-build.sh \
+	.ci/linux-util.sh \
 	.ci/osx-build.sh \
 	.ci/osx-prepare.sh \
 	.ci/ovn-kubernetes/Dockerfile \
@@ -99,6 +100,7 @@ EXTRA_DIST = \
 	.github/workflows/test.yml \
 	.github/workflows/ovn-kubernetes.yml \
 	.github/workflows/ovn-fake-multinode-tests.yml \
+	.readthedocs.yaml \
 	boot.sh \
 	$(MAN_FRAGMENTS) \
 	$(MAN_ROOTS) \
@@ -414,16 +416,10 @@ ALL_LOCAL += flake8-check
 #   F811 redefinition of unused <name> from line <N> (only from flake8 v2.0)
 # D*** -- warnings from flake8-docstrings plugin
 # H*** -- warnings from flake8 hacking plugin (custom style checks beyond PEP8)
-#   H231 Python 3.x incompatible 'except x,y:' construct
-#   H232 Python 3.x incompatible octal 077 should be written as 0o77
-#   H233 Python 3.x incompatible use of print operator
-#   H238 old style class declaration, use new style (inherit from `object`)
-FLAKE8_SELECT = H231,H232,H233,H238
 FLAKE8_IGNORE = E121,E123,E125,E126,E127,E128,E129,E131,E722,W503,W504,F811,D,H,I
 flake8-check: $(FLAKE8_PYFILES)
 	$(FLAKE8_WERROR)$(AM_V_GEN) \
 	  src='$^' && \
-	  flake8 $$src --select=$(FLAKE8_SELECT) $(FLAKE8_FLAGS) && \
 	  flake8 $$src --ignore=$(FLAKE8_IGNORE) $(FLAKE8_FLAGS) && \
 	  touch $@
 endif
diff --git a/NEWS b/NEWS
index 75e046ab3..ed74d0f38 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,11 @@
+OVN v23.09.2 - xx xxx xxxx
+--------------------------
+
+
+OVN v23.09.1 - 01 Dec 2023
+--------------------------
+  - Bug fixes
+
 OVN v23.09.0 - 15 Sep 2023
 ----------------------------
   - Added FDB aging mechanism, that is disabled by default.
diff --git a/configure.ac b/configure.ac
index ab404b959..e4d5134dd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 AC_PREREQ(2.63)
-AC_INIT(ovn, 23.09.0, bugs@openvswitch.org)
+AC_INIT(ovn, 23.09.2, bugs@openvswitch.org)
 AC_CONFIG_MACRO_DIR([m4])
 AC_CONFIG_AUX_DIR([build-aux])
 AC_CONFIG_HEADERS([config.h])
diff --git a/controller-vtep/ovn-controller-vtep.c b/controller-vtep/ovn-controller-vtep.c
index 23b368179..4472e5285 100644
--- a/controller-vtep/ovn-controller-vtep.c
+++ b/controller-vtep/ovn-controller-vtep.c
@@ -306,10 +306,12 @@ parse_options(int argc, char *argv[])
 
         switch (c) {
         case 'd':
+            free(ovnsb_remote);
             ovnsb_remote = xstrdup(optarg);
             break;
 
         case 'D':
+            free(vtep_remote);
             vtep_remote = xstrdup(optarg);
             break;
 
diff --git a/controller/binding.c b/controller/binding.c
index a521f2828..2afc5d48a 100644
--- a/controller/binding.c
+++ b/controller/binding.c
@@ -55,8 +55,13 @@ struct claimed_port {
     long long int last_claimed;
 };
 
+struct qos_port {
+    bool added;
+};
+
 static struct shash _claimed_ports = SHASH_INITIALIZER(&_claimed_ports);
 static struct sset _postponed_ports = SSET_INITIALIZER(&_postponed_ports);
+static struct shash _qos_ports = SHASH_INITIALIZER(&_qos_ports);
 
 static void
 remove_additional_chassis(const struct sbrec_port_binding *pb,
@@ -218,6 +223,17 @@ get_qos_egress_port_interface(struct shash *bridge_mappings,
     return NULL;
 }
 
+static void
+add_or_del_qos_port(const char *ovn_port, bool add)
+{
+    struct qos_port *qos_port = shash_find_data(&_qos_ports, ovn_port);
+    if (!qos_port) {
+        qos_port = xzalloc(sizeof *qos_port);
+        shash_add(&_qos_ports, ovn_port, qos_port);
+    }
+    qos_port->added = add;
+}
+
 /* 34359738360 == (2^32 - 1) * 8.  netdev_set_qos() doesn't support
  * 64-bit rate netlink attributes, so the maximum value is 2^32 - 1
  * bytes. The 'max-rate' config option is in bits, so multiplying by 8.
@@ -225,7 +241,7 @@ get_qos_egress_port_interface(struct shash *bridge_mappings,
  * can be unrecognized for certain NICs or reported too low for virtual
  * interfaces. */
 #define OVN_QOS_MAX_RATE    34359738360ULL
-static void
+static bool
 add_ovs_qos_table_entry(struct ovsdb_idl_txn *ovs_idl_txn,
                         const struct ovsrec_port *port,
                         unsigned long long min_rate,
@@ -239,7 +255,7 @@ add_ovs_qos_table_entry(struct ovsdb_idl_txn *ovs_idl_txn,
     const struct ovsrec_qos *qos = port->qos;
     if (qos && !smap_get_bool(&qos->external_ids, "ovn_qos", false)) {
         /* External configured QoS, do not overwrite it. */
-        return;
+        return false;
     }
 
     if (!qos) {
@@ -282,22 +298,18 @@ add_ovs_qos_table_entry(struct ovsdb_idl_txn *ovs_idl_txn,
     ovsrec_queue_verify_external_ids(queue);
     ovsrec_queue_set_external_ids(queue, &external_ids);
     smap_destroy(&external_ids);
+    return true;
 }
 
 static void
-remove_stale_qos_entry(struct ovsdb_idl_txn *ovs_idl_txn,
-                       const struct sbrec_port_binding *pb,
+remove_stale_qos_entry( const char *logical_port,
                        struct ovsdb_idl_index *ovsrec_port_by_qos,
                        const struct ovsrec_qos_table *qos_table,
                        struct hmap *queue_map)
 {
-    if (!ovs_idl_txn) {
-        return;
-    }
-
     struct qos_queue *q = find_qos_queue(
-            queue_map, hash_string(pb->logical_port, 0),
-            pb->logical_port);
+            queue_map, hash_string(logical_port, 0),
+            logical_port);
     if (!q) {
         return;
     }
@@ -338,8 +350,12 @@ remove_stale_qos_entry(struct ovsdb_idl_txn *ovs_idl_txn,
 
 static void
 configure_qos(const struct sbrec_port_binding *pb,
-              struct binding_ctx_in *b_ctx_in,
-              struct binding_ctx_out *b_ctx_out)
+              struct ovsdb_idl_txn *ovs_idl_txn,
+              struct ovsdb_idl_index *ovsrec_port_by_qos,
+              const struct ovsrec_qos_table *qos_table,
+              struct hmap *qos_map,
+              const struct ovsrec_open_vswitch_table *ovs_table,
+              const struct ovsrec_bridge_table *bridge_table)
 {
     unsigned long long min_rate = smap_get_ullong(
             &pb->options, "qos_min_rate", 0);
@@ -351,20 +367,20 @@ configure_qos(const struct sbrec_port_binding *pb,
 
     if ((!min_rate && !max_rate && !burst) || !queue_id) {
         /* Qos is not configured for this port. */
-        remove_stale_qos_entry(b_ctx_in->ovs_idl_txn, pb,
-                               b_ctx_in->ovsrec_port_by_qos,
-                               b_ctx_in->qos_table, b_ctx_out->qos_map);
+        remove_stale_qos_entry(pb->logical_port,
+                               ovsrec_port_by_qos,
+                               qos_table, qos_map);
         return;
     }
 
     const char *network = smap_get(&pb->options, "qos_physical_network");
     uint32_t hash = hash_string(pb->logical_port, 0);
-    struct qos_queue *q = find_qos_queue(b_ctx_out->qos_map, hash,
+    struct qos_queue *q = find_qos_queue(qos_map, hash,
                                          pb->logical_port);
     if (!q || q->min_rate != min_rate || q->max_rate != max_rate ||
         q->burst != burst || (network && strcmp(network, q->network))) {
         struct shash bridge_mappings = SHASH_INITIALIZER(&bridge_mappings);
-        add_ovs_bridge_mappings(b_ctx_in->ovs_table, b_ctx_in->bridge_table,
+        add_ovs_bridge_mappings(ovs_table, bridge_table,
                                 &bridge_mappings);
 
         const struct ovsrec_port *port = NULL;
@@ -375,25 +391,58 @@ configure_qos(const struct sbrec_port_binding *pb,
         }
         if (iface) {
             /* Add new QoS entries. */
-            add_ovs_qos_table_entry(b_ctx_in->ovs_idl_txn, port, min_rate,
+            if (add_ovs_qos_table_entry(ovs_idl_txn, port, min_rate,
                                     max_rate, burst, queue_id,
-                                    pb->logical_port);
-            if (!q) {
-                q = xzalloc(sizeof *q);
-                hmap_insert(b_ctx_out->qos_map, &q->node, hash);
-                q->port = xstrdup(pb->logical_port);
-                q->queue_id = queue_id;
+                                    pb->logical_port)) {
+                if (!q) {
+                    q = xzalloc(sizeof *q);
+                    hmap_insert(qos_map, &q->node, hash);
+                    q->port = xstrdup(pb->logical_port);
+                    q->queue_id = queue_id;
+                }
+                free(q->network);
+                q->network = network ? xstrdup(network) : NULL;
+                q->min_rate = min_rate;
+                q->max_rate = max_rate;
+                q->burst = burst;
             }
-            free(q->network);
-            q->network = network ? xstrdup(network) : NULL;
-            q->min_rate = min_rate;
-            q->max_rate = max_rate;
-            q->burst = burst;
         }
         shash_destroy(&bridge_mappings);
     }
 }
 
+void
+update_qos(struct ovsdb_idl_index *sbrec_port_binding_by_name,
+           struct ovsdb_idl_txn *ovs_idl_txn,
+           struct ovsdb_idl_index *ovsrec_port_by_qos,
+           const struct ovsrec_qos_table *qos_table,
+           struct hmap *qos_map,
+           const struct ovsrec_open_vswitch_table *ovs_table,
+           const struct ovsrec_bridge_table *bridge_table)
+{
+    /* Remove qos for any ports for which we could not do it before */
+    const struct sbrec_port_binding *pb;
+
+    struct shash_node *node;
+    SHASH_FOR_EACH_SAFE (node, &_qos_ports) {
+        struct qos_port *qos_port = (struct qos_port *) node->data;
+        if (qos_port->added) {
+            pb = lport_lookup_by_name(sbrec_port_binding_by_name,
+                                      node->name);
+            if (pb) {
+                configure_qos(pb, ovs_idl_txn, ovsrec_port_by_qos, qos_table,
+                              qos_map, ovs_table, bridge_table);
+            }
+        } else {
+            remove_stale_qos_entry(node->name,
+                                   ovsrec_port_by_qos,
+                                   qos_table, qos_map);
+        }
+        free(qos_port);
+        shash_delete(&_qos_ports, node);
+    }
+}
+
 static const struct ovsrec_queue *
 find_qos_queue_by_external_ids(const struct smap *external_ids,
     struct ovsdb_idl_index *ovsrec_queue_by_external_ids)
@@ -1144,33 +1193,22 @@ local_binding_set_pb(struct shash *local_bindings, const char *pb_name,
  * - set the 'pb.up' field to true if 'parent_pb.up' is 'true' (e.g., for
  *   container and virtual ports).
  *
- * Returns false if lport is not claimed due to 'sb_readonly'.
- * Returns true otherwise.
- *
  * Note:
  *   Updates the 'pb->up' field only if it's explicitly set to 'false'.
  *   This is to ensure compatibility with older versions of ovn-northd.
  */
-static bool
+void
 claimed_lport_set_up(const struct sbrec_port_binding *pb,
-                     const struct sbrec_port_binding *parent_pb,
-                     bool sb_readonly)
+                     const struct sbrec_port_binding *parent_pb)
 {
-    /* When notify_up is false in claim_port(), no state is created
-     * by if_status_mgr. In such cases, return false (i.e. trigger recompute)
-     * if we can't update sb (because it is readonly).
-     */
     bool up = true;
     if (!parent_pb || (parent_pb->n_up && parent_pb->up[0])) {
-        if (!sb_readonly) {
-            if (pb->n_up) {
-                sbrec_port_binding_set_up(pb, &up, 1);
-            }
-        } else if (pb->n_up && !pb->up[0]) {
-            return false;
+        if (pb->n_up) {
+            VLOG_INFO("Setting lport %s up in Southbound",
+                      pb->logical_port);
+            sbrec_port_binding_set_up(pb, &up, 1);
         }
     }
-    return true;
 }
 
 typedef void (*set_func)(const struct sbrec_port_binding *pb,
@@ -1250,7 +1288,7 @@ remove_additional_chassis(const struct sbrec_port_binding *pb,
     remove_additional_encap_for_chassis(pb, chassis_rec);
 }
 
-static bool
+bool
 lport_maybe_postpone(const char *port_name, long long int now,
                      struct sset *postponed_ports)
 {
@@ -1273,7 +1311,7 @@ claim_lport(const struct sbrec_port_binding *pb,
             const struct sbrec_port_binding *parent_pb,
             const struct sbrec_chassis *chassis_rec,
             const struct ovsrec_interface *iface_rec,
-            bool sb_readonly, bool notify_up,
+            bool sb_readonly, bool is_vif,
             struct hmap *tracked_datapaths,
             struct if_status_mgr *if_mgr,
             struct sset *postponed_ports)
@@ -1298,39 +1336,27 @@ claim_lport(const struct sbrec_port_binding *pb,
             }
             update_tracked = true;
 
-            if (!notify_up) {
-                if (!claimed_lport_set_up(pb, parent_pb, sb_readonly)) {
-                    return false;
-                }
-                if (sb_readonly) {
-                    return false;
-                }
-                set_pb_chassis_in_sbrec(pb, chassis_rec, true);
-            } else {
-                if_status_mgr_claim_iface(if_mgr, pb, chassis_rec, iface_rec,
-                                          sb_readonly, can_bind);
-            }
+            if_status_mgr_claim_iface(if_mgr, pb, chassis_rec, iface_rec,
+                                      sb_readonly, can_bind, is_vif,
+                                      parent_pb);
             register_claim_timestamp(pb->logical_port, now);
             sset_find_and_delete(postponed_ports, pb->logical_port);
         } else {
-            if (!notify_up) {
-                if (!claimed_lport_set_up(pb, parent_pb, sb_readonly)) {
-                    return false;
-                }
-            } else {
-                if ((pb->n_up && !pb->up[0]) ||
-                    !smap_get_bool(&iface_rec->external_ids,
-                                   OVN_INSTALLED_EXT_ID, false)) {
-                    if_status_mgr_claim_iface(if_mgr, pb, chassis_rec,
-                                              iface_rec, sb_readonly,
-                                              can_bind);
-                }
+            update_tracked = true;
+            if ((pb->n_up && !pb->up[0]) ||
+                (is_vif && !smap_get_bool(&iface_rec->external_ids,
+                                          OVN_INSTALLED_EXT_ID, false))) {
+                if_status_mgr_claim_iface(if_mgr, pb, chassis_rec,
+                                          iface_rec, sb_readonly,
+                                          can_bind, is_vif,
+                                          parent_pb);
             }
         }
     } else if (can_bind == CAN_BIND_AS_ADDITIONAL) {
         if (!is_additional_chassis(pb, chassis_rec)) {
             if_status_mgr_claim_iface(if_mgr, pb, chassis_rec, iface_rec,
-                                      sb_readonly, can_bind);
+                                      sb_readonly, can_bind, is_vif,
+                                      parent_pb);
             update_tracked = true;
         }
     }
@@ -1524,8 +1550,8 @@ consider_vif_lport_(const struct sbrec_port_binding *pb,
                 tracked_datapath_lport_add(pb, TRACKED_RESOURCE_UPDATED,
                                            b_ctx_out->tracked_dp_bindings);
             }
-            if (b_lport->lbinding->iface && b_ctx_in->ovs_idl_txn) {
-                configure_qos(pb, b_ctx_in, b_ctx_out);
+            if (b_lport->lbinding->iface) {
+                add_or_del_qos_port(pb->logical_port, true);
             }
         } else {
             /* We could, but can't claim the lport. */
@@ -1814,8 +1840,10 @@ consider_nonvif_lport_(const struct sbrec_port_binding *pb,
                            b_ctx_out->postponed_ports);
     }
 
-    if (pb->chassis == b_ctx_in->chassis_rec ||
-            is_additional_chassis(pb, b_ctx_in->chassis_rec)) {
+    if (pb->chassis == b_ctx_in->chassis_rec
+            || is_additional_chassis(pb, b_ctx_in->chassis_rec)
+            || if_status_is_port_claimed(b_ctx_out->if_mgr,
+                                         pb->logical_port)) {
         return release_lport(pb, b_ctx_in->chassis_rec,
                              !b_ctx_in->ovnsb_idl_txn,
                              b_ctx_out->tracked_dp_bindings,
@@ -1851,7 +1879,6 @@ consider_l3gw_lport(const struct sbrec_port_binding *pb,
 
 static void
 consider_localnet_lport(const struct sbrec_port_binding *pb,
-                        struct binding_ctx_in *b_ctx_in,
                         struct binding_ctx_out *b_ctx_out)
 {
     struct local_datapath *ld
@@ -1860,14 +1887,23 @@ consider_localnet_lport(const struct sbrec_port_binding *pb,
     if (!ld) {
         return;
     }
+
+    bool pb_localnet_learn_fdb = smap_get_bool(&pb->options,
+                                               "localnet_learn_fdb", false);
+    if (pb_localnet_learn_fdb != b_ctx_out->localnet_learn_fdb) {
+        b_ctx_out->localnet_learn_fdb = pb_localnet_learn_fdb;
+        if (b_ctx_out->tracked_dp_bindings) {
+            b_ctx_out->localnet_learn_fdb_changed = true;
+            tracked_datapath_lport_add(pb, TRACKED_RESOURCE_UPDATED,
+                                       b_ctx_out->tracked_dp_bindings);
+        }
+    }
+
     /* Add all localnet ports to local_ifaces so that we allocate ct zones
      * for them. */
     update_local_lports(pb->logical_port, b_ctx_out);
 
-    if (b_ctx_in->ovs_idl_txn) {
-        configure_qos(pb, b_ctx_in, b_ctx_out);
-    }
-
+    add_or_del_qos_port(pb->logical_port, true);
     update_related_lport(pb, b_ctx_out);
 }
 
@@ -1988,6 +2024,7 @@ binding_run(struct binding_ctx_in *b_ctx_in, struct binding_ctx_out *b_ctx_out)
         return;
     }
 
+    shash_clear_free_data(&_qos_ports);
     struct shash bridge_mappings = SHASH_INITIALIZER(&bridge_mappings);
 
     if (b_ctx_in->br_int) {
@@ -2103,7 +2140,7 @@ binding_run(struct binding_ctx_in *b_ctx_in, struct binding_ctx_out *b_ctx_out)
      * accordingly. */
     struct lport *lnet_lport;
     LIST_FOR_EACH_POP (lnet_lport, list_node, &localnet_lports) {
-        consider_localnet_lport(lnet_lport->pb, b_ctx_in, b_ctx_out);
+        consider_localnet_lport(lnet_lport->pb, b_ctx_out);
         update_ld_localnet_port(lnet_lport->pb, &bridge_mappings,
                                 b_ctx_out->local_datapaths);
         free(lnet_lport);
@@ -2345,7 +2382,7 @@ consider_iface_release(const struct ovsrec_interface *iface_rec,
 
     lbinding = local_binding_find(local_bindings, iface_id);
 
-   if (lbinding) {
+    if (lbinding) {
         if (lbinding->multiple_bindings) {
             VLOG_INFO("Multiple bindings for %s: force recompute to clean up",
                       iface_id);
@@ -2365,54 +2402,50 @@ consider_iface_release(const struct ovsrec_interface *iface_rec,
                 return true;
             }
         }
-    }
 
-    struct binding_lport *b_lport =
-        local_binding_get_primary_or_localport_lport(lbinding);
-    if (is_binding_lport_this_chassis(b_lport, b_ctx_in->chassis_rec)) {
-        struct local_datapath *ld =
-            get_local_datapath(b_ctx_out->local_datapaths,
-                               b_lport->pb->datapath->tunnel_key);
-        if (ld) {
-            remove_pb_from_local_datapath(b_lport->pb,
-                                          b_ctx_out, ld);
-        }
-
-        remove_stale_qos_entry(b_ctx_in->ovs_idl_txn, b_lport->pb,
-                               b_ctx_in->ovsrec_port_by_qos,
-                               b_ctx_in->qos_table, b_ctx_out->qos_map);
+        struct binding_lport *b_lport =
+            local_binding_get_primary_or_localport_lport(lbinding);
 
-        /* Release the primary binding lport and other children lports if
-         * any. */
-        LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) {
-            if (!release_binding_lport(b_ctx_in->chassis_rec, b_lport,
-                                       !b_ctx_in->ovnsb_idl_txn,
-                                       b_ctx_out)) {
-                return false;
+        if (is_binding_lport_this_chassis(b_lport, b_ctx_in->chassis_rec)) {
+            struct local_datapath *ld =
+                get_local_datapath(b_ctx_out->local_datapaths,
+                                   b_lport->pb->datapath->tunnel_key);
+            if (ld) {
+                remove_pb_from_local_datapath(b_lport->pb,
+                                              b_ctx_out, ld);
             }
-        }
-        if (lbinding->iface && lbinding->iface->name) {
-            if_status_mgr_remove_ovn_installed(b_ctx_out->if_mgr,
-                                               lbinding->iface->name,
-                                               &lbinding->iface->header_.uuid);
-        }
+            add_or_del_qos_port(b_lport->pb->logical_port, false);
 
-    } else if (lbinding && b_lport && b_lport->type == LP_LOCALPORT) {
-        /* lbinding is associated with a localport.  Remove it from the
-         * related lports. */
-        remove_related_lport(b_lport->pb, b_ctx_out);
-    }
+            /* Release the primary binding lport and other children lports if
+             * any. */
+            LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) {
+                if (!release_binding_lport(b_ctx_in->chassis_rec, b_lport,
+                                           !b_ctx_in->ovnsb_idl_txn,
+                                           b_ctx_out)) {
+                    return false;
+                }
+            }
+            if (lbinding->iface && lbinding->iface->name) {
+                if_status_mgr_remove_ovn_installed(b_ctx_out->if_mgr,
+                                           lbinding->iface->name,
+                                           &lbinding->iface->header_.uuid);
+            }
 
-    if (lbinding) {
-        /* Clear the iface of the local binding. */
-        lbinding->iface = NULL;
-    }
+        } else if (b_lport && b_lport->type == LP_LOCALPORT) {
+            /* lbinding is associated with a localport.  Remove it from the
+             * related lports. */
+            remove_related_lport(b_lport->pb, b_ctx_out);
+        }
 
-    /* Check if the lbinding has children of type PB_CONTAINER.
-     * If so, don't delete the local_binding. */
-    if (lbinding && !is_lbinding_container_parent(lbinding)) {
-        local_binding_delete(lbinding, local_bindings, binding_lports,
-                             b_ctx_out->if_mgr);
+        /* Check if the lbinding has children of type PB_CONTAINER.
+         * If so, don't delete the local_binding. */
+        if (!is_lbinding_container_parent(lbinding)) {
+            local_binding_delete(lbinding, local_bindings, binding_lports,
+                                 b_ctx_out->if_mgr);
+        } else {
+            /* Clear the iface of the local binding. */
+            lbinding->iface = NULL;
+        }
     }
 
     remove_local_lports(iface_id, b_ctx_out);
@@ -2938,7 +2971,7 @@ handle_updated_port(struct binding_ctx_in *b_ctx_in,
         break;
 
     case LP_LOCALNET: {
-        consider_localnet_lport(pb, b_ctx_in, b_ctx_out);
+        consider_localnet_lport(pb, b_ctx_out);
 
         struct shash bridge_mappings =
             SHASH_INITIALIZER(&bridge_mappings);
@@ -3026,9 +3059,7 @@ binding_handle_port_binding_changes(struct binding_ctx_in *b_ctx_in,
             shash_add(&deleted_other_pbs, pb->logical_port, pb);
         }
 
-        remove_stale_qos_entry(b_ctx_in->ovs_idl_txn, pb,
-                               b_ctx_in->ovsrec_port_by_qos,
-                               b_ctx_in->qos_table, b_ctx_out->qos_map);
+        add_or_del_qos_port(pb->logical_port, false);
     }
 
     struct shash_node *node;
@@ -3140,7 +3171,7 @@ delete_done:
                 b_ctx_in->sbrec_port_binding_by_datapath) {
                 enum en_lport_type lport_type = get_lport_type(pb);
                 if (lport_type == LP_LOCALNET) {
-                    consider_localnet_lport(pb, b_ctx_in, b_ctx_out);
+                    consider_localnet_lport(pb, b_ctx_out);
                     update_ld_localnet_port(pb, &bridge_mappings,
                                             b_ctx_out->local_datapaths);
                 } else if (lport_type == LP_EXTERNAL) {
@@ -3207,7 +3238,7 @@ local_binding_delete(struct local_binding *lbinding,
                      struct if_status_mgr *if_mgr)
 {
     shash_find_and_delete(local_bindings, lbinding->name);
-    if_status_mgr_delete_iface(if_mgr, lbinding->name);
+    if_status_mgr_delete_iface(if_mgr, lbinding->name, lbinding->iface);
     local_binding_destroy(lbinding, binding_lports);
 }
 
@@ -3424,6 +3455,80 @@ port_binding_set_down(const struct sbrec_chassis *chassis_rec,
         }
 }
 
+void
+port_binding_set_up(const struct sbrec_chassis *chassis_rec,
+                    const struct sbrec_port_binding_table *pb_table,
+                    const char *iface_id,
+                    const struct uuid *pb_uuid)
+{
+    const struct sbrec_port_binding *pb =
+        sbrec_port_binding_table_get_for_uuid(pb_table, pb_uuid);
+    if (!pb) {
+        VLOG_DBG("port_binding already deleted for %s", iface_id);
+        return;
+    }
+    if (pb->n_up && !pb->up[0]) {
+        bool up = true;
+        sbrec_port_binding_set_up(pb, &up, 1);
+        VLOG_INFO("Setting lport %s up in Southbound", pb->logical_port);
+        set_pb_chassis_in_sbrec(pb, chassis_rec, true);
+    }
+}
+
+void
+port_binding_set_pb(const struct sbrec_chassis *chassis_rec,
+                    const struct sbrec_port_binding_table *pb_table,
+                    const char *iface_id,
+                    const struct uuid *pb_uuid,
+                    enum can_bind bind_type)
+{
+    const struct sbrec_port_binding *pb =
+        sbrec_port_binding_table_get_for_uuid(pb_table, pb_uuid);
+    if (!pb) {
+        VLOG_DBG("port_binding already deleted for %s", iface_id);
+        return;
+    }
+    if (bind_type == CAN_BIND_AS_MAIN) {
+        set_pb_chassis_in_sbrec(pb, chassis_rec, true);
+    } else  if (bind_type == CAN_BIND_AS_ADDITIONAL) {
+        set_pb_additional_chassis_in_sbrec(pb, chassis_rec, true);
+    }
+}
+
+bool
+port_binding_pb_chassis_is_set(
+    const struct sbrec_chassis *chassis_rec,
+    const struct sbrec_port_binding_table *pb_table,
+    const struct uuid *pb_uuid)
+{
+    const struct sbrec_port_binding *pb =
+        sbrec_port_binding_table_get_for_uuid(pb_table, pb_uuid);
+
+    if (pb && ((pb->chassis == chassis_rec) ||
+        is_additional_chassis(pb, chassis_rec))) {
+        return true;
+    }
+    return false;
+}
+
+bool
+port_binding_is_up(const struct sbrec_chassis *chassis_rec,
+                   const struct sbrec_port_binding_table *pb_table,
+                   const struct uuid *pb_uuid)
+{
+    const struct sbrec_port_binding *pb =
+        sbrec_port_binding_table_get_for_uuid(pb_table, pb_uuid);
+
+    if (!pb || pb->chassis != chassis_rec) {
+        return false;
+    }
+
+    if (pb->n_up && !pb->up[0]) {
+        return false;
+    }
+    return true;
+}
+
 static void
 binding_lport_set_up(struct binding_lport *b_lport, bool sb_readonly)
 {
@@ -3614,5 +3719,6 @@ void
 binding_destroy(void)
 {
     shash_destroy_free_data(&_claimed_ports);
+    shash_destroy_free_data(&_qos_ports);
     sset_clear(&_postponed_ports);
 }
diff --git a/controller/binding.h b/controller/binding.h
index 24bc84079..75e7a2679 100644
--- a/controller/binding.h
+++ b/controller/binding.h
@@ -108,6 +108,9 @@ struct binding_ctx_out {
     struct if_status_mgr *if_mgr;
 
     struct sset *postponed_ports;
+
+    bool localnet_learn_fdb;
+    bool localnet_learn_fdb_changed;
 };
 
 /* Local bindings. binding.c module binds the logical port (represented by
@@ -217,6 +220,18 @@ void port_binding_set_down(const struct sbrec_chassis *chassis_rec,
                            const struct sbrec_port_binding_table *pb_table,
                            const char *iface_id,
                            const struct uuid *pb_uuid);
+void port_binding_set_up(const struct sbrec_chassis *chassis_rec,
+                         const struct sbrec_port_binding_table *,
+                         const char *iface_id,
+                         const struct uuid *pb_uuid);
+void port_binding_set_pb(const struct sbrec_chassis *chassis_rec,
+                         const struct sbrec_port_binding_table *,
+                         const char *iface_id,
+                         const struct uuid *pb_uuid,
+                         enum can_bind bind_type);
+bool port_binding_pb_chassis_is_set(const struct sbrec_chassis *chassis_rec,
+                                    const struct sbrec_port_binding_table *,
+                                    const struct uuid *pb_uuid);
 
 /* This structure represents a logical port (or port binding)
  * which is associated with 'struct local_binding'.
@@ -250,4 +265,20 @@ void binding_destroy(void);
 
 void destroy_qos_map(struct hmap *);
 
+void update_qos(struct ovsdb_idl_index * sbrec_port_binding_by_name,
+                struct ovsdb_idl_txn *ovs_idl_txn,
+                struct ovsdb_idl_index *ovsrec_port_by_qos,
+                const struct ovsrec_qos_table *qos_table,
+                struct hmap *qos_map,
+                const struct ovsrec_open_vswitch_table *ovs_table,
+                const struct ovsrec_bridge_table *bridge_table);
+
+bool lport_maybe_postpone(const char *port_name, long long int now,
+                          struct sset *postponed_ports);
+
+void claimed_lport_set_up(const struct sbrec_port_binding *pb,
+                          const struct sbrec_port_binding *parent_pb);
+bool port_binding_is_up(const struct sbrec_chassis *chassis_rec,
+                        const struct sbrec_port_binding_table *,
+                        const struct uuid *pb_uuid);
 #endif /* controller/binding.h */
diff --git a/controller/chassis.c b/controller/chassis.c
index 031bd4463..a6f13ccc4 100644
--- a/controller/chassis.c
+++ b/controller/chassis.c
@@ -369,6 +369,7 @@ chassis_build_other_config(const struct ovs_chassis_cfg *ovs_cfg,
     smap_replace(config, OVN_FEATURE_MAC_BINDING_TIMESTAMP, "true");
     smap_replace(config, OVN_FEATURE_CT_LB_RELATED, "true");
     smap_replace(config, OVN_FEATURE_FDB_TIMESTAMP, "true");
+    smap_replace(config, OVN_FEATURE_LS_DPG_COLUMN, "true");
 }
 
 /*
@@ -502,6 +503,12 @@ chassis_other_config_changed(const struct ovs_chassis_cfg *ovs_cfg,
         return true;
     }
 
+    if (!smap_get_bool(&chassis_rec->other_config,
+                       OVN_FEATURE_LS_DPG_COLUMN,
+                       false)) {
+        return true;
+    }
+
     return false;
 }
 
@@ -632,6 +639,7 @@ update_supported_sset(struct sset *supported)
     sset_add(supported, OVN_FEATURE_MAC_BINDING_TIMESTAMP);
     sset_add(supported, OVN_FEATURE_CT_LB_RELATED);
     sset_add(supported, OVN_FEATURE_FDB_TIMESTAMP);
+    sset_add(supported, OVN_FEATURE_LS_DPG_COLUMN);
 }
 
 static void
diff --git a/controller/if-status.c b/controller/if-status.c
index 6c5afc866..6e8aa1f7e 100644
--- a/controller/if-status.c
+++ b/controller/if-status.c
@@ -177,7 +177,9 @@ static const char *if_state_names[] = {
 
 struct ovs_iface {
     char *id;               /* Extracted from OVS external_ids.iface_id. */
+    char *name;             /* OVS iface name. */
     struct uuid pb_uuid;    /* Port_binding uuid */
+    struct uuid parent_pb_uuid; /* Parent port_binding uuid */
     enum if_state state;    /* State of the interface in the state machine. */
     uint32_t install_seqno; /* Seqno at which this interface is expected to
                              * be fully programmed in OVS.  Only used in state
@@ -185,6 +187,7 @@ struct ovs_iface {
                              */
     uint16_t mtu;           /* Extracted from OVS interface.mtu field. */
     enum can_bind bind_type;/* CAN_BIND_AS_MAIN or CAN_BIND_AS_ADDITIONAL */
+    bool is_vif;            /* Vifs, container or virtual ports */
 };
 
 static uint64_t ifaces_usage;
@@ -224,6 +227,7 @@ static void if_status_mgr_update_bindings(
     struct if_status_mgr *mgr, struct local_binding_data *binding_data,
     const struct sbrec_chassis *,
     const struct ovsrec_interface_table *iface_table,
+    const struct sbrec_port_binding_table *pb_table,
     bool sb_readonly, bool ovs_readonly);
 
 static void ovn_uninstall_hash_account_mem(const char *name, bool erase);
@@ -273,12 +277,23 @@ if_status_mgr_destroy(struct if_status_mgr *mgr)
     free(mgr);
 }
 
+/* is_vif controls whether we wait for flows to be updated
+ * before setting the interface up. It is true for VIF, CONTAINER
+ * and VIRTUAL ports.
+ * Non-VIF ports are reported up as soon as they are claimed
+ * to maintain compatibility with older versions.
+ * See aae25e6 binding: Correctly set Port_Binding.up for container/virtual
+ * ports.
+ */
+
 void
 if_status_mgr_claim_iface(struct if_status_mgr *mgr,
                           const struct sbrec_port_binding *pb,
                           const struct sbrec_chassis *chassis_rec,
                           const struct ovsrec_interface *iface_rec,
-                          bool sb_readonly, enum can_bind bind_type)
+                          bool sb_readonly, enum can_bind bind_type,
+                          bool is_vif,
+                          const struct sbrec_port_binding *parent_pb)
 {
     const char *iface_id = pb->logical_port;
     struct ovs_iface *iface = shash_find_data(&mgr->ifaces, iface_id);
@@ -287,8 +302,13 @@ if_status_mgr_claim_iface(struct if_status_mgr *mgr,
         iface = ovs_iface_create(mgr, iface_id, iface_rec, OIF_CLAIMED);
     }
     iface->bind_type = bind_type;
+    iface->is_vif = is_vif;
 
     memcpy(&iface->pb_uuid, &pb->header_.uuid, sizeof(iface->pb_uuid));
+    if (parent_pb) {
+        memcpy(&iface->parent_pb_uuid, &parent_pb->header_.uuid,
+               sizeof(iface->pb_uuid));
+    }
     if (!sb_readonly) {
         if (bind_type == CAN_BIND_AS_MAIN) {
             set_pb_chassis_in_sbrec(pb, chassis_rec, true);
@@ -357,7 +377,8 @@ if_status_mgr_release_iface(struct if_status_mgr *mgr, const char *iface_id)
 }
 
 void
-if_status_mgr_delete_iface(struct if_status_mgr *mgr, const char *iface_id)
+if_status_mgr_delete_iface(struct if_status_mgr *mgr, const char *iface_id,
+                           const struct ovsrec_interface *iface_rec)
 {
     struct ovs_iface *iface = shash_find_data(&mgr->ifaces, iface_id);
 
@@ -365,6 +386,12 @@ if_status_mgr_delete_iface(struct if_status_mgr *mgr, const char *iface_id)
         return;
     }
 
+    if (iface_rec && strcmp(iface->name, iface_rec->name)) {
+        VLOG_DBG("Interface %s not deleted as port %s bound to %s",
+                 iface_rec->name, iface_id, iface->name);
+        return;
+    }
+
     switch (iface->state) {
     case OIF_CLAIMED:
     case OIF_INSTALL_FLOWS:
@@ -394,6 +421,7 @@ if_status_handle_claims(struct if_status_mgr *mgr,
                         struct local_binding_data *binding_data,
                         const struct sbrec_chassis *chassis_rec,
                         struct hmap *tracked_datapath,
+                        const struct sbrec_port_binding_table *pb_table,
                         bool sb_readonly)
 {
     if (!binding_data || sb_readonly) {
@@ -406,9 +434,15 @@ if_status_handle_claims(struct if_status_mgr *mgr,
     bool rc = false;
     HMAPX_FOR_EACH (node, &mgr->ifaces_per_state[OIF_CLAIMED]) {
         struct ovs_iface *iface = node->data;
-        VLOG_INFO("if_status_handle_claims for %s", iface->id);
-        local_binding_set_pb(bindings, iface->id, chassis_rec,
-                             tracked_datapath, true, iface->bind_type);
+        VLOG_DBG("if_status_handle_claims for %s, is_vif = %d", iface->id,
+                  iface->is_vif);
+        if (iface->is_vif) {
+            local_binding_set_pb(bindings, iface->id, chassis_rec,
+                                 tracked_datapath, true, iface->bind_type);
+        } else {
+            port_binding_set_pb(chassis_rec, pb_table, iface->id,
+                                &iface->pb_uuid, iface->bind_type);
+        }
         rc = true;
     }
     return rc;
@@ -470,18 +504,37 @@ if_status_mgr_update(struct if_status_mgr *mgr,
      */
     HMAPX_FOR_EACH_SAFE (node, &mgr->ifaces_per_state[OIF_MARK_UP]) {
         struct ovs_iface *iface = node->data;
-
-        if (!local_bindings_pb_chassis_is_set(bindings, iface->id,
-            chassis_rec)) {
-            if (!sb_readonly) {
-                local_binding_set_pb(bindings, iface->id, chassis_rec,
-                                     NULL, true, iface->bind_type);
-            } else {
-                continue;
+        if (iface->is_vif) {
+            if (!local_bindings_pb_chassis_is_set(bindings, iface->id,
+                chassis_rec)) {
+                if (!sb_readonly) {
+                    long long int now = time_msec();
+                    if (lport_maybe_postpone(iface->id, now,
+                                             get_postponed_ports())) {
+                        continue;
+                    }
+                    local_binding_set_pb(bindings, iface->id, chassis_rec,
+                                         NULL, true, iface->bind_type);
+                } else {
+                    continue;
+                }
+            }
+            if (local_binding_is_up(bindings, iface->id, chassis_rec)) {
+                ovs_iface_set_state(mgr, iface, OIF_INSTALLED);
+            }
+        } else {
+            if (!port_binding_pb_chassis_is_set(chassis_rec, pb_table,
+                                                &iface->pb_uuid)) {
+                if (!sb_readonly) {
+                    port_binding_set_pb(chassis_rec, pb_table, iface->id,
+                                        &iface->pb_uuid, iface->bind_type);
+                } else {
+                    continue;
+                }
+            }
+            if (port_binding_is_up(chassis_rec, pb_table, &iface->pb_uuid)) {
+                ovs_iface_set_state(mgr, iface, OIF_INSTALLED);
             }
-        }
-        if (local_binding_is_up(bindings, iface->id, chassis_rec)) {
-            ovs_iface_set_state(mgr, iface, OIF_INSTALLED);
         }
     }
 
@@ -510,9 +563,13 @@ if_status_mgr_update(struct if_status_mgr *mgr,
     if (!sb_readonly) {
         HMAPX_FOR_EACH_SAFE (node, &mgr->ifaces_per_state[OIF_INSTALL_FLOWS]) {
             struct ovs_iface *iface = node->data;
-
             if (!local_bindings_pb_chassis_is_set(bindings, iface->id,
                 chassis_rec)) {
+                long long int now = time_msec();
+                if (lport_maybe_postpone(iface->id, now,
+                                         get_postponed_ports())) {
+                    continue;
+                }
                 local_binding_set_pb(bindings, iface->id, chassis_rec,
                                      NULL, true, iface->bind_type);
             }
@@ -528,9 +585,13 @@ if_status_mgr_update(struct if_status_mgr *mgr,
             /* No need to to update pb->chassis as already done
              * in if_status_handle_claims or if_status_mgr_claim_iface
              */
-            ovs_iface_set_state(mgr, iface, OIF_INSTALL_FLOWS);
-            iface->install_seqno = mgr->iface_seqno + 1;
-            new_ifaces = true;
+            if (iface->is_vif) {
+                ovs_iface_set_state(mgr, iface, OIF_INSTALL_FLOWS);
+                iface->install_seqno = mgr->iface_seqno + 1;
+                new_ifaces = true;
+            } else {
+                ovs_iface_set_state(mgr, iface, OIF_MARK_UP);
+            }
         }
     } else {
         HMAPX_FOR_EACH_SAFE (node, &mgr->ifaces_per_state[OIF_CLAIMED]) {
@@ -587,6 +648,7 @@ if_status_mgr_run(struct if_status_mgr *mgr,
                   struct local_binding_data *binding_data,
                   const struct sbrec_chassis *chassis_rec,
                   const struct ovsrec_interface_table *iface_table,
+                  const struct sbrec_port_binding_table *pb_table,
                   bool sb_readonly, bool ovs_readonly)
 {
     struct ofctrl_acked_seqnos *acked_seqnos =
@@ -622,15 +684,18 @@ if_status_mgr_run(struct if_status_mgr *mgr,
 
     /* Update binding states. */
     if_status_mgr_update_bindings(mgr, binding_data, chassis_rec,
-                                  iface_table,
+                                  iface_table, pb_table,
                                   sb_readonly, ovs_readonly);
 }
 
 static void
-ovs_iface_account_mem(const char *iface_id, bool erase)
+ovs_iface_account_mem(const char *iface_id, char *iface_name, bool erase)
 {
     uint32_t size = (strlen(iface_id) + sizeof(struct ovs_iface) +
                      sizeof(struct shash_node));
+    if (iface_name) {
+        size += strlen(iface_name);
+    }
     if (erase) {
         ifaces_usage -= size;
     } else {
@@ -682,12 +747,18 @@ ovs_iface_create(struct if_status_mgr *mgr, const char *iface_id,
 {
     struct ovs_iface *iface = xzalloc(sizeof *iface);
 
-    VLOG_DBG("Interface %s create.", iface_id);
+    VLOG_DBG("Interface %s create for iface %s.", iface_id,
+             iface_rec ? iface_rec->name : "");
     iface->id = xstrdup(iface_id);
     shash_add_nocopy(&mgr->ifaces, iface->id, iface);
     ovs_iface_set_state(mgr, iface, state);
-    ovs_iface_account_mem(iface_id, false);
-    if_status_mgr_iface_update(mgr, iface_rec);
+    if (iface_rec) {
+        ovs_iface_account_mem(iface_id, iface_rec->name, false);
+        if_status_mgr_iface_update(mgr, iface_rec);
+        iface->name = xstrdup(iface_rec->name);
+    } else {
+        ovs_iface_account_mem(iface_id, NULL, false);
+    }
     return iface;
 }
 
@@ -711,7 +782,8 @@ ovs_iface_destroy(struct if_status_mgr *mgr, struct ovs_iface *iface)
     if (node) {
         shash_steal(&mgr->ifaces, node);
     }
-    ovs_iface_account_mem(iface->id, true);
+    ovs_iface_account_mem(iface->id, iface->name, true);
+    free(iface->name);
     free(iface->id);
     free(iface);
 }
@@ -752,6 +824,7 @@ if_status_mgr_update_bindings(struct if_status_mgr *mgr,
                               struct local_binding_data *binding_data,
                               const struct sbrec_chassis *chassis_rec,
                               const struct ovsrec_interface_table *iface_table,
+                              const struct sbrec_port_binding_table *pb_table,
                               bool sb_readonly, bool ovs_readonly)
 {
     if (!binding_data) {
@@ -788,9 +861,20 @@ if_status_mgr_update_bindings(struct if_status_mgr *mgr,
     char *ts_now_str = xasprintf("%lld", time_wall_msec());
     HMAPX_FOR_EACH (node, &mgr->ifaces_per_state[OIF_MARK_UP]) {
         struct ovs_iface *iface = node->data;
-
-        local_binding_set_up(bindings, iface->id, chassis_rec, ts_now_str,
-                             sb_readonly, ovs_readonly);
+        if (iface->is_vif) {
+            local_binding_set_up(bindings, iface->id, chassis_rec, ts_now_str,
+                                 sb_readonly, ovs_readonly);
+        } else if (!sb_readonly) {
+            const struct sbrec_port_binding *pb =
+                sbrec_port_binding_table_get_for_uuid(pb_table,
+                                                      &iface->pb_uuid);
+            if (pb) {
+                const struct sbrec_port_binding *parent_pb =
+                    sbrec_port_binding_table_get_for_uuid(pb_table,
+                                                  &iface->parent_pb_uuid);
+                claimed_lport_set_up(pb, parent_pb);
+            }
+        }
     }
     free(ts_now_str);
 
diff --git a/controller/if-status.h b/controller/if-status.h
index 9714f6d8d..4ae5ad481 100644
--- a/controller/if-status.h
+++ b/controller/if-status.h
@@ -32,9 +32,12 @@ void if_status_mgr_claim_iface(struct if_status_mgr *,
                                const struct sbrec_port_binding *pb,
                                const struct sbrec_chassis *chassis_rec,
                                const struct ovsrec_interface *iface_rec,
-                               bool sb_readonly, enum can_bind bind_type);
+                               bool sb_readonly, enum can_bind bind_type,
+                               bool notify_up,
+                               const struct sbrec_port_binding *parent_pb);
 void if_status_mgr_release_iface(struct if_status_mgr *, const char *iface_id);
-void if_status_mgr_delete_iface(struct if_status_mgr *, const char *iface_id);
+void if_status_mgr_delete_iface(struct if_status_mgr *, const char *iface_id,
+                                const struct ovsrec_interface *iface_rec);
 
 void if_status_mgr_update(struct if_status_mgr *, struct local_binding_data *,
                           const struct sbrec_chassis *chassis,
@@ -45,6 +48,7 @@ void if_status_mgr_update(struct if_status_mgr *, struct local_binding_data *,
 void if_status_mgr_run(struct if_status_mgr *mgr, struct local_binding_data *,
                        const struct sbrec_chassis *,
                        const struct ovsrec_interface_table *iface_table,
+                       const struct sbrec_port_binding_table *pb_table,
                        bool sb_readonly, bool ovs_readonly);
 void if_status_mgr_get_memory_usage(struct if_status_mgr *mgr,
                                     struct simap *usage);
@@ -54,6 +58,7 @@ bool if_status_handle_claims(struct if_status_mgr *mgr,
                              struct local_binding_data *binding_data,
                              const struct sbrec_chassis *chassis_rec,
                              struct hmap *tracked_datapath,
+                             const struct sbrec_port_binding_table *pb_table,
                              bool sb_readonly);
 void if_status_mgr_remove_ovn_installed(struct if_status_mgr *mgr,
                                         const char *name,
diff --git a/controller/lflow.c b/controller/lflow.c
index f70080e8e..b0cf4253c 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -1017,6 +1017,7 @@ convert_match_to_expr(const struct sbrec_logical_flow *lflow,
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
         VLOG_WARN_RL(&rl, "error parsing match \"%s\": %s",
                     lflow->match, error);
+        expr_destroy(e);
         free(error);
         return NULL;
     }
@@ -1892,6 +1893,7 @@ add_lb_ct_snat_hairpin_vip_flow(const struct ovn_controller_lb *lb,
                                           local_datapaths, &match,
                                           &ofpacts, flow_table);
         }
+        /* datapath_group column is deprecated. */
         if (lb->slb->datapath_group) {
             for (size_t i = 0; i < lb->slb->datapath_group->n_datapaths; i++) {
                 add_lb_ct_snat_hairpin_for_dp(
@@ -1900,6 +1902,15 @@ add_lb_ct_snat_hairpin_vip_flow(const struct ovn_controller_lb *lb,
                     local_datapaths, &match, &ofpacts, flow_table);
             }
         }
+        if (lb->slb->ls_datapath_group) {
+            for (size_t i = 0;
+                 i < lb->slb->ls_datapath_group->n_datapaths; i++) {
+                add_lb_ct_snat_hairpin_for_dp(
+                    lb, !!lb_vip->vip_port,
+                    lb->slb->ls_datapath_group->datapaths[i],
+                    local_datapaths, &match, &ofpacts, flow_table);
+            }
+        }
     }
 
     ofpbuf_uninit(&ofpacts);
@@ -2055,11 +2066,17 @@ lflow_handle_changed_static_mac_bindings(
 static void
 consider_fdb_flows(const struct sbrec_fdb *fdb,
                    const struct hmap *local_datapaths,
-                   struct ovn_desired_flow_table *flow_table)
+                   struct ovn_desired_flow_table *flow_table,
+                   struct ovsdb_idl_index *sbrec_port_binding_by_key,
+                   bool localnet_learn_fdb)
 {
-    if (!get_local_datapath(local_datapaths, fdb->dp_key)) {
+    struct local_datapath *ld = get_local_datapath(local_datapaths,
+                                                   fdb->dp_key);
+    if (!ld) {
         return;
     }
+    const struct sbrec_port_binding *pb = lport_lookup_by_key_with_dp(
+        sbrec_port_binding_by_key, ld->datapath, fdb->port_key);
 
     struct eth_addr mac;
     if (!eth_addr_from_string(fdb->mac, &mac)) {
@@ -2081,6 +2098,7 @@ consider_fdb_flows(const struct sbrec_fdb *fdb,
     ofpbuf_clear(&ofpacts);
 
     uint8_t value = 1;
+    uint8_t is_vif =  pb ? !strcmp(pb->type, "") : 0;
     put_load(&value, sizeof value, MFF_LOG_FLAGS,
              MLF_LOOKUP_FDB_BIT, 1, &ofpacts);
 
@@ -2091,6 +2109,18 @@ consider_fdb_flows(const struct sbrec_fdb *fdb,
     ofctrl_add_flow(flow_table, OFTABLE_LOOKUP_FDB, 100,
                     fdb->header_.uuid.parts[0], &lookup_match, &ofpacts,
                     &fdb->header_.uuid);
+
+    if (is_vif && localnet_learn_fdb) {
+        struct match lookup_match_vif = MATCH_CATCHALL_INITIALIZER;
+        match_set_metadata(&lookup_match_vif, htonll(fdb->dp_key));
+        match_set_dl_src(&lookup_match_vif, mac);
+        match_set_reg_masked(&lookup_match_vif, MFF_LOG_FLAGS - MFF_REG0,
+                             MLF_LOCALNET, MLF_LOCALNET);
+
+        ofctrl_add_flow(flow_table, OFTABLE_LOOKUP_FDB, 100,
+                        fdb->header_.uuid.parts[0], &lookup_match_vif,
+                        &ofpacts, &fdb->header_.uuid);
+    }
     ofpbuf_uninit(&ofpacts);
 }
 
@@ -2099,11 +2129,14 @@ consider_fdb_flows(const struct sbrec_fdb *fdb,
 static void
 add_fdb_flows(const struct sbrec_fdb_table *fdb_table,
               const struct hmap *local_datapaths,
-              struct ovn_desired_flow_table *flow_table)
+              struct ovn_desired_flow_table *flow_table,
+              struct ovsdb_idl_index *sbrec_port_binding_by_key,
+              bool localnet_learn_fdb)
 {
     const struct sbrec_fdb *fdb;
     SBREC_FDB_TABLE_FOR_EACH (fdb, fdb_table) {
-        consider_fdb_flows(fdb, local_datapaths, flow_table);
+        consider_fdb_flows(fdb, local_datapaths, flow_table,
+                           sbrec_port_binding_by_key, localnet_learn_fdb);
     }
 }
 
@@ -2126,7 +2159,9 @@ lflow_run(struct lflow_ctx_in *l_ctx_in, struct lflow_ctx_out *l_ctx_out)
                          l_ctx_in->lb_hairpin_use_ct_mark,
                          l_ctx_out->flow_table);
     add_fdb_flows(l_ctx_in->fdb_table, l_ctx_in->local_datapaths,
-                  l_ctx_out->flow_table);
+                  l_ctx_out->flow_table,
+                  l_ctx_in->sbrec_port_binding_by_key,
+                  l_ctx_in->localnet_learn_fdb);
     add_port_sec_flows(l_ctx_in->binding_lports, l_ctx_in->chassis,
                        l_ctx_out->flow_table);
 }
@@ -2216,7 +2251,9 @@ lflow_add_flows_for_datapath(const struct sbrec_datapath_binding *dp,
     SBREC_FDB_FOR_EACH_EQUAL (fdb_row, fdb_index_row,
                               l_ctx_in->sbrec_fdb_by_dp_key) {
         consider_fdb_flows(fdb_row, l_ctx_in->local_datapaths,
-                           l_ctx_out->flow_table);
+                           l_ctx_out->flow_table,
+                           l_ctx_in->sbrec_port_binding_by_key,
+                           l_ctx_in->localnet_learn_fdb);
     }
     sbrec_fdb_index_destroy_row(fdb_index_row);
 
@@ -2280,6 +2317,15 @@ lflow_handle_flows_for_lport(const struct sbrec_port_binding *pb,
                                           pb->logical_port)) {
         consider_port_sec_flows(pb, l_ctx_out->flow_table);
     }
+    if (l_ctx_in->localnet_learn_fdb_changed && l_ctx_in->localnet_learn_fdb) {
+        const struct sbrec_fdb *fdb;
+        SBREC_FDB_TABLE_FOR_EACH (fdb, l_ctx_in->fdb_table) {
+            consider_fdb_flows(fdb, l_ctx_in->local_datapaths,
+                               l_ctx_out->flow_table,
+                               l_ctx_in->sbrec_port_binding_by_key,
+                               l_ctx_in->localnet_learn_fdb);
+        }
+    }
     return true;
 }
 
@@ -2409,7 +2455,9 @@ lflow_handle_changed_fdbs(struct lflow_ctx_in *l_ctx_in,
         VLOG_DBG("Add fdb flows for fdb "UUID_FMT,
                  UUID_ARGS(&fdb->header_.uuid));
         consider_fdb_flows(fdb, l_ctx_in->local_datapaths,
-                           l_ctx_out->flow_table);
+                           l_ctx_out->flow_table,
+                           l_ctx_in->sbrec_port_binding_by_key,
+                           l_ctx_in->localnet_learn_fdb);
     }
 
     return true;
diff --git a/controller/lflow.h b/controller/lflow.h
index 5da4385e4..9b7ffa19c 100644
--- a/controller/lflow.h
+++ b/controller/lflow.h
@@ -100,6 +100,7 @@ struct lflow_ctx_in {
     struct ovsdb_idl_index *sbrec_logical_flow_by_logical_datapath;
     struct ovsdb_idl_index *sbrec_logical_flow_by_logical_dp_group;
     struct ovsdb_idl_index *sbrec_port_binding_by_name;
+    struct ovsdb_idl_index *sbrec_port_binding_by_key;
     struct ovsdb_idl_index *sbrec_fdb_by_dp_key;
     struct ovsdb_idl_index *sbrec_mac_binding_by_datapath;
     struct ovsdb_idl_index *sbrec_static_mac_binding_by_datapath;
@@ -127,6 +128,8 @@ struct lflow_ctx_in {
     const struct flow_collector_ids *collector_ids;
     const struct hmap *local_lbs;
     bool lb_hairpin_use_ct_mark;
+    bool localnet_learn_fdb;
+    bool localnet_learn_fdb_changed;
 };
 
 struct lflow_ctx_out {
diff --git a/controller/local_data.c b/controller/local_data.c
index cf0b21bb1..ab18386c7 100644
--- a/controller/local_data.c
+++ b/controller/local_data.c
@@ -56,6 +56,20 @@ static bool datapath_is_transit_switch(const struct sbrec_datapath_binding *);
 
 static uint64_t local_datapath_usage;
 
+/* To be used when hmap_node.hash might be wrong e.g. tunnel_key got updated */
+struct local_datapath *
+get_local_datapath_no_hash(const struct hmap *local_datapaths,
+                           uint32_t tunnel_key)
+{
+    struct local_datapath *ld;
+    HMAP_FOR_EACH (ld, hmap_node, local_datapaths) {
+        if (ld->datapath->tunnel_key == tunnel_key) {
+            return ld;
+        }
+    }
+    return NULL;
+}
+
 struct local_datapath *
 get_local_datapath(const struct hmap *local_datapaths, uint32_t tunnel_key)
 {
@@ -661,8 +675,24 @@ lb_is_local(const struct sbrec_load_balancer *sbrec_lb,
         }
     }
 
+    /* datapath_group column is deprecated. */
     struct sbrec_logical_dp_group *dp_group = sbrec_lb->datapath_group;
+    for (size_t i = 0; dp_group && i < dp_group->n_datapaths; i++) {
+        if (get_local_datapath(local_datapaths,
+                               dp_group->datapaths[i]->tunnel_key)) {
+            return true;
+        }
+    }
+
+    dp_group = sbrec_lb->ls_datapath_group;
+    for (size_t i = 0; dp_group && i < dp_group->n_datapaths; i++) {
+        if (get_local_datapath(local_datapaths,
+                               dp_group->datapaths[i]->tunnel_key)) {
+            return true;
+        }
+    }
 
+    dp_group = sbrec_lb->lr_datapath_group;
     for (size_t i = 0; dp_group && i < dp_group->n_datapaths; i++) {
         if (get_local_datapath(local_datapaths,
                                dp_group->datapaths[i]->tunnel_key)) {
diff --git a/controller/local_data.h b/controller/local_data.h
index f6d8f725f..2a1a3c0f9 100644
--- a/controller/local_data.h
+++ b/controller/local_data.h
@@ -69,6 +69,10 @@ struct local_datapath *local_datapath_alloc(
     const struct sbrec_datapath_binding *);
 struct local_datapath *get_local_datapath(const struct hmap *,
                                           uint32_t tunnel_key);
+struct local_datapath *get_local_datapath_no_hash(
+    const struct hmap *local_datapaths,
+    uint32_t tunnel_key);
+
 bool
 need_add_peer_to_local(
     struct ovsdb_idl_index *sbrec_port_binding_by_name,
diff --git a/controller/lport.c b/controller/lport.c
index add7e91aa..b3721024b 100644
--- a/controller/lport.c
+++ b/controller/lport.c
@@ -44,13 +44,10 @@ lport_lookup_by_name(struct ovsdb_idl_index *sbrec_port_binding_by_name,
 }
 
 const struct sbrec_port_binding *
-lport_lookup_by_key(struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
-                    struct ovsdb_idl_index *sbrec_port_binding_by_key,
-                    uint64_t dp_key, uint64_t port_key)
+lport_lookup_by_key_with_dp(struct ovsdb_idl_index *sbrec_port_binding_by_key,
+                            const struct sbrec_datapath_binding *db,
+                            uint64_t port_key)
 {
-    /* Lookup datapath corresponding to dp_key. */
-    const struct sbrec_datapath_binding *db = datapath_lookup_by_key(
-        sbrec_datapath_binding_by_key, dp_key);
     if (!db) {
         return NULL;
     }
@@ -69,6 +66,19 @@ lport_lookup_by_key(struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
     return retval;
 }
 
+const struct sbrec_port_binding *
+lport_lookup_by_key(struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+                    struct ovsdb_idl_index *sbrec_port_binding_by_key,
+                    uint64_t dp_key, uint64_t port_key)
+{
+    /* Lookup datapath corresponding to dp_key. */
+    const struct sbrec_datapath_binding *db = datapath_lookup_by_key(
+        sbrec_datapath_binding_by_key, dp_key);
+
+    return lport_lookup_by_key_with_dp(sbrec_port_binding_by_key, db,
+                                       port_key);
+}
+
 bool
 lport_is_chassis_resident(struct ovsdb_idl_index *sbrec_port_binding_by_name,
                           const struct sbrec_chassis *chassis,
diff --git a/controller/lport.h b/controller/lport.h
index 644c67255..2f72aef5e 100644
--- a/controller/lport.h
+++ b/controller/lport.h
@@ -43,6 +43,10 @@ const struct sbrec_port_binding *lport_lookup_by_key(
     struct ovsdb_idl_index *sbrec_port_binding_by_key,
     uint64_t dp_key, uint64_t port_key);
 
+const struct sbrec_port_binding *lport_lookup_by_key_with_dp(
+    struct ovsdb_idl_index *sbrec_port_binding_by_key,
+    const struct sbrec_datapath_binding *dp, uint64_t port_key);
+
 enum can_bind {
     CANNOT_BIND = 0,
     CAN_BIND_AS_MAIN,
diff --git a/controller/ovn-controller.8.xml b/controller/ovn-controller.8.xml
index 7b4100592..0b9641045 100644
--- a/controller/ovn-controller.8.xml
+++ b/controller/ovn-controller.8.xml
@@ -363,7 +363,10 @@
         The boolean flag indicates if <code>ovn-controller</code> when create
         tunnel ports should set <code>local_ip</code> parameter.  Can be
         heplful to pin source outer IP for the tunnel when multiple interfaces
-        are used on the host for overlay traffic.
+        are used on the host for overlay traffic. This is also useful when
+        running multiple <code>ovn-controller</code> instances on the same
+        chassis, in which case this setting will guarantee that their tunnel
+        ports have unique configuration and can exist in parallel.
       </dd>
       <dt><code>external_ids:garp-max-timeout-sec</code></dt>
       <dd>
@@ -398,9 +401,11 @@
         names on the same host using the same <code>vswitchd</code> instance.
         This may be useful when running a hybrid setup with more than one CMS
         managing ports on the host, or to use different datapath types on the
-        same host. Note that this ability is highly experimental and has known
-        limitations (for example, stateful ACLs are not supported). Use at your
-        own risk.
+        same host. Make sure you also set
+        <code>external_ids:ovn-set-local-ip</code> when using such
+        configuration. Also note that this ability is highly experimental and
+        has known limitations (for example, stateful ACLs are not supported).
+        Use at your own risk.
     </p>
 
     <p>
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index b3e4e0da8..6787dda80 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -88,7 +88,6 @@
 
 VLOG_DEFINE_THIS_MODULE(main);
 
-static unixctl_cb_func ovn_controller_exit;
 static unixctl_cb_func ct_zone_list;
 static unixctl_cb_func extend_table_list;
 static unixctl_cb_func inject_pkt;
@@ -211,8 +210,8 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
 {
     /* Monitor Port_Bindings rows for local interfaces and local datapaths.
      *
-     * Monitor Logical_Flow, MAC_Binding, Multicast_Group, and DNS tables for
-     * local datapaths.
+     * Monitor Logical_Flow, MAC_Binding, FDB, Multicast_Group, and DNS tables
+     * for local datapaths.
      *
      * Monitor Controller_Event rows for local chassis.
      *
@@ -230,6 +229,7 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
     struct ovsdb_idl_condition lf = OVSDB_IDL_CONDITION_INIT(&lf);
     struct ovsdb_idl_condition ldpg = OVSDB_IDL_CONDITION_INIT(&ldpg);
     struct ovsdb_idl_condition mb = OVSDB_IDL_CONDITION_INIT(&mb);
+    struct ovsdb_idl_condition fdb = OVSDB_IDL_CONDITION_INIT(&fdb);
     struct ovsdb_idl_condition mg = OVSDB_IDL_CONDITION_INIT(&mg);
     struct ovsdb_idl_condition dns = OVSDB_IDL_CONDITION_INIT(&dns);
     struct ovsdb_idl_condition ce =  OVSDB_IDL_CONDITION_INIT(&ce);
@@ -248,6 +248,7 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
         ovsdb_idl_condition_add_clause_true(&pb);
         ovsdb_idl_condition_add_clause_true(&lf);
         ovsdb_idl_condition_add_clause_true(&mb);
+        ovsdb_idl_condition_add_clause_true(&fdb);
         ovsdb_idl_condition_add_clause_true(&mg);
         ovsdb_idl_condition_add_clause_true(&dns);
         ovsdb_idl_condition_add_clause_true(&ce);
@@ -337,6 +338,8 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
             sbrec_logical_flow_add_clause_logical_datapath(&lf, OVSDB_F_EQ,
                                                            uuid);
             sbrec_mac_binding_add_clause_datapath(&mb, OVSDB_F_EQ, uuid);
+            sbrec_fdb_add_clause_dp_key(&fdb, OVSDB_F_EQ,
+                                        ld->datapath->tunnel_key);
             sbrec_multicast_group_add_clause_datapath(&mg, OVSDB_F_EQ, uuid);
             sbrec_dns_add_clause_datapaths(&dns, OVSDB_F_INCLUDES, &uuid, 1);
             sbrec_ip_multicast_add_clause_datapath(&ip_mcast, OVSDB_F_EQ,
@@ -360,6 +363,7 @@ out:;
         sb_table_set_req_mon_condition(ovnsb_idl, logical_flow, &lf),
         sb_table_set_req_mon_condition(ovnsb_idl, logical_dp_group, &ldpg),
         sb_table_set_req_mon_condition(ovnsb_idl, mac_binding, &mb),
+        sb_table_set_req_mon_condition(ovnsb_idl, fdb, &fdb),
         sb_table_set_req_mon_condition(ovnsb_idl, multicast_group, &mg),
         sb_table_set_req_mon_condition(ovnsb_idl, dns, &dns),
         sb_table_set_req_mon_condition(ovnsb_idl, controller_event, &ce),
@@ -378,6 +382,7 @@ out:;
     ovsdb_idl_condition_destroy(&lf);
     ovsdb_idl_condition_destroy(&ldpg);
     ovsdb_idl_condition_destroy(&mb);
+    ovsdb_idl_condition_destroy(&fdb);
     ovsdb_idl_condition_destroy(&mg);
     ovsdb_idl_condition_destroy(&dns);
     ovsdb_idl_condition_destroy(&ce);
@@ -1499,6 +1504,8 @@ struct ed_type_runtime_data {
     /* Tracked data. See below for more details and comments. */
     bool tracked;
     bool local_lports_changed;
+    bool localnet_learn_fdb;
+    bool localnet_learn_fdb_changed;
     struct hmap tracked_dp_bindings;
 
     struct shash local_active_ports_ipv6_pd;
@@ -1709,6 +1716,8 @@ init_binding_ctx(struct engine_node *node,
     b_ctx_out->postponed_ports = rt_data->postponed_ports;
     b_ctx_out->tracked_dp_bindings = NULL;
     b_ctx_out->if_mgr = ctrl_ctx->if_mgr;
+    b_ctx_out->localnet_learn_fdb = rt_data->localnet_learn_fdb;
+    b_ctx_out->localnet_learn_fdb_changed = false;
 }
 
 static void
@@ -1766,6 +1775,7 @@ en_runtime_data_run(struct engine_node *node, void *data)
     }
 
     binding_run(&b_ctx_in, &b_ctx_out);
+    rt_data->localnet_learn_fdb = b_ctx_out.localnet_learn_fdb;
 
     engine_set_node_state(node, EN_UPDATED);
 }
@@ -1815,6 +1825,8 @@ runtime_data_sb_ro_handler(struct engine_node *node, void *data)
         engine_ovsdb_node_get_index(
                 engine_get_input("SB_chassis", node),
                 "name");
+    const struct sbrec_port_binding_table *pb_table =
+        EN_OVSDB_GET(engine_get_input("SB_port_binding", node));
 
     if (chassis_id) {
         chassis = chassis_lookup_by_name(sbrec_chassis_by_name, chassis_id);
@@ -1829,7 +1841,7 @@ runtime_data_sb_ro_handler(struct engine_node *node, void *data)
                                     &rt_data->lbinding_data,
                                     chassis,
                                     &rt_data->tracked_dp_bindings,
-                                    sb_readonly)) {
+                                    pb_table, sb_readonly)) {
             engine_set_node_state(node, EN_UPDATED);
             rt_data->tracked = true;
         }
@@ -1878,9 +1890,12 @@ runtime_data_sb_port_binding_handler(struct engine_node *node, void *data)
     }
 
     rt_data->local_lports_changed = b_ctx_out.local_lports_changed;
+    rt_data->localnet_learn_fdb = b_ctx_out.localnet_learn_fdb;
+    rt_data->localnet_learn_fdb_changed = b_ctx_out.localnet_learn_fdb_changed;
     if (b_ctx_out.related_lports_changed ||
             b_ctx_out.non_vif_ports_changed ||
             b_ctx_out.local_lports_changed ||
+            b_ctx_out.localnet_learn_fdb_changed ||
             !hmap_is_empty(b_ctx_out.tracked_dp_bindings)) {
         engine_set_node_state(node, EN_UPDATED);
     }
@@ -1897,6 +1912,7 @@ runtime_data_sb_datapath_binding_handler(struct engine_node *node OVS_UNUSED,
             engine_get_input("SB_datapath_binding", node));
     const struct sbrec_datapath_binding *dp;
     struct ed_type_runtime_data *rt_data = data;
+    struct local_datapath *ld;
 
     SBREC_DATAPATH_BINDING_TABLE_FOR_EACH_TRACKED (dp, dp_table) {
         if (sbrec_datapath_binding_is_deleted(dp)) {
@@ -1904,6 +1920,28 @@ runtime_data_sb_datapath_binding_handler(struct engine_node *node OVS_UNUSED,
                                    dp->tunnel_key)) {
                 return false;
             }
+            /* If the tunnel key got updated, get_local_datapath will not find
+             * the ld. Use get_local_datapath_no_hash which does not
+             * rely on the hash.
+             */
+            if (sbrec_datapath_binding_is_updated(
+                    dp, SBREC_DATAPATH_BINDING_COL_TUNNEL_KEY)) {
+                if (get_local_datapath_no_hash(&rt_data->local_datapaths,
+                                               dp->tunnel_key)) {
+                    return false;
+                }
+            }
+        } else if (sbrec_datapath_binding_is_updated(
+                        dp, SBREC_DATAPATH_BINDING_COL_TUNNEL_KEY)
+                   && !sbrec_datapath_binding_is_new(dp)) {
+            /* If the tunnel key is updated, remove the entry (with a wrong
+             * hash) from the map. It will be (properly) added back later.
+             */
+            if ((ld = get_local_datapath_no_hash(&rt_data->local_datapaths,
+                                                 dp->tunnel_key))) {
+                hmap_remove(&rt_data->local_datapaths, &ld->hmap_node);
+                local_datapath_destroy(ld);
+            }
         }
     }
 
@@ -2637,9 +2675,10 @@ ct_zones_runtime_data_handler(struct engine_node *node, void *data)
             struct tracked_lport *t_lport = shash_node->data;
             if (strcmp(t_lport->pb->type, "")
                 && strcmp(t_lport->pb->type, "localport")
+                && strcmp(t_lport->pb->type, "l3gateway")
                 && strcmp(t_lport->pb->type, "localnet")) {
-                /* We allocate zone-id's only to VIF, localport, and localnet
-                 * lports. */
+                /* We allocate zone-id's only to VIF, localport, l3gateway,
+                 * and localnet lports. */
                 continue;
             }
 
@@ -2803,12 +2842,25 @@ load_balancers_by_dp_init(const struct hmap *local_datapaths,
             load_balancers_by_dp_add_one(local_datapaths,
                                          lb->datapaths[i], lb, lbs);
         }
+        /* datapath_group column is deprecated. */
         for (size_t i = 0; lb->datapath_group
                            && i < lb->datapath_group->n_datapaths; i++) {
             load_balancers_by_dp_add_one(local_datapaths,
                                          lb->datapath_group->datapaths[i],
                                          lb, lbs);
         }
+        for (size_t i = 0; lb->ls_datapath_group
+                           && i < lb->ls_datapath_group->n_datapaths; i++) {
+            load_balancers_by_dp_add_one(local_datapaths,
+                                         lb->ls_datapath_group->datapaths[i],
+                                         lb, lbs);
+        }
+        for (size_t i = 0; lb->lr_datapath_group
+                           && i < lb->lr_datapath_group->n_datapaths; i++) {
+            load_balancers_by_dp_add_one(local_datapaths,
+                                         lb->lr_datapath_group->datapaths[i],
+                                         lb, lbs);
+        }
     }
     return lbs;
 }
@@ -3788,6 +3840,11 @@ init_lflow_ctx(struct engine_node *node,
                 engine_get_input("SB_port_binding", node),
                 "name");
 
+    struct ovsdb_idl_index *sbrec_port_binding_by_key =
+        engine_ovsdb_node_get_index(
+                engine_get_input("SB_port_binding", node),
+                "key");
+
     struct ovsdb_idl_index *sbrec_logical_flow_by_dp =
         engine_ovsdb_node_get_index(
                 engine_get_input("SB_logical_flow", node),
@@ -3887,6 +3944,7 @@ init_lflow_ctx(struct engine_node *node,
     l_ctx_in->sbrec_logical_flow_by_logical_dp_group =
         sbrec_logical_flow_by_dp_group;
     l_ctx_in->sbrec_port_binding_by_name = sbrec_port_binding_by_name;
+    l_ctx_in->sbrec_port_binding_by_key = sbrec_port_binding_by_key;
     l_ctx_in->sbrec_fdb_by_dp_key = sbrec_fdb_by_dp_key;
     l_ctx_in->sbrec_mac_binding_by_datapath = sbrec_mac_binding_by_datapath;
     l_ctx_in->sbrec_static_mac_binding_by_datapath =
@@ -3905,6 +3963,8 @@ init_lflow_ctx(struct engine_node *node,
     l_ctx_in->active_tunnels = &rt_data->active_tunnels;
     l_ctx_in->related_lport_ids = &rt_data->related_lports.lport_ids;
     l_ctx_in->binding_lports = &rt_data->lbinding_data.lports;
+    l_ctx_in->localnet_learn_fdb = rt_data->localnet_learn_fdb;
+    l_ctx_in->localnet_learn_fdb_changed = rt_data->localnet_learn_fdb_changed;
     l_ctx_in->chassis_tunnels = &non_vif_data->chassis_tunnels;
     l_ctx_in->lb_hairpin_use_ct_mark = n_opts->lb_hairpin_use_ct_mark;
     l_ctx_in->nd_ra_opts = &fo->nd_ra_opts;
@@ -3930,8 +3990,8 @@ en_lflow_output_init(struct engine_node *node OVS_UNUSED,
 {
     struct ed_type_lflow_output *data = xzalloc(sizeof *data);
     ovn_desired_flow_table_init(&data->flow_table);
-    ovn_extend_table_init(&data->group_table);
-    ovn_extend_table_init(&data->meter_table);
+    ovn_extend_table_init(&data->group_table, "group-table", 0);
+    ovn_extend_table_init(&data->meter_table, "meter-table", 0);
     objdep_mgr_init(&data->lflow_deps_mgr);
     lflow_conj_ids_init(&data->conj_ids);
     uuidset_init(&data->objs_processed);
@@ -4895,11 +4955,6 @@ controller_output_mac_cache_handler(struct engine_node *node,
     return true;
 }
 
-struct ovn_controller_exit_args {
-    bool *exiting;
-    bool *restart;
-};
-
 /* Handles sbrec_chassis changes.
  * If a new chassis is added or removed return false, so that
  * flows are recomputed.  For any updates, there is no need for
@@ -4974,9 +5029,7 @@ int
 main(int argc, char *argv[])
 {
     struct unixctl_server *unixctl;
-    bool exiting;
-    bool restart;
-    struct ovn_controller_exit_args exit_args = {&exiting, &restart};
+    struct ovn_exit_args exit_args = {};
     int retval;
 
     /* Read from system-id-override file once on startup. */
@@ -4996,7 +5049,7 @@ main(int argc, char *argv[])
     if (retval) {
         exit(EXIT_FAILURE);
     }
-    unixctl_command_register("exit", "", 0, 1, ovn_controller_exit,
+    unixctl_command_register("exit", "", 0, 1, ovn_exit_command_callback,
                              &exit_args);
 
     daemonize_complete();
@@ -5508,10 +5561,8 @@ main(int argc, char *argv[])
     VLOG_INFO("OVN internal version is : [%s]", ovn_version);
 
     /* Main loop. */
-    exiting = false;
-    restart = false;
     bool sb_monitor_all = false;
-    while (!exiting) {
+    while (!exit_args.exiting) {
         memory_run();
         if (memory_should_report()) {
             struct simap usage = SIMAP_INITIALIZER(&usage);
@@ -5645,9 +5696,20 @@ main(int argc, char *argv[])
                                            br_int ? br_int->name : NULL)) {
                 VLOG_INFO("OVS feature set changed, force recompute.");
                 engine_set_force_recompute(true);
+                if (ovs_feature_set_discovered()) {
+                    uint32_t max_groups = ovs_feature_max_select_groups_get();
+                    uint32_t max_meters = ovs_feature_max_meters_get();
+                    struct ed_type_lflow_output *lflow_out_data =
+                        engine_get_internal_data(&en_lflow_output);
+
+                    ovn_extend_table_reinit(&lflow_out_data->group_table,
+                                            max_groups);
+                    ovn_extend_table_reinit(&lflow_out_data->meter_table,
+                                            max_meters);
+                }
             }
 
-            if (br_int) {
+            if (br_int && ovs_feature_set_discovered()) {
                 ct_zones_data = engine_get_data(&en_ct_zones);
                 if (ct_zones_data && ofctrl_run(br_int, ovs_table,
                                                 &ct_zones_data->pending)) {
@@ -5809,6 +5871,13 @@ main(int argc, char *argv[])
                                     &runtime_data->local_datapaths,
                                     sb_monitor_all);
                         }
+                        if (ovs_idl_txn) {
+                            update_qos(sbrec_port_binding_by_name, ovs_idl_txn,
+                                       ovsrec_port_by_qos,
+                                       ovsrec_qos_table_get(ovs_idl_loop.idl),
+                                       &runtime_data->qos_map,
+                                       ovs_table, bridge_table);
+                        }
                     }
 
                     if (mac_cache_data) {
@@ -5864,6 +5933,8 @@ main(int argc, char *argv[])
                     if_status_mgr_run(if_mgr, binding_data, chassis,
                                       ovsrec_interface_table_get(
                                                   ovs_idl_loop.idl),
+                                      sbrec_port_binding_table_get(
+                                                 ovnsb_idl_loop.idl),
                                       !ovnsb_idl_txn, !ovs_idl_txn);
                     stopwatch_stop(IF_STATUS_MGR_RUN_STOPWATCH_NAME,
                                    time_msec());
@@ -5941,7 +6012,7 @@ main(int argc, char *argv[])
         unixctl_server_run(unixctl);
 
         unixctl_server_wait(unixctl);
-        if (exiting || pending_pkt.conn) {
+        if (exit_args.exiting || pending_pkt.conn) {
             poll_immediate_wake();
         }
 
@@ -5992,7 +6063,7 @@ loop_done:
         memory_wait();
         poll_block();
         if (should_service_stop()) {
-            exiting = true;
+            exit_args.exiting = true;
         }
     }
 
@@ -6000,7 +6071,7 @@ loop_done:
     engine_cleanup();
 
     /* It's time to exit.  Clean up the databases if we are not restarting */
-    if (!restart) {
+    if (!exit_args.restart) {
         bool done = !ovsdb_idl_has_ever_connected(ovnsb_idl_loop.idl);
         while (!done) {
             update_sb_db(ovs_idl_loop.idl, ovnsb_idl_loop.idl,
@@ -6052,7 +6123,6 @@ loop_done:
     }
 
     free(ovn_version);
-    unixctl_server_destroy(unixctl);
     lflow_destroy();
     ofctrl_destroy();
     pinctrl_destroy();
@@ -6077,6 +6147,8 @@ loop_done:
     if (cli_system_id) {
         free(cli_system_id);
     }
+    ovn_exit_args_finish(&exit_args);
+    unixctl_server_destroy(unixctl);
     service_stop();
     ovsrcu_exit();
 
@@ -6156,6 +6228,7 @@ parse_options(int argc, char *argv[])
             break;
 
         case 'n':
+            free(cli_system_id);
             cli_system_id = xstrdup(optarg);
             break;
 
@@ -6200,16 +6273,6 @@ usage(void)
     exit(EXIT_SUCCESS);
 }
 
-static void
-ovn_controller_exit(struct unixctl_conn *conn, int argc,
-             const char *argv[], void *exit_args_)
-{
-    struct ovn_controller_exit_args *exit_args = exit_args_;
-    *exit_args->exiting = true;
-    *exit_args->restart = argc == 2 && !strcmp(argv[1], "--restart");
-    unixctl_command_reply(conn, NULL);
-}
-
 static void
 ct_zone_list(struct unixctl_conn *conn, int argc OVS_UNUSED,
              const char *argv[] OVS_UNUSED, void *ct_zones_)
diff --git a/controller/physical.c b/controller/physical.c
index 75257bc85..3b862ca34 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -341,7 +341,7 @@ get_remote_tunnels(const struct sbrec_port_binding *binding,
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
             VLOG_WARN_RL(
                 &rl, "Failed to locate tunnel to reach main chassis %s "
-                     "for port %s. Cloning packets disabled for the chassis.",
+                     "for port %s.",
                 binding->chassis->name, binding->logical_port);
         } else {
             struct tunnel *tun_elem = xmalloc(sizeof *tun_elem);
@@ -363,7 +363,7 @@ get_remote_tunnels(const struct sbrec_port_binding *binding,
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
             VLOG_WARN_RL(
                 &rl, "Failed to locate tunnel to reach additional chassis %s "
-                     "for port %s. Cloning packets disabled for the chassis.",
+                     "for port %s.",
                 binding->additional_chassis[i]->name, binding->logical_port);
             continue;
         }
@@ -703,22 +703,33 @@ put_replace_chassis_mac_flows(const struct simap *ct_zones,
     }
 }
 
-#define VLAN_80211AD_ETHTYPE 0x88a8
-#define VLAN_80211Q_ETHTYPE 0x8100
+#define VLAN_8021AD_ETHTYPE 0x88a8
+#define VLAN_8021Q_ETHTYPE 0x8100
 
 static void
 ofpact_put_push_vlan(struct ofpbuf *ofpacts, const struct smap *options, int tag)
 {
     const char *ethtype_opt = options ? smap_get(options, "ethtype") : NULL;
 
-    int ethtype = VLAN_80211Q_ETHTYPE;
+    int ethtype = VLAN_8021Q_ETHTYPE;
     if (ethtype_opt) {
-        if (!strcasecmp(ethtype_opt, "802.11ad")) {
-            ethtype = VLAN_80211AD_ETHTYPE;
-        } else if (strcasecmp(ethtype_opt, "802.11q")) {
+      if (!strcasecmp(ethtype_opt, "802.11ad")
+          || !strcasecmp(ethtype_opt, "802.1ad")) {
+        ethtype = VLAN_8021AD_ETHTYPE;
+      } else if (!strcasecmp(ethtype_opt, "802.11q")
+                 || !strcasecmp(ethtype_opt, "802.1q")) {
+        ethtype = VLAN_8021Q_ETHTYPE;
+      } else {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
             VLOG_WARN_RL(&rl, "Unknown port ethtype: %s", ethtype_opt);
-        }
+      }
+      if (!strcasecmp(ethtype_opt, "802.11ad")
+          || !strcasecmp(ethtype_opt, "802.11q")) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+        VLOG_WARN_RL(&rl, "Using incorrect value ethtype: %s for either "
+                          "802.1q or 802.1ad please correct this value",
+                          ethtype_opt);
+      }
     }
 
     struct ofpact_push_vlan *push_vlan;
diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index ff5a3444c..ececbdb48 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -373,9 +373,13 @@ static const struct sbrec_fdb *fdb_lookup(
     uint32_t dp_key, const char *mac);
 static void run_put_fdb(struct ovsdb_idl_txn *ovnsb_idl_txn,
                         struct ovsdb_idl_index *sbrec_fdb_by_dp_key_mac,
+            struct ovsdb_idl_index *sbrec_port_binding_by_key,
+            struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
                         const struct fdb_entry *fdb_e)
                         OVS_REQUIRES(pinctrl_mutex);
 static void run_put_fdbs(struct ovsdb_idl_txn *ovnsb_idl_txn,
+            struct ovsdb_idl_index *sbrec_port_binding_by_key,
+            struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
                         struct ovsdb_idl_index *sbrec_fdb_by_dp_key_mac)
                         OVS_REQUIRES(pinctrl_mutex);
 static void wait_put_fdbs(struct ovsdb_idl_txn *ovnsb_idl_txn);
@@ -1280,11 +1284,25 @@ fill_ipv6_prefix_state(struct ovsdb_idl_txn *ovnsb_idl_txn,
             continue;
         }
 
+        /* To reach this point, the port binding must be a logical router
+         * port. LRPs are configured with a single MAC that is always non-NULL.
+         * Therefore, as long as we are working with a port_binding that was
+         * inserted into the southbound database by northd, we can always
+         * safely extract pb->mac[0] since it will be non-NULL.
+         *
+         * However, if a port_binding was inserted by someone else, then we
+         * need to double-check our assumption first.
+         */
+        if (pb->n_mac != 1) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_ERR_RL(&rl, "Port binding "UUID_FMT" has %"PRIuSIZE" MACs "
+                        "instead of 1", UUID_ARGS(&pb->header_.uuid),
+                        pb->n_mac);
+            continue;
+        }
         struct lport_addresses c_addrs;
-        for (size_t j = 0; j < pb->n_mac; j++) {
-            if (extract_lsp_addresses(pb->mac[j], &c_addrs)) {
-                    break;
-            }
+        if (!extract_lsp_addresses(pb->mac[0], &c_addrs)) {
+            continue;
         }
 
         pfd = shash_find_data(&ipv6_prefixd, pb->logical_port);
@@ -3577,7 +3595,8 @@ pinctrl_run(struct ovsdb_idl_txn *ovnsb_idl_txn,
                       chassis);
     bfd_monitor_run(ovnsb_idl_txn, bfd_table, sbrec_port_binding_by_name,
                     chassis, active_tunnels);
-    run_put_fdbs(ovnsb_idl_txn, sbrec_fdb_by_dp_key_mac);
+    run_put_fdbs(ovnsb_idl_txn, sbrec_port_binding_by_key,
+                 sbrec_datapath_binding_by_key, sbrec_fdb_by_dp_key_mac);
     run_activated_ports(ovnsb_idl_txn, sbrec_datapath_binding_by_key,
                         sbrec_port_binding_by_key, chassis);
     ovs_mutex_unlock(&pinctrl_mutex);
@@ -6226,6 +6245,12 @@ pinctrl_handle_put_nd_ra_opts(
 
     /* Set the IPv6 payload length and calculate the ICMPv6 checksum. */
     struct ovs_16aligned_ip6_hdr *nh = dp_packet_l3(&pkt_out);
+
+    /* Set the source to "ff02::1" if the original source is "::". */
+    if (!memcmp(&nh->ip6_src, &in6addr_any, sizeof in6addr_any)) {
+        memcpy(&nh->ip6_src, &in6addr_all_hosts, sizeof in6addr_all_hosts);
+    }
+
     nh->ip6_plen = htons(userdata->size);
     struct ovs_ra_msg *ra = dp_packet_l4(&pkt_out);
     ra->icmph.icmp6_cksum = 0;
@@ -6338,26 +6363,31 @@ pinctrl_handle_empty_lb_backends_opts(struct ofpbuf *userdata)
     char *protocol = NULL;
     char *load_balancer = NULL;
 
+    struct empty_lb_backends_event *event = NULL;
+
     while (userdata->size) {
         userdata_opt = ofpbuf_try_pull(userdata, sizeof opt_hdr);
         if (!userdata_opt) {
-            return false;
+            goto cleanup;
         }
         memcpy(&opt_hdr, userdata_opt, sizeof opt_hdr);
 
         size_t size = ntohs(opt_hdr.size);
         char *userdata_opt_data = ofpbuf_try_pull(userdata, size);
         if (!userdata_opt_data) {
-            return false;
+            goto cleanup;
         }
         switch (ntohs(opt_hdr.opt_code)) {
         case EMPTY_LB_VIP:
+            free(vip);
             vip = xmemdup0(userdata_opt_data, size);
             break;
         case EMPTY_LB_PROTOCOL:
+            free(protocol);
             protocol = xmemdup0(userdata_opt_data, size);
             break;
         case EMPTY_LB_LOAD_BALANCER:
+            free(load_balancer);
             load_balancer = xmemdup0(userdata_opt_data, size);
             break;
         default:
@@ -6368,36 +6398,39 @@ pinctrl_handle_empty_lb_backends_opts(struct ofpbuf *userdata)
     if (!vip || !protocol || !load_balancer) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
         VLOG_WARN_RL(&rl, "missing lb parameters in userdata");
-        free(vip);
-        free(protocol);
-        free(load_balancer);
-        return false;
+        goto cleanup;
     }
 
-    struct empty_lb_backends_event *event;
-
     event = pinctrl_find_empty_lb_backends_event(vip, protocol,
                                                  load_balancer, hash);
-    if (!event) {
-        if (hmap_count(&event_table[OVN_EVENT_EMPTY_LB_BACKENDS]) >= 1000) {
-            COVERAGE_INC(pinctrl_drop_controller_event);
-            return false;
-        }
+    if (event) {
+        goto cleanup;
+    }
 
-        event = xzalloc(sizeof *event);
-        hmap_insert(&event_table[OVN_EVENT_EMPTY_LB_BACKENDS],
-                    &event->hmap_node, hash);
-        event->vip = vip;
-        event->protocol = protocol;
-        event->load_balancer = load_balancer;
-        event->timestamp = time_msec();
-        notify_pinctrl_main();
-    } else {
-        free(vip);
-        free(protocol);
-        free(load_balancer);
+    if (hmap_count(&event_table[OVN_EVENT_EMPTY_LB_BACKENDS]) >= 1000) {
+        COVERAGE_INC(pinctrl_drop_controller_event);
+        goto cleanup;
     }
-    return true;
+
+    event = xzalloc(sizeof *event);
+    hmap_insert(&event_table[OVN_EVENT_EMPTY_LB_BACKENDS],
+                &event->hmap_node, hash);
+    event->vip = vip;
+    event->protocol = protocol;
+    event->load_balancer = load_balancer;
+    event->timestamp = time_msec();
+    notify_pinctrl_main();
+
+    vip = NULL;
+    protocol = NULL;
+    load_balancer = NULL;
+
+cleanup:
+    free(vip);
+    free(protocol);
+    free(load_balancer);
+
+    return event != NULL;
 }
 
 static void
@@ -7755,7 +7788,9 @@ svc_monitors_run(struct rconn *swconn,
             if (svc_mon->n_success >= svc_mon->success_count) {
                 svc_mon->status = SVC_MON_ST_ONLINE;
                 svc_mon->n_success = 0;
+                svc_mon->n_failures = 0;
             }
+
             if (current_time >= svc_mon->next_send_time) {
                 svc_monitor_send_health_check(swconn, svc_mon);
                 next_run_time = svc_mon->wait_time;
@@ -7767,6 +7802,7 @@ svc_monitors_run(struct rconn *swconn,
         case SVC_MON_S_OFFLINE:
             if (svc_mon->n_failures >= svc_mon->failure_count) {
                 svc_mon->status = SVC_MON_ST_OFFLINE;
+                svc_mon->n_success = 0;
                 svc_mon->n_failures = 0;
             }
 
@@ -7812,7 +7848,6 @@ pinctrl_handle_tcp_svc_check(struct rconn *swconn,
         return false;
     }
 
-    uint32_t tcp_seq = ntohl(get_16aligned_be32(&th->tcp_seq));
     uint32_t tcp_ack = ntohl(get_16aligned_be32(&th->tcp_ack));
 
     if (th->tcp_dst != svc_mon->tp_src) {
@@ -7829,10 +7864,10 @@ pinctrl_handle_tcp_svc_check(struct rconn *swconn,
         svc_mon->n_success++;
         svc_mon->state = SVC_MON_S_ONLINE;
 
-        /* Send RST-ACK packet. */
-        svc_monitor_send_tcp_health_check__(swconn, svc_mon, TCP_RST | TCP_ACK,
-                                            htonl(tcp_ack + 1),
-                                            htonl(tcp_seq + 1), th->tcp_dst);
+        /* Send RST packet. */
+        svc_monitor_send_tcp_health_check__(swconn, svc_mon, TCP_RST,
+                                            htonl(tcp_ack),
+                                            htonl(0), th->tcp_dst);
         /* Calculate next_send_time. */
         svc_mon->next_send_time = time_msec() + svc_mon->interval;
         return true;
@@ -8184,6 +8219,8 @@ fdb_lookup(struct ovsdb_idl_index *sbrec_fdb_by_dp_key_mac, uint32_t dp_key,
 static void
 run_put_fdb(struct ovsdb_idl_txn *ovnsb_idl_txn,
             struct ovsdb_idl_index *sbrec_fdb_by_dp_key_mac,
+            struct ovsdb_idl_index *sbrec_port_binding_by_key,
+            struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
             const struct fdb_entry *fdb_e)
 {
     /* Convert ethernet argument to string form for database. */
@@ -8192,14 +8229,28 @@ run_put_fdb(struct ovsdb_idl_txn *ovnsb_idl_txn,
              ETH_ADDR_FMT, ETH_ADDR_ARGS(fdb_e->mac));
 
     /* Update or add an FDB entry. */
+    const struct sbrec_port_binding *sb_entry_pb = NULL;
+    const struct sbrec_port_binding *new_entry_pb = NULL;
     const struct sbrec_fdb *sb_fdb =
         fdb_lookup(sbrec_fdb_by_dp_key_mac, fdb_e->dp_key, mac_string);
     if (!sb_fdb) {
         sb_fdb = sbrec_fdb_insert(ovnsb_idl_txn);
         sbrec_fdb_set_dp_key(sb_fdb, fdb_e->dp_key);
         sbrec_fdb_set_mac(sb_fdb, mac_string);
+    } else {
+        /* check whether sb_fdb->port_key is vif or localnet type */
+        sb_entry_pb = lport_lookup_by_key(
+            sbrec_datapath_binding_by_key, sbrec_port_binding_by_key,
+            sb_fdb->dp_key, sb_fdb->port_key);
+        new_entry_pb = lport_lookup_by_key(
+            sbrec_datapath_binding_by_key, sbrec_port_binding_by_key,
+            fdb_e->dp_key, fdb_e->port_key);
+    }
+    /* Do not have localnet overwrite a previous vif entry */
+    if (!sb_entry_pb || !new_entry_pb || strcmp(sb_entry_pb->type, "") ||
+        strcmp(new_entry_pb->type, "localnet")) {
+        sbrec_fdb_set_port_key(sb_fdb, fdb_e->port_key);
     }
-    sbrec_fdb_set_port_key(sb_fdb, fdb_e->port_key);
 
     /* For backward compatibility check if timestamp column is available
      * in SB DB. */
@@ -8210,6 +8261,8 @@ run_put_fdb(struct ovsdb_idl_txn *ovnsb_idl_txn,
 
 static void
 run_put_fdbs(struct ovsdb_idl_txn *ovnsb_idl_txn,
+             struct ovsdb_idl_index *sbrec_port_binding_by_key,
+             struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
              struct ovsdb_idl_index *sbrec_fdb_by_dp_key_mac)
              OVS_REQUIRES(pinctrl_mutex)
 {
@@ -8219,7 +8272,9 @@ run_put_fdbs(struct ovsdb_idl_txn *ovnsb_idl_txn,
 
     const struct fdb_entry *fdb_e;
     HMAP_FOR_EACH (fdb_e, hmap_node, &put_fdbs) {
-        run_put_fdb(ovnsb_idl_txn, sbrec_fdb_by_dp_key_mac, fdb_e);
+        run_put_fdb(ovnsb_idl_txn, sbrec_fdb_by_dp_key_mac,
+                    sbrec_port_binding_by_key,
+                    sbrec_datapath_binding_by_key, fdb_e);
     }
     ovn_fdbs_flush(&put_fdbs);
 }
diff --git a/controller/statctrl.c b/controller/statctrl.c
index cb1545cbb..8cce97df8 100644
--- a/controller/statctrl.c
+++ b/controller/statctrl.c
@@ -233,7 +233,7 @@ statctrl_thread_handler(void *arg)
     struct statctrl_ctx *ctx = arg;
 
     /* OpenFlow connection to the switch. */
-    struct rconn *swconn = rconn_create(5, 0, DSCP_DEFAULT,
+    struct rconn *swconn = rconn_create(0, 0, DSCP_DEFAULT,
                                         1 << OFP15_VERSION);
 
     while (!latch_is_set(&ctx->exit_latch)) {
diff --git a/debian/changelog b/debian/changelog
index 96d132784..479d4c944 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,15 @@
+OVN (23.09.2-1) unstable; urgency=low
+   [ OVN team ]
+   * New upstream version
+
+ -- OVN team <dev@openvswitch.org>  Fri, 01 Dec 2023 14:41:31 -0500
+
+OVN (23.09.1-1) unstable; urgency=low
+   [ OVN team ]
+   * New upstream version
+
+ -- OVN team <dev@openvswitch.org>  Fri, 01 Dec 2023 14:41:31 -0500
+
 ovn (23.09.0-1) unstable; urgency=low
 
    * New upstream version
diff --git a/ic/ovn-ic.c b/ic/ovn-ic.c
index e2023c2ba..020b1d60b 100644
--- a/ic/ovn-ic.c
+++ b/ic/ovn-ic.c
@@ -1630,13 +1630,18 @@ collect_lr_routes(struct ic_context *ctx,
     const struct icnbrec_transit_switch *key;
 
     struct hmap *routes_ad;
+    const struct icnbrec_transit_switch *t_sw;
     for (int i = 0; i < ic_lr->n_isb_pbs; i++) {
         isb_pb = ic_lr->isb_pbs[i];
         key = icnbrec_transit_switch_index_init_row(
             ctx->icnbrec_transit_switch_by_name);
         icnbrec_transit_switch_index_set_name(key, isb_pb->transit_switch);
-        ts_name = icnbrec_transit_switch_index_find(
-            ctx->icnbrec_transit_switch_by_name, key)->name;
+        t_sw = icnbrec_transit_switch_index_find(
+             ctx->icnbrec_transit_switch_by_name, key);
+        if (!t_sw) {
+            continue;
+        }
+        ts_name = t_sw->name;
         icnbrec_transit_switch_index_destroy_row(key);
         routes_ad = shash_find_data(routes_ad_by_ts, ts_name);
         if (!routes_ad) {
@@ -2216,10 +2221,19 @@ main(int argc, char *argv[])
                 ovn_db_run(&ctx);
             }
 
-            ovsdb_idl_loop_commit_and_wait(&ovnnb_idl_loop);
-            ovsdb_idl_loop_commit_and_wait(&ovnsb_idl_loop);
-            ovsdb_idl_loop_commit_and_wait(&ovninb_idl_loop);
-            ovsdb_idl_loop_commit_and_wait(&ovnisb_idl_loop);
+            int rc1 = ovsdb_idl_loop_commit_and_wait(&ovnnb_idl_loop);
+            int rc2 = ovsdb_idl_loop_commit_and_wait(&ovnsb_idl_loop);
+            int rc3 = ovsdb_idl_loop_commit_and_wait(&ovninb_idl_loop);
+            int rc4 = ovsdb_idl_loop_commit_and_wait(&ovnisb_idl_loop);
+            if (!rc1 || !rc2 || !rc3 || !rc4) {
+                VLOG_DBG(" a transaction failed in: %s %s %s %s",
+                         !rc1 ? "nb" : "", !rc2 ? "sb" : "",
+                         !rc3 ? "ic_nb" : "", rc4 ? "ic_sb" : "");
+                /* A transaction failed. Wake up immediately to give
+                 * opportunity to send the proper transaction
+                 */
+                poll_immediate_wake();
+            }
         } else {
             /* ovn-ic is paused
              *    - we still want to handle any db updates and update the
diff --git a/include/ovn/features.h b/include/ovn/features.h
index 3bf536127..2c47ab766 100644
--- a/include/ovn/features.h
+++ b/include/ovn/features.h
@@ -26,6 +26,7 @@
 #define OVN_FEATURE_MAC_BINDING_TIMESTAMP "mac-binding-timestamp"
 #define OVN_FEATURE_CT_LB_RELATED "ovn-ct-lb-related"
 #define OVN_FEATURE_FDB_TIMESTAMP "fdb-timestamp"
+#define OVN_FEATURE_LS_DPG_COLUMN "ls-dpg-column"
 
 /* OVS datapath supported features.  Based on availability OVN might generate
  * different types of openflows.
@@ -48,5 +49,8 @@ void ovs_feature_support_destroy(void);
 bool ovs_feature_is_supported(enum ovs_feature_value feature);
 bool ovs_feature_support_run(const struct smap *ovs_capabilities,
                              const char *br_name);
+bool ovs_feature_set_discovered(void);
+uint32_t ovs_feature_max_meters_get(void);
+uint32_t ovs_feature_max_select_groups_get(void);
 
 #endif
diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
index a7b64ef67..272277ec4 100644
--- a/include/ovn/logical-fields.h
+++ b/include/ovn/logical-fields.h
@@ -77,6 +77,7 @@ enum mff_log_flags_bits {
     MLF_CHECK_PORT_SEC_BIT = 12,
     MLF_LOOKUP_COMMIT_ECMP_NH_BIT = 13,
     MLF_USE_LB_AFF_SESSION_BIT = 14,
+    MLF_LOCALNET_BIT = 15,
 };
 
 /* MFF_LOG_FLAGS_REG flag assignments */
@@ -124,6 +125,10 @@ enum mff_log_flags {
     MLF_LOOKUP_COMMIT_ECMP_NH = (1 << MLF_LOOKUP_COMMIT_ECMP_NH_BIT),
 
     MLF_USE_LB_AFF_SESSION = (1 << MLF_USE_LB_AFF_SESSION_BIT),
+
+    /* Indicate that the port is localnet. */
+    MLF_LOCALNET = (1 << MLF_LOCALNET_BIT),
+
 };
 
 /* OVN logical fields
diff --git a/lib/actions.c b/lib/actions.c
index b880927b6..4d408d82d 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -5004,6 +5004,7 @@ encode_COMMIT_LB_AFF(const struct ovnact_commit_lb_aff *lb_aff,
     ol->hard_timeout = OFP_FLOW_PERMANENT;
     ol->priority = OFP_DEFAULT_PRIORITY;
     ol->table_id = OFTABLE_CHK_LB_AFFINITY;
+    ol->cookie = htonll(ep->lflow_uuid.parts[0]);
 
     /* Match on metadata of the packet that created the new table. */
     ol_spec = ofpbuf_put_zeros(ofpacts, sizeof *ol_spec);
diff --git a/lib/extend-table.c b/lib/extend-table.c
index ebb1a054c..8e79d7177 100644
--- a/lib/extend-table.c
+++ b/lib/extend-table.c
@@ -17,9 +17,9 @@
 #include <config.h>
 #include <string.h>
 
-#include "bitmap.h"
 #include "extend-table.h"
 #include "hash.h"
+#include "id-pool.h"
 #include "lib/uuid.h"
 #include "openvswitch/vlog.h"
 
@@ -30,13 +30,29 @@ ovn_extend_table_delete_desired(struct ovn_extend_table *table,
                                 struct ovn_extend_table_lflow_to_desired *l);
 
 void
-ovn_extend_table_init(struct ovn_extend_table *table)
+ovn_extend_table_init(struct ovn_extend_table *table, const char *table_name,
+                      uint32_t n_ids)
 {
-    table->table_ids = bitmap_allocate(MAX_EXT_TABLE_ID);
-    bitmap_set1(table->table_ids, 0); /* table id 0 is invalid. */
-    hmap_init(&table->desired);
-    hmap_init(&table->lflow_to_desired);
-    hmap_init(&table->existing);
+    *table = (struct ovn_extend_table) {
+        .name = xstrdup(table_name),
+        .n_ids = n_ids,
+        /* Table id 0 is invalid, set id-pool base to 1. */
+        .table_ids = id_pool_create(1, n_ids),
+        .desired = HMAP_INITIALIZER(&table->desired),
+        .lflow_to_desired = HMAP_INITIALIZER(&table->lflow_to_desired),
+        .existing = HMAP_INITIALIZER(&table->existing),
+    };
+}
+
+void
+ovn_extend_table_reinit(struct ovn_extend_table *table, uint32_t n_ids)
+{
+    if (n_ids != table->n_ids) {
+        ovn_extend_table_clear(table, true);
+        id_pool_destroy(table->table_ids);
+        table->table_ids = id_pool_create(1, n_ids);
+        table->n_ids = n_ids;
+    }
 }
 
 static struct ovn_extend_table_info *
@@ -117,13 +133,13 @@ ovn_extend_table_add_desired_to_lflow(struct ovn_extend_table *table,
         ovs_list_init(&l->desired);
         hmap_insert(&table->lflow_to_desired, &l->hmap_node,
                     uuid_hash(lflow_uuid));
-        VLOG_DBG("%s: add new lflow_to_desired entry "UUID_FMT,
-                 __func__, UUID_ARGS(lflow_uuid));
+        VLOG_DBG("%s: table %s: add new lflow_to_desired entry "UUID_FMT,
+                 __func__, table->name, UUID_ARGS(lflow_uuid));
     }
 
     ovs_list_insert(&l->desired, &r->list_node);
-    VLOG_DBG("%s: lflow "UUID_FMT" use new item %s, id %"PRIu32,
-             __func__, UUID_ARGS(lflow_uuid), r->desired->name,
+    VLOG_DBG("%s: table %s: lflow "UUID_FMT" use new item %s, id %"PRIu32,
+             __func__, table->name, UUID_ARGS(lflow_uuid), r->desired->name,
              r->desired->table_id);
 }
 
@@ -160,10 +176,11 @@ ovn_extend_info_add_lflow_ref(struct ovn_extend_table *table,
 }
 
 static void
-ovn_extend_info_del_lflow_ref(struct ovn_extend_table_lflow_ref *r)
+ovn_extend_info_del_lflow_ref(struct ovn_extend_table *table,
+                              struct ovn_extend_table_lflow_ref *r)
 {
-    VLOG_DBG("%s: name %s, lflow "UUID_FMT" n %"PRIuSIZE, __func__,
-             r->desired->name, UUID_ARGS(&r->lflow_uuid),
+    VLOG_DBG("%s: table %s: name %s, lflow "UUID_FMT" n %"PRIuSIZE, __func__,
+             table->name, r->desired->name, UUID_ARGS(&r->lflow_uuid),
              hmap_count(&r->desired->references));
     hmap_remove(&r->desired->references, &r->hmap_node);
     ovs_list_remove(&r->list_node);
@@ -191,8 +208,8 @@ ovn_extend_table_clear(struct ovn_extend_table *table, bool existing)
         if (g->peer) {
             g->peer->peer = NULL;
         } else {
-            /* Unset the bitmap because the peer is deleted already. */
-            bitmap_set0(table->table_ids, g->table_id);
+            /* Unset the id because the peer is deleted already. */
+            id_pool_free_id(table->table_ids, g->table_id);
         }
         ovn_extend_table_info_destroy(g);
     }
@@ -206,7 +223,8 @@ ovn_extend_table_destroy(struct ovn_extend_table *table)
     hmap_destroy(&table->lflow_to_desired);
     ovn_extend_table_clear(table, true);
     hmap_destroy(&table->existing);
-    bitmap_free(table->table_ids);
+    id_pool_destroy(table->table_ids);
+    free(table->name);
 }
 
 /* Remove an entry from existing table */
@@ -221,7 +239,7 @@ ovn_extend_table_remove_existing(struct ovn_extend_table *table,
         existing->peer->peer = NULL;
     } else {
         /* Dealloc the ID. */
-        bitmap_set0(table->table_ids, existing->table_id);
+        id_pool_free_id(table->table_ids, existing->table_id);
     }
     ovn_extend_table_info_destroy(existing);
 }
@@ -234,15 +252,15 @@ ovn_extend_table_delete_desired(struct ovn_extend_table *table,
     struct ovn_extend_table_lflow_ref *r;
     LIST_FOR_EACH_SAFE (r, list_node, &l->desired) {
         struct ovn_extend_table_info *e = r->desired;
-        ovn_extend_info_del_lflow_ref(r);
+        ovn_extend_info_del_lflow_ref(table, r);
         if (hmap_is_empty(&e->references)) {
-            VLOG_DBG("%s: %s, "UUID_FMT, __func__,
-                     e->name, UUID_ARGS(&l->lflow_uuid));
+            VLOG_DBG("%s: table %s: %s, "UUID_FMT, __func__,
+                     table->name, e->name, UUID_ARGS(&l->lflow_uuid));
             hmap_remove(&table->desired, &e->hmap_node);
             if (e->peer) {
                 e->peer->peer = NULL;
             } else {
-                bitmap_set0(table->table_ids, e->table_id);
+                id_pool_free_id(table->table_ids, e->table_id);
             }
             ovn_extend_table_info_destroy(e);
         }
@@ -284,7 +302,7 @@ ovn_extend_table_sync(struct ovn_extend_table *table)
     }
 }
 
-/* Assign a new table ID for the table information from the bitmap.
+/* Assign a new table ID for the table information from the ID pool.
  * If it already exists, return the old ID. */
 uint32_t
 ovn_extend_table_assign_id(struct ovn_extend_table *table, const char *name,
@@ -298,9 +316,9 @@ ovn_extend_table_assign_id(struct ovn_extend_table *table, const char *name,
     /* Check whether we have non installed but allocated group_id. */
     HMAP_FOR_EACH_WITH_HASH (table_info, hmap_node, hash, &table->desired) {
         if (!strcmp(table_info->name, name)) {
-            VLOG_DBG("ovn_externd_table_assign_id: reuse old id %"PRIu32
-                     " for %s, used by lflow "UUID_FMT,
-                     table_info->table_id, table_info->name,
+            VLOG_DBG("ovn_extend_table_assign_id: table %s: "
+                     "reuse old id %"PRIu32" for %s, used by lflow "UUID_FMT,
+                     table->name, table_info->table_id, table_info->name,
                      UUID_ARGS(&lflow_uuid));
             ovn_extend_info_add_lflow_ref(table, table_info, &lflow_uuid);
             return table_info->table_id;
@@ -320,15 +338,13 @@ ovn_extend_table_assign_id(struct ovn_extend_table *table, const char *name,
 
     if (!existing_info) {
         /* Reserve a new id. */
-        table_id = bitmap_scan(table->table_ids, 0, 1, MAX_EXT_TABLE_ID + 1);
-    }
+        if (!id_pool_alloc_id(table->table_ids, &table_id)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
 
-    if (table_id == MAX_EXT_TABLE_ID + 1) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-        VLOG_ERR_RL(&rl, "%"PRIu32" out of table ids.", table_id);
-        return EXT_TABLE_ID_INVALID;
+            VLOG_ERR_RL(&rl, "table %s: out of table ids.", table->name);
+            return EXT_TABLE_ID_INVALID;
+        }
     }
-    bitmap_set1(table->table_ids, table_id);
 
     table_info = ovn_extend_table_info_alloc(name, table_id, existing_info,
                                              hash);
diff --git a/lib/extend-table.h b/lib/extend-table.h
index b43a146b4..90e6e470d 100644
--- a/lib/extend-table.h
+++ b/lib/extend-table.h
@@ -17,18 +17,21 @@
 #ifndef EXTEND_TABLE_H
 #define EXTEND_TABLE_H 1
 
-#define MAX_EXT_TABLE_ID 65535
 #define EXT_TABLE_ID_INVALID 0
 
 #include "openvswitch/hmap.h"
 #include "openvswitch/list.h"
 #include "openvswitch/uuid.h"
 
+struct id_pool;
+
 /* Used to manage expansion tables associated with Flow table,
  * such as the Group Table or Meter Table. */
 struct ovn_extend_table {
-    unsigned long *table_ids;  /* Used as a bitmap with value set
-                                * for allocated ids in either desired or
+    char *name; /* Used to identify this table in a user friendly way,
+                 * e.g., for logging. */
+    uint32_t n_ids;
+    struct id_pool *table_ids; /* Used to allocate ids in either desired or
                                 * existing (or both).  If the same "name"
                                 * exists in both desired and existing tables,
                                 * they must share the same ID.  The "peer"
@@ -81,7 +84,9 @@ struct ovn_extend_table_lflow_ref {
     struct ovn_extend_table_info *desired;
 };
 
-void ovn_extend_table_init(struct ovn_extend_table *);
+void ovn_extend_table_init(struct ovn_extend_table *, const char *table_name,
+                           uint32_t n_ids);
+void ovn_extend_table_reinit(struct ovn_extend_table *, uint32_t n_ids);
 
 void ovn_extend_table_destroy(struct ovn_extend_table *);
 
diff --git a/lib/features.c b/lib/features.c
index d24e8f6c5..b31b5f6dd 100644
--- a/lib/features.c
+++ b/lib/features.c
@@ -27,6 +27,7 @@
 #include "openvswitch/rconn.h"
 #include "openvswitch/ofp-msgs.h"
 #include "openvswitch/ofp-meter.h"
+#include "openvswitch/ofp-group.h"
 #include "openvswitch/ofp-util.h"
 #include "ovn/features.h"
 
@@ -81,6 +82,18 @@ static struct ovs_feature all_ovs_features[] = {
 /* A bitmap of OVS features that have been detected as 'supported'. */
 static uint32_t supported_ovs_features;
 
+/* Last set of received feature replies. */
+static struct ofputil_meter_features ovs_meter_features_reply;
+static struct ofputil_group_features ovs_group_features_reply;
+
+/* Currently discovered set of features. */
+static struct ofputil_meter_features ovs_meter_features;
+static struct ofputil_group_features ovs_group_features;
+
+/* Number of features replies still expected to receive for the requests
+ * we sent already. */
+static uint32_t n_features_reply_expected;
+
 static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
 
 /* ovs-vswitchd connection. */
@@ -145,11 +158,19 @@ ovs_feature_get_openflow_cap(const char *br_name)
 
     /* send new requests just after reconnect. */
     if (conn_seq_no != rconn_get_connection_seqno(swconn)) {
-        /* dump datapath meter capabilities. */
+        n_features_reply_expected = 0;
+
+        /* Dump OpenFlow switch meter capabilities. */
         msg = ofpraw_alloc(OFPRAW_OFPST13_METER_FEATURES_REQUEST,
                            rconn_get_version(swconn), 0);
         rconn_send(swconn, msg, NULL);
+        n_features_reply_expected++;
+        /* Dump OpenFlow switch group capabilities. */
+        msg = ofputil_encode_group_features_request(rconn_get_version(swconn));
+        rconn_send(swconn, msg, NULL);
+        n_features_reply_expected++;
     }
+    conn_seq_no = rconn_get_connection_seqno(swconn);
 
     bool ret = false;
     for (int i = 0; i < 50; i++) {
@@ -163,21 +184,13 @@ ovs_feature_get_openflow_cap(const char *br_name)
         ofptype_decode(&type, oh);
 
         if (type == OFPTYPE_METER_FEATURES_STATS_REPLY) {
-            struct ofputil_meter_features mf;
-            ofputil_decode_meter_features(oh, &mf);
-
-            bool old_state = supported_ovs_features & OVS_DP_METER_SUPPORT;
-            bool new_state = mf.max_meters > 0;
-
-            if (old_state != new_state) {
-                ret = true;
-                if (new_state) {
-                    supported_ovs_features |= OVS_DP_METER_SUPPORT;
-                } else {
-                    supported_ovs_features &= ~OVS_DP_METER_SUPPORT;
-                }
-            }
-            conn_seq_no = rconn_get_connection_seqno(swconn);
+            ofputil_decode_meter_features(oh, &ovs_meter_features_reply);
+            ovs_assert(n_features_reply_expected);
+            n_features_reply_expected--;
+        } else if (type == OFPTYPE_GROUP_FEATURES_STATS_REPLY) {
+            ofputil_decode_group_features_reply(oh, &ovs_group_features_reply);
+            ovs_assert(n_features_reply_expected);
+            n_features_reply_expected--;
         } else if (type == OFPTYPE_ECHO_REQUEST) {
             rconn_send(swconn, ofputil_encode_echo_reply(oh), NULL);
         }
@@ -186,6 +199,26 @@ ovs_feature_get_openflow_cap(const char *br_name)
     rconn_run_wait(swconn);
     rconn_recv_wait(swconn);
 
+    /* If all feature replies were received, update the set of supported
+     * features. */
+    if (!n_features_reply_expected) {
+        if (memcmp(&ovs_meter_features, &ovs_meter_features_reply,
+                   sizeof ovs_meter_features_reply)) {
+            ovs_meter_features = ovs_meter_features_reply;
+            if (ovs_meter_features.max_meters) {
+                supported_ovs_features |= OVS_DP_METER_SUPPORT;
+            } else {
+                supported_ovs_features &= ~OVS_DP_METER_SUPPORT;
+            }
+            ret = true;
+        }
+        if (memcmp(&ovs_group_features, &ovs_group_features_reply,
+                   sizeof ovs_group_features_reply)) {
+            ovs_group_features = ovs_group_features_reply;
+            ret = true;
+        }
+    }
+
     return ret;
 }
 
@@ -229,3 +262,26 @@ ovs_feature_support_run(const struct smap *ovs_capabilities,
     }
     return updated;
 }
+
+bool
+ovs_feature_set_discovered(void)
+{
+    /* The supported feature set has been discovered if we're connected
+     * to OVS and it replied to all our feature request messages. */
+    return swconn && rconn_is_connected(swconn) &&
+           n_features_reply_expected == 0;
+}
+
+/* Returns the number of meters the OVS datapath supports. */
+uint32_t
+ovs_feature_max_meters_get(void)
+{
+    return ovs_meter_features.max_meters;
+}
+
+/* Returns the number of select groups the OVS datapath supports. */
+uint32_t
+ovs_feature_max_select_groups_get(void)
+{
+    return ovs_group_features.max_groups[OFPGT11_SELECT];
+}
diff --git a/lib/logical-fields.c b/lib/logical-fields.c
index fd509d9ee..7a1e66d0a 100644
--- a/lib/logical-fields.c
+++ b/lib/logical-fields.c
@@ -129,6 +129,10 @@ ovn_init_symtab(struct shash *symtab)
              MLF_USE_SNAT_ZONE);
     expr_symtab_add_subfield(symtab, "flags.use_snat_zone", NULL,
                              flags_str);
+    snprintf(flags_str, sizeof flags_str, "flags[%d]",
+             MLF_LOCALNET_BIT);
+    expr_symtab_add_subfield(symtab, "flags.localnet", NULL,
+                             flags_str);
 
     /* Connection tracking state. */
     expr_symtab_add_field_scoped(symtab, "ct_mark", MFF_CT_MARK, NULL, false,
diff --git a/lib/memory-trim.c b/lib/memory-trim.c
index d8c6b4743..be8983fa7 100644
--- a/lib/memory-trim.c
+++ b/lib/memory-trim.c
@@ -71,13 +71,14 @@ memory_trimmer_can_run(struct memory_trimmer *mt)
     }
 
     long long int now = time_msec();
-    if (now < mt->last_active_ms || now < mt->trim_timeout_ms) {
+    if (now < mt->last_active_ms) {
         VLOG_WARN_RL(&rl, "Detected last active timestamp overflow");
         mt->recently_active = false;
         return true;
     }
 
-    if (now - mt->trim_timeout_ms >= mt->last_active_ms) {
+    if (now > mt->trim_timeout_ms
+        && now - mt->trim_timeout_ms >= mt->last_active_ms) {
         VLOG_INFO_RL(&rl, "Detected inactivity "
                     "(last active %lld ms ago): trimming memory",
                     now - mt->last_active_ms);
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index ffe295696..33105202f 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -1302,3 +1302,34 @@ sorted_array_apply_diff(const struct sorted_array *a1,
         apply_callback(arg, a2->arr[idx2], false);
     }
 }
+
+/* Call for the unixctl command that will store the connection and
+ * set the appropriate conditions. */
+void
+ovn_exit_command_callback(struct unixctl_conn *conn, int argc,
+                          const char *argv[], void *exit_args_)
+{
+    struct ovn_exit_args *exit_args = exit_args_;
+
+    exit_args->n_conns++;
+    exit_args->conns = xrealloc(exit_args->conns,
+                                exit_args->n_conns * sizeof *exit_args->conns);
+
+    exit_args->exiting = true;
+    exit_args->conns[exit_args->n_conns - 1] = conn;
+
+    if (!exit_args->restart) {
+        exit_args->restart = argc == 2 && !strcmp(argv[1], "--restart");
+    }
+}
+
+/* Reply to all waiting unixctl connections and free the connection array.
+ * This function should be called after the heaviest cleanup has finished. */
+void
+ovn_exit_args_finish(struct ovn_exit_args *exit_args)
+{
+    for (size_t i = 0; i < exit_args->n_conns; i++) {
+        unixctl_command_reply(exit_args->conns[i], NULL);
+    }
+    free(exit_args->conns);
+}
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index 16f18353d..bff50dbde 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -474,4 +474,17 @@ void sorted_array_apply_diff(const struct sorted_array *a1,
                                                     const char *item,
                                                     bool add),
                              const void *arg);
+
+/* Utilities around properly handling exit command. */
+struct ovn_exit_args {
+    struct unixctl_conn **conns;
+    size_t n_conns;
+    bool exiting;
+    bool restart;
+};
+
+void ovn_exit_command_callback(struct unixctl_conn *conn, int argc,
+                               const char *argv[], void *exit_args_);
+void ovn_exit_args_finish(struct ovn_exit_args *exit_args);
+
 #endif /* OVN_UTIL_H */
diff --git a/northd/en-lb-data.c b/northd/en-lb-data.c
index 250cec848..d06f46a54 100644
--- a/northd/en-lb-data.c
+++ b/northd/en-lb-data.c
@@ -144,6 +144,12 @@ lb_data_load_balancer_handler(struct engine_node *node, void *data)
 
     const struct nbrec_load_balancer *tracked_lb;
     NBREC_LOAD_BALANCER_TABLE_FOR_EACH_TRACKED (tracked_lb, nb_lb_table) {
+        /* "New" + "Deleted" is a no-op. */
+        if (nbrec_load_balancer_is_new(tracked_lb)
+            && nbrec_load_balancer_is_deleted(tracked_lb)) {
+            continue;
+        }
+
         struct ovn_northd_lb *lb;
         if (nbrec_load_balancer_is_new(tracked_lb)) {
             /* New load balancer. */
@@ -153,19 +159,22 @@ lb_data_load_balancer_handler(struct engine_node *node, void *data)
             add_crupdated_lb_to_tracked_data(lb, trk_lb_data,
                                              lb->health_checks);
             trk_lb_data->has_routable_lb |= lb->routable;
-        } else if (nbrec_load_balancer_is_deleted(tracked_lb)) {
-            lb = ovn_northd_lb_find(&lb_data->lbs,
-                                    &tracked_lb->header_.uuid);
-            ovs_assert(lb);
+            continue;
+        }
+
+        /* Protect against "spurious" deletes reported by the IDL. */
+        lb = ovn_northd_lb_find(&lb_data->lbs, &tracked_lb->header_.uuid);
+        if (!lb) {
+            continue;
+        }
+
+        if (nbrec_load_balancer_is_deleted(tracked_lb)) {
             hmap_remove(&lb_data->lbs, &lb->hmap_node);
             add_deleted_lb_to_tracked_data(lb, trk_lb_data,
                                            lb->health_checks);
             trk_lb_data->has_routable_lb |= lb->routable;
         } else {
             /* Load balancer updated. */
-            lb = ovn_northd_lb_find(&lb_data->lbs,
-                                    &tracked_lb->header_.uuid);
-            ovs_assert(lb);
             bool health_checks = lb->health_checks;
             struct sset old_ips_v4 = SSET_INITIALIZER(&old_ips_v4);
             struct sset old_ips_v6 = SSET_INITIALIZER(&old_ips_v6);
@@ -217,6 +226,12 @@ lb_data_load_balancer_group_handler(struct engine_node *node, void *data)
     const struct nbrec_load_balancer_group *tracked_lb_group;
     NBREC_LOAD_BALANCER_GROUP_TABLE_FOR_EACH_TRACKED (tracked_lb_group,
                                                       nb_lbg_table) {
+        /* "New" + "Deleted" is a no-op. */
+        if (nbrec_load_balancer_group_is_new(tracked_lb_group)
+            && nbrec_load_balancer_group_is_deleted(tracked_lb_group)) {
+            continue;
+        }
+
         if (nbrec_load_balancer_group_is_new(tracked_lb_group)) {
             struct ovn_lb_group *lb_group =
                 create_lb_group(tracked_lb_group, &lb_data->lbs,
@@ -228,21 +243,22 @@ lb_data_load_balancer_group_handler(struct engine_node *node, void *data)
             }
 
             trk_lb_data->has_routable_lb |= lb_group->has_routable_lb;
-        } else if (nbrec_load_balancer_group_is_deleted(tracked_lb_group)) {
-            struct ovn_lb_group *lb_group;
-            lb_group = ovn_lb_group_find(&lb_data->lbgrps,
-                                         &tracked_lb_group->header_.uuid);
-            ovs_assert(lb_group);
+            continue;
+        }
+
+        /* Protect against "spurious" deletes reported by the IDL. */
+        struct ovn_lb_group *lb_group;
+        lb_group = ovn_lb_group_find(&lb_data->lbgrps,
+                                     &tracked_lb_group->header_.uuid);
+        if (!lb_group) {
+            continue;
+        }
+
+        if (nbrec_load_balancer_group_is_deleted(tracked_lb_group)) {
             hmap_remove(&lb_data->lbgrps, &lb_group->hmap_node);
             add_deleted_lbgrp_to_tracked_data(lb_group, trk_lb_data);
             trk_lb_data->has_routable_lb |= lb_group->has_routable_lb;
         } else {
-
-            struct ovn_lb_group *lb_group;
-            lb_group = ovn_lb_group_find(&lb_data->lbgrps,
-                                         &tracked_lb_group->header_.uuid);
-            ovs_assert(lb_group);
-
             /* Determine the lbs which are added or deleted for this
              * lb group and add them to tracked data.
              * Eg.  If an lb group lbg1 before the update had [lb1, lb2, lb3]
diff --git a/northd/en-sync-sb.c b/northd/en-sync-sb.c
index aae396a43..2ec3bf54f 100644
--- a/northd/en-sync-sb.c
+++ b/northd/en-sync-sb.c
@@ -220,7 +220,8 @@ en_sync_to_sb_lb_run(struct engine_node *node, void *data OVS_UNUSED)
     struct northd_data *northd_data = engine_get_input_data("northd", node);
 
     sync_lbs(eng_ctx->ovnsb_idl_txn, sb_load_balancer_table,
-             &northd_data->ls_datapaths, &northd_data->lb_datapaths_map);
+             &northd_data->ls_datapaths, &northd_data->lr_datapaths,
+             &northd_data->lb_datapaths_map, &northd_data->features);
     engine_set_node_state(node, EN_UPDATED);
 }
 
@@ -248,6 +249,23 @@ sync_to_sb_lb_northd_handler(struct engine_node *node, void *data OVS_UNUSED)
     return true;
 }
 
+bool
+sync_to_sb_lb_sb_load_balancer(struct engine_node *node, void *data OVS_UNUSED)
+{
+    const struct sbrec_load_balancer_table *sb_load_balancer_table =
+        EN_OVSDB_GET(engine_get_input("SB_load_balancer", node));
+
+    /* The only reason to handle SB.Load_Balancer updates is to detect
+     * spurious records being created in clustered databases due to
+     * lack of indexing on the SB.Load_Balancer table.  All other changes
+     * are valid and performed by northd, the only write-client for
+     * this table. */
+    if (check_sb_lb_duplicates(sb_load_balancer_table)) {
+        return false;
+    }
+    return true;
+}
+
 /* sync_to_sb_pb engine node functions.
  * This engine node syncs the SB Port Bindings (partly).
  * en_northd engine create the SB Port binding rows and
diff --git a/northd/en-sync-sb.h b/northd/en-sync-sb.h
index f08565eee..3bcbb8259 100644
--- a/northd/en-sync-sb.h
+++ b/northd/en-sync-sb.h
@@ -21,6 +21,8 @@ void *en_sync_to_sb_lb_init(struct engine_node *, struct engine_arg *);
 void en_sync_to_sb_lb_run(struct engine_node *, void *data);
 void en_sync_to_sb_lb_cleanup(void *data);
 bool sync_to_sb_lb_northd_handler(struct engine_node *, void *data OVS_UNUSED);
+bool sync_to_sb_lb_sb_load_balancer(struct engine_node *,
+                                    void *data OVS_UNUSED);
 
 void *en_sync_to_sb_pb_init(struct engine_node *, struct engine_arg *);
 void en_sync_to_sb_pb_run(struct engine_node *, void *data);
diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
index 8b0817117..04df0b06c 100644
--- a/northd/inc-proc-northd.c
+++ b/northd/inc-proc-northd.c
@@ -230,7 +230,8 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
 
     engine_add_input(&en_sync_to_sb_lb, &en_northd,
                      sync_to_sb_lb_northd_handler);
-    engine_add_input(&en_sync_to_sb_lb, &en_sb_load_balancer, NULL);
+    engine_add_input(&en_sync_to_sb_lb, &en_sb_load_balancer,
+                     sync_to_sb_lb_sb_load_balancer);
 
     engine_add_input(&en_sync_to_sb_pb, &en_northd,
                      sync_to_sb_pb_northd_handler);
diff --git a/northd/northd.c b/northd/northd.c
index 04afe62a8..8aeca33f8 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -491,6 +491,15 @@ build_chassis_features(const struct sbrec_chassis_table *sbrec_chassis_table,
             chassis_features->fdb_timestamp) {
             chassis_features->fdb_timestamp = false;
         }
+
+        bool ls_dpg_column =
+            smap_get_bool(&chassis->other_config,
+                          OVN_FEATURE_LS_DPG_COLUMN,
+                          false);
+        if (!ls_dpg_column &&
+            chassis_features->ls_dpg_column) {
+            chassis_features->ls_dpg_column = false;
+        }
     }
 }
 
@@ -4511,6 +4520,7 @@ struct sb_lb {
 
     const struct sbrec_load_balancer *slb;
     struct ovn_dp_group *dpg;
+    struct ovn_dp_group *lr_dpg;
     struct uuid lb_uuid;
 };
 
@@ -4533,10 +4543,13 @@ find_slb_in_sb_lbs(struct hmap *sb_lbs, const struct uuid *lb_uuid)
 void
 sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
          const struct sbrec_load_balancer_table *sbrec_load_balancer_table,
-         struct ovn_datapaths *ls_datapaths, struct hmap *lb_dps_map)
+         struct ovn_datapaths *ls_datapaths,
+         struct ovn_datapaths *lr_datapaths,
+         struct hmap *lb_dps_map,
+         struct chassis_features *chassis_features)
 {
-    struct hmap dp_groups = HMAP_INITIALIZER(&dp_groups);
-    size_t bitmap_len = ods_size(ls_datapaths);
+    struct hmap ls_dp_groups = HMAP_INITIALIZER(&ls_dp_groups);
+    struct hmap lr_dp_groups = HMAP_INITIALIZER(&lr_dp_groups);
     struct ovn_lb_datapaths *lb_dps;
     struct hmap sb_lbs = HMAP_INITIALIZER(&sb_lbs);
 
@@ -4554,7 +4567,8 @@ sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
         }
 
         /* Delete any SB load balancer entries that refer to NB load balancers
-         * that don't exist anymore or are not applied to switches anymore.
+         * that don't exist anymore or are not applied to switches/routers
+         * anymore.
          *
          * There is also a special case in which duplicate LBs might be created
          * in the SB, e.g., due to the fact that OVSDB only ensures
@@ -4562,7 +4576,8 @@ sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
          * are not indexed in any way.
          */
         lb_dps = ovn_lb_datapaths_find(lb_dps_map, &lb_uuid);
-        if (!lb_dps || !lb_dps->n_nb_ls || !hmapx_add(&existing_lbs, lb_dps)) {
+        if (!lb_dps || (!lb_dps->n_nb_ls && !lb_dps->n_nb_lr) ||
+            !hmapx_add(&existing_lbs, lb_dps)) {
             sbrec_load_balancer_delete(sbrec_lb);
             continue;
         }
@@ -4573,12 +4588,26 @@ sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
         hmap_insert(&sb_lbs, &sb_lb->hmap_node, uuid_hash(&lb_uuid));
 
         /* Find or create datapath group for this load balancer. */
-        sb_lb->dpg = ovn_dp_group_get_or_create(ovnsb_txn, &dp_groups,
-                                                sb_lb->slb->datapath_group,
-                                                lb_dps->n_nb_ls,
-                                                lb_dps->nb_ls_map,
-                                                bitmap_len, true,
-                                                ls_datapaths, NULL);
+        if (lb_dps->n_nb_ls) {
+            struct sbrec_logical_dp_group *ls_datapath_group
+                = chassis_features->ls_dpg_column
+                    ? sb_lb->slb->ls_datapath_group
+                    : sb_lb->slb->datapath_group; /* deprecated */
+            sb_lb->dpg = ovn_dp_group_get_or_create(
+                    ovnsb_txn, &ls_dp_groups,
+                    ls_datapath_group,
+                    lb_dps->n_nb_ls, lb_dps->nb_ls_map,
+                    ods_size(ls_datapaths), true,
+                    ls_datapaths, NULL);
+        }
+        if (lb_dps->n_nb_lr) {
+            sb_lb->lr_dpg = ovn_dp_group_get_or_create(
+                    ovnsb_txn, &lr_dp_groups,
+                    sb_lb->slb->lr_datapath_group,
+                    lb_dps->n_nb_lr, lb_dps->nb_lr_map,
+                    ods_size(lr_datapaths), false,
+                    NULL, lr_datapaths);
+        }
     }
     hmapx_destroy(&existing_lbs);
 
@@ -4586,7 +4615,7 @@ sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
      * the SB load balancer columns. */
     HMAP_FOR_EACH (lb_dps, hmap_node, lb_dps_map) {
 
-        if (!lb_dps->n_nb_ls) {
+        if (!lb_dps->n_nb_ls && !lb_dps->n_nb_lr) {
             continue;
         }
 
@@ -4599,8 +4628,8 @@ sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
 
         struct sb_lb *sb_lb = find_slb_in_sb_lbs(&sb_lbs,
                                             &lb_dps->lb->nlb->header_.uuid);
-        ovs_assert(!sb_lb || (sb_lb->slb && sb_lb->dpg));
-        struct ovn_dp_group *lb_dpg = NULL;
+        ovs_assert(!sb_lb || (sb_lb->slb && (sb_lb->dpg || sb_lb->lr_dpg)));
+        struct ovn_dp_group *lb_dpg = NULL, *lb_lr_dpg = NULL;
         if (!sb_lb) {
             sbrec_lb = sbrec_load_balancer_insert(ovnsb_txn);
             char *lb_id = xasprintf(
@@ -4612,15 +4641,29 @@ sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
         } else {
             sbrec_lb = sb_lb->slb;
             lb_dpg = sb_lb->dpg;
+            lb_lr_dpg = sb_lb->lr_dpg;
         }
 
         /* Find or create datapath group for this load balancer. */
-        if (!lb_dpg) {
-            lb_dpg = ovn_dp_group_get_or_create(ovnsb_txn, &dp_groups,
-                                                sbrec_lb->datapath_group,
-                                                lb_dps->n_nb_ls,
-                                                lb_dps->nb_ls_map, bitmap_len,
-                                                true, ls_datapaths, NULL);
+        if (!lb_dpg && lb_dps->n_nb_ls) {
+            struct sbrec_logical_dp_group *ls_datapath_group
+                = chassis_features->ls_dpg_column
+                    ? sbrec_lb->ls_datapath_group
+                    : sbrec_lb->datapath_group; /* deprecated */
+            lb_dpg = ovn_dp_group_get_or_create(
+                    ovnsb_txn, &ls_dp_groups,
+                    ls_datapath_group,
+                    lb_dps->n_nb_ls, lb_dps->nb_ls_map,
+                    ods_size(ls_datapaths), true,
+                    ls_datapaths, NULL);
+        }
+        if (!lb_lr_dpg && lb_dps->n_nb_lr) {
+            lb_lr_dpg = ovn_dp_group_get_or_create(
+                    ovnsb_txn, &lr_dp_groups,
+                    sbrec_lb->lr_datapath_group,
+                    lb_dps->n_nb_lr, lb_dps->nb_lr_map,
+                    ods_size(lr_datapaths), false,
+                    NULL, lr_datapaths);
         }
 
         /* Update columns. */
@@ -4628,7 +4671,23 @@ sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
         sbrec_load_balancer_set_vips(sbrec_lb,
                                      ovn_northd_lb_get_vips(lb_dps->lb));
         sbrec_load_balancer_set_protocol(sbrec_lb, lb_dps->lb->nlb->protocol);
-        sbrec_load_balancer_set_datapath_group(sbrec_lb, lb_dpg->dp_group);
+
+        if (chassis_features->ls_dpg_column) {
+            sbrec_load_balancer_set_ls_datapath_group(
+                sbrec_lb, lb_dpg ? lb_dpg->dp_group : NULL
+            );
+            sbrec_load_balancer_set_datapath_group(sbrec_lb, NULL);
+        } else {
+            /* datapath_group column is deprecated. */
+            sbrec_load_balancer_set_ls_datapath_group(sbrec_lb, NULL);
+            sbrec_load_balancer_set_datapath_group(
+                sbrec_lb, lb_dpg ? lb_dpg->dp_group : NULL
+            );
+        }
+
+        sbrec_load_balancer_set_lr_datapath_group(
+            sbrec_lb, lb_lr_dpg ? lb_lr_dpg->dp_group : NULL
+        );
         sbrec_load_balancer_set_options(sbrec_lb, &options);
         /* Clearing 'datapaths' column, since 'dp_group' is in use. */
         sbrec_load_balancer_set_datapaths(sbrec_lb, NULL, 0);
@@ -4636,11 +4695,17 @@ sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
     }
 
     struct ovn_dp_group *dpg;
-    HMAP_FOR_EACH_POP (dpg, node, &dp_groups) {
+    HMAP_FOR_EACH_POP (dpg, node, &ls_dp_groups) {
+        bitmap_free(dpg->bitmap);
+        free(dpg);
+    }
+    hmap_destroy(&ls_dp_groups);
+
+    HMAP_FOR_EACH_POP (dpg, node, &lr_dp_groups) {
         bitmap_free(dpg->bitmap);
         free(dpg);
     }
-    hmap_destroy(&dp_groups);
+    hmap_destroy(&lr_dp_groups);
 
     struct sb_lb *sb_lb;
     HMAP_FOR_EACH_POP (sb_lb, hmap_node, &sb_lbs) {
@@ -4659,6 +4724,33 @@ sync_lbs(struct ovsdb_idl_txn *ovnsb_txn,
             sbrec_datapath_binding_set_load_balancers(od->sb, NULL, 0);
         }
     }
+    HMAP_FOR_EACH (od, key_node, &lr_datapaths->datapaths) {
+        ovs_assert(od->nbr);
+
+        if (od->sb->n_load_balancers) {
+            sbrec_datapath_binding_set_load_balancers(od->sb, NULL, 0);
+        }
+    }
+}
+
+bool
+check_sb_lb_duplicates(const struct sbrec_load_balancer_table *table)
+{
+    struct sset existing_nb_lb_uuids =
+        SSET_INITIALIZER(&existing_nb_lb_uuids);
+    const struct sbrec_load_balancer *sbrec_lb;
+    bool duplicates = false;
+
+    SBREC_LOAD_BALANCER_TABLE_FOR_EACH (sbrec_lb, table) {
+        const char *nb_lb_uuid = smap_get(&sbrec_lb->external_ids, "lb_id");
+        if (nb_lb_uuid && !sset_add(&existing_nb_lb_uuids, nb_lb_uuid)) {
+            duplicates = true;
+            break;
+        }
+    }
+
+    sset_destroy(&existing_nb_lb_uuids);
+    return duplicates;
 }
 
 /* Syncs the SB port binding for the ovn_port 'op'.  Caller should make sure
@@ -5089,23 +5181,21 @@ lsp_can_be_inc_processed(const struct nbrec_logical_switch_port *nbsp)
 }
 
 static bool
-ls_port_has_changed(const struct nbrec_logical_switch_port *old,
-                    const struct nbrec_logical_switch_port *new)
+ls_port_has_changed(const struct nbrec_logical_switch_port *new)
 {
-    if (old != new) {
-        return true;
-    }
     /* XXX: Need a better OVSDB IDL interface for this check. */
     return (nbrec_logical_switch_port_row_get_seqno(new,
                                 OVSDB_IDL_CHANGE_MODIFY) > 0);
 }
 
 static struct ovn_port *
-ovn_port_find_in_datapath(struct ovn_datapath *od, const char *name)
+ovn_port_find_in_datapath(struct ovn_datapath *od,
+                          const struct nbrec_logical_switch_port *nbsp)
 {
     struct ovn_port *op;
-    HMAP_FOR_EACH_WITH_HASH (op, dp_node, hash_string(name, 0), &od->ports) {
-        if (!strcmp(op->key, name)) {
+    HMAP_FOR_EACH_WITH_HASH (op, dp_node, hash_string(nbsp->name, 0),
+                             &od->ports) {
+        if (!strcmp(op->key, nbsp->name) && op->nbsp == nbsp) {
             return op;
         }
     }
@@ -5290,7 +5380,7 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
     /* Compare the individual ports in the old and new Logical Switches */
     for (size_t j = 0; j < changed_ls->n_ports; ++j) {
         struct nbrec_logical_switch_port *new_nbsp = changed_ls->ports[j];
-        op = ovn_port_find_in_datapath(od, new_nbsp->name);
+        op = ovn_port_find_in_datapath(od, new_nbsp);
 
         if (!op) {
             if (!lsp_can_be_inc_processed(new_nbsp)) {
@@ -5307,7 +5397,7 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
             }
             ovs_list_push_back(&ls_change->added_ports,
                                 &op->list);
-        } else if (ls_port_has_changed(op->nbsp, new_nbsp)) {
+        } else if (ls_port_has_changed(new_nbsp)) {
             /* Existing port updated */
             bool temp = false;
             if (lsp_is_type_changed(op->sb, new_nbsp, &temp) ||
@@ -5580,9 +5670,9 @@ northd_handle_sb_port_binding_changes(
              * notification of that transaction, and we can ignore in this
              * case. Fallback to recompute otherwise, to avoid dangling
              * sb idl pointers and other unexpected behavior. */
-            if (op) {
-                VLOG_WARN_RL(&rl, "A port-binding for %s is deleted but the "
-                            "LSP still exists.", pb->logical_port);
+            if (op && op->sb == pb) {
+                VLOG_WARN_RL(&rl, "A port-binding for %s is deleted but "
+                            "the LSP still exists.", pb->logical_port);
                 return false;
             }
         } else {
@@ -6954,6 +7044,9 @@ build_lswitch_learn_fdb_op(
         ds_clear(match);
         ds_clear(actions);
         ds_put_format(match, "inport == %s", op->json_key);
+        if (lsp_is_localnet(op->nbsp)) {
+            ds_put_cstr(actions, "flags.localnet = 1; ");
+        }
         ds_put_format(actions, REGBIT_LKUP_FDB
                       " = lookup_fdb(inport, eth.src); next;");
         ovn_lflow_add_with_lport_and_hint(lflows, op->od,
@@ -9307,6 +9400,37 @@ build_lswitch_rport_arp_req_flows(struct ovn_port *op,
         }
     }
 
+    struct shash_node *snat_snode;
+    SHASH_FOR_EACH (snat_snode, &op->od->snat_ips) {
+        struct ovn_snat_ip *snat_ip = snat_snode->data;
+
+        if (ovs_list_is_empty(&snat_ip->snat_entries)) {
+            continue;
+        }
+
+        struct ovn_nat *nat_entry =
+            CONTAINER_OF(ovs_list_front(&snat_ip->snat_entries),
+                         struct ovn_nat, ext_addr_list_node);
+        const struct nbrec_nat *nat = nat_entry->nb;
+
+        /* Check if the ovn port has a network configured on which we could
+         * expect ARP requests/NS for the SNAT external_ip.
+         */
+        if (nat_entry_is_v6(nat_entry)) {
+            if (!sset_contains(&op->od->lb_ips->ips_v6, nat->external_ip)) {
+                build_lswitch_rport_arp_req_flow(
+                    nat->external_ip, AF_INET6, sw_op, sw_od, 80, lflows,
+                    stage_hint);
+            }
+        } else {
+            if (!sset_contains(&op->od->lb_ips->ips_v4, nat->external_ip)) {
+                build_lswitch_rport_arp_req_flow(
+                    nat->external_ip, AF_INET, sw_op, sw_od, 80, lflows,
+                    stage_hint);
+            }
+        }
+    }
+
     for (size_t i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
         build_lswitch_rport_arp_req_flow(
             op->lrp_networks.ipv4_addrs[i].addr_s, AF_INET, sw_op, sw_od, 80,
@@ -13047,9 +13171,9 @@ build_neigh_learning_flows_for_lrouter(
          *   address, the all-nodes multicast address. */
         ds_clear(actions);
         ds_put_format(actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
-                               " = lookup_nd(inport, ip6.src, nd.tll); "
+                               " = lookup_nd(inport, nd.target, nd.tll); "
                                REGBIT_LOOKUP_NEIGHBOR_IP_RESULT
-                               " = lookup_nd_ip(inport, ip6.src); next;");
+                               " = lookup_nd_ip(inport, nd.target); next;");
         ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_NEIGHBOR, 110,
                       "nd_na && ip6.src == fe80::/10 && ip6.dst == ff00::/8",
                       ds_cstr(actions));
@@ -13895,7 +14019,15 @@ build_icmperr_pkt_big_flows(struct ovn_port *op, int mtu, struct hmap *lflows,
                                               outport->json_key)
                                   : NULL;
 
-    if (op->lrp_networks.ipv4_addrs) {
+    char *ip4_src = NULL;
+
+    if (outport && outport->lrp_networks.ipv4_addrs) {
+        ip4_src = outport->lrp_networks.ipv4_addrs[0].addr_s;
+    } else if (op->lrp_networks.ipv4_addrs) {
+        ip4_src = op->lrp_networks.ipv4_addrs[0].addr_s;
+    }
+
+    if (ip4_src) {
         ds_clear(match);
         ds_put_format(match, "inport == %s && %sip4 && "REGBIT_PKT_LARGER
                       " && "REGBIT_EGRESS_LOOPBACK" == 0", op->json_key,
@@ -13915,9 +14047,8 @@ build_icmperr_pkt_big_flows(struct ovn_port *op, int mtu, struct hmap *lflows,
             "icmp4.code = 4; /* Frag Needed and DF was Set. */ "
             "icmp4.frag_mtu = %d; "
             "next(pipeline=ingress, table=%d); };",
-            op->lrp_networks.ea_s,
-            op->lrp_networks.ipv4_addrs[0].addr_s,
-            mtu, ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
+            op->lrp_networks.ea_s, ip4_src, mtu,
+            ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
         ovn_lflow_add_with_hint__(lflows, op->od, stage, 150,
                                   ds_cstr(match), ds_cstr(actions),
                                   NULL,
@@ -13928,7 +14059,15 @@ build_icmperr_pkt_big_flows(struct ovn_port *op, int mtu, struct hmap *lflows,
                                   &op->nbrp->header_);
     }
 
-    if (op->lrp_networks.ipv6_addrs) {
+    char *ip6_src = NULL;
+
+    if (outport && outport->lrp_networks.ipv6_addrs) {
+        ip6_src = outport->lrp_networks.ipv6_addrs[0].addr_s;
+    } else if (op->lrp_networks.ipv6_addrs) {
+        ip6_src = op->lrp_networks.ipv6_addrs[0].addr_s;
+    }
+
+    if (ip6_src) {
         ds_clear(match);
         ds_put_format(match, "inport == %s && %sip6 && "REGBIT_PKT_LARGER
                       " && "REGBIT_EGRESS_LOOPBACK" == 0", op->json_key,
@@ -13948,9 +14087,8 @@ build_icmperr_pkt_big_flows(struct ovn_port *op, int mtu, struct hmap *lflows,
             "icmp6.code = 0; "
             "icmp6.frag_mtu = %d; "
             "next(pipeline=ingress, table=%d); };",
-            op->lrp_networks.ea_s,
-            op->lrp_networks.ipv6_addrs[0].addr_s,
-            mtu, ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
+            op->lrp_networks.ea_s, ip6_src, mtu,
+            ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
         ovn_lflow_add_with_hint__(lflows, op->od, stage, 150,
                                   ds_cstr(match), ds_cstr(actions),
                                   NULL,
@@ -15217,6 +15355,7 @@ build_lrouter_out_snat_in_czone_flow(struct hmap *lflows,
         ds_put_format(&zone_actions, "eth.src = "ETH_ADDR_FMT"; ",
                       ETH_ADDR_ARGS(mac));
     }
+    ds_put_format(match, " && (!ct.trk || !ct.rpl)");
 
     ds_put_cstr(&zone_actions, REGBIT_DST_NAT_IP_LOCAL" = 0; ");
 
@@ -15275,10 +15414,8 @@ build_lrouter_out_snat_flow(struct hmap *lflows, struct ovn_datapath *od,
             ds_put_format(actions, "eth.src = "ETH_ADDR_FMT"; ",
                           ETH_ADDR_ARGS(mac));
         }
-    } else {
-        /* Gateway router. */
-        ds_put_cstr(match, " && (!ct.trk || !ct.rpl)");
     }
+    ds_put_cstr(match, " && (!ct.trk || !ct.rpl)");
 
     ds_put_format(actions, "ct_snat(%s", nat->external_ip);
     if (nat->external_port_range[0]) {
@@ -17655,6 +17792,7 @@ northd_init(struct northd_data *data)
         .mac_binding_timestamp = true,
         .ct_lb_related = true,
         .fdb_timestamp = true,
+        .ls_dpg_column = true,
     };
     data->ovn_internal_version_changed = false;
     sset_init(&data->svc_monitor_lsps);
@@ -18051,13 +18189,15 @@ static void
 handle_cr_port_binding_changes(const struct sbrec_port_binding *sb,
                 struct ovn_port *orp)
 {
+    const struct nbrec_logical_router_port *nbrec_lrp = orp->l3dgw_port->nbrp;
+
     if (sb->chassis) {
-        nbrec_logical_router_port_update_status_setkey(
-            orp->l3dgw_port->nbrp, "hosting-chassis",
-            sb->chassis->name);
-    } else {
-        nbrec_logical_router_port_update_status_delkey(
-            orp->l3dgw_port->nbrp, "hosting-chassis");
+        nbrec_logical_router_port_update_status_setkey(nbrec_lrp,
+                                                       "hosting-chassis",
+                                                       sb->chassis->name);
+    } else if (smap_get(&nbrec_lrp->status, "hosting-chassis")) {
+        nbrec_logical_router_port_update_status_delkey(nbrec_lrp,
+                                                       "hosting-chassis");
     }
 }
 
diff --git a/northd/northd.h b/northd/northd.h
index 4d030b10d..76dfc392d 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -70,6 +70,7 @@ struct chassis_features {
     bool mac_binding_timestamp;
     bool ct_lb_related;
     bool fdb_timestamp;
+    bool ls_dpg_column;
 };
 
 /* A collection of datapaths. E.g. all logical switch datapaths, or all
@@ -365,7 +366,11 @@ const char *northd_get_svc_monitor_mac(void);
 const struct ovn_datapath *northd_get_datapath_for_port(
     const struct hmap *ls_ports, const char *port_name);
 void sync_lbs(struct ovsdb_idl_txn *, const struct sbrec_load_balancer_table *,
-              struct ovn_datapaths *ls_datapaths, struct hmap *lbs);
+              struct ovn_datapaths *ls_datapaths,
+              struct ovn_datapaths *lr_datapaths,
+              struct hmap *lbs,
+              struct chassis_features *chassis_features);
+bool check_sb_lb_duplicates(const struct sbrec_load_balancer_table *);
 
 void sync_pbs(struct ovsdb_idl_txn *, struct hmap *ls_ports);
 bool sync_pbs_for_northd_ls_changes(struct tracked_ls_changes *);
diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index df8ed6750..97718821f 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -399,14 +399,16 @@
     <p>
       This table looks up the MAC learning table of the logical switch
       datapath to check if the <code>port-mac</code> pair is present
-      or not. MAC is learnt only for logical switch VIF ports whose
-      port security is disabled and 'unknown' address set.
+      or not. MAC is learnt for logical switch VIF ports whose
+      port security is disabled and 'unknown' address setn as well as
+      for localnet ports with option localnet_learn_fdb. A localnet
+      port entry does not overwrite a VIF port entry.
     </p>
 
     <ul>
       <li>
         <p>
-          For each such logical port <var>p</var> whose port security
+          For each such VIF logical port <var>p</var> whose port security
           is disabled and 'unknown' address set following flow
           is added.
         </p>
@@ -420,6 +422,22 @@
         </ul>
       </li>
 
+      <li>
+        <p>
+          For each such localnet logical port <var>p</var> following flow
+          is added.
+        </p>
+
+        <ul>
+          <li>
+            Priority 100 flow with the match
+            <code>inport == <var>p</var></code> and action
+            <code>flags.localnet = 1;</code>
+            <code>reg0[11] = lookup_fdb(inport, eth.src); next;</code>
+          </li>
+        </ul>
+      </li>
+
       <li>
         One priority-0 fallback flow that matches all packets and advances to
         the next table.
@@ -429,17 +447,20 @@
     <h3>Ingress Table 3: Learn MAC of 'unknown' ports.</h3>
 
     <p>
-      This table learns the MAC addresses seen on the logical ports
-      whose port security is disabled and 'unknown' address set
+      This table learns the MAC addresses seen on the VIF logical ports
+      whose port security is disabled and 'unknown' address set as well
+      as on localnet ports with localnet_learn_fdb option set
       if the <code>lookup_fdb</code> action returned false in the
-      previous table.
+      previous table. For localnet ports (with flags.localnet = 1),
+      lookup_fdb returns true if (port, mac) is found or if a mac
+      is found for a port of type vif.
     </p>
 
     <ul>
       <li>
         <p>
-          For each such logical port <var>p</var> whose port security
-          is disabled and 'unknown' address set following flow
+          For each such VIF logical port <var>p</var> whose port security is
+          disabled and 'unknown' address set and localnet port following flow
           is added.
         </p>
 
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 68fc8836e..b2c5552f6 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -47,7 +47,6 @@
 
 VLOG_DEFINE_THIS_MODULE(ovn_northd);
 
-static unixctl_cb_func ovn_northd_exit;
 static unixctl_cb_func ovn_northd_pause;
 static unixctl_cb_func ovn_northd_resume;
 static unixctl_cb_func ovn_northd_is_paused;
@@ -752,7 +751,7 @@ main(int argc, char *argv[])
     int res = EXIT_SUCCESS;
     struct unixctl_server *unixctl;
     int retval;
-    bool exiting;
+    struct ovn_exit_args exit_args = {};
     int n_threads = 1;
     struct northd_state state = {
         .had_lock = false,
@@ -774,7 +773,8 @@ main(int argc, char *argv[])
     if (retval) {
         exit(EXIT_FAILURE);
     }
-    unixctl_command_register("exit", "", 0, 0, ovn_northd_exit, &exiting);
+    unixctl_command_register("exit", "", 0, 0, ovn_exit_command_callback,
+                             &exit_args);
     unixctl_command_register("pause", "", 0, 0, ovn_northd_pause, &state);
     unixctl_command_register("resume", "", 0, 0, ovn_northd_resume, &state);
     unixctl_command_register("is-paused", "", 0, 0, ovn_northd_is_paused,
@@ -868,6 +868,8 @@ main(int argc, char *argv[])
     stopwatch_create(LFLOWS_IGMP_STOPWATCH_NAME, SW_MS);
     stopwatch_create(LFLOWS_DP_GROUPS_STOPWATCH_NAME, SW_MS);
     stopwatch_create(LFLOWS_TO_SB_STOPWATCH_NAME, SW_MS);
+    stopwatch_create(PORT_GROUP_RUN_STOPWATCH_NAME, SW_MS);
+    stopwatch_create(SYNC_METERS_RUN_STOPWATCH_NAME, SW_MS);
 
     /* Initialize incremental processing engine for ovn-northd */
     inc_proc_northd_init(&ovnnb_idl_loop, &ovnsb_idl_loop);
@@ -878,11 +880,9 @@ main(int argc, char *argv[])
     run_update_worker_pool(n_threads);
 
     /* Main loop. */
-    exiting = false;
-
     struct northd_engine_context eng_ctx = {};
 
-    while (!exiting) {
+    while (!exit_args.exiting) {
         update_ssl_config();
         memory_run();
         if (memory_should_report()) {
@@ -1024,7 +1024,7 @@ main(int argc, char *argv[])
         unixctl_server_run(unixctl);
         unixctl_server_wait(unixctl);
         memory_wait();
-        if (exiting) {
+        if (exit_args.exiting) {
             poll_immediate_wake();
         }
 
@@ -1057,15 +1057,16 @@ main(int argc, char *argv[])
         stopwatch_stop(NORTHD_LOOP_STOPWATCH_NAME, time_msec());
         poll_block();
         if (should_service_stop()) {
-            exiting = true;
+            exit_args.exiting = true;
         }
         stopwatch_start(NORTHD_LOOP_STOPWATCH_NAME, time_msec());
     }
     inc_proc_northd_cleanup();
 
-    unixctl_server_destroy(unixctl);
     ovsdb_idl_loop_destroy(&ovnnb_idl_loop);
     ovsdb_idl_loop_destroy(&ovnsb_idl_loop);
+    ovn_exit_args_finish(&exit_args);
+    unixctl_server_destroy(unixctl);
     service_stop();
     run_update_worker_pool(0);
     ovsrcu_exit();
@@ -1073,16 +1074,6 @@ main(int argc, char *argv[])
     exit(res);
 }
 
-static void
-ovn_northd_exit(struct unixctl_conn *conn, int argc OVS_UNUSED,
-                const char *argv[] OVS_UNUSED, void *exiting_)
-{
-    bool *exiting = exiting_;
-    *exiting = true;
-
-    unixctl_command_reply(conn, NULL);
-}
-
 static void
 ovn_northd_pause(struct unixctl_conn *conn, int argc OVS_UNUSED,
                 const char *argv[] OVS_UNUSED, void *state_)
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 9131305ea..f84ad90c4 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -1101,7 +1101,7 @@
 
         <column name="options" key="ethtype">
           Optional. VLAN EtherType field value for encapsulating VLAN
-          headers. Supported values: 802.11q (default), 802.11ad.
+          headers. Supported values: 802.1q (default), 802.1ad.
         </column>
 
         <column name="options" key="localnet_learn_fdb"
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index 7b20aa21b..a512e00e5 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@
 {
     "name": "OVN_Southbound",
-    "version": "20.27.4",
-    "cksum": "3194181501 30531",
+    "version": "20.29.0",
+    "cksum": "3967183656 30959",
     "tables": {
         "SB_Global": {
             "columns": {
@@ -534,6 +534,14 @@
                     {"type": {"key": {"type": "uuid",
                                       "refTable": "Logical_DP_Group"},
                               "min": 0, "max": 1}},
+                "ls_datapath_group":
+                    {"type": {"key": {"type": "uuid",
+                                      "refTable": "Logical_DP_Group"},
+                              "min": 0, "max": 1}},
+                "lr_datapath_group":
+                    {"type": {"key": {"type": "uuid",
+                                      "refTable": "Logical_DP_Group"},
+                              "min": 0, "max": 1}},
                 "options": {
                      "type": {"key": "string",
                               "value": "string",
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 46aedf973..519b9da8f 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -1721,9 +1721,12 @@
 
           <p>
             Looks up <var>A</var> in fdb table. If an entry is found
-            and the the logical port key is <var>P</var>, <code>P</code>,
+            and the logical port key is <var>P</var>, <code>P</code>,
             stores <code>1</code> in the 1-bit subfield
-            <var>R</var>, else 0.
+            <var>R</var>, else 0. If <code>flags.localnet</code> is set
+            then <code>1</code> is stored if an entry is found and the
+            logical port key is <var>P</var> or if an entry is found and
+            the entry port type is VIF.
           </p>
 
           <p>
@@ -4888,10 +4891,22 @@ tcp.flags = RST;
     </column>
 
     <column name="datapath_group">
+      Deprecated. The group of datapaths to which this load balancer applies
+      to. This means that the same load balancer applies to all datapaths in
+      a group.
+    </column>
+
+    <column name="ls_datapath_group">
       The group of datapaths to which this load balancer applies to.  This
       means that the same load balancer applies to all datapaths in a group.
     </column>
 
+    <column name="lr_datapath_group">
+      The group of logical router datapaths to which this load balancer
+      applies to. This means that the same load balancer applies to all
+      datapaths in a group.
+    </column>
+
     <group title="Load_Balancer options">
     <column name="options" key="hairpin_snat_ip">
       IP to be used as source IP for packets that have been hair-pinned after
diff --git a/tests/automake.mk b/tests/automake.mk
index eea0d00f4..f6f0f0e33 100644
--- a/tests/automake.mk
+++ b/tests/automake.mk
@@ -310,7 +310,8 @@ CHECK_PYFILES = \
 	tests/test-l7.py \
 	tests/uuidfilt.py \
 	tests/test-tcp-rst.py \
-	tests/check_acl_log.py
+	tests/check_acl_log.py \
+	tests/scapy-server.py
 
 EXTRA_DIST += $(CHECK_PYFILES)
 PYCOV_CLEAN_FILES += $(CHECK_PYFILES:.py=.py,cover) .coverage
diff --git a/tests/checkpatch.at b/tests/checkpatch.at
index 26f319705..e7322fff4 100755
--- a/tests/checkpatch.at
+++ b/tests/checkpatch.at
@@ -8,7 +8,14 @@ OVS_START_SHELL_HELPERS
 try_checkpatch() {
     # Take the patch to test from $1.  Remove an initial four-space indent
     # from it and, if it is just headers with no body, add a null body.
+    # If it does not have a 'Subject', add a valid one.
     echo "$1" | sed 's/^    //' > test.patch
+    if grep 'Subject\:' test.patch >/dev/null 2>&1; then :
+    else
+        sed -i'' -e '1i\
+Subject: Patch this is.
+' test.patch
+    fi
     if grep '---' expout >/dev/null 2>&1; then :
     else
         printf '\n---\n' >> test.patch
@@ -236,6 +243,22 @@ done
 AT_CLEANUP
 
 
+AT_SETUP([checkpatch - catastrophic backtracking])
+dnl Special case this rather than using the above construct because sometimes a
+dnl warning needs to be generated for line lengths (f.e. when the 'while'
+dnl keyword is used).
+try_checkpatch \
+   "COMMON_PATCH_HEADER
+    +     if (!b_ctx_in->chassis_rec || !b_ctx_in->br_int || !b_ctx_in->ovs_idl_txn)
+    " \
+    "ERROR: Inappropriate bracing around statement
+    #8 FILE: A.c:1:
+         if (!b_ctx_in->chassis_rec || !b_ctx_in->br_int || !b_ctx_in->ovs_idl_txn)
+"
+
+AT_CLEANUP
+
+
 AT_SETUP([checkpatch - parenthesized constructs - for])
 try_checkpatch \
    "COMMON_PATCH_HEADER
@@ -560,3 +583,25 @@ try_checkpatch \
 "
 
 AT_CLEANUP
+
+AT_SETUP([checkpatch - subject])
+try_checkpatch \
+   "Author: A
+    Commit: A
+    Subject: netdev: invalid case and dot ending
+
+    Signed-off-by: A" \
+    "WARNING: The subject summary should start with a capital.
+    WARNING: The subject summary should end with a dot.
+    Subject: netdev: invalid case and dot ending"
+
+try_checkpatch \
+   "Author: A
+    Commit: A
+    Subject: netdev: This is a way to long commit summary and therefor it should report a WARNING!
+
+    Signed-off-by: A" \
+    "WARNING: The subject, '<area>: <summary>', is over 70 characters, i.e., 85.
+    Subject: netdev: This is a way to long commit summary and therefor it should report a WARNING!"
+
+AT_CLEANUP
diff --git a/tests/ovn-controller-vtep.at b/tests/ovn-controller-vtep.at
index 73971b3f4..50f31b22f 100644
--- a/tests/ovn-controller-vtep.at
+++ b/tests/ovn-controller-vtep.at
@@ -653,9 +653,6 @@ AT_CHECK([grep -c $northd_version vtep1/ovn-controller-vtep.log], [0], [1
 as northd
 OVS_APP_EXIT_AND_WAIT([ovn-northd])
 
-as northd-backup
-OVS_APP_EXIT_AND_WAIT([ovn-northd])
-
 check ovn-sbctl set SB_Global . options:northd_internal_version=foo
 check ovn-sbctl set Chassis vtep1 vtep_logical_switches=foo
 
diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
index 4212d601a..6f496d142 100644
--- a/tests/ovn-controller.at
+++ b/tests/ovn-controller.at
@@ -60,7 +60,6 @@ check_bridge_mappings () {
 # the RBAC rules programmed into the SB-DB. The test instruments the SB-DB
 # directly and we need to stop northd to avoid overwriting the instrumentation.
 kill `cat northd/ovn-northd.pid`
-kill `cat northd-backup/ovn-northd.pid`
 kill `cat ovn-nb/ovsdb-server.pid`
 
 # Initially there should be no patch ports.
@@ -526,6 +525,10 @@ OVS_WAIT_UNTIL([
     test "$(ovn-sbctl get chassis hv1 other_config:port-up-notif)" = '"true"'
 ])
 
+OVS_WAIT_UNTIL([
+    test "$(ovn-sbctl get chassis hv1 other_config:ls-dpg-column)" = '"true"'
+])
+
 OVN_CLEANUP([hv1])
 AT_CLEANUP
 ])
@@ -575,10 +578,8 @@ localport lport : [[lsp1]]
 
 # pause ovn-northd
 check as northd ovn-appctl -t ovn-northd pause
-check as northd-backup ovn-appctl -t ovn-northd pause
 
 as northd ovn-appctl -t ovn-northd status
-as northd-backup ovn-appctl -t ovn-northd status
 
 pb_types=(patch chassisredirect l3gateway localnet localport l2gateway
           virtual external remote vtep)
@@ -2093,7 +2094,6 @@ AT_CHECK([ovs-ofctl dump-flows br-int table=46 | grep -c "priority=1100"], [0],
 
 # pause ovn-northd
 check as northd ovn-appctl -t ovn-northd pause
-check as northd-backup ovn-appctl -t ovn-northd pause
 
 # Simulate a SB address set "del and add" notification to ovn-controller in the
 # same IDL iteration. The flows programmed by ovn-controller should reflect the
@@ -2118,7 +2118,7 @@ AT_CLEANUP
 
 AT_SETUP([ovn-controller - I-P handle lb_hairpin_use_ct_mark change])
 
-ovn_start --backup-northd=none
+ovn_start
 
 net_add n1
 sim_add hv1
@@ -2202,6 +2202,10 @@ OVS_APP_EXIT_AND_WAIT([ovn-controller])
 # The old OVS flows should remain (this is regardless of the configuration)
 AT_CHECK([ovs-ofctl dump-flows br-int | grep 10.1.2.3], [0], [ignore])
 
+# We should have 2 flows with groups.
+AT_CHECK([ovs-ofctl dump-flows br-int | grep group -c], [0], [2
+])
+
 # Make a change to the ls1-lp1's IP
 check ovn-nbctl --wait=sb lsp-set-addresses ls1-lp1 "f0:00:00:00:00:01 10.1.2.4"
 
@@ -2214,10 +2218,18 @@ sleep 2
 lflow_run_1=$(ovn-appctl -t ovn-controller coverage/read-counter lflow_run)
 AT_CHECK([ovs-ofctl dump-flows br-int | grep 10.1.2.3], [0], [ignore])
 
+# We should have 2 flows with groups.
+AT_CHECK([ovs-ofctl dump-flows br-int | grep group -c], [0], [2
+])
+
 sleep 5
 
 # Check after the wait
 OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 10.1.2.4])
+
+# We should have 2 flows with groups.
+AT_CHECK([ovs-ofctl dump-flows br-int | grep group -c], [0], [2
+])
 lflow_run_2=$(ovn-appctl -t ovn-controller coverage/read-counter lflow_run)
 
 # Verify that the flow compute completed during the wait (after the wait it
@@ -2230,7 +2242,7 @@ OVS_APP_EXIT_AND_WAIT([ovs-vswitchd])
 start_daemon ovs-vswitchd --enable-dummy=system -vvconn -vofproto_dpif -vunixctl
 OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 10.1.2.4])
 
-check ovn-nbctl --wait=hv lb-add lb3 2.2.2.2 10.1.2.5 \
+check ovn-nbctl --wait=hv lb-add lb3 3.3.3.3 10.1.2.5 \
 -- ls-lb-add ls1 lb3
 
 # There should be 3 group IDs allocated (this is to ensure the group ID
@@ -2238,6 +2250,10 @@ check ovn-nbctl --wait=hv lb-add lb3 2.2.2.2 10.1.2.5 \
 AT_CHECK([ovn-appctl -t ovn-controller group-table-list | awk '{print $2}' | sort | uniq | wc -l], [0], [3
 ])
 
+# We should have 3 flows with groups.
+AT_CHECK([ovs-ofctl dump-flows br-int | grep group -c], [0], [3
+])
+
 OVN_CLEANUP([hv1])
 AT_CLEANUP
 
diff --git a/tests/ovn-ic.at b/tests/ovn-ic.at
index d265bb90b..d4c436f84 100644
--- a/tests/ovn-ic.at
+++ b/tests/ovn-ic.at
@@ -92,6 +92,7 @@ check ovn-nbctl lr-add lr1
 check ovn-nbctl lrp-add lr1 lrp1 00:00:00:00:00:01 10.0.0.1/24
 check ovn-nbctl lrp-set-gateway-chassis lrp1 gw-az1
 
+OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep ts1])
 check ovn-nbctl lsp-add ts1 lsp1 -- \
     lsp-set-addresses lsp1 router -- \
     lsp-set-type lsp1 router -- \
@@ -156,6 +157,7 @@ create_ic_infra() {
     ovn_as $az
 
     check ovn-ic-nbctl ts-add $ts
+    OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep $ts])
     check ovn-nbctl lr-add $lr
     check ovn-nbctl lrp-add $lr $lrp 00:00:00:00:00:0$az_id 10.0.$az_id.1/24
     check ovn-nbctl lrp-set-gateway-chassis $lrp gw-$az
@@ -227,6 +229,7 @@ for i in 1 2; do
     check ovn-nbctl lrp-add lr1 lrp$i 00:00:00:00:0$i:01 10.0.$i.1/24
     check ovn-nbctl lrp-set-gateway-chassis lrp$i gw-az$i
 
+    OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep ts1])
     check ovn-nbctl lsp-add ts1 lsp$i -- \
         lsp-set-addresses lsp$i router -- \
         lsp-set-type lsp$i router -- \
@@ -290,7 +293,7 @@ ovs-vsctl set open . external-ids:ovn-is-interconn=true
 OVS_WAIT_UNTIL([ovn_as az2 ovn-sbctl show | grep gw1])
 
 OVN_CLEANUP_SBOX(gw1)
-AT_CHECK([ovn_as az2 ovn-sbctl show], [0], [dnl
+OVS_WAIT_FOR_OUTPUT([ovn_as az2 ovn-sbctl show], [0], [dnl
 ])
 
 # Test encap change
@@ -335,16 +338,17 @@ ovn-nbctl lsp-add ts1 lsp-ts1-lr1
 ovn-nbctl lsp-set-addresses lsp-ts1-lr1 router
 ovn-nbctl lsp-set-type lsp-ts1-lr1 router
 ovn-nbctl --wait=hv lsp-set-options lsp-ts1-lr1 router-port=lrp-lr1-ts1
-
+OVS_WAIT_UNTIL([ovn_as az2 ovn-nbctl show | grep lsp-ts1-lr1])
 ovn_as az2 ovn-nbctl lsp-set-options lsp-ts1-lr1 requested-chassis=gw1
-AT_CHECK([ovn_as az2 ovn-nbctl show | uuidfilt], [0], [dnl
+
+OVS_WAIT_FOR_OUTPUT([ovn_as az2 ovn-nbctl show | uuidfilt], [0], [dnl
 switch <0> (ts1)
     port lsp-ts1-lr1
         type: remote
         addresses: [["aa:aa:aa:aa:aa:01 169.254.100.1/24"]]
 ])
 
-AT_CHECK([ovn_as az2 ovn-sbctl -f csv -d bare --no-headings --columns logical_port,type list port_binding], [0], [dnl
+OVS_WAIT_FOR_OUTPUT([ovn_as az2 ovn-sbctl -f csv -d bare --no-headings --columns logical_port,type list port_binding], [0], [dnl
 lsp-ts1-lr1,remote
 ])
 
@@ -529,6 +533,7 @@ for i in 1 2; do
     for j in 1 2; do
         ts=ts$j$j
         ovn-ic-nbctl --may-exist ts-add $ts
+        OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep $ts])
 
         # Create LRP and connect to TS
         lr=lr$j$i
@@ -552,6 +557,9 @@ ovn_as az1 ovn-nbctl show
 echo az2
 ovn_as az2 ovn-nbctl show
 
+OVS_WAIT_UNTIL([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep learned | grep 192.168])
+OVS_WAIT_UNTIL([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep learned | grep 10.10.10])
+
 # Test routes from lr12 were learned to lr11
 AT_CHECK([ovn_as az1 ovn-nbctl lr-route-list lr11 |
              grep learned | awk '{print $1, $2}' | sort], [0], [dnl
@@ -584,6 +592,7 @@ for i in 1 2; do
     # Create LRP and connect to TS
     ovn-nbctl lr-add lr$i
     ovn-nbctl lrp-add lr$i lrp-lr$i-ts1 aa:aa:aa:aa:aa:0$i 169.254.100.$i/24
+    OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep ts1])
     ovn-nbctl lsp-add ts1 lsp-ts1-lr$i \
             -- lsp-set-addresses lsp-ts1-lr$i router \
             -- lsp-set-type lsp-ts1-lr$i router \
@@ -909,6 +918,7 @@ for i in 1 2; do
     for j in 1 2 3; do
         ts=ts1$j
         ovn-ic-nbctl --may-exist ts-add $ts
+        OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep $ts])
 
         lrp=lrp-$lr-$ts
         lsp=lsp-$ts-$lr
@@ -934,6 +944,7 @@ for i in 1 2; do
     for j in 1 2; do
         ts=ts2$j
         ovn-ic-nbctl --may-exist ts-add $ts
+        OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep $ts])
 
         lrp=lrp-$lr-$ts
         lsp=lsp-$ts-$lr
@@ -957,7 +968,7 @@ ovn_as az2 ovn-nbctl --route-table=rtb3 lr-route-add lr12 10.10.10.0/24 192.168.
 ovn_as az2 ovn-nbctl --wait=sb lrp-add lr22 lrp-lr22 aa:aa:aa:aa:bb:01 "192.168.0.1/24"
 
 # Test direct routes from lr12 were learned to lr11
-AT_CHECK([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 192.168 |
+OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 192.168 |
              grep learned | awk '{print $1, $2, $5}' | sort ], [0], [dnl
 192.168.0.0/24 169.254.101.2 ecmp
 192.168.0.0/24 169.254.102.2 ecmp
@@ -965,24 +976,24 @@ AT_CHECK([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 192.168 |
 ])
 
 # Test static routes from lr12 rtbs rtb1,rtb2,rtb3 were learned to lr11
-AT_CHECK([ovn_as az1 ovn-nbctl --route-table=rtb1 lr-route-list lr11], [0], [dnl
+OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl --route-table=rtb1 lr-route-list lr11], [0], [dnl
 IPv4 Routes
 Route Table rtb1:
             10.10.10.0/24             169.254.101.2 dst-ip (learned)
 ])
-AT_CHECK([ovn_as az1 ovn-nbctl --route-table=rtb2 lr-route-list lr11], [0], [dnl
+OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl --route-table=rtb2 lr-route-list lr11], [0], [dnl
 IPv4 Routes
 Route Table rtb2:
             10.10.10.0/24             169.254.102.2 dst-ip (learned)
 ])
-AT_CHECK([ovn_as az1 ovn-nbctl --route-table=rtb3 lr-route-list lr11], [0], [dnl
+OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl --route-table=rtb3 lr-route-list lr11], [0], [dnl
 IPv4 Routes
 Route Table rtb3:
             10.10.10.0/24             169.254.103.2 dst-ip (learned)
 ])
 
 # Test routes from lr12 didn't leak as learned to lr21
-AT_CHECK([ovn_as az1 ovn-nbctl lr-route-list lr21 | grep 192.168 | sort], [0], [dnl
+OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl lr-route-list lr21 | grep 192.168 | sort], [0], [dnl
            192.168.0.0/24             169.254.101.2 dst-ip (learned) ecmp
            192.168.0.0/24             169.254.102.2 dst-ip (learned) ecmp
 ])
@@ -1035,6 +1046,7 @@ for i in 1 2; do
     for j in 1 2 3; do
         ts=ts1$j
         ovn-ic-nbctl --may-exist ts-add $ts
+        OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep $ts])
 
         lrp=lrp-$lr-$ts
         lsp=lsp-$ts-$lr
@@ -1060,6 +1072,7 @@ for i in 1 2; do
     for j in 1 2; do
         ts=ts2$j
         ovn-ic-nbctl --may-exist ts-add $ts
+        OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep $ts])
 
         lrp=lrp-$lr-$ts
         lsp=lsp-$ts-$lr
@@ -1083,6 +1096,7 @@ ovn_as az2 ovn-nbctl --route-table=rtb3 lr-route-add lr12 2001:db8:aaaa::/64 200
 ovn_as az2 ovn-nbctl --wait=sb lrp-add lr22 lrp-lr22 aa:aa:aa:aa:bb:01 "2001:db8:200::1/64"
 
 # Test direct routes from lr12 were learned to lr11
+OVS_WAIT_UNTIL([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 2001:db8:3::2])
 AT_CHECK([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 2001:db8:200 |
              grep learned | awk '{print $1, $2, $5}' | sort], [0], [dnl
 2001:db8:200::/64 2001:db8:1::2 ecmp
@@ -1091,16 +1105,19 @@ AT_CHECK([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 2001:db8:200 |
 ])
 
 # Test static routes from lr12 rtbs rtb1,rtb2,rtb3 were learned to lr11
+OVS_WAIT_UNTIL([ovn_as az1 ovn-nbctl --route-table=rtb1 lr-route-list lr11 | grep learned])
 AT_CHECK([ovn_as az1 ovn-nbctl --route-table=rtb1 lr-route-list lr11], [0], [dnl
 IPv6 Routes
 Route Table rtb1:
        2001:db8:aaaa::/64             2001:db8:1::2 dst-ip (learned)
 ])
+OVS_WAIT_UNTIL([ovn_as az1 ovn-nbctl --route-table=rtb2 lr-route-list lr11 | grep learned])
 AT_CHECK([ovn_as az1 ovn-nbctl --route-table=rtb2 lr-route-list lr11], [0], [dnl
 IPv6 Routes
 Route Table rtb2:
        2001:db8:aaaa::/64             2001:db8:2::2 dst-ip (learned)
 ])
+OVS_WAIT_UNTIL([ovn_as az1 ovn-nbctl --route-table=rtb3 lr-route-list lr11 | grep learned])
 AT_CHECK([ovn_as az1 ovn-nbctl --route-table=rtb3 lr-route-list lr11], [0], [dnl
 IPv6 Routes
 Route Table rtb3:
@@ -1108,6 +1125,7 @@ Route Table rtb3:
 ])
 
 # Test routes from lr12 didn't leak as learned to lr21
+OVS_WAIT_UNTIL([ovn_as az1 ovn-nbctl lr-route-list lr21 | grep "2001:db8:2::2" | grep learned])
 AT_CHECK([ovn_as az1 ovn-nbctl lr-route-list lr21 | grep 2001 | sort], [0], [dnl
         2001:db8:200::/64             2001:db8:1::2 dst-ip (learned) ecmp
         2001:db8:200::/64             2001:db8:2::2 dst-ip (learned) ecmp
@@ -1127,6 +1145,7 @@ ovn-ic-nbctl ts-add ts1
 for i in 1 2; do
     ovn_start az$i
     ovn_as az$i
+    OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep ts1])
 
     # Enable route learning at AZ level
     ovn-nbctl set nb_global . options:ic-route-learn=true
@@ -1152,13 +1171,13 @@ for i in 1 2; do
     check ovn-nbctl --wait=sb lr-route-add $lr 0.0.0.0/0 192.168.$i.11
 done
 
-AT_CHECK([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep dst-ip | sort], [0], [dnl
+OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep dst-ip | sort] , [0], [dnl
                 0.0.0.0/0              192.168.1.11 dst-ip
               10.0.0.0/24              192.168.1.10 dst-ip
            192.168.2.0/24             169.254.100.2 dst-ip (learned)
 ])
 
-AT_CHECK([ovn_as az2 ovn-nbctl lr-route-list lr12 | grep dst-ip | sort], [0], [dnl
+OVS_WAIT_FOR_OUTPUT([ovn_as az2 ovn-nbctl lr-route-list lr12 | grep dst-ip | sort], [0], [dnl
                 0.0.0.0/0              192.168.2.11 dst-ip
               10.0.0.0/24              192.168.2.10 dst-ip
            192.168.1.0/24             169.254.100.1 dst-ip (learned)
@@ -1191,10 +1210,10 @@ done
 # each LR has one connected subnet except TS port
 
 
-# create lr11, lr21, lr22, ts1 and connect them
-ovn-ic-nbctl ts-add ts1
+# create lr11, lr21, lr22 and connect them
 
 ovn_as az1
+OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep ts1])
 
 lr=lr11
 ovn-nbctl lr-add $lr
@@ -1209,6 +1228,7 @@ ovn-nbctl lsp-add ts1 $lsp \
         -- lsp-set-options $lsp router-port=$lrp
 
 ovn_as az2
+OVS_WAIT_UNTIL([ovn-nbctl show | grep switch | grep ts1])
 for i in 1 2; do
     lr=lr2$i
     ovn-nbctl lr-add $lr
@@ -1230,7 +1250,7 @@ ovn_as az2 ovn-nbctl lrp-add lr21 lrp-lr21 aa:aa:aa:aa:bc:01 "192.168.1.1/24"
 ovn_as az2 ovn-nbctl lrp-add lr22 lrp-lr22 aa:aa:aa:aa:bc:02 "192.168.2.1/24"
 
 # Test direct routes from lr21 and lr22 were learned to lr11
-AT_CHECK([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 192.168 |
+OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 192.168 |
              grep learned | awk '{print $1, $2}' | sort ], [0], [dnl
 192.168.1.0/24 169.254.10.21
 192.168.2.0/24 169.254.10.22
diff --git a/tests/ovn-ipsec.at b/tests/ovn-ipsec.at
index 10ef97878..f8df8d60e 100644
--- a/tests/ovn-ipsec.at
+++ b/tests/ovn-ipsec.at
@@ -48,11 +48,11 @@ ovn-nbctl set nb_global . options:ipsec_encapsulation=true
 
 check ovn-nbctl --wait=hv sync
 
-AT_CHECK([as hv2 ovs-vsctl get Interface ovn-hv1-0 options:remote_ip | tr -d '"\n'], [0], [192.168.0.1])
+OVS_WAIT_UNTIL([test x`as hv2 ovs-vsctl get Interface ovn-hv1-0 options:remote_ip | tr -d '"\n'` = x192.168.0.1])
 AT_CHECK([as hv2 ovs-vsctl get Interface ovn-hv1-0 options:local_ip | tr -d '"\n'], [0], [192.168.0.2])
 AT_CHECK([as hv2 ovs-vsctl get Interface ovn-hv1-0 options:remote_name | tr -d '\n'], [0], [hv1])
 AT_CHECK([as hv2 ovs-vsctl get Interface ovn-hv1-0 options:ipsec_encapsulation | tr -d '\n'], [0], [yes])
-AT_CHECK([as hv1 ovs-vsctl get Interface ovn-hv2-0 options:remote_ip | tr -d '"\n'], [0], [192.168.0.2])
+OVS_WAIT_UNTIL([test x`as hv1 ovs-vsctl get Interface ovn-hv2-0 options:remote_ip | tr -d '"\n'` = x192.168.0.2])
 AT_CHECK([as hv1 ovs-vsctl get Interface ovn-hv2-0 options:local_ip | tr -d '"\n'], [0], [192.168.0.1])
 AT_CHECK([as hv1 ovs-vsctl get Interface ovn-hv2-0 options:remote_name | tr -d '\n'], [0], [hv2])
 AT_CHECK([as hv1 ovs-vsctl get Interface ovn-hv2-0 options:ipsec_encapsulation | tr -d '\n'], [0], [yes])
diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
index 13d5dc3d4..bb93fa5f0 100644
--- a/tests/ovn-macros.at
+++ b/tests/ovn-macros.at
@@ -188,16 +188,16 @@ ovn_start_northd() {
 # ovn-sbctl and ovn-nbctl use them by default, and starts ovn-northd running
 # against them.
 #
-# Normally this starts an active northd and a backup northd.  The following
+# Normally this starts only an active northd and no backup northd.  The following
 # options are accepted to adjust that:
-#   --backup-northd=none    Don't start a backup northd.
+#   --backup-northd         Start a backup northd.
 #   --backup-northd=paused  Start the backup northd in the paused state.
 ovn_start () {
-    local backup_northd=:
+    local backup_northd=false
     local backup_northd_options=
     case $1 in
-        --backup-northd=none) backup_northd=false; shift ;;
-        --backup-northd=paused) backup_northd_options=--paused; shift ;;
+        --backup-northd) backup_northd=true; shift ;;
+        --backup-northd=paused) backup_northd=true; backup_northd_options=--paused; shift ;;
     esac
     local AZ=$1
     local msg_prefix=${AZ:+$AZ: }
@@ -288,11 +288,44 @@ net_attach () {
         || return 1
 }
 
+ovn_wait_for_encaps() {
+    local systemid=$1
+
+    if [[ -f "${OVN_SYSCONFDIR}/system-id-override" ]]; then
+        systemid=$(cat ${OVN_SYSCONFDIR}/system-id-override)
+    fi
+
+    local encap=$(ovs-vsctl get Open_vSwitch . external_ids:ovn-encap-type-$systemid)
+    if [[ -z "$encap" ]]; then
+        encap=$(ovs-vsctl get Open_vSwitch . external_ids:ovn-encap-type)
+    fi
+    encap=$(tr -d '"' <<< $encap)
+
+    local ip=$(ovs-vsctl get Open_vSwitch . external_ids:ovn-encap-ip-$systemid)
+    if [[ -z "$ip" ]]; then
+        ip=$(ovs-vsctl get Open_vSwitch . external_ids:ovn-encap-ip)
+    fi
+    ip=$(tr -d '"' <<< $ip)
+
+    IFS="," read -r -a encap_types <<< "$encap"
+    for e in "${encap_types[[@]]}"; do
+        wait_column "$ip" sb:Encap ip chassis_name="$systemid" type="$e"
+    done
+}
+
 # ovn_az_attach AZ NETWORK BRIDGE IP [MASKLEN] [ENCAP]
 ovn_az_attach() {
-    local az=$1 net=$2 bridge=$3 ip=$4 masklen=${5-24} encap=${6-geneve,vxlan} systemid=${7-$sandbox} cli_args=${@:8}
+    local az=$1 net=$2 bridge=$3 ip=$4 masklen=${5-24} encap=${6-geneve,vxlan}
+    local systemid=${7-$sandbox} systemid_override=$8
     net_attach $net $bridge || return 1
 
+    local expected_encap_id=$systemid
+    local cli_args=""
+    if [[ -n "$systemid_override" ]]; then
+        cli_args="-n $systemid_override"
+        expected_encap_id=$systemid_override
+    fi
+
     mac=`ovs-vsctl get Interface $bridge mac_in_use | sed s/\"//g`
     arp_table="$arp_table $sandbox,$bridge,$ip,$mac"
     if test -z $(echo $ip | sed '/:/d'); then
@@ -332,6 +365,11 @@ ovn_az_attach() {
     fi
 
     start_daemon ovn-controller --enable-dummy-vif-plug ${cli_args} || return 1
+    if test X"$az" = XNONE; then
+        ovn_wait_for_encaps $expected_encap_id
+    else
+        ovn_as $az ovn_wait_for_encaps $expected_encap_id
+    fi
 }
 
 # ovn_attach NETWORK BRIDGE IP [MASKLEN] [ENCAP]
@@ -372,6 +410,7 @@ start_virtual_controller() {
         || return 1
 
     ovn-controller --enable-dummy-vif-plug ${cli_args} -vconsole:off --detach --no-chdir
+    ovn_wait_for_encaps $systemid
 }
 
 # ovn_setenv AZ
@@ -833,15 +872,40 @@ ovn_trace_client() {
 # ovs-appctl netdev-dummy/receive $vif $packet
 #
 fmt_pkt() {
-    echo "from scapy.all import *; \
-          import binascii; \
-          out = binascii.hexlify(raw($1)); \
-          print(out.decode())" | $PYTHON3
+    ctlfile=$ovs_base/scapy.ctl
+    if [[ ! -S $ctlfile ]]; then
+        start_scapy_server
+    fi
+    while [[ ! -S $ctlfile ]]; do sleep 0.1; done
+    ovs-appctl -t $ctlfile payload "$1"
+}
+
+start_scapy_server() {
+    pidfile=$ovs_base/scapy.pid
+    ctlfile=$ovs_base/scapy.ctl
+    logfile=$ovs_base/scapy.log
+    lockfile=$ovs_base/scapy.lock
+
+    flock -n $lockfile "$top_srcdir"/tests/scapy-server.py \
+        --pidfile=$pidfile --unixctl=$ctlfile --log-file=$logfile --detach \
+    && on_exit "test -e \"$pidfile\" && ovs-appctl -t $ctlfile exit"
+}
+
+sleep_northd() {
+  echo Northd going to sleep
+  AT_CHECK([kill -STOP $(cat northd/ovn-northd.pid)])
+  on_exit "kill -CONT $(cat northd/ovn-northd.pid)"
+}
+
+wake_up_northd() {
+  echo Northd waking up
+  AT_CHECK([kill -CONT $(cat northd/ovn-northd.pid)])
 }
 
 sleep_sb() {
   echo SB going to sleep
   AT_CHECK([kill -STOP $(cat ovn-sb/ovsdb-server.pid)])
+  on_exit "kill -CONT $(cat ovn-sb/ovsdb-server.pid)"
 }
 wake_up_sb() {
   echo SB waking up
@@ -865,6 +929,7 @@ sleep_ovs() {
   hv=$1
   echo ovs $hv going to sleep
   AT_CHECK([kill -STOP $(cat $hv/ovs-vswitchd.pid)])
+  on_exit "kill -CONT $(cat $hv/ovs-vswitchd.pid)"
 }
 
 wake_up_ovs() {
@@ -876,12 +941,17 @@ wake_up_ovs() {
 sleep_ovsdb() {
   echo OVSDB $1 going to sleep
   AT_CHECK([kill -STOP $(cat $1/ovsdb-server.pid)])
+  on_exit "kill -CONT $(cat $1/ovsdb-server.pid)"
 }
 wake_up_ovsdb() {
   echo OVSDB $1 waking up
   AT_CHECK([kill -CONT $(cat $1/ovsdb-server.pid)])
 }
 
+trim_zeros() {
+    sed 's/\(00\)\{1,\}$//'
+}
+
 OVS_END_SHELL_HELPERS
 
 m4_define([OVN_POPULATE_ARP], [AT_CHECK(ovn_populate_arp__, [0], [ignore])])
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index fde3a28ee..94641f2f0 100644
--- a/tests/ovn-nbctl.at
+++ b/tests/ovn-nbctl.at
@@ -2695,7 +2695,9 @@ dnl ---------------------------------------------------------------------
 
 AT_SETUP([ovn-nbctl - daemon retry connection])
 OVN_NBCTL_TEST_START daemon
-AT_CHECK([kill `cat ovsdb-server.pid`])
+pid=$(cat ovsdb-server.pid)
+AT_CHECK([kill $pid])
+OVS_WAIT_WHILE([kill -0 $pid 2>/dev/null])
 AT_CHECK([ovsdb-server --detach --no-chdir --pidfile --log-file --remote=punix:$OVS_RUNDIR/ovnnb_db.sock ovn-nb.db], [0], [], [stderr])
 AT_CHECK([ovn-nbctl show], [0], [ignore])
 OVN_NBCTL_TEST_STOP "/terminating with signal 15/d"
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 65d3f4b03..c32122025 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -20,6 +20,14 @@ m4_define([_DUMP_DB_TABLES], [
 # sure nothing is changed by the recompute. It is used for ensuring the
 # correctness of incremental processing.
 m4_define([CHECK_NO_CHANGE_AFTER_RECOMPUTE], [
+    wait_port_up=$1
+    if test X$wait_port_up = X1; then
+        # Wait for hv to have received all previous commands
+        # Make sure ports are up as otherwise it might come as a difference after recompute
+        echo "waiting for ports up"
+        check ovn-nbctl --wait=hv sync
+        wait_for_ports_up
+    fi
     _DUMP_DB_TABLES(before)
     check as northd ovn-appctl -t NORTHD_TYPE inc-engine/recompute
     check ovn-nbctl --wait=sb sync
@@ -813,7 +821,7 @@ AT_CLEANUP
 
 OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([ovn-northd restart])
-ovn_start --backup-northd=none
+ovn_start
 
 # Check that ovn-northd is active, by verifying that it creates and
 # destroys southbound datapaths as one would expect.
@@ -832,7 +840,7 @@ sleep 5
 check_row_count Datapath_Binding 1
 
 # Now resume ovn-northd.  Changes should catch up.
-ovn_start_northd primary
+ovn_start_northd
 wait_row_count Datapath_Binding 2
 
 AT_CLEANUP
@@ -841,7 +849,7 @@ AT_CLEANUP
 OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([northbound database reconnection])
 
-ovn_start --backup-northd=none
+ovn_start
 
 # Check that ovn-northd is active, by verifying that it creates and
 # destroys southbound datapaths as one would expect.
@@ -873,7 +881,7 @@ AT_CLEANUP
 OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([southbound database reconnection])
 
-ovn_start --backup-northd=none
+ovn_start
 
 # Check that ovn-northd is active, by verifying that it creates and
 # destroys southbound datapaths as one would expect.
@@ -1100,7 +1108,7 @@ AT_CAPTURE_FILE([crflows])
 AT_CHECK([grep -e "lr_out_snat" drflows | 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=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $allowed_range), action=(ct_snat(172.16.1.1);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
 ])
 
 AT_CHECK([grep -e "lr_out_snat" crflows | sed 's/table=../table=??/' | sort], [0], [dnl
@@ -1130,7 +1138,7 @@ AT_CAPTURE_FILE([crflows2])
 AT_CHECK([grep -e "lr_out_snat" drflows2 | 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=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat(172.16.1.1);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
   table=??(lr_out_snat        ), priority=163  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $disallowed_range), action=(next;)
 ])
 
@@ -1159,7 +1167,7 @@ AT_CAPTURE_FILE([crflows2])
 AT_CHECK([grep -e "lr_out_snat" drflows3 | 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=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $allowed_range), action=(ct_snat(172.16.1.2);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
 ])
 
 AT_CHECK([grep -e "lr_out_snat" crflows3 | sed 's/table=../table=??/' | sort], [0], [dnl
@@ -1186,7 +1194,7 @@ AT_CAPTURE_FILE([crflows2])
 AT_CHECK([grep -e "lr_out_snat" drflows4 | 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=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat(172.16.1.2);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
   table=??(lr_out_snat        ), priority=163  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $disallowed_range), action=(next;)
 ])
 
@@ -2860,7 +2868,7 @@ lbg0_uuid=$(fetch_column sb:load_balancer _uuid name=lbg0)
 echo
 echo "__file__:__line__: Check that SB lb0 has sw0 in datapaths column."
 
-lb0_dp_group=$(fetch_column sb:load_balancer datapath_group name=lb0)
+lb0_dp_group=$(fetch_column sb:load_balancer ls_datapath_group name=lb0)
 AT_CHECK_UNQUOTED([ovn-sbctl --bare --columns _uuid,datapaths find Logical_DP_Group dnl
                     | grep -A1 $lb0_dp_group | tail -1], [0], [dnl
 $sw0_sb_uuid
@@ -2871,7 +2879,7 @@ check_column "" sb:datapath_binding load_balancers external_ids:name=sw0
 echo
 echo "__file__:__line__: Check that SB lbg0 has sw0 in datapaths column."
 
-lbg0_dp_group=$(fetch_column sb:load_balancer datapath_group name=lbg0)
+lbg0_dp_group=$(fetch_column sb:load_balancer ls_datapath_group name=lbg0)
 AT_CHECK_UNQUOTED([ovn-sbctl --bare --columns _uuid,datapaths find Logical_DP_Group dnl
                     | grep -A1 $lbg0_dp_group | tail -1], [0], [dnl
 $sw0_sb_uuid
@@ -2897,6 +2905,15 @@ sb:load_balancer vips,protocol name=lbg0
 
 check ovn-nbctl lr-add lr0 -- add logical_router lr0 load_balancer_group $lbg
 check ovn-nbctl --wait=sb lr-lb-add lr0 lb0
+check_row_count sb:load_balancer 2
+
+lr0_sb_uuid=$(fetch_column datapath_binding _uuid external_ids:name=lr0)
+lb0_lr_dp_group=$(fetch_column sb:load_balancer lr_datapath_group name=lb0)
+
+AT_CHECK_UNQUOTED([ovn-sbctl --bare --columns _uuid,datapaths find Logical_DP_Group dnl
+                    | grep -A1 $lb0_lr_dp_group | tail -1], [0], [dnl
+$lr0_sb_uuid
+])
 
 echo
 echo "__file__:__line__: Check that SB lb0 has only sw0 in datapaths column."
@@ -2942,7 +2959,13 @@ check_row_count sb:load_balancer 2
 lbg1=$(fetch_column nb:load_balancer _uuid name=lbg1)
 check ovn-nbctl add load_balancer_group $lbg load_balancer $lbg1
 check ovn-nbctl --wait=sb lr-lb-add lr0 lb1
-check_row_count sb:load_balancer 3
+check_row_count sb:load_balancer 4
+
+lb1_lr_dp_group=$(fetch_column sb:load_balancer lr_datapath_group name=lb1)
+AT_CHECK_UNQUOTED([ovn-sbctl --bare --columns _uuid,datapaths find Logical_DP_Group dnl
+                    | grep -A1 $lb1_lr_dp_group | tail -1], [0], [dnl
+$lr0_sb_uuid
+])
 
 echo
 echo "__file__:__line__: Associate lb1 to sw1 and check that lb1 is created in SB DB."
@@ -2959,7 +2982,7 @@ echo "__file__:__line__: Check that SB lbg1 has vips and protocol columns are se
 check_column "20.0.0.30:80=20.0.0.50:8080 udp" sb:load_balancer vips,protocol name=lbg1
 
 lb1_uuid=$(fetch_column sb:load_balancer _uuid name=lb1)
-lb1_dp_group=$(fetch_column sb:load_balancer datapath_group name=lb1)
+lb1_dp_group=$(fetch_column sb:load_balancer ls_datapath_group name=lb1)
 
 echo
 echo "__file__:__line__: Check that SB lb1 has sw1 in datapaths column."
@@ -2970,7 +2993,7 @@ $sw1_sb_uuid
 ])
 
 lbg1_uuid=$(fetch_column sb:load_balancer _uuid name=lbg1)
-lbg1_dp_group=$(fetch_column sb:load_balancer datapath_group name=lbg1)
+lbg1_dp_group=$(fetch_column sb:load_balancer ls_datapath_group name=lbg1)
 
 echo
 echo "__file__:__line__: Check that SB lbg1 has sw0 and sw1 in datapaths column."
@@ -4596,7 +4619,7 @@ AT_SKIP_IF([test "$HAVE_OPENSSL" = no])
 PKIDIR="$(cd $abs_top_builddir/tests && pwd)"
 AT_SKIP_IF([expr "$PKIDIR" : ".*[[ 	'\"
 \\]]"])
-ovn_start --backup-northd=none
+ovn_start
 
 as northd
 OVS_APP_EXIT_AND_WAIT([NORTHD_TYPE])
@@ -5095,6 +5118,7 @@ AT_CHECK([grep "ls_in_l2_lkup" ls1_lflows | sed 's/table=../table=??/' | sort],
   table=??(ls_in_l2_lkup      ), priority=70   , match=(eth.mcast), action=(outport = "_MC_flood"; output;)
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == {00:00:00:00:01:01} && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.200), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.1.1), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && nd_ns && nd.target == fe80::200:ff:fe00:101), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
 ])
@@ -5109,6 +5133,7 @@ AT_CHECK([grep "ls_in_l2_lkup" ls2_lflows | sed 's/table=../table=??/' | sort],
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == {00:00:00:00:02:01} && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.2.1), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 20.0.0.100), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 20.0.0.200), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && nd_ns && nd.target == fe80::200:ff:fe00:201), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
 ])
 
@@ -5129,8 +5154,10 @@ AT_CHECK([grep "ls_in_l2_lkup" ls1_lflows | sed 's/table=../table=??/' | sort],
   table=??(ls_in_l2_lkup      ), priority=70   , match=(eth.mcast), action=(outport = "_MC_flood"; output;)
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == {00:00:00:00:01:01} && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.200), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.1.1), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 30.0.0.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 30.0.0.200), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && nd_ns && nd.target == fe80::200:ff:fe00:101), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
 ])
 
@@ -5144,7 +5171,9 @@ AT_CHECK([grep "ls_in_l2_lkup" ls2_lflows | sed 's/table=../table=??/' | sort],
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == {00:00:00:00:02:01} && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.2.1), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 20.0.0.100), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 20.0.0.200), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 40.0.0.100), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 40.0.0.200), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && nd_ns && nd.target == fe80::200:ff:fe00:201), action=(clone {outport = "ls2-ro2"; output; }; outport = "_MC_flood_l2"; output;)
 ])
 
@@ -5162,9 +5191,11 @@ AT_CHECK([grep "ls_in_l2_lkup" ls1_lflows | sed 's/table=../table=??/' | sort],
   table=??(ls_in_l2_lkup      ), priority=70   , match=(eth.mcast), action=(outport = "_MC_flood"; output;)
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == {00:00:00:00:01:01} && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.200), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.1.1), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.1.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 30.0.0.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 30.0.0.200), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && nd_ns && nd.target == fe80::200:ff:fe00:101), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
 ])
 
@@ -5180,9 +5211,11 @@ AT_CHECK([grep "ls_in_l2_lkup" ls1_lflows | sed 's/table=../table=??/' | sort],
   table=??(ls_in_l2_lkup      ), priority=70   , match=(eth.mcast), action=(outport = "_MC_flood"; output;)
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == {00:00:00:00:01:01} && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.200), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.1.1), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.1.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 30.0.0.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 30.0.0.200), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && nd_ns && nd.target == fe80::200:ff:fe00:101), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
 ])
 
@@ -5204,9 +5237,11 @@ AT_CHECK([grep "ls_in_l2_lkup" ls1_lflows | sed 's/table=../table=??/' | sort],
   table=??(ls_in_l2_lkup      ), priority=70   , match=(eth.mcast), action=(outport = "_MC_flood"; output;)
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == {00:00:00:00:01:01} && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 10.0.0.200), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.1.1), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 192.168.1.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 30.0.0.100), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && arp.op == 1 && arp.tpa == 30.0.0.200), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && nd_ns && nd.target == fe80::200:ff:fe00:101), action=(clone {outport = "ls1-ro1"; output; }; outport = "_MC_flood_l2"; output;)
 ])
 
@@ -5364,12 +5399,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=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat_in_czone(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=154  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat_in_czone(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat_in_czone(172.168.0.20);)
-  table=? (lr_out_snat        ), priority=162  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=162  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=154  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=162  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=162  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.20);)
 ])
 
 # Separate zones for DGP
@@ -5412,9 +5447,9 @@ 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=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
 ])
 
 # Associate load balancer to lr0
@@ -5494,12 +5529,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=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat_in_czone(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=154  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat_in_czone(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat_in_czone(172.168.0.20);)
-  table=? (lr_out_snat        ), priority=162  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=162  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=154  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=162  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=162  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.20);)
 ])
 
 # Separate zones for DGP
@@ -5560,9 +5595,9 @@ 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=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat(172.168.0.10);)
-  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat(172.168.0.30);)
-  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat(172.168.0.20);)
+  table=? (lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
+  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
+  table=? (lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
 ])
 
 # Make the logical router as Gateway router
@@ -6019,9 +6054,9 @@ ovn_start
 check ovn-nbctl ls-add ls -- lb-add lb1 10.0.0.1:80 10.0.0.2:80 -- ls-lb-add ls lb1
 check ovn-nbctl --wait=sb sync
 
-dps=$(fetch_column Load_Balancer datapath_group)
+dps=$(fetch_column Load_Balancer ls_datapath_group)
 nlb=$(fetch_column nb:Load_Balancer _uuid)
-AT_CHECK([ovn-sbctl create Load_Balancer name=lb1 datapath_group="$dps" external_ids="lb_id=$nlb"], [0], [ignore])
+AT_CHECK([ovn-sbctl create Load_Balancer name=lb1 ls_datapath_group="$dps" external_ids="lb_id=$nlb"], [0], [ignore])
 
 check ovn-nbctl --wait=sb sync
 check_row_count Load_Balancer 1
@@ -6105,10 +6140,10 @@ AT_CHECK([grep -e "chk_pkt_len" -e "lr_in_larger_pkts" lr0flows | sed 's/table=.
   table=??(lr_in_chk_pkt_len  ), priority=0    , match=(1), action=(next;)
   table=??(lr_in_chk_pkt_len  ), priority=50   , match=(outport == "lr0-public"), action=(reg9[[1]] = check_pkt_larger(1514); next;)
   table=??(lr_in_larger_pkts  ), priority=0    , match=(1), action=(next;)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 20.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff02; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
 ])
 
 AT_CHECK([grep -E "lr_in_admission.*check_pkt_larger" lr0flows | sort], [0], [dnl
@@ -6136,10 +6171,10 @@ AT_CHECK([grep -e "chk_pkt_len" -e "lr_in_larger_pkts" lr0flows | sed 's/table=.
   table=??(lr_in_chk_pkt_len  ), priority=0    , match=(1), action=(next;)
   table=??(lr_in_chk_pkt_len  ), priority=50   , match=(outport == "lr0-public"), action=(reg9[[1]] = check_pkt_larger(1514); next;)
   table=??(lr_in_larger_pkts  ), priority=0    , match=(1), action=(next;)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 20.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff02; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
 ])
 
 AT_CHECK([grep -E "lr_in_admission.*check_pkt_larger" lr0flows | sort], [0], [dnl
@@ -6165,10 +6200,10 @@ AT_CHECK([grep -e "chk_pkt_len" -e "lr_in_larger_pkts" lr0flows | sed 's/table=.
   table=??(lr_in_chk_pkt_len  ), priority=50   , match=(outport == "lr0-public"), action=(reg9[[1]] = check_pkt_larger(1514); next;)
   table=??(lr_in_chk_pkt_len  ), priority=55   , match=(outport == "lr0-public" && (tcp)), action=(next;)
   table=??(lr_in_larger_pkts  ), priority=0    , match=(1), action=(next;)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 20.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff02; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
 ])
 
 AT_CHECK([grep "lr_in_admission" lr0flows | grep -e "check_pkt_larger" -e "tcp" | sort], [0], [dnl
@@ -6190,14 +6225,14 @@ AT_CHECK([grep -e "chk_pkt_len" -e "lr_in_larger_pkts" lr0flows | sed 's/table=.
   table=??(lr_in_chk_pkt_len  ), priority=50   , match=(outport == "lr0-sw0"), action=(reg9[[1]] = check_pkt_larger(1414); next;)
   table=??(lr_in_chk_pkt_len  ), priority=55   , match=(outport == "lr0-public" && (tcp)), action=(next;)
   table=??(lr_in_larger_pkts  ), priority=0    , match=(1), action=(next;)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 20.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff02; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 20.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff02; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
 ])
 
 AT_CHECK([grep "lr_in_admission.*check_pkt_larger" lr0flows | sort], [0], [dnl
@@ -6229,14 +6264,14 @@ AT_CHECK([grep -e "chk_pkt_len" -e "lr_in_larger_pkts" lr0flows | sed 's/table=.
   table=??(lr_in_chk_pkt_len  ), priority=55   , match=(outport == "lr0-public" && (tcp)), action=(next;)
   table=??(lr_in_chk_pkt_len  ), priority=55   , match=(outport == "lr0-sw0" && (tcp)), action=(next;)
   table=??(lr_in_larger_pkts  ), priority=0    , match=(1), action=(next;)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 20.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff02; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 20.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff02; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw0" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:01; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-public" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1500; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
 ])
 
 AT_CHECK([grep "lr_in_admission" lr0flows | grep -e "check_pkt_larger" -e "tcp" | sort], [0], [dnl
@@ -6255,15 +6290,16 @@ check ovn-nbctl --wait=sb clear logical_router_port lr0-public options
 ovn-sbctl dump-flows lr0 > lr0flows
 AT_CAPTURE_FILE([lr0flows])
 
+grep -e "chk_pkt_len" -e "lr_in_larger_pkts" lr0flows | sed 's/table=../table=??/' | sort
 AT_CHECK([grep -e "chk_pkt_len" -e "lr_in_larger_pkts" lr0flows | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_in_chk_pkt_len  ), priority=0    , match=(1), action=(next;)
   table=??(lr_in_chk_pkt_len  ), priority=50   , match=(outport == "lr0-sw0"), action=(reg9[[1]] = check_pkt_larger(1414); next;)
   table=??(lr_in_chk_pkt_len  ), priority=55   , match=(outport == "lr0-sw0" && (tcp)), action=(next;)
   table=??(lr_in_larger_pkts  ), priority=0    , match=(1), action=(next;)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip4.dst = ip4.src; ip4.src = 172.168.0.100; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip6.dst = ip6.src; ip6.src = fe80::200:20ff:fe20:1213; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 20.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
-  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff02; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-public" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:20:20:12:13; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip4 && reg9[[1]] && reg9[[0]] == 0), action=(icmp4_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip4.dst = ip4.src; ip4.src = 10.0.0.1; ip.ttl = 255; icmp4.type = 3; /* Destination Unreachable. */ icmp4.code = 4; /* Frag Needed and DF was Set. */ icmp4.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
+  table=??(lr_in_larger_pkts  ), priority=150  , match=(inport == "lr0-sw1" && outport == "lr0-sw0" && ip6 && reg9[[1]] && reg9[[0]] == 0), action=(icmp6_error {reg9[[0]] = 1; reg9[[1]] = 0; eth.dst = 00:00:00:00:ff:02; ip6.dst = ip6.src; ip6.src = fe80::200:ff:fe00:ff01; ip.ttl = 255; icmp6.type = 2; /* Packet Too Big. */ icmp6.code = 0; icmp6.frag_mtu = 1400; next(pipeline=ingress, table=0); };)
 ])
 
 check ovn-nbctl --wait=sb clear logical_router_port lr0-sw0 options
@@ -7361,9 +7397,9 @@ AT_CHECK([grep lr_in_unsnat lrflows | grep ct_snat | sed 's/table=../table=??/'
 ])
 
 AT_CHECK([grep lr_out_snat lrflows | grep ct_snat | sed 's/table=../table=??/' | sort], [0], [dnl
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat(172.16.1.10);)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_snat(10.0.0.10);)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_snat(192.168.0.10);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.10);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2") && (!ct.trk || !ct.rpl)), action=(ct_snat(10.0.0.10);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3") && (!ct.trk || !ct.rpl)), action=(ct_snat(192.168.0.10);)
 ])
 
 check ovn-nbctl --wait=sb lr-nat-del DR snat 20.0.0.10
@@ -7437,9 +7473,9 @@ AT_CHECK([grep lr_in_unsnat lrflows | grep ct_snat | sed 's/table=../table=??/'
 ])
 
 AT_CHECK([grep lr_out_snat lrflows | grep ct_snat | sed 's/table=../table=??/' | sort], [0], [dnl
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat(172.16.1.10);)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_snat(10.0.0.10);)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_snat(192.168.0.10);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.10);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2") && (!ct.trk || !ct.rpl)), action=(ct_snat(10.0.0.10);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3") && (!ct.trk || !ct.rpl)), action=(ct_snat(192.168.0.10);)
 ])
 
 AT_CHECK([grep lr_in_dnat lrflows | grep ct_dnat | sed 's/table=../table=??/' | sort], [0], [dnl
@@ -8618,7 +8654,7 @@ AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e 'ls_in_\(put\|lookup\)_fdb' | sort
 AT_CHECK([ovn-nbctl --wait=sb lsp-set-options ln_port localnet_learn_fdb=true])
 AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e 'ls_in_\(put\|lookup\)_fdb' | sort | sed 's/table=./table=?/'], [0], [dnl
   table=? (ls_in_lookup_fdb   ), priority=0    , match=(1), action=(next;)
-  table=? (ls_in_lookup_fdb   ), priority=100  , match=(inport == "ln_port"), action=(reg0[[11]] = lookup_fdb(inport, eth.src); next;)
+  table=? (ls_in_lookup_fdb   ), priority=100  , match=(inport == "ln_port"), action=(flags.localnet = 1; reg0[[11]] = lookup_fdb(inport, eth.src); next;)
   table=? (ls_in_put_fdb      ), priority=0    , match=(1), action=(next;)
   table=? (ls_in_put_fdb      ), priority=100  , match=(inport == "ln_port" && reg0[[11]] == 0), action=(put_fdb(inport, eth.src); next;)
 ])
@@ -8701,7 +8737,7 @@ AT_CHECK([grep "ls_in_lb " S1flows | sed 's/table=../table=??/' | sort], [0], [d
 
 ovn-sbctl get datapath S0 _uuid > dp_uuids
 ovn-sbctl get datapath S1 _uuid >> dp_uuids
-lb_dp_group=$(ovn-sbctl --bare --columns datapath_group find Load_Balancer name=lb0)
+lb_dp_group=$(ovn-sbctl --bare --columns ls_datapath_group find Load_Balancer name=lb0)
 AT_CHECK_UNQUOTED([ovn-sbctl --bare --columns _uuid,datapaths find Logical_DP_Group dnl
                     | grep -A1 $lb_dp_group | tail -1 | tr ' ' '\n' | sort], [0], [dnl
 $(cat dp_uuids | sort)
@@ -8985,7 +9021,6 @@ grep -c mutate], [0], [2
 # Pause ovn-northd and add/remove few addresses.  when it is resumed
 # it should use mutate for updating the address sets.
 check as northd ovn-appctl -t NORTHD_TYPE pause
-check as northd-backup ovn-appctl -t NORTHD_TYPE pause
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 check ovn-nbctl add address_set $foo_as_uuid addresses 1.1.1.5
@@ -10008,14 +10043,21 @@ ovs-vsctl add-br br-phys
 ovn_attach n1 br-phys 192.168.0.11
 
 check_recompute_counter() {
+    northd_recomp_min=$1
+    northd_recomp_max=$2
+    lflow_recomp_min=$3
+    lflow_recomp_max=$4
+    sync_sb_pb_recomp_min=$5
+    sync_sb_pb_recomp_max=$6
+
     northd_recomp=$(as northd ovn-appctl -t NORTHD_TYPE inc-engine/show-stats northd recompute)
-    AT_CHECK([test x$northd_recomp = x$1])
+    AT_CHECK([test $northd_recomp -ge $northd_recomp_min && test $northd_recomp -le $northd_recomp_max])
 
     lflow_recomp=$(as northd ovn-appctl -t NORTHD_TYPE inc-engine/show-stats lflow recompute)
-    AT_CHECK([test x$lflow_recomp = x$2])
+    AT_CHECK([test $lflow_recomp -ge $lflow_recomp_min && test $lflow_recomp -le $lflow_recomp_max])
 
     sync_sb_pb_recomp=$(as northd ovn-appctl -t NORTHD_TYPE inc-engine/show-stats sync_to_sb_pb recompute)
-    AT_CHECK([test x$sync_sb_pb_recomp = x$3])
+    AT_CHECK([test $sync_sb_pb_recomp -ge $sync_sb_pb_recomp_min && test $sync_sb_pb_recomp -le $sync_sb_pb_recomp_max])
 }
 
 check ovn-nbctl --wait=hv ls-add ls0
@@ -10032,29 +10074,29 @@ check ovn-nbctl --wait=hv lsp-add ls0 lsp0-0 -- lsp-set-addresses lsp0-0 "unknow
 ovs-vsctl add-port br-int lsp0-0 -- set interface lsp0-0 external_ids:iface-id=lsp0-0
 wait_for_ports_up
 check ovn-nbctl --wait=hv sync
-check_recompute_counter 5 5 5
+check_recompute_counter 4 5 5 5 5 5
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 check ovn-nbctl --wait=hv lsp-add ls0 lsp0-1 -- lsp-set-addresses lsp0-1 "aa:aa:aa:00:00:01 192.168.0.11"
 ovs-vsctl add-port br-int lsp0-1 -- set interface lsp0-1 external_ids:iface-id=lsp0-1
 wait_for_ports_up
 check ovn-nbctl --wait=hv sync
-check_recompute_counter 0 0 0
+check_recompute_counter 0 0 0 0 0 0
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 check ovn-nbctl --wait=hv lsp-add ls0 lsp0-2 -- lsp-set-addresses lsp0-2 "aa:aa:aa:00:00:02 192.168.0.12"
 ovs-vsctl add-port br-int lsp0-2 -- set interface lsp0-2 external_ids:iface-id=lsp0-2
 wait_for_ports_up
 check ovn-nbctl --wait=hv sync
-check_recompute_counter 0 0 0
+check_recompute_counter 0 0 0 0 0 0
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 check ovn-nbctl --wait=hv lsp-del lsp0-1
-check_recompute_counter 0 0 0
+check_recompute_counter 0 0 0 0 0 0
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 check ovn-nbctl --wait=hv lsp-set-addresses lsp0-2 "aa:aa:aa:00:00:88 192.168.0.88"
-check_recompute_counter 0 0 0
+check_recompute_counter 0 0 0 0 0 0
 
 # Delete and re-add a LSP for several times continuously, to ensure
 # frequent operations do not trigger recompute when there are in-flight
@@ -10068,14 +10110,14 @@ for i in $(seq 10); do
     check ovn-nbctl lsp-del lsp0-2
     check ovn-nbctl lsp-add ls0 lsp0-2 -- lsp-set-addresses lsp0-2 "aa:aa:aa:00:00:02 192.168.0.12"
 done
-check_recompute_counter 0 0 0
+check_recompute_counter 0 0 0 0 0 0
 
 # No change, no recompute
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 check ovn-nbctl --wait=sb sync
-check_recompute_counter 0 0 0
+check_recompute_counter 0 0 0 0 0 0
 
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 # Associate DHCP for lsp0-2
 ovn-nbctl dhcp-options-create 192.168.0.0/24
@@ -10085,9 +10127,9 @@ ovn-nbctl dhcp-options-set-options $CIDR_UUID   lease_time=3600  router=192.168.
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 ovn-nbctl --wait=sb lsp-set-dhcpv4-options lsp0-2 $CIDR_UUID
-check_recompute_counter 0 0 0
+check_recompute_counter 0 0 0 0 0 0
 
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 # Add IPv6 address and associate DHCPv6 for lsp0-2
 check ovn-nbctl lsp-set-addresses lsp0-2 "aa:aa:aa:00:00:01 192.168.0.11 aef0::4"
@@ -10096,9 +10138,9 @@ options="\"server_id\"=\"00:00:00:10:00:01\"")"
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 ovn-nbctl --wait=sb lsp-set-dhcpv6-options lsp0-2 ${d1}
-check_recompute_counter 0 0 0
+check_recompute_counter 0 0 0 0 0 0
 
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 check ovn-nbctl --wait=hv ls-del ls0
 
@@ -10125,11 +10167,11 @@ ovn-nbctl lsp-add ls0 ls0-lr0
 ovn-nbctl lsp-set-type ls0-lr0 router
 ovn-nbctl lsp-set-addresses ls0-lr0 router
 check ovn-nbctl --wait=sb lsp-set-options ls0-lr0 router-port=lr0-ls0
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 ovn-nbctl lb-add lb0 192.168.0.10:80 10.0.0.10:8080
 check ovn-nbctl --wait=sb ls-lb-add ls0 lb0
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 # Add a lsp.  northd and lflow engine shouldn't recompute even though this is
@@ -10216,14 +10258,14 @@ check ovn-nbctl --wait=sb meter-add m drop 1 pktps
 check ovn-nbctl --wait=sb acl-add ls from-lport 1 1 allow
 dnl Only triggers recompute of the sync_meters and lflow nodes.
 check_recompute_counter 0 2 2
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 check ovn-nbctl --wait=sb meter-del m
 check ovn-nbctl --wait=sb acl-del ls
 dnl Only triggers recompute of the sync_meters and lflow nodes.
 check_recompute_counter 0 2 2
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 AT_CLEANUP
 ])
@@ -10339,7 +10381,7 @@ wait_for_ports_up sw0-r1
 check_column $remote_chassis_uuid Port_Binding chassis logical_port=sw0-r1
 
 # Set the type to router and ovn-northd should not claim it.
-check ovn-nbctl lsp-set-type sw0-r1 router
+check ovn-nbctl --wait=hv lsp-set-type sw0-r1 router
 check_column '' Port_Binding chassis logical_port=sw0-r1
 
 AT_CLEANUP
@@ -10391,34 +10433,39 @@ check ovn-nbctl --wait=sb lb-add lb1 10.0.0.10:80 10.0.0.3:80
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 
 check ovn-nbctl --wait=sb set load_balancer . ip_port_mappings:10.0.0.3=sw0-p1:10.0.0.2
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 check ovn-nbctl --wait=sb set load_balancer . options:foo=bar
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 check ovn-nbctl --wait=sb -- lb-add lb2 20.0.0.10:80 20.0.0.20:80 -- lb-add lb3 30.0.0.10:80 30.0.0.20:80
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 
 check ovn-nbctl --wait=sb -- lb-del lb2 -- lb-del lb3
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 
 AT_CHECK([ovn-nbctl --wait=sb \
@@ -10429,6 +10476,7 @@ AT_CHECK([ovn-nbctl --wait=sb \
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 # Any change to load balancer health check should also result in full recompute
 # of northd node (but not northd_lb_data node)
@@ -10437,6 +10485,7 @@ check ovn-nbctl --wait=sb set load_balancer_health_check . options:foo=bar1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 # Delete the health check from the load balancer.  northd engine node should do a full recompute.
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10444,6 +10493,7 @@ check ovn-nbctl --wait=sb clear Load_Balancer . health_check
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 check ovn-nbctl ls-add sw0
@@ -10457,6 +10507,7 @@ ovn-nbctl --wait=sb lsp-set-options sw0-lr0 router-port=lr0-sw0
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 # Associate lb1 to sw0. There should be no recompute of northd engine node
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10464,7 +10515,11 @@ check ovn-nbctl --wait=sb ls-lb-add sw0 lb1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+# A LB applied to a switch/router triggers:
+# - a recompute in the first iteration (handling northd change)
+# - a compute in the second iteration (handling SB update)
+check_engine_stats sync_to_sb_lb recompute compute
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 # Modify the backend of the lb1 vip
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10472,7 +10527,8 @@ check ovn-nbctl --wait=sb set load_balancer lb1 vips:'"10.0.0.10:80"'='"10.0.0.1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+check_engine_stats sync_to_sb_lb recompute compute
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 # Cleanup the vip of lb1.
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10480,7 +10536,8 @@ check ovn-nbctl --wait=sb clear load_Balancer lb1 vips
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+check_engine_stats sync_to_sb_lb recompute compute
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 # Set the vips of lb1 back
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10488,7 +10545,8 @@ check ovn-nbctl --wait=sb lb-add lb1 10.0.0.10:80 10.0.0.3:80
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+check_engine_stats sync_to_sb_lb recompute compute
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 # Add another vip to lb1
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10496,7 +10554,8 @@ check ovn-nbctl --wait=sb lb-add lb1 10.0.0.20:80 10.0.0.30:8080
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+check_engine_stats sync_to_sb_lb recompute compute
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 # Disassociate lb1 from sw0. There should be a full recompute of northd engine node.
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10504,7 +10563,8 @@ check ovn-nbctl --wait=sb ls-lb-del sw0 lb1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
-CHECK_NO_CHANGE_AFTER_RECOMPUTE
+check_engine_stats sync_to_sb_lb recompute compute
+CHECK_NO_CHANGE_AFTER_RECOMPUTE(1)
 
 # Associate lb1 to sw0 and also create a port sw0p1.  This should not result in
 # full recompute of northd, but should rsult in full recompute of lflow node.
@@ -10513,6 +10573,7 @@ check ovn-nbctl --wait=sb ls-lb-add sw0 lb1 -- lsp-add sw0 sw0p1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10523,6 +10584,7 @@ check ovn-nbctl --wait=sb ls-lb-del sw0 lb1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Add lb1 to lr0 and then disassociate
@@ -10531,6 +10593,7 @@ check ovn-nbctl --wait=sb lr-lb-add lr0 lb1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Modify the backend of the lb1 vip
@@ -10539,6 +10602,7 @@ check ovn-nbctl --wait=sb set load_balancer lb1 vips:'"10.0.0.10:80"'='"10.0.0.1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Cleanup the vip of lb1.
@@ -10547,6 +10611,7 @@ check ovn-nbctl --wait=sb clear load_Balancer lb1 vips
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Set the vips of lb1 back
@@ -10555,6 +10620,7 @@ check ovn-nbctl --wait=sb lb-add lb1 10.0.0.10:80 10.0.0.3:80
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Add another vip to lb1
@@ -10563,6 +10629,7 @@ check ovn-nbctl --wait=sb lb-add lb1 10.0.0.20:80 10.0.0.30:8080
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10570,14 +10637,16 @@ check ovn-nbctl --wait=sb lr-lb-del lr0 lb1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Test load balancer group now
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-lbg1_uuid=$(ovn-nbctl create load_balancer_group name=lbg1)
+lbg1_uuid=$(ovn-nbctl --wait=sb create load_balancer_group name=lbg1)
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10586,31 +10655,35 @@ lb1_uuid=$(fetch_column nb:Load_Balancer _uuid)
 
 # Add lb to the lbg1 group
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl add load_balancer_group . load_Balancer $lb1_uuid
+check ovn-nbctl --wait=sb add load_balancer_group . load_Balancer $lb1_uuid
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl clear load_balancer_group . load_Balancer
+check ovn-nbctl --wait=sb clear load_balancer_group . load_Balancer
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 # Add back lb to the lbg1 group
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl add load_balancer_group . load_Balancer $lb1_uuid
+check ovn-nbctl --wait=sb add load_balancer_group . load_Balancer $lb1_uuid
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl add logical_switch sw0 load_balancer_group $lbg1_uuid
+check ovn-nbctl --wait=sb add logical_switch sw0 load_balancer_group $lbg1_uuid
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 
 # Update lb and this should not result in northd recompute
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10618,6 +10691,7 @@ check ovn-nbctl --wait=sb set load_balancer . options:bar=foo
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 
 # Modify the backend of the lb1 vip
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10625,6 +10699,7 @@ check ovn-nbctl --wait=sb set load_balancer lb1 vips:'"10.0.0.10:80"'='"10.0.0.1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Cleanup the vip of lb1.
@@ -10633,6 +10708,7 @@ check ovn-nbctl --wait=sb clear load_Balancer lb1 vips
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Set the vips of lb1 back
@@ -10641,6 +10717,7 @@ check ovn-nbctl --wait=sb lb-add lb1 10.0.0.10:80 10.0.0.3:80
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Add another vip to lb1
@@ -10649,19 +10726,22 @@ check ovn-nbctl --wait=sb lb-add lb1 10.0.0.20:80 10.0.0.30:8080
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl clear logical_switch sw0 load_balancer_group
+check ovn-nbctl --wait=sb clear logical_switch sw0 load_balancer_group
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
 check ovn-nbctl add logical_router lr0 load_balancer_group $lbg1_uuid
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Modify the backend of the lb1 vip
@@ -10670,6 +10750,7 @@ check ovn-nbctl --wait=sb set load_balancer lb1 vips:'"10.0.0.10:80"'='"10.0.0.1
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Cleanup the vip of lb1.
@@ -10678,6 +10759,7 @@ check ovn-nbctl --wait=sb clear load_Balancer lb1 vips
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Set the vips of lb1 back
@@ -10686,6 +10768,7 @@ check ovn-nbctl --wait=sb lb-add lb1 10.0.0.10:80 10.0.0.3:80
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Add another vip to lb1
@@ -10694,27 +10777,31 @@ check ovn-nbctl --wait=sb lb-add lb1 10.0.0.20:80 10.0.0.30:8080
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl clear logical_router lr0 load_balancer_group
+check ovn-nbctl --wait=sb clear logical_router lr0 load_balancer_group
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 
 # Add back lb group to logical switch and then delete it.
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl add logical_switch sw0 load_balancer_group $lbg1_uuid
+check ovn-nbctl --wait=sb add logical_switch sw0 load_balancer_group $lbg1_uuid
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl clear logical_switch sw0 load_balancer_group -- \
+check ovn-nbctl --wait=sb clear logical_switch sw0 load_balancer_group -- \
     destroy load_balancer_group $lbg1_uuid
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb compute compute
 
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
@@ -10733,29 +10820,33 @@ lb3_uuid=$(fetch_column nb:Load_Balancer _uuid name=lb3)
 lb4_uuid=$(fetch_column nb:Load_Balancer _uuid name=lb4)
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-lbg1_uuid=$(ovn-nbctl create load_balancer_group name=lbg1)
+lbg1_uuid=$(ovn-nbctl --wait=sb create load_balancer_group name=lbg1)
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl set load_balancer_group . load_balancer="$lb2_uuid,$lb3_uuid,$lb4_uuid"
+check ovn-nbctl --wait=sb set load_balancer_group . load_balancer="$lb2_uuid,$lb3_uuid,$lb4_uuid"
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl set logical_switch sw0 load_balancer_group=$lbg1_uuid
+check ovn-nbctl --wait=sb set logical_switch sw0 load_balancer_group=$lbg1_uuid
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
-check ovn-nbctl set logical_router lr1 load_balancer_group=$lbg1_uuid
+check ovn-nbctl --wait=sb set logical_router lr1 load_balancer_group=$lbg1_uuid
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10763,6 +10854,7 @@ check ovn-nbctl --wait=sb ls-lb-add sw0 lb2
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10770,6 +10862,7 @@ check ovn-nbctl --wait=sb ls-lb-add sw0 lb3
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10778,6 +10871,7 @@ check ovn-nbctl --wait=sb lr-lb-add lr1 lb2
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10785,6 +10879,7 @@ check ovn-nbctl --wait=sb ls-lb-del sw0 lb2
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10792,6 +10887,7 @@ check ovn-nbctl --wait=sb lr-lb-del lr1 lb2
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute nocompute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Deleting lb4 should not result in lflow recompute as it is
@@ -10801,6 +10897,7 @@ check ovn-nbctl --wait=sb lb-del lb4
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 # Deleting lb2 should result in lflow recompute as it is
@@ -10810,6 +10907,7 @@ check ovn-nbctl --wait=sb lb-del lb2
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd norecompute compute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t NORTHD_TYPE inc-engine/clear-stats
@@ -10817,7 +10915,61 @@ check ovn-nbctl --wait=sb remove load_balancer_group . load_balancer $lb3_uuid
 check_engine_stats lb_data norecompute compute
 check_engine_stats northd recompute nocompute
 check_engine_stats lflow recompute nocompute
+check_engine_stats sync_to_sb_lb recompute compute
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([Load balancer incremental processing - batched updates])
+ovn_start
+
+# Check the scenario when a LB is created and quickly deleted (northd
+# processes this in a single iteration).
+
+check ovn-nbctl ls-add sw0
+lbg_uuid=$(ovn-nbctl --wait=sb create load_balancer_group name=lbg)
+check ovn-nbctl --wait=sb add logical_switch sw0 load_balancer_group $lbg_uuid
+
+# Pause ovn-northd.
+sleep_northd
+
+check ovn-nbctl lb-add lb-temp 50.0.0.10:80 50.0.0.20:8080
+lb_temp_uuid=$(fetch_column nb:Load_Balancer _uuid name=lb-temp)
+check ovn-nbctl add load_balancer_group $lbg_uuid load_balancer $lb_temp_uuid
+check ovn-nbctl lb-del lb-temp
+
+# Let ovn-northd process all the updates that happened since it went to sleep.
+wake_up_northd
+
+# Add a new load balancer to make sure northd still functions properly.
+check ovn-nbctl lb-add lb1 50.0.0.10:80 50.0.0.30:8080
+lb1_uuid=$(fetch_column nb:Load_Balancer _uuid name=lb1)
+check ovn-nbctl add load_balancer_group $lbg_uuid load_balancer $lb1_uuid
+
+check ovn-nbctl --wait=sb sync
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
+# Re-check the same scenario but now also batch the additional LB creation.
+sleep_northd
+check ovn-nbctl lb-add lb-temp 50.0.0.10:80 50.0.0.20:8080
+lb_temp_uuid=$(fetch_column nb:Load_Balancer _uuid name=lb-temp)
+check ovn-nbctl add load_balancer_group $lbg_uuid load_balancer $lb_temp_uuid
+check ovn-nbctl lb-del lb-temp
+
+# Add a new load balancer to make sure northd still functions properly.
+check ovn-nbctl lb-add lb2 50.0.0.10:80 50.0.0.30:8080
+lb2_uuid=$(fetch_column nb:Load_Balancer _uuid name=lb2)
+check ovn-nbctl add load_balancer_group $lbg_uuid load_balancer $lb2_uuid
+
+wake_up_northd
+check ovn-nbctl --wait=sb sync
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+
+AT_CHECK([as northd ovn-appctl -t NORTHD_TYPE status], [0], [dnl
+Status: active
+])
+
 AT_CLEANUP
 ])
diff --git a/tests/ovn.at b/tests/ovn.at
index e127530f6..13f5575be 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -23,8 +23,11 @@ m4_divert_text([PREPARE_TESTS],
      diff -u $exp_text.sorted $rcv_text.sorted
    }
    ovn_check_packets__ () {
-     echo
-     echo "$3: checking packets in $1 against $2:"
+     if [[ -n "$4" ]]; then
+       echo "$3: checking packets in $1 against $2: using $4"
+     else
+       echo "$3: checking packets in $1 against $2:"
+     fi
      rcv_pcap=$1
      rcv_text=`echo "$rcv_pcap.packets" | sed 's/\.pcap//'`
      exp_text=$2
@@ -35,7 +38,13 @@ m4_divert_text([PREPARE_TESTS],
         echo "rcv_n=$rcv_n exp_n=$exp_n"
         test $rcv_n -ge $exp_n],
        [dump_diff__ "$rcv_pcap" "$exp_text"])
-     sort $exp_text > expout
+     if [[ -n "$4" ]]; then
+       sort $exp_text | $4 > expout
+       cat $rcv_text | $4 > rcv_tmp
+       mv rcv_tmp $rcv_text
+     else
+       sort $exp_text > expout
+     fi
    }
    ovn_check_packets_remove_broadcast__ () {
      echo "$3: checking packets in $1 against $2:"
@@ -54,14 +63,26 @@ m4_divert_text([PREPARE_TESTS],
    }
    ovn_wait_packets__ () {
      echo "$3: waiting for packets from $2 at $1:"
+     if [[ -n "$4" ]]; then
+       echo "$3: checking packets from $2 at $1: using $4"
+     else
+       echo "$3: checking packets from $2 at $1:"
+     fi
      rcv_pcap=$1
      rcv_text=`echo "$rcv_pcap.packets" | sed 's/\.pcap//'`
      exp_text=$2
+     if [[ -n "$4" ]]; then
+       cmd=$4
+     else
+       cmd=:
+     fi
      OVS_WAIT_UNTIL(
        [$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $rcv_pcap > $rcv_text
-        sort $exp_text > expout
-        test x"$(sort $rcv_text | comm -2 -3 expout -)" = "x"],
+        sort $exp_text | $cmd > expout
+        test x"$(sort $rcv_text | $cmd | comm -2 -3 expout -)" = "x"],
        [dump_diff__ "$rcv_pcap" "$exp_text"])
+     cat $rcv_text | $cmd > rcv_tmp
+     mv rcv_tmp $rcv_text
    }
    ovn_wait_packets_uniq__ () {
      echo "$3: waiting for packets from $2 at $1:"
@@ -140,7 +161,7 @@ m4_divert_text([PREPARE_TESTS],
 
 m4_define([OVN_CHECK_PACKETS],
   [AT_CHECK([$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $1 ], [0], [ignore])
-   ovn_check_packets__ "$1" "$2" "__file__:__line__"
+   ovn_check_packets__ "$1" "$2" "__file__:__line__" $3
    AT_CHECK([sort $rcv_text], [0], [expout], [ignore], [dump_diff__ "$1" "$2"])])
 
 m4_define([OVN_CHECK_PACKETS_REMOVE_BROADCAST],
@@ -2198,13 +2219,13 @@ reg9[5] = chk_ecmp_nh();
 
 # commit_lb_aff
 commit_lb_aff(vip = "172.16.0.123:8080", backend = "10.0.0.3:8080", proto = tcp, timeout = 30);
-    encodes as learn(table=78,idle_timeout=30,delete_learned,OXM_OF_METADATA[],eth_type=0x800,NXM_OF_IP_SRC[],ip_dst=172.16.0.123,nw_proto=6,tcp_dst=8080,load:0x1->NXM_NX_REG10[14],load:0xa000003->NXM_NX_REG4[],load:0x1f90->NXM_NX_REG8[0..15])
+    encodes as learn(table=78,idle_timeout=30,delete_learned,cookie=0xaaaaaaaa,OXM_OF_METADATA[],eth_type=0x800,NXM_OF_IP_SRC[],ip_dst=172.16.0.123,nw_proto=6,tcp_dst=8080,load:0x1->NXM_NX_REG10[14],load:0xa000003->NXM_NX_REG4[],load:0x1f90->NXM_NX_REG8[0..15])
 
 commit_lb_aff(vip = "172.16.0.123", backend = "10.0.0.3", timeout = 30);
-    encodes as learn(table=78,idle_timeout=30,delete_learned,OXM_OF_METADATA[],eth_type=0x800,NXM_OF_IP_SRC[],ip_dst=172.16.0.123,load:0x1->NXM_NX_REG10[14],load:0xa000003->NXM_NX_REG4[])
+    encodes as learn(table=78,idle_timeout=30,delete_learned,cookie=0xaaaaaaaa,OXM_OF_METADATA[],eth_type=0x800,NXM_OF_IP_SRC[],ip_dst=172.16.0.123,load:0x1->NXM_NX_REG10[14],load:0xa000003->NXM_NX_REG4[])
 
 commit_lb_aff(vip = "[::1]:8080", backend = "[::2]:8080", proto = tcp, timeout = 30);
-    encodes as learn(table=78,idle_timeout=30,delete_learned,OXM_OF_METADATA[],eth_type=0x86dd,NXM_NX_IPV6_SRC[],ipv6_dst=::1,nw_proto=6,tcp_dst=8080,load:0x1->NXM_NX_REG10[14],load:0x2->NXM_NX_XXREG0[],load:0x1f90->NXM_NX_REG8[0..15])
+    encodes as learn(table=78,idle_timeout=30,delete_learned,cookie=0xaaaaaaaa,OXM_OF_METADATA[],eth_type=0x86dd,NXM_NX_IPV6_SRC[],ipv6_dst=::1,nw_proto=6,tcp_dst=8080,load:0x1->NXM_NX_REG10[14],load:0x2->NXM_NX_XXREG0[],load:0x1f90->NXM_NX_REG8[0..15])
 
 # chk_lb_aff()
 reg9[6] = chk_lb_aff();
@@ -3849,7 +3870,7 @@ OVN_FOR_EACH_NORTHD([
 AT_SETUP([VLAN transparency, passthru=true, multiple hosts, custom ethtype])
 ovn_start
 
-ethtype=802.11ad
+ethtype=802.1ad
 
 check ovn-nbctl ls-add ls
 check ovn-nbctl --wait=sb add Logical-Switch ls other_config vlan-passthru=true
@@ -4650,6 +4671,12 @@ OVN_POPULATE_ARP
 
 wait_for_ports_up
 check ovn-nbctl --wait=hv sync
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv1"],["hv2"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv2"],["hv1"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv1"],["hv_gw"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv_gw"],["hv1"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv2"],["hv_gw"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv_gw"],["hv2"])
 
 # test_packet INPORT DST SRC ETHTYPE OUTPORT...
 #
@@ -4849,6 +4876,13 @@ check ovn-nbctl --wait=hv sync
 # for ARP resolution).
 OVN_POPULATE_ARP
 
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv1"],["hv2"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv2"],["hv1"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv2"],["hv3"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv3"],["hv2"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv1"],["hv3"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv3"],["hv1"])
+
 # test_ip INPORT SRC_MAC DST_MAC SRC_IP DST_IP OUTPORT...
 #
 # This shell function causes a packet to be received on INPORT.  The packet's
@@ -5320,10 +5354,11 @@ test_arp() {
 }
 
 test_na() {
-    local inport=$1 sha=$2 spa=$3
+    local inport=$1 sha=$2 spa=$3 src=${4-$3}
     local request=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
-                             IPv6(dst='ff01::1', src='${spa}')/ \
-                             ICMPv6ND_NA(tgt='${spa}')")
+                             IPv6(dst='ff01::1', src='${src}')/ \
+                             ICMPv6ND_NA(tgt='${spa}')/ \
+                             ICMPv6NDOptDstLLAddr(lladdr='${sha}')")
 
     hv=hv`vif_to_hv $inport`
     as $hv ovs-appctl netdev-dummy/receive vif$inport $request
@@ -5399,6 +5434,24 @@ for i in 1 2; do
     done
 done
 
+# Make sure that we can update existing entry with
+# "always_learn_from_arp_request=false" even when the source of NA src is LLA.
+check ovn-sbctl --all destroy mac_binding
+check ovn-nbctl --wait=hv set logical_router lr0 options:always_learn_from_arp_request=false
+
+sha="f0:00:00:00:00:11"
+spa6="fd00::abcd:1"
+
+test_na 11 $sha $spa6
+wait_row_count MAC_Binding 1 ip=\"$spa6\" mac=\"$sha\"
+
+sha="f0:00:00:00:00:12"
+lla6="fe80::abcd:1"
+
+test_na 11 $sha $spa6 $lla6
+wait_row_count MAC_Binding 1 ip=\"$spa6\" mac=\"$sha\"
+check_row_count MAC_Binding 0 ip=\"$lla6\"
+
 # Gracefully terminate daemons
 OVN_CLEANUP([hv1], [hv2])
 
@@ -6758,13 +6811,7 @@ test_dhcp() {
 }
 
 compare_dhcp_packets() {
-    received=$($PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif$1-tx.pcap)
-    expected=$(cat $1.expected)
-
-    if test "$received" != "$expected"; then
-        AT_CHECK_UNQUOTED([echo "$received"; tcpdump_hex "$received"], [0],
-            [$(echo "$expected"; tcpdump_hex "$expected")])
-    fi
+    OVN_CHECK_PACKETS([hv1/vif$1-tx.pcap], [$1.expected])
 }
 
 AT_CAPTURE_FILE([sbflows])
@@ -6962,8 +7009,9 @@ expected_dhcp_opts=0
 test_dhcp 11 2 f00000000002 07 0 $ciaddr $offer_ip $request_ip 0 0 ff1000000001
 
 # There is no reply for this. Check for the INFO log in ovn-controller.log
-AT_CHECK([test 1 = $(cat hv1/ovn-controller.log | \
-grep "DHCPRELEASE f0:00:00:00:00:02 10.0.0.6" -c)])
+OVS_WAIT_UNTIL(
+    [test 1 = $(cat hv1/ovn-controller.log | grep "DHCPRELEASE f0:00:00:00:00:02 10.0.0.6" -c)
+])
 
 $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap > 2.packets
 AT_CHECK([cat 2.packets], [0], [])
@@ -7120,7 +7168,9 @@ ciaddr=`ip_to_hex 0 0 0 0`
 request_ip=0
 expected_dhcp_opts=""
 test_dhcp 18 1 f00000000001 04 0 $ciaddr $offer_ip $request_ip 0 0 ff1000000001 $server_ip 02 $expected_dhcp_opts
-AT_CHECK([grep -F -iq 'DHCPDECLINE from f0:00:00:00:00:01, 10.0.0.4 duplicated' hv1/ovn-controller.log], [0], [])
+OVS_WAIT_UNTIL(
+    [test 1 -le $(grep -F -i -c 'DHCPDECLINE from f0:00:00:00:00:01, 10.0.0.4 duplicated' hv1/ovn-controller.log)
+])
 
 # Send Etherboot.
 
@@ -7138,7 +7188,7 @@ ovn-nbctl dhcp-options-set-options $d3 \
    lease_time=3600 router=10.0.0.1 bootfile_name_alt=\"bootfile_name_alt\" \
    bootfile_name=\"bootfile\"
 
-ovn-nbctl lsp-set-dhcpv4-options ls1-lp1 $d3
+ovn-nbctl --wait=hv lsp-set-dhcpv4-options ls1-lp1 $d3
 
 offer_ip=`ip_to_hex 10 0 0 4`
 server_ip=`ip_to_hex 10 0 0 1`
@@ -7156,9 +7206,6 @@ compare_dhcp_packets 1
 as northd
 OVS_APP_EXIT_AND_WAIT([NORTHD_TYPE])
 
-as northd-backup
-OVS_APP_EXIT_AND_WAIT([NORTHD_TYPE])
-
 northd_version=$(ovn-sbctl get SB_Global . options:northd_internal_version | sed s/\"//g)
 echo "northd version = $northd_version"
 
@@ -7295,10 +7342,6 @@ check ovn-nbctl --wait=hv sync
 # Start with 0 because the first request will not have NXT_RESUME
 n_resume=0
 
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
 # This shell function sends a DHCPv6 request packet
 # test_dhcpv6 INPORT SRC_MAC SRC_LLA DHCPv6_MSG_TYPE OFFER_IP OUTPORT...
 # The OUTPORTs (zero or more) list the VIFs on which the original DHCPv6
@@ -7400,12 +7443,8 @@ test_dhcpv6_release() {
 check_packets() {
     local port=$1
 
-    $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif$port-tx.pcap | trim_zeros > $port.packets
     # Skipping UDP checksum
-    cat $port.expected | cut -c 1-120,125- > expout
-    AT_CHECK([cat $port.packets | cut -c 1-120,125- ], [0], [expout])
-
-    rm $port.packets
+    OVN_CHECK_PACKETS([hv1/vif$port-tx.pcap], [$port.expected], ["trim_zeros | cut -c 1-120,125-"])
     rm $port.expected
 }
 
@@ -7508,7 +7547,7 @@ ovn-nbctl dhcp-options-set-options $d1 \
     server_id=00:00:00:10:00:01 \
     bootfile_name_alt=\"bootfile_name_alt\" \
     bootfile_name=\"bootfile_name\"
-ovn-nbctl lsp-set-dhcpv6-options ls1-lp2 ${d1}
+ovn-nbctl --wait=hv lsp-set-dhcpv6-options ls1-lp2 ${d1}
 
 reset_pcap_file hv1-vif2 hv1/vif2
 
@@ -7645,6 +7684,9 @@ ovn-nbctl lsp-add alice alice1 \
 wait_for_ports_up
 check ovn-nbctl --wait=hv sync
 
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv1"],["hv2"])
+OVN_WAIT_REMOTE_OUTPUT_FLOWS(["hv2"],["hv1"])
+
 # Send ip packets between foo1 and alice1
 src_mac="f00000010203"
 dst_mac="000001010203"
@@ -8418,6 +8460,7 @@ check_dynamic_addresses() {
     check_row_count nb:Logical_Switch_Port 1 name="$1" dynamic_addresses="$arg"
 }
 
+check ovn-nbctl --wait=sb sync
 # Add a port to a switch that does not have a subnet set, then set the
 # subnet which should result in an address being allocated for the port.
 ovn-nbctl --wait=hv set NB_Global . options:mac_prefix="0a:00:00:00:00:00"
@@ -8733,7 +8776,7 @@ OVN_FOR_EACH_NORTHD([
 AT_SETUP([ipam connectivity])
 ovn_start
 
-ovn-nbctl lr-add R1
+ovn-nbctl --wait=sb lr-add R1
 
 # Test for a ping using dynamically allocated addresses.
 ovn-nbctl --wait=hv set NB_Global . options:mac_prefix="0a:00:00:00:00:00"
@@ -9019,10 +9062,6 @@ packet=${dst_mac}${src_mac}08004500001c0000000040110000${src_ip}${dst_ip}0035111
 # Send IP packet destined to 8.8.8.8 from lsp1lp2
 as hv1 ovs-appctl netdev-dummy/receive hv1-ls1lp2 $packet
 
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
 # ARP packet should be received with Target IP Address set to 192.168.1.254 and
 # not 8.8.8.8
 
@@ -9078,9 +9117,6 @@ AT_CAPTURE_FILE([sbflows])
 
 # Wait for packet to be received.
 OVS_WAIT_UNTIL([test `wc -c < "hv1/snoopvif-tx.pcap"` -ge 140])
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
 $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap | trim_zeros |sort | uniq > packets
 AT_CHECK([sort packets], [0], [dnl
 fffffffffffff0000000000108060001080006040001f00000000001c0a80001000000000000c0a80001
@@ -9277,9 +9313,6 @@ ovn-nbctl list logical_router_port lrp0
 ovn-nbctl show
 # Wait for packet to be received.
 OVS_WAIT_UNTIL([test `wc -c < "hv1/snoopvif-tx.pcap"` -ge 50])
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
 $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap | trim_zeros | sort | uniq > packets
 expected="fffffffffffff0000000000108060001080006040001f00000000001c0a80001000000000000c0a80001"
 echo $expected > expout
@@ -9300,9 +9333,6 @@ ovn-nbctl lsp-set-options lrp0-rp router-port=lrp0 nat-addresses="router" exclud
 
 # Wait for packets to be received.
 OVS_WAIT_UNTIL([test `wc -c < "hv1/snoopvif-tx.pcap"` -ge 250])
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
 
 $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap | trim_zeros > packets
 g0="fffffffffffff0000000000108060001080006040001f00000000001c0a80001000000000000c0a80001"
@@ -10721,10 +10751,6 @@ OVN_POPULATE_ARP
 wait_for_ports_up
 check ovn-nbctl --wait=hv sync
 
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
 # Send ip packets between foo1 and bar1
 # (East-west traffic should flow normally)
 src_mac="f00000010203"
@@ -11004,12 +11030,9 @@ test_dns 1 f00000000001 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $d
 # NXT_RESUMEs should be 1.
 OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap > 1.packets
-cat 1.expected | cut -c -48 > expout
-AT_CHECK([cat 1.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat 1.expected | cut -c 53- > expout
-AT_CHECK([cat 1.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c 53-"])
 
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
@@ -11026,12 +11049,9 @@ test_dns 2 f00000000002 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $d
 # NXT_RESUMEs should be 2.
 OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap > 2.packets
-cat 2.expected | cut -c -48 > expout
-AT_CHECK([cat 2.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [2.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat 2.expected | cut -c 53- > expout
-AT_CHECK([cat 2.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [2.expected], ["cut -c 53-"])
 
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
@@ -11049,12 +11069,9 @@ test_dns 2 f00000000002 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $d
 # NXT_RESUMEs should be 3.
 OVS_WAIT_UNTIL([test 3 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap > 2.packets
-cat 2.expected | cut -c -48 > expout
-AT_CHECK([cat 2.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [2.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat 2.expected | cut -c 53- > expout
-AT_CHECK([cat 2.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [2.expected], ["cut -c 53-"])
 
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
@@ -11131,12 +11148,9 @@ test_dns 2 f00000000002 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $d
 # NXT_RESUMEs should be 5.
 OVS_WAIT_UNTIL([test 5 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap > 2.packets
-cat 2.expected | cut -c -48 > expout
-AT_CHECK([cat 2.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [2.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat 2.expected | cut -c 53- > expout
-AT_CHECK([cat 2.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [2.expected], ["cut -c 53-"])
 
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
@@ -11154,12 +11168,9 @@ test_dns 2 f00000000002 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $d
 # NXT_RESUMEs should be 6.
 OVS_WAIT_UNTIL([test 6 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap > 2.packets
-cat 2.expected | cut -c -48 > expout
-AT_CHECK([cat 2.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [2.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat 2.expected | cut -c 53- > expout
-AT_CHECK([cat 2.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [2.expected], ["cut -c 53-"])
 
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
@@ -11221,12 +11232,9 @@ test_dns 1 f00000000001 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $d
 # NXT_RESUMEs should be 9.
 OVS_WAIT_UNTIL([test 9 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap > 1.packets
-cat 1.expected | cut -c -48 > expout
-AT_CHECK([cat 1.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat 1.expected | cut -c 53- > expout
-AT_CHECK([cat 1.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c 53-"])
 
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
@@ -11244,10 +11252,8 @@ test_dns6 1 f00000000001 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $
 # NXT_RESUMEs should be 10
 OVS_WAIT_UNTIL([test 10 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap > 1.packets
 # Skipping the UDP checksum.
-cat 1.expected | cut -c 1-120,125- > expout
-AT_CHECK([cat 1.packets | cut -c 1-120,125-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c 1-120,125-"])
 
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
@@ -11273,12 +11279,9 @@ test_dns 1 f00000000001 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $d
 # NXT_RESUMEs should be 11.
 OVS_WAIT_UNTIL([test 11 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap > 1.packets
-cat 1.expected | cut -c -48 > expout
-AT_CHECK([cat 1.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat 1.expected | cut -c 53- > expout
-AT_CHECK([cat 1.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c 53-"])
 
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
@@ -11296,12 +11299,9 @@ test_dns 1 f00000000001 f000000000f0 $src_ip $dst_ip $dns_reply $dns_req_data $d
 # NXT_RESUMEs should be 12.
 OVS_WAIT_UNTIL([test 12 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap > 1.packets
-cat 1.expected | cut -c -48 > expout
-AT_CHECK([cat 1.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat 1.expected | cut -c 53- > expout
-AT_CHECK([cat 1.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c 53-"])
 
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
@@ -11481,30 +11481,7 @@ test_ip_packet()
     # Resend packet from foo1 to outside1
     check as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 $packet
 
-    AT_CAPTURE_FILE([exp])
-    AT_CAPTURE_FILE([rcv])
-    check_packets() {
-        > exp
-        > rcv
-
-        pcap=ext1/vif1-tx.pcap
-        type=ext1-vif1.expected
-        echo "--- $pcap" | tee -a exp >> rcv
-        sort -u "$type" >> exp
-        $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $pcap | sort -u >> rcv
-        echo | tee -a exp >> rcv
-
-        pcap=$active_gw/br-phys_n1-tx.pcap
-        echo "--- $pcap" | tee -a exp >> rcv
-        sort -u "$type" >> exp
-        $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $pcap > packets
-        (grep "$expected" packets; grep "$exp_gw_ip_garp" packets) | sort -u >> rcv
-        echo | tee -a exp >> rcv
-
-        $at_diff exp rcv >/dev/null
-    }
-
-    OVS_WAIT_UNTIL([check_packets], [$at_diff -F'^---' exp rcv])
+    OVN_CHECK_PACKETS_CONTAIN([ext1/vif1-tx.pcap], [ext1-vif1.expected])
 
     if test $backup_vswitchd_dead != 1; then
         # Check for backup gw only if vswitchd is alive
@@ -12206,9 +12183,6 @@ OVN_WAIT_PATCH_PORT_FLOWS(["ln_port"], ["hv2"])
 
 # Wait for packets to be received.
 OVS_WAIT_UNTIL([test `wc -c < "hv1/snoopvif-tx.pcap"` -ge 100])
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
 $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap | trim_zeros > packets
 expected="fffffffffffff0000000000108060001080006040001f00000000001c0a80001000000000000c0a80001"
 echo $expected > expout
@@ -12240,21 +12214,12 @@ OVN_WAIT_PATCH_PORT_FLOWS(["ln_port"], ["hv3"])
 # Re-add nat-addresses option
 ovn-nbctl lsp-set-options lrp0-rp router-port=lrp0 nat-addresses="router"
 
-# Wait for packets to be received.
-OVS_WAIT_UNTIL([test `wc -c < "hv1/snoopvif-tx.pcap"` -ge 250])
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap | trim_zeros | sort | uniq > packets
 garp_1="fffffffffffff0000000000308060001080006040001f00000000003c0a80003000000000000c0a80003"
-echo $garp_1 > expout
+echo $garp_1 > expected_out
 garp_2="fffffffffffff0000000000408060001080006040001f00000000004c0a80004000000000000c0a80004"
-echo $garp_2 >> expout
+echo $garp_2 >> expected_out
 
-cat packets | grep $garp_1 | head -1 > exp
-cat packets | grep $garp_2 | head -1 >> exp
-AT_CHECK([cat exp], [0], [expout])
+OVN_CHECK_PACKETS_CONTAIN([hv1/snoopvif-tx.pcap], [expected_out], "trim_zeros")
 
 OVN_CLEANUP([hv1],[hv2],[hv3])
 
@@ -12577,6 +12542,7 @@ AT_CLEANUP
 
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([IPv6 ND Router Solicitation responder])
+AT_SKIP_IF([test $HAVE_SCAPY = no])
 AT_KEYWORDS([ovn-nd_ra])
 ovn_start
 
@@ -12643,76 +12609,120 @@ ovs-vsctl -- add-port br-int hv1-vif3 -- \
 wait_for_ports_up
 check ovn-nbctl --wait=hv sync
 
+n_resume=1
+
 # Make sure that ovn-controller has installed the corresponding OF Flow.
 OVS_WAIT_UNTIL([test 1 = `as hv1 ovs-ofctl dump-flows br-int | grep -c "ipv6_dst=ff02::2,nw_ttl=255,icmp_type=133,icmp_code=0"`])
 
 # This shell function sends a Router Solicitation packet.
-# test_ipv6_ra INPORT SRC_MAC SRC_LLA ADDR_MODE MTU RA_PREFIX_OPT RDNSS DNSSL ROUTE_INFO
+# test_ipv6_ra INPORT SRC_MAC SRC_LLA RA_OPT MTU PREFIX RDNSS DNSSL ROUTES
 test_ipv6_ra() {
-    local inport=$1 src_mac=$2 src_lla=$3 addr_mode=$4 mtu=$5 prefix_opt=$6
-    local rdnss=$7 dnssl=$8 route_info=$9
-    local request=333300000002${src_mac}86dd6000000000103aff${src_lla}ff02000000000000000000000000000285000efc000000000101${src_mac}
+    local inport=$1 src_mac=$2 src_ip=$3 ra=$4 mtu=$5 prefix=$6
+    local rdnss=$7 dnssl=$8 routes=$9
 
-    local len=24
-    local mtu_opt=""
-    if test $mtu != 0; then
-        len=`expr $len + 8`
-        mtu_opt=05010000${mtu}
+    local request=$(fmt_pkt "Ether(dst='33:33:00:00:00:02', src='${src_mac}')/ \
+                             IPv6(dst='ff02::2', src='${src_ip}')/ \
+                             ICMPv6ND_RS()/ \
+                             ICMPv6NDOptSrcLLAddr(lladdr='${src_mac}')")
+
+    local reply_dst=$src_ip
+    if test "${reply_dst}" = "::"; then
+        reply_dst="ff02::1"
     fi
 
-    if test ${#rdnss} != 0; then
-        len=`expr $len + ${#rdnss} / 2`
+    local rep_scapy="Ether(dst='${src_mac}', src='fa:16:3e:00:00:01')/ \
+                     IPv6(dst='${reply_dst}', src='fe80::f816:3eff:fe00:1')/ \
+                     ${ra}/ \
+                     ICMPv6NDOptSrcLLAddr(lladdr='fa:16:3e:00:00:01')"
+
+    if test "${mtu}" != "0"; then
+        rep_scapy="${rep_scapy}/ICMPv6NDOptMTU(mtu=${mtu})"
     fi
 
-    if test ${#dnssl} != 0; then
-        len=`expr $len + ${#dnssl} / 2`
+    if test -n "${rdnss}"; then
+        rep_scapy="${rep_scapy}/ICMPv6NDOptRDNSS(dns=[['${rdnss}']])"
     fi
 
-    if test ${#route_info} != 0; then
-        len=`expr $len + ${#route_info} / 2`
+    if test -n "${dnssl}"; then
+        rep_scapy="${rep_scapy}/ICMPv6NDOptDNSSL(searchlist=[['${dnssl}']])"
     fi
 
-    if test ${#prefix_opt} != 0; then
-        prefix_opt=${prefix_opt}fdad1234567800000000000000000000
-        len=`expr $len + ${#prefix_opt} / 2`
+    if test -n "${routes}"; then
+        rep_scapy="${rep_scapy}/${routes}"
     fi
 
-    len=$(printf "%x" $len)
-    local lrp_mac=fa163e000001
-    local lrp_lla=fe80000000000000f8163efffe000001
-    local reply=${src_mac}${lrp_mac}86dd6000000000${len}3aff${lrp_lla}${src_lla}8600XXXXff${addr_mode}ffff00000000000000000101${lrp_mac}${mtu_opt}${rdnss}${dnssl}${route_info}${prefix_opt}
+    if test -n "${prefix}"; then
+        local a_flag=$(echo $ra | grep -vc "M=1")
+        rep_scapy="${rep_scapy}/ICMPv6NDOptPrefixInfo(prefix='${prefix}', A=${a_flag})"
+    fi
+
+    local reply=$(fmt_pkt "${rep_scapy}")
     echo $reply >> $inport.expected
 
     as hv1 ovs-appctl netdev-dummy/receive hv1-vif${inport} $request
+
+    OVS_WAIT_UNTIL([test $n_resume = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
+    n_resume=$((n_resume + 1))
+}
+
+check_packets() {
+    local port=$1
+
+    # Skipping UDP checksum
+    OVN_CHECK_PACKETS([hv1/vif$port-tx.pcap], [$port.expected], ["cut -c 1-112,117-"])
+
+    rm $port.packets
+    rm $port.expected
+
+    reset_pcap_file hv1-vif1 hv1/vif1
+    reset_pcap_file hv1-vif2 hv1/vif2
+    reset_pcap_file hv1-vif3 hv1/vif3
+}
+
+prepare_ra_opt() {
+    local mode=$1 priority=$2
+
+    if test "${mode}" = "stateful"; then
+        echo "ICMPv6ND_RA(chlim=255, routerlifetime=65535, M=1, prf=${priority})"
+    elif test "${mode}" = "stateless"; then
+        echo "ICMPv6ND_RA(chlim=255, routerlifetime=65535, O=1, prf=${priority})"
+    else
+        echo "ICMPv6ND_RA(chlim=255, routerlifetime=65535, prf=${priority})"
+    fi
+}
+
+prepare_route_opt() {
+    local prefix=$1 priority=$2 plen=$3
+
+    local len=2
+    if test $plen -gt 64; then
+        len=3
+    fi
+
+    echo "ICMPv6NDOptRouteInfo(len=$len, prf=${priority}, prefix='${prefix}', plen=$plen)"
 }
 
 AT_CAPTURE_FILE([ofctl_monitor0.log])
 as hv1 ovs-ofctl monitor br-int resume --detach --no-chdir \
 --pidfile=ovs-ofctl0.pid 2> ofctl_monitor0.log
 
+prefix="fdad:1234:5678::"
+
 # MTU is not set and the address mode is set to slaac
-addr_mode=00
-default_prefix_option_config=030440c0ffffffffffffffff00000000
-src_mac=fa163e000002
-src_lla=fe80000000000000f8163efffe000002
-test_ipv6_ra 1 $src_mac $src_lla $addr_mode 0 $default_prefix_option_config
+ra=$(prepare_ra_opt "" 0)
+src_mac="fa:16:3e:00:00:02"
+src_lla="fe80::f816:3eff:fe00:2"
 
-# NXT_RESUME should be 1.
-OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
+test_ipv6_ra 1 $src_mac $src_lla "$ra" 0 $prefix "" "" ""
+check_packets 1
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap  > 1.packets
+# Check with RA with src being "::".
+ovn-nbctl --wait=hv lsp-set-port-security lp1 ""
 
-cat 1.expected | cut -c -112 > expout
-AT_CHECK([cat 1.packets | cut -c -112], [0], [expout])
+test_ipv6_ra 1 $src_mac "::" "$ra" 0 $prefix "" "" ""
+check_packets 1
 
-# Skipping the ICMPv6 checksum.
-cat 1.expected | cut -c 117- > expout
-AT_CHECK([cat 1.packets | cut -c 117-], [0], [expout])
-
-rm -f *.expected
-reset_pcap_file hv1-vif1 hv1/vif1
-reset_pcap_file hv1-vif2 hv1/vif2
-reset_pcap_file hv1-vif3 hv1/vif3
+ovn-nbctl --wait=hv lsp-set-port-security lp1 "fa:16:3e:00:00:02 10.0.0.12 fdad:1234:5678:0:f816:3eff:fe:2"
 
 # Set the MTU to 1500, send_periodic to false, preference to LOW
 ovn-nbctl --wait=hv set Logical_Router_Port lrp0 ipv6_ra_configs:mtu=1500
@@ -12726,33 +12736,14 @@ ovn-nbctl --wait=hv set Logical_Router_port lrp0 ipv6_ra_configs:route_info=HIGH
 OVS_WAIT_UNTIL([test 1 = `as hv1 ovs-ofctl dump-flows br-int | grep -c "ipv6_dst=ff02::2,nw_ttl=255,icmp_type=133,icmp_code=0"`])
 
 # addr_mode byte also includes router preference information
-addr_mode=18
-default_prefix_option_config=030440c0ffffffffffffffff00000000
-src_mac=fa163e000003
-src_lla=fe80000000000000f8163efffe000003
-mtu=000005dc
-rdnss=19030000ffffffff10000000000000000000000000000011
-dnssl=1f030000ffffffff02616102626202636300000000000000
-route_info=18023008ffffffff100100000000000018036018ffffffff10020000000000000000000000000000
-
-test_ipv6_ra 2 $src_mac $src_lla $addr_mode $mtu $default_prefix_option_config $rdnss $dnssl $route_info
-
-# NXT_RESUME should be 2.
-OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
-
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap  > 2.packets
-
-cat 2.expected | cut -c -112 > expout
-AT_CHECK([cat 2.packets | cut -c -112], [0], [expout])
-
-# Skipping the ICMPv6 checksum.
-cat 2.expected | cut -c 117- > expout
-AT_CHECK([cat 2.packets | cut -c 117-], [0], [expout])
+ra=$(prepare_ra_opt "" 3)
+routes=$(prepare_route_opt '1001::' 1 48)
+routes="${routes}/$(prepare_route_opt '1002::' 3 96)"
+src_mac="fa:16:3e:00:00:03"
+src_lla="fe80::f816:3eff:fe00:3"
 
-rm -f *.expected
-reset_pcap_file hv1-vif1 hv1/vif1
-reset_pcap_file hv1-vif2 hv1/vif2
-reset_pcap_file hv1-vif3 hv1/vif3
+test_ipv6_ra 2 $src_mac $src_lla "$ra" 1500 $prefix "1000::11" "aa.bb.cc" "$routes"
+check_packets 2
 
 # Set the address mode to dhcpv6_stateful, router_preference to HIGH
 ovn-nbctl --wait=hv set Logical_Router_Port lrp0 ipv6_ra_configs:address_mode=dhcpv6_stateful
@@ -12763,30 +12754,12 @@ ovn-nbctl --wait=hv remove Logical_Router_Port lrp0 ipv6_ra_configs route_info
 OVS_WAIT_UNTIL([test 1 = `as hv1 ovs-ofctl dump-flows br-int | grep -c "ipv6_dst=ff02::2,nw_ttl=255,icmp_type=133,icmp_code=0"`])
 
 # addr_mode byte also includes router preference information
-addr_mode=88
-default_prefix_option_config=03044080ffffffffffffffff00000000
-src_mac=fa163e000004
-src_lla=fe80000000000000f8163efffe000004
-mtu=000005dc
-
-test_ipv6_ra 3 $src_mac $src_lla $addr_mode $mtu $default_prefix_option_config "" $dnssl
-
-# NXT_RESUME should be 3.
-OVS_WAIT_UNTIL([test 3 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
-
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif3-tx.pcap  > 3.packets
-
-cat 3.expected | cut -c -112 > expout
-AT_CHECK([cat 3.packets | cut -c -112], [0], [expout])
-
-# Skipping the ICMPv6 checksum.
-cat 3.expected | cut -c 117- > expout
-AT_CHECK([cat 3.packets | cut -c 117-], [0], [expout])
+ra=$(prepare_ra_opt "stateful" 1)
+src_mac="fa:16:3e:00:00:04"
+src_lla="fe80::f816:3eff:fe00:4"
 
-rm -f *.expected
-reset_pcap_file hv1-vif1 hv1/vif1
-reset_pcap_file hv1-vif2 hv1/vif2
-reset_pcap_file hv1-vif3 hv1/vif3
+test_ipv6_ra 3 $src_mac $src_lla "$ra" 1500 $prefix "" "aa.bb.cc" ""
+check_packets 3
 
 # Set the address mode to dhcpv6_stateless, reset router preference to default
 ovn-nbctl --wait=hv set Logical_Router_Port lrp0 ipv6_ra_configs:address_mode=dhcpv6_stateless
@@ -12795,46 +12768,27 @@ ovn-nbctl --wait=hv remove Logical_Router_Port lrp0 ipv6_ra_configs dnssl
 # Make sure that ovn-controller has installed the corresponding OF Flow.
 OVS_WAIT_UNTIL([test 1 = `as hv1 ovs-ofctl dump-flows br-int | grep -c "ipv6_dst=ff02::2,nw_ttl=255,icmp_type=133,icmp_code=0"`])
 
-addr_mode=40
-default_prefix_option_config=030440c0ffffffffffffffff00000000
-src_mac=fa163e000002
-src_lla=fe80000000000000f8163efffe000002
-mtu=000005dc
-
-test_ipv6_ra 1 $src_mac $src_lla $addr_mode $mtu $default_prefix_option_config
-
-# NXT_RESUME should be 4.
-OVS_WAIT_UNTIL([test 4 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
-
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap  > 1.packets
+ra=$(prepare_ra_opt "stateless" 0)
+src_mac="fa:16:3e:00:00:02"
+src_lla="fe80::f816:3eff:fe00:2"
 
-cat 1.expected | cut -c -112 > expout
-AT_CHECK([cat 1.packets | cut -c -112], [0], [expout])
-
-# Skipping the ICMPv6 checksum.
-cat 1.expected | cut -c 117- > expout
-AT_CHECK([cat 1.packets | cut -c 117-], [0], [expout])
-
-rm -f *.expected
-reset_pcap_file hv1-vif1 hv1/vif1
-reset_pcap_file hv1-vif2 hv1/vif2
-reset_pcap_file hv1-vif3 hv1/vif3
+test_ipv6_ra 1 $src_mac $src_lla "$ra" 1500 $prefix "" "" ""
+check_packets 1
 
 # Set the address mode to invalid.
 ovn-nbctl --wait=hv set Logical_Router_Port lrp0 ipv6_ra_configs:address_mode=invalid
 # Make sure that ovn-controller has not installed any OF Flow for IPv6 ND RA.
 OVS_WAIT_UNTIL([test 0 = `as hv1 ovs-ofctl dump-flows br-int | grep -c "ipv6_dst=ff02::2,nw_ttl=255,icmp_type=133,icmp_code=0"`])
 
-addr_mode=40
-default_prefix_option_config=""
-src_mac=fa163e000002
-src_lla=fe80000000000000f8163efffe000002
-mtu=000005dc
+ra=$(prepare_ra_opt "stateless" 0)
 
-test_ipv6_ra 1 $src_mac $src_lla $addr_mode $mtu $default_prefix_option_config
+src_mac="fa:16:3e:00:00:02"
+src_lla="fe80::f816:3eff:fe00:2"
 
-# NXT_RESUME should be 4 only.
-OVS_WAIT_UNTIL([test 4 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
+# This shouldn't produce any NXT_RESUME.
+n_resume=$((n_resume - 1))
+
+test_ipv6_ra 1 $src_mac $src_lla "$ra" 1500 "" "" "" ""
 
 $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap  > 1.packets
 AT_CHECK([cat 1.packets], [0], [])
@@ -13879,33 +13833,15 @@ as hv1 reset_pcap_file snoopvif hv1/snoopvif
 # add nat-addresses option
 ovn-nbctl --wait=hv lsp-set-options lrp0-rp router-port=lrp0 nat-addresses="router"
 
-# Wait for packets to be received through hv2.
-OVS_WAIT_UNTIL([test `wc -c < "hv1/snoopvif-tx.pcap"` -ge 100])
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
 only_broadcast_from_lrp1() {
     grep "fffffffffffff00000000001"
 }
 
 garp="fffffffffffff0000000000108060001080006040001f00000000001c0a80064000000000000c0a80064"
-echo $garp > expout
+echo $garp > expected_out
 
-OVS_WAIT_UNTIL(
-    [$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap > rcv_text
-     exp_rcvd=$(cat rcv_text | grep $garp | wc -l)
-     echo "expected received = $exp_rcvd"
-     test $exp_rcvd -ge 1])
-
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap | trim_zeros | only_broadcast_from_lrp1 | uniq > hv1_snoop_tx
-echo "packets on hv1-snoopvif:"
-cat hv1_snoop_tx
-AT_CHECK([sort hv1_snoop_tx], [0], [expout])
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv2/br-phys_n1-tx.pcap | trim_zeros | only_broadcast_from_lrp1 | uniq > hv2_br_phys_tx
-echo "packets on hv2 br-phys tx"
-cat hv2_br_phys_tx
-AT_CHECK([grep $garp hv2_br_phys_tx | sort], [0], [expout])
+OVN_CHECK_PACKETS_CONTAIN([hv1/snoopvif-tx.pcap], [expected_out], "trim_zeros")
+OVN_CHECK_PACKETS_CONTAIN([hv2/br-phys_n1-tx.pcap], [expected_out], "trim_zeros")
 $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv3/br-phys_n1-tx.pcap | trim_zeros | only_broadcast_from_lrp1 | uniq > hv3_br_phys_tx
 echo "packets on hv3 br-phys tx"
 cat hv3_br_phys_tx
@@ -13914,9 +13850,6 @@ AT_CHECK([grep $garp hv3_br_phys_tx | sort], [0], [])
 
 # at this point, we invert the priority of the gw chassis between hv2 and hv3
 
-# Reset hv2/br-phys_n1 before inverting gw chassis, as otherwise packets might
-# be reset (while not resetting hv1/snoopvif because not yet sent).
-as hv2 reset_pcap_file br-phys_n1 hv2/br-phys_n1
 ovn-nbctl --wait=hv \
           --id=@gc0 create Gateway_Chassis \
                     name=outside_gw1 chassis_name=hv2 priority=1 -- \
@@ -13924,24 +13857,17 @@ ovn-nbctl --wait=hv \
                     name=outside_gw2 chassis_name=hv3 priority=10 -- \
           set Logical_Router_Port lrp0 'gateway_chassis=[@gc0,@gc1]'
 
+# We expect not to receive garp on hv2 after inverting the priority.
+# Hence  reset hv2 after inverting priority as otherwise a garp might
+# be received on hv2 between the reset and the priority change.
+
+as hv2 reset_pcap_file br-phys_n1 hv2/br-phys_n1
 as hv3 reset_pcap_file br-phys_n1 hv3/br-phys_n1
 as hv1 reset_pcap_file snoopvif hv1/snoopvif
 
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
-# Wait for packets to be received.
-OVS_WAIT_UNTIL(
-    [$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap > rcv_text
-     exp_rcvd=$(cat rcv_text | grep $garp | wc -l)
-     echo "expected received = $exp_rcvd"
-     test $exp_rcvd -ge 1])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap | trim_zeros | only_broadcast_from_lrp1 | uniq >  hv1_snoopvif_tx
-AT_CHECK([sort hv1_snoopvif_tx], [0], [expout])
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv3/br-phys_n1-tx.pcap | trim_zeros | only_broadcast_from_lrp1 | uniq > hv3_br_phys_tx
-AT_CHECK([grep $garp hv3_br_phys_tx | sort], [0], [expout])
+OVN_CHECK_PACKETS_CONTAIN([hv1/snoopvif-tx.pcap], [expected_out], "trim_zeros")
+OVN_CHECK_PACKETS_CONTAIN([hv3/br-phys_n1-tx.pcap], [expected_out], "trim_zeros")
 $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv2/br-phys_n1-tx.pcap | trim_zeros | only_broadcast_from_lrp1 | uniq > hv2_br_phys_tx
 AT_CHECK([grep $garp hv2_br_phys_tx | sort], [0], [])
 
@@ -13974,25 +13900,11 @@ ovn-nbctl --wait=hv lsp-set-options lrp0-rp router-port=lrp0 nat-addresses="rout
 OVS_WAIT_UNTIL([test 1 = `ovn-sbctl --bare --columns nat_addresses find port_binding \
 logical_port=lrp0-rp | grep is_chassis | wc -l`])
 
-# Wait for packets to be received.
-OVS_WAIT_UNTIL([test `wc -c < "hv1/snoopvif-tx.pcap"` -ge 100])
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
 garp="fffffffffffff00000000001810007de08060001080006040001f00000000001c0a80064000000000000c0a80064"
-echo $garp > expout
+echo $garp > expected_out
 
-OVS_WAIT_UNTIL(
-    [$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap > rcv_text
-     exp_rcvd=$(cat rcv_text | grep $garp | wc -l)
-     echo "expected received = $exp_rcvd"
-     test $exp_rcvd -ge 1])
-
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/snoopvif-tx.pcap | trim_zeros | only_broadcast_from_lrp1 | uniq >  hv1_snoopvif_tx
-AT_CHECK([sort hv1_snoopvif_tx], [0], [expout])
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv3/br-phys_n1-tx.pcap | trim_zeros | only_broadcast_from_lrp1 | uniq > hv3_br_phys_tx
-AT_CHECK([grep $garp hv3_br_phys_tx | sort], [0], [expout])
+OVN_CHECK_PACKETS_CONTAIN([hv1/snoopvif-tx.pcap], [expected_out], "trim_zeros")
+OVN_CHECK_PACKETS_CONTAIN([hv3/br-phys_n1-tx.pcap], [expected_out], "trim_zeros")
 $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv2/br-phys_n1-tx.pcap | trim_zeros | only_broadcast_from_lrp1 | uniq > hv2_br_phys_tx
 AT_CHECK([grep $garp hv2_br_phys_tx | sort], [0], [])
 
@@ -14333,10 +14245,6 @@ wc -l], [0], [4
 chassis_uuid=$(fetch_column Chassis _uuid name=hv1)
 wait_row_count Port_Binding 1 logical_port=cr-ip6_public chassis=$chassis_uuid
 
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
 # Test the IPv6 Neighbor Solicitation (NS) - nd_ns action for unknown MAC
 # addresses. ovn-controller should generate an IPv6 NS request for IPv6
 # packets whose MAC is unknown (in the ARP_REQUEST router pipeline stage.
@@ -14888,6 +14796,10 @@ wait_column "$hv2_uuid" Port_Binding requested_additional_chassis logical_port=m
 # ovn-installed on hv2 should guarantee that.
 OVS_WAIT_UNTIL([test `as hv2 ovs-vsctl get Interface migrator external_ids:ovn-installed` = '"true"'])
 
+# Still, this does not guarantee that all flows are installed on hv3: hv3 (might) still need to receive and handle
+# additional_chassis for migrator port
+ovn-nbctl --wait=hv sync
+
 # check that...
 # unicast from First arrives to hv1:Migrator
 # unicast from First arrives to hv2:Migrator
@@ -14974,6 +14886,9 @@ wait_column "" Port_Binding requested_additional_chassis logical_port=migrator
 # For instance, migrator port might still be up from prior to complete migration to hv2
 OVS_WAIT_UNTIL([test `as hv2 ovs-vsctl get Interface migrator external_ids:ovn-installed` = '"true"'])
 
+# Give time for hv3 to handle the change of Port_Binding  for migrator port
+ovn-nbctl --wait=hv sync
+
 # check that...
 # unicast from Third doesn't arrive to hv1:Migrator
 # unicast from Third arrives to hv2:Migrator
@@ -16188,7 +16103,7 @@ echo "verifying that lsp0 binding moves when requested-chassis is changed"
 
 ovn-nbctl lsp-set-options lsp0 requested-chassis=hv2
 # We might see multiple "Releasing lport ...", when sb is read only
-OVS_WAIT_UNTIL([test 1 -le $(grep -c "Releasing lport lsp0 from this chassis" hv1/ovn-controller.log)])
+OVS_WAIT_UNTIL([test 1 -le $(grep -c "Releasing lport lsp0" hv1/ovn-controller.log)])
 
 wait_column "$hv2_uuid" Port_Binding chassis logical_port=lsp0
 
@@ -16276,7 +16191,7 @@ AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=0 | grep in_port=1], [0], [ig
 AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=65 | grep actions=output:1], [0], [ignore])
 
 check ovn-nbctl --wait=hv lsp-set-options lsp0 requested-chassis=non-existant-chassis
-OVS_WAIT_UNTIL([test 1 -le $(grep -c "Releasing lport lsp0 from this chassis" hv1/ovn-controller.log)])
+OVS_WAIT_UNTIL([test 1 -le $(grep -c "Releasing lport lsp0" hv1/ovn-controller.log)])
 check ovn-nbctl --wait=hv sync
 wait_column '' Port_Binding chasssi logical_port=lsp0
 AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=0 | grep in_port=1], [1], [])
@@ -18989,6 +18904,46 @@ for sf in 0 1; do
     done
 done
 
+check_packets() {
+    n_allowed=$1
+    > expected
+    > received
+    for i in 1 2 3; do
+        echo "--- hv$i vif${i}1" | tee -a expected >> received
+        sort ${i}1.expected >> expected
+        $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv$i/vif${i}1-tx.pcap | sort >> received
+        echo | tee -a expected >> received
+    done
+
+    # need to verify the log for ACL hit as well, since in the allow case
+    # (unlike the drop case) it is tricky to pass just with the expected;
+    # since with the stateful rule the packet will still get by (default
+    # rule) even if it doesn't hit the allow rule.
+    # The hit count for the ACL is 6 (1 unicast + 2 non-unicast) * 2
+    # (with/without stateful rule) for hv1 and hv2, each.
+    cat >>expected <<EOF
+--- acl logging
+hv1_drop hit 6
+hv2_drop hit 6
+hv1_allow hit $n_allowed
+hv2_allow hit $n_allowed
+EOF
+
+cat >>received <<EOF
+--- acl logging
+hv1_drop hit `grep -c 'acl_log.*|INFO|name="drop-acl"' hv1/ovn-controller.log`
+hv2_drop hit `grep -c 'acl_log.*|INFO|name="drop-acl"' hv2/ovn-controller.log`
+hv1_allow hit `grep -c 'acl_log.*|INFO|name="allow-acl"' hv1/ovn-controller.log`
+hv2_allow hit `grep -c 'acl_log.*|INFO|name="allow-acl"' hv2/ovn-controller.log`
+EOF
+
+    $at_diff expected received >/dev/null
+}
+
+# We need to wait and check here that packets are received as they should as otherwise packets
+# which were just sent might by handled after setting next ACL (allow) rules.
+OVS_WAIT_UNTIL([check_packets 0], [$at_diff -F'^---' expected received])
+
 # Test allow rule
 #----------------
 ovn-nbctl acl-del lsw0
@@ -19037,41 +18992,7 @@ as hv3 ovs-ofctl -O OpenFlow13 dump-flows br-int > offlows3
 # Now check the packets actually received against the ones expected.
 AT_CAPTURE_FILE([expected])
 AT_CAPTURE_FILE([received])
-check_packets() {
-    > expected
-    > received
-    for i in 1 2 3; do
-        echo "--- hv$i vif${i}1" | tee -a expected >> received
-        sort ${i}1.expected >> expected
-        $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv$i/vif${i}1-tx.pcap | sort >> received
-        echo | tee -a expected >> received
-    done
-
-    # need to verify the log for ACL hit as well, since in the allow case
-    # (unlike the drop case) it is tricky to pass just with the expected;
-    # since with the stateful rule the packet will still get by (default
-    # rule) even if it doesn't hit the allow rule.
-    # The hit count for the ACL is 6 (1 unicast + 2 non-unicast) * 2
-    # (with/without stateful rule) for hv1 and hv2, each.
-    cat >>expected <<EOF
---- acl logging
-hv1_drop hit 6
-hv2_drop hit 6
-hv1_allow hit 6
-hv2_allow hit 6
-EOF
-
-cat >>received <<EOF
---- acl logging
-hv1_drop hit `grep -c 'acl_log.*|INFO|name="drop-acl"' hv1/ovn-controller.log`
-hv2_drop hit `grep -c 'acl_log.*|INFO|name="drop-acl"' hv2/ovn-controller.log`
-hv1_allow hit `grep -c 'acl_log.*|INFO|name="allow-acl"' hv1/ovn-controller.log`
-hv2_allow hit `grep -c 'acl_log.*|INFO|name="allow-acl"' hv2/ovn-controller.log`
-EOF
-
-    $at_diff expected received >/dev/null
-}
-OVS_WAIT_UNTIL([check_packets], [$at_diff -F'^---' expected received])
+OVS_WAIT_UNTIL([check_packets 6], [$at_diff -F'^---' expected received])
 
 OVN_CLEANUP([hv1],[hv2],[hv3])
 
@@ -19790,11 +19711,6 @@ test_dhcp() {
     as hv1 ovs-appctl netdev-dummy/receive hv${inport}-ext${inport} $request
 }
 
-
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
 # This shell function sends a DHCPv6 request packet
 # test_dhcpv6 INPORT SRC_MAC SRC_LLA DHCPv6_MSG_TYPE OFFER_IP OUTPORT...
 # The OUTPORTs (zero or more) list the VIFs on which the original DHCPv6
@@ -19876,12 +19792,9 @@ OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
 # NXT_RESUMEs should be 0 in hv2.
 OVS_WAIT_UNTIL([test 0 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap > ext1_v4.packets
-cat ext1_v4.expected | cut -c -48 > expout
-AT_CHECK([cat ext1_v4.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v4.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat ext1_v4.expected | cut -c 53- > expout
-AT_CHECK([cat ext1_v4.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v4.expected], ["cut -c 53-"])
 
 # ovs-ofctl also resumes the packets and this causes other ports to receive
 # the DHCP request packet. So reset the pcap files so that its easier to test.
@@ -19903,13 +19816,9 @@ OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
 # NXT_RESUMEs should be 0 in hv2.
 OVS_WAIT_UNTIL([test 0 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap | \
-sort > ext1_v6.packets
-cat ext1_v6.expected | cut -c -120 > expout
-AT_CHECK([cat ext1_v6.packets | cut -c -120], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v6.expected], ["cut -c -120"])
 # Skipping the UDP checksum
-cat ext1_v6.expected | cut -c 125- > expout
-AT_CHECK([cat ext1_v6.packets | cut -c 125-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v6.expected], ["cut -c 125-"])
 
 rm -f ext1_v6.expected
 rm -f ext1_v6.packets
@@ -19969,12 +19878,9 @@ OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
 # NXT_RESUMEs should be 1 in hv2.
 OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap > ext1_v4.packets
-cat ext1_v4.expected | cut -c -48 > expout
-AT_CHECK([cat ext1_v4.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v4.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat ext1_v4.expected | cut -c 53- > expout
-AT_CHECK([cat ext1_v4.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v4.expected], ["cut -c 53-"])
 
 # ovs-ofctl also resumes the packets and this causes other ports to receive
 # the DHCP request packet. So reset the pcap files so that its easier to test.
@@ -19995,13 +19901,9 @@ OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
 # NXT_RESUMEs should be 2 in hv2.
 OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap | \
-sort > ext1_v6.packets
-cat ext1_v6.expected | cut -c -120 > expout
-AT_CHECK([cat ext1_v6.packets | cut -c -120], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v6.expected], ["cut -c -120"])
 # Skipping the UDP checksum
-cat ext1_v6.expected | cut -c 125- > expout
-AT_CHECK([cat ext1_v6.packets | cut -c 125-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v6.expected], ["cut -c 125-"])
 
 rm -f ext1_v6.expected
 rm -f ext1_v6.packets
@@ -20077,12 +19979,9 @@ OVS_WAIT_UNTIL([test 3 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
 # NXT_RESUMEs should be 2 in hv2.
 OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap > ext1_v4.packets
-cat ext1_v4.expected | cut -c -48 > expout
-AT_CHECK([cat ext1_v4.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v4.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat ext1_v4.expected | cut -c 53- > expout
-AT_CHECK([cat ext1_v4.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v4.expected], ["cut -c 53-"])
 
 # ovs-ofctl also resumes the packets and this causes other ports to receive
 # the DHCP request packet. So reset the pcap files so that its easier to test.
@@ -20104,13 +20003,9 @@ OVS_WAIT_UNTIL([test 4 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
 # NXT_RESUMEs should be 2 in hv2.
 OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap | \
-sort > ext1_v6.packets
-cat ext1_v6.expected | cut -c -120 > expout
-AT_CHECK([cat ext1_v6.packets | cut -c -120], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v6.expected], ["cut -c -120"])
 # Skipping the UDP checksum
-cat ext1_v6.expected | cut -c 125- > expout
-AT_CHECK([cat ext1_v6.packets | cut -c 125-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v6.expected], ["cut -c 125-"])
 
 rm -f ext1_v6.expected
 rm -f ext1_v6.packets
@@ -20158,12 +20053,9 @@ OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
 # NXT_RESUMEs should be 1 in hv3.
 OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor0_hv3.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap > ext1_v4.packets
-cat ext1_v4.expected | cut -c -48 > expout
-AT_CHECK([cat ext1_v4.packets | cut -c -48], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v4.expected], ["cut -c -48"])
 # Skipping the IPv4 checksum.
-cat ext1_v4.expected | cut -c 53- > expout
-AT_CHECK([cat ext1_v4.packets | cut -c 53-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v4.expected], ["cut -c 53-"])
 
 # ovs-ofctl also resumes the packets and this causes other ports to receive
 # the DHCP request packet. So reset the pcap files so that its easier to test.
@@ -20188,13 +20080,9 @@ OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
 # NXT_RESUMEs should be 2 in hv3.
 OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv3.log | grep -c NXT_RESUME`])
 
-$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap | \
-sort > ext1_v6.packets
-cat ext1_v6.expected | cut -c -120 > expout
-AT_CHECK([cat ext1_v6.packets | cut -c -120], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v6.expected], ["cut -c -120"])
 # Skipping the UDP checksum
-cat ext1_v6.expected | cut -c 125- > expout
-AT_CHECK([cat ext1_v6.packets | cut -c 125-], [0], [expout])
+OVN_CHECK_PACKETS([hv1/ext1-tx.pcap], [ext1_v6.expected], ["cut -c 125-"])
 
 # disconnect hv3 from the network, hv1 should take over
 as hv3
@@ -20568,12 +20456,12 @@ test_ip_packet_larger() {
         expected=${expected}0000000000000000000000000000
         echo $expected > br_phys_n1.expected
     else
-        src_ip=`ip_to_hex 10 0 0 1`
+        src_ip=`ip_to_hex 172.168.0.100`
         dst_ip=`ip_to_hex 10 0 0 3`
         # pkt len should be 146 (28 (icmp packet) + 118 (orig ip + payload))
         reply_pkt_len=008e
         ip_csum=fc97
-        icmp_reply=${src_mac}${dst_mac}08004500${reply_pkt_len}00004000fe01686b
+        icmp_reply=${src_mac}${dst_mac}08004500${reply_pkt_len}00004000fe01c55f
         icmp_reply=${icmp_reply}${src_ip}${dst_ip}0304${ip_csum}0000$(printf "%04x" $mtu)
         icmp_reply=${icmp_reply}4500${pkt_len}000000003f01c4dd
         icmp_reply=${icmp_reply}${orig_packet_l3}
@@ -20665,7 +20553,7 @@ test_ip6_packet_larger() {
 
     local ipv6_src=10000000000000000000000000000003
     local ipv6_dst=20000000000000000000000000000002
-    local ipv6_rt=10000000000000000000000000000001
+    local ipv6_rt=20000000000000000000000000000001
 
     local payload=0000000000000000000000000000000000000000
     local payload=${payload}0000000000000000000000000000000000000000
@@ -21223,6 +21111,7 @@ OVN_FOR_EACH_NORTHD([
 AT_SETUP([ipam to non-ipam])
 ovn_start
 
+check ovn-nbctl --wait=sb sync
 ovn-nbctl --wait=hv set NB_Global . options:mac_prefix="0a:00:00:00:00:00"
 ovn-nbctl ls-add sw0
 ovn-nbctl lsp-add sw0 p0 -- lsp-set-addresses p0 dynamic
@@ -23426,7 +23315,7 @@ check ovn-nbctl set Logical_Switch sw2 \
     other_config:mcast_querier="false"
 
 # Enable multicast snooping on sw3.
-check ovn-nbctl --wait=sb set Logical_Switch sw3       \
+check ovn-nbctl --wait=hv set Logical_Switch sw3       \
     other_config:mcast_querier="false" \
     other_config:mcast_snoop="true"
 
@@ -23465,7 +23354,7 @@ OVS_WAIT_UNTIL(
   [$at_diff -F'^---' exp rcv])
 
 # Enable multicast relay on rtr
-check ovn-nbctl --wait=sb set logical_router rtr options:mcast_relay="true"
+check ovn-nbctl --wait=hv set logical_router rtr options:mcast_relay="true"
 
 AT_CAPTURE_FILE([sbflows6])
 ovn-sbctl dump-flows > sbflows6
@@ -26413,10 +26302,13 @@ check ovn-nbctl lsp-add ts ts-lr3 \
 wait_for_ports_up
 
 ovn_as az1
+OVS_WAIT_UNTIL([ovn-nbctl show | grep ts-lr2])
 check ovn-nbctl lsp-set-options ts-lr2 requested-chassis=hv2
+OVS_WAIT_UNTIL([ovn-nbctl show | grep ts-lr3])
 check ovn-nbctl lsp-set-options ts-lr3 requested-chassis=hv2
 
 ovn_as az2
+OVS_WAIT_UNTIL([ovn-nbctl show | grep ts-lr1])
 check ovn-nbctl lsp-set-options ts-lr1 requested-chassis=hv1
 
 dnl Enable unregistered IP multicast flooding and IP multicast relay.
@@ -26625,10 +26517,13 @@ check ovn-nbctl lsp-add ts ts-lr3 \
 wait_for_ports_up
 
 ovn_as az1
+OVS_WAIT_UNTIL([ovn-nbctl show | grep ts-lr2])
 check ovn-nbctl lsp-set-options ts-lr2 requested-chassis=hv2
+OVS_WAIT_UNTIL([ovn-nbctl show | grep ts-lr3])
 check ovn-nbctl lsp-set-options ts-lr3 requested-chassis=hv2
 
 ovn_as az2
+OVS_WAIT_UNTIL([ovn-nbctl show | grep ts-lr1])
 check ovn-nbctl lsp-set-options ts-lr1 requested-chassis=hv1
 
 dnl Enable IP multicast snooping and IP multicast relay.  Reports are
@@ -27984,8 +27879,9 @@ ovn-nbctl ls-add ls1
 ovn-nbctl --wait=sb lsp-add ls1 lsp1
 
 # Simulate the fact that lsp1 had been previously bound on hv1.
-ovn-sbctl --id=@e create encap chassis_name=hv1 ip="192.168.0.1" type="geneve" \
-    -- --id=@c create chassis name=hv1 encaps=@e \
+ovn-sbctl --id=@e1 create encap chassis_name=hv1 ip="192.168.0.1" type="geneve" \
+    --id=@e2 create encap chassis_name=hv1 ip="192.168.0.1" type="vxlan" \
+    -- --id=@c create chassis name=hv1 encaps=@e1,@e2 \
     -- set Port_Binding lsp1 chassis=@c
 
 as hv1
@@ -28011,8 +27907,9 @@ ovn-nbctl ls-add ls1
 ovn-nbctl --wait=sb lsp-add ls1 lsp1
 
 # Simulate the fact that lsp1 had been previously bound on hv1.
-ovn-sbctl --id=@e create encap chassis_name=hv1 ip="192.168.0.1" type="geneve" \
-    -- --id=@c create chassis hostname=hv1 name=hv1 encaps=@e \
+ovn-sbctl --id=@e1 create encap chassis_name=hv1 ip="192.168.0.1" type="geneve" \
+    --id=@e2 create encap chassis_name=hv1 ip="192.168.0.1" type="vxlan" \
+    -- --id=@c create chassis name=hv1 encaps=@e1,@e2 \
     -- set Port_Binding lsp1 chassis=@c
 
 as hv1
@@ -28137,9 +28034,10 @@ OVS_WAIT_UNTIL([
 ])
 
 as hv1 check ovs-ofctl del-flows br-phys
+
 AT_DATA([flows.txt], [dnl
-table=0, priority=0 actions=NORMAL
 table=0, priority=200 arp,actions=drop
+table=0, priority=0 actions=NORMAL
 table=0, priority=100, pkt_mark=0x64 actions=drop
 table=0, priority=100, pkt_mark=0x2 actions=drop
 table=0, priority=100, pkt_mark=0x3 actions=drop
@@ -30785,7 +30683,6 @@ check ovn-nbctl --wait=hv ls-lb-del sw0 lb-ipv6
 # original destination tuple.
 #
 # ovn-controller should fall back to matching on ct_nw_dst()/ct_tp_dst().
-as northd-backup ovn-appctl -t NORTHD_TYPE pause
 as northd ovn-appctl -t NORTHD_TYPE pause
 
 check ovn-sbctl \
@@ -30836,7 +30733,6 @@ OVS_WAIT_FOR_OUTPUT([as hv2 ovs-ofctl dump-flows br-int table=70 | ofctl_strip_a
 
 # Resume ovn-northd.
 as northd ovn-appctl -t NORTHD_TYPE resume
-as northd-backup ovn-appctl -t NORTHD_TYPE resume
 check ovn-nbctl --wait=hv sync
 
 as hv2 ovs-vsctl del-port hv2-vif1
@@ -30957,9 +30853,6 @@ AT_CHECK([grep -c $northd_version hv1/ovn-controller.log], [0], [1
 as northd
 OVS_APP_EXIT_AND_WAIT([NORTHD_TYPE])
 
-as northd-backup
-OVS_APP_EXIT_AND_WAIT([NORTHD_TYPE])
-
 check ovn-sbctl set SB_Global . options:northd_internal_version=foo
 
 as hv1
@@ -31101,7 +30994,6 @@ AS_BOX([ovn-controller should not reset Port_Binding.up without northd])
 # Pause northd and clear the "up" field to simulate older ovn-northd
 # versions writing to the Southbound DB.
 as northd ovn-appctl -t NORTHD_TYPE pause
-as northd-backup ovn-appctl -t NORTHD_TYPE pause
 
 as hv1 ovn-appctl -t ovn-controller debug/pause
 check ovn-sbctl clear Port_Binding lsp1 up
@@ -31117,7 +31009,6 @@ check_column "" Port_Binding up logical_port=lsp1
 # Once northd should explicitly set the Port_Binding.up field to 'false' and
 # ovn-controller sets it to 'true' as soon as the update is processed.
 as northd ovn-appctl -t NORTHD_TYPE resume
-as northd-backup ovn-appctl -t NORTHD_TYPE resume
 wait_column "true" Port_Binding up logical_port=lsp1
 wait_column "true" nb:Logical_Switch_Port up name=lsp1
 
@@ -31272,7 +31163,6 @@ check ovn-nbctl lsp-set-addresses sw0-p4 "00:00:00:00:00:04 192.168.47.4"
 # will be sent in one transaction.
 
 check as northd ovn-appctl -t NORTHD_TYPE pause
-check as northd-backup ovn-appctl -t NORTHD_TYPE pause
 
 check ovn-nbctl lsp-add sw0 sw0-p1
 check ovn-nbctl lsp-set-addresses sw0-p1 "00:00:00:00:00:01 192.168.47.1"
@@ -31453,10 +31343,6 @@ send_icmp_packet() {
     as hv$hv ovs-appctl netdev-dummy/receive hv$hv-vif$inport $packet
 }
 
-trim_zeros() {
-    sed 's/\(00\)\{1,\}$//'
-}
-
 AS_BOX([Wait for all ports to be up])
 wait_for_ports_up
 
@@ -33727,7 +33613,6 @@ check as hv1 ovn-appctl -t ovn-controller exit
 
 # Pause northd to guarantee that ovn-controller starts before requested_chassis
 # column is filled.
-check as northd-backup ovn-appctl -t ovn-northd pause
 check as northd ovn-appctl -t ovn-northd pause
 
 # Wait until requested_chassis is empty
@@ -33741,7 +33626,6 @@ wait_column "hv1" Chassis name
 
 # Start northd and wait for events to be processed
 check as northd ovn-appctl -t ovn-northd resume
-check as northd-backup ovn-appctl -t ovn-northd resume
 
 wait_for_ports_up lsp1
 
@@ -34471,7 +34355,7 @@ dnat_zone=$(ovs-ofctl dump-flows br-int table=$DNAT_TABLE,metadata=0x${lr0_dp_ke
 if test -n "$dnat_zone"; then
   dnat_zone=${dnat_zone::-1}
 fi
-snat_zone=$(ovs-ofctl dump-flows br-int table=$SNAT_TABLE,metadata=0x${lr0_dp_key} | grep priority=153 | grep -o zone=.*, | cut -d '=' -f 2)
+snat_zone=$(ovs-ofctl dump-flows br-int table=$SNAT_TABLE,metadata=0x${lr0_dp_key} | grep priority=153 | grep ct_state=-trk | grep -o zone=.*, | cut -d '=' -f 2)
 if test -n "$snat_zone"; then
   snat_zone=${snat_zone::-1}
 fi
@@ -34488,7 +34372,7 @@ dnat_zone=$(ovs-ofctl dump-flows br-int table=$DNAT_TABLE,metadata=0x${lr0_dp_ke
 if test -n "$dnat_zone"; then
   dnat_zone=${dnat_zone::-1}
 fi
-snat_zone=$(ovs-ofctl dump-flows br-int table=$SNAT_TABLE,metadata=0x${lr0_dp_key} | grep priority=153 | grep -o zone=.*, | cut -d '=' -f 2)
+snat_zone=$(ovs-ofctl dump-flows br-int table=$SNAT_TABLE,metadata=0x${lr0_dp_key} | grep priority=153 | grep ct_state=-trk | grep -o zone=.*, | cut -d '=' -f 2)
 if test -n "$snat_zone"; then
   snat_zone=${snat_zone::-1}
 fi
@@ -34572,6 +34456,7 @@ AT_CLEANUP
 
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([MAC binding aging])
+AT_SKIP_IF([test $HAVE_SCAPY = no])
 ovn_start
 
 net_add n1
@@ -35389,37 +35274,6 @@ OVN_CLEANUP([hv1],[hv2])
 AT_CLEANUP
 ])
 
-OVN_FOR_EACH_NORTHD([
-AT_SETUP([feature inactivity probe])
-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
-
-dnl Ensure that there are 4 openflow connections.
-OVS_WAIT_UNTIL([test "$(grep -c 'negotiated OpenFlow version' hv1/ovs-vswitchd.log)" -eq "4"])
-
-dnl "Wait" 3 times 60 seconds and ensure ovn-controller writes to the
-dnl openflow connections in the meantime.  This should allow ovs-vswitchd
-dnl to probe the openflow connections at least twice.
-
-as hv1 ovs-appctl time/warp 60000
-check ovn-nbctl --wait=hv sync
-
-as hv1 ovs-appctl time/warp 60000
-check ovn-nbctl --wait=hv sync
-
-as hv1 ovs-appctl time/warp 60000
-check ovn-nbctl --wait=hv sync
-
-AT_CHECK([test -z "`grep disconnecting hv1/ovs-vswitchd.log`"])
-OVN_CLEANUP([hv1])
-AT_CLEANUP
-])
-
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([Logical flows with Chassis_Template_Var references])
 AT_KEYWORDS([templates])
@@ -35795,7 +35649,7 @@ as hv1 ovs-vsctl set-ssl \
 echo hv3 > ${OVN_SYSCONFDIR}/test_hv
 
 # the last argument is passed to ovn-controller through cli
-ovn_attach n1 br-phys 192.168.0.1 24 vxlan hv1 -n hv3
+ovn_attach n1 br-phys 192.168.0.1 24 vxlan hv1 hv3
 
 sim_add hv2
 as hv2
@@ -36728,7 +36582,6 @@ check ovn-nbctl lsp-add ls2 public2
 check ovn-nbctl lsp-set-addresses public2 unknown
 check ovn-nbctl lsp-set-type public2 localnet
 check ovn-nbctl --wait=sb set Logical_Switch_Port public2 options:qos_min_rate=6000000000 options:qos_max_rate=7000000000 options:qos_burst=8000000000 options:network_name=phys
-check ovn-nbctl --wait=sb lsp-set-options public2 qos_min_rate=6000000000 qos_max_rate=7000000000 qos_burst=8000000000
 
 # Let's now send ovn controller to sleep, so it will receive both ofport notification and ls deletion simultaneously
 sleep_controller hv-1
@@ -36861,14 +36714,14 @@ check ovn-nbctl ls-add sw0
 check ovn-nbctl lsp-add sw0 sw0-port1
 check ovn-nbctl lsp-set-addresses sw0-port1 "50:54:00:00:00:01 192.168.0.2"
 
+ovs-vsctl add-port br-int p1 -- \
+    set Interface p1 external_ids:iface-id=sw0-port1
+
 check ovn-nbctl lsp-add sw0 sw0-lr0
 check ovn-nbctl lsp-set-type sw0-lr0 router
 check ovn-nbctl lsp-set-addresses sw0-lr0 00:00:00:00:ff:01
 check ovn-nbctl lsp-set-options sw0-lr0 router-port=lr0-sw0
 
-ovs-vsctl add-port br-int p1 -- \
-    set Interface p1 external_ids:iface-id=sw0-port1
-
 check ovn-appctl -t ovn-controller vlog/set dbg:main
 
 wait_for_ports_up
@@ -37005,3 +36858,548 @@ AT_CHECK([grep -c "NXT_CT_FLUSH_ZONE" hv1/ovs-vswitchd.log], [0], [dnl
 OVN_CLEANUP([hv1])
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([virtual port claim race condition])
+AT_KEYWORDS([virtual ports])
+ovn_start
+
+send_garp() {
+    local hv=$1 inport=$2 eth_src=$3 eth_dst=$4 spa=$5 tpa=$6
+    local request=${eth_dst}${eth_src}08060001080006040001${eth_src}${spa}${eth_dst}${tpa}
+    as hv$hv ovs-appctl netdev-dummy/receive hv${hv}-vif$inport $request
+}
+
+net_add n1
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovn-appctl vlog/set dbg
+ovs-vsctl -- add-port br-int hv1-vif1 -- \
+    set interface hv1-vif1 external-ids:iface-id=sw0-p1 \
+    options:tx_pcap=hv1/vif1-tx.pcap \
+    options:rxq_pcap=hv1/vif1-rx.pcap \
+    ofport-request=1
+ovs-vsctl -- add-port br-int hv1-vif3 -- \
+    set interface hv1-vif3 \
+    options:tx_pcap=hv1/vif3-tx.pcap \
+    options:rxq_pcap=hv1/vif3-rx.pcap \
+    ofport-request=3
+
+ovs-appctl -t ovn-controller vlog/set dbg
+
+ovn-nbctl ls-add sw0
+
+check ovn-nbctl lsp-add sw0 sw0-vir
+check ovn-nbctl lsp-set-addresses sw0-vir "50:54:00:00:00:10 10.0.0.10"
+check ovn-nbctl lsp-set-port-security sw0-vir "50:54:00:00:00:10 10.0.0.10"
+check ovn-nbctl lsp-set-type sw0-vir virtual
+check ovn-nbctl set logical_switch_port sw0-vir options:virtual-ip=10.0.0.10
+check ovn-nbctl set logical_switch_port sw0-vir options:virtual-parents=sw0-p1
+
+check ovn-nbctl lsp-add sw0 sw0-p1
+check ovn-nbctl lsp-set-addresses sw0-p1 "50:54:00:00:00:03 10.0.0.3 1000::3"
+check ovn-nbctl lsp-set-port-security sw0-p1 "50:54:00:00:00:03 10.0.0.3 10.0.0.10 1000::3"
+
+# Create a logical router and attach both logical switches
+check ovn-nbctl lr-add lr0
+check ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24 1000::a/64
+check ovn-nbctl lsp-add sw0 sw0-lr0
+check ovn-nbctl lsp-set-type sw0-lr0 router
+check ovn-nbctl lsp-set-addresses sw0-lr0 00:00:00:00:ff:01
+check ovn-nbctl lsp-set-options sw0-lr0 router-port=lr0-sw0
+
+wait_for_ports_up
+ovn-nbctl --wait=hv sync
+hv1_ch_uuid=`ovn-sbctl --bare --columns _uuid find chassis name="hv1"`
+
+# Try to bind sw0-vir directly to an OVS port. This should be ignored by
+# ovn-controller.
+as hv1 ovs-vsctl set interface hv1-vif3 external-ids:iface-id=sw0-vir
+
+# Make sb to sleep, so that claim of sw0-vir (through pinctrl) and hv1-vif3 can be handled within same idl by controller
+sleep_sb
+
+# From sw0-p0 send GARP for 10.0.0.10. hv1 should claim sw0-vir
+# and sw0-p1 should be its virtual_parent.
+eth_src=505400000003
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 10)
+tpa=$(ip_to_hex 10 0 0 10)
+send_garp 1 1 $eth_src $eth_dst $spa $tpa
+
+OVS_WAIT_UNTIL([test 1 = `cat hv1/ovn-controller.log | grep "pinctrl received  packet-in" | \
+grep opcode=BIND_VPORT | grep OF_Table_ID=29 | wc -l`])
+
+sleep_controller hv1
+
+# Cleanup hv1-vif3. This should not interfere with sw0-vir claim
+as hv1 ovs-vsctl del-port hv1-vif3
+
+wake_up_sb
+ovn-nbctl --wait=sb sync
+wake_up_controller hv1
+check ovn-nbctl --wait=hv sync
+
+wait_row_count Port_Binding 1 logical_port=sw0-vir chassis=$hv1_ch_uuid
+check_row_count Port_Binding 1 logical_port=sw0-vir virtual_parent=sw0-p1
+wait_for_ports_up sw0-vir
+check ovn-nbctl --wait=hv sync
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([pod to pod with localnet_learn_fdb])
+AT_SKIP_IF([test $HAVE_SCAPY = no])
+
+# 6 VIFs, 3 per HV: vif11, vif12, vif13 on hv1.
+# vif11 will exchange packets with vif21, vif12 w/ vif22 and so on.
+#
+ovn_start
+net_add n1
+
+check ovn-nbctl ls-add ls0
+
+check ovn-nbctl lsp-add ls0 ln_port -- \
+      lsp-set-addresses ln_port unknown -- \
+      lsp-set-type ln_port localnet -- \
+      lsp-set-options ln_port network_name=physnet1
+
+for i in $(seq 1 3); do
+    check ovn-nbctl lsp-add ls0 vif1$i -- \
+          lsp-set-addresses vif1$i unknown
+    check ovn-nbctl lsp-add ls0 vif2$i -- \
+          lsp-set-addresses vif2$i unknown
+done
+
+n_pkt=(0 0 0 0 0 0)
+
+for hv in 1 2; do
+    sim_add hv${hv}
+    as hv${hv}
+    ovs-vsctl add-br br-phys
+    ovn_attach n1 br-phys 192.168.0.${hv}
+
+    for i in $(seq 1 3); do
+        ovs-vsctl -- add-port br-int vif${hv}${i} -- \
+            set interface vif${hv}${i} external-ids:iface-id=vif${hv}${i} \
+            options:tx_pcap=hv${hv}/vif${hv}${i}-tx.pcap \
+            options:rxq_pcap=hv${hv}/vif${hv}${i}-rx.pcap \
+            ofport-request=$i
+    done
+    ovs-vsctl -- add-port br-phys ext0 -- \
+        set interface ext0 \
+        options:tx_pcap=hv${hv}/ext0-tx.pcap \
+        options:rxq_pcap=hv${hv}/ext0-rx.pcap \
+        ofport-request=4
+    ovs-vsctl set open . external_ids:ovn-bridge-mappings=physnet1:br-phys
+done
+
+OVN_POPULATE_ARP
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+ln_port_key=$(fetch_column port_binding tunnel_key logical_port=ln_port)
+vif11_key=$(fetch_column port_binding tunnel_key logical_port=vif11)
+vif12_key=$(fetch_column port_binding tunnel_key logical_port=vif12)
+vif13_key=$(fetch_column port_binding tunnel_key logical_port=vif13)
+vif21_key=$(fetch_column port_binding tunnel_key logical_port=vif21)
+vif22_key=$(fetch_column port_binding tunnel_key logical_port=vif22)
+vif23_key=$(fetch_column port_binding tunnel_key logical_port=vif23)
+
+ensure_controller_run() {
+# We want to make sure controller could run at least one full loop.
+# We can't use wait=hv as sb might be sleeping.
+# Use 2 ovn-appctl to guarentee that ovn-controller run the full loop, and not just the unixctl handling
+  hv=$1
+  OVS_WAIT_UNTIL([test x$(as $hv ovn-appctl -t ovn-controller debug/status) = "xrunning"])
+  OVS_WAIT_UNTIL([test x$(as $hv ovn-appctl -t ovn-controller debug/status) = "xrunning"])
+}
+
+wait_for_packets() {
+    local hv=$1
+    local vif=$2
+    counter=$(((${hv:2} - 1) * 3 + ${vif:4} - 1))
+    n_pkt[[$counter]]=$((${n_pkt[[$counter]]} + 1))
+    echo "waiting for ${n_pkt[[$counter]]} packets on ${hv}/${vif} using counter $counter"
+    OVS_WAIT_UNTIL([test $(tcpdump ip -nnner ${hv}/${vif}-tx.pcap | wc -l) -eq ${n_pkt[[$counter]]}])
+}
+
+check_no_packets() {
+    local hv=$1
+    local vif=$2
+    counter=$(((${hv:2} - 1) * 3 + ${vif:4} - 1))
+    echo "waiting for ${n_pkt[[$counter]]} packets on ${hv}/${vif} using counter $counter"
+    OVS_WAIT_UNTIL([test $(tcpdump ip -nnner ${hv}/${vif}-tx.pcap | wc -l) -eq ${n_pkt[[$counter]]}])
+}
+
+send_packet() {
+    hv=$1
+    a_src=$2
+    a_dst=$3
+    dev=vif$2
+    AS_BOX([$(date +%H:%M:%S.%03N) sending packet from $dev in $hv $a_src to $a_dst])
+    packet=$(fmt_pkt "
+            Ether(dst='00:00:00:00:10:${a_dst}', src='00:00:00:00:10:${a_src}') /
+            IP(src='192.168.10.${a_src}', dst='192.168.10.${a_dst}') /
+            UDP(sport=1234, dport=1235)
+           ")
+    as $hv ovs-appctl netdev-dummy/receive $dev $packet
+}
+
+check_flow_count() {
+    hv=$1
+    count=$2
+    echo "Checking flow count for $hv is $count"
+    OVS_WAIT_UNTIL([test $count = $(as $hv ovs-ofctl dump-flows br-int table=72 | grep -v "NXST_FLOW reply" | wc -l)])
+}
+
+# Sending packet in both direction. Should create FDB entries for vifs
+# No localnet_learn_fdb yet
+AS_BOX([$(date +%H:%M:%S.%03N) vif11 <=> vif21])
+send_packet hv1 11 21
+for i in 1 2 3; do
+    wait_for_packets hv2 vif2${i}
+done
+for i in 2 3; do
+    wait_for_packets hv1 vif1${i}
+done
+# vif11 should now own the mac
+wait_column "$vif11_key" fdb port_key mac='"00:00:00:00:10:11"'
+
+send_packet hv2 21 11
+wait_for_packets hv1 vif11
+for i in 2 3; do
+    check_no_packets hv1 vif1${i}
+done
+wait_column "$vif21_key" fdb port_key mac='"00:00:00:00:10:21"'
+
+check_flow_count hv1 2
+check_flow_count hv2 2
+
+# We now enable localnet_learn_fdb
+# We check how it behaves with existing vif entries in fdb
+check ovn-nbctl --wait=hv set logical_switch_port ln_port options:localnet_learn_fdb=true
+
+AS_BOX([$(date +%H:%M:%S.%03N) vif11 <=> vif21 after learn_fdb])
+send_packet hv1 11 21
+
+wait_for_packets hv2 vif21
+for i in 2 3; do
+    check_no_packets hv2 vif2${i}
+done
+wait_column "$vif11_key" fdb port_key mac='"00:00:00:00:10:11"'
+
+send_packet hv2 21 11
+wait_for_packets hv1 vif11
+for i in 2 3; do
+    check_no_packets hv1 vif1${i}
+done
+wait_column "$vif21_key" fdb port_key mac='"00:00:00:00:10:21"'
+
+# In both controllers,
+# - 1st packet (in both dir) should have reached controller for the vif, not for localnet (as learn_fdb disabled for localnet)
+# - 2nd packet should not cause any upcall to controller as vif already owns the mac.
+AT_CHECK([test 1 = `cat hv1/ovs-vswitchd.log | grep NXT_PACKET_IN2 | wc -l`])
+AT_CHECK([test 1 = `cat hv2/ovs-vswitchd.log | grep NXT_PACKET_IN2 | wc -l`])
+
+check_flow_count hv1 4
+check_flow_count hv2 4
+
+# Send a few more packets
+send_packet hv1 11 21
+send_packet hv2 21 11
+
+for hv in 1 2; do
+    wait_for_packets hv${hv} vif${hv}1
+    for i in 2 3; do
+        echo CHECK
+        check_no_packets hv${hv} vif${hv}${i}
+    done
+done
+
+# The last packets should have gone through the fast path
+AT_CHECK([test 1 = `cat hv1/ovs-vswitchd.log | grep NXT_PACKET_IN2 | wc -l`])
+AT_CHECK([test 1 = `cat hv2/ovs-vswitchd.log | grep NXT_PACKET_IN2 | wc -l`])
+
+# Check that there are no bad surprises
+wait_column "$vif11_key" fdb port_key mac='"00:00:00:00:10:11"'
+wait_column "$vif21_key" fdb port_key mac='"00:00:00:00:10:21"'
+
+# We will now create fdb entries AFTER enabing localnet_learn_fdb
+# We make ovn_controller (hv1 or hv2) to sleep to control who writes first to fdb
+# as otherwise no guarentee.
+AS_BOX([$(date +%H:%M:%S.%03N) vif12 <=> vif22])
+# We make sure that the fdb update by the vif is done after the localnet update
+sleep_controller hv1
+send_packet hv1 12 22
+for i in 1 3; do
+    wait_for_packets hv1 vif1${i}
+done
+for i in 1 2 3; do
+    wait_for_packets hv2 vif2${i}
+done
+
+# ln_port should own the mac as vif not written yet
+wait_column "$ln_port_key" fdb port_key mac='"00:00:00:00:10:12"'
+
+wake_up_controller hv1
+# vif1 should now own the mac
+wait_column "$vif12_key" fdb port_key mac='"00:00:00:00:10:12"'
+
+sleep_controller hv2
+send_packet hv2 22 12
+wait_for_packets hv1 vif12
+for i in 1 3; do
+    check_no_packets hv1 vif1${i}
+done
+wait_column "$ln_port_key" fdb port_key mac='"00:00:00:00:10:22"'
+
+wake_up_controller hv2
+wait_column "$vif22_key" fdb port_key mac='"00:00:00:00:10:22"'
+
+check_flow_count hv1 8
+check_flow_count hv2 8
+
+AS_BOX([$(date +%H:%M:%S.%03N) vif13 <=> vif23 ])
+# Now we do the other way around: we make sure that the localnet update is done after the vif update.
+# So, when packet is sent from vif1 to vif2, vif1 will be learnt (by hv1) and written in sb
+# Then, when we wake up ovn-controller on hv2, it will learn on localnet.
+# This used to cause localnet entry to overwrite vif entry in sb
+sleep_controller hv2
+send_packet hv1 13 23
+for i in 1 2; do
+    wait_for_packets hv1 vif1${i}
+done
+for i in 1 2 3; do
+    wait_for_packets hv2 vif2${i}
+done
+
+wait_column "$vif13_key" fdb port_key mac='"00:00:00:00:10:13"'
+
+
+# At this point, FDB contains vif1 entry for mac 00:00:00:00:10:13.
+# However, as hv2 controller is sleeping, the flows in hv2 do not
+# contain the flows related to that fdb entry.
+# Hence, the packet which went through still failed the lookup.
+wake_up_controller hv2
+
+# FDB shoud not have changed. Just make sure controller has run and check fdb
+ensure_controller_run hv2
+wait_column "$vif13_key" fdb port_key mac='"00:00:00:00:10:13"'
+
+sleep_controller hv1
+send_packet hv2 23 13
+wait_for_packets hv1 vif13
+for i in 1 2; do
+    check_no_packets hv2 vif2${i}
+done
+for i in 1 2; do
+    check_no_packets hv1 vif1${i}
+done
+wait_column "$vif23_key" fdb port_key mac='"00:00:00:00:10:23"'
+
+wake_up_controller hv1
+ensure_controller_run hv1
+wait_column "$vif23_key" fdb port_key mac='"00:00:00:00:10:23"'
+
+# In both controllers
+# - vif11 <=> vif21: 1 PACKET_IN
+# - vif12 <=> vif22: 2 PACKET_IN
+# - vif13 <=> vif23: 2 PACKET_IN
+# controller + .
+AT_CHECK([test 5 = `cat hv1/ovs-vswitchd.log | grep NXT_PACKET_IN2 | wc -l`])
+AT_CHECK([test 5 = `cat hv2/ovs-vswitchd.log | grep NXT_PACKET_IN2 | wc -l`])
+
+# Send a few more packets
+send_packet hv1 13 23
+send_packet hv2 23 13
+wait_for_packets hv2 vif23
+wait_for_packets hv1 vif13
+for i in 1 2; do
+    check_no_packets hv1 vif1${i}
+    check_no_packets hv2 vif2${i}
+done
+send_packet hv1 13 23
+send_packet hv2 23 13
+wait_for_packets hv2 vif23
+wait_for_packets hv1 vif13
+for i in 1 2; do
+    check_no_packets hv1 vif1${i}
+    check_no_packets hv2 vif2${i}
+done
+
+# The last packets should have gone through the fast path
+AT_CHECK([test 5 = `cat hv1/ovs-vswitchd.log | grep NXT_PACKET_IN2 | wc -l`])
+AT_CHECK([test 5 = `cat hv2/ovs-vswitchd.log | grep NXT_PACKET_IN2 | wc -l`])
+
+# Check that there are no bad surprises
+wait_column "$vif11_key" fdb port_key mac='"00:00:00:00:10:11"'
+wait_column "$vif12_key" fdb port_key mac='"00:00:00:00:10:12"'
+wait_column "$vif13_key" fdb port_key mac='"00:00:00:00:10:13"'
+wait_column "$vif21_key" fdb port_key mac='"00:00:00:00:10:21"'
+wait_column "$vif22_key" fdb port_key mac='"00:00:00:00:10:22"'
+wait_column "$vif23_key" fdb port_key mac='"00:00:00:00:10:23"'
+
+check_flow_count hv1 12
+check_flow_count hv2 12
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([port up with slow northd])
+ovn_start
+
+sleep_northd() {
+  echo northd going to sleep
+  AT_CHECK([kill -STOP $(cat northd/ovn-northd.pid)])
+}
+
+wake_up_northd() {
+  echo northd going to sleep
+  AT_CHECK([kill -CONT $(cat northd/ovn-northd.pid)])
+}
+
+net_add n1
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.11
+
+check ovn-nbctl --wait=hv ls-add ls0
+# Create a pilot port and wait it up to make sure we are ready for the real
+# tests, so that the counters measured are accurate.
+check ovn-nbctl --wait=hv lsp-add ls0 lsp-pilot -- lsp-set-addresses lsp-pilot "unknown"
+ovs-vsctl add-port br-int lsp-pilot -- set interface lsp-pilot external_ids:iface-id=lsp-pilot
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+check ovn-nbctl --wait=hv lsp-add ls0 lsp0-2 -- lsp-set-addresses lsp0-2 "aa:aa:aa:00:00:02 192.168.0.12"
+ovs-vsctl add-port br-int lsp0-2 -- set interface lsp0-2 external_ids:iface-id=lsp0-2
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+sleep_northd
+check ovn-nbctl lsp-del lsp0-2
+check ovn-nbctl lsp-add ls0 lsp0-2 -- lsp-set-addresses lsp0-2 "aa:aa:aa:00:00:02 192.168.0.12"
+wake_up_northd
+
+check ovn-nbctl --wait=sb sync
+wait_for_ports_up
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([gratuitous arp max timeout])
+AT_KEYWORDS([slowtest])
+TAG_UNSTABLE
+AT_SKIP_IF([test $HAVE_TCPDUMP = no])
+ovn_start
+
+check ovn-nbctl ls-add ls0
+check ovn-nbctl lr-add lr0
+check ovn-nbctl lrp-add lr0 lr0-ls0 f0:00:00:00:00:01 192.168.0.1/24
+check 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"'
+
+check ovn-nbctl lsp-add ls0 ln_port
+check ovn-nbctl lsp-set-addresses ln_port unknown
+check ovn-nbctl lsp-set-type ln_port localnet
+check ovn-nbctl --wait=hv lsp-set-options ln_port network_name=physnet1
+
+net_add n1
+sim_add hv1
+as hv1
+check ovs-vsctl \
+    -- add-br br-phys \
+    -- add-br br-eth0
+
+ovn_attach n1 br-phys 192.168.0.10
+
+check ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=physnet1:br-eth0
+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
+
+# set garp max timeout to 2s
+as hv1 check ovs-vsctl set Open_vSwitch . external-ids:garp-max-timeout-sec=2
+
+# Wait until the patch ports are created in hv1 to connect br-int to br-eth0
+check ovn-nbctl set logical_router lr0 options:chassis=hv1
+OVN_WAIT_PATCH_PORT_FLOWS(["ln_port"], ["hv1"])
+
+# sleep for 12s to get a garp every ~ 2s
+sleep 12
+
+n_arp=$(tcpdump -ner hv1/snoopvif-tx.pcap arp | wc -l)
+AT_CHECK([test $n_arp -ge 5 -a $n_arp -lt 10])
+
+# Temporarily remove lr0 chassis
+# Wait for hv confirmation to make sure chassis is removed before we reset pcap
+# Otherwise a garp might be sent after pcap have been reset but before chassis is removed
+check ovn-nbctl --wait=hv remove logical_router lr0 options chassis
+
+as hv1 reset_pcap_file snoopvif hv1/snoopvif
+# set garp max timeout to 1s
+as hv1 check ovs-vsctl set Open_vSwitch . external-ids:garp-max-timeout-sec=1
+check ovn-nbctl set logical_router lr0 options:chassis=hv1
+
+# sleep for 7s to get a garp every ~ 1s
+sleep 7
+
+n_arp=$(tcpdump -ner hv1/snoopvif-tx.pcap arp | wc -l)
+AT_CHECK([test $n_arp -ge 5 -a $n_arp -lt 10])
+
+OVN_CLEANUP([hv1])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([Changing tunnel_key])
+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.11
+
+check ovn-nbctl --wait=hv ls-add ls \
+          -- lsp-add ls lsp1 \
+          -- lsp-add ls ls-lr \
+          -- lr-add lr \
+          -- lrp-add lr lr-ls f0:00:00:00:00:f1 192.168.1.1/24 \
+          -- set Logical_Switch_Port ls-lr \
+               type=router \
+               options:router-port=lr-ls \
+               addresses=router \
+          -- lrp-set-gateway-chassis lr-ls hv1
+
+sleep_controller hv1
+
+check ovn-nbctl --wait=sb set Logical_Switch ls other_config:requested-tnl-key=1000
+check ovn-nbctl --wait=sb ls-del ls
+wake_up_controller hv1
+
+check ovn-nbctl --wait=hv sync
+
+check ovn-nbctl --wait=hv ls-add ls1 \
+      -- lsp-add ls1 ls1-lr \
+      -- lrp-add lr lr-ls1 f0:00:00:00:00:f2 192.168.2.1/24 \
+      -- set Logical_Switch_Port ls1-lr type=router options:router-port=lr-ls1 addresses=router \
+      -- lrp-set-gateway-chassis lr-ls1 hv1
+
+check ovn-nbctl --wait=hv set Logical_Switch ls1 other_config:requested-tnl-key=1001
+
+OVN_CLEANUP([hv1])
+
+AT_CLEANUP
+])
diff --git a/tests/scapy-server.py b/tests/scapy-server.py
new file mode 100755
index 000000000..1cc616f70
--- /dev/null
+++ b/tests/scapy-server.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+
+import argparse
+import time
+
+import ovs.daemon
+import ovs.unixctl
+import ovs.unixctl.server
+
+import binascii
+from scapy.all import *  # noqa: F401,F403
+from scapy.all import raw
+
+
+vlog = ovs.vlog.Vlog("scapy-server")
+exiting = False
+
+
+def exit(conn, argv, aux):
+    global exiting
+
+    exiting = True
+    conn.reply(None)
+
+
+def process(data):
+    start_time = time.perf_counter()
+    vlog.info(f"received payload request: {data}")
+    try:
+        data = data.replace('\n', '')
+        return binascii.hexlify(raw(eval(data))).decode()
+    except Exception as e:
+        vlog.exception(f"failed to process payload request: {e}")
+        return ""
+    finally:
+        total_time = (time.perf_counter() - start_time) * 1000
+        vlog.info(f"took {total_time:.2f}ms to process payload request")
+
+
+def payload(conn, argv, aux):
+    try:
+        conn.reply(process(argv[0]))
+    except Exception as e:
+        vlog.exception(f"failed to reply to payload request: {e}")
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Scapy-based Frame Payload Generator")
+    parser.add_argument("--unixctl", help="UNIXCTL socket location or 'none'.")
+
+    ovs.daemon.add_args(parser)
+    ovs.vlog.add_args(parser)
+    args = parser.parse_args()
+    ovs.daemon.handle_args(args)
+    ovs.vlog.handle_args(args)
+
+    ovs.daemon.daemonize_start()
+    error, server = ovs.unixctl.server.UnixctlServer.create(args.unixctl)
+    if error:
+        ovs.util.ovs_fatal(error, "could not create unixctl server at %s"
+                           % args.unixctl, vlog)
+
+    ovs.unixctl.command_register("exit", "", 0, 0, exit, None)
+    ovs.unixctl.command_register("payload", "", 1, 1, payload, None)
+    ovs.daemon.daemonize_complete()
+
+    vlog.info("scapy server ready")
+
+    poller = ovs.poller.Poller()
+    while not exiting:
+        server.run()
+        server.wait(poller)
+        if exiting:
+            poller.immediate_wake()
+        poller.block()
+    server.close()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/system-common-macros.at b/tests/system-common-macros.at
index 97c6e433b..65c4884ea 100644
--- a/tests/system-common-macros.at
+++ b/tests/system-common-macros.at
@@ -372,7 +372,7 @@ ADD_VETH(sw11, sw11, br-int, "192.168.2.2/24", "f0:00:00:02:02:03", \
          "192.168.2.1")
 ADD_NAMESPACES(server)
 ADD_VETH(s1, server, br-ext, "2001:1db8:3333::2/64", "f0:00:00:01:02:05", \
-         "2001:1db8:3333::1")
+         "2001:1db8:3333::1", "nodad")
 
 if test X"$1" = X"GR"; then
    ovn-nbctl create Logical_Router name=R1 options:chassis=hv1
@@ -409,9 +409,6 @@ ovn-nbctl lsp-add sw0 sw01 \
 ovn-nbctl lsp-add sw1 sw11 \
     -- lsp-set-addresses sw11 "f0:00:00:02:02:03 192.168.2.2"
 
-OVS_WAIT_UNTIL([test "$(ip netns exec server ip a | grep 2001:1db8:3333::2 | grep tentative)" = ""])
-OVS_WAIT_UNTIL([test "$(ip netns exec server ip a | grep fe80 | grep tentative)" = ""])
-
 AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext])
 ovn-nbctl lsp-add public public1 \
         -- lsp-set-addresses public1 unknown \
diff --git a/tests/system-ovn-kmod.at b/tests/system-ovn-kmod.at
index b29e6b55a..039d71170 100644
--- a/tests/system-ovn-kmod.at
+++ b/tests/system-ovn-kmod.at
@@ -1,224 +1,5 @@
 AT_BANNER([system-ovn-kmod])
 
-# SCTP and userspace conntrack do not mix. Therefore this
-# test only can be run with kernel datapath. Otherwise,
-# this is mostly a copy of existing load balancer tests
-# in system-ovn.at
-AT_SETUP([load balancing in gateway router - SCTP])
-AT_SKIP_IF([test $HAVE_SCTP = no])
-AT_SKIP_IF([test $HAVE_NC = no])
-AT_KEYWORDS([ovnlb sctp])
-
-# Make sure the SCTP kernel module is loaded.
-LOAD_MODULE([sctp])
-
-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:
-# Two LRs - R1 and R2 that are connected to each other via LS "join"
-# in 20.0.0.0/24 network. R1 has switchess foo (192.168.1.0/24) and
-# bar (192.168.2.0/24) connected to it. R2 has alice (172.16.1.0/24) connected
-# to it.  R2 is a gateway router on which we add load-balancing rules.
-#
-#    foo -- R1 -- join - R2 -- alice
-#           |
-#    bar ----
-
-ovn-nbctl create Logical_Router name=R1
-ovn-nbctl create Logical_Router name=R2 options:chassis=hv1
-
-ovn-nbctl ls-add foo
-ovn-nbctl ls-add bar
-ovn-nbctl ls-add alice
-ovn-nbctl ls-add join
-
-# Connect foo to R1
-ovn-nbctl lrp-add R1 foo 00:00:01:01:02:03 192.168.1.1/24
-ovn-nbctl lsp-add foo rp-foo -- set Logical_Switch_Port rp-foo \
-    type=router options:router-port=foo addresses=\"00:00:01:01:02:03\"
-
-# Connect bar to R1
-ovn-nbctl lrp-add R1 bar 00:00:01:01:02:04 192.168.2.1/24
-ovn-nbctl lsp-add bar rp-bar -- set Logical_Switch_Port rp-bar \
-    type=router options:router-port=bar addresses=\"00:00:01:01:02:04\"
-
-# Connect alice to R2
-ovn-nbctl lrp-add R2 alice 00:00:02:01:02:03 172.16.1.1/24
-ovn-nbctl lsp-add alice rp-alice -- set Logical_Switch_Port rp-alice \
-    type=router options:router-port=alice addresses=\"00:00:02:01:02:03\"
-
-# Connect R1 to join
-ovn-nbctl lrp-add R1 R1_join 00:00:04:01:02:03 20.0.0.1/24
-ovn-nbctl lsp-add join r1-join -- set Logical_Switch_Port r1-join \
-    type=router options:router-port=R1_join addresses='"00:00:04:01:02:03"'
-
-# Connect R2 to join
-ovn-nbctl lrp-add R2 R2_join 00:00:04:01:02:04 20.0.0.2/24
-ovn-nbctl lsp-add join r2-join -- set Logical_Switch_Port r2-join \
-    type=router options:router-port=R2_join addresses='"00:00:04:01:02:04"'
-
-# Static routes.
-ovn-nbctl lr-route-add R1 172.16.1.0/24 20.0.0.2
-ovn-nbctl lr-route-add R2 192.168.0.0/16 20.0.0.1
-
-# Logical port 'foo1' in switch 'foo'.
-ADD_NAMESPACES(foo1)
-ADD_VETH(foo1, foo1, br-int, "192.168.1.2/24", "f0:00:00:01:02:03", \
-         "192.168.1.1")
-ovn-nbctl lsp-add foo foo1 \
--- lsp-set-addresses foo1 "f0:00:00:01:02:03 192.168.1.2"
-
-# Logical port 'alice1' in switch 'alice'.
-ADD_NAMESPACES(alice1)
-ADD_VETH(alice1, alice1, br-int, "172.16.1.2/24", "f0:00:00:01:02:04", \
-         "172.16.1.1")
-ovn-nbctl lsp-add alice alice1 \
--- lsp-set-addresses alice1 "f0:00:00:01:02:04 172.16.1.2"
-
-# Logical port 'bar1' in switch 'bar'.
-ADD_NAMESPACES(bar1)
-ADD_VETH(bar1, bar1, br-int, "192.168.2.2/24", "f0:00:00:01:02:05", \
-"192.168.2.1")
-ovn-nbctl lsp-add bar bar1 \
--- lsp-set-addresses bar1 "f0:00:00:01:02:05 192.168.2.2"
-
-# Config OVN load-balancer with a VIP.
-uuid=`ovn-nbctl  create load_balancer protocol=sctp vips:30.0.0.1="192.168.1.2,192.168.2.2"`
-ovn-nbctl set logical_router R2 load_balancer=$uuid
-
-# Config OVN load-balancer with another VIP (this time with ports).
-ovn-nbctl set load_balancer $uuid vips:'"30.0.0.2:8000"'='"192.168.1.2:12345,192.168.2.2:12345"'
-
-# Add SNAT rule to make sure that Load-balancing still works with a SNAT rule.
-ovn-nbctl -- --id=@nat create nat type="snat" logical_ip=192.168.2.2 \
-    external_ip=30.0.0.2 -- add logical_router R2 nat @nat
-
-# Wait for ovn-controller to catch up.
-ovn-nbctl --wait=hv sync
-OVS_WAIT_UNTIL([ovs-ofctl -O OpenFlow13 dump-groups br-int | \
-grep 'nat(dst=192.168.2.2:12345)'])
-
-# Start webservers in 'foo1', 'bar1'.
-OVS_START_L7([foo1], [sctp])
-OVS_START_L7([bar1], [sctp])
-
-on_exit "ovs-ofctl -O OpenFlow13 dump-flows br-int"
-
-dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [nc --sctp --recv-only 30.0.0.1 12345 > client$i.log])
-done
-
-dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/' |
-sed -e 's/vtag_orig=[[0-9]]*/vtag_orig=<cleared>/' |
-sed -e 's/vtag_reply=[[0-9]]*/vtag_reply=<cleared>/' | uniq], [0], [dnl
-sctp,orig=(src=172.16.1.2,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>,vtag_orig=<cleared>,vtag_reply=<cleared>)
-sctp,orig=(src=172.16.1.2,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>,vtag_orig=<cleared>,vtag_reply=<cleared>)
-])
-
-dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [nc --sctp --recv-only 30.0.0.2 8000 > clients$i.log])
-done
-
-dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/' |
-sed -e 's/vtag_orig=[[0-9]]*/vtag_orig=<cleared>/' |
-sed -e 's/vtag_reply=[[0-9]]*/vtag_reply=<cleared>/' | uniq], [0], [dnl
-sctp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>,vtag_orig=<cleared>,vtag_reply=<cleared>)
-sctp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>,vtag_orig=<cleared>,vtag_reply=<cleared>)
-])
-
-check_est_flows () {
-    n=$(ovs-ofctl dump-flows br-int table=15 | grep "+est" \
-        | grep "ct_mark=$1" | sed -n 's/.*n_packets=\([[0-9]]\{1,\}\).*/\1/p')
-
-    echo "n_packets=$n"
-    test -n "$n" && test "$n" != "0"
-}
-
-OVS_WAIT_UNTIL([check_est_flows 0x2], [check established flows])
-
-
-ovn-nbctl set logical_router R2 options:lb_force_snat_ip="20.0.0.2"
-
-# Destroy the load balancer and create again. ovn-controller will
-# clear the OF flows and re add again and clears the n_packets
-# for these flows.
-ovn-nbctl destroy load_balancer $uuid
-uuid=`ovn-nbctl  create load_balancer protocol=sctp vips:30.0.0.1="192.168.1.2,192.168.2.2"`
-ovn-nbctl set logical_router R2 load_balancer=$uuid
-
-check ovs-appctl dpctl/flush-conntrack
-
-# Config OVN load-balancer with another VIP (this time with ports).
-ovn-nbctl set load_balancer $uuid vips:'"30.0.0.2:8000"'='"192.168.1.2:12345,192.168.2.2:12345"'
-
-ovn-nbctl list load_balancer
-ovn-sbctl dump-flows R2
-OVS_WAIT_UNTIL([ovs-ofctl -O OpenFlow13 dump-flows br-int table=45 | grep 'nat(src=20.0.0.2)'])
-
-dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [nc --sctp --recv-only 30.0.0.2 8000 > clients$i.log])
-done
-
-dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/' |
-sed -e 's/vtag_orig=[[0-9]]*/vtag_orig=<cleared>/' |
-sed -e 's/vtag_reply=[[0-9]]*/vtag_reply=<cleared>/' | uniq], [0], [dnl
-sctp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>,vtag_orig=<cleared>,vtag_reply=<cleared>)
-sctp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>,vtag_orig=<cleared>,vtag_reply=<cleared>)
-])
-
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0.2) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/' |
-sed -e 's/vtag_orig=[[0-9]]*/vtag_orig=<cleared>/' |
-sed -e 's/vtag_reply=[[0-9]]*/vtag_reply=<cleared>/' | uniq], [0], [dnl
-sctp,orig=(src=172.16.1.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>,vtag_orig=<cleared>,vtag_reply=<cleared>)
-sctp,orig=(src=172.16.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>,vtag_orig=<cleared>,vtag_reply=<cleared>)
-])
-
-OVS_WAIT_UNTIL([check_est_flows 0xa], [check established flows])
-
-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 balancing affinity sessions - IPv4])
 AT_KEYWORDS([ovnlb])
@@ -365,7 +146,7 @@ tcp,orig=(src=172.16.1.2,dst=172.16.1.100,sport=<cleared>,dport=<cleared>),reply
 ])
 
 dp_key=$(printf "0x%x" $(fetch_column datapath tunnel_key external_ids:name=R2))
-AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=78 --no-stats | sed -e 's/load:0xc0a80[[0-9]]02/load:0xc0a80<cleared>02/'], [0], [dnl
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=78 --no-stats | strip_cookie | sed -e 's/load:0xc0a80[[0-9]]02/load:0xc0a80<cleared>02/'], [0], [dnl
  table=78, idle_timeout=60, tcp,metadata=$dp_key,nw_src=172.16.1.2,nw_dst=172.16.1.100,tp_dst=8080 actions=load:0x1->NXM_NX_REG10[[14]],load:0xc0a80<cleared>02->NXM_NX_REG4[[]],load:0x50->NXM_NX_REG8[[0..15]]
 ])
 
@@ -588,31 +369,27 @@ ovn-nbctl lr-route-add R2 fd12::/64 fd20::1
 # Logical port 'foo1' in switch 'foo'.
 ADD_NAMESPACES(foo1)
 ADD_VETH(foo1, foo1, br-int, "fd11::2/64", "f0:00:00:01:02:03", \
-         "fd11::1")
-OVS_WAIT_UNTIL([test "$(ip -n foo1 a | grep fd11::2 | grep tentative)" = ""])
+         "fd11::1", "nodad")
 ovn-nbctl lsp-add foo foo1 \
 -- lsp-set-addresses foo1 "f0:00:00:01:02:03 fd11::2"
 
 # Logical port 'alice1' in switch 'alice'.
 ADD_NAMESPACES(alice1)
 ADD_VETH(alice1, alice1, br-int, "fd72::2/64", "f0:00:00:01:02:04", \
-         "fd72::1")
-OVS_WAIT_UNTIL([test "$(ip -n alice1 a | grep fd72::2 | grep tentative)" = ""])
+         "fd72::1", "nodad")
 ovn-nbctl lsp-add alice alice1 \
 -- lsp-set-addresses alice1 "f0:00:00:01:02:04 fd72::2"
 
 # Logical port 'bar1' in switch 'bar'.
 ADD_NAMESPACES(bar1)
 ADD_VETH(bar1, bar1, br-int, "fd12::2/64", "f0:00:00:01:02:05", \
-"fd12::1")
-OVS_WAIT_UNTIL([test "$(ip -n bar1 a | grep fd12::2 | grep tentative)" = ""])
+         "fd12::1", "nodad")
 ovn-nbctl lsp-add bar bar1 \
 -- lsp-set-addresses bar1 "f0:00:00:01:02:05 fd12::2"
 
 ADD_NAMESPACES(bar2)
 ADD_VETH(bar2, bar2, br-int, "fd12::3/64", "e0:00:00:01:02:05", \
-"fd12::1")
-OVS_WAIT_UNTIL([test "$(ip -n bar2 a | grep fd12::3 | grep tentative)" = ""])
+         "fd12::1", "nodad")
 ovn-nbctl lsp-add bar bar2 \
 -- lsp-set-addresses bar2 "e0:00:00:01:02:05 fd12::3"
 
@@ -666,7 +443,7 @@ tcp,orig=(src=fd72::2,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd
 ])
 
 dp_key=$(printf "0x%x" $(fetch_column datapath tunnel_key external_ids:name=R2))
-AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=78 --no-stats | sed -e 's/load:0xfd1[[0-9]]000000000000/load:0xfd1<cleared>000000000000/'], [0], [dnl
+AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=78 --no-stats | strip_cookie | sed -e 's/load:0xfd1[[0-9]]000000000000/load:0xfd1<cleared>000000000000/'], [0], [dnl
  table=78, idle_timeout=60, tcp6,metadata=$dp_key,ipv6_src=fd72::2,ipv6_dst=fd30::1,tp_dst=8080 actions=load:0x1->NXM_NX_REG10[[14]],load:0x2->NXM_NX_XXREG1[[0..63]],load:0xfd1<cleared>000000000000->NXM_NX_XXREG1[[64..127]],load:0x50->NXM_NX_REG8[[0..15]]
 ])
 
@@ -1115,3 +892,155 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
 /connection dropped.*/d"])
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([LR with SNAT fragmentation needed for external server])
+AT_KEYWORDS([ovnlb])
+
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+ADD_BR([br-ext])
+
+dnl Logical network:
+dnl 2 logical switches "public" (192.168.1.0/24) and "internal" (172.16.1.0/24)
+dnl connected to a router lr.
+dnl internal has a client.
+dnl server is connected through localnet.
+dnl
+dnl Server IP 192.168.1.2 MTU 900
+dnl Client IP  172.16.1.2 MTU 800
+dnl
+dnl SNAT for internal 172.16.1.2/24 with router ip 192.168.1.1.
+
+check ovs-ofctl add-flow br-ext action=normal
+# Set external-ids in br-int needed for ovn-controller
+check ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true \
+        -- set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext
+
+dnl Start ovn-controller
+start_daemon ovn-controller
+
+check ovn-nbctl lr-add lr
+check ovn-nbctl ls-add internal
+check ovn-nbctl ls-add public
+
+check ovn-nbctl lrp-add lr lr-pub 00:00:01:01:02:03 192.168.1.1/24
+check ovn-nbctl lsp-add  public pub-lr -- set Logical_Switch_Port pub-lr \
+    type=router options:router-port=lr-pub addresses=\"00:00:01:01:02:03\"
+
+check ovn-nbctl lrp-add lr lr-internal 00:00:01:01:02:04 172.16.1.1/24
+check ovn-nbctl lsp-add internal internal-lr -- set Logical_Switch_Port internal-lr \
+    type=router options:router-port=lr-internal addresses=\"00:00:01:01:02:04\"
+
+ovn-nbctl lsp-add public ln_port \
+                -- lsp-set-addresses ln_port unknown \
+                -- lsp-set-type ln_port localnet \
+                -- lsp-set-options ln_port network_name=phynet
+
+ADD_NAMESPACES(server)
+ADD_VETH([server], [server], [br-ext], ["192.168.1.2/24"],
+         ["f0:00:00:01:02:03"], ["192.168.1.1"])
+NS_EXEC([server], [ip l set dev server mtu 900])
+NS_EXEC([server], [ip l show dev server])
+
+ADD_NAMESPACES(client)
+ADD_VETH([client], [client], [br-int], ["172.16.1.2/24"],
+         ["f0:00:0f:01:02:03"], ["172.16.1.1"])
+NS_EXEC([client], [ip l set dev client mtu 800])
+NS_EXEC([client], [ip l show dev client])
+check ovn-nbctl lsp-add internal client \
+  -- lsp-set-addresses client "f0:00:0f:01:02:03 172.16.1.2"
+
+dnl Config OVN load-balancer with a VIP.  (not necessary, but if we do not
+dnl have a load balancer and comment out snat, we will receive a stray fragment
+dnl on the client side.)
+dnl check ovn-nbctl lb-add lb1 192.168.1.20:4242 172.16.1.2:4242 udp
+dnl check ovn-nbctl lr-lb-add lr lb1
+check ovn-nbctl set logical_router lr options:chassis=hv1
+check ovn-nbctl set logical_router_port lr-internal options:gateway_mtu=800
+
+check ovn-nbctl lr-nat-add lr snat 192.168.1.1 172.16.1.2/24
+
+check ovn-nbctl --wait=hv sync
+
+ovn-nbctl show
+ovs-vsctl show
+ovn-appctl -t ovn-controller vlog/set vconn:file:dbg pinctrl:file:dbg
+
+AT_DATA([server.py], [dnl
+import socket
+
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+server_address = '192.168.1.2'
+server_port = 4242
+
+server = (server_address, server_port)
+sock.bind(server)
+print("Listening on ", server_address, ":", str(server_port), flush=True)
+
+while True:
+  payload, client_address = sock.recvfrom(1000)
+  print("Received data from ", str(client_address), ": ", payload)
+  sent = sock.sendto(b"x" * 1017, client_address)
+  print("Sent back: ", str(sent), "bytes", flush=True)
+])
+NETNS_DAEMONIZE([server], [$PYTHON3 ./server.py > server.log], [server.pid])
+
+dnl Collect packets on server side.
+NETNS_DAEMONIZE([server], [tcpdump -l -U -i server -vnne \
+          'ip and (icmp or udp)' > server.tcpdump 2>server_err], [tcpdump0.pid])
+OVS_WAIT_UNTIL([grep "listening" server_err])
+
+dnl Collect packets on client side.
+NETNS_DAEMONIZE([client], [tcpdump -l -U -i client -vnne \
+          'ip and (icmp or udp)' > client.tcpdump 2>client_err], [tcpdump1.pid])
+OVS_WAIT_UNTIL([grep "listening" client_err])
+
+dnl Send two packets to the server with a short interval.
+dnl First packet should generate 'needs frag', the second should result in
+dnl corectly fragmented reply.
+AT_DATA([client.py], [dnl
+import socket
+import time
+
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+sock.sendto(b"x" * 7, ("192.168.1.2", 4242))
+time.sleep(1)
+sock.sendto(b"x" * 7, ("192.168.1.2", 4242))
+time.sleep(5)
+])
+NS_CHECK_EXEC([client], [$PYTHON3 ./client.py])
+
+dnl Expecting 2 outgoing packets and 2 fragments back - 8 lines total.
+OVS_WAIT_UNTIL([test "$(cat client.tcpdump | wc -l)" = "8"])
+
+ovn-appctl -t ovn-controller vlog/set info
+
+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([NORTHD_TYPE])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["
+  /failed to query port patch-.*/d
+  /connection dropped.*/d
+"])
+AT_CLEANUP
+])
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 59d0cb2a0..3ecf1db42 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -251,24 +251,21 @@ ovn-nbctl lr-route-add R2 fd12::/64 fd00::1
 # Logical port 'foo1' in switch 'foo'.
 ADD_NAMESPACES(foo1)
 ADD_VETH(foo1, foo1, br-int, "fd11::2/64", "f0:00:00:01:02:03", \
-         "fd11::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo1 ip a | grep fd11::2 | grep tentative)" = ""])
+         "fd11::1", "nodad")
 ovn-nbctl lsp-add foo foo1 \
 -- lsp-set-addresses foo1 "f0:00:00:01:02:03 fd11::2"
 
 # Logical port 'alice1' in switch 'alice'.
 ADD_NAMESPACES(alice1)
 ADD_VETH(alice1, alice1, br-int, "fd21::2/64", "f0:00:00:01:02:04", \
-         "fd21::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec alice1 ip a | grep fd21::2 | grep tentative)" = ""])
+         "fd21::1", "nodad")
 ovn-nbctl lsp-add alice alice1 \
 -- lsp-set-addresses alice1 "f0:00:00:01:02:04 fd21::2"
 
 # Logical port 'bar1' in switch 'bar'.
 ADD_NAMESPACES(bar1)
 ADD_VETH(bar1, bar1, br-int, "fd12::2/64", "f0:00:00:01:02:05", \
-         "fd12::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec bar1 ip a | grep fd12::2 | grep tentative)" = ""])
+         "fd12::1", "nodad")
 ovn-nbctl lsp-add bar bar1 \
 -- lsp-set-addresses bar1 "f0:00:00:01:02:05 fd12::2"
 
@@ -538,16 +535,14 @@ ovn-nbctl lr-route-add R2 fd10::/64 fd20::1
 # Logical port 'foo1' in switch 'foo'.
 ADD_NAMESPACES(foo1)
 ADD_VETH(foo1, foo1, br-int, "fd10::2/64", "f0:00:00:01:02:03", \
-         "fd10::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo1 ip a | grep fd10::2 | grep tentative)" = ""])
+         "fd10::1", "nodad")
 ovn-nbctl lsp-add foo foo1 \
 -- lsp-set-addresses foo1 "f0:00:00:01:02:03 fd10::2"
 
 # Logical port 'alice1' in switch 'alice'.
 ADD_NAMESPACES(alice1)
 ADD_VETH(alice1, alice1, br-int, "fd30::2/64", "f0:00:00:01:02:04", \
-         "fd30::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec alice1 ip a | grep fd30::2 | grep tentative)" = ""])
+         "fd30::1", "nodad")
 ovn-nbctl lsp-add alice alice1 \
 -- lsp-set-addresses alice1 "f0:00:00:01:02:04 fd30::2"
 
@@ -908,32 +903,28 @@ ovn-nbctl set logical_router R3 options:dnat_force_snat_ip=fd20::3
 # Logical port 'foo1' in switch 'foo'.
 ADD_NAMESPACES(foo1)
 ADD_VETH(foo1, foo1, br-int, "fd11::2/64", "f0:00:00:01:02:03", \
-         "fd11::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo1 ip a | grep fd11::2 | grep tentative)" = ""])
+         "fd11::1", "nodad")
 ovn-nbctl lsp-add foo foo1 \
 -- lsp-set-addresses foo1 "f0:00:00:01:02:03 fd11::2"
 
 # Logical port 'alice1' in switch 'alice'.
 ADD_NAMESPACES(alice1)
 ADD_VETH(alice1, alice1, br-int, "fd30::3/64", "f0:00:00:01:02:04", \
-         "fd30::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec alice1 ip a | grep fd30::3 | grep tentative)" = ""])
+         "fd30::1", "nodad")
 ovn-nbctl lsp-add alice alice1 \
 -- lsp-set-addresses alice1 "f0:00:00:01:02:04 fd30::3"
 
 # Logical port 'bar1' in switch 'bar'.
 ADD_NAMESPACES(bar1)
 ADD_VETH(bar1, bar1, br-int, "fd12::2/64", "f0:00:00:01:02:05", \
-         "fd12::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec bar1 ip a | grep fd12::2 | grep tentative)" = ""])
+         "fd12::1", "nodad")
 ovn-nbctl lsp-add bar bar1 \
 -- lsp-set-addresses bar1 "f0:00:00:01:02:05 fd12::2"
 
 # Logical port 'bob1' in switch 'bob'.
 ADD_NAMESPACES(bob1)
 ADD_VETH(bob1, bob1, br-int, "fd30::4/64", "f0:00:00:01:02:06", \
-         "fd30::2")
-OVS_WAIT_UNTIL([test "$(ip netns exec bob1 ip a | grep fd30::4 | grep tentative)" = ""])
+         "fd30::2", "nodad")
 ovn-nbctl lsp-add bob bob1 \
 -- lsp-set-addresses bob1 "f0:00:00:01:02:06 fd30::4"
 
@@ -1149,8 +1140,7 @@ ovn-nbctl lsp-add foo foo1 \
 
 ADD_NAMESPACES(foo16)
 ADD_VETH(foo16, foo16, br-int, "fd11::2/64", "f0:00:00:02:02:03", \
-         "fd11::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo16 ip a | grep fd11::2 | grep tentative)" = ""])
+         "fd11::1", "nodad")
 ovn-nbctl lsp-add foo foo16 \
 -- lsp-set-addresses foo16 "f0:00:00:02:02:03 fd11::2"
 
@@ -1163,8 +1153,7 @@ ovn-nbctl lsp-add alice alice1 \
 
 ADD_NAMESPACES(alice16)
 ADD_VETH(alice16, alice16, br-int, "fd30::3/64", "f0:00:00:02:02:04", \
-         "fd30::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec alice16 ip a | grep fd30::3 | grep tentative)" = ""])
+         "fd30::1", "nodad")
 ovn-nbctl lsp-add alice alice16 \
 -- lsp-set-addresses alice16 "f0:00:00:02:02:04 fd30::3"
 
@@ -1177,8 +1166,7 @@ ovn-nbctl lsp-add bar bar1 \
 
 ADD_NAMESPACES(bar16)
 ADD_VETH(bar16, bar16, br-int, "fd12::2/64", "f0:00:00:02:02:05", \
-         "fd12::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec bar16 ip a | grep fd12::2 | grep tentative)" = ""])
+         "fd12::1", "nodad")
 ovn-nbctl lsp-add bar bar16 \
 -- lsp-set-addresses bar16 "f0:00:00:02:02:05 fd12::2"
 
@@ -1191,8 +1179,7 @@ ovn-nbctl lsp-add bob bob1 \
 
 ADD_NAMESPACES(bob16)
 ADD_VETH(bob16, bob16, br-int, "fd30::4/64", "f0:00:00:02:02:06", \
-         "fd30::2")
-OVS_WAIT_UNTIL([test "$(ip netns exec bob16 ip a | grep fd30::4 | grep tentative)" = ""])
+         "fd30::2", "nodad")
 ovn-nbctl lsp-add bob bob16 \
 -- lsp-set-addresses bob16 "f0:00:00:02:02:06 fd30::4"
 
@@ -2376,14 +2363,10 @@ ADD_NAMESPACES(server)
 ADD_VETH(s1, server, br-ext, "172.16.1.100/24", "1a:00:00:00:00:01", \
          "172.16.1.1")
 
-OVS_WAIT_UNTIL([test "$(ip netns exec server ip a | grep fe80 | grep tentative)" = ""])
-
 ADD_NAMESPACES(client)
 ADD_VETH(c1, client, br-ext, "172.16.1.110/24", "1a:00:00:00:00:02", \
          "172.16.1.1")
 
-OVS_WAIT_UNTIL([test "$(ip netns exec client ip a | grep fe80 | grep tentative)" = ""])
-
 # Start webservers in 'server'.
 OVS_START_L7([server], [http])
 
@@ -3670,32 +3653,28 @@ ovn-nbctl lsp-add alice rp-alice -- set Logical_Switch_Port rp-alice \
 # Logical port 'foo1' in switch 'foo'.
 ADD_NAMESPACES(foo1)
 ADD_VETH(foo1, foo1, br-int, "fd11::2/64", "f0:00:00:01:02:03", \
-         "fd11::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo1 ip a | grep fd11::2 | grep tentative)" = ""])
+         "fd11::1", "nodad")
 ovn-nbctl lsp-add foo foo1 \
 -- lsp-set-addresses foo1 "f0:00:00:01:02:03 fd11::2"
 
 # Logical port 'foo2' in switch 'foo'.
 ADD_NAMESPACES(foo2)
 ADD_VETH(foo2, foo2, br-int, "fd11::3/64", "f0:00:00:01:02:06", \
-         "fd11::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo2 ip a | grep fd11::3 | grep tentative)" = ""])
+         "fd11::1", "nodad")
 ovn-nbctl lsp-add foo foo2 \
 -- lsp-set-addresses foo2 "f0:00:00:01:02:06 fd11::3"
 
 # Logical port 'bar1' in switch 'bar'.
 ADD_NAMESPACES(bar1)
 ADD_VETH(bar1, bar1, br-int, "fd12::2/64", "f0:00:00:01:02:04", \
-         "fd12::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec bar1 ip a | grep fd12::2 | grep tentative)" = ""])
+         "fd12::1", "nodad")
 ovn-nbctl lsp-add bar bar1 \
 -- lsp-set-addresses bar1 "f0:00:00:01:02:04 fd12::2"
 
 # Logical port 'alice1' in switch 'alice'.
 ADD_NAMESPACES(alice1)
 ADD_VETH(alice1, alice1, br-int, "fd20::2/64", "f0:00:00:01:02:05", \
-         "fd20::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec alice1 ip a | grep fd20::2 | grep tentative)" = ""])
+         "fd20::1", "nodad")
 ovn-nbctl lsp-add alice alice1 \
 -- lsp-set-addresses alice1 "f0:00:00:01:02:05 fd20::2"
 
@@ -4018,32 +3997,28 @@ ovn-nbctl lsp-add alice rp-alice -- set Logical_Switch_Port rp-alice \
 # Logical port 'foo1' in switch 'foo'.
 ADD_NAMESPACES(foo1)
 ADD_VETH(foo1, foo1, br-int, "fd11::2/64", "f0:00:00:01:02:03", \
-         "fd11::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo1 ip a | grep fd11::2 | grep tentative)" = ""])
+         "fd11::1", "nodad")
 ovn-nbctl lsp-add foo foo1 \
 -- lsp-set-addresses foo1 "f0:00:00:01:02:03 fd11::2"
 
 # Logical port 'foo2' in switch 'foo'.
 ADD_NAMESPACES(foo2)
 ADD_VETH(foo2, foo2, br-int, "fd11::3/64", "f0:00:00:01:02:06", \
-         "fd11::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo2 ip a | grep fd11::3 | grep tentative)" = ""])
+         "fd11::1", "nodad")
 ovn-nbctl lsp-add foo foo2 \
 -- lsp-set-addresses foo2 "f0:00:00:01:02:06 fd11::3"
 
 # Logical port 'bar1' in switch 'bar'.
 ADD_NAMESPACES(bar1)
 ADD_VETH(bar1, bar1, br-int, "fd12::2/64", "f0:00:00:01:02:04", \
-         "fd12::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec bar1 ip a | grep fd12::2 | grep tentative)" = ""])
+         "fd12::1", "nodad")
 ovn-nbctl lsp-add bar bar1 \
 -- lsp-set-addresses bar1 "f0:00:00:01:02:04 fd12::2"
 
 # Logical port 'alice1' in switch 'alice'.
 ADD_NAMESPACES(alice1)
 ADD_VETH(alice1, alice1, br-int, "fd20::2/64", "f0:00:00:01:02:05", \
-         "fd20::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec alice1 ip a | grep fd20::2 | grep tentative)" = ""])
+         "fd20::1", "nodad")
 ovn-nbctl lsp-add alice alice1 \
 -- lsp-set-addresses alice1 "f0:00:00:01:02:05 fd20::2"
 
@@ -4928,8 +4903,7 @@ ovn-nbctl lsp-add sw sw-rtr                       \
     -- lsp-set-options sw-rtr router-port=rtr-sw
 
 ADD_NAMESPACES(lsp)
-ADD_VETH(lsp, lsp, br-int, "4200::1/64", "00:00:00:00:00:01", "4200::00ff")
-OVS_WAIT_UNTIL([test "$(ip netns exec lsp ip a | grep 4200::1 | grep tentative)" = ""])
+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
 
 # Start IPv6 TCP server on lsp.
@@ -5058,8 +5032,8 @@ ADD_NAMESPACES(sw0-p2-rej)
 ADD_VETH(sw0-p2-rej, sw0-p2-rej, br-int, "10.0.0.4/24", "50:54:00:00:00:04", \
          "10.0.0.1")
 
-NS_CHECK_EXEC([sw0-p1-rej], [ip a a aef0::3/64 dev sw0-p1-rej], [0])
-NS_CHECK_EXEC([sw0-p2-rej], [ip a a aef0::4/64 dev sw0-p2-rej], [0])
+NS_CHECK_EXEC([sw0-p1-rej], [ip a a aef0::3/64 dev sw0-p1-rej nodad], [0])
+NS_CHECK_EXEC([sw0-p2-rej], [ip a a aef0::4/64 dev sw0-p2-rej nodad], [0])
 
 ADD_NAMESPACES(sw1-p1-rej)
 ADD_VETH(sw1-p1-rej, sw1-p1-rej, br-int, "20.0.0.3/24", "40:54:00:00:00:03", \
@@ -5099,9 +5073,6 @@ NS_CHECK_EXEC([sw0-p2-rej], [tcpdump -l -nn -i sw0-p2-rej tcp port 80 > sw0-p2-r
 #Wait for tcpdump to get started before generating first packets
 OVS_WAIT_UNTIL([test 1 = $(cat err | grep -c listening)])
 
-OVS_WAIT_UNTIL([test "$(ip netns exec sw0-p1-rej ip a | grep aef0::3 | grep tentative)" = ""])
-OVS_WAIT_UNTIL([test "$(ip netns exec sw0-p2-rej ip a | grep aef0::4 | grep tentative)" = ""])
-
 OVS_WAIT_UNTIL([
     ip netns exec sw0-p2-rej nc -vz6 aef0::3 80 2>&1 | grep -i 'connection refused'
 ])
@@ -5308,8 +5279,8 @@ ADD_NAMESPACES(sw0-p2-rej)
 ADD_VETH(sw0-p2-rej, sw0-p2-rej, br-int, "10.0.0.4/24", "50:54:00:00:00:04", \
          "10.0.0.1")
 
-NS_CHECK_EXEC([sw0-p1-rej], [ip a a aef0::3/64 dev sw0-p1-rej], [0])
-NS_CHECK_EXEC([sw0-p2-rej], [ip a a aef0::4/64 dev sw0-p2-rej], [0])
+NS_CHECK_EXEC([sw0-p1-rej], [ip a a aef0::3/64 dev sw0-p1-rej nodad], [0])
+NS_CHECK_EXEC([sw0-p2-rej], [ip a a aef0::4/64 dev sw0-p2-rej nodad], [0])
 
 ADD_NAMESPACES(sw1-p1-rej)
 ADD_VETH(sw1-p1-rej, sw1-p1-rej, br-int, "20.0.0.3/24", "40:54:00:00:00:03", \
@@ -5349,9 +5320,6 @@ NS_CHECK_EXEC([sw0-p2-rej], [tcpdump -l -nn -i sw0-p2-rej tcp port 80 > sw0-p2-r
 #Wait for tcpdump to get started before generating first packets
 OVS_WAIT_UNTIL([test 1 = $(cat err | grep -c listening)])
 
-OVS_WAIT_UNTIL([test "$(ip netns exec sw0-p1-rej ip a | grep aef0::3 | grep tentative)" = ""])
-OVS_WAIT_UNTIL([test "$(ip netns exec sw0-p2-rej ip a | grep aef0::4 | grep tentative)" = ""])
-
 OVS_WAIT_UNTIL([
     ip netns exec sw0-p2-rej nc -vz6 aef0::3 80 2>&1 | grep -i 'connection refused'
 ])
@@ -5878,12 +5846,10 @@ check ovn-nbctl                                                  \
     -- ls-lb-add ls lb-test
 
 ADD_NAMESPACES(vm1)
-ADD_VETH(vm1, vm1, br-int, "4242::2/64", "00:00:00:00:00:01", "4242::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec vm1 ip a | grep 4242::2 | grep tentative)" = ""])
+ADD_VETH(vm1, vm1, br-int, "4242::2/64", "00:00:00:00:00:01", "4242::1", "nodad")
 
 ADD_NAMESPACES(vm2)
-ADD_VETH(vm2, vm2, br-int, "4242::3/64", "00:00:00:00:00:02", "4242::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec vm2 ip a | grep 4242::3 | grep tentative)" = ""])
+ADD_VETH(vm2, vm2, br-int, "4242::3/64", "00:00:00:00:00:02", "4242::1", "nodad")
 
 # Wait for ovn-controller to catch up.
 wait_for_ports_up
@@ -6306,8 +6272,7 @@ NS_CHECK_EXEC([alice1], [sysctl -w net.ipv6.conf.default.router_solicitations=1]
 net.ipv6.conf.default.router_solicitations = 1
 ])
 ADD_VETH(alice1, alice1, br-int, "fd01::2/64", "f0:00:00:01:02:04", \
-         "fd01::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec alice1 ip a | grep fd01::2 | grep tentative)" = ""])
+         "fd01::1", "nodad")
 ovn-nbctl lsp-add alice alice1 \
 -- lsp-set-addresses alice1 "f0:00:00:01:02:04 fd01::2"
 # Add neighbour MAC address to avoid sending IPv6 NS messages which could
@@ -6322,8 +6287,7 @@ NS_CHECK_EXEC([bob1], [sysctl -w net.ipv6.conf.default.router_solicitations=1],
 net.ipv6.conf.default.router_solicitations = 1
 ])
 ADD_VETH(bob1, bob1, br-int, "fd07::1/64", "f0:00:00:01:02:06", \
-         "fd07::2")
-OVS_WAIT_UNTIL([test "$(ip netns exec bob1 ip a | grep fd07::1 | grep tentative)" = ""])
+         "fd07::2", "nodad")
 # Add neighbour MAC addresses to avoid sending IPv6 NS messages which could
 # cause datapath flows to be evicted
 NS_CHECK_EXEC([bob1], [ip -6 neigh add fd07::2 lladdr 00:00:02:01:02:03 dev bob1], [0])
@@ -6513,8 +6477,6 @@ ADD_NAMESPACES(ext-foo)
 ADD_VETH(ext-foo, ext-foo, br-ext, "172.16.1.100/24", "00:10:10:01:02:13", \
          "172.16.1.1")
 
-OVS_WAIT_UNTIL([test "$(ip netns exec ext-foo ip a | grep fe80 | grep tentative)" = ""])
-
 AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext])
 ovn-nbctl lsp-add public public1 \
         -- lsp-set-addresses public1 unknown \
@@ -6630,6 +6592,28 @@ AT_CHECK([ovn-nbctl set Logical_Switch_Port ext options:qos_min_rate=400000])
 AT_CHECK([ovn-nbctl set Logical_Switch_Port ext options:qos_max_rate=600000])
 AT_CHECK([ovn-nbctl set Logical_Switch_Port ext options:qos_burst=6000000])
 
+OVS_WAIT_UNTIL([tc qdisc show | grep -q 'htb 1: dev ovs-public'])
+OVS_WAIT_UNTIL([tc class show dev ovs-public | \
+                grep -q 'class htb .* rate 200Kbit ceil 300Kbit burst 375000b cburst 375000b'])
+
+OVS_WAIT_UNTIL([tc qdisc show | grep -q 'htb 1: dev ovs-ext'])
+OVS_WAIT_UNTIL([tc class show dev ovs-ext | \
+                grep -q 'class htb .* rate 400Kbit ceil 600Kbit burst 750000b cburst 750000b'])
+
+# The same now with ovs db read only
+#
+AT_CHECK([ovn-nbctl remove Logical_Switch_Port ext options qos_min_rate=400000])
+AT_CHECK([ovn-nbctl remove Logical_Switch_Port ext options qos_max_rate=600000])
+AT_CHECK([ovn-nbctl remove Logical_Switch_Port ext options qos_burst=6000000])
+OVS_WAIT_UNTIL([test "$(tc class show dev ovs-ext | grep 'class htb')" == ""])
+
+sleep_ovsdb .
+
+AT_CHECK([ovn-nbctl set Logical_Switch_Port ext options:qos_min_rate=400000])
+AT_CHECK([ovn-nbctl set Logical_Switch_Port ext options:qos_max_rate=600000])
+AT_CHECK([ovn-nbctl set Logical_Switch_Port ext options:qos_burst=6000000])
+wake_up_ovsdb .
+
 OVS_WAIT_UNTIL([tc qdisc show | grep -q 'htb 1: dev ovs-public'])
 OVS_WAIT_UNTIL([tc class show dev ovs-public | \
                 grep -q 'class htb .* rate 200Kbit ceil 300Kbit burst 375000b cburst 375000b'])
@@ -7348,9 +7332,9 @@ check ovn-nbctl lsp-add public public1 \
 
 NS_EXEC([sw01], [tcpdump -l -n -i sw01 icmp -Q in > reject.pcap &])
 check ovn-nbctl meter-add acl-meter drop 1 pktps 0
-check ovn-nbctl --wait=hv copp-add copp0 reject acl-meter
-check ovn-nbctl --wait=hv ls-copp-add copp0 sw0
-check ovn-nbctl acl-add sw0 from-lport 1002 'inport == "sw01" && ip && udp' reject
+check ovn-nbctl copp-add copp0 reject acl-meter
+check ovn-nbctl ls-copp-add copp0 sw0
+check ovn-nbctl --wait=hv acl-add sw0 from-lport 1002 'inport == "sw01" && ip && udp' reject
 
 AT_CHECK([ovn-nbctl copp-list copp0], [0], [dnl
 reject: acl-meter
@@ -7371,7 +7355,7 @@ rm -f reject.pcap
 
 # Let's update the meter
 NS_EXEC([sw01], [tcpdump -l -n -i sw01 icmp -Q in > reject.pcap &])
-check ovn-nbctl --may-exist meter-add acl-meter drop 10 pktps 0
+check ovn-nbctl --may-exist --wait=hv meter-add acl-meter drop 10 pktps 0
 ip netns exec sw01 scapy -H <<-EOF
 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 = 40)
@@ -7402,7 +7386,7 @@ kill $(pidof tcpdump)
 
 NS_EXEC([server], [tcpdump -l -n -i s1 arp[[24:4]]=0xac100164 > arp.pcap &])
 check ovn-nbctl meter-add arp-meter drop 1 pktps 0
-check ovn-nbctl --wait=hv copp-add copp1 arp-resolve arp-meter
+check ovn-nbctl copp-add copp1 arp-resolve arp-meter
 check ovn-nbctl --wait=hv lr-copp-add copp1 R1
 AT_CHECK([ovn-nbctl copp-list copp1], [0], [dnl
 arp-resolve: arp-meter
@@ -7421,7 +7405,7 @@ OVS_WAIT_UNTIL([
 kill $(pidof tcpdump)
 
 check ovn-nbctl meter-add icmp-meter drop 1 pktps 0
-check ovn-nbctl --wait=hv copp-add copp2 icmp4-error icmp-meter
+check ovn-nbctl copp-add copp2 icmp4-error icmp-meter
 check ovn-nbctl --wait=hv lr-copp-add copp2 R1
 AT_CHECK([ovn-nbctl copp-list copp2 |grep icmp4-error], [0], [dnl
 icmp4-error: icmp-meter
@@ -7441,7 +7425,7 @@ OVS_WAIT_UNTIL([
 kill $(pidof tcpdump)
 
 check ovn-nbctl meter-add bfd-meter drop 1 pktps 0
-check ovn-nbctl --wait=hv copp-add copp3 bfd bfd-meter
+check ovn-nbctl copp-add copp3 bfd bfd-meter
 check ovn-nbctl --wait=hv lr-copp-add copp3 R1
 AT_CHECK([ovn-nbctl copp-list copp3 |grep bfd], [0], [dnl
 bfd: bfd-meter
@@ -7471,7 +7455,7 @@ kill $(pidof tcpdump)
 
 check ovn-nbctl set nb_global . options:svc_monitor_mac="33:33:33:33:33:33"
 check ovn-nbctl meter-add svc-meter drop 1 pktps 0
-check ovn-nbctl --wait=hv copp-add copp4 svc-monitor svc-meter
+check ovn-nbctl copp-add copp4 svc-monitor svc-meter
 check ovn-nbctl --wait=hv ls-copp-add copp4 sw0
 check ovn-appctl -t ovn-controller vlog/set vconn:dbg
 AT_CHECK([ovn-nbctl copp-list copp4], [0], [dnl
@@ -8696,22 +8680,19 @@ check ovn-nbctl lr-nat-add lr1 snat 172.16.1.10 192.168.1.0/24
 check ovn-nbctl lr-nat-add lr1 snat 1711::10 2001::/64
 
 ADD_NAMESPACES(ls1p1)
-ADD_VETH(ls1p1, ls1p1, br-int, "192.168.1.1/24", "00:00:00:01:01:01", \
-         "192.168.1.254", , "2001::1/64", "2001::a")
+ADD_VETH(ls1p1, ls1p1, br-int, "2001::1/64", "00:00:00:01:01:01", \
+         "2001::a", "nodad", "192.168.1.1/24", "192.168.1.254")
 
 ADD_NAMESPACES(ls1p2)
-ADD_VETH(ls1p2, ls1p2, br-int, "192.168.1.2/24", "00:00:00:01:01:02", \
-         "192.168.1.254", , "2001::2/64", "2001::a")
+ADD_VETH(ls1p2, ls1p2, br-int, "2001::2/64", "00:00:00:01:01:02", \
+         "2001::a", "nodad", "192.168.1.2/24", "192.168.1.254")
 
 ADD_NAMESPACES(ext1)
-ADD_VETH(ext1, ext1, br0, "172.16.1.1/24", "00:ee:00:01:01:01", \
-         "172.16.1.254", , "1711::1/64", "1711::a")
+ADD_VETH(ext1, ext1, br0, "1711::1/64", "00:ee:00:01:01:01", \
+         "1711::a", "nodad", "172.16.1.1/24", "172.16.1.254")
 
 check ovn-nbctl --wait=hv sync
 wait_for_ports_up
-OVS_WAIT_UNTIL([test "$(ip netns exec ls1p1 ip a | grep 2001::1 | grep tentative)" = ""])
-OVS_WAIT_UNTIL([test "$(ip netns exec ls1p2 ip a | grep 2002::2 | grep tentative)" = ""])
-OVS_WAIT_UNTIL([test "$(ip netns exec ext1 ip a | grep 1711::1 | grep tentative)" = ""])
 
 NS_CHECK_EXEC([ls1p1], [ping -q -c 3 -i 0.3 -w 2  172.16.1.1 | FORMAT_PING], \
 [0], [dnl
@@ -9231,8 +9212,9 @@ name: 'vport4' value: '999'
 NETNS_DAEMONIZE([vm1], [nc -k -l 42.42.42.2 4242], [nc-vm1.pid])
 
 NETNS_DAEMONIZE([vm1],
-    [tcpdump -n -i vm1 -nnleX -c6 udp and dst 42.42.42.2 and dst port 4343 > vm1.pcap 2>/dev/null],
+    [tcpdump -n -i vm1 -nnleX -c6 udp and dst 42.42.42.2 and dst port 4343 > vm1.pcap 2> vm1.pcap.stderr],
     [tcpdump1.pid])
+OVS_WAIT_UNTIL([grep "listening" vm1.pcap.stderr])
 
 # Make sure connecting to the VIP works (hairpin, via ls and via lr).
 NS_CHECK_EXEC([vm1], [nc 66.66.66.66 666 -z], [0], [ignore], [ignore])
@@ -9355,16 +9337,13 @@ check ovn-nbctl --template lb-add lb-test-udp2 "^vip:^vport4" "[[4242::2]]:4343"
     -- lr-lb-add rtr lb-test-udp2
 
 ADD_NAMESPACES(vm1)
-ADD_VETH(vm1, vm1, br-int, "4242::2/64", "00:00:00:00:00:01", "4242::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec vm1 ip a | grep 4242::2 | grep tentative)" = ""])
+ADD_VETH(vm1, vm1, br-int, "4242::2/64", "00:00:00:00:00:01", "4242::1", "nodad")
 
 ADD_NAMESPACES(vm2)
-ADD_VETH(vm2, vm2, br-int, "4242::3/64", "00:00:00:00:00:02", "4242::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec vm2 ip a | grep 4242::3 | grep tentative)" = ""])
+ADD_VETH(vm2, vm2, br-int, "4242::3/64", "00:00:00:00:00:02", "4242::1", "nodad")
 
 ADD_NAMESPACES(vm3)
-ADD_VETH(vm3, vm3, br-int, "4343::2/64", "00:00:00:00:00:03", "4343::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec vm3 ip a | grep 4343::2 | grep tentative)" = ""])
+ADD_VETH(vm3, vm3, br-int, "4343::2/64", "00:00:00:00:00:03", "4343::1", "nodad")
 
 # Wait for ovn-controller to catch up.
 wait_for_ports_up
@@ -9385,8 +9364,9 @@ name: 'vport4' value: '999'
 NETNS_DAEMONIZE([vm1], [nc -k -l 4242::2 4242], [nc-vm1.pid])
 
 NETNS_DAEMONIZE([vm1],
-    [tcpdump -n -i vm1 -nnleX -c6 udp and dst 4242::2 and dst port 4343 > vm1.pcap 2>/dev/null],
+    [tcpdump -n -i vm1 -nnleX -c6 udp and dst 4242::2 and dst port 4343 > vm1.pcap 2> vm1.pcap.stderr],
     [tcpdump1.pid])
+OVS_WAIT_UNTIL([grep "listening" vm1.pcap.stderr])
 
 # Make sure connecting to the VIP works (hairpin, via ls and via lr).
 NS_CHECK_EXEC([vm1], [nc 6666::1 666 -z], [0], [ignore], [ignore])
@@ -10825,32 +10805,28 @@ check ovn-nbctl lsp-add bar rp-bar -- set Logical_Switch_Port rp-bar \
 # Logical port 'foo1' in switch 'foo'.
 ADD_NAMESPACES(foo1)
 ADD_VETH(foo1, foo1, br-int, "fd11::2/64", "f0:00:00:01:02:03", \
-         "fd7b:6b4d:7b25:d22f::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo1 ip a | grep fd11::2 | grep tentative)" = ""])
+         "fd7b:6b4d:7b25:d22f::1", "nodad")
 check ovn-nbctl lsp-add foo foo1 \
 -- lsp-set-addresses foo1 "f0:00:00:01:02:03 fd11::2"
 
 # Logical port 'foo2' in switch 'foo'.
 ADD_NAMESPACES(foo2)
 ADD_VETH(foo2, foo2, br-int, "fd11::3/64", "f0:00:00:01:02:04", \
-         "fd7b:6b4d:7b25:d22f::2")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo2 ip a | grep fd11::3 | grep tentative)" = ""])
+         "fd7b:6b4d:7b25:d22f::2", "nodad")
 check ovn-nbctl lsp-add foo foo2 \
 -- lsp-set-addresses foo2 "f0:00:00:01:02:04 fd11::3"
 
 # Logical port 'foo3' in switch 'foo'.
 ADD_NAMESPACES(foo3)
 ADD_VETH(foo3, foo3, br-int, "fd11::4/64", "f0:00:00:01:02:05", \
-         "fd7b:6b4d:7b25:d22d::1")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo3 ip a | grep fd11::4 | grep tentative)" = ""])
+         "fd7b:6b4d:7b25:d22d::1", "nodad")
 check ovn-nbctl lsp-add foo foo3 \
 -- lsp-set-addresses foo3 "f0:00:00:01:02:05 fd11::4"
 
 # Logical port 'bar1' in switch 'bar'.
 ADD_NAMESPACES(bar1)
 ADD_VETH(bar1, bar1, br-int, "fd12::2/64", "f0:00:00:01:02:06", \
-"fd7b:6b4d:7b25:d22f::3")
-OVS_WAIT_UNTIL([test "$(ip netns exec foo1 ip a | grep fd12::2 | grep tentative)" = ""])
+         "fd7b:6b4d:7b25:d22f::3", "nodad")
 check ovn-nbctl lsp-add bar bar1 \
 -- lsp-set-addresses bar1 "f0:00:00:01:02:06 fd12::2"
 
@@ -11686,7 +11662,7 @@ check ovn-nbctl lsp-add ls0 ls0-lr0 \
     -- lsp-set-options ls0-lr0 router-port=lr0-ls0
 
 ADD_NAMESPACES(vif0)
-ADD_VETH(vif0, vif0, br-int, "fd00::2/64", "00:00:00:00:00:02", "fd00::1")
+ADD_VETH(vif0, vif0, br-int, "fd00::2/64", "00:00:00:00:00:02", "fd00::1", "nodad")
 OVS_WAIT_UNTIL([test "$(ip netns exec vif0 ip a | grep fe80:: | grep tentative)" = ""])
 
 check ovn-nbctl set logical_router lr0 options:always_learn_from_arp_request=false
@@ -11729,3 +11705,417 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
 
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ct_flush on logical router load balancer])
+AT_KEYWORDS([ct-lr-flush])
+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_daemon ovn-controller
+
+check ovn-nbctl lr-add R1
+
+check ovn-nbctl ls-add sw0
+check ovn-nbctl ls-add public
+
+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-public 00:00:02:01:02:03 172.16.1.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 sw0 sw0-vm \
+    -- lsp-set-addresses sw0-vm "00:00:01:01:02:04 192.168.1.2/24"
+
+check ovn-nbctl lsp-add public public-rp -- set Logical_Switch_Port public-rp \
+    type=router options:router-port=rp-public \
+    -- lsp-set-addresses public-rp router
+
+check ovn-nbctl lsp-add public public-vm \
+   -- lsp-set-addresses public-vm "00:00:02:01:02:04 172.16.1.2/24"
+
+ADD_NAMESPACES(sw0-vm)
+ADD_VETH(sw0-vm, sw0-vm, br-int, "192.168.1.2/24", "00:00:01:01:02:04", \
+         "192.168.1.1")
+
+ADD_NAMESPACES(public-vm)
+ADD_VETH(public-vm, public-vm, br-int, "172.16.1.2/24", "00:00:02:01:02:04", \
+         "172.16.1.1")
+
+# Start webservers in 'server'.
+OVS_START_L7([sw0-vm], [http])
+
+# Create a load balancer and associate to R1
+check ovn-nbctl lb-add lb1 172.16.1.150:80 192.168.1.2:80 \
+    -- set load_balancer lb1 options:ct_flush="true"
+check ovn-nbctl lr-lb-add R1 lb1
+
+check ovn-nbctl --wait=hv sync
+
+for i in $(seq 1 5); do
+    echo Request $i
+    NS_CHECK_EXEC([public-vm], [wget 172.16.1.150 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+done
+
+OVS_WAIT_FOR_OUTPUT([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.150) | wc -l ], [0], [dnl
+1
+])
+
+check ovn-nbctl --wait=hv lb-del lb1
+
+OVS_WAIT_FOR_OUTPUT([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.150) | wc -l ], [0], [dnl
+0
+])
+
+check ovn-nbctl lb-add lb2 172.16.1.151:80 192.168.1.2:80
+check ovn-nbctl lr-lb-add R1 lb2
+
+check ovn-nbctl --wait=hv sync
+
+for i in $(seq 1 5); do
+    echo Request $i
+    NS_CHECK_EXEC([public-vm], [wget 172.16.1.151 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+done
+
+OVS_WAIT_FOR_OUTPUT([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.151) | wc -l ], [0], [dnl
+1
+])
+
+check ovn-nbctl --wait=hv lb-del lb2
+
+OVS_WAIT_FOR_OUTPUT([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.151) | wc -l ], [0], [dnl
+1
+])
+
+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([NORTHD_TYPE])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/Failed to acquire.*/d
+/connection dropped.*/d"])
+AT_CLEANUP
+])
+
+AT_SETUP([load balancing in gateway router - SCTP])
+AT_SKIP_IF([test $HAVE_SCTP = no])
+AT_SKIP_IF([test $HAVE_NC = no])
+AT_KEYWORDS([ovnlb sctp])
+
+# Make sure the SCTP kernel module is loaded.
+LOAD_MODULE([sctp])
+
+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 the ovn-controller.
+start_daemon ovn-controller
+
+# Logical network:
+# Two LRs - R1 and R2 that are connected to each other via LS "join"
+# in 20.0.0.0/24 network. R1 has switchess foo (192.168.1.0/24) and
+# bar (192.168.2.0/24) connected to it. R2 has alice (172.16.1.0/24) connected
+# to it.  R2 is a gateway router on which we add load-balancing rules.
+#
+#    foo -- R1 -- join - R2 -- alice
+#           |
+#    bar ----
+
+ovn-nbctl create Logical_Router name=R1
+ovn-nbctl create Logical_Router name=R2 options:chassis=hv1
+
+ovn-nbctl ls-add foo
+ovn-nbctl ls-add bar
+ovn-nbctl ls-add alice
+ovn-nbctl ls-add join
+
+# Connect foo to R1
+ovn-nbctl lrp-add R1 foo 00:00:01:01:02:03 192.168.1.1/24
+ovn-nbctl lsp-add foo rp-foo -- set Logical_Switch_Port rp-foo \
+    type=router options:router-port=foo addresses=\"00:00:01:01:02:03\"
+
+# Connect bar to R1
+ovn-nbctl lrp-add R1 bar 00:00:01:01:02:04 192.168.2.1/24
+ovn-nbctl lsp-add bar rp-bar -- set Logical_Switch_Port rp-bar \
+    type=router options:router-port=bar addresses=\"00:00:01:01:02:04\"
+
+# Connect alice to R2
+ovn-nbctl lrp-add R2 alice 00:00:02:01:02:03 172.16.1.1/24
+ovn-nbctl lsp-add alice rp-alice -- set Logical_Switch_Port rp-alice \
+    type=router options:router-port=alice addresses=\"00:00:02:01:02:03\"
+
+# Connect R1 to join
+ovn-nbctl lrp-add R1 R1_join 00:00:04:01:02:03 20.0.0.1/24
+ovn-nbctl lsp-add join r1-join -- set Logical_Switch_Port r1-join \
+    type=router options:router-port=R1_join addresses='"00:00:04:01:02:03"'
+
+# Connect R2 to join
+ovn-nbctl lrp-add R2 R2_join 00:00:04:01:02:04 20.0.0.2/24
+ovn-nbctl lsp-add join r2-join -- set Logical_Switch_Port r2-join \
+    type=router options:router-port=R2_join addresses='"00:00:04:01:02:04"'
+
+# Static routes.
+ovn-nbctl lr-route-add R1 172.16.1.0/24 20.0.0.2
+ovn-nbctl lr-route-add R2 192.168.0.0/16 20.0.0.1
+
+# Logical port 'foo1' in switch 'foo'.
+ADD_NAMESPACES(foo1)
+ADD_VETH(foo1, foo1, br-int, "192.168.1.2/24", "f0:00:00:01:02:03", \
+         "192.168.1.1")
+ovn-nbctl lsp-add foo foo1 \
+-- lsp-set-addresses foo1 "f0:00:00:01:02:03 192.168.1.2"
+
+# Logical port 'alice1' in switch 'alice'.
+ADD_NAMESPACES(alice1)
+ADD_VETH(alice1, alice1, br-int, "172.16.1.2/24", "f0:00:00:01:02:04", \
+         "172.16.1.1")
+ovn-nbctl lsp-add alice alice1 \
+-- lsp-set-addresses alice1 "f0:00:00:01:02:04 172.16.1.2"
+
+# Logical port 'bar1' in switch 'bar'.
+ADD_NAMESPACES(bar1)
+ADD_VETH(bar1, bar1, br-int, "192.168.2.2/24", "f0:00:00:01:02:05", \
+"192.168.2.1")
+ovn-nbctl lsp-add bar bar1 \
+-- lsp-set-addresses bar1 "f0:00:00:01:02:05 192.168.2.2"
+
+# Config OVN load-balancer with a VIP.
+uuid=`ovn-nbctl  create load_balancer protocol=sctp vips:30.0.0.1="192.168.1.2,192.168.2.2"`
+ovn-nbctl set logical_router R2 load_balancer=$uuid
+
+# Config OVN load-balancer with another VIP (this time with ports).
+ovn-nbctl set load_balancer $uuid vips:'"30.0.0.2:8000"'='"192.168.1.2:12345,192.168.2.2:12345"'
+
+# Add SNAT rule to make sure that Load-balancing still works with a SNAT rule.
+ovn-nbctl -- --id=@nat create nat type="snat" logical_ip=192.168.2.2 \
+    external_ip=30.0.0.2 -- add logical_router R2 nat @nat
+
+# Wait for ovn-controller to catch up.
+ovn-nbctl --wait=hv sync
+OVS_WAIT_UNTIL([ovs-ofctl -O OpenFlow13 dump-groups br-int | \
+grep 'nat(dst=192.168.2.2:12345)'])
+
+# Start webservers in 'foo1', 'bar1'.
+OVS_START_L7([foo1], [sctp])
+OVS_START_L7([bar1], [sctp])
+
+on_exit "ovs-ofctl -O OpenFlow13 dump-flows br-int"
+
+dnl Should work with the virtual IP address through NAT
+for i in `seq 1 20`; do
+    echo Request $i
+    NS_CHECK_EXEC([alice1], [nc --sctp --recv-only 30.0.0.1 12345 > client$i.log])
+done
+
+dnl Each server should have at least one connection.
+AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) |
+sed -e 's/zone=[[0-9]]*/zone=<cleared>/' |
+sed 's/,protoinfo=.*$//' | uniq], [0], [dnl
+sctp,orig=(src=172.16.1.2,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2
+sctp,orig=(src=172.16.1.2,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2
+])
+
+dnl Test load-balancing that includes L4 ports in NAT.
+for i in `seq 1 20`; do
+    echo Request $i
+    NS_CHECK_EXEC([alice1], [nc --sctp --recv-only 30.0.0.2 8000 > clients$i.log])
+done
+
+dnl Each server should have at least one connection.
+AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) |
+sed -e 's/zone=[[0-9]]*/zone=<cleared>/' |
+sed 's/,protoinfo=.*$//' | uniq], [0], [dnl
+sctp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2
+sctp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2
+])
+
+check_est_flows () {
+    n=$(ovs-ofctl dump-flows br-int table=15 | grep "+est" \
+        | grep "ct_mark=$1" | sed -n 's/.*n_packets=\([[0-9]]\{1,\}\).*/\1/p')
+
+    echo "n_packets=$n"
+    test -n "$n" && test "$n" != "0"
+}
+
+OVS_WAIT_UNTIL([check_est_flows 0x2], [check established flows])
+
+
+ovn-nbctl set logical_router R2 options:lb_force_snat_ip="20.0.0.2"
+
+# Destroy the load balancer and create again. ovn-controller will
+# clear the OF flows and re add again and clears the n_packets
+# for these flows.
+ovn-nbctl destroy load_balancer $uuid
+uuid=`ovn-nbctl  create load_balancer protocol=sctp vips:30.0.0.1="192.168.1.2,192.168.2.2"`
+ovn-nbctl set logical_router R2 load_balancer=$uuid
+
+check ovs-appctl dpctl/flush-conntrack
+
+# Config OVN load-balancer with another VIP (this time with ports).
+ovn-nbctl set load_balancer $uuid vips:'"30.0.0.2:8000"'='"192.168.1.2:12345,192.168.2.2:12345"'
+
+ovn-nbctl list load_balancer
+ovn-sbctl dump-flows R2
+OVS_WAIT_UNTIL([ovs-ofctl -O OpenFlow13 dump-flows br-int table=45 | grep 'nat(src=20.0.0.2)'])
+
+dnl Test load-balancing that includes L4 ports in NAT.
+for i in `seq 1 20`; do
+    echo Request $i
+    NS_CHECK_EXEC([alice1], [nc --sctp --recv-only 30.0.0.2 8000 > clients$i.log])
+done
+
+dnl Each server should have at least one connection.
+AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) |
+sed -e 's/zone=[[0-9]]*/zone=<cleared>/' |
+sed 's/,protoinfo=.*$//' | uniq], [0], [dnl
+sctp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10
+sctp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10
+])
+
+AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0.2) |
+sed -e 's/zone=[[0-9]]*/zone=<cleared>/' |
+sed 's/,protoinfo=.*$//' | uniq], [0], [dnl
+sctp,orig=(src=172.16.1.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>
+sctp,orig=(src=172.16.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>
+])
+
+OVS_WAIT_UNTIL([check_est_flows 0xa], [check established flows])
+
+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 balancing affinity sessions - auto clear learnt flows])
+AT_SKIP_IF([test $HAVE_NC = no])
+AT_KEYWORDS([lb])
+
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+check ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+start_daemon ovn-controller
+
+check ovn-nbctl lr-add lr
+check ovn-nbctl lrp-add lr lr-ls 00:00:00:00:01:00 42.42.42.3/24
+check ovn-nbctl ls-add ls
+
+check ovn-nbctl lsp-add ls ls-lr
+check ovn-nbctl lsp-set-addresses ls-lr 00:00:00:00:01:00
+check ovn-nbctl lsp-set-type ls-lr router
+check ovn-nbctl lsp-set-options ls-lr router-port=lr-ls
+check ovn-nbctl lsp-add ls vm1
+check ovn-nbctl lsp-set-addresses vm1 00:00:00:00:00:01
+check ovn-nbctl lsp-add ls vm2
+check ovn-nbctl lsp-set-addresses vm2 00:00:00:00:00:02
+check ovn-nbctl lb-add lb-test 43.43.43.43:80 42.42.42.1:8080,42.42.42.2:8080 tcp \
+    -- set load_balancer lb-test options:affinity_timeout=65535 \
+    -- ls-lb-add ls lb-test
+
+dnl Start a server on vm1.
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "42.42.42.1/24", "00:00:00:00:00:01", "42.42.42.3")
+NETNS_DAEMONIZE([vm1], [nc -l -k 42.42.42.1 8080], [vm1.pid])
+
+dnl Start a server on vm2.
+ADD_NAMESPACES(vm2)
+ADD_VETH(vm2, vm2, br-int, "42.42.42.2/24", "00:00:00:00:00:02", "42.42.42.3")
+NETNS_DAEMONIZE([vm2], [nc -l -k 42.42.42.2 8080], [vm2.pid])
+
+dnl Wait for ovn-controller to catch up.
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+dnl Test the connection.
+OVS_WAIT_UNTIL([
+    ip netns exec vm1 nc -z 43.43.43.43 80 &> /dev/null
+])
+
+OVS_WAIT_UNTIL([test $(ovs-ofctl dump-flows br-int | grep 'table=78, n_packets' -c) -eq 1])
+
+dnl Find the backend that was hit.
+backend=$(ovs-ofctl dump-flows br-int table=78 | \
+    grep -oE 'load:0x2a2a2a0[[12]]' | sed -n 's/load:0x2a2a2a0\(.*\)/\1/p')
+
+dnl Remove the backend that was hit.
+if [[ "$backend" == "1" ]]; then
+    check ovn-nbctl set load_balancer lb-test vip:\"43.43.43.43:80\"=\"42.42.42.2:8080\"
+else
+    check ovn-nbctl set load_balancer lb-test vip:\"43.43.43.43:80\"=\"42.42.42.1:8080\"
+fi
+check ovn-nbctl --wait=hv sync
+
+dnl The learnt flow should also be auto deleted.
+AT_CHECK([ovs-ofctl dump-flows br-int | grep 'table=78, n_packets' -c], [1], [dnl
+0
+])
+
+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 1f1e27b51..aaf2825ed 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -1300,11 +1300,11 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
 
     /* Initialize group ids. */
     struct ovn_extend_table group_table;
-    ovn_extend_table_init(&group_table);
+    ovn_extend_table_init(&group_table, "group-table", OFPG_MAX);
 
     /* Initialize meter ids for QoS. */
     struct ovn_extend_table meter_table;
-    ovn_extend_table_init(&meter_table);
+    ovn_extend_table_init(&meter_table, "meter-table", OFPM13_MAX);
 
     /* Initialize collector sets. */
     struct flow_collector_ids collector_ids;
diff --git a/utilities/checkpatch.py b/utilities/checkpatch.py
index 5467d604d..52d3fa845 100755
--- a/utilities/checkpatch.py
+++ b/utilities/checkpatch.py
@@ -40,6 +40,15 @@ MAX_LINE_LEN = 79
 def open_spell_check_dict():
     import enchant
 
+    try:
+        import codespell_lib
+        codespell_dir = os.path.dirname(codespell_lib.__file__)
+        codespell_file = os.path.join(codespell_dir, 'data', 'dictionary.txt')
+        if not os.path.exists(codespell_file):
+            codespell_file = ''
+    except:
+        codespell_file = ''
+
     try:
         extra_keywords = ['ovs', 'vswitch', 'vswitchd', 'ovs-vswitchd',
                           'netdev', 'selinux', 'ovs-ctl', 'dpctl', 'ofctl',
@@ -92,9 +101,18 @@ def open_spell_check_dict():
                           'syscall', 'lacp', 'ipf', 'skb', 'valgrind']
 
         global spell_check_dict
+
         spell_check_dict = enchant.Dict("en_US")
+
+        if codespell_file:
+            with open(codespell_file) as f:
+                for line in f.readlines():
+                    words = line.strip().split('>')[1].strip(', ').split(',')
+                    for word in words:
+                        spell_check_dict.add_to_session(word.strip())
+
         for kw in extra_keywords:
-            spell_check_dict.add(kw)
+            spell_check_dict.add_to_session(kw)
 
         return True
     except:
@@ -190,6 +208,7 @@ skip_trailing_whitespace_check = False
 skip_gerrit_change_id_check = False
 skip_block_whitespace_check = False
 skip_signoff_check = False
+skip_committer_signoff_check = False
 
 # Don't enforce character limit on files that include these characters in their
 # name, as they may have legitimate reasons to have longer lines.
@@ -282,9 +301,13 @@ def if_and_for_end_with_bracket_check(line):
         if len(line) == MAX_LINE_LEN - 1 and line[-1] == ')':
             return True
 
-        if __regex_ends_with_bracket.search(line) is None and \
-           __regex_if_macros.match(line) is None:
-            return False
+        if __regex_ends_with_bracket.search(line) is None:
+            if line.endswith("\\") and \
+               __regex_if_macros.match(line) is not None:
+                return True
+            else:
+                return False
+
     if __regex_conditional_else_bracing.match(line) is not None:
         return False
     if __regex_conditional_else_bracing2.match(line) is not None:
@@ -410,9 +433,15 @@ def check_spelling(line, comment):
     if not spell_check_dict or not spellcheck:
         return False
 
+    if line.startswith('Fixes: '):
+        return False
+
     words = filter_comments(line, True) if comment else line
     words = words.replace(':', ' ').split(' ')
 
+    flagged_words = []
+    num_suggestions = 3
+
     for word in words:
         skip = False
         strword = re.subn(r'\W+', '', word)[0].replace(',', '')
@@ -437,9 +466,15 @@ def check_spelling(line, comment):
                 skip = True
 
             if not skip:
-                print_warning("Check for spelling mistakes (e.g. \"%s\")"
-                              % strword)
-                return True
+                flagged_words.append(strword)
+
+    if len(flagged_words) > 0:
+        for mistake in flagged_words:
+            print_warning("Possible misspelled word: \"%s\"" % mistake)
+            print("Did you mean: ",
+                  spell_check_dict.suggest(mistake)[:num_suggestions])
+
+        return True
 
     return False
 
@@ -780,6 +815,36 @@ def run_file_checks(text):
             check['check'](text)
 
 
+def run_subject_checks(subject, spellcheck=False):
+    warnings = False
+
+    if spellcheck and check_spelling(subject, False):
+        warnings = True
+
+    summary = subject[subject.rindex(': ') + 2:]
+    area_summary = subject[subject.index(': ') + 2:]
+    area_summary_len = len(area_summary)
+    if area_summary_len > 70:
+        print_warning("The subject, '<area>: <summary>', is over 70 "
+                      "characters, i.e., %u." % area_summary_len)
+        warnings = True
+
+    if summary[0].isalpha() and summary[0].islower():
+        print_warning(
+            "The subject summary should start with a capital.")
+        warnings = True
+
+    if subject[-1] not in [".", "?", "!"]:
+        print_warning(
+            "The subject summary should end with a dot.")
+        warnings = True
+
+    if warnings:
+        print(subject)
+
+    return warnings
+
+
 def ovs_checkpatch_parse(text, filename, author=None, committer=None):
     global print_file_name, total_line, checking_file, \
         empty_return_check_state
@@ -800,6 +865,7 @@ def ovs_checkpatch_parse(text, filename, author=None, committer=None):
         r'^@@ ([0-9-+]+),([0-9-+]+) ([0-9-+]+),([0-9-+]+) @@')
     is_author = re.compile(r'^(Author|From): (.*)$', re.I | re.M | re.S)
     is_committer = re.compile(r'^(Commit: )(.*)$', re.I | re.M | re.S)
+    is_subject = re.compile(r'^(Subject: )(.*)$', re.I | re.M | re.S)
     is_signature = re.compile(r'^(Signed-off-by: )(.*)$',
                               re.I | re.M | re.S)
     is_co_author = re.compile(r'^(Co-authored-by: )(.*)$',
@@ -874,7 +940,8 @@ def ovs_checkpatch_parse(text, filename, author=None, committer=None):
                             break
                     if (committer
                         and author != committer
-                        and committer not in signatures):
+                        and committer not in signatures
+                        and not skip_committer_signoff_check):
                         print_error("Committer %s needs to sign off."
                                     % committer)
 
@@ -899,6 +966,8 @@ def ovs_checkpatch_parse(text, filename, author=None, committer=None):
                 committer = is_committer.match(line).group(2)
             elif is_author.match(line):
                 author = is_author.match(line).group(2)
+            elif is_subject.match(line):
+                run_subject_checks(line, spellcheck)
             elif is_signature.match(line):
                 m = is_signature.match(line)
                 signatures.append(m.group(2))
@@ -990,7 +1059,8 @@ Check options:
 -S|--spellcheck                Check C comments and commit-message for possible
                                spelling mistakes
 -t|--skip-trailing-whitespace  Skips the trailing whitespace test
-   --skip-gerrit-change-id     Skips the gerrit change id test"""
+   --skip-gerrit-change-id     Skips the gerrit change id test
+   --skip-committer-signoff    Skips the committer sign-off test"""
           % sys.argv[0])
 
 
@@ -1017,6 +1087,19 @@ def ovs_checkpatch_file(filename):
     result = ovs_checkpatch_parse(part.get_payload(decode=False), filename,
                                   mail.get('Author', mail['From']),
                                   mail['Commit'])
+
+    if not mail['Subject'] or not mail['Subject'].strip():
+        if mail['Subject']:
+            mail.replace_header('Subject', sys.argv[-1])
+        else:
+            mail.add_header('Subject', sys.argv[-1])
+
+        print("Subject missing! Your provisional subject is",
+              mail['Subject'])
+
+    if run_subject_checks('Subject: ' + mail['Subject'], spellcheck):
+        result = True
+
     ovs_checkpatch_print_result()
     return result
 
@@ -1048,6 +1131,7 @@ if __name__ == '__main__':
                                        "skip-signoff-lines",
                                        "skip-trailing-whitespace",
                                        "skip-gerrit-change-id",
+                                       "skip-committer-signoff",
                                        "spellcheck",
                                        "quiet"])
     except:
@@ -1068,6 +1152,8 @@ if __name__ == '__main__':
             skip_trailing_whitespace_check = True
         elif o in ("--skip-gerrit-change-id"):
             skip_gerrit_change_id_check = True
+        elif o in ("--skip-committer-signoff"):
+            skip_committer_signoff_check = True
         elif o in ("-f", "--check-file"):
             checking_file = True
         elif o in ("-S", "--spellcheck"):
diff --git a/utilities/containers/fedora/Dockerfile b/utilities/containers/fedora/Dockerfile
index 4058d7f5b..c11ea37b7 100755
--- a/utilities/containers/fedora/Dockerfile
+++ b/utilities/containers/fedora/Dockerfile
@@ -1,4 +1,4 @@
-FROM quay.io/fedora/fedora:latest
+FROM quay.io/fedora/fedora:38
 
 ARG CONTAINERS_PATH
 
diff --git a/utilities/containers/py-requirements.txt b/utilities/containers/py-requirements.txt
index 0d90765c9..aac98443b 100644
--- a/utilities/containers/py-requirements.txt
+++ b/utilities/containers/py-requirements.txt
@@ -1,5 +1,4 @@
-flake8
-hacking>=3.0
+flake8==5.0.4
 scapy
 sphinx
 setuptools
diff --git a/utilities/containers/ubuntu/Dockerfile b/utilities/containers/ubuntu/Dockerfile
index 5d5bedbd9..3c7fe7775 100755
--- a/utilities/containers/ubuntu/Dockerfile
+++ b/utilities/containers/ubuntu/Dockerfile
@@ -1,4 +1,4 @@
-FROM registry.hub.docker.com/library/ubuntu:latest
+FROM registry.hub.docker.com/library/ubuntu:22.04
 
 ARG CONTAINERS_PATH
 
diff --git a/utilities/ovn-ctl.8.xml b/utilities/ovn-ctl.8.xml
index 82804096f..01f4aa26b 100644
--- a/utilities/ovn-ctl.8.xml
+++ b/utilities/ovn-ctl.8.xml
@@ -136,12 +136,14 @@
     <p><code>--db-nb-cluster-remote-addr=<var>IP ADDRESS</var></code></p>
     <p><code>--db-nb-cluster-remote-port=<var>PORT NUMBER</var></code></p>
     <p><code>--db-nb-cluster-remote-proto=<var>PROTO (tcp/ssl)</var></code></p>
+    <p><code>--db-nb-election-timer=<var>Timeout in milliseconds</var></code></p>
     <p><code>--db-sb-cluster-local-addr=<var>IP ADDRESS</var></code></p>
     <p><code>--db-sb-cluster-local-port=<var>PORT NUMBER</var></code></p>
     <p><code>--db-sb-cluster-local-proto=<var>PROTO (tcp/ssl)</var></code></p>
     <p><code>--db-sb-cluster-remote-addr=<var>IP ADDRESS</var></code></p>
     <p><code>--db-sb-cluster-remote-port=<var>PORT NUMBER</var></code></p>
     <p><code>--db-sb-cluster-remote-proto=<var>PROTO (tcp/ssl)</var></code></p>
+    <p><code>--db-sb-election-timer=<var>Timeout in milliseconds</var></code></p>
     <p><code>--db-ic-nb-cluster-local-addr=<var>IP ADDRESS</var></code></p>
     <p><code>--db-ic-nb-cluster-local-port=<var>PORT NUMBER</var></code></p>
     <p><code>--db-ic-nb-cluster-local-proto=<var>PROTO (tcp/ssl)</var></code></p>
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index 444fbd2fe..821ad44dd 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -4693,6 +4693,7 @@ static void
             nexthop = normalize_prefix_str(ctx->argv[3]);
             if (!nexthop) {
                 ctl_error(ctx, "bad nexthop argument: %s", ctx->argv[3]);
+                free(prefix);
                 return;
             }
         }
diff --git a/utilities/ovn-sbctl.c b/utilities/ovn-sbctl.c
index 3948cae3f..f1f8c2b42 100644
--- a/utilities/ovn-sbctl.c
+++ b/utilities/ovn-sbctl.c
@@ -396,7 +396,9 @@ pre_get_info(struct ctl_context *ctx)
     ovsdb_idl_add_column(ctx->idl, &sbrec_mac_binding_col_mac);
 
     ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_datapaths);
+    /* datapath_group column is deprecated. */
     ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_datapath_group);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_ls_datapath_group);
     ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_vips);
     ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_name);
     ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_protocol);
@@ -932,10 +934,15 @@ cmd_lflow_list_load_balancers(struct ctl_context *ctx, struct vconn *vconn,
                     break;
                 }
             }
+            /* datapath_group column is deprecated. */
             if (lb->datapath_group && !dp_found) {
                 dp_found = datapath_group_contains_datapath(lb->datapath_group,
                                                             datapath);
             }
+            if (lb->ls_datapath_group && !dp_found) {
+                dp_found = datapath_group_contains_datapath(
+                        lb->ls_datapath_group, datapath);
+            }
             if (!dp_found) {
                 continue;
             }
@@ -954,11 +961,17 @@ cmd_lflow_list_load_balancers(struct ctl_context *ctx, struct vconn *vconn,
                 print_vflow_datapath_name(lb->datapaths[i], true,
                                           &ctx->output);
             }
+            /* datapath_group column is deprecated. */
             for (size_t i = 0; lb->datapath_group
                                && i < lb->datapath_group->n_datapaths; i++) {
                 print_vflow_datapath_name(lb->datapath_group->datapaths[i],
                                           true, &ctx->output);
             }
+            for (size_t i = 0; lb->ls_datapath_group
+                    && i < lb->ls_datapath_group->n_datapaths; i++) {
+                print_vflow_datapath_name(lb->ls_datapath_group->datapaths[i],
+                                          true, &ctx->output);
+            }
         }
 
         ds_put_cstr(&ctx->output, "\n  vips:\n");
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index 0b86eae7b..13ae464ad 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -983,6 +983,7 @@ parse_lflow_for_datapath(const struct sbrec_logical_flow *sblf,
         if (error) {
             VLOG_WARN("%s: parsing expression failed (%s)",
                       sblf->match, error);
+            expr_destroy(match);
             free(error);
             return;
         }