Repository: antrea-io/antrea
Pull request #6264 (2024-04-26 09:30:27)

Skip IPSec/WireGuard e2e test when the Multicast feature is enabled
This is an e2e fix because of PR #5920.


Patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index fb0233b38..9a9e0c658 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -7,7 +7,24 @@ on:
     branches: [ "main" ]
 
 jobs:
+  check-changes:
+    name: Check whether tests need to be run based on diff
+    runs-on: [ubuntu-latest]
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+        show-progress: false
+    - uses: antrea-io/has-changes@v2
+      id: check_diff
+      with:
+        paths-ignore: docs/* ci/jenkins/* *.md hack/.notableofcontents
+    outputs:
+      has_changes: ${{ steps.check_diff.outputs.has_changes }}
+
   analyze-on-linux:
+    needs: check-changes
+    if: ${{ needs.check-changes.outputs.has_changes == 'yes' }}
     name: Analyze on Linux
     runs-on: ubuntu-latest
     permissions:
@@ -50,6 +67,8 @@ jobs:
         category: "/language:${{matrix.language}}"
 
   analyze-on-windows:
+    needs: check-changes
+    if: ${{ needs.check-changes.outputs.has_changes == 'yes' }}
     name: Analyze on Windows
     runs-on: windows-latest
     permissions:
diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml
index b6873ea2a..e252cde02 100644
--- a/.github/workflows/kind.yml
+++ b/.github/workflows/kind.yml
@@ -238,7 +238,8 @@ jobs:
             --proxy-all \
             --node-ipam \
             --extra-vlan \
-            --multicast
+            --multicast \
+            --networkpolicy-evaluation
       - name: Tar coverage files
         run: tar -czf test-e2e-encap-all-features-enabled-coverage.tar.gz test-e2e-encap-all-features-enabled-coverage
       - name: Upload coverage for test-e2e-encap-all-features-enabled-coverage
@@ -741,6 +742,47 @@ jobs:
           path: log.tar.gz
           retention-days: 30
 
+  run-post-installation-checks:
+    name: Test connectivity using 'antctl check' command
+    needs: [ build-antrea-coverage-image ]
+    runs-on: [ ubuntu-latest ]
+    steps:
+      - name: Free disk space
+        run: |
+          sudo apt-get clean
+          df -h
+      - uses: actions/checkout@v4
+        with:
+          show-progress: false
+      - uses: actions/setup-go@v5
+        with:
+          go-version-file: 'go.mod'
+      - name: Download Antrea image from previous job
+        uses: actions/download-artifact@v4
+        with:
+          name: antrea-ubuntu-cov
+      - name: Load Antrea image
+        run: |
+          docker load -i antrea-ubuntu.tar
+      - name: Install Kind
+        run: |
+          KIND_VERSION=$(head -n1 ./ci/kind/version)
+          curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-$(uname)-amd64
+          chmod +x ./kind
+          sudo mv kind /usr/local/bin
+      - name: Create Kind Cluster
+        run: |
+          ./ci/kind/kind-setup.sh create kind --ip-family dual
+      - name: Deploy Antrea
+        run: |
+          kubectl apply -f build/yamls/antrea.yml
+      - name: Build antctl binary
+        run: |
+          make antctl-linux
+      - name: Run antctl command
+        run: |
+          ./bin/antctl-linux check installation
+
   validate-prometheus-metrics-doc:
     name: Validate metrics in Prometheus document match running deployment's
     needs: build-antrea-coverage-image
diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml
index 0710e6aa0..52993ab2b 100644
--- a/.github/workflows/trivy_scan.yml
+++ b/.github/workflows/trivy_scan.yml
@@ -37,7 +37,7 @@ jobs:
         docker pull antrea/antrea-controller-ubuntu:${{ steps.find-antrea-greatest-version.outputs.antrea_version }}
     - name: Run Trivy vulnerability scanner on latest antrea-agent Docker image
       if: ${{ always() && steps.pull.conclusion == 'success' }}
-      uses: aquasecurity/trivy-action@0.19.0
+      uses: aquasecurity/trivy-action@0.20.0
       # we cannot use .trivy.yml as we need to override some config parameters
       # and that is not supported by aquasecurity/trivy-action
       with:
@@ -50,7 +50,7 @@ jobs:
         output: 'trivy.latest.txt'
     - name: Run Trivy vulnerability scanner on latest antrea-controller Docker image
       if: ${{ always() && steps.pull.conclusion == 'success' }}
-      uses: aquasecurity/trivy-action@0.19.0
+      uses: aquasecurity/trivy-action@0.20.0
       # we cannot use .trivy.yml as we need to override some config parameters
       # and that is not supported by aquasecurity/trivy-action
       with:
@@ -63,7 +63,7 @@ jobs:
         output: 'trivy.latest.txt'
     - name: Run Trivy vulnerability scanner on antrea-agent Docker image for latest released version
       if: ${{ always() && steps.pull.conclusion == 'success' }}
-      uses: aquasecurity/trivy-action@0.19.0
+      uses: aquasecurity/trivy-action@0.20.0
       with:
         scan-type: 'image'
         image-ref: 'antrea/antrea-agent-ubuntu:${{ steps.find-antrea-greatest-version.outputs.antrea_version }}'
@@ -74,7 +74,7 @@ jobs:
         output: 'trivy.${{ steps.find-antrea-greatest-version.outputs.antrea_version }}.txt'
     - name: Run Trivy vulnerability scanner on antrea-controller Docker image for latest released version
       if: ${{ always() && steps.pull.conclusion == 'success' }}
-      uses: aquasecurity/trivy-action@0.19.0
+      uses: aquasecurity/trivy-action@0.20.0
       with:
         scan-type: 'image'
         image-ref: 'antrea/antrea-controller-ubuntu:${{ steps.find-antrea-greatest-version.outputs.antrea_version }}'
diff --git a/.github/workflows/trivy_scan_before_release.yml b/.github/workflows/trivy_scan_before_release.yml
index b68f179b7..398804097 100644
--- a/.github/workflows/trivy_scan_before_release.yml
+++ b/.github/workflows/trivy_scan_before_release.yml
@@ -15,13 +15,13 @@ jobs:
       run: |
         ./hack/build-antrea-linux-all.sh --pull
     - name: Run Trivy vulnerability scanner on the antrea-agent Docker image
-      uses: aquasecurity/trivy-action@0.19.0
+      uses: aquasecurity/trivy-action@0.20.0
       with:
         scan-type: 'image'
         image-ref: 'antrea/antrea-agent-ubuntu:latest'
         trivy-config: '.trivy.yml'
     - name: Run Trivy vulnerability scanner on the antrea-controller Docker image
-      uses: aquasecurity/trivy-action@0.19.0
+      uses: aquasecurity/trivy-action@0.20.0
       with:
         scan-type: 'image'
         image-ref: 'antrea/antrea-controller-ubuntu:latest'
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3a17c7021..16325590c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -183,6 +183,14 @@ Here are the trigger phrases for individual checks:
 * `/test-rancher-e2e`: Linux IPv4 e2e tests on Rancher clusters.
 * `/test-rancher-conformance`: Linux IPv4 conformance tests on Rancher clusters.
 * `/test-rancher-networkpolicy`: Linux IPv4 networkpolicy tests on Rancher clusters.
+* `/test-kind-ipv6-e2e`: Linux dual stack e2e tests on Kind cluster.
+* `/test-kind-ipv6-only-e2e`: Linux IPv6 only e2e tests on Kind cluster.
+* `/test-kind-conformance`: Linux IPv4 conformance tests on Kind cluster.
+* `/test-kind-ipv6-only-conformance`: Linux IPv6 only conformance tests on Kind cluster.
+* `/test-kind-ipv6-conformance`: Linux dual stack conformance tests on Kind cluster.
+* `/test-kind-networkpolicy`: Linux IPv4 networkpolicy tests on Kind cluster.
+* `/test-kind-ipv6-only-networkpolicy`: Linux IPv6 only networkpolicy tests on Kind cluster.
+* `/test-kind-ipv6-networkpolicy`: Linux dual stack networkpolicy tests on Kind cluster.
 
 Here are the trigger phrases for groups of checks:
 
@@ -190,6 +198,8 @@ Here are the trigger phrases for groups of checks:
 * `/test-windows-all`: Windows IPv4 tests, including e2e tests with proxyAll enabled. It also includes all containerd runtime based Windows tests since 1.10.0.
 * `/test-ipv6-all`: Linux dual stack tests
 * `/test-ipv6-only-all`: Linux IPv6 only tests
+* `/test-kind-ipv6-only-all`: Linux IPv6 only tests on Kind cluster.
+* `/test-kind-ipv6-all`: Linux dual stack tests on Kind cluster.
 
 Besides, you can skip a check with `/skip-*`, e.g. `/skip-e2e`: skip Linux IPv4
 e2e tests.
diff --git a/VERSION b/VERSION
index 853153e77..3edcf5ad4 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-v2.0.0-dev
+v2.1.0-dev
diff --git a/build/charts/antrea/README.md b/build/charts/antrea/README.md
index 610cdd123..6811acdbb 100644
--- a/build/charts/antrea/README.md
+++ b/build/charts/antrea/README.md
@@ -1,6 +1,6 @@
 # antrea
 
-![Version: 2.0.0-dev](https://img.shields.io/badge/Version-2.0.0--dev-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square)
+![Version: 2.1.0-dev](https://img.shields.io/badge/Version-2.1.0--dev-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square)
 
 Kubernetes networking based on Open vSwitch
 
diff --git a/build/charts/antrea/crds/traceflow.yaml b/build/charts/antrea/crds/traceflow.yaml
index 2df4650a1..115aa5f22 100644
--- a/build/charts/antrea/crds/traceflow.yaml
+++ b/build/charts/antrea/crds/traceflow.yaml
@@ -217,6 +217,8 @@ spec:
                               type: string
                             egressNode:
                               type: string
+                            srcPodIP:
+                              type: string
                 capturedPacket:
                   properties:
                     srcIP:
diff --git a/build/charts/flow-aggregator/README.md b/build/charts/flow-aggregator/README.md
index ba13ab538..f79b6fdfb 100644
--- a/build/charts/flow-aggregator/README.md
+++ b/build/charts/flow-aggregator/README.md
@@ -1,6 +1,6 @@
 # flow-aggregator
 
-![Version: 2.0.0-dev](https://img.shields.io/badge/Version-2.0.0--dev-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square)
+![Version: 2.1.0-dev](https://img.shields.io/badge/Version-2.1.0--dev-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square)
 
 Antrea Flow Aggregator
 
diff --git a/build/images/scripts/start_ovs b/build/images/scripts/start_ovs
index daff5c5a7..4949a37b0 100755
--- a/build/images/scripts/start_ovs
+++ b/build/images/scripts/start_ovs
@@ -130,9 +130,12 @@ function quit {
     # been restored. The uplink is moved to the OVS bridge to support AntreaFlexibleIPAM mode.
     while [ "`ovsdb-client dump Port|grep antrea-type=uplink`" != "" ]; do
       log_info $CONTAINER_NAME "Uplink found on OVS, wait 1s and retry..."
-      sleep 1 &
-      SLEEP_PID=$!
-      wait $SLEEP_PID
+      # If a second signal (besides SIGKILL) is received, bash will wait for the
+      # sleep to complete, and this function will run again. There is no point
+      # in running sleep in the background here (and using wait): it would just
+      # create extra "concurrent" sleep processes in case multiple signals are
+      # received.
+      sleep 1
     done
     stop_ovs
     # terminate background sleep process
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 1856ea30c..5581d58a1 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -3101,6 +3101,8 @@ spec:
                               type: string
                             egressNode:
                               type: string
+                            srcPodIP:
+                              type: string
                 capturedPacket:
                   properties:
                     srcIP:
diff --git a/build/yamls/antrea-crds.yml b/build/yamls/antrea-crds.yml
index d047fa884..b41fc8e8e 100644
--- a/build/yamls/antrea-crds.yml
+++ b/build/yamls/antrea-crds.yml
@@ -3074,6 +3074,8 @@ spec:
                               type: string
                             egressNode:
                               type: string
+                            srcPodIP:
+                              type: string
                 capturedPacket:
                   properties:
                     srcIP:
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index d59e30e5f..e60b8f5c8 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -3101,6 +3101,8 @@ spec:
                               type: string
                             egressNode:
                               type: string
+                            srcPodIP:
+                              type: string
                 capturedPacket:
                   properties:
                     srcIP:
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index 33a0f87d1..89bc22a6b 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -3101,6 +3101,8 @@ spec:
                               type: string
                             egressNode:
                               type: string
+                            srcPodIP:
+                              type: string
                 capturedPacket:
                   properties:
                     srcIP:
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index 702ec6728..8fd779217 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -3101,6 +3101,8 @@ spec:
                               type: string
                             egressNode:
                               type: string
+                            srcPodIP:
+                              type: string
                 capturedPacket:
                   properties:
                     srcIP:
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index 76fb1bf1c..b7d129b0c 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -3101,6 +3101,8 @@ spec:
                               type: string
                             egressNode:
                               type: string
+                            srcPodIP:
+                              type: string
                 capturedPacket:
                   properties:
                     srcIP:
diff --git a/docs/antrea-network-policy.md b/docs/antrea-network-policy.md
index 812962fae..ff1fbbacb 100644
--- a/docs/antrea-network-policy.md
+++ b/docs/antrea-network-policy.md
@@ -39,6 +39,7 @@
     - [K8s clusters with version 1.21 and above](#k8s-clusters-with-version-121-and-above)
     - [K8s clusters with version 1.20 and below](#k8s-clusters-with-version-120-and-below)
   - [Selecting Pods in the same Namespace with Self](#selecting-pods-in-the-same-namespace-with-self)
+  - [Selecting Namespaces with the same label values using SameLabels](#selecting-namespaces-with-the-same-label-values-using-samelabels)
   - [FQDN based filtering](#fqdn-based-filtering)
   - [Node Selector](#node-selector)
   - [toServices egress rules](#toservices-egress-rules)
@@ -1301,7 +1302,7 @@ spec:
 ```
 
 The policy above ensures that x/a, x/b and x/c can communicate with each other, but nothing else
-(unless there are higher precedenced policies which say otherwise). Same for Namespaces y and z.
+(unless there are higher precedence policies that say otherwise). Same for Namespaces y and z.
 
 ```yaml
 apiVersion: crd.antrea.io/v1beta1
@@ -1334,6 +1335,97 @@ These two policies shown above are for demonstration purposes only. For more rea
 `namespaces` field, refer to this [sample](#acnp-for-strict-namespace-isolation) YAML in the previous
 section.
 
+### Selecting Namespaces with the same label values using SameLabels
+
+Starting from Antrea v2.0, Antrea ClusterNetworkPolicy supports creating policy rules between groups
+of Namespaces that share common label values. The most prominent use case of this feature is to provide
+isolation between Namespaces that have different values for some pre-defined labels, e.g. "org", by
+applying a single ACNP in the cluster.
+
+Consider a minimalistic cluster with the following Namespaces:
+
+```text
+NAME            LABELS
+kube-system     kubernetes.io/metadata.name=kube-system
+accounting1     kubernetes.io/metadata.name=accounting1, org=accounting, region=us-west
+accounting2     kubernetes.io/metadata.name=accounting2, org=accounting, region=us-east
+sales1          kubernetes.io/metadata.name=sales1, org=sales, region=us-west
+sales2          kubernetes.io/metadata.name=sales2, org=sales, region=us-east
+```
+
+An administrator of such cluster typically would want to enforce some boundaries between the "tenants"
+in the cluster (the accounting team and the sales team in this case, who each own two Namespaces).
+This can be easily achieved by the following ACNP:
+
+```yaml
+apiVersion: crd.antrea.io/v1beta1
+kind: ClusterNetworkPolicy
+metadata:
+  name: isolation-based-on-org
+spec:
+  priority: 1
+  tier: securityops
+  appliedTo:
+    - namespaceSelector:
+        matchExpressions:
+          - { key: org, operator: Exists }
+  ingress:
+    - action: Allow
+      from:
+        - namespaces:
+            sameLabels: [org]
+    - action: Deny
+  egress:
+    - action: Allow
+      to:
+        - namespaces:
+            sameLabels: [org]
+    - action: Deny
+```
+
+The above policy will also automatically adapt to the changes in the cluster, i.e., any new Namespace
+created in the cluster with a different "org" label value will be automatically isolated from both the
+accounting and the sales Namespaces. In addition, the Namespace grouping criteria can be easily extended
+to match more than one label keys, and Namespaces will be grouped together ONLY IF ALL the values of the
+label keys listed in the `sameLabels` field have the same value. For example, if we change the `sameLabels`
+list to `[org, region]` in the example above, then this ACNP will create four Namespace groups instead of
+two, which are all isolated from each other. The reason is that individual Namespaces for the accounting
+or sales organizations have different values for the "region" label, even though they share the same value
+for the "org" label.
+
+Another important note is that such policy is a no-op on Namespaces that do not have all the labels listed
+in the `sameLabels` field, even if such Namespaces are selected in `appliedTo`. In other words, we can
+rewrite the `appliedTo` in the policy above to `- namespaceSelector: {}` and it will work exactly the same.
+There will be no effective rules created for the `kube-system` Namespace since it does not have the "org"
+label. On the other hand, if the following policy (alone) is applied in this cluster:
+
+```yaml
+apiVersion: crd.antrea.io/v1beta1
+kind: ClusterNetworkPolicy
+metadata:
+  name: isolation-based-on-org-and-env
+spec:
+  priority: 1
+  tier: securityops
+  appliedTo:
+    - namespaceSelector: {}
+  ingress:
+    - action: Allow
+      from:
+        - namespaces:
+            sameLabels: [org, env]
+    - action: Deny
+      from:
+        - namespaceSelector: {}
+```
+
+it will have no effect whatsoever because no Namespace has both the "org" and "env" label keys.
+To take the example further, if we now add another Namespace `dev` with labels "org=dev, env=test" the end
+result is that only the `dev` Namespace will be selected by the `isolation-based-on-org-and-env` ACNP, which
+denies ingress from all other Namespaces in the cluster since they don't have the same values for labels
+"org" and "env" compared to `dev` (in fact, there is no other Namespace with the "env" label key). All the
+other Namespaces, on the other hand, will not have effective ingress rules created by this policy.
+
 ### FQDN based filtering
 
 Antrea-native policy features a `fqdn` field in egress rules to select Fully Qualified Domain Names
diff --git a/docs/antrea-proxy.md b/docs/antrea-proxy.md
index 77e74c88a..1469a76fb 100644
--- a/docs/antrea-proxy.md
+++ b/docs/antrea-proxy.md
@@ -253,6 +253,26 @@ data:
       proxyLoadBalancerIPs: false
 ```
 
+With the above configuration, AntreaProxy will ignore all external `loadBalancerIP`s.
+Starting with K8s v1.29, feature [LoadBalancerIPMode](https://kubernetes.io/docs/concepts/services-networking/service/#load-balancer-ip-mode)
+was introduced, providing users with a more fine-grained mechanism to control how
+every external `loadBalancerIP` behaves in a LoadBalancer Service.
+
+- If the value of `LoadBalancerIPMode` is `LoadBalancerIPModeVIP` or nil, the
+  traffic destined for the corresponding external `loadBalancerIP` should follow
+  the default behavior and get load-balanced at the source Node.
+- If the value of `LoadBalancerIPMode` is `LoadBalancerIPModeProxy`, the traffic
+  destined for the corresponding external `loadBalancerIP` should be sent to the
+  external LoadBalancer.
+
+Starting with Antrea v2.0, AntreaProxy will respect `LoadBalancerIPMode` in LoadBalancer
+Services when the configuration option `proxyLoadBalancerIPs` is set to `true`
+(default). In this case, AntreaProxy will serve only the external `loadBalancerIP`s
+configured with `LoadBalancerIPModeVIP`, and those configured with
+`LoadBalancerIPModeProxy` will bypass AntreaProxy. If the configuration option
+`proxyLoadBalancerIPs` is set to `false`, AntreaProxy will ignore the external
+`loadBalancerIP`s even if configured with `LoadBalancerIPModeVIP`.
+
 There are two important prerequisites for this feature:
 
 * You must enable `proxyAll` and [remove kube-proxy](#removing-kube-proxy) from
diff --git a/docs/noencap-hybrid-modes.md b/docs/noencap-hybrid-modes.md
index d6f56a410..ab0c3b311 100644
--- a/docs/noencap-hybrid-modes.md
+++ b/docs/noencap-hybrid-modes.md
@@ -87,7 +87,7 @@ network is able to route Pod traffic between Nodes. This Route Controller
 functionality is supported by the Cloud Provider implementations of the major
 clouds, including: [AWS](https://github.com/kubernetes/cloud-provider-aws),
 [Azure](https://github.com/kubernetes-sigs/cloud-provider-azure),
-[GCE](https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/legacy-cloud-providers/gce),
+[GCP](https://github.com/kubernetes/cloud-provider-gcp),
 and [vSphere (with NSX-T)](https://github.com/kubernetes/cloud-provider-vsphere).
 
 * Run a routing protocol or even manually configure routers to add routes to
diff --git a/go.mod b/go.mod
index d6440796f..45e998636 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
 	github.com/containernetworking/plugins v1.1.1
 	github.com/coreos/go-iptables v0.7.0
 	github.com/davecgh/go-spew v1.1.1
+	github.com/fatih/color v1.16.0
 	github.com/fsnotify/fsnotify v1.7.0
 	github.com/gammazero/deque v0.1.2
 	github.com/go-logr/logr v1.4.1
@@ -39,8 +40,8 @@ require (
 	github.com/mdlayher/packet v1.1.2
 	github.com/miekg/dns v1.1.59
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
-	github.com/onsi/ginkgo/v2 v2.17.1
-	github.com/onsi/gomega v1.33.0
+	github.com/onsi/ginkgo/v2 v2.17.3
+	github.com/onsi/gomega v1.33.1
 	github.com/pkg/sftp v1.13.6
 	github.com/prometheus/client_golang v1.18.0
 	github.com/prometheus/common v0.47.0
@@ -53,16 +54,16 @@ require (
 	github.com/vishvananda/netlink v1.2.1-beta.2
 	github.com/vmware/go-ipfix v0.9.0
 	go.uber.org/mock v0.4.0
-	golang.org/x/crypto v0.22.0
+	golang.org/x/crypto v0.23.0
 	golang.org/x/mod v0.17.0
-	golang.org/x/net v0.24.0
+	golang.org/x/net v0.25.0
 	golang.org/x/sync v0.7.0
-	golang.org/x/sys v0.19.0
+	golang.org/x/sys v0.20.0
 	golang.org/x/time v0.5.0
-	golang.org/x/tools v0.20.0
+	golang.org/x/tools v0.21.0
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5
 	google.golang.org/grpc v1.63.2
-	google.golang.org/protobuf v1.33.0
+	google.golang.org/protobuf v1.34.1
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/yaml.v2 v2.4.0
 	gopkg.in/yaml.v3 v3.0.1
@@ -126,7 +127,6 @@ require (
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
 	github.com/evanphx/json-patch/v5 v5.6.0 // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
-	github.com/fatih/color v1.15.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fvbommel/sortorder v1.1.0 // indirect
 	github.com/go-errors/errors v1.4.2 // indirect
@@ -136,14 +136,14 @@ require (
 	github.com/go-openapi/jsonpointer v0.19.6 // indirect
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
 	github.com/go-openapi/swag v0.22.3 // indirect
-	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/cel-go v0.17.7 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
+	github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
@@ -166,7 +166,7 @@ require (
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
-	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/mdlayher/genetlink v1.0.0 // indirect
 	github.com/mdlayher/netlink v1.7.2 // indirect
@@ -219,8 +219,8 @@ require (
 	go.uber.org/zap v1.25.0 // indirect
 	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
 	golang.org/x/oauth2 v0.17.0 // indirect
-	golang.org/x/term v0.19.0 // indirect
-	golang.org/x/text v0.14.0 // indirect
+	golang.org/x/term v0.20.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
diff --git a/go.sum b/go.sum
index 41c8d242f..39a598cd6 100644
--- a/go.sum
+++ b/go.sum
@@ -211,8 +211,8 @@ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2Vvl
 github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
 github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
-github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -297,8 +297,8 @@ github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2K
 github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -366,8 +366,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -505,8 +505,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -578,16 +578,16 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
-github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
-github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
+github.com/onsi/ginkgo/v2 v2.17.3 h1:oJcvKpIb7/8uLpDDtnQuf18xVnwKp8DTD7DQ6gTd/MU=
+github.com/onsi/ginkgo/v2 v2.17.3/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 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.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
-github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
-github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
 github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
 github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
@@ -704,7 +704,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -816,8 +815,8 @@ golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
-golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
@@ -867,8 +866,8 @@ golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
-golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
-golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -943,16 +942,16 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
-golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
-golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
+golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -963,8 +962,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -988,8 +987,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
-golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
+golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
+golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1040,8 +1039,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pkg/agent/controller/networkpolicy/node_reconciler_linux.go b/pkg/agent/controller/networkpolicy/node_reconciler_linux.go
index 9b88a77b7..dba3bb55b 100644
--- a/pkg/agent/controller/networkpolicy/node_reconciler_linux.go
+++ b/pkg/agent/controller/networkpolicy/node_reconciler_linux.go
@@ -350,7 +350,7 @@ func (r *nodeReconciler) computeIPTRules(rule *CompletedRule) (map[iptables.Prot
 	} else {
 		coreIPTChain = config.NodeNetworkPolicyEgressRulesChain
 	}
-	coreIPTRuleComment := fmt.Sprintf("Antrea: for rule %s, policy %s", ruleID, rule.SourceRef.ToString())
+	coreIPTRuleComment := fmt.Sprintf("Antrea: for rule %s, policy %s", rule.Name, rule.SourceRef.ToString())
 	lastRealized.coreIPTChain = coreIPTChain
 
 	nodePolicyRules := make(map[iptables.Protocol]*types.NodePolicyRule)
diff --git a/pkg/agent/controller/traceflow/packetin.go b/pkg/agent/controller/traceflow/packetin.go
index f67b8fdac..623714545 100644
--- a/pkg/agent/controller/traceflow/packetin.go
+++ b/pkg/agent/controller/traceflow/packetin.go
@@ -173,6 +173,17 @@ func (c *Controller) parsePacketIn(pktIn *ofctrl.PacketIn) (*crdv1beta1.Traceflo
 		ob := new(crdv1beta1.Observation)
 		ob.Component = crdv1beta1.ComponentSpoofGuard
 		ob.Action = crdv1beta1.ActionForwarded
+		// For SNATed packet(hairpin), ipSrc and ctNwSrc are different.
+		// We noticed that ctNwSrc is invalid for ICMPv6 packets: it should contain
+		// the original src Pod IP but it is always empty due to an issue in OVS.
+		// https://github.com/openvswitch/ovs-issues/issues/327
+		if isValidCtNw(ctNwSrc) {
+			ob.SrcPodIP = ctNwSrc
+		} else {
+			// In the case of ICMPv6, since ctNwSrc is invalid, we can use ipSrc as
+			// hairpin is not applicable, so ipSrc always contains src pod IP.
+			ob.SrcPodIP = ipSrc
+		}
 		obs = append(obs, *ob)
 	} else {
 		ob := new(crdv1beta1.Observation)
@@ -461,10 +472,7 @@ func isValidCtNw(ipStr string) bool {
 	}
 	// Reserved by IETF [RFC3513][RFC4291]
 	_, cidr, _ := net.ParseCIDR("0000::/8")
-	if cidr.Contains(ip) {
-		return false
-	}
-	return true
+	return !cidr.Contains(ip)
 }
 
 func parseCapturedPacket(pktIn *ofctrl.PacketIn) *crdv1beta1.Packet {
diff --git a/pkg/agent/nodeportlocal/k8s/npl_controller.go b/pkg/agent/nodeportlocal/k8s/npl_controller.go
index 592098e7a..9469af9ad 100644
--- a/pkg/agent/nodeportlocal/k8s/npl_controller.go
+++ b/pkg/agent/nodeportlocal/k8s/npl_controller.go
@@ -493,9 +493,13 @@ func (c *NPLController) handleAddUpdatePod(key string, obj interface{}) error {
 		}
 		podPorts[targetPortProto] = struct{}{}
 		portData := c.portTable.GetEntry(podIP, port, protocol)
-		if portData != nil && !portData.ProtocolInUse(protocol) {
-			// If the PortTable has an entry for the Pod but does not have an
-			// entry with protocol, we enforce AddRule for the missing Protocol.
+		// Special handling for a rule that was previously marked for deletion but could not
+		// be deleted properly: we have to retry now.
+		if portData != nil && portData.Defunct() {
+			klog.InfoS("Deleting defunct rule for Pod to prevent re-use", "pod", klog.KObj(pod), "podIP", podIP, "port", port, "protocol", protocol)
+			if err := c.portTable.DeleteRule(podIP, port, protocol); err != nil {
+				return fmt.Errorf("failed to delete defunct rule for Pod IP %s, Pod Port %d, Protocol %s: %w", podIP, port, protocol, err)
+			}
 			portData = nil
 		}
 		if portData == nil {
@@ -526,13 +530,11 @@ func (c *NPLController) handleAddUpdatePod(key string, obj interface{}) error {
 	// second, delete any existing rule that is not needed based on the current Pod
 	// specification.
 	entries := c.portTable.GetDataForPodIP(podIP)
-	if nplExists {
-		for _, data := range entries {
-			proto := data.Protocol
-			if _, exists := podPorts[util.BuildPortProto(fmt.Sprint(data.PodPort), proto.Protocol)]; !exists {
-				if err := c.portTable.DeleteRule(podIP, int(data.PodPort), proto.Protocol); err != nil {
-					return fmt.Errorf("failed to delete rule for Pod IP %s, Pod Port %d, Protocol %s: %v", podIP, data.PodPort, proto.Protocol, err)
-				}
+	for _, data := range entries {
+		proto := data.Protocol
+		if _, exists := podPorts[util.BuildPortProto(fmt.Sprint(data.PodPort), proto.Protocol)]; !exists {
+			if err := c.portTable.DeleteRule(podIP, int(data.PodPort), proto.Protocol); err != nil {
+				return fmt.Errorf("failed to delete rule for Pod IP %s, Pod Port %d, Protocol %s: %w", podIP, data.PodPort, proto.Protocol, err)
 			}
 		}
 	}
diff --git a/pkg/agent/nodeportlocal/portcache/port_table.go b/pkg/agent/nodeportlocal/portcache/port_table.go
index c2c99203e..b1856861b 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table.go
@@ -32,13 +32,8 @@ const (
 	PodIPIndex       = "podIPIndex"
 )
 
-// protocolSocketState represents the state of the socket corresponding to a
-// given (Node port, protocol) tuple.
-type protocolSocketState int
-
 type ProtocolSocketData struct {
 	Protocol string
-	State    protocolSocketState
 	socket   io.Closer
 }
 
@@ -47,14 +42,13 @@ type NodePortData struct {
 	PodPort  int
 	PodIP    string
 	Protocol ProtocolSocketData
+	// defunct is used to indicate that a rule has been partially deleted: it is no longer
+	// usable and deletion needs to be re-attempted.
+	defunct bool
 }
 
-func (d *NodePortData) ProtocolInUse(protocol string) bool {
-	protocolSocketData := d.Protocol
-	if protocolSocketData.Protocol == protocol {
-		return protocolSocketData.State == stateInUse
-	}
-	return false
+func (d *NodePortData) Defunct() bool {
+	return d.defunct
 }
 
 type LocalPortOpener interface {
@@ -204,8 +198,8 @@ func podIPPortProtoFormat(ip string, port int, protocol string) string {
 }
 
 func (pt *PortTable) getEntryByPodIPPortProto(ip string, port int, protocol string) *NodePortData {
-	data, exists := pt.getPortTableCacheFromPodEndpointIndex(podIPPortProtoFormat(ip, port, protocol))
-	if exists == false {
+	data, ok := pt.getPortTableCacheFromPodEndpointIndex(podIPPortProtoFormat(ip, port, protocol))
+	if !ok {
 		return nil
 	}
 	return data
@@ -214,10 +208,8 @@ func (pt *PortTable) getEntryByPodIPPortProto(ip string, port int, protocol stri
 func (pt *PortTable) RuleExists(podIP string, podPort int, protocol string) bool {
 	pt.tableLock.RLock()
 	defer pt.tableLock.RUnlock()
-	if data := pt.getEntryByPodIPPortProto(podIP, podPort, protocol); data != nil {
-		return data.ProtocolInUse(protocol)
-	}
-	return false
+	data := pt.getEntryByPodIPPortProto(podIP, podPort, protocol)
+	return data != nil
 }
 
 // nodePortProtoFormat formats the nodeport, protocol to string port:protocol.
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_others.go b/pkg/agent/nodeportlocal/portcache/port_table_others.go
index 30ec417e7..c13eaf1d7 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_others.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_others.go
@@ -26,18 +26,6 @@ import (
 	"antrea.io/antrea/pkg/agent/nodeportlocal/rules"
 )
 
-const (
-	// stateOpen means that a listening socket has been opened for the
-	// protocol (as a means to reserve the port for this protocol), but no
-	// NPL rule has been installed for it.
-	stateOpen protocolSocketState = iota
-	// stateInUse means that a listening socket has been opened AND a NPL
-	// rule has been installed.
-	stateInUse
-	// stateClosed means that the socket has been closed.
-	stateClosed
-)
-
 func openSocketsForPort(localPortOpener LocalPortOpener, port int, protocol string) (ProtocolSocketData, error) {
 	// Port only needs to be available for the protocol used by the NPL rule.
 	// We don't need to allocate the same nodePort for all protocols anymore.
@@ -48,7 +36,6 @@ func openSocketsForPort(localPortOpener LocalPortOpener, port int, protocol stri
 	}
 	protocolData := ProtocolSocketData{
 		Protocol: protocol,
-		State:    stateInUse,
 		socket:   socket,
 	}
 	return protocolData, nil
@@ -83,28 +70,6 @@ func (pt *PortTable) getFreePort(podIP string, podPort int, protocol string) (in
 	return 0, ProtocolSocketData{}, fmt.Errorf("no free port found")
 }
 
-func (d *NodePortData) CloseSockets() error {
-	if d.Protocol.Protocol != "" {
-		protocolSocketData := &d.Protocol
-		switch protocolSocketData.State {
-		case stateClosed:
-			// already closed
-			return nil
-		case stateInUse:
-			// should not happen
-			return fmt.Errorf("protocol %s is still in use, cannot release socket", protocolSocketData.Protocol)
-		case stateOpen:
-			if err := protocolSocketData.socket.Close(); err != nil {
-				return fmt.Errorf("error when releasing local port %d with protocol %s: %v", d.NodePort, protocolSocketData.Protocol, err)
-			}
-			protocolSocketData.State = stateClosed
-		default:
-			return fmt.Errorf("invalid protocol socket state")
-		}
-	}
-	return nil
-}
-
 func (pt *PortTable) AddRule(podIP string, podPort int, protocol string) (int, error) {
 	pt.tableLock.Lock()
 	defer pt.tableLock.Unlock()
@@ -128,11 +93,38 @@ func (pt *PortTable) AddRule(podIP string, podPort int, protocol string) (int, e
 		pt.addPortTableCache(npData)
 	} else {
 		// Only add rules if the entry does not exist.
-		return 0, fmt.Errorf("existed Linux Nodeport entry for %s:%d:%s", podIP, podPort, protocol)
+		return 0, fmt.Errorf("existing Linux Nodeport entry for %s:%d:%s", podIP, podPort, protocol)
 	}
 	return npData.NodePort, nil
 }
 
+func (pt *PortTable) deleteRule(data *NodePortData) error {
+	protocolSocketData := &data.Protocol
+	protocol := protocolSocketData.Protocol
+
+	// In theory, we should not be modifying a cache item in-place. However, the field we are
+	// modifying (defunct) does NOT participate in indexing and the modification is thread-safe
+	// because of pt.tableLock.
+	// TODO: stop modifying cache items in-place.
+	// We could set defunct after the call to DeleteRule, because a failed call to DeleteRule
+	// should mean that the rule is still present and valid, but there is no harm in being more
+	// conservative.
+	data.defunct = true
+
+	// Calling DeleteRule is idempotent.
+	if err := pt.PodPortRules.DeleteRule(data.NodePort, data.PodIP, data.PodPort, protocol); err != nil {
+		return err
+	}
+	if err := protocolSocketData.socket.Close(); err != nil {
+		return fmt.Errorf("error when releasing local port %d with protocol %s: %w", data.NodePort, protocol, err)
+	}
+	// We don't need to delete cache from different indexes repeatedly because they map to the same entry.
+	// Deletion errors are not possible because our Index functions cannot return errors.
+	// See https://github.com/kubernetes/client-go/blob/3aa45779f2e5592d52edf68da66abfbd0805e413/tools/cache/store.go#L189-L196
+	pt.deletePortTableCache(data)
+	return nil
+}
+
 func (pt *PortTable) DeleteRule(podIP string, podPort int, protocol string) error {
 	pt.tableLock.Lock()
 	defer pt.tableLock.Unlock()
@@ -141,15 +133,7 @@ func (pt *PortTable) DeleteRule(podIP string, podPort int, protocol string) erro
 		// Delete not required when the PortTable entry does not exist
 		return nil
 	}
-	if err := pt.PodPortRules.DeleteRule(data.NodePort, podIP, podPort, protocol); err != nil {
-		return err
-	}
-	if err := data.CloseSockets(); err != nil {
-		return err
-	}
-	// We don't need to delete cache from different indexes repeatedly because they map to the same entry.
-	pt.deletePortTableCache(data)
-	return nil
+	return pt.deleteRule(data)
 }
 
 func (pt *PortTable) DeleteRulesForPod(podIP string) error {
@@ -157,14 +141,7 @@ func (pt *PortTable) DeleteRulesForPod(podIP string) error {
 	defer pt.tableLock.Unlock()
 	podEntries := pt.getDataForPodIP(podIP)
 	for _, podEntry := range podEntries {
-		protocolSocketData := podEntry.Protocol
-		if err := pt.PodPortRules.DeleteRule(podEntry.NodePort, podIP, podEntry.PodPort, protocolSocketData.Protocol); err != nil {
-			return err
-		}
-		if err := protocolSocketData.socket.Close(); err != nil {
-			return fmt.Errorf("error when releasing local port %d with protocol %s: %v", podEntry.NodePort, protocolSocketData.Protocol, err)
-		}
-		pt.deletePortTableCache(podEntry)
+		return pt.deleteRule(podEntry)
 	}
 	return nil
 }
@@ -177,16 +154,11 @@ func (pt *PortTable) syncRules() error {
 	nplPorts := make([]rules.PodNodePort, 0, len(objs))
 	for _, obj := range objs {
 		npData := obj.(*NodePortData)
-		protocols := make([]string, 0, 1)
-		protocol := npData.Protocol
-		if protocol.State == stateInUse {
-			protocols = append(protocols, protocol.Protocol)
-		}
 		nplPorts = append(nplPorts, rules.PodNodePort{
 			NodePort: npData.NodePort,
 			PodPort:  npData.PodPort,
 			PodIP:    npData.PodIP,
-			Protocol: protocols[0],
+			Protocol: npData.Protocol.Protocol,
 		})
 	}
 	if err := pt.PodPortRules.AddAllRules(nplPorts); err != nil {
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_windows.go b/pkg/agent/nodeportlocal/portcache/port_table_windows.go
index aff2a3224..e084bde18 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_windows.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_windows.go
@@ -25,11 +25,6 @@ import (
 	"antrea.io/antrea/pkg/agent/nodeportlocal/rules"
 )
 
-const (
-	// stateInUse means that the NPL rule has been installed.
-	stateInUse protocolSocketState = 1
-)
-
 func addRuleForPort(podPortRules rules.PodPortRules, port int, podIP string, podPort int, protocol string) (ProtocolSocketData, error) {
 	// Only the protocol used here should be returned if NetNatStaticMapping rule
 	// can be inserted to an unused protocol port.
@@ -40,7 +35,6 @@ func addRuleForPort(podPortRules rules.PodPortRules, port int, podIP string, pod
 	}
 	protocolData := ProtocolSocketData{
 		Protocol: protocol,
-		State:    stateInUse,
 		socket:   nil,
 	}
 	return protocolData, nil
@@ -137,6 +131,8 @@ func (pt *PortTable) DeleteRule(podIP string, podPort int, protocol string) erro
 		return nil
 	}
 
+	data.defunct = true
+	// Calling DeleteRule is idempotent.
 	if err := pt.PodPortRules.DeleteRule(data.NodePort, podIP, podPort, protocol); err != nil {
 		return err
 	}
diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go
index c5244adcc..2addc2d44 100644
--- a/pkg/antctl/antctl.go
+++ b/pkg/antctl/antctl.go
@@ -19,6 +19,7 @@ import (
 
 	agentapis "antrea.io/antrea/pkg/agent/apis"
 	fallbackversion "antrea.io/antrea/pkg/antctl/fallback/version"
+	checkinstallation "antrea.io/antrea/pkg/antctl/raw/check/installation"
 	"antrea.io/antrea/pkg/antctl/raw/featuregates"
 	"antrea.io/antrea/pkg/antctl/raw/multicluster"
 	"antrea.io/antrea/pkg/antctl/raw/proxy"
@@ -633,6 +634,12 @@ $ antctl get podmulticaststats pod -n namespace`,
 		},
 	},
 	rawCommands: []rawCommand{
+		{
+			cobraCommand:      checkinstallation.Command(),
+			supportAgent:      false,
+			supportController: false,
+			commandGroup:      check,
+		},
 		{
 			cobraCommand:      supportbundle.Command,
 			supportAgent:      true,
diff --git a/pkg/antctl/command_definition.go b/pkg/antctl/command_definition.go
index 2382f3208..ff4635482 100644
--- a/pkg/antctl/command_definition.go
+++ b/pkg/antctl/command_definition.go
@@ -70,6 +70,7 @@ const (
 	query
 	mc
 	upgrade
+	check
 )
 
 var groupCommands = map[commandGroup]*cobra.Command{
@@ -93,6 +94,10 @@ var groupCommands = map[commandGroup]*cobra.Command{
 		Short: "Sub-commands for upgrade operations",
 		Long:  "Sub-commands for upgrade operations",
 	},
+	check: {
+		Use:   "check",
+		Short: "Performs pre and post installation checks",
+	},
 }
 
 type endpointResponder interface {
diff --git a/pkg/antctl/command_list.go b/pkg/antctl/command_list.go
index e60f381e4..d780d9e69 100644
--- a/pkg/antctl/command_list.go
+++ b/pkg/antctl/command_list.go
@@ -64,7 +64,8 @@ func (cl *commandList) applyToRootCommand(root *cobra.Command, client AntctlClie
 			(runtime.Mode == runtime.ModeController && cmd.supportController) ||
 			(runtime.Mode == runtime.ModeFlowAggregator && cmd.supportFlowAggregator) ||
 			(!runtime.InPod && cmd.commandGroup == mc) ||
-			(!runtime.InPod && cmd.commandGroup == upgrade) {
+			(!runtime.InPod && cmd.commandGroup == upgrade) ||
+			(!runtime.InPod && cmd.commandGroup == check) {
 			if groupCommand, ok := groupCommands[cmd.commandGroup]; ok {
 				groupCommand.AddCommand(cmd.cobraCommand)
 			} else {
diff --git a/pkg/antctl/raw/check/installation/command.go b/pkg/antctl/raw/check/installation/command.go
new file mode 100644
index 000000000..e5da1b1df
--- /dev/null
+++ b/pkg/antctl/raw/check/installation/command.go
@@ -0,0 +1,443 @@
+// Copyright 2024 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 installation
+
+import (
+	"context"
+	"crypto/rand"
+	"errors"
+	"fmt"
+	"net"
+	"os"
+	"time"
+
+	"github.com/fatih/color"
+	"github.com/spf13/cobra"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+
+	"antrea.io/antrea/pkg/antctl/raw/check"
+)
+
+func Command() *cobra.Command {
+	o := newOptions()
+	command := &cobra.Command{
+		Use:   "installation",
+		Short: "Runs post installation checks",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return Run(o)
+		},
+	}
+	command.Flags().StringVarP(&o.antreaNamespace, "Namespace", "n", o.antreaNamespace, "Configure Namespace in which Antrea is running")
+	return command
+}
+
+type options struct {
+	antreaNamespace string
+}
+
+func newOptions() *options {
+	return &options{
+		antreaNamespace: "kube-system",
+	}
+}
+
+const (
+	testNamespacePrefix         = "antrea-test"
+	clientDeploymentName        = "test-client"
+	echoSameNodeDeploymentName  = "echo-same-node"
+	echoOtherNodeDeploymentName = "echo-other-node"
+	kindEchoName                = "echo"
+	kindClientName              = "client"
+	agentDaemonSetName          = "antrea-agent"
+	deploymentImage             = "registry.k8s.io/e2e-test-images/agnhost:2.29"
+	podReadyTimeout             = 1 * time.Minute
+)
+
+type notRunnableError struct {
+	reason string
+}
+
+func (e notRunnableError) Error() string {
+	return fmt.Sprintf("test is not runnable: %s", e.reason)
+}
+
+func newNotRunnableError(reason string) notRunnableError {
+	return notRunnableError{reason: reason}
+}
+
+type Test interface {
+	// Run executes the test using the provided testContext. It returns a non-nil error when the test doesn't succeed.
+	// If a test is not runnable, notRunnableError should be wrapped in the returned error.
+	Run(ctx context.Context, testContext *testContext) error
+}
+
+var testsRegistry = make(map[string]Test)
+
+func RegisterTest(name string, test Test) {
+	testsRegistry[name] = test
+}
+
+type testContext struct {
+	client           kubernetes.Interface
+	config           *rest.Config
+	clusterName      string
+	antreaNamespace  string
+	clientPods       []corev1.Pod
+	echoSameNodePod  *corev1.Pod
+	echoOtherNodePod *corev1.Pod
+	namespace        string
+}
+
+func Run(o *options) error {
+	client, config, clusterName, err := check.NewClient()
+	if err != nil {
+		return fmt.Errorf("unable to create Kubernetes client: %s", err)
+	}
+	ctx := context.Background()
+	testContext := NewTestContext(client, config, clusterName, o)
+	if err := testContext.setup(ctx); err != nil {
+		return err
+	}
+	var numSuccess, numFailure, numSkipped int
+	for name, test := range testsRegistry {
+		testContext.Header("Running test: %s", name)
+		if err := test.Run(ctx, testContext); err != nil {
+			if errors.As(err, new(notRunnableError)) {
+				testContext.Warning("Test %s was skipped: %v", name, err)
+				numSkipped++
+			} else {
+				testContext.Fail("Test %s failed: %v", name, err)
+				numFailure++
+			}
+		} else {
+			testContext.Success("Test %s passed", name)
+			numSuccess++
+		}
+	}
+	testContext.Log("Test finished: %v tests succeeded, %v tests failed, %v tests were skipped", numSuccess, numFailure, numSkipped)
+	testContext.teardown(ctx)
+	if numFailure > 0 {
+		return fmt.Errorf("%v/%v tests failed", numFailure, len(testsRegistry))
+	}
+	return nil
+}
+
+func agnhostConnectCommand(ip string, port string) []string {
+	hostPort := net.JoinHostPort(ip, port)
+	return []string{"/agnhost", "connect", hostPort, "--timeout=5s"}
+}
+
+func newService(name string, selector map[string]string, port int) *corev1.Service {
+	return &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+		Spec: corev1.ServiceSpec{
+			Type: corev1.ServiceTypeClusterIP,
+			Ports: []corev1.ServicePort{
+				{Name: name, Port: int32(port)},
+			},
+			Selector: selector,
+		},
+	}
+}
+
+type deploymentParameters struct {
+	Name        string
+	Role        string
+	Image       string
+	Replicas    int
+	Port        int
+	Command     []string
+	Affinity    *corev1.Affinity
+	Tolerations []corev1.Toleration
+	Labels      map[string]string
+}
+
+func newDeployment(p deploymentParameters) *appsv1.Deployment {
+	if p.Replicas == 0 {
+		p.Replicas = 1
+	}
+	replicas32 := int32(p.Replicas)
+	labels := map[string]string{
+		"name": p.Name,
+		"kind": p.Role,
+	}
+	return &appsv1.Deployment{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:   p.Name,
+			Labels: labels,
+		},
+		Spec: appsv1.DeploymentSpec{
+			Template: corev1.PodTemplateSpec{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:   p.Name,
+					Labels: labels,
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name: p.Name,
+							Env: []corev1.EnvVar{
+								{Name: "PORT", Value: fmt.Sprintf("%d", p.Port)},
+							},
+							Ports: []corev1.ContainerPort{
+								{ContainerPort: int32(p.Port)},
+							},
+							Image:           p.Image,
+							ImagePullPolicy: corev1.PullIfNotPresent,
+							Command:         p.Command,
+						},
+					},
+					Affinity:    p.Affinity,
+					Tolerations: p.Tolerations,
+				},
+			},
+			Replicas: &replicas32,
+			Selector: &metav1.LabelSelector{
+				MatchLabels: map[string]string{
+					"name": p.Name,
+					"kind": p.Role,
+				},
+			},
+		},
+	}
+}
+
+func NewTestContext(client kubernetes.Interface, config *rest.Config, clusterName string, o *options) *testContext {
+	return &testContext{
+		client:          client,
+		config:          config,
+		clusterName:     clusterName,
+		antreaNamespace: o.antreaNamespace,
+		namespace:       generateRandomNamespace(testNamespacePrefix),
+	}
+}
+
+func generateRandomNamespace(baseName string) string {
+	const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
+	bytes := make([]byte, 5)
+	_, err := rand.Read(bytes)
+	if err != nil {
+		panic(err)
+	}
+	for i, b := range bytes {
+		bytes[i] = letters[b%byte(len(letters))]
+	}
+	return fmt.Sprintf("%s-%s", baseName, string(bytes))
+}
+
+func (t *testContext) teardown(ctx context.Context) {
+	t.Log("Deleting post installation tests setup...")
+	t.client.CoreV1().Namespaces().Delete(ctx, t.namespace, metav1.DeleteOptions{})
+	t.Log("Waiting for Namespace %s to disappear", t.namespace)
+	err := wait.PollUntilContextTimeout(ctx, 2*time.Second, 1*time.Minute, true, func(ctx context.Context) (bool, error) {
+		_, err := t.client.CoreV1().Namespaces().Get(ctx, t.namespace, metav1.GetOptions{})
+		if err != nil {
+			return true, nil
+		}
+		return false, nil
+	})
+	if err != nil {
+		t.Log("Setup deletion failed")
+	} else {
+		t.Log("Setup deletion successful")
+	}
+}
+
+func (t *testContext) setup(ctx context.Context) error {
+	t.Log("Test starting....")
+	_, err := t.client.AppsV1().DaemonSets(t.antreaNamespace).Get(ctx, agentDaemonSetName, metav1.GetOptions{})
+	if err != nil {
+		return fmt.Errorf("unable to determine status of Antrea DaemonSet: %w", err)
+	}
+	t.Log("Creating Namespace %s for post installation tests...", t.namespace)
+	_, err = t.client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: t.namespace}}, metav1.CreateOptions{})
+	if err != nil {
+		return fmt.Errorf("unable to create Namespace %s: %s", t.namespace, err)
+	}
+	t.Log("Deploying echo-same-node Service %s...", echoSameNodeDeploymentName)
+	svc := newService(echoSameNodeDeploymentName, map[string]string{"name": echoSameNodeDeploymentName}, 80)
+	_, err = t.client.CoreV1().Services(t.namespace).Create(ctx, svc, metav1.CreateOptions{})
+	if err != nil {
+		return err
+	}
+	commonToleration := []corev1.Toleration{
+		{
+			Key:      "node-role.kubernetes.io/control-plane",
+			Operator: "Exists",
+			Effect:   "NoSchedule",
+		},
+	}
+	echoDeployment := newDeployment(deploymentParameters{
+		Name:    echoSameNodeDeploymentName,
+		Role:    kindEchoName,
+		Port:    80,
+		Image:   deploymentImage,
+		Command: []string{"/agnhost", "netexec", "--http-port=80"},
+		Affinity: &corev1.Affinity{
+			PodAffinity: &corev1.PodAffinity{
+				RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{
+					{
+						LabelSelector: &metav1.LabelSelector{
+							MatchExpressions: []metav1.LabelSelectorRequirement{
+								{
+									Key:      "name",
+									Operator: metav1.LabelSelectorOpIn,
+									Values:   []string{clientDeploymentName},
+								},
+							},
+						},
+						TopologyKey: "kubernetes.io/hostname",
+					},
+				},
+			},
+		},
+		Tolerations: commonToleration,
+		Labels:      map[string]string{"app": echoSameNodeDeploymentName},
+	})
+	_, err = t.client.AppsV1().Deployments(t.namespace).Create(ctx, echoDeployment, metav1.CreateOptions{})
+	if err != nil {
+		return fmt.Errorf("unable to create Deployment %s: %s", echoSameNodeDeploymentName, err)
+	}
+	t.Log("Deploying client Deployment %s...", clientDeploymentName)
+	clientDeployment := newDeployment(deploymentParameters{
+		Name:        clientDeploymentName,
+		Role:        kindClientName,
+		Image:       deploymentImage,
+		Command:     []string{"/agnhost", "pause"},
+		Port:        80,
+		Tolerations: commonToleration,
+		Labels:      map[string]string{"app": clientDeploymentName},
+	})
+	_, err = t.client.AppsV1().Deployments(t.namespace).Create(ctx, clientDeployment, metav1.CreateOptions{})
+	if err != nil {
+		return fmt.Errorf("unable to create Deployment %s: %s", clientDeploymentName, err)
+	}
+
+	t.Log("Deploying echo-other-node Service %s...", echoOtherNodeDeploymentName)
+	svc = newService(echoOtherNodeDeploymentName, map[string]string{"name": echoOtherNodeDeploymentName}, 80)
+	_, err = t.client.CoreV1().Services(t.namespace).Create(ctx, svc, metav1.CreateOptions{})
+	if err != nil {
+		return err
+	}
+	echoOtherNodeDeployment := newDeployment(deploymentParameters{
+		Name:    echoOtherNodeDeploymentName,
+		Role:    kindEchoName,
+		Port:    80,
+		Image:   deploymentImage,
+		Command: []string{"/agnhost", "netexec", "--http-port=80"},
+		Affinity: &corev1.Affinity{
+			PodAntiAffinity: &corev1.PodAntiAffinity{
+				RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{
+					{
+						LabelSelector: &metav1.LabelSelector{
+							MatchExpressions: []metav1.LabelSelectorRequirement{
+								{Key: "name", Operator: metav1.LabelSelectorOpIn, Values: []string{clientDeploymentName}},
+							},
+						},
+						TopologyKey: "kubernetes.io/hostname",
+					},
+				},
+			},
+		},
+		Tolerations: commonToleration,
+		Labels:      map[string]string{"app": echoOtherNodeDeploymentName},
+	})
+	nodes, err := t.client.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
+	if err != nil {
+		return fmt.Errorf("unable to list Nodes: %s", err)
+	}
+	if len(nodes.Items) >= 2 {
+		_, err = t.client.AppsV1().Deployments(t.namespace).Create(ctx, echoOtherNodeDeployment, metav1.CreateOptions{})
+		if err != nil {
+			return fmt.Errorf("unable to create Deployment %s: %s", echoOtherNodeDeploymentName, err)
+		}
+		if err := t.waitForDeploymentsReady(ctx, time.Second, podReadyTimeout, clientDeploymentName, echoSameNodeDeploymentName, echoOtherNodeDeploymentName); err != nil {
+			return err
+		}
+		podList, err := t.client.CoreV1().Pods(t.namespace).List(ctx, metav1.ListOptions{LabelSelector: "name=" + echoOtherNodeDeploymentName})
+		if err != nil {
+			return fmt.Errorf("unable to list Echo Other Node Pod: %s", err)
+		}
+		if len(podList.Items) > 0 {
+			t.echoOtherNodePod = &podList.Items[0]
+		}
+	} else {
+		t.Log("skipping other Node Deployments as multiple Nodes are not available")
+		if err := t.waitForDeploymentsReady(ctx, time.Second, podReadyTimeout, clientDeploymentName, echoSameNodeDeploymentName); err != nil {
+			return err
+		}
+	}
+	podList, err := t.client.CoreV1().Pods(t.namespace).List(ctx, metav1.ListOptions{LabelSelector: "kind=" + kindClientName})
+	if err != nil {
+		return fmt.Errorf("unable to list client Pods: %s", err)
+	}
+	t.clientPods = podList.Items
+	podList, err = t.client.CoreV1().Pods(t.namespace).List(ctx, metav1.ListOptions{LabelSelector: "name=" + echoSameNodeDeploymentName})
+	if err != nil {
+		return fmt.Errorf("unable to list Echo Same Node Pod: %s", err)
+	}
+	if len(podList.Items) > 0 {
+		t.echoSameNodePod = &podList.Items[0]
+	}
+	t.Log("Deployment is validated successfully")
+	return nil
+}
+
+func (t *testContext) waitForDeploymentsReady(ctx context.Context, interval, timeout time.Duration, deployments ...string) error {
+	for _, deployment := range deployments {
+		t.Log("Waiting for Deployment %s to become ready...", deployment)
+		err := wait.PollUntilContextTimeout(ctx, interval, timeout, false, func(ctx context.Context) (bool, error) {
+			ready, err := check.DeploymentIsReady(ctx, t.client, t.namespace, deployment)
+			if err != nil {
+				return false, fmt.Errorf("error checking readiness of Deployment %s: %w", deployment, err)
+			}
+			return ready, nil
+		})
+		if err != nil {
+			return fmt.Errorf("waiting for Deployment %s to become ready has been interrupted: %w", deployment, err)
+		}
+		t.Log("Deployment %s is ready.", deployment)
+	}
+	return nil
+}
+
+func (t *testContext) Log(format string, a ...interface{}) {
+	fmt.Fprintf(os.Stdout, fmt.Sprintf("[%s] ", t.clusterName)+format+"\n", a...)
+}
+
+func (t *testContext) Success(format string, a ...interface{}) {
+	fmt.Fprintf(os.Stdout, fmt.Sprintf("[%s] ", t.clusterName)+color.GreenString(format, a...)+"\n")
+}
+
+func (t *testContext) Fail(format string, a ...interface{}) {
+	fmt.Fprintf(os.Stdout, fmt.Sprintf("[%s] ", t.clusterName)+color.RedString(format, a...)+"\n")
+}
+
+func (t *testContext) Warning(format string, a ...interface{}) {
+	fmt.Fprintf(os.Stdout, fmt.Sprintf("[%s] ", t.clusterName)+color.YellowString(format, a...)+"\n")
+}
+
+func (t *testContext) Header(format string, a ...interface{}) {
+	t.Log("-------------------------------------------------------------------------------------------")
+	t.Log(format, a...)
+	t.Log("-------------------------------------------------------------------------------------------")
+}
diff --git a/pkg/antctl/raw/check/util.go b/pkg/antctl/raw/check/util.go
new file mode 100644
index 000000000..2a6e29936
--- /dev/null
+++ b/pkg/antctl/raw/check/util.go
@@ -0,0 +1,105 @@
+// Copyright 2024 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 check
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/remotecommand"
+)
+
+func NewClient() (client kubernetes.Interface, config *rest.Config, clusterName string, err error) {
+	rules := clientcmd.NewDefaultClientConfigLoadingRules()
+	nonInteractiveClient := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
+	config, err = nonInteractiveClient.ClientConfig()
+	if err != nil {
+		return nil, nil, "", err
+	}
+	rawConfig, err := nonInteractiveClient.RawConfig()
+	if err != nil {
+		return nil, nil, "", err
+	}
+	clientset, err := kubernetes.NewForConfig(config)
+	if err != nil {
+		return nil, nil, "", err
+	}
+	contextName := rawConfig.CurrentContext
+	clusterName = ""
+	if context, ok := rawConfig.Contexts[contextName]; ok {
+		clusterName = context.Cluster
+	}
+	return clientset, config, clusterName, nil
+}
+
+func DeploymentIsReady(ctx context.Context, client kubernetes.Interface, namespace, deploymentName string) (bool, error) {
+	deployment, err := client.AppsV1().Deployments(namespace).Get(ctx, deploymentName, metav1.GetOptions{})
+	if err != nil {
+		return false, err
+	}
+	if deployment.Generation <= deployment.Status.ObservedGeneration {
+		for _, cond := range deployment.Status.Conditions {
+			if cond.Type == appsv1.DeploymentProgressing && cond.Reason == "ProgressDeadlineExceeded" {
+				return false, fmt.Errorf("deployment %q exceeded its progress deadline", deployment.Name)
+			}
+		}
+		if deployment.Spec.Replicas != nil && deployment.Status.UpdatedReplicas < *deployment.Spec.Replicas {
+			return false, nil
+		}
+		if deployment.Status.Replicas > deployment.Status.UpdatedReplicas {
+			return false, nil
+		}
+		if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas {
+			return false, nil
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func ExecInPod(ctx context.Context, client kubernetes.Interface, config *rest.Config, namespace, pod, container string, command []string) (string, string, error) {
+	req := client.CoreV1().RESTClient().Post().Resource("pods").Name(pod).Namespace(namespace).SubResource("exec")
+	req.VersionedParams(&corev1.PodExecOptions{
+		Command:   command,
+		Container: container,
+		Stdin:     false,
+		Stdout:    true,
+		Stderr:    true,
+		TTY:       false,
+	}, scheme.ParameterCodec)
+	exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+	if err != nil {
+		return "", "", fmt.Errorf("error while creating executor: %w", err)
+	}
+	var stdout, stderr bytes.Buffer
+	err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{
+		Stdin:  nil,
+		Stdout: &stdout,
+		Stderr: &stderr,
+		Tty:    false,
+	})
+	if err != nil {
+		return "", "", fmt.Errorf("error in stream: %w", err)
+	}
+	return stdout.String(), stderr.String(), nil
+}
diff --git a/pkg/apis/crd/v1beta1/types.go b/pkg/apis/crd/v1beta1/types.go
index 8c81064b4..6f3dc018d 100644
--- a/pkg/apis/crd/v1beta1/types.go
+++ b/pkg/apis/crd/v1beta1/types.go
@@ -1268,6 +1268,8 @@ type Observation struct {
 	EgressIP    string `json:"egressIP,omitempty" yaml:"egressIP,omitempty"`
 	// EgressNode is the name of the Egress Node.
 	EgressNode string `json:"egressNode,omitempty" yaml:"egressNode,omitempty"`
+	// SrcPodIP is the IP of source Pod.
+	SrcPodIP string `json:"srcPodIP,omitempty" yaml:"srcPodIP,omitempty"`
 }
 
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
diff --git a/pkg/apiserver/openapi/zz_generated.openapi.go b/pkg/apiserver/openapi/zz_generated.openapi.go
index bf58ce0d3..2350ef803 100644
--- a/pkg/apiserver/openapi/zz_generated.openapi.go
+++ b/pkg/apiserver/openapi/zz_generated.openapi.go
@@ -5401,6 +5401,13 @@ func schema_pkg_apis_crd_v1beta1_Observation(ref common.ReferenceCallback) commo
 							Format:      "",
 						},
 					},
+					"srcPodIP": {
+						SchemaProps: spec.SchemaProps{
+							Description: "SrcPodIP is the IP of source Pod.",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
 				},
 			},
 		},


Test patch:
diff --git a/ci/jenkins/test-mc.sh b/ci/jenkins/test-mc.sh
index 83283ae83..f7f1f7c7b 100755
--- a/ci/jenkins/test-mc.sh
+++ b/ci/jenkins/test-mc.sh
@@ -136,7 +136,7 @@ function clean_tmp() {
 function clean_images() {
     docker images --format "{{.Repository}}:{{.Tag}}" | grep -E 'mc-controller|antrea-ubuntu' | xargs -r docker rmi -f || true
     # Clean up dangling images generated in previous builds.
-    docker image prune -f --filter "until=24h" || true > /dev/null
+    docker image prune -af --filter "until=24h" || true > /dev/null
     check_and_cleanup_docker_build_cache
 }
 
diff --git a/ci/kind/test-e2e-kind.sh b/ci/kind/test-e2e-kind.sh
index fded9d433..34c28b482 100755
--- a/ci/kind/test-e2e-kind.sh
+++ b/ci/kind/test-e2e-kind.sh
@@ -29,14 +29,15 @@ _usage="Usage: $0 [--encap-mode <mode>] [--ip-family <v4|v6|dual>] [--coverage]
         --run                         Run only tests matching the regexp.
         --proxy-all                   Enables Antrea proxy with all Service support.
         --load-balancer-mode          LoadBalancer mode.
-        --node-ipam                   Enables Antrea NodeIPAN.
+        --node-ipam                   Enables Antrea NodeIPAM.
         --multicast                   Enables Multicast.
         --flow-visibility             Only run flow visibility related e2e tests.
+        --networkpolicy-evaluation    Configures additional NetworkPolicy evaluation level when running e2e tests.
         --extra-network               Creates an extra network that worker Nodes will connect to. Cannot be specified with the hybrid mode.
         --extra-vlan                  Creates an subnet-based VLAN that worker Nodes will connect to.
         --deploy-external-server      Deploy a container running as an external server for the cluster.
         --skip                        A comma-separated list of keywords, with which tests should be skipped.
-        --coverage                    Enables measure Antrea code coverage when run e2e tests on kind.
+        --coverage                    Enables measure Antrea code coverage when running e2e tests on kind.
         --setup-only                  Only perform setting up the cluster and run test.
         --cleanup-only                Only perform cleaning up the cluster.
         --test-only                   Only run test on current cluster. Not set up/clean up the cluster.
@@ -76,6 +77,7 @@ load_balancer_mode=""
 node_ipam=false
 multicast=false
 flow_visibility=false
+np_evaluation=false
 extra_network=false
 extra_vlan=false
 deploy_external_server=false
@@ -126,6 +128,10 @@ case $key in
     flow_visibility=true
     shift
     ;;
+    --networkpolicy-evaluation)
+    np_evaluation=true
+    shift
+    ;;
     --encap-mode)
     mode="$2"
     shift 2
@@ -368,11 +374,16 @@ function run_test {
     RUN_OPT="-run $run"
   fi
 
+  np_evaluation_flag=""
+  if $np_evaluation; then
+    np_evaluation_flag="--networkpolicy-evaluation"
+  fi
+
   external_server_cid=$(docker ps -f name="^antrea-external-server" --format '{{.ID}}')
   external_server_ips=$(docker inspect $external_server_cid -f '{{.NetworkSettings.Networks.kind.IPAddress}},{{.NetworkSettings.Networks.kind.GlobalIPv6Address}}')
   EXTRA_ARGS="$vlan_args --external-server-ips $external_server_ips"
 
-  go test -v -timeout=$timeout $RUN_OPT antrea.io/antrea/test/e2e $flow_visibility_args -provider=kind --logs-export-dir=$ANTREA_LOG_DIR --skip-cases=$skiplist $coverage_args $EXTRA_ARGS
+  go test -v -timeout=$timeout $RUN_OPT antrea.io/antrea/test/e2e $flow_visibility_args -provider=kind --logs-export-dir=$ANTREA_LOG_DIR $np_evaluation_flag --skip-cases=$skiplist $coverage_args $EXTRA_ARGS
 }
 
 if [[ "$mode" == "" ]] || [[ "$mode" == "encap" ]]; then
diff --git a/ci/test-conformance-aks.sh b/ci/test-conformance-aks.sh
index 4e692a4ff..c4738373f 100755
--- a/ci/test-conformance-aks.sh
+++ b/ci/test-conformance-aks.sh
@@ -191,7 +191,7 @@ function deliver_antrea_to_aks() {
     fi
     # Clean up dangling images generated in previous builds. Recent ones must be excluded
     # because they might be being used in other builds running simultaneously.
-    docker image prune -f --filter "until=2h" > /dev/null
+    docker image prune -af --filter "until=2h" > /dev/null
     docker system df -v
     check_and_cleanup_docker_build_cache
 
diff --git a/ci/test-conformance-eks.sh b/ci/test-conformance-eks.sh
index 2059bb3f6..a2fc300aa 100755
--- a/ci/test-conformance-eks.sh
+++ b/ci/test-conformance-eks.sh
@@ -268,7 +268,7 @@ function deliver_antrea_to_eks() {
     fi
     # Clean up dangling images generated in previous builds. Recent ones must be excluded
     # because they might be being used in other builds running simultaneously.
-    docker image prune -f --filter "until=2h" > /dev/null
+    docker image prune -af --filter "until=2h" > /dev/null
     docker system df -v
     check_and_cleanup_docker_build_cache
     set -e
diff --git a/ci/test-conformance-gke.sh b/ci/test-conformance-gke.sh
index 255fbcf3f..750ff1926 100755
--- a/ci/test-conformance-gke.sh
+++ b/ci/test-conformance-gke.sh
@@ -216,7 +216,7 @@ function deliver_antrea_to_gke() {
     fi
     # Clean up dangling images generated in previous builds. Recent ones must be excluded
     # because they might be being used in other builds running simultaneously.
-    docker image prune -f --filter "until=2h" > /dev/null
+    docker image prune -af --filter "until=2h" > /dev/null
     docker system df -v
     check_and_cleanup_docker_build_cache
     set -e
diff --git a/pkg/agent/controller/networkpolicy/node_reconciler_linux_test.go b/pkg/agent/controller/networkpolicy/node_reconciler_linux_test.go
index 75abd9f6b..08428520c 100644
--- a/pkg/agent/controller/networkpolicy/node_reconciler_linux_test.go
+++ b/pkg/agent/controller/networkpolicy/node_reconciler_linux_test.go
@@ -76,7 +76,7 @@ var (
 	ingressRule1   = &CompletedRule{
 		rule: &rule{
 			ID:             ingressRuleID1,
-			Name:           "rule-01",
+			Name:           "ingress-rule-01",
 			PolicyName:     "ingress-policy",
 			From:           ipBlocks,
 			Direction:      v1beta2.DirectionIn,
@@ -93,7 +93,7 @@ var (
 	ingressRule2 = &CompletedRule{
 		rule: &rule{
 			ID:             ingressRuleID2,
-			Name:           "rule-02",
+			Name:           "ingress-rule-02",
 			PolicyName:     "ingress-policy",
 			Direction:      v1beta2.DirectionIn,
 			Services:       []v1beta2.Service{serviceTCP443},
@@ -109,7 +109,7 @@ var (
 	ingressRule3 = &CompletedRule{
 		rule: &rule{
 			ID:             ingressRuleID3,
-			Name:           "rule-03",
+			Name:           "ingress-rule-03",
 			PolicyName:     "ingress-policy",
 			From:           ipBlocksToMatchAny,
 			Direction:      v1beta2.DirectionIn,
@@ -127,7 +127,7 @@ var (
 	updatedIngressRule3WithOneFromAddress = &CompletedRule{
 		rule: &rule{
 			ID:             ingressRuleID3,
-			Name:           "rule-03",
+			Name:           "ingress-rule-03",
 			PolicyName:     "ingress-policy",
 			Direction:      v1beta2.DirectionIn,
 			Services:       []v1beta2.Service{serviceTCP8080},
@@ -143,7 +143,7 @@ var (
 	updatedIngressRule3WithAnotherFromAddress = &CompletedRule{
 		rule: &rule{
 			ID:             ingressRuleID3,
-			Name:           "rule-03",
+			Name:           "ingress-rule-03",
 			PolicyName:     "ingress-policy",
 			Direction:      v1beta2.DirectionIn,
 			Services:       []v1beta2.Service{serviceTCP8080},
@@ -159,7 +159,7 @@ var (
 	updatedIngressRule3WithMultipleFromAddresses = &CompletedRule{
 		rule: &rule{
 			ID:             ingressRuleID3,
-			Name:           "rule-03",
+			Name:           "ingress-rule-03",
 			PolicyName:     "ingress-policy",
 			Direction:      v1beta2.DirectionIn,
 			Services:       []v1beta2.Service{serviceTCP8080},
@@ -175,7 +175,7 @@ var (
 	updatedIngressRule3WithOtherMultipleFromAddresses = &CompletedRule{
 		rule: &rule{
 			ID:             ingressRuleID3,
-			Name:           "rule-03",
+			Name:           "ingress-rule-03",
 			PolicyName:     "ingress-policy",
 			Direction:      v1beta2.DirectionIn,
 			Services:       []v1beta2.Service{serviceTCP8080},
@@ -191,7 +191,7 @@ var (
 	updatedIngressRule3WithFromNoAddress = &CompletedRule{
 		rule: &rule{
 			ID:             ingressRuleID3,
-			Name:           "rule-03",
+			Name:           "ingress-rule-03",
 			PolicyName:     "ingress-policy",
 			Direction:      v1beta2.DirectionIn,
 			Services:       []v1beta2.Service{serviceTCP8080},
@@ -207,7 +207,7 @@ var (
 	egressRule1 = &CompletedRule{
 		rule: &rule{
 			ID:             egressRuleID1,
-			Name:           "rule-01",
+			Name:           "egress-rule-01",
 			PolicyName:     "egress-policy",
 			Direction:      v1beta2.DirectionOut,
 			Services:       []v1beta2.Service{serviceTCP80, serviceTCP443},
@@ -223,7 +223,7 @@ var (
 	egressRule2 = &CompletedRule{
 		rule: &rule{
 			ID:             egressRuleID2,
-			Name:           "rule-02",
+			Name:           "egress-rule-02",
 			PolicyName:     "egress-policy",
 			Direction:      v1beta2.DirectionOut,
 			Services:       []v1beta2.Service{serviceTCP443},
@@ -264,7 +264,7 @@ func TestNodeReconcilerReconcileAndForget(t *testing.T) {
 				}
 				coreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", sets.New[string]("1.1.1.1/32", "192.168.1.0/25"), false).Times(1)
@@ -294,7 +294,7 @@ func TestNodeReconcilerReconcileAndForget(t *testing.T) {
 				}
 				coreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESSRULE1"}, serviceRules, true).Times(1)
@@ -322,12 +322,12 @@ func TestNodeReconcilerReconcileAndForget(t *testing.T) {
 				}
 				coreRulesIPv4 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRulesIPv6 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", sets.New[string]("1.1.1.1/32", "192.168.1.0/25"), false).Times(1)
@@ -363,31 +363,31 @@ func TestNodeReconcilerReconcileAndForget(t *testing.T) {
 				}
 				coreRules1 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRules2 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRules3 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRulesDeleted3 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRulesDelete2 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 
@@ -416,13 +416,13 @@ func TestNodeReconcilerReconcileAndForget(t *testing.T) {
 			expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) {
 				coreRules3 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRules2 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				serviceRules1 := [][]string{
@@ -433,20 +433,20 @@ func TestNodeReconcilerReconcileAndForget(t *testing.T) {
 				}
 				coreRules1 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRulesDelete3 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRulesDelete1 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRules3, false).Times(1)
@@ -476,7 +476,7 @@ func TestNodeReconcilerReconcileAndForget(t *testing.T) {
 			expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) {
 				coreRules2 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				serviceRules1 := [][]string{
@@ -487,26 +487,26 @@ func TestNodeReconcilerReconcileAndForget(t *testing.T) {
 				}
 				coreRules1 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRules3 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRulesDelete2 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRulesDelete1 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 
@@ -537,22 +537,22 @@ func TestNodeReconcilerReconcileAndForget(t *testing.T) {
 			expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) {
 				coreRules1 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRules2 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRules3 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.2/32 -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.2/32 -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRules4 := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE3-4 src -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE3-4 src -p tcp --dport 8080 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-03, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				coreRules6 := coreRules2
@@ -666,8 +666,8 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				coreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				svcChains := []string{
@@ -681,7 +681,7 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				updatedCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 
@@ -710,8 +710,8 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				coreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				svcChains := []string{
@@ -725,7 +725,7 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				updatedCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 
@@ -753,14 +753,14 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				ipv4CoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				ipv6CoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				svcChains := []string{
@@ -775,12 +775,12 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				ipv6SvcRules := ipv4SvcRules
 				updatedIPv4CoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				updatedIPv6CoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 
@@ -815,8 +815,8 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				coreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				svcChains := []string{
@@ -830,7 +830,7 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				updatedCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 
@@ -857,8 +857,8 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				coreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				svcChains := []string{
@@ -872,7 +872,7 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				updatedCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 
@@ -900,14 +900,14 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				}
 				ipv4CoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				ipv6CoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				svcChains := []string{
@@ -922,12 +922,12 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				ipv6SvcRules := ipv4SvcRules
 				updatedIPv4CoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				updatedIPv6CoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(svcChains, ipv4SvcRules, false).Times(1)
@@ -973,24 +973,24 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				egressCoreChains := []string{"ANTREA-POL-EGRESS-RULES"}
 				ingressCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				egressCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				updatedIngressCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				updatedEgressCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 1.1.1.1/32 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 
@@ -1038,24 +1038,24 @@ func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) {
 				egressCoreChains := []string{"ANTREA-POL-EGRESS-RULES"}
 				ingressCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				egressCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`,
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egress-rule-01, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				updatedIngressCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-INGRESS-RULES -s 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule ingress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 				updatedEgressCoreRules := [][]string{
 					{
-						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`,
+						`-A ANTREA-POL-EGRESS-RULES -d 2002:1a23:fb44::1/128 -p tcp --dport 443 -j ACCEPT -m comment --comment "Antrea: for rule egress-rule-02, policy AntreaClusterNetworkPolicy:name1"`,
 					},
 				}
 
diff --git a/pkg/agent/controller/traceflow/packetin_test.go b/pkg/agent/controller/traceflow/packetin_test.go
index bd6e97d30..253405820 100644
--- a/pkg/agent/controller/traceflow/packetin_test.go
+++ b/pkg/agent/controller/traceflow/packetin_test.go
@@ -209,8 +209,8 @@ func getTestPacketBytes(dstIP string) []byte {
 		Protocol: uint8(8),
 		DSCP:     1,
 		Length:   20,
-		NWSrc:    net.IP(pod1IPv4),
-		NWDst:    net.IP(dstIP),
+		NWSrc:    net.ParseIP(pod1IPv4),
+		NWDst:    net.ParseIP(dstIP),
 	}
 	ethernetPkt := protocol.NewEthernet()
 	ethernetPkt.HWSrc = pod1MAC
@@ -238,6 +238,13 @@ func TestParsePacketIn(t *testing.T) {
 			Data: 1,
 		},
 	}
+	matchCTSrc := &openflow15.MatchField{
+		Class: openflow15.OXM_CLASS_NXM_1,
+		Field: openflow15.NXM_NX_CT_NW_SRC,
+		Value: &openflow15.Ipv4SrcField{
+			Ipv4Src: net.ParseIP(pod1IPv4),
+		},
+	}
 	matchTunDst := openflow15.NewTunnelIpv4DstField(net.ParseIP(egressIP), nil)
 
 	conjData := make([]byte, 8)
@@ -298,7 +305,7 @@ func TestParsePacketIn(t *testing.T) {
 				PacketIn: &openflow15.PacketIn{
 					TableId: openflow.OutputTable.GetID(),
 					Match: openflow15.Match{
-						Fields: []openflow15.MatchField{*matchOutPort, *matchPktMark},
+						Fields: []openflow15.MatchField{*matchOutPort, *matchPktMark, *matchCTSrc},
 					},
 					Data: util.NewBuffer(pktBytesPodToIP),
 				},
@@ -329,6 +336,7 @@ func TestParsePacketIn(t *testing.T) {
 					{
 						Component: crdv1beta1.ComponentSpoofGuard,
 						Action:    crdv1beta1.ActionForwarded,
+						SrcPodIP:  pod1IPv4,
 					},
 					{
 						Component:  crdv1beta1.ComponentEgress,
@@ -365,6 +373,8 @@ func TestParsePacketIn(t *testing.T) {
 				PacketIn: &openflow15.PacketIn{
 					TableId: openflow.OutputTable.GetID(),
 					Match: openflow15.Match{
+						// We are omitting matchCTSrc intentionally here to test
+						// the case where there is no valid ct_nw_src match in the packet metadata.
 						Fields: []openflow15.MatchField{*matchTunDst, *matchOutPort},
 					},
 					Data: util.NewBuffer(pktBytesPodToIP),
@@ -396,6 +406,7 @@ func TestParsePacketIn(t *testing.T) {
 					{
 						Component: crdv1beta1.ComponentSpoofGuard,
 						Action:    crdv1beta1.ActionForwarded,
+						SrcPodIP:  pod1IPv4,
 					},
 					{
 						Component:  crdv1beta1.ComponentEgress,
@@ -489,7 +500,7 @@ func TestParsePacketIn(t *testing.T) {
 				PacketIn: &openflow15.PacketIn{
 					TableId: openflow.EgressRuleTable.GetID(),
 					Match: openflow15.Match{
-						Fields: []openflow15.MatchField{*matchTFEgressConjID},
+						Fields: []openflow15.MatchField{*matchTFEgressConjID, *matchCTSrc},
 					},
 					Data: util.NewBuffer(pktBytesPodToPod),
 				},
@@ -531,6 +542,7 @@ func TestParsePacketIn(t *testing.T) {
 					{
 						Component: crdv1beta1.ComponentSpoofGuard,
 						Action:    crdv1beta1.ActionForwarded,
+						SrcPodIP:  pod1IPv4,
 					},
 					{
 						Component:         crdv1beta1.ComponentNetworkPolicy,
@@ -618,7 +630,7 @@ func TestParsePacketIn(t *testing.T) {
 				PacketIn: &openflow15.PacketIn{
 					TableId: openflow.EgressMetricTable.GetID(),
 					Match: openflow15.Match{
-						Fields: []openflow15.MatchField{*matchAPConjID},
+						Fields: []openflow15.MatchField{*matchAPConjID, *matchCTSrc},
 					},
 					Data: util.NewBuffer(pktBytesPodToPod),
 				},
@@ -658,6 +670,7 @@ func TestParsePacketIn(t *testing.T) {
 					{
 						Component: crdv1beta1.ComponentSpoofGuard,
 						Action:    crdv1beta1.ActionForwarded,
+						SrcPodIP:  pod1IPv4,
 					},
 					{
 						Component:         crdv1beta1.ComponentNetworkPolicy,
diff --git a/pkg/agent/nodeportlocal/npl_agent_test.go b/pkg/agent/nodeportlocal/npl_agent_test.go
index 53b33f61a..bcdd90d92 100644
--- a/pkg/agent/nodeportlocal/npl_agent_test.go
+++ b/pkg/agent/nodeportlocal/npl_agent_test.go
@@ -175,11 +175,12 @@ func getTestSvcWithPortName(portName string) *corev1.Service {
 
 type testData struct {
 	*testing.T
-	stopCh    chan struct{}
-	ctrl      *gomock.Controller
-	k8sClient *k8sfake.Clientset
-	portTable *portcache.PortTable
-	wg        sync.WaitGroup
+	stopCh      chan struct{}
+	ctrl        *gomock.Controller
+	k8sClient   *k8sfake.Clientset
+	portTable   *portcache.PortTable
+	svcInformer cache.SharedIndexInformer
+	wg          sync.WaitGroup
 }
 
 func (t *testData) runWrapper(c *k8s.NPLController) {
@@ -233,22 +234,18 @@ func setUp(t *testing.T, tc *testConfig, objects ...runtime.Object) *testData {
 		mockPortOpener.EXPECT().OpenLocalPort(gomock.Any(), gomock.Any()).AnyTimes().Return(&fakeSocket{}, nil)
 	}
 
-	data := &testData{
-		T:         t,
-		stopCh:    make(chan struct{}),
-		ctrl:      mockCtrl,
-		k8sClient: k8sfake.NewSimpleClientset(objects...),
-		portTable: newPortTable(mockIPTables, mockPortOpener),
-	}
+	k8sClient := k8sfake.NewSimpleClientset(objects...)
+
+	portTable := newPortTable(mockIPTables, mockPortOpener)
 
 	resyncPeriod := 0 * time.Minute
 	// informerFactory is initialized and started from cmd/antrea-agent/agent.go
-	informerFactory := informers.NewSharedInformerFactory(data.k8sClient, resyncPeriod)
+	informerFactory := informers.NewSharedInformerFactory(k8sClient, resyncPeriod)
 	listOptions := func(options *metav1.ListOptions) {
 		options.FieldSelector = fields.OneTermEqualSelector("spec.nodeName", defaultNodeName).String()
 	}
 	localPodInformer := coreinformers.NewFilteredPodInformer(
-		data.k8sClient,
+		k8sClient,
 		metav1.NamespaceAll,
 		resyncPeriod,
 		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, // NamespaceIndex is used in NPLController.
@@ -256,7 +253,16 @@ func setUp(t *testing.T, tc *testConfig, objects ...runtime.Object) *testData {
 	)
 	svcInformer := informerFactory.Core().V1().Services().Informer()
 
-	c := k8s.NewNPLController(data.k8sClient, localPodInformer, svcInformer, data.portTable, defaultNodeName)
+	c := k8s.NewNPLController(k8sClient, localPodInformer, svcInformer, portTable, defaultNodeName)
+
+	data := &testData{
+		T:           t,
+		stopCh:      make(chan struct{}),
+		ctrl:        mockCtrl,
+		k8sClient:   k8sClient,
+		portTable:   portTable,
+		svcInformer: svcInformer,
+	}
 
 	data.runWrapper(c)
 	informerFactory.Start(data.stopCh)
@@ -303,31 +309,41 @@ func (t *testData) tearDown() {
 	t.wg.Wait()
 }
 
-func (t *testData) pollForPodAnnotation(podName string, found bool) ([]types.NPLAnnotation, error) {
-	var data string
-	var exists bool
+func conditionMatchAll([]types.NPLAnnotation) bool {
+	return true
+}
+
+// If conditionFn is nil, we will assume you are looking for a non-existing annotation.
+// If you want to match all, use conditionMatchAll as the conditionFn.
+func (t *testData) pollForPodAnnotationWithCondition(podName string, conditionFn func([]types.NPLAnnotation) bool) ([]types.NPLAnnotation, error) {
+	var nplValue []types.NPLAnnotation
 	// do not use PollImmediate: 1 second is reserved for the controller to do his job and
 	// update Pod NPL annotations as needed.
 	err := wait.PollUntilContextTimeout(context.Background(), time.Second, 20*time.Second, false, func(ctx context.Context) (bool, error) {
 		updatedPod, err := t.k8sClient.CoreV1().Pods(defaultNS).Get(context.TODO(), podName, metav1.GetOptions{})
 		require.NoError(t, err, "Failed to get Pod")
 		annotation := updatedPod.GetAnnotations()
-		data, exists = annotation[types.NPLAnnotationKey]
-		if found {
-			return exists, nil
+		data, exists := annotation[types.NPLAnnotationKey]
+		if !exists {
+			return conditionFn == nil, nil
 		}
-		return !exists, nil
+		if conditionFn == nil {
+			return false, nil
+		}
+		if err := json.Unmarshal([]byte(data), &nplValue); err != nil {
+			return false, err
+		}
+		return conditionFn(nplValue), nil
 	})
+	return nplValue, err
+}
 
-	if err != nil {
-		return []types.NPLAnnotation{}, err
-	}
-	if data == "" {
-		return []types.NPLAnnotation{}, nil
+func (t *testData) pollForPodAnnotation(podName string, found bool) ([]types.NPLAnnotation, error) {
+	var conditionFn func([]types.NPLAnnotation) bool
+	if found {
+		conditionFn = conditionMatchAll
 	}
-	var nplValue []types.NPLAnnotation
-	err = json.Unmarshal([]byte(data), &nplValue)
-	return nplValue, err
+	return t.pollForPodAnnotationWithCondition(podName, conditionFn)
 }
 
 func (t *testData) updateServiceOrFail(testSvc *corev1.Service) {
@@ -495,6 +511,7 @@ func TestPodDelete(t *testing.T) {
 
 // TestPodAddMultiPort creates a Pod and a Service with two target ports.
 // It verifies that the Pod's NPL annotation and the local port table are updated with both ports.
+// It then updates the Service to remove one of the target ports.
 func TestAddMultiPortPodSvc(t *testing.T) {
 	newPort := 90
 	testSvc := getTestSvc(defaultPort, int32(newPort))
@@ -508,6 +525,16 @@ func TestAddMultiPortPodSvc(t *testing.T) {
 	expectedAnnotations.Check(t, value)
 	assert.True(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
 	assert.True(t, testData.portTable.RuleExists(defaultPodIP, newPort, protocolTCP))
+
+	// Remove the second target port.
+	testSvc.Spec.Ports = testSvc.Spec.Ports[:1]
+	testData.updateServiceOrFail(testSvc)
+	// Wait for annotation to be updated (single mapping).
+	value, err = testData.pollForPodAnnotationWithCondition(testPod.Name, func(value []types.NPLAnnotation) bool { return len(value) == 1 })
+	require.NoError(t, err, "Poll for annotation check failed")
+	expectedAnnotations = newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
+	expectedAnnotations.Check(t, value)
+	assert.False(t, testData.portTable.RuleExists(defaultPodIP, newPort, protocolTCP))
 }
 
 // TestPodAddMultiPort creates a Pod with multiple ports and a Service with only one target port.
@@ -805,3 +832,99 @@ func TestSyncRulesError(t *testing.T) {
 	testData, _, _ := setUpWithTestServiceAndPod(t, testConfig, nil)
 	defer testData.tearDown()
 }
+
+func TestSingleRuleDeletionError(t *testing.T) {
+	newPort := 90
+	testSvc := getTestSvc(defaultPort, int32(newPort))
+	testPod := getTestPod()
+
+	testConfig := newTestConfig().withCustomPodPortRulesExpectations(func(mockIPTables *rulestesting.MockPodPortRules) {
+		mockIPTables.EXPECT().AddAllRules(gomock.Any()).AnyTimes()
+		gomock.InOrder(
+			mockIPTables.EXPECT().AddRule(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2),
+			mockIPTables.EXPECT().DeleteRule(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("iptables failure")),
+			mockIPTables.EXPECT().DeleteRule(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
+		)
+	})
+
+	testData := setUp(t, testConfig, testSvc, testPod)
+	defer testData.tearDown()
+
+	value, err := testData.pollForPodAnnotation(testPod.Name, true)
+	require.NoError(t, err, "Poll for annotation check failed")
+	expectedAnnotations := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP).Add(nil, newPort, protocolTCP)
+	expectedAnnotations.Check(t, value)
+	assert.True(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodIP, newPort, protocolTCP))
+
+	// Remove the second target port, to force one mapping to be deleted.
+	testSvc.Spec.Ports = testSvc.Spec.Ports[:1]
+	testData.updateServiceOrFail(testSvc)
+	// The first deletion attempt will fail, but the second should succeed.
+	// Wait for annotation to be updated (single mapping).
+	value, err = testData.pollForPodAnnotationWithCondition(testPod.Name, func(value []types.NPLAnnotation) bool { return len(value) == 1 })
+	require.NoError(t, err, "Poll for annotation check failed")
+	expectedAnnotations = newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
+	expectedAnnotations.Check(t, value)
+	assert.False(t, testData.portTable.RuleExists(defaultPodIP, newPort, protocolTCP))
+}
+
+func TestPreventDefunctRuleReuse(t *testing.T) {
+	newPort := 90
+	testSvc := getTestSvc(defaultPort, int32(newPort))
+	testPod := getTestPod()
+
+	var testData *testData
+
+	ports := testSvc.Spec.Ports
+	// This function will be executed synchronously when DeleteRule is called for the first time
+	// and we simulate a failure. It restores the second target port for the Service, which was
+	// deleted previously, and waits for the change to be reflected in the informer's
+	// store. After that, we know that the next time the NPL controller processes the test Pod,
+	// it will need to ensure that both NPL mappings are configured correctly. Because one of
+	// the rules will be marked as "defunct", it will first need to delete the rule properly
+	// before adding it back.
+	restoreServiceTargetPorts := func() {
+		testSvc.Spec.Ports = ports
+		_, err := testData.k8sClient.CoreV1().Services(defaultNS).Update(context.TODO(), testSvc, metav1.UpdateOptions{})
+		if !assert.NoError(t, err) {
+			return
+		}
+		assert.EventuallyWithT(t, func(c *assert.CollectT) {
+			obj, exists, err := testData.svcInformer.GetIndexer().GetByKey(testSvc.Namespace + "/" + testSvc.Name)
+			if !assert.NoError(t, err) || !assert.True(t, exists) {
+				return
+			}
+			svc := obj.(*corev1.Service)
+			assert.Len(t, svc.Spec.Ports, 2)
+		}, 2*time.Second, 50*time.Millisecond)
+	}
+
+	testConfig := newTestConfig().withCustomPodPortRulesExpectations(func(mockIPTables *rulestesting.MockPodPortRules) {
+		mockIPTables.EXPECT().AddAllRules(gomock.Any()).AnyTimes()
+		gomock.InOrder(
+			mockIPTables.EXPECT().AddRule(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2),
+			mockIPTables.EXPECT().DeleteRule(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(
+				func(nodePort int, podIP string, podPort int, protocol string) { restoreServiceTargetPorts() },
+			).Return(fmt.Errorf("iptables failure")),
+			mockIPTables.EXPECT().DeleteRule(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
+			mockIPTables.EXPECT().AddRule(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
+		)
+	})
+
+	testData = setUp(t, testConfig, testSvc, testPod)
+	defer testData.tearDown()
+
+	value, err := testData.pollForPodAnnotation(testPod.Name, true)
+	require.NoError(t, err, "Poll for annotation check failed")
+	expectedAnnotations := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP).Add(nil, newPort, protocolTCP)
+	expectedAnnotations.Check(t, value)
+	assert.True(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodIP, newPort, protocolTCP))
+
+	// Remove the second target port, to force one mapping to be deleted.
+	testSvc.Spec.Ports = testSvc.Spec.Ports[:1]
+	testData.updateServiceOrFail(testSvc)
+
+	assert.Eventually(t, testData.ctrl.Satisfied, 2*time.Second, 50*time.Millisecond)
+}
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_others_test.go b/pkg/agent/nodeportlocal/portcache/port_table_others_test.go
index 2ac40b34e..c5e1e4245 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_others_test.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_others_test.go
@@ -18,9 +18,12 @@
 package portcache
 
 import (
+	"fmt"
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
 
 	portcachetesting "antrea.io/antrea/pkg/agent/nodeportlocal/portcache/testing"
@@ -73,3 +76,56 @@ func TestRestoreRules(t *testing.T) {
 		t.Fatalf("Rule restoration not complete after %v", timeout)
 	}
 }
+
+type mockCloser struct {
+	closeErr error
+}
+
+func (m *mockCloser) Close() error {
+	return m.closeErr
+}
+
+func TestDeleteRule(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	mockIPTables := rulestesting.NewMockPodPortRules(mockCtrl)
+	mockPortOpener := portcachetesting.NewMockLocalPortOpener(mockCtrl)
+	portTable := newPortTable(mockIPTables, mockPortOpener)
+
+	const (
+		podPort  = 1001
+		protocol = "tcp"
+	)
+
+	closer := &mockCloser{}
+
+	data := &NodePortData{
+		NodePort: nodePort1,
+		PodPort:  podPort,
+		PodIP:    podIP,
+		Protocol: ProtocolSocketData{
+			Protocol: protocol,
+			socket:   closer,
+		},
+	}
+
+	require.NoError(t, portTable.addPortTableCache(data))
+	assert.False(t, data.Defunct())
+
+	mockIPTables.EXPECT().DeleteRule(nodePort1, podIP, podPort, protocol).Return(fmt.Errorf("iptables error"))
+	require.ErrorContains(t, portTable.DeleteRule(podIP, podPort, protocol), "iptables error")
+
+	mockIPTables.EXPECT().DeleteRule(nodePort1, podIP, podPort, protocol)
+	closer.closeErr = fmt.Errorf("close error")
+	require.ErrorContains(t, portTable.DeleteRule(podIP, podPort, protocol), "close error")
+	assert.True(t, data.Defunct())
+
+	closer.closeErr = nil
+
+	// First successful call to DeleteRule.
+	mockIPTables.EXPECT().DeleteRule(nodePort1, podIP, podPort, protocol)
+	assert.NoError(t, portTable.DeleteRule(podIP, podPort, protocol))
+
+	// Calling DeleteRule again will return immediately as the NodePortData entry has been
+	// removed from the cache.
+	assert.NoError(t, portTable.DeleteRule(podIP, podPort, protocol))
+}
diff --git a/pkg/antctl/raw/check/installation/test_podtointernet.go b/pkg/antctl/raw/check/installation/test_podtointernet.go
new file mode 100644
index 000000000..50bc7978d
--- /dev/null
+++ b/pkg/antctl/raw/check/installation/test_podtointernet.go
@@ -0,0 +1,41 @@
+// Copyright 2024 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 installation
+
+import (
+	"context"
+	"fmt"
+
+	"antrea.io/antrea/pkg/antctl/raw/check"
+)
+
+type PodToInternetConnectivityTest struct{}
+
+func init() {
+	RegisterTest("pod-to-internet-connectivity", &PodToInternetConnectivityTest{})
+}
+
+func (t *PodToInternetConnectivityTest) Run(ctx context.Context, testContext *testContext) error {
+	for _, clientPod := range testContext.clientPods {
+		srcPod := testContext.namespace + "/" + clientPod.Name
+		testContext.Log("Validating connectivity from Pod %s to the world (google.com)...", srcPod)
+		_, _, err := check.ExecInPod(ctx, testContext.client, testContext.config, testContext.namespace, clientPod.Name, clientDeploymentName, agnhostConnectCommand("google.com", "80"))
+		if err != nil {
+			return fmt.Errorf("Pod %s was not able to connect to google.com: %w", srcPod, err)
+		}
+		testContext.Log("Pod %s was able to connect to google.com", srcPod)
+	}
+	return nil
+}
diff --git a/pkg/antctl/raw/check/installation/test_podtopodinternode.go b/pkg/antctl/raw/check/installation/test_podtopodinternode.go
new file mode 100644
index 000000000..556e72faf
--- /dev/null
+++ b/pkg/antctl/raw/check/installation/test_podtopodinternode.go
@@ -0,0 +1,48 @@
+// Copyright 2024 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 installation
+
+import (
+	"context"
+	"fmt"
+
+	"antrea.io/antrea/pkg/antctl/raw/check"
+)
+
+type PodToPodInterNodeConnectivityTest struct{}
+
+func init() {
+	RegisterTest("pod-to-pod-internode-connectivity", &PodToPodInterNodeConnectivityTest{})
+}
+
+func (t *PodToPodInterNodeConnectivityTest) Run(ctx context.Context, testContext *testContext) error {
+	if testContext.echoOtherNodePod == nil {
+		return newNotRunnableError("Inter-Node test requires multiple Nodes")
+	}
+	for _, clientPod := range testContext.clientPods {
+		srcPod := testContext.namespace + "/" + clientPod.Name
+		dstPod := testContext.namespace + "/" + testContext.echoOtherNodePod.Name
+		for _, podIP := range testContext.echoOtherNodePod.Status.PodIPs {
+			echoIP := podIP.IP
+			testContext.Log("Validating from Pod %s to Pod %s at IP %s...", srcPod, dstPod, echoIP)
+			_, _, err := check.ExecInPod(ctx, testContext.client, testContext.config, testContext.namespace, clientPod.Name, "", agnhostConnectCommand(echoIP, "80"))
+			if err != nil {
+				return fmt.Errorf("client Pod %s was not able to communicate with echo Pod %s (%s): %w", clientPod.Name, testContext.echoOtherNodePod.Name, echoIP, err)
+			}
+			testContext.Log("client Pod %s was able to communicate with echo Pod %s (%s)", clientPod.Name, testContext.echoOtherNodePod.Name, echoIP)
+		}
+	}
+	return nil
+}
diff --git a/pkg/antctl/raw/check/installation/test_podtopodintranode.go b/pkg/antctl/raw/check/installation/test_podtopodintranode.go
new file mode 100644
index 000000000..5fae4cbe9
--- /dev/null
+++ b/pkg/antctl/raw/check/installation/test_podtopodintranode.go
@@ -0,0 +1,45 @@
+// Copyright 2024 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 installation
+
+import (
+	"context"
+	"fmt"
+
+	"antrea.io/antrea/pkg/antctl/raw/check"
+)
+
+type PodToPodIntraNodeConnectivityTest struct{}
+
+func init() {
+	RegisterTest("pod-to-pod-intranode-connectivity", &PodToPodIntraNodeConnectivityTest{})
+}
+
+func (t *PodToPodIntraNodeConnectivityTest) Run(ctx context.Context, testContext *testContext) error {
+	for _, clientPod := range testContext.clientPods {
+		srcPod := testContext.namespace + "/" + clientPod.Name
+		dstPod := testContext.namespace + "/" + testContext.echoSameNodePod.Name
+		for _, podIP := range testContext.echoSameNodePod.Status.PodIPs {
+			echoIP := podIP.IP
+			testContext.Log("Validating from Pod %s to Pod %s at IP %s...", srcPod, dstPod, echoIP)
+			_, _, err := check.ExecInPod(ctx, testContext.client, testContext.config, testContext.namespace, clientPod.Name, "", agnhostConnectCommand(echoIP, "80"))
+			if err != nil {
+				return fmt.Errorf("client Pod %s was not able to communicate with echo Pod %s (%s): %w", clientPod.Name, testContext.echoSameNodePod.Name, echoIP, err)
+			}
+			testContext.Log("client Pod %s was able to communicate with echo Pod %s (%s)", clientPod.Name, testContext.echoSameNodePod.Name, echoIP)
+		}
+	}
+	return nil
+}
diff --git a/test/e2e/antreapolicy_test.go b/test/e2e/antreapolicy_test.go
index 9d91bf745..6d49539a4 100644
--- a/test/e2e/antreapolicy_test.go
+++ b/test/e2e/antreapolicy_test.go
@@ -72,9 +72,11 @@ const (
 	// Verification of deleting/creating resources timed out.
 	timeout = 10 * time.Second
 	// audit log directory on Antrea Agent
-	logDir          = "/var/log/antrea/networkpolicy/"
-	logfileName     = "np.log"
-	defaultTierName = "application"
+	logDir              = "/var/log/antrea/networkpolicy/"
+	logfileName         = "np.log"
+	npEvaluationPodName = "antctl"
+	defaultTierName     = "application"
+	defaultDenyKNPName  = "default-deny-namespace"
 )
 
 func failOnError(err error, t *testing.T) {
@@ -157,7 +159,7 @@ func applyDefaultDenyToAllNamespaces(k8s *KubernetesUtils, namespaces map[string
 	}
 	for _, ns := range namespaces {
 		builder := &NetworkPolicySpecBuilder{}
-		builder = builder.SetName(ns.Name, "default-deny-namespace")
+		builder = builder.SetName(ns.Name, defaultDenyKNPName)
 		builder.SetTypeIngress()
 		if _, err := k8s.CreateOrUpdateNetworkPolicy(builder.Get()); err != nil {
 			return err
@@ -469,37 +471,25 @@ func testACNPSourcePort(t *testing.T) {
 
 	testSteps := []*TestStep{
 		{
-			"Port 80",
-			reachability,
-			[]metav1.Object{builder.Get()},
-			[]int32{80},
-			ProtocolTCP,
-			0,
-			nil,
-			nil,
-			nil,
+			Name:          "Port 80",
+			Reachability:  reachability,
+			TestResources: []metav1.Object{builder.Get()},
+			Ports:         []int32{80},
+			Protocol:      ProtocolTCP,
 		},
 		{
-			"Port 81",
-			updatedReachability,
-			[]metav1.Object{builder2.Get()},
-			[]int32{81},
-			ProtocolTCP,
-			0,
-			nil,
-			nil,
-			nil,
+			Name:          "Port 81",
+			Reachability:  updatedReachability,
+			TestResources: []metav1.Object{builder2.Get()},
+			Ports:         []int32{81},
+			Protocol:      ProtocolTCP,
 		},
 		{
-			"Port range 80-81",
-			reachability,
-			[]metav1.Object{builder3.Get()},
-			[]int32{80, 81},
-			ProtocolTCP,
-			0,
-			nil,
-			nil,
-			nil,
+			Name:          "Port range 80-81",
+			Reachability:  reachability,
+			TestResources: []metav1.Object{builder3.Get()},
+			Ports:         []int32{80, 81},
+			Protocol:      ProtocolTCP,
 		},
 	}
 	testCase := []*TestCase{
@@ -541,7 +531,7 @@ func testACNPAllowXBtoYA(t *testing.T) {
 // testACNPPriorityOverrideDefaultDeny tests priority override in ACNP. It applies a higher priority ACNP to drop
 // traffic from namespace Z to X/A, and in the meantime applies a lower priority ACNP to allow traffic from Z to X.
 // It is tested with default deny k8s NetworkPolicies in all namespaces.
-func testACNPPriorityOverrideDefaultDeny(t *testing.T) {
+func testACNPPriorityOverrideDefaultDeny(t *testing.T, data *TestData) {
 	builder1 := &ClusterNetworkPolicySpecBuilder{}
 	builder1 = builder1.SetName("acnp-priority2").
 		SetPriority(2).
@@ -566,10 +556,16 @@ func testACNPPriorityOverrideDefaultDeny(t *testing.T) {
 	reachabilityBothACNP.Expect(getPod("z", "c"), getPod("x", "c"), Connected)
 	reachabilityBothACNP.ExpectSelf(allPods, Connected)
 
+	evaluationBothACNPs := NewNPEvaluation(allPods).
+		Expect(getPod("y", "a"), getPod("x", "a"), defaultDenyKNPName, NPEvalIsolate).
+		Expect(getPod("z", "b"), getPod("x", "a"), builder2.Name, NPEvalDrop).
+		Expect(getPod("z", "b"), getPod("x", "b"), builder1.Name, NPEvalAllow)
+
 	testStep := []*TestStep{
 		{
 			Name:          "Both ACNP",
 			Reachability:  reachabilityBothACNP,
+			NPEvaluation:  evaluationBothACNPs,
 			TestResources: []metav1.Object{builder1.Get(), builder2.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -578,7 +574,7 @@ func testACNPPriorityOverrideDefaultDeny(t *testing.T) {
 	testCase := []*TestCase{
 		{"ACNP PriorityOverride Default Deny", testStep},
 	}
-	executeTests(t, testCase)
+	executeTestsWithData(t, testCase, data)
 }
 
 // testACNPAllowNoDefaultIsolation tests that no default isolation rules are created for Policies.
@@ -1645,7 +1641,7 @@ func testANNPNestedGroupCreateAndUpdate(t *testing.T, data *TestData) {
 
 // testBaselineNamespaceIsolation tests that an ACNP in the baseline Tier is able to enforce default namespace isolation,
 // which can be later overridden by developer K8s NetworkPolicies.
-func testBaselineNamespaceIsolation(t *testing.T) {
+func testBaselineNamespaceIsolation(t *testing.T, data *TestData) {
 	builder := &ClusterNetworkPolicySpecBuilder{}
 	nsExpOtherThanX := metav1.LabelSelectorRequirement{
 		Key:      "ns",
@@ -1664,6 +1660,13 @@ func testBaselineNamespaceIsolation(t *testing.T) {
 	reachability.ExpectNamespaceIngressFromNamespace(getNS("x"), getNS("y"), Dropped)
 	reachability.ExpectNamespaceIngressFromNamespace(getNS("x"), getNS("z"), Dropped)
 
+	evaluation := NewNPEvaluation(allPods).
+		Expect(getPod("y", "a"), getPod("x", "a"), builder.Name, NPEvalDrop).
+		Expect(getPod("y", "b"), getPod("x", "a"), builder.Name, NPEvalDrop).
+		Expect(getPod("z", "a"), getPod("x", "a"), builder.Name, NPEvalDrop).
+		ExpectNone(getPod("x", "b"), getPod("x", "a")).
+		ExpectNone(getPod("z", "b"), getPod("y", "b"))
+
 	// create a K8s NetworkPolicy for the x/a Pod to allow ingress traffic from the y/a Pod.
 	// It should open up ingress from y/a and additionally deny ingress from x/b and x/c based on
 	// Kubernetes NetworkPolicy default isolation model, since it's evaluated before the baseline tier.
@@ -1682,10 +1685,19 @@ func testBaselineNamespaceIsolation(t *testing.T) {
 	reachabilityUpdated.ExpectEgressToNamespace(getPod("y", "b"), getNS("x"), Dropped)
 	reachabilityUpdated.ExpectEgressToNamespace(getPod("y", "c"), getNS("x"), Dropped)
 	reachabilityUpdated.ExpectNamespaceIngressFromNamespace(getNS("x"), getNS("z"), Dropped)
+
+	evaluationUpdated := NewNPEvaluation(allPods).
+		Expect(getPod("y", "a"), getPod("x", "a"), k8sNPBuilder.Name, NPEvalAllow).
+		Expect(getPod("y", "b"), getPod("x", "a"), k8sNPBuilder.Name, NPEvalIsolate).
+		Expect(getPod("z", "a"), getPod("x", "a"), k8sNPBuilder.Name, NPEvalIsolate).
+		Expect(getPod("x", "b"), getPod("x", "a"), k8sNPBuilder.Name, NPEvalIsolate).
+		ExpectNone(getPod("z", "b"), getPod("y", "b"))
+
 	testStep := []*TestStep{
 		{
 			Name:          "Baseline ACNP",
 			Reachability:  reachability,
+			NPEvaluation:  evaluation,
 			TestResources: []metav1.Object{builder.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -1693,6 +1705,7 @@ func testBaselineNamespaceIsolation(t *testing.T) {
 		{
 			Name:          "Baseline ACNP with KNP",
 			Reachability:  reachabilityUpdated,
+			NPEvaluation:  evaluationUpdated,
 			TestResources: []metav1.Object{builder.Get(), k8sNPBuilder.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -1701,7 +1714,7 @@ func testBaselineNamespaceIsolation(t *testing.T) {
 	testCase := []*TestCase{
 		{"ACNP baseline tier namespace isolation", testStep},
 	}
-	executeTests(t, testCase)
+	executeTestsWithData(t, testCase, data)
 	// Cleanup the K8s NetworkPolicy created for this test.
 	failOnError(k8sUtils.CleanNetworkPolicies(map[string]TestNamespaceMeta{"x": {Name: getNS("x")}}), t)
 	time.Sleep(networkPolicyDelay)
@@ -1709,7 +1722,7 @@ func testBaselineNamespaceIsolation(t *testing.T) {
 
 // testACNPPriorityOverride tests priority overriding in three ACNPs. Those three ACNPs are applied in a specific order to
 // test priority reassignment, and each controls a smaller set of traffic patterns as priority increases.
-func testACNPPriorityOverride(t *testing.T) {
+func testACNPPriorityOverride(t *testing.T, data *TestData) {
 	builder1 := &ClusterNetworkPolicySpecBuilder{}
 	builder1 = builder1.SetName("acnp-priority1").
 		SetPriority(1.001).
@@ -1751,10 +1764,22 @@ func testACNPPriorityOverride(t *testing.T) {
 	reachabilityAllACNPs.Expect(getPod("z", "c"), getPod("x", "b"), Dropped)
 	reachabilityAllACNPs.Expect(getPod("z", "c"), getPod("x", "c"), Dropped)
 
+	evaluationTwoACNPs := NewNPEvaluation(allPods).
+		ExpectNone(getPod("y", "a"), getPod("x", "a")).
+		Expect(getPod("z", "b"), getPod("x", "a"), builder2.Name, NPEvalAllow).
+		Expect(getPod("z", "b"), getPod("x", "b"), builder3.Name, NPEvalDrop)
+
+	evaluationAllACNPs := NewNPEvaluation(allPods).
+		ExpectNone(getPod("y", "a"), getPod("x", "a")).
+		Expect(getPod("z", "a"), getPod("x", "a"), builder2.Name, NPEvalAllow).
+		Expect(getPod("z", "a"), getPod("x", "b"), builder3.Name, NPEvalDrop).
+		Expect(getPod("z", "b"), getPod("x", "a"), builder1.Name, NPEvalDrop)
+
 	testStepTwoACNP := []*TestStep{
 		{
 			Name:          "Two Policies with different priorities",
 			Reachability:  reachabilityTwoACNPs,
+			NPEvaluation:  evaluationTwoACNPs,
 			TestResources: []metav1.Object{builder3.Get(), builder2.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -1765,6 +1790,7 @@ func testACNPPriorityOverride(t *testing.T) {
 		{
 			Name:          "All three Policies",
 			Reachability:  reachabilityAllACNPs,
+			NPEvaluation:  evaluationAllACNPs,
 			TestResources: []metav1.Object{builder3.Get(), builder1.Get(), builder2.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -1774,12 +1800,12 @@ func testACNPPriorityOverride(t *testing.T) {
 		{"ACNP PriorityOverride Intermediate", testStepTwoACNP},
 		{"ACNP PriorityOverride All", testStepAll},
 	}
-	executeTests(t, testCase)
+	executeTestsWithData(t, testCase, data)
 }
 
 // testACNPTierOverride tests tier priority overriding in three ACNPs. Each ACNP controls a smaller set of traffic patterns
 // as tier priority increases.
-func testACNPTierOverride(t *testing.T) {
+func testACNPTierOverride(t *testing.T, data *TestData) {
 	builder1 := &ClusterNetworkPolicySpecBuilder{}
 	builder1 = builder1.SetName("acnp-tier-emergency").
 		SetTier("emergency").
@@ -1824,10 +1850,22 @@ func testACNPTierOverride(t *testing.T) {
 	reachabilityAllACNPs.Expect(getPod("z", "c"), getPod("x", "b"), Dropped)
 	reachabilityAllACNPs.Expect(getPod("z", "c"), getPod("x", "c"), Dropped)
 
+	evaluationTwoACNPs := NewNPEvaluation(allPods).
+		ExpectNone(getPod("y", "a"), getPod("x", "a")).
+		Expect(getPod("z", "b"), getPod("x", "a"), builder2.Name, NPEvalAllow).
+		Expect(getPod("z", "b"), getPod("x", "b"), builder3.Name, NPEvalDrop)
+
+	evaluationAllACNPs := NewNPEvaluation(allPods).
+		ExpectNone(getPod("y", "a"), getPod("x", "a")).
+		Expect(getPod("z", "a"), getPod("x", "a"), builder2.Name, NPEvalAllow).
+		Expect(getPod("z", "a"), getPod("x", "b"), builder3.Name, NPEvalDrop).
+		Expect(getPod("z", "b"), getPod("x", "a"), builder1.Name, NPEvalDrop)
+
 	testStepTwoACNP := []*TestStep{
 		{
 			Name:          "Two Policies in different tiers",
 			Reachability:  reachabilityTwoACNPs,
+			NPEvaluation:  evaluationTwoACNPs,
 			TestResources: []metav1.Object{builder3.Get(), builder2.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -1837,6 +1875,7 @@ func testACNPTierOverride(t *testing.T) {
 		{
 			Name:          "All three Policies in different tiers",
 			Reachability:  reachabilityAllACNPs,
+			NPEvaluation:  evaluationAllACNPs,
 			TestResources: []metav1.Object{builder3.Get(), builder1.Get(), builder2.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -1846,12 +1885,12 @@ func testACNPTierOverride(t *testing.T) {
 		{"ACNP TierOverride Intermediate", testStepTwoACNP},
 		{"ACNP TierOverride All", testStepAll},
 	}
-	executeTests(t, testCase)
+	executeTestsWithData(t, testCase, data)
 }
 
-// testACNPTierOverride tests tier priority overriding in three ACNPs with custom created tiers. Each ACNP controls a
+// testACNPTierOverride tests tier priority overriding in two ACNPs with custom created tiers. Each ACNP controls a
 // smaller set of traffic patterns as tier priority increases.
-func testACNPCustomTiers(t *testing.T) {
+func testACNPCustomTiers(t *testing.T, data *TestData) {
 	k8sUtils.DeleteTier("high-priority")
 	k8sUtils.DeleteTier("low-priority")
 	// Create two custom tiers with tier priority immediately next to each other.
@@ -1885,10 +1924,17 @@ func testACNPCustomTiers(t *testing.T) {
 	reachabilityTwoACNPs.Expect(getPod("z", "b"), getPod("x", "c"), Dropped)
 	reachabilityTwoACNPs.Expect(getPod("z", "c"), getPod("x", "b"), Dropped)
 	reachabilityTwoACNPs.Expect(getPod("z", "c"), getPod("x", "c"), Dropped)
+
+	evaluationTwoACNPs := NewNPEvaluation(allPods).
+		ExpectNone(getPod("y", "a"), getPod("x", "a")).
+		Expect(getPod("z", "b"), getPod("x", "a"), builder1.Name, NPEvalAllow).
+		Expect(getPod("z", "b"), getPod("x", "b"), builder2.Name, NPEvalDrop)
+
 	testStepTwoACNP := []*TestStep{
 		{
 			Name:          "Two Policies in different tiers",
 			Reachability:  reachabilityTwoACNPs,
+			NPEvaluation:  evaluationTwoACNPs,
 			TestResources: []metav1.Object{builder2.Get(), builder1.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -1897,7 +1943,7 @@ func testACNPCustomTiers(t *testing.T) {
 	testCase := []*TestCase{
 		{"ACNP Custom Tier priority", testStepTwoACNP},
 	}
-	executeTests(t, testCase)
+	executeTestsWithData(t, testCase, data)
 	// Cleanup customized tiers. ACNPs created in those tiers need to be deleted first.
 	failOnError(k8sUtils.CleanACNPs(), t)
 	failOnError(k8sUtils.DeleteTier("high-priority"), t)
@@ -1906,7 +1952,7 @@ func testACNPCustomTiers(t *testing.T) {
 
 // testACNPPriorityConflictingRule tests that if there are two Policies in the cluster with rules that conflicts with
 // each other, the ACNP with higher priority will prevail.
-func testACNPPriorityConflictingRule(t *testing.T) {
+func testACNPPriorityConflictingRule(t *testing.T, data *TestData) {
 	builder1 := &ClusterNetworkPolicySpecBuilder{}
 	builder1 = builder1.SetName("acnp-drop").
 		SetPriority(1).
@@ -1927,10 +1973,16 @@ func testACNPPriorityConflictingRule(t *testing.T) {
 	reachabilityBothACNP.ExpectEgressToNamespace(getPod("z", "a"), getNS("x"), Dropped)
 	reachabilityBothACNP.ExpectEgressToNamespace(getPod("z", "b"), getNS("x"), Dropped)
 	reachabilityBothACNP.ExpectEgressToNamespace(getPod("z", "c"), getNS("x"), Dropped)
+
+	evaluationBothACNPs := NewNPEvaluation(allPods).
+		ExpectNone(getPod("y", "a"), getPod("x", "a")).
+		Expect(getPod("z", "a"), getPod("x", "a"), builder1.Name, NPEvalDrop)
+
 	testStep := []*TestStep{
 		{
 			Name:          "Both ACNP",
 			Reachability:  reachabilityBothACNP,
+			NPEvaluation:  evaluationBothACNPs,
 			TestResources: []metav1.Object{builder1.Get(), builder2.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -1939,12 +1991,12 @@ func testACNPPriorityConflictingRule(t *testing.T) {
 	testCase := []*TestCase{
 		{"ACNP Priority Conflicting Rule", testStep},
 	}
-	executeTests(t, testCase)
+	executeTestsWithData(t, testCase, data)
 }
 
 // testACNPRulePriority tests that if there are two rules in the cluster that conflicts with each other, the rule with
 // higher precedence will prevail.
-func testACNPRulePriority(t *testing.T) {
+func testACNPRulePriority(t *testing.T, data *TestData) {
 	builder1 := &ClusterNetworkPolicySpecBuilder{}
 	// acnp-deny will apply to all pods in namespace x
 	builder1 = builder1.SetName("acnp-deny").
@@ -1972,10 +2024,17 @@ func testACNPRulePriority(t *testing.T) {
 	reachabilityBothACNP.ExpectIngressFromNamespace(getPod("y", "a"), getNS("x"), Dropped)
 	reachabilityBothACNP.ExpectIngressFromNamespace(getPod("y", "b"), getNS("x"), Dropped)
 	reachabilityBothACNP.ExpectIngressFromNamespace(getPod("y", "c"), getNS("x"), Dropped)
+
+	evaluationBothACNPs := NewNPEvaluation(allPods).
+		ExpectNone(getPod("x", "b"), getPod("x", "a")).
+		Expect(getPod("x", "a"), getPod("y", "a"), builder1.Name, NPEvalDrop).
+		Expect(getPod("x", "a"), getPod("z", "a"), builder2.Name, NPEvalAllow)
+
 	testStep := []*TestStep{
 		{
 			Name:          "Both ACNP",
 			Reachability:  reachabilityBothACNP,
+			NPEvaluation:  evaluationBothACNPs,
 			TestResources: []metav1.Object{builder2.Get(), builder1.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -1984,7 +2043,7 @@ func testACNPRulePriority(t *testing.T) {
 	testCase := []*TestCase{
 		{"ACNP Rule Priority", testStep},
 	}
-	executeTests(t, testCase)
+	executeTestsWithData(t, testCase, data)
 }
 
 // testACNPPortRange tests the port range in an ACNP can work.
@@ -2018,7 +2077,7 @@ func testACNPPortRange(t *testing.T) {
 }
 
 // testACNPRejectEgress tests that an ACNP is able to reject egress traffic from pods labelled A to namespace Z.
-func testACNPRejectEgress(t *testing.T) {
+func testACNPRejectEgress(t *testing.T, data *TestData) {
 	builder := &ClusterNetworkPolicySpecBuilder{}
 	builder = builder.SetName("acnp-reject-a-to-z-egress").
 		SetPriority(1.0).
@@ -2031,10 +2090,16 @@ func testACNPRejectEgress(t *testing.T) {
 	reachability.ExpectEgressToNamespace(getPod("y", "a"), getNS("z"), Rejected)
 	reachability.Expect(getPod("z", "a"), getPod("z", "b"), Rejected)
 	reachability.Expect(getPod("z", "a"), getPod("z", "c"), Rejected)
+
+	evaluation := NewNPEvaluation(allPods).
+		ExpectNone(getPod("x", "b"), getPod("z", "a")).
+		Expect(getPod("x", "a"), getPod("z", "a"), builder.Name, NPEvalReject)
+
 	testStep := []*TestStep{
 		{
 			Name:          "Port 80",
 			Reachability:  reachability,
+			NPEvaluation:  evaluation,
 			TestResources: []metav1.Object{builder.Get()},
 			Ports:         []int32{80},
 			Protocol:      ProtocolTCP,
@@ -2043,7 +2108,7 @@ func testACNPRejectEgress(t *testing.T) {
 	testCase := []*TestCase{
 		{"ACNP Reject egress From All Pod:a to NS:z", testStep},
 	}
-	executeTests(t, testCase)
+	executeTestsWithData(t, testCase, data)
 }
 
 // testACNPRejectIngress tests that an ACNP is able to reject ingress traffic from pods labelled A to namespace Z.
@@ -2343,7 +2408,7 @@ func testANNPBasic(t *testing.T) {
 // update on the Antrea NetworkPolicy allows traffic from X/B to Y/A on port 80.
 func testANNPUpdate(t *testing.T, data *TestData) {
 	builder := &AntreaNetworkPolicySpecBuilder{}
-	builder = builder.SetName(getNS("y"), "np-before-update").
+	builder = builder.SetName(getNS("y"), "np-update").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANNPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}})
 	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": getNS("x")}, nil,
@@ -2351,6 +2416,9 @@ func testANNPUpdate(t *testing.T, data *TestData) {
 
 	reachability := NewReachability(allPods, Connected)
 	reachability.Expect(getPod("x", "b"), getPod("y", "a"), Dropped)
+	evaluation := NewNPEvaluation(allPods).
+		ExpectNone(getPod("x", "a"), getPod("y", "a")).
+		Expect(getPod("x", "b"), getPod("y", "a"), builder.Name, NPEvalDrop)
 	annp, err := k8sUtils.CreateOrUpdateANNP(builder.Get())
 	failOnError(err, t)
 	failOnError(data.waitForANNPRealized(t, annp.Namespace, annp.Name, policyRealizedTimeout), t)
@@ -2360,14 +2428,20 @@ func testANNPUpdate(t *testing.T, data *TestData) {
 		t.Errorf("Failure -- %d wrong results", wrong)
 		reachability.PrintSummary(true, true, true)
 	}
+	if testOptions.npEvaluation {
+		doEvaluation(t, data, evaluation)
+	}
 
 	updatedBuilder := &AntreaNetworkPolicySpecBuilder{}
-	updatedBuilder = updatedBuilder.SetName(getNS("y"), "np-before-update").
+	updatedBuilder = updatedBuilder.SetName(getNS("y"), "np-update").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANNPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}})
 	updatedBuilder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": getNS("x")}, nil,
 		nil, nil, nil, nil, crdv1beta1.RuleActionAllow, "", "")
 	updatedReachability := NewReachability(allPods, Connected)
+	updatedEvaluation := NewNPEvaluation(allPods).
+		ExpectNone(getPod("x", "a"), getPod("y", "a")).
+		Expect(getPod("x", "b"), getPod("y", "a"), builder.Name, NPEvalAllow)
 	annp, err = k8sUtils.CreateOrUpdateANNP(updatedBuilder.Get())
 	failOnError(err, t)
 	failOnError(data.waitForANNPRealized(t, annp.Namespace, annp.Name, policyRealizedTimeout), t)
@@ -2377,6 +2451,10 @@ func testANNPUpdate(t *testing.T, data *TestData) {
 		t.Errorf("Failure -- %d wrong results", wrong)
 		reachability.PrintSummary(true, true, true)
 	}
+	if testOptions.npEvaluation {
+		doEvaluation(t, data, updatedEvaluation)
+	}
+
 	failOnError(k8sUtils.DeleteANNP(annp.Namespace, annp.Name), t)
 }
 
@@ -2402,6 +2480,9 @@ func testANNPMultipleAppliedTo(t *testing.T, data *TestData, singleRule bool) {
 
 	reachability := NewReachability(allPods, Connected)
 	reachability.Expect(getPod("x", "b"), getPod("y", "a"), Dropped)
+	evaluation := NewNPEvaluation(allPods).
+		ExpectNone(getPod("x", "b"), getPod("y", "c")).
+		Expect(getPod("x", "b"), getPod("y", "a"), builder.Name, NPEvalDrop)
 
 	annp, err := k8sUtils.CreateOrUpdateANNP(builder.Get())
 	failOnError(err, t)
@@ -2412,6 +2493,9 @@ func testANNPMultipleAppliedTo(t *testing.T, data *TestData, singleRule bool) {
 		t.Errorf("Failure -- %d wrong results", wrong)
 		reachability.PrintSummary(true, true, true)
 	}
+	if testOptions.npEvaluation {
+		doEvaluation(t, data, evaluation)
+	}
 
 	t.Logf("Making the Policy apply to y/c by labeling it with the temporary label that matches the dummy AppliedTo")
 	podYC, err := k8sUtils.GetPodByLabel(getNS("y"), "c")
@@ -2424,6 +2508,9 @@ func testANNPMultipleAppliedTo(t *testing.T, data *TestData, singleRule bool) {
 	reachability = NewReachability(allPods, Connected)
 	reachability.Expect(getPod("x", "b"), getPod("y", "a"), Dropped)
 	reachability.Expect(getPod("x", "b"), getPod("y", "c"), Dropped)
+	updatedEvaluation := NewNPEvaluation(allPods).
+		Expect(getPod("x", "b"), getPod("y", "c"), builder.Name, NPEvalDrop).
+		Expect(getPod("x", "b"), getPod("y", "a"), builder.Name, NPEvalDrop)
 	time.Sleep(networkPolicyDelay)
 	k8sUtils.Validate(allPods, reachability, []int32{80}, ProtocolTCP)
 	_, wrong, _ = reachability.Summary()
@@ -2431,6 +2518,9 @@ func testANNPMultipleAppliedTo(t *testing.T, data *TestData, singleRule bool) {
 		t.Errorf("Failure -- %d wrong results", wrong)
 		reachability.PrintSummary(true, true, true)
 	}
+	if testOptions.npEvaluation {
+		doEvaluation(t, data, updatedEvaluation)
+	}
 
 	t.Logf("Making the Policy not apply to y/c by removing the temporary label")
 	delete(podYC.Labels, tempLabel)
@@ -2445,6 +2535,9 @@ func testANNPMultipleAppliedTo(t *testing.T, data *TestData, singleRule bool) {
 		t.Errorf("Failure -- %d wrong results", wrong)
 		reachability.PrintSummary(true, true, true)
 	}
+	if testOptions.npEvaluation {
+		doEvaluation(t, data, evaluation)
+	}
 
 	failOnError(k8sUtils.DeleteANNP(builder.Namespace, builder.Name), t)
 }
@@ -4127,9 +4220,9 @@ func executeTests(t *testing.T, testList []*TestCase) {
 
 func executeTestsWithData(t *testing.T, testList []*TestCase, data *TestData) {
 	for _, testCase := range testList {
-		log.Infof("running test case %s", testCase.Name)
+		log.Infof("Running test case %s", testCase.Name)
 		for _, step := range testCase.Steps {
-			log.Infof("running step %s of test case %s", step.Name, testCase.Name)
+			log.Infof("Running step %s of test case %s", step.Name, testCase.Name)
 			applyTestStepResources(t, step)
 			if step.CustomSetup != nil {
 				step.CustomSetup()
@@ -4156,6 +4249,13 @@ func executeTestsWithData(t *testing.T, testList []*TestCase, data *TestData) {
 			if step.CustomTeardown != nil {
 				step.CustomTeardown()
 			}
+			if step.NPEvaluation != nil {
+				if data == nil {
+					t.Errorf("test case %s with networkpolicyevaluation must set test data", testCase.Name)
+				} else if testOptions.npEvaluation {
+					doEvaluation(t, data, step.NPEvaluation)
+				}
+			}
 		}
 		log.Debug("Cleaning-up all policies and groups created by this Testcase")
 		cleanupTestCaseResources(t, testCase)
@@ -4180,6 +4280,43 @@ func doProbe(t *testing.T, data *TestData, p *CustomProbe, protocol AntreaPolicy
 	}
 }
 
+func doEvaluation(t *testing.T, data *TestData, npEvaluation *NPEvaluation) {
+	log.Infof("Running networkpolicyevaluation tests")
+	antreaController, err := data.getAntreaController()
+	require.NoError(t, err, "Error occurred when trying to get the Antrea Controller")
+	antctlArgs := []string{"antctl", "query", "networkpolicyevaluation"}
+	getRealizedPod := func(label Pod) string {
+		realizedPod, err := k8sUtils.GetPodByLabel(label.Namespace(), label.PodName())
+		require.NoError(t, err, "Failed to get Pod %s", label.String())
+		return realizedPod.Namespace + "/" + realizedPod.Name
+	}
+	processResponse := func(stdout string) map[string]string {
+		// the first row is the header row, the second row contains the actual value
+		rows := strings.Split(strings.TrimSpace(stdout), "\n")
+		require.Len(t, rows, 2)
+		headerFields := strings.Fields(rows[0])
+		valueFields := strings.Fields(rows[1])
+		require.Equal(t, len(headerFields), len(valueFields))
+		antctlData := make(map[string]string)
+		for idx, name := range headerFields {
+			antctlData[name] = valueFields[idx]
+		}
+		return antctlData
+	}
+
+	for _, eval := range npEvaluation.Assertions {
+		cmds := append(antctlArgs, "-S", getRealizedPod(eval.Source), "-D", getRealizedPod(eval.Destination))
+		log.Tracef("Running command: %s", strings.Join(cmds, " "))
+		stdout, stderr, err := data.RunCommandFromPod(antreaNamespace, antreaController.Name, "antrea-controller", cmds)
+		if !assert.NoError(t, err, "Failure -- Command was not successful:\n%s", antctlOutput(stdout, stderr)) {
+			continue
+		}
+		gotEval := processResponse(stdout)
+		assert.Equal(t, eval.NPName, gotEval["NAME"], "Failure -- Policy name mismatch in NetworkPolicyEvaluation response")
+		assert.EqualValues(t, eval.Action, gotEval["ACTION"], "Failure -- Policy rule action mismatch in NetworkPolicyEvaluation response")
+	}
+}
+
 // applyTestStepResources creates in the resources of a testStep in specified order.
 // The ordering can be used to test different scenarios, like creating an ACNP before
 // creating its referred ClusterGroup, and vice versa.
@@ -4379,7 +4516,7 @@ func TestAntreaPolicy(t *testing.T) {
 		applyDefaultDenyToAllNamespaces(k8sUtils, namespaces)
 		t.Run("Case=ACNPAllowXBtoA", func(t *testing.T) { testACNPAllowXBtoA(t) })
 		t.Run("Case=ACNPAllowXBtoYA", func(t *testing.T) { testACNPAllowXBtoYA(t) })
-		t.Run("Case=ACNPPriorityOverrideDefaultDeny", func(t *testing.T) { testACNPPriorityOverrideDefaultDeny(t) })
+		t.Run("Case=ACNPPriorityOverrideDefaultDeny", func(t *testing.T) { testACNPPriorityOverrideDefaultDeny(t, data) }) // Includes evaluation.
 		cleanupDefaultDenyNPs(k8sUtils, namespaces)
 	})
 
@@ -4394,23 +4531,23 @@ func TestAntreaPolicy(t *testing.T) {
 		t.Run("Case=ACNPDropIngressInNamespace", func(t *testing.T) { testACNPDropIngressInSelectedNamespace(t) })
 		t.Run("Case=ACNPPortRange", func(t *testing.T) { testACNPPortRange(t) })
 		t.Run("Case=ACNPSourcePort", func(t *testing.T) { testACNPSourcePort(t) })
-		t.Run("Case=ACNPRejectEgress", func(t *testing.T) { testACNPRejectEgress(t) })
+		t.Run("Case=ACNPRejectEgress", func(t *testing.T) { testACNPRejectEgress(t, data) }) // Includes evaluation.
 		t.Run("Case=ACNPRejectIngress", func(t *testing.T) { testACNPRejectIngress(t, ProtocolTCP) })
 		t.Run("Case=ACNPRejectIngressUDP", func(t *testing.T) { testACNPRejectIngress(t, ProtocolUDP) })
 		t.Run("Case=RejectServiceTraffic", func(t *testing.T) { testRejectServiceTraffic(t, data, data.testNamespace, data.testNamespace) })
 		t.Run("Case=RejectNoInfiniteLoop", func(t *testing.T) { testRejectNoInfiniteLoop(t, data, data.testNamespace, data.testNamespace) })
 		t.Run("Case=ACNPNoEffectOnOtherProtocols", func(t *testing.T) { testACNPNoEffectOnOtherProtocols(t) })
-		t.Run("Case=ACNPBaselinePolicy", func(t *testing.T) { testBaselineNamespaceIsolation(t) })
-		t.Run("Case=ACNPPriorityOverride", func(t *testing.T) { testACNPPriorityOverride(t) })
-		t.Run("Case=ACNPTierOverride", func(t *testing.T) { testACNPTierOverride(t) })
-		t.Run("Case=ACNPCustomTiers", func(t *testing.T) { testACNPCustomTiers(t) })
-		t.Run("Case=ACNPPriorityConflictingRule", func(t *testing.T) { testACNPPriorityConflictingRule(t) })
-		t.Run("Case=ACNPRulePriority", func(t *testing.T) { testACNPRulePriority(t) })
+		t.Run("Case=ACNPBaselinePolicy", func(t *testing.T) { testBaselineNamespaceIsolation(t, data) })           // Includes evaluation.
+		t.Run("Case=ACNPPriorityOverride", func(t *testing.T) { testACNPPriorityOverride(t, data) })               // Includes evaluation.
+		t.Run("Case=ACNPTierOverride", func(t *testing.T) { testACNPTierOverride(t, data) })                       // Includes evaluation.
+		t.Run("Case=ACNPCustomTiers", func(t *testing.T) { testACNPCustomTiers(t, data) })                         // Includes evaluation.
+		t.Run("Case=ACNPPriorityConflictingRule", func(t *testing.T) { testACNPPriorityConflictingRule(t, data) }) // Includes evaluation.
+		t.Run("Case=ACNPRulePriority", func(t *testing.T) { testACNPRulePriority(t, data) })                       // Includes evaluation.
 		t.Run("Case=ANNPPortRange", func(t *testing.T) { testANNPPortRange(t) })
 		t.Run("Case=ANNPBasic", func(t *testing.T) { testANNPBasic(t) })
-		t.Run("Case=ANNPUpdate", func(t *testing.T) { testANNPUpdate(t, data) })
-		t.Run("Case=testANNPMultipleAppliedToSingleRule", func(t *testing.T) { testANNPMultipleAppliedTo(t, data, true) })
-		t.Run("Case=testANNPMultipleAppliedToMultipleRules", func(t *testing.T) { testANNPMultipleAppliedTo(t, data, false) })
+		t.Run("Case=ANNPUpdate", func(t *testing.T) { testANNPUpdate(t, data) })                                               // Includes evaluation.
+		t.Run("Case=testANNPMultipleAppliedToSingleRule", func(t *testing.T) { testANNPMultipleAppliedTo(t, data, true) })     // Includes evaluation.
+		t.Run("Case=testANNPMultipleAppliedToMultipleRules", func(t *testing.T) { testANNPMultipleAppliedTo(t, data, false) }) // Includes evaluation.
 		t.Run("Case=AppliedToPerRule", func(t *testing.T) { testAppliedToPerRule(t) })
 		t.Run("Case=ACNPNamespaceIsolation", func(t *testing.T) { testACNPNamespaceIsolation(t) })
 		t.Run("Case=ACNPStrictNamespaceIsolation", func(t *testing.T) { testACNPStrictNamespacesIsolation(t) })
diff --git a/test/e2e/fixtures.go b/test/e2e/fixtures.go
index 71fa87275..f29615371 100644
--- a/test/e2e/fixtures.go
+++ b/test/e2e/fixtures.go
@@ -159,6 +159,16 @@ func skipIfNoVMs(tb testing.TB) {
 	}
 }
 
+func skipIfMulticastEnabled(tb testing.TB, data *TestData) {
+	agentConf, err := data.GetAntreaAgentConf()
+	if err != nil {
+		tb.Fatalf("Error getting option multicast.enable value")
+	}
+	if agentConf.Multicast.Enable {
+		tb.Skipf("Skipping test because option multicast.enable is true")
+	}
+}
+
 func skipIfFeatureDisabled(tb testing.TB, feature featuregate.Feature, checkAgent bool, checkController bool) {
 	if checkAgent {
 		if featureGate, err := GetAgentFeatures(); err != nil {
@@ -675,6 +685,7 @@ func testMain(m *testing.M) int {
 	flag.BoolVar(&testOptions.enableCoverage, "coverage", false, "Run tests and measure coverage")
 	flag.BoolVar(&testOptions.enableAntreaIPAM, "antrea-ipam", false, "Run tests with AntreaIPAM")
 	flag.BoolVar(&testOptions.flowVisibility, "flow-visibility", false, "Run flow visibility tests")
+	flag.BoolVar(&testOptions.npEvaluation, "networkpolicy-evaluation", false, "Run NetworkPolicy evaluation tests")
 	flag.BoolVar(&testOptions.deployAntrea, "deploy-antrea", true, "Deploy Antrea before running tests")
 	flag.StringVar(&testOptions.coverageDir, "coverage-dir", "", "Directory for coverage data files")
 	flag.StringVar(&testOptions.skipCases, "skip-cases", "", "Key words to skip cases")
diff --git a/test/e2e/framework.go b/test/e2e/framework.go
index 140555f00..c72a99078 100644
--- a/test/e2e/framework.go
+++ b/test/e2e/framework.go
@@ -209,6 +209,7 @@ type TestOptions struct {
 	enableCoverage      bool
 	enableAntreaIPAM    bool
 	flowVisibility      bool
+	npEvaluation        bool
 	coverageDir         string
 	skipCases           string
 	linuxVMs            string
diff --git a/test/e2e/ipsec_test.go b/test/e2e/ipsec_test.go
index b972f18ec..931b8e49f 100644
--- a/test/e2e/ipsec_test.go
+++ b/test/e2e/ipsec_test.go
@@ -42,6 +42,7 @@ func TestIPSec(t *testing.T) {
 		t.Fatalf("Error when setting up test: %v", err)
 	}
 	defer teardownTest(t, data)
+	skipIfMulticastEnabled(t, data)
 
 	t.Logf("Redeploy Antrea with IPsec tunnel enabled")
 	data.redeployAntrea(t, deployAntreaIPsec)
diff --git a/test/e2e/k8s_util.go b/test/e2e/k8s_util.go
index a614f83b1..d665cce4c 100644
--- a/test/e2e/k8s_util.go
+++ b/test/e2e/k8s_util.go
@@ -64,6 +64,7 @@ type TestCase struct {
 type TestStep struct {
 	Name           string
 	Reachability   *Reachability
+	NPEvaluation   *NPEvaluation
 	TestResources  []metav1.Object
 	Ports          []int32
 	Protocol       utils.AntreaPolicyProtocol
diff --git a/test/e2e/nodeportlocal_test.go b/test/e2e/nodeportlocal_test.go
index 6aa801c38..21bb85d5a 100644
--- a/test/e2e/nodeportlocal_test.go
+++ b/test/e2e/nodeportlocal_test.go
@@ -95,7 +95,7 @@ func TestNodePortLocal(t *testing.T) {
 	t.Run("testNPLChangePortRangeAgentRestart", func(t *testing.T) { testNPLChangePortRangeAgentRestart(t, data) })
 }
 
-func getNPLAnnotations(t *testing.T, data *TestData, r *require.Assertions, testPodName string, conditionFn func(types.NPLAnnotation) bool) ([]types.NPLAnnotation, string) {
+func getNPLAnnotations(t *testing.T, data *TestData, r *require.Assertions, testPodName string, conditionFn func([]types.NPLAnnotation) bool) ([]types.NPLAnnotation, string) {
 	var nplAnnotations []types.NPLAnnotation
 	var testPodIP *PodIPs
 
@@ -135,12 +135,8 @@ func getNPLAnnotations(t *testing.T, data *TestData, r *require.Assertions, test
 				return false, nil
 			}
 			json.Unmarshal([]byte(nplAnn), &nplAnnotations)
-			if conditionFn != nil {
-				for _, annotation := range nplAnnotations {
-					if !conditionFn(annotation) {
-						return false, nil
-					}
-				}
+			if conditionFn != nil && !conditionFn(nplAnnotations) {
+				return false, nil
 			}
 			return found, nil
 		})
@@ -495,13 +491,11 @@ func NPLTestPodAddMultiProtocol(t *testing.T, data *TestData) {
 		Add(nil, 8080, "tcp").Add(nil, 8080, "udp")
 
 	// Creating a Pod using agnhost image to support multiple protocols, instead of nginx.
-	cmd := []string{"/bin/bash", "-c"}
-	args := []string{
-		fmt.Sprintf("/agnhost serve-hostname --udp --http=false --port %d & /agnhost serve-hostname --tcp --http=false --port %d", 8080, 8080),
-	}
+
+	args := []string{"serve-hostname", "--tcp", "--udp", "--http=false", "--port=8080"}
 	port := corev1.ContainerPort{ContainerPort: 8080}
 	containerName := fmt.Sprintf("c%v", 8080)
-	err := NewPodBuilder(testPodName, data.testNamespace, agnhostImage).OnNode(serverNode).WithContainerName(containerName).WithCommand(cmd).WithArgs(args).WithPorts([]corev1.ContainerPort{port}).WithLabels(selector).Create(testData)
+	err := NewPodBuilder(testPodName, data.testNamespace, agnhostImage).OnNode(serverNode).WithContainerName(containerName).WithArgs(args).WithPorts([]corev1.ContainerPort{port}).WithLabels(selector).Create(testData)
 	r.NoError(err, "Error creating test Pod: %v", err)
 
 	nplAnnotations, testPodIP := getNPLAnnotations(t, testData, r, testPodName, nil)
@@ -517,14 +511,26 @@ func NPLTestPodAddMultiProtocol(t *testing.T, data *TestData) {
 	r.NoError(err, "Error when getting Antrea Agent Pod on Node '%s'", serverNode)
 
 	checkNPLRules(t, testData, r, nplAnnotations, antreaPod, testPodIP, serverNode, true)
+	expectedAnnotations.Check(t, nplAnnotations)
+	checkTrafficForNPL(testData, r, nplAnnotations, clientName)
+
+	// We now delete one of the Services, and we expect the corresponding NPL rule to be deleted.
+	testData.DeleteService(data.testNamespace, "agnhost2")
+	expectedAnnotations = newExpectedNPLAnnotations(defaultStartPort, defaultEndPort).
+		Add(nil, 8080, "tcp")
+	// Wait until we have only one NPL rule annotation.
+	conditionFn := func(annotations []types.NPLAnnotation) bool {
+		return len(annotations) == 1
+	}
+	nplAnnotations, testPodIP = getNPLAnnotations(t, testData, r, testPodName, conditionFn)
 
+	checkNPLRules(t, testData, r, nplAnnotations, antreaPod, testPodIP, serverNode, true)
 	expectedAnnotations.Check(t, nplAnnotations)
 	checkTrafficForNPL(testData, r, nplAnnotations, clientName)
 
 	testData.DeletePod(data.testNamespace, clientName)
 	testData.DeletePod(data.testNamespace, testPodName)
 	testData.DeleteService(data.testNamespace, "agnhost1")
-	testData.DeleteService(data.testNamespace, "agnhost2")
 	checkNPLRules(t, testData, r, nplAnnotations, antreaPod, testPodIP, serverNode, false)
 }
 
@@ -697,8 +703,11 @@ func testNPLChangePortRangeAgentRestart(t *testing.T, data *TestData) {
 	}
 
 	for _, testPodName := range testPods {
-		conditionFn := func(ann types.NPLAnnotation) bool {
-			return ann.NodePort >= updatedStartPort
+		conditionFn := func(annotations []types.NPLAnnotation) bool {
+			for idx := range annotations {
+				return annotations[idx].NodePort >= updatedStartPort
+			}
+			return true
 		}
 		nplAnnotations, testPodIP := getNPLAnnotations(t, data, r, testPodName, conditionFn)
 
diff --git a/test/e2e/reachability.go b/test/e2e/reachability.go
index 48166588a..27bea99c8 100644
--- a/test/e2e/reachability.go
+++ b/test/e2e/reachability.go
@@ -381,3 +381,51 @@ func (r *Reachability) PrintSummary(printExpected bool, printObserved bool, prin
 		fmt.Printf("comparison:\n\n%s\n\n\n", comparison.PrettyPrint(""))
 	}
 }
+
+type NPEvalActionType string
+
+const (
+	NPEvalNone    NPEvalActionType = "<NONE>"
+	NPEvalAllow   NPEvalActionType = "Allow"
+	NPEvalDrop    NPEvalActionType = "Drop"
+	NPEvalIsolate NPEvalActionType = "Isolate"
+	NPEvalReject  NPEvalActionType = "Reject"
+)
+
+type NPEvaluationSpec struct {
+	Source      Pod
+	Destination Pod
+	NPName      string
+	Action      NPEvalActionType
+}
+
+type NPEvaluation struct {
+	itemSet    map[string]bool
+	Assertions []*NPEvaluationSpec
+}
+
+func NewNPEvaluation(pods []Pod) *NPEvaluation {
+	itemSet := map[string]bool{}
+	for _, pod := range pods {
+		itemSet[pod.String()] = true
+	}
+	return &NPEvaluation{
+		itemSet:    itemSet,
+		Assertions: []*NPEvaluationSpec{},
+	}
+}
+
+func (e *NPEvaluation) Expect(from, to Pod, npName string, action NPEvalActionType) *NPEvaluation {
+	if _, ok := e.itemSet[from.String()]; !ok {
+		panic(fmt.Errorf("key %s not allowed", from))
+	}
+	if _, ok := e.itemSet[to.String()]; !ok {
+		panic(fmt.Errorf("key %s not allowed", to))
+	}
+	e.Assertions = append(e.Assertions, &NPEvaluationSpec{from, to, npName, action})
+	return e
+}
+
+func (e *NPEvaluation) ExpectNone(from Pod, to Pod) *NPEvaluation {
+	return e.Expect(from, to, string(NPEvalNone), NPEvalNone)
+}
diff --git a/test/e2e/traceflow_test.go b/test/e2e/traceflow_test.go
index f8df54783..dff030cc0 100644
--- a/test/e2e/traceflow_test.go
+++ b/test/e2e/traceflow_test.go
@@ -115,12 +115,20 @@ func testTraceflowIntraNodeANNP(t *testing.T, data *TestData) {
 		nodeIdx = clusterInfo.windowsNodes[0]
 	}
 	node1 := nodeName(nodeIdx)
-	node1Pods, _, node1CleanupFn := createTestAgnhostPods(t, data, 3, data.testNamespace, node1)
+	node1Pods, node1PodIPs, node1CleanupFn := createTestAgnhostPods(t, data, 3, data.testNamespace, node1)
 	defer node1CleanupFn()
 	// Give a little time for Windows containerd Nodes to setup OVS.
 	// Containerd configures port asynchronously, which could cause execution time of installing flow longer than docker.
 	time.Sleep(time.Second * 1)
 
+	var pod0IPv4Str, pod0IPv6Str string
+	if node1PodIPs[0].IPv4 != nil {
+		pod0IPv4Str = node1PodIPs[0].IPv4.String()
+	}
+	if node1PodIPs[0].IPv6 != nil {
+		pod0IPv6Str = node1PodIPs[0].IPv6.String()
+	}
+
 	var denyIngress *v1beta1.NetworkPolicy
 	denyIngressName := "test-annp-deny-ingress"
 	if denyIngress, err = data.createANNPDenyIngress("antrea-e2e", node1Pods[1], denyIngressName, false); err != nil {
@@ -187,6 +195,7 @@ func testTraceflowIntraNodeANNP(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv4Str,
 						},
 						{
 							Component:         v1beta1.ComponentNetworkPolicy,
@@ -237,6 +246,7 @@ func testTraceflowIntraNodeANNP(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv4Str,
 						},
 						{
 							Component:         v1beta1.ComponentNetworkPolicy,
@@ -287,6 +297,7 @@ func testTraceflowIntraNodeANNP(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv6Str,
 						},
 						{
 							Component:         v1beta1.ComponentNetworkPolicy,
@@ -325,10 +336,13 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 	// Give a little time for Windows containerd Nodes to setup OVS.
 	// Containerd configures port asynchronously, which could cause execution time of installing flow longer than docker.
 	time.Sleep(time.Second * 1)
-	var pod0IPv4Str, pod1IPv4Str, dstPodIPv4Str, dstPodIPv6Str string
+	var pod0IPv4Str, pod0IPv6Str, pod1IPv4Str, dstPodIPv4Str, dstPodIPv6Str string
 	if node1IPs[0].IPv4 != nil {
 		pod0IPv4Str = node1IPs[0].IPv4.String()
 	}
+	if node1IPs[0].IPv6 != nil {
+		pod0IPv6Str = node1IPs[0].IPv6.String()
+	}
 	if node1IPs[1].IPv4 != nil {
 		pod1IPv4Str = node1IPs[1].IPv4.String()
 	}
@@ -423,6 +437,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -476,6 +491,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -528,6 +544,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -574,6 +591,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -638,6 +656,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -753,6 +772,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -806,6 +826,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -858,6 +879,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -904,6 +926,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -978,6 +1001,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1027,6 +1051,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1076,6 +1101,7 @@ func testTraceflowIntraNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  pod0IPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1118,18 +1144,24 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 	node1 := nodeName(nodeIdx0)
 	node2 := nodeName(nodeIdx1)
 
-	node1Pods, _, node1CleanupFn := createTestAgnhostPods(t, data, 1, data.testNamespace, node1)
+	node1Pods, node1IPs, node1CleanupFn := createTestAgnhostPods(t, data, 1, data.testNamespace, node1)
 	node2Pods, node2IPs, node2CleanupFn := createTestAgnhostPods(t, data, 3, data.testNamespace, node2)
 	gatewayIPv4, gatewayIPv6 := nodeGatewayIPs(1)
 	defer node1CleanupFn()
 	defer node2CleanupFn()
-	var dstPodIPv4Str, dstPodIPv6Str string
+	var dstPodIPv4Str, dstPodIPv6Str, srcPodIPv4Str, srcPodIPv6Str string
 	if node2IPs[0].IPv4 != nil {
 		dstPodIPv4Str = node2IPs[0].IPv4.String()
 	}
 	if node2IPs[0].IPv6 != nil {
 		dstPodIPv6Str = node2IPs[0].IPv6.String()
 	}
+	if node1IPs[0].IPv4 != nil {
+		srcPodIPv4Str = node1IPs[0].IPv4.String()
+	}
+	if node1IPs[0].IPv6 != nil {
+		srcPodIPv6Str = node1IPs[0].IPv6.String()
+	}
 
 	// Create Service backend Pod. The "hairpin" testcases require the Service to have a single backend Pod,
 	// and no more, in order to be deterministic.
@@ -1241,6 +1273,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1307,6 +1340,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1368,6 +1402,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1439,6 +1474,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv4Str,
 						},
 						{
 							Component:       v1beta1.ComponentLB,
@@ -1516,6 +1552,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  agnhostIPv4Str,
 						},
 						{
 							Component:       v1beta1.ComponentLB,
@@ -1567,6 +1604,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv4Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1635,6 +1673,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1704,6 +1743,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1764,6 +1804,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -1832,6 +1873,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv6Str,
 						},
 						{
 							Component:       v1beta1.ComponentLB,
@@ -1906,6 +1948,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  agnhostIPv6Str,
 						},
 						{
 							Component:       v1beta1.ComponentLB,
@@ -1962,6 +2005,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 						{
 							Component: v1beta1.ComponentSpoofGuard,
 							Action:    v1beta1.ActionForwarded,
+							SrcPodIP:  srcPodIPv6Str,
 						},
 						{
 							Component:     v1beta1.ComponentNetworkPolicy,
@@ -2017,12 +2061,17 @@ func testTraceflowExternalIP(t *testing.T, data *TestData) {
 	}
 	node := nodeName(nodeIdx)
 	nodeIP := nodeIP(nodeIdx)
-	podNames, _, cleanupFn := createTestAgnhostPods(t, data, 1, data.testNamespace, node)
+	podNames, podIPs, cleanupFn := createTestAgnhostPods(t, data, 1, data.testNamespace, node)
 	defer cleanupFn()
 	// Give a little time for Windows containerd Nodes to setup OVS.
 	// Containerd configures port asynchronously, which could cause execution time of installing flow longer than docker.
 	time.Sleep(time.Second * 1)
-
+	var srcPodIP string
+	if podIPs[0].IPv4 != nil {
+		srcPodIP = podIPs[0].IPv4.String()
+	} else {
+		srcPodIP = podIPs[0].IPv6.String()
+	}
 	testcase := testcase{
 		name:      "nodeIPDestination",
 		ipVersion: 4,
@@ -2053,6 +2102,7 @@ func testTraceflowExternalIP(t *testing.T, data *TestData) {
 					{
 						Component: v1beta1.ComponentSpoofGuard,
 						Action:    v1beta1.ActionForwarded,
+						SrcPodIP:  srcPodIP,
 					},
 					{
 						Component:     v1beta1.ComponentForwarding,
@@ -2072,8 +2122,14 @@ func testTraceflowEgress(t *testing.T, data *TestData) {
 	egressIP := nodeIP(0)
 	externalDstIP := "1.1.1.1"
 
-	localPodNames, _, localCleanupFn := createTestAgnhostPods(t, data, 1, data.testNamespace, egressNode)
+	localPodNames, localPodIPs, localCleanupFn := createTestAgnhostPods(t, data, 1, data.testNamespace, egressNode)
 	defer localCleanupFn()
+	var srcPodIP string
+	if localPodIPs[0].IPv4 != nil {
+		srcPodIP = localPodIPs[0].IPv4.String()
+	} else {
+		srcPodIP = localPodIPs[0].IPv6.String()
+	}
 
 	matchExpressions := []metav1.LabelSelectorRequirement{
 		{
@@ -2116,6 +2172,7 @@ func testTraceflowEgress(t *testing.T, data *TestData) {
 					{
 						Component: v1beta1.ComponentSpoofGuard,
 						Action:    v1beta1.ActionForwarded,
+						SrcPodIP:  srcPodIP,
 					},
 					{
 						Component:  v1beta1.ComponentEgress,
@@ -2140,8 +2197,13 @@ func testTraceflowEgress(t *testing.T, data *TestData) {
 
 	skipIfNumNodesLessThan(t, 2)
 	remoteNode := nodeName(1)
-	remotePodNames, _, remoteCleanupFn := createTestAgnhostPods(t, data, 1, data.testNamespace, remoteNode)
+	remotePodNames, remotePodIPs, remoteCleanupFn := createTestAgnhostPods(t, data, 1, data.testNamespace, remoteNode)
 	defer remoteCleanupFn()
+	if remotePodIPs[0].IPv4 != nil {
+		srcPodIP = remotePodIPs[0].IPv4.String()
+	} else {
+		srcPodIP = remotePodIPs[0].IPv6.String()
+	}
 
 	toUpdate := egress.DeepCopy()
 	err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
@@ -2188,6 +2250,7 @@ func testTraceflowEgress(t *testing.T, data *TestData) {
 					{
 						Component: v1beta1.ComponentSpoofGuard,
 						Action:    v1beta1.ActionForwarded,
+						SrcPodIP:  srcPodIP,
 					},
 					{
 						Component:  v1beta1.ComponentEgress,
@@ -2344,6 +2407,7 @@ func compareObservations(expected v1beta1.NodeResult, actual v1beta1.NodeResult)
 		if exObs[i].Component != acObs[i].Component ||
 			exObs[i].ComponentInfo != acObs[i].ComponentInfo ||
 			exObs[i].Pod != acObs[i].Pod ||
+			exObs[i].SrcPodIP != acObs[i].SrcPodIP ||
 			exObs[i].TranslatedDstIP != acObs[i].TranslatedDstIP ||
 			exObs[i].EgressIP != acObs[i].EgressIP ||
 			exObs[i].Egress != acObs[i].Egress ||
diff --git a/test/e2e/wireguard_test.go b/test/e2e/wireguard_test.go
index 89c86b83d..5b27a97a7 100644
--- a/test/e2e/wireguard_test.go
+++ b/test/e2e/wireguard_test.go
@@ -40,17 +40,15 @@ func TestWireGuard(t *testing.T) {
 		t.Fatalf("Error when setting up test: %v", err)
 	}
 	defer teardownTest(t, data)
+	skipIfMulticastEnabled(t, data)
 	skipIfEncapModeIsNot(t, data, config.TrafficEncapModeEncap)
 	for _, node := range clusterInfo.nodes {
 		skipIfMissingKernelModule(t, data, node.name, []string{"wireguard"})
 	}
 	var previousTrafficEncryptionMode string
-	var previousMulticastEnabledState bool
 	ac := func(config *agentconfig.AgentConfig) {
 		previousTrafficEncryptionMode = config.TrafficEncryptionMode
 		config.TrafficEncryptionMode = "wireguard"
-		previousMulticastEnabledState = config.Multicast.Enable
-		config.Multicast.Enable = false
 	}
 	if err := data.mutateAntreaConfigMap(nil, ac, false, true); err != nil {
 		t.Fatalf("Failed to enable WireGuard tunnel: %v", err)
@@ -58,7 +56,6 @@ func TestWireGuard(t *testing.T) {
 	defer func() {
 		ac := func(config *agentconfig.AgentConfig) {
 			config.TrafficEncryptionMode = previousTrafficEncryptionMode
-			config.Multicast.Enable = previousMulticastEnabledState
 		}
 		if err := data.mutateAntreaConfigMap(nil, ac, false, true); err != nil {
 			t.Errorf("Failed to disable WireGuard tunnel: %v", err)