diff options
-rw-r--r-- | data/templates/accel-ppp/ipoe.config.j2 | 3 | ||||
-rw-r--r-- | data/templates/accel-ppp/l2tp.config.j2 | 3 | ||||
-rw-r--r-- | data/templates/accel-ppp/pptp.config.j2 | 3 | ||||
-rw-r--r-- | data/templates/accel-ppp/sstp.config.j2 | 3 | ||||
-rw-r--r-- | data/templates/ipsec/swanctl/peer.j2 | 10 | ||||
-rw-r--r-- | data/templates/ipsec/swanctl/remote_access.j2 | 18 | ||||
-rw-r--r-- | interface-definitions/vpn_ipsec.xml.in | 6 | ||||
-rw-r--r-- | python/vyos/utils/process.py | 2 | ||||
-rw-r--r-- | smoketest/scripts/cli/base_vyostest_shim.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_wireless.py | 52 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_protocols_ospf.py | 14 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_snmp.py | 14 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_vpn_ipsec.py | 130 | ||||
-rwxr-xr-x | smoketest/scripts/system/test_kernel_options.py | 6 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_openvpn.py | 17 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_wireless.py | 75 | ||||
-rwxr-xr-x | src/conf_mode/service_snmp.py | 13 | ||||
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 16 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 1 | ||||
-rwxr-xr-x | src/op_mode/interfaces.py | 20 |
20 files changed, 327 insertions, 81 deletions
diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index d87b90473..9729b295e 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -16,6 +16,9 @@ net-snmp {% if limits is vyos_defined %} connlimit {% endif %} +{% if extended_scripts is vyos_defined %} +pppd_compat +{% endif %} [core] thread-count={{ thread_count }} diff --git a/data/templates/accel-ppp/l2tp.config.j2 b/data/templates/accel-ppp/l2tp.config.j2 index db4db66a7..099bc59da 100644 --- a/data/templates/accel-ppp/l2tp.config.j2 +++ b/data/templates/accel-ppp/l2tp.config.j2 @@ -16,6 +16,9 @@ net-snmp {% if limits is vyos_defined %} connlimit {% endif %} +{% if extended_scripts is vyos_defined %} +pppd_compat +{% endif %} [core] thread-count={{ thread_count }} diff --git a/data/templates/accel-ppp/pptp.config.j2 b/data/templates/accel-ppp/pptp.config.j2 index 44f35998b..52ef3cb0e 100644 --- a/data/templates/accel-ppp/pptp.config.j2 +++ b/data/templates/accel-ppp/pptp.config.j2 @@ -16,6 +16,9 @@ net-snmp {% if limits is vyos_defined %} connlimit {% endif %} +{% if extended_scripts is vyos_defined %} +pppd_compat +{% endif %} [core] thread-count={{ thread_count }} diff --git a/data/templates/accel-ppp/sstp.config.j2 b/data/templates/accel-ppp/sstp.config.j2 index 38da829f3..45d0658af 100644 --- a/data/templates/accel-ppp/sstp.config.j2 +++ b/data/templates/accel-ppp/sstp.config.j2 @@ -16,6 +16,9 @@ net-snmp {% if limits is vyos_defined %} connlimit {% endif %} +{% if extended_scripts is vyos_defined %} +pppd_compat +{% endif %} [core] thread-count={{ thread_count }} diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2 index 58f0199fa..3a9af2c94 100644 --- a/data/templates/ipsec/swanctl/peer.j2 +++ b/data/templates/ipsec/swanctl/peer.j2 @@ -63,6 +63,11 @@ life_packets = {{ vti_esp.life_packets }} {% endif %} life_time = {{ vti_esp.lifetime }}s +{% if vti_esp.disable_rekey is vyos_defined %} + rekey_bytes = 0 + rekey_packets = 0 + rekey_time = 0s +{% endif %} local_ts = 0.0.0.0/0,::/0 remote_ts = 0.0.0.0/0,::/0 updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }}" @@ -108,6 +113,11 @@ life_packets = {{ tunnel_esp.life_packets }} {% endif %} life_time = {{ tunnel_esp.lifetime }}s +{% if tunnel_esp.disable_rekey is vyos_defined %} + rekey_bytes = 0 + rekey_packets = 0 + rekey_time = 0s +{% endif %} {% if tunnel_esp.mode is not defined or tunnel_esp.mode == 'tunnel' %} {% if tunnel_conf.local.prefix is vyos_defined %} {% set local_prefix = tunnel_conf.local.prefix if 'any' not in tunnel_conf.local.prefix else ['0.0.0.0/0', '::/0'] %} diff --git a/data/templates/ipsec/swanctl/remote_access.j2 b/data/templates/ipsec/swanctl/remote_access.j2 index 6bced88c7..e384ae972 100644 --- a/data/templates/ipsec/swanctl/remote_access.j2 +++ b/data/templates/ipsec/swanctl/remote_access.j2 @@ -8,6 +8,10 @@ proposals = {{ ike_group[rw_conf.ike_group] | get_esp_ike_cipher | join(',') }} version = {{ ike.key_exchange[4:] if ike.key_exchange is vyos_defined else "0" }} send_certreq = no +{% if ike.dead_peer_detection is vyos_defined %} + dpd_timeout = {{ ike.dead_peer_detection.timeout }} + dpd_delay = {{ ike.dead_peer_detection.interval }} +{% endif %} rekey_time = {{ ike.lifetime }}s keyingtries = 0 {% if rw_conf.unique is vyos_defined %} @@ -44,8 +48,18 @@ children { ikev2-vpn { esp_proposals = {{ esp | get_esp_ike_cipher(ike) | join(',') }} - rekey_time = {{ esp.lifetime }}s - rand_time = 540s +{% if esp.life_bytes is vyos_defined %} + life_bytes = {{ esp.life_bytes }} +{% endif %} +{% if esp.life_packets is vyos_defined %} + life_packets = {{ esp.life_packets }} +{% endif %} + life_time = {{ esp.lifetime }}s +{% if esp.disable_rekey is vyos_defined %} + rekey_bytes = 0 + rekey_packets = 0 + rekey_time = 0s +{% endif %} dpd_action = clear inactivity = {{ rw_conf.timeout }} {% if rw_conf.replay_window is vyos_defined %} diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 7f425d982..4a7fde75b 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -99,6 +99,12 @@ </constraint> </properties> </leafNode> + <leafNode name="disable-rekey"> + <properties> + <help>Do not locally initiate a re-key of the SA, remote peer must re-key before expiration</help> + <valueless/> + </properties> + </leafNode> <leafNode name="mode"> <properties> <help>ESP mode</help> diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py index 60ef87a51..ce880f4a4 100644 --- a/python/vyos/utils/process.py +++ b/python/vyos/utils/process.py @@ -225,7 +225,7 @@ def process_named_running(name: str, cmdline: str=None, timeout: int=0): if not tmp: if time.time() > time_expire: break - time.sleep(0.100) # wait 250ms + time.sleep(0.100) # wait 100ms continue return tmp else: diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py index 4bcc50453..5acfe20fd 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -80,6 +80,8 @@ class VyOSUnitTestSHIM: self._session.discard() def cli_commit(self): + if self.debug: + print('commit') self._session.commit() # during a commit there is a process opening commit_lock, and run() returns 0 while run(f'sudo lsof -nP {commit_lock}') == 0: diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index 58aef0001..7bfe0d221 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -22,9 +22,11 @@ from base_interfaces_test import BasicInterfaceTest from glob import glob from vyos.configsession import ConfigSessionError -from vyos.utils.process import process_named_running -from vyos.utils.kernel import check_kmod from vyos.utils.file import read_file +from vyos.utils.kernel import check_kmod +from vyos.utils.network import interface_exists +from vyos.utils.process import process_named_running +from vyos.utils.process import call from vyos.xml_ref import default_value def get_config_value(interface, key): @@ -33,7 +35,7 @@ def get_config_value(interface, key): return tmp[0] wifi_cc_path = ['system', 'wireless', 'country-code'] - +country = 'se' class WirelessInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): @@ -66,7 +68,8 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): cls._test_ipv6 = False cls._test_vlan = False - cls.cli_set(cls, wifi_cc_path + ['se']) + cls.cli_set(cls, wifi_cc_path + [country]) + def test_wireless_add_single_ip_address(self): # derived method to check if member interfaces are enslaved properly @@ -84,7 +87,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_config(self): # Only set the hostapd (access-point) options - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'ssid' self.cli_set(self._base_path + [interface, 'ssid', ssid]) @@ -161,7 +164,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_vht_mu_beamformer_config(self): # Multi-User-Beamformer - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'vht_mu-beamformer' antennas = '3' @@ -230,7 +233,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_vht_su_beamformer_config(self): # Single-User-Beamformer - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'vht_su-beamformer' antennas = '3' @@ -299,16 +302,14 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_he_config(self): # Only set the hostapd (access-point) options - HE mode for 802.11ax at 6GHz - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'ssid' channel = '1' sae_pw = 'VyOSVyOSVyOS' - country = 'de' bss_color = '37' channel_set_width = '134' center_channel_freq_1 = '15' - self.cli_set(wifi_cc_path + [country]) self.cli_set(self._base_path + [interface, 'ssid', ssid]) self.cli_set(self._base_path + [interface, 'type', 'access-point']) self.cli_set(self._base_path + [interface, 'channel', channel]) @@ -353,10 +354,6 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): tmp = get_config_value(interface, 'he_oper_centr_freq_seg0_idx') self.assertEqual(center_channel_freq_1, tmp) - # Country code - tmp = get_config_value(interface, 'country_code') - self.assertEqual(country.upper(), tmp) - # BSS coloring tmp = get_config_value(interface, 'he_bss_color') self.assertEqual(bss_color, tmp) @@ -386,15 +383,12 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_wpa_config(self): # Only set the hostapd (access-point) options - interface = 'wlan1' - phy = 'phy0' + interface = self._interfaces[1] # wlan1 ssid = 'VyOS-SMOKETEST' channel = '1' wpa_key = 'VyOSVyOSVyOS' mode = 'n' - country = 'de' - self.cli_set(self._base_path + [interface, 'physical-device', phy]) self.cli_set(self._base_path + [interface, 'type', 'access-point']) self.cli_set(self._base_path + [interface, 'mode', mode]) @@ -454,7 +448,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.assertTrue(process_named_running('hostapd')) def test_wireless_access_point_bridge(self): - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'VyOS-Test' bridge = 'br42477' @@ -491,7 +485,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(bridge_path) def test_wireless_security_station_address(self): - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'VyOS-ACL' hostapd_accept_station_conf = f'/run/hostapd/{interface}_station_accept.conf' @@ -511,6 +505,12 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() + self.assertTrue(interface_exists(interface)) + self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_accept.conf')) + self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_deny.conf')) + + self.assertTrue(process_named_running('hostapd')) + # in accept mode all addresses are allowed unless specified in the deny list tmp = get_config_value(interface, 'macaddr_acl') self.assertEqual(tmp, '0') @@ -526,6 +526,11 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): # Switch mode accept -> deny self.cli_set(self._base_path + [interface, 'security', 'station-address', 'mode', 'deny']) self.cli_commit() + + self.assertTrue(interface_exists(interface)) + self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_accept.conf')) + self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_deny.conf')) + # In deny mode all addresses are denied unless specified in the allow list tmp = get_config_value(interface, 'macaddr_acl') self.assertEqual(tmp, '1') @@ -535,4 +540,9 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): if __name__ == '__main__': check_kmod('mac80211_hwsim') - unittest.main(verbosity=2, failfast=True) + # loading the module created two WIFI Interfaces in the background (wlan0 and wlan1) + # remove them to have a clean test start + for interface in ['wlan0', 'wlan1']: + if interface_exists(interface): + call(f'sudo iw dev {interface} del') + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 585c1dc89..905eaf2e9 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -16,7 +16,6 @@ import unittest -from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError @@ -27,6 +26,7 @@ PROCESS_NAME = 'ospfd' base_path = ['protocols', 'ospf'] route_map = 'foo-bar-baz10' +dummy_if = 'dum3562' class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): @classmethod @@ -38,6 +38,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit']) + cls.cli_set(cls, ['interfaces', 'dummy', dummy_if]) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) @@ -46,6 +47,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): @classmethod def tearDownClass(cls): cls.cli_delete(cls, ['policy', 'route-map', route_map]) + cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if]) super(TestProtocolsOSPF, cls).tearDownClass() def tearDown(self): @@ -441,14 +443,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): global_block_high = "399" local_block_low = "400" local_block_high = "499" - interface = 'lo' maximum_stack_size = '5' prefix_one = '192.168.0.1/32' prefix_two = '192.168.0.2/32' prefix_one_value = '1' prefix_two_value = '2' - self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['interface', dummy_if]) self.cli_set(base_path + ['segment-routing', 'maximum-label-depth', maximum_stack_size]) self.cli_set(base_path + ['segment-routing', 'global-block', 'low-label-value', global_block_low]) self.cli_set(base_path + ['segment-routing', 'global-block', 'high-label-value', global_block_high]) @@ -472,17 +473,14 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): def test_ospf_15_ldp_sync(self): holddown = "500" - interface = 'lo' interfaces = Section.interfaces('ethernet') - self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['interface', dummy_if]) self.cli_set(base_path + ['ldp-sync', 'holddown', holddown]) # Commit main OSPF changes self.cli_commit() - sleep(10) - # Verify main OSPF changes frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) self.assertIn(f'router ospf', frrconfig) @@ -514,7 +512,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf dead-interval 40', config) - self.assertIn(f' no ip ospf mpls ldp-sync', config) + self.assertNotIn(f' ip ospf mpls ldp-sync', config) def test_ospf_16_graceful_restart(self): period = '300' diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index b3daa90d0..7d5eaa440 100755 --- a/smoketest/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -246,5 +246,19 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): for excluded in snmpv3_view_oid_exclude: self.assertIn(f'view {snmpv3_view} excluded .{excluded}', tmp) + def test_snmp_script_extensions(self): + extensions = { + 'default': 'snmp_smoketest_extension_script.sh', + 'external': '/run/external_snmp_smoketest_extension_script.sh' + } + + for key, val in extensions.items(): + self.cli_set(base_path + ['script-extensions', 'extension-name', key, 'script', val]) + self.cli_commit() + + self.assertEqual(get_config_value('extend default'), f'/config/user-data/{extensions["default"]}') + self.assertEqual(get_config_value('extend external'), extensions["external"]) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 27356d70e..2dc66485b 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -252,6 +252,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): for line in swanctl_conf_lines: self.assertIn(line, swanctl_conf) + # if dpd is not specified it should not be enabled (see T6599) + swanctl_unexpected_lines = [ + f'dpd_timeout' + f'dpd_delay' + ] + + for unexpected_line in swanctl_unexpected_lines: + self.assertNotIn(unexpected_line, swanctl_conf) + swanctl_secrets_lines = [ f'id-{regex_uuid4} = "{local_id}"', f'id-{regex_uuid4} = "{remote_id}"', @@ -639,8 +648,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'auth = eap-mschapv2', f'eap_id = %any', f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', - f'rekey_time = {eap_lifetime}s', - f'rand_time = 540s', + f'life_time = {eap_lifetime}s', f'dpd_action = clear', f'replay_window = 32', f'inactivity = 28800', @@ -761,8 +769,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'auth = eap-tls', f'eap_id = %any', f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', - f'rekey_time = {eap_lifetime}s', - f'rand_time = 540s', + f'life_time = {eap_lifetime}s', f'dpd_action = clear', f'inactivity = 28800', f'local_ts = 0.0.0.0/0,::/0', @@ -876,8 +883,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'certs = peer1.pem', f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem', f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', - f'rekey_time = {eap_lifetime}s', - f'rand_time = 540s', + f'life_time = {eap_lifetime}s', f'dpd_action = clear', f'inactivity = 28800', f'local_ts = 0.0.0.0/0,::/0', @@ -968,5 +974,117 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.tearDownPKI() + def test_remote_access_no_rekey(self): + # In some RA secnarios, disabling server-initiated rekey of IKE and CHILD SA is desired + self.setupPKI() + + ike_group = 'IKE-RW' + esp_group = 'ESP-RW' + + conn_name = 'vyos-rw' + local_address = '192.0.2.1' + ip_pool_name = 'ra-rw-ipv4' + ike_lifetime = '7200' + eap_lifetime = '3600' + local_id = 'ipsec.vyos.net' + + name_servers = ['172.16.254.100', '172.16.254.101'] + prefix = '172.16.250.0/28' + + # IKE + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', '0']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256']) + + # ESP + self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime]) + self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable']) + self.cli_set(base_path + ['esp-group', esp_group, 'disable-rekey']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256']) + + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id]) + # Use client-mode x509 instead of default EAP-MSCHAPv2 + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'client-mode', 'x509']) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509']) + + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name]) + # verify() - CA cert required for x509 auth + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name]) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', int_ca_name]) + + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group]) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group]) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address]) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name]) + + for ns in name_servers: + self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns]) + self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix]) + + self.cli_commit() + + # verify applied configuration + swanctl_conf = read_file(swanctl_file) + swanctl_lines = [ + f'{conn_name}', + f'remote_addrs = %any', + f'local_addrs = {local_address}', + f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048', + f'version = 2', + f'send_certreq = no', + f'rekey_time = 0s', + f'keyingtries = 0', + f'pools = {ip_pool_name}', + f'id = "{local_id}"', + f'auth = pubkey', + f'certs = peer1.pem', + f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem', + f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', + f'life_time = {eap_lifetime}s', + f'rekey_time = 0s', + f'dpd_action = clear', + f'inactivity = 28800', + f'local_ts = 0.0.0.0/0,::/0', + ] + for line in swanctl_lines: + self.assertIn(line, swanctl_conf) + + swanctl_pool_lines = [ + f'{ip_pool_name}', + f'addrs = {prefix}', + f'dns = {",".join(name_servers)}', + ] + for line in swanctl_pool_lines: + self.assertIn(line, swanctl_conf) + + # Check Root CA, Intermediate CA and Peer cert/key pair is present + self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem'))) + self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}.pem'))) + self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem'))) + + self.tearDownPKI() + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py index 4666e98e7..700e4cec7 100755 --- a/smoketest/scripts/system/test_kernel_options.py +++ b/smoketest/scripts/system/test_kernel_options.py @@ -19,8 +19,9 @@ import os import platform import unittest -kernel = platform.release() +from vyos.utils.kernel import check_kmod +kernel = platform.release() class TestKernelModules(unittest.TestCase): """ VyOS makes use of a lot of Kernel drivers, modules and features. The required modules which are essential for VyOS should be tested that they are @@ -35,9 +36,8 @@ class TestKernelModules(unittest.TestCase): super(TestKernelModules, cls).setUpClass() CONFIG = '/proc/config.gz' - if not os.path.isfile(CONFIG): - call('sudo modprobe configs') + check_kmod('configs') with gzip.open(CONFIG, 'rt') as f: cls._config_data = f.read() diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index 0dc76b39a..320ab7b7b 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -235,10 +235,6 @@ def verify_pki(openvpn): def verify(openvpn): if 'deleted' in openvpn: - # remove totp secrets file if totp is not configured - if os.path.isfile(otp_file.format(**openvpn)): - os.remove(otp_file.format(**openvpn)) - verify_bridge_delete(openvpn) return None @@ -635,9 +631,19 @@ def generate_pki_files(openvpn): def generate(openvpn): + if 'deleted' in openvpn: + # remove totp secrets file if totp is not configured + if os.path.isfile(otp_file.format(**openvpn)): + os.remove(otp_file.format(**openvpn)) + return None + + if 'disable' in openvpn: + return None + interface = openvpn['ifname'] directory = os.path.dirname(cfg_file.format(**openvpn)) openvpn['plugin_dir'] = '/usr/lib/openvpn' + # create base config directory on demand makedir(directory, user, group) # enforce proper permissions on /run/openvpn @@ -654,9 +660,6 @@ def generate(openvpn): if os.path.isdir(service_dir): rmtree(service_dir, ignore_errors=True) - if 'deleted' in openvpn or 'disable' in openvpn: - return None - # create client config directory on demand makedir(ccd_dir, user, group) diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py index 5fd7ab6e9..f35a250cb 100755 --- a/src/conf_mode/interfaces_wireless.py +++ b/src/conf_mode/interfaces_wireless.py @@ -19,6 +19,7 @@ import os from sys import exit from re import findall from netaddr import EUI, mac_unix_expanded +from time import sleep from vyos.config import Config from vyos.configdict import get_interface_dict @@ -34,6 +35,9 @@ from vyos.template import render from vyos.utils.dict import dict_search from vyos.utils.kernel import check_kmod from vyos.utils.process import call +from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import is_systemd_service_running +from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag airbag.enable() @@ -93,6 +97,11 @@ def get_config(config=None): if wifi.from_defaults(['security', 'wpa']): # if not set by user del wifi['security']['wpa'] + # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number + if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []): + wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable'] + del wifi['capabilities']['ht']['40mhz_incapable'] + if dict_search('security.wpa', wifi) != None: wpa_cipher = wifi['security']['wpa'].get('cipher') wpa_mode = wifi['security']['wpa'].get('mode') @@ -120,7 +129,7 @@ def get_config(config=None): tmp = find_other_stations(conf, base, wifi['ifname']) if tmp: wifi['station_interfaces'] = tmp - # used in hostapt.conf.j2 + # used in hostapd.conf.j2 wifi['hostapd_accept_station_conf'] = hostapd_accept_station_conf.format(**wifi) wifi['hostapd_deny_station_conf'] = hostapd_deny_station_conf.format(**wifi) @@ -232,11 +241,6 @@ def verify(wifi): def generate(wifi): interface = wifi['ifname'] - # always stop hostapd service first before reconfiguring it - call(f'systemctl stop hostapd@{interface}.service') - # always stop wpa_supplicant service first before reconfiguring it - call(f'systemctl stop wpa_supplicant@{interface}.service') - # Delete config files if interface is removed if 'deleted' in wifi: if os.path.isfile(hostapd_conf.format(**wifi)): @@ -272,11 +276,6 @@ def generate(wifi): mac.dialect = mac_unix_expanded wifi['mac'] = str(mac) - # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number - if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []): - wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable'] - del wifi['capabilities']['ht']['40mhz_incapable'] - # render appropriate new config files depending on access-point or station mode if wifi['type'] == 'access-point': render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', wifi) @@ -290,23 +289,45 @@ def generate(wifi): def apply(wifi): interface = wifi['ifname'] + # From systemd source code: + # If there's a stop job queued before we enter the DEAD state, we shouldn't act on Restart=, + # in order to not undo what has already been enqueued. */ + # + # It was found that calling restart on hostapd will (4 out of 10 cases) deactivate + # the service instead of restarting it, when it was not yet properly stopped + # systemd[1]: hostapd@wlan1.service: Deactivated successfully. + # Thus kill all WIFI service and start them again after it's ensured nothing lives + call(f'systemctl stop hostapd@{interface}.service') + call(f'systemctl stop wpa_supplicant@{interface}.service') + if 'deleted' in wifi: - WiFiIf(interface).remove() - else: - # Finally create the new interface - w = WiFiIf(**wifi) - w.update(wifi) - - # Enable/Disable interface - interface is always placed in - # administrative down state in WiFiIf class - if 'disable' not in wifi: - # Physical interface is now configured. Proceed by starting hostapd or - # wpa_supplicant daemon. When type is monitor we can just skip this. - if wifi['type'] == 'access-point': - call(f'systemctl start hostapd@{interface}.service') - - elif wifi['type'] == 'station': - call(f'systemctl start wpa_supplicant@{interface}.service') + WiFiIf(**wifi).remove() + return None + + while (is_systemd_service_running(f'hostapd@{interface}.service') or \ + is_systemd_service_active(f'hostapd@{interface}.service')): + sleep(0.250) # wait 250ms + + # Finally create the new interface + w = WiFiIf(**wifi) + w.update(wifi) + + # Enable/Disable interface - interface is always placed in + # administrative down state in WiFiIf class + if 'disable' not in wifi: + # Wait until interface was properly added to the Kernel + ii = 0 + while not (interface_exists(interface) and ii < 20): + sleep(0.250) # wait 250ms + ii += 1 + + # Physical interface is now configured. Proceed by starting hostapd or + # wpa_supplicant daemon. When type is monitor we can just skip this. + if wifi['type'] == 'access-point': + call(f'systemctl start hostapd@{interface}.service') + + elif wifi['type'] == 'station': + call(f'systemctl start wpa_supplicant@{interface}.service') return None diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py index 6f025cc23..c9c0ed9a0 100755 --- a/src/conf_mode/service_snmp.py +++ b/src/conf_mode/service_snmp.py @@ -41,6 +41,7 @@ config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' +default_script_dir = r'/config/user-data/' systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf' systemd_service = 'snmpd.service' @@ -85,8 +86,20 @@ def get_config(config=None): tmp = {'::1': {'port': '161'}} snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) + if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']: + for key, val in snmp['script_extensions']['extension_name'].items(): + if 'script' not in val: + continue + script_path = val['script'] + # if script has not absolute path, use pre configured path + if not os.path.isabs(script_path): + script_path = os.path.join(default_script_dir, script_path) + + snmp['script_extensions']['extension_name'][key]['script'] = script_path + return snmp + def verify(snmp): if 'deleted' in snmp: return None diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index dc78c755e..cf82b767f 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -24,6 +24,7 @@ from time import sleep from vyos.base import Warning from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdep import set_dependents from vyos.configdep import call_dependents from vyos.configdict import leaf_node_changed @@ -86,9 +87,22 @@ def get_config(config=None): ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, - with_recursive_defaults=True, with_pki=True) + # We have to cleanup the default dict, as default values could + # enable features which are not explicitly enabled on the + # CLI. E.g. dead-peer-detection defaults should not be injected + # unless the feature is explicitly opted in to by setting the + # top-level node + default_values = conf.get_config_defaults(**ipsec.kwargs, recursive=True) + + if 'ike_group' in ipsec: + for name, ike in ipsec['ike_group'].items(): + if 'dead_peer_detection' not in ike: + del default_values['ike_group'][name]['dead_peer_detection'] + + ipsec = config_dict_merge(default_values, ipsec) + ipsec['dhcp_interfaces'] = set() ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 12edb7d02..184725573 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -26,7 +26,6 @@ from vyos.ifconfig import Interface from vyos.template import render from vyos.template import render_to_string from vyos.utils.dict import dict_search -from vyos.utils.network import get_interface_config from vyos.utils.network import get_vrf_tableid from vyos.utils.network import get_vrf_members from vyos.utils.network import interface_exists diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py index 14ffdca9f..e7afc4caa 100755 --- a/src/op_mode/interfaces.py +++ b/src/op_mode/interfaces.py @@ -445,12 +445,24 @@ def _format_show_counters(data: list): print (output) return output + +def _show_raw(data: list, intf_name: str): + if intf_name is not None and len(data) <= 1: + try: + return data[0] + except IndexError: + raise vyos.opmode.UnconfiguredObject( + f"Interface {intf_name} does not exist") + else: + return data + + def show(raw: bool, intf_name: typing.Optional[str], intf_type: typing.Optional[str], vif: bool, vrrp: bool): data = _get_raw_data(intf_name, intf_type, vif, vrrp) if raw: - return data + return _show_raw(data, intf_name) return _format_show_data(data) def show_summary(raw: bool, intf_name: typing.Optional[str], @@ -458,7 +470,7 @@ def show_summary(raw: bool, intf_name: typing.Optional[str], vif: bool, vrrp: bool): data = _get_summary_data(intf_name, intf_type, vif, vrrp) if raw: - return data + return _show_raw(data, intf_name) return _format_show_summary(data) def show_summary_extended(raw: bool, intf_name: typing.Optional[str], @@ -466,7 +478,7 @@ def show_summary_extended(raw: bool, intf_name: typing.Optional[str], vif: bool, vrrp: bool): data = _get_summary_data(intf_name, intf_type, vif, vrrp) if raw: - return data + return _show_raw(data, intf_name) return _format_show_summary_extended(data) def show_counters(raw: bool, intf_name: typing.Optional[str], @@ -474,7 +486,7 @@ def show_counters(raw: bool, intf_name: typing.Optional[str], vif: bool, vrrp: bool): data = _get_counter_data(intf_name, intf_type, vif, vrrp) if raw: - return data + return _show_raw(data, intf_name) return _format_show_counters(data) def clear_counters(intf_name: typing.Optional[str], |