Repository: antrea-io/antrea
Pull request #4410 (2022-11-23 17:31:08)

Add L7 Antrea native NetworkPolicy datapath support
This PR depends on #4380 #4406 

Signed-off-by: Hongliang Liu <lhongliang@vmware.com>

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

[![Impacted file tree graph](https://codecov.io/gh/antrea-io/antrea/pull/4410/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/4410?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    #4410      +/-   ##
==========================================
+ Coverage   65.56%   66.91%   +1.35%     
==========================================
  Files         400      377      -23     
  Lines       56847    56437     -410     
==========================================
+ Hits        37270    37767     +497     
+ Misses      16882    15968     -914     
- Partials     2695     2702       +7     
```

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

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

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/4410?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | Coverage Δ | |
|---|---|---|
| [cmd/antrea-agent/agent.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-Y21kL2FudHJlYS1hZ2VudC9hZ2VudC5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/agent/agent\_linux.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2FnZW50X2xpbnV4Lmdv) | `4.00% <0.00%> (-0.66%)` | :arrow_down: |
| [pkg/agent/config/node\_config.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbmZpZy9ub2RlX2NvbmZpZy5nbw==) | `69.04% <ø> (-26.96%)` | :arrow_down: |
| [pkg/agent/controller/networkpolicy/cache.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvbmV0d29ya3BvbGljeS9jYWNoZS5nbw==) | `85.59% <ø> (-1.16%)` | :arrow_down: |
| [pkg/agent/types/networkpolicy.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L3R5cGVzL25ldHdvcmtwb2xpY3kuZ28=) | `89.28% <0.00%> (-5.31%)` | :arrow_down: |
| [pkg/apis/controlplane/types.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FwaXMvY29udHJvbHBsYW5lL3R5cGVzLmdv) | `100.00% <ø> (ø)` | |
| [pkg/agent/openflow/network\_policy.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L29wZW5mbG93L25ldHdvcmtfcG9saWN5Lmdv) | `74.87% <6.06%> (-4.96%)` | :arrow_down: |
| [pkg/agent/agent.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2FnZW50Lmdv) | `49.90% <20.00%> (-3.72%)` | :arrow_down: |
| [...ntroller/networkpolicy/networkpolicy\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvbmV0d29ya3BvbGljeS9uZXR3b3JrcG9saWN5X2NvbnRyb2xsZXIuZ28=) | `73.66% <45.45%> (+0.31%)` | :arrow_up: |
| [...nt/controller/l7networkpolicy/suricata/enforcer.go](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvbDduZXR3b3JrcG9saWN5L3N1cmljYXRhL2VuZm9yY2VyLmdv) | `55.67% <55.67%> (ø)` | |
| ... and [56 more](https://codecov.io/gh/antrea-io/antrea/pull/4410/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | |

hongliangl: Could you help review this PR? @tnqn @qiyueyao  I have polished the code and I'll add e2e tests in this PR later.
tnqn: /test-all
tnqn: and subsequent rules will not be considered.
tnqn: /test-all
tnqn: /skip-all which have succeeded

Patch:
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index fe0de0c5c..102945f38 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -141,6 +141,7 @@ func run(o *Options) error {
 
 	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,
@@ -236,6 +237,7 @@ func run(o *Options) error {
 		k8sClient,
 		crdClient,
 		ovsBridgeClient,
+		ovsCtlClient,
 		ofClient,
 		routeClient,
 		ifaceStore,
@@ -666,7 +668,7 @@ func run(o *Options) error {
 		tcController := trafficcontrol.NewTrafficControlController(ofClient,
 			ifaceStore,
 			ovsBridgeClient,
-			ovsctl.NewClient(o.config.OVSBridge),
+			ovsCtlClient,
 			trafficControlInformer,
 			localPodInformer,
 			namespaceInformer,
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index d23059cdc..9faaf2c7f 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,7 @@ type Initializer struct {
 	wireGuardConfig       *config.WireGuardConfig
 	egressConfig          *config.EgressConfig
 	serviceConfig         *config.ServiceConfig
+	l7NetworkPolicyConfig *config.L7NetworkPolicyConfig
 	enableProxy           bool
 	connectUplinkToBridge bool
 	// networkReadyCh should be closed once the Node's network is ready.
@@ -122,6 +124,7 @@ func NewInitializer(
 	k8sClient clientset.Interface,
 	crdClient versioned.Interface,
 	ovsBridgeClient ovsconfig.OVSBridgeClient,
+	ovsCtlClient ovsctl.OVSCtlClient,
 	ofClient openflow.Client,
 	routeClient route.Interface,
 	ifaceStore interfacestore.InterfaceStore,
@@ -142,6 +145,7 @@ func NewInitializer(
 ) *Initializer {
 	return &Initializer{
 		ovsBridgeClient:       ovsBridgeClient,
+		ovsCtlClient:          ovsCtlClient,
 		client:                k8sClient,
 		crdClient:             crdClient,
 		ifaceStore:            ifaceStore,
@@ -154,6 +158,7 @@ func NewInitializer(
 		wireGuardConfig:       wireGuardConfig,
 		egressConfig:          egressConfig,
 		serviceConfig:         serviceConfig,
+		l7NetworkPolicyConfig: &config.L7NetworkPolicyConfig{},
 		networkReadyCh:        networkReadyCh,
 		stopCh:                stopCh,
 		nodeType:              nodeType,
@@ -386,6 +391,11 @@ func (i *Initializer) Initialize() error {
 		return err
 	}
 
+	// 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 +521,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/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..005fd5611
--- /dev/null
+++ b/pkg/agent/controller/networkpolicy/l7engine/reconciler.go
@@ -0,0 +1,450 @@
+// 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
+}
+
+func convertProtocolHTTP(http *v1beta.HTTPProtocol) string {
+	var keywords []string
+	if http.Path != "" {
+		keywords = append(keywords, fmt.Sprintf(`http.uri; content:"%s";`, 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; content:"%s";`, 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..f75493d05 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,
@@ -97,6 +103,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
@@ -148,6 +159,8 @@ func NewNetworkPolicyController(antreaClientGetter agent.AntreaClientProvider,
 		loggingEnabled:       loggingEnabled,
 		gwPort:               gwPort,
 		tunPort:              tunPort,
+		l7RuleReconciler:     l7engine.NewReconciler(),
+		l7VlanIDAllocator:    newL7VlanIDAllocator(),
 	}
 
 	if antreaPolicyEnabled {
@@ -592,7 +605,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 +614,12 @@ func (c *Controller) syncRule(key string) error {
 			// harmless to delete it.
 			c.statusManager.DeleteRuleRealization(key)
 		}
+		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 +628,17 @@ func (c *Controller) syncRule(key string) error {
 		klog.V(2).InfoS("Rule is not realizable, skipping", "ruleID", key)
 		return nil
 	}
+
+	if 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 +674,15 @@ func (c *Controller) syncRules(keys []string) error {
 		} else if !realizable {
 			klog.Errorf("Rule %s is effective but not realizable", key)
 		} else {
+			if 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/openflow/client.go b/pkg/agent/openflow/client.go
index 36c57e467..e3cfcb420 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,6 +836,7 @@ func (c *client) generatePipelines() {
 	c.featureNetworkPolicy = newFeatureNetworkPolicy(c.cookieAllocator,
 		c.ipProtocols,
 		c.bridge,
+		c.l7NetworkPolicyConfig,
 		c.ovsMetersAreSupported,
 		c.enableDenyTracking,
 		c.enableAntreaPolicy,
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..04ed8f110 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.
@@ -2030,6 +2032,7 @@ func newFeatureNetworkPolicy(
 	cookieAllocator cookie.Allocator,
 	ipProtocols []binding.Protocol,
 	bridge binding.Bridge,
+	l7NetworkPolicyConfig *config.L7NetworkPolicyConfig,
 	ovsMetersAreSupported,
 	enableDenyTracking,
 	enableAntreaPolicy bool,
@@ -2042,6 +2045,7 @@ func newFeatureNetworkPolicy(
 		ipProtocols:              ipProtocols,
 		bridge:                   bridge,
 		nodeType:                 nodeType,
+		l7NetworkPolicyConfig:    l7NetworkPolicyConfig,
 		globalConjMatchFlowCache: make(map[string]*conjMatchFlowContext),
 		policyCache:              cache.NewIndexer(policyConjKeyFunc, cache.Indexers{priorityIndex: priorityIndexFunc}),
 		enableMulticast:          enableMulticast,
@@ -2065,6 +2069,9 @@ func (f *featureNetworkPolicy) initFlows() []binding.Flow {
 	var flows []binding.Flow
 	if f.nodeType == config.K8sNode {
 		flows = append(flows, f.ingressClassifierFlows()...)
+		if f.enableAntreaPolicy {
+			flows = append(flows, f.l7NPTrafficControlFlows()...)
+		}
 	}
 	flows = append(flows, f.skipPolicyRuleCheckFlows()...)
 	return flows
@@ -2118,3 +2125,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..3f2e866f1 100644
--- a/pkg/agent/openflow/pipeline.go
+++ b/pkg/agent/openflow/pipeline.go
@@ -434,11 +434,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 +1743,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 +1770,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 +1793,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.
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/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


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..80c14d559
--- /dev/null
+++ b/pkg/agent/controller/networkpolicy/l7engine/reconciler_test.go
@@ -0,0 +1,217 @@
+// 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,path",
+			http: &v1beta.HTTPProtocol{
+				Host:   "www.google.com",
+				Method: "GET",
+				Path:   "index.html",
+			},
+			expected: "http.uri; content:\"index.html\"; http.method; content:\"GET\"; http.host; content:\"www.google.com\";",
+		},
+		{
+			name: "with host",
+			http: &v1beta.HTTPProtocol{
+				Host: "www.google.com",
+			},
+			expected: "http.host; content:\"www.google.com\";",
+		},
+	}
+	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"; http.method; content:"GET"; http.host; content:"www.google.com"; 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/openflow/client_test.go b/pkg/agent/openflow/client_test.go
index fc75cdf8b..8d6cf29d9 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) {
@@ -360,6 +363,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 +429,13 @@ func newFakeClient(mockOFEntryOperations *oftest.MockOFEntryOperations,
 		NodePortAddressesIPv6: nodePortAddressesIPv6,
 	}
 
+	if o.enableAntreaPolicy {
+		l7NetworkPolicyConfig = &config.L7NetworkPolicyConfig{
+			TargetOFPort: fakeL7NPTargetOFPort,
+			ReturnOFPort: fakeL7NPReturnOFPort,
+		}
+	}
+
 	client.cookieAllocator = cookie.NewAllocator(1)
 	client.ofEntryOperations = mockOFEntryOperations
 	client.nodeConfig = nodeConfig
@@ -432,6 +443,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 +640,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 +653,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 +666,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 +681,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 +695,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 +708,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 +726,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 +737,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 +758,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",
 			},
 		},
@@ -2123,7 +2135,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..18358da70 100644
--- a/pkg/agent/openflow/network_policy_test.go
+++ b/pkg/agent/openflow/network_policy_test.go
@@ -1395,13 +1395,16 @@ func Test_featureNetworkPolicy_initFlows(t *testing.T) {
 			nodeType:      config.K8sNode,
 			clientOptions: []clientOptionsFn{enableMulticast},
 			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",
 			},
 		},
 
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/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..7e29b24b4
--- /dev/null
+++ b/test/e2e/l7networkpolicy_test.go
@@ -0,0 +1,253 @@
+// 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/test/e2e/utils"
+)
+
+func TestL7NetworkPolicy(t *testing.T) {
+	skipIfHasWindowsNodes(t)
+
+	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:   "/hostname",
+			},
+		},
+	}
+	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..c4428d8dd 100644
--- a/test/integration/agent/openflow_test.go
+++ b/test/integration/agent/openflow_test.go
@@ -284,7 +284,7 @@ func TestReplayFlowsNetworkPolicyFlows(t *testing.T) {
 	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) {
@@ -467,7 +467,7 @@ func TestNetworkPolicyFlows(t *testing.T) {
 	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() {
@@ -629,7 +629,7 @@ func TestProxyServiceFlowsAntreaPolicyDisabled(t *testing.T) {
 	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() {
@@ -719,7 +719,7 @@ func TestProxyServiceFlowsAntreaPoilcyEnabled(t *testing.T) {
 	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() {
@@ -1788,7 +1788,7 @@ func TestEgressMarkFlows(t *testing.T) {
 	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() {
@@ -1845,7 +1845,7 @@ func TestTrafficControlFlows(t *testing.T) {
 	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() {