Repository: antrea-io/antrea
Pull request #4419 (2022-11-29 13:44:37)

Set NO_FLOOD to IPsec tunnel ports
Set NO_FLOOD to IPsec tunnel ports to avoid ARP flooding.

Signed-off-by: Xu Liu <xliu2@vmware.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/4419?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) Report
> Merging [#4419](https://codecov.io/gh/antrea-io/antrea/pull/4419?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (11025af) into [main](https://codecov.io/gh/antrea-io/antrea/commit/b977b1df5d52333d828553039ca409b82078a7bf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (b977b1d) will **increase** coverage by `2.04%`.
> The diff coverage is `16.66%`.

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

```diff
@@            Coverage Diff             @@
##             main    #4419      +/-   ##
==========================================
+ Coverage   65.56%   67.61%   +2.04%     
==========================================
  Files         400      376      -24     
  Lines       56847    54864    -1983     
==========================================
- Hits        37270    37094     -176     
+ Misses      16882    15092    -1790     
+ Partials     2695     2678      -17     
```

| Flag | Coverage Δ | | *Carryforward flag |
|---|---|---|---|
| integration-tests | `34.50% <ø> (ø)` | | Carriedforward from [b977b1d](https://codecov.io/gh/antrea-io/antrea/commit/b977b1df5d52333d828553039ca409b82078a7bf?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) |
| kind-e2e-tests | `48.11% <25.00%> (ø)` | | Carriedforward from [b977b1d](https://codecov.io/gh/antrea-io/antrea/commit/b977b1df5d52333d828553039ca409b82078a7bf?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) |
| unit-tests | `51.17% <20.00%> (+1.36%)` | :arrow_up: | |

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

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/4419?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | Coverage Δ | |
|---|---|---|
| [cmd/antrea-agent/agent.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-Y21kL2FudHJlYS1hZ2VudC9hZ2VudC5nbw==) | `0.00% <0.00%> (ø)` | |
| [...gent/controller/noderoute/node\_route\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvbm9kZXJvdXRlL25vZGVfcm91dGVfY29udHJvbGxlci5nbw==) | `56.90% <20.00%> (-1.09%)` | :arrow_down: |
| [pkg/features/antrea\_features.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2ZlYXR1cmVzL2FudHJlYV9mZWF0dXJlcy5nbw==) | `64.00% <0.00%> (-36.00%)` | :arrow_down: |
| [cmd/antrea-agent/options.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-Y21kL2FudHJlYS1hZ2VudC9vcHRpb25zLmdv) | `3.55% <0.00%> (-20.31%)` | :arrow_down: |
| [pkg/apiserver/handlers/endpoint/handler.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FwaXNlcnZlci9oYW5kbGVycy9lbmRwb2ludC9oYW5kbGVyLmdv) | `56.52% <0.00%> (-13.05%)` | :arrow_down: |
| [pkg/apiserver/certificate/certificate.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FwaXNlcnZlci9jZXJ0aWZpY2F0ZS9jZXJ0aWZpY2F0ZS5nbw==) | `70.37% <0.00%> (-6.49%)` | :arrow_down: |
| [pkg/agent/types/networkpolicy.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L3R5cGVzL25ldHdvcmtwb2xpY3kuZ28=) | `89.28% <0.00%> (-5.31%)` | :arrow_down: |
| [pkg/util/env/env.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL3V0aWwvZW52L2Vudi5nbw==) | `58.73% <0.00%> (-4.77%)` | :arrow_down: |
| [pkg/antctl/runtime/runtime.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FudGN0bC9ydW50aW1lL3J1bnRpbWUuZ28=) | `33.33% <0.00%> (-3.34%)` | :arrow_down: |
| [...catesigningrequest/ipsec\_csr\_signing\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2NvbnRyb2xsZXIvY2VydGlmaWNhdGVzaWduaW5ncmVxdWVzdC9pcHNlY19jc3Jfc2lnbmluZ19jb250cm9sbGVyLmdv) | `61.65% <0.00%> (-2.46%)` | :arrow_down: |
| ... and [35 more](https://codecov.io/gh/antrea-io/antrea/pull/4419/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | |

tnqn: /test-all
xliuxu: > @xliuxu I suppose this should be backported?

I think so. I will backport this PR once merged.
xliuxu: /test-e2e
tnqn: It failed on testIPSecTunnelConnectivity twice. Is it related to the change?
xliuxu: /test-e2e
xliuxu: > It failed on testIPSecTunnelConnectivity twice. Is it related to the change?

I cannot reproduce the same error locally. I will trigger a new e2e test and check the results on the testbed if it still fails.
tnqn: > > It failed on testIPSecTunnelConnectivity twice. Is it related to the change?
> 
> I cannot reproduce the same error locally. I will trigger a new e2e test and check the results on the testbed if it still fails.

Could you check the previous failures too? Even if the 3rd succeeds, we need to understand the failures as they are frequent.
xliuxu: > > > It failed on testIPSecTunnelConnectivity twice. Is it related to the change?
> > 
> > 
> > I cannot reproduce the same error locally. I will trigger a new e2e test and check the results on the testbed if it still fails.
> 
> Could you check the previous failures too? Even if the 3rd succeeds, we need to understand the failures as they are frequent.

I cannot reproduce the error in the testbeds. I will trigger more e2e tests to check.
/test-e2e
xliuxu: /test-latest-e2e
xliuxu: 
/test-e2e


xliuxu: 
/test-e2e


xliuxu: 
/test-e2e


xliuxu: /test-e2e
xliuxu: /test-e2e
xliuxu: /test-e2e
xliuxu: /test-e2e
xliuxu: /test-e2e
xliuxu: /test-e2e
xliuxu: /test-e2e
xliuxu: /test-e2e
xliuxu: /test-e2e

xliuxu: /test-e2e

xliuxu: /test-e2e
xliuxu: /test-e2e
xliuxu: /test-e2e
xliuxu: /test-e2e

xliuxu: /test-all

Patch:
diff --git a/build/charts/antrea/README.md b/build/charts/antrea/README.md
index 1279c1168..5f3b63026 100644
--- a/build/charts/antrea/README.md
+++ b/build/charts/antrea/README.md
@@ -67,6 +67,7 @@ Kubernetes: `>= 1.16.0-0`
 | disableTXChecksumOffload | bool | `false` | Disable TX checksum offloading for container network interfaces. It's supposed to be set to true when the datapath doesn't support TX checksum offloading, which causes packets to be dropped due to bad checksum. It affects Pods running on Linux Nodes only. |
 | dnsServerOverride | string | `""` | Address of DNS server, to override the kube-dns service. It's used to resolve hostname in FQDN policy. |
 | egress.exceptCIDRs | list | `[]` | CIDR ranges to which outbound Pod traffic will not be SNAT'd by Egresses. |
+| egress.maxEgressIPsPerNode | int | `255` | The maximum number of Egress IPs that can be assigned to a Node. It's useful when the Node network restricts the number of secondary IPs a Node can have, e.g. EKS. It must not be greater than 255. |
 | enableBridgingMode | bool | `false` | Enable bridging mode of Pod network on Nodes, in which the Node's transport interface is connected to the OVS bridge. |
 | featureGates | object | `{}` | To explicitly enable or disable a FeatureGate and bypass the Antrea defaults, add an entry to the dictionary with the FeatureGate's name as the key and a boolean as the value. |
 | flowCollector.activeFlowExportTimeout | string | `"5s"` | timeout after which a flow record is sent to the collector for active flows. |
diff --git a/build/charts/antrea/conf/antrea-agent.conf b/build/charts/antrea/conf/antrea-agent.conf
index b86007494..6e70c76b7 100644
--- a/build/charts/antrea/conf/antrea-agent.conf
+++ b/build/charts/antrea/conf/antrea-agent.conf
@@ -169,6 +169,9 @@ egress:
   {{- with .exceptCIDRs }}
   {{- toYaml . | nindent 4 }}
   {{- end }}
+  # The maximum number of Egress IPs that can be assigned to a Node. It's useful when the Node network restricts
+  # the number of secondary IPs a Node can have, e.g. EKS. It must not be greater than 255.
+  maxEgressIPsPerNode: {{ .maxEgressIPsPerNode }}
 {{- end }}
 
 # ClusterIP CIDR range for Services. It's required when AntreaProxy is not enabled, and should be
diff --git a/build/charts/antrea/values.yaml b/build/charts/antrea/values.yaml
index 636101e65..82c8b1b83 100644
--- a/build/charts/antrea/values.yaml
+++ b/build/charts/antrea/values.yaml
@@ -107,6 +107,9 @@ ipsec:
 egress:
   # -- CIDR ranges to which outbound Pod traffic will not be SNAT'd by Egresses.
   exceptCIDRs: []
+  # -- The maximum number of Egress IPs that can be assigned to a Node. It's useful when the Node network restricts
+  # the number of secondary IPs a Node can have, e.g. EKS. It must not be greater than 255.
+  maxEgressIPsPerNode: 255
 
 nodePortLocal:
   # -- Enable the NodePortLocal feature.
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 0ebb37f8d..5c04eb545 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -3087,6 +3087,9 @@ data:
     egress:
       # exceptCIDRs is the CIDR ranges to which outbound Pod traffic will not be SNAT'd by Egresses.
       exceptCIDRs:
+      # The maximum number of Egress IPs that can be assigned to a Node. It's useful when the Node network restricts
+      # the number of secondary IPs a Node can have, e.g. EKS. It must not be greater than 255.
+      maxEgressIPsPerNode: 255
 
     # ClusterIP CIDR range for Services. It's required when AntreaProxy is not enabled, and should be
     # set to the same value as the one specified by --service-cluster-ip-range for kube-apiserver. When
@@ -4294,7 +4297,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: a5cdb0e182ac7ccdd59fd7f435fa07bc90d48422fbdc98cdd53359aef80bf59a
+        checksum/config: 01eb4b4048215050463f3b01c05831615c060b02d6f379d6d27a80346185d544
       labels:
         app: antrea
         component: antrea-agent
@@ -4535,7 +4538,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: a5cdb0e182ac7ccdd59fd7f435fa07bc90d48422fbdc98cdd53359aef80bf59a
+        checksum/config: 01eb4b4048215050463f3b01c05831615c060b02d6f379d6d27a80346185d544
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index c9e6231cc..4c4346c36 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -3087,6 +3087,9 @@ data:
     egress:
       # exceptCIDRs is the CIDR ranges to which outbound Pod traffic will not be SNAT'd by Egresses.
       exceptCIDRs:
+      # The maximum number of Egress IPs that can be assigned to a Node. It's useful when the Node network restricts
+      # the number of secondary IPs a Node can have, e.g. EKS. It must not be greater than 255.
+      maxEgressIPsPerNode: 255
 
     # ClusterIP CIDR range for Services. It's required when AntreaProxy is not enabled, and should be
     # set to the same value as the one specified by --service-cluster-ip-range for kube-apiserver. When
@@ -4294,7 +4297,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: a5cdb0e182ac7ccdd59fd7f435fa07bc90d48422fbdc98cdd53359aef80bf59a
+        checksum/config: 01eb4b4048215050463f3b01c05831615c060b02d6f379d6d27a80346185d544
       labels:
         app: antrea
         component: antrea-agent
@@ -4536,7 +4539,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: a5cdb0e182ac7ccdd59fd7f435fa07bc90d48422fbdc98cdd53359aef80bf59a
+        checksum/config: 01eb4b4048215050463f3b01c05831615c060b02d6f379d6d27a80346185d544
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index 08a3ac3ee..567f05626 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -3087,6 +3087,9 @@ data:
     egress:
       # exceptCIDRs is the CIDR ranges to which outbound Pod traffic will not be SNAT'd by Egresses.
       exceptCIDRs:
+      # The maximum number of Egress IPs that can be assigned to a Node. It's useful when the Node network restricts
+      # the number of secondary IPs a Node can have, e.g. EKS. It must not be greater than 255.
+      maxEgressIPsPerNode: 255
 
     # ClusterIP CIDR range for Services. It's required when AntreaProxy is not enabled, and should be
     # set to the same value as the one specified by --service-cluster-ip-range for kube-apiserver. When
@@ -4294,7 +4297,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 0abb3d19aa5b5e3a83d4f46868d66f1904eab0572aed86ae91eb3c0e4d6cb75a
+        checksum/config: aff837005adc6d91f4b5ace3a87c08cfa49e26c60e284ebd234eea34ce5de91f
       labels:
         app: antrea
         component: antrea-agent
@@ -4533,7 +4536,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 0abb3d19aa5b5e3a83d4f46868d66f1904eab0572aed86ae91eb3c0e4d6cb75a
+        checksum/config: aff837005adc6d91f4b5ace3a87c08cfa49e26c60e284ebd234eea34ce5de91f
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index 198be2da5..bb4bb6dca 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -3100,6 +3100,9 @@ data:
     egress:
       # exceptCIDRs is the CIDR ranges to which outbound Pod traffic will not be SNAT'd by Egresses.
       exceptCIDRs:
+      # The maximum number of Egress IPs that can be assigned to a Node. It's useful when the Node network restricts
+      # the number of secondary IPs a Node can have, e.g. EKS. It must not be greater than 255.
+      maxEgressIPsPerNode: 255
 
     # ClusterIP CIDR range for Services. It's required when AntreaProxy is not enabled, and should be
     # set to the same value as the one specified by --service-cluster-ip-range for kube-apiserver. When
@@ -4307,7 +4310,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 995b258674ed96418bc039673e5d725bf0dac26cabd1447bce5a995175f9a652
+        checksum/config: e3e3255bb4f4cd13bce262d8c8d5f4aead3e84e52ea1775c34898c69b80fad33
         checksum/ipsec-secret: d0eb9c52d0cd4311b6d252a951126bf9bea27ec05590bed8a394f0f792dcb2a4
       labels:
         app: antrea
@@ -4592,7 +4595,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 995b258674ed96418bc039673e5d725bf0dac26cabd1447bce5a995175f9a652
+        checksum/config: e3e3255bb4f4cd13bce262d8c8d5f4aead3e84e52ea1775c34898c69b80fad33
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index f4006e944..b7af06e2d 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -3087,6 +3087,9 @@ data:
     egress:
       # exceptCIDRs is the CIDR ranges to which outbound Pod traffic will not be SNAT'd by Egresses.
       exceptCIDRs:
+      # The maximum number of Egress IPs that can be assigned to a Node. It's useful when the Node network restricts
+      # the number of secondary IPs a Node can have, e.g. EKS. It must not be greater than 255.
+      maxEgressIPsPerNode: 255
 
     # ClusterIP CIDR range for Services. It's required when AntreaProxy is not enabled, and should be
     # set to the same value as the one specified by --service-cluster-ip-range for kube-apiserver. When
@@ -4294,7 +4297,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 9d9c26964205adb87ebfdd1eac07428ad82014be6014f70d1a46bf6731e8091d
+        checksum/config: 846220b3b64851cea85fb1e374c3ffdb29376ea729a494dad1cb230b3e5efe8c
       labels:
         app: antrea
         component: antrea-agent
@@ -4533,7 +4536,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 9d9c26964205adb87ebfdd1eac07428ad82014be6014f70d1a46bf6731e8091d
+        checksum/config: 846220b3b64851cea85fb1e374c3ffdb29376ea729a494dad1cb230b3e5efe8c
       labels:
         app: antrea
         component: antrea-controller
diff --git a/ci/jenkins/README.md b/ci/jenkins/README.md
index 9f098d99c..ddc2b8966 100644
--- a/ci/jenkins/README.md
+++ b/ci/jenkins/README.md
@@ -169,7 +169,7 @@ DOCKER_REGISTRY="$(head -n1 ci/docker-registry)"
 
   |  K8s Version |    Node Type    |  Node AMI Family |  Status  |
   | :----------: | :-------------: | :--------------: | :------: |
-  |     1.21     |  EC2 t3.medium  |   AmazonLinux2   |[![Build Status](http://jenkins.antrea-ci.rocks/buildStatus/icon?job=cloud-antrea-eks-conformance-net-policy)](http://jenkins.antrea-ci.rocks/view/cloud/job/cloud-antrea-eks-conformance-net-policy/)|
+  |     1.24     |  EC2 t3.medium  |   AmazonLinux2   |[![Build Status](http://jenkins.antrea-ci.rocks/buildStatus/icon?job=cloud-antrea-eks-conformance-net-policy)](http://jenkins.antrea-ci.rocks/view/cloud/job/cloud-antrea-eks-conformance-net-policy/)|
 
 * [GKE conformance/network policy [bi-daily]](http://jenkins.antrea-ci.rocks/view/cloud/job/cloud-antrea-gke-conformance-net-policy/)
   community tests on GKE cluster using sonobuoy, focusing on "Conformance" and "Feature:NetworkPolicy", skipping the same regexes as in job __conformance__ above.\
@@ -177,7 +177,7 @@ DOCKER_REGISTRY="$(head -n1 ci/docker-registry)"
 
   |  K8s Version   |     Node OS     | VPC Native Mode (on by default) |  Status  |
   | :------------: | :-------------: | :-----------------------------: |:-------: |
-  |    1.21.6      |     Ubuntu      |  On                             |[![Build Status](http://jenkins.antrea-ci.rocks/buildStatus/icon?job=cloud-antrea-gke-conformance-net-policy)](http://jenkins.antrea-ci.rocks/view/cloud/job/cloud-antrea-gke-conformance-net-policy/)|
+  |    1.25.5      |     Ubuntu      |  On                             |[![Build Status](http://jenkins.antrea-ci.rocks/buildStatus/icon?job=cloud-antrea-gke-conformance-net-policy)](http://jenkins.antrea-ci.rocks/view/cloud/job/cloud-antrea-gke-conformance-net-policy/)|
 
 * [AKS conformance/network policy [bi-daily]](http://jenkins.antrea-ci.rocks/view/cloud/job/cloud-antrea-aks-conformance-net-policy/)
   community tests on AKS cluster using sonobuoy, focusing on "Conformance" and "Feature:NetworkPolicy", skipping the same regexes as in job __conformance__ above.\
@@ -185,7 +185,7 @@ DOCKER_REGISTRY="$(head -n1 ci/docker-registry)"
 
   |  K8s Version   |  Node Type          |  Node OS        |  Status  |
   | :------------: | :-----------------: | :-------------: | :------: |
-  |    1.21.7      |  Standard_DS2_v2    |  Ubuntu 18.04   |[![Build Status](http://jenkins.antrea-ci.rocks/buildStatus/icon?job=cloud-antrea-aks-conformance-net-policy)](http://jenkins.antrea-ci.rocks/view/cloud/job/cloud-antrea-aks-conformance-net-policy/)|
+  |    1.24.9      |  Standard_DS2_v2    |  Ubuntu 18.04   |[![Build Status](http://jenkins.antrea-ci.rocks/buildStatus/icon?job=cloud-antrea-aks-conformance-net-policy)](http://jenkins.antrea-ci.rocks/view/cloud/job/cloud-antrea-aks-conformance-net-policy/)|
 
 * [matrix-test [weekly]](https://jenkins.antrea-ci.rocks/job/antrea-weekly-matrix-compatibility-test/):
   runs Antrea e2e, K8s Conformance and NetworkPolicy tests, using different combinations of various operating systems and K8s releases.
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index 180ed8607..3cd581fc8 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -278,6 +278,7 @@ func run(o *Options) error {
 			k8sClient,
 			informerFactory,
 			ofClient,
+			ovsctl.NewClient(o.config.OVSBridge),
 			ovsBridgeClient,
 			routeClient,
 			ifaceStore,
@@ -462,7 +463,7 @@ func run(o *Options) error {
 	if egressEnabled {
 		egressController, err = egress.NewEgressController(
 			ofClient, antreaClientProvider, crdClient, ifaceStore, routeClient, nodeConfig.Name, nodeConfig.NodeTransportInterfaceName,
-			memberlistCluster, egressInformer, podUpdateChannel,
+			memberlistCluster, egressInformer, podUpdateChannel, o.config.Egress.MaxEgressIPsPerNode,
 		)
 		if err != nil {
 			return fmt.Errorf("error creating new Egress controller: %v", err)
diff --git a/cmd/antrea-agent/options.go b/cmd/antrea-agent/options.go
index 578ecc302..b1f09c155 100644
--- a/cmd/antrea-agent/options.go
+++ b/cmd/antrea-agent/options.go
@@ -55,6 +55,7 @@ const (
 	defaultStaleConnectionTimeout  = 5 * time.Minute
 	defaultNPLPortRange            = "61000-62000"
 	defaultNodeType                = config.K8sNode
+	defaultMaxEgressIPsPerNode     = 255
 )
 
 type Options struct {
@@ -409,6 +410,12 @@ func (o *Options) setK8sNodeDefaultOptions() {
 		// Multicluster.EnableGateway.
 		o.config.Multicluster.EnableGateway = true
 	}
+
+	if features.DefaultFeatureGate.Enabled(features.Egress) {
+		if o.config.Egress.MaxEgressIPsPerNode == 0 {
+			o.config.Egress.MaxEgressIPsPerNode = defaultMaxEgressIPsPerNode
+		}
+	}
 }
 
 func (o *Options) validateK8sNodeOptions() error {
@@ -507,6 +514,12 @@ func (o *Options) validateK8sNodeOptions() error {
 		}
 		o.dnsServerOverride = hostPort
 	}
+
+	if features.DefaultFeatureGate.Enabled(features.Egress) {
+		if o.config.Egress.MaxEgressIPsPerNode > defaultMaxEgressIPsPerNode {
+			return fmt.Errorf("maxEgressIPsPerNode cannot be greater than %d", defaultMaxEgressIPsPerNode)
+		}
+	}
 	return nil
 }
 
diff --git a/pkg/agent/controller/egress/egress_controller.go b/pkg/agent/controller/egress/egress_controller.go
index 6bf53a3e3..652c5733a 100644
--- a/pkg/agent/controller/egress/egress_controller.go
+++ b/pkg/agent/controller/egress/egress_controller.go
@@ -68,6 +68,7 @@ const (
 
 	egressIPIndex       = "egressIP"
 	externalIPPoolIndex = "externalIPPool"
+	egressNodeIndex     = "egressNode"
 
 	// egressDummyDevice is the dummy device that holds the Egress IPs configured to the system by antrea-agent.
 	egressDummyDevice = "antrea-egress0"
@@ -75,6 +76,8 @@ const (
 
 var emptyWatch = watch.NewEmptyWatch()
 
+var newIPAssigner = ipassigner.NewIPAssigner
+
 // egressState keeps the actual state of an Egress that has been realized.
 type egressState struct {
 	// The actual egress IP of the Egress. If it's different from the desired IP, there is an update to EgressIP, and we
@@ -141,8 +144,10 @@ type EgressController struct {
 	egressIPStates      map[string]*egressIPState
 	egressIPStatesMutex sync.Mutex
 
-	cluster    *memberlist.Cluster
+	cluster    memberlist.Interface
 	ipAssigner ipassigner.IPAssigner
+
+	maxEgressIPsPerNode int
 }
 
 func NewEgressController(
@@ -153,9 +158,10 @@ func NewEgressController(
 	routeClient route.Interface,
 	nodeName string,
 	nodeTransportInterface string,
-	cluster *memberlist.Cluster,
+	cluster memberlist.Interface,
 	egressInformer crdinformers.EgressInformer,
 	podUpdateSubscriber channel.Subscriber,
+	maxEgressIPsPerNode int,
 ) (*EgressController, error) {
 	c := &EgressController{
 		ofClient:             ofClient,
@@ -175,31 +181,47 @@ func NewEgressController(
 		localIPDetector:      ipassigner.NewLocalIPDetector(),
 		idAllocator:          newIDAllocator(minEgressMark, maxEgressMark),
 		cluster:              cluster,
+		maxEgressIPsPerNode:  maxEgressIPsPerNode,
 	}
-	ipAssigner, err := ipassigner.NewIPAssigner(nodeTransportInterface, egressDummyDevice)
+	ipAssigner, err := newIPAssigner(nodeTransportInterface, egressDummyDevice)
 	if err != nil {
 		return nil, fmt.Errorf("initializing egressIP assigner failed: %v", err)
 	}
 	c.ipAssigner = ipAssigner
 
-	c.egressInformer.AddIndexers(cache.Indexers{egressIPIndex: func(obj interface{}) ([]string, error) {
-		egress, ok := obj.(*crdv1a2.Egress)
-		if !ok {
-			return nil, fmt.Errorf("obj is not Egress: %+v", obj)
-		}
-		return []string{egress.Spec.EgressIP}, nil
-	}})
-	// externalIPPoolIndex will be used to get all Egresses associated with a given ExternalIPPool.
-	c.egressInformer.AddIndexers(cache.Indexers{externalIPPoolIndex: func(obj interface{}) (strings []string, e error) {
-		egress, ok := obj.(*crdv1a2.Egress)
-		if !ok {
-			return nil, fmt.Errorf("obj is not Egress: %+v", obj)
-		}
-		if egress.Spec.ExternalIPPool == "" {
-			return nil, nil
-		}
-		return []string{egress.Spec.ExternalIPPool}, nil
-	}})
+	c.egressInformer.AddIndexers(
+		cache.Indexers{
+			// egressIPIndex will be used to get all Egresses sharing the same Egress IP.
+			egressIPIndex: func(obj interface{}) ([]string, error) {
+				egress, ok := obj.(*crdv1a2.Egress)
+				if !ok {
+					return nil, fmt.Errorf("obj is not Egress: %+v", obj)
+				}
+				return []string{egress.Spec.EgressIP}, nil
+			},
+			// externalIPPoolIndex will be used to get all Egresses associated with a given ExternalIPPool.
+			externalIPPoolIndex: func(obj interface{}) (strings []string, e error) {
+				egress, ok := obj.(*crdv1a2.Egress)
+				if !ok {
+					return nil, fmt.Errorf("obj is not Egress: %+v", obj)
+				}
+				if egress.Spec.ExternalIPPool == "" {
+					return nil, nil
+				}
+				return []string{egress.Spec.ExternalIPPool}, nil
+			},
+			// egressNodeIndex will be used to get all Egresses assigned to a given Node.
+			egressNodeIndex: func(obj interface{}) ([]string, error) {
+				egress, ok := obj.(*crdv1a2.Egress)
+				if !ok {
+					return nil, fmt.Errorf("obj is not Egress: %+v", obj)
+				}
+				if egress.Status.EgressNode == "" {
+					return nil, nil
+				}
+				return []string{egress.Status.EgressNode}, nil
+			},
+		})
 	c.egressInformer.AddEventHandlerWithResyncPeriod(
 		cache.ResourceEventHandlerFuncs{
 			AddFunc:    c.addEgress,
@@ -241,13 +263,11 @@ func (c *EgressController) addEgress(obj interface{}) {
 }
 
 // updateEgress processes Egress UPDATE events.
-func (c *EgressController) updateEgress(old, cur interface{}) {
-	oldEgress := old.(*crdv1a2.Egress)
+func (c *EgressController) updateEgress(_, cur interface{}) {
 	curEgress := cur.(*crdv1a2.Egress)
-	// Ignore handling the Egress Status change if Egress IP already has been assigned on current node.
-	if curEgress.Status.EgressNode == c.nodeName && oldEgress.GetGeneration() == curEgress.GetGeneration() {
-		return
-	}
+	// We need to sync the Egress once even if its spec doesn't change as a Node's EgressIP capacity may be exceeded
+	// when multiple Egresses were processed in parallel and were assigned to the same Node.
+	// Re-sync after status change could correct the assignment eventually.
 	c.queue.Add(curEgress.Name)
 	klog.V(2).InfoS("Processed Egress UPDATE event", "egress", klog.KObj(curEgress))
 }
@@ -628,9 +648,30 @@ func (c *EgressController) syncEgress(egressName string) error {
 		eState = c.newEgressState(egressName, egress.Spec.EgressIP)
 	}
 
-	localNodeSelected, err := c.cluster.ShouldSelectIP(egress.Spec.EgressIP, egress.Spec.ExternalIPPool)
-	if err != nil {
-		return err
+	localNodeSelected := false
+	// Only check whether the Egress IP should be assigned to this Node when ExternalIPPool is set.
+	// If ExternalIPPool is empty, users are responsible for assigning the Egress IPs to Nodes.
+	if egress.Spec.ExternalIPPool != "" {
+		maxEgressIPsFilter := func(node string) bool {
+			// Assuming this Egress IP is assigned to this Node.
+			egressIPsOnNode := sets.NewString(egress.Spec.EgressIP)
+			// Add the Egress IPs that are already assigned to this Node.
+			egressesOnNode, _ := c.egressInformer.GetIndexer().ByIndex(egressNodeIndex, node)
+			for _, obj := range egressesOnNode {
+				egressOnNode := obj.(*crdv1a2.Egress)
+				// We don't count manually managed Egress IPs.
+				if egressOnNode.Spec.ExternalIPPool == "" {
+					continue
+				}
+				egressIPsOnNode.Insert(egressOnNode.Spec.EgressIP)
+			}
+			// Check if this Node can accommodate all Egress IPs.
+			return egressIPsOnNode.Len() <= c.maxEgressIPsPerNode
+		}
+		localNodeSelected, err = c.cluster.ShouldSelectIP(egress.Spec.EgressIP, egress.Spec.ExternalIPPool, maxEgressIPsFilter)
+		if err != nil {
+			return err
+		}
 	}
 	if localNodeSelected {
 		// Ensure the Egress IP is assigned to the system.
diff --git a/pkg/agent/controller/noderoute/node_route_controller.go b/pkg/agent/controller/noderoute/node_route_controller.go
index 7c40d9336..e05779a72 100644
--- a/pkg/agent/controller/noderoute/node_route_controller.go
+++ b/pkg/agent/controller/noderoute/node_route_controller.go
@@ -40,6 +40,7 @@ import (
 	"antrea.io/antrea/pkg/agent/util"
 	"antrea.io/antrea/pkg/agent/wireguard"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
+	"antrea.io/antrea/pkg/ovs/ovsctl"
 	utilip "antrea.io/antrea/pkg/util/ip"
 	"antrea.io/antrea/pkg/util/k8s"
 	"antrea.io/antrea/pkg/util/runtime"
@@ -65,6 +66,7 @@ type Controller struct {
 	kubeClient       clientset.Interface
 	ovsBridgeClient  ovsconfig.OVSBridgeClient
 	ofClient         openflow.Client
+	ovsCtlClient     ovsctl.OVSCtlClient
 	routeClient      route.Interface
 	interfaceStore   interfacestore.InterfaceStore
 	networkConfig    *config.NetworkConfig
@@ -92,6 +94,7 @@ func NewNodeRouteController(
 	kubeClient clientset.Interface,
 	informerFactory informers.SharedInformerFactory,
 	client openflow.Client,
+	ovsCtlClient ovsctl.OVSCtlClient,
 	ovsBridgeClient ovsconfig.OVSBridgeClient,
 	routeClient route.Interface,
 	interfaceStore interfacestore.InterfaceStore,
@@ -107,6 +110,7 @@ func NewNodeRouteController(
 		kubeClient:              kubeClient,
 		ovsBridgeClient:         ovsBridgeClient,
 		ofClient:                client,
+		ovsCtlClient:            ovsCtlClient,
 		routeClient:             routeClient,
 		interfaceStore:          interfaceStore,
 		networkConfig:           networkConfig,
@@ -671,11 +675,6 @@ func (c *Controller) createIPSecTunnelPort(nodeName string, nodeIP net.IP) (int3
 			}
 			c.interfaceStore.DeleteInterface(interfaceConfig)
 			exists = false
-		} else {
-			if interfaceConfig.OFPort != 0 {
-				klog.V(2).InfoS("Found cached IPsec tunnel interface", "node", nodeName, "interface", interfaceConfig.InterfaceName, "port", interfaceConfig.OFPort)
-				return interfaceConfig.OFPort, nil
-			}
 		}
 	}
 	if !exists {
@@ -715,6 +714,11 @@ func (c *Controller) createIPSecTunnelPort(nodeName string, nodeIP net.IP) (int3
 		// Let NodeRouteController retry at errors.
 		return 0, fmt.Errorf("failed to get of_port of IPsec tunnel port for Node %s", nodeName)
 	}
+	// Set the port with no-flood to reject ARP flood packets.
+	if err := c.ovsCtlClient.SetPortNoFlood(int(ofPort)); err != nil {
+		return 0, fmt.Errorf("failed to set port %s with no-flood config: %w", portName, err)
+	}
+
 	interfaceConfig.OFPort = ofPort
 	return ofPort, nil
 }
diff --git a/pkg/agent/ipassigner/ip_assigner_linux.go b/pkg/agent/ipassigner/ip_assigner_linux.go
index 1ed1ab2f2..bd3575f10 100644
--- a/pkg/agent/ipassigner/ip_assigner_linux.go
+++ b/pkg/agent/ipassigner/ip_assigner_linux.go
@@ -51,7 +51,7 @@ type ipAssigner struct {
 }
 
 // NewIPAssigner returns an *ipAssigner.
-func NewIPAssigner(nodeTransportInterface string, dummyDeviceName string) (*ipAssigner, error) {
+func NewIPAssigner(nodeTransportInterface string, dummyDeviceName string) (IPAssigner, error) {
 	ipv4, ipv6, externalInterface, err := util.GetIPNetDeviceByName(nodeTransportInterface)
 	if err != nil {
 		return nil, fmt.Errorf("get IPNetDevice from name %s error: %+v", nodeTransportInterface, err)
diff --git a/pkg/agent/ipassigner/ip_assigner_windows.go b/pkg/agent/ipassigner/ip_assigner_windows.go
index 4b3db923f..252974feb 100644
--- a/pkg/agent/ipassigner/ip_assigner_windows.go
+++ b/pkg/agent/ipassigner/ip_assigner_windows.go
@@ -14,32 +14,8 @@
 
 package ipassigner
 
-import (
-	"k8s.io/apimachinery/pkg/util/sets"
-)
+import "errors"
 
-type ipAssigner struct {
-}
-
-func NewIPAssigner(nodeTransportInterface string, dummyDeviceName string) (*ipAssigner, error) {
-	return nil, nil
-}
-
-func (a *ipAssigner) AssignIP(ip string) error {
-	return nil
-}
-
-func (a *ipAssigner) UnassignIP(ip string) error {
-	return nil
-}
-
-func (a *ipAssigner) AssignedIPs() sets.String {
-	return nil
-}
-
-func (a *ipAssigner) InitIPs(ips sets.String) error {
-	return nil
-}
-
-func (a *ipAssigner) Run(ch <-chan struct{}) {
+func NewIPAssigner(nodeTransportInterface string, dummyDeviceName string) (IPAssigner, error) {
+	return nil, errors.New("IPAssigner is not implemented on Windows")
 }
diff --git a/pkg/agent/memberlist/cluster.go b/pkg/agent/memberlist/cluster.go
index 0b633c5cc..1592d8b83 100644
--- a/pkg/agent/memberlist/cluster.go
+++ b/pkg/agent/memberlist/cluster.go
@@ -83,6 +83,7 @@ var mapNodeEventType = map[memberlist.NodeEventType]nodeEventType{
 type ClusterNodeEventHandler func(objName string)
 
 type Interface interface {
+	ShouldSelectIP(ip string, pool string, filters ...func(node string) bool) (bool, error)
 	SelectNodeForIP(ip, externalIPPool string, filters ...func(string) bool) (string, error)
 	AliveNodes() sets.String
 	AddClusterEventHandler(handler ClusterNodeEventHandler)
@@ -517,9 +518,6 @@ func (c *Cluster) ShouldSelectIP(ip, externalIPPool string, filters ...func(stri
 		return false, fmt.Errorf("local Node consistentHashMap has not synced, ExternalIPPool %s", externalIPPool)
 	}
 	node := consistentHash.GetWithFilters(ip, filters...)
-	if node == "" && len(filters) > 0 {
-		return false, ErrNoNodeAvailable
-	}
 	return node == c.nodeName, nil
 }
 
diff --git a/pkg/config/agent/config.go b/pkg/config/agent/config.go
index 72f5f8d5a..8e9007074 100644
--- a/pkg/config/agent/config.go
+++ b/pkg/config/agent/config.go
@@ -271,6 +271,10 @@ type MulticastConfig struct {
 
 type EgressConfig struct {
 	ExceptCIDRs []string `yaml:"exceptCIDRs,omitempty"`
+	// The maximum number of Egress IPs that can be assigned to a Node. It's useful when the Node network restricts
+	// the number of secondary IPs a Node can have, e.g. EKS. It must not be greater than 255.
+	// Defaults to 255.
+	MaxEgressIPsPerNode int `yaml:"maxEgressIPsPerNode,omitempty"`
 }
 
 type IPsecConfig struct {


Test patch:
diff --git a/ci/test-conformance-eks.sh b/ci/test-conformance-eks.sh
index 5a582cacb..d4ff2fb32 100755
--- a/ci/test-conformance-eks.sh
+++ b/ci/test-conformance-eks.sh
@@ -22,7 +22,7 @@ function echoerr {
 
 CLUSTER=""
 REGION="us-west-2"
-K8S_VERSION="1.21"
+K8S_VERSION="1.24"
 AWS_NODE_TYPE="t3.medium"
 SSH_KEY_PATH="$HOME/.ssh/id_rsa.pub"
 SSH_PRIVATE_KEY_PATH="$HOME/.ssh/id_rsa"
@@ -46,7 +46,7 @@ Setup a EKS cluster to run K8s e2e community tests (Conformance & Network Policy
 
         --cluster-name                The cluster name to be used for the generated EKS cluster. Must be specified if not run in Jenkins environment.
         --kubeconfig                  Path to save kubeconfig of generated EKS cluster.
-        --k8s-version                 GKE K8s cluster version. Defaults to 1.17.
+        --k8s-version                 EKS K8s cluster version. Defaults to 1.24.
         --aws-access-key              AWS Acess Key for logging in to awscli.
         --aws-secret-key              AWS Secret Key for logging in to awscli.
         --aws-service-user-role-arn   AWS Service User Role ARN for logging in to awscli.
diff --git a/pkg/agent/controller/egress/egress_controller_test.go b/pkg/agent/controller/egress/egress_controller_test.go
index 163bd1892..d08a39e6a 100644
--- a/pkg/agent/controller/egress/egress_controller_test.go
+++ b/pkg/agent/controller/egress/egress_controller_test.go
@@ -36,6 +36,7 @@ import (
 	"antrea.io/antrea/pkg/agent/interfacestore"
 	"antrea.io/antrea/pkg/agent/ipassigner"
 	ipassignertest "antrea.io/antrea/pkg/agent/ipassigner/testing"
+	"antrea.io/antrea/pkg/agent/memberlist"
 	openflowtest "antrea.io/antrea/pkg/agent/openflow/testing"
 	routetest "antrea.io/antrea/pkg/agent/route/testing"
 	"antrea.io/antrea/pkg/agent/types"
@@ -86,6 +87,43 @@ func (g *antreaClientGetter) GetAntreaClient() (versioned.Interface, error) {
 	return g.clientset, nil
 }
 
+type fakeSingleNodeCluster struct {
+	node string
+}
+
+func (c *fakeSingleNodeCluster) ShouldSelectIP(ip string, pool string, filters ...func(node string) bool) (bool, error) {
+	selectedNode, err := c.SelectNodeForIP(ip, pool, filters...)
+	if err != nil {
+		return false, err
+	}
+	return selectedNode == c.node, nil
+}
+
+func (c *fakeSingleNodeCluster) SelectNodeForIP(ip, externalIPPool string, filters ...func(string) bool) (string, error) {
+	for _, filter := range filters {
+		if !filter(c.node) {
+			return "", nil
+		}
+	}
+	return c.node, nil
+}
+
+func (c *fakeSingleNodeCluster) AliveNodes() sets.String {
+	return sets.NewString(c.node)
+}
+
+func (c *fakeSingleNodeCluster) AddClusterEventHandler(handler memberlist.ClusterNodeEventHandler) {}
+
+func mockNewIPAssigner(ipAssigner ipassigner.IPAssigner) func() {
+	originalNewIPAssigner := newIPAssigner
+	newIPAssigner = func(_, _ string) (ipassigner.IPAssigner, error) {
+		return ipAssigner, nil
+	}
+	return func() {
+		newIPAssigner = originalNewIPAssigner
+	}
+}
+
 type fakeController struct {
 	*EgressController
 	mockController     *gomock.Controller
@@ -103,13 +141,14 @@ func newFakeController(t *testing.T, initObjects []runtime.Object) *fakeControll
 	mockOFClient := openflowtest.NewMockClient(controller)
 	mockRouteClient := routetest.NewMockInterface(controller)
 	mockIPAssigner := ipassignertest.NewMockIPAssigner(controller)
+	defer mockNewIPAssigner(mockIPAssigner)()
+	mockCluster := &fakeSingleNodeCluster{fakeNode}
 
 	clientset := &fakeversioned.Clientset{}
 	crdClient := fakeversioned.NewSimpleClientset(initObjects...)
 	crdInformerFactory := crdinformers.NewSharedInformerFactory(crdClient, 0)
 	egressInformer := crdInformerFactory.Crd().V1alpha2().Egresses()
 	localIPDetector := &fakeLocalIPDetector{localIPs: sets.NewString(fakeLocalEgressIP1, fakeLocalEgressIP2)}
-	idAllocator := newIDAllocator(minEgressMark, maxEgressMark)
 
 	ifaceStore := interfacestore.NewInterfaceStore()
 	addPodInterface(ifaceStore, "ns1", "pod1", 1)
@@ -119,26 +158,19 @@ func newFakeController(t *testing.T, initObjects []runtime.Object) *fakeControll
 
 	podUpdateChannel := channel.NewSubscribableChannel("PodUpdate", 100)
 
-	egressController := &EgressController{
-		ofClient:             mockOFClient,
-		routeClient:          mockRouteClient,
-		crdClient:            crdClient,
-		antreaClientProvider: &antreaClientGetter{clientset},
-		egressInformer:       egressInformer.Informer(),
-		egressLister:         egressInformer.Lister(),
-		egressListerSynced:   egressInformer.Informer().HasSynced,
-		queue:                workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "egressgroup"),
-		localIPDetector:      localIPDetector,
-		ifaceStore:           ifaceStore,
-		nodeName:             fakeNode,
-		idAllocator:          idAllocator,
-		egressGroups:         map[string]sets.String{},
-		egressBindings:       map[string]*egressBinding{},
-		egressStates:         map[string]*egressState{},
-		egressIPStates:       map[string]*egressIPState{},
-		ipAssigner:           mockIPAssigner,
-	}
-	podUpdateChannel.Subscribe(egressController.processPodUpdate)
+	egressController, _ := NewEgressController(mockOFClient,
+		&antreaClientGetter{clientset},
+		crdClient,
+		ifaceStore,
+		mockRouteClient,
+		fakeNode,
+		"eth0",
+		mockCluster,
+		egressInformer,
+		podUpdateChannel,
+		255,
+	)
+	egressController.localIPDetector = localIPDetector
 	return &fakeController{
 		EgressController:   egressController,
 		mockController:     controller,
@@ -154,6 +186,7 @@ func newFakeController(t *testing.T, initObjects []runtime.Object) *fakeControll
 func TestSyncEgress(t *testing.T) {
 	tests := []struct {
 		name                string
+		maxEgressIPsPerNode int
 		existingEgress      *crdv1a2.Egress
 		newEgress           *crdv1a2.Egress
 		existingEgressGroup *cpv1b2.EgressGroup
@@ -504,12 +537,111 @@ func TestSyncEgress(t *testing.T) {
 				mockIPAssigner.EXPECT().UnassignIP(fakeLocalEgressIP1).Times(3)
 			},
 		},
+		{
+			name:                "Not exceed maxEgressIPsPerNode",
+			maxEgressIPsPerNode: 1,
+			// It's on this Node but doesn't occupy the quota as it's not allocated from an ExternalIPPool.
+			existingEgress: &crdv1a2.Egress{
+				ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
+				Spec:       crdv1a2.EgressSpec{EgressIP: fakeLocalEgressIP1},
+			},
+			newEgress: &crdv1a2.Egress{
+				ObjectMeta: metav1.ObjectMeta{Name: "egressB", UID: "uidB"},
+				Spec:       crdv1a2.EgressSpec{EgressIP: fakeLocalEgressIP2, ExternalIPPool: "external-ip-pool"},
+			},
+			existingEgressGroup: &cpv1b2.EgressGroup{
+				ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
+				GroupMembers: []cpv1b2.GroupMember{
+					{Pod: &cpv1b2.PodReference{Name: "pod1", Namespace: "ns1"}},
+					{Pod: &cpv1b2.PodReference{Name: "pod2", Namespace: "ns2"}},
+				},
+			},
+			newEgressGroup: &cpv1b2.EgressGroup{
+				ObjectMeta: metav1.ObjectMeta{Name: "egressB", UID: "uidB"},
+				GroupMembers: []cpv1b2.GroupMember{
+					{Pod: &cpv1b2.PodReference{Name: "pod3", Namespace: "ns3"}},
+				},
+			},
+			expectedEgresses: []*crdv1a2.Egress{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
+					Spec:       crdv1a2.EgressSpec{EgressIP: fakeLocalEgressIP1},
+					Status:     crdv1a2.EgressStatus{EgressNode: fakeNode},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "egressB", UID: "uidB"},
+					Spec:       crdv1a2.EgressSpec{EgressIP: fakeLocalEgressIP2, ExternalIPPool: "external-ip-pool"},
+					Status:     crdv1a2.EgressStatus{EgressNode: fakeNode},
+				},
+			},
+			expectedCalls: func(mockOFClient *openflowtest.MockClient, mockRouteClient *routetest.MockInterface, mockIPAssigner *ipassignertest.MockIPAssigner) {
+				mockIPAssigner.EXPECT().UnassignIP(fakeLocalEgressIP1)
+				mockOFClient.EXPECT().InstallSNATMarkFlows(net.ParseIP(fakeLocalEgressIP1), uint32(1))
+				mockOFClient.EXPECT().InstallPodSNATFlows(uint32(1), net.ParseIP(fakeLocalEgressIP1), uint32(1))
+				mockOFClient.EXPECT().InstallPodSNATFlows(uint32(2), net.ParseIP(fakeLocalEgressIP1), uint32(1))
+				mockRouteClient.EXPECT().AddSNATRule(net.ParseIP(fakeLocalEgressIP1), uint32(1))
+				mockIPAssigner.EXPECT().AssignIP(fakeLocalEgressIP2).Times(2)
+				mockOFClient.EXPECT().InstallSNATMarkFlows(net.ParseIP(fakeLocalEgressIP2), uint32(2))
+				mockOFClient.EXPECT().InstallPodSNATFlows(uint32(3), net.ParseIP(fakeLocalEgressIP2), uint32(2))
+				mockRouteClient.EXPECT().AddSNATRule(net.ParseIP(fakeLocalEgressIP2), uint32(2))
+			},
+		},
+		{
+			name:                "Exceed maxEgressIPsPerNode",
+			maxEgressIPsPerNode: 1,
+			existingEgress: &crdv1a2.Egress{
+				ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
+				Spec:       crdv1a2.EgressSpec{EgressIP: fakeLocalEgressIP1, ExternalIPPool: "external-ip-pool"},
+			},
+			newEgress: &crdv1a2.Egress{
+				ObjectMeta: metav1.ObjectMeta{Name: "egressB", UID: "uidB"},
+				Spec:       crdv1a2.EgressSpec{EgressIP: fakeRemoteEgressIP1, ExternalIPPool: "external-ip-pool"},
+			},
+			existingEgressGroup: &cpv1b2.EgressGroup{
+				ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
+				GroupMembers: []cpv1b2.GroupMember{
+					{Pod: &cpv1b2.PodReference{Name: "pod1", Namespace: "ns1"}},
+					{Pod: &cpv1b2.PodReference{Name: "pod2", Namespace: "ns2"}},
+				},
+			},
+			newEgressGroup: &cpv1b2.EgressGroup{
+				ObjectMeta: metav1.ObjectMeta{Name: "egressB", UID: "uidB"},
+				GroupMembers: []cpv1b2.GroupMember{
+					{Pod: &cpv1b2.PodReference{Name: "pod3", Namespace: "ns3"}},
+				},
+			},
+			expectedEgresses: []*crdv1a2.Egress{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "egressA", UID: "uidA"},
+					Spec:       crdv1a2.EgressSpec{EgressIP: fakeLocalEgressIP1, ExternalIPPool: "external-ip-pool"},
+					Status:     crdv1a2.EgressStatus{EgressNode: fakeNode},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "egressB", UID: "uidB"},
+					Spec:       crdv1a2.EgressSpec{EgressIP: fakeRemoteEgressIP1, ExternalIPPool: "external-ip-pool"},
+					Status:     crdv1a2.EgressStatus{EgressNode: ""},
+				},
+			},
+			expectedCalls: func(mockOFClient *openflowtest.MockClient, mockRouteClient *routetest.MockInterface, mockIPAssigner *ipassignertest.MockIPAssigner) {
+				mockIPAssigner.EXPECT().AssignIP(fakeLocalEgressIP1)
+				mockOFClient.EXPECT().InstallSNATMarkFlows(net.ParseIP(fakeLocalEgressIP1), uint32(1))
+				mockOFClient.EXPECT().InstallPodSNATFlows(uint32(1), net.ParseIP(fakeLocalEgressIP1), uint32(1))
+				mockOFClient.EXPECT().InstallPodSNATFlows(uint32(2), net.ParseIP(fakeLocalEgressIP1), uint32(1))
+				mockRouteClient.EXPECT().AddSNATRule(net.ParseIP(fakeLocalEgressIP1), uint32(1))
+				mockIPAssigner.EXPECT().UnassignIP(fakeRemoteEgressIP1).Times(2)
+				mockOFClient.EXPECT().InstallPodSNATFlows(uint32(3), net.ParseIP(fakeRemoteEgressIP1), uint32(0))
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			c := newFakeController(t, []runtime.Object{tt.existingEgress})
 			defer c.mockController.Finish()
 
+			if tt.maxEgressIPsPerNode > 0 {
+				c.maxEgressIPsPerNode = tt.maxEgressIPsPerNode
+			}
+
 			stopCh := make(chan struct{})
 			defer close(stopCh)
 			c.crdInformerFactory.Start(stopCh)
@@ -642,14 +774,7 @@ func TestSyncOverlappingEgress(t *testing.T) {
 	c.addEgressGroup(egressGroup1)
 	c.addEgressGroup(egressGroup2)
 	c.addEgressGroup(egressGroup3)
-	require.Equal(t, 3, c.queue.Len())
-	// Drain the queue.
-	item, _ := c.queue.Get()
-	c.queue.Done(item)
-	item, _ = c.queue.Get()
-	c.queue.Done(item)
-	item, _ = c.queue.Get()
-	c.queue.Done(item)
+	checkQueueItemExistence(t, c.queue, egress1.Name, egress2.Name, egress3.Name)
 
 	c.mockOFClient.EXPECT().InstallSNATMarkFlows(net.ParseIP(fakeLocalEgressIP1), uint32(1))
 	c.mockOFClient.EXPECT().InstallPodSNATFlows(uint32(1), net.ParseIP(fakeLocalEgressIP1), uint32(1))
@@ -671,6 +796,9 @@ func TestSyncOverlappingEgress(t *testing.T) {
 	err = c.syncEgress(egress3.Name)
 	assert.NoError(t, err)
 
+	// egress1 and egress3 are expected to be triggered for resync because their status is updated.
+	checkQueueItemExistence(t, c.queue, egress1.Name, egress3.Name)
+
 	// After deleting egress1, pod1 and pod2 no longer enforces egress1. The Egress IP shouldn't be released as egress3
 	// is still referring to it.
 	// egress2 and egress3 are expected to be triggered for resync.
@@ -681,18 +809,11 @@ func TestSyncOverlappingEgress(t *testing.T) {
 		_, err := c.egressLister.Get(egress1.Name)
 		return err != nil, nil
 	}))
+	checkQueueItemExistence(t, c.queue, egress1.Name)
 	c.mockIPAssigner.EXPECT().UnassignIP(fakeLocalEgressIP1)
 	err = c.syncEgress(egress1.Name)
 	assert.NoError(t, err)
-	require.Equal(t, 2, c.queue.Len())
-	var pendingItems []string
-	item, _ = c.queue.Get()
-	c.queue.Done(item)
-	pendingItems = append(pendingItems, item.(string))
-	item, _ = c.queue.Get()
-	c.queue.Done(item)
-	pendingItems = append(pendingItems, item.(string))
-	assert.ElementsMatch(t, []string{egress2.Name, egress3.Name}, pendingItems)
+	checkQueueItemExistence(t, c.queue, egress2.Name, egress3.Name)
 
 	// pod1 is expected to enforce egress2.
 	c.mockOFClient.EXPECT().InstallPodSNATFlows(uint32(1), net.ParseIP(fakeRemoteEgressIP1), uint32(0))
@@ -715,6 +836,7 @@ func TestSyncOverlappingEgress(t *testing.T) {
 		_, err := c.egressLister.Get(egress2.Name)
 		return err != nil, nil
 	}))
+	checkQueueItemExistence(t, c.queue, egress2.Name)
 	err = c.syncEgress(egress2.Name)
 	assert.NoError(t, err)
 	require.Equal(t, 0, c.queue.Len())
@@ -730,6 +852,7 @@ func TestSyncOverlappingEgress(t *testing.T) {
 		_, err := c.egressLister.Get(egress3.Name)
 		return err != nil, nil
 	}))
+	checkQueueItemExistence(t, c.queue, egress3.Name)
 	err = c.syncEgress(egress3.Name)
 	assert.NoError(t, err)
 	require.Equal(t, 0, c.queue.Len())
@@ -950,3 +1073,18 @@ func TestGetEgressIPByMark(t *testing.T) {
 		})
 	}
 }
+
+func checkQueueItemExistence(t *testing.T, queue workqueue.RateLimitingInterface, items ...string) {
+	t.Logf("queue len %d", queue.Len())
+	require.Eventually(t, func() bool {
+		return len(items) == queue.Len()
+	}, time.Second, 10*time.Millisecond, "Didn't find enough items in the queue")
+	expectedItems := sets.NewString(items...)
+	actualItems := sets.NewString()
+	for i := 0; i < len(expectedItems); i++ {
+		key, _ := queue.Get()
+		actualItems.Insert(key.(string))
+		queue.Done(key)
+	}
+	assert.Equal(t, expectedItems, actualItems)
+}
diff --git a/pkg/agent/controller/networkpolicy/fqdn_test.go b/pkg/agent/controller/networkpolicy/fqdn_test.go
index 2989556c5..85fdb0de3 100644
--- a/pkg/agent/controller/networkpolicy/fqdn_test.go
+++ b/pkg/agent/controller/networkpolicy/fqdn_test.go
@@ -16,6 +16,7 @@ package networkpolicy
 
 import (
 	"context"
+	"net"
 	"testing"
 	"time"
 
@@ -305,3 +306,84 @@ func TestLookupIPFallback(t *testing.T) {
 	err := f.lookupIP(ctx, "www.google.com")
 	require.NoError(t, err, "Error when resolving name")
 }
+
+func TestString(t *testing.T) {
+	tests := []struct {
+		name           string
+		selectorItem   *fqdnSelectorItem
+		expectedOutput string
+	}{
+		{
+			name: "matching the regex",
+			selectorItem: &fqdnSelectorItem{
+				matchRegex: "^.*antrea[.]io$",
+			},
+			expectedOutput: "matchRegex:^.*antrea[.]io$",
+		},
+		{
+			name: "matching the name",
+			selectorItem: &fqdnSelectorItem{
+				matchName: "test.antrea.io",
+			},
+			expectedOutput: "matchName:test.antrea.io",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			gotOutput := tc.selectorItem.String()
+			assert.Equal(t, tc.expectedOutput, gotOutput)
+		})
+	}
+}
+
+func TestGetIPsForFQDNSelectors(t *testing.T) {
+	selectorItem := fqdnSelectorItem{
+		matchName: "test.antrea.io",
+	}
+	tests := []struct {
+		name                       string
+		fqdns                      []string
+		existingSelectorItemToFQDN map[fqdnSelectorItem]sets.String
+		existingDNSCache           map[string]dnsMeta
+		expectedMatchedIPs         []net.IP
+	}{
+		{
+			name:  "matched ip found",
+			fqdns: []string{"test.antrea.io"},
+			existingSelectorItemToFQDN: map[fqdnSelectorItem]sets.String{
+				selectorItem: sets.NewString("test.antrea.io"),
+			},
+			existingDNSCache: map[string]dnsMeta{
+				"test.antrea.io": {
+					responseIPs: map[string]net.IP{
+						"127.0.0.1":    net.ParseIP("127.0.0.1"),
+						"192.155.12.1": net.ParseIP("192.155.12.1"),
+						"192.158.1.38": net.ParseIP("192.158.1.38"),
+					},
+				},
+			},
+			expectedMatchedIPs: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("192.155.12.1"), net.ParseIP("192.158.1.38")},
+		},
+		{
+			name:               "no matched ip",
+			fqdns:              []string{"^.*antrea[.]io$"},
+			expectedMatchedIPs: nil,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			controller := gomock.NewController(t)
+			f, _ := newMockFQDNController(t, controller, nil)
+			if tc.existingSelectorItemToFQDN != nil {
+				f.selectorItemToFQDN = tc.existingSelectorItemToFQDN
+			}
+			if tc.existingDNSCache != nil {
+				f.dnsEntryCache = tc.existingDNSCache
+			}
+			gotOutput := f.getIPsForFQDNSelectors(tc.fqdns)
+			assert.ElementsMatch(t, tc.expectedMatchedIPs, gotOutput)
+		})
+	}
+}
diff --git a/pkg/agent/controller/noderoute/node_route_controller_test.go b/pkg/agent/controller/noderoute/node_route_controller_test.go
index fd70d2042..dbfe9a995 100644
--- a/pkg/agent/controller/noderoute/node_route_controller_test.go
+++ b/pkg/agent/controller/noderoute/node_route_controller_test.go
@@ -35,6 +35,7 @@ import (
 	"antrea.io/antrea/pkg/agent/util"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
 	ovsconfigtest "antrea.io/antrea/pkg/ovs/ovsconfig/testing"
+	ovsctltest "antrea.io/antrea/pkg/ovs/ovsctl/testing"
 	utilip "antrea.io/antrea/pkg/util/ip"
 )
 
@@ -58,6 +59,7 @@ type fakeController struct {
 	ovsClient       *ovsconfigtest.MockOVSBridgeClient
 	routeClient     *routetest.MockInterface
 	interfaceStore  interfacestore.InterfaceStore
+	ovsCtlClient    *ovsctltest.MockOVSCtlClient
 }
 
 type fakeIPsecCertificateManager struct{}
@@ -75,7 +77,9 @@ func newController(t *testing.T, networkConfig *config.NetworkConfig) (*fakeCont
 	routeClient := routetest.NewMockInterface(ctrl)
 	interfaceStore := interfacestore.NewInterfaceStore()
 	ipsecCertificateManager := &fakeIPsecCertificateManager{}
-	c := NewNodeRouteController(clientset, informerFactory, ofClient, ovsClient, routeClient, interfaceStore, networkConfig, &config.NodeConfig{GatewayConfig: &config.GatewayConfig{
+	ovsCtlClient := ovsctltest.NewMockOVSCtlClient(ctrl)
+
+	c := NewNodeRouteController(clientset, informerFactory, ofClient, ovsCtlClient, ovsClient, routeClient, interfaceStore, networkConfig, &config.NodeConfig{GatewayConfig: &config.GatewayConfig{
 		IPv4: nil,
 		MAC:  gatewayMAC,
 	}}, nil, false, ipsecCertificateManager)
@@ -86,6 +90,7 @@ func newController(t *testing.T, networkConfig *config.NetworkConfig) (*fakeCont
 		ofClient:        ofClient,
 		ovsClient:       ovsClient,
 		routeClient:     routeClient,
+		ovsCtlClient:    ovsCtlClient,
 		interfaceStore:  interfaceStore,
 	}, ctrl.Finish
 }
@@ -339,6 +344,7 @@ func TestCreateIPSecTunnelPortPSK(t *testing.T) {
 
 	node1PortName := util.GenerateNodeTunnelInterfaceName("xyz-k8s-0-1")
 	node2PortName := util.GenerateNodeTunnelInterfaceName("xyz-k8s-0-2")
+	node3PortName := util.GenerateNodeTunnelInterfaceName("xyz-k8s-0-3")
 	c.ovsClient.EXPECT().CreateTunnelPortExt(
 		node1PortName, ovsconfig.TunnelType("vxlan"), int32(0),
 		false, "", nodeIP1.String(), "", "changeme", nil,
@@ -348,7 +354,11 @@ func TestCreateIPSecTunnelPortPSK(t *testing.T) {
 		false, "", nodeIP2.String(), "", "changeme", nil,
 		map[string]interface{}{ovsExternalIDNodeName: "xyz-k8s-0-2"}).Times(1)
 	c.ovsClient.EXPECT().GetOFPort(node1PortName, false).Return(int32(1), nil)
+	c.ovsCtlClient.EXPECT().SetPortNoFlood(1)
 	c.ovsClient.EXPECT().GetOFPort(node2PortName, false).Return(int32(2), nil)
+	c.ovsCtlClient.EXPECT().SetPortNoFlood(2)
+	c.ovsClient.EXPECT().GetOFPort(node3PortName, false).Return(int32(5), nil)
+	c.ovsCtlClient.EXPECT().SetPortNoFlood(5)
 	c.ovsClient.EXPECT().DeletePort("123").Times(1)
 
 	tests := []struct {
@@ -407,6 +417,7 @@ func TestCreateIPSecTunnelPortCert(t *testing.T) {
 		false, "", nodeIP1.String(), "xyz-k8s-0-1", "", nil,
 		map[string]interface{}{ovsExternalIDNodeName: "xyz-k8s-0-1"}).Times(1)
 	c.ovsClient.EXPECT().GetOFPort(node1PortName, false).Return(int32(1), nil)
+	c.ovsCtlClient.EXPECT().SetPortNoFlood(1)
 
 	tests := []struct {
 		name       string
diff --git a/pkg/agent/controller/serviceexternalip/controller_test.go b/pkg/agent/controller/serviceexternalip/controller_test.go
index ea24b84fc..555756977 100644
--- a/pkg/agent/controller/serviceexternalip/controller_test.go
+++ b/pkg/agent/controller/serviceexternalip/controller_test.go
@@ -101,6 +101,10 @@ func (f *fakeMemberlistCluster) SelectNodeForIP(ip, externalIPPool string, filte
 	return selectNode, nil
 }
 
+func (f *fakeMemberlistCluster) ShouldSelectIP(ip string, pool string, filters ...func(node string) bool) (bool, error) {
+	return false, nil
+}
+
 type fakeController struct {
 	*ServiceExternalIPController
 	mockController        *gomock.Controller