diff options
Diffstat (limited to 'src/conf_mode')
63 files changed, 585 insertions, 361 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index e967bee71..a73a18ffa 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -32,6 +32,7 @@ from vyos.utils.file import write_file from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import run +from vyos.utils.network import interface_exists from vyos.template import bracketize_ipv6 from vyos.template import inc_ip from vyos.template import is_ipv4 @@ -261,12 +262,11 @@ def generate_run_arguments(name, container_config): restart = container_config['restart'] # Add capability options. Should be in uppercase - cap_add = '' - if 'cap_add' in container_config: - for c in container_config['cap_add']: - c = c.upper() - c = c.replace('-', '_') - cap_add += f' --cap-add={c}' + capabilities = '' + if 'capability' in container_config: + for cap in container_config['capability']: + cap = cap.upper().replace('-', '_') + capabilities += f' --cap-add={cap}' # Add a host device to the container /dev/x:/dev/x device = '' @@ -329,7 +329,7 @@ def generate_run_arguments(name, container_config): prop = vol_config['propagation'] volume += f' --volume {svol}:{dvol}:{mode},{prop}' - container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \ + container_base_cmd = f'--detach --interactive --tty --replace {capabilities} ' \ f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid}' @@ -471,10 +471,10 @@ def apply(container): # T5147: Networks are started only as soon as there is a consumer. # If only a network is created in the first place, no need to assign # it to a VRF as there's no consumer, yet. - if os.path.exists(f'/sys/class/net/{network_name}'): + if interface_exists(network_name): tmp = Interface(network_name) - tmp.add_ipv6_eui64_address('fe80::/64') tmp.set_vrf(network_config.get('vrf', '')) + tmp.add_ipv6_eui64_address('fe80::/64') return None diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 810437dda..e96e57154 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -18,7 +18,6 @@ import os import re from glob import glob -from json import loads from sys import exit from vyos.base import Warning @@ -31,11 +30,9 @@ from vyos.ethtool import Ethtool from vyos.firewall import fqdn_config_parse from vyos.firewall import geoip_update from vyos.template import render -from vyos.utils.process import call -from vyos.utils.process import cmd from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive -from vyos.utils.process import process_named_running +from vyos.utils.process import call from vyos.utils.process import rc_cmd from vyos import ConfigError from vyos import airbag @@ -47,6 +44,7 @@ nftables_conf = '/run/nftables.conf' sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'enable': '0', 'disable': '1'}, + 'directed_broadcast' : {'sysfs': '/proc/sys/net/ipv4/conf/all/bc_forwarding', 'enable': '1', 'disable': '0'}, 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route'}, 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects'}, 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'enable': '0', 'disable': '-1'}, @@ -491,7 +489,7 @@ def apply_sysfs(firewall): f.write(value) def apply(firewall): - install_result, output = rc_cmd(f'nft -f {nftables_conf}') + install_result, output = rc_cmd(f'nft --file {nftables_conf}') if install_result == 1: raise ConfigError(f'Failed to apply firewall: {output}') diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 8184d8415..371b219c0 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,10 +14,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/>. -import os - from sys import exit -from netifaces import interfaces + from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed @@ -29,7 +27,6 @@ from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_mtu_ipv6 -from vyos.configverify import verify_source_interface from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.ifconfig import BondIf @@ -38,6 +35,7 @@ from vyos.ifconfig import Section from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values +from vyos.utils.network import interface_exists from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured from vyos.configdep import set_dependents, call_dependents @@ -209,7 +207,7 @@ def verify(bond): if interface == 'lo': raise ConfigError('Loopback interface "lo" can not be added to a bond') - if interface not in interfaces(): + if not interface_exists(interface): raise ConfigError(error_msg + 'it does not exist!') if 'is_bridge_member' in interface_config: diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index 2c0f846c3..6da7e6a69 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -15,9 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import pprint -from glob import glob from sys import exit from vyos.base import Warning @@ -26,7 +24,6 @@ from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_dhcpv6 -from vyos.configverify import verify_eapol from vyos.configverify import verify_interface_exists from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_mtu @@ -34,6 +31,8 @@ from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.configverify import verify_bond_bridge_member +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate from vyos.ethtool import Ethtool from vyos.ifconfig import EthernetIf from vyos.ifconfig import BondIf @@ -153,6 +152,19 @@ def get_config(config=None): base = ['interfaces', 'ethernet'] ifname, ethernet = get_interface_dict(conf, base, with_pki=True) + # T5862 - default MTU is not acceptable in some environments + # There are cloud environments available where the maximum supported + # ethernet MTU is e.g. 1450 bytes, thus we clamp this to the adapters + # maximum MTU value or 1500 bytes - whatever is lower + if 'mtu' not in ethernet: + try: + ethernet['mtu'] = '1500' + max_mtu = EthernetIf(ifname).get_max_mtu() + if max_mtu < int(ethernet['mtu']): + ethernet['mtu'] = str(max_mtu) + except: + pass + if 'is_bond_member' in ethernet: update_bond_options(conf, ethernet) @@ -263,6 +275,22 @@ def verify_allowedbond_changes(ethernet: dict): f' on interface "{ethernet["ifname"]}".' \ f' Interface is a bond member') +def verify_eapol(ethernet: dict): + """ + Common helper function used by interface implementations to perform + recurring validation of EAPoL configuration. + """ + if 'eapol' not in ethernet: + return + + if 'certificate' not in ethernet['eapol']: + raise ConfigError('Certificate must be specified when using EAPoL!') + + verify_pki_certificate(ethernet, ethernet['eapol']['certificate'], no_password_protected=True) + + if 'ca_certificate' in ethernet['eapol']: + for ca_cert in ethernet['eapol']['ca_certificate']: + verify_pki_ca_certificate(ethernet, ca_cert) def verify(ethernet): if 'deleted' in ethernet: diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py index f6694ddde..769139e0f 100755 --- a/src/conf_mode/interfaces_geneve.py +++ b/src/conf_mode/interfaces_geneve.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from sys import exit -from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict @@ -26,6 +25,7 @@ from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import GeneveIf +from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag @@ -77,8 +77,8 @@ def generate(geneve): def apply(geneve): # Check if GENEVE interface already exists if 'rebuild_required' in geneve or 'delete' in geneve: - if geneve['ifname'] in interfaces(): - g = GeneveIf(geneve['ifname']) + if interface_exists(geneve['ifname']): + g = GeneveIf(**geneve) # GENEVE is super picky and the tunnel always needs to be recreated, # thus we can simply always delete it first. g.remove() diff --git a/src/conf_mode/interfaces_l2tpv3.py b/src/conf_mode/interfaces_l2tpv3.py index e1db3206e..e25793543 100755 --- a/src/conf_mode/interfaces_l2tpv3.py +++ b/src/conf_mode/interfaces_l2tpv3.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,10 +14,7 @@ # 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 netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict @@ -30,6 +27,7 @@ from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import L2TPv3If from vyos.utils.kernel import check_kmod from vyos.utils.network import is_addr_assigned +from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag airbag.enable() @@ -87,7 +85,7 @@ def generate(l2tpv3): def apply(l2tpv3): # Check if L2TPv3 interface already exists - if l2tpv3['ifname'] in interfaces(): + if interface_exists(l2tpv3['ifname']): # L2TPv3 is picky when changing tunnels/sessions, thus we can simply # always delete it first. l = L2TPv3If(**l2tpv3) diff --git a/src/conf_mode/interfaces_loopback.py b/src/conf_mode/interfaces_loopback.py index 08d34477a..a784e9ec2 100755 --- a/src/conf_mode/interfaces_loopback.py +++ b/src/conf_mode/interfaces_loopback.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,6 @@ # 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 diff --git a/src/conf_mode/interfaces_macsec.py b/src/conf_mode/interfaces_macsec.py index 0a927ac88..eb0ca9a8b 100755 --- a/src/conf_mode/interfaces_macsec.py +++ b/src/conf_mode/interfaces_macsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -16,7 +16,6 @@ import os -from netifaces import interfaces from sys import exit from vyos.config import Config @@ -35,6 +34,7 @@ from vyos.ifconfig import Interface from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search +from vyos.utils.network import interface_exists from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag @@ -172,8 +172,8 @@ def apply(macsec): if 'deleted' in macsec or 'shutdown_required' in macsec: call(f'systemctl stop {systemd_service}') - if macsec['ifname'] in interfaces(): - tmp = MACsecIf(macsec['ifname']) + if interface_exists(macsec['ifname']): + tmp = MACsecIf(**macsec) tmp.remove() if 'deleted' in macsec: diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index 45569dd21..0ecffd3be 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -16,7 +16,6 @@ import os import re -import tempfile from cryptography.hazmat.primitives.asymmetric import ec from glob import glob @@ -26,7 +25,6 @@ from ipaddress import IPv4Network from ipaddress import IPv6Address from ipaddress import IPv6Network from ipaddress import summarize_address_range -from netifaces import interfaces from secrets import SystemRandom from shutil import rmtree @@ -63,6 +61,7 @@ from vyos.utils.process import call from vyos.utils.permission import chown from vyos.utils.process import cmd from vyos.utils.network import is_addr_assigned +from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag @@ -199,6 +198,12 @@ def verify_pki(openvpn): raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}') if 'dh_params' in tls: + if 'dh' not in pki: + raise ConfigError(f'pki dh is not configured') + proposed_dh = tls['dh_params'] + if proposed_dh not in pki['dh'].keys(): + raise ConfigError(f"pki dh '{proposed_dh}' is not configured") + pki_dh = pki['dh'][tls['dh_params']] dh_params = load_dh_parameters(pki_dh['parameters']) dh_numbers = dh_params.parameter_numbers() @@ -683,7 +688,7 @@ def apply(openvpn): if os.path.isfile(cleanup_file): os.unlink(cleanup_file) - if interface in interfaces(): + if interface_exists(interface): VTunIf(interface).remove() # dynamically load/unload DCO Kernel extension if requested diff --git a/src/conf_mode/interfaces_pppoe.py b/src/conf_mode/interfaces_pppoe.py index 42f084309..412676c7d 100755 --- a/src/conf_mode/interfaces_pppoe.py +++ b/src/conf_mode/interfaces_pppoe.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,16 +17,12 @@ import os from sys import exit -from copy import deepcopy -from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed -from vyos.configdict import get_pppoe_interfaces from vyos.configverify import verify_authentication from vyos.configverify import verify_source_interface -from vyos.configverify import verify_interface_exists from vyos.configverify import verify_vrf from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect diff --git a/src/conf_mode/interfaces_pseudo-ethernet.py b/src/conf_mode/interfaces_pseudo-ethernet.py index dce5c2358..446beffd3 100755 --- a/src/conf_mode/interfaces_pseudo-ethernet.py +++ b/src/conf_mode/interfaces_pseudo-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from sys import exit -from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict @@ -29,8 +28,8 @@ 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_mirror_redirect -from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import MACVLANIf +from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag @@ -84,8 +83,8 @@ def generate(peth): def apply(peth): # Check if the MACVLAN interface already exists if 'rebuild_required' in peth or 'deleted' in peth: - if peth['ifname'] in interfaces(): - p = MACVLANIf(peth['ifname']) + if interface_exists(peth['ifname']): + p = MACVLANIf(**peth) # MACVLAN is always needs to be recreated, # thus we can simply always delete it first. p.remove() diff --git a/src/conf_mode/interfaces_tunnel.py b/src/conf_mode/interfaces_tunnel.py index efa5ebc64..43ba72857 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-2022 yOS maintainers and contributors +# Copyright (C) 2018-2024 yOS 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,10 +14,7 @@ # 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 netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict @@ -31,10 +28,10 @@ from vyos.configverify import verify_vrf from vyos.configverify import verify_tunnel from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import Interface -from vyos.ifconfig import Section from vyos.ifconfig import TunnelIf -from vyos.utils.network import get_interface_config from vyos.utils.dict import dict_search +from vyos.utils.network import get_interface_config +from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag airbag.enable() @@ -202,7 +199,7 @@ def apply(tunnel): if ('deleted' in tunnel or 'encapsulation_changed' in tunnel or encap in ['gretap', 'ip6gretap', 'erspan', 'ip6erspan'] or remote in ['any'] or 'key_changed' in tunnel): - if interface in interfaces(): + if interface_exists(interface): tmp = Interface(interface) tmp.remove() if 'deleted' in tunnel: diff --git a/src/conf_mode/interfaces_virtual-ethernet.py b/src/conf_mode/interfaces_virtual-ethernet.py index 8efe89c41..cb6104f59 100755 --- a/src/conf_mode/interfaces_virtual-ethernet.py +++ b/src/conf_mode/interfaces_virtual-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -16,7 +16,6 @@ from sys import exit -from netifaces import interfaces from vyos import ConfigError from vyos import airbag from vyos.config import Config @@ -25,7 +24,7 @@ from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_vrf from vyos.ifconfig import VethIf - +from vyos.utils.network import interface_exists airbag.enable() def get_config(config=None): @@ -92,8 +91,8 @@ def generate(peth): def apply(veth): # Check if the Veth interface already exists if 'rebuild_required' in veth or 'deleted' in veth: - if veth['ifname'] in interfaces(): - p = VethIf(veth['ifname']) + if interface_exists(veth['ifname']): + p = VethIf(**veth) p.remove() if 'deleted' not in veth: diff --git a/src/conf_mode/interfaces_vti.py b/src/conf_mode/interfaces_vti.py index 9871810ae..e6a833df7 100755 --- a/src/conf_mode/interfaces_vti.py +++ b/src/conf_mode/interfaces_vti.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,14 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from netifaces import interfaces from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_mirror_redirect from vyos.ifconfig import VTIIf -from vyos.utils.dict import dict_search from vyos import ConfigError from vyos import airbag airbag.enable() diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py index 4251e611b..39365968a 100755 --- a/src/conf_mode/interfaces_vxlan.py +++ b/src/conf_mode/interfaces_vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,10 +14,7 @@ # 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 netifaces import interfaces from vyos.base import Warning from vyos.config import Config @@ -35,6 +32,7 @@ from vyos.ifconfig import Interface from vyos.ifconfig import VXLANIf from vyos.template import is_ipv6 from vyos.utils.dict import dict_search +from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag airbag.enable() @@ -212,8 +210,8 @@ def generate(vxlan): def apply(vxlan): # Check if the VXLAN interface already exists if 'rebuild_required' in vxlan or 'delete' in vxlan: - if vxlan['ifname'] in interfaces(): - v = VXLANIf(vxlan['ifname']) + if interface_exists(vxlan['ifname']): + v = VXLANIf(**vxlan) # VXLAN is super picky and the tunnel always needs to be recreated, # thus we can simply always delete it first. v.remove() diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py index 79e5d3f44..0e0b77877 100755 --- a/src/conf_mode/interfaces_wireguard.py +++ b/src/conf_mode/interfaces_wireguard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,7 +17,6 @@ from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py index 7338fe573..694a4e1ea 100755 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ b/src/conf_mode/load-balancing_reverse-proxy.py @@ -20,6 +20,9 @@ from sys import exit from shutil import rmtree from vyos.config import Config +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate +from vyos.utils.dict import dict_search from vyos.utils.process import call from vyos.utils.network import check_port_availability from vyos.utils.network import is_listen_port_bind_service @@ -33,8 +36,7 @@ airbag.enable() load_balancing_dir = '/run/haproxy' load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg' systemd_service = 'haproxy.service' -systemd_override = r'/run/systemd/system/haproxy.service.d/10-override.conf' - +systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf' def get_config(config=None): if config: @@ -54,7 +56,6 @@ def get_config(config=None): return lb - def verify(lb): if not lb: return None @@ -83,6 +84,15 @@ def verify(lb): if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf): raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"') + 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', front_config) + if tmp: verify_pki_ca_certificate(lb, tmp) + + def generate(lb): if not lb: # Delete /run/haproxy/haproxy.cfg diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index b3f38c04a..4cd9b570d 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,6 @@ # 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 jmespath -import json import os from sys import exit @@ -223,19 +221,19 @@ def generate(nat): render(nftables_static_nat_conf, 'firewall/nftables-static-nat.j2', nat) # dry-run newly generated configuration - tmp = run(f'nft -c -f {nftables_nat_config}') + tmp = run(f'nft --check --file {nftables_nat_config}') if tmp > 0: raise ConfigError('Configuration file errors encountered!') - tmp = run(f'nft -c -f {nftables_static_nat_conf}') + tmp = run(f'nft --check --file {nftables_static_nat_conf}') if tmp > 0: raise ConfigError('Configuration file errors encountered!') return None def apply(nat): - cmd(f'nft -f {nftables_nat_config}') - cmd(f'nft -f {nftables_static_nat_conf}') + cmd(f'nft --file {nftables_nat_config}') + cmd(f'nft --file {nftables_static_nat_conf}') if not nat or 'deleted' in nat: os.unlink(nftables_nat_config) diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py index 6026c61d0..c1e7ebf85 100755 --- a/src/conf_mode/nat64.py +++ b/src/conf_mode/nat64.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -26,7 +26,6 @@ from json import dumps as json_write from vyos import ConfigError from vyos import airbag from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configdict import is_node_changed from vyos.utils.dict import dict_search from vyos.utils.file import write_file diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index 4c1ead258..fe017527d 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,6 @@ # 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 jmespath -import json import os from sys import exit @@ -106,7 +104,7 @@ def apply(nat): if not nat: return None - cmd(f'nft -f {nftables_nat66_config}') + cmd(f'nft --file {nftables_nat66_config}') call_dependents() return None diff --git a/src/conf_mode/nat_cgnat.py b/src/conf_mode/nat_cgnat.py new file mode 100755 index 000000000..f41d66c66 --- /dev/null +++ b/src/conf_mode/nat_cgnat.py @@ -0,0 +1,288 @@ +#!/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 ipaddress +import jmespath +import os + +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.utils.process import cmd +from vyos.utils.process import run +from vyos import ConfigError +from vyos import airbag + +airbag.enable() + + +nftables_cgnat_config = '/run/nftables-cgnat.nft' + + +class IPOperations: + def __init__(self, ip_prefix: str): + self.ip_prefix = ip_prefix + self.ip_network = ipaddress.ip_network(ip_prefix) if '/' in ip_prefix else None + + def get_ips_count(self) -> int: + """Returns the number of IPs in a prefix or range. + + Example: + % ip = IPOperations('192.0.2.0/30') + % ip.get_ips_count() + 4 + % ip = IPOperations('192.0.2.0-192.0.2.2') + % ip.get_ips_count() + 3 + """ + if '-' in self.ip_prefix: + start_ip, end_ip = self.ip_prefix.split('-') + start_ip = ipaddress.ip_address(start_ip) + end_ip = ipaddress.ip_address(end_ip) + return int(end_ip) - int(start_ip) + 1 + elif '/31' in self.ip_prefix: + return 2 + elif '/32' in self.ip_prefix: + return 1 + else: + return sum( + 1 + for _ in [self.ip_network.network_address] + + list(self.ip_network.hosts()) + + [self.ip_network.broadcast_address] + ) + + def convert_prefix_to_list_ips(self) -> list: + """Converts a prefix or IP range to a list of IPs including the network and broadcast addresses. + + Example: + % ip = IPOperations('192.0.2.0/30') + % ip.convert_prefix_to_list_ips() + ['192.0.2.0', '192.0.2.1', '192.0.2.2', '192.0.2.3'] + % + % ip = IPOperations('192.0.0.1-192.0.2.5') + % ip.convert_prefix_to_list_ips() + ['192.0.2.1', '192.0.2.2', '192.0.2.3', '192.0.2.4', '192.0.2.5'] + """ + if '-' in self.ip_prefix: + start_ip, end_ip = self.ip_prefix.split('-') + start_ip = ipaddress.ip_address(start_ip) + end_ip = ipaddress.ip_address(end_ip) + return [ + str(ipaddress.ip_address(ip)) + for ip in range(int(start_ip), int(end_ip) + 1) + ] + elif '/31' in self.ip_prefix: + return [ + str(ip) + for ip in [ + self.ip_network.network_address, + self.ip_network.broadcast_address, + ] + ] + elif '/32' in self.ip_prefix: + return [str(self.ip_network.network_address)] + else: + return [ + str(ip) + for ip in [self.ip_network.network_address] + + list(self.ip_network.hosts()) + + [self.ip_network.broadcast_address] + ] + + +def generate_port_rules( + external_hosts: list, + internal_hosts: list, + port_count: int, + global_port_range: str = '1024-65535', +) -> list: + """Generates list of nftables rules for the batch file.""" + rules = [] + proto_map_elements = [] + other_map_elements = [] + start_port, end_port = map(int, global_port_range.split('-')) + total_possible_ports = (end_port - start_port) + 1 + + # Calculate the required number of ports per host + required_ports_per_host = port_count + + # Check if there are enough external addresses for all internal hosts + if required_ports_per_host * len(internal_hosts) > total_possible_ports * len( + external_hosts + ): + raise ConfigError("Not enough ports available for the specified parameters!") + + current_port = start_port + current_external_index = 0 + + for internal_host in internal_hosts: + external_host = external_hosts[current_external_index] + next_end_port = current_port + required_ports_per_host - 1 + + # If the port range exceeds the end_port, move to the next external host + while next_end_port > end_port: + current_external_index = (current_external_index + 1) % len(external_hosts) + external_host = external_hosts[current_external_index] + current_port = start_port + next_end_port = current_port + required_ports_per_host - 1 + + # Ensure the same port is not assigned to the same external host + if any( + rule.endswith(f'{external_host}:{current_port}-{next_end_port}') + for rule in rules + ): + raise ConfigError("Not enough ports available for the specified parameters") + + proto_map_elements.append( + f'{internal_host} : {external_host} . {current_port}-{next_end_port}' + ) + other_map_elements.append(f'{internal_host} : {external_host}') + + current_port = next_end_port + 1 + if current_port > end_port: + current_port = start_port + current_external_index += 1 # Move to the next external host + + return [proto_map_elements, other_map_elements] + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['nat', 'cgnat'] + config = conf.get_config_dict( + base, + get_first_key=True, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + with_recursive_defaults=True, + ) + + return config + + +def verify(config): + # bail out early - looks like removal from running config + if not config: + return None + + if 'pool' not in config: + raise ConfigError(f'Pool must be defined!') + if 'rule' not in config: + raise ConfigError(f'Rule must be defined!') + + # As PoC allow only one rule for CGNAT translations + # one internal pool and one external pool + if len(config['rule']) > 1: + raise ConfigError(f'Only one rule is allowed for translations!') + + for pool in ('external', 'internal'): + if pool not in config['pool']: + raise ConfigError(f'{pool} pool must be defined!') + for pool_name, pool_config in config['pool'][pool].items(): + if 'range' not in pool_config: + raise ConfigError( + f'Range for "{pool} pool {pool_name}" must be defined!' + ) + + for rule, rule_config in config['rule'].items(): + if 'source' not in rule_config: + raise ConfigError(f'Rule "{rule}" source pool must be defined!') + if 'pool' not in rule_config['source']: + raise ConfigError(f'Rule "{rule}" source pool must be defined!') + + if 'translation' not in rule_config: + raise ConfigError(f'Rule "{rule}" translation pool must be defined!') + + +def generate(config): + if not config: + return None + # first external pool as we allow only one as PoC + ext_pool_name = jmespath.search("rule.*.translation | [0]", config).get('pool') + int_pool_name = jmespath.search("rule.*.source | [0]", config).get('pool') + ext_query = f"pool.external.{ext_pool_name}.range | keys(@)" + int_query = f"pool.internal.{int_pool_name}.range" + external_ranges = jmespath.search(ext_query, config) + internal_ranges = [jmespath.search(int_query, config)] + + external_list_count = [] + external_list_hosts = [] + internal_list_count = [] + internal_list_hosts = [] + for ext_range in external_ranges: + # External hosts count + e_count = IPOperations(ext_range).get_ips_count() + external_list_count.append(e_count) + # External hosts list + e_hosts = IPOperations(ext_range).convert_prefix_to_list_ips() + external_list_hosts.extend(e_hosts) + for int_range in internal_ranges: + # Internal hosts count + i_count = IPOperations(int_range).get_ips_count() + internal_list_count.append(i_count) + # Internal hosts list + i_hosts = IPOperations(int_range).convert_prefix_to_list_ips() + internal_list_hosts.extend(i_hosts) + + external_host_count = sum(external_list_count) + internal_host_count = sum(internal_list_count) + ports_per_user = int( + jmespath.search(f'pool.external.{ext_pool_name}.per_user_limit.port', config) + ) + external_port_range: str = jmespath.search( + f'pool.external.{ext_pool_name}.external_port_range', config + ) + + proto_maps, other_maps = generate_port_rules( + external_list_hosts, internal_list_hosts, ports_per_user, external_port_range + ) + + config['proto_map_elements'] = ', '.join(proto_maps) + config['other_map_elements'] = ', '.join(other_maps) + + render(nftables_cgnat_config, 'firewall/nftables-cgnat.j2', config) + + # dry-run newly generated configuration + tmp = run(f'nft --check --file {nftables_cgnat_config}') + if tmp > 0: + raise ConfigError('Configuration file errors encountered!') + + +def apply(config): + if not config: + # Cleanup cgnat + cmd('nft delete table ip cgnat') + if os.path.isfile(nftables_cgnat_config): + os.unlink(nftables_cgnat_config) + return None + cmd(f'nft --file {nftables_cgnat_config}') + + +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/netns.py b/src/conf_mode/netns.py index 7cee33bc6..b57e46a0d 100755 --- a/src/conf_mode/netns.py +++ b/src/conf_mode/netns.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,14 +17,11 @@ import os from sys import exit -from tempfile import NamedTemporaryFile from vyos.config import Config from vyos.configdict import node_changed -from vyos.ifconfig import Interface from vyos.utils.process import call from vyos.utils.dict import dict_search -from vyos.utils.network import get_interface_config from vyos import ConfigError from vyos import airbag airbag.enable() diff --git a/src/conf_mode/policy_local-route.py b/src/conf_mode/policy_local-route.py index 91e4fce2c..f458f4e82 100755 --- a/src/conf_mode/policy_local-route.py +++ b/src/conf_mode/policy_local-route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,23 +14,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from itertools import product from sys import exit -from netifaces import interfaces from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configdict import leaf_node_changed -from vyos.template import render +from vyos.configverify import verify_interface_exists from vyos.utils.process import call from vyos import ConfigError from vyos import airbag airbag.enable() - def get_config(config=None): if config: @@ -227,8 +223,7 @@ def verify(pbr): if 'inbound_interface' in pbr_route['rule'][rule]: interface = pbr_route['rule'][rule]['inbound_interface'] - if interface not in interfaces(): - raise ConfigError(f'Interface "{interface}" does not exist') + verify_interface_exists(interface) return None diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py index 6d7a06714..c58fe1bce 100755 --- a/src/conf_mode/policy_route.py +++ b/src/conf_mode/policy_route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -177,7 +177,7 @@ def cleanup_table_marks(): cmd(f'{cmd_str} rule del fwmark {fwmark} table {table}') def apply(policy): - install_result = run(f'nft -f {nftables_conf}') + install_result = run(f'nft --file {nftables_conf}') if install_result == 1: raise ConfigError('Failed to apply policy based routing') diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py index 104711b55..90b6e4a31 100755 --- a/src/conf_mode/protocols_babel.py +++ b/src/conf_mode/protocols_babel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,15 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from sys import exit from vyos.config import Config from vyos.config import config_dict_merge from vyos.configdict import dict_merge from vyos.configdict import node_changed -from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.utils.dict import dict_search diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 37421efb4..1c01a9013 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,6 @@ # 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 vyos.config import Config from vyos.configverify import verify_vrf from vyos.template import is_ipv6 diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index f1c59cbde..2b16de775 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -285,6 +285,7 @@ def verify(bgp): elif tmp != 'default': raise ConfigError(f'{error_msg} "{tmp}"!') + peer_groups_context = dict() # Common verification for both peer-group and neighbor statements for neighbor in ['neighbor', 'peer_group']: # bail out early if there is no neighbor or peer-group statement @@ -301,6 +302,18 @@ def verify(bgp): raise ConfigError(f'Specified peer-group "{peer_group}" for '\ f'neighbor "{neighbor}" does not exist!') + if 'remote_as' in peer_config: + is_ibgp = True + if peer_config['remote_as'] != 'internal' and \ + peer_config['remote_as'] != bgp['system_as']: + is_ibgp = False + + if peer_group not in peer_groups_context: + peer_groups_context[peer_group] = is_ibgp + elif peer_groups_context[peer_group] != is_ibgp: + raise ConfigError(f'Peer-group members must be ' + f'all internal or all external') + if 'local_role' in peer_config: #Ensure Local Role has only one value. if len(peer_config['local_role']) > 1: @@ -450,15 +463,15 @@ def verify(bgp): verify_route_map(afi_config['route_map'][tmp], bgp) if 'route_reflector_client' in afi_config: - if 'remote_as' in peer_config and peer_config['remote_as'] != 'internal' and peer_config['remote_as'] != bgp['system_as']: + peer_group_as = peer_config.get('remote_as') + + if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): raise ConfigError('route-reflector-client only supported for iBGP peers') else: if 'peer_group' in peer_config: peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp) - if peer_group_as != None and peer_group_as != 'internal' and peer_group_as != bgp['system_as']: + if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): raise ConfigError('route-reflector-client only supported for iBGP peers') - else: - raise ConfigError('route-reflector-client only supported for iBGP peers') # Throw an error if a peer group is not configured for allow range for prefix in dict_search('listen.range', bgp) or []: diff --git a/src/conf_mode/protocols_igmp-proxy.py b/src/conf_mode/protocols_igmp-proxy.py index 40db417dd..afcef0985 100755 --- a/src/conf_mode/protocols_igmp-proxy.py +++ b/src/conf_mode/protocols_igmp-proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,10 +17,10 @@ import os from sys import exit -from netifaces import interfaces from vyos.base import Warning from vyos.config import Config +from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search @@ -65,8 +65,7 @@ def verify(igmp_proxy): upstream = 0 for interface, config in igmp_proxy['interface'].items(): - if interface not in interfaces(): - raise ConfigError(f'Interface "{interface}" does not exist') + verify_interface_exists(interface) if dict_search('role', config) == 'upstream': upstream += 1 diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 6c9925b80..9cadfd081 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -14,8 +14,6 @@ # 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 diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py index c339c6391..0bd68b7d8 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-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,7 +19,6 @@ import os from vyos.config import Config from vyos.configdict import node_changed from vyos.template import render -from vyos.utils.process import process_named_running from vyos.utils.process import run from vyos import ConfigError from vyos import airbag @@ -93,7 +92,7 @@ def generate(nhrp): return None def apply(nhrp): - nft_rc = run(f'nft -f {nhrp_nftables_conf}') + nft_rc = run(f'nft --file {nhrp_nftables_conf}') if nft_rc != 0: raise ConfigError('Failed to apply NHRP tunnel firewall rules') diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index afd767dbf..1bb172293 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -14,8 +14,6 @@ # 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 diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index bd47dfd00..9afac544d 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,6 @@ # 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 diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py index dd1550033..23416ff96 100755 --- a/src/conf_mode/protocols_ripng.py +++ b/src/conf_mode/protocols_ripng.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,6 @@ # 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 diff --git a/src/conf_mode/protocols_segment-routing.py b/src/conf_mode/protocols_segment-routing.py index d865c2ac0..b36c2ca11 100755 --- a/src/conf_mode/protocols_segment-routing.py +++ b/src/conf_mode/protocols_segment-routing.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,6 @@ # 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 diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 5def8d645..a2373218a 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,6 @@ # 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 diff --git a/src/conf_mode/protocols_static_neighbor-proxy.py b/src/conf_mode/protocols_static_neighbor-proxy.py index 10cc1e748..8a1ea1df9 100755 --- a/src/conf_mode/protocols_static_neighbor-proxy.py +++ b/src/conf_mode/protocols_static_neighbor-proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,19 +14,14 @@ # 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.configdict import node_changed from vyos.utils.process import call from vyos import ConfigError from vyos import airbag - airbag.enable() - def get_config(config=None): if config: conf = config @@ -38,9 +33,7 @@ def get_config(config=None): return config - def verify(config): - if 'arp' in config: for neighbor, neighbor_conf in config['arp'].items(): if 'interface' not in neighbor_conf: @@ -55,11 +48,9 @@ def verify(config): f"ARP neighbor-proxy for '{neighbor}' requires an interface to be set!" ) - def generate(config): pass - def apply(config): if not config: # Cleanup proxy @@ -83,7 +74,6 @@ def apply(config): for interface in neighbor_conf['interface']: call(f'ip -6 neighbor add proxy {neighbor} dev {interface}') - if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index 4a0b4d0c5..3dfb4bab8 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -14,15 +14,14 @@ # 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 netifaces import interfaces -from vyos.base import Warning from vyos.config import Config -from vyos.configdep import set_dependents, call_dependents +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.configdict import dict_merge +from vyos.configverify import verify_interface_exists from vyos.ifconfig import Section from vyos.qos import CAKE from vyos.qos import DropTail @@ -36,8 +35,8 @@ from vyos.qos import RateLimiter from vyos.qos import RoundRobin from vyos.qos import TrafficShaper from vyos.qos import TrafficShaperHFSC -from vyos.utils.process import run from vyos.utils.dict import dict_search_recursive +from vyos.utils.process import run from vyos import ConfigError from vyos import airbag airbag.enable() @@ -214,11 +213,10 @@ def apply(qos): return None for interface, interface_config in qos['interface'].items(): - if not os.path.exists(f'/sys/class/net/{interface}'): + if not verify_interface_exists(interface, warning_only=True): # When shaper is bound to a dialup (e.g. PPPoE) interface it is # possible that it is yet not availbale when to QoS code runs. - # Skip the configuration and inform the user - Warning(f'Interface "{interface}" does not exist!') + # Skip the configuration and inform the user via warning_only=True continue for direction in ['egress', 'ingress']: diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index ba3d69b07..e89448e2d 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -16,6 +16,7 @@ import os +from glob import glob from ipaddress import ip_address from ipaddress import ip_network from netaddr import IPRange @@ -28,6 +29,7 @@ from vyos.template import render from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.file import chmod_775 +from vyos.utils.file import chown from vyos.utils.file import makedir from vyos.utils.file import write_file from vyos.utils.process import call @@ -42,6 +44,7 @@ ctrl_config_file = '/run/kea/kea-ctrl-agent.conf' ctrl_socket = '/run/kea/dhcp4-ctrl-socket' config_file = '/run/kea/kea-dhcp4.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' @@ -143,8 +146,12 @@ def get_config(config=None): dhcp['shared_network_name'][network]['subnet'][subnet].update( {'range' : new_range_dict}) - 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) + 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) return dhcp @@ -161,7 +168,6 @@ def verify(dhcp): # Inspect shared-network/subnet listen_ok = False subnets = [] - failover_ok = False shared_networks = len(dhcp['shared_network_name']) disabled_shared_networks = 0 @@ -316,7 +322,7 @@ def verify(dhcp): raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability') for address in (dict_search('listen_address', dhcp) or []): - if is_addr_assigned(address): + if is_addr_assigned(address, include_vrf=True): listen_ok = True # no need to probe further networks, we have one that is valid continue @@ -351,6 +357,10 @@ def generate(dhcp): makedir(lease_dir, group='vyattacfg') chmod_775(lease_dir) + # Ensure correct permissions on lease files + backups + for file in glob(lease_file_glob): + chown(file, user=user_group, group='vyattacfg') + # Create lease file if necessary and let kea own it - 'kea-lfc' expects it that way if not os.path.exists(lease_file): write_file(lease_file, '', user=user_group, group=user_group, mode=0o644) diff --git a/src/conf_mode/service_dhcpv6-server.py b/src/conf_mode/service_dhcpv6-server.py index add83eb0d..c7333dd3a 100755 --- a/src/conf_mode/service_dhcpv6-server.py +++ b/src/conf_mode/service_dhcpv6-server.py @@ -16,6 +16,7 @@ import os +from glob import glob from ipaddress import ip_address from ipaddress import ip_network from sys import exit @@ -24,6 +25,7 @@ from vyos.config import Config from vyos.template import render from vyos.utils.process import call from vyos.utils.file import chmod_775 +from vyos.utils.file import chown from vyos.utils.file import makedir from vyos.utils.file import write_file from vyos.utils.dict import dict_search @@ -35,6 +37,7 @@ airbag.enable() config_file = '/run/kea/kea-dhcp6.conf' ctrl_socket = '/run/kea/dhcp6-ctrl-socket' lease_file = '/config/dhcp/dhcp6-leases.csv' +lease_file_glob = '/config/dhcp/dhcp6-leases*' user_group = '_kea' def get_config(config=None): @@ -224,6 +227,10 @@ def generate(dhcpv6): makedir(lease_dir, group='vyattacfg') chmod_775(lease_dir) + # Ensure correct permissions on lease files + backups + for file in glob(lease_file_glob): + chown(file, user=user_group, group='vyattacfg') + # Create lease file if necessary and let kea own it - 'kea-lfc' expects it that way if not os.path.exists(lease_file): write_file(lease_file, '', user=user_group, group=user_group, mode=0o644) diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py index ecad765f4..7e863073a 100755 --- a/src/conf_mode/service_dns_forwarding.py +++ b/src/conf_mode/service_dns_forwarding.py @@ -16,7 +16,6 @@ import os -from netifaces import interfaces from sys import exit from glob import glob @@ -24,9 +23,9 @@ from vyos.config import Config from vyos.hostsd_client import Client as hostsd_client from vyos.template import render from vyos.template import bracketize_ipv6 +from vyos.utils.network import interface_exists from vyos.utils.process import call from vyos.utils.permission import chown - from vyos import ConfigError from vyos import airbag airbag.enable() @@ -330,7 +329,7 @@ def apply(dns): # names (DHCP) to use DNS servers. We need to check if the # value is an interface name - only if this is the case, add the # interface based DNS forwarder. - if interface in interfaces(): + if interface_exists(interface): hc.add_name_server_tags_recursor(['dhcp-' + interface, 'dhcpv6-' + interface ]) diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py index 46efc3c93..9e58b4c72 100755 --- a/src/conf_mode/service_https.py +++ b/src/conf_mode/service_https.py @@ -24,13 +24,14 @@ from time import sleep from vyos.base import Warning from vyos.config import Config from vyos.config import config_dict_merge -from vyos.configdiff import get_config_diff 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.defaults import api_config_state from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.pki import wrap_dh_parameters -from vyos.pki import load_dh_parameters from vyos.template import render from vyos.utils.dict import dict_search from vyos.utils.process import call @@ -84,33 +85,14 @@ def verify(https): if https is None: return None - if 'certificates' in https and 'certificate' in https['certificates']: - cert_name = https['certificates']['certificate'] - if 'pki' not in https: - raise ConfigError('PKI is not configured!') - - if cert_name not in https['pki']['certificate']: - raise ConfigError('Invalid certificate in configuration!') + if dict_search('certificates.certificate', https) != None: + verify_pki_certificate(https, https['certificates']['certificate']) - pki_cert = https['pki']['certificate'][cert_name] - - if 'certificate' not in pki_cert: - raise ConfigError('Missing certificate in configuration!') + tmp = dict_search('certificates.ca_certificate', https) + if tmp != None: verify_pki_ca_certificate(https, tmp) - if 'private' not in pki_cert or 'key' not in pki_cert['private']: - raise ConfigError('Missing certificate private key in configuration!') - - if 'dh_params' in https['certificates']: - dh_name = https['certificates']['dh_params'] - if dh_name not in https['pki']['dh']: - raise ConfigError('Invalid DH parameter in configuration!') - - pki_dh = https['pki']['dh'][dh_name] - dh_params = load_dh_parameters(pki_dh['parameters']) - dh_numbers = dh_params.parameter_numbers() - dh_bits = dh_numbers.p.bit_length() - if dh_bits < 2048: - raise ConfigError(f'Minimum DH key-size is 2048 bits') + tmp = dict_search('certificates.dh_params', https) + if tmp != None: verify_pki_dh_parameters(https, tmp, 2048) else: Warning('No certificate specified, using build-in self-signed certificates. '\ @@ -214,7 +196,8 @@ def apply(https): https_service_name = 'nginx.service' if https is None: - call(f'systemctl stop {http_api_service_name}') + if is_systemd_service_active(http_api_service_name): + call(f'systemctl stop {http_api_service_name}') call(f'systemctl stop {https_service_name}') return diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 852b714eb..11e950782 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -68,8 +68,8 @@ def verify(ipoe): for interface, iface_config in ipoe['interface'].items(): verify_interface_exists(interface) if 'client_subnet' in iface_config and 'vlan' in iface_config: - raise ConfigError('Option "client-subnet" incompatible with "vlan"!' - 'Use "ipoe client-ip-pool" instead.') + raise ConfigError('Option "client-subnet" and "vlan" are mutually exclusive, ' + 'use "client-ip-pool" instead!') verify_accel_ppp_authentication(ipoe, local_users=False) verify_accel_ppp_ip_pool(ipoe) diff --git a/src/conf_mode/service_lldp.py b/src/conf_mode/service_lldp.py index 3c647a0e8..04b1db880 100755 --- a/src/conf_mode/service_lldp.py +++ b/src/conf_mode/service_lldp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017-2022 VyOS maintainers and contributors +# Copyright (C) 2017-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 @@ -24,7 +24,6 @@ from vyos.utils.network import is_addr_assigned from vyos.utils.network import is_loopback_addr from vyos.version import get_version_data from vyos.utils.process import call -from vyos.utils.dict import dict_search from vyos.template import render from vyos import ConfigError from vyos import airbag diff --git a/src/conf_mode/service_mdns_repeater.py b/src/conf_mode/service_mdns_repeater.py index 6526c23d1..207da5e03 100755 --- a/src/conf_mode/service_mdns_repeater.py +++ b/src/conf_mode/service_mdns_repeater.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017-2022 VyOS maintainers and contributors +# Copyright (C) 2017-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -18,9 +18,10 @@ import os from json import loads from sys import exit -from netifaces import ifaddresses, interfaces, AF_INET, AF_INET6 +from netifaces import ifaddresses, AF_INET, AF_INET6 from vyos.config import Config +from vyos.configverify import verify_interface_exists from vyos.ifconfig.vrrp import VRRP from vyos.template import render from vyos.utils.process import call @@ -64,8 +65,7 @@ def verify(mdns): # For mdns-repeater to work it is essential that the interfaces has # an IPv4 address assigned for interface in mdns['interface']: - if interface not in interfaces(): - raise ConfigError(f'Interface "{interface}" does not exist!') + verify_interface_exists(interface) if mdns['ip_version'] in ['ipv4', 'both'] and AF_INET not in ifaddresses(interface): raise ConfigError('mDNS repeater requires an IPv4 address to be ' diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index c9d1e805f..b9d174933 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -38,6 +38,16 @@ airbag.enable() pppoe_conf = r'/run/accel-pppd/pppoe.conf' pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets' +def convert_pado_delay(pado_delay): + new_pado_delay = {'delays_without_sessions': [], + 'delays_with_sessions': []} + for delay, sessions in pado_delay.items(): + if not sessions: + new_pado_delay['delays_without_sessions'].append(delay) + else: + new_pado_delay['delays_with_sessions'].append((delay, int(sessions['sessions']))) + return new_pado_delay + def get_config(config=None): if config: conf = config @@ -54,6 +64,10 @@ def get_config(config=None): # Multiple named pools require ordered values T5099 pppoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', pppoe)) + if dict_search('pado_delay', pppoe): + pado_delay = dict_search('pado_delay', pppoe) + pppoe['pado_delay'] = convert_pado_delay(pado_delay) + # reload-or-restart does not implemented in accel-ppp # use this workaround until it will be implemented # https://phabricator.accel-ppp.org/T3 @@ -65,6 +79,17 @@ def get_config(config=None): pppoe['server_type'] = 'pppoe' return pppoe +def verify_pado_delay(pppoe): + if 'pado_delay' in pppoe: + pado_delay = pppoe['pado_delay'] + + delays_without_sessions = pado_delay['delays_without_sessions'] + if len(delays_without_sessions) > 1: + raise ConfigError( + f'Cannot add more then ONE pado-delay without sessions, ' + f'but {len(delays_without_sessions)} were set' + ) + def verify(pppoe): if not pppoe: return None @@ -73,7 +98,7 @@ def verify(pppoe): verify_accel_ppp_ip_pool(pppoe) verify_accel_ppp_name_servers(pppoe) verify_accel_ppp_wins_servers(pppoe) - + verify_pado_delay(pppoe) if 'interface' not in pppoe: raise ConfigError('At least one listen interface must be defined!') diff --git a/src/conf_mode/service_ssh.py b/src/conf_mode/service_ssh.py index ee5e1eca2..9abdd33dc 100755 --- a/src/conf_mode/service_ssh.py +++ b/src/conf_mode/service_ssh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -30,7 +30,6 @@ from vyos import airbag airbag.enable() config_file = r'/run/sshd/sshd_config' -systemd_override = r'/run/systemd/system/ssh.service.d/override.conf' sshguard_config_file = '/etc/sshguard/sshguard.conf' sshguard_whitelist = '/etc/sshguard/whitelist' @@ -81,8 +80,6 @@ def generate(ssh): if not ssh: if os.path.isfile(config_file): os.unlink(config_file) - if os.path.isfile(systemd_override): - os.unlink(systemd_override) return None @@ -99,13 +96,10 @@ def generate(ssh): call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}') render(config_file, 'ssh/sshd_config.j2', ssh) - render(systemd_override, 'ssh/override.conf.j2', ssh) if 'dynamic_protection' in ssh: render(sshguard_config_file, 'ssh/sshguard_config.j2', ssh) render(sshguard_whitelist, 'ssh/sshguard_whitelist.j2', ssh) - # Reload systemd manager configuration - call('systemctl daemon-reload') return None @@ -114,7 +108,7 @@ def apply(ssh): systemd_service_sshguard = 'sshguard.service' if not ssh: # SSH access is removed in the commit - call(f'systemctl stop {systemd_service_ssh}') + call(f'systemctl stop ssh@*.service') call(f'systemctl stop {systemd_service_sshguard}') return None @@ -126,9 +120,13 @@ def apply(ssh): # we need to restart the service if e.g. the VRF name changed systemd_action = 'reload-or-restart' if 'restart_required' in ssh: + # this is only true if something for the VRFs changed, thus we + # stop all VRF services and only restart then new ones + call(f'systemctl stop ssh@*.service') systemd_action = 'restart' - call(f'systemctl {systemd_action} {systemd_service_ssh}') + for vrf in ssh['vrf']: + call(f'systemctl {systemd_action} ssh@{vrf}.service') return None if __name__ == '__main__': diff --git a/src/conf_mode/service_tftp-server.py b/src/conf_mode/service_tftp-server.py index 3ad346e2e..5b7303c40 100755 --- a/src/conf_mode/service_tftp-server.py +++ b/src/conf_mode/service_tftp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import stat import pwd from copy import deepcopy diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py index cf26bf9ce..0df8dc09e 100755 --- a/src/conf_mode/service_upnp.py +++ b/src/conf_mode/service_upnp.py @@ -54,9 +54,7 @@ def get_config(config=None): def get_all_interface_addr(prefix, filter_dev, filter_family): list_addr = [] - interfaces = netifaces.interfaces() - - for interface in interfaces: + for interface in netifaces.interfaces(): if filter_dev and interface in filter_dev: continue addrs = netifaces.ifaddresses(interface) diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py index 3d42389f6..031fe63b0 100755 --- a/src/conf_mode/system_conntrack.py +++ b/src/conf_mode/system_conntrack.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,19 +15,16 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re from sys import exit from vyos.config import Config from vyos.configdep import set_dependents, call_dependents -from vyos.utils.process import process_named_running 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.process import cmd from vyos.utils.process import rc_cmd -from vyos.utils.process import run from vyos.template import render from vyos import ConfigError from vyos import airbag @@ -223,7 +220,7 @@ def apply(conntrack): cmd(f'modprobe -a {module_str}') # Load new nftables ruleset - install_result, output = rc_cmd(f'nft -f {nftables_ct_file}') + install_result, output = rc_cmd(f'nft --file {nftables_ct_file}') if install_result == 1: raise ConfigError(f'Failed to apply configuration: {output}') diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index a888b125e..19bbb8875 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,13 +15,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re from pathlib import Path from vyos.config import Config from vyos.utils.process import call -from vyos.utils.file import read_file -from vyos.utils.file import write_file from vyos.system import grub_util from vyos.template import render from vyos import ConfigError diff --git a/src/conf_mode/system_flow-accounting.py b/src/conf_mode/system_flow-accounting.py index 206f513c8..2dacd92da 100755 --- a/src/conf_mode/system_flow-accounting.py +++ b/src/conf_mode/system_flow-accounting.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -20,11 +20,10 @@ import re from sys import exit from ipaddress import ip_address -from vyos.base import Warning from vyos.config import Config from vyos.config import config_dict_merge from vyos.configverify import verify_vrf -from vyos.ifconfig import Section +from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos.utils.process import cmd @@ -184,10 +183,7 @@ def verify(flow_config): # check that all configured interfaces exists in the system for interface in flow_config['interface']: - if interface not in Section.interfaces(): - # Changed from error to warning to allow adding dynamic interfaces - # and interface templates - Warning(f'Interface "{interface}" is not presented in the system') + verify_interface_exists(interface, warning_only=True) # check sFlow configuration if 'sflow' in flow_config: diff --git a/src/conf_mode/system_frr.py b/src/conf_mode/system_frr.py index 07f291000..d9ac543d0 100755 --- a/src/conf_mode/system_frr.py +++ b/src/conf_mode/system_frr.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,7 +14,6 @@ # 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 pathlib import Path from sys import exit from vyos import ConfigError diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py index 6204cf247..8975cadb6 100755 --- a/src/conf_mode/system_host-name.py +++ b/src/conf_mode/system_host-name.py @@ -71,9 +71,9 @@ def get_config(config=None): hosts['nameserver'].append(ns) else: tmp = '' - if_type = Section.section(ns) - if conf.exists(['interfaces', if_type, ns, 'address']): - tmp = conf.return_values(['interfaces', if_type, ns, 'address']) + config_path = Section.get_config_path(ns) + if conf.exists(['interfaces', config_path, 'address']): + tmp = conf.return_values(['interfaces', config_path, 'address']) hosts['nameservers_dhcp_interfaces'].update({ ns : tmp }) diff --git a/src/conf_mode/system_ip.py b/src/conf_mode/system_ip.py index 833f89554..2a0bda91a 100755 --- a/src/conf_mode/system_ip.py +++ b/src/conf_mode/system_ip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -22,7 +22,6 @@ from vyos.configverify import verify_route_map from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.file import write_file -from vyos.utils.process import call from vyos.utils.process import is_systemd_service_active from vyos.utils.system import sysctl_write @@ -82,11 +81,6 @@ def apply(opt): value = '0' if (tmp != None) else '1' write_file('/proc/sys/net/ipv4/conf/all/forwarding', value) - # enable/disable IPv4 directed broadcast forwarding - tmp = dict_search('disable_directed_broadcast', opt) - value = '0' if (tmp != None) else '1' - write_file('/proc/sys/net/ipv4/conf/all/bc_forwarding', value) - # configure multipath tmp = dict_search('multipath.ignore_unreachable_nexthops', opt) value = '1' if (tmp != None) else '0' diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py index 49306c894..20121f170 100755 --- a/src/conf_mode/system_login.py +++ b/src/conf_mode/system_login.py @@ -336,27 +336,31 @@ def apply(login): command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea {user}' try: cmd(command) - # we should not rely on the value stored in - # user_config['home_directory'], as a crazy user will choose - # username root or any other system user which will fail. + # we should not rely on the value stored in user_config['home_directory'], as a + # crazy user will choose username root or any other system user which will fail. # # XXX: Should we deny using root at all? home_dir = getpwnam(user).pw_dir - # T5875: ensure UID is properly set on home directory if user is re-added - # the home directory will always exist, as it's created above by --create-home, - # retrieve current owner of home directory and adjust it on demand - dir_owner = getpwuid(os.stat(home_dir).st_uid).pw_name - if dir_owner != user: - chown(home_dir, user=user, recursive=True) - + # always re-render SSH keys with appropriate permissions render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.j2', user_config, permission=0o600, formater=lambda _: _.replace(""", '"'), user=user, group='users') - except Exception as e: raise ConfigError(f'Adding user "{user}" raised exception: "{e}"') + # T5875: ensure UID is properly set on home directory if user is re-added + # the home directory will always exist, as it's created above by --create-home, + # retrieve current owner of home directory and adjust on demand + dir_owner = None + try: + dir_owner = getpwuid(os.stat(home_dir).st_uid).pw_name + except: + pass + + if dir_owner != user: + chown(home_dir, user=user, recursive=True) + # Generate 2FA/MFA One-Time-Pad configuration if dict_search('authentication.otp.key', user_config): enable_otp = True diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index 7ed451e16..a2e5db575 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -16,12 +16,12 @@ import os -from netifaces import interfaces from sys import exit from time import sleep from vyos.config import Config from vyos.configverify import verify_source_interface +from vyos.configverify import verify_interface_exists from vyos.system import grub_util from vyos.template import render from vyos.utils.process import cmd @@ -56,9 +56,7 @@ def verify(options): if 'http_client' in options: config = options['http_client'] if 'source_interface' in config: - if not config['source_interface'] in interfaces(): - raise ConfigError(f'Source interface {source_interface} does not ' - f'exist'.format(**config)) + verify_interface_exists(config['source_interface']) if {'source_address', 'source_interface'} <= set(config): raise ConfigError('Can not define both HTTP source-interface and source-address') diff --git a/src/conf_mode/system_timezone.py b/src/conf_mode/system_timezone.py index cd3d4b229..39770fdb4 100755 --- a/src/conf_mode/system_timezone.py +++ b/src/conf_mode/system_timezone.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys -import os from copy import deepcopy from vyos.config import Config diff --git a/src/conf_mode/system_update-check.py b/src/conf_mode/system_update-check.py index 8d641a97d..71ac13e51 100755 --- a/src/conf_mode/system_update-check.py +++ b/src/conf_mode/system_update-check.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,9 +14,7 @@ # 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 -import jmespath from pathlib import Path from sys import exit @@ -27,7 +25,6 @@ from vyos import ConfigError from vyos import airbag airbag.enable() - base = ['system', 'update-check'] service_name = 'vyos-system-update' service_conf = Path(f'/run/{service_name}.conf') diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 0c2f232df..dc78c755e 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -21,7 +21,6 @@ import jmespath from sys import exit from time import sleep -from time import time from vyos.base import Warning from vyos.config import Config @@ -47,7 +46,6 @@ from vyos.utils.network import interface_exists from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.process import call -from vyos.utils.process import run from vyos import ConfigError from vyos import airbag airbag.enable() @@ -168,9 +166,7 @@ def verify(ipsec): for interface in ipsec['interface']: # exclude check interface for dynamic interfaces if tmp.match(interface): - if not interface_exists(interface): - Warning(f'Interface "{interface}" does not exist yet and cannot be used ' - f'for IPsec until it is up!') + verify_interface_exists(interface, warning_only=True) else: verify_interface_exists(interface) diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 08e4fc6db..8159fedea 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,6 +19,8 @@ from sys import exit from vyos.base import Warning from vyos.config import Config +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render @@ -75,7 +77,7 @@ def verify(ocserv): if "accounting" in ocserv: if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]: if not origin["accounting"]['radius']['server']: - raise ConfigError('Openconnect accounting mode radius requires at least one RADIUS server') + raise ConfigError('OpenConnect accounting mode radius requires at least one RADIUS server') if "authentication" not in ocserv or "mode" not in ocserv["authentication"]: raise ConfigError('Accounting depends on OpenConnect authentication configuration') elif "radius" not in ocserv["authentication"]["mode"]: @@ -89,12 +91,12 @@ def verify(ocserv): raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration') if "radius" in ocserv["authentication"]["mode"]: if not ocserv["authentication"]['radius']['server']: - raise ConfigError('Openconnect authentication mode radius requires at least one RADIUS server') + raise ConfigError('OpenConnect authentication mode radius requires at least one RADIUS server') if "local" in ocserv["authentication"]["mode"]: if not ocserv.get("authentication", {}).get("local_users"): - raise ConfigError('openconnect mode local required at least one user') + raise ConfigError('OpenConnect mode local required at least one user') if not ocserv["authentication"]["local_users"]["username"]: - raise ConfigError('openconnect mode local required at least one user') + raise ConfigError('OpenConnect mode local required at least one user') else: # For OTP mode: verify that each local user has an OTP key if "otp" in ocserv["authentication"]["mode"]["local"]: @@ -127,40 +129,20 @@ def verify(ocserv): if 'default_config' not in ocserv["authentication"]["identity_based_config"]: raise ConfigError('OpenConnect identity-based-config enabled but default-config not set') else: - raise ConfigError('openconnect authentication mode required') + raise ConfigError('OpenConnect authentication mode required') else: - raise ConfigError('openconnect authentication credentials required') + raise ConfigError('OpenConnect authentication credentials required') # Check ssl if 'ssl' not in ocserv: - raise ConfigError('openconnect ssl required') + raise ConfigError('SSL missing on OpenConnect config!') - if not ocserv['pki'] or 'certificate' not in ocserv['pki']: - raise ConfigError('PKI not configured') + if 'certificate' not in ocserv['ssl']: + raise ConfigError('SSL certificate missing on OpenConnect config!') + verify_pki_certificate(ocserv, ocserv['ssl']['certificate']) - ssl = ocserv['ssl'] - if 'certificate' not in ssl: - raise ConfigError('openconnect ssl certificate required') - - cert_name = ssl['certificate'] - - if cert_name not in ocserv['pki']['certificate']: - raise ConfigError('Invalid openconnect ssl certificate') - - cert = ocserv['pki']['certificate'][cert_name] - - if 'certificate' not in cert: - raise ConfigError('Missing certificate in PKI') - - if 'private' not in cert or 'key' not in cert['private']: - raise ConfigError('Missing private key in PKI') - - if 'ca_certificate' in ssl: - if 'ca' not in ocserv['pki']: - raise ConfigError('PKI not configured') - - if ssl['ca_certificate'] not in ocserv['pki']['ca']: - raise ConfigError('Invalid openconnect ssl CA certificate') + if 'ca_certificate' in ocserv['ssl']: + verify_pki_ca_certificate(ocserv, ocserv['ssl']['ca_certificate']) # Check network settings if "network_settings" in ocserv: @@ -172,7 +154,7 @@ def verify(ocserv): else: ocserv["network_settings"]["push_route"] = ["default"] else: - raise ConfigError('openconnect network settings required') + raise ConfigError('OpenConnect network settings required!') def generate(ocserv): if not ocserv: @@ -276,7 +258,7 @@ def apply(ocserv): break sleep(0.250) if counter > 5: - raise ConfigError('openconnect failed to start, check the logs for details') + raise ConfigError('OpenConnect failed to start, check the logs for details') break counter += 1 diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 8661a8aff..7490fd0e0 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -20,6 +20,8 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict +from vyos.configverify import verify_pki_certificate +from vyos.configverify import verify_pki_ca_certificate from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render @@ -46,51 +48,6 @@ cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key') ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem') -def verify_certificate(config): - # - # SSL certificate checks - # - if not config['pki']: - raise ConfigError('PKI is not configured') - - if 'ssl' not in config: - raise ConfigError('SSL missing on SSTP config') - - ssl = config['ssl'] - - # CA - if 'ca_certificate' not in ssl: - raise ConfigError('SSL CA certificate missing on SSTP config') - - ca_name = ssl['ca_certificate'] - - if ca_name not in config['pki']['ca']: - raise ConfigError('Invalid CA certificate on SSTP config') - - if 'certificate' not in config['pki']['ca'][ca_name]: - raise ConfigError('Missing certificate data for CA certificate on SSTP config') - - # Certificate - if 'certificate' not in ssl: - raise ConfigError('SSL certificate missing on SSTP config') - - cert_name = ssl['certificate'] - - if cert_name not in config['pki']['certificate']: - raise ConfigError('Invalid certificate on SSTP config') - - pki_cert = config['pki']['certificate'][cert_name] - - if 'certificate' not in pki_cert: - raise ConfigError('Missing certificate data for certificate on SSTP config') - - if 'private' not in pki_cert or 'key' not in pki_cert['private']: - raise ConfigError('Missing private key for certificate on SSTP config') - - if 'password_protected' in pki_cert['private']: - raise ConfigError('Encrypted private key is not supported on SSTP config') - - def get_config(config=None): if config: conf = config @@ -124,7 +81,17 @@ def verify(sstp): verify_accel_ppp_ip_pool(sstp) verify_accel_ppp_name_servers(sstp) verify_accel_ppp_wins_servers(sstp) - verify_certificate(sstp) + + if 'ssl' not in sstp: + raise ConfigError('SSL missing on SSTP config!') + + if 'certificate' not in sstp['ssl']: + raise ConfigError('SSL certificate missing on SSTP config!') + verify_pki_certificate(sstp, sstp['ssl']['certificate']) + + if 'ca_certificate' not in sstp['ssl']: + raise ConfigError('SSL CA certificate missing on SSTP config!') + verify_pki_ca_certificate(sstp, sstp['ssl']['ca_certificate']) def generate(sstp): @@ -154,15 +121,15 @@ def generate(sstp): def apply(sstp): + systemd_service = 'accel-ppp@sstp.service' if not sstp: - call('systemctl stop accel-ppp@sstp.service') + call(f'systemctl stop {systemd_service}') for file in [sstp_chap_secrets, sstp_conf]: if os.path.exists(file): os.unlink(file) - return None - call('systemctl restart accel-ppp@sstp.service') + call(f'systemctl reload-or-restart {systemd_service}') if __name__ == '__main__': diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 16908100f..1fc813189 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -14,8 +14,6 @@ # 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 json import loads @@ -33,6 +31,7 @@ from vyos.utils.network import get_vrf_members from vyos.utils.network import interface_exists from vyos.utils.process import call from vyos.utils.process import cmd +from vyos.utils.process import popen from vyos.utils.system import sysctl_write from vyos import ConfigError from vyos import frr @@ -227,7 +226,11 @@ def apply(vrf): # Remove nftables conntrack zone map item nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}' - cmd(f'nft {nft_del_element}') + # Check if deleting is possible first to avoid raising errors + _, err = popen(f'nft --check {nft_del_element}') + if not err: + # Remove map element + cmd(f'nft {nft_del_element}') # Delete the VRF Kernel interface call(f'ip link delete dev {tmp}') @@ -307,7 +310,7 @@ def apply(vrf): if vrf['conntrack']: for chain, rule in nftables_rules.items(): cmd(f'nft add rule inet vrf_zones {chain} {rule}') - + if 'name' not in vrf or not vrf['conntrack']: for chain, rule in nftables_rules.items(): cmd(f'nft flush chain inet vrf_zones {chain}') diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py index 23b341079..8dab164d7 100644 --- a/src/conf_mode/vrf_vni.py +++ b/src/conf_mode/vrf_vni.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,7 +19,6 @@ from sys import exit from vyos.config import Config from vyos.template import render_to_string -from vyos.utils.dict import dict_search from vyos import ConfigError from vyos import frr from vyos import airbag |