Skip to content

Commit

Permalink
Allow empty host port in --publish flag
Browse files Browse the repository at this point in the history
I didn't believe that this was actually legal, but it looks like
it is. And, unlike our previous understanding (host port being
empty means just use container port), empty host port actually
carries the same meaning as `--expose` + `--publish-all` (that
is, assign a random host port to the given container port). This
requires a significant rework of our port handling code to handle
this new case. I don't foresee this being commonly used, so I
optimized having a fixed port number as fast path, which this
random assignment code running after the main port handling code
only if necessary.

Fixes containers#6806

Signed-off-by: Matthew Heon <[email protected]>
  • Loading branch information
mheon committed Jul 6, 2020
1 parent 56c873c commit 1c02d5a
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 34 deletions.
26 changes: 14 additions & 12 deletions cmd/podman/common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,22 +184,24 @@ func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string)
}
if hostPort != nil {
if *hostPort == "" {
return newPort, errors.Errorf("must provide a non-empty container host port to publish")
}
hostStart, hostLen, err := parseAndValidateRange(*hostPort)
if err != nil {
return newPort, errors.Wrapf(err, "error parsing host port")
}
if hostLen != ctrLen {
return newPort, errors.Errorf("host and container port ranges have different lengths: %d vs %d", hostLen, ctrLen)
// Set 0 as a placeholder. The server side of Specgen
// will find a random, open, unused port to use.
newPort.HostPort = 0
} else {
hostStart, hostLen, err := parseAndValidateRange(*hostPort)
if err != nil {
return newPort, errors.Wrapf(err, "error parsing host port")
}
if hostLen != ctrLen {
return newPort, errors.Errorf("host and container port ranges have different lengths: %d vs %d", hostLen, ctrLen)
}
newPort.HostPort = hostStart
}
newPort.HostPort = hostStart
} else {
newPort.HostPort = newPort.ContainerPort
}

hport := newPort.HostPort
if hport == 0 {
hport = newPort.ContainerPort
}
logrus.Debugf("Adding port mapping from %d to %d length %d protocol %q", hport, newPort.ContainerPort, newPort.Range, newPort.Protocol)

return newPort, nil
Expand Down
2 changes: 2 additions & 0 deletions docs/source/markdown/podman-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,8 @@ When specifying ranges for both, the number of container ports in the range must
(e.g., `podman run -p 1234-1236:1222-1224 --name thisWorks -t busybox`
but not `podman run -p 1230-1236:1230-1240 --name RangeContainerPortsBiggerThanRangeHostPorts -t busybox`)
With ip: `podman run -p 127.0.0.1:$HOSTPORT:$CONTAINERPORT --name CONTAINER -t someimage`
Host port does not have to be specified (e.g. `podman run -p 127.0.0.1::80`).
If it is not, the container port will be randomly assigned a port on the host.
Use `podman port` to see the actual mapping: `podman port CONTAINER $CONTAINERPORT`

**--publish-all**, **-P**=*true|false*
Expand Down
3 changes: 3 additions & 0 deletions docs/source/markdown/podman-run.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,9 @@ Both hostPort and containerPort can be specified as a range of ports.

When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range.

Host port does not have to be specified (e.g. `podman run -p 127.0.0.1::80`).
If it is not, the container port will be randomly assigned a port on the host.

Use **podman port** to see the actual mapping: **podman port $CONTAINER $CONTAINERPORT**.

**--publish-all**, **-P**=**true**|**false**
Expand Down
112 changes: 91 additions & 21 deletions pkg/specgen/generate/ports.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func parsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping,
containerPortValidate[proto] = make(map[string]map[uint16]uint16)
}

postAssignHostPort := false

// Iterate through all port mappings, generating OCICNI PortMapping
// structs and validating there is no overlap.
for _, port := range portMappings {
Expand Down Expand Up @@ -71,9 +73,6 @@ func parsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping,
return nil, nil, nil, errors.Errorf("container port number must be non-0")
}
hostPort := port.HostPort
if hostPort == 0 {
hostPort = containerPort
}
if uint32(len-1)+uint32(containerPort) > 65535 {
return nil, nil, nil, errors.Errorf("container port range exceeds maximum allowable port number")
}
Expand Down Expand Up @@ -105,26 +104,42 @@ func parsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping,
cPort := containerPort + index
hPort := hostPort + index

if cPort == 0 || hPort == 0 {
return nil, nil, nil, errors.Errorf("host and container ports cannot be 0")
}

testCPort := ctrPortMap[cPort]
if testCPort != 0 && testCPort != hPort {
// This is an attempt to redefine a port
return nil, nil, nil, errors.Errorf("conflicting port mappings for container port %d (protocol %s)", cPort, p)
if cPort == 0 {
return nil, nil, nil, errors.Errorf("container port cannot be 0")
}
ctrPortMap[cPort] = hPort

testHPort := hostPortMap[hPort]
if testHPort != 0 && testHPort != cPort {
return nil, nil, nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", hPort, p)
}
hostPortMap[hPort] = cPort

// If we have an exact duplicate, just continue
if testCPort == hPort && testHPort == cPort {
continue
// Host port is allowed to be 0. If it is, we
// select a random port on the host.
// This will happen *after* all other ports are
// placed, to ensure we don't accidentally
// select a port that a later mapping wanted.
if hPort == 0 {
// If we already have a host port
// assigned to their container port -
// just use that.
if ctrPortMap[cPort] != 0 {
hPort = ctrPortMap[cPort]
} else {
postAssignHostPort = true
}
} else {
testCPort := ctrPortMap[cPort]
if testCPort != 0 && testCPort != hPort {
// This is an attempt to redefine a port
return nil, nil, nil, errors.Errorf("conflicting port mappings for container port %d (protocol %s)", cPort, p)
}
ctrPortMap[cPort] = hPort

testHPort := hostPortMap[hPort]
if testHPort != 0 && testHPort != cPort {
return nil, nil, nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", hPort, p)
}
hostPortMap[hPort] = cPort

// If we have an exact duplicate, just continue
if testCPort == hPort && testHPort == cPort {
continue
}
}

// We appear to be clear. Make an OCICNI port
Expand All @@ -142,6 +157,61 @@ func parsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping,
}
}

