summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
7 files changed, 150 insertions, 115 deletions
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