diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/container.py | 9 | ||||
| -rwxr-xr-x | src/conf_mode/firewall.py | 35 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-macsec.py | 74 | ||||
| -rwxr-xr-x | src/conf_mode/nat.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/vrf.py | 21 | ||||
| -rwxr-xr-x | src/op_mode/neighbor.py | 5 | ||||
| -rwxr-xr-x | src/op_mode/vrf.py | 21 | 
7 files changed, 93 insertions, 74 deletions
| diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index ed7cc809c..478868a9a 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -33,6 +33,7 @@ from vyos.utils.process import call  from vyos.utils.process import cmd  from vyos.utils.process import run  from vyos.utils.process import rc_cmd +from vyos.template import bracketize_ipv6  from vyos.template import inc_ip  from vyos.template import is_ipv4  from vyos.template import is_ipv6 @@ -280,6 +281,14 @@ def generate_run_arguments(name, container_config):              protocol = container_config['port'][portmap]['protocol']              sport = container_config['port'][portmap]['source']              dport = container_config['port'][portmap]['destination'] +            listen_addresses = container_config['port'][portmap].get('listen_address', []) + +        # If listen_addresses is not empty, include them in the publish command +        if listen_addresses: +            for listen_address in listen_addresses: +                port += f' --publish {bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}' +        else: +            # If listen_addresses is empty, just include the standard publish command              port += f' --publish {sport}:{dport}/{protocol}'      # Bind volume diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index e946704b3..8ad3f27fc 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -351,39 +351,6 @@ def apply_sysfs(firewall):                  with open(path, 'w') as f:                      f.write(value) -def post_apply_trap(firewall): -    if 'first_install' in firewall: -        return None - -    if not process_named_running('snmpd'): -        return None - -    trap_username = os.getlogin() - -    for host, target_conf in firewall['trap_targets'].items(): -        community = target_conf['community'] if 'community' in target_conf else 'public' -        port = int(target_conf['port']) if 'port' in target_conf else 162 - -        base_cmd = f'snmptrap -v2c -c {community} {host}:{port} 0 {snmp_trap_mib}::{snmp_trap_name} ' - -        for change_type, changes in firewall['trap_diff'].items(): -            for path_str, value in changes.items(): -                objects = [ -                    f'mgmtEventUser s "{trap_username}"', -                    f'mgmtEventSource i {snmp_event_source}', -                    f'mgmtEventType i {snmp_change_type[change_type]}' -                ] - -                if change_type == 'add': -                    objects.append(f'mgmtEventCurrCfg s "{path_str} {value}"') -                elif change_type == 'delete': -                    objects.append(f'mgmtEventPrevCfg s "{path_str} {value}"') -                elif change_type == 'change': -                    objects.append(f'mgmtEventPrevCfg s "{path_str} {value[0]}"') -                    objects.append(f'mgmtEventCurrCfg s "{path_str} {value[1]}"') - -                cmd(base_cmd + ' '.join(objects)) -  def apply(firewall):      install_result, output = rc_cmd(f'nft -f {nftables_conf}')      if install_result == 1: @@ -408,8 +375,6 @@ def apply(firewall):              print('Updating GeoIP. Please wait...')              geoip_update(firewall) -    post_apply_trap(firewall) -      return None  if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 3f86e2638..0a927ac88 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-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -43,6 +43,14 @@ airbag.enable()  # XXX: wpa_supplicant works on the source interface  wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf' +# Constants +## gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit +GCM_AES_128_LEN: int = 32 +GCM_128_KEY_ERROR = 'gcm-aes-128 requires a 128bit long key!' +## gcm-aes-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit +GCM_AES_256_LEN: int = 64 +GCM_256_KEY_ERROR = 'gcm-aes-256 requires a 256bit long key!' +  def get_config(config=None):      """      Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -89,18 +97,54 @@ def verify(macsec):          raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))      if dict_search('security.encrypt', macsec) != None: -        if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: -            raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') +        # Check that only static or MKA config is present +        if dict_search('security.static', macsec) != None and (dict_search('security.mka.cak', macsec) != None or dict_search('security.mka.ckn', macsec) != None): +            raise ConfigError('Only static or MKA can be used!') + +        # Logic to check static configuration +        if dict_search('security.static', macsec) != None: +            # tx-key must be defined +            if dict_search('security.static.key', macsec) == None: +                raise ConfigError('Static MACsec tx-key must be defined.') + +            tx_len = len(dict_search('security.static.key', macsec)) + +            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and tx_len != GCM_AES_128_LEN: +                raise ConfigError(GCM_128_KEY_ERROR) + +            if dict_search('security.cipher', macsec) == 'gcm-aes-256' and tx_len != GCM_AES_256_LEN: +                raise ConfigError(GCM_256_KEY_ERROR) + +            # Make sure at least one peer is defined +            if 'peer' not in macsec['security']['static']: +                raise ConfigError('Must have at least one peer defined for static MACsec') + +            # For every enabled peer, make sure a MAC and rx-key is defined +            for peer, peer_config in macsec['security']['static']['peer'].items(): +                if 'disable' not in peer_config and ('mac' not in peer_config or 'key' not in peer_config): +                    raise ConfigError('Every enabled MACsec static peer must have a MAC address and rx-key defined.') + +                # check rx-key length against cipher suite +                rx_len = len(peer_config['key']) + +                if dict_search('security.cipher', macsec) == 'gcm-aes-128' and rx_len != GCM_AES_128_LEN: +                    raise ConfigError(GCM_128_KEY_ERROR) + +                if dict_search('security.cipher', macsec) == 'gcm-aes-256' and rx_len != GCM_AES_256_LEN: +                    raise ConfigError(GCM_256_KEY_ERROR) + +        # Logic to check MKA configuration +        else: +            if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: +                raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') -        cak_len = len(dict_search('security.mka.cak', macsec)) +            cak_len = len(dict_search('security.mka.cak', macsec)) -        if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != 32: -            # gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit -            raise ConfigError('gcm-aes-128 requires a 128bit long key!') +            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != GCM_AES_128_LEN: +                raise ConfigError(GCM_128_KEY_ERROR) -        elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != 64: -            # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit -            raise ConfigError('gcm-aes-128 requires a 256bit long key!') +            elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != GCM_AES_256_LEN: +                raise ConfigError(GCM_256_KEY_ERROR)      if 'source_interface' in macsec:          # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad @@ -115,7 +159,9 @@ def verify(macsec):  def generate(macsec): -    render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec) +    # Only generate wpa_supplicant config if using MKA +    if dict_search('security.mka.cak', macsec): +        render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec)      return None @@ -142,8 +188,10 @@ def apply(macsec):      i = MACsecIf(**macsec)      i.update(macsec) -    if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: -        call(f'systemctl reload-or-restart {systemd_service}') +    # Only reload/restart if using MKA +    if dict_search('security.mka.cak', macsec): +        if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: +            call(f'systemctl reload-or-restart {systemd_service}')      return None diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index f9d711b36..9da7fbe80 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -224,7 +224,7 @@ def verify(nat):              elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():                  Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') -            if not dict_search('translation.address', config) and not dict_search('translation.port', config) and not dict_search('translation.redirect.port', config): +            if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']:                  if 'exclude' not in config and 'backend' not in config['load_balance']:                      raise ConfigError(f'{err_msg} translation requires address and/or port') diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index be867b208..37625142c 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -28,6 +28,8 @@ from vyos.template import render  from vyos.template import render_to_string  from vyos.utils.dict import dict_search  from vyos.utils.network import get_interface_config +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 @@ -143,7 +145,7 @@ def verify(vrf):                  raise ConfigError(f'VRF "{name}" table id is mandatory!')              # routing table id can't be changed - OS restriction -            if os.path.isdir(f'/sys/class/net/{name}'): +            if interface_exists(name):                  tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name)))                  if tmp and tmp != vrf_config['table']:                      raise ConfigError(f'VRF "{name}" table id modification not possible!') @@ -195,12 +197,23 @@ def apply(vrf):      sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all)      for tmp in (dict_search('vrf_remove', vrf) or []): -        if os.path.isdir(f'/sys/class/net/{tmp}'): -            call(f'ip link delete dev {tmp}') +        if interface_exists(tmp): +            # T5492: deleting a VRF instance may leafe processes running +            # (e.g. dhclient) as there is a depedency ordering issue in the CLI. +            # We need to ensure that we stop the dhclient processes first so +            # a proper DHCLP RELEASE message is sent +            for interface in get_vrf_members(tmp): +                vrf_iface = Interface(interface) +                vrf_iface.set_dhcp(False) +                vrf_iface.set_dhcpv6(False) +              # 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}') +            # Delete the VRF Kernel interface +            call(f'ip link delete dev {tmp}') +      if 'name' in vrf:          # Separate VRFs in conntrack table          # check if table already exists @@ -245,7 +258,7 @@ def apply(vrf):          for name, config in vrf['name'].items():              table = config['table'] -            if not os.path.isdir(f'/sys/class/net/{name}'): +            if not interface_exists(name):                  # For each VRF apart from your default context create a VRF                  # interface with a separate routing table                  call(f'ip link add {name} type vrf table {table}') diff --git a/src/op_mode/neighbor.py b/src/op_mode/neighbor.py index 1edeb0045..8b3c45c7c 100755 --- a/src/op_mode/neighbor.py +++ b/src/op_mode/neighbor.py @@ -31,14 +31,11 @@ import sys  import typing  import vyos.opmode +from vyos.utils.network import interface_exists  ArgFamily = typing.Literal['inet', 'inet6']  ArgState = typing.Literal['reachable', 'stale', 'failed', 'permanent'] -def interface_exists(interface): -    import os -    return os.path.exists(f'/sys/class/net/{interface}') -  def get_raw_data(family, interface=None, state=None):      from json import loads      from vyos.utils.process import cmd diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py index 1f0bbbaeb..51032a4b5 100755 --- a/src/op_mode/vrf.py +++ b/src/op_mode/vrf.py @@ -20,11 +20,11 @@ import sys  import typing  from tabulate import tabulate +from vyos.utils.network import get_vrf_members  from vyos.utils.process import cmd  import vyos.opmode -  def _get_raw_data(name=None):      """      If vrf name is not set - get all VRFs @@ -45,21 +45,6 @@ def _get_raw_data(name=None):      return data -def _get_vrf_members(vrf: str) -> list: -    """ -    Get list of interface VRF members -    :param vrf: str -    :return: list -    """ -    output = cmd(f'ip --json --brief link show master {vrf}') -    answer = json.loads(output) -    interfaces = [] -    for data in answer: -        if 'ifname' in data: -            interfaces.append(data.get('ifname')) -    return interfaces if len(interfaces) > 0 else ['n/a'] - -  def _get_formatted_output(raw_data):      data_entries = []      for vrf in raw_data: @@ -67,7 +52,9 @@ def _get_formatted_output(raw_data):          state = vrf.get('operstate').lower()          hw_address = vrf.get('address')          flags = ','.join(vrf.get('flags')).lower() -        members = ','.join(_get_vrf_members(name)) +        tmp = get_vrf_members(name) +        if tmp: members = ','.join(get_vrf_members(name)) +        else: members = 'n/a'          data_entries.append([name, state, hw_address, flags, members])      headers = ["Name", "State", "MAC address", "Flags", "Interfaces"] | 
