daemon: return port-mappings from all endpoints

With improved IPv6 support, a dual-stack container can map a port using
two different networks -- one IPv4-only, the other IPv6-only.

The daemon was updating containers' `EndpointSettings.Ports` by looking
for the first network providing port-mappings. This was incorrect.

Instead, iterate over the whole list of endpoints, and merge everything
together.

The function doing that, ie. `getEndpointPortMapInfo`, is also
considered exposed ports, and nil the PortMap entry if an exposed port
is found. However, exposed ports are always set on a bridge network, so
this was erasing port-mappings found for other networks.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
This commit is contained in:
Albin Kerouanton
2025-03-18 16:17:13 +01:00
parent 6b3b479192
commit f2a183a991
2 changed files with 47 additions and 16 deletions

View File

@@ -1005,24 +1005,16 @@ func getPortMapInfo(sb *libnetwork.Sandbox) nat.PortMap {
}
for _, ep := range sb.Endpoints() {
pm = getEndpointPortMapInfo(ep)
if len(pm) > 0 {
break
}
getEndpointPortMapInfo(pm, ep)
}
return pm
}
func getEndpointPortMapInfo(ep *libnetwork.Endpoint) nat.PortMap {
pm := nat.PortMap{}
driverInfo, err := ep.DriverInfo()
if err != nil {
return pm
}
func getEndpointPortMapInfo(pm nat.PortMap, ep *libnetwork.Endpoint) {
driverInfo, _ := ep.DriverInfo()
if driverInfo == nil {
// It is not an error for epInfo to be nil
return pm
return
}
if expData, ok := driverInfo[netlabel.ExposedPorts]; ok {
@@ -1033,14 +1025,16 @@ func getEndpointPortMapInfo(ep *libnetwork.Endpoint) nat.PortMap {
log.G(context.TODO()).Errorf("invalid exposed port %s: %v", tp.String(), err)
continue
}
pm[natPort] = nil
if _, ok := pm[natPort]; !ok {
pm[natPort] = nil
}
}
}
}
mapData, ok := driverInfo[netlabel.PortMap]
if !ok {
return pm
return
}
if portMapping, ok := mapData.([]lntypes.PortBinding); ok {
@@ -1059,8 +1053,6 @@ func getEndpointPortMapInfo(ep *libnetwork.Endpoint) nat.PortMap {
pm[natPort] = append(pm[natPort], natBndg)
}
}
return pm
}
// buildEndpointInfo sets endpoint-related fields on container.NetworkSettings based on the provided network and endpoint.

View File

@@ -454,3 +454,42 @@ func TestPublishedPortAlreadyInUse(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.Equal(inspect.State.Status, "created"))
}
// TestAllPortMappingsAreReturned check that dual-stack ports mapped through
// different networks are correctly reported as dual-stakc.
//
// Regression test for https://github.com/moby/moby/issues/49654.
func TestAllPortMappingsAreReturned(t *testing.T) {
ctx := setupTest(t)
d := daemon.New(t)
d.StartWithBusybox(ctx, t, "--userland-proxy=false")
defer d.Stop(t)
apiClient := d.NewClientT(t)
defer apiClient.Close()
nwV4 := network.CreateNoError(ctx, t, apiClient, "testnetv4")
defer network.RemoveNoError(ctx, t, apiClient, nwV4)
nwV6 := network.CreateNoError(ctx, t, apiClient, "testnetv6",
network.WithIPv4(false),
network.WithIPv6())
defer network.RemoveNoError(ctx, t, apiClient, nwV6)
ctrID := ctr.Run(ctx, t, apiClient,
ctr.WithExposedPorts("80/tcp", "81/tcp"),
ctr.WithPortMap(nat.PortMap{"80/tcp": {{HostPort: "8000"}}}),
ctr.WithEndpointSettings("testnetv4", &networktypes.EndpointSettings{}),
ctr.WithEndpointSettings("testnetv6", &networktypes.EndpointSettings{}))
defer ctr.Remove(ctx, t, apiClient, ctrID, containertypes.RemoveOptions{Force: true})
inspect := ctr.Inspect(ctx, t, apiClient, ctrID)
assert.DeepEqual(t, inspect.NetworkSettings.Ports, nat.PortMap{
"80/tcp": []nat.PortBinding{
{HostIP: "0.0.0.0", HostPort: "8000"},
{HostIP: "::", HostPort: "8000"},
},
"81/tcp": nil,
})
}