Repository: antrea-io/antrea
Pull request #4250 (2022-09-27 16:30:10)

Make tunnel csum option configurable and default to false
For Linux kernel before Mar 2021, UDP checksum must be present to trigger GRO on the receiver for better performance of Geneve and VXLAN tunnels. The issue has been fixed in Linux kernel [1], thus computing UDP checksum is no longer necessary.

This patch exposes a configuration parameter TunnelCsum which determines whether to compute UDP encapsulation header (Geneve or VXLAN) checksums on outgoing packets and makes it default to false. It should only be set to true when Nodes run an unpatched Linux kernel and poor transfer performance is observed.

[1] https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063

Signed-off-by: Quan Tian <qtian@vmware.com>

I have confirmed the following distributions have included the above patch:

- Ubuntu 20.04.3 LTS (Focal Fossa), kernel: 5.4.0-104.118-generic 5.4.166 (>5.4.106 https://lwn.net/Articles/849644/)
- Photon OS 3.0, kernel: 4.19.198-1.ph3-esx (>4.19.181 https://lwn.net/Articles/849645/)

Making it default to false could avoid UDP checksum issues caused by checksum offload in some virtual and physical network adapters. 

codecov[bot]: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/4250?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) Report
> Merging [#4250](https://codecov.io/gh/antrea-io/antrea/pull/4250?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (ac34c54) into [main](https://codecov.io/gh/antrea-io/antrea/commit/0f7752959bdbee2a9813b0a4e42918316d449080?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (0f77529) will **increase** coverage by `2.01%`.
> The diff coverage is `80.95%`.

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

```diff
@@            Coverage Diff             @@
##             main    #4250      +/-   ##
==========================================
+ Coverage   62.26%   64.27%   +2.01%     
==========================================
  Files         385      361      -24     
  Lines       54501    52319    -2182     
==========================================
- Hits        33933    33627     -306     
+ Misses      18069    16218    -1851     
+ Partials     2499     2474      -25     
```

| Flag | Coverage Δ | | *Carryforward flag |
|---|---|---|---|
| integration-tests | `34.80% <0.00%> (-0.04%)` | :arrow_down: | |
| kind-e2e-tests | `48.16% <52.63%> (ø)` | | Carriedforward from [0f77529](https://codecov.io/gh/antrea-io/antrea/commit/0f7752959bdbee2a9813b0a4e42918316d449080?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) |
| unit-tests | `45.14% <68.42%> (+1.28%)` | :arrow_up: | |

*This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) to find out more.

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/4250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | Coverage Δ | |
|---|---|---|
| [cmd/antrea-agent/agent.go](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-Y21kL2FudHJlYS1hZ2VudC9hZ2VudC5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/agent/config/node\_config.go](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbmZpZy9ub2RlX2NvbmZpZy5nbw==) | `89.65% <ø> (-6.35%)` | :arrow_down: |
| [...gent/controller/noderoute/node\_route\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvbm9kZXJvdXRlL25vZGVfcm91dGVfY29udHJvbGxlci5nbw==) | `57.01% <75.00%> (-2.40%)` | :arrow_down: |
| [pkg/agent/agent.go](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2FnZW50Lmdv) | `52.92% <83.33%> (+1.90%)` | :arrow_up: |
| [pkg/agent/interfacestore/types.go](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2ludGVyZmFjZXN0b3JlL3R5cGVzLmdv) | `77.77% <100.00%> (ø)` | |
| [pkg/features/antrea\_features.go](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2ZlYXR1cmVzL2FudHJlYV9mZWF0dXJlcy5nbw==) | `8.00% <0.00%> (-52.00%)` | :arrow_down: |
| [cmd/antrea-agent/options.go](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-Y21kL2FudHJlYS1hZ2VudC9vcHRpb25zLmdv) | `0.00% <0.00%> (-21.23%)` | :arrow_down: |
| [pkg/apiserver/certificate/certificate.go](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FwaXNlcnZlci9jZXJ0aWZpY2F0ZS9jZXJ0aWZpY2F0ZS5nbw==) | `70.37% <0.00%> (-6.49%)` | :arrow_down: |
| [pkg/util/env/env.go](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL3V0aWwvZW52L2Vudi5nbw==) | `58.73% <0.00%> (-4.77%)` | :arrow_down: |
| ... and [36 more](https://codecov.io/gh/antrea-io/antrea/pull/4250/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | |

tnqn: > > Making it default to false could avoid checksum issues caused by checksum offload in some virtual and physical network adapters.
> 
> Is that only observed when the Node network is itself an overlay? Otherwise, this checksum is the outermost transport checksum, so it's hard to imagine that there would be an issue there.

I remember some adapters didn't calculate UDP checksums correctly when tx-udp_tnl-segmentation and tx-udp_tnl-csum-segmentation were on, but I don't know if it's double encap or not, let me check with people who experienced that issue.
tnqn: > > > Making it default to false could avoid checksum issues caused by checksum offload in some virtual and physical network adapters.
> > 
> > 
> > Is that only observed when the Node network is itself an overlay? Otherwise, this checksum is the outermost transport checksum, so it's hard to imagine that there would be an issue there.
> 
> I remember some adapters didn't calculate UDP checksums correctly when tx-udp_tnl-segmentation and tx-udp_tnl-csum-segmentation were on, but I don't know if it's double encap or not, let me check with people who experienced that issue.

Just found that was bad inner checksums in single encap scenario and you were the one who troubleshooted the issue. So yes, AFAIK, the issue only happened with double encap.
tnqn: Latest change fixed windows unit test
tnqn: /test-all

Patch:
diff --git a/.github/workflows/cancel_workflows.yml b/.github/workflows/cancel_workflows.yml
index 76f0289f2..ce6763840 100644
--- a/.github/workflows/cancel_workflows.yml
+++ b/.github/workflows/cancel_workflows.yml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     if: ${{ github.event.workflow_run.event == 'pull_request' }}
     steps:
-    - uses: styfle/cancel-workflow-action@0.10.0
+    - uses: styfle/cancel-workflow-action@0.10.1
       with:
         all_but_latest: true
         workflow_id: ${{ github.event.workflow.id }}
diff --git a/.golangci.yml b/.golangci.yml
index 311f761a7..1a4d7c681 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -13,13 +13,13 @@ linters-settings:
     ignore-generated-header: false
     severity: warning
     confidence: 0.8
-    # errorf, var-declaration temporarily disabled because of https://github.com/golangci/golangci-lint/issues/2997
     rules:
       - name: unreachable-code
-      # - name: errorf
+      - name: errorf
       - name: range
       - name: superfluous-else
-      # - name: var-declaration
+      - name: var-declaration
+      - name: duplicated-imports
 
 linters:
   disable-all: true
diff --git a/Makefile b/Makefile
index 62bfb1a48..e2b3e21ef 100644
--- a/Makefile
+++ b/Makefile
@@ -261,7 +261,7 @@ fmt:
 
 .golangci-bin:
 	@echo "===> Installing Golangci-lint <==="
-	@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $@ v1.48.0
+	@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $@ v1.50.0
 
 .PHONY: golangci
 golangci: .golangci-bin
diff --git a/build/charts/antrea/README.md b/build/charts/antrea/README.md
index 49b8e60e6..a79fd8924 100644
--- a/build/charts/antrea/README.md
+++ b/build/charts/antrea/README.md
@@ -107,6 +107,7 @@ Kubernetes: `>= 1.16.0-0`
 | trafficEncryptionMode | string | `"none"` | Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.It must be one of "none", "ipsec", "wireGuard". |
 | transportInterface | string | `""` | Name of the interface on Node which is used for tunneling or routing the traffic across Nodes. |
 | transportInterfaceCIDRs | list | `[]` | Network CIDRs of the interface on Node which is used for tunneling or routing the traffic across Nodes. |
+| tunnelCsum | bool | `false` | TunnelCsum determines whether to compute UDP encapsulation header (Geneve or VXLAN) checksums on outgoing packets. For Linux kernel before Mar 2021, UDP checksum must be present to trigger GRO on the receiver for better performance of Geneve and VXLAN tunnels. The issue has been fixed by https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063, thus computing UDP checksum is no longer necessary. It should only be set to true when you are using an unpatched Linux kernel and observing poor transfer performance. |
 | tunnelPort | int | `0` | TunnelPort is the destination port for UDP and TCP based tunnel protocols (Geneve, VXLAN, and STT). If zero, it will use the assigned IANA port for the protocol, i.e. 6081 for Geneve, 4789 for VXLAN, and 7471 for STT. |
 | tunnelType | string | `"geneve"` | Tunnel protocol used for encapsulating traffic across Nodes. It must be one of "geneve", "vxlan", "gre", "stt". |
 | webhooks.labelsMutator.enable | bool | `false` |  |
diff --git a/build/charts/antrea/conf/antrea-agent.conf b/build/charts/antrea/conf/antrea-agent.conf
index 866d71a93..6aae8fd14 100644
--- a/build/charts/antrea/conf/antrea-agent.conf
+++ b/build/charts/antrea/conf/antrea-agent.conf
@@ -112,6 +112,14 @@ tunnelType: {{ .Values.tunnelType | quote }}
 # and 7471 for STT.
 tunnelPort: {{ .Values.tunnelPort }}
 
+# TunnelCsum determines whether to compute UDP encapsulation header (Geneve or VXLAN) checksums on outgoing
+# packets. For Linux kernel before Mar 2021, UDP checksum must be present to trigger GRO on the receiver for better
+# performance of Geneve and VXLAN tunnels. The issue has been fixed by
+# https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063, thus computing UDP checksum is
+# no longer necessary.
+# It should only be set to true when you are using an unpatched Linux kernel and observing poor transfer performance.
+tunnelCsum: {{ .Values.tunnelCsum }}
+
 # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
 # It has the following options:
 # - none (default):  Inter-node Pod traffic will not be encrypted.
diff --git a/build/charts/antrea/values.yaml b/build/charts/antrea/values.yaml
index 127ed880b..a328b546b 100644
--- a/build/charts/antrea/values.yaml
+++ b/build/charts/antrea/values.yaml
@@ -14,6 +14,15 @@ tunnelType: "geneve"
 # (Geneve, VXLAN, and STT). If zero, it will use the assigned IANA port for the
 # protocol, i.e. 6081 for Geneve, 4789 for VXLAN, and 7471 for STT.
 tunnelPort: 0
+# -- TunnelCsum determines whether to compute UDP encapsulation header (Geneve
+# or VXLAN) checksums on outgoing packets. For Linux kernel before Mar 2021, UDP
+# checksum must be present to trigger GRO on the receiver for better performance
+# of Geneve and VXLAN tunnels. The issue has been fixed by
+# https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063,
+# thus computing UDP checksum is no longer necessary.
+# It should only be set to true when you are using an unpatched Linux kernel and
+# observing poor transfer performance.
+tunnelCsum: false
 # -- Determines how tunnel traffic is encrypted. Currently encryption only works
 # with encap mode.It must be one of "none", "ipsec", "wireGuard".
 trafficEncryptionMode: "none"
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 8e6d9def0..bb4a22bd8 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -2952,6 +2952,14 @@ data:
     # and 7471 for STT.
     tunnelPort: 0
 
+    # TunnelCsum determines whether to compute UDP encapsulation header (Geneve or VXLAN) checksums on outgoing
+    # packets. For Linux kernel before Mar 2021, UDP checksum must be present to trigger GRO on the receiver for better
+    # performance of Geneve and VXLAN tunnels. The issue has been fixed by
+    # https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063, thus computing UDP checksum is
+    # no longer necessary.
+    # It should only be set to true when you are using an unpatched Linux kernel and observing poor transfer performance.
+    tunnelCsum: false
+
     # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
     # It has the following options:
     # - none (default):  Inter-node Pod traffic will not be encrypted.
@@ -4140,7 +4148,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 030faeb129b70e3abd5720d2262d122abea2428e342f02836846a3af26f20999
+        checksum/config: ec59529f4b774af0f12a30c31eb48e5494ac6371ce624e872d1e6b76b797325e
       labels:
         app: antrea
         component: antrea-agent
@@ -4381,7 +4389,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 030faeb129b70e3abd5720d2262d122abea2428e342f02836846a3af26f20999
+        checksum/config: ec59529f4b774af0f12a30c31eb48e5494ac6371ce624e872d1e6b76b797325e
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index 57b18ce5c..16a52ce78 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -2952,6 +2952,14 @@ data:
     # and 7471 for STT.
     tunnelPort: 0
 
+    # TunnelCsum determines whether to compute UDP encapsulation header (Geneve or VXLAN) checksums on outgoing
+    # packets. For Linux kernel before Mar 2021, UDP checksum must be present to trigger GRO on the receiver for better
+    # performance of Geneve and VXLAN tunnels. The issue has been fixed by
+    # https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063, thus computing UDP checksum is
+    # no longer necessary.
+    # It should only be set to true when you are using an unpatched Linux kernel and observing poor transfer performance.
+    tunnelCsum: false
+
     # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
     # It has the following options:
     # - none (default):  Inter-node Pod traffic will not be encrypted.
@@ -4140,7 +4148,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 030faeb129b70e3abd5720d2262d122abea2428e342f02836846a3af26f20999
+        checksum/config: ec59529f4b774af0f12a30c31eb48e5494ac6371ce624e872d1e6b76b797325e
       labels:
         app: antrea
         component: antrea-agent
@@ -4383,7 +4391,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 030faeb129b70e3abd5720d2262d122abea2428e342f02836846a3af26f20999
+        checksum/config: ec59529f4b774af0f12a30c31eb48e5494ac6371ce624e872d1e6b76b797325e
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index b6e5a4b42..f44a4eca9 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -2952,6 +2952,14 @@ data:
     # and 7471 for STT.
     tunnelPort: 0
 
+    # TunnelCsum determines whether to compute UDP encapsulation header (Geneve or VXLAN) checksums on outgoing
+    # packets. For Linux kernel before Mar 2021, UDP checksum must be present to trigger GRO on the receiver for better
+    # performance of Geneve and VXLAN tunnels. The issue has been fixed by
+    # https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063, thus computing UDP checksum is
+    # no longer necessary.
+    # It should only be set to true when you are using an unpatched Linux kernel and observing poor transfer performance.
+    tunnelCsum: false
+
     # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
     # It has the following options:
     # - none (default):  Inter-node Pod traffic will not be encrypted.
@@ -4140,7 +4148,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 9f057043633784b60338fda16b8a439e85a763cbabdf377c1318c56a0e0abc26
+        checksum/config: ae585cead46246e4d15fa336e081caaa51d432909dfd21a22720a1c37b2e37e9
       labels:
         app: antrea
         component: antrea-agent
@@ -4380,7 +4388,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 9f057043633784b60338fda16b8a439e85a763cbabdf377c1318c56a0e0abc26
+        checksum/config: ae585cead46246e4d15fa336e081caaa51d432909dfd21a22720a1c37b2e37e9
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index 8566bc608..1cf044549 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -2965,6 +2965,14 @@ data:
     # and 7471 for STT.
     tunnelPort: 0
 
+    # TunnelCsum determines whether to compute UDP encapsulation header (Geneve or VXLAN) checksums on outgoing
+    # packets. For Linux kernel before Mar 2021, UDP checksum must be present to trigger GRO on the receiver for better
+    # performance of Geneve and VXLAN tunnels. The issue has been fixed by
+    # https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063, thus computing UDP checksum is
+    # no longer necessary.
+    # It should only be set to true when you are using an unpatched Linux kernel and observing poor transfer performance.
+    tunnelCsum: false
+
     # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
     # It has the following options:
     # - none (default):  Inter-node Pod traffic will not be encrypted.
@@ -4153,7 +4161,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 0d23a6a2c5f2f815ef77b338ae72534ce3715e5f0a84de4c2910ddf2fd013f5b
+        checksum/config: 8dc962ec6575509a540500f9cc1bd399118256fc0485c02705b129922b981c4e
         checksum/ipsec-secret: d0eb9c52d0cd4311b6d252a951126bf9bea27ec05590bed8a394f0f792dcb2a4
       labels:
         app: antrea
@@ -4439,7 +4447,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 0d23a6a2c5f2f815ef77b338ae72534ce3715e5f0a84de4c2910ddf2fd013f5b
+        checksum/config: 8dc962ec6575509a540500f9cc1bd399118256fc0485c02705b129922b981c4e
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index 86e45a883..a3c85aa82 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -2952,6 +2952,14 @@ data:
     # and 7471 for STT.
     tunnelPort: 0
 
+    # TunnelCsum determines whether to compute UDP encapsulation header (Geneve or VXLAN) checksums on outgoing
+    # packets. For Linux kernel before Mar 2021, UDP checksum must be present to trigger GRO on the receiver for better
+    # performance of Geneve and VXLAN tunnels. The issue has been fixed by
+    # https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063, thus computing UDP checksum is
+    # no longer necessary.
+    # It should only be set to true when you are using an unpatched Linux kernel and observing poor transfer performance.
+    tunnelCsum: false
+
     # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
     # It has the following options:
     # - none (default):  Inter-node Pod traffic will not be encrypted.
@@ -4140,7 +4148,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: cda023525442c38c869663e4fd703a76cafa307220ae85ec0a8bd393587fa3ed
+        checksum/config: b609bc3b220de28b3fe935093fc7d99dfdc533916597e906a6c627c6233fedbc
       labels:
         app: antrea
         component: antrea-agent
@@ -4380,7 +4388,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: cda023525442c38c869663e4fd703a76cafa307220ae85ec0a8bd393587fa3ed
+        checksum/config: b609bc3b220de28b3fe935093fc7d99dfdc533916597e906a6c627c6233fedbc
       labels:
         app: antrea
         component: antrea-controller
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index aec095bb9..003d41177 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -170,6 +170,7 @@ func run(o *Options) error {
 	networkConfig := &config.NetworkConfig{
 		TunnelType:            ovsconfig.TunnelType(o.config.TunnelType),
 		TunnelPort:            o.config.TunnelPort,
+		TunnelCsum:            o.config.TunnelCsum,
 		TrafficEncapMode:      encapMode,
 		TrafficEncryptionMode: encryptionMode,
 		TransportIface:        o.config.TransportInterface,
diff --git a/docs/antrea-network-policy.md b/docs/antrea-network-policy.md
index e6d2f89f6..5fbe7e3d7 100644
--- a/docs/antrea-network-policy.md
+++ b/docs/antrea-network-policy.md
@@ -636,7 +636,7 @@ to select Pods. More details can be found in the [ServiceAccountSelector](#servi
 **Note**: The order in which the egress rules are specified matters, i.e., rules will
 be enforced in the order in which they are written.
 
-**enableLogging**: A ClusterNetworkPolicy ingress or egress rule can be
+**enableLogging**: Antrea-native policy ingress or egress rules can be
 audited by enabling its logging field. When `enableLogging` field is set to
 true, the first packet of any connection that matches this rule will be logged
 to a separate file (`/var/log/antrea/networkpolicy/np.log`) on the Node on
@@ -645,16 +645,19 @@ analysis. By default, rules are not logged. The example policy logs all
 traffic that matches the "DropToThirdParty" egress rule, while the rule
 "AllowFromFrontend" is not logged. Specifically for drop and reject rules,
 deduplication is applied to reduce duplicated logs, and duplication buffer
-length is set to 1 second. The rules are logged in the following format:
+length is set to 1 second. If a rule name is not provided, an identifiable
+name will be generated for the rule and displayed in the log.
+The rules are logged in the following format:
 
 ```text
-    <yyyy/mm/dd> <time> <ovs-table-name> <antrea-native-policy-reference> <action> <openflow-priority> <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length>
+    <yyyy/mm/dd> <time> <ovs-table-name> <antrea-native-policy-reference> <rule-name> <action> <openflow-priority> <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length>
     Deduplication:
-    <yyyy/mm/dd> <time> <ovs-table-name> <antrea-native-policy-reference> <action> <openflow-priority> <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length> [<num of packets> packets in <duplicate duration>]
+    <yyyy/mm/dd> <time> <ovs-table-name> <antrea-native-policy-reference> <rule-name> <action> <openflow-priority> <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length> [<num of packets> packets in <duplicate duration>]
 
     Examples:
-    2020/11/02 22:21:21.148395 AntreaPolicyAppTierIngressRule AntreaNetworkPolicy:default/test-anp Allow 61800 10.10.1.65 35402 10.0.0.5 80 TCP 60
-    2021/06/24 23:56:41.346165 AntreaPolicyEgressRule AntreaNetworkPolicy:default/test-anp Drop 44900 10.10.1.65 35402 10.0.0.5 80 TCP 60 [3 packets in 1.011379442s]
+    2020/11/02 22:21:21.148395 AntreaPolicyAppTierIngressRule AntreaNetworkPolicy:default/test-anp test-rule Allow 61800 10.10.1.65 35402 10.0.0.5 80 TCP 60
+    2021/06/24 23:56:41.346165 AntreaPolicyEgressRule AntreaNetworkPolicy:default/test-anp test-rule Drop 44900 10.10.1.65 35402 10.0.0.5 80 TCP 60 [3 packets in 1.011379442s]
+    2022/09/20 02:21:25.879364 AntreaPolicyIngressRule AntreaNetworkPolicy:default/test-anp ingress-drop-1cffec1 Drop 44900 10.10.1.14 <nil> 10.10.1.15 <nil> ICMP 84
 ```
 
 Kubernetes NetworkPolicies can also be audited using Antrea logging to the same file
@@ -663,17 +666,18 @@ Kubernetes NetworkPolicies can also be audited using Antrea logging to the same
 for all NetworkPolicies in the Namespace. Packets of any connection that match
 a NetworkPolicy rule will be logged with a reference to the NetworkPolicy name,
 but packets dropped by the implicit "default drop" (not allowed by any NetworkPolicy)
-will only be logged with consistent name `K8sNetworkPolicy` for reference.
-The rules are logged in the following format:
+will only be logged with consistent name `K8sNetworkPolicy` for reference. When
+using Antrea logging for Kubernetes NetworkPolicies, the rule name field is not
+set and defaults to `<nil>` value. The rules are logged in the following format:
 
 ```text
-    <yyyy/mm/dd> <time> <ovs-table-name> <k8s-network-policy-reference> Allow <openflow-priority> <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length>
+    <yyyy/mm/dd> <time> <ovs-table-name> <k8s-network-policy-reference> <nil> Allow <openflow-priority> <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length>
     Default dropped traffic:
-    <yyyy/mm/dd> <time> <ovs-table-name> K8sNetworkPolicy Drop -1 <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length> [<num of packets> packets in <duplicate duration>]
+    <yyyy/mm/dd> <time> <ovs-table-name> K8sNetworkPolicy <nil> Drop <nil> <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length> [<num of packets> packets in <duplicate duration>]
 
     Examples:
-    2022/07/26 06:55:56.170456 IngressRule K8sNetworkPolicy:default/test-np-log Allow 190 10.10.1.82 49518 10.10.1.84 80 TCP 60
-    2022/07/26 06:55:57.142206 IngressDefaultRule K8sNetworkPolicy Drop -1 10.10.1.83 38608 10.10.1.84 80 TCP 60
+    2022/07/26 06:55:56.170456 IngressRule K8sNetworkPolicy:default/test-np-log <nil> Allow 190 10.10.1.82 49518 10.10.1.84 80 TCP 60
+    2022/07/26 06:55:57.142206 IngressDefaultRule K8sNetworkPolicy <nil> Drop <nil> 10.10.1.83 38608 10.10.1.84 80 TCP 60
 ```
 
 Fluentd can be used to assist with collecting and analyzing the logs. Refer to the
diff --git a/go.mod b/go.mod
index d4b21df35..dad076898 100644
--- a/go.mod
+++ b/go.mod
@@ -30,7 +30,7 @@ require (
 	github.com/golang/protobuf v1.5.2
 	github.com/google/btree v1.1.2
 	github.com/google/uuid v1.3.0
-	github.com/hashicorp/memberlist v0.4.0
+	github.com/hashicorp/memberlist v0.5.0
 	github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.1.1
 	github.com/k8snetworkplumbingwg/sriov-cni v2.1.0+incompatible
 	github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd
@@ -42,7 +42,7 @@ require (
 	github.com/miekg/dns v1.1.50
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
 	github.com/onsi/ginkgo v1.16.5
-	github.com/onsi/gomega v1.20.2
+	github.com/onsi/gomega v1.21.1
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.13.0
 	github.com/prometheus/common v0.37.0
@@ -61,8 +61,9 @@ require (
 	golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
 	golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
+	golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5
-	google.golang.org/grpc v1.49.0
+	google.golang.org/grpc v1.50.0
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 	gopkg.in/yaml.v2 v2.4.0
@@ -197,7 +198,6 @@ require (
 	golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
 	golang.org/x/text v0.3.7 // indirect
-	golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
diff --git a/go.sum b/go.sum
index 19e73afae..664e44d64 100644
--- a/go.sum
+++ b/go.sum
@@ -559,8 +559,8 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/memberlist v0.4.0 h1:k3uda5gZcltmafuFF+UFqNEl5PrH+yPZ4zkjp1f/H/8=
-github.com/hashicorp/memberlist v0.4.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
+github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
+github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -748,8 +748,8 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.20.2 h1:8uQq0zMgLEfa0vRrrBgaJF2gyW9Da9BmfGV+OyUzfkY=
-github.com/onsi/gomega v1.20.2/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
+github.com/onsi/gomega v1.21.1 h1:OB/euWYIExnPBohllTicTHmGTrMaqJ67nIu80j0/uEM=
+github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
 github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
@@ -1486,8 +1486,8 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
 google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
-google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
+google.golang.org/grpc v1.50.0 h1:fPVVDxY9w++VjTZsYvXWqEf9Rqar/e+9zYfxKK+W+YU=
+google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/multicluster/apis/multicluster/v1alpha1/gateway_types.go b/multicluster/apis/multicluster/v1alpha1/gateway_types.go
index ab2fd2431..c1c0ec10d 100644
--- a/multicluster/apis/multicluster/v1alpha1/gateway_types.go
+++ b/multicluster/apis/multicluster/v1alpha1/gateway_types.go
@@ -50,6 +50,8 @@ type ClusterInfo struct {
 	ServiceCIDR string `json:"serviceCIDR,omitempty"`
 	// GatewayInfos has information of Gateways
 	GatewayInfos []GatewayInfo `json:"gatewayInfos,omitempty"`
+	// PodCIDRs is the Pod IP address CIDRs.
+	PodCIDRs []string `json:"podCIDRs,omitempty"`
 }
 
 //+kubebuilder:object:root=true
diff --git a/multicluster/apis/multicluster/v1alpha1/labelidentity_types.go b/multicluster/apis/multicluster/v1alpha1/labelidentity_types.go
new file mode 100644
index 000000000..cbee7371f
--- /dev/null
+++ b/multicluster/apis/multicluster/v1alpha1/labelidentity_types.go
@@ -0,0 +1,61 @@
+/*
+Copyright 2022 Antrea Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+// +genclient
+// +genclient:nonNamespaced
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:path=labelidentities,scope=Cluster
+
+// +kubebuilder:printcolumn:name="Label",type=string,JSONPath=`.spec.label`,description="Normalized string of a label identity"
+// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.spec.id`,description="ID allocated for the label identity by the leader cluster"
+// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=`.metadata.creationTimestamp`
+// LabelIdentity is an imported label identity from the ClusterSet.
+// For each unique label identity, a LabelIdentity will be created in the member cluster.
+type LabelIdentity struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec LabelIdentitySpec `json:"spec,omitempty"`
+}
+
+type LabelIdentitySpec struct {
+	// Label is the normalized string of a label identity.
+	// The format of normalized label identity is `ns:(?P<nslabels>(.)*)&pod:(?P<podlabels>(.)*)`
+	// E.g., `ns:kubernetes.io/metadata.name=kube-system&pod:app=db`
+	Label string `json:"label,omitempty"`
+	// ID is the ID allocated for the label identity by the leader cluster.
+	ID uint32 `json:"id,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// LabelIdentityList contains a list of LabelIdentity.
+type LabelIdentityList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []LabelIdentity `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(
+		&LabelIdentity{},
+		&LabelIdentityList{},
+	)
+}
diff --git a/multicluster/apis/multicluster/v1alpha1/multiclusterconfig_types.go b/multicluster/apis/multicluster/v1alpha1/multiclusterconfig_types.go
index 1a06080ed..7d0d38204 100644
--- a/multicluster/apis/multicluster/v1alpha1/multiclusterconfig_types.go
+++ b/multicluster/apis/multicluster/v1alpha1/multiclusterconfig_types.go
@@ -41,6 +41,8 @@ type MultiClusterConfig struct {
 	config.ControllerManagerConfigurationSpec `json:",inline"`
 	// ServiceCIDR allows user to set the ClusterIP range of the cluster manually.
 	ServiceCIDR string `json:"serviceCIDR,omitempty"`
+	// PodCIDRs is the Pod IP address CIDRs.
+	PodCIDRs []string `json:"podCIDRs,omitempty"`
 	// The precedence about which IP address (internal or external IP) of Node is preferred to
 	// be used as the cross-cluster tunnel endpoint. if not specified, internal IP will be chosen.
 	GatewayIPPrecedence Precedence `json:"gatewayIPPrecedence,omitempty"`
diff --git a/multicluster/apis/multicluster/v1alpha1/resourceexport_types.go b/multicluster/apis/multicluster/v1alpha1/resourceexport_types.go
index eb3e7f848..1316574cb 100644
--- a/multicluster/apis/multicluster/v1alpha1/resourceexport_types.go
+++ b/multicluster/apis/multicluster/v1alpha1/resourceexport_types.go
@@ -39,6 +39,10 @@ type ExternalEntityExport struct {
 	ExternalEntitySpec v1alpha2.ExternalEntitySpec `json:"externalEntitySpec,omitempty"`
 }
 
+type LabelIdentityExport struct {
+	NormalizedLabel string `json:"normalizedLabel,omitempty"`
+}
+
 // RawResourceExport exports opaque resources.
 type RawResourceExport struct {
 	Data []byte `json:"data,omitempty"`
@@ -65,6 +69,8 @@ type ResourceExportSpec struct {
 	ExternalEntity *ExternalEntityExport `json:"externalEntity,omitempty"`
 	// If exported resource is AntreaClusterNetworkPolicy.
 	ClusterNetworkPolicy *v1alpha1.ClusterNetworkPolicySpec `json:"clusterNetworkPolicy,omitempty"`
+	// If exported resource is LabelIdentity of a cluster.
+	LabelIdentity *LabelIdentityExport `json:"labelIdentity,omitempty"`
 	// If exported resource kind is unknown.
 	Raw *RawResourceExport `json:"raw,omitempty"`
 }
@@ -102,8 +108,8 @@ type ResourceExportStatus struct {
 }
 
 // +genclient
-//+kubebuilder:object:root=true
-//+kubebuilder:subresource:status
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
 
 // +kubebuilder:printcolumn:name="Cluster ID",type=string,JSONPath=`.spec.clusterID`,description="Cluster ID of the exporting cluster"
 // +kubebuilder:printcolumn:name="Kind",type=string,JSONPath=`.spec.kind`,description="Kind of the exported resource"
@@ -119,7 +125,7 @@ type ResourceExport struct {
 	Status ResourceExportStatus `json:"status,omitempty"`
 }
 
-//+kubebuilder:object:root=true
+// +kubebuilder:object:root=true
 
 // ResourceExportList contains a list of ResourceExport.
 type ResourceExportList struct {
diff --git a/multicluster/apis/multicluster/v1alpha1/resourceimport_types.go b/multicluster/apis/multicluster/v1alpha1/resourceimport_types.go
index 0d2ca07f4..5e1d81d60 100644
--- a/multicluster/apis/multicluster/v1alpha1/resourceimport_types.go
+++ b/multicluster/apis/multicluster/v1alpha1/resourceimport_types.go
@@ -62,10 +62,8 @@ type ResourceImportSpec struct {
 	ExternalEntity *ExternalEntityImport `json:"externalentity,omitempty"`
 	// If imported resource is AntreaClusterNetworkPolicy.
 	ClusterNetworkPolicy *v1alpha1.ClusterNetworkPolicySpec `json:"clusternetworkpolicy,omitempty"`
-	// If imported resource is ANP.
-	// TODO:
-	// ANP uses float64 as priority.  Type float64 is discouraged by k8s, and is not supported by controller-gen tools.
-	// NetworkPolicy *v1alpha1.NetworkPolicySpec `json:"networkpolicy,omitempty"`
+	// If imported resource kind is LabelIdentity.
+	LabelIdentity *LabelIdentitySpec `json:"labelIdentity,omitempty"`
 	// If imported resource kind is unknown.
 	Raw *RawResourceImport `json:"raw,omitempty"`
 }
@@ -105,8 +103,8 @@ type ResourceImportStatus struct {
 }
 
 // +genclient
-//+kubebuilder:object:root=true
-//+kubebuilder:subresource:status
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
 
 // +kubebuilder:printcolumn:name="Kind",type=string,JSONPath=`.spec.kind`,description="Kind of the imported resource"
 // +kubebuilder:printcolumn:name="Namespace",type=string,JSONPath=`.spec.namespace`,description="Namespace of the imported resource"
@@ -121,7 +119,7 @@ type ResourceImport struct {
 	Status ResourceImportStatus `json:"status,omitempty"`
 }
 
-//+kubebuilder:object:root=true
+// +kubebuilder:object:root=true
 
 // ResourceImportList contains a list of ResourceImport.
 type ResourceImportList struct {
@@ -131,5 +129,8 @@ type ResourceImportList struct {
 }
 
 func init() {
-	SchemeBuilder.Register(&ResourceImport{}, &ResourceImportList{})
+	SchemeBuilder.Register(
+		&ResourceImport{},
+		&ResourceImportList{},
+	)
 }
diff --git a/multicluster/apis/multicluster/v1alpha1/zz_generated.deepcopy.go b/multicluster/apis/multicluster/v1alpha1/zz_generated.deepcopy.go
index 51ab9ca0f..ac9bddd93 100644
--- a/multicluster/apis/multicluster/v1alpha1/zz_generated.deepcopy.go
+++ b/multicluster/apis/multicluster/v1alpha1/zz_generated.deepcopy.go
@@ -447,6 +447,94 @@ func (in *GatewayList) DeepCopyObject() runtime.Object {
 	return nil
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LabelIdentity) DeepCopyInto(out *LabelIdentity) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	out.Spec = in.Spec
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LabelIdentity.
+func (in *LabelIdentity) DeepCopy() *LabelIdentity {
+	if in == nil {
+		return nil
+	}
+	out := new(LabelIdentity)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *LabelIdentity) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LabelIdentityExport) DeepCopyInto(out *LabelIdentityExport) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LabelIdentityExport.
+func (in *LabelIdentityExport) DeepCopy() *LabelIdentityExport {
+	if in == nil {
+		return nil
+	}
+	out := new(LabelIdentityExport)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LabelIdentityList) DeepCopyInto(out *LabelIdentityList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]LabelIdentity, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LabelIdentityList.
+func (in *LabelIdentityList) DeepCopy() *LabelIdentityList {
+	if in == nil {
+		return nil
+	}
+	out := new(LabelIdentityList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *LabelIdentityList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LabelIdentitySpec) DeepCopyInto(out *LabelIdentitySpec) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LabelIdentitySpec.
+func (in *LabelIdentitySpec) DeepCopy() *LabelIdentitySpec {
+	if in == nil {
+		return nil
+	}
+	out := new(LabelIdentitySpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *MemberCluster) DeepCopyInto(out *MemberCluster) {
 	*out = *in
@@ -703,6 +791,11 @@ func (in *ResourceExportSpec) DeepCopyInto(out *ResourceExportSpec) {
 		*out = new(crdv1alpha1.ClusterNetworkPolicySpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.LabelIdentity != nil {
+		in, out := &in.LabelIdentity, &out.LabelIdentity
+		*out = new(LabelIdentityExport)
+		**out = **in
+	}
 	if in.Raw != nil {
 		in, out := &in.Raw, &out.Raw
 		*out = new(RawResourceExport)
@@ -872,6 +965,11 @@ func (in *ResourceImportSpec) DeepCopyInto(out *ResourceImportSpec) {
 		*out = new(crdv1alpha1.ClusterNetworkPolicySpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.LabelIdentity != nil {
+		in, out := &in.LabelIdentity, &out.LabelIdentity
+		*out = new(LabelIdentitySpec)
+		**out = **in
+	}
 	if in.Raw != nil {
 		in, out := &in.Raw, &out.Raw
 		*out = new(RawResourceImport)
diff --git a/multicluster/build/yamls/antrea-multicluster-leader-global.yml b/multicluster/build/yamls/antrea-multicluster-leader-global.yml
index a5eb12175..b9803b105 100644
--- a/multicluster/build/yamls/antrea-multicluster-leader-global.yml
+++ b/multicluster/build/yamls/antrea-multicluster-leader-global.yml
@@ -379,6 +379,11 @@ spec:
                           type: string
                       type: object
                     type: array
+                  podCIDRs:
+                    description: PodCIDRs is the Pod IP address CIDRs.
+                    items:
+                      type: string
+                    type: array
                   serviceCIDR:
                     description: ServiceCIDR is the IP ranges used by Service ClusterIP.
                     type: string
@@ -2819,6 +2824,12 @@ spec:
               kind:
                 description: Kind of exported resource.
                 type: string
+              labelIdentity:
+                description: If exported resource is LabelIdentity of a cluster.
+                properties:
+                  normalizedLabel:
+                    type: string
+                type: object
               name:
                 description: Name of exported resource.
                 type: string
@@ -3268,6 +3279,11 @@ spec:
                           type: string
                       type: object
                     type: array
+                  podCIDRs:
+                    description: PodCIDRs is the Pod IP address CIDRs.
+                    items:
+                      type: string
+                    type: array
                   serviceCIDR:
                     description: ServiceCIDR is the IP ranges used by Service ClusterIP.
                     type: string
@@ -5708,6 +5724,20 @@ spec:
               kind:
                 description: Kind of imported resource.
                 type: string
+              labelIdentity:
+                description: If imported resource kind is LabelIdentity.
+                properties:
+                  id:
+                    description: ID is the ID allocated for the label identity by
+                      the leader cluster.
+                    format: int32
+                    type: integer
+                  label:
+                    description: Label is the normalized string of a label identity.
+                      The format of normalized label identity is `ns:(?P<nslabels>(.)*)&pod:(?P<podlabels>(.)*)`
+                      E.g., `ns:kubernetes.io/metadata.name=kube-system&pod:app=db`
+                    type: string
+                type: object
               name:
                 description: Name of imported resource.
                 type: string
@@ -5715,10 +5745,7 @@ spec:
                 description: Namespace of imported resource.
                 type: string
               raw:
-                description: 'If imported resource is ANP. TODO: ANP uses float64
-                  as priority.  Type float64 is discouraged by k8s, and is not supported
-                  by controller-gen tools. NetworkPolicy *v1alpha1.NetworkPolicySpec
-                  `json:"networkpolicy,omitempty"` If imported resource kind is unknown.'
+                description: If imported resource kind is unknown.
                 properties:
                   data:
                     format: byte
diff --git a/multicluster/build/yamls/antrea-multicluster-leader-namespaced.yml b/multicluster/build/yamls/antrea-multicluster-leader-namespaced.yml
index 6755a4665..cf2e8c5e7 100644
--- a/multicluster/build/yamls/antrea-multicluster-leader-namespaced.yml
+++ b/multicluster/build/yamls/antrea-multicluster-leader-namespaced.yml
@@ -322,6 +322,8 @@ data:
     leaderElection:
       leaderElect: false
     serviceCIDR: ""
+    podCIDRs:
+      - ""
     gatewayIPPrecedence: "private"
     endpointIPType: "ClusterIP"
 kind: ConfigMap
diff --git a/multicluster/build/yamls/antrea-multicluster-member.yml b/multicluster/build/yamls/antrea-multicluster-member.yml
index 12263c286..1b7911b74 100644
--- a/multicluster/build/yamls/antrea-multicluster-member.yml
+++ b/multicluster/build/yamls/antrea-multicluster-member.yml
@@ -111,6 +111,11 @@ spec:
                       type: string
                   type: object
                 type: array
+              podCIDRs:
+                description: PodCIDRs is the Pod IP address CIDRs.
+                items:
+                  type: string
+                type: array
               serviceCIDR:
                 description: ServiceCIDR is the IP ranges used by Service ClusterIP.
                 type: string
@@ -401,6 +406,60 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.0
+  creationTimestamp: null
+  labels:
+    app: antrea
+  name: labelidentities.multicluster.crd.antrea.io
+spec:
+  group: multicluster.crd.antrea.io
+  names:
+    kind: LabelIdentity
+    listKind: LabelIdentityList
+    plural: labelidentities
+    singular: labelidentity
+  scope: Cluster
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: LabelIdentity is an imported label identity from the ClusterSet.
+          For each unique label identity, a LabelIdentity will be created in the member
+          cluster.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              id:
+                description: ID is the ID allocated for the label identity by the
+                  leader cluster.
+                format: int32
+                type: integer
+              label:
+                description: Label is the normalized string of a label identity. The
+                  format of normalized label identity is `ns:(?P<nslabels>(.)*)&pod:(?P<podlabels>(.)*)`
+                  E.g., `ns:kubernetes.io/metadata.name=kube-system&pod:app=db`
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   labels:
     app: antrea
@@ -740,6 +799,22 @@ rules:
   - patch
   - update
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - namespaces
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:
@@ -890,6 +965,18 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - multicluster.crd.antrea.io
+  resources:
+  - labelidentities
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
 - apiGroups:
   - multicluster.crd.antrea.io
   resources:
@@ -946,6 +1033,8 @@ data:
     leaderElection:
       leaderElect: false
     serviceCIDR: ""
+    podCIDRs:
+      - ""
     gatewayIPPrecedence: "private"
     endpointIPType: "ClusterIP"
 kind: ConfigMap
diff --git a/multicluster/cmd/multicluster-controller/leader.go b/multicluster/cmd/multicluster-controller/leader.go
index b45e31166..1b065f378 100644
--- a/multicluster/cmd/multicluster-controller/leader.go
+++ b/multicluster/cmd/multicluster-controller/leader.go
@@ -52,6 +52,7 @@ func newLeaderCommand() *cobra.Command {
 func runLeader(o *Options) error {
 	// on the leader we want the reconciler to run for a given Namespace instead of cluster scope
 	o.options.Namespace = env.GetPodNamespace()
+	stopCh := signals.RegisterSignalHandlers()
 
 	mgr, err := setupManagerAndCertController(o)
 	if err != nil {
@@ -89,10 +90,18 @@ func runLeader(o *Options) error {
 	if err = resExportReconciler.SetupWithManager(mgr); err != nil {
 		return fmt.Errorf("error creating ResourceExport controller: %v", err)
 	}
+	labelExportReconciler := multiclustercontrollers.NewLabelIdentityExportReconciler(
+		mgr.GetClient(),
+		mgr.GetScheme(),
+		env.GetPodNamespace())
+	if err = labelExportReconciler.SetupWithManager(mgr); err != nil {
+		return fmt.Errorf("error creating LabelIdentityExport controller: %v", err)
+	}
+	go labelExportReconciler.Run(stopCh)
+
 	if err = (&multiclusterv1alpha1.ResourceExport{}).SetupWebhookWithManager(mgr); err != nil {
 		return fmt.Errorf("error creating ResourceExport webhook: %v", err)
 	}
-	stopCh := signals.RegisterSignalHandlers()
 	staleController := multiclustercontrollers.NewStaleResCleanupController(
 		mgr.GetClient(),
 		mgr.GetScheme(),
diff --git a/multicluster/cmd/multicluster-controller/member.go b/multicluster/cmd/multicluster-controller/member.go
index 64ba6e453..9e7b88bcf 100644
--- a/multicluster/cmd/multicluster-controller/member.go
+++ b/multicluster/cmd/multicluster-controller/member.go
@@ -55,6 +55,7 @@ func runMember(o *Options) error {
 		return err
 	}
 
+	stopCh := signals.RegisterSignalHandlers()
 	hookServer := mgr.GetWebhookServer()
 	hookServer.Register("/validate-multicluster-crd-antrea-io-v1alpha1-gateway",
 		&webhook.Admission{Handler: &gatewayValidator{
@@ -78,12 +79,22 @@ func runMember(o *Options) error {
 	if err = svcExportReconciler.SetupWithManager(mgr); err != nil {
 		return fmt.Errorf("error creating ServiceExport controller: %v", err)
 	}
+	labelIdentityReconciler := multiclustercontrollers.NewLabelIdentityReconciler(
+		mgr.GetClient(),
+		mgr.GetScheme(),
+		commonAreaGetter)
+	if err = labelIdentityReconciler.SetupWithManager(mgr); err != nil {
+		return fmt.Errorf("error creating LabelIdentity controller: %v", err)
+	}
+
+	go labelIdentityReconciler.Run(stopCh)
 
 	gwReconciler := multiclustercontrollers.NewGatewayReconciler(
 		mgr.GetClient(),
 		mgr.GetScheme(),
 		env.GetPodNamespace(),
 		opts.ServiceCIDR,
+		opts.PodCIDRs,
 		commonAreaGetter)
 	if err = gwReconciler.SetupWithManager(mgr); err != nil {
 		return fmt.Errorf("error creating Gateway controller: %v", err)
@@ -98,7 +109,6 @@ func runMember(o *Options) error {
 		return fmt.Errorf("error creating Node controller: %v", err)
 	}
 
-	stopCh := signals.RegisterSignalHandlers()
 	staleController := multiclustercontrollers.NewStaleResCleanupController(
 		mgr.GetClient(),
 		mgr.GetScheme(),
diff --git a/multicluster/cmd/multicluster-controller/options.go b/multicluster/cmd/multicluster-controller/options.go
index 8ad8837b9..06c8f6f8f 100644
--- a/multicluster/cmd/multicluster-controller/options.go
+++ b/multicluster/cmd/multicluster-controller/options.go
@@ -32,6 +32,8 @@ type Options struct {
 	options        ctrl.Options
 	// The Service ClusterIP range used in the member cluster.
 	ServiceCIDR string
+	// PodCIDRs is the Pod IP address CIDRs of the member cluster.
+	PodCIDRs []string
 	// The precedence about which IP (private or public one) of Node is preferred to
 	// be used as tunnel endpoint. If not specified, private IP will be chosen.
 	GatewayIPPrecedence mcsv1alpha1.Precedence
@@ -64,7 +66,17 @@ func (o *Options) complete(args []string) error {
 				return fmt.Errorf("failed to parse serviceCIDR, invalid CIDR string %s", ctrlConfig.ServiceCIDR)
 			}
 		}
+		cidrs := []string{}
+		for _, cidr := range ctrlConfig.PodCIDRs {
+			if _, _, err := net.ParseCIDR(cidr); err != nil && cidr != "" {
+				return fmt.Errorf("failed to parse podCIDRs, invalid CIDR string %s", cidr)
+			}
+			if cidr != "" {
+				cidrs = append(cidrs, cidr)
+			}
+		}
 		o.ServiceCIDR = ctrlConfig.ServiceCIDR
+		o.PodCIDRs = cidrs
 		o.GatewayIPPrecedence = ctrlConfig.GatewayIPPrecedence
 		if ctrlConfig.EndpointIPType == "" {
 			o.EndpointIPType = "ClusterIP"
diff --git a/multicluster/config/crd/bases/multicluster.crd.antrea.io_clusterinfoimports.yaml b/multicluster/config/crd/bases/multicluster.crd.antrea.io_clusterinfoimports.yaml
index cb8a98f76..5bb209cbf 100644
--- a/multicluster/config/crd/bases/multicluster.crd.antrea.io_clusterinfoimports.yaml
+++ b/multicluster/config/crd/bases/multicluster.crd.antrea.io_clusterinfoimports.yaml
@@ -57,6 +57,11 @@ spec:
                       type: string
                   type: object
                 type: array
+              podCIDRs:
+                description: PodCIDRs is the Pod IP address CIDRs.
+                items:
+                  type: string
+                type: array
               serviceCIDR:
                 description: ServiceCIDR is the IP ranges used by Service ClusterIP.
                 type: string
diff --git a/multicluster/config/crd/bases/multicluster.crd.antrea.io_labelidentities.yaml b/multicluster/config/crd/bases/multicluster.crd.antrea.io_labelidentities.yaml
new file mode 100644
index 000000000..9dc513a1c
--- /dev/null
+++ b/multicluster/config/crd/bases/multicluster.crd.antrea.io_labelidentities.yaml
@@ -0,0 +1,52 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.0
+  creationTimestamp: null
+  name: labelidentities.multicluster.crd.antrea.io
+spec:
+  group: multicluster.crd.antrea.io
+  names:
+    kind: LabelIdentity
+    listKind: LabelIdentityList
+    plural: labelidentities
+    singular: labelidentity
+  scope: Cluster
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: LabelIdentity is an imported label identity from the ClusterSet.
+          For each unique label identity, a LabelIdentity will be created in the member
+          cluster.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              id:
+                description: ID is the ID allocated for the label identity by the
+                  leader cluster.
+                format: int32
+                type: integer
+              label:
+                description: Label is the normalized string of a label identity. The
+                  format of normalized label identity is `ns:(?P<nslabels>(.)*)&pod:(?P<podlabels>(.)*)`
+                  E.g., `ns:kubernetes.io/metadata.name=kube-system&pod:app=db`
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
diff --git a/multicluster/config/crd/bases/multicluster.crd.antrea.io_resourceexports.yaml b/multicluster/config/crd/bases/multicluster.crd.antrea.io_resourceexports.yaml
index 0b9440868..a971352d6 100644
--- a/multicluster/config/crd/bases/multicluster.crd.antrea.io_resourceexports.yaml
+++ b/multicluster/config/crd/bases/multicluster.crd.antrea.io_resourceexports.yaml
@@ -74,6 +74,11 @@ spec:
                           type: string
                       type: object
                     type: array
+                  podCIDRs:
+                    description: PodCIDRs is the Pod IP address CIDRs.
+                    items:
+                      type: string
+                    type: array
                   serviceCIDR:
                     description: ServiceCIDR is the IP ranges used by Service ClusterIP.
                     type: string
@@ -2514,6 +2519,12 @@ spec:
               kind:
                 description: Kind of exported resource.
                 type: string
+              labelIdentity:
+                description: If exported resource is LabelIdentity of a cluster.
+                properties:
+                  normalizedLabel:
+                    type: string
+                type: object
               name:
                 description: Name of exported resource.
                 type: string
diff --git a/multicluster/config/crd/bases/multicluster.crd.antrea.io_resourceimports.yaml b/multicluster/config/crd/bases/multicluster.crd.antrea.io_resourceimports.yaml
index bbb60b5d3..d46930500 100644
--- a/multicluster/config/crd/bases/multicluster.crd.antrea.io_resourceimports.yaml
+++ b/multicluster/config/crd/bases/multicluster.crd.antrea.io_resourceimports.yaml
@@ -72,6 +72,11 @@ spec:
                           type: string
                       type: object
                     type: array
+                  podCIDRs:
+                    description: PodCIDRs is the Pod IP address CIDRs.
+                    items:
+                      type: string
+                    type: array
                   serviceCIDR:
                     description: ServiceCIDR is the IP ranges used by Service ClusterIP.
                     type: string
@@ -2512,6 +2517,20 @@ spec:
               kind:
                 description: Kind of imported resource.
                 type: string
+              labelIdentity:
+                description: If imported resource kind is LabelIdentity.
+                properties:
+                  id:
+                    description: ID is the ID allocated for the label identity by
+                      the leader cluster.
+                    format: int32
+                    type: integer
+                  label:
+                    description: Label is the normalized string of a label identity.
+                      The format of normalized label identity is `ns:(?P<nslabels>(.)*)&pod:(?P<podlabels>(.)*)`
+                      E.g., `ns:kubernetes.io/metadata.name=kube-system&pod:app=db`
+                    type: string
+                type: object
               name:
                 description: Name of imported resource.
                 type: string
@@ -2519,10 +2538,7 @@ spec:
                 description: Namespace of imported resource.
                 type: string
               raw:
-                description: 'If imported resource is ANP. TODO: ANP uses float64
-                  as priority.  Type float64 is discouraged by k8s, and is not supported
-                  by controller-gen tools. NetworkPolicy *v1alpha1.NetworkPolicySpec
-                  `json:"networkpolicy,omitempty"` If imported resource kind is unknown.'
+                description: If imported resource kind is unknown.
                 properties:
                   data:
                     format: byte
diff --git a/multicluster/config/crd/kustomization.yaml b/multicluster/config/crd/kustomization.yaml
index ab9774571..cf0174c37 100644
--- a/multicluster/config/crd/kustomization.yaml
+++ b/multicluster/config/crd/kustomization.yaml
@@ -11,6 +11,7 @@ resources:
 - k8smcs/multicluster.x-k8s.io_serviceimports.yaml
 - bases/multicluster.crd.antrea.io_clusterinfoimports.yaml
 - bases/multicluster.crd.antrea.io_gateways.yaml
+- bases/multicluster.crd.antrea.io_labelidentities.yaml
 #+kubebuilder:scaffold:crdkustomizeresource
 
 patchesStrategicMerge:
diff --git a/multicluster/config/default/configmap/controller_manager_config.yaml b/multicluster/config/default/configmap/controller_manager_config.yaml
index a2f3d3efe..decbb468c 100644
--- a/multicluster/config/default/configmap/controller_manager_config.yaml
+++ b/multicluster/config/default/configmap/controller_manager_config.yaml
@@ -9,5 +9,7 @@ webhook:
 leaderElection:
   leaderElect: false
 serviceCIDR: ""
+podCIDRs:
+  - ""
 gatewayIPPrecedence: "private"
 endpointIPType: "ClusterIP"
diff --git a/multicluster/config/overlays/leader-global/crd_patch.yaml b/multicluster/config/overlays/leader-global/crd_patch.yaml
index cd8cabfb8..4897c62d7 100644
--- a/multicluster/config/overlays/leader-global/crd_patch.yaml
+++ b/multicluster/config/overlays/leader-global/crd_patch.yaml
@@ -21,3 +21,9 @@ kind: CustomResourceDefinition
 metadata:
   name: clusterinfoimports.multicluster.crd.antrea.io
 $patch: delete
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: labelidentities.multicluster.crd.antrea.io
+$patch: delete
diff --git a/multicluster/config/overlays/member/role.yaml b/multicluster/config/overlays/member/role.yaml
index 07114f031..f2eb2476d 100644
--- a/multicluster/config/overlays/member/role.yaml
+++ b/multicluster/config/overlays/member/role.yaml
@@ -38,6 +38,22 @@ rules:
   - patch
   - update
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - namespaces
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:
@@ -188,6 +204,18 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - multicluster.crd.antrea.io
+  resources:
+  - labelidentities
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
 - apiGroups:
   - multicluster.crd.antrea.io
   resources:
diff --git a/multicluster/controllers/multicluster/common/helper.go b/multicluster/controllers/multicluster/common/helper.go
index 76c8ceca0..eeec1c9b9 100644
--- a/multicluster/controllers/multicluster/common/helper.go
+++ b/multicluster/controllers/multicluster/common/helper.go
@@ -13,7 +13,12 @@ limitations under the License.
 
 package common
 
-import corev1 "k8s.io/api/core/v1"
+import (
+	"crypto/sha1" // #nosec G505: not used for security purposes
+	"encoding/hex"
+
+	corev1 "k8s.io/api/core/v1"
+)
 
 const (
 	AntreaMCServiceAnnotation = "multicluster.antrea.io/imported-service"
@@ -25,6 +30,7 @@ const (
 	ServiceKind                    = "Service"
 	EndpointsKind                  = "Endpoints"
 	AntreaClusterNetworkPolicyKind = "AntreaClusterNetworkPolicy"
+	LabelIdentityKind              = "LabelIdentity"
 	ServiceImportKind              = "ServiceImport"
 	ClusterInfoKind                = "ClusterInfo"
 
@@ -33,7 +39,8 @@ const (
 	SourceClusterID = "sourceClusterID"
 	SourceKind      = "sourceKind"
 
-	DefaultWorkerCount = 5
+	DefaultWorkerCount      = 5
+	labelIdentityHashLength = 16
 
 	ResourceExportFinalizer = "resourceexport.finalizers.antrea.io"
 )
@@ -91,10 +98,10 @@ func GetServiceEndpointPorts(ports []corev1.ServicePort) []corev1.EndpointPort {
 // FilterEndpointSubsets keeps IPs only and removes other fields from EndpointSubset
 // which are unnecessary information for other member clusters.
 func FilterEndpointSubsets(subsets []corev1.EndpointSubset) []corev1.EndpointSubset {
-	newSubsets := []corev1.EndpointSubset{}
+	var newSubsets []corev1.EndpointSubset
 	for _, s := range subsets {
 		subset := corev1.EndpointSubset{}
-		newAddresses := []corev1.EndpointAddress{}
+		var newAddresses []corev1.EndpointAddress
 		for _, addr := range s.Addresses {
 			newAddresses = append(newAddresses, corev1.EndpointAddress{
 				IP: addr.IP,
@@ -108,3 +115,11 @@ func FilterEndpointSubsets(subsets []corev1.EndpointSubset) []corev1.EndpointSub
 	}
 	return newSubsets
 }
+
+// HashLabelIdentity generates a hash value for label identity string.
+func HashLabelIdentity(l string) string {
+	hash := sha1.New() // #nosec G401: not used for security purposes
+	hash.Write([]byte(l))
+	hashValue := hex.EncodeToString(hash.Sum(nil))
+	return hashValue[:labelIdentityHashLength]
+}
diff --git a/multicluster/controllers/multicluster/commonarea/labelidentityimport_controller.go b/multicluster/controllers/multicluster/commonarea/labelidentityimport_controller.go
new file mode 100644
index 000000000..febed5da7
--- /dev/null
+++ b/multicluster/controllers/multicluster/commonarea/labelidentityimport_controller.go
@@ -0,0 +1,141 @@
+/*
+Copyright 2022 Antrea Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commonarea
+
+import (
+	"context"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/klog/v2"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+
+	multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	"antrea.io/antrea/multicluster/controllers/multicluster/common"
+)
+
+// LabelIdentityResourceImportReconciler reconciles a LabelIdentity kind of ResourceImport object in the member cluster.
+type LabelIdentityResourceImportReconciler struct {
+	client.Client
+	Scheme             *runtime.Scheme
+	localClusterClient client.Client
+	localClusterID     string
+	namespace          string
+	remoteCommonArea   RemoteCommonArea
+}
+
+func NewLabelIdentityResourceImportReconciler(client client.Client, scheme *runtime.Scheme, localClusterClient client.Client,
+	localClusterID string, namespace string, remoteCommonArea RemoteCommonArea) *LabelIdentityResourceImportReconciler {
+	return &LabelIdentityResourceImportReconciler{
+		Client:             client,
+		Scheme:             scheme,
+		localClusterClient: localClusterClient,
+		localClusterID:     localClusterID,
+		namespace:          namespace,
+		remoteCommonArea:   remoteCommonArea,
+	}
+}
+
+func (r *LabelIdentityResourceImportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	klog.V(2).InfoS("Reconciling LabelIdentity kind of ResourceImport", "resourceImport", req.NamespacedName)
+	var labelIdentityResImport multiclusterv1alpha1.ResourceImport
+	err := r.remoteCommonArea.Get(ctx, req.NamespacedName, &labelIdentityResImport)
+	if err != nil {
+		if !apierrors.IsNotFound(err) {
+			klog.ErrorS(err, "Unable to fetch LabelIdentity kind of ResourceImport", "resourceImport", req.NamespacedName)
+			return ctrl.Result{}, err
+		}
+		return r.handleLabelIdentityImpDelete(ctx, req.NamespacedName)
+	}
+	return r.handleLabelIdentityImpCreateOrUpdate(ctx, &labelIdentityResImport)
+}
+
+func (r *LabelIdentityResourceImportReconciler) handleLabelIdentityImpCreateOrUpdate(ctx context.Context,
+	labelResImp *multiclusterv1alpha1.ResourceImport) (ctrl.Result, error) {
+	labelIdentityName := types.NamespacedName{
+		Name: labelResImp.Name,
+	}
+	labelIdentity := &multiclusterv1alpha1.LabelIdentity{}
+	err := r.localClusterClient.Get(ctx, labelIdentityName, labelIdentity)
+	labelIdentityNotFound := apierrors.IsNotFound(err)
+	if err != nil && !labelIdentityNotFound {
+		return ctrl.Result{}, err
+	}
+	if labelIdentityNotFound {
+		newLabelIdentity := &multiclusterv1alpha1.LabelIdentity{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: labelResImp.Name,
+			},
+			Spec: multiclusterv1alpha1.LabelIdentitySpec{
+				Label: labelResImp.Spec.LabelIdentity.Label,
+				ID:    labelResImp.Spec.LabelIdentity.ID,
+			},
+		}
+		if err = r.localClusterClient.Create(ctx, newLabelIdentity, &client.CreateOptions{}); err != nil {
+			klog.ErrorS(err, "Failed to create LabelIdentity", "clusterID", r.localClusterID, "label", labelResImp.Spec.LabelIdentity.Label)
+			return ctrl.Result{}, err
+		}
+	} else if labelIdentity.Spec.ID != labelResImp.Spec.LabelIdentity.ID {
+		labelIdentity.Spec.ID = labelResImp.Spec.LabelIdentity.ID
+		if err = r.localClusterClient.Update(ctx, labelIdentity, &client.UpdateOptions{}); err != nil {
+			klog.ErrorS(err, "Failed to update LabelIdentity", "clusterID", r.localClusterID, "label", labelResImp.Spec.LabelIdentity.Label)
+			return ctrl.Result{}, err
+		}
+	}
+	return ctrl.Result{}, nil
+}
+
+func (r *LabelIdentityResourceImportReconciler) handleLabelIdentityImpDelete(ctx context.Context,
+	resImpName types.NamespacedName) (ctrl.Result, error) {
+	klog.V(2).InfoS("Deleting LabelIdentity corresponding to LabelIdentity kind of ResourceImport", "resourceImport", resImpName.String())
+	labelIdentity := &multiclusterv1alpha1.LabelIdentity{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: resImpName.Name,
+		},
+	}
+	if err := r.localClusterClient.Delete(ctx, labelIdentity, &client.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
+		klog.ErrorS(err, "Failed to delete LabelIdentity", "clusterID", r.localClusterID, "labelIdentity", labelIdentity.Name)
+		return ctrl.Result{}, err
+	}
+	return ctrl.Result{}, nil
+}
+
+func (r *LabelIdentityResourceImportReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	// Ignore status update event via GenerationChangedPredicate
+	generationPredicate := predicate.GenerationChangedPredicate{}
+	// Only register this controller to reconcile LabelIdentity kind of ResourceImport
+	labelIdentityResImportFilter := func(object client.Object) bool {
+		if resImport, ok := object.(*multiclusterv1alpha1.ResourceImport); ok {
+			return resImport.Spec.Kind == common.LabelIdentityKind
+		}
+		return false
+	}
+	labelIdentityResImportPredicate := predicate.NewPredicateFuncs(labelIdentityResImportFilter)
+	instance := predicate.And(generationPredicate, labelIdentityResImportPredicate)
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&multiclusterv1alpha1.ResourceImport{}).
+		WithEventFilter(instance).
+		WithOptions(controller.Options{
+			MaxConcurrentReconciles: common.DefaultWorkerCount,
+		}).
+		Complete(r)
+}
diff --git a/multicluster/controllers/multicluster/commonarea/remote_common_area.go b/multicluster/controllers/multicluster/commonarea/remote_common_area.go
index c85ce79a7..b869c3dc7 100644
--- a/multicluster/controllers/multicluster/commonarea/remote_common_area.go
+++ b/multicluster/controllers/multicluster/commonarea/remote_common_area.go
@@ -377,15 +377,24 @@ func (r *remoteCommonArea) StartWatching() error {
 		r.ClusterManager.GetClient(),
 		r.ClusterManager.GetScheme(),
 		r.localClusterClient,
-		string(r.GetLocalClusterID()),
+		r.GetLocalClusterID(),
 		r.localNamespace,
 		r,
 	)
-
 	if err := resImportReconciler.SetupWithManager(r.ClusterManager); err != nil {
-		klog.V(2).ErrorS(err, "Error creating ResourceImport controller for RemoteCommonArea", "cluster", r.ClusterID)
 		return fmt.Errorf("error creating ResourceImport controller for RemoteCommonArea: %v", err)
 	}
+	labelIdentityImpReconciler := NewLabelIdentityResourceImportReconciler(
+		r.ClusterManager.GetClient(),
+		r.ClusterManager.GetScheme(),
+		r.localClusterClient,
+		string(r.ClusterID),
+		r.Namespace,
+		r,
+	)
+	if err := labelIdentityImpReconciler.SetupWithManager(r.ClusterManager); err != nil {
+		return fmt.Errorf("error creating LabelIdentityResourceImport controller for RemoteCommonArea: %v", err)
+	}
 
 	go func() {
 		stopCtx, stopFunc := context.WithCancel(context.Background())
diff --git a/multicluster/controllers/multicluster/commonarea/resourceimport_controller.go b/multicluster/controllers/multicluster/commonarea/resourceimport_controller.go
index 9d27cd1de..78abb6794 100644
--- a/multicluster/controllers/multicluster/commonarea/resourceimport_controller.go
+++ b/multicluster/controllers/multicluster/commonarea/resourceimport_controller.go
@@ -80,30 +80,22 @@ func NewResourceImportReconciler(client client.Client, scheme *runtime.Scheme, l
 	}
 }
 
-//+kubebuilder:rbac:groups=crd.antrea.io,resources=clusternetworkpolicies,verbs=get;list;watch;create;update;patch;delete
-//+kubebuilder:rbac:groups=crd.antrea.io,resources=tiers,verbs=get;list;watch
-//+kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceimports,verbs=get;list;watch;create;update;patch;delete
-//+kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceimports/status,verbs=get;update;patch
-//+kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceimports/finalizers,verbs=update
-//+kubebuilder:rbac:groups=multicluster.x-k8s.io,resources=serviceimports,verbs=get;list;watch;create;update;patch;delete
-//+kubebuilder:rbac:groups=multicluster.x-k8s.io,resources=serviceimports/status,verbs=get;update;patch
-//+kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list;watch;update;create;patch;delete
-//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;update;create;patch;delete
-//+kubebuilder:rbac:groups="",resources=events,verbs=create
+// +kubebuilder:rbac:groups=crd.antrea.io,resources=clusternetworkpolicies,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=crd.antrea.io,resources=tiers,verbs=get;list;watch
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceimports,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceimports/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceimports/finalizers,verbs=update
+// +kubebuilder:rbac:groups=multicluster.x-k8s.io,resources=serviceimports,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=multicluster.x-k8s.io,resources=serviceimports/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list;watch;update;create;patch;delete
+// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;update;create;patch;delete
+// +kubebuilder:rbac:groups="",resources=events,verbs=create
 
 // Reconcile will attempt to ensure that the imported Resource is installed in local cluster as per the
 // ResourceImport object.
 func (r *ResourceImportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	klog.V(2).InfoS("Reconciling ResourceImport", "resourceimport", req.NamespacedName)
 	// TODO: Must check whether this ResourceImport must be reconciled by this member cluster. Check `spec.clusters` field.
-	if r.localClusterClient == nil {
-		return ctrl.Result{}, errors.New("localClusterClient has not been initialized properly, no local cluster client")
-	}
-
-	if r.remoteCommonArea == nil {
-		return ctrl.Result{}, errors.New("remoteCommonArea has not been initialized properly, no remote common area")
-	}
-
 	var resImp multiclusterv1alpha1.ResourceImport
 	err := r.remoteCommonArea.Get(ctx, req.NamespacedName, &resImp)
 	var isDeleted bool
@@ -123,7 +115,6 @@ func (r *ResourceImportReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 			}
 		}
 	}
-
 	switch resImp.Spec.Kind {
 	case common.ServiceImportKind:
 		if isDeleted {
@@ -374,7 +365,16 @@ func getMCServiceImport(resImp *multiclusterv1alpha1.ResourceImport) *k8smcsv1al
 // in the remoteCommonArea.
 func (r *ResourceImportReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	// Ignore status update event via GenerationChangedPredicate
-	instance := predicate.GenerationChangedPredicate{}
+	generationPredicate := predicate.GenerationChangedPredicate{}
+	// Register this filter to ignore LabelIdentity kind of ResourceImport
+	labelIdentityResImportFilter := func(object client.Object) bool {
+		if resImport, ok := object.(*multiclusterv1alpha1.ResourceImport); ok {
+			return resImport.Spec.Kind != common.LabelIdentityKind
+		}
+		return false
+	}
+	labelIdentityResImportPredicate := predicate.NewPredicateFuncs(labelIdentityResImportFilter)
+	instance := predicate.And(generationPredicate, labelIdentityResImportPredicate)
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&multiclusterv1alpha1.ResourceImport{}).
 		WithEventFilter(instance).
diff --git a/multicluster/controllers/multicluster/gateway_controller.go b/multicluster/controllers/multicluster/gateway_controller.go
index 5efe37cad..3cdac092a 100644
--- a/multicluster/controllers/multicluster/gateway_controller.go
+++ b/multicluster/controllers/multicluster/gateway_controller.go
@@ -44,6 +44,7 @@ type (
 		namespace        string
 		localClusterID   string
 		serviceCIDR      string
+		podCIDRs         []string
 		leaderNamespace  string
 	}
 )
@@ -55,12 +56,14 @@ func NewGatewayReconciler(
 	scheme *runtime.Scheme,
 	namespace string,
 	serviceCIDR string,
+	podCIDRs []string,
 	commonAreaGetter RemoteCommonAreaGetter) *GatewayReconciler {
 	reconciler := &GatewayReconciler{
 		Client:           client,
 		Scheme:           scheme,
 		namespace:        namespace,
 		serviceCIDR:      serviceCIDR,
+		podCIDRs:         podCIDRs,
 		commonAreaGetter: commonAreaGetter,
 	}
 	return reconciler
@@ -144,6 +147,7 @@ func (r *GatewayReconciler) updateResourceExport(ctx context.Context, req ctrl.R
 	resExportSpec.ClusterInfo = &mcsv1alpha1.ClusterInfo{
 		ClusterID:    r.localClusterID,
 		ServiceCIDR:  r.serviceCIDR,
+		PodCIDRs:     r.podCIDRs,
 		GatewayInfos: []mcsv1alpha1.GatewayInfo{*gwInfo},
 	}
 	if reflect.DeepEqual(existingResExport.Spec, resExportSpec) {
@@ -171,6 +175,7 @@ func (r *GatewayReconciler) createResourceExport(ctx context.Context, req ctrl.R
 	resExportSpec.ClusterInfo = &mcsv1alpha1.ClusterInfo{
 		ClusterID:   r.localClusterID,
 		ServiceCIDR: r.serviceCIDR,
+		PodCIDRs:    r.podCIDRs,
 		GatewayInfos: []mcsv1alpha1.GatewayInfo{
 			{
 				GatewayIP: gatewayIP,
diff --git a/multicluster/controllers/multicluster/label_identity_controller.go b/multicluster/controllers/multicluster/label_identity_controller.go
new file mode 100644
index 000000000..10b2a4944
--- /dev/null
+++ b/multicluster/controllers/multicluster/label_identity_controller.go
@@ -0,0 +1,322 @@
+/*
+Copyright 2022 Antrea Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package multicluster
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/util/workqueue"
+	"k8s.io/klog/v2"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/controller-runtime/pkg/source"
+
+	mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	"antrea.io/antrea/multicluster/controllers/multicluster/common"
+	"antrea.io/antrea/multicluster/controllers/multicluster/commonarea"
+)
+
+// LabelIdentityReconciler watches relevant Pod and Namespace events in the member cluster,
+// computes the label identities added to and deleted from the cluster, and exports them to the
+// leader cluster for further processing.
+type (
+	LabelIdentityReconciler struct {
+		client.Client
+		Scheme           *runtime.Scheme
+		commonAreaGetter RemoteCommonAreaGetter
+		remoteCommonArea commonarea.RemoteCommonArea
+		// labelMutex prevents concurrent access to labelToPodsCache and podLabelCache.
+		// It also prevents concurrent updates to labelExportUpdatesInProgress.
+		labelMutex sync.RWMutex
+		// labelToPodsCache stores mapping from label identities to Pods that have this label identity.
+		labelToPodsCache map[string]sets.String
+		// podLabelCache stores mapping from Pods to their label identities.
+		podLabelCache map[string]string
+		// labelQueue maintains the normalized labels whose corresponding ResourceExport objects are
+		// determined to be created/deleted by the reconciler.
+		labelQueue     workqueue.RateLimitingInterface
+		localClusterID string
+	}
+)
+
+func NewLabelIdentityReconciler(
+	client client.Client,
+	scheme *runtime.Scheme,
+	commonAreaGetter RemoteCommonAreaGetter) *LabelIdentityReconciler {
+	return &LabelIdentityReconciler{
+		Client:           client,
+		Scheme:           scheme,
+		commonAreaGetter: commonAreaGetter,
+		labelToPodsCache: map[string]sets.String{},
+		podLabelCache:    map[string]string{},
+		labelQueue:       workqueue.NewRateLimitingQueue(workqueue.DefaultItemBasedRateLimiter()),
+	}
+}
+
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch
+// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
+func (r *LabelIdentityReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	klog.V(2).InfoS("Reconciling Pod for label identity", "pod", req.NamespacedName)
+	var commonArea commonarea.RemoteCommonArea
+	var err error
+	commonArea, r.localClusterID, err = r.commonAreaGetter.GetRemoteCommonAreaAndLocalID()
+	if commonArea == nil {
+		return ctrl.Result{Requeue: true}, err
+	}
+	r.remoteCommonArea = commonArea
+	var pod v1.Pod
+	var ns v1.Namespace
+
+	if err := r.Client.Get(ctx, req.NamespacedName, &pod); err != nil {
+		if apierrors.IsNotFound(err) {
+			klog.V(2).InfoS("Pod is deleted", "pod", req.NamespacedName)
+			r.onPodDelete(req.NamespacedName.String())
+			return ctrl.Result{}, nil
+		}
+		return ctrl.Result{}, err
+	}
+	if err := r.Client.Get(ctx, types.NamespacedName{Name: req.Namespace}, &ns); err != nil {
+		klog.ErrorS(err, "Cannot get corresponding Namespace of the Pod", "pod", req.NamespacedName)
+		return ctrl.Result{}, err
+	}
+	normalizedLabel := getNormalizedLabel(ns.Labels, pod.Labels, ns.Name)
+	r.onPodCreateOrUpdate(req.NamespacedName.String(), normalizedLabel)
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *LabelIdentityReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&v1.Pod{}).
+		WithEventFilter(predicate.LabelChangedPredicate{}).
+		Watches(&source.Kind{Type: &v1.Namespace{}},
+			handler.EnqueueRequestsFromMapFunc(r.namespaceMapFunc),
+			builder.WithPredicates(predicate.LabelChangedPredicate{})).
+		WithOptions(controller.Options{
+			MaxConcurrentReconciles: common.DefaultWorkerCount,
+		}).
+		Complete(r)
+}
+
+// namespaceMapFunc handles Namespace update events (Namespace label change) by enqueuing
+// all Pods in the Namespace into the reconciler processing queue.
+func (r *LabelIdentityReconciler) namespaceMapFunc(ns client.Object) []reconcile.Request {
+	podList := &v1.PodList{}
+	r.Client.List(context.TODO(), podList, client.InNamespace(ns.GetName()))
+	requests := make([]reconcile.Request, len(podList.Items))
+	for i, pod := range podList.Items {
+		requests[i] = reconcile.Request{
+			NamespacedName: types.NamespacedName{
+				Name:      pod.GetName(),
+				Namespace: pod.GetNamespace(),
+			},
+		}
+	}
+	return requests
+}
+
+// onPodDelete removes the Pod and label identity mapping from the cache, and queues the
+// label identity for ResourceExport deletion if necessary (the Pod update/deletion event causes
+// a label identity no longer present in the cluster).
+func (r *LabelIdentityReconciler) onPodDelete(podNamespacedName string) {
+	r.labelMutex.Lock()
+	defer r.labelMutex.Unlock()
+
+	if originalLabel, isCached := r.podLabelCache[podNamespacedName]; isCached {
+		r.removeLabelForPod(podNamespacedName, originalLabel)
+	}
+}
+
+// removeLabelForPod removes the Pod and label identity mapping from the cache.
+// It must be invoked with labelMutex held.
+func (r *LabelIdentityReconciler) removeLabelForPod(podNamespacedName, originalLabel string) {
+	delete(r.podLabelCache, podNamespacedName)
+	// Check if the original label is stale.
+	if podNames, ok := r.labelToPodsCache[originalLabel]; ok && len(podNames) == 1 && podNames.Has(podNamespacedName) {
+		klog.V(2).InfoS("Label no longer exists in the cluster, queuing for ResourceExport deletion", "label", originalLabel)
+		delete(r.labelToPodsCache, originalLabel)
+		r.labelQueue.Add(originalLabel)
+	} else {
+		// The original label still has other Pod that refers to it. Simply update the cache.
+		podNames.Delete(podNamespacedName)
+	}
+}
+
+// onPodCreateOrUpdate updates the Pod and label identity mapping in the cache, and
+// updates label identity ResourceExport if necessary (the Pod creation/update
+// event causes a new label identity to appear in the cluster or a label identity
+// no longer present in the cluster or both).
+func (r *LabelIdentityReconciler) onPodCreateOrUpdate(podNamespacedName, currentLabel string) {
+	r.labelMutex.Lock()
+	defer r.labelMutex.Unlock()
+
+	if originalLabel, isCached := r.podLabelCache[podNamespacedName]; isCached && originalLabel != currentLabel {
+		r.removeLabelForPod(podNamespacedName, originalLabel)
+	}
+	r.podLabelCache[podNamespacedName] = currentLabel
+	podNames, ok := r.labelToPodsCache[currentLabel]
+	if !ok {
+		// Create a ResourceExport for this label as this is a new label.
+		klog.V(2).InfoS("New label in cluster, queuing for ResourceExport creation", "label", currentLabel)
+		r.labelToPodsCache[currentLabel] = sets.NewString(podNamespacedName)
+		r.labelQueue.Add(currentLabel)
+	} else {
+		// This is a seen label. Simply update the cache.
+		podNames.Insert(podNamespacedName)
+	}
+}
+
+// Run begins syncing of ResourceExports for label identities.
+func (r *LabelIdentityReconciler) Run(stopCh <-chan struct{}) {
+	defer r.labelQueue.ShutDown()
+
+	for i := 0; i < common.DefaultWorkerCount; i++ {
+		go wait.Until(r.labelQueueWorker, time.Second, stopCh)
+	}
+	<-stopCh
+}
+
+func (r *LabelIdentityReconciler) labelQueueWorker() {
+	for r.processLabelForResourceExport() {
+	}
+}
+
+// Processes an item in the labelQueue. If syncLabelResourceExport returns an error,
+// this function handles it by re-queuing the item so that it can be processed again
+// later. If syncLabelResourceExport is successful, the label is removed from the queue
+// until we get notified of a new change. This function return false if and only if the
+// work queue was shutdown (no more items will be processed).
+func (r *LabelIdentityReconciler) processLabelForResourceExport() bool {
+	key, quit := r.labelQueue.Get()
+	if quit {
+		return false
+	}
+	defer r.labelQueue.Done(key)
+	err := r.syncLabelResourceExport(key.(string))
+	if err != nil {
+		// Put the item back on the workqueue to handle any transient errors.
+		r.labelQueue.AddRateLimited(key)
+		klog.ErrorS(err, "Failed to sync ResourceExport for label identity", "label", key)
+		return true
+	}
+
+	// If no error occurs, we forget this item so that it does not get queued again until
+	// another change happens.
+	r.labelQueue.Forget(key)
+	return true
+}
+
+// syncLabelResourceExport checks labelToPodsCache and determines whether a ResourceExport
+// needs to be created or deleted for the label identity.
+func (r *LabelIdentityReconciler) syncLabelResourceExport(label string) error {
+	r.labelMutex.RLock()
+	_, exists := r.labelToPodsCache[label]
+	r.labelMutex.RUnlock()
+	if exists {
+		// The queue received an event for this label, and there are Pods referring
+		// to this label. Either 1) a new label is encountered, and we need to create
+		// a ResourceExport for it, or 2) a Pod update/delete event triggered a label
+		// deletion, but is immediately followed by another Pod event triggering
+		// add for the same label, which is a quite improbable event. We can simply
+		// ignore AlreadyExists error in ResourceExport creation for the second case.
+		if err := r.createLabelIdentityResExport(ctx, label); err != nil {
+			return err
+		}
+	} else {
+		if err := r.deleteLabelIdentityResExport(ctx, label); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// createLabelIdentityResExport creates a ResourceExport for a newly added label.
+func (r *LabelIdentityReconciler) createLabelIdentityResExport(ctx context.Context, labelToAdd string) error {
+	resExportName := getResourceExportNameForLabelIdentity(r.localClusterID, labelToAdd)
+	labelResExport := r.getLabelIdentityResourceExport(resExportName, labelToAdd)
+	klog.V(4).InfoS("Creating ResourceExport for label", "resourceExport", labelResExport.Name, "label", labelToAdd)
+	err := r.remoteCommonArea.Create(ctx, labelResExport, &client.CreateOptions{})
+	if err != nil && !apierrors.IsAlreadyExists(err) {
+		return err
+	}
+	return nil
+}
+
+// deleteLabelIdentityResExport deletes a ResourceExport for a stale label.
+func (r *LabelIdentityReconciler) deleteLabelIdentityResExport(ctx context.Context, labelToDelete string) error {
+	labelResExport := &mcsv1alpha1.ResourceExport{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      getResourceExportNameForLabelIdentity(r.localClusterID, labelToDelete),
+			Namespace: r.remoteCommonArea.GetNamespace(),
+		},
+	}
+	klog.V(4).InfoS("Deleting ResourceExport for label", "resourceExport", labelResExport.Name, "label", labelToDelete)
+	err := r.remoteCommonArea.Delete(ctx, labelResExport, &client.DeleteOptions{})
+	return client.IgnoreNotFound(err)
+}
+
+func (r *LabelIdentityReconciler) getLabelIdentityResourceExport(name, normalizedLabel string) *mcsv1alpha1.ResourceExport {
+	return &mcsv1alpha1.ResourceExport{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: r.remoteCommonArea.GetNamespace(),
+			Labels: map[string]string{
+				common.SourceKind:      common.LabelIdentityKind,
+				common.SourceClusterID: r.localClusterID,
+			},
+		},
+		Spec: mcsv1alpha1.ResourceExportSpec{
+			ClusterID: r.localClusterID,
+			Kind:      common.LabelIdentityKind,
+			LabelIdentity: &mcsv1alpha1.LabelIdentityExport{
+				NormalizedLabel: normalizedLabel,
+			},
+		},
+	}
+}
+
+func getNormalizedLabel(nsLabels, podLabels map[string]string, ns string) string {
+	if _, ok := nsLabels[v1.LabelMetadataName]; !ok {
+		// NamespaceDefaultLabelName is supported from K8s v1.21. For K8s versions before v1.21,
+		// we append the Namespace name label to the Namespace label set, so that the exported
+		// label is guaranteed to have Namespace name information.
+		nsLabels[v1.LabelMetadataName] = ns
+	}
+	return "ns:" + labels.FormatLabels(nsLabels) + "&pod:" + labels.FormatLabels(podLabels)
+}
+
+// getResourceExportNameForLabelIdentity retrieves the ResourceExport name for exporting
+// label identities in that cluster.
+func getResourceExportNameForLabelIdentity(clusterID, normalizedLabel string) string {
+	return clusterID + "-" + common.HashLabelIdentity(normalizedLabel)
+}
diff --git a/multicluster/controllers/multicluster/label_identity_export_controller.go b/multicluster/controllers/multicluster/label_identity_export_controller.go
new file mode 100644
index 000000000..b7cef97c5
--- /dev/null
+++ b/multicluster/controllers/multicluster/label_identity_export_controller.go
@@ -0,0 +1,377 @@
+/*
+Copyright 2022 Antrea Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package multicluster
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+
+	"golang.org/x/tools/container/intsets"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/util/workqueue"
+	"k8s.io/klog/v2"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+
+	mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	"antrea.io/antrea/multicluster/controllers/multicluster/common"
+)
+
+const (
+	// 24 bits are available in VNI. The max value 16777215 is reserved for unknown ID.
+	maxIDForAllocation = 16777214
+)
+
+// LabelIdentityExportReconciler watches LabelIdentity ResourceExport events in the Common Area,
+// computes if such event cause a new LabelIdentity to become present/stale in the entire
+// ClusterSet, and update ResourceImports accordingly.
+type (
+	LabelIdentityExportReconciler struct {
+		client.Client
+		Scheme           *runtime.Scheme
+		mutex            sync.RWMutex
+		namespace        string
+		clusterToLabels  map[string]sets.String
+		labelsToClusters map[string]sets.String
+		hashToLabels     map[string]string
+		labelQueue       workqueue.RateLimitingInterface
+		labelsToID       map[string]uint32
+		allocator        *idAllocator
+	}
+)
+
+func NewLabelIdentityExportReconciler(
+	client client.Client,
+	scheme *runtime.Scheme,
+	namespace string) *LabelIdentityExportReconciler {
+	return &LabelIdentityExportReconciler{
+		Client:           client,
+		Scheme:           scheme,
+		namespace:        namespace,
+		clusterToLabels:  map[string]sets.String{},
+		labelsToClusters: map[string]sets.String{},
+		hashToLabels:     map[string]string{},
+		labelQueue:       workqueue.NewRateLimitingQueue(workqueue.DefaultItemBasedRateLimiter()),
+		labelsToID:       map[string]uint32{},
+		allocator:        newIDAllocator(1, maxIDForAllocation),
+	}
+}
+
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports/status,verbs=get;update;patch
+func (r *LabelIdentityExportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	var resExport mcsv1alpha1.ResourceExport
+	clusterID, labelHash := parseLabelIdentityExportNamespacedName(req.NamespacedName)
+	if err := r.Client.Get(ctx, req.NamespacedName, &resExport); err != nil {
+		if apierrors.IsNotFound(err) {
+			klog.V(2).InfoS("ResourceExport is deleted", "resourceexport", req.NamespacedName, "cluster", clusterID)
+			r.onLabelExportDelete(clusterID, labelHash)
+			return ctrl.Result{}, nil
+		}
+		return ctrl.Result{}, err
+	}
+	normalizedLabel := resExport.Spec.LabelIdentity.NormalizedLabel
+	r.onLabelExportAdd(clusterID, labelHash, normalizedLabel)
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *LabelIdentityExportReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	// Ignore status update event via GenerationChangedPredicate
+	generationPredicate := predicate.GenerationChangedPredicate{}
+	// Only register this controller to reconcile LabelIdentity kind of ResourceExport.
+	// We expect LabelIdentity ResourceExport events to have higher volume than the other (i.e. Service or
+	// ACNP ResourceExport), and do not want sync of these resources to be blocked by potentially huge number
+	// of LabelIdentity ResourceExport requests. Hence, LabelIdentity reconciler has dedicated workers.
+	labelIdentityResExportFilter := func(object client.Object) bool {
+		if resExport, ok := object.(*mcsv1alpha1.ResourceExport); ok {
+			return resExport.Spec.Kind == common.LabelIdentityKind
+		}
+		return false
+	}
+	labelIdentityResExportPredicate := predicate.NewPredicateFuncs(labelIdentityResExportFilter)
+	instance := predicate.And(generationPredicate, labelIdentityResExportPredicate)
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&mcsv1alpha1.ResourceExport{}).
+		WithEventFilter(instance).
+		WithOptions(controller.Options{
+			MaxConcurrentReconciles: common.DefaultWorkerCount,
+		}).
+		Complete(r)
+}
+
+// onLabelExportDelete updates the label to cluster caches, and deletes stale LabelIdentity kind of
+// ResourceImport object if needed.
+func (r *LabelIdentityExportReconciler) onLabelExportDelete(clusterID, labelHash string) {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+
+	if clusterLabels, ok := r.clusterToLabels[clusterID]; ok {
+		clusterLabels.Delete(labelHash)
+	}
+	if clusters, ok := r.labelsToClusters[labelHash]; ok && len(clusters) == 1 && clusters.Has(clusterID) {
+		// The cluster where the label identity is being deleted was the only cluster that has
+		// the label identity. Hence, the label identity is no longer present in the ClusterSet.
+		delete(r.labelsToClusters, labelHash)
+		delete(r.hashToLabels, labelHash)
+		r.labelQueue.Add(labelHash)
+	} else {
+		// Remove mapping from label to cluster
+		clusters.Delete(clusterID)
+	}
+}
+
+// onLabelExportAdd updates the label to cluster caches, and creates LabelIdentity kind of
+// ResourceImport object if needed.
+func (r *LabelIdentityExportReconciler) onLabelExportAdd(clusterID, labelHash, label string) {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+
+	if labels, ok := r.clusterToLabels[clusterID]; ok {
+		labels.Insert(labelHash)
+	} else {
+		r.clusterToLabels[clusterID] = sets.NewString(labelHash)
+	}
+	if clusters, ok := r.labelsToClusters[labelHash]; !ok {
+		// This is a new label identity in the entire ClusterSet.
+		r.labelsToClusters[labelHash] = sets.NewString(clusterID)
+		r.hashToLabels[labelHash] = label
+		r.labelQueue.Add(labelHash)
+	} else {
+		clusters.Insert(clusterID)
+	}
+}
+
+// Run begins syncing of ResourceImports for label identities.
+func (r *LabelIdentityExportReconciler) Run(stopCh <-chan struct{}) {
+	defer r.labelQueue.ShutDown()
+
+	for i := 0; i < common.DefaultWorkerCount; i++ {
+		go wait.Until(r.labelQueueWorker, time.Second, stopCh)
+	}
+	<-stopCh
+}
+
+func (r *LabelIdentityExportReconciler) labelQueueWorker() {
+	for r.processLabelForResourceImport() {
+	}
+}
+
+// Processes an item in the labelQueue. If syncLabelResourceImport returns an error,
+// this function handles it by re-queuing the item so that it can be processed again
+// later. If syncLabelResourceExport is successful, the label is removed from the queue
+// until we get notified of a new change.
+func (r *LabelIdentityExportReconciler) processLabelForResourceImport() bool {
+	key, quit := r.labelQueue.Get()
+	if quit {
+		return false
+	}
+	defer r.labelQueue.Done(key)
+	err := r.syncLabelResourceImport(key.(string))
+	if err != nil {
+		// Put the item back on the workqueue to handle any transient errors.
+		r.labelQueue.AddRateLimited(key)
+		klog.ErrorS(err, "Failed to sync ResourceImport for label identity", "label", key)
+		return true
+	}
+	// If no error occurs, we forget this item so that it does not get queued again until
+	// another change happens.
+	r.labelQueue.Forget(key)
+	return true
+}
+
+// syncLabelResourceImport checks the label cache and determines whether a ResourceImport
+// needs to be created or deleted for the queued label hash.
+func (r *LabelIdentityExportReconciler) syncLabelResourceImport(labelHash string) error {
+	r.mutex.RLock()
+	normalizedLabel, exists := r.hashToLabels[labelHash]
+	r.mutex.RUnlock()
+	if exists {
+		if err := r.handleLabelIdentityAdd(ctx, labelHash, normalizedLabel); err != nil {
+			return err
+		}
+	} else {
+		// If a label hash does not exist in hashToLabels, it means no cluster in the
+		// ClusterSet still has the corresponding label identity.
+		if err := r.handleLabelIdentityDelete(ctx, labelHash); err != nil {
+			klog.ErrorS(err, "Failed to delete LabelIdentity kind of ResourceImport for stale label", "label", normalizedLabel)
+			return err
+		}
+	}
+	return nil
+}
+
+// handleLabelIdentityDelete deletes the ResourceImport of a label identity hash that no longer exists
+// in the ClusterSet. Note that the ID of a label identity hash is only released if the deletion of its
+// corresponding ResourceImport succeeded.
+func (r *LabelIdentityExportReconciler) handleLabelIdentityDelete(ctx context.Context, deletedLabelHash string) error {
+	labelIdentityImport := &mcsv1alpha1.ResourceImport{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      deletedLabelHash,
+			Namespace: r.namespace,
+		},
+	}
+	if err := r.Client.Delete(ctx, labelIdentityImport, &client.DeleteOptions{}); err != nil {
+		return err
+	}
+	// Delete caches for the label ID mapping and release the ID assigned for the label
+	id := r.labelsToID[deletedLabelHash]
+	r.allocator.release(id)
+	delete(r.labelsToID, deletedLabelHash)
+	return nil
+}
+
+// handleLabelIdentityAdd creates ResourceImport of a label identity that is added in the ClusterSet.
+// Note that the ID of a label identity is only allocated and stored if the creation of its corresponding
+// ResourceImport succeeded.
+func (r *LabelIdentityExportReconciler) handleLabelIdentityAdd(ctx context.Context, labelHash, label string) error {
+	labelIdentityResImport := &mcsv1alpha1.ResourceImport{}
+	ResImpKey := types.NamespacedName{Name: labelHash, Namespace: r.namespace}
+	err := r.Client.Get(ctx, ResImpKey, labelIdentityResImport)
+	if err == nil {
+		// ResourceImport for this label hash could already exist if the reconciler restarted
+		// and has an outdated cache. In that case, simply restore the ID originally assigned
+		// for the label hash.
+		idPreviouslyAllocated := labelIdentityResImport.Spec.LabelIdentity.ID
+		if err := r.allocator.setAllocated(idPreviouslyAllocated); err == nil {
+			r.labelsToID[labelHash] = idPreviouslyAllocated
+			return nil
+		} else {
+			if err := r.Client.Delete(ctx, labelIdentityResImport, &client.DeleteOptions{}); err != nil {
+				klog.ErrorS(err, "Failed to delete outdated LabelIdentity kind of ResourceImport for label", "label", label)
+				return err
+			}
+			// Continue with the normal id allocation process.
+		}
+	}
+	id, err := r.allocator.allocate()
+	if err != nil {
+		klog.ErrorS(err, "Failed to allocate ID for new label", "label", label)
+		return err
+	}
+	labelIdentityResImport = getLabelIdentityResImport(labelHash, label, r.namespace, id)
+	if err = r.Client.Create(ctx, labelIdentityResImport, &client.CreateOptions{}); err != nil {
+		klog.ErrorS(err, "Failed to create LabelIdentity kind of ResourceImport for new label", "label", label)
+		r.allocator.release(id)
+		return err
+	}
+	r.labelsToID[labelHash] = id
+	return nil
+}
+
+func getLabelIdentityResImport(labelHash, label, ns string, id uint32) *mcsv1alpha1.ResourceImport {
+	return &mcsv1alpha1.ResourceImport{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      labelHash,
+			Namespace: ns,
+		},
+		Spec: mcsv1alpha1.ResourceImportSpec{
+			Kind: common.LabelIdentityKind,
+			LabelIdentity: &mcsv1alpha1.LabelIdentitySpec{
+				Label: label,
+				ID:    id,
+			},
+		},
+	}
+}
+
+// parseLabelIdentityExportNamespacedName gets the clusterID and label identity
+// hash from the API request.
+func parseLabelIdentityExportNamespacedName(namespacedName types.NamespacedName) (string, string) {
+	lastIdx := strings.LastIndex(namespacedName.Name, "-")
+	clusterID := namespacedName.Name[:lastIdx]
+	labelHash := namespacedName.Name[lastIdx+1:]
+	return clusterID, labelHash
+}
+
+// idAllocator allocates an unqiue uint32 ID for each label identity.
+type idAllocator struct {
+	sync.Mutex
+	maxID                  uint32
+	nextID                 uint32
+	previouslyAllocatedIDs intsets.Sparse
+	releasedIDs            intsets.Sparse
+}
+
+// allocate will first try to allocate an ID within the pool of IDs that has been
+// released (due to label identity deletions). If there's no such IDs, it will
+// then allocate the first ID that has not been pre-allocated, or return an error
+// if all IDs have been exhausted.
+func (a *idAllocator) allocate() (uint32, error) {
+	a.Lock()
+	defer a.Unlock()
+
+	available := -1
+	if ok := a.releasedIDs.TakeMin(&available); ok {
+		return uint32(available), nil
+	}
+	for a.previouslyAllocatedIDs.Has(int(a.nextID)) {
+		a.nextID += 1
+	}
+	if a.nextID <= a.maxID {
+		allocated := a.nextID
+		a.nextID += 1
+		return allocated, nil
+	}
+	return 0, fmt.Errorf("no ID available")
+}
+
+// setAllocated reserves IDs allocated during the previous round of label identity
+// ResourceExport reconcilaion, before controller restarted.
+func (a *idAllocator) setAllocated(id uint32) error {
+	a.Lock()
+	defer a.Unlock()
+
+	if a.releasedIDs.Has(int(id)) {
+		a.releasedIDs.Remove(int(id))
+		return nil
+	} else if id >= a.nextID {
+		a.previouslyAllocatedIDs.Insert(int(id))
+		return nil
+	}
+	return fmt.Errorf("ID %d has already been allocated", id)
+}
+
+func (a *idAllocator) release(id uint32) {
+	a.Lock()
+	defer a.Unlock()
+
+	a.releasedIDs.Insert(int(id))
+	if a.previouslyAllocatedIDs.Has(int(a.nextID)) {
+		a.previouslyAllocatedIDs.Remove(int(id))
+	}
+}
+
+func newIDAllocator(minID, maxID uint32) *idAllocator {
+	preAllocated, availableIDs := intsets.Sparse{}, intsets.Sparse{}
+	return &idAllocator{
+		nextID:                 minID,
+		maxID:                  maxID,
+		previouslyAllocatedIDs: preAllocated,
+		releasedIDs:            availableIDs,
+	}
+}
diff --git a/multicluster/controllers/multicluster/resourceexport_controller.go b/multicluster/controllers/multicluster/resourceexport_controller.go
index b31321aaa..47f738265 100644
--- a/multicluster/controllers/multicluster/resourceexport_controller.go
+++ b/multicluster/controllers/multicluster/resourceexport_controller.go
@@ -65,9 +65,9 @@ func NewResourceExportReconciler(
 	return reconciler
 }
 
-//+kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports,verbs=get;list;watch;create;update;patch;delete
-//+kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports/status,verbs=get;update;patch
-//+kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports/finalizers,verbs=update
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports/finalizers,verbs=update
 
 // Reconcile is part of the main kubernetes reconciliation loop which aims to
 // move the current state of the cluster closer to the desired state.
@@ -470,7 +470,16 @@ func (r *ResourceExportReconciler) deleteResourceExport(resExport *mcsv1alpha1.R
 // SetupWithManager sets up the controller with the Manager.
 func (r *ResourceExportReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	// Ignore status update event via GenerationChangedPredicate
-	instance := predicate.GenerationChangedPredicate{}
+	generationPredicate := predicate.GenerationChangedPredicate{}
+	// Register this controller to ignore LabelIdentity kind of ResourceExport
+	labelIdentityResExportFilter := func(object client.Object) bool {
+		if resExport, ok := object.(*mcsv1alpha1.ResourceExport); ok {
+			return resExport.Spec.Kind != common.LabelIdentityKind
+		}
+		return false
+	}
+	labelIdentityResExportPredicate := predicate.NewPredicateFuncs(labelIdentityResExportFilter)
+	instance := predicate.And(generationPredicate, labelIdentityResExportPredicate)
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&mcsv1alpha1.ResourceExport{}).
 		WithEventFilter(instance).
diff --git a/multicluster/controllers/multicluster/stale_controller.go b/multicluster/controllers/multicluster/stale_controller.go
index a0512d64a..14137df2d 100644
--- a/multicluster/controllers/multicluster/stale_controller.go
+++ b/multicluster/controllers/multicluster/stale_controller.go
@@ -22,6 +22,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/util/workqueue"
@@ -42,8 +43,8 @@ const (
 	memberClusterAnnounceStaleTime = 5 * time.Minute
 )
 
-// StaleResCleanupController will clean up ServiceImport, MC Service, ACNP, ClusterInfoImport resources
-// if no corresponding ResourceImports in the leader cluster and remove stale ResourceExports
+// StaleResCleanupController will clean up ServiceImport, MC Service, ACNP, ClusterInfoImport and LabelIdentity
+// resources if no corresponding ResourceImports in the leader cluster and remove stale ResourceExports
 // in the leader cluster if no corresponding ServiceExport or Gateway in the member cluster when it runs in
 // the member cluster.
 // It will clean up stale MemberClusterAnnounce resources in the leader cluster if no corresponding member
@@ -77,10 +78,10 @@ func NewStaleResCleanupController(
 	return reconciler
 }
 
-//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;delete
-//+kubebuilder:rbac:groups=multicluster.x-k8s.io,resources=serviceimports,verbs=get;list;watch;delete
-//+kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceimports,verbs=get;list;watch;
-//+kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports,verbs=get;list;watch;delete
+// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;delete
+// +kubebuilder:rbac:groups=multicluster.x-k8s.io,resources=serviceimports,verbs=get;list;watch;delete
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceimports,verbs=get;list;watch;
+// +kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports,verbs=get;list;watch;delete
 
 func (c *StaleResCleanupController) cleanup() error {
 	switch c.clusterRole {
@@ -109,13 +110,20 @@ func (c *StaleResCleanupController) cleanupStaleResourcesOnMember() error {
 		return err
 	}
 	if err := c.cleanupStaleServiceResources(commonArea, resImpList); err != nil {
+		klog.ErrorS(err, "Failed to cleanup stale imported Services")
 		return err
 	}
-
+	// Cleanup any imported ACNPs that do not have corresponding ResourceImport anymore
 	if err := c.cleanupACNPResources(resImpList); err != nil {
+		klog.ErrorS(err, "Failed to cleanup stale imported ACNPs")
 		return err
 	}
 	if err := c.cleanupClusterInfoImport(resImpList); err != nil {
+		klog.ErrorS(err, "Failed to cleanup stale ClusterInfoImports")
+		return err
+	}
+	if err := c.cleanupLabelIdentities(resImpList); err != nil {
+		klog.ErrorS(err, "Failed to cleanup stale imported LabelIdentities")
 		return err
 	}
 
@@ -134,6 +142,9 @@ func (c *StaleResCleanupController) cleanupStaleResourcesOnMember() error {
 	if err := c.cleanupClusterInfoResourceExport(commonArea, resExpList); err != nil {
 		return err
 	}
+	if err := c.cleanupLabelIdentityResourceExport(commonArea, resExpList); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -170,7 +181,7 @@ func (c *StaleResCleanupController) cleanupStaleServiceResources(commonArea comm
 
 	for _, staleSvc := range mcsSvcItems {
 		svc := staleSvc
-		klog.InfoS("Cleaning up stale Service", "service", klog.KObj(&svc))
+		klog.InfoS("Cleaning up stale imported Service", "service", klog.KObj(&svc))
 		if err := c.Client.Delete(ctx, &svc, &client.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
 			return err
 		}
@@ -204,7 +215,7 @@ func (c *StaleResCleanupController) cleanupACNPResources(resImpList *mcsv1alpha1
 	}
 	for _, stalePolicy := range staleMCACNPItems {
 		acnp := stalePolicy
-		klog.InfoS("Cleaning up stale ACNP", "acnp", klog.KObj(&acnp))
+		klog.InfoS("Cleaning up stale imported ACNP", "acnp", klog.KObj(&acnp))
 		if err := c.Client.Delete(ctx, &acnp, &client.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
 			return err
 		}
@@ -237,6 +248,28 @@ func (c *StaleResCleanupController) cleanupClusterInfoImport(resImpList *mcsv1al
 	return nil
 }
 
+func (c *StaleResCleanupController) cleanupLabelIdentities(resImpList *mcsv1alpha1.ResourceImportList) error {
+	labelIdentityList := &mcsv1alpha1.LabelIdentityList{}
+	if err := c.List(ctx, labelIdentityList, &client.ListOptions{}); err != nil {
+		return err
+	}
+	staleLabelIdentities := map[string]mcsv1alpha1.LabelIdentity{}
+	for _, labelIdentityObj := range labelIdentityList.Items {
+		staleLabelIdentities[labelIdentityObj.Name] = labelIdentityObj
+	}
+	for _, labelImp := range resImpList.Items {
+		delete(staleLabelIdentities, labelImp.Name)
+	}
+	for _, l := range staleLabelIdentities {
+		labelIdentity := l
+		klog.V(2).InfoS("Cleaning up stale imported LabelIdentity", "labelidentity", klog.KObj(&labelIdentity))
+		if err := c.Client.Delete(ctx, &labelIdentity, &client.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
+			return err
+		}
+	}
+	return nil
+}
+
 // cleanupServiceResourceExport removes any Service/Endpoint kind of ResourceExports when there is no
 // corresponding ServiceExport in the local cluster.
 func (c *StaleResCleanupController) cleanupServiceResourceExport(commonArea commonarea.RemoteCommonArea,
@@ -265,7 +298,50 @@ func (c *StaleResCleanupController) cleanupServiceResourceExport(commonArea comm
 
 	for _, r := range staleResExpItems {
 		re := r
-		klog.InfoS("Cleaning up ResourceExport", "ResourceExport", klog.KObj(&re))
+		klog.InfoS("Cleaning up stale ResourceExport", "ResourceExport", klog.KObj(&re))
+		if err := commonArea.Delete(ctx, &re, &client.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
+			return err
+		}
+	}
+	return nil
+}
+
+func (c *StaleResCleanupController) cleanupLabelIdentityResourceExport(commonArea commonarea.RemoteCommonArea,
+	resExpList *mcsv1alpha1.ResourceExportList) error {
+	podList, nsList := &corev1.PodList{}, &corev1.NamespaceList{}
+	if err := c.List(ctx, podList, &client.ListOptions{}); err != nil {
+		return err
+	}
+	if err := c.List(ctx, nsList, &client.ListOptions{}); err != nil {
+		return err
+	}
+	allResExpItems := resExpList.Items
+	staleResExpItems := map[string]mcsv1alpha1.ResourceExport{}
+	for _, resExp := range allResExpItems {
+		if resExp.Spec.Kind == common.LabelIdentityKind && resExp.Labels[common.SourceClusterID] == c.localClusterID {
+			staleResExpItems[resExp.Spec.LabelIdentity.NormalizedLabel] = resExp
+		}
+	}
+	nsLabelMap := map[string]string{}
+	for _, ns := range nsList.Items {
+		if _, ok := ns.Labels[corev1.LabelMetadataName]; !ok {
+			// NamespaceDefaultLabelName is supported from K8s v1.21. For K8s versions before v1.21,
+			// we append the Namespace name label to the Namespace label set.
+			ns.Labels[corev1.LabelMetadataName] = ns.Name
+		}
+		nsLabelMap[ns.Name] = "namespace:" + labels.FormatLabels(ns.Labels)
+	}
+	for _, p := range podList.Items {
+		podNSlabel, ok := nsLabelMap[p.Namespace]
+		if !ok {
+			continue
+		}
+		normalizedLabel := podNSlabel + "&pod:" + labels.FormatLabels(p.Labels)
+		delete(staleResExpItems, normalizedLabel)
+	}
+	for _, r := range staleResExpItems {
+		re := r
+		klog.InfoS("Cleaning up stale ResourceExport", "ResourceExport", klog.KObj(&re))
 		if err := commonArea.Delete(ctx, &re, &client.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
 			return err
 		}
diff --git a/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/fake_labelidentity.go b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/fake_labelidentity.go
new file mode 100644
index 000000000..0d695b323
--- /dev/null
+++ b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/fake_labelidentity.go
@@ -0,0 +1,119 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeLabelIdentities implements LabelIdentityInterface
+type FakeLabelIdentities struct {
+	Fake *FakeMulticlusterV1alpha1
+}
+
+var labelidentitiesResource = schema.GroupVersionResource{Group: "multicluster.crd.antrea.io", Version: "v1alpha1", Resource: "labelidentities"}
+
+var labelidentitiesKind = schema.GroupVersionKind{Group: "multicluster.crd.antrea.io", Version: "v1alpha1", Kind: "LabelIdentity"}
+
+// Get takes name of the labelIdentity, and returns the corresponding labelIdentity object, and an error if there is any.
+func (c *FakeLabelIdentities) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LabelIdentity, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootGetAction(labelidentitiesResource, name), &v1alpha1.LabelIdentity{})
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.LabelIdentity), err
+}
+
+// List takes label and field selectors, and returns the list of LabelIdentities that match those selectors.
+func (c *FakeLabelIdentities) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LabelIdentityList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootListAction(labelidentitiesResource, labelidentitiesKind, opts), &v1alpha1.LabelIdentityList{})
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.LabelIdentityList{ListMeta: obj.(*v1alpha1.LabelIdentityList).ListMeta}
+	for _, item := range obj.(*v1alpha1.LabelIdentityList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested labelIdentities.
+func (c *FakeLabelIdentities) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewRootWatchAction(labelidentitiesResource, opts))
+}
+
+// Create takes the representation of a labelIdentity and creates it.  Returns the server's representation of the labelIdentity, and an error, if there is any.
+func (c *FakeLabelIdentities) Create(ctx context.Context, labelIdentity *v1alpha1.LabelIdentity, opts v1.CreateOptions) (result *v1alpha1.LabelIdentity, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootCreateAction(labelidentitiesResource, labelIdentity), &v1alpha1.LabelIdentity{})
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.LabelIdentity), err
+}
+
+// Update takes the representation of a labelIdentity and updates it. Returns the server's representation of the labelIdentity, and an error, if there is any.
+func (c *FakeLabelIdentities) Update(ctx context.Context, labelIdentity *v1alpha1.LabelIdentity, opts v1.UpdateOptions) (result *v1alpha1.LabelIdentity, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootUpdateAction(labelidentitiesResource, labelIdentity), &v1alpha1.LabelIdentity{})
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.LabelIdentity), err
+}
+
+// Delete takes name of the labelIdentity and deletes it. Returns an error if one occurs.
+func (c *FakeLabelIdentities) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewRootDeleteActionWithOptions(labelidentitiesResource, name, opts), &v1alpha1.LabelIdentity{})
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeLabelIdentities) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewRootDeleteCollectionAction(labelidentitiesResource, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.LabelIdentityList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched labelIdentity.
+func (c *FakeLabelIdentities) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LabelIdentity, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootPatchSubresourceAction(labelidentitiesResource, name, pt, data, subresources...), &v1alpha1.LabelIdentity{})
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.LabelIdentity), err
+}
diff --git a/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/fake_multicluster_client.go b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/fake_multicluster_client.go
index f2e4e085d..ea6f25db6 100644
--- a/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/fake_multicluster_client.go
+++ b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/fake_multicluster_client.go
@@ -37,6 +37,10 @@ func (c *FakeMulticlusterV1alpha1) Gateways(namespace string) v1alpha1.GatewayIn
 	return &FakeGateways{c, namespace}
 }
 
+func (c *FakeMulticlusterV1alpha1) LabelIdentities() v1alpha1.LabelIdentityInterface {
+	return &FakeLabelIdentities{c}
+}
+
 func (c *FakeMulticlusterV1alpha1) MemberClusterAnnounces(namespace string) v1alpha1.MemberClusterAnnounceInterface {
 	return &FakeMemberClusterAnnounces{c, namespace}
 }
diff --git a/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/generated_expansion.go b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/generated_expansion.go
index 4f2c7dff7..58cacee8b 100644
--- a/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/generated_expansion.go
+++ b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/generated_expansion.go
@@ -21,6 +21,8 @@ type ClusterSetExpansion interface{}
 
 type GatewayExpansion interface{}
 
+type LabelIdentityExpansion interface{}
+
 type MemberClusterAnnounceExpansion interface{}
 
 type ResourceExportExpansion interface{}
diff --git a/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/labelidentity.go b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/labelidentity.go
new file mode 100644
index 000000000..76633352c
--- /dev/null
+++ b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/labelidentity.go
@@ -0,0 +1,165 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	"time"
+
+	v1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	scheme "antrea.io/antrea/multicluster/pkg/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// LabelIdentitiesGetter has a method to return a LabelIdentityInterface.
+// A group's client should implement this interface.
+type LabelIdentitiesGetter interface {
+	LabelIdentities() LabelIdentityInterface
+}
+
+// LabelIdentityInterface has methods to work with LabelIdentity resources.
+type LabelIdentityInterface interface {
+	Create(ctx context.Context, labelIdentity *v1alpha1.LabelIdentity, opts v1.CreateOptions) (*v1alpha1.LabelIdentity, error)
+	Update(ctx context.Context, labelIdentity *v1alpha1.LabelIdentity, opts v1.UpdateOptions) (*v1alpha1.LabelIdentity, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.LabelIdentity, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.LabelIdentityList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LabelIdentity, err error)
+	LabelIdentityExpansion
+}
+
+// labelIdentities implements LabelIdentityInterface
+type labelIdentities struct {
+	client rest.Interface
+}
+
+// newLabelIdentities returns a LabelIdentities
+func newLabelIdentities(c *MulticlusterV1alpha1Client) *labelIdentities {
+	return &labelIdentities{
+		client: c.RESTClient(),
+	}
+}
+
+// Get takes name of the labelIdentity, and returns the corresponding labelIdentity object, and an error if there is any.
+func (c *labelIdentities) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LabelIdentity, err error) {
+	result = &v1alpha1.LabelIdentity{}
+	err = c.client.Get().
+		Resource("labelidentities").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of LabelIdentities that match those selectors.
+func (c *labelIdentities) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LabelIdentityList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1alpha1.LabelIdentityList{}
+	err = c.client.Get().
+		Resource("labelidentities").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested labelIdentities.
+func (c *labelIdentities) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Resource("labelidentities").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a labelIdentity and creates it.  Returns the server's representation of the labelIdentity, and an error, if there is any.
+func (c *labelIdentities) Create(ctx context.Context, labelIdentity *v1alpha1.LabelIdentity, opts v1.CreateOptions) (result *v1alpha1.LabelIdentity, err error) {
+	result = &v1alpha1.LabelIdentity{}
+	err = c.client.Post().
+		Resource("labelidentities").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(labelIdentity).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a labelIdentity and updates it. Returns the server's representation of the labelIdentity, and an error, if there is any.
+func (c *labelIdentities) Update(ctx context.Context, labelIdentity *v1alpha1.LabelIdentity, opts v1.UpdateOptions) (result *v1alpha1.LabelIdentity, err error) {
+	result = &v1alpha1.LabelIdentity{}
+	err = c.client.Put().
+		Resource("labelidentities").
+		Name(labelIdentity.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(labelIdentity).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the labelIdentity and deletes it. Returns an error if one occurs.
+func (c *labelIdentities) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Resource("labelidentities").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *labelIdentities) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Resource("labelidentities").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched labelIdentity.
+func (c *labelIdentities) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LabelIdentity, err error) {
+	result = &v1alpha1.LabelIdentity{}
+	err = c.client.Patch(pt).
+		Resource("labelidentities").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/multicluster_client.go b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/multicluster_client.go
index d69388084..39ed9605c 100644
--- a/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/multicluster_client.go
+++ b/multicluster/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/multicluster_client.go
@@ -28,6 +28,7 @@ type MulticlusterV1alpha1Interface interface {
 	ClusterInfoImportsGetter
 	ClusterSetsGetter
 	GatewaysGetter
+	LabelIdentitiesGetter
 	MemberClusterAnnouncesGetter
 	ResourceExportsGetter
 	ResourceImportsGetter
@@ -50,6 +51,10 @@ func (c *MulticlusterV1alpha1Client) Gateways(namespace string) GatewayInterface
 	return newGateways(c, namespace)
 }
 
+func (c *MulticlusterV1alpha1Client) LabelIdentities() LabelIdentityInterface {
+	return newLabelIdentities(c)
+}
+
 func (c *MulticlusterV1alpha1Client) MemberClusterAnnounces(namespace string) MemberClusterAnnounceInterface {
 	return newMemberClusterAnnounces(c, namespace)
 }
diff --git a/multicluster/pkg/client/informers/externalversions/generic.go b/multicluster/pkg/client/informers/externalversions/generic.go
index 39a2b8e10..c9226e3b5 100644
--- a/multicluster/pkg/client/informers/externalversions/generic.go
+++ b/multicluster/pkg/client/informers/externalversions/generic.go
@@ -57,6 +57,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1alpha1().ClusterSets().Informer()}, nil
 	case v1alpha1.SchemeGroupVersion.WithResource("gateways"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1alpha1().Gateways().Informer()}, nil
+	case v1alpha1.SchemeGroupVersion.WithResource("labelidentities"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1alpha1().LabelIdentities().Informer()}, nil
 	case v1alpha1.SchemeGroupVersion.WithResource("memberclusterannounces"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1alpha1().MemberClusterAnnounces().Informer()}, nil
 	case v1alpha1.SchemeGroupVersion.WithResource("resourceexports"):
diff --git a/multicluster/pkg/client/informers/externalversions/multicluster/v1alpha1/interface.go b/multicluster/pkg/client/informers/externalversions/multicluster/v1alpha1/interface.go
index 4abe9ddd0..ac20b6eba 100644
--- a/multicluster/pkg/client/informers/externalversions/multicluster/v1alpha1/interface.go
+++ b/multicluster/pkg/client/informers/externalversions/multicluster/v1alpha1/interface.go
@@ -27,6 +27,8 @@ type Interface interface {
 	ClusterSets() ClusterSetInformer
 	// Gateways returns a GatewayInformer.
 	Gateways() GatewayInformer
+	// LabelIdentities returns a LabelIdentityInformer.
+	LabelIdentities() LabelIdentityInformer
 	// MemberClusterAnnounces returns a MemberClusterAnnounceInformer.
 	MemberClusterAnnounces() MemberClusterAnnounceInformer
 	// ResourceExports returns a ResourceExportInformer.
@@ -61,6 +63,11 @@ func (v *version) Gateways() GatewayInformer {
 	return &gatewayInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
 }
 
+// LabelIdentities returns a LabelIdentityInformer.
+func (v *version) LabelIdentities() LabelIdentityInformer {
+	return &labelIdentityInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
+}
+
 // MemberClusterAnnounces returns a MemberClusterAnnounceInformer.
 func (v *version) MemberClusterAnnounces() MemberClusterAnnounceInformer {
 	return &memberClusterAnnounceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
diff --git a/multicluster/pkg/client/informers/externalversions/multicluster/v1alpha1/labelidentity.go b/multicluster/pkg/client/informers/externalversions/multicluster/v1alpha1/labelidentity.go
new file mode 100644
index 000000000..b5f234508
--- /dev/null
+++ b/multicluster/pkg/client/informers/externalversions/multicluster/v1alpha1/labelidentity.go
@@ -0,0 +1,86 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	time "time"
+
+	multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	versioned "antrea.io/antrea/multicluster/pkg/client/clientset/versioned"
+	internalinterfaces "antrea.io/antrea/multicluster/pkg/client/informers/externalversions/internalinterfaces"
+	v1alpha1 "antrea.io/antrea/multicluster/pkg/client/listers/multicluster/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// LabelIdentityInformer provides access to a shared informer and lister for
+// LabelIdentities.
+type LabelIdentityInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1alpha1.LabelIdentityLister
+}
+
+type labelIdentityInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// NewLabelIdentityInformer constructs a new informer for LabelIdentity type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewLabelIdentityInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredLabelIdentityInformer(client, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredLabelIdentityInformer constructs a new informer for LabelIdentity type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredLabelIdentityInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.MulticlusterV1alpha1().LabelIdentities().List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.MulticlusterV1alpha1().LabelIdentities().Watch(context.TODO(), options)
+			},
+		},
+		&multiclusterv1alpha1.LabelIdentity{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *labelIdentityInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredLabelIdentityInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *labelIdentityInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&multiclusterv1alpha1.LabelIdentity{}, f.defaultInformer)
+}
+
+func (f *labelIdentityInformer) Lister() v1alpha1.LabelIdentityLister {
+	return v1alpha1.NewLabelIdentityLister(f.Informer().GetIndexer())
+}
diff --git a/multicluster/pkg/client/listers/multicluster/v1alpha1/expansion_generated.go b/multicluster/pkg/client/listers/multicluster/v1alpha1/expansion_generated.go
index f00f3e1d0..84536171c 100644
--- a/multicluster/pkg/client/listers/multicluster/v1alpha1/expansion_generated.go
+++ b/multicluster/pkg/client/listers/multicluster/v1alpha1/expansion_generated.go
@@ -39,6 +39,10 @@ type GatewayListerExpansion interface{}
 // GatewayNamespaceLister.
 type GatewayNamespaceListerExpansion interface{}
 
+// LabelIdentityListerExpansion allows custom methods to be added to
+// LabelIdentityLister.
+type LabelIdentityListerExpansion interface{}
+
 // MemberClusterAnnounceListerExpansion allows custom methods to be added to
 // MemberClusterAnnounceLister.
 type MemberClusterAnnounceListerExpansion interface{}
diff --git a/multicluster/pkg/client/listers/multicluster/v1alpha1/labelidentity.go b/multicluster/pkg/client/listers/multicluster/v1alpha1/labelidentity.go
new file mode 100644
index 000000000..457cf1418
--- /dev/null
+++ b/multicluster/pkg/client/listers/multicluster/v1alpha1/labelidentity.go
@@ -0,0 +1,65 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// LabelIdentityLister helps list LabelIdentities.
+// All objects returned here must be treated as read-only.
+type LabelIdentityLister interface {
+	// List lists all LabelIdentities in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1alpha1.LabelIdentity, err error)
+	// Get retrieves the LabelIdentity from the index for a given name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*v1alpha1.LabelIdentity, error)
+	LabelIdentityListerExpansion
+}
+
+// labelIdentityLister implements the LabelIdentityLister interface.
+type labelIdentityLister struct {
+	indexer cache.Indexer
+}
+
+// NewLabelIdentityLister returns a new LabelIdentityLister.
+func NewLabelIdentityLister(indexer cache.Indexer) LabelIdentityLister {
+	return &labelIdentityLister{indexer: indexer}
+}
+
+// List lists all LabelIdentities in the indexer.
+func (s *labelIdentityLister) List(selector labels.Selector) (ret []*v1alpha1.LabelIdentity, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.LabelIdentity))
+	})
+	return ret, err
+}
+
+// Get retrieves the LabelIdentity from the index for a given name.
+func (s *labelIdentityLister) Get(name string) (*v1alpha1.LabelIdentity, error) {
+	obj, exists, err := s.indexer.GetByKey(name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1alpha1.Resource("labelidentity"), name)
+	}
+	return obj.(*v1alpha1.LabelIdentity), nil
+}
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index a122608c6..69a6614ad 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -747,8 +747,10 @@ func (i *Initializer) setupDefaultTunnelInterface() error {
 	}
 
 	// Enabling UDP checksum can greatly improve the performance for Geneve and
-	// VXLAN tunnels by triggering GRO on the receiver.
-	shouldEnableCsum := i.networkConfig.TunnelType == ovsconfig.GeneveTunnel || i.networkConfig.TunnelType == ovsconfig.VXLANTunnel
+	// VXLAN tunnels by triggering GRO on the receiver for old Linux kernel versions.
+	// It's not necessary for new Linux kernel versions with the following patch:
+	// https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063.
+	shouldEnableCsum := i.networkConfig.TunnelCsum && (i.networkConfig.TunnelType == ovsconfig.GeneveTunnel || i.networkConfig.TunnelType == ovsconfig.VXLANTunnel)
 
 	// Check the default tunnel port.
 	if portExists {
@@ -757,12 +759,12 @@ func (i *Initializer) setupDefaultTunnelInterface() error {
 			tunnelIface.TunnelInterfaceConfig.DestinationPort == i.networkConfig.TunnelPort &&
 			tunnelIface.TunnelInterfaceConfig.LocalIP.Equal(localIP) {
 			klog.V(2).Infof("Tunnel port %s already exists on OVS bridge", tunnelPortName)
-			// This could happen when upgrading from previous versions that didn't set it.
-			if shouldEnableCsum && !tunnelIface.TunnelInterfaceConfig.Csum {
-				if err := i.enableTunnelCsum(tunnelPortName); err != nil {
-					return fmt.Errorf("failed to enable csum for tunnel port %s: %v", tunnelPortName, err)
+			if shouldEnableCsum != tunnelIface.TunnelInterfaceConfig.Csum {
+				klog.InfoS("Updating csum for tunnel port", "port", tunnelPortName, "csum", shouldEnableCsum)
+				if err := i.setTunnelCsum(tunnelPortName, shouldEnableCsum); err != nil {
+					return fmt.Errorf("failed to update csum for tunnel port %s to %v: %v", tunnelPortName, shouldEnableCsum, err)
 				}
-				tunnelIface.TunnelInterfaceConfig.Csum = true
+				tunnelIface.TunnelInterfaceConfig.Csum = shouldEnableCsum
 			}
 			i.nodeConfig.TunnelOFPort = uint32(tunnelIface.OFPort)
 			return nil
@@ -806,15 +808,15 @@ func (i *Initializer) setupDefaultTunnelInterface() error {
 			return err
 		}
 		klog.InfoS("Allocated OpenFlow port for tunnel interface", "port", tunnelPortName, "ofPort", tunPort)
-		tunnelIface = interfacestore.NewTunnelInterface(tunnelPortName, i.networkConfig.TunnelType, i.networkConfig.TunnelPort, localIP, shouldEnableCsum)
-		tunnelIface.OVSPortConfig = &interfacestore.OVSPortConfig{PortUUID: tunnelPortUUID, OFPort: tunPort}
+		ovsPortConfig := &interfacestore.OVSPortConfig{PortUUID: tunnelPortUUID, OFPort: tunPort}
+		tunnelIface = interfacestore.NewTunnelInterface(tunnelPortName, i.networkConfig.TunnelType, i.networkConfig.TunnelPort, localIP, shouldEnableCsum, ovsPortConfig)
 		i.ifaceStore.AddInterface(tunnelIface)
 		i.nodeConfig.TunnelOFPort = uint32(tunPort)
 	}
 	return nil
 }
 
-func (i *Initializer) enableTunnelCsum(tunnelPortName string) error {
+func (i *Initializer) setTunnelCsum(tunnelPortName string, enable bool) error {
 	options, err := i.ovsBridgeClient.GetInterfaceOptions(tunnelPortName)
 	if err != nil {
 		return fmt.Errorf("error getting interface options: %w", err)
@@ -824,7 +826,7 @@ func (i *Initializer) enableTunnelCsum(tunnelPortName string) error {
 	for k, v := range options {
 		updatedOptions[k] = v
 	}
-	updatedOptions["csum"] = "true"
+	updatedOptions["csum"] = strconv.FormatBool(enable)
 	return i.ovsBridgeClient.SetInterfaceOptions(tunnelPortName, updatedOptions)
 }
 
diff --git a/pkg/agent/agent_windows.go b/pkg/agent/agent_windows.go
index 615cf41d1..054817f00 100644
--- a/pkg/agent/agent_windows.go
+++ b/pkg/agent/agent_windows.go
@@ -31,7 +31,6 @@ import (
 	"antrea.io/antrea/pkg/agent/util"
 	"antrea.io/antrea/pkg/apis/crd/v1alpha1"
 	"antrea.io/antrea/pkg/ovs/ovsctl"
-	"antrea.io/antrea/pkg/util/ip"
 	utilip "antrea.io/antrea/pkg/util/ip"
 )
 
@@ -65,7 +64,7 @@ func (i *Initializer) prepareHNSNetworkAndOVSExtension() error {
 	// Get uplink network configuration. The uplink interface is the one used for transporting Pod traffic across Nodes.
 	// Use the interface specified with "transportInterface" in the configuration if configured, otherwise the interface
 	// configured with NodeIP is used as uplink.
-	_, _, adapter, err := i.getNodeInterfaceFromIP(&ip.DualStackIPs{IPv4: i.nodeConfig.NodeTransportIPv4Addr.IP})
+	_, _, adapter, err := i.getNodeInterfaceFromIP(&utilip.DualStackIPs{IPv4: i.nodeConfig.NodeTransportIPv4Addr.IP})
 	if err != nil {
 		return err
 	}
diff --git a/pkg/agent/config/node_config.go b/pkg/agent/config/node_config.go
index d93fbf7d6..86ab00c61 100644
--- a/pkg/agent/config/node_config.go
+++ b/pkg/agent/config/node_config.go
@@ -192,6 +192,7 @@ type NetworkConfig struct {
 	TrafficEncapMode      TrafficEncapModeType
 	TunnelType            ovsconfig.TunnelType
 	TunnelPort            int32
+	TunnelCsum            bool
 	TrafficEncryptionMode TrafficEncryptionModeType
 	IPsecConfig           IPsecConfig
 	TransportIface        string
diff --git a/pkg/agent/controller/networkpolicy/audit_logging.go b/pkg/agent/controller/networkpolicy/audit_logging.go
index c29190374..51424a913 100644
--- a/pkg/agent/controller/networkpolicy/audit_logging.go
+++ b/pkg/agent/controller/networkpolicy/audit_logging.go
@@ -66,7 +66,8 @@ type AntreaPolicyLogger struct {
 // logInfo will be set by retrieving info from packetin and register.
 type logInfo struct {
 	tableName   string // name of the table sending packetin
-	npRef       string // Network Policy name reference for Antrea NetworkPolicy
+	npRef       string // Network Policy name reference
+	ruleName    string // Network Policy rule name for Antrea-native policies
 	disposition string // Allow/Drop of the rule sending packetin
 	ofPriority  string // openflow priority of the flow sending packetin
 	srcIP       string // source IP of the traffic logged
@@ -134,7 +135,7 @@ func (l *AntreaPolicyLogger) updateLogKey(logMsg string, bufferLength time.Durat
 // LogDedupPacket logs information in ob based on disposition and duplication conditions.
 func (l *AntreaPolicyLogger) LogDedupPacket(ob *logInfo) {
 	// Deduplicate non-Allow packet log.
-	logMsg := fmt.Sprintf("%s %s %s %s %s %s %s %s %s %d", ob.tableName, ob.npRef, ob.disposition, ob.ofPriority, ob.srcIP, ob.srcPort, ob.destIP, ob.destPort, ob.protocolStr, ob.pktLength)
+	logMsg := fmt.Sprintf("%s %s %s %s %s %s %s %s %s %s %d", ob.tableName, ob.npRef, ob.ruleName, ob.disposition, ob.ofPriority, ob.srcIP, ob.srcPort, ob.destIP, ob.destPort, ob.protocolStr, ob.pktLength)
 	if ob.disposition == openflow.DispositionToString[openflow.DispositionAllow] {
 		l.anpLogger.Printf(logMsg)
 	} else {
@@ -188,29 +189,51 @@ func getNetworkPolicyInfo(pktIn *ofctrl.PacketIn, c *Controller, ob *logInfo) er
 
 	// Get disposition Allow or Drop.
 	match = getMatchRegField(matchers, openflow.APDispositionField)
-	info, err := getInfoInReg(match, openflow.APDispositionField.GetRange().ToNXRange())
+	disposition, err := getInfoInReg(match, openflow.APDispositionField.GetRange().ToNXRange())
 	if err != nil {
 		return fmt.Errorf("received error while unloading disposition from reg: %v", err)
 	}
-	ob.disposition = openflow.DispositionToString[info]
+	ob.disposition = openflow.DispositionToString[disposition]
 
 	// Set match to corresponding ingress/egress reg according to disposition.
-	match = getMatch(matchers, tableID, info)
+	match = getMatch(matchers, tableID, disposition)
 
 	if match != nil {
 		// Get NetworkPolicy full name and OF priority of the conjunction.
-		info, err = getInfoInReg(match, nil)
+		conjID, err := getInfoInReg(match, nil)
 		if err != nil {
 			return fmt.Errorf("received error while unloading conjunction id from reg: %v", err)
 		}
-		ob.npRef, ob.ofPriority = c.ofClient.GetPolicyInfoFromConjunction(info)
+		ob.npRef, ob.ofPriority, ob.ruleName = c.ofClient.GetPolicyInfoFromConjunction(conjID)
+		if ob.npRef == "" || ob.ofPriority == "" {
+			return fmt.Errorf("networkpolicy not found for conjunction id: %v", conjID)
+		}
+		// Placeholder for K8s NetworkPolicies without rule names.
+		if ob.ruleName == "" {
+			ob.ruleName = "<nil>"
+		}
 	} else {
 		// For K8s NetworkPolicy implicit drop action, we cannot get Namespace/name.
-		ob.npRef, ob.ofPriority = string(v1beta2.K8sNetworkPolicy), "-1"
+		ob.npRef, ob.ofPriority, ob.ruleName = string(v1beta2.K8sNetworkPolicy), "<nil>", "<nil>"
 	}
 	return nil
 }
 
+// getPacketInfo fills in IP, packet length, protocol, port number of logInfo ob.
+func getPacketInfo(packet *binding.Packet, ob *logInfo) {
+	ob.srcIP = packet.SourceIP.String()
+	ob.destIP = packet.DestinationIP.String()
+	ob.pktLength = packet.IPLength
+	ob.protocolStr = ip.IPProtocolNumberToString(packet.IPProto, "UnknownProtocol")
+	if ob.protocolStr == "TCP" || ob.protocolStr == "UDP" {
+		ob.srcPort = strconv.Itoa(int(packet.SourcePort))
+		ob.destPort = strconv.Itoa(int(packet.DestinationPort))
+	} else {
+		// Placeholders for ICMP packets without port numbers.
+		ob.srcPort, ob.destPort = "<nil>", "<nil>"
+	}
+}
+
 // logPacket retrieves information from openflow reg, controller cache, packet-in
 // packet to log. Log is deduplicated for non-Allow packets from record in logDeduplication.
 // Deduplication is safe guarded by logRecordDedupMap mutex.
@@ -226,17 +249,7 @@ func (c *Controller) logPacket(pktIn *ofctrl.PacketIn) error {
 	if err != nil {
 		return fmt.Errorf("received error while retrieving NetworkPolicy info: %v", err)
 	}
-	ob.srcIP = packet.SourceIP.String()
-	ob.destIP = packet.DestinationIP.String()
-	ob.pktLength = packet.IPLength
-	ob.protocolStr = ip.IPProtocolNumberToString(packet.IPProto, "UnknownProtocol")
-	if ob.protocolStr == "TCP" || ob.protocolStr == "UDP" {
-		ob.srcPort = strconv.Itoa(int(packet.SourcePort))
-		ob.destPort = strconv.Itoa(int(packet.DestinationPort))
-	} else {
-		// Placeholders for ICMP packets without port numbers.
-		ob.srcPort, ob.destPort = "<nil>", "<nil>"
-	}
+	getPacketInfo(packet, ob)
 
 	// Log the ob info to corresponding file w/ deduplication.
 	c.antreaPolicyLogger.LogDedupPacket(ob)
diff --git a/pkg/agent/controller/noderoute/node_route_controller.go b/pkg/agent/controller/noderoute/node_route_controller.go
index 5194c9902..7c40d9336 100644
--- a/pkg/agent/controller/noderoute/node_route_controller.go
+++ b/pkg/agent/controller/noderoute/node_route_controller.go
@@ -704,8 +704,8 @@ func (c *Controller) createIPSecTunnelPort(nodeName string, nodeIP net.IP) (int3
 			nodeIP,
 			psk,
 			remoteName,
+			ovsPortConfig,
 		)
-		interfaceConfig.OVSPortConfig = ovsPortConfig
 		c.interfaceStore.AddInterface(interfaceConfig)
 	}
 	// GetOFPort will wait for up to 1 second for OVSDB to report the OFPort number.
@@ -746,6 +746,7 @@ func ParseTunnelInterfaceConfig(
 			remoteIP,
 			psk,
 			remoteName,
+			portConfig,
 		)
 	} else {
 		interfaceConfig = interfacestore.NewTunnelInterface(
@@ -753,9 +754,9 @@ func ParseTunnelInterfaceConfig(
 			ovsconfig.TunnelType(portData.IFType),
 			tunnelPort,
 			localIP,
-			csum)
+			csum,
+			portConfig)
 	}
-	interfaceConfig.OVSPortConfig = portConfig
 	return interfaceConfig
 }
 
diff --git a/pkg/agent/interfacestore/types.go b/pkg/agent/interfacestore/types.go
index 06b4215fa..903af7169 100644
--- a/pkg/agent/interfacestore/types.go
+++ b/pkg/agent/interfacestore/types.go
@@ -31,8 +31,6 @@ const (
 	TunnelInterface
 	// UplinkInterface is used to mark current interface is for uplink port
 	UplinkInterface
-	// HostInterface is used to mark current interface is for host
-	HostInterface
 	// TrafficControlInterface is used to mark current interface is for traffic control port
 	TrafficControlInterface
 	// ExternalEntityInterface is used to mark current interface is for ExternalEntity Endpoint
@@ -155,16 +153,16 @@ func NewGatewayInterface(gatewayName string) *InterfaceConfig {
 
 // NewTunnelInterface creates InterfaceConfig for the default tunnel port
 // interface.
-func NewTunnelInterface(tunnelName string, tunnelType ovsconfig.TunnelType, destinationPort int32, localIP net.IP, csum bool) *InterfaceConfig {
+func NewTunnelInterface(tunnelName string, tunnelType ovsconfig.TunnelType, destinationPort int32, localIP net.IP, csum bool, ovsPortConfig *OVSPortConfig) *InterfaceConfig {
 	tunnelConfig := &TunnelInterfaceConfig{Type: tunnelType, DestinationPort: destinationPort, LocalIP: localIP, Csum: csum}
-	return &InterfaceConfig{InterfaceName: tunnelName, Type: TunnelInterface, TunnelInterfaceConfig: tunnelConfig}
+	return &InterfaceConfig{InterfaceName: tunnelName, Type: TunnelInterface, TunnelInterfaceConfig: tunnelConfig, OVSPortConfig: ovsPortConfig}
 }
 
 // NewIPSecTunnelInterface creates InterfaceConfig for the IPsec tunnel to the
 // Node.
-func NewIPSecTunnelInterface(interfaceName string, tunnelType ovsconfig.TunnelType, nodeName string, nodeIP net.IP, psk, remoteName string) *InterfaceConfig {
+func NewIPSecTunnelInterface(interfaceName string, tunnelType ovsconfig.TunnelType, nodeName string, nodeIP net.IP, psk, remoteName string, ovsPortConfig *OVSPortConfig) *InterfaceConfig {
 	tunnelConfig := &TunnelInterfaceConfig{Type: tunnelType, NodeName: nodeName, RemoteIP: nodeIP, PSK: psk, RemoteName: remoteName}
-	return &InterfaceConfig{InterfaceName: interfaceName, Type: TunnelInterface, TunnelInterfaceConfig: tunnelConfig}
+	return &InterfaceConfig{InterfaceName: interfaceName, Type: TunnelInterface, TunnelInterfaceConfig: tunnelConfig, OVSPortConfig: ovsPortConfig}
 }
 
 // NewUplinkInterface creates InterfaceConfig for the uplink interface.
@@ -173,10 +171,6 @@ func NewUplinkInterface(uplinkName string) *InterfaceConfig {
 	return uplinkConfig
 }
 
-func NewHostInterface(hostInterfaceName string) *InterfaceConfig {
-	return &InterfaceConfig{InterfaceName: hostInterfaceName, Type: HostInterface}
-}
-
 func NewTrafficControlInterface(interfaceName string) *InterfaceConfig {
 	trafficControlConfig := &InterfaceConfig{InterfaceName: interfaceName, Type: TrafficControlInterface}
 	return trafficControlConfig
diff --git a/pkg/agent/multicluster/mc_route_controller.go b/pkg/agent/multicluster/mc_route_controller.go
index ff872d432..4319733b2 100644
--- a/pkg/agent/multicluster/mc_route_controller.go
+++ b/pkg/agent/multicluster/mc_route_controller.go
@@ -331,7 +331,8 @@ func (c *MCRouteController) addMCFlowsForSingleCIImp(activeGW *mcv1alpha1.Gatewa
 
 	if installedCIImp != nil {
 		oldTunnelPeerIPToRemoteGW := getPeerGatewayIP(installedCIImp.Spec)
-		if oldTunnelPeerIPToRemoteGW.Equal(tunnelPeerIPToRemoteGW) && installedCIImp.Spec.ServiceCIDR == ciImport.Spec.ServiceCIDR {
+		if oldTunnelPeerIPToRemoteGW.Equal(tunnelPeerIPToRemoteGW) && installedCIImp.Spec.ServiceCIDR == ciImport.Spec.ServiceCIDR &&
+			sets.NewString(installedCIImp.Spec.PodCIDRs...).Equal(sets.NewString(ciImport.Spec.PodCIDRs...)) {
 			klog.V(2).InfoS("No difference between new and installed ClusterInfoImports, skip updating", "clusterinfoimport", ciImport.Name)
 			return nil
 		}
@@ -339,7 +340,7 @@ func (c *MCRouteController) addMCFlowsForSingleCIImp(activeGW *mcv1alpha1.Gatewa
 
 	klog.InfoS("Adding/updating remote Gateway Node flows for Multi-cluster", "gateway", klog.KObj(activeGW),
 		"node", c.nodeConfig.Name, "peer", tunnelPeerIPToRemoteGW)
-	allCIDRs := []string{ciImport.Spec.ServiceCIDR}
+	allCIDRs := append([]string{ciImport.Spec.ServiceCIDR}, ciImport.Spec.PodCIDRs...)
 	peerConfigs, err := generatePeerConfigs(allCIDRs, tunnelPeerIPToRemoteGW)
 	if err != nil {
 		klog.ErrorS(err, "Parse error for serviceCIDR from remote cluster", "clusterinfoimport", ciImport.Name, "gateway", activeGW.Name)
diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go
index 034f7471b..1a61ca5ca 100644
--- a/pkg/agent/openflow/client.go
+++ b/pkg/agent/openflow/client.go
@@ -211,7 +211,7 @@ type Client interface {
 	InitialTLVMap() error
 
 	// Find Network Policy reference and OFpriority by conjunction ID.
-	GetPolicyInfoFromConjunction(ruleID uint32) (string, string)
+	GetPolicyInfoFromConjunction(ruleID uint32) (string, string, string)
 
 	// RegisterPacketInHandler uses SubscribePacketIn to get PacketIn message and process received
 	// packets through registered handlers.
@@ -1220,6 +1220,9 @@ func (c *client) InstallMulticastInitialFlows(pktInReason uint8) error {
 	flows := c.featureMulticast.igmpPktInFlows(pktInReason)
 	flows = append(flows, c.featureMulticast.externalMulticastReceiverFlow())
 	flows = append(flows, c.featureMulticast.multicastSkipIGMPMetricFlows()...)
+	if c.enableAntreaPolicy {
+		flows = append(flows, c.featureMulticast.igmpEgressFlow())
+	}
 	cacheKey := "multicast"
 	c.replayMutex.RLock()
 	defer c.replayMutex.RUnlock()
diff --git a/pkg/agent/openflow/framework.go b/pkg/agent/openflow/framework.go
index f4a24d945..76d006594 100644
--- a/pkg/agent/openflow/framework.go
+++ b/pkg/agent/openflow/framework.go
@@ -216,12 +216,10 @@ func (f *featureNetworkPolicy) getRequiredTables() []*Table {
 		if f.enableMulticast {
 			tables = append(tables,
 				MulticastEgressRuleTable,
+				MulticastEgressPodMetricTable,
 				MulticastEgressMetricTable,
 				MulticastIngressRuleTable,
 				MulticastIngressPodMetricTable,
-				MulticastEgressRuleTable,
-				MulticastEgressMetricTable,
-				MulticastEgressPodMetricTable,
 				MulticastIngressMetricTable,
 			)
 		}
diff --git a/pkg/agent/openflow/network_policy.go b/pkg/agent/openflow/network_policy.go
index 0d6d0f819..784bddbcd 100644
--- a/pkg/agent/openflow/network_policy.go
+++ b/pkg/agent/openflow/network_policy.go
@@ -639,6 +639,7 @@ type policyRuleConjunction struct {
 	// 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
+	ruleName    string
 	ruleTableID uint8
 }
 
@@ -1109,8 +1110,9 @@ func (f *featureNetworkPolicy) calculateActionFlowChangesForRule(rule *types.Pol
 		return nil
 	}
 	conj = &policyRuleConjunction{
-		id:    ruleOfID,
-		npRef: rule.PolicyRef,
+		id:       ruleOfID,
+		npRef:    rule.PolicyRef,
+		ruleName: rule.Name,
 	}
 	nClause, ruleTable, dropTable := conj.calculateClauses(rule)
 	conj.ruleTableID = rule.TableID
@@ -1450,16 +1452,16 @@ func (f *featureNetworkPolicy) getPolicyRuleConjunction(ruleID uint32) *policyRu
 	return conj.(*policyRuleConjunction)
 }
 
-func (c *client) GetPolicyInfoFromConjunction(ruleID uint32) (string, string) {
+func (c *client) GetPolicyInfoFromConjunction(ruleID uint32) (string, string, string) {
 	conjunction := c.featureNetworkPolicy.getPolicyRuleConjunction(ruleID)
 	if conjunction == nil {
-		return "", ""
+		return "", "", ""
 	}
 	priorities := conjunction.ActionFlowPriorities()
 	if len(priorities) == 0 {
-		return "", ""
+		return "", "", ""
 	}
-	return conjunction.npRef.ToString(), priorities[0]
+	return conjunction.npRef.ToString(), priorities[0], conjunction.ruleName
 }
 
 // UninstallPolicyRuleFlows removes the Openflow entry relevant to the specified NetworkPolicy rule.
@@ -1709,6 +1711,7 @@ func (f *featureNetworkPolicy) updateConjunctionActionFlows(conj *policyRuleConj
 		serviceClause: conj.serviceClause,
 		actionFlows:   newActionFlows,
 		npRef:         conj.npRef,
+		ruleName:      conj.ruleName,
 		ruleTableID:   conj.ruleTableID,
 	}
 	return newConj
diff --git a/pkg/agent/openflow/pipeline.go b/pkg/agent/openflow/pipeline.go
index 5fa6317d8..6239b254d 100644
--- a/pkg/agent/openflow/pipeline.go
+++ b/pkg/agent/openflow/pipeline.go
@@ -2579,6 +2579,18 @@ func pipelineClassifyFlow(cookieID uint64, protocol binding.Protocol, pipeline b
 		Done()
 }
 
+// igmpEgressFlow generates flows to match IGMP report to jump to table MulticastRoutingTable.
+// This is because normal multicast egress rule can match IGMP v1 report, when there is egress
+// rule to block multicast traffic, IGMP v1 report will also be blocked, which is not expected.
+func (f *featureMulticast) igmpEgressFlow() binding.Flow {
+	return MulticastEgressRuleTable.ofTable.BuildFlow(priorityTopAntreaPolicy).
+		Cookie(f.cookieAllocator.Request(f.category).Raw()).
+		MatchProtocol(binding.ProtocolIGMP).
+		MatchRegMark(FromLocalRegMark).
+		Action().GotoStage(stageRouting).
+		Done()
+}
+
 // igmpPktInFlows generates the flow to load CustomReasonIGMPRegMark to mark the IGMP packet in MulticastRoutingTable
 // and sends it to antrea-agent.
 func (f *featureMulticast) igmpPktInFlows(reason uint8) []binding.Flow {
diff --git a/pkg/agent/proxy/proxier.go b/pkg/agent/proxy/proxier.go
index 036124794..8394e24fc 100644
--- a/pkg/agent/proxy/proxier.go
+++ b/pkg/agent/proxy/proxier.go
@@ -27,7 +27,6 @@ import (
 	discovery "k8s.io/api/discovery/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	apimachinerytypes "k8s.io/apimachinery/pkg/types"
-	k8sapitypes "k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/informers"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/klog/v2"
@@ -863,7 +862,7 @@ func (p *proxier) GetProxyProvider() k8sproxy.Provider {
 }
 
 func (p *proxier) GetServiceFlowKeys(serviceName, namespace string) ([]string, []binding.GroupIDType, bool) {
-	namespacedName := k8sapitypes.NamespacedName{Namespace: namespace, Name: serviceName}
+	namespacedName := apimachinerytypes.NamespacedName{Namespace: namespace, Name: serviceName}
 	p.serviceEndpointsMapsMutex.Lock()
 	defer p.serviceEndpointsMapsMutex.Unlock()
 
diff --git a/pkg/config/agent/config.go b/pkg/config/agent/config.go
index 33bf44dba..5a8e47418 100644
--- a/pkg/config/agent/config.go
+++ b/pkg/config/agent/config.go
@@ -71,6 +71,14 @@ type AgentConfig struct {
 	// If zero, it will use the assigned IANA port for the protocol, i.e. 6081 for Geneve, 4789 for VXLAN,
 	// and 7471 for STT.
 	TunnelPort int32 `yaml:"tunnelPort,omitempty"`
+	// TunnelCsum determines whether to compute UDP encapsulation header (Geneve or VXLAN) checksums on outgoing
+	// packets. For Linux kernel before Mar 2021, UDP checksum must be present to trigger GRO on the receiver for better
+	// performance of Geneve and VXLAN tunnels. The issue has been fixed by
+	// https://github.com/torvalds/linux/commit/89e5c58fc1e2857ccdaae506fb8bc5fed57ee063, thus computing UDP checksum is
+	// no longer necessary.
+	// Default is false. It should only be set to true when you are using an unpatched Linux kernel and observing poor
+	// transfer performance.
+	TunnelCsum bool `yaml:"tunnelCsum,omitempty"`
 	// 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/pkg/controller/certificatesigningrequest/ipsec_csr_signing_controller.go b/pkg/controller/certificatesigningrequest/ipsec_csr_signing_controller.go
index e6fc51bc2..2f2e847d7 100644
--- a/pkg/controller/certificatesigningrequest/ipsec_csr_signing_controller.go
+++ b/pkg/controller/certificatesigningrequest/ipsec_csr_signing_controller.go
@@ -38,7 +38,6 @@ import (
 	corev1informers "k8s.io/client-go/informers/core/v1"
 	clientset "k8s.io/client-go/kubernetes"
 	csrlister "k8s.io/client-go/listers/certificates/v1"
-	csrlisters "k8s.io/client-go/listers/certificates/v1"
 	corev1listers "k8s.io/client-go/listers/core/v1"
 	"k8s.io/client-go/tools/cache"
 	certutil "k8s.io/client-go/util/cert"
@@ -110,7 +109,7 @@ func (c *certificateAuthority) signCSR(template *x509.Certificate, requestKey cr
 }
 
 // NewIPsecCSRSigningController returns a new *IPsecCSRSigningController.
-func NewIPsecCSRSigningController(client clientset.Interface, csrInformer cache.SharedIndexInformer, csrLister csrlisters.CertificateSigningRequestLister, selfSignedCA bool) *IPsecCSRSigningController {
+func NewIPsecCSRSigningController(client clientset.Interface, csrInformer cache.SharedIndexInformer, csrLister csrlister.CertificateSigningRequestLister, selfSignedCA bool) *IPsecCSRSigningController {
 
 	caConfigMapInformer := corev1informers.NewFilteredConfigMapInformer(client, env.GetAntreaNamespace(), resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, func(listOptions *metav1.ListOptions) {
 		listOptions.FieldSelector = fields.OneTermEqualSelector("metadata.name", ipsecRootCAName).String()
diff --git a/pkg/controller/serviceexternalip/controller.go b/pkg/controller/serviceexternalip/controller.go
index 12ebb858e..e8276bffa 100644
--- a/pkg/controller/serviceexternalip/controller.go
+++ b/pkg/controller/serviceexternalip/controller.go
@@ -24,7 +24,6 @@ import (
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
-	v1 "k8s.io/api/core/v1"
 	apimachineryerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -182,7 +181,7 @@ func (c *ServiceExternalIPController) restoreIPAllocations(services []*corev1.Se
 		}
 		ip := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 		allocation := externalippool.IPAllocation{
-			ObjectReference: v1.ObjectReference{
+			ObjectReference: corev1.ObjectReference{
 				Name:      svc.Name,
 				Namespace: svc.Namespace,
 				Kind:      svc.Kind,
diff --git a/plugins/octant/go.mod b/plugins/octant/go.mod
index 5dfe3c3bc..00a193fd9 100644
--- a/plugins/octant/go.mod
+++ b/plugins/octant/go.mod
@@ -112,7 +112,7 @@ require (
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect
-	google.golang.org/grpc v1.49.0 // indirect
+	google.golang.org/grpc v1.50.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
diff --git a/plugins/octant/go.sum b/plugins/octant/go.sum
index 106c186a6..ab8159aea 100644
--- a/plugins/octant/go.sum
+++ b/plugins/octant/go.sum
@@ -666,7 +666,7 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
-github.com/onsi/gomega v1.20.2 h1:8uQq0zMgLEfa0vRrrBgaJF2gyW9Da9BmfGV+OyUzfkY=
+github.com/onsi/gomega v1.21.1 h1:OB/euWYIExnPBohllTicTHmGTrMaqJ67nIu80j0/uEM=
 github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@@ -1342,8 +1342,8 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
 google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
-google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
+google.golang.org/grpc v1.50.0 h1:fPVVDxY9w++VjTZsYvXWqEf9Rqar/e+9zYfxKK+W+YU=
+google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=


Test patch:
diff --git a/multicluster/cmd/multicluster-controller/options_test.go b/multicluster/cmd/multicluster-controller/options_test.go
new file mode 100644
index 000000000..0d43cb78f
--- /dev/null
+++ b/multicluster/cmd/multicluster-controller/options_test.go
@@ -0,0 +1,79 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	ctrl "sigs.k8s.io/controller-runtime"
+)
+
+func TestComplete(t *testing.T) {
+	testCases := []struct {
+		name       string
+		o          Options
+		configFile string
+		exceptdErr error
+	}{
+		{
+			name: "options with valid PodCIDRs",
+			o: Options{
+				configFile:          "./testdata/antrea-mc-config-with-valid-podcidrs.yml",
+				SelfSignedCert:      false,
+				options:             ctrl.Options{},
+				ServiceCIDR:         "",
+				PodCIDRs:            nil,
+				GatewayIPPrecedence: "",
+				EndpointIPType:      "",
+			},
+			exceptdErr: nil,
+		},
+		{
+			name: "options with empty PodCIDRs",
+			o: Options{
+				configFile:          "./testdata/antrea-mc-config-with-empty-podcidrs.yml",
+				SelfSignedCert:      false,
+				options:             ctrl.Options{},
+				ServiceCIDR:         "",
+				PodCIDRs:            nil,
+				GatewayIPPrecedence: "",
+				EndpointIPType:      "",
+			},
+			exceptdErr: nil,
+		},
+		{
+			name: "options without PodCIDRs",
+			o: Options{
+				configFile:          "./testdata/antrea-mc-config-with-invalid-podcidrs.yml",
+				SelfSignedCert:      false,
+				options:             ctrl.Options{},
+				ServiceCIDR:         "10.100.0.0/16",
+				PodCIDRs:            nil,
+				GatewayIPPrecedence: "",
+				EndpointIPType:      "",
+			},
+			exceptdErr: fmt.Errorf("failed to parse podCIDRs, invalid CIDR string 10.10a.0.0/16"),
+		},
+	}
+
+	for _, tt := range testCases {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.o.complete(nil)
+			assert.Equal(t, tt.exceptdErr, err)
+		})
+	}
+}
diff --git a/multicluster/cmd/multicluster-controller/testdata/antrea-mc-config-with-empty-podcidrs.yml b/multicluster/cmd/multicluster-controller/testdata/antrea-mc-config-with-empty-podcidrs.yml
new file mode 100644
index 000000000..decbb468c
--- /dev/null
+++ b/multicluster/cmd/multicluster-controller/testdata/antrea-mc-config-with-empty-podcidrs.yml
@@ -0,0 +1,15 @@
+apiVersion: multicluster.crd.antrea.io/v1alpha1
+kind: MultiClusterConfig
+health:
+  healthProbeBindAddress: :8080
+metrics:
+  bindAddress: "0"
+webhook:
+  port: 9443
+leaderElection:
+  leaderElect: false
+serviceCIDR: ""
+podCIDRs:
+  - ""
+gatewayIPPrecedence: "private"
+endpointIPType: "ClusterIP"
diff --git a/multicluster/cmd/multicluster-controller/testdata/antrea-mc-config-with-invalid-podcidrs.yml b/multicluster/cmd/multicluster-controller/testdata/antrea-mc-config-with-invalid-podcidrs.yml
new file mode 100644
index 000000000..5a8401735
--- /dev/null
+++ b/multicluster/cmd/multicluster-controller/testdata/antrea-mc-config-with-invalid-podcidrs.yml
@@ -0,0 +1,16 @@
+apiVersion: multicluster.crd.antrea.io/v1alpha1
+kind: MultiClusterConfig
+health:
+  healthProbeBindAddress: :8080
+metrics:
+  bindAddress: "0"
+webhook:
+  port: 9443
+leaderElection:
+  leaderElect: false
+serviceCIDR: "10.100.0.0/16"
+podCIDRs:
+  - "10.10a.0.0/16"
+  - ""
+gatewayIPPrecedence: "private"
+endpointIPType: "ClusterIP"
diff --git a/multicluster/cmd/multicluster-controller/testdata/antrea-mc-config-with-valid-podcidrs.yml b/multicluster/cmd/multicluster-controller/testdata/antrea-mc-config-with-valid-podcidrs.yml
new file mode 100644
index 000000000..0638f8f28
--- /dev/null
+++ b/multicluster/cmd/multicluster-controller/testdata/antrea-mc-config-with-valid-podcidrs.yml
@@ -0,0 +1,16 @@
+apiVersion: multicluster.crd.antrea.io/v1alpha1
+kind: MultiClusterConfig
+health:
+  healthProbeBindAddress: :8080
+metrics:
+  bindAddress: "0"
+webhook:
+  port: 9443
+leaderElection:
+  leaderElect: false
+serviceCIDR: ""
+podCIDRs:
+  - "10.10.0.0/16"
+  - ""
+gatewayIPPrecedence: "private"
+endpointIPType: "ClusterIP"
diff --git a/multicluster/controllers/multicluster/common/helper_test.go b/multicluster/controllers/multicluster/common/helper_test.go
new file mode 100644
index 000000000..65dfa618a
--- /dev/null
+++ b/multicluster/controllers/multicluster/common/helper_test.go
@@ -0,0 +1,30 @@
+/*
+Copyright 2022 Antrea Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package common
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestHashLabelIdentity(t *testing.T) {
+	normalizedLabelIdenity := "ns:kubernetes.io/metadata.name=us-west-1,purpose=test&pod:app=client"
+	expHashedLabel := "c5d961e1f6175a5a"
+	actHashLabel := HashLabelIdentity(normalizedLabelIdenity)
+	assert.Equal(t, expHashedLabel, actHashLabel)
+}
diff --git a/multicluster/controllers/multicluster/commonarea/labelidentityimport_controller_test.go b/multicluster/controllers/multicluster/commonarea/labelidentityimport_controller_test.go
new file mode 100644
index 000000000..9fb62f890
--- /dev/null
+++ b/multicluster/controllers/multicluster/commonarea/labelidentityimport_controller_test.go
@@ -0,0 +1,137 @@
+/*
+Copyright 2022 Antrea Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commonarea
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+)
+
+var (
+	resImpNamespacedName = types.NamespacedName{
+		Name:      "label-identity-app-client",
+		Namespace: "default",
+	}
+
+	labelIdentityResImp1 = &mcsv1alpha1.ResourceImport{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      resImpNamespacedName.Name,
+			Namespace: resImpNamespacedName.Namespace,
+		},
+		Spec: mcsv1alpha1.ResourceImportSpec{
+			LabelIdentity: &mcsv1alpha1.LabelIdentitySpec{
+				Label: "ns:kubernetes.io/metadata.name=ns&pod:app=client",
+				ID:    uint32(1),
+			},
+		},
+	}
+
+	labelIdentityResImp2 = &mcsv1alpha1.ResourceImport{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      resImpNamespacedName.Name,
+			Namespace: resImpNamespacedName.Namespace,
+		},
+		Spec: mcsv1alpha1.ResourceImportSpec{
+			LabelIdentity: &mcsv1alpha1.LabelIdentitySpec{
+				Label: "ns:kubernetes.io/metadata.name=ns&pod:app=client",
+				ID:    uint32(2),
+			},
+		},
+	}
+
+	labelIdentity1 = &mcsv1alpha1.LabelIdentity{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: resImpNamespacedName.Name,
+		},
+		Spec: mcsv1alpha1.LabelIdentitySpec{
+			Label: "ns:kubernetes.io/metadata.name=ns&pod:app=client",
+			ID:    uint32(1),
+		},
+	}
+
+	labelIdentity2 = &mcsv1alpha1.LabelIdentity{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: resImpNamespacedName.Name,
+		},
+		Spec: mcsv1alpha1.LabelIdentitySpec{
+			Label: "ns:kubernetes.io/metadata.name=ns&pod:app=client",
+			ID:    uint32(2),
+		},
+	}
+)
+
+func TestLabelIdentityResourceImportReconcile(t *testing.T) {
+	tests := []struct {
+		name                    string
+		existLabelIdentity      *mcsv1alpha1.LabelIdentity
+		existResImp             mcsv1alpha1.ResourceImportList
+		resImportNamespacedName types.NamespacedName
+		expLabelIdentity        mcsv1alpha1.LabelIdentitySpec
+		isDeleted               bool
+	}{
+		{
+			"create LabelIdentity",
+			&mcsv1alpha1.LabelIdentity{},
+			mcsv1alpha1.ResourceImportList{Items: []mcsv1alpha1.ResourceImport{*labelIdentityResImp1}},
+			resImpNamespacedName,
+			labelIdentity1.Spec,
+			false,
+		},
+		{
+			"update LabelIdentity",
+			labelIdentity1,
+			mcsv1alpha1.ResourceImportList{Items: []mcsv1alpha1.ResourceImport{*labelIdentityResImp2}},
+			resImpNamespacedName,
+			labelIdentity2.Spec,
+			false,
+		},
+		{
+			"delete LabelIdentity",
+			labelIdentity1,
+			mcsv1alpha1.ResourceImportList{},
+			resImpNamespacedName,
+			mcsv1alpha1.LabelIdentitySpec{},
+			true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.existLabelIdentity).Build()
+			fakeRemoteClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(&tt.existResImp).Build()
+			remoteCluster := NewFakeRemoteCommonArea(fakeRemoteClient, "leader-cluster", localClusterID, "default")
+			r := NewLabelIdentityResourceImportReconciler(fakeClient, scheme, fakeClient, localClusterID, "default", remoteCluster)
+
+			resImpReq := ctrl.Request{NamespacedName: tt.resImportNamespacedName}
+			_, err := r.Reconcile(ctx, resImpReq)
+			require.NoError(t, err, "LabelIdentityResourceImport Reconciler should handle LabelIdentity event successfully but got error")
+			actLabelIdentity := &mcsv1alpha1.LabelIdentity{}
+			err = fakeClient.Get(ctx, types.NamespacedName{Namespace: "", Name: tt.resImportNamespacedName.Name}, actLabelIdentity)
+			assert.Equal(t, tt.isDeleted, apierrors.IsNotFound(err))
+			assert.Equal(t, tt.expLabelIdentity, actLabelIdentity.Spec)
+		})
+	}
+}
diff --git a/multicluster/controllers/multicluster/gateway_controller_test.go b/multicluster/controllers/multicluster/gateway_controller_test.go
index d2f78d7ae..cb0d0b8a4 100644
--- a/multicluster/controllers/multicluster/gateway_controller_test.go
+++ b/multicluster/controllers/multicluster/gateway_controller_test.go
@@ -141,7 +141,7 @@ func TestGatewayReconciler(t *testing.T) {
 		mcReconciler := NewMemberClusterSetReconciler(fakeClient, scheme, "default")
 		mcReconciler.SetRemoteCommonArea(commonArea)
 		commonAreaGatter := mcReconciler
-		r := NewGatewayReconciler(fakeClient, scheme, "default", "10.96.0.0/12", commonAreaGatter)
+		r := NewGatewayReconciler(fakeClient, scheme, "default", "10.96.0.0/12", []string{"10.200.1.1/16"}, commonAreaGatter)
 		t.Run(tt.name, func(t *testing.T) {
 			req := ctrl.Request{NamespacedName: tt.namespacedName}
 			if _, err := r.Reconcile(ctx, req); err != nil {
diff --git a/multicluster/controllers/multicluster/label_identity_controller_test.go b/multicluster/controllers/multicluster/label_identity_controller_test.go
new file mode 100644
index 000000000..862c838a8
--- /dev/null
+++ b/multicluster/controllers/multicluster/label_identity_controller_test.go
@@ -0,0 +1,278 @@
+/*
+Copyright 2022 Antrea Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package multicluster
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/sets"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	"antrea.io/antrea/multicluster/controllers/multicluster/commonarea"
+)
+
+const (
+	addEvent = iota
+	updateEvent
+	deleteEvent
+)
+
+var (
+	ns = &v1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-ns",
+			Labels: map[string]string{
+				"kubernetes.io/metadata.name": "test-ns",
+			},
+		},
+	}
+	podA = &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "test-ns",
+			Name:      "pod-a",
+			Labels: map[string]string{
+				"app": "client",
+			},
+		},
+	}
+	newPodA = &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "test-ns",
+			Name:      "pod-a",
+			Labels: map[string]string{
+				"app": "db",
+			},
+		},
+	}
+	podB = &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "test-ns",
+			Name:      "pod-b",
+			Labels: map[string]string{
+				"app": "client",
+			},
+		},
+	}
+	podC = &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "test-ns",
+			Name:      "pod-c",
+			Labels: map[string]string{
+				"app": "web",
+			},
+		},
+	}
+
+	normalizedLabelDB  = "ns:kubernetes.io/metadata.name=test-ns&pod:app=db"
+	podANamespacedName = &types.NamespacedName{Namespace: "test-ns", Name: "pod-a"}
+	podBNamespacedName = &types.NamespacedName{Namespace: "test-ns", Name: "pod-b"}
+	podCNamespacedName = &types.NamespacedName{Namespace: "test-ns", Name: "pod-c"}
+	podAName           = podANamespacedName.String()
+	podBName           = podBNamespacedName.String()
+)
+
+func TestLabelIdentityReconciler(t *testing.T) {
+	tests := []struct {
+		name                 string
+		existingPods         *v1.PodList
+		podEvent             *v1.Pod
+		podNamespaceName     *types.NamespacedName
+		eventType            int
+		expLabelsQueued      bool
+		expNormalizedLabels  []string
+		expLabelsToPodsCache map[string]sets.String
+		expPodLabelCache     map[string]string
+	}{
+		{
+			name:                 "pod add event",
+			existingPods:         &v1.PodList{Items: []v1.Pod{}},
+			podEvent:             podA,
+			podNamespaceName:     podANamespacedName,
+			eventType:            addEvent,
+			expLabelsQueued:      true,
+			expNormalizedLabels:  []string{normalizedLabel},
+			expLabelsToPodsCache: map[string]sets.String{normalizedLabel: sets.NewString(podAName)},
+			expPodLabelCache:     map[string]string{podANamespacedName.String(): normalizedLabel},
+		},
+		{
+			name:                 "pod add event existing label",
+			existingPods:         &v1.PodList{Items: []v1.Pod{*podA}},
+			podEvent:             podB,
+			podNamespaceName:     podBNamespacedName,
+			eventType:            addEvent,
+			expLabelsQueued:      false,
+			expNormalizedLabels:  []string{normalizedLabel},
+			expLabelsToPodsCache: map[string]sets.String{normalizedLabel: sets.NewString(podAName, podBName)},
+			expPodLabelCache:     map[string]string{podAName: normalizedLabel, podBName: normalizedLabel},
+		},
+		{
+			name:                 "pod update event",
+			existingPods:         &v1.PodList{Items: []v1.Pod{*podA}},
+			podEvent:             newPodA,
+			podNamespaceName:     podANamespacedName,
+			eventType:            updateEvent,
+			expLabelsQueued:      true,
+			expNormalizedLabels:  []string{normalizedLabelDB},
+			expLabelsToPodsCache: map[string]sets.String{normalizedLabelDB: sets.NewString(podAName)},
+			expPodLabelCache:     map[string]string{podAName: normalizedLabelDB},
+		},
+		{
+			name:                 "pod delete event stale label",
+			existingPods:         &v1.PodList{Items: []v1.Pod{*podA}},
+			podEvent:             podA,
+			podNamespaceName:     podANamespacedName,
+			eventType:            deleteEvent,
+			expLabelsQueued:      true,
+			expNormalizedLabels:  []string{},
+			expLabelsToPodsCache: map[string]sets.String{},
+			expPodLabelCache:     map[string]string{},
+		},
+		{
+			name:                 "pod delete event no stale label",
+			existingPods:         &v1.PodList{Items: []v1.Pod{*podA, *podB}},
+			podEvent:             podB,
+			podNamespaceName:     podBNamespacedName,
+			eventType:            deleteEvent,
+			expLabelsQueued:      false,
+			expNormalizedLabels:  []string{normalizedLabel},
+			expLabelsToPodsCache: map[string]sets.String{normalizedLabel: sets.NewString(podAName)},
+			expPodLabelCache:     map[string]string{podAName: normalizedLabel},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			stopCh := make(chan struct{})
+			defer close(stopCh)
+
+			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(tt.existingPods).WithObjects(ns).Build()
+			fakeRemoteClient := fake.NewClientBuilder().WithScheme(scheme).Build()
+			commonArea := commonarea.NewFakeRemoteCommonArea(fakeRemoteClient, "leader-cluster", localClusterID, leaderNamespace)
+			mcReconciler := NewMemberClusterSetReconciler(fakeClient, scheme, "default")
+			mcReconciler.SetRemoteCommonArea(commonArea)
+			r := NewLabelIdentityReconciler(fakeClient, scheme, mcReconciler)
+			go r.Run(stopCh)
+
+			for _, p := range tt.existingPods.Items {
+				req := ctrl.Request{
+					NamespacedName: types.NamespacedName{
+						Namespace: p.Namespace,
+						Name:      p.Name,
+					},
+				}
+				_, err := r.Reconcile(ctx, req)
+				assert.NoError(t, err, "LabelIdentity Reconciler got error during reconciling initial Pod events")
+			}
+			time.Sleep(10 * time.Millisecond)
+			assert.Equalf(t, r.labelQueue.Len(), 0, "LabelIdentity Reconciler failed to process label ResourceExport for existing Pods")
+			switch tt.eventType {
+			case addEvent:
+				tt.podEvent.ResourceVersion = ""
+				r.Client.Create(ctx, tt.podEvent, &client.CreateOptions{})
+			case updateEvent:
+				r.Client.Update(ctx, tt.podEvent, &client.UpdateOptions{})
+			case deleteEvent:
+				r.Client.Delete(ctx, tt.podEvent, &client.DeleteOptions{})
+			}
+			var err error
+			req := ctrl.Request{NamespacedName: *tt.podNamespaceName}
+
+			_, err = r.Reconcile(ctx, req)
+			assert.NoError(t, err, "LabelIdentity Reconciler got error during reconciling Pod event")
+
+			if !reflect.DeepEqual(r.labelToPodsCache, tt.expLabelsToPodsCache) {
+				t.Errorf("Unexpected labelToPodsCache in LabelIdentity Reconciler. Exp: %s, Act: %s", tt.expLabelsToPodsCache, r.labelToPodsCache)
+			}
+			if !reflect.DeepEqual(r.podLabelCache, tt.expPodLabelCache) {
+				t.Errorf("Unexpected podLabelCache in LabelIdentity Reconciler. Exp: %s, Act: %s", tt.expPodLabelCache, r.podLabelCache)
+			}
+			time.Sleep(10 * time.Millisecond)
+			assert.Equalf(t, r.labelQueue.Len(), 0, "LabelIdentity Reconciler failed to process label ResourceExport for testcase Pod")
+			actLabelIdentityResourceExports := &mcsv1alpha1.ResourceExportList{}
+			err = commonArea.List(ctx, actLabelIdentityResourceExports)
+			assert.NoError(t, err, "Failed to list ResourceExports after reconciliation")
+			var actNormalizedLabels []string
+			for _, re := range actLabelIdentityResourceExports.Items {
+				if re.Spec.LabelIdentity != nil {
+					actNormalizedLabels = append(actNormalizedLabels, re.Spec.LabelIdentity.NormalizedLabel)
+				}
+			}
+			assert.ElementsMatchf(t, tt.expNormalizedLabels, actNormalizedLabels,
+				"Unexpected LabelIdentity ResourceExports, expect ResourceExports for labels: %s, actual: %s", tt.expNormalizedLabels, actNormalizedLabels)
+		})
+	}
+}
+
+func TestNamespaceMapFunc(t *testing.T) {
+	expReq := []reconcile.Request{
+		{
+			NamespacedName: *podANamespacedName,
+		},
+		{
+			NamespacedName: *podCNamespacedName,
+		},
+	}
+	fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(podA, podC, ns).Build()
+	fakeRemoteClient := fake.NewClientBuilder().WithScheme(scheme).Build()
+	commonArea := commonarea.NewFakeRemoteCommonArea(fakeRemoteClient, "leader-cluster", localClusterID, leaderNamespace)
+	mcReconciler := NewMemberClusterSetReconciler(fakeClient, scheme, "default")
+	mcReconciler.SetRemoteCommonArea(commonArea)
+
+	r := NewLabelIdentityReconciler(fakeClient, scheme, mcReconciler)
+	actualReq := r.namespaceMapFunc(ns)
+	assert.ElementsMatch(t, expReq, actualReq)
+}
+
+func TestGetNormalizedLabel(t *testing.T) {
+	tests := []struct {
+		name               string
+		namespace          string
+		podLabels          map[string]string
+		nsLabels           map[string]string
+		expNormalizedLabel string
+	}{
+		{
+			"regular Pod",
+			"test-ns",
+			map[string]string{"purpose": "test"},
+			map[string]string{v1.LabelMetadataName: "test-ns"},
+			"ns:kubernetes.io/metadata.name=test-ns&pod:purpose=test",
+		},
+		{
+			"no Namespace default name label",
+			"test-ns",
+			map[string]string{"purpose": "test"},
+			map[string]string{"region": "west"},
+			"ns:kubernetes.io/metadata.name=test-ns,region=west&pod:purpose=test",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			normalizedLabel := getNormalizedLabel(tt.nsLabels, tt.podLabels, tt.namespace)
+			assert.Equal(t, tt.expNormalizedLabel, normalizedLabel)
+		})
+	}
+}
diff --git a/multicluster/controllers/multicluster/label_identity_export_controller_test.go b/multicluster/controllers/multicluster/label_identity_export_controller_test.go
new file mode 100644
index 000000000..42c01b445
--- /dev/null
+++ b/multicluster/controllers/multicluster/label_identity_export_controller_test.go
@@ -0,0 +1,360 @@
+/*
+Copyright 2022 Antrea Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package multicluster
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/sets"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	mcsv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
+	"antrea.io/antrea/multicluster/controllers/multicluster/common"
+)
+
+var (
+	normalizedLabel = "ns:kubernetes.io/metadata.name=test-ns&pod:app=client"
+	labelHash       = common.HashLabelIdentity(normalizedLabel)
+
+	resExpNamespacedName = types.NamespacedName{
+		Namespace: leaderNamespace,
+		Name:      localClusterID + "-" + labelHash,
+	}
+
+	labelIdentityResExp = &mcsv1alpha1.ResourceExport{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: leaderNamespace,
+			Name:      localClusterID + "-" + labelHash,
+		},
+		Spec: mcsv1alpha1.ResourceExportSpec{
+			ClusterID: localClusterID,
+			LabelIdentity: &mcsv1alpha1.LabelIdentityExport{
+				NormalizedLabel: normalizedLabel,
+			},
+		},
+	}
+	labelIdentityResImp = &mcsv1alpha1.ResourceImport{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: leaderNamespace,
+			Name:      labelHash,
+		},
+		Spec: mcsv1alpha1.ResourceImportSpec{
+			ClusterIDs: []string{localClusterID},
+			LabelIdentity: &mcsv1alpha1.LabelIdentitySpec{
+				Label: normalizedLabel,
+				ID:    1,
+			},
+		},
+	}
+
+	clusterBID                   = "cluster-b"
+	resExpNamespacedNameClusterB = types.NamespacedName{
+		Namespace: "cluster-b-namespace",
+		Name:      clusterBID + "-" + labelHash,
+	}
+)
+
+func TestLabelIdentityResourceExportReconclie(t *testing.T) {
+	tests := []struct {
+		name                     string
+		existResExp              *mcsv1alpha1.ResourceExportList
+		existResImp              *mcsv1alpha1.ResourceImportList
+		resExpNamespacedName     types.NamespacedName
+		expNormalizedLabel       string
+		originalLabelsToClusters map[string]sets.String
+		originalClusterToLabels  map[string]sets.String
+		expLabelsToClusters      map[string]sets.String
+		expClusterToLabels       map[string]sets.String
+		expLabelResImpDeleted    bool
+	}{
+		{
+			name: "create LabelIdentity kind of ResImp",
+			existResExp: &mcsv1alpha1.ResourceExportList{
+				Items: []mcsv1alpha1.ResourceExport{*labelIdentityResExp},
+			},
+			existResImp:           &mcsv1alpha1.ResourceImportList{},
+			resExpNamespacedName:  resExpNamespacedName,
+			expNormalizedLabel:    normalizedLabel,
+			expLabelsToClusters:   map[string]sets.String{labelHash: sets.NewString(localClusterID)},
+			expClusterToLabels:    map[string]sets.String{localClusterID: sets.NewString(labelHash)},
+			expLabelResImpDeleted: false,
+		},
+		{
+			name: "LabelIdentity kind of ResImp already exist",
+			existResExp: &mcsv1alpha1.ResourceExportList{
+				Items: []mcsv1alpha1.ResourceExport{*labelIdentityResExp},
+			},
+			existResImp: &mcsv1alpha1.ResourceImportList{
+				Items: []mcsv1alpha1.ResourceImport{*labelIdentityResImp},
+			},
+			resExpNamespacedName:  resExpNamespacedName,
+			expNormalizedLabel:    normalizedLabel,
+			expLabelsToClusters:   map[string]sets.String{labelHash: sets.NewString(localClusterID)},
+			expClusterToLabels:    map[string]sets.String{localClusterID: sets.NewString(labelHash)},
+			expLabelResImpDeleted: false,
+		},
+		{
+			name: "ResExport delete LabelIdentity not stale in ClusterSet",
+			existResExp: &mcsv1alpha1.ResourceExportList{
+				Items: []mcsv1alpha1.ResourceExport{*labelIdentityResExp},
+			},
+			existResImp: &mcsv1alpha1.ResourceImportList{
+				Items: []mcsv1alpha1.ResourceImport{*labelIdentityResImp},
+			},
+			resExpNamespacedName:     resExpNamespacedNameClusterB,
+			expNormalizedLabel:       normalizedLabel,
+			originalLabelsToClusters: map[string]sets.String{labelHash: sets.NewString(localClusterID, clusterBID)},
+			originalClusterToLabels:  map[string]sets.String{localClusterID: sets.NewString(labelHash), clusterBID: sets.NewString(labelHash)},
+			expLabelsToClusters:      map[string]sets.String{labelHash: sets.NewString(localClusterID)},
+			expClusterToLabels:       map[string]sets.String{localClusterID: sets.NewString(labelHash), clusterBID: sets.NewString()},
+			expLabelResImpDeleted:    false,
+		},
+		{
+			name:        "delete LabelIdentity kind of ResImp",
+			existResExp: &mcsv1alpha1.ResourceExportList{},
+			existResImp: &mcsv1alpha1.ResourceImportList{
+				Items: []mcsv1alpha1.ResourceImport{*labelIdentityResImp},
+			},
+			resExpNamespacedName:     resExpNamespacedName,
+			expNormalizedLabel:       "",
+			originalLabelsToClusters: map[string]sets.String{labelHash: sets.NewString(localClusterID)},
+			originalClusterToLabels:  map[string]sets.String{localClusterID: sets.NewString(labelHash)},
+			expLabelsToClusters:      map[string]sets.String{},
+			expClusterToLabels:       map[string]sets.String{localClusterID: sets.NewString()},
+			expLabelResImpDeleted:    true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			stopCh := make(chan struct{})
+			defer close(stopCh)
+
+			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(tt.existResExp, tt.existResImp).Build()
+			r := NewLabelIdentityExportReconciler(fakeClient, scheme, leaderNamespace)
+			if len(tt.originalLabelsToClusters) > 0 {
+				r.clusterToLabels = tt.originalClusterToLabels
+				r.labelsToClusters = tt.originalLabelsToClusters
+			}
+			go r.Run(stopCh)
+			resExpReq := ctrl.Request{NamespacedName: tt.resExpNamespacedName}
+			if _, err := r.Reconcile(ctx, resExpReq); err != nil {
+				t.Errorf("LabelIdentityExport Reconciler got error during reconciling. error = %v", err)
+			}
+
+			if !reflect.DeepEqual(r.labelsToClusters, tt.expLabelsToClusters) {
+				t.Errorf("LabelIdentityExport Reconciler operated labelsToClusters incorrectly. Exp: %s, Act: %s", tt.expLabelsToClusters, r.labelsToClusters)
+			}
+			if !reflect.DeepEqual(r.clusterToLabels, tt.expClusterToLabels) {
+				t.Errorf("LabelIdentityExport Reconciler operated clusterToLabels incorrectly. Exp: %s, Act: %s", tt.expClusterToLabels, r.clusterToLabels)
+			}
+
+			time.Sleep(100 * time.Millisecond)
+			actLabelIdentityResImp := &mcsv1alpha1.ResourceImport{}
+			lastIdx := strings.LastIndex(tt.resExpNamespacedName.Name, "-")
+			parsedLabelHash := tt.resExpNamespacedName.Name[lastIdx+1:]
+			err := fakeClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: parsedLabelHash}, actLabelIdentityResImp)
+			assert.Equalf(t, tt.expLabelResImpDeleted, apierrors.IsNotFound(err), "Unexpected error status when getting LabelIdentity kind of ResourceImport: %v", err)
+			if tt.expNormalizedLabel != "" {
+				actualLabel := actLabelIdentityResImp.Spec.LabelIdentity.Label
+				assert.Equalf(t, tt.expNormalizedLabel, actualLabel, "Unexpected normalized label in ResourceImport. Exp: %s, Act: %s", tt.expNormalizedLabel, actualLabel)
+			}
+		})
+	}
+}
+
+func TestIDAllocatorBasic(t *testing.T) {
+	testSteps := []struct {
+		op                        string
+		idExpectedOrReleasing     uint32
+		expectedAvailableReuseIDs int
+		expectErr                 bool
+	}{
+		{
+			"allocate",
+			1,
+			0,
+			false,
+		},
+		{
+			"allocate",
+			2,
+			0,
+			false,
+		},
+		{
+			"allocate",
+			3,
+			0,
+			false,
+		},
+		{
+			"release",
+			2,
+			1,
+			false,
+		},
+		{
+			"release",
+			1,
+			2,
+			false,
+		},
+		{
+			"allocate",
+			1,
+			1,
+			false,
+		},
+		{
+			"allocate",
+			2,
+			0,
+			false,
+		},
+		{
+			"allocate",
+			0,
+			0,
+			true,
+		},
+	}
+	idAllocator := newIDAllocator(1, 3)
+	for _, step := range testSteps {
+		if step.op == "allocate" {
+			id, err := idAllocator.allocate()
+			if step.expectErr && err == nil {
+				t.Errorf("Expect id allocation to fail because the pool is exhausted, but got id %d", id)
+			} else if !step.expectErr && err != nil {
+				t.Errorf("Expect allocation to succeed but got err %v", err)
+			}
+			if id != step.idExpectedOrReleasing {
+				t.Errorf("ID allocated is not expected, expect %d, got %d", step.expectedAvailableReuseIDs, id)
+			}
+		} else {
+			idAllocator.release(step.idExpectedOrReleasing)
+		}
+		if idAllocator.releasedIDs.Len() != step.expectedAvailableReuseIDs {
+			t.Errorf("Unexpected number of IDs available for reuse")
+		}
+	}
+}
+
+func TestIDAllocatorWithPreAllocation(t *testing.T) {
+	testSteps := []struct {
+		op                        string
+		id                        uint32
+		expectedAvailableReuseIDs int
+		expectedPreAllocatedIDs   int
+		expectErr                 bool
+	}{
+		{
+			"setAllocated",
+			4,
+			0,
+			1,
+			false,
+		},
+		{
+			"setAllocated",
+			1,
+			0,
+			2,
+			false,
+		},
+		{
+			"setAllocated",
+			2,
+			0,
+			3,
+			false,
+		},
+		{
+			"allocate",
+			3,
+			0,
+			3,
+			false,
+		},
+		{
+			"setAllocated",
+			3,
+			0,
+			3,
+			true,
+		},
+		{
+			"release",
+			2,
+			1,
+			2,
+			false,
+		},
+		{
+			"allocate",
+			2,
+			0,
+			2,
+			false,
+		},
+		{
+			"allocate",
+			0,
+			0,
+			2,
+			true,
+		},
+	}
+	idAllocator := newIDAllocator(1, 4)
+	for _, step := range testSteps {
+		switch step.op {
+		case "setAllocated":
+			err := idAllocator.setAllocated(step.id)
+			if step.expectErr && err == nil {
+				t.Errorf("Expect setting id allocated to fail but succeeded")
+			} else if !step.expectErr && err != nil {
+				t.Errorf("Expect set allocation to succeed but got err %v", err)
+			}
+		case "allocate":
+			id, err := idAllocator.allocate()
+			if step.expectErr && err == nil {
+				t.Errorf("Expect id allocation to fail because the pool is exhausted, but got id %d", id)
+			}
+			if id != step.id {
+				t.Errorf("ID allocated is not expected, expect %d, got %d", step.id, id)
+			}
+		case "release":
+			idAllocator.release(step.id)
+		}
+		if idAllocator.previouslyAllocatedIDs.Len() != step.expectedPreAllocatedIDs {
+			t.Errorf("Unexpected number of pre-allocated IDs on step %s %d, expect: %d actual: %d",
+				step.op, step.id, step.expectedPreAllocatedIDs, idAllocator.previouslyAllocatedIDs.Len())
+		}
+		if idAllocator.releasedIDs.Len() != step.expectedAvailableReuseIDs {
+			t.Errorf("Unexpected number of IDs available for reuse on step %s %d, expect: %d actual: %d",
+				step.op, step.id, step.expectedAvailableReuseIDs, idAllocator.releasedIDs.Len())
+		}
+	}
+}
diff --git a/multicluster/controllers/multicluster/stale_controller_test.go b/multicluster/controllers/multicluster/stale_controller_test.go
index efdf83527..8fcced395 100644
--- a/multicluster/controllers/multicluster/stale_controller_test.go
+++ b/multicluster/controllers/multicluster/stale_controller_test.go
@@ -25,7 +25,6 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/sets"
-	"k8s.io/klog/v2"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	k8smcsv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1"
@@ -274,7 +273,7 @@ func TestStaleController_CleanupResourceExport(t *testing.T) {
 		},
 	}
 
-	svcResExportFromOther := mcsv1alpha1.ResourceExport{
+	svcResExportFromOtherCluster := mcsv1alpha1.ResourceExport{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: "default",
 			Name:      "cluster-b-default-nginx-service",
@@ -288,15 +287,79 @@ func TestStaleController_CleanupResourceExport(t *testing.T) {
 			Kind:      common.ServiceKind,
 		},
 	}
+	existingNamespaces := &corev1.NamespaceList{
+		Items: []corev1.Namespace{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "test-ns",
+					Labels: map[string]string{
+						"purpose": "test",
+					},
+				},
+			},
+		},
+	}
+	existingPods := &corev1.PodList{
+		Items: []corev1.Pod{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "test-ns",
+					Name:      "test-pod",
+					Labels: map[string]string{
+						"app": "web",
+					},
+				},
+			},
+		},
+	}
+	labelNormalizedExist := "namespace:kubernetes.io/metadata.name=test-ns,purpose=test&pod:app=web"
+	labelNormalizedNonExist := "namespace:kubernetes.io/metadata.name=test-ns,purpose=test&pod:app=db"
+	toKeepLabelResExport := mcsv1alpha1.ResourceExport{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+			Name:      "cluster-a-" + common.HashLabelIdentity(labelNormalizedExist),
+			Labels: map[string]string{
+				common.SourceKind:      common.LabelIdentityKind,
+				common.SourceClusterID: "cluster-a",
+			},
+		},
+		Spec: mcsv1alpha1.ResourceExportSpec{
+			Kind: common.LabelIdentityKind,
+			LabelIdentity: &mcsv1alpha1.LabelIdentityExport{
+				NormalizedLabel: labelNormalizedExist,
+			},
+		},
+	}
+	toDeleteLabelResExport := mcsv1alpha1.ResourceExport{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+			Name:      "cluster-a-" + common.HashLabelIdentity(labelNormalizedNonExist),
+			Labels: map[string]string{
+				common.SourceKind:      common.LabelIdentityKind,
+				common.SourceClusterID: "cluster-a",
+			},
+		},
+		Spec: mcsv1alpha1.ResourceExportSpec{
+			Kind: common.LabelIdentityKind,
+			LabelIdentity: &mcsv1alpha1.LabelIdentityExport{
+				NormalizedLabel: labelNormalizedNonExist,
+			},
+		},
+	}
 	tests := []struct {
-		name            string
-		existSvcList    *corev1.ServiceList
-		existSvcExpList *k8smcsv1alpha1.ServiceExportList
-		existResExpList *mcsv1alpha1.ResourceExportList
-		wantErr         bool
+		name                   string
+		existSvcList           *corev1.ServiceList
+		existPodList           *corev1.PodList
+		existNamespaceList     *corev1.NamespaceList
+		existLabelIdentityList *mcsv1alpha1.LabelIdentityList
+		existSvcExpList        *k8smcsv1alpha1.ServiceExportList
+		existResExpList        *mcsv1alpha1.ResourceExportList
+		wantErr                bool
 	}{
 		{
-			name: "clean up ResourceExport successfully",
+			name:               "clean up ResourceExport successfully",
+			existNamespaceList: existingNamespaces,
+			existPodList:       existingPods,
 			existSvcExpList: &k8smcsv1alpha1.ServiceExportList{
 				Items: []k8smcsv1alpha1.ServiceExport{
 					svcExpNginx,
@@ -308,14 +371,16 @@ func TestStaleController_CleanupResourceExport(t *testing.T) {
 					toDeleteEPResExport,
 					toDeleteCIResExport,
 					toKeepSvcResExport,
-					svcResExportFromOther,
+					toKeepLabelResExport,
+					toDeleteLabelResExport,
+					svcResExportFromOtherCluster,
 				},
 			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(tt.existSvcExpList).Build()
+			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(tt.existSvcExpList, tt.existPodList, tt.existNamespaceList).Build()
 			fakeRemoteClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(tt.existResExpList).Build()
 			commonArea := commonarea.NewFakeRemoteCommonArea(fakeRemoteClient, "leader-cluster", localClusterID, "default")
 
@@ -325,16 +390,12 @@ func TestStaleController_CleanupResourceExport(t *testing.T) {
 			if err := c.cleanup(); err != nil {
 				t.Errorf("StaleController.cleanup() should clean up all stale ResourceExports but got err = %v", err)
 			}
-			ctx := context.TODO()
-			svcResExpList := &mcsv1alpha1.ResourceExportList{}
-			err := fakeRemoteClient.List(ctx, svcResExpList, &client.ListOptions{})
-			resExpLen := len(svcResExpList.Items)
+			resExpList := &mcsv1alpha1.ResourceExportList{}
+			err := fakeRemoteClient.List(context.TODO(), resExpList, &client.ListOptions{})
+			resExpLen := len(resExpList.Items)
 			if err == nil {
-				if resExpLen != 2 {
-					for _, re := range svcResExpList.Items {
-						klog.Infof("left resourceexport %v", re)
-					}
-					t.Errorf("Should only two valid ResourceExports left but got %v", resExpLen)
+				if resExpLen != 3 {
+					t.Errorf("Should only THREE valid ResourceExports left but got %v", resExpLen)
 				}
 			} else {
 				t.Errorf("Should list ResourceExport successfully but got err = %v", err)
@@ -535,3 +596,86 @@ func TestStaleController_CleanupMemberClusterAnnounce(t *testing.T) {
 		})
 	}
 }
+
+func TestStaleController_CleanupLabelIdentites(t *testing.T) {
+	normalizedLabelA := "namespace:kubernetes.io/metadata.name=test&pod:app=client"
+	normalizedLabelB := "namespace:kubernetes.io/metadata.name=test&pod:app=db"
+	labelHashA := common.HashLabelIdentity(normalizedLabelA)
+	labelHashB := common.HashLabelIdentity(normalizedLabelB)
+	labelIdentityA := mcsv1alpha1.LabelIdentity{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: labelHashA,
+		},
+		Spec: mcsv1alpha1.LabelIdentitySpec{
+			Label: normalizedLabelA,
+			ID:    1,
+		},
+	}
+	resImpA := mcsv1alpha1.ResourceImport{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "antrea-mcs",
+			Name:      labelHashA,
+		},
+		Spec: mcsv1alpha1.ResourceImportSpec{
+			Kind: common.LabelIdentityKind,
+			LabelIdentity: &mcsv1alpha1.LabelIdentitySpec{
+				Label: normalizedLabelA,
+				ID:    1,
+			},
+		},
+	}
+	labelIdentityB := mcsv1alpha1.LabelIdentity{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: labelHashB,
+		},
+		Spec: mcsv1alpha1.LabelIdentitySpec{
+			Label: normalizedLabelB,
+			ID:    2,
+		},
+	}
+	tests := []struct {
+		name                   string
+		existLabelIdentityList *mcsv1alpha1.LabelIdentityList
+		existingResImpList     *mcsv1alpha1.ResourceImportList
+		wantErr                bool
+	}{
+		{
+			name: "clean up LabelIdentities successfully",
+			existLabelIdentityList: &mcsv1alpha1.LabelIdentityList{
+				Items: []mcsv1alpha1.LabelIdentity{
+					labelIdentityA, labelIdentityB,
+				},
+			},
+			existingResImpList: &mcsv1alpha1.ResourceImportList{
+				Items: []mcsv1alpha1.ResourceImport{
+					resImpA,
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(tt.existLabelIdentityList).Build()
+			fakeRemoteClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(tt.existingResImpList).Build()
+			ca := commonarea.NewFakeRemoteCommonArea(fakeRemoteClient, "leader-cluster", localClusterID, "antrea-mcs")
+
+			mcReconciler := NewMemberClusterSetReconciler(fakeClient, scheme, "default")
+			mcReconciler.SetRemoteCommonArea(ca)
+			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler, MemberCluster)
+			if err := c.cleanup(); err != nil {
+				t.Errorf("StaleController.cleanup() should clean up all stale LabelIdentities but got err = %v", err)
+			}
+			ctx := context.TODO()
+			labelList := &mcsv1alpha1.LabelIdentityList{}
+			err := fakeClient.List(ctx, labelList, &client.ListOptions{})
+			labelListLen := len(labelList.Items)
+			if err == nil {
+				if labelListLen != 1 {
+					t.Errorf("Should only one valid LabelIdentity left but got %v", labelListLen)
+				}
+			} else {
+				t.Errorf("Should list LabelIdentity successfully but got err = %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go
index a8f31aad5..7e4dd7dfc 100644
--- a/pkg/agent/agent_test.go
+++ b/pkg/agent/agent_test.go
@@ -39,6 +39,7 @@ import (
 	ovsconfigtest "antrea.io/antrea/pkg/ovs/ovsconfig/testing"
 	"antrea.io/antrea/pkg/util/env"
 	"antrea.io/antrea/pkg/util/ip"
+	"antrea.io/antrea/pkg/util/runtime"
 )
 
 func newAgentInitializer(ovsBridgeClient ovsconfig.OVSBridgeClient, ifaceStore interfacestore.InterfaceStore) *Initializer {
@@ -450,3 +451,130 @@ func mockGetIPNetDeviceByCIDRs(ipV4Net, ipV6Net *net.IPNet, ipDevice *net.Interf
 	}
 	return func() { getIPNetDeviceByCIDRs = prevGetIPNetDeviceByCIDRs }
 }
+
+func TestSetupDefaultTunnelInterface(t *testing.T) {
+	_, nodeIPNet, _ := net.ParseCIDR("192.168.10.10/24")
+	var tunnelPortLocalIP net.IP
+	var tunnelPortLocalIPStr string
+	if runtime.IsWindowsPlatform() {
+		tunnelPortLocalIP = nodeIPNet.IP
+		tunnelPortLocalIPStr = tunnelPortLocalIP.String()
+	}
+	tests := []struct {
+		name                    string
+		nodeConfig              *config.NodeConfig
+		networkConfig           *config.NetworkConfig
+		existingTunnelInterface *interfacestore.InterfaceConfig
+		expectedOVSCalls        func(client *ovsconfigtest.MockOVSBridgeClientMockRecorder)
+		expectedErr             error
+	}{
+		{
+			name: "create default Geneve tunnel",
+			nodeConfig: &config.NodeConfig{
+				DefaultTunName:        defaultTunInterfaceName,
+				NodeTransportIPv4Addr: nodeIPNet,
+			},
+			networkConfig: &config.NetworkConfig{
+				TrafficEncapMode: config.TrafficEncapModeEncap,
+				TunnelType:       ovsconfig.GeneveTunnel,
+			},
+			expectedOVSCalls: func(client *ovsconfigtest.MockOVSBridgeClientMockRecorder) {
+				client.CreateTunnelPortExt(defaultTunInterfaceName,
+					ovsconfig.TunnelType(ovsconfig.GeneveTunnel),
+					int32(config.DefaultTunOFPort),
+					false,
+					tunnelPortLocalIPStr,
+					"",
+					"",
+					"",
+					map[string]interface{}{},
+					map[string]interface{}{interfacestore.AntreaInterfaceTypeKey: interfacestore.AntreaTunnel})
+				client.GetOFPort(defaultTunInterfaceName, false)
+			},
+		},
+		{
+			name: "update Geneve tunnel csum",
+			nodeConfig: &config.NodeConfig{
+				DefaultTunName:        defaultTunInterfaceName,
+				NodeTransportIPv4Addr: nodeIPNet,
+			},
+			networkConfig: &config.NetworkConfig{
+				TrafficEncapMode: config.TrafficEncapModeEncap,
+				TunnelType:       ovsconfig.GeneveTunnel,
+				TunnelCsum:       false,
+			},
+			existingTunnelInterface: interfacestore.NewTunnelInterface(defaultTunInterfaceName, ovsconfig.GeneveTunnel, 0, tunnelPortLocalIP, true, &interfacestore.OVSPortConfig{OFPort: 1}),
+			expectedOVSCalls: func(client *ovsconfigtest.MockOVSBridgeClientMockRecorder) {
+				client.GetInterfaceOptions(defaultTunInterfaceName).Return(map[string]string{"csum": "true"}, nil)
+				client.SetInterfaceOptions(defaultTunInterfaceName, map[string]interface{}{"csum": "false"})
+			},
+		},
+		{
+			name: "update tunnel type and port",
+			nodeConfig: &config.NodeConfig{
+				DefaultTunName:        defaultTunInterfaceName,
+				NodeTransportIPv4Addr: nodeIPNet,
+			},
+			networkConfig: &config.NetworkConfig{
+				TrafficEncapMode: config.TrafficEncapModeEncap,
+				TunnelType:       ovsconfig.VXLANTunnel,
+				TunnelPort:       9999,
+			},
+			existingTunnelInterface: interfacestore.NewTunnelInterface(defaultTunInterfaceName, ovsconfig.GeneveTunnel, 0, tunnelPortLocalIP, true, &interfacestore.OVSPortConfig{
+				PortUUID: "foo",
+				OFPort:   1,
+			}),
+			expectedOVSCalls: func(client *ovsconfigtest.MockOVSBridgeClientMockRecorder) {
+				client.DeletePort("foo")
+				client.CreateTunnelPortExt(defaultTunInterfaceName,
+					ovsconfig.TunnelType(ovsconfig.VXLANTunnel),
+					int32(config.DefaultTunOFPort),
+					false,
+					tunnelPortLocalIPStr,
+					"",
+					"",
+					"",
+					map[string]interface{}{"dst_port": "9999"},
+					map[string]interface{}{interfacestore.AntreaInterfaceTypeKey: interfacestore.AntreaTunnel})
+				client.GetOFPort(defaultTunInterfaceName, false)
+			},
+		},
+		{
+			name: "no change",
+			nodeConfig: &config.NodeConfig{
+				DefaultTunName:        defaultTunInterfaceName,
+				NodeTransportIPv4Addr: nodeIPNet,
+			},
+			networkConfig: &config.NetworkConfig{
+				TrafficEncapMode: config.TrafficEncapModeEncap,
+				TunnelType:       ovsconfig.GeneveTunnel,
+				TunnelCsum:       false,
+			},
+			existingTunnelInterface: interfacestore.NewTunnelInterface(defaultTunInterfaceName, ovsconfig.GeneveTunnel, 0, tunnelPortLocalIP, false, &interfacestore.OVSPortConfig{OFPort: 1}),
+			expectedOVSCalls:        func(client *ovsconfigtest.MockOVSBridgeClientMockRecorder) {},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			controller := mock.NewController(t)
+			defer controller.Finish()
+			mockOVSBridgeClient := ovsconfigtest.NewMockOVSBridgeClient(controller)
+			client := fake.NewSimpleClientset()
+			ifaceStore := interfacestore.NewInterfaceStore()
+			if tt.existingTunnelInterface != nil {
+				ifaceStore.AddInterface(tt.existingTunnelInterface)
+			}
+			initializer := &Initializer{
+				client:          client,
+				ifaceStore:      ifaceStore,
+				ovsBridgeClient: mockOVSBridgeClient,
+				ovsBridge:       "br-int",
+				networkConfig:   tt.networkConfig,
+				nodeConfig:      tt.nodeConfig,
+			}
+			tt.expectedOVSCalls(mockOVSBridgeClient.EXPECT())
+			err := initializer.setupDefaultTunnelInterface()
+			assert.Equal(t, err, tt.expectedErr)
+		})
+	}
+}
diff --git a/pkg/agent/controller/networkpolicy/audit_logging_test.go b/pkg/agent/controller/networkpolicy/audit_logging_test.go
index 36323b616..5388a7259 100644
--- a/pkg/agent/controller/networkpolicy/audit_logging_test.go
+++ b/pkg/agent/controller/networkpolicy/audit_logging_test.go
@@ -18,14 +18,23 @@ import (
 	"errors"
 	"fmt"
 	"log"
+	"net"
 	"strconv"
 	"strings"
 	"sync"
 	"testing"
 	"time"
 
+	"antrea.io/libOpenflow/openflow15"
+	"antrea.io/ofnet/ofctrl"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
+	"antrea.io/antrea/pkg/agent/openflow"
+	openflowtest "antrea.io/antrea/pkg/agent/openflow/testing"
+	"antrea.io/antrea/pkg/apis/controlplane/v1beta2"
+	binding "antrea.io/antrea/pkg/ovs/openflow"
+	"antrea.io/antrea/pkg/util/ip"
 )
 
 const (
@@ -115,6 +124,7 @@ func newLogInfo(disposition string) (*logInfo, string) {
 	testLogInfo := &logInfo{
 		tableName:   "AntreaPolicyIngressRule",
 		npRef:       "AntreaNetworkPolicy:default/test",
+		ruleName:    "test-rule",
 		ofPriority:  "0",
 		disposition: disposition,
 		srcIP:       "0.0.0.0",
@@ -124,8 +134,8 @@ func newLogInfo(disposition string) (*logInfo, string) {
 		protocolStr: "TCP",
 		pktLength:   60,
 	}
-	expected := fmt.Sprintf("%s %s %s %s %s %s %s %s %s %d", testLogInfo.tableName, testLogInfo.npRef, testLogInfo.disposition,
-		testLogInfo.ofPriority, testLogInfo.srcIP, testLogInfo.srcPort, testLogInfo.destIP, testLogInfo.destPort,
+	expected := fmt.Sprintf("%s %s %s %s %s %s %s %s %s %s %d", testLogInfo.tableName, testLogInfo.npRef, testLogInfo.ruleName,
+		testLogInfo.disposition, testLogInfo.ofPriority, testLogInfo.srcIP, testLogInfo.srcPort, testLogInfo.destIP, testLogInfo.destPort,
 		testLogInfo.protocolStr, testLogInfo.pktLength)
 	return testLogInfo, expected
 }
@@ -220,3 +230,85 @@ func TestDropPacketMultiDedupLog(t *testing.T) {
 	require.NoError(t, err)
 	assert.Equal(t, 1, c2)
 }
+
+func TestGetNetworkPolicyInfo(t *testing.T) {
+	openflow.InitMockTables(
+		map[*openflow.Table]uint8{
+			openflow.AntreaPolicyEgressRuleTable:  uint8(5),
+			openflow.EgressRuleTable:              uint8(6),
+			openflow.EgressDefaultTable:           uint8(7),
+			openflow.AntreaPolicyIngressRuleTable: uint8(12),
+			openflow.IngressRuleTable:             uint8(13),
+			openflow.IngressDefaultTable:          uint8(14),
+		})
+	c := &Controller{ofClient: &openflowtest.MockClient{}}
+	ob := new(logInfo)
+	regID := openflow.APDispositionField.GetRegID()
+	dispositionMatch := openflow15.MatchField{
+		Class:   openflow15.OXM_CLASS_PACKET_REGS,
+		Field:   uint8(regID / 2),
+		HasMask: false,
+		Value:   &openflow15.ByteArrayField{Data: []byte{1, 1, 1, 1}},
+	}
+	matchers := []openflow15.MatchField{dispositionMatch}
+	pktIn := &ofctrl.PacketIn{TableId: 17, Match: openflow15.Match{Fields: matchers}}
+
+	err := getNetworkPolicyInfo(pktIn, c, ob)
+	assert.Equal(t, string(v1beta2.K8sNetworkPolicy), ob.npRef)
+	assert.Equal(t, "<nil>", ob.ofPriority)
+	assert.Equal(t, "<nil>", ob.ruleName)
+	require.NoError(t, err)
+}
+
+func TestGetPacketInfo(t *testing.T) {
+	tests := []struct {
+		name   string
+		packet *binding.Packet
+		ob     *logInfo
+		wantOb *logInfo
+	}{
+		{
+			name: "TCP packet",
+			packet: &binding.Packet{
+				SourceIP:        net.IPv4zero,
+				DestinationIP:   net.IPv4(1, 1, 1, 1),
+				IPLength:        60,
+				IPProto:         ip.TCPProtocol,
+				SourcePort:      35402,
+				DestinationPort: 80,
+			},
+			ob: new(logInfo),
+			wantOb: &logInfo{
+				srcIP:       "0.0.0.0",
+				srcPort:     "35402",
+				destIP:      "1.1.1.1",
+				destPort:    "80",
+				protocolStr: "TCP",
+				pktLength:   60,
+			},
+		},
+		{
+			name: "ICMP packet",
+			packet: &binding.Packet{
+				SourceIP:      net.IPv4zero,
+				DestinationIP: net.IPv4(1, 1, 1, 1),
+				IPLength:      60,
+				IPProto:       ip.ICMPProtocol,
+			},
+			ob: new(logInfo),
+			wantOb: &logInfo{
+				srcIP:       "0.0.0.0",
+				srcPort:     "<nil>",
+				destIP:      "1.1.1.1",
+				destPort:    "<nil>",
+				protocolStr: "ICMP",
+				pktLength:   60,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		getPacketInfo(tc.packet, tc.ob)
+		assert.Equal(t, tc.wantOb, tc.ob)
+	}
+}
diff --git a/pkg/agent/controller/trafficcontrol/controller_test.go b/pkg/agent/controller/trafficcontrol/controller_test.go
index 59fc7da73..498851ed4 100644
--- a/pkg/agent/controller/trafficcontrol/controller_test.go
+++ b/pkg/agent/controller/trafficcontrol/controller_test.go
@@ -279,7 +279,7 @@ func generateTrafficControlState(direction v1alpha2.Direction,
 }
 
 func waitEvents(t *testing.T, expectedEvents int, c *fakeController) {
-	require.NoError(t, wait.PollImmediate(10*time.Millisecond, time.Second, func() (done bool, err error) {
+	require.NoError(t, wait.PollImmediate(10*time.Millisecond, 5*time.Second, func() (done bool, err error) {
 		return c.queue.Len() == expectedEvents, nil
 	}))
 }
diff --git a/pkg/agent/flowexporter/exporter/exporter_test.go b/pkg/agent/flowexporter/exporter/exporter_test.go
index 72328ea5c..78e9b2040 100644
--- a/pkg/agent/flowexporter/exporter/exporter_test.go
+++ b/pkg/agent/flowexporter/exporter/exporter_test.go
@@ -26,7 +26,6 @@ import (
 	ipfixentities "github.com/vmware/go-ipfix/pkg/entities"
 	ipfixentitiestesting "github.com/vmware/go-ipfix/pkg/entities/testing"
 	"github.com/vmware/go-ipfix/pkg/exporter"
-	"github.com/vmware/go-ipfix/pkg/registry"
 	ipfixregistry "github.com/vmware/go-ipfix/pkg/registry"
 	"k8s.io/component-base/metrics/legacyregistry"
 
@@ -45,7 +44,7 @@ const (
 )
 
 func init() {
-	registry.LoadRegistry()
+	ipfixregistry.LoadRegistry()
 }
 
 func TestFlowExporter_sendTemplateSet(t *testing.T) {
@@ -389,11 +388,11 @@ func getConnection(isIPv6 bool, isPresent bool, statusFlag uint32, protoID uint8
 		DestinationPodName:            "",
 		IngressNetworkPolicyName:      "",
 		IngressNetworkPolicyNamespace: "",
-		IngressNetworkPolicyType:      registry.PolicyTypeK8sNetworkPolicy,
+		IngressNetworkPolicyType:      ipfixregistry.PolicyTypeK8sNetworkPolicy,
 		IngressNetworkPolicyRuleName:  "",
 		EgressNetworkPolicyName:       "np",
 		EgressNetworkPolicyNamespace:  "np-ns",
-		EgressNetworkPolicyType:       registry.PolicyTypeK8sNetworkPolicy,
+		EgressNetworkPolicyType:       ipfixregistry.PolicyTypeK8sNetworkPolicy,
 		EgressNetworkPolicyRuleName:   "",
 		DestinationServicePortName:    "service",
 		TCPState:                      tcpState,
diff --git a/pkg/agent/interfacestore/interface_cache_test.go b/pkg/agent/interfacestore/interface_cache_test.go
new file mode 100644
index 000000000..21a44f09c
--- /dev/null
+++ b/pkg/agent/interfacestore/interface_cache_test.go
@@ -0,0 +1,257 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package interfacestore
+
+import (
+	"net"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"antrea.io/antrea/pkg/agent/util"
+	"antrea.io/antrea/pkg/ovs/ovsconfig"
+)
+
+var (
+	podMAC, _     = net.ParseMAC("11:22:33:44:55:66")
+	podIP         = net.ParseIP("1.2.3.4")
+	gwIP          = net.ParseIP("1.2.3.1")
+	hostIP        = net.ParseIP("2.2.2.2")
+	ipsecTunnelIP = net.ParseIP("2.2.2.3")
+	nodeName      = "n1"
+	peerNodeName  = "n2"
+)
+
+func TestNewInterfaceStore(t *testing.T) {
+	t.Run("testContainerInterface", testContainerInterface)
+	t.Run("testGatewayInterface", testGatewayInterface)
+	t.Run("testTunnelInterface", testTunnelInterface)
+	t.Run("testUplinkInterface", testUplinkInterface)
+	t.Run("testTrafficControlInterface", testTrafficControlInterface)
+	t.Run("testExternalEntityInterface", testEntityInterface)
+}
+
+func testContainerInterface(t *testing.T) {
+	store := NewInterfaceStore()
+	containerInterface := NewContainerInterface("ns0p0c0", "c0", "p0", "ns0", podMAC, []net.IP{podIP}, 2)
+	containerInterface.OVSPortConfig = &OVSPortConfig{
+		OFPort:   12,
+		PortUUID: "1234567890",
+	}
+	containerInterfaceKey := util.GenerateContainerInterfaceKey(containerInterface.ContainerID)
+	store.Initialize([]*InterfaceConfig{containerInterface})
+	assert.Equal(t, 1, store.Len())
+	storedIface, exists := store.GetInterface(containerInterfaceKey)
+	assert.True(t, exists)
+	assert.True(t, reflect.DeepEqual(storedIface, containerInterface))
+	// The name of Container InterfaceConfig is not the key in InterfaceStore
+	_, exists = store.GetInterface(containerInterface.InterfaceName)
+	assert.False(t, exists)
+	_, exists = store.GetInterfaceByName(containerInterface.InterfaceName)
+	assert.True(t, exists)
+	_, exists = store.GetContainerInterface(containerInterface.ContainerID)
+	assert.True(t, exists)
+	_, exists = store.GetInterfaceByIP(podIP.String())
+	assert.True(t, exists)
+	_, exists = store.GetInterfaceByOFPort(uint32(containerInterface.OVSPortConfig.OFPort))
+	assert.True(t, exists)
+	ifaces := store.GetContainerInterfacesByPod(containerInterface.PodName, containerInterface.PodNamespace)
+	assert.Equal(t, 1, len(ifaces))
+	assert.True(t, reflect.DeepEqual(ifaces[0], containerInterface))
+	ifaceNames := store.GetInterfaceKeysByType(ContainerInterface)
+	assert.Equal(t, 1, len(ifaceNames))
+	assert.Equal(t, containerInterfaceKey, ifaceNames[0])
+	assert.Equal(t, 1, store.GetContainerInterfaceNum())
+	store.DeleteInterface(containerInterface)
+	assert.Equal(t, 0, store.GetContainerInterfaceNum())
+	_, exists = store.GetContainerInterface(containerInterface.ContainerID)
+	assert.False(t, exists)
+	_, exists = store.GetInterfaceByIP(containerInterface.IPs[0].String())
+	assert.False(t, exists)
+	containerInterface.IPs = nil
+	store.AddInterface(containerInterface)
+	assert.Equal(t, 1, store.GetContainerInterfaceNum())
+	_, exists = store.GetInterfaceByIP(podIP.String())
+	assert.False(t, exists)
+}
+
+func testGatewayInterface(t *testing.T) {
+	gatewayInterface := NewGatewayInterface("antrea-gw0")
+	gatewayInterface.IPs = []net.IP{gwIP}
+	gatewayInterface.OVSPortConfig = &OVSPortConfig{
+		OFPort:   13,
+		PortUUID: "1234567890",
+	}
+	testGeneralInterface(t, gatewayInterface, GatewayInterface)
+}
+
+func testTunnelInterface(t *testing.T) {
+	store := NewInterfaceStore()
+	tunnelInterface := NewTunnelInterface("antrea-tun0", ovsconfig.GeneveTunnel, 6081, hostIP, false)
+	tunnelInterface.IPs = []net.IP{hostIP}
+	tunnelInterface.OVSPortConfig = &OVSPortConfig{
+		OFPort:   14,
+		PortUUID: "1234567890",
+	}
+	ipsecTunnelInterface := NewIPSecTunnelInterface("antrea-ipsec0", ovsconfig.ERSPANTunnel, nodeName, hostIP, "abcdefg", peerNodeName)
+	ipsecTunnelInterface.IPs = []net.IP{ipsecTunnelIP}
+	ipsecTunnelInterface.OVSPortConfig = &OVSPortConfig{
+		OFPort:   15,
+		PortUUID: "1234567890",
+	}
+	store.Initialize([]*InterfaceConfig{tunnelInterface, ipsecTunnelInterface})
+	assert.Equal(t, 2, store.Len())
+	for _, tunIface := range []*InterfaceConfig{tunnelInterface, ipsecTunnelInterface} {
+		storedIface, exists := store.GetInterfaceByName(tunIface.InterfaceName)
+		assert.True(t, exists)
+		assert.True(t, reflect.DeepEqual(storedIface, tunIface))
+		storedIface, exists = store.GetInterfaceByIP(tunIface.IPs[0].String())
+		assert.True(t, exists)
+		assert.True(t, reflect.DeepEqual(storedIface, tunIface))
+		storedIface, exists = store.GetInterfaceByOFPort(uint32(tunIface.OVSPortConfig.OFPort))
+		assert.True(t, exists)
+		assert.True(t, reflect.DeepEqual(storedIface, tunIface))
+	}
+
+	ipsecTunnelKey := util.GenerateNodeTunnelInterfaceKey(ipsecTunnelInterface.NodeName)
+	storedIface, exists := store.GetInterface(ipsecTunnelKey)
+	assert.True(t, exists)
+	assert.True(t, reflect.DeepEqual(storedIface, ipsecTunnelInterface))
+	_, exists = store.GetInterface(ipsecTunnelInterface.InterfaceName)
+	assert.False(t, exists)
+	storedIface, exists = store.GetInterface(tunnelInterface.InterfaceName)
+	assert.True(t, exists)
+	assert.True(t, reflect.DeepEqual(storedIface, tunnelInterface))
+	storedIface, exists = store.GetNodeTunnelInterface(nodeName)
+	assert.True(t, exists)
+	assert.True(t, reflect.DeepEqual(storedIface, ipsecTunnelInterface))
+	_, exists = store.GetNodeTunnelInterface(peerNodeName)
+	assert.False(t, exists)
+
+	ifaceNames := store.GetInterfaceKeysByType(TunnelInterface)
+	assert.Equal(t, 2, len(ifaceNames))
+	store.DeleteInterface(ipsecTunnelInterface)
+	assert.Equal(t, 1, len(store.GetInterfaceKeysByType(TunnelInterface)))
+	_, exists = store.GetInterfaceByName(ipsecTunnelInterface.InterfaceName)
+	assert.False(t, exists)
+	store.AddInterface(ipsecTunnelInterface)
+	ifaceNames = store.GetInterfaceKeysByType(TunnelInterface)
+	assert.Equal(t, 2, len(ifaceNames))
+	_, exists = store.GetInterfaceByName(ipsecTunnelInterface.InterfaceName)
+	assert.True(t, exists)
+}
+
+func testUplinkInterface(t *testing.T) {
+	uplinkInterface := NewUplinkInterface("ens224")
+	uplinkInterface.IPs = []net.IP{hostIP}
+	uplinkInterface.OVSPortConfig = &OVSPortConfig{
+		OFPort:   16,
+		PortUUID: "1234567890",
+	}
+	testGeneralInterface(t, uplinkInterface, UplinkInterface)
+}
+
+func testTrafficControlInterface(t *testing.T) {
+	tcInterface := NewTrafficControlInterface("tc0")
+	tcInterface.IPs = []net.IP{hostIP}
+	tcInterface.OVSPortConfig = &OVSPortConfig{
+		OFPort:   17,
+		PortUUID: "1234567890",
+	}
+	testGeneralInterface(t, tcInterface, TrafficControlInterface)
+}
+
+func testEntityInterface(t *testing.T) {
+	store := NewInterfaceStore()
+	portConfig := &OVSPortConfig{OFPort: 18, PortUUID: "123456789"}
+	uplinkConfig := &OVSPortConfig{OFPort: 19, PortUUID: "987654321"}
+	entityIPv4 := net.ParseIP("2.3.4.5")
+	entityIPv6 := net.ParseIP("abcd::1234")
+	entityIPs := []net.IP{
+		entityIPv4,
+		entityIPv6,
+	}
+	entityInterface := newExternalEntityInterface("vm1-ens192", entityIPs, "ens192", "ns2", portConfig, uplinkConfig)
+	store.Initialize([]*InterfaceConfig{entityInterface})
+	storedIface, exists := store.GetInterface(entityInterface.InterfaceName)
+	assert.True(t, exists)
+	assert.True(t, reflect.DeepEqual(storedIface, entityInterface))
+	assert.Equal(t, entityIPv4, storedIface.GetIPv4Addr())
+	assert.Equal(t, entityIPv6, storedIface.GetIPv6Addr())
+	_, exists = store.GetInterfaceByName(entityInterface.InterfaceName)
+	assert.True(t, exists)
+	for _, entityIP := range entityInterface.IPs {
+		_, exists = store.GetInterfaceByIP(entityIP.String())
+		assert.True(t, exists)
+	}
+	_, exists = store.GetInterfaceByOFPort(uint32(entityInterface.OVSPortConfig.OFPort))
+	assert.True(t, exists)
+	_, exists = store.GetInterfaceByOFPort(uint32(entityInterface.UplinkPort.OFPort))
+	assert.False(t, exists)
+	ifaces := store.GetInterfacesByEntity(entityInterface.EntityName, entityInterface.EntityNamespace)
+	assert.Equal(t, 1, len(ifaces))
+	assert.True(t, reflect.DeepEqual(ifaces[0], entityInterface))
+	ifaceNames := store.GetInterfaceKeysByType(ExternalEntityInterface)
+	assert.Equal(t, 1, len(ifaceNames))
+	assert.Equal(t, entityInterface.InterfaceName, ifaceNames[0])
+	store.DeleteInterface(entityInterface)
+	assert.Equal(t, 0, len(store.GetInterfaceKeysByType(ExternalEntityInterface)))
+	store.AddInterface(entityInterface)
+	assert.Equal(t, 1, len(store.GetInterfaceKeysByType(ExternalEntityInterface)))
+}
+
+func testGeneralInterface(t *testing.T, ifaceConfig *InterfaceConfig, ifaceType InterfaceType) {
+	store := NewInterfaceStore()
+	store.Initialize([]*InterfaceConfig{ifaceConfig})
+	storedIface, exists := store.GetInterface(ifaceConfig.InterfaceName)
+	assert.True(t, exists)
+	assert.True(t, reflect.DeepEqual(storedIface, ifaceConfig))
+	_, exists = store.GetInterfaceByName(ifaceConfig.InterfaceName)
+	assert.True(t, exists)
+	_, exists = store.GetInterfaceByIP(ifaceConfig.IPs[0].String())
+	assert.True(t, exists)
+	_, exists = store.GetInterfaceByOFPort(uint32(ifaceConfig.OVSPortConfig.OFPort))
+	assert.True(t, exists)
+	fooPort := uint32(1)
+	_, exists = store.GetInterfaceByOFPort(fooPort)
+	assert.False(t, exists)
+	ifaceNames := store.GetInterfaceKeysByType(ifaceType)
+	assert.Equal(t, 1, len(ifaceNames))
+	assert.Equal(t, ifaceConfig.InterfaceName, ifaceNames[0])
+	store.DeleteInterface(ifaceConfig)
+	assert.Equal(t, 0, len(store.GetInterfaceKeysByType(ifaceType)))
+	store.AddInterface(ifaceConfig)
+	ifaceNames = store.GetInterfaceKeysByType(ifaceType)
+	assert.Equal(t, 1, len(ifaceNames))
+	assert.Equal(t, ifaceConfig.InterfaceName, ifaceNames[0])
+	ifaces := store.GetInterfacesByType(ifaceType)
+	assert.Equal(t, 1, len(ifaces))
+	assert.Equal(t, ifaceConfig, ifaces[0])
+}
+
+func newExternalEntityInterface(name string, entityIPs []net.IP, entityName string, entityNamespace string, ovsPortConfig, uplinkPortConfig *OVSPortConfig) *InterfaceConfig {
+	return &InterfaceConfig{
+		Type:          ExternalEntityInterface,
+		InterfaceName: name,
+		IPs:           entityIPs,
+		OVSPortConfig: ovsPortConfig,
+		EntityInterfaceConfig: &EntityInterfaceConfig{
+			EntityName:      entityName,
+			EntityNamespace: entityNamespace,
+			UplinkPort:      uplinkPortConfig,
+		},
+	}
+}
diff --git a/pkg/agent/multicast/mcast_discovery_test.go b/pkg/agent/multicast/mcast_discovery_test.go
index b23dff218..34673af4f 100644
--- a/pkg/agent/multicast/mcast_discovery_test.go
+++ b/pkg/agent/multicast/mcast_discovery_test.go
@@ -180,7 +180,6 @@ func generatePacketInForRemoteReport(t *testing.T, snooper *IGMPSnooper, groups
 }
 
 func createTunnelInterface(tunnelPort uint32, localNodeIP net.IP) *interfacestore.InterfaceConfig {
-	tunnelInterface := interfacestore.NewTunnelInterface("antrea-tun0", ovsconfig.GeneveTunnel, 6081, localNodeIP, false)
-	tunnelInterface.OVSPortConfig = &interfacestore.OVSPortConfig{OFPort: int32(tunnelPort)}
+	tunnelInterface := interfacestore.NewTunnelInterface("antrea-tun0", ovsconfig.GeneveTunnel, 6081, localNodeIP, false, &interfacestore.OVSPortConfig{OFPort: int32(tunnelPort)})
 	return tunnelInterface
 }
diff --git a/pkg/agent/openflow/multicast_test.go b/pkg/agent/openflow/multicast_test.go
new file mode 100644
index 000000000..2a6eec7f7
--- /dev/null
+++ b/pkg/agent/openflow/multicast_test.go
@@ -0,0 +1,185 @@
+// Copyright 2022 Antrea Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package openflow
+
+import (
+	"sync"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/require"
+
+	"antrea.io/antrea/pkg/agent/config"
+	"antrea.io/antrea/pkg/agent/openflow/cookie"
+	oftest "antrea.io/antrea/pkg/agent/openflow/testing"
+	binding "antrea.io/antrea/pkg/ovs/openflow"
+	mocks "antrea.io/antrea/pkg/ovs/openflow/testing"
+)
+
+var (
+	bakMulticastEgressRuleTable       binding.Table
+	bakMulticastEgressPodMetricTable  binding.Table
+	bakMulticastRoutingTable          binding.Table
+	bakMulticastOutputTable           binding.Table
+	bakMulticastIngressPodMetricTable binding.Table
+
+	mockMulticastEgressRuleTable       *mocks.MockTable
+	mockMulticastEgressPodMetricTable  *mocks.MockTable
+	mockMulticastRoutingTable          *mocks.MockTable
+	mockMulticastOutputTable           *mocks.MockTable
+	mockMulticastIngressPodMetricTable *mocks.MockTable
+
+	mockFeatureMulticast = featureMulticast{
+		enableAntreaPolicy: true,
+		cookieAllocator:    cookie.NewAllocator(0),
+		category:           cookie.Multicast,
+		cachedFlows:        newFlowCategoryCache(),
+		groupCache:         sync.Map{},
+	}
+)
+
+func newMockPktInFlowBuilder(ctrl *gomock.Controller) *mocks.MockFlowBuilder {
+	pktInFlowBuilder := mocks.NewMockFlowBuilder(ctrl)
+	pktInFlowBuilder.EXPECT().Cookie(gomock.Any()).Return(pktInFlowBuilder).AnyTimes()
+	pktInFlowBuilder.EXPECT().MatchProtocol(gomock.Any()).Return(pktInFlowBuilder).AnyTimes()
+	pktInFlowBuilder.EXPECT().MatchRegMark(gomock.Any()).Return(pktInFlowBuilder).AnyTimes()
+	action := mocks.NewMockAction(ctrl)
+	action.EXPECT().LoadRegMark(gomock.Any()).Return(pktInFlowBuilder).AnyTimes()
+	pktInFlowBuilder.EXPECT().Action().Return(action).Times(1)
+	action.EXPECT().SendToController(gomock.Any()).Return(pktInFlowBuilder).AnyTimes()
+	pktInFlowBuilder.EXPECT().Action().Return(action).Times(1)
+
+	pktInFlow := mocks.NewMockFlow(ctrl)
+	pktInFlowBuilder.EXPECT().Done().Return(pktInFlow).Times(1)
+	pktInFlow.EXPECT().MatchString().Return("").AnyTimes()
+
+	return pktInFlowBuilder
+}
+
+func newMockExternalMulticastReceiver(ctrl *gomock.Controller) *mocks.MockFlowBuilder {
+	externalMcastReceiverFlowBuilder := mocks.NewMockFlowBuilder(ctrl)
+
+	externalMcastReceiverFlowBuilder.EXPECT().Cookie(gomock.Any()).Return(externalMcastReceiverFlowBuilder).AnyTimes()
+	externalMcastReceiverFlowBuilder.EXPECT().MatchProtocol(gomock.Any()).Return(externalMcastReceiverFlowBuilder).AnyTimes()
+
+	action := mocks.NewMockAction(ctrl)
+	action.EXPECT().LoadRegMark(gomock.Any()).Return(externalMcastReceiverFlowBuilder).AnyTimes()
+	externalMcastReceiverFlowBuilder.EXPECT().Action().Return(action).Times(1)
+	action.EXPECT().LoadToRegField(gomock.Any(), gomock.Any()).Return(externalMcastReceiverFlowBuilder).AnyTimes()
+	externalMcastReceiverFlowBuilder.EXPECT().Action().Return(action).Times(1)
+	action.EXPECT().GotoStage(gomock.Any()).Return(externalMcastReceiverFlowBuilder).AnyTimes()
+	externalMcastReceiverFlowBuilder.EXPECT().Action().Return(action).Times(1)
+
+	externalMcastReceiverFlow := mocks.NewMockFlow(ctrl)
+	externalMcastReceiverFlowBuilder.EXPECT().Done().Return(externalMcastReceiverFlow).Times(1)
+	externalMcastReceiverFlow.EXPECT().MatchString().Return("").AnyTimes()
+
+	return externalMcastReceiverFlowBuilder
+}
+
+func newMockMulticastSkipIGMPMetricFlows(ctrl *gomock.Controller) *mocks.MockFlowBuilder {
+	mockFlowBuilder := mocks.NewMockFlowBuilder(ctrl)
+	mockFlowBuilder.EXPECT().Cookie(gomock.Any()).Return(mockFlowBuilder).AnyTimes()
+	mockFlowBuilder.EXPECT().MatchProtocol(gomock.Any()).Return(mockFlowBuilder).AnyTimes()
+	action := mocks.NewMockAction(ctrl)
+	action.EXPECT().NextTable().Return(mockFlowBuilder).AnyTimes()
+	mockFlowBuilder.EXPECT().Action().Return(action).Times(1)
+	mockFlow := mocks.NewMockFlow(ctrl)
+	mockFlowBuilder.EXPECT().Done().Return(mockFlow).Times(1)
+	mockFlow.EXPECT().MatchString().Return("").AnyTimes()
+	return mockFlowBuilder
+}
+
+func newMockIGMPFlowBuilder(ctrl *gomock.Controller) *mocks.MockFlowBuilder {
+	igmpFlowBuilder := mocks.NewMockFlowBuilder(ctrl)
+	igmpFlowBuilder.EXPECT().Cookie(gomock.Any()).Return(igmpFlowBuilder).Times(1)
+	igmpFlowBuilder.EXPECT().MatchProtocol(gomock.Any()).Return(igmpFlowBuilder).Times(1)
+	igmpFlowBuilder.EXPECT().MatchRegMark(gomock.Any()).Return(igmpFlowBuilder).Times(1)
+	action := mocks.NewMockAction(ctrl)
+	action.EXPECT().GotoStage(gomock.Any()).Return(igmpFlowBuilder).Times(1)
+	igmpFlowBuilder.EXPECT().Action().Return(action).Times(1)
+	igmpFlow := mocks.NewMockFlow(ctrl)
+	igmpFlowBuilder.EXPECT().Done().Return(igmpFlow).Times(1)
+	igmpFlow.EXPECT().MatchString().Return("").AnyTimes()
+	return igmpFlowBuilder
+}
+
+func initMockTables(ctrl *gomock.Controller) {
+	mockMulticastEgressRuleTable = mocks.NewMockTable(ctrl)
+	mockMulticastEgressPodMetricTable = mocks.NewMockTable(ctrl)
+	mockMulticastRoutingTable = mocks.NewMockTable(ctrl)
+	mockMulticastOutputTable = mocks.NewMockTable(ctrl)
+	mockMulticastIngressPodMetricTable = mocks.NewMockTable(ctrl)
+
+	bakMulticastEgressRuleTable = MulticastEgressRuleTable.ofTable
+	bakMulticastEgressPodMetricTable = MulticastEgressPodMetricTable.ofTable
+	bakMulticastRoutingTable = MulticastRoutingTable.ofTable
+	bakMulticastOutputTable = MulticastOutputTable.ofTable
+	bakMulticastIngressPodMetricTable = MulticastIngressPodMetricTable.ofTable
+
+	MulticastEgressRuleTable.ofTable = mockMulticastEgressRuleTable
+	MulticastEgressPodMetricTable.ofTable = mockMulticastEgressPodMetricTable
+	MulticastRoutingTable.ofTable = mockMulticastRoutingTable
+	MulticastOutputTable.ofTable = mockMulticastOutputTable
+	MulticastIngressPodMetricTable.ofTable = mockMulticastIngressPodMetricTable
+}
+
+func resetMulticastTables() {
+	MulticastEgressRuleTable.ofTable = bakMulticastEgressRuleTable
+	MulticastEgressPodMetricTable.ofTable = bakMulticastEgressPodMetricTable
+	MulticastRoutingTable.ofTable = bakMulticastRoutingTable
+	MulticastOutputTable.ofTable = bakMulticastOutputTable
+	MulticastIngressPodMetricTable.ofTable = bakMulticastIngressPodMetricTable
+}
+
+func newMcastClient(ctrl *gomock.Controller) *client {
+	bridge := mocks.NewMockBridge(ctrl)
+	bridge.EXPECT().AddFlowsInBundle(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
+	m := oftest.NewMockOFEntryOperations(ctrl)
+	m.EXPECT().AddAll(gomock.Any()).Return(nil).AnyTimes()
+	return &client{
+		bridge:             bridge,
+		ipProtocols:        []binding.Protocol{binding.ProtocolIP},
+		cookieAllocator:    cookie.NewAllocator(0),
+		ofEntryOperations:  m,
+		featureMulticast:   &mockFeatureMulticast,
+		enableMulticast:    true,
+		enableAntreaPolicy: true,
+	}
+}
+
+func TestInstallMulticastInitialFlows(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	c = newMcastClient(ctrl)
+	initMockTables(ctrl)
+	defer resetMulticastTables()
+
+	c.enableMulticast = true
+	c.enableAntreaPolicy = true
+	c.featureMulticast = &mockFeatureMulticast
+	mockMulticastRoutingTable.EXPECT().BuildFlow(gomock.Any()).Return(newMockPktInFlowBuilder(ctrl)).Times(1)
+	mockMulticastRoutingTable.EXPECT().BuildFlow(gomock.Any()).Return(newMockExternalMulticastReceiver(ctrl)).Times(1)
+	mockMulticastEgressPodMetricTable.EXPECT().BuildFlow(gomock.Any()).Return(newMockMulticastSkipIGMPMetricFlows(ctrl)).Times(1)
+	mockMulticastIngressPodMetricTable.EXPECT().BuildFlow(gomock.Any()).Return(newMockMulticastSkipIGMPMetricFlows(ctrl)).Times(1)
+	mockMulticastEgressRuleTable.EXPECT().BuildFlow(priorityTopAntreaPolicy).Return(newMockIGMPFlowBuilder(ctrl)).Times(1)
+	c.nodeConfig = &config.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: nil}
+	c.networkConfig = &config.NetworkConfig{}
+	c.pipelines = pipelineMap
+
+	err := c.InstallMulticastInitialFlows(uint8(PacketInReasonMC))
+	require.NoErrorf(t, err, "Failed to install multicast initial flows")
+}
diff --git a/pkg/agent/openflow/network_policy_test.go b/pkg/agent/openflow/network_policy_test.go
index f8a5b3d93..c22269587 100644
--- a/pkg/agent/openflow/network_policy_test.go
+++ b/pkg/agent/openflow/network_policy_test.go
@@ -1263,3 +1263,71 @@ func setMockOFTables(ctrl *gomock.Controller, tableMap map[*Table]**mocks.MockTa
 		*mockTable = t // Update the value with generated mock table.
 	}
 }
+
+func TestClient_GetPolicyInfoFromConjunction(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	preparePipelines()
+	c = prepareClient(ctrl, false)
+
+	ruleID1 := uint32(101)
+	ruleID2 := uint32(102)
+	npRef := &v1beta2.NetworkPolicyReference{
+		Type:      v1beta2.K8sNetworkPolicy,
+		Namespace: "ns1",
+		Name:      "np1",
+		UID:       "id1",
+	}
+	conj1 := &policyRuleConjunction{
+		id:       ruleID1,
+		npRef:    npRef,
+		ruleName: fmt.Sprint(ruleID1),
+	}
+	conj2 := &policyRuleConjunction{
+		id:          ruleID2,
+		actionFlows: []binding.Flow{EgressRuleTable.ofTable.BuildFlow(priority100).Done()},
+		npRef:       npRef,
+		ruleName:    fmt.Sprint(ruleID2),
+	}
+	c.featureNetworkPolicy.policyCache.Add(conj1)
+	c.featureNetworkPolicy.policyCache.Add(conj2)
+
+	tests := []struct {
+		name         string
+		ruleID       uint32
+		wantNpRef    string
+		wantPriority string
+		wantRuleName string
+	}{
+		{
+			name:         "conjunction not found",
+			ruleID:       uint32(100),
+			wantNpRef:    "",
+			wantPriority: "",
+			wantRuleName: "",
+		},
+		{
+			name:         "conjunction empty priorities",
+			ruleID:       ruleID1,
+			wantNpRef:    "",
+			wantPriority: "",
+			wantRuleName: "",
+		},
+		{
+			name:         "conjunction no error",
+			ruleID:       ruleID2,
+			wantNpRef:    "K8sNetworkPolicy:ns1/np1",
+			wantPriority: "100",
+			wantRuleName: fmt.Sprint(ruleID2),
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			gotNpRef, gotPriority, gotRuleName := c.GetPolicyInfoFromConjunction(tc.ruleID)
+			assert.Equal(t, tc.wantNpRef, gotNpRef)
+			assert.Equal(t, tc.wantPriority, gotPriority)
+			assert.Equal(t, tc.wantRuleName, gotRuleName)
+		})
+	}
+}
diff --git a/pkg/agent/openflow/pipeline_test.go b/pkg/agent/openflow/pipeline_test.go
index 2285ee27b..12bbb5a40 100644
--- a/pkg/agent/openflow/pipeline_test.go
+++ b/pkg/agent/openflow/pipeline_test.go
@@ -262,11 +262,13 @@ func TestBuildPipeline(t *testing.T) {
 				pipelineMulticast: {
 					MulticastEgressRuleTable,
 					MulticastEgressMetricTable,
+					MulticastEgressPodMetricTable,
 
 					MulticastRoutingTable,
 
 					MulticastIngressRuleTable,
 					MulticastIngressMetricTable,
+					MulticastIngressPodMetricTable,
 
 					MulticastOutputTable,
 				},
diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go
index be27e495a..393728772 100644
--- a/pkg/agent/openflow/testing/mock_openflow.go
+++ b/pkg/agent/openflow/testing/mock_openflow.go
@@ -197,12 +197,13 @@ func (mr *MockClientMockRecorder) GetPodFlowKeys(arg0 interface{}) *gomock.Call
 }
 
 // GetPolicyInfoFromConjunction mocks base method
-func (m *MockClient) GetPolicyInfoFromConjunction(arg0 uint32) (string, string) {
+func (m *MockClient) GetPolicyInfoFromConjunction(arg0 uint32) (string, string, string) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "GetPolicyInfoFromConjunction", arg0)
 	ret0, _ := ret[0].(string)
 	ret1, _ := ret[1].(string)
-	return ret0, ret1
+	ret2, _ := ret[2].(string)
+	return ret0, ret1, ret2
 }
 
 // GetPolicyInfoFromConjunction indicates an expected call of GetPolicyInfoFromConjunction
diff --git a/pkg/controller/egress/controller_test.go b/pkg/controller/egress/controller_test.go
index d5880ced7..0ec499208 100644
--- a/pkg/controller/egress/controller_test.go
+++ b/pkg/controller/egress/controller_test.go
@@ -36,7 +36,6 @@ import (
 
 	"antrea.io/antrea/pkg/apis/controlplane"
 	"antrea.io/antrea/pkg/apis/crd/v1alpha2"
-	corev1a2 "antrea.io/antrea/pkg/apis/crd/v1alpha2"
 	"antrea.io/antrea/pkg/client/clientset/versioned"
 	fakeversioned "antrea.io/antrea/pkg/client/clientset/versioned/fake"
 	crdinformers "antrea.io/antrea/pkg/client/informers/externalversions"
@@ -69,7 +68,7 @@ func newEgress(name, egressIP, externalIPPool string, podSelector, namespaceSele
 	egress := &v1alpha2.Egress{
 		ObjectMeta: metav1.ObjectMeta{Name: name},
 		Spec: v1alpha2.EgressSpec{
-			AppliedTo: corev1a2.AppliedTo{
+			AppliedTo: v1alpha2.AppliedTo{
 				PodSelector:       podSelector,
 				NamespaceSelector: namespaceSelector,
 			},
@@ -87,10 +86,10 @@ func newExternalIPPool(name, cidr, start, end string) *v1alpha2.ExternalIPPool {
 		},
 	}
 	if len(cidr) > 0 {
-		pool.Spec.IPRanges = append(pool.Spec.IPRanges, corev1a2.IPRange{CIDR: cidr})
+		pool.Spec.IPRanges = append(pool.Spec.IPRanges, v1alpha2.IPRange{CIDR: cidr})
 	}
 	if len(start) > 0 && len(end) > 0 {
-		pool.Spec.IPRanges = append(pool.Spec.IPRanges, corev1a2.IPRange{Start: start, End: end})
+		pool.Spec.IPRanges = append(pool.Spec.IPRanges, v1alpha2.IPRange{Start: start, End: end})
 	}
 	return pool
 }
@@ -171,7 +170,7 @@ func TestAddEgress(t *testing.T) {
 			inputEgress: &v1alpha2.Egress{
 				ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
 				Spec: v1alpha2.EgressSpec{
-					AppliedTo: corev1a2.AppliedTo{
+					AppliedTo: v1alpha2.AppliedTo{
 						PodSelector: &metav1.LabelSelector{
 							MatchLabels: map[string]string{"app": "foo"},
 						},
@@ -205,7 +204,7 @@ func TestAddEgress(t *testing.T) {
 			inputEgress: &v1alpha2.Egress{
 				ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
 				Spec: v1alpha2.EgressSpec{
-					AppliedTo: corev1a2.AppliedTo{
+					AppliedTo: v1alpha2.AppliedTo{
 						NamespaceSelector: &metav1.LabelSelector{
 							MatchLabels: nsDefault.Labels,
 						},
@@ -237,7 +236,7 @@ func TestAddEgress(t *testing.T) {
 			inputEgress: &v1alpha2.Egress{
 				ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
 				Spec: v1alpha2.EgressSpec{
-					AppliedTo: corev1a2.AppliedTo{
+					AppliedTo: v1alpha2.AppliedTo{
 						PodSelector: &metav1.LabelSelector{
 							MatchLabels: map[string]string{"app": "foo"},
 						},
@@ -269,7 +268,7 @@ func TestAddEgress(t *testing.T) {
 			inputEgress: &v1alpha2.Egress{
 				ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
 				Spec: v1alpha2.EgressSpec{
-					AppliedTo: corev1a2.AppliedTo{
+					AppliedTo: v1alpha2.AppliedTo{
 						PodSelector: &metav1.LabelSelector{
 							MatchLabels: map[string]string{"app": "foo"},
 						},
@@ -374,7 +373,7 @@ func TestUpdateEgress(t *testing.T) {
 	egress := &v1alpha2.Egress{
 		ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
 		Spec: v1alpha2.EgressSpec{
-			AppliedTo: corev1a2.AppliedTo{
+			AppliedTo: v1alpha2.AppliedTo{
 				PodSelector: &metav1.LabelSelector{
 					MatchLabels: map[string]string{"app": "foo"},
 				},
@@ -449,7 +448,7 @@ func TestUpdateEgress(t *testing.T) {
 	}, getEvent())
 
 	// Updating the Egress's spec to make it match no Pods on this Node and use a new ExternalIPPool.
-	egress.Spec.AppliedTo = corev1a2.AppliedTo{
+	egress.Spec.AppliedTo = v1alpha2.AppliedTo{
 		PodSelector: &metav1.LabelSelector{
 			MatchLabels: map[string]string{"app": "non-existing-app"},
 		},
diff --git a/pkg/graphviz/traceflow_test.go b/pkg/graphviz/traceflow_test.go
new file mode 100644
index 000000000..98558968e
--- /dev/null
+++ b/pkg/graphviz/traceflow_test.go
@@ -0,0 +1,371 @@
+// Copyright 2022 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package graphviz
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1"
+)
+
+func TestGenGraph(t *testing.T) {
+	liveTf := crdv1alpha1.Traceflow{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-live-traceflow",
+		},
+		Spec: crdv1alpha1.TraceflowSpec{
+			LiveTraffic: true,
+			Packet: crdv1alpha1.Packet{
+				IPHeader: crdv1alpha1.IPHeader{
+					Protocol: 1,
+				},
+				TransportHeader: crdv1alpha1.TransportHeader{
+					ICMP: &crdv1alpha1.ICMPEchoRequestHeader{
+						ID:       123,
+						Sequence: 1,
+					},
+				},
+			},
+			Source: crdv1alpha1.Source{
+				Namespace: "default",
+				Pod:       "test-pod",
+			},
+		},
+		Status: crdv1alpha1.TraceflowStatus{
+			CapturedPacket: &crdv1alpha1.Packet{
+				DstIP: "10.10.0.2",
+				IPHeader: crdv1alpha1.IPHeader{
+					Flags:    2,
+					Protocol: 1,
+					TTL:      63,
+				},
+				Length: 84,
+				SrcIP:  "10.10.1.4",
+			},
+			Phase: crdv1alpha1.Succeeded,
+			Results: []crdv1alpha1.NodeResult{
+				{
+					Node: "k8s-node-worker-1",
+					Observations: []crdv1alpha1.Observation{
+						{
+							Action:    crdv1alpha1.ActionForwarded,
+							Component: crdv1alpha1.ComponentSpoofGuard,
+						},
+						{
+							Action:        crdv1alpha1.ActionForwarded,
+							Component:     crdv1alpha1.ComponentForwarding,
+							ComponentInfo: "Output",
+							TunnelDstIP:   "192.168.77.100",
+						},
+					},
+				},
+				{
+					Node: "k8s-node-control-plane",
+					Observations: []crdv1alpha1.Observation{
+						{
+							Action:        crdv1alpha1.ActionReceived,
+							Component:     crdv1alpha1.ComponentForwarding,
+							ComponentInfo: "Classification",
+						},
+						{
+							Action:        crdv1alpha1.ActionDelivered,
+							Component:     crdv1alpha1.ComponentForwarding,
+							ComponentInfo: "Output",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	liveTfWithSourceIP := crdv1alpha1.Traceflow{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-live-traceflow-source-ip",
+		},
+		Spec: crdv1alpha1.TraceflowSpec{
+			LiveTraffic: true,
+			Packet: crdv1alpha1.Packet{
+				IPHeader: crdv1alpha1.IPHeader{
+					Protocol: 1,
+				},
+				TransportHeader: crdv1alpha1.TransportHeader{},
+			},
+			Source: crdv1alpha1.Source{
+				IP: "192.168.225.5",
+			},
+		},
+		Status: crdv1alpha1.TraceflowStatus{
+			CapturedPacket: &crdv1alpha1.Packet{
+				DstIP: "192.168.226.5",
+				IPHeader: crdv1alpha1.IPHeader{
+					Flags:    2,
+					Protocol: 1,
+					TTL:      63,
+				},
+				Length: 84,
+				SrcIP:  "192.168.225.5",
+			},
+			Phase: crdv1alpha1.Succeeded,
+			Results: []crdv1alpha1.NodeResult{
+				{
+					Node: "k8s-node-1",
+					Observations: []crdv1alpha1.Observation{
+						{
+							Action:    crdv1alpha1.ActionReceived,
+							Component: crdv1alpha1.ComponentForwarding,
+						},
+						{
+							Action:        crdv1alpha1.ActionDelivered,
+							Component:     crdv1alpha1.ComponentForwarding,
+							ComponentInfo: "Output",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	tfInOneNode := crdv1alpha1.Traceflow{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-traceflow-one-node",
+		},
+		Spec: crdv1alpha1.TraceflowSpec{
+			Packet: crdv1alpha1.Packet{
+				IPHeader: crdv1alpha1.IPHeader{
+					Protocol: 1,
+				},
+				TransportHeader: crdv1alpha1.TransportHeader{},
+			},
+			Source: crdv1alpha1.Source{
+				Namespace: "default",
+				Pod:       "pod-1",
+			},
+			Destination: crdv1alpha1.Destination{
+				Namespace: "default",
+				Pod:       "pod-2",
+			},
+		},
+		Status: crdv1alpha1.TraceflowStatus{
+			Phase: crdv1alpha1.Succeeded,
+			Results: []crdv1alpha1.NodeResult{
+				{
+					Node: "k8s-node-1",
+					Observations: []crdv1alpha1.Observation{
+						{
+							Action:    crdv1alpha1.ActionForwarded,
+							Component: crdv1alpha1.ComponentSpoofGuard,
+						},
+						{
+							Action:        crdv1alpha1.ActionDelivered,
+							Component:     crdv1alpha1.ComponentForwarding,
+							ComponentInfo: "Output",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	expectedOutputWithSourcePod := `digraph G {
+	center=true;
+	label="test-live-traceflow";
+	labelloc=t;
+	"default/test-pod"->cluster_source_1[ color="#C0C0C0", dir=forward, minlen=1, penwidth=2.0 ];
+	cluster_source_1->cluster_source_2[ color="#C0C0C0", dir=forward, minlen=1, penwidth=2.0 ];
+	"10.10.0.2"->cluster_destination_1[ color="#C0C0C0", dir=back, minlen=1, penwidth=2.0 ];
+	cluster_destination_1->cluster_destination_2[ color="#C0C0C0", dir=back, minlen=1, penwidth=2.0 ];
+	cluster_source_2->cluster_destination_2[ color="#C0C0C0", constraint=false, dir=forward, penwidth=2.0 ];
+	subgraph cluster_source {
+	bgcolor="#F8F8FF";
+	label="k8s-node-worker-1";
+	labeljust=l;
+	style="filled,bold";
+	"default/test-pod" [ color="#808080", fillcolor="#C8C8C8", style="filled,bold" ];
+	cluster_source_1 [ color="#696969", fillcolor="#DCDCDC", label="SpoofGuard
+Forwarded", shape=box, style="rounded,filled,solid" ];
+	cluster_source_2 [ color="#696969", fillcolor="#DCDCDC", label="Forwarding
+Output
+Forwarded
+Tunnel Destination IP : 192.168.77.100", shape=box, style="rounded,filled,solid" ];
+
+}
+;
+	subgraph cluster_destination {
+	bgcolor="#F8F8FF";
+	label="k8s-node-control-plane";
+	labeljust=r;
+	style="filled,bold";
+	"10.10.0.2" [ color="#808080", fillcolor="#C8C8C8", style="filled,bold" ];
+	capturedPacket [ color="#696969", label="Captured Packet:\lSource IP: 10.10.1.4\lDestination IP: 10.10.0.2\lLength: 84\lIPv4 Header: \l    Flags: 2\l    Protocol: 1\l    TTL: 63\l", shape=note, style="rounded,filled,solid" ];
+	cluster_destination_1 [ color="#696969", fillcolor="#DCDCDC", label="Forwarding
+Output
+Delivered", shape=box, style="rounded,filled,solid" ];
+	cluster_destination_2 [ color="#696969", fillcolor="#DCDCDC", label="Forwarding
+Classification
+Received", shape=box, style="rounded,filled,solid" ];
+
+}
+;
+
+}
+`
+
+	expectedOutputWithSourceIP := `digraph G {
+	center=true;
+	label="test-live-traceflow-source-ip";
+	labelloc=t;
+	newrank=true;
+	"192.168.225.5"->"192.168.225.5"[ style=invis ];
+	"192.168.226.5"->cluster_destination_1[ color="#C0C0C0", dir=back, minlen=1, penwidth=2.0 ];
+	cluster_destination_1->cluster_destination_2[ color="#C0C0C0", dir=back, minlen=1, penwidth=2.0 ];
+	"192.168.225.5"->cluster_destination_2[ color="#C0C0C0", constraint=false, dir=forward, penwidth=2.0 ];
+	subgraph cluster_source {
+	bgcolor="#F8F8FF";
+	label=source;
+	labeljust=l;
+	style="filled,bold";
+	"192.168.225.5" [ color="#808080", fillcolor="#C8C8C8", style="filled,bold" ];
+
+}
+;
+	subgraph cluster_destination {
+	bgcolor="#F8F8FF";
+	label="k8s-node-1";
+	labeljust=r;
+	style="filled,bold";
+	"192.168.226.5" [ color="#808080", fillcolor="#C8C8C8", style="filled,bold" ];
+	capturedPacket [ color="#696969", label="Captured Packet:\lSource IP: 192.168.225.5\lDestination IP: 192.168.226.5\lLength: 84\lIPv4 Header: \l    Flags: 2\l    Protocol: 1\l    TTL: 63\l", shape=note, style="rounded,filled,solid" ];
+	cluster_destination_1 [ color="#696969", fillcolor="#DCDCDC", label="Forwarding
+Output
+Delivered", shape=box, style="rounded,filled,solid" ];
+	cluster_destination_2 [ color="#696969", fillcolor="#DCDCDC", label="Forwarding
+Received", shape=box, style="rounded,filled,solid" ];
+
+}
+;
+	subgraph force_node_same_level {
+	rank=same;
+	"192.168.225.5" [ color="#808080", fillcolor="#C8C8C8", style="filled,bold" ];
+	cluster_destination_2 [ color="#696969", fillcolor="#DCDCDC", label="Forwarding
+Received", shape=box, style="rounded,filled,solid" ];
+
+}
+;
+
+}
+`
+
+	expectedOutputInOneNode := `digraph G {
+	center=true;
+	label="test-traceflow-one-node";
+	labelloc=t;
+	"default/pod-1"->cluster_source_1[ color="#C0C0C0", dir=forward, minlen=1, penwidth=2.0 ];
+	cluster_source_1->cluster_source_2[ color="#C0C0C0", dir=forward, minlen=1, penwidth=2.0 ];
+	cluster_source_2->"default/pod-2"[ color="#C0C0C0", dir=forward, penwidth=2.0 ];
+	subgraph cluster_source {
+	bgcolor="#F8F8FF";
+	label="k8s-node-1";
+	labeljust=l;
+	style="filled,bold";
+	"default/pod-1" [ color="#808080", fillcolor="#C8C8C8", style="filled,bold" ];
+	"default/pod-2" [ color="#808080", fillcolor="#C8C8C8", style="filled,bold" ];
+	cluster_source_1 [ color="#696969", fillcolor="#DCDCDC", label="SpoofGuard
+Forwarded", shape=box, style="rounded,filled,solid" ];
+	cluster_source_2 [ color="#696969", fillcolor="#DCDCDC", label="Forwarding
+Output
+Delivered", shape=box, style="rounded,filled,solid" ];
+
+}
+;
+
+}
+`
+	tests := []struct {
+		name           string
+		traceflow      crdv1alpha1.Traceflow
+		expectedOutput string
+	}{
+		{
+			name:           "live traceflow with source Pod",
+			traceflow:      liveTf,
+			expectedOutput: expectedOutputWithSourcePod,
+		},
+		{
+			name:           "live traceflow with source IP",
+			traceflow:      liveTfWithSourceIP,
+			expectedOutput: expectedOutputWithSourceIP,
+		},
+		{
+			name:           "traceflow in one Node",
+			traceflow:      tfInOneNode,
+			expectedOutput: expectedOutputInOneNode,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			output, err := GenGraph(&tt.traceflow)
+			assert.NoError(t, err)
+			assert.Equal(t, tt.expectedOutput, output)
+		})
+	}
+}
+
+func TestGetTraceflowStatusMessage(t *testing.T) {
+	baseTf := crdv1alpha1.Traceflow{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-tf",
+		},
+		Status: crdv1alpha1.TraceflowStatus{
+			Reason: "test",
+		},
+	}
+	tests := []struct {
+		name         string
+		phase        crdv1alpha1.TraceflowPhase
+		expectOutput string
+	}{
+		{
+			name:         "failed traceflow",
+			phase:        crdv1alpha1.Failed,
+			expectOutput: "\"Traceflow test-tf failed: test\"",
+		},
+		{
+			name:         "running traceflow",
+			phase:        crdv1alpha1.Running,
+			expectOutput: "\"Traceflow test-tf is running...\"",
+		},
+		{
+			name:         "pending traceflow",
+			phase:        crdv1alpha1.Pending,
+			expectOutput: "\"Traceflow test-tf is pending...\"",
+		},
+		{
+			name:         "pending traceflow",
+			expectOutput: "\"Unknown Traceflow status. Please check whether Antrea is running.\"",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			baseTf.Status.Phase = tt.phase
+			got := getTraceflowStatusMessage(&baseTf)
+			assert.Equal(t, tt.expectOutput, got)
+		})
+	}
+}
diff --git a/test/e2e/antreapolicy_test.go b/test/e2e/antreapolicy_test.go
index 60979921d..7c9ea1aaf 100644
--- a/test/e2e/antreapolicy_test.go
+++ b/test/e2e/antreapolicy_test.go
@@ -2557,12 +2557,14 @@ func testANPMultipleAppliedTo(t *testing.T, data *TestData, singleRule bool) {
 
 // testAuditLoggingBasic tests that audit logs are generated when egress drop applied
 func testAuditLoggingBasic(t *testing.T, data *TestData) {
+	npRef := "test-log-acnp-deny"
+	ruleName := "DropToZ"
 	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("test-log-acnp-deny").
+	builder = builder.SetName(npRef).
 		SetPriority(1.0).
 		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}, NSSelector: map[string]string{"ns": namespaces["x"]}}})
 	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"ns": namespaces["z"]},
-		nil, nil, false, nil, crdv1alpha1.RuleActionDrop, "", "", nil)
+		nil, nil, false, nil, crdv1alpha1.RuleActionDrop, "", ruleName, nil)
 	builder.AddEgressLogging()
 
 	acnp, err := k8sUtils.CreateOrUpdateACNP(builder.Get())
@@ -2621,7 +2623,7 @@ func testAuditLoggingBasic(t *testing.T, data *TestData) {
 					}
 					expectedNumEntries += 1
 					// The audit log should contain log entry `... Drop <ofPriority> <x/a IP> <z/* IP> ...`
-					re := regexp.MustCompile(`Drop [0-9]+ ` + srcIPs[i] + ` [0-9]+ ` + dstIPs[j] + ` ` + strconv.Itoa(int(p80)))
+					re := regexp.MustCompile(npRef + ` ` + ruleName + ` Drop [0-9]+ ` + srcIPs[i] + ` [0-9]+ ` + dstIPs[j] + ` ` + strconv.Itoa(int(p80)))
 					if re.MatchString(stdout) {
 						actualNumEntries += 1
 					} else {
@@ -2649,8 +2651,9 @@ func testAuditLoggingEnableNP(t *testing.T, data *TestData) {
 	failOnError(data.updateNamespaceWithAnnotations(namespaces["x"], map[string]string{networkpolicy.EnableNPLoggingAnnotationKey: "true"}), t)
 	// Add a K8s namespaced NetworkPolicy in ns x that allow ingress traffic from
 	// Pod x/b to x/a which default denies other ingress including from Pod x/c to x/a
+	npRef := "allow-x-b-to-x-a"
 	k8sNPBuilder := &NetworkPolicySpecBuilder{}
-	k8sNPBuilder = k8sNPBuilder.SetName(namespaces["x"], "allow-x-b-to-x-a").
+	k8sNPBuilder = k8sNPBuilder.SetName(namespaces["x"], npRef).
 		SetPodSelector(map[string]string{"pod": "a"}).
 		SetTypeIngress().
 		AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil,
@@ -2699,7 +2702,7 @@ func testAuditLoggingEnableNP(t *testing.T, data *TestData) {
 
 		var expectedNumEntries, actualNumEntries int
 		srcPods := []string{namespaces["x"] + "/b", namespaces["x"] + "/c"}
-		expectedLogPrefix := []string{"allow-x-b-to-x-a Allow [0-9]+ ", "K8sNetworkPolicy Drop -1 "}
+		expectedLogPrefix := []string{npRef + " <nil> Allow [0-9]+ ", "K8sNetworkPolicy <nil> Drop <nil> "}
 		destIPs, _ := podIPs[namespaces["x"]+"/a"]
 		for i := 0; i < len(srcPods); i++ {
 			srcIPs, _ := podIPs[srcPods[i]]
diff --git a/test/e2e/providers/vagrant.go b/test/e2e/providers/vagrant.go
index e1d546a5f..ea90ac36f 100644
--- a/test/e2e/providers/vagrant.go
+++ b/test/e2e/providers/vagrant.go
@@ -139,7 +139,7 @@ func (provider *VagrantProvider) GetKubeconfigPath() (string, error) {
 	}
 	kubeconfigPath := path.Join(vagrantPath, "playbook", "kube", "config")
 	if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) {
-		return "", fmt.Errorf("Kubeconfig file not found at expected location '%s'", kubeconfigPath)
+		return "", fmt.Errorf("kubeconfig file not found at expected location '%s'", kubeconfigPath)
 	}
 	return kubeconfigPath, nil
 }
diff --git a/test/e2e/service_externalip_test.go b/test/e2e/service_externalip_test.go
index a639bb808..40635e36f 100644
--- a/test/e2e/service_externalip_test.go
+++ b/test/e2e/service_externalip_test.go
@@ -26,7 +26,6 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -215,7 +214,7 @@ func testServiceExternalTrafficPolicyLocal(t *testing.T, data *TestData) {
 				antreaagenttypes.ServiceExternalIPPoolAnnotationKey: ipPool.Name,
 			}
 			service, err = data.CreateServiceWithAnnotations(fmt.Sprintf("test-svc-local-%d", idx),
-				data.testNamespace, 80, 80, corev1.ProtocolTCP, nil, false, true, v1.ServiceTypeLoadBalancer, nil, annotation)
+				data.testNamespace, 80, 80, v1.ProtocolTCP, nil, false, true, v1.ServiceTypeLoadBalancer, nil, annotation)
 			require.NoError(t, err)
 			defer data.clientset.CoreV1().Services(service.Namespace).Delete(context.TODO(), service.Name, metav1.DeleteOptions{})
 
@@ -332,7 +331,7 @@ func testServiceWithExternalIPCRUD(t *testing.T, data *TestData) {
 				antreaagenttypes.ServiceExternalIPPoolAnnotationKey: ipPool.Name,
 			}
 			service, err = data.CreateServiceWithAnnotations(fmt.Sprintf("test-svc-eip-%d", idx),
-				data.testNamespace, 80, 80, corev1.ProtocolTCP, nil, false, false, v1.ServiceTypeLoadBalancer, nil, annotation)
+				data.testNamespace, 80, 80, v1.ProtocolTCP, nil, false, false, v1.ServiceTypeLoadBalancer, nil, annotation)
 			require.NoError(t, err)
 
 			defer data.clientset.CoreV1().Services(service.Namespace).Delete(context.TODO(), service.Name, metav1.DeleteOptions{})
@@ -424,7 +423,7 @@ func testServiceUpdateExternalIP(t *testing.T, data *TestData) {
 				antreaagenttypes.ServiceExternalIPPoolAnnotationKey: originalPool.Name,
 			}
 			service, err := data.CreateServiceWithAnnotations(fmt.Sprintf("test-update-eip-%d", idx),
-				data.testNamespace, 80, 80, corev1.ProtocolTCP, nil, false, false, v1.ServiceTypeLoadBalancer, nil, annotation)
+				data.testNamespace, 80, 80, v1.ProtocolTCP, nil, false, false, v1.ServiceTypeLoadBalancer, nil, annotation)
 			require.NoError(t, err)
 			defer data.clientset.CoreV1().Services(service.Namespace).Delete(context.TODO(), service.Name, metav1.DeleteOptions{})
 
@@ -506,7 +505,7 @@ func testServiceNodeFailure(t *testing.T, data *TestData) {
 				antreaagenttypes.ServiceExternalIPPoolAnnotationKey: externalIPPoolTwoNodes.Name,
 			}
 			service, err := data.CreateServiceWithAnnotations("test-service-node-failure", data.testNamespace, 80, 80,
-				corev1.ProtocolTCP, nil, false, false, v1.ServiceTypeLoadBalancer, nil, annotation)
+				v1.ProtocolTCP, nil, false, false, v1.ServiceTypeLoadBalancer, nil, annotation)
 			require.NoError(t, err)
 			defer data.clientset.CoreV1().Services(service.Namespace).Delete(context.TODO(), service.Name, metav1.DeleteOptions{})
 
@@ -565,10 +564,10 @@ func testExternalIPAccess(t *testing.T, data *TestData) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			ipFamily := corev1.IPv4Protocol
+			ipFamily := v1.IPv4Protocol
 			if utilnet.IsIPv6CIDRString(tt.externalIPCIDR) {
 				skipIfNotIPv6Cluster(t)
-				ipFamily = corev1.IPv6Protocol
+				ipFamily = v1.IPv6Protocol
 			} else {
 				skipIfNotIPv4Cluster(t)
 			}
@@ -624,7 +623,7 @@ func testExternalIPAccess(t *testing.T, data *TestData) {
 					annotations := map[string]string{
 						antreaagenttypes.ServiceExternalIPPoolAnnotationKey: ipPool.Name,
 					}
-					service, err := data.CreateServiceWithAnnotations(et.serviceName, data.testNamespace, port, port, corev1.ProtocolTCP, map[string]string{"app": "agnhost"}, false, et.externalTrafficPolicyLocal, corev1.ServiceTypeLoadBalancer, &ipFamily, annotations)
+					service, err := data.CreateServiceWithAnnotations(et.serviceName, data.testNamespace, port, port, v1.ProtocolTCP, map[string]string{"app": "agnhost"}, false, et.externalTrafficPolicyLocal, v1.ServiceTypeLoadBalancer, &ipFamily, annotations)
 					require.NoError(t, err)
 					defer data.deleteService(service.Namespace, service.Name)
 
@@ -664,8 +663,8 @@ sleep 3600`, tt.clientName, tt.clientIP, tt.localIP, tt.clientIPMaskLen)
 
 					_, err = data.PodWaitFor(defaultTimeout, tt.clientName, data.testNamespace, func(p *v1.Pod) (bool, error) {
 						for _, condition := range p.Status.Conditions {
-							if condition.Type == corev1.PodReady {
-								return condition.Status == corev1.ConditionTrue, nil
+							if condition.Type == v1.PodReady {
+								return condition.Status == v1.ConditionTrue, nil
 							}
 						}
 						return false, nil
@@ -722,7 +721,7 @@ func (data *TestData) getServiceAssignedNode(node string, service *v1.Service) (
 	return serviceExternalIPInfo[0].AssignedNode, nil
 }
 
-func (data *TestData) waitForServiceConfigured(service *v1.Service, expectedExternalIP string, expectedNodeName string) (*corev1.Service, string, error) {
+func (data *TestData) waitForServiceConfigured(service *v1.Service, expectedExternalIP string, expectedNodeName string) (*v1.Service, string, error) {
 	var assignedNode string
 	err := wait.PollImmediate(200*time.Millisecond, 15*time.Second, func() (done bool, err error) {
 		service, err = data.clientset.CoreV1().Services(service.Namespace).Get(context.TODO(), service.Name, metav1.GetOptions{})