mirror of
https://github.com/usestrix/strix.git
synced 2026-11-15 09:11:26 +00:00
Merge a8d1da6bc1 into cb581bf93d
This commit is contained in:
commit
f2b2fab29d
5 changed files with 451 additions and 10 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_DOCKER_SANDBOX_NETWORK" type="string">
|
||||
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.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_DOCKER_EXTRA_HOSTS" type="string">
|
||||
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.
|
||||
</ParamField>
|
||||
|
||||
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
|
||||
|
||||
<ParamField path="STRIX_SANDBOX_EXECUTION_TIMEOUT" default="120" type="integer">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
292
tests/test_docker_client_network.py
Normal file
292
tests/test_docker_client_network.py
Normal file
|
|
@ -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()
|
||||
Loading…
Add table
Reference in a new issue