diff options
-rw-r--r-- | data/config-mode-dependencies/vyos-1x.json | 4 | ||||
-rw-r--r-- | data/templates/ipsec/ios_profile.j2 | 18 | ||||
-rw-r--r-- | interface-definitions/container.xml.in | 29 | ||||
-rw-r--r-- | python/vyos/template.py | 13 | ||||
-rw-r--r-- | smoketest/config-tests/container-simple | 1 | ||||
-rw-r--r-- | smoketest/configs/container-simple | 5 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_container.py | 5 | ||||
-rwxr-xr-x | src/completion/list_container_sysctl_parameters.sh | 20 | ||||
-rwxr-xr-x | src/conf_mode/container.py | 55 | ||||
-rwxr-xr-x | src/conf_mode/firewall.py | 1 | ||||
-rwxr-xr-x | src/conf_mode/pki.py | 4 | ||||
-rwxr-xr-x | src/op_mode/ikev2_profile_generator.py | 36 |
12 files changed, 148 insertions, 43 deletions
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 13de434bd..3f381169b 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -29,8 +29,10 @@ "https": ["service_https"], "ipsec": ["vpn_ipsec"], "openconnect": ["vpn_openconnect"], + "reverse_proxy": ["load-balancing_reverse-proxy"], "rpki": ["protocols_rpki"], - "sstp": ["vpn_sstp"] + "sstp": ["vpn_sstp"], + "sstpc": ["interfaces_sstpc"] }, "vpn_ipsec": { "nhrp": ["protocols_nhrp"] diff --git a/data/templates/ipsec/ios_profile.j2 b/data/templates/ipsec/ios_profile.j2 index a9ae1c7a9..935acbf8e 100644 --- a/data/templates/ipsec/ios_profile.j2 +++ b/data/templates/ipsec/ios_profile.j2 @@ -48,10 +48,10 @@ <!-- Optional, if it matches the CN of the root CA certificate (not the full subject DN) a certificate request will be sent NOTE: If this is not configured make sure to configure leftsendcert=always on the server, otherwise it won't send its certificate --> <key>ServerCertificateIssuerCommonName</key> - <string>{{ ca_cn }}</string> + <string>{{ ca_common_name }}</string> <!-- Optional, the CN or one of the subjectAltNames of the server certificate to verify it, if not set RemoteIdentifier will be used --> <key>ServerCertificateCommonName</key> - <string>{{ cert_cn }}</string> + <string>{{ cert_common_name }}</string> <!-- The server is authenticated using a certificate --> <key>AuthenticationMethod</key> <string>Certificate</string> @@ -83,24 +83,22 @@ </dict> </dict> </dict> -{% if certs is vyos_defined %} +{% if ca_certificates is vyos_defined %} <!-- This payload is optional but it provides an easy way to install the CA certificate together with the configuration --> -{% for cert in certs %} - <!-- Payload for: {{ cert.ca_cn }} --> +{% for ca in ca_certificates %} + <!-- Payload for: {{ ca.ca_name }} --> <dict> <key>PayloadIdentifier</key> - <string>org.{{ cert.ca_cn | lower | replace(' ', '.') | replace('_', '.') }}</string> + <string>org.{{ ca.ca_name | lower | replace(' ', '.') | replace('_', '.') }}</string> <key>PayloadUUID</key> - <string>{{ cert.ca_cn | generate_uuid4 }}</string> + <string>{{ ca.ca_name | get_uuid }}</string> <key>PayloadType</key> <string>com.apple.security.root</string> <key>PayloadVersion</key> <integer>1</integer> <!-- This is the Base64 (PEM) encoded CA certificate --> <key>PayloadContent</key> - <data> - {{ cert.ca_cert }} - </data> + <data>{{ ca.ca_chain }}</data> </dict> {% endfor %} {% endif %} diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 1ad7215e5..6ea44a6d4 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -71,6 +71,35 @@ <multi/> </properties> </leafNode> + <node name="sysctl"> + <properties> + <help>Configure namespaced kernel parameters of the container</help> + </properties> + <children> + <tagNode name="parameter"> + <properties> + <help>Sysctl key name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_container_sysctl_parameters.sh</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Sysctl key name</description> + </valueHelp> + <constraint> + <validator name="sysctl"/> + </constraint> + </properties> + <children> + <leafNode name="value"> + <properties> + <help>Sysctl configuration value</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> #include <include/generic-description.xml.i> <tagNode name="device"> <properties> diff --git a/python/vyos/template.py b/python/vyos/template.py index fbc5f1456..e8d7ba669 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -525,10 +525,17 @@ def get_esp_ike_cipher(group_config, ike_group=None): return ciphers @register_filter('get_uuid') -def get_uuid(interface): +def get_uuid(seed): """ Get interface IP addresses""" - from uuid import uuid1 - return uuid1() + if seed: + from hashlib import md5 + from uuid import UUID + tmp = md5() + tmp.update(seed.encode('utf-8')) + return str(UUID(tmp.hexdigest())) + else: + from uuid import uuid1 + return uuid1() openvpn_translate = { 'des': 'des-cbc', diff --git a/smoketest/config-tests/container-simple b/smoketest/config-tests/container-simple index cc80ef4cf..5af365cf9 100644 --- a/smoketest/config-tests/container-simple +++ b/smoketest/config-tests/container-simple @@ -11,3 +11,4 @@ set container name c02 allow-host-networks set container name c02 allow-host-pid set container name c02 capability 'sys-time' set container name c02 image 'busybox:stable' +set container name c02 sysctl parameter kernel.msgmax value '8192'
\ No newline at end of file diff --git a/smoketest/configs/container-simple b/smoketest/configs/container-simple index 82983afb7..b98a440b5 100644 --- a/smoketest/configs/container-simple +++ b/smoketest/configs/container-simple @@ -10,6 +10,11 @@ container { allow-host-pid cap-add sys-time image busybox:stable + sysctl { + parameter kernel.msgmax { + value "8192" + } + } } } interfaces { diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index 90f821c60..3dd97a175 100755 --- a/smoketest/scripts/cli/test_container.py +++ b/smoketest/scripts/cli/test_container.py @@ -80,6 +80,7 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['name', cont_name, 'image', cont_image]) self.cli_set(base_path + ['name', cont_name, 'allow-host-networks']) + self.cli_set(base_path + ['name', cont_name, 'sysctl', 'parameter', 'kernel.msgmax', 'value', '4096']) # commit changes self.cli_commit() @@ -91,6 +92,10 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertEqual(process_named_running(PROCESS_NAME), pid) + # verify + tmp = cmd(f'sudo podman exec -it {cont_name} sysctl kernel.msgmax') + self.assertEqual(tmp, 'kernel.msgmax = 4096') + def test_cpu_limit(self): cont_name = 'c2' diff --git a/src/completion/list_container_sysctl_parameters.sh b/src/completion/list_container_sysctl_parameters.sh new file mode 100755 index 000000000..cf8d006e5 --- /dev/null +++ b/src/completion/list_container_sysctl_parameters.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# +# Copyright (C) 2024 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/>. + +declare -a vals +eval "vals=($(/sbin/sysctl -N -a|grep -E '^(fs.mqueue|net)\.|^(kernel.msgmax|kernel.msgmnb|kernel.msgmni|kernel.sem|kernel.shmall|kernel.shmmax|kernel.shmmni|kernel.shm_rmid_forced)$'))" +echo ${vals[@]} +exit 0 diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 3efeb9b40..a969626a9 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -43,6 +43,7 @@ from vyos.template import render from vyos.xml_ref import default_value from vyos import ConfigError from vyos import airbag + airbag.enable() config_containers = '/etc/containers/containers.conf' @@ -50,16 +51,19 @@ config_registry = '/etc/containers/registries.conf' config_storage = '/etc/containers/storage.conf' systemd_unit_path = '/run/systemd/system' + def _cmd(command): if os.path.exists('/tmp/vyos.container.debug'): print(command) return cmd(command) + def network_exists(name): # Check explicit name for network, returns True if network exists c = _cmd(f'podman network ls --quiet --filter name=^{name}$') return bool(c) + # Common functions def get_config(config=None): if config: @@ -86,21 +90,22 @@ def get_config(config=None): # registry is a tagNode with default values - merge the list from # default_values['registry'] into the tagNode variables if 'registry' not in container: - container.update({'registry' : {}}) + container.update({'registry': {}}) default_values = default_value(base + ['registry']) for registry in default_values: - tmp = {registry : {}} + tmp = {registry: {}} container['registry'] = dict_merge(tmp, container['registry']) # Delete container network, delete containers tmp = node_changed(conf, base + ['network']) - if tmp: container.update({'network_remove' : tmp}) + if tmp: container.update({'network_remove': tmp}) tmp = node_changed(conf, base + ['name']) - if tmp: container.update({'container_remove' : tmp}) + if tmp: container.update({'container_remove': tmp}) return container + def verify(container): # bail out early - looks like removal from running config if not container: @@ -125,8 +130,8 @@ def verify(container): # of image upgrade and deletion. image = container_config['image'] if run(f'podman image exists {image}') != 0: - Warning(f'Image "{image}" used in container "{name}" does not exist '\ - f'locally. Please use "add container image {image}" to add it '\ + Warning(f'Image "{image}" used in container "{name}" does not exist ' \ + f'locally. Please use "add container image {image}" to add it ' \ f'to the system! Container "{name}" will not be started!') if 'cpu_quota' in container_config: @@ -167,11 +172,11 @@ def verify(container): # We can not use the first IP address of a network prefix as this is used by podman if ip_address(address) == ip_network(network)[1]: - raise ConfigError(f'IP address "{address}" can not be used for a container, '\ + raise ConfigError(f'IP address "{address}" can not be used for a container, ' \ 'reserved for the container engine!') if cnt_ipv4 > 1 or cnt_ipv6 > 1: - raise ConfigError(f'Only one IP address per address family can be used for '\ + raise ConfigError(f'Only one IP address per address family can be used for ' \ f'container "{name}". {cnt_ipv4} IPv4 and {cnt_ipv6} IPv6 address(es)!') if 'device' in container_config: @@ -186,6 +191,13 @@ def verify(container): if not os.path.exists(source): raise ConfigError(f'Device "{dev}" source path "{source}" does not exist!') + if 'sysctl' in container_config and 'parameter' in container_config['sysctl']: + for var, cfg in container_config['sysctl']['parameter'].items(): + if 'value' not in cfg: + raise ConfigError(f'sysctl parameter {var} has no value assigned!') + if var.startswith('net.') and 'allow_host_networks' in container_config: + raise ConfigError(f'sysctl parameter {var} cannot be set when using host networking!') + if 'environment' in container_config: for var, cfg in container_config['environment'].items(): if 'value' not in cfg: @@ -219,7 +231,8 @@ def verify(container): # Can not set both allow-host-networks and network at the same time if {'allow_host_networks', 'network'} <= set(container_config): - raise ConfigError(f'"allow-host-networks" and "network" for "{name}" cannot be both configured at the same time!') + raise ConfigError( + f'"allow-host-networks" and "network" for "{name}" cannot be both configured at the same time!') # gid cannot be set without uid if 'gid' in container_config and 'uid' not in container_config: @@ -235,8 +248,10 @@ def verify(container): raise ConfigError(f'prefix for network "{network}" must be defined!') for prefix in network_config['prefix']: - if is_ipv4(prefix): v4_prefix += 1 - elif is_ipv6(prefix): v6_prefix += 1 + if is_ipv4(prefix): + v4_prefix += 1 + elif is_ipv6(prefix): + v6_prefix += 1 if v4_prefix > 1: raise ConfigError(f'Only one IPv4 prefix can be defined for network "{network}"!') @@ -262,6 +277,7 @@ def verify(container): return None + def generate_run_arguments(name, container_config): image = container_config['image'] cpu_quota = container_config['cpu_quota'] @@ -269,6 +285,12 @@ def generate_run_arguments(name, container_config): shared_memory = container_config['shared_memory'] restart = container_config['restart'] + # Add sysctl options + sysctl_opt = '' + if 'sysctl' in container_config and 'parameter' in container_config['sysctl']: + for k, v in container_config['sysctl']['parameter'].items(): + sysctl_opt += f" --sysctl {k}={v['value']}" + # Add capability options. Should be in uppercase capabilities = '' if 'capability' in container_config: @@ -341,7 +363,7 @@ def generate_run_arguments(name, container_config): if 'allow_host_pid' in container_config: host_pid = '--pid host' - container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} ' \ + container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} {sysctl_opt} ' \ f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid} {host_pid}' @@ -375,6 +397,7 @@ def generate_run_arguments(name, container_config): return f'{container_base_cmd} --no-healthcheck --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip() + def generate(container): # bail out early - looks like removal from running config if not container: @@ -387,7 +410,7 @@ def generate(container): for network, network_config in container['network'].items(): tmp = { 'name': network, - 'id' : sha256(f'{network}'.encode()).hexdigest(), + 'id': sha256(f'{network}'.encode()).hexdigest(), 'driver': 'bridge', 'network_interface': f'pod-{network}', 'subnets': [], @@ -399,7 +422,7 @@ def generate(container): } } for prefix in network_config['prefix']: - net = {'subnet' : prefix, 'gateway' : inc_ip(prefix, 1)} + net = {'subnet': prefix, 'gateway': inc_ip(prefix, 1)} tmp['subnets'].append(net) if is_ipv6(prefix): @@ -418,11 +441,12 @@ def generate(container): file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') run_args = generate_run_arguments(name, container_config) - render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args,}, + render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args, }, formater=lambda _: _.replace(""", '"').replace("'", "'")) return None + def apply(container): # Delete old containers if needed. We can't delete running container # Option "--force" allows to delete containers with any status @@ -485,6 +509,7 @@ def apply(container): return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 4c289b921..ec6b86ef2 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -17,7 +17,6 @@ import os import re -from glob import glob from sys import exit from vyos.base import Warning diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 8deec0e85..f37cac524 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -67,6 +67,10 @@ sync_search = [ 'path': ['interfaces', 'sstpc'], }, { + 'keys': ['certificate', 'ca_certificate'], + 'path': ['load_balancing', 'reverse_proxy'], + }, + { 'keys': ['key'], 'path': ['protocols', 'rpki', 'cache'], }, diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py index 4ac4fb14a..169a15840 100755 --- a/src/op_mode/ikev2_profile_generator.py +++ b/src/op_mode/ikev2_profile_generator.py @@ -21,6 +21,10 @@ from socket import getfqdn from cryptography.x509.oid import NameOID from vyos.configquery import ConfigTreeQuery +from vyos.pki import CERT_BEGIN +from vyos.pki import CERT_END +from vyos.pki import find_chain +from vyos.pki import encode_certificate from vyos.pki import load_certificate from vyos.template import render_to_string from vyos.utils.io import ask_input @@ -146,27 +150,33 @@ data['rfqdn'] = '.'.join(tmp) pki = conf.get_config_dict(pki_base, get_first_key=True) cert_name = data['authentication']['x509']['certificate'] -data['certs'] = [] +cert_data = load_certificate(pki['certificate'][cert_name]['certificate']) +data['cert_common_name'] = cert_data.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value +data['ca_common_name'] = cert_data.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value +data['ca_certificates'] = [] -for ca_name in data['authentication']['x509']['ca_certificate']: - tmp = {} - ca_cert = load_certificate(pki['ca'][ca_name]['certificate']) - cert = load_certificate(pki['certificate'][cert_name]['certificate']) - - - tmp['ca_cn'] = ca_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value - tmp['cert_cn'] = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value - tmp['ca_cert'] = conf.value(pki_base + ['ca', ca_name, 'certificate']) - - data['certs'].append(tmp) +loaded_ca_certs = {load_certificate(c['certificate']) + for c in pki['ca'].values()} if 'ca' in pki else {} +for ca_name in data['authentication']['x509']['ca_certificate']: + loaded_ca_cert = load_certificate(pki['ca'][ca_name]['certificate']) + ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) + for ca in ca_full_chain: + tmp = { + 'ca_name' : ca.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value, + 'ca_chain' : encode_certificate(ca).replace(CERT_BEGIN, '').replace(CERT_END, '').replace('\n', ''), + } + data['ca_certificates'].append(tmp) + +# Remove duplicate list entries for CA certificates, as they are added by their common name +# https://stackoverflow.com/a/9427216 +data['ca_certificates'] = [dict(t) for t in {tuple(d.items()) for d in data['ca_certificates']}] esp_proposals = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'], key_mangling=('-', '_'), get_first_key=True) ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'], key_mangling=('-', '_'), get_first_key=True) - # This script works only for Apple iOS/iPadOS and Windows. Both operating systems # have different limitations thus we load the limitations based on the operating # system used. |