From f40fcee5b1b53e62f23db7b4fe509edadfdc65dd Mon Sep 17 00:00:00 2001 From: Andrew Yourtchenko Date: Wed, 18 Aug 2021 20:10:22 +0200 Subject: [PATCH] npol: Network Policies plugin New plugin that implements Network policies in VPP. Type: feature Change-Id: I04449a9b123ae34a35bdd56cfcfe068daef2c253 Signed-off-by: Andrew Yourtchenko Signed-off-by: Nathan Skrzypczak Signed-off-by: Aloys Augustin Signed-off-by: MathiasRaoul Signed-off-by: hedi bouattour --- MAINTAINERS | 7 + docs/developer/plugins/index.rst | 1 + docs/developer/plugins/npol.rst | 1 + docs/spelling_wordlist.txt | 3 + src/plugins/npol/CMakeLists.txt | 20 + src/plugins/npol/FEATURE.yaml | 16 + src/plugins/npol/npol.api | 245 ++++++++ src/plugins/npol/npol.c | 84 +++ src/plugins/npol/npol.h | 42 ++ src/plugins/npol/npol.rst | 124 ++++ src/plugins/npol/npol_api.c | 421 +++++++++++++ src/plugins/npol/npol_format.c | 442 ++++++++++++++ src/plugins/npol/npol_format.h | 19 + src/plugins/npol/npol_interface.c | 224 +++++++ src/plugins/npol/npol_interface.h | 55 ++ src/plugins/npol/npol_ipset.c | 335 +++++++++++ src/plugins/npol/npol_ipset.h | 48 ++ src/plugins/npol/npol_match.c | 749 +++++++++++++++++++++++ src/plugins/npol/npol_match.h | 18 + src/plugins/npol/npol_policy.c | 188 ++++++ src/plugins/npol/npol_policy.h | 38 ++ src/plugins/npol/npol_rule.c | 258 ++++++++ src/plugins/npol/npol_rule.h | 70 +++ test/test_npol.py | 1195 +++++++++++++++++++++++++++++++++++++ 24 files changed, 4603 insertions(+) create mode 120000 docs/developer/plugins/npol.rst create mode 100644 src/plugins/npol/CMakeLists.txt create mode 100644 src/plugins/npol/FEATURE.yaml create mode 100644 src/plugins/npol/npol.api create mode 100644 src/plugins/npol/npol.c create mode 100644 src/plugins/npol/npol.h create mode 100644 src/plugins/npol/npol.rst create mode 100644 src/plugins/npol/npol_api.c create mode 100644 src/plugins/npol/npol_format.c create mode 100644 src/plugins/npol/npol_format.h create mode 100644 src/plugins/npol/npol_interface.c create mode 100644 src/plugins/npol/npol_interface.h create mode 100644 src/plugins/npol/npol_ipset.c create mode 100644 src/plugins/npol/npol_ipset.h create mode 100644 src/plugins/npol/npol_match.c create mode 100644 src/plugins/npol/npol_match.h create mode 100644 src/plugins/npol/npol_policy.c create mode 100644 src/plugins/npol/npol_policy.h create mode 100644 src/plugins/npol/npol_rule.c create mode 100644 src/plugins/npol/npol_rule.h create mode 100644 test/test_npol.py diff --git a/MAINTAINERS b/MAINTAINERS index 7b0dc9d9a99..647dd1b2630 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -769,6 +769,13 @@ M: Nathan Skrzypczak M: Neale Ranns F: src/plugins/cnat +Plugin - NPol +I: npol +M: Aloys Augustin +M: Nathan Skrzypczak +M: Hedi Bouattour +F: src/plugins/npol/ + Plugin - Wireguard I: wireguard M: Artem Glazychev diff --git a/docs/developer/plugins/index.rst b/docs/developer/plugins/index.rst index 064ce6afe9c..edfbbbe868b 100644 --- a/docs/developer/plugins/index.rst +++ b/docs/developer/plugins/index.rst @@ -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 index 00000000000..d138f060d1b --- /dev/null +++ b/docs/developer/plugins/npol.rst @@ -0,0 +1 @@ +../../../src/plugins/npol/npol.rst \ No newline at end of file diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index d22e625ae16..73b26a430f0 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -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 index 00000000000..965b262a636 --- /dev/null +++ b/src/plugins/npol/CMakeLists.txt @@ -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 index 00000000000..25a1472a953 --- /dev/null +++ b/src/plugins/npol/FEATURE.yaml @@ -0,0 +1,16 @@ +--- +name: Network Policy +maintainer: Nathan Skrzypczak +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 index 00000000000..b4c48d30a13 --- /dev/null +++ b/src/plugins/npol/npol.api @@ -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 ",(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 index 00000000000..6fce47f3e89 --- /dev/null +++ b/src/plugins/npol/npol.c @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include +#include +#include + +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 [|sw_if_index ] [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 index 00000000000..2543dd9336d --- /dev/null +++ b/src/plugins/npol/npol.h @@ -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 +#include +#include + +#include +#include +#include + +#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 index 00000000000..d6f2dcab007 --- /dev/null +++ b/src/plugins/npol/npol.rst @@ -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 tx ... + 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 index 00000000000..6d2061a7583 --- /dev/null +++ b/src/plugins/npol/npol_api.c @@ -0,0 +1,421 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define REPLY_MSG_ID_BASE cpm->msg_id_base +#include + +#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 +#include + +#include +#include + +/* Declare message IDs */ +#include +#include +#undef vl_print +#define vl_print(handle, ...) +#undef vl_print +#define vl_endianfun /* define message structures */ +#include +#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 index 00000000000..e79e2d7274a --- /dev/null +++ b/src/plugins/npol/npol_format.c @@ -0,0 +1,442 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include +#include +#include +#include + +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, ""); + 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 index 00000000000..a9c97010fa5 --- /dev/null +++ b/src/plugins/npol/npol_format.h @@ -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 index 00000000000..1fba8b7fb92 --- /dev/null +++ b/src/plugins/npol/npol_interface.c @@ -0,0 +1,224 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include +#include +#include +#include + +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 " + " tx rx-policy-def " + "tx-policy-def " + "rx-profile-def tx-profile-def " + " [invert] ...", +}; diff --git a/src/plugins/npol/npol_interface.h b/src/plugins/npol/npol_interface.h new file mode 100644 index 00000000000..27a1e5f82c7 --- /dev/null +++ b/src/plugins/npol/npol_interface.h @@ -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 + +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 index 00000000000..52556ac3b5a --- /dev/null +++ b/src/plugins/npol/npol_ipset.c @@ -0,0 +1,335 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include +#include +#include + +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 index 00000000000..509324809a5 --- /dev/null +++ b/src/plugins/npol/npol_ipset.h @@ -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 + +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 index 00000000000..645e003e914 --- /dev/null +++ b/src/plugins/npol/npol_match.c @@ -0,0 +1,749 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include + +#include +#include + +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 index 00000000000..259717c1421 --- /dev/null +++ b/src/plugins/npol/npol_match.h @@ -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 +#include + +#include +#include +#include + +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 index 00000000000..42b206b0b1d --- /dev/null +++ b/src/plugins/npol/npol_policy.c @@ -0,0 +1,188 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include +#include +#include +#include + +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 index 00000000000..0d23fb59f3e --- /dev/null +++ b/src/plugins/npol/npol_policy.h @@ -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 + +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 index 00000000000..838bd2a9515 --- /dev/null +++ b/src/plugins/npol/npol_rule.c @@ -0,0 +1,258 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include +#include +#include +#include + +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 index 00000000000..1698aaf1efa --- /dev/null +++ b/src/plugins/npol/npol_rule.h @@ -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 + +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 index 00000000000..a01af78ec3c --- /dev/null +++ b/test/test_npol.py @@ -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() -- 2.16.6