diff --git a/Makefile b/Makefile index d32c5826..ffcef6e0 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ JAIL_NET?=16 MYPYPATH = $(shell pwd)/.travis/mypy-stubs deps: - if [ "`uname`" = "FreeBSD" ]; then pkg install -q -y libucl py36-cython rsync python36 py36-libzfs py36-sysctl; fi + if [ "`uname`" = "FreeBSD" ]; then pkg install -q -y libucl py36-cython rsync python36 py36-libzfs; fi python3.6 -m ensurepip python3.6 -m pip install -Ur requirements.txt install: deps diff --git a/libioc/Config/Jail/BaseConfig.py b/libioc/Config/Jail/BaseConfig.py index e886b59b..47902f4a 100644 --- a/libioc/Config/Jail/BaseConfig.py +++ b/libioc/Config/Jail/BaseConfig.py @@ -32,6 +32,7 @@ import libioc.errors import libioc.helpers import libioc.helpers_object +import libioc.JailParams # mypy import libioc.Logger @@ -815,6 +816,8 @@ def _is_user_property(self, key: str) -> bool: def _is_known_property(self, key: str) -> bool: """Return True when the key is a known config property.""" + if self._is_known_jail_param(key): + return True if key in libioc.Config.Jail.Defaults.DEFAULTS.keys(): return True # key is default if f"_set_{key}" in dict.__dir__(self): @@ -827,6 +830,9 @@ def _is_known_property(self, key: str) -> bool: return True # user.* property return False + def _is_known_jail_param(self, key: str) -> bool: + return key in libioc.JailParams.HostJailParams() + @property def _sorted_user_properties(self) -> typing.List[str]: return sorted(self.keys()) diff --git a/libioc/Config/Jail/Defaults.py b/libioc/Config/Jail/Defaults.py index 0ddb3cce..d7cdeb72 100644 --- a/libioc/Config/Jail/Defaults.py +++ b/libioc/Config/Jail/Defaults.py @@ -94,7 +94,7 @@ "mount_procfs": "0", "mount_devfs": "1", "mount_fdescfs": "0", - "securelevel": "2", + "securelevel": 2, "tags": [], "template": False, "jail_zfs": False, diff --git a/libioc/Firewall.py b/libioc/Firewall.py index 7a4dbd5a..fd15ab61 100644 --- a/libioc/Firewall.py +++ b/libioc/Firewall.py @@ -26,7 +26,7 @@ import typing import shlex -import sysctl +import freebsd_sysctl import libioc.helpers import libioc.helpers_object @@ -58,15 +58,30 @@ def _required_sysctl_properties(self) -> typing.Dict[str, int]: def ensure_firewall_enabled(self) -> None: """Raise an FirewallDisabled exception if the firewall is disabled.""" requirements = self._required_sysctl_properties - requirement_keys = list(requirements.keys()) - for item in sysctl.filter("net"): - if item.name in requirement_keys: - if item.value != requirements[item.name]: - state = ("en" if (item.value == 0) else "dis") + "abled" - raise libioc.errors.FirewallDisabled( - hint=f"sysctl {item.name} is not {state}", - logger=self.logger + + if len(requirements) == 0: + return + + try: + current = "not found" + for key in requirements: + expected = requirements[key] + current = freebsd_sysctl.Sysctl(key).value + if current == expected: + raise ValueError( + f"Invalid Sysctl {key}: " + f"{current} found, but expected: {expected}" ) + return + except Exception: + # an IocageException is raised in the next step at the right level + pass + + hint = f"sysctl {key} is expected to be {expected}, but was {current}" + raise libioc.errors.FirewallDisabled( + hint=hint, + logger=self.logger + ) def delete_rule( self, diff --git a/libioc/Host.py b/libioc/Host.py index a10007e6..420c7f7e 100644 --- a/libioc/Host.py +++ b/libioc/Host.py @@ -27,7 +27,7 @@ import os import platform import re -import sysctl +import freebsd_sysctl import libzfs @@ -54,7 +54,7 @@ class HostGenerator: _devfs: libioc.DevfsRules.DevfsRules _defaults: libioc.Resource.DefaultResource - _defaults_initialized = False + __user_provided_defaults: typing.Optional[libioc.Resource.DefaultResource] __hostid: str releases_dataset: libzfs.ZFSDataset datasets: libioc.Datasets.Datasets @@ -99,13 +99,12 @@ def __init__( zfs=self.zfs ) - self._init_defaults(defaults) - - def _init_defaults( - self, - defaults: typing.Optional[libioc.Resource.DefaultResource]=None - ) -> None: + # this variable stores user provided defaults until the property + # was initialized and accessed the first time + self.__user_provided_defaults = defaults + def __init_defaults(self) -> None: + defaults = self.__user_provided_defaults if defaults is not None: self._defaults = defaults else: @@ -114,6 +113,8 @@ def _init_defaults( logger=self.logger, zfs=self.zfs ) + self.__user_provided_defaults = None + self._defaults.read_config() @property def id(self) -> str: @@ -131,9 +132,12 @@ def id(self) -> str: @property def defaults(self) -> 'libioc.Resource.DefaultResource': """Return the lazy-loaded defaults.""" - if self._defaults_initialized is False: - self._defaults.read_config() - self._defaults_initialized = True + try: + return self._defaults + except AttributeError: + pass + + self.__init_defaults() return self._defaults @property @@ -187,8 +191,11 @@ def processor(self) -> str: @property def ipfw_enabled(self) -> bool: """Return True if ipfw is enabled on the host system.""" - _sysctl = sysctl.filter("net.inet.ip.fw.enable") - return ((len(_sysctl) == 1) and (_sysctl[0].value == 1)) + try: + firewall_enabled = freebsd_sysctl.Sysctl("net.inet.ip.fw.enable") + return (firewall_enabled.value == 1) is True + except Exception: + return False class Host(HostGenerator): diff --git a/libioc/Jail.py b/libioc/Jail.py index eb735186..f7ce874e 100644 --- a/libioc/Jail.py +++ b/libioc/Jail.py @@ -30,6 +30,7 @@ import shutil import libzfs +import freebsd_sysctl import libioc.Types import libioc.errors @@ -1653,93 +1654,69 @@ def devfs_ruleset(self) -> libioc.DevfsRules.DevfsRuleset: ruleset_line_position = self.host.devfs.index(devfs_ruleset) return self.host.devfs[ruleset_line_position].number - @property - def _launch_command(self) -> typing.List[str]: - - command = ["/usr/sbin/jail", "-c"] + @staticmethod + def __get_launch_command(jail_args: typing.List[str]) -> typing.List[str]: + return ["/usr/sbin/jail", "-c"] + jail_args - if self.config["vnet"]: - command.append("vnet") - else: + @property + def _launch_args(self) -> typing.List[str]: + config = self.config + vnet = (config["vnet"] is True) + value: str + jail_param_args: typing.List[str] = [] + for sysctl_name, sysctl in libioc.JailParams.JailParams().items(): + if sysctl.ctl_type == freebsd_sysctl.types.NODE: + # skip NODE + continue - if self.config["ip4_addr"] is not None: - ip4_addr = self.config["ip4_addr"] - command += [ - f"ip4.addr={ip4_addr}", - f"ip4.saddrsel={self.config['ip4_saddrsel']}", - f"ip4={self.config['ip4']}", - ] + if sysctl_name == "security.jail.param.devfs_ruleset": + value = str(self.devfs_ruleset) + elif sysctl_name == "security.jail.param.path": + value = self.root_dataset.mountpoint + elif sysctl_name == "security.jail.param.name": + value = self.identifier + elif sysctl_name == "security.jail.param.allow.mount.zfs": + value = str(self._allow_mount_zfs) + elif sysctl_name == "security.jail.param.vnet": + if vnet is False: + # vnet is only used when explicitly enabled + # (friendly to Kernels without VIMAGE support) + continue + value = "vnet" + elif vnet and sysctl_name.startswith("security.jail.param.ip"): + continue + else: + config_property_name = sysctl.iocage_name + if self.config._is_known_property(config_property_name): + value = config[config_property_name] + else: + continue - if self.config['ip6_addr'] is not None: - ip6_addr = self.config['ip6_addr'] - command += [ - f"ip6.addr={ip6_addr}", - f"ip6.saddrsel={self.config['ip6_saddrsel']}", - f"ip6={self.config['ip6']}", - ] + sysctl.value = value + jail_param_args.append(str(sysctl)) - command += [ - f"name={self.identifier}", - f"host.hostname={self.config['host_hostname']}", - f"host.domainname={self.config['host_domainname']}", - f"path={self.root_dataset.mountpoint}", - f"securelevel={self._get_value('securelevel')}", - f"host.hostuuid={self.name}", - f"devfs_ruleset={self.devfs_ruleset}", - f"enforce_statfs={self._get_value('enforce_statfs')}", - f"children.max={self._get_value('children_max')}", - f"allow.set_hostname={self._get_value('allow_set_hostname')}", - f"allow.sysvipc={self._get_value('allow_sysvipc')}", + jail_args = [ + f"exec.timeout={self._get_value('exec_timeout')}", + f"stop.timeout={self._get_value('stop_timeout')}", f"exec.prestart=\"{self.get_hook_script_path('prestart')}\"", f"exec.prestop=\"{self.get_hook_script_path('prestop')}\"", f"exec.poststop=\"{self.get_hook_script_path('poststop')}\"", - f"exec.jail_user={self._get_value('exec_jail_user')}" - ] - - if self.host.userland_version > 10.3: - command += [ - f"sysvmsg={self._get_value('sysvmsg')}", - f"sysvsem={self._get_value('sysvsem')}", - f"sysvshm={self._get_value('sysvshm')}" - ] - - command += [ - f"allow.raw_sockets={self._get_value('allow_raw_sockets')}", - f"allow.chflags={self._get_value('allow_chflags')}", - f"allow.mount={self._allow_mount}", - f"allow.mount.devfs={self._get_value('allow_mount_devfs')}", - f"allow.mount.nullfs={self._get_value('allow_mount_nullfs')}", - f"allow.mount.procfs={self._get_value('allow_mount_procfs')}", - f"allow.mount.fdescfs={self._get_value('allow_mount_fdescfs')}", - f"allow.mount.zfs={self._allow_mount_zfs}", - f"allow.quotas={self._get_value('allow_quotas')}", - f"allow.socket_af={self._get_value('allow_socket_af')}", - f"exec.timeout={self._get_value('exec_timeout')}", - f"stop.timeout={self._get_value('stop_timeout')}", + f"exec.jail_user={self._get_value('exec_jail_user')}", f"mount.fstab={self.fstab.path}", - f"mount.devfs={self._get_value('mount_devfs')}" + f"mount.devfs={self._get_value('mount_devfs')}", + "allow.dying" ] - if self.config["allow_vmm"] is True: - command.append("allow.vmm=1") - - if self.host.userland_version > 9.3: - command += [ - f"mount.fdescfs={self._get_value('mount_fdescfs')}", - f"allow.mount.tmpfs={self._get_value('allow_mount_tmpfs')}" - ] - - command += ["allow.dying"] - return command + return jail_param_args + jail_args def _launch_persistent_jail( self, passthru: bool ) -> libioc.helpers.CommandOutput: - command = self._launch_command + [ + command = self.__get_launch_command(self._launch_args + [ "persist", f"exec.poststart=\"{self.get_hook_script_path('poststart')}\"" - ] + ]) stdout, stderr, returncode = self._exec_host_command( command=command, @@ -1799,11 +1776,11 @@ def _launch_single_command_jail( jail_command: str, passthru: bool ) -> libioc.helpers.CommandOutput: - command = self._launch_command + [ + command = self.__get_launch_command(self._launch_args + [ "nopersist", f"exec.poststart=\"{self.get_hook_script_path('host_command')}\"", "command=/usr/bin/true" - ] + ]) _identifier = str(shlex.quote(self.identifier)) _jls_command = f"/usr/sbin/jls -j {_identifier} jid" @@ -2009,16 +1986,16 @@ def _clear_resource_limits(self) -> typing.List[str]: return [f"/usr/bin/rctl -r jail:{self.identifier} 2>/dev/null || true"] @property - def _allow_mount(self) -> str: - if self._allow_mount_zfs == "1": - return "1" - return self._get_value("allow_mount") + def _allow_mount(self) -> int: + if self._allow_mount_zfs == 1: + return 1 + return int(self._get_value("allow_mount")) @property - def _allow_mount_zfs(self) -> str: + def _allow_mount_zfs(self) -> int: if self.config["jail_zfs"] is True: - return "1" - return self._get_value("allow_mount_zfs") + return 1 + return int(self._get_value("allow_mount_zfs")) def _configure_routes_commands(self) -> typing.List[str]: diff --git a/libioc/JailParams.py b/libioc/JailParams.py new file mode 100644 index 00000000..917b852e --- /dev/null +++ b/libioc/JailParams.py @@ -0,0 +1,181 @@ +# Copyright (c) 2017-2019, Stefan Grönke +# Copyright (c) 2014-2018, iocage +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted providing that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +"""Sysctl jail params signeton.""" +import typing +import freebsd_sysctl +import freebsd_sysctl.types +import collections.abc +import shlex + +import libioc.helpers + +JailParamValueType = typing.Optional[typing.Union[bool, int, str]] + + +class JailParam(freebsd_sysctl.Sysctl): + """Single jail parameter represented by sysctl.""" + + user_value: JailParamValueType + + @property + def value(self) -> JailParamValueType: + """Return the user defined value of this jail parameter.""" + return self.user_value + + @value.setter + def value(self, value: JailParamValueType) -> None: + """Set the user defined value of this jail parameter.""" + if self.ctl_type == freebsd_sysctl.types.NODE: + raise TypeError("sysctl NODE has no value") + + if self.ctl_type in [ + freebsd_sysctl.types.STRING, + freebsd_sysctl.types.OPAQUE, + ]: + if (isinstance(value, int) or isinstance(value, str)) is False: + try: + value = str(value) + except Exception: + self.__raise_value_type_error() + else: + if (isinstance(value, int) or isinstance(value, bool)) is False: + try: + value = int(value) # noqa: T484 + except Exception: + self.__raise_value_type_error() + self.user_value = value + + @property + def sysctl_value(self) -> JailParamValueType: + """Return the original freebsd_sysctl.Sysctl value.""" + return typing.cast( + JailParamValueType, + super().value + ) + + def __raise_value_type_error(self) -> None: + type_name = self.ctl_type.__name__ + raise TypeError(f"{self.name} sysctl requires {type_name}") + + @property + def jail_arg_name(self) -> str: + """Return the name of the param formatted for the jail command.""" + name = str(self.name) + prefix = "security.jail.param." + if name.startswith(prefix) is True: + return name[len(prefix):] + return name + + @property + def iocage_name(self) -> str: + """Return the name of the param formatted for iocage config.""" + return self.jail_arg_name.replace(".", "_") + + def __str__(self) -> str: + """Return the jail command argument notation of the param.""" + if (self.value is None): + return self.jail_arg_name + + if (self.ctl_type == freebsd_sysctl.types.STRING): + escaped_value = shlex.quote(str(self.value)) + return f"{self.jail_arg_name}={escaped_value}" + + mapped_value = str(libioc.helpers.to_string( + self.value, + true="1", + false="0" + )) + return f"{self.jail_arg_name}={mapped_value}" + + +class JailParams(collections.abc.MutableMapping): + """Collection of jail parameters.""" + + __base_class = JailParam + __sysctl_params: typing.Dict[str, freebsd_sysctl.Sysctl] + + def __iter__(self) -> typing.Iterator[str]: + """Iterate over the jail param names.""" + yield from self.memoized_params.__iter__() + + def __len__(self) -> int: + """Return the number of available jail params.""" + return self.memoized_params.__len__() + + def items(self) -> typing.ItemsView[str, freebsd_sysctl.Sysctl]: + """Iterate over the keys and values.""" + return self.memoized_params.items() + + def keys(self) -> typing.KeysView[str]: + """Return a list of all jail param names.""" + return collections.abc.KeysView(list(self.__iter__())) # noqa: T484 + + def __getitem__(self, key: str) -> typing.Any: + """Set of jail params sysctl is not implemented.""" + return self.memoized_params.__getitem__(key) + + def __setitem__(self, key: str, value: typing.Any) -> None: + """Set of jail params sysctl is not supportes.""" + self.memoized_params.__setitem__(key, value) + + def __delitem__(self, key: str) -> None: + """Delete of jail param sysctl not supported.""" + self.memoized_params.__delitem__(key) + + @property + def memoized_params(self) -> typing.Dict[str, freebsd_sysctl.Sysctl]: + """Return the memorized params initialized on first access.""" + try: + return self.__sysctl_params + except AttributeError: + pass + self.__update_sysctl_jail_params() + return self.__sysctl_params + + def __update_sysctl_jail_params(self) -> None: + prefix = "security.jail.param" + jail_params = filter( + lambda x: not any(( + x.name.endswith("."), # quick filter NODE + x.name == "security.jail.allow_raw_sockets", # deprecated + )), + self.__base_class(prefix).children + ) + # permanently store the queried sysctl in the singleton class + JailParams.__sysctl_params = dict([(x.name, x,) for x in jail_params]) + + +class HostJailParams(JailParams): + """Read-only host jail parameters obtained from sysctl.""" + + __base_class = freebsd_sysctl.Sysctl + + def __setitem__(self, key: str, value: typing.Any) -> None: + """Set of jail params sysctl is not supportes.""" + raise NotImplementedError("jail param sysctl cannot be modified") + + def __delitem__(self, key: str) -> None: + """Delete of jail param sysctl not supported.""" + raise NotImplementedError("jail param sysctl cannot be deleted") diff --git a/requirements.txt b/requirements.txt index f27d34d6..d9ca61dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ ucl==0.8.0 -gitpython \ No newline at end of file +gitpython +freebsd_sysctl==0.0.4