diff options
-rw-r--r-- | data/templates/ssh/override.conf.j2 | 14 | ||||
-rw-r--r-- | debian/vyos-1x.postinst | 7 | ||||
-rw-r--r-- | interface-definitions/include/constraint/vrf.xml.i | 6 | ||||
-rw-r--r-- | interface-definitions/include/interface/vrf.xml.i | 1 | ||||
-rw-r--r-- | interface-definitions/include/vrf-multi.xml.i | 22 | ||||
-rw-r--r-- | interface-definitions/service_ssh.xml.in | 2 | ||||
-rw-r--r-- | interface-definitions/vrf.xml.in | 5 | ||||
-rw-r--r-- | python/vyos/configverify.py | 24 | ||||
-rw-r--r-- | python/vyos/template.py | 3 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_ssh.py | 38 | ||||
-rwxr-xr-x | src/conf_mode/container.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/qos.py | 5 | ||||
-rwxr-xr-x | src/conf_mode/service_ssh.py | 16 | ||||
-rw-r--r-- | src/etc/systemd/system/ssh@.service.d/vrf-override.conf | 13 | ||||
-rw-r--r-- | src/tests/test_template.py | 6 |
15 files changed, 111 insertions, 54 deletions
diff --git a/data/templates/ssh/override.conf.j2 b/data/templates/ssh/override.conf.j2 deleted file mode 100644 index 4454ad1b8..000000000 --- a/data/templates/ssh/override.conf.j2 +++ /dev/null @@ -1,14 +0,0 @@ -{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %} -[Unit] -StartLimitIntervalSec=0 -After=vyos-router.service -ConditionPathExists={{ config_file }} - -[Service] -EnvironmentFile= -ExecStart= -ExecStart={{ vrf_command }}/usr/sbin/sshd -f {{ config_file }} -Restart=always -RestartPreventExitStatus= -RestartSec=10 -RuntimeDirectoryPreserve=yes diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index 0e6e3c863..78e895d6e 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -194,3 +194,10 @@ systemctl enable vyos-config-cloud-init.service # Update XML cache python3 /usr/lib/python3/dist-packages/vyos/xml_ref/update_cache.py + +# Generate hardlinks for systemd units for multi VRF support +# as softlinks will fail in systemd: +# symlink target name type "ssh.service" does not match source, rejecting. +if [ ! -f /lib/systemd/system/ssh@.service ]; then + ln /lib/systemd/system/ssh.service /lib/systemd/system/ssh@.service +fi diff --git a/interface-definitions/include/constraint/vrf.xml.i b/interface-definitions/include/constraint/vrf.xml.i new file mode 100644 index 000000000..a1922bb6d --- /dev/null +++ b/interface-definitions/include/constraint/vrf.xml.i @@ -0,0 +1,6 @@ +<!-- include start from constraint/vrf.xml.i --> +<constraint> + <validator name="vrf-name"/> +</constraint> +<constraintErrorMessage>VRF instance name must be 15 characters or less and can not\nbe named as regular network interfaces.\nA name must starts from a letter.\n</constraintErrorMessage> +<!-- include end --> diff --git a/interface-definitions/include/interface/vrf.xml.i b/interface-definitions/include/interface/vrf.xml.i index 8605f56e8..ef0058f86 100644 --- a/interface-definitions/include/interface/vrf.xml.i +++ b/interface-definitions/include/interface/vrf.xml.i @@ -9,6 +9,7 @@ <completionHelp> <path>vrf name</path> </completionHelp> + #include <include/constraint/vrf.xml.i> </properties> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/vrf-multi.xml.i b/interface-definitions/include/vrf-multi.xml.i new file mode 100644 index 000000000..0b22894e4 --- /dev/null +++ b/interface-definitions/include/vrf-multi.xml.i @@ -0,0 +1,22 @@ +<!-- include start from interface/vrf.xml.i --> +<leafNode name="vrf"> + <properties> + <help>VRF instance name</help> + <completionHelp> + <path>vrf name</path> + <list>default</list> + </completionHelp> + <valueHelp> + <format>default</format> + <description>Explicitly start in default VRF</description> + </valueHelp> + <valueHelp> + <format>txt</format> + <description>VRF instance name</description> + </valueHelp> + #include <include/constraint/vrf.xml.i> + <multi/> + </properties> + <defaultValue>default</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/service_ssh.xml.in b/interface-definitions/service_ssh.xml.in index 5c893bd35..d9eee1ab8 100644 --- a/interface-definitions/service_ssh.xml.in +++ b/interface-definitions/service_ssh.xml.in @@ -262,7 +262,7 @@ </constraint> </properties> </leafNode> - #include <include/interface/vrf.xml.i> + #include <include/vrf-multi.xml.i> </children> </node> </children> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index 25f26d0cc..94ed96e4b 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -16,10 +16,7 @@ <tagNode name="name"> <properties> <help>Virtual Routing and Forwarding instance</help> - <constraint> - <validator name="vrf-name"/> - </constraint> - <constraintErrorMessage>VRF instance name must be 15 characters or less and can not\nbe named as regular network interfaces.\nA name must starts from a letter.\n</constraintErrorMessage> + #include <include/constraint/vrf.xml.i> <valueHelp> <format>txt</format> <description>VRF instance name</description> diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 6508ccdd9..651036bad 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -99,10 +99,17 @@ def verify_vrf(config): Common helper function used by interface implementations to perform recurring validation of VRF configuration. """ - from netifaces import interfaces - if 'vrf' in config and config['vrf'] != 'default': - if config['vrf'] not in interfaces(): - raise ConfigError('VRF "{vrf}" does not exist'.format(**config)) + from vyos.utils.network import interface_exists + if 'vrf' in config: + vrfs = config['vrf'] + if isinstance(vrfs, str): + vrfs = [vrfs] + + for vrf in vrfs: + if vrf == 'default': + continue + if not interface_exists(vrf): + raise ConfigError(f'VRF "{vrf}" does not exist!') if 'is_bridge_member' in config: raise ConfigError( @@ -206,13 +213,13 @@ def verify_mirror_redirect(config): It makes no sense to mirror traffic back at yourself! """ - import os + from vyos.utils.network import interface_exists if {'mirror', 'redirect'} <= set(config): raise ConfigError('Mirror and redirect can not be enabled at the same time!') if 'mirror' in config: for direction, mirror_interface in config['mirror'].items(): - if not os.path.exists(f'/sys/class/net/{mirror_interface}'): + if not interface_exists(mirror_interface): raise ConfigError(f'Requested mirror interface "{mirror_interface}" '\ 'does not exist!') @@ -222,7 +229,7 @@ def verify_mirror_redirect(config): if 'redirect' in config: redirect_ifname = config['redirect'] - if not os.path.exists(f'/sys/class/net/{redirect_ifname}'): + if not interface_exists(redirect_ifname): raise ConfigError(f'Requested redirect interface "{redirect_ifname}" '\ 'does not exist!') @@ -280,6 +287,7 @@ def verify_interface_exists(ifname, warning_only=False): from vyos.base import Warning from vyos.configquery import ConfigTreeQuery from vyos.utils.dict import dict_search_recursive + from vyos.utils.network import interface_exists # Check if interface is present in CLI config config = ConfigTreeQuery() @@ -288,7 +296,7 @@ def verify_interface_exists(ifname, warning_only=False): return True # Interface not found on CLI, try Linux Kernel - if os.path.exists(f'/sys/class/net/{ifname}'): + if interface_exists(ifname): return True message = f'Interface "{ifname}" does not exist!' diff --git a/python/vyos/template.py b/python/vyos/template.py index 1aa9ace8b..3e468eb82 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -307,7 +307,8 @@ def network_from_ipv4(address): @register_filter('is_interface') def is_interface(interface): """ Check if parameter is a valid local interface name """ - return os.path.exists(f'/sys/class/net/{interface}') + from vyos.utils.network import interface_exists + return interface_exists(interface) @register_filter('is_ip') def is_ip(addr): diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index 947d7d568..031897c26 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -32,7 +32,6 @@ from vyos.utils.file import read_file PROCESS_NAME = 'sshd' SSHD_CONF = '/run/sshd/sshd_config' base_path = ['service', 'ssh'] -vrf = 'mgmt' key_rsa = '/etc/ssh/ssh_host_rsa_key' key_dsa = '/etc/ssh/ssh_host_dsa_key' @@ -51,6 +50,7 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['vrf']) def tearDown(self): # Check for running process @@ -58,6 +58,7 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # delete testing SSH config self.cli_delete(base_path) + self.cli_delete(['vrf']) self.cli_commit() self.assertTrue(os.path.isfile(key_rsa)) @@ -79,7 +80,7 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # Check configured port port = get_config_value('Port')[0] - self.assertEqual('22', port) + self.assertEqual('22', port) # default value def test_ssh_single_listen_address(self): # Check if SSH service can be configured and runs @@ -141,10 +142,9 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): for address in addresses: self.assertIn(address, tmp) - def test_ssh_vrf(self): + def test_ssh_vrf_single(self): + vrf = 'mgmt' # Check if SSH service can be bound to given VRF - port = '22' - self.cli_set(base_path + ['port', port]) self.cli_set(base_path + ['vrf', vrf]) # VRF does yet not exist - an error must be thrown @@ -156,16 +156,32 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - # Check configured port - tmp = get_config_value('Port') - self.assertIn(port, tmp) - # Check for process in VRF tmp = cmd(f'ip vrf pids {vrf}') self.assertIn(PROCESS_NAME, tmp) - # delete VRF - self.cli_delete(['vrf', 'name', vrf]) + def test_ssh_vrf_multi(self): + # Check if SSH service can be bound to multiple VRFs + vrfs = ['red', 'blue', 'green'] + for vrf in vrfs: + self.cli_set(base_path + ['vrf', vrf]) + + # VRF does yet not exist - an error must be thrown + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + table = 12345 + for vrf in vrfs: + self.cli_set(['vrf', 'name', vrf, 'table', str(table)]) + table += 1 + + # commit changes + self.cli_commit() + + # Check for process in VRF + for vrf in vrfs: + tmp = cmd(f'ip vrf pids {vrf}') + self.assertIn(PROCESS_NAME, tmp) def test_ssh_login(self): # Perform SSH login and command execution with a predefined user. The diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index e967bee71..910a92a7c 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -32,6 +32,7 @@ from vyos.utils.file import write_file from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import run +from vyos.utils.network import interface_exists from vyos.template import bracketize_ipv6 from vyos.template import inc_ip from vyos.template import is_ipv4 @@ -471,7 +472,7 @@ def apply(container): # T5147: Networks are started only as soon as there is a consumer. # If only a network is created in the first place, no need to assign # it to a VRF as there's no consumer, yet. - if os.path.exists(f'/sys/class/net/{network_name}'): + if interface_exists(network_name): tmp = Interface(network_name) tmp.add_ipv6_eui64_address('fe80::/64') tmp.set_vrf(network_config.get('vrf', '')) diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index 4a0b4d0c5..2b4fcc1bf 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -36,8 +36,9 @@ from vyos.qos import RateLimiter from vyos.qos import RoundRobin from vyos.qos import TrafficShaper from vyos.qos import TrafficShaperHFSC -from vyos.utils.process import run from vyos.utils.dict import dict_search_recursive +from vyos.utils.network import interface_exists +from vyos.utils.process import run from vyos import ConfigError from vyos import airbag airbag.enable() @@ -214,7 +215,7 @@ def apply(qos): return None for interface, interface_config in qos['interface'].items(): - if not os.path.exists(f'/sys/class/net/{interface}'): + if not interface_exists(interface): # When shaper is bound to a dialup (e.g. PPPoE) interface it is # possible that it is yet not availbale when to QoS code runs. # Skip the configuration and inform the user diff --git a/src/conf_mode/service_ssh.py b/src/conf_mode/service_ssh.py index ee5e1eca2..9abdd33dc 100755 --- a/src/conf_mode/service_ssh.py +++ b/src/conf_mode/service_ssh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-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 @@ -30,7 +30,6 @@ from vyos import airbag airbag.enable() config_file = r'/run/sshd/sshd_config' -systemd_override = r'/run/systemd/system/ssh.service.d/override.conf' sshguard_config_file = '/etc/sshguard/sshguard.conf' sshguard_whitelist = '/etc/sshguard/whitelist' @@ -81,8 +80,6 @@ def generate(ssh): if not ssh: if os.path.isfile(config_file): os.unlink(config_file) - if os.path.isfile(systemd_override): - os.unlink(systemd_override) return None @@ -99,13 +96,10 @@ def generate(ssh): call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}') render(config_file, 'ssh/sshd_config.j2', ssh) - render(systemd_override, 'ssh/override.conf.j2', ssh) if 'dynamic_protection' in ssh: render(sshguard_config_file, 'ssh/sshguard_config.j2', ssh) render(sshguard_whitelist, 'ssh/sshguard_whitelist.j2', ssh) - # Reload systemd manager configuration - call('systemctl daemon-reload') return None @@ -114,7 +108,7 @@ def apply(ssh): systemd_service_sshguard = 'sshguard.service' if not ssh: # SSH access is removed in the commit - call(f'systemctl stop {systemd_service_ssh}') + call(f'systemctl stop ssh@*.service') call(f'systemctl stop {systemd_service_sshguard}') return None @@ -126,9 +120,13 @@ def apply(ssh): # we need to restart the service if e.g. the VRF name changed systemd_action = 'reload-or-restart' if 'restart_required' in ssh: + # this is only true if something for the VRFs changed, thus we + # stop all VRF services and only restart then new ones + call(f'systemctl stop ssh@*.service') systemd_action = 'restart' - call(f'systemctl {systemd_action} {systemd_service_ssh}') + for vrf in ssh['vrf']: + call(f'systemctl {systemd_action} ssh@{vrf}.service') return None if __name__ == '__main__': diff --git a/src/etc/systemd/system/ssh@.service.d/vrf-override.conf b/src/etc/systemd/system/ssh@.service.d/vrf-override.conf new file mode 100644 index 000000000..b8952d86c --- /dev/null +++ b/src/etc/systemd/system/ssh@.service.d/vrf-override.conf @@ -0,0 +1,13 @@ +[Unit] +StartLimitIntervalSec=0 +After=vyos-router.service +ConditionPathExists=/run/sshd/sshd_config + +[Service] +EnvironmentFile= +ExecStart= +ExecStart=ip vrf exec %i /usr/sbin/sshd -f /run/sshd/sshd_config +Restart=always +RestartPreventExitStatus= +RestartSec=10 +RuntimeDirectoryPreserve=yes diff --git a/src/tests/test_template.py b/src/tests/test_template.py index aba97015e..dbb86b40b 100644 --- a/src/tests/test_template.py +++ b/src/tests/test_template.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 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 @@ -14,9 +14,9 @@ # 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 os import vyos.template +from vyos.utils.network import interface_exists from ipaddress import ip_network from unittest import TestCase @@ -26,7 +26,7 @@ class TestVyOSTemplate(TestCase): def test_is_interface(self): for interface in ['lo', 'eth0']: - if os.path.exists(f'/sys/class/net/{interface}'): + if interface_exists(interface): self.assertTrue(vyos.template.is_interface(interface)) else: self.assertFalse(vyos.template.is_interface(interface)) |