diff options
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-x | src/conf_mode/dhcp_server.py | 134 | ||||
-rwxr-xr-x | src/conf_mode/dhcpv6_server.py | 42 | ||||
-rwxr-xr-x | src/conf_mode/dns_dynamic.py | 26 | ||||
-rwxr-xr-x | src/conf_mode/protocols_bgp.py | 1 | ||||
-rwxr-xr-x | src/conf_mode/protocols_segment_routing.py | 74 | ||||
-rwxr-xr-x | src/conf_mode/system-login.py | 9 |
6 files changed, 231 insertions, 55 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 |