From 04d2e67355ce5e3d34844a808f0701df72aea07f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Fri, 10 Apr 2020 11:34:03 +0200
Subject: [PATCH 01/10] replace socket wrapper with Linux namespace

---
 .gitmodules                          |   3 -
 Makefile                             |  22 +-
 conftest.py                          |  33 +--
 contrib/libswrap                     |   1 -
 deckard.py                           | 363 ++++++---------------------
 deckard_pytest.py                    | 108 +++++++-
 namespaces.py                        | 124 +++++++++
 networking.py                        |  77 ++++++
 pydnstest/mock_client.py             |   7 +-
 pydnstest/scenario.py                |  20 +-
 pydnstest/tests/test_parse_config.py |   2 +-
 pydnstest/testserver.py              |  19 +-
 run.sh                               |   4 +-
 sets/resolver/iter_minmaxttl.rpl     |   1 +
 sets/resolver/val_wild_pos_multi.rpl |   3 +-
 15 files changed, 435 insertions(+), 352 deletions(-)
 delete mode 160000 contrib/libswrap
 create mode 100644 namespaces.py
 create mode 100644 networking.py

diff --git a/.gitmodules b/.gitmodules
index d503253..7a3c587 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,3 @@
 [submodule "contrib/libfaketime"]
 	path = contrib/libfaketime
 	url = git://github.com/wolfcw/libfaketime.git
-[submodule "contrib/libswrap"]
-	path = contrib/libswrap
-	url = https://gitlab.labs.nic.cz/labs/socket_wrapper.git
diff --git a/Makefile b/Makefile
index 675aa42..656487c 100644
--- a/Makefile
+++ b/Makefile
@@ -8,30 +8,24 @@ endif
 
 # Dependencies
 include platform.mk
-libcwrap_DIR := contrib/libswrap
-libcwrap_cmake_DIR := $(libcwrap_DIR)/obj
-libcwrap=$(abspath $(libcwrap_cmake_DIR))/src/libsocket_wrapper$(LIBEXT).0
-ifeq ($(PLATFORM),Darwin)
-	libcwrap=$(abspath $(libcwrap_cmake_DIR))/src/libsocket_wrapper.0$(LIBEXT)
-endif
 libfaketime_DIR := contrib/libfaketime
 libfaketime := $(abspath $(libfaketime_DIR))/src/libfaketime$(LIBEXT).1
 
 # Platform-specific targets
 ifeq ($(PLATFORM),Darwin)
 	libfaketime := $(abspath $(libfaketime_DIR))/src/libfaketime.1$(LIBEXT)
-	preload_syms := DYLD_LIBRARY_PATH=$(DYLD_LIBRARY_PATH) DYLD_FORCE_FLAT_NAMESPACE=1 DYLD_INSERT_LIBRARIES="$(libfaketime):$(libcwrap)"
+	preload_syms := DYLD_LIBRARY_PATH=$(DYLD_LIBRARY_PATH) DYLD_FORCE_FLAT_NAMESPACE=1 DYLD_INSERT_LIBRARIES="$(libfaketime)"
 else
-	preload_syms := LD_PRELOAD="$(libfaketime):$(libcwrap)"
+	preload_syms := LD_PRELOAD="$(libfaketime)"
 endif
 
 
 # Targets
 all:
 	@echo "Deckard is now run using *run.sh scripts in its root directory."
-	@echo "To build the dependencies (libfaketime and libcwrap) run 'make depend'."
+	@echo "To build the dependencies (libfaketime) run 'make depend'."
 	exit 1
-depend: $(libfaketime) $(libcwrap)
+depend: $(libfaketime)
 	@echo "export DONT_FAKE_MONOTONIC=1" > env.sh
 	@echo "export $(preload_syms)" >> env.sh
 
@@ -40,15 +34,9 @@ submodules: .gitmodules
 	@git submodule update --init
 # indirection through submodules target is necessary
 # to prevent make from running "git submodule" commands in parallel
-$(libfaketime_DIR)/Makefile $(libcwrap_DIR)/CMakeLists.txt: submodules
+$(libfaketime_DIR)/Makefile: submodules
 $(libfaketime): $(libfaketime_DIR)/Makefile
 	@CFLAGS="-O0 -g" $(MAKE) -s -C $(libfaketime_DIR)
-$(libcwrap_cmake_DIR):$(libcwrap_DIR)/CMakeLists.txt
-	@mkdir -p $(libcwrap_cmake_DIR)
-$(libcwrap_cmake_DIR)/Makefile: $(libcwrap_cmake_DIR)
-	@cd $(libcwrap_cmake_DIR); cmake ..
-$(libcwrap): $(libcwrap_cmake_DIR)/Makefile
-	@CFLAGS="-O0 -g" $(MAKE) -s -C $(libcwrap_cmake_DIR)
 
 
 .PHONY: submodules depend all
diff --git a/conftest.py b/conftest.py
index 17b06ca..46a848f 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1,39 +1,17 @@
-from collections import namedtuple, OrderedDict
 import glob
 import logging
 import os
 import re
+from collections import namedtuple
 
 import pytest
 import yaml
 
+from namespaces import LinuxNamespace
 
 Scenario = namedtuple("Scenario", ["path", "qmin", "config"])
 
 
-def ordered_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict):
-    """Make YaML load to OrderedDict.
-    This is done to ensure compability with Python versions prior to 3.6.
-    See docs.python.org/3.6/whatsnew/3.6.html#new-dict-implementation for more information.
-
-    repr(config) is a part of testcase's name in pytest.
-    We need to ensure that it is ordered in the same way.
-    See https://github.com/pytest-dev/pytest/issues/1075.
-    """
-    class OrderedLoader(Loader):  # pylint: disable=too-many-ancestors
-        pass
-
-    def construct_mapping(loader, node):
-        loader.flatten_mapping(node)
-        return object_pairs_hook(loader.construct_pairs(node))
-
-    OrderedLoader.add_constructor(
-        yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
-        construct_mapping)
-
-    return yaml.load(stream, OrderedLoader)
-
-
 def config_sanity_check(config_dict, config_name):
     """Checks if parsed configuration is valid"""
     mandatory_keys = {'name', 'binary', 'templates', 'configs', 'additional'}
@@ -76,7 +54,8 @@ def scenarios(paths, configs):
     scenario_list = []
 
     for path, config in zip(paths, configs):
-        config_dict = ordered_load(open(config), yaml.SafeLoader)
+        with open(config) as f:
+            config_dict = yaml.load(f, yaml.SafeLoader)
         config_sanity_check(config_dict, config)
 
         if os.path.isfile(path):
@@ -154,3 +133,7 @@ def pytest_configure(config):
         except ValueError:
             log_level = logging.getLevelName(log_level)
         check_log_level_xdist(log_level)
+
+
+def pytest_runtest_setup(item):  # pylint: disable=unused-argument
+    LinuxNamespace("user").__enter__()
diff --git a/contrib/libswrap b/contrib/libswrap
deleted file mode 160000
index 7a4d408..0000000
--- a/contrib/libswrap
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 7a4d4087b10fd4ec82853d51c7a45a882d11d314
diff --git a/deckard.py b/deckard.py
index 648e199..896d2f3 100755
--- a/deckard.py
+++ b/deckard.py
@@ -1,23 +1,19 @@
 #!/usr/bin/env python3
-from datetime import datetime
 import errno
-import ipaddress
 import logging
 import logging.config
 import os
 import shutil
 import socket
 import subprocess
-import tempfile
 import time
+from datetime import datetime
 from typing import Set  # noqa
 
-import dpkt
 import jinja2
 
 from pydnstest import scenario, testserver
 
-
 # path to Deckard files
 INSTALLDIR = os.path.dirname(os.path.abspath(__file__))
 # relative to working directory
@@ -28,66 +24,11 @@ class DeckardUnderLoadError(Exception):
     pass
 
 
-class IfaceManager:
-    """
-    Network interface allocation manager
-
-    Keeps mapping between 'name', interface number, and IP address.
-    """
-    def __init__(self, sockfamily):
-        """
-        Parameters:
-            sockfamily Address family used in given test scenatio
-                       (a constant from socket module)
-        """
-        if sockfamily not in {socket.AF_INET, socket.AF_INET6}:
-            raise NotImplementedError("address family not supported '%i'" % sockfamily)
-        self.sockfamily = sockfamily
-        self.free = list(range(40, 10, -1))  # range accepted by libswrap
-        self.name2iface = {}
-
-    def allocate(self, name):
-        """
-        Map name to a free interface number.
-        """
-        if name in self.name2iface:
-            raise ValueError('duplicate interface name %s' % name)
-        iface = str(self.free.pop())
-        self.name2iface[name] = iface
-        return iface
-
-    def getiface(self, name):
-        """
-        Map name to allocated interface number.
-
-        Returns:
-            Interface number as string (so it can be assigned to os.environ)
-        """
-        return self.name2iface[name]
-
-    def getipaddr(self, name):
-        """
-        Get default IP address assigned to interface allocated to given name.
-
-        Returns:
-            Address from address family specified during IfaceManager init.
-        """
-        iface = self.getiface(name)
-        if self.sockfamily == socket.AF_INET:
-            addr_local_pattern = "127.0.0.{}"
-        elif self.sockfamily == socket.AF_INET6:
-            addr_local_pattern = "fd00::5357:5f{:02X}"
-        return addr_local_pattern.format(int(iface))
-
-    def getalladdrs(self):
-        """
-        Get mapping from all names to all IP addresses.
-
-        Returns:
-            {name: IP address}
-        """
-        return {name: self.getipaddr(name)
-                for name in self.name2iface}
+def setup_internal_addresses(context):
+    context["DECKARD_IP"] = context["if_manager"].assign_internal_address(context["_SOCKET_FAMILY"])
+    for program in context["programs"]:
+        program["address"] = context["if_manager"].assign_internal_address(
+            context["_SOCKET_FAMILY"])
 
 
 def write_timestamp_file(path, tst):
@@ -97,7 +38,7 @@ def write_timestamp_file(path, tst):
     time_file.close()
 
 
-def setup_common_env(ctx):
+def setup_faketime(config):
     """
     Setup environment shared between Deckard and binaries under test.
 
@@ -107,83 +48,22 @@ def setup_common_env(ctx):
     Returns:
         path to working directory
     """
