npol: Network Policies plugin 10/43710/14
authorAndrew Yourtchenko <[email protected]>
Wed, 18 Aug 2021 18:10:22 +0000 (20:10 +0200)
committerFlorin Coras <[email protected]>
Mon, 20 Oct 2025 17:52:58 +0000 (17:52 +0000)
New plugin that implements Network policies in VPP.

Type: feature

Change-Id: I04449a9b123ae34a35bdd56cfcfe068daef2c253
Signed-off-by: Andrew Yourtchenko <[email protected]>
Signed-off-by: Nathan Skrzypczak <[email protected]>
Signed-off-by: Aloys Augustin <[email protected]>
Signed-off-by: MathiasRaoul <[email protected]>
Signed-off-by: hedi bouattour <[email protected]>
24 files changed:
MAINTAINERS
docs/developer/plugins/index.rst
docs/developer/plugins/npol.rst [new symlink]
docs/spelling_wordlist.txt
src/plugins/npol/CMakeLists.txt [new file with mode: 0644]
src/plugins/npol/FEATURE.yaml [new file with mode: 0644]
src/plugins/npol/npol.api [new file with mode: 0644]
src/plugins/npol/npol.c [new file with mode: 0644]
src/plugins/npol/npol.h [new file with mode: 0644]
src/plugins/npol/npol.rst [new file with mode: 0644]
src/plugins/npol/npol_api.c [new file with mode: 0644]
src/plugins/npol/npol_format.c [new file with mode: 0644]
src/plugins/npol/npol_format.h [new file with mode: 0644]
src/plugins/npol/npol_interface.c [new file with mode: 0644]
src/plugins/npol/npol_interface.h [new file with mode: 0644]
src/plugins/npol/npol_ipset.c [new file with mode: 0644]
src/plugins/npol/npol_ipset.h [new file with mode: 0644]
src/plugins/npol/npol_match.c [new file with mode: 0644]
src/plugins/npol/npol_match.h [new file with mode: 0644]
src/plugins/npol/npol_policy.c [new file with mode: 0644]
src/plugins/npol/npol_policy.h [new file with mode: 0644]
src/plugins/npol/npol_rule.c [new file with mode: 0644]
src/plugins/npol/npol_rule.h [new file with mode: 0644]
test/test_npol.py [new file with mode: 0644]

index 7b0dc9d..647dd1b 100644 (file)
@@ -769,6 +769,13 @@ M: Nathan Skrzypczak <[email protected]>
 M:     Neale Ranns <[email protected]>
 F:     src/plugins/cnat
 
+Plugin - NPol
+I:     npol
+M:     Aloys Augustin <[email protected]>
+M:     Nathan Skrzypczak <[email protected]>
+M:     Hedi Bouattour <[email protected]>
+F:     src/plugins/npol/
+
 Plugin - Wireguard
 I:     wireguard
 M:     Artem Glazychev <[email protected]>
index 064ce6a..edfbbbe 100644 (file)
@@ -19,6 +19,7 @@ For more on plugins please refer to :ref:`add_plugin`.
 
     quic
     cnat
+    npol
     dev_armada
     lcp
     srv6/index
diff --git a/docs/developer/plugins/npol.rst b/docs/developer/plugins/npol.rst
new file mode 120000 (symlink)
index 0000000..d138f06
--- /dev/null
@@ -0,0 +1 @@
+../../../src/plugins/npol/npol.rst
\ No newline at end of file
index d22e625..73b26a4 100644 (file)
@@ -537,6 +537,8 @@ ipN
 ipsec
 IPsec
 ipsecmb
+ipset
+ipsets
 iptables
 ipv
 iPv
@@ -777,6 +779,7 @@ nodaemon
 noevaluate
 nonaddress
 nosyslog
+npol
 npt
 npt66
 ns
