Skip to content

Commit

Permalink
furnace/context.py: bind mount '/etc/resolv.conf' in host network mode
Browse files Browse the repository at this point in the history
With this solution the '/etc/resolv.conf' of the host can be used inside
the container, because it is bind mounted before every run in host network
mode (when network is not isolated). The bind mount is read-only to avoid
the modification of the '/etc/resolv.conf' of the host machine. With this
commit bind mounts can be added to the ContainerContext.
  • Loading branch information
Balazs Kocso authored and kocsob committed Dec 19, 2018
1 parent 09b3183 commit 02386f5
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 8 deletions.
9 changes: 9 additions & 0 deletions furnace/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
),
]
36 changes: 31 additions & 5 deletions furnace/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Furnace. If not, see <http://www.gnu.org/licenses/>.
#

import contextlib
import json
import logging
import os
import signal
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__)

Expand Down Expand Up @@ -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

Expand Down
18 changes: 15 additions & 3 deletions test/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 02386f5

Please sign in to comment.