Merge "Verifies app-filtering in UP4 tests"
diff --git a/TestON/drivers/common/cli/p4runtimeclidriver.py b/TestON/drivers/common/cli/p4runtimeclidriver.py
index abf9620..b44c2cb 100644
--- a/TestON/drivers/common/cli/p4runtimeclidriver.py
+++ b/TestON/drivers/common/cli/p4runtimeclidriver.py
@@ -235,19 +235,22 @@
             main.cleanAndExit()
 
     def buildP4RtTableEntry(self, tableName, actionName, actionParams={},
-                            matchFields={}):
+                            matchFields={}, priority=0):
         """
         Build a Table Entry
         :param tableName: The name of table
         :param actionName: The name of the action
         :param actionParams: A dictionary containing name and values for the action parameters
         :param matchFields: A dictionary containing name and values for the match fields
+        :param priority: for ternary match entries
         :return: main.TRUE or main.FALSE on error
         """
         # TODO: improve error checking when creating the table entry, add
         #  params, and match fields.
         try:
-            main.log.debug(self.name + ": Building P4RT Table Entry")
+            main.log.debug("%s: Building P4RT Table Entry "
+                           "(table=%s, match=%s, priority=%s, action=%s, params=%s)" % (
+                self.name, tableName, matchFields, priority, actionName, actionParams))
             cmd = 'te = table_entry["%s"](action="%s"); ' % (
                 tableName, actionName)
 
@@ -259,6 +262,9 @@
             for name, value in matchFields.items():
                 cmd += 'te.match["%s"]="%s";' % (name, str(value))
 
+            if priority:
+                cmd += 'te.priority=%s;' % priority
+
             response = self.__clearSendAndExpect(cmd)
             if "Unknown action" in response:
                 main.log.error("Unknown action: " + response)
diff --git a/TestON/tests/USECASE/SegmentRouting/QOS/QOS.params b/TestON/tests/USECASE/SegmentRouting/QOS/QOS.params
index 3bdbd04..324fe5a 100644
--- a/TestON/tests/USECASE/SegmentRouting/QOS/QOS.params
+++ b/TestON/tests/USECASE/SegmentRouting/QOS/QOS.params
@@ -45,6 +45,13 @@
                 <five_g>False</five_g>
             </ue2>
         </ues>
+        <app_filters>
+            <allowPort>
+                <app_id>0</app_id>
+                <!-- Default ALLOW -->
+                <action>allow</action>
+            </allowPort>
+        </app_filters>
     </UP4>
 
     <TREX>
diff --git a/TestON/tests/USECASE/SegmentRouting/UP4/UP4.params b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.params
index f4c3173..38f1701 100644
--- a/TestON/tests/USECASE/SegmentRouting/UP4/UP4.params
+++ b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.params
@@ -52,13 +52,33 @@
             </ue2>
             <ue3>
                 <ue_address>10.240.0.3</ue_address>
-                <teid>201</teid>
+                <teid>250</teid>
                 <up_id>30</up_id>
                 <down_id>31</down_id>
                 <tc>0</tc>
                 <five_g>False</five_g>
             </ue3>
         </ues>
+        <app_filters>
+            <allowPort>
+                <app_id>1</app_id>
+                <!-- Should be the same as pdn_host -->
+                <!-- MgmtServer -->
+                <ip_prefix>10.32.11.1/32</ip_prefix>
+                <ip_proto>17</ip_proto>
+                <port_range>80..80</port_range>
+                <priority>20</priority>
+                <action>allow</action>
+            </allowPort>
+            <denyHost>
+                <app_id>2</app_id>
+                <ip_prefix>10.32.11.1/32</ip_prefix>
+                <ip_proto></ip_proto>
+                <port_range></port_range>
+                <priority>10</priority>
+                <action>deny</action>
+            </denyHost>
+        </app_filters>
         <UP4_dataplane_fail>
             <switch_to_kill>Leaf2</switch_to_kill> <!-- Component name of the switch to kill in CASE 5 -->
             <k8s_switch_node>leaf2</k8s_switch_node>
diff --git a/TestON/tests/USECASE/SegmentRouting/UP4/UP4.py b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.py
index f60d353..f0d7fa5 100644
--- a/TestON/tests/USECASE/SegmentRouting/UP4/UP4.py
+++ b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.py
@@ -50,12 +50,14 @@
         )
 
         # ------- Test Upstream traffic (enb->pdn)
-        main.step("Test upstream traffic")
-        up4.testUpstreamTraffic()
+        for app_filter_name in up4.app_filters:
+            main.step("Test upstream traffic %s" % app_filter_name)
+            up4.testUpstreamTraffic(app_filter_name=app_filter_name)
 
         # ------- Test Downstream traffic (pdn->enb)
