Repository: antrea-io/antrea
Pull request #6522 (2024-07-10 03:35:00)

Bump aquasecurity/trivy-action from 0.23.0 to 0.24.0
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.23.0 to 0.24.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/aquasecurity/trivy-action/releases">aquasecurity/trivy-action's releases</a>.</em></p>
<blockquote>
<h2>v0.24.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade trivy to v0.53.0 by <a href="https://github.com/Dr-DevOps"><code>@​Dr-DevOps</code></a> in <a href="https://redirect.github.com/aquasecurity/trivy-action/pull/369">aquasecurity/trivy-action#369</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/aquasecurity/trivy-action/compare/0.23.0...0.24.0">https://github.com/aquasecurity/trivy-action/compare/0.23.0...0.24.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/aquasecurity/trivy-action/commit/6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8"><code>6e7b7d1</code></a> Upgrade trivy to v0.53.0 (<a href="https://redirect.github.com/aquasecurity/trivy-action/issues/369">#369</a>)</li>
<li>See full diff in <a href="https://github.com/aquasecurity/trivy-action/compare/0.23.0...0.24.0">compare view</a></li>
</ul>
</details>
<br />

<details>
<summary>Most Recent Ignore Conditions Applied to This Pull Request</summary>

| Dependency Name | Ignore Conditions |
| --- | --- |
| aquasecurity/trivy-action | [>= 0.11.a, < 0.12] |
</details>


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aquasecurity/trivy-action&package-manager=github_actions&previous-version=0.23.0&new-version=0.24.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml
index af97debaf..2002d0295 100644
--- a/.github/workflows/conformance.yml
+++ b/.github/workflows/conformance.yml
@@ -71,10 +71,30 @@ jobs:
           ./hack/build-antrea-linux-all.sh --pull --distro ${{ inputs.antrea-image-distro }}
       - name: Install Kind
         run: |
-          KIND_VERSION=$(head -n1 ./ci/kind/version || echo v0.20.0)
+          KIND_VERSION=$(head -n1 ./ci/kind/version || echo v0.23.0)
           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: Build local image for conformance test
+        run: |
+          image="kindest/node:${{ inputs.k8s-version }}"
+          if docker pull $image 2>&1; then
+            echo "Image $image exists, no need to build it."
+          else
+            echo "Image $image does not exist, preparing to build it."
+            # Building a local Kind Node image with the latest Kubernetes version will consume a lot of disk space.
+            # We need to free up some disk space before building the image.
+            sudo apt-get clean
+            sudo rm -rf /usr/share/dotnet || true
+            sudo rm -rf /opt/ghc || true
+            sudo rm -rf "/usr/local/share/boost" || true
+            sudo rm -rf "$AGENT_TOOLSDIRECTORY" || true
+
+            git clone --depth 1 --branch ${{ inputs.k8s-version }} https://github.com/kubernetes/kubernetes.git /tmp/kubernetes
+            echo "Building Kind Node image with Kubernetes version ${{ inputs.k8s-version }}"
+            kind build node-image --image kindest/node:${{ inputs.k8s-version }} /tmp/kubernetes
+            rm -rf /tmp/kubernetes
+          fi
       - name: Create K8s cluster
         run: |
           # If an image does not exist (unified vs split), a warning will be printed, but the script
diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml
index a8b174831..3ade70978 100644
--- a/.github/workflows/kind.yml
+++ b/.github/workflows/kind.yml
@@ -170,6 +170,7 @@ jobs:
           --coverage \
           --encap-mode encap \
           --proxy-all \
+          --no-kube-proxy \
           --feature-gates LoadBalancerModeDSR=true \
           --load-balancer-mode dsr \
           --node-ipam
diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml
index 49ea30ae1..55ef8d942 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.23.0
+      uses: aquasecurity/trivy-action@0.24.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.agent.latest.txt'
     - name: Run Trivy vulnerability scanner on latest antrea-controller Docker image
       if: ${{ always() && steps.pull.conclusion == 'success' }}