diff --git a/src/plugins/npol/CMakeLists.txt b/src/plugins/npol/CMakeLists.txt
new file mode 100644 (file)
index 0000000..965b262
--- /dev/null
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright(c) 2025 Cisco Systems, Inc.
+
+add_vpp_plugin(npol
+  SOURCES
+  npol.c
+  npol_api.c
+  npol_policy.c
+  npol_rule.c
+  npol_ipset.c
+  npol_interface.c
+  npol_format.c
+  npol_match.c
+
+  MULTIARCH_SOURCES
+  npol_match.c
+
+  API_FILES
+  npol.api
+)
diff --git a/src/plugins/npol/FEATURE.yaml b/src/plugins/npol/FEATURE.yaml
new file mode 100644 (file)
index 0000000..25a1472
--- /dev/null
@@ -0,0 +1,16 @@
+---
+name: Network Policy
+maintainer: Nathan Skrzypczak <[email protected]>
+features:
+  - Interface-level policy configuration for RX and TX traffic
+  - Rule and IP set-based packet filtering for IPv4 and IPv6
+
+description: "This plugin provides a programmable network policy engine in VPP.
+              It allows creation of policies composed of rules and IP sets,
+              which can be applied to interfaces for controlling packet forwarding,
+              filtering by IP addresses, ports, and protocols. It supports both
+              inbound and outbound traffic with default behaviors and can integrate
+              into VPP's packet processing using ACL..."
+
+state: development
+properties: [CLI, API]
\ No newline at end of file
diff --git a/src/plugins/npol/npol.api b/src/plugins/npol/npol.api
new file mode 100644 (file)
index 0000000..b4c48d3
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2020 Cisco and/or its affiliates.
+ * 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.
+ */
+
+/** \file
+    This file defines the vpp control-plane API messages
+    used to configure Network policies
+*/
+
+option version = "0.1.0";
+import "vnet/ip/ip_types.api";
+import "vnet/fib/fib_types.api";
+
+/** \brief Get the plugin version
+    @param client_index - opaque cookie to identify the sender
+    @param context - sender context, to match reply w/ request
+*/
+
+define npol_get_version
+{
+  u32 client_index;
+  u32 context;
+};
+
+/** \brief Reply to get the plugin version
+    @param context - returned sender context, to match reply w/ request
+    @param major - Incremented every time a known breaking behavior change is introduced
+    @param minor - Incremented with small changes, may be used to avoid buggy versions
+*/
+
+define npol_get_version_reply
+{
+  u32 context;
+  u32 major;
+  u32 minor;
+};
+
+enum npol_ipset_type : u8 {
+  NPOL_IP = 0,           /* Each member is an IP address */
+  NPOL_IP_AND_PORT = 1,  /* Each member is "<IP>,(tcp|udp):port" (3-tuple) */
+  NPOL_NET = 2,          /* Each member is a CIDR */
+};
+
+typedef npol_three_tuple {
+  vl_api_address_t address;
+  u8 l4_proto;
+  u16 port;
+};
+
+union npol_ipset_member_val {
+  vl_api_address_t address;
+  vl_api_prefix_t prefix;
+  vl_api_npol_three_tuple_t tuple;
+};
+
+typedef npol_ipset_member {
+  vl_api_npol_ipset_member_val_t val;
+};
+
+define npol_ipset_create
+{
+  u32 client_index;
+  u32 context;
+  vl_api_npol_ipset_type_t type;
+};
+
+define npol_ipset_create_reply
+{
+  u32 context;
+  i32 retval;
+  u32 set_id;
+};
+
+
+autoreply define npol_ipset_add_del_members
+{
+  u32 client_index;
+  u32 context;
+  u32 set_id;
+  bool is_add;
+  u32 len;
+  vl_api_npol_ipset_member_t members[len];
+};
+
+autoreply define npol_ipset_delete
+{
+  u32 client_index;
+  u32 context;
+  u32 set_id;
+};
+
+enum npol_rule_action : u8 {
+  NPOL_ALLOW = 0,  // Accept packet
+  NPOL_DENY,       // Drop / reject packet
+  NPOL_LOG,        // Ignored for now
+  NPOL_PASS,       // Skip following rules, resume evaluation at the policy
+                   // with the id configured in npol_configure_policies
+};
+
+enum npol_entry_type : u8 {
+  NPOL_CIDR = 0,     // simple prefix
+  NPOL_PORT_RANGE,
+  NPOL_PORT_IP_SET,  // Points to an ip + proto + port set
+  NPOL_IP_SET,       // Points to an ip only set
+};
+
+enum npol_policy_default : u8 {
+  NPOL_DEFAULT_ALLOW = 0,     // allow per default
+  NPOL_DEFAULT_DENY, // deny per default
+  NPOL_DEFAULT_PASS,  // pass to profiles per default
+};
+
+typedef npol_port_range {
+  u16 start;
+  u16 end;    // Inclusive, for a single port start==end
+};
+
+typedef npol_entry_set_id {
+  u32 set_id;
+};
+
+union npol_entry_data {
+  vl_api_prefix_t cidr;
+  vl_api_npol_port_range_t port_range;
+  vl_api_npol_entry_set_id_t set_id;
+};
+
+// A rule contains several such entries, each belong to a category
+// categories are: [not_]{src,dst}_{cidr,port_range,port_ip_set,ip_set}
+// (defined byt the 3 first fields in the rule_entry)
+// A rule matches a packet iff:
+// - for every "not" category, the source / destination do not match any entry
+// - for every positive match category, the source / destination matches at
+// least one entry in each category EXCEPT for port ranges and port+ip sets,
+// where the packet only needs to match one entry in either category
+
+typedef npol_rule_entry {
+  bool is_src;
+  bool is_not;
+  vl_api_npol_entry_type_t type;
+  vl_api_npol_entry_data_t data;
+};
+
+enum npol_rule_filter_type : u8 {
+  NPOL_RULE_FILTER_NONE_TYPE = 0,
+  NPOL_RULE_FILTER_ICMP_TYPE,
+  NPOL_RULE_FILTER_ICMP_CODE,
+  NPOL_RULE_FILTER_L4_PROTO,
+};
+
+typedef npol_rule_filter {
+  u32 value;
+  vl_api_npol_rule_filter_type_t type;
+  u8 should_match;
+};
+
+typedef npol_rule {
+  vl_api_npol_rule_action_t action;
+  vl_api_npol_rule_filter_t filters[3];
+  u32 num_entries;
+  vl_api_npol_rule_entry_t matches[num_entries]; // List of other criteria
+};
+
+define npol_rule_create {
+  u32 client_index;
+  u32 context;
+  vl_api_npol_rule_t rule;
+};
+
+autoreply define npol_rule_update {
+  u32 client_index;
+  u32 context;
+  u32 rule_id;
+  vl_api_npol_rule_t rule;
+};
+
+define npol_rule_create_reply {
+  u32 context;
+  i32 retval;
+  u32 rule_id;
+};
+
+autoreply define npol_rule_delete {
+  u32 client_index;
+  u32 context;
+  u32 rule_id;
+};
+
+typedef npol_policy_item {
+  bool is_inbound; // 0 for outbound, 1 for is_inbound
+  u32 rule_id;
+};
+
+define npol_policy_create {
+  u32 client_index;
+  u32 context;
+  u32 num_items;
+  vl_api_npol_policy_item_t rules[num_items];
+};
+
+define npol_policy_create_reply {
+  u32 context;
+  i32 retval;
+  u32 policy_id;
+};
+
+autoreply define npol_policy_update {
+  u32 client_index;
+  u32 context;
+  u32 policy_id;
+  u32 num_items;
+  vl_api_npol_policy_item_t rules[num_items];
+};
+
+autoreply define npol_policy_delete {
+  u32 client_index;
+  u32 context;
+  u32 policy_id;
+};
+
+autoreply define npol_configure_policies {
+  u32 client_index;
+  u32 context;
+  u32 sw_if_index;
+  u32 num_rx_policies;
+  u32 num_tx_policies;
+  u32 total_ids;
+  u8 invert_rx_tx;
+  vl_api_npol_policy_default_t policy_default_rx;
+  vl_api_npol_policy_default_t policy_default_tx;
+  vl_api_npol_policy_default_t profile_default_rx;
+  vl_api_npol_policy_default_t profile_default_tx;
+  u32 policy_ids[total_ids]; // rx_policies, then tx_policies, then profiles
+};
diff --git a/src/plugins/npol/npol.c b/src/plugins/npol/npol.c
new file mode 100644 (file)
index 0000000..6fce47f
--- /dev/null
@@ -0,0 +1,84 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <npol/npol.h>
+#include <npol/npol_match.h>
+#include <npol/npol_format.h>
+
+static clib_error_t *
+npol_match_fn (vlib_main_t *vm, unformat_input_t *input,
+              vlib_cli_command_t *cmd)
+{
+  vnet_main_t *vnm = vnet_get_main ();
+  u32 sw_if_index = NPOL_INVALID_INDEX;
+  u8 _r_action = NPOL_ACTION_UNKNOWN, *r_action = &_r_action;
+  fa_5tuple_t _pkt_5tuple = { 0 }, *pkt_5tuple = &_pkt_5tuple;
+  clib_error_t *error = 0;
+  u32 is_inbound = 0;
+  int is_ip6 = 0;
+  u32 sport = 0, dport = 0, proto = 0;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "%U", unformat_vnet_sw_interface, vnm,
+                   &sw_if_index))
+       ;
+      else if (unformat (input, "sw_if_index %u", &sw_if_index))
+       ;
+      else if (unformat (input, "inbound"))
+       is_inbound = 1;
+      else if (unformat (input, "outbound"))
+       is_inbound = 0;
+      else if (unformat (input, "ip6"))
+       is_ip6 = 1;
+      else if (unformat (input, "ip4"))
+       is_ip6 = 0;
+      else if (unformat (input, "%U;%u->%U;%u", unformat_ip4_address,
+                        &pkt_5tuple->ip4_addr[SRC], &sport,
+                        unformat_ip4_address, &pkt_5tuple->ip4_addr[DST],
+                        &dport))
+       {
+         pkt_5tuple->l4.port[SRC] = sport;
+         pkt_5tuple->l4.port[DST] = dport;
+       }
+      else if (unformat (input, "%U;%u->%U;%u", unformat_ip6_address,
+                        &pkt_5tuple->ip6_addr[SRC], &sport,
+                        unformat_ip6_address, &pkt_5tuple->ip6_addr[DST],
+                        &dport))
+       {
+         pkt_5tuple->l4.port[SRC] = sport;
+         pkt_5tuple->l4.port[DST] = dport;
+       }
+      else if (unformat (input, "%U", unformat_ip_protocol, &proto))
+       pkt_5tuple->l4.proto = proto;
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  if (sw_if_index == NPOL_INVALID_INDEX)
+    {
+      error = clib_error_return (0, "interface not specified");
+      goto done;
+    }
+
+  rv = npol_match_func (sw_if_index, is_inbound, pkt_5tuple, is_ip6, r_action);
+
+  vlib_cli_output (vm, "matched:%d action:%U", rv, format_npol_action,
+                  *r_action);
+
+done:
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_match, static) = {
+  .path = "npol match",
+  .function = npol_match_fn,
+  .short_help = "npol match [<interface>|sw_if_index <idx>] [ip4|ip6] "
+               "[inbound|outbound] 1.1.1.1;65000->3.3.3.3;8080 tcp",
+};
diff --git a/src/plugins/npol/npol.h b/src/plugins/npol/npol.h
new file mode 100644 (file)
index 0000000..2543dd9
--- /dev/null
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#ifndef included_npol_h
+#define included_npol_h
+
+#include <vnet/ip/ip.h>
+#include <vnet/ip/ip_types_api.h>
+#include <acl/public_inlines.h>
+
+#include <npol/npol.api_enum.h>
+#include <npol/npol.api_types.h>
+#include <npol/npol_interface.h>
+
+#define NPOL_INVALID_INDEX ((u32) ~0)
+
+#define SRC 0
+#define DST 1
+
+#define NPOL_ACTION_ALLOW   2
+#define NPOL_ACTION_UNKNOWN 1
+#define NPOL_ACTION_DENY    0
+
+typedef struct
+{
+  u16 start;
+  u16 end;
+} npol_port_range_t;
+
+typedef struct
+{
+  u32 calico_acl_user_id;
+
+  /* API message ID base */
+  u16 msg_id_base;
+
+} npol_main_t;
+
+extern npol_main_t npol_main;
+
+#endif
diff --git a/src/plugins/npol/npol.rst b/src/plugins/npol/npol.rst
new file mode 100644 (file)
index 0000000..d6f2dca
--- /dev/null
@@ -0,0 +1,124 @@
+=============================
+Network Policy (npol) Plugin
+=============================
+
+Overview
+--------
+
+The **Network Policy (npol)** plugin provides a programmable policy engine
+for applying packet filtering and forwarding rules in VPP.
+It allows you to:
+
+- Create and manage **IP sets** (collections of IPs, subnets, or IP:port entries).
+- Define **rules** to allow, deny, or log traffic based on IPs, prefixes, sets, ports, and direction.
+- Build **policies** from rules and apply them on interfaces in RX (inbound) and TX (outbound) directions.
+
+
+Quick Start
+-----------
+
+This example shows how to configure and apply a network policy on a loopback interface.
+
+1. **Create a loopback interface and configure an IP address**
+
+.. code-block:: console
+
+   DBGvpp# create loopback interface
+   loop0
+
+   DBGvpp# set interface state loop0 up
+
+   DBGvpp# set interface ip address loop0 10.0.0.1/32
+
+   DBGvpp# sh int addr
+   local0 (dn):
+   loop0 (up):
+     L3 10.0.0.1/32
+
+2. **Explore npol commands**
+
+.. code-block:: console
+
+   DBGvpp# npol ?
+        npol interface clear                     npol interface clear [interface | sw_if_index N]
+        npol interface configure                 npol interface configure [interface | sw_if_index N] rx <num_rx> tx <num_tx> <policy_id> ...
+        npol ipset add member                    npol ipset add member [id] [prefix]
+        npol ipset add                           npol ipset add [prefix|proto ip port|ip]
+        npol ipset del member                    npol ipset del member [id] [prefix]
+        npol ipset del                           npol ipset del [id]
+        npol policy add                          npol policy add [rx rule_id rule_id ...] [tx rule_id rule_id ...] [update [id]]
+        npol policy del                          npol policy del [id]
+        npol rule add                            npol rule add [ip4|ip6] [allow|deny|log|pass][filter[==|!=]value][[src|dst][==|!=][prefix|set ID|[port-port]]]
+        npol rule del                            npol rule del [id]
+
+3. **Create an IP set**
+
+.. code-block:: console
+
+   DBGvpp# npol ipset add 20.0.0.0/24
+   npol ipset 0 added
+
+   DBGvpp# sh npol ipsets
+   [ipset#0;prefix;20.0.0.0/24,]
+
+4. **Add rules**
+
+- Rule 0: Deny packets with a source IP in the created set.
+- Rule 1: Allow all other packets.
+
+.. code-block:: console
+
+   DBGvpp# npol rule add ip4 deny src==set0
+   npol rule 0 added
+
+   DBGvpp# npol rule add ip4 allow
+   npol rule 1 added
+
+   DBGvpp# sh npol rules
+   [rule#0;deny][src==[ipset#0;prefix;20.0.0.0/24,],]
+   [rule#1;allow][]
+
+5. **Create a policy**
+
+This policy applies Rule 0 and Rule 1 on RX,
+and Rule 1 on TX.
+
+.. code-block:: console
+
+   DBGvpp# npol policy add rx 0 1 tx 1
+   npol policy 0 added
+
+   DBGvpp# sh npol policies verbose
+   [policy#0]
+     tx:[rule#1;allow][]
+     rx:[rule#0;deny][src==[ipset#0;prefix;20.0.0.0/24,],]
+     rx:[rule#1;allow][]
+
+6. **Apply the policy to an interface**
+
+.. code-block:: console
+
+   DBGvpp# npol interface configure loop0 0
+   npol interface 1 configured
+
+   DBGvpp# sh npol interfaces
+   Interfaces with policies configured:
+   [loop0 sw_if_index=1  addr=10.0.0.1]
+      rx-policy-default:1 rx-profile-default:1
+      tx-policy-default:1 tx-profile-default:1
+     profiles:
+       [policy#0]
+         tx:[rule#1;allow][]
+         rx:[rule#0;deny][src==[ipset#0;prefix;20.0.0.0/24,],]
+         rx:[rule#1;allow][]
+
+Summary
+-------
+
+- **IP sets** define groups of IPs, prefixes, or IP:port pairs.
+- **Rules** define match conditions and actions (allow, deny, log, pass).
+- **Policies** group rules per direction (RX/TX).
+- **Interfaces** are configured with policies, enforcing filtering in the datapath.
+
+This modular design allows fine-grained policy enforcement
+directly in VPP with efficient data structures.
diff --git a/src/plugins/npol/npol_api.c b/src/plugins/npol/npol_api.c
new file mode 100644 (file)
index 0000000..6d2061a
--- /dev/null
@@ -0,0 +1,421 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <vnet/vnet.h>
+#include <vnet/plugin/plugin.h>
+#include <vlibapi/api.h>
+#include <vlibmemory/api.h>
+#include <vpp/app/version.h>
+#include <stdbool.h>
+
+#include <npol/npol.h>
+#include <npol/npol_rule.h>
+#include <npol/npol_policy.h>
+#include <npol/npol_ipset.h>
+#include <npol/npol_interface.h>
+
+#define REPLY_MSG_ID_BASE cpm->msg_id_base
+#include <vlibapi/api_helper_macros.h>
+
+#define CALICO_POLICY_VERSION_MAJOR 0
+#define CALICO_POLICY_VERSION_MINOR 0
+
+npol_main_t npol_main = { 0 };
+
+void
+npol_policy_rule_decode (const vl_api_npol_policy_item_t *in,
+                        npol_policy_rule_t *out)
+{
+  out->rule_id = clib_net_to_host_u32 (in->rule_id);
+  out->direction = in->is_inbound ? VLIB_RX : VLIB_TX;
+}
+
+int
+npol_ipset_member_decode (npol_ipset_type_t type,
+                         const vl_api_npol_ipset_member_t *in,
+                         npol_ipset_member_t *out)
+{
+  switch (type)
+    {
+    case IPSET_TYPE_IP:
+      ip_address_decode2 (&in->val.address, &out->address);
+      break;
+    case IPSET_TYPE_IPPORT:
+      ip_address_decode2 (&in->val.tuple.address, &out->ipport.addr);
+      out->ipport.l4proto = in->val.tuple.l4_proto;
+      out->ipport.port = clib_net_to_host_u16 (in->val.tuple.port);
+      break;
+    case IPSET_TYPE_NET:
+      return ip_prefix_decode2 (&in->val.prefix, &out->prefix);
+    }
+  return 0;
+}
+
+void
+npol_port_range_decode (const vl_api_npol_port_range_t *in,
+                       npol_port_range_t *out)
+{
+  out->start = clib_net_to_host_u16 (in->start);
+  out->end = clib_net_to_host_u16 (in->end);
+}
+
+int
+npol_rule_entry_decode (const vl_api_npol_rule_entry_t *in,
+                       npol_rule_entry_t *out)
+{
+  out->flags = 0;
+  if (in->is_src)
+    out->flags |= NPOL_IS_SRC;
+  if (in->is_not)
+    out->flags |= NPOL_IS_NOT;
+  out->type = (npol_entry_type_t) in->type;
+  switch (in->type)
+    {
+    case NPOL_CIDR:
+      return ip_prefix_decode2 (&in->data.cidr, &out->data.cidr);
+    case NPOL_PORT_RANGE:
+      npol_port_range_decode (&in->data.port_range, &out->data.port_range);
+      return 0;
+    case NPOL_PORT_IP_SET:
+    case NPOL_IP_SET:
+      out->data.set_id = clib_net_to_host_u32 (in->data.set_id.set_id);
+      return 0;
+    default:
+      return -1;
+    }
+}
+
+void
+npol_rule_filter_decode (const vl_api_npol_rule_filter_t *in,
+                        npol_rule_filter_t *out)
+{
+  out->type = (npol_rule_filter_type_t) in->type;
+  out->should_match = in->should_match;
+  out->value = clib_net_to_host_u32 (in->value);
+}
+
+static void
+vl_api_npol_get_version_t_handler (vl_api_npol_get_version_t *mp)
+{
+  npol_main_t *cpm = &npol_main;
+  vl_api_npol_get_version_reply_t *rmp;
+  int msg_size = sizeof (*rmp);
+  vl_api_registration_t *reg;
+
+  reg = vl_api_client_index_to_registration (mp->client_index);
+  if (!reg)
+    return;
+
+  rmp = vl_msg_api_alloc (msg_size);
+  clib_memset (rmp, 0, msg_size);
+  rmp->_vl_msg_id = ntohs (VL_API_NPOL_GET_VERSION_REPLY + cpm->msg_id_base);
+  rmp->context = mp->context;
+  rmp->major = htonl (CALICO_POLICY_VERSION_MAJOR);
+  rmp->minor = htonl (CALICO_POLICY_VERSION_MINOR);
+
+  vl_api_send_msg (reg, (u8 *) rmp);
+}
+
+/* NAME: ipset_create */
+static void
+vl_api_npol_ipset_create_t_handler (vl_api_npol_ipset_create_t *mp)
+{
+  npol_main_t *cpm = &npol_main;
+  vl_api_npol_ipset_create_reply_t *rmp;
+  int rv = 0;
+  u32 id;
+
+  id = npol_ipset_create ((npol_ipset_type_t) mp->type);
+
+  REPLY_MACRO2 (VL_API_NPOL_IPSET_CREATE_REPLY,
+               ({ rmp->set_id = clib_host_to_net_u32 (id); }));
+}
+
+/* NAME: ipset_add_del_members */
+static void
+vl_api_npol_ipset_add_del_members_t_handler (
+  vl_api_npol_ipset_add_del_members_t *mp)
+{
+  npol_main_t *cpm = &npol_main;
+  vl_api_npol_ipset_add_del_members_reply_t *rmp;
+  u32 set_id, i, n_members;
+  npol_ipset_type_t type;
+  int rv = 0;
+
+  set_id = clib_net_to_host_u32 (mp->set_id);
+  n_members = clib_net_to_host_u32 (mp->len);
+
+  rv = npol_ipset_get_type (set_id, &type);
+  if (rv)
+    goto done;
+
+  for (i = 0; i < n_members; i++)
+    {
+      npol_ipset_member_t _m, *member = &_m;
+      rv = npol_ipset_member_decode (type, &mp->members[i], member);
+      if (rv)
+       break;
+      if (mp->is_add)
+       rv = npol_ipset_add_member (set_id, member);
+      else
+       rv = npol_ipset_del_member (set_id, member);
+      if (rv)
+       break;
+    }
+
+done:
+  REPLY_MACRO (VL_API_NPOL_IPSET_ADD_DEL_MEMBERS_REPLY);
+}
+
+/* NAME: ipset_delete */
+static void
+vl_api_npol_ipset_delete_t_handler (vl_api_npol_ipset_delete_t *mp)
+{
+  npol_main_t *cpm = &npol_main;
+  vl_api_npol_ipset_delete_reply_t *rmp;
+  u32 set_id;
+  int rv;
+
+  set_id = clib_net_to_host_u32 (mp->set_id);
+  rv = npol_ipset_delete (set_id);
+
+  REPLY_MACRO (VL_API_NPOL_IPSET_DELETE_REPLY);
+}
+
+static int
+vl_api_npol_rule_update_create_handler (u32 *id, vl_api_npol_rule_t *rule)
+{
+  npol_rule_filter_t *filters = 0, *filter;
+  npol_rule_entry_t *entries = 0, *entry;
+  npol_rule_action_t action;
+  int rv;
+  u32 n_matches;
+  u32 i;
+
+  action = (npol_rule_action_t) rule->action;
+
+  for (i = 0; i < ARRAY_LEN (rule->filters); i++)
+    {
+      vec_add2 (filters, filter, 1);
+      npol_rule_filter_decode (&rule->filters[i], filter);
+    }
+
+  n_matches = clib_net_to_host_u32 (rule->num_entries);
+  for (i = 0; i < n_matches; i++)
+    {
+      vec_add2 (entries, entry, 1);
+      if ((rv = npol_rule_entry_decode (&rule->matches[i], entry)))
+       goto done;
+    }
+
+  rv = npol_rule_update (id, action, filters, entries);
+
+done:
+  vec_free (filters);
+  vec_free (entries);
+  return rv;
+}
+
+/* NAME: rule_create */
+static void
+vl_api_npol_rule_create_t_handler (vl_api_npol_rule_create_t *mp)
+{
+  vl_api_npol_rule_create_reply_t *rmp;
+  npol_main_t *cpm = &npol_main;
+  u32 id = NPOL_INVALID_INDEX;
+  int rv;
+
+  rv = vl_api_npol_rule_update_create_handler (&id, &mp->rule);
+
+  REPLY_MACRO2 (VL_API_NPOL_RULE_CREATE_REPLY,
+               ({ rmp->rule_id = clib_host_to_net_u32 (id); }));
+}
+
+/* NAME: rule_update */
+static void
+vl_api_npol_rule_update_t_handler (vl_api_npol_rule_update_t *mp)
+{
+  vl_api_npol_rule_update_reply_t *rmp;
+  npol_main_t *cpm = &npol_main;
+  u32 id;
+  int rv;
+
+  id = clib_net_to_host_u32 (mp->rule_id);
+  rv = vl_api_npol_rule_update_create_handler (&id, &mp->rule);
+
+  REPLY_MACRO (VL_API_NPOL_RULE_UPDATE_REPLY);
+}
+
+/* NAME: rule_delete */
+static void
+vl_api_npol_rule_delete_t_handler (vl_api_npol_rule_delete_t *mp)
+{
+  vl_api_npol_rule_delete_reply_t *rmp;
+  npol_main_t *cpm = &npol_main;
+  u32 id;
+  int rv;
+
+  id = clib_net_to_host_u32 (mp->rule_id);
+  rv = npol_rule_delete (id);
+
+  REPLY_MACRO (VL_API_NPOL_RULE_DELETE_REPLY);
+}
+
+static int
+vl_api_npol_policy_update_create_handler (u32 *id, u32 n_rules,
+                                         vl_api_npol_policy_item_t *api_rules)
+{
+  npol_policy_rule_t *rules = 0, *rule;
+  int rv;
+
+  for (u32 i = 0; i < n_rules; i++)
+    {
+      vec_add2 (rules, rule, 1);
+      npol_policy_rule_decode (&api_rules[i], rule);
+    }
+
+  rv = npol_policy_update (id, rules);
+
+  vec_free (rules);
+  return rv;
+}
+
+/* NAME: policy_create */
+static void
+vl_api_npol_policy_create_t_handler (vl_api_npol_policy_create_t *mp)
+{
+  vl_api_npol_policy_create_reply_t *rmp;
+  npol_main_t *cpm = &npol_main;
+  u32 id = NPOL_INVALID_INDEX, n_rules;
+  int rv;
+
+  n_rules = clib_net_to_host_u32 (mp->num_items);
+  rv = vl_api_npol_policy_update_create_handler (&id, n_rules, mp->rules);
+
+  REPLY_MACRO2 (VL_API_NPOL_POLICY_CREATE_REPLY,
+               ({ rmp->policy_id = clib_host_to_net_u32 (id); }));
+}
+
+/* NAME: policy_update */
+static void
+vl_api_npol_policy_update_t_handler (vl_api_npol_policy_update_t *mp)
+{
+  vl_api_npol_policy_update_reply_t *rmp;
+  npol_main_t *cpm = &npol_main;
+  u32 id, n_rules;
+  int rv;
+
+  id = clib_net_to_host_u32 (mp->policy_id);
+  n_rules = clib_net_to_host_u32 (mp->num_items);
+  rv = vl_api_npol_policy_update_create_handler (&id, n_rules, mp->rules);
+
+  REPLY_MACRO (VL_API_NPOL_POLICY_UPDATE_REPLY);
+}
+
+/* NAME: policy_delete */
+static void
+vl_api_npol_policy_delete_t_handler (vl_api_npol_policy_delete_t *mp)
+{
+  vl_api_npol_policy_delete_reply_t *rmp;
+  npol_main_t *cpm = &npol_main;
+  u32 id;
+  int rv = 0;
+
+  id = clib_net_to_host_u32 (mp->policy_id);
+  rv = npol_policy_delete (id);
+
+  REPLY_MACRO (VL_API_NPOL_POLICY_DELETE_REPLY);
+}
+
+static void
+npol_interface_config_decode (const vl_api_npol_configure_policies_t *in,
+                             npol_interface_config_t *out)
+{
+  u32 num_rx_policies, num_tx_policies, total_ids, num_profiles;
+  int i = 0;
+
+  num_rx_policies = clib_net_to_host_u32 (in->num_rx_policies);
+  num_tx_policies = clib_net_to_host_u32 (in->num_tx_policies);
+  total_ids = clib_net_to_host_u32 (in->total_ids);
+  num_profiles = total_ids - num_rx_policies - num_tx_policies;
+
+  out->invert_rx_tx = in->invert_rx_tx;
+  out->policy_default_rx = in->policy_default_rx;
+  out->policy_default_tx = in->policy_default_tx;
+  out->profile_default_rx = in->profile_default_rx;
+  out->profile_default_tx = in->profile_default_tx;
+  vec_resize (out->rx_policies, num_rx_policies);
+  for (i = 0; i < num_rx_policies; i++)
+    out->rx_policies[i] = clib_net_to_host_u32 (in->policy_ids[i]);
+  vec_resize (out->tx_policies, num_tx_policies);
+  for (i = 0; i < num_tx_policies; i++)
+    out->tx_policies[i] =
+      clib_net_to_host_u32 (in->policy_ids[num_rx_policies + i]);
+  vec_resize (out->profiles, num_profiles);
+  for (i = 0; i < num_profiles; i++)
+    out->profiles[i] = clib_net_to_host_u32 (
+      in->policy_ids[num_rx_policies + num_tx_policies + i]);
+}
+
+/* NAME: configure_policies */
+static void
+vl_api_npol_configure_policies_t_handler (vl_api_npol_configure_policies_t *mp)
+{
+  npol_main_t *cpm = &npol_main;
+  npol_interface_config_t _conf = { 0 }, *conf = &_conf;
+  vl_api_npol_configure_policies_reply_t *rmp;
+  u32 sw_if_index;
+  int rv = -1;
+
+  sw_if_index = clib_net_to_host_u32 (mp->sw_if_index);
+  npol_interface_config_decode (mp, conf);
+
+  rv = npol_configure_policies (sw_if_index, conf);
+
+  REPLY_MACRO (VL_API_NPOL_CONFIGURE_POLICIES_REPLY);
+}
+
+/* Set up the API message handling tables */
+#include <vnet/format_fns.h>
+#include <npol/npol.api.c>
+
+#include <vat/vat.h>
+#include <vlibapi/vat_helper_macros.h>
+
+/* Declare message IDs */
+#include <acl/acl.api_enum.h>
+#include <acl/acl.api_types.h>
+#undef vl_print
+#define vl_print(handle, ...)
+#undef vl_print
+#define vl_endianfun /* define message structures */
+#include <acl/acl.api.h>
+#undef vl_endianfun
+
+static clib_error_t *
+calpol_init (vlib_main_t *vm)
+{
+  npol_main_t *cpm = &npol_main;
+
+  cpm->msg_id_base = setup_message_id_table ();
+
+  return (NULL);
+}
+
+static clib_error_t *
+calpol_plugin_config (vlib_main_t *vm, unformat_input_t *input)
+{
+  return NULL;
+}
+
+VLIB_PLUGIN_REGISTER () = {
+  .version = VPP_BUILD_VER,
+  .description = "Network Policy",
+};
+
+VLIB_CONFIG_FUNCTION (calpol_plugin_config, "calico-policy-plugin");
+
+VLIB_INIT_FUNCTION (calpol_init) = {
+  .runs_after = VLIB_INITS ("acl_init"),
+};
diff --git a/src/plugins/npol/npol_format.c b/src/plugins/npol/npol_format.c
new file mode 100644 (file)
index 0000000..e79e2d7
--- /dev/null
@@ -0,0 +1,442 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <npol/npol.h>
+#include <npol/npol_rule.h>
+#include <npol/npol_policy.h>
+#include <npol/npol_ipset.h>
+
+u8 *
+format_npol_action (u8 *s, va_list *args)
+{
+  int action = va_arg (*args, int);
+  switch (action)
+    {
+    case NPOL_ACTION_ALLOW:
+      return format (s, "ALLOW");
+    case NPOL_ACTION_DENY:
+      return format (s, "DENY");
+    default:
+      return format (s, "unknown type %d", action);
+    }
+}
+
+u8 *
+format_npol_ipport (u8 *s, va_list *args)
+{
+  npol_ipport_t *ipport = va_arg (*args, npol_ipport_t *);
+  return format (s, "%U %U;%u", format_ip_protocol, ipport->l4proto,
+                format_ip_address, &ipport->addr, ipport->port);
+}
+
+u8 *
+format_npol_ipset_member (u8 *s, va_list *args)
+{
+  npol_ipset_member_t *member = va_arg (*args, npol_ipset_member_t *);
+  npol_ipset_type_t type = va_arg (*args, npol_ipset_type_t);
+  switch (type)
+    {
+    case IPSET_TYPE_IP:
+      return format (s, "%U", format_ip_address, &member->address);
+    case IPSET_TYPE_IPPORT:
+      return format (s, "%U", format_npol_ipport, &member->ipport);
+    case IPSET_TYPE_NET:
+      return format (s, "%U", format_ip_prefix, &member->prefix);
+    default:
+      return format (s, "unknown type");
+    }
+}
+
+uword
+unformat_npol_ipport (unformat_input_t *input, va_list *args)
+{
+  npol_ipport_t *ipport = va_arg (*args, npol_ipport_t *);
+  u32 proto;
+  u32 port;
+  if (unformat (input, "%U %U %d", unformat_ip_protocol, &proto,
+               unformat_ip_address, &ipport->addr, &port))
+    ;
+  else
+    return 0;
+
+  ipport->port = port;
+  ipport->l4proto = (u8) proto;
+  return 1;
+}
+
+u8 *
+format_npol_ipset_type (u8 *s, va_list *args)
+{
+  npol_ipset_type_t type = va_arg (*args, npol_ipset_type_t);
+  switch (type)
+    {
+    case IPSET_TYPE_IP:
+      return format (s, "ip");
+    case IPSET_TYPE_IPPORT:
+      return format (s, "ip+port");
+    case IPSET_TYPE_NET:
+      return format (s, "prefix");
+    default:
+      return format (s, "unknownipsettype");
+    }
+}
+
+uword
+unformat_npol_ipset_member (unformat_input_t *input, va_list *args)
+{
+  npol_ipset_member_t *member = va_arg (*args, npol_ipset_member_t *);
+  npol_ipset_type_t *type = va_arg (*args, npol_ipset_type_t *);
+  if (unformat_user (input, unformat_ip_prefix, &member->prefix))
+    *type = IPSET_TYPE_NET;
+  else if (unformat_user (input, unformat_ip_address, &member->address))
+    *type = IPSET_TYPE_IP;
+  else if (unformat_user (input, unformat_npol_ipport, &member->ipport))
+    *type = IPSET_TYPE_IPPORT;
+  else
+    return 0;
+
+  return 1;
+}
+
+u8 *
+format_npol_ipset (u8 *s, va_list *args)
+{
+  npol_ipset_t *ipset = va_arg (*args, npol_ipset_t *);
+  npol_ipset_member_t *member;
+
+  if (ipset == NULL)
+    return format (s, "deleted ipset");
+
+  s = format (s, "[ipset#%d;%U;", ipset - npol_ipsets, format_npol_ipset_type,
+             ipset->type);
+
+  pool_foreach (member, ipset->members)
+    s = format (s, "%U,", format_npol_ipset_member, member, ipset->type);
+
+  s = format (s, "]");
+
+  return (s);
+}
+
+u8 *
+format_npol_rule_action (u8 *s, va_list *args)
+{
+  npol_rule_action_t action = va_arg (*args, int);
+  switch (action)
+    {
+    case NPOL_ALLOW:
+      return format (s, "allow");
+    case NPOL_DENY:
+      return format (s, "deny");
+    case NPOL_LOG:
+      return format (s, "log");
+    case NPOL_PASS:
+      return format (s, "pass");
+    default:
+      return format (s, "unknownaction");
+    }
+}
+
+uword
+unformat_npol_rule_action (unformat_input_t *input, va_list *args)
+{
+  npol_rule_action_t *action = va_arg (*args, npol_rule_action_t *);
+  if (unformat (input, "allow"))
+    *action = NPOL_ALLOW;
+  else if (unformat (input, "deny"))
+    *action = NPOL_DENY;
+  else if (unformat (input, "log"))
+    *action = NPOL_LOG;
+  else if (unformat (input, "pass"))
+    *action = NPOL_PASS;
+  else
+    return 0;
+  return 1;
+}
+
+u8 *
+format_npol_rule_port_range (u8 *s, va_list *args)
+{
+  npol_port_range_t *port_range = va_arg (*args, npol_port_range_t *);
+
+  if (port_range->start != port_range->end)
+    s = format (s, "[%u-%u]", port_range->start, port_range->end);
+  else
+    s = format (s, "%u", port_range->start);
+
+  return (s);
+}
+
+u8 *
+format_npol_rule_entry (u8 *s, va_list *args)
+{
+  npol_rule_entry_t *entry = va_arg (*args, npol_rule_entry_t *);
+  npol_ipset_t *ipset;
+
+  s = format (s, "%s", entry->flags & NPOL_IS_SRC ? "src" : "dst");
+  s = format (s, "%s", entry->flags & NPOL_IS_NOT ? "!=" : "==");
+  switch (entry->type)
+    {
+    case NPOL_CIDR:
+      s = format (s, "%U", format_ip_prefix, &entry->data.cidr);
+      break;
+    case NPOL_PORT_RANGE:
+      s =
+       format (s, "%U", format_npol_rule_port_range, &entry->data.port_range);
+      break;
+    case NPOL_IP_SET:
+      ipset = npol_ipsets_get_if_exists (entry->data.set_id);
+      s = format (s, "%U", format_npol_ipset, ipset);
+      break;
+    case NPOL_PORT_IP_SET:
+      ipset = npol_ipsets_get_if_exists (entry->data.set_id);
+      s = format (s, "%U", format_npol_ipset, ipset);
+      break;
+    default:
+      s = format (s, "unknown");
+      break;
+    }
+  return (s);
+}
+
+uword
+unformat_rule_key_flag (unformat_input_t *input, va_list *args)
+{
+  npol_rule_key_flag_t *flags = va_arg (*args, npol_rule_key_flag_t *);
+  if (unformat (input, "src=="))
+    *flags = NPOL_IS_SRC;
+  else if (unformat (input, "src!="))
+    *flags = NPOL_IS_SRC | NPOL_IS_NOT;
+  else if (unformat (input, "dst!="))
+    *flags = NPOL_IS_NOT;
+  else if (unformat (input, "dst=="))
+    *flags = 0;
+  else
+    return 0;
+  return 1;
+}
+
+uword
+unformat_npol_port_range (unformat_input_t *input, va_list *args)
+{
+  npol_port_range_t *port_range = va_arg (*args, npol_port_range_t *);
+  u32 start, end;
+  if (unformat (input, "[%d-%d]", &start, &end))
+    {
+      port_range->start = (u16) start;
+      port_range->end = (u16) end;
+    }
+  else
+    return 0;
+  return 1;
+}
+
+uword
+unformat_npol_rule_entry (unformat_input_t *input, va_list *args)
+{
+  npol_rule_entry_t *entry = va_arg (*args, npol_rule_entry_t *);
+  if (unformat (input, "%U %U", unformat_rule_key_flag, &entry->flags,
+               unformat_ip_prefix, &entry->data.cidr))
+    entry->type = NPOL_CIDR;
+  else if (unformat (input, "%U %U", unformat_rule_key_flag, &entry->flags,
+                    unformat_npol_port_range, &entry->data.port_range))
+    entry->type = NPOL_PORT_RANGE;
+  else if (unformat (input, "%Uset %u", unformat_rule_key_flag, &entry->flags,
+                    &entry->data.set_id))
+    entry->type = NPOL_PORT_IP_SET;
+  else
+    return 0;
+  return 1;
+}
+
+u8 *
+format_npol_rule_filter (u8 *s, va_list *args)
+{
+  npol_rule_filter_t *filter = va_arg (*args, npol_rule_filter_t *);
+  switch (filter->type)
+    {
+    case NPOL_RULE_FILTER_NONE_TYPE:
+      return format (s, "<no filter>");
+    case NPOL_RULE_FILTER_ICMP_TYPE:
+      return format (s, "icmp-type%s=%d", filter->should_match ? "=" : "!",
+                    filter->value);
+    case NPOL_RULE_FILTER_ICMP_CODE:
+      return format (s, "icmp-code%s=%d", filter->should_match ? "=" : "!",
+                    filter->value);
+    case NPOL_RULE_FILTER_L4_PROTO:
+      return format (s, "proto%s=%U", filter->should_match ? "=" : "!",
+                    format_ip_protocol, filter->value);
+    default:
+      return format (s, "unknown");
+    }
+}
+
+uword
+unformat_npol_should_match (unformat_input_t *input, va_list *args)
+{
+  u8 *should_match = va_arg (*args, u8 *);
+  if (unformat (input, "=="))
+    *should_match = 1;
+  else if (unformat (input, "!="))
+    *should_match = 0;
+  else
+    return 0;
+  return 1;
+}
+
+uword
+unformat_npol_rule_filter (unformat_input_t *input, va_list *args)
+{
+  u8 tmp_value;
+  npol_rule_filter_t *filter = va_arg (*args, npol_rule_filter_t *);
+  if (unformat (input, "icmp-type%U%d", unformat_npol_should_match,
+               &filter->should_match, &filter->value))
+    filter->type = NPOL_RULE_FILTER_ICMP_TYPE;
+  else if (unformat (input, "icmp-code%U%d", unformat_npol_should_match,
+                    &filter->should_match, &filter->value))
+    filter->type = NPOL_RULE_FILTER_ICMP_CODE;
+  else if (unformat (input, "proto%U%U", unformat_npol_should_match,
+                    &filter->should_match, unformat_ip_protocol, &tmp_value))
+    {
+      filter->value = tmp_value;
+      filter->type = NPOL_RULE_FILTER_L4_PROTO;
+    }
+  else
+    return 0;
+  return 1;
+}
+
+u8 *
+format_npol_rule (u8 *s, va_list *args)
+{
+  npol_rule_t *rule = va_arg (*args, npol_rule_t *);
+  npol_rule_filter_t *filter;
+  npol_rule_entry_t *entry, *entries;
+
+  if (rule == NULL)
+    return format (s, "deleted rule");
+
+  s = format (s, "[rule#%d;%U][", rule - npol_rules, format_npol_rule_action,
+             rule->action);
+
+  /* filters */
+  vec_foreach (filter, rule->filters)
+    {
+      if (filter->type != NPOL_RULE_FILTER_NONE_TYPE)
+       s = format (s, "%U,", format_npol_rule_filter, filter);
+    }
+
+  entries = npol_rule_get_entries (rule);
+  vec_foreach (entry, entries)
+    s = format (s, "%U,", format_npol_rule_entry, entry);
+  vec_free (entries);
+  s = format (s, "]");
+
+  return (s);
+}
+
+u8 *
+format_npol_policy (u8 *s, va_list *args)
+{
+  npol_policy_t *policy = va_arg (*args, npol_policy_t *);
+  int indent = va_arg (*args, int);
+  int verbose = va_arg (*args, int);
+  int invert_rx_tx = va_arg (*args, int);
+  u32 *rule_id;
+
+  if (policy == NULL)
+    return format (s, "deleted policy");
+
+  if (verbose)
+    {
+      s = format (s, "[policy#%u]\n", policy - npol_policies);
+      npol_rule_t *rule;
+      if (verbose != NPOL_POLICY_ONLY_RX)
+       vec_foreach (rule_id, policy->rule_ids[VLIB_TX ^ invert_rx_tx])
+         {
+           rule = npol_rule_get_if_exists (*rule_id);
+           s = format (s, "%Utx:%U\n", format_white_space, indent + 2,
+                       format_npol_rule, rule);
+         }
+      if (verbose != NPOL_POLICY_ONLY_TX)
+       vec_foreach (rule_id, policy->rule_ids[VLIB_RX ^ invert_rx_tx])
+         {
+           rule = npol_rule_get_if_exists (*rule_id);
+           s = format (s, "%Urx:%U\n", format_white_space, indent + 2,
+                       format_npol_rule, rule);
+         }
+    }
+  else
+    {
+      s = format (s, "[policy#%u] rx-rules:%d tx-rules:%d\n",
+                 policy - npol_policies,
+                 vec_len (policy->rule_ids[VLIB_RX ^ invert_rx_tx]),
+                 vec_len (policy->rule_ids[VLIB_TX ^ invert_rx_tx]));
+    }
+
+  return (s);
+}
+
+u8 *
+format_npol_interface (u8 *s, va_list *args)
+{
+  u32 sw_if_index = va_arg (*args, u32);
+  npol_interface_config_t *conf = va_arg (*args, npol_interface_config_t *);
+  vnet_main_t *vnm = vnet_get_main ();
+  npol_policy_t *policy = NULL;
+  u32 *rx_policies = conf->rx_policies;
+  u32 *tx_policies = conf->tx_policies;
+  u32 i;
+
+  s = format (s, "[%U sw_if_index=%u ", format_vnet_sw_if_index_name, vnm,
+             sw_if_index, sw_if_index);
+  if (conf->invert_rx_tx)
+    {
+      s = format (s, "inverted");
+      rx_policies = conf->tx_policies;
+      tx_policies = conf->rx_policies;
+    }
+  ip4_address_t *ip4 = 0;
+  ip4 = ip4_interface_first_address (&ip4_main, sw_if_index, 0);
+  if (ip4)
+    s = format (s, " addr=%U", format_ip4_address, ip4);
+  ip6_address_t *ip6 = 0;
+  ip6 = ip6_interface_first_address (&ip6_main, sw_if_index);
+  if (ip6)
+    s = format (s, " addr6=%U", format_ip6_address, ip6);
+  s = format (s, "]\n");
+  if (vec_len (rx_policies))
+    {
+      s = format (s, "  rx:\n");
+    }
+  s = format (s, "   rx-policy-default:%d rx-profile-default:%d \n",
+             conf->policy_default_rx, conf->profile_default_rx);
+  vec_foreach_index (i, rx_policies)
+    {
+      policy = npol_policy_get_if_exists (rx_policies[i]);
+      s = format (s, "    %U", format_npol_policy, policy, 4 /* indent */,
+                 NPOL_POLICY_ONLY_RX, conf->invert_rx_tx);
+    }
+  if (vec_len (tx_policies))
+    {
+      s = format (s, "  tx:\n");
+    }
+  s = format (s, "   tx-policy-default:%d tx-profile-default:%d \n",
+             conf->policy_default_tx, conf->profile_default_tx);
+  vec_foreach_index (i, tx_policies)
+    {
+      policy = npol_policy_get_if_exists (tx_policies[i]);
+      s = format (s, "    %U", format_npol_policy, policy, 4 /* indent */,
+                 NPOL_POLICY_ONLY_TX, conf->invert_rx_tx);
+    }
+  if (vec_len (conf->profiles))
+    s = format (s, "  profiles:\n");
+  vec_foreach_index (i, conf->profiles)
+    {
+      policy = npol_policy_get_if_exists (conf->profiles[i]);
+      s = format (s, "    %U", format_npol_policy, policy, 4 /* indent */,
+                 NPOL_POLICY_VERBOSE, conf->invert_rx_tx);
+    }
+  return s;
+}
diff --git a/src/plugins/npol/npol_format.h b/src/plugins/npol/npol_format.h
new file mode 100644 (file)
index 0000000..a9c9701
--- /dev/null
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#ifndef included_npol_format_h
+#define included_npol_format_h
+
+u8 *format_npol_interface (u8 *s, va_list *args);
+u8 *format_npol_policy (u8 *s, va_list *args);
+u8 *format_npol_ipset (u8 *s, va_list *args);
+u8 *format_npol_rule (u8 *s, va_list *args);
+uword unformat_npol_ipset_member (unformat_input_t *input, va_list *args);
+uword unformat_npol_rule_entry (unformat_input_t *input, va_list *args);
+uword unformat_npol_rule_action (unformat_input_t *input, va_list *args);
+uword unformat_npol_rule_filter (unformat_input_t *input, va_list *args);
+u8 *format_npol_rule_filter (u8 *s, va_list *args);
+u8 *format_npol_action (u8 *s, va_list *args);
+
+#endif
\ No newline at end of file
diff --git a/src/plugins/npol/npol_interface.c b/src/plugins/npol/npol_interface.c
new file mode 100644 (file)
index 0000000..1fba8b7
--- /dev/null
@@ -0,0 +1,224 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <npol/npol.h>
+#include <npol/npol_match.h>
+#include <npol/npol_policy.h>
+#include <npol/npol_format.h>
+
+uword unformat_sw_if_index (unformat_input_t *input, va_list *args);
+
+npol_interface_config_t *npol_interface_configs;
+
+int
+npol_unconfigure_policies (u32 sw_if_index)
+{
+  npol_interface_config_t *conf;
+  conf = vec_elt_at_index (npol_interface_configs, sw_if_index);
+  if (!conf->enabled)
+    return 0;
+
+  conf = vec_elt_at_index (npol_interface_configs, sw_if_index);
+  vec_free (conf->rx_policies);
+  vec_free (conf->tx_policies);
+  vec_free (conf->profiles);
+
+  conf->enabled = 0;
+  return 0;
+}
+
+int
+npol_configure_policies (u32 sw_if_index, npol_interface_config_t *new_conf)
+{
+  npol_interface_config_t *conf;
+  u32 *idx;
+
+  if (!vnet_sw_interface_is_valid (vnet_get_main (), sw_if_index))
+    return VNET_API_ERROR_INVALID_SW_IF_INDEX;
+
+  vec_validate_init_empty (npol_interface_configs, sw_if_index,
+                          (npol_interface_config_t){ 0 });
+  conf = vec_elt_at_index (npol_interface_configs, sw_if_index);
+
+  vec_foreach (idx, new_conf->rx_policies)
+    if (pool_is_free_index (npol_policies, *idx))
+      goto error;
+  vec_foreach (idx, new_conf->tx_policies)
+    if (pool_is_free_index (npol_policies, *idx))
+      goto error;
+  vec_foreach (idx, new_conf->profiles)
+    if (pool_is_free_index (npol_policies, *idx))
+      goto error;
+
+  if (conf->enabled)
+    {
+      vec_free (conf->rx_policies);
+      vec_free (conf->tx_policies);
+      vec_free (conf->profiles);
+    }
+  *conf = *new_conf;
+  conf->enabled = 1;
+  return 0;
+
+error:
+  vec_free (new_conf->rx_policies);
+  vec_free (new_conf->tx_policies);
+  vec_free (new_conf->profiles);
+  return 1;
+}
+
+static clib_error_t *
+npol_sw_interface_add_del (vnet_main_t *vnm, u32 sw_if_index, u32 is_add)
+{
+  int rv;
+  if (is_add)
+    vec_validate_init_empty (npol_interface_configs, sw_if_index,
+                            (npol_interface_config_t){ 0 });
+  else
+    {
+      rv = npol_unconfigure_policies (sw_if_index);
+      if (rv)
+       return clib_error_return (
+         0, "Error calling npol_unconfigure_policies %d", rv);
+    }
+  return NULL;
+}
+
+VNET_SW_INTERFACE_ADD_DEL_FUNCTION (npol_sw_interface_add_del);
+
+static clib_error_t *
+npol_interface_show_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                           vlib_cli_command_t *cmd)
+{
+  u32 sw_if_index;
+  npol_interface_config_t *conf;
+  vlib_cli_output (vm, "Interfaces with policies configured:");
+  vec_foreach_index (sw_if_index, npol_interface_configs)
+    {
+      conf = &npol_interface_configs[sw_if_index];
+      if (conf->enabled)
+       {
+         vlib_cli_output (vm, "%U", format_npol_interface, sw_if_index, conf);
+       }
+    }
+  return NULL;
+}
+
+VLIB_CLI_COMMAND (npol_policies_show_cmd, static) = {
+  .path = "show npol interfaces",
+  .function = npol_interface_show_cmd_fn,
+  .short_help = "show npol interfaces",
+};
+
+static clib_error_t *
+npol_interface_clear_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                            vlib_cli_command_t *cmd)
+{
+  u32 sw_if_index = NPOL_INVALID_INDEX;
+  clib_error_t *error = 0;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "%U", unformat_sw_if_index, NULL, &sw_if_index))
+       ;
+      else if (unformat (input, "sw_if_index %d", &sw_if_index))
+       ;
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  if (sw_if_index == NPOL_INVALID_INDEX)
+    {
+      error = clib_error_return (0, "interface not specified");
+      goto done;
+    }
+
+  rv = npol_unconfigure_policies (sw_if_index);
+  if (rv)
+    error =
+      clib_error_return (0, "npol_unconfigure_policies errored with %d", rv);
+  else
+    vlib_cli_output (vm, "npol interface %d cleared", sw_if_index);
+
+done:
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_interface_clear_cmd, static) = {
+  .path = "npol interface clear",
+  .function = npol_interface_clear_cmd_fn,
+  .short_help = "npol interface clear [interface | sw_if_index N]",
+};
+
+static clib_error_t *
+npol_interface_configure_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                                vlib_cli_command_t *cmd)
+{
+  npol_interface_config_t _conf = { 0 }, *conf = &_conf;
+  clib_error_t *error = 0;
+  u32 sw_if_index = NPOL_INVALID_INDEX;
+  u32 tmp;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "%U", unformat_sw_if_index, NULL, &sw_if_index))
+       ;
+      else if (unformat (input, "sw_if_index %d", &sw_if_index))
+       ;
+      else if (unformat (input, "rx %d", &tmp))
+       vec_add1 (conf->rx_policies, tmp);
+      else if (unformat (input, "tx %d", &tmp))
+       vec_add1 (conf->tx_policies, tmp);
+      else if (unformat (input, "profiles %d", &tmp))
+       vec_add1 (conf->profiles, tmp);
+      else if (unformat (input, "rx-policy-def %d", &tmp))
+       conf->policy_default_rx = tmp;
+      else if (unformat (input, "rx-profile-def %d", &tmp))
+       conf->profile_default_rx = tmp;
+      else if (unformat (input, "tx-policy-def %d", &tmp))
+       conf->policy_default_tx = tmp;
+      else if (unformat (input, "tx-profile-def %d", &tmp))
+       conf->profile_default_tx = tmp;
+      else if (unformat (input, "invert"))
+       conf->invert_rx_tx = 1;
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  if (sw_if_index == NPOL_INVALID_INDEX)
+    {
+      error = clib_error_return (0, "interface not specified");
+      goto done;
+    }
+
+  rv = npol_configure_policies (sw_if_index, conf);
+  if (rv)
+    error =
+      clib_error_return (0, "npol_configure_policies errored with %d", rv);
+  else
+    vlib_cli_output (vm, "npol interface %d configured", sw_if_index);
+
+done:
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_interface_configure_cmd, static) = {
+  .path = "npol interface configure",
+  .function = npol_interface_configure_cmd_fn,
+  .short_help = "npol interface configure [interface | sw_if_index N] rx "
+               "<num_rx> tx <num_tx> rx-policy-def <rx-policy-def> "
+               "tx-policy-def <tx-policy-def> "
+               "rx-profile-def <rx-profile-def> tx-profile-def "
+               "<tx-profile-def> [invert] <policy_id> ...",
+};
diff --git a/src/plugins/npol/npol_interface.h b/src/plugins/npol/npol_interface.h
new file mode 100644 (file)
index 0000000..27a1e5f
--- /dev/null
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#ifndef included_npol_interface_h
+#define included_npol_interface_h
+
+#include <vppinfra/clib.h>
+
+typedef struct
+{
+  /*
+   * vec of policies indexes to apply on rx
+   */
+  u32 *rx_policies;
+  /*
+   * vec of policies indexes to apply on tx
+   */
+  u32 *tx_policies;
+  /*
+   *vec of policies indexes to use as profiles
+   */
+  u32 *profiles;
+  /* set to 1 when policy is used for interface
+   * as policy confs are stored in a sw_if_index
+   * indexed vector, initialized to zero
+   */
+  u8 enabled;
+  /*
+   * Should we invert RX and TX
+   */
+  u8 invert_rx_tx;
+  /*
+   * Default action to apply after all policies on RX
+   */
+  u8 policy_default_rx;
+  /*
+   * Default action to apply after all policies on TX
+   */
+  u8 policy_default_tx;
+  /*
+   * Default action to apply after profiles on RX
+   */
+  u8 profile_default_rx;
+  /*
+   * Default action to apply after profiles on TX
+   */
+  u8 profile_default_tx;
+} npol_interface_config_t;
+
+extern npol_interface_config_t *npol_interface_configs;
+
+int npol_configure_policies (u32 sw_if_index, npol_interface_config_t *conf);
+
+#endif
diff --git a/src/plugins/npol/npol_ipset.c b/src/plugins/npol/npol_ipset.c
new file mode 100644 (file)
index 0000000..52556ac
--- /dev/null
@@ -0,0 +1,335 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <npol/npol.h>
+#include <npol/npol_ipset.h>
+#include <npol/npol_format.h>
+
+npol_ipset_t *npol_ipsets;
+
+npol_ipset_t *
+npol_ipsets_get_if_exists (u32 index)
+{
+  if (pool_is_free_index (npol_ipsets, index))
+    return (NULL);
+  return pool_elt_at_index (npol_ipsets, index);
+}
+
+u32
+npol_ipset_create (npol_ipset_type_t type)
+{
+  npol_ipset_t *ipset;
+  pool_get (npol_ipsets, ipset);
+  ipset->type = type;
+  ipset->members = NULL;
+  return ipset - npol_ipsets;
+}
+
+int
+npol_ipset_delete (u32 id)
+{
+  npol_ipset_t *ipset;
+  ipset = npol_ipsets_get_if_exists (id);
+  if (NULL == ipset)
+    return VNET_API_ERROR_NO_SUCH_ENTRY;
+
+  pool_free (ipset->members);
+  pool_put (npol_ipsets, ipset);
+  return 0;
+}
+
+int
+npol_ipset_get_type (u32 id, npol_ipset_type_t *type)
+{
+  npol_ipset_t *ipset;
+  ipset = npol_ipsets_get_if_exists (id);
+  if (NULL == ipset)
+    return VNET_API_ERROR_NO_SUCH_ENTRY;
+
+  *type = ipset->type;
+  return 0;
+}
+
+int
+npol_ipset_add_member (u32 ipset_id, npol_ipset_member_t *member)
+{
+  npol_ipset_member_t *m;
+  npol_ipset_t *ipset = &npol_ipsets[ipset_id];
+
+  if (pool_is_free (npol_ipsets, ipset))
+    {
+      return 1;
+    }
+
+  /* zero so that we can memcmp later */
+  pool_get_zero (ipset->members, m);
+  clib_memcpy (m, member, sizeof (*m));
+  return 0;
+}
+
+static size_t
+npol_ipset_member_cmp (npol_ipset_member_t *m1, npol_ipset_member_t *m2,
+                      npol_ipset_type_t type)
+{
+  switch (type)
+    {
+    case IPSET_TYPE_IP:
+      return ip_address_cmp (&m1->address, &m2->address);
+    case IPSET_TYPE_IPPORT:
+      return ((m1->ipport.port == m2->ipport.port) &&
+             (m1->ipport.l4proto == m2->ipport.l4proto) &&
+             ip_address_cmp (&m1->ipport.addr, &m2->ipport.addr));
+    case IPSET_TYPE_NET:
+      return ip_prefix_cmp (&m1->prefix, &m2->prefix);
+    default:
+      return 1;
+    }
+}
+
+int
+npol_ipset_del_member (u32 id, npol_ipset_member_t *member)
+{
+  index_t *index, *indexes = NULL;
+  npol_ipset_member_t *m;
+  npol_ipset_t *ipset;
+
+  ipset = npol_ipsets_get_if_exists (id);
+  if (NULL == ipset)
+    return VNET_API_ERROR_NO_SUCH_ENTRY;
+
+  pool_foreach (m, ipset->members)
+    {
+      if (!npol_ipset_member_cmp (m, member, ipset->type))
+       vec_add1 (indexes, m - ipset->members);
+    }
+
+  vec_foreach (index, indexes)
+    pool_put_index (ipset->members, *index);
+  vec_free (indexes);
+
+  return 0;
+}
+
+static clib_error_t *
+npol_ipsets_show_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                        vlib_cli_command_t *cmd)
+{
+  npol_ipset_t *ipset;
+
+  pool_foreach (ipset, npol_ipsets)
+    vlib_cli_output (vm, "%U", format_npol_ipset, ipset);
+
+  return 0;
+}
+
+VLIB_CLI_COMMAND (npol_ipsets_show_cmd, static) = {
+  .path = "show npol ipsets",
+  .function = npol_ipsets_show_cmd_fn,
+  .short_help = "show npol ipsets",
+};
+
+static clib_error_t *
+npol_ipsets_add_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                       vlib_cli_command_t *cmd)
+{
+  npol_ipset_member_t tmp, *members = 0, *member;
+  clib_error_t *error = 0;
+  npol_ipset_type_t type;
+  npol_ipset_t *ipset;
+  u32 id;
+  int rv;
+
+  id = npol_ipset_create ((npol_ipset_type_t) ~0);
+  vlib_cli_output (vm, "npol ipset %d added", id);
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "%U", unformat_npol_ipset_member, &tmp, &type))
+       vec_add1 (members, tmp);
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  ipset = pool_elt_at_index (npol_ipsets, id);
+  ipset->type = type;
+
+  vec_foreach (member, members)
+    {
+      rv = npol_ipset_add_member (id, member);
+      if (rv)
+       error = clib_error_return (0, "npol_ipset_add_member error %d", rv);
+    }
+
+done:
+  vec_free (members);
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_ipsets_add_cmd, static) = {
+  .path = "npol ipset add",
+  .function = npol_ipsets_add_cmd_fn,
+  .short_help = "npol ipset add [prefix|proto ip port|ip]",
+};
+
+static clib_error_t *
+npol_ipsets_del_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                       vlib_cli_command_t *cmd)
+{
+  clib_error_t *error = 0;
+  u32 id = NPOL_INVALID_INDEX;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "%u", &id))
+       ;
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  if (NPOL_INVALID_INDEX == id)
+    {
+      error = clib_error_return (0, "missing ipset id");
+      goto done;
+    }
+
+  rv = npol_ipset_delete (id);
+  if (rv)
+    error = clib_error_return (0, "npol_ipset_delete errored with %d", rv);
+
+done:
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_ipsets_del_cmd, static) = {
+  .path = "npol ipset del",
+  .function = npol_ipsets_del_cmd_fn,
+  .short_help = "npol ipset del [id]",
+};
+
+static clib_error_t *
+npol_ipsets_add_member_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                              vlib_cli_command_t *cmd)
+{
+  npol_ipset_member_t tmp, *members = 0, *member;
+  u32 id = NPOL_INVALID_INDEX;
+  clib_error_t *error = 0;
+  npol_ipset_type_t type;
+  npol_ipset_t *ipset;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "id %u", &id))
+       ;
+      else if (unformat (input, "%U", unformat_npol_ipset_member, &tmp, &type))
+       vec_add1 (members, tmp);
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  if (NPOL_INVALID_INDEX == id)
+    {
+      error = clib_error_return (0, "missing ipset id");
+      goto done;
+    }
+
+  ipset = npol_ipsets_get_if_exists (id);
+  if (NULL == ipset)
+    return clib_error_return (0, "ipset not found");
+  if (ipset->type != type && ~0 != ipset->type)
+    {
+      error = clib_error_return (0, "cannot change ipset type");
+      goto done;
+    }
+  ipset->type = type;
+
+  vec_foreach (member, members)
+    {
+      rv = npol_ipset_add_member (id, member);
+      if (rv)
+       error = clib_error_return (0, "npol_ipset_add_member error %d", rv);
+    }
+
+done:
+  vec_free (members);
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_ipsets_add_member_cmd, static) = {
+  .path = "npol ipset add member",
+  .function = npol_ipsets_add_member_cmd_fn,
+  .short_help = "npol ipset add member [id] [prefix]",
+};
+
+static clib_error_t *
+npol_ipsets_del_member_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                              vlib_cli_command_t *cmd)
+{
+  clib_error_t *error = 0;
+  u32 id = NPOL_INVALID_INDEX;
+  npol_ipset_type_t type;
+  npol_ipset_member_t tmp, *members = 0, *member;
+  npol_ipset_t *ipset;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "id %u", &id))
+       ;
+      else if (unformat (input, "%U", unformat_npol_ipset_member, &tmp, &type))
+       vec_add1 (members, tmp);
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  if (NPOL_INVALID_INDEX == id)
+    {
+      error = clib_error_return (0, "missing ipset id");
+      goto done;
+    }
+
+  ipset = npol_ipsets_get_if_exists (id);
+  if (NULL == ipset)
+    return clib_error_return (0, "ipset not found");
+  if (ipset->type != type)
+    {
+      error = clib_error_return (0, "wrong member type");
+      goto done;
+    }
+
+  vec_foreach (member, members)
+    {
+      rv = npol_ipset_del_member (id, member);
+      if (rv)
+       error =
+         clib_error_return (0, "npol_ipset_del_member errored with %d", rv);
+    }
+
+done:
+  vec_free (members);
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_ipsets_del_member_cmd, static) = {
+  .path = "npol ipset del member",
+  .function = npol_ipsets_del_member_cmd_fn,
+  .short_help = "npol ipset del member [id] [prefix]",
+};
diff --git a/src/plugins/npol/npol_ipset.h b/src/plugins/npol/npol_ipset.h
new file mode 100644 (file)
index 0000000..5093248
--- /dev/null
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#ifndef included_npol_ipset_h
+#define included_npol_ipset_h
+
+#include <npol/npol.h>
+
+typedef enum
+{
+  IPSET_TYPE_IP = 0,
+  IPSET_TYPE_IPPORT = 1,
+  IPSET_TYPE_NET = 2
+} npol_ipset_type_t;
+
+typedef struct
+{
+  ip_address_t addr;
+  u16 port;
+  u8 l4proto;
+} npol_ipport_t;
+
+typedef union
+{
+  ip_address_t address;
+  npol_ipport_t ipport;
+  ip_prefix_t prefix;
+} npol_ipset_member_t;
+
+typedef struct
+{
+  npol_ipset_type_t type;
+  npol_ipset_member_t *members;
+} npol_ipset_t;
+
+u32 npol_ipset_create (npol_ipset_type_t type);
+int npol_ipset_delete (u32 id);
+
+int npol_ipset_add_member (u32 ipset_id, npol_ipset_member_t *member);
+int npol_ipset_del_member (u32 ipset_id, npol_ipset_member_t *member);
+
+int npol_ipset_get_type (u32 id, npol_ipset_type_t *type);
+npol_ipset_t *npol_ipsets_get_if_exists (u32 index);
+
+extern npol_ipset_t *npol_ipsets;
+
+#endif
diff --git a/src/plugins/npol/npol_match.c b/src/plugins/npol/npol_match.c
new file mode 100644 (file)
index 0000000..645e003
--- /dev/null
@@ -0,0 +1,749 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <vnet/ip/ip.h>
+
+#include <npol/npol.h>
+#include <npol/npol_match.h>
+
+always_inline u8
+ip_ipset_contains_ip4 (npol_ipset_t *ipset, ip4_address_t *addr)
+{
+  ASSERT (ipset->type == IPSET_TYPE_IP);
+  npol_ipset_member_t *member;
+  pool_foreach (member, ipset->members)
+    {
+      if (member->address.version != AF_IP4)
+       continue;
+      if (!ip4_address_compare (addr, &ip_addr_v4 (&member->address)))
+       return 1;
+    }
+  return 0;
+}
+
+always_inline u8
+ip_ipset_contains_ip6 (npol_ipset_t *ipset, ip6_address_t *addr)
+{
+  ASSERT (ipset->type == IPSET_TYPE_IP);
+  npol_ipset_member_t *member;
+  pool_foreach (member, ipset->members)
+    {
+      if (member->address.version != AF_IP6)
+       continue;
+      if (!ip6_address_compare (addr, &ip_addr_v6 (&member->address)))
+       return 1;
+    }
+  return 0;
+}
+
+always_inline u8
+net_ipset_contains_ip4 (npol_ipset_t *ipset, ip4_address_t *addr)
+{
+  ASSERT (ipset->type == IPSET_TYPE_NET);
+  npol_ipset_member_t *member;
+  pool_foreach (member, ipset->members)
+    {
+      if (member->prefix.addr.version != AF_IP4)
+       continue;
+      if (ip4_destination_matches_route (&ip4_main, addr,
+                                        &ip_addr_v4 (&member->prefix.addr),
+                                        member->prefix.len))
+       {
+         return 1;
+       }
+    }
+  return 0;
+}
+
+always_inline u8
+net_ipset_contains_ip6 (npol_ipset_t *ipset, ip6_address_t *addr)
+{
+  ASSERT (ipset->type == IPSET_TYPE_NET);
+  npol_ipset_member_t *member;
+  pool_foreach (member, ipset->members)
+    {
+      if (member->prefix.addr.version != AF_IP6)
+       continue;
+      if (ip6_destination_matches_route (&ip6_main, addr,
+                                        &ip_addr_v6 (&member->prefix.addr),
+                                        member->prefix.len))
+       {
+         return 1;
+       }
+    }
+  return 0;
+}
+
+always_inline u8
+ipset_contains_ip4 (npol_ipset_t *ipset, ip4_address_t *addr)
+{
+  switch (ipset->type)
+    {
+    case IPSET_TYPE_IP:
+      return ip_ipset_contains_ip4 (ipset, addr);
+    case IPSET_TYPE_NET:
+      return net_ipset_contains_ip4 (ipset, addr);
+    default:
+      clib_warning ("Wrong ipset type");
+    }
+  return 0;
+}
+
+always_inline u8
+ipset_contains_ip6 (npol_ipset_t *ipset, ip6_address_t *addr)
+{
+  switch (ipset->type)
+    {
+    case IPSET_TYPE_IP:
+      return ip_ipset_contains_ip6 (ipset, addr);
+    case IPSET_TYPE_NET:
+      return net_ipset_contains_ip6 (ipset, addr);
+    default:
+      clib_warning ("Wrong ipset type");
+    }
+  return 0;
+}
+
+always_inline u8
+ipport_ipset_contains_ip4 (npol_ipset_t *ipset, ip4_address_t *addr,
+                          u8 l4proto, u16 port)
+{
+  ASSERT (ipset->type == IPSET_TYPE_IPPORT);
+  npol_ipset_member_t *member;
+  pool_foreach (member, ipset->members)
+    {
+      if (member->ipport.addr.version != AF_IP4)
+       continue;
+      if (l4proto == member->ipport.l4proto && port == member->ipport.port &&
+         !ip4_address_compare (addr, &ip_addr_v4 (&member->ipport.addr)))
+       {
+         return 1;
+       }
+    }
+  return 0;
+}
+
+always_inline u8
+ipport_ipset_contains_ip6 (npol_ipset_t *ipset, ip6_address_t *addr,
+                          u8 l4proto, u16 port)
+{
+  ASSERT (ipset->type == IPSET_TYPE_IPPORT);
+  npol_ipset_member_t *member;
+  pool_foreach (member, ipset->members)
+    {
+      if (member->ipport.addr.version != AF_IP6)
+       continue;
+      if (l4proto == member->ipport.l4proto && port == member->ipport.port &&
+         !ip6_address_compare (addr, &ip_addr_v6 (&member->ipport.addr)))
+       {
+         return 1;
+       }
+    }
+  return 0;
+}
+
+always_inline int
+npol_match_rule (npol_rule_t *rule, u32 is_ip6, fa_5tuple_t *pkt_5tuple)
+{
+  ip4_address_t *src_ip4 = &pkt_5tuple->ip4_addr[SRC];
+  ip4_address_t *dst_ip4 = &pkt_5tuple->ip4_addr[DST];
+  ip6_address_t *src_ip6 = &pkt_5tuple->ip6_addr[SRC];
+  ip6_address_t *dst_ip6 = &pkt_5tuple->ip6_addr[DST];
+  u8 l4proto = pkt_5tuple->l4.proto;
+  u16 src_port = pkt_5tuple->l4.port[SRC];
+  u16 dst_port = pkt_5tuple->l4.port[DST];
+  u16 type = pkt_5tuple->l4.port[0];
+  u16 code = pkt_5tuple->l4.port[1];
+
+  npol_rule_filter_t *filter;
+  vec_foreach (filter, rule->filters)
+    {
+      switch (filter->type)
+       {
+       case NPOL_RULE_FILTER_NONE_TYPE:
+         break;
+       case NPOL_RULE_FILTER_L4_PROTO:
+         if (filter->should_match && filter->value != l4proto)
+           return -1;
+         if (!filter->should_match && filter->value == l4proto)
+           return -2;
+         break;
+       case NPOL_RULE_FILTER_ICMP_TYPE:
+         if (l4proto == IP_PROTOCOL_ICMP || l4proto == IP_PROTOCOL_ICMP6)
+           {
+             if (filter->should_match && filter->value != type)
+               return -3;
+             if (!filter->should_match && filter->value == type)
+               return -4;
+           }
+         else
+           /* A rule with an ICMP type / code specified doesn't match a
+            * non-icmp packet
+            */
+           return -5;
+         break;
+       case NPOL_RULE_FILTER_ICMP_CODE:
+         if (l4proto == IP_PROTOCOL_ICMP || l4proto == IP_PROTOCOL_ICMP6)
+           {
+             if (filter->should_match && filter->value != code)
+               return -6;
+             if (!filter->should_match && filter->value == code)
+               return -7;
+           }
+         else
+           /* A rule with an ICMP type / code specified doesn't match a
+            * non-icmp packet
+            */
+           return -8;
+         break;
+       default:
+         break;
+       }
+    }
+
+  /* prefixes */
+  if (rule->prefixes[NPOL_SRC])
+    {
+      ip_prefix_t *prefix;
+      u8 found = 0;
+      vec_foreach (prefix, rule->prefixes[NPOL_SRC])
+       {
+         u8 pfx_af = ip_prefix_version (prefix);
+         if (is_ip6 && pfx_af == AF_IP6)
+           {
+             if (ip6_destination_matches_route (&ip6_main, src_ip6,
+                                                &ip_addr_v6 (&prefix->addr),
+                                                prefix->len))
+               {
+                 found = 1;
+                 break;
+               }
+           }
+         else if (!is_ip6 && pfx_af == AF_IP4)
+           {
+             if (ip4_destination_matches_route (&ip4_main, src_ip4,
+                                                &ip_addr_v4 (&prefix->addr),
+                                                prefix->len))
+               {
+                 found = 1;
+                 break;
+               }
+           }
+       }
+      if (!found)
+       {
+         return -9;
+       }
+    }
+
+  if (rule->prefixes[NPOL_NOT_SRC])
+    {
+      ip_prefix_t *prefix;
+      vec_foreach (prefix, rule->prefixes[NPOL_NOT_SRC])
+       {
+         u8 pfx_af = ip_prefix_version (prefix);
+         if (is_ip6 && pfx_af == AF_IP6)
+           {
+             if (ip6_destination_matches_route (&ip6_main, src_ip6,
+                                                &ip_addr_v6 (&prefix->addr),
+                                                prefix->len))
+               {
+                 return -10;
+               }
+           }
+         else if (!is_ip6 && pfx_af == AF_IP4)
+           {
+             if (ip4_destination_matches_route (&ip4_main, src_ip4,
+                                                &ip_addr_v4 (&prefix->addr),
+                                                prefix->len))
+               {
+                 return -11;
+               }
+           }
+       }
+    }
+
+  if (rule->prefixes[NPOL_DST])
+    {
+      ip_prefix_t *prefix;
+      u8 found = 0;
+      vec_foreach (prefix, rule->prefixes[NPOL_DST])
+       {
+         u8 pfx_af = ip_prefix_version (prefix);
+         if (is_ip6 && pfx_af == AF_IP6)
+           {
+             if (ip6_destination_matches_route (&ip6_main, dst_ip6,
+                                                &ip_addr_v6 (&prefix->addr),
+                                                prefix->len))
+               {
+                 found = 1;
+                 break;
+               }
+           }
+         else if (!is_ip6 && pfx_af == AF_IP4)
+           {
+             if (ip4_destination_matches_route (&ip4_main, dst_ip4,
+                                                &ip_addr_v4 (&prefix->addr),
+                                                prefix->len))
+               {
+                 found = 1;
+                 break;
+               }
+           }
+       }
+      if (!found)
+       {
+         return -12;
+       }
+    }
+
+  if (rule->prefixes[NPOL_NOT_DST])
+    {
+      ip_prefix_t *prefix;
+      vec_foreach (prefix, rule->prefixes[NPOL_NOT_DST])
+       {
+         u8 pfx_af = ip_prefix_version (prefix);
+         if (is_ip6 && pfx_af == AF_IP6)
+           {
+             if (ip6_destination_matches_route (&ip6_main, dst_ip6,
+                                                &ip_addr_v6 (&prefix->addr),
+                                                prefix->len))
+               {
+                 return -13;
+               }
+           }
+         else if (!is_ip6 && pfx_af == AF_IP4)
+           {
+             if (ip4_destination_matches_route (&ip4_main, dst_ip4,
+                                                &ip_addr_v4 (&prefix->addr),
+                                                prefix->len))
+               {
+                 return -14;
+               }
+           }
+       }
+    }
+
+  /* IP ipsets */
+  if (rule->ip_ipsets[NPOL_SRC])
+    {
+      u32 *ipset;
+      u8 found = 0;
+      vec_foreach (ipset, rule->ip_ipsets[NPOL_SRC])
+       {
+         if (is_ip6)
+           {
+             if (ipset_contains_ip6 (&npol_ipsets[*ipset], src_ip6))
+               {
+                 found = 1;
+                 break;
+               }
+           }
+         else
+           {
+             if (ipset_contains_ip4 (&npol_ipsets[*ipset], src_ip4))
+               {
+                 found = 1;
+                 break;
+               }
+           }
+       }
+      if (!found)
+       {
+         return -15;
+       }
+    }
+
+  if (rule->ip_ipsets[NPOL_NOT_SRC])
+    {
+      u32 *ipset;
+      vec_foreach (ipset, rule->ip_ipsets[NPOL_NOT_SRC])
+       {
+         if (is_ip6)
+           {
+             if (ipset_contains_ip6 (&npol_ipsets[*ipset], src_ip6))
+               {
+                 return -16;
+               }
+           }
+         else
+           {
+             if (ipset_contains_ip4 (&npol_ipsets[*ipset], src_ip4))
+               {
+                 return -17;
+               }
+           }
+       }
+    }
+
+  if (rule->ip_ipsets[NPOL_DST])
+    {
+      u32 *ipset;
+      u8 found = 0;
+      vec_foreach (ipset, rule->ip_ipsets[NPOL_DST])
+       {
+         if (is_ip6)
+           {
+             if (ipset_contains_ip6 (&npol_ipsets[*ipset], dst_ip6))
+               {
+                 found = 1;
+                 break;
+               }
+           }
+         else
+           {
+             if (ipset_contains_ip4 (&npol_ipsets[*ipset], dst_ip4))
+               {
+                 found = 1;
+                 break;
+               }
+           }
+       }
+      if (!found)
+       {
+         return -18;
+       }
+    }
+
+  if (rule->ip_ipsets[NPOL_NOT_DST])
+    {
+      u32 *ipset;
+      vec_foreach (ipset, rule->ip_ipsets[NPOL_NOT_DST])
+       {
+         if (is_ip6)
+           {
+             if (ipset_contains_ip6 (&npol_ipsets[*ipset], dst_ip6))
+               {
+                 return -19;
+               }
+           }
+         else
+           {
+             if (ipset_contains_ip4 (&npol_ipsets[*ipset], dst_ip4))
+               {
+                 return -20;
+               }
+           }
+       }
+    }
+
+  /* Special treatment for src / dst ports: they need to be in either the port
+     ranges or the port + ip ipsets / */
+  u8 src_port_found = 0;
+  u8 dst_port_found = 0;
+
+  /* port ranges */
+  if (rule->port_ranges[NPOL_SRC])
+    {
+      npol_port_range_t *range;
+      vec_foreach (range, rule->port_ranges[NPOL_SRC])
+       {
+         if (range->start <= src_port && src_port <= range->end)
+           {
+             src_port_found = 1;
+             break;
+           }
+       }
+    }
+
+  if (rule->port_ranges[NPOL_NOT_SRC])
+    {
+      npol_port_range_t *range;
+      vec_foreach (range, rule->port_ranges[NPOL_NOT_SRC])
+       {
+         if (range->start <= src_port && src_port <= range->end)
+           {
+             return -21;
+           }
+       }
+    }
+
+  if (rule->port_ranges[NPOL_DST])
+    {
+      npol_port_range_t *range;
+      vec_foreach (range, rule->port_ranges[NPOL_DST])
+       {
+         if (range->start <= dst_port && dst_port <= range->end)
+           {
+             dst_port_found = 1;
+             break;
+           }
+       }
+    }
+
+  if (rule->port_ranges[NPOL_NOT_DST])
+    {
+      npol_port_range_t *range;
+      vec_foreach (range, rule->port_ranges[NPOL_NOT_DST])
+       {
+         if (range->start <= dst_port && dst_port <= range->end)
+           {
+             return -22;
+           }
+       }
+    }
+
+  /* ipport ipsets */
+  if (rule->ipport_ipsets[NPOL_SRC])
+    {
+      u32 *ipset;
+      vec_foreach (ipset, rule->ipport_ipsets[NPOL_SRC])
+       {
+         if (is_ip6)
+           {
+             if (ipport_ipset_contains_ip6 (&npol_ipsets[*ipset], src_ip6,
+                                            l4proto, src_port))
+               {
+                 src_port_found = 1;
+                 break;
+               }
+           }
+         else
+           {
+             if (ipport_ipset_contains_ip4 (&npol_ipsets[*ipset], src_ip4,
+                                            l4proto, src_port))
+               {
+                 src_port_found = 1;
+                 break;
+               }
+           }
+       }
+    }
+
+  if (rule->ipport_ipsets[NPOL_NOT_SRC])
+    {
+      u32 *ipset;
+      vec_foreach (ipset, rule->ipport_ipsets[NPOL_NOT_SRC])
+       {
+         if (is_ip6)
+           {
+             if (ipport_ipset_contains_ip6 (&npol_ipsets[*ipset], src_ip6,
+                                            l4proto, src_port))
+               {
+                 return -23;
+               }
+           }
+         else
+           {
+             if (ipport_ipset_contains_ip4 (&npol_ipsets[*ipset], src_ip4,
+                                            l4proto, src_port))
+               {
+                 return -24;
+               }
+           }
+       }
+    }
+
+  if (rule->ipport_ipsets[NPOL_DST])
+    {
+      u32 *ipset;
+      vec_foreach (ipset, rule->ipport_ipsets[NPOL_DST])
+       {
+         if (is_ip6)
+           {
+             if (ipport_ipset_contains_ip6 (&npol_ipsets[*ipset], dst_ip6,
+                                            l4proto, dst_port))
+               {
+                 dst_port_found = 1;
+                 break;
+               }
+           }
+         else
+           {
+             if (ipport_ipset_contains_ip4 (&npol_ipsets[*ipset], dst_ip4,
+                                            l4proto, dst_port))
+               {
+                 dst_port_found = 1;
+                 break;
+               }
+           }
+       }
+    }
+
+  if (rule->ipport_ipsets[NPOL_NOT_DST])
+    {
+      u32 *ipset;
+      vec_foreach (ipset, rule->ipport_ipsets[NPOL_NOT_DST])
+       {
+         if (is_ip6)
+           {
+             if (ipport_ipset_contains_ip6 (&npol_ipsets[*ipset], dst_ip6,
+                                            l4proto, dst_port))
+               {
+                 return -25;
+               }
+           }
+         else
+           {
+             if (ipport_ipset_contains_ip4 (&npol_ipsets[*ipset], dst_ip4,
+                                            l4proto, dst_port))
+               {
+                 return -26;
+               }
+           }
+       }
+    }
+
+  if ((rule->port_ranges[NPOL_SRC] || rule->ipport_ipsets[NPOL_SRC]) &&
+      (!src_port_found))
+    {
+      return -27;
+    }
+  if ((rule->port_ranges[NPOL_DST] || rule->ipport_ipsets[NPOL_DST]) &&
+      (!dst_port_found))
+    {
+      return -28;
+    }
+
+  return rule->action;
+}
+
+always_inline int
+npol_match_policy (npol_policy_t *policy, u32 is_inbound, u32 is_ip6,
+                  fa_5tuple_t *pkt_5tuple)
+{
+  /* packets RX/TX from VPP perspective */
+  u32 *rules =
+    is_inbound ? policy->rule_ids[VLIB_RX] : policy->rule_ids[VLIB_TX];
+  u32 *rule_id;
+  npol_rule_t *rule;
+  int r;
+
+  vec_foreach (rule_id, rules)
+    {
+      rule = &npol_rules[*rule_id];
+      r = npol_match_rule (rule, is_ip6, pkt_5tuple);
+      if (r >= 0)
+       {
+         return r;
+       }
+    }
+  return -1;
+}
+
+/*
+ * npol_match_func evalutes policies on the packet for which the 5tuple is
+ * passed This packet can be :
+ * - is_inbound = 1 : received on interface sw_if_index
+ * - is_inbound = 0 : to be txed on interface sw_if_index
+ * The function sets r_action to NPOL_ACTION_ALLOW or NPOL_ACTION_DENY
+ * It returns 1 if a rule was matched, 0 otherwise
+ */
+CLIB_MARCH_FN (npol_match, int, u32 sw_if_index, u32 is_inbound,
+              fa_5tuple_t *pkt_5tuple, int is_ip6, u8 *r_action)
+{
+  npol_interface_config_t *if_config;
+  npol_policy_t *policy;
+  u32 *policies;
+  int r;
+  u32 i;
+  u8 policy_default;
+  u8 profile_default;
+
+  if_config = vec_elt_at_index (npol_interface_configs, sw_if_index);
+  if (!if_config->enabled)
+    {
+      /* no config for this interface found, allow */
+      *r_action = NPOL_ACTION_ALLOW;
+      return 0;
+    }
+  policies = is_inbound ^ if_config->invert_rx_tx ? if_config->rx_policies :
+                                                   if_config->tx_policies;
+  policy_default =
+    is_inbound ? if_config->policy_default_rx : if_config->policy_default_tx;
+  profile_default =
+    is_inbound ? if_config->profile_default_rx : if_config->profile_default_tx;
+
+  vec_foreach_index (i, policies)
+    {
+      policy = &npol_policies[policies[i]];
+      r = npol_match_policy (policy, is_inbound ^ if_config->invert_rx_tx,
+                            is_ip6, pkt_5tuple);
+      switch (r)
+       {
+       case NPOL_ALLOW:
+         *r_action = NPOL_ACTION_ALLOW; /* allow */
+         return 1;
+       case NPOL_DENY:
+         *r_action = NPOL_ACTION_DENY;
+         return 1;
+       case NPOL_PASS:
+         goto profiles;
+       case NPOL_LOG:
+         /* TODO: support LOG action */
+         break;
+       default:
+         break;
+       }
+    };
+  /* nothing matched, or no policies */
+  switch (policy_default)
+    {
+    case NPOL_ALLOW:
+      *r_action = NPOL_ACTION_ALLOW;
+      return 1;
+    case NPOL_DEFAULT_DENY:
+      *r_action = NPOL_ACTION_DENY;
+      return 1;
+    case NPOL_DEFAULT_PASS:
+      goto profiles;
+    default:
+      break;
+    }
+
+profiles:
+
+  vec_foreach_index (i, if_config->profiles)
+    {
+      policy = &npol_policies[if_config->profiles[i]];
+      r = npol_match_policy (policy, is_inbound ^ if_config->invert_rx_tx,
+                            is_ip6, pkt_5tuple);
+      switch (r)
+       {
+       case NPOL_ALLOW:
+         *r_action = NPOL_ACTION_ALLOW;
+         return 1;
+       case NPOL_DENY:
+         *r_action = NPOL_ACTION_DENY;
+         return 1;
+       case NPOL_PASS:
+         clib_warning ("error: pass in profile %u", if_config->profiles[i]);
+         return 1;
+       case NPOL_LOG:
+         /* TODO: support LOG action */
+         break;
+       default:
+         break;
+       }
+    };
+
+  /* nothing matched, or no profiles */
+  switch (profile_default)
+    {
+    case NPOL_ALLOW:
+      *r_action = NPOL_ACTION_ALLOW;
+      return 1;
+    case NPOL_DEFAULT_DENY:
+      *r_action = NPOL_ACTION_DENY;
+      return 1;
+    case NPOL_DEFAULT_PASS:
+      clib_warning ("error: default pass in profile %u",
+                   if_config->profiles[i]);
+      return 1;
+    default:
+      break;
+    }
+  return 1;
+}
+
+#ifndef CLIB_MARCH_VARIANT
+int
+npol_match_func (u32 sw_if_index, u32 is_inbound, fa_5tuple_t *pkt_5tuple,
+                int is_ip6, u8 *r_action)
+{
+  return CLIB_MARCH_FN_SELECT (npol_match) (sw_if_index, is_inbound,
+                                           pkt_5tuple, is_ip6, r_action);
+}
+
+#endif /* CLIB_MARCH_VARIANT */
diff --git a/src/plugins/npol/npol_match.h b/src/plugins/npol/npol_match.h
new file mode 100644 (file)
index 0000000..259717c
--- /dev/null
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#ifndef included_npol_match_h
+#define included_npol_match_h
+
+#include <acl/acl.h>
+#include <acl/fa_node.h>
+
+#include <npol/npol_ipset.h>
+#include <npol/npol_policy.h>
+#include <npol/npol_rule.h>
+
+int npol_match_func (u32 sw_if_index, u32 is_inbound, fa_5tuple_t *pkt_5tuple,
+                    int is_ip6, u8 *r_action);
+
+#endif
diff --git a/src/plugins/npol/npol_policy.c b/src/plugins/npol/npol_policy.c
new file mode 100644 (file)
index 0000000..42b206b
--- /dev/null
@@ -0,0 +1,188 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <npol/npol.h>
+#include <npol/npol_policy.h>
+#include <npol/npol_rule.h>
+#include <npol/npol_format.h>
+
+npol_policy_t *npol_policies;
+
+static npol_policy_t *
+npol_policy_alloc ()
+{
+  npol_policy_t *policy;
+  pool_get_zero (npol_policies, policy);
+  return policy;
+}
+
+npol_policy_t *
+npol_policy_get_if_exists (u32 index)
+{
+  if (pool_is_free_index (npol_policies, index))
+    return (NULL);
+  return pool_elt_at_index (npol_policies, index);
+}
+
+static void
+npol_policy_cleanup (npol_policy_t *policy)
+{
+  for (int i = 0; i < VLIB_N_RX_TX; i++)
+    vec_free (policy->rule_ids[i]);
+}
+
+int
+npol_policy_update (u32 *id, npol_policy_rule_t *rules)
+{
+  npol_policy_t *policy;
+  npol_policy_rule_t *rule;
+
+  policy = npol_policy_get_if_exists (*id);
+  if (policy)
+    npol_policy_cleanup (policy);
+  else
+    policy = npol_policy_alloc ();
+
+  vec_foreach (rule, rules)
+    vec_add1 (policy->rule_ids[rule->direction], rule->rule_id);
+
+  *id = policy - npol_policies;
+  return 0;
+}
+
+int
+npol_policy_delete (u32 id)
+{
+  npol_policy_t *policy;
+  policy = npol_policy_get_if_exists (id);
+  if (NULL == policy)
+    return VNET_API_ERROR_NO_SUCH_ENTRY;
+
+  npol_policy_cleanup (policy);
+  pool_put (npol_policies, policy);
+
+  return 0;
+}
+
+static clib_error_t *
+npol_policies_show_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                          vlib_cli_command_t *cmd)
+{
+  clib_error_t *error = 0;
+  npol_policy_t *policy;
+  u8 verbose = 0;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "verbose"))
+       verbose = 1;
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  pool_foreach (policy, npol_policies)
+    vlib_cli_output (vm, "%U", format_npol_policy, policy, 0, /* indent */
+                    verbose, 0 /* invert_rx_tx */);
+
+done:
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_policies_show_cmd, static) = {
+  .path = "show npol policies",
+  .function = npol_policies_show_cmd_fn,
+  .short_help = "show npol policies [verbose]",
+};
+
+static clib_error_t *
+npol_policies_add_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                         vlib_cli_command_t *cmd)
+{
+  clib_error_t *error = 0;
+  u32 id = NPOL_INVALID_INDEX, rule_id;
+  npol_policy_rule_t *policy_rules = 0, *policy_rule;
+  int direction = VLIB_RX;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "update %u", &id))
+       ;
+      else if (unformat (input, "%U", unformat_vlib_rx_tx, &direction))
+       ;
+      else if (unformat (input, "%u", &rule_id))
+       {
+         vec_add2 (policy_rules, policy_rule, 1);
+         policy_rule->rule_id = rule_id;
+         policy_rule->direction = direction;
+       }
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  rv = npol_policy_update (&id, policy_rules);
+  if (rv)
+    error = clib_error_return (0, "npol_policy_delete errored with %d", rv);
+  else
+    vlib_cli_output (vm, "npol policy %d added", id);
+
+done:
+  vec_free (policy_rules);
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_policies_add_cmd, static) = {
+  .path = "npol policy add",
+  .function = npol_policies_add_cmd_fn,
+  .short_help = "npol policy add [rx rule_id rule_id ...] [tx rule_id rule_id "
+               "...] [update [id]]",
+};
+
+static clib_error_t *
+npol_policies_del_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                         vlib_cli_command_t *cmd)
+{
+  clib_error_t *error = 0;
+  u32 id = NPOL_INVALID_INDEX;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "%u", &id))
+       ;
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  if (NPOL_INVALID_INDEX == id)
+    {
+      error = clib_error_return (0, "missing policy id");
+      goto done;
+    }
+
+  rv = npol_policy_delete (id);
+  if (rv)
+    error = clib_error_return (0, "npol_policy_delete errored with %d", rv);
+
+done:
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_policies_del_cmd, static) = {
+  .path = "npol policy del",
+  .function = npol_policies_del_cmd_fn,
+  .short_help = "npol policy del [id]",
+};
diff --git a/src/plugins/npol/npol_policy.h b/src/plugins/npol/npol_policy.h
new file mode 100644 (file)
index 0000000..0d23fb5
--- /dev/null
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#ifndef included_npol_policy_h
+#define included_npol_policy_h
+
+#include <npol/npol.h>
+
+typedef struct
+{
+  /* VLIB_RX for inbound
+     VLIB_TX for outbound */
+  u32 *rule_ids[VLIB_N_RX_TX];
+} npol_policy_t;
+
+typedef struct
+{
+  u32 rule_id;
+  /* VLIB_RX or VLIB_TX */
+  u8 direction;
+} npol_policy_rule_t;
+
+typedef enum
+{
+  NPOL_POLICY_QUIET,
+  NPOL_POLICY_VERBOSE,
+  NPOL_POLICY_ONLY_RX,
+  NPOL_POLICY_ONLY_TX,
+} npol_policy_format_type_t;
+
+extern npol_policy_t *npol_policies;
+
+int npol_policy_update (u32 *id, npol_policy_rule_t *rules);
+int npol_policy_delete (u32 id);
+npol_policy_t *npol_policy_get_if_exists (u32 index);
+
+#endif
diff --git a/src/plugins/npol/npol_rule.c b/src/plugins/npol/npol_rule.c
new file mode 100644 (file)
index 0000000..838bd2a
--- /dev/null
@@ -0,0 +1,258 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <npol/npol.h>
+#include <npol/npol_rule.h>
+#include <npol/npol_ipset.h>
+#include <npol/npol_format.h>
+
+npol_rule_t *npol_rules;
+
+npol_rule_entry_t *
+npol_rule_get_entries (npol_rule_t *rule)
+{
+  npol_rule_entry_t *entries = NULL, *entry;
+  npol_port_range_t *pr;
+  ip_prefix_t *pfx;
+  u32 *set_id;
+  for (int i = 0; i < NPOL_RULE_MAX_FLAGS; i++)
+    {
+      vec_foreach (pfx, rule->prefixes[i])
+       {
+         vec_add2 (entries, entry, 1);
+         entry->type = NPOL_CIDR;
+         entry->flags = i;
+         clib_memcpy (&entry->data.cidr, pfx, sizeof (*pfx));
+       }
+      vec_foreach (pr, rule->port_ranges[i])
+       {
+         vec_add2 (entries, entry, 1);
+         entry->type = NPOL_PORT_RANGE;
+         entry->flags = i;
+         clib_memcpy (&entry->data.port_range, pr, sizeof (*pr));
+       }
+      vec_foreach (set_id, rule->ip_ipsets[i])
+       {
+         vec_add2 (entries, entry, 1);
+         entry->type = NPOL_IP_SET;
+         entry->flags = i;
+         entry->data.set_id = *set_id;
+       }
+      vec_foreach (set_id, rule->ipport_ipsets[i])
+       {
+         vec_add2 (entries, entry, 1);
+         entry->type = NPOL_PORT_IP_SET;
+         entry->flags = i;
+         entry->data.set_id = *set_id;
+       }
+    }
+  return entries;
+}
+
+npol_rule_t *
+npol_rule_alloc ()
+{
+  npol_rule_t *rule;
+  pool_get_zero (npol_rules, rule);
+  return rule;
+}
+
+npol_rule_t *
+npol_rule_get_if_exists (u32 index)
+{
+  if (pool_is_free_index (npol_rules, index))
+    return (NULL);
+  return pool_elt_at_index (npol_rules, index);
+}
+
+static void
+npol_rule_cleanup (npol_rule_t *rule)
+{
+  int i;
+  vec_free (rule->filters);
+  for (i = 0; i < NPOL_RULE_MAX_FLAGS; i++)
+    {
+      vec_free (rule->prefixes[i]);
+      vec_free (rule->port_ranges[i]);
+      vec_free (rule->ip_ipsets[i]);
+      vec_free (rule->ipport_ipsets[i]);
+    }
+}
+
+int
+npol_rule_update (u32 *id, npol_rule_action_t action,
+                 npol_rule_filter_t *filters, npol_rule_entry_t *entries)
+{
+  npol_rule_filter_t *filter;
+  npol_rule_entry_t *entry;
+  npol_rule_t *rule;
+  int rv;
+
+  rule = npol_rule_get_if_exists (*id);
+  if (rule)
+    npol_rule_cleanup (rule);
+  else
+    rule = npol_rule_alloc ();
+
+  rule->action = action;
+  vec_foreach (filter, filters)
+    vec_add1 (rule->filters, *filter);
+
+  vec_foreach (entry, entries)
+    {
+      u8 flags = entry->flags;
+      switch (entry->type)
+       {
+       case NPOL_CIDR:
+         vec_add1 (rule->prefixes[flags], entry->data.cidr);
+         break;
+       case NPOL_PORT_RANGE:
+         vec_add1 (rule->port_ranges[flags], entry->data.port_range);
+         break;
+       case NPOL_PORT_IP_SET:
+         vec_add1 (rule->ipport_ipsets[flags], entry->data.set_id);
+         break;
+       case NPOL_IP_SET:
+         vec_add1 (rule->ip_ipsets[flags], entry->data.set_id);
+         break;
+       default:
+         rv = 1;
+         goto error;
+       }
+    }
+  *id = rule - npol_rules;
+  return 0;
+error:
+  npol_rule_cleanup (rule);
+  pool_put (npol_rules, rule);
+  return rv;
+}
+
+int
+npol_rule_delete (u32 id)
+{
+  npol_rule_t *rule;
+  rule = npol_rule_get_if_exists (id);
+  if (NULL == rule)
+    return VNET_API_ERROR_NO_SUCH_ENTRY;
+
+  npol_rule_cleanup (rule);
+  pool_put (npol_rules, rule);
+
+  return 0;
+}
+
+static clib_error_t *
+npol_rules_show_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                       vlib_cli_command_t *cmd)
+{
+  npol_rule_t *rule;
+
+  pool_foreach (rule, npol_rules)
+    {
+      vlib_cli_output (vm, "%U", format_npol_rule, rule);
+    }
+
+  return 0;
+}
+
+VLIB_CLI_COMMAND (npol_rules_show_cmd, static) = {
+  .path = "show npol rules",
+  .function = npol_rules_show_cmd_fn,
+  .short_help = "show npol rules",
+};
+
+static clib_error_t *
+npol_rules_add_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                      vlib_cli_command_t *cmd)
+{
+  npol_rule_filter_t tmp_filter, *filters = 0;
+  npol_rule_entry_t tmp_entry, *entries = 0;
+  clib_error_t *error = 0;
+  npol_rule_action_t action;
+  u32 id = NPOL_INVALID_INDEX;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "update %u", &id))
+       ;
+      else if (unformat_user (input, unformat_npol_rule_action, &action))
+       ;
+      else if (unformat_user (input, unformat_npol_rule_entry, &tmp_entry))
+       vec_add1 (entries, tmp_entry);
+      else if (unformat_user (input, unformat_npol_rule_filter, &tmp_filter))
+       {
+         vec_add1 (filters, tmp_filter);
+         vlib_cli_output (vm, "%U", format_npol_rule_filter, &tmp_filter);
+       }
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  rv = npol_rule_update (&id, action, filters, entries);
+  if (rv)
+    error = clib_error_return (0, "npol_rule_update error %d", rv);
+  else
+    vlib_cli_output (vm, "npol rule %d added", id);
+
+done:
+  vec_free (filters);
+  vec_free (entries);
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_rules_add_cmd, static) = {
+  .path = "npol rule add",
+  .function = npol_rules_add_cmd_fn,
+  .short_help = "npol rule add [allow|deny|log|pass]"
+               "[filter[==|!=]value]"
+               "[[src|dst][==|!=][prefix|set ID|[port-port]]]",
+  .long_help = "Add a rule, with given filters and entries\n"
+              "filters can be `icmp-type`, `icmp-code` and `proto`",
+};
+
+static clib_error_t *
+npol_rules_del_cmd_fn (vlib_main_t *vm, unformat_input_t *input,
+                      vlib_cli_command_t *cmd)
+{
+  clib_error_t *error = 0;
+  u32 id = NPOL_INVALID_INDEX;
+  int rv;
+
+  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
+    {
+      if (unformat (input, "%u", &id))
+       ;
+      else
+       {
+         error = clib_error_return (0, "unknown input '%U'",
+                                    format_unformat_error, input);
+         goto done;
+       }
+    }
+
+  if (NPOL_INVALID_INDEX == id)
+    {
+      error = clib_error_return (0, "missing rule id");
+      goto done;
+    }
+
+  rv = npol_rule_delete (id);
+  if (rv)
+    error = clib_error_return (0, "npol_rule_delete errored with %d", rv);
+
+done:
+  return error;
+}
+
+VLIB_CLI_COMMAND (npol_rules_del_cmd, static) = {
+  .path = "npol rule del",
+  .function = npol_rules_del_cmd_fn,
+  .short_help = "npol rule del [id]",
+};
diff --git a/src/plugins/npol/npol_rule.h b/src/plugins/npol/npol_rule.h
new file mode 100644 (file)
index 0000000..1698aaf
--- /dev/null
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#ifndef included_npol_rule_h
+#define included_npol_rule_h
+
+#include <npol/npol.h>
+
+typedef vl_api_npol_rule_action_t npol_rule_action_t;
+typedef vl_api_npol_entry_type_t npol_entry_type_t;
+typedef vl_api_npol_rule_filter_type_t npol_rule_filter_type_t;
+
+typedef struct npol_rule_filter_
+{
+  npol_rule_filter_type_t type;
+  /* Content to filter against */
+  u32 value;
+  /* If true match packet.property == opaque, else packet.property != opaque */
+  u8 should_match;
+} npol_rule_filter_t;
+
+typedef union npol_entry_data_t_
+{
+  ip_prefix_t cidr;
+  npol_port_range_t port_range;
+  u32 set_id;
+} npol_entry_data_t;
+
+typedef enum npol_rule_key_flag_
+{
+  NPOL_IS_SRC = 1 << 0,
+  NPOL_IS_NOT = 1 << 1,
+  NPOL_RULE_MAX_FLAGS = 1 << 2,
+} npol_rule_key_flag_t;
+
+#define NPOL_SRC     NPOL_IS_SRC
+#define NPOL_NOT_SRC (NPOL_IS_SRC | NPOL_IS_NOT)
+#define NPOL_DST     0
+#define NPOL_NOT_DST NPOL_IS_NOT
+
+typedef struct npol_rule_entry_t_
+{
+  npol_entry_type_t type;
+  npol_entry_data_t data;
+  npol_rule_key_flag_t flags;
+} npol_rule_entry_t;
+
+typedef struct npol_rule_
+{
+  npol_rule_action_t action;
+
+  npol_rule_filter_t *filters;
+
+  /* Indexed by npol_rule_key_flag_t */
+  ip_prefix_t *prefixes[NPOL_RULE_MAX_FLAGS];
+  u32 *ip_ipsets[NPOL_RULE_MAX_FLAGS];
+  npol_port_range_t *port_ranges[NPOL_RULE_MAX_FLAGS];
+  u32 *ipport_ipsets[NPOL_RULE_MAX_FLAGS];
+} npol_rule_t;
+
+extern npol_rule_t *npol_rules;
+
+int npol_rule_delete (u32 id);
+int npol_rule_update (u32 *id, npol_rule_action_t action,
+                     npol_rule_filter_t *filters, npol_rule_entry_t *entries);
+npol_rule_t *npol_rule_get_if_exists (u32 index);
+npol_rule_entry_t *npol_rule_get_entries (npol_rule_t *rule);
+
+#endif
diff --git a/test/test_npol.py b/test/test_npol.py
new file mode 100644 (file)
index 0000000..a01af78
--- /dev/null
@@ -0,0 +1,1195 @@
+#!/usr/bin/env python3
+
+import random
+import unittest
+from ipaddress import (
+    IPv4Address,
+    IPv4Network,
+    IPv6Address,
+    IPv6Network,
+    ip_address,
+    ip_network,
+)
+from itertools import product
+
+from framework import VppTestCase
+from asfframework import VppTestRunner
+from scapy.layers.inet import (
+    ICMP,
+    IP,
+    TCP,
+    UDP,
+    ICMPerror,
+    IPerror,
+    TCPerror,
+    UDPerror,
+)
+from scapy.layers.inet6 import (
+    ICMPv6DestUnreach,
+    ICMPv6EchoReply,
+    ICMPv6EchoRequest,
+    IPerror6,
+    IPv6,
+)
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+from vpp_ip import INVALID_INDEX, DpoProto
+from vpp_object import VppObject
+from vpp_papi import VppEnum
+
+icmp4_type = 8  # echo request
+icmp4_code = 3
+icmp6_type = 128  # echo request
+icmp6_code = 3
+tcp_protocol = 6
+icmp_protocol = 1
+icmp6_protocol = 58
+udp_protocol = 17
+src_l4 = 1234
+dst_l4 = 4321
+
+
+def random_payload():
+    return Raw(load=bytearray(random.getrandbits(8) for _ in range(20)))
+
+
+class VppNpolPolicyItem:
+    def __init__(self, is_inbound, rule_id):
+        self._is_inbound = is_inbound
+        self._rule_id = rule_id
+
+    def encode(self):
+        return {"rule_id": self._rule_id, "is_inbound": self._is_inbound}
+
+
+class VppNpolPolicy(VppObject):
+    def __init__(self, test, rules):
+        self._test = test
+        self._rules = rules
+        self.encoded_rules = []
+        self.init_rules()
+
+    def init_rules(self):
+        self.encoded_rules = []
+        for rule in self._rules:
+            self.encoded_rules.append(rule.encode())
+
+    def add_vpp_config(self):
+        r = self._test.vapi.npol_policy_create(
+            len(self.encoded_rules), self.encoded_rules
+        )
+        self._test.assertEqual(0, r.retval)
+        self._test.registry.register(self, self._test.logger)
+        self._test.logger.info("npol_policy_create retval=" + str(r.retval))
+        self._policy_id = r.policy_id
+        self._test.logger.info(self._test.vapi.cli("show npol policies verbose"))
+
+    def npol_policy_update(self, rules):
+        self._rules = rules
+        self.init_rules()
+        r = self._test.vapi.npol_policy_update(
+            self._policy_id, len(self.encoded_rules), self.encoded_rules
+        )
+        self._test.assertEqual(0, r.retval)
+
+    def npol_policy_delete(self):
+        r = self._test.vapi.npol_policy_delete(self._policy_id)
+        self._test.assertEqual(0, r.retval)
+        self._test.logger.info(self._test.vapi.cli("show npol policies"))
+
+    def remove_vpp_config(self):
+        self.npol_policy_delete()
+
+    def query_vpp_config(self):
+        self._test.logger.info("query vpp config")
+        self._test.logger.info(self._test.vapi.cli("show npol policies verbose"))
+
+
+class VppNpolFilter:
+    def __init__(self, type=None, value=0, should_match=0):
+        self._filter_type = type if type != None else FILTER_TYPE_NONE
+        self._filter_value = value
+        self._should_match = should_match
+
+    def encode(self):
+        return {
+            "type": self._filter_type,
+            "value": self._filter_value,
+            "should_match": self._should_match,
+        }
+
+
+class VppNpolRule(VppObject):
+    def __init__(self, test, is_v6, action, filters=[], matches=[]):
+        self._test = test
+        # This is actually unused
+        self._af = 255
+        self.init_rule(action, filters, matches)
+
+    def vpp_id(self):
+        return self._rule_id
+
+    def init_rule(self, action, filters=[], matches=[]):
+        self._action = action
+        self._filters = filters
+        self._matches = matches
+        self.encoded_filters = []
+        for filter in self._filters:
+            self.encoded_filters.append(filter.encode())
+        while len(self.encoded_filters) < 3:
+            self.encoded_filters.append(VppNpolFilter().encode())
+        self._test.assertEqual(len(self.encoded_filters), 3)
+
+    def add_vpp_config(self):
+        r = self._test.vapi.npol_rule_create(
+            {
+                "af": self._af,
+                "action": self._action,
+                "filters": self.encoded_filters,
+                "num_entries": len(self._matches),
+                "matches": self._matches,
+            }
+        )
+        self._test.assertEqual(0, r.retval)
+        self._test.registry.register(self, self._test.logger)
+        self._test.logger.info("npol_rule_create retval=" + str(r.retval))
+        self._rule_id = r.rule_id
+        self._test.logger.info("rules id : " + str(self._rule_id))
+        self._test.logger.info(self._test.vapi.cli("show npol rules"))
+
+    def npol_rule_update(self, filters, matches):
+        self.init_rule(self._action, filters, matches)
+        r = self._test.vapi.npol_rule_update(
+            self._rule_id,
+            {
+                "af": self._af,
+                "action": self._action,
+                "filters": self.encoded_filters,
+                "num_entries": len(self._matches),
+                "matches": self._matches,
+            },
+        )
+        self._test.assertEqual(0, r.retval)
+        self._test.registry.register(self, self._test.logger)
+        self._test.logger.info("npol rule update")
+        self._test.logger.info(self._test.vapi.cli("show npol rules"))
+
+    def npol_rule_delete(self):
+        r = self._test.vapi.npol_rule_delete(self._rule_id)
+        self._test.assertEqual(0, r.retval)
+        self._test.logger.info(self._test.vapi.cli("show npol rules"))
+
+    def remove_vpp_config(self):
+        self.npol_rule_delete()
+
+    def query_vpp_config(self):
+        self._test.logger.info("query vpp config")
+        self._test.logger.info(self._test.vapi.cli("show npol rules"))
+
+
+class VppNpolIpset(VppObject):
+    def __init__(self, test, type, members):
+        self.test = test
+        self.type = type
+        self.members = members
+
+    def add_vpp_config(self):
+        r = self.test.vapi.npol_ipset_create(self.type)
+        self.test.assertEqual(0, r.retval)
+        self.vpp_id = r.set_id
+        encoded_members = []
+        for m in self.members:
+            if self.type == IPSET_TYPE_IP:
+                encoded_members.append({"val": {"address": m}})
+            elif self.type == IPSET_TYPE_IP_PORT:
+                encoded_members.append({"val": {"tuple": m}})
+            elif self.type == IPSET_TYPE_NET:
+                encoded_members.append({"val": {"prefix": m}})
+        r = self.test.vapi.npol_ipset_add_del_members(
+            set_id=self.vpp_id,
+            is_add=True,
+            len=len(encoded_members),
+            members=encoded_members,
+        )
+        self.test.assertEqual(0, r.retval)
+
+    def query_vpp_config(self):
+        pass
+
+    def remove_vpp_config(self):
+        r = self.test.vapi.npol_ipset_delete(set_id=self.vpp_id)
+        self.test.assertEqual(0, r.retval)
+
+
+ACTION_ALLOW = VppEnum.vl_api_npol_rule_action_t.NPOL_ALLOW
+ACTION_DENY = VppEnum.vl_api_npol_rule_action_t.NPOL_DENY
+ACTION_PASS = VppEnum.vl_api_npol_rule_action_t.NPOL_PASS
+ACTION_LOG = VppEnum.vl_api_npol_rule_action_t.NPOL_LOG
+DEFAULT_ALLOW = VppEnum.vl_api_npol_policy_default_t.NPOL_DEFAULT_ALLOW
+DEFAULT_DENY = VppEnum.vl_api_npol_policy_default_t.NPOL_DEFAULT_DENY
+DEFAULT_PASS = VppEnum.vl_api_npol_policy_default_t.NPOL_DEFAULT_PASS
+FILTER_TYPE_NONE = VppEnum.vl_api_npol_rule_filter_type_t.NPOL_RULE_FILTER_NONE_TYPE
+FILTER_TYPE_L4_PROTO = VppEnum.vl_api_npol_rule_filter_type_t.NPOL_RULE_FILTER_L4_PROTO
+FILTER_TYPE_ICMP_CODE = (
+    VppEnum.vl_api_npol_rule_filter_type_t.NPOL_RULE_FILTER_ICMP_CODE
+)
+FILTER_TYPE_ICMP_TYPE = (
+    VppEnum.vl_api_npol_rule_filter_type_t.NPOL_RULE_FILTER_ICMP_TYPE
+)
+ENTRY_CIDR = VppEnum.vl_api_npol_entry_type_t.NPOL_CIDR
+ENTRY_PORT_RANGE = VppEnum.vl_api_npol_entry_type_t.NPOL_PORT_RANGE
+ENTRY_PORT_IP_SET = VppEnum.vl_api_npol_entry_type_t.NPOL_PORT_IP_SET
+ENTRY_IP_SET = VppEnum.vl_api_npol_entry_type_t.NPOL_IP_SET
+IPSET_TYPE_IP = VppEnum.vl_api_npol_ipset_type_t.NPOL_IP
+IPSET_TYPE_IP_PORT = VppEnum.vl_api_npol_ipset_type_t.NPOL_IP_AND_PORT
+IPSET_TYPE_NET = VppEnum.vl_api_npol_ipset_type_t.NPOL_NET
+
+
+class BaseNpolTest(VppTestCase):
+    @classmethod
+    def setUpClass(self):
+        super(BaseNpolTest, self).setUpClass()
+        # We can't define these before the API is loaded, so here they are...
+
+        self.create_pg_interfaces(range(2))
+        for i in self.pg_interfaces:
+            i.admin_up()
+            # Add one additional neighbor on each side
+            # for tests with different addresses
+            i.generate_remote_hosts(2)
+            i.config_ip4()
+            i.configure_ipv4_neighbors()
+            i.config_ip6()
+            i.configure_ipv6_neighbors()
+
+    @classmethod
+    def tearDownClass(self):
+        for i in self.pg_interfaces:
+            i.unconfig_ip4()
+            i.unconfig_ip6()
+            i.admin_down()
+        super(BaseNpolTest, self).tearDownClass()
+
+    def setUp(self):
+        super(BaseNpolTest, self).setUp()
+
+    def tearDown(self):
+        super(BaseNpolTest, self).tearDown()
+
+    def configure_policies(
+        self,
+        interface,
+        ingress,
+        egress,
+        profiles,
+        defrxpolicy=1,
+        deftxpolicy=1,
+        defrxprofile=1,
+        deftxprofile=1,
+    ):
+        id_list = []
+        for policy in ingress + egress + profiles:
+            id_list.append(policy._policy_id)
+        r = self.vapi.npol_configure_policies(
+            interface.sw_if_index,
+            len(ingress),
+            len(egress),
+            len(ingress) + len(egress) + len(profiles),
+            1,
+            defrxpolicy,
+            deftxpolicy,
+            defrxprofile,
+            deftxprofile,
+            id_list,
+        )
+        self.assertEqual(0, r.retval)
+
+    def base_ip_packet(self, is_v6=False, src_ip2=False, dst_ip2=False):
+        IP46 = IPv6 if is_v6 else IP
+        src_host = self.pg0.remote_hosts[1 if src_ip2 else 0]
+        dst_host = self.pg1.remote_hosts[1 if dst_ip2 else 0]
+        src_addr = src_host.ip6 if is_v6 else src_host.ip4
+        dst_addr = dst_host.ip6 if is_v6 else dst_host.ip4
+        return Ether(src=src_host.mac, dst=self.pg0.local_mac) / IP46(
+            src=src_addr, dst=dst_addr
+        )
+
+    def do_test_one_rule(
+        self, filters, matches, matching_packets, not_matching_packets
+    ):
+        # Caution: because of how vpp works, packets may be reordered
+        # (v4 first, v6 next) which may break the check on received packets
+        # Therefore, in matching packets, all v4 packets must be before
+        # all v6 packets
+        self.rule.npol_rule_update(filters, matches)
+        self.send_test_packets(
+            self.pg0, self.pg1, matching_packets, not_matching_packets
+        )
+
+    def vapi_npol_match(self, sw_if_index, pkt, direction):
+        if pkt.haslayer(IP):
+            ipv = "ip4"
+            src_addr = pkt.getlayer(IP).src
+            dst_addr = pkt.getlayer(IP).dst
+        if pkt.haslayer(IPv6):
+            ipv = "ip6"
+            src_addr = pkt.getlayer(IPv6).src
+            dst_addr = pkt.getlayer(IPv6).dst
+        if pkt.haslayer(UDP):
+            proto = "udp"
+            src_port = pkt.getlayer(UDP).sport
+            dst_port = pkt.getlayer(UDP).dport
+        if pkt.haslayer(TCP):
+            proto = "tcp"
+            src_port = pkt.getlayer(TCP).sport
+            dst_port = pkt.getlayer(TCP).dport
+        if pkt.haslayer(ICMP):
+            proto = "icmp"
+            src_port = pkt.getlayer(ICMP).type
+            dst_port = pkt.getlayer(ICMP).code
+        if pkt.haslayer(ICMPv6EchoRequest):
+            proto = "icmp6"
+            src_port = pkt.getlayer(ICMPv6EchoRequest).type
+            dst_port = pkt.getlayer(ICMPv6EchoRequest).code
+
+        return self.vapi.cli(
+            f"npol match {sw_if_index} {direction} {ipv} "
+            f"{proto} " + f"{src_addr};{src_port}->{dst_addr};{dst_port}"
+        )
+
+    def send_test_packets(self, from_if, to_if, passing_packets, drop_packets):
+        for pkt in passing_packets:
+            self.assertTrue(
+                self.vapi_npol_match(from_if, pkt, "inbound")
+                .strip()
+                .endswith("action:ALLOW")
+            )
+            self.assertTrue(
+                self.vapi_npol_match(to_if, pkt, "outbound")
+                .strip()
+                .endswith("action:ALLOW")
+            )
+        for pkt in drop_packets:
+            self.assertTrue(
+                self.vapi_npol_match(from_if, pkt, "inbound")
+                .strip()
+                .endswith("action:DENY")
+                or self.vapi_npol_match(to_if, pkt, "outbound")
+                .strip()
+                .endswith("action:DENY")
+            )
+
+        # if len(passing_packets) > 0:
+        #     rxl = self.send_and_expect(from_if, passing_packets, to_if)
+        #     self.assertEqual(len(rxl), len(passing_packets))
+        #     for i in range(len(passing_packets)):
+        #         rx = rxl[i].payload
+        #         tx = passing_packets[i].payload
+        #         tx = tx.__class__(bytes(tx))  # Compute all fields
+        #         # Remove IP[v6] TTL / checksum that are changed on forwarding
+        #         if IP in tx:
+        #             del tx.chksum, tx.ttl, rx.chksum, rx.ttl
+        #         elif IPv6 in tx:
+        #             del tx.hlim, rx.hlim
+        #         self.assertEqual(rx, tx)
+        # if len(drop_packets) > 0:
+        #     self.send_and_assert_no_replies(
+        #         from_if, drop_packets, to_if, timeout=0.1
+        #     )
+        # self.vapi.cli("clear acl-plugin sessions")
+
+
+class TestNpolMatches(BaseNpolTest):
+    """Network Policies rule matching tests"""
+
+    @classmethod
+    def setUpClass(self):
+        super(TestNpolMatches, self).setUpClass()
+
+    @classmethod
+    def tearDownClass(self):
+        super(TestNpolMatches, self).tearDownClass()
+
+    def setUp(self):
+        super(TestNpolMatches, self).setUp()
+        self.rule = VppNpolRule(self, is_v6=False, action=ACTION_ALLOW)
+        self.rule.add_vpp_config()
+        self.policy = VppNpolPolicy(
+            self, [VppNpolPolicyItem(is_inbound=1, rule_id=self.rule.vpp_id())]
+        )
+        self.policy.add_vpp_config()
+        self.configure_policies(self.pg1, [self.policy], [], [])
+        self.src_ip_ipset = VppNpolIpset(
+            self, IPSET_TYPE_IP, [self.pg0.remote_ip4, self.pg0.remote_ip6]
+        )
+        self.src_ip_ipset.add_vpp_config()
+        self.dst_ip_ipset = VppNpolIpset(
+            self, IPSET_TYPE_IP, [self.pg1.remote_ip4, self.pg1.remote_ip6]
+        )
+        self.dst_ip_ipset.add_vpp_config()
+        self.src_net_ipset = VppNpolIpset(
+            self,
+            IPSET_TYPE_NET,
+            [self.pg0.remote_ip4 + "/32", self.pg0.remote_ip6 + "/128"],
+        )
+        self.src_net_ipset.add_vpp_config()
+        self.dst_net_ipset = VppNpolIpset(
+            self,
+            IPSET_TYPE_NET,
+            [self.pg1.remote_ip4 + "/32", self.pg1.remote_ip6 + "/128"],
+        )
+        self.dst_net_ipset.add_vpp_config()
+        self.src_ipport_ipset = VppNpolIpset(
+            self,
+            IPSET_TYPE_IP_PORT,
+            [
+                {
+                    "address": self.pg0.remote_ip4,
+                    "l4_proto": tcp_protocol,
+                    "port": src_l4,
+                },
+                {
+                    "address": self.pg0.remote_ip6,
+                    "l4_proto": tcp_protocol,
+                    "port": src_l4,
+                },
+            ],
+        )
+        self.src_ipport_ipset.add_vpp_config()
+        self.dst_ipport_ipset = VppNpolIpset(
+            self,
+            IPSET_TYPE_IP_PORT,
+            [
+                {
+                    "address": self.pg1.remote_ip4,
+                    "l4_proto": tcp_protocol,
+                    "port": dst_l4,
+                },
+                {
+                    "address": self.pg1.remote_ip6,
+                    "l4_proto": tcp_protocol,
+                    "port": dst_l4,
+                },
+            ],
+        )
+        self.dst_ipport_ipset.add_vpp_config()
+
+    def tearDown(self):
+        self.vapi.cli("clear acl-plugin sessions")
+        self.configure_policies(self.pg1, [], [], [])
+        self.policy.npol_policy_delete()
+        self.rule.npol_rule_delete()
+        super(TestNpolMatches, self).tearDown()
+
+    def test_empty_rule(self):
+        # Empty rule matches everything
+        valid = [
+            self.base_ip_packet(False)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(False)
+            / UDP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(False)
+            / ICMP(type=icmp4_type, code=icmp4_code)
+            / random_payload(),
+            self.base_ip_packet(True)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(True)
+            / UDP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(True)
+            / ICMPv6EchoRequest(type=icmp6_type, code=icmp6_code)
+            / random_payload(),
+        ]
+        self.do_test_one_rule([], [], valid, [])
+
+    def npol_test_icmp(self, is_v6):
+        ICMP46 = ICMPv6EchoRequest if is_v6 else ICMP
+        icmp_type = icmp6_type if is_v6 else icmp4_type
+        icmp_code = icmp6_code if is_v6 else icmp4_code
+
+        # Define filter on ICMP type
+        filters = [
+            VppNpolFilter(FILTER_TYPE_ICMP_TYPE, value=icmp_type, should_match=1)
+        ]
+        valid = (
+            self.base_ip_packet(is_v6)
+            / ICMP46(type=icmp_type, code=icmp_code)
+            / random_payload()
+        )
+        invalid = (
+            self.base_ip_packet(is_v6) / ICMP46(type=11, code=22) / random_payload()
+        )
+        self.do_test_one_rule(filters, [], [valid], [invalid])
+
+        # Define filter on ICMP type  / should match = 0
+        filters = [VppNpolFilter(FILTER_TYPE_ICMP_TYPE, value=11, should_match=0)]
+        invalid = (
+            self.base_ip_packet(is_v6)
+            / ICMP46(type=11, code=icmp_code)
+            / random_payload()
+        )
+        valid = (
+            self.base_ip_packet(is_v6)
+            / ICMP46(type=icmp_type, code=icmp_code)
+            / random_payload()
+        )
+        self.do_test_one_rule(filters, [], [valid], [invalid])
+
+    def test_icmp4_type(self):
+        self.npol_test_icmp(is_v6=False)
+
+    def test_icmp6_type(self):
+        self.npol_test_icmp(is_v6=True)
+
+    def npol_test_icmp_code(self, is_v6):
+        ICMP46 = ICMPv6EchoRequest if is_v6 else ICMP
+        icmp_type = 1 if is_v6 else 3  # Destination unreachable
+        icmp_code = 9  # admin prohibited
+
+        # Define filter on ICMP type
+        filters = [
+            VppNpolFilter(FILTER_TYPE_ICMP_CODE, value=icmp_code, should_match=1)
+        ]
+        valid = (
+            self.base_ip_packet(is_v6)
+            / ICMP46(type=icmp_type, code=icmp_code)
+            / random_payload()
+        )
+        invalid = (
+            self.base_ip_packet(is_v6)
+            / ICMP46(type=icmp_type, code=icmp_code - 1)
+            / random_payload()
+        )
+        self.do_test_one_rule(filters, [], [valid], [invalid])
+
+        # Define filter on ICMP type  / should match = 0
+        filters = [
+            VppNpolFilter(FILTER_TYPE_ICMP_CODE, value=icmp_code, should_match=0)
+        ]
+        valid = (
+            self.base_ip_packet(is_v6)
+            / ICMP46(type=icmp_type, code=icmp_code + 1)
+            / random_payload()
+        )
+        invalid = (
+            self.base_ip_packet(is_v6)
+            / ICMP46(type=icmp_type, code=icmp_code)
+            / random_payload()
+        )
+        self.do_test_one_rule(filters, [], [valid], [invalid])
+
+    def test_icmp4_code(self):
+        self.npol_test_icmp(is_v6=False)
+
+    def test_icmp6_code(self):
+        self.npol_test_icmp(is_v6=True)
+
+    def npol_test_l4proto(self, is_v6, l4proto):
+        filter_value = 0
+        if l4proto == TCP:
+            filter_value = tcp_protocol
+        elif l4proto == UDP:
+            filter_value = udp_protocol
+
+        # Define filter on l4proto type
+        filters = [
+            VppNpolFilter(FILTER_TYPE_L4_PROTO, value=filter_value, should_match=1)
+        ]
+
+        # Send tcp pg0 -> pg1
+        valid = (
+            self.base_ip_packet(is_v6)
+            / l4proto(sport=src_l4, dport=dst_l4)
+            / random_payload()
+        )
+        # send icmp packet (different l4proto) and expect packet is filtered
+        invalid = self.base_ip_packet(is_v6) / ICMP(type=8, code=3) / random_payload()
+        self.do_test_one_rule(filters, [], [valid], [invalid])
+
+        # Define filter on l4proto / should match = 0
+        filters = [
+            VppNpolFilter(FILTER_TYPE_L4_PROTO, value=filter_value, should_match=0)
+        ]
+        # send l4proto packet and expect it is filtered
+        invalid = (
+            self.base_ip_packet(is_v6)
+            / l4proto(sport=src_l4, dport=dst_l4)
+            / random_payload()
+        )
+        # send icmp packet (different l4proto) and expect it is not filtered
+        valid = self.base_ip_packet(is_v6) / ICMP(type=8, code=3) / random_payload()
+        self.do_test_one_rule(filters, [], [valid], [invalid])
+
+    def test_l4proto_tcp4(self):
+        self.npol_test_l4proto(False, TCP)
+
+    def test_l4proto_tcp6(self):
+        self.npol_test_l4proto(True, TCP)
+
+    def test_l4proto_udp4(self):
+        self.npol_test_l4proto(False, UDP)
+
+    def test_l4proto_udp6(self):
+        self.npol_test_l4proto(True, UDP)
+
+    def test_prefixes_ip6(self):
+        self.test_prefixes(True)
+
+    def test_prefixes(self, is_ip6=False):
+        def pload():
+            return TCP(sport=src_l4, dport=dst_l4) / random_payload()
+
+        dst_ip_match = (
+            self.pg1.remote_ip6 + "/128" if is_ip6 else self.pg1.remote_ip4 + "/32"
+        )
+        match = {
+            "is_src": False,
+            "is_not": False,
+            "type": ENTRY_CIDR,
+            "data": {"cidr": dst_ip_match},
+        }
+        valid = self.base_ip_packet(is_ip6) / pload()
+        invalid = self.base_ip_packet(is_ip6, dst_ip2=True) / pload()
+        self.do_test_one_rule([], [match], [valid], [invalid])
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], [invalid], [valid])
+
+        src_ip_match = (
+            self.pg0.remote_ip6 + "/128" if is_ip6 else self.pg0.remote_ip4 + "/32"
+        )
+        match = {
+            "is_src": True,
+            "is_not": False,
+            "type": ENTRY_CIDR,
+            "data": {"cidr": src_ip_match},
+        }
+        valid = self.base_ip_packet(is_ip6) / pload()
+        invalid = self.base_ip_packet(is_ip6, src_ip2=True) / pload()
+        self.do_test_one_rule([], [match], [valid], [invalid])
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], [invalid], [valid])
+
+    def test_port_ranges_ip6(self):
+        self.test_prefixes(True)
+
+    def test_port_ranges(self, is_ip6=False):
+        base = self.base_ip_packet(is_ip6)
+        test_port = 5123
+        # Test all match kinds
+        match = {
+            "is_src": False,
+            "is_not": False,
+            "type": ENTRY_PORT_RANGE,
+            "data": {"port_range": {"start": test_port, "end": test_port}},
+        }
+        valid = base / TCP(sport=test_port, dport=test_port) / random_payload()
+        invalid = [
+            base / TCP(sport=test_port, dport=test_port + 1) / random_payload(),
+            base / TCP(sport=test_port, dport=test_port - 1) / random_payload(),
+        ]
+        self.do_test_one_rule([], [match], [valid], invalid)
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], invalid, [valid])
+
+        match = {
+            "is_src": True,
+            "is_not": False,
+            "type": ENTRY_PORT_RANGE,
+            "data": {"port_range": {"start": test_port, "end": test_port}},
+        }
+        valid = base / TCP(sport=test_port, dport=test_port) / random_payload()
+        invalid = [
+            base / TCP(sport=test_port + 1, dport=test_port) / random_payload(),
+            base / TCP(sport=test_port - 1, dport=test_port) / random_payload(),
+        ]
+        self.do_test_one_rule([], [match], [valid], invalid)
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], invalid, [valid])
+
+        # Test port ranges with several ports & UDP
+        match = {
+            "is_src": False,
+            "is_not": False,
+            "type": ENTRY_PORT_RANGE,
+            "data": {
+                "port_range": {
+                    "start": test_port,
+                    "end": test_port + 10,
+                }
+            },
+        }
+        valid = [
+            base / TCP(sport=test_port, dport=test_port) / random_payload(),
+            base / TCP(sport=test_port, dport=test_port + 5) / random_payload(),
+            base / TCP(sport=test_port, dport=test_port + 10) / random_payload(),
+            base / UDP(sport=test_port, dport=test_port) / random_payload(),
+            base / UDP(sport=test_port, dport=test_port + 5) / random_payload(),
+            base / UDP(sport=test_port, dport=test_port + 10) / random_payload(),
+        ]
+        invalid = [
+            base / TCP(sport=test_port, dport=test_port - 1) / random_payload(),
+            base / TCP(sport=test_port, dport=test_port + 11) / random_payload(),
+        ]
+        self.do_test_one_rule([], [match], valid, invalid)
+
+    def test_ip_ipset_ip6(self):
+        self.test_ip_ipset(True)
+
+    def test_ip_ipset(self, is_ip6=False):
+        def pload():
+            return TCP(sport=src_l4, dport=dst_l4) / random_payload()
+
+        dst_ip_match = (
+            self.pg1.remote_ip6 + "/128" if is_ip6 else self.pg1.remote_ip4 + "/32"
+        )
+        match = {
+            "is_src": False,
+            "is_not": False,
+            "type": ENTRY_IP_SET,
+            "data": {"set_id": {"set_id": self.dst_ip_ipset.vpp_id}},
+        }
+        valid = self.base_ip_packet(is_ip6) / pload()
+        invalid = self.base_ip_packet(is_ip6, dst_ip2=True) / pload()
+        self.do_test_one_rule([], [match], [valid], [invalid])
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], [invalid], [valid])
+
+        src_ip_match = (
+            self.pg0.remote_ip6 + "/128" if is_ip6 else self.pg0.remote_ip4 + "/32"
+        )
+        match = {
+            "is_src": True,
+            "is_not": False,
+            "type": ENTRY_IP_SET,
+            "data": {"set_id": {"set_id": self.src_ip_ipset.vpp_id}},
+        }
+        valid = self.base_ip_packet(is_ip6) / pload()
+        invalid = self.base_ip_packet(is_ip6, src_ip2=True) / pload()
+        self.do_test_one_rule([], [match], [valid], [invalid])
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], [invalid], [valid])
+
+    def test_net_ipset_ip6(self):
+        self.test_net_ipset(True)
+
+    def test_net_ipset(self, is_ip6=False):
+        def pload():
+            return TCP(sport=src_l4, dport=dst_l4) / random_payload()
+
+        dst_ip_match = (
+            self.pg1.remote_ip6 + "/128" if is_ip6 else self.pg1.remote_ip4 + "/32"
+        )
+        match = {
+            "is_src": False,
+            "is_not": False,
+            "type": ENTRY_IP_SET,
+            "data": {"set_id": {"set_id": self.dst_net_ipset.vpp_id}},
+        }
+        valid = self.base_ip_packet(is_ip6) / pload()
+        invalid = self.base_ip_packet(is_ip6, dst_ip2=True) / pload()
+        self.do_test_one_rule([], [match], [valid], [invalid])
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], [invalid], [valid])
+
+        src_ip_match = (
+            self.pg0.remote_ip6 + "/128" if is_ip6 else self.pg0.remote_ip4 + "/32"
+        )
+        match = {
+            "is_src": True,
+            "is_not": False,
+            "type": ENTRY_IP_SET,
+            "data": {"set_id": {"set_id": self.src_net_ipset.vpp_id}},
+        }
+        valid = self.base_ip_packet(is_ip6) / pload()
+        invalid = self.base_ip_packet(is_ip6, src_ip2=True) / pload()
+        self.do_test_one_rule([], [match], [valid], [invalid])
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], [invalid], [valid])
+
+    def test_ipport_ipset_ip6(self):
+        self.test_ipport_ipset(True)
+
+    def test_ipport_ipset(self, is_ip6=False):
+        match = {
+            "is_src": False,
+            "is_not": False,
+            "type": ENTRY_PORT_IP_SET,
+            "data": {"set_id": {"set_id": self.dst_ipport_ipset.vpp_id}},
+        }
+        valid = (
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload()
+        )
+        invalid = [  # Change all criteria: address, proto, port
+            self.base_ip_packet(is_ip6, dst_ip2=True)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / UDP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=src_l4, dport=dst_l4 + 1)
+            / random_payload(),
+        ]
+        self.do_test_one_rule([], [match], [valid], invalid)
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], invalid, [valid])
+
+        match = {
+            "is_src": True,
+            "is_not": False,
+            "type": ENTRY_PORT_IP_SET,
+            "data": {"set_id": {"set_id": self.src_ipport_ipset.vpp_id}},
+        }
+        valid = (
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload()
+        )
+        invalid = [  # Change all criteria: address, proto, port
+            self.base_ip_packet(is_ip6, src_ip2=True)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / UDP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=src_l4 + 1, dport=dst_l4)
+            / random_payload(),
+        ]
+        self.do_test_one_rule([], [match], [valid], invalid)
+
+        match["is_not"] = True
+        self.do_test_one_rule([], [match], invalid, [valid])
+
+    # Calico specificity: if a rule has port ranges and ipport ipsets,
+    # a packet matches the rule if it matches either category
+    def test_port_range_and_ipport_ipset_ip6(self):
+        self.test_port_range_and_ipport_ipset(True)
+
+    def test_port_range_and_ipport_ipset(self, is_ip6=False):
+        # Test all match types to exercies all code (but not all combinations)
+        test_port = 4569
+        matches = [
+            {
+                "is_src": False,
+                "is_not": False,
+                "type": ENTRY_PORT_IP_SET,
+                "data": {"set_id": {"set_id": self.dst_ipport_ipset.vpp_id}},
+            },
+            {
+                "is_src": False,
+                "is_not": False,
+                "type": ENTRY_PORT_RANGE,
+                "data": {"port_range": {"start": test_port, "end": test_port}},
+            },
+        ]
+        valid = [
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=src_l4, dport=test_port)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / UDP(sport=src_l4, dport=test_port)
+            / random_payload(),
+            self.base_ip_packet(is_ip6, src_ip2=True)
+            / TCP(sport=src_l4, dport=test_port)
+            / random_payload(),
+            self.base_ip_packet(is_ip6, dst_ip2=True)
+            / TCP(sport=src_l4, dport=test_port)
+            / random_payload(),
+        ]
+        invalid = [
+            self.base_ip_packet(is_ip6, dst_ip2=True)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / UDP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=src_l4, dport=(dst_l4 + test_port) // 2)
+            / random_payload(),
+        ]
+        self.do_test_one_rule([], matches, valid, invalid)
+
+        for match in matches:
+            match["is_not"] = True
+        self.do_test_one_rule([], matches, invalid, valid)
+
+        matches = [
+            {
+                "is_src": True,
+                "is_not": False,
+                "type": ENTRY_PORT_IP_SET,
+                "data": {"set_id": {"set_id": self.src_ipport_ipset.vpp_id}},
+            },
+            {
+                "is_src": True,
+                "is_not": False,
+                "type": ENTRY_PORT_RANGE,
+                "data": {"port_range": {"start": test_port, "end": test_port}},
+            },
+        ]
+        valid = [
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=test_port, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / UDP(sport=test_port, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6, src_ip2=True)
+            / TCP(sport=test_port, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6, dst_ip2=True)
+            / TCP(sport=test_port, dport=dst_l4)
+            / random_payload(),
+        ]
+        invalid = [
+            self.base_ip_packet(is_ip6, src_ip2=True)
+            / TCP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / UDP(sport=src_l4, dport=dst_l4)
+            / random_payload(),
+            self.base_ip_packet(is_ip6)
+            / TCP(sport=(src_l4 + test_port) // 2, dport=dst_l4)
+            / random_payload(),
+        ]
+        self.do_test_one_rule([], matches, valid, invalid)
+
+        for match in matches:
+            match["is_not"] = True
+        self.do_test_one_rule([], matches, invalid, valid)
+
+
+class TestNpolPolicies(BaseNpolTest):
+    """Network Policies tests"""
+
+    @classmethod
+    def setUpClass(self):
+        super(TestNpolPolicies, self).setUpClass()
+
+    @classmethod
+    def tearDownClass(self):
+        super(TestNpolPolicies, self).tearDownClass()
+
+    def setUp(self):
+        super(TestNpolPolicies, self).setUp()
+
+    def tearDown(self):
+        super(TestNpolPolicies, self).tearDown()
+
+    def tcp_dport_rule(self, port, action):
+        return VppNpolRule(
+            self,
+            is_v6=False,
+            action=action,
+            filters=[VppNpolFilter(FILTER_TYPE_L4_PROTO, tcp_protocol, True)],
+            matches=[
+                {
+                    "is_src": False,
+                    "is_not": False,
+                    "type": ENTRY_PORT_RANGE,
+                    "data": {"port_range": {"start": port, "end": port}},
+                }
+            ],
+        )
+
+    def test_inbound_outbound(self):
+        r = self.tcp_dport_rule(1000, ACTION_ALLOW)
+        r.add_vpp_config()
+        pin = VppNpolPolicy(self, [VppNpolPolicyItem(is_inbound=1, rule_id=r.vpp_id())])
+        pout = VppNpolPolicy(
+            self, [VppNpolPolicyItem(is_inbound=0, rule_id=r.vpp_id())]
+        )
+        pin.add_vpp_config()
+        pout.add_vpp_config()
+
+        matching = self.base_ip_packet() / TCP(sport=1, dport=1000) / random_payload()
+        not_matching = (
+            self.base_ip_packet() / TCP(sport=1, dport=2000) / random_payload()
+        )
+
+        # out policy at src
+        self.configure_policies(self.pg0, [], [pout], [])
+        self.send_test_packets(self.pg0, self.pg1, [matching], [not_matching])
+
+        # policies configured at src + dst
+        self.configure_policies(self.pg1, [pin], [], [])
+        self.send_test_packets(self.pg0, self.pg1, [matching], [not_matching])
+
+        # policies configured at dst
+        self.configure_policies(self.pg0, [], [], [], 0, 0)
+        self.send_test_packets(self.pg0, self.pg1, [matching], [not_matching])
+
+        # no policies
+        self.configure_policies(self.pg1, [], [], [], 0, 0)
+        self.send_test_packets(self.pg0, self.pg1, [matching, not_matching], [])
+
+    def test_default_verdict(self):
+        # If profiles only are configured (pass_id = 0), default is deny
+        # If there are policies + profiles (pass_id > 0), then default
+        # is to deny before evaluating profiles, unless a rule with a PASS
+        # target matches
+        rule1 = self.tcp_dport_rule(1000, ACTION_ALLOW)
+        rule2 = self.tcp_dport_rule(2000, ACTION_ALLOW)
+        rule3 = self.tcp_dport_rule(1000, ACTION_DENY)
+        rule4 = self.tcp_dport_rule(1000, ACTION_PASS)
+        rule1.add_vpp_config()
+        rule2.add_vpp_config()
+        rule3.add_vpp_config()
+        rule4.add_vpp_config()
+        policy1 = VppNpolPolicy(
+            self, [VppNpolPolicyItem(is_inbound=1, rule_id=rule1.vpp_id())]
+        )
+        policy2 = VppNpolPolicy(
+            self, [VppNpolPolicyItem(is_inbound=1, rule_id=rule2.vpp_id())]
+        )
+        policy3 = VppNpolPolicy(
+            self, [VppNpolPolicyItem(is_inbound=1, rule_id=rule3.vpp_id())]
+        )
+        policy4 = VppNpolPolicy(
+            self, [VppNpolPolicyItem(is_inbound=1, rule_id=rule4.vpp_id())]
+        )
+        policy5 = VppNpolPolicy(
+            self,
+            [
+                VppNpolPolicyItem(is_inbound=1, rule_id=rule4.vpp_id()),
+                VppNpolPolicyItem(is_inbound=1, rule_id=rule3.vpp_id()),
+            ],
+        )
+        policy1.add_vpp_config()
+        policy2.add_vpp_config()
+        policy3.add_vpp_config()
+        policy4.add_vpp_config()
+        policy5.add_vpp_config()
+
+        # Test profile default deny: 1 allow rule, pass_id=0
+        self.configure_policies(self.pg1, [], [], [policy1], 2, 2, 1, 1)
+        passing = [self.base_ip_packet() / TCP(sport=1, dport=1000) / random_payload()]
+        dropped = [self.base_ip_packet() / TCP(sport=1, dport=2000) / random_payload()]
+        self.send_test_packets(self.pg0, self.pg1, passing, dropped)
+
+        # Test policy default deny: 1 allow rule, pass_id=1
+        self.configure_policies(self.pg1, [policy1], [], [])
+        self.send_test_packets(self.pg0, self.pg1, passing, dropped)
+
+        # Test that profiles are not executed when policies are configured
+        # 1 allow policy, 1 allow profile, pass_id=1
+        self.configure_policies(self.pg1, [policy1], [], [policy2])
+        self.send_test_packets(self.pg0, self.pg1, passing, dropped)
+
+        # Test that pass target does not evaluate further policies and
+        # jumps to profiles 1 pass policy, 1 deny policy, 1 allow profile,
+        # pass_id=2
+        self.configure_policies(self.pg1, [policy4, policy3], [], [policy1])
+        self.send_test_packets(self.pg0, self.pg1, passing, dropped)
+
+        # Test that pass target does not evaluate further rules in the
+        # policy and jumps to profiles 1 policy w/ 1 pass rule & 1 deny rule,
+        # 1 deny profile, pass_id=1
+        self.configure_policies(self.pg1, [policy5], [], [policy1])
+        self.send_test_packets(self.pg0, self.pg1, passing, dropped)
+
+        policy1.remove_vpp_config()
+        policy2.remove_vpp_config()
+        policy3.remove_vpp_config()
+        policy4.remove_vpp_config()
+        policy5.remove_vpp_config()
+        rule1.remove_vpp_config()
+        rule2.remove_vpp_config()
+        rule3.remove_vpp_config()
+        rule4.remove_vpp_config()
+
+    def test_realistic_policy(self):
+        # Rule 1 allows ping from everywhere
+        rule1 = VppNpolRule(
+            self,
+            is_v6=False,
+            action=ACTION_ALLOW,
+            filters=[
+                VppNpolFilter(FILTER_TYPE_L4_PROTO, icmp_protocol, True),
+                VppNpolFilter(FILTER_TYPE_ICMP_TYPE, 8, True),
+                VppNpolFilter(FILTER_TYPE_ICMP_CODE, 0, True),
+            ],
+            matches=[],
+        )
+        rule1.add_vpp_config()
+        # Rule 2 allows tcp dport 8080 from a single container
+        src_ipset = VppNpolIpset(
+            self,
+            IPSET_TYPE_NET,
+            [self.pg0.remote_ip4 + "/32", self.pg0.remote_ip6 + "/128"],
+        )
+        src_ipset.add_vpp_config()
+        rule2 = VppNpolRule(
+            self,
+            is_v6=False,
+            action=ACTION_ALLOW,
+            filters=[
+                VppNpolFilter(FILTER_TYPE_L4_PROTO, tcp_protocol, True),
+            ],
+            matches=[
+                {
+                    "is_src": True,
+                    "is_not": False,
+                    "type": ENTRY_IP_SET,
+                    "data": {"set_id": {"set_id": src_ipset.vpp_id}},
+                },
+                {
+                    "is_src": False,
+                    "is_not": False,
+                    "type": ENTRY_PORT_RANGE,
+                    "data": {"port_range": {"start": 8080, "end": 8080}},
+                },
+            ],
+        )
+        rule2.add_vpp_config()
+        policy = VppNpolPolicy(
+            self,
+            [
+                VppNpolPolicyItem(is_inbound=1, rule_id=rule1.vpp_id()),
+                VppNpolPolicyItem(is_inbound=1, rule_id=rule2.vpp_id()),
+            ],
+        )
+        policy.add_vpp_config()
+        self.configure_policies(self.pg1, [policy], [], [])
+
+        passing = [
+            self.base_ip_packet() / ICMP(type=8),
+            self.base_ip_packet(src_ip2=True) / ICMP(type=8),
+            self.base_ip_packet() / TCP(sport=1, dport=8080) / random_payload(),
+        ]
+        dropped = [
+            self.base_ip_packet() / ICMP(type=3),
+            self.base_ip_packet(src_ip2=True)
+            / TCP(sport=1, dport=8080)
+            / random_payload(),
+            self.base_ip_packet() / UDP(sport=1, dport=8080) / random_payload(),
+            self.base_ip_packet() / TCP(sport=1, dport=8081) / random_payload(),
+        ]
+        self.send_test_packets(self.pg0, self.pg1, passing, dropped)
+        # Cleanup
+        self.configure_policies(self.pg1, [], [], [], 0, 0)
+        policy.remove_vpp_config()
+        rule1.remove_vpp_config()
+        rule2.remove_vpp_config()
+        src_ipset.remove_vpp_config()