-        main.step("Test downstream traffic")
-        up4.testDownstreamTraffic()
+        for app_filter_name in up4.app_filters:
+            main.step("Test downstream traffic %s" % app_filter_name)
+            up4.testDownstreamTraffic(app_filter_name=app_filter_name)
 
         main.step("Remove and Verify UPF entities via UP4")
         up4.detachUes()
@@ -673,17 +675,26 @@
             enodebs_fail = main.params["UP4"]["UP4_dataplane_fail"]["enodebs_fail"].split(",")
             enodebs_no_fail = list(set(up4.enodebs.keys()) - set(enodebs_fail))
 
-            # ------- Test Upstream traffic (enbs->pdn)
-            main.step("Test upstream traffic FAIL")
-            up4.testUpstreamTraffic(enb_names=enodebs_fail, shouldFail=True)
-            main.step("Test upstream traffic NO FAIL")
-            up4.testUpstreamTraffic(enb_names=enodebs_no_fail, shouldFail=False)
 
-            # ------- Test Downstream traffic (pdn->enbs)
-            main.step("Test downstream traffic FAIL")
-            up4.testDownstreamTraffic(enb_names=enodebs_fail, shouldFail=True)
-            main.step("Test downstream traffic NO FAIL")
-            up4.testDownstreamTraffic(enb_names=enodebs_no_fail, shouldFail=False)
+            for app_filter_name in up4.app_filters:
+                # Failure only when we forward traffic, when dropping we should
+                # still see traffic being dropped.
+                if up4.app_filters["action"] == "allow":
+                    main.step("Test upstream traffic FAIL %s" % app_filter_name)
+                    up4.testUpstreamTraffic(enb_names=enodebs_fail, app_filter_name=app_filter_name, shouldFail=True)
+                    main.step("Test downstream traffic FAIL %s" % app_filter_name)
+                    up4.testDownstreamTraffic(enb_names=enodebs_fail, app_filter_name=app_filter_name, shouldFail=True)
+                else:
+                    main.step("Test upstream traffic FAIL %s" % app_filter_name)
+                    up4.testUpstreamTraffic(enb_names=enodebs_fail, app_filter_name=app_filter_name)
+                    main.step("Test downstream traffic FAIL %s" % app_filter_name)
+                    up4.testDownstreamTraffic(enb_names=enodebs_fail, app_filter_name=app_filter_name)
+
+                main.step("Test upstream traffic NO FAIL %s" % app_filter_name)
+                up4.testUpstreamTraffic(enb_names=enodebs_no_fail, app_filter_name=app_filter_name)
+                main.step("Test downstream traffic NO FAIL %s" % app_filter_name)
+                up4.testDownstreamTraffic(enb_names=enodebs_no_fail, app_filter_name=app_filter_name)
+
         except Exception as e:
             main.log.error("Unhandled exception!")
             main.log.error(e)
@@ -731,8 +742,9 @@
                 onfail="Switch is not available in ONOS, may influence subsequent tests!"
             )
 
-        main.step("Test upstream traffic AFTER switch reboot")
-        up4.testUpstreamTraffic()
+        for app_filter_name in up4.app_filters:
+            main.step("Test upstream traffic AFTER switch reboot %s" % app_filter_name)
+            up4.testUpstreamTraffic(app_filter_name=app_filter_name)
 
         main.step("Cleanup UPF entities via UP4")
         up4.detachUes()
diff --git a/TestON/tests/USECASE/SegmentRouting/dependencies/up4.py b/TestON/tests/USECASE/SegmentRouting/dependencies/up4.py
index ad65697..98c2e8d 100644
--- a/TestON/tests/USECASE/SegmentRouting/dependencies/up4.py
+++ b/TestON/tests/USECASE/SegmentRouting/dependencies/up4.py
@@ -11,7 +11,7 @@
 TUNNEL_TYPE_GPDU = '3'
 
 UE_PORT = 400
-PDN_PORT = 800
+DEFAULT_PDN_PORT = 800
 GPDU_PORT = 2152
 
 N_FLOWS_PER_UE = 5
@@ -70,6 +70,7 @@
         self.pdn_interface = None
         self.router_mac = None
         self.emulated_ues = {}
+        self.app_filters = {}
         self.up4_client = None
         self.no_host = False
 
