diff options
-rw-r--r-- | data/templates/dynamic-dns/ddclient.conf.j2 | 2 | ||||
-rw-r--r-- | data/templates/ocserv/ocserv_config.j2 | 13 | ||||
-rw-r--r-- | data/templates/rsyslog/rsyslog.conf.j2 | 2 | ||||
-rw-r--r-- | interface-definitions/load-balancing-wan.xml.in | 1 | ||||
-rw-r--r-- | interface-definitions/vpn-openconnect.xml.in | 52 | ||||
-rw-r--r-- | python/vyos/base.py | 3 | ||||
-rw-r--r-- | python/vyos/ifconfig/ethernet.py | 9 | ||||
-rw-r--r-- | python/vyos/util.py | 7 | ||||
-rw-r--r-- | python/vyos/utils/dict.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_dns_dynamic.py | 18 | ||||
-rwxr-xr-x | src/conf_mode/dynamic_dns.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/high-availability.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-ethernet.py | 9 | ||||
-rwxr-xr-x | src/conf_mode/vpn_openconnect.py | 14 | ||||
-rwxr-xr-x | src/op_mode/interfaces.py | 34 | ||||
-rw-r--r-- | src/tests/test_util.py | 16 |
16 files changed, 145 insertions, 50 deletions
diff --git a/data/templates/dynamic-dns/ddclient.conf.j2 b/data/templates/dynamic-dns/ddclient.conf.j2 index c2c9b1dd6..e8ef5ac90 100644 --- a/data/templates/dynamic-dns/ddclient.conf.j2 +++ b/data/templates/dynamic-dns/ddclient.conf.j2 @@ -34,7 +34,9 @@ zone={{ config.zone }} # DynDNS provider configuration for {{ service }}, {{ dns_record }} protocol={{ config.protocol }}, max-interval=28d, +{% if config.login is vyos_defined %} login={{ config.login }}, +{% endif %} password='{{ config.password }}', {% if config.server is vyos_defined %} server={{ config.server }}, diff --git a/data/templates/ocserv/ocserv_config.j2 b/data/templates/ocserv/ocserv_config.j2 index aa1073bca..1401b8b26 100644 --- a/data/templates/ocserv/ocserv_config.j2 +++ b/data/templates/ocserv/ocserv_config.j2 @@ -16,6 +16,12 @@ acct = "radius [config=/run/ocserv/radiusclient.conf]" {% if "radius" in authentication.mode %} auth = "radius [config=/run/ocserv/radiusclient.conf{{ ',groupconfig=true' if authentication.radius.groupconfig is vyos_defined else '' }}]" +{% if authentication.identity_based_config.disabled is not vyos_defined %} +{% if "group" in authentication.identity_based_config.mode %} +config-per-group = {{ authentication.identity_based_config.directory }} +default-group-config = {{ authentication.identity_based_config.default_config }} +{% endif %} +{% endif %} {% elif "local" in authentication.mode %} {% if authentication.mode.local == "password-otp" %} auth = "plain[passwd=/run/ocserv/ocpasswd,otp=/run/ocserv/users.oath]" @@ -28,6 +34,13 @@ auth = "plain[/run/ocserv/ocpasswd]" auth = "plain[/run/ocserv/ocpasswd]" {% endif %} +{% if "identity_based_config" in authentication %} +{% if "user" in authentication.identity_based_config.mode %} +config-per-user = {{ authentication.identity_based_config.directory }} +default-user-config = {{ authentication.identity_based_config.default_config }} +{% endif %} +{% endif %} + {% if ssl.certificate is vyos_defined %} server-cert = /run/ocserv/cert.pem server-key = /run/ocserv/cert.key diff --git a/data/templates/rsyslog/rsyslog.conf.j2 b/data/templates/rsyslog/rsyslog.conf.j2 index 0460ae5f0..5352fc367 100644 --- a/data/templates/rsyslog/rsyslog.conf.j2 +++ b/data/templates/rsyslog/rsyslog.conf.j2 @@ -49,7 +49,7 @@ $outchannel {{ file_name }},/var/log/user/{{ file_name }},{{ file_options.archiv {% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level) %} {% endfor %} {% if host_options.protocol is vyos_defined('tcp') %} -{% if host_options.oct_count is vyos_defined %} +{% if host_options.format.octet_counted is vyos_defined %} {{ tmp | join(';') }} @@(o){{ host_name | bracketize_ipv6 }}:{{ host_options.port }};RSYSLOG_SyslogProtocol23Format {% else %} {{ tmp | join(';') }} @@{{ host_name | bracketize_ipv6 }}:{{ host_options.port }} diff --git a/interface-definitions/load-balancing-wan.xml.in b/interface-definitions/load-balancing-wan.xml.in index 3a2c111ac..c12cab22a 100644 --- a/interface-definitions/load-balancing-wan.xml.in +++ b/interface-definitions/load-balancing-wan.xml.in @@ -179,6 +179,7 @@ <regex>(ping|ttl|user-defined)</regex> </constraint> </properties> + <defaultValue>ping</defaultValue> </leafNode> </children> </tagNode> diff --git a/interface-definitions/vpn-openconnect.xml.in b/interface-definitions/vpn-openconnect.xml.in index a426f604d..75c64a99a 100644 --- a/interface-definitions/vpn-openconnect.xml.in +++ b/interface-definitions/vpn-openconnect.xml.in @@ -71,6 +71,58 @@ </leafNode> </children> </node> + <node name="identity-based-config"> + <properties> + <help>Include configuration file by username or RADIUS group attribute</help> + </properties> + <children> + #include <include/generic-disable-node.xml.i> + <leafNode name="mode"> + <properties> + <help>Select per user or per group configuration file - ignored if authentication group is configured</help> + <completionHelp> + <list>user group</list> + </completionHelp> + <valueHelp> + <format>user</format> + <description>Match configuration file on username</description> + </valueHelp> + <valueHelp> + <format>group</format> + <description>Match RADIUS response class attribute as file name</description> + </valueHelp> + <constraint> + <regex>(user|group)</regex> + </constraint> + <constraintErrorMessage>Invalid mode, must be either user or group</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="directory"> + <properties> + <help>Directory to containing configuration files</help> + <valueHelp> + <format>path</format> + <description>Path to configuration directory, must be under /config/auth</description> + </valueHelp> + <constraint> + <validator name="file-path" argument="--directory --parent-dir /config/auth --strict"/> + </constraint> + </properties> + </leafNode> + <leafNode name="default-config"> + <properties> + <help>Default configuration if discrete config could not be found</help> + <valueHelp> + <format>filename</format> + <description>Default configuration filename, must be under /config/auth</description> + </valueHelp> + <constraint> + <validator name="file-path" argument="--file --parent-dir /config/auth --strict"/> + </constraint> + </properties> + </leafNode> + </children> + </node> <leafNode name="group"> <properties> <help>Group that a client is allowed to select (from a list). Maps to RADIUS Class attribute.</help> diff --git a/python/vyos/base.py b/python/vyos/base.py index c1acfd060..9b93cb2f2 100644 --- a/python/vyos/base.py +++ b/python/vyos/base.py @@ -1,4 +1,4 @@ -# Copyright 2018-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2018-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -41,6 +41,7 @@ class BaseWarning: isfirstmessage = False initial_indent = self.standardindent print(f'{mes}') + print('') class Warning(): diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 6a49c022a..30bea3b86 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -374,10 +374,11 @@ class EthernetIf(Interface): self.set_tso(dict_search('offload.tso', config) != None) # Set physical interface speed and duplex - if {'speed', 'duplex'} <= set(config): - speed = config.get('speed') - duplex = config.get('duplex') - self.set_speed_duplex(speed, duplex) + if 'speed_duplex_changed' in config: + if {'speed', 'duplex'} <= set(config): + speed = config.get('speed') + duplex = config.get('duplex') + self.set_speed_duplex(speed, duplex) # Set interface ring buffer if 'ring_buffer' in config: diff --git a/python/vyos/util.py b/python/vyos/util.py index 0593184cc..d5330db13 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -1146,13 +1146,6 @@ def sysctl_write(name, value): return True return False -# approach follows a discussion in: -# https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case -def camel_to_snake_case(name: str) -> str: - pattern = r'\d+|[A-Z]?[a-z]+|\W|[A-Z]{2,}(?=[A-Z][a-z]|\d|\W|$)' - words = re.findall(pattern, name) - return '_'.join(map(str.lower, words)) - def load_as_module(name: str, path: str): import importlib.util diff --git a/python/vyos/utils/dict.py b/python/vyos/utils/dict.py index 4afc9f54e..7c93deef6 100644 --- a/python/vyos/utils/dict.py +++ b/python/vyos/utils/dict.py @@ -253,4 +253,4 @@ def check_mutually_exclusive_options(d, keys, required=False): raise ValueError(f"Options {orig_keys} are mutually-exclusive but more than one of them is present: {orig_present_keys}") if required and (len(present_keys) < 1): - raise ValueError(f"At least one of the following options is required: {orig_present_keys}") + raise ValueError(f"At least one of the following options is required: {orig_keys}") diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 57705e26f..a3aa41f94 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -45,22 +45,32 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_commit() def test_dyndns_service(self): + from itertools import product ddns = ['interface', interface, 'service'] + users = [None, 'vyos_user'] services = ['cloudflare', 'afraid', 'dyndns', 'zoneedit'] - for service in services: - user = 'vyos_user' + for user, service in product(users, services): password = 'vyos_pass' zone = 'vyos.io' self.cli_delete(base_path) self.cli_set(base_path + ddns + [service, 'host-name', hostname]) - self.cli_set(base_path + ddns + [service, 'login', user]) + if user is not None: + self.cli_set(base_path + ddns + [service, 'login', user]) self.cli_set(base_path + ddns + [service, 'password', password]) self.cli_set(base_path + ddns + [service, 'zone', zone]) # commit changes if service == 'cloudflare': self.cli_commit() + elif user is None: + # not set user is only allowed for cloudflare + with self.assertRaises(ConfigSessionError): + # remove zone to test not set user + self.cli_delete(base_path + ddns + [service, 'zone', 'vyos.io']) + self.cli_commit() + # this case is fininshed, user not set is not allowed when service isn't cloudflare + continue else: # zone option only works on cloudflare, an exception is raised # for all others @@ -72,7 +82,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # we can only read the configuration file when we operate as 'root' protocol = get_config_value('protocol') - login = get_config_value('login') + login = None if user is None else get_config_value('login') pwd = get_config_value('password') # some services need special treatment diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 06a2f7e15..426e3d693 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -108,7 +108,8 @@ def verify(dyndns): raise ConfigError(f'"host-name" {error_msg}') if 'login' not in config: - raise ConfigError(f'"login" (username) {error_msg}') + if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'): + raise ConfigError(f'"login" (username) {error_msg}, unless using CloudFlare') if 'password' not in config: raise ConfigError(f'"password" {error_msg}') diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index 7a63f5b4b..e18b426b1 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -21,6 +21,7 @@ from ipaddress import ip_interface from ipaddress import IPv4Interface from ipaddress import IPv6Interface +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.ifconfig.vrrp import VRRP @@ -107,11 +108,16 @@ def verify(ha): raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"') if 'health_check' in group_config: + health_check_types = ["script", "ping"] from vyos.utils.dict import check_mutually_exclusive_options try: - check_mutually_exclusive_options(group_config["health_check"], ["script", "ping"], required=True) + check_mutually_exclusive_options(group_config["health_check"], health_check_types, required=True) except ValueError as e: - raise ConfigError(f'Health check config is incorrect in VRRP group "{group}": {e}') + Warning(f'Health check configuration for VRRP group "{group}" will remain unused ' \ + f'until it has one of the following options: {health_check_types}') + # XXX: health check has default options so we need to remove it + # to avoid generating useless config statements in keepalived.conf + del group_config["health_check"] # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction # We also need to make sure VRID is not used twice on the same interface with the diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index b49c945cd..31cfab368 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -22,6 +22,7 @@ from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_eapol @@ -66,11 +67,17 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) base = ['interfaces', 'ethernet'] - _, ethernet = get_interface_dict(conf, base) + ifname, ethernet = get_interface_dict(conf, base) if 'deleted' not in ethernet: if pki: ethernet['pki'] = pki + tmp = is_node_changed(conf, base + [ifname, 'speed']) + if tmp: ethernet.update({'speed_duplex_changed': {}}) + + tmp = is_node_changed(conf, base + [ifname, 'duplex']) + if tmp: ethernet.update({'speed_duplex_changed': {}}) + return ethernet def verify(ethernet): diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 68da70d7d..83021a3e6 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -17,6 +17,7 @@ import os from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.pki import wrap_certificate @@ -173,6 +174,19 @@ def verify(ocserv): users_wo_pswd.append(user) if users_wo_pswd: raise ConfigError(f'password required for users:\n{users_wo_pswd}') + + # Validate that if identity-based-config is configured all child config nodes are set + if 'identity_based_config' in ocserv["authentication"]: + if 'disabled' not in ocserv["authentication"]["identity_based_config"]: + Warning("Identity based configuration files is a 3rd party addition. Use at your own risk, this might break the ocserv daemon!") + if 'mode' not in ocserv["authentication"]["identity_based_config"]: + raise ConfigError('OpenConnect radius identity-based-config enabled but mode not selected') + elif 'group' in ocserv["authentication"]["identity_based_config"]["mode"] and "radius" not in ocserv["authentication"]["mode"]: + raise ConfigError('OpenConnect config-per-group must be used with radius authentication') + if 'directory' not in ocserv["authentication"]["identity_based_config"]: + raise ConfigError('OpenConnect identity-based-config enabled but directory not set') + if 'default_config' not in ocserv["authentication"]["identity_based_config"]: + raise ConfigError('OpenConnect identity-based-config enabled but default-config not set') else: raise ConfigError('openconnect authentication mode required') else: diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py index dd87b5901..f38b95a71 100755 --- a/src/op_mode/interfaces.py +++ b/src/op_mode/interfaces.py @@ -277,6 +277,10 @@ def _get_counter_data(ifname: typing.Optional[str], res_intf['rx_bytes'] = _get_counter_val(cache['rx_bytes'], stats['rx_bytes']) res_intf['tx_packets'] = _get_counter_val(cache['tx_packets'], stats['tx_packets']) res_intf['tx_bytes'] = _get_counter_val(cache['tx_bytes'], stats['tx_bytes']) + res_intf['rx_dropped'] = _get_counter_val(cache['rx_dropped'], stats['rx_dropped']) + res_intf['tx_dropped'] = _get_counter_val(cache['tx_dropped'], stats['tx_dropped']) + res_intf['rx_over_errors'] = _get_counter_val(cache['rx_over_errors'], stats['rx_over_errors']) + res_intf['tx_carrier_errors'] = _get_counter_val(cache['tx_carrier_errors'], stats['tx_carrier_errors']) ret.append(res_intf) @@ -368,19 +372,23 @@ def _format_show_summary(data): @catch_broken_pipe def _format_show_counters(data: list): - formatting = '%-12s %10s %10s %10s %10s' - print(formatting % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes')) - - for intf in data: - print(formatting % ( - intf['ifname'], - intf['rx_packets'], - intf['rx_bytes'], - intf['tx_packets'], - intf['tx_bytes'] - )) - - return 0 + data_entries = [] + for entry in data: + interface = entry.get('ifname') + rx_packets = entry.get('rx_packets') + rx_bytes = entry.get('rx_bytes') + tx_packets = entry.get('tx_packets') + tx_bytes = entry.get('tx_bytes') + rx_dropped = entry.get('rx_dropped') + tx_dropped = entry.get('tx_dropped') + rx_errors = entry.get('rx_over_errors') + tx_errors = entry.get('tx_carrier_errors') + data_entries.append([interface, rx_packets, rx_bytes, tx_packets, tx_bytes, rx_dropped, tx_dropped, rx_errors, tx_errors]) + + headers = ['Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes', 'Rx Dropped', 'Tx Dropped', 'Rx Errors', 'Tx Errors'] + output = tabulate(data_entries, headers, numalign="left") + print (output) + return output def show(raw: bool, intf_name: typing.Optional[str], intf_type: typing.Optional[str], diff --git a/src/tests/test_util.py b/src/tests/test_util.py index d8b2b7940..473052bef 100644 --- a/src/tests/test_util.py +++ b/src/tests/test_util.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 @@ -26,17 +26,3 @@ class TestVyOSUtil(TestCase): def test_sysctl_read(self): self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1') - - def test_camel_to_snake_case(self): - self.assertEqual(camel_to_snake_case('ConnectionTimeout'), - 'connection_timeout') - self.assertEqual(camel_to_snake_case('connectionTimeout'), - 'connection_timeout') - self.assertEqual(camel_to_snake_case('TCPConnectionTimeout'), - 'tcp_connection_timeout') - self.assertEqual(camel_to_snake_case('TCPPort'), - 'tcp_port') - self.assertEqual(camel_to_snake_case('UseHTTPProxy'), - 'use_http_proxy') - self.assertEqual(camel_to_snake_case('CustomerID'), - 'customer_id') |