diff --git a/containers/Dockerfile b/containers/Dockerfile
index a1ff49ef0..f1f52474c 100644
--- a/containers/Dockerfile
+++ b/containers/Dockerfile
@@ -333,4 +333,5 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
USER pentester
WORKDIR /workspace
+LABEL strix.sandbox.caido-loopback="true"
ENTRYPOINT ["docker-entrypoint.sh"]
diff --git a/containers/docker-entrypoint.sh b/containers/docker-entrypoint.sh
index e4f472d62..e1ab0b4f5 100644
--- a/containers/docker-entrypoint.sh
+++ b/containers/docker-entrypoint.sh
@@ -38,7 +38,12 @@ if [ -n "${STRIX_CAIDO_ALLOWED_DOMAINS:-}" ]; then
done
fi
-caido-cli --listen 0.0.0.0:${CAIDO_PORT} \
+# Bridge mode publishes this port on the host loopback. Host networking sets
+# STRIX_CAIDO_LISTEN_ADDRESS=127.0.0.1 so --allow-guests is not reachable on
+# every host interface. Unset keeps the bridge listener.
+CAIDO_LISTEN_ADDRESS="${STRIX_CAIDO_LISTEN_ADDRESS:-0.0.0.0}"
+
+caido-cli --listen "${CAIDO_LISTEN_ADDRESS}:${CAIDO_PORT}" \
--allow-guests \
--no-logging \
--no-open \
diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx
index 29be4c871..88b2be333 100644
--- a/docs/advanced/configuration.mdx
+++ b/docs/advanced/configuration.mdx
@@ -198,6 +198,27 @@ When remote vars are set, Strix dual-writes telemetry to both local JSONL and th
Runtime backend for the sandbox environment.
+
+ Docker network for the sandbox. Leave unset to use the default bridge and publish the Caido port on `127.0.0.1`. Set to `host` when the target is reachable only through a VPN route on a Linux host running a local Docker engine. Any other value is the name of a Docker network to attach.
+
+
+
+ Extra sandbox hosts entries, as a JSON object of hostname to a literal IPv4 or IPv6 address (for example `{"app.htb":"10.10.11.42","v6.htb":"fd00::20"}`). Entries merge with the reserved `host.docker.internal` → `host-gateway` mapping. Empty hostnames, values that are not IP addresses, and replacing `host.docker.internal` are rejected. The host `/etc/hosts` file is not inherited.
+
+
+The sandbox does not copy `/etc/hosts` from the machine running Docker. On a Linux host with a local Docker engine and the VPN already connected, map the lab hostname explicitly:
+
+```bash
+export STRIX_DOCKER_SANDBOX_NETWORK=host
+export STRIX_DOCKER_EXTRA_HOSTS='{"app.htb":"10.10.11.42"}'
+```
+
+`host` shares the Docker engine's network namespace. Use it with a Docker engine on the same Linux machine as Strix. Docker Desktop runs the engine in a virtual machine, so this does not follow a VPN connected on macOS or Windows. A remote daemon (`DOCKER_HOST`) applies host networking on that other machine, while Strix still resolves Caido at `127.0.0.1` on the machine running the CLI.
+
+Host mode binds Caido to the fixed port `48080`, so only one host-mode scan can run on a daemon at a time. The default bridge network publishes that container port to a free localhost port for each scan.
+
+Host mode requires image label `strix.sandbox.caido-loopback=true`. That label marks an entrypoint that honors `STRIX_CAIDO_LISTEN_ADDRESS` and binds Caido to loopback. A cached older image is rejected instead of listening on every host interface.
+
## Sandbox Configuration
diff --git a/strix/runtime/docker_client.py b/strix/runtime/docker_client.py
index 68ffa2fba..7395fa33e 100644
--- a/strix/runtime/docker_client.py
+++ b/strix/runtime/docker_client.py
@@ -3,7 +3,7 @@ NET_ADMIN/NET_RAW capabilities + host-gateway.
The SDK's ``DockerSandboxClient._create_container`` does not expose a hook for
extending ``create_kwargs`` before ``containers.create`` is called. We subclass
-and reimplement the method body verbatim from the SDK source, with three
+and reimplement the method body verbatim from the SDK source, with five
deltas:
1. Drop the SDK's ``entrypoint=["tail"]`` override; supply ``["tail", "-f",
@@ -15,6 +15,12 @@ deltas:
other raw-socket tools).
3. Add ``host.docker.internal`` → host-gateway to ``extra_hosts`` so the
agent can reach host-served apps.
+4. Honor ``STRIX_DOCKER_SANDBOX_NETWORK``. ``host`` omits published ports,
+ tells the entrypoint to bind Caido to loopback, and resolves exposed
+ ports on the daemon loopback. Any other name attaches that network and
+ resolves the container IP. Unset keeps the SDK published-port path.
+5. Merge ``STRIX_DOCKER_EXTRA_HOSTS`` (a JSON hostname-to-IP object) into
+ ``extra_hosts``. ``host.docker.internal`` stays reserved for host-gateway.
Pinned to ``openai-agents==0.14.6``. Bumping the SDK requires
re-merging the parent body. Track upstream for an injection hook.
@@ -23,6 +29,8 @@ re-merging the parent body. Track upstream for an injection hook.
from __future__ import annotations
import contextlib
+import ipaddress
+import json
import logging
import os
import uuid
@@ -52,6 +60,14 @@ logger = logging.getLogger(__name__)
_SANDBOX_NETWORK_ENV = "STRIX_DOCKER_SANDBOX_NETWORK"
+_EXTRA_HOSTS_ENV = "STRIX_DOCKER_EXTRA_HOSTS"
+_HOST_GATEWAY_HOSTNAME = "host.docker.internal"
+_HOST_NETWORK_NAME = "host"
+_DAEMON_LOOPBACK = "127.0.0.1"
+# Read by containers/docker-entrypoint.sh. Set only for host networking so the
+# unauthenticated Caido listener stays on loopback instead of 0.0.0.0.
+_CAIDO_LISTEN_ADDRESS_ENV = "STRIX_CAIDO_LISTEN_ADDRESS"
+_CAIDO_LOOPBACK_LABEL = "strix.sandbox.caido-loopback"
def _sandbox_network() -> str | None:
@@ -59,11 +75,114 @@ def _sandbox_network() -> str | None:
return value or None
-def _apply_sandbox_network(create_kwargs: dict[str, Any]) -> None:
+def _apply_host_network_listener(create_kwargs: dict[str, Any]) -> None:
+ """Point the entrypoint at loopback when the sandbox uses host networking.
+
+ ``docker-entrypoint.sh`` otherwise runs ``caido-cli --listen 0.0.0.0:48080``
+ with guest access enabled. Host networking would expose that proxy on every
+ host interface. Named and bridge networks keep the bridge listener.
+ """
+ raw_environment = create_kwargs.get("environment")
+ if isinstance(raw_environment, dict):
+ environment: dict[str, Any] = dict(cast("dict[str, Any]", raw_environment))
+ else:
+ environment = {}
+ environment[_CAIDO_LISTEN_ADDRESS_ENV] = _DAEMON_LOOPBACK
+ create_kwargs["environment"] = environment
+
+
+def _require_host_network_image(docker_client: Any, image: str) -> None:
+ """Reject host mode when a cached image may still bind Caido on 0.0.0.0."""
+ try:
+ attrs = docker_client.images.get(image).attrs
+ except docker_errors.ImageNotFound:
+ attrs = None
+ config = attrs.get("Config") if isinstance(attrs, dict) else None
+ labels = config.get("Labels") if isinstance(config, dict) else None
+ if isinstance(labels, dict) and labels.get(_CAIDO_LOOPBACK_LABEL) == "true":
+ return
+ raise ValueError(
+ f"{_SANDBOX_NETWORK_ENV}=host requires label {_CAIDO_LOOPBACK_LABEL}=true "
+ f"so {_CAIDO_LISTEN_ADDRESS_ENV} is honored. Cached older images bind "
+ "Caido to 0.0.0.0 with guest access."
+ )
+
+
+def _apply_sandbox_network(create_kwargs: dict[str, Any], docker_client: Any, image: str) -> None:
network = _sandbox_network()
- if network:
- create_kwargs["network"] = network
- create_kwargs.pop("ports", None)
+ if not network:
+ return
+ if network == _HOST_NETWORK_NAME:
+ _require_host_network_image(docker_client, image)
+ create_kwargs["network"] = network
+ create_kwargs.pop("ports", None)
+ if network == _HOST_NETWORK_NAME:
+ _apply_host_network_listener(create_kwargs)
+
+
+def _extra_host_mappings() -> dict[str, str]:
+ """Hostname → literal IP entries from ``STRIX_DOCKER_EXTRA_HOSTS``.
+
+ Unset or blank leaves the built-in host-gateway mapping untouched. A
+ malformed value raises before the container is created.
+ """
+ raw = os.environ.get(_EXTRA_HOSTS_ENV, "").strip()
+ if not raw:
+ return {}
+
+ try:
+ parsed: object = json.loads(raw)
+ except json.JSONDecodeError as exc:
+ raise ValueError(
+ f"{_EXTRA_HOSTS_ENV} must be a JSON object mapping hostnames to "
+ f"IP addresses ({exc.msg})"
+ ) from exc
+
+ if not isinstance(parsed, dict):
+ got = "array" if isinstance(parsed, list) else type(parsed).__name__
+ raise TypeError(
+ f"{_EXTRA_HOSTS_ENV} must be a JSON object mapping hostnames to IP addresses, got {got}"
+ )
+
+ mappings: dict[str, str] = {}
+ for name, address in cast("dict[object, object]", parsed).items():
+ hostname = _extra_hostname(name)
+ mappings[hostname] = _extra_host_address(hostname, address)
+ return mappings
+
+
+def _extra_hostname(name: object) -> str:
+ if not isinstance(name, str):
+ raise TypeError(f"{_EXTRA_HOSTS_ENV} hostnames must be strings")
+ hostname = name.strip()
+ if not hostname:
+ raise ValueError(f"{_EXTRA_HOSTS_ENV} contains an empty hostname")
+ if hostname.lower() == _HOST_GATEWAY_HOSTNAME:
+ raise ValueError(
+ f"{_EXTRA_HOSTS_ENV} cannot replace the reserved {_HOST_GATEWAY_HOSTNAME} mapping"
+ )
+ return hostname
+
+
+def _extra_host_address(hostname: str, address: object) -> str:
+ if not isinstance(address, str):
+ raise TypeError(
+ f"{_EXTRA_HOSTS_ENV} value for {hostname!r} must be an IPv4 or IPv6 address string"
+ )
+ candidate = address.strip()
+ try:
+ ipaddress.ip_address(candidate)
+ except ValueError as exc:
+ raise ValueError(
+ f"{_EXTRA_HOSTS_ENV} value for {hostname!r} is not a literal IPv4 or IPv6 address"
+ ) from exc
+ return candidate
+
+
+def _apply_extra_hosts(create_kwargs: dict[str, Any]) -> None:
+ extra_hosts = create_kwargs.setdefault("extra_hosts", {})
+ extra_hosts[_HOST_GATEWAY_HOSTNAME] = "host-gateway"
+ extra_hosts.update(_extra_host_mappings())
_SANDBOX_DEVICES_ENV = "STRIX_DOCKER_SANDBOX_DEVICES"
@@ -169,6 +288,11 @@ class StrixDockerSandboxSession(DockerSandboxSession):
cause=e,
) from e
+ # Host mode has no container IP. The entrypoint binds Caido to the
+ # daemon loopback on the requested port.
+ if self.sandbox_network == _HOST_NETWORK_NAME:
+ return ExposedPortEndpoint(host=_DAEMON_LOOPBACK, port=port, tls=False)
+
attrs = getattr(self._container, "attrs", {}) or {}
networks = attrs.get("NetworkSettings", {}).get("Networks", {})
endpoint = networks.get(self.sandbox_network) or {}
@@ -256,10 +380,8 @@ class StrixDockerSandboxClient(DockerSandboxClient):
if cap not in cap_add:
cap_add.append(cap)
- extra_hosts = create_kwargs.setdefault("extra_hosts", {})
- extra_hosts["host.docker.internal"] = "host-gateway"
-
- _apply_sandbox_network(create_kwargs)
+ _apply_extra_hosts(create_kwargs)
+ _apply_sandbox_network(create_kwargs, self.docker_client, image)
_apply_sandbox_devices(create_kwargs)
_apply_resource_limits(create_kwargs)
_apply_log_limits(create_kwargs)
diff --git a/tests/test_docker_client_network.py b/tests/test_docker_client_network.py
new file mode 100644
index 000000000..796f85255
--- /dev/null
+++ b/tests/test_docker_client_network.py
@@ -0,0 +1,292 @@
+"""Sandbox network override and explicit hostname mappings.
+
+Host networking cannot use the SDK's published-port resolver: Docker reports
+no container IP, and published ports are omitted. ``STRIX_DOCKER_EXTRA_HOSTS``
+adds target names the sandbox does not inherit from the host hosts file.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+from unittest.mock import MagicMock
+
+import pytest
+from agents.sandbox.errors import ExposedPortUnavailableError
+from agents.sandbox.manifest import Environment, Manifest
+from agents.sandbox.sandboxes.docker import DockerSandboxClientOptions, DockerSandboxSession
+from agents.sandbox.types import ExposedPortEndpoint
+from docker import errors as docker_errors
+
+from strix.runtime.docker_client import StrixDockerSandboxClient, StrixDockerSandboxSession
+
+
+_IMAGE = "strix-sandbox:test"
+_CAIDO_PORT = 48080
+_GATEWAY = {"host.docker.internal": "host-gateway"}
+_CAIDO_LOOPBACK_LABEL = "strix.sandbox.caido-loopback"
+
+
+def _clear_network_env(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("STRIX_DOCKER_SANDBOX_NETWORK", raising=False)
+ monkeypatch.delenv("STRIX_DOCKER_EXTRA_HOSTS", raising=False)
+
+
+def _client(
+ *, caido_loopback: bool = False
+) -> tuple[StrixDockerSandboxClient, MagicMock, MagicMock]:
+ docker = MagicMock()
+ container = MagicMock()
+ container.id = "container-1"
+ container.short_id = "container-1"
+ docker.containers.create.return_value = container
+ if caido_loopback:
+ image = MagicMock()
+ image.attrs = {"Config": {"Labels": {_CAIDO_LOOPBACK_LABEL: "true"}}}
+ docker.images.get.return_value = image
+ return StrixDockerSandboxClient(docker), docker, container
+
+
+def _created_kwargs(docker: MagicMock) -> dict[str, Any]:
+ kwargs: dict[str, Any] = docker.containers.create.call_args.kwargs
+ return kwargs
+
+
+async def _create_session(
+ monkeypatch: pytest.MonkeyPatch,
+ network: str | None,
+ *,
+ manifest: Manifest | None = None,
+) -> tuple[Any, MagicMock, MagicMock]:
+ _clear_network_env(monkeypatch)
+ if network is not None:
+ monkeypatch.setenv("STRIX_DOCKER_SANDBOX_NETWORK", network)
+ client, docker, container = _client(caido_loopback=network == "host")
+ session = await client.create(
+ options=DockerSandboxClientOptions(_IMAGE, (_CAIDO_PORT,)),
+ manifest=manifest,
+ )
+ return session, docker, container
+
+
+@pytest.mark.asyncio
+async def test_default_network_publishes_localhost_and_keeps_entrypoint(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ _clear_network_env(monkeypatch)
+ client, docker, _container = _client()
+
+ await client._create_container(_IMAGE, exposed_ports=(_CAIDO_PORT,))
+
+ kwargs = _created_kwargs(docker)
+ docker.images.get.assert_called_with(_IMAGE)
+ docker.images.pull.assert_not_called()
+ assert kwargs["ports"] == {f"{_CAIDO_PORT}/tcp": ("127.0.0.1", None)}
+ assert kwargs["command"] == ["tail", "-f", "/dev/null"]
+ assert "entrypoint" not in kwargs
+ assert kwargs["cap_add"] == ["NET_ADMIN", "NET_RAW"]
+ assert kwargs["extra_hosts"] == _GATEWAY
+ assert "network" not in kwargs
+ assert kwargs["environment"] is None
+
+
+@pytest.mark.asyncio
+async def test_host_network_resolves_loopback_without_container_ip(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ manifest = Manifest(environment=Environment(value={"SCAN_TARGET": "app.htb"}))
+ session, docker, container = await _create_session(monkeypatch, "host", manifest=manifest)
+
+ kwargs = _created_kwargs(docker)
+ assert "ports" not in kwargs
+ assert kwargs["network"] == "host"
+ assert kwargs["environment"] == {
+ "SCAN_TARGET": "app.htb",
+ "STRIX_CAIDO_LISTEN_ADDRESS": "127.0.0.1",
+ }
+ assert kwargs["extra_hosts"] == _GATEWAY
+ assert type(session._inner) is StrixDockerSandboxSession
+
+ container.attrs = {
+ "NetworkSettings": {
+ "Networks": {"host": {"IPAddress": "", "GlobalIPv6Address": ""}},
+ }
+ }
+ endpoint = await session.resolve_exposed_port(_CAIDO_PORT)
+ assert endpoint == ExposedPortEndpoint(host="127.0.0.1", port=_CAIDO_PORT, tls=False)
+ container.reload.assert_called()
+
+ container.reload.side_effect = docker_errors.APIError("reload failed")
+ with pytest.raises(ExposedPortUnavailableError) as exc_info:
+ await session.resolve_exposed_port(_CAIDO_PORT)
+ assert exc_info.value.context["detail"] == "container_reload_failed"
+
+
+@pytest.mark.asyncio
+async def test_host_network_rejects_image_without_caido_loopback_label(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ _clear_network_env(monkeypatch)
+ monkeypatch.setenv("STRIX_DOCKER_SANDBOX_NETWORK", "host")
+ client, docker, _container = _client()
+ docker.images.get.return_value.attrs = {"Config": {"Labels": None}}
+ with pytest.raises(ValueError, match="caido-loopback"):
+ await client._create_container(_IMAGE, exposed_ports=(_CAIDO_PORT,))
+ docker.containers.create.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_named_network_resolves_ipv4_and_ipv6(monkeypatch: pytest.MonkeyPatch) -> None:
+ session, docker, container = await _create_session(monkeypatch, "lab")
+
+ kwargs = _created_kwargs(docker)
+ assert kwargs["network"] == "lab"
+ assert "ports" not in kwargs
+ assert kwargs["environment"] == {}
+ assert type(session._inner) is StrixDockerSandboxSession
+ assert session._inner.sandbox_network == "lab"
+
+ container.attrs = {
+ "NetworkSettings": {"Networks": {"lab": {"IPAddress": "172.18.0.8"}}},
+ }
+ assert await session.resolve_exposed_port(_CAIDO_PORT) == ExposedPortEndpoint(
+ host="172.18.0.8", port=_CAIDO_PORT, tls=False
+ )
+
+ container.attrs = {
+ "NetworkSettings": {
+ "Networks": {"lab": {"IPAddress": "", "GlobalIPv6Address": "fd00::8"}},
+ }
+ }
+ assert await session.resolve_exposed_port(_CAIDO_PORT) == ExposedPortEndpoint(
+ host="[fd00::8]", port=_CAIDO_PORT, tls=False
+ )
+
+
+@pytest.mark.asyncio
+async def test_named_network_missing_address_and_reload_failure(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ session, _docker, container = await _create_session(monkeypatch, "lab")
+ container.attrs = {"NetworkSettings": {"Networks": {"lab": {"IPAddress": ""}}}}
+
+ with pytest.raises(ExposedPortUnavailableError) as missing:
+ await session.resolve_exposed_port(_CAIDO_PORT)
+ assert missing.value.context["detail"] == "container_not_on_network"
+
+ container.reload.side_effect = docker_errors.APIError("reload failed")
+ with pytest.raises(ExposedPortUnavailableError) as failed:
+ await session.resolve_exposed_port(_CAIDO_PORT)
+ assert failed.value.context["detail"] == "container_reload_failed"
+
+
+@pytest.mark.parametrize("network", [None, "", " "])
+@pytest.mark.asyncio
+async def test_unset_network_keeps_sdk_published_port_resolver(
+ monkeypatch: pytest.MonkeyPatch,
+ network: str | None,
+) -> None:
+ session, docker, container = await _create_session(monkeypatch, network)
+
+ kwargs = _created_kwargs(docker)
+ assert "network" not in kwargs
+ assert kwargs["ports"] == {f"{_CAIDO_PORT}/tcp": ("127.0.0.1", None)}
+ assert type(session._inner) is DockerSandboxSession
+ container.attrs = {
+ "NetworkSettings": {
+ "Ports": {f"{_CAIDO_PORT}/tcp": [{"HostIp": "127.0.0.1", "HostPort": "35123"}]},
+ "Networks": {},
+ }
+ }
+ endpoint = await session.resolve_exposed_port(_CAIDO_PORT)
+ assert endpoint == ExposedPortEndpoint(host="127.0.0.1", port=35123, tls=False)
+
+
+@pytest.mark.parametrize("network", [None, "host"])
+@pytest.mark.parametrize(
+ ("raw", "expected"),
+ [
+ ('{"app.htb":"10.10.11.42"}', {"app.htb": "10.10.11.42"}),
+ (
+ '{"app.htb":"10.10.11.42","db.htb":"10.10.11.7","v6.htb":"fd00::10"}',
+ {"app.htb": "10.10.11.42", "db.htb": "10.10.11.7", "v6.htb": "fd00::10"},
+ ),
+ ],
+)
+@pytest.mark.asyncio
+async def test_extra_hosts_merge_with_gateway(
+ monkeypatch: pytest.MonkeyPatch,
+ network: str | None,
+ raw: str,
+ expected: dict[str, str],
+) -> None:
+ _clear_network_env(monkeypatch)
+ if network is not None:
+ monkeypatch.setenv("STRIX_DOCKER_SANDBOX_NETWORK", network)
+ monkeypatch.setenv("STRIX_DOCKER_EXTRA_HOSTS", raw)
+ client, docker, _container = _client(caido_loopback=network == "host")
+
+ await client._create_container(_IMAGE, exposed_ports=(_CAIDO_PORT,))
+
+ assert _created_kwargs(docker)["extra_hosts"] == {**_GATEWAY, **expected}
+
+
+@pytest.mark.parametrize("raw", [None, "", " "])
+@pytest.mark.asyncio
+async def test_blank_extra_hosts_keep_gateway_mapping(
+ monkeypatch: pytest.MonkeyPatch,
+ raw: str | None,
+) -> None:
+ _clear_network_env(monkeypatch)
+ if raw is not None:
+ monkeypatch.setenv("STRIX_DOCKER_EXTRA_HOSTS", raw)
+ client, docker, _container = _client()
+
+ await client._create_container(_IMAGE, exposed_ports=(_CAIDO_PORT,))
+
+ assert _created_kwargs(docker)["extra_hosts"] == _GATEWAY
+
+
+@pytest.mark.parametrize(
+ ("raw", "exc_type", "match"),
+ [
+ ("{", ValueError, r"STRIX_DOCKER_EXTRA_HOSTS must be a JSON object"),
+ ('["app.htb"]', TypeError, r"got array"),
+ (
+ '{"app.htb": 1}',
+ TypeError,
+ r"value for 'app\.htb' must be an IPv4 or IPv6 address string",
+ ),
+ (
+ '{"app.htb": "not-an-ip"}',
+ ValueError,
+ r"value for 'app\.htb' is not a literal IPv4 or IPv6 address",
+ ),
+ ('{"": "10.0.0.1"}', ValueError, r"empty hostname"),
+ ('{" ": "10.0.0.1"}', ValueError, r"empty hostname"),
+ (
+ '{"host.docker.internal": "10.0.0.1"}',
+ ValueError,
+ r"cannot replace the reserved host\.docker\.internal mapping",
+ ),
+ (
+ '{"HOST.Docker.Internal": "10.0.0.1"}',
+ ValueError,
+ r"cannot replace the reserved host\.docker\.internal mapping",
+ ),
+ ],
+)
+@pytest.mark.asyncio
+async def test_extra_hosts_rejected_before_container_create(
+ monkeypatch: pytest.MonkeyPatch,
+ raw: str,
+ exc_type: type[Exception],
+ match: str,
+) -> None:
+ _clear_network_env(monkeypatch)
+ monkeypatch.setenv("STRIX_DOCKER_EXTRA_HOSTS", raw)
+ client, docker, _container = _client()
+
+ with pytest.raises(exc_type, match=match):
+ await client._create_container(_IMAGE, exposed_ports=(_CAIDO_PORT,))
+
+ docker.containers.create.assert_not_called()