mirror of
https://github.com/containers/podman.git
synced 2025-05-17 23:26:08 +08:00
Allow empty host port in --publish flag
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 #6806 Signed-off-by: Matthew Heon <matthew.heon@pm.me>
This commit is contained in:
@ -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")
|
||||
// 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
|
||||
}
|
||||
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
|
||||
} 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
|
||||
|
@ -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*
|
||||
|
@ -636,6 +636,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**
|
||||
|
@ -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 {
|
||||
@ -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")
|
||||
}
|
||||
@ -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")
|
||||
if cPort == 0 {
|
||||
return nil, nil, nil, errors.Errorf("container port 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)
|
||||
}
|
||||
ctrPortMap[cPort] = hPort
|
||||
// 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
|
||||
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
|
||||
// If we have an exact duplicate, just continue
|
||||
if testCPort == hPort && testHPort == cPort {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// We appear to be clear. Make an OCICNI port
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
@ -435,7 +435,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.
|
||||
|
@ -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"})
|
||||
|
Reference in New Issue
Block a user