-    # working directory
-    if "SOCKET_WRAPPER_DIR" in os.environ:
-        tmpdir = os.environ["SOCKET_WRAPPER_DIR"]
-        if os.path.lexists(tmpdir):
-            raise ValueError('SOCKET_WRAPPER_DIR "%s" must not exist' % tmpdir)
-    else:
-        # uses TMPDIR environment variable (if dir exists)
-        tmpdir = tempfile.mkdtemp(suffix='', prefix='tmpdeckard')
-
     # Set up libfaketime
     os.environ["FAKETIME_NO_CACHE"] = "1"
-    os.environ["FAKETIME_TIMESTAMP_FILE"] = '%s/.time' % tmpdir
-    # fake initial time
-    write_timestamp_file(os.environ["FAKETIME_TIMESTAMP_FILE"],
-                         ctx.get('_OVERRIDE_TIMESTAMP', time.time()))
-
-    # Set up socket_wrapper
-    os.environ["SOCKET_WRAPPER_DIR"] = tmpdir
-    os.environ["SOCKET_WRAPPER_PCAP_FILE"] = '%s/deckard.pcap' % tmpdir
-
-    return tmpdir
-
-
-def setup_daemon_env(prog_cfg, tmpdir):
-    """ Set up test environment and config """
-    name = prog_cfg['name']
-    log = logging.getLogger('deckard.daemon.%s.setup_env' % name)
-    # Set up child process env() to use socket wrapper interface
-    child_env = os.environ.copy()
-    child_env['SOCKET_WRAPPER_DEFAULT_IFACE'] = prog_cfg['iface']
-    prog_cfg['dir'] = os.path.join(tmpdir, name)
-    log.debug('directory: %s', prog_cfg['dir'])
-    child_env['SOCKET_WRAPPER_PCAP_FILE'] = '%s/pcap' % prog_cfg['dir']
-
-    return child_env
-
-
-def setup_network(sockfamily, prog_cfgs):
-    """Allocate fake interfaces and IP addresses to all entities.
-
-    Returns:
-    - SOCKET_WRAPPER_DEFAULT_IFACE will be set in os.environ
-    - Dict suitable for usage in Jinja2 templates will be returned
-        {
-         ROOT_ADDR: <DeckardIP>,
-         IPADDRS: {name: <IPaddress>}
-        }
-    """
-    net_config = {}
-    # assign interfaces and IP addresses to all involved programs
-    ifacemgr = IfaceManager(sockfamily)
-    # fake interface for Deckard itself
-    deckard_iface = ifacemgr.allocate('deckard')
-    os.environ['SOCKET_WRAPPER_DEFAULT_IFACE'] = deckard_iface
-    net_config['ROOT_ADDR'] = ifacemgr.getipaddr('deckard')
-
-    for prog_cfg in prog_cfgs['programs']:
-        prog_cfg['iface'] = ifacemgr.allocate(prog_cfg['name'])
-        prog_cfg['ipaddr'] = ifacemgr.getipaddr(prog_cfg['name'])
-    net_config['IPADDRS'] = ifacemgr.getalladdrs()
-
-    return net_config
+    os.environ["FAKETIME_TIMESTAMP_FILE"] = os.path.join(config["tmpdir"], ".time")
+    os.unsetenv("FAKETIME")
 
+    write_timestamp_file(os.environ["FAKETIME_TIMESTAMP_FILE"],
+                         config.get('_OVERRIDE_TIMESTAMP', time.time()))
 
-def _fixme_prebind_hack(sockfamily, childaddr):
-    """
-    Prebind to sockets to create necessary files
 
-    @TODO: this is probably a workaround for socket_wrapper bug
-    """
-    if 'NOPRELOAD' not in os.environ:
-        for sock_type in (socket.SOCK_STREAM, socket.SOCK_DGRAM):
-            sock = socket.socket(sockfamily, sock_type)
-            sock.setsockopt(sockfamily, socket.SO_REUSEADDR, 1)
-            sock.bind((childaddr, 53))
-            if sock_type & socket.SOCK_STREAM:
-                sock.listen(5)
+def setup_daemon_environment(program_config, global_config):
+    program_config["WORKING_DIR"] = os.path.join(global_config["tmpdir"], program_config["name"])
+    os.mkdir(program_config['WORKING_DIR'])
+    program_config["DAEMON_NAME"] = program_config["name"]
+    program_config['SELF_ADDR'] = program_config['address']
+    program_config['TRUST_ANCHOR_FILES'] = create_trust_anchor_files(
+        global_config["TRUST_ANCHOR_FILES"], program_config['WORKING_DIR'])
 
 
 def create_trust_anchor_files(ta_files, work_dir):
@@ -212,51 +92,36 @@ def create_trust_anchor_files(ta_files, work_dir):
     return full_paths
 
 
-def setup_daemon_files(prog_cfg, template_ctx, ta_files):
-    name = prog_cfg['name']
-    # add program-specific variables
-    subst = template_ctx.copy()
-    subst['DAEMON_NAME'] = name
-
-    subst['WORKING_DIR'] = prog_cfg['dir']
-    os.mkdir(prog_cfg['dir'])
-    subst['SELF_ADDR'] = prog_cfg['ipaddr']
+def generate_from_templates(program_config, global_config):
+    """Generate configuration for the program"""
+    config = global_config.copy()
+    config.update(program_config)
 
-    # daemons might write to TA files so every daemon gets its own copy
-    subst['TRUST_ANCHOR_FILES'] = create_trust_anchor_files(
-        ta_files, prog_cfg['dir'])
-
-    # generate configuration files
     j2template_loader = jinja2.FileSystemLoader(searchpath=os.getcwd())
-    print(os.path.abspath(os.getcwd()))
     j2template_env = jinja2.Environment(loader=j2template_loader)
-    logging.getLogger('deckard.daemon.%s.template' % name).debug(subst)
 
-    assert len(prog_cfg['templates']) == len(prog_cfg['configs'])
-    for template_name, config_name in zip(prog_cfg['templates'], prog_cfg['configs']):
+    for template_name, config_name in zip(config['templates'], config['configs']):
         j2template = j2template_env.get_template(template_name)
-        cfg_rendered = j2template.render(subst)
-        with open(os.path.join(prog_cfg['dir'], config_name), 'w') as output:
+        cfg_rendered = j2template.render(config)
+        with open(os.path.join(config['WORKING_DIR'], config_name), 'w') as output:
             output.write(cfg_rendered)
 
-    _fixme_prebind_hack(template_ctx['_SOCKET_FAMILY'], subst['SELF_ADDR'])
-
 
-def run_daemon(cfg, environ):
+def run_daemon(program_config):
     """Start binary and return its process object"""
-    name = cfg['name']
+    name = program_config['DAEMON_NAME']
     proc = None
-    cfg['log'] = os.path.join(cfg['dir'], 'server.log')
-    daemon_log_file = open(cfg['log'], 'w')
-    cfg['args'] = args = [cfg['binary']] + cfg['additional']
-    logging.getLogger('deckard.daemon.%s.env' % name).debug('%s', environ)
-    logging.getLogger('deckard.daemon.%s.argv' % name).debug('%s', args)
+    program_config['log'] = os.path.join(program_config["WORKING_DIR"], 'server.log')
+    daemon_log_file = open(program_config['log'], 'w')
+    program_config['args'] = [program_config['binary']] + program_config['additional']
+    logging.getLogger('deckard.daemon.%s.argv' % name).debug('%s', program_config['args'])
     try:
-        proc = subprocess.Popen(args, stdout=daemon_log_file, stderr=subprocess.STDOUT,
-                                cwd=cfg['dir'], env=environ, start_new_session=True)
+        proc = subprocess.Popen(program_config['args'], stdout=daemon_log_file,
+                                stderr=subprocess.STDOUT, cwd=program_config['WORKING_DIR'],
+                                start_new_session=True)
     except subprocess.CalledProcessError:
         logger = logging.getLogger('deckard.daemon_log.%s' % name)
-        logger.exception("Can't start '%s'", args)
+        logger.exception("Can't start '%s'", program_config['args'])
         raise
     return proc
 
@@ -271,142 +136,51 @@ def conncheck_daemon(process, cfg, sockfamily):
             raise RuntimeError("Server took too long to respond")
         # Check if the process is running
         if process.poll() is not None:
-            msg = 'process died "%s", logs in "%s"' % (cfg['name'], cfg['dir'])
+            msg = 'process died "%s", logs in "%s"' % (cfg['name'], cfg['WORKING_DIR'])
             logger = logging.getLogger('deckard.daemon_log.%s' % cfg['name'])
             logger.critical(msg)
             logger.error(open(cfg['log']).read())
             raise subprocess.CalledProcessError(process.returncode, cfg['args'], msg)
         try:
-            sock.connect((cfg['ipaddr'], 53))
+            sock.connect((cfg['address'], 53))
         except socket.error:
             continue
         break
     sock.close()
 
 
-def process_file(path, qmin, prog_cfgs):
-    """Parse scenario from a file object and create workdir."""
-    # Parse scenario
-    case, cfg_text = scenario.parse_file(os.path.realpath(path))
-    cfg_ctx, ta_files = scenario.parse_config(cfg_text, qmin, INSTALLDIR)
-    template_ctx = setup_network(cfg_ctx['_SOCKET_FAMILY'], prog_cfgs)
-    # merge variables from scenario with generated network variables (scenario has priority)
-    template_ctx.update(cfg_ctx)
-    # Deckard will communicate with first program
-    prog_under_test = prog_cfgs['programs'][0]['name']
-    prog_under_test_ip = template_ctx['IPADDRS'][prog_under_test]
-
-    # get working directory and environment variables
-    tmpdir = setup_common_env(cfg_ctx)
-    shutil.copy2(path, os.path.join(tmpdir))
-    try:
-        daemons = setup_daemons(tmpdir, prog_cfgs, template_ctx, ta_files)
-        run_testcase(daemons,
-                     case,
-                     template_ctx['ROOT_ADDR'],
-                     template_ctx['_SOCKET_FAMILY'],
-                     prog_under_test_ip)
-        if prog_cfgs.get('noclean'):
-            logging.getLogger('deckard.hint').info(
-                'test working directory %s', tmpdir)
-        else:
-            shutil.rmtree(tmpdir)
-    except Exception:
-        logging.getLogger('deckard.hint').error(
-            'test failed, inspect working directory %s', tmpdir)
-        raise
-
-
-def setup_daemons(tmpdir, prog_cfgs, template_ctx, ta_files):
-    """Configure daemons and run the test"""
+def setup_daemons(config):
+    """Configure daemons and start them"""
     # Setup daemon environment
     daemons = []
