podman/pkg/specgen/generate/ports.go

429 lines
13 KiB
Go

package generate
import (
"context"
"net"
"strconv"
"strings"
"github.com/containers/common/libimage"
"github.com/containers/podman/v3/utils"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/cri-o/ocicni/pkg/ocicni"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
protoTCP = "tcp"
protoUDP = "udp"
protoSCTP = "sctp"
)
// Parse port maps to OCICNI port mappings.
// Returns a set of OCICNI port mappings, and maps of utilized container and
// host ports.
func ParsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping, map[string]map[string]map[uint16]uint16, map[string]map[string]map[uint16]uint16, error) {
// First, we need to validate the ports passed in the specgen, and then
// convert them into CNI port mappings.
type tempMapping struct {
mapping ocicni.PortMapping
startOfRange bool
isInRange bool
}
tempMappings := []tempMapping{}
// To validate, we need two maps: one for host ports, one for container
// ports.
// Each is a map of protocol to map of IP address to map of port to
// port (for hostPortValidate, it's host port to container port;
// for containerPortValidate, container port to host port.
// These will ensure no collisions.
hostPortValidate := make(map[string]map[string]map[uint16]uint16)
containerPortValidate := make(map[string]map[string]map[uint16]uint16)
// Initialize the first level of maps (we can't really guess keys for
// the rest).
for _, proto := range []string{protoTCP, protoUDP, protoSCTP} {
hostPortValidate[proto] = make(map[string]map[uint16]uint16)
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 {
// First, check proto
protocols, err := checkProtocol(port.Protocol, true)
if err != nil {
return nil, nil, nil, err
}
// Validate host IP
hostIP := port.HostIP
if hostIP == "" {
hostIP = "0.0.0.0"
}
if ip := net.ParseIP(hostIP); ip == nil {
return nil, nil, nil, errors.Errorf("invalid IP address %s in port mapping", port.HostIP)
}
// Validate port numbers and range.
len := port.Range
if len == 0 {
len = 1
}
containerPort := port.ContainerPort
if containerPort == 0 {
return nil, nil, nil, errors.Errorf("container port number must be non-0")
}
hostPort := port.HostPort
if uint32(len-1)+uint32(containerPort) > 65535 {
return nil, nil, nil, errors.Errorf("container port range exceeds maximum allowable port number")
}
if uint32(len-1)+uint32(hostPort) > 65536 {
return nil, nil, nil, errors.Errorf("host port range exceeds maximum allowable port number")
}
// Iterate through ports, populating maps to check for conflicts
// and generating CNI port mappings.
for _, p := range protocols {
hostIPMap := hostPortValidate[p]
ctrIPMap := containerPortValidate[p]
hostPortMap, ok := hostIPMap[hostIP]
if !ok {
hostPortMap = make(map[uint16]uint16)
hostIPMap[hostIP] = hostPortMap
}
ctrPortMap, ok := ctrIPMap[hostIP]
if !ok {
ctrPortMap = make(map[uint16]uint16)
ctrIPMap[hostIP] = ctrPortMap
}
// Iterate through all port numbers in the requested
// range.
var index uint16
for index = 0; index < len; index++ {
cPort := containerPort + index
hPort := hostPort
// Only increment host port if it's not 0.
if hostPort != 0 {
hPort += index
}
if cPort == 0 {
return nil, nil, nil, errors.Errorf("container port cannot be 0")
}
// 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 {
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
// Mapping a container port to multiple
// host ports is allowed.
// We only store the latest of these in
// the container port map - we don't
// need to know all of them, just one.
testCPort := ctrPortMap[cPort]
ctrPortMap[cPort] = hPort
// If we have an exact duplicate, just continue
if testCPort == hPort && testHPort == cPort {
continue
}
}
// We appear to be clear. Make an OCICNI port
// struct.
// Don't use hostIP - we want to preserve the
// empty string hostIP by default for compat.
cniPort := ocicni.PortMapping{
HostPort: int32(hPort),
ContainerPort: int32(cPort),
Protocol: p,
HostIP: port.HostIP,
}
tempMappings = append(
tempMappings,
tempMapping{
mapping: cniPort,
startOfRange: port.Range > 1 && index == 0,
isInRange: port.Range > 1,
},
)
}
}
}
// Handle any 0 host ports now by setting random container ports.
if postAssignHostPort {
remadeMappings := make([]ocicni.PortMapping, 0, len(tempMappings))
var (
candidate int
err error
)
// Iterate over all
for _, tmp := range tempMappings {
p := tmp.mapping
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++ {
// Only get a random candidate for single entries or the start
// of a range. Otherwise we just increment the candidate.
if !tmp.isInRange || tmp.startOfRange {
candidate, err = utils.GetRandomPort()
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "error getting candidate host port for container port %d", p.ContainerPort)
}
} else {
candidate++
}
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
}
finalMappings := []ocicni.PortMapping{}
for _, m := range tempMappings {
finalMappings = append(finalMappings, m.mapping)
}
return finalMappings, containerPortValidate, hostPortValidate, nil
}
// Make final port mappings for the container
func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]ocicni.PortMapping, error) {
finalMappings, containerPortValidate, hostPortValidate, err := ParsePortMapping(s.PortMappings)
if err != nil {
return nil, err
}
// If not publishing exposed ports, or if we are publishing and there is
// nothing to publish - then just return the port mappings we've made so
// far.
if !s.PublishExposedPorts || (len(s.Expose) == 0 && imageData == nil) {
return finalMappings, nil
}
logrus.Debugf("Adding exposed ports")
expose := make(map[uint16]string)
if imageData != nil {
expose, err = GenExposedPorts(imageData.Config.ExposedPorts)
if err != nil {
return nil, err
}
}
// We need to merge s.Expose into image exposed ports
for k, v := range s.Expose {
expose[k] = v
}
// There's been a request to expose some ports. Let's do that.
// Start by figuring out what needs to be exposed.
// This is a map of container port number to protocols to expose.
toExpose := make(map[uint16][]string)
for port, proto := range expose {
// Validate protocol first
protocols, err := checkProtocol(proto, false)
if err != nil {
return nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port)
}
if port == 0 {
return nil, errors.Errorf("cannot expose 0 as it is not a valid port number")
}
// Check to see if the port is already present in existing
// mappings.
for _, p := range protocols {
ctrPortMap, ok := containerPortValidate[p]["0.0.0.0"]
if !ok {
ctrPortMap = make(map[uint16]uint16)
containerPortValidate[p]["0.0.0.0"] = ctrPortMap
}
if portNum := ctrPortMap[port]; portNum == 0 {
// We want to expose this port for this protocol
exposeProto, ok := toExpose[port]
if !ok {
exposeProto = []string{}
}
exposeProto = append(exposeProto, p)
toExpose[port] = exposeProto
}
}
}
// We now have a final list of ports that we want exposed.
// Let's find empty, unallocated host ports for them.
for port, protocols := range toExpose {
for _, p := range protocols {
// Find an open port on the host.
// I see a faint possibility that this will infinite
// loop trying to find a valid open port, so I've
// included a max-tries counter.
hostPort := 0
tries := 15
for hostPort == 0 && tries > 0 {
// We can't select a specific protocol, which is
// unfortunate for the UDP case.
candidate, err := utils.GetRandomPort()
if err != nil {
return nil, err
}
// Check if the host port is already bound
hostPortMap, ok := hostPortValidate[p]["0.0.0.0"]
if !ok {
hostPortMap = make(map[uint16]uint16)
hostPortValidate[p]["0.0.0.0"] = hostPortMap
}
if checkPort := hostPortMap[uint16(candidate)]; checkPort != 0 {
// Host port is already allocated, try again
tries--
continue
}
hostPortMap[uint16(candidate)] = port
hostPort = candidate
logrus.Debugf("Mapping exposed port %d/%s to host port %d", port, p, hostPort)
// Make a CNI port mapping
cniPort := ocicni.PortMapping{
HostPort: int32(candidate),
ContainerPort: int32(port),
Protocol: p,
HostIP: "",
}
finalMappings = append(finalMappings, cniPort)
}
if tries == 0 && hostPort == 0 {
// We failed to find an open port.
return nil, errors.Errorf("failed to find an open port to expose container port %d on the host", port)
}
}
}
return finalMappings, nil
}
// Check a string to ensure it is a comma-separated set of valid protocols
func checkProtocol(protocol string, allowSCTP bool) ([]string, error) {
protocols := make(map[string]struct{})
splitProto := strings.Split(protocol, ",")
// Don't error on duplicates - just deduplicate
for _, p := range splitProto {
p = strings.ToLower(p)
switch p {
case protoTCP, "":
protocols[protoTCP] = struct{}{}
case protoUDP:
protocols[protoUDP] = struct{}{}
case protoSCTP:
if !allowSCTP {
return nil, errors.Errorf("protocol SCTP is not allowed for exposed ports")
}
protocols[protoSCTP] = struct{}{}
default:
return nil, errors.Errorf("unrecognized protocol %q in port mapping", p)
}
}
finalProto := []string{}
for p := range protocols {
finalProto = append(finalProto, p)
}
// This shouldn't be possible, but check anyways
if len(finalProto) == 0 {
return nil, errors.Errorf("no valid protocols specified for port mapping")
}
return finalProto, nil
}
func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error) {
expose := make(map[uint16]string)
for imgExpose := range exposedPorts {
// Expose format is portNumber[/protocol]
splitExpose := strings.SplitN(imgExpose, "/", 2)
num, err := strconv.Atoi(splitExpose[0])
if err != nil {
return nil, errors.Wrapf(err, "unable to convert image EXPOSE statement %q to port number", imgExpose)
}
if num > 65535 || num < 1 {
return nil, errors.Errorf("%d from image EXPOSE statement %q is not a valid port number", num, imgExpose)
}
// No need to validate protocol, we'll do it below.
if len(splitExpose) == 1 {
expose[uint16(num)] = "tcp"
} else {
expose[uint16(num)] = splitExpose[1]
}
}
return expose, nil
}