summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/container.py23
-rwxr-xr-xsrc/conf_mode/firewall.py21
-rwxr-xr-xsrc/conf_mode/high-availability.py47
-rwxr-xr-xsrc/conf_mode/nat.py18
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py6
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py8
-rwxr-xr-xsrc/conf_mode/service_router-advert.py18
-rwxr-xr-xsrc/conf_mode/system_conntrack.py31
-rwxr-xr-xsrc/conf_mode/system_login_banner.py22
-rwxr-xr-xsrc/conf_mode/system_option.py2
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py21
-rwxr-xr-xsrc/conf_mode/vrf.py18
-rwxr-xr-xsrc/helpers/vyos-config-encrypt.py276
-rwxr-xr-xsrc/helpers/vyos_config_sync.py26
-rwxr-xr-xsrc/init/vyos-router71
-rwxr-xr-xsrc/migration-scripts/dhcp-server/6-to-769
-rwxr-xr-xsrc/migration-scripts/dhcp-server/7-to-866
-rwxr-xr-xsrc/migration-scripts/dhcp-server/8-to-942
-rwxr-xr-xsrc/migration-scripts/dhcp-server/9-to-1075
-rwxr-xr-xsrc/migration-scripts/dhcpv6-server/4-to-57
-rwxr-xr-xsrc/migration-scripts/firewall/6-to-734
-rwxr-xr-xsrc/migration-scripts/policy/1-to-218
-rwxr-xr-xsrc/op_mode/clear_dhcp_lease.py88
-rwxr-xr-xsrc/op_mode/conntrack.py3
-rwxr-xr-xsrc/op_mode/container.py42
-rwxr-xr-xsrc/op_mode/dhcp.py23
-rwxr-xr-xsrc/op_mode/image_installer.py83
-rwxr-xr-xsrc/op_mode/image_manager.py21
-rwxr-xr-xsrc/services/vyos-configd19
-rwxr-xr-xsrc/services/vyos-http-api-server3
-rw-r--r--src/shim/vyshim.c12
-rw-r--r--src/system/grub_update.py4
-rwxr-xr-xsrc/system/vyos-event-handler.py7
33 files changed, 918 insertions, 306 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 321d00abf..e967bee71 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# Copyright (C) 2021-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -32,7 +32,6 @@ from vyos.utils.file import write_file
from vyos.utils.process import call
from vyos.utils.process import cmd
from vyos.utils.process import run
-from vyos.utils.process import rc_cmd
from vyos.template import bracketize_ipv6
from vyos.template import inc_ip
from vyos.template import is_ipv4
@@ -251,7 +250,7 @@ def verify(container):
if 'authentication' not in registry_config:
continue
if not {'username', 'password'} <= set(registry_config['authentication']):
- raise ConfigError('If registry username or or password is defined, so must be the other!')
+ raise ConfigError('Container registry requires both username and password to be set!')
return None
@@ -401,24 +400,6 @@ def generate(container):
write_file(f'/etc/containers/networks/{network}.json', json_write(tmp, indent=2))
- if 'registry' in container:
- cmd = f'podman logout --all'
- rc, out = rc_cmd(cmd)
- if rc != 0:
- raise ConfigError(out)
-
- for registry, registry_config in container['registry'].items():
- if 'disable' in registry_config:
- continue
- if 'authentication' in registry_config:
- if {'username', 'password'} <= set(registry_config['authentication']):
- username = registry_config['authentication']['username']
- password = registry_config['authentication']['password']
- cmd = f'podman login --username {username} --password {password} {registry}'
- rc, out = rc_cmd(cmd)
- if rc != 0:
- raise ConfigError(out)
-
render(config_containers, 'container/containers.conf.j2', container)
render(config_registry, 'container/registries.conf.j2', container)
render(config_storage, 'container/storage.conf.j2', container)
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index acb7dfa41..810437dda 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -268,6 +268,18 @@ def verify_rule(firewall, rule_conf, ipv6):
if 'port' in side_conf and dict_search_args(side_conf, 'group', 'port_group'):
raise ConfigError(f'{side} port-group and port cannot both be defined')
+ if 'add_address_to_group' in rule_conf:
+ for type in ['destination_address', 'source_address']:
+ if type in rule_conf['add_address_to_group']:
+ if 'address_group' not in rule_conf['add_address_to_group'][type]:
+ raise ConfigError(f'Dynamic address group must be defined.')
+ else:
+ target = rule_conf['add_address_to_group'][type]['address_group']
+ fwall_group = 'ipv6_address_group' if ipv6 else 'address_group'
+ group_obj = dict_search_args(firewall, 'group', 'dynamic_group', fwall_group, target)
+ if group_obj is None:
+ raise ConfigError(f'Invalid dynamic address group on firewall rule')
+
if 'log_options' in rule_conf:
if 'log' not in rule_conf:
raise ConfigError('log-options defined, but log is not enable')
@@ -282,6 +294,15 @@ def verify_rule(firewall, rule_conf, ipv6):
if direction in rule_conf:
if 'name' in rule_conf[direction] and 'group' in rule_conf[direction]:
raise ConfigError(f'Cannot specify both interface group and interface name for {direction}')
+ if 'group' in rule_conf[direction]:
+ group_name = rule_conf[direction]['group']
+ if group_name[0] == '!':
+ group_name = group_name[1:]
+ group_obj = dict_search_args(firewall, 'group', 'interface_group', group_name)
+ if group_obj is None:
+ raise ConfigError(f'Invalid interface group "{group_name}" on firewall rule')
+ if not group_obj:
+ Warning(f'interface-group "{group_name}" has no members!')
def verify_nested_group(group_name, group, groups, seen):
if 'include' not in group:
diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py
index b3b27b14e..c726db8b2 100755
--- a/src/conf_mode/high-availability.py
+++ b/src/conf_mode/high-availability.py
@@ -86,16 +86,7 @@ def verify(ha):
raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"')
if 'health_check' in group_config:
- health_check_types = ["script", "ping"]
- from vyos.utils.dict import check_mutually_exclusive_options
- try:
- check_mutually_exclusive_options(group_config["health_check"], health_check_types, required=True)
- except ValueError:
- Warning(f'Health check configuration for VRRP group "{group}" will remain unused ' \
- f'until it has one of the following options: {health_check_types}')
- # XXX: health check has default options so we need to remove it
- # to avoid generating useless config statements in keepalived.conf
- del group_config["health_check"]
+ _validate_health_check(group, group_config)
# Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
# We also need to make sure VRID is not used twice on the same interface with the
@@ -125,8 +116,9 @@ def verify(ha):
raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!')
if 'peer_address' in group_config:
- if is_ipv6(group_config['peer_address']):
- raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!')
+ for peer_address in group_config['peer_address']:
+ if is_ipv6(peer_address):
+ raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!')
if vaddrs6:
tmp = {'interface': interface, 'vrid': vrid, 'ipver': 'IPv6'}
@@ -139,16 +131,28 @@ def verify(ha):
raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!')
if 'peer_address' in group_config:
- if is_ipv4(group_config['peer_address']):
- raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!')
+ for peer_address in group_config['peer_address']:
+ if is_ipv4(peer_address):
+ raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!')
# Check sync groups
if 'vrrp' in ha and 'sync_group' in ha['vrrp']:
for sync_group, sync_config in ha['vrrp']['sync_group'].items():
+ if 'health_check' in sync_config:
+ _validate_health_check(sync_group, sync_config)
+
if 'member' in sync_config:
for member in sync_config['member']:
if member not in ha['vrrp']['group']:
raise ConfigError(f'VRRP sync-group "{sync_group}" refers to VRRP group "{member}", '\
'but it does not exist!')
+ else:
+ ha['vrrp']['group'][member]['_is_sync_group_member'] = True
+ if ha['vrrp']['group'][member].get('health_check') is not None:
+ raise ConfigError(
+ f'Health check configuration for VRRP group "{member}" will remain unused '
+ f'while it has member of sync group "{sync_group}" '
+ f'Only sync group health check will be used'
+ )
# Virtual-server
if 'virtual_server' in ha:
@@ -170,6 +174,21 @@ def verify(ha):
raise ConfigError(f'Port is required but not set for virtual-server "{vs}" real-server "{rs}"')
+def _validate_health_check(group, group_config):
+ health_check_types = ["script", "ping"]
+ from vyos.utils.dict import check_mutually_exclusive_options
+ try:
+ check_mutually_exclusive_options(group_config["health_check"],
+ health_check_types, required=True)
+ except ValueError:
+ Warning(
+ f'Health check configuration for VRRP group "{group}" will remain unused ' \
+ f'until it has one of the following options: {health_check_types}')
+ # XXX: health check has default options so we need to remove it
+ # to avoid generating useless config statements in keepalived.conf
+ del group_config["health_check"]
+
+
def generate(ha):
if not ha or 'disable' in ha:
if os.path.isfile(systemd_override):
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 26822b755..b3f38c04a 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -153,6 +153,15 @@ def verify(nat):
elif 'name' in config['outbound_interface']:
if config['outbound_interface']['name'] not in 'any' and config['outbound_interface']['name'] not in interfaces():
Warning(f'NAT interface "{config["outbound_interface"]["name"]}" for source NAT rule "{rule}" does not exist!')
+ else:
+ group_name = config['outbound_interface']['group']
+ if group_name[0] == '!':
+ group_name = group_name[1:]
+ group_obj = dict_search_args(nat['firewall_group'], 'interface_group', group_name)
+ if group_obj is None:
+ raise ConfigError(f'Invalid interface group "{group_name}" on source nat rule')
+ if not group_obj:
+ Warning(f'interface-group "{group_name}" has no members!')
if not dict_search('translation.address', config) and not dict_search('translation.port', config):
if 'exclude' not in config and 'backend' not in config['load_balance']:
@@ -177,6 +186,15 @@ def verify(nat):
elif 'name' in config['inbound_interface']:
if config['inbound_interface']['name'] not in 'any' and config['inbound_interface']['name'] not in interfaces():
Warning(f'NAT interface "{config["inbound_interface"]["name"]}" for destination NAT rule "{rule}" does not exist!')
+ else:
+ group_name = config['inbound_interface']['group']
+ if group_name[0] == '!':
+ group_name = group_name[1:]
+ group_obj = dict_search_args(nat['firewall_group'], 'interface_group', group_name)
+ if group_obj is None:
+ raise ConfigError(f'Invalid interface group "{group_name}" on destination nat rule')
+ if not group_obj:
+ Warning(f'interface-group "{group_name}" has no members!')
if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']:
if 'exclude' not in config and 'backend' not in config['load_balance']:
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 34cf49286..695842795 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# Copyright (C) 2021-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -91,6 +91,8 @@ def get_config(config=None):
for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']:
if dict_search(f'redistribute.{protocol}', ospf) is None:
del default_values['redistribute'][protocol]
+ if not bool(default_values['redistribute']):
+ del default_values['redistribute']
for interface in ospf.get('interface', []):
# We need to reload the defaults on every pass b/c of
@@ -213,7 +215,7 @@ def verify(ospf):
raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\
f'and no-php-flag configured at the same time.')
- # Check for index ranges being larger than the segment routing global block
+ # Check for index ranges being larger than the segment routing global block
if dict_search('segment_routing.global_block', ospf):
g_high_label_value = dict_search('segment_routing.global_block.high_label_value', ospf)
g_low_label_value = dict_search('segment_routing.global_block.low_label_value', ospf)
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index 5b1adce30..afd767dbf 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# Copyright (C) 2021-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -85,6 +85,12 @@ def get_config(config=None):
if 'graceful_restart' not in ospfv3:
del default_values['graceful_restart']
+ for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'ripng', 'static']:
+ if dict_search(f'redistribute.{protocol}', ospfv3) is None:
+ del default_values['redistribute'][protocol]
+ if not bool(default_values['redistribute']):
+ del default_values['redistribute']
+
default_values.pop('interface', {})
# merge in remaining default values
diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py
index dbb47de4e..88d767bb8 100755
--- a/src/conf_mode/service_router-advert.py
+++ b/src/conf_mode/service_router-advert.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2022 VyOS maintainers and contributors
+# Copyright (C) 2018-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -17,6 +17,8 @@
import os
from sys import exit
+from ipaddress import IPv6Network
+
from vyos.base import Warning
from vyos.config import Config
from vyos.template import render
@@ -47,7 +49,9 @@ def verify(rtradv):
return None
for interface, interface_config in rtradv['interface'].items():
- if 'prefix' in interface:
+ interval_max = int(interface_config['interval']['max'])
+
+ if 'prefix' in interface_config:
for prefix, prefix_config in interface_config['prefix'].items():
valid_lifetime = prefix_config['valid_lifetime']
if valid_lifetime == 'infinity':
@@ -60,6 +64,15 @@ def verify(rtradv):
if not (int(valid_lifetime) >= int(preferred_lifetime)):
raise ConfigError('Prefix valid-lifetime must be greater then or equal to preferred-lifetime')
+ if 'nat64prefix' in interface_config:
+ nat64_supported_lengths = [32, 40, 48, 56, 64, 96]
+ for prefix, prefix_config in interface_config['nat64prefix'].items():
+ if IPv6Network(prefix).prefixlen not in nat64_supported_lengths:
+ raise ConfigError(f'Invalid NAT64 prefix length for "{prefix}", can only be one of: /' + ', /'.join(nat64_supported_lengths))
+
+ if int(prefix_config['valid_lifetime']) < interval_max:
+ raise ConfigError(f'NAT64 valid-lifetime must not be smaller then "interval max" which is "{interval_max}"!')
+
if 'name_server' in interface_config:
if len(interface_config['name_server']) > 3:
raise ConfigError('No more then 3 IPv6 name-servers supported!')
@@ -72,7 +85,6 @@ def verify(rtradv):
# ensure stale RDNSS info gets removed in a timely fashion, this
# should not be greater than 2*MaxRtrAdvInterval.
lifetime = int(interface_config['name_server_lifetime'])
- interval_max = int(interface_config['interval']['max'])
if lifetime > 0:
if lifetime < int(interval_max):
raise ConfigError(f'RDNSS lifetime must be at least "{interval_max}" seconds!')
diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py
index 7f6c71440..a1472aaaa 100755
--- a/src/conf_mode/system_conntrack.py
+++ b/src/conf_mode/system_conntrack.py
@@ -42,33 +42,38 @@ nftables_ct_file = r'/run/nftables-ct.conf'
module_map = {
'ftp': {
'ko': ['nf_nat_ftp', 'nf_conntrack_ftp'],
- 'nftables': ['ct helper set "ftp_tcp" tcp dport {21} return']
+ 'nftables': ['tcp dport {21} ct helper set "ftp_tcp" return']
},
'h323': {
'ko': ['nf_nat_h323', 'nf_conntrack_h323'],
- 'nftables': ['ct helper set "ras_udp" udp dport {1719} return',
- 'ct helper set "q931_tcp" tcp dport {1720} return']
+ 'nftables': ['udp dport {1719} ct helper set "ras_udp" return',
+ 'tcp dport {1720} ct helper set "q931_tcp" return']
},
'nfs': {
- 'nftables': ['ct helper set "rpc_tcp" tcp dport {111} return',
- 'ct helper set "rpc_udp" udp dport {111} return']
+ 'nftables': ['tcp dport {111} ct helper set "rpc_tcp" return',
+ 'udp dport {111} ct helper set "rpc_udp" return']
},
'pptp': {
'ko': ['nf_nat_pptp', 'nf_conntrack_pptp'],
- 'nftables': ['ct helper set "pptp_tcp" tcp dport {1723} return'],
+ 'nftables': ['tcp dport {1723} ct helper set "pptp_tcp" return'],
'ipv4': True
},
+ 'rtsp': {
+ 'ko': ['nf_nat_rtsp', 'nf_conntrack_rtsp'],
+ 'nftables': ['tcp dport {554} ct helper set "rtsp_tcp" return'],
+ 'ipv4': True
+ },
'sip': {
'ko': ['nf_nat_sip', 'nf_conntrack_sip'],
- 'nftables': ['ct helper set "sip_tcp" tcp dport {5060,5061} return',
- 'ct helper set "sip_udp" udp dport {5060,5061} return']
+ 'nftables': ['tcp dport {5060,5061} ct helper set "sip_tcp" return',
+ 'udp dport {5060,5061} ct helper set "sip_udp" return']
},
'sqlnet': {
- 'nftables': ['ct helper set "tns_tcp" tcp dport {1521,1525,1536} return']
+ 'nftables': ['tcp dport {1521,1525,1536} ct helper set "tns_tcp" return']
},
'tftp': {
'ko': ['nf_nat_tftp', 'nf_conntrack_tftp'],
- 'nftables': ['ct helper set "tftp_udp" udp dport {69} return']
+ 'nftables': ['udp dport {69} ct helper set "tftp_udp" return']
},
}
@@ -104,6 +109,10 @@ def get_config(config=None):
if conf.exists(['service', 'conntrack-sync']):
set_dependents('conntrack_sync', conf)
+ # If conntrack status changes, VRF zone rules need updating
+ if conf.exists(['vrf']):
+ set_dependents('vrf', conf)
+
return conntrack
def verify(conntrack):
@@ -191,7 +200,7 @@ def generate(conntrack):
def apply(conntrack):
# Depending on the enable/disable state of the ALG (Application Layer Gateway)
# modules we need to either insmod or rmmod the helpers.
-
+
add_modules = []
rm_modules = []
diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py
index 65fa04417..923e1bf57 100755
--- a/src/conf_mode/system_login_banner.py
+++ b/src/conf_mode/system_login_banner.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -18,30 +18,26 @@ from sys import exit
from copy import deepcopy
from vyos.config import Config
+from vyos.template import render
from vyos.utils.file import write_file
+from vyos.version import get_version_data
from vyos import ConfigError
from vyos import airbag
airbag.enable()
-try:
- with open('/usr/share/vyos/default_motd') as f:
- motd = f.read()
-except:
- # Use an empty banner if the default banner file cannot be read
- motd = "\n"
-
PRELOGIN_FILE = r'/etc/issue'
PRELOGIN_NET_FILE = r'/etc/issue.net'
POSTLOGIN_FILE = r'/etc/motd'
default_config_data = {
'issue': 'Welcome to VyOS - \\n \\l\n\n',
- 'issue_net': '',
- 'motd': motd
+ 'issue_net': ''
}
def get_config(config=None):
banner = deepcopy(default_config_data)
+ banner['version_data'] = get_version_data()
+
if config:
conf = config
else:
@@ -92,7 +88,11 @@ def generate(banner):
def apply(banner):
write_file(PRELOGIN_FILE, banner['issue'])
write_file(PRELOGIN_NET_FILE, banner['issue_net'])
- write_file(POSTLOGIN_FILE, banner['motd'])
+ if 'motd' in banner:
+ write_file(POSTLOGIN_FILE, banner['motd'])
+ else:
+ render(POSTLOGIN_FILE, 'login/default_motd.j2', banner,
+ permission=0o644, user='root', group='root')
return None
diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py
index 3b5b67437..7ed451e16 100755
--- a/src/conf_mode/system_option.py
+++ b/src/conf_mode/system_option.py
@@ -92,6 +92,8 @@ def generate(options):
if 'kernel' in options:
if 'disable_mitigations' in options['kernel']:
cmdline_options.append('mitigations=off')
+ if 'disable_power_saving' in options['kernel']:
+ cmdline_options.append('intel_idle.max_cstate=0 processor.max_cstate=1')
grub_util.update_kernel_cmdline_options(' '.join(cmdline_options))
return None
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index d074ed159..388f2a709 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -25,6 +25,8 @@ from time import time
from vyos.base import Warning
from vyos.config import Config
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_interface_exists
from vyos.configverify import dynamic_interface_pattern
@@ -97,6 +99,9 @@ def get_config(config=None):
ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
+ if ipsec['nhrp_exists']:
+ set_dependents('nhrp', conf)
+
tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'),
no_tag_node_value_mangle=True,
get_first_key=True)
@@ -575,13 +580,6 @@ def generate(ipsec):
render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec)
render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec)
-def resync_nhrp(ipsec):
- if ipsec and not ipsec['nhrp_exists']:
- return
-
- tmp = run('/usr/libexec/vyos/conf_mode/protocols_nhrp.py')
- if tmp > 0:
- print('ERROR: failed to reapply NHRP settings!')
def apply(ipsec):
systemd_service = 'strongswan.service'
@@ -590,7 +588,14 @@ def apply(ipsec):
else:
call(f'systemctl reload-or-restart {systemd_service}')
- resync_nhrp(ipsec)
+ if ipsec.get('nhrp_exists', False):
+ try:
+ call_dependents()
+ except ConfigError:
+ # Ignore config errors on dependent due to being called too early. Example:
+ # ConfigError("ConfigError('Interface ethN requires an IP address!')")
+ pass
+
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index a2f4956be..16908100f 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -23,6 +23,7 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configverify import verify_route_map
+from vyos.firewall import conntrack_required
from vyos.ifconfig import Interface
from vyos.template import render
from vyos.template import render_to_string
@@ -41,6 +42,12 @@ airbag.enable()
config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf'
k_mod = ['vrf']
+nftables_table = 'inet vrf_zones'
+nftables_rules = {
+ 'vrf_zones_ct_in': 'counter ct original zone set iifname map @ct_iface_map',
+ 'vrf_zones_ct_out': 'counter ct original zone set oifname map @ct_iface_map'
+}
+
def has_rule(af : str, priority : int, table : str=None):
"""
Check if a given ip rule exists
@@ -114,6 +121,9 @@ def get_config(config=None):
routes = vrf_routing(conf, name)
if routes: vrf['vrf_remove'][name]['route'] = routes
+ if 'name' in vrf:
+ vrf['conntrack'] = conntrack_required(conf)
+
# We also need the route-map information from the config
#
# XXX: one MUST always call this without the key_mangling() option! See
@@ -294,6 +304,14 @@ def apply(vrf):
nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
cmd(f'nft {nft_add_element}')
+ if vrf['conntrack']:
+ for chain, rule in nftables_rules.items():
+ cmd(f'nft add rule inet vrf_zones {chain} {rule}')
+
+ if 'name' not in vrf or not vrf['conntrack']:
+ for chain, rule in nftables_rules.items():
+ cmd(f'nft flush chain inet vrf_zones {chain}')
+
# Apply FRR filters
zebra_daemon = 'zebra'
# Save original configuration prior to starting any commit actions
diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py
new file mode 100755
index 000000000..8f7359767
--- /dev/null
+++ b/src/helpers/vyos-config-encrypt.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import shutil
+import sys
+
+from argparse import ArgumentParser
+from cryptography.fernet import Fernet
+from tempfile import NamedTemporaryFile
+from tempfile import TemporaryDirectory
+
+from vyos.tpm import clear_tpm_key
+from vyos.tpm import init_tpm
+from vyos.tpm import read_tpm_key
+from vyos.tpm import write_tpm_key
+from vyos.util import ask_input
+from vyos.util import ask_yes_no
+from vyos.util import cmd
+
+persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath'
+mount_paths = ['/config', '/opt/vyatta/etc/config']
+dm_device = '/dev/mapper/vyos_config'
+
+def is_opened():
+ return os.path.exists(dm_device)
+
+def get_current_image():
+ with open('/proc/cmdline', 'r') as f:
+ args = f.read().split(" ")
+ for arg in args:
+ if 'vyos-union' in arg:
+ k, v = arg.split("=")
+ path_split = v.split("/")
+ return path_split[-1]
+ return None
+
+def load_config(key):
+ if not key:
+ return
+
+ persist_path = cmd(persistpath_cmd).strip()
+ image_name = get_current_image()
+ image_path = os.path.join(persist_path, 'luks', image_name)
+
+ if not os.path.exists(image_path):
+ raise Exception("Encrypted config volume doesn't exist")
+
+ if is_opened():
+ print('Encrypted config volume is already mounted')
+ return
+
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+
+ for path in mount_paths:
+ cmd(f'mount /dev/mapper/vyos_config {path}')
+ cmd(f'chgrp -R vyattacfg {path}')
+
+ os.unlink(key_file)
+
+ return True
+
+def encrypt_config(key, recovery_key):
+ if is_opened():
+ raise Exception('An encrypted config volume is already mapped')
+
+ # Clear and write key to TPM
+ try:
+ clear_tpm_key()
+ except:
+ pass
+ write_tpm_key(key)
+
+ persist_path = cmd(persistpath_cmd).strip()
+ size = ask_input('Enter size of encrypted config partition (MB): ', numeric_only=True, default=512)
+
+ luks_folder = os.path.join(persist_path, 'luks')
+
+ if not os.path.isdir(luks_folder):
+ os.mkdir(luks_folder)
+
+ image_name = get_current_image()
+ image_path = os.path.join(luks_folder, image_name)
+
+ # Create file for encrypted config
+ cmd(f'fallocate -l {size}M {image_path}')
+
+ # Write TPM key for slot #1
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ # Format and add main key to volume
+ cmd(f'cryptsetup -q luksFormat {image_path} {key_file}')
+
+ if recovery_key:
+ # Write recovery key for slot 2
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(recovery_key)
+ recovery_key_file = f.name
+
+ cmd(f'cryptsetup -q luksAddKey {image_path} {recovery_key_file} --key-file={key_file}')
+
+ # Open encrypted volume and format with ext4
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+ cmd('mkfs.ext4 /dev/mapper/vyos_config')
+
+ with TemporaryDirectory() as d:
+ cmd(f'mount /dev/mapper/vyos_config {d}')
+
+ # Move /config to encrypted volume
+ shutil.copytree('/config', d, copy_function=shutil.move, dirs_exist_ok=True)
+
+ cmd(f'umount {d}')
+
+ os.unlink(key_file)
+
+ if recovery_key:
+ os.unlink(recovery_key_file)
+
+ for path in mount_paths:
+ cmd(f'mount /dev/mapper/vyos_config {path}')
+ cmd(f'chgrp vyattacfg {path}')
+
+ return True
+
+def decrypt_config(key):
+ if not key:
+ return
+
+ persist_path = cmd(persistpath_cmd).strip()
+ image_name = get_current_image()
+ image_path = os.path.join(persist_path, 'luks', image_name)
+
+ if not os.path.exists(image_path):
+ raise Exception("Encrypted config volume doesn't exist")
+
+ key_file = None
+
+ if not is_opened():
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+
+ # unmount encrypted volume mount points
+ for path in mount_paths:
+ if os.path.ismount(path):
+ cmd(f'umount {path}')
+
+ # If /config is populated, move to /config.old
+ if len(os.listdir('/config')) > 0:
+ print('Moving existing /config folder to /config.old')
+ shutil.move('/config', '/config.old')
+
+ # Temporarily mount encrypted volume and migrate files to /config on rootfs
+ with TemporaryDirectory() as d:
+ cmd(f'mount /dev/mapper/vyos_config {d}')
+
+ # Move encrypted volume to /config
+ shutil.copytree(d, '/config', copy_function=shutil.move, dirs_exist_ok=True)
+ cmd(f'chgrp -R vyattacfg /config')
+
+ cmd(f'umount {d}')
+
+ # Close encrypted volume
+ cmd('cryptsetup -q close vyos_config')
+
+ # Remove encrypted volume image file and key
+ if key_file:
+ os.unlink(key_file)
+ os.unlink(image_path)
+
+ try:
+ clear_tpm_key()
+ except:
+ pass
+
+ return True
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Must specify action.")
+ sys.exit(1)
+
+ parser = ArgumentParser(description='Config encryption')
+ parser.add_argument('--disable', help='Disable encryption', action="store_true")
+ parser.add_argument('--enable', help='Enable encryption', action="store_true")
+ parser.add_argument('--load', help='Load encrypted config volume', action="store_true")
+ args = parser.parse_args()
+
+ tpm_exists = os.path.exists('/sys/class/tpm/tpm0')
+
+ key = None
+ recovery_key = None
+ need_recovery = False
+
+ question_key_str = 'recovery key' if tpm_exists else 'key'
+
+ if tpm_exists:
+ if args.enable:
+ key = Fernet.generate_key()
+ elif args.disable or args.load:
+ try:
+ key = read_tpm_key()
+ need_recovery = False
+ except:
+ print('Failed to read key from TPM, recovery key required')
+ need_recovery = True
+ else:
+ need_recovery = True
+
+ if args.enable and not tpm_exists:
+ print('WARNING: VyOS will boot into a default config when encrypted without a TPM')
+ print('You will need to manually login with default credentials and use "encryption load"')
+ print('to mount the encrypted volume and use "load /config/config.boot"')
+
+ if not ask_yes_no('Are you sure you want to proceed?'):
+ sys.exit(0)
+
+ if need_recovery or (args.enable and not ask_yes_no(f'Automatically generate a {question_key_str}?', default=True)):
+ while True:
+ recovery_key = ask_input(f'Enter {question_key_str}:', default=None).encode()
+
+ if len(recovery_key) >= 32:
+ break
+
+ print('Invalid key - must be at least 32 characters, try again.')
+ else:
+ recovery_key = Fernet.generate_key()
+
+ try:
+ if args.disable:
+ decrypt_config(key or recovery_key)
+
+ print('Encrypted config volume has been disabled')
+ print('Contents have been migrated to /config on rootfs')
+ elif args.load:
+ load_config(key or recovery_key)
+
+ print('Encrypted config volume has been mounted')
+ print('Use "load /config/config.boot" to load configuration')
+ elif args.enable and tpm_exists:
+ encrypt_config(key, recovery_key)
+
+ print('Encrypted config volume has been enabled with TPM')
+ print('Backup the recovery key in a safe place!')
+ print('Recovery key: ' + recovery_key.decode())
+ elif args.enable:
+ encrypt_config(recovery_key)
+
+ print('Encrypted config volume has been enabled without TPM')
+ print('Backup the key in a safe place!')
+ print('Key: ' + recovery_key.decode())
+ except Exception as e:
+ word = 'decrypt' if args.disable or args.load else 'encrypt'
+ print(f'Failed to {word} config: {e}')
diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py
index 7cfa8fe88..572fea61f 100755
--- a/src/helpers/vyos_config_sync.py
+++ b/src/helpers/vyos_config_sync.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright (C) 2023-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -60,6 +60,7 @@ def post_request(url: str,
return response
+
def retrieve_config(section: str = None) -> Optional[Dict[str, Any]]:
"""Retrieves the configuration from the local server.
@@ -71,8 +72,6 @@ def retrieve_config(section: str = None) -> Optional[Dict[str, Any]]:
"""
if section is None:
section = []
- else:
- section = section.split()
conf = Config()
config = conf.get_config_dict(section, get_first_key=True)
@@ -101,8 +100,6 @@ def set_remote_config(
if path is None:
path = []
- else:
- path = path.split()
headers = {'Content-Type': 'application/json'}
# Disable the InsecureRequestWarning
@@ -127,17 +124,16 @@ def set_remote_config(
def is_section_revised(section: str) -> bool:
from vyos.config_mgmt import is_node_revised
- return is_node_revised([section])
+ return is_node_revised(section)
def config_sync(secondary_address: str,
secondary_key: str,
- sections: List[str],
+ sections: List[list],
mode: str):
"""Retrieve a config section from primary router in JSON format and send it to
secondary router
"""
- # Config sync only if sections changed
if not any(map(is_section_revised, sections)):
return
@@ -188,5 +184,17 @@ if __name__ == '__main__':
"Missing required configuration data for config synchronization.")
exit(0)
+ # Generate list_sections of sections/subsections
+ # [
+ # ['interfaces', 'pseudo-ethernet'], ['interfaces', 'virtual-ethernet'], ['nat'], ['nat66']
+ # ]
+ list_sections = []
+ for section, subsections in sections.items():
+ if subsections:
+ for subsection in subsections:
+ list_sections.append([section, subsection])
+ else:
+ list_sections.append([section])
+
config_sync(secondary_address, secondary_key,
- sections, mode)
+ list_sections, mode)
diff --git a/src/init/vyos-router b/src/init/vyos-router
index eac3e7e47..adf892371 100755
--- a/src/init/vyos-router
+++ b/src/init/vyos-router
@@ -64,6 +64,69 @@ disabled () {
grep -q -w no-vyos-$1 /proc/cmdline
}
+# Load encrypted config volume
+mount_encrypted_config() {
+ persist_path=$(/opt/vyatta/sbin/vyos-persistpath)
+ if [ $? == 0 ]; then
+ if [ -e $persist_path/boot ]; then
+ image_name=$(cat /proc/cmdline | sed -e s+^.*vyos-union=/boot/++ | sed -e 's/ .*$//')
+
+ if [ -z "$image_name" ]; then
+ return
+ fi
+
+ if [ ! -f $persist_path/luks/$image_name ]; then
+ return
+ fi
+
+ vyos_tpm_key=$(python3 -c 'from vyos.tpm import read_tpm_key; print(read_tpm_key().decode())' 2>/dev/null)
+
+ if [ $? -ne 0 ]; then
+ echo "ERROR: Failed to fetch encryption key from TPM. Encrypted config volume has not been mounted"
+ echo "Use 'encryption load' to load volume with recovery key"
+ echo "or 'encryption disable' to decrypt volume with recovery key"
+ return
+ fi
+
+ echo $vyos_tpm_key | tr -d '\r\n' | cryptsetup open $persist_path/luks/$image_name vyos_config --key-file=-
+
+ if [ $? -ne 0 ]; then
+ echo "ERROR: Failed to decrypt config volume. Encrypted config volume has not been mounted"
+ echo "Use 'encryption load' to load volume with recovery key"
+ echo "or 'encryption disable' to decrypt volume with recovery key"
+ return
+ fi
+
+ mount /dev/mapper/vyos_config /config
+ mount /dev/mapper/vyos_config $vyatta_sysconfdir/config
+
+ echo "Mounted encrypted config volume"
+ fi
+ fi
+}
+
+unmount_encrypted_config() {
+ persist_path=$(/opt/vyatta/sbin/vyos-persistpath)
+ if [ $? == 0 ]; then
+ if [ -e $persist_path/boot ]; then
+ image_name=$(cat /proc/cmdline | sed -e s+^.*vyos-union=/boot/++ | sed -e 's/ .*$//')
+
+ if [ -z "$image_name" ]; then
+ return
+ fi
+
+ if [ ! -f $persist_path/luks/$image_name ]; then
+ return
+ fi
+
+ umount /config
+ umount $vyatta_sysconfdir/config
+
+ cryptsetup close vyos_config
+ fi
+ fi
+}
+
# if necessary, provide initial config
init_bootfile () {
if [ ! -r $BOOTFILE ] ; then
@@ -218,8 +281,8 @@ cleanup_post_commit_hooks () {
# note that this approach only supports hooks that are "configured",
# i.e., it does not support hooks that need to always be present.
cpostdir=$(cli-shell-api getPostCommitHookDir)
- # exclude commits hooks from vyatta-cfg
- excluded="10vyatta-log-commit.pl 99vyos-user-postcommit-hooks"
+ # exclude commit hooks that need to always be present
+ excluded="00vyos-sync 10vyatta-log-commit.pl 99vyos-user-postcommit-hooks"
if [ -d "$cpostdir" ]; then
for f in $cpostdir/*; do
if [[ ! $excluded =~ $(basename $f) ]]; then
@@ -402,6 +465,8 @@ start ()
&& chgrp ${GROUP} ${vyatta_configdir}
log_action_end_msg $?
+ mount_encrypted_config
+
# T5239: early read of system hostname as this value is read-only once during
# FRR initialisation
tmp=$(${vyos_libexec_dir}/read-saved-value.py --path "system host-name")
@@ -470,6 +535,8 @@ stop()
log_action_end_msg $?
systemctl stop frr.service
+
+ unmount_encrypted_config
}
case "$action" in
diff --git a/src/migration-scripts/dhcp-server/6-to-7 b/src/migration-scripts/dhcp-server/6-to-7
index ccf385a30..e6c298a60 100755
--- a/src/migration-scripts/dhcp-server/6-to-7
+++ b/src/migration-scripts/dhcp-server/6-to-7
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright (C) 2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,19 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# T3316: Migrate to Kea
-# - global-parameters will not function
-# - shared-network-parameters will not function
-# - subnet-parameters will not function
-# - static-mapping-parameters will not function
-# - host-decl-name is on by default, option removed
-# - ping-check no longer supported
-# - failover is default enabled on all subnets that exist on failover servers
+# T6079: Disable duplicate static mappings
import sys
from vyos.configtree import ConfigTree
-if (len(sys.argv) < 2):
+if len(sys.argv) < 2:
print("Must specify file name!")
sys.exit(1)
@@ -38,46 +31,42 @@ with open(file_name, 'r') as f:
base = ['service', 'dhcp-server']
config = ConfigTree(config_file)
-if not config.exists(base):
+if not config.exists(base + ['shared-network-name']):
# Nothing to do
- sys.exit(0)
+ exit(0)
-if config.exists(base + ['host-decl-name']):
- config.delete(base + ['host-decl-name'])
+# Run this for every instance if 'shared-network-name'
+for network in config.list_nodes(base + ['shared-network-name']):
+ base_network = base + ['shared-network-name', network]
-if config.exists(base + ['global-parameters']):
- config.delete(base + ['global-parameters'])
+ if not config.exists(base_network + ['subnet']):
+ continue
-if config.exists(base + ['shared-network-name']):
- for network in config.list_nodes(base + ['shared-network-name']):
- base_network = base + ['shared-network-name', network]
+ for subnet in config.list_nodes(base_network + ['subnet']):
+ base_subnet = base_network + ['subnet', subnet]
- if config.exists(base_network + ['ping-check']):
- config.delete(base_network + ['ping-check'])
+ if config.exists(base_subnet + ['static-mapping']):
+ used_mac = []
+ used_ip = []
- if config.exists(base_network + ['shared-network-parameters']):
- config.delete(base_network +['shared-network-parameters'])
+ for mapping in config.list_nodes(base_subnet + ['static-mapping']):
+ base_mapping = base_subnet + ['static-mapping', mapping]
- if not config.exists(base_network + ['subnet']):
- continue
+ if config.exists(base_mapping + ['mac-address']):
+ mac = config.return_value(base_mapping + ['mac-address'])
- # Run this for every specified 'subnet'
- for subnet in config.list_nodes(base_network + ['subnet']):
- base_subnet = base_network + ['subnet', subnet]
+ if mac in used_mac:
+ config.set(base_mapping + ['disable'])
+ else:
+ used_mac.append(mac)
- if config.exists(base_subnet + ['enable-failover']):
- config.delete(base_subnet + ['enable-failover'])
+ if config.exists(base_mapping + ['ip-address']):
+ ip = config.return_value(base_mapping + ['ip-address'])
- if config.exists(base_subnet + ['ping-check']):
- config.delete(base_subnet + ['ping-check'])
-
- if config.exists(base_subnet + ['subnet-parameters']):
- config.delete(base_subnet + ['subnet-parameters'])
-
- if config.exists(base_subnet + ['static-mapping']):
- for mapping in config.list_nodes(base_subnet + ['static-mapping']):
- if config.exists(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']):
- config.delete(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters'])
+ if ip in used_ip:
+ config.set(base_subnet + ['static-mapping', mapping, 'disable'])
+ else:
+ used_ip.append(ip)
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/dhcp-server/7-to-8 b/src/migration-scripts/dhcp-server/7-to-8
index 151aa6d7b..ccf385a30 100755
--- a/src/migration-scripts/dhcp-server/7-to-8
+++ b/src/migration-scripts/dhcp-server/7-to-8
@@ -14,16 +14,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# T3316:
-# - Adjust hostname to have valid FQDN characters only (underscores aren't allowed anymore)
-# - Rename "service dhcp-server shared-network-name ... static-mapping <hostname> mac-address ..."
-# to "service dhcp-server shared-network-name ... static-mapping <hostname> mac ..."
+# T3316: Migrate to Kea
+# - global-parameters will not function
+# - shared-network-parameters will not function
+# - subnet-parameters will not function
+# - static-mapping-parameters will not function
+# - host-decl-name is on by default, option removed
+# - ping-check no longer supported
+# - failover is default enabled on all subnets that exist on failover servers
import sys
-import re
from vyos.configtree import ConfigTree
-if len(sys.argv) < 2:
+if (len(sys.argv) < 2):
print("Must specify file name!")
sys.exit(1)
@@ -32,30 +35,49 @@ file_name = sys.argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
-base = ['service', 'dhcp-server', 'shared-network-name']
+base = ['service', 'dhcp-server']
config = ConfigTree(config_file)
if not config.exists(base):
# Nothing to do
sys.exit(0)
-for network in config.list_nodes(base):
- # Run this for every specified 'subnet'
- if config.exists(base + [network, 'subnet']):
- for subnet in config.list_nodes(base + [network, 'subnet']):
- base_subnet = base + [network, 'subnet', subnet]
- if config.exists(base_subnet + ['static-mapping']):
- for hostname in config.list_nodes(base_subnet + ['static-mapping']):
- base_mapping = base_subnet + ['static-mapping', hostname]
+if config.exists(base + ['host-decl-name']):
+ config.delete(base + ['host-decl-name'])
+
+if config.exists(base + ['global-parameters']):
+ config.delete(base + ['global-parameters'])
+
+if config.exists(base + ['shared-network-name']):
+ for network in config.list_nodes(base + ['shared-network-name']):
+ base_network = base + ['shared-network-name', network]
+
+ if config.exists(base_network + ['ping-check']):
+ config.delete(base_network + ['ping-check'])
+
+ if config.exists(base_network + ['shared-network-parameters']):
+ config.delete(base_network +['shared-network-parameters'])
- # Rename the 'mac-address' node to 'mac'
- if config.exists(base_mapping + ['mac-address']):
- config.rename(base_mapping + ['mac-address'], 'mac')
+ if not config.exists(base_network + ['subnet']):
+ continue
- # Adjust hostname to have valid FQDN characters only
- new_hostname = re.sub(r'[^a-zA-Z0-9-.]', '-', hostname)
- if new_hostname != hostname:
- config.rename(base_mapping, new_hostname)
+ # Run this for every specified 'subnet'
+ for subnet in config.list_nodes(base_network + ['subnet']):
+ base_subnet = base_network + ['subnet', subnet]
+
+ if config.exists(base_subnet + ['enable-failover']):
+ config.delete(base_subnet + ['enable-failover'])
+
+ if config.exists(base_subnet + ['ping-check']):
+ config.delete(base_subnet + ['ping-check'])
+
+ if config.exists(base_subnet + ['subnet-parameters']):
+ config.delete(base_subnet + ['subnet-parameters'])
+
+ if config.exists(base_subnet + ['static-mapping']):
+ for mapping in config.list_nodes(base_subnet + ['static-mapping']):
+ if config.exists(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']):
+ config.delete(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters'])
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/dhcp-server/8-to-9 b/src/migration-scripts/dhcp-server/8-to-9
index 810e403a6..151aa6d7b 100755
--- a/src/migration-scripts/dhcp-server/8-to-9
+++ b/src/migration-scripts/dhcp-server/8-to-9
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright (C) 2023 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
@@ -15,8 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# T3316:
-# - Migrate dhcp options under new option node
-# - Add subnet IDs to existing subnets
+# - Adjust hostname to have valid FQDN characters only (underscores aren't allowed anymore)
+# - Rename "service dhcp-server shared-network-name ... static-mapping <hostname> mac-address ..."
+# to "service dhcp-server shared-network-name ... static-mapping <hostname> mac ..."
import sys
import re
@@ -38,34 +39,23 @@ if not config.exists(base):
# Nothing to do
sys.exit(0)
-option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal',
- 'client-prefix-length', 'default-router', 'domain-name', 'domain-search',
- 'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server',
- 'pop-server', 'server-identifier', 'smtp-server', 'static-route',
- 'tftp-server-name', 'time-offset', 'time-server', 'time-zone',
- 'vendor-option', 'wins-server', 'wpad-url']
-
-subnet_id = 1
-
for network in config.list_nodes(base):
- for option in option_nodes:
- if config.exists(base + [network, option]):
- config.set(base + [network, 'option'])
- config.copy(base + [network, option], base + [network, 'option', option])
- config.delete(base + [network, option])
-
+ # Run this for every specified 'subnet'
if config.exists(base + [network, 'subnet']):
for subnet in config.list_nodes(base + [network, 'subnet']):
base_subnet = base + [network, 'subnet', subnet]
-
- for option in option_nodes:
- if config.exists(base_subnet + [option]):
- config.set(base_subnet + ['option'])
- config.copy(base_subnet + [option], base_subnet + ['option', option])
- config.delete(base_subnet + [option])
+ if config.exists(base_subnet + ['static-mapping']):
+ for hostname in config.list_nodes(base_subnet + ['static-mapping']):
+ base_mapping = base_subnet + ['static-mapping', hostname]
+
+ # Rename the 'mac-address' node to 'mac'
+ if config.exists(base_mapping + ['mac-address']):
+ config.rename(base_mapping + ['mac-address'], 'mac')
- config.set(base_subnet + ['subnet-id'], value=subnet_id)
- subnet_id += 1
+ # Adjust hostname to have valid FQDN characters only
+ new_hostname = re.sub(r'[^a-zA-Z0-9-.]', '-', hostname)
+ if new_hostname != hostname:
+ config.rename(base_mapping, new_hostname)
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/dhcp-server/9-to-10 b/src/migration-scripts/dhcp-server/9-to-10
new file mode 100755
index 000000000..810e403a6
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/9-to-10
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# T3316:
+# - Migrate dhcp options under new option node
+# - Add subnet IDs to existing subnets
+
+import sys
+import re
+from vyos.configtree import ConfigTree
+
+if len(sys.argv) < 2:
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['service', 'dhcp-server', 'shared-network-name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal',
+ 'client-prefix-length', 'default-router', 'domain-name', 'domain-search',
+ 'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server',
+ 'pop-server', 'server-identifier', 'smtp-server', 'static-route',
+ 'tftp-server-name', 'time-offset', 'time-server', 'time-zone',
+ 'vendor-option', 'wins-server', 'wpad-url']
+
+subnet_id = 1
+
+for network in config.list_nodes(base):
+ for option in option_nodes:
+ if config.exists(base + [network, option]):
+ config.set(base + [network, 'option'])
+ config.copy(base + [network, option], base + [network, 'option', option])
+ config.delete(base + [network, option])
+
+ if config.exists(base + [network, 'subnet']):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ base_subnet = base + [network, 'subnet', subnet]
+
+ for option in option_nodes:
+ if config.exists(base_subnet + [option]):
+ config.set(base_subnet + ['option'])
+ config.copy(base_subnet + [option], base_subnet + ['option', option])
+ config.delete(base_subnet + [option])
+
+ config.set(base_subnet + ['subnet-id'], value=subnet_id)
+ subnet_id += 1
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/dhcpv6-server/4-to-5 b/src/migration-scripts/dhcpv6-server/4-to-5
index ae506b9c5..55fda91b3 100755
--- a/src/migration-scripts/dhcpv6-server/4-to-5
+++ b/src/migration-scripts/dhcpv6-server/4-to-5
@@ -42,8 +42,11 @@ def find_subnet_interface(subnet):
def check_addr(if_path):
if config.exists(if_path + ['address']):
for addr in config.return_values(if_path + ['address']):
- if ip_network(addr, strict=False) == subnet_net:
- return True
+ try:
+ if ip_network(addr, strict=False) == subnet_net:
+ return True
+ except:
+ pass # interface address was probably "dhcp" or other magic string
return None
for iftype in config.list_nodes(['interfaces']):
diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7
index b918833e9..72f07880b 100755
--- a/src/migration-scripts/firewall/6-to-7
+++ b/src/migration-scripts/firewall/6-to-7
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -19,6 +19,7 @@
# utc: nftables userspace uses localtime and calculates the UTC offset automatically
# icmp/v6: migrate previously available `type-name` to valid type/code
# T4178: Update tcp flags to use multi value node
+# T6071: CLI description limit of 256 characters
import re
@@ -26,7 +27,6 @@ from sys import argv
from sys import exit
from vyos.configtree import ConfigTree
-from vyos.ifconfig import Section
if len(argv) < 2:
print("Must specify file name!")
@@ -37,6 +37,8 @@ file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
+max_len_description = 255
+
base = ['firewall']
config = ConfigTree(config_file)
@@ -105,12 +107,30 @@ icmpv6_translations = {
'unknown-option': [4, 2]
}
+if config.exists(base + ['group']):
+ for group_type in config.list_nodes(base + ['group']):
+ for group_name in config.list_nodes(base + ['group', group_type]):
+ name_description = base + ['group', group_type, group_name, 'description']
+ if config.exists(name_description):
+ tmp = config.return_value(name_description)
+ config.set(name_description, value=tmp[:max_len_description])
+
if config.exists(base + ['name']):
for name in config.list_nodes(base + ['name']):
+ name_description = base + ['name', name, 'description']
+ if config.exists(name_description):
+ tmp = config.return_value(name_description)
+ config.set(name_description, value=tmp[:max_len_description])
+
if not config.exists(base + ['name', name, 'rule']):
continue
for rule in config.list_nodes(base + ['name', name, 'rule']):
+ rule_description = base + ['name', name, 'rule', rule, 'description']
+ if config.exists(rule_description):
+ tmp = config.return_value(rule_description)
+ config.set(rule_description, value=tmp[:max_len_description])
+
rule_recent = base + ['name', name, 'rule', rule, 'recent']
rule_time = base + ['name', name, 'rule', rule, 'time']
rule_tcp_flags = base + ['name', name, 'rule', rule, 'tcp', 'flags']
@@ -161,10 +181,20 @@ if config.exists(base + ['name']):
if config.exists(base + ['ipv6-name']):
for name in config.list_nodes(base + ['ipv6-name']):
+ name_description = base + ['ipv6-name', name, 'description']
+ if config.exists(name_description):
+ tmp = config.return_value(name_description)
+ config.set(name_description, value=tmp[:max_len_description])
+
if not config.exists(base + ['ipv6-name', name, 'rule']):
continue
for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']):
+ rule_description = base + ['ipv6-name', name, 'rule', rule, 'description']
+ if config.exists(rule_description):
+ tmp = config.return_value(rule_description)
+ config.set(rule_description, value=tmp[:max_len_description])
+
rule_recent = base + ['ipv6-name', name, 'rule', rule, 'recent']
rule_time = base + ['ipv6-name', name, 'rule', rule, 'time']
rule_tcp_flags = base + ['ipv6-name', name, 'rule', rule, 'tcp', 'flags']
diff --git a/src/migration-scripts/policy/1-to-2 b/src/migration-scripts/policy/1-to-2
index c70490ce9..c7a983bba 100755
--- a/src/migration-scripts/policy/1-to-2
+++ b/src/migration-scripts/policy/1-to-2
@@ -32,23 +32,23 @@ file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
-base = ['policy', 'ipv6-route']
+base = ['policy']
config = ConfigTree(config_file)
if not config.exists(base):
# Nothing to do
exit(0)
-config.rename(base, 'route6')
-config.set_tag(['policy', 'route6'])
+if config.exists(base + ['ipv6-route']):
+ config.rename(base + ['ipv6-route'],'route6')
+ config.set_tag(['policy', 'route6'])
for route in ['route', 'route6']:
- route_path = ['policy', route]
- if config.exists(route_path):
- for name in config.list_nodes(route_path):
- if config.exists(route_path + [name, 'rule']):
- for rule in config.list_nodes(route_path + [name, 'rule']):
- rule_tcp_flags = route_path + [name, 'rule', rule, 'tcp', 'flags']
+ if config.exists(base + [route]):
+ for name in config.list_nodes(base + [route]):
+ if config.exists(base + [route, name, 'rule']):
+ for rule in config.list_nodes(base + [route, name, 'rule']):
+ rule_tcp_flags = base + [route, name, 'rule', rule, 'tcp', 'flags']
if config.exists(rule_tcp_flags):
tmp = config.return_value(rule_tcp_flags)
diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py
deleted file mode 100755
index 7d4b47104..000000000
--- a/src/op_mode/clear_dhcp_lease.py
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import argparse
-import re
-
-from vyos.configquery import ConfigTreeQuery
-from vyos.kea import kea_parse_leases
-from vyos.utils.io import ask_yes_no
-from vyos.utils.process import call
-from vyos.utils.commit import commit_in_progress
-
-# TODO: Update to use Kea control socket command "lease4-del"
-
-config = ConfigTreeQuery()
-base = ['service', 'dhcp-server']
-lease_file = '/config/dhcp/dhcp4-leases.csv'
-
-
-def del_lease_ip(address):
- """
- Read lease_file and write data to this file
- without specific section "lease ip"
- Delete section "lease x.x.x.x { x;x;x; }"
- """
- with open(lease_file, encoding='utf-8') as f:
- data = f.read().rstrip()
- pattern = rf"^{address},[^\n]+\n"
- # Delete lease for ip block
- data = re.sub(pattern, '', data)
-
- # Write new data to original lease_file
- with open(lease_file, 'w', encoding='utf-8') as f:
- f.write(data)
-
-def is_ip_in_leases(address):
- """
- Return True if address found in the lease file
- """
- leases = kea_parse_leases(lease_file)
- for lease in leases:
- if address == lease['address']:
- return True
- print(f'Address "{address}" not found in "{lease_file}"')
- return False
-
-if not config.exists(base):
- print('DHCP-server not configured!')
- exit(0)
-
-if config.exists(base + ['failover']):
- print('Lease cannot be reset in failover mode!')
- exit(0)
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument('--ip', help='IPv4 address', action='store', required=True)
-
- args = parser.parse_args()
- address = args.ip
-
- if not is_ip_in_leases(address):
- exit(1)
-
- if commit_in_progress():
- print('Cannot clear DHCP lease while a commit is in progress')
- exit(1)
-
- if not ask_yes_no(f'This will restart DHCP server.\nContinue?'):
- exit(1)
- else:
- del_lease_ip(address)
- call('systemctl restart kea-dhcp4-server.service')
diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py
index cf8adf795..6ea213bec 100755
--- a/src/op_mode/conntrack.py
+++ b/src/op_mode/conntrack.py
@@ -112,7 +112,8 @@ def get_formatted_output(dict_data):
proto = meta['layer4']['protoname']
if direction == 'independent':
conn_id = meta['id']
- timeout = meta['timeout']
+ # T6138 flowtable offload conntrack entries without 'timeout'
+ timeout = meta.get('timeout', 'n/a')
orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src
orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst
reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src
diff --git a/src/op_mode/container.py b/src/op_mode/container.py
index 5a022d0c0..d29af8821 100755
--- a/src/op_mode/container.py
+++ b/src/op_mode/container.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 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
@@ -20,6 +20,8 @@ import sys
from sys import exit
from vyos.utils.process import cmd
+from vyos.utils.process import call
+from vyos.utils.process import rc_cmd
import vyos.opmode
@@ -29,23 +31,51 @@ def _get_json_data(command: str) -> list:
"""
return cmd(f'{command} --format json')
-
def _get_raw_data(command: str) -> list:
json_data = _get_json_data(command)
data = json.loads(json_data)
return data
def add_image(name: str):
- from vyos.utils.process import rc_cmd
+ """ Pull image from container registry. If registry authentication
+ is defined within VyOS CLI, credentials are used to login befroe pull """
+ from vyos.configquery import ConfigTreeQuery
+
+ conf = ConfigTreeQuery()
+ container = conf.get_config_dict(['container', 'registry'])
+
+ do_logout = False
+ if 'registry' in container:
+ for registry, registry_config in container['registry'].items():
+ if 'disable' in registry_config:
+ continue
+ if 'authentication' in registry_config:
+ do_logout = True
+ if {'username', 'password'} <= set(registry_config['authentication']):
+ username = registry_config['authentication']['username']
+ password = registry_config['authentication']['password']
+ cmd = f'podman login --username {username} --password {password} {registry}'
+ rc, out = rc_cmd(cmd)
+ if rc != 0: raise vyos.opmode.InternalError(out)
rc, output = rc_cmd(f'podman image pull {name}')
if rc != 0:
raise vyos.opmode.InternalError(output)
+ if do_logout:
+ rc_cmd('podman logout --all')
+
def delete_image(name: str):
from vyos.utils.process import rc_cmd
- rc, output = rc_cmd(f'podman image rm --force {name}')
+ if name == 'all':
+ # gather list of all images and pass them to the removal list
+ name = cmd('sudo podman image ls --quiet')
+ # If there are no container images left, we can not delete them all
+ if not name: return
+ # replace newline with whitespace
+ name = name.replace('\n', ' ')
+ rc, output = rc_cmd(f'podman image rm {name}')
if rc != 0:
raise vyos.opmode.InternalError(output)
@@ -57,7 +87,6 @@ def show_container(raw: bool):
else:
return cmd(command)
-
def show_image(raw: bool):
command = 'podman image ls'
container_data = _get_raw_data('podman image ls')
@@ -66,7 +95,6 @@ def show_image(raw: bool):
else:
return cmd(command)
-
def show_network(raw: bool):
command = 'podman network ls'
container_data = _get_raw_data(command)
@@ -75,7 +103,6 @@ def show_network(raw: bool):
else:
return cmd(command)
-
def restart(name: str):
from vyos.utils.process import rc_cmd
@@ -86,7 +113,6 @@ def restart(name: str):
print(f'Container "{name}" restarted!')
return output
-
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index 1d9ad0e76..d27e1baf7 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -31,6 +31,7 @@ from vyos.configquery import ConfigTreeQuery
from vyos.kea import kea_get_active_config
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
time_string = "%a %b %d %H:%M:%S %Z %Y"
@@ -360,6 +361,28 @@ def show_server_static_mappings(raw: bool, family: ArgFamily, pool: typing.Optio
else:
return _get_formatted_server_static_mappings(static_mappings, family=family)
+def _lease_valid(inet, address):
+ leases = kea_get_leases(inet)
+ for lease in leases:
+ if address == lease['ip-address']:
+ return True
+ return False
+
+@_verify
+def clear_dhcp_server_lease(family: ArgFamily, address: str):
+ v = 'v6' if family == 'inet6' else ''
+ inet = '6' if family == 'inet6' else '4'
+
+ if not _lease_valid(inet, address):
+ print(f'Lease not found on DHCP{v} server')
+ return None
+
+ if not kea_delete_lease(inet, address):
+ print(f'Failed to clear lease for "{address}"')
+ return None
+
+ print(f'Lease "{address}" has been cleared')
+
def _get_raw_client_leases(family='inet', interface=None):
from time import mktime
from datetime import datetime
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index d677c2cf8..b0567305a 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -65,6 +65,8 @@ MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root part
MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-Serial)?'
MSG_INPUT_COPY_DATA: str = 'Would you like to copy data to the new image?'
MSG_INPUT_CHOOSE_COPY_DATA: str = 'From which image would you like to save config information?'
+MSG_INPUT_COPY_ENC_DATA: str = 'Would you like to copy the encrypted config to the new image?'
+MSG_INPUT_CHOOSE_COPY_ENC_DATA: str = 'From which image would you like to copy the encrypted config?'
MSG_WARN_ISO_SIGN_INVALID: str = 'Signature is not valid. Do you want to continue with installation?'
MSG_WARN_ISO_SIGN_UNAVAL: str = 'Signature is not available. Do you want to continue with installation?'
MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.'
@@ -212,14 +214,17 @@ def search_previous_installation(disks: list[str]) -> None:
disks (list[str]): a list of available disks
"""
mnt_config = '/mnt/config'
+ mnt_encrypted_config = '/mnt/encrypted_config'
mnt_ssh = '/mnt/ssh'
mnt_tmp = '/mnt/tmp'
rmtree(Path(mnt_config), ignore_errors=True)
rmtree(Path(mnt_ssh), ignore_errors=True)
Path(mnt_tmp).mkdir(exist_ok=True)
+ Path(mnt_encrypted_config).unlink(missing_ok=True)
print('Searching for data from previous installations')
image_data = []
+ encrypted_configs = []
for disk_name in disks:
for partition in disk.partition_list(disk_name):
if disk.partition_mount(partition, mnt_tmp):
@@ -227,32 +232,61 @@ def search_previous_installation(disks: list[str]) -> None:
for path in Path(mnt_tmp + '/boot').iterdir():
if path.joinpath('rw/config/.vyatta_config').exists():
image_data.append((path.name, partition))
+ if Path(mnt_tmp + '/luks').exists():
+ for path in Path(mnt_tmp + '/luks').iterdir():
+ encrypted_configs.append((path.name, partition))
disk.partition_umount(partition)
- if len(image_data) == 1:
- image_name, image_drive = image_data[0]
- print('Found data from previous installation:')
- print(f'\t{image_name} on {image_drive}')
- if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
- return
-
- elif len(image_data) > 1:
- print('Found data from previous installations')
- if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
- return
-
- image_name, image_drive = select_entry(image_data,
- 'Available versions:',
- MSG_INPUT_CHOOSE_COPY_DATA,
- search_format_selection)
+ image_name = None
+ image_drive = None
+ encrypted = False
+
+ if len(image_data) > 0:
+ if len(image_data) == 1:
+ print('Found data from previous installation:')
+ print(f'\t{" on ".join(image_data[0])}')
+ if ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
+ image_name, image_drive = image_data[0]
+
+ elif len(image_data) > 1:
+ print('Found data from previous installations')
+ if ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
+ image_name, image_drive = select_entry(image_data,
+ 'Available versions:',
+ MSG_INPUT_CHOOSE_COPY_DATA,
+ search_format_selection)
+ elif len(encrypted_configs) > 0:
+ if len(encrypted_configs) == 1:
+ print('Found encrypted config from previous installation:')
+ print(f'\t{" on ".join(encrypted_configs[0])}')
+ if ask_yes_no(MSG_INPUT_COPY_ENC_DATA, default=True):
+ image_name, image_drive = encrypted_configs[0]
+ encrypted = True
+
+ elif len(encrypted_configs) > 1:
+ print('Found encrypted configs from previous installations')
+ if ask_yes_no(MSG_INPUT_COPY_ENC_DATA, default=True):
+ image_name, image_drive = select_entry(encrypted_configs,
+ 'Available versions:',
+ MSG_INPUT_CHOOSE_COPY_ENC_DATA,
+ search_format_selection)
+ encrypted = True
+
else:
print('No previous installation found')
return
+ if not image_name:
+ return
+
disk.partition_mount(image_drive, mnt_tmp)
- copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
+ if not encrypted:
+ copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
+ else:
+ copy(f'{mnt_tmp}/luks/{image_name}', mnt_encrypted_config)
+
Path(mnt_ssh).mkdir()
host_keys: list[str] = glob(f'{mnt_tmp}/boot/{image_name}/rw/etc/ssh/ssh_host*')
for host_key in host_keys:
@@ -279,6 +313,12 @@ def copy_previous_installation_data(target_dir: str) -> None:
dirs_exist_ok=True)
+def copy_previous_encrypted_config(target_dir: str, image_name: str) -> None:
+ if Path('/mnt/encrypted_config').exists():
+ Path(target_dir).mkdir(exist_ok=True)
+ copy('/mnt/encrypted_config', Path(target_dir).joinpath(image_name))
+
+
def ask_single_disk(disks_available: dict[str, int]) -> str:
"""Ask user to select a disk for installation
@@ -712,6 +752,9 @@ def install_image() -> None:
# owner restored on copy of config data by chmod_2775, above
copy_previous_installation_data(f'{DIR_DST_ROOT}/boot/{image_name}/rw')
+ # copy saved encrypted config volume
+ copy_previous_encrypted_config(f'{DIR_DST_ROOT}/luks', image_name)
+
if is_raid_install(install_target):
write_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw'
raid.update_default(write_dir)
@@ -743,6 +786,10 @@ def install_image() -> None:
grub.install(install_target.name, f'{DIR_DST_ROOT}/boot/',
f'{DIR_DST_ROOT}/boot/efi')
+ # sort inodes (to make GRUB read config files in alphabetical order)
+ grub.sort_inodes(f'{DIR_DST_ROOT}/{grub.GRUB_DIR_VYOS}')
+ grub.sort_inodes(f'{DIR_DST_ROOT}/{grub.GRUB_DIR_VYOS_VERS}')
+
# umount filesystems and remove temporary files
if is_raid_install(install_target):
cleanup([install_target.name],
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index e64a85b95..1510a667c 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -95,6 +95,15 @@ def delete_image(image_name: Optional[str] = None,
except Exception as err:
exit(f'Unable to remove the image "{image_name}": {err}')
+ # remove LUKS volume if it exists
+ luks_path: Path = Path(f'{persistence_storage}/luks/{image_name}')
+ if luks_path.is_file():
+ try:
+ luks_path.unlink()
+ print(f'The encrypted config for "{image_name}" was successfully deleted')
+ except Exception as err:
+ exit(f'Unable to remove the encrypted config for "{image_name}": {err}')
+
@compat.grub_cfg_update
def set_image(image_name: Optional[str] = None,
@@ -174,6 +183,16 @@ def rename_image(name_old: str, name_new: str) -> None:
except Exception as err:
exit(f'Unable to rename image "{name_old}" to "{name_new}": {err}')
+ # rename LUKS volume if it exists
+ old_luks_path: Path = Path(f'{persistence_storage}/luks/{name_old}')
+ if old_luks_path.is_file():
+ try:
+ new_luks_path: Path = Path(f'{persistence_storage}/luks/{name_new}')
+ old_luks_path.rename(new_luks_path)
+ print(f'The encrypted config for "{name_old}" was successfully renamed to "{name_new}"')
+ except Exception as err:
+ exit(f'Unable to rename the encrypted config for "{name_old}" to "{name_new}": {err}')
+
def list_images() -> None:
"""Print list of available images for CLI hints"""
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index 355182b26..648a017d5 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2023 VyOS maintainers and contributors
+# Copyright (C) 2020-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -19,6 +19,7 @@ import sys
import grp
import re
import json
+import typing
import logging
import signal
import importlib.util
@@ -29,6 +30,7 @@ from vyos.defaults import directories
from vyos.utils.boot import boot_configuration_complete
from vyos.configsource import ConfigSourceString
from vyos.configsource import ConfigSourceError
+from vyos.configdep import call_dependents
from vyos.config import Config
from vyos import ConfigError
@@ -198,10 +200,12 @@ def initialization(socket):
return None
config = Config(config_source=configsource)
+ dependent_func: dict[str, list[typing.Callable]] = {}
+ setattr(config, 'dependent_func', dependent_func)
return config
-def process_node_data(config, data) -> int:
+def process_node_data(config, data, last: bool = False) -> int:
if not config:
logger.critical(f"Empty config")
return R_ERROR_DAEMON
@@ -223,11 +227,18 @@ def process_node_data(config, data) -> int:
args.insert(0, f'{script_name}.py')
if script_name not in include_set:
+ # call dependents now if last element of prio queue is run
+ # independent of configd
+ if last:
+ call_dependents(dependent_func=config.dependent_func)
return R_PASS
with stdout_redirected(session_out, session_mode):
result = run_script(conf_mode_scripts[script_name], config, args)
+ if last:
+ call_dependents(dependent_func=config.dependent_func)
+
return result
def remove_if_file(f: str):
@@ -281,7 +292,9 @@ if __name__ == '__main__':
socket.send(resp.encode())
config = initialization(socket)
elif message["type"] == "node":
- res = process_node_data(config, message["data"])
+ if message["last"]:
+ logger.debug(f'final element of priority queue')
+ res = process_node_data(config, message["data"], message["last"])
response = res.to_bytes(1, byteorder=sys.byteorder)
logger.debug(f"Sending response {res}")
socket.send(response)
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 40d442e30..a7b14a1a3 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -459,7 +459,6 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
request: Request, background_tasks: BackgroundTasks):
session = app.state.vyos_session
env = session.get_session_env()
- config = Config(session_env=env)
endpoint = request.url.path
@@ -474,6 +473,8 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
# so the lock is really global
lock.acquire()
+ config = Config(session_env=env)
+
status = 200
msg = None
error_msg = None
diff --git a/src/shim/vyshim.c b/src/shim/vyshim.c
index cae8b6152..41723e7a4 100644
--- a/src/shim/vyshim.c
+++ b/src/shim/vyshim.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 VyOS maintainers and contributors
+ * Copyright (C) 2020-2024 VyOS maintainers and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 or later as
@@ -49,6 +49,7 @@
#define GET_SESSION "cli-shell-api --show-working-only --show-show-defaults --show-ignore-edit showConfig"
#define COMMIT_MARKER "/var/tmp/initial_in_commit"
+#define QUEUE_MARKER "/var/tmp/last_in_queue"
enum {
SUCCESS = 1 << 0,
@@ -77,6 +78,7 @@ int main(int argc, char* argv[])
int ex_index;
int init_timeout = 0;
+ int last = 0;
debug_print("Connecting to vyos-configd ...\n");
zmq_connect(requester, SOCKET_PATH);
@@ -101,10 +103,16 @@ int main(int argc, char* argv[])
return ret;
}
+ if (access(QUEUE_MARKER, F_OK) != -1) {
+ last = 1;
+ remove(QUEUE_MARKER);
+ }
+
char error_code[1];
debug_print("Sending node data ...\n");
- char *string_node_data_msg = mkjson(MKJSON_OBJ, 2,
+ char *string_node_data_msg = mkjson(MKJSON_OBJ, 3,
MKJSON_STRING, "type", "node",
+ MKJSON_BOOL, "last", last,
MKJSON_STRING, "data", &string_node_data[0]);
zmq_send(requester, string_node_data_msg, strlen(string_node_data_msg), 0);
diff --git a/src/system/grub_update.py b/src/system/grub_update.py
index 5a7d8eb72..5a0534195 100644
--- a/src/system/grub_update.py
+++ b/src/system/grub_update.py
@@ -105,4 +105,8 @@ if __name__ == '__main__':
else:
render(grub_cfg_main, grub.TMPL_GRUB_MAIN, {})
+ # sort inodes (to make GRUB read config files in alphabetical order)
+ grub.sort_inodes(f'{root_dir}/{grub.GRUB_DIR_VYOS}')
+ grub.sort_inodes(f'{root_dir}/{grub.GRUB_DIR_VYOS_VERS}')
+
exit(0)
diff --git a/src/system/vyos-event-handler.py b/src/system/vyos-event-handler.py
index 74112ec91..dd2793046 100755
--- a/src/system/vyos-event-handler.py
+++ b/src/system/vyos-event-handler.py
@@ -153,7 +153,12 @@ if __name__ == '__main__':
continue
for entry in data:
message = entry['MESSAGE']
- pid = entry['_PID']
+ pid = -1
+ try:
+ pid = entry['_PID']
+ except Exception as ex:
+ journal.send(f'Unable to extract PID from message entry: {entry}', SYSLOG_IDENTIFIER=my_name)
+ continue
# Skip empty messages and messages from this process
if message and pid != my_pid:
try: