Repository: antrea-io/antrea
Pull request #4051 (2022-07-22 03:15:54)

Automated cherry pick of #3882: Add a new option --service-cidr
Cherry pick of #3882 on release-1.7.

#3882: Add a new option --service-cidr

For details on the cherry pick process, see the [cherry pick requests](https://github.com/antrea-io/antrea/blob/main/docs/contributors/cherry-picks.md) page.

codecov[bot]: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/4051?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) Report
> Merging [#4051](https://codecov.io/gh/antrea-io/antrea/pull/4051?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (28bac23) into [release-1.7](https://codecov.io/gh/antrea-io/antrea/commit/c23c6ea781b38181ede87d43c1db6d817684d142?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (c23c6ea) will **decrease** coverage by `19.15%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/antrea-io/antrea/pull/4051/graphs/tree.svg?width=650&height=150&src=pr&token=TjWaLL2GoC&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io)](https://codecov.io/gh/antrea-io/antrea/pull/4051?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io)

```diff
@@               Coverage Diff                @@
##           release-1.7    #4051       +/-   ##
================================================
- Coverage        63.62%   44.47%   -19.16%     
================================================
  Files              293      224       -69     
  Lines            43405    39457     -3948     
================================================
- Hits             27616    17548    -10068     
- Misses           13558    20545     +6987     
+ Partials          2231     1364      -867     
```

| Flag | Coverage Δ | |
|---|---|---|
| kind-e2e-tests | `?` | |
| unit-tests | `44.47% <ø> (-0.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/4051?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | Coverage Δ | |
|---|---|---|
| [pkg/ovs/openflow/default.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL292cy9vcGVuZmxvdy9kZWZhdWx0Lmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/util/runtime/runtime.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL3V0aWwvcnVudGltZS9ydW50aW1lLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/agent/openflow/groups.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L29wZW5mbG93L2dyb3Vwcy5nbw==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/agent/nodeportlocal/k8s/annotations.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L25vZGVwb3J0bG9jYWwvazhzL2Fubm90YXRpb25zLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/ovs/openflow/ofctrl\_pipeline.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL292cy9vcGVuZmxvdy9vZmN0cmxfcGlwZWxpbmUuZ28=) | `0.00% <0.00%> (-95.84%)` | :arrow_down: |
| [pkg/ovs/openflow/logs.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL292cy9vcGVuZmxvdy9sb2dzLmdv) | `10.34% <0.00%> (-89.66%)` | :arrow_down: |
| [pkg/agent/controller/networkpolicy/reject.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvbmV0d29ya3BvbGljeS9yZWplY3QuZ28=) | `0.00% <0.00%> (-85.88%)` | :arrow_down: |
| [pkg/apis/controlplane/register.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FwaXMvY29udHJvbHBsYW5lL3JlZ2lzdGVyLmdv) | `0.00% <0.00%> (-83.34%)` | :arrow_down: |
| [pkg/agent/client.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NsaWVudC5nbw==) | `0.00% <0.00%> (-76.75%)` | :arrow_down: |
| [pkg/flowaggregator/certificate.go](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2Zsb3dhZ2dyZWdhdG9yL2NlcnRpZmljYXRlLmdv) | `0.00% <0.00%> (-75.53%)` | :arrow_down: |
| ... and [202 more](https://codecov.io/gh/antrea-io/antrea/pull/4051/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | |


Patch:
diff --git a/Makefile b/Makefile
index e0bdb7624..06e91d9bb 100644
--- a/Makefile
+++ b/Makefile
@@ -10,9 +10,11 @@ DOCKER_CACHE       := $(CURDIR)/.cache
 ANTCTL_BINARY_NAME ?= antctl
 OVS_VERSION        := $(shell head -n 1 build/images/deps/ovs-version)
 GO_VERSION         := $(shell head -n 1 build/images/deps/go-version)
+BUILD_TAG          := $(shell build/images/build-tag.sh)
 
-DOCKER_BUILD_ARGS = --build-arg OVS_VERSION=$(OVS_VERSION)
+DOCKER_BUILD_ARGS := --build-arg OVS_VERSION=$(OVS_VERSION)
 DOCKER_BUILD_ARGS += --build-arg GO_VERSION=$(GO_VERSION)
+DOCKER_BUILD_ARGS += --build-arg BUILD_TAG=$(BUILD_TAG)
 
 .PHONY: all
 all: build
diff --git a/build/charts/antrea/templates/agent/daemonset.yaml b/build/charts/antrea/templates/agent/daemonset.yaml
index 9cee03c60..59e605766 100644
--- a/build/charts/antrea/templates/agent/daemonset.yaml
+++ b/build/charts/antrea/templates/agent/daemonset.yaml
@@ -65,12 +65,29 @@ spec:
       {{- end }}
       serviceAccountName: antrea-agent
       initContainers:
+        {{- if .Values.whereabouts.enable }}
+        - name: install-whereabouts-config
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+          resources:
+            requests:
+              cpu: "100m"
+          command: ["install_whereabouts_config"]
+          volumeMounts:
+          - name: whereabouts-cni-conf
+            mountPath: /host/etc/cni/net.d/whereabouts.d
+          - name: whereabouts-secret
+            mountPath: /var/run/secrets/whereabouts
+        {{- end }}
+      {{- if eq .Values.trafficEncapMode "networkPolicyOnly" }}
+      containers:
+      {{- end }}
         - name: install-cni
           image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
           imagePullPolicy: {{ .Values.image.pullPolicy }}
           resources: {{- .Values.agent.installCNI.resources | toYaml | nindent 12 }}
           {{- if eq .Values.trafficEncapMode "networkPolicyOnly" }}
           command: ["install_cni_chaining"]
+          args: ["--monitor"]
           {{- else }}
           command: ["install_cni"]
           {{- end }}
@@ -100,20 +117,9 @@ spec:
           # For changing the default permissions of the run directory.
           - name: host-var-run-antrea
             mountPath: /var/run/antrea
-        {{- if .Values.whereabouts.enable }}
-        - name: install-whereabouts-config
-          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
-          resources:
-            requests:
-              cpu: "100m"
-          command: ["install_whereabouts_config"]
-          volumeMounts:
-          - name: whereabouts-cni-conf
-            mountPath: /host/etc/cni/net.d/whereabouts.d
-          - name: whereabouts-secret
-            mountPath: /var/run/secrets/whereabouts
-        {{- end }}
+      {{- if ne .Values.trafficEncapMode "networkPolicyOnly" }}
       containers:
+      {{- end }}
         - name: antrea-agent
           image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
           imagePullPolicy: {{ .Values.image.pullPolicy }}
@@ -169,12 +175,12 @@ spec:
               name: api
               protocol: TCP
           livenessProbe:
-            exec:
-              command:
-                - /bin/sh
-                - -c
-                - container_liveness_probe agent
-            initialDelaySeconds: 5
+            httpGet:
+              host: localhost
+              path: /livez
+              port: api
+              scheme: HTTPS
+            initialDelaySeconds: 10
             timeoutSeconds: 5
             periodSeconds: 10
             failureThreshold: 5
diff --git a/build/images/Dockerfile.build.coverage b/build/images/Dockerfile.build.coverage
index dd9869bfb..6d86c4dcb 100644
--- a/build/images/Dockerfile.build.coverage
+++ b/build/images/Dockerfile.build.coverage
@@ -1,5 +1,5 @@
 ARG GO_VERSION
-ARG OVS_VERSION
+ARG BUILD_TAG
 FROM golang:${GO_VERSION} as antrea-build
 
 WORKDIR /antrea
@@ -13,7 +13,7 @@ COPY . /antrea
 RUN make antrea-agent antrea-controller antrea-cni antctl-linux antrea-controller-instr-binary antrea-agent-instr-binary antctl-instr-binary
 RUN mv bin/antctl-linux bin/antctl
 
-FROM antrea/base-ubuntu:${OVS_VERSION}
+FROM antrea/base-ubuntu:${BUILD_TAG}
 
 LABEL maintainer="Antrea <projectantrea-dev@googlegroups.com>"
 LABEL description="The Docker image to deploy the Antrea CNI with code coverage measurement enabled (used for testing)."
diff --git a/build/images/Dockerfile.build.ubi b/build/images/Dockerfile.build.ubi
index 29cbb8132..7eb3356c9 100644
--- a/build/images/Dockerfile.build.ubi
+++ b/build/images/Dockerfile.build.ubi
@@ -1,5 +1,5 @@
 ARG GO_VERSION
-ARG OVS_VERSION
+ARG BUILD_TAG
 FROM golang:${GO_VERSION} as antrea-build
 
 WORKDIR /antrea
@@ -13,7 +13,7 @@ COPY . /antrea
 RUN make antrea-agent antrea-controller antrea-cni antctl-linux
 RUN mv bin/antctl-linux bin/antctl
 
-FROM antrea/base-ubi:${OVS_VERSION}
+FROM antrea/base-ubi:${BUILD_TAG}
 
 LABEL maintainer="Antrea <projectantrea-dev@googlegroups.com>"
 LABEL description="The Docker image to deploy the Antrea CNI. "
diff --git a/build/images/Dockerfile.build.ubuntu b/build/images/Dockerfile.build.ubuntu
index cb25d626e..cad5dd09e 100644
--- a/build/images/Dockerfile.build.ubuntu
+++ b/build/images/Dockerfile.build.ubuntu
@@ -1,5 +1,5 @@
 ARG GO_VERSION
-ARG OVS_VERSION
+ARG BUILD_TAG
 FROM golang:${GO_VERSION} as antrea-build
 
 WORKDIR /antrea
@@ -13,7 +13,7 @@ COPY . /antrea
 RUN make antrea-agent antrea-controller antrea-cni antctl-linux
 RUN mv bin/antctl-linux bin/antctl
 
-FROM antrea/base-ubuntu:${OVS_VERSION}
+FROM antrea/base-ubuntu:${BUILD_TAG}
 
 LABEL maintainer="Antrea <projectantrea-dev@googlegroups.com>"
 LABEL description="The Docker image to deploy the Antrea CNI."
diff --git a/build/images/Dockerfile.ubuntu b/build/images/Dockerfile.ubuntu
index f3e4660fc..05db9e4ab 100644
--- a/build/images/Dockerfile.ubuntu
+++ b/build/images/Dockerfile.ubuntu
@@ -1,5 +1,19 @@
-ARG OVS_VERSION
-FROM antrea/base-ubuntu:${OVS_VERSION}
+# Copyright 2022 Antrea Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ARG BUILD_TAG
+FROM antrea/base-ubuntu:${BUILD_TAG}
 
 LABEL maintainer="Antrea <projectantrea-dev@googlegroups.com>"
 LABEL description="The Docker image to deploy the Antrea CNI."
diff --git a/build/images/base/Dockerfile b/build/images/base/Dockerfile
index 71af10f5a..9ae2e1538 100644
--- a/build/images/base/Dockerfile
+++ b/build/images/base/Dockerfile
@@ -1,5 +1,19 @@
-ARG OVS_VERSION
-FROM ubuntu:20.04 as cni-binaries
+# Copyright 2022 Antrea Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ARG BUILD_TAG
+FROM ubuntu:22.04 as cni-binaries
 
 ARG CNI_BINARIES_VERSION
 ARG WHEREABOUTS_VERSION=v0.5.1
@@ -23,7 +37,7 @@ RUN set -eux; \
     wget -q -O - https://github.com/containernetworking/plugins/releases/download/$CNI_BINARIES_VERSION/cni-plugins-linux-${pluginsArch}-$CNI_BINARIES_VERSION.tgz | tar xz -C /opt/cni/bin $CNI_PLUGINS; \
     wget -q -O /opt/cni/bin/whereabouts https://github.com/k8snetworkplumbingwg/whereabouts/releases/download/$WHEREABOUTS_VERSION/whereabouts-${pluginsArch} && chmod +x /opt/cni/bin/whereabouts
 
-FROM antrea/openvswitch:${OVS_VERSION}
+FROM antrea/openvswitch:${BUILD_TAG}
 
 LABEL maintainer="Antrea <projectantrea-dev@googlegroups.com>"
 LABEL description="An Ubuntu based Docker base image for Antrea."
@@ -36,7 +50,7 @@ USER root
 # chmod in the RUN command below instead.
 ADD https://raw.githubusercontent.com/kubernetes-sigs/iptables-wrappers/9e6ce59c864623ea71a6f7d59c35fcb13a919b87/iptables-wrapper-installer.sh /iptables-wrapper-installer.sh
 
-RUN apt-get update && apt-get install -y --no-install-recommends ipset jq && \
+RUN apt-get update && apt-get install -y --no-install-recommends ipset jq inotify-tools && \
     rm -rf /var/lib/apt/lists/* && \
     chmod +x /iptables-wrapper-installer.sh && \
     /iptables-wrapper-installer.sh
diff --git a/build/images/base/Dockerfile.ubi b/build/images/base/Dockerfile.ubi
index 23dce6af5..ee03fa1a4 100644
--- a/build/images/base/Dockerfile.ubi
+++ b/build/images/base/Dockerfile.ubi
@@ -1,5 +1,19 @@
-ARG OVS_VERSION
-FROM ubuntu:20.04 as cni-binaries
+# Copyright 2022 Antrea Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ARG BUILD_TAG
+FROM ubuntu:22.04 as cni-binaries
 
 ARG CNI_BINARIES_VERSION
 ARG WHEREABOUTS_VERSION=v0.5.1
@@ -23,7 +37,7 @@ RUN set -eux; \
     wget -q -O - https://github.com/containernetworking/plugins/releases/download/$CNI_BINARIES_VERSION/cni-plugins-linux-${pluginsArch}-$CNI_BINARIES_VERSION.tgz | tar xz -C /opt/cni/bin $CNI_PLUGINS; \
     wget -q -O /opt/cni/bin/whereabouts https://github.com/k8snetworkplumbingwg/whereabouts/releases/download/$WHEREABOUTS_VERSION/whereabouts-${pluginsArch} && chmod +x /opt/cni/bin/whereabouts
 
-FROM antrea/openvswitch-ubi:${OVS_VERSION}
+FROM antrea/openvswitch-ubi:${BUILD_TAG}
 
 LABEL maintainer="Antrea <projectantrea-dev@googlegroups.com>"
 LABEL description="An UBI8 based Docker base image for Antrea."
diff --git a/build/images/base/build.sh b/build/images/base/build.sh
index 1348242b1..7f16ce987 100755
--- a/build/images/base/build.sh
+++ b/build/images/base/build.sh
@@ -24,7 +24,7 @@ function echoerr {
 }
 
 _usage="Usage: $0 [--pull] [--push] [--platform <PLATFORM>] [--distro [ubuntu|ubi]]
-Build the antrea/base-ubuntu:<OVS_VERSION> image.
+Build the antrea base image.
         --pull                  Always attempt to pull a newer version of the base images
         --push                  Push the built image to the registry
         --platform <PLATFORM>   Target platform for the image if server is multi-platform capable
@@ -90,28 +90,29 @@ THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
 
 pushd $THIS_DIR > /dev/null
 
-OVS_VERSION=$(head -n 1 ../deps/ovs-version)
 CNI_BINARIES_VERSION=$(head -n 1 ../deps/cni-binaries-version)
 
+BUILD_TAG=$(../build-tag.sh)
+
 if $PULL; then
     if [[ ${DOCKER_REGISTRY} == "" ]]; then
-        docker pull $PLATFORM_ARG ubuntu:20.04
+        docker pull $PLATFORM_ARG ubuntu:22.04
     else
-        docker pull ${DOCKER_REGISTRY}/antrea/ubuntu:20.04
-        docker tag ${DOCKER_REGISTRY}/antrea/ubuntu:20.04 ubuntu:20.04
+        docker pull ${DOCKER_REGISTRY}/antrea/ubuntu:22.04
+        docker tag ${DOCKER_REGISTRY}/antrea/ubuntu:22.04 ubuntu:22.04
     fi
 
     if [ "$DISTRO" == "ubuntu" ]; then
         IMAGES_LIST=(
-            "antrea/openvswitch:$OVS_VERSION"
+            "antrea/openvswitch:$BUILD_TAG"
             "antrea/cni-binaries:$CNI_BINARIES_VERSION"
-            "antrea/base-ubuntu:$OVS_VERSION"
+            "antrea/base-ubuntu:$BUILD_TAG"
         )
     elif [ "$DISTRO" == "ubi" ]; then
         IMAGES_LIST=(
-            "antrea/openvswitch-ubi:$OVS_VERSION"
+            "antrea/openvswitch-ubi:$BUILD_TAG"
             "antrea/cni-binaries:$CNI_BINARIES_VERSION"
-            "antrea/base-ubi:$OVS_VERSION"
+            "antrea/base-ubi:$BUILD_TAG"
         )
     fi
     for image in "${IMAGES_LIST[@]}"; do
@@ -131,28 +132,28 @@ docker build $PLATFORM_ARG --target cni-binaries \
        --cache-from antrea/cni-binaries:$CNI_BINARIES_VERSION \
        -t antrea/cni-binaries:$CNI_BINARIES_VERSION \
        --build-arg CNI_BINARIES_VERSION=$CNI_BINARIES_VERSION \
-       --build-arg OVS_VERSION=$OVS_VERSION .
+       --build-arg BUILD_TAG=$BUILD_TAG .
 
 if [ "$DISTRO" == "ubuntu" ]; then
     docker build $PLATFORM_ARG \
            --cache-from antrea/cni-binaries:$CNI_BINARIES_VERSION \
-           --cache-from antrea/base-ubuntu:$OVS_VERSION \
-           -t antrea/base-ubuntu:$OVS_VERSION \
+           --cache-from antrea/base-ubuntu:$BUILD_TAG \
+           -t antrea/base-ubuntu:$BUILD_TAG \
            --build-arg CNI_BINARIES_VERSION=$CNI_BINARIES_VERSION \
-           --build-arg OVS_VERSION=$OVS_VERSION .
+           --build-arg BUILD_TAG=$BUILD_TAG .
 elif [ "$DISTRO" == "ubi" ]; then
     docker build $PLATFORM_ARG \
            --cache-from antrea/cni-binaries:$CNI_BINARIES_VERSION \
-           --cache-from antrea/base-ubuntu:$OVS_VERSION \
-           -t antrea/base-ubi:$OVS_VERSION \
+           --cache-from antrea/base-ubi:$BUILD_TAG \
+           -t antrea/base-ubi:$BUILD_TAG \
            -f Dockerfile.ubi \
            --build-arg CNI_BINARIES_VERSION=$CNI_BINARIES_VERSION \
-           --build-arg OVS_VERSION=$OVS_VERSION .
+           --build-arg BUILD_TAG=$BUILD_TAG .
 fi
 
 if $PUSH; then
     docker push antrea/cni-binaries:$CNI_BINARIES_VERSION
-    docker push antrea/base-$DISTRO:$OVS_VERSION
+    docker push antrea/base-$DISTRO:$BUILD_TAG
 fi
 
 popd > /dev/null
diff --git a/build/images/build-tag.sh b/build/images/build-tag.sh
new file mode 100755
index 000000000..0f8cc05d9
--- /dev/null
+++ b/build/images/build-tag.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+# Copyright 2022 Antrea Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# build_tag is used to generate the tag for all the base images
+# (e.g. antrea/openvswitch) used as part of the build chain to produce the
+# Antrea (Linux) images.
+function build_tag() {
+    local this_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+    local antrea_version=$(head -n 1 $this_dir/../../VERSION | cut -f1,2 -d'.')
+    local tag="antrea-${antrea_version}"
+    echo "$tag"
+}
+
+echo "$(build_tag "$@")"
diff --git a/build/images/ovs/Dockerfile b/build/images/ovs/Dockerfile
index e37d2564f..ae50e7f4d 100644
--- a/build/images/ovs/Dockerfile
+++ b/build/images/ovs/Dockerfile
@@ -1,4 +1,18 @@
-FROM ubuntu:20.04 as ovs-debs
+# Copyright 2022 Antrea Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM ubuntu:22.04 as ovs-debs
 
 # Some patches may not apply cleanly if a non-default version is provided.
 # See build/images/deps/ovs-version for the default version.
@@ -25,10 +39,10 @@ RUN wget -q -O - https://www.openvswitch.org/releases/openvswitch-$OVS_VERSION.t
     cd / && rm -rf /tmp/openvswitch*
 
 
-FROM ubuntu:20.04
+FROM ubuntu:22.04
 
 LABEL maintainer="Antrea <projectantrea-dev@googlegroups.com>"
-LABEL description="A Docker image based on Ubuntu 20.04 which includes Open vSwitch built from source."
+LABEL description="A Docker image based on Ubuntu 22.04 which includes Open vSwitch built from source."
 
 COPY --from=ovs-debs /tmp/ovs-debs/* /tmp/ovs-debs/
 COPY charon-logging.conf /tmp
@@ -39,7 +53,7 @@ COPY charon-logging.conf /tmp
 # final image.
 RUN apt-get update && \
     apt-get install -y --no-install-recommends iptables logrotate libstrongswan-standard-plugins && \
-    (dpkg -i /tmp/ovs-debs/*.deb || apt-get -f -y --no-install-recommends install) && \
+    apt-get -f -y --no-install-recommends install /tmp/ovs-debs/*.deb && \
     rm -rf /var/cache/apt/* /var/lib/apt/lists/* && \
     sed -i "/rotate /a\    #size 100M" /etc/logrotate.d/openvswitch-switch && \
     sed -i "/^.*filelog.*{/r /tmp/charon-logging.conf" /etc/strongswan.d/charon-logging.conf && \
diff --git a/build/images/ovs/Dockerfile.ubi b/build/images/ovs/Dockerfile.ubi
index 12eed251b..0275b2556 100644
--- a/build/images/ovs/Dockerfile.ubi
+++ b/build/images/ovs/Dockerfile.ubi
@@ -37,10 +37,13 @@ COPY charon-logging.conf /tmp
 COPY --from=ovs-rpms /tmp/ovs-rpms/* /tmp/ovs-rpms/
 RUN rm -f /etc/yum.repos.d/* && mv /tmp/CentOS.repo /etc/yum.repos.d/CentOS.repo && \
     curl https://www.centos.org/keys/RPM-GPG-KEY-CentOS-Official -o /etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial && \
-    subscription-manager config --rhsm.manage_repos=0 && \
+    # Disable the default redhat.repo. This substitutes `subscription-manager config --rhsm.manage_repos=0`
+    # as subscription-manager is not supported running in containers.
+    sed -i.bak "s/^manage_repos = .$/manage_repos = 0/g" /etc/rhsm/rhsm.conf && \
     yum clean all -y && yum reinstall yum -y && \
     yum install /tmp/ovs-rpms/* -y && yum install epel-release -y && \
     yum install iptables logrotate strongswan -y && \
     mv /etc/logrotate.d/openvswitch /etc/logrotate.d/openvswitch-switch && \
     sed -i "/rotate /a\    #size 100M" /etc/logrotate.d/openvswitch-switch && \
+    rm /etc/rhsm/rhsm.conf.bak && \
     rm -rf /tmp/* && yum clean all
diff --git a/build/images/ovs/README.md b/build/images/ovs/README.md
index 455e4b8b1..533fc9579 100644
--- a/build/images/ovs/README.md
+++ b/build/images/ovs/README.md
@@ -2,14 +2,13 @@
 
 This directory contains utilities to build a Docker image which includes Open
 vSwitch (OVS) built from source. We build OVS from source because some features
-of Antrea (such as IPsec) require a recent version of OVS, more recent than the
-version included in Ubuntu 20.04. The built image is then used as the base image
-for the Antrea main Docker image.
+of Antrea (such as IPsec) may require a recent version of OVS, more recent than
+the version included in the base distribution. The built image is then used as
+the base image for the Antrea main Docker image.
 
-The image is re-built and pushed to Dockerhub periodically (every 12 hours) by a
-[Github workflow](/.github/workflows/update_ovs_image.yml). Therefore, there
-should be no need to update the registry image manually. If it's needed for any
-reason, you can follow the instructions below.
+The image is re-built and pushed to Dockerhub every time the main branch is
+updated. Therefore, there should be no need to update the registry image
+manually. If it's needed for any reason, you can follow the instructions below.
 
 ## Manually building the image and pushing it to Dockerhub
 
@@ -18,9 +17,5 @@ cd build/images/ovs
 ./build.sh --pull --push
 ```
 
-The image will be pushed to Dockerhub as `antrea/openvswitch:<OVS_VERSION>`. The
-OVS version used is determined by the contents of the
-`build/images/deps/ovs-version` file.
-
 The script will fail if you do not have permission to push to the `antrea`
 Dockerhub repository.
diff --git a/build/images/ovs/apply-patches.sh b/build/images/ovs/apply-patches.sh
index 3d3bead50..eb70265e2 100755
--- a/build/images/ovs/apply-patches.sh
+++ b/build/images/ovs/apply-patches.sh
@@ -100,7 +100,7 @@ if version_lt "$OVS_VERSION" "2.18.0" ; then
 fi
 
 # OVS hardcodes the installation path to /usr/lib/python3.7/dist-packages/ but this location
-# does not seem to be in the Python path in Ubuntu 20.04. There may be a better way to do this,
+# does not seem to be in the Python path in Ubuntu 22.04. There may be a better way to do this,
 # but this seems like an acceptable workaround.
-sed -i 's/python3\.7/python3\.8/' debian/openvswitch-test.install
-sed -i 's/python3\.7/python3\.8/' debian/python3-openvswitch.install
+sed -i 's/python3\.7/python3\.10/' debian/openvswitch-test.install
+sed -i 's/python3\.7/python3\.10/' debian/python3-openvswitch.install
diff --git a/build/images/ovs/build.sh b/build/images/ovs/build.sh
index 3e09006db..4dc13c81c 100755
--- a/build/images/ovs/build.sh
+++ b/build/images/ovs/build.sh
@@ -24,7 +24,7 @@ function echoerr {
 }
 
 _usage="Usage: $0 [--pull] [--push] [--platform <PLATFORM>] [--distro [ubuntu|ubi]]
-Build the antrea/base-ubuntu:<OVS_VERSION> image.
+Build the antrea openvswitch image.
         --pull                  Always attempt to pull a newer version of the base images
         --push                  Push the built image to the registry
         --platform <PLATFORM>   Target platform for the image if server is multi-platform capable
@@ -92,6 +92,8 @@ pushd $THIS_DIR > /dev/null
 
 OVS_VERSION=$(head -n 1 ../deps/ovs-version)
 
+BUILD_TAG=$(../build-tag.sh)
+
 # This is a bit complicated but we make sure that we only build OVS if
 # necessary, and at the moment --cache-from does not play nicely with multistage
 # builds: we need to push the intermediate image to the registry. Note that the
@@ -101,20 +103,20 @@ OVS_VERSION=$(head -n 1 ../deps/ovs-version)
 
 if $PULL; then
     if [[ ${DOCKER_REGISTRY} == "" ]]; then
-        docker pull $PLATFORM_ARG ubuntu:20.04
+        docker pull $PLATFORM_ARG ubuntu:22.04
     else
-        docker pull ${DOCKER_REGISTRY}/antrea/ubuntu:20.04
-        docker tag ${DOCKER_REGISTRY}/antrea/ubuntu:20.04 ubuntu:20.04
+        docker pull ${DOCKER_REGISTRY}/antrea/ubuntu:22.04
+        docker tag ${DOCKER_REGISTRY}/antrea/ubuntu:22.04 ubuntu:22.04
     fi
     if [ "$DISTRO" == "ubuntu" ]; then
         IMAGES_LIST=(
-            "antrea/openvswitch-debs:$OVS_VERSION"
-            "antrea/openvswitch:$OVS_VERSION"
+            "antrea/openvswitch-debs:$BUILD_TAG"
+            "antrea/openvswitch:$BUILD_TAG"
         )
     elif [ "$DISTRO" == "ubi" ]; then
         IMAGES_LIST=(
-            "antrea/openvswitch-rpms:$OVS_VERSION"
-            "antrea/openvswitch-ubi:$OVS_VERSION"
+            "antrea/openvswitch-rpms:$BUILD_TAG"
+            "antrea/openvswitch-ubi:$BUILD_TAG"
         )
     fi
     for image in "${IMAGES_LIST[@]}"; do
@@ -132,37 +134,37 @@ fi
 
 if [ "$DISTRO" == "ubuntu" ]; then
     docker build $PLATFORM_ARG --target ovs-debs \
-           --cache-from antrea/openvswitch-debs:$OVS_VERSION \
-           -t antrea/openvswitch-debs:$OVS_VERSION \
+           --cache-from antrea/openvswitch-debs:$BUILD_TAG \
+           -t antrea/openvswitch-debs:$BUILD_TAG \
            --build-arg OVS_VERSION=$OVS_VERSION .
 
     docker build $PLATFORM_ARG \
-           --cache-from antrea/openvswitch-debs:$OVS_VERSION \
-           --cache-from antrea/openvswitch:$OVS_VERSION \
-           -t antrea/openvswitch:$OVS_VERSION \
+           --cache-from antrea/openvswitch-debs:$BUILD_TAG \
+           --cache-from antrea/openvswitch:$BUILD_TAG \
+           -t antrea/openvswitch:$BUILD_TAG \
            --build-arg OVS_VERSION=$OVS_VERSION .
 elif [ "$DISTRO" == "ubi" ]; then
     docker build $PLATFORM_ARG --target ovs-rpms \
-           --cache-from antrea/openvswitch-rpms:$OVS_VERSION \
-           -t antrea/openvswitch-rpms:$OVS_VERSION \
+           --cache-from antrea/openvswitch-rpms:$BUILD_TAG \
+           -t antrea/openvswitch-rpms:$BUILD_TAG \
            --build-arg OVS_VERSION=$OVS_VERSION \
            -f Dockerfile.ubi .
 
     docker build \
-           --cache-from antrea/openvswitch-rpms:$OVS_VERSION \
-           --cache-from antrea/openvswitch-ubi:$OVS_VERSION \
-           -t antrea/openvswitch-ubi:$OVS_VERSION \
+           --cache-from antrea/openvswitch-rpms:$BUILD_TAG \
+           --cache-from antrea/openvswitch-ubi:$BUILD_TAG \
+           -t antrea/openvswitch-ubi:$BUILD_TAG \
            --build-arg OVS_VERSION=$OVS_VERSION \
            -f Dockerfile.ubi .
 fi
 
 if $PUSH; then
     if [ "$DISTRO" == "ubuntu" ]; then
-        docker push antrea/openvswitch-debs:$OVS_VERSION
-        docker push antrea/openvswitch:$OVS_VERSION
+        docker push antrea/openvswitch-debs:$BUILD_TAG
+        docker push antrea/openvswitch:$BUILD_TAG
     elif [ "$DISTRO" == "ubi" ]; then
-        docker push antrea/openvswitch-rpms:$OVS_VERSION
-        docker push antrea/openvswitch-ubi:$OVS_VERSION
+        docker push antrea/openvswitch-rpms:$BUILD_TAG
+        docker push antrea/openvswitch-ubi:$BUILD_TAG
     fi
 fi
 
diff --git a/build/images/scripts/container_liveness_probe b/build/images/scripts/container_liveness_probe
index a20f09af0..21420633a 100755
--- a/build/images/scripts/container_liveness_probe
+++ b/build/images/scripts/container_liveness_probe
@@ -2,9 +2,7 @@
 
 source daemon_status
 
-if [ $1 == agent ]; then
-    exit 0
-elif [ $1 == ovs ]; then
+if [ $1 == ovs ]; then
     check_ovs_status && exit 0
 elif [ $1 == ovs-ipsec ]; then
     check_ovs_ipsec_status && exit 0
diff --git a/build/images/scripts/install_cni_chaining b/build/images/scripts/install_cni_chaining
index 4047d564f..5dd143eae 100755
--- a/build/images/scripts/install_cni_chaining
+++ b/build/images/scripts/install_cni_chaining
@@ -2,30 +2,134 @@
 
 source logging
 
+HOST_CNI_NET_DIR="/host/etc/cni/net.d"
+
+run_monitor="false"
+
+function usage {
+    echo "install_cni_chaining"
+    echo -e "  -h|--help           Print help message"
+    echo -e "  --monitor           Monitor the CNI conf file and re-apply changes every time it is overwritten"
+}
+
+while (( "$#" )); do
+  case "$1" in
+    -h|--help)
+      usage
+      exit 0
+      ;;
+    --monitor)
+      run_monitor="true"
+      ;;
+    -*|--*) # unsupported flags
+      echo "Error: unsupported flag $1" >&2
+      exit 1
+      ;;
+    *) # standalone arguments are not supported
+      echo "Error: unsupported argument $1" >&2
+      exit 1
+      ;;
+  esac
+  shift
+done
+
 # Find the cni conf file with lowest name
 while true; do