-    for prog_cfg in prog_cfgs['programs']:
-        daemon_env = setup_daemon_env(prog_cfg, tmpdir)
-        setup_daemon_files(prog_cfg, template_ctx, ta_files)
-        daemon_proc = run_daemon(prog_cfg, daemon_env)
-        daemons.append({'proc': daemon_proc, 'cfg': prog_cfg})
+
+    for program_config in config['programs']:
+        setup_daemon_environment(program_config, config)
+        generate_from_templates(program_config, config)
+
+        daemon_proc = run_daemon(program_config)
+        daemons.append({'proc': daemon_proc, 'cfg': program_config})
         try:
-            conncheck_daemon(daemon_proc, prog_cfg, template_ctx['_SOCKET_FAMILY'])
+            conncheck_daemon(daemon_proc, program_config, config['_SOCKET_FAMILY'])
         except:  # noqa  -- bare except might be valid here?
             daemon_proc.terminate()
             raise
-    return daemons
-
 
-def check_for_icmp():
-    """ Checks Deckards's PCAP for ICMP packets """
-    # Deckard's responses to resolvers might be delayed due to load which
-    # leads the resolver to close the port and to the test failing in the
-    # end. We partially detect these by checking the PCAP for ICMP packets.
-    path = os.environ["SOCKET_WRAPPER_PCAP_FILE"]
-    udp_seen = False
-    with open(path, "rb") as f:
-        pcap = dpkt.pcap.Reader(f)
-        for _, packet in pcap:
-            try:
-                ip = dpkt.ip.IP(packet)
-            except dpkt.dpkt.UnpackError:
-                ip = dpkt.ip6.IP6(packet)
-            if isinstance(ip.data, dpkt.udp.UDP):
-                udp_seen = True
-
-            if udp_seen:
-                if isinstance(ip.data, (dpkt.icmp.ICMP, dpkt.icmp6.ICMP6)):
-                    raise DeckardUnderLoadError("Deckard is under load. "
-                                                "Other errors might be false negatives. "
-                                                "Consider retrying the job later.")
-        return False
+    return daemons
 
 
 def check_for_reply_steps(case: scenario.Scenario) -> bool:
     return any(s.type == "REPLY" for s in case.steps)
 
 
-def check_for_unknown_servers(case: scenario.Scenario, daemon: dict) -> None:
-    """ Checks Deckards's PCAP for packets going to servers not present in scenario """
-    path = os.path.join(daemon["cfg"]["dir"], "pcap")
-    asked_servers = set()
-    with open(path, "rb") as f:
-        pcap = dpkt.pcap.Reader(f)
-        for _, packet in pcap:
-            try:
-                ip = dpkt.ip.IP(packet)
-            except dpkt.dpkt.UnpackError:
-                ip = dpkt.ip6.IP6(packet)
-            # pylint: disable=no-member
-            dest = ipaddress.ip_address(int.from_bytes(ip.dst, byteorder="big"))
-            # pylint: enable=no-member
-
-            # Socket wrapper asigns (random) link local addresses to the binary under test
-            # and Deckard itself. We have to filter them out of the pcap.
-            if dest.is_global:
-                asked_servers.add(dest)
-
-    scenario_ips = set()  # type: Set[str]
-    for r in case.ranges:
-        scenario_ips |= r.addresses
-
-    scenario_servers = {ipaddress.ip_address(ip) for ip in scenario_ips}
-
-    servers_not_in_scenario = asked_servers - scenario_servers
-
-    if servers_not_in_scenario:
-        if not check_for_reply_steps(case):
-            raise RuntimeError("Binary in test asked an IP address not present in scenario %s"
-                               % servers_not_in_scenario)
-
-
-def run_testcase(daemons, case, root_addr, addr_family, prog_under_test_ip):
+def run_testcase(case, daemons, config, prog_under_test_ip):
     """Run actual test and raise exception if the test failed"""
-    server = testserver.TestServer(case, root_addr, addr_family)
+    server = testserver.TestServer(case, config["ROOT_ADDR"], config["_SOCKET_FAMILY"],
+                                   config["DECKARD_IP"], config["if_manager"])
     server.start()
 
     try:
         server.play(prog_under_test_ip)
-    except ValueError as e:
-        if not check_for_icmp():
-            raise e
     finally:
         server.stop()
 
@@ -425,9 +199,36 @@ def run_testcase(daemons, case, root_addr, addr_family, prog_under_test_ip):
             if daemon['proc'].returncode != 0 and not ignore_exit:
                 raise ValueError('process %s terminated with return code %s'
                                  % (daemon['cfg']['name'], daemon['proc'].returncode))
-            check_for_unknown_servers(case, daemon)
 
-    # Do not clear files if the server crashed (for analysis)
     if server.undefined_answers > 0:
-        if not check_for_icmp():
-            raise ValueError('the scenario does not define all necessary answers (see error log)')
+        raise ValueError('the scenario does not define all necessary answers (see error log)')
+
+
+def process_file(path, qmin, config):
+    """Parse scenario from a file object and create workdir."""
+    # Parse scenario
+    case, case_config_text = scenario.parse_file(os.path.realpath(path))
+    case_config = scenario.parse_config(case_config_text, qmin, INSTALLDIR)
+
+    # Merge global and scenario configs
+    config.update(case_config)
+
+    # Asign addresses to the programs and Deckard itself
+    setup_internal_addresses(config)
+
+    # Deckard will communicate with first program
+    prog_under_test_ip = config['programs'][0]['address']
+
+    setup_faketime(config)
+
+    # Copy the scenario to tmpdir for future reference
+    shutil.copy2(path, os.path.join(config["tmpdir"]))
+
+    try:
+        daemons = setup_daemons(config)
+        run_testcase(case, daemons, config, prog_under_test_ip)
+
+    except Exception:
+        logging.getLogger('deckard.hint').error(
+            'test failed, inspect working directory %s', config["tmpdir"])
+        raise
diff --git a/deckard_pytest.py b/deckard_pytest.py
index 3c5f4b8..d5d309a 100755
--- a/deckard_pytest.py
+++ b/deckard_pytest.py
@@ -1,13 +1,19 @@
 import logging
 import os
-import subprocess
 import random
+import shutil
+import subprocess
 import sys
+import tempfile
 import time
+from ipaddress import ip_address
 
+import dpkt
 import pytest
 
 import deckard
+from namespaces import LinuxNamespace
+from networking import InterfaceManager
 
 
 def set_coverage_env(path, qmin):
@@ -35,14 +41,104 @@ logging.getLogger("augeas").setLevel(logging.ERROR)
 check_platform()
 
 
+class DeckardUnderLoadError(Exception):
+    pass
+
+
+class TCPDump:
+    """This context manager captures a PCAP file and than checks it for obvious errors."""
+
+    # -f option filters out all ICMP messages that are not Destination Unreachable
+    DUMPCAP_CMD = ["dumpcap", "-i", "any", "-q", "-f",
+                   "not icmp6[0]==135 and not icmp6[0]==133 and not (ip6[6]==0 and ip6[40]==58)",
+                   "-P", "-w"]
+
+    def __init__(self, config):
+        self.config = config
+        self.config["tmpdir"] = self.get_tmpdir()
+        self.tcpdump = None
+        self.config["pcap"] = os.path.join(self.config["tmpdir"], "deckard.pcap")
+
+    def __enter__(self):
+        cmd = self.DUMPCAP_CMD.copy()
+        cmd.append(self.config["pcap"])
+        self.tcpdump = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+
+    def __exit__(self, _, exc_value, __):
+        if exc_value is not None or self.config.get('noclean'):
+            # Wait for the PCAP to be finalized
+            time.sleep(1)
+
+        self.tcpdump.terminate()
+
+        self.check_for_unknown_server()
+
+        if exc_value is None:
+            if self.config.get('noclean'):
+                # Do not clear files if the server crashed (for analysis)
+                logging.getLogger('deckard.hint').info(
+                    'test working directory %s', self.config["tmpdir"])
+            else:
+                shutil.rmtree(self.config["tmpdir"])
+        else:
+            if isinstance(exc_value, ValueError):
+                self.check_for_icmp()
+            raise
+
+    @staticmethod
+    def get_tmpdir():
+        if "DECKARD_DIR" in os.environ:
+            tmpdir = os.environ["DECKARD_DIR"]
+            if os.path.lexists(tmpdir):
+                raise ValueError('DECKARD_DIR "%s" must not exist' % tmpdir)
+        else:
+            tmpdir = tempfile.mkdtemp(suffix='', prefix='tmpdeckard')
+
+        return tmpdir
+
+    def check_for_icmp(self):
+        """ Checks Deckards's PCAP for ICMP packets """
+        # Deckard's responses to resolvers might be delayed due to load which
+        # leads the resolver to close the port and to the test failing in the
+        # end. We partially detect these by checking the PCAP for ICMP packets.
+        udp_seen = False
+        with open(self.config["pcap"], "rb") as f:
+            pcap = dpkt.pcap.Reader(f)
+            for _, packet in pcap:
+                ip = dpkt.sll.SLL(packet).data
+
+                if isinstance(ip.data, dpkt.udp.UDP):
+                    udp_seen = True
+
+                if udp_seen:
+                    if isinstance(ip.data, (dpkt.icmp.ICMP, dpkt.icmp6.ICMP6)):
+                        raise DeckardUnderLoadError("Deckard is under load. "
+                                                    "Other errors might be false negatives. "
+                                                    "Consider retrying the job later.")
+
+    def check_for_unknown_server(self):
+        unknown_addresses = set()
+        with open(self.config["pcap"], "rb") as f:
+            pcap = dpkt.pcap.Reader(f)
+            for _, packet in pcap:
+                ip = dpkt.sll.SLL(packet).data
+                dest = str(ip_address(ip.dst))
+                if dest not in self.config["if_manager"].added_addresses:
+                    unknown_addresses.add(dest)
+
+        if unknown_addresses:
+            raise RuntimeError("Binary under test queried an IP address not present"
+                               " in scenario %s" % unknown_addresses)
+
+
 def run_test(path, qmin, config, max_retries, retries=0):
     set_coverage_env(path, qmin)
