diff options
Diffstat (limited to 'src')
41 files changed, 806 insertions, 257 deletions
diff --git a/src/activation-scripts/20-ethernet_offload.py b/src/activation-scripts/20-ethernet_offload.py index 33b0ea469..ca7213512 100755 --- a/src/activation-scripts/20-ethernet_offload.py +++ b/src/activation-scripts/20-ethernet_offload.py @@ -17,9 +17,12 @@ # CLI. See https://vyos.dev/T3619#102254 for all the details. # T3787: Remove deprecated UDP fragmentation offloading option # T6006: add to activation-scripts: migration-scripts/interfaces/20-to-21 +# T6716: Honor the configured offload settings and don't automatically add +# them to the config if the kernel has them set (unless its a live boot) from vyos.ethtool import Ethtool from vyos.configtree import ConfigTree +from vyos.system.image import is_live_boot def activate(config: ConfigTree): base = ['interfaces', 'ethernet'] @@ -36,7 +39,7 @@ def activate(config: ConfigTree): enabled, fixed = eth.get_generic_receive_offload() if configured and fixed: config.delete(base + [ifname, 'offload', 'gro']) - elif enabled and not fixed: + elif is_live_boot() and enabled and not fixed: config.set(base + [ifname, 'offload', 'gro']) # If GSO is enabled by the Kernel - we reflect this on the CLI. If GSO is @@ -45,7 +48,7 @@ def activate(config: ConfigTree): enabled, fixed = eth.get_generic_segmentation_offload() if configured and fixed: config.delete(base + [ifname, 'offload', 'gso']) - elif enabled and not fixed: + elif is_live_boot() and enabled and not fixed: config.set(base + [ifname, 'offload', 'gso']) # If LRO is enabled by the Kernel - we reflect this on the CLI. If LRO is @@ -54,7 +57,7 @@ def activate(config: ConfigTree): enabled, fixed = eth.get_large_receive_offload() if configured and fixed: config.delete(base + [ifname, 'offload', 'lro']) - elif enabled and not fixed: + elif is_live_boot() and enabled and not fixed: config.set(base + [ifname, 'offload', 'lro']) # If SG is enabled by the Kernel - we reflect this on the CLI. If SG is @@ -63,7 +66,7 @@ def activate(config: ConfigTree): enabled, fixed = eth.get_scatter_gather() if configured and fixed: config.delete(base + [ifname, 'offload', 'sg']) - elif enabled and not fixed: + elif is_live_boot() and enabled and not fixed: config.set(base + [ifname, 'offload', 'sg']) # If TSO is enabled by the Kernel - we reflect this on the CLI. If TSO is @@ -72,7 +75,7 @@ def activate(config: ConfigTree): enabled, fixed = eth.get_tcp_segmentation_offload() if configured and fixed: config.delete(base + [ifname, 'offload', 'tso']) - elif enabled and not fixed: + elif is_live_boot() and enabled and not fixed: config.set(base + [ifname, 'offload', 'tso']) # Remove deprecated UDP fragmentation offloading option diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index ded370a7a..14387cbbf 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -421,6 +421,10 @@ def generate(container): 'driver': 'host-local' } } + + if 'no_name_server' in network_config: + tmp['dns_enabled'] = False + for prefix in network_config['prefix']: net = {'subnet': prefix, 'gateway': inc_ip(prefix, 1)} tmp['subnets'].append(net) diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 5e5d5fba1..bbbfb0385 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -25,6 +25,7 @@ from vyos.configdict import is_source_interface from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_eapol from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config @@ -73,7 +74,7 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'bonding'] - ifname, bond = get_interface_dict(conf, base) + ifname, bond = get_interface_dict(conf, base, with_pki=True) # To make our own life easier transfor the list of member interfaces # into a dictionary - we will use this to add additional information @@ -196,6 +197,7 @@ def verify(bond): verify_dhcpv6(bond) verify_vrf(bond) verify_mirror_redirect(bond) + verify_eapol(bond) # use common function to verify VLAN configuration verify_vlan_config(bond) diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py index 7b2c1ee0b..637db442a 100755 --- a/src/conf_mode/interfaces_bridge.py +++ b/src/conf_mode/interfaces_bridge.py @@ -53,20 +53,22 @@ def get_config(config=None): tmp = node_changed(conf, base + [ifname, 'member', 'interface']) if tmp: if 'member' in bridge: - bridge['member'].update({'interface_remove' : tmp }) + bridge['member'].update({'interface_remove': {t: {} for t in tmp}}) else: - bridge.update({'member' : {'interface_remove' : tmp }}) - for interface in tmp: - # When using VXLAN member interfaces that are configured for Single - # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to - # re-create VLAN to VNI mappings if required, but only if the interface - # is already live on the system - this must not be done on first commit - if interface.startswith('vxlan') and interface_exists(interface): - set_dependents('vxlan', conf, interface) - # When using Wireless member interfaces we need to inform hostapd - # to properly set-up the bridge - elif interface.startswith('wlan') and interface_exists(interface): - set_dependents('wlan', conf, interface) + bridge.update({'member': {'interface_remove': {t: {} for t in tmp}}}) + for interface in tmp: + # When using VXLAN member interfaces that are configured for Single + # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to + # re-create VLAN to VNI mappings if required, but only if the interface + # is already live on the system - this must not be done on first commit + if interface.startswith('vxlan') and interface_exists(interface): + set_dependents('vxlan', conf, interface) + _, vxlan = get_interface_dict(conf, ['interfaces', 'vxlan'], ifname=interface) + bridge['member']['interface_remove'].update({interface: vxlan}) + # When using Wireless member interfaces we need to inform hostapd + # to properly set-up the bridge + elif interface.startswith('wlan') and interface_exists(interface): + set_dependents('wlan', conf, interface) if dict_search('member.interface', bridge) is not None: for interface in list(bridge['member']['interface']): @@ -118,6 +120,16 @@ def get_config(config=None): return bridge def verify(bridge): + # to delete interface or remove a member interface VXLAN first need to check if + # VXLAN does not require to be a member of a bridge interface + if dict_search('member.interface_remove', bridge): + for iface, iface_config in bridge['member']['interface_remove'].items(): + if iface.startswith('vxlan') and dict_search('parameters.neighbor_suppress', iface_config) != None: + raise ConfigError( + f'To detach interface {iface} from bridge you must first ' + f'disable "neighbor-suppress" parameter in the VXLAN interface {iface}' + ) + if 'deleted' in bridge: return None @@ -192,7 +204,7 @@ def apply(bridge): try: call_dependents() except ConfigError: - raise ConfigError('Error updating member interface configuration after changing bridge!') + raise ConfigError(f'Error updating member interface {interface} configuration after changing bridge!') return None diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index afc48ead8..34ce7bc47 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -31,32 +31,20 @@ 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.configverify import verify_eapol from vyos.ethtool import Ethtool from vyos.ifconfig import EthernetIf from vyos.ifconfig import BondIf -from vyos.pki import find_chain -from vyos.pki import encode_certificate -from vyos.pki import load_certificate -from vyos.pki import wrap_private_key -from vyos.template import render from vyos.template import render_to_string -from vyos.utils.process import call from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.utils.dict import dict_set from vyos.utils.dict import dict_delete -from vyos.utils.file import write_file from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() -# XXX: wpa_supplicant works on the source interface -cfg_dir = '/run/wpa_supplicant' -wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' - def update_bond_options(conf: Config, eth_conf: dict) -> list: """ Return list of blocked options if interface is a bond member @@ -277,23 +265,6 @@ 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: return None @@ -346,51 +317,10 @@ def verify_ethernet(ethernet): verify_vlan_config(ethernet) return None - def generate(ethernet): - # render real configuration file once - wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet) - if 'deleted' in ethernet: - # delete configuration on interface removal - if os.path.isfile(wpa_supplicant_conf): - os.unlink(wpa_supplicant_conf) return None - if 'eapol' in ethernet: - ifname = ethernet['ifname'] - - render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', ethernet) - - cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem') - cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key') - - cert_name = ethernet['eapol']['certificate'] - pki_cert = ethernet['pki']['certificate'][cert_name] - - loaded_pki_cert = load_certificate(pki_cert['certificate']) - loaded_ca_certs = {load_certificate(c['certificate']) - for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {} - - cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) - - write_file(cert_file_path, - '\n'.join(encode_certificate(c) for c in cert_full_chain)) - write_file(cert_key_path, wrap_private_key(pki_cert['private']['key'])) - - if 'ca_certificate' in ethernet['eapol']: - ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem') - ca_chains = [] - - for ca_cert_name in ethernet['eapol']['ca_certificate']: - pki_ca_cert = ethernet['pki']['ca'][ca_cert_name] - loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) - ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) - ca_chains.append( - '\n'.join(encode_certificate(c) for c in ca_full_chain)) - - write_file(ca_cert_file_path, '\n'.join(ca_chains)) - ethernet['frr_zebra_config'] = '' if 'deleted' not in ethernet: ethernet['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', ethernet) @@ -399,8 +329,6 @@ def generate(ethernet): def apply(ethernet): ifname = ethernet['ifname'] - # take care about EAPoL supplicant daemon - eapol_action='stop' e = EthernetIf(ifname) if 'deleted' in ethernet: @@ -408,10 +336,6 @@ def apply(ethernet): e.remove() else: e.update(ethernet) - if 'eapol' in ethernet: - eapol_action='reload-or-restart' - - call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}') zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index c44320f36..95dfae3a5 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -26,6 +26,7 @@ from vyos.utils.dict import dict_search from vyos.utils.kernel import check_kmod from vyos.utils.network import interface_exists from vyos.utils.process import cmd +from vyos.utils.process import run from vyos.template import is_ipv6 from vyos import ConfigError from vyos import airbag @@ -48,6 +49,14 @@ def get_config(config=None): if not conf.exists(base): nat['deleted'] = '' + return nat + + nat['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + # Remove dynamic firewall groups if present: + if 'dynamic_group' in nat['firewall_group']: + del nat['firewall_group']['dynamic_group'] return nat @@ -99,22 +108,33 @@ def verify(nat): if not interface_exists(interface_name): Warning(f'Interface "{interface_name}" for destination NAT66 rule "{rule}" does not exist!') + if 'destination' in config and 'group' in config['destination']: + if len({'address_group', 'network_group', 'domain_group'} & set(config['destination']['group'])) > 1: + raise ConfigError('Only one address-group, network-group or domain-group can be specified') + return None def generate(nat): if not os.path.exists(nftables_nat66_config): nat['first_install'] = True - render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat, permission=0o755) + render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat) + + # dry-run newly generated configuration + tmp = run(f'nft --check --file {nftables_nat66_config}') + if tmp > 0: + raise ConfigError('Configuration file errors encountered!') + return None def apply(nat): - if not nat: - return None - check_kmod(k_mod) cmd(f'nft --file {nftables_nat66_config}') + + if not nat or 'deleted' in nat: + os.unlink(nftables_nat66_config) + call_dependents() return None diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index 4df893ebf..a5963e72c 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -167,10 +167,10 @@ def verify(policy): continue for rule, rule_config in route_map_config['rule'].items(): - # Action 'deny' cannot be used with "continue" - # FRR does not validate it T4827 - if rule_config['action'] == 'deny' and 'continue' in rule_config: - raise ConfigError(f'rule {rule} "continue" cannot be used with action deny!') + # Action 'deny' cannot be used with "continue" or "on-match" + # FRR does not validate it T4827, T6676 + if rule_config['action'] == 'deny' and ('continue' in rule_config or 'on_match' in rule_config): + raise ConfigError(f'rule {rule} "continue" or "on-match" cannot be used with action deny!') # Specified community-list must exist tmp = dict_search('match.community.community_list', diff --git a/src/conf_mode/protocols_openfabric.py b/src/conf_mode/protocols_openfabric.py new file mode 100644 index 000000000..8e8c50c06 --- /dev/null +++ b/src/conf_mode/protocols_openfabric.py @@ -0,0 +1,145 @@ +#!/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/>. + +from sys import exit + +from vyos.base import Warning +from vyos.config import Config +from vyos.configdict import node_changed +from vyos.configverify import verify_interface_exists +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr +from vyos import airbag + +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base_path = ['protocols', 'openfabric'] + + openfabric = conf.get_config_dict(base_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + # Remove per domain MPLS configuration - get a list of all changed Openfabric domains + # (removed and added) so that they will be properly rendered for the FRR config. + openfabric['domains_all'] = list(conf.list_nodes(' '.join(base_path) + f' domain') + + node_changed(conf, base_path + ['domain'])) + + # Get a list of all interfaces + openfabric['interfaces_all'] = [] + for domain in openfabric['domains_all']: + interfaces_modified = list(node_changed(conf, base_path + ['domain', domain, 'interface']) + + conf.list_nodes(' '.join(base_path) + f' domain {domain} interface')) + openfabric['interfaces_all'].extend(interfaces_modified) + + if not conf.exists(base_path): + openfabric.update({'deleted': ''}) + + return openfabric + +def verify(openfabric): + # bail out early - looks like removal from running config + if not openfabric or 'deleted' in openfabric: + return None + + if 'net' not in openfabric: + raise ConfigError('Network entity is mandatory!') + + # last byte in OpenFabric area address must be 0 + tmp = openfabric['net'].split('.') + if int(tmp[-1]) != 0: + raise ConfigError('Last byte of OpenFabric network entity title must always be 0!') + + if 'domain' not in openfabric: + raise ConfigError('OpenFabric domain name is mandatory!') + + interfaces_used = [] + + for domain, domain_config in openfabric['domain'].items(): + # If interface not set + if 'interface' not in domain_config: + raise ConfigError(f'Interface used for routing updates in OpenFabric "{domain}" is mandatory!') + + for iface, iface_config in domain_config['interface'].items(): + verify_interface_exists(openfabric, iface) + + # interface can be activated only on one OpenFabric instance + if iface in interfaces_used: + raise ConfigError(f'Interface {iface} is already used in different OpenFabric instance!') + + if 'address_family' not in iface_config or len(iface_config['address_family']) < 1: + raise ConfigError(f'Need to specify address family for the interface "{iface}"!') + + # If md5 and plaintext-password set at the same time + if 'password' in iface_config: + if {'md5', 'plaintext_password'} <= set(iface_config['password']): + raise ConfigError(f'Can use either md5 or plaintext-password for password for the interface!') + + if iface == 'lo' and 'passive' not in iface_config: + Warning('For loopback interface passive mode is implied!') + + interfaces_used.append(iface) + + # If md5 and plaintext-password set at the same time + password = 'domain_password' + if password in domain_config: + if {'md5', 'plaintext_password'} <= set(domain_config[password]): + raise ConfigError(f'Can use either md5 or plaintext-password for domain-password!') + + return None + +def generate(openfabric): + if not openfabric or 'deleted' in openfabric: + return None + + openfabric['frr_fabricd_config'] = render_to_string('frr/fabricd.frr.j2', openfabric) + return None + +def apply(openfabric): + openfabric_daemon = 'fabricd' + + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + frr_cfg.load_configuration(openfabric_daemon) + for domain in openfabric['domains_all']: + frr_cfg.modify_section(f'^router openfabric {domain}', stop_pattern='^exit', remove_stop_mark=True) + + for interface in openfabric['interfaces_all']: + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) + + if 'frr_fabricd_config' in openfabric: + frr_cfg.add_before(frr.default_add_before, openfabric['frr_fabricd_config']) + + frr_cfg.commit_configuration(openfabric_daemon) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py index 70686534f..e3bdbc9f8 100755 --- a/src/conf_mode/service_dns_forwarding.py +++ b/src/conf_mode/service_dns_forwarding.py @@ -224,6 +224,18 @@ def get_config(config=None): dns['authoritative_zones'].append(zone) + if 'zone_cache' in dns: + # convert refresh interval to sec: + for _, zone_conf in dns['zone_cache'].items(): + if 'options' in zone_conf \ + and 'refresh' in zone_conf['options']: + + if 'on_reload' in zone_conf['options']['refresh']: + interval = 0 + else: + interval = zone_conf['options']['refresh']['interval'] + zone_conf['options']['refresh']['interval'] = interval + return dns def verify(dns): @@ -259,8 +271,16 @@ def verify(dns): if not 'system_name_server' in dns: print('Warning: No "system name-server" configured') + if 'zone_cache' in dns: + for name, conf in dns['zone_cache'].items(): + if ('source' not in conf) \ + or ('url' in conf['source'] and 'axfr' in conf['source']): + raise ConfigError(f'Invalid configuration for zone "{name}": ' + f'Please select one source type "url" or "axfr".') + return None + def generate(dns): # bail out early - looks like removal from running config if not dns: diff --git a/src/conf_mode/service_ntp.py b/src/conf_mode/service_ntp.py index 83880fd72..32563aa0e 100755 --- a/src/conf_mode/service_ntp.py +++ b/src/conf_mode/service_ntp.py @@ -17,6 +17,7 @@ import os from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_interface_exists @@ -42,13 +43,21 @@ def get_config(config=None): if not conf.exists(base): return None - ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, with_defaults=True) + ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) ntp['config_file'] = config_file ntp['user'] = user_group tmp = is_node_changed(conf, base + ['vrf']) if tmp: ntp.update({'restart_required': {}}) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(**ntp.kwargs, recursive=True) + # Only defined PTP default port, if PTP feature is in use + if 'ptp' not in ntp: + del default_values['ptp'] + + ntp = config_dict_merge(default_values, ntp) return ntp def verify(ntp): @@ -87,6 +96,15 @@ def verify(ntp): if ipv6_addresses > 1: raise ConfigError(f'NTP Only admits one ipv6 value for listen-address parameter ') + if 'server' in ntp: + for host, server in ntp['server'].items(): + if 'ptp' in server: + if 'ptp' not in ntp: + raise ConfigError('PTP must be enabled for the NTP service '\ + f'before it can be used for server "{host}"') + else: + break + return None def generate(ntp): diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index 52d0b7cda..a84572f83 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -19,11 +19,13 @@ import os 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.cpu import get_cpus from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.kernel import check_kmod @@ -35,6 +37,7 @@ from vyos.configdep import set_dependents from vyos.configdep import call_dependents from vyos import ConfigError from vyos import airbag + airbag.enable() curlrc_config = r'/etc/curlrc' @@ -42,10 +45,8 @@ ssh_config = r'/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf' systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' usb_autosuspend = r'/etc/udev/rules.d/40-usb-autosuspend.rules' kernel_dynamic_debug = r'/sys/kernel/debug/dynamic_debug/control' -time_format_to_locale = { - '12-hour': 'en_US.UTF-8', - '24-hour': 'en_GB.UTF-8' -} +time_format_to_locale = {'12-hour': 'en_US.UTF-8', '24-hour': 'en_GB.UTF-8'} + def get_config(config=None): if config: @@ -53,9 +54,9 @@ def get_config(config=None): else: conf = Config() base = ['system', 'option'] - options = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) + options = conf.get_config_dict( + base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=True + ) if 'performance' in options: # Update IPv4/IPv6 and sysctl options after tuned applied it's settings @@ -64,6 +65,7 @@ def get_config(config=None): return options + def verify(options): if 'http_client' in options: config = options['http_client'] @@ -71,7 +73,9 @@ def verify(options): verify_interface_exists(options, config['source_interface']) if {'source_address', 'source_interface'} <= set(config): - raise ConfigError('Can not define both HTTP source-interface and source-address') + raise ConfigError( + 'Can not define both HTTP source-interface and source-address' + ) if 'source_address' in config: if not is_addr_assigned(config['source_address']): @@ -92,10 +96,20 @@ def verify(options): address = config['source_address'] interface = config['source_interface'] if not is_intf_addr_assigned(interface, address): - raise ConfigError(f'Address "{address}" not assigned on interface "{interface}"!') + raise ConfigError( + f'Address "{address}" not assigned on interface "{interface}"!' + ) + + if 'kernel' in options: + cpu_vendor = get_cpus()[0]['vendor_id'] + if 'amd_pstate_driver' in options['kernel'] and cpu_vendor != 'AuthenticAMD': + raise ConfigError( + f'AMD pstate driver cannot be used with "{cpu_vendor}" CPU!' + ) return None + def generate(options): render(curlrc_config, 'system/curlrc.j2', options) render(ssh_config, 'system/ssh_config.j2', options) @@ -107,10 +121,16 @@ def generate(options): cmdline_options.append('mitigations=off') if 'disable_power_saving' in options['kernel']: cmdline_options.append('intel_idle.max_cstate=0 processor.max_cstate=1') + if 'amd_pstate_driver' in options['kernel']: + mode = options['kernel']['amd_pstate_driver'] + cmdline_options.append( + f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}' + ) grub_util.update_kernel_cmdline_options(' '.join(cmdline_options)) return None + def apply(options): # System bootup beep beep_service = 'vyos-beep.service' @@ -149,7 +169,7 @@ def apply(options): if 'performance' in options: cmd('systemctl restart tuned.service') # wait until daemon has started before sending configuration - while (not is_systemd_service_running('tuned.service')): + while not is_systemd_service_running('tuned.service'): sleep(0.250) cmd('tuned-adm profile network-{performance}'.format(**options)) else: @@ -164,9 +184,9 @@ def apply(options): # Enable/diable root-partition-auto-resize SystemD service if 'root_partition_auto_resize' in options: - cmd('systemctl enable root-partition-auto-resize.service') + cmd('systemctl enable root-partition-auto-resize.service') else: - cmd('systemctl disable root-partition-auto-resize.service') + cmd('systemctl disable root-partition-auto-resize.service') # Time format 12|24-hour if 'time_format' in options: @@ -186,6 +206,7 @@ def apply(options): else: write_file(kernel_dynamic_debug, f'module {module} -p') + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/system_syslog.py b/src/conf_mode/system_syslog.py index 07fbb0734..eb2f02eb3 100755 --- a/src/conf_mode/system_syslog.py +++ b/src/conf_mode/system_syslog.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-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 @@ -18,6 +18,7 @@ import os from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf @@ -52,12 +53,29 @@ def get_config(config=None): if syslog.from_defaults(['global']): del syslog['global'] + if ( + 'global' in syslog + and 'preserve_fqdn' in syslog['global'] + and conf.exists(['system', 'host-name']) + and conf.exists(['system', 'domain-name']) + ): + hostname = conf.return_value(['system', 'host-name']) + domain = conf.return_value(['system', 'domain-name']) + fqdn = f'{hostname}.{domain}' + syslog['global']['local_host_name'] = fqdn + return syslog def verify(syslog): if not syslog: return None + if 'host' in syslog: + for host, host_options in syslog['host'].items(): + if 'protocol' in host_options and host_options['protocol'] == 'udp': + if 'format' in host_options and 'octet_counted' in host_options['format']: + Warning(f'Syslog UDP transport for "{host}" should not use octet-counted format!') + verify_vrf(syslog) def generate(syslog): diff --git a/src/etc/sudoers.d/vyos b/src/etc/sudoers.d/vyos index 63a944f41..67d7babc4 100644 --- a/src/etc/sudoers.d/vyos +++ b/src/etc/sudoers.d/vyos @@ -57,4 +57,7 @@ Cmnd_Alias KEA_IP6_ROUTES = /sbin/ip -6 route replace *,\ # Allow members of group sudo to execute any command %sudo ALL=NOPASSWD: ALL +# Allow any user to query Machine Owner Key status +%sudo ALL=NOPASSWD: /usr/bin/mokutil + _kea ALL=NOPASSWD: KEA_IP6_ROUTES diff --git a/src/op_mode/monitor_bandwidth_test.sh b/src/op_mode/execute_bandwidth_test.sh index a6ad0b42c..a6ad0b42c 100755 --- a/src/op_mode/monitor_bandwidth_test.sh +++ b/src/op_mode/execute_bandwidth_test.sh diff --git a/src/op_mode/execute_port-scan.py b/src/op_mode/execute_port-scan.py new file mode 100644 index 000000000..bf17d0379 --- /dev/null +++ b/src/op_mode/execute_port-scan.py @@ -0,0 +1,155 @@ +#! /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 sys + +from vyos.utils.process import call + + +options = { + 'port': { + 'cmd': '{command} -p {value}', + 'type': '<1-65535> <list>', + 'help': 'Scan specified ports.' + }, + 'tcp': { + 'cmd': '{command} -sT', + 'type': 'noarg', + 'help': 'Use TCP scan.' + }, + 'udp': { + 'cmd': '{command} -sU', + 'type': 'noarg', + 'help': 'Use UDP scan.' + }, + 'skip-ping': { + 'cmd': '{command} -Pn', + 'type': 'noarg', + 'help': 'Skip the Nmap discovery stage altogether.' + }, + 'ipv6': { + 'cmd': '{command} -6', + 'type': 'noarg', + 'help': 'Enable IPv6 scanning.' + }, +} + +nmap = 'sudo /usr/bin/nmap' + + +class List(list): + def first(self): + return self.pop(0) if self else '' + + def last(self): + return self.pop() if self else '' + + def prepend(self, value): + self.insert(0, value) + + +def completion_failure(option: str) -> None: + """ + Shows failure message after TAB when option is wrong + :param option: failure option + :type str: + """ + sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option)) + sys.stdout.write('<nocomps>') + sys.exit(1) + + +def expansion_failure(option, completions): + reason = 'Ambiguous' if completions else 'Invalid' + sys.stderr.write( + '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv), + option)) + if completions: + sys.stderr.write(' Possible completions:\n ') + sys.stderr.write('\n '.join(completions)) + sys.stderr.write('\n') + sys.stdout.write('<nocomps>') + sys.exit(1) + + +def complete(prefix): + return [o for o in options if o.startswith(prefix)] + + +def convert(command, args): + while args: + shortname = args.first() + longnames = complete(shortname) + if len(longnames) != 1: + expansion_failure(shortname, longnames) + longname = longnames[0] + if options[longname]['type'] == 'noarg': + command = options[longname]['cmd'].format( + command=command, value='') + elif not args: + sys.exit(f'port-scan: missing argument for {longname} option') + else: + command = options[longname]['cmd'].format( + command=command, value=args.first()) + return command + + +if __name__ == '__main__': + args = List(sys.argv[1:]) + host = args.first() + + if host == '--get-options-nested': + args.first() # pop execute + args.first() # pop port-scan + args.first() # pop host + args.first() # pop <host> + usedoptionslist = [] + while args: + option = args.first() # pop option + matched = complete(option) # get option parameters + usedoptionslist.append(option) # list of used options + # Select options + if not args: + # remove from Possible completions used options + for o in usedoptionslist: + if o in matched: + matched.remove(o) + if not matched: + sys.stdout.write('<nocomps>') + else: + sys.stdout.write(' '.join(matched)) + sys.exit(0) + + if len(matched) > 1: + sys.stdout.write(' '.join(matched)) + sys.exit(0) + # If option doesn't have value + if matched: + if options[matched[0]]['type'] == 'noarg': + continue + else: + # Unexpected option + completion_failure(option) + + value = args.first() # pop option's value + if not args: + matched = complete(option) + helplines = options[matched[0]]['type'] + sys.stdout.write(helplines) + sys.exit(0) + + command = convert(nmap, args) + call(f'{command} -T4 {host}') diff --git a/src/op_mode/interfaces_wireguard.py b/src/op_mode/interfaces_wireguard.py new file mode 100644 index 000000000..627af0579 --- /dev/null +++ b/src/op_mode/interfaces_wireguard.py @@ -0,0 +1,53 @@ +#!/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 sys +import vyos.opmode + +from vyos.ifconfig import WireGuardIf +from vyos.configquery import ConfigTreeQuery + + +def _verify(func): + """Decorator checks if WireGuard interface config exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + interface = kwargs.get('intf_name') + if not config.exists(['interfaces', 'wireguard', interface]): + unconf_message = f'WireGuard interface {interface} is not configured' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + + return _wrapper + + +@_verify +def show_summary(raw: bool, intf_name: str): + intf = WireGuardIf(intf_name, create=False, debug=False) + return intf.operational.show_interface() + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py index 813d3a2b7..a83c8b9d8 100755 --- a/src/op_mode/restart.py +++ b/src/op_mode/restart.py @@ -25,11 +25,11 @@ from vyos.utils.commit import commit_in_progress config = ConfigTreeQuery() service_map = { - 'dhcp' : { + 'dhcp': { 'systemd_service': 'kea-dhcp4-server', 'path': ['service', 'dhcp-server'], }, - 'dhcpv6' : { + 'dhcpv6': { 'systemd_service': 'kea-dhcp6-server', 'path': ['service', 'dhcpv6-server'], }, @@ -61,24 +61,40 @@ service_map = { 'systemd_service': 'radvd', 'path': ['service', 'router-advert'], }, - 'snmp' : { + 'snmp': { 'systemd_service': 'snmpd', }, - 'ssh' : { + 'ssh': { 'systemd_service': 'ssh', }, - 'suricata' : { + 'suricata': { 'systemd_service': 'suricata', }, - 'vrrp' : { + 'vrrp': { 'systemd_service': 'keepalived', 'path': ['high-availability', 'vrrp'], }, - 'webproxy' : { + 'webproxy': { 'systemd_service': 'squid', }, } -services = typing.Literal['dhcp', 'dhcpv6', 'dns_dynamic', 'dns_forwarding', 'igmp_proxy', 'ipsec', 'mdns_repeater', 'reverse_proxy', 'router_advert', 'snmp', 'ssh', 'suricata' 'vrrp', 'webproxy'] +services = typing.Literal[ + 'dhcp', + 'dhcpv6', + 'dns_dynamic', + 'dns_forwarding', + 'igmp_proxy', + 'ipsec', + 'mdns_repeater', + 'reverse_proxy', + 'router_advert', + 'snmp', + 'ssh', + 'suricata', + 'vrrp', + 'webproxy', +] + def _verify(func): """Decorator checks if DHCP(v6) config exists""" @@ -102,13 +118,18 @@ def _verify(func): # Check if config does not exist if not config.exists(path): - raise vyos.opmode.UnconfiguredSubsystem(f'Service {human_name} is not configured!') + raise vyos.opmode.UnconfiguredSubsystem( + f'Service {human_name} is not configured!' + ) if config.exists(path + ['disable']): - raise vyos.opmode.UnconfiguredSubsystem(f'Service {human_name} is disabled!') + raise vyos.opmode.UnconfiguredSubsystem( + f'Service {human_name} is disabled!' + ) return func(*args, **kwargs) return _wrapper + @_verify def restart_service(raw: bool, name: services, vrf: typing.Optional[str]): systemd_service = service_map[name]['systemd_service'] @@ -117,6 +138,7 @@ def restart_service(raw: bool, name: services, vrf: typing.Optional[str]): else: call(f'systemctl restart "{systemd_service}.service"') + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 8841b0eca..83146f5ec 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -139,7 +139,7 @@ def _reload_config(daemon): # define program arguments cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons') cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons') -cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'eigrpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pimd', 'pim6d', 'ldpd', 'babeld', 'bfdd'], required=False, nargs='*', help='select single or multiple daemons') +cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'eigrpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pimd', 'pim6d', 'ldpd', 'babeld', 'bfdd', 'fabricd'], required=False, nargs='*', help='select single or multiple daemons') # parse arguments cmd_args = cmd_args_parser.parse_args() diff --git a/src/op_mode/secure_boot.py b/src/op_mode/secure_boot.py new file mode 100755 index 000000000..5f6390a15 --- /dev/null +++ b/src/op_mode/secure_boot.py @@ -0,0 +1,50 @@ +#!/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 sys +import vyos.opmode + +from vyos.utils.boot import is_uefi_system +from vyos.utils.system import get_secure_boot_state + +def _get_raw_data(name=None): + sb_data = { + 'state' : get_secure_boot_state(), + 'uefi' : is_uefi_system() + } + return sb_data + +def _get_formatted_output(raw_data): + if not raw_data['uefi']: + print('System run in legacy BIOS mode!') + state = 'enabled' if raw_data['state'] else 'disabled' + return f'SecureBoot {state}' + +def show(raw: bool): + sb_data = _get_raw_data() + if raw: + return sb_data + else: + return _get_formatted_output(sb_data) + +if __name__ == "__main__": + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/version.py b/src/op_mode/version.py index 09d69ad1d..71a40dd50 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -25,6 +25,9 @@ import vyos.opmode import vyos.version import vyos.limericks +from vyos.utils.boot import is_uefi_system +from vyos.utils.system import get_secure_boot_state + from jinja2 import Template version_output_tmpl = """ @@ -43,6 +46,7 @@ Build comment: {{build_comment}} Architecture: {{system_arch}} Boot via: {{boot_via}} System type: {{system_type}} +Secure Boot: {{secure_boot}} Hardware vendor: {{hardware_vendor}} Hardware model: {{hardware_model}} @@ -57,6 +61,11 @@ Copyright: VyOS maintainers and contributors def _get_raw_data(funny=False): version_data = vyos.version.get_full_version_data() + version_data["secure_boot"] = "n/a (BIOS)" + if is_uefi_system(): + version_data["secure_boot"] = "disabled" + if get_secure_boot_state(): + version_data["secure_boot"] = "enabled" if funny: version_data["limerick"] = vyos.limericks.get_random() diff --git a/src/op_mode/vpn_ike_sa.py b/src/op_mode/vpn_ike_sa.py index 5e2aaae6b..9385bcd0c 100755 --- a/src/op_mode/vpn_ike_sa.py +++ b/src/op_mode/vpn_ike_sa.py @@ -38,6 +38,8 @@ def ike_sa(peer, nat): peers = [] for conn in sas: for name, sa in conn.items(): + if peer and s(sa['remote-host']) != peer: + continue if name.startswith('peer_') and name in peers: continue if nat and 'nat-local' not in sa: diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 3674d9627..cb23642dc 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -14,6 +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/>. +# pylint: disable=redefined-outer-name + import os import sys import grp @@ -22,9 +24,12 @@ import json import typing import logging import signal +import traceback import importlib.util +import io +from contextlib import redirect_stdout + import zmq -from contextlib import contextmanager from vyos.defaults import directories from vyos.utils.boot import boot_configuration_complete @@ -49,7 +54,8 @@ if debug: else: logger.setLevel(logging.INFO) -SOCKET_PATH = "ipc:///run/vyos-configd.sock" +SOCKET_PATH = 'ipc:///run/vyos-configd.sock' +MAX_MSG_SIZE = 65535 # Response error codes R_SUCCESS = 1 @@ -64,9 +70,6 @@ configd_env_unset_file = os.path.join(directories['data'], 'vyos-configd-env-uns # sourced on entering config session configd_env_file = '/etc/default/vyos-configd-env' -session_out = None -session_mode = None - def key_name_from_file_name(f): return os.path.splitext(f)[0] @@ -76,17 +79,19 @@ def module_name_from_key(k): def path_from_file_name(f): return os.path.join(vyos_conf_scripts_dir, f) + # opt-in to be run by daemon with open(configd_include_file) as f: try: include = json.load(f) except OSError as e: - logger.critical(f"configd include file error: {e}") + logger.critical(f'configd include file error: {e}') sys.exit(1) except json.JSONDecodeError as e: - logger.critical(f"JSON load error: {e}") + logger.critical(f'JSON load error: {e}') sys.exit(1) + # import conf_mode scripts (_, _, filenames) = next(iter(os.walk(vyos_conf_scripts_dir))) filenames.sort() @@ -110,31 +115,17 @@ conf_mode_scripts = dict(zip(imports, modules)) exclude_set = {key_name_from_file_name(f) for f in filenames if f not in include} include_set = {key_name_from_file_name(f) for f in filenames if f in include} -@contextmanager -def stdout_redirected(filename, mode): - saved_stdout_fd = None - destination_file = None - try: - sys.stdout.flush() - saved_stdout_fd = os.dup(sys.stdout.fileno()) - destination_file = open(filename, mode) - os.dup2(destination_file.fileno(), sys.stdout.fileno()) - yield - finally: - if saved_stdout_fd is not None: - os.dup2(saved_stdout_fd, sys.stdout.fileno()) - os.close(saved_stdout_fd) - if destination_file is not None: - destination_file.close() - -def explicit_print(path, mode, msg): - try: - with open(path, mode) as f: - f.write(f"\n{msg}\n\n") - except OSError: - logger.critical("error explicit_print") -def run_script(script_name, config, args) -> int: +def write_stdout_log(file_name, msg): + if boot_configuration_complete(): + return + with open(file_name, 'a') as f: + f.write(msg) + + +def run_script(script_name, config, args) -> tuple[int, str]: + # pylint: disable=broad-exception-caught + script = conf_mode_scripts[script_name] script.argv = args config.set_level([]) @@ -145,64 +136,54 @@ def run_script(script_name, config, args) -> int: script.apply(c) except ConfigError as e: logger.error(e) - explicit_print(session_out, session_mode, str(e)) - return R_ERROR_COMMIT - except Exception as e: - logger.critical(e) - return R_ERROR_DAEMON + return R_ERROR_COMMIT, str(e) + except Exception: + tb = traceback.format_exc() + logger.error(tb) + return R_ERROR_COMMIT, tb + + return R_SUCCESS, '' - return R_SUCCESS def initialization(socket): - global session_out - global session_mode + # pylint: disable=broad-exception-caught,too-many-locals + # Reset config strings: active_string = '' session_string = '' # check first for resent init msg, in case of client timeout while True: - msg = socket.recv().decode("utf-8", "ignore") + msg = socket.recv().decode('utf-8', 'ignore') try: message = json.loads(msg) - if message["type"] == "init": - resp = "init" + if message['type'] == 'init': + resp = 'init' socket.send(resp.encode()) - except: + except Exception: break # zmq synchronous for ipc from single client: active_string = msg - resp = "active" + resp = 'active' socket.send(resp.encode()) - session_string = socket.recv().decode("utf-8", "ignore") - resp = "session" + session_string = socket.recv().decode('utf-8', 'ignore') + resp = 'session' socket.send(resp.encode()) - pid_string = socket.recv().decode("utf-8", "ignore") - resp = "pid" + pid_string = socket.recv().decode('utf-8', 'ignore') + resp = 'pid' socket.send(resp.encode()) - sudo_user_string = socket.recv().decode("utf-8", "ignore") - resp = "sudo_user" + sudo_user_string = socket.recv().decode('utf-8', 'ignore') + resp = 'sudo_user' socket.send(resp.encode()) - temp_config_dir_string = socket.recv().decode("utf-8", "ignore") - resp = "temp_config_dir" + temp_config_dir_string = socket.recv().decode('utf-8', 'ignore') + resp = 'temp_config_dir' socket.send(resp.encode()) - changes_only_dir_string = socket.recv().decode("utf-8", "ignore") - resp = "changes_only_dir" + changes_only_dir_string = socket.recv().decode('utf-8', 'ignore') + resp = 'changes_only_dir' socket.send(resp.encode()) - logger.debug(f"config session pid is {pid_string}") - logger.debug(f"config session sudo_user is {sudo_user_string}") - - try: - session_out = os.readlink(f"/proc/{pid_string}/fd/1") - session_mode = 'w' - except FileNotFoundError: - session_out = None - - # if not a 'live' session, for example on boot, write to file - if not session_out or not boot_configuration_complete(): - session_out = script_stdout_log - session_mode = 'a' + logger.debug(f'config session pid is {pid_string}') + logger.debug(f'config session sudo_user is {sudo_user_string}') os.environ['SUDO_USER'] = sudo_user_string if temp_config_dir_string: @@ -229,10 +210,12 @@ def initialization(socket): return config -def process_node_data(config, data, last: bool = False) -> int: + +def process_node_data(config, data, _last: bool = False) -> tuple[int, str]: if not config: - logger.critical(f"Empty config") - return R_ERROR_DAEMON + out = 'Empty config' + logger.critical(out) + return R_ERROR_DAEMON, out script_name = None os.environ['VYOS_TAGNODE_VALUE'] = '' @@ -246,8 +229,9 @@ def process_node_data(config, data, last: bool = False) -> int: if res.group(2): script_name = res.group(2) if not script_name: - logger.critical(f"Missing script_name") - return R_ERROR_DAEMON + out = 'Missing script_name' + logger.critical(out) + return R_ERROR_DAEMON, out if res.group(3): args = res.group(3).split() args.insert(0, f'{script_name}.py') @@ -259,26 +243,55 @@ def process_node_data(config, data, last: bool = False) -> int: scripts_called.append(script_record) if script_name not in include_set: - return R_PASS + return R_PASS, '' + + with redirect_stdout(io.StringIO()) as o: + result, err_out = run_script(script_name, config, args) + amb_out = o.getvalue() + o.close() + + out = amb_out + err_out + + return result, out + - with stdout_redirected(session_out, session_mode): - result = run_script(script_name, config, args) +def send_result(sock, err, msg): + msg_size = min(MAX_MSG_SIZE, len(msg)) if msg else 0 + + err_rep = err.to_bytes(1, byteorder=sys.byteorder) + logger.debug(f'Sending reply: {err}') + sock.send(err_rep) + + # size req from vyshim client + size_req = sock.recv().decode() + logger.debug(f'Received request: {size_req}') + msg_size_rep = hex(msg_size).encode() + sock.send(msg_size_rep) + logger.debug(f'Sending reply: {msg_size}') + + if msg_size > 0: + # send req is sent from vyshim client only if msg_size > 0 + send_req = sock.recv().decode() + logger.debug(f'Received request: {send_req}') + sock.send(msg.encode()) + logger.debug('Sending reply with output') + + write_stdout_log(script_stdout_log, msg) - return result def remove_if_file(f: str): try: os.remove(f) except FileNotFoundError: pass - except OSError: - raise + def shutdown(): remove_if_file(configd_env_file) os.symlink(configd_env_unset_file, configd_env_file) sys.exit(0) + if __name__ == '__main__': context = zmq.Context() socket = context.socket(zmq.REP) @@ -294,6 +307,7 @@ if __name__ == '__main__': os.environ['VYOS_CONFIGD'] = 't' def sig_handler(signum, frame): + # pylint: disable=unused-argument shutdown() signal.signal(signal.SIGTERM, sig_handler) @@ -308,20 +322,19 @@ if __name__ == '__main__': while True: # Wait for next request from client msg = socket.recv().decode() - logger.debug(f"Received message: {msg}") + logger.debug(f'Received message: {msg}') message = json.loads(msg) - if message["type"] == "init": - resp = "init" + if message['type'] == 'init': + resp = 'init' socket.send(resp.encode()) config = initialization(socket) - elif message["type"] == "node": - res = process_node_data(config, message["data"], message["last"]) - response = res.to_bytes(1, byteorder=sys.byteorder) - logger.debug(f"Sending response {res}") - socket.send(response) - if message["last"] and config: + elif message['type'] == 'node': + res, out = process_node_data(config, message['data'], message['last']) + send_result(socket, res, out) + + if message['last'] and config: scripts_called = getattr(config, 'scripts_called', []) logger.debug(f'scripts_called: {scripts_called}') else: - logger.critical(f"Unexpected message: {message}") + logger.critical(f'Unexpected message: {message}') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 97633577d..91100410c 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -577,7 +577,9 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel, background_tasks.add_task(call_commit, session) msg = self_ref_msg else: - session.commit() + # capture non-fatal warnings + out = session.commit() + msg = out if out else msg logger.info(f"Configuration modified via HTTP API using key '{app.state.vyos_id}'") except ConfigSessionError as e: diff --git a/src/shim/vyshim.c b/src/shim/vyshim.c index a78f62a7b..68e6c4015 100644 --- a/src/shim/vyshim.c +++ b/src/shim/vyshim.c @@ -67,6 +67,8 @@ void timer_handler(int); double get_posix_clock_time(void); +static char * s_recv_string (void *, int); + int main(int argc, char* argv[]) { // string for node data: conf_mode script and tagnode, if applicable @@ -119,31 +121,44 @@ int main(int argc, char* argv[]) zmq_recv(requester, error_code, 1, 0); debug_print("Received node data receipt\n"); - int err = (int)error_code[0]; + char msg_size_str[7]; + zmq_send(requester, "msg_size", 8, 0); + zmq_recv(requester, msg_size_str, 6, 0); + msg_size_str[6] = '\0'; + int msg_size = (int)strtol(msg_size_str, NULL, 16); + debug_print("msg_size: %d\n", msg_size); + + if (msg_size > 0) { + zmq_send(requester, "send", 4, 0); + char *msg = s_recv_string(requester, msg_size); + printf("%s", msg); + free(msg); + } free(string_node_data_msg); - zmq_close(requester); - zmq_ctx_destroy(context); + int err = (int)error_code[0]; + int ret = 0; if (err & PASS) { debug_print("Received PASS\n"); - int ret = pass_through(argv, ex_index); - return ret; + ret = pass_through(argv, ex_index); } if (err & ERROR_DAEMON) { debug_print("Received ERROR_DAEMON\n"); - int ret = pass_through(argv, ex_index); - return ret; + ret = pass_through(argv, ex_index); } if (err & ERROR_COMMIT) { debug_print("Received ERROR_COMMIT\n"); - return -1; + ret = -1; } - return 0; + zmq_close(requester); + zmq_ctx_destroy(context); + + return ret; } int initialization(void* Requester) @@ -342,3 +357,15 @@ double get_posix_clock_time(void) double get_posix_clock_time(void) {return (double)0;} #endif + +// Receive string from socket and convert into C string +static char * s_recv_string (void *socket, int bufsize) { + char * buffer = (char *)malloc(bufsize+1); + int size = zmq_recv(socket, buffer, bufsize, 0); + if (size == -1) + return NULL; + if (size > bufsize) + size = bufsize; + buffer[size] = '\0'; + return buffer; +} diff --git a/src/systemd/podman.service b/src/systemd/podman.service new file mode 100644 index 000000000..20a16304b --- /dev/null +++ b/src/systemd/podman.service @@ -0,0 +1,16 @@ +[Unit] +Description=Podman API Service +Requires=podman.socket +After=podman.socket +Documentation=man:podman-system-service(1) +StartLimitIntervalSec=0 + +[Service] +Delegate=true +Type=exec +KillMode=process +Environment=LOGGING="--log-level=info" +ExecStart=/usr/bin/podman $LOGGING system service + +[Install] +WantedBy=default.target diff --git a/src/systemd/podman.socket b/src/systemd/podman.socket new file mode 100644 index 000000000..397058ee4 --- /dev/null +++ b/src/systemd/podman.socket @@ -0,0 +1,10 @@ +[Unit] +Description=Podman API Socket +Documentation=man:podman-system-service(1) + +[Socket] +ListenStream=%t/podman/podman.sock +SocketMode=0660 + +[Install] +WantedBy=sockets.target diff --git a/src/validators/interface-address b/src/validators/interface-address index 4c203956b..2a2583fc3 100755 --- a/src/validators/interface-address +++ b/src/validators/interface-address @@ -1,3 +1,3 @@ #!/bin/sh -ipaddrcheck --is-ipv4-host $1 || ipaddrcheck --is-ipv6-host $1 +ipaddrcheck --is-any-host "$1" diff --git a/src/validators/ip-address b/src/validators/ip-address index 11d6df09e..351f728a6 100755 --- a/src/validators/ip-address +++ b/src/validators/ip-address @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-any-single $1 +ipaddrcheck --is-any-single "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IP address" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ip-cidr b/src/validators/ip-cidr index 60d2ac295..8a01e7ad9 100755 --- a/src/validators/ip-cidr +++ b/src/validators/ip-cidr @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-any-cidr $1 +ipaddrcheck --is-any-cidr "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IP CIDR" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ip-host b/src/validators/ip-host index 77c578fa2..7c5ad2612 100755 --- a/src/validators/ip-host +++ b/src/validators/ip-host @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-any-host $1 +ipaddrcheck --is-any-host "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IP host" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ip-prefix b/src/validators/ip-prefix index e5a64fea8..25204ace5 100755 --- a/src/validators/ip-prefix +++ b/src/validators/ip-prefix @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-any-net $1 +ipaddrcheck --is-any-net "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IP prefix" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv4 b/src/validators/ipv4 index 8676d5800..11f854cf1 100755 --- a/src/validators/ipv4 +++ b/src/validators/ipv4 @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv4 $1 +ipaddrcheck --is-ipv4 "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not IPv4" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv4-address b/src/validators/ipv4-address index 058db088b..1cfd961ba 100755 --- a/src/validators/ipv4-address +++ b/src/validators/ipv4-address @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv4-single $1 +ipaddrcheck --is-ipv4-single "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IPv4 address" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv4-host b/src/validators/ipv4-host index 74b8c36a7..eb8faaa2a 100755 --- a/src/validators/ipv4-host +++ b/src/validators/ipv4-host @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv4-host $1 +ipaddrcheck --is-ipv4-host "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IPv4 host" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv4-multicast b/src/validators/ipv4-multicast index 3f28c51db..cf871bd59 100755 --- a/src/validators/ipv4-multicast +++ b/src/validators/ipv4-multicast @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv4-multicast $1 && ipaddrcheck --is-ipv4-single $1 +ipaddrcheck --is-ipv4-multicast "$1" && ipaddrcheck --is-ipv4-single "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IPv4 multicast address" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv4-prefix b/src/validators/ipv4-prefix index 7e1e0e8dd..f8d46c69c 100755 --- a/src/validators/ipv4-prefix +++ b/src/validators/ipv4-prefix @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv4-net $1 +ipaddrcheck --is-ipv4-net "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IPv4 prefix" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv6 b/src/validators/ipv6 index 4ae130eb5..57696add7 100755 --- a/src/validators/ipv6 +++ b/src/validators/ipv6 @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv6 $1 +ipaddrcheck --is-ipv6 "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not IPv6" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv6-address b/src/validators/ipv6-address index 1fca77668..460639090 100755 --- a/src/validators/ipv6-address +++ b/src/validators/ipv6-address @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv6-single $1 +ipaddrcheck --is-ipv6-single "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IPv6 address" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv6-host b/src/validators/ipv6-host index 7085809a9..1eb4d8e35 100755 --- a/src/validators/ipv6-host +++ b/src/validators/ipv6-host @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv6-host $1 +ipaddrcheck --is-ipv6-host "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IPv6 host" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv6-multicast b/src/validators/ipv6-multicast index 5aa7d734a..746ff7edf 100755 --- a/src/validators/ipv6-multicast +++ b/src/validators/ipv6-multicast @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv6-multicast $1 && ipaddrcheck --is-ipv6-single $1 +ipaddrcheck --is-ipv6-multicast "$1" && ipaddrcheck --is-ipv6-single "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IPv6 multicast address" exit 1 fi -exit 0
\ No newline at end of file +exit 0 diff --git a/src/validators/ipv6-prefix b/src/validators/ipv6-prefix index 890dda723..1bb9b42fe 100755 --- a/src/validators/ipv6-prefix +++ b/src/validators/ipv6-prefix @@ -1,10 +1,10 @@ #!/bin/sh -ipaddrcheck --is-ipv6-net $1 +ipaddrcheck --is-ipv6-net "$1" if [ $? -gt 0 ]; then echo "Error: $1 is not a valid IPv6 prefix" exit 1 fi -exit 0
\ No newline at end of file +exit 0 |