summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/dhcp_server.py134
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py42
-rwxr-xr-xsrc/conf_mode/dns_dynamic.py26
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py1
-rwxr-xr-xsrc/conf_mode/protocols_segment_routing.py74
-rwxr-xr-xsrc/conf_mode/system-login.py9
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