Repository: antrea-io/antrea
Pull request #3235 (2022-01-24 03:19:29)

Update workflows for MC
The core of multicluster is in main branch now, so no need to keep
the feature branch, we will submit new PR to main in the future
directly.

Signed-off-by: Lan Luo <luola@vmware.com>

codecov-commenter: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/3235?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#3235](https://codecov.io/gh/antrea-io/antrea/pull/3235?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (8540e85) into [main](https://codecov.io/gh/antrea-io/antrea/commit/df6094bd0870a763d0d63b4fc69738a5370aa0de?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (df6094b) will **decrease** coverage by `24.70%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/antrea-io/antrea/pull/3235/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/3235?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@             Coverage Diff             @@
##             main    #3235       +/-   ##
===========================================
- Coverage   58.76%   34.05%   -24.71%     
===========================================
  Files         331      131      -200     
  Lines       28437    15555    -12882     
===========================================
- Hits        16711     5298    -11413     
+ Misses       9894     9770      -124     
+ Partials     1832      487     -1345     
```

| Flag | Coverage Δ | |
|---|---|---|
| integration-tests | `34.05% <ø> (?)` | |
| kind-e2e-tests | `?` | |
| unit-tests | `?` | |

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/3235?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [pkg/controller/egress/validate.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2NvbnRyb2xsZXIvZWdyZXNzL3ZhbGlkYXRlLmdv) | | |
| [...controller/crdmirroring/crdhandler/clustergroup.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2NvbnRyb2xsZXIvY3JkbWlycm9yaW5nL2NyZGhhbmRsZXIvY2x1c3Rlcmdyb3VwLmdv) | | |
| [...egator/apiserver/handlers/recordmetrics/handler.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2Zsb3dhZ2dyZWdhdG9yL2FwaXNlcnZlci9oYW5kbGVycy9yZWNvcmRtZXRyaWNzL2hhbmRsZXIuZ28=) | | |
| [pkg/controller/networkpolicy/clustergroup.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2NvbnRyb2xsZXIvbmV0d29ya3BvbGljeS9jbHVzdGVyZ3JvdXAuZ28=) | | |
| [.../legacyapis/clusterinformation/v1beta1/register.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2xlZ2FjeWFwaXMvY2x1c3RlcmluZm9ybWF0aW9uL3YxYmV0YTEvcmVnaXN0ZXIuZ28=) | | |
| [pkg/controller/egress/store/egressgroup.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2NvbnRyb2xsZXIvZWdyZXNzL3N0b3JlL2VncmVzc2dyb3VwLmdv) | | |
| [pkg/agent/multicast/mcast\_discovery.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2FnZW50L211bHRpY2FzdC9tY2FzdF9kaXNjb3ZlcnkuZ28=) | | |
| [pkg/util/logdir/logdir.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL3V0aWwvbG9nZGlyL2xvZ2Rpci5nbw==) | | |
| [pkg/controller/networkpolicy/mutate.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2NvbnRyb2xsZXIvbmV0d29ya3BvbGljeS9tdXRhdGUuZ28=) | | |
| [pkg/controller/networkpolicy/crd\_utils.go](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL2NvbnRyb2xsZXIvbmV0d29ya3BvbGljeS9jcmRfdXRpbHMuZ28=) | | |
| ... and [452 more](https://codecov.io/gh/antrea-io/antrea/pull/3235/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b70493048..dd6fdc760 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,14 +5,10 @@ on:
     branches:
       - main
       - release-*
-      # TODO: remove this before merging feature branch to main
-      - feature/multi-cluster
   push:
     branches:
       - main
       - release-*
-      # TODO: remove this before merging feature branch to main
-      - feature/multi-cluster
 
 jobs:
   check-changes:
@@ -120,7 +116,7 @@ jobs:
     - name: Build antrea-mc-controller Docker image
       run: make antrea-mc-controller
     - name: Push antrea-mc-controller Docker image to registry
-      if: ${{ github.repository == 'antrea-io/antrea' && github.event_name == 'push' && github.ref == 'refs/heads/feature/multi-cluster' }}
+      if: ${{ github.repository == 'antrea-io/antrea' && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
       env:
         DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
         DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index ec2f50fd1..970d42b2d 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -4,12 +4,10 @@ on:
     branches:
     - main
     - release-*
-    - feature/multi-cluster
   push:
     branches:
     - main
     - release-*
-    - feature/multi-cluster
 
 env:
   go-cache-name: go
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 156b3a7e4..60ce7aca9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -227,7 +227,11 @@ do:
 
 The second step will compile the Antrea code in a `golang` container, and build
 a `Ubuntu 20.04` Docker image that includes all the generated binaries. [`Docker`](https://docs.docker.com/install)
-must be installed on your local machine in advance.
+must be installed on your local machine in advance. If you are a macOS user and
+cannot use [Docker Desktop](https://www.docker.com/products/docker-desktop) to
+contribute to Antrea for licensing reasons, check out this
+[document](docs/contributors/docker-desktop-alternatives.md) for possible
+alternatives.
 
 Alternatively, you can build the Antrea code in your local Go environment. The
 Antrea project uses the [Go modules support](https://github.com/golang/go/wiki/Modules) which was introduced in Go 1.11. It
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 37158f849..3e9360705 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -4037,6 +4037,11 @@ data:
     # Enable multicast traffic. This feature is supported only with noEncap mode.
     #  Multicast: false
 
+    # Enable support for provisioning secondary network interfaces for Pods (using
+    # Pod annotations). At the moment, Antrea can only create secondary network
+    # interfaces using SR-IOV VFs on baremetal Nodes.
+    #  SecondaryNetwork: false
+
     # Enable managing external IPs of Services of LoadBalancer type.
     #  ServiceExternalIP: false
 
@@ -4333,7 +4338,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-t4d76tttk7
+  name: antrea-config-7gm92g46hf
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4404,7 +4409,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-t4d76tttk7
+          value: antrea-config-7gm92g46hf
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4455,7 +4460,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-t4d76tttk7
+          name: antrea-config-7gm92g46hf
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4736,7 +4741,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-t4d76tttk7
+          name: antrea-config-7gm92g46hf
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index 41f3ee075..27b17cd63 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -4037,6 +4037,11 @@ data:
     # Enable multicast traffic. This feature is supported only with noEncap mode.
     #  Multicast: false
 
+    # Enable support for provisioning secondary network interfaces for Pods (using
+    # Pod annotations). At the moment, Antrea can only create secondary network
+    # interfaces using SR-IOV VFs on baremetal Nodes.
+    #  SecondaryNetwork: false
+
     # Enable managing external IPs of Services of LoadBalancer type.
     #  ServiceExternalIP: false
 
@@ -4333,7 +4338,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-t4d76tttk7
+  name: antrea-config-7gm92g46hf
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4404,7 +4409,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-t4d76tttk7
+          value: antrea-config-7gm92g46hf
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4455,7 +4460,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-t4d76tttk7
+          name: antrea-config-7gm92g46hf
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4738,7 +4743,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-t4d76tttk7
+          name: antrea-config-7gm92g46hf
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index 09ded9d69..50e984b40 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -4037,6 +4037,11 @@ data:
     # Enable multicast traffic. This feature is supported only with noEncap mode.
     #  Multicast: false
 
+    # Enable support for provisioning secondary network interfaces for Pods (using
+    # Pod annotations). At the moment, Antrea can only create secondary network
+    # interfaces using SR-IOV VFs on baremetal Nodes.
+    #  SecondaryNetwork: false
+
     # Enable managing external IPs of Services of LoadBalancer type.
     #  ServiceExternalIP: false
 
@@ -4333,7 +4338,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-b696bmff84
+  name: antrea-config-hcg89gk472
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4404,7 +4409,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-b696bmff84
+          value: antrea-config-hcg89gk472
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4455,7 +4460,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-b696bmff84
+          name: antrea-config-hcg89gk472
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4739,7 +4744,7 @@ spec:
           path: /home/kubernetes/bin
         name: host-cni-bin
       - configMap:
-          name: antrea-config-b696bmff84
+          name: antrea-config-hcg89gk472
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index 699821483..29a6409db 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -4037,6 +4037,11 @@ data:
     # Enable multicast traffic. This feature is supported only with noEncap mode.
     #  Multicast: false
 
+    # Enable support for provisioning secondary network interfaces for Pods (using
+    # Pod annotations). At the moment, Antrea can only create secondary network
+    # interfaces using SR-IOV VFs on baremetal Nodes.
+    #  SecondaryNetwork: false
+
     # Enable managing external IPs of Services of LoadBalancer type.
     #  ServiceExternalIP: false
 
@@ -4338,7 +4343,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-5fm86722mm
+  name: antrea-config-h7767t7cfh
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4418,7 +4423,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-5fm86722mm
+          value: antrea-config-h7767t7cfh
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4469,7 +4474,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-5fm86722mm
+          name: antrea-config-h7767t7cfh
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4785,7 +4790,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-5fm86722mm
+          name: antrea-config-h7767t7cfh
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-kind.yml b/build/yamls/antrea-kind.yml
index 561367286..2b8187468 100644
--- a/build/yamls/antrea-kind.yml
+++ b/build/yamls/antrea-kind.yml
@@ -4037,6 +4037,11 @@ data:
     # Enable multicast traffic. This feature is supported only with noEncap mode.
     #  Multicast: false
 
+    # Enable support for provisioning secondary network interfaces for Pods (using
+    # Pod annotations). At the moment, Antrea can only create secondary network
+    # interfaces using SR-IOV VFs on baremetal Nodes.
+    #  SecondaryNetwork: false
+
     # Enable managing external IPs of Services of LoadBalancer type.
     #  ServiceExternalIP: false
 
@@ -4338,7 +4343,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-65d688467c
+  name: antrea-config-kkmb8ft29d
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4409,7 +4414,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-65d688467c
+          value: antrea-config-kkmb8ft29d
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4460,7 +4465,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-65d688467c
+          name: antrea-config-kkmb8ft29d
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4737,7 +4742,7 @@ spec:
           type: CharDevice
         name: dev-tun
       - configMap:
-          name: antrea-config-65d688467c
+          name: antrea-config-kkmb8ft29d
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index f0258e4e2..afb487f30 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -4037,6 +4037,11 @@ data:
     # Enable multicast traffic. This feature is supported only with noEncap mode.
     #  Multicast: false
 
+    # Enable support for provisioning secondary network interfaces for Pods (using
+    # Pod annotations). At the moment, Antrea can only create secondary network
+    # interfaces using SR-IOV VFs on baremetal Nodes.
+    #  SecondaryNetwork: false
+
     # Enable managing external IPs of Services of LoadBalancer type.
     #  ServiceExternalIP: false
 
@@ -4338,7 +4343,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-2k82d6f6t4
+  name: antrea-config-k7tg4t4t8h
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4409,7 +4414,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-2k82d6f6t4
+          value: antrea-config-k7tg4t4t8h
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4460,7 +4465,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-2k82d6f6t4
+          name: antrea-config-k7tg4t4t8h
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4741,7 +4746,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-2k82d6f6t4
+          name: antrea-config-k7tg4t4t8h
         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 38fee3d10..21fb3c61d 100644
--- a/build/yamls/base/conf/antrea-agent.conf
+++ b/build/yamls/base/conf/antrea-agent.conf
@@ -38,6 +38,11 @@ featureGates:
 # Enable multicast traffic. This feature is supported only with noEncap mode.
 #  Multicast: false
 
+# Enable support for provisioning secondary network interfaces for Pods (using
+# Pod annotations). At the moment, Antrea can only create secondary network
+# interfaces using SR-IOV VFs on baremetal Nodes.
+#  SecondaryNetwork: false
+
 # Enable managing external IPs of Services of LoadBalancer type.
 #  ServiceExternalIP: false
 
diff --git a/ci/jenkins/jobs/job-templates.yaml b/ci/jenkins/jobs/job-templates.yaml
index f89cef368..256e05281 100644
--- a/ci/jenkins/jobs/job-templates.yaml
+++ b/ci/jenkins/jobs/job-templates.yaml
@@ -47,6 +47,55 @@
         started-status: '{started_status}'
     wrappers: '{wrappers}'
 
+- job-template:
+    name: '{name}-{test_name}-for-pr'
+    node: '{node}'
+    block-downstream: false
+    block-upstream: false
+    builders: '{builders}'
+    concurrent: false
+    description: '{description}'
+    project-type: freestyle
+    properties:
+    - build-discarder:
+        artifact-days-to-keep: -1
+        artifact-num-to-keep: -1
+        days-to-keep: 7
+        num-to-keep: 30
+    - github:
+        url: 'https://github.com/{org_repo}'
+    publishers: '{publishers}'
+    scm:
+    - git:
+        branches: '{branches}'
+        credentials-id: '{git_credentials_id}'
+        name: origin
+        refspec: +refs/heads/*:refs/remotes/origin/* +refs/pull/${{ghprbPullId}}/*:refs/remotes/origin/pr/${{ghprbPullId}}/*
+        url: 'https://github.com/{org_repo}'
+        wipe-workspace: true
+    triggers:
+    - github-pull-request:
+        admin-list: '{admin_list}'
+        allow-whitelist-orgs-as-admins: '{allow_whitelist_orgs_as_admins}'
+        auth-id: '{ghpr_auth}'
+        auto-close-on-fail: false
+        build-desc-template: null
+        github-hooks: true
+        only-trigger-phrase: '{only_trigger_phrase}'
+        org-list: '{org_list}'
+        permit-all: '{trigger_permit_all}'
+        trigger-phrase: '{trigger_phrase}'
+        white-list-target-branches: '{white_list_target_branches}'
+        white-list: '{white_list}'
+        status-context: '{status_context}'
+        status-url: '{status_url}'
+        success-status: '{success_status}'
+        failure-status: '{failure_status}'
+        error-status: '{error_status}'
+        triggered-status: '{triggered_status}'
+        started-status: '{started_status}'
+    wrappers: '{wrappers}'
+
 - job-template:
     name: '{name}-{test_name}-integration-for-pull-request'
     node: '{node}'
diff --git a/ci/jenkins/jobs/projects.yaml b/ci/jenkins/jobs/projects.yaml
index 46c46d2b6..873965c13 100644
--- a/ci/jenkins/jobs/projects.yaml
+++ b/ci/jenkins/jobs/projects.yaml
@@ -547,7 +547,9 @@
           started_status: null
           wrappers: []
           publishers: []
-      - '{name}-{test_name}-for-pull-request':
+      - '{name}-{test_name}-for-pr':
+          # The cluster name of this job could be too long since job name is encoded in CAPI cluster name.
+          # Use the shorter name as a workaround.
           test_name: all-features-conformance
           node: 'antrea-test-node'
           description: 'This is the {test_name} test for {name}.'
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index ed2d18d02..c80035c4d 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -238,8 +238,8 @@ func run(o *Options) error {
 	v4GroupCounter := proxytypes.NewGroupCounter(false, groupIDUpdates)
 	v6GroupCounter := proxytypes.NewGroupCounter(true, groupIDUpdates)
 
-	v4Enabled := config.IsIPv4Enabled(nodeConfig, networkConfig.TrafficEncapMode)
-	v6Enabled := config.IsIPv6Enabled(nodeConfig, networkConfig.TrafficEncapMode)
+	v4Enabled := networkConfig.IPv4Enabled
+	v6Enabled := networkConfig.IPv6Enabled
 	var proxier proxy.Proxier
 	if features.DefaultFeatureGate.Enabled(features.AntreaProxy) {
 		proxyAll := o.config.AntreaProxy.ProxyAll
@@ -290,7 +290,9 @@ func run(o *Options) error {
 		statusManagerEnabled,
 		loggingEnabled,
 		asyncRuleDeleteInterval,
-		o.config.DNSServerOverride)
+		o.config.DNSServerOverride,
+		v4Enabled,
+		v6Enabled)
 	if err != nil {
 		return fmt.Errorf("error creating new NetworkPolicy controller: %v", err)
 	}
@@ -612,7 +614,9 @@ func run(o *Options) error {
 		*o.config.EnablePrometheusMetrics,
 		o.config.ClientConnection.Kubeconfig,
 		cipherSuites,
-		cipher.TLSVersionMap[o.config.TLSMinVersion])
+		cipher.TLSVersionMap[o.config.TLSMinVersion],
+		v4Enabled,
+		v6Enabled)
 	if err != nil {
 		return fmt.Errorf("error when creating agent API server: %v", err)
 	}
diff --git a/docs/contributors/cherry-picks.md b/docs/contributors/cherry-picks.md
index f9a464750..5e2c2db40 100644
--- a/docs/contributors/cherry-picks.md
+++ b/docs/contributors/cherry-picks.md
@@ -17,7 +17,7 @@ policy](../versioning.md#minor-releases-and-patch-releases).
 
 * A PR which was approved and merged into the main branch.
 * The PR was identified as a good candidate for backporting by an Antrea
-  maintainer: they will leave a comment on Github for the PR and provide a list
+  maintainer: they will label the PR with `action/backport` and comment a list
   of release branches to which the patch should be backported (example:
   [`release-1.0`](https://github.com/antrea-io/antrea/tree/release-1.0)).
 * Have the [Github CLI](https://cli.github.com/) installed (version >= 1.3) and
diff --git a/docs/contributors/docker-desktop-alternatives.md b/docs/contributors/docker-desktop-alternatives.md
new file mode 100644
index 000000000..97b20b0fd
--- /dev/null
+++ b/docs/contributors/docker-desktop-alternatives.md
@@ -0,0 +1,53 @@
+# Docker Desktop Alternatives
+
+The Antrea build system relies on Docker to build container images, which can
+then be used to test Antrea locally. As an Antrea developer, if you run `make`,
+`docker build` will be invoked to build the `antrea-ubuntu` container image. On
+Linux, Docker Engine (based on moby) runs natively, but if you use macOS or
+Windows for Antrea development, Docker needs to run inside a Linux Virtual
+Machine (VM). This VM is typically managed by [Docker
+Desktop](https://www.docker.com/products/docker-desktop). Starting January 31
+2022, Docker Desktop requires a per user paid subscription for professional use
+in "large" companies (more than 250 employees or more than $10 million in annual
+revenue). See <https://www.docker.com/pricing/faq> for details. For developers
+who contribute to Antrea as an employee of such a company (and not in their own
+individual capacity), it is no longer possible to use Docker Desktop to build
+(and possibly run) Antrea Docker images locally, unless they have a Docker
+subscription.
+
+For contributors who do not have a Docker subscription, we recommend the
+following Docker Desktop alternatives.
+
+## Colima (macOS)
+
+[Colima](https://github.com/abiosoft/colima) is a UI built with
+[Lima](https://github.com/lima-vm/lima). It supports running a container runtime
+(docker, containerd or kuberneters) on macOS, inside a Lima VM. Major benefits
+of Colima include its ability to be used as a drop-in replacement for Docker
+Desktop and its ability to coexist with Docker Desktop on the same macOS
+machine.
+
+To install and run Colima, follow these steps:
+
+* `brew install colima`
+* `colima start` to start Colima (the Linux VM) with the default
+  configuration. Check the Colima documentation for configuration options. By
+  default, Colima will use the Docker runtime. This means that you can keep
+  using the `docker` CLI and that no changes are required to build Antrea.
+* `docker context list` and check that the `colima` context is selected. You can
+  use `docker context use desktop-linux` to go back to Docker Desktop.
+* `make` to build Antrea locally. Check that the `antrea-ubuntu` image is
+  available by listing all images with `docker images`.
+
+TODO: validate that Kind can be used with Colima without any issue.
+
+## Rancher Desktop (macOS and Windows)
+
+Rancher Desktop is another possible alternative to Docker Desktop, which
+supports Windows in addition to macOS. On macOS, it also uses Lima as the Linux
+VM. Two major differences with Colima are that Rancher Desktop will always run
+Kubernetes, and that Rancher Desktop uses the
+[`nerdctl`](https://github.com/containerd/nerdctl) UI for container management
+instead of `docker`. However, the `nerdctl` and `docker` UIs are supposed to be
+compatible, so in theory it should be possible to alias `docker` to `nerdctl`
+and keep using the Antrea build system as is (to be tested).
diff --git a/docs/contributors/github-labels.md b/docs/contributors/github-labels.md
index cdea5aa57..480d50074 100644
--- a/docs/contributors/github-labels.md
+++ b/docs/contributors/github-labels.md
@@ -111,6 +111,8 @@ The labels in this list originated within Kubernetes at
 | triage/needs-information           | Indicates an issue needs more information in order to work on it. | Humans |
 | triage/not-reproducible            | Indicates an issue can not be reproduced as described. | Humans |
 | triage/unresolved                  | Indicates an issue that can not or will not be resolved. | Humans |
+| action/backport                    | Indicates a PR that requires backports. | Humans |
+| action/release-note                | Indicates a PR that should be included in release notes.  | Humans |
 
 ## Labels that apply only to issues
 
diff --git a/docs/maintainers/release.md b/docs/maintainers/release.md
index 1a91d966b..d2a9f0924 100644
--- a/docs/maintainers/release.md
+++ b/docs/maintainers/release.md
@@ -15,13 +15,14 @@ release. We use `<TAG>` as a placeholder for the release tag (e.g. `v1.4.0`).
 2. Open a PR (labelled with `kind/release`) against the appropriate release
    branch with the following commits:
    - a commit to update the [CHANGELOG](../../CHANGELOG). *For a minor release*,
-     all significant changes and all bug fixes since the first version of the
-     previous minor release should be mentioned, even bug fixes which have
-     already been included in some patch release. *For a patch release*, you
-     will mention all the bug fixes since the previous release with the same
-     minor version. The commit message must be *exactly* `"Update CHANGELOG for
-     <TAG> release"`, as a bot will look for this commit and cherry-pick it to
-     update the main branch (starting with Antrea v1.0). The
+     all significant changes and all bug fixes (labelled with
+     `action/release-note` since the first version of the previous minor release
+     should be mentioned, even bug fixes which have already been included in
+     some patch release. *For a patch release*, you will mention all the bug
+     fixes since the previous release with the same minor version. The commit
+     message must be *exactly* `"Update CHANGELOG for <TAG> release"`, as a bot
+     will look for this commit and cherry-pick it to update the main branch
+     (starting with Antrea v1.0). The
      [process-changelog.go](../../hack/release/process-changelog.go) script may
      be used to easily generate links to PRs and the Github profiles of PR
      authors.
diff --git a/docs/multicluster/getting-started.md b/docs/multicluster/getting-started.md
index 70eb5356d..8d1508646 100644
--- a/docs/multicluster/getting-started.md
+++ b/docs/multicluster/getting-started.md
@@ -36,7 +36,7 @@ run both member and leader controllers in one cluster.
      kubectl apply -f https://github.com/antrea-io/antrea/releases/download/<TAG>/antrea-multicluster-leader-global.yml
      ```
 
-   * To deploy the latest version (checkout and built from the `feature/multi-cluster` branch).
+   * To deploy the latest version (checkout and built from the `main` branch).
 
      ```bash
      kubectl apply -f multicluster/build/yamls/antrea-multicluster-leader-global.yml
@@ -54,7 +54,7 @@ manifest with new Namespace.
   sed 's/changeme/antrea-mcs-ns/g' antrea-multicluster-leader-namespaced.yml | kubectl apply -f -
   ```
   
-* To deploy the latest version (checkout and built from the `feature/multi-cluster` branch).
+* To deploy the latest version (checkout and built from the `main` branch).
   
   ```bash
   kubectl create ns antrea-mcs-ns
@@ -73,7 +73,7 @@ Namespace by default.
 kubectl apply -f https://github.com/antrea-io/antrea/releases/download/<TAG>/antrea-multicluster-member.yml
 ```
 
-* To deploy the latest version (checkout and built from the `feature/multi-cluster` branch).
+* To deploy the latest version (checkout and built from the `main` branch).
 
 ```bash
 kubectl apply -f multicluster/build/yamls/antrea-multicluster-member.yml
diff --git a/hack/.notableofcontents b/hack/.notableofcontents
index 8b28362ff..42f5e577f 100644
--- a/hack/.notableofcontents
+++ b/hack/.notableofcontents
@@ -6,6 +6,7 @@ docs/assets/README.md
 docs/assets/logo/README.md
 docs/contributors/cherry-picks.md
 docs/contributors/code-generation.md
+docs/contributors/docker-desktop-alternatives.md
 docs/contributors/eks-terraform.md
 docs/contributors/github-labels.md
 docs/contributors/manual-installation.md
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index 4ce1790d4..015f25085 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -343,6 +343,9 @@ func (i *Initializer) Initialize() error {
 		return err
 	}
 
+	i.networkConfig.IPv4Enabled = config.IsIPv4Enabled(i.nodeConfig, i.networkConfig.TrafficEncapMode)
+	i.networkConfig.IPv6Enabled = config.IsIPv6Enabled(i.nodeConfig, i.networkConfig.TrafficEncapMode)
+
 	if err := i.prepareHostNetwork(); err != nil {
 		return err
 	}
diff --git a/pkg/agent/apiserver/apiserver.go b/pkg/agent/apiserver/apiserver.go
index 434ccde40..763df7492 100644
--- a/pkg/agent/apiserver/apiserver.go
+++ b/pkg/agent/apiserver/apiserver.go
@@ -84,10 +84,10 @@ func installHandlers(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolic
 	s.Handler.NonGoRestfulMux.HandleFunc("/ovstracing", ovstracing.HandleFunc(aq))
 }
 
-func installAPIGroup(s *genericapiserver.GenericAPIServer, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier) error {
+func installAPIGroup(s *genericapiserver.GenericAPIServer, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, v4Enabled, v6Enabled bool) error {
 	systemGroup := genericapiserver.NewDefaultAPIGroupInfo(systemv1beta1.GroupName, scheme, metav1.ParameterCodec, codecs)
 	systemStorage := map[string]rest.Storage{}
-	supportBundleStorage := supportbundle.NewAgentStorage(ovsctl.NewClient(aq.GetNodeConfig().OVSBridge), aq, npq)
+	supportBundleStorage := supportbundle.NewAgentStorage(ovsctl.NewClient(aq.GetNodeConfig().OVSBridge), aq, npq, v4Enabled, v6Enabled)
 	systemStorage["supportbundles"] = supportBundleStorage.SupportBundle
 	systemStorage["supportbundles/download"] = supportBundleStorage.Download
 	systemGroup.VersionedResourcesStorageMap["v1beta1"] = systemStorage
@@ -96,7 +96,7 @@ func installAPIGroup(s *genericapiserver.GenericAPIServer, aq agentquerier.Agent
 
 // New creates an APIServer for running in antrea agent.
 func New(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, bindPort int,
-	enableMetrics bool, kubeconfig string, cipherSuites []uint16, tlsMinVersion uint16) (*agentAPIServer, error) {
+	enableMetrics bool, kubeconfig string, cipherSuites []uint16, tlsMinVersion uint16, v4Enabled, v6Enabled bool) (*agentAPIServer, error) {
 	cfg, err := newConfig(npq, bindPort, enableMetrics, kubeconfig)
 	if err != nil {
 		return nil, err
@@ -107,7 +107,7 @@ func New(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier
 	}
 	s.SecureServingInfo.CipherSuites = cipherSuites
 	s.SecureServingInfo.MinTLSVersion = tlsMinVersion
-	if err := installAPIGroup(s, aq, npq); err != nil {
+	if err := installAPIGroup(s, aq, npq, v4Enabled, v6Enabled); err != nil {
 		return nil, err
 	}
 	installHandlers(aq, npq, s)
diff --git a/pkg/agent/config/node_config.go b/pkg/agent/config/node_config.go
index 248d9c835..01bd20a96 100644
--- a/pkg/agent/config/node_config.go
+++ b/pkg/agent/config/node_config.go
@@ -154,6 +154,8 @@ type NetworkConfig struct {
 	IPSecPSK              string
 	TransportIface        string
 	TransportIfaceCIDRs   []string
+	IPv4Enabled           bool
+	IPv6Enabled           bool
 }
 
 // IsIPv4Enabled returns true if the cluster network supports IPv4.
diff --git a/pkg/agent/controller/networkpolicy/fqdn.go b/pkg/agent/controller/networkpolicy/fqdn.go
index cfe0ddae0..f7962039e 100644
--- a/pkg/agent/controller/networkpolicy/fqdn.go
+++ b/pkg/agent/controller/networkpolicy/fqdn.go
@@ -148,9 +148,11 @@ type fqdnController struct {
 	selectorItemToFQDN map[fqdnSelectorItem]sets.String
 	// selectorItemToRuleIDs maps fqdnToSelectorItem to the rules that contains the selector.
 	selectorItemToRuleIDs map[fqdnSelectorItem]sets.String
+	ipv4Enabled           bool
+	ipv6Enabled           bool
 }
 
-func newFQDNController(client openflow.Client, allocator *idAllocator, dnsServerOverride string, dirtyRuleHandler func(string)) (*fqdnController, error) {
+func newFQDNController(client openflow.Client, allocator *idAllocator, dnsServerOverride string, dirtyRuleHandler func(string), v4Enabled, v6Enabled bool) (*fqdnController, error) {
 	controller := &fqdnController{
 		ofClient:               client,
 		dirtyRuleHandler:       dirtyRuleHandler,
@@ -162,6 +164,8 @@ func newFQDNController(client openflow.Client, allocator *idAllocator, dnsServer
 		fqdnToSelectorItem:     map[string]map[fqdnSelectorItem]struct{}{},
 		selectorItemToFQDN:     map[fqdnSelectorItem]sets.String{},
 		selectorItemToRuleIDs:  map[fqdnSelectorItem]sets.String{},
+		ipv4Enabled:            v4Enabled,
+		ipv6Enabled:            v6Enabled,
 	}
 	if controller.ofClient != nil {
 		if err := controller.ofClient.NewDNSpacketInConjunction(dnsInterceptRuleID); err != nil {
@@ -584,14 +588,14 @@ func (f *fqdnController) parseDNSResponse(msg *dns.Msg) (string, map[string]net.
 	for _, ans := range msg.Answer {
 		switch r := ans.(type) {
 		case *dns.A:
-			if f.ofClient.IsIPv4Enabled() {
+			if f.ipv4Enabled {
 				responseIPs[r.A.String()] = r.A
 				if r.Header().Ttl < lowestTTL {
 					lowestTTL = r.Header().Ttl
 				}
 			}
 		case *dns.AAAA:
-			if f.ofClient.IsIPv6Enabled() {
+			if f.ipv6Enabled {
 				responseIPs[r.AAAA.String()] = r.AAAA
 				if r.Header().Ttl < lowestTTL {
 					lowestTTL = r.Header().Ttl
@@ -650,7 +654,7 @@ func (f *fqdnController) lookupIP(ctx context.Context, fqdn string) error {
 		return responseIPs
 	}
 
-	if f.ofClient.IsIPv4Enabled() {
+	if f.ipv4Enabled {
 		lookupTime := time.Now()
 		if ips, err := resolver.LookupIP(ctx, "ip4", fqdn); err == nil {
 			f.onDNSResponse(fqdn, makeResponseIPs(ips), defaultTTL, lookupTime, nil)
@@ -658,7 +662,7 @@ func (f *fqdnController) lookupIP(ctx context.Context, fqdn string) error {
 			v4ok = false
 		}
 	}
-	if f.ofClient.IsIPv6Enabled() {
+	if f.ipv6Enabled {
 		lookupTime := time.Now()
 		if ips, err := resolver.LookupIP(ctx, "ip6", fqdn); err == nil {
 			f.onDNSResponse(fqdn, makeResponseIPs(ips), defaultTTL, lookupTime, nil)
@@ -695,7 +699,7 @@ func (f *fqdnController) makeDNSRequest(ctx context.Context, fqdn string) error
 		return r, nil
 	}
 	v4ok, v6ok := true, true
-	if f.ofClient.IsIPv4Enabled() {
+	if f.ipv4Enabled {
 		m := dns.Msg{}
 		m.SetQuestion(fqdnToQuery, dns.TypeA)
 		lookupTime := time.Now()
@@ -705,7 +709,7 @@ func (f *fqdnController) makeDNSRequest(ctx context.Context, fqdn string) error
 			v4ok = false
 		}
 	}
-	if f.ofClient.IsIPv6Enabled() {
+	if f.ipv6Enabled {
 		m := dns.Msg{}
 		m.SetQuestion(fqdnToQuery, dns.TypeAAAA)
 		lookupTime := time.Now()
diff --git a/pkg/agent/controller/networkpolicy/networkpolicy_controller.go b/pkg/agent/controller/networkpolicy/networkpolicy_controller.go
index 2bb74f562..33ba8111d 100644
--- a/pkg/agent/controller/networkpolicy/networkpolicy_controller.go
+++ b/pkg/agent/controller/networkpolicy/networkpolicy_controller.go
@@ -120,7 +120,9 @@ func NewNetworkPolicyController(antreaClientGetter agent.AntreaClientProvider,
 	statusManagerEnabled bool,
 	loggingEnabled bool,
 	asyncRuleDeleteInterval time.Duration,
-	dnsServerOverride string) (*Controller, error) {
+	dnsServerOverride string,
+	v4Enabled bool,
+	v6Enabled bool) (*Controller, error) {
 	idAllocator := newIDAllocator(asyncRuleDeleteInterval, dnsInterceptRuleID)
 	c := &Controller{
 		antreaClientProvider: antreaClientGetter,
@@ -133,14 +135,14 @@ func NewNetworkPolicyController(antreaClientGetter agent.AntreaClientProvider,
 	}
 	if antreaPolicyEnabled {
 		var err error
-		if c.fqdnController, err = newFQDNController(ofClient, idAllocator, dnsServerOverride, c.enqueueRule); err != nil {
+		if c.fqdnController, err = newFQDNController(ofClient, idAllocator, dnsServerOverride, c.enqueueRule, v4Enabled, v6Enabled); err != nil {
 			return nil, err
 		}
 		if c.ofClient != nil {
 			c.ofClient.RegisterPacketInHandler(uint8(openflow.PacketInReasonNP), "dnsresponse", c.fqdnController)
 		}
 	}
-	c.reconciler = newReconciler(ofClient, ifaceStore, idAllocator, c.fqdnController, groupCounters)
+	c.reconciler = newReconciler(ofClient, ifaceStore, idAllocator, c.fqdnController, groupCounters, v4Enabled, v6Enabled)
 	c.ruleCache = newRuleCache(c.enqueueRule, entityUpdates, groupIDUpdates)
 	if statusManagerEnabled {
 		c.statusManager = newStatusController(antreaClientGetter, nodeName, c.ruleCache)
diff --git a/pkg/agent/controller/networkpolicy/reconciler.go b/pkg/agent/controller/networkpolicy/reconciler.go
index 06a75ae40..7d9a369d6 100644
--- a/pkg/agent/controller/networkpolicy/reconciler.go
+++ b/pkg/agent/controller/networkpolicy/reconciler.go
@@ -217,6 +217,8 @@ func newReconciler(ofClient openflow.Client,
 	idAllocator *idAllocator,
 	fqdnController *fqdnController,
 	groupCounters []proxytypes.GroupCounter,
+	v4Enabled bool,
+	v6Enabled bool,
 ) *reconciler {
 	priorityAssigners := map[uint8]*tablePriorityAssigner{}
 	for _, table := range openflow.GetAntreaPolicyBaselineTierTables() {
@@ -240,8 +242,8 @@ func newReconciler(ofClient openflow.Client,
 	}
 	// Check if ofClient is nil or not to be compatible with unit tests.
 	if ofClient != nil {
-		reconciler.ipv4Enabled = ofClient.IsIPv4Enabled()
-		reconciler.ipv6Enabled = ofClient.IsIPv6Enabled()
+		reconciler.ipv4Enabled = v4Enabled
+		reconciler.ipv6Enabled = v6Enabled
 	}
 	return reconciler
 }
diff --git a/pkg/agent/controller/noderoute/node_route_controller.go b/pkg/agent/controller/noderoute/node_route_controller.go
index 543368775..fd9e96910 100644
--- a/pkg/agent/controller/noderoute/node_route_controller.go
+++ b/pkg/agent/controller/noderoute/node_route_controller.go
@@ -532,7 +532,7 @@ func (c *Controller) addNodeRoute(nodeName string, node *corev1.Node) error {
 
 		peerPodCIDRAddr, peerPodCIDR, err := net.ParseCIDR(podCIDR)
 		if err != nil {
-			klog.Errorf("Failed to parse PodCIDR %s for Node %s", node.Spec.PodCIDR, nodeName)
+			klog.Errorf("Failed to parse PodCIDR %s for Node %s", podCIDR, nodeName)
 			return nil
 		}
 		peerGatewayIP := ip.NextIP(peerPodCIDRAddr)
diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go
index ad577481a..09627844d 100644
--- a/pkg/agent/openflow/client.go
+++ b/pkg/agent/openflow/client.go
@@ -248,10 +248,6 @@ type Client interface {
 	StartPacketInHandler(packetInStartedReason []uint8, stopCh <-chan struct{})
 	// Get traffic metrics of each NetworkPolicy rule.
 	NetworkPolicyMetrics() map[uint32]*types.RuleMetric
-	// Returns if IPv4 is supported on this Node or not.
-	IsIPv4Enabled() bool
-	// Returns if IPv6 is supported on this Node or not.
-	IsIPv6Enabled() bool
 	// SendTCPPacketOut sends TCP packet as a packet-out to OVS.
 	SendTCPPacketOut(
 		srcMAC string,
@@ -652,7 +648,7 @@ func (c *client) InstallDefaultServiceFlows(nodePortAddressesIPv4, nodePortAddre
 		c.sessionAffinityReselectFlow(),
 		c.l2ForwardOutputServiceHairpinFlow(),
 	}
-	if c.IsIPv4Enabled() {
+	if c.networkConfig.IPv4Enabled {
 		flows = append(flows, c.serviceHairpinResponseDNATFlow(binding.ProtocolIP))
 		flows = append(flows, c.serviceLBBypassFlows(binding.ProtocolIP)...)
 		flows = append(flows, c.l3FwdServiceDefaultFlowsViaGW(binding.ProtocolIP, cookie.Service)...)
@@ -666,7 +662,7 @@ func (c *client) InstallDefaultServiceFlows(nodePortAddressesIPv4, nodePortAddre
 			flows = append(flows, c.serviceClassifierFlows(nodePortAddressesIPv4, binding.ProtocolIP)...)
 		}
 	}
-	if c.IsIPv6Enabled() {
+	if c.networkConfig.IPv6Enabled {
 		flows = append(flows, c.serviceHairpinResponseDNATFlow(binding.ProtocolIPv6))
 		flows = append(flows, c.serviceLBBypassFlows(binding.ProtocolIPv6)...)
 		flows = append(flows, c.l3FwdServiceDefaultFlowsViaGW(binding.ProtocolIPv6, cookie.Service)...)
@@ -786,10 +782,10 @@ func (c *client) Initialize(roundInfo types.RoundInfo, nodeConfig *config.NodeCo
 	c.nodeConfig = nodeConfig
 	c.networkConfig = networkConfig
 
-	if config.IsIPv4Enabled(nodeConfig, c.networkConfig.TrafficEncapMode) {
+	if networkConfig.IPv4Enabled {
 		c.ipProtocols = append(c.ipProtocols, binding.ProtocolIP)
 	}
-	if config.IsIPv6Enabled(nodeConfig, c.networkConfig.TrafficEncapMode) {
+	if networkConfig.IPv6Enabled {
 		c.ipProtocols = append(c.ipProtocols, binding.ProtocolIPv6)
 	}
 
@@ -1058,14 +1054,6 @@ func (c *client) InitialTLVMap() error {
 	return c.bridge.AddTLVMap(0x0104, 0x80, 4, 0)
 }
 
-func (c *client) IsIPv4Enabled() bool {
-	return config.IsIPv4Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode)
-}
-
-func (c *client) IsIPv6Enabled() bool {
-	return config.IsIPv6Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode)
-}
-
 // setBasePacketOutBuilder sets base IP properties of a packetOutBuilder which can have more packet data added.
 func setBasePacketOutBuilder(packetOutBuilder binding.PacketOutBuilder, srcMAC string, dstMAC string, srcIP string, dstIP string, inPort uint32, outPort uint32) (binding.PacketOutBuilder, error) {
 	// Set ethernet header.
diff --git a/pkg/agent/openflow/network_policy.go b/pkg/agent/openflow/network_policy.go
index af36a63f4..badd3020c 100644
--- a/pkg/agent/openflow/network_policy.go
+++ b/pkg/agent/openflow/network_policy.go
@@ -781,7 +781,7 @@ func (c *clause) addAddrFlows(client *client, addrType types.AddressType, addres
 func (c *clause) addServiceFlows(client *client, ports []v1beta2.Service, priority *uint16, matchSrc bool) []*conjMatchFlowContextChange {
 	var conjMatchFlowContextChanges []*conjMatchFlowContextChange
 	for _, port := range ports {
-		matches := generateServicePortConjMatches(c.ruleTable.GetID(), port, priority, client.IsIPv4Enabled(), client.IsIPv6Enabled(), matchSrc)
+		matches := generateServicePortConjMatches(c.ruleTable.GetID(), port, priority, client.networkConfig.IPv4Enabled, client.networkConfig.IPv6Enabled, matchSrc)
 		for _, match := range matches {
 			ctxChange := c.addConjunctiveMatchFlow(client, match)
 			conjMatchFlowContextChanges = append(conjMatchFlowContextChanges, ctxChange)
@@ -994,7 +994,7 @@ func (c *client) addRuleToConjunctiveMatch(conj *policyRuleConjunction, rule *ty
 	}
 	if conj.serviceClause != nil {
 		for _, port := range rule.Service {
-			matches := generateServicePortConjMatches(conj.serviceClause.ruleTable.GetID(), port, rule.Priority, c.IsIPv4Enabled(), c.IsIPv6Enabled(), false)
+			matches := generateServicePortConjMatches(conj.serviceClause.ruleTable.GetID(), port, rule.Priority, c.networkConfig.IPv4Enabled, c.networkConfig.IPv6Enabled, false)
 			for _, match := range matches {
 				c.addActionToConjunctiveMatch(conj.serviceClause, match)
 			}
diff --git a/pkg/agent/route/route_linux.go b/pkg/agent/route/route_linux.go
index 42dc5cc47..67de65a80 100644
--- a/pkg/agent/route/route_linux.go
+++ b/pkg/agent/route/route_linux.go
@@ -394,10 +394,8 @@ func (c *Client) writeEKSMangleRule(iptablesData *bytes.Buffer) {
 // It's idempotent and can safely be called on every startup.
 func (c *Client) syncIPTables() error {
 	var err error
-	v4Enabled := config.IsIPv4Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode)
-	v6Enabled := config.IsIPv6Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode)
 
-	c.ipt, err = iptables.New(v4Enabled, v6Enabled)
+	c.ipt, err = iptables.New(c.networkConfig.IPv4Enabled, c.networkConfig.IPv6Enabled)
 	if err != nil {
 		return fmt.Errorf("error creating IPTables instance: %v", err)
 	}
@@ -443,7 +441,7 @@ func (c *Client) syncIPTables() error {
 		return true
 	})
 	// Use iptables-restore to configure IPv4 settings.
-	if v4Enabled {
+	if c.networkConfig.IPv4Enabled {
 		iptablesData := c.restoreIptablesData(c.nodeConfig.PodIPv4CIDR, antreaPodIPSet, localAntreaFlexibleIPAMPodIPSet, 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 {
@@ -452,7 +450,7 @@ func (c *Client) syncIPTables() error {
 	}
 
 	// Use ip6tables-restore to configure IPv6 settings.
-	if v6Enabled {
+	if c.networkConfig.IPv6Enabled {
 		iptablesData := c.restoreIptablesData(c.nodeConfig.PodIPv6CIDR, antreaPodIP6Set, localAntreaFlexibleIPAMPodIP6Set, 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 {
@@ -668,12 +666,12 @@ func (c *Client) initIPRoutes() error {
 }
 
 func (c *Client) initServiceIPRoutes() error {
-	if config.IsIPv4Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode) {
+	if c.networkConfig.IPv4Enabled {
 		if err := c.addVirtualServiceIPRoute(false); err != nil {
 			return err
 		}
 	}
-	if config.IsIPv6Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode) {
+	if c.networkConfig.IPv6Enabled {
 		if err := c.addVirtualServiceIPRoute(true); err != nil {
 			return err
 		}
@@ -696,7 +694,7 @@ func (c *Client) Reconcile(podCIDRs []string, svcIPs map[string]bool) error {
 			if desiredPodCIDRs.Has(entry) {
 				continue
 			}
-			klog.Infof("Deleting orphaned PodIP %s from ipset and route table", entry)
+			klog.Infof("Deleting orphaned Pod IP %s from ipset and route table", entry)
 			if err := ipset.DelEntry(ipsetName, entry); err != nil {
 				return err
 			}
@@ -750,7 +748,7 @@ func (c *Client) Reconcile(podCIDRs []string, svcIPs map[string]bool) error {
 		if desiredGWs.Has(neighIP) {
 			continue
 		}
-		// Don't delete the virtual Service IP neigh which is added by AntreaProxy.
+		// Don't delete the virtual Service IP neighbor which is added by AntreaProxy.
 		if actualNeigh.IP.Equal(config.VirtualServiceIPv6) {
 			continue
 		}
@@ -1159,7 +1157,7 @@ func (c *Client) AddClusterIPRoute(svcIP net.IP) error {
 			} 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)
+			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)
 		}
diff --git a/pkg/agent/route/route_windows.go b/pkg/agent/route/route_windows.go
index 047b45cf6..81b6fb7d8 100644
--- a/pkg/agent/route/route_windows.go
+++ b/pkg/agent/route/route_windows.go
@@ -106,12 +106,12 @@ func (c *Client) Initialize(nodeConfig *config.NodeConfig, done func()) error {
 }
 
 func (c *Client) initServiceIPRoutes() error {
-	if config.IsIPv4Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode) {
+	if c.networkConfig.IPv4Enabled {
 		if err := c.addVirtualServiceIPRoute(false); err != nil {
 			return err
 		}
 	}
-	if config.IsIPv6Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode) {
+	if c.networkConfig.IPv6Enabled {
 		return fmt.Errorf("IPv6 is not supported on Windows")
 	}
 	return nil
diff --git a/pkg/apiserver/registry/system/supportbundle/rest.go b/pkg/apiserver/registry/system/supportbundle/rest.go
index 7c7b6cb22..55dbf4ed8 100644
--- a/pkg/apiserver/registry/system/supportbundle/rest.go
+++ b/pkg/apiserver/registry/system/supportbundle/rest.go
@@ -71,7 +71,7 @@ func NewControllerStorage() Storage {
 }
 
 // NewAgentStorage creates a support bundle storage for working on antrea agent.
-func NewAgentStorage(client ovsctl.OVSCtlClient, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier) Storage {
+func NewAgentStorage(client ovsctl.OVSCtlClient, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, v4Enabled, v6Enabled bool) Storage {
 	bundle := &supportBundleREST{
 		mode:         modeAgent,
 		ovsCtlClient: client,
@@ -81,6 +81,8 @@ func NewAgentStorage(client ovsctl.OVSCtlClient, aq agentquerier.AgentQuerier, n
 			ObjectMeta: metav1.ObjectMeta{Name: modeAgent},
 			Status:     systemv1beta1.SupportBundleStatusNone,
 		},
+		v4Enabled: v4Enabled,
+		v6Enabled: v6Enabled,
 	}
 	return Storage{
 		Mode:          modeAgent,
@@ -114,6 +116,8 @@ type supportBundleREST struct {
 	ovsCtlClient ovsctl.OVSCtlClient
 	aq           agentquerier.AgentQuerier
 	npq          querier.AgentNetworkPolicyInfoQuerier
+	v4Enabled    bool
+	v6Enabled    bool
 }
 
 // Create triggers a bundle generation. It only allows resource creation when
@@ -253,7 +257,7 @@ func (r *supportBundleREST) collect(ctx context.Context, dumpers ...func(string)
 }
 
 func (r *supportBundleREST) collectAgent(ctx context.Context, since string) (*systemv1beta1.SupportBundle, error) {
-	dumper := support.NewAgentDumper(defaultFS, defaultExecutor, r.ovsCtlClient, r.aq, r.npq, since)
+	dumper := support.NewAgentDumper(defaultFS, defaultExecutor, r.ovsCtlClient, r.aq, r.npq, since, r.v4Enabled, r.v6Enabled)
 	return r.collect(
 		ctx,
 		dumper.DumpLog,
diff --git a/pkg/support/dump.go b/pkg/support/dump.go
index f60e9b201..c8c621b7d 100644
--- a/pkg/support/dump.go
+++ b/pkg/support/dump.go
@@ -281,6 +281,8 @@ type agentDumper struct {
 	aq           agentquerier.AgentQuerier
 	npq          querier.AgentNetworkPolicyInfoQuerier
 	since        string
+	v4Enabled    bool
+	v6Enabled    bool
 }
 
 func (d *agentDumper) DumpAgentInfo(basedir string) error {
@@ -326,7 +328,7 @@ func (d *agentDumper) DumpOVSPorts(basedir string) error {
 	return writeFile(d.fs, filepath.Join(basedir, "ovsports"), "ports", []byte(strings.Join(portData, "\n")))
 }
 
-func NewAgentDumper(fs afero.Fs, executor exec.Interface, ovsCtlClient ovsctl.OVSCtlClient, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, since string) AgentDumper {
+func NewAgentDumper(fs afero.Fs, executor exec.Interface, ovsCtlClient ovsctl.OVSCtlClient, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, since string, v4Enabled, v6Enabled bool) AgentDumper {
 	return &agentDumper{
 		fs:           fs,
 		executor:     executor,
@@ -334,5 +336,7 @@ func NewAgentDumper(fs afero.Fs, executor exec.Interface, ovsCtlClient ovsctl.OV
 		aq:           aq,
 		npq:          npq,
 		since:        since,
+		v4Enabled:    v4Enabled,
+		v6Enabled:    v6Enabled,
 	}
 }
diff --git a/pkg/support/dump_others.go b/pkg/support/dump_others.go
index 36c81b700..b3b9e061c 100644
--- a/pkg/support/dump_others.go
+++ b/pkg/support/dump_others.go
@@ -22,7 +22,6 @@ import (
 	"path"
 	"path/filepath"
 
-	"antrea.io/antrea/pkg/agent/config"
 	"antrea.io/antrea/pkg/agent/util/iptables"
 	"antrea.io/antrea/pkg/util/logdir"
 )
@@ -48,11 +47,7 @@ func (d *agentDumper) DumpHostNetworkInfo(basedir string) error {
 }
 
 func (d *agentDumper) dumpIPTables(basedir string) error {
-	nodeConfig := d.aq.GetNodeConfig()
-	networkConfig := d.aq.GetNetworkConfig()
-	v4Enabled := config.IsIPv4Enabled(nodeConfig, networkConfig.TrafficEncapMode)
-	v6Enabled := config.IsIPv6Enabled(nodeConfig, networkConfig.TrafficEncapMode)
-	c, err := iptables.New(v4Enabled, v6Enabled)
+	c, err := iptables.New(d.v4Enabled, d.v6Enabled)
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/ci/kind/test-e2e-kind.sh b/ci/kind/test-e2e-kind.sh
index 0341637bd..e6820860b 100755
--- a/ci/kind/test-e2e-kind.sh
+++ b/ci/kind/test-e2e-kind.sh
@@ -127,7 +127,7 @@ fi
 
 COMMON_IMAGES_LIST=("k8s.gcr.io/e2e-test-images/agnhost:2.29" \
                     "projects.registry.vmware.com/library/busybox"  \
-                    "projects.registry.vmware.com/antrea/nginx" \
+                    "projects.registry.vmware.com/antrea/nginx:1.21.6-alpine" \
                     "projects.registry.vmware.com/antrea/perftool" \
                     "projects.registry.vmware.com/antrea/ipfix-collector:v0.5.11" \
                     "projects.registry.vmware.com/antrea/wireguard-go:0.0.20210424")
diff --git a/pkg/agent/controller/networkpolicy/fqdn_test.go b/pkg/agent/controller/networkpolicy/fqdn_test.go
index 3abe00ce2..273520d3a 100644
--- a/pkg/agent/controller/networkpolicy/fqdn_test.go
+++ b/pkg/agent/controller/networkpolicy/fqdn_test.go
@@ -29,8 +29,6 @@ import (
 
 func newMockFQDNController(t *testing.T, controller *gomock.Controller, dnsServer *string) (*fqdnController, *openflowtest.MockClient) {
 	mockOFClient := openflowtest.NewMockClient(controller)
-	mockOFClient.EXPECT().IsIPv4Enabled().Return(true).AnyTimes()
-	mockOFClient.EXPECT().IsIPv6Enabled().Return(false).AnyTimes()
 	mockOFClient.EXPECT().NewDNSpacketInConjunction(gomock.Any()).Return(nil).AnyTimes()
 	dirtyRuleHandler := func(rule string) {}
 	dnsServerAddr := "8.8.8.8:53" // dummy DNS server, will not be used since we don't send any request in these tests
@@ -42,6 +40,8 @@ func newMockFQDNController(t *testing.T, controller *gomock.Controller, dnsServe
 		newIDAllocator(testAsyncDeleteInterval),
 		dnsServerAddr,
 		dirtyRuleHandler,
+		true,
+		false,
 	)
 	require.NoError(t, err)
 	return f, mockOFClient
diff --git a/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go b/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go
index 545fb338e..09fff89c3 100644
--- a/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go
+++ b/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go
@@ -56,7 +56,7 @@ func newTestController() (*Controller, *fake.Clientset, *mockReconciler) {
 	ch2 := make(chan string, 100)
 	groupCounters := []proxytypes.GroupCounter{proxytypes.NewGroupCounter(false, ch2)}
 	controller, _ := NewNetworkPolicyController(&antreaClientGetter{clientset}, nil, nil, "node1", ch, groupCounters, ch2,
-		true, true, true, true, testAsyncDeleteInterval, "8.8.8.8:53")
+		true, true, true, true, testAsyncDeleteInterval, "8.8.8.8:53", true, false)
 	reconciler := newMockReconciler()
 	controller.reconciler = reconciler
 	controller.antreaPolicyLogger = nil
diff --git a/pkg/agent/controller/networkpolicy/reconciler_test.go b/pkg/agent/controller/networkpolicy/reconciler_test.go
index ac4fc0366..4ffd07571 100644
--- a/pkg/agent/controller/networkpolicy/reconciler_test.go
+++ b/pkg/agent/controller/networkpolicy/reconciler_test.go
@@ -98,11 +98,11 @@ func newCIDR(cidrStr string) *net.IPNet {
 	return tmpIPNet
 }
 
-func newTestReconciler(t *testing.T, controller *gomock.Controller, ifaceStore interfacestore.InterfaceStore, ofClient *openflowtest.MockClient) *reconciler {
+func newTestReconciler(t *testing.T, controller *gomock.Controller, ifaceStore interfacestore.InterfaceStore, ofClient *openflowtest.MockClient, v4Enabled, v6Enabled bool) *reconciler {
 	f, _ := newMockFQDNController(t, controller, nil)
 	ch := make(chan string, 100)
 	groupCounters := []proxytypes.GroupCounter{proxytypes.NewGroupCounter(false, ch)}
-	r := newReconciler(ofClient, ifaceStore, newIDAllocator(testAsyncDeleteInterval), f, groupCounters)
+	r := newReconciler(ofClient, ifaceStore, newIDAllocator(testAsyncDeleteInterval), f, groupCounters, v4Enabled, v6Enabled)
 	return r
 }
 
@@ -177,8 +177,6 @@ func TestReconcilerForget(t *testing.T) {
 			defer controller.Finish()
 			ifaceStore := interfacestore.NewInterfaceStore()
 			mockOFClient := openflowtest.NewMockClient(controller)
-			mockOFClient.EXPECT().IsIPv4Enabled().Return(true).AnyTimes()
-			mockOFClient.EXPECT().IsIPv6Enabled().Return(false).AnyTimes()
 			if len(tt.expectedOFRuleIDs) == 0 {
 				mockOFClient.EXPECT().UninstallPolicyRuleFlows(gomock.Any()).Times(0)
 			} else {
@@ -186,7 +184,7 @@ func TestReconcilerForget(t *testing.T) {
 					mockOFClient.EXPECT().UninstallPolicyRuleFlows(ofID)
 				}
 			}
-			r := newTestReconciler(t, controller, ifaceStore, mockOFClient)
+			r := newTestReconciler(t, controller, ifaceStore, mockOFClient, true, false)
 			for key, value := range tt.lastRealizeds {
 				r.lastRealizeds.Store(key, value)
 			}
@@ -524,13 +522,11 @@ func TestReconcilerReconcile(t *testing.T) {
 			controller := gomock.NewController(t)
 			defer controller.Finish()
 			mockOFClient := openflowtest.NewMockClient(controller)
-			mockOFClient.EXPECT().IsIPv4Enabled().Return(true).AnyTimes()
-			mockOFClient.EXPECT().IsIPv6Enabled().Return(false).AnyTimes()
 			// TODO: mock idAllocator and priorityAssigner
 			for i := 0; i < len(tt.expectedOFRules); i++ {
 				mockOFClient.EXPECT().InstallPolicyRuleFlows(gomock.Any())
 			}
-			r := newTestReconciler(t, controller, ifaceStore, mockOFClient)
+			r := newTestReconciler(t, controller, ifaceStore, mockOFClient, true, false)
 			if err := r.Reconcile(tt.args); (err != nil) != tt.wantErr {
 				t.Fatalf("Reconcile() error = %v, wantErr %v", err, tt.wantErr)
 			}
@@ -589,9 +585,7 @@ func TestReconcileWithTransientError(t *testing.T) {
 	controller := gomock.NewController(t)
 	defer controller.Finish()
 	mockOFClient := openflowtest.NewMockClient(controller)
-	mockOFClient.EXPECT().IsIPv4Enabled().Return(true).AnyTimes()
-	mockOFClient.EXPECT().IsIPv6Enabled().Return(true).AnyTimes()
-	r := newTestReconciler(t, controller, ifaceStore, mockOFClient)
+	r := newTestReconciler(t, controller, ifaceStore, mockOFClient, true, true)
 	// Set deleteInterval to verify openflow ID is released immediately.
 	r.idAllocator.deleteInterval = 0
 
@@ -750,9 +744,7 @@ func TestReconcilerBatchReconcile(t *testing.T) {
 			controller := gomock.NewController(t)
 			defer controller.Finish()
 			mockOFClient := openflowtest.NewMockClient(controller)
-			mockOFClient.EXPECT().IsIPv4Enabled().Return(true).AnyTimes()
-			mockOFClient.EXPECT().IsIPv6Enabled().Return(false).AnyTimes()
-			r := newTestReconciler(t, controller, ifaceStore, mockOFClient)
+			r := newTestReconciler(t, controller, ifaceStore, mockOFClient, true, true)
 			if tt.numInstalledRules > 0 {
 				// BatchInstall should skip rules already installed
 				r.lastRealizeds.Store(tt.args[0].ID, newLastRealized(tt.args[0]))
@@ -947,8 +939,6 @@ func TestReconcilerUpdate(t *testing.T) {
 			defer controller.Finish()
 			mockOFClient := openflowtest.NewMockClient(controller)
 			mockOFClient.EXPECT().InstallPolicyRuleFlows(gomock.Any()).MaxTimes(2)
-			mockOFClient.EXPECT().IsIPv4Enabled().Return(true).AnyTimes()
-			mockOFClient.EXPECT().IsIPv6Enabled().Return(true).AnyTimes()
 			priority := gomock.Any()
 			if !tt.originalRule.isAntreaNetworkPolicyRule() {
 				priority = nil
@@ -968,7 +958,7 @@ func TestReconcilerUpdate(t *testing.T) {
 			if len(tt.expectedDeletedTo) > 0 {
 				mockOFClient.EXPECT().DeletePolicyRuleAddress(gomock.Any(), types.DstAddress, gomock.Eq(tt.expectedDeletedTo), priority)
 			}
-			r := newTestReconciler(t, controller, ifaceStore, mockOFClient)
+			r := newTestReconciler(t, controller, ifaceStore, mockOFClient, true, true)
 			if err := r.Reconcile(tt.originalRule); (err != nil) != tt.wantErr {
 				t.Fatalf("Reconcile() error = %v, wantErr %v", err, tt.wantErr)
 			}
@@ -1468,13 +1458,11 @@ func TestReconcilerReconcileIPv6Only(t *testing.T) {
 			controller := gomock.NewController(t)
 			defer controller.Finish()
 			mockOFClient := openflowtest.NewMockClient(controller)
-			mockOFClient.EXPECT().IsIPv4Enabled().Return(false).AnyTimes()
-			mockOFClient.EXPECT().IsIPv6Enabled().Return(true).AnyTimes()
 			// TODO: mock idAllocator and priorityAssigner
 			for i := 0; i < len(tt.expectedOFRules); i++ {
 				mockOFClient.EXPECT().InstallPolicyRuleFlows(gomock.Any())
 			}
-			r := newTestReconciler(t, controller, ifaceStore, mockOFClient)
+			r := newTestReconciler(t, controller, ifaceStore, mockOFClient, false, true)
 			if err := r.Reconcile(tt.args); (err != nil) != tt.wantErr {
 				t.Fatalf("Reconcile() error = %v, wantErr %v", err, tt.wantErr)
 			}
@@ -1871,13 +1859,11 @@ func TestReconcilerReconcileDualStack(t *testing.T) {
 			controller := gomock.NewController(t)
 			defer controller.Finish()
 			mockOFClient := openflowtest.NewMockClient(controller)
-			mockOFClient.EXPECT().IsIPv4Enabled().Return(true).AnyTimes()
-			mockOFClient.EXPECT().IsIPv6Enabled().Return(true).AnyTimes()
 			// TODO: mock idAllocator and priorityAssigner
 			for i := 0; i < len(tt.expectedOFRules); i++ {
 				mockOFClient.EXPECT().InstallPolicyRuleFlows(gomock.Any())
 			}
-			r := newTestReconciler(t, controller, ifaceStore, mockOFClient)
+			r := newTestReconciler(t, controller, ifaceStore, mockOFClient, true, true)
 			if err := r.Reconcile(tt.args); (err != nil) != tt.wantErr {
 				t.Fatalf("Reconcile() error = %v, wantErr %v", err, tt.wantErr)
 			}
diff --git a/pkg/agent/openflow/network_policy_test.go b/pkg/agent/openflow/network_policy_test.go
index d1c983d44..4b547fa9f 100644
--- a/pkg/agent/openflow/network_policy_test.go
+++ b/pkg/agent/openflow/network_policy_test.go
@@ -156,7 +156,7 @@ func TestInstallPolicyRuleFlows(t *testing.T) {
 
 	c = prepareClient(ctrl)
 	c.nodeConfig = &config.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: nil}
-	c.networkConfig = &config.NetworkConfig{}
+	c.networkConfig = &config.NetworkConfig{IPv4Enabled: true}
 	c.ipProtocols = []binding.Protocol{binding.ProtocolIP}
 	defaultAction := crdv1alpha1.RuleActionAllow
 	ruleID1 := uint32(101)
@@ -512,7 +512,7 @@ func TestBatchInstallPolicyRuleFlows(t *testing.T) {
 			c.cookieAllocator = cookie.NewAllocator(0)
 			c.ofEntryOperations = mockOperations
 			c.nodeConfig = &config.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: nil}
-			c.networkConfig = &config.NetworkConfig{}
+			c.networkConfig = &config.NetworkConfig{IPv4Enabled: true}
 			c.ipProtocols = []binding.Protocol{binding.ProtocolIP}
 			c.deterministic = true
 
@@ -638,7 +638,7 @@ func TestInstallPolicyRuleFlowsInDualStackCluster(t *testing.T) {
 
 	c = prepareClient(ctrl)
 	c.nodeConfig = &config.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: podIPv6CIDR}
-	c.networkConfig = &config.NetworkConfig{}
+	c.networkConfig = &config.NetworkConfig{IPv4Enabled: true, IPv6Enabled: true}
 	c.ipProtocols = []binding.Protocol{binding.ProtocolIP, binding.ProtocolIPv6}
 	defaultAction := crdv1alpha1.RuleActionAllow
 	ruleID1 := uint32(101)
@@ -1114,7 +1114,7 @@ func TestGetMatchFlowUpdates(t *testing.T) {
 
 	c = prepareClient(ctrl)
 	c.nodeConfig = &config.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: nil}
-	c.networkConfig = &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}
+	c.networkConfig = &config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true}
 	c.ipProtocols = []binding.Protocol{binding.ProtocolIP}
 	outDropTable.EXPECT().BuildFlow(gomock.Any()).Return(newMockDropFlowBuilder(ctrl)).AnyTimes()
 	cnpOutTable.EXPECT().BuildFlow(gomock.Any()).Return(newMockRuleFlowBuilder(ctrl)).AnyTimes()
diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go
index 7647ebe10..ab49be308 100644
--- a/pkg/agent/openflow/testing/mock_openflow.go
+++ b/pkg/agent/openflow/testing/mock_openflow.go
@@ -533,34 +533,6 @@ func (mr *MockClientMockRecorder) IsConnected() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockClient)(nil).IsConnected))
 }
 
-// IsIPv4Enabled mocks base method
-func (m *MockClient) IsIPv4Enabled() bool {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "IsIPv4Enabled")
-	ret0, _ := ret[0].(bool)
-	return ret0
-}
-
-// IsIPv4Enabled indicates an expected call of IsIPv4Enabled
-func (mr *MockClientMockRecorder) IsIPv4Enabled() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsIPv4Enabled", reflect.TypeOf((*MockClient)(nil).IsIPv4Enabled))
-}
-
-// IsIPv6Enabled mocks base method
-func (m *MockClient) IsIPv6Enabled() bool {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "IsIPv6Enabled")
-	ret0, _ := ret[0].(bool)
-	return ret0
-}
-
-// IsIPv6Enabled indicates an expected call of IsIPv6Enabled
-func (mr *MockClientMockRecorder) IsIPv6Enabled() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsIPv6Enabled", reflect.TypeOf((*MockClient)(nil).IsIPv6Enabled))
-}
-
 // NetworkPolicyMetrics mocks base method
 func (m *MockClient) NetworkPolicyMetrics() map[uint32]*types.RuleMetric {
 	m.ctrl.T.Helper()
diff --git a/pkg/controller/networkpolicy/validate_test.go b/pkg/controller/networkpolicy/validate_test.go
new file mode 100644
index 000000000..d5f65466f
--- /dev/null
+++ b/pkg/controller/networkpolicy/validate_test.go
@@ -0,0 +1,957 @@
+// 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 networkpolicy
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	admv1 "k8s.io/api/admission/v1"
+	authenticationv1 "k8s.io/api/authentication/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1"
+)
+
+func TestValidateAntreaPolicy(t *testing.T) {
+	allowAction := crdv1alpha1.RuleActionAllow
+	passAction := crdv1alpha1.RuleActionPass
+	int32For80 := int32(80)
+
+	tests := []struct {
+		name           string
+		policy         *crdv1alpha1.ClusterNetworkPolicy
+		expectedReason string
+	}{
+		{
+			name: "acnp-non-existent-tier",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "non-existent-tier",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							PodSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo": "bar"},
+							},
+						},
+					},
+					Tier: "non-existent-tier",
+				},
+			},
+			expectedReason: "tier non-existent-tier does not exist",
+		},
+		{
+			name: "acnp-static-tier",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-static-tier",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							PodSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo": "bar"},
+							},
+						},
+					},
+					Tier: "NetworkOps",
+				},
+			},
+			expectedReason: "",
+		},
+		{
+			name: "acnp-ingress-baseline-pass-action",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-ingress-baseline-pass-action",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							PodSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Tier: "Baseline",
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &passAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "`Pass` action should not be set for Baseline Tier policy rules",
+		},
+		{
+			name: "acnp-egress-baseline-pass-action",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-egress-baseline-pass-action",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							PodSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Tier: "Baseline",
+					Egress: []crdv1alpha1.Rule{
+						{
+							Action: &passAction,
+							To: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "`Pass` action should not be set for Baseline Tier policy rules",
+		},
+		{
+			name: "acnp-egress-pass-action",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-egress-pass-action",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							PodSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Tier: "Application",
+					Egress: []crdv1alpha1.Rule{
+						{
+							Action: &passAction,
+							To: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "",
+		},
+		{
+			name: "acnp-non-unique-rule-name",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-non-unique-rule-name",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							PodSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+								},
+							},
+							Name: "rule1",
+						},
+						{
+							Action: &passAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo3": "bar3"},
+									},
+								},
+							},
+							Name: "rule1",
+						},
+					},
+				},
+			},
+			expectedReason: "rules names must be unique within the policy",
+		},
+		{
+			name: "acnp-appliedto-both-spec-rule",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-appliedto-both-spec-rule",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							PodSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+								},
+							},
+							AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo3": "bar3"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "appliedTo should not be set in both spec and rules",
+		},
+		{
+			name: "acnp-no-appliedto",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-no-appliedto",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "appliedTo needs to be set in either spec or rules",
+		},
+		{
+			name: "acnp-portion-rule-appliedto",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-portion-rule-appliedto",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo1": "bar1"},
+									},
+								},
+							},
+							AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+								},
+							},
+							Name: "rule1",
+						},
+						{
+							Action: &passAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo3": "bar3"},
+									},
+								},
+							},
+							Name: "rule2",
+						},
+					},
+				},
+			},
+			expectedReason: "appliedTo field should either be set in all rules or in none of them",
+		},
+		{
+			name: "acnp-rule-appliedto",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-rule-appliedto",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo1": "bar1"},
+									},
+								},
+							},
+							AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+								},
+							},
+							Name: "rule1",
+						},
+						{
+							Action: &passAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo3": "bar3"},
+									},
+								},
+							},
+							AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo4": "bar4"},
+									},
+								},
+							},
+							Name: "rule2",
+						},
+					},
+				},
+			},
+			expectedReason: "",
+		},
+		{
+			name: "acnp-appliedto-group-set-with-psel",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-appliedto-group-set-with-psel",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							PodSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+							Group: "group1",
+						},
+					},
+				},
+			},
+			expectedReason: "group cannot be set with other peers in appliedTo",
+		},
+		{
+			name: "acnp-appliedto-group-set-with-nssel",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-appliedto-group-set-with-nssel",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+							Group: "group1",
+						},
+					},
+				},
+			},
+			expectedReason: "group cannot be set with other peers in appliedTo",
+		},
+		{
+			name: "acnp-appliedto-group-alone",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-appliedto-group-alone",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							Group: "group1",
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									Namespaces: &crdv1alpha1.PeerNamespaces{
+										Match: crdv1alpha1.NamespaceMatchSelf,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "",
+		},
+		{
+			name: "acnp-rule-group-set-with-psel",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-rule-group-set-with-psel",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									PodSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+									Group: "group1",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "group cannot be set with other peers in rules",
+		},
+		{
+			name: "acnp-rule-group-set-with-nssel",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-rule-group-set-with-nssel",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									NamespaceSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+									Group: "group1",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "group cannot be set with other peers in rules",
+		},
+		{
+			name: "acnp-rule-group-set-with-ipblock",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-rule-group-set-with-ipblock",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									IPBlock: &crdv1alpha1.IPBlock{
+										CIDR: "10.0.0.10/32",
+									},
+									Group: "group1",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "group cannot be set with other peers in rules",
+		},
+		{
+			name: "acnp-rule-group-set-with-ns",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-rule-group-set-with-ns",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									Namespaces: &crdv1alpha1.PeerNamespaces{
+										Match: crdv1alpha1.NamespaceMatchSelf,
+									},
+									Group: "group1",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "group cannot be set with other peers in rules",
+		},
+		{
+			name: "acnp-rule-group-set-with-fqdn",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-rule-group-set-with-fqdn",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Egress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							To: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									FQDN:  "foo.bar",
+									Group: "group1",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "group cannot be set with other peers in rules",
+		},
+		{
+			name: "acnp-rule-group-set-with-eesel",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-rule-group-set-with-eesel",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									ExternalEntitySelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+									Group: "group1",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "group cannot be set with other peers in rules",
+		},
+		{
+			name: "acnp-rule-group-alone",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-rule-group-alone",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									Group: "group1",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "",
+		},
+		{
+			name: "acnp-rule-ns-set-with-nssel",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-rule-ns-set-with-nssel",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							From: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									NamespaceSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+									Namespaces: &crdv1alpha1.PeerNamespaces{
+										Match: crdv1alpha1.NamespaceMatchSelf,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "namespaces and namespaceSelector cannot be set at the same time for a single NetworkPolicyPeer",
+		},
+		{
+			name: "acnp-toservice-set-with-to",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-toservice-set-with-to",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Egress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							To: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									NamespaceSelector: &metav1.LabelSelector{
+										MatchLabels: map[string]string{"foo2": "bar2"},
+									},
+								},
+							},
+							ToServices: []crdv1alpha1.ServiceReference{
+								{
+									Name:      "foo",
+									Namespace: "bar",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "`toServices` can't be used with `to` or `ports`",
+		},
+		{
+			name: "acnp-toservice-set-with-ports",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-toservice-set-with-ports",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Egress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							Ports: []crdv1alpha1.NetworkPolicyPort{
+								{
+									Port: &int80,
+								},
+							},
+							ToServices: []crdv1alpha1.ServiceReference{
+								{
+									Name:      "foo",
+									Namespace: "bar",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "`toServices` can't be used with `to` or `ports`",
+		},
+		{
+			name: "acnp-toservice-alone",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-toservice-alone",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Egress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							ToServices: []crdv1alpha1.ServiceReference{
+								{
+									Name:      "foo",
+									Namespace: "bar",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "",
+		},
+		{
+			name: "acnp-invalid-fqdn",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-invalid-fqdn",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Egress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							To: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									FQDN: "foo!bar",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "invalid characters in egress rule fqdn field: foo!bar",
+		},
+		{
+			name: "acnp-valid-fqdn",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-valid-fqdn",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Egress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							To: []crdv1alpha1.NetworkPolicyPeer{
+								{
+									FQDN: "foo.bar",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "",
+		},
+		{
+			name: "acnp-endport-without-port",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-endport-without-port",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							Ports: []crdv1alpha1.NetworkPolicyPort{
+								{
+									EndPort: &int32For80,
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "if `endPort` is specified `port` must be specified",
+		},
+		{
+			name: "acnp-endport-smaller-port",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-endport-smaller-port",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							Ports: []crdv1alpha1.NetworkPolicyPort{
+								{
+									Port:    &int81,
+									EndPort: &int32For80,
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "`endPort` should be greater than or equal to `port`",
+		},
+		{
+			name: "acnp-named-port-with-endport",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-named-port-with-endport",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							Ports: []crdv1alpha1.NetworkPolicyPort{
+								{
+									Port:    &strHTTP,
+									EndPort: &int32For80,
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "if `port` is a string `endPort` cannot be specified",
+		},
+		{
+			name: "acnp-port-range",
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "acnp-port-range",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.NetworkPolicyPeer{
+						{
+							NamespaceSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{"foo1": "bar1"},
+							},
+						},
+					},
+					Ingress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							Ports: []crdv1alpha1.NetworkPolicyPort{
+								{
+									Port:    &int80,
+									EndPort: &int32For1999,
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, c := newController()
+			v := NewNetworkPolicyValidator(c.NetworkPolicyController)
+			actualReason, allowed := v.validateAntreaPolicy(tt.policy, nil, admv1.Create, authenticationv1.UserInfo{})
+			assert.Equal(t, tt.expectedReason, actualReason)
+			if tt.expectedReason == "" {
+				assert.True(t, allowed)
+			} else {
+				assert.False(t, allowed)
+			}
+		})
+	}
+}
diff --git a/test/e2e/antreapolicy_test.go b/test/e2e/antreapolicy_test.go
index 13b46cde8..ff10af7d3 100644
--- a/test/e2e/antreapolicy_test.go
+++ b/test/e2e/antreapolicy_test.go
@@ -323,293 +323,6 @@ func testInvalidACNPNoPriority(t *testing.T) {
 	}
 }
 
-func testInvalidACNPRuleNameNotUnique(t *testing.T) {
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy without unique rule names accepted")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-rule-name-not-unique").
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}}).
-		AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": "x"},
-			nil, nil, false, nil, crdv1alpha1.RuleActionAllow, "", "not-unique").
-		AddIngress(v1.ProtocolTCP, &p81, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": "x"},
-			nil, nil, false, nil, crdv1alpha1.RuleActionAllow, "", "not-unique")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
-func testInvalidACNPTierDoesNotExist(t *testing.T) {
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy without existing Tier accepted")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-tier-not-exist").
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}}).
-		SetTier("i-dont-exist")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
-func testInvalidACNPPortRangePortUnset(t *testing.T) {
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy egress rule with endPort but no port accepted")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-egress-port-range-port-unset").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder.AddEgress(v1.ProtocolTCP, nil, nil, &p8085, nil, map[string]string{"pod": "c"}, map[string]string{"ns": "x"},
-		nil, nil, false, nil, crdv1alpha1.RuleActionDrop, "", "acnp-port-range")
-
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
-func testInvalidACNPPortRangeEndPortSmall(t *testing.T) {
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy egress rule with endPort smaller than port accepted")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-egress-port-range-endport-small").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder.AddEgress(v1.ProtocolTCP, &p8082, nil, &p8081, nil, map[string]string{"pod": "c"}, map[string]string{"ns": "x"},
-		nil, nil, false, nil, crdv1alpha1.RuleActionDrop, "", "acnp-port-range")
-
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
-func testInvalidACNPSpecAppliedToRuleAppliedToSet(t *testing.T) {
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with spec appliedTo and rules appliedTo set")
-	ruleAppTo := ACNPAppliedToSpec{
-		PodSelector: map[string]string{"pod": "b"},
-	}
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-spec-appto-and-rules-appto").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": "x"},
-		nil, nil, false, []ACNPAppliedToSpec{ruleAppTo}, crdv1alpha1.RuleActionAllow, "", "")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
-func testInvalidACNPAppliedToNotSetInAllRules(t *testing.T) {
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with appliedTo not set in all rules")
-	ruleAppTo := ACNPAppliedToSpec{
-		PodSelector: map[string]string{"pod": "b"},
-	}
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-appto-not-set-in-all-rules").
-		SetPriority(1.0)
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": "x"},
-		nil, nil, false, []ACNPAppliedToSpec{ruleAppTo}, crdv1alpha1.RuleActionAllow, "", "").
-		AddIngress(v1.ProtocolTCP, &p81, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": "x"},
-			nil, nil, false, nil, crdv1alpha1.RuleActionAllow, "", "")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
-func testInvalidACNPAppliedToGroupSetWithPodSelector(t *testing.T) {
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with appliedTo set group with podSelector")
-	appTo := ACNPAppliedToSpec{
-		PodSelector: map[string]string{"pod": "b"},
-		Group:       "cgA",
-	}
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-appto-set-group-with-psel").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{appTo})
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": "x"},
-		nil, nil, false, nil, crdv1alpha1.RuleActionAllow, "", "")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
-func testInvalidACNPAppliedToGroupSetWithNSSelector(t *testing.T) {
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with appliedTo set group with nsSelector")
-	appTo := ACNPAppliedToSpec{
-		NSSelector: map[string]string{"ns": "x"},
-		Group:      "cgA",
-	}
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-appto-set-group-with-nssel").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{appTo})
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": "x"},
-		nil, nil, false, nil, crdv1alpha1.RuleActionAllow, "", "")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
-func testInvalidACNPIngressPeerCGSetWithPodSelector(t *testing.T) {
-	cgA := "cgA"
-	selectorA := metav1.LabelSelector{MatchLabels: map[string]string{"foo1": "bar1"}}
-	ruleAppTo := ACNPAppliedToSpec{
-		PodSelector: map[string]string{"pod": "b"},
-	}
-	k8sUtils.CreateCG(cgA, &selectorA, nil, nil)
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with group and podSelector in NetworkPolicyPeer set")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-ingress-group-podselector-set").
-		SetPriority(1.0)
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, map[string]string{"pod": "b"}, nil,
-		nil, nil, false, []ACNPAppliedToSpec{ruleAppTo}, crdv1alpha1.RuleActionAllow, "cgA", "")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-	failOnError(k8sUtils.CleanCGs(), t)
-}
-
-func testInvalidACNPIngressPeerCGSetWithNSSelector(t *testing.T) {
-	cgA := "cgA"
-	selectorA := metav1.LabelSelector{MatchLabels: map[string]string{"foo1": "bar1"}}
-	k8sUtils.CreateCG(cgA, &selectorA, nil, nil)
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with group and namespaceSelector in NetworkPolicyPeer set")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-ingress-group-nsselector-set").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, nil, map[string]string{"ns": "x"},
-		nil, nil, false, nil, crdv1alpha1.RuleActionAllow, "cgA", "")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-	failOnError(k8sUtils.CleanCGs(), t)
-}
-
-func testInvalidACNPIngressPeerCGSetWithIPBlock(t *testing.T) {
-	cgA := "cgA"
-	selectorA := metav1.LabelSelector{MatchLabels: map[string]string{"foo1": "bar1"}}
-	k8sUtils.CreateCG(cgA, &selectorA, nil, nil)
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with group and ipBlock in NetworkPolicyPeer set")
-	cidr := "10.0.0.10/32"
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-ingress-group-ipblock-set").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{{Group: "cgA"}})
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, &cidr, map[string]string{"pod": "b"}, map[string]string{"ns": "x"},
-		nil, nil, false, []ACNPAppliedToSpec{{Group: "cgB"}}, crdv1alpha1.RuleActionAllow, "", "")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
-func testInvalidACNPEgressPeerCGSetWithFQDN(t *testing.T) {
-	cgA := "cgA"
-	selectorA := metav1.LabelSelector{MatchLabels: map[string]string{"foo1": "bar1"}}
-	k8sUtils.CreateCG(cgA, &selectorA, nil, nil)
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with group and fqdn in NetworkPolicyPeer set")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-egress-group-fqdn-set").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder = builder.AddEgress(v1.ProtocolTCP, &p80, nil, nil, nil, nil, nil,
-		nil, nil, false, nil, crdv1alpha1.RuleActionAllow, "cgA", "")
-	builder.Spec.Egress[0].To[0].FQDN = "google.com"
-
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-	failOnError(k8sUtils.CleanCGs(), t)
-}
-
-func testInvalidACNPIngressPeerCGSetWithNamespace(t *testing.T) {
-	cgA := "cgA"
-	selectorA := metav1.LabelSelector{MatchLabels: map[string]string{"foo1": "bar1"}}
-	k8sUtils.CreateCG(cgA, &selectorA, nil, nil)
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with group and namespace in NetworkPolicyPeer set")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-ingress-group-ns-set").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, nil, nil,
-		nil, nil, true, nil, crdv1alpha1.RuleActionAllow, "cgA", "")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-	failOnError(k8sUtils.CleanCGs(), t)
-}
-
-func testInvalidACNPIngressPeerCGSetWithEESelector(t *testing.T) {
-	cgA := "cgA"
-	selectorA := metav1.LabelSelector{MatchLabels: map[string]string{"foo1": "bar1"}}
-	k8sUtils.CreateCG(cgA, &selectorA, nil, nil)
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with group and external entity selector in NetworkPolicyPeer set")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-ingress-group-eeselector-set").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, nil, nil,
-		nil, nil, true, nil, crdv1alpha1.RuleActionAllow, "cgA", "")
-	builder.Spec.Ingress[0].From[0].ExternalEntitySelector = &metav1.LabelSelector{
-		MatchLabels: map[string]string{"foo2": "bar2"},
-	}
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-	failOnError(k8sUtils.CleanCGs(), t)
-}
-
-func testInvalidACNPIngressPeerNamespacesSetWithNSSelector(t *testing.T) {
-	invalidNpErr := fmt.Errorf("invalid Antrea ClusterNetworkPolicy with namespaces and namespaceSelector in NetworkPolicyPeer set")
-	builder := &ClusterNetworkPolicySpecBuilder{}
-	builder = builder.SetName("acnp-ingress-namespaces-nsselector-set").
-		SetPriority(1.0).
-		SetAppliedToGroup([]ACNPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder = builder.AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil, nil, map[string]string{"ns": "x"},
-		nil, nil, true, nil, crdv1alpha1.RuleActionAllow, "", "")
-	acnp := builder.Get()
-	log.Debugf("creating ACNP %v", acnp.Name)
-	if _, err := k8sUtils.CreateOrUpdateACNP(acnp); err == nil {
-		// Above creation of ACNP must fail as it is an invalid spec.
-		failOnError(invalidNpErr, t)
-	}
-}
-
 func testInvalidANPNoPriority(t *testing.T) {
 	invalidNpErr := fmt.Errorf("invalid Antrea NetworkPolicy without a priority accepted")
 	builder := &AntreaNetworkPolicySpecBuilder{}
@@ -3087,21 +2800,6 @@ func TestAntreaPolicy(t *testing.T) {
 
 	t.Run("TestGroupValidateAntreaNativePolicies", func(t *testing.T) {
 		t.Run("Case=ACNPNoPriority", func(t *testing.T) { testInvalidACNPNoPriority(t) })
-		t.Run("Case=ACNPRuleNameNotUniqueDenied", func(t *testing.T) { testInvalidACNPRuleNameNotUnique(t) })
-		t.Run("Case=ACNPTierDoesNotExistDenied", func(t *testing.T) { testInvalidACNPTierDoesNotExist(t) })
-		t.Run("Case=ACNPPortRangePortUnsetDenied", func(t *testing.T) { testInvalidACNPPortRangePortUnset(t) })
-		t.Run("Case=ACNPPortRangePortEndPortSmallDenied", func(t *testing.T) { testInvalidACNPPortRangeEndPortSmall(t) })
-		t.Run("Case=ACNPIngressPeerCGSetWithIPBlock", func(t *testing.T) { testInvalidACNPIngressPeerCGSetWithIPBlock(t) })
-		t.Run("Case=ACNPIngressPeerCGSetWithPodSelector", func(t *testing.T) { testInvalidACNPIngressPeerCGSetWithPodSelector(t) })
-		t.Run("Case=ACNPIngressPeerCGSetWithNSSelector", func(t *testing.T) { testInvalidACNPIngressPeerCGSetWithNSSelector(t) })
-		t.Run("Case=ACNPEgressPeerCGSetWithFQDN", func(t *testing.T) { testInvalidACNPEgressPeerCGSetWithFQDN(t) })
-		t.Run("Case=ACNPIngressPeerCGSetWithNamespace", func(t *testing.T) { testInvalidACNPIngressPeerCGSetWithNamespace(t) })
-		t.Run("Case=ACNPIngressPeerCGSetWithEESelector", func(t *testing.T) { testInvalidACNPIngressPeerCGSetWithEESelector(t) })
-		t.Run("Case=ACNPIngressPeerNamespaceSetWithNSSelector", func(t *testing.T) { testInvalidACNPIngressPeerNamespacesSetWithNSSelector(t) })
-		t.Run("Case=ACNPSpecAppliedToRuleAppliedToSet", func(t *testing.T) { testInvalidACNPSpecAppliedToRuleAppliedToSet(t) })
-		t.Run("Case=ACNPAppliedToNotSetInAllRules", func(t *testing.T) { testInvalidACNPAppliedToNotSetInAllRules(t) })
-		t.Run("Case=ACNPAppliedToGroupSetWithPodSelector", func(t *testing.T) { testInvalidACNPAppliedToGroupSetWithPodSelector(t) })
-		t.Run("Case=ACNPAppliedToGroupSetWithNSSelector", func(t *testing.T) { testInvalidACNPAppliedToGroupSetWithNSSelector(t) })
 		t.Run("Case=ANPNoPriority", func(t *testing.T) { testInvalidANPNoPriority(t) })
 		t.Run("Case=ANPRuleNameNotUniqueDenied", func(t *testing.T) { testInvalidANPRuleNameNotUnique(t) })
 		t.Run("Case=ANPTierDoesNotExistDenied", func(t *testing.T) { testInvalidANPTierDoesNotExist(t) })
diff --git a/test/e2e/framework.go b/test/e2e/framework.go
index 240ecc34b..7df6b4fe4 100644
--- a/test/e2e/framework.go
+++ b/test/e2e/framework.go
@@ -119,7 +119,7 @@ const (
 	busyboxImage        = "projects.registry.vmware.com/library/busybox"
 	mcjoinImage         = "projects.registry.vmware.com/antrea/mcjoin:v2.9"
 	netshootImage       = "projects.registry.vmware.com/antrea/netshoot:v0.1"
-	nginxImage          = "projects.registry.vmware.com/antrea/nginx"
+	nginxImage          = "projects.registry.vmware.com/antrea/nginx:1.21.6-alpine"
 	perftoolImage       = "projects.registry.vmware.com/antrea/perftool"
 	ipfixCollectorImage = "projects.registry.vmware.com/antrea/ipfix-collector:v0.5.11"
 	ipfixCollectorPort  = "4739"
@@ -1169,8 +1169,10 @@ type PodCondition func(*corev1.Pod) (bool, error)
 // podWaitFor polls the K8s apiserver until the specified Pod is found (in the test Namespace) and
 // the condition predicate is met (or until the provided timeout expires).
 func (data *TestData) podWaitFor(timeout time.Duration, name, namespace string, condition PodCondition) (*corev1.Pod, error) {
+	var pod *corev1.Pod
 	err := wait.Poll(defaultInterval, timeout, func() (bool, error) {
-		pod, err := data.clientset.CoreV1().Pods(namespace).Get(context.TODO(), name, metav1.GetOptions{})
+		var err error
+		pod, err = data.clientset.CoreV1().Pods(namespace).Get(context.TODO(), name, metav1.GetOptions{})
 		if err != nil {
 			if errors.IsNotFound(err) {
 				return false, nil
@@ -1180,9 +1182,12 @@ func (data *TestData) podWaitFor(timeout time.Duration, name, namespace string,
 		return condition(pod)
 	})
 	if err != nil {
+		if err == wait.ErrWaitTimeout && pod != nil {
+			return nil, fmt.Errorf("timed out waiting for the condition, Pod.Status: %s", pod.Status.String())
+		}
 		return nil, err
 	}
-	return data.clientset.CoreV1().Pods(namespace).Get(context.TODO(), name, metav1.GetOptions{})
+	return pod, nil
 }
 
 // podWaitForRunning polls the k8s apiserver until the specified Pod is in the "running" state (or
diff --git a/test/integration/agent/openflow_test.go b/test/integration/agent/openflow_test.go
index a2f1c30db..e4ddef735 100644
--- a/test/integration/agent/openflow_test.go
+++ b/test/integration/agent/openflow_test.go
@@ -329,7 +329,7 @@ func testReplayFlows(t *testing.T) {
 }
 
 func testInitialize(t *testing.T, config *testConfig) {
-	if _, err := c.Initialize(roundInfo, config.nodeConfig, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap}); err != nil {
+	if _, err := c.Initialize(roundInfo, config.nodeConfig, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap, IPv4Enabled: config.enableIPv4, IPv6Enabled: config.enableIPv6}); err != nil {
 		t.Errorf("Failed to initialize openflow client: %v", err)
 	}
 	for _, tableFlow := range prepareDefaultFlows(config) {
@@ -427,7 +427,7 @@ func TestNetworkPolicyFlows(t *testing.T) {
 	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, GatewayConfig: gwConfig}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
+	_, err = c.Initialize(roundInfo, &config1.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: podIPv6CIDR, GatewayConfig: gwConfig}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap, IPv4Enabled: true, IPv6Enabled: true})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -579,7 +579,7 @@ func TestProxyServiceFlows(t *testing.T) {
 	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, GatewayConfig: gwConfig}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
+	_, err = c.Initialize(roundInfo, &config1.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: podIPv6CIDR, GatewayConfig: gwConfig}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap, IPv4Enabled: true})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -1519,7 +1519,7 @@ func TestSNATFlows(t *testing.T) {
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
 	config := prepareConfiguration()
-	_, err = c.Initialize(roundInfo, config.nodeConfig, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
+	_, err = c.Initialize(roundInfo, config.nodeConfig, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap, IPv4Enabled: true, IPv6Enabled: true})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
diff --git a/test/integration/agent/route_test.go b/test/integration/agent/route_test.go
index d5c30245b..ab72879d1 100644
--- a/test/integration/agent/route_test.go
+++ b/test/integration/agent/route_test.go
@@ -109,6 +109,7 @@ func TestInitialize(t *testing.T) {
 		{
 			networkConfig: &config.NetworkConfig{
 				TrafficEncapMode: config.TrafficEncapModeNoEncap,
+				IPv4Enabled:      true,
 			},
 			expectNoTrackRules: false,
 		},
@@ -116,6 +117,7 @@ func TestInitialize(t *testing.T) {
 			networkConfig: &config.NetworkConfig{
 				TrafficEncapMode: config.TrafficEncapModeHybrid,
 				TunnelType:       ovsconfig.GeneveTunnel,
+				IPv4Enabled:      true,
 			},
 			noSNAT:               true,
 			expectNoTrackRules:   true,
@@ -125,6 +127,7 @@ func TestInitialize(t *testing.T) {
 			networkConfig: &config.NetworkConfig{
 				TrafficEncapMode: config.TrafficEncapModeEncap,
 				TunnelType:       ovsconfig.VXLANTunnel,
+				IPv4Enabled:      true,
 			},
 			expectNoTrackRules:   true,
 			expectUDPPortInRules: 4789,
@@ -132,6 +135,7 @@ func TestInitialize(t *testing.T) {
 		{
 			networkConfig: &config.NetworkConfig{
 				TrafficEncapMode: config.TrafficEncapModeNoEncap,
+				IPv4Enabled:      true,
 			},
 			xtablesHoldDuration: 5 * time.Second,
 			expectNoTrackRules:  false,
@@ -246,7 +250,7 @@ func TestIpTablesSync(t *testing.T) {
 	gwLink := createDummyGW(t)
 	defer netlink.LinkDel(gwLink)
 
-	routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false, false, false, false)
+	routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true}, false, false, false, false)
 	assert.Nil(t, err)
 
 	inited := make(chan struct{})
@@ -297,7 +301,7 @@ func TestAddAndDeleteSNATRule(t *testing.T) {
 	gwLink := createDummyGW(t)
 	defer netlink.LinkDel(gwLink)
 
-	routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false, false, false, false)
+	routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true}, false, false, false, false)
 	assert.Nil(t, err)
 
 	inited := make(chan struct{})
@@ -351,7 +355,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(&config.NetworkConfig{TrafficEncapMode: tc.mode}, false, false, false, false)
+		routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: tc.mode, IPv4Enabled: true}, false, false, false, false)
 		assert.NoError(t, err)
 		err = routeClient.Initialize(nodeConfig, func() {})
 		assert.NoError(t, err)
@@ -414,7 +418,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(&config.NetworkConfig{TrafficEncapMode: tc.mode}, false, false, false, false)
+		routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: tc.mode, IPv4Enabled: true}, false, false, false, false)
 		assert.NoError(t, err)
 		err = routeClient.Initialize(nodeConfig, func() {})
 		assert.NoError(t, err)
@@ -555,7 +559,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(&config.NetworkConfig{TrafficEncapMode: tc.mode}, false, false, false, false)
+		routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: tc.mode, IPv4Enabled: true}, false, false, false, false)
 		assert.NoError(t, err)
 		err = routeClient.Initialize(nodeConfig, func() {})
 		assert.NoError(t, err)
@@ -593,7 +597,7 @@ func TestRouteTablePolicyOnly(t *testing.T) {
 	gwLink := createDummyGW(t)
 	defer netlink.LinkDel(gwLink)
 
-	routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeNetworkPolicyOnly}, false, false, false, false)
+	routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeNetworkPolicyOnly, IPv4Enabled: true}, false, false, false, false)
 	assert.NoError(t, err)
 	err = routeClient.Initialize(nodeConfig, func() {})
 	assert.NoError(t, err)
@@ -649,7 +653,7 @@ func TestIPv6RoutesAndNeighbors(t *testing.T) {
 	gwLink := createDummyGW(t)
 	defer netlink.LinkDel(gwLink)
 
-	routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false, false, false, false)
+	routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true, IPv6Enabled: true}, false, false, false, false)
 	assert.Nil(t, err)
 	_, ipv6Subnet, _ := net.ParseCIDR("fd74:ca9b:172:19::/64")
 	gwIPv6 := net.ParseIP("fd74:ca9b:172:19::1")