@@ -82,6 +83,7 @@
         """
         self.s1u_address = main.params["UP4"]["s1u_address"]
         self.emulated_ues = main.params["UP4"]['ues']
+        self.app_filters = main.params["UP4"]['app_filters']
         self.up4_client = p4rt_client
         self.no_host = no_host
 
@@ -119,25 +121,39 @@
                 self.pdn_host.stopScapy()
 
     def attachUes(self):
+        for app_filter in self.app_filters.values():
+            self.insertAppFilter(**app_filter)
         for (name, ue) in self.emulated_ues.items():
             ue = UP4.__sanitizeUeData(ue)
             self.attachUe(name, **ue)
 
     def detachUes(self):
+        for app_filter in self.app_filters.values():
+            self.removeAppFilter(**app_filter)
         for (name, ue) in self.emulated_ues.items():
             ue = UP4.__sanitizeUeData(ue)
             self.detachUe(name, **ue)
 
-    def testUpstreamTraffic(self, enb_names=None, shouldFail=False):
+    def __pickPortFromRange(self, range):
+        if range is None or len(range) == 0:
+            return DEFAULT_PDN_PORT
+        # First port in range
+        return int(range.split('..')[0])
+
+    def testUpstreamTraffic(self, enb_names=None, app_filter_name=None, shouldFail=False):
         if self.enodebs is None or self.pdn_host is None:
             main.log.error(
                 "Need eNodeB and PDN host params to generate scapy traffic")
             return
-        # Scapy filter needs to start before sending traffic
         if enb_names is None or enb_names == []:
             enodebs = self.enodebs.values()
         else:
             enodebs = [self.enodebs[enb] for enb in enb_names]
+
+        app_filter = self.app_filters[app_filter_name]
+        pdn_port = self.__pickPortFromRange(app_filter.get("port_range", None))
+        app_filter_should_drop = app_filter["action"] != "allow"
+
         pkt_filter_upstream = ""
         ues = []
         for enb in enodebs:
@@ -149,7 +165,7 @@
                         pkt_filter_upstream += " or "
                     pkt_filter_upstream += "src host " + ue["ue_address"]
         pkt_filter_upstream = "ip and udp dst port %s and (%s) and dst host %s" % \
-                              (PDN_PORT, pkt_filter_upstream,
+                              (pdn_port, pkt_filter_upstream,
                                self.pdn_interface["ips"][0])
         main.log.info("Start listening on %s intf %s" %
                       (self.pdn_host.name, self.pdn_interface["name"]))
@@ -171,30 +187,42 @@
                                    src_ip_inner=ue["ue_address"],
                                    dst_ip_inner=self.pdn_interface["ips"][0],
                                    src_udp_inner=UE_PORT,
-                                   dst_udp_inner=PDN_PORT,
+                                   dst_udp_inner=pdn_port,
                                    teid=int(ue["teid"]))
                 enb["host"].sendPacket(iface=enb["interface"])
 
         packets = UP4.checkFilterAndGetPackets(self.pdn_host)
+        if app_filter_should_drop:
+            expected_pkt_count = 0
+        else:
+            # We expect exactly 1 packet per UE.
+            expected_pkt_count = len(ues)
+        actual_pkt_count = packets.count('Ether')
         fail = False
-        if len(ues) != packets.count('Ether'):
+        if expected_pkt_count != actual_pkt_count:
             fail = True
-            msg = "Failed to capture packets in PDN.\n" + str(packets)
+            msg = "Received %d packets (expected %d)\n%s\n" % (
+                actual_pkt_count, expected_pkt_count, str(packets)
+            )
         else:
-            msg = "Correctly captured packet in PDN. "
-        # We expect exactly 1 packet per UE
-        pktsFiltered = [packets.count("src=" + ue["ue_address"]) for ue in ues]
-        if pktsFiltered.count(1) != len(pktsFiltered):
-            fail = True
-            msg += "\nError on the number of packets per UE in downstream.\n" + str(
-                packets)
-        else:
-            msg += "\nOne packet per UE in upstream. "
+            msg = "Received %d packets (expected %d)\n" % (
+                actual_pkt_count, expected_pkt_count
+            )
 
+        if expected_pkt_count > 0:
+            # Make sure the captured packets are from the expected UE addresses.
+            for ue in ues:
+                ue_pkt_count = packets.count("src=" + ue["ue_address"])
+                if ue_pkt_count != 1:
+                    fail = True
+                msg += "Received %d packet(s) from UE %s (expected 1)\n" % (
+                    ue_pkt_count, ue["ue_address"]
+                )
         utilities.assert_equal(
-            expect=shouldFail, actual=fail, onpass=msg, onfail=msg)
+            expect=shouldFail, actual=fail, onpass=msg, onfail=msg
+        )
 
-    def testDownstreamTraffic(self, enb_names=None, shouldFail=False):
+    def testDownstreamTraffic(self, enb_names=None, app_filter_name=None, shouldFail=False):
         if self.enodebs is None or self.pdn_host is None:
             main.log.error(
                 "Need eNodeB and PDN host params to generate scapy traffic")
@@ -203,6 +231,11 @@
             enodebs = self.enodebs.values()
         else:
             enodebs = [self.enodebs[enb] for enb in enb_names]
+
+        app_filter = self.app_filters[app_filter_name]
+        pdn_port = self.__pickPortFromRange(app_filter.get("port_range", None))
+        app_filter_should_drop = app_filter["action"] != "allow"
+
         pkt_filter_downstream = "ip and udp src port %d and udp dst port %d and src host %s" % (
             GPDU_PORT, GPDU_PORT, self.s1u_address)
         ues = []
@@ -226,7 +259,7 @@
                                dst_eth=self.router_mac,
                                src_ip=self.pdn_interface["ips"][0],
                                dst_ip=ue["ue_address"],
-                               src_udp=PDN_PORT,
+                               src_udp=pdn_port,
                                dst_udp=UE_PORT)
             self.pdn_host.sendPacket(iface=self.pdn_interface["name"])
         packets = ""
@@ -236,28 +269,54 @@
         # The BPF filter might capture non-GTP packets because we can't filter
         # GTP header in BPF. For this reason, check that the captured packets
         # are from the expected tunnels.
-        # TODO: check inner UDP and IP fields as well
+        # TODO: check inner IP fields as well
         # FIXME: with newer scapy TEID becomes teid (required for Scapy 2.4.5)
-        pktsFiltered = [packets.count("TEID=" + hex(int(ue["teid"])) + "L ")
-                        for ue in ues]
-        main.log.info("PACKETS: " + str(packets))
-        main.log.info("PKTs Filtered: " + str(pktsFiltered))
+        downlink_teids = [int(ue["teid"]) + 1 for ue in ues]
+        # Number of GTP packets from expected TEID per UEs
+        gtp_pkts = [
+            packets.count("TEID=" + hex(teid) + "L ") for teid in downlink_teids
+        ]
+        # Number of packets from the expected PDN port
+        app_pkts = packets.count("UDP  sport=" + str(pdn_port))
+        if app_filter_should_drop:
+            expected_pkt_count = 0
+        else:
+            # We expect exactly 1 packet per UE.
+            expected_pkt_count = len(ues)
+
         fail = False
-        if len(ues) != sum(pktsFiltered):
+        if expected_pkt_count != sum(gtp_pkts):
             fail = True
-            msg = "Failed to capture packets in eNodeB.\n" + str(packets)
+            msg = "Received %d packets (expected %d) from TEIDs %s\n%s\n" % (
+                sum(gtp_pkts), expected_pkt_count, downlink_teids, str(packets)
+            )
         else:
-            msg = "Correctly captured packets in eNodeB. "
-        # We expect exactly 1 packet per UE
-        if pktsFiltered.count(1) != len(pktsFiltered):
+            msg = "Received %d packets (expected %d) from TEIDs %s\n" % (
+                sum(gtp_pkts), expected_pkt_count, downlink_teids
+            )
+        if expected_pkt_count != app_pkts:
             fail = True
-            msg += "\nError on the number of packets per GTP TEID in downstream.\n" + str(
-                packets)
+            msg += "Received %d packets (expected %d) from PDN port %s\n%s\n" % (
+                sum(gtp_pkts), expected_pkt_count, pdn_port, str(packets)
+            )
         else:
-            msg += "\nOne packet per GTP TEID in downstream. "
+            msg += "Received %d packets (expected %d) from PDN port %s\n" % (
+                sum(gtp_pkts), expected_pkt_count, pdn_port
+            )
+        if expected_pkt_count > 0:
+            if gtp_pkts.count(1) != len(gtp_pkts):
+                fail = True
+                msg += "Received %s packet(s) per UE (expected %s)\n%s\n" % (
+                    gtp_pkts, [1] * len(gtp_pkts), packets
+                )
+            else:
+                msg += "Received %s packet(s) per UE (expected %s)\n" % (
+                    gtp_pkts, [1] * len(gtp_pkts)
+                )
 
         utilities.assert_equal(
-            expect=shouldFail, actual=fail, onpass=msg, onfail=msg)
+            expect=shouldFail, actual=fail, onpass=msg, onfail=msg
+        )
 
     def readUeSessionsNumber(self):
         """
