mirror of
https://github.com/containers/podman.git
synced 2025-05-20 00:27:03 +08:00

The OCICNI port format has one big problem: It does not support ranges. So if a users forwards a range of 1k ports with podman run -p 1001-2000 we have to store each of the thousand ports individually as array element. This bloats the db and makes the JSON encoding and decoding much slower. In many places we already use a better port struct type which supports ranges, e.g. `pkg/specgen` or the new network interface. Because of this we have to do many runtime conversions between the two port formats. If everything uses the new format we can skip the runtime conversions. This commit adds logic to replace all occurrences of the old format with the new one. The database will automatically migrate the ports to new format when the container config is read for the first time after the update. The `ParsePortMapping` function is `pkg/specgen/generate` has been reworked to better work with the new format. The new logic is able to deduplicate the given ports. This is necessary the ensure we store them efficiently in the DB. The new code should also be more performant than the old one. To prove that the code is fast enough I added go benchmarks. Parsing 1 million ports took less than 0.5 seconds on my laptop. Benchmark normalize PortMappings in specgen: Please note that the 1 million ports are actually 20x 50k ranges because we cannot have bigger ranges than 65535 ports. ``` $ go test -bench=. -benchmem ./pkg/specgen/generate/ goos: linux goarch: amd64 pkg: github.com/containers/podman/v3/pkg/specgen/generate cpu: Intel(R) Core(TM) i7-10850H CPU @ 2.70GHz BenchmarkParsePortMappingNoPorts-12 480821532 2.230 ns/op 0 B/op 0 allocs/op BenchmarkParsePortMapping1-12 38972 30183 ns/op 131584 B/op 9 allocs/op BenchmarkParsePortMapping100-12 18752 60688 ns/op 141088 B/op 315 allocs/op BenchmarkParsePortMapping1k-12 3104 331719 ns/op 223840 B/op 3018 allocs/op BenchmarkParsePortMapping10k-12 376 3122930 ns/op 1223650 B/op 30027 allocs/op BenchmarkParsePortMapping1m-12 3 390869926 ns/op 124593840 B/op 4000624 allocs/op BenchmarkParsePortMappingReverse100-12 18940 63414 ns/op 141088 B/op 315 allocs/op BenchmarkParsePortMappingReverse1k-12 3015 362500 ns/op 223841 B/op 3018 allocs/op BenchmarkParsePortMappingReverse10k-12 343 3318135 ns/op 1223650 B/op 30027 allocs/op BenchmarkParsePortMappingReverse1m-12 3 403392469 ns/op 124593840 B/op 4000624 allocs/op BenchmarkParsePortMappingRange1-12 37635 28756 ns/op 131584 B/op 9 allocs/op BenchmarkParsePortMappingRange100-12 39604 28935 ns/op 131584 B/op 9 allocs/op BenchmarkParsePortMappingRange1k-12 38384 29921 ns/op 131584 B/op 9 allocs/op BenchmarkParsePortMappingRange10k-12 29479 40381 ns/op 131584 B/op 9 allocs/op BenchmarkParsePortMappingRange1m-12 927 1279369 ns/op 143022 B/op 164 allocs/op PASS ok github.com/containers/podman/v3/pkg/specgen/generate 25.492s ``` Benchmark convert old port format to new one: ``` go test -bench=. -benchmem ./libpod/ goos: linux goarch: amd64 pkg: github.com/containers/podman/v3/libpod cpu: Intel(R) Core(TM) i7-10850H CPU @ 2.70GHz Benchmark_ocicniPortsToNetTypesPortsNoPorts-12 663526126 1.663 ns/op 0 B/op 0 allocs/op Benchmark_ocicniPortsToNetTypesPorts1-12 7858082 141.9 ns/op 72 B/op 2 allocs/op Benchmark_ocicniPortsToNetTypesPorts10-12 2065347 571.0 ns/op 536 B/op 4 allocs/op Benchmark_ocicniPortsToNetTypesPorts100-12 138478 8641 ns/op 4216 B/op 4 allocs/op Benchmark_ocicniPortsToNetTypesPorts1k-12 9414 120964 ns/op 41080 B/op 4 allocs/op Benchmark_ocicniPortsToNetTypesPorts10k-12 781 1490526 ns/op 401528 B/op 4 allocs/op Benchmark_ocicniPortsToNetTypesPorts1m-12 4 250579010 ns/op 40001656 B/op 4 allocs/op PASS ok github.com/containers/podman/v3/libpod 11.727s ``` Signed-off-by: Paul Holzinger <pholzing@redhat.com>
441 lines
14 KiB
Go
441 lines
14 KiB
Go
package generate
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/containers/common/libimage"
|
|
"github.com/containers/podman/v3/libpod/network/types"
|
|
"github.com/containers/podman/v3/utils"
|
|
|
|
"github.com/containers/podman/v3/pkg/specgen"
|
|
"github.com/containers/podman/v3/pkg/util"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
protoTCP = "tcp"
|
|
protoUDP = "udp"
|
|
protoSCTP = "sctp"
|
|
)
|
|
|
|
// joinTwoPortsToRangePortIfPossible will expect two ports the previous port one must have a lower or equal hostPort than the current port.
|
|
func joinTwoPortsToRangePortIfPossible(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool,
|
|
previousPort *types.PortMapping, port types.PortMapping) (*types.PortMapping, error) {
|
|
// no previous port just return the current one
|
|
if previousPort == nil {
|
|
return &port, nil
|
|
}
|
|
if previousPort.HostPort+previousPort.Range >= port.HostPort {
|
|
// check if the port range matches the host and container ports
|
|
portDiff := port.HostPort - previousPort.HostPort
|
|
if portDiff == port.ContainerPort-previousPort.ContainerPort {
|
|
// calc the new range use the old range and add the difference between the ports
|
|
newRange := port.Range + portDiff
|
|
// if the newRange is greater than the old range use it
|
|
// this is important otherwise we would could lower the range
|
|
if newRange > previousPort.Range {
|
|
previousPort.Range = newRange
|
|
}
|
|
return previousPort, nil
|
|
}
|
|
// if both host port ranges overlap and the container port range did not match
|
|
// we have to error because we cannot assign the same host port to more than one container port
|
|
if previousPort.HostPort+previousPort.Range-1 > port.HostPort {
|
|
return nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", port.HostPort, port.Protocol)
|
|
}
|
|
}
|
|
// we could not join the ports so we append the old one to the list
|
|
// and return the current port as previous port
|
|
addPortToUsedPorts(ports, allHostPorts, allContainerPorts, currentHostPorts, previousPort)
|
|
return &port, nil
|
|
}
|
|
|
|
// joinTwoContainerPortsToRangePortIfPossible will expect two ports with both no host port set,
|
|
// the previous port one must have a lower or equal containerPort than the current port.
|
|
func joinTwoContainerPortsToRangePortIfPossible(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool,
|
|
previousPort *types.PortMapping, port types.PortMapping) (*types.PortMapping, error) {
|
|
// no previous port just return the current one
|
|
if previousPort == nil {
|
|
return &port, nil
|
|
}
|
|
if previousPort.ContainerPort+previousPort.Range > port.ContainerPort {
|
|
// calc the new range use the old range and add the difference between the ports
|
|
newRange := port.ContainerPort - previousPort.ContainerPort + port.Range
|
|
// if the newRange is greater than the old range use it
|
|
// this is important otherwise we would could lower the range
|
|
if newRange > previousPort.Range {
|
|
previousPort.Range = newRange
|
|
}
|
|
return previousPort, nil
|
|
}
|
|
// we could not join the ports so we append the old one to the list
|
|
// and return the current port as previous port
|
|
newPort, err := getRandomHostPort(currentHostPorts, *previousPort)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addPortToUsedPorts(ports, allHostPorts, allContainerPorts, currentHostPorts, &newPort)
|
|
return &port, nil
|
|
}
|
|
|
|
func addPortToUsedPorts(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool, port *types.PortMapping) {
|
|
for i := uint16(0); i < port.Range; i++ {
|
|
h := port.HostPort + i
|
|
allHostPorts[h] = true
|
|
currentHostPorts[h] = true
|
|
c := port.ContainerPort + i
|
|
allContainerPorts[c] = true
|
|
}
|
|
*ports = append(*ports, *port)
|
|
}
|
|
|
|
// getRandomHostPort get a random host port mapping for the given port
|
|
// the caller has to supply a array with he already used ports
|
|
func getRandomHostPort(hostPorts *[65536]bool, port types.PortMapping) (types.PortMapping, error) {
|
|
outer:
|
|
for i := 0; i < 15; i++ {
|
|
ranPort, err := utils.GetRandomPort()
|
|
if err != nil {
|
|
return port, err
|
|
}
|
|
|
|
// if port range is exceeds max port we cannot use it
|
|
if ranPort+int(port.Range) > 65535 {
|
|
continue
|
|
}
|
|
|
|
// check if there is a port in the range which is used
|
|
for j := 0; j < int(port.Range); j++ {
|
|
// port already used
|
|
if hostPorts[ranPort+j] {
|
|
continue outer
|
|
}
|
|
}
|
|
|
|
port.HostPort = uint16(ranPort)
|
|
return port, nil
|
|
}
|
|
|
|
// add range to error message if needed
|
|
rangePort := ""
|
|
if port.Range > 1 {
|
|
rangePort = fmt.Sprintf("with range %d ", port.Range)
|
|
}
|
|
|
|
return port, errors.Errorf("failed to find an open port to expose container port %d %son the host", port.ContainerPort, rangePort)
|
|
}
|
|
|
|
// Parse port maps to port mappings.
|
|
// Returns a set of port mappings, and maps of utilized container and
|
|
// host ports.
|
|
func ParsePortMapping(portMappings []types.PortMapping, exposePorts map[uint16][]string) ([]types.PortMapping, error) {
|
|
if len(portMappings) == 0 && len(exposePorts) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// tempMapping stores the ports without ip and protocol
|
|
type tempMapping struct {
|
|
hostPort uint16
|
|
containerPort uint16
|
|
rangePort uint16
|
|
}
|
|
|
|
// portMap is a temporary structure to sort all ports
|
|
// the map is hostIp -> protocol -> array of mappings
|
|
portMap := make(map[string]map[string][]tempMapping)
|
|
|
|
// allUsedContainerPorts stores all used ports for each protocol
|
|
// the key is the protocol and the array is 65536 elements long for each port.
|
|
allUsedContainerPortsMap := make(map[string][65536]bool)
|
|
allUsedHostPortsMap := make(map[string][65536]bool)
|
|
|
|
// First, we need to validate the ports passed in the specgen
|
|
for _, port := range portMappings {
|
|
// First, check proto
|
|
protocols, err := checkProtocol(port.Protocol, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if port.HostIP != "" {
|
|
if ip := net.ParseIP(port.HostIP); ip == nil {
|
|
return nil, errors.Errorf("invalid IP address %q in port mapping", port.HostIP)
|
|
}
|
|
}
|
|
|
|
// Validate port numbers and range.
|
|
portRange := port.Range
|
|
if portRange == 0 {
|
|
portRange = 1
|
|
}
|
|
containerPort := port.ContainerPort
|
|
if containerPort == 0 {
|
|
return nil, errors.Errorf("container port number must be non-0")
|
|
}
|
|
hostPort := port.HostPort
|
|
if uint32(portRange-1)+uint32(containerPort) > 65535 {
|
|
return nil, errors.Errorf("container port range exceeds maximum allowable port number")
|
|
}
|
|
if uint32(portRange-1)+uint32(hostPort) > 65535 {
|
|
return nil, errors.Errorf("host port range exceeds maximum allowable port number")
|
|
}
|
|
|
|
hostProtoMap, ok := portMap[port.HostIP]
|
|
if !ok {
|
|
hostProtoMap = make(map[string][]tempMapping)
|
|
for _, proto := range []string{protoTCP, protoUDP, protoSCTP} {
|
|
hostProtoMap[proto] = make([]tempMapping, 0)
|
|
}
|
|
portMap[port.HostIP] = hostProtoMap
|
|
}
|
|
|
|
p := tempMapping{
|
|
hostPort: port.HostPort,
|
|
containerPort: port.ContainerPort,
|
|
rangePort: portRange,
|
|
}
|
|
|
|
for _, proto := range protocols {
|
|
hostProtoMap[proto] = append(hostProtoMap[proto], p)
|
|
}
|
|
}
|
|
|
|
// we do no longer need the original port mappings
|
|
// set it to 0 length so we can resuse it to populate
|
|
// the slice again while keeping the underlying capacity
|
|
portMappings = portMappings[:0]
|
|
|
|
for hostIP, protoMap := range portMap {
|
|
for protocol, ports := range protoMap {
|
|
ports := ports
|
|
if len(ports) == 0 {
|
|
continue
|
|
}
|
|
// 1. sort the ports by host port
|
|
// use a small hack to make sure ports with host port 0 are sorted last
|
|
sort.Slice(ports, func(i, j int) bool {
|
|
if ports[i].hostPort == ports[j].hostPort {
|
|
return ports[i].containerPort < ports[j].containerPort
|
|
}
|
|
if ports[i].hostPort == 0 {
|
|
return false
|
|
}
|
|
if ports[j].hostPort == 0 {
|
|
return true
|
|
}
|
|
return ports[i].hostPort < ports[j].hostPort
|
|
})
|
|
|
|
allUsedContainerPorts := allUsedContainerPortsMap[protocol]
|
|
allUsedHostPorts := allUsedHostPortsMap[protocol]
|
|
var usedHostPorts [65536]bool
|
|
|
|
var previousPort *types.PortMapping
|
|
var i int
|
|
for i = 0; i < len(ports); i++ {
|
|
if ports[i].hostPort == 0 {
|
|
// because the ports are sorted and host port 0 is last
|
|
// we can break when we hit 0
|
|
// we will fit them in afterwards
|
|
break
|
|
}
|
|
p := types.PortMapping{
|
|
HostIP: hostIP,
|
|
Protocol: protocol,
|
|
HostPort: ports[i].hostPort,
|
|
ContainerPort: ports[i].containerPort,
|
|
Range: ports[i].rangePort,
|
|
}
|
|
var err error
|
|
previousPort, err = joinTwoPortsToRangePortIfPossible(&portMappings, &allUsedHostPorts,
|
|
&allUsedContainerPorts, &usedHostPorts, previousPort, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if previousPort != nil {
|
|
addPortToUsedPorts(&portMappings, &allUsedHostPorts,
|
|
&allUsedContainerPorts, &usedHostPorts, previousPort)
|
|
}
|
|
|
|
// now take care of the hostPort = 0 ports
|
|
previousPort = nil
|
|
for i < len(ports) {
|
|
p := types.PortMapping{
|
|
HostIP: hostIP,
|
|
Protocol: protocol,
|
|
ContainerPort: ports[i].containerPort,
|
|
Range: ports[i].rangePort,
|
|
}
|
|
var err error
|
|
previousPort, err = joinTwoContainerPortsToRangePortIfPossible(&portMappings, &allUsedHostPorts,
|
|
&allUsedContainerPorts, &usedHostPorts, previousPort, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i++
|
|
}
|
|
if previousPort != nil {
|
|
newPort, err := getRandomHostPort(&usedHostPorts, *previousPort)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addPortToUsedPorts(&portMappings, &allUsedHostPorts,
|
|
&allUsedContainerPorts, &usedHostPorts, &newPort)
|
|
}
|
|
|
|
allUsedContainerPortsMap[protocol] = allUsedContainerPorts
|
|
allUsedHostPortsMap[protocol] = allUsedHostPorts
|
|
}
|
|
}
|
|
|
|
if len(exposePorts) > 0 {
|
|
logrus.Debugf("Adding exposed ports")
|
|
|
|
for port, protocols := range exposePorts {
|
|
newProtocols := make([]string, 0, len(protocols))
|
|
for _, protocol := range protocols {
|
|
if !allUsedContainerPortsMap[protocol][port] {
|
|
p := types.PortMapping{
|
|
ContainerPort: port,
|
|
Protocol: protocol,
|
|
Range: 1,
|
|
}
|
|
allPorts := allUsedContainerPortsMap[protocol]
|
|
p, err := getRandomHostPort(&allPorts, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
portMappings = append(portMappings, p)
|
|
} else {
|
|
newProtocols = append(newProtocols, protocol)
|
|
}
|
|
}
|
|
// make sure to delete the key from the map if there are no protocols left
|
|
if len(newProtocols) == 0 {
|
|
delete(exposePorts, port)
|
|
} else {
|
|
exposePorts[port] = newProtocols
|
|
}
|
|
}
|
|
}
|
|
return portMappings, nil
|
|
}
|
|
|
|
func appendProtocolsNoDuplicates(slice []string, protocols []string) []string {
|
|
for _, proto := range protocols {
|
|
if util.StringInSlice(proto, slice) {
|
|
continue
|
|
}
|
|
slice = append(slice, proto)
|
|
}
|
|
return slice
|
|
}
|
|
|
|
// Make final port mappings for the container
|
|
func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.PortMapping, map[uint16][]string, error) {
|
|
expose := make(map[uint16]string)
|
|
var err error
|
|
if imageData != nil {
|
|
expose, err = GenExposedPorts(imageData.Config.ExposedPorts)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
toExpose := make(map[uint16][]string, len(s.Expose)+len(expose))
|
|
for _, expose := range []map[uint16]string{expose, s.Expose} {
|
|
for port, proto := range expose {
|
|
if port == 0 {
|
|
return nil, nil, errors.Errorf("cannot expose 0 as it is not a valid port number")
|
|
}
|
|
protocols, err := checkProtocol(proto, false)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port)
|
|
}
|
|
toExpose[port] = appendProtocolsNoDuplicates(toExpose[port], protocols)
|
|
}
|
|
}
|
|
|
|
publishPorts := toExpose
|
|
if !s.PublishExposedPorts {
|
|
publishPorts = nil
|
|
}
|
|
|
|
finalMappings, err := ParsePortMapping(s.PortMappings, publishPorts)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return finalMappings, toExpose, 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, len(exposedPorts))
|
|
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 later.
|
|
newProto := "tcp"
|
|
if len(splitExpose) == 2 {
|
|
newProto = splitExpose[1]
|
|
}
|
|
|
|
proto := expose[uint16(num)]
|
|
if len(proto) > 1 {
|
|
proto = proto + "," + newProto
|
|
} else {
|
|
proto = newProto
|
|
}
|
|
expose[uint16(num)] = proto
|
|
}
|
|
return expose, nil
|
|
}
|