Repository: antrea-io/antrea
Pull request #4505 (2022-12-21 05:02:11)

Remove unnecessary proxy structs
In Golang, a struct implements an interface as long as it implements all methods of the interface. Thus, instead of creating proxy structs to pass method invocation to the real implementation, it could use the real implementation directly, which doesn't affect using mock implementation in unit tests.

Signed-off-by: Quan Tian <qtian@vmware.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/4505?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) Report
> Merging [#4505](https://codecov.io/gh/antrea-io/antrea/pull/4505?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (11b6949) into [main](https://codecov.io/gh/antrea-io/antrea/commit/721fc9ed24bae41fcf56d6f53b3b593ac2159b65?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (721fc9e) will **decrease** coverage by `0.04%`.
> The diff coverage is `100.00%`.

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

```diff
@@            Coverage Diff             @@
##             main    #4505      +/-   ##
==========================================
- Coverage   67.91%   67.87%   -0.05%     
==========================================
  Files         403      403              
  Lines       57698    57698              
==========================================
- Hits        39187    39162      -25     
- Misses      15788    15809      +21     
- Partials     2723     2727       +4     
```

| Flag | Coverage Δ | | *Carryforward flag |
|---|---|---|---|
| integration-tests | `34.50% <100.00%> (-0.15%)` | :arrow_down: | |
| kind-e2e-tests | `47.64% <100.00%> (ø)` | | Carriedforward from [721fc9e](https://codecov.io/gh/antrea-io/antrea/commit/721fc9ed24bae41fcf56d6f53b3b593ac2159b65?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) |
| unit-tests | `56.70% <42.85%> (ø)` | | Carriedforward from [721fc9e](https://codecov.io/gh/antrea-io/antrea/commit/721fc9ed24bae41fcf56d6f53b3b593ac2159b65?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) |

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

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/4505?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | Coverage Δ | |
|---|---|---|
| [pkg/agent/util/netlink/netlink\_linux.go](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L3V0aWwvbmV0bGluay9uZXRsaW5rX2xpbnV4Lmdv) | `50.00% <ø> (-41.67%)` | :arrow_down: |
| [pkg/ipfix/ipfix\_collector.go](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2lwZml4L2lwZml4X2NvbGxlY3Rvci5nbw==) | `83.33% <ø> (ø)` | |
| [pkg/ipfix/ipfix\_intermediate.go](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2lwZml4L2lwZml4X2ludGVybWVkaWF0ZS5nbw==) | `90.90% <ø> (ø)` | |
| [pkg/ipfix/ipfix\_process.go](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2lwZml4L2lwZml4X3Byb2Nlc3MuZ28=) | `81.25% <ø> (ø)` | |
| [pkg/agent/flowexporter/exporter/exporter.go](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2Zsb3dleHBvcnRlci9leHBvcnRlci9leHBvcnRlci5nbw==) | `81.68% <100.00%> (ø)` | |
| [pkg/agent/route/route\_linux.go](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L3JvdXRlL3JvdXRlX2xpbnV4Lmdv) | `71.48% <100.00%> (ø)` | |
| [pkg/flowaggregator/exporter/ipfix.go](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2Zsb3dhZ2dyZWdhdG9yL2V4cG9ydGVyL2lwZml4Lmdv) | `63.11% <100.00%> (ø)` | |
| [pkg/flowaggregator/flowaggregator.go](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2Zsb3dhZ2dyZWdhdG9yL2Zsb3dhZ2dyZWdhdG9yLmdv) | `68.42% <100.00%> (ø)` | |
| [...ntrollers/multicluster/serviceexport\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-bXVsdGljbHVzdGVyL2NvbnRyb2xsZXJzL211bHRpY2x1c3Rlci9zZXJ2aWNlZXhwb3J0X2NvbnRyb2xsZXIuZ28=) | `76.33% <0.00%> (-2.80%)` | :arrow_down: |
| ... and [4 more](https://codecov.io/gh/antrea-io/antrea/pull/4505/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | |


Patch:
diff --git a/.github/workflows/process_release.yml b/.github/workflows/process_release.yml
index 7e47a1a9c..254755c0f 100644
--- a/.github/workflows/process_release.yml
+++ b/.github/workflows/process_release.yml
@@ -182,6 +182,15 @@ jobs:
         asset_path: ./assets/antrea-windows.yml
         asset_name: antrea-windows.yml
         asset_content_type: application/octet-stream
+    - name: Upload antrea-windows-containerd.yml
+      uses: actions/upload-release-asset@v1
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      with:
+        upload_url: ${{ github.event.release.upload_url }}
+        asset_path: ./assets/antrea-windows-containerd.yml
+        asset_name: antrea-windows-containerd.yml
+        asset_content_type: application/octet-stream
     - name: Upload flow-aggregator.yml
       uses: actions/upload-release-asset@v1
       env:
diff --git a/CHANGELOG/CHANGELOG-1.10.md b/CHANGELOG/CHANGELOG-1.10.md
new file mode 100644
index 000000000..d83c6a51d
--- /dev/null
+++ b/CHANGELOG/CHANGELOG-1.10.md
@@ -0,0 +1,73 @@
+# Changelog 1.10
+
+## 1.10.0 - 2022-12-23
+
+### Added
+
+- Add L7NetworkPolicy feature which enables users to protect their applications by specifying how they are allowed to communicate with others, taking into account application context. ([#4380](https://github.com/antrea-io/antrea/pull/4380) [#4406](https://github.com/antrea-io/antrea/pull/4406) [#4410](https://github.com/antrea-io/antrea/pull/4410), [@hongliangl] [@qiyueyao] [@tnqn])
+    * Layer 7 NetworkPolicy can be configured through the `l7Protocols` field of Antrea-native policies.
+    * Refer to [this document](https://github.com/antrea-io/antrea/blob/release-1.10/docs/antrea-l7-network-policy.md) for more information about this feature.
+- Add SupportBundleCollection feature which enables a CRD API for Antrea to collect support bundle files on any K8s Node or ExternalNode, and upload to a user-defined file server. ([#4184](https://github.com/antrea-io/antrea/pull/4184) [#4338](https://github.com/antrea-io/antrea/pull/4338) [#4249](https://github.com/antrea-io/antrea/pull/4249), [@wenyingd] [@mengdie-song] [@ceclinux])
+    * Refer to [this document](https://github.com/antrea-io/antrea/blob/release-1.10/docs/support-bundle-guide.md) for more information about this feature.
+- Add support for NetworkPolicy for cross-cluster traffic. ([#4432](https://github.com/antrea-io/antrea/pull/4432) [#3914](https://github.com/antrea-io/antrea/pull/3914), [@Dyanngg] [@GraysonWu])
+    * Setting `scope` of an ingress peer to `clusterSet` expands the scope of the `podSelector` or `namespaceSelector` to the entire ClusterSet.
+    * Setting `scope` of `toServices` to `clusterSet` selects a Multi-cluster Service. ([#4397](https://github.com/antrea-io/antrea/pull/4397), [@Dyanngg])
+    * Refer to [this document](https://github.com/antrea-io/antrea/blob/release-1.10/docs/multicluster/user-guide.md#networkpolicy-for-cross-cluster-traffic) for more information about this feature.
+- Add the following capabilities to the ExternalNode feature:
+    * Containerized option for antrea-agent installation on Linux VMs. ([#4413](https://github.com/antrea-io/antrea/pull/4413), [@Nithish555])
+    * Support for RHEL 8.4. ([#4323](https://github.com/antrea-io/antrea/pull/4323), [@Nithish555])
+- Add support for running antrea-agent as DaemonSet when using containerd as the runtime on Windows. ([#4279](https://github.com/antrea-io/antrea/pull/4279), [@XinShuYang])
+- Add [documentation](https://github.com/antrea-io/antrea/blob/release-1.10/docs/multicast-guide.md) for Antrea Multicast. ([#4339](https://github.com/antrea-io/antrea/pull/4339), [@ceclinux])
+
+### Changed
+
+- Extend `antctl mc get joinconfig` to print member token Secret. ([#4363](https://github.com/antrea-io/antrea/pull/4363), [@jianjuns])
+- Improve support for Egress in Traceflow. ([#3926](https://github.com/antrea-io/antrea/pull/3926), [@Atish-iaf])
+- Add NodePortLocalPortRange field for AntreaAgentInfo. ([#4379](https://github.com/antrea-io/antrea/pull/4379), [@wenqiq])
+- Use format "namespace/name" as the key for ExternalNode span calculation. ([#4401](https://github.com/antrea-io/antrea/pull/4401), [@wenyingd])
+- Enclose Pod labels with single quotes when uploading CSV record to S3 in the FlowAggregator. ([#4334](https://github.com/antrea-io/antrea/pull/4334), [@dreamtalen])
+- Upgrade Antrea base image to ubuntu 22.04. ([#4459](https://github.com/antrea-io/antrea/pull/4459) [#4499](https://github.com/antrea-io/antrea/pull/4499), [@antoninbas])
+- Update OVS to 2.17.3. ([#4402](https://github.com/antrea-io/antrea/pull/4402), [@mnaser])
+- Reduce confusion caused by transient error encountered when creating static Tiers. ([#4414](https://github.com/antrea-io/antrea/pull/4414), [@tnqn])
+
+### Fixed
+
+- Add a periodic job to rejoin dead Nodes, to fix Egress not working properly after long network downtime. ([#4491](https://github.com/antrea-io/antrea/pull/4491), [@tnqn])
+- Fix potential deadlocks and memory leaks of memberlist maintenance in large-scale clusters. ([#4469](https://github.com/antrea-io/antrea/pull/4469), [@wenyingd])
+- Fix connectivity issues caused by MAC address changes with systemd v242 and later. ([#4428](https://github.com/antrea-io/antrea/pull/4428), [@wenyingd])
+- Fix error handling when S3Uploader partially succeeds. ([#4433](https://github.com/antrea-io/antrea/pull/4433), [@heanlan])
+- Fix a ClusterInfo export bug when Multi-cluster Gateway changes. ([#4412](https://github.com/antrea-io/antrea/pull/4412), [@luolanzone])
+- Fix OpenFlow rules not being updated when Multi-cluster Gateway updates. ([#4388](https://github.com/antrea-io/antrea/pull/4388), [@luolanzone])
+- Delete Pod specific VF resource cache when a Pod gets deleted. ([#4285](https://github.com/antrea-io/antrea/pull/4285), [@arunvelayutham])
+- Fix OpenAPI descriptions for AntreaAgentInfo and AntreaControllerInfo. ([#4390](https://github.com/antrea-io/antrea/pull/4390), [@tnqn])
+
+
+[@Atish-iaf]: https://github.com/Atish-iaf
+[@Dyanngg]: https://github.com/Dyanngg
+[@GraysonWu]: https://github.com/GraysonWu
+[@NamanAg30]: https://github.com/NamanAg30
+[@Nithish555]: https://github.com/Nithish555
+[@XinShuYang]: https://github.com/XinShuYang
+[@adwaitni]: https://github.com/adwaitni
+[@antoninbas]: https://github.com/antoninbas
+[@antrea-bot]: https://github.com/antrea-bot
+[@arunvelayutham]: https://github.com/arunvelayutham
+[@bangqipropel]: https://github.com/bangqipropel
+[@ceclinux]: https://github.com/ceclinux
+[@dependabot]: https://github.com/dependabot
+[@dreamtalen]: https://github.com/dreamtalen
+[@heanlan]: https://github.com/heanlan
+[@hjiajing]: https://github.com/hjiajing
+[@hongliangl]: https://github.com/hongliangl
+[@jainpulkit22]: https://github.com/jainpulkit22
+[@jianjuns]: https://github.com/jianjuns
+[@liu4480]: https://github.com/liu4480
+[@luolanzone]: https://github.com/luolanzone
+[@mengdie-song]: https://github.com/mengdie-song
+[@mnaser]: https://github.com/mnaser
+[@qiyueyao]: https://github.com/qiyueyao
+[@tnqn]: https://github.com/tnqn
+[@urharshitha]: https://github.com/urharshitha
+[@wenqiq]: https://github.com/wenqiq
+[@wenyingd]: https://github.com/wenyingd
+[@xliuxu]: https://github.com/xliuxu
\ No newline at end of file
diff --git a/CHANGELOG/README.md b/CHANGELOG/README.md
index f78dcf658..018f77585 100644
--- a/CHANGELOG/README.md
+++ b/CHANGELOG/README.md
@@ -10,6 +10,7 @@ stages](https://github.com/kubernetes/community/blob/master/contributors/devel/s
 Some experimental features can be enabled / disabled using [Feature
 Gates](../docs/feature-gates.md).
 
+- [CHANGELOG-1.10](CHANGELOG-1.10.md)
 - [CHANGELOG-1.9](CHANGELOG-1.9.md)
 - [CHANGELOG-1.8](CHANGELOG-1.8.md)
 - [CHANGELOG-1.7](CHANGELOG-1.7.md)
diff --git a/build/charts/antrea/README.md b/build/charts/antrea/README.md
index ae4ec661c..c12fc5b4b 100644
--- a/build/charts/antrea/README.md
+++ b/build/charts/antrea/README.md
@@ -85,7 +85,7 @@ Kubernetes: `>= 1.16.0-0`
 | multicast.igmpQueryInterval | string | `"125s"` | The interval at which the antrea-agent sends IGMP queries to Pods. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". |
 | multicast.multicastInterfaces | list | `[]` | Names of the interfaces on Nodes that are used to forward multicast traffic. |
 | multicluster.enable | bool | `false` | Enable Antrea Multi-cluster Gateway to support cross-cluster traffic. This feature is supported only with encap mode. |
-| multicluster.enableStretchedNetworkPolicy | bool | `false` | Enable StretchedNetworkPolicy which allow Antrea-native policies to select peers from other clusters in a ClusterSet. This feature is supported only with encap mode when the tunnel type is Geneve. |
+| multicluster.enableStretchedNetworkPolicy | bool | `false` | Enable StretchedNetworkPolicy which allows Antrea-native policies to select peers from other clusters in a ClusterSet. This feature is supported only with encap mode. |
 | multicluster.namespace | string | `""` | The Namespace where Antrea Multi-cluster Controller is running. The default is antrea-agent's Namespace. |
 | noSNAT | bool | `false` | Whether or not to SNAT (using the Node IP) the egress traffic from a Pod to the external network. |
 | nodeIPAM.clusterCIDRs | list | `[]` | CIDR ranges to use when allocating Pod IP addresses. |
diff --git a/build/charts/antrea/conf/antrea-agent.conf b/build/charts/antrea/conf/antrea-agent.conf
index 33746f64a..eca4a29a7 100644
--- a/build/charts/antrea/conf/antrea-agent.conf
+++ b/build/charts/antrea/conf/antrea-agent.conf
@@ -70,6 +70,10 @@ featureGates:
 # Enable collecting support bundle files with SupportBundleCollection CRD.
 {{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "SupportBundleCollection" "default" false) }}
 
+# Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+# into account application context.
+{{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "L7NetworkPolicy" "default" false) }}
+
 # Name of the OpenVSwitch bridge antrea-agent will create and use.
 # Make sure it doesn't conflict with your existing OpenVSwitch bridges.
 ovsBridge: {{ .Values.ovs.bridgeName | quote }}
@@ -334,7 +338,7 @@ multicluster:
 # The default is antrea-agent's Namespace.
   namespace: {{ .namespace | quote }}
 # Enable StretchedNetworkPolicy which could be enforced on cross-cluster traffic.
-# This feature is supported only with encap mode when the tunnel type is Geneve.
+# This feature is supported only with encap mode.
   enableStretchedNetworkPolicy: {{ .enableStretchedNetworkPolicy }}
 {{- end }}
 
diff --git a/build/charts/antrea/conf/antrea-controller.conf b/build/charts/antrea/conf/antrea-controller.conf
index 520b0a0c3..d6d8edfce 100644
--- a/build/charts/antrea/conf/antrea-controller.conf
+++ b/build/charts/antrea/conf/antrea-controller.conf
@@ -46,6 +46,10 @@ featureGates:
 # Enable multi-cluster features.
 {{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "Multicluster" "default" false) }}
 
+# Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+# into account application context.
+{{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "L7NetworkPolicy" "default" false) }}
+
 # The port for the antrea-controller APIServer to serve on.
 # Note that if it's set to another value, the `containerPort` of the `api` port of the
 # `antrea-controller` container must be set to the same value.
@@ -111,7 +115,7 @@ ipsecCSRSigner:
 
 multicluster:
 {{- with .Values.multicluster }}
-  # Enable StretchedNetworkPolicy which allow Antrea-native policies to select peers
+  # Enable StretchedNetworkPolicy which allows Antrea-native policies to select peers
   # from other clusters in a ClusterSet.
   enableStretchedNetworkPolicy: {{ .enableStretchedNetworkPolicy }}
 {{- end }}
diff --git a/build/charts/antrea/values.yaml b/build/charts/antrea/values.yaml
index e1af501f1..f1f9e5e47 100644
--- a/build/charts/antrea/values.yaml
+++ b/build/charts/antrea/values.yaml
@@ -319,7 +319,7 @@ logVerbosity: 0
 whereabouts:
   enable: false
 
-## -- Configure Multicluster defaults for both Antrea controller and agent.
+## -- Configure Multicluster defaults for both Antrea Controller and Agent.
 multicluster:
   # -- Enable Antrea Multi-cluster Gateway to support cross-cluster traffic.
   # This feature is supported only with encap mode.
@@ -327,9 +327,9 @@ multicluster:
   # -- The Namespace where Antrea Multi-cluster Controller is running.
   # The default is antrea-agent's Namespace.
   namespace: ""
-  # -- Enable StretchedNetworkPolicy which allow Antrea-native policies to select peers
+  # -- Enable StretchedNetworkPolicy which allows Antrea-native policies to select peers
   # from other clusters in a ClusterSet.
-  # This feature is supported only with encap mode when the tunnel type is Geneve.
+  # This feature is supported only with encap mode.
   enableStretchedNetworkPolicy: false
 
 testing:
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 2992b4363..a4daa83da 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -2993,6 +2993,10 @@ data:
     # Enable collecting support bundle files with SupportBundleCollection CRD.
     #  SupportBundleCollection: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # Name of the OpenVSwitch bridge antrea-agent will create and use.
     # Make sure it doesn't conflict with your existing OpenVSwitch bridges.
     ovsBridge: "br-int"
@@ -3229,7 +3233,7 @@ data:
     # The default is antrea-agent's Namespace.
       namespace: ""
     # Enable StretchedNetworkPolicy which could be enforced on cross-cluster traffic.
-    # This feature is supported only with encap mode when the tunnel type is Geneve.
+    # This feature is supported only with encap mode.
       enableStretchedNetworkPolicy: false
   antrea-cni.conflist: |
     {
@@ -3303,6 +3307,10 @@ data:
     # Enable multi-cluster features.
     #  Multicluster: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # The port for the antrea-controller APIServer to serve on.
     # Note that if it's set to another value, the `containerPort` of the `api` port of the
     # `antrea-controller` container must be set to the same value.
@@ -3360,7 +3368,7 @@ data:
       selfSignedCA: true
 
     multicluster:
-      # Enable StretchedNetworkPolicy which allow Antrea-native policies to select peers
+      # Enable StretchedNetworkPolicy which allows Antrea-native policies to select peers
       # from other clusters in a ClusterSet.
       enableStretchedNetworkPolicy: false
 ---
@@ -4285,7 +4293,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: f06bc0519d38f9edaaa4516e816c564429500bf878c777abc41033b346e8edff
+        checksum/config: 7b21809cc047cd7464cdb8789428396de9dcf2684166821d8e5dbd7816e005c4
       labels:
         app: antrea
         component: antrea-agent
@@ -4526,7 +4534,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: f06bc0519d38f9edaaa4516e816c564429500bf878c777abc41033b346e8edff
+        checksum/config: 7b21809cc047cd7464cdb8789428396de9dcf2684166821d8e5dbd7816e005c4
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index b72422591..9d66dee12 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -2993,6 +2993,10 @@ data:
     # Enable collecting support bundle files with SupportBundleCollection CRD.
     #  SupportBundleCollection: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # Name of the OpenVSwitch bridge antrea-agent will create and use.
     # Make sure it doesn't conflict with your existing OpenVSwitch bridges.
     ovsBridge: "br-int"
@@ -3229,7 +3233,7 @@ data:
     # The default is antrea-agent's Namespace.
       namespace: ""
     # Enable StretchedNetworkPolicy which could be enforced on cross-cluster traffic.
-    # This feature is supported only with encap mode when the tunnel type is Geneve.
+    # This feature is supported only with encap mode.
       enableStretchedNetworkPolicy: false
   antrea-cni.conflist: |
     {
@@ -3303,6 +3307,10 @@ data:
     # Enable multi-cluster features.
     #  Multicluster: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # The port for the antrea-controller APIServer to serve on.
     # Note that if it's set to another value, the `containerPort` of the `api` port of the
     # `antrea-controller` container must be set to the same value.
@@ -3360,7 +3368,7 @@ data:
       selfSignedCA: true
 
     multicluster:
-      # Enable StretchedNetworkPolicy which allow Antrea-native policies to select peers
+      # Enable StretchedNetworkPolicy which allows Antrea-native policies to select peers
       # from other clusters in a ClusterSet.
       enableStretchedNetworkPolicy: false
 ---
@@ -4285,7 +4293,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: f06bc0519d38f9edaaa4516e816c564429500bf878c777abc41033b346e8edff
+        checksum/config: 7b21809cc047cd7464cdb8789428396de9dcf2684166821d8e5dbd7816e005c4
       labels:
         app: antrea
         component: antrea-agent
@@ -4528,7 +4536,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: f06bc0519d38f9edaaa4516e816c564429500bf878c777abc41033b346e8edff
+        checksum/config: 7b21809cc047cd7464cdb8789428396de9dcf2684166821d8e5dbd7816e005c4
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index 73132f44b..945c128c8 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -2993,6 +2993,10 @@ data:
     # Enable collecting support bundle files with SupportBundleCollection CRD.
     #  SupportBundleCollection: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # Name of the OpenVSwitch bridge antrea-agent will create and use.
     # Make sure it doesn't conflict with your existing OpenVSwitch bridges.
     ovsBridge: "br-int"
@@ -3229,7 +3233,7 @@ data:
     # The default is antrea-agent's Namespace.
       namespace: ""
     # Enable StretchedNetworkPolicy which could be enforced on cross-cluster traffic.
-    # This feature is supported only with encap mode when the tunnel type is Geneve.
+    # This feature is supported only with encap mode.
       enableStretchedNetworkPolicy: false
   antrea-cni.conflist: |
     {
@@ -3303,6 +3307,10 @@ data:
     # Enable multi-cluster features.
     #  Multicluster: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # The port for the antrea-controller APIServer to serve on.
     # Note that if it's set to another value, the `containerPort` of the `api` port of the
     # `antrea-controller` container must be set to the same value.
@@ -3360,7 +3368,7 @@ data:
       selfSignedCA: true
 
     multicluster:
-      # Enable StretchedNetworkPolicy which allow Antrea-native policies to select peers
+      # Enable StretchedNetworkPolicy which allows Antrea-native policies to select peers
       # from other clusters in a ClusterSet.
       enableStretchedNetworkPolicy: false
 ---
@@ -4285,7 +4293,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 4c94da583c601f67c4369808e59e884c0ea07d35b3f295e7a9fe3e14b80221a5
+        checksum/config: f6d41cf17b8d26efc6cc18a1609cf3fb5f1a86c538423ae2925b689bf153937a
       labels:
         app: antrea
         component: antrea-agent
@@ -4525,7 +4533,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 4c94da583c601f67c4369808e59e884c0ea07d35b3f295e7a9fe3e14b80221a5
+        checksum/config: f6d41cf17b8d26efc6cc18a1609cf3fb5f1a86c538423ae2925b689bf153937a
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index cd494d4ff..67e79a066 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -3006,6 +3006,10 @@ data:
     # Enable collecting support bundle files with SupportBundleCollection CRD.
     #  SupportBundleCollection: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # Name of the OpenVSwitch bridge antrea-agent will create and use.
     # Make sure it doesn't conflict with your existing OpenVSwitch bridges.
     ovsBridge: "br-int"
@@ -3242,7 +3246,7 @@ data:
     # The default is antrea-agent's Namespace.
       namespace: ""
     # Enable StretchedNetworkPolicy which could be enforced on cross-cluster traffic.
-    # This feature is supported only with encap mode when the tunnel type is Geneve.
+    # This feature is supported only with encap mode.
       enableStretchedNetworkPolicy: false
   antrea-cni.conflist: |
     {
@@ -3316,6 +3320,10 @@ data:
     # Enable multi-cluster features.
     #  Multicluster: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # The port for the antrea-controller APIServer to serve on.
     # Note that if it's set to another value, the `containerPort` of the `api` port of the
     # `antrea-controller` container must be set to the same value.
@@ -3373,7 +3381,7 @@ data:
       selfSignedCA: true
 
     multicluster:
-      # Enable StretchedNetworkPolicy which allow Antrea-native policies to select peers
+      # Enable StretchedNetworkPolicy which allows Antrea-native policies to select peers
       # from other clusters in a ClusterSet.
       enableStretchedNetworkPolicy: false
 ---
@@ -4298,7 +4306,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 8a0ea8f57943caf7de83015414c2bb78c4283a7280118ce3a2160e121e8829b9
+        checksum/config: f5735f8044ed27afdd88d1d3e120a60aaef5e513bfe15602f958d6f33b0c487a
         checksum/ipsec-secret: d0eb9c52d0cd4311b6d252a951126bf9bea27ec05590bed8a394f0f792dcb2a4
       labels:
         app: antrea
@@ -4584,7 +4592,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 8a0ea8f57943caf7de83015414c2bb78c4283a7280118ce3a2160e121e8829b9
+        checksum/config: f5735f8044ed27afdd88d1d3e120a60aaef5e513bfe15602f958d6f33b0c487a
       labels:
         app: antrea
         component: antrea-controller
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index 34bcec8fb..0d4ad7c43 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -2993,6 +2993,10 @@ data:
     # Enable collecting support bundle files with SupportBundleCollection CRD.
     #  SupportBundleCollection: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # Name of the OpenVSwitch bridge antrea-agent will create and use.
     # Make sure it doesn't conflict with your existing OpenVSwitch bridges.
     ovsBridge: "br-int"
@@ -3229,7 +3233,7 @@ data:
     # The default is antrea-agent's Namespace.
       namespace: ""
     # Enable StretchedNetworkPolicy which could be enforced on cross-cluster traffic.
-    # This feature is supported only with encap mode when the tunnel type is Geneve.
+    # This feature is supported only with encap mode.
       enableStretchedNetworkPolicy: false
   antrea-cni.conflist: |
     {
@@ -3303,6 +3307,10 @@ data:
     # Enable multi-cluster features.
     #  Multicluster: false
 
+    # Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+    # into account application context.
+    #  L7NetworkPolicy: false
+
     # The port for the antrea-controller APIServer to serve on.
     # Note that if it's set to another value, the `containerPort` of the `api` port of the
     # `antrea-controller` container must be set to the same value.
@@ -3360,7 +3368,7 @@ data:
       selfSignedCA: true
 
     multicluster:
-      # Enable StretchedNetworkPolicy which allow Antrea-native policies to select peers
+      # Enable StretchedNetworkPolicy which allows Antrea-native policies to select peers
       # from other clusters in a ClusterSet.
       enableStretchedNetworkPolicy: false
 ---
@@ -4285,7 +4293,7 @@ spec:
         kubectl.kubernetes.io/default-container: antrea-agent
         # Automatically restart Pods with a RollingUpdate if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 7ef1495672b7b4b2ed3d7729f27af861e3f619bd3343488d45b9c8c3195106cd
+        checksum/config: e89bb782608697941dc73d3b38a678483ec45559a617d8ed614c1397abb40524
       labels:
         app: antrea
         component: antrea-agent
@@ -4525,7 +4533,7 @@ spec:
       annotations:
         # Automatically restart Pod if the ConfigMap changes
         # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments
-        checksum/config: 7ef1495672b7b4b2ed3d7729f27af861e3f619bd3343488d45b9c8c3195106cd
+        checksum/config: e89bb782608697941dc73d3b38a678483ec45559a617d8ed614c1397abb40524
       labels:
         app: antrea
         component: antrea-controller
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index fe0de0c5c..88baf12c5 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -136,16 +136,19 @@ func run(o *Options) error {
 	enableBridgingMode := enableAntreaIPAM && o.config.EnableBridgingMode
 	enableNodePortLocal := features.DefaultFeatureGate.Enabled(features.NodePortLocal) && o.config.NodePortLocal.Enable
 	enableMulticluster := features.DefaultFeatureGate.Enabled(features.Multicluster) && o.config.Multicluster.Enable
+	l7NetworkPolicyEnabled := features.DefaultFeatureGate.Enabled(features.L7NetworkPolicy)
 	// Bridging mode will connect the uplink interface to the OVS bridge.
 	connectUplinkToBridge := enableBridgingMode
 
 	ovsDatapathType := ovsconfig.OVSDatapathType(o.config.OVSDatapathType)
 	ovsBridgeClient := ovsconfig.NewOVSBridge(o.config.OVSBridge, ovsDatapathType, ovsdbConnection)
+	ovsCtlClient := ovsctl.NewClient(o.config.OVSBridge)
 	ovsBridgeMgmtAddr := ofconfig.GetMgmtAddress(o.config.OVSRunDir, o.config.OVSBridge)
 	multicastEnabled := features.DefaultFeatureGate.Enabled(features.Multicast)
 	ofClient := openflow.NewClient(o.config.OVSBridge, ovsBridgeMgmtAddr,
 		features.DefaultFeatureGate.Enabled(features.AntreaProxy),
 		features.DefaultFeatureGate.Enabled(features.AntreaPolicy),
+		l7NetworkPolicyEnabled,
 		egressEnabled,
 		features.DefaultFeatureGate.Enabled(features.FlowExporter),
 		o.config.AntreaProxy.ProxyAll,
@@ -236,6 +239,7 @@ func run(o *Options) error {
 		k8sClient,
 		crdClient,
 		ovsBridgeClient,
+		ovsCtlClient,
 		ofClient,
 		routeClient,
 		ifaceStore,
@@ -252,7 +256,8 @@ func run(o *Options) error {
 		o.config.ExternalNode.ExternalNodeNamespace,
 		features.DefaultFeatureGate.Enabled(features.AntreaProxy),
 		o.config.AntreaProxy.ProxyAll,
-		connectUplinkToBridge)
+		connectUplinkToBridge,
+		l7NetworkPolicyEnabled)
 	err = agentInitializer.Initialize()
 	if err != nil {
 		return fmt.Errorf("error initializing agent: %v", err)
@@ -411,6 +416,7 @@ func run(o *Options) error {
 		groupCounters,
 		groupIDUpdates,
 		antreaPolicyEnabled,
+		l7NetworkPolicyEnabled,
 		antreaProxyEnabled,
 		statusManagerEnabled,
 		multicastEnabled,
@@ -666,7 +672,7 @@ func run(o *Options) error {
 		tcController := trafficcontrol.NewTrafficControlController(ofClient,
 			ifaceStore,
 			ovsBridgeClient,
-			ovsctl.NewClient(o.config.OVSBridge),
+			ovsCtlClient,
 			trafficControlInformer,
 			localPodInformer,
 			namespaceInformer,
diff --git a/docs/antrea-l7-network-policy.md b/docs/antrea-l7-network-policy.md
new file mode 100644
index 000000000..198a8b5de
--- /dev/null
+++ b/docs/antrea-l7-network-policy.md
@@ -0,0 +1,205 @@
+# Antrea Layer 7 NetworkPolicy
+
+## Table of Contents
+
+<!-- toc -->
+- [Introduction](#introduction)
+- [Prerequisites](#prerequisites)
+- [Usage](#usage)
+  - [HTTP](#http)
+    - [More examples](#more-examples)
+- [Limitations](#limitations)
+<!-- /toc -->
+
+## Introduction
+
+NetworkPolicy was initially used to restrict network access at layer 3 (Network) and 4 (Transport) in the OSI model,
+based on IP address, transport protocol, and port. Securing applications at IP and port level provides limited security
+capabilities, as the service an application provides is either entirely exposed to a client or not accessible by that
+client at all. Starting with v1.10, Antrea introduces support for layer 7 NetworkPolicy, an application-aware policy
+which provides fine-grained control over the network traffic beyond IP, transport protocol, and port. It enables users
+to protect their applications by specifying how they are allowed to communicate with others, taking into account
+application context. For example, you can enforce policies to:
+
+- Grant access of privileged URLs to specific clients while make other URLs publicly accessible.
+- Prevent applications from accessing unauthorized domains.
+- Block network traffic using an unauthorized application protocol regardless of port used.
+
+This guide demonstrates how to configure layer 7 NetworkPolicy.
+
+## Prerequisites
+
+Layer 7 NetworkPolicy was introduced in v1.10 as an alpha feature and is disabled by default. A feature gate,
+`L7NetworkPolicy`, must be enabled in antrea-controller.conf and antrea-agent.conf in the `antrea-config` ConfigMap.
+Additionally, due to the constraint of the application detection engine, TX checksum offloading must be disabled via the
+`disableTXChecksumOffload` option in antrea-agent.conf for the feature to work. An example configuration is as below:
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: antrea-config
+  namespace: kube-system
+data:
+  antrea-agent.conf: |
+    disableTXChecksumOffload: true
+    featureGates:
+      L7NetworkPolicy: true
+  antrea-controller.conf: |
+    featureGates:
+      L7NetworkPolicy: true
+```
+
+Alternatively, you can use the following helm installation command to configure the above options:
+
+```bash
+helm install antrea antrea/antrea --namespace kube-system --set featureGates.L7NetworkPolicy=true,disableTXChecksumOffload=true
+```
+
+## Usage
+
+There isn't a separate resource type for layer 7 NetworkPolicy. It is one kind of Antrea-native policies, which has the
+`l7Protocols` field specified in the rules. Like layer 3 and layer 4 policies, the `l7Protocols` field can be specified
+for ingress and egress rules in Antrea ClusterNetworkPolicy and Antrea NetworkPolicy. It can be used with the `from` or
+`to` field to select the network peer, and the `ports` to select the transport protocol and/or port for which the layer
+7 rule applies to. The `action` of a layer 7 rule can only be `Allow`.
+
+**Note**: Any traffic matching the layer 3/4 criteria (specified by `from`, `to`, and `port`) of a layer 7 rule will be
+forwarded to an application-aware engine for protocol detection and rule enforcement, and the traffic will be allowed if
+the layer 7 criteria is also matched, otherwise it will be dropped. Therefore, any rules after a layer 7 rule will not
+be enforced for the traffic that match the layer 7 rule's layer 3/4 criteria.
+
+As of now, the only supported layer 7 protocol is HTTP. More protocols will be supported in the near future, and we
+welcome feature requests for protocols that you are interested in.
+
+### HTTP
+
+A typical layer 7 NetworkPolicy for HTTP protocol is as below:
+
+```yaml
+apiVersion: crd.antrea.io/v1alpha1
+kind: NetworkPolicy
+metadata:
+  name: ingress-allow-http-request-to-api-v2
+spec:
+  priority: 5
+  tier: application
+  appliedTo:
+    - podSelector:
+        matchLabels:
+          app: web
+  ingress:
+    - name: allow-http   # Allow inbound HTTP GET requests to "/api/v2" from Pods with app=client label.
+      action: Allow      # All other traffic from these Pods will be automatically dropped, and subsequent rules will not be considered.
+      from:
+        - podSelector:
+            matchLabels:
+              app: client
+      l7Protocols:
+        - http:
+            path: "/api/v2/*"
+            host: "foo.bar.com"
+            method: "GET"
+    - name: drop-other   # Drop all other inbound traffic (i.e., from Pods without the app=client label or from external clients).
+      action: Drop
+```
+
+**path**: The `path` field represents the URI path to match. Both exact matches and wildcards are supported, e.g.
+`/api/v2/*`, `*/v2/*`, `/index.html`. If not set, the rule matches all URI paths.
+
+**host**: The `host` field represents the hostname present in the URI or the HTTP Host header to match. It does not
+contain the port associated with the host. Both exact matches and wildcards are supported, e.g. `*.foo.com`, `*.foo.*`,
+`foo.bar.com`. If not set, the rule matches all hostnames.
+
+**method**: The `method` field represents the HTTP method to match. It could be GET, POST, PUT, HEAD, DELETE, TRACE,
+OPTIONS, CONNECT and PATCH. If not set, the rule matches all methods.
+
+#### More examples
+
+The following NetworkPolicy grants access of privileged URLs to specific clients while make other URLs publicly
+accessible:
+
+```yaml
+apiVersion: crd.antrea.io/v1alpha1
+kind: NetworkPolicy
+metadata:
+  name: allow-privileged-url-to-admin-role
+spec:
+  priority: 5
+  tier: securityops
+  appliedTo:
+    - podSelector:
+        matchLabels:
+          app: web
+  ingress:
+    - name: for-admin    # Allow inbound HTTP GET requests to "/admin" and "/public" from Pods with role=admin label.
+      action: Allow
+      from:
+        - podSelector:
+            matchLabels:
+              role: admin
+      l7Protocols:
+        - http:
+            path: "/admin/*"
+        - http:
+            path: "/public/*"
+    - name: for-public   # Allow inbound HTTP GET requests to "/public" from Pods with app=client label.
+      action: Allow      # All other inbound traffic will be automatically dropped.
+      l7Protocols:
+        - http:
+            path: "/public/*"
+```
+
+The following NetworkPolicy prevents applications from accessing unauthorized domains:
+
+```yaml
+apiVersion: crd.antrea.io/v1alpha1
+kind: ClusterNetworkPolicy
+metadata:
+  name: allow-web-access-to-internal-domain
+spec:
+  priority: 5
+  tier: securityops
+  appliedTo:
+    - podSelector:
+        matchLabels:
+          egress-restriction: internal-domain-only
+  egress:
+    - name: allow-dns          # Allow outbound DNS requests.
+      action: Allow
+      ports:
+        - protocol: TCP
+          port: 53
+        - protocol: UDP
+          port: 53
+    - name: allow-http-only    # Allow outbound HTTP requests towards foo.bar.com. 
+      action: Allow            # As the rule's "to" and "ports" are empty, which means it selects traffic to any network
+      l7Protocols:             # peer's any port using any transport protocol, all outbound HTTP requests towards other
+        - http:                # domains and non-HTTP requests will be automatically dropped, and subsequent rules will
+            host: "*.bar.com"  # not be considered.
+```
+
+The following NetworkPolicy blocks network traffic using an unauthorized application protocol regardless of port used.
+
+```yaml
+apiVersion: crd.antrea.io/v1alpha1
+kind: NetworkPolicy
+metadata:
+  name: allow-http-only
+spec:
+  priority: 5
+  tier: application
+  appliedTo:
+    - podSelector:
+        matchLabels:
+          app: web
+  ingress:
+    - name: http-only    # Allow inbound HTTP requests only.
+      action: Allow      # As the rule's "from" and "ports" are empty, which means it selects traffic from any network
+      l7Protocols:       # peer to any port of the Pods this policy applies to, all inbound non-HTTP requests will be
+        - http: {}       # automatically dropped, and subsequent rules will not be considered.
+```
+
+## Limitations
+
+This feature is currently only supported for Nodes running Linux.
diff --git a/docs/antrea-network-policy.md b/docs/antrea-network-policy.md
index 5fbe7e3d7..1083bf5c8 100644
--- a/docs/antrea-network-policy.md
+++ b/docs/antrea-network-policy.md
@@ -19,6 +19,7 @@
     - [ACNP for ICMP traffic](#acnp-for-icmp-traffic)
     - [ACNP for IGMP traffic](#acnp-for-igmp-traffic)
     - [ACNP for multicast egress traffic](#acnp-for-multicast-egress-traffic)
+    - [ACNP for HTTP traffic](#acnp-for-http-traffic)
   - [Behavior of <em>to</em> and <em>from</em> selectors](#behavior-of-to-and-from-selectors)
   - [Key differences from K8s NetworkPolicy](#key-differences-from-k8s-networkpolicy)
   - [<em>kubectl</em> commands for Antrea ClusterNetworkPolicy](#kubectl-commands-for-antrea-clusternetworkpolicy)
@@ -477,6 +478,65 @@ spec:
         name: dropMcastUDPTraffic
 ```
 
+#### ACNP for HTTP traffic
+
+```yaml
+apiVersion: crd.antrea.io/v1alpha1
+kind: NetworkPolicy
+metadata:
+  name: ingress-allow-http-request-to-api-v2
+spec:
+  priority: 5
+  tier: application
+  appliedTo:
+    - podSelector:
+        matchLabels:
+          app: web
+  ingress:
+    - name: allow-http   # Allow inbound HTTP GET requests to "/api/v2" from Pods with app=client label.
+      action: Allow      # All other traffic from these Pods will be automatically dropped, and subsequent rules will not be considered.
+      from:
+        - podSelector:
+            matchLabels:
+              app: client
+      l7Protocols:
+        - http:
+            path: "/api/v2/*"
+            host: "foo.bar.com"
+            method: "GET"
+    - name: drop-other   # Drop all other inbound traffic (i.e., from Pods without the app=client label or from external clients).
+      action: Drop
+```
+
+```yaml
+apiVersion: crd.antrea.io/v1alpha1
+kind: ClusterNetworkPolicy
+metadata:
+  name: allow-web-access-to-internal-domain
+spec:
+  priority: 5
+  tier: securityops
+  appliedTo:
+    - podSelector:
+        matchLabels:
+          egress-restriction: internal-domain-only
+  egress:
+    - name: allow-dns          # Allow outbound DNS requests.
+      action: Allow
+      ports:
+        - protocol: TCP
+          port: 53
+        - protocol: UDP
+          port: 53
+    - name: allow-http-only    # Allow outbound HTTP requests towards foo.bar.com.
+      action: Allow            # As the rule's "to" and "ports" are empty, which means it selects traffic to any network
+      l7Protocols:             # peer's any port using any transport protocol, all outbound HTTP requests towards other
+        - http:                # domains and non-HTTP requests will be automatically dropped, and subsequent rules will
+            host: "*.bar.com"  # not be considered.
+```
+
+Please refer to [Antrea Layer 7 NetworkPolicy](antrea-l7-network-policy.md) for extra information.
+
 **spec**: The ClusterNetworkPolicy `spec` has all the information needed to
 define a cluster-wide security policy.
 
diff --git a/docs/feature-gates.md b/docs/feature-gates.md
index cc5efaf51..e6904f235 100644
--- a/docs/feature-gates.md
+++ b/docs/feature-gates.md
@@ -49,8 +49,10 @@ edit the Agent configuration in the
 | `SecondaryNetwork`        | Agent              | `false` | Alpha | v1.5          | N/A          | N/A        | Yes                |       |
 | `ServiceExternalIP`       | Agent + Controller | `false` | Alpha | v1.5          | N/A          | N/A        | Yes                |       |
 | `TrafficControl`          | Agent              | `false` | Alpha | v1.7          | N/A          | N/A        | No                 |       |
+| `Multicluster`            | Agent + Controller | `false` | Alpha | v1.7          | N/A          | N/A        | Yes                | Controller side feature gate added in v1.10.0 |
 | `ExternalNode`            | Agent              | `false` | Alpha | v1.8          | N/A          | N/A        | Yes                |       |
 | `SupportBundleCollection` | Agent + Controller | `false` | Alpha | v1.10         | N/A          | N/A        | Yes                |       |
+| `L7NetworkPolicy`         | Agent + Controller | `false` | Alpha | v1.10         | N/A          | N/A        | Yes                |       |
 
 ## Description and Requirements of Features
 
@@ -314,6 +316,18 @@ device or a remote destination via a tunnel of various types. It enables a monit
 into network traffic, including both north-south and east-west traffic. Refer to this [document](traffic-control.md)
 for more information.
 
+### Multicluster
+
+The `Multicluster` feature gate of Antrea Agent enables [Antrea Multi-cluster Gateways](multicluster/user-guide.md#multi-cluster-gateway-configuration)
+which route Multi-cluster Service and Pod traffic through tunnels across clusters. The `Multicluster` feature gate
+of Antrea Controller enables support for [Multi-cluster NetworkPolicy](multicluster/user-guide.md#networkpolicy-for-cross-cluster-traffic).
+
+#### Requirements for this Feature
+
+User should install Antrea Multi-cluster controllers in a ClusterSet to enable multi-cluster Service and Pod access
+across a group of member clusters. This feature is currently only supported for IPv4. Refer to this
+[document](multicluster/architecture.md) for more details.
+
 ### ExternalNode
 
 The `ExternalNode` feature enables Antrea Agent runs on a virtual machine or a bare-metal server which is not a
@@ -341,3 +355,14 @@ More documentation will be coming in the future.
 
 User should provide a file server with this feature, and store its authentication credential in a Secret. Antrea
 Controller is required to be configured with the permission to read the Secret.
+
+### L7NetworkPolicy
+
+`L7NetworkPolicy` enables users to protect their applications by specifying how they are allowed to communicate with
+others, taking into account application context, providing fine-grained control over the network traffic beyond IP,
+transport protocol, and port. Refer to this [document](antrea-l7-network-policy.md) for more information.
+
+#### Requirements for this Feature
+
+This feature is currently only supported for Nodes running Linux, and TX checksum offloading must be disabled. Refer to
+this [document](antrea-l7-network-policy.md#prerequisites) for more information and how it can be configured.
diff --git a/docs/multicluster/user-guide.md b/docs/multicluster/user-guide.md
index d7e62ef3f..475664654 100644
--- a/docs/multicluster/user-guide.md
+++ b/docs/multicluster/user-guide.md
@@ -10,8 +10,11 @@
   - [Create ClusterSet](#create-clusterset)
 - [Multi-cluster Gateway Configuration](#multi-cluster-gateway-configuration)
 - [Multi-cluster Service](#multi-cluster-service)
-- [Multi-cluster ClusterNetworkPolicy Replication](#multi-cluster-clusternetworkpolicy-replication)
 - [Multi-cluster Pod to Pod Connectivity](#multi-cluster-pod-to-pod-connectivity)
+- [Multi-cluster ClusterNetworkPolicy Replication](#multi-cluster-clusternetworkpolicy-replication)
+- [NetworkPolicy for Cross-cluster Traffic](#networkpolicy-for-cross-cluster-traffic)
+  - [Egress NetworkPolicy to Multi-cluster Services](#egress-networkpolicy-to-multi-cluster-services)
+  - [Ingress NetworkPolicy for Cross-cluster Traffic](#ingress-networkpolicy-for-cross-cluster-traffic)
 - [Build Antrea Multi-cluster Image](#build-antrea-multi-cluster-image)
 - [Known Issue](#known-issue)
 <!-- /toc -->
@@ -502,6 +505,44 @@ clusters, and it also requires Multi-cluster Gateways when there is no direct Po
 connectivity across clusters. Also refer to [Multi-cluster Pod-to-Pod Connectivity](#multi-cluster-pod-to-pod-connectivity)
 for more information.
 
+## Multi-cluster Pod-to-Pod Connectivity
+
+Since Antrea v1.9.0, Multi-cluster supports routing Pod traffic across clusters
+through Multi-cluster Gateways. Pod IPs can be reached in all member clusters
+within a ClusterSet. To enable this feature, the cluster's Pod CIDRs must be set in
+ConfigMap `antrea-mc-controller-config` of each member cluster like the example
+below. Note, **Pod CIDRs must not overlap among clusters to enable cross-cluster
+Pod-to-Pod connectivity**.
+
+```yaml
+apiVersion: v1
+data:
+  controller_manager_config.yaml: |
+    apiVersion: multicluster.crd.antrea.io/v1alpha1
+    kind: MultiClusterConfig
+    ...
+    podCIDRs:
+      - "10.10.1.1/16"
+kind: ConfigMap
+metadata:
+  labels:
+    app: antrea
+  name: antrea-mc-controller-config
+  namespace: kube-system
+```
+
+You can edit [antrea-multicluster-member.yml](../../multicluster/build/yamls/antrea-multicluster-member.yml),
+or use `kubectl edit` to change the ConfigMap:
+
+```bash
+kubectl edit configmap -n kube-system antrea-mc-controller-config
+```
+
+Normally, `podCIDRs` should be the value of `kube-controller-manager`'s
+`cluster-cidr` option. If it's left empty, the Pod-to-Pod connectivity feature
+will not be enabled. If you use `kubectl edit` to edit the ConfigMap, then you
+need to restart the `antrea-mc-controller` Pod to load the latest configuration.
+
 ## Multi-cluster ClusterNetworkPolicy Replication
 
 Since Antrea v1.6.0, Multi-cluster admins can specify certain
@@ -592,43 +633,150 @@ In future releases, some additional tooling may become available to automate the
 creation of ResourceExports for ACNPs, and provide a user-friendly way to define
 Multi-cluster NetworkPolicies to be enforced in the ClusterSet.
 
-## Multi-cluster Pod-to-Pod Connectivity
+## NetworkPolicy for Cross-cluster Traffic
 
-Since Antrea v1.9.0, Multi-cluster supports routing Pod traffic across clusters
-through Multi-cluster Gateways. Pod IPs can be reached in all member clusters
-within a ClusterSet. To enable this feature, the cluster's Pod CIDRs must be set in
-ConfigMap `antrea-mc-controller-config` of each member cluster like the example
-below. Note, **Pod CIDRs must not overlap among clusters to enable cross-cluster
-Pod-to-Pod connectivity**.
+Antrea-native policies can be enforced on cross-cluster traffic in a ClusterSet.
+To use this feature set, make sure to check the Antrea controller and agent ConfigMaps
+and confirm that `enableStretchedNetworkPolicy` is set to `true` in addition to enabling
+the `multicluster` feature:
 
 ```yaml
-apiVersion: v1
-data:
-  controller_manager_config.yaml: |
-    apiVersion: multicluster.crd.antrea.io/v1alpha1
-    kind: MultiClusterConfig
-    ...
-    podCIDRs:
-      - "10.10.1.1/16"
-kind: ConfigMap
+antrea-agent.conf: |
+  # FeatureGates is a map of feature names to bools that enable or disable experimental features.
+  featureGates:
+...
+    Multicluster: true
+...
+  multicluster:
+    enable: true
+    namespace: ""
+    enableStretchedNetworkPolicy: true
+```
+
+```yaml
+antrea-controller.conf: |
+  # FeatureGates is a map of feature names to bools that enable or disable experimental features.
+  featureGates:
+...
+    Multicluster: true
+...
+  multicluster:
+    enableStretchedNetworkPolicy: true
+```
+
+### Egress NetworkPolicy to Multi-cluster Services
+
+Restricting Pod egress traffic to backends of a Multi-cluster Service (which can be on the
+same cluster of the source Pod or on a different cluster) is supported by Antrea-native
+policy's `toServices` feature in egress rules. To define such a policy, simply put the exported
+Service name and Namespace in the `toServices` field of an Antrea-native policy, and set `scope`
+of the `toServices` peer to `clusterSet`:
+
+```yaml
+apiVersion: crd.antrea.io/v1alpha1
+kind: ClusterNetworkPolicy
 metadata:
-  labels:
-    app: antrea
-  name: antrea-mc-controller-config
-  namespace: kube-system
+  name: acnp-drop-tenant-to-secured-mc-service
+spec:
+  priority: 1
+  tier: securityops
+  appliedTo:
+    - podSelector:
+        matchLabels:
+          role: tenant
+  egress:
+    - action: Drop
+      toServices:
+        - name: secured-service   # an exported Multi-cluster Service
+          namespace: svcNamespace
+          scope: clusterSet
 ```
 
-You can edit [antrea-multicluster-member.yml](../../multicluster/build/yamls/antrea-multicluster-member.yml),
-or use `kubectl edit` to change the ConfigMap:
+The `scope` field of `toServices` rules is supported since Antrea v1.10. For earlier versions
+of Antrea, an equivalent rule can be written by not specifying `scope` and providing the
+imported Service name instead (i.e. `antrea-mc-[svcName]`).
 
-```bash
-kubectl edit configmap -n kube-system antrea-mc-controller-config
+Note that the scope of policy's `appliedTo` field will still be restricted to the cluster
+where the policy is created in. To enforce such a policy for all `role=tenant` Pods in the
+entire ClusterSet, use the [ClusterNetworkPolicy Replication](#multi-cluster-clusternetworkpolicy-replication)
+feature mentioned in the previous section, and set the `clusterNetworkPolicy` field of
+the ResourceExport to the `acnp-drop-tenant-to-secured-mc-service` spec above. Such
+replication should only be performed by ClusterSet admins, who have clearance of creating
+ClusterNetworkPolicies in all clusters of a ClusterSet.
+
+### Ingress NetworkPolicy for Cross-cluster Traffic
+
+Antrea-native policies now support selecting ingress peers in the ClusterSet scope (since v1.10.0).
+Policy rules can be created to enforce security postures on ingress traffic from all member
+clusters in a ClusterSet:
+
+```yaml
+apiVersion: crd.antrea.io/v1alpha1
+kind: ClusterNetworkPolicy
+metadata:
+  name: drop-tenant-access-to-admin-namespace
+spec:
+  appliedTo:
+  - namespaceSelector:
+      matchLabels:
+        role: admin
+  priority: 1
+  tier: securityops
+  ingress:
+  - action: Deny
+    from:
+    # Select all Pods in role=tenant Namespaces in the ClusterSet
+    - scope: clusterSet
+      namespaceSelector:        
+        matchLabels:
+          role: tenant
 ```
 
-Normally, `podCIDRs` should be the value of `kube-controller-manager`'s
-`cluster-cidr` option. If it's left empty, the Pod-to-Pod connectivity feature
-will not be enabled. If you use `kubectl edit` to edit the ConfigMap, then you
-need to restart the `antrea-mc-controller` Pod to load the latest configuration.
+```yaml
+apiVersion: crd.antrea.io/v1alpha1
+kind: AntreaNetworkPolicy
+metadata:
+  name: db-svc-allow-ingress-from-client-only
+  namespace: prod-us-west
+spec:
+  appliedTo:
+  - podSelector:
+      matchLabels:
+        app: db
+  priority: 1
+  tier: application 
+  ingress:
+  - action: Allow
+    from:
+    # Select all Pods in Namespace "prod-us-west" from all clusters in the ClusterSet (if the 
+    # Namespace exists in that cluster) whose labels match app=client
+    - scope: clusterSet
+      podSelector:        
+        matchLabels:
+          app: client
+  - action: Deny
+```
+
+As shown in the examples above, setting `scope` to `clusterSet` expands the scope of the
+`podSelector` or `namespaceSelector` of an ingress peer to the entire ClusterSet that the
+policy is created in. Similar to Egress NetworkPolicy, the scope of `appliedTo`
+is also restricted to the local cluster.
+
+To use the ingress cross-cluster NetworkPolicy feature, the `enableStretchedNetworkPolicy`
+option needs to be set to `true` in `antrea-mc-controller-config`, for each `antrea-mc-controller`
+running in the ClusterSet. Refer to the [previous section](#multi-cluster-pod-to-pod-connectivity)
+on how to change the ConfigMap:
+
+```yaml
+  controller_manager_config.yaml: |
+    apiVersion: multicluster.crd.antrea.io/v1alpha1
+    kind: MultiClusterConfig
+    ...
+    enableStretchedNetworkPolicy: true
+```
+
+Note that currently ingress stretched NetworkPolicy only works with the Antrea `encap`
+traffic mode.
 
 ## Build Antrea Multi-cluster Image
 
diff --git a/go.mod b/go.mod
index c91b7846e..abdc33ec6 100644
--- a/go.mod
+++ b/go.mod
@@ -58,7 +58,7 @@ require (
 	golang.org/x/crypto v0.1.0
 	golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6
 	golang.org/x/mod v0.7.0
-	golang.org/x/net v0.2.0
+	golang.org/x/net v0.4.0
 	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
 	golang.org/x/sys v0.3.0
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
@@ -198,8 +198,8 @@ require (
 	go.uber.org/multierr v1.6.0 // indirect
 	go.uber.org/zap v1.19.1 // indirect
 	golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
-	golang.org/x/term v0.2.0 // indirect
-	golang.org/x/text v0.4.0 // indirect
+	golang.org/x/term v0.3.0 // indirect
+	golang.org/x/text v0.5.0 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
diff --git a/go.sum b/go.sum
index 4b64c38cf..33ca40dd4 100644
--- a/go.sum
+++ b/go.sum
@@ -1162,8 +1162,8 @@ golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1303,8 +1303,8 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
-golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
 golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1315,8 +1315,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index d23059cdc..c61ec5310 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -95,6 +95,7 @@ type Initializer struct {
 	client                clientset.Interface
 	crdClient             versioned.Interface
 	ovsBridgeClient       ovsconfig.OVSBridgeClient
+	ovsCtlClient          ovsctl.OVSCtlClient
 	ofClient              openflow.Client
 	routeClient           route.Interface
 	wireGuardClient       wireguard.Interface
@@ -107,6 +108,8 @@ type Initializer struct {
 	wireGuardConfig       *config.WireGuardConfig
 	egressConfig          *config.EgressConfig
 	serviceConfig         *config.ServiceConfig
+	l7NetworkPolicyConfig *config.L7NetworkPolicyConfig
+	enableL7NetworkPolicy bool
 	enableProxy           bool
 	connectUplinkToBridge bool
 	// networkReadyCh should be closed once the Node's network is ready.
@@ -122,6 +125,7 @@ func NewInitializer(
 	k8sClient clientset.Interface,
 	crdClient versioned.Interface,
 	ovsBridgeClient ovsconfig.OVSBridgeClient,
+	ovsCtlClient ovsctl.OVSCtlClient,
 	ofClient openflow.Client,
 	routeClient route.Interface,
 	ifaceStore interfacestore.InterfaceStore,
@@ -139,9 +143,11 @@ func NewInitializer(
 	enableProxy bool,
 	proxyAll bool,
 	connectUplinkToBridge bool,
+	enableL7NetworkPolicy bool,
 ) *Initializer {
 	return &Initializer{
 		ovsBridgeClient:       ovsBridgeClient,
+		ovsCtlClient:          ovsCtlClient,
 		client:                k8sClient,
 		crdClient:             crdClient,
 		ifaceStore:            ifaceStore,
@@ -154,6 +160,7 @@ func NewInitializer(
 		wireGuardConfig:       wireGuardConfig,
 		egressConfig:          egressConfig,
 		serviceConfig:         serviceConfig,
+		l7NetworkPolicyConfig: &config.L7NetworkPolicyConfig{},
 		networkReadyCh:        networkReadyCh,
 		stopCh:                stopCh,
 		nodeType:              nodeType,
@@ -161,6 +168,7 @@ func NewInitializer(
 		enableProxy:           enableProxy,
 		proxyAll:              proxyAll,
 		connectUplinkToBridge: connectUplinkToBridge,
+		enableL7NetworkPolicy: enableL7NetworkPolicy,
 	}
 }
 
@@ -386,6 +394,13 @@ func (i *Initializer) Initialize() error {
 		return err
 	}
 
+	if i.enableL7NetworkPolicy {
+		// prepareL7NetworkPolicyInterfaces must be executed after setupOVSBridge since it requires interfaceStore.
+		if err := i.prepareL7NetworkPolicyInterfaces(); err != nil {
+			return err
+		}
+	}
+
 	// initializeWireGuard must be executed after setupOVSBridge as it requires gateway addresses on the OVS bridge.
 	if i.networkConfig.TrafficEncryptionMode == config.TrafficEncryptionModeWireGuard {
 		if err := i.initializeWireGuard(); err != nil {
@@ -511,7 +526,7 @@ func (i *Initializer) initOpenFlowPipeline() error {
 	roundInfo := getRoundInfo(i.ovsBridgeClient)
 
 	// Set up all basic flows.
-	ofConnCh, err := i.ofClient.Initialize(roundInfo, i.nodeConfig, i.networkConfig, i.egressConfig, i.serviceConfig)
+	ofConnCh, err := i.ofClient.Initialize(roundInfo, i.nodeConfig, i.networkConfig, i.egressConfig, i.serviceConfig, i.l7NetworkPolicyConfig)
 	if err != nil {
 		klog.Errorf("Failed to initialize openflow client: %v", err)
 		return err
diff --git a/pkg/agent/agent_linux.go b/pkg/agent/agent_linux.go
index 790e41f19..23ea7342c 100644
--- a/pkg/agent/agent_linux.go
+++ b/pkg/agent/agent_linux.go
@@ -312,3 +312,58 @@ func (i *Initializer) prepareOVSBridgeForVM() error {
 func (i *Initializer) installVMInitialFlows() error {
 	return nil
 }
+
+// prepareL7NetworkPolicyInterfaces creates two OVS internal ports. An application-aware engine will connect to OVS
+// through these two ports.
+func (i *Initializer) prepareL7NetworkPolicyInterfaces() error {
+	trafficControlPortExternalIDs := map[string]interface{}{
+		interfacestore.AntreaInterfaceTypeKey: interfacestore.AntreaTrafficControl,
+	}
+
+	for _, portName := range []string{config.L7NetworkPolicyTargetPortName, config.L7NetworkPolicyReturnPortName} {
+		_, exists := i.ifaceStore.GetInterface(portName)
+		if exists {
+			continue
+		}
+
+		portUUID, err := i.ovsBridgeClient.CreateInternalPort(portName, 0, "", trafficControlPortExternalIDs)
+		if err != nil {
+			return err
+		}
+		if pollErr := wait.PollImmediate(time.Second, 5*time.Second, func() (bool, error) {
+			_, _, err := util.SetLinkUp(portName)
+			if err == nil {
+				return true, nil
+			}
+			if _, ok := err.(util.LinkNotFound); ok {
+				return false, nil
+			}
+			return false, err
+		}); pollErr != nil {
+			return pollErr
+		}
+
+		ofPort, err := i.ovsBridgeClient.GetOFPort(portName, false)
+		if err != nil {
+			return err
+		}
+
+		itf := interfacestore.NewTrafficControlInterface(portName)
+		itf.OVSPortConfig = &interfacestore.OVSPortConfig{PortUUID: portUUID, OFPort: ofPort}
+		i.ifaceStore.AddInterface(itf)
+	}
+
+	targetPort, _ := i.ifaceStore.GetInterfaceByName(config.L7NetworkPolicyTargetPortName)
+	returnPort, _ := i.ifaceStore.GetInterfaceByName(config.L7NetworkPolicyReturnPortName)
+	i.l7NetworkPolicyConfig.TargetOFPort = uint32(targetPort.OFPort)
+	i.l7NetworkPolicyConfig.ReturnOFPort = uint32(returnPort.OFPort)
+	// Set the ports with no-flood to reject ARP flood packets.
+	if err := i.ovsCtlClient.SetPortNoFlood(int(targetPort.OFPort)); err != nil {
+		return fmt.Errorf("failed to set port %s with no-flood config: %w", config.L7NetworkPolicyTargetPortName, err)
+	}
+	if err := i.ovsCtlClient.SetPortNoFlood(int(returnPort.OFPort)); err != nil {
+		return fmt.Errorf("failed to set port %s with no-flood config: %w", config.L7NetworkPolicyReturnPortName, err)
+	}
+
+	return nil
+}
diff --git a/pkg/agent/agent_windows.go b/pkg/agent/agent_windows.go
index 104b53786..abfc5e2d4 100644
--- a/pkg/agent/agent_windows.go
+++ b/pkg/agent/agent_windows.go
@@ -479,3 +479,7 @@ func (i *Initializer) installVMInitialFlows() error {
 	}
 	return nil
 }
+
+func (i *Initializer) prepareL7NetworkPolicyInterfaces() error {
+	return nil
+}
diff --git a/pkg/agent/cniserver/interface_configuration_linux.go b/pkg/agent/cniserver/interface_configuration_linux.go
index a45264ac9..d871efdf7 100644
--- a/pkg/agent/cniserver/interface_configuration_linux.go
+++ b/pkg/agent/cniserver/interface_configuration_linux.go
@@ -447,12 +447,12 @@ func validateContainerVFInterface(intf *current.Interface, sriovVFDeviceID strin
 	if err != nil {
 		return nil, fmt.Errorf("failed to find netdevice to PCI address %s: %v", sriovVFDeviceID, err)
 	}
-	// the check makes sure that the SR-IOV VF netdevice is not in the host namespace
-	// the GetNetDevicesFromPci is using linux sysfs to find the VF netdevice
-	// the method is running in container network namespace, but we are still in the antrea agent filesystem
-	// the antrea agent container is privileged, which allow access to the host sysfs
-	// therefore the validation is to make sure that the VF netdevice is not in the host network
-	// namespace
+	// The check makes sure that the SR-IOV VF netdevice is not in the host namespace.
+	// GetNetDevicesFromPci is using linux sysfs to find the VF netdevice. The method
+	// is running in container network namespace, but we are still in the antrea-agent
+	// filesystem. The antrea-agent container is privileged, which allows access to
+	// the host sysfs, therefore the validation is to make sure that the VF netdevice
+	// is not in the host network namespace.
 	if len(netdevices) != 0 {
 		return nil, fmt.Errorf("VF netdevice still in host network namespace %s %+v", sriovVFDeviceID, netdevices)
 	}
diff --git a/pkg/agent/config/node_config.go b/pkg/agent/config/node_config.go
index 86ab00c61..80abd669d 100644
--- a/pkg/agent/config/node_config.go
+++ b/pkg/agent/config/node_config.go
@@ -44,6 +44,11 @@ const (
 	IPv6ExtraOverhead = 20
 )
 
+const (
+	L7NetworkPolicyTargetPortName = "antrea-l7-tap0"
+	L7NetworkPolicyReturnPortName = "antrea-l7-tap1"
+)
+
 var (
 	// VirtualServiceIPv4 or VirtualServiceIPv6 is used in the following scenarios:
 	// - The IP is used to perform SNAT for packets of Service sourced from Antrea gateway and destined for external
@@ -233,3 +238,9 @@ type ServiceConfig struct {
 	NodePortAddressesIPv4 []net.IP
 	NodePortAddressesIPv6 []net.IP
 }
+
+// L7NetworkPolicyConfig includes target and return ofPorts for L7 NetworkPolicy.
+type L7NetworkPolicyConfig struct {
+	TargetOFPort uint32 // Matched L7 NetworkPolicy traffic is forwarded to an application-aware engine via this ofPort.
+	ReturnOFPort uint32 // Scanned L7 NetworkPolicy traffic is returned from an application-aware engine via this ofPort.
+}
diff --git a/pkg/agent/controller/networkpolicy/allocator.go b/pkg/agent/controller/networkpolicy/allocator.go
index 0187cf113..6d48ecd5b 100644
--- a/pkg/agent/controller/networkpolicy/allocator.go
+++ b/pkg/agent/controller/networkpolicy/allocator.go
@@ -213,3 +213,63 @@ func (a *idAllocator) release(id uint32) error {
 	a.availableSlice = append(a.availableSlice, id)
 	return nil
 }
+
+// l7VlanIDAllocator provides interfaces to allocate and release VLAN IDs for L7 rules. It also caches the mapping of
+// rule IDs to released VLAN IDs and provides an interface for L7 rule to query its allocated VLAN ID.
+type l7VlanIDAllocator struct {
+	sync.RWMutex
+
+	idCounter      uint32
+	recycled       []uint32
+	ruleIDToVlanID map[string]uint32
+}
+
+func newL7VlanIDAllocator() *l7VlanIDAllocator {
+	return &l7VlanIDAllocator{
+		ruleIDToVlanID: make(map[string]uint32),
+	}
+}
+
+func (l *l7VlanIDAllocator) allocate(ruleID string) uint32 {
+	l.Lock()
+	defer l.Unlock()
+
+	if vlanID, ok := l.ruleIDToVlanID[ruleID]; ok {
+		return vlanID
+	}
+
+	var vlanID uint32
+	if len(l.recycled) != 0 {
+		vlanID = l.recycled[len(l.recycled)-1]
+		l.recycled = l.recycled[:len(l.recycled)-1]
+	} else {
+		l.idCounter += 1
+		vlanID = l.idCounter
+	}
+	l.ruleIDToVlanID[ruleID] = vlanID
+	return vlanID
+}
+
+func (l *l7VlanIDAllocator) release(ruleID string) {
+	l.Lock()
+	defer l.Unlock()
+
+	vlanID, ok := l.ruleIDToVlanID[ruleID]
+	if !ok {
+		return
+	}
+
+	l.recycled = append(l.recycled, vlanID)
+	delete(l.ruleIDToVlanID, ruleID)
+}
+
+func (l *l7VlanIDAllocator) query(ruleID string) uint32 {
+	l.RLock()
+	defer l.RUnlock()
+
+	vlanID, ok := l.ruleIDToVlanID[ruleID]
+	if ok {
+		return vlanID
+	}
+	return 0
+}
diff --git a/pkg/agent/controller/networkpolicy/cache.go b/pkg/agent/controller/networkpolicy/cache.go
index b76464256..270ce0970 100644
--- a/pkg/agent/controller/networkpolicy/cache.go
+++ b/pkg/agent/controller/networkpolicy/cache.go
@@ -69,7 +69,7 @@ type rule struct {
 	// Protocols and Ports of this rule.
 	Services []v1beta.Service
 	// Layer 7 protocols of this rule.
-	L7Protocols []v1beta.Protocol
+	L7Protocols []v1beta.L7Protocol
 	// Name of this rule. Empty for k8s NetworkPolicy.
 	Name string
 	// Action of this rule. nil for k8s NetworkPolicy.
@@ -149,6 +149,8 @@ type CompletedRule struct {
 	ToAddresses v1beta.GroupMemberSet
 	// Target GroupMembers of this rule.
 	TargetMembers v1beta.GroupMemberSet
+	// Vlan ID allocated for this rule if this rule is for L7 NetworkPolicy.
+	L7RuleVlanID *uint32
 }
 
 // String returns the string representation of the CompletedRule.
@@ -676,6 +678,7 @@ func toRule(r *v1beta.NetworkPolicyRule, policy *v1beta.NetworkPolicy, maxPriori
 		From:            r.From,
 		To:              r.To,
 		Services:        r.Services,
+		L7Protocols:     r.L7Protocols,
 		Action:          r.Action,
 		Priority:        r.Priority,
 		PolicyPriority:  policy.Priority,
diff --git a/pkg/agent/controller/networkpolicy/l7engine/reconciler.go b/pkg/agent/controller/networkpolicy/l7engine/reconciler.go
new file mode 100644
index 000000000..22972064a
--- /dev/null
+++ b/pkg/agent/controller/networkpolicy/l7engine/reconciler.go
@@ -0,0 +1,470 @@
+// Copyright 2022 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 l7engine
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/spf13/afero"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/klog/v2"
+
+	"antrea.io/antrea/pkg/agent/config"
+	v1beta "antrea.io/antrea/pkg/apis/controlplane/v1beta2"
+)
+
+const (
+	defaultSuricataConfigPath = "/etc/suricata/suricata.yaml"
+	antreaSuricataConfigPath  = "/etc/suricata/antrea.yaml"
+
+	tenantConfigsDir = "/etc/suricata"
+	tenantRulesDir   = "/etc/suricata/rules"
+
+	suricataCommandSocket = "/var/run/suricata/suricata-command.socket"
+
+	protocolHTTP = "http"
+
+	scCmdOK = "OK"
+)
+
+type scCmdRet struct {
+	Message string `json:"message"`
+	Return  string `json:"return"`
+}
+
+var (
+	// Declared as a variable for testing.
+	defaultFS = afero.NewOsFs()
+)
+
+type threadSafeInt32Set struct {
+	sync.RWMutex
+	cached sets.Int32
+}
+
+func (g *threadSafeInt32Set) has(key uint32) bool {
+	g.RLock()
+	defer g.RUnlock()
+	return g.cached.Has(int32(key))
+}
+
+func (g *threadSafeInt32Set) insert(key uint32) {
+	g.Lock()
+	defer g.Unlock()
+	g.cached.Insert(int32(key))
+}
+
+func (g *threadSafeInt32Set) delete(key uint32) {
+	g.Lock()
+	defer g.Unlock()
+	g.cached.Delete(int32(key))
+}
+
+type Reconciler struct {
+	// Declared as member variables for testing.
+	startSuricataFn func()
+	suricataScFn    func(scCmd string) (*scCmdRet, error)
+
+	suricataTenantCache        *threadSafeInt32Set
+	suricataTenantHandlerCache *threadSafeInt32Set
+
+	once sync.Once
+}
+
+func NewReconciler() *Reconciler {
+	return &Reconciler{
+		suricataScFn:    suricataSc,
+		startSuricataFn: startSuricata,
+		suricataTenantCache: &threadSafeInt32Set{
+			cached: sets.NewInt32(),
+		},
+		suricataTenantHandlerCache: &threadSafeInt32Set{
+			cached: sets.NewInt32(),
+		},
+	}
+}
+
+func generateTenantRulesData(policyName string, protoKeywords map[string]sets.String) *bytes.Buffer {
+	rulesData := bytes.NewBuffer(nil)
+	sid := 1
+
+	// Generate default reject rule.
+	allKeywords := fmt.Sprintf(`msg: "Reject by %s"; flow: to_server, established; sid: %d;`, policyName, sid)
+	rule := fmt.Sprintf("reject ip any any -> any any (%s)\n", allKeywords)
+	rulesData.WriteString(rule)
+	sid++
+
+	// Generate rules.
+	for proto, keywordsSet := range protoKeywords {
+		for keywords := range keywordsSet {
+			// It is a convention that the sid is provided as the last keyword (or second-to-last if there is a rev)
+			// of a rule.
+			if keywords != "" {
+				allKeywords = fmt.Sprintf(`msg: "Allow %s by %s"; %s sid: %d;`, proto, policyName, keywords, sid)
+			} else {
+				allKeywords = fmt.Sprintf(`msg: "Allow %s by %s"; sid: %d;`, proto, policyName, sid)
+			}
+			rule = fmt.Sprintf("pass %s any any -> any any (%s)\n", proto, allKeywords)
+			rulesData.WriteString(rule)
+			sid++
+		}
+	}
+
+	return rulesData
+}
+
+func generateTenantRulesPath(vlanID uint32) string {
+	return fmt.Sprintf("%s/antrea-l7-networkpolicy-%d.rules", tenantRulesDir, vlanID)
+}
+
+func generateTenantConfigPath(vlanID uint32) string {
+	return fmt.Sprintf("%s/antrea-tenant-%d.yaml", tenantConfigsDir, vlanID)
+}
+
+func writeConfigFile(path string, data *bytes.Buffer) error {
+	f, err := defaultFS.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	if _, err = f.Write(data.Bytes()); err != nil {
+		return err
+	}
+	return nil
+}
+
+// By default, Suricata performs pattern-matching for provided content. To support exact match, prefix match, and suffix
+// match, we use wildcards to indicate whether an exact match is expected.
+// - A string starting with * means suffix match. For example, "*.foo.com" matches "www.foo.com".
+// - A string ending with * means prefix match. For example, "/public/*" matches "/public/index.html".
+// - A string starting with and ending with * means pattern-matching. For example, "*/v2/*" matches "/api/v2/pods".
+// - A string having no * means exact match. For example, "/index.html" can only match "/index.html".
+func convertContent(content string) string {
+	startsWith := " startswith;"
+	if strings.HasPrefix(content, "*") {
+		startsWith = ""
+		content = content[1:]
+	}
+	endsWith := " endswith;"
+	if strings.HasSuffix(content, "*") {
+		endsWith = ""
+		content = content[:len(content)-1]
+	}
+	return fmt.Sprintf(`content:"%s";%s%s`, content, startsWith, endsWith)
+}
+
+func convertProtocolHTTP(http *v1beta.HTTPProtocol) string {
+	var keywords []string
+	if http.Path != "" {
+		keywords = append(keywords, fmt.Sprintf("http.uri; %s", convertContent(http.Path)))
+	}
+	if http.Method != "" {
+		keywords = append(keywords, fmt.Sprintf(`http.method; content:"%s";`, http.Method))
+	}
+	if http.Host != "" {
+		keywords = append(keywords, fmt.Sprintf("http.host; %s", convertContent(http.Host)))
+	}
+	return strings.Join(keywords, " ")
+}
+
+func (r *Reconciler) AddRule(ruleID, policyName string, vlanID uint32, l7Protocols []v1beta.L7Protocol) error {
+	start := time.Now()
+	defer func() {
+		klog.V(5).Infof("AddRule took %v", time.Since(start))
+	}()
+
+	r.once.Do(func() {
+		r.startSuricata()
+	})
+
+	// Generate the keyword part used in Suricata rules.
+	protoKeywords := make(map[string]sets.String)
+	for _, protocol := range l7Protocols {
+		if protocol.HTTP != nil {
+			httpKeywords := convertProtocolHTTP(protocol.HTTP)
+			if _, ok := protoKeywords[protocolHTTP]; !ok {
+				protoKeywords[protocolHTTP] = sets.NewString()
+			}
+			protoKeywords[protocolHTTP].Insert(httpKeywords)
+		}
+	}
+
+	klog.InfoS("Reconciling L7 rule", "RuleID", ruleID, "PolicyName", policyName)
+	// Write the Suricata rules to file.
+	rulesPath := generateTenantRulesPath(vlanID)
+	rulesData := generateTenantRulesData(policyName, protoKeywords)
+	if err := writeConfigFile(rulesPath, rulesData); err != nil {
+		return fmt.Errorf("failed to write Suricata rules data to file %s for L7 rule %s of %s", rulesPath, ruleID, policyName)
+	}
+
+	// Add a Suricata tenant.
+	if err := r.addBindingSuricataTenant(vlanID, rulesPath); err != nil {
+		return fmt.Errorf("failed to add Suricata tenant for L7 rule %s of %s: %w", ruleID, policyName, err)
+	}
+
+	return nil
+}
+
+func (r *Reconciler) DeleteRule(ruleID string, vlanID uint32) error {
+	start := time.Now()
+	defer func() {
+		klog.V(5).Infof("DeleteRule took %v", time.Since(start))
+	}()
+
+	// Delete the Suricata tenant.
+	if err := r.deleteBindingSuricataTenant(vlanID); err != nil {
+		return fmt.Errorf("failed to delete Suricata tenant %d for L7 rule %s: %w", vlanID, ruleID, err)
+	}
+
+	// Delete the Suricata rules file.
+	rulesPath := generateTenantRulesPath(vlanID)
+	if err := defaultFS.Remove(rulesPath); err != nil {
+		klog.ErrorS(err, "Failed to delete rules file", "FilePath", rulesPath, "RuleID", ruleID)
+	}
+
+	return nil
+}
+
+func (r *Reconciler) addBindingSuricataTenant(vlanID uint32, rulesPath string) error {
+	tenantConfigPath := generateTenantConfigPath(vlanID)
+	exists, err := afero.Exists(defaultFS, tenantConfigPath)
+	if err != nil {
+		return fmt.Errorf("failed to stat config file %s", tenantConfigPath)
+	}
+
+	// If the tenant config file exists, it means that this tenant has been added, just reload the tenant to load the
+	// updated rules.
+	if exists {
+		resp, err := r.reloadSuricataTenant(vlanID, tenantConfigPath)
+		if err != nil {
+			return err
+		}
+		if resp.Return != scCmdOK {
+			return fmt.Errorf("failed to reload Suricata tenant %d with config file %s: %v", vlanID, tenantConfigPath, resp.Message)
+		}
+		klog.V(4).InfoS("Reloaded Suricata tenant successfully", "TenantID", vlanID, "TenantConfigPath", tenantConfigPath, "ResponseMsg", resp.Message)
+		return nil
+	}
+
+	success := false
+	// If the tenant config file doesn't exist, create a config file for the tenant.
+	tenantConfigData := bytes.NewBuffer([]byte(fmt.Sprintf(`%%YAML 1.1
+
+---
+default-rule-path: /etc/suricata/rules
+rule-files:
+  - %s
+`, rulesPath)))
+	if err = writeConfigFile(tenantConfigPath, tenantConfigData); err != nil {
+		return fmt.Errorf("failed to write config file %s for Suricata tenant %d: %w", tenantConfigPath, vlanID, err)
+	}
+	defer func() {
+		if !success {
+			// Delete the config file regardless if it is created.
+			defaultFS.Remove(tenantConfigPath)
+		}
+	}()
+
+	// Register the tenant with the config file. Note that, to be simple, use the VLAN id as the tenant ID.
+	if !r.suricataTenantCache.has(vlanID) {
+		resp, err := r.registerSuricataTenant(vlanID, tenantConfigPath)
+		if err != nil {
+			return err
+		}
+		if resp.Return != scCmdOK {
+			return fmt.Errorf("failed to register Suricata tenant %d with config file %s: %v", vlanID, tenantConfigPath, resp.Message)
+		}
+		klog.V(4).InfoS("Registered Suricata tenant successfully", "TenantID", vlanID, "TenantConfigPath", tenantConfigPath, "ResponseMsg", resp.Message)
+		r.suricataTenantCache.insert(vlanID)
+	}
+
+	// Register the tenant handler by mapping the tenant to the allocated VLAN ID.
+	if !r.suricataTenantHandlerCache.has(vlanID) {
+		resp, err := r.registerSuricataTenantHandler(vlanID, vlanID)
+		if err != nil {
+			return err
+		}
+		if resp.Return != scCmdOK {
+			return fmt.Errorf("failed to register Suricata tenant %d handler to VLAN %d: %v", vlanID, vlanID, resp.Message)
+		}
+		klog.V(4).InfoS("Registered Suricata tenant handler successfully", "TenantID", vlanID, "VLANID", vlanID, "ResponseMsg", resp.Message)
+		r.suricataTenantHandlerCache.insert(vlanID)
+	}
+
+	success = true
+
+	return nil
+}
+
+func (r *Reconciler) deleteBindingSuricataTenant(vlanID uint32) error {
+	// Unregister the tenant handler.
+	if r.suricataTenantHandlerCache.has(vlanID) {
+		resp, err := r.unregisterSuricataTenantHandler(vlanID, vlanID)
+		if err != nil {
+			return err
+		}
+		if resp.Return != scCmdOK {
+			return fmt.Errorf("failed to unregister Suricata tenant %d handler: %v", vlanID, resp.Message)
+		}
+		klog.V(4).InfoS("Unregistered Suricata tenant handler successfully", "TenantID", vlanID, "VLANID", vlanID, "ResponseMsg", resp.Message)
+		r.suricataTenantHandlerCache.delete(vlanID)
+	}
+
+	// Unregister the tenant.
+	if r.suricataTenantCache.has(vlanID) {
+		resp, err := r.unregisterSuricataTenant(vlanID)
+		if err != nil {
+			return err
+		}
+		if resp.Return != scCmdOK {
+			return fmt.Errorf("failed to unregister Suricata tenant %d: %v", vlanID, resp.Message)
+		}
+		klog.V(4).InfoS("Unregistered Suricata tenant successfully", "TenantID", vlanID, "ResponseMsg", resp.Message)
+		r.suricataTenantCache.delete(vlanID)
+	}
+
+	// Delete the tenant config file.
+	configPath := generateTenantConfigPath(vlanID)
+	if err := defaultFS.Remove(configPath); err != nil {
+		if err != afero.ErrFileNotFound {
+			return fmt.Errorf("failed to delete config file %s: %w", configPath, err)
+		}
+	}
+	return nil
+}
+
+func (r *Reconciler) reloadSuricataTenant(tenantID uint32, tenantConfigPath string) (*scCmdRet, error) {
+	scCmd := fmt.Sprintf("reload-tenant %d %s", tenantID, tenantConfigPath)
+	return r.suricataScFn(scCmd)
+}
+
+func (r *Reconciler) registerSuricataTenant(tenantID uint32, tenantConfigPath string) (*scCmdRet, error) {
+	scCmd := fmt.Sprintf("register-tenant %d %s", tenantID, tenantConfigPath)
+	return r.suricataScFn(scCmd)
+}
+
+func (r *Reconciler) unregisterSuricataTenant(tenantID uint32) (*scCmdRet, error) {
+	scCmd := fmt.Sprintf("unregister-tenant %d", tenantID)
+	return r.suricataScFn(scCmd)
+}
+
+func (r *Reconciler) registerSuricataTenantHandler(tenantID, vlanID uint32) (*scCmdRet, error) {
+	scCmd := fmt.Sprintf("register-tenant-handler %d vlan %d", tenantID, vlanID)
+	return r.suricataScFn(scCmd)
+}
+
+func (r *Reconciler) unregisterSuricataTenantHandler(tenantID, vlanID uint32) (*scCmdRet, error) {
+	scCmd := fmt.Sprintf("unregister-tenant-handler %d vlan %d", tenantID, vlanID)
+	return r.suricataScFn(scCmd)
+}
+
+func (r *Reconciler) startSuricata() {
+	// Create the config file /etc/suricata/antrea.yaml for Antrea which will be included in the default Suricata config file
+	// /etc/suricata/suricata.yaml.
+	suricataAntreaConfigData := fmt.Sprintf(`%%YAML 1.1
+---
+af-packet:
+  - interface: %[1]s
+    threads: auto
+    cluster-id: 80
+    cluster-type: cluster_flow
+    defrag: no
+    use-mmap: yes
+    tpacket-v2: yes
+    checksum-checks: no
+    copy-mode: ips
+    copy-iface: %[2]s
+  - interface:  %[2]s
+    threads: auto
+    cluster-id: 81
+    cluster-type: cluster_flow
+    defrag: no
+    use-mmap: yes
+    tpacket-v2: yes
+    checksum-checks: no
+    copy-mode: ips
+    copy-iface: %[1]s
+multi-detect:
+  enabled: yes
+  selector: vlan
+`, config.L7NetworkPolicyTargetPortName, config.L7NetworkPolicyReturnPortName)
+	f, err := defaultFS.Create(antreaSuricataConfigPath)
+	if err != nil {
+		klog.ErrorS(err, "Failed to create Suricata config file", "FilePath", antreaSuricataConfigPath)
+		return
+	}
+	defer f.Close()
+	if _, err = f.WriteString(suricataAntreaConfigData); err != nil {
+		klog.ErrorS(err, "Failed to write Suricata config file", "FilePath", antreaSuricataConfigPath)
+		return
+	}
+
+	// Open the default Suricata config file /etc/suricata/suricata.yaml.
+	f, err = defaultFS.OpenFile(defaultSuricataConfigPath, os.O_APPEND|os.O_WRONLY, 0600)
+	if err != nil {
+		klog.ErrorS(err, "Failed to open default Suricata config file", "FilePath", defaultSuricataConfigPath)
+		return
+	}
+	defer f.Close()
+	// Include the config file /etc/suricata/antrea.yaml for Antrea in the default Suricata config file /etc/suricata/suricata.yaml.
+	if _, err = f.WriteString(fmt.Sprintf("include: %s\n", antreaSuricataConfigPath)); err != nil {
+		klog.ErrorS(err, "Failed to update default Suricata config file", "FilePath", defaultSuricataConfigPath)
+		return
+	}
+
+	r.startSuricataFn()
+
+	// Wait Suricata command socket file to be ready.
+	err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) {
+		if _, err = defaultFS.Stat(suricataCommandSocket); err != nil {
+			return false, nil
+		}
+		return true, nil
+	})
+	if err != nil {
+		klog.ErrorS(err, "Failed to find Suricata command socket file")
+	} else {
+		klog.InfoS("Started Suricata instance successfully")
+	}
+}
+
+func startSuricata() {
+	// Start Suricata with default Suricata config file /etc/suricata/suricata.yaml.
+	cmd := exec.Command("suricata", "-c", defaultSuricataConfigPath, "--af-packet", "-D")
+	if err := cmd.Start(); err != nil {
+		klog.ErrorS(err, "Failed to start Suricata instance")
+	}
+}
+
+func suricataSc(scCmd string) (*scCmdRet, error) {
+	cmd := exec.Command("suricatasc", "-c", scCmd)
+	retBytes, err := cmd.CombinedOutput()
+	if err != nil {
+		return nil, fmt.Errorf("failed to run Suricata command '%s': %w", scCmd, err)
+	}
+	var ret scCmdRet
+	if err = json.Unmarshal(retBytes, &ret); err != nil {
+		return nil, err
+	}
+	return &ret, nil
+}
diff --git a/pkg/agent/controller/networkpolicy/networkpolicy_controller.go b/pkg/agent/controller/networkpolicy/networkpolicy_controller.go
index 656e1107f..1ce441585 100644
--- a/pkg/agent/controller/networkpolicy/networkpolicy_controller.go
+++ b/pkg/agent/controller/networkpolicy/networkpolicy_controller.go
@@ -32,6 +32,7 @@ import (
 
 	"antrea.io/antrea/pkg/agent"
 	"antrea.io/antrea/pkg/agent/config"
+	"antrea.io/antrea/pkg/agent/controller/networkpolicy/l7engine"
 	"antrea.io/antrea/pkg/agent/flowexporter/connections"
 	"antrea.io/antrea/pkg/agent/interfacestore"
 	"antrea.io/antrea/pkg/agent/openflow"
@@ -56,6 +57,11 @@ const (
 	dnsInterceptRuleID = uint32(1)
 )
 
+type L7RuleReconciler interface {
+	AddRule(ruleID, policyName string, vlanID uint32, l7Protocols []v1beta2.L7Protocol) error
+	DeleteRule(ruleID string, vlanID uint32) error
+}
+
 var emptyWatch = watch.NewEmptyWatch()
 
 // Controller is responsible for watching Antrea AddressGroups, AppliedToGroups,
@@ -71,7 +77,8 @@ var emptyWatch = watch.NewEmptyWatch()
 type Controller struct {
 	// antreaPolicyEnabled indicates whether Antrea NetworkPolicy and
 	// ClusterNetworkPolicy are enabled.
-	antreaPolicyEnabled bool
+	antreaPolicyEnabled    bool
+	l7NetworkPolicyEnabled bool
 	// antreaProxyEnabled indicates whether Antrea proxy is enabled.
 	antreaProxyEnabled bool
 	// statusManagerEnabled indicates whether a statusManager is configured.
@@ -97,6 +104,11 @@ type Controller struct {
 	// reconciler provides interfaces to reconcile the desired state of
 	// NetworkPolicy rules with the actual state of Openflow entries.
 	reconciler Reconciler
+	// l7RuleReconciler provides interfaces to reconcile the desired state of
+	// NetworkPolicy rules which have L7 rules with the actual state of Suricata rules.
+	l7RuleReconciler L7RuleReconciler
+	// l7VlanIDAllocator allocates a VLAN ID for every L7 rule.
+	l7VlanIDAllocator *l7VlanIDAllocator
 	// ofClient registers packetin for Antrea Policy logging.
 	ofClient           openflow.Client
 	antreaPolicyLogger *AntreaPolicyLogger
@@ -125,6 +137,7 @@ func NewNetworkPolicyController(antreaClientGetter agent.AntreaClientProvider,
 	groupCounters []proxytypes.GroupCounter,
 	groupIDUpdates <-chan string,
 	antreaPolicyEnabled bool,
+	l7NetworkPolicyEnabled bool,
 	antreaProxyEnabled bool,
 	statusManagerEnabled bool,
 	multicastEnabled bool,
@@ -137,17 +150,23 @@ func NewNetworkPolicyController(antreaClientGetter agent.AntreaClientProvider,
 	gwPort, tunPort uint32) (*Controller, error) {
 	idAllocator := newIDAllocator(asyncRuleDeleteInterval, dnsInterceptRuleID)
 	c := &Controller{
-		antreaClientProvider: antreaClientGetter,
-		queue:                workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "networkpolicyrule"),
-		ofClient:             ofClient,
-		nodeType:             nodeType,
-		antreaPolicyEnabled:  antreaPolicyEnabled,
-		antreaProxyEnabled:   antreaProxyEnabled,
-		statusManagerEnabled: statusManagerEnabled,
-		multicastEnabled:     multicastEnabled,
-		loggingEnabled:       loggingEnabled,
-		gwPort:               gwPort,
-		tunPort:              tunPort,
+		antreaClientProvider:   antreaClientGetter,
+		queue:                  workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "networkpolicyrule"),
+		ofClient:               ofClient,
+		nodeType:               nodeType,
+		antreaPolicyEnabled:    antreaPolicyEnabled,
+		l7NetworkPolicyEnabled: l7NetworkPolicyEnabled,
+		antreaProxyEnabled:     antreaProxyEnabled,
+		statusManagerEnabled:   statusManagerEnabled,
+		multicastEnabled:       multicastEnabled,
+		loggingEnabled:         loggingEnabled,
+		gwPort:                 gwPort,
+		tunPort:                tunPort,
+	}
+
+	if l7NetworkPolicyEnabled {
+		c.l7RuleReconciler = l7engine.NewReconciler()
+		c.l7VlanIDAllocator = newL7VlanIDAllocator()
 	}
 
 	if antreaPolicyEnabled {
@@ -592,7 +611,7 @@ func (c *Controller) syncRule(key string) error {
 	}()
 	rule, effective, realizable := c.ruleCache.GetCompletedRule(key)
 	if !effective {
-		klog.V(2).InfoS("Rule was not effective, removing its flows", "ruleID", key)
+		klog.V(2).InfoS("Rule was not effective, removing it", "ruleID", key)
 		if err := c.reconciler.Forget(key); err != nil {
 			return err
 		}
@@ -601,6 +620,14 @@ func (c *Controller) syncRule(key string) error {
 			// harmless to delete it.
 			c.statusManager.DeleteRuleRealization(key)
 		}
+		if c.l7NetworkPolicyEnabled {
+			if vlanID := c.l7VlanIDAllocator.query(key); vlanID != 0 {
+				if err := c.l7RuleReconciler.DeleteRule(key, vlanID); err != nil {
+					return err
+				}
+				c.l7VlanIDAllocator.release(key)
+			}
+		}
 		return nil
 	}
 	// If the rule is not realizable, we can simply skip it as it will be marked as dirty
@@ -609,6 +636,17 @@ func (c *Controller) syncRule(key string) error {
 		klog.V(2).InfoS("Rule is not realizable, skipping", "ruleID", key)
 		return nil
 	}
+
+	if c.l7NetworkPolicyEnabled && len(rule.L7Protocols) != 0 {
+		// Allocate VLAN ID for the L7 rule.
+		vlanID := c.l7VlanIDAllocator.allocate(key)
+		rule.L7RuleVlanID = &vlanID
+
+		if err := c.l7RuleReconciler.AddRule(key, rule.SourceRef.ToString(), vlanID, rule.L7Protocols); err != nil {
+			return err
+		}
+	}
+
 	err := c.reconciler.Reconcile(rule)
 	if c.fqdnController != nil {
 		// No matter whether the rule reconciliation succeeds or not, fqdnController
@@ -644,6 +682,15 @@ func (c *Controller) syncRules(keys []string) error {
 		} else if !realizable {
 			klog.Errorf("Rule %s is effective but not realizable", key)
 		} else {
+			if c.l7NetworkPolicyEnabled && len(rule.L7Protocols) != 0 {
+				// Allocate VLAN ID for the L7 rule.
+				vlanID := c.l7VlanIDAllocator.allocate(key)
+				rule.L7RuleVlanID = &vlanID
+
+				if err := c.l7RuleReconciler.AddRule(key, rule.SourceRef.ToString(), vlanID, rule.L7Protocols); err != nil {
+					return err
+				}
+			}
 			allRules = append(allRules, rule)
 		}
 	}
diff --git a/pkg/agent/controller/networkpolicy/reconciler.go b/pkg/agent/controller/networkpolicy/reconciler.go
index 87151f7c6..41c949bf8 100644
--- a/pkg/agent/controller/networkpolicy/reconciler.go
+++ b/pkg/agent/controller/networkpolicy/reconciler.go
@@ -572,6 +572,8 @@ func (r *reconciler) computeOFRulesForAdd(rule *CompletedRule, ofPriority *uint1
 				From:          from,
 				To:            toAddresses,
 				Service:       filterUnresolvablePort(servicesMap[svcKey]),
+				L7Protocols:   rule.L7Protocols,
+				L7RuleVlanID:  rule.L7RuleVlanID,
 				Action:        rule.Action,
 				Name:          rule.Name,
 				Priority:      ofPriority,
@@ -600,6 +602,8 @@ func (r *reconciler) computeOFRulesForAdd(rule *CompletedRule, ofPriority *uint1
 				From:          from,
 				To:            groupMembersToOFAddresses(members),
 				Service:       filterUnresolvablePort(servicesMap[svcKey]),
+				L7Protocols:   rule.L7Protocols,
+				L7RuleVlanID:  rule.L7RuleVlanID,
 				Action:        rule.Action,
 				Priority:      ofPriority,
 				Name:          rule.Name,
@@ -721,6 +725,8 @@ func (r *reconciler) update(lastRealized *lastRealized, newRule *CompletedRule,
 				Direction:     v1beta2.DirectionIn,
 				To:            ofPortsToOFAddresses(newOFPorts),
 				Service:       newRule.Services,
+				L7Protocols:   newRule.L7Protocols,
+				L7RuleVlanID:  newRule.L7RuleVlanID,
 				Action:        newRule.Action,
 				Priority:      ofPriority,
 				FlowID:        ofID,
@@ -776,6 +782,8 @@ func (r *reconciler) update(lastRealized *lastRealized, newRule *CompletedRule,
 					From:          append(from1, from2...),
 					To:            toAddresses,
 					Service:       filterUnresolvablePort(servicesMap[svcKey]),
+					L7Protocols:   newRule.L7Protocols,
+					L7RuleVlanID:  newRule.L7RuleVlanID,
 					Action:        newRule.Action,
 					Priority:      ofPriority,
 					FlowID:        ofID,
@@ -845,6 +853,8 @@ func (r *reconciler) update(lastRealized *lastRealized, newRule *CompletedRule,
 					From:          from,
 					To:            groupMembersToOFAddresses(members),
 					Service:       filterUnresolvablePort(servicesMap[svcKey]),
+					L7Protocols:   newRule.L7Protocols,
+					L7RuleVlanID:  newRule.L7RuleVlanID,
 					Action:        newRule.Action,
 					Priority:      ofPriority,
 					FlowID:        ofID,
diff --git a/pkg/agent/flowexporter/exporter/exporter.go b/pkg/agent/flowexporter/exporter/exporter.go
index 77ede0950..49afe8b4e 100644
--- a/pkg/agent/flowexporter/exporter/exporter.go
+++ b/pkg/agent/flowexporter/exporter/exporter.go
@@ -281,7 +281,7 @@ func (exp *FlowExporter) initFlowExporter() error {
 		// For UDP transport, hardcoding tempRefTimeout value as 1800s.
 		exp.exporterInput.TempRefTimeout = 1800
 	}
-	expProcess, err := ipfix.NewIPFIXExportingProcess(exp.exporterInput)
+	expProcess, err := exporter.InitExportingProcess(exp.exporterInput)
 	if err != nil {
 		return fmt.Errorf("error when starting exporter: %v", err)
 	}
diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go
index 36c57e467..532a0120e 100644
--- a/pkg/agent/openflow/client.go
+++ b/pkg/agent/openflow/client.go
@@ -48,7 +48,8 @@ type Client interface {
 		config *config.NodeConfig,
 		networkConfig *config.NetworkConfig,
 		egressConfig *config.EgressConfig,
-		serviceConfig *config.ServiceConfig) (<-chan struct{}, error)
+		serviceConfig *config.ServiceConfig,
+		l7NetworkPolicyConfig *config.L7NetworkPolicyConfig) (<-chan struct{}, error)
 
 	// InstallNodeFlows should be invoked when a connection to a remote Node is going to be set
 	// up. The hostname is used to identify the added flows. When IPsec tunnel is enabled,
@@ -747,11 +748,13 @@ func (c *client) Initialize(roundInfo types.RoundInfo,
 	nodeConfig *config.NodeConfig,
 	networkConfig *config.NetworkConfig,
 	egressConfig *config.EgressConfig,
-	serviceConfig *config.ServiceConfig) (<-chan struct{}, error) {
+	serviceConfig *config.ServiceConfig,
+	l7NetworkPolicyConfig *config.L7NetworkPolicyConfig) (<-chan struct{}, error) {
 	c.nodeConfig = nodeConfig
 	c.networkConfig = networkConfig
 	c.egressConfig = egressConfig
 	c.serviceConfig = serviceConfig
+	c.l7NetworkPolicyConfig = l7NetworkPolicyConfig
 	c.nodeType = nodeConfig.Type
 
 	if networkConfig.IPv4Enabled {
@@ -833,9 +836,11 @@ func (c *client) generatePipelines() {
 	c.featureNetworkPolicy = newFeatureNetworkPolicy(c.cookieAllocator,
 		c.ipProtocols,
 		c.bridge,
+		c.l7NetworkPolicyConfig,
 		c.ovsMetersAreSupported,
 		c.enableDenyTracking,
 		c.enableAntreaPolicy,
+		c.enableL7NetworkPolicy,
 		c.enableMulticast,
 		c.proxyAll,
 		c.connectUplinkToBridge,
diff --git a/pkg/agent/openflow/fields.go b/pkg/agent/openflow/fields.go
index 2f6a1e517..72ddd62b3 100644
--- a/pkg/agent/openflow/fields.go
+++ b/pkg/agent/openflow/fields.go
@@ -190,6 +190,11 @@ var (
 	// CTMark[6]: Mark to indicate the connection is hairpin.
 	// This CT mark is used in CtZone / CtZoneV6 and SNATCtZone / SNATCtZoneV6.
 	HairpinCTMark = binding.NewOneBitCTMark(6)
+
+	// CTMark[7]: Mark to indicate the connection should be redirected to an application-aware engine. This mark is only
+	// for L7 NetworkPolicy.
+	// This CT mark is used in CtZone / CtZoneV6.
+	L7NPRedirectCTMark = binding.NewOneBitCTMark(7)
 )
 
 // Fields using CT label.
@@ -199,4 +204,7 @@ var (
 
 	// Field to store the egress rule ID.
 	EgressRuleCTLabel = binding.NewCTLabel(32, 63)
+
+	// Field to store the VLAN ID allocated for a L7 NetworkPolicy rule.
+	L7NPRuleVlanIDCTLabel = binding.NewCTLabel(64, 75)
 )
diff --git a/pkg/agent/openflow/framework.go b/pkg/agent/openflow/framework.go
index 76d006594..c8fa85c59 100644
--- a/pkg/agent/openflow/framework.go
+++ b/pkg/agent/openflow/framework.go
@@ -213,6 +213,9 @@ func (f *featureNetworkPolicy) getRequiredTables() []*Table {
 			AntreaPolicyEgressRuleTable,
 			AntreaPolicyIngressRuleTable,
 		)
+		if f.nodeType == config.K8sNode {
+			tables = append(tables, TrafficControlTable) // For L7 NetworkPolicy.
+		}
 		if f.enableMulticast {
 			tables = append(tables,
 				MulticastEgressRuleTable,
diff --git a/pkg/agent/openflow/network_policy.go b/pkg/agent/openflow/network_policy.go
index c6fcc582b..5b232d57e 100644
--- a/pkg/agent/openflow/network_policy.go
+++ b/pkg/agent/openflow/network_policy.go
@@ -21,6 +21,7 @@ import (
 	"strings"
 	"sync"
 
+	"antrea.io/libOpenflow/openflow15"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/klog/v2"
@@ -1156,7 +1157,7 @@ func (f *featureNetworkPolicy) calculateActionFlowChangesForRule(rule *types.Pol
 			actionFlows = append(actionFlows, f.conjunctionActionPassFlow(ruleOfID, ruleTable, rule.Priority, rule.EnableLogging))
 		} else {
 			metricFlows = append(metricFlows, f.allowRulesMetricFlows(ruleOfID, isIngress, rule.TableID)...)
-			actionFlows = append(actionFlows, f.conjunctionActionFlow(ruleOfID, ruleTable, dropTable.GetNext(), rule.Priority, rule.EnableLogging)...)
+			actionFlows = append(actionFlows, f.conjunctionActionFlow(ruleOfID, ruleTable, dropTable.GetNext(), rule.Priority, rule.EnableLogging, rule.L7RuleVlanID)...)
 		}
 		conj.actionFlows = actionFlows
 		conj.metricFlows = metricFlows
@@ -1993,10 +1994,11 @@ func (c *client) NetworkPolicyMetrics() map[uint32]*types.RuleMetric {
 }
 
 type featureNetworkPolicy struct {
-	cookieAllocator cookie.Allocator
-	ipProtocols     []binding.Protocol
-	bridge          binding.Bridge
-	nodeType        config.NodeType
+	cookieAllocator       cookie.Allocator
+	ipProtocols           []binding.Protocol
+	bridge                binding.Bridge
+	nodeType              config.NodeType
+	l7NetworkPolicyConfig *config.L7NetworkPolicyConfig
 
 	// globalConjMatchFlowCache is a global map for conjMatchFlowContext. The key is a string generated from the
 	// conjMatchFlowContext.
@@ -2011,6 +2013,7 @@ type featureNetworkPolicy struct {
 	ovsMetersAreSupported bool
 	enableDenyTracking    bool
 	enableAntreaPolicy    bool
+	enableL7NetworkPolicy bool
 	enableMulticast       bool
 	proxyAll              bool
 	ctZoneSrcField        *binding.RegField
@@ -2030,9 +2033,11 @@ func newFeatureNetworkPolicy(
 	cookieAllocator cookie.Allocator,
 	ipProtocols []binding.Protocol,
 	bridge binding.Bridge,
+	l7NetworkPolicyConfig *config.L7NetworkPolicyConfig,
 	ovsMetersAreSupported,
 	enableDenyTracking,
 	enableAntreaPolicy bool,
+	enableL7NetworkPolicy bool,
 	enableMulticast bool,
 	proxyAll bool,
 	connectUplinkToBridge bool,
@@ -2042,6 +2047,8 @@ func newFeatureNetworkPolicy(
 		ipProtocols:              ipProtocols,
 		bridge:                   bridge,
 		nodeType:                 nodeType,
+		enableL7NetworkPolicy:    enableL7NetworkPolicy,
+		l7NetworkPolicyConfig:    l7NetworkPolicyConfig,
 		globalConjMatchFlowCache: make(map[string]*conjMatchFlowContext),
 		policyCache:              cache.NewIndexer(policyConjKeyFunc, cache.Indexers{priorityIndex: priorityIndexFunc}),
 		enableMulticast:          enableMulticast,
@@ -2065,6 +2072,9 @@ func (f *featureNetworkPolicy) initFlows() []binding.Flow {
 	var flows []binding.Flow
 	if f.nodeType == config.K8sNode {
 		flows = append(flows, f.ingressClassifierFlows()...)
+		if f.enableL7NetworkPolicy {
+			flows = append(flows, f.l7NPTrafficControlFlows()...)
+		}
 	}
 	flows = append(flows, f.skipPolicyRuleCheckFlows()...)
 	return flows
@@ -2118,3 +2128,39 @@ func (f *featureNetworkPolicy) skipPolicyRuleCheckFlows() []binding.Flow {
 	}
 	return flows
 }
+
+func (f *featureNetworkPolicy) l7NPTrafficControlFlows() []binding.Flow {
+	cookieID := f.cookieAllocator.Request(f.category).Raw()
+	vlanMask := uint16(openflow15.OFPVID_PRESENT)
+	return []binding.Flow{
+		// This generates the flow to output the packets marked with L7NPRedirectCTMark to an application-aware engine
+		// via the target ofPort. Note that, before outputting the packets, VLAN ID stored on field L7NPRuleVlanIDCTMarkField
+		// will be copied to VLAN ID register (OXM_OF_VLAN_VID) to set VLAN ID of the packets.
+		L2ForwardingOutTable.ofTable.BuildFlow(priorityHigh+1).
+			Cookie(cookieID).
+			MatchRegMark(OFPortFoundRegMark).
+			MatchCTMark(L7NPRedirectCTMark).
+			Action().PushVLAN(EtherTypeDot1q).
+			Action().MoveRange(binding.NxmFieldCtLabel, binding.OxmFieldVLANVID, *L7NPRuleVlanIDCTLabel.GetRange(), *binding.VLANVIDRange).
+			Action().Output(f.l7NetworkPolicyConfig.TargetOFPort).
+			Done(),
+		// This generates the flow to mark the packets from an application-aware engine via the return ofPort and forward
+		// the packets to stageRouting directly. Note that, for the packets which are originally to be output to a tunnel
+		// port, value of NXM_NX_TUN_IPV4_DST needs to be loaded in L3ForwardingTable of stageRouting.
+		ClassifierTable.ofTable.BuildFlow(priorityNormal).
+			Cookie(f.cookieAllocator.Request(f.category).Raw()).
+			MatchInPort(f.l7NetworkPolicyConfig.ReturnOFPort).
+			MatchVLAN(false, 0, &vlanMask).
+			Action().PopVLAN().
+			Action().LoadRegMark(FromTCReturnRegMark).
+			Action().GotoStage(stageRouting).
+			Done(),
+		// This generates the flow to forward the returned packets (with FromTCReturnRegMark) to stageOutput directly
+		// after loading output port number to reg1 in L2ForwardingCalcTable.
+		TrafficControlTable.ofTable.BuildFlow(priorityHigh).
+			Cookie(cookieID).
+			MatchRegMark(OFPortFoundRegMark, FromTCReturnRegMark).
+			Action().GotoStage(stageOutput).
+			Done(),
+	}
+}
diff --git a/pkg/agent/openflow/pipeline.go b/pkg/agent/openflow/pipeline.go
index 1715fbdc4..15819c410 100644
--- a/pkg/agent/openflow/pipeline.go
+++ b/pkg/agent/openflow/pipeline.go
@@ -405,6 +405,7 @@ type client struct {
 	enableProxy           bool
 	proxyAll              bool
 	enableAntreaPolicy    bool
+	enableL7NetworkPolicy bool
 	enableDenyTracking    bool
 	enableEgress          bool
 	enableMulticast       bool
@@ -434,11 +435,12 @@ type client struct {
 	// enables convenient mocking in unit tests.
 	ofEntryOperations OFEntryOperations
 	// replayMutex provides exclusive access to the OFSwitch to the ReplayFlows method.
-	replayMutex   sync.RWMutex
-	nodeConfig    *config.NodeConfig
-	networkConfig *config.NetworkConfig
-	egressConfig  *config.EgressConfig
-	serviceConfig *config.ServiceConfig
+	replayMutex           sync.RWMutex
+	nodeConfig            *config.NodeConfig
+	networkConfig         *config.NetworkConfig
+	egressConfig          *config.EgressConfig
+	serviceConfig         *config.ServiceConfig
+	l7NetworkPolicyConfig *config.L7NetworkPolicyConfig
 	// ovsMetersAreSupported indicates whether the OVS datapath supports OpenFlow meters.
 	ovsMetersAreSupported bool
 	// packetInHandlers stores handler to process PacketIn event. Each packetin reason can have multiple handlers registered.
@@ -1742,7 +1744,7 @@ func (f *featurePodConnectivity) ipv6Flows() []binding.Flow {
 
 // For normal traffic, conjunctionActionFlow generates the flow to jump to a specific table if policyRuleConjunction ID is matched. Priority of
 // conjunctionActionFlow is created at priorityLow for k8s network policies, and *priority assigned by PriorityAssigner for AntreaPolicy.
-func (f *featureNetworkPolicy) conjunctionActionFlow(conjunctionID uint32, table binding.Table, nextTable uint8, priority *uint16, enableLogging bool) []binding.Flow {
+func (f *featureNetworkPolicy) conjunctionActionFlow(conjunctionID uint32, table binding.Table, nextTable uint8, priority *uint16, enableLogging bool, l7RuleVlanID *uint32) []binding.Flow {
 	tableID := table.GetID()
 	cookieID := f.cookieAllocator.Request(f.category).Raw()
 	var ofPriority uint16
@@ -1769,6 +1771,19 @@ func (f *featureNetworkPolicy) conjunctionActionFlow(conjunctionID uint32, table
 			if f.ovsMetersAreSupported {
 				fb = fb.Action().Meter(PacketInMeterIDNP)
 			}
+			if l7RuleVlanID != nil {
+				return fb.
+					Action().LoadToRegField(conjReg, conjunctionID).                           // Traceflow.
+					Action().LoadRegMark(DispositionAllowRegMark, CustomReasonLoggingRegMark). // AntreaPolicy, Enable logging.
+					Action().SendToController(uint8(PacketInReasonNP)).
+					Action().CT(true, nextTable, ctZone, f.ctZoneSrcField). // CT action requires commit flag if actions other than NAT without arguments are specified.
+					LoadToLabelField(uint64(conjunctionID), labelField).
+					LoadToCtMark(L7NPRedirectCTMark).                               // Mark the packets of the connection should be redirected to an application-aware engine.
+					LoadToLabelField(uint64(*l7RuleVlanID), L7NPRuleVlanIDCTLabel). // Load the VLAN ID allocated for L7 NetworkPolicy rule to CT mark field L7NPRuleVlanIDCTMarkField.
+					CTDone().
+					Cookie(cookieID).
+					Done()
+			}
 			return fb.
 				Action().LoadToRegField(conjReg, conjunctionID).                           // Traceflow.
 				Action().LoadRegMark(DispositionAllowRegMark, CustomReasonLoggingRegMark). // AntreaPolicy, Enable logging.
@@ -1779,6 +1794,18 @@ func (f *featureNetworkPolicy) conjunctionActionFlow(conjunctionID uint32, table
 				Cookie(cookieID).
 				Done()
 		}
+		if l7RuleVlanID != nil {
+			return table.BuildFlow(ofPriority).MatchProtocol(proto).
+				MatchConjID(conjunctionID).
+				Action().LoadToRegField(conjReg, conjunctionID).        // Traceflow.
+				Action().CT(true, nextTable, ctZone, f.ctZoneSrcField). // CT action requires commit flag if actions other than NAT without arguments are specified.
+				LoadToLabelField(uint64(conjunctionID), labelField).
+				LoadToCtMark(L7NPRedirectCTMark).                               // Mark the packets of the connection should be redirected to an application-aware engine.
+				LoadToLabelField(uint64(*l7RuleVlanID), L7NPRuleVlanIDCTLabel). // Load the VLAN ID allocated for L7 NetworkPolicy rule to CT mark field L7NPRuleVlanIDCTMarkField.
+				CTDone().
+				Cookie(cookieID).
+				Done()
+		}
 		return table.BuildFlow(ofPriority).MatchProtocol(proto).
 			MatchConjID(conjunctionID).
 			Action().LoadToRegField(conjReg, conjunctionID).        // Traceflow.
@@ -2672,6 +2699,7 @@ func NewClient(bridgeName string,
 	mgmtAddr string,
 	enableProxy bool,
 	enableAntreaPolicy bool,
+	enableL7NetworkPolicy bool,
 	enableEgress bool,
 	enableDenyTracking bool,
 	proxyAll bool,
@@ -2685,6 +2713,7 @@ func NewClient(bridgeName string,
 		enableProxy:           enableProxy,
 		proxyAll:              proxyAll,
 		enableAntreaPolicy:    enableAntreaPolicy,
+		enableL7NetworkPolicy: enableL7NetworkPolicy,
 		enableDenyTracking:    enableDenyTracking,
 		enableEgress:          enableEgress,
 		enableMulticast:       enableMulticast,
diff --git a/pkg/agent/route/route_linux.go b/pkg/agent/route/route_linux.go
index 3ad4f5c7b..734286c76 100644
--- a/pkg/agent/route/route_linux.go
+++ b/pkg/agent/route/route_linux.go
@@ -137,7 +137,7 @@ func NewClient(networkConfig *config.NetworkConfig, noSNAT, proxyAll, connectUpl
 		multicastEnabled:      multicastEnabled,
 		connectUplinkToBridge: connectUplinkToBridge,
 		ipset:                 ipset.NewClient(),
-		netlink:               utilnetlink.NewClient(),
+		netlink:               &netlink.Handle{},
 		isCloudEKS:            env.IsCloudEKS(),
 	}, nil
 }
diff --git a/pkg/agent/types/networkpolicy.go b/pkg/agent/types/networkpolicy.go
index da5a5bbd0..90bcc6a27 100644
--- a/pkg/agent/types/networkpolicy.go
+++ b/pkg/agent/types/networkpolicy.go
@@ -80,6 +80,7 @@ type PolicyRule struct {
 	To            []Address
 	Service       []v1beta2.Service
 	L7Protocols   []v1beta2.L7Protocol
+	L7RuleVlanID  *uint32
 	Action        *secv1alpha1.RuleAction
 	Priority      *uint16
 	Name          string
@@ -94,10 +95,6 @@ func (r *PolicyRule) IsAntreaNetworkPolicyRule() bool {
 	return r.PolicyRef.Type != v1beta2.K8sNetworkPolicy
 }
 
-func (r *PolicyRule) IsL7Rule() bool {
-	return len(r.L7Protocols) > 0
-}
-
 // Priority is a struct that is composed of Antrea NetworkPolicy priority, rule priority and Tier priority.
 // It is used as the basic unit for priority sorting.
 type Priority struct {
diff --git a/pkg/agent/util/netlink/netlink_linux.go b/pkg/agent/util/netlink/netlink_linux.go
index f8648fb9a..fda905e9a 100644
--- a/pkg/agent/util/netlink/netlink_linux.go
+++ b/pkg/agent/util/netlink/netlink_linux.go
@@ -40,53 +40,3 @@ type Interface interface {
 
 	LinkByName(name string) (netlink.Link, error)
 }
-
-type Client struct{}
-
-func NewClient() *Client {
-	return &Client{}
-}
-
-func (c *Client) RouteReplace(route *netlink.Route) error {
-	return netlink.RouteReplace(route)
-}
-
-func (c *Client) RouteList(link netlink.Link, family int) ([]netlink.Route, error) {
-	return netlink.RouteList(link, family)
-}
-
-func (c *Client) RouteListFiltered(family int, filter *netlink.Route, filterMask uint64) ([]netlink.Route, error) {
-	return netlink.RouteListFiltered(family, filter, filterMask)
-}
-
-func (c *Client) RouteDel(route *netlink.Route) error {
-	return netlink.RouteDel(route)
-}
-
-func (c *Client) AddrList(link netlink.Link, family int) ([]netlink.Addr, error) {
-	return netlink.AddrList(link, family)
-}
-
-func (c *Client) AddrReplace(link netlink.Link, addr *netlink.Addr) error {
-	return netlink.AddrReplace(link, addr)
-}
-
-func (c *Client) AddrDel(link netlink.Link, addr *netlink.Addr) error {
-	return netlink.AddrDel(link, addr)
-}
-
-func (c *Client) NeighList(linkIndex, family int) ([]netlink.Neigh, error) {
-	return netlink.NeighList(linkIndex, family)
-}
-
-func (c *Client) NeighSet(neigh *netlink.Neigh) error {
-	return netlink.NeighSet(neigh)
-}
-
-func (c *Client) NeighDel(neigh *netlink.Neigh) error {
-	return netlink.NeighDel(neigh)
-}
-
-func (c *Client) LinkByName(name string) (netlink.Link, error) {
-	return netlink.LinkByName(name)
-}
diff --git a/pkg/config/agent/config.go b/pkg/config/agent/config.go
index c2ebaec0d..650966a70 100644
--- a/pkg/config/agent/config.go
+++ b/pkg/config/agent/config.go
@@ -279,7 +279,7 @@ type IPsecConfig struct {
 }
 
 type MulticlusterConfig struct {
-	// Enable Multicluster which allow cross-cluster traffic between member clusters
+	// Enable Multicluster which allows cross-cluster traffic between member clusters
 	// in a ClusterSet.
 	Enable bool `yaml:"enable,omitempty"`
 	// The Namespace where the Antrea Multi-cluster controller is running.
diff --git a/pkg/config/controller/config.go b/pkg/config/controller/config.go
index ecec3eb43..a7b70c02a 100644
--- a/pkg/config/controller/config.go
+++ b/pkg/config/controller/config.go
@@ -72,7 +72,7 @@ type ControllerConfig struct {
 }
 
 type MulticlusterConfig struct {
-	// Enable StretchedNetworkPolicy which allow Antrea-native policies to select peers
+	// Enable StretchedNetworkPolicy which allows Antrea-native policies to select peers
 	// from other clusters in a ClusterSet.
 	EnableStretchedNetworkPolicy bool `yaml:"enableStretchedNetworkPolicy,omitempty"`
 	// The Namespace where the Antrea Multi-cluster controller is running.
diff --git a/pkg/controller/networkpolicy/networkpolicy_controller.go b/pkg/controller/networkpolicy/networkpolicy_controller.go
index 5a3a4a60b..629016f3a 100644
--- a/pkg/controller/networkpolicy/networkpolicy_controller.go
+++ b/pkg/controller/networkpolicy/networkpolicy_controller.go
@@ -240,7 +240,7 @@ type NetworkPolicyController struct {
 	groupingInterfaceSynced func() bool
 
 	labelIdentityInterface labelidentity.Interface
-	// Enable Stretched Networkpolicy feature which allow Antrea-native policies to select peer
+	// Enable Stretched Networkpolicy feature which allows Antrea-native policies to select peer
 	// from other clusters in a ClusterSet.
 	stretchNPEnabled bool
 	// heartbeatCh is an internal channel for testing. It's used to know whether all tasks have been
diff --git a/pkg/controller/networkpolicy/validate.go b/pkg/controller/networkpolicy/validate.go
index 3fd62ad0d..a76a4188d 100644
--- a/pkg/controller/networkpolicy/validate.go
+++ b/pkg/controller/networkpolicy/validate.go
@@ -772,6 +772,9 @@ func (v *antreaPolicyValidator) validateL7Protocols(ingressRules, egressRules []
 		if len(r.L7Protocols) == 0 {
 			continue
 		}
+		if !features.DefaultFeatureGate.Enabled(features.L7NetworkPolicy) {
+			return fmt.Sprintf("layer 7 protocols can only be used when L7NetworkPolicy is enabled"), false
+		}
 		if *r.Action != crdv1alpha1.RuleActionAllow {
 			return "layer 7 protocols only support Allow", false
 		}
diff --git a/pkg/features/antrea_features.go b/pkg/features/antrea_features.go
index 4230b914c..4475ef876 100644
--- a/pkg/features/antrea_features.go
+++ b/pkg/features/antrea_features.go
@@ -113,9 +113,14 @@ const (
 	// Enable running agent on an unmanaged VM/BM.
 	ExternalNode featuregate.Feature = "ExternalNode"
 
-	// alpha: v1.9
+	// alpha: v1.10
 	// Enable collecting support bundle files with SupportBundleCollection CRD.
 	SupportBundleCollection featuregate.Feature = "SupportBundleCollection"
+
+	// alpha: v1.10
+	// Enable users to protect their applications by specifying how they are allowed to communicate with others, taking
+	// into account application context.
+	L7NetworkPolicy featuregate.Feature = "L7NetworkPolicy"
 )
 
 var (
@@ -149,6 +154,7 @@ var (
 		IPsecCertAuth:           {Default: false, PreRelease: featuregate.Alpha},
 		ExternalNode:            {Default: false, PreRelease: featuregate.Alpha},
 		SupportBundleCollection: {Default: false, PreRelease: featuregate.Alpha},
+		L7NetworkPolicy:         {Default: false, PreRelease: featuregate.Alpha},
 	}
 
 	// UnsupportedFeaturesOnWindows records the features not supported on
@@ -170,7 +176,8 @@ var (
 		IPsecCertAuth:     {},
 		// Multicluster feature is not validated on Windows yet. This can removed
 		// in the future if it's fully tested on Windows.
-		Multicluster: {},
+		Multicluster:    {},
+		L7NetworkPolicy: {},
 	}
 	// supportedFeaturesOnExternalNode records the features supported on an external
 	// Node. Antrea Agent checks the enabled features if it is running on an
@@ -180,6 +187,7 @@ var (
 		AntreaPolicy:            {},
 		NetworkPolicyStats:      {},
 		SupportBundleCollection: {},
+		L7NetworkPolicy:         {},
 	}
 )
 
diff --git a/pkg/flowaggregator/exporter/ipfix.go b/pkg/flowaggregator/exporter/ipfix.go
index 2f0eed1c1..df78e962b 100644
--- a/pkg/flowaggregator/exporter/ipfix.go
+++ b/pkg/flowaggregator/exporter/ipfix.go
@@ -191,7 +191,7 @@ func (e *IPFIXExporter) initExportingProcess() error {
 			SendJSONRecord:      e.sendJSONRecord,
 		}
 	}
-	ep, err := ipfix.NewIPFIXExportingProcess(expInput)
+	ep, err := exporter.InitExportingProcess(expInput)
 	if err != nil {
 		return fmt.Errorf("got error when initializing IPFIX exporting process: %v", err)
 	}
diff --git a/pkg/flowaggregator/flowaggregator.go b/pkg/flowaggregator/flowaggregator.go
index f31e7d358..8fc1bf857 100644
--- a/pkg/flowaggregator/flowaggregator.go
+++ b/pkg/flowaggregator/flowaggregator.go
@@ -266,7 +266,7 @@ func (fa *flowAggregator) InitCollectingProcess() error {
 	cpInput.NumExtraElements = len(infoelements.AntreaSourceStatsElementList) + len(infoelements.AntreaDestinationStatsElementList) + len(infoelements.AntreaLabelsElementList) +
 		len(infoelements.AntreaFlowEndSecondsElementList) + len(infoelements.AntreaThroughputElementList) + len(infoelements.AntreaSourceThroughputElementList) + len(infoelements.AntreaDestinationThroughputElementList)
 	var err error
-	fa.collectingProcess, err = ipfix.NewIPFIXCollectingProcess(cpInput)
+	fa.collectingProcess, err = collector.InitCollectingProcess(cpInput)
 	return err
 }
 
@@ -280,7 +280,7 @@ func (fa *flowAggregator) InitAggregationProcess() error {
 		InactiveExpiryTimeout: fa.inactiveFlowRecordTimeout,
 		AggregateElements:     aggregationElements,
 	}
-	fa.aggregationProcess, err = ipfix.NewIPFIXAggregationProcess(apInput)
+	fa.aggregationProcess, err = ipfixintermediate.InitAggregationProcess(apInput)
 	return err
 }
 
@@ -385,11 +385,11 @@ func (fa *flowAggregator) sendFlowKeyRecord(key ipfixintermediate.FlowKey, recor
 	isRecordIPv4 := fa.aggregationProcess.IsAggregatedRecordIPv4(*record)
 	if !fa.aggregationProcess.AreCorrelatedFieldsFilled(*record) {
 		fa.fillK8sMetadata(key, record.Record)
-		fa.aggregationProcess.SetCorrelatedFieldsFilled(record)
+		fa.aggregationProcess.SetCorrelatedFieldsFilled(record, true)
 	}
 	if fa.includePodLabels && !fa.aggregationProcess.AreExternalFieldsFilled(*record) {
 		fa.fillPodLabels(key, record.Record)
-		fa.aggregationProcess.SetExternalFieldsFilled(record)
+		fa.aggregationProcess.SetExternalFieldsFilled(record, true)
 	}
 	if fa.ipfixExporter != nil {
 		if err := fa.ipfixExporter.AddRecord(record.Record, !isRecordIPv4); err != nil {
diff --git a/pkg/ipfix/ipfix_collector.go b/pkg/ipfix/ipfix_collector.go
index 320551ab1..659ce466a 100644
--- a/pkg/ipfix/ipfix_collector.go
+++ b/pkg/ipfix/ipfix_collector.go
@@ -15,14 +15,9 @@
 package ipfix
 
 import (
-	"fmt"
-
-	ipfixcollect "github.com/vmware/go-ipfix/pkg/collector"
 	ipfixentities "github.com/vmware/go-ipfix/pkg/entities"
 )
 
-var _ IPFIXCollectingProcess = new(ipfixCollectingProcess)
-
 // IPFIXCollectingProcess interface is added to facilitate unit testing without involving the code from go-ipfix library.
 type IPFIXCollectingProcess interface {
 	Start()
@@ -31,38 +26,3 @@ type IPFIXCollectingProcess interface {
 	GetNumRecordsReceived() int64
 	GetNumConnToCollector() int64
 }
-
-type ipfixCollectingProcess struct {
-	CollectingProcess *ipfixcollect.CollectingProcess
-}
-
-func NewIPFIXCollectingProcess(input ipfixcollect.CollectorInput) (*ipfixCollectingProcess, error) {
-	cp, err := ipfixcollect.InitCollectingProcess(input)
-	if err != nil {
-		return nil, fmt.Errorf("error while initializing IPFIX collecting process: %v", err)
-	}
-
-	return &ipfixCollectingProcess{
-		CollectingProcess: cp,
-	}, nil
-}
-
-func (cp *ipfixCollectingProcess) Start() {
-	cp.CollectingProcess.Start()
-}
-
-func (cp *ipfixCollectingProcess) Stop() {
-	cp.CollectingProcess.Stop()
-}
-
-func (cp *ipfixCollectingProcess) GetMsgChan() chan *ipfixentities.Message {
-	return cp.CollectingProcess.GetMsgChan()
-}
-
-func (cp *ipfixCollectingProcess) GetNumRecordsReceived() int64 {
-	return cp.CollectingProcess.GetNumRecordsReceived()
-}
-
-func (cp *ipfixCollectingProcess) GetNumConnToCollector() int64 {
-	return cp.CollectingProcess.GetNumConnToCollector()
-}
diff --git a/pkg/ipfix/ipfix_intermediate.go b/pkg/ipfix/ipfix_intermediate.go
index 83a54657c..e075f0f58 100644
--- a/pkg/ipfix/ipfix_intermediate.go
+++ b/pkg/ipfix/ipfix_intermediate.go
@@ -15,15 +15,12 @@
 package ipfix
 
 import (
-	"fmt"
 	"time"
 
 	ipfixentities "github.com/vmware/go-ipfix/pkg/entities"
 	ipfixintermediate "github.com/vmware/go-ipfix/pkg/intermediate"
 )
 
-var _ IPFIXAggregationProcess = new(ipfixAggregationProcess)
-
 // IPFIXAggregationProcess interface is added to facilitate unit testing without involving the code from go-ipfix library.
 type IPFIXAggregationProcess interface {
 	Start()
@@ -32,74 +29,10 @@ type IPFIXAggregationProcess interface {
 	GetExpiryFromExpirePriorityQueue() time.Duration
 	GetRecords(flowKey *ipfixintermediate.FlowKey) []map[string]interface{}
 	ResetStatAndThroughputElementsInRecord(record ipfixentities.Record) error
-	SetCorrelatedFieldsFilled(record *ipfixintermediate.AggregationFlowRecord)
+	SetCorrelatedFieldsFilled(record *ipfixintermediate.AggregationFlowRecord, isFilled bool)
 	AreCorrelatedFieldsFilled(record ipfixintermediate.AggregationFlowRecord) bool
 	IsAggregatedRecordIPv4(record ipfixintermediate.AggregationFlowRecord) bool
-	SetExternalFieldsFilled(record *ipfixintermediate.AggregationFlowRecord)
+	SetExternalFieldsFilled(record *ipfixintermediate.AggregationFlowRecord, isFilled bool)
 	AreExternalFieldsFilled(record ipfixintermediate.AggregationFlowRecord) bool
 	GetNumFlows() int64
 }
-
-type ipfixAggregationProcess struct {
-	AggregationProcess *ipfixintermediate.AggregationProcess
-}
-
-func NewIPFIXAggregationProcess(input ipfixintermediate.AggregationInput) (*ipfixAggregationProcess, error) {
-	ap, err := ipfixintermediate.InitAggregationProcess(input)
-	if err != nil {
-		return nil, fmt.Errorf("error while initializing IPFIX intermediate process: %v", err)
-	}
-
-	return &ipfixAggregationProcess{
-		AggregationProcess: ap,
-	}, nil
-}
-
-func (ap *ipfixAggregationProcess) Start() {
-	ap.AggregationProcess.Start()
-}
-
-func (ap *ipfixAggregationProcess) Stop() {
-	ap.AggregationProcess.Stop()
-}
-
-func (ap *ipfixAggregationProcess) ForAllExpiredFlowRecordsDo(callback ipfixintermediate.FlowKeyRecordMapCallBack) error {
-	err := ap.AggregationProcess.ForAllExpiredFlowRecordsDo(callback)
-	return err
-}
-
-func (ap *ipfixAggregationProcess) GetExpiryFromExpirePriorityQueue() time.Duration {
-	return ap.AggregationProcess.GetExpiryFromExpirePriorityQueue()
-}
-
-func (ap *ipfixAggregationProcess) GetRecords(flowKey *ipfixintermediate.FlowKey) []map[string]interface{} {
-	return ap.AggregationProcess.GetRecords(flowKey)
-}
-
-func (ap *ipfixAggregationProcess) ResetStatAndThroughputElementsInRecord(record ipfixentities.Record) error {
-	return ap.AggregationProcess.ResetStatAndThroughputElementsInRecord(record)
-}
-
-func (ap *ipfixAggregationProcess) SetCorrelatedFieldsFilled(record *ipfixintermediate.AggregationFlowRecord) {
-	ap.AggregationProcess.SetCorrelatedFieldsFilled(record, true)
-}
-
-func (ap *ipfixAggregationProcess) AreCorrelatedFieldsFilled(record ipfixintermediate.AggregationFlowRecord) bool {
-	return ap.AggregationProcess.AreCorrelatedFieldsFilled(record)
-}
-
-func (ap *ipfixAggregationProcess) IsAggregatedRecordIPv4(record ipfixintermediate.AggregationFlowRecord) bool {
-	return ap.AggregationProcess.IsAggregatedRecordIPv4(record)
-}
-
-func (ap *ipfixAggregationProcess) SetExternalFieldsFilled(record *ipfixintermediate.AggregationFlowRecord) {
-	ap.AggregationProcess.SetExternalFieldsFilled(record, true)
-}
-
-func (ap *ipfixAggregationProcess) AreExternalFieldsFilled(record ipfixintermediate.AggregationFlowRecord) bool {
-	return ap.AggregationProcess.AreExternalFieldsFilled(record)
-}
-
-func (ap *ipfixAggregationProcess) GetNumFlows() int64 {
-	return ap.AggregationProcess.GetNumFlows()
-}
diff --git a/pkg/ipfix/ipfix_process.go b/pkg/ipfix/ipfix_process.go
index d4cda6787..6a185a1bb 100644
--- a/pkg/ipfix/ipfix_process.go
+++ b/pkg/ipfix/ipfix_process.go
@@ -15,46 +15,12 @@
 package ipfix
 
 import (
-	"fmt"
-
 	ipfixentities "github.com/vmware/go-ipfix/pkg/entities"
-	ipfixexport "github.com/vmware/go-ipfix/pkg/exporter"
 )
 
-var _ IPFIXExportingProcess = new(ipfixExportingProcess)
-
 // IPFIXExportingProcess interface is added to facilitate unit testing without involving the code from go-ipfix library.
 type IPFIXExportingProcess interface {
 	NewTemplateID() uint16
 	SendSet(set ipfixentities.Set) (int, error)
 	CloseConnToCollector()
 }
-
-type ipfixExportingProcess struct {
-	*ipfixexport.ExportingProcess
-}
-
-func NewIPFIXExportingProcess(input ipfixexport.ExporterInput) (*ipfixExportingProcess, error) {
-	expProcess, err := ipfixexport.InitExportingProcess(input)
-	if err != nil {
-		return nil, fmt.Errorf("error while initializing IPFIX exporting process: %v", err)
-	}
-
-	return &ipfixExportingProcess{
-		ExportingProcess: expProcess,
-	}, nil
-}
-
-func (exp *ipfixExportingProcess) SendSet(set ipfixentities.Set) (int, error) {
-	sentBytes, err := exp.ExportingProcess.SendSet(set)
-	return sentBytes, err
-}
-
-func (exp *ipfixExportingProcess) CloseConnToCollector() {
-	exp.ExportingProcess.CloseConnToCollector()
-	return
-}
-
-func (exp *ipfixExportingProcess) NewTemplateID() uint16 {
-	return exp.ExportingProcess.NewTemplateID()
-}
diff --git a/pkg/ovs/openflow/interfaces.go b/pkg/ovs/openflow/interfaces.go
index 9e7db1349..6fbc0d735 100644
--- a/pkg/ovs/openflow/interfaces.go
+++ b/pkg/ovs/openflow/interfaces.go
@@ -89,6 +89,9 @@ const (
 // IPDSCPToSRange stores the DSCP bits in ToS field of IP header.
 var IPDSCPToSRange = &Range{2, 7}
 
+// VLANVIDRange stores the VLAN VID range
+var VLANVIDRange = &Range{0, 11}
+
 // Bridge defines operations on an openflow bridge.
 type Bridge interface {
 	CreateTable(table Table, next uint8, missAction MissActionType) Table
diff --git a/pkg/ovs/openflow/ofctrl_builder.go b/pkg/ovs/openflow/ofctrl_builder.go
index 88179adb3..456873d17 100644
--- a/pkg/ovs/openflow/ofctrl_builder.go
+++ b/pkg/ovs/openflow/ofctrl_builder.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 
 	"antrea.io/libOpenflow/openflow15"
+	"antrea.io/libOpenflow/protocol"
 	"antrea.io/ofnet/ofctrl"
 )
 
@@ -40,10 +41,39 @@ func (b *ofFlowBuilder) MatchTunMetadata(index int, data uint32) FlowBuilder {
 	return b
 }
 
-// MatchVLAN matches the VLAN VID. It holds the VLAN VID in its least significant 12 bits.
+// MatchVLAN can be used as follows:
+// - to match the packets of a specific VLAN, there are two cases:
+//   - to match VLAN 0, nonVLAN should be false, vlanID should be 0, value of vlanMask must be 0x1fff.
+//   - VLAN 1-4095, nonVLAN should be false, vlanID should be the VLAN ID, vlanMask can be nil, or its value can be 0x1fff.
+//
+// - to match the packets of all VLANs, nonVLAN should be false, vlanID must be 0, value of vlanMask must be 0x1000.
+// - to match the packets of non-VLAN, nonVLAN should be true, vlanID must be 0, vlanMask can be nil, or its value can be 0x1000.
 func (b *ofFlowBuilder) MatchVLAN(nonVLAN bool, vlanID uint16, vlanMask *uint16) FlowBuilder {
-	// TODO(gran): correct matchStr
-	matchStr := fmt.Sprintf("dl_vlan=%d", vlanID)
+	if vlanMask == nil {
+		var vlanMaskValue uint16
+		// To match the packets of a VLAN whose VLAN ID is not 0, when vlanMask is nil, set the value of vlanMask to 0x1ffff.
+		if vlanID != 0 {
+			vlanMaskValue = uint16(openflow15.OFPVID_PRESENT | protocol.VID_MASK)
+		}
+		// To match the packets of non-VLAN, when vlanMask is nil, set the value of vlanMask to 0x1000.
+		if nonVLAN {
+			vlanMaskValue = uint16(openflow15.OFPVID_PRESENT)
+		}
+		vlanMask = &vlanMaskValue
+	}
+
+	value := vlanID
+	if !nonVLAN {
+		value |= openflow15.OFPVID_PRESENT
+	}
+	mask := *vlanMask
+
+	var matchStr string
+	if mask == uint16(openflow15.OFPVID_PRESENT|protocol.VID_MASK) {
+		matchStr = fmt.Sprintf("dl_vlan=%d", value&protocol.VID_MASK)
+	} else {
+		matchStr = fmt.Sprintf("vlan_tci=0x%04x/0x%04x", value&openflow15.OFPVID_PRESENT, openflow15.OFPVID_PRESENT)
+	}
 	b.matchers = append(b.matchers, matchStr)
 	b.Match.NonVlan = nonVLAN
 	b.Match.VlanId = &vlanID
diff --git a/plugins/octant/go.mod b/plugins/octant/go.mod
index 1a89f6084..3c8f07d5c 100644
--- a/plugins/octant/go.mod
+++ b/plugins/octant/go.mod
@@ -103,12 +103,12 @@ require (
 	go.uber.org/goleak v1.1.12 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
 	go.uber.org/zap v1.19.1 // indirect
-	golang.org/x/net v0.2.0 // indirect
+	golang.org/x/net v0.4.0 // indirect
 	golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
 	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
 	golang.org/x/sys v0.3.0 // indirect
-	golang.org/x/term v0.2.0 // indirect
-	golang.org/x/text v0.4.0 // indirect
+	golang.org/x/term v0.3.0 // indirect
+	golang.org/x/text v0.5.0 // indirect
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect
diff --git a/plugins/octant/go.sum b/plugins/octant/go.sum
index 287ce7250..9ee15a07e 100644
--- a/plugins/octant/go.sum
+++ b/plugins/octant/go.sum
@@ -1021,8 +1021,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1154,8 +1154,8 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
-golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1165,8 +1165,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=


Test patch:
diff --git a/pkg/agent/controller/networkpolicy/allocator_test.go b/pkg/agent/controller/networkpolicy/allocator_test.go
index cb99e6faf..4b14c0eb0 100644
--- a/pkg/agent/controller/networkpolicy/allocator_test.go
+++ b/pkg/agent/controller/networkpolicy/allocator_test.go
@@ -254,3 +254,31 @@ func TestIdAllocatorWorker(t *testing.T) {
 		})
 	}
 }
+
+func TestVlanIDAllocator(t *testing.T) {
+	vlanIDAllocator := newL7VlanIDAllocator()
+	ruleID1 := "rule1"
+	ruleID2 := "rule2"
+	ruleID3 := "rule3"
+	ruleID4 := "rule4"
+
+	vlanID1 := vlanIDAllocator.allocate(ruleID1)
+	assert.Equal(t, vlanID1, vlanIDAllocator.query(ruleID1))
+
+	vlanID2 := vlanIDAllocator.allocate(ruleID2)
+	assert.Equal(t, vlanID2, vlanIDAllocator.query(ruleID2))
+
+	vlanIDAllocator.release(ruleID1)
+	assert.Equal(t, uint32(0), vlanIDAllocator.query(ruleID1))
+
+	vlanIDAllocator.release(ruleID2)
+	assert.Equal(t, uint32(0), vlanIDAllocator.query(ruleID2))
+
+	vlanID3 := vlanIDAllocator.allocate(ruleID3)
+	assert.Equal(t, vlanID3, vlanIDAllocator.query(ruleID3))
+	assert.Equal(t, vlanID2, vlanID3)
+
+	vlanID4 := vlanIDAllocator.allocate(ruleID4)
+	assert.Equal(t, vlanID4, vlanIDAllocator.query(ruleID4))
+	assert.Equal(t, vlanID1, vlanID4)
+}
diff --git a/pkg/agent/controller/networkpolicy/l7engine/reconciler_test.go b/pkg/agent/controller/networkpolicy/l7engine/reconciler_test.go
new file mode 100644
index 000000000..2f99afbd9
--- /dev/null
+++ b/pkg/agent/controller/networkpolicy/l7engine/reconciler_test.go
@@ -0,0 +1,225 @@
+// Copyright 2022 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 l7engine
+
+import (
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/util/sets"
+
+	v1beta "antrea.io/antrea/pkg/apis/controlplane/v1beta2"
+)
+
+type fakeSuricata struct {
+	calledScCommands      sets.String
+	startSuricataFnCalled bool
+}
+
+func newFakeSuricata() *fakeSuricata {
+	return &fakeSuricata{
+		calledScCommands:      sets.NewString(),
+		startSuricataFnCalled: false,
+	}
+}
+
+func (f *fakeSuricata) suricataScFunc(scCmd string) (*scCmdRet, error) {
+	f.calledScCommands.Insert(scCmd)
+	return &scCmdRet{Return: scCmdOK}, nil
+}
+
+func (f *fakeSuricata) startSuricataFn() {
+	f.startSuricataFnCalled = true
+	defaultFS.Create(suricataCommandSocket)
+}
+
+func TestConvertProtocolHTTP(t *testing.T) {
+	testCases := []struct {
+		name     string
+		http     *v1beta.HTTPProtocol
+		expected string
+	}{
+		{
+			name:     "without host,method,path",
+			http:     &v1beta.HTTPProtocol{},
+			expected: "",
+		},
+		{
+			name: "with host,method,exact path",
+			http: &v1beta.HTTPProtocol{
+				Host:   "www.google.com",
+				Method: "GET",
+				Path:   "/index.html",
+			},
+			expected: `http.uri; content:"/index.html"; startswith; endswith; http.method; content:"GET"; http.host; content:"www.google.com"; startswith; endswith;`,
+		},
+		{
+			name: "with host suffix, path prefix",
+			http: &v1beta.HTTPProtocol{
+				Host: "*.foo.com",
+				Path: "/api/v2/*",
+			},
+			expected: `http.uri; content:"/api/v2/"; startswith; http.host; content:".foo.com"; endswith;`,
+		},
+		{
+			name: "with host pattern",
+			http: &v1beta.HTTPProtocol{
+				Host: "*.foo.*",
+			},
+			expected: `http.host; content:".foo.";`,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			assert.Equal(t, tc.expected, convertProtocolHTTP(tc.http))
+		})
+	}
+}
+
+func TestStartSuricata(t *testing.T) {
+	defaultFS = afero.NewMemMapFs()
+	defer func() {
+		defaultFS = afero.NewOsFs()
+	}()
+
+	_, err := defaultFS.Create(defaultSuricataConfigPath)
+	assert.NoError(t, err)
+
+	fe := NewReconciler()
+	fs := newFakeSuricata()
+	fe.suricataScFn = fs.suricataScFunc
+	fe.startSuricataFn = fs.startSuricataFn
+
+	fe.startSuricata()
+
+	ok, err := afero.FileContainsBytes(defaultFS, antreaSuricataConfigPath, []byte(`---
+af-packet:
+  - interface: antrea-l7-tap0
+    threads: auto
+    cluster-id: 80
+    cluster-type: cluster_flow
+    defrag: no
+    use-mmap: yes
+    tpacket-v2: yes
+    checksum-checks: no
+    copy-mode: ips
+    copy-iface: antrea-l7-tap1
+  - interface:  antrea-l7-tap1
+    threads: auto
+    cluster-id: 81
+    cluster-type: cluster_flow
+    defrag: no
+    use-mmap: yes
+    tpacket-v2: yes
+    checksum-checks: no
+    copy-mode: ips
+    copy-iface: antrea-l7-tap0
+multi-detect:
+  enabled: yes
+  selector: vlan`))
+	assert.NoError(t, err)
+	assert.True(t, ok)
+
+	ok, err = afero.FileContainsBytes(defaultFS, defaultSuricataConfigPath, []byte("include: /etc/suricata/antrea.yaml"))
+	assert.NoError(t, err)
+	assert.True(t, ok)
+}
+
+func TestRuleLifecycle(t *testing.T) {
+	ruleID := "123456"
+	vlanID := uint32(1)
+	policyName := "AntreaNetworkPolicy:test-l7"
+
+	testCases := []struct {
+		name                 string
+		l7Protocols          []v1beta.L7Protocol
+		updatedL7Protocols   []v1beta.L7Protocol
+		expectedRules        string
+		expectedUpdatedRules string
+	}{
+		{
+			name: "protocol HTTP",
+			l7Protocols: []v1beta.L7Protocol{
+				{
+					HTTP: &v1beta.HTTPProtocol{
+						Host:   "www.google.com",
+						Method: "GET",
+						Path:   "/index.html",
+					},
+				},
+			},
+			updatedL7Protocols: []v1beta.L7Protocol{
+				{
+					HTTP: &v1beta.HTTPProtocol{},
+				},
+			},
+			expectedRules:        `pass http any any -> any any (msg: "Allow http by AntreaNetworkPolicy:test-l7"; http.uri; content:"/index.html"; startswith; endswith; http.method; content:"GET"; http.host; content:"www.google.com"; startswith; endswith; sid: 2;)`,
+			expectedUpdatedRules: `pass http any any -> any any (msg: "Allow http by AntreaNetworkPolicy:test-l7"; sid: 2;)`,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			defaultFS = afero.NewMemMapFs()
+			defer func() {
+				defaultFS = afero.NewOsFs()
+			}()
+
+			_, err := defaultFS.Create(defaultSuricataConfigPath)
+			assert.NoError(t, err)
+
+			fe := NewReconciler()
+			fs := newFakeSuricata()
+			fe.suricataScFn = fs.suricataScFunc
+			fe.startSuricataFn = fs.startSuricataFn
+
+			// Test add a L7 NetworkPolicy.
+			assert.NoError(t, fe.AddRule(ruleID, policyName, vlanID, tc.l7Protocols))
+
+			rulesPath := generateTenantRulesPath(vlanID)
+			ok, err := afero.FileContainsBytes(defaultFS, rulesPath, []byte(tc.expectedRules))
+			assert.NoError(t, err)
+			assert.True(t, ok)
+
+			configPath := generateTenantConfigPath(vlanID)
+			ok, err = afero.FileContainsBytes(defaultFS, configPath, []byte(rulesPath))
+			assert.NoError(t, err)
+			assert.True(t, ok)
+
+			expectedScCommands := sets.NewString("register-tenant 1 /etc/suricata/antrea-tenant-1.yaml", "register-tenant-handler 1 vlan 1")
+			assert.True(t, fs.startSuricataFnCalled)
+			assert.Equal(t, expectedScCommands, fs.calledScCommands)
+
+			// Update the added L7 NetworkPolicy.
+			assert.NoError(t, fe.AddRule(ruleID, policyName, vlanID, tc.updatedL7Protocols))
+			expectedScCommands.Insert("reload-tenant 1 /etc/suricata/antrea-tenant-1.yaml")
+			assert.Equal(t, expectedScCommands, fs.calledScCommands)
+
+			// Delete the L7 NetworkPolicy.
+			assert.NoError(t, fe.DeleteRule(ruleID, vlanID))
+			expectedScCommands.Insert("unregister-tenant-handler 1 vlan 1", "unregister-tenant 1")
+			assert.Equal(t, expectedScCommands, fs.calledScCommands)
+
+			exists, err := afero.Exists(defaultFS, rulesPath)
+			assert.NoError(t, err)
+			assert.False(t, exists)
+
+			exists, err = afero.Exists(defaultFS, configPath)
+			assert.NoError(t, err)
+			assert.False(t, exists)
+		})
+	}
+}
diff --git a/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go b/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go
index f18c4f925..cb396d778 100644
--- a/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go
+++ b/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go
@@ -61,7 +61,7 @@ func newTestController() (*Controller, *fake.Clientset, *mockReconciler) {
 	ch2 := make(chan string, 100)
 	groupIDAllocator := openflow.NewGroupAllocator(false)
 	groupCounters := []proxytypes.GroupCounter{proxytypes.NewGroupCounter(groupIDAllocator, ch2)}
-	controller, _ := NewNetworkPolicyController(&antreaClientGetter{clientset}, nil, nil, "node1", podUpdateChannel, nil, groupCounters, ch2, true, true, true, false, true, testAsyncDeleteInterval, "8.8.8.8:53", config.K8sNode, true, false, config.HostGatewayOFPort, config.DefaultTunOFPort)
+	controller, _ := NewNetworkPolicyController(&antreaClientGetter{clientset}, nil, nil, "node1", podUpdateChannel, nil, groupCounters, ch2, true, true, true, true, false, true, testAsyncDeleteInterval, "8.8.8.8:53", config.K8sNode, true, false, config.HostGatewayOFPort, config.DefaultTunOFPort)
 	reconciler := newMockReconciler()
 	controller.reconciler = reconciler
 	controller.antreaPolicyLogger = nil
diff --git a/pkg/agent/openflow/client_test.go b/pkg/agent/openflow/client_test.go
index fc75cdf8b..08af50960 100644
--- a/pkg/agent/openflow/client_test.go
+++ b/pkg/agent/openflow/client_test.go
@@ -63,6 +63,9 @@ var (
 
 	_, fakeEgressExceptIPv4CIDR, _ = net.ParseCIDR("192.168.78.0/24")
 	_, fakeEgressExceptIPv6CIDR, _ = net.ParseCIDR("fec0:192:168:78::/80")
+
+	fakeL7NPTargetOFPort = uint32(10)
+	fakeL7NPReturnOFPort = uint32(11)
 )
 
 func skipTest(tb testing.TB, skipLinux, skipWindows bool) {
@@ -83,6 +86,7 @@ type clientOptions struct {
 	enableMulticast       bool
 	enableTrafficControl  bool
 	enableMulticluster    bool
+	enableL7NetworkPolicy bool
 }
 
 type clientOptionsFn func(*clientOptions)
@@ -113,6 +117,10 @@ func enableMulticast(o *clientOptions) {
 	o.enableMulticast = true
 }
 
+func enableL7NetworkPolicy(o *clientOptions) {
+	o.enableL7NetworkPolicy = true
+}
+
 func enableTrafficControl(o *clientOptions) {
 	o.enableTrafficControl = true
 }
@@ -338,6 +346,7 @@ func newFakeClient(mockOFEntryOperations *oftest.MockOFEntryOperations,
 		enableMulticast:       false,
 		enableTrafficControl:  false,
 		enableMulticluster:    false,
+		enableL7NetworkPolicy: false,
 	}
 	for _, fn := range options {
 		fn(o)
@@ -347,6 +356,7 @@ func newFakeClient(mockOFEntryOperations *oftest.MockOFEntryOperations,
 		bridgeMgmtAddr,
 		o.enableProxy,
 		o.enableAntreaPolicy,
+		o.enableL7NetworkPolicy,
 		o.enableEgress,
 		false,
 		o.proxyAll,
@@ -360,6 +370,7 @@ func newFakeClient(mockOFEntryOperations *oftest.MockOFEntryOperations,
 	var serviceIPv4CIDR, serviceIPv6CIDR *net.IPNet
 	var nodePortAddressesIPv4, nodePortAddressesIPv6 []net.IP
 	var ipProtocols []binding.Protocol
+	var l7NetworkPolicyConfig *config.L7NetworkPolicyConfig
 
 	if enableIPv4 {
 		fakeNodeIPv4Addr.IP = fakeNodeIPv4
@@ -425,6 +436,13 @@ func newFakeClient(mockOFEntryOperations *oftest.MockOFEntryOperations,
 		NodePortAddressesIPv6: nodePortAddressesIPv6,
 	}
 
+	if o.enableL7NetworkPolicy {
+		l7NetworkPolicyConfig = &config.L7NetworkPolicyConfig{
+			TargetOFPort: fakeL7NPTargetOFPort,
+			ReturnOFPort: fakeL7NPReturnOFPort,
+		}
+	}
+
 	client.cookieAllocator = cookie.NewAllocator(1)
 	client.ofEntryOperations = mockOFEntryOperations
 	client.nodeConfig = nodeConfig
@@ -432,6 +450,7 @@ func newFakeClient(mockOFEntryOperations *oftest.MockOFEntryOperations,
 	client.networkConfig = networkConfig
 	client.egressConfig = egressConfig
 	client.serviceConfig = serviceConfig
+	client.l7NetworkPolicyConfig = l7NetworkPolicyConfig
 	client.ipProtocols = ipProtocols
 	client.generatePipelines()
 	client.realizePipelines()
@@ -628,7 +647,7 @@ func Test_client_InstallPodFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=Classifier, priority=190,in_port=100 actions=set_field:0x3/0xf->reg0,goto_table:SpoofGuard",
 				"cookie=0x1010000000000, table=SpoofGuard, priority=200,ip,in_port=100,dl_src=00:00:10:10:00:66,nw_src=10.10.0.66 actions=goto_table:UnSNAT",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ip,reg0=0x200/0x200,nw_dst=10.10.0.66 actions=set_field:0a:00:00:00:00:01->eth_src,set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 			},
 		},
 		{
@@ -641,7 +660,7 @@ func Test_client_InstallPodFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=SpoofGuard, priority=200,ip,in_port=100,dl_src=00:00:10:10:00:66,nw_src=10.10.0.66 actions=goto_table:PipelineIPClassifier",
 				"cookie=0x1010000000000, table=Classifier, priority=190,in_port=100 actions=set_field:0x3/0xf->reg0,goto_table:SpoofGuard",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ip,reg0=0x200/0x200,nw_dst=10.10.0.66 actions=set_field:0a:00:00:00:00:01->eth_src,set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1050000000000, table=MulticastEgressPodMetric, priority=200,ip,nw_src=10.10.0.66 actions=goto_table:MulticastRouting",
 				"cookie=0x1050000000000, table=MulticastIngressPodMetric, priority=200,ip,reg1=0x64 actions=goto_table:MulticastOutput",
 			},
@@ -654,7 +673,7 @@ func Test_client_InstallPodFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=Classifier, priority=190,in_port=100 actions=set_field:0x3/0xf->reg0,goto_table:SpoofGuard",
 				"cookie=0x1010000000000, table=SpoofGuard, priority=200,ipv6,in_port=100,dl_src=00:00:10:10:00:66,ipv6_src=fec0:10:10::66 actions=goto_table:IPv6",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ipv6,reg0=0x200/0x200,ipv6_dst=fec0:10:10::66 actions=set_field:0a:00:00:00:00:01->eth_src,set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 			},
 		},
 		{
@@ -669,7 +688,7 @@ func Test_client_InstallPodFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=SpoofGuard, priority=200,ipv6,in_port=100,dl_src=00:00:10:10:00:66,ipv6_src=fec0:10:10::66 actions=goto_table:IPv6",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ip,reg0=0x200/0x200,nw_dst=10.10.0.66 actions=set_field:0a:00:00:00:00:01->eth_src,set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ipv6,reg0=0x200/0x200,ipv6_dst=fec0:10:10::66 actions=set_field:0a:00:00:00:00:01->eth_src,set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 			},
 		},
 		{
@@ -683,7 +702,7 @@ func Test_client_InstallPodFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=SpoofGuard, priority=200,ip,in_port=100,dl_src=00:00:10:10:00:66,nw_src=10.10.0.66 actions=goto_table:UnSNAT",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ip,reg0=0x200/0x200,nw_dst=10.10.0.66 actions=set_field:0a:00:00:00:00:01->eth_src,set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ip,nw_dst=10.10.0.66 actions=set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 			},
 		},
 		{
@@ -696,7 +715,7 @@ func Test_client_InstallPodFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=SpoofGuard, priority=200,ipv6,in_port=100,dl_src=00:00:10:10:00:66,ipv6_src=fec0:10:10::66 actions=goto_table:IPv6",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ipv6,reg0=0x200/0x200,ipv6_dst=fec0:10:10::66 actions=set_field:0a:00:00:00:00:01->eth_src,set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ipv6,ipv6_dst=fec0:10:10::66 actions=set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 			},
 		},
 		{
@@ -714,7 +733,7 @@ func Test_client_InstallPodFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ipv6,ipv6_dst=fec0:10:10::66 actions=set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ip,reg0=0x200/0x200,nw_dst=10.10.0.66 actions=set_field:0a:00:00:00:00:01->eth_src,set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ip,nw_dst=10.10.0.66 actions=set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 			},
 		},
 		{
@@ -725,12 +744,12 @@ func Test_client_InstallPodFlows(t *testing.T) {
 			trafficEncapMode: config.TrafficEncapModeNoEncap,
 			expectedFlows: []string{
 				"cookie=0x1010000000000, table=ARPSpoofGuard, priority=200,arp,in_port=100,arp_spa=192.168.77.200,arp_sha=00:00:10:10:00:66 actions=goto_table:ARPResponder",
-				"cookie=0x1010000000000, table=Classifier, priority=210,ip,in_port=4,dl_vlan=0,dl_dst=00:00:10:10:00:66 actions=set_field:0x1000/0xf000->reg8,set_field:0x4/0xf->reg0,set_field:0x0/0xfff->reg8,goto_table:UnSNAT",
-				"cookie=0x1010000000000, table=Classifier, priority=210,ip,in_port=4294967294,dl_vlan=0,dl_dst=00:00:10:10:00:66 actions=set_field:0x1000/0xf000->reg8,set_field:0x5/0xf->reg0,goto_table:UnSNAT",
+				"cookie=0x1010000000000, table=Classifier, priority=210,ip,in_port=4,vlan_tci=0x0000/0x1000,dl_dst=00:00:10:10:00:66 actions=set_field:0x1000/0xf000->reg8,set_field:0x4/0xf->reg0,set_field:0x0/0xfff->reg8,goto_table:UnSNAT",
+				"cookie=0x1010000000000, table=Classifier, priority=210,ip,in_port=4294967294,vlan_tci=0x0000/0x1000,dl_dst=00:00:10:10:00:66 actions=set_field:0x1000/0xf000->reg8,set_field:0x5/0xf->reg0,goto_table:UnSNAT",
 				"cookie=0x1010000000000, table=Classifier, priority=190,in_port=100 actions=set_field:0x3/0xf->reg0,set_field:0x100000/0x100000->reg4,set_field:0x200/0x200->reg0,goto_table:SpoofGuard",
 				"cookie=0x1010000000000, table=SpoofGuard, priority=200,ip,in_port=100,dl_src=00:00:10:10:00:66,nw_src=192.168.77.200 actions=set_field:0x1000/0xf000->reg8,set_field:0x0/0xfff->reg8,goto_table:UnSNAT",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ip,reg8=0x0/0xfff,nw_dst=192.168.77.200 actions=set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 			},
 		},
 		{
@@ -746,7 +765,7 @@ func Test_client_InstallPodFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=Classifier, priority=190,in_port=100 actions=set_field:0x3/0xf->reg0,set_field:0x100000/0x100000->reg4,set_field:0x200/0x200->reg0,goto_table:SpoofGuard",
 				"cookie=0x1010000000000, table=SpoofGuard, priority=200,ip,in_port=100,dl_src=00:00:10:10:00:66,nw_src=192.168.77.200 actions=set_field:0x1000/0xf000->reg8,set_field:0x1/0xfff->reg8,goto_table:UnSNAT",
 				"cookie=0x1010000000000, table=L3Forwarding, priority=200,ip,reg8=0x1/0xfff,nw_dst=192.168.77.200 actions=set_field:00:00:10:10:00:66->eth_dst,goto_table:L3DecTTL",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=00:00:10:10:00:66 actions=set_field:0x64->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1010000000000, table=VLAN, priority=190,reg1=0x4,in_port=100 actions=push_vlan:0x8100,set_field:4097->vlan_vid,goto_table:Output",
 			},
 		},
@@ -1513,7 +1532,7 @@ func Test_client_setBasePacketOutBuilder(t *testing.T) {
 }
 
 func prepareSetBasePacketOutBuilder(ctrl *gomock.Controller, success bool) *client {
-	ofClient := NewClient(bridgeName, bridgeMgmtAddr, true, true, false, false, false, false, false, false, false)
+	ofClient := NewClient(bridgeName, bridgeMgmtAddr, true, true, false, false, false, false, false, false, false, false)
 	c := ofClient.(*client)
 	m := ovsoftest.NewMockBridge(ctrl)
 	c.bridge = m
@@ -2123,7 +2142,7 @@ func Test_client_InstallMulticlusterClassifierFlows(t *testing.T) {
 	tunnelOFPort := uint32(200)
 	expectedFlows := []string{
 		"cookie=0x1060000000000, table=Classifier, priority=210,in_port=200,dl_dst=aa:bb:cc:dd:ee:f0 actions=set_field:0x1/0xf->reg0,set_field:0x200/0x200->reg0,goto_table:UnSNAT",
-		"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=aa:bb:cc:dd:ee:f0 actions=set_field:0xc8->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+		"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=aa:bb:cc:dd:ee:f0 actions=set_field:0xc8->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 		"cookie=0x1060000000000, table=Output, priority=210,reg1=0xc8,in_port=200 actions=IN_PORT",
 	}
 
diff --git a/pkg/agent/openflow/framework_test.go b/pkg/agent/openflow/framework_test.go
index b1d75de08..c4dae0f9f 100644
--- a/pkg/agent/openflow/framework_test.go
+++ b/pkg/agent/openflow/framework_test.go
@@ -77,6 +77,7 @@ func TestBuildPipeline(t *testing.T) {
 					SNATMarkTable,
 					SNATTable,
 					L2ForwardingCalcTable,
+					TrafficControlTable,
 					AntreaPolicyIngressRuleTable,
 					IngressRuleTable,
 					IngressDefaultTable,
@@ -124,6 +125,7 @@ func TestBuildPipeline(t *testing.T) {
 					SNATMarkTable,
 					SNATTable,
 					L2ForwardingCalcTable,
+					TrafficControlTable,
 					AntreaPolicyIngressRuleTable,
 					IngressRuleTable,
 					IngressDefaultTable,
@@ -158,6 +160,7 @@ func TestBuildPipeline(t *testing.T) {
 					L3ForwardingTable,
 					L3DecTTLTable,
 					L2ForwardingCalcTable,
+					TrafficControlTable,
 					AntreaPolicyIngressRuleTable,
 					IngressRuleTable,
 					IngressDefaultTable,
@@ -203,6 +206,7 @@ func TestBuildPipeline(t *testing.T) {
 					SNATMarkTable,
 					SNATTable,
 					L2ForwardingCalcTable,
+					TrafficControlTable,
 					AntreaPolicyIngressRuleTable,
 					IngressRuleTable,
 					IngressDefaultTable,
@@ -248,6 +252,7 @@ func TestBuildPipeline(t *testing.T) {
 					SNATMarkTable,
 					SNATTable,
 					L2ForwardingCalcTable,
+					TrafficControlTable,
 					AntreaPolicyIngressRuleTable,
 					IngressRuleTable,
 					IngressDefaultTable,
diff --git a/pkg/agent/openflow/network_policy_test.go b/pkg/agent/openflow/network_policy_test.go
index fbd1f66c2..6b27ba987 100644
--- a/pkg/agent/openflow/network_policy_test.go
+++ b/pkg/agent/openflow/network_policy_test.go
@@ -561,7 +561,7 @@ func TestBatchInstallPolicyRuleFlows(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			defer ctrl.Finish()
 			mockOperations := oftest.NewMockOFEntryOperations(ctrl)
-			ofClient := NewClient(bridgeName, bridgeMgmtAddr, false, true, false, false, false, false, false, false, false)
+			ofClient := NewClient(bridgeName, bridgeMgmtAddr, false, true, false, false, false, false, false, false, false, false)
 			c = ofClient.(*client)
 			c.cookieAllocator = cookie.NewAllocator(0)
 			c.ofEntryOperations = mockOperations
@@ -1391,7 +1391,24 @@ func Test_featureNetworkPolicy_initFlows(t *testing.T) {
 		expectedFlows []string
 	}{
 		{
-			name:          "K8s Node",
+			name:          "K8s Node with Multicast and L7NetworkPolicy",
+			nodeType:      config.K8sNode,
+			clientOptions: []clientOptionsFn{enableMulticast, enableL7NetworkPolicy},
+			expectedFlows: []string{
+				"cookie=0x1020000000000, table=Classifier, priority=200,in_port=11,vlan_tci=0x1000/0x1000 actions=pop_vlan,set_field:0x6/0xf->reg0,goto_table:L3Forwarding",
+				"cookie=0x1020000000000, table=IngressSecurityClassifier, priority=200,reg0=0x20/0xf0 actions=goto_table:IngressMetric",
+				"cookie=0x1020000000000, table=IngressSecurityClassifier, priority=200,reg0=0x10/0xf0 actions=goto_table:IngressMetric",
+				"cookie=0x1020000000000, table=IngressSecurityClassifier, priority=200,reg0=0x40/0xf0 actions=goto_table:IngressMetric",
+				"cookie=0x1020000000000, table=AntreaPolicyEgressRule, priority=64990,ct_state=-new+est,ip actions=goto_table:EgressMetric",
+				"cookie=0x1020000000000, table=AntreaPolicyEgressRule, priority=64990,ct_state=-new+rel,ip actions=goto_table:EgressMetric",
+				"cookie=0x1020000000000, table=TrafficControl, priority=210,reg0=0x106/0x10f actions=goto_table:Output",
+				"cookie=0x1020000000000, table=AntreaPolicyIngressRule, priority=64990,ct_state=-new+est,ip actions=goto_table:IngressMetric",
+				"cookie=0x1020000000000, table=AntreaPolicyIngressRule, priority=64990,ct_state=-new+rel,ip actions=goto_table:IngressMetric",
+				"cookie=0x1020000000000, table=Output, priority=211,ct_mark=0x80/0x80,reg0=0x100/0x100 actions=push_vlan:0x8100,move:NXM_NX_CT_LABEL[64..75]->OXM_OF_VLAN_VID[0..11],output:10",
+			},
+		},
+		{
+			name:          "K8s Node with Multicast",
 			nodeType:      config.K8sNode,
 			clientOptions: []clientOptionsFn{enableMulticast},
 			expectedFlows: []string{
@@ -1404,7 +1421,6 @@ func Test_featureNetworkPolicy_initFlows(t *testing.T) {
 				"cookie=0x1020000000000, table=AntreaPolicyIngressRule, priority=64990,ct_state=-new+rel,ip actions=goto_table:IngressMetric",
 			},
 		},
-
 		{
 			name:     "External Node",
 			nodeType: config.ExternalNode,
diff --git a/pkg/agent/openflow/pod_connectivity_test.go b/pkg/agent/openflow/pod_connectivity_test.go
index 5ad81e398..87c16e3c2 100644
--- a/pkg/agent/openflow/pod_connectivity_test.go
+++ b/pkg/agent/openflow/pod_connectivity_test.go
@@ -54,8 +54,8 @@ func Test_featurePodConnectivity_initFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=L3Forwarding, priority=0 actions=set_field:0x20/0xf0->reg0,goto_table:L2ForwardingCalc",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=210,ip,reg0=0x2/0xf actions=goto_table:SNATMark",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=200,ip actions=dec_ttl,goto_table:SNATMark",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=aa:bb:cc:dd:ee:ff actions=set_field:0x1->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=aa:bb:cc:dd:ee:ff actions=set_field:0x1->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1010000000000, table=IngressSecurityClassifier, priority=210,pkt_mark=0x80000000/0x80000000,ct_state=-rpl+trk,ip actions=goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=ConntrackCommit, priority=200,ct_state=+new+trk-snat,ct_mark=0x0/0x10,ip actions=ct(commit,table=Output,zone=65520,exec(move:NXM_NX_REG0[0..3]->NXM_NX_CT_MARK[0..3]))",
 				"cookie=0x1010000000000, table=Output, priority=200,reg0=0x100/0x100 actions=output:NXM_NX_REG1[]",
@@ -86,8 +86,8 @@ func Test_featurePodConnectivity_initFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=L3Forwarding, priority=0 actions=set_field:0x20/0xf0->reg0,goto_table:L2ForwardingCalc",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=210,ip,reg0=0x2/0xf actions=goto_table:SNATMark",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=200,ip actions=dec_ttl,goto_table:SNATMark",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=aa:bb:cc:dd:ee:ff actions=set_field:0x1->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=aa:bb:cc:dd:ee:ff actions=set_field:0x1->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1010000000000, table=IngressSecurityClassifier, priority=210,ct_state=-rpl+trk,ip,nw_src=10.10.0.1 actions=goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=ConntrackCommit, priority=200,ct_state=+new+trk-snat,ct_mark=0x0/0x10,ip actions=ct(commit,table=Output,zone=65520,exec(move:NXM_NX_REG0[0..3]->NXM_NX_CT_MARK[0..3]))",
 				"cookie=0x1010000000000, table=Output, priority=200,reg0=0x100/0x100 actions=output:NXM_NX_REG1[]",
@@ -122,12 +122,12 @@ func Test_featurePodConnectivity_initFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=L3Forwarding, priority=0 actions=set_field:0x20/0xf0->reg0,goto_table:L2ForwardingCalc",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=210,ip,reg0=0x2/0xf actions=goto_table:SNATMark",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=200,ip actions=dec_ttl,goto_table:SNATMark",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:02 actions=set_field:0xfffffffe->reg1,set_field:0x100/0x100->reg0,goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=190,reg4=0x100000/0x100000 actions=set_field:0x4->reg1,set_field:0x100/0x100->reg0,goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=IngressSecurityClassifier, priority=210,pkt_mark=0x80000000/0x80000000,ct_state=-rpl+trk,ip actions=goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=ConntrackCommit, priority=200,ct_state=+new+trk-snat,ct_mark=0x0/0x10,ip actions=ct(commit,table=VLAN,zone=NXM_NX_REG8[0..15],exec(move:NXM_NX_REG0[0..3]->NXM_NX_CT_MARK[0..3]))",
-				"cookie=0x1010000000000, table=VLAN, priority=190,in_port=4,dl_vlan=0 actions=pop_vlan,goto_table:Output",
+				"cookie=0x1010000000000, table=VLAN, priority=190,in_port=4,vlan_tci=0x1000/0x1000 actions=pop_vlan,goto_table:Output",
 				"cookie=0x1010000000000, table=Output, priority=210,ip,reg0=0x100/0x100,reg1=0xfffffffe actions=output:4294967294",
 				"cookie=0x1010000000000, table=Output, priority=200,reg0=0x100/0x100 actions=output:NXM_NX_REG1[]",
 			},
@@ -162,10 +162,10 @@ func Test_featurePodConnectivity_initFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=L3Forwarding, priority=0 actions=set_field:0x20/0xf0->reg0,goto_table:L2ForwardingCalc",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=210,ip,reg0=0x2/0xf actions=goto_table:SNATMark",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=200,ip actions=dec_ttl,goto_table:SNATMark",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1010000000000, table=IngressSecurityClassifier, priority=210,ct_state=-rpl+trk,ip,nw_src=10.10.0.1 actions=goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=ConntrackCommit, priority=200,ct_state=+new+trk-snat,ct_mark=0x0/0x10,ip actions=ct(commit,table=VLAN,zone=NXM_NX_REG8[0..15],exec(move:NXM_NX_REG0[0..3]->NXM_NX_CT_MARK[0..3]))",
-				"cookie=0x1010000000000, table=VLAN, priority=190,in_port=4,dl_vlan=0 actions=pop_vlan,goto_table:Output",
+				"cookie=0x1010000000000, table=VLAN, priority=190,in_port=4,vlan_tci=0x1000/0x1000 actions=pop_vlan,goto_table:Output",
 				"cookie=0x1010000000000, table=Output, priority=200,reg0=0x100/0x100 actions=output:NXM_NX_REG1[]",
 			},
 		},
@@ -189,7 +189,7 @@ func Test_featurePodConnectivity_initFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=L3Forwarding, priority=0 actions=set_field:0x20/0xf0->reg0,goto_table:L2ForwardingCalc",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=210,ip,reg0=0x2/0xf actions=goto_table:SNATMark",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=200,ip actions=dec_ttl,goto_table:SNATMark",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1010000000000, table=IngressSecurityClassifier, priority=210,pkt_mark=0x80000000/0x80000000,ct_state=-rpl+trk,ip actions=goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=ConntrackCommit, priority=200,ct_state=+new+trk-snat,ct_mark=0x0/0x10,ip actions=ct(commit,table=Output,zone=65520,exec(move:NXM_NX_REG0[0..3]->NXM_NX_CT_MARK[0..3]))",
 				"cookie=0x1010000000000, table=ConntrackState, priority=0 actions=goto_table:PreRoutingClassifier",
@@ -219,8 +219,8 @@ func Test_featurePodConnectivity_initFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=L3Forwarding, priority=0 actions=set_field:0x20/0xf0->reg0,goto_table:L2ForwardingCalc",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=210,ipv6,reg0=0x2/0xf actions=goto_table:SNATMark",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=200,ipv6 actions=dec_ttl,goto_table:SNATMark",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=aa:bb:cc:dd:ee:ff actions=set_field:0x1->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=aa:bb:cc:dd:ee:ff actions=set_field:0x1->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1010000000000, table=IngressSecurityClassifier, priority=210,pkt_mark=0x80000000/0x80000000,ct_state=-rpl+trk,ipv6 actions=goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=ConntrackCommit, priority=200,ct_state=+new+trk-snat,ct_mark=0x0/0x10,ipv6 actions=ct(commit,table=Output,zone=65510,exec(move:NXM_NX_REG0[0..3]->NXM_NX_CT_MARK[0..3]))",
 				"cookie=0x1010000000000, table=Output, priority=200,reg0=0x100/0x100 actions=output:NXM_NX_REG1[]",
@@ -248,7 +248,7 @@ func Test_featurePodConnectivity_initFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=L3Forwarding, priority=0 actions=set_field:0x20/0xf0->reg0,goto_table:L2ForwardingCalc",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=210,ipv6,reg0=0x2/0xf actions=goto_table:SNATMark",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=200,ipv6 actions=dec_ttl,goto_table:SNATMark",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1010000000000, table=IngressSecurityClassifier, priority=210,pkt_mark=0x80000000/0x80000000,ct_state=-rpl+trk,ipv6 actions=goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=ConntrackCommit, priority=200,ct_state=+new+trk-snat,ct_mark=0x0/0x10,ipv6 actions=ct(commit,table=Output,zone=65510,exec(move:NXM_NX_REG0[0..3]->NXM_NX_CT_MARK[0..3]))",
 				"cookie=0x1010000000000, table=Output, priority=200,reg0=0x100/0x100 actions=output:NXM_NX_REG1[]",
@@ -276,7 +276,7 @@ func Test_featurePodConnectivity_initFlows(t *testing.T) {
 				"cookie=0x1010000000000, table=L3Forwarding, priority=0 actions=set_field:0x20/0xf0->reg0,goto_table:L2ForwardingCalc",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=210,ipv6,reg0=0x2/0xf actions=goto_table:SNATMark",
 				"cookie=0x1010000000000, table=L3DecTTL, priority=200,ipv6 actions=dec_ttl,goto_table:SNATMark",
-				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:IngressSecurityClassifier",
+				"cookie=0x1010000000000, table=L2ForwardingCalc, priority=200,dl_dst=0a:00:00:00:00:01 actions=set_field:0x2->reg1,set_field:0x100/0x100->reg0,goto_table:TrafficControl",
 				"cookie=0x1010000000000, table=IngressSecurityClassifier, priority=210,pkt_mark=0x80000000/0x80000000,ct_state=-rpl+trk,ipv6 actions=goto_table:ConntrackCommit",
 				"cookie=0x1010000000000, table=ConntrackCommit, priority=200,ct_state=+new+trk-snat,ct_mark=0x0/0x10,ipv6 actions=ct(commit,table=Output,zone=65510,exec(move:NXM_NX_REG0[0..3]->NXM_NX_CT_MARK[0..3]))",
 				"cookie=0x1010000000000, table=Output, priority=200,reg0=0x100/0x100 actions=output:NXM_NX_REG1[]",
diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go
index 596c9c6da..e2c25c159 100644
--- a/pkg/agent/openflow/testing/mock_openflow.go
+++ b/pkg/agent/openflow/testing/mock_openflow.go
@@ -255,18 +255,18 @@ func (mr *MockClientMockRecorder) InitialTLVMap() *gomock.Call {
 }
 
 // Initialize mocks base method
-func (m *MockClient) Initialize(arg0 types.RoundInfo, arg1 *config.NodeConfig, arg2 *config.NetworkConfig, arg3 *config.EgressConfig, arg4 *config.ServiceConfig) (<-chan struct{}, error) {
+func (m *MockClient) Initialize(arg0 types.RoundInfo, arg1 *config.NodeConfig, arg2 *config.NetworkConfig, arg3 *config.EgressConfig, arg4 *config.ServiceConfig, arg5 *config.L7NetworkPolicyConfig) (<-chan struct{}, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Initialize", arg0, arg1, arg2, arg3, arg4)
+	ret := m.ctrl.Call(m, "Initialize", arg0, arg1, arg2, arg3, arg4, arg5)
 	ret0, _ := ret[0].(<-chan struct{})
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // Initialize indicates an expected call of Initialize
-func (mr *MockClientMockRecorder) Initialize(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+func (mr *MockClientMockRecorder) Initialize(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Initialize", reflect.TypeOf((*MockClient)(nil).Initialize), arg0, arg1, arg2, arg3, arg4)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Initialize", reflect.TypeOf((*MockClient)(nil).Initialize), arg0, arg1, arg2, arg3, arg4, arg5)
 }
 
 // InstallEndpointFlows mocks base method
diff --git a/pkg/controller/networkpolicy/validate_test.go b/pkg/controller/networkpolicy/validate_test.go
index 8932c264c..dcddd52f2 100644
--- a/pkg/controller/networkpolicy/validate_test.go
+++ b/pkg/controller/networkpolicy/validate_test.go
@@ -21,10 +21,13 @@ import (
 	admv1 "k8s.io/api/admission/v1"
 	authenticationv1 "k8s.io/api/authentication/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/component-base/featuregate"
+	featuregatetesting "k8s.io/component-base/featuregate/testing"
 
 	crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1"
 	crdv1alpha2 "antrea.io/antrea/pkg/apis/crd/v1alpha2"
 	crdv1alpha3 "antrea.io/antrea/pkg/apis/crd/v1alpha3"
+	"antrea.io/antrea/pkg/features"
 )
 
 func TestValidateAntreaPolicy(t *testing.T) {
@@ -34,6 +37,7 @@ func TestValidateAntreaPolicy(t *testing.T) {
 
 	tests := []struct {
 		name           string
+		featureGates   map[featuregate.Feature]bool
 		policy         *crdv1alpha1.ClusterNetworkPolicy
 		expectedReason string
 	}{
@@ -1225,7 +1229,8 @@ func TestValidateAntreaPolicy(t *testing.T) {
 			expectedReason: "",
 		},
 		{
-			name: "acnp-l7protocols-used-with-allow",
+			name:         "acnp-l7protocols-used-with-allow",
+			featureGates: map[featuregate.Feature]bool{features.L7NetworkPolicy: true},
 			policy: &crdv1alpha1.ClusterNetworkPolicy{
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "ingress-rule-l7protocols",
@@ -1258,7 +1263,8 @@ func TestValidateAntreaPolicy(t *testing.T) {
 			expectedReason: "",
 		},
 		{
-			name: "acnp-l7protocols-used-with-pass",
+			name:         "acnp-l7protocols-used-with-pass",
+			featureGates: map[featuregate.Feature]bool{features.L7NetworkPolicy: true},
 			policy: &crdv1alpha1.ClusterNetworkPolicy{
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "ingress-rule-l7protocols",
@@ -1290,7 +1296,8 @@ func TestValidateAntreaPolicy(t *testing.T) {
 			expectedReason: "layer 7 protocols only support Allow",
 		},
 		{
-			name: "acnp-l7protocols-HTTP-used-with-UDP",
+			name:         "acnp-l7protocols-HTTP-used-with-UDP",
+			featureGates: map[featuregate.Feature]bool{features.L7NetworkPolicy: true},
 			policy: &crdv1alpha1.ClusterNetworkPolicy{
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "ingress-rule-l7protocols",
@@ -1327,7 +1334,8 @@ func TestValidateAntreaPolicy(t *testing.T) {
 			expectedReason: "HTTP protocol can only be used when layer 4 protocol is TCP or unset",
 		},
 		{
-			name: "acnp-l7protocols-HTTP-used-with-ICMP",
+			name:         "acnp-l7protocols-HTTP-used-with-ICMP",
+			featureGates: map[featuregate.Feature]bool{features.L7NetworkPolicy: true},
 			policy: &crdv1alpha1.ClusterNetworkPolicy{
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "ingress-rule-l7protocols",
@@ -1364,7 +1372,8 @@ func TestValidateAntreaPolicy(t *testing.T) {
 			expectedReason: "HTTP protocol can not be used with protocol IGMP or ICMP",
 		},
 		{
-			name: "acnp-l7protocols-used-with-toService",
+			name:         "acnp-l7protocols-used-with-toService",
+			featureGates: map[featuregate.Feature]bool{features.L7NetworkPolicy: true},
 			policy: &crdv1alpha1.ClusterNetworkPolicy{
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "egress-rule-l7protocols",
@@ -1400,9 +1409,40 @@ func TestValidateAntreaPolicy(t *testing.T) {
 			},
 			expectedReason: "layer 7 protocols can not be used with toServices",
 		},
+		{
+			name:         "L7NetworkPolicy-disabled",
+			featureGates: map[featuregate.Feature]bool{features.L7NetworkPolicy: false},
+			policy: &crdv1alpha1.ClusterNetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "egress-rule-l7protocols",
+				},
+				Spec: crdv1alpha1.ClusterNetworkPolicySpec{
+					AppliedTo: []crdv1alpha1.AppliedTo{
+						{
+							NamespaceSelector: &metav1.LabelSelector{},
+						},
+					},
+					Egress: []crdv1alpha1.Rule{
+						{
+							Action: &allowAction,
+							L7Protocols: []crdv1alpha1.L7Protocol{
+								{
+									HTTP: nil,
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedReason: "layer 7 protocols can only be used when L7NetworkPolicy is enabled",
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			for feature, value := range tt.featureGates {
+				defer featuregatetesting.SetFeatureGateDuringTest(t, features.DefaultFeatureGate, feature, value)()
+			}
+
 			_, c := newController()
 			v := NewNetworkPolicyValidator(c.NetworkPolicyController)
 			actualReason, allowed := v.validateAntreaPolicy(tt.policy, nil, admv1.Create, authenticationv1.UserInfo{})
diff --git a/pkg/flowaggregator/flowaggregator_test.go b/pkg/flowaggregator/flowaggregator_test.go
index 802b43518..30ad2f817 100644
--- a/pkg/flowaggregator/flowaggregator_test.go
+++ b/pkg/flowaggregator/flowaggregator_test.go
@@ -153,7 +153,7 @@ func TestFlowAggregator_sendFlowKeyRecord(t *testing.T) {
 		mockRecord.EXPECT().GetInfoElementWithValue("sourcePodName").Return(sourcePodNameElem, 0, false)
 		destPodNameElem, _ := ipfixentities.DecodeAndCreateInfoElementWithValue(ipfixentities.NewInfoElement("destinationPodName", 0, 0, ipfixregistry.AntreaEnterpriseID, 0), emptyStr)
 		mockRecord.EXPECT().GetInfoElementWithValue("destinationPodName").Return(destPodNameElem, 0, false)
-		mockAggregationProcess.EXPECT().SetCorrelatedFieldsFilled(tc.flowRecord)
+		mockAggregationProcess.EXPECT().SetCorrelatedFieldsFilled(tc.flowRecord, true)
 		if tc.includePodLabels {
 			mockAggregationProcess.EXPECT().AreExternalFieldsFilled(*tc.flowRecord).Return(false)
 			sourcePodLabelsElement := ipfixentities.NewInfoElement("sourcePodLabels", 0, 0, ipfixregistry.AntreaEnterpriseID, 0)
@@ -164,7 +164,7 @@ func TestFlowAggregator_sendFlowKeyRecord(t *testing.T) {
 			mockIPFIXRegistry.EXPECT().GetInfoElement("destinationPodLabels", ipfixregistry.AntreaEnterpriseID).Return(ipfixentities.NewInfoElement("destinationPodLabels", 0, 0, ipfixregistry.AntreaEnterpriseID, 0), nil)
 			destinationPodLabelsIE, _ := ipfixentities.DecodeAndCreateInfoElementWithValue(destinationPodLabelsElement, bytes.NewBufferString("").Bytes())
 			mockRecord.EXPECT().AddInfoElement(destinationPodLabelsIE).Return(nil)
-			mockAggregationProcess.EXPECT().SetExternalFieldsFilled(tc.flowRecord)
+			mockAggregationProcess.EXPECT().SetExternalFieldsFilled(tc.flowRecord, true)
 		}
 		mockAggregationProcess.EXPECT().IsAggregatedRecordIPv4(*tc.flowRecord).Return(!tc.isIPv6)
 
diff --git a/pkg/ipfix/testing/mock_ipfix.go b/pkg/ipfix/testing/mock_ipfix.go
index b7d2d01fb..0b1b668ad 100644
--- a/pkg/ipfix/testing/mock_ipfix.go
+++ b/pkg/ipfix/testing/mock_ipfix.go
@@ -366,27 +366,27 @@ func (mr *MockIPFIXAggregationProcessMockRecorder) ResetStatAndThroughputElement
 }
 
 // SetCorrelatedFieldsFilled mocks base method
-func (m *MockIPFIXAggregationProcess) SetCorrelatedFieldsFilled(arg0 *intermediate.AggregationFlowRecord) {
+func (m *MockIPFIXAggregationProcess) SetCorrelatedFieldsFilled(arg0 *intermediate.AggregationFlowRecord, arg1 bool) {
 	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SetCorrelatedFieldsFilled", arg0)
+	m.ctrl.Call(m, "SetCorrelatedFieldsFilled", arg0, arg1)
 }
 
 // SetCorrelatedFieldsFilled indicates an expected call of SetCorrelatedFieldsFilled
-func (mr *MockIPFIXAggregationProcessMockRecorder) SetCorrelatedFieldsFilled(arg0 interface{}) *gomock.Call {
+func (mr *MockIPFIXAggregationProcessMockRecorder) SetCorrelatedFieldsFilled(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCorrelatedFieldsFilled", reflect.TypeOf((*MockIPFIXAggregationProcess)(nil).SetCorrelatedFieldsFilled), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCorrelatedFieldsFilled", reflect.TypeOf((*MockIPFIXAggregationProcess)(nil).SetCorrelatedFieldsFilled), arg0, arg1)
 }
 
 // SetExternalFieldsFilled mocks base method
-func (m *MockIPFIXAggregationProcess) SetExternalFieldsFilled(arg0 *intermediate.AggregationFlowRecord) {
+func (m *MockIPFIXAggregationProcess) SetExternalFieldsFilled(arg0 *intermediate.AggregationFlowRecord, arg1 bool) {
 	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SetExternalFieldsFilled", arg0)
+	m.ctrl.Call(m, "SetExternalFieldsFilled", arg0, arg1)
 }
 
 // SetExternalFieldsFilled indicates an expected call of SetExternalFieldsFilled
-func (mr *MockIPFIXAggregationProcessMockRecorder) SetExternalFieldsFilled(arg0 interface{}) *gomock.Call {
+func (mr *MockIPFIXAggregationProcessMockRecorder) SetExternalFieldsFilled(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetExternalFieldsFilled", reflect.TypeOf((*MockIPFIXAggregationProcess)(nil).SetExternalFieldsFilled), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetExternalFieldsFilled", reflect.TypeOf((*MockIPFIXAggregationProcess)(nil).SetExternalFieldsFilled), arg0, arg1)
 }
 
 // Start mocks base method
diff --git a/pkg/ovs/openflow/testing/utils.go b/pkg/ovs/openflow/testing/utils.go
index 3aeb2d6b6..373f4504a 100644
--- a/pkg/ovs/openflow/testing/utils.go
+++ b/pkg/ovs/openflow/testing/utils.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 
 	"antrea.io/libOpenflow/openflow15"
+	"antrea.io/libOpenflow/protocol"
 	"antrea.io/libOpenflow/util"
 	"antrea.io/ofnet/ofctrl"
 	"k8s.io/klog/v2"
@@ -353,7 +354,15 @@ func matchInPortToString(field *openflow15.MatchField) string {
 }
 
 func matchVlanToString(field *openflow15.MatchField) string {
-	return fmt.Sprintf("dl_vlan=%d", field.Value.(*openflow15.VlanIdField).VlanId&0xfff)
+	value := field.Value.(*openflow15.VlanIdField).VlanId
+	mask := uint16(openflow15.OFPVID_PRESENT)
+	if field.HasMask {
+		mask |= field.Mask.(*openflow15.VlanIdField).VlanId
+	}
+	if mask == (protocol.VID_MASK | openflow15.OFPVID_PRESENT) {
+		return fmt.Sprintf("dl_vlan=%d", value&protocol.VID_MASK)
+	}
+	return fmt.Sprintf("vlan_tci=0x%04x/0x%04x", value&openflow15.OFPVID_PRESENT, mask&openflow15.OFPVID_PRESENT)
 }
 
 func matchIpAddrToString(field *openflow15.MatchField, isCt, isSrc, isIPv6 bool) string {
@@ -956,8 +965,6 @@ func getFlowModMatch(flowMod *openflow15.FlowMod) string {
 		parts = append(parts, matchInPortToString(field))
 	}
 
-	// TODO: other match conditions about VLAN
-
 	// dl_vlan
 	if field, ok := matchMap["dl_vlan"]; ok {
 		parts = append(parts, matchVlanToString(field))
diff --git a/test/e2e/antreapolicy_test.go b/test/e2e/antreapolicy_test.go
index 02713c09e..28855576d 100644
--- a/test/e2e/antreapolicy_test.go
+++ b/test/e2e/antreapolicy_test.go
@@ -252,7 +252,7 @@ func testMutateANPNoRuleName(t *testing.T) {
 	builder = builder.SetName(namespaces["x"], "anp-no-rule-name").
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}}).
 		SetPriority(10.0).
-		AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+		AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 			nil, nil, nil, nil, crdv1alpha1.RuleActionAllow, "", "")
 	anp := builder.Get()
 	log.Debugf("creating ANP %v", anp.Name)
@@ -296,7 +296,7 @@ func testInvalidANPIngressPeerGroupSetWithPodSelector(t *testing.T) {
 	builder := &AntreaNetworkPolicySpecBuilder{}
 	builder = builder.SetName(namespace, "anp-ingress-group-podselector-set").
 		SetPriority(1.0)
-	builder = builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, nil, nil,
+	builder = builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, nil, nil,
 		nil, nil, nil, []ANPAppliedToSpec{ruleAppTo}, crdv1alpha1.RuleActionAllow, gA, "")
 	anp := builder.Get()
 	log.Debugf("creating ANP %v", anp.Name)
@@ -318,7 +318,7 @@ func testInvalidANPIngressPeerGroupSetWithIPBlock(t *testing.T) {
 	builder = builder.SetName(namespace, "anp-ingress-group-ipblock-set").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{Group: "gA"}})
-	builder = builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, &cidr, map[string]string{"pod": "b"}, map[string]string{"ns": "x"}, nil,
+	builder = builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, &cidr, map[string]string{"pod": "b"}, map[string]string{"ns": "x"}, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionAllow, gA, "")
 	anp := builder.Get()
 	log.Debugf("creating ANP %v", anp.Name)
@@ -347,9 +347,9 @@ func testInvalidANPRuleNameNotUnique(t *testing.T) {
 	builder := &AntreaNetworkPolicySpecBuilder{}
 	builder = builder.SetName(namespaces["x"], "anp-rule-name-not-unique").
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}}).
-		AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+		AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 			nil, nil, nil, nil, crdv1alpha1.RuleActionAllow, "", "not-unique").
-		AddIngress(ProtocolTCP, &p81, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": namespaces["x"]}, nil,
+		AddIngress(ProtocolTCP, &p81, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": namespaces["x"]}, nil,
 			nil, nil, nil, nil, crdv1alpha1.RuleActionAllow, "", "not-unique")
 	anp := builder.Get()
 	log.Debugf("creating ANP %v", anp.Name)
@@ -379,7 +379,7 @@ func testInvalidANPPortRangePortUnset(t *testing.T) {
 	builder = builder.SetName(namespaces["y"], "anp-egress-port-range-port-unset").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder.AddEgress(ProtocolTCP, nil, nil, &p8085, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": namespaces["x"]}, nil,
+	builder.AddEgress(ProtocolTCP, nil, nil, &p8085, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": namespaces["x"]}, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, "", "anp-port-range")
 
 	anp := builder.Get()
@@ -396,7 +396,7 @@ func testInvalidANPPortRangeEndPortSmall(t *testing.T) {
 	builder = builder.SetName(namespaces["y"], "anp-egress-port-range-endport-small").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder.AddEgress(ProtocolTCP, &p8082, nil, &p8081, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": namespaces["x"]}, nil,
+	builder.AddEgress(ProtocolTCP, &p8082, nil, &p8081, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": namespaces["x"]}, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, "", "anp-port-range")
 
 	anp := builder.Get()
@@ -1216,7 +1216,7 @@ func testANPEgressRulePodsAToGrpWithPodsC(t *testing.T) {
 	builder = builder.SetName(namespaces["x"], "anp-deny-xa-to-grp-xc-egress").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}})
-	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, grpName, "")
 
 	reachability := NewReachability(allPods, Connected)
@@ -1249,7 +1249,7 @@ func testANPIngressRuleDenyGrpWithXCtoXA(t *testing.T) {
 	builder = builder.SetName(namespaces["x"], "anp-deny-grp-with-xb-to-xa").
 		SetPriority(2.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}})
-	builder.AddIngress(ProtocolTCP, nil, &port81Name, nil, nil, nil, nil, nil, nil,
+	builder.AddIngress(ProtocolTCP, nil, &port81Name, nil, nil, nil, nil, nil, nil, nil,
 		nil, nil, nil, nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, grpName, "")
 
 	reachability := NewReachability(allPods, Connected)
@@ -1284,7 +1284,7 @@ func testANPGroupUpdate(t *testing.T) {
 	builder = builder.SetName(namespaces["x"], "anp-deny-xa-to-grp-with-xc-egress").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}})
-	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, grpName, "")
 
 	reachability := NewReachability(allPods, Connected)
@@ -1328,7 +1328,7 @@ func testANPAppliedToDenyXBtoGrpWithXA(t *testing.T) {
 	builder = builder.SetName(namespaces["x"], "anp-deny-grp-with-xa-from-xb").
 		SetPriority(2.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{Group: grpName}})
-	builder.AddIngress(ProtocolTCP, nil, &port81Name, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, nil, nil,
+	builder.AddIngress(ProtocolTCP, nil, &port81Name, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, nil, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, "", "")
 
 	reachability := NewReachability(allPods, Connected)
@@ -1361,7 +1361,7 @@ func testANPAppliedToRuleGrpWithPodsAToPodsC(t *testing.T) {
 	builder := &AntreaNetworkPolicySpecBuilder{}
 	builder = builder.SetName(namespaces["x"], "anp-deny-grp-with-a-to-c").
 		SetPriority(1.0)
-	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, nil, nil,
+	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, nil, nil,
 		nil, nil, nil, []ANPAppliedToSpec{{Group: grpName}}, crdv1alpha1.RuleActionDrop, "", "")
 
 	reachability := NewReachability(allPods, Connected)
@@ -1395,7 +1395,7 @@ func testANPGroupUpdateAppliedTo(t *testing.T) {
 	builder = builder.SetName(namespaces["x"], "anp-deny-grp-xc-to-xa-egress").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{Group: grpName}})
-	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, nil, nil,
+	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, nil, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, "", "")
 
 	reachability := NewReachability(allPods, Connected)
@@ -1437,7 +1437,7 @@ func testANPGroupAppliedToPodAdd(t *testing.T, data *TestData) {
 	builder = builder.SetName(namespaces["x"], "anp-deny-grp-with-xj-to-xd-egress").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{Group: grpName}})
-	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "d"}, nil, nil,
+	builder.AddEgress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "d"}, nil, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, "", "")
 	cp := []*CustomProbe{
 		{
@@ -1482,7 +1482,7 @@ func testANPGroupServiceRefPodAdd(t *testing.T, data *TestData) {
 
 	builder := &AntreaNetworkPolicySpecBuilder{}
 	builder = builder.SetName(namespaces["x"], "anp-grp-svc-ref").SetPriority(1.0).SetAppliedToGroup([]ANPAppliedToSpec{{Group: grp1Name}})
-	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, grp2Name, "")
 
 	svc1PodName := randName("test-pod-svc1-")
@@ -1543,7 +1543,7 @@ func testANPGroupServiceRefDelete(t *testing.T) {
 
 	builder := &AntreaNetworkPolicySpecBuilder{}
 	builder = builder.SetName(namespaces["x"], "anp-grp-svc-ref").SetPriority(1.0).SetAppliedToGroup([]ANPAppliedToSpec{{Group: grp1Name}})
-	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, grp2Name, "")
 	anp := builder.Get()
 	k8sUtils.CreateOrUpdateANP(anp)
@@ -1584,7 +1584,7 @@ func testANPGroupServiceRefCreateAndUpdate(t *testing.T) {
 
 	builder := &AntreaNetworkPolicySpecBuilder{}
 	builder = builder.SetName(namespaces["x"], "anp-grp-svc-ref").SetPriority(1.0).SetAppliedToGroup([]ANPAppliedToSpec{{Group: grp1Name}})
-	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, grp2Name, "")
 
 	// Pods backing svc1 (label pod=a) in Namespace x should not allow ingress from Pods backing svc2 (label pod=b) in Namespace x.
@@ -1653,7 +1653,7 @@ func testANPGroupRefRuleIPBlocks(t *testing.T) {
 	builder = builder.SetName(namespaces["x"], "anp-deny-xb-xc-ips-ingress-for-xa").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}})
-	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, grpName, "")
 
 	reachability := NewReachability(allPods, Connected)
@@ -1693,7 +1693,7 @@ func testANPNestedGroupCreateAndUpdate(t *testing.T, data *TestData) {
 	builder := &AntreaNetworkPolicySpecBuilder{}
 	builder = builder.SetName(namespaces["x"], "anp-nested-grp").SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{}}).
-		AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+		AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
 			nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, grpNestedName, "")
 
 	// Pods in Namespace x should not allow traffic from Pods backing svc1 (label pod=a) in Namespace x.
@@ -2416,7 +2416,7 @@ func testANPPortRange(t *testing.T) {
 	builder = builder.SetName(namespaces["y"], "anp-deny-yb-to-xc-egress-port-range").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "b"}}})
-	builder.AddEgress(ProtocolTCP, &p8080, nil, &p8082, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": namespaces["x"]}, nil,
+	builder.AddEgress(ProtocolTCP, &p8080, nil, &p8082, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "c"}, map[string]string{"ns": namespaces["x"]}, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, "", "anp-port-range")
 
 	reachability := NewReachability(allPods, Connected)
@@ -2446,7 +2446,7 @@ func testANPBasic(t *testing.T) {
 	builder = builder.SetName(namespaces["y"], "np-same-name").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}})
-	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, "", "")
 
 	reachability := NewReachability(allPods, Connected)
@@ -2497,12 +2497,12 @@ func testANPMultipleAppliedTo(t *testing.T, data *TestData, singleRule bool) {
 	// See https://github.com/antrea-io/antrea/issues/2083.
 	if singleRule {
 		builder.SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}, {PodSelector: map[string]string{tempLabel: ""}}})
-		builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+		builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 			nil, nil, nil, nil, crdv1alpha1.RuleActionDrop, "", "")
 	} else {
-		builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+		builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 			nil, nil, nil, []ANPAppliedToSpec{{PodSelector: map[string]string{"pod": "a"}}}, crdv1alpha1.RuleActionDrop, "", "")
-		builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+		builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 			nil, nil, nil, []ANPAppliedToSpec{{PodSelector: map[string]string{tempLabel: ""}}}, crdv1alpha1.RuleActionDrop, "", "")
 	}
 
@@ -2744,9 +2744,9 @@ func testAppliedToPerRule(t *testing.T) {
 	builder = builder.SetName(namespaces["y"], "np1").SetPriority(1.0)
 	anpATGrp1 := ANPAppliedToSpec{PodSelector: map[string]string{"pod": "a"}, PodSelectorMatchExp: nil}
 	anpATGrp2 := ANPAppliedToSpec{PodSelector: map[string]string{"pod": "b"}, PodSelectorMatchExp: nil}
-	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 		nil, nil, nil, []ANPAppliedToSpec{anpATGrp1}, crdv1alpha1.RuleActionDrop, "", "")
-	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["z"]}, nil,
+	builder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["z"]}, nil,
 		nil, nil, nil, []ANPAppliedToSpec{anpATGrp2}, crdv1alpha1.RuleActionDrop, "", "")
 
 	reachability := NewReachability(allPods, Connected)
@@ -4331,7 +4331,7 @@ func TestAntreaPolicyStatus(t *testing.T) {
 	anpBuilder = anpBuilder.SetName(data.testNamespace, "anp-applied-to-two-nodes").
 		SetPriority(1.0).
 		SetAppliedToGroup([]ANPAppliedToSpec{{PodSelector: map[string]string{"app": "nginx"}}})
-	anpBuilder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+	anpBuilder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 		nil, nil, nil, nil, crdv1alpha1.RuleActionAllow, "", "")
 	anp := anpBuilder.Get()
 	log.Debugf("creating ANP %v", anp.Name)
@@ -4380,9 +4380,9 @@ func TestAntreaPolicyStatusWithAppliedToPerRule(t *testing.T) {
 	anpBuilder := &AntreaNetworkPolicySpecBuilder{}
 	anpBuilder = anpBuilder.SetName(data.testNamespace, "anp-applied-to-per-rule").
 		SetPriority(1.0)
-	anpBuilder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+	anpBuilder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 		nil, nil, nil, []ANPAppliedToSpec{{PodSelector: map[string]string{"antrea-e2e": server0Name}}}, crdv1alpha1.RuleActionAllow, "", "")
-	anpBuilder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
+	anpBuilder.AddIngress(ProtocolTCP, &p80, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"pod": "b"}, map[string]string{"ns": namespaces["x"]}, nil,
 		nil, nil, nil, []ANPAppliedToSpec{{PodSelector: map[string]string{"antrea-e2e": server1Name}}}, crdv1alpha1.RuleActionAllow, "", "")
 	anp := anpBuilder.Get()
 	log.Debugf("creating ANP %v", anp.Name)
diff --git a/test/e2e/flowaggregator_test.go b/test/e2e/flowaggregator_test.go
index e111e1569..0c26d489a 100644
--- a/test/e2e/flowaggregator_test.go
+++ b/test/e2e/flowaggregator_test.go
@@ -1167,7 +1167,7 @@ func deployAntreaNetworkPolicies(t *testing.T, data *TestData, srcPod, dstPod st
 	builder1 = builder1.SetName(data.testNamespace, ingressAntreaNetworkPolicyName).
 		SetPriority(2.0).
 		SetAppliedToGroup([]utils.ANPAppliedToSpec{{PodSelector: map[string]string{"antrea-e2e": dstPod}}})
-	builder1 = builder1.AddIngress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": srcPod}, map[string]string{}, nil,
+	builder1 = builder1.AddIngress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": srcPod}, map[string]string{}, nil,
 		nil, nil, nil, nil, secv1alpha1.RuleActionAllow, "", testIngressRuleName)
 	anp1 = builder1.Get()
 	anp1, err1 := k8sUtils.CreateOrUpdateANP(anp1)
@@ -1180,7 +1180,7 @@ func deployAntreaNetworkPolicies(t *testing.T, data *TestData, srcPod, dstPod st
 	builder2 = builder2.SetName(data.testNamespace, egressAntreaNetworkPolicyName).
 		SetPriority(2.0).
 		SetAppliedToGroup([]utils.ANPAppliedToSpec{{PodSelector: map[string]string{"antrea-e2e": srcPod}}})
-	builder2 = builder2.AddEgress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": dstPod}, map[string]string{}, nil,
+	builder2 = builder2.AddEgress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": dstPod}, map[string]string{}, nil,
 		nil, nil, nil, nil, secv1alpha1.RuleActionAllow, "", testEgressRuleName)
 	anp2 = builder2.Get()
 	anp2, err2 := k8sUtils.CreateOrUpdateANP(anp2)
@@ -1211,12 +1211,12 @@ func deployDenyAntreaNetworkPolicies(t *testing.T, data *TestData, srcPod, podRe
 		builder1 = builder1.SetName(data.testNamespace, ingressRejectANPName).
 			SetPriority(2.0).
 			SetAppliedToGroup([]utils.ANPAppliedToSpec{{PodSelector: map[string]string{"antrea-e2e": podReject}}})
-		builder1 = builder1.AddIngress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": srcPod}, map[string]string{}, nil,
+		builder1 = builder1.AddIngress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": srcPod}, map[string]string{}, nil,
 			nil, nil, nil, nil, secv1alpha1.RuleActionReject, "", testIngressRuleName)
 		builder2 = builder2.SetName(data.testNamespace, ingressDropANPName).
 			SetPriority(2.0).
 			SetAppliedToGroup([]utils.ANPAppliedToSpec{{PodSelector: map[string]string{"antrea-e2e": podDrop}}})
-		builder2 = builder2.AddIngress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": srcPod}, map[string]string{}, nil,
+		builder2 = builder2.AddIngress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": srcPod}, map[string]string{}, nil,
 			nil, nil, nil, nil, secv1alpha1.RuleActionDrop, "", testIngressRuleName)
 		table = openflow.AntreaPolicyIngressRuleTable
 		flowCount = antreaIngressTableInitFlowCount + 2
@@ -1226,12 +1226,12 @@ func deployDenyAntreaNetworkPolicies(t *testing.T, data *TestData, srcPod, podRe
 		builder1 = builder1.SetName(data.testNamespace, egressRejectANPName).
 			SetPriority(2.0).
 			SetAppliedToGroup([]utils.ANPAppliedToSpec{{PodSelector: map[string]string{"antrea-e2e": srcPod}}})
-		builder1 = builder1.AddEgress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": podReject}, map[string]string{}, nil,
+		builder1 = builder1.AddEgress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": podReject}, map[string]string{}, nil,
 			nil, nil, nil, nil, secv1alpha1.RuleActionReject, "", testEgressRuleName)
 		builder2 = builder2.SetName(data.testNamespace, egressDropANPName).
 			SetPriority(2.0).
 			SetAppliedToGroup([]utils.ANPAppliedToSpec{{PodSelector: map[string]string{"antrea-e2e": srcPod}}})
-		builder2 = builder2.AddEgress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": podDrop}, map[string]string{}, nil,
+		builder2 = builder2.AddEgress(utils.ProtocolTCP, nil, nil, nil, nil, nil, nil, nil, nil, nil, map[string]string{"antrea-e2e": podDrop}, map[string]string{}, nil,
 			nil, nil, nil, nil, secv1alpha1.RuleActionDrop, "", testEgressRuleName)
 		table = openflow.AntreaPolicyEgressRuleTable
 		flowCount = antreaEgressTableInitFlowCount + 2
diff --git a/test/e2e/l7networkpolicy_test.go b/test/e2e/l7networkpolicy_test.go
new file mode 100644
index 000000000..63fe993bd
--- /dev/null
+++ b/test/e2e/l7networkpolicy_test.go
@@ -0,0 +1,255 @@
+// Copyright 2022 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 e2e
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1"
+	agentconfig "antrea.io/antrea/pkg/config/agent"
+	"antrea.io/antrea/pkg/features"
+	. "antrea.io/antrea/test/e2e/utils"
+)
+
+func TestL7NetworkPolicy(t *testing.T) {
+	skipIfHasWindowsNodes(t)
+	skipIfFeatureDisabled(t, features.L7NetworkPolicy, true, true)
+
+	data, err := setupTest(t)
+	if err != nil {
+		t.Fatalf("Error when setting up test: %v", err)
+	}
+	defer teardownTest(t, data)
+
+	ac := func(config *agentconfig.AgentConfig) { config.DisableTXChecksumOffload = true }
+	if err = data.mutateAntreaConfigMap(nil, ac, false, true); err != nil {
+		t.Fatalf("Failed to enable option DisableTXChecksumOffload: %v", err)
+	}
+	defer func() {
+		ac = func(config *agentconfig.AgentConfig) { config.DisableTXChecksumOffload = false }
+		if err = data.mutateAntreaConfigMap(nil, ac, false, true); err != nil {
+			t.Fatalf("Failed to disable option DisableTXChecksumOffload: %v", err)
+		}
+	}()
+
+	t.Run("HTTP", func(t *testing.T) {
+		testL7NetworkPolicyHTTP(t, data)
+	})
+}
+
+func createL7NetworkPolicy(t *testing.T,
+	data *TestData,
+	isIngress bool,
+	name string,
+	priority float64,
+	podSelector,
+	appliedToPodSelector map[string]string,
+	l4Protocol AntreaPolicyProtocol,
+	port int32,
+	l7Protocols []crdv1alpha1.L7Protocol) {
+	anpBuilder := &AntreaNetworkPolicySpecBuilder{}
+	anpBuilder = anpBuilder.SetName(data.testNamespace, name).SetPriority(priority)
+	if isIngress {
+		anpBuilder.AddIngress(l4Protocol,
+			&port,
+			nil,
+			nil,
+			nil,
+			nil,
+			nil,
+			l7Protocols,
+			nil,
+			nil,
+			podSelector,
+			nil,
+			nil,
+			nil,
+			nil,
+			nil,
+			[]ANPAppliedToSpec{{PodSelector: appliedToPodSelector}},
+			crdv1alpha1.RuleActionAllow,
+			"",
+			"")
+	} else {
+		anpBuilder.AddEgress(l4Protocol,
+			&port,
+			nil,
+			nil,
+			nil,
+			nil,
+			nil,
+			l7Protocols,
+			nil,
+			nil,
+			podSelector,
+			nil,
+			nil,
+			nil,
+			nil,
+			nil,
+			[]ANPAppliedToSpec{{PodSelector: appliedToPodSelector}},
+			crdv1alpha1.RuleActionAllow,
+			"",
+			"")
+	}
+
+	anp := anpBuilder.Get()
+	t.Logf("Creating ANP %v", anp.Name)
+	_, err := data.crdClient.CrdV1alpha1().NetworkPolicies(data.testNamespace).Create(context.TODO(), anp, metav1.CreateOptions{})
+	assert.NoError(t, err)
+}
+
+func testL7NetworkPolicyHTTP(t *testing.T, data *TestData) {
+	clientPodName := "test-l7-http-client-selected"
+	clientPodLabels := map[string]string{"test-l7-http-e2e": "client"}
+	cmd := []string{"bash", "-c", "sleep 3600"}
+
+	// Create a client Pod which will be selected by test L7 NetworkPolices.
+	require.NoError(t, NewPodBuilder(clientPodName, data.testNamespace, agnhostImage).OnNode(nodeName(0)).WithCommand(cmd).WithLabels(clientPodLabels).Create(data))
+	if _, err := data.podWaitForIPs(defaultTimeout, clientPodName, data.testNamespace); err != nil {
+		t.Fatalf("Error when waiting for IP for Pod '%s': %v", clientPodName, err)
+	}
+	require.NoError(t, data.podWaitForRunning(defaultTimeout, clientPodName, data.testNamespace))
+
+	serverPodName := "test-l7-http-server"
+	serverPodLabels := map[string]string{"test-l7-http-e2e": "server"}
+	cmd = []string{"bash", "-c", "/agnhost netexec --http-port=8080"}
+	require.NoError(t, NewPodBuilder(serverPodName, data.testNamespace, agnhostImage).OnNode(nodeName(0)).WithCommand(cmd).WithLabels(serverPodLabels).Create(data))
+	podIPs, err := data.podWaitForIPs(defaultTimeout, serverPodName, data.testNamespace)
+	if err != nil {
+		t.Fatalf("Error when waiting for IP for Pod '%s': %v", serverPodName, err)
+	}
+	require.NoError(t, data.podWaitForRunning(defaultTimeout, serverPodName, data.testNamespace))
+	var serverIPs []*net.IP
+	if podIPs.ipv4 != nil {
+		serverIPs = append(serverIPs, podIPs.ipv4)
+	}
+	if podIPs.ipv6 != nil {
+		serverIPs = append(serverIPs, podIPs.ipv6)
+	}
+
+	l7ProtocolAllowsPathHostname := []crdv1alpha1.L7Protocol{
+		{
+			HTTP: &crdv1alpha1.HTTPProtocol{
+				Method: "GET",
+				Path:   "/host*",
+			},
+		},
+	}
+	l7ProtocolAllowsAnyPath := []crdv1alpha1.L7Protocol{
+		{
+			HTTP: &crdv1alpha1.HTTPProtocol{
+				Method: "GET",
+			},
+		},
+	}
+
+	policyAllowPathHostname := "test-l7-http-allow-path-hostname"
+	policyAllowAnyPath := "test-l7-http-allow-any-path"
+
+	probeFn := func(allowHTTPPathHostname, allowHTTPPathClientIP bool) {
+		for _, ip := range serverIPs {
+			baseURL := net.JoinHostPort(ip.String(), "8080")
+
+			// To verify that if path 'hostname' is Allowed.
+			hostname, err := probeHostnameFromPod(data, clientPodName, agnhostContainerName, baseURL)
+			if allowHTTPPathHostname {
+				assert.NoError(t, err)
+				assert.Equal(t, serverPodName, hostname)
+			} else {
+				assert.NotNil(t, err)
+			}
+
+			// To verify that if path 'clientip' is Allowted.
+			_, err = probeClientIPFromPod(data, clientPodName, agnhostContainerName, baseURL)
+			if allowHTTPPathClientIP {
+				assert.NoError(t, err)
+			} else {
+				assert.NotNil(t, err)
+			}
+
+			// For IPv4, non-HTTP connections should be rejected by Suricata. For IPv6, there is an issue that reject
+			// packet cannot be generated by Suricata and sent back to client.
+			if ip.To4() != nil {
+				cmd = []string{"bash", "-c", fmt.Sprintf("dig @%s google.com a +tcp -p 8080", ip)}
+				stdout, _, err := data.RunCommandFromPod(data.testNamespace, clientPodName, agnhostContainerName, cmd)
+				// For the client Pod which is selected by the L7 NetworkPolicy, the expected output returned
+				// from Suricata should contain "connection reset".
+				assert.NoError(t, err)
+				assert.Contains(t, stdout, fmt.Sprintf("communications error to %s#8080: connection reset", ip))
+			}
+		}
+
+	}
+
+	t.Run("Ingress", func(t *testing.T) {
+		// Create two L7 NetworkPolicies, one allows HTTP path 'hostname', the other allows any HTTP path. Note that,
+		// the priority of the first one is higher than the second one, and they have the same appliedTo labels and Pod
+		// selector labels.
+		createL7NetworkPolicy(t, data, true, policyAllowPathHostname, 1, clientPodLabels, serverPodLabels, ProtocolTCP, 8080, l7ProtocolAllowsPathHostname)
+		createL7NetworkPolicy(t, data, true, policyAllowAnyPath, 2, clientPodLabels, serverPodLabels, ProtocolTCP, 8080, l7ProtocolAllowsAnyPath)
+		time.Sleep(networkPolicyDelay)
+
+		// HTTP path 'hostname' is allowed by the first L7 NetworkPolicy, and the priority of the second L7 NetworkPolicy
+		// is lower than the first L7 NetworkPolicy. Since they have the appliedTo labels and Pod selector labels and
+		// the first L7 NetworkPolicy has higher priority, matched packets will be only matched by the first L7 NetworkPolicy.
+		// As a result, only HTTP path 'hostname' is allowed by the first L7 NetworkPolicy, other HTTP path like 'clientip'
+		// will be rejected.
+		probeFn(true, false)
+
+		// Delete the first L7 NetworkPolicy that only allows HTTP path 'hostname'.
+		data.crdClient.CrdV1alpha1().NetworkPolicies(data.testNamespace).Delete(context.TODO(), policyAllowPathHostname, metav1.DeleteOptions{})
+		time.Sleep(networkPolicyDelay)
+
+		// Since the fist L7 NetworkPolicy has been deleted, corresponding packets will be matched by the second L7 NetworkPolicy,
+		// and the second L7 NetworkPolicy allows any HTTP path, then both path 'hostname' and 'clientip' are allowed.
+		probeFn(true, true)
+
+		data.crdClient.CrdV1alpha1().NetworkPolicies(data.testNamespace).Delete(context.TODO(), policyAllowAnyPath, metav1.DeleteOptions{})
+	})
+
+	time.Sleep(networkPolicyDelay)
+	t.Run("Egress", func(t *testing.T) {
+		// Create two L7 NetworkPolicies, one allows HTTP path 'hostname', the other allows any HTTP path. Note that,
+		// the priority of the first one is higher than the second one, and they have the same appliedTo labels and Pod
+		// selector labels.
+		createL7NetworkPolicy(t, data, false, policyAllowPathHostname, 1, serverPodLabels, clientPodLabels, ProtocolTCP, 8080, l7ProtocolAllowsPathHostname)
+		createL7NetworkPolicy(t, data, false, policyAllowAnyPath, 2, serverPodLabels, clientPodLabels, ProtocolTCP, 8080, l7ProtocolAllowsAnyPath)
+		time.Sleep(networkPolicyDelay)
+
+		// HTTP path 'hostname' is allowed by the first L7 NetworkPolicy, and the priority of the second L7 NetworkPolicy
+		// is lower than the first L7 NetworkPolicy. Since they have the appliedTo labels and Pod selector labels and
+		// the first L7 NetworkPolicy has higher priority, matched packets will be only matched by the first L7 NetworkPolicy.
+		// As a result, only HTTP path 'hostname' is allowed by the first L7 NetworkPolicy, other HTTP path like 'clientip'
+		// will be rejected.
+		probeFn(true, false)
+
+		// Delete the first L7 NetworkPolicy that only allows HTTP path 'hostname'.
+		data.crdClient.CrdV1alpha1().NetworkPolicies(data.testNamespace).Delete(context.TODO(), policyAllowPathHostname, metav1.DeleteOptions{})
+		time.Sleep(networkPolicyDelay)
+
+		// Since the fist L7 NetworkPolicy has been deleted, corresponding packets will be matched by the second L7 NetworkPolicy,
+		// and the second L7 NetworkPolicy allows any HTTP path, then both path 'hostname' and 'clientip' are allowed.
+		probeFn(true, true)
+	})
+}
diff --git a/test/e2e/utils/anp_spec_builder.go b/test/e2e/utils/anp_spec_builder.go
index dc350e1ca..42645991e 100644
--- a/test/e2e/utils/anp_spec_builder.go
+++ b/test/e2e/utils/anp_spec_builder.go
@@ -103,7 +103,7 @@ func (b *AntreaNetworkPolicySpecBuilder) GetAppliedToPeer(podSelector map[string
 }
 
 func (b *AntreaNetworkPolicySpecBuilder) AddIngress(protoc AntreaPolicyProtocol,
-	port *int32, portName *string, endPort, icmpType, icmpCode, igmpType *int32,
+	port *int32, portName *string, endPort, icmpType, icmpCode, igmpType *int32, l7Protocols []crdv1alpha1.L7Protocol,
 	groupAddress, cidr *string, podSelector map[string]string, nsSelector map[string]string, eeSelector map[string]string,
 	podSelectorMatchExp []metav1.LabelSelectorRequirement, nsSelectorMatchExp []metav1.LabelSelectorRequirement, eeSelectorMatchExp []metav1.LabelSelectorRequirement,
 	ruleAppliedToSpecs []ANPAppliedToSpec, action crdv1alpha1.RuleAction, ruleGroup, name string) *AntreaNetworkPolicySpecBuilder {
@@ -153,19 +153,20 @@ func (b *AntreaNetworkPolicySpecBuilder) AddIngress(protoc AntreaPolicyProtocol,
 	}
 	ports, protocols := GenPortsOrProtocols(protoc, port, portName, endPort, icmpType, icmpCode, igmpType, groupAddress)
 	newRule := crdv1alpha1.Rule{
-		From:      policyPeer,
-		Ports:     ports,
-		Protocols: protocols,
-		Action:    &action,
-		Name:      name,
-		AppliedTo: appliedTos,
+		From:        policyPeer,
+		Ports:       ports,
+		Protocols:   protocols,
+		L7Protocols: l7Protocols,
+		Action:      &action,
+		Name:        name,
+		AppliedTo:   appliedTos,
 	}
 	b.Spec.Ingress = append(b.Spec.Ingress, newRule)
 	return b
 }
 
 func (b *AntreaNetworkPolicySpecBuilder) AddEgress(protoc AntreaPolicyProtocol,
-	port *int32, portName *string, endPort, icmpType, icmpCode, igmpType *int32,
+	port *int32, portName *string, endPort, icmpType, icmpCode, igmpType *int32, l7Protocols []crdv1alpha1.L7Protocol,
 	groupAddress, cidr *string, podSelector map[string]string, nsSelector map[string]string, eeSelector map[string]string,
 	podSelectorMatchExp []metav1.LabelSelectorRequirement, nsSelectorMatchExp []metav1.LabelSelectorRequirement, eeSelectorMatchExp []metav1.LabelSelectorRequirement,
 	ruleAppliedToSpecs []ANPAppliedToSpec, action crdv1alpha1.RuleAction, ruleGroup, name string) *AntreaNetworkPolicySpecBuilder {
@@ -173,16 +174,17 @@ func (b *AntreaNetworkPolicySpecBuilder) AddEgress(protoc AntreaPolicyProtocol,
 	// For simplicity, we just reuse the Ingress code here.  The underlying data model for ingress/egress is identical
 	// With the exception of calling the rule `To` vs. `From`.
 	c := &AntreaNetworkPolicySpecBuilder{}
-	c.AddIngress(protoc, port, portName, endPort, icmpType, icmpCode, igmpType, groupAddress, cidr, podSelector, nsSelector, eeSelector,
+	c.AddIngress(protoc, port, portName, endPort, icmpType, icmpCode, igmpType, l7Protocols, groupAddress, cidr, podSelector, nsSelector, eeSelector,
 		podSelectorMatchExp, nsSelectorMatchExp, eeSelectorMatchExp, ruleAppliedToSpecs, action, ruleGroup, name)
 	theRule := c.Get().Spec.Ingress[0]
 
 	b.Spec.Egress = append(b.Spec.Egress, crdv1alpha1.Rule{
-		To:        theRule.From,
-		Ports:     theRule.Ports,
-		Action:    theRule.Action,
-		Name:      theRule.Name,
-		AppliedTo: theRule.AppliedTo,
+		To:          theRule.From,
+		Ports:       theRule.Ports,
+		Action:      theRule.Action,
+		Name:        theRule.Name,
+		AppliedTo:   theRule.AppliedTo,
+		L7Protocols: theRule.L7Protocols,
 	})
 	return b
 }
diff --git a/test/e2e/vmagent_test.go b/test/e2e/vmagent_test.go
index 6eaa732e0..808c3b589 100644
--- a/test/e2e/vmagent_test.go
+++ b/test/e2e/vmagent_test.go
@@ -460,11 +460,11 @@ func createANPForExternalNode(t *testing.T, data *TestData, name, namespace stri
 			cidr = &peerIPCIDR
 		}
 		port := int32(iperfPort)
-		ruleFunc(proto, &port, nil, nil, nil, nil, nil, nil, cidr, nil, nil, peerLabel,
+		ruleFunc(proto, &port, nil, nil, nil, nil, nil, nil, nil, cidr, nil, nil, peerLabel,
 			nil, nil, nil, nil, ruleAction, "", "")
 	case ProtocolICMP:
 		peerIPCIDR := fmt.Sprintf("%s/32", nodeIP(0))
-		ruleFunc(ProtocolICMP, nil, nil, nil, &icmpType, &icmpCode, nil, nil, &peerIPCIDR, nil, nil, nil,
+		ruleFunc(ProtocolICMP, nil, nil, nil, &icmpType, &icmpCode, nil, nil, nil, &peerIPCIDR, nil, nil, nil,
 			nil, nil, nil, nil, ruleAction, "", "")
 	}
 	anpRule := builder.Get()
diff --git a/test/integration/agent/openflow_test.go b/test/integration/agent/openflow_test.go
index 52b828ee3..ab1e036ef 100644
--- a/test/integration/agent/openflow_test.go
+++ b/test/integration/agent/openflow_test.go
@@ -118,7 +118,7 @@ func TestConnectivityFlows(t *testing.T) {
 		antrearuntime.WindowsOS = runtime.GOOS
 	}
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, true, false, false, false, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, true, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 	defer func() {
@@ -174,7 +174,7 @@ func TestAntreaFlexibleIPAMConnectivityFlows(t *testing.T) {
 	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, true, false, false, true, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, true, false, false, true, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 	defer func() {
@@ -237,7 +237,7 @@ func TestReplayFlowsConnectivityFlows(t *testing.T) {
 	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, true, false, false, false, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, true, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 
@@ -279,12 +279,12 @@ func TestReplayFlowsNetworkPolicyFlows(t *testing.T) {
 	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, false, false, false, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, false, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 
 	config := prepareConfiguration(true, false, false)
-	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: true}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{})
+	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: true}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{}, &agentconfig.L7NetworkPolicyConfig{})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -369,7 +369,7 @@ func testReplayFlows(t *testing.T) {
 }
 
 func testInitialize(t *testing.T, config *testConfig) {
-	if _, err := c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: config.enableIPv4, IPv6Enabled: config.enableIPv6}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{}); err != nil {
+	if _, err := c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: config.enableIPv4, IPv6Enabled: config.enableIPv6}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{}, &agentconfig.L7NetworkPolicyConfig{}); err != nil {
 		t.Errorf("Failed to initialize openflow client: %v", err)
 	}
 	for _, tableFlow := range prepareDefaultFlows(config) {
@@ -462,12 +462,12 @@ func TestNetworkPolicyFlows(t *testing.T) {
 	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, false, false, false, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, false, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
 	config := prepareConfiguration(true, true, false)
-	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: true, IPv6Enabled: true}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{})
+	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: true, IPv6Enabled: true}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{}, &agentconfig.L7NetworkPolicyConfig{})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -576,7 +576,7 @@ func TestIPv6ConnectivityFlows(t *testing.T) {
 	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, true, false, false, false, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, true, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 
@@ -624,12 +624,12 @@ func TestProxyServiceFlowsAntreaPolicyDisabled(t *testing.T) {
 	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, false, false, false, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, true, false, false, false, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
 	config := prepareConfiguration(true, false, false)
-	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: true}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{})
+	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: true}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{}, &agentconfig.L7NetworkPolicyConfig{})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -714,12 +714,12 @@ func TestProxyServiceFlowsAntreaPoilcyEnabled(t *testing.T) {
 	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, true, true, false, false, false, false, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, true, true, false, false, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
 	config := prepareConfiguration(true, false, false)
-	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: true}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{})
+	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: true}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{}, &agentconfig.L7NetworkPolicyConfig{})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -1783,12 +1783,12 @@ func TestEgressMarkFlows(t *testing.T) {
 	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, false, false, true, false, false, false, false, false, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, false, false, false, true, false, false, false, false, false, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
 	config := prepareConfiguration(true, true, false)
-	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{})
+	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{}, &agentconfig.L7NetworkPolicyConfig{})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -1840,12 +1840,12 @@ func TestTrafficControlFlows(t *testing.T) {
 	legacyregistry.Reset()
 	metrics.InitializeOVSMetrics()
 
-	c = ofClient.NewClient(br, bridgeMgmtAddr, false, false, false, false, false, false, false, true, false)
+	c = ofClient.NewClient(br, bridgeMgmtAddr, false, false, false, false, false, false, false, false, true, false)
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
 	config := prepareConfiguration(true, false, false)
-	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: config.enableIPv4}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{})
+	_, err = c.Initialize(roundInfo, config.nodeConfig, &agentconfig.NetworkConfig{TrafficEncapMode: agentconfig.TrafficEncapModeEncap, IPv4Enabled: config.enableIPv4}, &agentconfig.EgressConfig{}, &agentconfig.ServiceConfig{}, &agentconfig.L7NetworkPolicyConfig{})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {