diff options
29 files changed, 511 insertions, 101 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 933894447..cd348ead7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,8 +18,8 @@ the box, please use [x] - [ ] Other (please describe): ## Related Task(s) -<!-- All submitted PRs must be linked to a Task on Phabricator. --> -* https://vyos.dev/Txxxx +<!-- optional: Link to related other tasks on Phabricator. --> +<!-- * https://vyos.dev/Txxxx --> ## Related PR(s) <!-- Link here any PRs in other repositories that are required by this PR --> diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i index fdd62b63d..02e7ab057 100644 --- a/interface-definitions/include/interface/vif-s.xml.i +++ b/interface-definitions/include/interface/vif-s.xml.i @@ -18,27 +18,7 @@ #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> - <leafNode name="protocol"> - <properties> - <help>Protocol used for service VLAN (default: 802.1ad)</help> - <completionHelp> - <list>802.1ad 802.1q</list> - </completionHelp> - <valueHelp> - <format>802.1ad</format> - <description>Provider Bridging (IEEE 802.1ad, Q-inQ), ethertype 0x88a8</description> - </valueHelp> - <valueHelp> - <format>802.1q</format> - <description>VLAN-tagged frame (IEEE 802.1q), ethertype 0x8100</description> - </valueHelp> - <constraint> - <regex>(802.1q|802.1ad)</regex> - </constraint> - <constraintErrorMessage>Ethertype must be 802.1ad or 802.1q</constraintErrorMessage> - </properties> - <defaultValue>802.1ad</defaultValue> - </leafNode> + #include <include/interface/vlan-protocol.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> diff --git a/interface-definitions/include/interface/vlan-protocol.xml.i b/interface-definitions/include/interface/vlan-protocol.xml.i new file mode 100644 index 000000000..2fe8d65d7 --- /dev/null +++ b/interface-definitions/include/interface/vlan-protocol.xml.i @@ -0,0 +1,23 @@ +<!-- include start from interface/vif.xml.i --> +<leafNode name="protocol"> + <properties> + <help>Protocol used for service VLAN (default: 802.1ad)</help> + <completionHelp> + <list>802.1ad 802.1q</list> + </completionHelp> + <valueHelp> + <format>802.1ad</format> + <description>Provider Bridging (IEEE 802.1ad, Q-inQ), ethertype 0x88a8</description> + </valueHelp> + <valueHelp> + <format>802.1q</format> + <description>VLAN-tagged frame (IEEE 802.1q), ethertype 0x8100</description> + </valueHelp> + <constraint> + <regex>(802.1q|802.1ad)</regex> + </constraint> + <constraintErrorMessage>Ethertype must be 802.1ad or 802.1q</constraintErrorMessage> + </properties> + <defaultValue>802.1ad</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/mtu.xml.i b/interface-definitions/include/qos/mtu.xml.i new file mode 100644 index 000000000..161d4c27f --- /dev/null +++ b/interface-definitions/include/qos/mtu.xml.i @@ -0,0 +1,14 @@ +<!-- include start from qos/mtu.xml.i --> +<leafNode name="mtu"> + <properties> + <help>MTU size for this class</help> + <valueHelp> + <format>u32:256-65535</format> + <description>Bytes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 256-65535"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/interfaces_bridge.xml.in b/interface-definitions/interfaces_bridge.xml.in index d4d277cfc..7fb5f121a 100644 --- a/interface-definitions/interfaces_bridge.xml.in +++ b/interface-definitions/interfaces_bridge.xml.in @@ -98,6 +98,10 @@ <valueless/> </properties> </leafNode> + #include <include/interface/vlan-protocol.xml.i> + <leafNode name="protocol"> + <defaultValue>802.1q</defaultValue> + </leafNode> <leafNode name="max-age"> <properties> <help>Interval at which neighbor bridges are removed</help> diff --git a/interface-definitions/qos.xml.in b/interface-definitions/qos.xml.in index 31b9a7d21..7618c3027 100644 --- a/interface-definitions/qos.xml.in +++ b/interface-definitions/qos.xml.in @@ -278,6 +278,7 @@ #include <include/generic-description.xml.i> #include <include/qos/bandwidth.xml.i> #include <include/qos/burst.xml.i> + #include <include/qos/mtu.xml.i> #include <include/qos/class-police-exceed.xml.i> #include <include/qos/class-match.xml.i> #include <include/qos/class-priority.xml.i> @@ -293,6 +294,7 @@ <children> #include <include/qos/bandwidth.xml.i> #include <include/qos/burst.xml.i> + #include <include/qos/mtu.xml.i> #include <include/qos/class-police-exceed.xml.i> </children> </node> diff --git a/interface-definitions/service_config-sync.xml.in b/interface-definitions/service_config-sync.xml.in index 9e9dcdb69..17f59340d 100644 --- a/interface-definitions/service_config-sync.xml.in +++ b/interface-definitions/service_config-sync.xml.in @@ -38,11 +38,11 @@ <properties> <help>Connection API timeout</help> <valueHelp> - <format>u32:1-300</format> + <format>u32:1-3600</format> <description>Connection API timeout</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-300"/> + <validator name="numeric" argument="--range 1-3600"/> </constraint> </properties> <defaultValue>60</defaultValue> diff --git a/op-mode-definitions/container.xml.in b/op-mode-definitions/container.xml.in index 4aa13e913..bb6f97b02 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -103,12 +103,28 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/container.py show_container</command> <children> - <leafNode name="image"> + <node name="json"> + <properties> + <help>Show containers in JSON format</help> + </properties> + <!-- no admin check --> + <command>sudo ${vyos_op_scripts_dir}/container.py show_container --raw</command> + </node> + <node name="image"> <properties> <help>Show container image</help> </properties> <command>sudo ${vyos_op_scripts_dir}/container.py show_image</command> - </leafNode> + <children> + <node name="json"> + <properties> + <help>Show container image in JSON format</help> + </properties> + <!-- no admin check --> + <command>sudo ${vyos_op_scripts_dir}/container.py show_image --raw</command> + </node> + </children> + </node> <tagNode name="log"> <properties> <help>Show logs from a given container</help> @@ -116,14 +132,25 @@ <path>container name</path> </completionHelp> </properties> + <!-- no admin check --> <command>sudo podman logs --names "$4"</command> </tagNode> - <leafNode name="network"> + <node name="network"> <properties> <help>Show available container networks</help> </properties> + <!-- no admin check --> <command>sudo ${vyos_op_scripts_dir}/container.py show_network</command> - </leafNode> + <children> + <node name="json"> + <properties> + <help>Show available container networks in JSON format</help> + </properties> + <!-- no admin check --> + <command>sudo ${vyos_op_scripts_dir}/container.py show_network --raw</command> + </node> + </children> + </node> </children> </node> <node name="log"> diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index d048901f0..423fe01ed 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -20,10 +20,22 @@ from ctypes import cdll, c_char_p, c_void_p, c_int, c_bool LIBPATH = '/usr/lib/libvyosconfig.so.0' +def replace_backslash(s, search, replace): + """Modify quoted strings containing backslashes not of escape sequences""" + def replace_method(match): + result = match.group().replace(search, replace) + return result + p = re.compile(r'("[^"]*[\\][^"]*"\n|\'[^\']*[\\][^\']*\'\n)') + return p.sub(replace_method, s) + def escape_backslash(string: str) -> str: - """Escape single backslashes in string that are not in escape sequence""" - p = re.compile(r'(?<!\\)[\\](?!b|f|n|r|t|\\[^bfnrt])') - result = p.sub(r'\\\\', string) + """Escape single backslashes in quoted strings""" + result = replace_backslash(string, '\\', '\\\\') + return result + +def unescape_backslash(string: str) -> str: + """Unescape backslashes in quoted strings""" + result = replace_backslash(string, '\\\\', '\\') return result def extract_version(s): @@ -165,11 +177,14 @@ class ConfigTree(object): def to_string(self, ordered_values=False): config_string = self.__to_string(self.__config, ordered_values).decode() + config_string = unescape_backslash(config_string) config_string = "{0}\n{1}".format(config_string, self.__version) return config_string def to_commands(self, op="set"): - return self.__to_commands(self.__config, op.encode()).decode() + commands = self.__to_commands(self.__config, op.encode()).decode() + commands = unescape_backslash(commands) + return commands def to_json(self): return self.__to_json(self.__config).decode() @@ -362,6 +377,7 @@ def show_diff(left, right, path=[], commands=False, libpath=LIBPATH): msg = __get_error().decode() raise ConfigTreeError(msg) + res = unescape_backslash(res) return res def union(left, right, libpath=LIBPATH): diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 5d3723876..6508ccdd9 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -269,14 +269,33 @@ def verify_bridge_delete(config): raise ConfigError(f'Interface "{interface}" cannot be deleted as it ' f'is a member of bridge "{bridge_name}"!') -def verify_interface_exists(ifname): +def verify_interface_exists(ifname, warning_only=False): """ Common helper function used by interface implementations to perform - recurring validation if an interface actually exists. + recurring validation if an interface actually exists. We first probe + if the interface is defined on the CLI, if it's not found we try if + it exists at the OS level. """ import os - if not os.path.exists(f'/sys/class/net/{ifname}'): - raise ConfigError(f'Interface "{ifname}" does not exist!') + from vyos.base import Warning + from vyos.configquery import ConfigTreeQuery + from vyos.utils.dict import dict_search_recursive + + # Check if interface is present in CLI config + config = ConfigTreeQuery() + tmp = config.get_config_dict(['interfaces'], get_first_key=True) + if bool(list(dict_search_recursive(tmp, ifname))): + return True + + # Interface not found on CLI, try Linux Kernel + if os.path.exists(f'/sys/class/net/{ifname}'): + return True + + message = f'Interface "{ifname}" does not exist!' + if warning_only: + Warning(message) + return False + raise ConfigError(message) def verify_source_interface(config): """ diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index b29e71394..7936e3da5 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-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 @@ -14,12 +14,11 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. from netifaces import interfaces -import json from vyos.ifconfig.interface import Interface from vyos.utils.assertion import assert_boolean +from vyos.utils.assertion import assert_list from vyos.utils.assertion import assert_positive -from vyos.utils.process import cmd from vyos.utils.dict import dict_search from vyos.configdict import get_vlan_ids from vyos.configdict import list_diff @@ -86,6 +85,10 @@ class BridgeIf(Interface): 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/vlan_filtering', }, + 'vlan_protocol': { + 'validate': lambda v: assert_list(v, ['0x88a8', '0x8100']), + 'location': '/sys/class/net/{ifname}/bridge/vlan_protocol', + }, 'multicast_querier': { 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', @@ -248,6 +251,26 @@ class BridgeIf(Interface): """ return self.set_interface('del_port', interface) + def set_vlan_protocol(self, protocol): + """ + Set protocol used for VLAN filtering. + The valid values are 0x8100(802.1q) or 0x88A8(802.1ad). + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + + if protocol not in ['802.1q', '802.1ad']: + raise ValueError() + + map = { + '802.1ad': '0x88a8', + '802.1q' : '0x8100' + } + + return self.set_interface('vlan_protocol', map[protocol]) + def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered @@ -294,10 +317,13 @@ class BridgeIf(Interface): if member in interfaces(): self.del_port(member) - # enable/disable Vlan Filter + # enable/disable VLAN Filter tmp = '1' if 'enable_vlan' in config else '0' self.set_vlan_filter(tmp) + tmp = config.get('protocol') + self.set_vlan_protocol(tmp) + # add VLAN interfaces to local 'parent' bridge to allow forwarding if 'enable_vlan' in config: for vlan in config.get('vif_remove', {}): diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index 9ebbeb9ed..9511386f4 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-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 @@ -52,8 +52,14 @@ class VTIIf(Interface): cmd += f' {iproute2_key} {tmp}' self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly self.set_interface('admin_state', 'down') + def set_admin_state(self, state): + """ Handled outside by /etc/ipsec.d/vti-up-down """ + pass + def get_mac(self): """ Get a synthetic MAC address. """ return self.get_mac_synthetic() diff --git a/python/vyos/priority.py b/python/vyos/priority.py new file mode 100644 index 000000000..ab4e6d411 --- /dev/null +++ b/python/vyos/priority.py @@ -0,0 +1,75 @@ +# 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 pathlib import Path +from typing import List + +from vyos.xml_ref import load_reference +from vyos.base import Warning as Warn + +def priority_data(d: dict) -> list: + def func(d, path, res, hier): + for k,v in d.items(): + if not 'node_data' in v: + continue + subpath = path + [k] + hier_prio = hier + data = v.get('node_data') + o = data.get('owner') + p = data.get('priority') + # a few interface-definitions have priority preceding owner + # attribute, instead of within properties; pass in descent + if p is not None and o is None: + hier_prio = p + if o is not None and p is None: + p = hier_prio + if o is not None and p is not None: + o = Path(o.split()[0]).name + p = int(p) + res.append((subpath, o, p)) + if isinstance(v, dict): + func(v, subpath, res, hier_prio) + return res + ret = func(d, [], [], 0) + ret = sorted(ret, key=lambda x: x[0]) + ret = sorted(ret, key=lambda x: x[2]) + return ret + +def get_priority_data() -> list: + xml = load_reference() + return priority_data(xml.ref) + +def priority_sort(sections: List[list[str]] = None, + owners: List[str] = None, + reverse=False) -> List: + if sections is not None: + index = 0 + collection: List = sections + elif owners is not None: + index = 1 + collection = owners + else: + raise ValueError('one of sections or owners is required') + + l = get_priority_data() + m = [item for item in l if item[index] in collection] + n = sorted(m, key=lambda x: x[2], reverse=reverse) + o = [item[index] for item in n] + # sections are unhashable; use comprehension + missed = [j for j in collection if j not in o] + if missed: + Warn(f'No priority available for elements {missed}') + + return o diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py index 47318122b..c8e881ee2 100644 --- a/python/vyos/qos/base.py +++ b/python/vyos/qos/base.py @@ -324,6 +324,11 @@ class QoSBase: if 'burst' in cls_config: burst = cls_config['burst'] filter_cmd += f' burst {burst}' + + if 'mtu' in cls_config: + mtu = cls_config['mtu'] + filter_cmd += f' mtu {mtu}' + cls = int(cls) filter_cmd += f' flowid {self._parent:x}:{cls:x}' self._cmd(filter_cmd) @@ -387,6 +392,10 @@ class QoSBase: burst = config['default']['burst'] filter_cmd += f' burst {burst}' + if 'mtu' in config['default']: + mtu = config['default']['mtu'] + filter_cmd += f' mtu {mtu}' + if 'class' in config: filter_cmd += f' flowid {self._parent:x}:{default_cls_id:x}' diff --git a/python/vyos/xml_ref/generate_cache.py b/python/vyos/xml_ref/generate_cache.py index 6a05d4608..d1ccb0f81 100755 --- a/python/vyos/xml_ref/generate_cache.py +++ b/python/vyos/xml_ref/generate_cache.py @@ -38,7 +38,8 @@ xml_tmp = join('/tmp', xml_cache_json) pkg_cache = abspath(join(_here, 'pkg_cache')) ref_cache = abspath(join(_here, 'cache.py')) -node_data_fields = ("node_type", "multi", "valueless", "default_value") +node_data_fields = ("node_type", "multi", "valueless", "default_value", + "owner", "priority") def trim_node_data(cache: dict): for k in list(cache): diff --git a/smoketest/scripts/cli/test_backslash_escape.py b/smoketest/scripts/cli/test_backslash_escape.py new file mode 100755 index 000000000..e94e9ab0a --- /dev/null +++ b/smoketest/scripts/cli/test_backslash_escape.py @@ -0,0 +1,68 @@ +#!/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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configtree import ConfigTree + +base_path = ['interfaces', 'ethernet', 'eth0', 'description'] + +cases_word = [r'fo\o', r'fo\\o', r'foço\o', r'foço\\o'] +# legacy CLI output quotes only if whitespace present; this is a notable +# difference that confounds the translation legacy -> modern, hence +# determines the regex used in function replace_backslash +cases_phrase = [r'some fo\o', r'some fo\\o', r'some foço\o', r'some foço\\o'] + +case_save_config = '/tmp/smoketest-case-save' + +class TestBackslashEscape(VyOSUnitTestSHIM.TestCase): + def test_backslash_escape_word(self): + for case in cases_word: + self.cli_set(base_path + [case]) + self.cli_commit() + # save_config tests translation though subsystems: + # legacy output -> config -> configtree -> file + self._session.save_config(case_save_config) + # reload to configtree and confirm: + with open(case_save_config) as f: + config_string = f.read() + ct = ConfigTree(config_string) + res = ct.return_value(base_path) + self.assertEqual(case, res, msg=res) + print(f'description: {res}') + self.cli_delete(base_path) + self.cli_commit() + + def test_backslash_escape_phrase(self): + for case in cases_phrase: + self.cli_set(base_path + [case]) + self.cli_commit() + # save_config tests translation though subsystems: + # legacy output -> config -> configtree -> file + self._session.save_config(case_save_config) + # reload to configtree and confirm: + with open(case_save_config) as f: + config_string = f.read() + ct = ConfigTree(config_string) + res = ct.return_value(base_path) + self.assertEqual(case, res, msg=res) + print(f'description: {res}') + self.cli_delete(base_path) + self.cli_commit() + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 9e8473fa4..fe6977252 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -598,14 +598,30 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_filter') - def test_ipv4_state_and_status_rules(self): - name = 'smoketest-state' - interface = 'eth0' - + def test_ipv4_global_state(self): self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept']) self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept']) self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop']) + self.cli_commit() + + nftables_search = [ + ['jump VYOS_STATE_POLICY'], + ['chain VYOS_STATE_POLICY'], + ['ct state established', 'accept'], + ['ct state invalid', 'drop'], + ['ct state related', 'accept'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_filter') + + # Check conntrack is enabled from state-policy + self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK') + self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK') + + def test_ipv4_state_and_status_rules(self): + name = 'smoketest-state' + self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'established']) @@ -632,12 +648,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"'], - ['jump VYOS_STATE_POLICY'], - ['chain VYOS_STATE_POLICY'], - ['ct state established', 'accept'], - ['ct state invalid', 'drop'], - ['ct state related', 'accept'] + ['drop', f'comment "{name} default-action drop"'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 3500e97d6..124c1fbcb 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -182,6 +182,10 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: cost = 1000 priority = 10 + + tmp = get_interface_config(interface) + self.assertEqual('802.1Q', tmp['linkinfo']['info_data']['vlan_protocol']) # default VLAN protocol + for member in self._members: tmp = get_interface_config(member) self.assertEqual(interface, tmp['master']) @@ -442,5 +446,19 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(['interfaces', 'tunnel', tunnel_if]) self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr]) + def test_bridge_vlan_protocol(self): + protocol = '802.1ad' + + # Add member interface to bridge and set VLAN filter + for interface in self._interfaces: + self.cli_set(self._base_path + [interface, 'protocol', protocol]) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + tmp = get_interface_config(interface) + self.assertEqual(protocol, tmp['linkinfo']['info_data']['vlan_protocol']) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_vti.py b/smoketest/scripts/cli/test_interfaces_vti.py index 7f13575a3..871ac650b 100755 --- a/smoketest/scripts/cli/test_interfaces_vti.py +++ b/smoketest/scripts/cli/test_interfaces_vti.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-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 @@ -18,6 +18,9 @@ import unittest from base_interfaces_test import BasicInterfaceTest +from vyos.ifconfig import Interface +from vyos.utils.network import is_intf_addr_assigned + class VTIInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): @@ -27,5 +30,19 @@ class VTIInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(VTIInterfaceTest, cls).setUpClass() + def test_add_single_ip_address(self): + addr = '192.0.2.0/31' + for intf in self._interfaces: + self.cli_set(self._base_path + [intf, 'address', addr]) + for option in self._options.get(intf, []): + self.cli_set(self._base_path + [intf] + option.split()) + + self.cli_commit() + + # VTI interface are always down and only brought up by IPSec + for intf in self._interfaces: + self.assertTrue(is_intf_addr_assigned(intf, addr)) + self.assertEqual(Interface(intf).get_admin_state(), 'down') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index aa5f2f38c..0fd18a6da 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -73,6 +73,12 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.isis_base_config() + + self.cli_set(base_path + ['redistribute', 'ipv4', 'connected']) + # verify() - Redistribute level-1 or level-2 should be specified + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map]) self.cli_set(base_path + ['log-adjacency-changes']) diff --git a/smoketest/scripts/cli/test_qos.py b/smoketest/scripts/cli/test_qos.py index 81e7326f8..46ef68b1d 100755 --- a/smoketest/scripts/cli/test_qos.py +++ b/smoketest/scripts/cli/test_qos.py @@ -38,6 +38,13 @@ def get_tc_filter_json(interface, direction) -> list: tmp = loads(tmp) return tmp +def get_tc_filter_details(interface, direction) -> list: + # json doesn't contain all params, such as mtu + if direction not in ['ingress', 'egress']: + raise ValueError() + tmp = cmd(f'tc -details filter show dev {interface} {direction}') + return tmp + class TestQoS(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -234,7 +241,12 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): def test_05_limiter(self): qos_config = { '1' : { - 'bandwidth' : '1000000', + 'bandwidth' : '3000000', + 'exceed' : 'pipe', + 'burst' : '100Kb', + 'mtu' : '1600', + 'not-exceed' : 'continue', + 'priority': '15', 'match4' : { 'ssh' : { 'dport' : '22', }, }, @@ -262,6 +274,10 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) # set default bandwidth parameter for all remaining connections self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'bandwidth', '500000']) + self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'burst', '200kb']) + self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'exceed', 'drop']) + self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'mtu', '3000']) + self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'not-exceed', 'ok']) for qos_class, qos_class_config in qos_config.items(): qos_class_base = base_path + ['policy', 'limiter', policy_name, 'class', qos_class] @@ -279,6 +295,21 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): if 'bandwidth' in qos_class_config: self.cli_set(qos_class_base + ['bandwidth', qos_class_config['bandwidth']]) + if 'exceed' in qos_class_config: + self.cli_set(qos_class_base + ['exceed', qos_class_config['exceed']]) + + if 'not-exceed' in qos_class_config: + self.cli_set(qos_class_base + ['not-exceed', qos_class_config['not-exceed']]) + + if 'burst' in qos_class_config: + self.cli_set(qos_class_base + ['burst', qos_class_config['burst']]) + + if 'mtu' in qos_class_config: + self.cli_set(qos_class_base + ['mtu', qos_class_config['mtu']]) + + if 'priority' in qos_class_config: + self.cli_set(qos_class_base + ['priority', qos_class_config['priority']]) + # commit changes self.cli_commit() @@ -303,6 +334,14 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): dport = int(match_config['dport']) self.assertEqual(f'{dport:x}', filter['options']['match']['value']) + tc_details = get_tc_filter_details(interface, 'ingress') + self.assertTrue('filter parent ffff: protocol all pref 20 u32 chain 0' in tc_details) + self.assertTrue('rate 1Gbit burst 15125b mtu 2Kb action drop overhead 0b linklayer ethernet' in tc_details) + self.assertTrue('filter parent ffff: protocol all pref 15 u32 chain 0' in tc_details) + self.assertTrue('rate 3Gbit burst 102000b mtu 1600b action pipe/continue overhead 0b linklayer ethernet' in tc_details) + self.assertTrue('rate 500Mbit burst 204687b mtu 3000b action drop overhead 0b linklayer ethernet' in tc_details) + self.assertTrue('filter parent ffff: protocol all pref 255 basic chain 0' in tc_details) + def test_06_network_emulator(self): policy_type = 'network-emulator' diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 8d594bb68..6c9925b80 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-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 @@ -155,12 +155,12 @@ def verify(isis): for proto, proto_config in isis['redistribute'][afi].items(): if 'level_1' not in proto_config and 'level_2' not in proto_config: raise ConfigError(f'Redistribute level-1 or level-2 should be specified in ' \ - f'"protocols isis {process} redistribute {afi} {proto}"!') + f'"protocols isis redistribute {afi} {proto}"!') for redistr_level, redistr_config in proto_config.items(): if proc_level and proc_level != 'level_1_2' and proc_level != redistr_level: - raise ConfigError(f'"protocols isis {process} redistribute {afi} {proto} {redistr_level}" ' \ - f'can not be used with \"protocols isis {process} level {proc_level}\"') + raise ConfigError(f'"protocols isis redistribute {afi} {proto} {redistr_level}" ' \ + f'can not be used with \"protocols isis level {proc_level}\"!') # Segment routing checks if dict_search('segment_routing.global_block', isis): @@ -220,8 +220,8 @@ def verify(isis): if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') - - # Check for index ranges being larger than the segment routing global block + + # Check for index ranges being larger than the segment routing global block if dict_search('segment_routing.global_block', isis): g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) @@ -233,7 +233,7 @@ def verify(isis): if int(index_size) > int(g_label_difference): raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ f'index base size larger than the SRGB label base.') - + # Check for LFA tiebreaker index duplication if dict_search('fast_reroute.lfa.local.tiebreaker', isis): comparison_dictionary = {} @@ -311,4 +311,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - exit(1)
\ No newline at end of file + exit(1) diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py index a1472aaaa..3d42389f6 100755 --- a/src/conf_mode/system_conntrack.py +++ b/src/conf_mode/system_conntrack.py @@ -185,12 +185,16 @@ def generate(conntrack): conntrack['ipv4_firewall_action'] = 'return' conntrack['ipv6_firewall_action'] = 'return' - for rules, path in dict_search_recursive(conntrack['firewall'], 'rule'): - if any(('state' in rule_conf or 'connection_status' in rule_conf or 'offload_target' in rule_conf) for rule_conf in rules.values()): - if path[0] == 'ipv4': - conntrack['ipv4_firewall_action'] = 'accept' - elif path[0] == 'ipv6': - conntrack['ipv6_firewall_action'] = 'accept' + if dict_search_args(conntrack['firewall'], 'global_options', 'state_policy') != None: + conntrack['ipv4_firewall_action'] = 'accept' + conntrack['ipv6_firewall_action'] = 'accept' + else: + for rules, path in dict_search_recursive(conntrack['firewall'], 'rule'): + if any(('state' in rule_conf or 'connection_status' in rule_conf or 'offload_target' in rule_conf) for rule_conf in rules.values()): + if path[0] == 'ipv4': + conntrack['ipv4_firewall_action'] = 'accept' + elif path[0] == 'ipv6': + conntrack['ipv6_firewall_action'] = 'accept' render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack) render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack) diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down index 441b316c2..01e9543c9 100755 --- a/src/etc/ipsec.d/vti-up-down +++ b/src/etc/ipsec.d/vti-up-down @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -57,7 +57,9 @@ if __name__ == '__main__': if 'disable' not in vti: tmp = VTIIf(interface) tmp.update(vti) + call(f'sudo ip link set {interface} up') else: + call(f'sudo ip link set {interface} down') syslog(f'Interface {interface} is admin down ...') elif verb in ['down-client', 'down-host']: if vti_link_up: diff --git a/src/helpers/priority.py b/src/helpers/priority.py new file mode 100755 index 000000000..04186104c --- /dev/null +++ b/src/helpers/priority.py @@ -0,0 +1,42 @@ +#!/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 sys +from argparse import ArgumentParser +from tabulate import tabulate + +from vyos.priority import get_priority_data + +if __name__ == '__main__': + parser = ArgumentParser() + parser.add_argument('--legacy-format', action='store_true', + help="format output for comparison with legacy 'priority.pl'") + args = parser.parse_args() + + prio_list = get_priority_data() + if args.legacy_format: + for p in prio_list: + print(f'{p[2]} {"/".join(p[0])}') + sys.exit(0) + + l = [] + for p in prio_list: + l.append((p[2], p[1], p[0])) + headers = ['priority', 'owner', 'path'] + out = tabulate(l, headers, numalign='right') + print(out) diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 572fea61f..77f7cd810 100755 --- a/src/helpers/vyos_config_sync.py +++ b/src/helpers/vyos_config_sync.py @@ -61,14 +61,16 @@ def post_request(url: str, -def retrieve_config(section: str = None) -> Optional[Dict[str, Any]]: +def retrieve_config(section: Optional[List[str]] = None) -> Optional[Dict[str, Any]]: """Retrieves the configuration from the local server. Args: - section: str: The section of the configuration to retrieve. Default is None. + section: List[str]: The section of the configuration to retrieve. + Default is None. Returns: - Optional[Dict[str, Any]]: The retrieved configuration as a dictionary, or None if an error occurred. + Optional[Dict[str, Any]]: The retrieved configuration as a + dictionary, or None if an error occurred. """ if section is None: section = [] @@ -83,23 +85,21 @@ def retrieve_config(section: str = None) -> Optional[Dict[str, Any]]: def set_remote_config( address: str, key: str, - op: str, - path: str = None, - section: Optional[str] = None) -> Optional[Dict[str, Any]]: + commands: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: """Loads the VyOS configuration in JSON format to a remote host. Args: address (str): The address of the remote host. key (str): The key to use for loading the configuration. - path (Optional[str]): The path of the configuration. Default is None. - section (Optional[str]): The section of the configuration to load. Default is None. + commands (list): List of set/load commands for request, given as: + [{'op': str, 'path': list[str], 'section': dict}, + ...] Returns: - Optional[Dict[str, Any]]: The response from the remote host as a dictionary, or None if an error occurred. + Optional[Dict[str, Any]]: The response from the remote host as a + dictionary, or None if a RequestException occurred. """ - if path is None: - path = [] headers = {'Content-Type': 'application/json'} # Disable the InsecureRequestWarning @@ -107,9 +107,7 @@ def set_remote_config( url = f'https://{address}/configure-section' data = json.dumps({ - 'op': mode, - 'path': path, - 'section': section, + 'commands': commands, 'key': key }) @@ -122,14 +120,14 @@ def set_remote_config( return None -def is_section_revised(section: str) -> bool: +def is_section_revised(section: List[str]) -> bool: from vyos.config_mgmt import is_node_revised return is_node_revised(section) def config_sync(secondary_address: str, secondary_key: str, - sections: List[list], + sections: List[list[str]], mode: str): """Retrieve a config section from primary router in JSON format and send it to secondary router @@ -142,21 +140,25 @@ def config_sync(secondary_address: str, ) # Sync sections ("nat", "firewall", etc) + commands = [] for section in sections: config_json = retrieve_config(section=section) # Check if config path deesn't exist, for example "set nat" # we set empty value for config_json data # As we cannot send to the remote host section "nat None" config if not config_json: - config_json = "" + config_json = {} logger.debug( f"Retrieved config for section '{section}': {config_json}") - set_config = set_remote_config(address=secondary_address, - key=secondary_key, - op=mode, - path=section, - section=config_json) - logger.debug(f"Set config for section '{section}': {set_config}") + + d = {'op': mode, 'path': section, 'section': config_json} + commands.append(d) + + set_config = set_remote_config(address=secondary_address, + key=secondary_key, + commands=commands) + + logger.debug(f"Set config for sections '{sections}': {set_config}") if __name__ == '__main__': diff --git a/src/migration-scripts/policy/3-to-4 b/src/migration-scripts/policy/3-to-4 index 1ebb248b0..476fa3af2 100755 --- a/src/migration-scripts/policy/3-to-4 +++ b/src/migration-scripts/policy/3-to-4 @@ -51,7 +51,7 @@ def community_migrate(config: ConfigTree, rule: list[str]) -> bool: :rtype: bool """ community_list = list((config.return_value(rule)).split(" ")) - + config.delete(rule) if 'none' in community_list: config.set(rule + ['none']) return False @@ -61,10 +61,8 @@ def community_migrate(config: ConfigTree, rule: list[str]) -> bool: community_action = 'add' community_list.remove('additive') for community in community_list: - if len(community): - config.set(rule + [community_action], value=community, - replace=False) - config.delete(rule) + config.set(rule + [community_action], value=community, + replace=False) if community_action == 'replace': return False else: diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py index cf8adf795..6ea213bec 100755 --- a/src/op_mode/conntrack.py +++ b/src/op_mode/conntrack.py @@ -112,7 +112,8 @@ def get_formatted_output(dict_data): proto = meta['layer4']['protoname'] if direction == 'independent': conn_id = meta['id'] - timeout = meta['timeout'] + # T6138 flowtable offload conntrack entries without 'timeout' + timeout = meta.get('timeout', 'n/a') orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index a7b14a1a3..77870a84c 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -463,7 +463,7 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel, endpoint = request.url.path # Allow users to pass just one command - if not isinstance(data, ConfigureListModel): + if not isinstance(data, (ConfigureListModel, ConfigSectionListModel)): data = [data] else: data = data.commands |