summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/container.py40
-rwxr-xr-xsrc/conf_mode/firewall.py124
-rwxr-xr-xsrc/conf_mode/interfaces_bonding.py28
-rwxr-xr-xsrc/conf_mode/interfaces_bridge.py7
-rwxr-xr-xsrc/conf_mode/interfaces_geneve.py2
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py28
-rwxr-xr-xsrc/conf_mode/interfaces_pseudo-ethernet.py2
-rwxr-xr-xsrc/conf_mode/interfaces_tunnel.py12
-rwxr-xr-xsrc/conf_mode/interfaces_virtual-ethernet.py2
-rwxr-xr-xsrc/conf_mode/interfaces_vti.py2
-rwxr-xr-xsrc/conf_mode/interfaces_vxlan.py2
-rwxr-xr-xsrc/conf_mode/interfaces_wireguard.py76
-rwxr-xr-xsrc/conf_mode/interfaces_wwan.py2
-rw-r--r--src/conf_mode/load-balancing_haproxy.py44
-rwxr-xr-xsrc/conf_mode/load-balancing_wan.py119
-rwxr-xr-xsrc/conf_mode/nat.py8
-rwxr-xr-xsrc/conf_mode/nat66.py4
-rwxr-xr-xsrc/conf_mode/pki.py106
-rwxr-xr-xsrc/conf_mode/policy_route.py47
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py23
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py118
-rwxr-xr-xsrc/conf_mode/service_console-server.py8
-rwxr-xr-xsrc/conf_mode/service_dhcp-server.py303
-rwxr-xr-xsrc/conf_mode/service_dns_forwarding.py7
-rwxr-xr-xsrc/conf_mode/service_https.py14
-rwxr-xr-xsrc/conf_mode/service_ids_ddos-protection.py104
-rw-r--r--src/conf_mode/service_monitoring_network_event.py93
-rwxr-xr-xsrc/conf_mode/service_monitoring_prometheus.py63
-rwxr-xr-xsrc/conf_mode/service_snmp.py8
-rwxr-xr-xsrc/conf_mode/system_flow-accounting.py53
-rwxr-xr-xsrc/conf_mode/system_host-name.py9
-rwxr-xr-xsrc/conf_mode/system_login.py16
-rwxr-xr-xsrc/conf_mode/system_login_banner.py8
-rwxr-xr-xsrc/conf_mode/system_option.py58
-rwxr-xr-xsrc/conf_mode/system_sflow.py2
-rwxr-xr-xsrc/conf_mode/system_syslog.py86
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py56
-rwxr-xr-xsrc/conf_mode/vrf.py5
38 files changed, 1128 insertions, 561 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 594de3eb0..94882fc14 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -22,6 +22,7 @@ from ipaddress import ip_address
from ipaddress import ip_network
from json import dumps as json_write
+import psutil
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -223,6 +224,21 @@ def verify(container):
if not os.path.exists(source):
raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!')
+ if 'tmpfs' in container_config:
+ for tmpfs, tmpfs_config in container_config['tmpfs'].items():
+ if 'destination' not in tmpfs_config:
+ raise ConfigError(f'tmpfs "{tmpfs}" has no destination path configured!')
+ if 'size' in tmpfs_config:
+ free_mem_mb: int = psutil.virtual_memory().available / 1024 / 1024
+ if int(tmpfs_config['size']) > free_mem_mb:
+ Warning(f'tmpfs "{tmpfs}" size is greater than the current free memory!')
+
+ total_mem_mb: int = (psutil.virtual_memory().total / 1024 / 1024) / 2
+ if int(tmpfs_config['size']) > total_mem_mb:
+ raise ConfigError(f'tmpfs "{tmpfs}" size should not be more than 50% of total system memory!')
+ else:
+ raise ConfigError(f'tmpfs "{tmpfs}" has no size configured!')
+
if 'port' in container_config:
for tmp in container_config['port']:
if not {'source', 'destination'} <= set(container_config['port'][tmp]):
@@ -273,6 +289,13 @@ def verify(container):
if 'registry' in container:
for registry, registry_config in container['registry'].items():
+ if 'mirror' in registry_config:
+ if 'host_name' in registry_config['mirror'] and 'address' in registry_config['mirror']:
+ raise ConfigError(f'Container registry mirror address/host-name are mutually exclusive!')
+
+ if 'path' in registry_config['mirror'] and not registry_config['mirror']['path'].startswith('/'):
+ raise ConfigError('Container registry mirror path must start with "/"!')
+
if 'authentication' not in registry_config:
continue
if not {'username', 'password'} <= set(registry_config['authentication']):
@@ -301,6 +324,11 @@ def generate_run_arguments(name, container_config):
cap = cap.upper().replace('-', '_')
capabilities += f' --cap-add={cap}'
+ # Grant root capabilities to the container
+ privileged = ''
+ if 'privileged' in container_config:
+ privileged = '--privileged'
+
# Add a host device to the container /dev/x:/dev/x
device = ''
if 'device' in container_config:
@@ -362,6 +390,14 @@ def generate_run_arguments(name, container_config):
prop = vol_config['propagation']
volume += f' --volume {svol}:{dvol}:{mode},{prop}'
+ # Mount tmpfs
+ tmpfs = ''
+ if 'tmpfs' in container_config:
+ for tmpfs_config in container_config['tmpfs'].values():
+ dest = tmpfs_config['destination']
+ size = tmpfs_config['size']
+ tmpfs += f' --mount=type=tmpfs,tmpfs-size={size}M,destination={dest}'
+
host_pid = ''
if 'allow_host_pid' in container_config:
host_pid = '--pid host'
@@ -371,9 +407,9 @@ def generate_run_arguments(name, container_config):
for ns in container_config['name_server']:
name_server += f'--dns {ns}'
- container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} {sysctl_opt} ' \
+ container_base_cmd = f'--detach --interactive --tty --replace {capabilities} {privileged} --cpus {cpu_quota} {sysctl_opt} ' \
f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \
- f'--name {name} {hostname} {device} {port} {name_server} {volume} {env_opt} {label} {uid} {host_pid}'
+ f'--name {name} {hostname} {device} {port} {name_server} {volume} {tmpfs} {env_opt} {label} {uid} {host_pid}'
entrypoint = ''
if 'entrypoint' in container_config:
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index ffbd915a2..274ca2ce6 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -18,7 +18,6 @@ import os
import re
from sys import exit
-
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import is_node_changed
@@ -34,6 +33,8 @@ from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import call
from vyos.utils.process import cmd
from vyos.utils.process import rc_cmd
+from vyos.utils.network import get_vrf_members
+from vyos.utils.network import get_interface_vrf
from vyos import ConfigError
from vyos import airbag
from pathlib import Path
@@ -43,7 +44,7 @@ airbag.enable()
nftables_conf = '/run/nftables.conf'
domain_resolver_usage = '/run/use-vyos-domain-resolver-firewall'
-domain_resolver_usage_nat = '/run/use-vyos-domain-resolver-nat'
+firewall_config_dir = "/config/firewall"
sysctl_file = r'/run/sysctl/10-vyos-firewall.conf'
@@ -53,7 +54,8 @@ valid_groups = [
'network_group',
'port_group',
'interface_group',
- ## Added for group ussage in bridge firewall
+ 'remote_group',
+ ## Added for group usage in bridge firewall
'ipv4_address_group',
'ipv6_address_group',
'ipv4_network_group',
@@ -134,6 +136,27 @@ def get_config(config=None):
fqdn_config_parse(firewall, 'firewall')
+ if not os.path.exists(nftables_conf):
+ firewall['first_install'] = True
+
+ if 'zone' in firewall:
+ for local_zone, local_zone_conf in firewall['zone'].items():
+ if 'local_zone' not in local_zone_conf:
+ # Get physical interfaces assigned to the zone if vrf is used:
+ if 'vrf' in local_zone_conf['member']:
+ local_zone_conf['vrf_interfaces'] = {}
+ for vrf_name in local_zone_conf['member']['vrf']:
+ local_zone_conf['vrf_interfaces'][vrf_name] = ','.join(get_vrf_members(vrf_name))
+ continue
+
+ local_zone_conf['from_local'] = {}
+
+ for zone, zone_conf in firewall['zone'].items():
+ if zone == local_zone or 'from' not in zone_conf:
+ continue
+ if local_zone in zone_conf['from']:
+ local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone]
+
set_dependents('conntrack', conf)
return firewall
@@ -182,7 +205,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
if 'jump' not in rule_conf['action']:
raise ConfigError('jump-target defined, but action jump needed and it is not defined')
target = rule_conf['jump_target']
- if hook != 'name': # This is a bit clumsy, but consolidates a chunk of code.
+ if hook != 'name': # This is a bit clumsy, but consolidates a chunk of code.
verify_jump_target(firewall, hook, target, family, recursive=True)
else:
verify_jump_target(firewall, hook, target, family, recursive=False)
@@ -245,12 +268,12 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
if dict_search_args(rule_conf, 'gre', 'flags', 'checksum') is None:
# There is no builtin match in nftables for the GRE key, so we need to do a raw lookup.
- # The offset of the key within the packet shifts depending on the C-flag.
- # 99% of the time, nobody will have checksums enabled - it's usually a manual config option.
- # We can either assume it is unset unless otherwise directed
+ # The offset of the key within the packet shifts depending on the C-flag.
+ # 99% of the time, nobody will have checksums enabled - it's usually a manual config option.
+ # We can either assume it is unset unless otherwise directed
# (confusing, requires doco to explain why it doesn't work sometimes)
- # or, demand an explicit selection to be made for this specific match rule.
- # This check enforces the latter. The user is free to create rules for both cases.
+ # or, demand an explicit selection to be made for this specific match rule.
+ # This check enforces the latter. The user is free to create rules for both cases.
raise ConfigError('Matching GRE tunnel key requires an explicit checksum flag match. For most cases, use "gre flags checksum unset"')
if dict_search_args(rule_conf, 'gre', 'flags', 'key', 'unset') is not None:
@@ -263,7 +286,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
if gre_inner_value < 0 or gre_inner_value > 65535:
raise ConfigError('inner-proto outside valid ethertype range 0-65535')
except ValueError:
- pass # Symbolic constant, pre-validated before reaching here.
+ pass # Symbolic constant, pre-validated before reaching here.
tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
if tcp_flags:
@@ -290,8 +313,8 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
raise ConfigError('Only one of address, fqdn or geoip can be specified')
if 'group' in side_conf:
- if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1:
- raise ConfigError('Only one address-group, network-group or domain-group can be specified')
+ if len({'address_group', 'network_group', 'domain_group', 'remote_group'} & set(side_conf['group'])) > 1:
+ raise ConfigError('Only one address-group, network-group, remote-group or domain-group can be specified')
for group in valid_groups:
if group in side_conf['group']:
@@ -311,7 +334,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
error_group = fw_group.replace("_", "-")
- if group in ['address_group', 'network_group', 'domain_group']:
+ if group in ['address_group', 'network_group', 'domain_group', 'remote_group']:
types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf]
if types:
raise ConfigError(f'{error_group} and {types[0]} cannot both be defined')
@@ -414,6 +437,16 @@ def verify(firewall):
for ifname in interfaces:
verify_hardware_offload(ifname)
+ if 'offload' in firewall.get('global_options', {}).get('state_policy', {}):
+ offload_path = firewall['global_options']['state_policy']['offload']
+ if 'offload_target' not in offload_path:
+ raise ConfigError('offload-target must be specified')
+
+ offload_target = offload_path['offload_target']
+
+ if not dict_search_args(firewall, 'flowtable', offload_target):
+ raise ConfigError(f'Invalid offload-target. Flowtable "{offload_target}" does not exist on the system')
+
if 'group' in firewall:
for group_type in nested_group_types:
if group_type in firewall['group']:
@@ -421,6 +454,11 @@ def verify(firewall):
for group_name, group in groups.items():
verify_nested_group(group_name, group, groups, [])
+ if 'remote_group' in firewall['group']:
+ for group_name, group in firewall['group']['remote_group'].items():
+ if 'url' not in group:
+ raise ConfigError(f'remote-group {group_name} must have a url configured')
+
for family in ['ipv4', 'ipv6', 'bridge']:
if family in firewall:
for chain in ['name','forward','input','output', 'prerouting']:
@@ -442,28 +480,45 @@ def verify(firewall):
local_zone = False
zone_interfaces = []
+ zone_vrf = []
if 'zone' in firewall:
for zone, zone_conf in firewall['zone'].items():
- if 'local_zone' not in zone_conf and 'interface' not in zone_conf:
+ if 'local_zone' not in zone_conf and 'member' not in zone_conf:
raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone')
if 'local_zone' in zone_conf:
if local_zone:
raise ConfigError('There cannot be multiple local zones')
- if 'interface' in zone_conf:
+ if 'member' in zone_conf:
raise ConfigError('Local zone cannot have interfaces assigned')
if 'intra_zone_filtering' in zone_conf:
raise ConfigError('Local zone cannot use intra-zone-filtering')
local_zone = True
- if 'interface' in zone_conf:
- found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces]
+ if 'member' in zone_conf:
+ if 'interface' in zone_conf['member']:
+ for iface in zone_conf['member']['interface']:
+
+ if iface in zone_interfaces:
+ raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
+
+ iface_vrf = get_interface_vrf(iface)
+ if iface_vrf != 'default':
+ Warning(f"Interface {iface} assigned to zone {zone} is in VRF {iface_vrf}. This might not work as expected.")
+ zone_interfaces.append(iface)
- if found_duplicates:
- raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
+ if 'vrf' in zone_conf['member']:
+ for vrf in zone_conf['member']['vrf']:
+ if vrf in zone_vrf:
+ raise ConfigError(f'VRF cannot be assigned to multiple zones')
+ zone_vrf.append(vrf)
- zone_interfaces += zone_conf['interface']
+ if 'vrf_interfaces' in zone_conf:
+ for vrf_name, vrf_interfaces in zone_conf['vrf_interfaces'].items():
+ if not vrf_interfaces:
+ raise ConfigError(
+ f'VRF "{vrf_name}" cannot be a member of any zone. It does not contain any interfaces.')
if 'intra_zone_filtering' in zone_conf:
intra_zone = zone_conf['intra_zone_filtering']
@@ -499,24 +554,17 @@ def verify(firewall):
return None
def generate(firewall):
- if not os.path.exists(nftables_conf):
- firewall['first_install'] = True
-
- if 'zone' in firewall:
- for local_zone, local_zone_conf in firewall['zone'].items():
- if 'local_zone' not in local_zone_conf:
- continue
-
- local_zone_conf['from_local'] = {}
-
- for zone, zone_conf in firewall['zone'].items():
- if zone == local_zone or 'from' not in zone_conf:
- continue
- if local_zone in zone_conf['from']:
- local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone]
-
render(nftables_conf, 'firewall/nftables.j2', firewall)
render(sysctl_file, 'firewall/sysctl-firewall.conf.j2', firewall)
+
+ # Cleanup remote-group cache files
+ if os.path.exists(firewall_config_dir):
+ for fw_file in os.listdir(firewall_config_dir):
+ # Delete matching files in 'config/firewall' that no longer exist as a remote-group in config
+ if fw_file.startswith("R_") and fw_file.endswith(".txt"):
+ if 'group' not in firewall or 'remote_group' not in firewall['group'] or fw_file[2:-4] not in firewall['group']['remote_group'].keys():
+ os.unlink(os.path.join(firewall_config_dir, fw_file))
+
return None
def parse_firewall_error(output):
@@ -576,7 +624,7 @@ def apply(firewall):
## DOMAIN RESOLVER
domain_action = 'restart'
- if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items():
+ if dict_search_args(firewall, 'group', 'remote_group') or dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items():
text = f'# Automatically generated by firewall.py\nThis file indicates that vyos-domain-resolver service is used by the firewall.\n'
Path(domain_resolver_usage).write_text(text)
else:
@@ -589,7 +637,7 @@ def apply(firewall):
# Call helper script to Update set contents
if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']:
print('Updating GeoIP. Please wait...')
- geoip_update(firewall)
+ geoip_update(firewall=firewall)
return None
diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py
index 4f1141dcb..84316c16e 100755
--- a/src/conf_mode/interfaces_bonding.py
+++ b/src/conf_mode/interfaces_bonding.py
@@ -126,9 +126,8 @@ def get_config(config=None):
# Restore existing config level
conf.set_level(old_level)
- if dict_search('member.interface', bond):
- for interface, interface_config in bond['member']['interface'].items():
-
+ if dict_search('member.interface', bond) is not None:
+ for interface in bond['member']['interface']:
interface_ethernet_config = conf.get_config_dict(
['interfaces', 'ethernet', interface],
key_mangling=('-', '_'),
@@ -137,44 +136,45 @@ def get_config(config=None):
with_defaults=False,
with_recursive_defaults=False)
- interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config)
+ bond['member']['interface'][interface].update({'config_paths' :
+ dict_to_paths_values(interface_ethernet_config)})
# Check if member interface is a new member
if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]):
bond['shutdown_required'] = {}
- interface_config['new_added'] = {}
+ bond['member']['interface'][interface].update({'new_added' : {}})
# Check if member interface is disabled
conf.set_level(['interfaces'])
section = Section.section(interface) # this will be 'ethernet' for 'eth0'
if conf.exists([section, interface, 'disable']):
- interface_config['disable'] = ''
+ if tmp: bond['member']['interface'][interface].update({'disable': ''})
conf.set_level(old_level)
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
- if tmp: interface_config['is_bridge_member'] = tmp
+ if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp})
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
- for tmp in is_member(conf, interface, 'bonding'):
- if bond['ifname'] == tmp:
- continue
- interface_config['is_bond_member'] = tmp
+ if ifname in tmp:
+ del tmp[ifname]
+ if tmp: bond['member']['interface'][interface].update({'is_bond_member' : tmp})
# Check if member interface is used as source-interface on another interface
tmp = is_source_interface(conf, interface)
- if tmp: interface_config['is_source_interface'] = tmp
+ if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp})
# bond members must not have an assigned address
tmp = has_address_configured(conf, interface)
- if tmp: interface_config['has_address'] = {}
+ if tmp: bond['member']['interface'][interface].update({'has_address' : ''})
# bond members must not have a VRF attached
tmp = has_vrf_configured(conf, interface)
- if tmp: interface_config['has_vrf'] = {}
+ if tmp: bond['member']['interface'][interface].update({'has_vrf' : ''})
+
return bond
diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py
index 637db442a..95dcc543e 100755
--- a/src/conf_mode/interfaces_bridge.py
+++ b/src/conf_mode/interfaces_bridge.py
@@ -25,6 +25,7 @@ from vyos.configdict import has_vlan_subinterface_configured
from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_mtu_ipv6
from vyos.ifconfig import BridgeIf
from vyos.configdict import has_address_configured
from vyos.configdict import has_vrf_configured
@@ -74,8 +75,9 @@ def get_config(config=None):
for interface in list(bridge['member']['interface']):
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
- if tmp and bridge['ifname'] not in tmp:
- bridge['member']['interface'][interface].update({'is_bridge_member' : tmp})
+ if ifname in tmp:
+ del tmp[ifname]
+ if tmp: bridge['member']['interface'][interface].update({'is_bridge_member' : tmp})
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
@@ -135,6 +137,7 @@ def verify(bridge):
verify_dhcpv6(bridge)
verify_vrf(bridge)
+ verify_mtu_ipv6(bridge)
verify_mirror_redirect(bridge)
ifname = bridge['ifname']
diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py
index 007708d4a..1c5b4d0e7 100755
--- a/src/conf_mode/interfaces_geneve.py
+++ b/src/conf_mode/interfaces_geneve.py
@@ -47,7 +47,7 @@ def get_config(config=None):
# GENEVE interfaces are picky and require recreation if certain parameters
# change. But a GENEVE interface should - of course - not be re-created if
# it's description or IP address is adjusted. Feels somehow logic doesn't it?
- for cli_option in ['remote', 'vni', 'parameters']:
+ for cli_option in ['remote', 'vni', 'parameters', 'port']:
if is_node_changed(conf, base + [ifname, cli_option]):
geneve.update({'rebuild_required': {}})
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index 8c1213e2b..a9b4e570d 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -32,6 +32,7 @@ from vyos.base import DeprecationWarning
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
+from vyos.configdiff import get_config_diff
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mirror_redirect
@@ -94,6 +95,23 @@ def get_config(config=None):
if 'deleted' in openvpn:
return openvpn
+ if not is_node_changed(conf, base) and dict_search_args(openvpn, 'tls'):
+ diff = get_config_diff(conf)
+ if diff.get_child_nodes_diff(['pki'], recursive=True).get('add') == ['ca', 'certificate']:
+ crl_path = os.path.join(cfg_dir, f'{ifname}_crl.pem')
+ if os.path.exists(crl_path):
+ # do not restart service when changed only CRL and crl file already exist
+ openvpn.update({'no_restart_crl': True})
+ for rec in diff.get_child_nodes_diff(['pki', 'ca'], recursive=True).get('add'):
+ if diff.get_child_nodes_diff(['pki', 'ca', rec], recursive=True).get('add') != ['crl']:
+ openvpn.update({'no_restart_crl': False})
+ break
+ if openvpn.get('no_restart_crl'):
+ for rec in diff.get_child_nodes_diff(['pki', 'certificate'], recursive=True).get('add'):
+ if diff.get_child_nodes_diff(['pki', 'certificate', rec], recursive=True).get('add') != ['revoke']:
+ openvpn.update({'no_restart_crl': False})
+ break
+
if is_node_changed(conf, base + [ifname, 'openvpn-option']):
openvpn.update({'restart_required': {}})
if is_node_changed(conf, base + [ifname, 'enable-dco']):
@@ -786,10 +804,12 @@ def apply(openvpn):
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- action = 'reload-or-restart'
- if 'restart_required' in openvpn:
- action = 'restart'
- call(f'systemctl {action} openvpn@{interface}.service')
+
+ if not openvpn.get('no_restart_crl'):
+ action = 'reload-or-restart'
+ if 'restart_required' in openvpn:
+ action = 'restart'
+ call(f'systemctl {action} openvpn@{interface}.service')
o = VTunIf(**openvpn)
o.update(openvpn)
diff --git a/src/conf_mode/interfaces_pseudo-ethernet.py b/src/conf_mode/interfaces_pseudo-ethernet.py
index 446beffd3..b066fd542 100755
--- a/src/conf_mode/interfaces_pseudo-ethernet.py
+++ b/src/conf_mode/interfaces_pseudo-ethernet.py
@@ -27,6 +27,7 @@ from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_source_interface
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_mtu_parent
+from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import MACVLANIf
from vyos.utils.network import interface_exists
@@ -71,6 +72,7 @@ def verify(peth):
verify_vrf(peth)
verify_address(peth)
verify_mtu_parent(peth, peth['parent'])
+ verify_mtu_ipv6(peth)
verify_mirror_redirect(peth)
# use common function to verify VLAN configuration
verify_vlan_config(peth)
diff --git a/src/conf_mode/interfaces_tunnel.py b/src/conf_mode/interfaces_tunnel.py
index 98ef98d12..ee1436e49 100755
--- a/src/conf_mode/interfaces_tunnel.py
+++ b/src/conf_mode/interfaces_tunnel.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 yOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -13,9 +13,8 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
from sys import exit
-
+import ipaddress
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
@@ -89,6 +88,13 @@ def verify(tunnel):
raise ConfigError('Tunnel used for NHRP, it can not be deleted!')
return None
+ if 'nhrp' in tunnel:
+ if 'address' in tunnel:
+ address_list = dict_search('address', tunnel)
+ for tunip in address_list:
+ if ipaddress.ip_network(tunip, strict=False).prefixlen != 32:
+ raise ConfigError(
+ 'Tunnel is used for NHRP, Netmask should be /32!')
verify_tunnel(tunnel)
diff --git a/src/conf_mode/interfaces_virtual-ethernet.py b/src/conf_mode/interfaces_virtual-ethernet.py
index cb6104f59..59ce474fc 100755
--- a/src/conf_mode/interfaces_virtual-ethernet.py
+++ b/src/conf_mode/interfaces_virtual-ethernet.py
@@ -23,6 +23,7 @@ from vyos.configdict import get_interface_dict
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_mtu_ipv6
from vyos.ifconfig import VethIf
from vyos.utils.network import interface_exists
airbag.enable()
@@ -62,6 +63,7 @@ def verify(veth):
return None
verify_vrf(veth)
+ verify_mtu_ipv6(veth)
verify_address(veth)
if 'peer_name' not in veth:
diff --git a/src/conf_mode/interfaces_vti.py b/src/conf_mode/interfaces_vti.py
index 20629c6c1..915bde066 100755
--- a/src/conf_mode/interfaces_vti.py
+++ b/src/conf_mode/interfaces_vti.py
@@ -20,6 +20,7 @@ from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_mtu_ipv6
from vyos.ifconfig import VTIIf
from vyos import ConfigError
from vyos import airbag
@@ -40,6 +41,7 @@ def get_config(config=None):
def verify(vti):
verify_vrf(vti)
+ verify_mtu_ipv6(vti)
verify_mirror_redirect(vti)
return None
diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py
index 68646e8ff..256b65708 100755
--- a/src/conf_mode/interfaces_vxlan.py
+++ b/src/conf_mode/interfaces_vxlan.py
@@ -95,6 +95,8 @@ def verify(vxlan):
if 'group' in vxlan:
if 'source_interface' not in vxlan:
raise ConfigError('Multicast VXLAN requires an underlaying interface')
+ if 'remote' in vxlan:
+ raise ConfigError('Both group and remote cannot be specified')
verify_source_interface(vxlan)
if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan):
diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py
index b6fd6b0b2..3ca6ecdca 100755
--- a/src/conf_mode/interfaces_wireguard.py
+++ b/src/conf_mode/interfaces_wireguard.py
@@ -19,6 +19,9 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
+from vyos.configdict import is_source_interface
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
@@ -29,8 +32,10 @@ from vyos.ifconfig import WireGuardIf
from vyos.utils.kernel import check_kmod
from vyos.utils.network import check_port_availability
from vyos.utils.network import is_wireguard_key_pair
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
+from pathlib import Path
airbag.enable()
@@ -54,11 +59,31 @@ def get_config(config=None):
if is_node_changed(conf, base + [ifname, 'peer']):
wireguard.update({'rebuild_required': {}})
+ wireguard['peers_need_resolve'] = []
+ if 'peer' in wireguard:
+ for peer, peer_config in wireguard['peer'].items():
+ if 'disable' not in peer_config and 'host_name' in peer_config:
+ wireguard['peers_need_resolve'].append(peer)
+
+ # Check if interface is used as source-interface on VXLAN interface
+ tmp = is_source_interface(conf, ifname, 'vxlan')
+ if tmp:
+ if 'deleted' not in wireguard:
+ set_dependents('vxlan', conf, tmp)
+ else:
+ wireguard['is_source_interface'] = tmp
+
return wireguard
+
def verify(wireguard):
if 'deleted' in wireguard:
verify_bridge_delete(wireguard)
+ if 'is_source_interface' in wireguard:
+ raise ConfigError(
+ f'Interface "{wireguard["ifname"]}" cannot be deleted as it is used '
+ f'as source interface for "{wireguard["is_source_interface"]}"!'
+ )
return None
verify_mtu_ipv6(wireguard)
@@ -72,7 +97,7 @@ def verify(wireguard):
if 'port' in wireguard and 'port_changed' in wireguard:
listen_port = int(wireguard['port'])
- if check_port_availability('0.0.0.0', listen_port, 'udp') is not True:
+ if check_port_availability(None, listen_port, protocol='udp') is not True:
raise ConfigError(f'UDP port {listen_port} is busy or unavailable and '
'cannot be used for the interface!')
@@ -82,28 +107,41 @@ def verify(wireguard):
for tmp in wireguard['peer']:
peer = wireguard['peer'][tmp]
+ base_error = f'WireGuard peer "{tmp}":'
+
+ if 'host_name' in peer and 'address' in peer:
+ raise ConfigError(f'{base_error} address/host-name are mutually exclusive!')
+
if 'allowed_ips' not in peer:
- raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
+ raise ConfigError(f'{base_error} missing mandatory allowed-ips!')
if 'public_key' not in peer:
- raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
-
- if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
- raise ConfigError('Both Wireguard port and address must be defined '
- f'for peer "{tmp}" if either one of them is set!')
+ raise ConfigError(f'{base_error} missing mandatory public-key!')
if peer['public_key'] in public_keys:
- raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"')
+ raise ConfigError(f'{base_error} duplicate public-key!')
if 'disable' not in peer:
if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']):
- raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"')
+ tmp = wireguard["ifname"]
+ raise ConfigError(f'{base_error} identical public key as interface "{tmp}"!')
+
+ port_addr_error = f'{base_error} both port and address/host-name must '\
+ 'be defined if either one of them is set!'
+ if 'port' not in peer:
+ if 'host_name' in peer or 'address' in peer:
+ raise ConfigError(port_addr_error)
+ else:
+ if 'host_name' not in peer and 'address' not in peer:
+ raise ConfigError(port_addr_error)
public_keys.append(peer['public_key'])
+
def generate(wireguard):
return None
+
def apply(wireguard):
check_kmod('wireguard')
@@ -122,8 +160,28 @@ def apply(wireguard):
wg = WireGuardIf(**wireguard)
wg.update(wireguard)
+ domain_resolver_usage = '/run/use-vyos-domain-resolver-interfaces-wireguard-' + wireguard['ifname']
+
+ ## DOMAIN RESOLVER
+ domain_action = 'restart'
+ if 'peers_need_resolve' in wireguard and len(wireguard['peers_need_resolve']) > 0 and 'disable' not in wireguard:
+ from vyos.utils.file import write_file
+
+ text = f'# Automatically generated by interfaces_wireguard.py\nThis file indicates that vyos-domain-resolver service is used by the interfaces_wireguard.\n'
+ text += "intefaces:\n" + "".join([f" - {peer}\n" for peer in wireguard['peers_need_resolve']])
+ Path(domain_resolver_usage).write_text(text)
+ write_file(domain_resolver_usage, text)
+ else:
+ Path(domain_resolver_usage).unlink(missing_ok=True)
+ if not Path('/run').glob('use-vyos-domain-resolver*'):
+ domain_action = 'stop'
+ call(f'systemctl {domain_action} vyos-domain-resolver.service')
+
+ call_dependents()
+
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/interfaces_wwan.py b/src/conf_mode/interfaces_wwan.py
index 230eb14d6..ddbebfb4a 100755
--- a/src/conf_mode/interfaces_wwan.py
+++ b/src/conf_mode/interfaces_wwan.py
@@ -26,6 +26,7 @@ from vyos.configverify import verify_authentication
from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_mtu_ipv6
from vyos.ifconfig import WWANIf
from vyos.utils.dict import dict_search
from vyos.utils.process import cmd
@@ -98,6 +99,7 @@ def verify(wwan):
verify_interface_exists(wwan, ifname)
verify_authentication(wwan)
verify_vrf(wwan)
+ verify_mtu_ipv6(wwan)
verify_mirror_redirect(wwan)
return None
diff --git a/src/conf_mode/load-balancing_haproxy.py b/src/conf_mode/load-balancing_haproxy.py
index 45042dd52..504a90596 100644
--- a/src/conf_mode/load-balancing_haproxy.py
+++ b/src/conf_mode/load-balancing_haproxy.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright (C) 2023-2025 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 os
from sys import exit
from shutil import rmtree
+from vyos.defaults import systemd_services
from vyos.config import Config
from vyos.configverify import verify_pki_certificate
from vyos.configverify import verify_pki_ca_certificate
@@ -39,7 +40,6 @@ airbag.enable()
load_balancing_dir = '/run/haproxy'
load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg'
-systemd_service = 'haproxy.service'
systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf'
def get_config(config=None):
@@ -65,29 +65,39 @@ def verify(lb):
return None
if 'backend' not in lb or 'service' not in lb:
- raise ConfigError(f'"service" and "backend" must be configured!')
+ raise ConfigError('Both "service" and "backend" must be configured!')
for front, front_config in lb['service'].items():
if 'port' not in front_config:
raise ConfigError(f'"{front} service port" must be configured!')
# Check if bind address:port are used by another service
- tmp_address = front_config.get('address', '0.0.0.0')
+ tmp_address = front_config.get('address', None)
tmp_port = front_config['port']
if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \
not is_listen_port_bind_service(int(tmp_port), 'haproxy'):
- raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service')
+ raise ConfigError(f'TCP port "{tmp_port}" is used by another service')
+
+ if 'http_compression' in front_config:
+ if front_config['mode'] != 'http':
+ raise ConfigError(f'service {front} must be set to http mode to use http-compression!')
+ if len(front_config['http_compression']['mime_type']) == 0:
+ raise ConfigError(f'service {front} must have at least one mime-type configured to use'
+ f'http_compression!')
+
+ for cert in dict_search('ssl.certificate', front_config) or []:
+ verify_pki_certificate(lb, cert)
for back, back_config in lb['backend'].items():
if 'http_check' in back_config:
http_check = back_config['http_check']
if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']:
- raise ConfigError(f'"expect status" and "expect string" can not be configured together!')
+ raise ConfigError('"expect status" and "expect string" can not be configured together!')
if 'health_check' in back_config:
if back_config['mode'] != 'tcp':
raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' +
- f'health-check whilst in TCP mode!')
+ 'health-check whilst in TCP mode!')
if 'http_check' in back_config:
raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!')
@@ -105,20 +115,15 @@ def verify(lb):
if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']):
raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!')
+ tmp = dict_search('ssl.ca_certificate', back_config)
+ if tmp: verify_pki_ca_certificate(lb, tmp)
+
# Check if http-response-headers are configured in any frontend/backend where mode != http
for group in ['service', 'backend']:
for config_name, config in lb[group].items():
if 'http_response_headers' in config and config['mode'] != 'http':
raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!')
- for front, front_config in lb['service'].items():
- for cert in dict_search('ssl.certificate', front_config) or []:
- verify_pki_certificate(lb, cert)
-
- for back, back_config in lb['backend'].items():
- tmp = dict_search('ssl.ca_certificate', back_config)
- if tmp: verify_pki_ca_certificate(lb, tmp)
-
def generate(lb):
if not lb:
@@ -186,12 +191,11 @@ def generate(lb):
return None
def apply(lb):
+ action = 'stop'
+ if lb:
+ action = 'reload-or-restart'
call('systemctl daemon-reload')
- if not lb:
- call(f'systemctl stop {systemd_service}')
- else:
- call(f'systemctl reload-or-restart {systemd_service}')
-
+ call(f'systemctl {action} {systemd_services["haproxy"]}')
return None
diff --git a/src/conf_mode/load-balancing_wan.py b/src/conf_mode/load-balancing_wan.py
index 5da0b906b..92d9acfba 100755
--- a/src/conf_mode/load-balancing_wan.py
+++ b/src/conf_mode/load-balancing_wan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright (C) 2023-2025 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,24 +14,16 @@
# 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
-
from sys import exit
-from shutil import rmtree
-from vyos.base import Warning
from vyos.config import Config
from vyos.configdep import set_dependents, call_dependents
from vyos.utils.process import cmd
-from vyos.template import render
from vyos import ConfigError
from vyos import airbag
airbag.enable()
-load_balancing_dir = '/run/load-balance'
-load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf'
-systemd_service = 'vyos-wan-load-balance.service'
-
+service = 'vyos-wan-load-balance.service'
def get_config(config=None):
if config:
@@ -40,6 +32,7 @@ def get_config(config=None):
conf = Config()
base = ['load-balancing', 'wan']
+
lb = conf.get_config_dict(base, key_mangling=('-', '_'),
no_tag_node_value_mangle=True,
get_first_key=True,
@@ -59,87 +52,61 @@ def verify(lb):
if not lb:
return None
- if 'interface_health' not in lb:
- raise ConfigError(
- 'A valid WAN load-balance configuration requires an interface with a nexthop!'
- )
-
- for interface, interface_config in lb['interface_health'].items():
- if 'nexthop' not in interface_config:
- raise ConfigError(
- f'interface-health {interface} nexthop must be specified!')
-
- if 'test' in interface_config:
- for test_rule, test_config in interface_config['test'].items():
- if 'type' in test_config:
- if test_config['type'] == 'user-defined' and 'test_script' not in test_config:
- raise ConfigError(
- f'test {test_rule} script must be defined for test-script!'
- )
-
- if 'rule' not in lb:
- Warning(
- 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!'
- )
+ if 'interface_health' in lb:
+ for ifname, health_conf in lb['interface_health'].items():
+ if 'nexthop' not in health_conf:
+ raise ConfigError(f'Nexthop must be configured for interface {ifname}')
+
+ if 'test' not in health_conf:
+ continue
+
+ for test_id, test_conf in health_conf['test'].items():
+ if 'type' not in test_conf:
+ raise ConfigError(f'No type configured for health test on interface {ifname}')
+
+ if test_conf['type'] == 'user-defined' and 'test_script' not in test_conf:
+ raise ConfigError(f'Missing user-defined script for health test on interface {ifname}')
else:
- for rule, rule_config in lb['rule'].items():
- if 'inbound_interface' not in rule_config:
- raise ConfigError(f'rule {rule} inbound-interface must be specified!')
- if {'failover', 'exclude'} <= set(rule_config):
- raise ConfigError(f'rule {rule} failover cannot be configured with exclude!')
- if {'limit', 'exclude'} <= set(rule_config):
- raise ConfigError(f'rule {rule} limit cannot be used with exclude!')
- if 'interface' not in rule_config:
- if 'exclude' not in rule_config:
- Warning(
- f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule'
- )
- for direction in {'source', 'destination'}:
- if direction in rule_config:
- if 'protocol' in rule_config and 'port' in rule_config[
- direction]:
- if rule_config['protocol'] not in {'tcp', 'udp'}:
- raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"')
+ raise ConfigError('Interface health tests must be configured')
+ if 'rule' in lb:
+ for rule_id, rule_conf in lb['rule'].items():
+ if 'interface' not in rule_conf and 'exclude' not in rule_conf:
+ raise ConfigError(f'Interface or exclude not specified on load-balancing wan rule {rule_id}')
-def generate(lb):
- if not lb:
- # Delete /run/load-balance/wlb.conf
- if os.path.isfile(load_balancing_conf_file):
- os.unlink(load_balancing_conf_file)
- # Delete old directories
- if os.path.isdir(load_balancing_dir):
- rmtree(load_balancing_dir, ignore_errors=True)
- if os.path.exists('/var/run/load-balance/wlb.out'):
- os.unlink('/var/run/load-balance/wlb.out')
+ if 'failover' in rule_conf and 'exclude' in rule_conf:
+ raise ConfigError(f'Failover cannot be configured with exclude on load-balancing wan rule {rule_id}')
- return None
+ if 'limit' in rule_conf:
+ if 'exclude' in rule_conf:
+ raise ConfigError(f'Limit cannot be configured with exclude on load-balancing wan rule {rule_id}')
- # Create load-balance dir
- if not os.path.isdir(load_balancing_dir):
- os.mkdir(load_balancing_dir)
+ if 'rate' in rule_conf['limit'] and 'period' not in rule_conf['limit']:
+ raise ConfigError(f'Missing "limit period" on load-balancing wan rule {rule_id}')
- render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb)
+ if 'period' in rule_conf['limit'] and 'rate' not in rule_conf['limit']:
+ raise ConfigError(f'Missing "limit rate" on load-balancing wan rule {rule_id}')
- return None
+ for direction in ['source', 'destination']:
+ if direction in rule_conf:
+ if 'port' in rule_conf[direction]:
+ if 'protocol' not in rule_conf:
+ raise ConfigError(f'Protocol required to specify port on load-balancing wan rule {rule_id}')
+
+ if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+ raise ConfigError(f'Protocol must be tcp, udp or tcp_udp to specify port on load-balancing wan rule {rule_id}')
+def generate(lb):
+ return None
def apply(lb):
if not lb:
- try:
- cmd(f'systemctl stop {systemd_service}')
- except Exception as e:
- print(f"Error message: {e}")
-
+ cmd(f'sudo systemctl stop {service}')
else:
- cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1')
- cmd(f'systemctl restart {systemd_service}')
+ cmd(f'sudo systemctl restart {service}')
call_dependents()
- return None
-
-
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 98b2f3f29..504b3e82a 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -17,6 +17,7 @@
import os
from sys import exit
+from pathlib import Path
from vyos.base import Warning
from vyos.config import Config
@@ -43,7 +44,6 @@ k_mod = ['nft_nat', 'nft_chain_nat']
nftables_nat_config = '/run/nftables_nat.conf'
nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft'
domain_resolver_usage = '/run/use-vyos-domain-resolver-nat'
-domain_resolver_usage_firewall = '/run/use-vyos-domain-resolver-firewall'
valid_groups = [
'address_group',
@@ -265,9 +265,9 @@ def apply(nat):
text = f'# Automatically generated by nat.py\nThis file indicates that vyos-domain-resolver service is used by nat.\n'
write_file(domain_resolver_usage, text)
elif os.path.exists(domain_resolver_usage):
- os.unlink(domain_resolver_usage)
- if not os.path.exists(domain_resolver_usage_firewall):
- # Firewall not using domain resolver
+ Path(domain_resolver_usage).unlink(missing_ok=True)
+
+ if not Path('/run').glob('use-vyos-domain-resolver*'):
domain_action = 'stop'
call(f'systemctl {domain_action} vyos-domain-resolver.service')
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index 95dfae3a5..c65950c9e 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -92,6 +92,10 @@ def verify(nat):
if prefix != None:
if not is_ipv6(prefix):
raise ConfigError(f'{err_msg} source-prefix not specified')
+
+ if 'destination' in config and 'group' in config['destination']:
+ if len({'address_group', 'network_group', 'domain_group'} & set(config['destination']['group'])) > 1:
+ raise ConfigError('Only one address-group, network-group or domain-group can be specified')
if dict_search('destination.rule', nat):
for rule, config in dict_search('destination.rule', nat).items():
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index acea2c9be..869518dd9 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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 os
from sys import argv
from sys import exit
+from vyos.base import Message
from vyos.config import Config
from vyos.config import config_dict_merge
from vyos.configdep import set_dependents
@@ -27,6 +28,8 @@ from vyos.configdict import node_changed
from vyos.configdiff import Diff
from vyos.configdiff import get_config_diff
from vyos.defaults import directories
+from vyos.defaults import internal_ports
+from vyos.defaults import systemd_services
from vyos.pki import encode_certificate
from vyos.pki import is_ca_certificate
from vyos.pki import load_certificate
@@ -42,9 +45,11 @@ from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
from vyos.utils.file import read_file
+from vyos.utils.network import check_port_availability
from vyos.utils.process import call
from vyos.utils.process import cmd
from vyos.utils.process import is_systemd_service_active
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -128,8 +133,20 @@ def certbot_request(name: str, config: dict, dry_run: bool=True):
f'--standalone --agree-tos --no-eff-email --expand --server {config["url"]} '\
f'--email {config["email"]} --key-type rsa --rsa-key-size {config["rsa_key_size"]} '\
f'{domains}'
+
+ listen_address = None
if 'listen_address' in config:
- tmp += f' --http-01-address {config["listen_address"]}'
+ listen_address = config['listen_address']
+
+ # When ACME is used behind a reverse proxy, we always bind to localhost
+ # whatever the CLI listen-address is configured for.
+ if ('haproxy' in dict_search('used_by', config) and
+ is_systemd_service_running(systemd_services['haproxy']) and
+ not check_port_availability(listen_address, 80)):
+ tmp += f' --http-01-address 127.0.0.1 --http-01-port {internal_ports["certbot_haproxy"]}'
+ elif listen_address:
+ tmp += f' --http-01-address {listen_address}'
+
# verify() does not need to actually request a cert but only test for plausability
if dry_run:
tmp += ' --dry-run'
@@ -150,14 +167,18 @@ def get_config(config=None):
if len(argv) > 1 and argv[1] == 'certbot_renew':
pki['certbot_renew'] = {}
- changed_keys = ['ca', 'certificate', 'dh', 'key-pair', 'openssh', 'openvpn']
+ # Walk through the list of sync_translate mapping and build a list
+ # which is later used to check if the node was changed in the CLI config
+ changed_keys = []
+ for value in sync_translate.values():
+ if value not in changed_keys:
+ changed_keys.append(value)
+ # Check for changes to said given keys in the CLI config
for key in changed_keys:
tmp = node_changed(conf, base + [key], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD)
-
if 'changed' not in pki:
pki.update({'changed':{}})
-
pki['changed'].update({key.replace('-', '_') : tmp})
# We only merge on the defaults of there is a configuration at all
@@ -219,8 +240,8 @@ def get_config(config=None):
continue
path = search['path']
- path_str = ' '.join(path + found_path)
- #print(f'PKI: Updating config: {path_str} {item_name}')
+ path_str = ' '.join(path + found_path).replace('_','-')
+ Message(f'Updating configuration: "{path_str} {item_name}"')
if path[0] == 'interfaces':
ifname = found_path[0]
@@ -230,6 +251,24 @@ def get_config(config=None):
if not D.node_changed_presence(path):
set_dependents(path[1], conf)
+ # Check PKI certificates if they are auto-generated by ACME. If they are,
+ # traverse the current configuration and determine the service where the
+ # certificate is used by.
+ # Required to check if we might need to run certbot behing a reverse proxy.
+ if 'certificate' in pki:
+ for name, cert_config in pki['certificate'].items():
+ if 'acme' not in cert_config:
+ continue
+ if not dict_search('system.load_balancing.haproxy', pki):
+ continue
+ used_by = []
+ for cert_list, _ in dict_search_recursive(
+ pki['system']['load_balancing']['haproxy'], 'certificate'):
+ if name in cert_list:
+ used_by.append('haproxy')
+ if used_by:
+ pki['certificate'][name]['acme'].update({'used_by': used_by})
+
return pki
def is_valid_certificate(raw_data):
@@ -321,6 +360,15 @@ def verify(pki):
raise ConfigError(f'An email address is required to request '\
f'certificate for "{name}" via ACME!')
+ listen_address = None
+ if 'listen_address' in cert_conf['acme']:
+ listen_address = cert_conf['acme']['listen_address']
+
+ if 'used_by' not in cert_conf['acme']:
+ if not check_port_availability(listen_address, 80):
+ raise ConfigError('Port 80 is already in use and not available '\
+ f'to provide ACME challenge for "{name}"!')
+
if 'certbot_renew' not in pki:
# Only run the ACME command if something on this entity changed,
# as this is time intensive
@@ -374,27 +422,35 @@ def verify(pki):
for search in sync_search:
for key in search['keys']:
changed_key = sync_translate[key]
-
if changed_key not in pki['changed']:
continue
-
for item_name in pki['changed'][changed_key]:
node_present = False
if changed_key == 'openvpn':
node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name)
else:
node_present = dict_search_args(pki, changed_key, item_name)
+ # If the node is still present, we can skip the check
+ # as we are not deleting it
+ if node_present:
+ continue
- if not node_present:
- search_dict = dict_search_args(pki['system'], *search['path'])
-
- if not search_dict:
- continue
+ search_dict = dict_search_args(pki['system'], *search['path'])
+ if not search_dict:
+ continue
- for found_name, found_path in dict_search_recursive(search_dict, key):
- if found_name == item_name:
- path_str = " ".join(search['path'] + found_path)
- raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"')
+ for found_name, found_path in dict_search_recursive(search_dict, key):
+ # Check if the name matches either by string compare, or beeing
+ # part of a list
+ if ((isinstance(found_name, str) and found_name == item_name) or
+ (isinstance(found_name, list) and item_name in found_name)):
+ # We do not support _ in CLI paths - this is only a convenience
+ # as we mangle all - to _, now it's time to reverse this!
+ path_str = ' '.join(search['path'] + found_path).replace('_','-')
+ object = changed_key.replace('_','-')
+ tmp = f'Embedded PKI {object} with name "{item_name}" is still '\
+ f'in use by CLI path "{path_str}"'
+ raise ConfigError(tmp)
return None
@@ -440,13 +496,21 @@ def generate(pki):
for name, cert_conf in pki['certificate'].items():
if 'acme' in cert_conf:
certbot_list.append(name)
- # generate certificate if not found on disk
+ # There is no ACME/certbot managed certificate presend on the
+ # system, generate it
if name not in certbot_list_on_disk:
certbot_request(name, cert_conf['acme'], dry_run=False)
+ # Now that the certificate was properly generated we have
+ # the PEM files on disk. We need to add the certificate to
+ # certbot_list_on_disk to automatically import the CA chain
+ certbot_list_on_disk.append(name)
+ # We alredy had an ACME managed certificate on the system, but
+ # something changed in the configuration
elif changed_certificates != None and name in changed_certificates:
- # when something for the certificate changed, we should delete it
+ # Delete old ACME certificate first
if name in certbot_list_on_disk:
certbot_delete(name)
+ # Request new certificate via certbot
certbot_request(name, cert_conf['acme'], dry_run=False)
# Cleanup certbot configuration and certificates if no longer in use by CLI
@@ -482,7 +546,7 @@ def generate(pki):
if not ca_cert_present:
tmp = dict_search_args(pki, 'ca', f'{autochain_prefix}{cert}', 'certificate')
if not bool(tmp) or tmp != cert_chain_base64:
- print(f'Adding/replacing automatically imported CA certificate for "{cert}" ...')
+ Message(f'Add/replace automatically imported CA certificate for "{cert}"...')
add_cli_node(['pki', 'ca', f'{autochain_prefix}{cert}', 'certificate'], value=cert_chain_base64)
return None
diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py
index 223175b8a..521764896 100755
--- a/src/conf_mode/policy_route.py
+++ b/src/conf_mode/policy_route.py
@@ -21,13 +21,16 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
+from vyos.configdiff import get_config_diff, Diff
from vyos.template import render
from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.utils.network import get_vrf_tableid
from vyos.defaults import rt_global_table
from vyos.defaults import rt_global_vrf
+from vyos.firewall import geoip_update
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -43,6 +46,43 @@ valid_groups = [
'interface_group'
]
+def geoip_updated(conf, policy):
+ diff = get_config_diff(conf)
+ node_diff = diff.get_child_nodes_diff(['policy'], expand_nodes=Diff.DELETE, recursive=True)
+
+ out = {
+ 'name': [],
+ 'ipv6_name': [],
+ 'deleted_name': [],
+ 'deleted_ipv6_name': []
+ }
+ updated = False
+
+ for key, path in dict_search_recursive(policy, 'geoip'):
+ set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}'
+ if (path[0] == 'route'):
+ out['name'].append(set_name)
+ elif (path[0] == 'route6'):
+ set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}'
+ out['ipv6_name'].append(set_name)
+
+ updated = True
+
+ if 'delete' in node_diff:
+ for key, path in dict_search_recursive(node_diff['delete'], 'geoip'):
+ set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}'
+ if (path[0] == 'route'):
+ out['deleted_name'].append(set_name)
+ elif (path[0] == 'route6'):
+ set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}'
+ out['deleted_ipv6_name'].append(set_name)
+ updated = True
+
+ if updated:
+ return out
+
+ return False
+
def get_config(config=None):
if config:
conf = config
@@ -60,6 +100,7 @@ def get_config(config=None):
if 'dynamic_group' in policy['firewall_group']:
del policy['firewall_group']['dynamic_group']
+ policy['geoip_updated'] = geoip_updated(conf, policy)
return policy
def verify_rule(policy, name, rule_conf, ipv6, rule_id):
@@ -203,6 +244,12 @@ def apply(policy):
apply_table_marks(policy)
+ if policy['geoip_updated']:
+ # Call helper script to Update set contents
+ if 'name' in policy['geoip_updated'] or 'ipv6_name' in policy['geoip_updated']:
+ print('Updating GeoIP. Please wait...')
+ geoip_update(policy=policy)
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index c4af717af..99d8eb9d1 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -413,15 +413,19 @@ def verify(config_dict):
verify_route_map(afi_config['route_map'][tmp], bgp)
if 'route_reflector_client' in afi_config:
- peer_group_as = peer_config.get('remote_as')
+ peer_as = peer_config.get('remote_as')
- if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):
+ if peer_as is not None and (peer_as != 'internal' and peer_as != bgp['system_as']):
raise ConfigError('route-reflector-client only supported for iBGP peers')
else:
+ # Check into the peer group for the remote as, if we are in a peer group, check in peer itself
if 'peer_group' in peer_config:
peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp)
- 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')
+ elif neighbor == 'peer_group':
+ 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')
# T5833 not all AFIs are supported for VRF
if 'vrf' in bgp and 'address_family' in peer_config:
@@ -523,12 +527,21 @@ def verify(config_dict):
raise ConfigError(
'Please unconfigure import vrf commands before using vpn commands in dependent VRFs!')
+ if (dict_search('route_map.vrf.import', afi_config) is not None
+ or dict_search('import.vrf', afi_config) is not None):
# FRR error: please unconfigure vpn to vrf commands before
# using import vrf commands
- if 'vpn' in afi_config['import'] or dict_search('export.vpn', afi_config) != None:
+ if (dict_search('import.vpn', afi_config) is not None
+ or dict_search('export.vpn', afi_config) is not None):
raise ConfigError('Please unconfigure VPN to VRF commands before '\
'using "import vrf" commands!')
+ if (dict_search('route_map.vpn.import', afi_config) is not None
+ or dict_search('route_map.vpn.export', afi_config) is not None) :
+ raise ConfigError('Please unconfigure route-map VPN to VRF commands before '\
+ 'using "import vrf" commands!')
+
+
# Verify that the export/import route-maps do exist
for export_import in ['export', 'import']:
tmp = dict_search(f'route_map.vpn.{export_import}', afi_config)
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
index 0bd68b7d8..ac92c9d99 100755
--- a/src/conf_mode/protocols_nhrp.py
+++ b/src/conf_mode/protocols_nhrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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,95 +14,112 @@
# 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
+from sys import exit
+from sys import argv
+import ipaddress
from vyos.config import Config
-from vyos.configdict import node_changed
from vyos.template import render
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.utils.process import run
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
+from vyos.utils.process import is_systemd_service_running
+
airbag.enable()
-opennhrp_conf = '/run/opennhrp/opennhrp.conf'
+nflog_redirect = 1
+nflog_multicast = 2
nhrp_nftables_conf = '/run/nftables_nhrp.conf'
+
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['protocols', 'nhrp']
-
- nhrp = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
- nhrp['del_tunnels'] = node_changed(conf, base + ['tunnel'])
-
- if not conf.exists(base):
- return nhrp
- nhrp['if_tunnel'] = conf.get_config_dict(['interfaces', 'tunnel'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ return get_frrender_dict(conf, argv)
- nhrp['profile_map'] = {}
- profile = conf.get_config_dict(['vpn', 'ipsec', 'profile'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
- for name, profile_conf in profile.items():
- if 'bind' in profile_conf and 'tunnel' in profile_conf['bind']:
- interfaces = profile_conf['bind']['tunnel']
- if isinstance(interfaces, str):
- interfaces = [interfaces]
- for interface in interfaces:
- nhrp['profile_map'][interface] = name
-
- return nhrp
-
-def verify(nhrp):
- if 'tunnel' in nhrp:
- for name, nhrp_conf in nhrp['tunnel'].items():
- if not nhrp['if_tunnel'] or name not in nhrp['if_tunnel']:
+def verify(config_dict):
+ if not config_dict or 'deleted' in config_dict:
+ return None
+ if 'tunnel' in config_dict:
+ for name, nhrp_conf in config_dict['tunnel'].items():
+ if not config_dict['if_tunnel'] or name not in config_dict['if_tunnel']:
raise ConfigError(f'Tunnel interface "{name}" does not exist')
- tunnel_conf = nhrp['if_tunnel'][name]
+ tunnel_conf = config_dict['if_tunnel'][name]
+ if 'address' in tunnel_conf:
+ address_list = dict_search('address', tunnel_conf)
+ for tunip in address_list:
+ if ipaddress.ip_network(tunip,
+ strict=False).prefixlen != 32:
+ raise ConfigError(
+ f'Tunnel {name} is used for NHRP, Netmask should be /32!')
if 'encapsulation' not in tunnel_conf or tunnel_conf['encapsulation'] != 'gre':
raise ConfigError(f'Tunnel "{name}" is not an mGRE tunnel')
+ if 'network_id' not in nhrp_conf:
+ raise ConfigError(f'network-id is not specified in tunnel "{name}"')
+
if 'remote' in tunnel_conf:
raise ConfigError(f'Tunnel "{name}" cannot have a remote address defined')
- if 'map' in nhrp_conf:
- for map_name, map_conf in nhrp_conf['map'].items():
- if 'nbma_address' not in map_conf:
+ map_tunnelip = dict_search('map.tunnel_ip', nhrp_conf)
+ if map_tunnelip:
+ for map_name, map_conf in map_tunnelip.items():
+ if 'nbma' not in map_conf:
raise ConfigError(f'nbma-address missing on map {map_name} on tunnel {name}')
- if 'dynamic_map' in nhrp_conf:
- for map_name, map_conf in nhrp_conf['dynamic_map'].items():
- if 'nbma_domain_name' not in map_conf:
- raise ConfigError(f'nbma-domain-name missing on dynamic-map {map_name} on tunnel {name}')
+ nhs_tunnelip = dict_search('nhs.tunnel_ip', nhrp_conf)
+ nbma_list = []
+ if nhs_tunnelip:
+ for nhs_name, nhs_conf in nhs_tunnelip.items():
+ if 'nbma' not in nhs_conf:
+ raise ConfigError(f'nbma-address missing on map nhs {nhs_name} on tunnel {name}')
+ if nhs_name != 'dynamic':
+ if len(list(dict_search('nbma', nhs_conf))) > 1:
+ raise ConfigError(
+ f'Static nhs tunnel-ip {nhs_name} cannot contain multiple nbma-addresses')
+ for nbma_ip in dict_search('nbma', nhs_conf):
+ if nbma_ip not in nbma_list:
+ nbma_list.append(nbma_ip)
+ else:
+ raise ConfigError(
+ f'Nbma address {nbma_ip} cannot be maped to several tunnel-ip')
return None
-def generate(nhrp):
- if not os.path.exists(nhrp_nftables_conf):
- nhrp['first_install'] = True
- render(opennhrp_conf, 'nhrp/opennhrp.conf.j2', nhrp)
- render(nhrp_nftables_conf, 'nhrp/nftables.conf.j2', nhrp)
+def generate(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'nhrp'):
+ return None
+
+ if 'deleted' in config_dict['nhrp']:
+ return None
+ render(nhrp_nftables_conf, 'frr/nhrpd_nftables.conf.j2', config_dict['nhrp'])
+
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(nhrp):
+
+def apply(config_dict):
+
nft_rc = run(f'nft --file {nhrp_nftables_conf}')
if nft_rc != 0:
raise ConfigError('Failed to apply NHRP tunnel firewall rules')
- action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop'
- service_rc = run(f'systemctl {action} opennhrp.service')
- if service_rc != 0:
- raise ConfigError(f'Failed to {action} the NHRP service')
-
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
+
if __name__ == '__main__':
try:
c = get_config()
@@ -112,3 +129,4 @@ if __name__ == '__main__':
except ConfigError as e:
print(e)
exit(1)
+
diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py
index b112add3f..b83c6dfb1 100755
--- a/src/conf_mode/service_console-server.py
+++ b/src/conf_mode/service_console-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -98,6 +98,12 @@ def generate(proxy):
return None
def apply(proxy):
+ if not os.path.exists('/etc/dropbear/dropbear_rsa_host_key'):
+ call('dropbearkey -t rsa -s 4096 -f /etc/dropbear/dropbear_rsa_host_key')
+
+ if not os.path.exists('/etc/dropbear/dropbear_ecdsa_host_key'):
+ call('dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key')
+
call('systemctl daemon-reload')
call('systemctl stop dropbear@*.service conserver-server.service')
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index 9c59aa63d..99c7e6a1f 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -38,20 +38,21 @@ from vyos.utils.network import is_subnet_connected
from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
+
airbag.enable()
-ctrl_config_file = '/run/kea/kea-ctrl-agent.conf'
ctrl_socket = '/run/kea/dhcp4-ctrl-socket'
config_file = '/run/kea/kea-dhcp4.conf'
+config_file_d2 = '/run/kea/kea-dhcp-ddns.conf'
lease_file = '/config/dhcp/dhcp4-leases.csv'
lease_file_glob = '/config/dhcp/dhcp4-leases*'
-systemd_override = r'/run/systemd/system/kea-ctrl-agent.service.d/10-override.conf'
user_group = '_kea'
ca_cert_file = '/run/kea/kea-failover-ca.pem'
cert_file = '/run/kea/kea-failover.pem'
cert_key_file = '/run/kea/kea-failover-key.pem'
+
def dhcp_slice_range(exclude_list, range_dict):
"""
This function is intended to slice a DHCP range. What does it mean?
@@ -74,19 +75,17 @@ def dhcp_slice_range(exclude_list, range_dict):
range_last_exclude = ''
for e in exclude_list:
- if (ip_address(e) >= ip_address(range_start)) and \
- (ip_address(e) <= ip_address(range_stop)):
+ if (ip_address(e) >= ip_address(range_start)) and (
+ ip_address(e) <= ip_address(range_stop)
+ ):
range_last_exclude = e
for e in exclude_list:
- if (ip_address(e) >= ip_address(range_start)) and \
- (ip_address(e) <= ip_address(range_stop)):
-
+ if (ip_address(e) >= ip_address(range_start)) and (
+ ip_address(e) <= ip_address(range_stop)
+ ):
# Build new address range ending one address before exclude address
- r = {
- 'start' : range_start,
- 'stop' : str(ip_address(e) -1)
- }
+ r = {'start': range_start, 'stop': str(ip_address(e) - 1)}
if 'option' in range_dict:
r['option'] = range_dict['option']
@@ -104,10 +103,7 @@ def dhcp_slice_range(exclude_list, range_dict):
# Take care of last IP address range spanning from the last exclude
# address (+1) to the end of the initial configured range
if ip_address(e) == ip_address(range_last_exclude):
- r = {
- 'start': str(ip_address(e) + 1),
- 'stop': str(range_stop)
- }
+ r = {'start': str(ip_address(e) + 1), 'stop': str(range_stop)}
if 'option' in range_dict:
r['option'] = range_dict['option']
@@ -115,14 +111,15 @@ def dhcp_slice_range(exclude_list, range_dict):
if not (ip_address(r['start']) > ip_address(r['stop'])):
output.append(r)
else:
- # if the excluded address was not part of the range, we simply return
- # the entire ranga again
- if not range_last_exclude:
- if range_dict not in output:
- output.append(range_dict)
+ # if the excluded address was not part of the range, we simply return
+ # the entire ranga again
+ if not range_last_exclude:
+ if range_dict not in output:
+ output.append(range_dict)
return output
+
def get_config(config=None):
if config:
conf = config
@@ -132,10 +129,13 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dhcp = conf.get_config_dict(base, key_mangling=('-', '_'),
- no_tag_node_value_mangle=True,
- get_first_key=True,
- with_recursive_defaults=True)
+ dhcp = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True,
+ )
if 'shared_network_name' in dhcp:
for network, network_config in dhcp['shared_network_name'].items():
@@ -147,22 +147,40 @@ def get_config(config=None):
new_range_id = 0
new_range_dict = {}
for r, r_config in subnet_config['range'].items():
- for slice in dhcp_slice_range(subnet_config['exclude'], r_config):
- new_range_dict.update({new_range_id : slice})
- new_range_id +=1
+ for slice in dhcp_slice_range(
+ subnet_config['exclude'], r_config
+ ):
+ new_range_dict.update({new_range_id: slice})
+ new_range_id += 1
dhcp['shared_network_name'][network]['subnet'][subnet].update(
- {'range' : new_range_dict})
+ {'range': new_range_dict}
+ )
if len(dhcp['high_availability']) == 1:
## only default value for mode is set, need to remove ha node
del dhcp['high_availability']
else:
if dict_search('high_availability.certificate', dhcp):
- dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
+ dhcp['pki'] = conf.get_config_dict(
+ ['pki'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
return dhcp
+def verify_ddns_domain_servers(domain_type, domain):
+ if 'dns_server' in domain:
+ invalid_servers = []
+ for server_no, server_config in domain['dns_server'].items():
+ if 'address' not in server_config:
+ invalid_servers.append(server_no)
+ if len(invalid_servers) > 0:
+ raise ConfigError(f'{domain_type} DNS servers {", ".join(invalid_servers)} in DDNS configuration need to have an IP address')
+ return None
+
def verify(dhcp):
# bail out early - looks like removal from running config
if not dhcp or 'disable' in dhcp:
@@ -170,13 +188,15 @@ def verify(dhcp):
# If DHCP is enabled we need one share-network
if 'shared_network_name' not in dhcp:
- raise ConfigError('No DHCP shared networks configured.\n' \
- 'At least one DHCP shared network must be configured.')
+ raise ConfigError(
+ 'No DHCP shared networks configured.\n'
+ 'At least one DHCP shared network must be configured.'
+ )
# Inspect shared-network/subnet
listen_ok = False
subnets = []
- shared_networks = len(dhcp['shared_network_name'])
+ shared_networks = len(dhcp['shared_network_name'])
disabled_shared_networks = 0
subnet_ids = []
@@ -187,12 +207,16 @@ def verify(dhcp):
disabled_shared_networks += 1
if 'subnet' not in network_config:
- raise ConfigError(f'No subnets defined for {network}. At least one\n' \
- 'lease subnet must be configured.')
+ raise ConfigError(
+ f'No subnets defined for {network}. At least one\n'
+ 'lease subnet must be configured.'
+ )
for subnet, subnet_config in network_config['subnet'].items():
if 'subnet_id' not in subnet_config:
- raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"')
+ raise ConfigError(
+ f'Unique subnet ID not specified for subnet "{subnet}"'
+ )
if subnet_config['subnet_id'] in subnet_ids:
raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique')
@@ -203,32 +227,46 @@ def verify(dhcp):
if 'static_route' in subnet_config:
for route, route_option in subnet_config['static_route'].items():
if 'next_hop' not in route_option:
- raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!')
+ raise ConfigError(
+ f'DHCP static-route "{route}" requires router to be defined!'
+ )
# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
networks = []
for range, range_config in subnet_config['range'].items():
if not {'start', 'stop'} <= set(range_config):
- raise ConfigError(f'DHCP range "{range}" start and stop address must be defined!')
+ raise ConfigError(
+ f'DHCP range "{range}" start and stop address must be defined!'
+ )
# Start/Stop address must be inside network
for key in ['start', 'stop']:
if ip_address(range_config[key]) not in ip_network(subnet):
- raise ConfigError(f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!')
+ raise ConfigError(
+ f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!'
+ )
# Stop address must be greater or equal to start address
- if ip_address(range_config['stop']) < ip_address(range_config['start']):
- raise ConfigError(f'DHCP range "{range}" stop address must be greater or equal\n' \
- 'to the ranges start address!')
+ if ip_address(range_config['stop']) < ip_address(
+ range_config['start']
+ ):
+ raise ConfigError(
+ f'DHCP range "{range}" stop address must be greater or equal\n'
+ 'to the ranges start address!'
+ )
for network in networks:
start = range_config['start']
stop = range_config['stop']
if start in network:
- raise ConfigError(f'Range "{range}" start address "{start}" already part of another range!')
+ raise ConfigError(
+ f'Range "{range}" start address "{start}" already part of another range!'
+ )
if stop in network:
- raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another range!')
+ raise ConfigError(
+ f'Range "{range}" stop address "{stop}" already part of another range!'
+ )
tmp = IPRange(range_config['start'], range_config['stop'])
networks.append(tmp)
@@ -237,12 +275,16 @@ def verify(dhcp):
if 'exclude' in subnet_config:
for exclude in subnet_config['exclude']:
if ip_address(exclude) not in ip_network(subnet):
- raise ConfigError(f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!')
+ raise ConfigError(
+ f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!'
+ )
# At least one DHCP address range or static-mapping required
if 'range' not in subnet_config and 'static_mapping' not in subnet_config:
- raise ConfigError(f'No DHCP address range or active static-mapping configured\n' \
- f'within shared-network "{network}, {subnet}"!')
+ raise ConfigError(
+ f'No DHCP address range or active static-mapping configured\n'
+ f'within shared-network "{network}, {subnet}"!'
+ )
if 'static_mapping' in subnet_config:
# Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
@@ -251,29 +293,42 @@ def verify(dhcp):
used_duid = []
for mapping, mapping_config in subnet_config['static_mapping'].items():
if 'ip_address' in mapping_config:
- if ip_address(mapping_config['ip_address']) not in ip_network(subnet):
- raise ConfigError(f'Configured static lease address for mapping "{mapping}" is\n' \
- f'not within shared-network "{network}, {subnet}"!')
-
- if ('mac' not in mapping_config and 'duid' not in mapping_config) or \
- ('mac' in mapping_config and 'duid' in mapping_config):
- raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
- f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
+ if ip_address(mapping_config['ip_address']) not in ip_network(
+ subnet
+ ):
+ raise ConfigError(
+ f'Configured static lease address for mapping "{mapping}" is\n'
+ f'not within shared-network "{network}, {subnet}"!'
+ )
+
+ if (
+ 'mac' not in mapping_config and 'duid' not in mapping_config
+ ) or ('mac' in mapping_config and 'duid' in mapping_config):
+ raise ConfigError(
+ f'Either MAC address or Client identifier (DUID) is required for '
+ f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!'
+ )
if 'disable' not in mapping_config:
if mapping_config['ip_address'] in used_ips:
- raise ConfigError(f'Configured IP address for static mapping "{mapping}" already exists on another static mapping')
+ raise ConfigError(
+ f'Configured IP address for static mapping "{mapping}" already exists on another static mapping'
+ )
used_ips.append(mapping_config['ip_address'])
if 'disable' not in mapping_config:
if 'mac' in mapping_config:
if mapping_config['mac'] in used_mac:
- raise ConfigError(f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping')
+ raise ConfigError(
+ f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping'
+ )
used_mac.append(mapping_config['mac'])
if 'duid' in mapping_config:
if mapping_config['duid'] in used_duid:
- raise ConfigError(f'Configured DUID for static mapping "{mapping}" already exists on another static mapping')
+ raise ConfigError(
+ f'Configured DUID for static mapping "{mapping}" already exists on another static mapping'
+ )
used_duid.append(mapping_config['duid'])
# There must be one subnet connected to a listen interface.
@@ -284,73 +339,118 @@ def verify(dhcp):
# Subnets must be non overlapping
if subnet in subnets:
- raise ConfigError(f'Configured subnets must be unique! Subnet "{subnet}"\n'
- 'defined multiple times!')
+ raise ConfigError(
+ f'Configured subnets must be unique! Subnet "{subnet}"\n'
+ 'defined multiple times!'
+ )
subnets.append(subnet)
# Check for overlapping subnets
net = ip_network(subnet)
for n in subnets:
net2 = ip_network(n)
- if (net != net2):
+ if net != net2:
if net.overlaps(net2):
- raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+ raise ConfigError(
+ f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!'
+ )
# Prevent 'disable' for shared-network if only one network is configured
if (shared_networks - disabled_shared_networks) < 1:
- raise ConfigError(f'At least one shared network must be active!')
+ raise ConfigError('At least one shared network must be active!')
if 'high_availability' in dhcp:
for key in ['name', 'remote', 'source_address', 'status']:
if key not in dhcp['high_availability']:
tmp = key.replace('_', '-')
- raise ConfigError(f'DHCP high-availability requires "{tmp}" to be specified!')
+ raise ConfigError(
+ f'DHCP high-availability requires "{tmp}" to be specified!'
+ )
if len({'certificate', 'ca_certificate'} & set(dhcp['high_availability'])) == 1:
- raise ConfigError(f'DHCP secured high-availability requires both certificate and CA certificate')
+ raise ConfigError(
+ 'DHCP secured high-availability requires both certificate and CA certificate'
+ )
if 'certificate' in dhcp['high_availability']:
cert_name = dhcp['high_availability']['certificate']
if cert_name not in dhcp['pki']['certificate']:
- raise ConfigError(f'Invalid certificate specified for DHCP high-availability')
-
- if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'):
- raise ConfigError(f'Invalid certificate specified for DHCP high-availability')
-
- if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'):
- raise ConfigError(f'Missing private key on certificate specified for DHCP high-availability')
+ raise ConfigError(
+ 'Invalid certificate specified for DHCP high-availability'
+ )
+
+ if not dict_search_args(
+ dhcp['pki']['certificate'], cert_name, 'certificate'
+ ):
+ raise ConfigError(
+ 'Invalid certificate specified for DHCP high-availability'
+ )
+
+ if not dict_search_args(
+ dhcp['pki']['certificate'], cert_name, 'private', 'key'
+ ):
+ raise ConfigError(
+ 'Missing private key on certificate specified for DHCP high-availability'
+ )
if 'ca_certificate' in dhcp['high_availability']:
ca_cert_name = dhcp['high_availability']['ca_certificate']
if ca_cert_name not in dhcp['pki']['ca']:
- raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability')
+ raise ConfigError(
+ 'Invalid CA certificate specified for DHCP high-availability'
+ )
if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'):
- raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability')
+ raise ConfigError(
+ 'Invalid CA certificate specified for DHCP high-availability'
+ )
- for address in (dict_search('listen_address', dhcp) or []):
+ for address in dict_search('listen_address', dhcp) or []:
if is_addr_assigned(address, include_vrf=True):
listen_ok = True
# no need to probe further networks, we have one that is valid
continue
else:
- raise ConfigError(f'listen-address "{address}" not configured on any interface')
+ raise ConfigError(
+ f'listen-address "{address}" not configured on any interface'
+ )
if not listen_ok:
- raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
- 'broadcast interface configured, nor was there an explicit listen-address\n'
- 'configured for serving DHCP relay packets!')
+ raise ConfigError(
+ 'None of the configured subnets have an appropriate primary IP address on any\n'
+ 'broadcast interface configured, nor was there an explicit listen-address\n'
+ 'configured for serving DHCP relay packets!'
+ )
if 'listen_address' in dhcp and 'listen_interface' in dhcp:
- raise ConfigError(f'Cannot define listen-address and listen-interface at the same time')
+ raise ConfigError(
+ 'Cannot define listen-address and listen-interface at the same time'
+ )
- for interface in (dict_search('listen_interface', dhcp) or []):
+ for interface in dict_search('listen_interface', dhcp) or []:
if not interface_exists(interface):
raise ConfigError(f'listen-interface "{interface}" does not exist')
+ if 'dynamic_dns_update' in dhcp:
+ ddns = dhcp['dynamic_dns_update']
+ if 'tsig_key' in ddns:
+ invalid_keys = []
+ for tsig_key_name, tsig_key_config in ddns['tsig_key'].items():
+ if not ('algorithm' in tsig_key_config and 'secret' in tsig_key_config):
+ invalid_keys.append(tsig_key_name)
+ if len(invalid_keys) > 0:
+ raise ConfigError(f'Both algorithm and secret need to be set for TSIG keys: {", ".join(invalid_keys)}')
+
+ if 'forward_domain' in ddns:
+ verify_ddns_domain_servers('Forward', ddns['forward_domain'])
+
+ if 'reverse_domain' in ddns:
+ verify_ddns_domain_servers('Reverse', ddns['reverse_domain'])
+
return None
+
def generate(dhcp):
# bail out early - looks like removal from running config
if not dhcp or 'disable' in dhcp:
@@ -382,8 +482,12 @@ def generate(dhcp):
cert_name = dhcp['high_availability']['certificate']
cert_data = dhcp['pki']['certificate'][cert_name]['certificate']
key_data = dhcp['pki']['certificate'][cert_name]['private']['key']
- write_file(cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600)
- write_file(cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600)
+ write_file(
+ cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600
+ )
+ write_file(
+ cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600
+ )
dhcp['high_availability']['cert_file'] = cert_file
dhcp['high_availability']['cert_key_file'] = cert_key_file
@@ -391,19 +495,36 @@ def generate(dhcp):
if 'ca_certificate' in dhcp['high_availability']:
ca_cert_name = dhcp['high_availability']['ca_certificate']
ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate']
- write_file(ca_cert_file, wrap_certificate(ca_cert_data), user=user_group, mode=0o600)
+ write_file(
+ ca_cert_file,
+ wrap_certificate(ca_cert_data),
+ user=user_group,
+ mode=0o600,
+ )
dhcp['high_availability']['ca_cert_file'] = ca_cert_file
- render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp)
-
- render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp, user=user_group, group=user_group)
- render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp, user=user_group, group=user_group)
+ render(
+ config_file,
+ 'dhcp-server/kea-dhcp4.conf.j2',
+ dhcp,
+ user=user_group,
+ group=user_group,
+ )
+ if 'dynamic_dns_update' in dhcp:
+ render(
+ config_file_d2,
+ 'dhcp-server/kea-dhcp-ddns.conf.j2',
+ dhcp,
+ user=user_group,
+ group=user_group
+ )
return None
+
def apply(dhcp):
- services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server']
+ services = ['kea-dhcp4-server', 'kea-dhcp-ddns-server']
if not dhcp or 'disable' in dhcp:
for service in services:
@@ -420,13 +541,11 @@ def apply(dhcp):
if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp:
action = 'stop'
- if service == 'kea-ctrl-agent' and 'high_availability' not in dhcp:
- action = 'stop'
-
call(f'systemctl {action} {service}.service')
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py
index e3bdbc9f8..5636d6f83 100755
--- a/src/conf_mode/service_dns_forwarding.py
+++ b/src/conf_mode/service_dns_forwarding.py
@@ -366,6 +366,13 @@ def apply(dns):
hc.add_name_server_tags_recursor(['dhcp-' + interface,
'dhcpv6-' + interface ])
+ # add dhcp interfaces
+ if 'dhcp' in dns:
+ for interface in dns['dhcp']:
+ if interface_exists(interface):
+ hc.add_name_server_tags_recursor(['dhcp-' + interface,
+ 'dhcpv6-' + interface ])
+
# hostsd will generate the forward-zones file
# the list and keys() are required as get returns a dict, not list
hc.delete_forward_zones(list(hc.get_forward_zones().keys()))
diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py
index 9e58b4c72..2123823f4 100755
--- a/src/conf_mode/service_https.py
+++ b/src/conf_mode/service_https.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_vrf
from vyos.configverify import verify_pki_certificate
from vyos.configverify import verify_pki_ca_certificate
from vyos.configverify import verify_pki_dh_parameters
+from vyos.configdiff import get_config_diff
from vyos.defaults import api_config_state
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
@@ -79,6 +80,14 @@ def get_config(config=None):
# merge CLI and default dictionary
https = config_dict_merge(default_values, https)
+
+ # some settings affecting nginx will require a restart:
+ # for example, a reload will not suffice when binding the listen address
+ # after nginx has started and dropped privileges; add flag here
+ diff = get_config_diff(conf)
+ children_changed = diff.node_changed_children(base)
+ https['nginx_restart_required'] = bool(set(children_changed) != set(['api']))
+
return https
def verify(https):
@@ -208,7 +217,10 @@ def apply(https):
elif is_systemd_service_active(http_api_service_name):
call(f'systemctl stop {http_api_service_name}')
- call(f'systemctl reload-or-restart {https_service_name}')
+ if https['nginx_restart_required']:
+ call(f'systemctl restart {https_service_name}')
+ else:
+ call(f'systemctl reload-or-restart {https_service_name}')
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/service_ids_ddos-protection.py b/src/conf_mode/service_ids_ddos-protection.py
deleted file mode 100755
index 276a71fcb..000000000
--- a/src/conf_mode/service_ids_ddos-protection.py
+++ /dev/null
@@ -1,104 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-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
-# 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
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.template import render
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/fastnetmon/fastnetmon.conf'
-networks_list = r'/run/fastnetmon/networks_list'
-excluded_networks_list = r'/run/fastnetmon/excluded_networks_list'
-attack_dir = '/var/log/fastnetmon_attacks'
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
- base = ['service', 'ids', 'ddos-protection']
- if not conf.exists(base):
- return None
-
- fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True,
- with_recursive_defaults=True)
-
- return fastnetmon
-
-def verify(fastnetmon):
- if not fastnetmon:
- return None
-
- if 'mode' not in fastnetmon:
- raise ConfigError('Specify operating mode!')
-
- if fastnetmon.get('mode') == 'mirror' and 'listen_interface' not in fastnetmon:
- raise ConfigError("Incorrect settings for 'mode mirror': must specify interface(s) for traffic mirroring")
-
- if fastnetmon.get('mode') == 'sflow' and 'listen_address' not in fastnetmon.get('sflow', {}):
- raise ConfigError("Incorrect settings for 'mode sflow': must specify sFlow 'listen-address'")
-
- if 'alert_script' in fastnetmon:
- if os.path.isfile(fastnetmon['alert_script']):
- # Check script permissions
- if not os.access(fastnetmon['alert_script'], os.X_OK):
- raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script']))
- else:
- raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon))
-
-def generate(fastnetmon):
- if not fastnetmon:
- for file in [config_file, networks_list]:
- if os.path.isfile(file):
- os.unlink(file)
-
- return None
-
- # Create dir for log attack details
- if not os.path.exists(attack_dir):
- os.mkdir(attack_dir)
-
- render(config_file, 'ids/fastnetmon.j2', fastnetmon)
- render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon)
- render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon)
- return None
-
-def apply(fastnetmon):
- systemd_service = 'fastnetmon.service'
- if not fastnetmon:
- # Stop fastnetmon service if removed
- call(f'systemctl stop {systemd_service}')
- else:
- call(f'systemctl reload-or-restart {systemd_service}')
-
- return None
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- exit(1)
diff --git a/src/conf_mode/service_monitoring_network_event.py b/src/conf_mode/service_monitoring_network_event.py
new file mode 100644
index 000000000..104e6ce23
--- /dev/null
+++ b/src/conf_mode/service_monitoring_network_event.py
@@ -0,0 +1,93 @@
+#!/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 json
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.utils.file import write_file
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+vyos_network_event_logger_config = r'/run/vyos-network-event-logger.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'monitoring', 'network-event']
+ if not conf.exists(base):
+ return None
+
+ monitoring = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ monitoring = conf.merge_defaults(monitoring, recursive=True)
+
+ return monitoring
+
+
+def verify(monitoring):
+ if not monitoring:
+ return None
+
+ return None
+
+
+def generate(monitoring):
+ if not monitoring:
+ # Delete config
+ if os.path.exists(vyos_network_event_logger_config):
+ os.unlink(vyos_network_event_logger_config)
+
+ return None
+
+ # Create config
+ log_conf_json = json.dumps(monitoring, indent=4)
+ write_file(vyos_network_event_logger_config, log_conf_json)
+
+ return None
+
+
+def apply(monitoring):
+ # Reload systemd manager configuration
+ systemd_service = 'vyos-network-event-logger.service'
+
+ if not monitoring:
+ call(f'systemctl stop {systemd_service}')
+ return
+
+ call(f'systemctl restart {systemd_service}')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/service_monitoring_prometheus.py b/src/conf_mode/service_monitoring_prometheus.py
index e0a9fc4ef..9a07d8593 100755
--- a/src/conf_mode/service_monitoring_prometheus.py
+++ b/src/conf_mode/service_monitoring_prometheus.py
@@ -26,15 +26,18 @@ from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
-
airbag.enable()
node_exporter_service_file = '/etc/systemd/system/node_exporter.service'
node_exporter_systemd_service = 'node_exporter.service'
+node_exporter_collector_path = '/run/node_exporter/collector'
frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service'
frr_exporter_systemd_service = 'frr_exporter.service'
+blackbox_exporter_service_file = '/etc/systemd/system/blackbox_exporter.service'
+blackbox_exporter_systemd_service = 'blackbox_exporter.service'
+
def get_config(config=None):
if config:
@@ -57,6 +60,12 @@ def get_config(config=None):
if tmp:
monitoring.update({'frr_exporter_restart_required': {}})
+ tmp = False
+ for node in ['vrf', 'config-file']:
+ tmp = tmp or is_node_changed(conf, base + ['blackbox-exporter', node])
+ if tmp:
+ monitoring.update({'blackbox_exporter_restart_required': {}})
+
return monitoring
@@ -70,6 +79,22 @@ def verify(monitoring):
if 'frr_exporter' in monitoring:
verify_vrf(monitoring['frr_exporter'])
+ if 'blackbox_exporter' in monitoring:
+ verify_vrf(monitoring['blackbox_exporter'])
+
+ if (
+ 'modules' in monitoring['blackbox_exporter']
+ and 'dns' in monitoring['blackbox_exporter']['modules']
+ and 'name' in monitoring['blackbox_exporter']['modules']['dns']
+ ):
+ for mod_name, mod_config in monitoring['blackbox_exporter']['modules'][
+ 'dns'
+ ]['name'].items():
+ if 'query_name' not in mod_config:
+ raise ConfigError(
+ f'query name not specified in dns module {mod_name}'
+ )
+
return None
@@ -84,6 +109,11 @@ def generate(monitoring):
if os.path.isfile(frr_exporter_service_file):
os.unlink(frr_exporter_service_file)
+ if not monitoring or 'blackbox_exporter' not in monitoring:
+ # Delete systemd files
+ if os.path.isfile(blackbox_exporter_service_file):
+ os.unlink(blackbox_exporter_service_file)
+
if not monitoring:
return None
@@ -94,6 +124,13 @@ def generate(monitoring):
'prometheus/node_exporter.service.j2',
monitoring['node_exporter'],
)
+ if (
+ 'collectors' in monitoring['node_exporter']
+ and 'textfile' in monitoring['node_exporter']['collectors']
+ ):
+ # Create textcollector folder
+ if not os.path.isdir(node_exporter_collector_path):
+ os.makedirs(node_exporter_collector_path)
if 'frr_exporter' in monitoring:
# Render frr_exporter service_file
@@ -103,6 +140,20 @@ def generate(monitoring):
monitoring['frr_exporter'],
)
+ if 'blackbox_exporter' in monitoring:
+ # Render blackbox_exporter service_file
+ render(
+ blackbox_exporter_service_file,
+ 'prometheus/blackbox_exporter.service.j2',
+ monitoring['blackbox_exporter'],
+ )
+ # Render blackbox_exporter config file
+ render(
+ '/run/blackbox_exporter/config.yml',
+ 'prometheus/blackbox_exporter.yml.j2',
+ monitoring['blackbox_exporter'],
+ )
+
return None
@@ -113,6 +164,8 @@ def apply(monitoring):
call(f'systemctl stop {node_exporter_systemd_service}')
if not monitoring or 'frr_exporter' not in monitoring:
call(f'systemctl stop {frr_exporter_systemd_service}')
+ if not monitoring or 'blackbox_exporter' not in monitoring:
+ call(f'systemctl stop {blackbox_exporter_systemd_service}')
if not monitoring:
return
@@ -133,6 +186,14 @@ def apply(monitoring):
call(f'systemctl {systemd_action} {frr_exporter_systemd_service}')
+ if 'blackbox_exporter' in monitoring:
+ # we need to restart the service if e.g. the VRF name changed
+ systemd_action = 'reload-or-restart'
+ if 'blackbox_exporter_restart_required' in monitoring:
+ systemd_action = 'restart'
+
+ call(f'systemctl {systemd_action} {blackbox_exporter_systemd_service}')
+
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py
index 1174b1238..c64c59af7 100755
--- a/src/conf_mode/service_snmp.py
+++ b/src/conf_mode/service_snmp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -22,6 +22,7 @@ from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configverify import verify_vrf
+from vyos.defaults import systemd_services
from vyos.snmpv3_hashgen import plaintext_to_md5
from vyos.snmpv3_hashgen import plaintext_to_sha1
from vyos.snmpv3_hashgen import random
@@ -43,7 +44,7 @@ config_file_access = r'/usr/share/snmp/snmpd.conf'
config_file_user = r'/var/lib/snmp/snmpd.conf'
default_script_dir = r'/config/user-data/'
systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf'
-systemd_service = 'snmpd.service'
+systemd_service = systemd_services['snmpd']
def get_config(config=None):
if config:
@@ -146,6 +147,9 @@ def verify(snmp):
return None
if 'user' in snmp['v3']:
+ if 'engineid' not in snmp['v3']:
+ raise ConfigError(f'EngineID must be configured for SNMPv3!')
+
for user, user_config in snmp['v3']['user'].items():
if 'group' not in user_config:
raise ConfigError(f'Group membership required for user "{user}"!')
diff --git a/src/conf_mode/system_flow-accounting.py b/src/conf_mode/system_flow-accounting.py
index a12ee363d..925c4a562 100755
--- a/src/conf_mode/system_flow-accounting.py
+++ b/src/conf_mode/system_flow-accounting.py
@@ -18,7 +18,6 @@ import os
import re
from sys import exit
-from ipaddress import ip_address
from vyos.config import Config
from vyos.config import config_dict_merge
@@ -159,9 +158,9 @@ def get_config(config=None):
# delete individual flow type defaults - should only be added if user
# sets this feature
- for flow_type in ['sflow', 'netflow']:
- if flow_type not in flow_accounting and flow_type in default_values:
- del default_values[flow_type]
+ flow_type = 'netflow'
+ if flow_type not in flow_accounting and flow_type in default_values:
+ del default_values[flow_type]
flow_accounting = config_dict_merge(default_values, flow_accounting)
@@ -171,9 +170,9 @@ def verify(flow_config):
if not flow_config:
return None
- # check if at least one collector is enabled
- if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config:
- raise ConfigError('You need to configure at least sFlow or NetFlow, ' \
+ # check if collector is enabled
+ if 'netflow' not in flow_config and 'disable_imt' in flow_config:
+ raise ConfigError('You need to configure NetFlow, ' \
'or not set "disable-imt" for flow-accounting!')
# Check if at least one interface is configured
@@ -185,45 +184,7 @@ def verify(flow_config):
for interface in flow_config['interface']:
verify_interface_exists(flow_config, interface, warning_only=True)
- # check sFlow configuration
- if 'sflow' in flow_config:
- # check if at least one sFlow collector is configured
- if 'server' not in flow_config['sflow']:
- raise ConfigError('You need to configure at least one sFlow server!')
-
- # check that all sFlow collectors use the same IP protocol version
- sflow_collector_ipver = None
- for server in flow_config['sflow']['server']:
- if sflow_collector_ipver:
- if sflow_collector_ipver != ip_address(server).version:
- raise ConfigError("All sFlow servers must use the same IP protocol")
- else:
- sflow_collector_ipver = ip_address(server).version
-
- # check if vrf is defined for Sflow
- verify_vrf(flow_config)
- sflow_vrf = None
- if 'vrf' in flow_config:
- sflow_vrf = flow_config['vrf']
-
- # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa
- for server in flow_config['sflow']['server']:
- if 'agent_address' in flow_config['sflow']:
- if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version:
- raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\
- 'server". You need to set the same IP version for both "agent-address" and '\
- 'all sFlow servers')
-
- if 'agent_address' in flow_config['sflow']:
- tmp = flow_config['sflow']['agent_address']
- if not is_addr_assigned(tmp, sflow_vrf):
- raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')
-
- # Check if configured sflow source-address exist in the system
- if 'source_address' in flow_config['sflow']:
- if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf):
- tmp = flow_config['sflow']['source_address']
- raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!')
+ verify_vrf(flow_config)
# check NetFlow configuration
if 'netflow' in flow_config:
diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py
index 3f245f166..de4accda2 100755
--- a/src/conf_mode/system_host-name.py
+++ b/src/conf_mode/system_host-name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -23,6 +23,7 @@ import vyos.hostsd_client
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import leaf_node_changed
+from vyos.defaults import systemd_services
from vyos.ifconfig import Section
from vyos.template import is_ip
from vyos.utils.process import cmd
@@ -174,11 +175,13 @@ def apply(config):
# Restart services that use the hostname
if hostname_new != hostname_old:
- call("systemctl restart rsyslog.service")
+ tmp = systemd_services['syslog']
+ call(f'systemctl restart {tmp}')
# If SNMP is running, restart it too
if process_named_running('snmpd') and config['snmpd_restart_reqired']:
- call('systemctl restart snmpd.service')
+ tmp = systemd_services['snmpd']
+ call(f'systemctl restart {tmp}')
return None
diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py
index d3a969d9b..4febb6494 100755
--- a/src/conf_mode/system_login.py
+++ b/src/conf_mode/system_login.py
@@ -24,10 +24,13 @@ from pwd import getpwuid
from sys import exit
from time import sleep
+from vyos.base import Warning
from vyos.config import Config
from vyos.configverify import verify_vrf
from vyos.template import render
from vyos.template import is_ipv4
+from vyos.utils.auth import EPasswdStrength
+from vyos.utils.auth import evaluate_strength
from vyos.utils.auth import get_current_user
from vyos.utils.configfs import delete_cli_node
from vyos.utils.configfs import add_cli_node
@@ -146,6 +149,19 @@ def verify(login):
if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID:
raise ConfigError(f'User "{user}" can not be created, conflict with local system account!')
+ # T6353: Check password for complexity using cracklib.
+ # A user password should be sufficiently complex
+ plaintext_password = dict_search(
+ path='authentication.plaintext_password',
+ dict_object=user_config
+ ) or None
+
+ failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR]
+ if plaintext_password is not None:
+ result = evaluate_strength(plaintext_password)
+ if result['strength'] in failed_check_status:
+ Warning(result['error'])
+
for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
if 'type' not in pubkey_options:
raise ConfigError(f'Missing type for public-key "{pubkey}"!')
diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py
index 5826d8042..cdd066649 100755
--- a/src/conf_mode/system_login_banner.py
+++ b/src/conf_mode/system_login_banner.py
@@ -95,8 +95,12 @@ def apply(banner):
render(POSTLOGIN_FILE, 'login/default_motd.j2', banner,
permission=0o644, user='root', group='root')
- render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', banner,
- permission=0o644, user='root', group='root')
+ if banner['version_data']['build_type'] != 'release':
+ render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.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 e2832cde6..5acad6599 100755
--- a/src/conf_mode/system_option.py
+++ b/src/conf_mode/system_option.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2024 VyOS maintainers and contributors
+# Copyright (C) 2019-2025 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
@@ -86,7 +86,7 @@ def verify(options):
if 'source_address' in config:
if not is_addr_assigned(config['source_address']):
- raise ConfigError('No interface with give address specified!')
+ raise ConfigError('No interface with given address specified!')
if 'ssh_client' in options:
config = options['ssh_client']
@@ -122,7 +122,14 @@ def generate(options):
render(ssh_config, 'system/ssh_config.j2', options)
render(usb_autosuspend, 'system/40_usb_autosuspend.j2', options)
+ # XXX: This code path and if statements must be kept in sync with the Kernel
+ # option handling in image_installer.py:get_cli_kernel_options(). This
+ # occurance is used for having the appropriate options passed to GRUB
+ # when re-configuring options on the CLI.
cmdline_options = []
+ kernel_opts = options.get('kernel', {})
+ k_cpu_opts = kernel_opts.get('cpu', {})
+ k_memory_opts = kernel_opts.get('memory', {})
if 'kernel' in options:
if 'disable_mitigations' in options['kernel']:
cmdline_options.append('mitigations=off')
@@ -131,8 +138,51 @@ def generate(options):
if 'amd_pstate_driver' in options['kernel']:
mode = options['kernel']['amd_pstate_driver']
cmdline_options.append(
- f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}'
- )
+ f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}')
+ if 'quiet' in options['kernel']:
+ cmdline_options.append('quiet')
+
+ if 'disable_hpet' in kernel_opts:
+ cmdline_options.append('hpet=disable')
+
+ if 'disable_mce' in kernel_opts:
+ cmdline_options.append('mce=off')
+
+ if 'disable_softlockup' in kernel_opts:
+ cmdline_options.append('nosoftlockup')
+
+ # CPU options
+ isol_cpus = k_cpu_opts.get('isolate_cpus')
+ if isol_cpus:
+ cmdline_options.append(f'isolcpus={isol_cpus}')
+
+ nohz_full = k_cpu_opts.get('nohz_full')
+ if nohz_full:
+ cmdline_options.append(f'nohz_full={nohz_full}')
+
+ rcu_nocbs = k_cpu_opts.get('rcu_no_cbs')
+ if rcu_nocbs:
+ cmdline_options.append(f'rcu_nocbs={rcu_nocbs}')
+
+ if 'disable_nmi_watchdog' in k_cpu_opts:
+ cmdline_options.append('nmi_watchdog=0')
+
+ # Memory options
+ if 'disable_numa_balancing' in k_memory_opts:
+ cmdline_options.append('numa_balancing=disable')
+
+ default_hp_size = k_memory_opts.get('default_hugepage_size')
+ if default_hp_size:
+ cmdline_options.append(f'default_hugepagesz={default_hp_size}')
+
+ hp_sizes = k_memory_opts.get('hugepage_size')
+ if hp_sizes:
+ for size, settings in hp_sizes.items():
+ cmdline_options.append(f'hugepagesz={size}')
+ count = settings.get('hugepage_count')
+ if count:
+ cmdline_options.append(f'hugepages={count}')
+
grub_util.update_kernel_cmdline_options(' '.join(cmdline_options))
return None
diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py
index 41119b494..a22dac36f 100755
--- a/src/conf_mode/system_sflow.py
+++ b/src/conf_mode/system_sflow.py
@@ -54,7 +54,7 @@ def verify(sflow):
# Check if configured sflow agent-address exist in the system
if 'agent_address' in sflow:
tmp = sflow['agent_address']
- if not is_addr_assigned(tmp):
+ if not is_addr_assigned(tmp, include_vrf=True):
raise ConfigError(
f'Configured "sflow agent-address {tmp}" does not exist in the system!'
)
diff --git a/src/conf_mode/system_syslog.py b/src/conf_mode/system_syslog.py
index eb2f02eb3..bdab09f3c 100755
--- a/src/conf_mode/system_syslog.py
+++ b/src/conf_mode/system_syslog.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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,17 +20,22 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
+from vyos.defaults import systemd_services
+from vyos.utils.network import is_addr_assigned
from vyos.utils.process import call
from vyos.template import render
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
from vyos import ConfigError
from vyos import airbag
airbag.enable()
-rsyslog_conf = '/etc/rsyslog.d/00-vyos.conf'
+rsyslog_conf = '/run/rsyslog/rsyslog.conf'
logrotate_conf = '/etc/logrotate.d/vyos-rsyslog'
-systemd_override = r'/run/systemd/system/rsyslog.service.d/override.conf'
+
+systemd_socket = 'syslog.socket'
+systemd_service = systemd_services['syslog']
def get_config(config=None):
if config:
@@ -46,23 +51,17 @@ def get_config(config=None):
syslog.update({ 'logrotate' : logrotate_conf })
- tmp = is_node_changed(conf, base + ['vrf'])
- if tmp: syslog.update({'restart_required': {}})
-
syslog = conf.merge_defaults(syslog, recursive=True)
- if syslog.from_defaults(['global']):
- del syslog['global']
-
- if (
- 'global' in syslog
- and 'preserve_fqdn' in syslog['global']
- and conf.exists(['system', 'host-name'])
- and conf.exists(['system', 'domain-name'])
- ):
- hostname = conf.return_value(['system', 'host-name'])
- domain = conf.return_value(['system', 'domain-name'])
- fqdn = f'{hostname}.{domain}'
- syslog['global']['local_host_name'] = fqdn
+ if syslog.from_defaults(['local']):
+ del syslog['local']
+
+ if 'preserve_fqdn' in syslog:
+ if conf.exists(['system', 'host-name']):
+ tmp = conf.return_value(['system', 'host-name'])
+ syslog['preserve_fqdn']['host_name'] = tmp
+ if conf.exists(['system', 'domain-name']):
+ tmp = conf.return_value(['system', 'domain-name'])
+ syslog['preserve_fqdn']['domain_name'] = tmp
return syslog
@@ -70,13 +69,33 @@ def verify(syslog):
if not syslog:
return None
- if 'host' in syslog:
- for host, host_options in syslog['host'].items():
- if 'protocol' in host_options and host_options['protocol'] == 'udp':
- if 'format' in host_options and 'octet_counted' in host_options['format']:
- Warning(f'Syslog UDP transport for "{host}" should not use octet-counted format!')
-
- verify_vrf(syslog)
+ if 'preserve_fqdn' in syslog:
+ if 'host_name' not in syslog['preserve_fqdn']:
+ Warning('No "system host-name" defined - cannot set syslog FQDN!')
+ if 'domain_name' not in syslog['preserve_fqdn']:
+ Warning('No "system domain-name" defined - cannot set syslog FQDN!')
+
+ if 'remote' in syslog:
+ for remote, remote_options in syslog['remote'].items():
+ if 'protocol' in remote_options and remote_options['protocol'] == 'udp':
+ if 'format' in remote_options and 'octet_counted' in remote_options['format']:
+ Warning(f'Syslog UDP transport for "{remote}" should not use octet-counted format!')
+
+ if 'vrf' in remote_options:
+ verify_vrf(remote_options)
+
+ if 'source_address' in remote_options:
+ vrf = None
+ if 'vrf' in remote_options:
+ vrf = remote_options['vrf']
+ if not is_addr_assigned(remote_options['source_address'], vrf):
+ raise ConfigError('No interface with given address specified!')
+
+ source_address = remote_options['source_address']
+ if ((is_ipv4(remote) and is_ipv6(source_address)) or
+ (is_ipv6(remote) and is_ipv4(source_address))):
+ raise ConfigError(f'Source-address "{source_address}" does not match '\
+ f'address-family of remote "{remote}"!')
def generate(syslog):
if not syslog:
@@ -88,26 +107,15 @@ def generate(syslog):
return None
render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog)
- render(systemd_override, 'rsyslog/override.conf.j2', syslog)
render(logrotate_conf, 'rsyslog/logrotate.j2', syslog)
-
- # Reload systemd manager configuration
- call('systemctl daemon-reload')
return None
def apply(syslog):
- systemd_socket = 'syslog.socket'
- systemd_service = 'syslog.service'
if not syslog:
call(f'systemctl stop {systemd_service} {systemd_socket}')
return None
- # we need to restart the service if e.g. the VRF name changed
- systemd_action = 'reload-or-restart'
- if 'restart_required' in syslog:
- systemd_action = 'restart'
-
- call(f'systemctl {systemd_action} {systemd_service}')
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index e22b7550c..2754314f7 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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
@@ -64,6 +64,7 @@ swanctl_dir = '/etc/swanctl'
charon_conf = '/etc/strongswan.d/charon.conf'
charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf'
+charon_systemd_conf = '/etc/strongswan.d/charon-systemd.conf'
interface_conf = '/etc/strongswan.d/interfaces_use.conf'
swanctl_conf = f'{swanctl_dir}/swanctl.conf'
@@ -86,8 +87,6 @@ def get_config(config=None):
conf = Config()
base = ['vpn', 'ipsec']
l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings']
- if not conf.exists(base):
- return None
# retrieve common dictionary keys
ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
@@ -95,6 +94,14 @@ def get_config(config=None):
get_first_key=True,
with_pki=True)
+ ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
+ if ipsec['nhrp_exists']:
+ set_dependents('nhrp', conf)
+
+ if not conf.exists(base):
+ ipsec.update({'deleted' : ''})
+ return ipsec
+
# We have to cleanup the default dict, as default values could
# enable features which are not explicitly enabled on the
# CLI. E.g. dead-peer-detection defaults should not be injected
@@ -115,7 +122,6 @@ def get_config(config=None):
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'])
- ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
if ipsec['nhrp_exists']:
set_dependents('nhrp', conf)
@@ -151,6 +157,8 @@ def get_config(config=None):
_, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface)
ipsec['vti_interface_dicts'][vti_interface] = vti
+ ipsec['vpp_ipsec_exists'] = conf.exists(['vpp', 'settings', 'ipsec'])
+
return ipsec
def get_dhcp_address(iface):
@@ -196,8 +204,8 @@ def verify_pki_rsa(pki, rsa_conf):
return True
def verify(ipsec):
- if not ipsec:
- return None
+ if not ipsec or 'deleted' in ipsec:
+ return
if 'authentication' in ipsec:
if 'psk' in ipsec['authentication']:
@@ -479,6 +487,17 @@ def verify(ipsec):
else:
raise ConfigError(f"Missing ike-group on site-to-site peer {peer}")
+ # verify encryption algorithm compatibility for IKE with VPP
+ if ipsec['vpp_ipsec_exists']:
+ ike_group = ipsec['ike_group'][peer_conf['ike_group']]
+ for proposal, proposal_config in ike_group.get('proposal', {}).items():
+ algs = ['gmac', 'serpent', 'twofish']
+ if any(alg in proposal_config['encryption'] for alg in algs):
+ raise ConfigError(
+ f'Encryption algorithm {proposal_config["encryption"]} cannot be used '
+ f'for IKE proposal {proposal} for site-to-site peer {peer} with VPP'
+ )
+
if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']:
raise ConfigError(f"Missing authentication on site-to-site peer {peer}")
@@ -557,7 +576,7 @@ def verify(ipsec):
esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group']
- if esp_group_name not in ipsec['esp_group']:
+ if esp_group_name not in ipsec.get('esp_group'):
raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}")
esp_group = ipsec['esp_group'][esp_group_name]
@@ -569,6 +588,18 @@ def verify(ipsec):
if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']):
raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
+ # verify ESP encryption algorithm compatibility with VPP
+ # because Marvel plugin for VPP doesn't support all algorithms that Strongswan does
+ if ipsec['vpp_ipsec_exists']:
+ for proposal, proposal_config in esp_group.get('proposal', {}).items():
+ algs = ['aes128', 'aes192', 'aes256', 'aes128gcm128', 'aes192gcm128', 'aes256gcm128']
+ if proposal_config['encryption'] not in algs:
+ raise ConfigError(
+ f'Encryption algorithm {proposal_config["encryption"]} cannot be used '
+ f'for ESP proposal {proposal} on tunnel {tunnel} for site-to-site peer {peer} with VPP'
+ )
+
+
def cleanup_pki_files():
for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]:
if not os.path.exists(path):
@@ -624,7 +655,7 @@ def generate_pki_files_rsa(pki, rsa_conf):
def generate(ipsec):
cleanup_pki_files()
- if not ipsec:
+ if not ipsec or 'deleted' in ipsec:
for config_file in [charon_dhcp_conf, charon_radius_conf, interface_conf, swanctl_conf]:
if os.path.isfile(config_file):
os.unlink(config_file)
@@ -715,21 +746,19 @@ def generate(ipsec):
render(charon_conf, 'ipsec/charon.j2', ipsec)
render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec)
render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec)
+ render(charon_systemd_conf, 'ipsec/charon_systemd.conf.j2', ipsec)
render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec)
render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec)
def apply(ipsec):
systemd_service = 'strongswan.service'
- if not ipsec:
+ if not ipsec or 'deleted' in ipsec:
call(f'systemctl stop {systemd_service}')
-
if vti_updown_db_exists():
remove_vti_updown_db()
-
else:
call(f'systemctl reload-or-restart {systemd_service}')
-
if ipsec['enabled_vti_interfaces']:
with open_vti_updown_db_for_create_or_update() as db:
db.removeAllOtherInterfaces(ipsec['enabled_vti_interfaces'])
@@ -737,7 +766,7 @@ def apply(ipsec):
db.commit(lambda interface: ipsec['vti_interface_dicts'][interface])
elif vti_updown_db_exists():
remove_vti_updown_db()
-
+ if ipsec:
if ipsec.get('nhrp_exists', False):
try:
call_dependents()
@@ -746,7 +775,6 @@ def apply(ipsec):
# ConfigError("ConfigError('Interface ethN requires an IP address!')")
pass
-
if __name__ == '__main__':
try:
ipsec = get_config()
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 74780b601..8baf55857 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -149,8 +149,9 @@ def verify(vrf):
f'static routes installed!')
if 'name' in vrf:
- reserved_names = ["add", "all", "broadcast", "default", "delete", "dev",
- "get", "inet", "mtu", "link", "type", "vrf"]
+ reserved_names = ['add', 'all', 'broadcast', 'default', 'delete', 'dev',
+ 'down', 'get', 'inet', 'link', 'mtu', 'type', 'up', 'vrf']
+
table_ids = []
vnis = []
for name, vrf_config in vrf['name'].items():