diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 2 | ||||
-rwxr-xr-x[-rw-r--r--] | data/templates/firewall/nftables.j2 | 6 | ||||
-rwxr-xr-x[-rw-r--r--] | interface-definitions/firewall.xml.in | 2 | ||||
-rw-r--r-- | op-mode-definitions/execute.xml.in | 8 | ||||
-rw-r--r-- | python/vyos/defaults.py | 3 | ||||
-rw-r--r-- | python/vyos/opmode.py | 2 | ||||
-rwxr-xr-x[-rw-r--r--] | python/vyos/template.py | 3 | ||||
-rw-r--r-- | python/vyos/utils/file.py | 2 | ||||
-rw-r--r-- | python/vyos/xml_ref/__init__.py | 23 | ||||
-rwxr-xr-x | python/vyos/xml_ref/generate_op_cache.py | 176 | ||||
-rw-r--r-- | python/vyos/xml_ref/op_definition.py | 49 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_firewall.py | 14 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_system_conntrack.py | 1 | ||||
-rw-r--r-- | src/opt/vyatta/etc/shell/level/users/allowed-op | 1 | ||||
-rw-r--r-- | src/opt/vyatta/etc/shell/level/users/allowed-op.in | 1 |
16 files changed, 279 insertions, 16 deletions
diff --git a/.gitignore b/.gitignore index 01333d5b1..c597d9c84 100644 --- a/.gitignore +++ b/.gitignore @@ -145,6 +145,8 @@ data/component-versions.json # vyos-1x XML cache python/vyos/xml_ref/cache.py python/vyos/xml_ref/pkg_cache/*_cache.py +python/vyos/xml_ref/op_cache.py +python/vyos/xml_ref/pkg_cache/*_op_cache.py # autogenerated vyos-configd JSON definition data/configd-include.json @@ -55,6 +55,8 @@ op_mode_definitions: $(op_xml_obj) find $(BUILD_DIR)/op-mode-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) || exit 1 + $(CURDIR)/python/vyos/xml_ref/generate_op_cache.py --xml-dir $(BUILD_DIR)/op-mode-definitions || exit 1 + # XXX: tcpdump, ping, traceroute and mtr must be able to recursivly call themselves as the # options are provided from the scripts themselves ln -s ../node.tag $(OP_TMPL_DIR)/ping/node.tag/node.tag/ diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index 82dcefac0..155b7f4d0 100644..100755 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -135,7 +135,7 @@ table ip vyos_filter { {% endif %} {% endfor %} {% endif %} - {{ conf | nft_default_rule(name_text, 'ipv4') }} + {{ conf | nft_default_rule('NAM-' + name_text, 'ipv4') }} } {% endfor %} {% endif %} @@ -287,7 +287,7 @@ table ip6 vyos_filter { {% endif %} {% endfor %} {% endif %} - {{ conf | nft_default_rule(name_text, 'ipv6') }} + {{ conf | nft_default_rule('NAM-' + name_text, 'ipv6') }} } {% endfor %} {% endif %} @@ -416,7 +416,7 @@ table bridge vyos_filter { {% endif %} {% endfor %} {% endif %} - {{ conf | nft_default_rule(name_text, 'bri') }} + {{ conf | nft_default_rule('NAM-' + name_text, 'bri') }} } {% endfor %} {% endif %} diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 816dd1855..07c88f799 100644..100755 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="firewall" owner="${vyos_conf_scripts_dir}/firewall.py"> <properties> - <priority>319</priority> + <priority>489</priority> <help>Firewall</help> </properties> <children> diff --git a/op-mode-definitions/execute.xml.in b/op-mode-definitions/execute.xml.in new file mode 100644 index 000000000..66069c927 --- /dev/null +++ b/op-mode-definitions/execute.xml.in @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="execute"> + <properties> + <help>Initiate an operation</help> + </properties> + </node> +</interfaceDefinition>
\ No newline at end of file diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 25ee45391..dec619d3e 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -35,7 +35,8 @@ directories = { 'vyos_udev_dir' : '/run/udev/vyos', 'isc_dhclient_dir' : '/run/dhclient', 'dhcp6_client_dir' : '/run/dhcp6c', - 'vyos_configdir' : '/opt/vyatta/config' + 'vyos_configdir' : '/opt/vyatta/config', + 'completion_dir' : f'{base_dir}/completion' } config_status = '/tmp/vyos-config-status' diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index a6c64adfb..066c8058f 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -89,7 +89,7 @@ class InternalError(Error): def _is_op_mode_function_name(name): - if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew|release)", name): + if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew|release|execute)", name): return True else: return False diff --git a/python/vyos/template.py b/python/vyos/template.py index aa99bed5a..be9f781a6 100644..100755 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -694,7 +694,8 @@ def conntrack_rule(rule_conf, rule_id, action, ipv6=False): else: for protocol, protocol_config in rule_conf['protocol'].items(): proto = protocol - output.append(f'meta l4proto {proto}') + if proto != 'all': + output.append(f'meta l4proto {proto}') tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') if tcp_flags and action != 'timeout': diff --git a/python/vyos/utils/file.py b/python/vyos/utils/file.py index c566f0334..eaebb57a3 100644 --- a/python/vyos/utils/file.py +++ b/python/vyos/utils/file.py @@ -51,7 +51,7 @@ def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=N If directory of file is not present, it is auto-created. """ dirname = os.path.dirname(fname) - if not os.path.isdir(dirname): + if dirname and not os.path.isdir(dirname): os.makedirs(dirname, mode=0o755, exist_ok=False) chown(dirname, user, group) diff --git a/python/vyos/xml_ref/__init__.py b/python/vyos/xml_ref/__init__.py index 2ba3da4e8..91ce394f7 100644 --- a/python/vyos/xml_ref/__init__.py +++ b/python/vyos/xml_ref/__init__.py @@ -15,6 +15,7 @@ from typing import Optional, Union, TYPE_CHECKING from vyos.xml_ref import definition +from vyos.xml_ref import op_definition if TYPE_CHECKING: from vyos.config import ConfigDict @@ -87,3 +88,25 @@ def from_source(d: dict, path: list) -> bool: def ext_dict_merge(source: dict, destination: Union[dict, 'ConfigDict']): return definition.ext_dict_merge(source, destination) + +def load_op_reference(op_cache=[]): + if op_cache: + return op_cache[0] + + op_xml = op_definition.OpXml() + + try: + from vyos.xml_ref.op_cache import op_reference + except Exception: + raise ImportError('no xml op reference cache !!') + + if not op_reference: + raise ValueError('empty xml op reference cache !!') + + op_xml.define(op_reference) + op_cache.append(op_xml) + + return op_xml + +def get_op_ref_path(path: list) -> list[op_definition.PathData]: + return load_op_reference()._get_op_ref_path(path) diff --git a/python/vyos/xml_ref/generate_op_cache.py b/python/vyos/xml_ref/generate_op_cache.py new file mode 100755 index 000000000..e93b07974 --- /dev/null +++ b/python/vyos/xml_ref/generate_op_cache.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# 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, see <http://www.gnu.org/licenses/>. +# +# + +import re +import sys +import json +import glob +from argparse import ArgumentParser +from argparse import ArgumentTypeError +from os.path import join +from os.path import abspath +from os.path import dirname +from xml.etree import ElementTree as ET +from xml.etree.ElementTree import Element +from typing import TypeAlias +from typing import Optional + +_here = dirname(__file__) + +sys.path.append(join(_here, '..')) +from defaults import directories + +from op_definition import NodeData +from op_definition import PathData + +xml_op_cache_json = 'xml_op_cache.json' +xml_op_tmp = join('/tmp', xml_op_cache_json) +op_ref_cache = abspath(join(_here, 'op_cache.py')) + +OptElement: TypeAlias = Optional[Element] +DEBUG = False + + +def translate_exec(s: str) -> str: + s = s.replace('${vyos_op_scripts_dir}', directories['op_mode']) + s = s.replace('${vyos_libexec_dir}', directories['base']) + return s + + +def translate_position(s: str, pos: list[str]) -> str: + pos = pos.copy() + pat: re.Pattern = re.compile(r'(?:\")?\${?([0-9]+)}?(?:\")?') + t: str = pat.sub(r'_place_holder_\1_', s) + + # preferred to .format(*list) to avoid collisions with braces + for i, p in enumerate(pos): + t = t.replace(f'_place_holder_{i+1}_', p) + + return t + + +def translate_command(s: str, pos: list[str]) -> str: + s = translate_exec(s) + s = translate_position(s, pos) + return s + + +def translate_op_script(s: str) -> str: + s = s.replace('${vyos_completion_dir}', directories['completion_dir']) + s = s.replace('${vyos_op_scripts_dir}', directories['op_mode']) + return s + + +def insert_node(n: Element, l: list[PathData], path = None) -> None: + # pylint: disable=too-many-locals,too-many-branches + prop: OptElement = n.find('properties') + children: OptElement = n.find('children') + command: OptElement = n.find('command') + # name is not None as required by schema + name: str = n.get('name', 'schema_error') + node_type: str = n.tag + if path is None: + path = [] + + path.append(name) + if node_type == 'tagNode': + path.append(f'{name}-tag_value') + + help_prop: OptElement = None if prop is None else prop.find('help') + help_text = None if help_prop is None else help_prop.text + command_text = None if command is None else command.text + if command_text is not None: + command_text = translate_command(command_text, path) + + comp_help = None + if prop is not None: + che = prop.findall("completionHelp") + for c in che: + lists = c.findall("list") + paths = c.findall("path") + scripts = c.findall("script") + + comp_help = {} + list_l = [] + for i in lists: + list_l.append(i.text) + path_l = [] + for i in paths: + path_str = re.sub(r'\s+', '/', i.text) + path_l.append(path_str) + script_l = [] + for i in scripts: + script_str = translate_op_script(i.text) + script_l.append(script_str) + + comp_help['list'] = list_l + comp_help['fs_path'] = path_l + comp_help['script'] = script_l + + for d in l: + if name in list(d): + break + else: + d = {} + l.append(d) + + inner_l = d.setdefault(name, []) + + inner_d: PathData = {'node_data': NodeData(node_type=node_type, + help_text=help_text, + comp_help=comp_help, + command=command_text, + path=path)} + inner_l.append(inner_d) + + if children is not None: + inner_nodes = children.iterfind("*") + for inner_n in inner_nodes: + inner_path = path[:] + insert_node(inner_n, inner_l, inner_path) + + +def parse_file(file_path, l): + tree = ET.parse(file_path) + root = tree.getroot() + for n in root.iterfind("*"): + insert_node(n, l) + + +def main(): + parser = ArgumentParser(description='generate dict from xml defintions') + parser.add_argument('--xml-dir', type=str, required=True, + help='transcluded xml op-mode-definition file') + + args = vars(parser.parse_args()) + + xml_dir = abspath(args['xml_dir']) + + l = [] + + for fname in glob.glob(f'{xml_dir}/*.xml'): + parse_file(fname, l) + + with open(xml_op_tmp, 'w') as f: + json.dump(l, f, indent=2) + + with open(op_ref_cache, 'w') as f: + f.write(f'op_reference = {str(l)}') + +if __name__ == '__main__': + main() diff --git a/python/vyos/xml_ref/op_definition.py b/python/vyos/xml_ref/op_definition.py new file mode 100644 index 000000000..914f3a105 --- /dev/null +++ b/python/vyos/xml_ref/op_definition.py @@ -0,0 +1,49 @@ +# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +from typing import TypedDict +from typing import TypeAlias +from typing import Optional +from typing import Union + + +class NodeData(TypedDict): + node_type: Optional[str] + help_text: Optional[str] + comp_help: Optional[dict[str, list]] + command: Optional[str] + path: Optional[list[str]] + + +PathData: TypeAlias = dict[str, Union[NodeData|list['PathData']]] + + +class OpXml: + def __init__(self): + self.op_ref = {} + + def define(self, op_ref: list[PathData]) -> None: + self.op_ref = op_ref + + def _get_op_ref_path(self, path: list[str]) -> list[PathData]: + def _get_path_list(path: list[str], l: list[PathData]) -> list[PathData]: + if not path: + return l + for d in l: + if path[0] in list(d): + return _get_path_list(path[1:], d[path[0]]) + return [] + l = self.op_ref + return _get_path_list(path, l) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 8aeeff149..b8031eed0 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -280,7 +280,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['chain NAME_smoketest'], ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[ipv4-NAM-smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'], ['tcp flags syn / syn,ack', 'tcp dport 8888', 'log prefix "[ipv4-NAM-smoketest-2-R]" log level err', 'ip ttl > 102', 'reject'], - ['log prefix "[ipv4-smoketest-default-D]"','smoketest default-action', 'drop'] + ['log prefix "[ipv4-NAM-smoketest-default-D]"','smoketest default-action', 'drop'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -341,7 +341,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): [f'chain NAME_{name}'], ['ip length { 64, 512, 1024 }', 'ip dscp { 0x11, 0x34 }', f'log prefix "[ipv4-NAM-{name}-6-A]" log group 66 snaplen 6666 queue-threshold 32000', 'accept'], ['ip length 1-30000', 'ip length != 60000-65535', 'ip dscp 0x03-0x0b', 'ip dscp != 0x15-0x19', 'accept'], - [f'log prefix "[ipv4-{name}-default-D]"', 'drop'] + [f'log prefix "[ipv4-NAM-{name}-default-D]"', 'drop'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -511,7 +511,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['PRE-raw default-action accept', 'accept'], [f'chain NAME6_{name}'], ['saddr 2002::1-2002::10', 'daddr 2002::1:1', 'log prefix "[ipv6-NAM-v6-smoketest-1-A]" log level crit', 'accept'], - [f'"{name} default-action drop"', f'log prefix "[ipv6-{name}-default-D]"', 'drop'], + [f'"NAM-{name} default-action drop"', f'log prefix "[ipv6-NAM-{name}-default-D]"', 'drop'], ['jump VYOS_STATE_POLICY6'], ['chain VYOS_STATE_POLICY6'], ['ct state established', 'accept'], @@ -522,9 +522,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_filter') def test_ipv6_advanced(self): - name = 'v6-smoketest-adv' - name2 = 'v6-smoketest-adv2' - interface = 'eth0' + name = 'v6-smoke-adv' self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv6', 'name', name, 'default-log']) @@ -559,7 +557,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['ip6 saddr 2001:db8::/64', 'meta mark != 0x000019ff-0x00001e56', f'jump NAME6_{name}'], [f'chain NAME6_{name}'], ['ip6 length { 65, 513, 1025 }', 'ip6 dscp { af21, 0x35 }', 'accept'], - [f'log prefix "[ipv6-{name}-default-D]"', 'drop'] + [f'log prefix "[ipv6-NAM-{name}-default-D]"', 'drop'] ] self.verify_nftables(nftables_search, 'ip6 vyos_filter') @@ -686,7 +684,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['ct state new', 'ct status dnat', 'accept'], ['ct state { established, new }', 'ct status snat', 'accept'], ['ct state related', 'ct helper { "ftp", "pptp" }', 'accept'], - ['drop', f'comment "{name} default-action drop"'] + ['drop', f'comment "NAM-{name} default-action drop"'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index c07fdce77..72deb7525 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -209,6 +209,7 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'source', 'address', '192.0.2.1']) self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'destination', 'group', 'address-group', address_group]) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'protocol', 'all']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'source', 'address', 'fe80::1']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'destination', 'address', 'fe80::2']) diff --git a/src/opt/vyatta/etc/shell/level/users/allowed-op b/src/opt/vyatta/etc/shell/level/users/allowed-op index 74c45af37..381fd26e5 100644 --- a/src/opt/vyatta/etc/shell/level/users/allowed-op +++ b/src/opt/vyatta/etc/shell/level/users/allowed-op @@ -6,6 +6,7 @@ clear connect delete disconnect +execute exit force monitor diff --git a/src/opt/vyatta/etc/shell/level/users/allowed-op.in b/src/opt/vyatta/etc/shell/level/users/allowed-op.in index 1976904e4..9752f99a2 100644 --- a/src/opt/vyatta/etc/shell/level/users/allowed-op.in +++ b/src/opt/vyatta/etc/shell/level/users/allowed-op.in @@ -2,6 +2,7 @@ clear connect delete disconnect +execute exit force monitor |