testcontainers-python icon indicating copy to clipboard operation
testcontainers-python copied to clipboard

Bug: DockerClient fails to start with auto-detected network and host bits set

Open torbjoernk opened this issue 2 months ago • 4 comments

Describe the bug

DockerClient.run() errors out on systems where the auto-detected network returns subnets with the host bit set.

Turns out, in DockerClient.find_host_network() each identified subnet is validated by instantiating ipaddress.IPv4Network. By default, this follows a strict validation and if that fails, raises ValueError. However, in DockerClient.find_host_network() is only checked for ipaddress.AddressValueError and ValueError is not handled.

Workaroud

After hot-patching DockerClient.find_host_network() to also ignore ValueError, everything works find (on my machine).

To Reproduce

  • system with a docker environment where at least one subnet contains an address with the host bit set
  • instantiate and launch a Docker container with Testcontainers
  • error:
@pytest.fixture(scope="session")
    def db_container() -> Generator[PostgresContainer]:
>       with PostgresContainer("postgres:16-alpine", driver="asyncpg") as postgres:
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

/[snip]/tests/conftest.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/[snip]/.venv/lib/python3.13/site-packages/testcontainers/core/container.py:216: in __enter__
    return self.start()
           ^^^^^^^^^^^^
/[snip]/.venv/lib/python3.13/site-packages/testcontainers/core/generic.py:77: in start
    super().start()
