summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/dhcp-client/override.conf.j23
-rw-r--r--data/templates/high-availability/keepalived.conf.j24
-rw-r--r--data/templates/ipsec/swanctl/remote_access.j22
-rw-r--r--debian/vyos-1x.postinst2
-rw-r--r--interface-definitions/high-availability.xml.in23
-rw-r--r--interface-definitions/include/ipsec/authentication-x509.xml.i2
-rw-r--r--interface-definitions/vpn_ipsec.xml.in1
-rw-r--r--op-mode-definitions/dhcp.xml.in4
-rw-r--r--python/vyos/configdict.py4
-rw-r--r--python/vyos/opmode.py2
-rw-r--r--python/vyos/system/grub.py14
-rw-r--r--python/vyos/template.py17
-rw-r--r--python/vyos/utils/system.py7
-rw-r--r--smoketest/scripts/cli/base_accel_ppp_test.py4
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py7
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py80
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py8
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py86
-rwxr-xr-xsrc/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook77
-rwxr-xr-xsrc/op_mode/dhcp.py42
-rwxr-xr-xsrc/op_mode/image_manager.py44
-rwxr-xr-xsrc/op_mode/openvpn.py7
-rw-r--r--src/systemd/dhclient@.service1
23 files changed, 283 insertions, 158 deletions
diff --git a/data/templates/dhcp-client/override.conf.j2 b/data/templates/dhcp-client/override.conf.j2
index d09320270..c2e059c7b 100644
--- a/data/templates/dhcp-client/override.conf.j2
+++ b/data/templates/dhcp-client/override.conf.j2
@@ -3,9 +3,6 @@
{% set if_metric = '-e IF_METRIC=' ~ dhcp_options.default_route_distance if dhcp_options.default_route_distance is vyos_defined else '' %}
{% set dhclient_options = '-d -nw -cf ' ~ isc_dhclient_dir ~ '/dhclient_' ~ ifname ~ '.conf -pf ' ~ isc_dhclient_dir ~ '/dhclient_' ~ ifname ~ '.pid -lf ' ~ isc_dhclient_dir ~ '/dhclient_' ~ ifname ~ '.leases ' ~ if_metric %}
-[Unit]
-ConditionPathExists={{ isc_dhclient_dir }}/dhclient_%i.conf
-
[Service]
ExecStart=
ExecStart={{ vrf_command }}/sbin/dhclient -4 {{ dhclient_options }} {{ ifname }}
diff --git a/data/templates/high-availability/keepalived.conf.j2 b/data/templates/high-availability/keepalived.conf.j2
index 240161748..c0d66ae54 100644
--- a/data/templates/high-availability/keepalived.conf.j2
+++ b/data/templates/high-availability/keepalived.conf.j2
@@ -138,8 +138,8 @@ vrrp_instance {{ name }} {
{% endif %}
{% if group_config.excluded_address is vyos_defined %}
virtual_ipaddress_excluded {
-{% for addr in group_config.excluded_address %}
- {{ addr }}
+{% for addr, addr_config in group_config.excluded_address.items() %}
+ {{ addr }}{{ ' dev ' + addr_config.interface if addr_config.interface is vyos_defined }}
{% endfor %}
}
{% endif %}
diff --git a/data/templates/ipsec/swanctl/remote_access.j2 b/data/templates/ipsec/swanctl/remote_access.j2
index bce8684fe..af7f2994e 100644
--- a/data/templates/ipsec/swanctl/remote_access.j2
+++ b/data/templates/ipsec/swanctl/remote_access.j2
@@ -4,7 +4,7 @@
{% set esp = esp_group[rw_conf.esp_group] %}
ra-{{ name }} {
remote_addrs = %any
- local_addrs = {{ rw_conf.local_address if rw_conf.local_address is vyos_defined else '%any' }}
+ local_addrs = {{ rw_conf.local_address if rw_conf.local_address is not vyos_defined('any') else '%any' }} # dhcp:{{ rw_conf.dhcp_interface if rw_conf.dhcp_interface is vyos_defined else 'no' }}
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
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index 74fd229b4..0e6e3c863 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -71,6 +71,7 @@ if ! grep -q '^tacacs' /etc/passwd; then
adduser --quiet tacacs${level} sudo
adduser --quiet tacacs${level} disk
adduser --quiet tacacs${level} frr
+ adduser --quiet tacacs${level} _kea
fi
level=$(( level+1 ))
done 2>&1 | grep -v "User tacacs${level} already exists"
@@ -102,6 +103,7 @@ if ! grep -q '^radius_priv_user' /etc/passwd; then
adduser --quiet radius_priv_user disk
adduser --quiet radius_priv_user users
adduser --quiet radius_priv_user frr
+ adduser --quiet radius_priv_user _kea
fi
# add hostsd group for vyos-hostsd
diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in
index 558404882..7108aa06c 100644
--- a/interface-definitions/high-availability.xml.in
+++ b/interface-definitions/high-availability.xml.in
@@ -294,25 +294,34 @@
#include <include/generic-interface-broadcast.xml.i>
</children>
</tagNode>
- <leafNode name="excluded-address">
+ <tagNode name="excluded-address">
<properties>
<help>Virtual address (If you need additional IPv4 and IPv6 in same group)</help>
<valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
<format>ipv4</format>
- <description>IP address</description>
+ <description>IPv4 address</description>
</valueHelp>
<valueHelp>
<format>ipv6</format>
<description>IPv6 address</description>
</valueHelp>
- <multi/>
<constraint>
- <validator name="ipv4-host"/>
- <validator name="ipv6-host"/>
+ <validator name="ip-host"/>
+ <validator name="ip-address"/>
</constraint>
- <constraintErrorMessage>Virtual address must be a valid IPv4 or IPv6 address with prefix length (e.g. 192.0.2.3/24 or 2001:db8:ff::10/64)</constraintErrorMessage>
</properties>
- </leafNode>
+ <children>
+ #include <include/generic-interface-broadcast.xml.i>
+ </children>
+ </tagNode>
<leafNode name="vrid">
<properties>
<help>Virtual router identifier</help>
diff --git a/interface-definitions/include/ipsec/authentication-x509.xml.i b/interface-definitions/include/ipsec/authentication-x509.xml.i
index db675c0bf..1d04c94ba 100644
--- a/interface-definitions/include/ipsec/authentication-x509.xml.i
+++ b/interface-definitions/include/ipsec/authentication-x509.xml.i
@@ -5,7 +5,7 @@
</properties>
<children>
#include <include/pki/certificate-key.xml.i>
- #include <include/pki/ca-certificate.xml.i>
+ #include <include/pki/ca-certificate-multi.xml.i>
</children>
</node>
<!-- include end -->
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 44ca1c7a0..833019d68 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -825,6 +825,7 @@
#include <include/ipsec/esp-group.xml.i>
#include <include/ipsec/ike-group.xml.i>
#include <include/ipsec/local-address.xml.i>
+ #include <include/dhcp-interface.xml.i>
#include <include/ipsec/local-traffic-selector.xml.i>
#include <include/ipsec/replay-window.xml.i>
<leafNode name="timeout">
diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in
index 3c42c8e8f..0b4a05ffe 100644
--- a/op-mode-definitions/dhcp.xml.in
+++ b/op-mode-definitions/dhcp.xml.in
@@ -293,7 +293,7 @@
<script>${vyos_completion_dir}/list_interfaces</script>
</completionHelp>
</properties>
- <command>sudo systemctl restart "dhclient@$4.service"</command>
+ <command>sudo ${vyos_op_scripts_dir}/dhcp.py renew_client_lease --family inet --interface "$4"</command>
</tagNode>
</children>
</node>
@@ -309,7 +309,7 @@
<script>${vyos_completion_dir}/list_interfaces</script>
</completionHelp>
</properties>
- <command>sudo systemctl restart "dhcp6c@$4.service"</command>
+ <command>sudo ${vyos_op_scripts_dir}/dhcp.py renew_client_lease --family inet6 --interface "$4"</command>
</tagNode>
</children>
</node>
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 4111d7271..cb9f0cbb8 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -633,7 +633,7 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False):
Return a dictionary with the necessary interface config keys.
"""
- from vyos.utils.system import get_half_cpus
+ from vyos.cpu import get_core_count
from vyos.template import is_ipv4
dict = config.get_config_dict(base, key_mangling=('-', '_'),
@@ -643,7 +643,7 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False):
with_pki=with_pki)
# set CPUs cores to process requests
- dict.update({'thread_count' : get_half_cpus()})
+ dict.update({'thread_count' : get_core_count()})
# we need to store the path to the secrets file
dict.update({'chap_secrets_file' : chap_secrets})
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
index e1af1a682..8dab9a4ca 100644
--- a/python/vyos/opmode.py
+++ b/python/vyos/opmode.py
@@ -81,7 +81,7 @@ class InternalError(Error):
def _is_op_mode_function_name(name):
- if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set)", name):
+ if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew)", name):
return True
else:
return False
diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py
index 864ed65aa..5ab9d76f0 100644
--- a/python/vyos/system/grub.py
+++ b/python/vyos/system/grub.py
@@ -56,7 +56,7 @@ REGEX_KERNEL_CMDLINE: str = r'^BOOT_IMAGE=/(?P<boot_type>boot|live)/((?P<image_v
REGEX_GRUB_BOOT_OPTS: str = r'^\s*set boot_opts="(?P<boot_opts>[^$]+)"$'
-def install(drive_path: str, boot_dir: str, efi_dir: str, id: str = 'VyOS') -> None:
+def install(drive_path: str, boot_dir: str, efi_dir: str, id: str = 'VyOS', chroot : str = "") -> None:
"""Install GRUB for both BIOS and EFI modes (hybrid boot)
Args:
@@ -65,17 +65,22 @@ def install(drive_path: str, boot_dir: str, efi_dir: str, id: str = 'VyOS') -> N
efi_dir (str): a path to '/boot/efi' directory
"""
+ if chroot:
+ chroot_cmd = f"chroot {chroot}"
+ else:
+ chroot_cmd = ""
+
efi_installation_arch = "x86_64"
if platform.machine() == "aarch64":
efi_installation_arch = "arm64"
elif platform.machine() == "x86_64":
cmd(
- f'grub-install --no-floppy --target=i386-pc \
+ f'{chroot_cmd} grub-install --no-floppy --target=i386-pc \
--boot-directory={boot_dir} {drive_path} --force'
)
cmd(
- f'grub-install --no-floppy --recheck --target={efi_installation_arch}-efi \
+ f'{chroot_cmd} grub-install --no-floppy --recheck --target={efi_installation_arch}-efi \
--force-extra-removable --boot-directory={boot_dir} \
--efi-directory={efi_dir} --bootloader-id="{id}" \
--no-uefi-secure-boot'
@@ -374,7 +379,7 @@ def create_structure(root_dir: str = '') -> None:
if not root_dir:
root_dir = disk.find_persistence()
- Path(f'{root_dir}/GRUB_DIR_VYOS_VERS').mkdir(parents=True, exist_ok=True)
+ Path(f'{root_dir}/{GRUB_DIR_VYOS_VERS}').mkdir(parents=True, exist_ok=True)
def set_console_type(console_type: str, root_dir: str = '') -> None:
@@ -458,3 +463,4 @@ def sort_inodes(dir_path: str) -> None:
for item in temp_list_new:
new_name = Path(f'{item.as_posix()[0:-4]}')
item.rename(new_name)
+
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 19a469c8f..1aa9ace8b 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2023 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
@@ -32,8 +32,21 @@ _TESTS = {}
# reuse Environments with identical settings to improve performance
@functools.lru_cache(maxsize=2)
def _get_environment(location=None):
+ from os import getenv
+
if location is None:
- loc_loader=FileSystemLoader(directories["templates"])
+ # Sometimes functions that rely on templates need to be executed outside of VyOS installations:
+ # for example, installer functions are executed for image builds,
+ # and anything may be invoked for testing from a developer's machine.
+ # This environment variable allows running any unmodified code
+ # with a custom template location.
+ location_env_var = getenv("VYOS_TEMPLATE_DIR")
+ if location_env_var:
+ print(f"Using environment variable {location_env_var}")
+ template_dir = location_env_var
+ else:
+ template_dir = directories["templates"]
+ loc_loader=FileSystemLoader(template_dir)
else:
loc_loader=FileSystemLoader(location)
env = Environment(
diff --git a/python/vyos/utils/system.py b/python/vyos/utils/system.py
index 5d41c0c05..55813a5f7 100644
--- a/python/vyos/utils/system.py
+++ b/python/vyos/utils/system.py
@@ -79,13 +79,6 @@ def sysctl_apply(sysctl_dict: dict[str, str], revert: bool = True) -> bool:
# everything applied
return True
-def get_half_cpus():
- """ return 1/2 of the numbers of available CPUs """
- cpu = os.cpu_count()
- if cpu > 1:
- cpu /= 2
- return int(cpu)
-
def find_device_file(device):
""" Recurively search /dev for the given device file and return its full path.
If no device file was found 'None' is returned """
diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py
index ac4bbcfe5..cc27cfbe9 100644
--- a/smoketest/scripts/cli/base_accel_ppp_test.py
+++ b/smoketest/scripts/cli/base_accel_ppp_test.py
@@ -21,7 +21,7 @@ from configparser import ConfigParser
from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.template import is_ipv4
-from vyos.utils.system import get_half_cpus
+from vyos.cpu import get_core_count
from vyos.utils.process import process_named_running
from vyos.utils.process import cmd
@@ -132,7 +132,7 @@ class BasicAccelPPPTest:
return out
def verify(self, conf):
- self.assertEqual(conf["core"]["thread-count"], str(get_half_cpus()))
+ self.assertEqual(conf["core"]["thread-count"], str(get_core_count()))
def test_accel_name_servers(self):
# Verify proper Name-Server configuration for IPv4 and IPv6
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
index 5f238b25a..e8556cf44 100755
--- a/smoketest/scripts/cli/test_protocols_bgp.py
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -1241,6 +1241,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
+ self.cli_set(base_path + ['peer-group', 'peer1', 'remote-as', 'internal'])
+ self.cli_commit()
+
+ conf = self.getFRRconfig(' address-family l2vpn evpn')
+
+ self.assertIn('neighbor peer1 route-reflector-client', conf)
+
def test_bgp_99_bmp(self):
target_name = 'instance-bmp'
target_address = '127.0.0.1'
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 09e10a2c4..ab832e91e 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -31,7 +31,7 @@ nhrp_path = ['protocols', 'nhrp']
base_path = ['vpn', 'ipsec']
charon_file = '/etc/strongswan.d/charon.conf'
-dhcp_waiting_file = '/tmp/ipsec_dhcp_waiting'
+dhcp_interfaces_file = '/tmp/ipsec_dhcp_interfaces'
swanctl_file = '/etc/swanctl/swanctl.conf'
peer_ip = '203.0.113.45'
@@ -178,10 +178,10 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- self.assertTrue(os.path.exists(dhcp_waiting_file))
+ self.assertTrue(os.path.exists(dhcp_interfaces_file))
- dhcp_waiting = read_file(dhcp_waiting_file)
- self.assertIn(f'{interface}.{vif}', dhcp_waiting) # Ensure dhcp-failed interface was added for dhclient hook
+ dhcp_interfaces = read_file(dhcp_interfaces_file)
+ self.assertIn(f'{interface}.{vif}', dhcp_interfaces) # Ensure dhcp interface was added for dhclient hook
self.cli_delete(ethernet_path + [interface, 'vif', vif, 'address'])
@@ -413,6 +413,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.cli_set(peer_base_path + ['authentication', 'local-id', peer_name])
self.cli_set(peer_base_path + ['authentication', 'mode', 'x509'])
self.cli_set(peer_base_path + ['authentication', 'remote-id', 'peer2'])
+ self.cli_set(peer_base_path + ['authentication', 'x509', 'ca-certificate', ca_name])
self.cli_set(peer_base_path + ['authentication', 'x509', 'ca-certificate', int_ca_name])
self.cli_set(peer_base_path + ['authentication', 'x509', 'certificate', peer_name])
self.cli_set(peer_base_path + ['connection-type', 'initiate'])
@@ -465,8 +466,8 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
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'{int_ca_name}_1.pem')))
- self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}_2.pem')))
+ 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')))
# There is only one VTI test so no need to delete this globally in tearDown()
@@ -666,7 +667,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
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}_1.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
self.tearDownPKI()
@@ -778,7 +779,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
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}_1.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
self.tearDownPKI()
@@ -893,10 +894,71 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
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}_1.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
self.tearDownPKI()
+ def test_remote_access_dhcp_fail_handling(self):
+ # Skip process check - connection is not created for this test
+ self.skip_process_check = True
+
+ # Interface for dhcp-interface
+ self.cli_set(ethernet_path + [interface, 'vif', vif, 'address', 'dhcp']) # Use VLAN to avoid getting IP from qemu dhcp server
+
+ # This is a known to be good configuration for Microsoft Windows 10 and Apple iOS 17
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ ip_pool_name = 'ra-rw-ipv4'
+ username = 'vyos'
+ password = 'secret'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_server = '172.16.254.100'
+ 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', ike_lifetime])
+ 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'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ 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 + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-users', 'username', username, 'password', password])
+ 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])
+ 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, '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, 'dhcp-interface', f'{interface}.{vif}'])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', name_server])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix])
+
+ self.cli_commit()
+
+ self.assertTrue(os.path.exists(dhcp_interfaces_file))
+
+ dhcp_interfaces = read_file(dhcp_interfaces_file)
+ self.assertIn(f'{interface}.{vif}', dhcp_interfaces) # Ensure dhcp interface was added for dhclient hook
+
+ self.cli_delete(ethernet_path + [interface, 'vif', vif, 'address'])
+
+ self.tearDownPKI()
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index f1c59cbde..512fa26e9 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -450,15 +450,15 @@ def verify(bgp):
verify_route_map(afi_config['route_map'][tmp], bgp)
if 'route_reflector_client' in afi_config:
- if 'remote_as' in peer_config and peer_config['remote_as'] != 'internal' and peer_config['remote_as'] != bgp['system_as']:
+ peer_group_as = peer_config.get('remote_as')
+
+ if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):
raise ConfigError('route-reflector-client only supported for iBGP peers')
else:
if 'peer_group' in peer_config:
peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp)
- if peer_group_as != None and peer_group_as != 'internal' and peer_group_as != bgp['system_as']:
+ if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):
raise ConfigError('route-reflector-client only supported for iBGP peers')
- else:
- raise ConfigError('route-reflector-client only supported for iBGP peers')
# Throw an error if a peer group is not configured for allow range
for prefix in dict_search('listen.range', bgp) or []:
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 388f2a709..0c2f232df 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -32,10 +32,7 @@ from vyos.configverify import verify_interface_exists
from vyos.configverify import dynamic_interface_pattern
from vyos.defaults import directories
from vyos.ifconfig import Interface
-from vyos.pki import encode_certificate
from vyos.pki import encode_public_key
-from vyos.pki import find_chain
-from vyos.pki import load_certificate
from vyos.pki import load_private_key
from vyos.pki import wrap_certificate
from vyos.pki import wrap_crl
@@ -75,7 +72,7 @@ KEY_PATH = f'{swanctl_dir}/private/'
CA_PATH = f'{swanctl_dir}/x509ca/'
CRL_PATH = f'{swanctl_dir}/x509crl/'
-DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_waiting'
+DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_interfaces'
def get_config(config=None):
if config:
@@ -94,6 +91,7 @@ def get_config(config=None):
with_recursive_defaults=True,
with_pki=True)
+ ipsec['dhcp_interfaces'] = set()
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
@@ -126,11 +124,11 @@ def verify_pki_x509(pki, x509_conf):
if not pki or 'ca' not in pki or 'certificate' not in pki:
raise ConfigError(f'PKI is not configured')
- ca_cert_name = x509_conf['ca_certificate']
cert_name = x509_conf['certificate']
- if not dict_search_args(pki, 'ca', ca_cert_name, 'certificate'):
- raise ConfigError(f'Missing CA certificate on specified PKI CA certificate "{ca_cert_name}"')
+ for ca_cert_name in x509_conf['ca_certificate']:
+ if not dict_search_args(pki, 'ca', ca_cert_name, 'certificate'):
+ raise ConfigError(f'Missing CA certificate on specified PKI CA certificate "{ca_cert_name}"')
if not dict_search_args(pki, 'certificate', cert_name, 'certificate'):
raise ConfigError(f'Missing certificate on specified PKI certificate "{cert_name}"')
@@ -229,6 +227,32 @@ def verify(ipsec):
if 'remote_access' in ipsec:
if 'connection' in ipsec['remote_access']:
for name, ra_conf in ipsec['remote_access']['connection'].items():
+ if 'local_address' not in ra_conf and 'dhcp_interface' not in ra_conf:
+ raise ConfigError(f"Missing local-address or dhcp-interface on remote-access connection {name}")
+
+ if 'dhcp_interface' in ra_conf:
+ dhcp_interface = ra_conf['dhcp_interface']
+
+ verify_interface_exists(dhcp_interface)
+ dhcp_base = directories['isc_dhclient_dir']
+
+ if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
+ raise ConfigError(f"Invalid dhcp-interface on remote-access connection {name}")
+
+ ipsec['dhcp_interfaces'].add(dhcp_interface)
+
+ address = get_dhcp_address(dhcp_interface)
+ count = 0
+ while not address and count < dhcp_wait_attempts:
+ address = get_dhcp_address(dhcp_interface)
+ count += 1
+ sleep(dhcp_wait_sleep)
+
+ if not address:
+ ipsec['dhcp_no_address'][f'ra_{name}'] = dhcp_interface
+ print(f"Failed to get address from dhcp-interface on remote-access connection {name} -- skipped")
+ continue
+
if 'esp_group' in ra_conf:
if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']:
raise ConfigError(f"Invalid esp-group on {name} remote-access config")
@@ -386,6 +410,8 @@ def verify(ipsec):
if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}")
+ ipsec['dhcp_interfaces'].add(dhcp_interface)
+
address = get_dhcp_address(dhcp_interface)
count = 0
while not address and count < dhcp_wait_attempts:
@@ -394,7 +420,7 @@ def verify(ipsec):
sleep(dhcp_wait_sleep)
if not address:
- ipsec['dhcp_no_address'][peer] = dhcp_interface
+ ipsec['dhcp_no_address'][f'peer_{peer}'] = dhcp_interface
print(f"Failed to get address from dhcp-interface on site-to-site peer {peer} -- skipped")
continue
@@ -443,32 +469,24 @@ def cleanup_pki_files():
os.unlink(file_path)
def generate_pki_files_x509(pki, x509_conf):
- ca_cert_name = x509_conf['ca_certificate']
- ca_cert_data = dict_search_args(pki, 'ca', ca_cert_name, 'certificate')
- ca_cert_crls = dict_search_args(pki, 'ca', ca_cert_name, 'crl') or []
- ca_index = 1
- crl_index = 1
+ for ca_cert_name in x509_conf['ca_certificate']:
+ ca_cert_data = dict_search_args(pki, 'ca', ca_cert_name, 'certificate')
+ ca_cert_crls = dict_search_args(pki, 'ca', ca_cert_name, 'crl') or []
+ crl_index = 1
- ca_cert = load_certificate(ca_cert_data)
- pki_ca_certs = [load_certificate(ca['certificate']) for ca in pki['ca'].values()]
+ with open(os.path.join(CA_PATH, f'{ca_cert_name}.pem'), 'w') as f:
+ f.write(wrap_certificate(ca_cert_data))
- ca_cert_chain = find_chain(ca_cert, pki_ca_certs)
+ for crl in ca_cert_crls:
+ with open(os.path.join(CRL_PATH, f'{ca_cert_name}_{crl_index}.pem'), 'w') as f:
+ f.write(wrap_crl(crl))
+ crl_index += 1
cert_name = x509_conf['certificate']
cert_data = dict_search_args(pki, 'certificate', cert_name, 'certificate')
key_data = dict_search_args(pki, 'certificate', cert_name, 'private', 'key')
protected = 'passphrase' in x509_conf
- for ca_cert_obj in ca_cert_chain:
- with open(os.path.join(CA_PATH, f'{ca_cert_name}_{ca_index}.pem'), 'w') as f:
- f.write(encode_certificate(ca_cert_obj))
- ca_index += 1
-
- for crl in ca_cert_crls:
- with open(os.path.join(CRL_PATH, f'{ca_cert_name}_{crl_index}.pem'), 'w') as f:
- f.write(wrap_crl(crl))
- crl_index += 1
-
with open(os.path.join(CERT_PATH, f'{cert_name}.pem'), 'w') as f:
f.write(wrap_certificate(cert_data))
@@ -503,9 +521,9 @@ def generate(ipsec):
render(charon_conf, 'ipsec/charon.j2', {'install_routes': default_install_routes})
return
- if ipsec['dhcp_no_address']:
+ if ipsec['dhcp_interfaces']:
with open(DHCP_HOOK_IFLIST, 'w') as f:
- f.write(" ".join(ipsec['dhcp_no_address'].values()))
+ f.write(" ".join(ipsec['dhcp_interfaces']))
elif os.path.exists(DHCP_HOOK_IFLIST):
os.unlink(DHCP_HOOK_IFLIST)
@@ -522,13 +540,23 @@ def generate(ipsec):
if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']:
for rw, rw_conf in ipsec['remote_access']['connection'].items():
+ if f'ra_{rw}' in ipsec['dhcp_no_address']:
+ continue
+
+ local_ip = ''
+ if 'local_address' in rw_conf:
+ local_ip = rw_conf['local_address']
+ elif 'dhcp_interface' in rw_conf:
+ local_ip = get_dhcp_address(rw_conf['dhcp_interface'])
+
+ ipsec['remote_access']['connection'][rw]['local_address'] = local_ip
if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']:
generate_pki_files_x509(ipsec['pki'], rw_conf['authentication']['x509'])
if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
for peer, peer_conf in ipsec['site_to_site']['peer'].items():
- if peer in ipsec['dhcp_no_address']:
+ if f'peer_{peer}' in ipsec['dhcp_no_address']:
continue
if peer_conf['authentication']['mode'] == 'x509':
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook
index e6edc1ac3..ebb100e8b 100755
--- a/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook
@@ -14,69 +14,32 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-if [ "$reason" == "REBOOT" ] || [ "$reason" == "EXPIRE" ]; then
- return 0
-fi
-
-DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_waiting"
+DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_interfaces"
-if [ -f $DHCP_HOOK_IFLIST ] && [ "$reason" == "BOUND" ]; then
- if grep -qw $interface $DHCP_HOOK_IFLIST; then
- sudo rm $DHCP_HOOK_IFLIST
- sudo /usr/libexec/vyos/conf_mode/vpn_ipsec.py
- return 0
- fi
+if ! { [ -f $DHCP_HOOK_IFLIST ] && grep -qw $interface $DHCP_HOOK_IFLIST; }; then
+ exit 0
fi
-if [ "$old_ip_address" == "$new_ip_address" ] && [ "$reason" == "BOUND" ]; then
- return 0
+# Re-generate the config on the following events:
+# - BOUND: always re-generate
+# - RENEW: re-generate if the IP address changed
+# - REBIND: re-generate if the IP address changed
+if [ "$reason" == "RENEW" ] || [ "$reason" == "REBIND" ]; then
+ if [ "$old_ip_address" == "$new_ip_address" ]; then
+ exit 0
+ fi
+elif [ "$reason" != "BOUND" ]; then
+ exit 0
fi
-python3 - <<PYEND
-import os
-import re
-
-from vyos.utils.process import call
-from vyos.utils.process import cmd
-from vyos.utils.file import read_file
-from vyos.utils.file import write_file
-
-SWANCTL_CONF="/etc/swanctl/swanctl.conf"
-
-def ipsec_down(ip_address):
- # This prevents the need to restart ipsec and kill all active connections, only the stale connection is closed
- status = cmd('sudo ipsec statusall')
- connection_name = None
- for line in status.split("\n"):
- if line.find(ip_address) > 0:
- regex_match = re.search(r'(peer_[^:\[]+)', line)
- if regex_match:
- connection_name = regex_match[1]
- break
- if connection_name:
- call(f'sudo ipsec down {connection_name}')
+# Best effort wait for any active commit to finish
+sudo python3 - <<PYEND
+from vyos.utils.commit import wait_for_commit_lock
if __name__ == '__main__':
- interface = os.getenv('interface')
- new_ip = os.getenv('new_ip_address')
- old_ip = os.getenv('old_ip_address')
-
- if os.path.exists(SWANCTL_CONF):
- conf_lines = read_file(SWANCTL_CONF)
- found = False
- to_match = f'# dhcp:{interface}'
-
- for i, line in enumerate(conf_lines):
- if line.find(to_match) > 0:
- conf_lines[i] = line.replace(old_ip, new_ip)
- found = True
-
- if found:
- write_file(SWANCTL_CONF, conf_lines)
- ipsec_down(old_ip)
- call('sudo ipsec rereadall')
- call('sudo ipsec reload')
- call('sudo swanctl -q')
-
+ wait_for_commit_lock()
exit(0)
PYEND
+
+# Now re-generate the config
+sudo /usr/libexec/vyos/conf_mode/vpn_ipsec.py
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index d27e1baf7..f6029c748 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright (C) 2022-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
@@ -33,6 +33,7 @@ from vyos.kea import kea_get_leases
from vyos.kea import kea_get_pool_from_subnet_id
from vyos.kea import kea_delete_lease
from vyos.utils.process import is_systemd_service_running
+from vyos.utils.process import call
time_string = "%a %b %d %H:%M:%S %Z %Y"
@@ -79,14 +80,20 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig
:return list
"""
inet_suffix = '6' if family == 'inet6' else '4'
- leases = kea_get_leases(inet_suffix)
+ try:
+ leases = kea_get_leases(inet_suffix)
+ except:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server lease information')
if pool is None:
pool = _get_dhcp_pools(family=family)
else:
pool = [pool]
- active_config = kea_get_active_config(inet_suffix)
+ try:
+ active_config = kea_get_active_config(inet_suffix)
+ except:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
data = []
for lease in leases:
@@ -309,6 +316,25 @@ def _verify(func):
return func(*args, **kwargs)
return _wrapper
+def _verify_client(func):
+ """Decorator checks if interface is configured as DHCP client"""
+ from functools import wraps
+ from vyos.ifconfig import Section
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ family = kwargs.get('family')
+ v = 'v6' if family == 'inet6' else ''
+ interface = kwargs.get('interface')
+ interface_path = Section.get_config_path(interface)
+ unconf_message = f'DHCP{v} client not configured on interface {interface}!'
+
+ # Check if config does not exist
+ if not config.exists(f'interfaces {interface_path} address dhcp{v}'):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+ return _wrapper
@_verify
def show_pool_statistics(raw: bool, family: ArgFamily, pool: typing.Optional[str]):
@@ -474,6 +500,16 @@ def show_client_leases(raw: bool, family: ArgFamily, interface: typing.Optional[
else:
return _get_formatted_client_leases(lease_data, family=family)
+@_verify_client
+def renew_client_lease(raw: bool, family: ArgFamily, interface: str):
+ if not raw:
+ v = 'v6' if family == 'inet6' else ''
+ print(f'Restarting DHCP{v} client on interface {interface}...')
+ if family == 'inet6':
+ call(f'systemctl restart dhcp6c@{interface}.service')
+ else:
+ call(f'systemctl restart dhclient@{interface}.service')
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index 1510a667c..1cfb5f5a1 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -33,27 +33,31 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:'
MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first'
MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first'
-def annotated_list(images_list: list[str]) -> list[str]:
+def annotate_list(images_list: list[str]) -> list[str]:
"""Annotate list of images with additional info
Args:
images_list (list[str]): a list of image names
Returns:
- list[str]: a list of image names with additional info
+ dict[str, str]: a dict of annotations indexed by image name
"""
- index_running: int = None
- index_default: int = None
- try:
- index_running = images_list.index(image.get_running_image())
- index_default = images_list.index(image.get_default_image())
- except ValueError:
- pass
- if index_running is not None:
- images_list[index_running] += ' (running)'
- if index_default is not None:
- images_list[index_default] += ' (default boot)'
- return images_list
+ running = image.get_running_image()
+ default = image.get_default_image()
+ annotated = {}
+ for image_name in images_list:
+ annotated[image_name] = f'{image_name}'
+ if running in images_list:
+ annotated[running] = annotated[running] + ' (running)'
+ if default in images_list:
+ annotated[default] = annotated[default] + ' (default boot)'
+ return annotated
+
+def define_format(images):
+ annotated = annotate_list(images)
+ def format_selection(image_name):
+ return annotated[image_name]
+ return format_selection
@compat.grub_cfg_update
def delete_image(image_name: Optional[str] = None,
@@ -63,14 +67,16 @@ def delete_image(image_name: Optional[str] = None,
Args:
image_name (str): a name of image to delete
"""
- available_images: list[str] = annotated_list(grub.version_list())
+ available_images: list[str] = grub.version_list()
+ format_selection = define_format(available_images)
if image_name is None:
if no_prompt:
exit('An image name is required for delete action')
else:
image_name = select_entry(available_images,
DELETE_IMAGE_LIST_MSG,
- DELETE_IMAGE_PROMPT_MSG)
+ DELETE_IMAGE_PROMPT_MSG,
+ format_selection)
if image_name == image.get_running_image():
exit(MSG_DELETE_IMAGE_RUNNING)
if image_name == image.get_default_image():
@@ -113,14 +119,16 @@ def set_image(image_name: Optional[str] = None,
Args:
image_name (str): an image name
"""
- available_images: list[str] = annotated_list(grub.version_list())
+ available_images: list[str] = grub.version_list()
+ format_selection = define_format(available_images)
if image_name is None:
if not prompt:
exit('An image name is required for set action')
else:
image_name = select_entry(available_images,
SET_IMAGE_LIST_MSG,
- SET_IMAGE_PROMPT_MSG)
+ SET_IMAGE_PROMPT_MSG,
+ format_selection)
if image_name == image.get_default_image():
exit(f'The image "{image_name}" already configured as default')
if image_name not in available_images:
diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py
index fd9d2db92..d54a67199 100755
--- a/src/op_mode/openvpn.py
+++ b/src/op_mode/openvpn.py
@@ -205,11 +205,11 @@ def _format_openvpn(data: list) -> str:
intf = d['intf']
l_host = d['local_host']
l_port = d['local_port']
+ out += f'\nOpenVPN status on {intf}\n\n'
for client in d['clients']:
r_host = client['remote_host']
r_port = client['remote_port']
- out += f'\nOpenVPN status on {intf}\n\n'
name = client['name']
remote = r_host + ':' + r_port if r_host and r_port else 'N/A'
tunnel = client['tunnel']
@@ -220,9 +220,8 @@ def _format_openvpn(data: list) -> str:
data_out.append([name, remote, tunnel, local, tx_bytes,
rx_bytes, online_since])
- if data_out:
- out += tabulate(data_out, headers)
- out += "\n"
+ out += tabulate(data_out, headers)
+ out += "\n"
return out
diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service
index 099f7ed52..d430d8868 100644
--- a/src/systemd/dhclient@.service
+++ b/src/systemd/dhclient@.service
@@ -3,6 +3,7 @@ Description=DHCP client on %i
Documentation=man:dhclient(8)
StartLimitIntervalSec=0
After=vyos-router.service
+ConditionPathExists=/run/dhclient/dhclient_%i.conf
[Service]
Type=exec