+
     try:
-        del os.environ["SOCKET_WRAPPER_DIR"]
-    except KeyError:
-        pass
-    try:
-        deckard.process_file(path, qmin, config)
+        with LinuxNamespace("net"):
+            config["if_manager"] = InterfaceManager()
+            with TCPDump(config):
+                deckard.process_file(path, qmin, config)
     except deckard.DeckardUnderLoadError as e:
         if retries < max_retries:
             logging.error("Deckard under load. Retrying…")
diff --git a/namespaces.py b/namespaces.py
new file mode 100644
index 0000000..868e5f9
--- /dev/null
+++ b/namespaces.py
@@ -0,0 +1,124 @@
+"""Taken from
+https://github.com/vincentbernat/lldpd/blob/master/tests/integration/fixtures/namespaces.py"""
+
+import contextlib
+import ctypes
+import errno
+import os
+import signal
+
+# All allowed namespace types
+NAMESPACE_FLAGS = dict(mnt=0x00020000,
+                       uts=0x04000000,
+                       ipc=0x08000000,
+                       user=0x10000000,
+                       pid=0x20000000,
+                       net=0x40000000)
+STACKSIZE = 1024*1024
+
+libc = ctypes.CDLL('libc.so.6', use_errno=True)
+
+
+@contextlib.contextmanager
+def keep_directory():
+    """Restore the current directory on exit."""
+    pwd = os.getcwd()
+    try:
+        yield
+    finally:
+        os.chdir(pwd)
+
+
+class LinuxNamespace:
+    """Combine several namespaces into one.
+    This gets a list of namespace types to create and combine into one. The
+    combined namespace can be used as a context manager to enter all the
+    created namespaces and exit them at the end.
+    """
+
+    def __init__(self, *namespaces):
+        self.namespaces = namespaces
+        for ns in namespaces:
+            assert ns in NAMESPACE_FLAGS
+
+        # Get a pipe to signal the future child to exit
+        self.pipe = os.pipe()
+
+        # First, create a child in the given namespaces
+        child = ctypes.CFUNCTYPE(ctypes.c_int)(self.child)
+        child_stack = ctypes.create_string_buffer(STACKSIZE)
+        child_stack_pointer = ctypes.c_void_p(
+            ctypes.cast(child_stack,
+                        ctypes.c_void_p).value + STACKSIZE)
+        flags = signal.SIGCHLD
+        for ns in namespaces:
+            flags |= NAMESPACE_FLAGS[ns]
+        pid = libc.clone(child, child_stack_pointer, flags)
+        if pid == -1:
+            e = ctypes.get_errno()
+            raise OSError(e, os.strerror(e))
+
+        # If a user namespace, map UID 0 to the current one
+        if 'user' in namespaces:
+            uid_map = '0 {} 1'.format(os.getuid())
+            gid_map = '0 {} 1'.format(os.getgid())
+            with open('/proc/{}/uid_map'.format(pid), 'w') as f:
+                f.write(uid_map)
+            with open('/proc/{}/setgroups'.format(pid), 'w') as f:
+                f.write('deny')
+            with open('/proc/{}/gid_map'.format(pid), 'w') as f:
+                f.write(gid_map)
+
+        # Retrieve a file descriptor to this new namespace
+        self.next = [os.open('/proc/{}/ns/{}'.format(pid, x),
+                             os.O_RDONLY) for x in namespaces]
+
+        # Keep a file descriptor to our old namespaces
+        self.previous = [os.open('/proc/self/ns/{}'.format(x),
+                                 os.O_RDONLY) for x in namespaces]
+
+        # Tell the child all is done and let it die
+        os.close(self.pipe[0])
+        if 'pid' not in namespaces:
+            os.close(self.pipe[1])
+            self.pipe = None
+            os.waitpid(pid, 0)
+
+    def __del__(self):
+        pass
+
+    def child(self):
+        """Cloned child.
+        Just be here until our parent extract the file descriptor from
+        us.
+        """
+        os.close(self.pipe[1])
+
+        while True:
+            try:
+                os.read(self.pipe[0], 1)
+            except OSError as e:
+                if e.errno in [errno.EAGAIN, errno.EINTR]:
+                    continue
+            break
+
+        os._exit(0)  # Adopted code. pylint: disable=protected-access
+
+    def fd(self, namespace):
+        """Return the file descriptor associated to a namespace"""
+        assert namespace in self.namespaces
+        return self.next[self.namespaces.index(namespace)]
+
+    def __enter__(self):
+        with keep_directory():
+            for n in self.next:
+                if libc.setns(n, 0) == -1:
+                    ns = self.namespaces[self.next.index(n)]  # NOQA
+                    e = ctypes.get_errno()
+                    raise OSError(e, os.strerror(e))
+
+    def __exit__(self, *exc):
+        pass
+
+    def __repr__(self):
+        return 'Namespace({})'.format(", ".join(self.namespaces))
diff --git a/networking.py b/networking.py
new file mode 100644
index 0000000..04bc28f
--- /dev/null
+++ b/networking.py
@@ -0,0 +1,77 @@
+import subprocess
+from ipaddress import IPv4Network, IPv6Network, ip_address
+from socket import AF_INET, AF_INET6
+
+
+class InterfaceManager:
+    """Wrapper for the `ip` command."""
+
+    def __init__(self,
+                 interface="deckard",
+                 ip4_range=IPv4Network('127.127.0.0/16'),
+                 ip6_range=IPv6Network('fd00:dec::/32')):
+        self.ip4_internal_range = ip4_range
+        self.ip6_internal_range = ip6_range
+        self.ip4_iterator = (host for host in ip4_range)
+        self.ip6_iterator = (host for host in ip6_range)
+        self.added_addresses = set()
+        self.interface = interface
+
+        try:
+            self._setup_interface()
+        except subprocess.CalledProcessError:
+            raise RuntimeError(f"Couldn't set interface `{self.interface}` up.")
+
+    def _setup_interface(self):
+        """Set up a dummy interface with default route as well as loopback.
+           This is done so the resulting PCAP contains as much of the communication
+           as possible (including ICMP Destination unreachable packets etc.)."""
+        # Create and set the interface up.
+        subprocess.run(["ip", "link", "add", "dev", self.interface, "type", "dummy"], check=True)
+        subprocess.run(["ip", "link", "set", "dev", self.interface, "up"], check=True)
+        # Set up default route for both IPv6 and IPv4
+        subprocess.run(["ip", "nei", "add", "169.254.1.1", "lladdr", "21:21:21:21:21:21", "dev",
+                        self.interface], check=True)
+        subprocess.run(["ip", "-6", "nei", "add", "fe80::1", "lladdr", "21:21:21:21:21:21", "dev",
+                        self.interface], check=True)
+        subprocess.run(["ip", "addr", "add", "169.254.1.2/24", "dev", self.interface], check=True)
+        subprocess.run(["ip", "route", "add", "default", "via", "169.254.1.1", "dev",
+                        self.interface], check=True)
+        subprocess.run(["ip", "-6", "route", "add", "default", "via", "fe80::1", "dev",
+                        self.interface], check=True)
+        # Set the loopback up as well since some of the packets go through there.
+        subprocess.run(["ip", "link", "set", "dev", "lo", "up"], check=True)
+
+    def assign_internal_address(self, sockfamily) -> str:
+        """Add and return new address from the internal range"""
+        try:
+            if sockfamily == AF_INET:
+                a = str(next(self.ip4_iterator))
+            elif sockfamily == AF_INET6:
+                a = str(next(self.ip6_iterator))
+            else:
+                raise ValueError(f"Unknown sockfamily {sockfamily}")
+        except StopIteration:
+            raise RuntimeError("Out of addresses.")
+
+        self._add_address(a)
+        return a
+
+    def add_address(self, address: str, check_duplicate=False):
+        """Add an arbitrary new address to the interface"""
+        if address in self.added_addresses and check_duplicate:
+            raise ValueError(f"Tried to add duplicate address {address}")
+        if ip_address(address) in self.ip4_internal_range or \
+           ip_address(address) in self.ip6_internal_range:
+            raise ValueError(f"Address {address} in the internally reserved range.")
+        self._add_address(address, check_duplicate)
+
+    def _add_address(self, address, check_duplicate=False):
+        try:
+            subprocess.run(f"ip addr add {address} dev {self.interface}",
+                           capture_output=True, check=True, shell=True)
+        except subprocess.CalledProcessError as e:
+            if e.stderr != b'RTNETLINK answers: File exists\n':
+                raise ValueError(f"Couldn't add {address}")
+
+        self.added_addresses.add(address)
diff --git a/pydnstest/mock_client.py b/pydnstest/mock_client.py
index 325dd6f..a12b633 100644
--- a/pydnstest/mock_client.py
+++ b/pydnstest/mock_client.py
@@ -61,6 +61,8 @@ def recvfrom_blob(sock: socket.socket,
             else:
                 raise NotImplementedError("[recvfrom_blob]: unknown socket type '%i'" % sock.type)
             return data, addr
+        except socket.timeout:
+            raise RuntimeError("Server took too long to respond")
         except OSError as ex:
             if ex.errno == errno.ENOBUFS:
                 time.sleep(0.1)
@@ -96,11 +98,14 @@ def sendto_msg(sock: socket.socket, message: bytes, addr: Optional[str] = None)
 
 def setup_socket(address: str,
                  port: int,
-                 tcp: bool = False) -> socket.socket:
+                 tcp: bool = False,
+                 src_address: str = None) -> socket.socket:
     family = dns.inet.af_for_address(address)
     sock = socket.socket(family, socket.SOCK_STREAM if tcp else socket.SOCK_DGRAM)
     if tcp:
         sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
+    if src_address is not None:
+        sock.bind((src_address, port))
     sock.settimeout(SOCKET_OPERATION_TIMEOUT)
     sock.connect((address, port))
     return sock
diff --git a/pydnstest/scenario.py b/pydnstest/scenario.py
index 4cc1af0..118a4ac 100644
--- a/pydnstest/scenario.py
+++ b/pydnstest/scenario.py
@@ -527,8 +527,8 @@ class Step:
             self.log.info('')
             self.log.debug(self.data[0].message.to_text())
             # Parse QUERY-specific parameters
