diff options
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-x | src/conf_mode/container.py | 23 | ||||
-rwxr-xr-x | src/conf_mode/firewall.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/high-availability.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-bonding.py | 7 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-geneve.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-pseudo-ethernet.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-sstpc.py | 5 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-tunnel.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-vxlan.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/pki.py | 5 | ||||
-rwxr-xr-x | src/conf_mode/policy-route.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/protocols_failover.py | 121 | ||||
-rwxr-xr-x | src/conf_mode/protocols_static.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/qos.py | 194 | ||||
-rwxr-xr-x | src/conf_mode/service_webproxy.py | 100 | ||||
-rwxr-xr-x | src/conf_mode/system-option.py | 17 | ||||
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/vpn_l2tp.py | 12 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 6 |
19 files changed, 460 insertions, 80 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 8efeaed54..7567444db 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -73,9 +73,19 @@ def get_config(config=None): # Merge per-container default values if 'name' in container: default_values = defaults(base + ['name']) + if 'port' in default_values: + del default_values['port'] for name in container['name']: container['name'][name] = dict_merge(default_values, container['name'][name]) + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'port' in container['name'][name]: + for port in container['name'][name]['port']: + default_values = defaults(base + ['name', 'port']) + container['name'][name]['port'][port] = dict_merge( + default_values, container['name'][name]['port'][port]) + # Delete container network, delete containers tmp = node_changed(conf, base + ['network']) if tmp: container.update({'network_remove' : tmp}) @@ -168,6 +178,11 @@ def verify(container): if not os.path.exists(source): raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!') + if 'port' in container_config: + for tmp in container_config['port']: + if not {'source', 'destination'} <= set(container_config['port'][tmp]): + raise ConfigError(f'Both "source" and "destination" must be specified for a port mapping!') + # If 'allow-host-networks' or 'network' not set. if 'allow_host_networks' not in container_config and 'network' not in container_config: raise ConfigError(f'Must either set "network" or "allow-host-networks" for container "{name}"!') @@ -237,14 +252,10 @@ def generate_run_arguments(name, container_config): if 'port' in container_config: protocol = '' for portmap in container_config['port']: - if 'protocol' in container_config['port'][portmap]: - protocol = container_config['port'][portmap]['protocol'] - protocol = f'/{protocol}' - else: - protocol = '/tcp' + protocol = container_config['port'][portmap]['protocol'] sport = container_config['port'][portmap]['source'] dport = container_config['port'][portmap]['destination'] - port += f' -p {sport}:{dport}{protocol}' + port += f' -p {sport}:{dport}/{protocol}' # Bind volume volume = '' diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index f68acfe02..20cf1ead1 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -65,7 +65,8 @@ valid_groups = [ 'address_group', 'domain_group', 'network_group', - 'port_group' + 'port_group', + 'interface_group' ] nested_group_types = [ diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index 8a959dc79..4ed16d0d7 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.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 @@ -144,8 +144,10 @@ def verify(ha): # Virtual-server if 'virtual_server' in ha: for vs, vs_config in ha['virtual_server'].items(): - if 'port' not in vs_config: - raise ConfigError(f'Port is required but not set for virtual-server "{vs}"') + if 'port' not in vs_config and 'fwmark' not in vs_config: + raise ConfigError(f'Port or fwmark is required but not set for virtual-server "{vs}"') + if 'port' in vs_config and 'fwmark' in vs_config: + raise ConfigError(f'Cannot set both port and fwmark for virtual-server "{vs}"') if 'real_server' not in vs_config: raise ConfigError(f'Real-server ip is required but not set for virtual-server "{vs}"') # Real-server diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 21cf204fc..9936620c8 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -21,6 +21,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configdict import is_node_changed from vyos.configdict import leaf_node_changed from vyos.configdict import is_member from vyos.configdict import is_source_interface @@ -81,10 +82,10 @@ def get_config(config=None): if 'mode' in bond: bond['mode'] = get_bond_mode(bond['mode']) - tmp = leaf_node_changed(conf, base + [ifname, 'mode']) + tmp = is_node_changed(conf, base + [ifname, 'mode']) if tmp: bond['shutdown_required'] = {} - tmp = leaf_node_changed(conf, base + [ifname, 'lacp-rate']) + tmp = is_node_changed(conf, base + [ifname, 'lacp-rate']) if tmp: bond['shutdown_required'] = {} # determine which members have been removed @@ -116,7 +117,7 @@ def get_config(config=None): if dict_search('member.interface', bond): for interface, interface_config in bond['member']['interface'].items(): # Check if member interface is a new member - if not conf.exists_effective(['member', 'interface', interface]): + if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]): bond['shutdown_required'] = {} # Check if member interface is disabled diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index 08cc3a48d..f6694ddde 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -14,14 +14,11 @@ # 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 netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict -from vyos.configdict import leaf_node_changed from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_mtu_ipv6 @@ -49,13 +46,10 @@ def get_config(config=None): # GENEVE interfaces are picky and require recreation if certain parameters # change. But a GENEVE interface should - of course - not be re-created if # it's description or IP address is adjusted. Feels somehow logic doesn't it? - for cli_option in ['remote', 'vni']: - if leaf_node_changed(conf, base + [ifname, cli_option]): + for cli_option in ['remote', 'vni', 'parameters']: + if is_node_changed(conf, base + [ifname, cli_option]): geneve.update({'rebuild_required': {}}) - if is_node_changed(conf, base + [ifname, 'parameters']): - geneve.update({'rebuild_required': {}}) - return geneve def verify(geneve): diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 4c65bc0b6..dce5c2358 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -21,7 +21,7 @@ from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configdict import is_source_interface -from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -51,7 +51,7 @@ def get_config(config=None): mode = is_node_changed(conf, ['mode']) if mode: peth.update({'shutdown_required' : {}}) - if leaf_node_changed(conf, base + [ifname, 'mode']): + if is_node_changed(conf, base + [ifname, 'mode']): peth.update({'rebuild_required': {}}) if 'source_interface' in peth: diff --git a/src/conf_mode/interfaces-sstpc.py b/src/conf_mode/interfaces-sstpc.py index 6b8094c51..b5cc4cf4e 100755 --- a/src/conf_mode/interfaces-sstpc.py +++ b/src/conf_mode/interfaces-sstpc.py @@ -70,7 +70,10 @@ def verify(sstpc): verify_authentication(sstpc) verify_vrf(sstpc) - if dict_search('ssl.ca_certificate', sstpc) == None: + if not dict_search('server', sstpc): + raise ConfigError('Remote SSTP server must be specified!') + + if not dict_search('ssl.ca_certificate', sstpc): raise ConfigError('Missing mandatory CA certificate!') return None diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index acef1fda7..e2701d9d3 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -21,7 +21,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict -from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_interface_exists @@ -52,7 +52,7 @@ def get_config(config=None): ifname, tunnel = get_interface_dict(conf, base) if 'deleted' not in tunnel: - tmp = leaf_node_changed(conf, base + [ifname, 'encapsulation']) + tmp = is_node_changed(conf, base + [ifname, 'encapsulation']) if tmp: tunnel.update({'encapsulation_changed': {}}) # We also need to inspect other configured tunnels as there are Kernel diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index af2d0588d..b1536148c 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -52,13 +52,11 @@ def get_config(config=None): # VXLAN interfaces are picky and require recreation if certain parameters # change. But a VXLAN interface should - of course - not be re-created if # it's description or IP address is adjusted. Feels somehow logic doesn't it? - for cli_option in ['external', 'gpe', 'group', 'port', 'remote', + for cli_option in ['parameters', 'external', 'gpe', 'group', 'port', 'remote', 'source-address', 'source-interface', 'vni']: - if leaf_node_changed(conf, base + [ifname, cli_option]): + if is_node_changed(conf, base + [ifname, cli_option]): vxlan.update({'rebuild_required': {}}) - - if is_node_changed(conf, base + [ifname, 'parameters']): - vxlan.update({'rebuild_required': {}}) + break # We need to verify that no other VXLAN tunnel is configured when external # mode is in use - Linux Kernel limitation diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index e8f3cc87a..54de467ca 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -51,6 +51,11 @@ sync_search = [ 'script': '/usr/libexec/vyos/conf_mode/interfaces-openvpn.py' }, { + 'keys': ['ca_certificate'], + 'path': ['interfaces', 'sstpc'], + 'script': '/usr/libexec/vyos/conf_mode/interfaces-sstpc.py' + }, + { 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'], 'path': ['vpn', 'ipsec'], 'script': '/usr/libexec/vyos/conf_mode/vpn_ipsec.py' diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 1d016695e..40a32efb3 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -36,7 +36,8 @@ valid_groups = [ 'address_group', 'domain_group', 'network_group', - 'port_group' + 'port_group', + 'interface_group' ] def get_config(config=None): diff --git a/src/conf_mode/protocols_failover.py b/src/conf_mode/protocols_failover.py new file mode 100755 index 000000000..048ba7a89 --- /dev/null +++ b/src/conf_mode/protocols_failover.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# +# 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 +# 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 json + +from pathlib import Path + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag + +airbag.enable() + + +service_name = 'vyos-failover' +service_conf = Path(f'/run/{service_name}.conf') +systemd_service = '/etc/systemd/system/vyos-failover.service' +rt_proto_failover = '/etc/iproute2/rt_protos.d/failover.conf' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['protocols', 'failover'] + failover = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # Set default values only if we set config + if failover.get('route'): + for route, route_config in failover.get('route').items(): + for next_hop, next_hop_config in route_config.get('next_hop').items(): + default_values = defaults(base + ['route']) + failover['route'][route]['next_hop'][next_hop] = dict_merge( + default_values['next_hop'], failover['route'][route]['next_hop'][next_hop]) + + return failover + +def verify(failover): + # bail out early - looks like removal from running config + if not failover: + return None + + if 'route' not in failover: + raise ConfigError(f'Failover "route" is mandatory!') + + for route, route_config in failover['route'].items(): + if not route_config.get('next_hop'): + raise ConfigError(f'Next-hop for "{route}" is mandatory!') + + for next_hop, next_hop_config in route_config.get('next_hop').items(): + if 'interface' not in next_hop_config: + raise ConfigError(f'Interface for route "{route}" next-hop "{next_hop}" is mandatory!') + + if not next_hop_config.get('check'): + raise ConfigError(f'Check target for next-hop "{next_hop}" is mandatory!') + + if 'target' not in next_hop_config['check']: + raise ConfigError(f'Check target for next-hop "{next_hop}" is mandatory!') + + check_type = next_hop_config['check']['type'] + if check_type == 'tcp' and 'port' not in next_hop_config['check']: + raise ConfigError(f'Check port for next-hop "{next_hop}" and type TCP is mandatory!') + + return None + +def generate(failover): + if not failover: + service_conf.unlink(missing_ok=True) + return None + + # Add own rt_proto 'failover' + # Helps to detect all own routes 'proto failover' + with open(rt_proto_failover, 'w') as f: + f.write('111 failover\n') + + # Write configuration file + conf_json = json.dumps(failover, indent=4) + service_conf.write_text(conf_json) + render(systemd_service, 'protocols/systemd_vyos_failover_service.j2', failover) + + return None + +def apply(failover): + if not failover: + call(f'systemctl stop {service_name}.service') + call('ip route flush protocol failover') + else: + call('systemctl daemon-reload') + call(f'systemctl restart {service_name}.service') + call(f'ip route flush protocol failover') + + 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/protocols_static.py b/src/conf_mode/protocols_static.py index 58e202928..3e5ebb805 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -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 @@ -25,12 +25,15 @@ from vyos.configdict import get_dhcp_interfaces from vyos.configdict import get_pppoe_interfaces from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_vrf +from vyos.template import render from vyos.template import render_to_string from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() +config_file = '/etc/iproute2/rt_tables.d/vyos-static.conf' + def get_config(config=None): if config: conf = config @@ -94,6 +97,9 @@ def verify(static): def generate(static): if not static: return None + + # Put routing table names in /etc/iproute2/rt_tables + render(config_file, 'iproute2/static.conf.j2', static) static['new_frr_config'] = render_to_string('frr/staticd.frr.j2', static) return None diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index dbe3be225..0418e8d82 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -15,14 +15,59 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from sys import exit +from netifaces import interfaces from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_interface_exists +from vyos.qos import CAKE +from vyos.qos import DropTail +from vyos.qos import FairQueue +from vyos.qos import FQCodel +from vyos.qos import Limiter +from vyos.qos import NetEm +from vyos.qos import Priority +from vyos.qos import RandomDetect +from vyos.qos import RateLimiter +from vyos.qos import RoundRobin +from vyos.qos import TrafficShaper +from vyos.qos import TrafficShaperHFSC +from vyos.util import call +from vyos.util import dict_search_recursive from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() +map_vyops_tc = { + 'cake' : CAKE, + 'drop_tail' : DropTail, + 'fair_queue' : FairQueue, + 'fq_codel' : FQCodel, + 'limiter' : Limiter, + 'network_emulator' : NetEm, + 'priority_queue' : Priority, + 'random_detect' : RandomDetect, + 'rate_control' : RateLimiter, + 'round_robin' : RoundRobin, + 'shaper' : TrafficShaper, + 'shaper_hfsc' : TrafficShaperHFSC, +} + +def get_shaper(qos, interface_config, direction): + policy_name = interface_config[direction] + # An interface might have a QoS configuration, search the used + # configuration referenced by this. Path will hold the dict element + # referenced by the config, as this will be of sort: + # + # ['policy', 'drop_tail', 'foo-dtail'] <- we are only interested in + # drop_tail as the policy/shaper type + _, path = next(dict_search_recursive(qos, policy_name)) + shaper_type = path[1] + shaper_config = qos['policy'][shaper_type][policy_name] + + return (map_vyops_tc[shaper_type], shaper_config) + def get_config(config=None): if config: conf = config @@ -32,48 +77,167 @@ def get_config(config=None): if not conf.exists(base): return None - qos = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + qos = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) if 'policy' in qos: for policy in qos['policy']: - # CLI mangles - to _ for better Jinja2 compatibility - do we need - # Jinja2 here? - policy = policy.replace('-','_') + # when calling defaults() we need to use the real CLI node, thus we + # need a hyphen + policy_hyphen = policy.replace('_', '-') + + if policy in ['random_detect']: + for rd_name, rd_config in qos['policy'][policy].items(): + # There are eight precedence levels - ensure all are present + # to be filled later down with the appropriate default values + default_precedence = {'precedence' : { '0' : {}, '1' : {}, '2' : {}, '3' : {}, + '4' : {}, '5' : {}, '6' : {}, '7' : {} }} + qos['policy']['random_detect'][rd_name] = dict_merge( + default_precedence, qos['policy']['random_detect'][rd_name]) - default_values = defaults(base + ['policy', policy]) + for p_name, p_config in qos['policy'][policy].items(): + default_values = defaults(base + ['policy', policy_hyphen]) - # class is another tag node which requires individual handling - class_default_values = defaults(base + ['policy', policy, 'class']) - if 'class' in default_values: - del default_values['class'] + if policy in ['priority_queue']: + if 'default' not in p_config: + raise ConfigError(f'QoS policy {p_name} misses "default" class!') + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'class' in default_values: + del default_values['class'] + if 'precedence' in default_values: + del default_values['precedence'] - for p_name, p_config in qos['policy'][policy].items(): qos['policy'][policy][p_name] = dict_merge( default_values, qos['policy'][policy][p_name]) + # class is another tag node which requires individual handling if 'class' in p_config: + default_values = defaults(base + ['policy', policy_hyphen, 'class']) for p_class in p_config['class']: qos['policy'][policy][p_name]['class'][p_class] = dict_merge( - class_default_values, qos['policy'][policy][p_name]['class'][p_class]) + default_values, qos['policy'][policy][p_name]['class'][p_class]) + + if 'precedence' in p_config: + default_values = defaults(base + ['policy', policy_hyphen, 'precedence']) + # precedence values are a bit more complex as they are calculated + # under specific circumstances - thus we need to iterate two times. + # first blend in the defaults from XML / CLI + for precedence in p_config['precedence']: + qos['policy'][policy][p_name]['precedence'][precedence] = dict_merge( + default_values, qos['policy'][policy][p_name]['precedence'][precedence]) + # second calculate defaults based on actual dictionary + for precedence in p_config['precedence']: + max_thr = int(qos['policy'][policy][p_name]['precedence'][precedence]['maximum_threshold']) + if 'minimum_threshold' not in qos['policy'][policy][p_name]['precedence'][precedence]: + qos['policy'][policy][p_name]['precedence'][precedence]['minimum_threshold'] = str( + int((9 + int(precedence)) * max_thr) // 18); + + if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]: + qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \ + str(int(4 * max_thr)) - import pprint - pprint.pprint(qos) return qos def verify(qos): - if not qos: + if not qos or 'interface' not in qos: return None # network policy emulator # reorder rerquires delay to be set + if 'policy' in qos: + for policy_type in qos['policy']: + for policy, policy_config in qos['policy'][policy_type].items(): + # a policy with it's given name is only allowed to exist once + # on the system. This is because an interface selects a policy + # for ingress/egress traffic, and thus there can only be one + # policy with a given name. + # + # We check if the policy name occurs more then once - error out + # if this is true + counter = 0 + for _, path in dict_search_recursive(qos['policy'], policy): + counter += 1 + if counter > 1: + raise ConfigError(f'Conflicting policy name "{policy}", already in use!') + + if 'class' in policy_config: + for cls, cls_config in policy_config['class'].items(): + # bandwidth is not mandatory for priority-queue - that is why this is on the exception list + if 'bandwidth' not in cls_config and policy_type not in ['priority_queue', 'round_robin']: + raise ConfigError(f'Bandwidth must be defined for policy "{policy}" class "{cls}"!') + if 'match' in cls_config: + for match, match_config in cls_config['match'].items(): + if {'ip', 'ipv6'} <= set(match_config): + raise ConfigError(f'Can not use both IPv6 and IPv4 in one match ({match})!') + + if policy_type in ['random_detect']: + if 'precedence' in policy_config: + for precedence, precedence_config in policy_config['precedence'].items(): + max_tr = int(precedence_config['maximum_threshold']) + if {'maximum_threshold', 'minimum_threshold'} <= set(precedence_config): + min_tr = int(precedence_config['minimum_threshold']) + if min_tr >= max_tr: + raise ConfigError(f'Policy "{policy}" uses min-threshold "{min_tr}" >= max-threshold "{max_tr}"!') + + if {'maximum_threshold', 'queue_limit'} <= set(precedence_config): + queue_lim = int(precedence_config['queue_limit']) + if queue_lim < max_tr: + raise ConfigError(f'Policy "{policy}" uses queue-limit "{queue_lim}" < max-threshold "{max_tr}"!') + + if 'default' in policy_config: + if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin']: + raise ConfigError('Bandwidth not defined for default traffic!') + + # we should check interface ingress/egress configuration after verifying that + # the policy name is used only once - this makes the logic easier! + for interface, interface_config in qos['interface'].items(): + verify_interface_exists(interface) + + for direction in ['egress', 'ingress']: + # bail out early if shaper for given direction is not used at all + if direction not in interface_config: + continue + + policy_name = interface_config[direction] + if 'policy' not in qos or list(dict_search_recursive(qos['policy'], policy_name)) == []: + raise ConfigError(f'Selected QoS policy "{policy_name}" does not exist!') + + shaper_type, shaper_config = get_shaper(qos, interface_config, direction) + tmp = shaper_type(interface).get_direction() + if direction not in tmp: + raise ConfigError(f'Selected QoS policy on interface "{interface}" only supports "{tmp}"!') - raise ConfigError('123') return None def generate(qos): + if not qos or 'interface' not in qos: + return None + return None def apply(qos): + # Always delete "old" shapers first + for interface in interfaces(): + # Ignore errors (may have no qdisc) + call(f'tc qdisc del dev {interface} parent ffff:') + call(f'tc qdisc del dev {interface} root') + + if not qos or 'interface' not in qos: + return None + + for interface, interface_config in qos['interface'].items(): + for direction in ['egress', 'ingress']: + # bail out early if shaper for given direction is not used at all + if direction not in interface_config: + continue + + shaper_type, shaper_config = get_shaper(qos, interface_config, direction) + tmp = shaper_type(interface) + tmp.update(shaper_config, direction) + return None if __name__ == '__main__': diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py index 32af31bde..41a1deaa3 100755 --- a/src/conf_mode/service_webproxy.py +++ b/src/conf_mode/service_webproxy.py @@ -28,8 +28,10 @@ from vyos.util import dict_search from vyos.util import write_file from vyos.validate import is_addr_assigned from vyos.xml import defaults +from vyos.base import Warning from vyos import ConfigError from vyos import airbag + airbag.enable() squid_config_file = '/etc/squid/squid.conf' @@ -37,24 +39,57 @@ squidguard_config_file = '/etc/squidguard/squidGuard.conf' squidguard_db_dir = '/opt/vyatta/etc/config/url-filtering/squidguard/db' user_group = 'proxy' -def generate_sg_localdb(category, list_type, role, proxy): + +def check_blacklist_categorydb(config_section): + if 'block_category' in config_section: + for category in config_section['block_category']: + check_categorydb(category) + if 'allow_category' in config_section: + for category in config_section['allow_category']: + check_categorydb(category) + + +def check_categorydb(category: str): + """ + Check if category's db exist + :param category: + :type str: + """ + path_to_cat: str = f'{squidguard_db_dir}/{category}' + if not os.path.exists(f'{path_to_cat}/domains.db') \ + and not os.path.exists(f'{path_to_cat}/urls.db') \ + and not os.path.exists(f'{path_to_cat}/expressions.db'): + Warning(f'DB of category {category} does not exist.\n ' + f'Use [update webproxy blacklists] ' + f'or delete undefined category!') + + +def generate_sg_rule_localdb(category, list_type, role, proxy): + if not category or not list_type or not role: + return None + cat_ = category.replace('-', '_') - if isinstance(dict_search(f'url_filtering.squidguard.{cat_}', proxy), - list): + if role == 'default': + path_to_cat = f'{cat_}' + else: + path_to_cat = f'rule.{role}.{cat_}' + if isinstance( + dict_search(f'url_filtering.squidguard.{path_to_cat}', proxy), + list): # local block databases must be generated "on-the-fly" tmp = { - 'squidguard_db_dir' : squidguard_db_dir, - 'category' : f'{category}-default', - 'list_type' : list_type, - 'rule' : role + 'squidguard_db_dir': squidguard_db_dir, + 'category': f'{category}-{role}', + 'list_type': list_type, + 'rule': role } sg_tmp_file = '/tmp/sg.conf' - db_file = f'{category}-default/{list_type}' - domains = '\n'.join(dict_search(f'url_filtering.squidguard.{cat_}', proxy)) - + db_file = f'{category}-{role}/{list_type}' + domains = '\n'.join( + dict_search(f'url_filtering.squidguard.{path_to_cat}', proxy)) # local file - write_file(f'{squidguard_db_dir}/{category}-default/local', '', + write_file(f'{squidguard_db_dir}/{category}-{role}/local', '', user=user_group, group=user_group) # database input file write_file(f'{squidguard_db_dir}/{db_file}', domains, @@ -64,17 +99,18 @@ def generate_sg_localdb(category, list_type, role, proxy): render(sg_tmp_file, 'squid/sg_acl.conf.j2', tmp, user=user_group, group=user_group) - call(f'su - {user_group} -c "squidGuard -d -c {sg_tmp_file} -C {db_file}"') + call( + f'su - {user_group} -c "squidGuard -d -c {sg_tmp_file} -C {db_file}"') if os.path.exists(sg_tmp_file): os.unlink(sg_tmp_file) - else: # if category is not part of our configuration, clean out the # squidguard lists - tmp = f'{squidguard_db_dir}/{category}-default' + tmp = f'{squidguard_db_dir}/{category}-{role}' if os.path.exists(tmp): - rmtree(f'{squidguard_db_dir}/{category}-default') + rmtree(f'{squidguard_db_dir}/{category}-{role}') + def get_config(config=None): if config: @@ -85,7 +121,8 @@ def get_config(config=None): if not conf.exists(base): return None - proxy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + proxy = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) @@ -110,10 +147,11 @@ def get_config(config=None): default_values = defaults(base + ['cache-peer']) for peer in proxy['cache_peer']: proxy['cache_peer'][peer] = dict_merge(default_values, - proxy['cache_peer'][peer]) + proxy['cache_peer'][peer]) return proxy + def verify(proxy): if not proxy: return None @@ -170,17 +208,30 @@ def generate(proxy): render(squidguard_config_file, 'squid/squidGuard.conf.j2', proxy) cat_dict = { - 'local-block' : 'domains', - 'local-block-keyword' : 'expressions', - 'local-block-url' : 'urls', - 'local-ok' : 'domains', - 'local-ok-url' : 'urls' + 'local-block': 'domains', + 'local-block-keyword': 'expressions', + 'local-block-url': 'urls', + 'local-ok': 'domains', + 'local-ok-url': 'urls' } - for category, list_type in cat_dict.items(): - generate_sg_localdb(category, list_type, 'default', proxy) + if dict_search(f'url_filtering.squidguard', proxy) is not None: + squidgard_config_section = proxy['url_filtering']['squidguard'] + + for category, list_type in cat_dict.items(): + generate_sg_rule_localdb(category, list_type, 'default', proxy) + check_blacklist_categorydb(squidgard_config_section) + + if 'rule' in squidgard_config_section: + for rule in squidgard_config_section['rule']: + rule_config_section = squidgard_config_section['rule'][ + rule] + for category, list_type in cat_dict.items(): + generate_sg_rule_localdb(category, list_type, rule, proxy) + check_blacklist_categorydb(rule_config_section) return None + def apply(proxy): if not proxy: # proxy is removed in the commit @@ -198,6 +249,7 @@ def apply(proxy): call('systemctl restart squid.service') return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index 36dbf155b..e6c7a0ed2 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -22,17 +22,19 @@ from time import sleep from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_source_interface from vyos.template import render from vyos.util import cmd from vyos.util import is_systemd_service_running from vyos.validate import is_addr_assigned +from vyos.validate import is_intf_addr_assigned from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() curlrc_config = r'/etc/curlrc' -ssh_config = r'/etc/ssh/ssh_config' +ssh_config = r'/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf' systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' def get_config(config=None): @@ -68,8 +70,17 @@ def verify(options): if 'ssh_client' in options: config = options['ssh_client'] if 'source_address' in config: + address = config['source_address'] if not is_addr_assigned(config['source_address']): - raise ConfigError('No interface with give address specified!') + raise ConfigError('No interface with address "{address}" configured!') + + if 'source_interface' in config: + verify_source_interface(config) + if 'source_address' in config: + address = config['source_address'] + interface = config['source_interface'] + if not is_intf_addr_assigned(interface, address): + raise ConfigError(f'Address "{address}" not assigned on interface "{interface}"!') return None diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index b79e9847a..04e2f2939 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -622,7 +622,7 @@ def wait_for_vici_socket(timeout=5, sleep_interval=0.1): sleep(sleep_interval) def apply(ipsec): - systemd_service = 'strongswan-starter.service' + systemd_service = 'strongswan.service' if not ipsec: call(f'systemctl stop {systemd_service}') else: diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 27e78db99..65623c2b1 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -58,6 +58,9 @@ default_config_data = { 'ppp_echo_failure' : '3', 'ppp_echo_interval' : '30', 'ppp_echo_timeout': '0', + 'ppp_ipv6_accept_peer_intf_id': False, + 'ppp_ipv6_intf_id': None, + 'ppp_ipv6_peer_intf_id': None, 'radius_server': [], 'radius_acct_inter_jitter': '', 'radius_acct_tmo': '3', @@ -314,6 +317,15 @@ def get_config(config=None): if conf.exists(['ppp-options', 'ipv6']): l2tp['ppp_ipv6'] = conf.return_value(['ppp-options', 'ipv6']) + if conf.exists(['ppp-options', 'ipv6-accept-peer-intf-id']): + l2tp['ppp_ipv6_accept_peer_intf_id'] = True + + if conf.exists(['ppp-options', 'ipv6-intf-id']): + l2tp['ppp_ipv6_intf_id'] = conf.return_value(['ppp-options', 'ipv6-intf-id']) + + if conf.exists(['ppp-options', 'ipv6-peer-intf-id']): + l2tp['ppp_ipv6_peer_intf_id'] = conf.return_value(['ppp-options', 'ipv6-peer-intf-id']) + return l2tp diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 1b4156895..c17cca3bd 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -140,11 +140,9 @@ def verify(vrf): def generate(vrf): - render(config_file, 'vrf/vrf.conf.j2', vrf) + render(config_file, 'iproute2/vrf.conf.j2', vrf) # Render nftables zones config - render(nft_vrf_config, 'firewall/nftables-vrf-zones.j2', vrf) - return None |