Reject port spec when host range length differs from container port#4988
Reject port spec when host range length differs from container port#4988s3onghyun wants to merge 1 commit into
Conversation
haytok
left a comment
There was a problem hiding this comment.
Thanks for creating this PR!
When the container port is a single port and the host port is specified as a range, Moby uses that range as a pool and dynamically allocates a free host port from it.
- https://github.com/docker/cli/blob/master/cli/command/container/opts.go#L432
- https://github.com/docker/go-connections/blob/main/nat/nat.go#L208C3-L215C4
- https://github.com/moby/moby/blob/master/daemon/libnetwork/portallocator/portallocator.go#L245C2-L281C32
Details of Docker's behavior:
Details
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
> docker run -d -p 127.0.0.1:3000-3001:8080/tcp nginx
c1b08c490d3c80f9614c4dce6259f2699020c0c83c75eb7964913cbace9328eb
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c1b08c490d3c nginx "/docker-entrypoint.…" 4 seconds ago Up 4 seconds 80/tcp, 127.0.0.1:3000->8080/tcp nervous_feynman
> docker run -d -p 127.0.0.1:3000-3001:8080/tcp nginx
f927b67be40779c03ee7c4dd4fb15ba74de2aece81683ec7cd5c91311097423d
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f927b67be407 nginx "/docker-entrypoint.…" 1 second ago Up 1 second 80/tcp, 127.0.0.1:3001->8080/tcp thirsty_yalow
c1b08c490d3c nginx "/docker-entrypoint.…" 10 seconds ago Up 10 seconds 80/tcp, 127.0.0.1:3000->8080/tcp nervous_feynman
nerdctl does not implement dynamic allocation within an explicit host range
As you know, nerdctl currently does not allocate a free host port from the range in this case, so implementing that could be one option.
Therefore, IMO there might be a better approach than this PR.
WDYT?
Docker treats `-p 3000-3001:8080` as a host-port pool: it binds the container port to a free host port from the range, rather than dropping the rest. nerdctl previously collapsed this to the first host port, silently discarding the rest of the range. Implement the Docker-compatible behavior: when the container side is a single port and the host side is a range, pick the first free port in that range (via the existing getUsedPorts) and map the container port to it. A genuine mismatch (both sides are ranges of unequal length) is still rejected. Signed-off-by: Seonghyun Hong <s3onghyun.hong@gmail.com>
ec1ab4c to
40d493f
Compare
|
Great point, @haytok — thank you for the references and the clear Docker walkthrough. You're right that the better fix is to implement the pool behavior rather than reject the spec. I've reworked the PR to do exactly that. When the container side is a single port and the host side is a range (e.g. A genuine mismatch (both sides are ranges of unequal length, e.g.
|
What
ParseFlagPonly rejected a host/container range-length mismatch when the container side was a range. A spec with a host-port range but a single container port slips through:Here
endPort == startPort(8080), so the innerif endPort != startPortguard is skipped, no error is raised, and the emit loop runsendPort-startPort+1 = 1time — producing a single mapping3000 -> 8080and silently dropping host port 3001. The user gets neither the range nor an error.Fix
Drop the inner guard so any host/container range-length mismatch is rejected:
nerdctl doesn't implement Docker's dynamic single-port allocation from within an explicit host range, so erroring is the minimal, deterministic, OS-independent correct behavior — far better than silently losing a port. (If dynamic allocation is desired, that's a larger follow-up; this PR stops the silent data loss.)
Test
Added a
TestParseFlagPcase for127.0.0.1:3000-3001:8080/tcpexpecting the range-mismatch error. Fails before (nil error + bogus single mapping), passes after. Existing range cases unchanged.go test ./pkg/portutil/green.