From 4e2354c3d8c1cf18ad79b2aa2f6b9767fb4072b4 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 16 Jan 2023 12:03:34 +0000 Subject: [PATCH] Add acl.get_network_list() + tests; Update docs to reference the ability to use prefixlist as a source/destination --- docs/config-guide.md | 39 +++++++++++++++++++++++++++++++++++++-- vppcfg/config/acl.py | 27 +++++++++++++++++++++++++++ vppcfg/config/test_acl.py | 27 +++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/docs/config-guide.md b/docs/config-guide.md index 947fad4..3beaaac 100644 --- a/docs/config-guide.md +++ b/docs/config-guide.md @@ -359,6 +359,39 @@ interfaces: exact-match: False ``` +### Prefix Lists + +This construct allows to enumerate a list of IPv4 or IPv6 host addresses and/or networks. Each +prefixlist has a name which consists of anywhere between 1 and 56 characters, and it must start +with a letter. The syntax is straight forward: + +* ***description***: A string, no longer than 64 characters, and excluding the single quote ' + and double quote ". This string is currently not used anywhere, and serves for enduser + documentation purposes. +* ***members***: A list of zero or more entries which can take the form: + * ***IPv4 Host***: an IPv4 address, eg. `192.0.2.1` + * ***IPv4 Prefix***: an IPv6 prefix, eg. `192.0.2.0/24` + * ***IPv6 Host***: an IPv4 address, eg. `2001:db8::1` + * ***IPv6 Prefix***: an IPv6 prefix, eg. `2001:db8::0/64` + +***NOTE***: It is valid to have host addresses with prefixlen, for example `192.168.1.1/24` +in other words, the prefix can be either a network or a host. + +A few examples: +``` +prefixlists: + example: + description: "An example prefixlist with hosts and prefixes" + members: + - 192.0.2.1 + - 192.0.2.0/24 + - 2001:db8::1 + - 2001:db8::/64 + empty: + description: "An empty prefixlist" + members: [] +``` + ### Access Control Lists In VPP, a common firewall function is provided by the `acl-plugin`. The anatomy of this plugin @@ -377,8 +410,10 @@ packets then either perform an action of `permit` or `deny` (for stateless) or ` * ***family***: Which IP address family to match, can be either `ipv4`, or `ipv6` or `any`, which is the default. If `any` is used, this term will also operate on any source and destination addresses, and it will emit two ACEs, one for each address family. - * ***source***: The IPv4 or IPv6 source prefix, eg. `192.0.2.0/24` or `2001:db8::/64`. If - left empty, this means any (ie. `0.0.0.0/0` or `::/0`). + * ***source***: Either an IPv4 or IPv6 host (without prefixlen, eg. `192.0.2.1` or + `2001:db8::1`), an IPv4 or IPv6 prefix (with prefixlen, eg. `192.0.2.0/24` or + `2001:db8::/64`), or a reference to the name of an existing _prefixlist_ (eg. `trusted`). + If left empty, this means all IPv4 and IPv6 (ie. `[ 0.0.0.0/0, ::/0 ]`). * ***destination***: Similar to `source`, but for the destination field of the packets. * ***protocol***: The L4 protocol, can be either a numeric value (eg. `6`), or a symbolic string value from `/etc/protocols` (eg. `tcp`). If omitted, only L3 matches are performed. diff --git a/vppcfg/config/acl.py b/vppcfg/config/acl.py index 8aef561..68d9e74 100644 --- a/vppcfg/config/acl.py +++ b/vppcfg/config/acl.py @@ -15,6 +15,7 @@ import logging import socket import ipaddress +from . import prefixlist def get_acls(yaml): @@ -151,6 +152,32 @@ def get_port_low_high(portstring): return None, None +def is_ip(ip_string): + """Return True if the given ip_string is either an IPv4/IPv6 address or prefix.""" + if not isinstance(ip_string, str): + return False + + try: + ipn = ipaddress.ip_network(ip_string, strict=False) + return True + except: + pass + return False + + +def get_network_list(yaml, network_string): + """Return the full list of source or destination address(es). This function resolves the + 'source' or 'destination' field, which can either be an IP address, a Prefix, or the name + of a Prefix List. It returns a list of ip_network() objects, including prefix. IP addresses + will receive prefixlen /32 or /128.""" + + if is_ip(network_string): + ipn = ipaddress.ip_network(network_string, strict=False) + return [ipn] + + return prefixlist.get_network_list(yaml, network_string) + + def get_protocol(protostring): """For a given protocol string, which can be either an integer or a symbolic port name in /etc/protocols, return the protocol number as integer, or None if it cannot diff --git a/vppcfg/config/test_acl.py b/vppcfg/config/test_acl.py index 984b7b1..d307849 100644 --- a/vppcfg/config/test_acl.py +++ b/vppcfg/config/test_acl.py @@ -113,3 +113,30 @@ class TestACLMethods(unittest.TestCase): lo, hi = acl.get_icmp_low_high("10-20") self.assertEqual(10, lo) self.assertEqual(20, hi) + + def test_is_ip(self): + self.assertTrue(acl.is_ip("192.0.2.1")) + self.assertTrue(acl.is_ip("192.0.2.1/24")) + self.assertTrue(acl.is_ip("192.0.2.0/24")) + self.assertTrue(acl.is_ip("2001:db8::1")) + self.assertTrue(acl.is_ip("2001:db8::1/64")) + self.assertTrue(acl.is_ip("2001:db8::/64")) + self.assertFalse(acl.is_ip(True)) + self.assertFalse(acl.is_ip("String")) + self.assertFalse(acl.is_ip([])) + self.assertFalse(acl.is_ip({})) + + def test_get_network_list(self): + for s in ["192.0.2.1", "192.0.2.1/24", "2001:db8::1", "2001:db8::1/64"]: + l = acl.get_network_list(self.cfg, s) + self.assertIsInstance(l, list) + self.assertEquals(1, len(l)) + n = l[0] + + l = acl.get_network_list(self.cfg, "trusted") + self.assertIsInstance(l, list) + self.assertEquals(4, len(l)) + + l = acl.get_network_list(self.cfg, "pl-notexist") + self.assertIsInstance(l, list) + self.assertEquals(0, len(l))