Class LoopbackPortPublisher

java.lang.Object
io.github.flink.gcp.connector.testutils.LoopbackPortPublisher
All Implemented Interfaces:
org.testcontainers.core.CreateContainerCmdModifier

@Internal public final class LoopbackPortPublisher extends Object implements org.testcontainers.core.CreateContainerCmdModifier
Publishes every container's ports on the loopback address the Docker host resolves to, rather than on the wildcard address, so a host-port collision with an unrelated local process cannot form (ADR-0132).

Docker's default publish is the wildcard address, and that coexists with a process already holding 127.0.0.1:<port>: the more specific bind wins, so a client resolving localhost to the IPv4 loopback reaches the stranger while the container sits there healthy. Asking for a loopback address without a port makes the kernel pick a port that is free on that address, so the collision has nowhere to form.

This is registered through META-INF/services rather than applied per harness because GenericContainer loads the SPI per container instance, which reaches containers no harness in this repository constructs — testcontainers' own Ryuk reaper above all. Ryuk is contacted before any harness runs, so a collision on its port fails a whole module's integration tests with a message naming no connector at all. No per-harness change can reach that.

What this does not cost. curl http://localhost:<port> still works. #1021 expected it to break, because curl prefers ::1 and nothing is bound there any more — measured 2026-08-22 with curl 8.7.1 on macOS, it tries ::1, is refused immediately, falls back to 127.0.0.1 and connects. Prefer curl http://127.0.0.1:<port> anyway: the fallback rests on that refusal being instant, and if something is listening on ::1:<port> curl reaches it rather than the container — the same shadowing hazard, moved to IPv6, and now reachable only by a hand-run curl rather than by the JVM.

Only a loopback Docker host, and the guard is an address test. Against a daemon reached at a routable address — DOCKER_HOST=tcp://…, Testcontainers Cloud — a loopback address is the daemon's, which the test JVM cannot reach, so the wildcard publish is left alone. What that test cannot see is a daemon reached at a loopback address but running elsewhere: an SSH tunnel on tcp://127.0.0.1:2375, or a VM whose port forwarder only carries wildcard-bound guest ports. There the modifier engages and binds inside the daemon, and the connection is refused. Nothing distinguishes that from a local daemon — a local one exposed over tcp://127.0.0.1 is a setup this should serve — so the escape is explicit rather than inferred: pass -Dflink.gcp.tests.loopback-publish=false. Maven forwards a command-line -D into the surefire fork, and .mvn/maven.config or MAVEN_OPTS carries it for a shell that sets no flags per run.

That restores testcontainers' own wildcard publish. The property is read per container rather than once, so clearing it mid-run re-engages the rewrite for later containers. Docker Desktop for macOS — itself VM-backed — is measured working without it; colima and Rancher Desktop are not measured, which is what the escape is for.

ADR-0132 carries the evidence, the declined alternatives, and the dependency-bump tripwire that this class's survival rests on.

  • Constructor Details

    • LoopbackPortPublisher

      public LoopbackPortPublisher()
      Required by ServiceLoader, which is the only intended caller.
  • Method Details

    • modify

      public com.github.dockerjava.api.command.CreateContainerCmd modify(com.github.dockerjava.api.command.CreateContainerCmd createContainerCmd)
      Specified by:
      modify in interface org.testcontainers.core.CreateContainerCmdModifier