-      uses: aquasecurity/trivy-action@0.23.0
+      uses: aquasecurity/trivy-action@0.24.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.controller.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.23.0
+      uses: aquasecurity/trivy-action@0.24.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.agent.${{ 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.23.0
+      uses: aquasecurity/trivy-action@0.24.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 bcf1c11fc..3debd1e94 100644
--- a/.github/workflows/trivy_scan_before_release.yml
+++ b/.github/workflows/trivy_scan_before_release.yml
@@ -17,13 +17,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.23.0
+      uses: aquasecurity/trivy-action@0.24.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.23.0
+      uses: aquasecurity/trivy-action@0.24.0
       with:
         scan-type: 'image'
         image-ref: 'antrea/antrea-controller-ubuntu:latest'
diff --git a/build/charts/antrea-windows/conf/antrea-agent.conf b/build/charts/antrea-windows/conf/antrea-agent.conf
index 5193ef5fd..8aa3a2817 100644
--- a/build/charts/antrea-windows/conf/antrea-agent.conf
+++ b/build/charts/antrea-windows/conf/antrea-agent.conf
@@ -119,7 +119,7 @@ antreaProxy:
   # To disable AntreaProxy, set this to false. It should be enabled on Windows, otherwise NetworkPolicy will
   # not take effect on Service traffic.
   enable: true
-  # ProxyAll tells antrea-agent to proxy ClusterIP Service traffic, regardless of where they come from.
+  # proxyAll tells antrea-agent to proxy ClusterIP Service traffic, regardless of where they come from.
   # Therefore, running kube-proxy is no longer required. This requires the AntreaProxy feature to be enabled.
   # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
   # apiserver directly.
diff --git a/build/charts/antrea/conf/antrea-agent.conf b/build/charts/antrea/conf/antrea-agent.conf
index e140f3c07..24eaf7df4 100644
--- a/build/charts/antrea/conf/antrea-agent.conf
+++ b/build/charts/antrea/conf/antrea-agent.conf
@@ -342,7 +342,7 @@ antreaProxy:
 {{- with .Values.antreaProxy }}
   # To disable AntreaProxy, set this to false.
   enable: {{.enable}}
-  # ProxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
+  # proxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
   # regardless of where they come from. Therefore, running kube-proxy is no longer required.
   # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
   # apiserver directly.
@@ -365,7 +365,7 @@ antreaProxy:
   # External IPs of LoadBalancer Services. This is useful when the external LoadBalancer provides additional
   # capabilities (e.g. TLS termination) and it is desirable for Pod-to-ExternalIP traffic to be sent to the
   # external LoadBalancer instead of being load-balanced to an Endpoint directly by AntreaProxy.
-  # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when ProxyAll is set to true and
+  # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when proxyAll is set to true and
   # kube-proxy is removed from the cluster, otherwise kube-proxy will still load-balance this traffic.
   proxyLoadBalancerIPs: {{ .proxyLoadBalancerIPs }}
   # The value of the "service.kubernetes.io/service-proxy-name" label for AntreaProxy to match. If it is set,
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 1d9c4c57e..64564b556 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -4038,7 +4038,7 @@ data:
     antreaProxy:
       # To disable AntreaProxy, set this to false.
       enable: true
-      # ProxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
+      # proxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
       # regardless of where they come from. Therefore, running kube-proxy is no longer required.
       # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
       # apiserver directly.
@@ -4055,7 +4055,7 @@ data:
       # External IPs of LoadBalancer Services. This is useful when the external LoadBalancer provides additional
       # capabilities (e.g. TLS termination) and it is desirable for Pod-to-ExternalIP traffic to be sent to the
       # external LoadBalancer instead of being load-balanced to an Endpoint directly by AntreaProxy.
-      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when ProxyAll is set to true and
+      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when proxyAll is set to true and
       # kube-proxy is removed from the cluster, otherwise kube-proxy will still load-balance this traffic.
       proxyLoadBalancerIPs: true
       # The value of the "service.kubernetes.io/service-proxy-name" label for AntreaProxy to match. If it is set,
@@ -5110,7 +5110,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: f976029accf54258d01ad907fe19b50ac671eee014cd8aea968c6a0bc7e8f95a
+        checksum/config: 71bf05ff579aa9bea7b360669c5e2ce2830ca88dc4ab54480638ce006eaeaf11
       labels:
         app: antrea
         component: antrea-agent
@@ -5348,7 +5348,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: f976029accf54258d01ad907fe19b50ac671eee014cd8aea968c6a0bc7e8f95a
+        checksum/config: 71bf05ff579aa9bea7b360669c5e2ce2830ca88dc4ab54480638ce006eaeaf11
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index 62e00bc5f..4cf276c99 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -4038,7 +4038,7 @@ data:
     antreaProxy:
       # To disable AntreaProxy, set this to false.
       enable: true
-      # ProxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
+      # proxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
       # regardless of where they come from. Therefore, running kube-proxy is no longer required.
       # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
       # apiserver directly.
@@ -4055,7 +4055,7 @@ data:
       # External IPs of LoadBalancer Services. This is useful when the external LoadBalancer provides additional
       # capabilities (e.g. TLS termination) and it is desirable for Pod-to-ExternalIP traffic to be sent to the
       # external LoadBalancer instead of being load-balanced to an Endpoint directly by AntreaProxy.
-      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when ProxyAll is set to true and
+      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when proxyAll is set to true and
       # kube-proxy is removed from the cluster, otherwise kube-proxy will still load-balance this traffic.
       proxyLoadBalancerIPs: true
       # The value of the "service.kubernetes.io/service-proxy-name" label for AntreaProxy to match. If it is set,
@@ -5110,7 +5110,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: f976029accf54258d01ad907fe19b50ac671eee014cd8aea968c6a0bc7e8f95a
+        checksum/config: 71bf05ff579aa9bea7b360669c5e2ce2830ca88dc4ab54480638ce006eaeaf11
       labels:
         app: antrea
         component: antrea-agent
@@ -5349,7 +5349,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: f976029accf54258d01ad907fe19b50ac671eee014cd8aea968c6a0bc7e8f95a
+        checksum/config: 71bf05ff579aa9bea7b360669c5e2ce2830ca88dc4ab54480638ce006eaeaf11
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index 22665a948..3e37bf206 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -4038,7 +4038,7 @@ data:
     antreaProxy:
       # To disable AntreaProxy, set this to false.
       enable: true
-      # ProxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
+      # proxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
       # regardless of where they come from. Therefore, running kube-proxy is no longer required.
       # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
       # apiserver directly.
@@ -4055,7 +4055,7 @@ data:
       # External IPs of LoadBalancer Services. This is useful when the external LoadBalancer provides additional
       # capabilities (e.g. TLS termination) and it is desirable for Pod-to-ExternalIP traffic to be sent to the
       # external LoadBalancer instead of being load-balanced to an Endpoint directly by AntreaProxy.
-      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when ProxyAll is set to true and
+      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when proxyAll is set to true and
       # kube-proxy is removed from the cluster, otherwise kube-proxy will still load-balance this traffic.
       proxyLoadBalancerIPs: true
       # The value of the "service.kubernetes.io/service-proxy-name" label for AntreaProxy to match. If it is set,
@@ -5110,7 +5110,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 5299e6235e262daf606758cf900766470fcb8dd21a0d707a3ae284548bd8c2b2
+        checksum/config: 91ff2b609519e4aaead6ab850252a49bbe674dec17f6f239c4d0fa6c7b5705f6
       labels:
         app: antrea
         component: antrea-agent
@@ -5346,7 +5346,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 5299e6235e262daf606758cf900766470fcb8dd21a0d707a3ae284548bd8c2b2
+        checksum/config: 91ff2b609519e4aaead6ab850252a49bbe674dec17f6f239c4d0fa6c7b5705f6
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index 0c9afaa98..c17048997 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -4051,7 +4051,7 @@ data:
     antreaProxy:
       # To disable AntreaProxy, set this to false.
       enable: true
-      # ProxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
+      # proxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
       # regardless of where they come from. Therefore, running kube-proxy is no longer required.
       # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
       # apiserver directly.
@@ -4068,7 +4068,7 @@ data:
       # External IPs of LoadBalancer Services. This is useful when the external LoadBalancer provides additional
       # capabilities (e.g. TLS termination) and it is desirable for Pod-to-ExternalIP traffic to be sent to the
       # external LoadBalancer instead of being load-balanced to an Endpoint directly by AntreaProxy.
-      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when ProxyAll is set to true and
+      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when proxyAll is set to true and
       # kube-proxy is removed from the cluster, otherwise kube-proxy will still load-balance this traffic.
       proxyLoadBalancerIPs: true
       # The value of the "service.kubernetes.io/service-proxy-name" label for AntreaProxy to match. If it is set,
@@ -5123,7 +5123,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: ba93df141f512a1f8483114b5994444c7231b298e7e9133483ddc1f4210ec395
+        checksum/config: 2d75956786eb552eaba94f89dfa5c6bab570bf662b82449e9af31a57ca138750
         checksum/ipsec-secret: d0eb9c52d0cd4311b6d252a951126bf9bea27ec05590bed8a394f0f792dcb2a4
       labels:
         app: antrea
@@ -5405,7 +5405,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: ba93df141f512a1f8483114b5994444c7231b298e7e9133483ddc1f4210ec395
+        checksum/config: 2d75956786eb552eaba94f89dfa5c6bab570bf662b82449e9af31a57ca138750
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-windows-with-ovs.yml b/build/yamls/antrea-windows-with-ovs.yml
index a3b0987f0..181af1890 100644
--- a/build/yamls/antrea-windows-with-ovs.yml
+++ b/build/yamls/antrea-windows-with-ovs.yml
@@ -246,7 +246,7 @@ data:
       # To disable AntreaProxy, set this to false. It should be enabled on Windows, otherwise NetworkPolicy will
       # not take effect on Service traffic.
       enable: true
-      # ProxyAll tells antrea-agent to proxy ClusterIP Service traffic, regardless of where they come from.
+      # proxyAll tells antrea-agent to proxy ClusterIP Service traffic, regardless of where they come from.
       # Therefore, running kube-proxy is no longer required. This requires the AntreaProxy feature to be enabled.
       # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
       # apiserver directly.
@@ -306,7 +306,7 @@ spec:
     metadata:
       annotations:
         checksum/agent-windows: 86f999cb18501659a52d982f20b3df5cdf666ffd849f50ed183c366e75d01ac5
-        checksum/windows-config: 10ad2be0a04b1752abc224fed0124f7b1da36efc5e7323e193eb38e11b25e798
+        checksum/windows-config: 4f07164f32afc61e20b4aef984a8781142e5d99f7c58f7581e4ccfeabb34855f
         microsoft.com/hostprocess-inherit-user: "true"
       labels:
         app: antrea
diff --git a/build/yamls/antrea-windows.yml b/build/yamls/antrea-windows.yml
index 00d330405..2a3837084 100644
--- a/build/yamls/antrea-windows.yml
+++ b/build/yamls/antrea-windows.yml
@@ -174,7 +174,7 @@ data:
       # To disable AntreaProxy, set this to false. It should be enabled on Windows, otherwise NetworkPolicy will
       # not take effect on Service traffic.
       enable: true
-      # ProxyAll tells antrea-agent to proxy ClusterIP Service traffic, regardless of where they come from.
+      # proxyAll tells antrea-agent to proxy ClusterIP Service traffic, regardless of where they come from.
       # Therefore, running kube-proxy is no longer required. This requires the AntreaProxy feature to be enabled.
       # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
       # apiserver directly.
@@ -234,7 +234,7 @@ spec:
     metadata:
       annotations:
         checksum/agent-windows: 63f16e1fadb6b1354efda21c73702b4290400181136d4d47d4b1cd6a5f82d037
-        checksum/windows-config: 10ad2be0a04b1752abc224fed0124f7b1da36efc5e7323e193eb38e11b25e798
+        checksum/windows-config: 4f07164f32afc61e20b4aef984a8781142e5d99f7c58f7581e4ccfeabb34855f
         microsoft.com/hostprocess-inherit-user: "true"
       labels:
         app: antrea
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index b938d9f83..97fd9640a 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -4038,7 +4038,7 @@ data:
     antreaProxy:
       # To disable AntreaProxy, set this to false.
       enable: true
-      # ProxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
+      # proxyAll tells antrea-agent to proxy all Service traffic, including NodePort, LoadBalancer, and ClusterIP traffic,
       # regardless of where they come from. Therefore, running kube-proxy is no longer required.
       # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
       # apiserver directly.
@@ -4055,7 +4055,7 @@ data:
       # External IPs of LoadBalancer Services. This is useful when the external LoadBalancer provides additional
       # capabilities (e.g. TLS termination) and it is desirable for Pod-to-ExternalIP traffic to be sent to the
       # external LoadBalancer instead of being load-balanced to an Endpoint directly by AntreaProxy.
-      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when ProxyAll is set to true and
+      # Note that setting ProxyLoadBalancerIPs to false usually only makes sense when proxyAll is set to true and
       # kube-proxy is removed from the cluster, otherwise kube-proxy will still load-balance this traffic.
       proxyLoadBalancerIPs: true
       # The value of the "service.kubernetes.io/service-proxy-name" label for AntreaProxy to match. If it is set,
@@ -5110,7 +5110,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: aca23e21519e0fc112647f23d3ce6f92a3dea0bc7ebf1c6d7a7eed2dbe80f0a3
+        checksum/config: ebc0be79b0fc65db51609f5c9185ca8a0533e265811d14c687f577cf93497a58
       labels:
         app: antrea
         component: antrea-agent
@@ -5346,7 +5346,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: aca23e21519e0fc112647f23d3ce6f92a3dea0bc7ebf1c6d7a7eed2dbe80f0a3
+        checksum/config: ebc0be79b0fc65db51609f5c9185ca8a0533e265811d14c687f577cf93497a58
       labels:
         app: antrea
         component: antrea-controller
diff --git a/ci/kind/kind-setup.sh b/ci/kind/kind-setup.sh
index eb31c3fe7..1aa6bbc66 100755
--- a/ci/kind/kind-setup.sh
+++ b/ci/kind/kind-setup.sh
@@ -38,7 +38,8 @@ KUBE_PROXY_MODE="iptables"
 PROMETHEUS=false
 K8S_VERSION=""
 KUBE_NODE_IPAM=true
-DEPLOY_EXTERNAL_SERVER=false
+DEPLOY_EXTERNAL_AGNHOST=false
+DEPLOY_EXTERNAL_FRR=false
 positional_args=()
 options=()
 
@@ -79,8 +80,9 @@ where:
     network. Note, '--extra-networks' and '--subnets' cannot be specified together.
   --ip-family: specify the ip-family for the kind cluster, default is $IP_FAMILY.
   --k8s-version: specify the Kubernetes version of the kind cluster, kind's default K8s version will be used if empty.
-  --deploy-external-server: deploy a container running as an external server for the cluster.
-  --all: delete all kind clusters
+  --deploy-external-agnhost: deploy a container running agnhost as an external server for the cluster, default is $DEPLOY_EXTERNAL_AGNHOST.
+  --deploy-external-frr: deploy a container running FRR as an external router for the cluster, default is $DEPLOY_EXTERNAL_FRR.
+  --all: delete all kind clusters.
   --until: delete kind clusters that have been created before the specified duration.
 "
 
@@ -411,7 +413,7 @@ EOF
   configure_networks
   configure_extra_networks
   configure_vlan_subnets
-  setup_external_server
+  setup_external_servers
   load_images
 
   if [[ $ANTREA_CNI == true ]]; then
@@ -441,7 +443,7 @@ function destroy {
   else
       kind delete cluster --name $CLUSTER_NAME
   fi
-  destroy_external_server
+  destroy_external_servers
   delete_networks
   delete_vlan_subnets
 }
@@ -455,15 +457,31 @@ function printUnixTimestamp {
     fi
 }
 
-function setup_external_server {
-  if [[ $DEPLOY_EXTERNAL_SERVER == true ]]; then
-    docker run -d --name antrea-external-server-$RANDOM --network kind -it --rm registry.k8s.io/e2e-test-images/agnhost:2.29 netexec &> /dev/null
+function setup_external_servers {
+  if [[ $DEPLOY_EXTERNAL_AGNHOST == true ]]; then
+    docker run -d --name antrea-external-agnhost-$RANDOM --network kind -it --rm registry.k8s.io/e2e-test-images/agnhost:2.40 netexec &> /dev/null
   fi
+
+  if [[ $DEPLOY_EXTERNAL_FRR == true ]]; then
+    docker run -d \
+      --name antrea-external-frr-$RANDOM \
+      --network kind --cap-add=NET_BIND_SERVICE \
+      --cap-add=NET_ADMIN \
+      --cap-add=NET_RAW \
+      --cap-add=SYS_ADMIN \
+      -it \
+      --rm \
+      frrouting/frr:v8.4.0 \
+      bash -c "/bin/sed -i s/bgpd=no/bgpd=yes/g /etc/frr/daemons && /sbin/tini -- /usr/lib/frr/docker-start" &> /dev/null
+    fi
 }
 
-function destroy_external_server {
-  echo "Deleting external server"
-  cid=$(docker ps -f name="^antrea-external-server" --format '{{.ID}}')
+function destroy_external_servers {
+  echo "Deleting external servers"
+  cid=$(docker ps -f name="^antrea-external-agnhost" --format '{{.ID}}')
+  docker rm -f $cid &> /dev/null || true
+
+  cid=$(docker ps -f name="^antrea-external-frr" --format '{{.ID}}')
   docker rm -f $cid &> /dev/null || true
 }
 
@@ -583,9 +601,14 @@ while [[ $# -gt 0 ]]
       K8S_VERSION="$2"
       shift 2
       ;;
-    --deploy-external-server)
-      add_option "--deploy-external-server" "create"
-      DEPLOY_EXTERNAL_SERVER=true
+    --deploy-external-agnhost)
+      add_option "--deploy-external-agnhost" "create"
+      DEPLOY_EXTERNAL_AGNHOST=true
+      shift
+      ;;
+    --deploy-external-frr)
+      add_option "--deploy-external-frr" "create"
+      DEPLOY_EXTERNAL_FRR=true
       shift
       ;;
     --all)
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index 83a2d0528..4f7099b6a 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -731,14 +731,15 @@ func run(o *Options) error {
 		go ipamController.Run(stopCh)
 	}
 
+	var secondaryNetworkController *secondarynetwork.Controller
 	if features.DefaultFeatureGate.Enabled(features.SecondaryNetwork) {
-		defer secondarynetwork.RestoreHostInterfaceConfiguration(&o.config.SecondaryNetwork)
-		if err := secondarynetwork.Initialize(
+		secondaryNetworkController, err = secondarynetwork.NewController(
 			o.config.ClientConnection, o.config.KubeAPIServerOverride,
-			k8sClient, localPodInformer.Get(), nodeConfig.Name,
-			podUpdateChannel, stopCh,
-			&o.config.SecondaryNetwork, ovsdbConnection); err != nil {
-			return fmt.Errorf("failed to initialize secondary network: %v", err)
+			k8sClient, localPodInformer.Get(),
+			podUpdateChannel,
+			&o.config.SecondaryNetwork, ovsdbConnection)
+		if err != nil {
+			return fmt.Errorf("failed to create secondary network controller: %w", err)
 		}
 	}
 
@@ -864,6 +865,15 @@ func run(o *Options) error {
 			return fmt.Errorf("failed to connect uplink to OVS bridge: %w", err)
 		}
 	}
+	// secondaryNetworkController Initialize must be run after FlowRestoreComplete for the case that Node
+	// IPs are moved to the secondary OVS bridge
+	if features.DefaultFeatureGate.Enabled(features.SecondaryNetwork) {
+		defer secondaryNetworkController.Restore()
+		if err = secondaryNetworkController.Initialize(); err != nil {
+			return fmt.Errorf("failed to initialize secondary network: %v", err)
+		}
+		go secondaryNetworkController.Run(stopCh)
+	}
 
 	// statsCollector collects stats and reports to the antrea-controller periodically. For now it's only used for
 	// NetworkPolicy stats and Multicast stats.
diff --git a/docs/antrea-ipam.md b/docs/antrea-ipam.md
index 3e4a41244..41baa073e 100644
--- a/docs/antrea-ipam.md
+++ b/docs/antrea-ipam.md
@@ -278,8 +278,8 @@ where the underlay router will route the traffic to the destination VLAN.
 ### Requirements for this Feature
 
 As of now, this feature is supported on Linux Nodes, with IPv4, `system` OVS datapath
-type, `noEncap`, `noSNAT` traffic mode, and `AntreaProxy` feature enabled. Configuration
-with `ProxyAll` feature enabled is not verified.
+type, `noEncap`, `noSNAT` traffic mode, and Antrea Proxy enabled. Configuration
+with `proxyAll` enabled is not verified.
 
 The IPs in the `IPPools` without VLAN must be in the same underlay subnet as the Node
 IP, because inter-Node traffic of AntreaIPAM Pods is forwarded by the Node network.
diff --git a/docs/antrea-network-policy.md b/docs/antrea-network-policy.md
index 96ed35c1f..db4e27e22 100644
--- a/docs/antrea-network-policy.md
+++ b/docs/antrea-network-policy.md
@@ -1490,7 +1490,7 @@ Kubernetes](https://kubernetes.io/docs/concepts/services-networking/dns-pod-serv
 Services. The reason is that Antrea will use the information included in A or
 AAAA DNS records to implement FQDN based policies. In the case of "normal" (not
 headless) Services, the DNS name resolves to the ClusterIP for the Service, but
-policy rules are enforced after AntreaProxy Service Load-Balancing and at that
+policy rules are enforced after Antrea Proxy Service Load-Balancing and at that
 stage the destination IP address has already been rewritten to the address of an
 endpoint backing the Service. For headless Services, a ClusterIP is not
 allocated and, assuming the Service has a selector, the DNS server returns A /
@@ -1571,8 +1571,8 @@ A combination of Service name and Service Namespace can be used in `toServices`
 by this field. A sample policy can be found [here](#acnp-for-toservices-rule).
 
 Since `toServices` represents a combination of IP+port, it cannot be used with `to` or `ports` within the same egress rule.
-Also, since the matching process relies on the groupID assigned to Service by AntreaProxy, this field can only be used when
-AntreaProxy is enabled.
+Also, since the matching process relies on the groupID assigned to Service by Antrea Proxy, this field can only be used when
+Antrea Proxy is enabled.
 
 This clusterIP-based match has one caveat: direct access to the Endpoints of this Service is not affected by
 `toServices` rules. To restrict access towards backend Endpoints of a Service, define a `ClusterGroup` with `ServiceReference`
@@ -1952,11 +1952,11 @@ Similar RBAC is applied to the ClusterGroup resource.
   won't be blocked by new rules.
 - For hairpin Service traffic, when a Pod initiates traffic towards the Service it
   provides, and the same Pod is selected as the Endpoint, NetworkPolicies will
-  consistently permit this traffic during ingress enforcement if AntreaProxy is enabled,
+  consistently permit this traffic during ingress enforcement if Antrea Proxy is enabled,
   irrespective of the ingress rules defined by the user. In the presence of ingress rules
   preventing access to the Service from Pods providing the Service, accessing the Service
   from one of these Pods will succeed if traffic is hairpinned back to the source Pod, and
-  will fail if a different Endpoint is selected by AntreaProxy. However, when AntreaProxy
+  will fail if a different Endpoint is selected by Antrea Proxy. However, when Antrea Proxy
   is disabled, NetworkPolicies may not function as expected for hairpin Service traffic.
   This is due to kube-proxy performing SNAT, which conceals the original source IP from
   Antrea. Consequently, NetworkPolicies are unable to differentiate between hairpin
diff --git a/docs/antrea-proxy.md b/docs/antrea-proxy.md
index 1469a76fb..ccdf1ba31 100644
--- a/docs/antrea-proxy.md
+++ b/docs/antrea-proxy.md
@@ -1,10 +1,10 @@
-# AntreaProxy
+# Antrea Proxy
 
 ## Table of Contents
 
 <!-- toc -->
 - [Introduction](#introduction)
-- [AntreaProxy with proxyAll](#antreaproxy-with-proxyall)
+- [Antrea Proxy with proxyAll](#antrea-proxy-with-proxyall)
   - [Removing kube-proxy](#removing-kube-proxy)
     - [Windows Nodes](#windows-nodes)
   - [Configuring load balancer mode for external traffic](#configuring-load-balancer-mode-for-external-traffic)
@@ -16,39 +16,48 @@
 
 ## Introduction
 
-AntreaProxy was first introduced in Antrea v0.8 and has been enabled by default
-on all platforms since v0.11. AntreaProxy enables some or all of the cluster's
+Antrea Proxy was first introduced in Antrea v0.8 and has been enabled by default
+on all platforms since v0.11. Antrea Proxy enables some or all of the cluster's
 Service traffic to be load-balanced as part of the OVS pipeline, instead of
 depending on kube-proxy. We typically observe latency improvements for Service
-traffic when AntreaProxy is used.
+traffic when Antrea Proxy is used.
 
-While AntreaProxy can be disabled on Linux Nodes by setting the `AntreaProxy`
+While Antrea Proxy can be disabled on Linux Nodes by setting the `AntreaProxy`
 Feature Gate to `false`, it should remain enabled on all Windows Nodes, as it is
 needed for correct NetworkPolicy implementation for Pod-to-Service traffic.
 
-By default, AntreaProxy will only handle Service traffic originating from Pods
+By default, Antrea Proxy will only handle Service traffic originating from Pods
 in the cluster, with no support for NodePort. However, starting with Antrea
-v1.4, a new operating mode was introduced in which AntreaProxy can handle all
+v1.4, a new operating mode was introduced in which Antrea Proxy can handle all
 Service traffic, including NodePort. See the following
-[section](#antreaproxy-with-proxyall) for more information.
+[section](#antrea-proxy-with-proxyall) for more information.
 
-## AntreaProxy with proxyAll
+## Antrea Proxy with proxyAll
 
 The `proxyAll` configuration parameter can be enabled in the Antrea
-configuration if you want AntreaProxy to handle all Service traffic, with the
+configuration if you want Antrea Proxy to handle all Service traffic, with the
 possibility to remove kube-proxy altogether and have one less DaemonSet running
 in the cluster. This is particularly interesting on Windows Nodes, since until
 the introduction of `proxyAll`, Antrea relied on userspace kube-proxy, which is
 no longer actively maintained by the K8s community and is slower than other
 kube-proxy backends.
 
-Note that on Linux, even when `proxyAll` is enabled, kube-proxy will usually
-take priority and will keep handling NodePort Service traffic (unless the source
-is a Pod, which is pretty unusual as Pods typically access Services by
-ClusterIP). This is because kube-proxy rules typically come before the rules
-installed by AntreaProxy to redirect traffic to OVS. When kube-proxy is not
-deployed or is removed from the cluster, AntreaProxy will then handle all
-Service traffic.
+Note that on Linux, before Antrea v2.1, when `proxyAll` is enabled, kube-proxy
+will usually take priority over Antrea Proxy and will keep handling all kinds of
+Service traffic (unless the source is a Pod, which is pretty unusual as Pods
+typically access Services by ClusterIP). This is because kube-proxy rules typically
+come before the rules installed by Antrea Proxy to redirect traffic to OVS. When
+kube-proxy is not deployed or is removed from the cluster, Antrea Proxy will then
+handle all Service traffic.
+
+Starting with Antrea v2.1, when `proxyAll` is enabled, Antrea Proxy will handle
+Service traffic destined to NodePort, LoadBalancerIP and ExternalIP, even if
+kube-proxy is present. This benefits users who want to take advantage of
+Antrea Proxy's advanced features, such as Direct Server Return (DSR) mode, but
+lack control over kube-proxy's installation. This is accomplished by
+prioritizing the rules installed by Antrea Proxy over those installed by
+kube-proxy, thus it works only with kube-proxy iptables mode. Support for other
+kube-proxy modes may be added in the future.
 
 ### Removing kube-proxy
 
@@ -132,7 +141,7 @@ kube-proxy:
 
 Starting with Antrea v1.13, the `defaultLoadBalancerMode` configuration
 parameter and the `service.antrea.io/load-balancer-mode` Service annotation
-can be used to specify how you want AntreaProxy to handle external traffic
+can be used to specify how you want Antrea Proxy to handle external traffic
 destined to LoadBalancerIPs and ExternalIPs of Services. Specifically, the mode
 determines how external traffic is processed when it's load balanced across
 Nodes. Currently, it has two options: `nat` (default) and `dsr`.
@@ -200,8 +209,8 @@ configured to bind to that address and can therefore intercept queries. In case
 of a cache miss, queries can be sent to the cluster CoreDNS Pods thanks to a
 "shadow" Service which will expose CoreDNS Pods via a new ClusterIP.
 
-When AntreaProxy is enabled (default), Pod DNS queries to the kube-dns ClusterIP
-will be load-balanced directly by AntreaProxy to a CoreDNS Pod endpoint. This
+When Antrea Proxy is enabled (default), Pod DNS queries to the kube-dns ClusterIP
+will be load-balanced directly by Antrea Proxy to a CoreDNS Pod endpoint. This
 means that NodeLocal DNSCache is completely bypassed, which is probably not
 acceptable for users who want to leverage this feature to improve DNS
 performance in their clusters. While these users can update the Pod
@@ -210,8 +219,8 @@ interface, this is not always ideal in the context of CaaS, as it can require
 everyone running Pods in the cluster to be aware of the situation.
 
 This is the reason why we initially introduced the `skipServices` configuration
-option for AntreaProxy in Antrea v1.4. By adding the kube-dns Service (which
-exposes CoreDNS) to the list, you can ensure that AntreaProxy will "ignore" Pod
+option for Antrea Proxy in Antrea v1.4. By adding the kube-dns Service (which
+exposes CoreDNS) to the list, you can ensure that Antrea Proxy will "ignore" Pod
 DNS queries, and that they will be forwarded to NodeLocal DNSCache. You can edit
 the `antrea-config` ConfigMap as follows:
 
@@ -232,11 +241,11 @@ data:
 In some cases, the external LoadBalancer for a cluster provides additional
 capabilities (e.g., TLS termination) and it is desirable for Pods to access
 in-cluster Services through the external LoadBalancer. By default, this is not
-the case as both kube-proxy and AntreaProxy will install rules to load-balance
+the case as both kube-proxy and Antrea Proxy will install rules to load-balance
 this traffic directly at the source Node (even when the destination IP is set to
 the external `loadBalancerIP`). To circumvent this behavior, we introduced the
-`proxyLoadBalancerIPs` configuration option for AntreaProxy in Antrea v1.5. This
-option defaults to `true`, but when setting it to `false`, AntreaProxy will no
+`proxyLoadBalancerIPs` configuration option for Antrea Proxy in Antrea v1.5. This
+option defaults to `true`, but when setting it to `false`, Antrea Proxy will no
 longer load-balance traffic destined to external `loadBalancerIP`s, hence
 ensuring that this traffic can go to the external LoadBalancer. You can set it
 to `false` by editing the `antrea-config` ConfigMap as follows:
@@ -253,7 +262,7 @@ data:
       proxyLoadBalancerIPs: false
 ```
 
-With the above configuration, AntreaProxy will ignore all external `loadBalancerIP`s.
+With the above configuration, Antrea Proxy 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.
@@ -265,12 +274,12 @@ every external `loadBalancerIP` behaves in a LoadBalancer Service.
   destined for the corresponding external `loadBalancerIP` should be sent to the
   external LoadBalancer.
 
-Starting with Antrea v2.0, AntreaProxy will respect `LoadBalancerIPMode` in LoadBalancer
+Starting with Antrea v2.0, Antrea Proxy 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
+(default). In this case, Antrea Proxy 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
+`LoadBalancerIPModeProxy` will bypass Antrea Proxy. If the configuration option
+`proxyLoadBalancerIPs` is set to `false`, Antrea Proxy will ignore the external
 `loadBalancerIP`s even if configured with `LoadBalancerIPModeVIP`.
 
 There are two important prerequisites for this feature:
diff --git a/docs/design/architecture.md b/docs/design/architecture.md
index 140a46e90..156719528 100644
--- a/docs/design/architecture.md
+++ b/docs/design/architecture.md
@@ -205,7 +205,7 @@ so their source IP will be rewritten to the Node's IP before going out.
 ### ClusterIP Service
 
 Antrea supports two ways to implement Services of type ClusterIP - leveraging
-`kube-proxy`, or AntreaProxy that implements load balancing for ClusterIP
+`kube-proxy`, or Antrea Proxy that implements load balancing for ClusterIP
 Service traffic with OVS.
 
 When leveraging `kube-proxy`, Antrea Agent adds OVS flows to forward the packets
@@ -222,12 +222,12 @@ the tunnel.
 See the [Kubernetes Service Proxies documentation](https://kubernetes.io/docs/reference/networking/virtual-ips)
 for more details.
 
-When AntreaProxy is enabled, Antrea Agent will add OVS flows that implement
+When Antrea Proxy is enabled, Antrea Agent will add OVS flows that implement
 load balancing and DNAT for the ClusterIP Service traffic. In this way, Service
 traffic load balancing is done inside OVS together with the rest of the
 forwarding, and it can achieve better performance than using `kube-proxy`, as
 there is no extra overhead of forwarding Service traffic to the host's network
-stack and iptables processing. The AntreaProxy implementation in Antrea Agent
+stack and iptables processing. The Antrea Proxy implementation in Antrea Agent
 leverages some `kube-proxy` packages to watch and process Service Endpoints.
 
 ### NetworkPolicy
diff --git a/docs/design/ovs-pipeline.md b/docs/design/ovs-pipeline.md
index 0dcf9ff7e..5f3aecd2f 100644
--- a/docs/design/ovs-pipeline.md
+++ b/docs/design/ovs-pipeline.md
@@ -257,7 +257,7 @@ Like K8s NetworkPolicy, several tables of the pipeline are dedicated to [Kuberne
 Service](https://kubernetes.io/docs/concepts/services-networking/service/) implementation (tables [NodePortMark],
 [SessionAffinity], [ServiceLB], and [EndpointDNAT]).
 
-By enabling `proxyAll`, ClusterIP, NodePort, LoadBalancer, and ExternalIP are all handled by AntreaProxy. Otherwise,
+By enabling `proxyAll`, ClusterIP, NodePort, LoadBalancer, and ExternalIP are all handled by Antrea Proxy. Otherwise,
 only in-cluster ClusterIP is handled. In this document, we use the sample K8s Services below. These Services select Pods
 with the label `app: web` as Endpoints.
 
@@ -788,12 +788,13 @@ Flow 1 is for case 1, matching packets received on the local Antrea gateway port
 addresses. There are some cases where the source IP of the packets through the local Antrea gateway port is not the local
 Antrea gateway IP address:
 
-- When Antrea is deployed with kube-proxy, and `AntreaProxy` is not enabled, packets from local Pods destined for Services
-  will first go through the gateway port, get load-balanced by the kube-proxy data path (undergoes DNAT) then re-enter
-  the OVS pipeline through the gateway port (through an "onlink" route, installed by Antrea, directing the DNAT'd packets
-  to the gateway port), resulting in the source IP being that of a local Pod.
-- When Antrea is deployed without kube-proxy, and both `AntreaProxy` and `proxyAll` are enabled, packets from the external
-  network destined for Services will be routed to OVS through the gateway port without masquerading source IP.
+- When Antrea is deployed with kube-proxy, and the feature `AntreaProxy` is not enabled, packets from local Pods destined
+  for Services will first go through the gateway port, get load-balanced by the kube-proxy data path (undergoes DNAT)
+  then re-enter the OVS pipeline through the gateway port (through an "onlink" route, installed by Antrea, directing the
+  DNAT'd packets to the gateway port), resulting in the source IP being that of a local Pod.
+- When Antrea is deployed without kube-proxy, and both the feature `AntreaProxy` and option `proxyAll` are enabled,
+  packets from the external network destined for Services will be routed to OVS through the gateway port without
+  masquerading source IP.
 - When Antrea is deployed with kube-proxy, packets from the external network destined for Services whose
   `externalTrafficPolicy` is set to `Local` will get load-balanced by the kube-proxy data path (undergoes DNAT with a
   local Endpoint selected by the kube-proxy) and then enter the OVS pipeline through the gateway (through a "onlink"
@@ -1303,7 +1304,7 @@ through the local Antrea gateway. In other words, these are connections for whic
 (SYN packet for TCP) was received through the local Antrea gateway. It rewrites the destination MAC address to
 that of the local Antrea gateway, loads `ToGatewayRegMark`, and forwards them to table [L3DecTTL]. This ensures that
 reply packets can be forwarded back to the local Antrea gateway in subsequent tables. This flow is required to handle
-the following cases when AntreaProxy is not enabled:
+the following cases when Antrea Proxy is not enabled:
 
 - Reply traffic for connections from a local Pod to a ClusterIP Service, which are handled by kube-proxy and go through
   DNAT. In this case, the destination IP address of the reply traffic is the Pod which initiated the connection to the
diff --git a/docs/design/windows-design.md b/docs/design/windows-design.md
index 0d80658c8..592d0f2f2 100644
--- a/docs/design/windows-design.md
+++ b/docs/design/windows-design.md
@@ -210,7 +210,7 @@ Kube-proxy userspace mode is configured to provide NodePort Service function. A
 "HNS Internal NIC" is provided to kube-proxy to configure Service addresses. The OpenFlow entries for the
 NodePort Service traffic on Windows are the same as those on Linux.
 
-AntreaProxy implements the ClusterIP Service function. Antrea Agent installs routes to send ClusterIP Service
+Antrea Proxy implements the ClusterIP Service function. Antrea Agent installs routes to send ClusterIP Service
 traffic from host network to the OVS bridge. For each Service, it adds a route that routes the traffic via a
 virtual IP (169.254.0.253), and it also adds a route to indicate that the virtual IP is reachable via
 antrea-gw0. The reason to add a virtual IP, rather than routing the traffic directly to antrea-gw0, is that
diff --git a/docs/feature-gates.md b/docs/feature-gates.md
index 287d9dacc..8efb4388e 100644
--- a/docs/feature-gates.md
+++ b/docs/feature-gates.md
@@ -64,18 +64,15 @@ edit the Agent configuration in the
 
 ### AntreaProxy
 
-`AntreaProxy` implements Service load-balancing for ClusterIP Services as part of the OVS pipeline, as opposed to
-relying on kube-proxy. This only applies to traffic originating from Pods, and destined to ClusterIP Services. In
-particular, it does not apply to NodePort Services. Please note that due to some restrictions on the implementation of
-Services in Antrea, the maximum number of Endpoints that Antrea can support at the moment is 800. If the number of
-Endpoints for a given Service exceeds 800, extra Endpoints will be dropped.
+`AntreaProxy` enables Antrea Proxy which implements Service load-balancing for ClusterIP Services as part of the OVS
+pipeline, as opposed to relying on kube-proxy. By default, this only applies to traffic originating from Pods, and
+destined to ClusterIP Services. However, it can be configured to support all types of Services, replacing kube-proxy
+entirely. Please refer to this [document](antrea-proxy.md) for extra information on Antrea Proxy and how it can be configured.
 
 Note that this feature must be enabled for Windows. The Antrea Windows YAML manifest provided as part of releases
 enables this feature by default. If you edit the manifest, make sure you do not disable it, as it is needed for correct
 NetworkPolicy implementation for Pod-to-Service traffic.
 
-Please refer to this [document](antrea-proxy.md) for extra information on AntreaProxy and how it can be configured.
-
 #### Requirements for this Feature
 
 When using the OVS built-in kernel module (which is the most common case), your kernel version must be >= 4.6 (as
@@ -83,9 +80,9 @@ opposed to >= 4.4 without this feature).
 
 ### EndpointSlice
 
-`EndpointSlice` enables Service EndpointSlice support in AntreaProxy. The EndpointSlice API was introduced in Kubernetes
+`EndpointSlice` enables Service EndpointSlice support in Antrea Proxy. The EndpointSlice API was introduced in Kubernetes
 1.16 (alpha) and it is enabled by default in Kubernetes 1.17 (beta), promoted to GA in Kubernetes 1.21. The EndpointSlice
-feature will take no effect if AntreaProxy is not enabled. Refer to this [link](https://kubernetes.io/docs/tasks/administer-cluster/enabling-endpointslices/)
+feature will take no effect if Antrea Proxy is not enabled. Refer to this [link](https://kubernetes.io/docs/tasks/administer-cluster/enabling-endpointslices/)
 for more information about EndpointSlice. If this feature is enabled but the EndpointSlice v1 API is not available
 (Kubernetes version is lower than 1.21), Antrea Agent will log a message and fallback to the Endpoints API.
 
@@ -96,7 +93,7 @@ for more information about EndpointSlice. If this feature is enabled but the End
 
 ### TopologyAwareHints
 
-`TopologyAwareHints` enables TopologyAwareHints support in AntreaProxy. For AntreaProxy, traffic can be routed to the
+`TopologyAwareHints` enables TopologyAwareHints support in Antrea Proxy. For Antrea Proxy, traffic can be routed to the
 Endpoint which is closer to where it originated when this feature is enabled. Refer to this [link](https://kubernetes.io/docs/concepts/services-networking/topology-aware-hints/)
 for more information about TopologyAwareHints.
 
@@ -111,7 +108,7 @@ for more information about TopologyAwareHints.
 mode determines how external traffic destined to LoadBalancerIPs and ExternalIPs of Services is processed when it's load
 balanced across Nodes. In DSR mode, external traffic is never SNAT'd and backend Pods running on Nodes that are not the
 ingress Node can reply to clients directly, bypassing the ingress Node. Therefore, DSR mode can preserve client IP of
-requests, and usually has lower latency and higher throughput. It's only meaningful to use this feature when AntreaProxy
+requests, and usually has lower latency and higher throughput. It's only meaningful to use this feature when Antrea Proxy
 is enabled and configured to proxy external traffic (proxyAll=true). Refer to this [link](
 antrea-proxy.md#configuring-load-balancer-mode-for-external-traffic) for more information about load balancer mode.
 
@@ -124,7 +121,7 @@ antrea-proxy.md#configuring-load-balancer-mode-for-external-traffic) for more in
 
 ### CleanupStaleUDPSvcConntrack
 
-`CleanupStaleUDPSvcConntrack` enables support for cleaning up stale UDP Service conntrack connections in AntreaProxy.
+`CleanupStaleUDPSvcConntrack` enables support for cleaning up stale UDP Service conntrack connections in Antrea Proxy.
 
 #### Requirements for this Feature
 
@@ -155,7 +152,7 @@ for both Linux and Windows). In v0.11, this feature was graduated to Beta (enabl
 lifted.
 
 In order to support cluster Services as the destination for tracing requests, option `antreaProxy.enable` should be set
-to true to enable AntreaProxy.
+to true to enable Antrea Proxy.
 
 ### Flow Exporter
 
diff --git a/docs/noencap-hybrid-modes.md b/docs/noencap-hybrid-modes.md
index ab0c3b311..0c96e121c 100644
--- a/docs/noencap-hybrid-modes.md
+++ b/docs/noencap-hybrid-modes.md
@@ -9,10 +9,10 @@ are in different subnets, but does not encapsulate when the source and the
 destination Nodes are in the same subnet. This document describes how to
 configure Antrea with the `NoEncap` and `Hybrid` modes.
 
-The NoEncap and Hybrid traffic modes require AntreaProxy to support correct
-NetworkPolicy enforcement, which is why trying to disable AntreaProxy in these
+The NoEncap and Hybrid traffic modes require Antrea Proxy to support correct
+NetworkPolicy enforcement, which is why trying to disable Antrea Proxy in these
 modes will normally cause the Antrea Agent to fail. It is possible to override
-this behavior and force AntreaProxy to be disabled by setting the
+this behavior and force Antrea Proxy to be disabled by setting the
 ALLOW_NO_ENCAP_WITHOUT_ANTREA_PROXY environment variable to true for the Antrea
 Agent in the [Antrea deployment yaml](../build/yamls/antrea.yml).
 For example:
diff --git a/docs/prometheus-integration.md b/docs/prometheus-integration.md
index 25a49b9b3..47a4daf7a 100644
--- a/docs/prometheus-integration.md
+++ b/docs/prometheus-integration.md
@@ -207,15 +207,15 @@ duration of syncing internal-networkpolicy
 #### Antrea Proxy Metrics
 
 - **antrea_proxy_sync_proxy_rules_duration_seconds:** SyncProxyRules duration
-of AntreaProxy in seconds
+of Antrea Proxy in seconds
 - **antrea_proxy_total_endpoints_installed:** The number of Endpoints
-installed by AntreaProxy
+installed by Antrea Proxy
 - **antrea_proxy_total_endpoints_updates:** The cumulative number of Endpoint
-updates received by AntreaProxy
+updates received by Antrea Proxy
 - **antrea_proxy_total_services_installed:** The number of Services installed
-by AntreaProxy
+by Antrea Proxy
 - **antrea_proxy_total_services_updates:** The cumulative number of Service
-updates received by AntreaProxy
+updates received by Antrea Proxy
 
 ### Common Metrics Provided by Infrastructure
 
diff --git a/docs/service-loadbalancer.md b/docs/service-loadbalancer.md
index 7f8235da0..3210ab513 100644
--- a/docs/service-loadbalancer.md
+++ b/docs/service-loadbalancer.md
@@ -36,7 +36,7 @@ LoadBalancer
 ## Service external IP management by Antrea
 
 Antrea supports external IP management for Services of type LoadBalancer
-since version 1.5, which can work together with `AntreaProxy` or
+since version 1.5, which can work together with Antrea Proxy or
 `kube-proxy` to implement Services of type LoadBalancer, without requiring an
 external load balancer. With the external IP management feature, Antrea can
 allocate an external IP for a Service of type LoadBalancer from an
@@ -44,7 +44,7 @@ allocate an external IP for a Service of type LoadBalancer from an
 based on the ExternalIPPool's NodeSelector to host the external IP. Antrea
 configures the Service's external IP on the selected Node, and thus Service
 requests to the external IP will get to the Node, and they are then handled by
-`AntreaProxy` or `kube-proxy` on the Node and distributed to the Service's
+Antrea Proxy or `kube-proxy` on the Node and distributed to the Service's
 Endpoints. Antrea also implements a Node failover mechanism for Service
 external IPs. When Antrea detects a Node hosting an external IP is down, it
 will move the external IP to another available Node of the ExternalIPPool.
@@ -56,7 +56,7 @@ enabled in the `kube-proxy` configuration. For more information about how to
 configure `kube-proxy`, please refer to the [Interoperability with kube-proxy
 IPVS mode](#interoperability-with-kube-proxy-ipvs-mode) section.
 
-If you are using `kube-proxy` iptables mode or [`AntreaProxy` with `proxyAll`](antrea-proxy.md#antreaproxy-with-proxyall),
+If you are using `kube-proxy` iptables mode or [Antrea Proxy with `proxyAll`](antrea-proxy.md#antrea-proxy-with-proxyall),
 no extra configuration change is needed.
 
 ### Configuration
@@ -84,15 +84,15 @@ data:
       ServiceExternalIP: true
 ```
 
-The feature works with both `AntreaProxy` and `kube-proxy`, including the
+The feature works with both Antrea Proxy and `kube-proxy`, including the
 following configurations:
 
-- `AntreaProxy` without `proxyAll` enabled - this is `antrea-agent`'s default
+- Antrea Proxy without `proxyAll` enabled - this is `antrea-agent`'s default
 configuration, in which `kube-proxy` serves the request traffic for Services
-of type LoadBalancer (while `AntreaProxy` handles Service requests from Pods).
-- `AntreaProxy` with `proxyAll` enabled - in this case, `AntreaProxy` handles
+of type LoadBalancer (while Antrea Proxy handles Service requests from Pods).
+- Antrea Proxy with `proxyAll` enabled - in this case, Antrea Proxy handles
 all Service traffic, including Services of type LoadBalancer.
-- `AntreaProxy` disabled - `kube-proxy` handles all Service traffic, including
+- Antrea Proxy disabled - `kube-proxy` handles all Service traffic, including
 Services of type LoadBalancer.
 
 #### Create an ExternalIPPool custom resource
@@ -312,7 +312,7 @@ As MetalLB will allocate external IPs for all Services of type LoadBalancer,
 once it is running, the Service external IP management feature of Antrea should
 not be enabled to avoid conflicts with MetalLB. You can deploy Antrea with the
 default configuration (in which the `ServiceExternalIP` feature gate of
-`antrea-agent` is set to `false`). MetalLB can work with both `AntreaProxy` and
+`antrea-agent` is set to `false`). MetalLB can work with both Antrea Proxy and
 `kube-proxy` configurations of `antrea-agent`.
 
 ### Configure MetalLB with layer 2 mode
diff --git a/docs/traceflow-guide.md b/docs/traceflow-guide.md
index 0a3f55310..bf4c2c060 100644
--- a/docs/traceflow-guide.md
+++ b/docs/traceflow-guide.md
@@ -30,7 +30,7 @@ graph.
 ## Prerequisites
 
 The Traceflow feature is enabled by default since Antrea version v0.11. In order
-to use a Service as the destination in traces, AntreaProxy (also enabled by
+to use a Service as the destination in traces, Antrea Proxy (also enabled by
 default since v0.11) is required.
 
 ## Start a New Traceflow
diff --git a/docs/windows.md b/docs/windows.md
index fc2c4e1d5..871de3474 100644
--- a/docs/windows.md
+++ b/docs/windows.md
@@ -103,6 +103,22 @@ higher (containerd 1.7 or higher is recommended). It relies on support for
 [Windows HostProcess Pods](https://kubernetes.io/docs/tasks/configure-pod-container/create-hostprocess-pod/),
 which is generally available starting with K8s 1.26.
 
+More detailed containerd Version requirements are outlined below:
+
+| Kubernetes Version  | Recommended containerd Version |
+| ------------------- | ---------------- |
+| 1.26                | 1.7.0+, 1.6.18+  |
+| 1.27                | 1.7.0+, 1.6.18+  |
+| 1.28                | 1.7.0+, 1.6.18+  |
+| 1.29                | 1.7.11+, 1.6.27+ |
+| 1.30                | 1.7.13+, 1.6.28+ |
+
+Note: Starting from Antrea v2.1, Antrea Windows image is built based on the HPC (Host
+Process Containers) image, containerd version 1.6.18 or higher is required because
+versions earlier than 1.6.18 do not support importing HPC images on Windows.
+
+For more detailed information on Kubernetes-supported containerd versions, refer to the [Containerd releases page](https://containerd.io/releases/#kubernetes-support)
+
 Starting with Antrea v1.13, Antrea takes over all the responsibilities of
 kube-proxy for Windows Nodes by default, and kube-proxy should not be deployed
 on Windows Nodes with Antrea.
@@ -126,10 +142,10 @@ configuration file as the Antrea Proxy `proxyAll` mode is enabled by default.
     # Defaults to "". It must be a host string, a host:port pair, or a URL to the base of the apiserver.
     kubeAPIServerOverride: "10.10.1.1:6443"
 
-    # Option antreaProxy contains AntreaProxy related configuration options.
+    # Option antreaProxy contains Antrea Proxy related configuration options.
     antreaProxy:
-      # ProxyAll tells antrea-agent to proxy ClusterIP Service traffic, regardless of where they come from.
-      # Therefore, running kube-proxy is no longer required. This requires the AntreaProxy feature to be enabled.
+      # Option proxyAll tells antrea-agent to proxy ClusterIP Service traffic, regardless of where they come from.
+      # Therefore, running kube-proxy is no longer required. This requires the Antrea Proxy feature to be enabled.
       # Note that this option is experimental. If kube-proxy is removed, option kubeAPIServerOverride must be used to access
       # apiserver directly.
       proxyAll: true
@@ -221,7 +237,7 @@ running the script. The following command downloads and executes
 ```powershell
 # Example:
 curl.exe -LO "https://raw.githubusercontent.com/antrea-io/antrea/main/hack/windows/Prepare-Node.ps1"
-.\Prepare-Node.ps1 -KubernetesVersion v1.29.0 -NodeIP 192.168.1.10
+.\Prepare-Node.ps1 -KubernetesVersion v1.30.0 -NodeIP 192.168.1.10
 ```
 
 ##### 4. Prepare Node environment needed by antrea-agent
diff --git a/hack/windows/Install-OVS.ps1 b/hack/windows/Install-OVS.ps1
index a648582c2..3f17c974b 100644
--- a/hack/windows/Install-OVS.ps1
+++ b/hack/windows/Install-OVS.ps1
@@ -175,7 +175,14 @@ function CheckAndInstallOVSDriver {
     $ExportType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert
     $Cert = (Get-AuthenticodeSignature $DriverFile).SignerCertificate
     [System.IO.File]::WriteAllBytes($CertificateFile, $Cert.Export($ExportType))
-    Import-Certificate -FilePath "$CertificateFile" -CertStoreLocation cert:\LocalMachine\TrustedPublisher
+    # Use certstore.Add to import cert into trusted publishers instead of Import-Certificate,
+    # otherwise an error "Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))"
+    # may occur when `Import-Certificate` is used to import a certificate to the trusted publisher
+    # store for the first time on a fresh Windows 2022 Node. See issue #6530.
+    $CertStore = Get-Item cert:\LocalMachine\TrustedPublisher
+    $CertStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]"ReadWrite")
+    $CertStore.Add($(Get-Item $CertificateFile).FullName)
+    $CertStore.Close()
     Import-Certificate -FilePath "$CertificateFile" -CertStoreLocation cert:\LocalMachine\Root
 
     # Install the OVSext driver with the desired version
diff --git a/hack/windows/Prepare-Node.ps1 b/hack/windows/Prepare-Node.ps1
index 3efff3f68..14ca12b3c 100644
--- a/hack/windows/Prepare-Node.ps1
+++ b/hack/windows/Prepare-Node.ps1
@@ -34,7 +34,7 @@ Specifies whether OVS userspace processes are included in the installation. If f
 processes will not be installed as a Windows service on the host.
 
 .EXAMPLE
-PS> .\Prepare-Node.ps1 -KubernetesVersion v1.27.0 -NodeIP 192.168.1.10 -ContainerRuntime containerd 
+PS> .\Prepare-Node.ps1 -KubernetesVersion v1.30.0 -NodeIP 192.168.1.10 -ContainerRuntime containerd 
 
 #>
 
diff --git a/pkg/agent/nodeportlocal/k8s/annotations.go b/pkg/agent/nodeportlocal/k8s/annotations.go
index 8601d73b7..62dba91a6 100644
--- a/pkg/agent/nodeportlocal/k8s/annotations.go
+++ b/pkg/agent/nodeportlocal/k8s/annotations.go
@@ -22,6 +22,7 @@ import (
 	"sort"
 
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	clientset "k8s.io/client-go/kubernetes"
@@ -62,7 +63,7 @@ func patchPod(value []npltypes.NPLAnnotation, pod *corev1.Pod, kubeClient client
 
 	payloadBytes, _ := json.Marshal(newPayload)
 	if _, err := kubeClient.CoreV1().Pods(pod.Namespace).Patch(context.TODO(), pod.Name, types.MergePatchType,
-		payloadBytes, metav1.PatchOptions{}, "status"); err != nil {
+		payloadBytes, metav1.PatchOptions{}, "status"); err != nil && !errors.IsNotFound(err) {
 		return fmt.Errorf("unable to update NodePortLocal annotation for Pod %s/%s: %v", pod.Namespace,
 			pod.Name, err)
 	}
diff --git a/pkg/agent/nodeportlocal/k8s/npl_controller.go b/pkg/agent/nodeportlocal/k8s/npl_controller.go
index 9469af9ad..60d24a644 100644
--- a/pkg/agent/nodeportlocal/k8s/npl_controller.go
+++ b/pkg/agent/nodeportlocal/k8s/npl_controller.go
@@ -18,13 +18,13 @@ import (
 	"encoding/json"
 	"fmt"
 	"reflect"
-	"sync"
 	"time"
 
 	"antrea.io/antrea/pkg/agent/nodeportlocal/portcache"
 	"antrea.io/antrea/pkg/agent/nodeportlocal/rules"
 	"antrea.io/antrea/pkg/agent/nodeportlocal/types"
 	"antrea.io/antrea/pkg/agent/nodeportlocal/util"
+	"antrea.io/antrea/pkg/util/k8s"
 	utilsets "antrea.io/antrea/pkg/util/sets"
 
 	corev1 "k8s.io/api/core/v1"
@@ -57,9 +57,7 @@ type NPLController struct {
 	podInformer cache.SharedIndexInformer
 	podLister   corelisters.PodLister
 	svcInformer cache.SharedIndexInformer
-	podToIP     map[string]string
 	nodeName    string
-	podIPLock   sync.RWMutex
 }
 
 func NewNPLController(kubeClient clientset.Interface,
@@ -73,7 +71,6 @@ func NewNPLController(kubeClient clientset.Interface,
 		podInformer: podInformer,
 		podLister:   corelisters.NewPodLister(podInformer.GetIndexer()),
 		svcInformer: svcInformer,
-		podToIP:     make(map[string]string),
 		nodeName:    nodeName,
 	}
 
@@ -294,10 +291,13 @@ func (c *NPLController) getPodsFromService(svc *corev1.Service) []string {
 	return pods
 }
 
-func (c *NPLController) getTargetPortsForServicesOfPod(obj interface{}) (sets.Set[string], sets.Set[string]) {
+func (c *NPLController) getTargetPortsForServicesOfPod(pod *corev1.Pod) (sets.Set[string], sets.Set[string]) {
 	targetPortsInt := sets.New[string]()
 	targetPortsStr := sets.New[string]()
-	pod := obj.(*corev1.Pod)
+	// If the Pod is already terminated, its NodePortLocal ports should be released.
+	if k8s.IsPodTerminated(pod) {
+		return targetPortsInt, targetPortsStr
+	}
 	services, err := c.svcInformer.GetIndexer().ByIndex(NPLEnabledAnnotationIndex, "true")
 	if err != nil {
 		klog.Errorf("Got error while listing Services with annotation %s: %v", types.NPLEnabledAnnotationKey, err)
@@ -377,27 +377,8 @@ func (c *NPLController) processNextWorkItem() bool {
 	return true
 }
 
-func (c *NPLController) getPodIPFromCache(key string) (string, bool) {
-	c.podIPLock.RLock()
-	defer c.podIPLock.RUnlock()
-	podIP, found := c.podToIP[key]
-	return podIP, found
-}
-
-func (c *NPLController) addPodIPToCache(key, podIP string) {
-	c.podIPLock.Lock()
-	defer c.podIPLock.Unlock()
-	c.podToIP[key] = podIP
-}
-
-func (c *NPLController) deletePodIPFromCache(key string) {
-	c.podIPLock.Lock()
-	defer c.podIPLock.Unlock()
-	delete(c.podToIP, key)
-}
-
-func (c *NPLController) deleteAllPortRulesIfAny(podIP string) error {
-	return c.portTable.DeleteRulesForPod(podIP)
+func (c *NPLController) deleteAllPortRulesIfAny(podKey string) error {
+	return c.portTable.DeleteRulesForPod(podKey)
 }
 
 // handleRemovePod removes rules from port table and
@@ -405,18 +386,11 @@ func (c *NPLController) deleteAllPortRulesIfAny(podIP string) error {
 // This also removes Pod annotation from Pods that are not selected by Service annotation.
 func (c *NPLController) handleRemovePod(key string) error {
 	klog.V(2).Infof("Got delete event for Pod: %s", key)
-	podIP, found := c.getPodIPFromCache(key)
-	if !found {
-		klog.Infof("IP address not found for Pod: %s", key)
-		return nil
-	}
 
-	if err := c.deleteAllPortRulesIfAny(podIP); err != nil {
+	if err := c.deleteAllPortRulesIfAny(key); err != nil {
 		return err
 	}
 
-	c.deletePodIPFromCache(key)
-
 	return nil
 }
 
@@ -430,9 +404,8 @@ func (c *NPLController) handleAddUpdatePod(key string, obj interface{}) error {
 		klog.Infof("IP address not set for Pod: %s", key)
 		return nil
 	}
-	c.addPodIPToCache(key, podIP)
 
-	targetPortsInt, targetPortsStr := c.getTargetPortsForServicesOfPod(obj)
+	targetPortsInt, targetPortsStr := c.getTargetPortsForServicesOfPod(pod)
 	klog.V(2).Infof("Pod %s is selected by a Service for which NodePortLocal is enabled", key)
 
 	var nodePort int
@@ -474,7 +447,7 @@ func (c *NPLController) handleAddUpdatePod(key string, obj interface{}) error {
 	// Pod have to be cleaned up. If a Service uses a named target port that doesn't match any named container port
 	// for the current Pod, no corresponding entry will be added to the targetPortsInt set by the code above.
 	if len(targetPortsInt) == 0 {
-		if err := c.deleteAllPortRulesIfAny(podIP); err != nil {
+		if err := c.deleteAllPortRulesIfAny(key); err != nil {
 			return err
 		}
 		if _, exists := pod.Annotations[types.NPLAnnotationKey]; exists {
@@ -492,13 +465,13 @@ func (c *NPLController) handleAddUpdatePod(key string, obj interface{}) error {
 			return fmt.Errorf("failed to parse port number and protocol from %s for Pod %s: %v", targetPortProto, key, err)
 		}
 		podPorts[targetPortProto] = struct{}{}
-		portData := c.portTable.GetEntry(podIP, port, protocol)
+		portData := c.portTable.GetEntry(key, port, 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)
+			if err := c.portTable.DeleteRule(key, port, protocol); err != nil {
+				return fmt.Errorf("failed to delete defunct rule for Pod %s, Pod Port %d, Protocol %s: %w", key, port, protocol, err)
 			}
 			portData = nil
 		}
@@ -506,7 +479,7 @@ func (c *NPLController) handleAddUpdatePod(key string, obj interface{}) error {
 			if hport, ok := hostPorts[targetPortProto]; ok {
 				nodePort = hport
 			} else {
-				nodePort, err = c.portTable.AddRule(podIP, port, protocol)
+				nodePort, err = c.portTable.AddRule(key, port, protocol, podIP)
 				if err != nil {
 					return fmt.Errorf("failed to add rule for Pod %s: %v", key, err)
 				}
@@ -529,12 +502,12 @@ 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)
+	entries := c.portTable.GetDataForPod(key)
 	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)
+			if err := c.portTable.DeleteRule(key, data.PodPort, proto.Protocol); err != nil {
+				return fmt.Errorf("failed to delete rule for Pod %s, Pod Port %d, Protocol %s: %w", key, data.PodPort, proto.Protocol, err)
 			}
 		}
 	}
@@ -576,6 +549,7 @@ func (c *NPLController) waitForRulesInitialization() {
 		//   if yes, verifiy validity of the Node port, update the port table and add a rule to the
 		//   rules buffer.
 		pod := podList[i]
+		podKey := podKeyFunc(pod)
 		annotations := pod.GetAnnotations()
 		nplAnnotation, ok := annotations[types.NPLAnnotationKey]
 		if !ok {
@@ -599,6 +573,7 @@ func (c *NPLController) waitForRulesInitialization() {
 				continue
 			}
 			allNPLPorts = append(allNPLPorts, rules.PodNodePort{
+				PodKey:   podKey,
 				NodePort: npl.NodePort,
 				PodPort:  npl.PodPort,
 				PodIP:    pod.Status.PodIP,
diff --git a/pkg/agent/nodeportlocal/portcache/port_table.go b/pkg/agent/nodeportlocal/portcache/port_table.go
index b1856861b..29894c201 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table.go
@@ -29,7 +29,7 @@ import (
 const (
 	NodePortIndex    = "nodePortIndex"
 	PodEndpointIndex = "podEndpointIndex"
-	PodIPIndex       = "podIPIndex"
+	PodKeyIndex      = "podKeyIndex"
 )
 
 type ProtocolSocketData struct {
@@ -38,6 +38,8 @@ type ProtocolSocketData struct {
 }
 
 type NodePortData struct {
+	// PodKey is the namespaced name of the Pod.
+	PodKey   string
 	NodePort int
 	PodPort  int
 	PodIP    string
@@ -69,7 +71,7 @@ type PortTable struct {
 
 func GetPortTableKey(obj interface{}) (string, error) {
 	npData := obj.(*NodePortData)
-	key := fmt.Sprintf("%d:%s:%d:%s", npData.NodePort, npData.PodIP, npData.PodPort, npData.Protocol.Protocol)
+	key := fmt.Sprintf("%d:%s:%d:%s", npData.NodePort, npData.PodKey, npData.PodPort, npData.Protocol.Protocol)
 	return key, nil
 }
 
@@ -103,9 +105,9 @@ func (pt *PortTable) getPortTableCacheFromPodEndpointIndex(index string) (*NodeP
 	return objs[0].(*NodePortData), true
 }
 
-func (pt *PortTable) getPortTableCacheFromPodIPIndex(index string) ([]*NodePortData, bool) {
+func (pt *PortTable) getPortTableCacheFromPodKeyIndex(index string) ([]*NodePortData, bool) {
 	var npData []*NodePortData
-	objs, _ := pt.PortTableCache.ByIndex(PodIPIndex, index)
+	objs, _ := pt.PortTableCache.ByIndex(PodKeyIndex, index)
 	if len(objs) == 0 {
 		return nil, false
 	}
@@ -133,13 +135,13 @@ func NodePortIndexFunc(obj interface{}) ([]string, error) {
 
 func PodEndpointIndexFunc(obj interface{}) ([]string, error) {
 	npData := obj.(*NodePortData)
-	podEndpointTuple := podIPPortProtoFormat(npData.PodIP, npData.PodPort, npData.Protocol.Protocol)
+	podEndpointTuple := podKeyPortProtoFormat(npData.PodKey, npData.PodPort, npData.Protocol.Protocol)
 	return []string{podEndpointTuple}, nil
 }
 
-func PodIPIndexFunc(obj interface{}) ([]string, error) {
+func PodKeyIndexFunc(obj interface{}) ([]string, error) {
 	npData := obj.(*NodePortData)
-	return []string{npData.PodIP}, nil
+	return []string{npData.PodKey}, nil
 }
 
 func NewPortTable(start, end int) (*PortTable, error) {
@@ -147,7 +149,7 @@ func NewPortTable(start, end int) (*PortTable, error) {
 		PortTableCache: cache.NewIndexer(GetPortTableKey, cache.Indexers{
 			NodePortIndex:    NodePortIndexFunc,
 			PodEndpointIndex: PodEndpointIndexFunc,
-			PodIPIndex:       PodIPIndexFunc,
+			PodKeyIndex:      PodKeyIndexFunc,
 		}),
 		StartPort:       start,
 		EndPort:         end,
@@ -167,48 +169,48 @@ func (pt *PortTable) CleanupAllEntries() {
 	pt.releaseDataFromPortTableCache()
 }
 
-func (pt *PortTable) GetDataForPodIP(ip string) []*NodePortData {
+func (pt *PortTable) GetDataForPod(podKey string) []*NodePortData {
 	pt.tableLock.RLock()
 	defer pt.tableLock.RUnlock()
-	return pt.getDataForPodIP(ip)
+	return pt.getDataForPod(podKey)
 }
 
-func (pt *PortTable) getDataForPodIP(ip string) []*NodePortData {
-	allData, exist := pt.getPortTableCacheFromPodIPIndex(ip)
+func (pt *PortTable) getDataForPod(podKey string) []*NodePortData {
+	allData, exist := pt.getPortTableCacheFromPodKeyIndex(podKey)
 	if exist == false {
 		return nil
 	}
 	return allData
 }
 
-func (pt *PortTable) GetEntry(ip string, port int, protocol string) *NodePortData {
+func (pt *PortTable) GetEntry(podKey string, port int, protocol string) *NodePortData {
 	pt.tableLock.RLock()
 	defer pt.tableLock.RUnlock()
 	// Return pointer to copy of data from the PodEndpointTable.
-	if data := pt.getEntryByPodIPPortProto(ip, port, protocol); data != nil {
+	if data := pt.getEntryByPodKeyPortProto(podKey, port, protocol); data != nil {
 		dataCopy := *data
 		return &dataCopy
 	}
 	return nil
 }
 
-// podIPPortProtoFormat formats the ip, port and protocol to string ip:port:protocol.
-func podIPPortProtoFormat(ip string, port int, protocol string) string {
-	return fmt.Sprintf("%s:%d:%s", ip, port, protocol)
+// podKeyPortProtoFormat formats the podKey, port and protocol to string key:port:protocol.
+func podKeyPortProtoFormat(podKey string, port int, protocol string) string {
+	return fmt.Sprintf("%s:%d:%s", podKey, port, protocol)
 }
 
-func (pt *PortTable) getEntryByPodIPPortProto(ip string, port int, protocol string) *NodePortData {
-	data, ok := pt.getPortTableCacheFromPodEndpointIndex(podIPPortProtoFormat(ip, port, protocol))
+func (pt *PortTable) getEntryByPodKeyPortProto(podKey string, port int, protocol string) *NodePortData {
+	data, ok := pt.getPortTableCacheFromPodEndpointIndex(podKeyPortProtoFormat(podKey, port, protocol))
 	if !ok {
 		return nil
 	}
 	return data
 }
 
-func (pt *PortTable) RuleExists(podIP string, podPort int, protocol string) bool {
+func (pt *PortTable) RuleExists(podKey string, podPort int, protocol string) bool {
 	pt.tableLock.RLock()
 	defer pt.tableLock.RUnlock()
-	data := pt.getEntryByPodIPPortProto(podIP, podPort, protocol)
+	data := pt.getEntryByPodKeyPortProto(podKey, podPort, protocol)
 	return data != nil
 }
 
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_others.go b/pkg/agent/nodeportlocal/portcache/port_table_others.go
index c13eaf1d7..e8c55ce91 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_others.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_others.go
@@ -70,10 +70,10 @@ func (pt *PortTable) getFreePort(podIP string, podPort int, protocol string) (in
 	return 0, ProtocolSocketData{}, fmt.Errorf("no free port found")
 }
 
-func (pt *PortTable) AddRule(podIP string, podPort int, protocol string) (int, error) {
+func (pt *PortTable) AddRule(podKey string, podPort int, protocol string, podIP string) (int, error) {
 	pt.tableLock.Lock()
 	defer pt.tableLock.Unlock()
-	npData := pt.getEntryByPodIPPortProto(podIP, podPort, protocol)
+	npData := pt.getEntryByPodKeyPortProto(podKey, podPort, protocol)
 	exists := (npData != nil)
 	if !exists {
 		nodePort, protocolData, err := pt.getFreePort(podIP, podPort, protocol)
@@ -81,6 +81,7 @@ func (pt *PortTable) AddRule(podIP string, podPort int, protocol string) (int, e
 			return 0, err
 		}
 		npData = &NodePortData{
+			PodKey:   podKey,
 			NodePort: nodePort,
 			PodIP:    podIP,
 			PodPort:  podPort,
@@ -125,10 +126,10 @@ func (pt *PortTable) deleteRule(data *NodePortData) error {
 	return nil
 }
 
-func (pt *PortTable) DeleteRule(podIP string, podPort int, protocol string) error {
+func (pt *PortTable) DeleteRule(podKey string, podPort int, protocol string) error {
 	pt.tableLock.Lock()
 	defer pt.tableLock.Unlock()
-	data := pt.getEntryByPodIPPortProto(podIP, podPort, protocol)
+	data := pt.getEntryByPodKeyPortProto(podKey, podPort, protocol)
 	if data == nil {
 		// Delete not required when the PortTable entry does not exist
 		return nil
@@ -136,10 +137,10 @@ func (pt *PortTable) DeleteRule(podIP string, podPort int, protocol string) erro
 	return pt.deleteRule(data)
 }
 
-func (pt *PortTable) DeleteRulesForPod(podIP string) error {
+func (pt *PortTable) DeleteRulesForPod(podKey string) error {
 	pt.tableLock.Lock()
 	defer pt.tableLock.Unlock()
-	podEntries := pt.getDataForPodIP(podIP)
+	podEntries := pt.getDataForPod(podKey)
 	for _, podEntry := range podEntries {
 		return pt.deleteRule(podEntry)
 	}
@@ -155,6 +156,7 @@ func (pt *PortTable) syncRules() error {
 	for _, obj := range objs {
 		npData := obj.(*NodePortData)
 		nplPorts = append(nplPorts, rules.PodNodePort{
+			PodKey:   npData.PodKey,
 			NodePort: npData.NodePort,
 			PodPort:  npData.PodPort,
 			PodIP:    npData.PodIP,
@@ -185,6 +187,7 @@ func (pt *PortTable) RestoreRules(allNPLPorts []rules.PodNodePort, synced chan<-
 		}
 
 		npData := &NodePortData{
+			PodKey:   nplPort.PodKey,
 			NodePort: nplPort.NodePort,
 			PodPort:  nplPort.PodPort,
 			PodIP:    nplPort.PodIP,
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_windows.go b/pkg/agent/nodeportlocal/portcache/port_table_windows.go
index e084bde18..5190987a0 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_windows.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_windows.go
@@ -69,10 +69,10 @@ func (pt *PortTable) addRuleforFreePort(podIP string, podPort int, protocol stri
 	return 0, ProtocolSocketData{}, fmt.Errorf("no free port found")
 }
 
-func (pt *PortTable) AddRule(podIP string, podPort int, protocol string) (int, error) {
+func (pt *PortTable) AddRule(podKey string, podPort int, protocol string, podIP string) (int, error) {
 	pt.tableLock.Lock()
 	defer pt.tableLock.Unlock()
-	npData := pt.getEntryByPodIPPortProto(podIP, podPort, protocol)
+	npData := pt.getEntryByPodKeyPortProto(podKey, podPort, protocol)
 	exists := (npData != nil)
 	if !exists {
 		nodePort, protocolData, err := pt.addRuleforFreePort(podIP, podPort, protocol)
@@ -81,6 +81,7 @@ func (pt *PortTable) AddRule(podIP string, podPort int, protocol string) (int, e
 			return 0, err
 		}
 		npData = &NodePortData{
+			PodKey:   podKey,
 			NodePort: nodePort,
 			PodIP:    podIP,
 			PodPort:  podPort,
@@ -110,6 +111,7 @@ func (pt *PortTable) RestoreRules(allNPLPorts []rules.PodNodePort, synced chan<-
 		}
 
 		npData := &NodePortData{
+			PodKey:   nplPort.PodKey,
 			NodePort: nplPort.NodePort,
 			PodPort:  nplPort.PodPort,
 			PodIP:    nplPort.PodIP,
@@ -122,10 +124,10 @@ func (pt *PortTable) RestoreRules(allNPLPorts []rules.PodNodePort, synced chan<-
 	return nil
 }
 
-func (pt *PortTable) DeleteRule(podIP string, podPort int, protocol string) error {
+func (pt *PortTable) DeleteRule(podKey string, podPort int, protocol string) error {
 	pt.tableLock.Lock()
 	defer pt.tableLock.Unlock()
-	data := pt.getEntryByPodIPPortProto(podIP, podPort, protocol)
+	data := pt.getEntryByPodKeyPortProto(podKey, podPort, protocol)
 	if data == nil {
 		// Delete not required when the PortTable entry does not exist
 		return nil
@@ -133,20 +135,20 @@ func (pt *PortTable) DeleteRule(podIP string, podPort int, protocol string) erro
 
 	data.defunct = true
 	// Calling DeleteRule is idempotent.
-	if err := pt.PodPortRules.DeleteRule(data.NodePort, podIP, podPort, protocol); err != nil {
+	if err := pt.PodPortRules.DeleteRule(data.NodePort, data.PodIP, podPort, protocol); err != nil {
 		return err
 	}
 	pt.deletePortTableCache(data)
 	return nil
 }
 
-func (pt *PortTable) DeleteRulesForPod(podIP string) error {
+func (pt *PortTable) DeleteRulesForPod(podKey string) error {
 	pt.tableLock.Lock()
 	defer pt.tableLock.Unlock()
-	podEntries := pt.getDataForPodIP(podIP)
+	podEntries := pt.getDataForPod(podKey)
 	for _, podEntry := range podEntries {
 		protocolSocketData := podEntry.Protocol
-		if err := pt.PodPortRules.DeleteRule(podEntry.NodePort, podIP, podEntry.PodPort, protocolSocketData.Protocol); err != nil {
+		if err := pt.PodPortRules.DeleteRule(podEntry.NodePort, podEntry.PodIP, podEntry.PodPort, protocolSocketData.Protocol); err != nil {
 			return err
 		}
 		pt.deletePortTableCache(podEntry)
diff --git a/pkg/agent/nodeportlocal/rules/types.go b/pkg/agent/nodeportlocal/rules/types.go
index 29768957d..c242a71c4 100644
--- a/pkg/agent/nodeportlocal/rules/types.go
+++ b/pkg/agent/nodeportlocal/rules/types.go
@@ -14,8 +14,10 @@
 
 package rules
 
-// PodNodePort contains the Node Port, Pod IP, Pod Port and Protocols for NodePortLocal.
+// PodNodePort contains the Pod namespaced name, Node Port, Pod IP, Pod Port and Protocol for NodePortLocal.
 type PodNodePort struct {
+	// PodKey is the namespaced name of the Pod.
+	PodKey   string
 	NodePort int
 	PodPort  int
 	PodIP    string
diff --git a/pkg/agent/proxy/metrics/metrics.go b/pkg/agent/proxy/metrics/metrics.go
index a14df1512..ea71dc05e 100644
--- a/pkg/agent/proxy/metrics/metrics.go
+++ b/pkg/agent/proxy/metrics/metrics.go
@@ -37,7 +37,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v4"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "sync_proxy_rules_duration_seconds",
-			Help:           "SyncProxyRules duration of AntreaProxy in seconds",
+			Help:           "SyncProxyRules duration of Antrea Proxy in seconds",
 		},
 	)
 	ServicesInstalledTotal = kmetrics.NewGauge(
@@ -47,7 +47,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v4"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "total_services_installed",
-			Help:           "The number of Services installed by AntreaProxy",
+			Help:           "The number of Services installed by Antrea Proxy",
 		},
 	)
 	EndpointsInstalledTotal = kmetrics.NewGauge(
@@ -57,7 +57,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v4"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "total_endpoints_installed",
-			Help:           "The number of Endpoints installed by AntreaProxy",
+			Help:           "The number of Endpoints installed by Antrea Proxy",
 		},
 	)
 	ServicesUpdatesTotal = kmetrics.NewCounter(
@@ -67,7 +67,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v4"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "total_services_updates",
-			Help:           "The cumulative number of Service updates received by AntreaProxy",
+			Help:           "The cumulative number of Service updates received by Antrea Proxy",
 		},
 	)
 	EndpointsUpdatesTotal = kmetrics.NewCounter(
@@ -77,7 +77,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v4"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "total_endpoints_updates",
-			Help:           "The cumulative number of Endpoint updates received by AntreaProxy",
+			Help:           "The cumulative number of Endpoint updates received by Antrea Proxy",
 		},
 	)
 
@@ -88,7 +88,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v6"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "sync_proxy_rules_duration_seconds",
-			Help:           "SyncProxyRules duration of AntreaProxy in seconds",
+			Help:           "SyncProxyRules duration of Antrea Proxy in seconds",
 		},
 	)
 	ServicesInstalledTotalV6 = kmetrics.NewGauge(
@@ -98,7 +98,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v6"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "total_services_installed",
-			Help:           "The number of Services installed by AntreaProxy",
+			Help:           "The number of Services installed by Antrea Proxy",
 		},
 	)
 	EndpointsInstalledTotalV6 = kmetrics.NewGauge(
@@ -108,7 +108,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v6"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "total_endpoints_installed",
-			Help:           "The number of Endpoints installed by AntreaProxy",
+			Help:           "The number of Endpoints installed by Antrea Proxy",
 		},
 	)
 	ServicesUpdatesTotalV6 = kmetrics.NewCounter(
@@ -118,7 +118,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v6"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "total_services_updates",
-			Help:           "The cumulative number of Service updates received by AntreaProxy",
+			Help:           "The cumulative number of Service updates received by Antrea Proxy",
 		},
 	)
 	EndpointsUpdatesTotalV6 = kmetrics.NewCounter(
@@ -128,7 +128,7 @@ var (
 			ConstLabels:    map[string]string{"ip_family": "v6"},
 			StabilityLevel: kmetrics.ALPHA,
 			Name:           "total_endpoints_updates",
-			Help:           "The cumulative number of Endpoint updates received by AntreaProxy",
+			Help:           "The cumulative number of Endpoint updates received by Antrea Proxy",
 		},
 	)
 )
diff --git a/pkg/agent/proxy/proxier.go b/pkg/agent/proxy/proxier.go
index f1ba21222..e38ce95f5 100644
--- a/pkg/agent/proxy/proxier.go
+++ b/pkg/agent/proxy/proxier.go
@@ -32,7 +32,6 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/selection"
 	apimachinerytypes "k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/sets"
 	coreinformers "k8s.io/client-go/informers/core/v1"
 	discoveryinformers "k8s.io/client-go/informers/discovery/v1"
 	clientset "k8s.io/client-go/kubernetes"
@@ -121,13 +120,6 @@ type proxier struct {
 	serviceHealthServer healthcheck.ServiceHealthServer
 	numLocalEndpoints   map[apimachinerytypes.NamespacedName]int
 
-	// serviceIPRouteReferences tracks the references of Service IP routes. The key is the Service IP and the value is
-	// the set of ServiceInfo strings. Because a Service could have multiple ports and each port will generate a
-	// ServicePort (which is the unit of the processing), a Service IP route may be required by several ServicePorts.
-	// With the references, we install a route exactly once as long as it's used by any ServicePorts and uninstall it
-	// exactly once when it's no longer used by any ServicePorts.
-	// It applies to ClusterIP and LoadBalancerIP.
-	serviceIPRouteReferences map[string]sets.Set[string]
 	// syncedOnce returns true if the proxier has synced rules at least once.
 	syncedOnce      bool
 	syncedOnceMutex sync.RWMutex
@@ -569,10 +561,10 @@ func (p *proxier) installNodePortService(localGroupID, clusterGroupID binding.Gr
 		IsNested:           false, // Unsupported for NodePort
 		IsDSR:              false, // Unsupported because external traffic has been DNAT'd in host network before it's forwarded to OVS.
 	}); err != nil {
-		return fmt.Errorf("failed to install NodePort load balancing flows: %w", err)
+		return fmt.Errorf("failed to install NodePort load balancing OVS flows: %w", err)
 	}
-	if err := p.routeClient.AddNodePort(p.nodePortAddresses, svcPort, protocol); err != nil {
-		return fmt.Errorf("failed to install NodePort traffic redirecting rules: %w", err)
+	if err := p.routeClient.AddNodePortConfigs(p.nodePortAddresses, svcPort, protocol); err != nil {
+		return fmt.Errorf("failed to install NodePort traffic redirecting routing configurations: %w", err)
 	}
 	return nil
 }
@@ -588,8 +580,8 @@ func (p *proxier) uninstallNodePortService(svcPort uint16, protocol binding.Prot
 	if err := p.ofClient.UninstallServiceFlows(svcIP, svcPort, protocol); err != nil {
 		return fmt.Errorf("failed to remove NodePort load balancing flows: %w", err)
 	}
-	if err := p.routeClient.DeleteNodePort(p.nodePortAddresses, svcPort, protocol); err != nil {
-		return fmt.Errorf("failed to remove NodePort traffic redirecting rules: %w", err)
+	if err := p.routeClient.DeleteNodePortConfigs(p.nodePortAddresses, svcPort, protocol); err != nil {
+		return fmt.Errorf("failed to remove NodePort traffic redirecting routing configurations: %w", err)
 	}
 	return nil
 }
@@ -618,10 +610,10 @@ func (p *proxier) installExternalIPService(svcInfoStr string,
 			IsNested:           false, // Unsupported for ExternalIP
 			IsDSR:              features.DefaultFeatureGate.Enabled(features.LoadBalancerModeDSR) && loadBalancerMode == agentconfig.LoadBalancerModeDSR,
 		}); err != nil {
-			return fmt.Errorf("failed to install ExternalIP load balancing flows: %w", err)
+			return fmt.Errorf("failed to install ExternalIP load balancing OVS flows: %w", err)
 		}
-		if err := p.addRouteForServiceIP(svcInfoStr, ip, p.routeClient.AddExternalIPRoute); err != nil {
-			return fmt.Errorf("failed to install ExternalIP traffic redirecting routes: %w", err)
+		if err := p.routeClient.AddExternalIPConfigs(svcInfoStr, ip); err != nil {
+			return fmt.Errorf("failed to install ExternalIP load balancing routing configurations: %w", err)
 		}
 	}
 	return nil
@@ -631,10 +623,10 @@ func (p *proxier) uninstallExternalIPService(svcInfoStr string, externalIPString
 	for _, externalIP := range externalIPStrings {
 		ip := net.ParseIP(externalIP)
 		if err := p.ofClient.UninstallServiceFlows(ip, svcPort, protocol); err != nil {
-			return fmt.Errorf("failed to remove ExternalIP load balancing flows: %w", err)
+			return fmt.Errorf("failed to remove ExternalIP load balancing OVS flows: %w", err)
 		}
-		if err := p.deleteRouteForServiceIP(svcInfoStr, ip, p.routeClient.DeleteExternalIPRoute); err != nil {
-			return fmt.Errorf("failed to remove ExternalIP traffic redirecting routes: %w", err)
+		if err := p.routeClient.DeleteExternalIPConfigs(svcInfoStr, ip); err != nil {
+			return fmt.Errorf("failed to remove ExternalIP traffic redirecting routing configurations: %w", err)
 		}
 	}
 	return nil
@@ -665,11 +657,11 @@ func (p *proxier) installLoadBalancerService(svcInfoStr string,
 				IsNested:           false, // Unsupported for LoadBalancerIP
 				IsDSR:              features.DefaultFeatureGate.Enabled(features.LoadBalancerModeDSR) && loadBalancerMode == agentconfig.LoadBalancerModeDSR,
 			}); err != nil {
-				return fmt.Errorf("failed to install LoadBalancer load balancing flows: %w", err)
+				return fmt.Errorf("failed to install LoadBalancerIP load balancing OVS flows: %w", err)
 			}
 			if p.proxyAll {
-				if err := p.addRouteForServiceIP(svcInfoStr, ip, p.routeClient.AddExternalIPRoute); err != nil {
-					return fmt.Errorf("failed to install LoadBalancer traffic redirecting routes: %w", err)
+				if err := p.routeClient.AddExternalIPConfigs(svcInfoStr, ip); err != nil {
+					return fmt.Errorf("failed to install LoadBalancerIP traffic redirecting routing configurations: %w", err)
 				}
 			}
 		}
@@ -677,33 +669,16 @@ func (p *proxier) installLoadBalancerService(svcInfoStr string,
 	return nil
 }
 
-func (p *proxier) addRouteForServiceIP(svcInfoStr string, ip net.IP, addRouteFn func(net.IP) error) error {
-	ipStr := ip.String()
-	references, exists := p.serviceIPRouteReferences[ipStr]
-	// If the IP was not referenced by any Service port, install a route for it.
-	// Otherwise, just reference it.
-	if !exists {
-		if err := addRouteFn(ip); err != nil {
-			return err
-		}
-		references = sets.New[string](svcInfoStr)
-		p.serviceIPRouteReferences[ipStr] = references
-	} else {
-		references.Insert(svcInfoStr)
-	}
-	return nil
-}
-
 func (p *proxier) uninstallLoadBalancerService(svcInfoStr string, loadBalancerIPStrings []string, svcPort uint16, protocol binding.Protocol) error {
 	for _, ingress := range loadBalancerIPStrings {
 		if ingress != "" {
 			ip := net.ParseIP(ingress)
 			if err := p.ofClient.UninstallServiceFlows(ip, svcPort, protocol); err != nil {
-				return fmt.Errorf("failed to remove LoadBalancer load balancing flows: %w", err)
+				return fmt.Errorf("failed to remove LoadBalancerIP load balancing OVS flows: %w", err)
 			}
 			if p.proxyAll {
-				if err := p.deleteRouteForServiceIP(svcInfoStr, ip, p.routeClient.DeleteExternalIPRoute); err != nil {
-					return fmt.Errorf("failed to remove LoadBalancer traffic redirecting routes: %w", err)
+				if err := p.routeClient.DeleteExternalIPConfigs(svcInfoStr, ip); err != nil {
+					return fmt.Errorf("failed to remove LoadBalancerIP traffic redirecting routing configurations: %w", err)
 				}
 			}
 		}
@@ -711,25 +686,6 @@ func (p *proxier) uninstallLoadBalancerService(svcInfoStr string, loadBalancerIP
 	return nil
 }
 
-func (p *proxier) deleteRouteForServiceIP(svcInfoStr string, ip net.IP, deleteRouteFn func(net.IP) error) error {
-	ipStr := ip.String()
-	references, exists := p.serviceIPRouteReferences[ipStr]
-	// If the IP was not referenced by this Service port, skip it.
-	if exists && references.Has(svcInfoStr) {
-		// Delete the IP only if this Service port is the last one referencing it.
-		// Otherwise, just dereference it.
-		if references.Len() == 1 {
-			if err := deleteRouteFn(ip); err != nil {
-				return err
-			}
-			delete(p.serviceIPRouteReferences, ipStr)
-		} else {
-			references.Delete(svcInfoStr)
-		}
-	}
-	return nil
-}
-
 func (p *proxier) installServices() {
 	for svcPortName, svcPort := range p.serviceMap {
 		svcInfo := svcPort.(*types.ServiceInfo)
@@ -1454,7 +1410,6 @@ func newProxier(
 		endpointsInstalledMap:       types.EndpointsMap{},
 		endpointsMap:                types.EndpointsMap{},
 		endpointReferenceCounter:    map[string]int{},
-		serviceIPRouteReferences:    map[string]sets.Set[string]{},
 		nodeLabels:                  map[string]string{},
 		serviceStringMap:            map[string]k8sproxy.ServicePortName{},
 		groupCounter:                groupCounter,
diff --git a/pkg/agent/route/interfaces.go b/pkg/agent/route/interfaces.go
index 5355efbf3..dac45c274 100644
--- a/pkg/agent/route/interfaces.go
+++ b/pkg/agent/route/interfaces.go
@@ -76,17 +76,19 @@ type Interface interface {
 	// DeleteEgressRule deletes the IP rule installed by AddEgressRule.
 	DeleteEgressRule(tableID uint32, mark uint32) error
 
-	// AddNodePort adds configurations when a NodePort Service is created.
-	AddNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error
+	// AddNodePortConfigs adds routing configurations for redirecting traffic to OVS when a NodePort Service is created.
+	AddNodePortConfigs(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error
 
-	// DeleteNodePort deletes related configurations when a NodePort Service is deleted.
-	DeleteNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error
+	// DeleteNodePortConfigs deletes corresponding routing configurations when a NodePort Service is deleted.
+	DeleteNodePortConfigs(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error
 
-	// AddExternalIPRoute adds a route entry when an external IP is added.
-	AddExternalIPRoute(externalIP net.IP) error
+	// AddExternalIPConfigs adds routing configurations for redirecting traffic to OVS when an external Service IP
+	// (externalIP or LoadBalancerIP) is created.
+	AddExternalIPConfigs(svcInfoStr string, externalIP net.IP) error
 
-	// DeleteExternalIPRoute deletes the related route entry when an external IP is deleted.
-	DeleteExternalIPRoute(externalIP net.IP) error
+	// DeleteExternalIPConfigs deletes corresponding routing configurations for redirecting traffic to OVS when an
+	// external Service IP (externalIP or LoadBalancerIP) is deleted.
+	DeleteExternalIPConfigs(svcInfoStr string, externalIP net.IP) error
 
 	// Run starts the sync loop.
 	Run(stopCh <-chan struct{})
diff --git a/pkg/agent/route/route_linux.go b/pkg/agent/route/route_linux.go
index ac61dcd2d..ced19f82e 100644
--- a/pkg/agent/route/route_linux.go
+++ b/pkg/agent/route/route_linux.go
@@ -21,6 +21,7 @@ import (
 	"reflect"
 	"sort"
 	"strconv"
+	"strings"
 	"sync"
 	"time"
 
@@ -66,9 +67,11 @@ const (
 	// clusterNodeIP6Set contains all other Node IP6s in the cluster.
 	clusterNodeIP6Set = "CLUSTER-NODE-IP6"
 
-	// Antrea proxy NodePort IP
-	antreaNodePortIPSet  = "ANTREA-NODEPORT-IP"
-	antreaNodePortIP6Set = "ANTREA-NODEPORT-IP6"
+	// Antrea managed ipsets for different types of Service IP addresses and ports.
+	antreaNodePortIPSet    = "ANTREA-NODEPORT-IP"
+	antreaNodePortIP6Set   = "ANTREA-NODEPORT-IP6"
+	antreaExternalIPIPSet  = "ANTREA-EXTERNAL-IP"
+	antreaExternalIPIP6Set = "ANTREA-EXTERNAL-IP6"
 
 	// Antrea managed iptables chains.
 	antreaForwardChain     = "ANTREA-FORWARD"
@@ -78,6 +81,8 @@ const (
 	antreaOutputChain      = "ANTREA-OUTPUT"
 	antreaMangleChain      = "ANTREA-MANGLE"
 
+	kubeProxyServiceChain = "KUBE-SERVICES"
+
 	serviceIPv4CIDRKey = "serviceIPv4CIDRKey"
 	serviceIPv6CIDRKey = "serviceIPv6CIDRKey"
 
@@ -121,12 +126,17 @@ type Client struct {
 	nodeNetworkPolicyEnabled bool
 	// serviceRoutes caches ip routes about Services.
 	serviceRoutes sync.Map
+	// serviceExternalIPReferences tracks the references of Service IP. The key is the Service IP and the value is
+	// the set of ServiceInfo strings. Because a Service could have multiple ports and each port will generate a
+	// ServicePort (which is the unit of the processing), a Service IP route may be required by several ServicePorts.
+	// With the references, we install the configurations for a Service IP exactly once as long as it's used by any
+	// ServicePorts and uninstall it exactly once when it's no longer used by any ServicePorts.
+	// It applies to externalIP and LoadBalancerIP.
+	serviceExternalIPReferences map[string]sets.Set[string]
 	// serviceNeighbors caches neighbors.
 	serviceNeighbors sync.Map
-	// nodePortsIPv4 caches all existing IPv4 NodePorts.
-	nodePortsIPv4 sync.Map
-	// nodePortsIPv6 caches all existing IPv6 NodePorts.
-	nodePortsIPv6 sync.Map
+	// serviceIPSets caches ipsets about Services.
+	serviceIPSets map[string]*sync.Map
 	// clusterNodeIPs stores the IPv4 of all other Nodes in the cluster
 	clusterNodeIPs sync.Map
 	// clusterNodeIP6s stores the IPv6 address of all other Nodes in the cluster. It is maintained but not consumed
@@ -159,16 +169,23 @@ func NewClient(networkConfig *config.NetworkConfig,
 	multicastEnabled bool,
 	serviceCIDRProvider servicecidr.Interface) (*Client, error) {
 	return &Client{
-		networkConfig:            networkConfig,
-		noSNAT:                   noSNAT,
-		proxyAll:                 proxyAll,
-		multicastEnabled:         multicastEnabled,
-		connectUplinkToBridge:    connectUplinkToBridge,
-		nodeNetworkPolicyEnabled: nodeNetworkPolicyEnabled,
-		ipset:                    ipset.NewClient(),
-		netlink:                  &netlink.Handle{},
-		isCloudEKS:               env.IsCloudEKS(),
-		serviceCIDRProvider:      serviceCIDRProvider,
+		networkConfig:               networkConfig,
+		noSNAT:                      noSNAT,
+		proxyAll:                    proxyAll,
+		multicastEnabled:            multicastEnabled,
+		connectUplinkToBridge:       connectUplinkToBridge,
+		nodeNetworkPolicyEnabled:    nodeNetworkPolicyEnabled,
+		ipset:                       ipset.NewClient(),
+		netlink:                     &netlink.Handle{},
+		isCloudEKS:                  env.IsCloudEKS(),
+		serviceCIDRProvider:         serviceCIDRProvider,
+		serviceExternalIPReferences: make(map[string]sets.Set[string]),
+		serviceIPSets: map[string]*sync.Map{
+			antreaNodePortIPSet:    {},
+			antreaNodePortIP6Set:   {},
+			antreaExternalIPIPSet:  {},
+			antreaExternalIPIP6Set: {},
+		},
 	}, nil
 }
 
@@ -388,27 +405,27 @@ func (c *Client) syncIPSet() error {
 	// AntreaProxy proxyAll is available in all traffic modes. If proxyAll is enabled, create the ipsets to store the
 	// pairs of Node IP and NodePort.
 	if c.proxyAll {
-		if err := c.ipset.CreateIPSet(antreaNodePortIPSet, ipset.HashIPPort, false); err != nil {
-			return err
-		}
-		if err := c.ipset.CreateIPSet(antreaNodePortIP6Set, ipset.HashIPPort, true); err != nil {
-			return err
-		}
+		for ipsetName, ipsetEntries := range c.serviceIPSets {
+			isIPv6 := ipsetName == antreaNodePortIP6Set || ipsetName == antreaExternalIPIP6Set
 
-		c.nodePortsIPv4.Range(func(k, _ interface{}) bool {
-			ipSetEntry := k.(string)
-			if err := c.ipset.AddEntry(antreaNodePortIPSet, ipSetEntry); err != nil {
-				return false
+			var ipsetType ipset.SetType
+			if ipsetName == antreaNodePortIP6Set || ipsetName == antreaNodePortIPSet {
+				ipsetType = ipset.HashIPPort
+			} else {
+				ipsetType = ipset.HashIP
 			}
-			return true
-		})
-		c.nodePortsIPv6.Range(func(k, _ interface{}) bool {
-			ipSetEntry := k.(string)
-			if err := c.ipset.AddEntry(antreaNodePortIP6Set, ipSetEntry); err != nil {
-				return false
+
+			if err := c.ipset.CreateIPSet(ipsetName, ipsetType, isIPv6); err != nil {
+				return err
 			}
-			return true
-		})
+			ipsetEntries.Range(func(k, _ interface{}) bool {
+				ipsetEntry := k.(string)
+				if err := c.ipset.AddEntry(ipsetName, ipsetEntry); err != nil {
+					return false
+				}
+				return true
+			})
+		}
 	}
 
 	// AntreaIPAM is available in noEncap mode. There is a validation in Antrea configuration about this traffic mode
@@ -494,6 +511,14 @@ func getNodePortIPSetName(isIPv6 bool) string {
 	}
 }
 
+func getExternalIPIPSetName(isIPv6 bool) string {
+	if isIPv6 {
+		return antreaExternalIPIP6Set
+	} else {
+		return antreaExternalIPIPSet
+	}
+}
+
 func getLocalAntreaFlexibleIPAMPodIPSetName(isIPv6 bool) string {
 	if isIPv6 {
 		return localAntreaFlexibleIPAMPodIP6Set
@@ -562,43 +587,101 @@ func (c *Client) writeEKSNATRules(iptablesData *bytes.Buffer) {
 	}...)
 }
 
+func (c *Client) getIPProtocol() iptables.Protocol {
+	switch {
+	case c.networkConfig.IPv4Enabled && c.networkConfig.IPv6Enabled:
+		return iptables.ProtocolDual
+	case c.networkConfig.IPv6Enabled:
+		return iptables.ProtocolIPv6
+	default:
+		return iptables.ProtocolIPv4
+	}
+}
+
+// Create the antrea managed chains and link them to built-in chains.
+// We cannot use iptables-restore for these jump rules because there
+// are non antrea managed rules in built-in chains.
+type jumpRule struct {
+	table    string
+	srcChain string
+	dstChain string
+	comment  string
+	insert   bool
+}
+
+func (c *Client) removeUnexpectedAntreaJumpRule(protocol iptables.Protocol, jumpRule jumpRule) error {
+	// List all the existing rules of the table and the chain where the Antrea jump rule will be added.
+	allExistingRules, err := c.iptables.ListRules(protocol, jumpRule.table, jumpRule.srcChain)
+	if err != nil {
+		return err
+	}
+
+	// Construct keywords to identify Antrea and kube-proxy jump rules.
+	antreaJumpRuleKeyword := fmt.Sprintf("-j %s", jumpRule.dstChain)
+	kubeProxyJumpRuleKeyword := fmt.Sprintf("-j %s", kubeProxyServiceChain)
+
+	for ipProtocol, rules := range allExistingRules {
+		var antreaJumpRuleIndex, kubeProxyJumpRuleIndex = -1, -1
+
+		for index, rule := range rules {
+			// Check if the current rule is the Antrea jump rule to be added.
+			if strings.Contains(rule, antreaJumpRuleKeyword) {
+				antreaJumpRuleIndex = index
+				// Check if the current rule is the kube-proxy jump rule.
+			} else if strings.Contains(rule, kubeProxyJumpRuleKeyword) {
+				kubeProxyJumpRuleIndex = index
+			}
+		}
+		// If the Antrea jump rule is installed after the kube-proxy jump rule, which is not expected, delete the
+		// existing Antrea jump rule to ensure that a new one will be installed before the kube-proxy one when syncing iptables.
+		if antreaJumpRuleIndex != -1 && kubeProxyJumpRuleIndex != -1 && antreaJumpRuleIndex > kubeProxyJumpRuleIndex {
+			ruleSpec := []string{"-j", jumpRule.dstChain, "-m", "comment", "--comment", jumpRule.comment}
+			if err := c.iptables.DeleteRule(ipProtocol, jumpRule.table, jumpRule.srcChain, ruleSpec); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
 // syncIPTables ensure that the iptables infrastructure we use is set up.
 // It's idempotent and can safely be called on every startup.
 func (c *Client) syncIPTables() error {
-	// Create the antrea managed chains and link them to built-in chains.
-	// We cannot use iptables-restore for these jump rules because there
-	// are non antrea managed rules in built-in chains.
-	type jumpRule struct {
-		table    string
-		srcChain string
-		dstChain string
-		comment  string
-	}
+	ipProtocol := c.getIPProtocol()
 	jumpRules := []jumpRule{
-		{iptables.RawTable, iptables.PreRoutingChain, antreaPreRoutingChain, "Antrea: jump to Antrea prerouting rules"},
-		{iptables.RawTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules"},
-		{iptables.FilterTable, iptables.ForwardChain, antreaForwardChain, "Antrea: jump to Antrea forwarding rules"},
-		{iptables.NATTable, iptables.PostRoutingChain, antreaPostRoutingChain, "Antrea: jump to Antrea postrouting rules"},
-		{iptables.MangleTable, iptables.PreRoutingChain, antreaMangleChain, "Antrea: jump to Antrea mangle rules"}, // TODO: unify the chain naming style
-		{iptables.MangleTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules"},
+		{iptables.RawTable, iptables.PreRoutingChain, antreaPreRoutingChain, "Antrea: jump to Antrea prerouting rules", false},
+		{iptables.RawTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules", false},
+		{iptables.FilterTable, iptables.ForwardChain, antreaForwardChain, "Antrea: jump to Antrea forwarding rules", false},
+		{iptables.NATTable, iptables.PostRoutingChain, antreaPostRoutingChain, "Antrea: jump to Antrea postrouting rules", false},
+		{iptables.MangleTable, iptables.PreRoutingChain, antreaMangleChain, "Antrea: jump to Antrea mangle rules", false}, // TODO: unify the chain naming style
+		{iptables.MangleTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules", false},
 	}
 	if c.proxyAll || c.isCloudEKS {
-		jumpRules = append(jumpRules, jumpRule{iptables.NATTable, iptables.PreRoutingChain, antreaPreRoutingChain, "Antrea: jump to Antrea prerouting rules"})
+		jumpRules = append(jumpRules, jumpRule{iptables.NATTable, iptables.PreRoutingChain, antreaPreRoutingChain, "Antrea: jump to Antrea prerouting rules", c.proxyAll == true})
 	}
 	if c.proxyAll {
-		jumpRules = append(jumpRules, jumpRule{iptables.NATTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules"})
+		jumpRules = append(jumpRules, jumpRule{iptables.NATTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules", true})
 	}
 	if c.nodeNetworkPolicyEnabled {
-		jumpRules = append(jumpRules, jumpRule{iptables.FilterTable, iptables.InputChain, antreaInputChain, "Antrea: jump to Antrea input rules"})
-		jumpRules = append(jumpRules, jumpRule{iptables.FilterTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules"})
+		jumpRules = append(jumpRules, jumpRule{iptables.FilterTable, iptables.InputChain, antreaInputChain, "Antrea: jump to Antrea input rules", false})
+		jumpRules = append(jumpRules, jumpRule{iptables.FilterTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules", false})
 	}
 	for _, rule := range jumpRules {
-		if err := c.iptables.EnsureChain(iptables.ProtocolDual, rule.table, rule.dstChain); err != nil {
+		if err := c.iptables.EnsureChain(ipProtocol, rule.table, rule.dstChain); err != nil {
 			return err
 		}
 		ruleSpec := []string{"-j", rule.dstChain, "-m", "comment", "--comment", rule.comment}
-		if err := c.iptables.AppendRule(iptables.ProtocolDual, rule.table, rule.srcChain, ruleSpec); err != nil {
-			return err
+		if rule.insert {
+			if err := c.removeUnexpectedAntreaJumpRule(ipProtocol, rule); err != nil {
+				return err
+			}
+			if err := c.iptables.InsertRule(ipProtocol, rule.table, rule.srcChain, ruleSpec); err != nil {
+				return err
+			}
+		} else {
+			if err := c.iptables.AppendRule(ipProtocol, rule.table, rule.srcChain, ruleSpec); err != nil {
+				return err
+			}
 		}
 	}
 
@@ -636,6 +719,7 @@ func (c *Client) syncIPTables() error {
 			antreaPodIPSet,
 			localAntreaFlexibleIPAMPodIPSet,
 			antreaNodePortIPSet,
+			antreaExternalIPIPSet,
 			clusterNodeIPSet,
 			config.VirtualNodePortDNATIPv4,
 			config.VirtualServiceIPv4,
@@ -655,6 +739,7 @@ func (c *Client) syncIPTables() error {
 			antreaPodIP6Set,
 			localAntreaFlexibleIPAMPodIP6Set,
 			antreaNodePortIP6Set,
+			antreaExternalIPIP6Set,
 			clusterNodeIP6Set,
 			config.VirtualNodePortDNATIPv6,
 			config.VirtualServiceIPv6,
@@ -674,6 +759,7 @@ func (c *Client) restoreIptablesData(podCIDR *net.IPNet,
 	podIPSet,
 	localAntreaFlexibleIPAMPodIPSet,
 	nodePortIPSet,
+	externalIPSet,
 	clusterNodeIPSet string,
 	nodePortDNATVirtualIP,
 	serviceVirtualIP net.IP,
@@ -731,6 +817,33 @@ func (c *Client) restoreIptablesData(podCIDR *net.IPNet,
 			}...)
 		}
 	}
+
+	if c.proxyAll {
+		// This rule is to bypass conntrack for packets sourced from external and destined to externalIPs, which also
+		// results in bypassing the chains managed by Antrea Proxy and kube-proxy in nat table.
+		writeLine(iptablesData, []string{
+			"-A", antreaPreRoutingChain,
+			"-m", "comment", "--comment", `"Antrea: do not track request packets destined to external IPs"`,
+			"-m", "set", "--match-set", externalIPSet, "dst",
+			"-j", iptables.NotrackTarget,
+		}...)
+		// This rule is to bypass conntrack for packets sourced from externalIPs, which also results in bypassing the
+		// chains managed by Antrea Proxy and kube-proxy in nat table.
+		writeLine(iptablesData, []string{
+			"-A", antreaPreRoutingChain,
+			"-m", "comment", "--comment", `"Antrea: do not track reply packets sourced from external IPs"`,
+			"-m", "set", "--match-set", externalIPSet, "src",
+			"-j", iptables.NotrackTarget,
+		}...)
+		// This rule is to bypass conntrack for packets sourced from local and destined to externalIPs, which also
+		// results in bypassing the chains managed by Antrea Proxy and kube-proxy in nat table.
+		writeLine(iptablesData, []string{
+			"-A", antreaOutputChain,
+			"-m", "comment", "--comment", `"Antrea: do not track request packets destined to external IPs"`,
+			"-m", "set", "--match-set", externalIPSet, "dst",
+			"-j", iptables.NotrackTarget,
+		}...)
+	}
 	writeLine(iptablesData, "COMMIT")
 
 	// Write head lines anyway so the undesired rules can be deleted when noEncap -> encap.
@@ -1614,9 +1727,9 @@ func (c *Client) addVirtualServiceIPRoute(isIPv6 bool) error {
 	return nil
 }
 
-// AddNodePort is used to add IP,port:protocol entries to target ip set when a NodePort Service is added. An entry is added
-// for every NodePort IP.
-func (c *Client) AddNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
+// AddNodePortConfigs is used to add IP,protocol:port entries to target ipset when a NodePort Service is added. An
+// entry is added for every NodePort IP.
+func (c *Client) AddNodePortConfigs(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
 	isIPv6 := isIPv6Protocol(protocol)
 	transProtocol := getTransProtocolStr(protocol)
 	ipSetName := getNodePortIPSetName(isIPv6)
@@ -1626,19 +1739,15 @@ func (c *Client) AddNodePort(nodePortAddresses []net.IP, port uint16, protocol b
 		if err := c.ipset.AddEntry(ipSetName, ipSetEntry); err != nil {
 			return err
 		}
-		if isIPv6 {
-			c.nodePortsIPv6.Store(ipSetEntry, struct{}{})
-		} else {
-			c.nodePortsIPv4.Store(ipSetEntry, struct{}{})
-		}
+		c.serviceIPSets[ipSetName].Store(ipSetEntry, struct{}{})
 		klog.V(4).InfoS("Added ipset for NodePort", "IP", nodePortAddresses[i], "Port", port, "Protocol", protocol)
 	}
 
 	return nil
 }
 
-// DeleteNodePort is used to delete related IP set entries when a NodePort Service is deleted.
-func (c *Client) DeleteNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
+// DeleteNodePortConfigs is used to delete corresponding ipset entries when a NodePort Service is deleted.
+func (c *Client) DeleteNodePortConfigs(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
 	isIPv6 := isIPv6Protocol(protocol)
 	transProtocol := getTransProtocolStr(protocol)
 	ipSetName := getNodePortIPSetName(isIPv6)
@@ -1648,11 +1757,8 @@ func (c *Client) DeleteNodePort(nodePortAddresses []net.IP, port uint16, protoco
 		if err := c.ipset.DelEntry(ipSetName, ipSetEntry); err != nil {
 			return err
 		}
-		if isIPv6 {
-			c.nodePortsIPv6.Delete(ipSetEntry)
-		} else {
-			c.nodePortsIPv4.Delete(ipSetEntry)
-		}
+		c.serviceIPSets[ipSetName].Delete(ipSetEntry)
+		klog.V(4).InfoS("Deleted ipset entry for NodePort IP", "IP", nodePortAddresses[i], "Port", port, "Protocol", protocol)
 	}
 
 	return nil
@@ -1752,11 +1858,19 @@ func (c *Client) addVirtualNodePortDNATIPRoute(isIPv6 bool) error {
 	return nil
 }
 
-// AddExternalIPRoute adds a route entry that forwards traffic destined for the external IP to the Antrea gateway interface.
-func (c *Client) AddExternalIPRoute(externalIP net.IP) error {
+// AddExternalIPConfigs adds a route entry to forward traffic destined for the external Service IP to the Antrea
+// gateway interface. Additionally, it adds the IP to the ipset ANTREA-EXTERNAL-IP or ANTREA-EXTERNAL-IP6, which is
+// used by iptables rules to bypass kube-proxy.
+func (c *Client) AddExternalIPConfigs(svcInfoStr string, externalIP net.IP) error {
 	externalIPStr := externalIP.String()
-	linkIndex := c.nodeConfig.GatewayConfig.LinkIndex
 	isIPv6 := utilnet.IsIPv6(externalIP)
+	references, exists := c.serviceExternalIPReferences[externalIPStr]
+	if exists {
+		references.Insert(svcInfoStr)
+		return nil
+	}
+
+	linkIndex := c.nodeConfig.GatewayConfig.LinkIndex
 	var gw net.IP
 	var mask int
 	if !isIPv6 {
@@ -1766,19 +1880,40 @@ func (c *Client) AddExternalIPRoute(externalIP net.IP) error {
 		gw = config.VirtualServiceIPv6
 		mask = net.IPv6len * 8
 	}
-
 	route := generateRoute(externalIP, mask, gw, linkIndex, netlink.SCOPE_UNIVERSE)
 	if err := c.netlink.RouteReplace(route); err != nil {
-		return fmt.Errorf("failed to install route for external IP %s: %w", externalIPStr, err)
+		return fmt.Errorf("failed to add route for external IP %s: %w", externalIPStr, err)
 	}
-	c.serviceRoutes.Store(externalIPStr, route)
 	klog.V(4).InfoS("Added route for external IP", "IP", externalIPStr)
+
+	ipsetName := getExternalIPIPSetName(isIPv6)
+	if err := c.ipset.AddEntry(ipsetName, externalIPStr); err != nil {
+		return fmt.Errorf("failed to add %s to ipset %s", externalIPStr, ipsetName)
+	}
+	klog.V(4).InfoS("Added external IP to ipset", "IPSet", ipsetName, "IP", externalIPStr)
+
+	references = sets.New[string](svcInfoStr)
+	c.serviceExternalIPReferences[externalIPStr] = references
+	c.serviceRoutes.Store(externalIPStr, route)
+	c.serviceIPSets[ipsetName].Store(externalIPStr, struct{}{})
 	return nil
 }
 
-// DeleteExternalIPRoute deletes the route entry for the external IP.
-func (c *Client) DeleteExternalIPRoute(externalIP net.IP) error {
+// DeleteExternalIPConfigs deletes the route entry to forward traffic destined for the external Service IP to the Antrea
+// gateway interface. Additionally, it removes the IP to the ipset ANTREA-EXTERNAL-IP or ANTREA-EXTERNAL-IP6, which is
+// used by iptables rules to bypass kube-proxy.
+func (c *Client) DeleteExternalIPConfigs(svcInfoStr string, externalIP net.IP) error {
 	externalIPStr := externalIP.String()
+	isIPv6 := utilnet.IsIPv6(externalIP)
+	references, exists := c.serviceExternalIPReferences[externalIPStr]
+	if !exists || !references.Has(svcInfoStr) {
+		return nil
+	}
+	if references.Len() > 1 {
+		references.Delete(svcInfoStr)
+		return nil
+	}
+
 	route, found := c.serviceRoutes.Load(externalIPStr)
 	if !found {
 		klog.V(2).InfoS("Didn't find route for external IP", "IP", externalIPStr)
@@ -1791,8 +1926,17 @@ func (c *Client) DeleteExternalIPRoute(externalIP net.IP) error {
 			return fmt.Errorf("failed to delete route for external IP %s: %w", externalIPStr, err)
 		}
 	}
-	c.serviceRoutes.Delete(externalIPStr)
 	klog.V(4).InfoS("Deleted route for external IP", "IP", externalIPStr)
+
+	ipsetName := getExternalIPIPSetName(isIPv6)
+	if err := c.ipset.DelEntry(ipsetName, externalIPStr); err != nil {
+		return err
+	}
+	klog.V(4).InfoS("Deleted external IP from ipset", "IPSet", ipsetName, "IP", externalIPStr)
+
+	delete(c.serviceExternalIPReferences, externalIPStr)
+	c.serviceRoutes.Delete(externalIPStr)
+	c.serviceIPSets[ipsetName].Delete(externalIPStr)
 	return nil
 }
 
diff --git a/pkg/agent/route/route_windows.go b/pkg/agent/route/route_windows.go
index 8fa585265..3c60ad576 100644
--- a/pkg/agent/route/route_windows.go
+++ b/pkg/agent/route/route_windows.go
@@ -62,6 +62,13 @@ type Client struct {
 	nodeRoutes sync.Map
 	// serviceRoutes caches ip routes about Services.
 	serviceRoutes sync.Map
+	// serviceExternalIPReferences tracks the references of Service IP. The key is the Service IP and the value is
+	// the set of ServiceInfo strings. Because a Service could have multiple ports and each port will generate a
+	// ServicePort (which is the unit of the processing), a Service IP route may be required by several ServicePorts.
+	// With the references, we install the configurations for a Service IP exactly once as long as it's used by any
+	// ServicePorts and uninstall it exactly once when it's no longer used by any ServicePorts.
+	// It applies to externalIP and LoadBalancerIP.
+	serviceExternalIPReferences map[string]sets.Set[string]
 	// netNatStaticMappings caches Windows NetNat for NodePort.
 	netNatStaticMappings sync.Map
 	fwClient             *winfirewall.Client
@@ -81,12 +88,13 @@ func NewClient(networkConfig *config.NetworkConfig,
 	multicastEnabled bool,
 	serviceCIDRProvider servicecidr.Interface) (*Client, error) {
 	return &Client{
-		networkConfig:       networkConfig,
-		winnet:              &winnet.Handle{},
-		fwClient:            winfirewall.NewClient(),
-		noSNAT:              noSNAT,
-		proxyAll:            proxyAll,
-		serviceCIDRProvider: serviceCIDRProvider,
+		networkConfig:               networkConfig,
+		winnet:                      &winnet.Handle{},
+		fwClient:                    winfirewall.NewClient(),
+		noSNAT:                      noSNAT,
+		proxyAll:                    proxyAll,
+		serviceCIDRProvider:         serviceCIDRProvider,
+		serviceExternalIPReferences: make(map[string]sets.Set[string]),
 	}, nil
 }
 
@@ -476,7 +484,7 @@ func (c *Client) DeleteSNATRule(mark uint32) error {
 }
 
 // TODO: nodePortAddresses is not supported currently.
-func (c *Client) AddNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
+func (c *Client) AddNodePortConfigs(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
 	netNatStaticMapping := &winnet.NetNatStaticMapping{
 		Name:         antreaNatNodePort,
 		ExternalIP:   net.ParseIP("0.0.0.0"),
@@ -493,7 +501,7 @@ func (c *Client) AddNodePort(nodePortAddresses []net.IP, port uint16, protocol b
 	return nil
 }
 
-func (c *Client) DeleteNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
+func (c *Client) DeleteNodePortConfigs(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
 	key := fmt.Sprintf("%d-%s", port, protocol)
 	obj, found := c.netNatStaticMappings.Load(key)
 	if !found {
@@ -509,26 +517,45 @@ func (c *Client) DeleteNodePort(nodePortAddresses []net.IP, port uint16, protoco
 	return nil
 }
 
-// AddExternalIPRoute adds a route entry that forwards traffic destined for the external IP to the Antrea gateway interface.
-func (c *Client) AddExternalIPRoute(externalIP net.IP) error {
+// AddExternalIPConfigs adds a route entry to forward traffic destined for the external Service IP to the Antrea
+// gateway interface.
+func (c *Client) AddExternalIPConfigs(svcInfoStr string, externalIP net.IP) error {
 	externalIPStr := externalIP.String()
+	references, exists := c.serviceExternalIPReferences[externalIPStr]
+	if exists {
+		references.Insert(svcInfoStr)
+		return nil
+	}
+
 	linkIndex := c.nodeConfig.GatewayConfig.LinkIndex
 	gw := config.VirtualServiceIPv4
 	metric := winnet.MetricHigh
 	svcIPNet := util.NewIPNet(externalIP)
-
 	route := generateRoute(svcIPNet, gw, linkIndex, metric)
 	if err := c.winnet.ReplaceNetRoute(route); err != nil {
-		return fmt.Errorf("failed to install route for external IP %s: %w", externalIPStr, err)
+		return fmt.Errorf("failed to add route for external IP %s: %w", externalIPStr, err)
 	}
-	c.serviceRoutes.Store(externalIPStr, route)
 	klog.V(4).InfoS("Added route for external IP", "IP", externalIPStr)
+
+	references = sets.New[string](svcInfoStr)
+	c.serviceExternalIPReferences[externalIPStr] = references
+	c.serviceRoutes.Store(externalIPStr, route)
 	return nil
 }
 
-// DeleteExternalIPRoute deletes the route entry for the external IP.
-func (c *Client) DeleteExternalIPRoute(externalIP net.IP) error {
+// DeleteExternalIPConfigs deletes the route entry to forward traffic destined for the external Service IP to the Antrea
+// gateway interface.
+func (c *Client) DeleteExternalIPConfigs(svcInfoStr string, externalIP net.IP) error {
 	externalIPStr := externalIP.String()
+	references, exists := c.serviceExternalIPReferences[externalIPStr]
+	if !exists || !references.Has(svcInfoStr) {
+		return nil
+	}
+	if references.Len() > 1 {
+		references.Delete(svcInfoStr)
+		return nil
+	}
+
 	route, found := c.serviceRoutes.Load(externalIPStr)
 	if !found {
 		klog.V(2).InfoS("Didn't find route for external IP", "IP", externalIPStr)
@@ -537,8 +564,10 @@ func (c *Client) DeleteExternalIPRoute(externalIP net.IP) error {
 	if err := c.winnet.RemoveNetRoute(route.(*winnet.Route)); err != nil {
 		return fmt.Errorf("failed to delete route for external IP %s: %w", externalIPStr, err)
 	}
-	c.serviceRoutes.Delete(externalIPStr)
 	klog.V(4).InfoS("Deleted route for external IP", "IP", externalIPStr)
+
+	delete(c.serviceExternalIPReferences, externalIPStr)
+	c.serviceRoutes.Delete(externalIPStr)
 	return nil
 }
 
diff --git a/pkg/agent/secondarynetwork/init.go b/pkg/agent/secondarynetwork/init.go
new file mode 100644
index 000000000..721027f1a
--- /dev/null
+++ b/pkg/agent/secondarynetwork/init.go
@@ -0,0 +1,109 @@
+// 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 secondarynetwork
+
+import (
+	"fmt"
+
+	"github.com/TomCodeLV/OVSDB-golang-lib/pkg/ovsdb"
+	netdefclient "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/typed/k8s.cni.cncf.io/v1"
+	clientset "k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/tools/cache"
+	componentbaseconfig "k8s.io/component-base/config"
+	"k8s.io/klog/v2"
+
+	"antrea.io/antrea/pkg/agent/secondarynetwork/podwatch"
+	agentconfig "antrea.io/antrea/pkg/config/agent"
+	"antrea.io/antrea/pkg/ovs/ovsconfig"
+	"antrea.io/antrea/pkg/util/channel"
+	"antrea.io/antrea/pkg/util/k8s"
+)
+
+var (
+	newOVSBridgeFn = ovsconfig.NewOVSBridge
+)
+
+type Controller struct {
+	ovsBridgeClient ovsconfig.OVSBridgeClient
+	secNetConfig    *agentconfig.SecondaryNetworkConfig
+	podController   *podwatch.PodController
+}
+
+func NewController(
+	clientConnectionConfig componentbaseconfig.ClientConnectionConfiguration,
+	kubeAPIServerOverride string,
+	k8sClient clientset.Interface,
+	podInformer cache.SharedIndexInformer,
+	podUpdateSubscriber channel.Subscriber,
+	secNetConfig *agentconfig.SecondaryNetworkConfig, ovsdb *ovsdb.OVSDB,
+) (*Controller, error) {
+	ovsBridgeClient, err := createOVSBridge(secNetConfig.OVSBridges, ovsdb)
+	if err != nil {
+		return nil, err
+	}
+
+	// Create the NetworkAttachmentDefinition client, which handles access to secondary network object
+	// definition from the API Server.
+	netAttachDefClient, err := createNetworkAttachDefClient(clientConnectionConfig, kubeAPIServerOverride)
+	if err != nil {
+		return nil, fmt.Errorf("NetworkAttachmentDefinition client creation failed: %v", err)
+	}
+
+	// Create podController to handle secondary network configuration for Pods with
+	// k8s.v1.cni.cncf.io/networks Annotation defined.
+	podWatchController, err := podwatch.NewPodController(
+		k8sClient, netAttachDefClient, podInformer,
+		podUpdateSubscriber, ovsBridgeClient)
+	if err != nil {
+		return nil, err
+	}
+	return &Controller{
+		ovsBridgeClient: ovsBridgeClient,
+		secNetConfig:    secNetConfig,
+		podController:   podWatchController}, nil
+}
+
+// Run starts the Pod controller for secondary networks.
+func (c *Controller) Run(stopCh <-chan struct{}) {
+	c.podController.Run(stopCh)
+}
+
+// CreateNetworkAttachDefClient creates net-attach-def client handle from the given config.
+func createNetworkAttachDefClient(config componentbaseconfig.ClientConnectionConfiguration, kubeAPIServerOverride string) (netdefclient.K8sCniCncfIoV1Interface, error) {
+	kubeConfig, err := k8s.CreateRestConfig(config, kubeAPIServerOverride)
+	if err != nil {
+		return nil, err
+	}
+
+	netAttachDefClient, err := netdefclient.NewForConfig(kubeConfig)
+	if err != nil {
+		return nil, err
+	}
+	return netAttachDefClient, nil
+}
+
+func createOVSBridge(bridges []agentconfig.OVSBridgeConfig, ovsdb *ovsdb.OVSDB) (ovsconfig.OVSBridgeClient, error) {
+	if len(bridges) == 0 {
+		return nil, nil
+	}
+	// Only one OVS bridge is supported.
+	bridgeConfig := bridges[0]
+	ovsBridgeClient := newOVSBridgeFn(bridgeConfig.BridgeName, ovsconfig.OVSDatapathSystem, ovsdb)
+	if err := ovsBridgeClient.Create(); err != nil {
+		return nil, fmt.Errorf("failed to create OVS bridge %s: %v", bridgeConfig.BridgeName, err)
+	}
+	klog.InfoS("OVS bridge created", "bridge", bridgeConfig.BridgeName)
+	return ovsBridgeClient, nil
+}
diff --git a/pkg/agent/secondarynetwork/init_linux.go b/pkg/agent/secondarynetwork/init_linux.go
index 6a8b16c20..136a2d27c 100644
--- a/pkg/agent/secondarynetwork/init_linux.go
+++ b/pkg/agent/secondarynetwork/init_linux.go
@@ -21,52 +21,28 @@ import (
 	"fmt"
 	"net"
 
-	"github.com/TomCodeLV/OVSDB-golang-lib/pkg/ovsdb"
-	netdefclient "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/typed/k8s.cni.cncf.io/v1"
-	clientset "k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/tools/cache"
-	componentbaseconfig "k8s.io/component-base/config"
 	"k8s.io/klog/v2"
 
 	"antrea.io/antrea/pkg/agent/interfacestore"
-	"antrea.io/antrea/pkg/agent/secondarynetwork/podwatch"
 	"antrea.io/antrea/pkg/agent/util"
-	agentconfig "antrea.io/antrea/pkg/config/agent"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
-	"antrea.io/antrea/pkg/util/channel"
-	"antrea.io/antrea/pkg/util/k8s"
 )
 
 var (
 	// Funcs which will be overridden with mock funcs in tests.
 	interfaceByNameFn = net.InterfaceByName
-	newOVSBridgeFn    = ovsconfig.NewOVSBridge
 )
 
-// Initialize sets up OVS bridges and starts the Pod controller for secondary networks.
-func Initialize(
-	clientConnectionConfig componentbaseconfig.ClientConnectionConfiguration,
-	kubeAPIServerOverride string,
-	k8sClient clientset.Interface,
-	podInformer cache.SharedIndexInformer,
-	nodeName string,
-	podUpdateSubscriber channel.Subscriber,
-	stopCh <-chan struct{},
-	secNetConfig *agentconfig.SecondaryNetworkConfig, ovsdb *ovsdb.OVSDB) error {
-
-	ovsBridgeClient, err := createOVSBridge(secNetConfig.OVSBridges, ovsdb)
-	if err != nil {
-		return err
-	}
-
+// Initialize sets up OVS bridges.
+func (c *Controller) Initialize() error {
 	// We only support moving and restoring of interface configuration to OVS Bridge for the single physical interface case.
-	if len(secNetConfig.OVSBridges) != 0 {
-		phyInterfaces := make([]string, len(secNetConfig.OVSBridges[0].PhysicalInterfaces))
-		copy(phyInterfaces, secNetConfig.OVSBridges[0].PhysicalInterfaces)
+	if len(c.secNetConfig.OVSBridges) != 0 {
+		phyInterfaces := make([]string, len(c.secNetConfig.OVSBridges[0].PhysicalInterfaces))
+		copy(phyInterfaces, c.secNetConfig.OVSBridges[0].PhysicalInterfaces)
 		if len(phyInterfaces) == 1 {
 
 			bridgedName, _, err := util.PrepareHostInterfaceConnection(
-				ovsBridgeClient,
+				c.ovsBridgeClient,
 				phyInterfaces[0],
 				0,
 				map[string]interface{}{
@@ -78,49 +54,18 @@ func Initialize(
 			}
 			phyInterfaces[0] = bridgedName
 		}
-		if err = connectPhyInterfacesToOVSBridge(ovsBridgeClient, phyInterfaces); err != nil {
+		if err := connectPhyInterfacesToOVSBridge(c.ovsBridgeClient, phyInterfaces); err != nil {
 			return err
 		}
 	}
-
-	// Create the NetworkAttachmentDefinition client, which handles access to secondary network object
-	// definition from the API Server.
-	netAttachDefClient, err := createNetworkAttachDefClient(clientConnectionConfig, kubeAPIServerOverride)
-	if err != nil {
-		return fmt.Errorf("NetworkAttachmentDefinition client creation failed: %v", err)
-	}
-
-	// Create podController to handle secondary network configuration for Pods with
-	// k8s.v1.cni.cncf.io/networks Annotation defined.
-	if podWatchController, err := podwatch.NewPodController(
-		k8sClient, netAttachDefClient, podInformer,
-		podUpdateSubscriber, ovsBridgeClient); err != nil {
-		return err
-	} else {
-		go podWatchController.Run(stopCh)
-	}
 	return nil
 }
 
-// RestoreHostInterfaceConfiguration restores interface configuration from secondary-bridge back to host-interface.
-func RestoreHostInterfaceConfiguration(secNetConfig *agentconfig.SecondaryNetworkConfig) {
-	if len(secNetConfig.OVSBridges) != 0 && len(secNetConfig.OVSBridges[0].PhysicalInterfaces) == 1 {
-		util.RestoreHostInterfaceConfiguration(secNetConfig.OVSBridges[0].BridgeName, secNetConfig.OVSBridges[0].PhysicalInterfaces[0])
-	}
-}
-
-func createOVSBridge(bridges []agentconfig.OVSBridgeConfig, ovsdb *ovsdb.OVSDB) (ovsconfig.OVSBridgeClient, error) {
-	if len(bridges) == 0 {
-		return nil, nil
+// Restore restores interface configuration from secondary-bridge back to host-interface.
+func (c *Controller) Restore() {
+	if len(c.secNetConfig.OVSBridges) != 0 && len(c.secNetConfig.OVSBridges[0].PhysicalInterfaces) == 1 {
+		util.RestoreHostInterfaceConfiguration(c.secNetConfig.OVSBridges[0].BridgeName, c.secNetConfig.OVSBridges[0].PhysicalInterfaces[0])
 	}
-	// Only one OVS bridge is supported.
-	bridgeConfig := bridges[0]
-	ovsBridgeClient := newOVSBridgeFn(bridgeConfig.BridgeName, ovsconfig.OVSDatapathSystem, ovsdb)
-	if err := ovsBridgeClient.Create(); err != nil {
-		return nil, fmt.Errorf("failed to create OVS bridge %s: %v", bridgeConfig.BridgeName, err)
-	}
-	klog.InfoS("OVS bridge created", "bridge", bridgeConfig.BridgeName)
-	return ovsBridgeClient, nil
 }
 
 func connectPhyInterfacesToOVSBridge(ovsBridgeClient ovsconfig.OVSBridgeClient, phyInterfaces []string) error {
@@ -146,17 +91,3 @@ func connectPhyInterfacesToOVSBridge(ovsBridgeClient ovsconfig.OVSBridgeClient,
 	}
 	return nil
 }
-
-// CreateNetworkAttachDefClient creates net-attach-def client handle from the given config.
-func createNetworkAttachDefClient(config componentbaseconfig.ClientConnectionConfiguration, kubeAPIServerOverride string) (netdefclient.K8sCniCncfIoV1Interface, error) {
-	kubeConfig, err := k8s.CreateRestConfig(config, kubeAPIServerOverride)
-	if err != nil {
-		return nil, err
-	}
-
-	netAttachDefClient, err := netdefclient.NewForConfig(kubeConfig)
-	if err != nil {
-		return nil, err
-	}
-	return netAttachDefClient, nil
-}
diff --git a/pkg/agent/secondarynetwork/init_windows.go b/pkg/agent/secondarynetwork/init_windows.go
index 45b862106..32004e7f6 100644
--- a/pkg/agent/secondarynetwork/init_windows.go
+++ b/pkg/agent/secondarynetwork/init_windows.go
@@ -17,30 +17,10 @@
 
 package secondarynetwork
 
-import (
-	"errors"
-
-	"github.com/TomCodeLV/OVSDB-golang-lib/pkg/ovsdb"
-	clientset "k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/tools/cache"
-	componentbaseconfig "k8s.io/component-base/config"
-
-	agentconfig "antrea.io/antrea/pkg/config/agent"
-	"antrea.io/antrea/pkg/util/channel"
-)
-
-func Initialize(
-	clientConnectionConfig componentbaseconfig.ClientConnectionConfiguration,
-	kubeAPIServerOverride string,
-	k8sClient clientset.Interface,
-	podInformer cache.SharedIndexInformer,
-	nodeName string,
-	podUpdateSubscriber channel.Subscriber,
-	stopCh <-chan struct{},
-	secNetConfig *agentconfig.SecondaryNetworkConfig, ovsdb *ovsdb.OVSDB) error {
-	return errors.New("not supported on Windows")
+func (c *Controller) Initialize() error {
+	return nil
 }
 
-func RestoreHostInterfaceConfiguration(secNetConfig *agentconfig.SecondaryNetworkConfig) {
+func (c *Controller) Restore() {
 	// Not supported on Windows.
 }
diff --git a/pkg/agent/secondarynetwork/podwatch/controller.go b/pkg/agent/secondarynetwork/podwatch/controller.go
index 3c9fb502c..43409fb12 100644
--- a/pkg/agent/secondarynetwork/podwatch/controller.go
+++ b/pkg/agent/secondarynetwork/podwatch/controller.go
@@ -82,7 +82,7 @@ type podCNIInfo struct {
 	netNS       string
 }
 
-type podController struct {
+type PodController struct {
 	kubeClient            clientset.Interface
 	netAttachDefClient    netdefclient.K8sCniCncfIoV1Interface
 	queue                 workqueue.RateLimitingInterface
@@ -103,13 +103,13 @@ func NewPodController(
 	podInformer cache.SharedIndexInformer,
 	podUpdateSubscriber channel.Subscriber,
 	ovsBridgeClient ovsconfig.OVSBridgeClient,
-) (*podController, error) {
+) (*PodController, error) {
 	ifaceStore := interfacestore.NewInterfaceStore()
 	interfaceConfigurator, err := cniserver.NewSecondaryInterfaceConfigurator(ovsBridgeClient, ifaceStore)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create SecondaryInterfaceConfigurator: %v", err)
 	}
-	pc := podController{
+	pc := PodController{
 		kubeClient:         kubeClient,
 		netAttachDefClient: netAttachDefClient,
 		queue: workqueue.NewNamedRateLimitingQueue(
@@ -153,7 +153,7 @@ func allocatePodSecondaryIfaceName(usedIFNames sets.Set[string]) (string, error)
 	return "", fmt.Errorf("no more interface names")
 }
 
-func (pc *podController) enqueuePod(obj interface{}) {
+func (pc *PodController) enqueuePod(obj interface{}) {
 	pod, isPod := obj.(*corev1.Pod)
 	if !isPod {
 		podDeletedState, ok := obj.(cache.DeletedFinalStateUnknown)
@@ -172,7 +172,7 @@ func (pc *podController) enqueuePod(obj interface{}) {
 }
 
 // processCNIUpdate will be called when CNIServer publishes a Pod update event.
-func (pc *podController) processCNIUpdate(e interface{}) {
+func (pc *PodController) processCNIUpdate(e interface{}) {
 	event := e.(types.PodUpdate)
 	podKey := podKeyGet(event.PodName, event.PodNamespace)
 	if event.IsAdd {
@@ -184,7 +184,7 @@ func (pc *podController) processCNIUpdate(e interface{}) {
 }
 
 // handleAddUpdatePod handles Pod Add, Update events and updates annotation if required.
-func (pc *podController) handleAddUpdatePod(pod *corev1.Pod, podCNIInfo *podCNIInfo,
+func (pc *PodController) handleAddUpdatePod(pod *corev1.Pod, podCNIInfo *podCNIInfo,
 	storedInterfaces []*interfacestore.InterfaceConfig) error {
 	if len(storedInterfaces) > 0 {
 		// We do not support secondary network update at the moment. Return as long as one
@@ -219,7 +219,7 @@ func (pc *podController) handleAddUpdatePod(pod *corev1.Pod, podCNIInfo *podCNII
 	return pc.configurePodSecondaryNetwork(pod, networklist, podCNIInfo)
 }
 
-func (pc *podController) removeInterfaces(interfaces []*interfacestore.InterfaceConfig) error {
+func (pc *PodController) removeInterfaces(interfaces []*interfacestore.InterfaceConfig) error {
 	var savedErr error
 	for _, interfaceConfig := range interfaces {
 		podName := interfaceConfig.PodName
@@ -256,7 +256,7 @@ func (pc *podController) removeInterfaces(interfaces []*interfacestore.Interface
 	return savedErr
 }
 
-func (pc *podController) syncPod(key string) error {
+func (pc *PodController) syncPod(key string) error {
 	var pod *corev1.Pod
 	var cniInfo *podCNIInfo
 	podExists := false
@@ -297,12 +297,12 @@ func (pc *podController) syncPod(key string) error {
 	return pc.handleAddUpdatePod(pod, cniInfo, storedInterfaces)
 }
 
-func (pc *podController) Worker() {
+func (pc *PodController) Worker() {
 	for pc.processNextWorkItem() {
 	}
 }
 
-func (pc *podController) processNextWorkItem() bool {
+func (pc *PodController) processNextWorkItem() bool {
 	obj, quit := pc.queue.Get()
 	if quit {
 		return false
@@ -319,7 +319,7 @@ func (pc *podController) processNextWorkItem() bool {
 }
 
 // Configure Secondary Network Interface.
-func (pc *podController) configureSecondaryInterface(
+func (pc *PodController) configureSecondaryInterface(
 	pod *corev1.Pod,
 	network *netdefv1.NetworkSelectionElement,
 	podCNIInfo *podCNIInfo,
@@ -370,7 +370,7 @@ func (pc *podController) configureSecondaryInterface(
 	return ifConfigErr
 }
 
-func (pc *podController) configurePodSecondaryNetwork(pod *corev1.Pod, networkList []*netdefv1.NetworkSelectionElement, podCNIInfo *podCNIInfo) error {
+func (pc *PodController) configurePodSecondaryNetwork(pod *corev1.Pod, networkList []*netdefv1.NetworkSelectionElement, podCNIInfo *podCNIInfo) error {
 	usedIFNames := sets.New[string]()
 	for _, network := range networkList {
 		if network.InterfaceRequest != "" {
@@ -477,7 +477,7 @@ func validateNetworkConfig(cniConfig []byte) (*SecondaryNetworkConfig, error) {
 	return &networkConfig, nil
 }
 
-func (pc *podController) Run(stopCh <-chan struct{}) {
+func (pc *PodController) Run(stopCh <-chan struct{}) {
 	defer func() {
 		klog.InfoS("Shutting down", "controller", controllerName)
 		pc.queue.ShutDown()
diff --git a/pkg/agent/secondarynetwork/podwatch/sriov.go b/pkg/agent/secondarynetwork/podwatch/sriov.go
index da94428c5..cca476660 100644
--- a/pkg/agent/secondarynetwork/podwatch/sriov.go
+++ b/pkg/agent/secondarynetwork/podwatch/sriov.go
@@ -107,7 +107,7 @@ func getPodContainerDeviceIDs(podName string, podNamespace string) ([]string, er
 // which is still not associated with a network device name.
 // NOTE: buildVFDeviceIDListPerPod is called only if a Pod specific VF to Interface mapping cache
 // was not build earlier. Sample initial entry per Pod: "{18:01.1,""},{18:01.2,""},{18:01.3,""}"
-func (pc *podController) buildVFDeviceIDListPerPod(podName, podNamespace string) ([]podSriovVFDeviceIDInfo, error) {
+func (pc *PodController) buildVFDeviceIDListPerPod(podName, podNamespace string) ([]podSriovVFDeviceIDInfo, error) {
 	podKey := podKeyGet(podName, podNamespace)
 	deviceCache, cacheFound := pc.vfDeviceIDUsageMap.Load(podKey)
 	if cacheFound {
@@ -127,7 +127,7 @@ func (pc *podController) buildVFDeviceIDListPerPod(podName, podNamespace string)
 	return vfDeviceIDInfoCache, nil
 }
 
-func (pc *podController) deleteVFDeviceIDListPerPod(podName, podNamespace string) {
+func (pc *PodController) deleteVFDeviceIDListPerPod(podName, podNamespace string) {
 	podKey := podKeyGet(podName, podNamespace)
 	_, cacheFound := pc.vfDeviceIDUsageMap.Load(podKey)
 	if cacheFound {
@@ -137,7 +137,7 @@ func (pc *podController) deleteVFDeviceIDListPerPod(podName, podNamespace string
 	return
 }
 
-func (pc *podController) releaseSriovVFDeviceID(podName, podNamespace, interfaceName string) {
+func (pc *PodController) releaseSriovVFDeviceID(podName, podNamespace, interfaceName string) {
 	podKey := podKeyGet(podName, podNamespace)
 	obj, cacheFound := pc.vfDeviceIDUsageMap.Load(podKey)
 	if !cacheFound {
@@ -151,7 +151,7 @@ func (pc *podController) releaseSriovVFDeviceID(podName, podNamespace, interface
 	}
 }
 
-func (pc *podController) assignUnusedSriovVFDeviceID(podName, podNamespace, interfaceName string) (string, error) {
+func (pc *PodController) assignUnusedSriovVFDeviceID(podName, podNamespace, interfaceName string) (string, error) {
 	var cache []podSriovVFDeviceIDInfo
 	cache, err := pc.buildVFDeviceIDListPerPod(podName, podNamespace)
 	if err != nil {
@@ -168,7 +168,7 @@ func (pc *podController) assignUnusedSriovVFDeviceID(podName, podNamespace, inte
 }
 
 // Configure SRIOV VF as a Secondary Network Interface.
-func (pc *podController) configureSriovAsSecondaryInterface(pod *corev1.Pod, network *netdefv1.NetworkSelectionElement, podCNIInfo *podCNIInfo, mtu int, result *current.Result) error {
+func (pc *PodController) configureSriovAsSecondaryInterface(pod *corev1.Pod, network *netdefv1.NetworkSelectionElement, podCNIInfo *podCNIInfo, mtu int, result *current.Result) error {
 	podSriovVFDeviceID, err := pc.assignUnusedSriovVFDeviceID(pod.Name, pod.Namespace, network.InterfaceRequest)
 	if err != nil {
 		return err
@@ -181,7 +181,7 @@ func (pc *podController) configureSriovAsSecondaryInterface(pod *corev1.Pod, net
 	return nil
 }
 
-func (pc *podController) deleteSriovSecondaryInterface(interfaceConfig *interfacestore.InterfaceConfig) error {
+func (pc *PodController) deleteSriovSecondaryInterface(interfaceConfig *interfacestore.InterfaceConfig) error {
 	// NOTE: SR-IOV VF interface clean-up will be handled by SR-IOV device plugin. The interface
 	// is not deleted here.
 	if err := pc.interfaceConfigurator.DeleteSriovSecondaryInterface(interfaceConfig); err != nil {
diff --git a/pkg/agent/util/iptables/iptables.go b/pkg/agent/util/iptables/iptables.go
index 3025db1bb..3eee6ba1d 100644
--- a/pkg/agent/util/iptables/iptables.go
+++ b/pkg/agent/util/iptables/iptables.go
@@ -48,6 +48,7 @@ const (
 	SNATTarget       = "SNAT"
 	DNATTarget       = "DNAT"
 	RejectTarget     = "REJECT"
+	NotrackTarget    = "NOTRACK"
 
 	PreRoutingChain  = "PREROUTING"
 	InputChain       = "INPUT"
@@ -101,7 +102,7 @@ type Interface interface {
 
 	DeleteChain(protocol Protocol, table string, chain string) error
 
-	ListRules(table string, chain string) ([]string, error)
+	ListRules(protocol Protocol, table string, chain string) (map[Protocol][]string, error)
 
 	Restore(data string, flush bool, useIPv6 bool) error
 
@@ -312,14 +313,18 @@ func (c *Client) DeleteChain(protocol Protocol, table string, chain string) erro
 }
 
 // ListRules lists all rules from a chain in a table.
-func (c *Client) ListRules(table string, chain string) ([]string, error) {
-	var allRules []string
+func (c *Client) ListRules(protocol Protocol, table string, chain string) (map[Protocol][]string, error) {
+	allRules := make(map[Protocol][]string)
 	for p := range c.ipts {
-		rules, err := c.ipts[p].List(table, chain)
+		ipt := c.ipts[p]
+		if !matchProtocol(ipt, protocol) {
+			continue
+		}
+		rules, err := ipt.List(table, chain)
 		if err != nil {
-			return rules, fmt.Errorf("error getting rules from table %s chain %s protocol %s: %v", table, chain, p, err)
+			return nil, fmt.Errorf("error getting rules from table %s chain %s protocol %s: %v", table, chain, p, err)
 		}
-		allRules = append(allRules, rules...)
+		allRules[p] = rules
 	}
 	return allRules, nil
 }
diff --git a/pkg/antctl/raw/check/installation/command.go b/pkg/antctl/raw/check/installation/command.go
index 209859799..d6120fbea 100644
--- a/pkg/antctl/raw/check/installation/command.go
+++ b/pkg/antctl/raw/check/installation/command.go
@@ -68,7 +68,7 @@ const (
 	kindEchoName                = "echo"
 	kindClientName              = "client"
 	agentDaemonSetName          = "antrea-agent"
-	deploymentImage             = "registry.k8s.io/e2e-test-images/agnhost:2.29"
+	deploymentImage             = "registry.k8s.io/e2e-test-images/agnhost:2.40"
 	podReadyTimeout             = 1 * time.Minute
 )
 


Test patch:
diff --git a/ci/kind/test-e2e-kind.sh b/ci/kind/test-e2e-kind.sh
index 17de0770d..1e91c95c8 100755
--- a/ci/kind/test-e2e-kind.sh
+++ b/ci/kind/test-e2e-kind.sh
@@ -24,19 +24,21 @@ function echoerr {
 
 _usage="Usage: $0 [--encap-mode <mode>] [--ip-family <v4|v6|dual>] [--coverage] [--help|-h]
         --encap-mode                  Traffic encapsulation mode. (default is 'encap').
-        --ip-family                   Configures the ipFamily for the KinD cluster.
+        --ip-family                   Configure the ipFamily for the KinD cluster.
         --feature-gates               A comma-separated list of key=value pairs that describe feature gates, e.g. AntreaProxy=true,Egress=false.
         --run                         Run only tests matching the regexp.
-        --proxy-all                   Enables Antrea proxy with all Service support.
+        --proxy-all                   Enable Antrea proxy with all Service support.
+        --no-kube-proxy               Don't deploy kube-proxy.
         --load-balancer-mode          LoadBalancer mode.
-        --node-ipam                   Enables Antrea NodeIPAM.
-        --multicast                   Enables Multicast.
+        --node-ipam                   Enable Antrea NodeIPAM.
+        --multicast                   Enable Multicast.
+        --bgp-policy                  Enable Antrea BGPPolicy.
         --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.
+        --networkpolicy-evaluation    Configure additional NetworkPolicy evaluation level when running e2e tests.
+        --extra-network               Create an extra network that worker Nodes will connect to. Cannot be specified with the hybrid mode.
+        --extra-vlan                  Create an subnet-based VLAN that worker Nodes will connect to.
         --skip                        A comma-separated list of keywords, with which tests should be skipped.
-        --coverage                    Enables measure Antrea code coverage when running e2e tests on kind.
+        --coverage                    Enable 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.
@@ -72,9 +74,11 @@ mode=""
 ipfamily="v4"
 feature_gates=""
 proxy_all=false
+no_kube_proxy=false
 load_balancer_mode=""
 node_ipam=false
 multicast=false
+bgp_policy=false
 flow_visibility=false
 np_evaluation=false
 extra_network=false
@@ -106,6 +110,10 @@ case $key in
     proxy_all=true
     shift
     ;;
+    --no-kube-proxy)
+    no_kube_proxy=true
+    shift
+    ;;
     --load-balancer-mode)
     load_balancer_mode="$2"
     shift 2
@@ -118,6 +126,10 @@ case $key in
     multicast=true
     shift
     ;;
+    --bgp-policy)
+    bgp_policy=true
+    shift
+    ;;
     --ip-family)
     ipfamily="$2"
     shift 2
@@ -230,11 +242,14 @@ fi
 if $multicast; then
     manifest_args="$manifest_args --multicast"
 fi
+if $bgp_policy; then
+    manifest_args="$manifest_args --feature-gates BGPPolicy=true"
+fi
 if $flow_visibility; then
     manifest_args="$manifest_args --feature-gates FlowExporter=true,L7FlowExporter=true --extra-helm-values-file $FLOW_VISIBILITY_HELM_VALUES"
 fi
 
-COMMON_IMAGES_LIST=("registry.k8s.io/e2e-test-images/agnhost:2.29" \
+COMMON_IMAGES_LIST=("registry.k8s.io/e2e-test-images/agnhost:2.40" \
                     "antrea/nginx:1.21.6-alpine" \
                     "antrea/toolbox:1.3-0")
 
@@ -299,7 +314,7 @@ function setup_cluster {
     echoerr "invalid value for --ip-family \"$ipfamily\", expected \"v4\" or \"v6\""
     exit 1
   fi
-  if $proxy_all; then
+  if $no_kube_proxy; then
     args="$args --no-kube-proxy"
   fi
   if $node_ipam; then
@@ -308,8 +323,12 @@ function setup_cluster {
   if $extra_network && [[ "$mode" != "hybrid" ]]; then
     args="$args --extra-networks \"20.20.30.0/24\""
   fi
-  # Deploy an external server which could be used when testing Pod-to-External traffic.
-  args="$args --deploy-external-server $vlan_args"
+  # Deploy an external agnhost which could be used when testing Pod-to-External traffic.
+  args="$args --deploy-external-agnhost $vlan_args"
+  # Deploy an external FRR which could be used when testing BGPPolicy.
+  if $bgp_policy; then
+    args="$args --deploy-external-frr"
+  fi
 
   echo "creating test bed with args $args"
   eval "timeout 600 $TESTBED_CMD create kind $args"
@@ -353,7 +372,7 @@ function run_test {
       cat $CH_OPERATOR_YML | docker exec -i kind-control-plane dd of=/root/clickhouse-operator-install-bundle.yml
   fi
 
-  if $proxy_all; then
+  if $no_kube_proxy; then
       apiserver=$(docker exec -i kind-control-plane kubectl get endpoints kubernetes --no-headers | awk '{print $2}')
       if $coverage; then
         docker exec -i kind-control-plane sed -i.bak -E "s/^[[:space:]]*[#]?kubeAPIServerOverride[[:space:]]*:[[:space:]]*[a-z\"]+[[:space:]]*$/    kubeAPIServerOverride: \"$apiserver\"/" /root/antrea-coverage.yml /root/antrea-ipsec-coverage.yml
@@ -373,9 +392,15 @@ function run_test {
     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"
+  external_agnhost_cid=$(docker ps -f name="^antrea-external-agnhost" --format '{{.ID}}')
+  external_agnhost_ips=$(docker inspect $external_agnhost_cid -f '{{.NetworkSettings.Networks.kind.IPAddress}},{{.NetworkSettings.Networks.kind.GlobalIPv6Address}}')
+  EXTRA_ARGS="$vlan_args --external-agnhost-ips $external_agnhost_ips"
+
+  if $bgp_policy; then
+    external_frr_cid=$(docker ps -f name="^antrea-external-frr" --format '{{.ID}}')
+    external_frr_ips=$(docker inspect $external_frr_cid -f '{{.NetworkSettings.Networks.kind.IPAddress}},{{.NetworkSettings.Networks.kind.GlobalIPv6Address}}')
+    EXTRA_ARGS="$EXTRA_ARGS --external-frr-cid $external_frr_cid --external-frr-ips $external_frr_ips"
+  fi
 
   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
 
diff --git a/hack/netpol-generator/test-kind.sh b/hack/netpol-generator/test-kind.sh
index 698b9cab9..48a555440 100755
--- a/hack/netpol-generator/test-kind.sh
+++ b/hack/netpol-generator/test-kind.sh
@@ -34,8 +34,8 @@ kind load docker-image antrea/antrea-controller-ubuntu:latest
 docker pull mfenwick100/cyclonus:v0.4.7
 kind load docker-image mfenwick100/cyclonus:v0.4.7
 # pre-load agnhost image
-docker pull registry.k8s.io/e2e-test-images/agnhost:2.29
-kind load docker-image registry.k8s.io/e2e-test-images/agnhost:2.29
+docker pull registry.k8s.io/e2e-test-images/agnhost:2.40
+kind load docker-image registry.k8s.io/e2e-test-images/agnhost:2.40
 
 "$ROOT_DIR"/hack/generate-manifest.sh | kubectl apply -f -
 
diff --git a/multicluster/test/e2e/framework.go b/multicluster/test/e2e/framework.go
index 824e2bcfc..6480640b6 100644
--- a/multicluster/test/e2e/framework.go
+++ b/multicluster/test/e2e/framework.go
@@ -51,7 +51,7 @@ const (
 	regularNodeClientSuffix string = "regular-client"
 
 	nginxImage   = "antrea/nginx:1.21.6-alpine"
-	agnhostImage = "registry.k8s.io/e2e-test-images/agnhost:2.29"
+	agnhostImage = "registry.k8s.io/e2e-test-images/agnhost:2.40"
 )
 
 var provider providers.ProviderInterface
diff --git a/pkg/agent/nodeportlocal/npl_agent_test.go b/pkg/agent/nodeportlocal/npl_agent_test.go
index bcdd90d92..e5bb5b123 100644
--- a/pkg/agent/nodeportlocal/npl_agent_test.go
+++ b/pkg/agent/nodeportlocal/npl_agent_test.go
@@ -52,6 +52,7 @@ const (
 	defaultPodName        = "test-pod"
 	defaultSvcName        = "test-svc"
 	defaultNS             = "default"
+	defaultPodKey         = defaultNS + "/" + defaultPodName
 	defaultNodeName       = "test-node"
 	defaultHostIP         = "10.10.10.10"
 	defaultPodIP          = "192.168.32.10"
@@ -69,7 +70,7 @@ func newPortTable(mockIPTables rules.PodPortRules, mockPortOpener portcache.Loca
 		PortTableCache: cache.NewIndexer(portcache.GetPortTableKey, cache.Indexers{
 			portcache.NodePortIndex:    portcache.NodePortIndexFunc,
 			portcache.PodEndpointIndex: portcache.PodEndpointIndexFunc,
-			portcache.PodIPIndex:       portcache.PodIPIndexFunc,
+			portcache.PodKeyIndex:      portcache.PodKeyIndexFunc,
 		}),
 		StartPort:       defaultStartPort,
 		EndPort:         defaultEndPort,
@@ -299,7 +300,7 @@ func setUpWithTestServiceAndPod(t *testing.T, tc *testConfig, customNodePort *in
 	require.NoError(t, err, "Poll for annotation check failed")
 	expectedAnnotations := newExpectedNPLAnnotations().Add(&nodePort, defaultPort, protocolTCP)
 	expectedAnnotations.Check(t, value)
-	assert.True(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 
 	return testData, testSvc, testPod
 }
@@ -377,7 +378,7 @@ func TestSvcNamespaceUpdate(t *testing.T) {
 	// Check that annotation and the rule are removed.
 	_, err = testData.pollForPodAnnotation(testPodDefaultNS.Name, false)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestSvcTypeUpdate updates Service type from ClusterIP to NodePort
@@ -393,7 +394,7 @@ func TestSvcTypeUpdate(t *testing.T) {
 	// Check that annotation and the rule are removed.
 	_, err := testData.pollForPodAnnotation(testPod.Name, false)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 
 	// Update Service type to ClusterIP.
 	testSvc.Spec.Type = corev1.ServiceTypeClusterIP
@@ -401,7 +402,7 @@ func TestSvcTypeUpdate(t *testing.T) {
 
 	_, err = testData.pollForPodAnnotation(testPod.Name, true)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.True(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestSvcUpdateAnnotation updates the Service spec to disabled NPL. It then verifies that the Pod's
@@ -417,7 +418,7 @@ func TestSvcUpdateAnnotation(t *testing.T) {
 	// Check that annotation and the rule is removed.
 	_, err := testData.pollForPodAnnotation(testPod.Name, false)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 
 	// Enable NPL back.
 	testSvc.Annotations = map[string]string{types.NPLEnabledAnnotationKey: "true"}
@@ -425,7 +426,7 @@ func TestSvcUpdateAnnotation(t *testing.T) {
 
 	_, err = testData.pollForPodAnnotation(testPod.Name, true)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.True(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestSvcRemoveAnnotation is the same as TestSvcUpdateAnnotation, but it deletes the NPL enabled
@@ -439,7 +440,7 @@ func TestSvcRemoveAnnotation(t *testing.T) {
 
 	_, err := testData.pollForPodAnnotation(testPod.Name, false)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestSvcUpdateSelector updates the Service selector so that it no longer selects the Pod, and
@@ -453,14 +454,14 @@ func TestSvcUpdateSelector(t *testing.T) {
 
 	_, err := testData.pollForPodAnnotation(testPod.Name, false)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 
 	testSvc.Spec.Selector = map[string]string{defaultAppSelectorKey: defaultAppSelectorVal}
 	testData.updateServiceOrFail(testSvc)
 
 	_, err = testData.pollForPodAnnotation(testPod.Name, true)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.True(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestPodUpdateSelectorLabel updates the Pod's labels so that the Pod is no longer selected by the
@@ -475,7 +476,7 @@ func TestPodUpdateSelectorLabel(t *testing.T) {
 
 	_, err := testData.pollForPodAnnotation(testPod.Name, false)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestSvcDelete deletes the Service. It then verifies that the Pod's NPL annotation is removed and
@@ -490,7 +491,7 @@ func TestSvcDelete(t *testing.T) {
 
 	_, err = testData.pollForPodAnnotation(testPod.Name, false)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestPodDelete verifies that when a Pod gets deleted, the corresponding entry gets deleted from
@@ -504,7 +505,7 @@ func TestPodDelete(t *testing.T) {
 	t.Logf("Successfully deleted Pod: %s", testPod.Name)
 
 	err = wait.PollUntilContextTimeout(context.Background(), time.Second, 20*time.Second, false, func(ctx context.Context) (bool, error) {
-		return !testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP), nil
+		return !testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP), nil
 	})
 	assert.NoError(t, err, "Error when polling for port table update")
 }
@@ -523,8 +524,8 @@ func TestAddMultiPortPodSvc(t *testing.T) {
 	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))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, newPort, protocolTCP))
 
 	// Remove the second target port.
 	testSvc.Spec.Ports = testSvc.Spec.Ports[:1]
@@ -534,7 +535,7 @@ func TestAddMultiPortPodSvc(t *testing.T) {
 	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))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, newPort, protocolTCP))
 }
 
 // TestPodAddMultiPort creates a Pod with multiple ports and a Service with only one target port.
@@ -560,8 +561,8 @@ func TestAddMultiPortPodSinglePortSvc(t *testing.T) {
 	require.NoError(t, err, "Poll for annotation check failed")
 	expectedAnnotations := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
 	expectedAnnotations.Check(t, value)
-	assert.True(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, newPort2, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, newPort2, protocolTCP))
 }
 
 // TestPodAddHostPort creates a Pod with host ports and verifies that the Pod's NPL annotation
@@ -582,7 +583,7 @@ func TestPodAddHostPort(t *testing.T) {
 	require.NoError(t, err, "Poll for annotation check failed")
 	expectedAnnotations := newExpectedNPLAnnotations().Add(&hostPort, defaultPort, protocolTCP)
 	expectedAnnotations.Check(t, value)
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestPodAddHostPort creates a Pod with multiple host ports having same value but different protocol.
@@ -607,7 +608,7 @@ func TestPodAddHostPortMultiProtocol(t *testing.T) {
 	require.NoError(t, err, "Poll for annotation check failed")
 	expectedAnnotations := newExpectedNPLAnnotations().Add(&hostPort, defaultPort, protocolTCP)
 	expectedAnnotations.Check(t, value)
-	assert.False(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestPodAddHostPortWrongProtocol creates a Pod with a host port but with protocol UDP instead of TCP.
@@ -627,7 +628,7 @@ func TestPodAddHostPortWrongProtocol(t *testing.T) {
 	require.NoError(t, err, "Poll for annotation check failed")
 	expectedAnnotations := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
 	expectedAnnotations.Check(t, value)
-	assert.True(t, testData.portTable.RuleExists(defaultPodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestTargetPortWithName creates a Service with target port name in string.
@@ -646,13 +647,13 @@ func TestTargetPortWithName(t *testing.T) {
 
 	_, err := testData.pollForPodAnnotation(testPod.Name, true)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.True(t, testData.portTable.RuleExists(testPod.Status.PodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 
 	testSvc = getTestSvcWithPortName("wrongPort")
 	testData.updateServiceOrFail(testSvc)
 	_, err = testData.pollForPodAnnotation(testPod.Name, false)
 	require.NoError(t, err, "Poll for annotation check failed")
-	assert.False(t, testData.portTable.RuleExists(testPod.Status.PodIP, defaultPort, protocolTCP))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 // TestMultiplePods creates multiple Pods and verifies that NPL annotations for both Pods are
@@ -661,10 +662,12 @@ func TestMultiplePods(t *testing.T) {
 	testSvc := getTestSvc()
 	testPod1 := getTestPod()
 	testPod1.Name = "pod1"
+	testPod1Key := testPod1.Namespace + "/" + testPod1.Name
 	testPod1.Status.PodIP = "192.168.32.1"
 	testPod2 := getTestPod()
 	testPod2.Name = "pod2"
 	testPod2.Status.PodIP = "192.168.32.2"
+	testPod2Key := testPod2.Namespace + "/" + testPod2.Name
 	testData := setUp(t, newTestConfig(), testSvc, testPod1, testPod2)
 	defer testData.tearDown()
 
@@ -672,14 +675,67 @@ func TestMultiplePods(t *testing.T) {
 	require.NoError(t, err, "Poll for annotation check failed")
 	expectedAnnotationsPod1 := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
 	expectedAnnotationsPod1.Check(t, pod1Value)
-	assert.True(t, testData.portTable.RuleExists(testPod1.Status.PodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(testPod1Key, defaultPort, protocolTCP))
 
 	pod2Value, err := testData.pollForPodAnnotation(testPod2.Name, true)
 	require.NoError(t, err, "Poll for annotation check failed")
 	expectedAnnotationsPod2 := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
 	expectedAnnotationsPod2.Check(t, pod2Value)
 	assert.NotEqual(t, pod1Value[0].NodePort, pod2Value[0].NodePort)
-	assert.True(t, testData.portTable.RuleExists(testPod2.Status.PodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(testPod2Key, defaultPort, protocolTCP))
+}
+
+// TestPodIPRecycle creates two Pods that have the same IP to simulate Pod IP recycle case.
+// It verifies that NPL annotations and rules for a Pod is not affected by another Pod's lifecycle events.
+func TestPodIPRecycle(t *testing.T) {
+	ctx := context.Background()
+	testSvc := getTestSvc()
+	// pod1 and pod2 have the same IP, pod1 will run into terminated phase eventually.
+	testPod1 := getTestPod()
+	testPod1.Name = "pod1"
+	testPod1Key := testPod1.Namespace + "/" + testPod1.Name
+	testPod2 := getTestPod()
+	testPod2.Name = "pod2"
+	testPod2Key := testPod2.Namespace + "/" + testPod2.Name
+	testData := setUp(t, newTestConfig(), testSvc, testPod1, testPod2)
+	defer testData.tearDown()
+
+	pod1Value, err := testData.pollForPodAnnotation(testPod1.Name, true)
+	require.NoError(t, err, "Poll for annotation check failed")
+	expectedAnnotationsPod1 := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
+	expectedAnnotationsPod1.Check(t, pod1Value)
+	assert.True(t, testData.portTable.RuleExists(testPod1Key, defaultPort, protocolTCP))
+
+	pod2Value, err := testData.pollForPodAnnotation(testPod2.Name, true)
+	require.NoError(t, err, "Poll for annotation check failed")
+	expectedAnnotationsPod2 := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
+	expectedAnnotationsPod2.Check(t, pod2Value)
+	assert.NotEqual(t, pod1Value[0].NodePort, pod2Value[0].NodePort)
+	assert.True(t, testData.portTable.RuleExists(testPod2Key, defaultPort, protocolTCP))
+
+	// After pod1 runs into succeeded phase, its NPL rule and annotation should be removed, while pod2 shouldn't be affected.
+	updatedTestPod1 := testPod1.DeepCopy()
+	updatedTestPod1.Status.Phase = corev1.PodSucceeded
+	_, err = testData.k8sClient.CoreV1().Pods(updatedTestPod1.Namespace).UpdateStatus(ctx, updatedTestPod1, metav1.UpdateOptions{})
+	require.NoError(t, err)
+	_, err = testData.pollForPodAnnotation(testPod1.Name, false)
+	require.NoError(t, err, "Poll for annotation check failed")
+	assert.False(t, testData.portTable.RuleExists(testPod1Key, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(testPod2Key, defaultPort, protocolTCP))
+
+	// Deleting pod1 shouldn't delete pod2's NPL rule and annotation.
+	require.NoError(t, testData.k8sClient.CoreV1().Pods(testPod1.Namespace).Delete(ctx, testPod1.Name, metav1.DeleteOptions{}))
+	_, err = testData.pollForPodAnnotation(testPod2.Name, true)
+	require.NoError(t, err, "Poll for annotation check failed")
+	expectedAnnotationsPod2 = newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
+	expectedAnnotationsPod2.Check(t, pod2Value)
+	assert.True(t, testData.portTable.RuleExists(testPod2Key, defaultPort, protocolTCP))
+
+	// Deleting pod2 should delete pod2's NPL rule.
+	require.NoError(t, testData.k8sClient.CoreV1().Pods(testPod2.Namespace).Delete(ctx, testPod2.Name, metav1.DeleteOptions{}))
+	assert.Eventually(t, func() bool {
+		return !testData.portTable.RuleExists(testPod2Key, defaultPort, protocolTCP)
+	}, 5*time.Second, 50*time.Millisecond)
 }
 
 // TestMultipleProtocols creates multiple Pods with multiple protocols and verifies that
@@ -699,6 +755,7 @@ func TestMultipleProtocols(t *testing.T) {
 	testPod2.Name = "pod2"
 	testPod2.Status.PodIP = "192.168.32.2"
 	testPod2.Labels = udpSvcLabel
+	testPod2Key := testPod2.Namespace + "/" + testPod2.Name
 
 	// Create TCP/80 testSvc1 for pod1.
 	testSvc1 := getTestSvc()
@@ -726,7 +783,7 @@ func TestMultipleProtocols(t *testing.T) {
 	expectedAnnotationsPod2 := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolUDP)
 	expectedAnnotationsPod2.Check(t, pod2Value)
 	assert.NotEqual(t, pod1Value[0].NodePort, pod2Value[0].NodePort)
-	assert.True(t, testData.portTable.RuleExists(testPod2.Status.PodIP, defaultPort, protocolUDP))
+	assert.True(t, testData.portTable.RuleExists(testPod2Key, defaultPort, protocolUDP))
 
 	// Update testSvc2 to serve TCP/80 and UDP/81 both, so pod2 is
 	// exposed on both TCP and UDP, with different NodePorts.
@@ -767,8 +824,8 @@ func TestMultipleServicesSameBackendPod(t *testing.T) {
 	require.NoError(t, err, "Poll for annotation check failed")
 	expectedAnnotations := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP).Add(nil, 9090, protocolTCP)
 	expectedAnnotations.Check(t, value)
-	assert.True(t, testData.portTable.RuleExists(testPod.Status.PodIP, defaultPort, protocolTCP))
-	assert.True(t, testData.portTable.RuleExists(testPod.Status.PodIP, 9090, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, 9090, protocolTCP))
 }
 
 // TestInitInvalidPod simulates an agent reboot case. A Pod with an invalid NPL annotation is
@@ -789,7 +846,7 @@ func TestInitInvalidPod(t *testing.T) {
 	require.NoError(t, err, "Poll for annotation check failed")
 	expectedAnnotations := newExpectedNPLAnnotations().Add(nil, defaultPort, protocolTCP)
 	expectedAnnotations.Check(t, value)
-	assert.True(t, testData.portTable.RuleExists(testPod.Status.PodIP, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
 }
 
 var (
@@ -854,8 +911,8 @@ func TestSingleRuleDeletionError(t *testing.T) {
 	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))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, newPort, protocolTCP))
 
 	// Remove the second target port, to force one mapping to be deleted.
 	testSvc.Spec.Ports = testSvc.Spec.Ports[:1]
@@ -866,7 +923,7 @@ func TestSingleRuleDeletionError(t *testing.T) {
 	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))
+	assert.False(t, testData.portTable.RuleExists(defaultPodKey, newPort, protocolTCP))
 }
 
 func TestPreventDefunctRuleReuse(t *testing.T) {
@@ -919,8 +976,8 @@ func TestPreventDefunctRuleReuse(t *testing.T) {
 	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))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, defaultPort, protocolTCP))
+	assert.True(t, testData.portTable.RuleExists(defaultPodKey, newPort, protocolTCP))
 
 	// Remove the second target port, to force one mapping to be deleted.
 	testSvc.Spec.Ports = testSvc.Spec.Ports[:1]
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_others_test.go b/pkg/agent/nodeportlocal/portcache/port_table_others_test.go
index c5e1e4245..842f21187 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_others_test.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_others_test.go
@@ -99,6 +99,7 @@ func TestDeleteRule(t *testing.T) {
 	closer := &mockCloser{}
 
 	data := &NodePortData{
+		PodKey:   podKey,
 		NodePort: nodePort1,
 		PodPort:  podPort,
 		PodIP:    podIP,
@@ -112,20 +113,20 @@ func TestDeleteRule(t *testing.T) {
 	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")
+	require.ErrorContains(t, portTable.DeleteRule(podKey, 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")
+	require.ErrorContains(t, portTable.DeleteRule(podKey, 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))
+	assert.NoError(t, portTable.DeleteRule(podKey, 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))
+	assert.NoError(t, portTable.DeleteRule(podKey, podPort, protocol))
 }
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_test.go b/pkg/agent/nodeportlocal/portcache/port_table_test.go
index 7a1fe3b86..b7b93084b 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_test.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_test.go
@@ -24,6 +24,7 @@ const (
 	startPort = 61000
 	endPort   = 65000
 	podIP     = "10.0.0.1"
+	podKey    = "default/test-pod"
 	nodePort1 = startPort
 	nodePort2 = startPort + 1
 )
@@ -33,7 +34,7 @@ func newPortTable(mockIPTables rules.PodPortRules, mockPortOpener LocalPortOpene
 		PortTableCache: cache.NewIndexer(GetPortTableKey, cache.Indexers{
 			NodePortIndex:    NodePortIndexFunc,
 			PodEndpointIndex: PodEndpointIndexFunc,
-			PodIPIndex:       PodIPIndexFunc,
+			PodKeyIndex:      PodKeyIndexFunc,
 		}),
 		StartPort:       startPort,
 		EndPort:         endPort,
diff --git a/pkg/agent/nodeportlocal/portcache/port_table_windows_test.go b/pkg/agent/nodeportlocal/portcache/port_table_windows_test.go
index 18b3022b3..43855473e 100644
--- a/pkg/agent/nodeportlocal/portcache/port_table_windows_test.go
+++ b/pkg/agent/nodeportlocal/portcache/port_table_windows_test.go
@@ -36,18 +36,21 @@ func TestRestoreRules(t *testing.T) {
 	portTable := newPortTable(mockPortRules, mockPortOpener)
 	allNPLPorts := []rules.PodNodePort{
 		{
+			PodKey:   podKey,
 			NodePort: nodePort1,
 			PodPort:  1001,
 			PodIP:    podIP,
 			Protocol: "tcp",
 		},
 		{
+			PodKey:   podKey,
 			NodePort: nodePort1,
 			PodPort:  1001,
 			PodIP:    podIP,
 			Protocol: "udp",
 		},
 		{
+			PodKey:   podKey,
 			NodePort: nodePort2,
 			PodPort:  1002,
 			PodIP:    podIP,
@@ -70,6 +73,7 @@ func TestDeleteRule(t *testing.T) {
 	mockPortOpener := portcachetesting.NewMockLocalPortOpener(mockCtrl)
 	portTable := newPortTable(mockPortRules, mockPortOpener)
 	npData := &NodePortData{
+		PodKey:   podKey,
 		NodePort: startPort,
 		PodIP:    podIP,
 		PodPort:  1001,
@@ -80,7 +84,7 @@ func TestDeleteRule(t *testing.T) {
 
 	portTable.addPortTableCache(npData)
 	mockPortRules.EXPECT().DeleteRule(startPort, podIP, 1001, "tcp")
-	err := portTable.DeleteRule(podIP, 1001, "tcp")
+	err := portTable.DeleteRule(podKey, 1001, "tcp")
 	require.NoError(t, err)
 }
 
@@ -92,6 +96,7 @@ func TestDeleteRulesForPod(t *testing.T) {
 
 	npData := []*NodePortData{
 		{
+			PodKey:   podKey,
 			NodePort: startPort,
 			PodIP:    podIP,
 			PodPort:  1001,
@@ -100,6 +105,7 @@ func TestDeleteRulesForPod(t *testing.T) {
 			},
 		},
 		{
+			PodKey:   podKey,
 			NodePort: startPort + 1,
 			PodIP:    podIP,
 			PodPort:  1002,
@@ -114,7 +120,7 @@ func TestDeleteRulesForPod(t *testing.T) {
 		mockPortRules.EXPECT().DeleteRule(data.NodePort, podIP, data.PodPort, data.Protocol.Protocol)
 	}
 
-	err := portTable.DeleteRulesForPod(podIP)
+	err := portTable.DeleteRulesForPod(podKey)
 	require.NoError(t, err)
 }
 
@@ -127,11 +133,11 @@ func TestAddRule(t *testing.T) {
 
 	// Adding the rule the first time should succeed.
 	mockPortRules.EXPECT().AddRule(startPort, podIP, podPort, "udp")
-	gotNodePort, err := portTable.AddRule(podIP, podPort, "udp")
+	gotNodePort, err := portTable.AddRule(podKey, podPort, "udp", podIP)
 	require.NoError(t, err)
 	assert.Equal(t, startPort, gotNodePort)
 
 	// Add the same rule the second time should fail.
-	_, err = portTable.AddRule(podIP, podPort, "udp")
+	_, err = portTable.AddRule(podKey, podPort, "udp", podIP)
 	assert.ErrorContains(t, err, "existing Windows Nodeport entry for")
 }
diff --git a/pkg/agent/proxy/proxier_test.go b/pkg/agent/proxy/proxier_test.go
index 3901f7f02..482352544 100644
--- a/pkg/agent/proxy/proxier_test.go
+++ b/pkg/agent/proxy/proxier_test.go
@@ -534,6 +534,7 @@ func testClusterIPAdd(t *testing.T,
 	}
 	allSvcs := append(extraSvcs, makeTestClusterIPService(&svcPortName, svcIP, externalIPs, int32(svcPort), corev1.ProtocolTCP, nil, &internalTrafficPolicy, true, nil))
 	makeServiceMap(fp, allSvcs...)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, corev1.ProtocolTCP)
 
 	if !endpointSliceEnabled {
 		remoteEpSubset := makeTestEndpointSubset(&svcPortName, ep1IP, int32(svcPort), corev1.ProtocolTCP, false)
@@ -612,7 +613,7 @@ func testClusterIPAdd(t *testing.T,
 		}
 	}
 	if externalIP != nil {
-		mockRouteClient.EXPECT().AddExternalIPRoute(externalIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, externalIP)
 	}
 	fp.syncProxyRules()
 	assert.Contains(t, fp.serviceInstalledMap, svcPortName)
@@ -671,6 +672,7 @@ func testLoadBalancerAdd(t *testing.T,
 		&internalTrafficPolicy,
 		externalTrafficPolicy)
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, corev1.ProtocolTCP)
 
 	if !endpointSliceEnabled {
 		remoteEpSubset := makeTestEndpointSubset(&svcPortName, ep1IP, int32(svcPort), corev1.ProtocolTCP, false)
@@ -805,11 +807,11 @@ func testLoadBalancerAdd(t *testing.T,
 		}
 	}
 	if proxyLoadBalancerIPs {
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	}
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 	if externalIP != nil {
-		mockRouteClient.EXPECT().AddExternalIPRoute(externalIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, externalIP)
 	}
 
 	fp.syncProxyRules()
@@ -856,6 +858,7 @@ func testNodePortAdd(t *testing.T,
 		internalTrafficPolicy,
 		externalTrafficPolicy)
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, corev1.ProtocolTCP)
 
 	if !endpointSliceEnabled {
 		remoteEpSubset := makeTestEndpointSubset(&svcPortName, ep1IP, int32(svcPort), corev1.ProtocolTCP, false)
@@ -957,9 +960,9 @@ func testNodePortAdd(t *testing.T,
 			})
 		}
 	}
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 	if externalIP != nil {
-		mockRouteClient.EXPECT().AddExternalIPRoute(externalIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, externalIP)
 	}
 
 	fp.syncProxyRules()
@@ -1139,6 +1142,8 @@ func TestLoadBalancerServiceWithMultiplePorts(t *testing.T) {
 		},
 	}
 	makeServiceMap(fp, svc)
+	svcPort80InfoStr := fmt.Sprintf("%s:%d/%s", svc1IPv4, port80Int32, corev1.ProtocolTCP)
+	svcPort443InfoStr := fmt.Sprintf("%s:%d/%s", svc1IPv4, port443Int32, corev1.ProtocolTCP)
 
 	endpointSlice := &discovery.EndpointSlice{
 		ObjectMeta: metav1.ObjectMeta{
@@ -1227,8 +1232,8 @@ func TestLoadBalancerServiceWithMultiplePorts(t *testing.T) {
 		ClusterGroupID:     clusterGroupID1,
 		IsExternal:         true,
 	})
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(port30001Int32), binding.ProtocolTCP)
-	mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIPv4)
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(port30001Int32), binding.ProtocolTCP)
+	mockRouteClient.EXPECT().AddExternalIPConfigs(svcPort80InfoStr, loadBalancerIPv4)
 
 	localGroupID2 := fp.groupCounter.AllocateIfNotExist(svcPortName2, true)
 	clusterGroupID2 := fp.groupCounter.AllocateIfNotExist(svcPortName2, false)
@@ -1261,7 +1266,8 @@ func TestLoadBalancerServiceWithMultiplePorts(t *testing.T) {
 		ClusterGroupID:     clusterGroupID2,
 		IsExternal:         true,
 	})
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(port30002Int32), binding.ProtocolTCP)
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(port30002Int32), binding.ProtocolTCP)
+	mockRouteClient.EXPECT().AddExternalIPConfigs(svcPort443InfoStr, loadBalancerIPv4)
 
 	fp.syncProxyRules()
 
@@ -1274,20 +1280,19 @@ func TestLoadBalancerServiceWithMultiplePorts(t *testing.T) {
 	mockOFClient.EXPECT().UninstallServiceFlows(svc1IPv4, uint16(port80Int32), binding.ProtocolTCP)
 	mockOFClient.EXPECT().UninstallServiceFlows(agentconfig.VirtualNodePortDNATIPv4, uint16(port30001Int32), binding.ProtocolTCP)
 	mockOFClient.EXPECT().UninstallServiceFlows(loadBalancerIPv4, uint16(port80Int32), binding.ProtocolTCP)
-	mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(port30001Int32), binding.ProtocolTCP)
+	mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(port30001Int32), binding.ProtocolTCP)
+	mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcPort80InfoStr, loadBalancerIPv4)
 
 	mockOFClient.EXPECT().UninstallEndpointFlows(binding.ProtocolTCP, gomock.InAnyOrder([]k8sproxy.Endpoint{localEndpointForPort443, remoteEndpointForPort443}))
 	mockOFClient.EXPECT().UninstallServiceGroup(gomock.Any()).Times(2)
 	mockOFClient.EXPECT().UninstallServiceFlows(svc1IPv4, uint16(port443Int32), binding.ProtocolTCP)
 	mockOFClient.EXPECT().UninstallServiceFlows(agentconfig.VirtualNodePortDNATIPv4, uint16(port30002Int32), binding.ProtocolTCP)
 	mockOFClient.EXPECT().UninstallServiceFlows(loadBalancerIPv4, uint16(port443Int32), binding.ProtocolTCP)
-	mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(port30002Int32), binding.ProtocolTCP)
+	mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(port30002Int32), binding.ProtocolTCP)
 	// The route for the ClusterIP and the LoadBalancer IP should only be uninstalled once.
-	mockRouteClient.EXPECT().DeleteExternalIPRoute(loadBalancerIPv4)
+	mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcPort443InfoStr, loadBalancerIPv4)
 
 	fp.syncProxyRules()
-
-	assert.Emptyf(t, fp.serviceIPRouteReferences, "serviceIPRouteReferences was not cleaned up after Service was removed")
 }
 
 func TestNodePortAdd(t *testing.T) {
@@ -1507,6 +1512,7 @@ func testClusterIPRemove(t *testing.T, protocol binding.Protocol, isIPv6 bool, n
 	}
 	svc := makeTestClusterIPService(&svcPortName, svcIP, []net.IP{externalIP}, int32(svcPort), apiProtocol, nil, &internalTrafficPolicy, true, nil)
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
 
 	var ep *corev1.Endpoints
 	var eps *discovery.EndpointSlice
@@ -1577,8 +1583,8 @@ func testClusterIPRemove(t *testing.T, protocol binding.Protocol, isIPv6 bool, n
 		mockOFClient.EXPECT().UninstallServiceFlows(svcIP, uint16(svcPort), protocol)
 	}
 	if externalIP != nil {
-		mockRouteClient.EXPECT().AddExternalIPRoute(externalIP)
-		mockRouteClient.EXPECT().DeleteExternalIPRoute(externalIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, externalIP)
+		mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, externalIP)
 	}
 	if needClearConntrackEntries(protocol) {
 		mockRouteClient.EXPECT().ClearConntrackEntryForService(svcIP, uint16(svcPort), nil, protocol)
@@ -1634,6 +1640,7 @@ func testNodePortRemove(t *testing.T, protocol binding.Protocol, isIPv6 bool, en
 		corev1.ServiceInternalTrafficPolicyCluster,
 		corev1.ServiceExternalTrafficPolicyTypeLocal)
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
 
 	var ep *corev1.Endpoints
 	var eps *discovery.EndpointSlice
@@ -1667,7 +1674,7 @@ func testNodePortRemove(t *testing.T, protocol binding.Protocol, isIPv6 bool, en
 		IsExternal:         true,
 		IsNodePort:         true,
 	})
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 	if externalIP != nil {
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
 			ServiceIP:          externalIP,
@@ -1678,17 +1685,17 @@ func testNodePortRemove(t *testing.T, protocol binding.Protocol, isIPv6 bool, en
 			ClusterGroupID:     2,
 			IsExternal:         true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(externalIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, externalIP)
 	}
 
 	mockOFClient.EXPECT().UninstallEndpointFlows(protocol, gomock.Any())
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP, uint16(svcPort), protocol)
 	mockOFClient.EXPECT().UninstallServiceFlows(virtualNodePortDNATIP, uint16(svcNodePort), protocol)
 	mockOFClient.EXPECT().UninstallServiceGroup(gomock.Any()).Times(2)
-	mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+	mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 	if externalIP != nil {
 		mockOFClient.EXPECT().UninstallServiceFlows(externalIP, uint16(svcPort), protocol)
-		mockRouteClient.EXPECT().DeleteExternalIPRoute(externalIP)
+		mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, externalIP)
 	}
 	if needClearConntrackEntries(protocol) {
 		mockRouteClient.EXPECT().ClearConntrackEntryForService(svcIP, uint16(svcPort), nil, protocol)
@@ -1751,6 +1758,7 @@ func testLoadBalancerRemove(t *testing.T, protocol binding.Protocol, isIPv6 bool
 		&internalTrafficPolicy,
 		externalTrafficPolicy)
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
 
 	var ep *corev1.Endpoints
 	var eps *discovery.EndpointSlice
@@ -1793,8 +1801,8 @@ func testLoadBalancerRemove(t *testing.T, protocol binding.Protocol, isIPv6 bool
 		ClusterGroupID:     2,
 		IsExternal:         true,
 	})
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
-	mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
+	mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	if externalIP != nil {
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
 			ServiceIP:          externalIP,
@@ -1805,7 +1813,7 @@ func testLoadBalancerRemove(t *testing.T, protocol binding.Protocol, isIPv6 bool
 			ClusterGroupID:     2,
 			IsExternal:         true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(externalIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, externalIP)
 	}
 
 	mockOFClient.EXPECT().UninstallEndpointFlows(protocol, gomock.Any())
@@ -1813,11 +1821,11 @@ func testLoadBalancerRemove(t *testing.T, protocol binding.Protocol, isIPv6 bool
 	mockOFClient.EXPECT().UninstallServiceFlows(virtualNodePortDNATIP, uint16(svcNodePort), protocol)
 	mockOFClient.EXPECT().UninstallServiceFlows(loadBalancerIP, uint16(svcPort), protocol)
 	mockOFClient.EXPECT().UninstallServiceGroup(gomock.Any()).Times(2)
-	mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
-	mockRouteClient.EXPECT().DeleteExternalIPRoute(loadBalancerIP)
+	mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
+	mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	if externalIP != nil {
 		mockOFClient.EXPECT().UninstallServiceFlows(externalIP, uint16(svcPort), protocol)
-		mockRouteClient.EXPECT().DeleteExternalIPRoute(externalIP)
+		mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, externalIP)
 	}
 	if needClearConntrackEntries(protocol) {
 		mockRouteClient.EXPECT().ClearConntrackEntryForService(svcIP, uint16(svcPort), nil, protocol)
@@ -2098,11 +2106,11 @@ func testNodePortNoEndpoint(t *testing.T, protocol binding.Protocol, isIPv6 bool
 		IsExternal:         true,
 		IsNodePort:         true,
 	})
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), gomock.Any())
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), gomock.Any())
 	fp.syncProxyRules()
 
 	mockOFClient.EXPECT().UninstallServiceFlows(virtualNodePortDNATIP, uint16(svcNodePort), gomock.Any())
-	mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(svcNodePort), gomock.Any())
+	mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(svcNodePort), gomock.Any())
 	if needClearConntrackEntries(protocol) {
 		for _, nodeIP := range nodePortAddresses {
 			mockRouteClient.EXPECT().ClearConntrackEntryForService(nodeIP, uint16(svcNodePort), nil, protocol)
@@ -2119,7 +2127,7 @@ func testNodePortNoEndpoint(t *testing.T, protocol binding.Protocol, isIPv6 bool
 		IsExternal:         true,
 		IsNodePort:         true,
 	})
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort)+1, gomock.Any())
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort)+1, gomock.Any())
 	fp.serviceChanges.OnServiceUpdate(svc, updatedSvc)
 	fp.syncProxyRules()
 }
@@ -2180,6 +2188,8 @@ func testLoadBalancerNoEndpoint(t *testing.T, protocol binding.Protocol, isIPv6
 		externalTrafficPolicy)
 	makeServiceMap(fp, svc)
 	makeEndpointSliceMap(fp)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
+	updatedSvcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort+1, apiProtocol)
 
 	mockOFClient.EXPECT().InstallServiceGroup(binding.GroupIDType(1), false, gomock.Any())
 	mockOFClient.EXPECT().InstallServiceGroup(binding.GroupIDType(2), false, gomock.Any())
@@ -2209,8 +2219,8 @@ func testLoadBalancerNoEndpoint(t *testing.T, protocol binding.Protocol, isIPv6
 		ClusterGroupID:     2,
 		IsExternal:         true,
 	})
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), gomock.Any())
-	mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), gomock.Any())
+	mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	fp.syncProxyRules()
 
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP, uint16(svcPort), gomock.Any())
@@ -2220,8 +2230,8 @@ func testLoadBalancerNoEndpoint(t *testing.T, protocol binding.Protocol, isIPv6
 		mockRouteClient.EXPECT().ClearConntrackEntryForService(svcIP, uint16(svcPort), nil, protocol)
 		mockRouteClient.EXPECT().ClearConntrackEntryForService(loadBalancerIP, uint16(svcPort), nil, protocol)
 	}
-	mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(svcNodePort), gomock.Any())
-	mockRouteClient.EXPECT().DeleteExternalIPRoute(loadBalancerIP)
+	mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(svcNodePort), gomock.Any())
+	mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
 		ServiceIP:      svcIP,
 		ServicePort:    uint16(svcPort + 1),
@@ -2248,8 +2258,8 @@ func testLoadBalancerNoEndpoint(t *testing.T, protocol binding.Protocol, isIPv6
 		ClusterGroupID:     2,
 		IsExternal:         true,
 	})
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), gomock.Any())
-	mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), gomock.Any())
+	mockRouteClient.EXPECT().AddExternalIPConfigs(updatedSvcInfoStr, loadBalancerIP)
 	fp.serviceChanges.OnServiceUpdate(svc, updatedSvc)
 	fp.syncProxyRules()
 }
@@ -2367,6 +2377,7 @@ func testLoadBalancerRemoveEndpoints(t *testing.T, protocol binding.Protocol, is
 		&internalTrafficPolicy,
 		externalTrafficPolicy)
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
 
 	ep, epPort := makeTestEndpointSliceEndpointAndPort(&svcPortName, epIP, int32(svcPort), apiProtocol, false)
 	eps := makeTestEndpointSlice(svcPortName.Namespace, svcPortName.Name, []discovery.Endpoint{*ep}, []discovery.EndpointPort{*epPort}, isIPv6)
@@ -2402,9 +2413,9 @@ func testLoadBalancerRemoveEndpoints(t *testing.T, protocol binding.Protocol, is
 		IsExternal:     true,
 		ClusterGroupID: 1,
 	})
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
-	mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
-	mockRouteClient.EXPECT().AddExternalIPRoute(externalIP)
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
+	mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
+	mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, externalIP)
 
 	fp.syncProxyRules()
 
@@ -2587,6 +2598,8 @@ func testServicePortUpdate(t *testing.T, protocol binding.Protocol, isIPv6 bool,
 		updatedSvc = makeTestLoadBalancerService(&svcPortName, svcIP, nil, []net.IP{loadBalancerIP}, nil, int32(svcPort+1), int32(svcNodePort), apiProtocol, nil, nil, corev1.ServiceExternalTrafficPolicyTypeCluster)
 	}
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
+	updatedSvcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort+1, apiProtocol)
 
 	ep, epPort := makeTestEndpointSliceEndpointAndPort(&svcPortName, epIP, int32(svcPort), apiProtocol, false)
 	eps := makeTestEndpointSlice(svcPortName.Namespace, svcPortName.Name, []discovery.Endpoint{*ep}, []discovery.EndpointPort{*epPort}, isIPv6)
@@ -2624,7 +2637,7 @@ func testServicePortUpdate(t *testing.T, protocol binding.Protocol, isIPv6 bool,
 			IsExternal:     true,
 			IsNodePort:     true,
 		})
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 
 		mockOFClient.EXPECT().UninstallServiceFlows(virtualNodePortDNATIP, uint16(svcNodePort), protocol)
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
@@ -2635,8 +2648,8 @@ func testServicePortUpdate(t *testing.T, protocol binding.Protocol, isIPv6 bool,
 			IsExternal:     true,
 			IsNodePort:     true,
 		})
-		mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 	}
 	if svcType == corev1.ServiceTypeLoadBalancer {
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
@@ -2646,7 +2659,7 @@ func testServicePortUpdate(t *testing.T, protocol binding.Protocol, isIPv6 bool,
 			ClusterGroupID: 1,
 			IsExternal:     true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 
 		s1 = mockOFClient.EXPECT().UninstallServiceFlows(loadBalancerIP, uint16(svcPort), protocol)
 		if needClearConntrackEntries(protocol) {
@@ -2661,8 +2674,8 @@ func testServicePortUpdate(t *testing.T, protocol binding.Protocol, isIPv6 bool,
 		})
 		s2.After(s1)
 
-		mockRouteClient.EXPECT().DeleteExternalIPRoute(loadBalancerIP)
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(updatedSvcInfoStr, loadBalancerIP)
 	}
 
 	fp.syncProxyRules()
@@ -2742,6 +2755,7 @@ func testServiceNodePortUpdate(t *testing.T, protocol binding.Protocol, isIPv6 b
 		updatedSvc = makeTestLoadBalancerService(&svcPortName, svcIP, nil, []net.IP{loadBalancerIP}, []net.IP{loadBalancerIPModeProxyIP}, int32(svcPort), int32(svcNodePort+1), apiProtocol, nil, nil, corev1.ServiceExternalTrafficPolicyTypeCluster)
 	}
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
 
 	ep, epPort := makeTestEndpointSliceEndpointAndPort(&svcPortName, epIP, int32(svcPort), apiProtocol, false)
 	eps := makeTestEndpointSlice(svcPortName.Namespace, svcPortName.Name, []discovery.Endpoint{*ep}, []discovery.EndpointPort{*epPort}, isIPv6)
@@ -2767,10 +2781,10 @@ func testServiceNodePortUpdate(t *testing.T, protocol binding.Protocol, isIPv6 b
 			IsExternal:     true,
 			IsNodePort:     true,
 		})
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 
 		s1 := mockOFClient.EXPECT().UninstallServiceFlows(virtualNodePortDNATIP, uint16(svcNodePort), protocol)
-		mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 		if needClearConntrackEntries(protocol) {
 			for _, nodeIP := range nodePortAddresses {
 				mockRouteClient.EXPECT().ClearConntrackEntryForService(nodeIP, uint16(svcNodePort), nil, protocol)
@@ -2785,7 +2799,7 @@ func testServiceNodePortUpdate(t *testing.T, protocol binding.Protocol, isIPv6 b
 			IsExternal:     true,
 			IsNodePort:     true,
 		})
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort+1), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort+1), protocol)
 		s2.After(s1)
 	}
 	if svcType == corev1.ServiceTypeLoadBalancer {
@@ -2796,7 +2810,7 @@ func testServiceNodePortUpdate(t *testing.T, protocol binding.Protocol, isIPv6 b
 			ClusterGroupID: 1,
 			IsExternal:     true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	}
 
 	fp.syncProxyRules()
@@ -2869,6 +2883,7 @@ func testServiceExternalTrafficPolicyUpdate(t *testing.T, protocol binding.Proto
 	updatedSvc = svc.DeepCopy()
 	updatedSvc.Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicyTypeLocal
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
 
 	remoteEp, remoteEpPort := makeTestEndpointSliceEndpointAndPort(&svcPortName, ep1IP, int32(svcPort), apiProtocol, false)
 	localEp, localEpPort := makeTestEndpointSliceEndpointAndPort(&svcPortName, ep2IP, int32(svcPort), apiProtocol, true)
@@ -2897,7 +2912,7 @@ func testServiceExternalTrafficPolicyUpdate(t *testing.T, protocol binding.Proto
 		ClusterGroupID: 1,
 		IsExternal:     true,
 	})
-	mockRouteClient.EXPECT().AddExternalIPRoute(externalIP)
+	mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, externalIP)
 
 	if svcType == corev1.ServiceTypeNodePort || svcType == corev1.ServiceTypeLoadBalancer {
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
@@ -2908,7 +2923,7 @@ func testServiceExternalTrafficPolicyUpdate(t *testing.T, protocol binding.Proto
 			IsExternal:     true,
 			IsNodePort:     true,
 		})
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 	}
 	if svcType == corev1.ServiceTypeLoadBalancer {
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
@@ -2918,7 +2933,7 @@ func testServiceExternalTrafficPolicyUpdate(t *testing.T, protocol binding.Proto
 			ClusterGroupID: 1,
 			IsExternal:     true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	}
 	fp.syncProxyRules()
 	assert.Contains(t, fp.serviceInstalledMap, svcPortName)
@@ -2946,8 +2961,8 @@ func testServiceExternalTrafficPolicyUpdate(t *testing.T, protocol binding.Proto
 		TrafficPolicyLocal: true,
 		IsExternal:         true,
 	})
-	mockRouteClient.EXPECT().DeleteExternalIPRoute(externalIP)
-	mockRouteClient.EXPECT().AddExternalIPRoute(externalIP)
+	mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, externalIP)
+	mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, externalIP)
 
 	if svcType == corev1.ServiceTypeNodePort || svcType == corev1.ServiceTypeLoadBalancer {
 		s1 := mockOFClient.EXPECT().UninstallServiceFlows(virtualNodePortDNATIP, uint16(svcNodePort), protocol)
@@ -2963,8 +2978,8 @@ func testServiceExternalTrafficPolicyUpdate(t *testing.T, protocol binding.Proto
 		})
 		s2.After(s1)
 
-		mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 	}
 	if svcType == corev1.ServiceTypeLoadBalancer {
 		s1 := mockOFClient.EXPECT().UninstallServiceFlows(loadBalancerIP, uint16(svcPort), protocol)
@@ -2979,8 +2994,8 @@ func testServiceExternalTrafficPolicyUpdate(t *testing.T, protocol binding.Proto
 		})
 		s2.After(s1)
 
-		mockRouteClient.EXPECT().DeleteExternalIPRoute(loadBalancerIP)
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	}
 	fp.syncProxyRules()
 	assert.Contains(t, fp.serviceInstalledMap, svcPortName)
@@ -3153,24 +3168,25 @@ func testServiceExternalIPsUpdate(t *testing.T, protocol binding.Protocol, isIPv
 	}
 	fp := newFakeProxier(mockRouteClient, mockOFClient, nodePortAddresses, groupAllocator, isIPv6, withProxyAll, withCleanupStaleUDPSvcConntrack)
 
-	var loadBalancerIPStrs, updatedLoadBalancerIPStrs []string
+	var loadBalancerIPStrings, updatedLoadBalancerIPStrings []string
 	for _, ip := range loadBalancerIPs {
-		loadBalancerIPStrs = append(loadBalancerIPStrs, ip.String())
+		loadBalancerIPStrings = append(loadBalancerIPStrings, ip.String())
 	}
 	for _, ip := range updatedLoadBalancerIPs {
-		updatedLoadBalancerIPStrs = append(updatedLoadBalancerIPStrs, ip.String())
+		updatedLoadBalancerIPStrings = append(updatedLoadBalancerIPStrings, ip.String())
 	}
-	var externalIPStrs, updatedExternalIPStrs []string
+	var externalIPStrings, updatedExternalIPStrings []string
 	for _, ip := range externalIPs {
-		externalIPStrs = append(externalIPStrs, ip.String())
+		externalIPStrings = append(externalIPStrings, ip.String())
 	}
 	for _, ip := range updatedExternalIPs {
-		updatedExternalIPStrs = append(updatedExternalIPStrs, ip.String())
+		updatedExternalIPStrings = append(updatedExternalIPStrings, ip.String())
 	}
 
 	svc := makeTestLoadBalancerService(&svcPortName, svcIP, externalIPs, loadBalancerIPs, nil, int32(svcPort), int32(svcNodePort), apiProtocol, nil, nil, corev1.ServiceExternalTrafficPolicyTypeCluster)
 	updatedSvc := makeTestLoadBalancerService(&svcPortName, svcIP, updatedExternalIPs, updatedLoadBalancerIPs, nil, int32(svcPort), int32(svcNodePort), apiProtocol, nil, nil, corev1.ServiceExternalTrafficPolicyTypeCluster)
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
 
 	ep, epPort := makeTestEndpointSliceEndpointAndPort(&svcPortName, epIP, int32(svcPort), apiProtocol, false)
 	eps := makeTestEndpointSlice(svcPortName.Namespace, svcPortName.Name, []discovery.Endpoint{*ep}, []discovery.EndpointPort{*epPort}, isIPv6)
@@ -3202,6 +3218,7 @@ func testServiceExternalIPsUpdate(t *testing.T, protocol binding.Protocol, isIPv
 			ClusterGroupID: 1,
 			IsExternal:     true,
 		})
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, ip)
 	}
 	for _, ip := range externalIPs {
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
@@ -3211,52 +3228,51 @@ func testServiceExternalIPsUpdate(t *testing.T, protocol binding.Protocol, isIPv
 			ClusterGroupID: 1,
 			IsExternal:     true,
 		})
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, ip)
 	}
-	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
-	for _, ip := range loadBalancerIPs {
-		mockRouteClient.EXPECT().AddExternalIPRoute(ip)
-	}
-	for _, ip := range externalIPs {
-		mockRouteClient.EXPECT().AddExternalIPRoute(ip)
-	}
+	mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 
-	toDeleteLoadBalancerIPs := smallSliceDifference(loadBalancerIPStrs, updatedLoadBalancerIPStrs)
-	toAddLoadBalancerIPs := smallSliceDifference(updatedLoadBalancerIPStrs, loadBalancerIPStrs)
-	for _, ipStr := range toDeleteLoadBalancerIPs {
-		mockOFClient.EXPECT().UninstallServiceFlows(net.ParseIP(ipStr), uint16(svcPort), protocol)
-		mockRouteClient.EXPECT().DeleteExternalIPRoute(net.ParseIP(ipStr))
+	toDeleteLoadBalancerIPStrings := smallSliceDifference(loadBalancerIPStrings, updatedLoadBalancerIPStrings)
+	toAddLoadBalancerIPStrings := smallSliceDifference(updatedLoadBalancerIPStrings, loadBalancerIPStrings)
+	for _, ipStr := range toDeleteLoadBalancerIPStrings {
+		ip := net.ParseIP(ipStr)
+		mockOFClient.EXPECT().UninstallServiceFlows(ip, uint16(svcPort), protocol)
+		mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, ip)
 		if needClearConntrackEntries(protocol) {
-			mockRouteClient.EXPECT().ClearConntrackEntryForService(net.ParseIP(ipStr), uint16(svcPort), nil, protocol)
+			mockRouteClient.EXPECT().ClearConntrackEntryForService(ip, uint16(svcPort), nil, protocol)
 		}
 	}
-	for _, ipStr := range toAddLoadBalancerIPs {
+	for _, ipStr := range toAddLoadBalancerIPStrings {
+		ip := net.ParseIP(ipStr)
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
-			ServiceIP:      net.ParseIP(ipStr),
+			ServiceIP:      ip,
 			ServicePort:    uint16(svcPort),
 			Protocol:       protocol,
 			ClusterGroupID: 1,
 			IsExternal:     true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(net.ParseIP(ipStr))
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, ip)
 	}
-	toDeleteExternalIPs := smallSliceDifference(externalIPStrs, updatedExternalIPStrs)
-	toAddLoadExternalIPs := smallSliceDifference(updatedExternalIPStrs, externalIPStrs)
-	for _, ipStr := range toDeleteExternalIPs {
-		mockOFClient.EXPECT().UninstallServiceFlows(net.ParseIP(ipStr), uint16(svcPort), protocol)
-		mockRouteClient.EXPECT().DeleteExternalIPRoute(net.ParseIP(ipStr))
+	toDeleteExternalIPStrings := smallSliceDifference(externalIPStrings, updatedExternalIPStrings)
+	toAddLoadExternalIPStrings := smallSliceDifference(updatedExternalIPStrings, externalIPStrings)
+	for _, ipStr := range toDeleteExternalIPStrings {
+		ip := net.ParseIP(ipStr)
+		mockOFClient.EXPECT().UninstallServiceFlows(ip, uint16(svcPort), protocol)
+		mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, ip)
 		if needClearConntrackEntries(protocol) {
-			mockRouteClient.EXPECT().ClearConntrackEntryForService(net.ParseIP(ipStr), uint16(svcPort), nil, protocol)
+			mockRouteClient.EXPECT().ClearConntrackEntryForService(ip, uint16(svcPort), nil, protocol)
 		}
 	}
-	for _, ipStr := range toAddLoadExternalIPs {
+	for _, ipStr := range toAddLoadExternalIPStrings {
+		ip := net.ParseIP(ipStr)
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
-			ServiceIP:      net.ParseIP(ipStr),
+			ServiceIP:      ip,
 			ServicePort:    uint16(svcPort),
 			Protocol:       protocol,
 			ClusterGroupID: 1,
 			IsExternal:     true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(net.ParseIP(ipStr))
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, ip)
 	}
 
 	fp.syncProxyRules()
@@ -3317,6 +3333,7 @@ func testServiceStickyMaxAgeSecondsUpdate(t *testing.T, protocol binding.Protoco
 		updatedSvc = makeTestLoadBalancerService(&svcPortName, svcIP, nil, []net.IP{loadBalancerIP}, []net.IP{loadBalancerIPModeProxyIP}, int32(svcPort), int32(svcNodePort), apiProtocol, &updatedAffinitySeconds, nil, corev1.ServiceExternalTrafficPolicyTypeCluster)
 	}
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
 
 	ep, epPort := makeTestEndpointSliceEndpointAndPort(&svcPortName, epIP, int32(svcPort), apiProtocol, false)
 	eps := makeTestEndpointSlice(svcPortName.Namespace, svcPortName.Name, []discovery.Endpoint{*ep}, []discovery.EndpointPort{*epPort}, isIPv6)
@@ -3352,9 +3369,9 @@ func testServiceStickyMaxAgeSecondsUpdate(t *testing.T, protocol binding.Protoco
 			IsNodePort:      true,
 			AffinityTimeout: uint16(affinitySeconds),
 		})
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 		mockOFClient.EXPECT().UninstallServiceFlows(virtualNodePortDNATIP, uint16(svcNodePort), protocol)
-		mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
 			ServiceIP:       virtualNodePortDNATIP,
 			ServicePort:     uint16(svcNodePort),
@@ -3364,7 +3381,7 @@ func testServiceStickyMaxAgeSecondsUpdate(t *testing.T, protocol binding.Protoco
 			IsNodePort:      true,
 			AffinityTimeout: uint16(updatedAffinitySeconds),
 		})
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 	}
 	if svcType == corev1.ServiceTypeLoadBalancer {
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
@@ -3375,9 +3392,9 @@ func testServiceStickyMaxAgeSecondsUpdate(t *testing.T, protocol binding.Protoco
 			AffinityTimeout: uint16(affinitySeconds),
 			IsExternal:      true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 		mockOFClient.EXPECT().UninstallServiceFlows(loadBalancerIP, uint16(svcPort), protocol)
-		mockRouteClient.EXPECT().DeleteExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, loadBalancerIP)
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
 			ServiceIP:       loadBalancerIP,
 			ServicePort:     uint16(svcPort),
@@ -3386,7 +3403,7 @@ func testServiceStickyMaxAgeSecondsUpdate(t *testing.T, protocol binding.Protoco
 			AffinityTimeout: uint16(updatedAffinitySeconds),
 			IsExternal:      true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	}
 
 	fp.syncProxyRules()
@@ -3470,6 +3487,7 @@ func testServiceSessionAffinityTypeUpdate(t *testing.T, protocol binding.Protoco
 		updatedSvc = makeTestLoadBalancerService(&svcPortName, svcIP, nil, []net.IP{loadBalancerIP}, []net.IP{loadBalancerIPModeProxyIP}, int32(svcPort), int32(svcNodePort), apiProtocol, &affinitySeconds, nil, corev1.ServiceExternalTrafficPolicyTypeCluster)
 	}
 	makeServiceMap(fp, svc)
+	svcInfoStr := fmt.Sprintf("%s:%d/%s", svcIP, svcPort, apiProtocol)
 
 	ep, epPort := makeTestEndpointSliceEndpointAndPort(&svcPortName, epIP, int32(svcPort), apiProtocol, false)
 	eps := makeTestEndpointSlice(svcPortName.Namespace, svcPortName.Name, []discovery.Endpoint{*ep}, []discovery.EndpointPort{*epPort}, isIPv6)
@@ -3505,7 +3523,7 @@ func testServiceSessionAffinityTypeUpdate(t *testing.T, protocol binding.Protoco
 			IsExternal:     true,
 			IsNodePort:     true,
 		})
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 
 		mockOFClient.EXPECT().UninstallServiceFlows(virtualNodePortDNATIP, uint16(svcNodePort), protocol)
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
@@ -3517,8 +3535,8 @@ func testServiceSessionAffinityTypeUpdate(t *testing.T, protocol binding.Protoco
 			IsNodePort:      true,
 			AffinityTimeout: uint16(affinitySeconds),
 		})
-		mockRouteClient.EXPECT().DeleteNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
-		mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().DeleteNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
+		mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddresses, uint16(svcNodePort), protocol)
 	}
 	if svcType == corev1.ServiceTypeLoadBalancer {
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
@@ -3528,7 +3546,7 @@ func testServiceSessionAffinityTypeUpdate(t *testing.T, protocol binding.Protoco
 			ClusterGroupID: 1,
 			IsExternal:     true,
 		})
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 
 		mockOFClient.EXPECT().UninstallServiceFlows(loadBalancerIP, uint16(svcPort), protocol)
 		mockOFClient.EXPECT().InstallServiceFlows(&antreatypes.ServiceConfig{
@@ -3539,8 +3557,8 @@ func testServiceSessionAffinityTypeUpdate(t *testing.T, protocol binding.Protoco
 			IsExternal:      true,
 			AffinityTimeout: uint16(affinitySeconds),
 		})
-		mockRouteClient.EXPECT().DeleteExternalIPRoute(loadBalancerIP)
-		mockRouteClient.EXPECT().AddExternalIPRoute(loadBalancerIP)
+		mockRouteClient.EXPECT().DeleteExternalIPConfigs(svcInfoStr, loadBalancerIP)
+		mockRouteClient.EXPECT().AddExternalIPConfigs(svcInfoStr, loadBalancerIP)
 	}
 
 	fp.syncProxyRules()
@@ -3779,7 +3797,7 @@ func TestGetServiceFlowKeys(t *testing.T) {
 				makeEndpointSliceMap(fp, eps)
 			}
 			if tc.svc != nil && tc.eps != nil && tc.serviceInstalled {
-				mockRouteClient.EXPECT().AddNodePort(nodePortAddressesIPv4, uint16(svcNodePort), binding.ProtocolTCP)
+				mockRouteClient.EXPECT().AddNodePortConfigs(nodePortAddressesIPv4, uint16(svcNodePort), binding.ProtocolTCP)
 				mockOFClient.EXPECT().InstallServiceGroup(binding.GroupIDType(1), gomock.Any(), gomock.Any())
 				mockOFClient.EXPECT().InstallServiceGroup(binding.GroupIDType(2), gomock.Any(), gomock.Any())
 				mockOFClient.EXPECT().InstallEndpointFlows(binding.ProtocolTCP, gomock.Any())
diff --git a/pkg/agent/route/route_linux_test.go b/pkg/agent/route/route_linux_test.go
index 5104f81c5..3107fef19 100644
--- a/pkg/agent/route/route_linux_test.go
+++ b/pkg/agent/route/route_linux_test.go
@@ -25,6 +25,7 @@ import (
 	"go.uber.org/mock/gomock"
 	"golang.org/x/sys/unix"
 	"k8s.io/apimachinery/pkg/util/sets"
+	utilnet "k8s.io/utils/net"
 
 	"antrea.io/antrea/pkg/agent/config"
 	"antrea.io/antrea/pkg/agent/openflow"
@@ -52,6 +53,13 @@ var (
 	ipv4Route2 = generateRoute(net.ParseIP(externalIPv4Addr2), 32, config.VirtualServiceIPv4, 10, netlink.SCOPE_UNIVERSE)
 	ipv6Route1 = generateRoute(net.ParseIP(externalIPv6Addr1), 128, config.VirtualServiceIPv6, 10, netlink.SCOPE_UNIVERSE)
 	ipv6Route2 = generateRoute(net.ParseIP(externalIPv6Addr2), 128, config.VirtualServiceIPv6, 10, netlink.SCOPE_UNIVERSE)
+
+	serviceIPSets = map[string]*sync.Map{
+		antreaNodePortIPSet:    {},
+		antreaNodePortIP6Set:   {},
+		antreaExternalIPIPSet:  {},
+		antreaExternalIPIP6Set: {},
+	}
 )
 
 func TestSyncRoutes(t *testing.T) {
@@ -157,8 +165,7 @@ func TestSyncIPSet(t *testing.T) {
 		nodeNetworkPolicyEnabled    bool
 		networkConfig               *config.NetworkConfig
 		nodeConfig                  *config.NodeConfig
-		nodePortsIPv4               []string
-		nodePortsIPv6               []string
+		serviceIPSets               map[string][]string
 		clusterNodeIPs              map[string]string
 		clusterNodeIP6s             map[string]string
 		nodeNetworkPolicyIPSetsIPv4 map[string]sets.Set[string]
@@ -204,8 +211,12 @@ func TestSyncIPSet(t *testing.T) {
 				PodIPv4CIDR: podCIDR,
 				PodIPv6CIDR: podCIDRv6,
 			},
-			nodePortsIPv4:               []string{"192.168.0.2,tcp:10000", "127.0.0.1,tcp:10000"},
-			nodePortsIPv6:               []string{"fe80::e643:4bff:fe44:ee,tcp:10000", "::1,tcp:10000"},
+			serviceIPSets: map[string][]string{
+				antreaNodePortIPSet:    {"192.168.0.2,tcp:10000", "127.0.0.1,tcp:10000"},
+				antreaNodePortIP6Set:   {"fe80::e643:4bff:fe44:ee,tcp:10000", "::1,tcp:10000"},
+				antreaExternalIPIPSet:  {"192.168.0.200", "192.168.0.201", "192.168.0.150", "192.168.0.151"},
+				antreaExternalIPIP6Set: {"2001::192:168:0:200", "2001::192:168:0:201", "2001::192:168:0:150", "2001::192:168:0:151"},
+			},
 			clusterNodeIPs:              map[string]string{"172.16.3.0/24": "192.168.0.3", "172.16.4.0/24": "192.168.0.4"},
 			clusterNodeIP6s:             map[string]string{"2001:ab03:cd04:5503::/64": "fe80::e643:4bff:fe03", "2001:ab03:cd04:5504::/64": "fe80::e643:4bff:fe04"},
 			nodeNetworkPolicyIPSetsIPv4: map[string]sets.Set[string]{"ANTREA-POL-RULE1-4": sets.New[string]("1.1.1.1/32", "2.2.2.2/32")},
@@ -221,6 +232,16 @@ func TestSyncIPSet(t *testing.T) {
 				mockIPSet.AddEntry(antreaNodePortIPSet, "127.0.0.1,tcp:10000")
 				mockIPSet.AddEntry(antreaNodePortIP6Set, "fe80::e643:4bff:fe44:ee,tcp:10000")
 				mockIPSet.AddEntry(antreaNodePortIP6Set, "::1,tcp:10000")
+				mockIPSet.CreateIPSet(antreaExternalIPIPSet, ipset.HashIP, false)
+				mockIPSet.CreateIPSet(antreaExternalIPIP6Set, ipset.HashIP, true)
+				mockIPSet.AddEntry(antreaExternalIPIPSet, "192.168.0.150")
+				mockIPSet.AddEntry(antreaExternalIPIPSet, "192.168.0.151")
+				mockIPSet.AddEntry(antreaExternalIPIP6Set, "2001::192:168:0:150")
+				mockIPSet.AddEntry(antreaExternalIPIP6Set, "2001::192:168:0:151")
+				mockIPSet.AddEntry(antreaExternalIPIPSet, "192.168.0.200")
+				mockIPSet.AddEntry(antreaExternalIPIPSet, "192.168.0.201")
+				mockIPSet.AddEntry(antreaExternalIPIP6Set, "2001::192:168:0:200")
+				mockIPSet.AddEntry(antreaExternalIPIP6Set, "2001::192:168:0:201")
 				mockIPSet.CreateIPSet(clusterNodeIPSet, ipset.HashIP, false)
 				mockIPSet.CreateIPSet(clusterNodeIP6Set, ipset.HashIP, true)
 				mockIPSet.AddEntry(clusterNodeIPSet, "192.168.0.3")
@@ -268,16 +289,15 @@ func TestSyncIPSet(t *testing.T) {
 				multicastEnabled:         tt.multicastEnabled,
 				connectUplinkToBridge:    tt.connectUplinkToBridge,
 				nodeNetworkPolicyEnabled: tt.nodeNetworkPolicyEnabled,
-				nodePortsIPv4:            sync.Map{},
-				nodePortsIPv6:            sync.Map{},
 				clusterNodeIPs:           sync.Map{},
 				clusterNodeIP6s:          sync.Map{},
+				serviceIPSets:            make(map[string]*sync.Map),
 			}
-			for _, nodePortIPv4 := range tt.nodePortsIPv4 {
-				c.nodePortsIPv4.Store(nodePortIPv4, struct{}{})
-			}
-			for _, nodePortIPv6 := range tt.nodePortsIPv6 {
-				c.nodePortsIPv6.Store(nodePortIPv6, struct{}{})
+			for ipsetName, ipsetEntries := range tt.serviceIPSets {
+				c.serviceIPSets[ipsetName] = &sync.Map{}
+				for _, entry := range ipsetEntries {
+					c.serviceIPSets[ipsetName].Store(entry, struct{}{})
+				}
 			}
 			for cidr, nodeIP := range tt.clusterNodeIPs {
 				c.clusterNodeIPs.Store(cidr, nodeIP)
@@ -307,8 +327,6 @@ func TestSyncIPTables(t *testing.T) {
 		nodeNetworkPolicyEnabled bool
 		networkConfig            *config.NetworkConfig
 		nodeConfig               *config.NodeConfig
-		nodePortsIPv4            []string
-		nodePortsIPv6            []string
 		markToSNATIP             map[uint32]string
 		expectedCalls            func(iptables *iptablestest.MockInterfaceMockRecorder)
 	}{
@@ -347,10 +365,34 @@ func TestSyncIPTables(t *testing.T) {
 				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.MangleTable, iptables.PreRoutingChain, []string{"-j", antreaMangleChain, "-m", "comment", "--comment", "Antrea: jump to Antrea mangle rules"})
 				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.MangleTable, antreaOutputChain)
 				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.MangleTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"})
+				mockIPTables.ListRules(iptables.ProtocolDual, iptables.NATTable, iptables.PreRoutingChain).Return(
+					map[iptables.Protocol][]string{
+						iptables.ProtocolIPv4: {
+							"-A " + iptables.PreRoutingChain + " -j " + kubeProxyServiceChain,
+							"-A " + iptables.PreRoutingChain + " -j " + antreaPreRoutingChain,
+						},
+						iptables.ProtocolIPv6: {
+							"-A " + iptables.PreRoutingChain + " -j " + antreaPreRoutingChain,
+							"-A " + iptables.PreRoutingChain + " -j " + kubeProxyServiceChain,
+						},
+					}, nil)
+				mockIPTables.DeleteRule(iptables.ProtocolIPv4, iptables.NATTable, iptables.PreRoutingChain, []string{"-j", antreaPreRoutingChain, "-m", "comment", "--comment", "Antrea: jump to Antrea prerouting rules"})
+				mockIPTables.ListRules(iptables.ProtocolDual, iptables.NATTable, iptables.OutputChain).Return(
+					map[iptables.Protocol][]string{
+						iptables.ProtocolIPv4: {
+							"-A " + iptables.OutputChain + " -j " + antreaOutputChain,
+							"-A " + iptables.OutputChain + " -j " + kubeProxyServiceChain,
+						},
+						iptables.ProtocolIPv6: {
+							"-A " + iptables.OutputChain + " -j " + kubeProxyServiceChain,
+							"-A " + iptables.OutputChain + " -j " + antreaOutputChain,
+						},
+					}, nil)
+				mockIPTables.DeleteRule(iptables.ProtocolIPv6, iptables.NATTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"})
 				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.NATTable, antreaPreRoutingChain)
-				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.NATTable, iptables.PreRoutingChain, []string{"-j", antreaPreRoutingChain, "-m", "comment", "--comment", "Antrea: jump to Antrea prerouting rules"})
+				mockIPTables.InsertRule(iptables.ProtocolDual, iptables.NATTable, iptables.PreRoutingChain, []string{"-j", antreaPreRoutingChain, "-m", "comment", "--comment", "Antrea: jump to Antrea prerouting rules"})
 				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.NATTable, antreaOutputChain)
-				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.NATTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"})
+				mockIPTables.InsertRule(iptables.ProtocolDual, iptables.NATTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"})
 				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.FilterTable, antreaInputChain)
 				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.FilterTable, iptables.InputChain, []string{"-j", antreaInputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea input rules"})
 				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.FilterTable, antreaOutputChain)
@@ -361,6 +403,9 @@ func TestSyncIPTables(t *testing.T) {
 -A ANTREA-PREROUTING -m comment --comment "Antrea: do not track incoming encapsulation packets" -m udp -p udp --dport 6081 -m addrtype --dst-type LOCAL -j NOTRACK
 -A ANTREA-OUTPUT -m comment --comment "Antrea: do not track outgoing encapsulation packets" -m udp -p udp --dport 6081 -m addrtype --src-type LOCAL -j NOTRACK
 -A ANTREA-PREROUTING -m comment --comment "Antrea: drop Pod multicast traffic forwarded via underlay network" -m set --match-set CLUSTER-NODE-IP src -d 224.0.0.0/4 -j DROP
+-A ANTREA-PREROUTING -m comment --comment "Antrea: do not track request packets destined to external IPs" -m set --match-set ANTREA-EXTERNAL-IP dst -j NOTRACK
+-A ANTREA-PREROUTING -m comment --comment "Antrea: do not track reply packets sourced from external IPs" -m set --match-set ANTREA-EXTERNAL-IP src -j NOTRACK
+-A ANTREA-OUTPUT -m comment --comment "Antrea: do not track request packets destined to external IPs" -m set --match-set ANTREA-EXTERNAL-IP dst -j NOTRACK
 COMMIT
 *mangle
 :ANTREA-MANGLE - [0:0]
@@ -404,6 +449,9 @@ COMMIT
 :ANTREA-OUTPUT - [0:0]
 -A ANTREA-PREROUTING -m comment --comment "Antrea: do not track incoming encapsulation packets" -m udp -p udp --dport 6081 -m addrtype --dst-type LOCAL -j NOTRACK
 -A ANTREA-OUTPUT -m comment --comment "Antrea: do not track outgoing encapsulation packets" -m udp -p udp --dport 6081 -m addrtype --src-type LOCAL -j NOTRACK
+-A ANTREA-PREROUTING -m comment --comment "Antrea: do not track request packets destined to external IPs" -m set --match-set ANTREA-EXTERNAL-IP6 dst -j NOTRACK
+-A ANTREA-PREROUTING -m comment --comment "Antrea: do not track reply packets sourced from external IPs" -m set --match-set ANTREA-EXTERNAL-IP6 src -j NOTRACK
+-A ANTREA-OUTPUT -m comment --comment "Antrea: do not track request packets destined to external IPs" -m set --match-set ANTREA-EXTERNAL-IP6 dst -j NOTRACK
 COMMIT
 *mangle
 :ANTREA-MANGLE - [0:0]
@@ -540,18 +588,18 @@ COMMIT
 				},
 			},
 			expectedCalls: func(mockIPTables *iptablestest.MockInterfaceMockRecorder) {
-				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.RawTable, antreaPreRoutingChain)
-				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.RawTable, iptables.PreRoutingChain, []string{"-j", antreaPreRoutingChain, "-m", "comment", "--comment", "Antrea: jump to Antrea prerouting rules"})
-				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.RawTable, antreaOutputChain)
-				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.RawTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"})
-				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.FilterTable, antreaForwardChain)
-				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.FilterTable, iptables.ForwardChain, []string{"-j", antreaForwardChain, "-m", "comment", "--comment", "Antrea: jump to Antrea forwarding rules"})
-				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.NATTable, antreaPostRoutingChain)
-				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.NATTable, iptables.PostRoutingChain, []string{"-j", antreaPostRoutingChain, "-m", "comment", "--comment", "Antrea: jump to Antrea postrouting rules"})
-				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.MangleTable, antreaMangleChain)
-				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.MangleTable, iptables.PreRoutingChain, []string{"-j", antreaMangleChain, "-m", "comment", "--comment", "Antrea: jump to Antrea mangle rules"})
-				mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.MangleTable, antreaOutputChain)
-				mockIPTables.AppendRule(iptables.ProtocolDual, iptables.MangleTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"})
+				mockIPTables.EnsureChain(iptables.ProtocolIPv4, iptables.RawTable, antreaPreRoutingChain)
+				mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.RawTable, iptables.PreRoutingChain, []string{"-j", antreaPreRoutingChain, "-m", "comment", "--comment", "Antrea: jump to Antrea prerouting rules"})
+				mockIPTables.EnsureChain(iptables.ProtocolIPv4, iptables.RawTable, antreaOutputChain)
+				mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.RawTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"})
+				mockIPTables.EnsureChain(iptables.ProtocolIPv4, iptables.FilterTable, antreaForwardChain)
+				mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.FilterTable, iptables.ForwardChain, []string{"-j", antreaForwardChain, "-m", "comment", "--comment", "Antrea: jump to Antrea forwarding rules"})
+				mockIPTables.EnsureChain(iptables.ProtocolIPv4, iptables.NATTable, antreaPostRoutingChain)
+				mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.NATTable, iptables.PostRoutingChain, []string{"-j", antreaPostRoutingChain, "-m", "comment", "--comment", "Antrea: jump to Antrea postrouting rules"})
+				mockIPTables.EnsureChain(iptables.ProtocolIPv4, iptables.MangleTable, antreaMangleChain)
+				mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.MangleTable, iptables.PreRoutingChain, []string{"-j", antreaMangleChain, "-m", "comment", "--comment", "Antrea: jump to Antrea mangle rules"})
+				mockIPTables.EnsureChain(iptables.ProtocolIPv4, iptables.MangleTable, antreaOutputChain)
+				mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.MangleTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"})
 				mockIPTables.Restore(`*raw
 :ANTREA-PREROUTING - [0:0]
 :ANTREA-OUTPUT - [0:0]
@@ -1315,7 +1363,7 @@ func TestDeleteSNATRule(t *testing.T) {
 	}
 }
 
-func TestAddNodePort(t *testing.T) {
+func TestAddNodePortConfigs(t *testing.T) {
 	tests := []struct {
 		name              string
 		nodePortAddresses []net.IP
@@ -1354,14 +1402,18 @@ func TestAddNodePort(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			ipset := ipsettest.NewMockInterface(ctrl)
-			c := &Client{ipset: ipset}
+			c := &Client{ipset: ipset,
+				serviceIPSets: map[string]*sync.Map{
+					antreaNodePortIPSet:  {},
+					antreaNodePortIP6Set: {},
+				}}
 			tt.expectedCalls(ipset.EXPECT())
-			assert.NoError(t, c.AddNodePort(tt.nodePortAddresses, tt.port, tt.protocol))
+			assert.NoError(t, c.AddNodePortConfigs(tt.nodePortAddresses, tt.port, tt.protocol))
 		})
 	}
 }
 
-func TestDeleteNodePort(t *testing.T) {
+func TestDeleteNodePortConfigs(t *testing.T) {
 	tests := []struct {
 		name              string
 		nodePortAddresses []net.IP
@@ -1400,9 +1452,9 @@ func TestDeleteNodePort(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			ipset := ipsettest.NewMockInterface(ctrl)
-			c := &Client{ipset: ipset}
+			c := &Client{ipset: ipset, serviceIPSets: serviceIPSets}
 			tt.expectedCalls(ipset.EXPECT())
-			assert.NoError(t, c.DeleteNodePort(tt.nodePortAddresses, tt.port, tt.protocol))
+			assert.NoError(t, c.DeleteNodePortConfigs(tt.nodePortAddresses, tt.port, tt.protocol))
 		})
 	}
 }
@@ -1561,26 +1613,47 @@ func TestAddServiceCIDRRoute(t *testing.T) {
 	}
 }
 
-func TestAddExternalIPRoute(t *testing.T) {
+func TestAddExternalIPConfigs(t *testing.T) {
 	tests := []struct {
-		name          string
-		externalIPs   []string
-		expectedCalls func(mockNetlink *netlinktest.MockInterfaceMockRecorder)
+		name                                string
+		svcToExternalIPs                    map[string][]string
+		expectedCalls                       func(mockNetlink *netlinktest.MockInterfaceMockRecorder, mockIPSet *ipsettest.MockInterfaceMockRecorder)
+		expectedServiceExternalIPReferences map[string]sets.Set[string]
 	}{
 		{
-			name:        "IPv4",
-			externalIPs: []string{externalIPv4Addr1, externalIPv4Addr2},
-			expectedCalls: func(mockNetlink *netlinktest.MockInterfaceMockRecorder) {
+			name: "IPv4",
+			svcToExternalIPs: map[string][]string{
+				"svc1": {externalIPv4Addr1},
+				"svc2": {externalIPv4Addr2},
+				"svc3": {externalIPv4Addr1, externalIPv4Addr2},
+			},
+			expectedCalls: func(mockNetlink *netlinktest.MockInterfaceMockRecorder, mockIPSet *ipsettest.MockInterfaceMockRecorder) {
 				mockNetlink.RouteReplace(ipv4Route1)
 				mockNetlink.RouteReplace(ipv4Route2)
+				mockIPSet.AddEntry(antreaExternalIPIPSet, externalIPv4Addr1)
+				mockIPSet.AddEntry(antreaExternalIPIPSet, externalIPv4Addr2)
+			},
+			expectedServiceExternalIPReferences: map[string]sets.Set[string]{
+				externalIPv4Addr1: sets.New[string]("svc1", "svc3"),
+				externalIPv4Addr2: sets.New[string]("svc2", "svc3"),
 			},
 		},
 		{
-			name:        "IPv6",
-			externalIPs: []string{externalIPv6Addr1, externalIPv6Addr2},
-			expectedCalls: func(mockNetlink *netlinktest.MockInterfaceMockRecorder) {
+			name: "IPv6",
+			svcToExternalIPs: map[string][]string{
+				"svc1": {externalIPv6Addr1},
+				"svc2": {externalIPv6Addr2},
+				"svc3": {externalIPv6Addr1, externalIPv6Addr2},
+			},
+			expectedCalls: func(mockNetlink *netlinktest.MockInterfaceMockRecorder, mockIPSet *ipsettest.MockInterfaceMockRecorder) {
 				mockNetlink.RouteReplace(ipv6Route1)
 				mockNetlink.RouteReplace(ipv6Route2)
+				mockIPSet.AddEntry(antreaExternalIPIP6Set, externalIPv6Addr1)
+				mockIPSet.AddEntry(antreaExternalIPIP6Set, externalIPv6Addr2)
+			},
+			expectedServiceExternalIPReferences: map[string]sets.Set[string]{
+				externalIPv6Addr1: sets.New[string]("svc1", "svc3"),
+				externalIPv6Addr2: sets.New[string]("svc2", "svc3"),
 			},
 		},
 	}
@@ -1588,48 +1661,82 @@ func TestAddExternalIPRoute(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			mockNetlink := netlinktest.NewMockInterface(ctrl)
+			mockIPSet := ipsettest.NewMockInterface(ctrl)
 			c := &Client{
-				netlink:    mockNetlink,
-				nodeConfig: nodeConfig,
+				ipset:                       mockIPSet,
+				netlink:                     mockNetlink,
+				nodeConfig:                  nodeConfig,
+				serviceExternalIPReferences: make(map[string]sets.Set[string]),
+				serviceIPSets: map[string]*sync.Map{
+					antreaExternalIPIPSet:  {},
+					antreaExternalIPIP6Set: {},
+				},
 			}
-			tt.expectedCalls(mockNetlink.EXPECT())
+			tt.expectedCalls(mockNetlink.EXPECT(), mockIPSet.EXPECT())
 
-			for _, externalIP := range tt.externalIPs {
-				assert.NoError(t, c.AddExternalIPRoute(net.ParseIP(externalIP)))
+			for svcInfo, externalIPs := range tt.svcToExternalIPs {
+				for _, externalIP := range externalIPs {
+					assert.NoError(t, c.AddExternalIPConfigs(svcInfo, net.ParseIP(externalIP)))
+				}
 			}
+			assert.Equal(t, tt.expectedServiceExternalIPReferences, c.serviceExternalIPReferences)
 		})
 	}
 }
 
 func TestDeleteExternalIPRoute(t *testing.T) {
 	tests := []struct {
-		name          string
-		serviceRoutes map[string]*netlink.Route
-		externalIPs   []string
-		expectedCalls func(mockNetlink *netlinktest.MockInterfaceMockRecorder)
+		name                        string
+		svcToExternalIPs            map[string][]string
+		serviceRoutes               map[string]*netlink.Route
+		serviceExternalIPReferences map[string]sets.Set[string]
+		externalIPs                 []string
+		expectedCalls               func(mockNetlink *netlinktest.MockInterfaceMockRecorder, mockIPSet *ipsettest.MockInterfaceMockRecorder)
 	}{
 		{
 			name: "IPv4",
+			svcToExternalIPs: map[string][]string{
+				"svc1": {externalIPv4Addr1},
+				"svc2": {externalIPv4Addr2},
+				"svc3": {externalIPv4Addr1, externalIPv4Addr2},
+			},
 			serviceRoutes: map[string]*netlink.Route{
 				externalIPv4Addr1: ipv4Route1,
 				externalIPv4Addr2: ipv4Route2,
 			},
+			serviceExternalIPReferences: map[string]sets.Set[string]{
+				externalIPv4Addr1: sets.New[string]("svc1", "svc3"),
+				externalIPv4Addr2: sets.New[string]("svc2", "svc3"),
+			},
 			externalIPs: []string{externalIPv4Addr1, externalIPv4Addr2},
-			expectedCalls: func(mockNetlink *netlinktest.MockInterfaceMockRecorder) {
+			expectedCalls: func(mockNetlink *netlinktest.MockInterfaceMockRecorder, mockIPSet *ipsettest.MockInterfaceMockRecorder) {
 				mockNetlink.RouteDel(ipv4Route1)
 				mockNetlink.RouteDel(ipv4Route2)
+				mockIPSet.DelEntry(antreaExternalIPIPSet, externalIPv4Addr1)
+				mockIPSet.DelEntry(antreaExternalIPIPSet, externalIPv4Addr2)
 			},
 		},
 		{
 			name: "IPv6",
+			svcToExternalIPs: map[string][]string{
+				"svc1": {externalIPv6Addr1},
+				"svc2": {externalIPv6Addr2},
+				"svc3": {externalIPv6Addr1, externalIPv6Addr2},
+			},
 			serviceRoutes: map[string]*netlink.Route{
 				externalIPv6Addr1: ipv6Route1,
 				externalIPv6Addr2: ipv6Route2,
 			},
+			serviceExternalIPReferences: map[string]sets.Set[string]{
+				externalIPv6Addr1: sets.New[string]("svc1", "svc3"),
+				externalIPv6Addr2: sets.New[string]("svc2", "svc3"),
+			},
 			externalIPs: []string{externalIPv6Addr1, externalIPv6Addr2},
-			expectedCalls: func(mockNetlink *netlinktest.MockInterfaceMockRecorder) {
+			expectedCalls: func(mockNetlink *netlinktest.MockInterfaceMockRecorder, mockIPSet *ipsettest.MockInterfaceMockRecorder) {
 				mockNetlink.RouteDel(ipv6Route1)
 				mockNetlink.RouteDel(ipv6Route2)
+				mockIPSet.DelEntry(antreaExternalIPIP6Set, externalIPv6Addr1)
+				mockIPSet.DelEntry(antreaExternalIPIP6Set, externalIPv6Addr2)
 			},
 		},
 	}
@@ -1637,19 +1744,33 @@ func TestDeleteExternalIPRoute(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			mockNetlink := netlinktest.NewMockInterface(ctrl)
+			mockIPSet := ipsettest.NewMockInterface(ctrl)
 			c := &Client{
-				netlink:       mockNetlink,
-				nodeConfig:    nodeConfig,
-				serviceRoutes: sync.Map{},
+				ipset:                       mockIPSet,
+				netlink:                     mockNetlink,
+				nodeConfig:                  nodeConfig,
+				serviceExternalIPReferences: tt.serviceExternalIPReferences,
+				serviceIPSets: map[string]*sync.Map{
+					antreaExternalIPIPSet:  {},
+					antreaExternalIPIP6Set: {},
+				},
 			}
 			for ipStr, route := range tt.serviceRoutes {
 				c.serviceRoutes.Store(ipStr, route)
+				if utilnet.IsIPv6String(ipStr) {
+					c.serviceIPSets[antreaExternalIPIP6Set].Store(ipStr, struct{}{})
+				} else {
+					c.serviceIPSets[antreaExternalIPIPSet].Store(ipStr, struct{}{})
+				}
 			}
-			tt.expectedCalls(mockNetlink.EXPECT())
 
-			for _, externalIP := range tt.externalIPs {
-				assert.NoError(t, c.DeleteExternalIPRoute(net.ParseIP(externalIP)))
+			tt.expectedCalls(mockNetlink.EXPECT(), mockIPSet.EXPECT())
+			for svcInfo, externalIPs := range tt.svcToExternalIPs {
+				for _, externalIP := range externalIPs {
+					assert.NoError(t, c.DeleteExternalIPConfigs(svcInfo, net.ParseIP(externalIP)))
+				}
 			}
+			assert.Equal(t, make(map[string]sets.Set[string]), c.serviceExternalIPReferences)
 		})
 	}
 }
diff --git a/pkg/agent/route/route_windows_test.go b/pkg/agent/route/route_windows_test.go
index 0f1648c10..d9542ab00 100644
--- a/pkg/agent/route/route_windows_test.go
+++ b/pkg/agent/route/route_windows_test.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"go.uber.org/mock/gomock"
+	"k8s.io/apimachinery/pkg/util/sets"
 
 	"antrea.io/antrea/pkg/agent/config"
 	servicecidrtesting "antrea.io/antrea/pkg/agent/servicecidr/testing"
@@ -260,17 +261,17 @@ func TestDeleteRoutes(t *testing.T) {
 	assert.NoError(t, c.DeleteRoutes(podCIDR))
 }
 
-func TestAddNodePort(t *testing.T) {
+func TestAddNodePortConf(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	mockWinnet := winnettesting.NewMockInterface(ctrl)
 	c := &Client{
 		winnet: mockWinnet,
 	}
 	mockWinnet.EXPECT().ReplaceNetNatStaticMapping(nodePortNetNatStaticMapping)
-	assert.NoError(t, c.AddNodePort(nil, nodePort, protocol))
+	assert.NoError(t, c.AddNodePortConfigs(nil, nodePort, protocol))
 }
 
-func TestDeleteNodePort(t *testing.T) {
+func TestDeleteNodePortConf(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	mockWinnet := winnettesting.NewMockInterface(ctrl)
 	c := &Client{
@@ -278,7 +279,7 @@ func TestDeleteNodePort(t *testing.T) {
 	}
 	c.netNatStaticMappings.Store(fmt.Sprintf("%d-%s", nodePort, protocol), nodePortNetNatStaticMapping)
 	mockWinnet.EXPECT().RemoveNetNatStaticMapping(nodePortNetNatStaticMapping)
-	assert.NoError(t, c.DeleteNodePort(nil, nodePort, protocol))
+	assert.NoError(t, c.DeleteNodePortConfigs(nil, nodePort, protocol))
 }
 
 func TestAddServiceCIDRRoute(t *testing.T) {
@@ -399,13 +400,22 @@ func TestAddServiceCIDRRoute(t *testing.T) {
 	}
 }
 
-func TestAddExternalIPRoute(t *testing.T) {
-	externalIPs := []string{externalIPv4Addr1, externalIPv4Addr2}
+func TestAddExternalIPConfigs(t *testing.T) {
+	svcToExternalIPs := map[string][]string{
+		"svc1": {externalIPv4Addr1},
+		"svc2": {externalIPv4Addr2},
+		"svc3": {externalIPv4Addr1, externalIPv4Addr2},
+	}
+	expectedServiceExternalIPReferences := map[string]sets.Set[string]{
+		externalIPv4Addr1: sets.New[string]("svc1", "svc3"),
+		externalIPv4Addr2: sets.New[string]("svc2", "svc3"),
+	}
 
 	ctrl := gomock.NewController(t)
 	mockWinnet := winnettesting.NewMockInterface(ctrl)
 	c := &Client{
-		winnet: mockWinnet,
+		winnet:                      mockWinnet,
+		serviceExternalIPReferences: make(map[string]sets.Set[string]),
 		nodeConfig: &config.NodeConfig{
 			GatewayConfig: &config.GatewayConfig{
 				LinkIndex: 10,
@@ -415,18 +425,29 @@ func TestAddExternalIPRoute(t *testing.T) {
 	mockWinnet.EXPECT().ReplaceNetRoute(ipv4Route1)
 	mockWinnet.EXPECT().ReplaceNetRoute(ipv4Route2)
 
-	for _, externalIP := range externalIPs {
-		assert.NoError(t, c.AddExternalIPRoute(net.ParseIP(externalIP)))
+	for svcInfo, externalIPs := range svcToExternalIPs {
+		for _, externalIP := range externalIPs {
+			assert.NoError(t, c.AddExternalIPConfigs(svcInfo, net.ParseIP(externalIP)))
+		}
 	}
+	assert.Equal(t, expectedServiceExternalIPReferences, c.serviceExternalIPReferences)
 }
 
-func TestDeleteExternalIPRoute(t *testing.T) {
-	externalIPs := []string{externalIPv4Addr1, externalIPv4Addr2}
+func TestDeleteExternalIPConfigs(t *testing.T) {
+	svcToExternalIPs := map[string][]string{
+		"svc1": {externalIPv4Addr1},
+		"svc2": {externalIPv4Addr2},
+		"svc3": {externalIPv4Addr1, externalIPv4Addr2},
+	}
 
 	ctrl := gomock.NewController(t)
 	mockWinnet := winnettesting.NewMockInterface(ctrl)
 	c := &Client{
 		winnet: mockWinnet,
+		serviceExternalIPReferences: map[string]sets.Set[string]{
+			externalIPv4Addr1: sets.New[string]("svc1", "svc3"),
+			externalIPv4Addr2: sets.New[string]("svc2", "svc3"),
+		},
 	}
 	for ipStr, route := range map[string]*winnet.Route{externalIPv4Addr1: ipv4Route1, externalIPv4Addr2: ipv4Route2} {
 		c.serviceRoutes.Store(ipStr, route)
@@ -434,8 +455,10 @@ func TestDeleteExternalIPRoute(t *testing.T) {
 
 	mockWinnet.EXPECT().RemoveNetRoute(ipv4Route1)
 	mockWinnet.EXPECT().RemoveNetRoute(ipv4Route2)
-
-	for _, externalIP := range externalIPs {
-		assert.NoError(t, c.DeleteExternalIPRoute(net.ParseIP(externalIP)))
+	for svcInfo, externalIPs := range svcToExternalIPs {
+		for _, externalIP := range externalIPs {
+			assert.NoError(t, c.DeleteExternalIPConfigs(svcInfo, net.ParseIP(externalIP)))
+		}
 	}
+	assert.Equal(t, make(map[string]sets.Set[string]), c.serviceExternalIPReferences)
 }
diff --git a/pkg/agent/route/testing/mock_route.go b/pkg/agent/route/testing/mock_route.go
index 220eec914..80e34f315 100644
--- a/pkg/agent/route/testing/mock_route.go
+++ b/pkg/agent/route/testing/mock_route.go
@@ -85,18 +85,18 @@ func (mr *MockInterfaceMockRecorder) AddEgressRule(arg0, arg1 any) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEgressRule", reflect.TypeOf((*MockInterface)(nil).AddEgressRule), arg0, arg1)
 }
 
-// AddExternalIPRoute mocks base method.
-func (m *MockInterface) AddExternalIPRoute(arg0 net.IP) error {
+// AddExternalIPConfigs mocks base method.
+func (m *MockInterface) AddExternalIPConfigs(arg0 string, arg1 net.IP) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "AddExternalIPRoute", arg0)
+	ret := m.ctrl.Call(m, "AddExternalIPConfigs", arg0, arg1)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// AddExternalIPRoute indicates an expected call of AddExternalIPRoute.
-func (mr *MockInterfaceMockRecorder) AddExternalIPRoute(arg0 any) *gomock.Call {
+// AddExternalIPConfigs indicates an expected call of AddExternalIPConfigs.
+func (mr *MockInterfaceMockRecorder) AddExternalIPConfigs(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddExternalIPRoute", reflect.TypeOf((*MockInterface)(nil).AddExternalIPRoute), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddExternalIPConfigs", reflect.TypeOf((*MockInterface)(nil).AddExternalIPConfigs), arg0, arg1)
 }
 
 // AddLocalAntreaFlexibleIPAMPodRule mocks base method.
@@ -113,18 +113,18 @@ func (mr *MockInterfaceMockRecorder) AddLocalAntreaFlexibleIPAMPodRule(arg0 any)
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLocalAntreaFlexibleIPAMPodRule", reflect.TypeOf((*MockInterface)(nil).AddLocalAntreaFlexibleIPAMPodRule), arg0)
 }
 
-// AddNodePort mocks base method.
-func (m *MockInterface) AddNodePort(arg0 []net.IP, arg1 uint16, arg2 openflow.Protocol) error {
+// AddNodePortConfigs mocks base method.
+func (m *MockInterface) AddNodePortConfigs(arg0 []net.IP, arg1 uint16, arg2 openflow.Protocol) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "AddNodePort", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "AddNodePortConfigs", arg0, arg1, arg2)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// AddNodePort indicates an expected call of AddNodePort.
-func (mr *MockInterfaceMockRecorder) AddNodePort(arg0, arg1, arg2 any) *gomock.Call {
+// AddNodePortConfigs indicates an expected call of AddNodePortConfigs.
+func (mr *MockInterfaceMockRecorder) AddNodePortConfigs(arg0, arg1, arg2 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddNodePort", reflect.TypeOf((*MockInterface)(nil).AddNodePort), arg0, arg1, arg2)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddNodePortConfigs", reflect.TypeOf((*MockInterface)(nil).AddNodePortConfigs), arg0, arg1, arg2)
 }
 
 // AddOrUpdateNodeNetworkPolicyIPSet mocks base method.
@@ -239,18 +239,18 @@ func (mr *MockInterfaceMockRecorder) DeleteEgressRule(arg0, arg1 any) *gomock.Ca
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEgressRule", reflect.TypeOf((*MockInterface)(nil).DeleteEgressRule), arg0, arg1)
 }
 
-// DeleteExternalIPRoute mocks base method.
-func (m *MockInterface) DeleteExternalIPRoute(arg0 net.IP) error {
+// DeleteExternalIPConfigs mocks base method.
+func (m *MockInterface) DeleteExternalIPConfigs(arg0 string, arg1 net.IP) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "DeleteExternalIPRoute", arg0)
+	ret := m.ctrl.Call(m, "DeleteExternalIPConfigs", arg0, arg1)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// DeleteExternalIPRoute indicates an expected call of DeleteExternalIPRoute.
-func (mr *MockInterfaceMockRecorder) DeleteExternalIPRoute(arg0 any) *gomock.Call {
+// DeleteExternalIPConfigs indicates an expected call of DeleteExternalIPConfigs.
+func (mr *MockInterfaceMockRecorder) DeleteExternalIPConfigs(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExternalIPRoute", reflect.TypeOf((*MockInterface)(nil).DeleteExternalIPRoute), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExternalIPConfigs", reflect.TypeOf((*MockInterface)(nil).DeleteExternalIPConfigs), arg0, arg1)
 }
 
 // DeleteLocalAntreaFlexibleIPAMPodRule mocks base method.
@@ -295,18 +295,18 @@ func (mr *MockInterfaceMockRecorder) DeleteNodeNetworkPolicyIPTables(arg0, arg1
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodeNetworkPolicyIPTables", reflect.TypeOf((*MockInterface)(nil).DeleteNodeNetworkPolicyIPTables), arg0, arg1)
 }
 
-// DeleteNodePort mocks base method.
-func (m *MockInterface) DeleteNodePort(arg0 []net.IP, arg1 uint16, arg2 openflow.Protocol) error {
+// DeleteNodePortConfigs mocks base method.
+func (m *MockInterface) DeleteNodePortConfigs(arg0 []net.IP, arg1 uint16, arg2 openflow.Protocol) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "DeleteNodePort", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "DeleteNodePortConfigs", arg0, arg1, arg2)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// DeleteNodePort indicates an expected call of DeleteNodePort.
-func (mr *MockInterfaceMockRecorder) DeleteNodePort(arg0, arg1, arg2 any) *gomock.Call {
+// DeleteNodePortConfigs indicates an expected call of DeleteNodePortConfigs.
+func (mr *MockInterfaceMockRecorder) DeleteNodePortConfigs(arg0, arg1, arg2 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodePort", reflect.TypeOf((*MockInterface)(nil).DeleteNodePort), arg0, arg1, arg2)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodePortConfigs", reflect.TypeOf((*MockInterface)(nil).DeleteNodePortConfigs), arg0, arg1, arg2)
 }
 
 // DeleteRouteForLink mocks base method.
diff --git a/pkg/agent/secondarynetwork/podwatch/controller_test.go b/pkg/agent/secondarynetwork/podwatch/controller_test.go
index f1c9e8157..ccce64c4c 100644
--- a/pkg/agent/secondarynetwork/podwatch/controller_test.go
+++ b/pkg/agent/secondarynetwork/podwatch/controller_test.go
@@ -587,7 +587,7 @@ func TestPodControllerAddPod(t *testing.T) {
 	podKey := podKeyGet(podName, testNamespace)
 
 	// Create Pod and wait for Informer cache updated.
-	createPodFn := func(pc *podController, pod *corev1.Pod) {
+	createPodFn := func(pc *PodController, pod *corev1.Pod) {
 		_, err := pc.kubeClient.CoreV1().Pods(testNamespace).Create(context.Background(),
 			pod, metav1.CreateOptions{})
 		require.NoError(t, err, "error when creating test Pod")
@@ -596,7 +596,7 @@ func TestPodControllerAddPod(t *testing.T) {
 			return ok == true && err == nil
 		}, 1*time.Second, 10*time.Millisecond)
 	}
-	deletePodFn := func(pc *podController, podName string) {
+	deletePodFn := func(pc *PodController, podName string) {
 		require.NoError(t, pc.kubeClient.CoreV1().Pods(testNamespace).Delete(context.Background(),
 			podName, metav1.DeleteOptions{}), "error when deleting test Pod")
 		assert.Eventually(t, func() bool {
@@ -899,7 +899,7 @@ func TestPodControllerAddPod(t *testing.T) {
 }
 
 func testPodController(ctrl *gomock.Controller) (
-	*podController, *podwatchtesting.MockIPAMAllocator,
+	*PodController, *podwatchtesting.MockIPAMAllocator,
 	*podwatchtesting.MockInterfaceConfigurator) {
 	client := fake.NewSimpleClientset()
 	netdefclient := netdefclientfake.NewSimpleClientset().K8sCniCncfIoV1()
@@ -907,8 +907,8 @@ func testPodController(ctrl *gomock.Controller) (
 	interfaceConfigurator := podwatchtesting.NewMockInterfaceConfigurator(ctrl)
 	mockIPAM := podwatchtesting.NewMockIPAMAllocator(ctrl)
 
-	// podController without event handlers.
-	return &podController{
+	// PodController without event handlers.
+	return &PodController{
 		kubeClient:         client,
 		netAttachDefClient: netdefclient,
 		queue: workqueue.NewNamedRateLimitingQueue(
@@ -921,9 +921,9 @@ func testPodController(ctrl *gomock.Controller) (
 	}, mockIPAM, interfaceConfigurator
 }
 
-// Create a test podController and start informerFactory.
+// Create a test PodController and start informerFactory.
 func testPodControllerStart(ctrl *gomock.Controller) (
-	*podController, *podwatchtesting.MockIPAMAllocator,
+	*PodController, *podwatchtesting.MockIPAMAllocator,
 	*podwatchtesting.MockInterfaceConfigurator) {
 	podController, mockIPAM, interfaceConfigurator := testPodController(ctrl)
 	informerFactory := informers.NewSharedInformerFactory(podController.kubeClient, resyncPeriod)
diff --git a/pkg/agent/util/iptables/testing/mock_iptables_linux.go b/pkg/agent/util/iptables/testing/mock_iptables_linux.go
index c26e7e3ca..3b2aaeb3e 100644
--- a/pkg/agent/util/iptables/testing/mock_iptables_linux.go
+++ b/pkg/agent/util/iptables/testing/mock_iptables_linux.go
@@ -140,18 +140,18 @@ func (mr *MockInterfaceMockRecorder) InsertRule(arg0, arg1, arg2, arg3 any) *gom
 }
 
 // ListRules mocks base method.
-func (m *MockInterface) ListRules(arg0, arg1 string) ([]string, error) {
+func (m *MockInterface) ListRules(arg0 iptables.Protocol, arg1, arg2 string) (map[iptables.Protocol][]string, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ListRules", arg0, arg1)
-	ret0, _ := ret[0].([]string)
+	ret := m.ctrl.Call(m, "ListRules", arg0, arg1, arg2)
+	ret0, _ := ret[0].(map[iptables.Protocol][]string)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // ListRules indicates an expected call of ListRules.
-func (mr *MockInterfaceMockRecorder) ListRules(arg0, arg1 any) *gomock.Call {
+func (mr *MockInterfaceMockRecorder) ListRules(arg0, arg1, arg2 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRules", reflect.TypeOf((*MockInterface)(nil).ListRules), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRules", reflect.TypeOf((*MockInterface)(nil).ListRules), arg0, arg1, arg2)
 }
 
 // Restore mocks base method.
diff --git a/test/e2e/fixtures.go b/test/e2e/fixtures.go
index 55297af4d..b8aa86746 100644
--- a/test/e2e/fixtures.go
+++ b/test/e2e/fixtures.go
@@ -685,7 +685,9 @@ func testMain(m *testing.M) int {
 	flag.StringVar(&testOptions.skipCases, "skip-cases", "", "Key words to skip cases")
 	flag.StringVar(&testOptions.linuxVMs, "linuxVMs", "", "hostname of Linux VMs")
 	flag.StringVar(&testOptions.windowsVMs, "windowsVMs", "", "hostname of Windows VMs")
-	flag.StringVar(&testOptions.externalServerIPs, "external-server-ips", "", "IP addresses of external server, at most one IP per IP family")
+	flag.StringVar(&testOptions.externalAgnhostIPs, "external-agnhost-ips", "", "IP addresses of external agnhost, at most one IP per IP family")
+	flag.StringVar(&testOptions.externalFRRIPs, "external-frr-ips", "", "IP addresses of external FRR, at most one IP per IP family")
+	flag.StringVar(&testOptions.externalFRRCID, "external-frr-cid", "", "Container ID of external FRR")
 	flag.StringVar(&testOptions.vlanSubnets, "vlan-subnets", "", "IP subnets of the VLAN network the Nodes reside in, at most one subnet per IP family")
 	flag.IntVar(&testOptions.vlanID, "vlan-id", 0, "ID of the VLAN network the Nodes reside in")
 	flag.Parse()
diff --git a/test/e2e/framework.go b/test/e2e/framework.go
index cedefbe83..39d43adcb 100644
--- a/test/e2e/framework.go
+++ b/test/e2e/framework.go
@@ -120,7 +120,7 @@ const (
 	antreaControllerConfName = "antrea-controller.conf"
 	flowAggregatorConfName   = "flow-aggregator.conf"
 
-	agnhostImage        = "registry.k8s.io/e2e-test-images/agnhost:2.29"
+	agnhostImage        = "registry.k8s.io/e2e-test-images/agnhost:2.40"
 	ToolboxImage        = "antrea/toolbox:1.3-0"
 	mcjoinImage         = "antrea/mcjoin:v2.9"
 	nginxImage          = "antrea/nginx:1.21.6-alpine"
@@ -190,6 +190,10 @@ type ExternalInfo struct {
 	vlanSubnetIPv6  string
 	vlanGatewayIPv6 string
 	vlanID          int
+
+	externalFRRIPv4 string
+	externalFRRIPv6 string
+	externalFRRCID  string
 }
 
 var clusterInfo ClusterInfo
@@ -213,9 +217,16 @@ type TestOptions struct {
 	// the home directory of the control-plane Node. Note it doesn't affect the tests that redeploy Antrea themselves.
 	deployAntrea bool
 
-	externalServerIPs string
-	vlanSubnets       string
-	vlanID            int
+	externalAgnhostIPs string
+	vlanSubnets        string
+	vlanID             int
+
+	externalFRRIPs string
+	// FRR cannot currently be configured remotely over networking. As a result, the e2e tests for BGPPolicy can only
+	// be run in a Kind cluster, where the FRR container can be configured using Docker exec with the container ID.
+	// TODO: Introduce a BGP router implementation that can be configured remotely over networking to replace FRR.
+	// This would allow the e2e tests for BGPPolicy to be run in environments other than just a Kind cluster.
+	externalFRRCID string
 }
 
 type flowVisibilityTestOptions struct {
@@ -498,14 +509,14 @@ func (data *TestData) RunCommandOnNodeExt(nodeName, cmd string, envs map[string]
 }
 
 func (data *TestData) collectExternalInfo() error {
-	ips := strings.Split(testOptions.externalServerIPs, ",")
+	ips := strings.Split(testOptions.externalAgnhostIPs, ",")
 	for _, ip := range ips {
 		if ip == "" {
 			continue
 		}
 		parsedIP := net.ParseIP(ip)
 		if parsedIP == nil {
-			return fmt.Errorf("invalid external server IP %s", ip)
+			return fmt.Errorf("invalid external agnhost IP %s", ip)
 		}
 		if parsedIP.To4() != nil {
 			externalInfo.externalServerIPv4 = ip
@@ -532,6 +543,25 @@ func (data *TestData) collectExternalInfo() error {
 		}
 	}
 	externalInfo.vlanID = testOptions.vlanID
+
+	frrIPs := strings.Split(testOptions.externalFRRIPs, ",")
+	for _, ip := range frrIPs {
+		if ip == "" {
+			continue
+		}
+		parsedIP := net.ParseIP(ip)
+		if parsedIP == nil {
+			return fmt.Errorf("invalid external FRR IP %s", ip)
+		}
+		if parsedIP.To4() != nil {
+			externalInfo.externalFRRIPv4 = ip
+		} else {
+			externalInfo.externalFRRIPv6 = ip
+		}
+	}
+
+	externalInfo.externalFRRCID = testOptions.externalFRRCID
+
 	return nil
 }
 
diff --git a/test/e2e/proxy_test.go b/test/e2e/proxy_test.go
index 9f87aa126..f5119a4e2 100644
--- a/test/e2e/proxy_test.go
+++ b/test/e2e/proxy_test.go
@@ -769,7 +769,13 @@ func testProxyIntraNodeHairpinCases(data *TestData, t *testing.T, expectedClient
 	t.Run("IntraNode/ClusterIP", func(t *testing.T) {
 		clientIP, err := probeClientIPFromPod(data, pod, agnhostContainerName, clusterIPUrl)
 		require.NoError(t, err, "ClusterIP hairpin should be able to be connected")
-		require.Equal(t, expectedClientIP, clientIP)
+		if _, err := data.clientset.AppsV1().DaemonSets(kubeNamespace).Get(context.TODO(), "kube-proxy", metav1.GetOptions{}); err == nil {
+			// When proxyAll is enabled and kube-proxy is present, Antrea Proxy doesn't handle ClusterIP traffic sourced
+			// from local Node, and the got clientIP is not the expected clientIP. As a result, skip the check.
+			t.Logf("Skip checking the clientIP because kube-proxy is present")
+		} else {
+			require.Equal(t, expectedClientIP, clientIP)
+		}
 	})
 	t.Run("IntraNode/NodePort/ExternalTrafficPolicy:Cluster", func(t *testing.T) {
 		skipIfProxyAllDisabled(t, data)