diff options
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-x | src/conf_mode/conntrack.py | 7 | ||||
-rwxr-xr-x | src/conf_mode/dns_dynamic.py | 92 | ||||
-rwxr-xr-x | src/conf_mode/http-api.py | 112 | ||||
-rwxr-xr-x | src/conf_mode/https.py | 73 | ||||
-rwxr-xr-x | src/conf_mode/nat64.py | 209 | ||||
-rwxr-xr-x | src/conf_mode/service_ipoe-server.py | 102 | ||||
-rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 18 | ||||
-rwxr-xr-x | src/conf_mode/vpn_l2tp.py | 47 | ||||
-rwxr-xr-x | src/conf_mode/vpn_pptp.py | 39 | ||||
-rwxr-xr-x | src/conf_mode/vpn_sstp.py | 13 |
10 files changed, 405 insertions, 307 deletions
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py index 4cece6921..7f6c71440 100755 --- a/src/conf_mode/conntrack.py +++ b/src/conf_mode/conntrack.py @@ -159,6 +159,13 @@ def verify(conntrack): if not group_obj: Warning(f'{error_group} "{group_name}" has no members!') + if dict_search_args(conntrack, 'timeout', 'custom', inet, 'rule') != None: + for rule, rule_config in conntrack['timeout']['custom'][inet]['rule'].items(): + if 'protocol' not in rule_config: + raise ConfigError(f'Conntrack custom timeout rule {rule} requires protocol tcp or udp') + else: + if 'tcp' in rule_config['protocol'] and 'udp' in rule_config['protocol']: + raise ConfigError(f'conntrack custom timeout rule {rule} - Cant use both tcp and udp protocol') return None def generate(conntrack): diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 2bccaee0f..3ddc8e7fd 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -30,16 +30,18 @@ config_file = r'/run/ddclient/ddclient.conf' systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' # Protocols that require zone -zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', + 'nfsn', 'nsupdate'] zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1'] # Protocols that do not require username username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla', - 'regfishde'] + 'nsupdate', 'regfishde'] # Protocols that support TTL -ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn'] +ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn', + 'nsupdate'] # Protocols that support both IPv4 and IPv6 dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns', @@ -70,63 +72,65 @@ def get_config(config=None): def verify(dyndns): # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: return None - for address in dyndns['address']: - # If dyndns address is an interface, ensure it exists - if address != 'web': - verify_interface_exists(address) + # Dynamic DNS service provider - configuration validation + for service, config in dyndns['name'].items(): - # RFC2136 - configuration validation - if 'rfc2136' in dyndns['address'][address]: - for config in dyndns['address'][address]['rfc2136'].values(): - for field in ['host_name', 'zone', 'server', 'key']: - if field not in config: - raise ConfigError(f'"{field.replace("_", "-")}" is required for RFC2136 ' - f'based Dynamic DNS service on "{address}"') + error_msg_req = f'is required for Dynamic DNS service "{service}"' + error_msg_uns = f'is not supported for Dynamic DNS service "{service}"' - # Dynamic DNS service provider - configuration validation - if 'web_options' in dyndns['address'][address] and address != 'web': - raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') + for field in ['protocol', 'address', 'host_name']: + if field not in config: + raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') - # Dynamic DNS service provider - configuration validation - if 'service' in dyndns['address'][address]: - for service, config in dyndns['address'][address]['service'].items(): - error_msg_req = f'is required for Dynamic DNS service "{service}" on "{address}"' - error_msg_uns = f'is not supported for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' + # If dyndns address is an interface, ensure that it exists + # and that web-options are not set + if config['address'] != 'web': + 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') - for field in ['host_name', 'password', 'protocol']: - if field not in config: - raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') + # RFC2136 uses 'key' instead of 'password' + if config['protocol'] != 'nsupdate' and 'password' not in config: + raise ConfigError(f'"password" {error_msg_req}') - if config['protocol'] in zone_necessary and 'zone' not in config: - raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"') + # Other RFC2136 specific configuration validation + if config['protocol'] == 'nsupdate': + if 'password' in config: + raise ConfigError(f'"password" {error_msg_uns} with protocol "{config["protocol"]}"') + for field in ['server', 'key']: + if field not in config: + raise ConfigError(f'"{field}" {error_msg_req} with protocol "{config["protocol"]}"') - if config['protocol'] not in zone_supported and 'zone' in config: - raise ConfigError(f'"zone" {error_msg_uns}') + if config['protocol'] in zone_necessary and 'zone' not in config: + raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"') - if config['protocol'] not in username_unnecessary and 'username' not in config: - raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"') + if config['protocol'] not in zone_supported and 'zone' in config: + raise ConfigError(f'"zone" {error_msg_uns} with protocol "{config["protocol"]}"') - if config['protocol'] not in ttl_supported and 'ttl' in config: - raise ConfigError(f'"ttl" {error_msg_uns}') + if config['protocol'] not in username_unnecessary and 'username' not in config: + raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"') - 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}') - # 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"]}"') + if config['protocol'] not in ttl_supported and 'ttl' in config: + raise ConfigError(f'"ttl" {error_msg_uns} 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"') + 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"]}"') + # 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"]}"') + + 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}"') return None def generate(dyndns): # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: return None render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns, permission=0o600) @@ -139,7 +143,7 @@ def apply(dyndns): call('systemctl daemon-reload') # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: call(f'systemctl stop {systemd_service}') if os.path.exists(config_file): os.unlink(config_file) diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py deleted file mode 100755 index 855d444c6..000000000 --- a/src/conf_mode/http-api.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019-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 -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import sys -import os -import json - -from time import sleep - -import vyos.defaults - -from vyos.config import Config -from vyos.configdep import set_dependents, call_dependents -from vyos.template import render -from vyos.utils.process import call -from vyos.utils.process import is_systemd_service_running -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -api_config_state = '/tmp/api-config-state' -systemd_service = '/run/systemd/system/vyos-http-api.service' - -vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode'] - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - # reset on creation/deletion of 'api' node - https_base = ['service', 'https'] - if conf.exists(https_base): - set_dependents("https", conf) - - base = ['service', 'https', 'api'] - if not conf.exists(base): - return None - - http_api = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True, - with_recursive_defaults=True) - - # Do we run inside a VRF context? - vrf_path = ['service', 'https', 'vrf'] - if conf.exists(vrf_path): - http_api['vrf'] = conf.return_value(vrf_path) - - if http_api.from_defaults(['graphql']): - del http_api['graphql'] - - return http_api - -def verify(_http_api): - return - -def generate(http_api): - if http_api is None: - if os.path.exists(systemd_service): - os.unlink(systemd_service) - return - - with open(api_config_state, 'w') as f: - json.dump(http_api, f, indent=2) - - render(systemd_service, 'https/vyos-http-api.service.j2', http_api) - -def apply(http_api): - # Reload systemd manager configuration - call('systemctl daemon-reload') - service_name = 'vyos-http-api.service' - - if http_api is not None: - if is_systemd_service_running(f'{service_name}'): - call(f'systemctl reload {service_name}') - else: - call(f'systemctl restart {service_name}') - else: - call(f'systemctl stop {service_name}') - - # Let uvicorn settle before restarting Nginx - sleep(1) - - call_dependents() - - if os.path.exists(api_config_state): - os.unlink(api_config_state) - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 81e510b0d..40b7de557 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -16,19 +16,24 @@ import os import sys +import json from copy import deepcopy +from time import sleep import vyos.defaults import vyos.certbot_util from vyos.config import Config +from vyos.configdiff import get_config_diff from vyos.configverify import verify_vrf from vyos import ConfigError from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.utils.process import call +from vyos.utils.process import is_systemd_service_running +from vyos.utils.process import is_systemd_service_active from vyos.utils.network import check_port_availability from vyos.utils.network import is_listen_port_bind_service from vyos.utils.file import write_file @@ -42,6 +47,9 @@ cert_dir = '/etc/ssl/certs' key_dir = '/etc/ssl/private' certbot_dir = vyos.defaults.directories['certbot'] +api_config_state = '/run/http-api-state' +systemd_service = '/run/systemd/system/vyos-http-api.service' + # https config needs to coordinate several subsystems: api, certbot, # self-signed certificate, as well as the virtual hosts defined within the # https config definition itself. Consequently, one needs a general dict, @@ -67,11 +75,35 @@ def get_config(config=None): if not conf.exists(base): return None + diff = get_config_diff(conf) + https = conf.get_config_dict(base, get_first_key=True) if https: https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) + no_tag_node_value_mangle=True, + get_first_key=True) + + https['children_changed'] = diff.node_changed_children(base) + https['api_add_or_delete'] = diff.node_changed_presence(base + ['api']) + + if 'api' not in https: + return https + + http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) + + if http_api.from_defaults(['graphql']): + del http_api['graphql'] + + # Do we run inside a VRF context? + vrf_path = ['service', 'https', 'vrf'] + if conf.exists(vrf_path): + http_api['vrf'] = conf.return_value(vrf_path) + + https['api'] = http_api return https @@ -103,7 +135,7 @@ def verify(https): if 'certbot' in https['certificates']: vhost_names = [] - for vh, vh_conf in https.get('virtual-host', {}).items(): + for _, vh_conf in https.get('virtual-host', {}).items(): vhost_names += vh_conf.get('server-name', []) domains = https['certificates']['certbot'].get('domain-name', []) domains_found = [domain for domain in domains if domain in vhost_names] @@ -167,6 +199,14 @@ def generate(https): if https is None: return None + if 'api' not in https: + if os.path.exists(systemd_service): + os.unlink(systemd_service) + else: + render(systemd_service, 'https/vyos-http-api.service.j2', https['api']) + with open(api_config_state, 'w') as f: + json.dump(https['api'], f, indent=2) + server_block_list = [] # organize by vhosts @@ -254,10 +294,31 @@ def generate(https): def apply(https): # Reload systemd manager configuration call('systemctl daemon-reload') - if https is not None: - call('systemctl restart nginx.service') - else: - call('systemctl stop nginx.service') + http_api_service_name = 'vyos-http-api.service' + https_service_name = 'nginx.service' + + if https is None: + if is_systemd_service_active(f'{http_api_service_name}'): + call(f'systemctl stop {http_api_service_name}') + call(f'systemctl stop {https_service_name}') + return + + if 'api' in https['children_changed']: + if 'api' in https: + if is_systemd_service_running(f'{http_api_service_name}'): + call(f'systemctl reload {http_api_service_name}') + else: + call(f'systemctl restart {http_api_service_name}') + # Let uvicorn settle before (possibly) restarting nginx + sleep(1) + else: + if is_systemd_service_active(f'{http_api_service_name}'): + call(f'systemctl stop {http_api_service_name}') + + if (not is_systemd_service_running(f'{https_service_name}') or + https['api_add_or_delete'] or + set(https['children_changed']) - set(['api'])): + call(f'systemctl restart {https_service_name}') if __name__ == '__main__': try: diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py new file mode 100755 index 000000000..a8b90fb11 --- /dev/null +++ b/src/conf_mode/nat64.py @@ -0,0 +1,209 @@ +#!/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/>. + +# pylint: disable=empty-docstring,missing-module-docstring + +import csv +import os +import re + +from ipaddress import IPv6Network +from json import dumps as json_write + +from vyos import ConfigError +from vyos import airbag +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import is_node_changed +from vyos.utils.dict import dict_search +from vyos.utils.file import write_file +from vyos.utils.kernel import check_kmod +from vyos.utils.process import cmd +from vyos.utils.process import run + +airbag.enable() + +INSTANCE_REGEX = re.compile(r"instance-(\d+)") +JOOL_CONFIG_DIR = "/run/jool" + + +def get_config(config: Config | None = None) -> None: + if config is None: + config = Config() + + base = ["nat64"] + nat64 = config.get_config_dict(base, key_mangling=("-", "_"), get_first_key=True) + + base_src = base + ["source", "rule"] + + # Load in existing instances so we can destroy any unknown + lines = cmd("jool instance display --csv").splitlines() + for _, instance, _ in csv.reader(lines): + match = INSTANCE_REGEX.fullmatch(instance) + if not match: + # FIXME: Instances that don't match should be ignored but WARN'ed to the user + continue + num = match.group(1) + + rules = nat64.setdefault("source", {}).setdefault("rule", {}) + # Mark it for deletion + if num not in rules: + rules[num] = {"deleted": True} + continue + + # If the user changes the mode, recreate the instance else Jool fails with: + # Jool error: Sorry; you can't change an instance's framework for now. + if is_node_changed(config, base_src + [f"instance-{num}", "mode"]): + rules[num]["recreate"] = True + + # If the user changes the pool6, recreate the instance else Jool fails with: + # Jool error: Sorry; you can't change a NAT64 instance's pool6 for now. + if dict_search("source.prefix", rules[num]) and is_node_changed( + config, + base_src + [num, "source", "prefix"], + ): + rules[num]["recreate"] = True + + return nat64 + + +def verify(nat64) -> None: + if not nat64: + # no need to verify the CLI as nat64 is going to be deactivated + return + + if dict_search("source.rule", nat64): + # Ensure only 1 netfilter instance per namespace + nf_rules = filter( + lambda i: "deleted" not in i and i.get('mode') == "netfilter", + nat64["source"]["rule"].values(), + ) + next(nf_rules, None) # Discard the first element + if next(nf_rules, None) is not None: + raise ConfigError( + "Jool permits only 1 NAT64 netfilter instance (per network namespace)" + ) + + for rule, instance in nat64["source"]["rule"].items(): + if "deleted" in instance: + continue + + # Verify that source.prefix is set and is a /96 + if not dict_search("source.prefix", instance): + raise ConfigError(f"Source NAT64 rule {rule} missing source prefix") + if IPv6Network(instance["source"]["prefix"]).prefixlen != 96: + raise ConfigError(f"Source NAT64 rule {rule} source prefix must be /96") + + pools = dict_search("translation.pool", instance) + if pools: + for num, pool in pools.items(): + if "address" not in pool: + raise ConfigError( + f"Source NAT64 rule {rule} translation pool " + f"{num} missing address/prefix" + ) + if "port" not in pool: + raise ConfigError( + f"Source NAT64 rule {rule} translation pool " + f"{num} missing port(-range)" + ) + + +def generate(nat64) -> None: + os.makedirs(JOOL_CONFIG_DIR, exist_ok=True) + + if dict_search("source.rule", nat64): + for rule, instance in nat64["source"]["rule"].items(): + if "deleted" in instance: + # Delete the unused instance file + os.unlink(os.path.join(JOOL_CONFIG_DIR, f"instance-{rule}.json")) + continue + + name = f"instance-{rule}" + config = { + "instance": name, + "framework": "netfilter", + "global": { + "pool6": instance["source"]["prefix"], + "manually-enabled": "disable" not in instance, + }, + # "bib": [], + } + + if "description" in instance: + config["comment"] = instance["description"] + + if dict_search("translation.pool", instance): + pool4 = [] + for pool in instance["translation"]["pool"].values(): + if "disable" in pool: + continue + + protos = pool.get("protocol", {}).keys() or ("tcp", "udp", "icmp") + for proto in protos: + obj = { + "protocol": proto.upper(), + "prefix": pool["address"], + "port range": pool["port"], + } + if "description" in pool: + obj["comment"] = pool["description"] + + pool4.append(obj) + + if pool4: + config["pool4"] = pool4 + + write_file(f'{JOOL_CONFIG_DIR}/{name}.json', json_write(config, indent=2)) + + +def apply(nat64) -> None: + if not nat64: + return + + if dict_search("source.rule", nat64): + # Deletions first to avoid conflicts + for rule, instance in nat64["source"]["rule"].items(): + if not any(k in instance for k in ("deleted", "recreate")): + continue + + ret = run(f"jool instance remove instance-{rule}") + if ret != 0: + raise ConfigError( + f"Failed to remove nat64 source rule {rule} (jool instance instance-{rule})" + ) + + # Now creations + for rule, instance in nat64["source"]["rule"].items(): + if "deleted" in instance: + continue + + name = f"instance-{rule}" + ret = run(f"jool -i {name} file handle {JOOL_CONFIG_DIR}/{name}.json") + if ret != 0: + raise ConfigError(f"Failed to set jool instance {name}") + + +if __name__ == "__main__": + try: + check_kmod(["jool"]) + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index b70e32373..36f00dec5 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -15,17 +15,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import jmespath from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict -from vyos.configverify import verify_accel_ppp_base_service from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search +from vyos.accel_ppp_util import get_pools_in_order +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos import ConfigError from vyos import airbag airbag.enable() @@ -35,87 +35,6 @@ ipoe_conf = '/run/accel-pppd/ipoe.conf' ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' -def get_pools_in_order(data: dict) -> list: - """Return a list of dictionaries representing pool data in the order - in which they should be allocated. Pool must be defined before we can - use it with 'next-pool' option. - - Args: - data: A dictionary of pool data, where the keys are pool names and the - values are dictionaries containing the 'subnet' key and the optional - 'next_pool' key. - - Returns: - list: A list of dictionaries - - Raises: - ValueError: If a 'next_pool' key references a pool name that - has not been defined. - ValueError: If a circular reference is found in the 'next_pool' keys. - - Example: - config_data = { - ... 'first-pool': { - ... 'next_pool': 'second-pool', - ... 'subnet': '192.0.2.0/25' - ... }, - ... 'second-pool': { - ... 'next_pool': 'third-pool', - ... 'subnet': '203.0.113.0/25' - ... }, - ... 'third-pool': { - ... 'subnet': '198.51.100.0/24' - ... }, - ... 'foo': { - ... 'subnet': '100.64.0.0/24', - ... 'next_pool': 'second-pool' - ... } - ... } - - % get_pools_in_order(config_data) - [{'third-pool': {'subnet': '198.51.100.0/24'}}, - {'second-pool': {'next_pool': 'third-pool', 'subnet': '203.0.113.0/25'}}, - {'first-pool': {'next_pool': 'second-pool', 'subnet': '192.0.2.0/25'}}, - {'foo': {'next_pool': 'second-pool', 'subnet': '100.64.0.0/24'}}] - """ - pools = [] - unresolved_pools = {} - - for pool, pool_config in data.items(): - if 'next_pool' not in pool_config: - pools.insert(0, {pool: pool_config}) - else: - unresolved_pools[pool] = pool_config - - while unresolved_pools: - resolved_pools = [] - - for pool, pool_config in unresolved_pools.items(): - next_pool_name = pool_config['next_pool'] - - if any(p for p in pools if next_pool_name in p): - index = next( - (i for i, p in enumerate(pools) if next_pool_name in p), - None) - pools.insert(index + 1, {pool: pool_config}) - resolved_pools.append(pool) - elif next_pool_name in unresolved_pools: - # next pool not yet resolved - pass - else: - raise ValueError( - f"Pool '{next_pool_name}' not defined in configuration data" - ) - - if not resolved_pools: - raise ValueError("Circular reference in configuration data") - - for pool in resolved_pools: - unresolved_pools.pop(pool) - - return pools - - def get_config(config=None): if config: conf = config @@ -128,18 +47,11 @@ def get_config(config=None): # retrieve common dictionary keys ipoe = get_accel_dict(conf, base, ipoe_chap_secrets) - if jmespath.search('client_ip_pool.name', ipoe): - dict_named_pools = jmespath.search('client_ip_pool.name', ipoe) + if dict_search('client_ip_pool', ipoe): # Multiple named pools require ordered values T5099 - ipoe['ordered_named_pools'] = get_pools_in_order(dict_named_pools) - # T5099 'next-pool' option - if jmespath.search('client_ip_pool.name.*.next_pool', ipoe): - for pool, pool_config in ipoe['client_ip_pool']['name'].items(): - if 'next_pool' in pool_config: - ipoe['first_named_pool'] = pool - ipoe['first_named_pool_subnet'] = pool_config - break + ipoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', ipoe)) + ipoe['server_type'] = 'ipoe' return ipoe @@ -156,9 +68,7 @@ def verify(ipoe): raise ConfigError('Option "client-subnet" incompatible with "vlan"!' 'Use "ipoe client-ip-pool" instead.') - #verify_accel_ppp_base_service(ipoe, local_users=False) - # IPoE server does not have 'gateway' option in the CLI - # we cannot use configverify.py verify_accel_ppp_base_service for ipoe-server + verify_accel_ppp_ip_pool(ipoe) if dict_search('authentication.mode', ipoe) == 'radius': if not dict_search('authentication.radius.server', ipoe): diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 87660c127..7c624f034 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -21,13 +21,16 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict from vyos.configdict import is_node_changed -from vyos.configverify import verify_accel_ppp_base_service from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search +from vyos.accel_ppp_util import verify_accel_ppp_base_service +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool +from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError from vyos import airbag + airbag.enable() pppoe_conf = r'/run/accel-pppd/pppoe.conf' @@ -45,6 +48,10 @@ def get_config(config=None): # retrieve common dictionary keys pppoe = get_accel_dict(conf, base, pppoe_chap_secrets) + if dict_search('client_ip_pool', pppoe): + # Multiple named pools require ordered values T5099 + pppoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', pppoe)) + # reload-or-restart does not implemented in accel-ppp # use this workaround until it will be implemented # https://phabricator.accel-ppp.org/T3 @@ -53,7 +60,7 @@ def get_config(config=None): is_node_changed(conf, base + ['interface'])] if any(conditions): pppoe.update({'restart_required': {}}) - + pppoe['server_type'] = 'pppoe' return pppoe def verify(pppoe): @@ -72,12 +79,7 @@ def verify(pppoe): for interface in pppoe['interface']: verify_interface_exists(interface) - # local ippool and gateway settings config checks - if not (dict_search('client_ip_pool.subnet', pppoe) or - (dict_search('client_ip_pool.name', pppoe) or - (dict_search('client_ip_pool.start', pppoe) and - dict_search('client_ip_pool.stop', pppoe)))): - print('Warning: No PPPoE client pool defined') + verify_accel_ppp_ip_pool(pppoe) if dict_search('authentication.radius.dynamic_author.server', pppoe): if not dict_search('authentication.radius.dynamic_author.key', pppoe): diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 6232ce64a..9a022d93c 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -21,15 +21,16 @@ from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit -from ipaddress import ip_network - from vyos.config import Config from vyos.template import is_ipv4 from vyos.template import render from vyos.utils.process import call from vyos.utils.system import get_half_cpus +from vyos.utils.dict import dict_search from vyos.utils.network import check_port_availability from vyos.utils.network import is_listen_port_bind_service +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool +from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError from vyos import airbag @@ -43,7 +44,7 @@ default_config_data = { 'auth_ppp_mppe': 'prefer', 'auth_proto': ['auth_mschap_v2'], 'chap_secrets_file': l2tp_chap_secrets, # used in Jinja2 template - 'client_ip_pool': None, + 'client_ip_pool': {}, 'client_ip_subnets': [], 'client_ipv6_pool': [], 'client_ipv6_pool_configured': False, @@ -246,13 +247,14 @@ def get_config(config=None): conf.set_level(base_path) if conf.exists(['client-ip-pool']): - if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): - start = conf.return_value(['client-ip-pool', 'start']) - stop = conf.return_value(['client-ip-pool', 'stop']) - l2tp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + for pool_name in conf.list_nodes(['client-ip-pool']): + l2tp['client_ip_pool'][pool_name] = {} + l2tp['client_ip_pool'][pool_name]['range'] = conf.return_value(['client-ip-pool', pool_name, 'range']) + l2tp['client_ip_pool'][pool_name]['next_pool'] = conf.return_value(['client-ip-pool', pool_name, 'next-pool']) - if conf.exists(['client-ip-pool', 'subnet']): - l2tp['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet']) + if dict_search('client_ip_pool', l2tp): + # Multiple named pools require ordered values T5099 + l2tp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', l2tp)) if conf.exists(['client-ipv6-pool', 'prefix']): l2tp['client_ipv6_pool_configured'] = True @@ -281,23 +283,15 @@ def get_config(config=None): l2tp['client_ipv6_delegate_prefix'].append(tmp) + if conf.exists(['default-pool']): + l2tp['default_pool'] = conf.return_value(['default-pool']) + if conf.exists(['mtu']): l2tp['mtu'] = conf.return_value(['mtu']) # gateway address if conf.exists(['gateway-address']): l2tp['gateway_address'] = conf.return_value(['gateway-address']) - else: - # calculate gw-ip-address - if conf.exists(['client-ip-pool', 'start']): - # use start ip as gw-ip-address - l2tp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) - - elif conf.exists(['client-ip-pool', 'subnet']): - # use first ip address from first defined pool - subnet = conf.return_values(['client-ip-pool', 'subnet'])[0] - subnet = ip_network(subnet) - l2tp['gateway_address'] = str(list(subnet.hosts())[0]) # LNS secret if conf.exists(['lns', 'shared-secret']): @@ -330,9 +324,13 @@ def get_config(config=None): if conf.exists(['ppp-options', 'ipv6-peer-intf-id']): l2tp['ppp_ipv6_peer_intf_id'] = conf.return_value(['ppp-options', 'ipv6-peer-intf-id']) + l2tp['server_type'] = 'l2tp' return l2tp + + + def verify(l2tp): if not l2tp: return None @@ -366,10 +364,11 @@ def verify(l2tp): not is_listen_port_bind_service(int(port), 'accel-pppd'): raise ConfigError(f'"{proto}" port "{port}" is used by another service') - # check for the existence of a client ip pool - if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']): - raise ConfigError( - "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") + if l2tp['auth_mode'] == 'local' or l2tp['auth_mode'] == 'noauth': + if not l2tp['client_ip_pool']: + raise ConfigError( + "L2TP local auth mode requires local client-ip-pool to be configured!") + verify_accel_ppp_ip_pool(l2tp) # check ipv6 if l2tp['client_ipv6_delegate_prefix'] and not l2tp['client_ipv6_pool']: diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index d542f57fe..6243c3ed3 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -21,10 +21,14 @@ from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit + from vyos.config import Config from vyos.template import render from vyos.utils.system import get_half_cpus from vyos.utils.process import call +from vyos.utils.dict import dict_search +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool +from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError from vyos import airbag @@ -54,7 +58,7 @@ default_pptp = { 'outside_addr': '', 'dnsv4': [], 'wins': [], - 'client_ip_pool': '', + 'client_ip_pool': {}, 'mtu': '1436', 'auth_proto' : ['auth_mschap_v2'], 'ppp_mppe' : 'prefer', @@ -205,22 +209,24 @@ def get_config(config=None): conf.set_level(base_path) if conf.exists(['client-ip-pool']): - if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): - start = conf.return_value(['client-ip-pool', 'start']) - stop = conf.return_value(['client-ip-pool', 'stop']) - pptp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + for pool_name in conf.list_nodes(['client-ip-pool']): + pptp['client_ip_pool'][pool_name] = {} + pptp['client_ip_pool'][pool_name]['range'] = conf.return_value(['client-ip-pool', pool_name, 'range']) + pptp['client_ip_pool'][pool_name]['next_pool'] = conf.return_value(['client-ip-pool', pool_name, 'next-pool']) + + if dict_search('client_ip_pool', pptp): + # Multiple named pools require ordered values T5099 + pptp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', pptp)) + + if conf.exists(['default-pool']): + pptp['default_pool'] = conf.return_value(['default-pool']) if conf.exists(['mtu']): pptp['mtu'] = conf.return_value(['mtu']) # gateway address if conf.exists(['gateway-address']): - pptp['gw_ip'] = conf.return_value(['gateway-address']) - else: - # calculate gw-ip-address - if conf.exists(['client-ip-pool', 'start']): - # use start ip as gw-ip-address - pptp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) + pptp['gateway_address'] = conf.return_value(['gateway-address']) if conf.exists(['authentication', 'require']): # clear default list content, now populate with actual CLI values @@ -238,6 +244,7 @@ def get_config(config=None): if conf.exists(['authentication', 'mppe']): pptp['ppp_mppe'] = conf.return_value(['authentication', 'mppe']) + pptp['server_type'] = 'pptp' return pptp @@ -248,21 +255,25 @@ def verify(pptp): if pptp['auth_mode'] == 'local': if not pptp['local_users']: raise ConfigError('PPTP local auth mode requires local users to be configured!') - for user in pptp['local_users']: username = user['name'] if not user['password']: raise ConfigError(f'Password required for local user "{username}"') - elif pptp['auth_mode'] == 'radius': if len(pptp['radius_server']) == 0: raise ConfigError('RADIUS authentication requires at least one server') - for radius in pptp['radius_server']: if not radius['key']: server = radius['server'] raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + if pptp['auth_mode'] == 'local' or pptp['auth_mode'] == 'noauth': + if not pptp['client_ip_pool']: + raise ConfigError( + "PPTP local auth mode requires local client-ip-pool to be configured!") + + verify_accel_ppp_ip_pool(pptp) + if len(pptp['dnsv4']) > 2: raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index e98d8385b..ac053cc76 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.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 @@ -21,13 +21,15 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict from vyos.configdict import dict_merge -from vyos.configverify import verify_accel_ppp_base_service from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.utils.process import call from vyos.utils.network import check_port_availability from vyos.utils.dict import dict_search +from vyos.accel_ppp_util import verify_accel_ppp_base_service +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool +from vyos.accel_ppp_util import get_pools_in_order from vyos.utils.network import is_listen_port_bind_service from vyos.utils.file import write_file from vyos import ConfigError @@ -53,13 +55,17 @@ def get_config(config=None): # retrieve common dictionary keys sstp = get_accel_dict(conf, base, sstp_chap_secrets) + if dict_search('client_ip_pool', sstp): + # Multiple named pools require ordered values T5099 + sstp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', sstp)) if sstp: sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - + sstp['server_type'] = 'sstp' return sstp + def verify(sstp): if not sstp: return None @@ -75,6 +81,7 @@ def verify(sstp): if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp: raise ConfigError('Client IP subnet required') + verify_accel_ppp_ip_pool(sstp) # # SSL certificate checks # |