diff --git a/furnace/config.py b/furnace/config.py index f83e145..cf1fe9a 100644 --- a/furnace/config.py +++ b/furnace/config.py @@ -26,6 +26,7 @@ Mount = namedtuple('Mount', ['destination', 'type', 'source', 'flags', 'options']) DeviceNode = namedtuple('DeviceNode', ['name', 'major', 'minor']) +BindMount = namedtuple('BindMount', ['source', 'destination', 'readonly']) HOSTNAME = 'localhost' @@ -137,3 +138,11 @@ minor=9, ), ] + +HOST_NETWORK_BIND_MOUNTS = [ + BindMount( + source=Path('/etc/resolv.conf'), # absolute path on host machine + destination=Path('etc', 'resolv.conf'), # relative path in container + readonly=True, + ), +] diff --git a/furnace/context.py b/furnace/context.py index bc8cbeb..903dfb2 100644 --- a/furnace/context.py +++ b/furnace/context.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Furnace. If not, see . # - +import contextlib import json import logging import os @@ -24,12 +24,12 @@ import subprocess import sys from pathlib import Path -from typing import Union +from typing import Union, List from . import pid1 -from .config import NAMESPACES +from .config import NAMESPACES, HOST_NETWORK_BIND_MOUNTS, BindMount from .libc import unshare, setns, CLONE_NEWPID -from .utils import PathEncoder +from .utils import PathEncoder, BindMountContext logger = logging.getLogger(__name__) @@ -152,20 +152,46 @@ def __exit__(self, type, value, traceback): class ContainerContext: - def __init__(self, root_dir: Union[str, Path], *, isolate_networking=False): + def __init__(self, root_dir: Union[str, Path], *, isolate_networking: bool=False, bind_mounts: List[BindMount]=None): if not isinstance(root_dir, Path): root_dir = Path(root_dir) self.root_dir = root_dir.resolve() + self.bind_mounts = bind_mounts + if self.bind_mounts is None: + self.bind_mounts = [] + if not isolate_networking: + self.bind_mounts.extend(HOST_NETWORK_BIND_MOUNTS) self.pid1 = ContainerPID1Manager(root_dir, isolate_networking=isolate_networking) self.setns_context = None + self.bind_mount_contexts = None + + def create_target(self, source, destination): + if source.is_file(): + if destination.is_symlink(): + destination.unlink() + destination.parent.mkdir(parents=True, exist_ok=True) + destination.touch() + else: + destination.mkdir(parents=True, exist_ok=True) def __enter__(self): self.pid1.start() self.setns_context = SetnsContext(self.pid1.pid) + self.bind_mount_contexts = contextlib.ExitStack() + for bind_mount in self.bind_mounts: + self.create_target(bind_mount.source, self.root_dir.joinpath(bind_mount.destination)) + self.bind_mount_contexts.enter_context(BindMountContext( + bind_mount.source, + self.root_dir.joinpath(bind_mount.destination), + bind_mount.readonly + )) return self def __exit__(self, type, value, traceback): self.setns_context = None + if self.bind_mount_contexts: + self.bind_mount_contexts.__exit__(type, value, traceback) + self.bind_mount_contexts = None self.pid1.kill() return False diff --git a/test/test_container.py b/test/test_container.py index 57c8ffa..a0084c4 100644 --- a/test/test_container.py +++ b/test/test_container.py @@ -144,15 +144,27 @@ def test_loop_mounts_work(rootfs_for_testing): # no assert, because the previous two commands would have thrown an Exception on error -def test_using_container_does_not_touch_files(debootstrapped_dir, tmpdir_factory): +def test_using_container_does_not_touch_files_if_network_isolated(debootstrapped_dir, tmpdir_factory): overlay_workdir = Path(tmpdir_factory.mktemp('overlay_work')) overlay_rwdir = Path(tmpdir_factory.mktemp('overlay_rw')) overlay_mounted = Path(tmpdir_factory.mktemp('overlay_mount')) with OverlayfsMountContext([debootstrapped_dir], overlay_rwdir, overlay_workdir, overlay_mounted): - with ContainerContext(overlay_mounted) as cnt: + with ContainerContext(overlay_mounted, isolate_networking=True) as cnt: cnt.run(["/bin/ls", "/"], check=True) + modified_files = list(overlay_rwdir.iterdir()) - assert len(modified_files) == 0, "No files should have been modified because of container run" + assert len(modified_files) == 0, "No files should have been modified because of container run if network isolated" + + +def test_using_container_with_host_network(rootfs_for_testing, tmpdir_factory): + host_resolvconf_content = Path('/etc/resolv.conf').read_bytes() + with ContainerContext(rootfs_for_testing) as cnt: + cnt.run(["/bin/ls", "/"], check=True) + container_resolvconf_content = rootfs_for_testing.joinpath('etc', 'resolv.conf').read_bytes() + + assert host_resolvconf_content == container_resolvconf_content, \ + "The content of '/etc/resolv.conf' of the host machine should be equal to '/etc/resolv.conf' of the container " \ + "if host networking is used" class ThreadForTesting(threading.Thread):