Repository: antrea-io/antrea
Pull request #4049 (2022-07-22 02:51:36)

Automated cherry pick of #3995: Set up Kind clusters to run multi-cluster e2e
Cherry pick of #3995 on release-1.7.

#3995: Set up Kind clusters to run multi-cluster e2e

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.

luolanzone: /test-multicluster-e2e
codecov[bot]: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/4049?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) Report
> Merging [#4049](https://codecov.io/gh/antrea-io/antrea/pull/4049?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (294e1e7) 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.11%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/antrea-io/antrea/pull/4049/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/4049?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    #4049       +/-   ##
================================================
- Coverage        63.62%   44.50%   -19.12%     
================================================
  Files              293      224       -69     
  Lines            43405    39457     -3948     
================================================
- Hits             27616    17561    -10055     
- Misses           13558    20535     +6977     
+ Partials          2231     1361      -870     
```

| Flag | Coverage Δ | |
|---|---|---|
| kind-e2e-tests | `?` | |
| unit-tests | `44.50% <ø> (+<0.01%)` | :arrow_up: |

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/4049?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/4049/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/4049/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/4049/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/4049/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/4049/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/4049/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/4049/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/4049/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/4049/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/4049/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 [204 more](https://codecov.io/gh/antrea-io/antrea/pull/4049/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | |

luolanzone: @tnqn  Could you help to check if it's Ok to back port this to v1.7? The multicluster e2e in jenkins is running with `--kind` by default now. In order to make mc e2e working on v1.7, we need to back port PR3995. It has a dependency PR3882, I created another PR to cherry-pick it. https://github.com/antrea-io/antrea/pull/4051

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-mc.sh b/ci/jenkins/test-mc.sh
index 9abb56b84..4170f9d81 100755
--- a/ci/jenkins/test-mc.sh
+++ b/ci/jenkins/test-mc.sh
@@ -21,40 +21,41 @@ function echoerr {
 }
 
 DEFAULT_WORKDIR="/var/lib/jenkins"
-DEFAULT_KUBECONFIG_PATH=$DEFAULT_WORKDIR/kube.conf
 WORKDIR=$DEFAULT_WORKDIR
 TESTCASE=""
 TEST_FAILURE=false
 DOCKER_REGISTRY=$(head -n1 "${WORKSPACE}/ci/docker-registry")
-GO_VERSION=$(head -n1 "${WORKSPACE}/build/images/deps/go-version")
-IMAGE_PULL_POLICY="Always"
 MULTICLUSTER_KUBECONFIG_PATH=$WORKDIR/.kube
 LEADER_CLUSTER_CONFIG="--kubeconfig=$MULTICLUSTER_KUBECONFIG_PATH/leader"
 EAST_CLUSTER_CONFIG="--kubeconfig=$MULTICLUSTER_KUBECONFIG_PATH/east"
 WEST_CLUSTER_CONFIG="--kubeconfig=$MULTICLUSTER_KUBECONFIG_PATH/west"
+CLUSTER_NAMES=("leader" "east" "west")
 ENABLE_MC_GATEWAY=false
 IS_CONTAINERD=false
 CODECOV_TOKEN=""
 COVERAGE=false
+KIND=false
+DEBUG=false
 
 multicluster_kubeconfigs=($EAST_CLUSTER_CONFIG $LEADER_CLUSTER_CONFIG $WEST_CLUSTER_CONFIG)
 membercluster_kubeconfigs=($EAST_CLUSTER_CONFIG $WEST_CLUSTER_CONFIG)
 
-
 CLEAN_STALE_IMAGES="docker system prune --force --all --filter until=48h"
 
 _usage="Usage: $0 [--kubeconfigs-path <KubeconfigSavePath>] [--workdir <HomePath>]
-                  [--testcase <e2e>] [--mc-gateway] [--codecov-token] [--coverage]
+                  [--testcase <e2e>] [--mc-gateway] [--codecov-token] [--coverage] [--kind] [--debug]
 
 Run Antrea multi-cluster e2e tests on a remote (Jenkins) Linux Cluster Set.
 
         --kubeconfigs-path            Path of cluster set kubeconfigs.
-        --workdir                     Home path for Go, vSphere information and antrea_logs during cluster setup. Default is $WORKDIR.
+        --workdir                     Home path for Go, antrea_logs during cluster setup. Default is $WORKDIR.
         --testcase                    Antrea multi-cluster e2e test cases on a Linux cluster set.
         --registry                    The docker registry to use instead of dockerhub.
         --mc-gateway                  Enable Multicluster Gateway.
         --codecov-token               Token used to upload coverage report(s) to Codecov.
-        --coverage                    Run e2e with coverage."
+        --coverage                    Run e2e with coverage.
+        --kind                        Run e2e on Kind clusters.
+        --debug                       Do not clean up Kind clusters when --kind is set."
 
 function print_usage {
     echoerr "$_usage"
@@ -94,6 +95,14 @@ case $key in
     COVERAGE=true
     shift
     ;;
+    --kind)
+    KIND=true
+    shift
+    ;;
+    --debug)
+    DEBUG=true
+    shift
+    ;;
     -h|--help)
     print_usage
     exit 0
@@ -151,14 +160,23 @@ function cleanup_multicluster_antrea {
 }
 
 function clean_multicluster {
-    echo "====== Cleanup Multicluster Antrea Installation in clusters ======"
-    for kubeconfig in "${multicluster_kubeconfigs[@]}"
-    do
-        cleanup_multicluster_ns "antrea-multicluster-test" $kubeconfig
-        cleanup_multicluster_ns "antrea-multicluster" $kubeconfig
-        cleanup_multicluster_controller $kubeconfig
-        cleanup_multicluster_antrea $kubeconfig
-    done
+    if [[ ${KIND} == "true" ]]; then
+        if [[ $DEBUG != "true" ]]; then
+            echo "====== Cleanup Kind clusters ======"
+            for name in ${CLUSTER_NAMES[*]}; do
+                ./ci/kind/kind-setup.sh destroy ${name}
+            done
+        fi
+    else
+        echo "====== Cleanup Multicluster Antrea Installation in clusters ======"
+        for kubeconfig in "${multicluster_kubeconfigs[@]}"
+        do
+            cleanup_multicluster_ns "antrea-multicluster-test" $kubeconfig
+            cleanup_multicluster_ns "antrea-multicluster" $kubeconfig
+            cleanup_multicluster_controller $kubeconfig
+            cleanup_multicluster_antrea $kubeconfig
+        done
+    fi
 }
 
 function wait_for_antrea_multicluster_pods_ready {
@@ -247,20 +265,26 @@ function deliver_antrea_multicluster {
 
     DOCKER_REGISTRY="${DOCKER_REGISTRY}" ./hack/build-antrea-linux-all.sh --pull
     echo "====== Delivering Antrea to all the Nodes ======"
-    docker save -o ${WORKDIR}/antrea-ubuntu.tar $DOCKER_REGISTRY/antrea/antrea-ubuntu:latest
+    docker save -o ${WORKDIR}/antrea-ubuntu.tar ${DOCKER_REGISTRY}/antrea/antrea-ubuntu:latest
 
 
-    for kubeconfig in "${multicluster_kubeconfigs[@]}"
-    do
-       kubectl get nodes -o wide --no-headers=true ${kubeconfig}| awk '{print $6}' | while read IP; do
-            rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" "${WORKDIR}"/antrea-ubuntu.tar jenkins@[${IP}]:${WORKDIR}/antrea-ubuntu.tar
-            if ${IS_CONTAINERD};then
-              ssh -o StrictHostKeyChecking=no -n jenkins@${IP} "${CLEAN_STALE_IMAGES}; sudo ctr -n=k8s.io images import ${WORKDIR}/antrea-ubuntu.tar" || true
-            else
-              ssh -o StrictHostKeyChecking=no -n jenkins@${IP} "${CLEAN_STALE_IMAGES}; docker load -i ${WORKDIR}/antrea-ubuntu.tar" || true
-            fi
-       done
-    done
+    if [[ ${KIND} == "true" ]]; then
+        for name in ${CLUSTER_NAMES[*]}; do
+            kind load docker-image ${DOCKER_REGISTRY}/antrea/antrea-ubuntu:latest --name ${name}
+        done
+    else
+        for kubeconfig in "${multicluster_kubeconfigs[@]}"
+        do
+            kubectl get nodes -o wide --no-headers=true ${kubeconfig}| awk '{print $6}' | while read IP; do
+                 rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" "${WORKDIR}"/antrea-ubuntu.tar jenkins@[${IP}]:${WORKDIR}/antrea-ubuntu.tar
+                 if ${IS_CONTAINERD};then
+                   ssh -o StrictHostKeyChecking=no -n jenkins@${IP} "${CLEAN_STALE_IMAGES}; sudo ctr -n=k8s.io images import ${WORKDIR}/antrea-ubuntu.tar" || true
+                 else
+                   ssh -o StrictHostKeyChecking=no -n jenkins@${IP} "${CLEAN_STALE_IMAGES}; docker load -i ${WORKDIR}/antrea-ubuntu.tar" || true
+                 fi
+            done
+        done
+    fi
 }
 
 function deliver_multicluster_controller {
@@ -270,42 +294,58 @@ function deliver_multicluster_controller {
     export GOROOT=/usr/local/go
     export PATH=${GOROOT}/bin:$PATH
 
+    DEFAULT_IMAGE="${DOCKER_REGISTRY}"/antrea/antrea-mc-controller:latest
     if $COVERAGE;then
-      export NO_PULL=1;make antrea-mc-controller-coverage
-      docker save "${DOCKER_REGISTRY}"/antrea/antrea-mc-controller-coverage:latest -o "${WORKDIR}"/antrea-mcs.tar
-      ./multicluster/hack/generate-manifest.sh -l antrea-multicluster -c > ./multicluster/test/yamls/leader-manifest.yml
-      ./multicluster/hack/generate-manifest.sh -m -c > ./multicluster/test/yamls/member-manifest.yml
+        export NO_PULL=1;make antrea-mc-controller-coverage
+        DEFAULT_IMAGE="${DOCKER_REGISTRY}"/antrea/antrea-mc-controller-coverage:latest
+        docker save "${DEFAULT_IMAGE}" -o "${WORKDIR}"/antrea-mcs.tar
+        ./multicluster/hack/generate-manifest.sh -l antrea-multicluster -c > ./multicluster/test/yamls/leader-manifest.yml
+        ./multicluster/hack/generate-manifest.sh -m -c > ./multicluster/test/yamls/member-manifest.yml
     else
-      export NO_PULL=1;make antrea-mc-controller
-      docker save "${DOCKER_REGISTRY}"/antrea/antrea-mc-controller:latest -o "${WORKDIR}"/antrea-mcs.tar
-      ./multicluster/hack/generate-manifest.sh -l antrea-multicluster > ./multicluster/test/yamls/leader-manifest.yml
-      ./multicluster/hack/generate-manifest.sh -m > ./multicluster/test/yamls/member-manifest.yml
+        export NO_PULL=1;make antrea-mc-controller
+        docker save "${DEFAULT_IMAGE}" -o "${WORKDIR}"/antrea-mcs.tar
+        ./multicluster/hack/generate-manifest.sh -l antrea-multicluster > ./multicluster/test/yamls/leader-manifest.yml
+        ./multicluster/hack/generate-manifest.sh -m > ./multicluster/test/yamls/member-manifest.yml
     fi
 
-    for kubeconfig in "${multicluster_kubeconfigs[@]}"
-    do
-        kubectl get nodes -o wide --no-headers=true "${kubeconfig}" | awk '{print $6}' | while read IP; do
-            rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" "${WORKDIR}"/antrea-mcs.tar jenkins@[${IP}]:${WORKDIR}/antrea-mcs.tar
-            if ${IS_CONTAINERD};then
-              ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "${CLEAN_STALE_IMAGES}; sudo ctr -n=k8s.io images import ${WORKDIR}/antrea-mcs.tar" || true
-            else
-              ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "${CLEAN_STALE_IMAGES}; docker load -i ${WORKDIR}/antrea-mcs.tar" || true
-            fi
+    if [[ ${KIND} == "true" ]]; then
+        for name in ${CLUSTER_NAMES[*]}; do
+            kind load docker-image ${DEFAULT_IMAGE} --name ${name}
         done
-    done
+    else
+        for kubeconfig in "${multicluster_kubeconfigs[@]}"
+        do
+            kubectl get nodes -o wide --no-headers=true "${kubeconfig}" | awk '{print $6}' | while read IP; do
+                rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" "${WORKDIR}"/antrea-mcs.tar jenkins@[${IP}]:${WORKDIR}/antrea-mcs.tar
+                if ${IS_CONTAINERD};then
+                  ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "${CLEAN_STALE_IMAGES}; sudo ctr -n=k8s.io images import ${WORKDIR}/antrea-mcs.tar" || true
+                else
+                  ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "${CLEAN_STALE_IMAGES}; docker load -i ${WORKDIR}/antrea-mcs.tar" || true
+                fi
+            done
+        done
+    fi
 
     leader_ip=$(kubectl get nodes -o wide --no-headers=true ${LEADER_CLUSTER_CONFIG} | awk -v role1="master" -v role2="control-plane" '($3 ~ role1 || $3 ~ role2) {print $6}')
     sed -i "s|<LEADER_CLUSTER_IP>|${leader_ip}|" ./multicluster/test/yamls/east-member-cluster.yml
     sed -i "s|<LEADER_CLUSTER_IP>|${leader_ip}|" ./multicluster/test/yamls/west-member-cluster.yml
-    rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" ./multicluster/test/yamls/test-acnp-copy-span-ns-isolation.yml jenkins@["${leader_ip}"]:"${WORKDIR}"/test-acnp-copy-span-ns-isolation.yml
+    if [[ ${KIND} == "true" ]]; then
+        docker cp ./multicluster/test/yamls/test-acnp-copy-span-ns-isolation.yml leader-control-plane:/root/test-acnp-copy-span-ns-isolation.yml
+    else
+        rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" ./multicluster/test/yamls/test-acnp-copy-span-ns-isolation.yml jenkins@["${leader_ip}"]:"${WORKDIR}"/test-acnp-copy-span-ns-isolation.yml
+    fi
 
     for kubeconfig in "${membercluster_kubeconfigs[@]}"
     do
-       # Remove the longest matched substring '*/' from a string like '--kubeconfig=/var/lib/jenkins/.kube/east'
-       # to get the last element which is the cluster name.
-       cluster=${kubeconfig##*/}
-       ip=$(kubectl get nodes -o wide --no-headers=true ${kubeconfig} | awk -v role1="master" -v role2="control-plane" '($3 ~ role1 || $3 ~ role2) {print $6}')
-       rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" ./multicluster/test/yamls/test-${cluster}-serviceexport.yml jenkins@["${ip}"]:"${WORKDIR}"/serviceexport.yml
+        # Remove the longest matched substring '*/' from a string like '--kubeconfig=/var/lib/jenkins/.kube/east'
+        # to get the last element which is the cluster name.
+        cluster=${kubeconfig##*/}
+        if [[ ${KIND} == "true" ]]; then
+            docker cp ./multicluster/test/yamls/test-${cluster}-serviceexport.yml ${cluster}-control-plane:/root/serviceexport.yml
+        else
+            ip=$(kubectl get nodes -o wide --no-headers=true ${kubeconfig} | awk -v role1="master" -v role2="control-plane" '($3 ~ role1 || $3 ~ role2) {print $6}')
+            rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" ./multicluster/test/yamls/test-${cluster}-serviceexport.yml jenkins@["${ip}"]:"${WORKDIR}"/serviceexport.yml
+        fi
     done
 }
 
@@ -317,7 +357,7 @@ function run_multicluster_e2e {
     export GOCACHE=${WORKDIR}/.cache/go-build
     export PATH=$GOROOT/bin:$PATH
 
-    if ${ENABLE_MC_GATEWAY}; then
+    if [[ ${ENABLE_MC_GATEWAY} == "true" ]]; then
     cat > build/yamls/chart-values/antrea.yml <<EOF
 multicluster:
   enable: true
@@ -340,31 +380,45 @@ EOF
     docker tag "${DOCKER_REGISTRY}/antrea/agnhost:2.26" "agnhost:2.26"
     docker save agnhost:2.26 -o "${WORKDIR}"/agnhost.tar
 
-    for kubeconfig in "${membercluster_kubeconfigs[@]}"
-    do
-        kubectl get nodes -o wide --no-headers=true "${kubeconfig}"| awk '{print $6}' | while read IP; do
-            rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" "${WORKDIR}"/nginx.tar jenkins@["${IP}"]:"${WORKDIR}"/nginx.tar
-            rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" "${WORKDIR}"/agnhost.tar jenkins@["${IP}"]:"${WORKDIR}"/agnhost.tar
-        if ${IS_CONTAINERD};then
-            ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "${CLEAN_STALE_IMAGES}; sudo ctr -n=k8s.io images import ${WORKDIR}/nginx.tar" || true
-            ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "sudo ctr -n=k8s.io images import ${WORKDIR}/agnhost.tar" || true
-        else
-            ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "${CLEAN_STALE_IMAGES}; docker load -i ${WORKDIR}/nginx.tar" || true
-            ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "docker load -i ${WORKDIR}/agnhost.tar" || true
-        fi
+    if [[ ${KIND} == "true" ]]; then
+        for name in ${CLUSTER_NAMES[*]}; do
+            if [[ "${name}" == "leader" ]];then
+                continue
+            fi
+            kind load docker-image "${DOCKER_REGISTRY}"/antrea/nginx:1.21.6-alpine --name ${name}
+            kind load docker-image "agnhost:2.26" --name ${name}
         done
-
-    done
+    else
+        for kubeconfig in "${membercluster_kubeconfigs[@]}"; do
+            kubectl get nodes -o wide --no-headers=true "${kubeconfig}"| awk '{print $6}' | while read IP; do
+                rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" "${WORKDIR}"/nginx.tar jenkins@["${IP}"]:"${WORKDIR}"/nginx.tar
+                rsync -avr --progress --inplace -e "ssh -o StrictHostKeyChecking=no" "${WORKDIR}"/agnhost.tar jenkins@["${IP}"]:"${WORKDIR}"/agnhost.tar
+            if ${IS_CONTAINERD};then
+                ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "${CLEAN_STALE_IMAGES}; sudo ctr -n=k8s.io images import ${WORKDIR}/nginx.tar" || true
+                ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "sudo ctr -n=k8s.io images import ${WORKDIR}/agnhost.tar" || true
+            else
+                ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "${CLEAN_STALE_IMAGES}; docker load -i ${WORKDIR}/nginx.tar" || true
+                ssh -o StrictHostKeyChecking=no -n jenkins@"${IP}" "docker load -i ${WORKDIR}/agnhost.tar" || true
+            fi
+            done
+        done
+    fi
 
     set +e
     CURRENT_DIR=`pwd`
     mkdir -p ${CURRENT_DIR}/antrea-multicluster-test-logs
-    if ${ENABLE_MC_GATEWAY};then
-      go test -v antrea.io/antrea/multicluster/test/e2e --logs-export-dir `pwd`/antrea-multicluster-test-logs --mc-gateway
-    else
-      go test -v antrea.io/antrea/multicluster/test/e2e --logs-export-dir `pwd`/antrea-multicluster-test-logs
+    options=""
+    if [[ ${ENABLE_MC_GATEWAY} == "true" ]]; then
+        options="--mc-gateway"
+    fi
+    if [[ ${KIND} == "true" ]]; then
+        options+=" --provider kind"
     fi
 
+    set -x
+    go test -v antrea.io/antrea/multicluster/test/e2e --logs-export-dir `pwd`/antrea-multicluster-test-logs $options
+    set +x
+
     if [[ "$?" != "0" ]]; then
         TEST_FAILURE=true
     fi
@@ -391,6 +445,22 @@ trap clean_multicluster EXIT
 clean_tmp
 clean_images
 
+if [[ ${KIND} == "true" ]]; then
+    # Preparing a ClusterSet contains three Kind clusters.
+    SERVICE_CIDRS=("10.96.10.0/24" "10.96.20.0/24" "10.96.30.0/24")
+    for i in {0..2}; do
+        ./ci/kind/kind-setup.sh create ${CLUSTER_NAMES[$i]} --service-cidr ${SERVICE_CIDRS[$i]} --num-workers 1
+    done
+
+    for name in ${CLUSTER_NAMES[*]}; do
+        kind get kubeconfig --name ${name} > ${MULTICLUSTER_KUBECONFIG_PATH}/${name}
+        set +e
+        kubectl taint node ${name}-control-plane --kubeconfig=${MULTICLUSTER_KUBECONFIG_PATH}/${name} node-role.kubernetes.io/master-
+        kubectl taint node ${name}-control-plane --kubeconfig=${MULTICLUSTER_KUBECONFIG_PATH}/${name} node-role.kubernetes.io/control-plane-
+        set -e
+    done
+fi
+
 # We assume all clusters in one testing ClusterSet are using the same runtime,
 # so check leader cluster only to set IS_CONTAINERD.
 set +e
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/multicluster/test/e2e/README.md b/multicluster/test/e2e/README.md
index 2b4d2481a..1ec9c3378 100644
--- a/multicluster/test/e2e/README.md
+++ b/multicluster/test/e2e/README.md
@@ -2,12 +2,39 @@
 
 ## Creating the test Kubernetes ClusterSet
 
-The tests must be run on an actual Kubernetes cluster set, and there must be at least three clusters. We can create the clusters using [kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm) or following [the e2e test instructions](https://github.com/antrea-io/antrea/blob/main/test/e2e/README.md) or through other ways.
+The tests can run either on an actual Kubernetes ClusterSet or a ClusterSet with
+Kind clusters, and there must be three clusters. You can create the clusters
+using [kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm)
+or following [the e2e test instructions](https://github.com/antrea-io/antrea/blob/main/test/e2e/README.md)
+or through other ways.
 
-The three clusters are called leader cluster, west cluster and east cluster. Pod CIDR of each cluster should not be the same. For example, Pod CIDRs could be `192.168.0.1/22`,`192.168.4.1/22` and `192.168.8.1/22` in three clusters.
+By default, the three clusters used in the e2e tests are called `leader`, `west`
+and `east`. If you plan to use your own existing Kubernetes clusters, please make
+sure you save kubeconfig files for three clusters with the same name as cluster's
+name, and Service CIDR of each cluster must not overlap. For example, Service
+CIDRs could be `10.96.10.0/24`, `10.96.20.0/24`, and `10.96.30.0/24` for three clusters.
 
 ## Running the tests
 
-Make sure that your clusters are provisioned and that the Antrea build artifacts are pushed to all the Nodes. If you install the multicluster manually, you can then run the tests from the top-level directory with `go test -v antrea.io/antrea/multicluster/test/e2e` or you can just run `bash ci/jenkins/test-mc.sh --testcase e2e`.
+Make sure that your clusters are provisioned and the Antrea build artifacts are
+uploaded to all the Nodes. If you install the Multi-cluster Controller manually,
+you can run the tests from the top-level directory with `go test -v antrea.io/antrea/multicluster/test/e2e --mc-gateway`
+or run `bash ci/jenkins/test-mc.sh --testcase e2e --mc-gateway`. If you'd like
+to run test with Kind clusters, you can run `bash ci/jenkins/test-mc.sh --testcase e2e --mc-gateway --kind`.
+The command will create three Kind clusters and deploy Multi-cluster controllers
+into the Kind clusters before running e2e tests.
 
-When you use `test-mc.sh`, make sure your kubeconfig files of the clusters in the specific path of the script, or you can change the parameter `MULTICLUSTER_KUBECONFIG_PATH` and other parameters to your own path in the script.
+When you use `test-mc.sh`, make sure your kubeconfig files of the clusters are placed
+in the right path. The default path is `${MULTICLUSTER_KUBECONFIG_PATH}`. Please
+change the parameter `MULTICLUSTER_KUBECONFIG_PATH` to the right path where you place
+kubeconfig files. Sample commands are like below:
+
+```bash
+export WORKSPACE=`pwd`
+export MULTICLUSTER_KUBECONFIG_PATH="`pwd`/.kube"
+DOCKER_REGISTRY="$(head -n1 ci/docker-registry)"
+./ci/jenkins/test-mc.sh --testcase e2e --registry ${DOCKER_REGISTRY} --mc-gateway [--kind]
+```
+
+Notes: if you pass `--kind`, `test-mc.sh` will write kubeconfig files to the given path
+`${MULTICLUSTER_KUBECONFIG_PATH}` automatically.
diff --git a/multicluster/test/e2e/antreapolicy_test.go b/multicluster/test/e2e/antreapolicy_test.go
index 49a4a8b39..8f7b70040 100644
--- a/multicluster/test/e2e/antreapolicy_test.go
+++ b/multicluster/test/e2e/antreapolicy_test.go
@@ -151,7 +151,7 @@ func executeTestsOnAllMemberClusters(t *testing.T, testList []*antreae2e.TestCas
 
 func (data *MCTestData) deployACNPResourceExport(t *testing.T, reFileName string) error {
 	t.Logf("Creating ResourceExport %s in the leader cluster", reFileName)
-	rc, _, stderr, err := provider.RunCommandOnNode(leaderCluster, fmt.Sprintf("kubectl apply -f %s", reFileName))
+	rc, _, stderr, err := provider.RunCommandOnNode(data.getControlPlaneNodeName(leaderCluster), fmt.Sprintf("kubectl apply -f %s", reFileName))
 	if err != nil || rc != 0 || stderr != "" {
 		return fmt.Errorf("error when deploying the ACNP ResourceExport in leader cluster: %v, stderr: %s", err, stderr)
 	}
@@ -159,7 +159,7 @@ func (data *MCTestData) deployACNPResourceExport(t *testing.T, reFileName string
 }
 
 func (data *MCTestData) deleteACNPResourceExport(reFileName string) error {
-	rc, _, stderr, err := provider.RunCommandOnNode(leaderCluster, fmt.Sprintf("kubectl delete -f %s", reFileName))
+	rc, _, stderr, err := provider.RunCommandOnNode(data.getControlPlaneNodeName(leaderCluster), fmt.Sprintf("kubectl delete -f %s", reFileName))
 	if err != nil || rc != 0 || stderr != "" {
 		return fmt.Errorf("error when deleting the ACNP ResourceExport in leader cluster: %v, stderr: %s", err, stderr)
 	}
diff --git a/multicluster/test/e2e/framework.go b/multicluster/test/e2e/framework.go
index bdad74359..9d08efca9 100644
--- a/multicluster/test/e2e/framework.go
+++ b/multicluster/test/e2e/framework.go
@@ -58,6 +58,7 @@ type TestOptions struct {
 	westClusterKubeConfigPath   string
 	eastClusterKubeConfigPath   string
 	enableGateway               bool
+	providerName                string
 	logsExportDir               string
 }
 
@@ -66,6 +67,7 @@ var testOptions TestOptions
 type MCTestData struct {
 	clusters            []string
 	clusterTestDataMap  map[string]antreae2e.TestData
+	controlPlaneNames   map[string]string
 	logsDirForTestCase  string
 	clusterGateways     map[string]string
 	clusterRegularNodes map[string]string
@@ -90,17 +92,30 @@ func (data *MCTestData) createClients() error {
 		}
 		data.clusterTestDataMap[cluster] = testData
 	}
+	data.controlPlaneNames = map[string]string{
+		"east-cluster":   "east-control-plane",
+		"west-cluster":   "west-control-plane",
+		"leader-cluster": "leader-control-plane",
+	}
 	return nil
 }
 
 func (data *MCTestData) initProviders() error {
+	providerName := "remote"
+	if testOptions.providerName == "kind" {
+		providerName = testOptions.providerName
+	}
 	for cluster, d := range data.clusterTestDataMap {
-		if err := d.InitProvider("remote", "multicluster"); err != nil {
+		if err := d.InitProvider(providerName, "multicluster"); err != nil {
 			log.Errorf("Failed to initialize provider for cluster %s", cluster)
 			return err
 		}
 	}
-	provider, _ = providers.NewRemoteProvider("multicluster")
+	if testOptions.providerName == "kind" {
+		provider, _ = providers.NewKindProvider("multicluster")
+	} else {
+		provider, _ = providers.NewRemoteProvider("multicluster")
+	}
 	return nil
 }
 
@@ -139,19 +154,6 @@ func (data *MCTestData) getService(clusterName, namespace, name string) (*corev1
 	return nil, fmt.Errorf("clusterName %s not found", clusterName)
 }
 
-func (data *MCTestData) deleteTestNamespaces(timeout time.Duration) error {
-	var failedClusters []string
-	for cluster, d := range data.clusterTestDataMap {
-		if err := d.DeleteNamespace(multiClusterTestNamespace, timeout); err != nil {
-			failedClusters = append(failedClusters, cluster)
-		}
-	}
-	if len(failedClusters) > 0 {
-		return fmt.Errorf("failed to delete Namespace %s in clusters %v", multiClusterTestNamespace, failedClusters)
-	}
-	return nil
-}
-
 func (data *MCTestData) createPod(clusterName, name, nodeName, namespace, ctrName, image string, command []string,
 	args []string, env []corev1.EnvVar, ports []corev1.ContainerPort, hostNetwork bool, mutateFunc func(pod *corev1.Pod)) error {
 	if d, ok := data.clusterTestDataMap[clusterName]; ok {
diff --git a/multicluster/test/e2e/main_test.go b/multicluster/test/e2e/main_test.go
index 735bcff42..faed38027 100644
--- a/multicluster/test/e2e/main_test.go
+++ b/multicluster/test/e2e/main_test.go
@@ -66,6 +66,7 @@ func testMain(m *testing.M) int {
 	flag.StringVar(&testOptions.eastClusterKubeConfigPath, "east-cluster-kubeconfig-path", path.Join(homedir, ".kube", "east"), "Kubeconfig Path of the east cluster")
 	flag.StringVar(&testOptions.westClusterKubeConfigPath, "west-cluster-kubeconfig-path", path.Join(homedir, ".kube", "west"), "Kubeconfig Path of the west cluster")
 	flag.BoolVar(&testOptions.enableGateway, "mc-gateway", false, "Run tests with Multicluster Gateway")
+	flag.StringVar(&testOptions.providerName, "provider", "", "K8s test cluster provider")
 	flag.Parse()
 
 	cleanupLogging := testOptions.setupLogging()
diff --git a/multicluster/test/e2e/service_test.go b/multicluster/test/e2e/service_test.go
index 578075ef8..edb709db2 100644
--- a/multicluster/test/e2e/service_test.go
+++ b/multicluster/test/e2e/service_test.go
@@ -167,7 +167,7 @@ func (data *MCTestData) verifyMCServiceACNP(t *testing.T, clientPodName, eastIP
 }
 
 func (data *MCTestData) deployServiceExport(clusterName string) error {
-	rc, _, stderr, err := provider.RunCommandOnNode(clusterName, fmt.Sprintf("kubectl apply -f %s", serviceExportYML))
+	rc, _, stderr, err := provider.RunCommandOnNode(data.getControlPlaneNodeName(clusterName), fmt.Sprintf("kubectl apply -f %s", serviceExportYML))
 	if err != nil || rc != 0 || stderr != "" {
 		return fmt.Errorf("error when deploying the ServiceExport: %v, stderr: %s", err, stderr)
 	}
@@ -176,7 +176,7 @@ func (data *MCTestData) deployServiceExport(clusterName string) error {
 }
 
 func (data *MCTestData) deleteServiceExport(clusterName string) error {
-	rc, _, stderr, err := provider.RunCommandOnNode(clusterName, fmt.Sprintf("kubectl delete -f %s", serviceExportYML))
+	rc, _, stderr, err := provider.RunCommandOnNode(data.getControlPlaneNodeName(clusterName), fmt.Sprintf("kubectl delete -f %s", serviceExportYML))
 	if err != nil || rc != 0 || stderr != "" {
 		return fmt.Errorf("error when deleting the ServiceExport: %v, stderr: %s", err, stderr)
 	}
@@ -186,8 +186,8 @@ func (data *MCTestData) deleteServiceExport(clusterName string) error {
 
 // getNodeNamesFromCluster will pick up a Node randomly as the Gateway
 // and also a regular Node from the specified cluster.
-func getNodeNamesFromCluster(clusterName string) (string, string, error) {
-	rc, output, stderr, err := provider.RunCommandOnNode(clusterName, "kubectl get node -o jsonpath='{range .items[*]}{.metadata.name}{\" \"}{end}'")
+func (data *MCTestData) getNodeNamesFromCluster(clusterName string) (string, string, error) {
+	rc, output, stderr, err := provider.RunCommandOnNode(data.getControlPlaneNodeName(clusterName), "/bin/sh -c kubectl get nodes -o custom-columns=:.metadata.name --no-headers | tr '\n' ' '")
 	if err != nil || rc != 0 || stderr != "" {
 		return "", "", fmt.Errorf("error when getting Node list: %v, stderr: %s", err, stderr)
 	}
@@ -205,7 +205,7 @@ func getNodeNamesFromCluster(clusterName string) (string, string, error) {
 
 // setGatewayNode adds an annotation to assign it as Gateway Node.
 func (data *MCTestData) setGatewayNode(t *testing.T, clusterName string, nodeName string) error {
-	rc, _, stderr, err := provider.RunCommandOnNode(clusterName, fmt.Sprintf("kubectl annotate node %s multicluster.antrea.io/gateway=true", nodeName))
+	rc, _, stderr, err := provider.RunCommandOnNode(data.getControlPlaneNodeName(clusterName), fmt.Sprintf("kubectl annotate node %s multicluster.antrea.io/gateway=true", nodeName))
 	if err != nil || rc != 0 || stderr != "" {
 		return fmt.Errorf("error when annotate the Node %s: %s, stderr: %s", nodeName, err, stderr)
 	}
@@ -214,13 +214,21 @@ func (data *MCTestData) setGatewayNode(t *testing.T, clusterName string, nodeNam
 }
 
 func (data *MCTestData) unsetGatewayNode(clusterName string, nodeName string) error {
-	rc, _, stderr, err := provider.RunCommandOnNode(clusterName, fmt.Sprintf("kubectl annotate node %s multicluster.antrea.io/gateway-", nodeName))
+	rc, _, stderr, err := provider.RunCommandOnNode(data.getControlPlaneNodeName(clusterName), fmt.Sprintf("kubectl annotate node %s multicluster.antrea.io/gateway-", nodeName))
 	if err != nil || rc != 0 || stderr != "" {
 		return fmt.Errorf("error when cleaning up annotation of the Node: %v, stderr: %s", err, stderr)
 	}
 	return nil
 }
 
+func (data *MCTestData) getControlPlaneNodeName(clusterName string) string {
+	controlplaneNodeName := clusterName
+	if testOptions.providerName == "kind" {
+		controlplaneNodeName = data.controlPlaneNames[clusterName]
+	}
+	return controlplaneNodeName
+}
+
 func initializeGateway(t *testing.T, data *MCTestData) {
 	data.clusterGateways = make(map[string]string)
 	data.clusterRegularNodes = make(map[string]string)
@@ -230,7 +238,7 @@ func initializeGateway(t *testing.T, data *MCTestData) {
 			// Skip Gateway initialization for the leader cluster
 			continue
 		}
-		gwName, regularNode, err := getNodeNamesFromCluster(clusterName)
+		gwName, regularNode, err := data.getNodeNamesFromCluster(clusterName)
 		failOnError(err, t)
 		err = data.setGatewayNode(t, clusterName, gwName)
 		failOnError(err, t)
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)