-            choice, tcp = None, False
-            return self.__query(ctx, tcp=tcp, choice=choice)
+            choice, tcp, src_address = None, False, ctx.deckard_address
+            return self.__query(ctx, tcp=tcp, choice=choice, src_address=src_address)
         elif self.type == 'CHECK_OUT_QUERY':  # ignore
             self.log.info('')
             return None
@@ -558,7 +558,7 @@ class Step:
             self.log.debug("answer: %s", ctx.last_answer.to_text())
             expected.match(ctx.last_answer)
 
-    def __query(self, ctx, tcp=False, choice=None):
+    def __query(self, ctx, tcp=False, choice=None, src_address=None):
         """
         Send query and wait for an answer (if the query is not RAW).
 
@@ -582,7 +582,8 @@ class Step:
         answer = None
         sock = pydnstest.mock_client.setup_socket(ctx.client[choice][0],
                                                   ctx.client[choice][1],
-                                                  tcp)
+                                                  tcp,
+                                                  src_address=src_address)
         pydnstest.mock_client.send_query(sock, data_to_wire)
         if self.data[0].raw_data is None:
             answer = pydnstest.mock_client.get_answer(sock)
@@ -619,7 +620,7 @@ class Step:
 class Scenario:
     log = logging.getLogger('pydnstest.scenatio.Scenario')
 
-    def __init__(self, node, filename):
+    def __init__(self, node, filename, deckard_address=None):
         """ Initialize scenario with description. """
         self.node = node
         self.info = node.value
@@ -629,6 +630,7 @@ class Scenario:
         self.steps = [Step(n) for n in node.match("/step")]
         self.current_step = None
         self.client = {}
+        self.deckard_address = deckard_address
 
     def __str__(self):
         txt = 'SCENARIO_BEGIN'
@@ -830,7 +832,7 @@ def parse_config(scn_cfg, qmin, installdir):  # FIXME: pylint: disable=too-many-
         "INSTALL_DIR": installdir,
         "QMIN": str(qmin).lower(),
         "TRUST_ANCHORS": trust_anchor_list,
-        "TRUST_ANCHOR_FILES": trust_anchor_files.keys()
+        "TRUST_ANCHOR_FILES": trust_anchor_files
     }
     if stub_addr:
         ctx['ROOT_ADDR'] = stub_addr
@@ -844,10 +846,10 @@ def parse_config(scn_cfg, qmin, installdir):  # FIXME: pylint: disable=too-many-
     ctx['_SOCKET_FAMILY'] = sockfamily
     if override_timestamp:
         ctx['_OVERRIDE_TIMESTAMP'] = override_timestamp
-    return (ctx, trust_anchor_files)
+    return ctx
 
 
-def parse_file(path):
+def parse_file(path, deckard_address=None):
     """ Parse scenario from a file. """
 
     aug = pydnstest.augwrap.AugeasWrapper(
@@ -864,5 +866,5 @@ def parse_file(path):
                 kv = [x.strip() for x in line.split(':', 1)]
                 if len(kv) >= 2:
                     config.append(kv)
-    scenario = Scenario(node["/scenario"], os.path.basename(node.path))
+    scenario = Scenario(node["/scenario"], os.path.basename(node.path), deckard_address)
     return scenario, config
diff --git a/pydnstest/tests/test_parse_config.py b/pydnstest/tests/test_parse_config.py
index 0668760..d8cdea1 100644
--- a/pydnstest/tests/test_parse_config.py
+++ b/pydnstest/tests/test_parse_config.py
@@ -13,5 +13,5 @@ def test_parse_config__trust_anchor():
                [u'trust-anchor', u'"{}"'.format(anchor2)],
                [u'trust-anchor', u'"{}"'.format(anchor3)]]
     args = (anchors, True, os.getcwd())
-    _, ta_files = parse_config(*args)
+    ta_files = parse_config(*args)["TRUST_ANCHOR_FILES"]
     assert sorted(ta_files.values()) == sorted([[anchor1, anchor3], [anchor2]])
diff --git a/pydnstest/testserver.py b/pydnstest/testserver.py
index 0f7d966..b5c17c7 100644
--- a/pydnstest/testserver.py
+++ b/pydnstest/testserver.py
@@ -18,7 +18,8 @@ from pydnstest import scenario, mock_client
 class TestServer:
     """ This simulates UDP DNS server returning scripted or mirror DNS responses. """
 
-    def __init__(self, test_scenario, root_addr, addr_family):
+    def __init__(self, test_scenario, root_addr, addr_family,
+                 deckard_address=None, if_manager=None):
         """ Initialize server instance. """
         self.thread = None
         self.srv_socks = []
@@ -28,12 +29,14 @@ class TestServer:
         self.active_lock = threading.Lock()
         self.condition = threading.Condition()
         self.scenario = test_scenario
+        self.scenario.deckard_address = deckard_address
         self.addr_map = []
         self.start_iface = 2
         self.cur_iface = self.start_iface
         self.kroot_local = root_addr
         self.addr_family = addr_family
         self.undefined_answers = 0
+        self.if_manager = if_manager
 
     def __del__(self):
         """ Cleanup after deletion. """
@@ -181,11 +184,21 @@ class TestServer:
 
         sock = socket.socket(family, socktype, proto)
         sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+        # Add address to interface when running from Deckard
+        if self.if_manager is not None:
+            self.if_manager.add_address(address[0])
+
         try:
             sock.bind(address)
         except Exception as ex:
+            # If this becomes a problem in CI, consider adding retries for `sock.bind`.
+            # A lot of addresses are added to the interface while runnning from Deckard in
+            # the small amount of time which caused ocassional hiccups while binding to them
+            # right afterwards in testing.
             print(ex, address)
             raise
+
         if proto == socket.IPPROTO_TCP:
             sock.listen(5)
         self.srv_socks.append(sock)
@@ -236,7 +249,7 @@ def standalone_self_test():
     Self-test code
 
     Usage:
-    LD_PRELOAD=libsocket_wrapper.so SOCKET_WRAPPER_DIR=/tmp $PYTHON -m pydnstest.testserver --help
+    TMPDIR=/tmp unshare -rn $PYTHON -m pydnstest.testserver --help
     """
     logging.basicConfig(level=logging.DEBUG)
     argparser = argparse.ArgumentParser()
@@ -247,7 +260,7 @@ def standalone_self_test():
     args = argparser.parse_args()
     if args.scenario:
         test_scenario, test_config_text = scenario.parse_file(args.scenario)
-        test_config, _ = scenario.parse_config(test_config_text, True, os.getcwd())
+        test_config = scenario.parse_config(test_config_text, True, os.getcwd())
     else:
         test_scenario, test_config = empty_test_case()
 
diff --git a/run.sh b/run.sh
index 2d1a33c..60e1a94 100755
--- a/run.sh
+++ b/run.sh
@@ -7,6 +7,4 @@ MAKEDIR="$(dirname "$0")"
 test ! -f "${MAKEDIR}/env.sh" && make depend -C "${MAKEDIR}"
 source "${MAKEDIR}/env.sh"
 
-# compatibility with old TESTS= env variable
-# add --scenarios= only if the variable TESTS is non-empty
-python3 -m pytest -c "${MAKEDIR}/deckard_pytest.ini" --tb=short -q ${VERBOSE:+"--log-level=DEBUG"} "${MAKEDIR}" ${DECKARDFLAGS:-} ${TESTS:+"--scenarios=${TESTS}"} "$@"
+python3 -m pytest -c "${MAKEDIR}/deckard_pytest.ini" --tb=short -q ${VERBOSE:+"--log-level=DEBUG"} "${MAKEDIR}" ${DECKARDFLAGS:-} ${TESTS:+"--scenarios=${TESTS}"} --boxed "$@"
diff --git a/sets/resolver/iter_minmaxttl.rpl b/sets/resolver/iter_minmaxttl.rpl
index 7ba7caa..45e0219 100644
--- a/sets/resolver/iter_minmaxttl.rpl
+++ b/sets/resolver/iter_minmaxttl.rpl
@@ -3,6 +3,7 @@
     features: max_ttl = 600
     ; the test is purely about cache so we do not need qmin complexity
     query-minimization: off
+    stub-addr: 1.2.3.4
 CONFIG_END
 
 SCENARIO_BEGIN Test configurable minimum and maximum TTL
diff --git a/sets/resolver/val_wild_pos_multi.rpl b/sets/resolver/val_wild_pos_multi.rpl
index b08e3e2..c11d7f5 100644
--- a/sets/resolver/val_wild_pos_multi.rpl
+++ b/sets/resolver/val_wild_pos_multi.rpl
@@ -9,13 +9,12 @@
 CONFIG_END
 
 SCENARIO_BEGIN Test validation of wildcard responses with multiple synthesized RRs.
-
 ; ns.
 RANGE_BEGIN 0 1000
 	ADDRESS 10.1.1.1
 	ADDRESS 10.2.2.2
 	ADDRESS 10.3.3.3
-	ADDRESS ::1
+	;ADDRESS ::1 ;FIXME: can't use ::1 in tests since the transition to linux namespaces
 	ADDRESS ::2
 	ADDRESS ::3
 
-- 
GitLab


From 7073ff398c399bd181653b8e8ecfbc41b9806848 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Thu, 16 Apr 2020 13:57:52 +0200
Subject: [PATCH 02/10] license: move namespaces to contrib and provide
 licensing information

---
 conftest.py                            |  2 +-
 contrib/__init__.py                    |  0
 contrib/licenses/ISC                   | 15 +++++++++++++++
 namespaces.py => contrib/namespaces.py |  5 +++--
 contrib/namespaces.spdx                | 10 ++++++++++
 deckard_pytest.py                      |  2 +-
 6 files changed, 30 insertions(+), 4 deletions(-)
 create mode 100644 contrib/__init__.py
 create mode 100644 contrib/licenses/ISC
 rename namespaces.py => contrib/namespaces.py (96%)
 create mode 100644 contrib/namespaces.spdx

diff --git a/conftest.py b/conftest.py
index 46a848f..be098da 100644
--- a/conftest.py
+++ b/conftest.py
@@ -7,7 +7,7 @@ from collections import namedtuple
 import pytest
 import yaml
 