-  cni_conf=$(ls /host/etc/cni/net.d | head -n1)
-  if [[ ! -z $cni_conf ]]; then
+  cni_conf_name=$(ls "$HOST_CNI_NET_DIR" | head -n1)
+  if [[ ! -z $cni_conf_name ]]; then
     break
   fi
-  log_info "CNI conf file not found. Retrying after 2 secs"
+  log_info "install_cni_chaining" "CNI conf file not found. Retrying after 2 secs"
   sleep 2s
 done
-cni_conf="/host/etc/cni/net.d/$cni_conf"
+cni_conf_path="$HOST_CNI_NET_DIR/$cni_conf_name"
 
-if grep -sq "azure" $cni_conf; then
-  sed -i 's/"mode":"bridge",/"mode":"transparent",/g' $cni_conf
-fi
+function update_cni_conf {
+    log_info "install_cni_chaining" "updating CNI conf file $cni_conf_name"
 
-cat $cni_conf | jq '.plugins[] | .type' | grep antrea > /dev/null 2>&1
-if [[ $? != 0 ]]; then
-  content=$(cat $cni_conf | jq '.plugins += [{"type": "antrea"}]')
-  echo "$content" > $cni_conf
-fi
+    # To limit the risk of issue because of race conditions, we use the
+    # following steps:
+    # 1. read the file once and store its contents in a variable
+    # 2. perform the necessary changes on the variable contents
+    # 3. compute the sha of the updated variable contents
+    # 4. write the variable contents back to the file
+    content=$(cat $cni_conf_path)
+
+    echo "$content" | grep -sq "azure"
+    if [[ $? == 0 ]]; then
+        # Note that in more recent AKS versions, transparent is the default:
+        # https://github.com/Azure/azure-container-networking/pull/709
+        content="$(echo "$content" | sed 's/"mode":"bridge",/"mode":"transparent",/g')"
+    fi
+
+    echo "$content" | jq '.plugins[] | .type' | grep -sq antrea
+    if [[ $? != 0 ]]; then
+        content="$(echo "$content" | jq '.plugins += [{"type": "antrea"}]')"
+    fi
+
+    cni_conf_sha="$(echo "$content" | sha256sum | while read -r s _; do echo "$s"; done)"
+
+    echo "$content" > $cni_conf_path
+}
+
+# monitor will start a watch on host's CNI config directory.
+# when we detect a change to the CNI conf file, we call update_cni_conf.
+function monitor {
+    inotifywait -m "$HOST_CNI_NET_DIR" -e create,close_write |
+        while read -r directory action filename; do
+            if [[ "$filename" == "$cni_conf_name" ]]; then
+                log_info "install_cni_chaining" "inotify event in $directory: $action $filename"
+                sha="$(sha256sum "$directory/$filename" | while read -r s _; do echo "$s"; done)"
+                if [[ "$sha" == "" ]]; then
+                    log_warning "install_cni_chaining" "unable to compute sha, file may have been deleted"
+                    continue
+                fi
+                if [[ "$sha" == "$cni_conf_sha" ]]; then
+                    log_info "install_cni_chaining" "sha matches existing one, ignoring event"
+                    continue
+                fi
+                update_cni_conf
+            fi
+        done
+}
+
+# Update conf file the first time
+update_cni_conf
 
 # Install Antrea binary file
 install -m 755 /usr/local/bin/antrea-cni /host/opt/cni/bin/antrea
 
 id
 # Load the OVS kernel module
-modprobe openvswitch || (echo "Failed to load the OVS kernel module from the container, try running 'modprobe openvswitch' on your Nodes"; exit 1)
+modprobe openvswitch || { echo "Failed to load the OVS kernel module from the container, try running 'modprobe openvswitch' on your Nodes"; exit 1;}
+
+if [[ "$run_monitor" == "false" ]]; then
+    exit 0
+fi
+
+SLEEP_PID=
+MONITOR_PID=
+function quit {
+    log_info "install_cni_chaining" "Exiting"
+    # terminate background monitor process
+    if [ "$MONITOR_PID" != "" ]; then kill $MONITOR_PID > /dev/null 2>&1 || true; fi
+    # terminate background sleep process
+    if [ "$SLEEP_PID" != "" ]; then kill $SLEEP_PID > /dev/null 2>&1 || true; fi
+    exit 0
+}
+
+# Do not trap EXIT as it would then ignore the "exit 0" statement in quit and
+# exit with code 128 + SIGNAL
+trap "quit" INT TERM HUP
+
+log_info "install_cni_chaining" "Starting inotify monitor for $cni_conf_name"
+
+monitor &
+MONITOR_PID=$!
+while true; do
+  # sleep so script never finishes
+  # we start sleep in bg so we can trap signals
+  sleep 3600 &
+  SLEEP_PID=$!
+  wait $SLEEP_PID
+done
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 629382c5c..bfe6c1a65 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -3680,6 +3680,7 @@ spec:
           operator: Exists
       serviceAccountName: antrea-agent
       initContainers:
+      containers:
         - name: install-cni
           image: "projects.registry.vmware.com/antrea/antrea-ubuntu:latest"
           imagePullPolicy: IfNotPresent
@@ -3687,6 +3688,7 @@ spec:
             requests:
               cpu: 100m
           command: ["install_cni_chaining"]
+          args: ["--monitor"]
           securityContext:
             capabilities:
               add:
@@ -3713,7 +3715,6 @@ spec:
           # For changing the default permissions of the run directory.
           - name: host-var-run-antrea
             mountPath: /var/run/antrea
-      containers:
         - name: antrea-agent
           image: "projects.registry.vmware.com/antrea/antrea-ubuntu:latest"
           imagePullPolicy: IfNotPresent
@@ -3748,12 +3749,12 @@ spec:
               name: api
               protocol: TCP
           livenessProbe:
-            exec:
-              command:
-                - /bin/sh
-                - -c
-                - container_liveness_probe agent
-            initialDelaySeconds: 5
+            httpGet:
+              host: localhost
+              path: /livez
+              port: api
+              scheme: HTTPS
+            initialDelaySeconds: 10
             timeoutSeconds: 5
             periodSeconds: 10
             failureThreshold: 5
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index 6f5ac9b6b..2f3a3d77e 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -3680,6 +3680,7 @@ spec:
           operator: Exists
       serviceAccountName: antrea-agent
       initContainers:
+      containers:
         - name: install-cni
           image: "projects.registry.vmware.com/antrea/antrea-ubuntu:latest"
           imagePullPolicy: IfNotPresent
@@ -3687,6 +3688,7 @@ spec:
             requests:
               cpu: 100m
           command: ["install_cni_chaining"]
+          args: ["--monitor"]
           securityContext:
             capabilities:
               add:
@@ -3713,7 +3715,6 @@ spec:
           # For changing the default permissions of the run directory.
           - name: host-var-run-antrea
             mountPath: /var/run/antrea
-      containers:
         - name: antrea-agent
           image: "projects.registry.vmware.com/antrea/antrea-ubuntu:latest"
           imagePullPolicy: IfNotPresent
@@ -3750,12 +3751,12 @@ spec:
               name: api
               protocol: TCP
           livenessProbe:
-            exec:
-              command:
-                - /bin/sh
-                - -c
-                - container_liveness_probe agent
-            initialDelaySeconds: 5
+            httpGet:
+              host: localhost
+              path: /livez
+              port: api
+              scheme: HTTPS
+            initialDelaySeconds: 10
             timeoutSeconds: 5
             periodSeconds: 10
             failureThreshold: 5
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index f1629a1fc..232791201 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -3748,12 +3748,12 @@ spec:
               name: api
               protocol: TCP
           livenessProbe:
-            exec:
-              command:
-                - /bin/sh
-                - -c
-                - container_liveness_probe agent
-            initialDelaySeconds: 5
+            httpGet:
+              host: localhost
+              path: /livez
+              port: api
+              scheme: HTTPS
+            initialDelaySeconds: 10
             timeoutSeconds: 5
             periodSeconds: 10
             failureThreshold: 5
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index 58d292741..282e8ecdd 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -3768,12 +3768,12 @@ spec:
               name: api
               protocol: TCP
           livenessProbe:
-            exec:
-              command:
-                - /bin/sh
-                - -c
-                - container_liveness_probe agent
-            initialDelaySeconds: 5
+            httpGet:
+              host: localhost
+              path: /livez
+              port: api
+              scheme: HTTPS
+            initialDelaySeconds: 10
             timeoutSeconds: 5
             periodSeconds: 10
             failureThreshold: 5
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index 94f278b51..ba9b00350 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -3748,12 +3748,12 @@ spec:
               name: api
               protocol: TCP
           livenessProbe:
-            exec:
-              command:
-                - /bin/sh
-                - -c
-                - container_liveness_probe agent
-            initialDelaySeconds: 5
+            httpGet:
+              host: localhost
+              path: /livez
+              port: api
+              scheme: HTTPS
+            initialDelaySeconds: 10
             timeoutSeconds: 5
             periodSeconds: 10
             failureThreshold: 5
diff --git a/ci/kind/kind-setup.sh b/ci/kind/kind-setup.sh
index 1bd32e081..28bfe113b 100755
--- a/ci/kind/kind-setup.sh
+++ b/ci/kind/kind-setup.sh
@@ -23,6 +23,7 @@ ANTREA_IMAGE="projects.registry.vmware.com/antrea/antrea-ubuntu:latest"
 IMAGES=$ANTREA_IMAGE
 ANTREA_CNI=false
 POD_CIDR="10.10.0.0/16"
+SERVICE_CIDR=""
 IP_FAMILY="ipv4"
 NUM_WORKERS=2
 SUBNETS=""
@@ -39,13 +40,14 @@ function echoerr {
 }
 
 _usage="
-Usage: $0 create CLUSTER_NAME [--pod-cidr POD_CIDR] [--antrea-cni] [--num-workers NUM_WORKERS] [--images IMAGES] [--subnets SUBNETS] [--ip-family ipv4|ipv6]
-                  destroy CLUSTER_NAME
-                  help
+Usage: $0 create CLUSTER_NAME [--pod-cidr POD_CIDR] [--service-cidr SERVICE_CIDR]  [--antrea-cni] [--num-workers NUM_WORKERS] [--images IMAGES] [--subnets SUBNETS] [--ip-family ipv4|ipv6]
+       $0 destroy CLUSTER_NAME
+       $0 help
 where:
   create: create a kind cluster with name CLUSTER_NAME
   destroy: delete a kind cluster with name CLUSTER_NAME
   --pod-cidr: specifies pod cidr used in kind cluster, default is $POD_CIDR
+  --service-cidr: specifies service clusterip cidr used in kind cluster, kind's default value will be used if empty.
   --encap-mode: inter-node pod traffic encap mode, default is encap
   --no-proxy: disable Antrea proxy
   --no-kube-proxy: disable Kube proxy
@@ -254,6 +256,7 @@ featureGates:
 networking:
   disableDefaultCNI: true
   podSubnet: $POD_CIDR
+  serviceSubnet: $SERVICE_CIDR
   ipFamily: $IP_FAMILY
   kubeProxyMode: $KUBE_PROXY_MODE
 nodes:
@@ -336,6 +339,10 @@ while [[ $# -gt 0 ]]
       POD_CIDR="$2"
       shift 2
       ;;
+    --service-cidr)
+      SERVICE_CIDR="$2"
+      shift 2
+      ;;
     --ip-family)
       IP_FAMILY="$2"
       shift 2
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index b007107ee..12dd7c8c0 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -24,6 +24,7 @@ import (
 	"k8s.io/apimachinery/pkg/fields"
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/apiserver/pkg/server/options"
 	"k8s.io/client-go/informers"
 	coreinformers "k8s.io/client-go/informers/core/v1"
 	"k8s.io/client-go/tools/cache"
@@ -69,7 +70,6 @@ import (
 	"antrea.io/antrea/pkg/ovs/ovsctl"
 	"antrea.io/antrea/pkg/signals"
 	"antrea.io/antrea/pkg/util/channel"
-	"antrea.io/antrea/pkg/util/cipher"
 	"antrea.io/antrea/pkg/util/env"
 	"antrea.io/antrea/pkg/util/k8s"
 	"antrea.io/antrea/pkg/version"
@@ -374,7 +374,7 @@ func run(o *Options) error {
 			return fmt.Errorf("invalid Node Transport IPAddr in Node config: %v", nodeConfig)
 		}
 		memberlistCluster, err = memberlist.NewCluster(nodeTransportIP, o.config.ClusterMembershipPort,
-			nodeConfig.Name, nodeInformer, externalIPPoolInformer,
+			nodeConfig.Name, nodeInformer, externalIPPoolInformer, nil,
 		)
 		if err != nil {
 			return fmt.Errorf("error creating new memberlist cluster: %v", err)
@@ -685,20 +685,23 @@ func run(o *Options) error {
 
 	go agentMonitor.Run(stopCh)
 
-	cipherSuites, err := cipher.GenerateCipherSuitesList(o.config.TLSCipherSuites)
-	if err != nil {
-		return fmt.Errorf("error generating Cipher Suite list: %v", err)
-	}
+	secureServing := options.NewSecureServingOptions().WithLoopback()
+	secureServing.BindAddress = net.IPv4zero
+	secureServing.BindPort = o.config.APIPort
+	secureServing.CipherSuites = o.tlsCipherSuites
+	secureServing.MinTLSVersion = o.config.TLSMinVersion
+	authentication := options.NewDelegatingAuthenticationOptions()
+	authorization := options.NewDelegatingAuthorizationOptions().WithAlwaysAllowPaths("/healthz", "/livez", "/readyz")
 	apiServer, err := apiserver.New(
 		agentQuerier,
 		networkPolicyController,
 		mcastController,
 		externalIPController,
-		o.config.APIPort,
+		secureServing,
+		authentication,
+		authorization,
 		*o.config.EnablePrometheusMetrics,
 		o.config.ClientConnection.Kubeconfig,
-		cipherSuites,
-		cipher.TLSVersionMap[o.config.TLSMinVersion],
 		v4Enabled,
 		v6Enabled)
 	if err != nil {
diff --git a/cmd/antrea-agent/options.go b/cmd/antrea-agent/options.go
index cf276a678..10f33d4d1 100644
--- a/cmd/antrea-agent/options.go
+++ b/cmd/antrea-agent/options.go
@@ -23,6 +23,7 @@ import (
 
 	"github.com/spf13/pflag"
 	"gopkg.in/yaml.v2"
+	cliflag "k8s.io/component-base/cli/flag"
 	"k8s.io/klog/v2"
 
 	"antrea.io/antrea/pkg/agent/config"
@@ -58,6 +59,8 @@ type Options struct {
 	configFile string
 	// The configuration object
 	config *agentconfig.AgentConfig
+	// tlsCipherSuites is a slice of TLSCipherSuites mapped to input provided by user.
+	tlsCipherSuites []string
 	// IPFIX flow collector address
 	flowCollectorAddr string
 	// IPFIX flow collector protocol
@@ -140,6 +143,10 @@ func (o *Options) validate(args []string) error {
 		return err
 	}
 
+	if err := o.validateTLSOptions(); err != nil {
+		return err
+	}
+
 	if encapMode.SupportsNoEncap() {
 		// When using NoEncap traffic mode without AntreaProxy, Pod-to-Service traffic is handled by kube-proxy
 		// (iptables/ipvs) in the root netns. If the Endpoint is not local the DNATed traffic will be output to
@@ -311,6 +318,23 @@ func (o *Options) setDefaults() {
 	}
 }
 
+func (o *Options) validateTLSOptions() error {
+	_, err := cliflag.TLSVersion(o.config.TLSMinVersion)
+	if err != nil {
+		return fmt.Errorf("invalid TLSMinVersion: %v", err)
+	}
+	trimmedTLSCipherSuites := strings.ReplaceAll(o.config.TLSCipherSuites, " ", "")
+	if trimmedTLSCipherSuites != "" {
+		tlsCipherSuites := strings.Split(trimmedTLSCipherSuites, ",")
+		_, err = cliflag.TLSCipherSuites(tlsCipherSuites)
+		if err != nil {
+			return fmt.Errorf("invalid TLSCipherSuites: %v", err)
+		}
+		o.tlsCipherSuites = tlsCipherSuites
+	}
+	return nil
+}
+
 func (o *Options) validateAntreaProxyConfig() error {
 	if !features.DefaultFeatureGate.Enabled(features.AntreaProxy) {
 		// Validate service CIDR configuration if AntreaProxy is not enabled.
diff --git a/docs/kind.md b/docs/kind.md
index 3dacb4969..32c207f24 100644
--- a/docs/kind.md
+++ b/docs/kind.md
@@ -49,7 +49,8 @@ The `kind-setup.sh` script may execute `kubectl` commands to set up the cluster,
 and requires that `kubectl` be present in your `PATH`.
 
 To specify a different number of worker Nodes, use `--num-workers <NUM>`. To
-create an IPv6 Kind cluster, use `--ip-family ipv6`.
+specify the IP family of the kind cluster, use `--ip-family <ipv4|ipv6>`.
+To specify the Service Cluster IP range, use `--service-cidr <CIDR>`.
 
 If you want to pre-load the Antrea image in each Node (to avoid having each Node
 pull from the registry), you can use:
diff --git a/go.mod b/go.mod
index cd85569e4..7317ada25 100644
--- a/go.mod
+++ b/go.mod
@@ -190,7 +190,3 @@ require (
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
 	sigs.k8s.io/yaml v1.3.0 // indirect
 )
-
-// Newer version of github.com/googleapis/gnostic make use of newer gopkg.in/yaml(v3), which conflicts with
-// explicit imports of gopkg.in/yaml.v2.
-replace github.com/googleapis/gnostic v0.5.5 => github.com/googleapis/gnostic v0.4.1
diff --git a/go.sum b/go.sum
index 0b990a41c..1dddf4da9 100644
--- a/go.sum
+++ b/go.sum
@@ -468,6 +468,7 @@ github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTV
 github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
 github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
 github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
+github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
 github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
diff --git a/hack/build-antrea-linux-all.sh b/hack/build-antrea-linux-all.sh
index bd931040d..9cf787bc1 100755
--- a/hack/build-antrea-linux-all.sh
+++ b/hack/build-antrea-linux-all.sh
@@ -21,9 +21,9 @@ function echoerr {
 }
 
 _usage="Usage: $0 [--pull] [--push-base-images] [--coverage] [--platform <PLATFORM>] [--distro [ubuntu|ubi]]
-Build the antrea/antrea-ubuntu image, as well as all the base images in the build chain. This is
-typically used in CI to build the image with the latest version of all dependencies, taking into
-account changes to all Dockerfiles.
+Build the antrea image, as well as all the base images in the build chain. This is typically used in
+CI to build the image with the latest version of all dependencies, taking into account changes to
+all Dockerfiles.
         --pull                  Always attempt to pull a newer version of the base images.
         --push-base-images      Push built images to the registry. Only base images will be pushed.
         --coverage              Build the image with support for code coverage.
@@ -101,10 +101,12 @@ if [ "$DISTRO" == "ubi" ]; then
     ARGS="$ARGS --distro ubi"
 fi
 
-OVS_VERSION=$(head -n 1 build/images/deps/ovs-version)
 CNI_BINARIES_VERSION=$(head -n 1 build/images/deps/cni-binaries-version)
 GO_VERSION=$(head -n 1 build/images/deps/go-version)
 
+BUILD_TAG=$(build/images/build-tag.sh)
+echo "BUILD_TAG: $BUILD_TAG"
+
 # We pull all images ahead of time, instead of calling the independent build.sh
 # scripts with "--pull". We do not want to overwrite the antrea/openvswitch
 # image we just built when calling build.sh to build the antrea/base-ubuntu
@@ -112,27 +114,27 @@ GO_VERSION=$(head -n 1 build/images/deps/go-version)
 # new base images in the build chain.
 if $PULL; then
     if [[ ${DOCKER_REGISTRY} == "" ]]; then
-        docker pull $PLATFORM_ARG ubuntu:20.04
+        docker pull $PLATFORM_ARG ubuntu:22.04
         docker pull $PLATFORM_ARG golang:$GO_VERSION
     else
-        docker pull ${DOCKER_REGISTRY}/antrea/ubuntu:20.04
-        docker tag ${DOCKER_REGISTRY}/antrea/ubuntu:20.04 ubuntu:20.04
+        docker pull ${DOCKER_REGISTRY}/antrea/ubuntu:22.04
+        docker tag ${DOCKER_REGISTRY}/antrea/ubuntu:22.04 ubuntu:22.04
         docker pull ${DOCKER_REGISTRY}/antrea/golang:$GO_VERSION
         docker tag ${DOCKER_REGISTRY}/antrea/golang:$GO_VERSION golang:$GO_VERSION
     fi
     if [ "$DISTRO" == "ubuntu" ]; then
         IMAGES_LIST=(
-            "antrea/openvswitch-debs:$OVS_VERSION"
-            "antrea/openvswitch:$OVS_VERSION"
+            "antrea/openvswitch-debs:$BUILD_TAG"
+            "antrea/openvswitch:$BUILD_TAG"
             "antrea/cni-binaries:$CNI_BINARIES_VERSION"
-            "antrea/base-ubuntu:$OVS_VERSION"
+            "antrea/base-ubuntu:$BUILD_TAG"
         )
     elif [ "$DISTRO" == "ubi" ]; then
         IMAGES_LIST=(
-            "antrea/openvswitch-rpms:$OVS_VERSION"
-            "antrea/openvswitch-ubi:$OVS_VERSION"
+            "antrea/openvswitch-rpms:$BUILD_TAG"
+            "antrea/openvswitch-ubi:$BUILD_TAG"
             "antrea/cni-binaries:$CNI_BINARIES_VERSION"
-            "antrea/base-ubi:$OVS_VERSION"
+            "antrea/base-ubi:$BUILD_TAG"
         )
     fi
     for image in "${IMAGES_LIST[@]}"; do
diff --git a/hack/update-codegen-dockerized.sh b/hack/update-codegen-dockerized.sh
index 4f59a72e9..83c59ada0 100755
--- a/hack/update-codegen-dockerized.sh
+++ b/hack/update-codegen-dockerized.sh
@@ -42,6 +42,7 @@ function generate_mocks {
     "pkg/agent/cniserver/ipam IPAMDriver testing"
     "pkg/agent/flowexporter/connections ConnTrackDumper,NetFilterConnTrack testing"
     "pkg/agent/interfacestore InterfaceStore testing"
+    "pkg/agent/memberlist Memberlist testing"
     "pkg/agent/multicast RouteInterface testing"
     "pkg/agent/types MulticastValidator testing"
     "pkg/agent/nodeportlocal/portcache LocalPortOpener testing"
@@ -55,7 +56,7 @@ function generate_mocks {
     "pkg/controller/networkpolicy EndpointQuerier testing"
     "pkg/controller/querier ControllerQuerier testing"
     "pkg/ipfix IPFIXExportingProcess,IPFIXRegistry,IPFIXCollectingProcess,IPFIXAggregationProcess testing"
-    "pkg/ovs/openflow Bridge,Table,Flow,Action,CTAction,FlowBuilder testing"
+    "pkg/ovs/openflow Bridge,Table,Flow,Action,CTAction,FlowBuilder,Group,BucketBuilder testing"
     "pkg/ovs/ovsconfig OVSBridgeClient testing"
     "pkg/ovs/ovsctl OVSCtlClient testing"
     "pkg/querier AgentNetworkPolicyInfoQuerier,AgentMulticastInfoQuerier testing"
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index f2d99a557..116eb11e3 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -15,6 +15,7 @@
 package agent
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -73,6 +74,12 @@ var (
 
 	// getTransportIPNetDeviceByName is meant to be overridden for testing.
 	getTransportIPNetDeviceByName = GetTransportIPNetDeviceByName
+
+	// setLinkUp is meant to be overridden for testing
+	setLinkUp = util.SetLinkUp
+
+	// configureLinkAddresses is meant to be overridden for testing
+	configureLinkAddresses = util.ConfigureLinkAddresses
 )
 
 // otherConfigKeysForIPsecCertificates are configurations added to OVS bridge when AuthenticationMode is "cert" and
@@ -228,6 +235,7 @@ func (i *Initializer) initInterfaceStore() error {
 		intf := &interfacestore.InterfaceConfig{
 			Type:          interfacestore.GatewayInterface,
 			InterfaceName: port.Name,
+			MAC:           port.MAC,
 			OVSPortConfig: ovsPort}
 		if intf.InterfaceName != i.hostGateway {
 			klog.Warningf("The discovered gateway interface name %s is different from the configured value: %s",
@@ -256,6 +264,7 @@ func (i *Initializer) initInterfaceStore() error {
 		return intf
 	}
 	ifaceList := make([]*interfacestore.InterfaceConfig, 0, len(ovsPorts))
+	ovsCtlClient := ovsctl.NewClient(i.ovsBridge)
 	for index := range ovsPorts {
 		port := &ovsPorts[index]
 		ovsPort := &interfacestore.OVSPortConfig{
@@ -283,6 +292,9 @@ func (i *Initializer) initInterfaceStore() error {
 				intf = cniserver.ParseOVSPortInterfaceConfig(port, ovsPort, true)
 			case interfacestore.AntreaTrafficControl:
 				intf = trafficcontrol.ParseTrafficControlInterfaceConfig(port, ovsPort)
+				if err := ovsCtlClient.SetPortNoFlood(int(ovsPort.OFPort)); err != nil {
+					klog.ErrorS(err, "Failed to set port with no-flood config", "PortName", port.Name)
+				}
 			default:
 				klog.InfoS("Unknown Antrea interface type", "type", interfaceType)
 			}
@@ -606,7 +618,8 @@ func (i *Initializer) setupGatewayInterface() error {
 		externalIDs := map[string]interface{}{
 			interfacestore.AntreaInterfaceTypeKey: interfacestore.AntreaGateway,
 		}
-		gwPortUUID, err := i.ovsBridgeClient.CreateInternalPort(i.hostGateway, config.HostGatewayOFPort, "", externalIDs)
+		mac := util.GenerateRandomMAC()
+		gwPortUUID, err := i.ovsBridgeClient.CreateInternalPort(i.hostGateway, config.HostGatewayOFPort, mac.String(), externalIDs)
 		if err != nil {
 			klog.ErrorS(err, "Failed to create gateway port on OVS bridge", "port", i.hostGateway)
 			return err
@@ -617,7 +630,7 @@ func (i *Initializer) setupGatewayInterface() error {
 			return err
 		}
 		klog.InfoS("Allocated OpenFlow port for gateway interface", "port", i.hostGateway, "ofPort", gwPort)
-		gatewayIface = interfacestore.NewGatewayInterface(i.hostGateway)
+		gatewayIface = interfacestore.NewGatewayInterface(i.hostGateway, mac)
 		gatewayIface.OVSPortConfig = &interfacestore.OVSPortConfig{PortUUID: gwPortUUID, OFPort: gwPort}
 		i.ifaceStore.AddInterface(gatewayIface)
 	} else {
@@ -625,7 +638,7 @@ func (i *Initializer) setupGatewayInterface() error {
 	}
 
 	// Idempotent operation to set the gateway's MTU: we perform this operation regardless of
-	// whether or not the gateway interface already exists, as the desired MTU may change across
+	// whether the gateway interface already exists, as the desired MTU may change across
 	// restarts.
 	klog.V(4).Infof("Setting gateway interface %s MTU to %d", i.hostGateway, i.nodeConfig.NodeMTU)
 
@@ -646,7 +659,7 @@ func (i *Initializer) configureGatewayInterface(gatewayIface *interfacestore.Int
 	// Host link might not be queried at once after creating OVS internal port; retry max 5 times with 1s
 	// delay each time to ensure the link is ready.
 	for retry := 0; retry < maxRetryForHostLink; retry++ {
-		gwMAC, gwLinkIdx, err = util.SetLinkUp(i.hostGateway)
+		gwMAC, gwLinkIdx, err = setLinkUp(i.hostGateway)
 		if err == nil {
 			break
 		}
@@ -662,9 +675,17 @@ func (i *Initializer) configureGatewayInterface(gatewayIface *interfacestore.Int
 		klog.Errorf("Failed to find host link for gateway %s: %v", i.hostGateway, err)
 		return err
 	}
-
+	// Persist the MAC configured in the network interface when the gatewayIface.MAC is not set. This may
+	// happen in upgrade case.
+	// Note the "mac" field in Windows OVS internal Interface has no impact on the network adapter's actual MAC,
+	// set it to the same value just to keep consistency.
+	if bytes.Compare(gatewayIface.MAC, gwMAC) != 0 {
+		gatewayIface.MAC = gwMAC
+		if err := i.ovsBridgeClient.SetInterfaceMAC(gatewayIface.InterfaceName, gwMAC); err != nil {
+			klog.ErrorS(err, "Failed to persist interface MAC address", "interface", gatewayIface.InterfaceName, "mac", gwMAC)
+		}
+	}
 	i.nodeConfig.GatewayConfig = &config.GatewayConfig{Name: i.hostGateway, MAC: gwMAC, OFPort: uint32(gatewayIface.OFPort)}
-	gatewayIface.MAC = gwMAC
 	gatewayIface.IPs = []net.IP{}
 	if i.networkConfig.TrafficEncapMode.IsNetworkPolicyOnly() {
 		// Assign IP to gw as required by SpoofGuard.
@@ -1092,13 +1113,13 @@ func (i *Initializer) allocateGatewayAddresses(localSubnets []*net.IPNet, gatewa
 	// (i.e. portExists is false). Indeed, it may be possible for the interface to exist even if the OVS bridge does
 	// not exist.
 	// Configure any missing IP address on the interface. Remove any extra IP address that may exist.
-	if err := util.ConfigureLinkAddresses(i.nodeConfig.GatewayConfig.LinkIndex, gwIPs); err != nil {
+	if err := configureLinkAddresses(i.nodeConfig.GatewayConfig.LinkIndex, gwIPs); err != nil {
 		return err
 	}
 	// Periodically check whether IP configuration of the gateway is correct.
 	// Terminate when stopCh is closed.
 	go wait.Until(func() {
-		if err := util.ConfigureLinkAddresses(i.nodeConfig.GatewayConfig.LinkIndex, gwIPs); err != nil {
+		if err := configureLinkAddresses(i.nodeConfig.GatewayConfig.LinkIndex, gwIPs); err != nil {
 			klog.Errorf("Failed to check IP configuration of the gateway: %v", err)
 		}
 	}, 60*time.Second, i.stopCh)
diff --git a/pkg/agent/agent_windows.go b/pkg/agent/agent_windows.go
index 888d82c9b..9ae4318ec 100644
--- a/pkg/agent/agent_windows.go
+++ b/pkg/agent/agent_windows.go
@@ -32,6 +32,11 @@ import (
 	"antrea.io/antrea/pkg/util/ip"
 )
 
+var (
+	// setInterfaceMTU is meant to be overridden for testing
+	setInterfaceMTU = util.SetInterfaceMTU
+)
+
 // prepareHostNetwork creates HNS Network for containers.
 func (i *Initializer) prepareHostNetwork() error {
 	// If the HNS Network already exists, return immediately.
@@ -292,5 +297,5 @@ func (i *Initializer) setInterfaceMTU(iface string, mtu int) error {
 	if err := i.ovsBridgeClient.SetInterfaceMTU(iface, mtu); err != nil {
 		return err
 	}
-	return util.SetInterfaceMTU(iface, mtu)
+	return setInterfaceMTU(iface, mtu)
 }
diff --git a/pkg/agent/apiserver/apiserver.go b/pkg/agent/apiserver/apiserver.go
index 85c5a4653..8687fe85c 100644
--- a/pkg/agent/apiserver/apiserver.go
+++ b/pkg/agent/apiserver/apiserver.go
@@ -99,9 +99,19 @@ func installAPIGroup(s *genericapiserver.GenericAPIServer, aq agentquerier.Agent
 }
 
 // New creates an APIServer for running in antrea agent.
-func New(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, mq querier.AgentMulticastInfoQuerier, seipq querier.ServiceExternalIPStatusQuerier,
-	bindPort int, enableMetrics bool, kubeconfig string, cipherSuites []uint16, tlsMinVersion uint16, v4Enabled, v6Enabled bool) (*agentAPIServer, error) {
-	cfg, err := newConfig(npq, bindPort, enableMetrics, kubeconfig)
+func New(aq agentquerier.AgentQuerier,
+	npq querier.AgentNetworkPolicyInfoQuerier,
+	mq querier.AgentMulticastInfoQuerier,
+	seipq querier.ServiceExternalIPStatusQuerier,
+	secureServing *genericoptions.SecureServingOptionsWithLoopback,
+	authentication *genericoptions.DelegatingAuthenticationOptions,
+	authorization *genericoptions.DelegatingAuthorizationOptions,
+	enableMetrics bool,
+	kubeconfig string,
+	v4Enabled,
+	v6Enabled bool,
+) (*agentAPIServer, error) {
+	cfg, err := newConfig(aq, npq, secureServing, authentication, authorization, enableMetrics, kubeconfig)
 	if err != nil {
 		return nil, err
 	}
@@ -109,8 +119,6 @@ func New(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier
 	if err != nil {
 		return nil, err
 	}
-	s.SecureServingInfo.CipherSuites = cipherSuites
-	s.SecureServingInfo.MinTLSVersion = tlsMinVersion
 	if err := installAPIGroup(s, aq, npq, v4Enabled, v6Enabled); err != nil {
 		return nil, err
 	}
@@ -118,11 +126,14 @@ func New(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier
 	return &agentAPIServer{GenericAPIServer: s}, nil
 }
 
-func newConfig(npq querier.AgentNetworkPolicyInfoQuerier, bindPort int, enableMetrics bool, kubeconfig string) (*genericapiserver.CompletedConfig, error) {
-	secureServing := genericoptions.NewSecureServingOptions().WithLoopback()
-	authentication := genericoptions.NewDelegatingAuthenticationOptions()
-	authorization := genericoptions.NewDelegatingAuthorizationOptions().WithAlwaysAllowPaths("/healthz", "/livez", "/readyz")
-
+func newConfig(aq agentquerier.AgentQuerier,
+	npq querier.AgentNetworkPolicyInfoQuerier,
+	secureServing *genericoptions.SecureServingOptionsWithLoopback,
+	authentication *genericoptions.DelegatingAuthenticationOptions,
+	authorization *genericoptions.DelegatingAuthorizationOptions,
+	enableMetrics bool,
+	kubeconfig string,
+) (*genericapiserver.CompletedConfig, error) {
 	// kubeconfig file is useful when antrea-agent isn't running as a Pod.
 	if len(kubeconfig) > 0 {
 		authentication.RemoteKubeConfigFile = kubeconfig
@@ -132,8 +143,6 @@ func newConfig(npq querier.AgentNetworkPolicyInfoQuerier, bindPort int, enableMe
 	// Set the PairName but leave certificate directory blank to generate in-memory by default.
 	secureServing.ServerCert.CertDirectory = ""
 	secureServing.ServerCert.PairName = Name
-	secureServing.BindAddress = net.IPv4zero
-	secureServing.BindPort = bindPort
 
 	if err := secureServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, []net.IP{net.ParseIP("127.0.0.1"), net.IPv6loopback}); err != nil {
 		return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
@@ -164,13 +173,22 @@ func newConfig(npq querier.AgentNetworkPolicyInfoQuerier, bindPort int, enableMe
 	}
 	serverConfig.EnableMetrics = enableMetrics
 	// Add readiness probe to check the status of watchers.
-	check := healthz.NamedCheck("watcher", func(_ *http.Request) error {
+	watcherCheck := healthz.NamedCheck("watcher", func(_ *http.Request) error {
 		if npq.GetControllerConnectionStatus() {
 			return nil
 		}
 		return fmt.Errorf("some watchers may not be connected")
 	})
-	serverConfig.ReadyzChecks = append(serverConfig.ReadyzChecks, check)
+	serverConfig.ReadyzChecks = append(serverConfig.ReadyzChecks, watcherCheck)
+	// Add liveness probe to check the connection with OFSwitch.
+	// This helps automatic recovery if some issues cause OFSwitch reconnection to not work properly, e.g. issue #4092.
+	ovsConnCheck := healthz.NamedCheck("ovs", func(_ *http.Request) error {
+		if aq.GetOpenflowClient().IsConnected() {
+			return nil
+		}
+		return fmt.Errorf("disconnected from OFSwitch")
+	})
+	serverConfig.LivezChecks = append(serverConfig.LivezChecks, ovsConnCheck)
 
 	completedServerCfg := serverConfig.Complete(nil)
 	return &completedServerCfg, nil
diff --git a/pkg/agent/cniserver/interface_configuration_linux.go b/pkg/agent/cniserver/interface_configuration_linux.go
index d165876bb..684a3f171 100644
--- a/pkg/agent/cniserver/interface_configuration_linux.go
+++ b/pkg/agent/cniserver/interface_configuration_linux.go
@@ -36,6 +36,7 @@ import (
 	"antrea.io/antrea/pkg/agent/util/ethtool"
 	cnipb "antrea.io/antrea/pkg/apis/cni/v1beta1"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
+	cniip "antrea.io/antrea/third_party/containernetworking/ip"
 )
 
 // NetDeviceType type Enum
@@ -253,13 +254,14 @@ func (ic *ifConfigurator) configureContainerLinkVeth(
 	containerIface := &current.Interface{Name: containerIfaceName, Sandbox: containerNetNS}
 	result.Interfaces = []*current.Interface{hostIface, containerIface}
 
+	podMAC := util.GenerateRandomMAC()
 	if err := ns.WithNetNSPath(containerNetNS, func(hostNS ns.NetNS) error {
 		klog.V(2).Infof("Creating veth devices (%s, %s) for container %s", containerIfaceName, hostIfaceName, containerID)
-		hostVeth, containerVeth, err := ip.SetupVethWithName(containerIfaceName, hostIfaceName, mtu, hostNS)
+		hostVeth, containerVeth, err := cniip.SetupVethWithName(containerIfaceName, hostIfaceName, mtu, podMAC.String(), hostNS)
 		if err != nil {
 			return fmt.Errorf("failed to create veth devices for container %s: %v", containerID, err)
 		}
-		containerIface.Mac = containerVeth.HardwareAddr.String()
+		containerIface.Mac = podMAC.String()
 		hostIface.Mac = hostVeth.HardwareAddr.String()
 		// Disable TX checksum offloading when it's configured explicitly or the datapath is netdev.
 		// OVS netdev datapath doesn't support TX checksum offloading, i.e. if packet
diff --git a/pkg/agent/config/node_config.go b/pkg/agent/config/node_config.go
index 650c25fc4..c4ee74451 100644
--- a/pkg/agent/config/node_config.go
+++ b/pkg/agent/config/node_config.go
@@ -51,7 +51,6 @@ var (
 	// - The IP is used as destination IP in host routing entry to forward replied SNATed Service packets back to Antrea
 	//   gateway.
 	// - The IP is used as the next hop of host routing entry for ClusterIP and virtual NodePort DNAT IP.
-	// - The IP is used for InternalIPAddress parameter of Add-NetNatStaticMapping on Windows.
 	//   The IP cannot be one used in the network, and cannot be within the 169.254.1.0 - 169.254.254.255 range
 	//   according to https://datatracker.ietf.org/doc/html/rfc3927#section-2.1
 	VirtualServiceIPv4 = net.ParseIP("169.254.0.253")
diff --git a/pkg/agent/interfacestore/types.go b/pkg/agent/interfacestore/types.go
index fa838a116..a3c3c4405 100644
--- a/pkg/agent/interfacestore/types.go
+++ b/pkg/agent/interfacestore/types.go
@@ -138,8 +138,8 @@ func NewContainerInterface(
 }
 
 // NewGatewayInterface creates InterfaceConfig for the host gateway interface.
-func NewGatewayInterface(gatewayName string) *InterfaceConfig {
-	gatewayConfig := &InterfaceConfig{InterfaceName: gatewayName, Type: GatewayInterface}
+func NewGatewayInterface(gatewayName string, gatewayMAC net.HardwareAddr) *InterfaceConfig {
+	gatewayConfig := &InterfaceConfig{InterfaceName: gatewayName, Type: GatewayInterface, MAC: gatewayMAC}
 	return gatewayConfig
 }
 
diff --git a/pkg/agent/memberlist/cluster.go b/pkg/agent/memberlist/cluster.go
index 75c5506ed..0b633c5cc 100644
--- a/pkg/agent/memberlist/cluster.go
+++ b/pkg/agent/memberlist/cluster.go
@@ -17,7 +17,7 @@ package memberlist
 import (
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net"
 	"reflect"
 	"sync"
@@ -88,13 +88,20 @@ type Interface interface {
 	AddClusterEventHandler(handler ClusterNodeEventHandler)
 }
 
+type Memberlist interface {
+	Join(existing []string) (int, error)
+	Members() []*memberlist.Node
+	Leave(timeout time.Duration) error
+	Shutdown() error
+}
+
 // Cluster implements ClusterInterface.
 type Cluster struct {
 	bindPort int
 	// Name of local Node. Node name must be unique in the cluster.
 	nodeName string
 
-	mList *memberlist.Memberlist
+	mList Memberlist
 	// consistentHash hold the consistentHashMap, when a Node join cluster, use method Add() to add a key to the hash.
 	// when a Node leave the cluster, the consistentHashMap should be update.
 	consistentHashMap     map[string]*consistenthash.Map
@@ -129,6 +136,7 @@ func NewCluster(
 	nodeName string,
 	nodeInformer coreinformers.NodeInformer,
 	externalIPPoolInformer crdinformers.ExternalIPPoolInformer,
+	ml Memberlist, // Parameterized for testing, could be left nil for production code.
 ) (*Cluster, error) {
 	// The Node join/leave events will be notified via it.
 	nodeEventCh := make(chan memberlist.NodeEvent, 1024)
@@ -136,6 +144,7 @@ func NewCluster(
 		bindPort:                        clusterBindPort,
 		nodeName:                        nodeName,
 		consistentHashMap:               make(map[string]*consistenthash.Map),
+		mList:                           ml,
 		nodeEventsCh:                    nodeEventCh,
 		nodeInformer:                    nodeInformer,
 		nodeLister:                      nodeInformer.Lister(),
@@ -146,20 +155,24 @@ func NewCluster(
 		queue:                           workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "externalIPPool"),
 	}
 
-	conf := memberlist.DefaultLocalConfig()
-	conf.Name = c.nodeName
-	conf.BindPort = c.bindPort
-	conf.AdvertisePort = c.bindPort
-	conf.AdvertiseAddr = nodeIP.String()
-	conf.Events = &memberlist.ChannelEventDelegate{Ch: nodeEventCh}
-	conf.LogOutput = ioutil.Discard
-	klog.V(1).InfoS("New memberlist cluster", "config", conf)
+	if ml == nil {
+		conf := memberlist.DefaultLocalConfig()
+		conf.Name = c.nodeName
+		conf.BindPort = c.bindPort
+		conf.AdvertisePort = c.bindPort
+		conf.AdvertiseAddr = nodeIP.String()
+		// Setting it to a non-zero value to allow reclaiming Nodes with different addresses for Node IP update case.
+		conf.DeadNodeReclaimTime = 10 * time.Millisecond
+		conf.Events = &memberlist.ChannelEventDelegate{Ch: nodeEventCh}
+		conf.LogOutput = io.Discard
+		klog.V(1).InfoS("New memberlist cluster", "config", conf)
 
-	mList, err := memberlist.Create(conf)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create memberlist cluster: %v", err)
+		mList, err := memberlist.Create(conf)
+		if err != nil {
+			return nil, fmt.Errorf("failed to create memberlist cluster: %v", err)
+		}
+		c.mList = mList
 	}
-	c.mList = mList
 
 	nodeInformer.Informer().AddEventHandlerWithResyncPeriod(
 		cache.ResourceEventHandlerFuncs{
@@ -188,13 +201,16 @@ func NewCluster(
 
 func (c *Cluster) handleCreateNode(obj interface{}) {
 	node := obj.(*corev1.Node)
-	if member, err := c.newClusterMember(node); err == nil {
-		_, err := c.mList.Join([]string{member})
-		if err != nil {
-			klog.ErrorS(err, "Processing Node CREATE event error, join cluster failed", "member", member)
+	// Ignore the Node itself.
+	if node.Name != c.nodeName {
+		if member, err := c.newClusterMember(node); err == nil {
+			_, err := c.mList.Join([]string{member})
+			if err != nil {
+				klog.ErrorS(err, "Processing Node CREATE event error, join cluster failed", "member", member)
+			}
+		} else {
+			klog.ErrorS(err, "Processing Node CREATE event error", "nodeName", node.Name)
 		}
-	} else {
-		klog.ErrorS(err, "Processing Node CREATE event error", "nodeName", node.Name)
 	}
 
 	affectedEIPs := c.filterEIPsFromNodeLabels(node)
@@ -261,8 +277,7 @@ func (c *Cluster) enqueueExternalIPPool(obj interface{}) {
 	c.queue.Add(eip.Name)
 }
 
-// newClusterMember gets the Node's IP and returns a cluster member "<IP>:<clusterMemberlistPort>"
-// representing that Node in the memberlist cluster.
+// newClusterMember gets the Node's IP and returns it as a cluster member for memberlist cluster to join.
 func (c *Cluster) newClusterMember(node *corev1.Node) (string, error) {
 	nodeAddrs, err := k8s.GetNodeAddrs(node)
 	if err != nil {
@@ -275,30 +290,9 @@ func (c *Cluster) newClusterMember(node *corev1.Node) (string, error) {
 	return nodeAddr.String(), nil
 }
 
-func (c *Cluster) allClusterMembers() (clusterNodes []string, err error) {
-	nodes, err := c.nodeLister.List(labels.Everything())
-	if err != nil {
-		return nil, fmt.Errorf("listing Nodes error: %v", err)
-	}
-
-	for _, node := range nodes {
-		member, err := c.newClusterMember(node)
-		if err != nil {
-			klog.ErrorS(err, "Get Node failed")
-			continue
-		}
-		clusterNodes = append(clusterNodes, member)
-	}
-	return
-}
-
 func (c *Cluster) filterEIPsFromNodeLabels(node *corev1.Node) sets.String {
 	pools := sets.NewString()
-	eips, err := c.externalIPPoolLister.List(labels.Everything())
-	if err != nil {
-		klog.ErrorS(err, "Filter ExternalIPPools from nodeLabels failed")
-		return pools
-	}
+	eips, _ := c.externalIPPoolLister.List(labels.Everything())
 	for _, eip := range eips {
 		nodeSelector, _ := metav1.LabelSelectorAsSelector(&eip.Spec.NodeSelector)
 		if nodeSelector.Matches(labels.Set(node.GetLabels())) {
@@ -325,27 +319,67 @@ func (c *Cluster) Run(stopCh <-chan struct{}) {
 		return
 	}
 
-	members, err := c.allClusterMembers()
-	if err != nil {
-		klog.ErrorS(err, "List cluster members failed")
-	} else if members != nil {
-		_, err := c.mList.Join(members)
-		if err != nil {
-			klog.ErrorS(err, "Join cluster failed")
-		}
-	}
-
 	for i := 0; i < defaultWorkers; i++ {
 		go wait.Until(c.worker, time.Second, stopCh)
 	}
 
-	for {
-		select {
-		case <-stopCh:
-			return
-		case nodeEvent := <-c.nodeEventsCh:
-			c.handleClusterNodeEvents(&nodeEvent)
+	go func() {
+		for {
+			select {
+			case <-stopCh:
+				return
+			case nodeEvent := <-c.nodeEventsCh:
+				c.handleClusterNodeEvents(&nodeEvent)
+			}
+		}
+	}()
+
+	// Rejoin Nodes periodically in case some Nodes are removed from the member list because of long downtime.
+	go func() {
+		ticker := time.NewTicker(1 * time.Minute)
+		for {
+			select {
+			case <-stopCh:
+				return
+			case <-ticker.C:
+				c.RejoinNodes()
+			}
 		}
+	}()
+
+	<-stopCh
+}
+
+// RejoinNodes rejoins Nodes that were removed from the member list by memberlist because they were unreachable for more
+// than 15 seconds (the GossipToTheDeadTime we are using). Without it, once there is a network downtime lasting more
+// than 15 seconds, the agent wouldn't try to reach any other Node and would think it's the only alive Node until it's
+// restarted.
+func (c *Cluster) RejoinNodes() {
+	nodes, _ := c.nodeLister.List(labels.Everything())
+	aliveNodes := c.AliveNodes()
+	var membersToJoin []string
+	for _, node := range nodes {
+		if !aliveNodes.Has(node.Name) {
+			member, err := c.newClusterMember(node)
+			if err != nil {
+				klog.ErrorS(err, "Failed to generate cluster member to join", "Node", node.Name)
+				continue
+			}
+			membersToJoin = append(membersToJoin, member)
+		}
+	}
+	// Every known Node is alive, do nothing.
+	if len(membersToJoin) == 0 {
+		return
+	}
+	// The Join method returns an error only when none could be reached.
+	numSuccess, err := c.mList.Join(membersToJoin)
+	if err != nil {
+		klog.ErrorS(err, "Failed to rejoin any members", "members", membersToJoin)
+	} else if numSuccess != len(membersToJoin) {
+		klog.ErrorS(err, "Failed to rejoin some members", "members", membersToJoin, "numSuccess", numSuccess)
+	} else {
+		klog.InfoS("Rejoined all members", "members", membersToJoin)
 	}
 }
 
@@ -446,12 +480,9 @@ func (c *Cluster) handleClusterNodeEvents(nodeEvent *memberlist.NodeEvent) {
 		// if the Node has failed, ExternalIPPools consistentHash maybe changed, and affected ExternalIPPool should be enqueued.
 		coreNode, err := c.nodeLister.Get(node.Name)
 		if err != nil {
-			if apierrors.IsNotFound(err) {
-				// Node has been deleted, and deleteNode handler has been executed.
-				klog.ErrorS(err, "Processing Node event, not found", "eventType", event)
-				return
-			}
-			klog.ErrorS(err, "Processing Node event, get Node failed", "eventType", event)
+			// It means the Node has been deleted, no further processing is needed as handleDeleteNode has enqueued
+			// related ExternalIPPools.
+			klog.InfoS("Received a Node event but did not find the Node object", "eventType", mapNodeEventType[event], "nodeName", node.Name)
 			return
 		}
 		affectedEIPs := c.filterEIPsFromNodeLabels(coreNode)
diff --git a/pkg/agent/multicast/mcast_controller.go b/pkg/agent/multicast/mcast_controller.go
index f00d5991f..2669852d7 100644
--- a/pkg/agent/multicast/mcast_controller.go
+++ b/pkg/agent/multicast/mcast_controller.go
@@ -388,7 +388,7 @@ func (c *Controller) syncGroup(groupKey string) error {
 				return err
 			}
 			// Remove the multicast flow entry if no local Pod is in the group.
-			if err := c.ofClient.UninstallGroup(status.ofGroupID); err != nil {
+			if err := c.ofClient.UninstallMulticastGroup(status.ofGroupID); err != nil {
 				klog.ErrorS(err, "Failed to uninstall multicast group", "group", groupKey)
 				return err
 			}
diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go
index 5606158e1..30b2d163c 100644
--- a/pkg/agent/openflow/client.go
+++ b/pkg/agent/openflow/client.go
@@ -83,9 +83,9 @@ type Client interface {
 	// InstallServiceGroup installs a group for Service LB. Each endpoint
 	// is a bucket of the group. For now, each bucket has the same weight.
 	InstallServiceGroup(groupID binding.GroupIDType, withSessionAffinity bool, endpoints []proxy.Endpoint) error
-	// UninstallGroup removes the group and its buckets that are
-	// installed by InstallServiceGroup or InstallMulticastGroup.
-	UninstallGroup(groupID binding.GroupIDType) error
+	// UninstallServiceGroup removes the group and its buckets that are
+	// installed by InstallServiceGroup.
+	UninstallServiceGroup(groupID binding.GroupIDType) error
 
 	// InstallEndpointFlows installs flows for accessing Endpoints.
 	// If an Endpoint is on the current Node, then flows for hairpin and endpoint
@@ -305,6 +305,10 @@ type Client interface {
 
 	InstallMulticastGroup(ofGroupID binding.GroupIDType, localReceivers []uint32) error
 
+	// UninstallMulticastGroup removes the group and its buckets that are
+	// installed by InstallMulticastGroup.
+	UninstallMulticastGroup(groupID binding.GroupIDType) error
+
 	// InstallMulticlusterNodeFlows installs flows to handle cross-cluster packets between a regular
 	// Node and a local Gateway.
 	InstallMulticlusterNodeFlows(
@@ -599,7 +603,7 @@ func (c *client) InstallServiceGroup(groupID binding.GroupIDType, withSessionAff
 	return nil
 }
 
-func (c *client) UninstallGroup(groupID binding.GroupIDType) error {
+func (c *client) UninstallServiceGroup(groupID binding.GroupIDType) error {
 	c.replayMutex.RLock()
 	defer c.replayMutex.RUnlock()
 	if !c.bridge.DeleteGroup(groupID) {
@@ -1253,6 +1257,16 @@ func (c *client) InstallMulticastGroup(groupID binding.GroupIDType, localReceive
 	return nil
 }
 
+func (c *client) UninstallMulticastGroup(groupID binding.GroupIDType) error {
+	c.replayMutex.RLock()
+	defer c.replayMutex.RUnlock()
+	if !c.bridge.DeleteGroup(groupID) {
+		return fmt.Errorf("group %d delete failed", groupID)
+	}
+	c.featureMulticast.groupCache.Delete(groupID)
+	return nil
+}
+
 // InstallMulticlusterNodeFlows installs flows to handle cross-cluster packets between a regular
 // Node and a local Gateway.
 func (c *client) InstallMulticlusterNodeFlows(clusterID string,
diff --git a/pkg/agent/proxy/proxier.go b/pkg/agent/proxy/proxier.go
index d891f0357..3c8be7238 100644
--- a/pkg/agent/proxy/proxier.go
+++ b/pkg/agent/proxy/proxier.go
@@ -173,7 +173,7 @@ func (p *proxier) removeStaleServices() {
 		// Remove Service group whose Endpoints are local.
 		if svcInfo.NodeLocalExternal() {
 			if groupIDLocal, exist := p.groupCounter.Get(svcPortName, true); exist {
-				if err := p.ofClient.UninstallGroup(groupIDLocal); err != nil {
+				if err := p.ofClient.UninstallServiceGroup(groupIDLocal); err != nil {
 					klog.ErrorS(err, "Failed to remove Group of local Endpoints for Service", "Service", svcPortName)
 					continue
 				}
@@ -182,7 +182,7 @@ func (p *proxier) removeStaleServices() {
 		}
 		// Remove Service group which has all Endpoints.
 		if groupID, exist := p.groupCounter.Get(svcPortName, false); exist {
-			if err := p.ofClient.UninstallGroup(groupID); err != nil {
+			if err := p.ofClient.UninstallServiceGroup(groupID); err != nil {
 				klog.ErrorS(err, "Failed to remove Group of all Endpoints for Service", "Service", svcPortName)
 				continue
 			}
@@ -555,7 +555,7 @@ func (p *proxier) installServices() {
 					continue
 				}
 				if groupID, exist := p.groupCounter.Get(svcPortName, !nodeLocalVal); exist {
-					if err := p.ofClient.UninstallGroup(groupID); err != nil {
+					if err := p.ofClient.UninstallServiceGroup(groupID); err != nil {
 						klog.ErrorS(err, "Failed to uninstall Group of all Endpoints for Service", "Service", svcPortName)
 						continue
 					}
diff --git a/pkg/agent/route/route_windows.go b/pkg/agent/route/route_windows.go
index 9d2d250e4..3e58e1556 100644
--- a/pkg/agent/route/route_windows.go
+++ b/pkg/agent/route/route_windows.go
@@ -387,7 +387,7 @@ func (c *Client) DeleteSNATRule(mark uint32) error {
 
 // TODO: nodePortAddresses is not supported currently.
 func (c *Client) AddNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
-	return util.ReplaceNetNatStaticMapping(antreaNatNodePort, "0.0.0.0", port, config.VirtualServiceIPv4.String(), port, string(protocol))
+	return util.ReplaceNetNatStaticMapping(antreaNatNodePort, "0.0.0.0", port, config.VirtualNodePortDNATIPv4.String(), port, string(protocol))
 }
 
 func (c *Client) DeleteNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
diff --git a/pkg/agent/util/net.go b/pkg/agent/util/net.go
index 990530716..8d1100152 100644
--- a/pkg/agent/util/net.go
+++ b/pkg/agent/util/net.go
@@ -15,6 +15,7 @@
 package util
 
 import (
+	"crypto/rand"
 	"crypto/sha1" // #nosec G505: not used for security purposes
 	"encoding/hex"
 	"errors"
@@ -404,3 +405,14 @@ func PortToUint16(port int) uint16 {
 func GenerateUplinkInterfaceName(name string) string {
 	return name + bridgedUplinkSuffix
 }
+
+func GenerateRandomMAC() net.HardwareAddr {
+	buf := make([]byte, 6)
+	if _, err := rand.Read(buf); err != nil {
+		klog.ErrorS(err, "Failed to generate a random MAC")
+	}
+	// Unset the multicast bit.
+	buf[0] &= 0xfe
+	buf[0] |= 0x02
+	return buf
+}
diff --git a/pkg/ovs/openflow/ofctrl_bridge.go b/pkg/ovs/openflow/ofctrl_bridge.go
index e5b2b886c..063ac74ed 100644
--- a/pkg/ovs/openflow/ofctrl_bridge.go
+++ b/pkg/ovs/openflow/ofctrl_bridge.go
@@ -171,6 +171,7 @@ type OFBridge struct {
 	// tableCache is used to cache ofTables.
 	tableCache map[uint8]*ofTable
 
+	ofSwitchMutex sync.RWMutex
 	// ofSwitch is the target OFSwitch.
 	ofSwitch *ofctrl.OFSwitch
 	// controller helps maintain connections to remote OFSwitch.
@@ -305,9 +306,13 @@ func (b *OFBridge) PacketRcvd(sw *ofctrl.OFSwitch, packet *ofctrl.PacketIn) {
 // SwitchConnected is a callback when the remote OFSwitch is connected.
 func (b *OFBridge) SwitchConnected(sw *ofctrl.OFSwitch) {
 	klog.Infof("OFSwitch is connected: %v", sw.DPID())
-	// initialize tables.
-	b.ofSwitch = sw
+	func() {
+		b.ofSwitchMutex.Lock()
+		defer b.ofSwitchMutex.Unlock()
+		b.ofSwitch = sw
+	}()
 	b.ofSwitch.EnableMonitor()
+	// initialize tables.
 	b.initialize()
 	go func() {
 		// b.connected is nil if it is an automatic reconnection but not triggered by OFSwitch.Connect.
@@ -422,7 +427,15 @@ func (b *OFBridge) DeleteFlowsByCookie(cookieID, cookieMask uint64) error {
 }
 
 func (b *OFBridge) IsConnected() bool {
-	return b.ofSwitch.IsReady()
+	sw := func() *ofctrl.OFSwitch {
+		b.ofSwitchMutex.RLock()
+		defer b.ofSwitchMutex.RUnlock()
+		return b.ofSwitch
+	}()
+	if sw == nil {
+		return false
+	}
+	return sw.IsReady()
 }
 
 func (b *OFBridge) AddFlowsInBundle(addflows []Flow, modFlows []Flow, delFlows []Flow) error {
@@ -747,7 +760,7 @@ func (b *OFBridge) processTableFeatures(ch chan *openflow13.MultipartReply) {
 	}
 }
 
-func NewOFBridge(br string, mgmtAddr string) Bridge {
+func NewOFBridge(br string, mgmtAddr string) *OFBridge {
 	s := &OFBridge{
 		bridgeName:        br,
 		mgmtAddr:          mgmtAddr,
diff --git a/pkg/ovs/openflow/ofctrl_group.go b/pkg/ovs/openflow/ofctrl_group.go
index ee37a35ad..987adaeb0 100644
--- a/pkg/ovs/openflow/ofctrl_group.go
+++ b/pkg/ovs/openflow/ofctrl_group.go
@@ -135,6 +135,6 @@ func (b *bucketBuilder) Weight(val uint16) BucketBuilder {
 }
 
 func (b *bucketBuilder) Done() Group {
-	b.group.ofctrl.AddBuckets(b.bucket)
+	b.group.ofctrl.Buckets = append(b.group.ofctrl.Buckets, b.bucket)
 	return b.group
 }
diff --git a/pkg/ovs/ovsconfig/interfaces.go b/pkg/ovs/ovsconfig/interfaces.go
index aa750bae4..607bfb155 100644
--- a/pkg/ovs/ovsconfig/interfaces.go
+++ b/pkg/ovs/ovsconfig/interfaces.go
@@ -14,6 +14,10 @@
 
 package ovsconfig
 
+import (
+	"net"
+)
+
 type TunnelType string
 
 type OVSDatapathType string
@@ -62,4 +66,5 @@ type OVSBridgeClient interface {
 	GetOVSDatapathType() OVSDatapathType
 	SetInterfaceType(name, ifType string) Error
 	SetPortExternalIDs(portName string, externalIDs map[string]interface{}) Error
+	SetInterfaceMAC(name string, mac net.HardwareAddr) Error
 }
diff --git a/pkg/ovs/ovsconfig/ovs_client.go b/pkg/ovs/ovsconfig/ovs_client.go
index 67f784f3d..7e4c108c8 100644
--- a/pkg/ovs/ovsconfig/ovs_client.go
+++ b/pkg/ovs/ovsconfig/ovs_client.go
@@ -48,6 +48,7 @@ type OVSPortData struct {
 	OFPort      int32
 	ExternalIDs map[string]string
 	Options     map[string]string
+	MAC         net.HardwareAddr
 }
 
 const (
@@ -690,6 +691,19 @@ func buildPortDataCommon(port, intf map[string]interface{}, portData *OVSPortDat
 	} else { // ofport not assigned by OVS yet
 		portData.OFPort = 0
 	}
+	var macStr string
+	if field, ok := intf["mac"].(string); ok {
+		macStr = field
+	} else if fields, ok := intf["mac"].([]interface{}); ok {
+		if len(fields) > 0 {
+			macStr = fields[0].(string)
+		}
+	}
+	if macStr != "" {
+		if mac, err := net.ParseMAC(macStr); err == nil {
+			portData.MAC = mac
+		}
+	}
 }
 
 // GetPortData retrieves port data given the OVS port UUID and interface name.
@@ -705,7 +719,7 @@ func (br *OVSBridge) GetPortData(portUUID, ifName string) (*OVSPortData, Error)
 	})
 	tx.Select(dbtransaction.Select{
 		Table:   "Interface",
-		Columns: []string{"_uuid", "type", "ofport", "options"},
+		Columns: []string{"_uuid", "type", "ofport", "options", "mac"},
 		Where:   [][]interface{}{{"name", "==", ifName}},
 	})
 
@@ -758,7 +772,7 @@ func (br *OVSBridge) GetPortList() ([]OVSPortData, Error) {
 	})
 	tx.Select(dbtransaction.Select{
 		Table:   "Interface",
-		Columns: []string{"_uuid", "type", "name", "ofport", "options"},
+		Columns: []string{"_uuid", "type", "name", "ofport", "options", "mac"},
 	})
 
 	res, err, temporary := tx.Commit()
@@ -1093,3 +1107,24 @@ func (br *OVSBridge) SetInterfaceMTU(name string, MTU int) error {
 
 	return nil
 }
+
+func (br *OVSBridge) SetInterfaceMAC(name string, mac net.HardwareAddr) Error {
+	tx := br.ovsdb.Transaction(openvSwitchSchema)
+
+	tx.Update(dbtransaction.Update{
+		Table: "Interface",
+		Where: [][]interface{}{{"name", "==", name}},
+		Row: map[string]interface{}{
+			"mac": mac.String(),
+		},
+	})
+
+	_, err, temporary := tx.Commit()
+	if err != nil {
+		klog.Error("Transaction failed: ", err)
+		return NewTransactionError(err, temporary)
+	}
+
+	return nil
+
+}
diff --git a/plugins/octant/go.mod b/plugins/octant/go.mod
index eacd5a2d9..f0e9e5ce3 100644
--- a/plugins/octant/go.mod
+++ b/plugins/octant/go.mod
@@ -9,6 +9,7 @@ require (
 )
 
 require (
+	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/BurntSushi/toml v0.3.1 // indirect
 	github.com/Microsoft/go-winio v0.5.0 // indirect
 	github.com/Microsoft/hcsshim v0.8.20 // indirect
@@ -42,7 +43,6 @@ require (
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/golang/snappy v0.0.3 // indirect
-	github.com/google/gnostic v0.5.7-v3refs // indirect
 	github.com/google/go-cmp v0.5.5 // indirect
 	github.com/google/go-intervals v0.0.2 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
@@ -137,6 +137,10 @@ replace (
 	// The current version of Octant does not build with version v0.24.0 of
 	// the K8s libraries.
 	k8s.io/api => k8s.io/api v0.21.3
+	k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.21.3
 	k8s.io/apimachinery => k8s.io/apimachinery v0.21.3
 	k8s.io/client-go => k8s.io/client-go v0.21.3
+	k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.21.3
+	k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7
+	k8s.io/metrics => k8s.io/metrics v0.21.3
 )
diff --git a/plugins/octant/go.sum b/plugins/octant/go.sum
index e23f4b8d7..ed8e7b157 100644
--- a/plugins/octant/go.sum
+++ b/plugins/octant/go.sum
@@ -90,10 +90,7 @@ github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:m
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
-github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
-github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
 github.com/Shopify/sarama v1.27.2/go.mod h1:g5s5osgELxgM+Md9Qni9rzo7Rbt+vvFQI4bt/Mc93II=
@@ -102,7 +99,6 @@ github.com/TomCodeLV/OVSDB-golang-lib v0.0.0-20200116135253-9bbdfadcd881/go.mod
 github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
 github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
-github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -110,14 +106,11 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
 github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
-github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/awalterschulze/gographviz v2.0.1+incompatible h1:XIECBRq9VPEQqkQL5pw2OtjCAdrtIgFKoJU8eT98AS8=
 github.com/awalterschulze/gographviz v2.0.1+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
@@ -138,7 +131,6 @@ github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwj
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
@@ -293,7 +285,6 @@ github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
 github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -324,7 +315,6 @@ github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
 github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker v1.4.2-0.20191219165747-a9416c67da9f h1:Sm8iD2lifO31DwXfkGzq8VgA7rwxPjRsYmeo0K/dF9Y=
 github.com/docker/docker v1.4.2-0.20191219165747-a9416c67da9f/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
@@ -336,7 +326,6 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6Uezg
 github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
 github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
 github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
-github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
@@ -369,7 +358,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
-github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
@@ -395,13 +383,9 @@ github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXt
 github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
 github.com/gammazero/deque v0.1.0/go.mod h1:KQw7vFau1hHuM8xmI9RbgKFbAsQFWmBpqQ2KenFLk6M=
 github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
-github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
 github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
-github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
-github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -420,53 +404,17 @@ github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE=
 github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk=
 github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro=
-github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
-github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
-github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
-github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=
-github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=
-github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
-github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
-github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
-github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
-github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
-github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
 github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
 github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
 github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
-github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
-github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
-github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
 github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
 github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
 github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
-github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
-github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
-github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
-github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=
-github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk=
-github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
-github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
-github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
-github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
-github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
-github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
-github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
 github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
 github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk=
-github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
-github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
-github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
-github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
-github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
-github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
-github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
 github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
-github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
-github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
-github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
 github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
 github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@@ -490,7 +438,6 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -508,7 +455,6 @@ github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71
 github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
 github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
 github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
-github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -534,10 +480,6 @@ github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
-github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w=
-github.com/google/cel-go v0.10.1/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w=
-github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA=
-github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
 github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -581,12 +523,9 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
-github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
 github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
-github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
 github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
 github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@@ -728,9 +667,6 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
@@ -891,7 +827,6 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt
 github.com/ostreedev/ostree-go v0.0.0-20190702140239-759a8c1ac913 h1:TnbXhKzrTOyuvWrjI8W6pcoI9XPbLHFXCdN2dtUw7Rw=
 github.com/ostreedev/ostree-go v0.0.0-20190702140239-759a8c1ac913/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
@@ -970,7 +905,6 @@ github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiB
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
-github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
@@ -1049,7 +983,6 @@ github.com/tchap/go-patricia v2.3.0+incompatible h1:GkY4dP3cEfEASBPPkWd+AmjYxhmD
 github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
 github.com/ti-mo/conntrack v0.4.0/go.mod h1:L0vkIzG/TECsuVYMMlID9QWmZQLjyP9gDq8XKTlbg4Q=
 github.com/ti-mo/netfilter v0.3.1/go.mod h1:t/5HvCCHA1LAYj/AZF2fWcJ23BQTA7lzTPCuwwi7xQY=
-github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -1065,7 +998,6 @@ github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
 github.com/vbatts/tar-split v0.11.1 h1:0Odu65rhcZ3JZaPHxl7tCI3V/C/Q9Zf82UFravl02dE=
 github.com/vbatts/tar-split v0.11.1/go.mod h1:LEuURwDEiWjRjwu46yU3KVGuUdVv/dcnpcEPSzR8z6g=
 github.com/vbauerster/mpb/v7 v7.0.3/go.mod h1:NXGsfPGx6G2JssqvEcULtDqUrxuuYs4llpv8W6ZUpzk=
-github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
 github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
 github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
 github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
@@ -1103,7 +1035,6 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
-go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
 go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
 go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
 go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
@@ -1115,9 +1046,6 @@ go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46O
 go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE=
 go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc=
 go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4=
-go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
-go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
-go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ -1162,16 +1090,13 @@ golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -1179,7 +1104,6 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1219,12 +1143,10 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
-golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1234,7 +1156,6 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -1287,7 +1208,6 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
 golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -1321,7 +1241,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1334,7 +1253,6 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1448,7 +1366,6 @@ golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9sn
 golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1469,10 +1386,8 @@ golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0k
 golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -1483,7 +1398,6 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@@ -1612,7 +1526,6 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
@@ -1730,33 +1643,20 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 k8s.io/api v0.21.3 h1:cblWILbLO8ar+Fj6xdDGr603HRsf8Wu9E9rngJeprZQ=
 k8s.io/api v0.21.3/go.mod h1:hUgeYHUbBp23Ue4qdX9tR8/ANi/g3ehylAqDn9NWVOg=
-k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY=
-k8s.io/apiextensions-apiserver v0.18.4/go.mod h1:NYeyeYq4SIpFlPxSAB6jHPIdvu3hL0pc36wuRChybio=
+k8s.io/apiextensions-apiserver v0.21.3 h1:+B6biyUWpqt41kz5x6peIsljlsuwvNAp/oFax/j2/aY=
 k8s.io/apiextensions-apiserver v0.21.3/go.mod h1:kl6dap3Gd45+21Jnh6utCx8Z2xxLm8LGDkprcd+KbsE=
-k8s.io/apiextensions-apiserver v0.23.5/go.mod h1:ntcPWNXS8ZPKN+zTXuzYMeg731CP0heCTl6gYBxLcuQ=
-k8s.io/apiextensions-apiserver v0.24.0 h1:JfgFqbA8gKJ/uDT++feAqk9jBIwNnL9YGdQvaI9DLtY=
-k8s.io/apiextensions-apiserver v0.24.0/go.mod h1:iuVe4aEpe6827lvO6yWQVxiPSpPoSKVjkq+MIdg84cM=
 k8s.io/apimachinery v0.21.3 h1:3Ju4nvjCngxxMYby0BimUk+pQHPOQp3eCGChk5kfVII=
 k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI=
-k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw=
-k8s.io/apiserver v0.18.4/go.mod h1:q+zoFct5ABNnYkGIaGQ3bcbUNdmPyOCoEBcg51LChY8=
 k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
 k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
 k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
 k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU=
-k8s.io/apiserver v0.23.5/go.mod h1:7wvMtGJ42VRxzgVI7jkbKvMbuCbVbgsWFT7RyXiRNTw=
 k8s.io/apiserver v0.24.0/go.mod h1:WFx2yiOMawnogNToVvUYT9nn1jaIkMKj41ZYCVycsBA=
 k8s.io/cli-runtime v0.24.0/go.mod h1:9XxoZDsEkRFUThnwqNviqzljtT/LdHtNWvcNFrAXl0A=
 k8s.io/client-go v0.21.3 h1:J9nxZTOmvkInRDCzcSNQmPJbDYN/PjlxXT9Mos3HcLg=
 k8s.io/client-go v0.21.3/go.mod h1:+VPhCgTsaFmGILxR/7E1N0S+ryO010QBeNCv5JwRGYU=
-k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc=
 k8s.io/code-generator v0.18.3/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
-k8s.io/code-generator v0.18.4/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
 k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo=
-k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk=
-k8s.io/code-generator v0.24.0/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w=
-k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmDfUM=
-k8s.io/component-base v0.18.4/go.mod h1:7jr/Ef5PGmKwQhyAz/pjByxJbC58mhKAhiaDu0vXfPk=
 k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
 k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
 k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
@@ -1769,14 +1669,9 @@ k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
 k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
 k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
 k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=
-k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
-k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
-k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
-k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
-k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
@@ -1786,34 +1681,23 @@ k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
 k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
 k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc=
 k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/kube-aggregator v0.21.3 h1:jS/6ZZGPCkBQhzGGusAd2St+KP/FtQBCXOCOo3H7/U4=
 k8s.io/kube-aggregator v0.21.3/go.mod h1:9OIUuR5KIsNZYP/Xsh4HBsaqbS7ICJpRz3XSKtKajRc=
-k8s.io/kube-aggregator v0.24.0 h1:ax2B6v5y+sLISgal5COnlDRKOSr97uXpwif6nnK3a/M=
-k8s.io/kube-aggregator v0.24.0/go.mod h1:ftfs6Fi46z3cKzeF2kvNBPLbMlSKuqZbesJGNp/cQnw=
-k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
-k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
-k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
+k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0=
 k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE=
-k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
-k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk=
-k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 h1:Gii5eqf+GmIEwGNKQYQClCayuJCe2/4fZUvF7VG99sU=
-k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk=
 k8s.io/kubectl v0.24.0/go.mod h1:pdXkmCyHiRTqjYfyUJiXtbVNURhv0/Q1TyRhy2d5ic0=
 k8s.io/kubelet v0.24.0/go.mod h1:p3BBacmHTCMpUf+nluhlyzuGHmONKAspqCvpu9oPAyA=
 k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
+k8s.io/metrics v0.21.3 h1:BXLcDFR/2XUNOcFDyNI6//9pK+WIDCbQ0+uEkIjcHEc=
 k8s.io/metrics v0.21.3/go.mod h1:mN3Klf203Lw1hOsfg1MG7DR/kKUhwiyu8GSFCXZdz+o=
-k8s.io/metrics v0.24.0 h1:nsFLJBDgj+B8mXvVBWFxTZBRRDJ8uTdf4C/Gedjy9BA=
-k8s.io/metrics v0.24.0/go.mod h1:jrLlFGdKl3X+szubOXPG0Lf2aVxuV3QJcbsgVRAM6fI=
-k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20200603063816-c1c6865ac451/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
-k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc=
 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
@@ -1829,8 +1713,6 @@ sigs.k8s.io/kustomize/cmd/config v0.10.6/go.mod h1:/S4A4nUANUa4bZJ/Edt7ZQTyKOY9W
 sigs.k8s.io/kustomize/kustomize/v4 v4.5.4/go.mod h1:Zo/Xc5FKD6sHl0lilbrieeGeZHVYCA4BzxeAaLI05Bg=
 sigs.k8s.io/kustomize/kyaml v0.13.6/go.mod h1:yHP031rn1QX1lr/Xd934Ri/xdVNG8BE2ECa78Ht/kEg=
 sigs.k8s.io/mcs-api v0.1.0/go.mod h1:gGiAryeFNB4GBsq2LBmVqSgKoobLxt+p7ii/WG5QYYw=
-sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
-sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
diff --git a/third_party/containernetworking/ip/link_linux.go b/third_party/containernetworking/ip/link_linux.go
new file mode 100644
index 000000000..12b351007
--- /dev/null
+++ b/third_party/containernetworking/ip/link_linux.go
@@ -0,0 +1,152 @@
+// Copyright 2015 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ip
+
+import (
+	"crypto/rand"
+	"fmt"
+	"net"
+	"os"
+
+	"github.com/containernetworking/plugins/pkg/ns"
+	"github.com/containernetworking/plugins/pkg/utils/sysctl"
+	"github.com/vishvananda/netlink"
+)
+
+// makeVethPair is called from within the container's network namespace
+func makeVethPair(name, peer string, mtu int, mac string, hostNS ns.NetNS) (netlink.Link, error) {
+	veth := &netlink.Veth{
+		LinkAttrs: netlink.LinkAttrs{
+			Name: name,
+			MTU:  mtu,
+		},
+		PeerName:      peer,
+		PeerNamespace: netlink.NsFd(int(hostNS.Fd())),
+	}
+	if mac != "" {
+		m, err := net.ParseMAC(mac)
+		if err != nil {
+			return nil, err
+		}
+		veth.LinkAttrs.HardwareAddr = m
+	}
+	if err := netlink.LinkAdd(veth); err != nil {
+		return nil, err
+	}
+	// Re-fetch the container link to get its creation-time parameters, e.g. index and mac
+	veth2, err := netlink.LinkByName(name)
+	if err != nil {
+		netlink.LinkDel(veth) // try and clean up the link if possible.
+		return nil, err
+	}
+
+	return veth2, nil
+}
+
+func peerExists(name string) bool {
+	if _, err := netlink.LinkByName(name); err != nil {
+		return false
+	}
+	return true
+}
+
+func makeVeth(name, vethPeerName string, mtu int, mac string, hostNS ns.NetNS) (peerName string, veth netlink.Link, err error) {
+	for i := 0; i < 10; i++ {
+		if vethPeerName != "" {
+			peerName = vethPeerName
+		} else {
+			peerName, err = RandomVethName()
+			if err != nil {
+				return
+			}
+		}
+
+		veth, err = makeVethPair(name, peerName, mtu, mac, hostNS)
+		switch {
+		case err == nil:
+			return
+
+		case os.IsExist(err):
+			if peerExists(peerName) && vethPeerName == "" {
+				continue
+			}
+			err = fmt.Errorf("container veth name provided (%v) already exists", name)
+			return
+
+		default:
+			err = fmt.Errorf("failed to make veth pair: %v", err)
+			return
+		}
+	}
+
+	// should really never be hit
+	err = fmt.Errorf("failed to find a unique veth name")
+	return
+}
+
+// RandomVethName returns string "veth" with random prefix (hashed from entropy)
+func RandomVethName() (string, error) {
+	entropy := make([]byte, 4)
+	_, err := rand.Read(entropy)
+	if err != nil {
+		return "", fmt.Errorf("failed to generate random veth name: %v", err)
+	}
+
+	// NetworkManager (recent versions) will ignore veth devices that start with "veth"
+	return fmt.Sprintf("veth%x", entropy), nil
+}
+
+func ifaceFromNetlinkLink(l netlink.Link) net.Interface {
+	a := l.Attrs()
+	return net.Interface{
+		Index:        a.Index,
+		MTU:          a.MTU,
+		Name:         a.Name,
+		HardwareAddr: a.HardwareAddr,
+		Flags:        a.Flags,
+	}
+}
+
+// SetupVethWithName sets up a pair of virtual ethernet devices.
+// Call SetupVethWithName from inside the container netns.  It will create both veth
+// devices and move the host-side veth into the provided hostNS namespace.
+// hostVethName: If hostVethName is not specified, the host-side veth name will use a random string.
+// On success, SetupVethWithName returns (hostVeth, containerVeth, nil)
+func SetupVethWithName(contVethName, hostVethName string, mtu int, contVethMac string, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
+	hostVethName, contVeth, err := makeVeth(contVethName, hostVethName, mtu, contVethMac, hostNS)
+	if err != nil {
+		return net.Interface{}, net.Interface{}, err
+	}
+
+	var hostVeth netlink.Link
+	err = hostNS.Do(func(_ ns.NetNS) error {
+		hostVeth, err = netlink.LinkByName(hostVethName)
+		if err != nil {
+			return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err)
+		}
+
+		if err = netlink.LinkSetUp(hostVeth); err != nil {
+			return fmt.Errorf("failed to set %q up: %v", hostVethName, err)
+		}
+
+		// we want to own the routes for this interface
+		_, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", hostVethName), "0")
+		return nil
+	})
+	if err != nil {
+		return net.Interface{}, net.Interface{}, err
+	}
+	return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil
+}


Test patch:
diff --git a/build/images/test/Dockerfile b/build/images/test/Dockerfile
index 2d6fbdcd5..a526968dd 100644
--- a/build/images/test/Dockerfile
+++ b/build/images/test/Dockerfile
@@ -1,5 +1,19 @@
-ARG OVS_VERSION
-FROM antrea/openvswitch:${OVS_VERSION}
+# Copyright 2022 Antrea Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ARG BUILD_TAG
+FROM antrea/openvswitch:${BUILD_TAG}
 
 LABEL maintainer="Antrea <projectantrea-dev@googlegroups.com>"
 LABEL description="A Docker image for Antrea integration tests."
diff --git a/ci/jenkins/test.sh b/ci/jenkins/test.sh
index 1af33498b..db2c4fed5 100755
--- a/ci/jenkins/test.sh
+++ b/ci/jenkins/test.sh
@@ -142,6 +142,8 @@ function clean_antrea {
     for antrea_yml in ${WORKDIR}/*.yml; do
         kubectl delete -f $antrea_yml --ignore-not-found=true || true
     done
+    docker images | grep 'antrea' | awk '{print $3}' | xargs -r docker rmi || true
+    docker images | grep '<none>' | awk '{print $3}' | xargs -r docker rmi || true
 }
 
 function clean_for_windows_install_cni {
diff --git a/cmd/antrea-agent/options_test.go b/cmd/antrea-agent/options_test.go
new file mode 100644
index 000000000..539cd288e
--- /dev/null
+++ b/cmd/antrea-agent/options_test.go
@@ -0,0 +1,75 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	agentconfig "antrea.io/antrea/pkg/config/agent"
+)
+
+func TestOptionsValidateTLSOptions(t *testing.T) {
+	tests := []struct {
+		name        string
+		config      *agentconfig.AgentConfig
+		expectedErr string
+	}{
+		{
+			name: "empty input",
+			config: &agentconfig.AgentConfig{
+				TLSCipherSuites: "",
+				TLSMinVersion:   "",
+			},
+			expectedErr: "",
+		},
+		{
+			name: "invalid TLSMinVersion",
+			config: &agentconfig.AgentConfig{
+				TLSCipherSuites: "",
+				TLSMinVersion:   "foo",
+			},
+			expectedErr: "invalid TLSMinVersion",
+		},
+		{
+			name: "invalid TLSCipherSuites",
+			config: &agentconfig.AgentConfig{
+				TLSCipherSuites: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, foo",
+				TLSMinVersion:   "VersionTLS10",
+			},
+			expectedErr: "invalid TLSCipherSuites",
+		},
+		{
+			name: "valid input",
+			config: &agentconfig.AgentConfig{
+				TLSCipherSuites: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, TLS_RSA_WITH_AES_128_GCM_SHA256",
+				TLSMinVersion:   "VersionTLS12",
+			},
+			expectedErr: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			o := &Options{config: tt.config}
+			err := o.validateTLSOptions()
+			if tt.expectedErr == "" {
+				assert.NoError(t, err)
+			} else {
+				assert.ErrorContains(t, err, tt.expectedErr)
+			}
+		})
+	}
+}
diff --git a/pkg/agent/agent_linux_test.go b/pkg/agent/agent_linux_test.go
new file mode 100644
index 000000000..5abb5cc4e
--- /dev/null
+++ b/pkg/agent/agent_linux_test.go
@@ -0,0 +1,19 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package agent
+
+func mockSetInterfaceMTU(returnErr error) func() {
+	return func() {}
+}
diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go
index 1cfe66bea..799db6f28 100644
--- a/pkg/agent/agent_test.go
+++ b/pkg/agent/agent_test.go
@@ -449,3 +449,70 @@ func mockGetIPNetDeviceByCIDRs(ipV4Net, ipV6Net *net.IPNet, ipDevice *net.Interf
 	}
 	return func() { getIPNetDeviceByCIDRs = prevGetIPNetDeviceByCIDRs }
 }
+
+func TestSetupGatewayInterface(t *testing.T) {
+	fakeMAC, _ := net.ParseMAC("12:34:56:78:76:54")
+	defer mockSetLinkUp(fakeMAC, 10, nil)()
+	defer mockConfigureLinkAddress(nil)()
+	defer mockSetInterfaceMTU(nil)()
+
+	controller := mock.NewController(t)
+	defer controller.Finish()
+
+	podCIDRStr := "172.16.10.0/24"
+	_, podCIDR, _ := net.ParseCIDR(podCIDRStr)
+	nodeConfig := &config.NodeConfig{
+		Name:        "n1",
+		OVSBridge:   "br-int",
+		PodIPv4CIDR: podCIDR,
+		NodeMTU:     1450,
+	}
+	networkConfig := &config.NetworkConfig{
+		TrafficEncapMode: config.TrafficEncapModeEncap,
+		TunnelType:       ovsconfig.GeneveTunnel,
+	}
+
+	mockOVSBridgeClient := ovsconfigtest.NewMockOVSBridgeClient(controller)
+	client := fake.NewSimpleClientset()
+	ifaceStore := interfacestore.NewInterfaceStore()
+	stopCh := make(chan struct{})
+	initializer := &Initializer{
+		client:          client,
+		ifaceStore:      ifaceStore,
+		ovsBridgeClient: mockOVSBridgeClient,
+		ovsBridge:       "br-int",
+		networkConfig:   networkConfig,
+		nodeConfig:      nodeConfig,
+		hostGateway:     "antrea-gw0",
+		stopCh:          stopCh,
+	}
+	close(stopCh)
+	portUUID := "123456780a"
+	ofport := int32(config.HostGatewayOFPort)
+	mockOVSBridgeClient.EXPECT().CreateInternalPort(initializer.hostGateway, ofport, mock.Any(), mock.Any()).Return(portUUID, nil)
+	mockOVSBridgeClient.EXPECT().SetInterfaceMAC(initializer.hostGateway, fakeMAC).Return(nil)
+	mockOVSBridgeClient.EXPECT().GetOFPort(initializer.hostGateway, false).Return(ofport, nil)
+	mockOVSBridgeClient.EXPECT().SetInterfaceMTU(initializer.hostGateway, nodeConfig.NodeMTU).Return(nil)
+	err := initializer.setupGatewayInterface()
+	assert.NoError(t, err)
+}
+
+func mockSetLinkUp(returnedMAC net.HardwareAddr, returnIndex int, returnErr error) func() {
+	originalSetLinkUp := setLinkUp
+	setLinkUp = func(name string) (net.HardwareAddr, int, error) {
+		return returnedMAC, returnIndex, returnErr
+	}
+	return func() {
+		setLinkUp = originalSetLinkUp
+	}
+}
+
+func mockConfigureLinkAddress(returnedErr error) func() {
+	originalConfigureLinkAddresses := configureLinkAddresses
+	configureLinkAddresses = func(idx int, ipNets []*net.IPNet) error {
+		return returnedErr
+	}
+	return func() {
+		configureLinkAddresses = originalConfigureLinkAddresses
+	}
+}
diff --git a/pkg/agent/agent_windows_test.go b/pkg/agent/agent_windows_test.go
new file mode 100644
index 000000000..97c04f33c
--- /dev/null
+++ b/pkg/agent/agent_windows_test.go
@@ -0,0 +1,25 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package agent
+
+func mockSetInterfaceMTU(returnErr error) func() {
+	originalSetInterfaceMTU := setInterfaceMTU
+	setInterfaceMTU = func(ifaceName string, mtu int) error {
+		return returnErr
+	}
+	return func() {
+		setInterfaceMTU = originalSetInterfaceMTU
+	}
+}
diff --git a/pkg/agent/apiserver/apiserver_test.go b/pkg/agent/apiserver/apiserver_test.go
new file mode 100644
index 000000000..6357bc1df
--- /dev/null
+++ b/pkg/agent/apiserver/apiserver_test.go
@@ -0,0 +1,161 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apiserver
+
+import (
+	"net"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"k8s.io/apiserver/pkg/server/options"
+
+	"antrea.io/antrea/pkg/agent/config"
+	oftest "antrea.io/antrea/pkg/agent/openflow/testing"
+	aqtest "antrea.io/antrea/pkg/agent/querier/testing"
+	queriertest "antrea.io/antrea/pkg/querier/testing"
+	"antrea.io/antrea/pkg/version"
+)
+
+type fakeAgentAPIServer struct {
+	*agentAPIServer
+	agentQuerier *aqtest.MockAgentQuerier
+	npQuerier    *queriertest.MockAgentNetworkPolicyInfoQuerier
+	ofClient     *oftest.MockClient
+}
+
+func newFakeAPIServer(t *testing.T) *fakeAgentAPIServer {
+	kubeConfigFile, err := os.CreateTemp("", "kubeconfig")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer func() {
+		kubeConfigFile.Close()
+		os.Remove(kubeConfigFile.Name())
+	}()
+	if _, err := kubeConfigFile.Write([]byte(`
+apiVersion: v1
+kind: Config
+clusters:
+- cluster:
+    server: http://localhost:56789
+  name: cluster
+contexts:
+- context:
+    cluster: cluster
+  name: cluster
+current-context: cluster
+`)); err != nil {
+		t.Fatal(err)
+	}
+	originalTokenPath := TokenPath
+	tokenFile, err := os.CreateTemp("", "token")
+	require.NoError(t, err)
+	TokenPath = tokenFile.Name()
+	defer func() {
+		TokenPath = originalTokenPath
+		tokenFile.Close()
+		os.Remove(tokenFile.Name())
+	}()
+	version.Version = "v1.2.3"
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	agentQuerier := aqtest.NewMockAgentQuerier(ctrl)
+	agentQuerier.EXPECT().GetNodeConfig().Return(&config.NodeConfig{OVSBridge: "br-int"})
+	npQuerier := queriertest.NewMockAgentNetworkPolicyInfoQuerier(ctrl)
+	ofClient := oftest.NewMockClient(ctrl)
+	agentQuerier.EXPECT().GetOpenflowClient().AnyTimes().Return(ofClient)
+
+	secureServing := options.NewSecureServingOptions().WithLoopback()
+	secureServing.BindAddress = net.ParseIP("127.0.0.1")
+	secureServing.BindPort = 10000
+	authentication := options.NewDelegatingAuthenticationOptions()
+	// InClusterLookup is skipped when testing, otherwise it would always fail as there is no real cluster.
+	authentication.SkipInClusterLookup = true
+	authorization := options.NewDelegatingAuthorizationOptions().WithAlwaysAllowPaths("/healthz", "/livez", "/readyz")
+	apiServer, err := New(agentQuerier, npQuerier, nil, nil, secureServing, authentication, authorization, true, kubeConfigFile.Name(), true, true)
+	require.NoError(t, err)
+	fakeAPIServer := &fakeAgentAPIServer{
+		agentAPIServer: apiServer,
+		agentQuerier:   agentQuerier,
+		npQuerier:      npQuerier,
+		ofClient:       ofClient,
+	}
+	return fakeAPIServer
+}
+
+func TestAPIServerLivezCheck(t *testing.T) {
+	tests := []struct {
+		name         string
+		setupFunc    func(*fakeAgentAPIServer)
+		expectedCode int
+		expectedBody string
+	}{
+		{
+			name: "ovs connected",
+			setupFunc: func(apiserver *fakeAgentAPIServer) {
+				apiserver.ofClient.EXPECT().IsConnected().Return(true)
+			},
+			expectedCode: 200,
+			expectedBody: "ok",
+		},
+		{
+			name: "ovs disconnected",
+			setupFunc: func(apiserver *fakeAgentAPIServer) {
+				apiserver.ofClient.EXPECT().IsConnected().Return(false)
+			},
+			expectedCode: 500,
+			expectedBody: `[+]ping ok
+[+]log ok
+[-]ovs failed: reason withheld
+[+]poststarthook/max-in-flight-filter ok
+livez check failed
+`,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			apiserver := newFakeAPIServer(t)
+			stopCh := make(chan struct{})
+			defer close(stopCh)
+			go func() {
+				apiserver.Run(stopCh)
+			}()
+			// Wait for APIServer to be healthy so checks installed by default are ensured ok.
+			assert.Eventuallyf(t, func() bool {
+				response := getResponse(apiserver, "/healthz")
+				return response.Body.String() == "ok"
+			}, time.Second*5, time.Millisecond*100, "APIServer didn't become health in 5 seconds")
+
+			tt.setupFunc(apiserver)
+			response := getResponse(apiserver, "/livez")
+			assert.Equal(t, tt.expectedBody, response.Body.String())
+			assert.Equal(t, tt.expectedCode, response.Code)
+		})
+	}
+}
+
+func getResponse(apiserver *fakeAgentAPIServer, query string) *httptest.ResponseRecorder {
+	req, _ := http.NewRequest(http.MethodGet, query, nil)
+	recorder := httptest.NewRecorder()
+	apiserver.GenericAPIServer.Handler.ServeHTTP(recorder, req)
+	return recorder
+}
diff --git a/pkg/agent/controller/ipseccertificate/ipsec_certificate_controller_test.go b/pkg/agent/controller/ipseccertificate/ipsec_certificate_controller_test.go
index b01d3e101..586a65159 100644
--- a/pkg/agent/controller/ipseccertificate/ipsec_certificate_controller_test.go
+++ b/pkg/agent/controller/ipseccertificate/ipsec_certificate_controller_test.go
@@ -209,11 +209,16 @@ func TestController_syncConfigurations(t *testing.T) {
 		assert.Equal(t, 0, fakeController.queue.Len())
 		ctx, cancel := context.WithCancel(context.Background())
 		defer cancel()
+		signCh := make(chan struct{})
 		watcher, err := fakeController.kubeClient.CertificatesV1().CertificateSigningRequests().Watch(ctx, metav1.ListOptions{})
 		require.NoError(t, err)
+		defer func() {
+			<-signCh
+		}()
 		defer watcher.Stop()
 		// start a fake signer in background to sign CSRs.
 		go func() {
+			defer close(signCh)
 			for ev := range watcher.ResultChan() {
 				switch ev.Type {
 				case watch.Added:
@@ -307,10 +312,16 @@ func TestController_RotateCertificates(t *testing.T) {
 	if delta < 0 {
 		delta = -delta
 	}
-	// the rotation interval should be in [7s, 9s], but it takes time to process the CSR request,
-	// so add one second to the upper bound.
-	assert.Less(t, delta, time.Second*10)
-	assert.LessOrEqual(t, time.Second*7, delta)
+	// the rotation interval is determined by nextRotationDeadline as notBefore + (notAfter -
+	// notBefore) * k, where k is >= 0.7 and <= 0.9. We would therefore expect the delta to
+	// fall in the interval [7s, 9s]. However we have to account for the following:
+	// a) the accuracy of notAfter is at the second level, which means that it will be
+	// truncated, which means that there can actually be less than 7s between the
+	// CreationTimestamp of the first certificate and the rotation deadline. As a result, we
+	// need to set the lower bound to 6s.
+	// b) it takes time to process the CSR request so we add one second to the upper bound.
+	assert.Less(t, delta, 10*time.Second)
+	assert.LessOrEqual(t, 6*time.Second, delta)
 }
 
 func newIPsecCertTemplate(t *testing.T, nodeName string, notBefore, notAfter time.Time) *x509.Certificate {
@@ -353,6 +364,12 @@ func signCSR(t *testing.T, controller *fakeController,
 	assert.Empty(t, remain)
 	req, err := x509.ParseCertificateRequest(block.Bytes)
 	assert.NoError(t, err)
+
+	// Wait one second as the fake clientset doesn't support watching with specific resourceVersion.
+	// Otherwise the update event would be missed by the watcher used in csrutil.WaitForCertificate()
+	// if it happens to be generated in-between the List and Watch calls.
+	time.Sleep(1 * time.Second)
+
 	newCert := createCertificate(t, req.Subject.CommonName, controller.caCert,
 		controller.caKey, req.PublicKey, csr.CreationTimestamp.Time, expirationDuration)
 	toUpdate := csr.DeepCopy()
@@ -369,6 +386,7 @@ func signCSR(t *testing.T, controller *fakeController,
 	_, err = controller.kubeClient.CertificatesV1().CertificateSigningRequests().
 		UpdateStatus(context.TODO(), toUpdate, metav1.UpdateOptions{})
 	assert.NoError(t, err)
+	t.Logf("Sign CSR %q successfully", csr.Name)
 }
 
 func Test_jitteryDuration(t *testing.T) {
diff --git a/pkg/agent/memberlist/cluster_test.go b/pkg/agent/memberlist/cluster_test.go
index 33f6c7995..e4a8a0eec 100644
--- a/pkg/agent/memberlist/cluster_test.go
+++ b/pkg/agent/memberlist/cluster_test.go
@@ -21,6 +21,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/golang/mock/gomock"
 	"github.com/hashicorp/memberlist"
 	"github.com/stretchr/testify/assert"
 	v1 "k8s.io/api/core/v1"
@@ -33,10 +34,12 @@ import (
 
 	"antrea.io/antrea/pkg/agent/config"
 	"antrea.io/antrea/pkg/agent/consistenthash"
+	memberlisttest "antrea.io/antrea/pkg/agent/memberlist/testing"
 	"antrea.io/antrea/pkg/apis"
 	crdv1a2 "antrea.io/antrea/pkg/apis/crd/v1alpha2"
 	fakeversioned "antrea.io/antrea/pkg/client/clientset/versioned/fake"
 	crdinformers "antrea.io/antrea/pkg/client/informers/externalversions"
+	"antrea.io/antrea/pkg/util/ip"
 )
 
 type fakeCluster struct {
@@ -45,18 +48,15 @@ type fakeCluster struct {
 	crdClient *fakeversioned.Clientset
 }
 
-func newFakeCluster(nodeConfig *config.NodeConfig, stopCh <-chan struct{}, i int) (*fakeCluster, error) {
-	port := apis.AntreaAgentClusterMembershipPort + i
-
-	clientset := fake.NewSimpleClientset()
+func newFakeCluster(nodeConfig *config.NodeConfig, stopCh <-chan struct{}, memberlist Memberlist, objs ...runtime.Object) (*fakeCluster, error) {
+	clientset := fake.NewSimpleClientset(objs...)
 	informerFactory := informers.NewSharedInformerFactory(clientset, 0)
 
 	nodeInformer := informerFactory.Core().V1().Nodes()
-	crdClient := fakeversioned.NewSimpleClientset([]runtime.Object{}...)
+	crdClient := fakeversioned.NewSimpleClientset()
 	crdInformerFactory := crdinformers.NewSharedInformerFactory(crdClient, 0)
 	ipPoolInformer := crdInformerFactory.Crd().V1alpha2().ExternalIPPools()
-	ip := net.ParseIP("127.0.0.1")
-	cluster, err := NewCluster(ip, port, nodeConfig.Name, nodeInformer, ipPoolInformer)
+	cluster, err := NewCluster(nodeConfig.NodeIPv4Addr.IP, apis.AntreaAgentClusterMembershipPort, nodeConfig.Name, nodeInformer, ipPoolInformer, memberlist)
 	if err != nil {
 		return nil, err
 	}
@@ -132,41 +132,60 @@ func TestCluster_Run(t *testing.T) {
 			expectEgressSelectResult: false,
 		},
 	}
-	for i, tCase := range testCases {
+	for _, tCase := range testCases {
 		t.Run(tCase.name, func(t *testing.T) {
+			controller := gomock.NewController(t)
+			stopCh := make(chan struct{})
+			stoppedCh := make(chan struct{})
+			defer func() {
+				// Make sure mock controller is closed after Run() finishes.
+				close(stopCh)
+				<-stoppedCh
+				controller.Finish()
+			}()
+
 			nodeConfig := &config.NodeConfig{
 				Name:         localNodeName,
 				NodeIPv4Addr: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 255)},
 			}
-			stopCh := make(chan struct{})
-			defer close(stopCh)
-			fakeCluster, err := newFakeCluster(nodeConfig, stopCh, i)
+			mockMemberlist := memberlisttest.NewMockMemberlist(controller)
+			fakeCluster, err := newFakeCluster(nodeConfig, stopCh, mockMemberlist)
 			if err != nil {
 				t.Fatalf("New fake memberlist server error: %v", err)
 			}
 
-			eip := tCase.externalIPPool
-			assert.NoError(t, createExternalIPPool(fakeCluster.crdClient, eip))
+			mockMemberlist.EXPECT().Leave(time.Second)
+			mockMemberlist.EXPECT().Shutdown()
+			mockMemberlist.EXPECT().Members().Return([]*memberlist.Node{
+				{Name: localNodeName},
+			}).AnyTimes()
+
+			assert.NoError(t, createExternalIPPool(fakeCluster.crdClient, tCase.externalIPPool))
 			assert.NoError(t, createNode(fakeCluster.clientSet, tCase.localNode))
 
-			go fakeCluster.cluster.Run(stopCh)
+			go func() {
+				defer close(stoppedCh)
+				fakeCluster.cluster.Run(stopCh)
+			}()
 
-			tCase.egress.Spec.ExternalIPPool = eip.Name
 			assert.NoError(t, wait.Poll(100*time.Millisecond, time.Second, func() (done bool, err error) {
-				res, err := fakeCluster.cluster.ShouldSelectIP(tCase.egress.Spec.EgressIP, eip.Name)
+				res, err := fakeCluster.cluster.ShouldSelectIP(tCase.egress.Spec.EgressIP, tCase.externalIPPool.Name)
 				return err == nil && res == tCase.expectEgressSelectResult, nil
 			}), "select Node result for Egress does not match")
-			allMembers, err := fakeCluster.cluster.allClusterMembers()
-			assert.NoError(t, err)
-			assert.Len(t, allMembers, 1, "expected Node member num is 1")
-			assert.Equal(t, 1, fakeCluster.cluster.mList.NumMembers(), "expected alive Node num is 1")
 		})
 	}
 }
 
 func TestCluster_RunClusterEvents(t *testing.T) {
+	controller := gomock.NewController(t)
 	stopCh := make(chan struct{})
-	defer close(stopCh)
+	stoppedCh := make(chan struct{})
+	defer func() {
+		// Make sure mock controller is closed after Run() finishes.
+		close(stopCh)
+		<-stoppedCh
+		controller.Finish()
+	}()
 
 	nodeName := "localNodeName"
 	nodeConfig := &config.NodeConfig{
@@ -186,7 +205,8 @@ func TestCluster_RunClusterEvents(t *testing.T) {
 		Spec:       crdv1a2.EgressSpec{ExternalIPPool: fakeEIP1.Name, EgressIP: "1.1.1.2"},
 	}
 
-	fakeCluster, err := newFakeCluster(nodeConfig, stopCh, 10)
+	mockMemberlist := memberlisttest.NewMockMemberlist(controller)
+	fakeCluster, err := newFakeCluster(nodeConfig, stopCh, mockMemberlist)
 	if err != nil {
 		t.Fatalf("New fake memberlist server error: %v", err)
 	}
@@ -195,42 +215,45 @@ func TestCluster_RunClusterEvents(t *testing.T) {
 		t.Logf("Detected cluster Node event, running fake handler, obj: %s", objName)
 	})
 
+	mockMemberlist.EXPECT().Leave(time.Second)
+	mockMemberlist.EXPECT().Shutdown()
+	mockMemberlist.EXPECT().Members().Return([]*memberlist.Node{
+		{Name: nodeName},
+	}).AnyTimes()
 	// Create local Node and ExternalIPPool.
 	assert.NoError(t, createNode(fakeCluster.clientSet, localNode))
 	assert.NoError(t, createExternalIPPool(fakeCluster.crdClient, fakeEIP1))
 
-	go fakeCluster.cluster.Run(stopCh)
+	go func() {
+		defer close(stoppedCh)
+		fakeCluster.cluster.Run(stopCh)
+	}()
 
 	// Test updating Node labels.
 	testCasesUpdateNode := []struct {
 		name                     string
 		expectEgressSelectResult bool
 		newNodeLabels            map[string]string
-		egress                   *crdv1a2.Egress
 	}{
 		{
 			name:                     "Update Node with the same labels then local Node should not be selected",
 			expectEgressSelectResult: false,
 			newNodeLabels:            localNode.Labels,
-			egress:                   fakeEgress1,
 		},
 		{
 			name:                     "Update Node with matched labels then local Node should be selected",
 			expectEgressSelectResult: true,
 			newNodeLabels:            map[string]string{"env": "pro"},
-			egress:                   fakeEgress1,
 		},
 		{
 			name:                     "Update Node with different but matched labels then local Node should be selected",
 			expectEgressSelectResult: true,
 			newNodeLabels:            map[string]string{"env": "pro", "env1": "test"},
-			egress:                   fakeEgress1,
 		},
 		{
 			name:                     "Update Node with not matched labels then local Node should not be selected",
 			expectEgressSelectResult: false,
 			newNodeLabels:            map[string]string{"env": "test"},
-			egress:                   fakeEgress1,
 		},
 	}
 	updateNode := func(node *v1.Node) {
@@ -244,7 +267,7 @@ func TestCluster_RunClusterEvents(t *testing.T) {
 			localNode.Labels = tCase.newNodeLabels
 			updateNode(localNode)
 			assert.NoError(t, wait.Poll(100*time.Millisecond, time.Second, func() (done bool, err error) {
-				res, err := fakeCluster.cluster.ShouldSelectIP(tCase.egress.Spec.EgressIP, tCase.egress.Spec.ExternalIPPool)
+				res, err := fakeCluster.cluster.ShouldSelectIP(fakeEgress1.Spec.EgressIP, fakeEgress1.Spec.ExternalIPPool)
 				return err == nil && res == tCase.expectEgressSelectResult, nil
 			}), "select Node result for Egress does not match")
 		})
@@ -373,6 +396,7 @@ func TestCluster_RunClusterEvents(t *testing.T) {
 	assertEgressSelectResult(fakeEgress2, false, true)
 	assertEgressSelectResult(fakeEgress1, false, false)
 
+	mockMemberlist.EXPECT().Join([]string{"1.1.1.1"})
 	// Test creating Node with valid IP.
 	fakeNode1 := &v1.Node{
 		ObjectMeta: metav1.ObjectMeta{Name: "fakeNode1"},
@@ -617,3 +641,44 @@ func BenchmarkCluster_ShouldSelect(b *testing.B) {
 		})
 	}
 }
+
+func TestCluster_RejoinNodes(t *testing.T) {
+	localNodeConfig := &config.NodeConfig{
+		Name:         "node1",
+		NodeIPv4Addr: ip.MustParseCIDR("10.0.0.1/24"),
+	}
+	node1 := &v1.Node{
+		ObjectMeta: metav1.ObjectMeta{Name: "node1"},
+		Status:     v1.NodeStatus{Addresses: []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "10.0.0.1"}}},
+	}
+	node2 := &v1.Node{
+		ObjectMeta: metav1.ObjectMeta{Name: "node2"},
+		Status:     v1.NodeStatus{Addresses: []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "10.0.0.2"}}},
+	}
+	node3 := &v1.Node{
+		ObjectMeta: metav1.ObjectMeta{Name: "node3"},
+		Status:     v1.NodeStatus{Addresses: []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "10.0.0.3"}}},
+	}
+	stopCh := make(chan struct{})
+	defer close(stopCh)
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+	mockMemberlist := memberlisttest.NewMockMemberlist(controller)
+	mockMemberlist.EXPECT().Join([]string{"10.0.0.2"})
+	mockMemberlist.EXPECT().Join([]string{"10.0.0.3"})
+	fakeCluster, _ := newFakeCluster(localNodeConfig, stopCh, mockMemberlist, node1, node2, node3)
+
+	mockMemberlist.EXPECT().Members().Return([]*memberlist.Node{
+		{Name: "node1"},
+		{Name: "node2"},
+	})
+	mockMemberlist.EXPECT().Join([]string{"10.0.0.3"})
+	fakeCluster.cluster.RejoinNodes()
+
+	mockMemberlist.EXPECT().Members().Return([]*memberlist.Node{
+		{Name: "node1"},
+		{Name: "node3"},
+	})
+	mockMemberlist.EXPECT().Join([]string{"10.0.0.2"})
+	fakeCluster.cluster.RejoinNodes()
+}
diff --git a/pkg/agent/memberlist/testing/mock_memberlist.go b/pkg/agent/memberlist/testing/mock_memberlist.go
new file mode 100644
index 000000000..b6a759707
--- /dev/null
+++ b/pkg/agent/memberlist/testing/mock_memberlist.go
@@ -0,0 +1,107 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+// Code generated by MockGen. DO NOT EDIT.
+// Source: antrea.io/antrea/pkg/agent/memberlist (interfaces: Memberlist)
+
+// Package testing is a generated GoMock package.
+package testing
+
+import (
+	gomock "github.com/golang/mock/gomock"
+	memberlist "github.com/hashicorp/memberlist"
+	reflect "reflect"
+	time "time"
+)
+
+// MockMemberlist is a mock of Memberlist interface
+type MockMemberlist struct {
+	ctrl     *gomock.Controller
+	recorder *MockMemberlistMockRecorder
+}
+
+// MockMemberlistMockRecorder is the mock recorder for MockMemberlist
+type MockMemberlistMockRecorder struct {
+	mock *MockMemberlist
+}
+
+// NewMockMemberlist creates a new mock instance
+func NewMockMemberlist(ctrl *gomock.Controller) *MockMemberlist {
+	mock := &MockMemberlist{ctrl: ctrl}
+	mock.recorder = &MockMemberlistMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockMemberlist) EXPECT() *MockMemberlistMockRecorder {
+	return m.recorder
+}
+
+// Join mocks base method
+func (m *MockMemberlist) Join(arg0 []string) (int, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Join", arg0)
+	ret0, _ := ret[0].(int)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Join indicates an expected call of Join
+func (mr *MockMemberlistMockRecorder) Join(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Join", reflect.TypeOf((*MockMemberlist)(nil).Join), arg0)
+}
+
+// Leave mocks base method
+func (m *MockMemberlist) Leave(arg0 time.Duration) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Leave", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Leave indicates an expected call of Leave
+func (mr *MockMemberlistMockRecorder) Leave(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Leave", reflect.TypeOf((*MockMemberlist)(nil).Leave), arg0)
+}
+
+// Members mocks base method
+func (m *MockMemberlist) Members() []*memberlist.Node {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Members")
+	ret0, _ := ret[0].([]*memberlist.Node)
+	return ret0
+}
+
+// Members indicates an expected call of Members
+func (mr *MockMemberlistMockRecorder) Members() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Members", reflect.TypeOf((*MockMemberlist)(nil).Members))
+}
+
+// Shutdown mocks base method
+func (m *MockMemberlist) Shutdown() error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Shutdown")
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Shutdown indicates an expected call of Shutdown
+func (mr *MockMemberlistMockRecorder) Shutdown() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockMemberlist)(nil).Shutdown))
+}
diff --git a/pkg/agent/multicast/mcast_controller_test.go b/pkg/agent/multicast/mcast_controller_test.go
index e1bf4a3d2..4eeed0409 100644
--- a/pkg/agent/multicast/mcast_controller_test.go
+++ b/pkg/agent/multicast/mcast_controller_test.go
@@ -156,12 +156,12 @@ func TestCheckLastMember(t *testing.T) {
 		if ev != nil {
 			status.group = ev.group
 			if ev.eType == groupLeave {
-				mockOFClient.EXPECT().UninstallGroup(gomock.Any())
+				mockOFClient.EXPECT().UninstallMulticastGroup(gomock.Any())
 				mockOFClient.EXPECT().UninstallMulticastFlows(status.group)
 			}
 		} else {
 			status.group = mgroup
-			mockOFClient.EXPECT().UninstallGroup(gomock.Any())
+			mockOFClient.EXPECT().UninstallMulticastGroup(gomock.Any())
 			mockOFClient.EXPECT().UninstallMulticastFlows(status.group)
 		}
 		_ = mctrl.groupCache.Add(status)
@@ -265,7 +265,7 @@ func TestClearStaleGroups(t *testing.T) {
 			fakePort++
 		}
 	}
-	mockOFClient.EXPECT().UninstallGroup(gomock.Any()).Times(len(staleGroups))
+	mockOFClient.EXPECT().UninstallMulticastGroup(gomock.Any()).Times(len(staleGroups))
 	mockOFClient.EXPECT().UninstallMulticastFlows(gomock.Any()).Times(len(staleGroups))
 	mockMulticastSocket.EXPECT().MulticastInterfaceLeaveMgroup(gomock.Any(), gomock.Any(), gomock.Any()).Times(len(staleGroups))
 	mctrl.clearStaleGroups()
diff --git a/pkg/agent/openflow/client_test.go b/pkg/agent/openflow/client_test.go
index d698ab6fe..8f8e715f5 100644
--- a/pkg/agent/openflow/client_test.go
+++ b/pkg/agent/openflow/client_test.go
@@ -34,6 +34,7 @@ import (
 	ovsoftest "antrea.io/antrea/pkg/ovs/openflow/testing"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
 	utilip "antrea.io/antrea/pkg/util/ip"
+	k8sproxy "antrea.io/antrea/third_party/proxy"
 )
 
 const bridgeName = "dummy-br"
@@ -582,3 +583,121 @@ func TestMulticlusterFlowsInstallation(t *testing.T) {
 	_, ok = client.featureMulticluster.cachedFlows.Load(cacheKey)
 	require.False(t, ok)
 }
+
+func TestServiceGroupInstallAndUninstall(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	mockOFBridge := ovsoftest.NewMockBridge(ctrl)
+	m := oftest.NewMockOFEntryOperations(ctrl)
+	ofClient := NewClient(bridgeName, bridgeMgmtAddr, true, false, false, false, true, false, false, false, false)
+	client := ofClient.(*client)
+	client.bridge = mockOFBridge
+	client.cookieAllocator = cookie.NewAllocator(0)
+	client.ofEntryOperations = m
+	client.nodeConfig = nodeConfig
+	client.networkConfig = networkConfig
+	client.serviceConfig = serviceConfig
+	client.ipProtocols = []binding.Protocol{binding.ProtocolIP}
+	client.generatePipelines()
+	endpoints := []k8sproxy.Endpoint{
+		&k8sproxy.BaseEndpointInfo{
+			Endpoint: net.JoinHostPort("192.168.1.2", "8081"),
+			IsLocal:  true,
+		},
+		&k8sproxy.BaseEndpointInfo{
+			Endpoint: net.JoinHostPort("10.20.1.11", "8081"),
+			IsLocal:  false,
+		},
+	}
+	groupID1 := binding.GroupIDType(1)
+	groupID2 := binding.GroupIDType(2)
+	for _, tc := range []struct {
+		groupID         binding.GroupIDType
+		sessionAffinity bool
+		deleteSucceeded bool
+	}{
+		{groupID: groupID1, deleteSucceeded: false, sessionAffinity: true},
+		{groupID: groupID2, deleteSucceeded: true, sessionAffinity: false},
+	} {
+		mockGroup := ovsoftest.NewMockGroup(ctrl)
+		mockBucketBuilder := ovsoftest.NewMockBucketBuilder(ctrl)
+		mockOFBridge.EXPECT().CreateGroup(tc.groupID).Return(mockGroup).Times(1)
+		mockGroup.EXPECT().ResetBuckets().Return(mockGroup).Times(1)
+		mockGroup.EXPECT().Bucket().Return(mockBucketBuilder).Times(len(endpoints))
+		mockBucketBuilder.EXPECT().Weight(gomock.Any()).Return(mockBucketBuilder).Times(len(endpoints))
+		mockBucketBuilder.EXPECT().LoadToRegField(gomock.Any(), gomock.Any()).Return(mockBucketBuilder).AnyTimes()
+		if tc.sessionAffinity {
+			mockBucketBuilder.EXPECT().ResubmitToTable(ServiceLBTable.GetID()).Return(mockBucketBuilder).Times(len(endpoints))
+		} else {
+			mockBucketBuilder.EXPECT().ResubmitToTable(EndpointDNATTable.GetID()).Return(mockBucketBuilder).Times(len(endpoints))
+		}
+		mockBucketBuilder.EXPECT().Done().Return(mockGroup).Times(len(endpoints))
+		mockGroup.EXPECT().Add().Return(nil)
+		err := client.InstallServiceGroup(tc.groupID, tc.sessionAffinity, endpoints)
+		require.NoError(t, err)
+		_, exists := client.featureService.groupCache.Load(tc.groupID)
+		assert.True(t, exists)
+		mockOFBridge.EXPECT().DeleteGroup(tc.groupID).Return(tc.deleteSucceeded)
+		err = client.UninstallServiceGroup(tc.groupID)
+		_, exists = client.featureService.groupCache.Load(tc.groupID)
+		if tc.deleteSucceeded {
+			assert.NoError(t, err)
+			assert.False(t, exists)
+		} else {
+			assert.Error(t, err)
+			assert.True(t, exists)
+		}
+	}
+}
+
+func TestMulticastGroupInstallAndUninstall(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	mockOFBridge := ovsoftest.NewMockBridge(ctrl)
+	m := oftest.NewMockOFEntryOperations(ctrl)
+	ofClient := NewClient(bridgeName, bridgeMgmtAddr, false, true, false, false, false, false, true, false, false)
+	client := ofClient.(*client)
+	client.bridge = mockOFBridge
+	client.cookieAllocator = cookie.NewAllocator(0)
+	client.ofEntryOperations = m
+	client.nodeConfig = nodeConfig
+	client.networkConfig = networkConfig
+	client.serviceConfig = serviceConfig
+	client.ipProtocols = []binding.Protocol{binding.ProtocolIP}
+	client.generatePipelines()
+	localReceivers := []uint32{101, 102}
+	groupID1 := binding.GroupIDType(1)
+	groupID2 := binding.GroupIDType(2)
+	for _, tc := range []struct {
+		groupID         binding.GroupIDType
+		deleteSucceeded bool
+	}{
+		{groupID: groupID1, deleteSucceeded: false},
+		{groupID: groupID2, deleteSucceeded: true},
+	} {
+		mockGroup := ovsoftest.NewMockGroup(ctrl)
+		mockBucketBuilder := ovsoftest.NewMockBucketBuilder(ctrl)
+		mockOFBridge.EXPECT().CreateGroupTypeAll(tc.groupID).Return(mockGroup).Times(1)
+		mockGroup.EXPECT().ResetBuckets().Return(mockGroup).Times(1)
+		mockGroup.EXPECT().Bucket().Return(mockBucketBuilder).Times(len(localReceivers))
+		mockBucketBuilder.EXPECT().LoadToRegField(gomock.Any(), gomock.Any()).Return(mockBucketBuilder).AnyTimes()
+		mockBucketBuilder.EXPECT().ResubmitToTable(MulticastIngressRuleTable.GetID()).Return(mockBucketBuilder).Times(len(localReceivers))
+		mockBucketBuilder.EXPECT().Done().Return(mockGroup).Times(len(localReceivers))
+		mockGroup.EXPECT().Add().Return(nil)
+		err := client.InstallMulticastGroup(tc.groupID, localReceivers)
+		require.NoError(t, err)
+		_, exists := client.featureMulticast.groupCache.Load(tc.groupID)
+		assert.True(t, exists)
+		mockOFBridge.EXPECT().DeleteGroup(tc.groupID).Return(tc.deleteSucceeded)
+		err = client.UninstallMulticastGroup(tc.groupID)
+		if tc.deleteSucceeded {
+			assert.NoError(t, err)
+			_, exists = client.featureMulticast.groupCache.Load(tc.groupID)
+			assert.False(t, exists)
+		} else {
+			assert.Error(t, err)
+			_, exists = client.featureMulticast.groupCache.Load(tc.groupID)
+			assert.True(t, exists)
+		}
+	}
+}
diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go
index f59dc1900..2809b29e5 100644
--- a/pkg/agent/openflow/testing/mock_openflow.go
+++ b/pkg/agent/openflow/testing/mock_openflow.go
@@ -752,32 +752,32 @@ func (mr *MockClientMockRecorder) UninstallEndpointFlows(arg0, arg1 interface{})
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallEndpointFlows", reflect.TypeOf((*MockClient)(nil).UninstallEndpointFlows), arg0, arg1)
 }
 
-// UninstallGroup mocks base method
-func (m *MockClient) UninstallGroup(arg0 openflow.GroupIDType) error {
+// UninstallMulticastFlows mocks base method
+func (m *MockClient) UninstallMulticastFlows(arg0 net.IP) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UninstallGroup", arg0)
+	ret := m.ctrl.Call(m, "UninstallMulticastFlows", arg0)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// UninstallGroup indicates an expected call of UninstallGroup
-func (mr *MockClientMockRecorder) UninstallGroup(arg0 interface{}) *gomock.Call {
+// UninstallMulticastFlows indicates an expected call of UninstallMulticastFlows
+func (mr *MockClientMockRecorder) UninstallMulticastFlows(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallGroup", reflect.TypeOf((*MockClient)(nil).UninstallGroup), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallMulticastFlows", reflect.TypeOf((*MockClient)(nil).UninstallMulticastFlows), arg0)
 }
 
-// UninstallMulticastFlows mocks base method
-func (m *MockClient) UninstallMulticastFlows(arg0 net.IP) error {
+// UninstallMulticastGroup mocks base method
+func (m *MockClient) UninstallMulticastGroup(arg0 openflow.GroupIDType) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UninstallMulticastFlows", arg0)
+	ret := m.ctrl.Call(m, "UninstallMulticastGroup", arg0)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// UninstallMulticastFlows indicates an expected call of UninstallMulticastFlows
-func (mr *MockClientMockRecorder) UninstallMulticastFlows(arg0 interface{}) *gomock.Call {
+// UninstallMulticastGroup indicates an expected call of UninstallMulticastGroup
+func (mr *MockClientMockRecorder) UninstallMulticastGroup(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallMulticastFlows", reflect.TypeOf((*MockClient)(nil).UninstallMulticastFlows), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallMulticastGroup", reflect.TypeOf((*MockClient)(nil).UninstallMulticastGroup), arg0)
 }
 
 // UninstallMulticlusterFlows mocks base method
@@ -879,6 +879,20 @@ func (mr *MockClientMockRecorder) UninstallServiceFlows(arg0, arg1, arg2 interfa
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallServiceFlows", reflect.TypeOf((*MockClient)(nil).UninstallServiceFlows), arg0, arg1, arg2)
 }
 
+// UninstallServiceGroup mocks base method
+func (m *MockClient) UninstallServiceGroup(arg0 openflow.GroupIDType) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UninstallServiceGroup", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// UninstallServiceGroup indicates an expected call of UninstallServiceGroup
+func (mr *MockClientMockRecorder) UninstallServiceGroup(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallServiceGroup", reflect.TypeOf((*MockClient)(nil).UninstallServiceGroup), arg0)
+}
+
 // UninstallTraceflowFlows mocks base method
 func (m *MockClient) UninstallTraceflowFlows(arg0 byte) error {
 	m.ctrl.T.Helper()
diff --git a/pkg/agent/proxy/proxier_test.go b/pkg/agent/proxy/proxier_test.go
index 6c6ec07c2..fc809925f 100644
--- a/pkg/agent/proxy/proxier_test.go
+++ b/pkg/agent/proxy/proxier_test.go
@@ -347,7 +347,7 @@ func testLoadBalancer(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep
 			mockOFClient.EXPECT().InstallServiceFlows(groupID, loadBalancerIP, uint16(svcPort), bindingProtocol, uint16(0), nodeLocalVal, corev1.ServiceTypeLoadBalancer).Times(1)
 		}
 		groupID = fp.groupCounter.AllocateIfNotExist(svcPortName, !nodeLocalVal)
-		mockOFClient.EXPECT().UninstallGroup(groupID).Times(1)
+		mockOFClient.EXPECT().UninstallServiceGroup(groupID).Times(1)
 	}
 	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 	if proxyLoadBalancerIPs {
@@ -466,7 +466,7 @@ func testNodePort(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep2IP
 		mockOFClient.EXPECT().InstallServiceFlows(groupID, gomock.Any(), uint16(svcNodePort), bindingProtocol, uint16(0), nodeLocalVal, corev1.ServiceTypeNodePort).Times(1)
 
 		groupID = fp.groupCounter.AllocateIfNotExist(svcPortName, !nodeLocalVal)
-		mockOFClient.EXPECT().UninstallGroup(groupID).Times(1)
+		mockOFClient.EXPECT().UninstallServiceGroup(groupID).Times(1)
 	}
 	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 	mockRouteClient.EXPECT().AddNodePort(gomock.Any(), uint16(svcNodePort), bindingProtocol).Times(1)
@@ -748,7 +748,7 @@ func testClusterIPRemoval(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool)
 	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP, uint16(svcPort), bindingProtocol).Times(1)
 	mockOFClient.EXPECT().UninstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().UninstallGroup(gomock.Any()).Times(1)
+	mockOFClient.EXPECT().UninstallServiceGroup(gomock.Any()).Times(1)
 	fp.syncProxyRules()
 
 	fp.serviceChanges.OnServiceUpdate(service, nil)
@@ -1231,8 +1231,8 @@ func TestServicesWithSameEndpoints(t *testing.T) {
 	mockOFClient.EXPECT().InstallServiceFlows(groupID2, svcIP2, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP1, uint16(svcPort), bindingProtocol).Times(1)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP2, uint16(svcPort), bindingProtocol).Times(1)
-	mockOFClient.EXPECT().UninstallGroup(groupID1).Times(1)
-	mockOFClient.EXPECT().UninstallGroup(groupID2).Times(1)
+	mockOFClient.EXPECT().UninstallServiceGroup(groupID1).Times(1)
+	mockOFClient.EXPECT().UninstallServiceGroup(groupID2).Times(1)
 	// Since these two Services reference to the same Endpoint, there should only be one operation.
 	mockOFClient.EXPECT().UninstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 
diff --git a/pkg/agent/util/net_test.go b/pkg/agent/util/net_test.go
index cb04c2d8f..57f8ac38c 100644
--- a/pkg/agent/util/net_test.go
+++ b/pkg/agent/util/net_test.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"k8s.io/apimachinery/pkg/util/sets"
 
 	"antrea.io/antrea/pkg/util/ip"
@@ -66,3 +67,15 @@ func TestGetDefaultLocalNodeAddr(t *testing.T) {
 	}
 	t.Logf("IP obtained %s, %v", localAddr, dev)
 }
+
+func TestGenerateRandomMAC(t *testing.T) {
+	validateBits := func(mac net.HardwareAddr) (byte, byte) {
+		localBit := mac[0] & 0x2 >> 1
+		mcastBit := mac[0] & 0x1
+		return localBit, mcastBit
+	}
+	mac1 := GenerateRandomMAC()
+	localBit, mcastBit := validateBits(mac1)
+	assert.Equal(t, uint8(1), localBit)
+	assert.Equal(t, uint8(0), mcastBit)
+}
diff --git a/pkg/ovs/openflow/ofctrl_bridge_test.go b/pkg/ovs/openflow/ofctrl_bridge_test.go
new file mode 100644
index 000000000..2295b3d22
--- /dev/null
+++ b/pkg/ovs/openflow/ofctrl_bridge_test.go
@@ -0,0 +1,114 @@
+// Copyright 2022 Antrea Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package openflow
+
+import (
+	"net"
+	"sync"
+	"testing"
+	"time"
+
+	"antrea.io/libOpenflow/openflow13"
+	"antrea.io/libOpenflow/util"
+	"antrea.io/ofnet/ofctrl"
+
+	"antrea.io/antrea/pkg/ovs/ovsconfig"
+)
+
+type fakeConn struct{}
+
+func (f *fakeConn) Close() error {
+	return nil
+}
+
+func (f *fakeConn) Read(b []byte) (int, error) {
+	return len(b), nil
+}
+
+func (f *fakeConn) Write(b []byte) (int, error) {
+	return len(b), nil
+}
+
+func (f *fakeConn) LocalAddr() net.Addr {
+	return nil
+}
+
+func (f *fakeConn) RemoteAddr() net.Addr {
+	return nil
+}
+
+func (f *fakeConn) SetDeadline(t time.Time) error {
+	return nil
+}
+
+func (f *fakeConn) SetReadDeadline(t time.Time) error {
+	return nil
+}
+
+func (f *fakeConn) SetWriteDeadline(t time.Time) error {
+	return nil
+}
+func newFakeOFSwitch(stream *util.MessageStream, app ofctrl.AppInterface) *ofctrl.OFSwitch {
+	dpid, _ := net.ParseMAC("01:02:03:04:05:06:07:08")
+	connCh := make(chan int)
+	sw := ofctrl.NewSwitch(stream, dpid, app, connCh, 100)
+	return sw
+}
+
+func sendTlvMapReply(stream *util.MessageStream) {
+	reply := &openflow13.TLVTableReply{
+		MaxSpace:  248,
+		MaxFields: 62,
+		TlvMaps: []*openflow13.TLVTableMap{
+			{
+				OptClass:  0xffff,
+				OptType:   0,
+				OptLength: 16,
+				Index:     0,
+			},
+			{
+				OptClass:  0xffff,
+				OptType:   1,
+				OptLength: 16,
+				Index:     1,
+			},
+		},
+	}
+	tlvReplyMessage := openflow13.NewNXTVendorHeader(openflow13.Type_TlvTableReply)
+	tlvReplyMessage.VendorData = reply
+	stream.Inbound <- tlvReplyMessage
+}
+
+// TestOFBridgeIsConnected verifies it's thread-safe to call OFBridge's IsConnected method.
+func TestOFBridgeIsConnected(t *testing.T) {
+	stream := util.NewMessageStream(&fakeConn{}, nil)
+	b := NewOFBridge("test-br", GetMgmtAddress(ovsconfig.DefaultOVSRunDir, "test-br"))
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		newFakeOFSwitch(stream, b)
+	}()
+	go func() {
+		time.Sleep(time.Millisecond * 100)
+		sendTlvMapReply(stream)
+	}()
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		b.IsConnected()
+	}()
+	wg.Wait()
+}
diff --git a/pkg/ovs/openflow/ofctrl_packetout_test.go b/pkg/ovs/openflow/ofctrl_packetout_test.go
index 64001df10..e3f30b069 100644
--- a/pkg/ovs/openflow/ofctrl_packetout_test.go
+++ b/pkg/ovs/openflow/ofctrl_packetout_test.go
@@ -15,6 +15,7 @@
 package openflow
 
 import (
+	"math/rand"
 	"net"
 	"reflect"
 	"testing"
@@ -602,6 +603,9 @@ func Test_ofPacketOutBuilder_SetIPFlags(t *testing.T) {
 }
 
 func Test_ofPacketOutBuilder_Done(t *testing.T) {
+	// reset the rand seed in case there are any other test cases use math/rand to generate
+	// random numbers before running this test case.
+	rand.Seed(1)
 	type fields struct {
 		pktOut  *ofctrl.PacketOut
 		icmpID  *uint16
diff --git a/pkg/ovs/openflow/testing/mock_openflow.go b/pkg/ovs/openflow/testing/mock_openflow.go
index 45a1e3f47..125fec994 100644
--- a/pkg/ovs/openflow/testing/mock_openflow.go
+++ b/pkg/ovs/openflow/testing/mock_openflow.go
@@ -14,7 +14,7 @@
 //
 
 // Code generated by MockGen. DO NOT EDIT.
-// Source: antrea.io/antrea/pkg/ovs/openflow (interfaces: Bridge,Table,Flow,Action,CTAction,FlowBuilder)
+// Source: antrea.io/antrea/pkg/ovs/openflow (interfaces: Bridge,Table,Flow,Action,CTAction,FlowBuilder,Group,BucketBuilder)
 
 // Package testing is a generated GoMock package.
 package testing
@@ -2154,3 +2154,272 @@ func (mr *MockFlowBuilderMockRecorder) SetIdleTimeout(arg0 interface{}) *gomock.
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIdleTimeout", reflect.TypeOf((*MockFlowBuilder)(nil).SetIdleTimeout), arg0)
 }
+
+// MockGroup is a mock of Group interface
+type MockGroup struct {
+	ctrl     *gomock.Controller
+	recorder *MockGroupMockRecorder
+}
+
+// MockGroupMockRecorder is the mock recorder for MockGroup
+type MockGroupMockRecorder struct {
+	mock *MockGroup
+}
+
+// NewMockGroup creates a new mock instance
+func NewMockGroup(ctrl *gomock.Controller) *MockGroup {
+	mock := &MockGroup{ctrl: ctrl}
+	mock.recorder = &MockGroupMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockGroup) EXPECT() *MockGroupMockRecorder {
+	return m.recorder
+}
+
+// Add mocks base method
+func (m *MockGroup) Add() error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Add")
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Add indicates an expected call of Add
+func (mr *MockGroupMockRecorder) Add() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockGroup)(nil).Add))
+}
+
+// Bucket mocks base method
+func (m *MockGroup) Bucket() openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Bucket")
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// Bucket indicates an expected call of Bucket
+func (mr *MockGroupMockRecorder) Bucket() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bucket", reflect.TypeOf((*MockGroup)(nil).Bucket))
+}
+
+// Delete mocks base method
+func (m *MockGroup) Delete() error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Delete")
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Delete indicates an expected call of Delete
+func (mr *MockGroupMockRecorder) Delete() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockGroup)(nil).Delete))
+}
+
+// GetBundleMessage mocks base method
+func (m *MockGroup) GetBundleMessage(arg0 openflow.OFOperation) (ofctrl.OpenFlowModMessage, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetBundleMessage", arg0)
+	ret0, _ := ret[0].(ofctrl.OpenFlowModMessage)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetBundleMessage indicates an expected call of GetBundleMessage
+func (mr *MockGroupMockRecorder) GetBundleMessage(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBundleMessage", reflect.TypeOf((*MockGroup)(nil).GetBundleMessage), arg0)
+}
+
+// KeyString mocks base method
+func (m *MockGroup) KeyString() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "KeyString")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// KeyString indicates an expected call of KeyString
+func (mr *MockGroupMockRecorder) KeyString() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyString", reflect.TypeOf((*MockGroup)(nil).KeyString))
+}
+
+// Modify mocks base method
+func (m *MockGroup) Modify() error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Modify")
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Modify indicates an expected call of Modify
+func (mr *MockGroupMockRecorder) Modify() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Modify", reflect.TypeOf((*MockGroup)(nil).Modify))
+}
+
+// Reset mocks base method
+func (m *MockGroup) Reset() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Reset")
+}
+
+// Reset indicates an expected call of Reset
+func (mr *MockGroupMockRecorder) Reset() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockGroup)(nil).Reset))
+}
+
+// ResetBuckets mocks base method
+func (m *MockGroup) ResetBuckets() openflow.Group {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ResetBuckets")
+	ret0, _ := ret[0].(openflow.Group)
+	return ret0
+}
+
+// ResetBuckets indicates an expected call of ResetBuckets
+func (mr *MockGroupMockRecorder) ResetBuckets() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetBuckets", reflect.TypeOf((*MockGroup)(nil).ResetBuckets))
+}
+
+// Type mocks base method
+func (m *MockGroup) Type() openflow.EntryType {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Type")
+	ret0, _ := ret[0].(openflow.EntryType)
+	return ret0
+}
+
+// Type indicates an expected call of Type
+func (mr *MockGroupMockRecorder) Type() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockGroup)(nil).Type))
+}
+
+// MockBucketBuilder is a mock of BucketBuilder interface
+type MockBucketBuilder struct {
+	ctrl     *gomock.Controller
+	recorder *MockBucketBuilderMockRecorder
+}
+
+// MockBucketBuilderMockRecorder is the mock recorder for MockBucketBuilder
+type MockBucketBuilderMockRecorder struct {
+	mock *MockBucketBuilder
+}
+
+// NewMockBucketBuilder creates a new mock instance
+func NewMockBucketBuilder(ctrl *gomock.Controller) *MockBucketBuilder {
+	mock := &MockBucketBuilder{ctrl: ctrl}
+	mock.recorder = &MockBucketBuilderMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockBucketBuilder) EXPECT() *MockBucketBuilderMockRecorder {
+	return m.recorder
+}
+
+// Done mocks base method
+func (m *MockBucketBuilder) Done() openflow.Group {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Done")
+	ret0, _ := ret[0].(openflow.Group)
+	return ret0
+}
+
+// Done indicates an expected call of Done
+func (mr *MockBucketBuilderMockRecorder) Done() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Done", reflect.TypeOf((*MockBucketBuilder)(nil).Done))
+}
+
+// LoadReg mocks base method
+func (m *MockBucketBuilder) LoadReg(arg0 int, arg1 uint32) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LoadReg", arg0, arg1)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// LoadReg indicates an expected call of LoadReg
+func (mr *MockBucketBuilderMockRecorder) LoadReg(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadReg", reflect.TypeOf((*MockBucketBuilder)(nil).LoadReg), arg0, arg1)
+}
+
+// LoadRegRange mocks base method
+func (m *MockBucketBuilder) LoadRegRange(arg0 int, arg1 uint32, arg2 *openflow.Range) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LoadRegRange", arg0, arg1, arg2)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// LoadRegRange indicates an expected call of LoadRegRange
+func (mr *MockBucketBuilderMockRecorder) LoadRegRange(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadRegRange", reflect.TypeOf((*MockBucketBuilder)(nil).LoadRegRange), arg0, arg1, arg2)
+}
+
+// LoadToRegField mocks base method
+func (m *MockBucketBuilder) LoadToRegField(arg0 *openflow.RegField, arg1 uint32) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LoadToRegField", arg0, arg1)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// LoadToRegField indicates an expected call of LoadToRegField
+func (mr *MockBucketBuilderMockRecorder) LoadToRegField(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadToRegField", reflect.TypeOf((*MockBucketBuilder)(nil).LoadToRegField), arg0, arg1)
+}
+
+// LoadXXReg mocks base method
+func (m *MockBucketBuilder) LoadXXReg(arg0 int, arg1 []byte) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LoadXXReg", arg0, arg1)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// LoadXXReg indicates an expected call of LoadXXReg
+func (mr *MockBucketBuilderMockRecorder) LoadXXReg(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadXXReg", reflect.TypeOf((*MockBucketBuilder)(nil).LoadXXReg), arg0, arg1)
+}
+
+// ResubmitToTable mocks base method
+func (m *MockBucketBuilder) ResubmitToTable(arg0 byte) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ResubmitToTable", arg0)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// ResubmitToTable indicates an expected call of ResubmitToTable
+func (mr *MockBucketBuilderMockRecorder) ResubmitToTable(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResubmitToTable", reflect.TypeOf((*MockBucketBuilder)(nil).ResubmitToTable), arg0)
+}
+
+// Weight mocks base method
+func (m *MockBucketBuilder) Weight(arg0 uint16) openflow.BucketBuilder {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Weight", arg0)
+	ret0, _ := ret[0].(openflow.BucketBuilder)
+	return ret0
+}
+
+// Weight indicates an expected call of Weight
+func (mr *MockBucketBuilderMockRecorder) Weight(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Weight", reflect.TypeOf((*MockBucketBuilder)(nil).Weight), arg0)
+}
diff --git a/pkg/ovs/ovsconfig/ovs_client_test.go b/pkg/ovs/ovsconfig/ovs_client_test.go
index f8e24a1a0..35daad09b 100644
--- a/pkg/ovs/ovsconfig/ovs_client_test.go
+++ b/pkg/ovs/ovsconfig/ovs_client_test.go
@@ -15,6 +15,7 @@
 package ovsconfig
 
 import (
+	"net"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -37,3 +38,83 @@ func TestOVSClient(t *testing.T) {
 	assert.NoError(t, err)
 
 }
+
+func TestBuildPortDataCommon(t *testing.T) {
+	macStr := "9a:23:45:23:22:41"
+	intfMAC, _ := net.ParseMAC(macStr)
+	for _, tc := range []struct {
+		name     string
+		port     map[string]interface{}
+		intf     map[string]interface{}
+		portData *OVSPortData
+	}{
+		{
+			name: "gw-port",
+			port: map[string]interface{}{"name": "antrea-gw0", "external_ids": []interface{}{"map", []interface{}{[]interface{}{"antrea-type", "gateway"}}}},
+			intf: map[string]interface{}{"name": "antrea-gw0", "mac": macStr, "type": "internal", "ofport": float64(2), "options": []interface{}{""}},
+			portData: &OVSPortData{
+				Name:        "antrea-gw0",
+				ExternalIDs: map[string]string{"antrea-type": "gateway"},
+				Options:     map[string]string{},
+				IFType:      "internal",
+				OFPort:      2,
+				MAC:         intfMAC,
+			},
+		}, {
+			name: "tun-port",
+			port: map[string]interface{}{"name": "antrea-tun0", "external_ids": []interface{}{"map", []interface{}{[]interface{}{"antrea-type", "tunnel"}}}},
+			intf: map[string]interface{}{"name": "antrea-tun0", "mac": macStr, "type": "geneve", "ofport": float64(1), "options": []interface{}{"map", []interface{}{[]interface{}{"key", "flow"}, []interface{}{"remote_ip", "flow"}}}},
+			portData: &OVSPortData{
+				Name:        "antrea-tun0",
+				ExternalIDs: map[string]string{"antrea-type": "tunnel"},
+				Options:     map[string]string{"key": "flow", "remote_ip": "flow"},
+				IFType:      "geneve",
+				OFPort:      1,
+				MAC:         intfMAC,
+			},
+		}, {
+			name: "general-port",
+			port: map[string]interface{}{"name": "p0", "external_ids": []interface{}{"map", []interface{}{[]interface{}{"antrea-type", "container"}, []interface{}{"ip", "1.2.3.4"}}}},
+			intf: map[string]interface{}{"name": "p0", "mac": []interface{}{macStr}, "type": "", "ofport": float64(3), "options": []interface{}{""}},
+			portData: &OVSPortData{
+				Name:        "p0",
+				ExternalIDs: map[string]string{"antrea-type": "container", "ip": "1.2.3.4"},
+				Options:     map[string]string{},
+				IFType:      "",
+				OFPort:      3,
+				MAC:         intfMAC,
+			},
+		}, {
+			name: "access-port",
+			port: map[string]interface{}{"name": "p1", "tag": float64(10), "external_ids": []interface{}{"map", []interface{}{[]interface{}{"antrea-type", "container"}, []interface{}{"ip", "1.2.3.5"}}}},
+			intf: map[string]interface{}{"name": "p1", "mac": macStr, "type": "", "ofport": float64(3), "options": []interface{}{""}},
+			portData: &OVSPortData{
+				Name:        "p1",
+				ExternalIDs: map[string]string{"antrea-type": "container", "ip": "1.2.3.5"},
+				Options:     map[string]string{},
+				IFType:      "",
+				OFPort:      3,
+				VLANID:      10,
+				MAC:         intfMAC,
+			},
+		}, {
+			name: "no-mac-port",
+			port: map[string]interface{}{"name": "p2", "external_ids": []interface{}{"map", []interface{}{[]interface{}{"antrea-type", "container"}, []interface{}{"ip", "1.2.3.5"}}}},
+			intf: map[string]interface{}{"name": "p2", "mac": []interface{}{}, "type": "", "ofport": float64(4), "options": []interface{}{""}},
+			portData: &OVSPortData{
+				Name:        "p2",
+				ExternalIDs: map[string]string{"antrea-type": "container", "ip": "1.2.3.5"},
+				Options:     map[string]string{},
+				IFType:      "",
+				OFPort:      4,
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			portData := &OVSPortData{}
+			buildPortDataCommon(tc.port, tc.intf, portData)
+			assert.Equal(t, tc.portData, portData)
+		})
+	}
+
+}
diff --git a/pkg/ovs/ovsconfig/testing/mock_ovsconfig.go b/pkg/ovs/ovsconfig/testing/mock_ovsconfig.go
index 486e66f64..3a478e84f 100644
--- a/pkg/ovs/ovsconfig/testing/mock_ovsconfig.go
+++ b/pkg/ovs/ovsconfig/testing/mock_ovsconfig.go
@@ -22,6 +22,7 @@ package testing
 import (
 	ovsconfig "antrea.io/antrea/pkg/ovs/ovsconfig"
 	gomock "github.com/golang/mock/gomock"
+	net "net"
 	reflect "reflect"
 )
 
@@ -440,6 +441,20 @@ func (mr *MockOVSBridgeClientMockRecorder) SetExternalIDs(arg0 interface{}) *gom
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetExternalIDs", reflect.TypeOf((*MockOVSBridgeClient)(nil).SetExternalIDs), arg0)
 }
 
+// SetInterfaceMAC mocks base method
+func (m *MockOVSBridgeClient) SetInterfaceMAC(arg0 string, arg1 net.HardwareAddr) ovsconfig.Error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SetInterfaceMAC", arg0, arg1)
+	ret0, _ := ret[0].(ovsconfig.Error)
+	return ret0
+}
+
+// SetInterfaceMAC indicates an expected call of SetInterfaceMAC
+func (mr *MockOVSBridgeClientMockRecorder) SetInterfaceMAC(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetInterfaceMAC", reflect.TypeOf((*MockOVSBridgeClient)(nil).SetInterfaceMAC), arg0, arg1)
+}
+
 // SetInterfaceMTU mocks base method
 func (m *MockOVSBridgeClient) SetInterfaceMTU(arg0 string, arg1 int) error {
 	m.ctrl.T.Helper()
diff --git a/test/e2e/antreapolicy_test.go b/test/e2e/antreapolicy_test.go
index b2bff5980..c3a5b1645 100644
--- a/test/e2e/antreapolicy_test.go
+++ b/test/e2e/antreapolicy_test.go
@@ -2477,23 +2477,31 @@ func testFQDNPolicy(t *testing.T) {
 	log.SetLevel(log.TraceLevel)
 	defer log.SetLevel(logLevel)
 	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("test-acnp-drop-all-google").
+	builder = builder.SetName("test-acnp-reject-all-github").
 		SetTier("application").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ACNPAppliedToSpec{{NSSelector: map[string]string{}}})
-	builder.AddFQDNRule("*google.com", ProtocolTCP, nil, nil, nil, "r1", nil, crdv1alpha1.RuleActionReject)
+	// The DNS server of e2e testbeds may reply large DNS response with a long list of AUTHORITY SECTION and ADDITIONAL
+	// SECTION, which causes the response to be truncated and the clients to retry over TCP. However, antrea-agent only
+	// inspects DNS UDP packets, the DNS resolution result will be missed by it if the clients uses DNS over TCP. And if
+	// the IP got from DNS/TCP response is different from the IP got from the first DNS/UDP response, the following
+	// application traffic will bypass FQDN NetworkPolicy.
+	// So we changed the target domain from google.com to github.com, which has a more stable DNS resolution result. The
+	// change could be reverted once we support inspecting DNS/TCP traffic.
+	// See https://github.com/antrea-io/antrea/issues/4130 for more details.
+	builder.AddFQDNRule("*github.com", ProtocolTCP, nil, nil, nil, "r1", nil, crdv1alpha1.RuleActionReject)
 	builder.AddFQDNRule("wayfair.com", ProtocolTCP, nil, nil, nil, "r2", nil, crdv1alpha1.RuleActionDrop)
 
 	testcases := []podToAddrTestStep{
 		{
 			Pod(namespaces["x"] + "/a"),
-			"drive.google.com",
+			"docs.github.com",
 			80,
 			Rejected,
 		},
 		{
 			Pod(namespaces["x"] + "/b"),
-			"maps.google.com",
+			"api.github.com",
 			80,
 			Rejected,
 		},
diff --git a/test/integration/agent/openflow_test.go b/test/integration/agent/openflow_test.go
index b3fdc9162..b7484b778 100644
--- a/test/integration/agent/openflow_test.go
+++ b/test/integration/agent/openflow_test.go
@@ -706,7 +706,7 @@ func uninstallServiceFlowsFunc(t *testing.T, gid uint32, svc svcConfig, endpoint
 	groupID := ofconfig.GroupIDType(gid)
 	err := c.UninstallServiceFlows(svc.ip, svc.port, svc.protocol)
 	assert.Nil(t, err)
-	err = c.UninstallGroup(groupID)
+	err = c.UninstallServiceGroup(groupID)
 	assert.Nil(t, err)
 	for _, ep := range endpointList {
 		err := c.UninstallEndpointFlows(svc.protocol, ep)