Repository: antrea-io/antrea
Pull request #2599 (2021-08-16 03:44:48)

NodePort, LoadBalancer and ClusterIP full support for AntreaProxy on Linux
For NodePort support, an ipset is used to store NodePort IP, port and
transparent protocol in an entry IP:port,protocol. Then an iptables
entry is used to match the ipset as destination and perform DNAT with
a virtual IP. For DNAT'd packets, a routing entry is used to route
them to Antrea gateway.

For LoadBalancer support, a routing entry will be created for every
ingress IP to route the packets from remote or localhost to Antrea
gateway.

For ClusterIP support, a routing entry is always used to route the
packets of all ClusterIPs to Antrea gateway. when a new ClusterIP is
created, the destination IP block of the routing entry might be
extended to include the ClusterIP address.

To support the Service traffic of above cases, the main changes of
OVS pipeline include:
- Change table serviceHairpinTable ID from 29 to 23.
- Change table hairpinSNATTable ID from 106 to 108.
- Add table serviceConntrackTable 24 to transform SNAT'd connections.
- Add table serviceClassifierTable 35 to classify Service traffic.
- Add table serviceConntrackCommitTable 106 to perform SNAT for Service
  traffic.

codecov-commenter: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/2599?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#2599](https://codecov.io/gh/antrea-io/antrea/pull/2599?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (6b7c7d3) into [main](https://codecov.io/gh/antrea-io/antrea/commit/95a167aa67801c63b503a33ea17a491dd55b94c5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (95a167a) will **decrease** coverage by `18.41%`.
> The diff coverage is `5.55%`.

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

```diff
@@             Coverage Diff             @@
##             main    #2599       +/-   ##
===========================================
- Coverage   59.67%   41.26%   -18.42%     
===========================================
  Files         281      152      -129     
  Lines       22207    18936     -3271     
===========================================
- Hits        13253     7814     -5439     
- Misses       7546    10404     +2858     
+ Partials     1408      718      -690     
```

