summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
authorNataliia S. <81954790+natali-rs1985@users.noreply.github.com>2025-05-13 17:33:35 +0300
committerGitHub <noreply@github.com>2025-05-13 15:33:35 +0100
commit151afffe15ce89755f3c3f81a9d2c647e487f647 (patch)
treec823ad347680bc0644f073e136848bc2ce13002d /src/conf_mode
parent528248af9628c3170b4eac399f0bb339072d8eae (diff)
downloadvyos-1x-151afffe15ce89755f3c3f81a9d2c647e487f647.tar.gz
vyos-1x-151afffe15ce89755f3c3f81a9d2c647e487f647.zip
T7419: VPP ACL implementation (#31)
CLI: ``` set vpp acl ip tag-name <tag-name> rule <nn> action <permit|deny|permit-reflect> set vpp acl ip tag-name <tag-name> rule <nn> source prefix <prefix> set vpp acl ip tag-name <tag-name> rule <nn> source port <port|range> set vpp acl ip tag-name <tag-name> rule <nn> destination prefix <prefix> set vpp acl ip tag-name <tag-name> rule <nn> destination port <port> set vpp acl ip tag-name <tag-name> rule <nn> protocol <protocol> set vpp acl ip tag-name <tag-name> rule <nn> tcp-flags <fin|syn|rst|psh|ack|urg|ecn|cwr> set vpp acl ip tag-name <tag-name> rule <nn> tcp-flags not <fin|syn|rst|psh|ack|urg|ecn|cwr> set vpp acl ip interface <interface_name> input acl-tag <n> tag-name <tag-name> set vpp acl ip interface <interface_name> output acl-tag <n> tag-name <tag-name> set vpp acl macip tag-name <tag-name> rule <nn> prefix <prefix> set vpp acl macip tag-name <tag-name> rule <nn> mac-address <mac> set vpp acl macip tag-name <tag-name> rule <nn> mac-mask <mac-mask> set vpp acl macip tag-name <tag-name> rule <nn> action <permit|deny> set vpp acl macip interface <interface_name> tag-name <tag-name> ``` OP mode ``` show vpp acl ip tag-name <tag_name> show vpp acl ip interface show vpp acl macip tag-name <tag_name> show vpp acl macip interface ```
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/vpp.py4
-rw-r--r--src/conf_mode/vpp_acl.py393
-rw-r--r--src/conf_mode/vpp_interfaces_bonding.py10
-rw-r--r--src/conf_mode/vpp_interfaces_gre.py10
-rw-r--r--src/conf_mode/vpp_interfaces_ipip.py10
-rw-r--r--src/conf_mode/vpp_interfaces_loopback.py10
-rw-r--r--src/conf_mode/vpp_interfaces_vxlan.py10
7 files changed, 447 insertions, 0 deletions
diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py
index 3718f3897..ed0e80c2c 100755
--- a/src/conf_mode/vpp.py
+++ b/src/conf_mode/vpp.py
@@ -141,6 +141,10 @@ def get_config(config=None):
if conf.exists(['vpp', 'nat', 'cgnat']):
set_dependents('vpp_nat_cgnat', conf)
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
if not conf.exists(base):
return {
'removed_ifaces': removed_ifaces,
diff --git a/src/conf_mode/vpp_acl.py b/src/conf_mode/vpp_acl.py
new file mode 100644
index 000000000..28eae485b
--- /dev/null
+++ b/src/conf_mode/vpp_acl.py
@@ -0,0 +1,393 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import ipaddress
+
+from vyos import ConfigError
+
+from vyos.configdiff import Diff
+from vyos.configdict import node_changed
+from vyos.config import Config
+from vyos.utils.network import get_protocol_by_name
+
+from vyos.vpp.utils import cli_ifaces_list
+from vyos.vpp.acl import Acl
+
+
+# TCP flag names to bit values
+TCP_FLAGS = {
+ 'FIN': 0x01,
+ 'SYN': 0x02,
+ 'RST': 0x04,
+ 'PSH': 0x08,
+ 'ACK': 0x10,
+ 'URG': 0x20,
+ 'ECN': 0x40,
+ 'CWR': 0x80,
+}
+
+# ACL action flags
+action_map = {
+ 'deny': 0,
+ 'permit': 1,
+ 'permit-reflect': 2,
+}
+
+
+def get_tcp_mask_value(set_flags, unset_flags):
+ mask = 0
+ value = 0
+
+ for flag in set_flags + unset_flags:
+ bit = TCP_FLAGS.get(flag.upper())
+ mask |= bit
+ if flag in set_flags:
+ value |= bit
+
+ return mask, value
+
+
+def get_port_first_last(port_range, protocol):
+ first_port = 0
+ last_port = 65535
+ if not port_range:
+ if protocol in ['icmp', 'ipv6-icmp']:
+ last_port = 255
+ elif '-' not in port_range:
+ first_port = last_port = port_range
+ else:
+ first_port, last_port = port_range.split('-')
+ return int(first_port), int(last_port)
+
+
+def create_ip_rules_list(rules):
+ rules_list = []
+ for rule in rules.values():
+ r = {
+ 'is_permit': action_map[rule.get('action')],
+ 'src_prefix': rule.get('source', {}).get('prefix', ''),
+ 'dst_prefix': rule.get('destination', {}).get('prefix', ''),
+ 'proto': (
+ int(get_protocol_by_name(rule.get('protocol')))
+ if rule.get('protocol') != 'all'
+ else 0
+ ),
+ }
+
+ tcp_flags = rule.get('tcp_flags', {})
+ set_flags = [flag for flag in tcp_flags if flag != 'not']
+ unet_flags = list(tcp_flags.get('not', {}).keys())
+ tcp_mask, tcp_value = get_tcp_mask_value(set_flags, unet_flags)
+ r['tcp_flags_mask'] = tcp_mask
+ r['tcp_flags_value'] = tcp_value
+
+ src_ports = rule.get('source', {}).get('port')
+ src_first_port, src_last_port = get_port_first_last(
+ src_ports, rule.get('protocol')
+ )
+ r['srcport_or_icmptype_first'] = src_first_port
+ r['srcport_or_icmptype_last'] = src_last_port
+
+ dst_ports = rule.get('destination', {}).get('port')
+ dst_first_port, dst_last_port = get_port_first_last(
+ dst_ports, rule.get('protocol')
+ )
+ r['dstport_or_icmpcode_first'] = dst_first_port
+ r['dstport_or_icmpcode_last'] = dst_last_port
+
+ rules_list.append(r)
+
+ return rules_list
+
+
+def create_macip_rules_list(rules):
+ rules_list = []
+ for rule in rules.values():
+ r = {
+ 'is_permit': action_map[rule.get('action')],
+ 'src_prefix': rule.get('prefix', ''),
+ 'src_mac': rule.get('mac_address', ''),
+ 'src_mac_mask': rule.get('mac_mask', ''),
+ }
+ rules_list.append(r)
+
+ return rules_list
+
+
+def get_config(config=None) -> dict:
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp', 'acl']
+
+ # Get config_dict with default values
+ config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True,
+ )
+
+ # Get effective config as we need full dictionary for deletion
+ effective_config = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ effective=True,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ if not config:
+ config['remove'] = True
+
+ changed_ip_ifaces = node_changed(
+ conf,
+ base + ['ip', 'interface'],
+ key_mangling=('-', '_'),
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+
+ changed_macip_ifaces = node_changed(
+ conf,
+ base + ['macip', 'interface'],
+ key_mangling=('-', '_'),
+ recursive=True,
+ expand_nodes=Diff.DELETE | Diff.ADD,
+ )
+
+ config.update(
+ {
+ 'changed_ip_ifaces': changed_ip_ifaces,
+ 'changed_macip_ifaces': changed_macip_ifaces,
+ 'vpp_ifaces': cli_ifaces_list(conf),
+ }
+ )
+
+ if effective_config:
+ config.update({'effective': effective_config})
+
+ return config
+
+
+def verify(config):
+ if 'remove' in config:
+ return None
+
+ for acl_type in ['ip', 'macip']:
+ if acl_type in config:
+ acl = config.get(acl_type)
+ if 'tag_name' not in acl:
+ raise ConfigError(f'"tag-name" is required for "acl {acl_type}"')
+
+ for acl_name, acl_config in acl.get('tag_name').items():
+ if 'rule' not in acl_config:
+ raise ConfigError(f'Rules must be configured for ACL {acl_name}')
+
+ for rule, rule_config in acl_config.get('rule').items():
+ err_msg = f'Configuration error for {acl_type} ACL {acl_name} in rule {rule}:'
+ if 'action' not in rule_config:
+ raise ConfigError(f'{err_msg} action must be defined')
+
+ for iface, iface_config in acl.get('interface', {}).items():
+ if iface not in config.get('vpp_ifaces'):
+ raise ConfigError(
+ f'{iface} must be a VPP interface for ACL interface'
+ )
+
+ if 'ip' in config:
+ acl = config.get('ip')
+ for acl_name, acl_config in acl.get('tag_name').items():
+ for rule, rule_config in acl_config.get('rule').items():
+ err_msg = (
+ f'Configuration error for {acl_type} ACL {acl_name} in rule {rule}:'
+ )
+
+ # verify IPv4 and IPv6 address family
+ src_prefix = rule_config.get('source', {}).get('prefix')
+ dst_prefix = rule_config.get('destination', {}).get('prefix')
+ src = ipaddress.ip_network(src_prefix) if src_prefix else None
+ dst = ipaddress.ip_network(dst_prefix) if dst_prefix else None
+
+ if src and dst:
+ if src.version != dst.version:
+ raise ConfigError(
+ f'{err_msg} source and destination prefixes must be from the same IP family'
+ )
+ elif src or dst:
+ family = src.version if src else dst.version
+ if family == 6:
+ raise ConfigError(
+ f'{err_msg} both source and destination prefixes must be defined for IPv6'
+ )
+
+ # verify protocol
+ protocol = rule_config.get('protocol')
+ if protocol != 'all':
+ proto = get_protocol_by_name(protocol)
+ if not isinstance(proto, int) and (
+ not proto.isdigit() or int(proto) > 147
+ ):
+ raise ConfigError(
+ f'{err_msg} protocol name {protocol} is not valid'
+ )
+
+ # verify TCP flags
+ if 'tcp_flags' in rule_config:
+ if rule_config.get('protocol') != 'tcp':
+ raise ConfigError(
+ f'{err_msg} protocol must be tcp when specifying tcp flags'
+ )
+
+ not_flags = rule_config.get('tcp_flags').get('not', [])
+ if not_flags:
+ duplicates = [
+ flag
+ for flag in rule_config.get('tcp_flags')
+ if flag in not_flags
+ ]
+ if duplicates:
+ raise ConfigError(
+ f'{err_msg} cannot match a tcp flag as set and not set: {duplicates}'
+ )
+
+ for iface, iface_config in acl.get('interface', {}).items():
+ if not any(key in iface_config for key in ('input', 'output')):
+ raise ConfigError(
+ f'Please specify direction input/output for interface {iface}'
+ )
+
+ for direction in ['input', 'output']:
+ if direction in iface_config:
+ iface_acl = iface_config.get(direction)
+ if 'acl_tag' not in iface_acl:
+ raise ConfigError(
+ f'"acl-tag" is required for {direction} interface {iface}'
+ )
+
+ used_names = []
+ for tag, tag_conf in iface_acl.get('acl_tag').items():
+ if 'tag_name' not in tag_conf:
+ raise ConfigError(
+ f'"tag-name" is required for {direction} interface {iface} with acl-tag {tag}'
+ )
+ name = tag_conf.get('tag_name')
+ if name not in acl.get('tag_name').keys():
+ raise ConfigError(
+ f'ACL with tag-name {name} does not exist. '
+ f'Cannot use it for {direction} interface {iface}'
+ )
+ if name in used_names:
+ raise ConfigError(
+ f'ACL with tag-name {name} is already used for {direction} interface {iface}'
+ )
+ used_names.append(name)
+
+ if 'macip' in config:
+ acl = config.get('macip')
+ for iface, iface_config in acl.get('interface', {}).items():
+ if 'tag_name' not in iface_config:
+ raise ConfigError(f'"tag-name" is required for interface {iface}')
+ name = iface_config.get('tag_name')
+ if name not in acl.get('tag_name').keys():
+ raise ConfigError(
+ f'ACL with tag-name {name} does not exist. Cannot use it for interface {iface}'
+ )
+
+
+def generate(config):
+ pass
+
+
+def apply(config):
+ acl = Acl()
+
+ if 'effective' in config:
+ # Delete ACL ip
+ if 'ip' in config.get('effective'):
+ remove_config_ip = config.get('effective').get('ip')
+
+ # Delete ACL interfaces
+ for interface in config.get('changed_ip_ifaces'):
+ acl.delete_acl_interface(interface)
+
+ # Delete ACLs
+ for acl_name in remove_config_ip.get('tag_name'):
+ if acl_name not in config.get('ip', {}).get('tag_name', {}):
+ acl.delete_acl(acl_name)
+
+ # Delete ACL macip
+ if 'macip' in config.get('effective'):
+ remove_config_macip = config.get('effective').get('macip')
+
+ # Delete ACL interfaces
+ for interface in config.get('changed_macip_ifaces'):
+ acl.delete_acl_macip_interface(interface)
+
+ # Delete ACL macip
+ for acl_name in remove_config_macip.get('tag_name'):
+ if acl_name not in config.get('macip', {}).get('tag_name', {}):
+ acl.delete_acl_macip(acl_name)
+
+ if 'remove' in config:
+ return None
+
+ # Add or replace ACL ip
+ config_ip = config.get('ip', {})
+ for acl_name in config_ip.get('tag_name', {}):
+ rules = create_ip_rules_list(
+ config_ip.get('tag_name').get(acl_name).get('rule')
+ )
+ acl.add_replace_acl(acl_name, rules)
+
+ for iface, iface_config in config_ip.get('interface', {}).items():
+ input_tags = [
+ v['tag_name']
+ for v in iface_config.get('input', {}).get('acl_tag', {}).values()
+ ]
+ output_tags = [
+ v['tag_name']
+ for v in iface_config.get('output', {}).get('acl_tag', {}).values()
+ ]
+ acl.add_acl_interface(iface, input_tags, output_tags)
+
+ # Add or replace ACL macip
+ config_macip = config.get('macip', {})
+ for acl_name in config_macip.get('tag_name', {}):
+ rules = create_macip_rules_list(
+ config_macip.get('tag_name').get(acl_name).get('rule')
+ )
+ acl.add_replace_acl_macip(acl_name, rules)
+
+ for iface, iface_config in config_macip.get('interface', {}).items():
+ acl.add_acl_macip_interface(iface, iface_config.get('tag_name'))
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpp_interfaces_bonding.py b/src/conf_mode/vpp_interfaces_bonding.py
index 7ab1abb24..2b17acb24 100644
--- a/src/conf_mode/vpp_interfaces_bonding.py
+++ b/src/conf_mode/vpp_interfaces_bonding.py
@@ -139,6 +139,16 @@ def get_config(config=None) -> dict:
if conf.exists(['vpp', 'kernel-interfaces', iface]):
set_dependents('vpp_kernel_interface', conf, iface)
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
config['ifname'] = ifname
return config
diff --git a/src/conf_mode/vpp_interfaces_gre.py b/src/conf_mode/vpp_interfaces_gre.py
index 823f3ff4d..f6da2d55d 100644
--- a/src/conf_mode/vpp_interfaces_gre.py
+++ b/src/conf_mode/vpp_interfaces_gre.py
@@ -110,6 +110,16 @@ def get_config(config=None) -> dict:
if conf.exists(['vpp', 'kernel-interfaces', iface]):
set_dependents('vpp_kernel_interface', conf, iface)
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
config['ifname'] = ifname
return config
diff --git a/src/conf_mode/vpp_interfaces_ipip.py b/src/conf_mode/vpp_interfaces_ipip.py
index c52e7c997..f40a4e243 100644
--- a/src/conf_mode/vpp_interfaces_ipip.py
+++ b/src/conf_mode/vpp_interfaces_ipip.py
@@ -109,6 +109,16 @@ def get_config(config=None) -> dict:
if iface in config.get('vpp_kernel_interfaces'):
set_dependents('vpp_kernel_interface', conf, iface)
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
config['ifname'] = ifname
return config
diff --git a/src/conf_mode/vpp_interfaces_loopback.py b/src/conf_mode/vpp_interfaces_loopback.py
index f0bcdfb18..2b37b5e1c 100644
--- a/src/conf_mode/vpp_interfaces_loopback.py
+++ b/src/conf_mode/vpp_interfaces_loopback.py
@@ -99,6 +99,16 @@ def get_config(config=None) -> dict:
if conf.exists(['vpp', 'kernel-interfaces', iface]):
set_dependents('vpp_kernel_interface', conf, iface)
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
config['ifname'] = ifname
return config
diff --git a/src/conf_mode/vpp_interfaces_vxlan.py b/src/conf_mode/vpp_interfaces_vxlan.py
index 963cd90ec..065a8eafb 100644
--- a/src/conf_mode/vpp_interfaces_vxlan.py
+++ b/src/conf_mode/vpp_interfaces_vxlan.py
@@ -116,6 +116,16 @@ def get_config(config=None) -> dict:
if conf.exists(['vpp', 'kernel-interfaces', iface]):
set_dependents('vpp_kernel_interface', conf, iface)
+ # NAT dependency
+ if conf.exists(['vpp', 'nat44']):
+ set_dependents('vpp_nat', conf)
+ if conf.exists(['vpp', 'nat', 'cgnat']):
+ set_dependents('vpp_nat_cgnat', conf)
+
+ # ACL dependency
+ if conf.exists(['vpp', 'acl']):
+ set_dependents('vpp_acl', conf)
+
config['ifname'] = ifname
return config