-from namespaces import LinuxNamespace
+from contrib.namespaces import LinuxNamespace
 
 Scenario = namedtuple("Scenario", ["path", "qmin", "config"])
 
diff --git a/contrib/__init__.py b/contrib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/contrib/licenses/ISC b/contrib/licenses/ISC
new file mode 100644
index 0000000..e813ff3
--- /dev/null
+++ b/contrib/licenses/ISC
@@ -0,0 +1,15 @@
+SPDX-License-Identifier: ISC
+SPDX-URL: https://spdx.org/licenses/ISC.html
+License-Text:
+
+Permission to use, copy, modify, and/or distribute this software for any purpose
+with or without fee is hereby granted, provided that the above copyright notice
+and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+THIS SOFTWARE.
diff --git a/namespaces.py b/contrib/namespaces.py
similarity index 96%
rename from namespaces.py
rename to contrib/namespaces.py
index 868e5f9..303aec3 100644
--- a/namespaces.py
+++ b/contrib/namespaces.py
@@ -1,5 +1,6 @@
-"""Taken from
-https://github.com/vincentbernat/lldpd/blob/master/tests/integration/fixtures/namespaces.py"""
+# SPDX-License-Identifier: ISC
+# Source: https://vincentbernat.github.io/lldpd/
+# Copyright (c) 2008-2017, Vincent Bernat <vincent@bernat.im>
 
 import contextlib
 import ctypes
diff --git a/contrib/namespaces.spdx b/contrib/namespaces.spdx
new file mode 100644
index 0000000..4eea4e8
--- /dev/null
+++ b/contrib/namespaces.spdx
@@ -0,0 +1,10 @@
+SPDXVersion: SPDX-2.1
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-DOCUMENT
+DocumentName: lldpd-namespaces
+DocumentNamespace: http://spdx.org/spdxdocs/spdx-v2.1-ab80d102-398e-43c0-b978-7a9559f5a76b
+
+PackageName: lldpd-namespaces
+PackageDownloadLocation: git+https://github.com/vincentbernat/lldpd.git@987454994be604a3b8c27e58d68ff7d88fcf24de#tests/integration/fixtures/namespaces.py
+PackageOriginator: Person: Vincent Bernat (vincent@bernat.im)
+PackageLicenseDeclared: ISC
diff --git a/deckard_pytest.py b/deckard_pytest.py
index d5d309a..c60eaa0 100755
--- a/deckard_pytest.py
+++ b/deckard_pytest.py
@@ -12,7 +12,7 @@ import dpkt
 import pytest
 
 import deckard
-from namespaces import LinuxNamespace
+from contrib.namespaces import LinuxNamespace
 from networking import InterfaceManager
 
 
-- 
GitLab


From 5ba72856d0e3de06375325d3ce640f0034a2c11e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Thu, 16 Apr 2020 14:19:21 +0200
Subject: [PATCH 03/10] ci: run Deckard worker in privileged mode

---
 .gitlab-ci.yml    | 17 +++++++++++------
 deckard_pytest.py |  5 +----
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c63b578..c2babc1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,6 +12,11 @@ stages:
     - linux
     - amd64
 
+.privileged_test: &privileged_test
+  stage: test
+  tags:
+    - privileged
+
 test:augeas:
   <<: *test
   script:
@@ -40,20 +45,20 @@ test:rplint:
     - /tmp/compare-rplint.sh
 
 test:unittests:
-  <<: *test
+  <<: *privileged_test
   script:
     - make check
 
 # There are no tests in the repo which use this feature but others do
 # and do not want to cause them breakage
 test:sanity:raw_id:
-  <<: *test
+  <<: *privileged_test
   script:
     - ci/raw_id_check.sh
 
 # changes in Deckard itself must not change result of tests
 test:comparative:kresd:
-  <<: *test
+  <<: *privileged_test
   script:
     # test kresd binary
     - git clone --recurse-submodules -j8 --depth=1 https://gitlab.labs.nic.cz/knot/knot-resolver.git /tmp/kresd-local-build
@@ -78,7 +83,7 @@ test:comparative:kresd:
 # Run all tests on the latest kresd version to ensure that we not push tests
 # which do not work on latest kresd. It would lead to breakage in kresd CI.
 test:latest:kresd:
-  <<: *test
+  <<: *privileged_test
   script:
     - git clone --recurse-submodules -j8 --depth=1 https://gitlab.labs.nic.cz/knot/knot-resolver.git kresd-local-build
     - pushd kresd-local-build
@@ -97,7 +102,7 @@ test:latest:kresd:
 # I've selected the only tests which are working
 # on kresd and Unbound 1.5.8 as well as 1.6.0
 test:sanity:unbound:
-  <<: *test
+  <<: *privileged_test
   script:
     - TMPDIR=$(pwd) ./unbound_run.sh -k sets/resolver/iter_hint_lame.rpl
     - TMPDIR=$(pwd) ./unbound_run.sh -k sets/resolver/iter_lame_root.rpl
@@ -114,7 +119,7 @@ test:sanity:unbound:
 # I've selected couple tests which are working
 # on kresd and PowerDNS recursor 4.0.0~alpha2 as well as 4.0.4
 test:sanity:pdnsrecursor:
-  <<: *test
+  <<: *privileged_test
   script:
     - TMPDIR=$(pwd) ./pdns_run.sh -k sets/resolver/iter_recurse.rpl
     - TMPDIR=$(pwd) ./pdns_run.sh -k sets/resolver/iter_tcbit.rpl
diff --git a/deckard_pytest.py b/deckard_pytest.py
index c60eaa0..106b298 100755
--- a/deckard_pytest.py
+++ b/deckard_pytest.py
@@ -48,10 +48,7 @@ class DeckardUnderLoadError(Exception):
 class TCPDump:
     """This context manager captures a PCAP file and than checks it for obvious errors."""
 
-    # -f option filters out all ICMP messages that are not Destination Unreachable
-    DUMPCAP_CMD = ["dumpcap", "-i", "any", "-q", "-f",
-                   "not icmp6[0]==135 and not icmp6[0]==133 and not (ip6[6]==0 and ip6[40]==58)",
-                   "-P", "-w"]
+    DUMPCAP_CMD = ["dumpcap", "-i", "any", "-q", "-P", "-w"]
 
     def __init__(self, config):
         self.config = config
-- 
GitLab


From f2df618cae718a2632472b67dd44b95e6af4899a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Thu, 16 Apr 2020 20:58:05 +0200
Subject: [PATCH 04/10] deckard.py: preserve original config and edit a copy

---
 deckard.py | 60 +++++++++++++++++++++++++++++-------------------------
 1 file changed, 32 insertions(+), 28 deletions(-)

diff --git a/deckard.py b/deckard.py
index 896d2f3..ad54ab5 100755
--- a/deckard.py
+++ b/deckard.py
@@ -38,7 +38,7 @@ def write_timestamp_file(path, tst):
     time_file.close()
 
 
-def setup_faketime(config):
+def setup_faketime(context):
     """
     Setup environment shared between Deckard and binaries under test.
 
@@ -50,20 +50,20 @@ def setup_faketime(config):
     """
     # Set up libfaketime
     os.environ["FAKETIME_NO_CACHE"] = "1"
-    os.environ["FAKETIME_TIMESTAMP_FILE"] = os.path.join(config["tmpdir"], ".time")
+    os.environ["FAKETIME_TIMESTAMP_FILE"] = os.path.join(context["tmpdir"], ".time")
     os.unsetenv("FAKETIME")
 
     write_timestamp_file(os.environ["FAKETIME_TIMESTAMP_FILE"],
-                         config.get('_OVERRIDE_TIMESTAMP', time.time()))
+                         context.get('_OVERRIDE_TIMESTAMP', time.time()))
 
 
-def setup_daemon_environment(program_config, global_config):
-    program_config["WORKING_DIR"] = os.path.join(global_config["tmpdir"], program_config["name"])
+def setup_daemon_environment(program_config, context):
+    program_config["WORKING_DIR"] = os.path.join(context["tmpdir"], program_config["name"])
     os.mkdir(program_config['WORKING_DIR'])
     program_config["DAEMON_NAME"] = program_config["name"]
     program_config['SELF_ADDR'] = program_config['address']
     program_config['TRUST_ANCHOR_FILES'] = create_trust_anchor_files(
-        global_config["TRUST_ANCHOR_FILES"], program_config['WORKING_DIR'])
+        context["TRUST_ANCHOR_FILES"], program_config['WORKING_DIR'])
 
 
 def create_trust_anchor_files(ta_files, work_dir):
@@ -92,18 +92,18 @@ def create_trust_anchor_files(ta_files, work_dir):
     return full_paths
 
 
-def generate_from_templates(program_config, global_config):
+def generate_from_templates(program_config, context):
     """Generate configuration for the program"""
-    config = global_config.copy()
-    config.update(program_config)
+    template_ctx = context.copy()
+    template_ctx.update(program_config)
 
     j2template_loader = jinja2.FileSystemLoader(searchpath=os.getcwd())
     j2template_env = jinja2.Environment(loader=j2template_loader)
 
-    for template_name, config_name in zip(config['templates'], config['configs']):
+    for template_name, config_name in zip(template_ctx['templates'], template_ctx['configs']):
         j2template = j2template_env.get_template(template_name)
-        cfg_rendered = j2template.render(config)
-        with open(os.path.join(config['WORKING_DIR'], config_name), 'w') as output:
+        cfg_rendered = j2template.render(template_ctx)
+        with open(os.path.join(template_ctx['WORKING_DIR'], config_name), 'w') as output:
             output.write(cfg_rendered)
 
 
@@ -149,19 +149,19 @@ def conncheck_daemon(process, cfg, sockfamily):
     sock.close()
 
 
-def setup_daemons(config):
+def setup_daemons(context):
     """Configure daemons and start them"""
     # Setup daemon environment
     daemons = []
 
-    for program_config in config['programs']:
-        setup_daemon_environment(program_config, config)
-        generate_from_templates(program_config, config)
+    for program_config in context['programs']:
+        setup_daemon_environment(program_config, context)
+        generate_from_templates(program_config, context)
 
         daemon_proc = run_daemon(program_config)
         daemons.append({'proc': daemon_proc, 'cfg': program_config})
         try:
-            conncheck_daemon(daemon_proc, program_config, config['_SOCKET_FAMILY'])
+            conncheck_daemon(daemon_proc, program_config, context['_SOCKET_FAMILY'])
         except:  # noqa  -- bare except might be valid here?
             daemon_proc.terminate()
             raise
@@ -173,10 +173,10 @@ def check_for_reply_steps(case: scenario.Scenario) -> bool:
     return any(s.type == "REPLY" for s in case.steps)
 
 
-def run_testcase(case, daemons, config, prog_under_test_ip):
+def run_testcase(case, daemons, context, prog_under_test_ip):
     """Run actual test and raise exception if the test failed"""
-    server = testserver.TestServer(case, config["ROOT_ADDR"], config["_SOCKET_FAMILY"],
-                                   config["DECKARD_IP"], config["if_manager"])
+    server = testserver.TestServer(case, context["ROOT_ADDR"], context["_SOCKET_FAMILY"],
+                                   context["DECKARD_IP"], context["if_manager"])
     server.start()
 
     try:
@@ -206,29 +206,33 @@ def run_testcase(case, daemons, config, prog_under_test_ip):
 
 def process_file(path, qmin, config):
     """Parse scenario from a file object and create workdir."""
+
+    # Preserve original configuration
+    context = config.copy()
+
     # Parse scenario
     case, case_config_text = scenario.parse_file(os.path.realpath(path))
     case_config = scenario.parse_config(case_config_text, qmin, INSTALLDIR)
 
     # Merge global and scenario configs
-    config.update(case_config)
+    context.update(case_config)
 
     # Asign addresses to the programs and Deckard itself
-    setup_internal_addresses(config)
+    setup_internal_addresses(context)
 
     # Deckard will communicate with first program
-    prog_under_test_ip = config['programs'][0]['address']
+    prog_under_test_ip = context['programs'][0]['address']
 
-    setup_faketime(config)
+    setup_faketime(context)
 
     # Copy the scenario to tmpdir for future reference
-    shutil.copy2(path, os.path.join(config["tmpdir"]))
+    shutil.copy2(path, os.path.join(context["tmpdir"]))
 
     try:
-        daemons = setup_daemons(config)
-        run_testcase(case, daemons, config, prog_under_test_ip)
+        daemons = setup_daemons(context)
+        run_testcase(case, daemons, context, prog_under_test_ip)
 
     except Exception:
         logging.getLogger('deckard.hint').error(
-            'test failed, inspect working directory %s', config["tmpdir"])
+            'test failed, inspect working directory %s', context["tmpdir"])
         raise
-- 
GitLab


From d1de93ed93baa16f33e32b64f00bdbeb1faec526 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Thu, 23 Apr 2020 13:33:40 +0200
Subject: [PATCH 05/10] testserver: add retries to sock.bind

---
 pydnstest/testserver.py | 26 ++++++++++++++++++--------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/pydnstest/testserver.py b/pydnstest/testserver.py
index b5c17c7..2a3a3bf 100644
--- a/pydnstest/testserver.py
+++ b/pydnstest/testserver.py
@@ -2,6 +2,7 @@ import argparse
 import itertools
 import logging
 import os
+import random
 import signal
 import selectors
 import socket
@@ -18,6 +19,8 @@ from pydnstest import scenario, mock_client
 class TestServer:
     """ This simulates UDP DNS server returning scripted or mirror DNS responses. """
 
+    RETRIES_ON_BIND = 3
+
     def __init__(self, test_scenario, root_addr, addr_family,
                  deckard_address=None, if_manager=None):
         """ Initialize server instance. """
@@ -189,15 +192,22 @@ class TestServer:
         if self.if_manager is not None:
             self.if_manager.add_address(address[0])
 
-        try:
-            sock.bind(address)
-        except Exception as ex:
-            # If this becomes a problem in CI, consider adding retries for `sock.bind`.
-            # A lot of addresses are added to the interface while runnning from Deckard in
-            # the small amount of time which caused ocassional hiccups while binding to them
-            # right afterwards in testing.
+        # A lot of addresses are added to the interface while runnning from Deckard in
+        # the small amount of time which caused ocassional hiccups while binding to them
+        # right afterwards in testing. Therefore, we retry a few times.
+        ex = None
+        for i in range(self.RETRIES_ON_BIND):
+            try:
+                sock.bind(address)
+                break
+            except OSError as e:
+                # Exponential backoff
+                time.sleep((2 ** i) + random.random())
+                ex = e
+                continue
+        else:
             print(ex, address)
-            raise
+            raise ex
 
         if proto == socket.IPPROTO_TCP:
             sock.listen(5)
-- 
GitLab


From 4b3d8cd559d886369dd332058e0e7be5e0aef79a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Thu, 23 Apr 2020 14:00:28 +0200
Subject: [PATCH 06/10] check_for_unknown_servers: only check TCP and UDP
 packets

---
 deckard_pytest.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/deckard_pytest.py b/deckard_pytest.py
index 106b298..391bf26 100755
--- a/deckard_pytest.py
+++ b/deckard_pytest.py
@@ -119,6 +119,11 @@ class TCPDump:
             pcap = dpkt.pcap.Reader(f)
             for _, packet in pcap:
                 ip = dpkt.sll.SLL(packet).data
+                try:
+                    if ip.p != dpkt.ip.IP_PROTO_TCP or ip.p != dpkt.ip.IP_PROTO_UDP:
+                        continue
+                except AttributeError:
+                    continue
                 dest = str(ip_address(ip.dst))
                 if dest not in self.config["if_manager"].added_addresses:
                     unknown_addresses.add(dest)
-- 
GitLab


From 7a3048a14b16f2ed99319d91edb6c7c8ae7015c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Fri, 24 Apr 2020 12:26:02 +0200
Subject: [PATCH 07/10] deckard_pytest: wait for the PCAP to be finalized
 everytime

---
 deckard.py        | 2 +-
 deckard_pytest.py | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/deckard.py b/deckard.py
index ad54ab5..1b32f09 100755
--- a/deckard.py
+++ b/deckard.py
@@ -133,7 +133,7 @@ def conncheck_daemon(process, cfg, sockfamily):
     while True:
         time.sleep(0.1)
         if (datetime.now() - tstart).total_seconds() > 5:
-            raise RuntimeError("Server took too long to respond")
+            raise DeckardUnderLoadError("Starting server took too long to respond")
         # Check if the process is running
         if process.poll() is not None:
             msg = 'process died "%s", logs in "%s"' % (cfg['name'], cfg['WORKING_DIR'])
diff --git a/deckard_pytest.py b/deckard_pytest.py
index 391bf26..3a284da 100755
--- a/deckard_pytest.py
+++ b/deckard_pytest.py
@@ -62,11 +62,12 @@ class TCPDump:
         self.tcpdump = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
 
     def __exit__(self, _, exc_value, __):
-        if exc_value is not None or self.config.get('noclean'):
-            # Wait for the PCAP to be finalized
+        # Wait for the PCAP to be finalized
+        while not os.path.exists(self.config["pcap"]):
             time.sleep(1)
 
         self.tcpdump.terminate()
+        self.tcpdump.wait()
 
         self.check_for_unknown_server()
 
-- 
GitLab


From 1c46fdfd54722e65d09e44e6a2cc8855020c158d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Fri, 24 Apr 2020 12:52:13 +0200
Subject: [PATCH 08/10] ci: make raw_id check work in namespaces

---
 .gitlab-ci.yml          | 2 +-
 ci/raw_id_check.sh      | 1 -
 pydnstest/testserver.py | 7 +++++--
 3 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c2babc1..f27581b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -54,7 +54,7 @@ test:unittests:
 test:sanity:raw_id:
   <<: *privileged_test
   script:
-    - ci/raw_id_check.sh
+    - unshare -rn ci/raw_id_check.sh
 
 # changes in Deckard itself must not change result of tests
 test:comparative:kresd:
diff --git a/ci/raw_id_check.sh b/ci/raw_id_check.sh
index 1983e1e..336b37b 100755
--- a/ci/raw_id_check.sh
+++ b/ci/raw_id_check.sh
@@ -2,7 +2,6 @@
 make depend
 cat env.sh
 source env.sh
-export SOCKET_WRAPPER_DIR=/tmp
 python3 -m pydnstest.testserver --scenario $(pwd)/tests/deckard_raw_id.rpl &
 sleep 1
 python3 -m ci.raw_id
\ No newline at end of file
diff --git a/pydnstest/testserver.py b/pydnstest/testserver.py
index 2a3a3bf..b1d6c5b 100644
--- a/pydnstest/testserver.py
+++ b/pydnstest/testserver.py
@@ -14,6 +14,7 @@ import dns.message
 import dns.rdatatype
 
 from pydnstest import scenario, mock_client
+from networking import InterfaceManager
 
 
 class TestServer:
@@ -259,7 +260,7 @@ def standalone_self_test():
     Self-test code
 
     Usage:
-    TMPDIR=/tmp unshare -rn $PYTHON -m pydnstest.testserver --help
+    unshare -rn $PYTHON -m pydnstest.testserver --help
     """
     logging.basicConfig(level=logging.DEBUG)
     argparser = argparse.ArgumentParser()
@@ -283,7 +284,9 @@ def standalone_self_test():
     else:
         test_scenario.current_step = test_scenario.steps[0]
 
-    server = TestServer(test_scenario, test_config['ROOT_ADDR'], test_config['_SOCKET_FAMILY'])
+    if_manager = InterfaceManager(interface="testserver")
+    server = TestServer(test_scenario, test_config['ROOT_ADDR'],
+                        test_config['_SOCKET_FAMILY'], if_manager=if_manager)
     server.start()
 
     logging.info("[==========] Mirror server running at %s", server.address())
-- 
GitLab


From 5d01054248866104a06a375ce1bbb094fdac2c21 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Fri, 24 Apr 2020 14:46:43 +0200
Subject: [PATCH 09/10] ci: reenable sendmsg for Knot Resolver (no swrap
 anymore)

---
 .gitlab-ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f27581b..5ac6318 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -64,7 +64,7 @@ test:comparative:kresd:
     - git clone --recurse-submodules -j8 --depth=1 https://gitlab.labs.nic.cz/knot/knot-resolver.git /tmp/kresd-local-build
     - pushd /tmp/kresd-local-build
     - git log -1
-    - meson build_local --default-library=static --prefix=/tmp/.local -Dsendmmsg=disabled
+    - meson build_local --default-library=static --prefix=/tmp/.local
     - ninja -C build_local install
     - popd
     # compare results from latest Deckard with results from merge base
@@ -88,7 +88,7 @@ test:latest:kresd:
     - git clone --recurse-submodules -j8 --depth=1 https://gitlab.labs.nic.cz/knot/knot-resolver.git kresd-local-build
     - pushd kresd-local-build
     - git log -1
-    - meson build_local --default-library=static --prefix="$PWD/../.local" -Dsendmmsg=disabled
+    - meson build_local --default-library=static --prefix="$PWD/../.local"
     - ninja -C build_local install
     - popd
     - TMPDIR=$(pwd) PATH=$(pwd)/.local/sbin:$PATH ./kresd_run.sh -n $(nproc)
-- 
GitLab


From 91df72dbe2497dd8bdb920b84f334ff36746517e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Bal=C3=A1=C5=BEik?=
 <stepan.balazik@nic.cz>
Date: Fri, 24 Apr 2020 16:31:42 +0200
Subject: [PATCH 10/10] doc: document the switch to network namespaces

---
 README.rst          | 25 ++++++++++---------------
 doc/devel_guide.rst | 19 -------------------
 doc/user_guide.rst  | 32 ++++++++++++++------------------
 3 files changed, 24 insertions(+), 52 deletions(-)

diff --git a/README.rst b/README.rst
index c24156a..a865aec 100644
--- a/README.rst
+++ b/README.rst
@@ -10,7 +10,7 @@ In essence, it works like this:
 - When a binary attempts to contact another server, Deckard intercepts the communication and replies with scripted answer.
 - Deckard can simulate network issues, DNS environment changes, and fake time (for DNSSEC validation tests).
 
-No changes to real network setup are required because all network communications are redirected over UNIX sockets (and recorded to PCAP).
+No changes to real network setup are required because all network communications are made in a network namespace.
 
 Test cases are described by `scenarios <doc/scenario_guide.rst>`_ that contain:
 
@@ -23,7 +23,8 @@ Requirements
 
 Deckard requires following software to be installed:
 
-- Python >= 3.5
+- Python >= 3.6
+- Linux kernel >= 3.8
 - augeas_ - library for editing configuration files
 - dnspython_ - DNS library for Python
 - Jinja2_ - template engine for generating config files
@@ -31,22 +32,18 @@ Deckard requires following software to be installed:
 - python-augeas_ - Python bindings for augeas API
 - pytest_ - testing framework for Python, used for running the test cases
 - pytest-xdist_ - module for pytest for distributed testing
-- custom C libraries (installed automatically, see below)
-
-For convenient use it is strongly recommended to have a C compiler, Git, and ``make`` available.
-First execution of ``make`` will automatically download and compile following libraries:
-
+- dumpcap_ - command line tool for network capture (part of Wireshark)
 - libfaketime_ - embedded because Deckard requires a rather recent version
-- `modified socket_wrapper`_ - custom modification of `original socket_wrapper`_ library (part of the cwrap_ tool set for creating an isolated networks)
 
+For convenient use it is strongly recommended to have a C compiler, Git, and ``make`` available.
 
 Compatibility
 -------------
 
-Works well on Linux, Mac OS X [#]_ and probably all BSDs. Tested with `Knot DNS Resolver`_, `Unbound`_, and `PowerDNS Recursor`_. It should work with other software as well as long as all functions used by the binary under test are supported by our `modified socket_wrapper`_.
-
-.. [#] Python from Homebrew must be used, as the built-in Python is protected by the CSR_ from OS X 10.11 and prevents library injection.
-
+Deckard uses user and network namespaces to simulate the network environment
+so only Linux (with kernel version 3.8 or newer) is supported. It however is possible
+to run Deckard on other platforms in Docker. Just note that your container has to run as
+`--priviledged` for the namespaces to run properly.
 
 Usage
 -----
@@ -88,11 +85,9 @@ Happy testing.
 .. _`PowerDNS Recursor`: https://doc.powerdns.com/md/recursor/
 .. _`PyYAML`: http://pyyaml.org/
 .. _`Unbound`: https://www.unbound.net/
-.. _`cwrap`: https://cwrap.org/
 .. _`dnspython`: http://www.dnspython.org/
 .. _`libfaketime`: https://github.com/wolfcw/libfaketime
-.. _`modified socket_wrapper`: https://gitlab.labs.nic.cz/labs/socket_wrapper
-.. _`original socket_wrapper`: https://cwrap.org/socket_wrapper.html
 .. _`python-augeas`: https://pypi.org/project/python-augeas/
 .. _`pytest`: https://pytest.org/
 .. _`pytest-xdist`: https://pypi.python.org/pypi/pytest-xdist
+.. _`dumpcap`: https://www.wireshark.org/docs/man-pages/dumpcap.html
diff --git a/doc/devel_guide.rst b/doc/devel_guide.rst
index a93b79d..bf2e607 100644
--- a/doc/devel_guide.rst
+++ b/doc/devel_guide.rst
@@ -1,25 +1,6 @@
 Notes for Deckard developers
 ============================
 
-socket wrapper library (cwrap)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Detailed instructions on using cwrap you can be found here_
-
-cwrap environment is managed by Deckard. Default values are sufficient, do not touch the environment unless you are trying to debug something. Variables available for direct use are:
-
-- ``SOCKET_WRAPPER_DIR`` is a generic working directory. It defaults
-  to a new temporary directory with randomly generated name,
-  prefixed by ``tmpdeckard``. When a test fails, the work directory can contain useful
-  information for post-mortem analysis. You can explicitly set ``SOCKET_WRAPPER_DIR``
-  to a custom path for more convenient analysis.
-- ``SOCKET_WRAPPER_DEBUGLEVEL`` is not set by default.
-
-Deckard automatically sets ``SOCKET_WRAPPER_PCAP_FILE`` to create separate PCAP files in working directory for Deckard itself and each daemon. Feel free to inspect them.
-
-.. _here: https://git.samba.org/?p=socket_wrapper.git;a=blob;f=doc/socket_wrapper.1.txt;hb=HEAD
-
-
 libfaketime
 ^^^^^^^^^^^
 Run-time changes to ``FAKETIME_`` environment variables might not be picked up by running process if ``FAKETIME_NO_CACHE=1`` variable is not set before the process starts.
diff --git a/doc/user_guide.rst b/doc/user_guide.rst
index b6e379f..e62d8ec 100644
--- a/doc/user_guide.rst
+++ b/doc/user_guide.rst
@@ -28,15 +28,8 @@ output from Git and C compiler:
 .. code-block::
 
    $ ./kresd_run.sh
-   Submodule 'contrib/libfaketime' (https://github.com/wolfcw/libfaketime.git) registered for path 'contrib/libfaketime'
-   Submodule 'contrib/libswrap' (https://gitlab.labs.nic.cz/labs/socket_wrapper.git) registered for path 'contrib/libswrap'
-      [...]
-   -- The C compiler identification is GNU 6.3.1
-      [...]
-   [ 50%] Building C object src/CMakeFiles/socket_wrapper.dir/socket_wrapper.c.o
-      [...]
-   [100%] Built target socket_wrapper
-   …
+   Submodule 'contrib/libfaketime' (git://github.com/wolfcw/libfaketime.git) registered for path 'contrib/libfaketime'
+   Submodule path 'contrib/libfaketime': checked out '9a2c84d68cca3750d28912010450844004510e81'
 
 For details see `README <../README.rst>`_.
 
@@ -46,17 +39,20 @@ Output is therefore generated by `pytest` as well (``.`` for passed test, ``F``
 .. code-block::
 
    $ ./kresd_run.sh
-   ........s...s...s....................ssss...s.ss.............ssss..s..ss [ 24%]
-   ssss.....sssssssssssssss.sssss.......ssssss.ss...s..s.ss.sss.s.s........ [ 49%]
-   .............ssss....................................................... [ 73%]
-   ........................................................................ [ 98%]
-   ....                                                                     [100%]
-   229 passed, 62 skipped in 76.50 seconds
+   deckard_pytest.py::test_passes_qmin_on[Scenario(path='sets/resolver/black_data.rpl',
+   qmin=False, config={'programs': [{'name': 'kresd', 'binary': 'kresd', 'additional':
+   ['-n'], 'templates': ['template/kresd.j2'], 'configs': ['config']}]})-max-retries-3]
+   SKIPPED [  0%]
 
-.. note:: There is a lot of tests skipped because we run them with query minimization both on and off and some of the scenarios work only with query minimization on (or off respectively). For details see `Scenario guide#Configuration <scenario_guide.rst#configuration-config-end>`_.
+   […many lines later…]
+
+   deckard_pytest.py::test_passes_qmin_off[Scenario(path='sets/resolver/world_mx_nic_www.rpl',
+   qmin=None, config={'programs': [{'name': 'kresd', 'binary': 'kresd', 'additional': ['-n'],
+   'templates': ['template/kresd.j2'], 'configs': ['config']}]})-max-retries-3] PASSED [100%]
 
-          Time elapsed which is printed by `py.test` is often not acurate (or even negative). `py.test` is confused about our time shifting shenanigans done with ``libfaketime``. We can overcome this by using ``-n`` command line argument. See below.
+   ======================= 275 passed, 97 skipped in 316.61s (0:05:16) =======================
 
+.. note:: There is a lot of tests skipped because we run them with query minimization both on and off and some of the scenarios work only with query minimization on (or off respectively). For details see `Scenario guide#Configuration <scenario_guide.rst#configuration-config-end>`_.
 
 Command line arguments
 ----------------------
@@ -282,7 +278,7 @@ Tips:
 - details about scenario format are in `the scenario guide <scenario_guide.rst>`_
 - network traffic from each binary is logged in PCAP format to a file in working directory
 - standard output and error from each binary is logged into log file in working directory
-- working directory can be explicitly specified in environment variable ``SOCKET_WRAPPER_DIR``
+- working directory can be explicitly specified in environment variable ``DECKARD_DIR`
 - command line argument ``--log-level DEBUG`` forces extra verbose logging, including logs from all binaries and packets handled by Deckard
 
 
-- 
GitLab