| Flag | Coverage Δ | |
|---|---|---|
| kind-e2e-tests | `?` | |
| unit-tests | `41.26% <5.55%> (-1.00%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/2599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [pkg/agent/agent.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L2FnZW50Lmdv) | `19.32% <ø> (-30.90%)` | :arrow_down: |
| [pkg/agent/config/node\_config.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L2NvbmZpZy9ub2RlX2NvbmZpZy5nbw==) | `0.00% <ø> (-100.00%)` | :arrow_down: |
| [pkg/agent/openflow/client.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L29wZW5mbG93L2NsaWVudC5nbw==) | `23.59% <0.00%> (-35.01%)` | :arrow_down: |
| [pkg/agent/openflow/pipeline.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L29wZW5mbG93L3BpcGVsaW5lLmdv) | `21.43% <0.00%> (-50.25%)` | :arrow_down: |
| [pkg/agent/util/iptables/iptables.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L3V0aWwvaXB0YWJsZXMvaXB0YWJsZXMuZ28=) | `0.00% <ø> (-37.40%)` | :arrow_down: |
| [pkg/agent/util/net.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L3V0aWwvbmV0Lmdv) | `13.86% <0.00%> (-26.14%)` | :arrow_down: |
| [pkg/agent/util/net\_linux.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L3V0aWwvbmV0X2xpbnV4Lmdv) | `0.00% <0.00%> (-33.67%)` | :arrow_down: |
| [pkg/agent/proxy/proxier.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L3Byb3h5L3Byb3hpZXIuZ28=) | `39.89% <21.55%> (-24.01%)` | :arrow_down: |
| [pkg/ovs/openflow/default.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL292cy9vcGVuZmxvdy9kZWZhdWx0Lmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/util/runtime/runtime.go](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL3V0aWwvcnVudGltZS9ydW50aW1lLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [236 more](https://codecov.io/gh/antrea-io/antrea/pull/2599/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |

hongliangl: /test-all
hongliangl: /test-all
hongliangl: /test-all
hongliangl: /test-all
hongliangl: /test-all
hongliangl: /test-all
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: @tnqn @jianjuns @antoninbas Thanks for reviewing the huge PR. Issue of test `testFQDNPolicy` has been resolved. Could you please help review the changed documents?  Thanks a lot again.
jianjuns: @hongliangl : great the test issue is fixed! Could I know what was the problem and how you fixed it?
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
tnqn: Could we change the title to "NodePort, LoadBalancer and ClusterIP full support for AntreaProxy on Linux" as it's not only for traffic from K8s Node, and a shorter title will look neater in git logs.
hongliangl: > Could we change the title to "NodePort, LoadBalancer and ClusterIP full support for AntreaProxy on Linux" as it's not only for traffic from K8s Node, and a shorter title will look neater in git logs.

Updated.
tnqn: @jianjuns will you take another look?
jianjuns: > @jianjuns will you take another look?

No, no extra comment from me.
hongliangl: > LGTM

Thanks for reviewing, @tnqn @jianjuns. I have just updated the commit message.
hongliangl: /test-all
/test-conformance
/test-ipv6-only-e2e
/test-ipv6-ds-e2e
tnqn: /test-ipv6-conformance
/test-ipv6-e2e
/test-ipv6-networkpolicy
/test-ipv6-only-conformance
/test-ipv6-only-networkpolicy
/test-windows-conformance
/test-windows-e2e
/test-windows-networkpolicy

Patch:
diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml
index ce06688c3..4111da0bd 100755
--- a/.github/workflows/kind.yml
+++ b/.github/workflows/kind.yml
@@ -177,6 +177,69 @@ jobs:
         path: log.tar.gz
         retention-days: 30
 
+  test-e2e-encap-proxy-all:
+    name: E2e tests on a Kind cluster on Linux with AntreaProxy all Service support
+    needs: [ build-antrea-coverage-image, build-flow-aggregator-coverage-image ]
+    runs-on: [ ubuntu-latest ]
+    steps:
+      - name: Free disk space
+        # https://github.com/actions/virtual-environments/issues/709
+        run: |
+          sudo apt-get clean
+          df -h
+      - uses: actions/checkout@v2
+      - uses: actions/setup-go@v2
+        with:
+          go-version: 1.17
+      - name: Download Antrea image from previous job
+        uses: actions/download-artifact@v1
+        with:
+          name: antrea-ubuntu-cov
+      - name: Download Flow Aggregator image from previous job
+        uses: actions/download-artifact@v1
+        with:
+          name: flow-aggregator-cov
+      - name: Load Antrea image
+        run: |
+          docker load -i antrea-ubuntu-cov/antrea-ubuntu.tar
+          docker load -i flow-aggregator-cov/flow-aggregator.tar
+      - name: Install Kind
+        run: |
+          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: Run e2e tests
+        run: |
+          mkdir log
+          mkdir test-e2e-encap-proxy-all-coverage
+          ANTREA_LOG_DIR=$PWD/log ANTREA_COV_DIR=$PWD/test-e2e-encap-proxy-all-coverage ./ci/kind/test-e2e-kind.sh --encap-mode encap --proxy-all --coverage --skip mode-irrelevant
+      - name: Tar coverage files
+        run: tar -czf test-e2e-encap-proxy-all-coverage.tar.gz test-e2e-encap-proxy-all-coverage
+      - name: Upload coverage for test-e2e-encap-proxy-all-coverage
+        uses: actions/upload-artifact@v2
+        with:
+          name: test-e2e-encap-proxy-all-coverage
+          path: test-e2e-encap-proxy-all-coverage.tar.gz
+          retention-days: 30
+      - name: Codecov
+        uses: codecov/codecov-action@v1
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          file: '*.cov.out*'
+          flags: kind-e2e-tests
+          name: codecov-test-e2e-encap-proxy-all
+          directory: test-e2e-encap-proxy-all-coverage
+      - name: Tar log files
+        if: ${{ failure() }}
+        run: tar -czf log.tar.gz log
+      - name: Upload test log
+        uses: actions/upload-artifact@v2
+        if: ${{ failure() }}
+        with:
+          name: e2e-kind-encap-proxy-all.tar.gz
+          path: log.tar.gz
+          retention-days: 30
+
   test-e2e-noencap:
     name: E2e tests on a Kind cluster on Linux (noEncap)
     needs: [build-antrea-coverage-image, build-flow-aggregator-coverage-image]
@@ -424,7 +487,7 @@ jobs:
   # yet.
   artifact-cleanup:
     name: Delete uploaded images
-    needs: [build-antrea-coverage-image, build-flow-aggregator-coverage-image, test-e2e-encap, test-e2e-encap-no-proxy, test-e2e-noencap, test-e2e-hybrid, test-e2e-encap-no-np, test-netpol-tmp, validate-prometheus-metrics-doc]
+    needs: [build-antrea-coverage-image, build-flow-aggregator-coverage-image, test-e2e-encap, test-e2e-encap-no-proxy, test-e2e-encap-proxy-all, test-e2e-noencap, test-e2e-hybrid, test-e2e-encap-no-np, test-netpol-tmp, validate-prometheus-metrics-doc]
     if: ${{ always() &&  (needs.build-antrea-coverage-image.result == 'success' || needs.build-flow-aggregator-coverage-image.result == 'success') }}
     runs-on: [ubuntu-latest]
     steps:
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index a35f35670..463d8e8b7 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -3922,6 +3922,19 @@ data:
     # If there are multiple IP addresses configured on the interface, the first one is used.
     # The interface configured with Node IP is used if this parameter is not set.
     #transportInterface:
+
+    # Option antreaProxy contains AntreaProxy related configuration options.
+    antreaProxy:
+      # 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. 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.
+      #proxyAll: false
+      # A string array of values which specifies the host IPv4/IPv6 addresses for NodePort. Values can be valid IP blocks.
+      # (e.g. 1.2.3.0/24, 1.2.3.4/32). An empty string slice is meant to select all host IPv4/IPv6 addresses.
+      # Note that the option is only valid when proxyAll is true.
+      #nodePortAddresses: []
   antrea-cni.conflist: |
     {
         "cniVersion":"0.3.0",
@@ -4028,7 +4041,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-886tmkcf7h
+  name: antrea-config-2cg6khh44b
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4099,7 +4112,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-886tmkcf7h
+          value: antrea-config-2cg6khh44b
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4150,7 +4163,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-886tmkcf7h
+          name: antrea-config-2cg6khh44b
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4431,7 +4444,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-886tmkcf7h
+          name: antrea-config-2cg6khh44b
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index 5e30b0ea1..cd3c34591 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -3922,6 +3922,19 @@ data:
     # If there are multiple IP addresses configured on the interface, the first one is used.
     # The interface configured with Node IP is used if this parameter is not set.
     #transportInterface:
+
+    # Option antreaProxy contains AntreaProxy related configuration options.
+    antreaProxy:
+      # 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. 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.
+      #proxyAll: false
+      # A string array of values which specifies the host IPv4/IPv6 addresses for NodePort. Values can be valid IP blocks.
+      # (e.g. 1.2.3.0/24, 1.2.3.4/32). An empty string slice is meant to select all host IPv4/IPv6 addresses.
+      # Note that the option is only valid when proxyAll is true.
+      #nodePortAddresses: []
   antrea-cni.conflist: |
     {
         "cniVersion":"0.3.0",
@@ -4028,7 +4041,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-886tmkcf7h
+  name: antrea-config-2cg6khh44b
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4099,7 +4112,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-886tmkcf7h
+          value: antrea-config-2cg6khh44b
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4150,7 +4163,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-886tmkcf7h
+          name: antrea-config-2cg6khh44b
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4433,7 +4446,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-886tmkcf7h
+          name: antrea-config-2cg6khh44b
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index 3614dfbf7..63d21544d 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -3922,6 +3922,19 @@ data:
     # If there are multiple IP addresses configured on the interface, the first one is used.
     # The interface configured with Node IP is used if this parameter is not set.
     #transportInterface:
+
+    # Option antreaProxy contains AntreaProxy related configuration options.
+    antreaProxy:
+      # 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. 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.
+      #proxyAll: false
+      # A string array of values which specifies the host IPv4/IPv6 addresses for NodePort. Values can be valid IP blocks.
+      # (e.g. 1.2.3.0/24, 1.2.3.4/32). An empty string slice is meant to select all host IPv4/IPv6 addresses.
+      # Note that the option is only valid when proxyAll is true.
+      #nodePortAddresses: []
   antrea-cni.conflist: |
     {
         "cniVersion":"0.3.0",
@@ -4028,7 +4041,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-7b5cffbt26
+  name: antrea-config-kh429k9mg7
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4099,7 +4112,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-7b5cffbt26
+          value: antrea-config-kh429k9mg7
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4150,7 +4163,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-7b5cffbt26
+          name: antrea-config-kh429k9mg7
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4434,7 +4447,7 @@ spec:
           path: /home/kubernetes/bin
         name: host-cni-bin
       - configMap:
-          name: antrea-config-7b5cffbt26
+          name: antrea-config-kh429k9mg7
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index a6a617af8..b2273a5f8 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -3927,6 +3927,19 @@ data:
     # If there are multiple IP addresses configured on the interface, the first one is used.
     # The interface configured with Node IP is used if this parameter is not set.
     #transportInterface:
+
+    # Option antreaProxy contains AntreaProxy related configuration options.
+    antreaProxy:
+      # 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. 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.
+      #proxyAll: false
+      # A string array of values which specifies the host IPv4/IPv6 addresses for NodePort. Values can be valid IP blocks.
+      # (e.g. 1.2.3.0/24, 1.2.3.4/32). An empty string slice is meant to select all host IPv4/IPv6 addresses.
+      # Note that the option is only valid when proxyAll is true.
+      #nodePortAddresses: []
   antrea-cni.conflist: |
     {
         "cniVersion":"0.3.0",
@@ -4033,7 +4046,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-ct4b8f8t4h
+  name: antrea-config-f8ktc7gg2c
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4113,7 +4126,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-ct4b8f8t4h
+          value: antrea-config-f8ktc7gg2c
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4164,7 +4177,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-ct4b8f8t4h
+          name: antrea-config-f8ktc7gg2c
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4480,7 +4493,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-ct4b8f8t4h
+          name: antrea-config-f8ktc7gg2c
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index 53e7f47a9..d11d97e9c 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -3927,6 +3927,19 @@ data:
     # If there are multiple IP addresses configured on the interface, the first one is used.
     # The interface configured with Node IP is used if this parameter is not set.
     #transportInterface:
+
+    # Option antreaProxy contains AntreaProxy related configuration options.
+    antreaProxy:
+      # 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. 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.
+      #proxyAll: false
+      # A string array of values which specifies the host IPv4/IPv6 addresses for NodePort. Values can be valid IP blocks.
+      # (e.g. 1.2.3.0/24, 1.2.3.4/32). An empty string slice is meant to select all host IPv4/IPv6 addresses.
+      # Note that the option is only valid when proxyAll is true.
+      #nodePortAddresses: []
   antrea-cni.conflist: |
     {
         "cniVersion":"0.3.0",
@@ -4033,7 +4046,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-fhh5d26dg8
+  name: antrea-config-48bgbgd97b
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4104,7 +4117,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-fhh5d26dg8
+          value: antrea-config-48bgbgd97b
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4155,7 +4168,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-fhh5d26dg8
+          name: antrea-config-48bgbgd97b
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4436,7 +4449,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-fhh5d26dg8
+          name: antrea-config-48bgbgd97b
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/base/conf/antrea-agent.conf b/build/yamls/base/conf/antrea-agent.conf
index 43488921c..5f3f651a8 100644
--- a/build/yamls/base/conf/antrea-agent.conf
+++ b/build/yamls/base/conf/antrea-agent.conf
@@ -164,3 +164,16 @@ wireGuard:
 # If there are multiple IP addresses configured on the interface, the first one is used.
 # The interface configured with Node IP is used if this parameter is not set.
 #transportInterface:
+
+# Option antreaProxy contains AntreaProxy related configuration options.
+antreaProxy:
+  # 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. 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.
+  #proxyAll: false
+  # A string array of values which specifies the host IPv4/IPv6 addresses for NodePort. Values can be valid IP blocks.
+  # (e.g. 1.2.3.0/24, 1.2.3.4/32). An empty string slice is meant to select all host IPv4/IPv6 addresses.
+  # Note that the option is only valid when proxyAll is true.
+  #nodePortAddresses: []
diff --git a/ci/kind/kind-setup.sh b/ci/kind/kind-setup.sh
index f7af16687..4687a26aa 100755
--- a/ci/kind/kind-setup.sh
+++ b/ci/kind/kind-setup.sh
@@ -28,6 +28,7 @@ NUM_WORKERS=2
 SUBNETS=""
 ENCAP_MODE=""
 PROXY=true
+KUBE_PROXY_MODE="iptables"
 PROMETHEUS=false
 
 THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
@@ -49,6 +50,7 @@ where:
   --pod-cidr: specifies pod cidr used in kind cluster, default is $POD_CIDR
   --encap-mode: inter-node pod traffic encap mode, default is encap
   --no-proxy: disable Antrea proxy
+  --no-kube-proxy: disable Kube proxy
   --antrea-cni: specifies install Antrea CNI in kind cluster, default is true
   --prometheus: create RBAC resources for Prometheus, default is false
   --num-workers: specifies number of worker nodes in kind cluster, default is $NUM_WORKERS
@@ -80,6 +82,13 @@ function modify {
   peerName=$(docker run --net=host antrea/ethtool:latest ip link | grep ^"$peerIdx": | awk -F[:@] '{ print $2 }' | cut -c 2-)
   echo "Disabling TX checksum offload for node $node ($peerName)"
   docker run --net=host --privileged antrea/ethtool:latest ethtool -K "$peerName" tx off
+  # In Kind cluster, DNAT operation is configured by Docker as all DNS requests from Pod CoreDNS are NAT'd to the Docker
+  # DNS embedded resolver, which is running on localhost. When kube-proxy is enabled, parameter net.ipv4.conf.all.route_localnet
+  # is set to 1 by kube-proxy. This setting ensures that the DNS response can be forwarded back to Pod CoreDNS, otherwise
+  # DNS response from Docker DNS embedded resolver will be discarded. When kube-proxy is disabled, to ensure that DNS
+  # response can be forwarded back to Pod CoreDNS, we also set parameter net.ipv4.conf.all.route_localnet to 1 through
+  # the following command:
+  docker exec "$node" sysctl -w net.ipv4.conf.all.route_localnet=1
 }
 
 function configure_networks {
@@ -244,6 +253,7 @@ networking:
   disableDefaultCNI: true
   podSubnet: $POD_CIDR
   ipFamily: $IP_FAMILY
+  kubeProxyMode: $KUBE_PROXY_MODE
 nodes:
 - role: control-plane
 EOF
@@ -340,6 +350,10 @@ while [[ $# -gt 0 ]]
       PROXY=false
       shift
       ;;
+    --no-kube-proxy)
+      KUBE_PROXY_MODE="none"
+      shift
+      ;;
     --prometheus)
       PROMETHEUS=true
       shift
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index aa748ad33..83c95a3bc 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -60,6 +60,9 @@ import (
 // https://github.com/kubernetes/kubernetes/blob/release-1.17/pkg/controller/apis/config/v1alpha1/defaults.go#L120
 const informerDefaultResync = 12 * time.Hour
 
+// The devices that should be excluded from NodePort.
+var excludeNodePortDevices = []string{"antrea-egress0", "kube-ipvs0"}
+
 // run starts Antrea agent with the given options and waits for termination signal.
 func run(o *Options) error {
 	klog.Infof("Starting Antrea agent (version %s)", version.GetFullVersion())
@@ -104,7 +107,8 @@ func run(o *Options) error {
 		features.DefaultFeatureGate.Enabled(features.AntreaProxy),
 		features.DefaultFeatureGate.Enabled(features.AntreaPolicy),
 		features.DefaultFeatureGate.Enabled(features.Egress),
-		features.DefaultFeatureGate.Enabled(features.FlowExporter))
+		features.DefaultFeatureGate.Enabled(features.FlowExporter),
+		o.config.AntreaProxy.ProxyAll)
 
 	_, serviceCIDRNet, _ := net.ParseCIDR(o.config.ServiceCIDR)
 	var serviceCIDRNetv6 *net.IPNet
@@ -129,7 +133,7 @@ func run(o *Options) error {
 	wireguardConfig := &config.WireGuardConfig{
 		Port: o.config.WireGuard.Port,
 	}
-	routeClient, err := route.NewClient(serviceCIDRNet, networkConfig, o.config.NoSNAT)
+	routeClient, err := route.NewClient(serviceCIDRNet, networkConfig, o.config.NoSNAT, o.config.AntreaProxy.ProxyAll)
 	if err != nil {
 		return fmt.Errorf("error creating route client: %v", err)
 	}
@@ -144,6 +148,16 @@ func run(o *Options) error {
 	// cause the stopCh channel to be closed; if another signal is received before the program
 	// exits, we will force exit.
 	stopCh := signals.RegisterSignalHandlers()
+
+	// Get all available NodePort addresses.
+	var nodePortAddressesIPv4, nodePortAddressesIPv6 []net.IP
+	if o.config.AntreaProxy.ProxyAll {
+		nodePortAddressesIPv4, nodePortAddressesIPv6, err = getAvailableNodePortAddresses(o.config.AntreaProxy.NodePortAddresses, append(excludeNodePortDevices, o.config.HostGateway))
+		if err != nil {
+			return fmt.Errorf("getting available NodePort IP addresses failed: %v", err)
+		}
+	}
+
 	// Initialize agent and node network.
 	agentInitializer := agent.NewInitializer(
 		k8sClient,
@@ -160,7 +174,11 @@ func run(o *Options) error {
 		wireguardConfig,
 		networkReadyCh,
 		stopCh,
-		features.DefaultFeatureGate.Enabled(features.AntreaProxy))
+		features.DefaultFeatureGate.Enabled(features.AntreaProxy),
+		o.config.AntreaProxy.ProxyAll,
+		nodePortAddressesIPv4,
+		nodePortAddressesIPv6)
+
 	err = agentInitializer.Initialize()
 	if err != nil {
 		return fmt.Errorf("error initializing agent: %v", err)
@@ -182,13 +200,15 @@ func run(o *Options) error {
 	if features.DefaultFeatureGate.Enabled(features.AntreaProxy) {
 		v4Enabled := config.IsIPv4Enabled(nodeConfig, networkConfig.TrafficEncapMode)
 		v6Enabled := config.IsIPv6Enabled(nodeConfig, networkConfig.TrafficEncapMode)
+		proxyAll := o.config.AntreaProxy.ProxyAll
+
 		switch {
 		case v4Enabled && v6Enabled:
-			proxier = proxy.NewDualStackProxier(nodeConfig.Name, informerFactory, ofClient)
+			proxier = proxy.NewDualStackProxier(nodeConfig.Name, informerFactory, ofClient, routeClient, nodePortAddressesIPv4, nodePortAddressesIPv6, proxyAll)
 		case v4Enabled:
-			proxier = proxy.NewProxier(nodeConfig.Name, informerFactory, ofClient, false)
+			proxier = proxy.NewProxier(nodeConfig.Name, informerFactory, ofClient, false, routeClient, nodePortAddressesIPv4, proxyAll)
 		case v6Enabled:
-			proxier = proxy.NewProxier(nodeConfig.Name, informerFactory, ofClient, true)
+			proxier = proxy.NewProxier(nodeConfig.Name, informerFactory, ofClient, true, routeClient, nodePortAddressesIPv6, proxyAll)
 		default:
 			return fmt.Errorf("at least one of IPv4 or IPv6 should be enabled")
 		}
diff --git a/cmd/antrea-agent/config.go b/cmd/antrea-agent/config.go
index 4ae77758b..4c10ff5a6 100644
--- a/cmd/antrea-agent/config.go
+++ b/cmd/antrea-agent/config.go
@@ -162,6 +162,18 @@ type AgentConfig struct {
 	// If there are multiple IP addresses configured on the interface, the first one is used.
 	// The interface configured with Node IP is used if this parameter is not set.
 	TransportInterface string `yaml:"transportInterface,omitempty"`
+	// AntreaProxy contains AntreaProxy related configuration options.
+	AntreaProxy AntreaProxyConfig `yaml:"antreaProxy,omitempty"`
+}
+
+type AntreaProxyConfig struct {
+	// 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. This requires the AntreaProxy
+	// feature to be enabled.
+	ProxyAll bool `yaml:"proxyAll,omitempty"`
+	// A string array of values which specifies the host IPv4/IPv6 addresses for NodePorts. Values may be valid IP blocks.
+	// (e.g. 1.2.3.0/24, 1.2.3.4/32). An empty string slice is meant to select all host IPv4/IPv6 addresses.
+	NodePortAddresses []string `yaml:"nodePortAddresses,omitempty"`
 }
 
 type WireGuardConfig struct {
diff --git a/cmd/antrea-agent/options.go b/cmd/antrea-agent/options.go
index 7c4f0973b..077a22e16 100644
--- a/cmd/antrea-agent/options.go
+++ b/cmd/antrea-agent/options.go
@@ -150,6 +150,9 @@ func (o *Options) validate(args []string) error {
 		// (but SNAT can be done by the primary CNI).
 		o.config.NoSNAT = true
 	}
+	if err := o.validateAntreaProxyConfig(); err != nil {
+		return fmt.Errorf("proxy config is invalid: %w", err)
+	}
 	if err := o.validateFlowExporterConfig(); err != nil {
 		return fmt.Errorf("failed to validate flow exporter config: %v", err)
 	}
@@ -228,6 +231,17 @@ func (o *Options) setDefaults() {
 	}
 }
 
+func (o *Options) validateAntreaProxyConfig() error {
+	if o.config.AntreaProxy.ProxyAll {
+		for _, nodePortAddress := range o.config.AntreaProxy.NodePortAddresses {
+			if _, _, err := net.ParseCIDR(nodePortAddress); err != nil {
+				return fmt.Errorf("invalid NodePort IP address `%s`: %w", nodePortAddress, err)
+			}
+		}
+	}
+	return nil
+}
+
 func (o *Options) validateFlowExporterConfig() error {
 	if features.DefaultFeatureGate.Enabled(features.FlowExporter) {
 		host, port, proto, err := flowexport.ParseFlowCollectorAddr(o.config.FlowCollectorAddr, defaultFlowCollectorPort, defaultFlowCollectorTransport)
diff --git a/cmd/antrea-agent/util.go b/cmd/antrea-agent/util.go
new file mode 100644
index 000000000..e10c2ab80
--- /dev/null
+++ b/cmd/antrea-agent/util.go
@@ -0,0 +1,55 @@
+// Copyright 2021 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"net"
+
+	"antrea.io/antrea/pkg/agent/util"
+)
+
+func getAvailableNodePortAddresses(nodePortAddressesFromConfig []string, excludeDevices []string) ([]net.IP, []net.IP, error) {
+	// Get all IP addresses of Node
+	nodeAddressesIPv4, nodeAddressesIPv6, err := util.GetAllNodeAddresses(excludeDevices)
+	if err != nil {
+		return nil, nil, err
+	}
+	// If option `NodePortAddresses` is not set, then all Node IP addresses will be used as NodePort IP address.
+	if len(nodePortAddressesFromConfig) == 0 {
+		return nodeAddressesIPv4, nodeAddressesIPv6, nil
+	}
+
+	var nodePortIPNets []*net.IPNet
+	for _, nodePortIP := range nodePortAddressesFromConfig {
+		_, ipNet, _ := net.ParseCIDR(nodePortIP)
+		nodePortIPNets = append(nodePortIPNets, ipNet)
+	}
+
+	var nodePortAddressesIPv4, nodePortAddressesIPv6 []net.IP
+	for _, nodePortIPNet := range nodePortIPNets {
+		for i := range nodeAddressesIPv4 {
+			if nodePortIPNet.Contains(nodeAddressesIPv4[i]) {
+				nodePortAddressesIPv4 = append(nodePortAddressesIPv4, nodeAddressesIPv4[i])
+			}
+		}
+		for i := range nodeAddressesIPv6 {
+			if nodePortIPNet.Contains(nodeAddressesIPv6[i]) {
+				nodePortAddressesIPv6 = append(nodePortAddressesIPv6, nodeAddressesIPv6[i])
+			}
+		}
+	}
+
+	return nodePortAddressesIPv4, nodePortAddressesIPv6, nil
+}
diff --git a/hack/generate-manifest.sh b/hack/generate-manifest.sh
index 6a779c635..a10ba160e 100755
--- a/hack/generate-manifest.sh
+++ b/hack/generate-manifest.sh
@@ -29,6 +29,7 @@ Generate a YAML manifest for Antrea using Kustomize and print it to stdout.
         --ipsec                       Generate a manifest with IPSec encryption of tunnel traffic enabled
         --all-features                Generate a manifest with all alpha features enabled
         --no-proxy                    Generate a manifest with Antrea proxy disabled
+        --proxy-all                   Generate a manifest with Antrea proxy with all Service support enabled
         --no-legacy-crd               Generate a manifest without legacy CRD mirroring support enabled
         --endpointslice               Generate a manifest with EndpointSlice support enabled
         --no-np                       Generate a manifest with Antrea-native policies disabled
@@ -71,6 +72,7 @@ KIND=false
 IPSEC=false
 ALLFEATURES=false
 PROXY=true
+PROXY_ALL=false
 LEGACY_CRD=true
 ENDPOINTSLICE=false
 NP=true
@@ -121,6 +123,11 @@ case $key in
     PROXY=false
     shift
     ;;
+    --proxy-all)
+    PROXY=true
+    PROXY_ALL=true
+    shift
+    ;;
     --no-legacy-crd)
     LEGACY_CRD=false
     shift
@@ -190,7 +197,13 @@ esac
 done
 
 if [ "$PROXY" == false ] && [ "$ENDPOINTSLICE" == true ]; then
-    echoerr "--endpointslice requires AntreaProxy and therefore cannot be used with --no-proxy"
+    echoerr "--endpointslice requires AntreaProxy, so it cannot be used with --no-proxy"
+    print_help
+    exit 1
+fi
+
+if [ "$PROXY" == false ] && [ "$PROXY_ALL" == true ]; then
+    echoerr "--proxy-all requires AntreaProxy, so it cannot be used with --no-proxy"
     print_help
     exit 1
 fi
@@ -293,12 +306,17 @@ if $ALLFEATURES; then
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*FlowExporter[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  FlowExporter: true/" antrea-agent.conf
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*NetworkPolicyStats[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  NetworkPolicyStats: true/" antrea-agent.conf
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*EndpointSlice[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  EndpointSlice: true/" antrea-agent.conf
+    sed -i.bak -E "s/^[[:space:]]*#proxyAll[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  proxyAll: true/" antrea-agent.conf
 fi
 
 if ! $PROXY; then
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*AntreaProxy[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  AntreaProxy: false/" antrea-agent.conf
 fi
 
+if $PROXY_ALL; then
+     sed -i.bak -E "s/^[[:space:]]*#proxyAll[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  proxyAll: true/" antrea-agent.conf
+fi
+
 if ! $LEGACY_CRD; then
     sed -i.bak -E "s/^#legacyCRDMirroring[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/legacyCRDMirroring: false/" antrea-controller.conf
 fi
diff --git a/hack/kind-fix-networking.sh b/hack/kind-fix-networking.sh
index e6351c4af..ed244d348 100755
--- a/hack/kind-fix-networking.sh
+++ b/hack/kind-fix-networking.sh
@@ -29,4 +29,5 @@ for node in "$@"; do
     peerName=$(docker run --net=host antrea/ethtool:latest ip link | grep ^"$peerIdx": | awk -F[:@] '{ print $2 }' | cut -c 2-)
     echo "Disabling TX checksum offload for node $node ($peerName)"
     docker run --net=host --privileged antrea/ethtool:latest ethtool -K "$peerName" tx off
+    docker exec "$node" sysctl -w net.ipv4.conf.all.route_localnet=1
 done
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index e237a5691..0c2e9236c 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -86,8 +86,11 @@ type Initializer struct {
 	enableProxy     bool
 	// networkReadyCh should be closed once the Node's network is ready.
 	// The CNI server will wait for it before handling any CNI Add requests.
-	networkReadyCh chan<- struct{}
-	stopCh         <-chan struct{}
+	proxyAll              bool
+	nodePortAddressesIPv4 []net.IP
+	nodePortAddressesIPv6 []net.IP
+	networkReadyCh        chan<- struct{}
+	stopCh                <-chan struct{}
 }
 
 func NewInitializer(
@@ -105,23 +108,31 @@ func NewInitializer(
 	wireGuardConfig *config.WireGuardConfig,
 	networkReadyCh chan<- struct{},
 	stopCh <-chan struct{},
-	enableProxy bool) *Initializer {
+	enableProxy bool,
+	proxyAll bool,
+	nodePortAddressesIPv4 []net.IP,
+	nodePortAddressesIPv6 []net.IP,
+) *Initializer {
+
 	return &Initializer{
-		ovsBridgeClient: ovsBridgeClient,
-		client:          k8sClient,
-		ifaceStore:      ifaceStore,
-		ofClient:        ofClient,
-		routeClient:     routeClient,
-		ovsBridge:       ovsBridge,
-		hostGateway:     hostGateway,
-		mtu:             mtu,
-		serviceCIDR:     serviceCIDR,
-		serviceCIDRv6:   serviceCIDRv6,
-		networkConfig:   networkConfig,
-		wireGuardConfig: wireGuardConfig,
-		networkReadyCh:  networkReadyCh,
-		stopCh:          stopCh,
-		enableProxy:     enableProxy,
+		ovsBridgeClient:       ovsBridgeClient,
+		client:                k8sClient,
+		ifaceStore:            ifaceStore,
+		ofClient:              ofClient,
+		routeClient:           routeClient,
+		ovsBridge:             ovsBridge,
+		hostGateway:           hostGateway,
+		mtu:                   mtu,
+		serviceCIDR:           serviceCIDR,
+		serviceCIDRv6:         serviceCIDRv6,
+		networkConfig:         networkConfig,
+		wireGuardConfig:       wireGuardConfig,
+		networkReadyCh:        networkReadyCh,
+		stopCh:                stopCh,
+		enableProxy:           enableProxy,
+		proxyAll:              proxyAll,
+		nodePortAddressesIPv4: nodePortAddressesIPv4,
+		nodePortAddressesIPv6: nodePortAddressesIPv6,
 	}
 }
 
@@ -403,7 +414,7 @@ func (i *Initializer) initOpenFlowPipeline() error {
 		// Set up flow entries to enable Service connectivity. The agent proxy handles
 		// ClusterIP Services while the upstream kube-proxy is leveraged to handle
 		// any other kinds of Services.
-		if err := i.ofClient.InstallClusterServiceFlows(); err != nil {
+		if err := i.ofClient.InstallDefaultServiceFlows(i.nodePortAddressesIPv4, i.nodePortAddressesIPv6); err != nil {
 			klog.Errorf("Failed to setup default OpenFlow entries for ClusterIP Services: %v", err)
 			return err
 		}
diff --git a/pkg/agent/config/node_config.go b/pkg/agent/config/node_config.go
index d5f8e0499..b15b118d3 100644
--- a/pkg/agent/config/node_config.go
+++ b/pkg/agent/config/node_config.go
@@ -44,6 +44,18 @@ const (
 	IPv6ExtraOverhead = 20
 )
 
+var (
+	// VirtualServiceIPv4 / VirtualServiceIPv6 are used in the following situations:
+	// - Use the virtual IP to perform SNAT for packets of Service from Antrea gateway and the Endpoint is not on
+	//   local Pod CIDR or any remote Pod CIDRs. It is used in OVS flow of table serviceConntrackCommitTable.
+	// - Use the virtual IP to perform DNAT for packets of NodePort on host. It is used in iptables rules on host.
+	// - Use the virtual IP as onlink routing entry gateway in host routing entry.
+	// - Use the virtual IP as destination IP in host routing entry. It is used to forward DNATed NodePort packets
+	//   or replied SNATed Service packets back to Antrea gateway.
+	VirtualServiceIPv4 = net.ParseIP("169.254.169.253")
+	VirtualServiceIPv6 = net.ParseIP("fc01::aabb:ccdd:eeff")
+)
+
 type GatewayConfig struct {
 	// Name is the name of host gateway, e.g. antrea-gw0.
 	Name string
diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go
index 5803ba568..eaea01da8 100644
--- a/pkg/agent/openflow/client.go
+++ b/pkg/agent/openflow/client.go
@@ -20,6 +20,7 @@ import (
 	"net"
 
 	"antrea.io/libOpenflow/protocol"
+	v1 "k8s.io/api/core/v1"
 	"k8s.io/klog/v2"
 
 	"antrea.io/antrea/pkg/agent/config"
@@ -50,9 +51,9 @@ type Client interface {
 	// the Cluster Service CIDR as a parameter.
 	InstallClusterServiceCIDRFlows(serviceNets []*net.IPNet) error
 
-	// InstallClusterServiceFlows sets up the appropriate flows so that traffic can reach
+	// InstallDefaultServiceFlows sets up the appropriate flows so that traffic can reach
 	// the different Services running in the Cluster. This method needs to be invoked once.
-	InstallClusterServiceFlows() error
+	InstallDefaultServiceFlows(nodePortAddressesIPv4, nodePortAddressesIPv6 []net.IP) error
 
 	// InstallDefaultTunnelFlows sets up the classification flow for the default (flow based) tunnel.
 	InstallDefaultTunnelFlows() error
@@ -103,13 +104,12 @@ type Client interface {
 	// InstallEndpointFlows.
 	UninstallEndpointFlows(protocol binding.Protocol, endpoint proxy.Endpoint) error
 
-	// InstallServiceFlows installs flows for accessing Service with clusterIP.
-	// It installs the flow that uses the group/bucket to do service LB. If the
-	// affinityTimeout is not zero, it also installs the flow which has a learn
-	// action to maintain the LB decision.
-	// The group with the groupID must be installed before, otherwise the
-	// installation will fail.
-	InstallServiceFlows(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16) error
+	// InstallServiceFlows installs flows for accessing Service NodePort, LoadBalancer and ClusterIP. It installs the
+	// flow that uses the group/bucket to do service LB. If the affinityTimeout is not zero, it also installs the flow
+	// which has a learn action to maintain the LB decision. The group with the groupID must be installed before,
+	// otherwise the installation will fail. If the externalTrafficPolicy of NodePort/LoadBalancer is Local,
+	// nodeLocalExternal will be true, otherwise it will be false.
+	InstallServiceFlows(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16, nodeLocalExternal bool, svcType v1.ServiceType) error
 	// UninstallServiceFlows removes flows installed by InstallServiceFlows.
 	UninstallServiceFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error
 	// InstallLoadBalancerServiceFromOutsideFlows installs flows for LoadBalancer Service traffic from outside node.
@@ -583,13 +583,13 @@ func (c *client) UninstallEndpointFlows(protocol binding.Protocol, endpoint prox
 	return c.deleteFlows(c.serviceFlowCache, cacheKey)
 }
 
-func (c *client) InstallServiceFlows(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16) error {
+func (c *client) InstallServiceFlows(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16, nodeLocalExternal bool, svcType v1.ServiceType) error {
 	c.replayMutex.RLock()
 	defer c.replayMutex.RUnlock()
 	var flows []binding.Flow
-	flows = append(flows, c.serviceLBFlow(groupID, svcIP, svcPort, protocol, affinityTimeout != 0))
+	flows = append(flows, c.serviceLBFlow(groupID, svcIP, svcPort, protocol, affinityTimeout != 0, nodeLocalExternal, svcType))
 	if affinityTimeout != 0 {
-		flows = append(flows, c.serviceLearnFlow(groupID, svcIP, svcPort, protocol, affinityTimeout))
+		flows = append(flows, c.serviceLearnFlow(groupID, svcIP, svcPort, protocol, affinityTimeout, nodeLocalExternal, svcType))
 	}
 	cacheKey := generateServicePortFlowCacheKey(svcIP, svcPort, protocol)
 	return c.addFlows(c.serviceFlowCache, cacheKey, flows)
@@ -613,7 +613,7 @@ func (c *client) GetServiceFlowKeys(svcIP net.IP, svcPort uint16, protocol bindi
 	return flowKeys
 }
 
-func (c *client) InstallClusterServiceFlows() error {
+func (c *client) InstallDefaultServiceFlows(nodePortAddressesIPv4, nodePortAddressesIPv6 []net.IP) error {
 	flows := []binding.Flow{
 		c.serviceNeedLBFlow(),
 		c.sessionAffinityReselectFlow(),
@@ -622,10 +622,27 @@ func (c *client) InstallClusterServiceFlows() error {
 	if c.IsIPv4Enabled() {
 		flows = append(flows, c.serviceHairpinResponseDNATFlow(binding.ProtocolIP))
 		flows = append(flows, c.serviceLBBypassFlows(binding.ProtocolIP)...)
+		flows = append(flows, c.l3FwdServiceDefaultFlowsViaGW(binding.ProtocolIP, cookie.Service)...)
+		if c.proxyAll {
+			// The output interface of a packet is the same as where it is from, and the action of the packet should be
+			// IN_PORT, rather than output. When a packet of Service is from Antrea gateway and its Endpoint is on host
+			// network, it needs hairpin mark (by setting a register, it will be matched at table L2ForwardingOutTable).
+			flows = append(flows, c.serviceHairpinRegSetFlows(binding.ProtocolIP))
+			// These flows are used to match the first packet of NodePort. The flows will set a bit of a register to mark
+			// the Service type of the packet as NodePort. The mark will be consumed in table serviceLBTable to match NodePort
+			flows = append(flows, c.serviceClassifierFlows(nodePortAddressesIPv4, binding.ProtocolIP)...)
+		}
 	}
 	if c.IsIPv6Enabled() {
 		flows = append(flows, c.serviceHairpinResponseDNATFlow(binding.ProtocolIPv6))
 		flows = append(flows, c.serviceLBBypassFlows(binding.ProtocolIPv6)...)
+		flows = append(flows, c.l3FwdServiceDefaultFlowsViaGW(binding.ProtocolIPv6, cookie.Service)...)
+		if c.proxyAll {
+			// As IPv4 above.
+			flows = append(flows, c.serviceHairpinRegSetFlows(binding.ProtocolIPv6))
+			// As IPv4 above.
+			flows = append(flows, c.serviceClassifierFlows(nodePortAddressesIPv6, binding.ProtocolIPv6)...)
+		}
 	}
 	if err := c.ofEntryOperations.AddAll(flows); err != nil {
 		return err
diff --git a/pkg/agent/openflow/fields.go b/pkg/agent/openflow/fields.go
index 9e6018159..692511b48 100644
--- a/pkg/agent/openflow/fields.go
+++ b/pkg/agent/openflow/fields.go
@@ -14,7 +14,10 @@
 
 package openflow
 
-import binding "antrea.io/antrea/pkg/ovs/openflow"
+import (
+	"antrea.io/antrea/pkg/agent/config"
+	binding "antrea.io/antrea/pkg/ovs/openflow"
+)
 
 // Fields using reg.
 var (
@@ -67,6 +70,8 @@ var (
 	// reg1(NXM_NX_REG1)
 	// Field to cache the ofPort of the OVS interface where to output packet.
 	TargetOFPortField = binding.NewRegField(1, 0, 31, "TargetOFPort")
+	// ToGatewayRegMark marks that the output interface is Antrea gateway.
+	ToGatewayRegMark = binding.NewRegMark(TargetOFPortField, config.HostGatewayOFPort)
 
 	// reg2(NXM_NX_REG2)
 	// Field to help swap values in two different flow fields in the OpenFlow actions. This field is only used in func
@@ -94,6 +99,16 @@ var (
 	// reg4[0..18]: Field to store the union value of Endpoint port and Endpoint status. It is used as a single match
 	// when needed.
 	EpUnionField = binding.NewRegField(4, 0, 18, "EndpointUnion")
+	// reg4[19]: Field to mark that whether Service type is NodePort.
+	NodePortAddressField = binding.NewRegField(4, 19, 19, "NodePortAddress")
+	// ToNodePortAddressRegMark marks that the Service type as NodePort.
+	ToNodePortAddressRegMark = binding.NewRegMark(NodePortAddressField, 0b1)
+	// reg4[20]: Field to mark that whether the packet of Service NodePort/LoadBalancer from gateway requires SNAT.
+	ServiceSNATField = binding.NewRegField(4, 20, 20, "ServiceSNAT")
+	// ServiceNeedSNATRegMark marks that the packet of Service NodePort/LoadBalancer requires SNAT.
+	ServiceNeedSNATRegMark = binding.NewRegMark(ServiceSNATField, 0b1)
+	// reg4[16..19]: Field to store the union value of Endpoint state and the mark of whether Service type is NodePort.
+	NodePortUnionField = binding.NewRegField(4, 16, 19, "NodePortUnion")
 
 	// reg5(NXM_NX_REG5)
 	// Field to cache the Egress conjunction ID hit by TraceFlow packet.
diff --git a/pkg/agent/openflow/pipeline.go b/pkg/agent/openflow/pipeline.go
index 7adf2cc0a..e64e8cc3a 100644
--- a/pkg/agent/openflow/pipeline.go
+++ b/pkg/agent/openflow/pipeline.go
@@ -26,6 +26,7 @@ import (
 
 	"antrea.io/libOpenflow/protocol"
 	"antrea.io/ofnet/ofctrl"
+	v1 "k8s.io/api/core/v1"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/klog/v2"
 
@@ -47,9 +48,11 @@ const (
 	spoofGuardTable              binding.TableIDType = 10
 	arpResponderTable            binding.TableIDType = 20
 	ipv6Table                    binding.TableIDType = 21
-	serviceHairpinTable          binding.TableIDType = 29
+	serviceHairpinTable          binding.TableIDType = 23
+	serviceConntrackTable        binding.TableIDType = 24 // serviceConntrackTable use a new ct_zone to transform SNAT'd connections.
 	conntrackTable               binding.TableIDType = 30
 	conntrackStateTable          binding.TableIDType = 31
+	serviceClassifierTable       binding.TableIDType = 35
 	sessionAffinityTable         binding.TableIDType = 40
 	dnatTable                    binding.TableIDType = 40
 	serviceLBTable               binding.TableIDType = 41
@@ -69,7 +72,8 @@ const (
 	IngressDefaultTable          binding.TableIDType = 100
 	IngressMetricTable           binding.TableIDType = 101
 	conntrackCommitTable         binding.TableIDType = 105
-	hairpinSNATTable             binding.TableIDType = 106
+	serviceConntrackCommitTable  binding.TableIDType = 106
+	hairpinSNATTable             binding.TableIDType = 108
 	L2ForwardingOutTable         binding.TableIDType = 110
 
 	// Flow priority level
@@ -130,8 +134,10 @@ var (
 		{arpResponderTable, "ARPResponder"},
 		{ipv6Table, "IPv6"},
 		{serviceHairpinTable, "ServiceHairpin"},
+		{serviceConntrackTable, "ServiceConntrack"},
 		{conntrackTable, "ConntrackZone"},
 		{conntrackStateTable, "ConntrackState"},
+		{serviceClassifierTable, "ServiceClassifier"},
 		{dnatTable, "DNAT(SessionAffinity)"},
 		{sessionAffinityTable, "SessionAffinity"},
 		{serviceLBTable, "ServiceLB"},
@@ -149,6 +155,7 @@ var (
 		{IngressDefaultTable, "IngressDefaultRule"},
 		{IngressMetricTable, "IngressMetric"},
 		{conntrackCommitTable, "ConntrackCommit"},
+		{serviceConntrackCommitTable, "ServiceConntrackCommit"},
 		{hairpinSNATTable, "HairpinSNATTable"},
 		{L2ForwardingOutTable, "Output"},
 	}
@@ -220,8 +227,10 @@ func (rt regType) reg() string {
 }
 
 const (
-	CtZone   = 0xfff0
-	CtZoneV6 = 0xffe6
+	CtZone       = 0xfff0
+	CtZoneV6     = 0xffe6
+	SNATCtZone   = 0xfff1
+	SNATCtZoneV6 = 0xffe7
 
 	// disposition values used in AP
 	DispositionAllow = 0b00
@@ -292,6 +301,7 @@ func portToUint16(port int) uint16 {
 
 type client struct {
 	enableProxy        bool
+	proxyAll           bool
 	enableAntreaPolicy bool
 	enableDenyTracking bool
 	enableEgress       bool
@@ -461,11 +471,15 @@ func (c *client) defaultFlows() (flows []binding.Flow) {
 
 // tunnelClassifierFlow generates the flow to mark traffic comes from the tunnelOFPort.
 func (c *client) tunnelClassifierFlow(tunnelOFPort uint32, category cookie.Category) binding.Flow {
+	nextTable := conntrackTable
+	if c.proxyAll {
+		nextTable = serviceConntrackTable
+	}
 	return c.pipeline[ClassifierTable].BuildFlow(priorityNormal).
 		MatchInPort(tunnelOFPort).
 		Action().LoadRegMark(FromTunnelRegMark).
 		Action().LoadRegMark(RewriteMACRegMark).
-		Action().GotoTable(conntrackTable).
+		Action().GotoTable(nextTable).
 		Cookie(c.cookieAllocator.Request(category).Raw()).
 		Done()
 }
@@ -508,37 +522,142 @@ func (c *client) connectionTrackFlows(category cookie.Category) []binding.Flow {
 	connectionTrackCommitTable := c.pipeline[conntrackCommitTable]
 	flows := c.conntrackBasicFlows(category)
 	if c.enableProxy {
-		flows = append(flows,
-			// Replace the default flow with multiple resubmits actions.
-			connectionTrackStateTable.BuildFlow(priorityMiss).
+		// Replace the default flow with multiple resubmits actions.
+		if c.proxyAll {
+			flows = append(flows, connectionTrackStateTable.BuildFlow(priorityMiss).
 				Cookie(c.cookieAllocator.Request(category).Raw()).
+				Action().ResubmitToTable(serviceClassifierTable).
 				Action().ResubmitToTable(sessionAffinityTable).
 				Action().ResubmitToTable(serviceLBTable).
-				Done(),
-			// Enable NAT.
-			connectionTrackTable.BuildFlow(priorityNormal).MatchProtocol(binding.ProtocolIP).
-				Action().CT(false, connectionTrackTable.GetNext(), CtZone).NAT().CTDone().
-				Cookie(c.cookieAllocator.Request(category).Raw()).
-				Done(),
-			connectionTrackTable.BuildFlow(priorityNormal).MatchProtocol(binding.ProtocolIPv6).
-				Action().CT(false, connectionTrackTable.GetNext(), CtZoneV6).NAT().CTDone().
-				Cookie(c.cookieAllocator.Request(category).Raw()).
-				Done(),
-			connectionTrackCommitTable.BuildFlow(priorityLow).MatchProtocol(binding.ProtocolIP).
-				MatchCTStateTrk(true).
-				MatchCTMark(ServiceCTMark).
-				MatchRegMark(EpSelectedRegMark).
-				Cookie(c.cookieAllocator.Request(category).Raw()).
-				Action().GotoTable(connectionTrackCommitTable.GetNext()).
-				Done(),
-			connectionTrackCommitTable.BuildFlow(priorityLow).MatchProtocol(binding.ProtocolIPv6).
-				MatchCTStateTrk(true).
-				MatchCTMark(ServiceCTMark).
-				MatchRegMark(EpSelectedRegMark).
+				Done())
+		} else {
+			flows = append(flows, connectionTrackStateTable.BuildFlow(priorityMiss).
 				Cookie(c.cookieAllocator.Request(category).Raw()).
-				Action().GotoTable(connectionTrackCommitTable.GetNext()).
-				Done(),
-		)
+				Action().ResubmitToTable(sessionAffinityTable).
+				Action().ResubmitToTable(serviceLBTable).
+				Done())
+		}
+
+		for _, proto := range c.ipProtocols {
+			gatewayIP := c.nodeConfig.GatewayConfig.IPv4
+			serviceVirtualIP := config.VirtualServiceIPv4
+			snatZone := SNATCtZone
+			ctZone := CtZone
+			if proto == binding.ProtocolIPv6 {
+				gatewayIP = c.nodeConfig.GatewayConfig.IPv6
+				serviceVirtualIP = config.VirtualServiceIPv6
+				snatZone = SNATCtZoneV6
+				ctZone = CtZoneV6
+			}
+			flows = append(flows,
+				// This flow is used to maintain DNAT conntrack for Service traffic.
+				connectionTrackTable.BuildFlow(priorityNormal).MatchProtocol(proto).
+					Action().CT(false, connectionTrackTable.GetNext(), ctZone).NAT().CTDone().
+					Cookie(c.cookieAllocator.Request(category).Raw()).
+					Done(),
+				connectionTrackCommitTable.BuildFlow(priorityLow).MatchProtocol(proto).
+					MatchCTStateTrk(true).
+					MatchCTMark(ServiceCTMark).
+					MatchRegMark(EpSelectedRegMark).
+					Cookie(c.cookieAllocator.Request(category).Raw()).
+					Action().GotoTable(connectionTrackCommitTable.GetNext()).
+					Done(),
+			)
+
+			if c.proxyAll {
+				serviceConnectionTrackTable := c.pipeline[serviceConntrackTable]
+				serviceConnectionTrackCommitTable := c.pipeline[serviceConntrackCommitTable]
+				flows = append(flows,
+					// This flow is used to match the Service traffic from Antrea gateway. The Service traffic from gateway
+					// should enter table serviceConntrackCommitTable, otherwise it will be matched by other flows in
+					// table connectionTrackCommit.
+					connectionTrackCommitTable.BuildFlow(priorityHigh).MatchProtocol(proto).
+						MatchCTMark(ServiceCTMark).
+						MatchRegMark(FromGatewayRegMark).
+						Action().GotoTable(serviceConntrackCommitTable).
+						Cookie(c.cookieAllocator.Request(category).Raw()).
+						Done(),
+					// This flow is used to maintain SNAT conntrack for Service traffic.
+					serviceConnectionTrackTable.BuildFlow(priorityNormal).MatchProtocol(proto).
+						Action().CT(false, serviceConnectionTrackTable.GetNext(), snatZone).NAT().CTDone().
+						Cookie(c.cookieAllocator.Request(category).Raw()).
+						Done(),
+					// This flow is used to match the following cases:
+					// - The first packet of NodePort/LoadBalancer whose Endpoint is not on local Pod CIDR or any remote
+					//   Pod CIDRs. Note that, this flow will change the behavior of the packet that NodePort/LoadBalancer
+					//   whose externalTrafficPolicy is Local and the Endpoint is on host network. According to the definition
+					//   of externalTrafficPolicy Local, the source IP should be retained. If the Endpoint is on host network,
+					//   there should be only one backend Pod of the Service on a Node (It is impossible to have more than
+					//   one Pods which listen on the same port on host network), so it is not useful to expose the Pod as
+					//   NodePort Service, as it makes no difference to access it directly.
+					// - The first packet of ClusterIP and the Endpoint is not on local Pod CIDR or any remote Pod CIDRs.
+					// As the packet is from Antrea gateway, and it will pass through Antrea gateway, a virtual IP is used
+					// to perform SNAT for the packet, rather than Antrea gateway's IP.
+					serviceConnectionTrackCommitTable.BuildFlow(priorityHigh).MatchProtocol(proto).
+						MatchRegMark(ToGatewayRegMark).
+						Cookie(c.cookieAllocator.Request(category).Raw()).
+						MatchCTStateNew(true).
+						MatchCTStateTrk(true).
+						Action().CT(true, serviceConnectionTrackCommitTable.GetNext(), snatZone).
+						SNAT(&binding.IPRange{StartIP: serviceVirtualIP, EndIP: serviceVirtualIP}, nil).
+						CTDone().
+						Done(),
+					// This flow is used to match the first packet of NodePort/LoadBalancer whose output port is not
+					// Antrea gateway, and externalTrafficPolicy is Cluster. This packet requires SNAT. Antrea gateway
+					// IP is used to perform SNAT for the packet.
+					serviceConnectionTrackCommitTable.BuildFlow(priorityNormal).MatchProtocol(proto).
+						MatchRegMark(ServiceNeedSNATRegMark).
+						Cookie(c.cookieAllocator.Request(category).Raw()).
+						MatchCTStateNew(true).
+						MatchCTStateTrk(true).
+						Action().CT(true, serviceConnectionTrackCommitTable.GetNext(), snatZone).
+						SNAT(&binding.IPRange{StartIP: gatewayIP, EndIP: gatewayIP}, nil).
+						CTDone().
+						Done(),
+					// This flow is used to match the consequent request packets of Service traffic whose first request packet has been committed
+					// and performed SNAT. For example:
+					/*
+							* 192.168.77.1 is the IP address of client.
+							* 192.168.77.100 is the IP address of k8s node.
+							* 30001 is a NodePort port.
+							* 10.10.0.1 is the IP address of Antrea gateway.
+							* 10.10.0.3 is the Endpoint of NodePort Service.
+
+							* pkt 1 (request)
+								* client                     192.168.77.1:12345->192.168.77.100:30001
+								* ct zone SNAT 65521         192.168.77.1:12345->192.168.77.100:30001
+								* ct zone DNAT 65520         192.168.77.1:12345->192.168.77.100:30001
+								* ct commit DNAT zone 65520  192.168.77.1:12345->192.168.77.100:30001  =>  192.168.77.1:12345->10.10.0.3:80
+								* ct commit SNAT zone 65521  192.168.77.1:12345->10.10.0.3:80          =>  10.10.0.1:12345->10.10.0.3:80
+								* output
+							  * pkt 2 (response)
+								* Pod                         10.10.0.3:80->10.10.0.1:12345
+								* ct zone SNAT 65521          10.10.0.3:80->10.10.0.1:12345            =>  10.10.0.3:80->192.168.77.1:12345
+								* ct zone DNAT 65520          10.10.0.3:80->192.168.77.1:12345         =>  192.168.77.1:30001->192.168.77.1:12345
+								* output
+							  * pkt 3 (request)
+								* client                     192.168.77.1:12345->192.168.77.100:30001
+								* ct zone SNAT 65521         192.168.77.1:12345->192.168.77.100:30001
+								* ct zone DNAT 65520         192.168.77.1:12345->10.10.0.3:80
+								* ct zone SNAT 65521         192.168.77.1:12345->10.10.0.3:80          =>  10.10.0.1:12345->10.10.0.3:80
+								* output
+						      * pkt ...
+
+							The source IP address of pkt 3 cannot be transformed through zone 65521 as there is no connection track about
+							192.168.77.1:12345<->192.168.77.100:30001, and the source IP is still 192.168.77.100.
+							Before output, pkt 3 needs SNAT, but the connection has been committed. The flow is for pkt 3 to perform SNAT.
+					*/
+					serviceConnectionTrackCommitTable.BuildFlow(priorityNormal).MatchProtocol(proto).
+						Cookie(c.cookieAllocator.Request(category).Raw()).
+						MatchCTStateNew(false).
+						MatchCTStateTrk(true).
+						Action().CT(false, serviceConnectionTrackCommitTable.GetNext(), snatZone).
+						NAT().
+						CTDone().
+						Done(),
+				)
+			}
+		}
 	} else {
 		flows = append(flows, c.kubeProxyFlows(category)...)
 	}
@@ -580,8 +699,11 @@ func (c *client) conntrackBypassRejectFlow(proto binding.Protocol) binding.Flow
 // dnsResponseBypassConntrackFlow generates a flow which is used to bypass the
 // dns response packetout from conntrack, to avoid unexpected packet drop.
 func (c *client) dnsResponseBypassConntrackFlow() binding.Flow {
-	conntrackTable := c.pipeline[conntrackTable]
-	return conntrackTable.BuildFlow(priorityHigh).
+	table := c.pipeline[conntrackTable]
+	if c.proxyAll {
+		table = c.pipeline[serviceConntrackTable]
+	}
+	return table.BuildFlow(priorityHigh).
 		MatchRegFieldWithValue(CustomReasonField, CustomReasonDNS).
 		Cookie(c.cookieAllocator.Request(cookie.Default).Raw()).
 		Action().ResubmitToTable(l2ForwardingCalcTable).
@@ -1166,6 +1288,31 @@ func (c *client) l3FwdFlowToRemoteViaGW(
 		Done()
 }
 
+// l3FwdServiceDefaultFlowsViaGW generates the default L3 forward flows to support Service traffic to pass through Antrea gateway.
+func (c *client) l3FwdServiceDefaultFlowsViaGW(ipProto binding.Protocol, category cookie.Category) []binding.Flow {
+	gatewayMAC := c.nodeConfig.GatewayConfig.MAC
+
+	flows := []binding.Flow{
+		// This flow is used to match the packets of Service traffic:
+		//	- NodePort/LoadBalancer request packets which pass through Antrea gateway and the Service Endpoint is not on
+		//    local Pod CIDR or any remote Pod CIDRs.
+		//	- ClusterIP request packets which are from Antrea gateway and the Service Endpoint is not on local Pod CIDR
+		// 	or any remote Pod CIDRs.
+		//  - NodePort/LoadBalancer/ClusterIP response packets.
+		// The matched packets should leave through Antrea gateway, however, they also enter through Antrea gateway. This
+		// is hairpin traffic.
+		c.pipeline[l3ForwardingTable].BuildFlow(priorityLow).MatchProtocol(ipProto).
+			MatchCTMark(ServiceCTMark).
+			MatchCTStateTrk(true).
+			MatchRegMark(RewriteMACRegMark).
+			Action().SetDstMAC(gatewayMAC).
+			Action().GotoTable(l3DecTTLTable).
+			Cookie(c.cookieAllocator.Request(category).Raw()).
+			Done(),
+	}
+	return flows
+}
+
 // arpResponderFlow generates the ARP responder flow entry that replies request comes from local gateway for peer
 // gateway MAC.
 func (c *client) arpResponderFlow(peerGatewayIP net.IP, category cookie.Category) binding.Flow {
@@ -1246,18 +1393,32 @@ func getIPProtocol(ip net.IP) binding.Protocol {
 // IP of the hairpin packet to the source IP.
 func (c *client) serviceHairpinResponseDNATFlow(ipProtocol binding.Protocol) binding.Flow {
 	hpIP := hairpinIP
-	from := "NXM_OF_IP_SRC"
-	to := "NXM_OF_IP_DST"
+	from := binding.NxmFieldSrcIPv4
+	to := binding.NxmFieldDstIPv4
 	if ipProtocol == binding.ProtocolIPv6 {
 		hpIP = hairpinIPv6
-		from = "NXM_NX_IPV6_SRC"
-		to = "NXM_NX_IPV6_DST"
+		from = binding.NxmFieldSrcIPv6
+		to = binding.NxmFieldDstIPv6
 	}
-	return c.pipeline[serviceHairpinTable].BuildFlow(priorityNormal).MatchProtocol(ipProtocol).
+	hairpinTable := c.pipeline[serviceHairpinTable]
+	return hairpinTable.BuildFlow(priorityNormal).MatchProtocol(ipProtocol).
 		MatchDstIP(hpIP).
 		Action().Move(from, to).
 		Action().LoadRegMark(HairpinRegMark).
-		Action().GotoTable(conntrackTable).
+		Action().GotoTable(hairpinTable.GetNext()).
+		Cookie(c.cookieAllocator.Request(cookie.Service).Raw()).
+		Done()
+}
+
+// serviceHairpinRegSetFlows generates the flow to set the hairpin mark for the packet which is from Antrea gateway and
+// its output interface is also Antrea gateway. In table L2ForwardingOutTable #110, a packet with hairpin mark will be
+// sent out with action IN_PORT, otherwise the packet with action output will be dropped.
+func (c *client) serviceHairpinRegSetFlows(ipProtocol binding.Protocol) binding.Flow {
+	return c.pipeline[hairpinSNATTable].BuildFlow(priorityNormal).MatchProtocol(ipProtocol).
+		MatchRegMark(FromGatewayRegMark).
+		MatchRegMark(ToGatewayRegMark).
+		Action().LoadRegMark(HairpinRegMark).
+		Action().GotoTable(L2ForwardingOutTable).
 		Cookie(c.cookieAllocator.Request(cookie.Service).Raw()).
 		Done()
 }
@@ -1972,22 +2133,66 @@ func (c *client) loadBalancerServiceFromOutsideFlow(svcIP net.IP, svcPort uint16
 		Done()
 }
 
+// serviceClassifierFlows generate the flows to match the first packet of Service NodePort and set a bit of a register
+// to mark the Service type as NodePort.
+func (c *client) serviceClassifierFlows(nodePortAddresses []net.IP, ipProtocol binding.Protocol) []binding.Flow {
+	virtualServiceIP := config.VirtualServiceIPv4
+	if ipProtocol == binding.ProtocolIPv6 {
+		virtualServiceIP = config.VirtualServiceIPv6
+	}
+	// Generate flows for every NodePort IP address. The flows are used to match the first packet of Service NodePort from
+	// Pod.
+	var flows []binding.Flow
+	for i := range nodePortAddresses {
+		flows = append(flows,
+			c.pipeline[serviceClassifierTable].BuildFlow(priorityNormal).
+				Cookie(c.cookieAllocator.Request(cookie.Service).Raw()).
+				MatchProtocol(ipProtocol).
+				MatchDstIP(nodePortAddresses[i]).
+				Action().LoadRegMark(ToNodePortAddressRegMark).
+				Done())
+	}
+	// Generate flow for the virtual IP. The flow is used to match the first packet of Service NodePort from Antrea gateway,
+	// because the destination IP of the packet has already performed DNAT with the virtual IP on host.
+	flows = append(flows,
+		c.pipeline[serviceClassifierTable].BuildFlow(priorityNormal).
+			Cookie(c.cookieAllocator.Request(cookie.Service).Raw()).
+			MatchProtocol(ipProtocol).
+			MatchDstIP(virtualServiceIP).
+			Action().LoadRegMark(ToNodePortAddressRegMark).
+			Done())
+
+	return flows
+}
+
 // serviceLearnFlow generates the flow with learn action which adds new flows in
 // sessionAffinityTable according to the Endpoint selection decision.
-func (c *client) serviceLearnFlow(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16) binding.Flow {
+func (c *client) serviceLearnFlow(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16, nodeLocalExternal bool, svcType v1.ServiceType) binding.Flow {
 	// Using unique cookie ID here to avoid learned flow cascade deletion.
 	cookieID := c.cookieAllocator.RequestWithObjectID(cookie.Service, uint32(groupID)).Raw()
-	learnFlowBuilder := c.pipeline[serviceLBTable].BuildFlow(priorityLow).
-		MatchRegMark(EpToLearnRegMark).
-		MatchDstIP(svcIP).
-		MatchProtocol(protocol).
-		MatchDstPort(svcPort, nil).
-		Cookie(cookieID)
+
+	var flowBuilder binding.FlowBuilder
+	if svcType == v1.ServiceTypeNodePort {
+		unionVal := (ToNodePortAddressRegMark.GetValue() << ServiceEPStateField.GetRange().Length()) + EpToLearnRegMark.GetValue()
+		flowBuilder = c.pipeline[serviceLBTable].BuildFlow(priorityLow).
+			Cookie(cookieID).
+			MatchRegFieldWithValue(NodePortUnionField, unionVal).
+			MatchProtocol(protocol).
+			MatchDstPort(svcPort, nil)
+	} else {
+		flowBuilder = c.pipeline[serviceLBTable].BuildFlow(priorityLow).
+			Cookie(cookieID).
+			MatchRegMark(EpToLearnRegMark).
+			MatchDstIP(svcIP).
+			MatchProtocol(protocol).
+			MatchDstPort(svcPort, nil)
+	}
+
 	// affinityTimeout is used as the OpenFlow "hard timeout": learned flow will be removed from
 	// OVS after that time regarding of whether traffic is still hitting the flow. This is the
 	// desired behavior based on the K8s spec. Note that existing connections will keep going to
 	// the same endpoint because of connection tracking; and that is also the desired behavior.
-	learnFlowBuilderLearnAction := learnFlowBuilder.
+	learnFlowBuilderLearnAction := flowBuilder.
 		Action().Learn(sessionAffinityTable, priorityNormal, 0, affinityTimeout, cookieID).
 		DeleteLearned()
 	ipProtocol := binding.ProtocolIP
@@ -2008,6 +2213,12 @@ func (c *client) serviceLearnFlow(groupID binding.GroupIDType, svcIP net.IP, svc
 		ipProtocol = binding.ProtocolIPv6
 		learnFlowBuilderLearnAction = learnFlowBuilderLearnAction.MatchLearnedSCTPv6DstPort()
 	}
+	// If externalTrafficPolicy of NodePort/LoadBalancer is Cluster, the learned flow which
+	// is used to match the first packet of NodePort/LoadBalancer also requires SNAT.
+	if (svcType == v1.ServiceTypeNodePort || svcType == v1.ServiceTypeLoadBalancer) && !nodeLocalExternal {
+		learnFlowBuilderLearnAction = learnFlowBuilderLearnAction.LoadRegMark(ServiceNeedSNATRegMark)
+	}
+
 	if ipProtocol == binding.ProtocolIP {
 		return learnFlowBuilderLearnAction.
 			MatchLearnedDstIP().
@@ -2038,7 +2249,7 @@ func (c *client) serviceLearnFlow(groupID binding.GroupIDType, svcIP net.IP, svc
 
 // serviceLBFlow generates the flow which uses the specific group to do Endpoint
 // selection.
-func (c *client) serviceLBFlow(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, withSessionAffinity bool) binding.Flow {
+func (c *client) serviceLBFlow(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, withSessionAffinity, nodeLocalExternal bool, svcType v1.ServiceType) binding.Flow {
 	var lbResultMark *binding.RegMark
 	if withSessionAffinity {
 		lbResultMark = EpToLearnRegMark
@@ -2046,16 +2257,37 @@ func (c *client) serviceLBFlow(groupID binding.GroupIDType, svcIP net.IP, svcPor
 		lbResultMark = EpSelectedRegMark
 	}
 
-	return c.pipeline[serviceLBTable].BuildFlow(priorityNormal).
-		MatchProtocol(protocol).
-		MatchDstPort(svcPort, nil).
-		MatchDstIP(svcIP).
-		MatchRegMark(EpToSelectRegMark).
-		Action().LoadRegMark(lbResultMark).
-		Action().LoadRegMark(RewriteMACRegMark).
-		Action().Group(groupID).
-		Cookie(c.cookieAllocator.Request(cookie.Service).Raw()).
-		Done()
+	var flowBuilder binding.FlowBuilder
+	if svcType == v1.ServiceTypeNodePort {
+		// If externalTrafficPolicy of NodePort is Cluster, the first packet of NodePort requires SNAT, so nodeLocalExternal
+		// will be false, and ServiceNeedSNATRegMark will be set. If externalTrafficPolicy of NodePort is Local, the first
+		// packet of NodePort doesn't require SNAT, ServiceNeedSNATRegMark won't be set.
+		unionVal := (ToNodePortAddressRegMark.GetValue() << ServiceEPStateField.GetRange().Length()) + EpToSelectRegMark.GetValue()
+		flowBuilder = c.pipeline[serviceLBTable].BuildFlow(priorityNormal).
+			Cookie(c.cookieAllocator.Request(cookie.Service).Raw()).
+			MatchProtocol(protocol).
+			MatchRegFieldWithValue(NodePortUnionField, unionVal).
+			MatchDstPort(svcPort, nil).
+			Action().LoadRegMark(lbResultMark).
+			Action().LoadRegMark(RewriteMACRegMark)
+		if !nodeLocalExternal {
+			flowBuilder = flowBuilder.Action().LoadRegMark(ServiceNeedSNATRegMark)
+		}
+	} else {
+		// If Service type is LoadBalancer, as above NodePort.
+		flowBuilder = c.pipeline[serviceLBTable].BuildFlow(priorityNormal).
+			Cookie(c.cookieAllocator.Request(cookie.Service).Raw()).
+			MatchProtocol(protocol).
+			MatchDstPort(svcPort, nil).
+			MatchDstIP(svcIP).
+			MatchRegMark(EpToSelectRegMark).
+			Action().LoadRegMark(lbResultMark).
+			Action().LoadRegMark(RewriteMACRegMark)
+		if svcType == v1.ServiceTypeLoadBalancer && !nodeLocalExternal {
+			flowBuilder = flowBuilder.Action().LoadRegMark(ServiceNeedSNATRegMark)
+		}
+	}
+	return flowBuilder.Action().Group(groupID).Done()
 }
 
 // endpointDNATFlow generates the flow which transforms the Service Cluster IP
@@ -2228,7 +2460,14 @@ func (c *client) generatePipeline() {
 	if c.enableProxy {
 		c.pipeline[spoofGuardTable] = bridge.CreateTable(spoofGuardTable, serviceHairpinTable, binding.TableMissActionDrop)
 		c.pipeline[ipv6Table] = bridge.CreateTable(ipv6Table, serviceHairpinTable, binding.TableMissActionNext)
-		c.pipeline[serviceHairpinTable] = bridge.CreateTable(serviceHairpinTable, conntrackTable, binding.TableMissActionNext)
+		if c.proxyAll {
+			c.pipeline[serviceHairpinTable] = bridge.CreateTable(serviceHairpinTable, serviceConntrackTable, binding.TableMissActionNext)
+			c.pipeline[serviceConntrackTable] = bridge.CreateTable(serviceConntrackTable, conntrackTable, binding.TableMissActionNext)
+			c.pipeline[serviceClassifierTable] = bridge.CreateTable(serviceClassifierTable, binding.LastTableID, binding.TableMissActionNone)
+			c.pipeline[serviceConntrackCommitTable] = bridge.CreateTable(serviceConntrackCommitTable, hairpinSNATTable, binding.TableMissActionNext)
+		} else {
+			c.pipeline[serviceHairpinTable] = bridge.CreateTable(serviceHairpinTable, conntrackTable, binding.TableMissActionNext)
+		}
 		c.pipeline[conntrackStateTable] = bridge.CreateTable(conntrackStateTable, endpointDNATTable, binding.TableMissActionNext)
 		c.pipeline[sessionAffinityTable] = bridge.CreateTable(sessionAffinityTable, binding.LastTableID, binding.TableMissActionNone)
 		c.pipeline[serviceLBTable] = bridge.CreateTable(serviceLBTable, endpointDNATTable, binding.TableMissActionNext)
@@ -2256,7 +2495,14 @@ func (c *client) generatePipeline() {
 }
 
 // NewClient is the constructor of the Client interface.
-func NewClient(bridgeName, mgmtAddr string, ovsDatapathType ovsconfig.OVSDatapathType, enableProxy, enableAntreaPolicy, enableEgress bool, enableDenyTracking bool) Client {
+func NewClient(bridgeName string,
+	mgmtAddr string,
+	ovsDatapathType ovsconfig.OVSDatapathType,
+	enableProxy bool,
+	enableAntreaPolicy bool,
+	enableEgress bool,
+	enableDenyTracking bool,
+	proxyAll bool) Client {
 	bridge := binding.NewOFBridge(bridgeName, mgmtAddr)
 	policyCache := cache.NewIndexer(
 		policyConjKeyFunc,
@@ -2265,6 +2511,7 @@ func NewClient(bridgeName, mgmtAddr string, ovsDatapathType ovsconfig.OVSDatapat
 	c := &client{
 		bridge:                   bridge,
 		enableProxy:              enableProxy,
+		proxyAll:                 proxyAll,
 		enableAntreaPolicy:       enableAntreaPolicy,
 		enableDenyTracking:       enableDenyTracking,
 		enableEgress:             enableEgress,
diff --git a/pkg/agent/proxy/proxier.go b/pkg/agent/proxy/proxier.go
index ebee1993c..c49bf44ef 100644
--- a/pkg/agent/proxy/proxier.go
+++ b/pkg/agent/proxy/proxier.go
@@ -32,9 +32,11 @@ import (
 	"k8s.io/klog/v2"
 	utilnet "k8s.io/utils/net"
 
+	agentconfig "antrea.io/antrea/pkg/agent/config"
 	"antrea.io/antrea/pkg/agent/openflow"
 	"antrea.io/antrea/pkg/agent/proxy/metrics"
 	"antrea.io/antrea/pkg/agent/proxy/types"
+	"antrea.io/antrea/pkg/agent/route"
 	"antrea.io/antrea/pkg/features"
 	binding "antrea.io/antrea/pkg/ovs/openflow"
 	k8sproxy "antrea.io/antrea/third_party/proxy"
@@ -99,11 +101,15 @@ type proxier struct {
 	// oversizeServiceSet records the Services that have more than 800 Endpoints.
 	oversizeServiceSet sets.String
 
-	runner              *k8sproxy.BoundedFrequencyRunner
-	stopChan            <-chan struct{}
-	ofClient            openflow.Client
-	isIPv6              bool
-	enableEndpointSlice bool
+	runner               *k8sproxy.BoundedFrequencyRunner
+	stopChan             <-chan struct{}
+	ofClient             openflow.Client
+	routeClient          route.Interface
+	nodePortAddresses    []net.IP
+	hostGateWay          string
+	isIPv6               bool
+	proxyAll             bool
+	endpointSliceEnabled bool
 }
 
 func endpointKey(endpoint k8sproxy.Endpoint, protocol binding.Protocol) string {
@@ -129,25 +135,45 @@ func (p *proxier) removeStaleServices() {
 			p.oversizeServiceSet.Delete(svcPortName.String())
 		}
 		if err := p.ofClient.UninstallServiceFlows(svcInfo.ClusterIP(), uint16(svcInfo.Port()), svcInfo.OFProtocol); err != nil {
-			klog.Errorf("Failed to remove flows of Service %v: %v", svcPortName, err)
+			klog.ErrorS(err, "Failed to remove flows of Service", "Service", svcPortName)
 			continue
 		}
-		for _, ingress := range svcInfo.LoadBalancerIPStrings() {
-			if ingress != "" {
-				if err := p.uninstallLoadBalancerServiceFlows(net.ParseIP(ingress), uint16(svcInfo.Port()), svcInfo.OFProtocol); err != nil {
-					klog.Errorf("Error when removing Service flows: %v", err)
+
+		if p.proxyAll {
+			// Remove NodePort flows and configurations.
+			if svcInfo.NodePort() > 0 {
+				if err := p.uninstallNodePortService(uint16(svcInfo.NodePort()), svcInfo.OFProtocol); err != nil {
+					klog.ErrorS(err, "Failed to remove flows and configurations of Service", "Service", svcPortName)
 					continue
 				}
 			}
 		}
-		groupID, _ := p.groupCounter.Get(svcPortName)
+		// Remove LoadBalancer flows and configurations.
+		if len(svcInfo.LoadBalancerIPStrings()) > 0 {
+			if err := p.uninstallLoadBalancerService(svcInfo.LoadBalancerIPStrings(), uint16(svcInfo.Port()), svcInfo.OFProtocol); err != nil {
+				klog.ErrorS(err, "Failed to remove flows and configurations of Service", "Service", svcPortName)
+				continue
+			}
+		}
+		// Remove Service group whose Endpoints are local.
+		if svcInfo.NodeLocalExternal() {
+			groupIDLocal, _ := p.groupCounter.Get(svcPortName, true)
+			if err := p.ofClient.UninstallServiceGroup(groupIDLocal); err != nil {
+				klog.ErrorS(err, "Failed to remove flows of Service", "Service", svcPortName)
+				continue
+			}
+			p.groupCounter.Recycle(svcPortName, true)
+		}
+		// Remove Service group which has all Endpoints.
+		groupID, _ := p.groupCounter.Get(svcPortName, false)
 		if err := p.ofClient.UninstallServiceGroup(groupID); err != nil {
-			klog.Errorf("Failed to remove flows of Service %v: %v", svcPortName, err)
+			klog.ErrorS(err, "Failed to remove flows of Service", "Service", svcPortName)
 			continue
 		}
+
 		delete(p.serviceInstalledMap, svcPortName)
 		p.deleteServiceByIP(svcInfo.String())
-		p.groupCounter.Recycle(svcPortName)
+		p.groupCounter.Recycle(svcPortName, false)
 	}
 }
 
@@ -217,7 +243,9 @@ func (p *proxier) removeStaleEndpoints() {
 func serviceIdentityChanged(svcInfo, pSvcInfo *types.ServiceInfo) bool {
 	return svcInfo.ClusterIP().String() != pSvcInfo.ClusterIP().String() ||
 		svcInfo.Port() != pSvcInfo.Port() ||
-		svcInfo.OFProtocol != pSvcInfo.OFProtocol
+		svcInfo.OFProtocol != pSvcInfo.OFProtocol ||
+		svcInfo.NodePort() != pSvcInfo.NodePort() ||
+		svcInfo.NodeLocalExternal() != pSvcInfo.NodeLocalExternal()
 }
 
 // smallSliceDifference builds a slice which includes all the strings from s1
@@ -241,10 +269,78 @@ func smallSliceDifference(s1, s2 []string) []string {
 	return diff
 }
 
+func (p *proxier) installNodePortService(groupID binding.GroupIDType, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16, nodeLocalExternal bool) error {
+	svcIP := agentconfig.VirtualServiceIPv4
+	if p.isIPv6 {
+		svcIP = agentconfig.VirtualServiceIPv6
+	}
+	if err := p.ofClient.InstallServiceFlows(groupID, svcIP, svcPort, protocol, affinityTimeout, nodeLocalExternal, corev1.ServiceTypeNodePort); err != nil {
+		return fmt.Errorf("failed to install Service NodePort load balancing flows: %w", err)
+	}
+	if err := p.routeClient.AddNodePort(p.nodePortAddresses, svcPort, protocol); err != nil {
+		return fmt.Errorf("failed to install Service NodePort traffic redirecting flows: %w", err)
+	}
+	return nil
+}
+
+func (p *proxier) uninstallNodePortService(svcPort uint16, protocol binding.Protocol) error {
+	svcIP := agentconfig.VirtualServiceIPv4
+	if p.isIPv6 {
+		svcIP = agentconfig.VirtualServiceIPv6
+	}
+	if err := p.ofClient.UninstallServiceFlows(svcIP, svcPort, protocol); err != nil {
+		return fmt.Errorf("failed to remove Service NodePort NodePort load balancing flows: %w", err)
+	}
+	if err := p.routeClient.DeleteNodePort(p.nodePortAddresses, svcPort, protocol); err != nil {
+		return fmt.Errorf("failed to remove Service NodePort traffic redirecting flows: %w", err)
+	}
+	return nil
+}
+
+func (p *proxier) installLoadBalancerService(groupID binding.GroupIDType, loadBalancerIPStrings []string, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16, nodeLocalExternal bool) error {
+	for _, ingress := range loadBalancerIPStrings {
+		if ingress != "" {
+			if err := p.ofClient.InstallServiceFlows(groupID, net.ParseIP(ingress), svcPort, protocol, affinityTimeout, nodeLocalExternal, corev1.ServiceTypeLoadBalancer); err != nil {
+				return fmt.Errorf("failed to install Service LoadBalancer load balancing flows: %w", err)
+			}
+			if err := p.ofClient.InstallLoadBalancerServiceFromOutsideFlows(net.ParseIP(ingress), svcPort, protocol); err != nil {
+				return fmt.Errorf("failed to install Service LoadBalancer flows: %w", err)
+			}
+		}
+	}
+	if p.proxyAll {
+		if err := p.routeClient.AddLoadBalancer(loadBalancerIPStrings); err != nil {
+			return fmt.Errorf("failed to install Service LoadBalancer traffic redirecting flows: %w", err)
+		}
+	}
+
+	return nil
+}
+
+func (p *proxier) uninstallLoadBalancerService(loadBalancerIPStrings []string, svcPort uint16, protocol binding.Protocol) error {
+	for _, ingress := range loadBalancerIPStrings {
+		if ingress != "" {
+			if err := p.ofClient.UninstallServiceFlows(net.ParseIP(ingress), svcPort, protocol); err != nil {
+				return fmt.Errorf("failed to remove Service LoadBalancer load balancing flows: %w", err)
+			}
+			if err := p.ofClient.UninstallLoadBalancerServiceFromOutsideFlows(net.ParseIP(ingress), svcPort, protocol); err != nil {
+				return fmt.Errorf("failed to remove Service LoadBalancer flows: %w", err)
+			}
+		}
+	}
+	if p.proxyAll {
+		if err := p.routeClient.DeleteLoadBalancer(loadBalancerIPStrings); err != nil {
+			return fmt.Errorf("failed to remove Service LoadBalancer traffic redirecting flows: %w", err)
+		}
+	}
+
+	return nil
+}
+
 func (p *proxier) installServices() {
 	for svcPortName, svcPort := range p.serviceMap {
 		svcInfo := svcPort.(*types.ServiceInfo)
-		groupID, _ := p.groupCounter.Get(svcPortName)
+		groupID, _ := p.groupCounter.Get(svcPortName, false)
 		endpointsInstalled, ok := p.endpointsInstalledMap[svcPortName]
 		if !ok {
 			endpointsInstalled = map[string]k8sproxy.Endpoint{}
@@ -334,14 +430,31 @@ func (p *proxier) installServices() {
 		if needUpdateEndpoints {
 			err := p.ofClient.InstallEndpointFlows(svcInfo.OFProtocol, endpointUpdateList)
 			if err != nil {
-				klog.Errorf("Error when installing Endpoints flows: %v", err)
+				klog.ErrorS(err, "Error when installing Endpoints flows")
 				continue
 			}
 			err = p.ofClient.InstallServiceGroup(groupID, svcInfo.StickyMaxAgeSeconds() != 0, endpointUpdateList)
 			if err != nil {
-				klog.Errorf("Error when installing Endpoints groups: %v", err)
+				klog.ErrorS(err, "Error when installing Endpoints groups")
 				continue
 			}
+
+			// Install another group when Service externalTrafficPolicy is Local.
+			if p.proxyAll && svcInfo.NodeLocalExternal() {
+				groupIDLocal, _ := p.groupCounter.Get(svcPortName, true)
+				var localEndpointList []k8sproxy.Endpoint
+				for _, ed := range endpointUpdateList {
+					if !ed.GetIsLocal() {
+						continue
+					}
+					localEndpointList = append(localEndpointList, ed)
+				}
+				if err = p.ofClient.InstallServiceGroup(groupIDLocal, svcInfo.StickyMaxAgeSeconds() != 0, localEndpointList); err != nil {
+					klog.ErrorS(err, "Error when installing Group for Service whose externalTrafficPolicy is Local")
+					continue
+				}
+			}
+
 			for _, e := range endpointUpdateList {
 				// If the Endpoint is newly installed, add a reference.
 				if _, ok := endpointsInstalled[e.String()]; !ok {
@@ -355,18 +468,57 @@ func (p *proxier) installServices() {
 		if needUpdateService {
 			// Delete previous flow.
 			if needRemoval {
+				// If previous Service should be removed, remove ClusterIP flows of previous Service.
 				if err := p.ofClient.UninstallServiceFlows(pSvcInfo.ClusterIP(), uint16(pSvcInfo.Port()), pSvcInfo.OFProtocol); err != nil {
-					klog.Errorf("Failed to remove flows of Service %v: %v", svcPortName, err)
+					klog.ErrorS(err, "Failed to remove flows of Service", "Service", svcPortName)
 					continue
 				}
+
+				if p.proxyAll {
+					// If previous Service which has NodePort should be removed, remove NodePort flows and configurations of previous Service.
+					if pSvcInfo.NodePort() > 0 {
+						if err := p.uninstallNodePortService(uint16(pSvcInfo.NodePort()), pSvcInfo.OFProtocol); err != nil {
+							klog.ErrorS(err, "Failed to remove flows and configurations of Service", "Service", svcPortName)
+							continue
+						}
+					}
+				}
 			}
-			if err := p.ofClient.InstallServiceFlows(groupID, svcInfo.ClusterIP(), uint16(svcInfo.Port()), svcInfo.OFProtocol, uint16(svcInfo.StickyMaxAgeSeconds())); err != nil {
+
+			// Install ClusterIP flows of current Service.
+			if err := p.ofClient.InstallServiceFlows(groupID, svcInfo.ClusterIP(), uint16(svcInfo.Port()), svcInfo.OFProtocol, uint16(svcInfo.StickyMaxAgeSeconds()), false, corev1.ServiceTypeClusterIP); err != nil {
 				klog.Errorf("Error when installing Service flows: %v", err)
 				continue
 			}
-			// Install OpenFlow entries for the ingress IPs of LoadBalancer Service.
-			// The LoadBalancer Service should be accessible from Pod, Node and
-			// external host.
+
+			// If externalTrafficPolicy of the Service is Local, Service NodePort or LoadBalancer should use the Service
+			// group whose Endpoints are local.
+			nGroupID := groupID
+			if svcInfo.NodeLocalExternal() {
+				nGroupID, _ = p.groupCounter.Get(svcPortName, true)
+			}
+
+			if p.proxyAll {
+				// Install ClusterIP route on Node so that ClusterIP can be accessed on Node. Every time a new ClusterIP
+				// is created, the routing target IP block will be recalculated for expansion to be able to route the new
+				// created ClusterIP. Deleting a ClusterIP will not shrink the target routing IP block. The Service CIDR
+				// can be finally calculated after creating enough ClusterIPs.
+				if err := p.routeClient.AddClusterIPRoute(svcInfo.ClusterIP()); err != nil {
+					klog.ErrorS(err, "Failed to install ClusterIP route of Service", "Service", svcPortName)
+					continue
+				}
+
+				// If previous Service is nil or NodePort flows and configurations of previous Service have been removed,
+				// install NodePort flows and configurations for current Service.
+				if svcInfo.NodePort() > 0 && (pSvcInfo == nil || needRemoval) {
+					if err := p.installNodePortService(nGroupID, uint16(svcInfo.NodePort()), svcInfo.OFProtocol, uint16(svcInfo.StickyMaxAgeSeconds()), svcInfo.NodeLocalExternal()); err != nil {
+						klog.ErrorS(err, "Failed to install NodePort flows and configurations of Service", "Service", svcPortName)
+						continue
+					}
+				}
+			}
+
+			// Service LoadBalancer flows can be partially updated.
 			var toDelete, toAdd []string
 			if needRemoval {
 				toDelete = pSvcInfo.LoadBalancerIPStrings()
@@ -375,22 +527,18 @@ func (p *proxier) installServices() {
 				toDelete = deletedLoadBalancerIPs
 				toAdd = addedLoadBalancerIPs
 			}
-			for _, ingress := range toDelete {
-				if ingress != "" {
-					// It is safe to access pSvcInfo here. If this is a new Service,
-					// then toDelete will be an empty slice.
-					if err := p.uninstallLoadBalancerServiceFlows(net.ParseIP(ingress), uint16(pSvcInfo.Port()), pSvcInfo.OFProtocol); err != nil {
-						klog.Errorf("Error when removing LoadBalancer Service flows: %v", err)
-						continue
-					}
+			// Remove LoadBalancer flows and configurations.
+			if len(toDelete) > 0 {
+				if err := p.uninstallLoadBalancerService(toDelete, uint16(pSvcInfo.Port()), pSvcInfo.OFProtocol); err != nil {
+					klog.ErrorS(err, "Failed to remove flows and configurations of Service", "Service", svcPortName)
+					continue
 				}
 			}
-			for _, ingress := range toAdd {
-				if ingress != "" {
-					if err := p.installLoadBalancerServiceFlows(groupID, net.ParseIP(ingress), uint16(svcInfo.Port()), svcInfo.OFProtocol, uint16(svcInfo.StickyMaxAgeSeconds())); err != nil {
-						klog.Errorf("Error when installing LoadBalancer Service flows: %v", err)
-						continue
-					}
+			// Install LoadBalancer flows and configurations.
+			if len(toAdd) > 0 {
+				if err := p.installLoadBalancerService(nGroupID, toAdd, uint16(svcInfo.Port()), svcInfo.OFProtocol, uint16(svcInfo.StickyMaxAgeSeconds()), svcInfo.NodeLocalExternal()); err != nil {
+					klog.ErrorS(err, "Failed to install LoadBalancer flows and configurations of Service", "Service", svcPortName)
+					continue
 				}
 			}
 		}
@@ -557,7 +705,7 @@ func (p *proxier) deleteServiceByIP(serviceStr string) {
 func (p *proxier) Run(stopCh <-chan struct{}) {
 	p.once.Do(func() {
 		go p.serviceConfig.Run(stopCh)
-		if p.enableEndpointSlice {
+		if p.endpointSliceEnabled {
 			go p.endpointSliceConfig.Run(stopCh)
 		} else {
 			go p.endpointsConfig.Run(stopCh)
@@ -605,7 +753,8 @@ func (p *proxier) GetServiceFlowKeys(serviceName, namespace string) ([]string, [
 		svcFlows := p.ofClient.GetServiceFlowKeys(svcInfo.ClusterIP(), uint16(svcInfo.Port()), svcInfo.OFProtocol, epList)
 		flows = append(flows, svcFlows...)
 
-		groupID, _ := p.groupCounter.Get(svcPortName)
+		// TODO: This causes antctl command output incomplete groups, fix it latter.
+		groupID, _ := p.groupCounter.Get(svcPortName, false)
 		groups = append(groups, groupID)
 	}
 
@@ -616,7 +765,10 @@ func NewProxier(
 	hostname string,
 	informerFactory informers.SharedInformerFactory,
 	ofClient openflow.Client,
-	isIPv6 bool) *proxier {
+	isIPv6 bool,
+	routeClient route.Interface,
+	nodePortAddresses []net.IP,
+	proxyAllEnabled bool) *proxier {
 	recorder := record.NewBroadcaster().NewRecorder(
 		runtime.NewScheme(),
 		corev1.EventSource{Component: componentName, Host: hostname},
@@ -624,18 +776,16 @@ func NewProxier(
 	metrics.Register()
 	klog.V(2).Infof("Creating proxier with IPv6 enabled=%t", isIPv6)
 
-	enableEndpointSlice := features.DefaultFeatureGate.Enabled(features.EndpointSlice)
-
+	endpointSliceEnabled := features.DefaultFeatureGate.Enabled(features.EndpointSlice)
 	ipFamily := corev1.IPv4Protocol
 	if isIPv6 {
 		ipFamily = corev1.IPv6Protocol
 	}
 
 	p := &proxier{
-		enableEndpointSlice:      enableEndpointSlice,
 		endpointsConfig:          config.NewEndpointsConfig(informerFactory.Core().V1().Endpoints(), resyncPeriod),
 		serviceConfig:            config.NewServiceConfig(informerFactory.Core().V1().Services(), resyncPeriod),
-		endpointsChanges:         newEndpointsChangesTracker(hostname, enableEndpointSlice, isIPv6),
+		endpointsChanges:         newEndpointsChangesTracker(hostname, endpointSliceEnabled, isIPv6),
 		serviceChanges:           newServiceChangesTracker(recorder, ipFamily),
 		serviceMap:               k8sproxy.ServiceMap{},
 		serviceInstalledMap:      k8sproxy.ServiceMap{},
@@ -646,12 +796,17 @@ func NewProxier(
 		oversizeServiceSet:       sets.NewString(),
 		groupCounter:             types.NewGroupCounter(isIPv6),
 		ofClient:                 ofClient,
+		routeClient:              routeClient,
+		nodePortAddresses:        nodePortAddresses,
 		isIPv6:                   isIPv6,
+		proxyAll:                 proxyAllEnabled,
+		endpointSliceEnabled:     endpointSliceEnabled,
 	}
+
 	p.serviceConfig.RegisterEventHandler(p)
 	p.endpointsConfig.RegisterEventHandler(p)
 	p.runner = k8sproxy.NewBoundedFrequencyRunner(componentName, p.syncProxyRules, time.Second, 30*time.Second, 2)
-	if enableEndpointSlice {
+	if endpointSliceEnabled {
 		p.endpointSliceConfig = config.NewEndpointSliceConfig(informerFactory.Discovery().V1beta1().EndpointSlices(), resyncPeriod)
 		p.endpointSliceConfig.RegisterEventHandler(p)
 	} else {
@@ -691,13 +846,19 @@ func (p *metaProxierWrapper) GetServiceByIP(serviceStr string) (k8sproxy.Service
 }
 
 func NewDualStackProxier(
-	hostname string, informerFactory informers.SharedInformerFactory, ofClient openflow.Client) *metaProxierWrapper {
+	hostname string,
+	informerFactory informers.SharedInformerFactory,
+	ofClient openflow.Client,
+	routeClient route.Interface,
+	nodePortAddressesIPv4 []net.IP,
+	nodePortAddressesIPv6 []net.IP,
+	proxyAllEnabled bool) *metaProxierWrapper {
 
-	// Create an ipv4 instance of the single-stack proxier
-	ipv4Proxier := NewProxier(hostname, informerFactory, ofClient, false)
+	// Create an ipv4 instance of the single-stack proxier.
+	ipv4Proxier := NewProxier(hostname, informerFactory, ofClient, false, routeClient, nodePortAddressesIPv4, proxyAllEnabled)
 
-	// Create an ipv6 instance of the single-stack proxier
-	ipv6Proxier := NewProxier(hostname, informerFactory, ofClient, true)
+	// Create an ipv6 instance of the single-stack proxier.
+	ipv6Proxier := NewProxier(hostname, informerFactory, ofClient, true, routeClient, nodePortAddressesIPv6, proxyAllEnabled)
 
 	// Create a meta-proxier that dispatch calls between the two
 	// single-stack proxier instances.
diff --git a/pkg/agent/proxy/proxier_others.go b/pkg/agent/proxy/proxier_others.go
deleted file mode 100644
index 08cf80cb5..000000000
--- a/pkg/agent/proxy/proxier_others.go
+++ /dev/null
@@ -1,41 +0,0 @@
-//go:build !windows
-// +build !windows
-
-// Copyright 2020 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 proxy
-
-import (
-	"net"
-
-	binding "antrea.io/antrea/pkg/ovs/openflow"
-)
-
-// installLoadBalancerServiceFlows install OpenFlow entries for LoadBalancer Service.
-// The rules for traffic from local Pod to LoadBalancer Service are same with rules for Cluster Service.
-// For the LoadBalancer Service traffic from outside, kube-proxy will handle it.
-func (p *proxier) installLoadBalancerServiceFlows(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16) error {
-	if err := p.ofClient.InstallServiceFlows(groupID, svcIP, svcPort, protocol, affinityTimeout); err != nil {
-		return err
-	}
-	return nil
-}
-
-func (p *proxier) uninstallLoadBalancerServiceFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error {
-	if err := p.ofClient.UninstallServiceFlows(svcIP, svcPort, protocol); err != nil {
-		return err
-	}
-	return nil
-}
diff --git a/pkg/agent/proxy/proxier_windows.go b/pkg/agent/proxy/proxier_windows.go
deleted file mode 100644
index 990797cfa..000000000
--- a/pkg/agent/proxy/proxier_windows.go
+++ /dev/null
@@ -1,48 +0,0 @@
-//go:build windows
-// +build windows
-
-// Copyright 2020 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 proxy
-
-import (
-	"net"
-
-	binding "antrea.io/antrea/pkg/ovs/openflow"
-)
-
-// installLoadBalancerServiceFlows installs OpenFlow entries for LoadBalancer Service.
-// The rules for traffic from local Pod to LoadBalancer Service are the same with rules for Cluster Service.
-// For the LoadBalancer Service traffic from outside, specific rules are install to forward the packets
-// to the host network to let kube-proxy handle the traffic.
-func (p *proxier) installLoadBalancerServiceFlows(groupID binding.GroupIDType, svcIP net.IP, svcPort uint16, protocol binding.Protocol, affinityTimeout uint16) error {
-	if err := p.ofClient.InstallServiceFlows(groupID, svcIP, svcPort, protocol, affinityTimeout); err != nil {
-		return err
-	}
-	if err := p.ofClient.InstallLoadBalancerServiceFromOutsideFlows(svcIP, svcPort, protocol); err != nil {
-		return err
-	}
-	return nil
-}
-
-func (p *proxier) uninstallLoadBalancerServiceFlows(svcIP net.IP, svcPort uint16, protocol binding.Protocol) error {
-	if err := p.ofClient.UninstallServiceFlows(svcIP, svcPort, protocol); err != nil {
-		return err
-	}
-	if err := p.ofClient.UninstallLoadBalancerServiceFromOutsideFlows(svcIP, svcPort, protocol); err != nil {
-		return err
-	}
-	return nil
-}
diff --git a/pkg/agent/proxy/types/groupcounter.go b/pkg/agent/proxy/types/groupcounter.go
index 5cfd5c911..d4c537924 100644
--- a/pkg/agent/proxy/types/groupcounter.go
+++ b/pkg/agent/proxy/types/groupcounter.go
@@ -15,6 +15,7 @@
 package types
 
 import (
+	"fmt"
 	"sync"
 
 	binding "antrea.io/antrea/pkg/ovs/openflow"
@@ -27,10 +28,10 @@ type GroupCounter interface {
 	// If the group ID of the service has been generated, then return the
 	// prior one. The bool return value indicates whether the groupID is newly
 	// generated.
-	Get(svcPortName k8sproxy.ServicePortName) (binding.GroupIDType, bool)
+	Get(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) (binding.GroupIDType, bool)
 	// Recycle removes a Service Group ID mapping. The recycled groupID can be
 	// reused.
-	Recycle(svcPortName k8sproxy.ServicePortName) bool
+	Recycle(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) bool
 }
 
 type groupCounter struct {
@@ -38,7 +39,7 @@ type groupCounter struct {
 	groupIDCounter binding.GroupIDType
 	recycled       []binding.GroupIDType
 
-	groupMap map[k8sproxy.ServicePortName]binding.GroupIDType
+	groupMap map[string]binding.GroupIDType
 }
 
 func NewGroupCounter(isIPv6 bool) *groupCounter {
@@ -46,33 +47,42 @@ func NewGroupCounter(isIPv6 bool) *groupCounter {
 	if isIPv6 {
 		groupIDCounter = 0x10000000
 	}
-	return &groupCounter{groupMap: map[k8sproxy.ServicePortName]binding.GroupIDType{}, groupIDCounter: groupIDCounter}
+	return &groupCounter{groupMap: map[string]binding.GroupIDType{}, groupIDCounter: groupIDCounter}
 }
 
-func (c *groupCounter) Get(svcPortName k8sproxy.ServicePortName) (binding.GroupIDType, bool) {
+func keyString(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) string {
+	key := svcPortName.String()
+	if isEndpointsLocal {
+		key = fmt.Sprintf("%s/local", key)
+	}
+	return key
+}
+
+func (c *groupCounter) Get(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) (binding.GroupIDType, bool) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
-
-	if id, ok := c.groupMap[svcPortName]; ok {
+	key := keyString(svcPortName, isEndpointsLocal)
+	if id, ok := c.groupMap[key]; ok {
 		return id, false
 	} else if len(c.recycled) != 0 {
 		id = c.recycled[len(c.recycled)-1]
 		c.recycled = c.recycled[:len(c.recycled)-1]
-		c.groupMap[svcPortName] = id
+		c.groupMap[key] = id
 		return id, true
 	} else {
 		c.groupIDCounter += 1
-		c.groupMap[svcPortName] = c.groupIDCounter
+		c.groupMap[key] = c.groupIDCounter
 		return c.groupIDCounter, true
 	}
 }
 
-func (c *groupCounter) Recycle(svcPortName k8sproxy.ServicePortName) bool {
+func (c *groupCounter) Recycle(svcPortName k8sproxy.ServicePortName, isEndpointsLocal bool) bool {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
-	if id, ok := c.groupMap[svcPortName]; ok {
-		delete(c.groupMap, svcPortName)
+	key := keyString(svcPortName, isEndpointsLocal)
+	if id, ok := c.groupMap[key]; ok {
+		delete(c.groupMap, key)
 		c.recycled = append(c.recycled, id)
 		return true
 	}
diff --git a/pkg/agent/route/interfaces.go b/pkg/agent/route/interfaces.go
index 7053b1c6d..abe3bf12a 100644
--- a/pkg/agent/route/interfaces.go
+++ b/pkg/agent/route/interfaces.go
@@ -18,6 +18,7 @@ import (
 	"net"
 
 	"antrea.io/antrea/pkg/agent/config"
+	binding "antrea.io/antrea/pkg/ovs/openflow"
 )
 
 // Interface is the interface for routing container packets in host network.
@@ -51,6 +52,21 @@ type Interface interface {
 	// DeleteSNATRule should delete rule to SNAT outgoing traffic with the mark.
 	DeleteSNATRule(mark uint32) error
 
+	// AddNodePort adds configurations when a NodePort Service is created.
+	AddNodePort(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
+
+	// AddClusterIPRoute adds route on K8s node for Service ClusterIP.
+	AddClusterIPRoute(svcIP net.IP) error
+
+	// AddLoadBalancer adds configurations when a LoadBalancer Service is created.
+	AddLoadBalancer(externalIPs []string) error
+
+	// DeleteLoadBalancer deletes related configurations when a LoadBalancer Service is deleted.
+	DeleteLoadBalancer(externalIPs []string) 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 6e7b02dec..29e1617a1 100644
--- a/pkg/agent/route/route_linux.go
+++ b/pkg/agent/route/route_linux.go
@@ -29,6 +29,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/klog/v2"
+	utilnet "k8s.io/utils/net"
 
 	"antrea.io/antrea/pkg/agent/config"
 	"antrea.io/antrea/pkg/agent/types"
@@ -36,6 +37,7 @@ import (
 	"antrea.io/antrea/pkg/agent/util/ipset"
 	"antrea.io/antrea/pkg/agent/util/iptables"
 	"antrea.io/antrea/pkg/agent/util/sysctl"
+	binding "antrea.io/antrea/pkg/ovs/openflow"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
 	"antrea.io/antrea/pkg/util/env"
 )
@@ -50,12 +52,19 @@ const (
 	// antreaPodIP6Set contains all IPv6 Pod CIDRs of this cluster.
 	antreaPodIP6Set = "ANTREA-POD-IP6"
 
+	// Antrea proxy NodePort IP
+	antreaNodePortIPSet  = "ANTREA-NODEPORT-IP"
+	antreaNodePortIP6Set = "ANTREA-NODEPORT-IP6"
+
 	// Antrea managed iptables chains.
 	antreaForwardChain     = "ANTREA-FORWARD"
 	antreaPreRoutingChain  = "ANTREA-PREROUTING"
 	antreaPostRoutingChain = "ANTREA-POSTROUTING"
 	antreaOutputChain      = "ANTREA-OUTPUT"
 	antreaMangleChain      = "ANTREA-MANGLE"
+
+	ipv4AddrLength = 32
+	ipv6AddrLength = 128
 )
 
 // Client implements Interface.
@@ -85,16 +94,30 @@ type Client struct {
 	markToSNATIP sync.Map
 	// iptablesInitialized is used to notify when iptables initialization is done.
 	iptablesInitialized chan struct{}
+	proxyAll            bool
+	// serviceRoutes caches ip routes about Services.
+	serviceRoutes sync.Map
+	// serviceNeighbors caches neighbors.
+	serviceNeighbors sync.Map
+	// nodePortsIPv4 caches all existing IPv4 NodePorts.
+	nodePortsIPv4 sync.Map
+	// nodePortsIPv6 caches all existing IPv6 NodePorts.
+	nodePortsIPv6 sync.Map
+	// clusterIPv4CIDR stores the calculated ClusterIP CIDR for IPv4.
+	clusterIPv4CIDR *net.IPNet
+	// clusterIPv6CIDR stores the calculated ClusterIP CIDR for IPv6.
+	clusterIPv6CIDR *net.IPNet
 }
 
 // NewClient returns a route client.
 // TODO: remove param serviceCIDR after kube-proxy is replaced by Antrea Proxy. This param is not used in this file;
 // leaving it here is to be compatible with the implementation on Windows.
-func NewClient(serviceCIDR *net.IPNet, networkConfig *config.NetworkConfig, noSNAT bool) (*Client, error) {
+func NewClient(serviceCIDR *net.IPNet, networkConfig *config.NetworkConfig, noSNAT, proxyAll bool) (*Client, error) {
 	return &Client{
 		serviceCIDR:   serviceCIDR,
 		networkConfig: networkConfig,
 		noSNAT:        noSNAT,
+		proxyAll:      proxyAll,
 	}, nil
 }
 
@@ -143,6 +166,13 @@ func (c *Client) Initialize(nodeConfig *config.NodeConfig, done func()) error {
 		}
 	}
 
+	// Set up the IP routes and sysctl parameters to support all Services in AntreaProxy.
+	if c.proxyAll {
+		if err := c.initServiceIPRoutes(); err != nil {
+			return fmt.Errorf("failed to initialize Service IP routes: %v", err)
+		}
+	}
+
 	return nil
 }
 
@@ -197,6 +227,20 @@ func (c *Client) syncRoutes() error {
 		}
 		return true
 	})
+	if c.proxyAll {
+		c.serviceRoutes.Range(func(_, v interface{}) bool {
+			route := v.(*netlink.Route)
+			r, ok := routeMap[route.Dst.String()]
+			if ok && routeEqual(route, r) {
+				return true
+			}
+			if err := netlink.RouteReplace(route); err != nil {
+				klog.Errorf("Failed to add route to the gateway: %v", err)
+				return false
+			}
+			return true
+		})
+	}
 	return nil
 }
 
@@ -232,6 +276,32 @@ func (c *Client) syncIPSet() error {
 			}
 		}
 	}
+
+	// If proxy full is enabled, create NodePort ipset.
+	if c.proxyAll {
+		if err := ipset.CreateIPSet(antreaNodePortIPSet, ipset.HashIPPort, false); err != nil {
+			return err
+		}
+		if err := ipset.CreateIPSet(antreaNodePortIP6Set, ipset.HashIPPort, true); err != nil {
+			return err
+		}
+
+		c.nodePortsIPv4.Range(func(k, _ interface{}) bool {
+			ipSetEntry := k.(string)
+			if err := ipset.AddEntry(antreaNodePortIPSet, ipSetEntry); err != nil {
+				return false
+			}
+			return true
+		})
+		c.nodePortsIPv6.Range(func(k, _ interface{}) bool {
+			ipSetEntry := k.(string)
+			if err := ipset.AddEntry(antreaNodePortIP6Set, ipSetEntry); err != nil {
+				return false
+			}
+			return true
+		})
+	}
+
 	return nil
 }
 
@@ -242,6 +312,14 @@ func getIPSetName(ip net.IP) string {
 	return antreaPodIPSet
 }
 
+func getNodePortIPSetName(isIPv6 bool) string {
+	if isIPv6 {
+		return antreaNodePortIP6Set
+	} else {
+		return antreaNodePortIPSet
+	}
+}
+
 // writeEKSMangleRule writes an additional iptables mangle rule to the
 // iptablesData buffer, which is required to ensure that the reverse path for
 // NodePort Service traffic is correct on EKS.
@@ -287,6 +365,14 @@ func (c *Client) syncIPTables() error {
 		{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"},
 	}
+	if c.proxyAll {
+		jumpRules = append(jumpRules,
+			[]struct{ table, srcChain, dstChain, comment string }{
+				{iptables.NATTable, iptables.PreRoutingChain, antreaPreRoutingChain, "Antrea: jump to Antrea prerouting rules"},
+				{iptables.NATTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules"},
+			}...,
+		)
+	}
 	for _, rule := range jumpRules {
 		if err := c.ipt.EnsureChain(iptables.ProtocolDual, rule.table, rule.dstChain); err != nil {
 			return err
@@ -311,7 +397,7 @@ func (c *Client) syncIPTables() error {
 	})
 	// Use iptables-restore to configure IPv4 settings.
 	if v4Enabled {
-		iptablesData := c.restoreIptablesData(c.nodeConfig.PodIPv4CIDR, antreaPodIPSet, snatMarkToIPv4)
+		iptablesData := c.restoreIptablesData(c.nodeConfig.PodIPv4CIDR, antreaPodIPSet, antreaNodePortIPSet, config.VirtualServiceIPv4, snatMarkToIPv4)
 		// Setting --noflush to keep the previous contents (i.e. non antrea managed chains) of the tables.
 		if err := c.ipt.Restore(iptablesData.Bytes(), false, false); err != nil {
 			return err
@@ -320,7 +406,7 @@ func (c *Client) syncIPTables() error {
 
 	// Use ip6tables-restore to configure IPv6 settings.
 	if v6Enabled {
-		iptablesData := c.restoreIptablesData(c.nodeConfig.PodIPv6CIDR, antreaPodIP6Set, snatMarkToIPv6)
+		iptablesData := c.restoreIptablesData(c.nodeConfig.PodIPv6CIDR, antreaPodIP6Set, antreaNodePortIP6Set, config.VirtualServiceIPv6, snatMarkToIPv6)
 		// Setting --noflush to keep the previous contents (i.e. non antrea managed chains) of the tables.
 		if err := c.ipt.Restore(iptablesData.Bytes(), false, true); err != nil {
 			return err
@@ -329,7 +415,7 @@ func (c *Client) syncIPTables() error {
 	return nil
 }
 
-func (c *Client) restoreIptablesData(podCIDR *net.IPNet, podIPSet string, snatMarkToIP map[uint32]net.IP) *bytes.Buffer {
+func (c *Client) restoreIptablesData(podCIDR *net.IPNet, podIPSet, nodePortIPSet string, serviceVirtualIP net.IP, snatMarkToIP map[uint32]net.IP) *bytes.Buffer {
 	// Create required rules in the antrea chains.
 	// Use iptables-restore as it flushes the involved chains and creates the desired rules
 	// with a single call, instead of string matching to clean up stale rules.
@@ -409,6 +495,24 @@ func (c *Client) restoreIptablesData(podCIDR *net.IPNet, podIPSet string, snatMa
 	writeLine(iptablesData, "COMMIT")
 
 	writeLine(iptablesData, "*nat")
+	if c.proxyAll {
+		writeLine(iptablesData, iptables.MakeChainLine(antreaPreRoutingChain))
+		writeLine(iptablesData, []string{
+			"-A", antreaPreRoutingChain,
+			"-m", "comment", "--comment", `"Antrea: DNAT external to NodePort packets"`,
+			"-m", "set", "--match-set", nodePortIPSet, "dst,dst",
+			"-j", iptables.DNATTarget,
+			"--to-destination", serviceVirtualIP.String(),
+		}...)
+		writeLine(iptablesData, iptables.MakeChainLine(antreaOutputChain))
+		writeLine(iptablesData, []string{
+			"-A", antreaOutputChain,
+			"-m", "comment", "--comment", `"Antrea: DNAT local to NodePort packets"`,
+			"-m", "set", "--match-set", nodePortIPSet, "dst,dst",
+			"-j", iptables.DNATTarget,
+			"--to-destination", serviceVirtualIP.String(),
+		}...)
+	}
 	writeLine(iptablesData, iptables.MakeChainLine(antreaPostRoutingChain))
 	// Egress rules must be inserted before the default masquerade rule.
 	for snatMark, snatIP := range snatMarkToIP {
@@ -427,6 +531,7 @@ func (c *Client) restoreIptablesData(podCIDR *net.IPNet, podIPSet string, snatMa
 			"-A", antreaPostRoutingChain,
 			"-m", "comment", "--comment", `"Antrea: masquerade Pod to external packets"`,
 			"-s", podCIDR.String(), "-m", "set", "!", "--match-set", podIPSet, "dst",
+			"!", "-o", c.nodeConfig.GatewayConfig.Name,
 			"-j", iptables.MasqueradeTarget,
 		}...)
 	}
@@ -445,6 +550,17 @@ func (c *Client) restoreIptablesData(podCIDR *net.IPNet, podIPSet string, snatMa
 		"-j", iptables.MasqueradeTarget, "--random-fully",
 	}...)
 
+	// If AntreaProxy full support is enabled, it SNATs the packets whose source IP is VirtualServiceIPv4/VirtualServiceIPv6
+	// so the packets can be routed back to this Node.
+	if c.proxyAll {
+		writeLine(iptablesData, []string{
+			"-A", antreaPostRoutingChain,
+			"-m", "comment", "--comment", `"Antrea: masquerade OVS virtual source IP"`,
+			"-s", serviceVirtualIP.String(),
+			"-j", iptables.MasqueradeTarget,
+		}...)
+	}
+
 	writeLine(iptablesData, "COMMIT")
 	return iptablesData
 }
@@ -468,6 +584,20 @@ func (c *Client) initIPRoutes() error {
 	return nil
 }
 
+func (c *Client) initServiceIPRoutes() error {
+	if config.IsIPv4Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode) {
+		if err := c.addVirtualServiceIPRoute(false); err != nil {
+			return err
+		}
+	}
+	if config.IsIPv6Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode) {
+		if err := c.addVirtualServiceIPRoute(true); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 // Reconcile removes orphaned podCIDRs from ipset and removes routes to orphaned podCIDRs
 // based on the desired podCIDRs.
 func (c *Client) Reconcile(podCIDRs []string) error {
@@ -498,7 +628,7 @@ func (c *Client) Reconcile(podCIDRs []string) error {
 		}
 	}
 
-	// Remove any unknown routes on antrea-gw0.
+	// Remove any unknown routes on Antrea gateway.
 	routes, err := c.listIPRoutesOnGW()
 	if err != nil {
 		return fmt.Errorf("error listing ip routes: %v", err)
@@ -511,13 +641,18 @@ func (c *Client) Reconcile(podCIDRs []string) error {
 		if desiredPodCIDRs.Has(route.Dst.String()) {
 			continue
 		}
+		// Don't delete the routes which are added by AntreaProxy.
+		if c.isServiceRoute(&route) {
+			continue
+		}
+
 		klog.Infof("Deleting unknown route %v", route)
 		if err := netlink.RouteDel(&route); err != nil && err != unix.ESRCH {
 			return err
 		}
 	}
 
-	// Remove any unknown IPv6 neighbors on antrea-gw0.
+	// Remove any unknown IPv6 neighbors on Antrea gateway.
 	desiredGWs := getIPv6Gateways(podCIDRs)
 	// Return immediately if there is no IPv6 gateway address configured on the Nodes.
 	if desiredGWs.Len() == 0 {
@@ -532,6 +667,10 @@ func (c *Client) Reconcile(podCIDRs []string) error {
 		if desiredGWs.Has(neighIP) {
 			continue
 		}
+		// Don't delete the virtual Service IP neigh which is added by AntreaProxy.
+		if actualNeigh.IP.Equal(config.VirtualServiceIPv6) {
+			continue
+		}
 		klog.V(4).Infof("Deleting orphaned IPv6 neighbor %v", actualNeigh)
 		if err := netlink.NeighDel(actualNeigh); err != nil {
 			return err
@@ -540,7 +679,16 @@ func (c *Client) Reconcile(podCIDRs []string) error {
 	return nil
 }
 
-// listIPRoutes returns list of routes on antrea-gw0.
+func (c *Client) isServiceRoute(route *netlink.Route) bool {
+	// If the destination IP or gateway IP is virtual Service IP , then it is a route which is added by AntreaProxy.
+	if route.Dst.IP.Equal(config.VirtualServiceIPv6) || route.Dst.IP.Equal(config.VirtualServiceIPv4) ||
+		route.Gw.Equal(config.VirtualServiceIPv6) || route.Gw.Equal(config.VirtualServiceIPv4) {
+		return true
+	}
+	return false
+}
+
+// listIPRoutes returns list of routes on Antrea gateway.
 func (c *Client) listIPRoutesOnGW() ([]netlink.Route, error) {
 	filter := &netlink.Route{
 		LinkIndex: c.nodeConfig.GatewayConfig.LinkIndex}
@@ -812,3 +960,258 @@ func (c *Client) DeleteSNATRule(mark uint32) error {
 	snatIP := value.(net.IP)
 	return c.ipt.DeleteRule(iptables.ProtocolDual, iptables.NATTable, antreaPostRoutingChain, c.snatRuleSpec(snatIP, mark))
 }
+
+// addVirtualServiceIPRoute is used to add routing entry which is used to forward the packets whose destination IP is
+// virtual Service IP back to Antrea gateway on host.
+func (c *Client) addVirtualServiceIPRoute(isIPv6 bool) error {
+	linkIndex := c.nodeConfig.GatewayConfig.LinkIndex
+	svcIP := config.VirtualServiceIPv4
+	mask := ipv4AddrLength
+	if isIPv6 {
+		svcIP = config.VirtualServiceIPv6
+		mask = ipv6AddrLength
+	}
+
+	neigh := generateNeigh(svcIP, linkIndex)
+	if err := netlink.NeighSet(neigh); err != nil {
+		return fmt.Errorf("failed to add new IP neighbour for %s: %v", svcIP, err)
+	}
+	c.serviceNeighbors.Store(svcIP.String(), neigh)
+
+	route := generateRoute(svcIP, mask, nil, linkIndex, netlink.SCOPE_LINK)
+	if err := netlink.RouteReplace(route); err != nil {
+		return fmt.Errorf("failed to install route for virtual Service IP %s: %w", svcIP.String(), err)
+	}
+	c.serviceRoutes.Store(svcIP.String(), route)
+	klog.InfoS("Added virtual Service IP route", "route", route)
+
+	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 {
+	isIPv6 := isIPv6Protocol(protocol)
+	transProtocol := getTransProtocolStr(protocol)
+	ipSetName := getNodePortIPSetName(isIPv6)
+
+	for i := range nodePortAddresses {
+		ipSetEntry := fmt.Sprintf("%s,%s:%d", nodePortAddresses[i], transProtocol, port)
+		if err := ipset.AddEntry(ipSetName, ipSetEntry); err != nil {
+			return err
+		}
+		if isIPv6 {
+			c.nodePortsIPv6.Store(ipSetEntry, struct{}{})
+		} else {
+			c.nodePortsIPv4.Store(ipSetEntry, struct{}{})
+		}
+	}
+
+	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 {
+	isIPv6 := isIPv6Protocol(protocol)
+	transProtocol := getTransProtocolStr(protocol)
+	ipSetName := getNodePortIPSetName(isIPv6)
+
+	for i := range nodePortAddresses {
+		ipSetEntry := fmt.Sprintf("%s,%s:%d", nodePortAddresses[i], transProtocol, port)
+		if err := ipset.DelEntry(ipSetName, ipSetEntry); err != nil {
+			return err
+		}
+		if isIPv6 {
+			c.nodePortsIPv6.Delete(ipSetEntry)
+		} else {
+			c.nodePortsIPv4.Delete(ipSetEntry)
+		}
+	}
+
+	return nil
+}
+
+// AddClusterIPRoute is used to add or update a routing entry which is used to route ClusterIP traffic to Antrea gateway.
+func (c *Client) AddClusterIPRoute(svcIP net.IP) error {
+	isIPv6 := utilnet.IsIPv6(svcIP)
+	linkIndex := c.nodeConfig.GatewayConfig.LinkIndex
+	scope := netlink.SCOPE_UNIVERSE
+	curClusterIPCIDR := c.clusterIPv4CIDR
+	mask := ipv4AddrLength
+	gw := config.VirtualServiceIPv4
+	if isIPv6 {
+		curClusterIPCIDR = c.clusterIPv6CIDR
+		mask = ipv6AddrLength
+		gw = config.VirtualServiceIPv6
+	}
+
+	if curClusterIPCIDR != nil {
+		// If the route exists, check that whether the route can cover the ClusterIP.
+		if !curClusterIPCIDR.Contains(svcIP) {
+			// If not, generate a new destination ipNet.
+			newClusterIPCIDR, err := util.ExtendCIDRWithIP(curClusterIPCIDR, svcIP)
+			if err != nil {
+				return fmt.Errorf("extend route CIDR with error: %v", err)
+			}
+			// Generate a new route with new ClusterIP CIDR and install the route.
+			mask, _ = newClusterIPCIDR.Mask.Size()
+			route := generateRoute(newClusterIPCIDR.IP, mask, gw, linkIndex, scope)
+			if err = netlink.RouteReplace(route); err != nil {
+				return fmt.Errorf("failed to install new route: %w", err)
+			}
+
+			// Generate the current route by replacing the destination CIDR with current ClusterIP CIDR and delete the
+			// route.
+			route.Dst = curClusterIPCIDR
+			if err = netlink.RouteDel(route); err != nil {
+				return fmt.Errorf("failed to uninstall old route: %w", err)
+			}
+
+			if isIPv6 {
+				c.clusterIPv6CIDR = newClusterIPCIDR
+			} else {
+				c.clusterIPv4CIDR = newClusterIPCIDR
+			}
+			klog.V(4).InfoS("Created a route with new CLusterIP CIDR to route the ClusterIP to Antrea gateway", "CLusterIP CIDR", newClusterIPCIDR, "clusterIP", svcIP)
+		} else {
+			klog.V(4).InfoS("Route with current ClusterIP CIDR can route the ClusterIP to Antrea gateway", "ClusterIP CIDR", curClusterIPCIDR, "clusterIP", svcIP)
+		}
+	} else {
+		route := generateRoute(svcIP, mask, gw, linkIndex, scope)
+		if err := netlink.RouteReplace(route); err != nil {
+			return fmt.Errorf("failed to install new ClusterIP route: %w", err)
+		}
+
+		if isIPv6 {
+			c.clusterIPv6CIDR = route.Dst
+		} else {
+			c.clusterIPv4CIDR = route.Dst
+		}
+	}
+
+	return nil
+}
+
+// addLoadBalancerIngressIPRoute is used to add routing entry which is used to route LoadBalancer ingress IP to Antrea
+// gateway on host.
+func (c *Client) addLoadBalancerIngressIPRoute(svcIPStr string) error {
+	linkIndex := c.nodeConfig.GatewayConfig.LinkIndex
+	svcIP := net.ParseIP(svcIPStr)
+	isIPv6 := utilnet.IsIPv6(svcIP)
+	var gw net.IP
+	var mask int
+	if !isIPv6 {
+		gw = config.VirtualServiceIPv4
+		mask = ipv4AddrLength
+	} else {
+		gw = config.VirtualServiceIPv6
+		mask = ipv6AddrLength
+	}
+
+	route := generateRoute(svcIP, mask, gw, linkIndex, netlink.SCOPE_UNIVERSE)
+	if err := netlink.RouteReplace(route); err != nil {
+		return fmt.Errorf("failed to install routing entry for LoadBalancer ingress IP %s: %w", svcIP.String(), err)
+	}
+	klog.V(4).InfoS("Added LoadBalancer ingress IP route", "route", route)
+	c.serviceRoutes.Store(svcIP.String(), route)
+
+	return nil
+}
+
+// deleteLoadBalancerIngressIPRoute is used to delete routing entry which is used to route LoadBalancer ingress IP to Antrea
+// gateway on host.
+func (c *Client) deleteLoadBalancerIngressIPRoute(svcIPStr string) error {
+	linkIndex := c.nodeConfig.GatewayConfig.LinkIndex
+	svcIP := net.ParseIP(svcIPStr)
+	isIPv6 := utilnet.IsIPv6(svcIP)
+	var gw net.IP
+	var mask int
+	if !isIPv6 {
+		gw = config.VirtualServiceIPv4
+		mask = ipv4AddrLength
+	} else {
+		gw = config.VirtualServiceIPv6
+		mask = ipv6AddrLength
+	}
+
+	route := generateRoute(svcIP, mask, gw, linkIndex, netlink.SCOPE_UNIVERSE)
+	if err := netlink.RouteDel(route); err != nil {
+		return fmt.Errorf("failed to delete routing entry for LoadBalancer ingress IP %s: %w", svcIP.String(), err)
+	}
+	klog.V(4).InfoS("Deleted LoadBalancer ingress IP route", "route", route)
+	c.serviceRoutes.Delete(svcIP.String())
+
+	return nil
+}
+
+// AddLoadBalancer is used to add routing entries when a LoadBalancer Service is added.
+func (c *Client) AddLoadBalancer(externalIPs []string) error {
+	for _, svcIPStr := range externalIPs {
+		if err := c.addLoadBalancerIngressIPRoute(svcIPStr); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// DeleteLoadBalancer is used to delete routing entries when a LoadBalancer Service is deleted.
+func (c *Client) DeleteLoadBalancer(externalIPs []string) error {
+	for _, svcIPStr := range externalIPs {
+		if err := c.deleteLoadBalancerIngressIPRoute(svcIPStr); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func getTransProtocolStr(protocol binding.Protocol) string {
+	if protocol == binding.ProtocolTCP || protocol == binding.ProtocolTCPv6 {
+		return "tcp"
+	} else if protocol == binding.ProtocolUDP || protocol == binding.ProtocolUDPv6 {
+		return "udp"
+	} else if protocol == binding.ProtocolSCTP || protocol == binding.ProtocolSCTPv6 {
+		return "sctp"
+	}
+	return ""
+}
+
+func isIPv6Protocol(protocol binding.Protocol) bool {
+	if protocol == binding.ProtocolTCPv6 || protocol == binding.ProtocolUDPv6 || protocol == binding.ProtocolSCTPv6 {
+		return true
+	}
+	return false
+}
+
+func generateRoute(ip net.IP, mask int, gw net.IP, linkIndex int, scope netlink.Scope) *netlink.Route {
+	addrBits := ipv4AddrLength
+	if ip.To4() == nil {
+		addrBits = ipv6AddrLength
+	}
+
+	route := &netlink.Route{
+		Dst: &net.IPNet{
+			IP:   ip,
+			Mask: net.CIDRMask(mask, addrBits),
+		},
+		Gw:        gw,
+		Scope:     scope,
+		LinkIndex: linkIndex,
+	}
+	return route
+}
+
+func generateNeigh(ip net.IP, linkIndex int) *netlink.Neigh {
+	family := netlink.FAMILY_V4
+	if utilnet.IsIPv6(ip) {
+		family = netlink.FAMILY_V6
+	}
+	return &netlink.Neigh{
+		LinkIndex:    linkIndex,
+		Family:       family,
+		State:        netlink.NUD_PERMANENT,
+		IP:           ip,
+		HardwareAddr: globalVMAC,
+	}
+}
diff --git a/pkg/agent/route/route_windows.go b/pkg/agent/route/route_windows.go
index 668ec95c6..42813bf95 100644
--- a/pkg/agent/route/route_windows.go
+++ b/pkg/agent/route/route_windows.go
@@ -29,6 +29,7 @@ import (
 	"antrea.io/antrea/pkg/agent/config"
 	"antrea.io/antrea/pkg/agent/util"
 	"antrea.io/antrea/pkg/agent/util/winfirewall"
+	binding "antrea.io/antrea/pkg/ovs/openflow"
 )
 
 const (
@@ -44,17 +45,19 @@ type Client struct {
 	fwClient       *winfirewall.Client
 	bridgeInfIndex int
 	noSNAT         bool
+	proxyAll       bool
 }
 
 // NewClient returns a route client.
 // Todo: remove param serviceCIDR after kube-proxy is replaced by Antrea Proxy completely.
-func NewClient(serviceCIDR *net.IPNet, networkConfig *config.NetworkConfig, noSNAT bool) (*Client, error) {
+func NewClient(serviceCIDR *net.IPNet, networkConfig *config.NetworkConfig, noSNAT, proxyAll bool) (*Client, error) {
 	return &Client{
 		networkConfig: networkConfig,
 		serviceCIDR:   serviceCIDR,
 		hostRoutes:    &sync.Map{},
 		fwClient:      winfirewall.NewClient(),
 		noSNAT:        noSNAT,
+		proxyAll:      proxyAll,
 	}, nil
 }
 
@@ -246,3 +249,23 @@ func (c *Client) AddSNATRule(snatIP net.IP, mark uint32) error {
 func (c *Client) DeleteSNATRule(mark uint32) error {
 	return nil
 }
+
+func (c *Client) AddNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
+	return nil
+}
+
+func (c *Client) DeleteNodePort(nodePortAddresses []net.IP, port uint16, protocol binding.Protocol) error {
+	return nil
+}
+
+func (c *Client) AddClusterIPRoute(svcIP net.IP) error {
+	return nil
+}
+
+func (c *Client) AddLoadBalancer(externalIPs []string) error {
+	return nil
+}
+
+func (c *Client) DeleteLoadBalancer(externalIPs []string) error {
+	return nil
+}
diff --git a/pkg/agent/util/ipset/ipset.go b/pkg/agent/util/ipset/ipset.go
index 4bfd933f4..0f13104a1 100644
--- a/pkg/agent/util/ipset/ipset.go
+++ b/pkg/agent/util/ipset/ipset.go
@@ -26,8 +26,9 @@ type SetType string
 const (
 	// The hash:net set type uses a hash to store different sized IP network addresses.
 	// The lookup time grows linearly with the number of the different prefix values added to the set.
-	HashNet SetType = "hash:net"
-	HashIP  SetType = "hash:ip"
+	HashNet    SetType = "hash:net"
+	HashIP     SetType = "hash:ip"
+	HashIPPort SetType = "hash:ip,port"
 )
 
 // memberPattern is used to match the members part of ipset list result.
diff --git a/pkg/agent/util/iptables/iptables.go b/pkg/agent/util/iptables/iptables.go
index a858d974d..7d1f1ec59 100644
--- a/pkg/agent/util/iptables/iptables.go
+++ b/pkg/agent/util/iptables/iptables.go
@@ -41,6 +41,7 @@ const (
 	ConnTrackTarget  = "CT"
 	NoTrackTarget    = "NOTRACK"
 	SNATTarget       = "SNAT"
+	DNATTarget       = "DNAT"
 
 	PreRoutingChain  = "PREROUTING"
 	ForwardChain     = "FORWARD"
diff --git a/pkg/agent/util/net.go b/pkg/agent/util/net.go
index dec5cf412..6b646bc0d 100644
--- a/pkg/agent/util/net.go
+++ b/pkg/agent/util/net.go
@@ -23,6 +23,8 @@ import (
 	"net"
 	"strings"
 
+	"k8s.io/apimachinery/pkg/util/sets"
+
 	"antrea.io/antrea/pkg/util/ip"
 )
 
@@ -205,3 +207,98 @@ func GetIPWithFamily(ips []net.IP, addrFamily uint8) (net.IP, error) {
 	}
 	return nil, errors.New("no IP found with IPv4 AddressFamily")
 }
+
+// ExtendCIDRWithIP is used for extending an IPNet with an IP.
+func ExtendCIDRWithIP(ipNet *net.IPNet, ip net.IP) (*net.IPNet, error) {
+	cpl := commonPrefixLen(ipNet.IP, ip)
+	if cpl == 0 {
+		return nil, fmt.Errorf("invalid common prefix length")
+	}
+	_, newIPNet, err := net.ParseCIDR(fmt.Sprintf("%s/%d", ipNet.IP.String(), cpl))
+	if err != nil {
+		return nil, err
+	}
+	return newIPNet, nil
+}
+
+// This is copied from net/addrselect.go as this function cannot be used outside of standard lib net.
+// Modifies:
+// - Replace argument type IP with argument type net.IP.
+func commonPrefixLen(a, b net.IP) (cpl int) {
+	if a4 := a.To4(); a4 != nil {
+		a = a4
+	}
+	if b4 := b.To4(); b4 != nil {
+		b = b4
+	}
+	if len(a) != len(b) {
+		return 0
+	}
+	// If IPv6, only up to the prefix (first 64 bits)
+	if len(a) > 8 {
+		a = a[:8]
+		b = b[:8]
+	}
+	for len(a) > 0 {
+		if a[0] == b[0] {
+			cpl += 8
+			a = a[1:]
+			b = b[1:]
+			continue
+		}
+		bits := 8
+		ab, bb := a[0], b[0]
+		for {
+			ab >>= 1
+			bb >>= 1
+			bits--
+			if ab == bb {
+				cpl += bits
+				return
+			}
+		}
+	}
+	return
+}
+
+// GetAllNodeAddresses gets all Node IP addresses (not including IPv6 link local address).
+func GetAllNodeAddresses(excludeDevices []string) ([]net.IP, []net.IP, error) {
+	var nodeAddressesIPv4, nodeAddressesIPv6 []net.IP
+	_, ipv6LinkLocalNet, _ := net.ParseCIDR("fe80::/64")
+
+	// Get all interfaces.
+	interfaces, err := net.Interfaces()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Transform excludeDevices to a set
+	excludeDevicesSet := sets.NewString(excludeDevices...)
+
+	for _, itf := range interfaces {
+		// If the device is in excludeDevicesSet, skip it.
+		if excludeDevicesSet.Has(itf.Name) {
+			continue
+		}
+
+		// Get all IPs of every interface
+		addrs, err := itf.Addrs()
+		if err != nil {
+			return nil, nil, err
+		}
+
+		for _, addr := range addrs {
+			ip, _, _ := net.ParseCIDR(addr.String())
+			if ipv6LinkLocalNet.Contains(ip) {
+				continue // Skip IPv6 link local address
+			}
+
+			if ip.To4() != nil {
+				nodeAddressesIPv4 = append(nodeAddressesIPv4, ip)
+			} else {
+				nodeAddressesIPv6 = append(nodeAddressesIPv6, ip)
+			}
+		}
+	}
+	return nodeAddressesIPv4, nodeAddressesIPv6, nil
+}
diff --git a/pkg/agent/wireguard/client_linux.go b/pkg/agent/wireguard/client_linux.go
index b55ec9a74..be46ead5a 100644
--- a/pkg/agent/wireguard/client_linux.go
+++ b/pkg/agent/wireguard/client_linux.go
@@ -1,4 +1,5 @@
 //go:build linux
+// +build linux
 
 // Copyright 2021 Antrea Authors
 //
diff --git a/pkg/ovs/openflow/interfaces.go b/pkg/ovs/openflow/interfaces.go
index c7e1ec2c2..a3385752d 100644
--- a/pkg/ovs/openflow/interfaces.go
+++ b/pkg/ovs/openflow/interfaces.go
@@ -71,6 +71,10 @@ const (
 	NxmFieldIPToS       = "NXM_OF_IP_TOS"
 	NxmFieldXXReg       = "NXM_NX_XXREG"
 	NxmFieldPktMark     = "NXM_NX_PKT_MARK"
+	NxmFieldSrcIPv4     = "NXM_OF_IP_SRC"
+	NxmFieldDstIPv4     = "NXM_OF_IP_DST"
+	NxmFieldSrcIPv6     = "NXM_NX_IPV6_SRC"
+	NxmFieldDstIPv6     = "NXM_NX_IPV6_DST"
 )
 
 const (


Test patch:
diff --git a/ci/kind/test-e2e-kind.sh b/ci/kind/test-e2e-kind.sh
index 31237c267..3e0720ae6 100755
--- a/ci/kind/test-e2e-kind.sh
+++ b/ci/kind/test-e2e-kind.sh
@@ -26,6 +26,7 @@ _usage="Usage: $0 [--encap-mode <mode>] [--ip-family <v4|v6>] [--no-proxy] [--np
         --encap-mode                  Traffic encapsulation mode. (default is 'encap').
         --ip-family                   Configures the ipFamily for the KinD cluster.
         --no-proxy                    Disables Antrea proxy.
+        --proxy-all                   Enables Antrea proxy with all Service support.
         --endpointslice               Enables Antrea proxy and EndpointSlice support.
         --no-np                       Disables Antrea-native policies.
         --skip                        A comma-separated list of keywords, with which tests should be skipped.
@@ -53,6 +54,7 @@ trap "quit" INT EXIT
 mode=""
 ipfamily="v4"
 proxy=true
+proxy_all=false
 endpointslice=false
 np=true
 coverage=false
@@ -66,6 +68,10 @@ case $key in
     proxy=false
     shift
     ;;
+    --proxy-all)
+    proxy_all=true
+    shift
+    ;;
     --ip-family)
     ipfamily="$2"
     shift 2
@@ -105,6 +111,13 @@ manifest_args=""
 if ! $proxy; then
     manifest_args="$manifest_args --no-proxy"
 fi
+if $proxy_all; then
+    if ! $proxy; then
+      echoerr "--proxy-all requires AntreaProxy, so it cannot be used with --no-proxy"
+      exit 1
+    fi
+    manifest_args="$manifest_args --proxy-all"
+fi
 if $endpointslice; then
     manifest_args="$manifest_args --endpointslice"
 fi
@@ -132,6 +145,9 @@ else
     COMMON_IMAGES_LIST+=("projects.registry.vmware.com/antrea/antrea-ubuntu:latest")
     COMMON_IMAGES_LIST+=("projects.registry.vmware.com/antrea/flow-aggregator:latest")
 fi
+if $proxy_all; then
+    COMMON_IMAGES_LIST+=("k8s.gcr.io/echoserver:1.10")
+fi
 
 printf -v COMMON_IMAGES "%s " "${COMMON_IMAGES_LIST[@]}"
 
@@ -145,11 +161,13 @@ function run_test {
     echoerr "invalid value for --ip-family \"$ipfamily\", expected \"v4\" or \"v6\""
     exit 1
   fi
+  if $proxy_all; then
+    args="$args --no-kube-proxy"
+  fi
 
   echo "creating test bed with args $args"
   eval "timeout 600 $TESTBED_CMD create kind --antrea-cni false $args"
 
-
   if $coverage; then
       $YML_CMD --kind --encap-mode $current_mode $manifest_args | docker exec -i kind-control-plane dd of=/root/antrea-coverage.yml
       $YML_CMD --kind --encap-mode $current_mode --wireguard-go $manifest_args | docker exec -i kind-control-plane dd of=/root/antrea-wireguard-go-coverage.yml
@@ -159,7 +177,16 @@ function run_test {
       $YML_CMD --kind --encap-mode $current_mode --wireguard-go $manifest_args | docker exec -i kind-control-plane dd of=/root/antrea-wireguard-go.yml
       $FLOWAGGREGATOR_YML_CMD | docker exec -i kind-control-plane dd of=/root/flow-aggregator.yml
   fi
-
+  if $proxy_all; 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
+        docker exec -i kind-control-plane sed -i.bak -E "s/^[[:space:]]*#kubeAPIServerOverride[[:space:]]*:[[:space:]]*[a-z\"]+[[:space:]]*$/    kubeAPIServerOverride: \"$apiserver\"/" /root/antrea-wireguard-go-coverage.yml
+      else
+        docker exec -i kind-control-plane sed -i.bak -E "s/^[[:space:]]*#kubeAPIServerOverride[[:space:]]*:[[:space:]]*[a-z\"]+[[:space:]]*$/    kubeAPIServerOverride: \"$apiserver\"/" /root/antrea.yml
+        docker exec -i kind-control-plane sed -i.bak -E "s/^[[:space:]]*#kubeAPIServerOverride[[:space:]]*:[[:space:]]*[a-z\"]+[[:space:]]*$/    kubeAPIServerOverride: \"$apiserver\"/" /root/antrea-wireguard-go.yml
+      fi
+  fi
   sleep 1
 
   if $coverage; then
diff --git a/pkg/agent/openflow/client_test.go b/pkg/agent/openflow/client_test.go
index f0d35e1c3..5a3cecce5 100644
--- a/pkg/agent/openflow/client_test.go
+++ b/pkg/agent/openflow/client_test.go
@@ -103,7 +103,7 @@ func TestIdempotentFlowInstallation(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			defer ctrl.Finish()
 			m := oftest.NewMockOFEntryOperations(ctrl)
-			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, false, false, false)
+			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, false, false, false, false)
 			client := ofClient.(*client)
 			client.cookieAllocator = cookie.NewAllocator(0)
 			client.ofEntryOperations = m
@@ -132,7 +132,7 @@ func TestIdempotentFlowInstallation(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			defer ctrl.Finish()
 			m := oftest.NewMockOFEntryOperations(ctrl)
-			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, false, false, false)
+			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, false, false, false, false)
 			client := ofClient.(*client)
 			client.cookieAllocator = cookie.NewAllocator(0)
 			client.ofEntryOperations = m
@@ -174,7 +174,7 @@ func TestFlowInstallationFailed(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			defer ctrl.Finish()
 			m := oftest.NewMockOFEntryOperations(ctrl)
-			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, false, false, false)
+			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, false, false, false, false)
 			client := ofClient.(*client)
 			client.cookieAllocator = cookie.NewAllocator(0)
 			client.ofEntryOperations = m
@@ -209,7 +209,7 @@ func TestConcurrentFlowInstallation(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			defer ctrl.Finish()
 			m := oftest.NewMockOFEntryOperations(ctrl)
-			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, false, false, false)
+			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, false, false, false, false)
 			client := ofClient.(*client)
 			client.cookieAllocator = cookie.NewAllocator(0)
 			client.ofEntryOperations = m
@@ -400,7 +400,7 @@ func Test_client_SendTraceflowPacket(t *testing.T) {
 }
 
 func prepareTraceflowFlow(ctrl *gomock.Controller) *client {
-	ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, true, false, false)
+	ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, true, false, false, false)
 	c := ofClient.(*client)
 	c.cookieAllocator = cookie.NewAllocator(0)
 	c.nodeConfig = nodeConfig
@@ -418,7 +418,7 @@ func prepareTraceflowFlow(ctrl *gomock.Controller) *client {
 }
 
 func prepareSendTraceflowPacket(ctrl *gomock.Controller, success bool) *client {
-	ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, true, false, false)
+	ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, true, false, false, false)
 	c := ofClient.(*client)
 	c.nodeConfig = nodeConfig
 	m := ovsoftest.NewMockBridge(ctrl)
@@ -506,7 +506,7 @@ func Test_client_setBasePacketOutBuilder(t *testing.T) {
 }
 
 func prepareSetBasePacketOutBuilder(ctrl *gomock.Controller, success bool) *client {
-	ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, true, false, false)
+	ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, true, true, false, false, false)
 	c := ofClient.(*client)
 	m := ovsoftest.NewMockBridge(ctrl)
 	c.bridge = m
diff --git a/pkg/agent/openflow/network_policy_test.go b/pkg/agent/openflow/network_policy_test.go
index 9eb4d3633..4ebe9a4d8 100644
--- a/pkg/agent/openflow/network_policy_test.go
+++ b/pkg/agent/openflow/network_policy_test.go
@@ -507,7 +507,7 @@ func TestBatchInstallPolicyRuleFlows(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			defer ctrl.Finish()
 			mockOperations := oftest.NewMockOFEntryOperations(ctrl)
-			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, false, true, false, false)
+			ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, false, true, false, false, false)
 			c = ofClient.(*client)
 			c.cookieAllocator = cookie.NewAllocator(0)
 			c.ofEntryOperations = mockOperations
@@ -575,7 +575,7 @@ func BenchmarkBatchInstallPolicyRuleFlows(b *testing.B) {
 	ctrl := gomock.NewController(b)
 	defer ctrl.Finish()
 	mockOperations := oftest.NewMockOFEntryOperations(ctrl)
-	ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, false, true, false, false)
+	ofClient := NewClient(bridgeName, bridgeMgmtAddr, ovsconfig.OVSDatapathSystem, false, true, false, false, false)
 	c = ofClient.(*client)
 	c.cookieAllocator = cookie.NewAllocator(0)
 	c.ofEntryOperations = mockOperations
diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go
index abf0bfe9f..971a2129a 100644
--- a/pkg/agent/openflow/testing/mock_openflow.go
+++ b/pkg/agent/openflow/testing/mock_openflow.go
@@ -26,6 +26,7 @@ import (
 	ip "antrea.io/antrea/pkg/util/ip"
 	proxy "antrea.io/antrea/third_party/proxy"
 	gomock "github.com/golang/mock/gomock"
+	v1 "k8s.io/api/core/v1"
 	net "net"
 	reflect "reflect"
 )
@@ -293,18 +294,18 @@ func (mr *MockClientMockRecorder) InstallClusterServiceCIDRFlows(arg0 interface{
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallClusterServiceCIDRFlows", reflect.TypeOf((*MockClient)(nil).InstallClusterServiceCIDRFlows), arg0)
 }
 
-// InstallClusterServiceFlows mocks base method
-func (m *MockClient) InstallClusterServiceFlows() error {
+// InstallDefaultServiceFlows mocks base method
+func (m *MockClient) InstallDefaultServiceFlows(arg0, arg1 []net.IP) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "InstallClusterServiceFlows")
+	ret := m.ctrl.Call(m, "InstallDefaultServiceFlows", arg0, arg1)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// InstallClusterServiceFlows indicates an expected call of InstallClusterServiceFlows
-func (mr *MockClientMockRecorder) InstallClusterServiceFlows() *gomock.Call {
+// InstallDefaultServiceFlows indicates an expected call of InstallDefaultServiceFlows
+func (mr *MockClientMockRecorder) InstallDefaultServiceFlows(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallClusterServiceFlows", reflect.TypeOf((*MockClient)(nil).InstallClusterServiceFlows))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallDefaultServiceFlows", reflect.TypeOf((*MockClient)(nil).InstallDefaultServiceFlows), arg0, arg1)
 }
 
 // InstallDefaultTunnelFlows mocks base method
@@ -448,17 +449,17 @@ func (mr *MockClientMockRecorder) InstallSNATMarkFlows(arg0, arg1 interface{}) *
 }
 
 // InstallServiceFlows mocks base method
-func (m *MockClient) InstallServiceFlows(arg0 openflow.GroupIDType, arg1 net.IP, arg2 uint16, arg3 openflow.Protocol, arg4 uint16) error {
+func (m *MockClient) InstallServiceFlows(arg0 openflow.GroupIDType, arg1 net.IP, arg2 uint16, arg3 openflow.Protocol, arg4 uint16, arg5 bool, arg6 v1.ServiceType) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "InstallServiceFlows", arg0, arg1, arg2, arg3, arg4)
+	ret := m.ctrl.Call(m, "InstallServiceFlows", arg0, arg1, arg2, arg3, arg4, arg5, arg6)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // InstallServiceFlows indicates an expected call of InstallServiceFlows
-func (mr *MockClientMockRecorder) InstallServiceFlows(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+func (mr *MockClientMockRecorder) InstallServiceFlows(arg0, arg1, arg2, arg3, arg4, arg5, arg6 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallServiceFlows", reflect.TypeOf((*MockClient)(nil).InstallServiceFlows), arg0, arg1, arg2, arg3, arg4)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallServiceFlows", reflect.TypeOf((*MockClient)(nil).InstallServiceFlows), arg0, arg1, arg2, arg3, arg4, arg5, arg6)
 }
 
 // InstallServiceGroup mocks base method
diff --git a/pkg/agent/proxy/proxier_test.go b/pkg/agent/proxy/proxier_test.go
index 2af7b21d9..577f13ca8 100644
--- a/pkg/agent/proxy/proxier_test.go
+++ b/pkg/agent/proxy/proxier_test.go
@@ -34,10 +34,28 @@ import (
 	ofmock "antrea.io/antrea/pkg/agent/openflow/testing"
 	"antrea.io/antrea/pkg/agent/proxy/metrics"
 	"antrea.io/antrea/pkg/agent/proxy/types"
+	"antrea.io/antrea/pkg/agent/route"
+	routemock "antrea.io/antrea/pkg/agent/route/testing"
 	binding "antrea.io/antrea/pkg/ovs/openflow"
 	k8sproxy "antrea.io/antrea/third_party/proxy"
 )
 
+var (
+	svcIPv4          = net.ParseIP("10.20.30.41")
+	svcIPv6          = net.ParseIP("2001::10:20:30:41")
+	ep1IPv4          = net.ParseIP("10.180.0.1")
+	ep1IPv6          = net.ParseIP("2001::10:180:0:1")
+	ep2IPv4          = net.ParseIP("10.180.0.2")
+	ep2IPv6          = net.ParseIP("2001::10:180:0:2")
+	loadBalancerIPv4 = net.ParseIP("169.254.169.1")
+	loadBalancerIPv6 = net.ParseIP("fec0::169:254:169:1")
+	svcNodePortIPv4  = net.ParseIP("192.168.77.100")
+	svcNodePortIPv6  = net.ParseIP("2001::192:168:77:100")
+
+	nodePortAddressesIPv4 = []net.IP{svcNodePortIPv4}
+	nodePortAddressesIPv6 = []net.IP{svcNodePortIPv6}
+)
+
 func makeNamespaceName(namespace, name string) apimachinerytypes.NamespacedName {
 	return apimachinerytypes.NamespacedName{Namespace: namespace, Name: name}
 }
@@ -81,7 +99,7 @@ func makeTestEndpoints(namespace, name string, eptFunc func(*corev1.Endpoints))
 	return ept
 }
 
-func NewFakeProxier(ofClient openflow.Client, isIPv6 bool) *proxier {
+func NewFakeProxier(routeClient route.Interface, ofClient openflow.Client, nodePortAddresses []net.IP, isIPv6, proxyAllEnabled bool) *proxier {
 	hostname := "localhost"
 	eventBroadcaster := record.NewBroadcaster()
 	recorder := eventBroadcaster.NewRecorder(
@@ -104,8 +122,11 @@ func NewFakeProxier(ofClient openflow.Client, isIPv6 bool) *proxier {
 		endpointsMap:             types.EndpointsMap{},
 		groupCounter:             types.NewGroupCounter(isIPv6),
 		ofClient:                 ofClient,
+		routeClient:              routeClient,
 		serviceStringMap:         map[string]k8sproxy.ServicePortName{},
 		isIPv6:                   isIPv6,
+		nodePortAddresses:        nodePortAddresses,
+		proxyAll:                 proxyAllEnabled,
 	}
 	p.runner = k8sproxy.NewBoundedFrequencyRunner(componentName, p.syncProxyRules, time.Second, 30*time.Second, 2)
 	return p
@@ -115,7 +136,8 @@ func testClusterIP(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, isIPv6)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nil, isIPv6, true)
 
 	svcPort := 80
 	svcPortName := k8sproxy.ServicePortName{
@@ -149,53 +171,76 @@ func testClusterIP(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool) {
 		}),
 	)
 
-	groupID, _ := fp.groupCounter.Get(svcPortName)
+	groupID, _ := fp.groupCounter.Get(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
 	bindingProtocol := binding.ProtocolTCP
 	if isIPv6 {
 		bindingProtocol = binding.ProtocolTCPv6
 	}
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0)).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
+	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 
 	fp.syncProxyRules()
 }
 
-func TestLoadbalancer(t *testing.T) {
+func testLoadBalancer(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep2IP, loadBalancerIP net.IP, isIPv6, nodeLocalExternal bool) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, false)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nodePortAddresses, isIPv6, true)
 
-	svcIPv4 := net.ParseIP("10.20.30.41")
 	svcPort := 80
-	loadBalancerIPv4 := net.ParseIP("169.254.0.1")
+	svcNodePort := 30008
 	svcPortName := k8sproxy.ServicePortName{
 		NamespacedName: makeNamespaceName("ns1", "svc1"),
 		Port:           "80",
 		Protocol:       corev1.ProtocolTCP,
 	}
+	externalTrafficPolicy := corev1.ServiceExternalTrafficPolicyTypeCluster
+	if nodeLocalExternal {
+		externalTrafficPolicy = corev1.ServiceExternalTrafficPolicyTypeLocal
+	}
+
 	makeServiceMap(fp,
 		makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *corev1.Service) {
-			svc.Spec.ClusterIP = svcIPv4.String()
-			svc.Spec.LoadBalancerIP = loadBalancerIPv4.String()
+			svc.Spec.ClusterIP = svcIP.String()
+			svc.Spec.LoadBalancerIP = loadBalancerIP.String()
 			svc.Spec.Type = corev1.ServiceTypeLoadBalancer
-			ingress := []corev1.LoadBalancerIngress{{IP: loadBalancerIPv4.String()}}
+			ingress := []corev1.LoadBalancerIngress{{IP: loadBalancerIP.String()}}
 			svc.Status.LoadBalancer.Ingress = ingress
 			svc.Spec.Ports = []corev1.ServicePort{{
+				NodePort: int32(svcNodePort),
 				Name:     svcPortName.Port,
 				Port:     int32(svcPort),
 				Protocol: corev1.ProtocolTCP,
 			}}
+			svc.Spec.ExternalTrafficPolicy = externalTrafficPolicy
 		}),
 	)
 
-	epIP := net.ParseIP("10.180.0.1")
-	makeEndpointsMap(fp,
-		makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, func(ept *corev1.Endpoints) {
+	var eps []*corev1.Endpoints
+	epFunc := func(ept *corev1.Endpoints) {
+		ept.Subsets = []corev1.EndpointSubset{{
+			Addresses: []corev1.EndpointAddress{{
+				IP:       ep1IP.String(),
+				Hostname: "localhost",
+			}},
+			Ports: []corev1.EndpointPort{{
+				Name:     svcPortName.Port,
+				Port:     int32(svcPort),
+				Protocol: corev1.ProtocolTCP,
+			}},
+		}}
+	}
+	eps = append(eps, makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, epFunc))
+	if nodeLocalExternal {
+		epFunc = func(ept *corev1.Endpoints) {
 			ept.Subsets = []corev1.EndpointSubset{{
 				Addresses: []corev1.EndpointAddress{{
-					IP: epIP.String(),
+					IP:       ep2IP.String(),
+					Hostname: "remote",
 				}},
 				Ports: []corev1.EndpointPort{{
 					Name:     svcPortName.Port,
@@ -203,33 +248,166 @@ func TestLoadbalancer(t *testing.T) {
 					Protocol: corev1.ProtocolTCP,
 				}},
 			}}
+		}
+		eps = append(eps, makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, epFunc))
+	}
+	makeEndpointsMap(fp, eps...)
+
+	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	bindingProtocol := binding.ProtocolTCP
+	if isIPv6 {
+		bindingProtocol = binding.ProtocolTCPv6
+	}
+	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
+	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
+	if nodeLocalExternal {
+		groupID, _ = fp.groupCounter.Get(svcPortName, true)
+		mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
+	}
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, loadBalancerIP, uint16(svcPort), bindingProtocol, uint16(0), nodeLocalExternal, corev1.ServiceTypeLoadBalancer).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, gomock.Any(), uint16(svcNodePort), bindingProtocol, uint16(0), nodeLocalExternal, corev1.ServiceTypeNodePort).Times(1)
+	mockOFClient.EXPECT().InstallLoadBalancerServiceFromOutsideFlows(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
+	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
+	mockRouteClient.EXPECT().AddLoadBalancer([]string{loadBalancerIP.String()}).Times(1)
+	mockRouteClient.EXPECT().AddNodePort(nodePortAddresses, uint16(svcNodePort), bindingProtocol).Times(1)
+
+	fp.syncProxyRules()
+}
+
+func testNodePort(t *testing.T, nodePortAddresses []net.IP, svcIP, ep1IP, ep2IP net.IP, isIPv6, nodeLocalExternal bool) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	mockOFClient := ofmock.NewMockClient(ctrl)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nodePortAddresses, isIPv6, true)
+
+	svcPort := 80
+	svcNodePort := 31000
+	svcPortName := k8sproxy.ServicePortName{
+		NamespacedName: makeNamespaceName("ns1", "svc1"),
+		Port:           "80",
+		Protocol:       corev1.ProtocolTCP,
+	}
+	externalTrafficPolicy := corev1.ServiceExternalTrafficPolicyTypeCluster
+	if nodeLocalExternal {
+		externalTrafficPolicy = corev1.ServiceExternalTrafficPolicyTypeLocal
+	}
+
+	makeServiceMap(fp,
+		makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *corev1.Service) {
+			svc.Spec.ClusterIP = svcIP.String()
+			svc.Spec.Type = corev1.ServiceTypeNodePort
+			svc.Spec.Ports = []corev1.ServicePort{{
+				NodePort: int32(svcNodePort),
+				Name:     svcPortName.Port,
+				Port:     int32(svcPort),
+				Protocol: corev1.ProtocolTCP,
+			}}
+			svc.Spec.ExternalTrafficPolicy = externalTrafficPolicy
 		}),
 	)
 
-	groupID, _ := fp.groupCounter.Get(svcPortName)
+	var eps []*corev1.Endpoints
+	epFunc := func(ept *corev1.Endpoints) {
+		ept.Subsets = []corev1.EndpointSubset{{
+			Addresses: []corev1.EndpointAddress{{
+				IP:       ep1IP.String(),
+				Hostname: "localhost",
+			}},
+			Ports: []corev1.EndpointPort{{
+				Name:     svcPortName.Port,
+				Port:     int32(svcPort),
+				Protocol: corev1.ProtocolTCP,
+			}},
+		}}
+	}
+	eps = append(eps, makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, epFunc))
+	if nodeLocalExternal {
+		epFunc = func(ept *corev1.Endpoints) {
+			ept.Subsets = []corev1.EndpointSubset{{
+				Addresses: []corev1.EndpointAddress{{
+					IP:       ep2IP.String(),
+					Hostname: "remote",
+				}},
+				Ports: []corev1.EndpointPort{{
+					Name:     svcPortName.Port,
+					Port:     int32(svcPort),
+					Protocol: corev1.ProtocolTCP,
+				}},
+			}}
+		}
+		eps = append(eps, makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, epFunc))
+	}
+	makeEndpointsMap(fp, eps...)
+
+	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	bindingProtocol := binding.ProtocolTCP
+	if isIPv6 {
+		bindingProtocol = binding.ProtocolTCPv6
+	}
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().InstallEndpointFlows(binding.ProtocolTCP, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIPv4, uint16(svcPort), binding.ProtocolTCP, uint16(0)).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID, loadBalancerIPv4, uint16(svcPort), binding.ProtocolTCP, uint16(0)).Times(1)
-	mockOFClient.EXPECT().InstallLoadBalancerServiceFromOutsideFlows(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
+	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
+	if nodeLocalExternal {
+		groupID, _ = fp.groupCounter.Get(svcPortName, true)
+		mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
+	}
+
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, gomock.Any(), uint16(svcNodePort), bindingProtocol, uint16(0), nodeLocalExternal, corev1.ServiceTypeNodePort).Times(1)
+	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
+	mockRouteClient.EXPECT().AddNodePort(gomock.Any(), uint16(svcNodePort), bindingProtocol).Times(1)
 
 	fp.syncProxyRules()
 }
 
+func TestLoadBalancerIPv4(t *testing.T) {
+	testLoadBalancer(t, nodePortAddressesIPv4, svcIPv4, ep1IPv4, nil, loadBalancerIPv4, false, false)
+}
+
+func TestLoadBalancerIPv4ExternalLocal(t *testing.T) {
+	testLoadBalancer(t, nodePortAddressesIPv4, svcIPv4, ep1IPv4, ep2IPv4, loadBalancerIPv4, false, true)
+}
+
+func TestLoadBalancerIPv6(t *testing.T) {
+	testLoadBalancer(t, nodePortAddressesIPv6, svcIPv6, ep1IPv6, nil, loadBalancerIPv6, true, false)
+}
+
+func TestLoadBalancerIPv6ExternalLocal(t *testing.T) {
+	testLoadBalancer(t, nodePortAddressesIPv6, svcIPv6, ep1IPv6, ep2IPv6, loadBalancerIPv6, true, true)
+}
+
+func TestNodePortIPv4(t *testing.T) {
+	testNodePort(t, nodePortAddressesIPv4, svcIPv4, ep1IPv4, nil, false, false)
+}
+
+func TestNodePortIPv4ExternalLocal(t *testing.T) {
+	testNodePort(t, nodePortAddressesIPv4, svcIPv4, ep1IPv4, ep2IPv4, false, true)
+}
+
+func TestNodePortIPv6(t *testing.T) {
+	testNodePort(t, nodePortAddressesIPv6, svcIPv6, ep1IPv6, nil, true, false)
+}
+
+func TestNodePortIPv6ExternalLocal(t *testing.T) {
+	testNodePort(t, nodePortAddressesIPv6, svcIPv6, ep1IPv6, ep2IPv6, true, true)
+}
+
 func TestClusterIPv4(t *testing.T) {
-	testClusterIP(t, net.ParseIP("10.20.30.41"), net.ParseIP("10.180.0.1"), false)
+	testClusterIP(t, svcIPv4, ep1IPv4, false)
 }
 
 func TestClusterIPv6(t *testing.T) {
-	testClusterIP(t, net.ParseIP("10:20::41"), net.ParseIP("10:180::1"), true)
+	testClusterIP(t, svcIPv6, ep1IPv6, true)
 }
 
 func TestDualStackService(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fpv4 := NewFakeProxier(mockOFClient, false)
-	fpv6 := NewFakeProxier(mockOFClient, true)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fpv4 := NewFakeProxier(mockRouteClient, mockOFClient, nil, false, false)
+	fpv6 := NewFakeProxier(mockRouteClient, mockOFClient, nil, true, false)
 	metaProxier := k8sproxy.NewMetaProxier(fpv4, fpv6)
 
 	svcPort := 80
@@ -284,16 +462,16 @@ func TestDualStackService(t *testing.T) {
 	metaProxier.OnEndpointsUpdate(nil, epv6)
 	metaProxier.OnEndpointsSynced()
 
-	groupIDv4, _ := fpv4.groupCounter.Get(svcPortName)
-	groupIDv6, _ := fpv6.groupCounter.Get(svcPortName)
+	groupIDv4, _ := fpv4.groupCounter.Get(svcPortName, false)
+	groupIDv6, _ := fpv6.groupCounter.Get(svcPortName, false)
 
 	mockOFClient.EXPECT().InstallServiceGroup(groupIDv4, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(binding.ProtocolTCP, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupIDv4, svcIPv4, uint16(svcPort), binding.ProtocolTCP, uint16(0)).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupIDv4, svcIPv4, uint16(svcPort), binding.ProtocolTCP, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 
 	mockOFClient.EXPECT().InstallServiceGroup(groupIDv6, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(binding.ProtocolTCPv6, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupIDv6, svcIPv6, uint16(svcPort), binding.ProtocolTCPv6, uint16(0)).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupIDv6, svcIPv6, uint16(svcPort), binding.ProtocolTCPv6, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 
 	fpv4.syncProxyRules()
 	fpv6.syncProxyRules()
@@ -303,7 +481,8 @@ func testClusterIPRemoval(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool)
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, isIPv6)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nil, isIPv6, true)
 
 	svcPort := 80
 	svcPortName := k8sproxy.ServicePortName{
@@ -340,10 +519,11 @@ func testClusterIPRemoval(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool)
 	}
 	ep := makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, epFunc)
 	makeEndpointsMap(fp, ep)
-	groupID, _ := fp.groupCounter.Get(svcPortName)
+	groupID, _ := fp.groupCounter.Get(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0)).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
+	mockRouteClient.EXPECT().AddClusterIPRoute(svcIP).Times(1)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP, uint16(svcPort), bindingProtocol).Times(1)
 	mockOFClient.EXPECT().UninstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().UninstallServiceGroup(groupID).Times(1)
@@ -367,7 +547,8 @@ func testClusterIPNoEndpoint(t *testing.T, svcIP net.IP, isIPv6 bool) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, isIPv6)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nil, isIPv6, false)
 
 	svcPort := 80
 	svcNodePort := 3001
@@ -404,7 +585,8 @@ func testClusterIPRemoveSamePortEndpoint(t *testing.T, svcIP net.IP, epIP net.IP
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, isIPv6)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nil, isIPv6, false)
 
 	svcPort := 80
 	svcPortName := k8sproxy.ServicePortName{
@@ -468,14 +650,14 @@ func testClusterIPRemoveSamePortEndpoint(t *testing.T, svcIP net.IP, epIP net.IP
 	}
 	makeEndpointsMap(fp, ep, epUDP)
 
-	groupID, _ := fp.groupCounter.Get(svcPortName)
-	groupIDUDP, _ := fp.groupCounter.Get(svcPortNameUDP)
+	groupID, _ := fp.groupCounter.Get(svcPortName, false)
+	groupIDUDP, _ := fp.groupCounter.Get(svcPortNameUDP, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceGroup(groupIDUDP, false, gomock.Any()).Times(2)
 	mockOFClient.EXPECT().InstallEndpointFlows(protocolTCP, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(protocolUDP, gomock.Any()).Times(2)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), protocolTCP, uint16(0)).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupIDUDP, svcIP, uint16(svcPort), protocolUDP, uint16(0)).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), protocolTCP, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupIDUDP, svcIP, uint16(svcPort), protocolUDP, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 	mockOFClient.EXPECT().UninstallEndpointFlows(protocolUDP, gomock.Any()).Times(1)
 	fp.syncProxyRules()
 
@@ -495,7 +677,8 @@ func testClusterIPRemoveEndpoints(t *testing.T, svcIP net.IP, epIP net.IP, isIPv
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, isIPv6)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nil, isIPv6, false)
 
 	svcPort := 80
 	svcPortName := k8sproxy.ServicePortName{
@@ -531,10 +714,10 @@ func testClusterIPRemoveEndpoints(t *testing.T, svcIP net.IP, epIP net.IP, isIPv
 		bindingProtocol = binding.ProtocolTCPv6
 	}
 	makeEndpointsMap(fp, ep)
-	groupID, _ := fp.groupCounter.Get(svcPortName)
+	groupID, _ := fp.groupCounter.Get(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(2)
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(2)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0)).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 	mockOFClient.EXPECT().UninstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
 	fp.syncProxyRules()
 
@@ -554,7 +737,8 @@ func testSessionAffinityNoEndpoint(t *testing.T, svcExternalIPs net.IP, svcIP ne
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, isIPv6)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nil, isIPv6, false)
 
 	svcPort := 80
 	svcNodePort := 3001
@@ -567,7 +751,7 @@ func testSessionAffinityNoEndpoint(t *testing.T, svcExternalIPs net.IP, svcIP ne
 
 	makeServiceMap(fp,
 		makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *corev1.Service) {
-			svc.Spec.Type = "NodePort"
+			svc.Spec.Type = corev1.ServiceTypeNodePort
 			svc.Spec.ClusterIP = svcIP.String()
 			svc.Spec.ExternalIPs = []string{svcExternalIPs.String()}
 			svc.Spec.SessionAffinity = corev1.ServiceAffinityClientIP
@@ -602,10 +786,10 @@ func testSessionAffinityNoEndpoint(t *testing.T, svcExternalIPs net.IP, svcIP ne
 	if isIPv6 {
 		bindingProtocol = binding.ProtocolTCPv6
 	}
-	groupID, _ := fp.groupCounter.Get(svcPortName)
+	groupID, _ := fp.groupCounter.Get(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, true, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(corev1.DefaultClientIPServiceAffinitySeconds)).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort), bindingProtocol, uint16(corev1.DefaultClientIPServiceAffinitySeconds), false, corev1.ServiceTypeClusterIP).Times(1)
 
 	fp.syncProxyRules()
 }
@@ -622,7 +806,7 @@ func testSessionAffinity(t *testing.T, svcExternalIPs net.IP, svcIP net.IP, isIP
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, isIPv6)
+	fp := NewFakeProxier(nil, mockOFClient, nil, isIPv6, false)
 
 	svcPort := 80
 	svcNodePort := 3001
@@ -635,7 +819,7 @@ func testSessionAffinity(t *testing.T, svcExternalIPs net.IP, svcIP net.IP, isIP
 
 	makeServiceMap(fp,
 		makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *corev1.Service) {
-			svc.Spec.Type = "NodePort"
+			svc.Spec.Type = corev1.ServiceTypeNodePort
 			svc.Spec.ClusterIP = svcIP.String()
 			svc.Spec.ExternalIPs = []string{svcExternalIPs.String()}
 			svc.Spec.SessionAffinity = corev1.ServiceAffinityClientIP
@@ -669,7 +853,8 @@ func testPortChange(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, isIPv6)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nil, isIPv6, false)
 
 	svcPort1 := 80
 	svcPort2 := 8080
@@ -708,12 +893,12 @@ func testPortChange(t *testing.T, svcIP net.IP, epIP net.IP, isIPv6 bool) {
 	}
 	ep := makeTestEndpoints(svcPortName.Namespace, svcPortName.Name, epFunc)
 	makeEndpointsMap(fp, ep)
-	groupID, _ := fp.groupCounter.Get(svcPortName)
+	groupID, _ := fp.groupCounter.Get(svcPortName, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort1), bindingProtocol, uint16(0))
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort1), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP, uint16(svcPort1), bindingProtocol)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort2), bindingProtocol, uint16(0))
+	mockOFClient.EXPECT().InstallServiceFlows(groupID, svcIP, uint16(svcPort2), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP)
 
 	fp.syncProxyRules()
 
@@ -742,7 +927,8 @@ func TestServicesWithSameEndpoints(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	mockOFClient := ofmock.NewMockClient(ctrl)
-	fp := NewFakeProxier(mockOFClient, false)
+	mockRouteClient := routemock.NewMockInterface(ctrl)
+	fp := NewFakeProxier(mockRouteClient, mockOFClient, nil, false, false)
 	epIP := net.ParseIP("10.50.60.71")
 	svcIP1 := net.ParseIP("10.180.30.41")
 	svcIP2 := net.ParseIP("10.180.30.42")
@@ -793,14 +979,14 @@ func TestServicesWithSameEndpoints(t *testing.T) {
 	ep1 := epMapFactory(svcPortName1, epIP.String())
 	ep2 := epMapFactory(svcPortName2, epIP.String())
 
-	groupID1, _ := fp.groupCounter.Get(svcPortName1)
-	groupID2, _ := fp.groupCounter.Get(svcPortName2)
+	groupID1, _ := fp.groupCounter.Get(svcPortName1, false)
+	groupID2, _ := fp.groupCounter.Get(svcPortName2, false)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID1, false, gomock.Any()).Times(1)
 	mockOFClient.EXPECT().InstallServiceGroup(groupID2, false, gomock.Any()).Times(1)
 	bindingProtocol := binding.ProtocolTCP
 	mockOFClient.EXPECT().InstallEndpointFlows(bindingProtocol, gomock.Any()).Times(2)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID1, svcIP1, uint16(svcPort), bindingProtocol, uint16(0)).Times(1)
-	mockOFClient.EXPECT().InstallServiceFlows(groupID2, svcIP2, uint16(svcPort), bindingProtocol, uint16(0)).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID1, svcIP1, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
+	mockOFClient.EXPECT().InstallServiceFlows(groupID2, svcIP2, uint16(svcPort), bindingProtocol, uint16(0), false, corev1.ServiceTypeClusterIP).Times(1)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP1, uint16(svcPort), bindingProtocol).Times(1)
 	mockOFClient.EXPECT().UninstallServiceFlows(svcIP2, uint16(svcPort), bindingProtocol).Times(1)
 	mockOFClient.EXPECT().UninstallServiceGroup(groupID1).Times(1)
diff --git a/pkg/agent/route/route_windows_test.go b/pkg/agent/route/route_windows_test.go
index 0371304de..04c1253b2 100644
--- a/pkg/agent/route/route_windows_test.go
+++ b/pkg/agent/route/route_windows_test.go
@@ -51,7 +51,7 @@ func TestRouteOperation(t *testing.T) {
 	gwIP2 := net.ParseIP("192.168.3.1")
 	_, destCIDR2, _ := net.ParseCIDR(dest2)
 
-	client, err := NewClient(serviceCIDR, &config.NetworkConfig{}, true)
+	client, err := NewClient(serviceCIDR, &config.NetworkConfig{}, true, false)
 	require.Nil(t, err)
 	nodeConfig := &config.NodeConfig{
 		OVSBridge: "Loopback Pseudo-Interface 1",
diff --git a/pkg/agent/route/testing/mock_route.go b/pkg/agent/route/testing/mock_route.go
index e7287d94e..d5a31a602 100644
--- a/pkg/agent/route/testing/mock_route.go
+++ b/pkg/agent/route/testing/mock_route.go
@@ -21,6 +21,7 @@ package testing
 
 import (
 	config "antrea.io/antrea/pkg/agent/config"
+	openflow "antrea.io/antrea/pkg/ovs/openflow"
 	gomock "github.com/golang/mock/gomock"
 	net "net"
 	reflect "reflect"
@@ -49,6 +50,48 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
 	return m.recorder
 }
 
+// AddClusterIPRoute mocks base method
+func (m *MockInterface) AddClusterIPRoute(arg0 net.IP) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AddClusterIPRoute", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// AddClusterIPRoute indicates an expected call of AddClusterIPRoute
+func (mr *MockInterfaceMockRecorder) AddClusterIPRoute(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClusterIPRoute", reflect.TypeOf((*MockInterface)(nil).AddClusterIPRoute), arg0)
+}
+
+// AddLoadBalancer mocks base method
+func (m *MockInterface) AddLoadBalancer(arg0 []string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AddLoadBalancer", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// AddLoadBalancer indicates an expected call of AddLoadBalancer
+func (mr *MockInterfaceMockRecorder) AddLoadBalancer(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLoadBalancer", reflect.TypeOf((*MockInterface)(nil).AddLoadBalancer), arg0)
+}
+
+// AddNodePort mocks base method
+func (m *MockInterface) AddNodePort(arg0 []net.IP, arg1 uint16, arg2 openflow.Protocol) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "AddNodePort", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// AddNodePort indicates an expected call of AddNodePort
+func (mr *MockInterfaceMockRecorder) AddNodePort(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddNodePort", reflect.TypeOf((*MockInterface)(nil).AddNodePort), arg0, arg1, arg2)
+}
+
 // AddRoutes mocks base method
 func (m *MockInterface) AddRoutes(arg0 *net.IPNet, arg1 string, arg2, arg3 net.IP) error {
 	m.ctrl.T.Helper()
@@ -77,6 +120,34 @@ func (mr *MockInterfaceMockRecorder) AddSNATRule(arg0, arg1 interface{}) *gomock
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSNATRule", reflect.TypeOf((*MockInterface)(nil).AddSNATRule), arg0, arg1)
 }
 
+// DeleteLoadBalancer mocks base method
+func (m *MockInterface) DeleteLoadBalancer(arg0 []string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteLoadBalancer", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeleteLoadBalancer indicates an expected call of DeleteLoadBalancer
+func (mr *MockInterfaceMockRecorder) DeleteLoadBalancer(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLoadBalancer", reflect.TypeOf((*MockInterface)(nil).DeleteLoadBalancer), arg0)
+}
+
+// DeleteNodePort mocks base method
+func (m *MockInterface) DeleteNodePort(arg0 []net.IP, arg1 uint16, arg2 openflow.Protocol) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteNodePort", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeleteNodePort indicates an expected call of DeleteNodePort
+func (mr *MockInterfaceMockRecorder) DeleteNodePort(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodePort", reflect.TypeOf((*MockInterface)(nil).DeleteNodePort), arg0, arg1, arg2)
+}
+
 // DeleteRoutes mocks base method
 func (m *MockInterface) DeleteRoutes(arg0 *net.IPNet) error {
 	m.ctrl.T.Helper()
diff --git a/pkg/agent/wireguard/client_test.go b/pkg/agent/wireguard/client_test.go
index e490318f0..6fbc875f3 100644
--- a/pkg/agent/wireguard/client_test.go
+++ b/pkg/agent/wireguard/client_test.go
@@ -1,4 +1,5 @@
 //go:build linux
+// +build linux
 
 // Copyright 2021 Antrea Authors
 //
diff --git a/test/e2e/antreapolicy_test.go b/test/e2e/antreapolicy_test.go
index 72d369357..6e22210f5 100644
--- a/test/e2e/antreapolicy_test.go
+++ b/test/e2e/antreapolicy_test.go
@@ -2862,9 +2862,9 @@ func TestAntreaPolicyStatus(t *testing.T) {
 	}
 	defer teardownTest(t, data)
 
-	_, _, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "server-0", controlPlaneNodeName(), testNamespace)
+	_, _, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "server-0", controlPlaneNodeName(), testNamespace, false)
 	defer cleanupFunc()
-	_, _, cleanupFunc = createAndWaitForPod(t, data, data.createNginxPodOnNode, "server-1", workerNodeName(1), testNamespace)
+	_, _, cleanupFunc = createAndWaitForPod(t, data, data.createNginxPodOnNode, "server-1", workerNodeName(1), testNamespace, false)
 	defer cleanupFunc()
 
 	anpBuilder := &AntreaNetworkPolicySpecBuilder{}
@@ -2952,10 +2952,10 @@ func (data *TestData) waitForACNPRealized(t *testing.T, name string) error {
 // testANPNetworkPolicyStatsWithDropAction tests antreanetworkpolicystats can correctly collect dropped packets stats from ANP if
 // networkpolicystats feature is enabled
 func testANPNetworkPolicyStatsWithDropAction(t *testing.T, data *TestData) {
-	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace)
+	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace, false)
 	defer cleanupFunc()
 
-	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	defer cleanupFunc()
 	var err error
 	k8sUtils, err = NewKubernetesUtils(data)
@@ -3087,10 +3087,10 @@ func testANPNetworkPolicyStatsWithDropAction(t *testing.T, data *TestData) {
 }
 
 func testAntreaClusterNetworkPolicyStats(t *testing.T, data *TestData) {
-	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace)
+	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace, false)
 	defer cleanupFunc()
 
-	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	defer cleanupFunc()
 	var err error
 	k8sUtils, err = NewKubernetesUtils(data)
diff --git a/test/e2e/bandwidth_test.go b/test/e2e/bandwidth_test.go
index 0c82e1776..470997440 100644
--- a/test/e2e/bandwidth_test.go
+++ b/test/e2e/bandwidth_test.go
@@ -91,7 +91,7 @@ func testBenchmarkBandwidthIntraNode(t *testing.T, data *TestData) {
 }
 
 func benchmarkBandwidthService(t *testing.T, endpointNode, clientNode string, data *TestData) {
-	svc, err := data.createService("perftest-b", iperfPort, iperfPort, map[string]string{"antrea-e2e": "perftest-b"}, false, v1.ServiceTypeClusterIP, nil)
+	svc, err := data.createService("perftest-b", iperfPort, iperfPort, map[string]string{"antrea-e2e": "perftest-b"}, false, false, v1.ServiceTypeClusterIP, nil)
 	if err != nil {
 		t.Fatalf("Error when creating perftest service: %v", err)
 	}
diff --git a/test/e2e/basic_test.go b/test/e2e/basic_test.go
index b4a26f3af..d594297ab 100644
--- a/test/e2e/basic_test.go
+++ b/test/e2e/basic_test.go
@@ -62,7 +62,7 @@ func testPodAssignIP(t *testing.T, data *TestData) {
 	podName := randName("test-pod-")
 
 	t.Logf("Creating a busybox test Pod")
-	if err := data.createBusyboxPodOnNode(podName, testNamespace, ""); err != nil {
+	if err := data.createBusyboxPodOnNode(podName, testNamespace, "", false); err != nil {
 		t.Fatalf("Error when creating busybox test Pod: %v", err)
 	}
 	defer deletePodWrapper(t, data, podName)
@@ -278,7 +278,7 @@ func testIPAMRestart(t *testing.T, data *TestData) {
 
 	createPodAndGetIP := func(podName string) (*PodIPs, error) {
 		t.Logf("Creating a busybox test Pod '%s' and waiting for IP", podName)
-		if err := data.createBusyboxPodOnNode(podName, testNamespace, nodeName); err != nil {
+		if err := data.createBusyboxPodOnNode(podName, testNamespace, nodeName, false); err != nil {
 			t.Fatalf("Error when creating busybox test Pod '%s': %v", podName, err)
 			return nil, err
 		}
@@ -390,6 +390,16 @@ func testReconcileGatewayRoutesOnStartup(t *testing.T, data *TestData, isIPv6 bo
 			if route.peerPodGW = net.ParseIP(matches[2]); route.peerPodGW == nil {
 				return nil, fmt.Errorf("%s is not a valid IP", matches[2])
 			}
+			// For gateway routes, the Antrea Node controller will not clean the route whose gateway is virtual Service IP.
+			// When running all e2e tests together, AntreaProxy could be installed and uninstalled several times.
+			// After uninstalling AntreaProxy, the route which is used to route ClusterIP to gateway still exists.
+			// When installing AntreaProxy again, a new route which used to route ClusterIP to gateway will be installed.
+			// The old route will be an orphan route, but Antrea Node controller cannot clean it because the func `Reconcile`
+			// of Antrea Node controller will not clean the route whose gateway is virtual Service IP. In short, the number
+			// of gateway routes is uncertain because there may be orphan routes, so it should not be counted.
+			if route.peerPodGW.Equal(config.VirtualServiceIPv4) || route.peerPodGW.Equal(config.VirtualServiceIPv6) {
+				continue
+			}
 			routes = append(routes, route)
 		}
 		return routes, nil
@@ -718,7 +728,7 @@ func testGratuitousARP(t *testing.T, data *TestData) {
 	nodeName := workerNodeName(1)
 
 	t.Logf("Creating Pod '%s' on '%s'", podName, nodeName)
-	if err := data.createBusyboxPodOnNode(podName, testNamespace, nodeName); err != nil {
+	if err := data.createBusyboxPodOnNode(podName, testNamespace, nodeName, false); err != nil {
 		t.Fatalf("Error when creating Pod '%s': %v", podName, err)
 	}
 	defer deletePodWrapper(t, data, podName)
diff --git a/test/e2e/connectivity_test.go b/test/e2e/connectivity_test.go
index 3eecd0634..56ea8a31a 100644
--- a/test/e2e/connectivity_test.go
+++ b/test/e2e/connectivity_test.go
@@ -155,7 +155,7 @@ func (data *TestData) testHostPortPodConnectivity(t *testing.T) {
 	hpPodHostIP := hpPod.Status.HostIP
 	// Create client Pod to test connectivity.
 	clientName := randName("test-client-")
-	if err := data.createBusyboxPodOnNode(clientName, testNamespace, ""); err != nil {
+	if err := data.createBusyboxPodOnNode(clientName, testNamespace, "", false); err != nil {
 		t.Fatalf("Error when creating test client Pod: %v", err)
 	}
 	defer deletePodWrapper(t, data, clientName)
@@ -368,7 +368,7 @@ func testOVSFlowReplay(t *testing.T, data *TestData) {
 	t.Logf("Creating %d busybox test Pods on '%s'", numPods, workerNode)
 	for i := range podInfos {
 		podInfos[i].os = clusterInfo.nodesOS[workerNode]
-		if err := data.createBusyboxPodOnNode(podInfos[i].name, testNamespace, workerNode); err != nil {
+		if err := data.createBusyboxPodOnNode(podInfos[i].name, testNamespace, workerNode, false); err != nil {
 			t.Fatalf("Error when creating busybox test Pod '%s': %v", podInfos[i].name, err)
 		}
 		defer deletePodWrapper(t, data, podInfos[i].name)
diff --git a/test/e2e/egress_test.go b/test/e2e/egress_test.go
index 060a69602..274f34323 100644
--- a/test/e2e/egress_test.go
+++ b/test/e2e/egress_test.go
@@ -132,14 +132,14 @@ ip netns exec %[1]s /agnhost netexec
 
 			localPod := fmt.Sprintf("localpod%s", tt.name)
 			remotePod := fmt.Sprintf("remotepod%s", tt.name)
-			if err := data.createBusyboxPodOnNode(localPod, testNamespace, egressNode); err != nil {
+			if err := data.createBusyboxPodOnNode(localPod, testNamespace, egressNode, false); err != nil {
 				t.Fatalf("Failed to create local Pod: %v", err)
 			}
 			defer deletePodWrapper(t, data, localPod)
 			if err := data.podWaitForRunning(defaultTimeout, localPod, testNamespace); err != nil {
 				t.Fatalf("Error when waiting for Pod '%s' to be in the Running state", localPod)
 			}
-			if err := data.createBusyboxPodOnNode(remotePod, testNamespace, workerNodeName(1)); err != nil {
+			if err := data.createBusyboxPodOnNode(remotePod, testNamespace, workerNodeName(1), false); err != nil {
 				t.Fatalf("Failed to create remote Pod: %v", err)
 			}
 			defer deletePodWrapper(t, data, remotePod)
diff --git a/test/e2e/fixtures.go b/test/e2e/fixtures.go
index 6203babb2..c124eeaf4 100644
--- a/test/e2e/fixtures.go
+++ b/test/e2e/fixtures.go
@@ -425,7 +425,7 @@ func createTestBusyboxPods(tb testing.TB, data *TestData, num int, ns string, no
 	createPodAndGetIP := func() (string, *PodIPs, error) {
 		podName := randName("test-pod-")
 		tb.Logf("Creating a busybox test Pod '%s' and waiting for IP", podName)
-		if err := data.createBusyboxPodOnNode(podName, ns, nodeName); err != nil {
+		if err := data.createBusyboxPodOnNode(podName, ns, nodeName, false); err != nil {
 			tb.Errorf("Error when creating busybox test Pod '%s': %v", podName, err)
 			return "", nil, err
 		}
diff --git a/test/e2e/flowaggregator_test.go b/test/e2e/flowaggregator_test.go
index b69524d94..cecc2ba35 100644
--- a/test/e2e/flowaggregator_test.go
+++ b/test/e2e/flowaggregator_test.go
@@ -454,12 +454,12 @@ func testHelper(t *testing.T, data *TestData, podAIPs, podBIPs, podCIPs, podDIPs
 	t.Run("ToExternalFlows", func(t *testing.T) {
 		// Creating an agnhost server as a host network Pod
 		serverPodPort := int32(80)
-		_, serverIPs, cleanupFunc := createAndWaitForPod(t, data, func(name string, ns string, nodeName string) error {
+		_, serverIPs, cleanupFunc := createAndWaitForPod(t, data, func(name string, ns string, nodeName string, hostNetwork bool) error {
 			return data.createServerPod(name, testNamespace, "", serverPodPort, false, true)
-		}, "test-server-", "", testNamespace)
+		}, "test-server-", "", testNamespace, false)
 		defer cleanupFunc()
 
-		clientName, clientIPs, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", nodeName(0), testNamespace)
+		clientName, clientIPs, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", nodeName(0), testNamespace, false)
 		defer cleanupFunc()
 
 		if !isIPv6 {
@@ -1000,12 +1000,12 @@ func createPerftestServices(data *TestData, isIPv6 bool) (svcB *corev1.Service,
 		svcIPFamily = corev1.IPv6Protocol
 	}
 
-	svcB, err = data.createService("perftest-b", iperfPort, iperfPort, map[string]string{"antrea-e2e": "perftest-b"}, false, corev1.ServiceTypeClusterIP, &svcIPFamily)
+	svcB, err = data.createService("perftest-b", iperfPort, iperfPort, map[string]string{"antrea-e2e": "perftest-b"}, false, false, corev1.ServiceTypeClusterIP, &svcIPFamily)
 	if err != nil {
 		return nil, nil, fmt.Errorf("Error when creating perftest-b Service: %v", err)
 	}
 
-	svcC, err = data.createService("perftest-c", iperfPort, iperfPort, map[string]string{"antrea-e2e": "perftest-c"}, false, corev1.ServiceTypeClusterIP, &svcIPFamily)
+	svcC, err = data.createService("perftest-c", iperfPort, iperfPort, map[string]string{"antrea-e2e": "perftest-c"}, false, false, corev1.ServiceTypeClusterIP, &svcIPFamily)
 	if err != nil {
 		return nil, nil, fmt.Errorf("Error when creating perftest-c Service: %v", err)
 	}
diff --git a/test/e2e/framework.go b/test/e2e/framework.go
index 190bf29b6..e9150d6ed 100644
--- a/test/e2e/framework.go
+++ b/test/e2e/framework.go
@@ -276,6 +276,28 @@ func workerNodeName(idx int) string {
 	return node.name
 }
 
+func workerNodeIPv4(idx int) string {
+	if idx == 0 { // control-plane Node
+		return ""
+	}
+	node, ok := clusterInfo.nodes[idx]
+	if !ok {
+		return ""
+	}
+	return node.ipv4Addr
+}
+
+func workerNodeIPv6(idx int) string {
+	if idx == 0 { // control-plane Node
+		return ""
+	}
+	node, ok := clusterInfo.nodes[idx]
+	if !ok {
+		return ""
+	}
+	return node.ipv6Addr
+}
+
 // workerNodeIP returns an empty string if there is no worker Node with the provided idx
 // (including if idx is 0, which is reserved for the control-plane Node)
 func workerNodeIP(idx int) string {
@@ -1007,28 +1029,21 @@ func (data *TestData) createPodOnNodeInNamespace(name, ns string, nodeName, ctrN
 
 // createBusyboxPodOnNode creates a Pod in the test namespace with a single busybox container. The
 // Pod will be scheduled on the specified Node (if nodeName is not empty).
-func (data *TestData) createBusyboxPodOnNode(name string, ns string, nodeName string) error {
-	sleepDuration := 3600 // seconds
-	return data.createPodOnNode(name, ns, nodeName, busyboxImage, []string{"sleep", strconv.Itoa(sleepDuration)}, nil, nil, nil, false, nil)
-}
-
-// createHostNetworkBusyboxPodOnNode creates a host network Pod in the test namespace with a single busybox container.
-// The Pod will be scheduled on the specified Node (if nodeName is not empty).
-func (data *TestData) createHostNetworkBusyboxPodOnNode(name string, ns string, nodeName string) error {
+func (data *TestData) createBusyboxPodOnNode(name string, ns string, nodeName string, hostNetowrk bool) error {
 	sleepDuration := 3600 // seconds
-	return data.createPodOnNode(name, ns, nodeName, busyboxImage, []string{"sleep", strconv.Itoa(sleepDuration)}, nil, nil, nil, true, nil)
+	return data.createPodOnNode(name, ns, nodeName, busyboxImage, []string{"sleep", strconv.Itoa(sleepDuration)}, nil, nil, nil, hostNetowrk, nil)
 }
 
 // createNginxPodOnNode creates a Pod in the test namespace with a single nginx container. The
 // Pod will be scheduled on the specified Node (if nodeName is not empty).
-func (data *TestData) createNginxPodOnNode(name string, ns string, nodeName string) error {
+func (data *TestData) createNginxPodOnNode(name string, ns string, nodeName string, hostNetwork bool) error {
 	return data.createPodOnNode(name, ns, nodeName, nginxImage, []string{}, nil, nil, []corev1.ContainerPort{
 		{
 			Name:          "http",
 			ContainerPort: 80,
 			Protocol:      corev1.ProtocolTCP,
 		},
-	}, false, nil)
+	}, hostNetwork, nil)
 }
 
 // createServerPod creates a Pod that can listen to specified port and have named port set.
@@ -1376,14 +1391,14 @@ func validatePodIP(podNetworkCIDR string, ip net.IP) (bool, error) {
 }
 
 // createService creates a service with port and targetPort.
-func (data *TestData) createService(serviceName string, port, targetPort int32, selector map[string]string, affinity bool,
+func (data *TestData) createService(serviceName string, port, targetPort int32, selector map[string]string, affinity, nodeLocalExternal bool,
 	serviceType corev1.ServiceType, ipFamily *corev1.IPFamily) (*corev1.Service, error) {
 	annotation := make(map[string]string)
-	return data.createServiceWithAnnotations(serviceName, port, targetPort, selector, affinity, serviceType, ipFamily, annotation)
+	return data.createServiceWithAnnotations(serviceName, port, targetPort, selector, affinity, nodeLocalExternal, serviceType, ipFamily, annotation)
 }
 
 // createService creates a service with Annotation
-func (data *TestData) createServiceWithAnnotations(serviceName string, port, targetPort int32, selector map[string]string, affinity bool,
+func (data *TestData) createServiceWithAnnotations(serviceName string, port, targetPort int32, selector map[string]string, affinity, nodeLocalExternal bool,
 	serviceType corev1.ServiceType, ipFamily *corev1.IPFamily, annotations map[string]string) (*corev1.Service, error) {
 	affinityType := corev1.ServiceAffinityNone
 	var ipFamilies []corev1.IPFamily
@@ -1414,24 +1429,51 @@ func (data *TestData) createServiceWithAnnotations(serviceName string, port, tar
 			IPFamilies: ipFamilies,
 		},
 	}
+	if (serviceType == corev1.ServiceTypeNodePort || serviceType == corev1.ServiceTypeLoadBalancer) && nodeLocalExternal {
+		service.Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicyTypeLocal
+	}
 	return data.clientset.CoreV1().Services(testNamespace).Create(context.TODO(), &service, metav1.CreateOptions{})
 }
 
 // createNginxClusterIPServiceWithAnnotations creates nginx service with Annotation
 func (data *TestData) createNginxClusterIPServiceWithAnnotations(affinity bool, ipFamily *corev1.IPFamily, annotation map[string]string) (*corev1.Service, error) {
-	return data.createServiceWithAnnotations("nginx", 80, 80, map[string]string{"app": "nginx"}, affinity, corev1.ServiceTypeClusterIP, ipFamily, annotation)
+	return data.createServiceWithAnnotations("nginx", 80, 80, map[string]string{"app": "nginx"}, affinity, false, corev1.ServiceTypeClusterIP, ipFamily, annotation)
 }
 
-// createNginxClusterIPService create a nginx service with the given name.
+// createNginxClusterIPService creates a nginx service with the given name.
 func (data *TestData) createNginxClusterIPService(name string, affinity bool, ipFamily *corev1.IPFamily) (*corev1.Service, error) {
 	if name == "" {
 		name = "nginx"
 	}
-	return data.createService(name, 80, 80, map[string]string{"app": "nginx"}, affinity, corev1.ServiceTypeClusterIP, ipFamily)
+	return data.createService(name, 80, 80, map[string]string{"app": "nginx"}, affinity, false, corev1.ServiceTypeClusterIP, ipFamily)
+}
+
+// createAgnhostNodePortService creates a NodePort agnhost service with the given name.
+func (data *TestData) createAgnhostNodePortService(serviceName string, affinity, nodeLocalExternal bool, ipFamily *corev1.IPFamily) (*corev1.Service, error) {
+	return data.createService(serviceName, 8080, 8080, map[string]string{"app": "agnhost"}, affinity, nodeLocalExternal, corev1.ServiceTypeNodePort, ipFamily)
+}
+
+// createAgnhostLoadBalancerService creates a LoadBalancer agnhost service with the given name.
+func (data *TestData) createAgnhostLoadBalancerService(serviceName string, affinity, nodeLocalExternal bool, ingressIPs []string, ipFamily *corev1.IPFamily) (*corev1.Service, error) {
+	svc, err := data.createService(serviceName, 8080, 8080, map[string]string{"app": "agnhost"}, affinity, nodeLocalExternal, corev1.ServiceTypeLoadBalancer, ipFamily)
+	if err != nil {
+		return svc, err
+	}
+	ingress := make([]corev1.LoadBalancerIngress, len(ingressIPs))
+	for idx, ingressIP := range ingressIPs {
+		ingress[idx].IP = ingressIP
+	}
+	updatedSvc := svc.DeepCopy()
+	updatedSvc.Status.LoadBalancer.Ingress = ingress
+	patchData, err := json.Marshal(updatedSvc)
+	if err != nil {
+		return svc, err
+	}
+	return data.clientset.CoreV1().Services(svc.Namespace).Patch(context.TODO(), svc.Name, types.MergePatchType, patchData, metav1.PatchOptions{}, "status")
 }
 
 func (data *TestData) createNginxLoadBalancerService(affinity bool, ingressIPs []string, ipFamily *corev1.IPFamily) (*corev1.Service, error) {
-	svc, err := data.createService(nginxLBService, 80, 80, map[string]string{"app": "nginx"}, affinity, corev1.ServiceTypeLoadBalancer, ipFamily)
+	svc, err := data.createService(nginxLBService, 80, 80, map[string]string{"app": "nginx"}, affinity, false, corev1.ServiceTypeLoadBalancer, ipFamily)
 	if err != nil {
 		return svc, err
 	}
@@ -1685,6 +1727,20 @@ func (data *TestData) GetEncapMode() (config.TrafficEncapModeType, error) {
 	return config.TrafficEncapModeEncap, nil
 }
 
+func (data *TestData) isProxyAll() (bool, error) {
+	configMap, err := data.GetAntreaConfigMap(antreaNamespace)
+	if err != nil {
+		return false, fmt.Errorf("failed to get Antrea ConfigMap: %v", err)
+	}
+	for _, antreaConfig := range configMap.Data {
+		searchStr := "proxyAll: true"
+		if strings.Index(strings.ToLower(antreaConfig), strings.ToLower(searchStr)) != -1 {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
 func getFeatures(confName string) (featuregate.FeatureGate, error) {
 	featureGate := features.DefaultMutableFeatureGate.DeepCopy()
 	var cfg interface{}
diff --git a/test/e2e/legacyantreapolicy_test.go b/test/e2e/legacyantreapolicy_test.go
index 63ccf305b..dfb48d7bf 100644
--- a/test/e2e/legacyantreapolicy_test.go
+++ b/test/e2e/legacyantreapolicy_test.go
@@ -2170,9 +2170,9 @@ func TestLegacyAntreaPolicyStatus(t *testing.T) {
 	}
 	defer teardownTest(t, data)
 
-	_, _, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "server-0", controlPlaneNodeName(), testNamespace)
+	_, _, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "server-0", controlPlaneNodeName(), testNamespace, false)
 	defer cleanupFunc()
-	_, _, cleanupFunc = createAndWaitForPod(t, data, data.createNginxPodOnNode, "server-1", workerNodeName(1), testNamespace)
+	_, _, cleanupFunc = createAndWaitForPod(t, data, data.createNginxPodOnNode, "server-1", workerNodeName(1), testNamespace, false)
 	defer cleanupFunc()
 
 	anpBuilder := &AntreaNetworkPolicySpecBuilder{}
@@ -2226,10 +2226,10 @@ func TestLegacyAntreaPolicyStatus(t *testing.T) {
 // testLegacyANPNetworkPolicyStatsWithDropAction tests antreanetworkpolicystats can correctly collect dropped packets stats from ANP if
 // networkpolicystats feature is enabled
 func testLegacyANPNetworkPolicyStatsWithDropAction(t *testing.T, data *TestData) {
-	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace)
+	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace, false)
 	defer cleanupFunc()
 
-	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	defer cleanupFunc()
 	var err error
 	k8sUtils, err = NewKubernetesUtils(data)
@@ -2361,10 +2361,10 @@ func testLegacyANPNetworkPolicyStatsWithDropAction(t *testing.T, data *TestData)
 }
 
 func testLegacyAntreaClusterNetworkPolicyStats(t *testing.T, data *TestData) {
-	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace)
+	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace, false)
 	defer cleanupFunc()
 
-	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	defer cleanupFunc()
 	var err error
 	k8sUtils, err = NewKubernetesUtils(data)
diff --git a/test/e2e/networkpolicy_test.go b/test/e2e/networkpolicy_test.go
index c465d8b33..06a4ee699 100644
--- a/test/e2e/networkpolicy_test.go
+++ b/test/e2e/networkpolicy_test.go
@@ -84,10 +84,10 @@ func TestNetworkPolicy(t *testing.T) {
 }
 
 func testNetworkPolicyStats(t *testing.T, data *TestData) {
-	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace)
+	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace, false)
 	defer cleanupFunc()
 
-	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	defer cleanupFunc()
 
 	// When using the userspace OVS datapath and tunneling,
@@ -227,21 +227,21 @@ func (data *TestData) setupDifferentNamedPorts(t *testing.T) (checkFn func(), cl
 	}()
 
 	server0Port := int32(80)
-	server0Name, server0IPs, cleanupFunc := createAndWaitForPod(t, data, func(name string, ns string, nodeName string) error {
+	server0Name, server0IPs, cleanupFunc := createAndWaitForPod(t, data, func(name string, ns string, nodeName string, hostNetwork bool) error {
 		return data.createServerPod(name, testNamespace, "http", server0Port, false, false)
-	}, "test-server-", "", testNamespace)
+	}, "test-server-", "", testNamespace, false)
 	cleanupFuncs = append(cleanupFuncs, cleanupFunc)
 
 	server1Port := int32(8080)
-	server1Name, server1IPs, cleanupFunc := createAndWaitForPod(t, data, func(name string, ns string, nodeName string) error {
+	server1Name, server1IPs, cleanupFunc := createAndWaitForPod(t, data, func(name string, ns string, nodeName string, hostNetwork bool) error {
 		return data.createServerPod(name, testNamespace, "http", server1Port, false, false)
-	}, "test-server-", "", testNamespace)
+	}, "test-server-", "", testNamespace, false)
 	cleanupFuncs = append(cleanupFuncs, cleanupFunc)
 
-	client0Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	client0Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	cleanupFuncs = append(cleanupFuncs, cleanupFunc)
 
-	client1Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	client1Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	cleanupFuncs = append(cleanupFuncs, cleanupFunc)
 
 	preCheckFunc := func(server0IP, server1IP string) {
@@ -346,21 +346,21 @@ func testDefaultDenyIngressPolicy(t *testing.T, data *TestData) {
 	serverNode := workerNodeName(1)
 	serverNodeIP := workerNodeIP(1)
 	serverPort := int32(80)
-	_, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", serverNode, testNamespace)
+	_, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", serverNode, testNamespace, false)
 	defer cleanupFunc()
 
-	service, err := data.createService("nginx", serverPort, serverPort, map[string]string{"app": "nginx"}, false, corev1.ServiceTypeNodePort, nil)
+	service, err := data.createService("nginx", serverPort, serverPort, map[string]string{"app": "nginx"}, false, false, corev1.ServiceTypeNodePort, nil)
 	if err != nil {
 		t.Fatalf("Error when creating nginx NodePort service: %v", err)
 	}
 	defer data.deleteService(service.Name)
 
 	// client1 is a host network Pod and is on the same node as the server Pod, simulating kubelet probe traffic.
-	client1Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createHostNetworkBusyboxPodOnNode, "test-hostnetwork-client-can-connect-", serverNode, testNamespace)
+	client1Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-hostnetwork-client-can-connect-", serverNode, testNamespace, true)
 	defer cleanupFunc()
 
 	// client2 is a host network Pod and is on a different node from the server Pod, accessing the server Pod via the NodePort service.
-	client2Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createHostNetworkBusyboxPodOnNode, "test-hostnetwork-client-cannot-connect-", controlPlaneNodeName(), testNamespace)
+	client2Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-hostnetwork-client-cannot-connect-", controlPlaneNodeName(), testNamespace, true)
 	defer cleanupFunc()
 
 	spec := &networkingv1.NetworkPolicySpec{
@@ -409,10 +409,10 @@ func testDefaultDenyIngressPolicy(t *testing.T, data *TestData) {
 
 func testDefaultDenyEgressPolicy(t *testing.T, data *TestData) {
 	serverPort := int32(80)
-	_, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace)
+	_, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace, false)
 	defer cleanupFunc()
 
-	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	defer cleanupFunc()
 
 	preCheckFunc := func(serverIP string) {
@@ -463,12 +463,12 @@ func testDefaultDenyEgressPolicy(t *testing.T, data *TestData) {
 // https://github.com/kubernetes/kubernetes/pull/93583
 func testEgressToServerInCIDRBlock(t *testing.T, data *TestData) {
 	workerNode := workerNodeName(1)
-	serverAName, serverAIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace)
+	serverAName, serverAIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace, false)
 	defer cleanupFunc()
-	serverBName, serverBIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace)
+	serverBName, serverBIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace, false)
 	defer cleanupFunc()
 
-	clientA, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", workerNode, testNamespace)
+	clientA, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", workerNode, testNamespace, false)
 	defer cleanupFunc()
 	var serverCIDR string
 	var serverAIP, serverBIP string
@@ -530,10 +530,10 @@ func testEgressToServerInCIDRBlock(t *testing.T, data *TestData) {
 // https://github.com/kubernetes/kubernetes/pull/93583
 func testEgressToServerInCIDRBlockWithException(t *testing.T, data *TestData) {
 	workerNode := workerNodeName(1)
-	serverAName, serverAIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace)
+	serverAName, serverAIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace, false)
 	defer cleanupFunc()
 
-	clientA, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", workerNode, testNamespace)
+	clientA, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", workerNode, testNamespace, false)
 	defer cleanupFunc()
 	var serverAAllowCIDR string
 	var serverAExceptList []string
@@ -595,16 +595,16 @@ func testNetworkPolicyResyncAfterRestart(t *testing.T, data *TestData) {
 		t.Fatalf("Error when getting antrea-agent pod name: %v", err)
 	}
 
-	server0Name, server0IPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace)
+	server0Name, server0IPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace, false)
 	defer cleanupFunc()
 
-	server1Name, server1IPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace)
+	server1Name, server1IPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", workerNode, testNamespace, false)
 	defer cleanupFunc()
 
-	client0Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", workerNode, testNamespace)
+	client0Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", workerNode, testNamespace, false)
 	defer cleanupFunc()
 
-	client1Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", workerNode, testNamespace)
+	client1Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", workerNode, testNamespace, false)
 	defer cleanupFunc()
 
 	netpol0, err := data.createNetworkPolicy("test-isolate-server0", &networkingv1.NetworkPolicySpec{
@@ -703,13 +703,13 @@ func testNetworkPolicyResyncAfterRestart(t *testing.T, data *TestData) {
 
 func testIngressPolicyWithoutPortNumber(t *testing.T, data *TestData) {
 	serverPort := int32(80)
-	_, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace)
+	_, serverIPs, cleanupFunc := createAndWaitForPod(t, data, data.createNginxPodOnNode, "test-server-", "", testNamespace, false)
 	defer cleanupFunc()
 
-	client0Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	client0Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	defer cleanupFunc()
 
-	client1Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	client1Name, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	defer cleanupFunc()
 
 	preCheckFunc := func(serverIP string) {
@@ -811,7 +811,7 @@ func testIngressPolicyWithEndPort(t *testing.T, data *TestData) {
 	// createAgnhostPodOnNodeWithMultiPort creates a Pod in the test namespace with
 	// multiple agnhost containers listening on multiple ports.
 	// The Pod will be scheduled on the specified Node (if nodeName is not empty).
-	createAgnhostPodOnNodeWithMultiPort := func(name string, ns string, nodeName string) error {
+	createAgnhostPodOnNodeWithMultiPort := func(name string, ns string, nodeName string, hostNetwork bool) error {
 		var containers []corev1.Container
 		for _, port := range serverPorts {
 			containers = append(containers, makeContainerSpec(port))
@@ -819,7 +819,7 @@ func testIngressPolicyWithEndPort(t *testing.T, data *TestData) {
 		podSpec := corev1.PodSpec{
 			Containers:    containers,
 			RestartPolicy: corev1.RestartPolicyNever,
-			HostNetwork:   false,
+			HostNetwork:   hostNetwork,
 		}
 		if nodeName != "" {
 			podSpec.NodeSelector = map[string]string{
@@ -847,10 +847,10 @@ func testIngressPolicyWithEndPort(t *testing.T, data *TestData) {
 		return nil
 	}
 
-	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, createAgnhostPodOnNodeWithMultiPort, "test-server-", "", testNamespace)
+	serverName, serverIPs, cleanupFunc := createAndWaitForPod(t, data, createAgnhostPodOnNodeWithMultiPort, "test-server-", "", testNamespace, false)
 	defer cleanupFunc()
 
-	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace)
+	clientName, _, cleanupFunc := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, "test-client-", "", testNamespace, false)
 	defer cleanupFunc()
 
 	preCheck := func(serverIP string) {
@@ -932,9 +932,9 @@ func testIngressPolicyWithEndPort(t *testing.T, data *TestData) {
 	}
 }
 
-func createAndWaitForPod(t *testing.T, data *TestData, createFunc func(name string, ns string, nodeName string) error, namePrefix string, nodeName string, ns string) (string, *PodIPs, func()) {
+func createAndWaitForPod(t *testing.T, data *TestData, createFunc func(name string, ns string, nodeName string, hostNetwork bool) error, namePrefix string, nodeName string, ns string, hostNetwork bool) (string, *PodIPs, func()) {
 	name := randName(namePrefix)
-	if err := createFunc(name, ns, nodeName); err != nil {
+	if err := createFunc(name, ns, nodeName, hostNetwork); err != nil {
 		t.Fatalf("Error when creating busybox test Pod: %v", err)
 	}
 	cleanupFunc := func() {
diff --git a/test/e2e/nodeportlocal_test.go b/test/e2e/nodeportlocal_test.go
index 1e6cd3280..94bb2b238 100644
--- a/test/e2e/nodeportlocal_test.go
+++ b/test/e2e/nodeportlocal_test.go
@@ -257,12 +257,12 @@ func NPLTestMultiplePods(t *testing.T) {
 	for i := 0; i < 4; i++ {
 		testPodName := randName("test-pod-")
 		testPods = append(testPods, testPodName)
-		err := testData.createNginxPodOnNode(testPodName, testNamespace, node)
+		err := testData.createNginxPodOnNode(testPodName, testNamespace, node, false)
 		r.NoError(err, "Error creating test Pod: %v", err)
 	}
 
 	clientName := randName("test-client-")
-	err := testData.createBusyboxPodOnNode(clientName, testNamespace, node)
+	err := testData.createBusyboxPodOnNode(clientName, testNamespace, node, false)
 	r.NoError(err, "Error creating Pod %s: %v", clientName)
 
 	err = testData.podWaitForRunning(defaultTimeout, clientName, testNamespace)
@@ -296,8 +296,8 @@ func NPLTestPodAddMultiPort(t *testing.T) {
 	selector := make(map[string]string)
 	selector["app"] = "agnhost"
 	ipFamily := corev1.IPv4Protocol
-	testData.createServiceWithAnnotations("agnhost1", 80, 80, selector, false, corev1.ServiceTypeClusterIP, &ipFamily, annotation)
-	testData.createServiceWithAnnotations("agnhost2", 80, 8080, selector, false, corev1.ServiceTypeClusterIP, &ipFamily, annotation)
+	testData.createServiceWithAnnotations("agnhost1", 80, 80, selector, false, false, corev1.ServiceTypeClusterIP, &ipFamily, annotation)
+	testData.createServiceWithAnnotations("agnhost2", 80, 8080, selector, false, false, corev1.ServiceTypeClusterIP, &ipFamily, annotation)
 	targetPorts := sets.NewInt(80, 8080)
 
 	podcmd := "porter"
@@ -328,7 +328,7 @@ func NPLTestPodAddMultiPort(t *testing.T) {
 	nplAnnotations, testPodIP := getNPLAnnotations(t, testData, r, testPodName)
 
 	clientName := randName("test-client-")
-	err = testData.createBusyboxPodOnNode(clientName, testNamespace, node)
+	err = testData.createBusyboxPodOnNode(clientName, testNamespace, node, false)
 	r.NoError(err, "Error when creating Pod %s", clientName)
 
 	err = testData.podWaitForRunning(defaultTimeout, clientName, testNamespace)
@@ -359,11 +359,11 @@ func NPLTestLocalAccess(t *testing.T) {
 	node := nodeName(0)
 
 	testPodName := randName("test-pod-")
-	err := testData.createNginxPodOnNode(testPodName, testNamespace, node)
+	err := testData.createNginxPodOnNode(testPodName, testNamespace, node, false)
 	r.NoError(err, "Error creating test Pod: %v", err)
 
 	clientName := randName("test-client-")
-	err = testData.createHostNetworkBusyboxPodOnNode(clientName, testNamespace, node)
+	err = testData.createBusyboxPodOnNode(clientName, testNamespace, node, true)
 	r.NoError(err, "Error creating hostNetwork Pod %s: %v", clientName)
 
 	err = testData.podWaitForRunning(defaultTimeout, clientName, testNamespace)
@@ -402,12 +402,12 @@ func testNPLMultiplePodsAgentRestart(t *testing.T, data *TestData) {
 	for i := 0; i < 4; i++ {
 		testPodName := randName("test-pod-")
 		testPods = append(testPods, testPodName)
-		err = data.createNginxPodOnNode(testPodName, testNamespace, node)
+		err = data.createNginxPodOnNode(testPodName, testNamespace, node, false)
 		r.NoError(err, "Error creating test Pod: %v", err)
 	}
 
 	clientName := randName("test-client-")
-	err = data.createBusyboxPodOnNode(clientName, testNamespace, node)
+	err = data.createBusyboxPodOnNode(clientName, testNamespace, node, false)
 	r.NoError(err, "Error when creating Pod %s", clientName)
 
 	err = data.podWaitForRunning(defaultTimeout, clientName, testNamespace)
@@ -466,12 +466,12 @@ func testNPLChangePortRangeAgentRestart(t *testing.T, data *TestData) {
 	for i := 0; i < 4; i++ {
 		testPodName := randName("test-pod-")
 		testPods = append(testPods, testPodName)
-		err = data.createNginxPodOnNode(testPodName, testNamespace, node)
+		err = data.createNginxPodOnNode(testPodName, testNamespace, node, false)
 		r.NoError(err, "Error Creating test Pod: %v", err)
 	}
 
 	clientName := randName("test-client-")
-	err = data.createBusyboxPodOnNode(clientName, testNamespace, node)
+	err = data.createBusyboxPodOnNode(clientName, testNamespace, node, false)
 	r.NoError(err, "Error when creating Pod %s", clientName)
 
 	err = data.podWaitForRunning(defaultTimeout, clientName, testNamespace)
diff --git a/test/e2e/proxy_test.go b/test/e2e/proxy_test.go
index e8e180ad4..311410df3 100644
--- a/test/e2e/proxy_test.go
+++ b/test/e2e/proxy_test.go
@@ -15,6 +15,7 @@
 package e2e
 
 import (
+	"context"
 	"encoding/hex"
 	"fmt"
 	"net"
@@ -68,7 +69,404 @@ func skipIfProxyDisabled(t *testing.T) {
 func testProxyServiceSessionAffinityCase(t *testing.T, data *TestData) {
 	if len(clusterInfo.podV4NetworkCIDR) != 0 {
 		ipFamily := corev1.IPv4Protocol
-		testProxyServiceSessionAffinity(&ipFamily, []string{"169.254.169.253", "169.254.169.254"}, data, t)
+		testProxyServiceSessionAffinity(&ipFamily, []string{"169.254.169.1", "169.254.169.2"}, data, t)
+	}
+	if len(clusterInfo.podV6NetworkCIDR) != 0 {
+		ipFamily := corev1.IPv6Protocol
+		testProxyServiceSessionAffinity(&ipFamily, []string{"fd75::aabb:ccdd:ef00", "fd75::aabb:ccdd:ef01"}, data, t)
+	}
+}
+
+func skipIfProxyAllDisabled(t *testing.T, data *TestData) {
+	isProxyAll, err := data.isProxyAll()
+	if err != nil {
+		t.Fatalf("Error getting option antreaProxy.proxyAll value")
+	}
+	if !isProxyAll {
+		t.Skipf("Skipping test because option antreaProxy.proxyAll is not enabled")
+	}
+}
+
+func skipIfKubeProxyEnabled(t *testing.T, data *TestData) {
+	_, err := data.clientset.AppsV1().DaemonSets(kubeNamespace).Get(context.TODO(), "kube-proxy", metav1.GetOptions{})
+	if err == nil {
+		t.Skipf("Skipping test because kube-proxy is running")
+	}
+}
+
+func probeFromNode(node string, url string) error {
+	_, _, _, err := RunCommandOnNode(node, fmt.Sprintf("curl --connect-timeout 1 --retry 5 --retry-connrefused %s", url))
+	return err
+}
+
+func probeHostnameFromNode(node string, baseUrl string) (string, error) {
+	url := fmt.Sprintf("%s/%s", baseUrl, "hostname")
+	_, hostname, _, err := RunCommandOnNode(node, fmt.Sprintf("curl --connect-timeout 1 --retry 5 --retry-connrefused %s", url))
+	return hostname, err
+}
+
+func probeClientIPFromNode(node string, baseUrl string) (string, error) {
+	url := fmt.Sprintf("%s/%s", baseUrl, "clientip")
+	_, hostPort, _, err := RunCommandOnNode(node, fmt.Sprintf("curl --connect-timeout 1 --retry 5 --retry-connrefused %s", url))
+	if err != nil {
+		return "", err
+	}
+	host, _, err := net.SplitHostPort(hostPort)
+	return host, err
+}
+
+func probeFromPod(data *TestData, pod string, url string) error {
+	_, _, err := data.runCommandFromPod(testNamespace, pod, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1"})
+	return err
+}
+
+func probeHostnameFromPod(data *TestData, pod string, baseUrl string) (string, error) {
+	url := fmt.Sprintf("%s/%s", baseUrl, "hostname")
+	hostname, _, err := data.runCommandFromPod(testNamespace, pod, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1"})
+	return hostname, err
+}
+
+func probeClientIPFromPod(data *TestData, pod string, baseUrl string) (string, error) {
+	url := fmt.Sprintf("%s/%s", baseUrl, "clientip")
+	hostPort, _, err := data.runCommandFromPod(testNamespace, pod, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1"})
+	if err != nil {
+		return "", err
+	}
+	host, _, err := net.SplitHostPort(hostPort)
+	return host, err
+}
+
+func TestProxyLoadBalancerServiceIPv4(t *testing.T) {
+	skipIfNotIPv4Cluster(t)
+	testProxyLoadBalancerService(t, false)
+}
+
+func TestProxyLoadBalancerServiceIPv6(t *testing.T) {
+	skipIfNotIPv6Cluster(t)
+	testProxyLoadBalancerService(t, true)
+}
+
+func testProxyLoadBalancerService(t *testing.T, isIPv6 bool) {
+	skipIfProxyDisabled(t)
+	skipIfHasWindowsNodes(t)
+	skipIfNumNodesLessThan(t, 2)
+	data, err := setupTest(t)
+	if err != nil {
+		t.Fatalf("Error when setting up test: %v", err)
+	}
+	defer teardownTest(t, data)
+	skipIfProxyAllDisabled(t, data)
+
+	// Create a busybox Pod on every Node. The busybox Pod is used as a client.
+	nodes := []string{nodeName(0), nodeName(1)}
+	var busyboxes, busyboxIPs []string
+	for idx, node := range nodes {
+		podName, ips, _ := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, fmt.Sprintf("busybox-%d-", idx), node, testNamespace, false)
+		busyboxes = append(busyboxes, podName)
+		if !isIPv6 {
+			busyboxIPs = append(busyboxIPs, ips.ipv4.String())
+		} else {
+			busyboxIPs = append(busyboxIPs, ips.ipv6.String())
+		}
+	}
+
+	clusterIngressIP := []string{"169.254.169.1"}
+	localIngressIP := []string{"169.254.169.2"}
+	ipProtocol := corev1.IPv4Protocol
+	if isIPv6 {
+		ipProtocol = corev1.IPv6Protocol
+		clusterIngressIP = []string{"fd75::aabb:ccdd:ef00"}
+		localIngressIP = []string{"fd75::aabb:ccdd:ef01"}
+	}
+
+	// Create two LoadBalancer Services. The externalTrafficPolicy of one Service is Cluster, and the externalTrafficPolicy
+	// of another one is Local.
+	_, err = data.createAgnhostLoadBalancerService("agnhost-cluster", true, false, clusterIngressIP, &ipProtocol)
+	require.NoError(t, err)
+	_, err = data.createAgnhostLoadBalancerService("agnhost-local", true, true, localIngressIP, &ipProtocol)
+	require.NoError(t, err)
+
+	port := "8080"
+	clusterUrl := net.JoinHostPort(clusterIngressIP[0], port)
+	localUrl := net.JoinHostPort(localIngressIP[0], port)
+
+	// Create agnhost Pods which are not on host network.
+	agnhosts := []string{"agnhost-0", "agnhost-1"}
+	for idx, node := range nodes {
+		createAgnhostPod(t, data, agnhosts[idx], node, false)
+	}
+	t.Run("Non-HostNetwork Endpoints", func(t *testing.T) {
+		loadBalancerTestCases(t, data, clusterUrl, localUrl, nodes, busyboxes, busyboxIPs, agnhosts)
+	})
+
+	// Delete agnhost Pods which are not on host network and create new agnhost Pods which are on host network.
+	hostAgnhosts := []string{"agnhost-host-0", "agnhost-host-1"}
+	for idx, node := range nodes {
+		require.NoError(t, data.deletePod(testNamespace, agnhosts[idx]))
+		createAgnhostPod(t, data, hostAgnhosts[idx], node, true)
+	}
+	t.Run("HostNetwork Endpoints", func(t *testing.T) {
+		loadBalancerTestCases(t, data, clusterUrl, localUrl, nodes, busyboxes, busyboxIPs, nodes)
+	})
+}
+
+func loadBalancerTestCases(t *testing.T, data *TestData, clusterUrl, localUrl string, nodes, pods, podIPs, hostnames []string) {
+	t.Run("ExternalTrafficPolicy:Cluster/Client:Node", func(t *testing.T) {
+		testLoadBalancerClusterFromNode(t, data, nodes, clusterUrl)
+	})
+	t.Run("ExternalTrafficPolicy:Cluster/Client:Pod", func(t *testing.T) {
+		testLoadBalancerClusterFromPod(t, data, pods, clusterUrl)
+	})
+	t.Run("ExternalTrafficPolicy:Local/Client:Node", func(t *testing.T) {
+		testLoadBalancerLocalFromNode(t, data, nodes, localUrl, hostnames)
+	})
+	t.Run("ExternalTrafficPolicy:Local/Client:Pod", func(t *testing.T) {
+		testLoadBalancerLocalFromPod(t, data, pods, localUrl, podIPs, hostnames)
+	})
+}
+
+func testLoadBalancerClusterFromNode(t *testing.T, data *TestData, nodes []string, url string) {
+	skipIfKubeProxyEnabled(t, data)
+	for _, node := range nodes {
+		require.NoError(t, probeFromNode(node, url), "Service LoadBalancer whose externalTrafficPolicy is Cluster should be able to be connected from Node")
+	}
+}
+
+func testLoadBalancerClusterFromPod(t *testing.T, data *TestData, pods []string, url string) {
+	for _, pod := range pods {
+		require.NoError(t, probeFromPod(data, pod, url), "Service LoadBalancer whose externalTrafficPolicy is Cluster should be able to be connected from Pod")
+	}
+}
+
+func testLoadBalancerLocalFromNode(t *testing.T, data *TestData, nodes []string, url string, expectedHostnames []string) {
+	skipIfKubeProxyEnabled(t, data)
+	for idx, node := range nodes {
+		hostname, err := probeHostnameFromNode(node, url)
+		require.NoError(t, err, "Service LoadBalancer whose externalTrafficPolicy is Local should be able to be connected from Node")
+		require.Equal(t, hostname, expectedHostnames[idx])
+	}
+}
+
+func testLoadBalancerLocalFromPod(t *testing.T, data *TestData, pods []string, url string, expectedClientIPs, expectedHostnames []string) {
+	errMsg := "Service NodePort whose externalTrafficPolicy is Local should be able to be connected from Pod"
+	for idx, pod := range pods {
+		hostname, err := probeHostnameFromPod(data, pod, url)
+		require.NoError(t, err, errMsg)
+		require.Equal(t, hostname, expectedHostnames[idx])
+
+		clientIP, err := probeClientIPFromPod(data, pod, url)
+		require.NoError(t, err, errMsg)
+		require.Equal(t, clientIP, expectedClientIPs[idx])
+	}
+}
+
+func TestProxyNodePortServiceIPv4(t *testing.T) {
+	skipIfNotIPv4Cluster(t)
+	testProxyNodePortService(t, false)
+}
+
+func TestProxyNodePortServiceIPv6(t *testing.T) {
+	skipIfNotIPv6Cluster(t)
+	testProxyNodePortService(t, true)
+}
+
+func testProxyNodePortService(t *testing.T, isIPv6 bool) {
+	skipIfHasWindowsNodes(t)
+	skipIfNumNodesLessThan(t, 2)
+	skipIfProxyDisabled(t)
+	data, err := setupTest(t)
+	if err != nil {
+		t.Fatalf("Error when setting up test: %v", err)
+	}
+	defer teardownTest(t, data)
+	skipIfProxyAllDisabled(t, data)
+
+	nodes := []string{nodeName(0), nodeName(1)}
+	nodeIPs := []string{controlPlaneNodeIPv4(), workerNodeIPv4(1)}
+	ipProtocol := corev1.IPv4Protocol
+	if isIPv6 {
+		nodeIPs = []string{controlPlaneNodeIPv6(), workerNodeIPv6(1)}
+		ipProtocol = corev1.IPv6Protocol
+	}
+
+	// Create a busybox Pod on every Node. The busybox Pod is used as a client.
+	var busyboxes, busyboxIPs []string
+	for idx, node := range nodes {
+		podName, ips, _ := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, fmt.Sprintf("busybox-%d-", idx), node, testNamespace, false)
+		busyboxes = append(busyboxes, podName)
+		if !isIPv6 {
+			busyboxIPs = append(busyboxIPs, ips.ipv4.String())
+		} else {
+			busyboxIPs = append(busyboxIPs, ips.ipv6.String())
+		}
+	}
+
+	// Create two NodePort Services. The externalTrafficPolicy of one Service is Cluster, and the externalTrafficPolicy
+	// of another one is Local.
+	var portCluster, portLocal string
+	nodePortSvc, err := data.createAgnhostNodePortService("agnhost-cluster", true, false, &ipProtocol)
+	require.NoError(t, err)
+	for _, port := range nodePortSvc.Spec.Ports {
+		if port.NodePort != 0 {
+			portCluster = fmt.Sprint(port.NodePort)
+			break
+		}
+	}
+	require.NotEqual(t, "", portCluster, "NodePort port number should not be empty")
+	nodePortSvc, err = data.createAgnhostNodePortService("agnhost-local", true, true, &ipProtocol)
+	require.NoError(t, err)
+	for _, port := range nodePortSvc.Spec.Ports {
+		if port.NodePort != 0 {
+			portLocal = fmt.Sprint(port.NodePort)
+			break
+		}
+	}
+	require.NotEqual(t, "", portLocal, "NodePort port number should not be empty")
+
+	// Create agnhost Pods which are not on host network.
+	agnhosts := []string{"agnhost-0", "agnhost-1"}
+	for idx, node := range nodes {
+		createAgnhostPod(t, data, agnhosts[idx], node, false)
+	}
+	t.Run("Non-HostNetwork Endpoints", func(t *testing.T) {
+		nodePortTestCases(t, data, portCluster, portLocal, nodes, nodeIPs, busyboxes, busyboxIPs, agnhosts, false)
+	})
+
+	// Delete agnhost Pods which are not on host network and create new agnhost Pods which are on host network.
+	hostAgnhosts := []string{"agnhost-host-0", "agnhost-host-1"}
+	for idx, node := range nodes {
+		require.NoError(t, data.deletePod(testNamespace, agnhosts[idx]))
+		createAgnhostPod(t, data, hostAgnhosts[idx], node, true)
+	}
+	t.Run("HostNetwork Endpoints", func(t *testing.T) {
+		nodePortTestCases(t, data, portCluster, portLocal, nodes, nodeIPs, busyboxes, busyboxIPs, nodes, true)
+	})
+}
+
+func nodePortTestCases(t *testing.T, data *TestData, portStrCluster, portStrLocal string, nodes, nodeIPs, pods, podIPs, hostnames []string, hostNetwork bool) {
+	var clusterUrls, localUrls []string
+	for _, nodeIP := range nodeIPs {
+		clusterUrls = append(clusterUrls, net.JoinHostPort(nodeIP, portStrCluster))
+		localUrls = append(localUrls, net.JoinHostPort(nodeIP, portStrLocal))
+	}
+	reverseStrs := func(strs []string) []string {
+		var res []string
+		for i := len(strs) - 1; i >= 0; i-- {
+			res = append(res, strs[i])
+		}
+		return res
+	}
+
+	t.Run("ExternalTrafficPolicy:Cluster/Client:Remote", func(t *testing.T) {
+		testNodePortClusterFromRemote(t, data, nodes, reverseStrs(clusterUrls))
+	})
+	t.Run("ExternalTrafficPolicy:Cluster/Client:Node", func(t *testing.T) {
+		testNodePortClusterFromNode(t, data, nodes, clusterUrls)
+	})
+	t.Run("ExternalTrafficPolicy:Cluster/Client:Pod", func(t *testing.T) {
+		testNodePortClusterFromPod(t, data, pods, clusterUrls)
+	})
+	t.Run("ExternalTrafficPolicy:Local/Client:Remote", func(t *testing.T) {
+		if hostNetwork {
+			t.Skipf("Skip this test as Endpoint is on host network")
+		}
+		testNodePortLocalFromRemote(t, data, nodes, reverseStrs(localUrls), nodeIPs, reverseStrs(hostnames))
+	})
+	t.Run("ExternalTrafficPolicy:Local/Client:Node", func(t *testing.T) {
+		testNodePortLocalFromNode(t, data, nodes, localUrls, hostnames)
+	})
+	t.Run("ExternalTrafficPolicy:Local/Client:Pod", func(t *testing.T) {
+		testNodePortLocalFromPod(t, data, pods, localUrls, podIPs, hostnames)
+	})
+}
+
+func createAgnhostPod(t *testing.T, data *TestData, podName string, node string, hostNetwork bool) {
+	args := []string{"netexec", "--http-port=8080"}
+	ports := []corev1.ContainerPort{
+		{
+			Name:          "http",
+			ContainerPort: 8080,
+			Protocol:      corev1.ProtocolTCP,
+		},
+	}
+
+	require.NoError(t, data.createPodOnNode(podName, testNamespace, node, agnhostImage, []string{}, args, nil, ports, hostNetwork, nil))
+	_, err := data.podWaitForIPs(defaultTimeout, podName, testNamespace)
+	require.NoError(t, err)
+	require.NoError(t, data.podWaitForRunning(defaultTimeout, podName, testNamespace))
+}
+
+func testNodePortClusterFromRemote(t *testing.T, data *TestData, nodes, urls []string) {
+	skipIfKubeProxyEnabled(t, data)
+	for idx, node := range nodes {
+		require.NoError(t, probeFromNode(node, urls[idx]), "Service NodePort whose externalTrafficPolicy is Cluster should be able to be connected from remote Node")
+	}
+}
+
+func testNodePortClusterFromNode(t *testing.T, data *TestData, nodes, urls []string) {
+	skipIfKubeProxyEnabled(t, data)
+	for idx, node := range nodes {
+		require.NoError(t, probeFromNode(node, urls[idx]), "Service NodePort whose externalTrafficPolicy is Cluster should be able to be connected from Node")
+	}
+}
+
+func testNodePortClusterFromPod(t *testing.T, data *TestData, pods, urls []string) {
+	for _, url := range urls {
+		for _, pod := range pods {
+			require.NoError(t, probeFromPod(data, pod, url), "Service NodePort whose externalTrafficPolicy is Cluster should be able to be connected from Pod")
+		}
+	}
+}
+
+func testNodePortLocalFromRemote(t *testing.T, data *TestData, nodes, urls, expectedClientIPs, expectedHostnames []string) {
+	skipIfKubeProxyEnabled(t, data)
+	errMsg := "Service NodePort whose externalTrafficPolicy is Local should be able to be connected from remote Node"
+	for idx, node := range nodes {
+		hostname, err := probeHostnameFromNode(node, urls[idx])
+		require.NoError(t, err, errMsg)
+		require.Equal(t, hostname, expectedHostnames[idx])
+
+		clientIP, err := probeClientIPFromNode(node, urls[idx])
+		require.NoError(t, err, errMsg)
+		require.Equal(t, clientIP, expectedClientIPs[idx])
+	}
+}
+
+func testNodePortLocalFromNode(t *testing.T, data *TestData, nodes, urls, expectedHostnames []string) {
+	skipIfKubeProxyEnabled(t, data)
+	for idx, node := range nodes {
+		hostname, err := probeHostnameFromNode(node, urls[idx])
+		require.NoError(t, err, "Service NodePort whose externalTrafficPolicy is Local should be able to be connected rom Node")
+		require.Equal(t, hostname, expectedHostnames[idx])
+	}
+}
+
+func testNodePortLocalFromPod(t *testing.T, data *TestData, pods, urls, expectedClientIPs, expectedHostnames []string) {
+	errMsg := "There should be no errors when accessing to Service NodePort whose externalTrafficPolicy is Local from Pod"
+	for idx, pod := range pods {
+		hostname, err := probeHostnameFromPod(data, pod, urls[idx])
+		require.NoError(t, err, errMsg)
+		require.Equal(t, hostname, expectedHostnames[idx])
+
+		clientIP, err := probeClientIPFromPod(data, pod, urls[idx])
+		require.NoError(t, err, errMsg)
+		require.Equal(t, clientIP, expectedClientIPs[idx])
+	}
+}
+
+func TestProxyServiceSessionAffinity(t *testing.T) {
+	skipIfProviderIs(t, "kind", "#881 Does not work in Kind, needs to be investigated.")
+	skipIfHasWindowsNodes(t)
+	skipIfProxyDisabled(t)
+	data, err := setupTest(t)
+	if err != nil {
+		t.Fatalf("Error when setting up test: %v", err)
+	}
+	defer teardownTest(t, data)
+
+	if len(clusterInfo.podV4NetworkCIDR) != 0 {
+		ipFamily := corev1.IPv4Protocol
+		testProxyServiceSessionAffinity(&ipFamily, []string{"169.254.169.1", "169.254.169.2"}, data, t)
 	}
 	if len(clusterInfo.podV6NetworkCIDR) != 0 {
 		ipFamily := corev1.IPv6Protocol
@@ -79,7 +477,8 @@ func testProxyServiceSessionAffinityCase(t *testing.T, data *TestData) {
 func testProxyServiceSessionAffinity(ipFamily *corev1.IPFamily, ingressIPs []string, data *TestData, t *testing.T) {
 	nodeName := nodeName(1)
 	nginx := randName("nginx-")
-	require.NoError(t, data.createNginxPodOnNode(nginx, testNamespace, nodeName))
+
+	require.NoError(t, data.createNginxPodOnNode(nginx, testNamespace, nodeName, false))
 	nginxIP, err := data.podWaitForIPs(defaultTimeout, nginx, testNamespace)
 	defer data.deletePodAndWait(defaultTimeout, nginx, testNamespace)
 	require.NoError(t, err)
@@ -92,7 +491,7 @@ func testProxyServiceSessionAffinity(ipFamily *corev1.IPFamily, ingressIPs []str
 	require.NoError(t, err)
 
 	busyboxPod := randName("busybox-")
-	require.NoError(t, data.createBusyboxPodOnNode(busyboxPod, testNamespace, nodeName))
+	require.NoError(t, data.createBusyboxPodOnNode(busyboxPod, testNamespace, nodeName, false))
 	defer data.deletePodAndWait(defaultTimeout, busyboxPod, testNamespace)
 	require.NoError(t, data.podWaitForRunning(defaultTimeout, busyboxPod, testNamespace))
 	stdout, stderr, err := data.runCommandFromPod(testNamespace, busyboxPod, busyboxContainerName, []string{"wget", "-O", "-", svc.Spec.ClusterIP, "-T", "1"})
@@ -124,7 +523,6 @@ func testProxyServiceSessionAffinity(ipFamily *corev1.IPFamily, ingressIPs []str
 		}
 	}
 }
-
 func testProxyHairpinCase(t *testing.T, data *TestData) {
 	if len(clusterInfo.podV4NetworkCIDR) != 0 {
 		ipFamily := corev1.IPv4Protocol
@@ -136,6 +534,25 @@ func testProxyHairpinCase(t *testing.T, data *TestData) {
 	}
 }
 
+func TestProxyHairpin(t *testing.T) {
+	skipIfHasWindowsNodes(t)
+	skipIfProxyDisabled(t)
+	data, err := setupTest(t)
+	if err != nil {
+		t.Fatalf("Error when setting up test: %v", err)
+	}
+	defer teardownTest(t, data)
+
+	if len(clusterInfo.podV4NetworkCIDR) != 0 {
+		ipFamily := corev1.IPv4Protocol
+		testProxyHairpin(&ipFamily, data, t)
+	}
+	if len(clusterInfo.podV6NetworkCIDR) != 0 {
+		ipFamily := corev1.IPv6Protocol
+		testProxyHairpin(&ipFamily, data, t)
+	}
+}
+
 func testProxyHairpin(ipFamily *corev1.IPFamily, data *TestData, t *testing.T) {
 	busybox := randName("busybox-")
 	nodeName := nodeName(1)
@@ -143,7 +560,7 @@ func testProxyHairpin(ipFamily *corev1.IPFamily, data *TestData, t *testing.T) {
 	defer data.deletePodAndWait(defaultTimeout, busybox, testNamespace)
 	require.NoError(t, err)
 	require.NoError(t, data.podWaitForRunning(defaultTimeout, busybox, testNamespace))
-	svc, err := data.createService(busybox, 80, 80, map[string]string{"antrea-e2e": busybox}, false, corev1.ServiceTypeClusterIP, ipFamily)
+	svc, err := data.createService(busybox, 80, 80, map[string]string{"antrea-e2e": busybox}, false, false, corev1.ServiceTypeClusterIP, ipFamily)
 	defer data.deleteServiceAndWait(defaultTimeout, busybox)
 	require.NoError(t, err)
 
@@ -165,10 +582,29 @@ func testProxyEndpointLifeCycleCase(t *testing.T, data *TestData) {
 	}
 }
 
+func TestProxyEndpointLifeCycle(t *testing.T) {
+	skipIfHasWindowsNodes(t)
+	skipIfProxyDisabled(t)
+	data, err := setupTest(t)
+	if err != nil {
+		t.Fatalf("Error when setting up test: %v", err)
+	}
+	defer teardownTest(t, data)
+
+	if len(clusterInfo.podV4NetworkCIDR) != 0 {
+		ipFamily := corev1.IPv4Protocol
+		testProxyEndpointLifeCycle(&ipFamily, data, t)
+	}
+	if len(clusterInfo.podV6NetworkCIDR) != 0 {
+		ipFamily := corev1.IPv6Protocol
+		testProxyEndpointLifeCycle(&ipFamily, data, t)
+	}
+}
+
 func testProxyEndpointLifeCycle(ipFamily *corev1.IPFamily, data *TestData, t *testing.T) {
 	nodeName := nodeName(1)
 	nginx := randName("nginx-")
-	require.NoError(t, data.createNginxPodOnNode(nginx, testNamespace, nodeName))
+	require.NoError(t, data.createNginxPodOnNode(nginx, testNamespace, nodeName, false))
 	nginxIPs, err := data.podWaitForIPs(defaultTimeout, nginx, testNamespace)
 	require.NoError(t, err)
 	_, err = data.createNginxClusterIPService(nginx, false, ipFamily)
@@ -190,25 +626,66 @@ func testProxyEndpointLifeCycle(ipFamily *corev1.IPFamily, data *TestData, t *te
 	keywords := make(map[int]string)
 	keywords[42] = fmt.Sprintf("nat(dst=%s)", net.JoinHostPort(nginxIP, "80")) // endpointNATTable
 
+	var groupKeywords []string
+	if *ipFamily == corev1.IPv6Protocol {
+		groupKeywords = append(groupKeywords, fmt.Sprintf("set_field:0x%s->xxreg3", strings.TrimPrefix(hex.EncodeToString(*nginxIPs.ipv6), "0")))
+	} else {
+		groupKeywords = append(groupKeywords, fmt.Sprintf("0x%s->NXM_NX_REG3[]", strings.TrimPrefix(hex.EncodeToString(nginxIPs.ipv4.To4()), "0")))
+	}
+
 	for tableID, keyword := range keywords {
 		tableOutput, _, err := data.runCommandFromPod(metav1.NamespaceSystem, agentName, "antrea-agent", []string{"ovs-ofctl", "dump-flows", defaultBridgeName, fmt.Sprintf("table=%d", tableID)})
 		require.NoError(t, err)
 		require.Contains(t, tableOutput, keyword)
 	}
 
+	groupOutput, _, err := data.runCommandFromPod(metav1.NamespaceSystem, agentName, "antrea-agent", []string{"ovs-ofctl", "dump-groups", defaultBridgeName})
+	require.NoError(t, err)
+	for _, k := range groupKeywords {
+		require.Contains(t, groupOutput, k)
+	}
+
 	require.NoError(t, data.deletePodAndWait(defaultTimeout, nginx, testNamespace))
 
+	// Wait for one second to make sure the pipeline to be updated.
+	time.Sleep(time.Second)
+
 	for tableID, keyword := range keywords {
 		tableOutput, _, err := data.runCommandFromPod(metav1.NamespaceSystem, agentName, "antrea-agent", []string{"ovs-ofctl", "dump-flows", defaultBridgeName, fmt.Sprintf("table=%d", tableID)})
 		require.NoError(t, err)
 		require.NotContains(t, tableOutput, keyword)
 	}
+
+	groupOutput, _, err = data.runCommandFromPod(metav1.NamespaceSystem, agentName, "antrea-agent", []string{"ovs-ofctl", "dump-groups", defaultBridgeName})
+	require.NoError(t, err)
+	for _, k := range groupKeywords {
+		require.NotContains(t, groupOutput, k)
+	}
 }
 
 func testProxyServiceLifeCycleCase(t *testing.T, data *TestData) {
 	if len(clusterInfo.podV4NetworkCIDR) != 0 {
 		ipFamily := corev1.IPv4Protocol
-		testProxyServiceLifeCycle(&ipFamily, []string{"169.254.169.253", "169.254.169.254"}, data, t)
+		testProxyServiceLifeCycle(&ipFamily, []string{"169.254.169.1", "169.254.169.2"}, data, t)
+	}
+	if len(clusterInfo.podV6NetworkCIDR) != 0 {
+		ipFamily := corev1.IPv6Protocol
+		testProxyServiceLifeCycle(&ipFamily, []string{"fd75::aabb:ccdd:ef00", "fd75::aabb:ccdd:ef01"}, data, t)
+	}
+}
+
+func TestProxyServiceLifeCycle(t *testing.T) {
+	skipIfHasWindowsNodes(t)
+	skipIfProxyDisabled(t)
+	data, err := setupTest(t)
+	if err != nil {
+		t.Fatalf("Error when setting up test: %v", err)
+	}
+	defer teardownTest(t, data)
+
+	if len(clusterInfo.podV4NetworkCIDR) != 0 {
+		ipFamily := corev1.IPv4Protocol
+		testProxyServiceLifeCycle(&ipFamily, []string{"169.254.169.1", "169.254.169.2"}, data, t)
 	}
 	if len(clusterInfo.podV6NetworkCIDR) != 0 {
 		ipFamily := corev1.IPv6Protocol
@@ -219,7 +696,8 @@ func testProxyServiceLifeCycleCase(t *testing.T, data *TestData) {
 func testProxyServiceLifeCycle(ipFamily *corev1.IPFamily, ingressIPs []string, data *TestData, t *testing.T) {
 	nodeName := nodeName(1)
 	nginx := randName("nginx-")
-	require.NoError(t, data.createNginxPodOnNode(nginx, testNamespace, nodeName))
+
+	require.NoError(t, data.createNginxPodOnNode(nginx, testNamespace, nodeName, false))
 	defer data.deletePodAndWait(defaultTimeout, nginx, testNamespace)
 	nginxIPs, err := data.podWaitForIPs(defaultTimeout, nginx, testNamespace)
 	require.NoError(t, err)
@@ -241,16 +719,16 @@ func testProxyServiceLifeCycle(ipFamily *corev1.IPFamily, ingressIPs []string, d
 	// Hold on to make sure that the Service is realized.
 	time.Sleep(3 * time.Second)
 
-	svcLBflows := make([]string, len(ingressIPs)+1)
+	var svcLBflows []string
 	if *ipFamily == corev1.IPv6Protocol {
-		svcLBflows[0] = fmt.Sprintf("ipv6_dst=%s,tp_dst=80", svc.Spec.ClusterIP)
-		for idx, ingressIP := range ingressIPs {
-			svcLBflows[idx+1] = fmt.Sprintf("ipv6_dst=%s,tp_dst=80", ingressIP)
+		svcLBflows = append(svcLBflows, fmt.Sprintf("ipv6_dst=%s,tp_dst=80", svc.Spec.ClusterIP))
+		for _, ingressIP := range ingressIPs {
+			svcLBflows = append(svcLBflows, fmt.Sprintf("ipv6_dst=%s,tp_dst=80", ingressIP))
 		}
 	} else {
-		svcLBflows[0] = fmt.Sprintf("nw_dst=%s,tp_dst=80", svc.Spec.ClusterIP)
-		for idx, ingressIP := range ingressIPs {
-			svcLBflows[idx+1] = fmt.Sprintf("nw_dst=%s,tp_dst=80", ingressIP)
+		svcLBflows = append(svcLBflows, fmt.Sprintf("nw_dst=%s,tp_dst=80", svc.Spec.ClusterIP))
+		for _, ingressIP := range ingressIPs {
+			svcLBflows = append(svcLBflows, fmt.Sprintf("nw_dst=%s,tp_dst=80", ingressIP))
 		}
 	}
 
diff --git a/test/e2e/service_test.go b/test/e2e/service_test.go
index d62612ffa..61007cffd 100644
--- a/test/e2e/service_test.go
+++ b/test/e2e/service_test.go
@@ -17,7 +17,6 @@ package e2e
 import (
 	"fmt"
 	"net"
-	"strconv"
 	"strings"
 	"testing"
 
@@ -25,85 +24,77 @@ import (
 	corev1 "k8s.io/api/core/v1"
 )
 
-// TestClusterIP tests traffic from Nodes and Pods to ClusterIP Service.
-func TestClusterIP(t *testing.T) {
-	// TODO: Support for dual-stack and IPv6-only clusters
-	skipIfIPv6Cluster(t)
+func TestClusterIPv4(t *testing.T) {
+	skipIfNotIPv4Cluster(t)
+	testClusterIP(t, false)
+}
+
+func TestClusterIPv6(t *testing.T) {
+	skipIfNotIPv6Cluster(t)
+	testClusterIP(t, true)
+}
 
+func testClusterIP(t *testing.T, isIPv6 bool) {
+	skipIfNumNodesLessThan(t, 2)
 	data, err := setupTest(t)
 	if err != nil {
 		t.Fatalf("Error when setting up test: %v", err)
 	}
 	defer teardownTest(t, data)
 
-	svcName := "nginx"
-	serverPodNode := nodeName(0)
-	svc, cleanup := data.createClusterIPServiceAndBackendPods(t, svcName, serverPodNode)
-	defer cleanup()
-	t.Logf("%s Service is ready", svcName)
-
-	testFromNode := func(node string) {
-		// Retry is needed for rules to be installed by kube-proxy/antrea-proxy.
-		cmd := fmt.Sprintf("curl --connect-timeout 1 --retry 5 --retry-connrefused %s:80", svc.Spec.ClusterIP)
-		rc, stdout, stderr, err := RunCommandOnNode(node, cmd)
-		if rc != 0 || err != nil {
-			t.Errorf("Error when running command '%s' on Node '%s', rc: %d, stdout: %s, stderr: %s, error: %v",
-				cmd, node, rc, stdout, stderr, err)
-		}
+	nodes := []string{nodeName(0), nodeName(1)}
+	var busyboxes []string
+	for idx, node := range nodes {
+		podName, _, _ := createAndWaitForPod(t, data, data.createBusyboxPodOnNode, fmt.Sprintf("busybox-%d-", idx), node, testNamespace, false)
+		busyboxes = append(busyboxes, podName)
 	}
 
-	testFromPod := func(podName, nodeName string, hostNetwork bool) {
-		require.NoError(t, data.createPodOnNode(podName, testNamespace, nodeName, busyboxImage, []string{"sleep", strconv.Itoa(3600)}, nil, nil, nil, hostNetwork, nil))
-		defer data.deletePodAndWait(defaultTimeout, podName, testNamespace)
-		require.NoError(t, data.podWaitForRunning(defaultTimeout, podName, testNamespace))
-		err := data.runNetcatCommandFromTestPod(podName, testNamespace, svc.Spec.ClusterIP, 80)
-		require.NoError(t, err, "Pod %s should be able to connect %s, but was not able to connect", podName, net.JoinHostPort(svc.Spec.ClusterIP, fmt.Sprint(80)))
+	nginx := fmt.Sprintf("nginx-%v", isIPv6)
+	hostNginx := fmt.Sprintf("nginx-host-%v", isIPv6)
+	ipProtocol := corev1.IPv4Protocol
+	if isIPv6 {
+		ipProtocol = corev1.IPv6Protocol
 	}
+	clusterIPSvc, err := data.createNginxClusterIPService(fmt.Sprintf("nginx-%v", isIPv6), true, &ipProtocol)
+	require.NoError(t, err)
+	require.NotEqual(t, "", clusterIPSvc.Spec.ClusterIP, "ClusterIP should not be empty")
+	url := net.JoinHostPort(clusterIPSvc.Spec.ClusterIP, "80")
+
+	createAndWaitForPod(t, data, data.createNginxPodOnNode, nginx, nodeName(0), testNamespace, false)
+	t.Run("Non-HostNetwork Endpoints", func(t *testing.T) {
+		testClusterIPCases(t, data, url, nodes, busyboxes)
+	})
 
-	t.Run("ClusterIP", func(t *testing.T) {
-		t.Run("Same Linux Node can access the Service", func(t *testing.T) {
-			t.Parallel()
-			testFromPod("hostnetwork-client-on-same-node", serverPodNode, true)
-		})
-		t.Run("Different Linux Node can access the Service", func(t *testing.T) {
-			t.Parallel()
-			skipIfNumNodesLessThan(t, 2)
-			testFromPod("hostnetwork-client-on-different-node", nodeName(1), true)
-		})
-		t.Run("Windows host can access the Service", func(t *testing.T) {
-			t.Parallel()
-			skipIfNoWindowsNodes(t)
-			idx := clusterInfo.windowsNodes[0]
-			winNode := clusterInfo.nodes[idx].name
-			testFromNode(winNode)
-		})
-		t.Run("Linux Pod on same Node can access the Service", func(t *testing.T) {
-			t.Parallel()
-			testFromPod("client-on-same-node", serverPodNode, false)
-		})
-		t.Run("Linux Pod on different Node can access the Service", func(t *testing.T) {
-			t.Parallel()
-			skipIfNumNodesLessThan(t, 2)
-			testFromPod("client-on-different-node", nodeName(1), false)
-		})
+	require.NoError(t, data.deletePod(testNamespace, nginx))
+	createAndWaitForPod(t, data, data.createNginxPodOnNode, hostNginx, nodeName(0), testNamespace, true)
+	t.Run("HostNetwork Endpoints", func(t *testing.T) {
+		testClusterIPCases(t, data, url, nodes, busyboxes)
 	})
 }
 
-func (data *TestData) createClusterIPServiceAndBackendPods(t *testing.T, name string, node string) (*corev1.Service, func()) {
-	ipv4Protocol := corev1.IPv4Protocol
-	require.NoError(t, data.createNginxPodOnNode(name, testNamespace, node))
-	_, err := data.podWaitForIPs(defaultTimeout, name, testNamespace)
-	require.NoError(t, err)
-	require.NoError(t, data.podWaitForRunning(defaultTimeout, name, testNamespace))
-	svc, err := data.createNginxClusterIPService(name, false, &ipv4Protocol)
-	require.NoError(t, err)
+func testClusterIPCases(t *testing.T, data *TestData, url string, nodes, pods []string) {
+	t.Run("All Nodes can access Service ClusterIP", func(t *testing.T) {
+		skipIfProxyAllDisabled(t, data)
+		skipIfKubeProxyEnabled(t, data)
+		for _, node := range nodes {
+			testClusterIPFromNode(t, url, node)
+		}
+	})
+	t.Run("Pods from all Nodes can access Service ClusterIP", func(t *testing.T) {
+		for _, pod := range pods {
+			testClusterIPFromPod(t, data, url, pod)
+		}
+	})
+}
 
-	cleanup := func() {
-		data.deletePodAndWait(defaultTimeout, name, testNamespace)
-		data.deleteServiceAndWait(defaultTimeout, name)
-	}
+func testClusterIPFromPod(t *testing.T, data *TestData, url, podName string) {
+	_, _, err := data.runCommandFromPod(testNamespace, podName, busyboxContainerName, []string{"wget", "-O", "-", url, "-T", "1"})
+	require.NoError(t, err, "Service ClusterIP should be able to be connected from Pod")
+}
 
-	return svc, cleanup
+func testClusterIPFromNode(t *testing.T, url, nodeName string) {
+	_, _, _, err := RunCommandOnNode(nodeName, strings.Join([]string{"wget", "-O", "-", url, "-T", "1"}, " "))
+	require.NoError(t, err, "Service ClusterIP should be able to be connected from Node")
 }
 
 // TestNodePortWindows tests NodePort Service on Windows Node. It is a temporary test to replace upstream Kubernetes one:
@@ -160,7 +151,7 @@ func (data *TestData) createAgnhostServiceAndBackendPods(t *testing.T, name stri
 	_, err := data.podWaitForIPs(defaultTimeout, name, testNamespace)
 	require.NoError(t, err)
 	require.NoError(t, data.podWaitForRunning(defaultTimeout, name, testNamespace))
-	svc, err := data.createService(name, 80, 80, map[string]string{"app": "agnhost"}, false, svcType, &ipv4Protocol)
+	svc, err := data.createService(name, 80, 80, map[string]string{"app": "agnhost"}, false, false, svcType, &ipv4Protocol)
 	require.NoError(t, err)
 
 	cleanup := func() {
diff --git a/test/e2e/traceflow_test.go b/test/e2e/traceflow_test.go
index a4f3b8653..09767aabe 100644
--- a/test/e2e/traceflow_test.go
+++ b/test/e2e/traceflow_test.go
@@ -1063,7 +1063,7 @@ func testTraceflowInterNode(t *testing.T, data *TestData) {
 	// Create Service backend Pod. The "hairpin" testcases require the Service to have a single backend Pod,
 	// and no more, in order to be deterministic.
 	nginxPodName := "nginx"
-	require.NoError(t, data.createNginxPodOnNode(nginxPodName, testNamespace, node2))
+	require.NoError(t, data.createNginxPodOnNode(nginxPodName, testNamespace, node2, false))
 	nginxIP, err := data.podWaitForIPs(defaultTimeout, nginxPodName, testNamespace)
 	require.NoError(t, err)
 
diff --git a/test/e2e/upgrade_test.go b/test/e2e/upgrade_test.go
index f0bfca6dc..0915375f7 100644
--- a/test/e2e/upgrade_test.go
+++ b/test/e2e/upgrade_test.go
@@ -54,7 +54,7 @@ func TestUpgrade(t *testing.T) {
 	podName := randName("test-pod-")
 
 	t.Logf("Creating a busybox test Pod on '%s'", nodeName)
-	if err := data.createBusyboxPodOnNode(podName, testNamespace, nodeName); err != nil {
+	if err := data.createBusyboxPodOnNode(podName, testNamespace, nodeName, false); err != nil {
 		t.Fatalf("Error when creating busybox test Pod: %v", err)
 	}
 	if err := data.podWaitForRunning(defaultTimeout, podName, testNamespace); err != nil {
diff --git a/test/integration/agent/openflow_test.go b/test/integration/agent/openflow_test.go
index dac34e96b..3e7be68e8 100644
--- a/test/integration/agent/openflow_test.go
+++ b/test/integration/agent/openflow_test.go
@@ -28,6 +28,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus/testutil"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/component-base/metrics/legacyregistry"
 
@@ -112,7 +113,7 @@ func TestConnectivityFlows(t *testing.T) {
 		antrearuntime.WindowsOS = runtime.GOOS
 	}
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, true, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, true, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 	defer func() {
@@ -153,7 +154,7 @@ func TestConnectivityFlows(t *testing.T) {
 }
 
 func TestReplayFlowsConnectivityFlows(t *testing.T) {
-	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 
@@ -189,7 +190,7 @@ func TestReplayFlowsConnectivityFlows(t *testing.T) {
 }
 
 func TestReplayFlowsNetworkPolicyFlows(t *testing.T) {
-	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 
@@ -300,7 +301,7 @@ func testInstallTunnelFlows(t *testing.T, config *testConfig) {
 }
 
 func testInstallServiceFlows(t *testing.T, config *testConfig) {
-	err := c.InstallClusterServiceFlows()
+	err := c.InstallDefaultServiceFlows(nil, nil)
 	if err != nil {
 		t.Fatalf("Failed to install Openflow entries to skip service CIDR from egress table: %v", err)
 	}
@@ -374,11 +375,11 @@ func TestNetworkPolicyFlows(t *testing.T) {
 	// Initialize ovs metrics (Prometheus) to test them
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
-	_, err = c.Initialize(roundInfo, &config1.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: podIPv6CIDR}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
+	_, err = c.Initialize(roundInfo, &config1.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: podIPv6CIDR, GatewayConfig: gwConfig}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -484,7 +485,7 @@ func TestIPv6ConnectivityFlows(t *testing.T) {
 	// Initialize ovs metrics (Prometheus) to test them
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, true, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, true, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 
@@ -526,11 +527,11 @@ type svcConfig struct {
 }
 
 func TestProxyServiceFlows(t *testing.T) {
-	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, true, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
-	_, err = c.Initialize(roundInfo, &config1.NodeConfig{}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
+	_, err = c.Initialize(roundInfo, &config1.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: podIPv6CIDR, GatewayConfig: gwConfig}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -542,7 +543,7 @@ func TestProxyServiceFlows(t *testing.T) {
 
 	endpoints := []k8sproxy.Endpoint{
 		k8stypes.NewEndpointInfo(&k8sproxy.BaseEndpointInfo{
-			Endpoint: net.JoinHostPort("10.20.0.11", "8081"),
+			Endpoint: net.JoinHostPort("192.168.1.2", "8081"),
 			IsLocal:  true,
 		}),
 		k8stypes.NewEndpointInfo(&k8sproxy.BaseEndpointInfo{
@@ -615,7 +616,7 @@ func installServiceFlows(t *testing.T, gid uint32, svc svcConfig, endpointList [
 	assert.NoError(t, err, "no error should return when installing flows for Endpoints")
 	err = c.InstallServiceGroup(groupID, svc.withSessionAffinity, endpointList)
 	assert.NoError(t, err, "no error should return when installing groups for Service")
-	err = c.InstallServiceFlows(groupID, svc.ip, svc.port, svc.protocol, stickyMaxAgeSeconds)
+	err = c.InstallServiceFlows(groupID, svc.ip, svc.port, svc.protocol, stickyMaxAgeSeconds, false, v1.ServiceTypeClusterIP)
 	assert.NoError(t, err, "no error should return when installing flows for Service")
 }
 
@@ -658,7 +659,7 @@ func expectedProxyServiceGroupAndFlows(gid uint32, svc svcConfig, endpointList [
 		},
 	}}
 	epDNATFlows := expectTableFlows{tableID: 42, flows: []*ofTestUtils.ExpectFlow{}}
-	hairpinFlows := expectTableFlows{tableID: 106, flows: []*ofTestUtils.ExpectFlow{}}
+	hairpinFlows := expectTableFlows{tableID: 108, flows: []*ofTestUtils.ExpectFlow{}}
 	groupBuckets = make([]string, 0)
 	for _, ep := range endpointList {
 		epIP := ipToHexString(net.ParseIP(ep.IP()))
@@ -1013,7 +1014,7 @@ func preparePodFlows(podIPs []net.IP, podMAC net.HardwareAddr, podOFPort uint32,
 						},
 					},
 				})
-			nextTableForSpoofguard = 29
+			nextTableForSpoofguard = 23
 		} else {
 			ipProto = "ipv6"
 			nwSrcField = "ipv6_src"
@@ -1083,7 +1084,7 @@ func prepareGatewayFlows(gwIPs []net.IP, gwMAC net.HardwareAddr, vMAC net.Hardwa
 						},
 						{
 							MatchStr: fmt.Sprintf("priority=200,ip,in_port=%d", config1.HostGatewayOFPort),
-							ActStr:   "goto_table:29",
+							ActStr:   "goto_table:23",
 						},
 					},
 				})
@@ -1202,19 +1203,25 @@ func prepareDefaultFlows(config *testConfig) []expectTableFlows {
 	}
 	table105Flows := expectTableFlows{
 		tableID: 105,
-		flows:   []*ofTestUtils.ExpectFlow{{MatchStr: "priority=0", ActStr: "goto_table:106"}},
+		flows:   []*ofTestUtils.ExpectFlow{{MatchStr: "priority=0", ActStr: "goto_table:108"}},
 	}
 	table72Flows := expectTableFlows{
 		tableID: 72,
 		flows:   []*ofTestUtils.ExpectFlow{{MatchStr: "priority=0", ActStr: "goto_table:80"}},
 	}
+	table30Flows := expectTableFlows{
+		tableID: 30,
+	}
 	if config.enableIPv4 {
+		table30Flows.flows = append(table30Flows.flows,
+			&ofTestUtils.ExpectFlow{MatchStr: "priority=200,ip", ActStr: "ct(table=31,zone=65520,nat)"},
+		)
 		table31Flows.flows = append(table31Flows.flows,
 			&ofTestUtils.ExpectFlow{MatchStr: "priority=190,ct_state=+inv+trk,ip", ActStr: "drop"},
 		)
 		table105Flows.flows = append(table105Flows.flows,
-			&ofTestUtils.ExpectFlow{MatchStr: "priority=200,ct_state=+new+trk,ip,reg0=0x1/0xf", ActStr: "ct(commit,table=106,zone=65520,exec(load:0x20->NXM_NX_CT_MARK[])"},
-			&ofTestUtils.ExpectFlow{MatchStr: "priority=190,ct_state=+new+trk,ip", ActStr: "ct(commit,table=106,zone=65520)"},
+			&ofTestUtils.ExpectFlow{MatchStr: "priority=200,ct_state=+new+trk,ip,reg0=0x1/0xf", ActStr: "ct(commit,table=108,zone=65520,exec(load:0x20->NXM_NX_CT_MARK[])"},
+			&ofTestUtils.ExpectFlow{MatchStr: "priority=190,ct_state=+new+trk,ip", ActStr: "ct(commit,table=108,zone=65520)"},
 		)
 		table72Flows.flows = append(table72Flows.flows,
 			&ofTestUtils.ExpectFlow{MatchStr: "priority=210,ip,reg0=0x1/0xf", ActStr: "goto_table:80"},
@@ -1222,12 +1229,15 @@ func prepareDefaultFlows(config *testConfig) []expectTableFlows {
 		)
 	}
 	if config.enableIPv6 {
+		table30Flows.flows = append(table30Flows.flows,
+			&ofTestUtils.ExpectFlow{MatchStr: "priority=200,ipv6", ActStr: "ct(table=31,zone=65510,nat)"},
+		)
 		table31Flows.flows = append(table31Flows.flows,
 			&ofTestUtils.ExpectFlow{MatchStr: "priority=190,ct_state=+inv+trk,ipv6", ActStr: "drop"},
 		)
 		table105Flows.flows = append(table105Flows.flows,
-			&ofTestUtils.ExpectFlow{MatchStr: "priority=200,ct_state=+new+trk,ipv6,reg0=0x1/0xf", ActStr: "ct(commit,table=106,zone=65510,exec(load:0x20->NXM_NX_CT_MARK[])"},
-			&ofTestUtils.ExpectFlow{MatchStr: "priority=190,ct_state=+new+trk,ipv6", ActStr: "ct(commit,table=106,zone=65510)"},
+			&ofTestUtils.ExpectFlow{MatchStr: "priority=200,ct_state=+new+trk,ipv6,reg0=0x1/0xf", ActStr: "ct(commit,table=108,zone=65510,exec(load:0x20->NXM_NX_CT_MARK[])"},
+			&ofTestUtils.ExpectFlow{MatchStr: "priority=190,ct_state=+new+trk,ipv6", ActStr: "ct(commit,table=108,zone=65510)"},
 		)
 		table72Flows.flows = append(table72Flows.flows,
 			&ofTestUtils.ExpectFlow{MatchStr: "priority=210,ipv6,reg0=0x1/0xf", ActStr: "goto_table:80"},
@@ -1235,7 +1245,7 @@ func prepareDefaultFlows(config *testConfig) []expectTableFlows {
 		)
 	}
 	return []expectTableFlows{
-		table31Flows, table105Flows, table72Flows,
+		table30Flows, table31Flows, table105Flows, table72Flows,
 		{
 			uint8(0),
 			[]*ofTestUtils.ExpectFlow{{MatchStr: "priority=0", ActStr: "drop"}},
@@ -1251,12 +1261,6 @@ func prepareDefaultFlows(config *testConfig) []expectTableFlows {
 				{MatchStr: "priority=0", ActStr: "drop"},
 			},
 		},
-		{
-			uint8(30),
-			[]*ofTestUtils.ExpectFlow{
-				{MatchStr: "priority=200,ip", ActStr: "ct(table=31,zone=65520,nat)"},
-			},
-		},
 		{
 			uint8(42),
 			[]*ofTestUtils.ExpectFlow{{MatchStr: "priority=0", ActStr: "goto_table:50"}},
@@ -1399,7 +1403,7 @@ func prepareSNATFlows(snatIP net.IP, mark, podOFPort, podOFPortRemote uint32, vM
 }
 
 func TestSNATFlows(t *testing.T) {
-	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, false, false, true, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, ovsconfig.OVSDatapathNetdev, false, false, true, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
diff --git a/test/integration/agent/route_test.go b/test/integration/agent/route_test.go
index e9a275695..167b4ef15 100644
--- a/test/integration/agent/route_test.go
+++ b/test/integration/agent/route_test.go
@@ -140,7 +140,7 @@ func TestInitialize(t *testing.T) {
 
 	for _, tc := range tcs {
 		t.Logf("Running Initialize test with mode %s node config %s", tc.networkConfig.TrafficEncapMode, nodeConfig)
-		routeClient, err := route.NewClient(serviceCIDR, tc.networkConfig, tc.noSNAT)
+		routeClient, err := route.NewClient(serviceCIDR, tc.networkConfig, tc.noSNAT, false)
 		assert.NoError(t, err)
 
 		var xtablesReleasedTime, initializedTime time.Time
@@ -209,7 +209,7 @@ func TestInitialize(t *testing.T) {
 `,
 			"nat": `:ANTREA-POSTROUTING - [0:0]
 -A POSTROUTING -m comment --comment "Antrea: jump to Antrea postrouting rules" -j ANTREA-POSTROUTING
--A ANTREA-POSTROUTING -s 10.10.10.0/24 -m comment --comment "Antrea: masquerade Pod to external packets" -m set ! --match-set ANTREA-POD-IP dst -j MASQUERADE
+-A ANTREA-POSTROUTING -s 10.10.10.0/24 ! -o antrea-gw0 -m comment --comment "Antrea: masquerade Pod to external packets" -m set ! --match-set ANTREA-POD-IP dst -j MASQUERADE
 -A ANTREA-POSTROUTING -o antrea-gw0 -m comment --comment "Antrea: masquerade LOCAL traffic" -m addrtype ! --src-type LOCAL --limit-iface-out -m addrtype --src-type LOCAL -j MASQUERADE --random-fully
 `}
 