// Handle any 0 host ports now by setting random container ports.
if postAssignHostPort {
remadeMappings := make([]ocicni.PortMapping, 0, len(finalMappings))

// Iterate over all
for _, p := range finalMappings {
if p.HostPort != 0 {
remadeMappings = append(remadeMappings, p)
continue
}

hostIPMap := hostPortValidate[p.Protocol]
ctrIPMap := containerPortValidate[p.Protocol]

hostPortMap, ok := hostIPMap[p.HostIP]
if !ok {
hostPortMap = make(map[uint16]uint16)
hostIPMap[p.HostIP] = hostPortMap
}
ctrPortMap, ok := ctrIPMap[p.HostIP]
if !ok {
ctrPortMap = make(map[uint16]uint16)
ctrIPMap[p.HostIP] = ctrPortMap
}

// See if container port has been used elsewhere
if ctrPortMap[uint16(p.ContainerPort)] != 0 {
// Duplicate definition. Let's not bother
// including it.
continue
}

// Max retries to ensure we don't loop forever.
for i := 0; i < 15; i++ {
candidate, err := getRandomPort()
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "error getting candidate host port for container port %d", p.ContainerPort)
}

if hostPortMap[uint16(candidate)] == 0 {
logrus.Debugf("Successfully assigned container port %d to host port %d (IP %s Protocol %s)", p.ContainerPort, candidate, p.HostIP, p.Protocol)
hostPortMap[uint16(candidate)] = uint16(p.ContainerPort)
ctrPortMap[uint16(p.ContainerPort)] = uint16(candidate)
p.HostPort = int32(candidate)
break
}
}
if p.HostPort == 0 {
return nil, nil, nil, errors.Errorf("could not find open host port to map container port %d to", p.ContainerPort)
}
remadeMappings = append(remadeMappings, p)
}
return remadeMappings, containerPortValidate, hostPortValidate, nil
}

return finalMappings, containerPortValidate, hostPortValidate, nil
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/specgen/specgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,8 @@ type PortMapping struct {
ContainerPort uint16 `json:"container_port"`
// HostPort is the port number that will be forwarded from the host into
// the container.
// If omitted, will be assumed to be identical to
// If omitted, a random port on the host (guaranteed to be over 1024)
// will be assigned.
HostPort uint16 `json:"host_port,omitempty"`
// Range is the number of ports that will be forwarded, starting at
// HostPort and ContainerPort and counting up.
Expand Down
24 changes: 24 additions & 0 deletions test/e2e/run_networking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,30 @@ var _ = Describe("Podman run networking", func() {
Expect(inspectOut[0].NetworkSettings.Ports["80/tcp"][0].HostIP).To(Equal(""))
})

It("podman run -p 127.0.0.1::8080/udp", func() {
name := "testctr"
session := podmanTest.Podman([]string{"create", "-t", "-p", "127.0.0.1::8080/udp", "--name", name, ALPINE, "/bin/sh"})
session.WaitWithDefaultTimeout()
inspectOut := podmanTest.InspectContainer(name)
Expect(len(inspectOut)).To(Equal(1))
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
Expect(len(inspectOut[0].NetworkSettings.Ports["8080/udp"])).To(Equal(1))
Expect(inspectOut[0].NetworkSettings.Ports["8080/udp"][0].HostPort).To(Not(Equal("8080")))
Expect(inspectOut[0].NetworkSettings.Ports["8080/udp"][0].HostIP).To(Equal("127.0.0.1"))
})

It("podman run -p :8080", func() {
name := "testctr"
session := podmanTest.Podman([]string{"create", "-t", "-p", ":8080", "--name", name, ALPINE, "/bin/sh"})
session.WaitWithDefaultTimeout()
inspectOut := podmanTest.InspectContainer(name)
Expect(len(inspectOut)).To(Equal(1))
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
Expect(len(inspectOut[0].NetworkSettings.Ports["8080/tcp"])).To(Equal(1))
Expect(inspectOut[0].NetworkSettings.Ports["8080/tcp"][0].HostPort).To(Not(Equal("8080")))
Expect(inspectOut[0].NetworkSettings.Ports["8080/tcp"][0].HostIP).To(Equal(""))
})

It("podman run network expose host port 80 to container port 8000", func() {
SkipIfRootless()
session := podmanTest.Podman([]string{"run", "-dt", "-p", "80:8000", ALPINE, "/bin/sh"})
Expand Down

0 comments on commit 1c02d5a

Please sign in to comment.