diff options
Diffstat (limited to 'src')
30 files changed, 1203 insertions, 785 deletions
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index ac7d95632..66f7c8057 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -21,10 +21,16 @@ from ipaddress import ip_network  from netaddr import IPAddress  from netaddr import IPRange  from sys import exit +from time import sleep  from vyos.config import Config +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key  from vyos.template import render  from vyos.utils.dict import dict_search +from vyos.utils.dict import dict_search_args +from vyos.utils.file import chmod_775 +from vyos.utils.file import write_file  from vyos.utils.process import call  from vyos.utils.process import run  from vyos.utils.network import is_subnet_connected @@ -33,8 +39,14 @@ from vyos import ConfigError  from vyos import airbag  airbag.enable() -config_file = '/run/dhcp-server/dhcpd.conf' -systemd_override = r'/run/systemd/system/isc-dhcp-server.service.d/10-override.conf' +ctrl_config_file = '/run/kea/kea-ctrl-agent.conf' +ctrl_socket = '/run/kea/dhcp4-ctrl-socket' +config_file = '/run/kea/kea-dhcp4.conf' +lease_file = '/config/dhcp4.leases' + +ca_cert_file = '/run/kea/kea-failover-ca.pem' +cert_file = '/run/kea/kea-failover.pem' +cert_key_file = '/run/kea/kea-failover-key.pem'  def dhcp_slice_range(exclude_list, range_dict):      """ @@ -130,6 +142,9 @@ def get_config(config=None):                          dhcp['shared_network_name'][network]['subnet'][subnet].update(                                  {'range' : new_range_dict}) +    if dict_search('failover.certificate', dhcp): +        dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)  +      return dhcp  def verify(dhcp): @@ -166,13 +181,6 @@ def verify(dhcp):                      if 'next_hop' not in route_option:                          raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!') -            # DHCP failover needs at least one subnet that uses it -            if 'enable_failover' in subnet_config: -                if 'failover' not in dhcp: -                    raise ConfigError(f'Can not enable failover for "{subnet}" in "{network}".\n' \ -                                      'Failover is not configured globally!') -                failover_ok = True -              # Check if DHCP address range is inside configured subnet declaration              if 'range' in subnet_config:                  networks = [] @@ -249,14 +257,34 @@ def verify(dhcp):          raise ConfigError(f'At least one shared network must be active!')      if 'failover' in dhcp: -        if not failover_ok: -            raise ConfigError('DHCP failover must be enabled for at least one subnet!') -          for key in ['name', 'remote', 'source_address', 'status']:              if key not in dhcp['failover']:                  tmp = key.replace('_', '-')                  raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!') +        if len({'certificate', 'ca_certificate'} & set(dhcp['failover'])) == 1: +            raise ConfigError(f'DHCP secured failover requires both certificate and CA certificate') + +        if 'certificate' in dhcp['failover']: +            cert_name = dhcp['failover']['certificate'] + +            if cert_name not in dhcp['pki']['certificate']: +                raise ConfigError(f'Invalid certificate specified for DHCP failover') + +            if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'): +                raise ConfigError(f'Invalid certificate specified for DHCP failover') + +            if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'): +                raise ConfigError(f'Missing private key on certificate specified for DHCP failover') + +        if 'ca_certificate' in dhcp['failover']: +            ca_cert_name = dhcp['failover']['ca_certificate'] +            if ca_cert_name not in dhcp['pki']['ca']: +                raise ConfigError(f'Invalid CA certificate specified for DHCP failover') + +            if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'): +                raise ConfigError(f'Invalid CA certificate specified for DHCP failover') +      for address in (dict_search('listen_address', dhcp) or []):          if is_addr_assigned(address):              listen_ok = True @@ -278,43 +306,71 @@ def generate(dhcp):      if not dhcp or 'disable' in dhcp:          return None -    # Please see: https://vyos.dev/T1129 for quoting of the raw -    # parameters we can pass to ISC DHCPd -    tmp_file = '/tmp/dhcpd.conf' -    render(tmp_file, 'dhcp-server/dhcpd.conf.j2', dhcp, -           formater=lambda _: _.replace(""", '"')) -    # XXX: as we have the ability for a user to pass in "raw" options via VyOS -    # CLI (see T3544) we now ask ISC dhcpd to test the newly rendered -    # configuration -    tmp = run(f'/usr/sbin/dhcpd -4 -q -t -cf {tmp_file}') -    if tmp > 0: -        if os.path.exists(tmp_file): -            os.unlink(tmp_file) -        raise ConfigError('Configuration file errors encountered - check your options!') - -    # Now that we know that the newly rendered configuration is "good" we can -    # render the "real" configuration -    render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp, -           formater=lambda _: _.replace(""", '"')) -    render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp) - -    # Clean up configuration test file -    if os.path.exists(tmp_file): -        os.unlink(tmp_file) +    dhcp['lease_file'] = lease_file +    dhcp['machine'] = os.uname().machine + +    if not os.path.exists(lease_file): +        write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755) + +    for f in [cert_file, cert_key_file, ca_cert_file]: +        if os.path.exists(f): +            os.unlink(f) + +    if 'failover' in dhcp: +        if 'certificate' in dhcp['failover']: +            cert_name = dhcp['failover']['certificate'] +            cert_data = dhcp['pki']['certificate'][cert_name]['certificate'] +            key_data = dhcp['pki']['certificate'][cert_name]['private']['key'] +            write_file(cert_file, wrap_certificate(cert_data), user='_kea', mode=0o600) +            write_file(cert_key_file, wrap_private_key(key_data), user='_kea', mode=0o600) + +            dhcp['failover']['cert_file'] = cert_file +            dhcp['failover']['cert_key_file'] = cert_key_file + +        if 'ca_certificate' in dhcp['failover']: +            ca_cert_name = dhcp['failover']['ca_certificate'] +            ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate'] +            write_file(ca_cert_file, wrap_certificate(ca_cert_data), user='_kea', mode=0o600) + +            dhcp['failover']['ca_cert_file'] = ca_cert_file + +    render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp) +    render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp)      return None  def apply(dhcp): -    call('systemctl daemon-reload') -    # bail out early - looks like removal from running config +    services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server'] +      if not dhcp or 'disable' in dhcp: -        call('systemctl stop isc-dhcp-server.service') +        for service in services: +            call(f'systemctl stop {service}.service') +          if os.path.exists(config_file):              os.unlink(config_file)          return None -    call('systemctl restart isc-dhcp-server.service') +    for service in services: +        action = 'restart' + +        if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp: +            action = 'stop' + +        if service == 'kea-ctrl-agent' and 'failover' not in dhcp: +            action = 'stop' + +        call(f'systemctl {action} {service}.service') + +    # op-mode needs ctrl socket permission change +    i = 0 +    while not os.path.exists(ctrl_socket): +        if i > 15: +            break +        i += 1 +        sleep(1) +    chmod_775(ctrl_socket) +      return None  if __name__ == '__main__': diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 427001609..73a708ff5 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -19,18 +19,23 @@ import os  from ipaddress import ip_address  from ipaddress import ip_network  from sys import exit +from time import sleep  from vyos.config import Config  from vyos.template import render  from vyos.template import is_ipv6  from vyos.utils.process import call +from vyos.utils.file import chmod_775 +from vyos.utils.file import write_file  from vyos.utils.dict import dict_search  from vyos.utils.network import is_subnet_connected  from vyos import ConfigError  from vyos import airbag  airbag.enable() -config_file = '/run/dhcp-server/dhcpdv6.conf' +config_file = '/run/kea/kea-dhcp6.conf' +ctrl_socket = '/run/kea/dhcp6-ctrl-socket' +lease_file = '/config/dhcp6.leases'  def get_config(config=None):      if config: @@ -110,17 +115,20 @@ def verify(dhcpv6):              # Prefix delegation sanity checks              if 'prefix_delegation' in subnet_config: -                if 'start' not in subnet_config['prefix_delegation']: -                    raise ConfigError('prefix-delegation start address not defined!') +                if 'prefix' not in subnet_config['prefix_delegation']: +                    raise ConfigError('prefix-delegation prefix not defined!') -                for prefix, prefix_config in subnet_config['prefix_delegation']['start'].items(): -                    if 'stop' not in prefix_config: -                        raise ConfigError(f'Stop address of delegated IPv6 prefix range "{prefix}" '\ +                for prefix, prefix_config in subnet_config['prefix_delegation']['prefix'].items(): +                    if 'delegated_length' not in prefix_config: +                        raise ConfigError(f'Delegated IPv6 prefix length for "{prefix}" '\                                            f'must be configured')                      if 'prefix_length' not in prefix_config:                          raise ConfigError('Length of delegated IPv6 prefix must be configured') +                    if prefix_config['prefix_length'] > prefix_config['delegated_length']: +                        raise ConfigError('Length of delegated IPv6 prefix must be within parent prefix') +              # Static mappings don't require anything (but check if IP is in subnet if it's set)              if 'static_mapping' in subnet_config:                  for mapping, mapping_config in subnet_config['static_mapping'].items(): @@ -168,12 +176,18 @@ def generate(dhcpv6):      if not dhcpv6 or 'disable' in dhcpv6:          return None -    render(config_file, 'dhcp-server/dhcpdv6.conf.j2', dhcpv6) +    dhcpv6['lease_file'] = lease_file +    dhcpv6['machine'] = os.uname().machine + +    if not os.path.exists(lease_file): +        write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755) + +    render(config_file, 'dhcp-server/kea-dhcp6.conf.j2', dhcpv6)      return None  def apply(dhcpv6):      # bail out early - looks like removal from running config -    service_name = 'isc-dhcp-server6.service' +    service_name = 'kea-dhcp6-server.service'      if not dhcpv6 or 'disable' in dhcpv6:          # DHCP server is removed in the commit          call(f'systemctl stop {service_name}') @@ -182,6 +196,16 @@ def apply(dhcpv6):          return None      call(f'systemctl restart {service_name}') + +    # op-mode needs ctrl socket permission change +    i = 0 +    while not os.path.exists(ctrl_socket): +        if i > 15: +            break +        i += 1 +        sleep(1) +    chmod_775(ctrl_socket) +      return None  if __name__ == '__main__': diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 3ddc8e7fd..809c650d9 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -18,6 +18,7 @@ import os  from sys import exit +from vyos.base import Warning  from vyos.config import Config  from vyos.configverify import verify_interface_exists  from vyos.template import render @@ -29,6 +30,9 @@ airbag.enable()  config_file = r'/run/ddclient/ddclient.conf'  systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' +# Dynamic interfaces that might not exist when the configuration is loaded +dynamic_interfaces = ('pppoe', 'sstpc') +  # Protocols that require zone  zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi',                    'nfsn', 'nsupdate'] @@ -85,12 +89,19 @@ def verify(dyndns):              if field not in config:                  raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') -        # If dyndns address is an interface, ensure that it exists +        # If dyndns address is an interface, ensure +        # that the interface exists (or just warn if dynamic interface)          # and that web-options are not set          if config['address'] != 'web': -            verify_interface_exists(config['address']) +            # exclude check interface for dynamic interfaces +            if config['address'].startswith(dynamic_interfaces): +                Warning(f'Interface "{config["address"]}" does not exist yet and cannot ' +                        f'be used for Dynamic DNS service "{service}" until it is up!') +            else: +                verify_interface_exists(config['address'])              if 'web_options' in config: -                raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') +                raise ConfigError(f'"web-options" is applicable only when using HTTP(S) ' +                                  f'web request to obtain the IP address')          # RFC2136 uses 'key' instead of 'password'          if config['protocol'] != 'nsupdate' and 'password' not in config: @@ -118,13 +129,16 @@ def verify(dyndns):          if config['ip_version'] == 'both':              if config['protocol'] not in dualstack_supported: -                raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} with protocol "{config["protocol"]}"') +                raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} ' +                                  f'with protocol "{config["protocol"]}"')              # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org)              if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers: -                raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} for "{config["server"]}" with protocol "{config["protocol"]}"') +                raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} ' +                                  f'for "{config["server"]}" with protocol "{config["protocol"]}"')          if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']): -                raise ConfigError(f'"expiry-time" must be greater than "wait-time" for Dynamic DNS service "{service}"') +                raise ConfigError(f'"expiry-time" must be greater than "wait-time" for ' +                                  f'Dynamic DNS service "{service}"')      return None diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 00015023c..557f0a9e9 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -93,6 +93,7 @@ def get_config(config=None):      tmp = conf.get_config_dict(['policy'])      # Merge policy dict into "regular" config dict      bgp = dict_merge(tmp, bgp) +      return bgp diff --git a/src/conf_mode/protocols_segment_routing.py b/src/conf_mode/protocols_segment_routing.py new file mode 100755 index 000000000..eb1653212 --- /dev/null +++ b/src/conf_mode/protocols_segment_routing.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.template import render_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 = ['protocols', 'segment-routing'] +    sr = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + +    # We have gathered the dict representation of the CLI, but there are default +    # options which we need to update into the dictionary retrived. +    sr = conf.merge_defaults(sr, recursive=True) + +    return sr + +def verify(static): +    return None + +def generate(static): +    if not static: +        return None + +    static['new_frr_config'] = render_to_string('frr/zebra.segment_routing.frr.j2', static) +    return None + +def apply(static): +    zebra_daemon = 'zebra' + +    # Save original configuration prior to starting any commit actions +    frr_cfg = frr.FRRConfig() +    frr_cfg.load_configuration(zebra_daemon) +    frr_cfg.modify_section(r'^segment-routing') +    if 'new_frr_config' in static: +        frr_cfg.add_before(frr.default_add_before, static['new_frr_config']) +    frr_cfg.commit_configuration(zebra_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/system-login.py b/src/conf_mode/system-login.py index 87a269499..aeac82462 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -306,6 +306,7 @@ def generate(login):  def apply(login): +    enable_otp = False      if 'user' in login:          for user, user_config in login['user'].items():              # make new user using vyatta shell and make home directory (-m), @@ -330,7 +331,7 @@ def apply(login):              if tmp: command += f" --home '{tmp}'"              else: command += f" --home '/home/{user}'" -            command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk {user}' +            command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea {user}'              try:                  cmd(command) @@ -350,6 +351,7 @@ def apply(login):              # Generate 2FA/MFA One-Time-Pad configuration              if dict_search('authentication.otp.key', user_config): +                enable_otp = True                  render(f'{home_dir}/.google_authenticator', 'login/pam_otp_ga.conf.j2',                         user_config, permission=0o400, user=user, group='users')              else: @@ -398,6 +400,11 @@ def apply(login):              pam_profile = 'tacplus-optional'          cmd(f'pam-auth-update --enable {pam_profile}') +    # Enable/disable Google authenticator +    cmd('pam-auth-update --disable mfa-google-authenticator') +    if enable_otp: +        cmd(f'pam-auth-update --enable mfa-google-authenticator') +      return None diff --git a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf new file mode 100644 index 000000000..0f5bf801e --- /dev/null +++ b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf @@ -0,0 +1,9 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-ctrl-agent -c /run/kea/kea-ctrl-agent.conf +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE diff --git a/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf new file mode 100644 index 000000000..682e5bbce --- /dev/null +++ b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-dhcp4 -c /run/kea/kea-dhcp4.conf diff --git a/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf new file mode 100644 index 000000000..cb33fc057 --- /dev/null +++ b/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-dhcp6 -c /run/kea/kea-dhcp6.conf diff --git a/src/helpers/simple-download.py b/src/helpers/simple-download.py new file mode 100755 index 000000000..501af75f5 --- /dev/null +++ b/src/helpers/simple-download.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import sys +from argparse import ArgumentParser +from vyos.remote import download + +parser = ArgumentParser() +parser.add_argument('--local-file', help='local file', required=True) +parser.add_argument('--remote-path', help='remote path', required=True) + +args = parser.parse_args() + +try: +    download(args.local_file, args.remote_path, +             check_space=True, raise_error=True) +except Exception as e: +    print(e) +    sys.exit(1) + +sys.exit() diff --git a/src/init/vyos-router b/src/init/vyos-router index 35095afe4..711681a8e 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -260,6 +260,8 @@ EOF      rm -f /etc/pam_radius_auth.conf      pam-auth-update --disable tacplus-mandatory tacplus-optional      rm -f /etc/tacplus_nss.conf /etc/tacplus_servers +    # and no Google authenticator for 2FA/MFA +    pam-auth-update --disable mfa-google-authenticator      # Certain configuration files are re-generated by the configuration      # subsystem and must reside under /etc and can not easily be moved to /run. diff --git a/src/migration-scripts/dhcp-server/6-to-7 b/src/migration-scripts/dhcp-server/6-to-7 new file mode 100755 index 000000000..ccf385a30 --- /dev/null +++ b/src/migration-scripts/dhcp-server/6-to-7 @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +# T3316: Migrate to Kea +#        - global-parameters will not function +#        - shared-network-parameters will not function +#        - subnet-parameters will not function +#        - static-mapping-parameters will not function +#        - host-decl-name is on by default, option removed +#        - ping-check no longer supported +#        - failover is default enabled on all subnets that exist on failover servers + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 2): +    print("Must specify file name!") +    sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['service', 'dhcp-server'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    sys.exit(0) + +if config.exists(base + ['host-decl-name']): +    config.delete(base + ['host-decl-name']) + +if config.exists(base + ['global-parameters']): +    config.delete(base + ['global-parameters']) + +if config.exists(base + ['shared-network-name']): +    for network in config.list_nodes(base + ['shared-network-name']): +        base_network = base + ['shared-network-name', network] + +        if config.exists(base_network + ['ping-check']): +            config.delete(base_network + ['ping-check']) + +        if config.exists(base_network + ['shared-network-parameters']): +            config.delete(base_network +['shared-network-parameters']) + +        if not config.exists(base_network + ['subnet']): +            continue + +        # Run this for every specified 'subnet' +        for subnet in config.list_nodes(base_network + ['subnet']): +            base_subnet = base_network + ['subnet', subnet] + +            if config.exists(base_subnet + ['enable-failover']): +                config.delete(base_subnet + ['enable-failover']) + +            if config.exists(base_subnet + ['ping-check']): +                config.delete(base_subnet + ['ping-check']) + +            if config.exists(base_subnet + ['subnet-parameters']): +                config.delete(base_subnet + ['subnet-parameters']) + +            if config.exists(base_subnet + ['static-mapping']): +                for mapping in config.list_nodes(base_subnet + ['static-mapping']): +                    if config.exists(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']): +                        config.delete(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) diff --git a/src/migration-scripts/dhcpv6-server/1-to-2 b/src/migration-scripts/dhcpv6-server/1-to-2 new file mode 100755 index 000000000..cc5a8900a --- /dev/null +++ b/src/migration-scripts/dhcpv6-server/1-to-2 @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +# T3316: Migrate to Kea +# - Kea was meant to have support for key "prefix-highest" under PD which would allow an address range +#   However this seems to have never been implemented. A conversion to prefix length is needed (where possible). +#   Ref: https://lists.isc.org/pipermail/kea-users/2022-November/003686.html +# - Remove prefix temporary value, convert to multi leafNode (https://kea.readthedocs.io/en/kea-2.2.0/arm/dhcp6-srv.html#dhcpv6-server-limitations) + +import sys +from vyos.configtree import ConfigTree +from vyos.utils.network import ipv6_prefix_length + +if (len(sys.argv) < 1): +    print("Must specify file name!") +    sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['service', 'dhcpv6-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +for network in config.list_nodes(base): +    if not config.exists(base + [network, 'subnet']): +        continue + +    for subnet in config.list_nodes(base + [network, 'subnet']): +        # Delete temporary value under address-range prefix, convert tagNode to leafNode multi +        if config.exists(base + [network, 'subnet', subnet, 'address-range', 'prefix']): +            prefix_base = base + [network, 'subnet', subnet, 'address-range', 'prefix'] +            prefixes = config.list_nodes(prefix_base) +             +            config.delete(prefix_base) + +            for prefix in prefixes: +                config.set(prefix_base, value=prefix, replace=False) + +        if config.exists(base + [network, 'subnet', subnet, 'prefix-delegation', 'prefix']): +            prefix_base = base + [network, 'subnet', subnet, 'prefix-delegation', 'prefix'] + +            config.set(prefix_base) +            config.set_tag(prefix_base) + +            for start in config.list_nodes(base + [network, 'subnet', subnet, 'prefix-delegation', 'start']): +                path = base + [network, 'subnet', subnet, 'prefix-delegation', 'start', start] + +                delegated_length = config.return_value(path + ['prefix-length']) +                stop = config.return_value(path + ['stop']) + +                prefix_length = ipv6_prefix_length(start, stop) + +                # This range could not be converted into a simple prefix length and must be skipped +                if not prefix_length: +                    continue + +                config.set(prefix_base + [start, 'delegated-length'], value=delegated_length) +                config.set(prefix_base + [start, 'prefix-length'], value=prefix_length) + +            config.delete(base + [network, 'subnet', subnet, 'prefix-delegation', 'start']) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) diff --git a/src/migration-scripts/interfaces/22-to-23 b/src/migration-scripts/interfaces/22-to-23 index 8b21fce51..04e023e77 100755 --- a/src/migration-scripts/interfaces/22-to-23 +++ b/src/migration-scripts/interfaces/22-to-23 @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -13,133 +13,45 @@  #  # 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 argv -from sys import exit +# +# Deletes Wireguard peers if they have the same public key as the router has. +import sys  from vyos.configtree import ConfigTree - -def migrate_ospf(config, path, interface): -    path = path + ['ospf'] -    if config.exists(path): -        new_base = ['protocols', 'ospf', 'interface'] -        config.set(new_base) -        config.set_tag(new_base) -        config.copy(path, new_base + [interface]) -        config.delete(path) - -        # if "ip ospf" was the only setting, we can clean out the empty -        # ip node afterwards -        if len(config.list_nodes(path[:-1])) == 0: -            config.delete(path[:-1]) - -def migrate_ospfv3(config, path, interface): -    path = path + ['ospfv3'] -    if config.exists(path): -        new_base = ['protocols', 'ospfv3', 'interface'] -        config.set(new_base) -        config.set_tag(new_base) -        config.copy(path, new_base + [interface]) -        config.delete(path) - -        # if "ipv6 ospfv3" was the only setting, we can clean out the empty -        # ip node afterwards -        if len(config.list_nodes(path[:-1])) == 0: -            config.delete(path[:-1]) - -def migrate_rip(config, path, interface): -    path = path + ['rip'] -    if config.exists(path): -        new_base = ['protocols', 'rip', 'interface'] -        config.set(new_base) -        config.set_tag(new_base) -        config.copy(path, new_base + [interface]) -        config.delete(path) - -        # if "ip rip" was the only setting, we can clean out the empty -        # ip node afterwards -        if len(config.list_nodes(path[:-1])) == 0: -            config.delete(path[:-1]) - -def migrate_ripng(config, path, interface): -    path = path + ['ripng'] -    if config.exists(path): -        new_base = ['protocols', 'ripng', 'interface'] -        config.set(new_base) -        config.set_tag(new_base) -        config.copy(path, new_base + [interface]) -        config.delete(path) - -        # if "ipv6 ripng" was the only setting, we can clean out the empty -        # ip node afterwards -        if len(config.list_nodes(path[:-1])) == 0: -            config.delete(path[:-1]) +from vyos.utils.network import is_wireguard_key_pair  if __name__ == '__main__': -    if len(argv) < 2: +    if len(sys.argv) < 2:          print("Must specify file name!") -        exit(1) +        sys.exit(1) + +    file_name = sys.argv[1] -    file_name = argv[1]      with open(file_name, 'r') as f:          config_file = f.read()      config = ConfigTree(config_file) - -    # -    # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" -    # -    for type in config.list_nodes(['interfaces']): -        for interface in config.list_nodes(['interfaces', type]): -            ip_base = ['interfaces', type, interface, 'ip'] -            ipv6_base = ['interfaces', type, interface, 'ipv6'] -            migrate_rip(config, ip_base, interface) -            migrate_ripng(config, ipv6_base, interface) -            migrate_ospf(config, ip_base, interface) -            migrate_ospfv3(config, ipv6_base, interface) - -            vif_path = ['interfaces', type, interface, 'vif'] -            if config.exists(vif_path): -                for vif in config.list_nodes(vif_path): -                    vif_ip_base = vif_path + [vif, 'ip'] -                    vif_ipv6_base = vif_path + [vif, 'ipv6'] -                    ifname = f'{interface}.{vif}' - -                    migrate_rip(config, vif_ip_base, ifname) -                    migrate_ripng(config, vif_ipv6_base, ifname) -                    migrate_ospf(config, vif_ip_base, ifname) -                    migrate_ospfv3(config, vif_ipv6_base, ifname) - - -            vif_s_path = ['interfaces', type, interface, 'vif-s'] -            if config.exists(vif_s_path): -                for vif_s in config.list_nodes(vif_s_path): -                    vif_s_ip_base = vif_s_path + [vif_s, 'ip'] -                    vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] - -                    # vif-c interfaces MUST be migrated before their parent vif-s -                    # interface as the migrate_*() functions delete the path! -                    vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] -                    if config.exists(vif_c_path): -                        for vif_c in config.list_nodes(vif_c_path): -                            vif_c_ip_base = vif_c_path + [vif_c, 'ip'] -                            vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] -                            ifname = f'{interface}.{vif_s}.{vif_c}' - -                            migrate_rip(config, vif_c_ip_base, ifname) -                            migrate_ripng(config, vif_c_ipv6_base, ifname) -                            migrate_ospf(config, vif_c_ip_base, ifname) -                            migrate_ospfv3(config, vif_c_ipv6_base, ifname) - - -                    ifname = f'{interface}.{vif_s}' -                    migrate_rip(config, vif_s_ip_base, ifname) -                    migrate_ripng(config, vif_s_ipv6_base, ifname) -                    migrate_ospf(config, vif_s_ip_base, ifname) -                    migrate_ospfv3(config, vif_s_ipv6_base, ifname) +    base = ['interfaces', 'wireguard'] +    if not config.exists(base): +        # Nothing to do +        sys.exit(0) +    for interface in config.list_nodes(base): +        if not config.exists(base + [interface, 'private-key']): +            continue +        private_key = config.return_value(base + [interface, 'private-key']) +        interface_base = base + [interface] +        if config.exists(interface_base + ['peer']): +            for peer in config.list_nodes(interface_base + ['peer']): +                peer_base = interface_base + ['peer', peer] +                if not config.exists(peer_base + ['public-key']): +                    continue +                peer_public_key = config.return_value(peer_base + ['public-key']) +                if not config.exists(peer_base + ['disable']) \ +                        and is_wireguard_key_pair(private_key, peer_public_key): +                    config.set(peer_base + ['disable'])      try:          with open(file_name, 'w') as f:              f.write(config.to_string())      except OSError as e:          print("Failed to save the modified config: {}".format(e)) -        exit(1) +        sys.exit(1) diff --git a/src/migration-scripts/interfaces/23-to-24 b/src/migration-scripts/interfaces/23-to-24 index 8fd79ecc6..8b21fce51 100755 --- a/src/migration-scripts/interfaces/23-to-24 +++ b/src/migration-scripts/interfaces/23-to-24 @@ -14,47 +14,132 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported -# having a VTI interface in the CLI but no IPSec configuration - drop VTI -# configuration if this is the case for VyOS 1.4 - -import sys +from sys import argv +from sys import exit  from vyos.configtree import ConfigTree +def migrate_ospf(config, path, interface): +    path = path + ['ospf'] +    if config.exists(path): +        new_base = ['protocols', 'ospf', 'interface'] +        config.set(new_base) +        config.set_tag(new_base) +        config.copy(path, new_base + [interface]) +        config.delete(path) + +        # if "ip ospf" was the only setting, we can clean out the empty +        # ip node afterwards +        if len(config.list_nodes(path[:-1])) == 0: +            config.delete(path[:-1]) + +def migrate_ospfv3(config, path, interface): +    path = path + ['ospfv3'] +    if config.exists(path): +        new_base = ['protocols', 'ospfv3', 'interface'] +        config.set(new_base) +        config.set_tag(new_base) +        config.copy(path, new_base + [interface]) +        config.delete(path) + +        # if "ipv6 ospfv3" was the only setting, we can clean out the empty +        # ip node afterwards +        if len(config.list_nodes(path[:-1])) == 0: +            config.delete(path[:-1]) + +def migrate_rip(config, path, interface): +    path = path + ['rip'] +    if config.exists(path): +        new_base = ['protocols', 'rip', 'interface'] +        config.set(new_base) +        config.set_tag(new_base) +        config.copy(path, new_base + [interface]) +        config.delete(path) + +        # if "ip rip" was the only setting, we can clean out the empty +        # ip node afterwards +        if len(config.list_nodes(path[:-1])) == 0: +            config.delete(path[:-1]) + +def migrate_ripng(config, path, interface): +    path = path + ['ripng'] +    if config.exists(path): +        new_base = ['protocols', 'ripng', 'interface'] +        config.set(new_base) +        config.set_tag(new_base) +        config.copy(path, new_base + [interface]) +        config.delete(path) + +        # if "ipv6 ripng" was the only setting, we can clean out the empty +        # ip node afterwards +        if len(config.list_nodes(path[:-1])) == 0: +            config.delete(path[:-1]) +  if __name__ == '__main__': -    if len(sys.argv) < 2: +    if len(argv) < 2:          print("Must specify file name!") -        sys.exit(1) - -    file_name = sys.argv[1] +        exit(1) +    file_name = argv[1]      with open(file_name, 'r') as f:          config_file = f.read()      config = ConfigTree(config_file) -    base = ['interfaces', 'vti'] -    if not config.exists(base): -        # Nothing to do -        sys.exit(0) - -    ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] -    for interface in config.list_nodes(base): -        found = False -        if config.exists(ipsec_base): -            for peer in config.list_nodes(ipsec_base): -                if config.exists(ipsec_base + [peer, 'vti', 'bind']): -                    tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind']) -                    if tmp == interface: -                        # Interface was found and we no longer need to search -                        # for it in our IPSec peers -                        found = True -                        break -        if not found: -            config.delete(base + [interface]) + +    # +    # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" +    # +    for type in config.list_nodes(['interfaces']): +        for interface in config.list_nodes(['interfaces', type]): +            ip_base = ['interfaces', type, interface, 'ip'] +            ipv6_base = ['interfaces', type, interface, 'ipv6'] +            migrate_rip(config, ip_base, interface) +            migrate_ripng(config, ipv6_base, interface) +            migrate_ospf(config, ip_base, interface) +            migrate_ospfv3(config, ipv6_base, interface) + +            vif_path = ['interfaces', type, interface, 'vif'] +            if config.exists(vif_path): +                for vif in config.list_nodes(vif_path): +                    vif_ip_base = vif_path + [vif, 'ip'] +                    vif_ipv6_base = vif_path + [vif, 'ipv6'] +                    ifname = f'{interface}.{vif}' + +                    migrate_rip(config, vif_ip_base, ifname) +                    migrate_ripng(config, vif_ipv6_base, ifname) +                    migrate_ospf(config, vif_ip_base, ifname) +                    migrate_ospfv3(config, vif_ipv6_base, ifname) + + +            vif_s_path = ['interfaces', type, interface, 'vif-s'] +            if config.exists(vif_s_path): +                for vif_s in config.list_nodes(vif_s_path): +                    vif_s_ip_base = vif_s_path + [vif_s, 'ip'] +                    vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] + +                    # vif-c interfaces MUST be migrated before their parent vif-s +                    # interface as the migrate_*() functions delete the path! +                    vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] +                    if config.exists(vif_c_path): +                        for vif_c in config.list_nodes(vif_c_path): +                            vif_c_ip_base = vif_c_path + [vif_c, 'ip'] +                            vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] +                            ifname = f'{interface}.{vif_s}.{vif_c}' + +                            migrate_rip(config, vif_c_ip_base, ifname) +                            migrate_ripng(config, vif_c_ipv6_base, ifname) +                            migrate_ospf(config, vif_c_ip_base, ifname) +                            migrate_ospfv3(config, vif_c_ipv6_base, ifname) + + +                    ifname = f'{interface}.{vif_s}' +                    migrate_rip(config, vif_s_ip_base, ifname) +                    migrate_ripng(config, vif_s_ipv6_base, ifname) +                    migrate_ospf(config, vif_s_ip_base, ifname) +                    migrate_ospfv3(config, vif_s_ipv6_base, ifname)      try:          with open(file_name, 'w') as f:              f.write(config.to_string())      except OSError as e:          print("Failed to save the modified config: {}".format(e)) -        sys.exit(1) +        exit(1) diff --git a/src/migration-scripts/interfaces/24-to-25 b/src/migration-scripts/interfaces/24-to-25 index 9aa6ea5e3..8fd79ecc6 100755 --- a/src/migration-scripts/interfaces/24-to-25 +++ b/src/migration-scripts/interfaces/24-to-25 @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -14,374 +14,47 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -# Migrate Wireguard to store keys in CLI -# Migrate EAPoL to PKI configuration +# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported +# having a VTI interface in the CLI but no IPSec configuration - drop VTI +# configuration if this is the case for VyOS 1.4 -import os  import sys -  from vyos.configtree import ConfigTree -from vyos.pki import CERT_BEGIN -from vyos.pki import load_certificate -from vyos.pki import load_crl -from vyos.pki import load_dh_parameters -from vyos.pki import load_private_key -from vyos.pki import encode_certificate -from vyos.pki import encode_dh_parameters -from vyos.pki import encode_private_key -from vyos.pki import verify_crl -from vyos.utils.process import run - -def wrapped_pem_to_config_value(pem): -    out = [] -    for line in pem.strip().split("\n"): -        if not line or line.startswith("-----") or line[0] == '#': -            continue -        out.append(line) -    return "".join(out) - -def read_file_for_pki(config_auth_path): -    full_path = os.path.join(AUTH_DIR, config_auth_path) -    output = None - -    if os.path.isfile(full_path): -        if not os.access(full_path, os.R_OK): -            run(f'sudo chmod 644 {full_path}') - -        with open(full_path, 'r') as f: -            output = f.read() - -    return output -if len(sys.argv) < 2: -    print("Must specify file name!") -    sys.exit(1) +if __name__ == '__main__': +    if len(sys.argv) < 2: +        print("Must specify file name!") +        sys.exit(1) -file_name = sys.argv[1] +    file_name = sys.argv[1] -with open(file_name, 'r') as f: -    config_file = f.read() +    with open(file_name, 'r') as f: +        config_file = f.read() -config = ConfigTree(config_file) +    config = ConfigTree(config_file) +    base = ['interfaces', 'vti'] +    if not config.exists(base): +        # Nothing to do +        sys.exit(0) -AUTH_DIR = '/config/auth' -pki_base = ['pki'] - -# OpenVPN -base = ['interfaces', 'openvpn'] - -if config.exists(base): +    ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer']      for interface in config.list_nodes(base): -        x509_base = base + [interface, 'tls'] -        pki_name = f'openvpn_{interface}' - -        if config.exists(base + [interface, 'shared-secret-key-file']): -            if not config.exists(pki_base + ['openvpn', 'shared-secret']): -                config.set(pki_base + ['openvpn', 'shared-secret']) -                config.set_tag(pki_base + ['openvpn', 'shared-secret']) - -            key_file = config.return_value(base + [interface, 'shared-secret-key-file']) -            key = read_file_for_pki(key_file) -            key_pki_name = f'{pki_name}_shared' - -            if key: -                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) -                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') -                config.set(base + [interface, 'shared-secret-key'], value=key_pki_name) -            else: -                print(f'Failed to migrate shared-secret-key on openvpn interface {interface}') - -            config.delete(base + [interface, 'shared-secret-key-file']) - -        if not config.exists(base + [interface, 'tls']): -            continue - -        if config.exists(base + [interface, 'tls', 'auth-file']): -            if not config.exists(pki_base + ['openvpn', 'shared-secret']): -                config.set(pki_base + ['openvpn', 'shared-secret']) -                config.set_tag(pki_base + ['openvpn', 'shared-secret']) - -            key_file = config.return_value(base + [interface, 'tls', 'auth-file']) -            key = read_file_for_pki(key_file) -            key_pki_name = f'{pki_name}_auth' - -            if key: -                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) -                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') -                config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name) -            else: -                print(f'Failed to migrate auth-key on openvpn interface {interface}') - -            config.delete(base + [interface, 'tls', 'auth-file']) - -        if config.exists(base + [interface, 'tls', 'crypt-file']): -            if not config.exists(pki_base + ['openvpn', 'shared-secret']): -                config.set(pki_base + ['openvpn', 'shared-secret']) -                config.set_tag(pki_base + ['openvpn', 'shared-secret']) - -            key_file = config.return_value(base + [interface, 'tls', 'crypt-file']) -            key = read_file_for_pki(key_file) -            key_pki_name = f'{pki_name}_crypt' - -            if key: -                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) -                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') -                config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name) -            else: -                print(f'Failed to migrate crypt-key on openvpn interface {interface}') - -            config.delete(base + [interface, 'tls', 'crypt-file']) - -        ca_certs = {} - -        if config.exists(x509_base + ['ca-cert-file']): -            if not config.exists(pki_base + ['ca']): -                config.set(pki_base + ['ca']) -                config.set_tag(pki_base + ['ca']) - -            cert_file = config.return_value(x509_base + ['ca-cert-file']) -            cert_path = os.path.join(AUTH_DIR, cert_file) - -            if os.path.isfile(cert_path): -                if not os.access(cert_path, os.R_OK): -                    run(f'sudo chmod 644 {cert_path}') - -                with open(cert_path, 'r') as f: -                    certs_str = f.read() -                    certs_data = certs_str.split(CERT_BEGIN) -                    index = 1 -                    for cert_data in certs_data[1:]: -                        cert = load_certificate(CERT_BEGIN + cert_data, wrap_tags=False) - -                        if cert: -                            ca_certs[f'{pki_name}_{index}'] = cert -                            cert_pem = encode_certificate(cert) -                            config.set(pki_base + ['ca', f'{pki_name}_{index}', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) -                            config.set(x509_base + ['ca-certificate'], value=f'{pki_name}_{index}', replace=False) -                        else: -                            print(f'Failed to migrate CA certificate on openvpn interface {interface}') - -                        index += 1 -            else: -                print(f'Failed to migrate CA certificate on openvpn interface {interface}') - -            config.delete(x509_base + ['ca-cert-file']) - -        if config.exists(x509_base + ['crl-file']): -            if not config.exists(pki_base + ['ca']): -                config.set(pki_base + ['ca']) -                config.set_tag(pki_base + ['ca']) - -            crl_file = config.return_value(x509_base + ['crl-file']) -            crl_path = os.path.join(AUTH_DIR, crl_file) -            crl = None -            crl_ca_name = None - -            if os.path.isfile(crl_path): -                if not os.access(crl_path, os.R_OK): -                    run(f'sudo chmod 644 {crl_path}') - -                with open(crl_path, 'r') as f: -                    crl_data = f.read() -                    crl = load_crl(crl_data, wrap_tags=False) - -                    for ca_name, ca_cert in ca_certs.items(): -                        if verify_crl(crl, ca_cert): -                            crl_ca_name = ca_name -                            break - -            if crl and crl_ca_name: -                crl_pem = encode_certificate(crl) -                config.set(pki_base + ['ca', crl_ca_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) -            else: -                print(f'Failed to migrate CRL on openvpn interface {interface}') - -            config.delete(x509_base + ['crl-file']) - -        if config.exists(x509_base + ['cert-file']): -            if not config.exists(pki_base + ['certificate']): -                config.set(pki_base + ['certificate']) -                config.set_tag(pki_base + ['certificate']) - -            cert_file = config.return_value(x509_base + ['cert-file']) -            cert_path = os.path.join(AUTH_DIR, cert_file) -            cert = None - -            if os.path.isfile(cert_path): -                if not os.access(cert_path, os.R_OK): -                    run(f'sudo chmod 644 {cert_path}') - -                with open(cert_path, 'r') as f: -                    cert_data = f.read() -                    cert = load_certificate(cert_data, wrap_tags=False) - -            if cert: -                cert_pem = encode_certificate(cert) -                config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) -                config.set(x509_base + ['certificate'], value=pki_name) -            else: -                print(f'Failed to migrate certificate on openvpn interface {interface}') - -            config.delete(x509_base + ['cert-file']) - -        if config.exists(x509_base + ['key-file']): -            key_file = config.return_value(x509_base + ['key-file']) -            key_path = os.path.join(AUTH_DIR, key_file) -            key = None - -            if os.path.isfile(key_path): -                if not os.access(key_path, os.R_OK): -                    run(f'sudo chmod 644 {key_path}') - -                with open(key_path, 'r') as f: -                    key_data = f.read() -                    key = load_private_key(key_data, passphrase=None, wrap_tags=False) - -            if key: -                key_pem = encode_private_key(key, passphrase=None) -                config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) -            else: -                print(f'Failed to migrate private key on openvpn interface {interface}') - -            config.delete(x509_base + ['key-file']) - -        if config.exists(x509_base + ['dh-file']): -            if not config.exists(pki_base + ['dh']): -                config.set(pki_base + ['dh']) -                config.set_tag(pki_base + ['dh']) - -            dh_file = config.return_value(x509_base + ['dh-file']) -            dh_path = os.path.join(AUTH_DIR, dh_file) -            dh = None - -            if os.path.isfile(dh_path): -                if not os.access(dh_path, os.R_OK): -                    run(f'sudo chmod 644 {dh_path}') - -                with open(dh_path, 'r') as f: -                    dh_data = f.read() -                    dh = load_dh_parameters(dh_data, wrap_tags=False) - -            if dh: -                dh_pem = encode_dh_parameters(dh) -                config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem)) -                config.set(x509_base + ['dh-params'], value=pki_name) -            else: -                print(f'Failed to migrate DH parameters on openvpn interface {interface}') - -            config.delete(x509_base + ['dh-file']) - -# Wireguard -base = ['interfaces', 'wireguard'] - -if config.exists(base): -    for interface in config.list_nodes(base): -        private_key_path = base + [interface, 'private-key'] - -        key_file = 'default' -        if config.exists(private_key_path): -            key_file = config.return_value(private_key_path) - -        full_key_path = f'/config/auth/wireguard/{key_file}/private.key' - -        if not os.path.exists(full_key_path): -            print(f'Could not find wireguard private key for migration on interface "{interface}"') -            continue - -        with open(full_key_path, 'r') as f: -            key_data = f.read().strip() -            config.set(private_key_path, value=key_data) - -        for peer in config.list_nodes(base + [interface, 'peer']): -            config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key') - -# Ethernet EAPoL -base = ['interfaces', 'ethernet'] - -if config.exists(base): -    for interface in config.list_nodes(base): -        if not config.exists(base + [interface, 'eapol']): -            continue - -        x509_base = base + [interface, 'eapol'] -        pki_name = f'eapol_{interface}' - -        if config.exists(x509_base + ['ca-cert-file']): -            if not config.exists(pki_base + ['ca']): -                config.set(pki_base + ['ca']) -                config.set_tag(pki_base + ['ca']) - -            cert_file = config.return_value(x509_base + ['ca-cert-file']) -            cert_path = os.path.join(AUTH_DIR, cert_file) -            cert = None - -            if os.path.isfile(cert_path): -                if not os.access(cert_path, os.R_OK): -                    run(f'sudo chmod 644 {cert_path}') - -                with open(cert_path, 'r') as f: -                    cert_data = f.read() -                    cert = load_certificate(cert_data, wrap_tags=False) - -            if cert: -                cert_pem = encode_certificate(cert) -                config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) -                config.set(x509_base + ['ca-certificate'], value=pki_name) -            else: -                print(f'Failed to migrate CA certificate on eapol config for interface {interface}') - -            config.delete(x509_base + ['ca-cert-file']) - -        if config.exists(x509_base + ['cert-file']): -            if not config.exists(pki_base + ['certificate']): -                config.set(pki_base + ['certificate']) -                config.set_tag(pki_base + ['certificate']) - -            cert_file = config.return_value(x509_base + ['cert-file']) -            cert_path = os.path.join(AUTH_DIR, cert_file) -            cert = None - -            if os.path.isfile(cert_path): -                if not os.access(cert_path, os.R_OK): -                    run(f'sudo chmod 644 {cert_path}') - -                with open(cert_path, 'r') as f: -                    cert_data = f.read() -                    cert = load_certificate(cert_data, wrap_tags=False) - -            if cert: -                cert_pem = encode_certificate(cert) -                config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) -                config.set(x509_base + ['certificate'], value=pki_name) -            else: -                print(f'Failed to migrate certificate on eapol config for interface {interface}') - -            config.delete(x509_base + ['cert-file']) - -        if config.exists(x509_base + ['key-file']): -            key_file = config.return_value(x509_base + ['key-file']) -            key_path = os.path.join(AUTH_DIR, key_file) -            key = None - -            if os.path.isfile(key_path): -                if not os.access(key_path, os.R_OK): -                    run(f'sudo chmod 644 {key_path}') - -                with open(key_path, 'r') as f: -                    key_data = f.read() -                    key = load_private_key(key_data, passphrase=None, wrap_tags=False) - -            if key: -                key_pem = encode_private_key(key, passphrase=None) -                config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) -            else: -                print(f'Failed to migrate private key on eapol config for interface {interface}') - -            config.delete(x509_base + ['key-file']) - -try: -    with open(file_name, 'w') as f: -        f.write(config.to_string()) -except OSError as e: -    print("Failed to save the modified config: {}".format(e)) -    sys.exit(1) +        found = False +        if config.exists(ipsec_base): +            for peer in config.list_nodes(ipsec_base): +                if config.exists(ipsec_base + [peer, 'vti', 'bind']): +                    tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind']) +                    if tmp == interface: +                        # Interface was found and we no longer need to search +                        # for it in our IPSec peers +                        found = True +                        break +        if not found: +            config.delete(base + [interface]) + +    try: +        with open(file_name, 'w') as f: +            f.write(config.to_string()) +    except OSError as e: +        print("Failed to save the modified config: {}".format(e)) +        sys.exit(1) diff --git a/src/migration-scripts/interfaces/25-to-26 b/src/migration-scripts/interfaces/25-to-26 index 4967a29fa..9aa6ea5e3 100755 --- a/src/migration-scripts/interfaces/25-to-26 +++ b/src/migration-scripts/interfaces/25-to-26 @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -14,41 +14,374 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -# T4384: pppoe: replace default-route CLI option with common CLI nodes already -#        present for DHCP +# Migrate Wireguard to store keys in CLI +# Migrate EAPoL to PKI configuration -from sys import argv +import os +import sys -from vyos.ethtool import Ethtool  from vyos.configtree import ConfigTree +from vyos.pki import CERT_BEGIN +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_dh_parameters +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_dh_parameters +from vyos.pki import encode_private_key +from vyos.pki import verify_crl +from vyos.utils.process import run -if len(argv) < 2: +def wrapped_pem_to_config_value(pem): +    out = [] +    for line in pem.strip().split("\n"): +        if not line or line.startswith("-----") or line[0] == '#': +            continue +        out.append(line) +    return "".join(out) + +def read_file_for_pki(config_auth_path): +    full_path = os.path.join(AUTH_DIR, config_auth_path) +    output = None + +    if os.path.isfile(full_path): +        if not os.access(full_path, os.R_OK): +            run(f'sudo chmod 644 {full_path}') + +        with open(full_path, 'r') as f: +            output = f.read() + +    return output + +if len(sys.argv) < 2:      print("Must specify file name!") -    exit(1) +    sys.exit(1) + +file_name = sys.argv[1] -file_name = argv[1]  with open(file_name, 'r') as f:      config_file = f.read() -base = ['interfaces', 'pppoe']  config = ConfigTree(config_file) -if not config.exists(base): -    exit(0) +AUTH_DIR = '/config/auth' +pki_base = ['pki'] + +# OpenVPN +base = ['interfaces', 'openvpn'] + +if config.exists(base): +    for interface in config.list_nodes(base): +        x509_base = base + [interface, 'tls'] +        pki_name = f'openvpn_{interface}' + +        if config.exists(base + [interface, 'shared-secret-key-file']): +            if not config.exists(pki_base + ['openvpn', 'shared-secret']): +                config.set(pki_base + ['openvpn', 'shared-secret']) +                config.set_tag(pki_base + ['openvpn', 'shared-secret']) + +            key_file = config.return_value(base + [interface, 'shared-secret-key-file']) +            key = read_file_for_pki(key_file) +            key_pki_name = f'{pki_name}_shared' + +            if key: +                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) +                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') +                config.set(base + [interface, 'shared-secret-key'], value=key_pki_name) +            else: +                print(f'Failed to migrate shared-secret-key on openvpn interface {interface}') + +            config.delete(base + [interface, 'shared-secret-key-file']) + +        if not config.exists(base + [interface, 'tls']): +            continue + +        if config.exists(base + [interface, 'tls', 'auth-file']): +            if not config.exists(pki_base + ['openvpn', 'shared-secret']): +                config.set(pki_base + ['openvpn', 'shared-secret']) +                config.set_tag(pki_base + ['openvpn', 'shared-secret']) + +            key_file = config.return_value(base + [interface, 'tls', 'auth-file']) +            key = read_file_for_pki(key_file) +            key_pki_name = f'{pki_name}_auth' + +            if key: +                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) +                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') +                config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name) +            else: +                print(f'Failed to migrate auth-key on openvpn interface {interface}') + +            config.delete(base + [interface, 'tls', 'auth-file']) + +        if config.exists(base + [interface, 'tls', 'crypt-file']): +            if not config.exists(pki_base + ['openvpn', 'shared-secret']): +                config.set(pki_base + ['openvpn', 'shared-secret']) +                config.set_tag(pki_base + ['openvpn', 'shared-secret']) + +            key_file = config.return_value(base + [interface, 'tls', 'crypt-file']) +            key = read_file_for_pki(key_file) +            key_pki_name = f'{pki_name}_crypt' + +            if key: +                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) +                config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') +                config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name) +            else: +                print(f'Failed to migrate crypt-key on openvpn interface {interface}') + +            config.delete(base + [interface, 'tls', 'crypt-file']) + +        ca_certs = {} + +        if config.exists(x509_base + ['ca-cert-file']): +            if not config.exists(pki_base + ['ca']): +                config.set(pki_base + ['ca']) +                config.set_tag(pki_base + ['ca']) + +            cert_file = config.return_value(x509_base + ['ca-cert-file']) +            cert_path = os.path.join(AUTH_DIR, cert_file) + +            if os.path.isfile(cert_path): +                if not os.access(cert_path, os.R_OK): +                    run(f'sudo chmod 644 {cert_path}') + +                with open(cert_path, 'r') as f: +                    certs_str = f.read() +                    certs_data = certs_str.split(CERT_BEGIN) +                    index = 1 +                    for cert_data in certs_data[1:]: +                        cert = load_certificate(CERT_BEGIN + cert_data, wrap_tags=False) + +                        if cert: +                            ca_certs[f'{pki_name}_{index}'] = cert +                            cert_pem = encode_certificate(cert) +                            config.set(pki_base + ['ca', f'{pki_name}_{index}', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) +                            config.set(x509_base + ['ca-certificate'], value=f'{pki_name}_{index}', replace=False) +                        else: +                            print(f'Failed to migrate CA certificate on openvpn interface {interface}') + +                        index += 1 +            else: +                print(f'Failed to migrate CA certificate on openvpn interface {interface}') + +            config.delete(x509_base + ['ca-cert-file']) + +        if config.exists(x509_base + ['crl-file']): +            if not config.exists(pki_base + ['ca']): +                config.set(pki_base + ['ca']) +                config.set_tag(pki_base + ['ca']) + +            crl_file = config.return_value(x509_base + ['crl-file']) +            crl_path = os.path.join(AUTH_DIR, crl_file) +            crl = None +            crl_ca_name = None + +            if os.path.isfile(crl_path): +                if not os.access(crl_path, os.R_OK): +                    run(f'sudo chmod 644 {crl_path}') + +                with open(crl_path, 'r') as f: +                    crl_data = f.read() +                    crl = load_crl(crl_data, wrap_tags=False) + +                    for ca_name, ca_cert in ca_certs.items(): +                        if verify_crl(crl, ca_cert): +                            crl_ca_name = ca_name +                            break + +            if crl and crl_ca_name: +                crl_pem = encode_certificate(crl) +                config.set(pki_base + ['ca', crl_ca_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) +            else: +                print(f'Failed to migrate CRL on openvpn interface {interface}') + +            config.delete(x509_base + ['crl-file']) + +        if config.exists(x509_base + ['cert-file']): +            if not config.exists(pki_base + ['certificate']): +                config.set(pki_base + ['certificate']) +                config.set_tag(pki_base + ['certificate']) + +            cert_file = config.return_value(x509_base + ['cert-file']) +            cert_path = os.path.join(AUTH_DIR, cert_file) +            cert = None + +            if os.path.isfile(cert_path): +                if not os.access(cert_path, os.R_OK): +                    run(f'sudo chmod 644 {cert_path}') + +                with open(cert_path, 'r') as f: +                    cert_data = f.read() +                    cert = load_certificate(cert_data, wrap_tags=False) + +            if cert: +                cert_pem = encode_certificate(cert) +                config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) +                config.set(x509_base + ['certificate'], value=pki_name) +            else: +                print(f'Failed to migrate certificate on openvpn interface {interface}') + +            config.delete(x509_base + ['cert-file']) + +        if config.exists(x509_base + ['key-file']): +            key_file = config.return_value(x509_base + ['key-file']) +            key_path = os.path.join(AUTH_DIR, key_file) +            key = None + +            if os.path.isfile(key_path): +                if not os.access(key_path, os.R_OK): +                    run(f'sudo chmod 644 {key_path}') + +                with open(key_path, 'r') as f: +                    key_data = f.read() +                    key = load_private_key(key_data, passphrase=None, wrap_tags=False) + +            if key: +                key_pem = encode_private_key(key, passphrase=None) +                config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) +            else: +                print(f'Failed to migrate private key on openvpn interface {interface}') + +            config.delete(x509_base + ['key-file']) + +        if config.exists(x509_base + ['dh-file']): +            if not config.exists(pki_base + ['dh']): +                config.set(pki_base + ['dh']) +                config.set_tag(pki_base + ['dh']) + +            dh_file = config.return_value(x509_base + ['dh-file']) +            dh_path = os.path.join(AUTH_DIR, dh_file) +            dh = None + +            if os.path.isfile(dh_path): +                if not os.access(dh_path, os.R_OK): +                    run(f'sudo chmod 644 {dh_path}') + +                with open(dh_path, 'r') as f: +                    dh_data = f.read() +                    dh = load_dh_parameters(dh_data, wrap_tags=False) + +            if dh: +                dh_pem = encode_dh_parameters(dh) +                config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem)) +                config.set(x509_base + ['dh-params'], value=pki_name) +            else: +                print(f'Failed to migrate DH parameters on openvpn interface {interface}') + +            config.delete(x509_base + ['dh-file']) + +# Wireguard +base = ['interfaces', 'wireguard'] + +if config.exists(base): +    for interface in config.list_nodes(base): +        private_key_path = base + [interface, 'private-key'] + +        key_file = 'default' +        if config.exists(private_key_path): +            key_file = config.return_value(private_key_path) + +        full_key_path = f'/config/auth/wireguard/{key_file}/private.key' + +        if not os.path.exists(full_key_path): +            print(f'Could not find wireguard private key for migration on interface "{interface}"') +            continue + +        with open(full_key_path, 'r') as f: +            key_data = f.read().strip() +            config.set(private_key_path, value=key_data) + +        for peer in config.list_nodes(base + [interface, 'peer']): +            config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key') + +# Ethernet EAPoL +base = ['interfaces', 'ethernet'] + +if config.exists(base): +    for interface in config.list_nodes(base): +        if not config.exists(base + [interface, 'eapol']): +            continue + +        x509_base = base + [interface, 'eapol'] +        pki_name = f'eapol_{interface}' + +        if config.exists(x509_base + ['ca-cert-file']): +            if not config.exists(pki_base + ['ca']): +                config.set(pki_base + ['ca']) +                config.set_tag(pki_base + ['ca']) + +            cert_file = config.return_value(x509_base + ['ca-cert-file']) +            cert_path = os.path.join(AUTH_DIR, cert_file) +            cert = None + +            if os.path.isfile(cert_path): +                if not os.access(cert_path, os.R_OK): +                    run(f'sudo chmod 644 {cert_path}') + +                with open(cert_path, 'r') as f: +                    cert_data = f.read() +                    cert = load_certificate(cert_data, wrap_tags=False) + +            if cert: +                cert_pem = encode_certificate(cert) +                config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) +                config.set(x509_base + ['ca-certificate'], value=pki_name) +            else: +                print(f'Failed to migrate CA certificate on eapol config for interface {interface}') + +            config.delete(x509_base + ['ca-cert-file']) + +        if config.exists(x509_base + ['cert-file']): +            if not config.exists(pki_base + ['certificate']): +                config.set(pki_base + ['certificate']) +                config.set_tag(pki_base + ['certificate']) + +            cert_file = config.return_value(x509_base + ['cert-file']) +            cert_path = os.path.join(AUTH_DIR, cert_file) +            cert = None + +            if os.path.isfile(cert_path): +                if not os.access(cert_path, os.R_OK): +                    run(f'sudo chmod 644 {cert_path}') + +                with open(cert_path, 'r') as f: +                    cert_data = f.read() +                    cert = load_certificate(cert_data, wrap_tags=False) + +            if cert: +                cert_pem = encode_certificate(cert) +                config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) +                config.set(x509_base + ['certificate'], value=pki_name) +            else: +                print(f'Failed to migrate certificate on eapol config for interface {interface}') + +            config.delete(x509_base + ['cert-file']) + +        if config.exists(x509_base + ['key-file']): +            key_file = config.return_value(x509_base + ['key-file']) +            key_path = os.path.join(AUTH_DIR, key_file) +            key = None + +            if os.path.isfile(key_path): +                if not os.access(key_path, os.R_OK): +                    run(f'sudo chmod 644 {key_path}') + +                with open(key_path, 'r') as f: +                    key_data = f.read() +                    key = load_private_key(key_data, passphrase=None, wrap_tags=False) + +            if key: +                key_pem = encode_private_key(key, passphrase=None) +                config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) +            else: +                print(f'Failed to migrate private key on eapol config for interface {interface}') -for ifname in config.list_nodes(base): -    tmp_config = base + [ifname, 'default-route'] -    if config.exists(tmp_config): -        # Retrieve current config value -        value = config.return_value(tmp_config) -        # Delete old Config node -        config.delete(tmp_config) -        if value == 'none': -            config.set(base + [ifname, 'no-default-route']) +            config.delete(x509_base + ['key-file'])  try:      with open(file_name, 'w') as f:          f.write(config.to_string())  except OSError as e: -    print(f'Failed to save the modified config: {e}') -    exit(1) +    print("Failed to save the modified config: {}".format(e)) +    sys.exit(1) diff --git a/src/migration-scripts/interfaces/26-to-27 b/src/migration-scripts/interfaces/26-to-27 index a0d043d11..4967a29fa 100755 --- a/src/migration-scripts/interfaces/26-to-27 +++ b/src/migration-scripts/interfaces/26-to-27 @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,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/>. -# T4995: pppoe, wwan, sstpc-client rename "authentication user" CLI node -#        to "authentication username" +# T4384: pppoe: replace default-route CLI option with common CLI nodes already +#        present for DHCP  from sys import argv @@ -30,16 +30,21 @@ file_name = argv[1]  with open(file_name, 'r') as f:      config_file = f.read() +base = ['interfaces', 'pppoe']  config = ConfigTree(config_file) -for type in ['pppoe', 'sstpc-client', 'wwam']: -    base = ['interfaces', type] -    if not config.exists(base): -        continue -    for interface in config.list_nodes(base): -        auth_base = base + [interface, 'authentication', 'user'] -        if config.exists(auth_base): -            config.rename(auth_base, 'username') +if not config.exists(base): +    exit(0) + +for ifname in config.list_nodes(base): +    tmp_config = base + [ifname, 'default-route'] +    if config.exists(tmp_config): +        # Retrieve current config value +        value = config.return_value(tmp_config) +        # Delete old Config node +        config.delete(tmp_config) +        if value == 'none': +            config.set(base + [ifname, 'no-default-route'])  try:      with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/27-to-28 b/src/migration-scripts/interfaces/27-to-28 index ad5bfa653..a0d043d11 100755 --- a/src/migration-scripts/interfaces/27-to-28 +++ b/src/migration-scripts/interfaces/27-to-28 @@ -14,8 +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/>. -# T5034: tunnel: rename "multicast enable" CLI node to "enable-multicast" -#        valueless node. +# T4995: pppoe, wwan, sstpc-client rename "authentication user" CLI node +#        to "authentication username"  from sys import argv @@ -30,21 +30,16 @@ file_name = argv[1]  with open(file_name, 'r') as f:      config_file = f.read() -base = ['interfaces', 'tunnel']  config = ConfigTree(config_file) -if not config.exists(base): -    exit(0) - -for ifname in config.list_nodes(base): -    multicast_base = base + [ifname, 'multicast'] -    if config.exists(multicast_base): -        tmp = config.return_value(multicast_base) -        print(tmp) -        # Delete old Config node -        config.delete(multicast_base) -        if tmp == 'enable': -            config.set(base + [ifname, 'enable-multicast']) +for type in ['pppoe', 'sstpc-client', 'wwam']: +    base = ['interfaces', type] +    if not config.exists(base): +        continue +    for interface in config.list_nodes(base): +        auth_base = base + [interface, 'authentication', 'user'] +        if config.exists(auth_base): +            config.rename(auth_base, 'username')  try:      with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/28-to-29 b/src/migration-scripts/interfaces/28-to-29 index acb6ee1fb..ad5bfa653 100755 --- a/src/migration-scripts/interfaces/28-to-29 +++ b/src/migration-scripts/interfaces/28-to-29 @@ -14,7 +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/>. -# T5286: remove XDP support in favour of VPP +# T5034: tunnel: rename "multicast enable" CLI node to "enable-multicast" +#        valueless node.  from sys import argv @@ -29,17 +30,21 @@ file_name = argv[1]  with open(file_name, 'r') as f:      config_file = f.read() -supports_xdp = ['bonding', 'ethernet'] +base = ['interfaces', 'tunnel']  config = ConfigTree(config_file) -for if_type in supports_xdp: -    base = ['interfaces', if_type] -    if not config.exists(base): -        continue -    for interface in config.list_nodes(base): -        if_base = base + [interface] -        if config.exists(if_base + ['xdp']): -            config.delete(if_base + ['xdp']) +if not config.exists(base): +    exit(0) + +for ifname in config.list_nodes(base): +    multicast_base = base + [ifname, 'multicast'] +    if config.exists(multicast_base): +        tmp = config.return_value(multicast_base) +        print(tmp) +        # Delete old Config node +        config.delete(multicast_base) +        if tmp == 'enable': +            config.set(base + [ifname, 'enable-multicast'])  try:      with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/29-to-30 b/src/migration-scripts/interfaces/29-to-30 index 97e1b329c..acb6ee1fb 100755 --- a/src/migration-scripts/interfaces/29-to-30 +++ b/src/migration-scripts/interfaces/29-to-30 @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 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 @@ -13,42 +13,37 @@  #  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -# -# Deletes Wireguard peers if they have the same public key as the router has. -import sys + +# T5286: remove XDP support in favour of VPP + +from sys import argv + +from vyos.ethtool import Ethtool  from vyos.configtree import ConfigTree -from vyos.utils.network import is_wireguard_key_pair -if __name__ == '__main__': -    if len(sys.argv) < 2: -        print("Must specify file name!") -        sys.exit(1) +if len(argv) < 2: +    print("Must specify file name!") +    exit(1) -    file_name = sys.argv[1] +file_name = argv[1] +with open(file_name, 'r') as f: +    config_file = f.read() -    with open(file_name, 'r') as f: -        config_file = f.read() +supports_xdp = ['bonding', 'ethernet'] +config = ConfigTree(config_file) -    config = ConfigTree(config_file) -    base = ['interfaces', 'wireguard'] +for if_type in supports_xdp: +    base = ['interfaces', if_type]      if not config.exists(base): -        # Nothing to do -        sys.exit(0) +        continue      for interface in config.list_nodes(base): -        private_key = config.return_value(base + [interface, 'private-key']) -        interface_base = base + [interface] -        if config.exists(interface_base + ['peer']): -            for peer in config.list_nodes(interface_base + ['peer']): -                peer_base = interface_base + ['peer', peer] -                peer_public_key = config.return_value(peer_base + ['public-key']) -                if config.exists(peer_base + ['public-key']): -                    if not config.exists(peer_base + ['disable']) \ -                            and is_wireguard_key_pair(private_key, peer_public_key): -                        config.set(peer_base + ['disable']) - -    try: -        with open(file_name, 'w') as f: -            f.write(config.to_string()) -    except OSError as e: -        print("Failed to save the modified config: {}".format(e)) -        sys.exit(1) +        if_base = base + [interface] +        if config.exists(if_base + ['xdp']): +            config.delete(if_base + ['xdp']) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print(f'Failed to save the modified config: {e}') +    exit(1) diff --git a/src/migration-scripts/nat/6-to-7 b/src/migration-scripts/nat/6-to-7 index b5f6328ef..a2e735394 100755 --- a/src/migration-scripts/nat/6-to-7 +++ b/src/migration-scripts/nat/6-to-7 @@ -21,6 +21,7 @@  # to  #   'set nat [source|destination] rule X [inbound-interface|outbound interface] name <iface>'  #   'set nat [source|destination] rule X [inbound-interface|outbound interface] group <iface_group>' +# Also remove command if interface == any  from sys import argv,exit  from vyos.configtree import ConfigTree @@ -56,8 +57,11 @@ for direction in ['source', 'destination']:              if config.exists(base + [iface]):                  if config.exists(base + [iface, 'interface-name']):                      tmp = config.return_value(base + [iface, 'interface-name']) -                    config.delete(base + [iface, 'interface-name']) -                    config.set(base + [iface, 'name'], value=tmp) +                    if tmp != 'any': +                        config.delete(base + [iface, 'interface-name']) +                        config.set(base + [iface, 'name'], value=tmp) +                    else: +                        config.delete(base + [iface])  try:      with open(file_name, 'w') as f: diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py index f372d3af0..2c95a2b08 100755 --- a/src/op_mode/clear_dhcp_lease.py +++ b/src/op_mode/clear_dhcp_lease.py @@ -1,20 +1,34 @@  #!/usr/bin/env python3 +# +# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>.  import argparse  import re -from isc_dhcp_leases import Lease -from isc_dhcp_leases import IscDhcpLeases -  from vyos.configquery import ConfigTreeQuery +from vyos.kea import kea_parse_leases  from vyos.utils.io import ask_yes_no  from vyos.utils.process import call  from vyos.utils.commit import commit_in_progress +# TODO: Update to use Kea control socket command "lease4-del"  config = ConfigTreeQuery()  base = ['service', 'dhcp-server'] -lease_file = '/config/dhcpd.leases' +lease_file = '/config/dhcp4.leases'  def del_lease_ip(address): @@ -25,8 +39,7 @@ def del_lease_ip(address):      """      with open(lease_file, encoding='utf-8') as f:          data = f.read().rstrip() -        lease_config_ip = '{(?P<config>[\s\S]+?)\n}' -        pattern = rf"lease {address} {lease_config_ip}" +        pattern = rf"^{address},[^\n]+\n"          # Delete lease for ip block          data = re.sub(pattern, '', data) @@ -38,15 +51,13 @@ def is_ip_in_leases(address):      """      Return True if address found in the lease file      """ -    leases = IscDhcpLeases(lease_file) +    leases = kea_parse_leases(lease_file)      lease_ips = [] -    for lease in leases.get(): -        lease_ips.append(lease.ip) -    if address not in lease_ips: -        print(f'Address "{address}" not found in "{lease_file}"') -        return False -    return True - +    for lease in leases: +        if address == lease['address']: +            return True +    print(f'Address "{address}" not found in "{lease_file}"') +    return False  if not config.exists(base):      print('DHCP-server not configured!') @@ -75,4 +86,4 @@ if __name__ == '__main__':          exit(1)      else:          del_lease_ip(address) -        call('systemctl restart isc-dhcp-server.service') +        call('systemctl restart kea-dhcp4-server.service') diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index d6b8aa0b8..a9271ea79 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -21,7 +21,6 @@ import typing  from datetime import datetime  from glob import glob  from ipaddress import ip_address -from isc_dhcp_leases import IscDhcpLeases  from tabulate import tabulate  import vyos.opmode @@ -29,6 +28,9 @@ import vyos.opmode  from vyos.base import Warning  from vyos.configquery import ConfigTreeQuery +from vyos.kea import kea_get_active_config +from vyos.kea import kea_get_pool_from_subnet_id +from vyos.kea import kea_parse_leases  from vyos.utils.dict import dict_search  from vyos.utils.file import read_file  from vyos.utils.process import cmd @@ -77,67 +79,62 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig      Get DHCP server leases      :return list      """ -    lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases' +    lease_file = '/config/dhcp6.leases' if family == 'inet6' else '/config/dhcp4.leases'      data = [] -    leases = IscDhcpLeases(lease_file).get() +    leases = kea_parse_leases(lease_file)      if pool is None:          pool = _get_dhcp_pools(family=family) -        aux = False      else:          pool = [pool] -        aux = True - -    ## Search leases for every pool -    for pool_name in pool: -        for lease in leases: -            if lease.sets.get('shared-networkname', '') == pool_name or lease.sets.get('shared-networkname', '') == '': -            #if lease.sets.get('shared-networkname', '') == pool_name: -                data_lease = {} -                data_lease['ip'] = lease.ip -                data_lease['state'] = lease.binding_state -                #data_lease['pool'] = pool_name if lease.sets.get('shared-networkname', '') != '' else 'Fail-Over Server' -                data_lease['pool'] = lease.sets.get('shared-networkname', '') -                data_lease['end'] = lease.end.timestamp() if lease.end else None -                data_lease['origin'] = 'local' if data_lease['pool'] != '' else 'remote' - -                if family == 'inet': -                    data_lease['mac'] = lease.ethernet -                    data_lease['start'] = lease.start.timestamp() -                    data_lease['hostname'] = lease.hostname - -                if family == 'inet6': -                    data_lease['last_communication'] = lease.last_communication.timestamp() -                    data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string) -                    lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'} -                    data_lease['type'] = lease_types_long[lease.type] - -                data_lease['remaining'] = '-' - -                if lease.end: -                    data_lease['remaining'] = lease.end - datetime.utcnow() - -                    if data_lease['remaining'].days >= 0: -                        # substraction gives us a timedelta object which can't be formatted with strftime -                        # so we use str(), split gets rid of the microseconds -                        data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] - -                # Do not add old leases -                if data_lease['remaining'] != '' and data_lease['state'] != 'free': -                    if not state or data_lease['state'] in state or state == 'all': -                        if not origin or data_lease['origin'] in origin: -                            if not aux or (aux and data_lease['pool'] == pool_name): -                                data.append(data_lease) - -                # deduplicate -                checked = [] -                for entry in data: -                    addr = entry.get('ip') -                    if addr not in checked: -                        checked.append(addr) -                    else: -                        idx = _find_list_of_dict_index(data, key='ip', value=addr) -                        data.pop(idx) + +    inet_suffix = '6' if family == 'inet6' else '4' +    active_config = kea_get_active_config(inet_suffix) + +    for lease in leases: +        data_lease = {} +        data_lease['ip'] = lease['address'] +        lease_state_long = {'0': 'active', '1': 'rejected', '2': 'expired'} +        data_lease['state'] = lease_state_long[lease['state']] +        data_lease['pool'] = kea_get_pool_from_subnet_id(active_config, inet_suffix, lease['subnet_id']) if active_config else '-' +        data_lease['end'] = lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None +        data_lease['origin'] = 'local' # TODO: Determine remote in HA + +        if family == 'inet': +            data_lease['mac'] = lease['hwaddr'] +            data_lease['start'] = lease['start_timestamp'].timestamp() +            data_lease['hostname'] = lease['hostname'] + +        if family == 'inet6': +            data_lease['last_communication'] = lease['start_timestamp'].timestamp() +            data_lease['iaid_duid'] = _format_hex_string(lease['duid']) +            lease_types_long = {'0': 'non-temporary', '1': 'temporary', '2': 'prefix delegation'} +            data_lease['type'] = lease_types_long[lease['lease_type']] + +        data_lease['remaining'] = '-' + +        if lease['expire']: +            data_lease['remaining'] = lease['expire_timestamp'] - datetime.utcnow() + +            if data_lease['remaining'].days >= 0: +                # substraction gives us a timedelta object which can't be formatted with strftime +                # so we use str(), split gets rid of the microseconds +                data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] + +        # Do not add old leases +        if data_lease['remaining'] != '' and data_lease['pool'] in pool and data_lease['state'] != 'free': +            if not state or state == 'all' or data_lease['state'] in state: +                data.append(data_lease) + +        # deduplicate +        checked = [] +        for entry in data: +            addr = entry.get('ip') +            if addr not in checked: +                checked.append(addr) +            else: +                idx = _find_list_of_dict_index(data, key='ip', value=addr) +                data.pop(idx)      if sorted:          if sorted == 'ip': @@ -282,10 +279,9 @@ def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str],                         sorted: typing.Optional[str], state: typing.Optional[ArgState],                         origin: typing.Optional[ArgOrigin] ):      # if dhcp server is down, inactive leases may still be shown as active, so warn the user. -    v = '6' if family == 'inet6' else '' -    service_name = 'DHCPv6' if family == 'inet6' else 'DHCP' -    if not is_systemd_service_running(f'isc-dhcp-server{v}.service'): -        Warning(f'{service_name} server is configured but not started. Data may be stale.') +    v = '6' if family == 'inet6' else '4' +    if not is_systemd_service_running(f'kea-dhcp{v}-server.service'): +        Warning('DHCP server is configured but not started. Data may be stale.')      v = 'v6' if family == 'inet6' else ''      if pool and pool not in _get_dhcp_pools(family=family): diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index b3e6e518c..6a8797aec 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -22,6 +22,7 @@ from pathlib import Path  from shutil import copy, chown, rmtree, copytree  from glob import glob  from sys import exit +from os import environ  from time import sleep  from typing import Union  from urllib.parse import urlparse @@ -83,6 +84,8 @@ DIR_KERNEL_SRC: str = '/boot/'  FILE_ROOTFS_SRC: str = '/usr/lib/live/mount/medium/live/filesystem.squashfs'  ISO_DOWNLOAD_PATH: str = '/tmp/vyos_installation.iso' +external_download_script = '/usr/libexec/vyos/simple-download.py' +  # default boot variables  DEFAULT_BOOT_VARS: dict[str, str] = {      'timeout': '5', @@ -179,6 +182,7 @@ def create_partitions(target_disk: str, target_size: int,          rootfs_size: int = available_size      print(MSG_INFO_INSTALL_PARTITONING) +    raid.clear()      disk.disk_cleanup(target_disk)      disk_details: disk.DiskDetails = disk.parttable_create(target_disk,                                                             rootfs_size) @@ -459,8 +463,23 @@ def validate_signature(file_path: str, sign_type: str) -> None:      else:          print('Signature is valid') - -def image_fetch(image_path: str, no_prompt: bool = False) -> Path: +def download_file(local_file: str, remote_path: str, vrf: str, +                  username: str, password: str, +                  progressbar: bool = False, check_space: bool = False): +    environ['REMOTE_USERNAME'] = username +    environ['REMOTE_PASSWORD'] = password +    if vrf is None: +        download(local_file, remote_path, progressbar=progressbar, +                 check_space=check_space, raise_error=True) +    else: +        vrf_cmd = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password} \ +                ip vrf exec {vrf} {external_download_script} \ +                --local-file {local_file} --remote-path {remote_path}' +        cmd(vrf_cmd) + +def image_fetch(image_path: str, vrf: str = None, +                username: str = '', password: str = '', +                no_prompt: bool = False) -> Path:      """Fetch an ISO image      Args: @@ -473,14 +492,17 @@ def image_fetch(image_path: str, no_prompt: bool = False) -> Path:          # check a type of path          if urlparse(image_path).scheme:              # download an image -            download(ISO_DOWNLOAD_PATH, image_path, True, True, -                     raise_error=True) +            download_file(ISO_DOWNLOAD_PATH, image_path, vrf, +                          username, password, +                          progressbar=True, check_space=True) +              # download a signature              sign_file = (False, '')              for sign_type in ['minisig', 'asc']:                  try: -                    download(f'{ISO_DOWNLOAD_PATH}.{sign_type}', -                             f'{image_path}.{sign_type}', raise_error=True) +                    download_file(f'{ISO_DOWNLOAD_PATH}.{sign_type}', +                                  f'{image_path}.{sign_type}', vrf, +                                  username, password)                      sign_file = (True, sign_type)                      break                  except Exception: @@ -501,8 +523,8 @@ def image_fetch(image_path: str, no_prompt: bool = False) -> Path:                  return local_path              else:                  raise FileNotFoundError -    except Exception: -        print(f'The image cannot be fetched from: {image_path}') +    except Exception as e: +        print(f'The image cannot be fetched from: {image_path} {e}')          exit(1) @@ -611,7 +633,8 @@ def install_image() -> None:          print(MSG_WARN_IMAGE_NAME_WRONG)      # ask for password -    user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos') +    user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos', +                                   no_echo=True)      # ask for default console      console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE, @@ -730,7 +753,8 @@ def install_image() -> None:  @compat.grub_cfg_update -def add_image(image_path: str, no_prompt: bool = False) -> None: +def add_image(image_path: str, vrf: str = None, username: str = '', +              password: str = '', no_prompt: bool = False) -> None:      """Add a new image      Args: @@ -740,7 +764,7 @@ def add_image(image_path: str, no_prompt: bool = False) -> None:          exit(MSG_ERR_LIVE)      # fetch an image -    iso_path: Path = image_fetch(image_path, no_prompt) +    iso_path: Path = image_fetch(image_path, vrf, username, password, no_prompt)      try:          # mount an ISO          Path(DIR_ISO_MOUNT).mkdir(mode=0o755, parents=True) @@ -840,10 +864,15 @@ def parse_arguments() -> Namespace:                          choices=['install', 'add'],                          required=True,                          help='action to perform with an image') +    parser.add_argument('--vrf', +                        help='vrf name for image download')      parser.add_argument('--no-prompt', action='store_true',                          help='perform action non-interactively') -    parser.add_argument( -        '--image-path', +    parser.add_argument('--username', default='', +                        help='username for image download') +    parser.add_argument('--password', default='', +                        help='password for image download') +    parser.add_argument('--image-path',          help='a path (HTTP or local file) to an image that needs to be installed'      )      # parser.add_argument('--image_new_name', help='a new name for image') @@ -861,7 +890,8 @@ if __name__ == '__main__':          if args.action == 'install':              install_image()          if args.action == 'add': -            add_image(args.image_path, args.no_prompt) +            add_image(args.image_path, args.vrf, +                      args.username, args.password, args.no_prompt)          exit() diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index 71a40c0e1..2bc7e24fe 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -28,9 +28,6 @@ from vyos.configquery import ConfigTreeQuery  from vyos.utils.process import cmd  from vyos.utils.dict import dict_search -base = 'nat' -unconf_message = 'NAT is not configured' -  ArgDirection = typing.Literal['source', 'destination']  ArgFamily = typing.Literal['inet', 'inet6'] @@ -293,8 +290,9 @@ def _verify(func):      @wraps(func)      def _wrapper(*args, **kwargs):          config = ConfigTreeQuery() +        base = 'nat66' if 'inet6' in sys.argv[1:] else 'nat'          if not config.exists(base): -            raise vyos.opmode.UnconfiguredSubsystem(unconf_message) +            raise vyos.opmode.UnconfiguredSubsystem(f'{base.upper()} is not configured')          return func(*args, **kwargs)      return _wrapper diff --git a/src/pam-configs/mfa-google-authenticator b/src/pam-configs/mfa-google-authenticator new file mode 100644 index 000000000..9e49e5ef9 --- /dev/null +++ b/src/pam-configs/mfa-google-authenticator @@ -0,0 +1,8 @@ +Name: Google Authenticator PAM module (2FA/MFA) +Default: no +Priority: 384 + +Auth-Type: Primary +Auth: +    [default=ignore success=ok auth_err=die] pam_google_authenticator.so nullok forward_pass + diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh index 49e53d7e1..7b25bf338 100755 --- a/src/system/on-dhcp-event.sh +++ b/src/system/on-dhcp-event.sh @@ -15,20 +15,20 @@ if [ $# -lt 5 ]; then  fi  action=$1 -client_name=$2 -client_ip=$3 -client_mac=$4 -domain=$5 +client_name=$LEASE4_HOSTNAME +client_ip=$LEASE4_ADDRESS +client_mac=$LEASE4_HWADDR +domain=$(echo "$client_name" | cut -d"." -f2-)  hostsd_client="/usr/bin/vyos-hostsd-client"  case "$action" in -  commit) # add mapping for new lease +  leases4_renew|lease4_recover) # add mapping for new lease      if [ -z "$client_name" ]; then          logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"          client_name=$(echo "client-"$client_mac | tr : -)      fi -    if [ "$domain" == "..YYZ!" ]; then +    if [ -z "$domain" ]; then          client_fqdn_name=$client_name          client_search_expr=$client_name      else @@ -39,7 +39,7 @@ case "$action" in      exit 0      ;; -  release) # delete mapping for released address +  lease4_release|lease4_expire) # delete mapping for released address)      $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply      exit 0      ;; diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service deleted file mode 100644 index 1345c5fc5..000000000 --- a/src/systemd/isc-dhcp-server6.service +++ /dev/null @@ -1,24 +0,0 @@ -[Unit] -Description=ISC DHCP IPv6 server -Documentation=man:dhcpd(8) -RequiresMountsFor=/run -ConditionPathExists=/run/dhcp-server/dhcpdv6.conf -After=vyos-router.service - -[Service] -Type=forking -WorkingDirectory=/run/dhcp-server -RuntimeDirectory=dhcp-server -RuntimeDirectoryPreserve=yes -Environment=PID_FILE=/run/dhcp-server/dhcpdv6.pid CONFIG_FILE=/run/dhcp-server/dhcpdv6.conf LEASE_FILE=/config/dhcpdv6.leases -PIDFile=/run/dhcp-server/dhcpdv6.pid -ExecStartPre=/bin/sh -ec '\ -touch ${LEASE_FILE}; \ -chown nobody:nogroup ${LEASE_FILE}* ; \ -chmod 664 ${LEASE_FILE}* ; \ -/usr/sbin/dhcpd -6 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' -ExecStart=/usr/sbin/dhcpd -6 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} -Restart=always - -[Install] -WantedBy=multi-user.target diff --git a/src/validators/bgp-large-community-list b/src/validators/bgp-large-community-list index 80112dfdc..9ba5b27eb 100755 --- a/src/validators/bgp-large-community-list +++ b/src/validators/bgp-large-community-list @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -17,9 +17,8 @@  import re  import sys -from vyos.template import is_ipv4 -  pattern = '(.*):(.*):(.*)' +allowedChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '*', '?', '^', '$', '(', ')', '[', ']', '{', '}', '|', '\\', ':', '-' }  if __name__ == '__main__':      if len(sys.argv) != 2: @@ -29,8 +28,7 @@ if __name__ == '__main__':      if not len(value) == 3:          sys.exit(1) -    if not (re.match(pattern, sys.argv[1]) and -           (is_ipv4(value[0]) or value[0].isdigit()) and (value[1].isdigit() or value[1] == '*')): +    if not (re.match(pattern, sys.argv[1]) and set(sys.argv[1]).issubset(allowedChars)):          sys.exit(1)      sys.exit(0)  | 
