Repository: antrea-io/antrea
Pull request #3347 (2022-02-23 11:30:21)

Fix NodePort e2e test
In NodePort e2e test, a test Service and its backend Pod are
created. The match label is app=agnhost. However, the test
client is also a Pod that has label app=agnhost, but netexec is
not running on the test client. As a result, the test has a 50%
chance of failing. This PR fixes the issue by using a busybox Pod
as test client.

Signed-off-by: Hongliang Liu <lhongliang@vmware.com>

codecov-commenter: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/3347?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#3347](https://codecov.io/gh/antrea-io/antrea/pull/3347?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (25fde93) into [main](https://codecov.io/gh/antrea-io/antrea/commit/3f30d848ba3fbb0be8e242130825d0fc6d5c0f45?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (3f30d84) will **decrease** coverage by `21.24%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/antrea-io/antrea/pull/3347/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=None)](https://codecov.io/gh/antrea-io/antrea/pull/3347?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@             Coverage Diff             @@
##             main    #3347       +/-   ##
===========================================
- Coverage   60.85%   39.60%   -21.25%     
===========================================
  Files         266      303       +37     
  Lines       26520    38276    +11756     
===========================================
- Hits        16139    15161      -978     
- Misses       8591    21595    +13004     
+ Partials     1790     1520      -270     
```

| Flag | Coverage Δ | |
|---|---|---|
| integration-tests | `36.26% <ø> (?)` | |
| kind-e2e-tests | `?` | |
| unit-tests | `41.60% <ø> (+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=None#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/3347?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [pkg/ovs/openflow/default.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL292cy9vcGVuZmxvdy9kZWZhdWx0Lmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/util/runtime/runtime.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL3V0aWwvcnVudGltZS9ydW50aW1lLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/agent/nodeportlocal/k8s/annotations.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L25vZGVwb3J0bG9jYWwvazhzL2Fubm90YXRpb25zLmdv) | `0.00% <0.00%> (-93.55%)` | :arrow_down: |
| [pkg/ovs/openflow/logs.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL292cy9vcGVuZmxvdy9sb2dzLmdv) | `9.52% <0.00%> (-90.48%)` | :arrow_down: |
| [pkg/apis/controlplane/register.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FwaXMvY29udHJvbHBsYW5lL3JlZ2lzdGVyLmdv) | `0.00% <0.00%> (-90.00%)` | :arrow_down: |
| [pkg/agent/controller/networkpolicy/reject.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvbmV0d29ya3BvbGljeS9yZWplY3QuZ28=) | `0.00% <0.00%> (-78.23%)` | :arrow_down: |
| [pkg/agent/client.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L2NsaWVudC5nbw==) | `0.00% <0.00%> (-77.42%)` | :arrow_down: |
| [pkg/ovs/ovsconfig/ovs\_client\_linux.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL292cy9vdnNjb25maWcvb3ZzX2NsaWVudF9saW51eC5nbw==) | `0.00% <0.00%> (-76.93%)` | :arrow_down: |
| [pkg/flowaggregator/certificate.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2Zsb3dhZ2dyZWdhdG9yL2NlcnRpZmljYXRlLmdv) | `0.00% <0.00%> (-76.58%)` | :arrow_down: |
| [...agent/controller/traceflow/traceflow\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvdHJhY2VmbG93L3RyYWNlZmxvd19jb250cm9sbGVyLmdv) | `0.00% <0.00%> (-73.41%)` | :arrow_down: |
| ... and [287 more](https://codecov.io/gh/antrea-io/antrea/pull/3347/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |


Patch:
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 856808189..b6d3a14b5 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -2667,8 +2667,8 @@ data:
     # Enable controlling SNAT IPs of Pod egress traffic.
     #  Egress: false
 
-    # Enable flexible IPAM mode for Antrea. This mode allows to assign IP Ranges to Namespaces,
-    # Deployments and StatefulSets via IP Pool annotation.
+    # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
+    # bridging mode and allocates IPs to Pods in bridging mode.
     #  AntreaIPAM: false
 
     # Enable multicast traffic. This feature is supported only with noEncap mode.
@@ -2734,6 +2734,15 @@ data:
     # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
     #trafficEncryptionMode: none
 
+    # Enable bridging mode of Pod network on Nodes, in which the Node's transport interface is connected
+    # to the OVS bridge, and cross-Node/VLAN traffic from AntreaIPAM Pods (Pods whose IP addresses are
+    # allocated by AntreaIPAM from IPPools) is sent to the underlay network via the uplink, and
+    # forwarded/routed by the underlay network.
+    # This option requires the `AntreaIPAM` feature gate to be enabled. At this moment, it supports only
+    # IPv4 and Linux Nodes, and can be enabled only when `ovsDatapathType` is `system`,
+    # `trafficEncapMode` is `noEncap`, and `noSNAT` is true.
+    #enableBridgingMode: false
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
@@ -2961,7 +2970,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-bg855thh55
+  name: antrea-config-tb4cm2bddc
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3032,7 +3041,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-bg855thh55
+          value: antrea-config-tb4cm2bddc
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3083,7 +3092,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-bg855thh55
+          name: antrea-config-tb4cm2bddc
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3319,7 +3328,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-bg855thh55
+          name: antrea-config-tb4cm2bddc
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index 8cc152dd2..67f8eae63 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -2667,8 +2667,8 @@ data:
     # Enable controlling SNAT IPs of Pod egress traffic.
     #  Egress: false
 
-    # Enable flexible IPAM mode for Antrea. This mode allows to assign IP Ranges to Namespaces,
-    # Deployments and StatefulSets via IP Pool annotation.
+    # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
+    # bridging mode and allocates IPs to Pods in bridging mode.
     #  AntreaIPAM: false
 
     # Enable multicast traffic. This feature is supported only with noEncap mode.
@@ -2734,6 +2734,15 @@ data:
     # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
     #trafficEncryptionMode: none
 
+    # Enable bridging mode of Pod network on Nodes, in which the Node's transport interface is connected
+    # to the OVS bridge, and cross-Node/VLAN traffic from AntreaIPAM Pods (Pods whose IP addresses are
+    # allocated by AntreaIPAM from IPPools) is sent to the underlay network via the uplink, and
+    # forwarded/routed by the underlay network.
+    # This option requires the `AntreaIPAM` feature gate to be enabled. At this moment, it supports only
+    # IPv4 and Linux Nodes, and can be enabled only when `ovsDatapathType` is `system`,
+    # `trafficEncapMode` is `noEncap`, and `noSNAT` is true.
+    #enableBridgingMode: false
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
@@ -2961,7 +2970,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-bg855thh55
+  name: antrea-config-tb4cm2bddc
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3032,7 +3041,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-bg855thh55
+          value: antrea-config-tb4cm2bddc
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3083,7 +3092,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-bg855thh55
+          name: antrea-config-tb4cm2bddc
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3321,7 +3330,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-bg855thh55
+          name: antrea-config-tb4cm2bddc
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index 021e6e739..d1a948295 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -2667,8 +2667,8 @@ data:
     # Enable controlling SNAT IPs of Pod egress traffic.
     #  Egress: false
 
-    # Enable flexible IPAM mode for Antrea. This mode allows to assign IP Ranges to Namespaces,
-    # Deployments and StatefulSets via IP Pool annotation.
+    # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
+    # bridging mode and allocates IPs to Pods in bridging mode.
     #  AntreaIPAM: false
 
     # Enable multicast traffic. This feature is supported only with noEncap mode.
@@ -2734,6 +2734,15 @@ data:
     # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
     #trafficEncryptionMode: none
 
+    # Enable bridging mode of Pod network on Nodes, in which the Node's transport interface is connected
+    # to the OVS bridge, and cross-Node/VLAN traffic from AntreaIPAM Pods (Pods whose IP addresses are
+    # allocated by AntreaIPAM from IPPools) is sent to the underlay network via the uplink, and
+    # forwarded/routed by the underlay network.
+    # This option requires the `AntreaIPAM` feature gate to be enabled. At this moment, it supports only
+    # IPv4 and Linux Nodes, and can be enabled only when `ovsDatapathType` is `system`,
+    # `trafficEncapMode` is `noEncap`, and `noSNAT` is true.
+    #enableBridgingMode: false
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
@@ -2961,7 +2970,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-d44h5956gg
+  name: antrea-config-557b79kcm4
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3032,7 +3041,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-d44h5956gg
+          value: antrea-config-557b79kcm4
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3083,7 +3092,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-d44h5956gg
+          name: antrea-config-557b79kcm4
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3322,7 +3331,7 @@ spec:
           path: /home/kubernetes/bin
         name: host-cni-bin
       - configMap:
-          name: antrea-config-d44h5956gg
+          name: antrea-config-557b79kcm4
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index 47302e9df..e5aed0897 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -2667,8 +2667,8 @@ data:
     # Enable controlling SNAT IPs of Pod egress traffic.
     #  Egress: false
 
-    # Enable flexible IPAM mode for Antrea. This mode allows to assign IP Ranges to Namespaces,
-    # Deployments and StatefulSets via IP Pool annotation.
+    # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
+    # bridging mode and allocates IPs to Pods in bridging mode.
     #  AntreaIPAM: false
 
     # Enable multicast traffic. This feature is supported only with noEncap mode.
@@ -2734,6 +2734,15 @@ data:
     # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
     trafficEncryptionMode: ipsec
 
+    # Enable bridging mode of Pod network on Nodes, in which the Node's transport interface is connected
+    # to the OVS bridge, and cross-Node/VLAN traffic from AntreaIPAM Pods (Pods whose IP addresses are
+    # allocated by AntreaIPAM from IPPools) is sent to the underlay network via the uplink, and
+    # forwarded/routed by the underlay network.
+    # This option requires the `AntreaIPAM` feature gate to be enabled. At this moment, it supports only
+    # IPv4 and Linux Nodes, and can be enabled only when `ovsDatapathType` is `system`,
+    # `trafficEncapMode` is `noEncap`, and `noSNAT` is true.
+    #enableBridgingMode: false
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
@@ -2966,7 +2975,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-56dhk99g5f
+  name: antrea-config-49chg5d6md
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3046,7 +3055,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-56dhk99g5f
+          value: antrea-config-49chg5d6md
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3097,7 +3106,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-56dhk99g5f
+          name: antrea-config-49chg5d6md
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3368,7 +3377,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-56dhk99g5f
+          name: antrea-config-49chg5d6md
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-kind.yml b/build/yamls/antrea-kind.yml
index 4d49f2be9..c1e317884 100644
--- a/build/yamls/antrea-kind.yml
+++ b/build/yamls/antrea-kind.yml
@@ -2667,8 +2667,8 @@ data:
     # Enable controlling SNAT IPs of Pod egress traffic.
     #  Egress: false
 
-    # Enable flexible IPAM mode for Antrea. This mode allows to assign IP Ranges to Namespaces,
-    # Deployments and StatefulSets via IP Pool annotation.
+    # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
+    # bridging mode and allocates IPs to Pods in bridging mode.
     #  AntreaIPAM: false
 
     # Enable multicast traffic. This feature is supported only with noEncap mode.
@@ -2734,6 +2734,15 @@ data:
     # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
     #trafficEncryptionMode: none
 
+    # Enable bridging mode of Pod network on Nodes, in which the Node's transport interface is connected
+    # to the OVS bridge, and cross-Node/VLAN traffic from AntreaIPAM Pods (Pods whose IP addresses are
+    # allocated by AntreaIPAM from IPPools) is sent to the underlay network via the uplink, and
+    # forwarded/routed by the underlay network.
+    # This option requires the `AntreaIPAM` feature gate to be enabled. At this moment, it supports only
+    # IPv4 and Linux Nodes, and can be enabled only when `ovsDatapathType` is `system`,
+    # `trafficEncapMode` is `noEncap`, and `noSNAT` is true.
+    #enableBridgingMode: false
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
@@ -2966,7 +2975,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-m4h4624d24
+  name: antrea-config-c2kfgd59tm
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3037,7 +3046,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-m4h4624d24
+          value: antrea-config-c2kfgd59tm
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3088,7 +3097,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-m4h4624d24
+          name: antrea-config-c2kfgd59tm
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3320,7 +3329,7 @@ spec:
           type: CharDevice
         name: dev-tun
       - configMap:
-          name: antrea-config-m4h4624d24
+          name: antrea-config-c2kfgd59tm
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index bc4c8ef89..08dff9cda 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -2667,8 +2667,8 @@ data:
     # Enable controlling SNAT IPs of Pod egress traffic.
     #  Egress: false
 
-    # Enable flexible IPAM mode for Antrea. This mode allows to assign IP Ranges to Namespaces,
-    # Deployments and StatefulSets via IP Pool annotation.
+    # Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
+    # bridging mode and allocates IPs to Pods in bridging mode.
     #  AntreaIPAM: false
 
     # Enable multicast traffic. This feature is supported only with noEncap mode.
@@ -2734,6 +2734,15 @@ data:
     # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
     #trafficEncryptionMode: none
 
+    # Enable bridging mode of Pod network on Nodes, in which the Node's transport interface is connected
+    # to the OVS bridge, and cross-Node/VLAN traffic from AntreaIPAM Pods (Pods whose IP addresses are
+    # allocated by AntreaIPAM from IPPools) is sent to the underlay network via the uplink, and
+    # forwarded/routed by the underlay network.
+    # This option requires the `AntreaIPAM` feature gate to be enabled. At this moment, it supports only
+    # IPv4 and Linux Nodes, and can be enabled only when `ovsDatapathType` is `system`,
+    # `trafficEncapMode` is `noEncap`, and `noSNAT` is true.
+    #enableBridgingMode: false
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
@@ -2966,7 +2975,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-895f7f85g9
+  name: antrea-config-82b78c27h8
   namespace: kube-system
 ---
 apiVersion: v1
@@ -3037,7 +3046,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-895f7f85g9
+          value: antrea-config-82b78c27h8
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -3088,7 +3097,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-895f7f85g9
+          name: antrea-config-82b78c27h8
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -3324,7 +3333,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-895f7f85g9
+          name: antrea-config-82b78c27h8
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/base/conf/antrea-agent.conf b/build/yamls/base/conf/antrea-agent.conf
index 21fb3c61d..181543dd7 100644
--- a/build/yamls/base/conf/antrea-agent.conf
+++ b/build/yamls/base/conf/antrea-agent.conf
@@ -31,8 +31,8 @@ featureGates:
 # Enable controlling SNAT IPs of Pod egress traffic.
 #  Egress: false
 
-# Enable flexible IPAM mode for Antrea. This mode allows to assign IP Ranges to Namespaces,
-# Deployments and StatefulSets via IP Pool annotation.
+# Enable AntreaIPAM, which can allocate IP addresses from IPPools. AntreaIPAM is required by the
+# bridging mode and allocates IPs to Pods in bridging mode.
 #  AntreaIPAM: false
 
 # Enable multicast traffic. This feature is supported only with noEncap mode.
@@ -98,6 +98,15 @@ featureGates:
 # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
 #trafficEncryptionMode: none
 
+# Enable bridging mode of Pod network on Nodes, in which the Node's transport interface is connected
+# to the OVS bridge, and cross-Node/VLAN traffic from AntreaIPAM Pods (Pods whose IP addresses are
+# allocated by AntreaIPAM from IPPools) is sent to the underlay network via the uplink, and
+# forwarded/routed by the underlay network.
+# This option requires the `AntreaIPAM` feature gate to be enabled. At this moment, it supports only
+# IPv4 and Linux Nodes, and can be enabled only when `ovsDatapathType` is `system`,
+# `trafficEncapMode` is `noEncap`, and `noSNAT` is true.
+#enableBridgingMode: false
+
 # Default MTU to use for the host gateway interface and the network interface of each Pod.
 # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
 # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
diff --git a/build/yamls/patches/legacy/controller.json b/build/yamls/patches/legacy/controller.json
deleted file mode 100644
index f32788e07..000000000
--- a/build/yamls/patches/legacy/controller.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[
-	{
-		"op": "replace",
-		"path": "/apiVersion",
-		"value": "admissionregistration.k8s.io/v1beta1"
-	}
-]
diff --git a/build/yamls/patches/legacy/crdClusterInformation.json b/build/yamls/patches/legacy/crdClusterInformation.json
deleted file mode 100644
index dc58b6a36..000000000
--- a/build/yamls/patches/legacy/crdClusterInformation.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[
-	{
-		"op": "move",
-		"from": "/spec/versions/0/schema",
-		"path": "/spec/validation"
-	}
-]
diff --git a/build/yamls/patches/legacy/crdClusterNetworkPolicy.json b/build/yamls/patches/legacy/crdClusterNetworkPolicy.json
deleted file mode 100644
index a5c01a007..000000000
--- a/build/yamls/patches/legacy/crdClusterNetworkPolicy.json
+++ /dev/null
@@ -1,41 +0,0 @@
-[
-	{
-		"op": "move",
-		"from": "/spec/versions/0/schema",
-		"path": "/spec/validation"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/0/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/0/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/1/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/1/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/2/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/2/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/3/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/3/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/4/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/4/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns",
-		"path": "/spec/additionalPrinterColumns"
-	},
-	{
-		"op": "remove",
-		"path": "/spec/versions/0/subresources"
-	}
-]
diff --git a/build/yamls/patches/legacy/crdExternalEntity.json b/build/yamls/patches/legacy/crdExternalEntity.json
deleted file mode 100644
index 33a4cd590..000000000
--- a/build/yamls/patches/legacy/crdExternalEntity.json
+++ /dev/null
@@ -1,11 +0,0 @@
-[
-	{
-		"op": "remove",
-		"path": "/spec/versions/0/schema"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/1/schema",
-		"path": "/spec/validation"
-	}
-]
diff --git a/build/yamls/patches/legacy/crdNetworkPolicy.json b/build/yamls/patches/legacy/crdNetworkPolicy.json
deleted file mode 100644
index a5c01a007..000000000
--- a/build/yamls/patches/legacy/crdNetworkPolicy.json
+++ /dev/null
@@ -1,41 +0,0 @@
-[
-	{
-		"op": "move",
-		"from": "/spec/versions/0/schema",
-		"path": "/spec/validation"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/0/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/0/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/1/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/1/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/2/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/2/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/3/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/3/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/4/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/4/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns",
-		"path": "/spec/additionalPrinterColumns"
-	},
-	{
-		"op": "remove",
-		"path": "/spec/versions/0/subresources"
-	}
-]
diff --git a/build/yamls/patches/legacy/crdTier.json b/build/yamls/patches/legacy/crdTier.json
deleted file mode 100644
index b7a73434a..000000000
--- a/build/yamls/patches/legacy/crdTier.json
+++ /dev/null
@@ -1,22 +0,0 @@
-[
-	{
-		"op": "move",
-		"from": "/spec/versions/0/schema",
-		"path": "/spec/validation"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/0/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/0/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/1/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/1/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns",
-		"path": "/spec/additionalPrinterColumns"
-	}
-]
diff --git a/build/yamls/patches/legacy/crdTraceflow.json b/build/yamls/patches/legacy/crdTraceflow.json
deleted file mode 100644
index a5c01a007..000000000
--- a/build/yamls/patches/legacy/crdTraceflow.json
+++ /dev/null
@@ -1,41 +0,0 @@
-[
-	{
-		"op": "move",
-		"from": "/spec/versions/0/schema",
-		"path": "/spec/validation"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/0/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/0/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/1/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/1/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/2/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/2/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/3/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/3/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns/4/jsonPath",
-		"path": "/spec/versions/0/additionalPrinterColumns/4/JSONPath"
-	},
-	{
-		"op": "move",
-		"from": "/spec/versions/0/additionalPrinterColumns",
-		"path": "/spec/additionalPrinterColumns"
-	},
-	{
-		"op": "remove",
-		"path": "/spec/versions/0/subresources"
-	}
-]
diff --git a/build/yamls/patches/legacy/crdVersion.json b/build/yamls/patches/legacy/crdVersion.json
deleted file mode 100644
index 40396f6da..000000000
--- a/build/yamls/patches/legacy/crdVersion.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[
-	{
-		"op": "replace",
-		"path": "/apiVersion",
-		"value": "apiextensions.k8s.io/v1beta1"
-	}
-]
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index f1776f749..2fb4e22a9 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -114,10 +114,9 @@ func run(o *Options) error {
 	}
 	defer ovsdbConnection.Close()
 
-	// Enable AntreaIPAM will set connectUplinkToBridge to True. Currently only Linux+IPv4 is supported.
-	// AntreaIPAM works with system OVSDatapathType and noEncap, noSNAT mode. Egress feature is not supported.
-	// AntreaIPAM is only verified with other FeatureGates at default state.
-	connectUplinkToBridge := features.DefaultFeatureGate.Enabled(features.AntreaIPAM)
+	enableBridgingMode := features.DefaultFeatureGate.Enabled(features.AntreaIPAM) && o.config.EnableBridgingMode
+	// Bridging mode will connect the uplink interface to the OVS bridge.
+	connectUplinkToBridge := enableBridgingMode
 
 	ovsDatapathType := ovsconfig.OVSDatapathType(o.config.OVSDatapathType)
 	ovsBridgeClient := ovsconfig.NewOVSBridge(o.config.OVSBridge, ovsDatapathType, ovsdbConnection)
@@ -353,14 +352,13 @@ func run(o *Options) error {
 	if networkConfig.TrafficEncapMode.IsNetworkPolicyOnly() {
 		isChaining = true
 	}
-	antreaIPAM := features.DefaultFeatureGate.Enabled(features.AntreaIPAM)
 	cniServer := cniserver.New(
 		o.config.CNISocket,
 		o.config.HostProcPathPrefix,
 		nodeConfig,
 		k8sClient,
 		isChaining,
-		antreaIPAM,
+		enableBridgingMode, // activate AntreaIPAM in CNIServer when bridging mode is enabled
 		routeClient,
 		networkReadyCh)
 
@@ -399,12 +397,12 @@ func run(o *Options) error {
 	if err := agentInitializer.FlowRestoreComplete(); err != nil {
 		return err
 	}
-	// BridgeUplinkToOVSBridge is required if connectUplinkToBridge is true, and must be run immediately after FlowRestoreComplete
+	// ConnectUplinkToOVSBridge must be run immediately after FlowRestoreComplete
 	if connectUplinkToBridge {
 		// Restore network config before shutdown. ovsdbConnection must be alive when restore.
 		defer agentInitializer.RestoreOVSBridge()
-		if err := agentInitializer.BridgeUplinkToOVSBridge(); err != nil {
-			return fmt.Errorf("error bridging uplink to OVS bridge: %w", err)
+		if err := agentInitializer.ConnectUplinkToOVSBridge(); err != nil {
+			return fmt.Errorf("failed to connect uplink to OVS bridge: %w", err)
 		}
 	}
 
@@ -442,10 +440,11 @@ func run(o *Options) error {
 		networkPolicyController.SetDenyConnStore(flowExporter.GetDenyConnStore())
 	}
 
-	// Initialize localPodInformer for NPLAgent and AntreaIPAMController
+	enableNodePortLocal := features.DefaultFeatureGate.Enabled(features.NodePortLocal) && o.config.NodePortLocal.Enable
+
+	// Initialize localPodInformer for NPLAgent, AntreaIPAMController, and secondary network controller.
 	var localPodInformer cache.SharedIndexInformer
-	if features.DefaultFeatureGate.Enabled(features.NodePortLocal) && o.config.NodePortLocal.Enable ||
-		features.DefaultFeatureGate.Enabled(features.AntreaIPAM) ||
+	if enableNodePortLocal || enableBridgingMode ||
 		features.DefaultFeatureGate.Enabled(features.SecondaryNetwork) {
 		listOptions := func(options *metav1.ListOptions) {
 			options.FieldSelector = fields.OneTermEqualSelector("spec.nodeName", nodeConfig.Name).String()
@@ -472,7 +471,7 @@ func run(o *Options) error {
 	go networkPolicyController.Run(stopCh)
 
 	// Initialize the NPL agent.
-	if features.DefaultFeatureGate.Enabled(features.NodePortLocal) && o.config.NodePortLocal.Enable {
+	if enableNodePortLocal {
 		nplController, err := npl.InitializeNPLAgent(
 			k8sClient,
 			informerFactory,
@@ -486,8 +485,9 @@ func run(o *Options) error {
 		go nplController.Run(stopCh)
 	}
 
-	// Initialize the Antrea IPAM agent.
-	if features.DefaultFeatureGate.Enabled(features.AntreaIPAM) {
+	// Now Antrea IPAM is used only by bridging mode, so we initialize AntreaIPAMController only
+	// when the bridging mode is enabled.
+	if enableBridgingMode {
 		ipamController, err := ipam.InitializeAntreaIPAMController(
 			k8sClient,
 			crdClient,
diff --git a/cmd/antrea-agent/options.go b/cmd/antrea-agent/options.go
index 31b351671..9068eb53e 100644
--- a/cmd/antrea-agent/options.go
+++ b/cmd/antrea-agent/options.go
@@ -349,21 +349,26 @@ func (o *Options) validateFlowExporterConfig() error {
 }
 
 func (o *Options) validateAntreaIPAMConfig() error {
-	if features.DefaultFeatureGate.Enabled(features.AntreaIPAM) {
-		// AntreaIPAM will bridge uplink to OVS bridge, which is not compatible with OVSDatapathSystem 'netdev'
-		if o.config.OVSDatapathType != string(ovsconfig.OVSDatapathSystem) {
-			return fmt.Errorf("AntreaIPAM requires 'system' OVSDatapathType, current: %s",
-				o.config.OVSDatapathType)
-		}
-		if !strings.EqualFold(o.config.TrafficEncapMode, config.TrafficEncapModeNoEncap.String()) {
-			return fmt.Errorf("AntreaIPAM requires 'noEncap' TrafficEncapMode, current: %s",
-				o.config.TrafficEncapMode)
-		}
-		// TODO(gran): support SNAT for Per-Node IPAM Pods
-		// SNAT needs to be updated to bypass traffic from AntreaIPAM Pod to Per-Node IPAM Pod
-		if !o.config.NoSNAT {
-			return fmt.Errorf("AntreaIPAM requires noSNAT")
-		}
+	if !o.config.EnableBridgingMode {
+		return nil
+	}
+	if !features.DefaultFeatureGate.Enabled(features.AntreaIPAM) {
+		klog.InfoS("The enableBridgingMode option is set to true, but it will be ignored because the AntreaIPAM feature gate is disabled")
+		return nil
+	}
+	// Bridging mode will connect uplink to OVS bridge, which is not compatible with OVSDatapathSystem 'netdev'.
+	if o.config.OVSDatapathType != string(ovsconfig.OVSDatapathSystem) {
+		return fmt.Errorf("Bridging mode requires 'system' OVSDatapathType, current: %s",
+			o.config.OVSDatapathType)
+	}
+	if !strings.EqualFold(o.config.TrafficEncapMode, config.TrafficEncapModeNoEncap.String()) {
+		return fmt.Errorf("Bridging mode requires 'noEncap' TrafficEncapMode, current: %s",
+			o.config.TrafficEncapMode)
+	}
+	// TODO(gran): support SNAT for Per-Node IPAM Pods
+	// SNAT needs to be updated to bypass traffic from AntreaIPAM Pod to Per-Node IPAM Pod
+	if !o.config.NoSNAT {
+		return fmt.Errorf("Bridging mode requires noSNAT")
 	}
 	return nil
 }
diff --git a/cmd/antrea-agent/options_windows.go b/cmd/antrea-agent/options_windows.go
index 905672134..94e90545a 100644
--- a/cmd/antrea-agent/options_windows.go
+++ b/cmd/antrea-agent/options_windows.go
@@ -53,6 +53,9 @@ func (o *Options) checkUnsupportedFeatures() error {
 	if encryptionMode != config.TrafficEncryptionModeNone {
 		unsupported = append(unsupported, "TrafficEncryptionMode: "+encryptionMode.String())
 	}
+	if o.config.EnableBridgingMode {
+		unsupported = append(unsupported, "EnableBridgingMode")
+	}
 	if unsupported != nil {
 		return fmt.Errorf("unsupported features on Windows: {%s}", strings.Join(unsupported, ", "))
 	}
diff --git a/docs/feature-gates.md b/docs/feature-gates.md
index 5b96d8ab5..a47e8a946 100644
--- a/docs/feature-gates.md
+++ b/docs/feature-gates.md
@@ -159,6 +159,42 @@ cluster-access-dns    10         120       12210   2020-09-07T13:22:42Z
 NAMESPACE     NAME                  SESSIONS   PACKETS   BYTES   CREATED AT
 default       access-http           3          36        5199    2020-09-07T13:19:38Z
 foo           bar                   1          12        1221    2020-09-07T13:22:42Z
+
+# List per-rule statistics for Antrea ClusterNetworkPolicy cluster-access-dns.
+# Both Antrea NetworkPolicy and Antrea ClusterNetworkPolicy support per-rule statistics.
+> kubectl get antreaclusternetworkpolicystats cluster-access-dns -o json
+{
+    "apiVersion": "stats.antrea.io/v1alpha1",
+    "kind": "AntreaClusterNetworkPolicyStats",
+    "metadata": {
+        "creationTimestamp": "2022-02-24T09:04:53Z",
+        "name": "cluster-access-dns",
+        "uid": "940cf76a-d836-4e76-b773-d275370b9328"
+    },
+    "ruleTrafficStats": [
+        {
+            "name": "rule1",
+            "trafficStats": {
+                "bytes": 392,
+                "packets": 4,
+                "sessions": 1
+            }
+        },
+        {
+            "name": "rule2",
+            "trafficStats": {
+                "bytes": 111,
+                "packets": 2,
+                "sessions": 1
+            }
+        }
+    ],
+    "trafficStats": {
+        "bytes": 503,
+        "packets": 6,
+        "sessions": 2
+    }
+}
 ```
 
 #### Requirements for this Feature
diff --git a/docs/service-loadbalancer.md b/docs/service-loadbalancer.md
new file mode 100644
index 000000000..be3543730
--- /dev/null
+++ b/docs/service-loadbalancer.md
@@ -0,0 +1,298 @@
+# Service of type LoadBalancer
+
+## Table of Contents
+
+<!-- toc -->
+- [Service external IP management by Antrea](#service-external-ip-management-by-antrea)
+  - [Configuration](#configuration)
+    - [Enable Service external IP management feature](#enable-service-external-ip-management-feature)
+    - [Create an ExternalIPPool custom resource](#create-an-externalippool-custom-resource)
+    - [Create a Service of type LoadBalancer](#create-a-service-of-type-loadbalancer)
+    - [Validate Service external IP](#validate-service-external-ip)
+  - [Limitations](#limitations)
+- [Using MetalLB with Antrea](#using-metallb-with-antrea)
+  - [Install MetalLB](#install-metallb)
+  - [Configure MetalLB with layer 2 mode](#configure-metallb-with-layer-2-mode)
+  - [Configure MetalLB with BGP mode](#configure-metallb-with-bgp-mode)
+<!-- /toc -->
+
+In Kubernetes, implementing Services of type LoadBalancer usually requires
+an external load balancer. On cloud platforms (including public clouds
+and platforms like NSX-T) that support load balancers, Services of type
+LoadBalancer can be implemented by the Kubernetes Cloud Provider, which
+configures the cloud load balancers for the Services. However, the load
+balancer support is not available on all platforms, or in some cases, it is
+complex or has extra cost to deploy external load balancers. This document
+describes two options for supporting Services of type LoadBalancer with Antrea,
+without an external load balancer:
+
+1. Using Antrea's built-in external IP management for Services of type
+LoadBalancer
+2. Leveraging [MetalLB](https://metallb.universe.tf)
+
+## Service external IP management by Antrea
+
+Antrea supports external IP management for Services of type LoadBalancer
+since version 1.5, which can work together with `AntreaProxy` or
+`kube-proxy` to implement Services of type LoadBalancer, without requiring an
+external load balancer. With the external IP management feature, Antrea can
+allocate an external IP for a Service of type LoadBalancer from an
+[ExternalIPPool](egress.md#the-externalippool-resource), and select a Node
+based on the ExternalIPPool's NodeSelector to host the external IP. Antrea
+configures the Service's external IP on the selected Node, and thus Service
+requests to the external IP will get to the Node, and they are then handled by
+`AntreaProxy` or `kube-proxy` on the Node and distributed to the Service's
+Endpoints. Antrea also implements a Node failover mechanism for Service
+external IPs. When Antrea detects a Node hosting an external IP is down, it
+will move the external IP to another available Node of the ExternalIPPool.
+
+### Configuration
+
+#### Enable Service external IP management feature
+
+At this moment, external IP management for Services is an alpha feature of
+Antrea. The `ServiceExternalIP` feature gate of `antrea-agent` and
+`antrea-controller` must be enabled for the feature to work. You can enable
+the `ServiceExternalIP` feature gate in the `antrea-config` ConfigMap in
+the Antrea deployment YAML:
+
+```yaml
+kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: antrea-config-dcfb6k2hkm
+  namespace: kube-system
+data:
+  antrea-agent.conf: |
+    featureGates:
+...
+      ServiceExternalIP: true
+...
+  antrea-controller.conf: |
+    featureGates:
+...
+      ServiceExternalIP: true
+...
+```
+
+The feature works with both `AntreaProxy` and `kube-proxy`, including the
+following configurations:
+
+- `AntreaProxy` without `proxyAll` enabled - this is `antrea-agent`'s default
+configuration, in which `kube-proxy` serves the request traffic for Services
+of type LoadBalancer (while `AntreaProxy` handles Service requests from Pods).
+- `AntreaProxy` with `proxyAll` enabled - in this case, `AntreaProxy` handles
+all Service traffic, including Services of type LoadBalancer.
+- `AntreaProxy` disabled - `kube-proxy` handles all Service traffic, including
+Services of type LoadBalancer.
+
+#### Create an ExternalIPPool custom resource
+
+Service external IPs are allocated from an ExternalIPPool, which defines a pool
+of external IPs and the set of Nodes to which the external IPs can be assigned.
+To learn more information about ExternalIPPool, please refer to [the Egress
+documentation](egress.md#the-externalippool-resource). The example below
+defines an ExternalIPPool with IP range "10.10.0.2 - 10.10.0.10", and it
+selects the Nodes with label "network-role: ingress-node" to host the external
+IPs:
+
+```yaml
+- apiVersion: crd.antrea.io/v1alpha2
+  kind: ExternalIPPool
+  metadata:
+    name: service-external-ip-pool
+  spec:
+    ipRanges:
+    - start: 10.10.0.2
+      end: 10.10.0.10
+    - cidr: 10.10.1.0/28
+    nodeSelector:
+      matchLabels:
+        network-role: ingress-node
+```
+
+#### Create a Service of type LoadBalancer
+
+For Antrea to manage the externalIP for a Service of type LoadBalancer, the
+Service should be annotated with `service.antrea.io/external-ip-pool`. For
+example:
+
+```yaml
+apiVersion: v1
+kind: Service
+metadata:
+  name: my-service
+  annotations:
+    service.antrea.io/external-ip-pool: "service-external-ip-pool"
+spec:
+  selector:
+    app: MyApp
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 9376
+  type: LoadBalancer
+```
+
+You can also request a particular IP from an ExternalIPPool by setting
+the loadBalancerIP field in the Service spec to that specific IP available
+in the ExternalIPPool, Antrea will allocate the IP from the ExternalIPPool
+for the Service. For example:
+
+```yaml
+apiVersion: v1
+kind: Service
+metadata:
+  name: my-service
+  annotations:
+    service.antrea.io/external-ip-pool: "service-external-ip-pool"
+spec:
+  selector:
+    app: MyApp
+  loadBalancerIP: "10.10.0.2"
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 9376
+  type: LoadBalancer
+```
+
+#### Validate Service external IP
+
+Once Antrea allocates an external IP for a Service of type LoadBalancer, it
+will set the IP to the `loadBalancer.ingress` field in the Service resource
+`status`.  For example:
+
+```yaml
+apiVersion: v1
+kind: Service
+metadata:
+  name: my-service
+  annotations:
+    service.antrea.io/external-ip-pool: "service-external-ip-pool"
+spec:
+  selector:
+    app: MyApp
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 9376
+  clusterIP: 10.96.0.11
+  type: LoadBalancer
+status:
+  loadBalancer:
+    ingress:
+    - ip: 10.10.0.2
+      hostname: node-1
+```
+
+You can validate that the Service can be accessed from the client using the
+`<external IP>:<port>` (`10.10.0.2:80/TCP` in the above example).
+
+### Limitations
+
+As described above, the Service externalIP management by Antrea configures a
+Service's external IP to a Node, so that the Node can receive Service requests.
+However, this requires that the externalIP on the Node be reachable through the
+Node network. The simplest way to achieve this is to reserve a range of IPs
+from the Node network subnet, and define Service ExternalIPPools with the
+reserved IPs, when the Nodes are connected to a layer 2 subnet. Or, another
+possible way might be to manually configure Node network routing (e.g. by
+adding a static route entry to the underlay router) to route the Service
+traffic to the Node that hosts the Service's externalIP.
+
+As of now, Antrea supports Service externalIP management only on Linux Nodes.
+Windows Nodes are not supported yet.
+
+## Using MetalLB with Antrea
+
+MetalLB also implements external IP management for Services of type
+LoadBalancer, and it can be deployed to a Kubernetes cluster with Antrea.
+MetalLB supports two modes - layer 2 mode and BGP mode - to advertise an
+Service external IP to the Node network. The layer 2 mode is similar to what
+Antrea external IP management implements and has the same limitation that the
+external IPs must be allocated from the Node network subnet. The BGP mode
+leverages BGP to advertise external IPs to the Node network router. It does
+not have the layer 2 subnet limitation, but requires the Node network to
+support BGP.
+
+MetalLB will automatically allocate external IPs for every Service of type
+LoadBalancer, and it sets the allocated IP to the `loadBalancer.ingress` field
+in the Service resource `status`. MetalLB also supports user specified `loadBalancerIP`
+in the Service spec. For more information, please refer to the [MetalLB usage](https://metallb.universe.tf/usage).
+
+To learn more about MetalLB concepts and functionalities, you can read the
+[MetalLB concepts](https://metallb.universe.tf/concepts).
+
+### Install MetalLB
+
+You can run the following commands to install MetalLB using the YAML manifests:
+
+```bash
+kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.11.0/manifests/namespace.yaml
+kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.11.0/manifests/metallb.yaml
+```
+
+The commands will deploy MetalLB of version 0.11.0 into Namespace
+`metallb-system`. You can also refer to this [MetalLB installation guide](https://metallb.universe.tf/installation)
+for other ways of installing MetalLB.
+
+As MetalLB will allocate external IPs for all Services of type LoadBalancer,
+once it is running, the Service external IP management feature of Antrea should
+not be enabled to avoid conflicts with MetalLB. You can deploy Antrea with the
+default configuration (in which the `ServiceExternalIP` feature gate of
+`antrea-agent` is set to `false`). MetalLB can work with both `AntreaProxy` and
+`kube-proxy` configurations of `antrea-agent`.
+
+### Configure MetalLB with layer 2 mode
+
+MetalLB is configured through a ConfigMap. To configure MetalLB to work in the
+layer 2 mode, you just need to provide the IP ranges to allocate external IPs.
+The IP ranges should be from the Node network subnet.
+
+For example:
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  namespace: metallb-system
+  name: config
+data:
+  config: |
+    address-pools:
+    - name: default
+      protocol: layer2
+      addresses:
+      - 10.10.0.2-10.10.0.10
+```
+
+### Configure MetalLB with BGP mode
+
+The BGP mode of MetalLB requires more configuration parameters to establish BGP
+peering to the router. The example below configures MetalLB using AS number
+64500 to connect to peer router 10.0.0.1 with AS number 64501:
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  namespace: metallb-system
+  name: config
+data:
+  config: |
+    peers:
+    - peer-address: 10.0.0.1
+      peer-asn: 64501
+      my-asn: 64500
+    address-pools:
+    - name: default
+      protocol: bgp
+      addresses:
+      - 10.10.0.2-10.10.0.10
+```
+
+In addition to the basic layer 2 and BGP mode configurations described in this
+document, MetalLB supports a few more advanced BGP configurations and supports
+configuring multiple IP pools which can use different modes. For more
+information, please refer to the [MetalLB configuration guide](https://metallb.universe.tf/configuration).
diff --git a/hack/generate-manifest.sh b/hack/generate-manifest.sh
index 3072f56c9..959c915a5 100755
--- a/hack/generate-manifest.sh
+++ b/hack/generate-manifest.sh
@@ -20,7 +20,7 @@ function echoerr {
     >&2 echo "$@"
 }
 
-_usage="Usage: $0 [--mode (dev|release)] [--encap-mode] [--kind] [--ipsec] [--no-proxy] [--no-np] [--k8s-1.15] [--keep] [--tun (geneve|vxlan|gre|stt)] [--verbose-log] [--help|-h]
+_usage="Usage: $0 [--mode (dev|release)] [--encap-mode] [--kind] [--ipsec] [--no-proxy] [--no-np] [--keep] [--tun (geneve|vxlan|gre|stt)] [--verbose-log] [--help|-h]
 Generate a YAML manifest for Antrea using Kustomize and print it to stdout.
         --mode (dev|release)          Choose the configuration variant that you need (default is 'dev')
         --encap-mode                  Traffic encapsulation mode. (default is 'encap')
@@ -33,7 +33,6 @@ Generate a YAML manifest for Antrea using Kustomize and print it to stdout.
         --no-legacy-crd               Generate a manifest without legacy CRD mirroring support enabled
         --endpointslice               Generate a manifest with EndpointSlice support enabled
         --no-np                       Generate a manifest with Antrea-native policies disabled
-        --k8s-1.15                    Generates a manifest which supports Kubernetes 1.15.
         --keep                        Debug flag which will preserve the generated kustomization.yml
         --tun (geneve|vxlan|gre|stt)  Choose encap tunnel type from geneve, gre, stt and vxlan (default is geneve)
         --verbose-log                 Generate a manifest with increased log-level (level 4) for Antrea agent and controller.
@@ -47,7 +46,7 @@ Generate a YAML manifest for Antrea using Kustomize and print it to stdout.
         --sriov                       Generates a manifest which enables use of Kubelet API for SR-IOV device info.
         --wireguard-go                Generate a manifest with WireGuard (golang implementation) encryption enabled.
                                       This option will work only for Kind clusters (when using '--kind').
-        --flexible-ipam               Generates a manifest for flexible ipam mode.
+        --flexible-ipam               Generates a manifest with flexible IPAM enabled.
         --whereabouts                 Generates a manifest which enables whereabouts configuration for secondary network IPAM.
         --help, -h                    Print this message and exit
         --multicast                   Generates a manifest for multicast.
@@ -150,6 +149,8 @@ case $key in
     shift
     ;;
     --k8s-1.15)
+    echoerr "The --k8s-1.15 flag is no longer supported"
+    exit 1
     K8S_115=true
     shift
     ;;
@@ -333,6 +334,7 @@ fi
 if $FLEXIBLE_IPAM; then
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*AntreaIPAM[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  AntreaIPAM: true/" antrea-controller.conf
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*AntreaIPAM[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  AntreaIPAM: true/" antrea-agent.conf
+    sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*enableBridgingMode[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/enableBridgingMode: true/" antrea-agent.conf
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*trafficEncapMode[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/trafficEncapMode: noEncap/" antrea-agent.conf
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*noSNAT[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/noSNAT: true/" antrea-agent.conf
 fi
@@ -582,24 +584,6 @@ if [ "$MODE" == "release" ]; then
     $KUSTOMIZE edit set image antrea=$IMG_NAME:$IMG_TAG
 fi
 
-# If --k8s-1.15 flag is set, then we have to patch certain resources.
-# For instance, the apiVersion/schema of CustomResourceDefinition and admission webhooks
-if $K8S_115; then
-    cp -a ../../patches/legacy ./
-    # Patch for controller.yml
-    $KUSTOMIZE edit add patch --path legacy/controller.json --kind MutatingWebhookConfiguration
-    $KUSTOMIZE edit add patch --path legacy/controller.json --kind ValidatingWebhookConfiguration
-    # Patch for all CustomResourceDefinition
-    $KUSTOMIZE edit add patch --path legacy/crdVersion.json --kind CustomResourceDefinition
-    $KUSTOMIZE edit add patch --path legacy/crdClusterInformation.json --kind CustomResourceDefinition --name antreaagentinfos.clusterinformation.antrea.tanzu.vmware.com
-    $KUSTOMIZE edit add patch --path legacy/crdClusterInformation.json --kind CustomResourceDefinition --name antreacontrollerinfos.clusterinformation.antrea.tanzu.vmware.com
-    $KUSTOMIZE edit add patch --path legacy/crdTraceflow.json --kind CustomResourceDefinition --name traceflows.ops.antrea.tanzu.vmware.com
-    $KUSTOMIZE edit add patch --path legacy/crdTier.json --kind CustomResourceDefinition --name tiers.security.antrea.tanzu.vmware.com
-    $KUSTOMIZE edit add patch --path legacy/crdClusterNetworkPolicy.json --kind CustomResourceDefinition --name clusternetworkpolicies.security.antrea.tanzu.vmware.com
-    $KUSTOMIZE edit add patch --path legacy/crdNetworkPolicy.json --kind CustomResourceDefinition --name networkpolicies.security.antrea.tanzu.vmware.com
-    $KUSTOMIZE edit add patch --path legacy/crdExternalEntity.json --kind CustomResourceDefinition --name externalentities.core.antrea.tanzu.vmware.com
-fi
-
 $KUSTOMIZE build
 
 popd > /dev/null
diff --git a/hack/windows/Helper.psm1 b/hack/windows/Helper.psm1
index 92ba4ec65..09d0b7f3d 100644
--- a/hack/windows/Helper.psm1
+++ b/hack/windows/Helper.psm1
@@ -163,7 +163,7 @@ function New-KubeProxyServiceInterface {
         Write-Host "Network adapter $INTERFACE_TO_ADD_SERVICE_IP exists, exit."
         return
     }
-    if (!(Get-VMSwitch -Name $hnsSwitchName -ErrorAction SilentlyContinue)) {
+    if (!(Get-VMSwitch -ComputerName $(hostname) -Name $hnsSwitchName -ErrorAction SilentlyContinue)) {
         Write-Host "Creating internal switch: $hnsSwitchName for kube-proxy"
         New-VMSwitch -name $hnsSwitchName -SwitchType Internal
     }
@@ -216,7 +216,7 @@ function Start-OVSServices {
     }
     # Try to cleanup ovsdb-server configurations if the antrea-hnsnetwork is not existing. Or ovs-vswitchd service
     # will can not get started.
-    if (!(Get-VMswitch -Name "antrea-hnsnetwork" -SwitchType External -ErrorAction SilentlyContinue)) {
+    if (!(Get-VMswitch -ComputerName $(hostname) -Name "antrea-hnsnetwork" -SwitchType External -ErrorAction SilentlyContinue)) {
         & ovs-vsctl.exe --no-wait --if-exists del-br br-int
         if ($LASTEXITCODE) {
             return $false
diff --git a/hack/windows/Prepare-ServiceInterface.ps1 b/hack/windows/Prepare-ServiceInterface.ps1
index 33159867e..c962c2cb8 100644
--- a/hack/windows/Prepare-ServiceInterface.ps1
+++ b/hack/windows/Prepare-ServiceInterface.ps1
@@ -25,7 +25,7 @@ if (Get-NetAdapter -InterfaceAlias $INTERFACE_TO_ADD_SERVICE_IP -ErrorAction Sil
     return
 }
 [Environment]::SetEnvironmentVariable("INTERFACE_TO_ADD_SERVICE_IP", $INTERFACE_TO_ADD_SERVICE_IP, [System.EnvironmentVariableTarget]::Machine)
-$hnsSwitchName = $(Get-VMSwitch -SwitchType Internal).Name
+$hnsSwitchName = $(Get-VMSwitch -ComputerName $(hostname) -SwitchType Internal).Name
 Add-VMNetworkAdapter -ManagementOS -Name $InterfaceAlias -SwitchName $hnsSwitchName
 Set-NetIPInterface -ifAlias $INTERFACE_TO_ADD_SERVICE_IP -Forwarding Enabled
 
diff --git a/pkg/agent/agent_linux.go b/pkg/agent/agent_linux.go
index 26cf1c0e2..d3e750554 100644
--- a/pkg/agent/agent_linux.go
+++ b/pkg/agent/agent_linux.go
@@ -39,8 +39,8 @@ func (i *Initializer) prepareHostNetwork() error {
 	return nil
 }
 
-// prepareOVSBridge returns immediately on Linux if connectUplinkToBridge is false.
 func (i *Initializer) prepareOVSBridge() error {
+	// Return immediately on Linux if connectUplinkToBridge is false.
 	if !i.connectUplinkToBridge {
 		return nil
 	}
@@ -194,8 +194,8 @@ func (i *Initializer) restoreHostRoutesToInterface(ifaceName string) error {
 	return nil
 }
 
-// BridgeUplinkToOVSBridge returns immediately on Linux if connectUplinkToBridge is false.
-func (i *Initializer) BridgeUplinkToOVSBridge() error {
+func (i *Initializer) ConnectUplinkToOVSBridge() error {
+	// Return immediately on Linux if connectUplinkToBridge is false.
 	if !i.connectUplinkToBridge {
 		return nil
 	}
@@ -208,16 +208,16 @@ func (i *Initializer) BridgeUplinkToOVSBridge() error {
 	uplink := uplinkNetConfig.Name
 	if _, err := i.ovsBridgeClient.GetOFPort(uplink, false); err == nil {
 		klog.Infof("Uplink %s already exists, skip the configuration", uplink)
-		return err
+		return nil
 	}
 	// Create uplink port.
-	uplinkPortUUId, err := i.ovsBridgeClient.CreateUplinkPort(uplink, config.UplinkOFPort, map[string]interface{}{interfacestore.AntreaInterfaceTypeKey: interfacestore.AntreaUplink})
+	uplinkPortUUID, err := i.ovsBridgeClient.CreateUplinkPort(uplink, config.UplinkOFPort, map[string]interface{}{interfacestore.AntreaInterfaceTypeKey: interfacestore.AntreaUplink})
 	if err != nil {
 		return fmt.Errorf("failed to add uplink port %s: err=%w", uplink, err)
 	}
 	// Add newly created uplinkInterface to interface cache. This will be overwritten by initInterfaceStore.
 	uplinkInterface := interfacestore.NewUplinkInterface(uplink)
-	uplinkInterface.OVSPortConfig = &interfacestore.OVSPortConfig{uplinkPortUUId, config.UplinkOFPort} //nolint: govet
+	uplinkInterface.OVSPortConfig = &interfacestore.OVSPortConfig{uplinkPortUUID, config.UplinkOFPort} //nolint: govet
 	i.ifaceStore.AddInterface(uplinkInterface)
 
 	// Move network configuration of uplink interface to OVS bridge local interface.
diff --git a/pkg/agent/agent_windows.go b/pkg/agent/agent_windows.go
index c43b71587..8b62f1f31 100644
--- a/pkg/agent/agent_windows.go
+++ b/pkg/agent/agent_windows.go
@@ -170,6 +170,14 @@ func (i *Initializer) prepareOVSBridge() error {
 	i.ifaceStore.AddInterface(uplinkInterface)
 	ovsCtlClient := ovsctl.NewClient(i.ovsBridge)
 
+	// Enable IP forwarding on the bridge local interface. Traffic from the uplink interface will be output to the bridge
+	// local interface directly. When an external client connects to a LoadBalancer type Service, and the packets of the
+	// connection are routed to the selected backend Pod via the bridge interface; if we do not enable IP forwarding on
+	// the bridge interface, the packet will be discarded on the bridge interface as the destination of the packet
+	// is not the Node.
+	if err = util.EnableIPForwarding(brName); err != nil {
+		return err
+	}
 	// Set the uplink with "no-flood" config, so that the IP of local Pods and "antrea-gw0" will not be leaked to the
 	// underlay network by the "normal" flow entry.
 	if err = ovsCtlClient.SetPortNoFlood(config.UplinkOFPort); err != nil {
@@ -263,8 +271,9 @@ func GetTransportIPNetDeviceByName(ifaceName string, ovsBridgeName string) (*net
 	return nil, nil, nil, fmt.Errorf("unable to find local IP and device")
 }
 
-// BridgeUplinkToOVSBridge returns immediately on Windows.
-func (i *Initializer) BridgeUplinkToOVSBridge() error { return nil }
+// ConnectUplinkToOVSBridge returns immediately on Windows. The uplink interface
+// will be connected to the bridge in prepareOVSBridge().
+func (i *Initializer) ConnectUplinkToOVSBridge() error { return nil }
 
 // RestoreOVSBridge returns immediately in Windows.
 // OVS is managed by system in Windows, network config can be retained after Antrea shutdown.
diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go
index 09627844d..33b11e48b 100644
--- a/pkg/agent/openflow/client.go
+++ b/pkg/agent/openflow/client.go
@@ -30,6 +30,7 @@ import (
 	"antrea.io/antrea/pkg/agent/util"
 	binding "antrea.io/antrea/pkg/ovs/openflow"
 	utilip "antrea.io/antrea/pkg/util/ip"
+	"antrea.io/antrea/pkg/util/runtime"
 	"antrea.io/antrea/third_party/proxy"
 )
 
@@ -113,13 +114,6 @@ type Client interface {
 	InstallServiceFlows(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16, nodeLocalExternal bool, svcType v1.ServiceType) error
 	// UninstallServiceFlows removes flows installed by InstallServiceFlows.
 	UninstallServiceFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error
-	// InstallLoadBalancerServiceFromOutsideFlows installs flows for LoadBalancer Service traffic from outside node.
-	// The traffic is received from uplink port and will be forwarded to gateway by the installed flows. And then
-	// kube-proxy will handle the traffic.
-	// This function is only used for Windows platform.
-	InstallLoadBalancerServiceFromOutsideFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error
-	// UninstallLoadBalancerServiceFromOutsideFlows removes flows installed by InstallLoadBalancerServiceFromOutsideFlows.
-	UninstallLoadBalancerServiceFromOutsideFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error
 
 	// GetFlowTableStatus should return an array of flow table status, all existing flow tables should be included in the list.
 	GetFlowTableStatus() []binding.TableStatus
@@ -146,9 +140,7 @@ type Client interface {
 	// are removed from PolicyRule.From, else from PolicyRule.To.
 	DeletePolicyRuleAddress(ruleID uint32, addrType types.AddressType, addresses []types.Address, priority *uint16) error
 
-	// InstallBridgeUplinkFlows installs Openflow flows between bridge local port and uplink port to support
-	// host networking.
-	// This function is only used for Windows platform.
+	// InstallBridgeUplinkFlows installs Openflow flows between bridge local port and uplink port to support host networking.
 	InstallBridgeUplinkFlows() error
 
 	// InstallExternalFlows sets up flows to enable Pods to communicate to
@@ -160,8 +152,7 @@ type Client interface {
 
 	// InstallSNATMarkFlows installs flows for a local SNAT IP. On Linux, a
 	// single flow is added to mark the packets tunnelled from remote Nodes
-	// that should be SNAT'd with the SNAT IP. On Windows, an extra flow is
-	// added to perform SNAT for the marked packets with the SNAT IP.
+	// that should be SNAT'd with the SNAT IP.
 	InstallSNATMarkFlows(snatIP net.IP, mark uint32) error
 
 	// UninstallSNATMarkFlows removes the flows installed to set the packet
@@ -850,11 +841,11 @@ func (c *client) InstallExternalFlows(exceptCIDRs []net.IPNet) error {
 }
 
 func (c *client) InstallSNATMarkFlows(snatIP net.IP, mark uint32) error {
-	flows := c.snatMarkFlows(snatIP, mark)
+	flow := c.snatIPFromTunnelFlow(snatIP, mark)
 	cacheKey := fmt.Sprintf("s%x", mark)
 	c.replayMutex.RLock()
 	defer c.replayMutex.RUnlock()
-	return c.addFlows(c.snatFlowCache, cacheKey, flows)
+	return c.addFlows(c.snatFlowCache, cacheKey, []binding.Flow{flow})
 }
 
 func (c *client) UninstallSNATMarkFlows(mark uint32) error {
@@ -1247,3 +1238,19 @@ func (c *client) SendIGMPQueryPacketOut(
 	packetOutObj := packetOutBuilder.Done()
 	return c.bridge.SendPacketOut(packetOutObj)
 }
+
+func (c *client) InstallBridgeUplinkFlows() error {
+	if runtime.IsWindowsPlatform() || c.connectUplinkToBridge {
+		podCIDRMap := make(map[binding.Protocol]net.IPNet)
+		if c.nodeConfig.PodIPv4CIDR != nil {
+			podCIDRMap[binding.ProtocolIP] = *c.nodeConfig.PodIPv4CIDR
+		}
+		//TODO: support IPv6
+		flows := c.hostBridgeUplinkFlows(podCIDRMap, cookie.Default)
+		if err := c.ofEntryOperations.AddAll(flows); err != nil {
+			return err
+		}
+		c.hostNetworkingFlows = flows
+	}
+	return nil
+}
diff --git a/pkg/agent/openflow/client_other.go b/pkg/agent/openflow/client_other.go
deleted file mode 100644
index 3e5562d50..000000000
--- a/pkg/agent/openflow/client_other.go
+++ /dev/null
@@ -1,50 +0,0 @@
-//go:build !windows
-// +build !windows
-
-// package openflow is needed by antctl which is compiled for macOS too.
-
-// Copyright 2021 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"
-
-	"antrea.io/antrea/pkg/agent/openflow/cookie"
-	binding "antrea.io/antrea/pkg/ovs/openflow"
-)
-
-func (c *client) InstallBridgeUplinkFlows() error {
-	if !c.connectUplinkToBridge {
-		return nil
-	}
-	// TODO(gran): support IPv6
-	if c.nodeConfig.PodIPv4CIDR != nil {
-		flows := c.hostBridgeUplinkFlows(*c.nodeConfig.PodIPv4CIDR, cookie.Default)
-		if err := c.ofEntryOperations.AddAll(flows); err != nil {
-			return err
-		}
-		c.hostNetworkingFlows = flows
-	}
-	return nil
-}
-
-func (c *client) InstallLoadBalancerServiceFromOutsideFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error {
-	return nil
-}
-
-func (c *client) UninstallLoadBalancerServiceFromOutsideFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error {
-	return nil
-}
diff --git a/pkg/agent/openflow/client_windows.go b/pkg/agent/openflow/client_windows.go
deleted file mode 100644
index bfe651df2..000000000
--- a/pkg/agent/openflow/client_windows.go
+++ /dev/null
@@ -1,53 +0,0 @@
-//go:build windows
-// +build windows
-
-// package openflow is needed by antctl which is compiled for macOS too.
-
-// Copyright 2021 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 (
-	"fmt"
-	"net"
-
-	"antrea.io/antrea/pkg/agent/openflow/cookie"
-	binding "antrea.io/antrea/pkg/ovs/openflow"
-)
-
-func (c *client) InstallBridgeUplinkFlows() error {
-	flows := c.hostBridgeUplinkFlows(*c.nodeConfig.PodIPv4CIDR, cookie.Default)
-	if err := c.ofEntryOperations.AddAll(flows); err != nil {
-		return err
-	}
-	c.hostNetworkingFlows = flows
-	return nil
-}
-
-func (c *client) InstallLoadBalancerServiceFromOutsideFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error {
-	c.replayMutex.RLock()
-	defer c.replayMutex.RUnlock()
-	var flows []binding.Flow
-	flows = append(flows, c.loadBalancerServiceFromOutsideFlow(svcIP, svcPort, protocol))
-	cacheKey := fmt.Sprintf("L%s%s%x", svcIP, protocol, svcPort)
-	return c.addFlows(c.serviceFlowCache, cacheKey, flows)
-}
-
-func (c *client) UninstallLoadBalancerServiceFromOutsideFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error {
-	c.replayMutex.RLock()
-	defer c.replayMutex.RUnlock()
-	cacheKey := fmt.Sprintf("L%s%s%x", svcIP, protocol, svcPort)
-	return c.deleteFlows(c.serviceFlowCache, cacheKey)
-}
diff --git a/pkg/agent/openflow/network_policy.go b/pkg/agent/openflow/network_policy.go
index badd3020c..fea261c7e 100644
--- a/pkg/agent/openflow/network_policy.go
+++ b/pkg/agent/openflow/network_policy.go
@@ -525,7 +525,8 @@ type policyRuleConjunction struct {
 	serviceClause *clause
 	actionFlows   []binding.Flow
 	metricFlows   []binding.Flow
-	// NetworkPolicy reference information for debugging usage.
+	// NetworkPolicy reference information for debugging usage, its value can be nil
+	// for conjunctions that are not built for a specific NetworkPolicy, e.g. DNS packetin Conjunction.
 	npRef       *v1beta2.NetworkPolicyReference
 	ruleTableID uint8
 }
@@ -1455,6 +1456,11 @@ func (c *client) GetNetworkPolicyFlowKeys(npName, npNamespace string) []string {
 
 	for _, conjObj := range c.policyCache.List() {
 		conj := conjObj.(*policyRuleConjunction)
+		// If the NetworkPolicyReference in the policyRuleConjunction is nil then that entry in client's
+		// policyCache should be ignored because here we need to dump flows of NetworkPolicy.
+		if conj.npRef == nil {
+			continue
+		}
 		if conj.npRef.Name == npName && conj.npRef.Namespace == npNamespace {
 			// There can be duplicated flows added due to conjunctive matches
 			// shared by multiple policy rules (clauses).
diff --git a/pkg/agent/openflow/pipeline.go b/pkg/agent/openflow/pipeline.go
index 784799b54..04c7feb29 100644
--- a/pkg/agent/openflow/pipeline.go
+++ b/pkg/agent/openflow/pipeline.go
@@ -42,7 +42,6 @@ import (
 
 var (
 	ClassifierTable              = binding.NewOFTable(0, "Classification")
-	UplinkTable                  = binding.NewOFTable(5, "Uplink")
 	SpoofGuardTable              = binding.NewOFTable(10, "SpoofGuard")
 	arpResponderTable            = binding.NewOFTable(20, "ARPResponder")
 	IPv6Table                    = binding.NewOFTable(21, "IPv6")
@@ -181,6 +180,23 @@ func GetTableList() []binding.Table {
 	return tables
 }
 
+// CleanOFTableCache is used to reset ofTableCache and only used in integration tests. When all integration tests about
+// openflow run in batch, unexpected flows could be installed on OVS due to stale ofTableCache, which may cause some tests
+// to fail. For example, for TestFuncA, SNATTable is needed; for TestFuncB, SNATTable is not needed. If TestFuncB is run
+// after TestFuncA, since ofTableCache (SNATTable is added by TestFuncA) is not reset, default flow of SNATTable will also
+// be realized on OVS when running TestFuncB (see "func (c *client) defaultFlows() (flows []binding.Flow)"). Note that,
+// the unexpected flows are not included in the map tableCache of OFBridge defined in pkg/ovs/openflow/ofctrl_bridge.go,
+// because the bridge will be destroyed after every test. For some tests, function checkOVSFlowMetrics (defined in
+// test/integration/agent/openflow_test.go) is used to check the flow number of every installed table. The expected table
+// list is read from the map tableCache of OFBridge, but the actual table list is dumped from OVS bridge (including the
+// unexpected flow). They are different, and as a result, TestFuncB will fail.
+func CleanOFTableCache() {
+	objs := ofTableCache.List()
+	for i := 0; i < len(objs); i++ {
+		ofTableCache.Delete(objs[i])
+	}
+}
+
 func GetAntreaPolicyEgressTables() []binding.Table {
 	return []binding.Table{
 		AntreaPolicyEgressRuleTable,
@@ -2225,20 +2241,6 @@ func (c *client) snatRuleFlow(ofPort uint32, snatIP net.IP, snatMark uint32, loc
 		Done()
 }
 
-// loadBalancerServiceFromOutsideFlow generates the flow to forward LoadBalancer service traffic from outside node
-// to gateway. kube-proxy will then handle the traffic.
-// This flow is for Windows Node only.
-func (c *client) loadBalancerServiceFromOutsideFlow(svcIP net.IP, svcPort uint16, protocol binding.Protocol) binding.Flow {
-	return UplinkTable.BuildFlow(priorityHigh).
-		MatchProtocol(protocol).
-		MatchDstPort(svcPort, nil).
-		MatchRegMark(FromUplinkRegMark).
-		MatchDstIP(svcIP).
-		Action().Output(config.HostGatewayOFPort).
-		Cookie(c.cookieAllocator.Request(cookie.Service).Raw()).
-		Done()
-}
-
 // serviceClassifierFlows generate the flows to match the first packet of Service NodePort and set a bit of a register
 // to mark the Service type as NodePort.
 func (c *client) serviceClassifierFlows(nodePortAddresses []net.IP, ipProtocol binding.Protocol) []binding.Flow {
@@ -2584,13 +2586,9 @@ func (c *client) generatePipeline() {
 	} else {
 		c.createOFTable(DNATTable, c.egressEntryTable, binding.TableMissActionNext)
 	}
-	// The default SNAT is implemented with OVS on Windows.
-	if c.enableEgress || runtime.IsWindowsPlatform() {
+	if c.enableEgress {
 		c.createOFTable(SNATTable, L2ForwardingCalcTable.GetID(), binding.TableMissActionNext)
 	}
-	if runtime.IsWindowsPlatform() || c.connectUplinkToBridge {
-		c.createOFTable(UplinkTable, SpoofGuardTable.GetID(), binding.TableMissActionNone)
-	}
 	if c.enableAntreaPolicy {
 		c.createOFTable(AntreaPolicyEgressRuleTable, EgressRuleTable.GetID(), binding.TableMissActionNext)
 		c.createOFTable(AntreaPolicyIngressRuleTable, IngressRuleTable.GetID(), binding.TableMissActionNext)
diff --git a/pkg/agent/openflow/pipeline_other.go b/pkg/agent/openflow/pipeline_other.go
index 379428004..4c1f72bde 100644
--- a/pkg/agent/openflow/pipeline_other.go
+++ b/pkg/agent/openflow/pipeline_other.go
@@ -27,14 +27,10 @@ import (
 	binding "antrea.io/antrea/pkg/ovs/openflow"
 )
 
-func (c *client) snatMarkFlows(snatIP net.IP, mark uint32) []binding.Flow {
-	return []binding.Flow{c.snatIPFromTunnelFlow(snatIP, mark)}
-}
-
 // hostBridgeUplinkFlows generates the flows that forward traffic between the
 // bridge local port and the uplink port to support the host traffic.
 // TODO(gran): sync latest changes from pipeline_windows.go
-func (c *client) hostBridgeUplinkFlows(localSubnet net.IPNet, category cookie.Category) (flows []binding.Flow) {
+func (c *client) hostBridgeUplinkFlows(localSubnetMap map[binding.Protocol]net.IPNet, category cookie.Category) (flows []binding.Flow) {
 	flows = []binding.Flow{
 		ClassifierTable.BuildFlow(priorityNormal).
 			MatchInPort(config.UplinkOFPort).
diff --git a/pkg/agent/openflow/pipeline_windows.go b/pkg/agent/openflow/pipeline_windows.go
index ccec12503..a3df5f328 100644
--- a/pkg/agent/openflow/pipeline_windows.go
+++ b/pkg/agent/openflow/pipeline_windows.go
@@ -22,65 +22,13 @@ import (
 
 	"antrea.io/antrea/pkg/agent/config"
 	"antrea.io/antrea/pkg/agent/openflow/cookie"
-	"antrea.io/antrea/pkg/agent/types"
 	binding "antrea.io/antrea/pkg/ovs/openflow"
 )
 
-const (
-	// ctZoneSNAT is only used on Windows and only when AntreaProxy is enabled.
-	// When a Pod access a ClusterIP Service, and the IP of the selected endpoint
-	// is not in "cluster-cidr". The request packets need to be SNAT'd(set src IP to local Node IP)
-	// after have been DNAT'd(set dst IP to endpoint IP).
-	// For example, the endpoint Pod may run in hostNetwork mode and the IP of the endpoint
-	// will be the current Node IP.
-	// We need to use a different ct_zone to track the SNAT'd connection because OVS
-	// does not support doing both DNAT and SNAT in the same ct_zone.
-	//
-	// An example of the connection is a Pod accesses kubernetes API service:
-	// Pod --> DNAT(CtZone) --> SNAT(ctZoneSNAT) --> Endpoint(API server NodeIP)
-	// Pod <-- unDNAT(CtZone) <-- unSNAT(ctZoneSNAT) <-- Endpoint(API server NodeIP)
-	ctZoneSNAT = 0xffdc
-)
-
-var (
-	// snatCTMark indicates SNAT is performed for packets of the connection.
-	snatCTMark = binding.NewCTMark(0x40, 0, 31)
-)
-
-func (c *client) snatMarkFlows(snatIP net.IP, mark uint32) []binding.Flow {
-	snatIPRange := &binding.IPRange{StartIP: snatIP, EndIP: snatIP}
-	nextTable := ConntrackCommitTable.GetNext()
-	flows := []binding.Flow{
-		c.snatIPFromTunnelFlow(snatIP, mark),
-		ConntrackCommitTable.BuildFlow(priorityNormal).
-			MatchProtocol(binding.ProtocolIP).
-			MatchCTStateNew(true).MatchCTStateTrk(true).MatchCTStateDNAT(false).
-			MatchPktMark(mark, &types.SNATIPMarkMask).
-			Action().CT(true, nextTable, CtZone).
-			SNAT(snatIPRange, nil).
-			LoadToCtMark(snatCTMark).CTDone().
-			Cookie(c.cookieAllocator.Request(cookie.SNAT).Raw()).
-			Done(),
-	}
-
-	if c.enableProxy {
-		flows = append(flows, ConntrackCommitTable.BuildFlow(priorityNormal).
-			MatchProtocol(binding.ProtocolIP).
-			MatchCTStateNew(true).MatchCTStateTrk(true).MatchCTStateDNAT(true).
-			MatchPktMark(mark, &types.SNATIPMarkMask).
-			Action().CT(true, nextTable, ctZoneSNAT).
-			SNAT(snatIPRange, nil).
-			LoadToCtMark(snatCTMark).CTDone().
-			Cookie(c.cookieAllocator.Request(cookie.SNAT).Raw()).
-			Done())
-	}
-	return flows
-}
-
 // hostBridgeUplinkFlows generates the flows that forward traffic between the
 // bridge local port and the uplink port to support the host traffic with
 // outside.
-func (c *client) hostBridgeUplinkFlows(localSubnet net.IPNet, category cookie.Category) (flows []binding.Flow) {
+func (c *client) hostBridgeUplinkFlows(localSubnetMap map[binding.Protocol]net.IPNet, category cookie.Category) (flows []binding.Flow) {
 	flows = []binding.Flow{
 		ClassifierTable.BuildFlow(priorityNormal).
 			MatchInPort(config.UplinkOFPort).
@@ -96,14 +44,17 @@ func (c *client) hostBridgeUplinkFlows(localSubnet net.IPNet, category cookie.Ca
 	if c.networkConfig.TrafficEncapMode.SupportsNoEncap() {
 		// If NoEncap is enabled, the reply packets from remote Pod can be forwarded to local Pod directly.
 		// by explicitly resubmitting them to ServiceHairpinTable and marking "macRewriteMark" at same time.
-		flows = append(flows, ClassifierTable.BuildFlow(priorityHigh).MatchProtocol(binding.ProtocolIP).
-			MatchInPort(config.UplinkOFPort).
-			MatchDstIPNet(localSubnet).
-			Action().LoadRegMark(FromUplinkRegMark).
-			Action().LoadRegMark(RewriteMACRegMark).
-			Action().GotoTable(ServiceHairpinTable.GetID()).
-			Cookie(c.cookieAllocator.Request(category).Raw()).
-			Done())
+		for ipProtocol, localSubnet := range localSubnetMap {
+			flows = append(flows, ClassifierTable.BuildFlow(priorityHigh).
+				MatchProtocol(ipProtocol).
+				MatchInPort(config.UplinkOFPort).
+				MatchDstIPNet(localSubnet).
+				Action().LoadRegMark(FromUplinkRegMark).
+				Action().LoadRegMark(RewriteMACRegMark).
+				Action().GotoTable(ServiceHairpinTable.GetID()).
+				Cookie(c.cookieAllocator.Request(category).Raw()).
+				Done())
+		}
 	}
 	return flows
 }
diff --git a/pkg/agent/proxy/proxier.go b/pkg/agent/proxy/proxier.go
index 571abc29c..2ab8f7eeb 100644
--- a/pkg/agent/proxy/proxier.go
+++ b/pkg/agent/proxy/proxier.go
@@ -168,23 +168,25 @@ func (p *proxier) removeStaleServices() {
 		}
 		// Remove Service group whose Endpoints are local.
 		if svcInfo.NodeLocalExternal() {
-			groupIDLocal, _ := p.groupCounter.Get(svcPortName, true)
-			if err := p.ofClient.UninstallServiceGroup(groupIDLocal); err != nil {
-				klog.ErrorS(err, "Failed to remove Group of local Endpoints for Service", "Service", svcPortName)
-				continue
+			if groupIDLocal, exist := p.groupCounter.Get(svcPortName, true); exist {
+				if err := p.ofClient.UninstallServiceGroup(groupIDLocal); err != nil {
+					klog.ErrorS(err, "Failed to remove Group of local Endpoints for Service", "Service", svcPortName)
+					continue
+				}
+				p.groupCounter.Recycle(svcPortName, true)
 			}
-			p.groupCounter.Recycle(svcPortName, true)
 		}
 		// Remove Service group which has all Endpoints.
-		groupID, _ := p.groupCounter.Get(svcPortName, false)
-		if err := p.ofClient.UninstallServiceGroup(groupID); err != nil {
-			klog.ErrorS(err, "Failed to remove Group of all Endpoints for Service", "Service", svcPortName)
-			continue
+		if groupID, exist := p.groupCounter.Get(svcPortName, false); exist {
+			if err := p.ofClient.UninstallServiceGroup(groupID); err != nil {
+				klog.ErrorS(err, "Failed to remove Group of all Endpoints for Service", "Service", svcPortName)
+				continue
+			}
+			p.groupCounter.Recycle(svcPortName, false)
 		}
 
 		delete(p.serviceInstalledMap, svcPortName)
 		p.deleteServiceByIP(svcInfo.String())
-		p.groupCounter.Recycle(svcPortName, false)
 	}
 }
 
@@ -314,9 +316,6 @@ func (p *proxier) installLoadBalancerService(groupID binding.GroupIDType, loadBa
 			if err := p.ofClient.InstallServiceFlows(groupID, net.ParseIP(ingress), svcPort, protocol, affinityTimeout, nodeLocalExternal, corev1.ServiceTypeLoadBalancer); err != nil {
 				return fmt.Errorf("failed to install Service LoadBalancer load balancing flows: %w", err)
 			}
-			if err := p.ofClient.InstallLoadBalancerServiceFromOutsideFlows(net.ParseIP(ingress), svcPort, protocol); err != nil {
-				return fmt.Errorf("failed to install Service LoadBalancer flows: %w", err)
-			}
 		}
 	}
 	if p.proxyAll {
@@ -334,9 +333,6 @@ func (p *proxier) uninstallLoadBalancerService(loadBalancerIPStrings []string, s
 			if err := p.ofClient.UninstallServiceFlows(net.ParseIP(ingress), svcPort, protocol); err != nil {
 				return fmt.Errorf("failed to remove Service LoadBalancer load balancing flows: %w", err)
 			}
-			if err := p.ofClient.UninstallLoadBalancerServiceFromOutsideFlows(net.ParseIP(ingress), svcPort, protocol); err != nil {
-				return fmt.Errorf("failed to remove Service LoadBalancer flows: %w", err)
-			}
 		}
 	}
 	if p.proxyAll {
@@ -351,7 +347,7 @@ func (p *proxier) uninstallLoadBalancerService(loadBalancerIPStrings []string, s
 func (p *proxier) installServices() {
 	for svcPortName, svcPort := range p.serviceMap {
 		svcInfo := svcPort.(*types.ServiceInfo)
-		groupID, _ := p.groupCounter.Get(svcPortName, false)
+		groupID := p.groupCounter.AllocateIfNotExist(svcPortName, false)
 		endpointsInstalled, ok := p.endpointsInstalledMap[svcPortName]
 		if !ok {
 			endpointsInstalled = map[string]k8sproxy.Endpoint{}
@@ -455,7 +451,7 @@ func (p *proxier) installServices() {
 			if p.proxyAll {
 				if svcInfo.NodeLocalExternal() {
 					// Install another group when Service externalTrafficPolicy is Local.
-					groupIDLocal, _ := p.groupCounter.Get(svcPortName, true)
+					groupIDLocal := p.groupCounter.AllocateIfNotExist(svcPortName, true)
 					var localEndpointList []k8sproxy.Endpoint
 					for _, ed := range endpointUpdateList {
 						if !ed.GetIsLocal() {
@@ -472,12 +468,13 @@ func (p *proxier) installServices() {
 					// unconditionally. If the group doesn't exist on OVS, then the return value will be nil; if the
 					// group exists on OVS, and after it is uninstalled successfully, then the return value will be also
 					// nil.
-					groupIDLocal, _ := p.groupCounter.Get(svcPortName, true)
-					if err := p.ofClient.UninstallServiceGroup(groupIDLocal); err != nil {
-						klog.ErrorS(err, "Failed to remove Group of local Endpoints for Service", "Service", svcPortName)
-						continue
+					if groupIDLocal, exist := p.groupCounter.Get(svcPortName, true); exist {
+						if err := p.ofClient.UninstallServiceGroup(groupIDLocal); err != nil {
+							klog.ErrorS(err, "Failed to remove Group of local Endpoints for Service", "Service", svcPortName)
+							continue
+						}
+						p.groupCounter.Recycle(svcPortName, true)
 					}
-					p.groupCounter.Recycle(svcPortName, true)
 				}
 			}
 
@@ -528,7 +525,7 @@ func (p *proxier) installServices() {
 			// group whose Endpoints are local.
 			nGroupID := groupID
 			if svcInfo.NodeLocalExternal() {
-				nGroupID, _ = p.groupCounter.Get(svcPortName, true)
+				nGroupID = p.groupCounter.AllocateIfNotExist(svcPortName, true)
 			}
 
 			if p.proxyAll {
@@ -792,9 +789,12 @@ func (p *proxier) GetServiceFlowKeys(serviceName, namespace string) ([]string, [
 		svcFlows := p.ofClient.GetServiceFlowKeys(svcInfo.ClusterIP(), uint16(svcInfo.Port()), svcInfo.OFProtocol, epList)
 		flows = append(flows, svcFlows...)
 
-		// TODO: This causes antctl command output incomplete groups, fix it latter.
-		groupID, _ := p.groupCounter.Get(svcPortName, false)
-		groups = append(groups, groupID)
+		if groupID, ok := p.groupCounter.Get(svcPortName, false); ok {
+			groups = append(groups, groupID)
+		}
+		if groupID, ok := p.groupCounter.Get(svcPortName, true); ok {
+			groups = append(groups, groupID)
+		}
 	}
 
 	return flows, groups, found
diff --git a/pkg/agent/proxy/types/groupcounter.go b/pkg/agent/proxy/types/groupcounter.go
index 0e18328c8..9005f9cb1 100644
--- a/pkg/agent/proxy/types/groupcounter.go
+++ b/pkg/agent/proxy/types/groupcounter.go
@@ -27,15 +27,14 @@ import (
 
 // GroupCounter generates and manages global unique group ID.
 type GroupCounter interface {
-	// Get generates a global unique group ID for a specific service.
-	// If the group ID of the service has been generated, then return the
-	// prior one. The bool return value indicates whether the groupID is newly
-	// generated.
+	// AllocateIfNotExist generates a global unique group ID for a Service if the group ID has not been generated, then
+	// return the group ID (newly allocated or already allocated).
+	AllocateIfNotExist(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) binding.GroupIDType
+	// Get gets the group ID for the Service.
 	Get(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) (binding.GroupIDType, bool)
-	// Recycle removes a Service Group ID mapping. The recycled groupID can be
-	// reused.
+	// Recycle removes the Service group ID mapping. The recycled group ID can be reused.
 	Recycle(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) bool
-	// GetAllGroupIDs gets all groupID related to a Service.
+	// GetAllGroupIDs gets all group IDs related to the Service.
 	GetAllGroupIDs(svcNamespacedName string) []binding.GroupIDType
 }
 
@@ -80,18 +79,26 @@ func (c *groupCounter) deleteServicePortNameMap(svcNamespacedName string, svcKey
 	}
 }
 
-func (c *groupCounter) Get(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) (binding.GroupIDType, bool) {
+func (c *groupCounter) AllocateIfNotExist(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) binding.GroupIDType {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 	key := keyString(svcPortName, isEndpointsLocal)
 	if id, ok := c.groupMap[key]; ok {
-		return id, false
+		return id
 	}
 	id := c.groupAllocator.Allocate()
 	c.groupMap[key] = id
 	c.updateServicePortNameMap(svcPortName.NamespacedName.String(), key)
 	c.groupIDUpdates <- svcPortName.NamespacedName.String()
-	return id, true
+	return id
+}
+
+func (c *groupCounter) Get(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) (binding.GroupIDType, bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	key := keyString(svcPortName, isEndpointsLocal)
+	id, exist := c.groupMap[key]
+	return id, exist
 }
 
 func (c *groupCounter) Recycle(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) bool {
diff --git a/pkg/config/agent/config.go b/pkg/config/agent/config.go
index c2cd74338..78775fd23 100644
--- a/pkg/config/agent/config.go
+++ b/pkg/config/agent/config.go
@@ -102,14 +102,21 @@ type AgentConfig struct {
 	TrafficEncryptionMode string `yaml:"trafficEncryptionMode,omitempty"`
 	// WireGuard related configurations.
 	WireGuard WireGuardConfig `yaml:"wireGuard"`
+	// Enable bridging mode of Pod network on Nodes, in which the Node's transport interface is connected
+	// to the OVS bridge, and cross-Node/VLAN traffic from AntreaIPAM Pods (Pods whose IP addresses are
+	// allocated by AntreaIPAM from IPPools) is sent to the underlay network via the uplink, and
+	// forwarded/routed by the underlay network.
+	// This option requires the `AntreaIPAM` feature gate to be enabled. At this moment, it supports only
+	// IPv4 and Linux Nodes, and can be enabled only when `ovsDatapathType` is `system`,
+	// `trafficEncapMode` is `noEncap`, and `noSNAT` is true.
+	EnableBridgingMode bool `yaml:"enableBridgingMode,omitempty"`
 	// APIPort is the port for the antrea-agent APIServer to serve on.
 	// Defaults to 10350.
 	APIPort int `yaml:"apiPort,omitempty"`
-
-	// ClusterMembershipPort is the server port used by the antrea-agent to run a gossip-based cluster membership protocol. Currently it's used only when the Egress feature is enabled.
+	// ClusterMembershipPort is the server port used by the antrea-agent to run a gossip-based cluster
+	// membership protocol. Currently it's used only when the Egress feature is enabled.
 	// Defaults to 10351.
 	ClusterMembershipPort int `yaml:"clusterPort,omitempty"`
-
 	// Enable metrics exposure via Prometheus. Initializes Prometheus metrics listener
 	// Defaults to true.
 	EnablePrometheusMetrics *bool `yaml:"enablePrometheusMetrics,omitempty"`
diff --git a/pkg/controller/networkpolicy/validate.go b/pkg/controller/networkpolicy/validate.go
index bdab883d2..2188bf7d8 100644
--- a/pkg/controller/networkpolicy/validate.go
+++ b/pkg/controller/networkpolicy/validate.go
@@ -27,6 +27,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apimachinery/pkg/util/validation"
 	"k8s.io/apiserver/pkg/authentication/serviceaccount"
 	"k8s.io/klog/v2"
 
@@ -472,6 +473,9 @@ func (v *antreaPolicyValidator) validateAppliedTo(ingress, egress []crdv1alpha1.
 			if eachAppliedTo.ServiceAccount != nil && appliedToFieldsNum > 1 {
 				return "serviceAccount cannot be set with other peers in appliedTo", false
 			}
+			if reason, allowed := checkSelectorsLabels(eachAppliedTo.PodSelector, eachAppliedTo.NamespaceSelector, eachAppliedTo.ExternalEntitySelector); !allowed {
+				return reason, allowed
+			}
 		}
 		return "", true
 	}
@@ -511,6 +515,9 @@ func (v *antreaPolicyValidator) validatePeers(ingress, egress []crdv1alpha1.Rule
 			if peer.ServiceAccount != nil && peerFieldsNum > 1 {
 				return "serviceAccount cannot be set with other peers in rules", false
 			}
+			if reason, allowed := checkSelectorsLabels(peer.PodSelector, peer.NamespaceSelector, peer.ExternalEntitySelector); !allowed {
+				return reason, allowed
+			}
 		}
 		return "", true
 	}
@@ -549,6 +556,31 @@ func numFieldsSetInPeer(peer crdv1alpha1.NetworkPolicyPeer) int {
 	return num
 }
 
+// checkSelectorsLabels validates labels used in all selectors passed in.
+func checkSelectorsLabels(selectors ...*metav1.LabelSelector) (string, bool) {
+	validateLabels := func(labels map[string]string) (string, bool) {
+		for k, v := range labels {
+			err := validation.IsQualifiedName(k)
+			if err != nil {
+				return fmt.Sprintf("Invalid label key: %s: %s", k, strings.Join(err, "; ")), false
+			}
+			err = validation.IsValidLabelValue(v)
+			if err != nil {
+				return fmt.Sprintf("Invalid label value: %s: %s", v, strings.Join(err, "; ")), false
+			}
+		}
+		return "", true
+	}
+	for _, selector := range selectors {
+		if selector != nil {
+			if reason, allowed := validateLabels(selector.MatchLabels); !allowed {
+				return reason, allowed
+			}
+		}
+	}
+	return "", true
+}
+
 // validateTierForPolicy validates whether a referenced Tier exists.
 func (v *antreaPolicyValidator) validateTierForPolicy(tier string) (string, bool) {
 	// "tier" must exist before referencing
@@ -706,6 +738,9 @@ func validateAntreaGroupSpec(s crdv1alpha2.GroupSpec) (string, bool) {
 	}
 	selector, serviceRef, ipBlock, ipBlocks, childGroups := 0, 0, 0, 0, 0
 	if s.NamespaceSelector != nil || s.ExternalEntitySelector != nil || s.PodSelector != nil {
+		if reason, allowed := checkSelectorsLabels(s.PodSelector, s.NamespaceSelector, s.ExternalEntitySelector); !allowed {
+			return reason, allowed
+		}
 		selector = 1
 	}
 	if s.IPBlock != nil {


Test patch:
diff --git a/pkg/agent/openflow/network_policy_test.go b/pkg/agent/openflow/network_policy_test.go
index 4b547fa9f..1e9a5774b 100644
--- a/pkg/agent/openflow/network_policy_test.go
+++ b/pkg/agent/openflow/network_policy_test.go
@@ -159,6 +159,11 @@ func TestInstallPolicyRuleFlows(t *testing.T) {
 	c.networkConfig = &config.NetworkConfig{IPv4Enabled: true}
 	c.ipProtocols = []binding.Protocol{binding.ProtocolIP}
 	defaultAction := crdv1alpha1.RuleActionAllow
+	// Create a policyRuleConjunction for the dns response interception flows
+	// to ensure nil NetworkPolicyReference is handled correctly by GetNetworkPolicyFlowKeys.
+	dnsID := uint32(1)
+	require.NoError(t, c.NewDNSpacketInConjunction(dnsID))
+
 	ruleID1 := uint32(101)
 	rule1 := &types.PolicyRule{
 		Direction: v1beta2.DirectionOut,
diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go
index ab49be308..5ca27c7bc 100644
--- a/pkg/agent/openflow/testing/mock_openflow.go
+++ b/pkg/agent/openflow/testing/mock_openflow.go
@@ -1,4 +1,4 @@
-// Copyright 2021 Antrea Authors
+// 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.
@@ -365,20 +365,6 @@ func (mr *MockClientMockRecorder) InstallGatewayFlows() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallGatewayFlows", reflect.TypeOf((*MockClient)(nil).InstallGatewayFlows))
 }
 
-// InstallLoadBalancerServiceFromOutsideFlows mocks base method
-func (m *MockClient) InstallLoadBalancerServiceFromOutsideFlows(arg0 net.IP, arg1 uint16, arg2 openflow.Protocol) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "InstallLoadBalancerServiceFromOutsideFlows", arg0, arg1, arg2)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// InstallLoadBalancerServiceFromOutsideFlows indicates an expected call of InstallLoadBalancerServiceFromOutsideFlows
-func (mr *MockClientMockRecorder) InstallLoadBalancerServiceFromOutsideFlows(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallLoadBalancerServiceFromOutsideFlows", reflect.TypeOf((*MockClient)(nil).InstallLoadBalancerServiceFromOutsideFlows), arg0, arg1, arg2)
-}
-
 // InstallMulticastFlow mocks base method
 func (m *MockClient) InstallMulticastFlow(arg0 net.IP) error {
 	m.ctrl.T.Helper()
@@ -709,20 +695,6 @@ func (mr *MockClientMockRecorder) UninstallEndpointFlows(arg0, arg1 interface{})
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallEndpointFlows", reflect.TypeOf((*MockClient)(nil).UninstallEndpointFlows), arg0, arg1)
 }
 
-// UninstallLoadBalancerServiceFromOutsideFlows mocks base method
-func (m *MockClient) UninstallLoadBalancerServiceFromOutsideFlows(arg0 net.IP, arg1 uint16, arg2 openflow.Protocol) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UninstallLoadBalancerServiceFromOutsideFlows", arg0, arg1, arg2)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// UninstallLoadBalancerServiceFromOutsideFlows indicates an expected call of UninstallLoadBalancerServiceFromOutsideFlows
-func (mr *MockClientMockRecorder) UninstallLoadBalancerServiceFromOutsideFlows(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallLoadBalancerServiceFromOutsideFlows", reflect.TypeOf((*MockClient)(nil).UninstallLoadBalancerServiceFromOutsideFlows), arg0, arg1, arg2)
-}
-
 // UninstallMulticastFlow mocks base method
 func (m *MockClient) UninstallMulticastFlow(arg0 net.IP) error {
 	m.ctrl.T.Helper()
diff --git a/pkg/agent/proxy/proxier_test.go b/pkg/agent/proxy/proxier_test.go
index fb76682be..f5685e3cb 100644
--- a/pkg/agent/proxy/proxier_test.go
+++ b/pkg/agent/proxy/proxier_test.go
@@ -196,7 +196,7 @@ func testClusterIP(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool, extraSv
 	}))
 	makeEndpointsMap(fp, allEps...)
 
-	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	groupID := fp.groupCounter.AllocateIfNotExist(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
 	bindingProtocol := binding.ProtocolTCP
 	if isIPv6 {
@@ -205,7 +205,6 @@ func testClusterIP(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool, extraSv
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
-	mockOFClient.EXPECT().UninstallServiceGroup(gomock.Any()).Times(1)
 
 	fp.syncProxyRules()
 }
@@ -283,7 +282,7 @@ func testLoadBalancer(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep
 		eps = append(eps, makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, epFunc))
 	}
 	makeEndpointsMap(fp, eps...)
-	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	groupID := fp.groupCounter.AllocateIfNotExist(svcPortName, false)
 	bindingProtocol := binding.ProtocolTCP
 	if isIPv6 {
 		bindingProtocol = binding.ProtocolTCPv6
@@ -292,18 +291,13 @@ func testLoadBalancer(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 	if nodeLocalExternal {
-		groupID, _ = fp.groupCounter.Get(svcPortName, true)
+		groupID = fp.groupCounter.AllocateIfNotExist(svcPortName, true)
 		mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
-	} else {
-		mockOFClient.EXPECT().UninstallServiceGroup(gomock.Any()).Times(1)
 	}
 	if proxyLoadBalancerIPs {
 		mockOFClient.EXPECT().InstallServiceFlows(groupID, loadBalancerIP, uint16(svcPort), bindingProtocol, uint16(0), nodeLocalExternal, corev1.ServiceTypeLoadBalancer).Times(1)
 	}
 	mockOFClient.EXPECT().InstallServiceFlows(groupID, gomock.Any(), uint16(svcNodePort), bindingProtocol, uint16(0), nodeLocalExternal, corev1.ServiceTypeNodePort).Times(1)
-	if proxyLoadBalancerIPs {
-		mockOFClient.EXPECT().InstallLoadBalancerServiceFromOutsideFlows(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
-	}
 	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 	if proxyLoadBalancerIPs {
 		mockRouteClient.EXPECT().AddLoadBalancer([]string{loadBalancerIP.String()}).Times(1)
@@ -380,7 +374,7 @@ func testNodePort(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep2IP
 	}
 	makeEndpointsMap(fp, eps...)
 
-	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	groupID := fp.groupCounter.AllocateIfNotExist(svcPortName, false)
 	bindingProtocol := binding.ProtocolTCP
 	if isIPv6 {
 		bindingProtocol = binding.ProtocolTCPv6
@@ -389,16 +383,13 @@ func testNodePort(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep2IP
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 	if nodeLocalExternal {
-		groupID, _ = fp.groupCounter.Get(svcPortName, true)
+		groupID = fp.groupCounter.AllocateIfNotExist(svcPortName, true)
 		mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
-	} else {
-		mockOFClient.EXPECT().UninstallServiceGroup(gomock.Any()).Times(1)
 	}
 
 	mockOFClient.EXPECT().InstallServiceFlows(groupID, gomock.Any(), uint16(svcNodePort), bindingProtocol, uint16(0), nodeLocalExternal, corev1.ServiceTypeNodePort).Times(1)
 	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 	mockRouteClient.EXPECT().AddNodePort(gomock.Any(), uint16(svcNodePort), bindingProtocol).Times(1)
-	mockOFClient.EXPECT().InstallLoadBalancerServiceFromOutsideFlows(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
 
 	fp.syncProxyRules()
 }
@@ -569,8 +560,8 @@ func TestDualStackService(t *testing.T) {
 	metaProxier.OnEndpointsUpdate(nil, epv6)
 	metaProxier.OnEndpointsSynced()
 
-	groupIDv4, _ := fpv4.groupCounter.Get(svcPortName, false)
-	groupIDv6, _ := fpv6.groupCounter.Get(svcPortName, false)
+	groupIDv4 := fpv4.groupCounter.AllocateIfNotExist(svcPortName, false)
+	groupIDv6 := fpv6.groupCounter.AllocateIfNotExist(svcPortName, false)
 
 	mockOFClient.EXPECT().InstallServiceGroup(groupIDv4, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(binding.ProtocolTCP, gomock.Any()).Times(1)
@@ -627,14 +618,14 @@ func testClusterIPRemoval(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool)
 	}
 	ep := makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, epFunc)
 	makeEndpointsMap(fp, ep)
-	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	groupID := fp.groupCounter.AllocateIfNotExist(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 	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().UninstallServiceGroup(gomock.Any()).Times(2)
+	mockOFClient.EXPECT().UninstallServiceGroup(gomock.Any()).Times(1)
 	fp.syncProxyRules()
 
 	fp.serviceChanges.OnServiceUpdate(service, nil)
@@ -759,8 +750,8 @@ func testClusterIPRemoveSamePortEndpoint(t *testing.T, svcIP net.IP, epIP net.IP
 	}
 	makeEndpointsMap(fp, ep, epUDP)
 
-	groupID, _ := fp.groupCounter.Get(svcPortName, false)
-	groupIDUDP, _ := fp.groupCounter.Get(svcPortNameUDP, false)
+	groupID := fp.groupCounter.AllocateIfNotExist(svcPortName, false)
+	groupIDUDP := fp.groupCounter.AllocateIfNotExist(svcPortNameUDP, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceGroup(groupIDUDP, false, gomock.Any()).Times(2)
 	mockOFClient.EXPECT().InstallEndpointFlows(protocolTCP, gomock.Any()).Times(1)
@@ -824,7 +815,7 @@ func testClusterIPRemoveEndpoints(t *testing.T, svcIP net.IP, epIP net.IP, isIPv
 		bindingProtocol = binding.ProtocolTCPv6
 	}
 	makeEndpointsMap(fp, ep)
-	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	groupID := fp.groupCounter.AllocateIfNotExist(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(2)
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(2)
 	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
@@ -897,7 +888,7 @@ func testSessionAffinityNoEndpoint(t *testing.T, svcExternalIPs net.IP, svcIP ne
 	if isIPv6 {
 		bindingProtocol = binding.ProtocolTCPv6
 	}
-	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	groupID := fp.groupCounter.AllocateIfNotExist(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, true, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(corev1.DefaultClientIPServiceAffinitySeconds), false, corev1.ServiceTypeClusterIP).Times(1)
@@ -1006,7 +997,7 @@ func testPortChange(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool) {
 	}
 	ep := makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, epFunc)
 	makeEndpointsMap(fp, ep)
-	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	groupID := fp.groupCounter.AllocateIfNotExist(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort1), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP)
@@ -1093,8 +1084,8 @@ func TestServicesWithSameEndpoints(t *testing.T) {
 	ep1 := epMapFactory(svcPortName1, epIP.String())
 	ep2 := epMapFactory(svcPortName2, epIP.String())
 
-	groupID1, _ := fp.groupCounter.Get(svcPortName1, false)
-	groupID2, _ := fp.groupCounter.Get(svcPortName2, false)
+	groupID1 := fp.groupCounter.AllocateIfNotExist(svcPortName1, false)
+	groupID2 := fp.groupCounter.AllocateIfNotExist(svcPortName2, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID1, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID2, false, gomock.Any()).Times(1)
 	bindingProtocol := binding.ProtocolTCP
diff --git a/pkg/controller/networkpolicy/validate_test.go b/pkg/controller/networkpolicy/validate_test.go
index d5f65466f..8297815b0 100644
--- a/pkg/controller/networkpolicy/validate_test.go
+++ b/pkg/controller/networkpolicy/validate_test.go
@@ -23,6 +23,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1"
+	crdv1alpha2 "antrea.io/antrea/pkg/apis/crd/v1alpha2"
 )
 
 func TestValidateAntreaPolicy(t *testing.T) {
@@ -940,6 +941,84 @@ func TestValidateAntreaPolicy(t *testing.T) {
 			},
 			expectedReason: "",
 		},
+		{
+			name: "acnp-invalid-label-key-applied-to",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-invalid-label-key-applied-to",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo=": "bar"},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "Invalid label key: foo=: name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')",
+		},
+		{
+			name: "acnp-invalid-label-value-applied-to",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-invalid-label-value-applied-to",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo": "bar"},
+									},
+								},
+							},
+							AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									NamespaceSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo1": "bar="},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "Invalid label value: bar=: a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')",
+		},
+		{
+			name: "acnp-invalid-label-key-rule",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-invalid-label-key-rule",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo": "bar"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo=": "bar1"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "Invalid label key: foo=: name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')",
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -955,3 +1034,53 @@ func TestValidateAntreaPolicy(t *testing.T) {
 		})
 	}
 }
+
+func TestValidateClusterAntreaGroup(t *testing.T) {
+	tests := []struct {
+		name           string
+		group          *crdv1alpha2.ClusterGroup
+		expectedReason string
+	}{
+		{
+			name: "cg-invalid-label-key",
+			group: &crdv1alpha2.ClusterGroup{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "cg-invalid-label-key",
+				},
+				Spec: crdv1alpha2.GroupSpec{
+					PodSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{"foo=": "bar"},
+					},
+				},
+			},
+			expectedReason: "Invalid label key: foo=: name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')",
+		},
+		{
+			name: "cg-invalid-label-value",
+			group: &crdv1alpha2.ClusterGroup{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "cg-invalid-label-value",
+				},
+				Spec: crdv1alpha2.GroupSpec{
+					PodSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{"foo": "bar="},
+					},
+				},
+			},
+			expectedReason: "Invalid label value: bar=: a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, c := newController()
+			v := NewNetworkPolicyValidator(c.NetworkPolicyController)
+			actualReason, allowed := v.validateAntreaGroup(tt.group, nil, admv1.Create, authenticationv1.UserInfo{})
+			assert.Equal(t, tt.expectedReason, actualReason)
+			if tt.expectedReason == "" {
+				assert.True(t, allowed)
+			} else {
+				assert.False(t, allowed)
+			}
+		})
+	}
+}
diff --git a/test/e2e/flowaggregator_test.go b/test/e2e/flowaggregator_test.go
index f585ccd04..ad2e6c413 100644
--- a/test/e2e/flowaggregator_test.go
+++ b/test/e2e/flowaggregator_test.go
@@ -129,6 +129,10 @@ const (
 	testEgressRuleName             = "test-egress-rule-name"
 	iperfTimeSec                   = 12
 	protocolIdentifierTCP          = 6
+	// Set target bandwidth(bits/sec) of iPerf traffic to a relatively small value
+	// (default unlimited for TCP), to reduce the variances caused by network performance
+	// during 12s, and make the throughput test more stable.
+	iperfBandwidth = "10m"
 )
 
 var (
@@ -611,9 +615,9 @@ func checkAntctlRecord(t *testing.T, record map[string]interface{}, srcIP, dstIP
 func checkRecordsForFlows(t *testing.T, data *TestData, srcIP string, dstIP string, isIPv6 bool, isIntraNode bool, checkService bool, checkK8sNetworkPolicy bool, checkAntreaNetworkPolicy bool, checkBandwidth bool) {
 	var cmdStr string
 	if !isIPv6 {
-		cmdStr = fmt.Sprintf("iperf3 -c %s -t %d", dstIP, iperfTimeSec)
+		cmdStr = fmt.Sprintf("iperf3 -c %s -t %d -b %s", dstIP, iperfTimeSec, iperfBandwidth)
 	} else {
-		cmdStr = fmt.Sprintf("iperf3 -6 -c %s -t %d", dstIP, iperfTimeSec)
+		cmdStr = fmt.Sprintf("iperf3 -6 -c %s -t %d -b %s", dstIP, iperfTimeSec, iperfBandwidth)
 	}
 	stdout, _, err := data.runCommandFromPod(testNamespace, "perftest-a", "perftool", []string{"bash", "-c", cmdStr})
 	require.NoErrorf(t, err, "Error when running iperf3 client: %v", err)
@@ -625,10 +629,8 @@ func checkRecordsForFlows(t *testing.T, data *TestData, srcIP string, dstIP stri
 	var bandwidthInMbps float64
 	if strings.Contains(bwSlice[1], "Mbits") {
 		bandwidthInMbps = bandwidthInFloat
-	} else if strings.Contains(bwSlice[1], "Gbits") {
-		bandwidthInMbps = bandwidthInFloat * float64(1024)
 	} else {
-		t.Fatalf("Unit of the traffic bandwidth reported by iperf should either be Mbits or Gbits, failing the test.")
+		t.Fatalf("Unit of the traffic bandwidth reported by iperf should be Mbits.")
 	}
 
 	collectorOutput, recordSlices := getCollectorOutput(t, srcIP, dstIP, srcPort, checkService, true, isIPv6)
@@ -684,19 +686,21 @@ func checkRecordsForFlows(t *testing.T, data *TestData, srcIP string, dstIP stri
 			if checkBandwidth && !strings.Contains(record, "throughput: 0") {
 				flowStartTime := int64(getUint64FieldFromRecord(t, record, "flowStartSeconds"))
 				exportTime := int64(getUint64FieldFromRecord(t, record, "flowEndSeconds"))
+				flowEndReason := int64(getUint64FieldFromRecord(t, record, "flowEndReason"))
 				var recBandwidth float64
-				// Check bandwidth with the field "throughput" except for the last record,
-				// as its throughput is significantly lower than the average Iperf throughput.
-				if exportTime < flowStartTime+iperfTimeSec {
+				// flowEndReason == 3 means the end of flow detected
+				if exportTime >= flowStartTime+iperfTimeSec || flowEndReason == 3 {
+					// Check average bandwidth on the last record.
+					octetTotalCount := getUint64FieldFromRecord(t, record, "octetTotalCount")
+					recBandwidth = float64(octetTotalCount) * 8 / float64(iperfTimeSec) / 1000000
+				} else {
+					// Check bandwidth with the field "throughput" except for the last record,
+					// as their throughput may be significantly lower than the average Iperf throughput.
 					throughput := getUint64FieldFromRecord(t, record, "throughput")
 					recBandwidth = float64(throughput) / 1000000
-				} else {
-					// Check average bandwidth after receiving all the records.
-					octetTotalCount := getUint64FieldFromRecord(t, record, "octetTotalCount")
-					recBandwidth = float64(octetTotalCount) * 8 / float64(exportTime-flowStartTime) / 1000000
 				}
-				t.Logf("Iperf throughput: %.2f Mbits/s, IPFIX record throughput: %.2f Mbits/s", bandwidthInMbps, recBandwidth)
-				assert.InDeltaf(recBandwidth, bandwidthInMbps, bandwidthInMbps*0.15, "Difference between Iperf bandwidth and IPFIX record bandwidth should be lower than 15%%")
+				t.Logf("Throughput check on record with flowEndSeconds-flowStartSeconds: %v, Iperf throughput: %.2f Mbits/s, IPFIX record throughput: %.2f Mbits/s", exportTime-flowStartTime, bandwidthInMbps, recBandwidth)
+				assert.InDeltaf(recBandwidth, bandwidthInMbps, bandwidthInMbps*0.15, "Difference between Iperf bandwidth and IPFIX record bandwidth should be lower than 15%%, record: %s", record)
 			}
 		}
 	}
@@ -852,7 +856,9 @@ func getUint64FieldFromRecord(t *testing.T, record string, field string) uint64
 func getCollectorOutput(t *testing.T, srcIP, dstIP, srcPort string, isDstService bool, checkAllRecords bool, isIPv6 bool) (string, []string) {
 	var collectorOutput string
 	var recordSlices []string
-	err := wait.PollImmediate(500*time.Millisecond, aggregatorInactiveFlowRecordTimeout, func() (bool, error) {
+	// In the ToExternalFlows test, flow record will arrive 5.5s (exporterActiveFlowExportTimeout+aggregatorActiveFlowRecordTimeout) after executing wget command
+	// We set the timeout to 9s (5.5s plus one more aggregatorActiveFlowRecordTimeout) to make the ToExternalFlows test more stable
+	err := wait.PollImmediate(500*time.Millisecond, exporterActiveFlowExportTimeout+aggregatorActiveFlowRecordTimeout*2, func() (bool, error) {
 		var rc int
 		var err error
 		// `pod-running-timeout` option is added to cover scenarios where ipfix flow-collector has crashed after being deployed
@@ -867,8 +873,10 @@ func getCollectorOutput(t *testing.T, srcIP, dstIP, srcPort string, isDstService
 			for _, record := range recordSlices {
 				flowStartTime := int64(getUint64FieldFromRecord(t, record, "flowStartSeconds"))
 				exportTime := int64(getUint64FieldFromRecord(t, record, "flowEndSeconds"))
+				flowEndReason := int64(getUint64FieldFromRecord(t, record, "flowEndReason"))
 				if strings.Contains(record, src) && strings.Contains(record, dst) && strings.Contains(record, srcPort) {
-					if exportTime >= flowStartTime+iperfTimeSec {
+					// flowEndReason == 3 means the end of flow detected
+					if exportTime >= flowStartTime+iperfTimeSec || flowEndReason == 3 {
 						return true, nil
 					}
 				}
diff --git a/test/e2e/proxy_test.go b/test/e2e/proxy_test.go
index aef107a25..9ae41f8c7 100644
--- a/test/e2e/proxy_test.go
+++ b/test/e2e/proxy_test.go
@@ -116,19 +116,19 @@ func probeClientIPFromNode(node string, baseUrl string) (string, error) {
 }
 
 func probeFromPod(data *TestData, pod string, url string) error {
-	_, _, err := data.runCommandFromPod(testNamespace, pod, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1"})
+	_, _, err := data.runCommandFromPod(testNamespace, pod, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1", "-t", "5"})
 	return err
 }
 
 func probeHostnameFromPod(data *TestData, pod string, baseUrl string) (string, error) {
 	url := fmt.Sprintf("%s/%s", baseUrl, "hostname")
-	hostname, _, err := data.runCommandFromPod(testNamespace, pod, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1"})
+	hostname, _, err := data.runCommandFromPod(testNamespace, pod, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1", "-t", "5"})
 	return hostname, err
 }
 
 func probeClientIPFromPod(data *TestData, pod string, baseUrl string) (string, error) {
 	url := fmt.Sprintf("%s/%s", baseUrl, "clientip")
-	hostPort, _, err := data.runCommandFromPod(testNamespace, pod, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1"})
+	hostPort, _, err := data.runCommandFromPod(testNamespace, pod, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1", "-t", "5"})
 	if err != nil {
 		return "", err
 	}
@@ -564,10 +564,10 @@ func testProxyServiceSessionAffinity(ipFamily *corev1.IPFamily, ingressIPs []str
 	require.NoError(t, data.createBusyboxPodOnNode(busyboxPod, testNamespace, nodeName, false))
 	defer data.deletePodAndWait(defaultTimeout, busyboxPod, testNamespace)
 	require.NoError(t, data.podWaitForRunning(defaultTimeout, busyboxPod, testNamespace))
-	stdout, stderr, err := data.runCommandFromPod(testNamespace, busyboxPod, busyboxContainerName, []string{"wget", "-O", "-", svc.Spec.ClusterIP, "-T", "1"})
+	stdout, stderr, err := data.runCommandFromPod(testNamespace, busyboxPod, busyboxContainerName, []string{"wget", "-O", "-", svc.Spec.ClusterIP, "-T", "1", "-t", "5"})
 	require.NoError(t, err, fmt.Sprintf("ipFamily: %v\nstdout: %s\nstderr: %s\n", *ipFamily, stdout, stderr))
 	for _, ingressIP := range ingressIPs {
-		stdout, stderr, err := data.runCommandFromPod(testNamespace, busyboxPod, busyboxContainerName, []string{"wget", "-O", "-", ingressIP, "-T", "1"})
+		stdout, stderr, err := data.runCommandFromPod(testNamespace, busyboxPod, busyboxContainerName, []string{"wget", "-O", "-", ingressIP, "-T", "1", "-t", "5"})
 		require.NoError(t, err, fmt.Sprintf("ipFamily: %v\nstdout: %s\nstderr: %s\n", *ipFamily, stdout, stderr))
 	}
 
diff --git a/test/e2e/service_test.go b/test/e2e/service_test.go
index 5702d0473..84fc35f00 100644
--- a/test/e2e/service_test.go
+++ b/test/e2e/service_test.go
@@ -147,8 +147,8 @@ func (data *TestData) testNodePort(t *testing.T, isWindows bool, namespace strin
 	// Unlike upstream Kubernetes Conformance, here the client is on a Linux Node (nodeName(0)).
 	// It doesn't need to be the control-plane for e2e test and other Linux workers will work as well. However, in this
 	// e2e framework, nodeName(0)/Control-plane Node is guaranteed to be a Linux one.
-	clientName := "agnhost-client"
-	require.NoError(t, data.createAgnhostPodOnNode(clientName, namespace, nodeName(0), false))
+	clientName := "busybox-client"
+	require.NoError(t, data.createBusyboxPodOnNode(clientName, namespace, nodeName(0), false))
 	defer data.deletePodAndWait(defaultTimeout, clientName, namespace)
 	podIPs, err := data.podWaitForIPs(defaultTimeout, clientName, namespace)
 	require.NoError(t, err)
@@ -156,15 +156,15 @@ func (data *TestData) testNodePort(t *testing.T, isWindows bool, namespace strin
 
 	nodeIP := clusterInfo.nodes[0].ip()
 	nodePort := int(svc.Spec.Ports[0].NodePort)
-	addr := fmt.Sprintf("http://%s:%d", nodeIP, nodePort)
+	url := fmt.Sprintf("http://%s:%d", nodeIP, nodePort)
 
-	cmd := append([]string{"curl", "--connect-timeout", "1", "--retry", "5", "--retry-connrefused"}, addr)
-	stdout, stderr, err := data.runCommandFromPod(namespace, clientName, agnhostContainerName, cmd)
+	cmd := []string{"wget", "-O", "-", url, "-T", "1", "-t", "5"}
+	stdout, stderr, err := data.runCommandFromPod(namespace, clientName, busyboxContainerName, cmd)
 	if err != nil {
 		t.Errorf("Error when running command '%s' from Pod '%s', stdout: %s, stderr: %s, error: %v",
 			strings.Join(cmd, " "), clientName, stdout, stderr, err)
 	} else {
-		t.Logf("curl from Pod '%s' to '%s' succeeded", clientName, addr)
+		t.Logf("wget from Pod '%s' to '%s' succeeded", clientName, url)
 	}
 }
 
diff --git a/test/integration/agent/openflow_test.go b/test/integration/agent/openflow_test.go
index e4ddef735..237aca507 100644
--- a/test/integration/agent/openflow_test.go
+++ b/test/integration/agent/openflow_test.go
@@ -106,7 +106,8 @@ var (
 )
 
 func TestConnectivityFlows(t *testing.T) {
-	// Initialize ovs metrics (Prometheus) to test them
+	// Reset OVS metrics (Prometheus) and reinitialize them to test.
+	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
 	// Hack the OS type if we run the test not on Windows Node.
@@ -123,6 +124,7 @@ func TestConnectivityFlows(t *testing.T) {
 		assert.Nil(t, err, fmt.Sprintf("Error while disconnecting from OVS bridge: %v", err))
 		err = ofTestUtils.DeleteOVSBridge(br)
 		assert.Nil(t, err, fmt.Sprintf("Error while deleting OVS bridge: %v", err))
+		ofClient.CleanOFTableCache()
 	}()
 
 	config := prepareConfiguration()
@@ -156,7 +158,8 @@ func TestConnectivityFlows(t *testing.T) {
 }
 
 func TestAntreaFlexibleIPAMConnectivityFlows(t *testing.T) {
-	// Initialize ovs metrics (Prometheus) to test them
+	// Reset OVS metrics (Prometheus) and reinitialize them to test.
+	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
 	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, true, false, false, true, false)
@@ -167,6 +170,7 @@ func TestAntreaFlexibleIPAMConnectivityFlows(t *testing.T) {
 		assert.Nil(t, err, fmt.Sprintf("Error while disconnecting from OVS bridge: %v", err))
 		err = ofTestUtils.DeleteOVSBridge(br)
 		assert.Nil(t, err, fmt.Sprintf("Error while deleting OVS bridge: %v", err))
+		ofClient.CleanOFTableCache()
 	}()
 
 	config := prepareConfiguration()
@@ -201,6 +205,10 @@ func TestAntreaFlexibleIPAMConnectivityFlows(t *testing.T) {
 }
 
 func TestReplayFlowsConnectivityFlows(t *testing.T) {
+	// Reset OVS metrics (Prometheus) and reinitialize them to test.
+	legacyregistry.Reset()
+	metrics.InitializeOVSMetrics()
+
 	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
@@ -210,6 +218,7 @@ func TestReplayFlowsConnectivityFlows(t *testing.T) {
 		assert.Nil(t, err, fmt.Sprintf("Error while disconnecting from OVS bridge: %v", err))
 		err = ofTestUtils.DeleteOVSBridge(br)
 		assert.Nil(t, err, fmt.Sprintf("Error while deleting OVS bridge: %v", err))
+		ofClient.CleanOFTableCache()
 	}()
 
 	config := prepareConfiguration()
@@ -237,6 +246,10 @@ func TestReplayFlowsConnectivityFlows(t *testing.T) {
 }
 
 func TestReplayFlowsNetworkPolicyFlows(t *testing.T) {
+	// Reset OVS metrics (Prometheus) and reinitialize them to test.
+	legacyregistry.Reset()
+	metrics.InitializeOVSMetrics()
+
 	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
@@ -249,6 +262,7 @@ func TestReplayFlowsNetworkPolicyFlows(t *testing.T) {
 		assert.Nil(t, err, fmt.Sprintf("Error while disconnecting from OVS bridge: %v", err))
 		err = ofTestUtils.DeleteOVSBridge(br)
 		assert.Nil(t, err, fmt.Sprintf("Error while deleting OVS bridge: %v", err))
+		ofClient.CleanOFTableCache()
 	}()
 
 	ruleID := uint32(100)
@@ -420,7 +434,8 @@ func testUninstallPodFlows(t *testing.T, config *testConfig) {
 }
 
 func TestNetworkPolicyFlows(t *testing.T) {
-	// Initialize ovs metrics (Prometheus) to test them
+	// Reset OVS metrics (Prometheus) and reinitialize them to test.
+	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
 	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false, false, false, false)
@@ -435,6 +450,7 @@ func TestNetworkPolicyFlows(t *testing.T) {
 		assert.Nil(t, err, fmt.Sprintf("Error while disconnecting from OVS bridge: %v", err))
 		err = ofTestUtils.DeleteOVSBridge(br)
 		assert.Nil(t, err, fmt.Sprintf("Error while deleting OVS bridge: %v", err))
+		ofClient.CleanOFTableCache()
 	}()
 
 	ruleID := uint32(100)
@@ -530,7 +546,8 @@ func TestNetworkPolicyFlows(t *testing.T) {
 }
 
 func TestIPv6ConnectivityFlows(t *testing.T) {
-	// Initialize ovs metrics (Prometheus) to test them
+	// Reset OVS metrics (Prometheus) and reinitialize them to test.
+	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
 	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, true, false, false, false, false)
@@ -542,6 +559,7 @@ func TestIPv6ConnectivityFlows(t *testing.T) {
 		assert.Nil(t, err, fmt.Sprintf("Error while disconnecting from OVS bridge: %v", err))
 		err = ofTestUtils.DeleteOVSBridge(br)
 		assert.Nil(t, err, fmt.Sprintf("Error while deleting OVS bridge: %v", err))
+		ofClient.CleanOFTableCache()
 	}()
 	config := prepareIPv6Configuration()
 	t.Run("testInitialize", func(t *testing.T) {
@@ -575,6 +593,10 @@ type svcConfig struct {
 }
 
 func TestProxyServiceFlows(t *testing.T) {
+	// Reset OVS metrics (Prometheus) and reinitialize them to test.
+	legacyregistry.Reset()
+	metrics.InitializeOVSMetrics()
+
 	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
@@ -587,6 +609,7 @@ func TestProxyServiceFlows(t *testing.T) {
 		assert.Nil(t, err, fmt.Sprintf("Error while disconnecting from OVS bridge: %v", err))
 		err = ofTestUtils.DeleteOVSBridge(br)
 		assert.Nil(t, err, fmt.Sprintf("Error while deleting OVS bridge: %v", err))
+		ofClient.CleanOFTableCache()
 	}()
 
 	endpoints := []k8sproxy.Endpoint{
@@ -1514,6 +1537,10 @@ func prepareSNATFlows(snatIP net.IP, mark, podOFPort, podOFPortRemote uint32, vM
 }
 
 func TestSNATFlows(t *testing.T) {
+	// Reset OVS metrics (Prometheus) and reinitialize them to test.
+	legacyregistry.Reset()
+	metrics.InitializeOVSMetrics()
+
 	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, false, false, true, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
@@ -1527,6 +1554,7 @@ func TestSNATFlows(t *testing.T) {
 		assert.Nil(t, err, fmt.Sprintf("Error while disconnecting from OVS bridge: %v", err))
 		err = ofTestUtils.DeleteOVSBridge(br)
 		assert.Nil(t, err, fmt.Sprintf("Error while deleting OVS bridge: %v", err))
+		ofClient.CleanOFTableCache()
 	}()
 
 	snatIP := net.ParseIP("10.10.10.14")
diff --git a/test/integration/ovs/ofctrl_test.go b/test/integration/ovs/ofctrl_test.go
index 0509eb9f6..7d8a3b417 100644
--- a/test/integration/ovs/ofctrl_test.go
+++ b/test/integration/ovs/ofctrl_test.go
@@ -28,6 +28,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/time/rate"
+	"k8s.io/apimachinery/pkg/util/wait"
 
 	binding "antrea.io/antrea/pkg/ovs/openflow"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
@@ -295,9 +296,12 @@ func TestOFctrlGroup(t *testing.T) {
 			}
 			// Check if the group could be added.
 			require.Nil(t, group.Add())
-			groups, err := OfCtlDumpGroups(ovsCtlClient)
-			require.Nil(t, err)
-			require.Len(t, groups, 1)
+			var groups [][]string
+			require.NoError(t, wait.PollImmediate(openFlowCheckInterval, openFlowCheckTimeout, func() (done bool, err error) {
+				groups, err = OfCtlDumpGroups(ovsCtlClient)
+				require.Nil(t, err)
+				return len(groups) == 1, nil
+			}), "Failed to install group")
 			dumpedGroup := groups[0]
 			for i, bucket := range buckets {
 				// Must have weight
@@ -317,9 +321,11 @@ func TestOFctrlGroup(t *testing.T) {
 			}
 			// Check if the group could be deleted.
 			require.Nil(t, group.Delete())
-			groups, err = OfCtlDumpGroups(ovsCtlClient)
-			require.Nil(t, err)
-			require.Len(t, groups, 0)
+			require.NoError(t, wait.PollImmediate(openFlowCheckInterval, openFlowCheckTimeout, func() (done bool, err error) {
+				groups, err = OfCtlDumpGroups(ovsCtlClient)
+				require.Nil(t, err)
+				return len(groups) == 0, nil
+			}), "Failed to delete group")
 		})
 	}
 }
diff --git a/test/integration/ovs/openflow_test_utils.go b/test/integration/ovs/openflow_test_utils.go
index 50605d30e..16acc5a41 100644
--- a/test/integration/ovs/openflow_test_utils.go
+++ b/test/integration/ovs/openflow_test_utils.go
@@ -19,11 +19,20 @@ import (
 	"os/exec"
 	"strings"
 	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/util/wait"
 
 	binding "antrea.io/antrea/pkg/ovs/openflow"
 	"antrea.io/antrea/pkg/ovs/ovsctl"
 )
 
+const (
+	openFlowCheckTimeout  = 500 * time.Millisecond
+	openFlowCheckInterval = 100 * time.Millisecond
+)
+
 func PrepareOVSBridge(brName string) error {
 	// using the netdev datapath type does not impact test coverage but
 	// ensures that the integration tests can be run with Docker Desktop on
@@ -55,53 +64,67 @@ func (f ExpectFlow) flowStr(name string) string {
 	return fmt.Sprintf("table=%s,%s actions=%s", name, f.MatchStr, f.ActStr)
 }
 
-func CheckFlowExists(t *testing.T, ovsCtlClient ovsctl.OVSCtlClient, tableName string, tableID uint8, exist bool, flows []*ExpectFlow) []string {
+func CheckFlowExists(t *testing.T, ovsCtlClient ovsctl.OVSCtlClient, tableName string, tableID uint8, expectFound bool, flows []*ExpectFlow) []string {
 	var flowList []string
-	if tableName != "" {
-		flowList, _ = OfctlDumpTableFlows(ovsCtlClient, tableName)
-	} else {
-		flowList, _ = OfctlDumpTableFlowsWithoutName(ovsCtlClient, tableID)
-		tableName = fmt.Sprintf("%d", tableID)
-	}
+	var unexpectedFlows []*ExpectFlow
+	if err := wait.PollImmediate(openFlowCheckInterval, openFlowCheckTimeout, func() (done bool, err error) {
+		unexpectedFlows = unexpectedFlows[:0]
+		if tableName != "" {
+			flowList, err = OfctlDumpTableFlows(ovsCtlClient, tableName)
+		} else {
+			flowList, err = OfctlDumpTableFlowsWithoutName(ovsCtlClient, tableID)
+			tableName = fmt.Sprintf("%d", tableID)
+		}
+		require.NoError(t, err, "Error dumping flows")
 
-	for _, flow := range flows {
-		found := OfctlFlowMatch(flowList, tableName, flow)
-		if exist && !found {
-			t.Errorf("Failed to install flow: %s", flow.flowStr(tableName))
+		for _, flow := range flows {
+			found := OfctlFlowMatch(flowList, tableName, flow)
+			if found != expectFound {
+				unexpectedFlows = append(unexpectedFlows, flow)
+			}
 		}
-		if !exist && found {
-			t.Errorf("Failed to uninstall flow: %s", flow.flowStr(tableName))
+		return len(unexpectedFlows) == 0, nil
+	}); err != nil {
+		for _, flow := range unexpectedFlows {
+			if expectFound {
+				t.Errorf("Failed to install flow: %s", flow.flowStr(tableName))
+			} else {
+				t.Errorf("Failed to uninstall flow: %s", flow.flowStr(tableName))
+			}
 		}
-	}
-	if t.Failed() {
-		t.Errorf("Existing flows:\n%s", strings.Join(flowList, "\n"))
+		t.Logf("Existing flows:\n%s", strings.Join(flowList, "\n"))
 	}
 	return flowList
 }
 
-func CheckGroupExists(t *testing.T, ovsCtlClient ovsctl.OVSCtlClient, groupID binding.GroupIDType, groupType string, buckets []string, expectExists bool) {
-	// dump groups
-	groupList, err := OfCtlDumpGroups(ovsCtlClient)
-	if err != nil {
-		t.Errorf("Error dumping flows: Err %v", err)
-	}
+func CheckGroupExists(t *testing.T, ovsCtlClient ovsctl.OVSCtlClient, groupID binding.GroupIDType, groupType string, buckets []string, expectFound bool) {
 	var bucketStrs []string
 	for _, bucket := range buckets {
 		bucketStr := fmt.Sprintf("bucket=%s", bucket)
 		bucketStrs = append(bucketStrs, bucketStr)
 	}
 	groupStr := fmt.Sprintf("group_id=%d,type=%s,%s", groupID, groupType, strings.Join(bucketStrs, ","))
-	found := false
-	for _, groupElems := range groupList {
-		groupEntry := fmt.Sprintf("%s,bucket=", groupElems[0])
-		groupEntry = fmt.Sprintf("%s%s", groupEntry, strings.Join(groupElems[1:], ",bucket="))
-		if strings.Contains(groupEntry, groupStr) {
-			found = true
-			break
+	var groupList [][]string
+	if err := wait.PollImmediate(openFlowCheckInterval, openFlowCheckTimeout, func() (done bool, err error) {
+		groupList, err = OfCtlDumpGroups(ovsCtlClient)
+		require.NoError(t, err, "Error dumping groups")
+		found := false
+		for _, groupElems := range groupList {
+			groupEntry := fmt.Sprintf("%s,bucket=", groupElems[0])
+			groupEntry = fmt.Sprintf("%s%s", groupEntry, strings.Join(groupElems[1:], ",bucket="))
+			if strings.Contains(groupEntry, groupStr) {
+				found = true
+				break
+			}
 		}
-	}
-	if found != expectExists {
-		t.Errorf("Failed to find group:\n%v\nExisting groups:\n%v", groupStr, groupList)
+		return found == expectFound, nil
+	}); err != nil {
+		if expectFound {
+			t.Errorf("Failed to install group: %s", groupStr)
+		} else {
+			t.Errorf("Failed to uninstall group: %s", groupStr)
+		}
+		t.Logf("Existing groups:\n%s", groupList)
 	}
 }