@@ -246,7 +246,7 @@ func TestIpTablesSync(t *testing.T) {
 	gwLink := createDummyGW(t)
 	defer netlink.LinkDel(gwLink)
 
-	routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false)
+	routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false, false)
 	assert.Nil(t, err)
 
 	inited := make(chan struct{})
@@ -297,7 +297,7 @@ func TestAddAndDeleteSNATRule(t *testing.T) {
 	gwLink := createDummyGW(t)
 	defer netlink.LinkDel(gwLink)
 
-	routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false)
+	routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false, false)
 	assert.Nil(t, err)
 
 	inited := make(chan struct{})
@@ -351,7 +351,7 @@ func TestAddAndDeleteRoutes(t *testing.T) {
 
 	for _, tc := range tcs {
 		t.Logf("Running test with mode %s peer cidr %s peer ip %s node config %s", tc.mode, tc.peerCIDR, tc.peerIP, nodeConfig)
-		routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: tc.mode}, false)
+		routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: tc.mode}, false, false)
 		assert.NoError(t, err)
 		err = routeClient.Initialize(nodeConfig, func() {})
 		assert.NoError(t, err)
@@ -414,7 +414,7 @@ func TestSyncRoutes(t *testing.T) {
 
 	for _, tc := range tcs {
 		t.Logf("Running test with mode %s peer cidr %s peer ip %s node config %s", tc.mode, tc.peerCIDR, tc.peerIP, nodeConfig)
-		routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: tc.mode}, false)
+		routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: tc.mode}, false, false)
 		assert.NoError(t, err)
 		err = routeClient.Initialize(nodeConfig, func() {})
 		assert.NoError(t, err)