@@ -291,9 +350,8 @@
         :return: True if the number of UE sessions and terminations is expected,
         False otherwise
         """
-        nUeSessions = self.readUeSessionsNumber()
-        nTerminations = self.readTerminationsNumber()
-        return nUeSessions == nTerminations == len(self.emulated_ues) * 2
+        return self.readUeSessionsNumber() == len(self.emulated_ues) * 2 and \
+               self.readTerminationsNumber() == len(self.emulated_ues) * 2 * len(self.app_filters)
 
     def verifyNoUesFlowNumberP4rt(self, preInstalledUes=0):
         """
@@ -303,7 +361,8 @@
          are still programmed
         :return:
         """
-        return self.readUeSessionsNumber() == self.readTerminationsNumber() == preInstalledUes * 2
+        return self.readUeSessionsNumber() == preInstalledUes * 2 and \
+               self.readTerminationsNumber() == preInstalledUes * 2 * len(self.app_filters)
 
     def verifyNoUesFlow(self, onosCli, retries=10):
         """
@@ -346,7 +405,7 @@
         :param onosCli: An instance of a OnosCliDriver
         :param retries: Number of retries
         """
-        failString = ""
+        failString = []
         retValue = utilities.retry(f=self.__internalVerifyUp4Flow,
                                    retValue=False,
                                    args=[onosCli, failString],
@@ -357,10 +416,18 @@
             actual=retValue,
             onpass="Correct UE session, terminations and GTP tunnel peers in ONOS",
             onfail="Wrong UE session, terminations and GTP tunnel peers in ONOS. " +
-                   "Missing:\n" + failString
+                   "Missing:\n" + '\n'.join(failString)
         )
 
-    def __internalVerifyUp4Flow(self, onosCli, failMsg=""):
+    def __internalVerifyUp4Flow(self, onosCli, failMsg=[]):
+        # Need to pass a list, so it's an object and we can use failMsg to
+        # return a string values from this method.
+
+        # Cleanup failMsg if any remaining from previous runs
+        del failMsg[:]
+        applications = onosCli.sendline(cmdStr="up4:read-entities -f",
+                                        showResponse=True,
+                                        noExit=True, expectJson=False)
         sessions = onosCli.sendline(cmdStr="up4:read-entities -s",
                                     showResponse=True,
                                     noExit=True, expectJson=False)
@@ -371,24 +438,40 @@
                                      showResponse=True,
                                      noExit=True, expectJson=False)
         fail = False
+        for app_filter in self.app_filters.values():
+            if not UP4.__defaultApp(**app_filter):
+                if applications.count(self.appFilterOnosString(**app_filter)) != 1:
+                    failMsg.append(self.appFilterOnosString(**app_filter))
+                    fail = True
         for (ue_name, ue) in self.emulated_ues.items():
             if sessions.count(self.upUeSessionOnosString(**ue)) != 1:
-                failMsg += self.upUeSessionOnosString(**ue) + "\n"
+                failMsg.append(self.upUeSessionOnosString(**ue))
                 fail = True
             if sessions.count(self.downUeSessionOnosString(**ue)) != 1:
-                failMsg += self.downUeSessionOnosString(**ue) + "\n"
+                failMsg.append(self.downUeSessionOnosString(**ue))
                 fail = True
-            if terminations.count(self.upTerminationOnosString(**ue)) != 1:
-                failMsg += self.upTerminationOnosString(**ue) + "\n"
-                fail = True
-            if terminations.count(self.downTerminationOnosString(**ue)) != 1:
-                failMsg += self.downTerminationOnosString(**ue) + "\n"
-                fail = True
+            for app_filter in self.app_filters.values():
+                if terminations.count(self.upTerminationOnosString(app_filter=app_filter, **ue)) != 1:
+                    failMsg.append(self.upTerminationOnosString(app_filter=app_filter, **ue))
+                    fail = True
+                if terminations.count(self.downTerminationOnosString(app_filter=app_filter, **ue)) != 1:
+                    failMsg.append(self.downTerminationOnosString(app_filter=app_filter, **ue))
+                    fail = True
             if tunn_peer.count(self.gtpTunnelPeerOnosString(ue_name, **ue)) != 1:
-                failMsg += self.gtpTunnelPeerOnosString(ue_name, **ue) + "\n"
+                failMsg.append(self.gtpTunnelPeerOnosString(ue_name, **ue))
                 fail = True
         return not fail
 
+    def appFilterOnosString(self, app_id, priority, ip_proto, ip_prefix, port_range, **kwargs):
+        return "UpfApplication(priority=%s, Match(%s%s%s%s) -> Action(app_id=%s))" % (
+            priority,
+            ("ip_prefix=%s, " % ip_prefix) if ip_prefix else "",
+            ("l4_port_range=[%s], " % port_range) if port_range else "",
+            ("ip_proto=%s, " % ip_proto) if ip_proto else "",
+            "slice_id=0",
+            app_id
+        )
+
     def upUeSessionOnosString(self, teid=None, teid_up=None, sess_meter_idx=DEFAULT_SESSION_METER_IDX, **kwargs):
         if teid is not None:
             teid_up = teid
@@ -403,23 +486,33 @@
         return "UpfSessionDL(Match(ue_addr={}) -> Action(FWD,  tun_peer={}, session_meter_idx={}))".format(
             ue_address, tunn_peer_id, sess_meter_idx)
 
-    def upTerminationOnosString(self, ue_address, up_id=None, app_id=DEFAULT_APP_ID,
+    def upTerminationOnosString(self, ue_address, app_filter, up_id=None,
                                 ctr_id_up=None, tc=None, app_meter_idx=DEFAULT_APP_METER_IDX, **kwargs):
         if up_id is not None:
             ctr_id_up = up_id
-        return "UpfTerminationUL(Match(ue_addr={}, app_id={}) -> Action(FWD, ctr_id={}, tc={}, app_meter_idx={}))".format(
-            ue_address, app_id, ctr_id_up, tc, app_meter_idx)
+        if app_filter["action"] == "allow":
+            return "UpfTerminationUL(Match(ue_addr={}, app_id={}) -> Action(FWD, ctr_id={}, tc={}, app_meter_idx={}))".format(
+                ue_address, app_filter["app_id"], ctr_id_up, tc, app_meter_idx)
+        else:
+            return "UpfTerminationUL(Match(ue_addr={}, app_id={}) -> Action(DROP, ctr_id={}, tc=null, app_meter_idx=0))".format(
+                ue_address, app_filter["app_id"], ctr_id_up)
 
-    def downTerminationOnosString(self, ue_address, teid=None, app_id=DEFAULT_APP_ID,
+    def downTerminationOnosString(self, ue_address, app_filter, teid=None,
                                   down_id=None, ctr_id_down=None, teid_down=None,
                                   tc=None, app_meter_idx=DEFAULT_APP_METER_IDX,
                                   **kwargs):
         if down_id is not None:
             ctr_id_down = down_id
         if teid is not None:
-            teid_down = teid
-        return "UpfTerminationDL(Match(ue_addr={}, app_id={}) -> Action(FWD, teid={}, ctr_id={}, qfi={}, tc={}, app_meter_idx={}))".format(
-            ue_address, app_id, teid_down, ctr_id_down, tc, tc, app_meter_idx)
+            teid_down = int(teid) + 1
+        if tc is None:
+            tc="null"
+        if app_filter["action"] == "allow":
+            return "UpfTerminationDL(Match(ue_addr={}, app_id={}) -> Action(FWD, teid={}, ctr_id={}, qfi={}, tc={}, app_meter_idx={}))".format(
+                ue_address, app_filter["app_id"], teid_down, ctr_id_down, tc, tc, app_meter_idx)
+        else:
+            return "UpfTerminationDL(Match(ue_addr={}, app_id={}) -> Action(DROP, teid=null, ctr_id={}, qfi=null, tc=null, app_meter_idx=0))".format(
+                ue_address, app_filter["app_id"], ctr_id_down)
 
     def gtpTunnelPeerOnosString(self, ue_name, down_id=None, tunn_peer_id=None,
                                 **kwargs):
@@ -437,19 +530,27 @@
             ue["tc"] = 0
         return ue
 
+    def insertAppFilter(self, **kwargs):
+        if not UP4.__defaultApp(**kwargs):
+            self.__programAppFilter(op="program", **kwargs)
+
+    def removeAppFilter(self, **kwargs):
+        if not UP4.__defaultApp(**kwargs):
+            self.__programAppFilter(op="clear", **kwargs)
+
     def attachUe(self, ue_name, ue_address,
                  teid=None, up_id=None, down_id=None,
                  teid_up=None, teid_down=None,
                  ctr_id_up=None, ctr_id_down=None,
                  tunn_peer_id=None,
                  tc=None, five_g=False):
-        self.__programUp4Rules(ue_name,
-                               ue_address,
-                               teid, up_id, down_id,
-                               teid_up, teid_down,
-                               ctr_id_up, ctr_id_down,
-                               tunn_peer_id,
-                               tc, five_g, action="program")
+        self.__programUeRules(ue_name,
+                              ue_address,
+                              teid, up_id, down_id,
+                              teid_up, teid_down,
+                              ctr_id_up, ctr_id_down,
+                              tunn_peer_id,
+                              tc, five_g, op="program")
 
     def detachUe(self, ue_name, ue_address,
                  teid=None, up_id=None, down_id=None,
@@ -457,20 +558,45 @@
                  ctr_id_up=None, ctr_id_down=None,
                  tunn_peer_id=None,
                  tc=None, five_g=False):
-        self.__programUp4Rules(ue_name,
-                               ue_address,
-                               teid, up_id, down_id,
-                               teid_up, teid_down,
-                               ctr_id_up, ctr_id_down,
-                               tunn_peer_id,
-                               tc, five_g, action="clear")
+        self.__programUeRules(ue_name,
+                              ue_address,
+                              teid, up_id, down_id,
+                              teid_up, teid_down,
+                              ctr_id_up, ctr_id_down,
+                              tunn_peer_id,
+                              tc, five_g, op="clear")
 
-    def __programUp4Rules(self, ue_name, ue_address,
-                          teid=None, up_id=None, down_id=None,
-                          teid_up=None, teid_down=None, ctr_id_up=None,
-                          ctr_id_down=None, tunn_peer_id=None,
-                          tc=0, five_g=False, app_id=DEFAULT_APP_ID,
-                          action="program"):
+    def __programAppFilter(self, app_id, ip_prefix=None, ip_proto=None,
+                           port_range=None, priority=0, op="program", **kwargs):
+
+        entries = []
+
+        tableName = 'PreQosPipe.applications'
+        actionName = 'PreQosPipe.set_app_id'
+        actionParams = {'app_id': str(app_id)}
+        matchFields = {}
+        if ip_prefix:
+            matchFields['app_ip_addr'] = str(ip_prefix)
+        if ip_proto:
+            matchFields['app_ip_proto'] = str(ip_proto)
+        if port_range:
+            matchFields['app_l4_port'] = str(port_range)
+
+        if not self.__add_entry(tableName, actionName, matchFields,
+                                actionParams, entries, op, priority):
+            return False
+
+        if op == "program":
+            main.log.info("Application entry added successfully.")
+        elif op == "clear":
+            self.__clear_entries(entries)
+
+    def __programUeRules(self, ue_name, ue_address,
+                         teid=None, up_id=None, down_id=None,
+                         teid_up=None, teid_down=None, ctr_id_up=None,
+                         ctr_id_down=None, tunn_peer_id=None,
+                         tc=0, five_g=False,
+                         op="program"):
         if up_id is not None:
             ctr_id_up = up_id
         if down_id is not None:
@@ -478,7 +604,7 @@
             ctr_id_down = down_id
         if teid is not None:
             teid_up = teid
-            teid_down = teid
+            teid_down = int(teid) + 1
 
         entries = []
 
@@ -493,6 +619,7 @@
         tableName = 'PreQosPipe.sessions_uplink'
         actionName = 'PreQosPipe.set_session_uplink'
         matchFields = {}
+        actionParams = {}
         # Match fields
         matchFields['n3_address'] = str(self.s1u_address)
         matchFields['teid'] = str(teid_up)
@@ -502,7 +629,7 @@
             # TODO: currently QFI match is unsupported in TNA
             main.log.warn("Matching on QFI is currently unsupported in TNA")
         if not self.__add_entry(tableName, actionName, matchFields,
-                                actionParams, entries, action):
+                                actionParams, entries, op):
             return False
 
         # Downlink
@@ -516,49 +643,63 @@
         actionParams['tunnel_peer_id'] = str(tunn_peer_id)
         actionParams["session_meter_idx"] = str(DEFAULT_SESSION_METER_IDX)
         if not self.__add_entry(tableName, actionName, matchFields,
-                                actionParams, entries, action):
+                                actionParams, entries, op):
             return False
 
         # ========================#
         # Terminations Entries
         # ========================#
 
-        # Uplink
-        tableName = 'PreQosPipe.terminations_uplink'
-        actionName = 'PreQosPipe.uplink_term_fwd'
-        matchFields = {}
-        actionParams = {}
+        # Insert one termination entry per app filtering rule,
 
-        # Match fields
-        matchFields['ue_address'] = str(ue_address)
-        matchFields['app_id'] = str(app_id)
-        # Action params
-        actionParams['ctr_idx'] = str(ctr_id_up)
-        actionParams['tc'] = str(tc)
-        actionParams['app_meter_idx'] = str(DEFAULT_APP_METER_IDX)
-        if not self.__add_entry(tableName, actionName, matchFields,
-                                actionParams, entries, action):
-            return False
+        # Uplink
+        for f in self.app_filters.values():
+            tableName = 'PreQosPipe.terminations_uplink'
+            matchFields = {}
+            actionParams = {}
+
+            # Match fields
+            matchFields['ue_address'] = str(ue_address)
+            matchFields['app_id'] = str(f["app_id"])
+
+            # Action params
+            if f['action'] == 'allow':
+                actionName = 'PreQosPipe.uplink_term_fwd'
+                actionParams['app_meter_idx'] = str(DEFAULT_APP_METER_IDX)
+                actionParams['tc'] = str(tc)
+            else:
+                actionName = 'PreQosPipe.uplink_term_drop'
+            actionParams['ctr_idx'] = str(ctr_id_up)
+            if not self.__add_entry(
+                    tableName, actionName, matchFields, actionParams, entries, op
+            ):
+                return False
 
         # Downlink
-        tableName = 'PreQosPipe.terminations_downlink'
-        actionName = 'PreQosPipe.downlink_term_fwd'
-        matchFields = {}
-        actionParams = {}
+        for f in self.app_filters.values():
+            tableName = 'PreQosPipe.terminations_downlink'
+            matchFields = {}
+            actionParams = {}
 
-        # Match fields
-        matchFields['ue_address'] = str(ue_address)
-        matchFields['app_id'] = str(app_id)
-        # Action params
-        actionParams['teid'] = str(teid_down)
-        actionParams['ctr_idx'] = str(ctr_id_down)
-        # 1-1 mapping between QFI and TC
-        actionParams['tc'] = str(tc)
-        actionParams['qfi'] = str(tc)
-        actionParams['app_meter_idx'] = str(DEFAULT_APP_METER_IDX)
-        if not self.__add_entry(tableName, actionName, matchFields,
-                                actionParams, entries, action):
-            return False
+            # Match fields
+            matchFields['ue_address'] = str(ue_address)
+            matchFields['app_id'] = str(f["app_id"])
+
+            # Action params
+            if f['action'] == 'allow':
+                actionName = 'PreQosPipe.downlink_term_fwd'
+                actionParams['teid'] = str(teid_down)
+                # 1-1 mapping between QFI and TC
+                actionParams['tc'] = str(tc)
+                actionParams['qfi'] = str(tc)
+                actionParams['app_meter_idx'] = str(DEFAULT_APP_METER_IDX)
+            else:
+                actionName = 'PreQosPipe.downlink_term_drop'
+            actionParams['ctr_idx'] = str(ctr_id_down)
+
+            if not self.__add_entry(tableName, actionName, matchFields,
+                                    actionParams, entries, op):
+                return False
 
         # ========================#
         # Tunnel Peer Entry
@@ -574,28 +715,31 @@
         actionParams['dst_addr'] = str(enb_address)
         actionParams['sport'] = TUNNEL_SPORT
         if not self.__add_entry(tableName, actionName, matchFields,
-                                actionParams, entries, action):
+                                actionParams, entries, op):
             return False
-        if action == "program":
+        if op == "program":
             main.log.info("All entries added successfully.")
-        elif action == "clear":
+        elif op == "clear":
             self.__clear_entries(entries)
 
     def __add_entry(self, tableName, actionName, matchFields, actionParams,
-                    entries, action):
-        if action == "program":
+                    entries, op, priority=0):
+        if op == "program":
             self.up4_client.buildP4RtTableEntry(
                 tableName=tableName, actionName=actionName,
-                actionParams=actionParams, matchFields=matchFields)
+                actionParams=actionParams, matchFields=matchFields, priority=priority)
             if self.up4_client.pushTableEntry(debug=True) == main.TRUE:
                 main.log.info("*** Entry added.")
             else:
                 main.log.error("Error during table insertion")
                 self.__clear_entries(entries)
                 return False
-        entries.append({"tableName": tableName, "actionName": actionName,
-                        "matchFields": matchFields,
-                        "actionParams": actionParams})
+        entries.append({
+            "tableName": tableName, "actionName": actionName,
+            "matchFields": matchFields,
+            "actionParams": actionParams,
+            "priority": priority
+        })
         return True
 
     def __clear_entries(self, entries):
@@ -646,3 +790,10 @@
             kill = host.killFilter()
             main.log.debug(kill)
         return ""
+
+    @staticmethod
+    def __defaultApp(ip_prefix=None, ip_proto=None, port_range=None, **kwargs):
+        if ip_prefix is None and ip_proto is None and port_range is None:
+            return True
+        return False
+