diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/firewall.py | 26 | ||||
-rwxr-xr-x | src/conf_mode/pki.py | 12 | ||||
-rwxr-xr-x | src/conf_mode/system_login.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/system_login_banner.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 2 | ||||
-rwxr-xr-x | src/op_mode/firewall.py | 143 | ||||
-rwxr-xr-x | src/op_mode/image_installer.py | 27 | ||||
-rwxr-xr-x | src/op_mode/stp.py | 185 | ||||
-rwxr-xr-x | src/services/vyos-commitd | 16 | ||||
-rwxr-xr-x | src/services/vyos-domain-resolver | 58 |
10 files changed, 412 insertions, 68 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 768bb127d..cebe57092 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -44,6 +44,7 @@ airbag.enable() nftables_conf = '/run/nftables.conf' domain_resolver_usage = '/run/use-vyos-domain-resolver-firewall' +firewall_config_dir = "/config/firewall" sysctl_file = r'/run/sysctl/10-vyos-firewall.conf' @@ -53,7 +54,8 @@ valid_groups = [ 'network_group', 'port_group', 'interface_group', - ## Added for group ussage in bridge firewall + 'remote_group', + ## Added for group usage in bridge firewall 'ipv4_address_group', 'ipv6_address_group', 'ipv4_network_group', @@ -311,8 +313,8 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): raise ConfigError('Only one of address, fqdn or geoip can be specified') if 'group' in side_conf: - if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: - raise ConfigError('Only one address-group, network-group or domain-group can be specified') + if len({'address_group', 'network_group', 'domain_group', 'remote_group'} & set(side_conf['group'])) > 1: + raise ConfigError('Only one address-group, network-group, remote-group or domain-group can be specified') for group in valid_groups: if group in side_conf['group']: @@ -332,7 +334,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): error_group = fw_group.replace("_", "-") - if group in ['address_group', 'network_group', 'domain_group']: + if group in ['address_group', 'network_group', 'domain_group', 'remote_group']: types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf] if types: raise ConfigError(f'{error_group} and {types[0]} cannot both be defined') @@ -442,6 +444,11 @@ def verify(firewall): for group_name, group in groups.items(): verify_nested_group(group_name, group, groups, []) + if 'remote_group' in firewall['group']: + for group_name, group in firewall['group']['remote_group'].items(): + if 'url' not in group: + raise ConfigError(f'remote-group {group_name} must have a url configured') + for family in ['ipv4', 'ipv6', 'bridge']: if family in firewall: for chain in ['name','forward','input','output', 'prerouting']: @@ -539,6 +546,15 @@ def verify(firewall): def generate(firewall): render(nftables_conf, 'firewall/nftables.j2', firewall) render(sysctl_file, 'firewall/sysctl-firewall.conf.j2', firewall) + + # Cleanup remote-group cache files + if os.path.exists(firewall_config_dir): + for fw_file in os.listdir(firewall_config_dir): + # Delete matching files in 'config/firewall' that no longer exist as a remote-group in config + if fw_file.startswith("R_") and fw_file.endswith(".txt"): + if 'group' not in firewall or 'remote_group' not in firewall['group'] or fw_file[2:-4] not in firewall['group']['remote_group'].keys(): + os.unlink(os.path.join(firewall_config_dir, fw_file)) + return None def parse_firewall_error(output): @@ -598,7 +614,7 @@ def apply(firewall): ## DOMAIN RESOLVER domain_action = 'restart' - if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items(): + if dict_search_args(firewall, 'group', 'remote_group') or dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items(): text = f'# Automatically generated by firewall.py\nThis file indicates that vyos-domain-resolver service is used by the firewall.\n' Path(domain_resolver_usage).write_text(text) else: diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index acea2c9be..724f97555 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -440,13 +440,21 @@ def generate(pki): for name, cert_conf in pki['certificate'].items(): if 'acme' in cert_conf: certbot_list.append(name) - # generate certificate if not found on disk + # There is no ACME/certbot managed certificate presend on the + # system, generate it if name not in certbot_list_on_disk: certbot_request(name, cert_conf['acme'], dry_run=False) + # Now that the certificate was properly generated we have + # the PEM files on disk. We need to add the certificate to + # certbot_list_on_disk to automatically import the CA chain + certbot_list_on_disk.append(name) + # We alredy had an ACME managed certificate on the system, but + # something changed in the configuration elif changed_certificates != None and name in changed_certificates: - # when something for the certificate changed, we should delete it + # Delete old ACME certificate first if name in certbot_list_on_disk: certbot_delete(name) + # Request new certificate via certbot certbot_request(name, cert_conf['acme'], dry_run=False) # Cleanup certbot configuration and certificates if no longer in use by CLI diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py index 1e6061ecf..3fed6d273 100755 --- a/src/conf_mode/system_login.py +++ b/src/conf_mode/system_login.py @@ -160,9 +160,10 @@ def verify(login): dict_object=user_config ) or None + failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR] if plaintext_password is not None: result = evaluate_strength(plaintext_password) - if result['strength'] == EPasswdStrength.WEAK: + if result['strength'] in failed_check_status: Warning(result['error']) for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items(): diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py index 5826d8042..cdd066649 100755 --- a/src/conf_mode/system_login_banner.py +++ b/src/conf_mode/system_login_banner.py @@ -95,8 +95,12 @@ def apply(banner): render(POSTLOGIN_FILE, 'login/default_motd.j2', banner, permission=0o644, user='root', group='root') - render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', banner, - permission=0o644, user='root', group='root') + if banner['version_data']['build_type'] != 'release': + render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', + banner, + permission=0o644, + user='root', + group='root') return None diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 71a503e61..2754314f7 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -64,6 +64,7 @@ swanctl_dir = '/etc/swanctl' charon_conf = '/etc/strongswan.d/charon.conf' charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf' +charon_systemd_conf = '/etc/strongswan.d/charon-systemd.conf' interface_conf = '/etc/strongswan.d/interfaces_use.conf' swanctl_conf = f'{swanctl_dir}/swanctl.conf' @@ -745,6 +746,7 @@ def generate(ipsec): render(charon_conf, 'ipsec/charon.j2', ipsec) render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec) render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec) + render(charon_systemd_conf, 'ipsec/charon_systemd.conf.j2', ipsec) render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec) render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec) diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index c197ca434..086536e4e 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -148,6 +148,38 @@ def get_nftables_group_members(family, table, name): return out +def get_nftables_remote_group_members(family, table, name): + prefix = 'ip6' if family == 'ipv6' else 'ip' + out = [] + + try: + results_str = cmd(f'nft -j list set {prefix} {table} {name}') + results = json.loads(results_str) + except: + return out + + if 'nftables' not in results: + return out + + for obj in results['nftables']: + if 'set' not in obj: + continue + + set_obj = obj['set'] + if 'elem' in set_obj: + for elem in set_obj['elem']: + # search for single IP elements + if isinstance(elem, str): + out.append(elem) + # search for prefix elements + elif isinstance(elem, dict) and 'prefix' in elem: + out.append(f"{elem['prefix']['addr']}/{elem['prefix']['len']}") + # search for IP range elements + elif isinstance(elem, dict) and 'range' in elem: + out.append(f"{elem['range'][0]}-{elem['range'][1]}") + + return out + def output_firewall_vertical(rules, headers, adjust=True): for rule in rules: adjusted_rule = rule + [""] * (len(headers) - len(rule)) if adjust else rule # account for different header length, like default-action @@ -253,15 +285,17 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule if not source_addr: source_addr = dict_search_args(rule_conf, 'source', 'group', 'domain_group') if not source_addr: - source_addr = dict_search_args(rule_conf, 'source', 'fqdn') + source_addr = dict_search_args(rule_conf, 'source', 'group', 'remote_group') if not source_addr: - source_addr = dict_search_args(rule_conf, 'source', 'geoip', 'country_code') - if source_addr: - source_addr = str(source_addr)[1:-1].replace('\'','') - if 'inverse_match' in dict_search_args(rule_conf, 'source', 'geoip'): - source_addr = 'NOT ' + str(source_addr) + source_addr = dict_search_args(rule_conf, 'source', 'fqdn') if not source_addr: - source_addr = 'any' + source_addr = dict_search_args(rule_conf, 'source', 'geoip', 'country_code') + if source_addr: + source_addr = str(source_addr)[1:-1].replace('\'','') + if 'inverse_match' in dict_search_args(rule_conf, 'source', 'geoip'): + source_addr = 'NOT ' + str(source_addr) + if not source_addr: + source_addr = 'any' # Get destination dest_addr = dict_search_args(rule_conf, 'destination', 'address') @@ -272,15 +306,17 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule if not dest_addr: dest_addr = dict_search_args(rule_conf, 'destination', 'group', 'domain_group') if not dest_addr: - dest_addr = dict_search_args(rule_conf, 'destination', 'fqdn') + dest_addr = dict_search_args(rule_conf, 'destination', 'group', 'remote_group') if not dest_addr: - dest_addr = dict_search_args(rule_conf, 'destination', 'geoip', 'country_code') - if dest_addr: - dest_addr = str(dest_addr)[1:-1].replace('\'','') - if 'inverse_match' in dict_search_args(rule_conf, 'destination', 'geoip'): - dest_addr = 'NOT ' + str(dest_addr) + dest_addr = dict_search_args(rule_conf, 'destination', 'fqdn') if not dest_addr: - dest_addr = 'any' + dest_addr = dict_search_args(rule_conf, 'destination', 'geoip', 'country_code') + if dest_addr: + dest_addr = str(dest_addr)[1:-1].replace('\'','') + if 'inverse_match' in dict_search_args(rule_conf, 'destination', 'geoip'): + dest_addr = 'NOT ' + str(dest_addr) + if not dest_addr: + dest_addr = 'any' # Get inbound interface iiface = dict_search_args(rule_conf, 'inbound_interface', 'name') @@ -552,30 +588,8 @@ def show_firewall_group(name=None): header_tail = [] for group_type, group_type_conf in firewall['group'].items(): - ## - if group_type != 'dynamic_group': - - for group_name, group_conf in group_type_conf.items(): - if name and name != group_name: - continue - - references = find_references(group_type, group_name) - row = [group_name, textwrap.fill(group_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D'] - if 'address' in group_conf: - row.append("\n".join(sorted(group_conf['address']))) - elif 'network' in group_conf: - row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) - elif 'mac_address' in group_conf: - row.append("\n".join(sorted(group_conf['mac_address']))) - elif 'port' in group_conf: - row.append("\n".join(sorted(group_conf['port']))) - elif 'interface' in group_conf: - row.append("\n".join(sorted(group_conf['interface']))) - else: - row.append('N/D') - rows.append(row) - - else: + # interate over dynamic-groups + if group_type == 'dynamic_group': if not args.detail: header_tail = ['Timeout', 'Expires'] @@ -622,6 +636,59 @@ def show_firewall_group(name=None): header_tail += [""] * (len(members) - 1) rows.append(row) + # iterate over remote-groups + elif group_type == 'remote_group': + for remote_name, remote_conf in group_type_conf.items(): + if name and name != remote_name: + continue + + references = find_references(group_type, remote_name) + row = [remote_name, textwrap.fill(remote_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D'] + members = get_nftables_remote_group_members("ipv4", 'vyos_filter', f'R_{remote_name}') + + if 'url' in remote_conf: + # display only the url if no members are found for both views + if not members: + if args.detail: + header_tail = ['Remote URL'] + row.append('N/D') + row.append(remote_conf['url']) + else: + row.append(remote_conf['url']) + rows.append(row) + else: + # display all table elements in detail view + if args.detail: + header_tail = ['Remote URL'] + row += [' '.join(members)] + row.append(remote_conf['url']) + rows.append(row) + else: + row.append(remote_conf['url']) + rows.append(row) + + # catch the rest of the group types + else: + for group_name, group_conf in group_type_conf.items(): + if name and name != group_name: + continue + + references = find_references(group_type, group_name) + row = [group_name, textwrap.fill(group_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D'] + if 'address' in group_conf: + row.append("\n".join(sorted(group_conf['address']))) + elif 'network' in group_conf: + row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) + elif 'mac_address' in group_conf: + row.append("\n".join(sorted(group_conf['mac_address']))) + elif 'port' in group_conf: + row.append("\n".join(sorted(group_conf['port']))) + elif 'interface' in group_conf: + row.append("\n".join(sorted(group_conf['interface']))) + else: + row.append('N/D') + rows.append(row) + if rows: print('Firewall Groups\n') if args.detail: diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index c6e9c7f6f..9c17d0229 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -58,6 +58,7 @@ MSG_ERR_FLAVOR_MISMATCH: str = 'The current image flavor is "{0}", the new image MSG_ERR_MISSING_ARCHITECTURE: str = 'The new image version data does not specify architecture, cannot check compatibility (is it a legacy release image?)' MSG_ERR_MISSING_FLAVOR: str = 'The new image version data does not specify flavor, cannot check compatibility (is it a legacy release image?)' MSG_ERR_CORRUPT_CURRENT_IMAGE: str = 'Version data in the current image is malformed: missing flavor and/or architecture fields. Upgrade compatibility cannot be checked.' +MSG_ERR_UNSUPPORTED_SIGNATURE_TYPE: str = 'Unsupported signature type, signature cannot be verified.' MSG_INFO_INSTALL_WELCOME: str = 'Welcome to VyOS installation!\nThis command will install VyOS to your permanent storage.' MSG_INFO_INSTALL_EXIT: str = 'Exiting from VyOS installation' MSG_INFO_INSTALL_SUCCESS: str = 'The image installed successfully; please reboot now.' @@ -514,7 +515,6 @@ def validate_signature(file_path: str, sign_type: str) -> None: """ print('Validating signature') signature_valid: bool = False - # validate with minisig if sign_type == 'minisig': pub_key_list = glob('/usr/share/vyos/keys/*.minisign.pub') for pubkey in pub_key_list: @@ -523,11 +523,8 @@ def validate_signature(file_path: str, sign_type: str) -> None: signature_valid = True break Path(f'{file_path}.minisig').unlink() - # validate with GPG - if sign_type == 'asc': - if run(f'gpg --verify ${file_path}.asc ${file_path}') == 0: - signature_valid = True - Path(f'{file_path}.asc').unlink() + else: + exit(MSG_ERR_UNSUPPORTED_SIGNATURE_TYPE) # warn or pass if not signature_valid: @@ -581,15 +578,18 @@ def image_fetch(image_path: str, vrf: str = None, try: # check a type of path if urlparse(image_path).scheme: - # download an image + # Download the image file ISO_DOWNLOAD_PATH = os.path.join(os.path.expanduser("~"), '{0}.iso'.format(uuid4())) download_file(ISO_DOWNLOAD_PATH, image_path, vrf, username, password, progressbar=True, check_space=True) - # download a signature + # Download the image signature + # VyOS only supports minisign signatures at the moment, + # but we keep the logic for multiple signatures + # in case we add something new in the future sign_file = (False, '') - for sign_type in ['minisig', 'asc']: + for sign_type in ['minisig']: try: download_file(f'{ISO_DOWNLOAD_PATH}.{sign_type}', f'{image_path}.{sign_type}', vrf, @@ -597,8 +597,8 @@ def image_fetch(image_path: str, vrf: str = None, sign_file = (True, sign_type) break except Exception: - print(f'{sign_type} signature is not available') - # validate a signature if it is available + print(f'Could not download {sign_type} signature') + # Validate the signature if it is available if sign_file[0]: validate_signature(ISO_DOWNLOAD_PATH, sign_file[1]) else: @@ -783,6 +783,7 @@ def install_image() -> None: break print(MSG_WARN_IMAGE_NAME_WRONG) + failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR] # ask for password while True: user_password: str = ask_input(MSG_INPUT_PASSWORD, no_echo=True, @@ -792,7 +793,7 @@ def install_image() -> None: Warning(MSG_WARN_CHANGE_PASSWORD) else: result = evaluate_strength(user_password) - if result['strength'] == EPasswdStrength.WEAK: + if result['strength'] in failed_check_status: Warning(result['error']) confirm: str = ask_input(MSG_INPUT_PASSWORD_CONFIRM, no_echo=True, @@ -1006,7 +1007,7 @@ def add_image(image_path: str, vrf: str = None, username: str = '', Path(target_config_dir).mkdir(parents=True) chown(target_config_dir, group='vyattacfg') chmod_2775(target_config_dir) - copytree('/opt/vyatta/etc/config/', target_config_dir, + copytree('/opt/vyatta/etc/config/', target_config_dir, symlinks=True, copy_function=copy_preserve_owner, dirs_exist_ok=True) else: Path(target_config_dir).mkdir(parents=True) diff --git a/src/op_mode/stp.py b/src/op_mode/stp.py new file mode 100755 index 000000000..fb57bd7ee --- /dev/null +++ b/src/op_mode/stp.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2025 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# 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 typing +import json +from tabulate import tabulate + +import vyos.opmode +from vyos.utils.process import cmd +from vyos.utils.network import interface_exists + +def detailed_output(dataset, headers): + for data in dataset: + adjusted_rule = data + [""] * (len(headers) - len(data)) # account for different header length, like default-action + transformed_rule = [[header, adjusted_rule[i]] for i, header in enumerate(headers) if i < len(adjusted_rule)] # create key-pair list from headers and rules lists; wrap at 100 char + + print(tabulate(transformed_rule, tablefmt="presto")) + print() + +def _get_bridge_vlan_data(iface): + allowed_vlans = [] + native_vlan = None + vlanData = json.loads(cmd(f"bridge -j -d vlan show")) + for vlans in vlanData: + if vlans['ifname'] == iface: + for allowed in vlans['vlans']: + if "flags" in allowed and "PVID" in allowed["flags"]: + native_vlan = allowed['vlan'] + elif allowed.get('vlanEnd', None): + allowed_vlans.append(f"{allowed['vlan']}-{allowed['vlanEnd']}") + else: + allowed_vlans.append(str(allowed['vlan'])) + + if not allowed_vlans: + allowed_vlans = ["none"] + if not native_vlan: + native_vlan = "none" + + return ",".join(allowed_vlans), native_vlan + +def _get_stp_data(ifname, brInfo, brStatus): + tmpInfo = {} + + tmpInfo['bridge_name'] = brInfo.get('ifname') + tmpInfo['up_state'] = brInfo.get('operstate') + tmpInfo['priority'] = brInfo.get('linkinfo').get('info_data').get('priority') + tmpInfo['vlan_filtering'] = "Enabled" if brInfo.get('linkinfo').get('info_data').get('vlan_filtering') == 1 else "Disabled" + tmpInfo['vlan_protocol'] = brInfo.get('linkinfo').get('info_data').get('vlan_protocol') + + # The version of VyOS I tested had am issue with the "ip -d link show type bridge" + # output. The root_id was always the local bridge, even though the underlying system + # understood when it wasn't. Could be an upstream Bug. I pull from the "/sys/class/net" + # structure instead. This can be changed later if the "ip link" behavior is corrected. + + #tmpInfo['bridge_id'] = brInfo.get('linkinfo').get('info_data').get('bridge_id') + #tmpInfo['root_id'] = brInfo.get('linkinfo').get('info_data').get('root_id') + + tmpInfo['bridge_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/bridge_id").split('.') + tmpInfo['root_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/root_id").split('.') + + # The "/sys/class/net" structure stores the IDs without seperators like ':' or '.' + # This adds a ':' after every 2 characters to make it resemble a MAC Address + tmpInfo['bridge_id'][1] = ':'.join(tmpInfo['bridge_id'][1][i:i+2] for i in range(0, len(tmpInfo['bridge_id'][1]), 2)) + tmpInfo['root_id'][1] = ':'.join(tmpInfo['root_id'][1][i:i+2] for i in range(0, len(tmpInfo['root_id'][1]), 2)) + + tmpInfo['stp_state'] = "Enabled" if brInfo.get('linkinfo', {}).get('info_data', {}).get('stp_state') == 1 else "Disabled" + + # I don't call any of these values, but I created them to be called within raw output if desired + + tmpInfo['mcast_snooping'] = "Enabled" if brInfo.get('linkinfo').get('info_data').get('mcast_snooping') == 1 else "Disabled" + tmpInfo['rxbytes'] = brInfo.get('stats64').get('rx').get('bytes') + tmpInfo['rxpackets'] = brInfo.get('stats64').get('rx').get('packets') + tmpInfo['rxerrors'] = brInfo.get('stats64').get('rx').get('errors') + tmpInfo['rxdropped'] = brInfo.get('stats64').get('rx').get('dropped') + tmpInfo['rxover_errors'] = brInfo.get('stats64').get('rx').get('over_errors') + tmpInfo['rxmulticast'] = brInfo.get('stats64').get('rx').get('multicast') + tmpInfo['txbytes'] = brInfo.get('stats64').get('tx').get('bytes') + tmpInfo['txpackets'] = brInfo.get('stats64').get('tx').get('packets') + tmpInfo['txerrors'] = brInfo.get('stats64').get('tx').get('errors') + tmpInfo['txdropped'] = brInfo.get('stats64').get('tx').get('dropped') + tmpInfo['txcarrier_errors'] = brInfo.get('stats64').get('tx').get('carrier_errors') + tmpInfo['txcollosions'] = brInfo.get('stats64').get('tx').get('collisions') + + tmpStatus = [] + for members in brStatus: + if members.get('master') == brInfo.get('ifname'): + allowed_vlans, native_vlan = _get_bridge_vlan_data(members['ifname']) + tmpStatus.append({'interface': members.get('ifname'), + 'state': members.get('state').capitalize(), + 'mtu': members.get('mtu'), + 'pathcost': members.get('cost'), + 'bpduguard': "Enabled" if members.get('guard') == True else "Disabled", + 'rootguard': "Enabled" if members.get('root_block') == True else "Disabled", + 'mac_learning': "Enabled" if members.get('learning') == True else "Disabled", + 'neigh_suppress': "Enabled" if members.get('neigh_suppress') == True else "Disabled", + 'vlan_tunnel': "Enabled" if members.get('vlan_tunnel') == True else "Disabled", + 'isolated': "Enabled" if members.get('isolated') == True else "Disabled", + **({'allowed_vlans': allowed_vlans} if allowed_vlans else {}), + **({'native_vlan': native_vlan} if native_vlan else {})}) + + tmpInfo['members'] = tmpStatus + return tmpInfo + +def show_stp(raw: bool, ifname: typing.Optional[str], detail: bool): + rawList = [] + rawDict = {'stp': []} + + if ifname: + if not interface_exists(ifname): + raise vyos.opmode.Error(f"{ifname} does not exist!") + else: + ifname = "" + + bridgeInfo = json.loads(cmd(f"ip -j -d -s link show type bridge {ifname}")) + + if not bridgeInfo: + raise vyos.opmode.Error(f"No Bridges configured!") + + bridgeStatus = json.loads(cmd(f"bridge -j -s -d link show")) + + for bridges in bridgeInfo: + output_list = [] + amRoot = "" + bridgeDict = _get_stp_data(ifname, bridges, bridgeStatus) + + if bridgeDict['bridge_id'][1] == bridgeDict['root_id'][1]: + amRoot = " (This bridge is the root)" + + print('-' * 80) + print(f"Bridge interface {bridgeDict['bridge_name']} ({bridgeDict['up_state']}):\n") + print(f"Spanning Tree is {bridgeDict['stp_state']}") + print(f"Bridge ID {bridgeDict['bridge_id'][1]}, Priority {int(bridgeDict['bridge_id'][0], 16)}") + print(f"Root ID {bridgeDict['root_id'][1]}, Priority {int(bridgeDict['root_id'][0], 16)}{amRoot}") + print(f"VLANs {bridgeDict['vlan_filtering'].capitalize()}, Protocol {bridgeDict['vlan_protocol']}") + print() + + for members in bridgeDict['members']: + output_list.append([members['interface'], + members['state'], + *([members['pathcost']] if detail else []), + members['bpduguard'], + members['rootguard'], + members['mac_learning'], + *([members['neigh_suppress']] if detail else []), + *([members['vlan_tunnel']] if detail else []), + *([members['isolated']] if detail else []), + *([members['allowed_vlans']] if detail else []), + *([members['native_vlan']] if detail else [])]) + + if raw: + rawList.append(bridgeDict) + elif detail: + headers = ['Interface', 'State', 'Pathcost', 'BPDU_Guard', 'Root_Guard', 'Learning', 'Neighbor_Suppression', 'Q-in-Q', 'Port_Isolation', 'Allowed VLANs', 'Native VLAN'] + detailed_output(output_list, headers) + else: + headers = ['Interface', 'State', 'BPDU_Guard', 'Root_Guard', 'Learning'] + print(tabulate(output_list, headers)) + print() + + if raw: + rawDict['stp'] = rawList + return rawDict + +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/services/vyos-commitd b/src/services/vyos-commitd index 8dbd39058..e7f2d82c7 100755 --- a/src/services/vyos-commitd +++ b/src/services/vyos-commitd @@ -72,8 +72,6 @@ class Session: # pylint: disable=too-many-instance-attributes session_id: str = '' - named_active: str = None - named_proposed: str = None dry_run: bool = False atomic: bool = False background: bool = False @@ -235,8 +233,9 @@ def initialization(session: Session) -> Session: scripts_called = [] setattr(config, 'scripts_called', scripts_called) - dry_run = False - setattr(config, 'dry_run', dry_run) + dry_run = session.dry_run + config.set_bool_attr('dry_run', dry_run) + logger.debug(f'commit dry_run is {dry_run}') session.config = config @@ -249,11 +248,16 @@ def run_script(script_name: str, config: Config, args: list) -> tuple[bool, str] script = conf_mode_scripts[script_name] script.argv = args config.set_level([]) + dry_run = config.get_bool_attr('dry_run') try: c = script.get_config(config) script.verify(c) - script.generate(c) - script.apply(c) + if not dry_run: + script.generate(c) + script.apply(c) + else: + if hasattr(script, 'call_dependents'): + script.call_dependents() except ConfigError as e: logger.error(e) return False, str(e) diff --git a/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver index 48c6b86d8..aba5ba9db 100755 --- a/src/services/vyos-domain-resolver +++ b/src/services/vyos-domain-resolver @@ -13,19 +13,22 @@ # # 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 json import time import logging +import os from vyos.configdict import dict_merge from vyos.configquery import ConfigTreeQuery from vyos.firewall import fqdn_config_parse from vyos.firewall import fqdn_resolve from vyos.ifconfig import WireGuardIf +from vyos.remote import download from vyos.utils.commit import commit_in_progress from vyos.utils.dict import dict_search_args from vyos.utils.kernel import WIREGUARD_REKEY_AFTER_TIME +from vyos.utils.file import makedir, chmod_775, write_file, read_file +from vyos.utils.network import is_valid_ipv4_address_or_range from vyos.utils.process import cmd from vyos.utils.process import run from vyos.xml_ref import get_defaults @@ -37,6 +40,8 @@ base_firewall = ['firewall'] base_nat = ['nat'] base_interfaces = ['interfaces'] +firewall_config_dir = "/config/firewall" + domain_state = {} ipv4_tables = { @@ -121,6 +126,56 @@ def nft_valid_sets(): except: return [] +def update_remote_group(config): + conf_lines = [] + count = 0 + valid_sets = nft_valid_sets() + + remote_groups = dict_search_args(config, 'group', 'remote_group') + if remote_groups: + # Create directory for list files if necessary + if not os.path.isdir(firewall_config_dir): + makedir(firewall_config_dir, group='vyattacfg') + chmod_775(firewall_config_dir) + + for set_name, remote_config in remote_groups.items(): + if 'url' not in remote_config: + continue + nft_set_name = f'R_{set_name}' + + # Create list file if necessary + list_file = os.path.join(firewall_config_dir, f"{nft_set_name}.txt") + if not os.path.exists(list_file): + write_file(list_file, '', user="root", group="vyattacfg", mode=0o644) + + # Attempt to download file, use cached version if download fails + try: + download(list_file, remote_config['url'], raise_error=True) + except: + logger.error(f'Failed to download list-file for {set_name} remote group') + logger.info(f'Using cached list-file for {set_name} remote group') + + # Read list file + ip_list = [] + for line in read_file(list_file).splitlines(): + line_first_word = line.strip().partition(' ')[0] + + if is_valid_ipv4_address_or_range(line_first_word): + ip_list.append(line_first_word) + + # Load tables + for table in ipv4_tables: + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip_list) + + count += 1 + + nft_conf_str = "\n".join(conf_lines) + "\n" + code = run(f'nft --file -', input=nft_conf_str) + + logger.info(f'Updated {count} remote-groups in firewall - result: {code}') + + def update_fqdn(config, node): conf_lines = [] count = 0 @@ -234,5 +289,6 @@ if __name__ == '__main__': while True: update_fqdn(firewall, 'firewall') update_fqdn(nat, 'nat') + update_remote_group(firewall) update_interfaces(interfaces, 'interfaces') time.sleep(timeout) |