spiegel_podman/cmd/podman/common/util.go
Matthew Heon 7ac3d906b5 Rework port parsing to support --expose and -P
As part of this, make a major change to the type we use to
represent port mappings in SpecGen (from using existing OCICNI
structs to using our own custom one). This struct has the
advantage of supporting ranges, massively reducing traffic over
the wire for Podman commands using them (for example, the
`podman run -p 5000-6000` command will now send only one struct
instead of 1000). This struct also allows us to easily validate
which ports are in use, and which are not, which is necessary for
--expose.

Once we have parsed the ports from the new struct, we can produce
an accurate map including all currently requested ports, and use
that to determine what ports need to be exposed (some requested
exposed ports may already be included in a mapping from --publish
and will be ignored) and what open ports on the host we can map
them to.

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
2020-05-04 20:57:27 -04:00

201 lines
5.7 KiB
Go

package common
import (
"net"
"strconv"
"strings"
"github.com/containers/libpod/pkg/specgen"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// createExpose parses user-provided exposed port definitions and converts them
// into SpecGen format.
// TODO: The SpecGen format should really handle ranges more sanely - we could
// be massively inflating what is sent over the wire with a large range.
func createExpose(expose []string) (map[uint16]string, error) {
toReturn := make(map[uint16]string)
for _, e := range expose {
// Check for protocol
proto := "tcp"
splitProto := strings.Split(e, "/")
if len(splitProto) > 2 {
return nil, errors.Errorf("invalid expose format - protocol can only be specified once")
} else if len(splitProto) == 2 {
proto = splitProto[1]
}
// Check for a range
start, len, err := parseAndValidateRange(splitProto[0])
if err != nil {
return nil, err
}
var index uint16
for index = 0; index < len; index++ {
portNum := start + index
protocols, ok := toReturn[portNum]
if !ok {
toReturn[portNum] = proto
} else {
newProto := strings.Join(append(strings.Split(protocols, ","), strings.Split(proto, ",")...), ",")
toReturn[portNum] = newProto
}
}
}
return toReturn, nil
}
// createPortBindings iterates ports mappings into SpecGen format.
func createPortBindings(ports []string) ([]specgen.PortMapping, error) {
// --publish is formatted as follows:
// [[hostip:]hostport[-endPort]:]containerport[-endPort][/protocol]
toReturn := make([]specgen.PortMapping, 0, len(ports))
for _, p := range ports {
var (
ctrPort string
proto, hostIP, hostPort *string
)
splitProto := strings.Split(p, "/")
switch len(splitProto) {
case 1:
// No protocol was provided
case 2:
proto = &(splitProto[1])
default:
return nil, errors.Errorf("invalid port format - protocol can only be specified once")
}
splitPort := strings.Split(splitProto[0], ":")
switch len(splitPort) {
case 1:
ctrPort = splitPort[0]
case 2:
hostPort = &(splitPort[0])
ctrPort = splitPort[1]
case 3:
hostIP = &(splitPort[0])
hostPort = &(splitPort[1])
ctrPort = splitPort[2]
default:
return nil, errors.Errorf("invalid port format - format is [[hostIP:]hostPort:]containerPort")
}
newPort, err := parseSplitPort(hostIP, hostPort, ctrPort, proto)
if err != nil {
return nil, err
}
toReturn = append(toReturn, newPort)
}
return toReturn, nil
}
// parseSplitPort parses individual components of the --publish flag to produce
// a single port mapping in SpecGen format.
func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (specgen.PortMapping, error) {
newPort := specgen.PortMapping{}
if ctrPort == "" {
return newPort, errors.Errorf("must provide a non-empty container port to publish")
}
ctrStart, ctrLen, err := parseAndValidateRange(ctrPort)
if err != nil {
return newPort, errors.Wrapf(err, "error parsing container port")
}
newPort.ContainerPort = ctrStart
newPort.Range = ctrLen
if protocol != nil {
if *protocol == "" {
return newPort, errors.Errorf("must provide a non-empty protocol to publish")
}
newPort.Protocol = *protocol
}
if hostIP != nil {
if *hostIP == "" {
return newPort, errors.Errorf("must provide a non-empty container host IP to publish")
}
testIP := net.ParseIP(*hostIP)
if testIP == nil {
return newPort, errors.Errorf("cannot parse %q as an IP address", *hostIP)
}
newPort.HostIP = testIP.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)
}
newPort.HostPort = hostStart
}
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
}
// Parse and validate a port range.
// Returns start port, length of range, error.
func parseAndValidateRange(portRange string) (uint16, uint16, error) {
splitRange := strings.Split(portRange, "-")
if len(splitRange) > 2 {
return 0, 0, errors.Errorf("invalid port format - port ranges are formatted as startPort-stopPort")
}
if splitRange[0] == "" {
return 0, 0, errors.Errorf("port numbers cannot be negative")
}
startPort, err := parseAndValidatePort(splitRange[0])
if err != nil {
return 0, 0, err
}
var rangeLen uint16 = 1
if len(splitRange) == 2 {
if splitRange[1] == "" {
return 0, 0, errors.Errorf("must provide ending number for port range")
}
endPort, err := parseAndValidatePort(splitRange[1])
if err != nil {
return 0, 0, err
}
if endPort <= startPort {
return 0, 0, errors.Errorf("the end port of a range must be higher than the start port - %d is not higher than %d", endPort, startPort)
}
// Our range is the total number of ports
// involved, so we need to add 1 (8080:8081 is
// 2 ports, for example, not 1)
rangeLen = endPort - startPort + 1
}
return startPort, rangeLen, nil
}
// Turn a single string into a valid U16 port.
func parseAndValidatePort(port string) (uint16, error) {
num, err := strconv.Atoi(port)
if err != nil {
return 0, errors.Wrapf(err, "cannot parse %q as a port number", port)
}
if num < 1 || num > 65535 {
return 0, errors.Errorf("port numbers must be between 1 and 65535 (inclusive), got %d", num)
}
return uint16(num), nil
}