/[snip]/.venv/lib/python3.13/site-packages/testcontainers/core/container.py:192: in start
    self._container = docker_client.run(
/[snip]/.venv/lib/python3.13/site-packages/testcontainers/core/docker_client.py:47: in wrapper
    return function(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
/[snip]/.venv/lib/python3.13/site-packages/testcontainers/core/docker_client.py:100: in run
    host_network = self.find_host_network()
                   ^^^^^^^^^^^^^^^^^^^^^^^^
/[snip]/.venv/lib/python3.13/site-packages/testcontainers/core/docker_client.py:155: in find_host_network
    subnet = ipaddress.IPv4Network(config["Subnet"])
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = IPv4Network('172.18.251.1/25'), address = '172.18.251.1/25'
strict = True

    def __init__(self, address, strict=True):
        """Instantiate a new IPv4 network object.
    
        Args:
            address: A string or integer representing the IP [& network].
              '192.0.2.0/24'
              '192.0.2.0/255.255.255.0'
              '192.0.2.0/0.0.0.255'
              are all functionally the same in IPv4. Similarly,
              '192.0.2.1'
              '192.0.2.1/255.255.255.255'
              '192.0.2.1/32'
              are also functionally equivalent. That is to say, failing to
              provide a subnetmask will create an object with a mask of /32.
    
              If the mask (portion after the / in the argument) is given in
              dotted quad form, it is treated as a netmask if it starts with a
              non-zero field (e.g. /255.0.0.0 == /8) and as a hostmask if it
              starts with a zero field (e.g. 0.255.255.255 == /8), with the
              single exception of an all-zero mask which is treated as a
              netmask == /0. If no mask is given, a default of /32 is used.
    
              Additionally, an integer can be passed, so
              IPv4Network('192.0.2.1') == IPv4Network(3221225985)
              or, more generally
              IPv4Interface(int(IPv4Interface('192.0.2.1'))) ==
                IPv4Interface('192.0.2.1')
    
        Raises:
            AddressValueError: If ipaddress isn't a valid IPv4 address.
            NetmaskValueError: If the netmask isn't valid for
              an IPv4 address.
            ValueError: If strict is True and a network address is not
              supplied.
        """
        addr, mask = self._split_addr_prefix(address)
    
        self.network_address = IPv4Address(addr)
        self.netmask, self._prefixlen = self._make_netmask(mask)
        packed = int(self.network_address)
        if packed & int(self.netmask) != packed:
            if strict:
>               raise ValueError('%s has host bits set' % self)
E               ValueError: 172.18.251.1/25 has host bits set

Runtime environment

Provide a summary of your runtime environment. Which operating system, python version, and docker version are you using? What is the version of testcontainers-python you are using? You can run the following commands to get the relevant information.

# Get the operating system information (on a unix os).
$ uname -a
Linux basestation-kubuntu 6.8.0-85-generic #85-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep 18 15:26:59 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

# Get the python version.
$ python --version
Python 3.13.2

# Get the docker version and other docker information.
$ docker info
Client: Docker Engine - Community
 Version:    28.5.0
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.29.1
    Path:     /usr/local/lib/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.40.0
    Path:     /usr/local/lib/docker/cli-plugins/docker-compose

Server:
 Containers: 4
  Running: 1
  Paused: 0
  Stopped: 3
 Images: 40
 Server Version: 28.5.0
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: b98a3aace656320842a23f4a392a33f46af97866
 runc version: v1.3.0-0-g4ca628d1
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.8.0-85-generic
 Operating System: Ubuntu 24.04.3 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 16
 Total Memory: 62.72GiB
 Name: basestation-kubuntu
 ID: c9b21f46-3e1f-4b8a-a957-546116146beb
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false

# Get all python packages.
$ pip freeze
aiosqlite==0.21.0
alembic==1.17.0
altair==5.5.0
annotated-types==0.7.0
anyio==4.11.0
argcomplete==3.6.2
argon2-cffi==25.1.0
argon2-cffi-bindings==25.1.0
arrow==1.3.0
asttokens==3.0.0
async-lru==2.0.5
asyncpg==0.30.0
attrs==25.4.0
babel==2.17.0
beautifulsoup4==4.14.2
bleach==6.2.0
blinker==1.9.0
certifi==2025.10.5
cffi==2.0.0
cfgv==3.4.0
charset-normalizer==3.4.4
click==8.3.0
colorama==0.4.6
comm==0.2.3
commitizen==4.9.1
contourpy==1.3.3
coverage==7.11.0
cycler==0.12.1
dash==3.2.0
dash-bootstrap-components==2.0.4
debugpy==1.8.17
decli==0.6.3
decorator==5.2.1
defusedxml==0.7.1
deprecated==1.2.18
distlib==0.4.0
docker==7.1.0
executing==2.2.1
fastapi==0.119.0
fastjsonschema==2.21.2
filelock==3.20.0
flask==3.1.2
fonttools==4.60.1
fqdn==1.5.1
greenlet==3.2.4
gunicorn==23.0.0
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
identify==2.6.15
idna==3.11
importlib-metadata==8.7.0
iniconfig==2.1.0
ipykernel==7.0.1
ipython==9.6.0
ipython-pygments-lexers==1.1.1
isoduration==20.11.0
itsdangerous==2.2.0
jedi==0.19.2
jinja2==3.1.6
json5==0.12.1
jsonpointer==3.0.0
jsonschema==4.25.1
jsonschema-specifications==2025.9.1
jupyter-client==8.6.3
jupyter-core==5.8.1
jupyter-events==0.12.0
jupyter-lsp==2.3.0
jupyter-server==2.17.0
jupyter-server-terminals==0.5.3
jupyterlab==4.4.9
jupyterlab-pygments==0.3.0
jupyterlab-server==2.27.3
kiwisolver==1.4.9
lark==1.3.0
mako==1.3.10
markupsafe==3.0.3
matplotlib==3.10.7
matplotlib-inline==0.1.7
mistune==3.1.4
narwhals==2.8.0
nbclient==0.10.2
nbconvert==7.16.6
nbformat==5.10.4
nest-asyncio==1.6.0
nodeenv==1.9.1
notebook==7.4.7
notebook-shim==0.2.4
numpy==2.3.3
packaging==25.0
pandas==2.3.3
pandocfilters==1.5.1
parso==0.8.5
pexpect==4.9.0
pillow==11.3.0
platformdirs==4.5.0
plotly==6.3.1
pluggy==1.6.0
polars==1.34.0
polars-runtime-32==1.34.0
pre-commit==4.3.0
prometheus-client==0.23.1
prompt-toolkit==3.0.51
psutil==7.1.0
psycopg2-binary==2.9.11
ptyprocess==0.7.0
pure-eval==0.2.3
pycparser==2.23
pydantic==2.12.2
pydantic-core==2.41.4
pydantic-settings==2.11.0
pygments==2.19.2
pyparsing==3.2.5
pytest==8.4.2
pytest-asyncio==0.21.2
pytest-cov==7.0.0
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-json-logger==4.0.0
python-multipart==0.0.20
pytz==2025.2
pyyaml==6.0.3
pyzmq==27.1.0
questionary==2.1.1
referencing==0.37.0
requests==2.32.5
retrying==1.4.2
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rfc3987-syntax==1.1.0
rpds-py==0.27.1
ruff==0.14.0
seaborn==0.13.2
send2trash==1.8.3
setuptools==80.9.0
six==1.17.0
sniffio==1.3.1
soupsieve==2.8
sqlalchemy==2.0.44
sqlmodel==0.0.27
stack-data==0.6.3
starlette==0.48.0
termcolor==3.1.0
terminado==0.18.1
testcontainers==4.13.2
tinycss2==1.4.0
tomlkit==0.13.3
tornado==6.5.2
tqdm==4.67.1
traitlets==5.14.3
types-python-dateutil==2.9.0.20251008
typing-extensions==4.15.0
typing-inspection==0.4.2
tzdata==2025.2
uri-template==1.3.0
urllib3==2.5.0
uvicorn==0.37.0
virtualenv==20.35.3
wcwidth==0.2.14
webcolors==24.11.1
webencodings==0.5.1
websocket-client==1.9.0
werkzeug==3.1.3
wrapt==1.17.3
zipp==3.23.0

torbjoernk avatar Oct 17 '25 08:10 torbjoernk

Addendum: testcontainers v4.1.2 works. It seems, the breaking change was introduced by https://github.com/testcontainers/testcontainers-python/commit/b10d916848cccc016fc457333f7b382b18a7b3ef

torbjoernk avatar Oct 17 '25 09:10 torbjoernk

do you know how you want this patched? is it simply making this not strict? subnet = ipaddress.IPv4Network(config["Subnet"]) - if yes just let me know and i can get this fix out asap, otherwise, i have to catch up a bit and this code is not my favorite or most familiar in the project.

alexanderankin avatar Oct 17 '25 15:10 alexanderankin

@alexanderankin Thank you for your very quick response!

Unfortunately, I'm by far no expert on network specifics - especially not in the Docker context. So, yes, my uneducated poor-man's fix would be to just add strict=False to the instantiation of ipaddress.IPv4Network(). However, I cannot estimate, whether that potentially breaks other people's environments.

TL;DR: I'd prefer a (Docker) networking expert takes a look at this.

torbjoernk avatar Oct 17 '25 16:10 torbjoernk

fair enough, thanks for creating the issue and the reply, I (we) will see what we can do

alexanderankin avatar Oct 17 '25 17:10 alexanderankin