@@ -504,7 +504,7 @@ func TestReconcile(t *testing.T) {
 
 	for _, tc := range tcs {
 		t.Logf("Running test with mode %s added routes %v desired routes %v", tc.mode, tc.addedRoutes, tc.desiredPeerCIDRs)
-		routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: tc.mode}, false)
+		routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: tc.mode}, false, false)
 		assert.NoError(t, err)
 		err = routeClient.Initialize(nodeConfig, func() {})
 		assert.NoError(t, err)
@@ -542,7 +542,7 @@ func TestRouteTablePolicyOnly(t *testing.T) {
 	gwLink := createDummyGW(t)
 	defer netlink.LinkDel(gwLink)
 
-	routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeNetworkPolicyOnly}, false)
+	routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeNetworkPolicyOnly}, false, false)
 	assert.NoError(t, err)
 	err = routeClient.Initialize(nodeConfig, func() {})
 	assert.NoError(t, err)
@@ -601,7 +601,7 @@ func TestIPv6RoutesAndNeighbors(t *testing.T) {
 	gwLink := createDummyGW(t)
 	defer netlink.LinkDel(gwLink)
 
-	routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false)
+	routeClient, err := route.NewClient(serviceCIDR, &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false, false)
 	assert.Nil(t, err)
 	_, ipv6Subnet, _ := net.ParseCIDR("fd74:ca9b:172:19::/64")
 	gwIPv6 := net.ParseIP("fd74:ca9b:172:19::1")