summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/containers/port.go50
-rw-r--r--cmd/podman/containers/ps.go183
-rw-r--r--cmd/podman/system/unshare.go10
-rw-r--r--cmd/rootlessport/main.go42
-rw-r--r--docs/source/markdown/podman-unshare.1.md8
-rw-r--r--libpod/boltdb_state_internal.go9
-rw-r--r--libpod/common_test.go4
-rw-r--r--libpod/container.go2
-rw-r--r--libpod/container_config.go12
-rw-r--r--libpod/container_internal.go13
-rw-r--r--libpod/kube.go51
-rw-r--r--libpod/network/types/network.go1
-rw-r--r--libpod/networking_linux.go330
-rw-r--r--libpod/networking_linux_test.go323
-rw-r--r--libpod/networking_slirp4netns.go4
-rw-r--r--libpod/oci_util.go169
-rw-r--r--libpod/options.go17
-rw-r--r--libpod/state_test.go40
-rw-r--r--libpod/util.go22
-rw-r--r--pkg/api/handlers/types.go8
-rw-r--r--pkg/api/server/handler_rid.go9
-rw-r--r--pkg/checkpoint/checkpoint_restore.go2
-rw-r--r--pkg/domain/entities/container_ps.go2
-rw-r--r--pkg/domain/entities/containers.go2
-rw-r--r--pkg/domain/entities/system.go2
-rw-r--r--pkg/domain/infra/abi/system.go10
-rw-r--r--pkg/rootlessport/rootlessport_linux.go2
-rw-r--r--pkg/specgen/generate/pod_create.go4
-rw-r--r--pkg/specgen/generate/ports.go589
-rw-r--r--pkg/specgen/generate/ports_bench_test.go197
-rw-r--r--pkg/specgen/generate/ports_test.go989
-rw-r--r--pkg/specgen/podspecgen.go1
-rw-r--r--pkg/specgen/specgen.go1
-rw-r--r--test/e2e/run_networking_test.go17
-rw-r--r--test/e2e/unshare_test.go2
-rw-r--r--test/upgrade/test-upgrade.bats3
36 files changed, 2348 insertions, 782 deletions
diff --git a/cmd/podman/containers/port.go b/cmd/podman/containers/port.go
index f309390c3..0e582ae52 100644
--- a/cmd/podman/containers/port.go
+++ b/cmd/podman/containers/port.go
@@ -8,7 +8,6 @@ import (
"github.com/containers/podman/v3/cmd/podman/common"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/cmd/podman/validate"
- "github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@@ -73,7 +72,8 @@ func port(_ *cobra.Command, args []string) error {
var (
container string
err error
- userPort types.OCICNIPortMapping
+ userPort uint16
+ userProto string
)
if len(args) == 0 && !portOpts.Latest && !portOpts.All {
@@ -101,16 +101,12 @@ func port(_ *cobra.Command, args []string) error {
fields = append(fields, "tcp")
}
- portNum, err := strconv.Atoi(fields[0])
+ portNum, err := strconv.ParseUint(fields[0], 10, 16)
if err != nil {
return err
}
- userPort = types.OCICNIPortMapping{
- HostPort: 0,
- ContainerPort: int32(portNum),
- Protocol: fields[1],
- HostIP: "",
- }
+ userPort = uint16(portNum)
+ userProto = fields[1]
}
reports, err := registry.ContainerEngine().ContainerPort(registry.GetContext(), container, portOpts)
@@ -120,24 +116,36 @@ func port(_ *cobra.Command, args []string) error {
var found bool
// Iterate mappings
for _, report := range reports {
+ allPrefix := ""
+ if portOpts.All {
+ allPrefix = report.Id[:12] + "\t"
+ }
for _, v := range report.Ports {
hostIP := v.HostIP
// Set host IP to 0.0.0.0 if blank
if hostIP == "" {
hostIP = "0.0.0.0"
}
- if portOpts.All {
- fmt.Printf("%s\t", report.Id[:12])
- }
- // If not searching by port or port/proto, then dump what we see
- if port == "" {
- fmt.Printf("%d/%s -> %s:%d\n", v.ContainerPort, v.Protocol, hostIP, v.HostPort)
- continue
- }
- if v.ContainerPort == userPort.ContainerPort {
- fmt.Printf("%s:%d\n", hostIP, v.HostPort)
- found = true
- break
+ protocols := strings.Split(v.Protocol, ",")
+ for _, protocol := range protocols {
+ // If not searching by port or port/proto, then dump what we see
+ if port == "" {
+ for i := uint16(0); i < v.Range; i++ {
+ fmt.Printf("%s%d/%s -> %s:%d\n", allPrefix, v.ContainerPort+i, protocol, hostIP, v.HostPort+i)
+ }
+ continue
+ }
+ // check if the proto matches and if the port is in the range
+ // this is faster than looping over the range for no reason
+ if v.Protocol == userProto &&
+ v.ContainerPort <= userPort &&
+ v.ContainerPort+v.Range > userPort {
+ // we have to add the current range to the host port
+ hostPort := v.HostPort + userPort - v.ContainerPort
+ fmt.Printf("%s%s:%d\n", allPrefix, hostIP, hostPort)
+ found = true
+ break
+ }
}
}
if !found && port != "" {
diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go
index 9687cd5bd..712de327c 100644
--- a/cmd/podman/containers/ps.go
+++ b/cmd/podman/containers/ps.go
@@ -3,8 +3,6 @@ package containers
import (
"fmt"
"os"
- "sort"
- "strconv"
"strings"
"time"
@@ -477,174 +475,31 @@ func (l psReporter) UTS() string {
// portsToString converts the ports used to a string of the from "port1, port2"
// and also groups a continuous list of ports into a readable format.
-func portsToString(ports []types.OCICNIPortMapping) string {
+// The format is IP:HostPort(-Range)->ContainerPort(-Range)/Proto
+func portsToString(ports []types.PortMapping) string {
if len(ports) == 0 {
return ""
}
- // Sort the ports, so grouping continuous ports become easy.
- sort.Slice(ports, func(i, j int) bool {
- return comparePorts(ports[i], ports[j])
- })
-
- portGroups := [][]types.OCICNIPortMapping{}
- currentGroup := []types.OCICNIPortMapping{}
- for i, v := range ports {
- var prevPort, nextPort *int32
- if i > 0 {
- prevPort = &ports[i-1].ContainerPort
- }
- if i+1 < len(ports) {
- nextPort = &ports[i+1].ContainerPort
- }
-
- port := v.ContainerPort
-
- // Helper functions
- addToCurrentGroup := func(x types.OCICNIPortMapping) {
- currentGroup = append(currentGroup, x)
- }
-
- addToPortGroup := func(x types.OCICNIPortMapping) {
- portGroups = append(portGroups, []types.OCICNIPortMapping{x})
- }
-
- finishCurrentGroup := func() {
- portGroups = append(portGroups, currentGroup)
- currentGroup = []types.OCICNIPortMapping{}
- }
-
- // Single entry slice
- if prevPort == nil && nextPort == nil {
- addToPortGroup(v)
- }
-
- // Start of the slice with len > 0
- if prevPort == nil && nextPort != nil {
- isGroup := *nextPort-1 == port
-
- if isGroup {
- // Start with a group
- addToCurrentGroup(v)
- } else {
- // Start with single item
- addToPortGroup(v)
- }
-
- continue
- }
-
- // Middle of the slice with len > 0
- if prevPort != nil && nextPort != nil {
- currentIsGroup := *prevPort+1 == port
- nextIsGroup := *nextPort-1 == port
-
- if currentIsGroup {
- // Maybe in the middle of a group
- addToCurrentGroup(v)
-
- if !nextIsGroup {
- // End of a group
- finishCurrentGroup()
- }
- } else if nextIsGroup {
- // Start of a new group
- addToCurrentGroup(v)
- } else {
- // No group at all
- addToPortGroup(v)
- }
-
- continue
- }
-
- // End of the slice with len > 0
- if prevPort != nil && nextPort == nil {
- isGroup := *prevPort+1 == port
-
- if isGroup {
- // End group
- addToCurrentGroup(v)
- finishCurrentGroup()
- } else {
- // End single item
- addToPortGroup(v)
- }
- }
- }
-
- portDisplay := []string{}
- for _, group := range portGroups {
- if len(group) == 0 {
- // Usually should not happen, but better do not crash.
- continue
- }
-
- first := group[0]
-
- hostIP := first.HostIP
+ sb := &strings.Builder{}
+ for _, port := range ports {
+ hostIP := port.HostIP
if hostIP == "" {
hostIP = "0.0.0.0"
}
-
- // Single mappings
- if len(group) == 1 {
- portDisplay = append(portDisplay,
- fmt.Sprintf(
- "%s:%d->%d/%s",
- hostIP, first.HostPort, first.ContainerPort, first.Protocol,
- ),
- )
- continue
- }
-
- // Group mappings
- last := group[len(group)-1]
- portDisplay = append(portDisplay, formatGroup(
- fmt.Sprintf("%s/%s", hostIP, first.Protocol),
- first.HostPort, last.HostPort,
- first.ContainerPort, last.ContainerPort,
- ))
- }
- return strings.Join(portDisplay, ", ")
-}
-
-func comparePorts(i, j types.OCICNIPortMapping) bool {
- if i.ContainerPort != j.ContainerPort {
- return i.ContainerPort < j.ContainerPort
- }
-
- if i.HostIP != j.HostIP {
- return i.HostIP < j.HostIP
- }
-
- if i.HostPort != j.HostPort {
- return i.HostPort < j.HostPort
- }
-
- return i.Protocol < j.Protocol
-}
-
-// formatGroup returns the group in the format:
-// <IP:firstHost:lastHost->firstCtr:lastCtr/Proto>
-// e.g 0.0.0.0:1000-1006->2000-2006/tcp.
-func formatGroup(key string, firstHost, lastHost, firstCtr, lastCtr int32) string {
- parts := strings.Split(key, "/")
- groupType := parts[0]
- var ip string
- if len(parts) > 1 {
- ip = parts[0]
- groupType = parts[1]
- }
-
- group := func(first, last int32) string {
- group := strconv.Itoa(int(first))
- if first != last {
- group = fmt.Sprintf("%s-%d", group, last)
+ protocols := strings.Split(port.Protocol, ",")
+ for _, protocol := range protocols {
+ if port.Range > 1 {
+ fmt.Fprintf(sb, "%s:%d-%d->%d-%d/%s, ",
+ hostIP, port.HostPort, port.HostPort+port.Range-1,
+ port.ContainerPort, port.ContainerPort+port.Range-1, protocol)
+ } else {
+ fmt.Fprintf(sb, "%s:%d->%d/%s, ",
+ hostIP, port.HostPort,
+ port.ContainerPort, protocol)
+ }
}
- return group
}
- hostGroup := group(firstHost, lastHost)
- ctrGroup := group(firstCtr, lastCtr)
-
- return fmt.Sprintf("%s:%s->%s/%s", ip, hostGroup, ctrGroup, groupType)
+ display := sb.String()
+ // make sure to trim the last ", " of the string
+ return display[:len(display)-2]
}
diff --git a/cmd/podman/system/unshare.go b/cmd/podman/system/unshare.go
index 50230609e..9b777dd8f 100644
--- a/cmd/podman/system/unshare.go
+++ b/cmd/podman/system/unshare.go
@@ -10,6 +10,7 @@ import (
"github.com/containers/podman/v3/pkg/rootless"
"github.com/pkg/errors"
"github.com/spf13/cobra"
+ "github.com/spf13/pflag"
)
var (
@@ -34,7 +35,14 @@ func init() {
})
flags := unshareCommand.Flags()
flags.SetInterspersed(false)
- flags.BoolVar(&unshareOptions.RootlessCNI, "rootless-cni", false, "Join the rootless network namespace used for CNI networking")
+ flags.BoolVar(&unshareOptions.RootlessNetNS, "rootless-netns", false, "Join the rootless network namespace used for CNI and netavark networking")
+ // backwards compat still allow --rootless-cni
+ flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
+ if name == "rootless-cni" {
+ name = "rootless-netns"
+ }
+ return pflag.NormalizedName(name)
+ })
}
func unshare(cmd *cobra.Command, args []string) error {
diff --git a/cmd/rootlessport/main.go b/cmd/rootlessport/main.go
index feb9f5c06..e691ce2fc 100644
--- a/cmd/rootlessport/main.go
+++ b/cmd/rootlessport/main.go
@@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strings"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containers/podman/v3/libpod/network/types"
@@ -289,25 +290,30 @@ func handler(ctx context.Context, conn io.Reader, pm rkport.Manager) error {
return nil
}
-func exposePorts(pm rkport.Manager, portMappings []types.OCICNIPortMapping, childIP string) error {
+func exposePorts(pm rkport.Manager, portMappings []types.PortMapping, childIP string) error {
ctx := context.TODO()
- for _, i := range portMappings {
- hostIP := i.HostIP
- if hostIP == "" {
- hostIP = "0.0.0.0"
- }
- spec := rkport.Spec{
- Proto: i.Protocol,
- ParentIP: hostIP,
- ParentPort: int(i.HostPort),
- ChildPort: int(i.ContainerPort),
- ChildIP: childIP,
- }
- if err := rkportutil.ValidatePortSpec(spec, nil); err != nil {
- return err
- }
- if _, err := pm.AddPort(ctx, spec); err != nil {
- return err
+ for _, port := range portMappings {
+ protocols := strings.Split(port.Protocol, ",")
+ for _, protocol := range protocols {
+ hostIP := port.HostIP
+ if hostIP == "" {
+ hostIP = "0.0.0.0"
+ }
+ for i := uint16(0); i < port.Range; i++ {
+ spec := rkport.Spec{
+ Proto: protocol,
+ ParentIP: hostIP,
+ ParentPort: int(port.HostPort + i),
+ ChildPort: int(port.ContainerPort + i),
+ ChildIP: childIP,
+ }
+ if err := rkportutil.ValidatePortSpec(spec, nil); err != nil {
+ return err
+ }
+ if _, err := pm.AddPort(ctx, spec); err != nil {
+ return err
+ }
+ }
}
}
return nil
diff --git a/docs/source/markdown/podman-unshare.1.md b/docs/source/markdown/podman-unshare.1.md
index 72821b6e5..fa5259ae1 100644
--- a/docs/source/markdown/podman-unshare.1.md
+++ b/docs/source/markdown/podman-unshare.1.md
@@ -30,10 +30,10 @@ The unshare session defines two environment variables:
Print usage statement
-#### **--rootless-cni**
+#### **--rootless-netns**
-Join the rootless network namespace used for CNI networking. It can be used to
-connect to a rootless container via IP address (CNI networking). This is otherwise
+Join the rootless network namespace used for CNI and netavark networking. It can be used to
+connect to a rootless container via IP address (bridge networking). This is otherwise
not possible from the host network namespace.
_Note: Using this option with more than one unshare session can have unexpected results._
@@ -78,7 +78,7 @@ $ podman unshare cat /proc/self/uid_map /proc/self/gid_map
0 1000 1
1 10000 65536
-$ podman unshare --rootless-cni ip addr
+$ podman unshare --rootless-netns ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go
index 3e3c17a9e..e71d82736 100644
--- a/libpod/boltdb_state_internal.go
+++ b/libpod/boltdb_state_internal.go
@@ -384,6 +384,15 @@ func (s *BoltState) getContainerConfigFromDB(id []byte, config *ContainerConfig,
return errors.Wrapf(err, "error unmarshalling container %s config", string(id))
}
+ // convert ports to the new format if needed
+ if len(config.ContainerNetworkConfig.OldPortMappings) > 0 && len(config.ContainerNetworkConfig.PortMappings) == 0 {
+ config.ContainerNetworkConfig.PortMappings = ocicniPortsToNetTypesPorts(config.ContainerNetworkConfig.OldPortMappings)
+ // keep the OldPortMappings in case an user has to downgrade podman
+
+ // indicate the the config was modified and should be written back to the db when possible
+ config.rewrite = true
+ }
+
return nil
}
diff --git a/libpod/common_test.go b/libpod/common_test.go
index 4662a33bd..67e29c265 100644
--- a/libpod/common_test.go
+++ b/libpod/common_test.go
@@ -41,18 +41,20 @@ func getTestContainer(id, name string, manager lock.Manager) (*Container, error)
ContainerNetworkConfig: ContainerNetworkConfig{
DNSServer: []net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("192.168.2.2")},
DNSSearch: []string{"example.com", "example.example.com"},
- PortMappings: []types.OCICNIPortMapping{
+ PortMappings: []types.PortMapping{
{
HostPort: 80,
ContainerPort: 90,
Protocol: "tcp",
HostIP: "192.168.3.3",
+ Range: 1,
},
{
HostPort: 100,
ContainerPort: 110,
Protocol: "udp",
HostIP: "192.168.4.4",
+ Range: 1,
},
},
},
diff --git a/libpod/container.go b/libpod/container.go
index 4d15c04c5..86989a02f 100644
--- a/libpod/container.go
+++ b/libpod/container.go
@@ -465,7 +465,7 @@ func (c *Container) NewNetNS() bool {
// PortMappings returns the ports that will be mapped into a container if
// a new network namespace is created
// If NewNetNS() is false, this value is unused
-func (c *Container) PortMappings() ([]types.OCICNIPortMapping, error) {
+func (c *Container) PortMappings() ([]types.PortMapping, error) {
// First check if the container belongs to a network namespace (like a pod)
if len(c.config.NetNsCtr) > 0 {
netNsCtr, err := c.runtime.GetContainer(c.config.NetNsCtr)
diff --git a/libpod/container_config.go b/libpod/container_config.go
index 8f803d744..412be835f 100644
--- a/libpod/container_config.go
+++ b/libpod/container_config.go
@@ -78,6 +78,11 @@ type ContainerConfig struct {
// These containers must be started before this container is started.
Dependencies []string
+ // rewrite is an internal bool to indicate that the config was modified after
+ // a read from the db, e.g. to migrate config fields after an upgrade.
+ // This field should never be written to the db, the json tag ensures this.
+ rewrite bool `json:"-"`
+
// embedded sub-configs
ContainerRootFSConfig
ContainerSecurityConfig
@@ -232,7 +237,12 @@ type ContainerNetworkConfig struct {
// PortMappings are the ports forwarded to the container's network
// namespace
// These are not used unless CreateNetNS is true
- PortMappings []types.OCICNIPortMapping `json:"portMappings,omitempty"`
+ PortMappings []types.PortMapping `json:"newPortMappings,omitempty"`
+ // OldPortMappings are the ports forwarded to the container's network
+ // namespace. As of podman 4.0 this field is deprecated, use PortMappings
+ // instead. The db will convert the old ports to the new structure for you.
+ // These are not used unless CreateNetNS is true
+ OldPortMappings []types.OCICNIPortMapping `json:"portMappings,omitempty"`
// ExposedPorts are the ports which are exposed but not forwarded
// into the container.
// The map key is the port and the string slice contains the protocols,
diff --git a/libpod/container_internal.go b/libpod/container_internal.go
index b9805faa3..19b48e14b 100644
--- a/libpod/container_internal.go
+++ b/libpod/container_internal.go
@@ -667,6 +667,19 @@ func (c *Container) refresh() error {
c.state.NetworkStatus = nil
c.state.NetworkStatusOld = nil
+ // Rewrite the config if necessary.
+ // Podman 4.0 uses a new port format in the config.
+ // getContainerConfigFromDB() already converted the old ports to the new one
+ // but it did not write the config to the db back for performance reasons.
+ // If a rewrite must happen the config.rewrite field is set to true.
+ if c.config.rewrite {
+ // SafeRewriteContainerConfig must be used with care. Make sure to not change config fields by accident.
+ if err := c.runtime.state.SafeRewriteContainerConfig(c, "", "", c.config); err != nil {
+ return errors.Wrapf(err, "failed to rewrite the config for container %s", c.config.ID)
+ }
+ c.config.rewrite = false
+ }
+
if err := c.save(); err != nil {
return errors.Wrapf(err, "error refreshing state for container %s", c.ID())
}
diff --git a/libpod/kube.go b/libpod/kube.go
index ad5acea78..e0ed911af 100644
--- a/libpod/kube.go
+++ b/libpod/kube.go
@@ -75,7 +75,7 @@ func (p *Pod) GenerateForKube(ctx context.Context) (*v1.Pod, []v1.ServicePort, e
Hostnames: []string{hostSli[0]},
})
}
- ports, err = ocicniPortMappingToContainerPort(infraContainer.config.PortMappings)
+ ports, err = portMappingToContainerPort(infraContainer.config.PortMappings)
if err != nil {
return nil, servicePorts, err
}
@@ -452,7 +452,7 @@ func containerToV1Container(ctx context.Context, c *Container) (v1.Container, []
if err != nil {
return kubeContainer, kubeVolumes, nil, annotations, err
}
- ports, err := ocicniPortMappingToContainerPort(portmappings)
+ ports, err := portMappingToContainerPort(portmappings)
if err != nil {
return kubeContainer, kubeVolumes, nil, annotations, err
}
@@ -588,29 +588,36 @@ func containerToV1Container(ctx context.Context, c *Container) (v1.Container, []
return kubeContainer, kubeVolumes, &dns, annotations, nil
}
-// ocicniPortMappingToContainerPort takes an ocicni portmapping and converts
+// portMappingToContainerPort takes an portmapping and converts
// it to a v1.ContainerPort format for kube output
-func ocicniPortMappingToContainerPort(portMappings []types.OCICNIPortMapping) ([]v1.ContainerPort, error) {
+func portMappingToContainerPort(portMappings []types.PortMapping) ([]v1.ContainerPort, error) {
containerPorts := make([]v1.ContainerPort, 0, len(portMappings))
for _, p := range portMappings {
- var protocol v1.Protocol
- switch strings.ToUpper(p.Protocol) {
- case "TCP":
- // do nothing as it is the default protocol in k8s, there is no need to explicitly
- // add it to the generated yaml
- case "UDP":
- protocol = v1.ProtocolUDP
- default:
- return containerPorts, errors.Errorf("unknown network protocol %s", p.Protocol)
- }
- cp := v1.ContainerPort{
- // Name will not be supported
- HostPort: p.HostPort,
- HostIP: p.HostIP,
- ContainerPort: p.ContainerPort,
- Protocol: protocol,
- }
- containerPorts = append(containerPorts, cp)
+ protocols := strings.Split(p.Protocol, ",")
+ for _, proto := range protocols {
+ var protocol v1.Protocol
+ switch strings.ToUpper(proto) {
+ case "TCP":
+ // do nothing as it is the default protocol in k8s, there is no need to explicitly
+ // add it to the generated yaml
+ case "UDP":
+ protocol = v1.ProtocolUDP
+ case "SCTP":
+ protocol = v1.ProtocolSCTP
+ default:
+ return containerPorts, errors.Errorf("unknown network protocol %s", p.Protocol)
+ }
+ for i := uint16(0); i < p.Range; i++ {
+ cp := v1.ContainerPort{
+ // Name will not be supported
+ HostPort: int32(p.HostPort + i),
+ HostIP: p.HostIP,
+ ContainerPort: int32(p.ContainerPort + i),
+ Protocol: protocol,
+ }
+ containerPorts = append(containerPorts, cp)
+ }
+ }
}
return containerPorts, nil
}
diff --git a/libpod/network/types/network.go b/libpod/network/types/network.go
index 5cf523e20..ba5e018fd 100644
--- a/libpod/network/types/network.go
+++ b/libpod/network/types/network.go
@@ -99,6 +99,7 @@ func (n *IPNet) UnmarshalText(text []byte) error {
// that it adds the json marshal/unmarshal methods.
// This allows us to read the mac from a json string
// and a byte array.
+// swagger:model MacAddress
type HardwareAddr net.HardwareAddr
func (h *HardwareAddr) String() string {
diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go
index e792a410c..ef261a438 100644
--- a/libpod/networking_linux.go
+++ b/libpod/networking_linux.go
@@ -11,6 +11,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
+ "sort"
"strconv"
"strings"
"syscall"
@@ -41,8 +42,11 @@ const (
// default slirp4ns subnet
defaultSlirp4netnsSubnet = "10.0.2.0/24"
- // rootlessCNINSName is the file name for the rootless network namespace bind mount
- rootlessCNINSName = "rootless-cni-ns"
+ // rootlessNetNsName is the file name for the rootless network namespace bind mount
+ rootlessNetNsName = "rootless-netns"
+
+ // rootlessNetNsSilrp4netnsPidFile is the name of the rootless netns slirp4netns pid file
+ rootlessNetNsSilrp4netnsPidFile = "rootless-netns-slirp4netns.pid"
// persistentCNIDir is the directory where the CNI files are stored
persistentCNIDir = "/var/lib/cni"
@@ -88,10 +92,7 @@ func (c *Container) getNetworkOptions() (types.NetworkOptions, error) {
ContainerID: c.config.ID,
ContainerName: getCNIPodName(c),
}
- // TODO remove ocicni PortMappings from container config and store as types PortMappings
- if len(c.config.PortMappings) > 0 {
- opts.PortMappings = ocicniPortsToNetTypesPorts(c.config.PortMappings)
- }
+ opts.PortMappings = c.config.PortMappings
networks, _, err := c.networks()
if err != nil {
return opts, err
@@ -136,21 +137,21 @@ func (c *Container) getNetworkOptions() (types.NetworkOptions, error) {
return opts, nil
}
-type RootlessCNI struct {
+type RootlessNetNS struct {
ns ns.NetNS
dir string
Lock lockfile.Locker
}
-// getPath will join the given path to the rootless cni dir
-func (r *RootlessCNI) getPath(path string) string {
+// getPath will join the given path to the rootless netns dir
+func (r *RootlessNetNS) getPath(path string) string {
return filepath.Join(r.dir, path)
}
-// Do - run the given function in the rootless cni ns.
+// Do - run the given function in the rootless netns.
// It does not lock the rootlessCNI lock, the caller
// should only lock when needed, e.g. for cni operations.
-func (r *RootlessCNI) Do(toRun func() error) error {
+func (r *RootlessNetNS) Do(toRun func() error) error {
err := r.ns.Do(func(_ ns.NetNS) error {
// Before we can run the given function,
// we have to setup all mounts correctly.
@@ -161,11 +162,11 @@ func (r *RootlessCNI) Do(toRun func() error) error {
// Because the plugins also need access to XDG_RUNTIME_DIR/netns some special setup is needed.
// The following bind mounts are needed
- // 1. XDG_RUNTIME_DIR/netns -> XDG_RUNTIME_DIR/rootless-cni/XDG_RUNTIME_DIR/netns
- // 2. /run/systemd -> XDG_RUNTIME_DIR/rootless-cni/run/systemd (only if it exists)
- // 3. XDG_RUNTIME_DIR/rootless-cni/resolv.conf -> /etc/resolv.conf or XDG_RUNTIME_DIR/rootless-cni/run/symlink/target
- // 4. XDG_RUNTIME_DIR/rootless-cni/var/lib/cni -> /var/lib/cni (if /var/lib/cni does not exists use the parent dir)
- // 5. XDG_RUNTIME_DIR/rootless-cni/run -> /run
+ // 1. XDG_RUNTIME_DIR -> XDG_RUNTIME_DIR/rootless-netns/XDG_RUNTIME_DIR
+ // 2. /run/systemd -> XDG_RUNTIME_DIR/rootless-netns/run/systemd (only if it exists)
+ // 3. XDG_RUNTIME_DIR/rootless-netns/resolv.conf -> /etc/resolv.conf or XDG_RUNTIME_DIR/rootless-netns/run/symlink/target
+ // 4. XDG_RUNTIME_DIR/rootless-netns/var/lib/cni -> /var/lib/cni (if /var/lib/cni does not exists use the parent dir)
+ // 5. XDG_RUNTIME_DIR/rootless-netns/run -> /run
// Create a new mount namespace,
// this must happen inside the netns thread.
@@ -174,16 +175,16 @@ func (r *RootlessCNI) Do(toRun func() error) error {
return errors.Wrapf(err, "cannot create a new mount namespace")
}
- netNsDir, err := netns.GetNSRunDir()
+ xdgRuntimeDir, err := util.GetRuntimeDir()
if err != nil {
- return errors.Wrap(err, "could not get network namespace directory")
+ return errors.Wrap(err, "could not get runtime directory")
}
- newNetNsDir := r.getPath(netNsDir)
+ newXDGRuntimeDir := r.getPath(xdgRuntimeDir)
// 1. Mount the netns into the new run to keep them accessible.
// Otherwise cni setup will fail because it cannot access the netns files.
- err = unix.Mount(netNsDir, newNetNsDir, "none", unix.MS_BIND|unix.MS_SHARED|unix.MS_REC, "")
+ err = unix.Mount(xdgRuntimeDir, newXDGRuntimeDir, "none", unix.MS_BIND|unix.MS_SHARED|unix.MS_REC, "")
if err != nil {
- return errors.Wrap(err, "failed to mount netns directory for rootless cni")
+ return errors.Wrap(err, "failed to mount runtime directory for rootless netns")
}
// 2. Also keep /run/systemd if it exists.
@@ -194,7 +195,7 @@ func (r *RootlessCNI) Do(toRun func() error) error {
newRunSystemd := r.getPath(runSystemd)
err = unix.Mount(runSystemd, newRunSystemd, "none", unix.MS_BIND|unix.MS_REC, "")
if err != nil {
- return errors.Wrap(err, "failed to mount /run/systemd directory for rootless cni")
+ return errors.Wrap(err, "failed to mount /run/systemd directory for rootless netns")
}
}
@@ -242,25 +243,25 @@ func (r *RootlessCNI) Do(toRun func() error) error {
rsr := r.getPath("/run/systemd/resolve")
err = unix.Mount("", rsr, "tmpfs", unix.MS_NOEXEC|unix.MS_NOSUID|unix.MS_NODEV, "")
if err != nil {
- return errors.Wrapf(err, "failed to mount tmpfs on %q for rootless cni", rsr)
+ return errors.Wrapf(err, "failed to mount tmpfs on %q for rootless netns", rsr)
}
}
if strings.HasPrefix(resolvePath, "/run/") {
resolvePath = r.getPath(resolvePath)
err = os.MkdirAll(filepath.Dir(resolvePath), 0700)
if err != nil {
- return errors.Wrap(err, "failed to create rootless-cni resolv.conf directory")
+ return errors.Wrap(err, "failed to create rootless-netns resolv.conf directory")
}
// we want to bind mount on this file so we have to create the file first
_, err = os.OpenFile(resolvePath, os.O_CREATE|os.O_RDONLY, 0700)
if err != nil {
- return errors.Wrap(err, "failed to create rootless-cni resolv.conf file")
+ return errors.Wrap(err, "failed to create rootless-netns resolv.conf file")
}
}
// mount resolv.conf to make use of the host dns
err = unix.Mount(r.getPath("resolv.conf"), resolvePath, "none", unix.MS_BIND, "")
if err != nil {
- return errors.Wrap(err, "failed to mount resolv.conf for rootless cni")
+ return errors.Wrap(err, "failed to mount resolv.conf for rootless netns")
}
// 4. CNI plugins need access to /var/lib/cni and /run
@@ -285,14 +286,14 @@ func (r *RootlessCNI) Do(toRun func() error) error {
// make sure to mount var first
err = unix.Mount(varDir, varTarget, "none", unix.MS_BIND, "")
if err != nil {
- return errors.Wrapf(err, "failed to mount %s for rootless cni", varTarget)
+ return errors.Wrapf(err, "failed to mount %s for rootless netns", varTarget)
}
// 5. Mount the new prepared run dir to /run, it has to be recursive to keep the other bind mounts.
runDir := r.getPath("run")
err = unix.Mount(runDir, "/run", "none", unix.MS_BIND|unix.MS_REC, "")
if err != nil {
- return errors.Wrap(err, "failed to mount /run for rootless cni")
+ return errors.Wrap(err, "failed to mount /run for rootless netns")
}
// run the given function in the correct namespace
@@ -302,10 +303,11 @@ func (r *RootlessCNI) Do(toRun func() error) error {
return err
}
-// Cleanup the rootless cni namespace if needed.
+// Cleanup the rootless network namespace if needed.
// It checks if we have running containers with the bridge network mode.
-// Cleanup() will try to lock RootlessCNI, therefore you have to call it with an unlocked
-func (r *RootlessCNI) Cleanup(runtime *Runtime) error {
+// Cleanup() will try to lock RootlessNetNS, therefore you have to call
+// it with an unlocked lock.
+func (r *RootlessNetNS) Cleanup(runtime *Runtime) error {
_, err := os.Stat(r.dir)
if os.IsNotExist(err) {
// the directory does not exists no need for cleanup
@@ -314,8 +316,25 @@ func (r *RootlessCNI) Cleanup(runtime *Runtime) error {
r.Lock.Lock()
defer r.Lock.Unlock()
running := func(c *Container) bool {
+ // no bridge => no need to check
+ if !c.config.NetMode.IsBridge() {
+ return false
+ }
+
// we cannot use c.state() because it will try to lock the container
- // using c.state.State directly should be good enough for this use case
+ // locking is a problem because cleanup is called after net teardown
+ // at this stage the container is already locked.
+ // also do not try to lock only containers which are not currently in net
+ // teardown because this will result in an ABBA deadlock between the rootless
+ // cni lock and the container lock
+ // because we need to get the state we have to sync otherwise this will not
+ // work because the state is empty by default
+ // I do not like this but I do not see a better way at moment
+ err := c.syncContainer()
+ if err != nil {
+ return false
+ }
+
state := c.state.State
return state == define.ContainerStateRunning
}
@@ -323,101 +342,89 @@ func (r *RootlessCNI) Cleanup(runtime *Runtime) error {
if err != nil {
return err
}
- cleanup := true
- for _, ctr := range ctrs {
- if ctr.config.NetMode.IsBridge() {
- cleanup = false
- }
+ // no cleanup if we found containers
+ if len(ctrs) > 0 {
+ return nil
}
- if cleanup {
- // make sure the the cni results (cache) dir is empty
- // libpod instances with another root dir are not covered by the check above
- // this allows several libpod instances to use the same rootless cni ns
- contents, err := ioutil.ReadDir(r.getPath("var/lib/cni/results"))
- if (err == nil && len(contents) == 0) || os.IsNotExist(err) {
- logrus.Debug("Cleaning up rootless cni namespace")
- err = netns.UnmountNS(r.ns)
- if err != nil {
- return err
- }
- // make the following errors not fatal
- err = r.ns.Close()
- if err != nil {
- logrus.Error(err)
- }
- b, err := ioutil.ReadFile(r.getPath("rootless-cni-slirp4netns.pid"))
- if err == nil {
- var i int
- i, err = strconv.Atoi(string(b))
- if err == nil {
- // kill the slirp process so we do not leak it
- err = syscall.Kill(i, syscall.SIGTERM)
- }
- }
- if err != nil {
- logrus.Errorf("Failed to kill slirp4netns process: %s", err)
- }
- err = os.RemoveAll(r.dir)
- if err != nil {
- logrus.Error(err)
- }
- } else if err != nil && !os.IsNotExist(err) {
- logrus.Errorf("Could not read rootless cni directory, skipping cleanup: %s", err)
+ logrus.Debug("Cleaning up rootless network namespace")
+ err = netns.UnmountNS(r.ns)
+ if err != nil {
+ return err
+ }
+ // make the following errors not fatal
+ err = r.ns.Close()
+ if err != nil {
+ logrus.Error(err)
+ }
+ b, err := ioutil.ReadFile(r.getPath(rootlessNetNsSilrp4netnsPidFile))
+ if err == nil {
+ var i int
+ i, err = strconv.Atoi(string(b))
+ if err == nil {
+ // kill the slirp process so we do not leak it
+ err = syscall.Kill(i, syscall.SIGTERM)
}
}
+ if err != nil {
+ logrus.Errorf("Failed to kill slirp4netns process: %s", err)
+ }
+ err = os.RemoveAll(r.dir)
+ if err != nil {
+ logrus.Error(err)
+ }
return nil
}
-// GetRootlessCNINetNs returns the rootless cni object. If create is set to true
-// the rootless cni namespace will be created if it does not exists already.
+// GetRootlessNetNs returns the rootless netns object. If create is set to true
+// the rootless network namespace will be created if it does not exists already.
// If called as root it returns always nil.
// On success the returned RootlessCNI lock is locked and must be unlocked by the caller.
-func (r *Runtime) GetRootlessCNINetNs(new bool) (*RootlessCNI, error) {
+func (r *Runtime) GetRootlessNetNs(new bool) (*RootlessNetNS, error) {
if !rootless.IsRootless() {
return nil, nil
}
- var rootlessCNINS *RootlessCNI
+ var rootlessNetNS *RootlessNetNS
runDir, err := util.GetRuntimeDir()
if err != nil {
return nil, err
}
- lfile := filepath.Join(runDir, "rootless-cni.lock")
+ lfile := filepath.Join(runDir, "rootless-netns.lock")
lock, err := lockfile.GetLockfile(lfile)
if err != nil {
- return nil, errors.Wrap(err, "failed to get rootless-cni lockfile")
+ return nil, errors.Wrap(err, "failed to get rootless-netns lockfile")
}
lock.Lock()
defer func() {
- // In case of an error (early exit) rootlessCNINS will be nil.
+ // In case of an error (early exit) rootlessNetNS will be nil.
// Make sure to unlock otherwise we could deadlock.
- if rootlessCNINS == nil {
+ if rootlessNetNS == nil {
lock.Unlock()
}
}()
- cniDir := filepath.Join(runDir, "rootless-cni")
- err = os.MkdirAll(cniDir, 0700)
+ rootlessNetNsDir := filepath.Join(runDir, rootlessNetNsName)
+ err = os.MkdirAll(rootlessNetNsDir, 0700)
if err != nil {
- return nil, errors.Wrap(err, "could not create rootless-cni directory")
+ return nil, errors.Wrap(err, "could not create rootless-netns directory")
}
nsDir, err := netns.GetNSRunDir()
if err != nil {
return nil, err
}
- path := filepath.Join(nsDir, rootlessCNINSName)
+ path := filepath.Join(nsDir, rootlessNetNsName)
ns, err := ns.GetNS(path)
if err != nil {
if !new {
// return a error if we could not get the namespace and should no create one
- return nil, errors.Wrap(err, "error getting rootless cni network namespace")
+ return nil, errors.Wrap(err, "error getting rootless network namespace")
}
// create a new namespace
- logrus.Debug("creating rootless cni network namespace")
- ns, err = netns.NewNSWithName(rootlessCNINSName)
+ logrus.Debug("creating rootless network namespace")
+ ns, err = netns.NewNSWithName(rootlessNetNsName)
if err != nil {
- return nil, errors.Wrap(err, "error creating rootless cni network namespace")
+ return nil, errors.Wrap(err, "error creating rootless network namespace")
}
// setup slirp4netns here
path := r.config.Engine.NetworkCmdPath
@@ -467,7 +474,7 @@ func (r *Runtime) GetRootlessCNINetNs(new bool) (*RootlessCNI, error) {
// Leak one end of the pipe in slirp4netns
cmd.ExtraFiles = append(cmd.ExtraFiles, syncW)
- logPath := filepath.Join(r.config.Engine.TmpDir, "slirp4netns-rootless-cni.log")
+ logPath := filepath.Join(r.config.Engine.TmpDir, "slirp4netns-rootless-netns.log")
logFile, err := os.Create(logPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to open slirp4netns log file %s", logPath)
@@ -486,9 +493,9 @@ func (r *Runtime) GetRootlessCNINetNs(new bool) (*RootlessCNI, error) {
// create pid file for the slirp4netns process
// this is need to kill the process in the cleanup
pid := strconv.Itoa(cmd.Process.Pid)
- err = ioutil.WriteFile(filepath.Join(cniDir, "rootless-cni-slirp4netns.pid"), []byte(pid), 0700)
+ err = ioutil.WriteFile(filepath.Join(rootlessNetNsDir, rootlessNetNsSilrp4netnsPidFile), []byte(pid), 0700)
if err != nil {
- errors.Wrap(err, "unable to write rootless-cni slirp4netns pid file")
+ errors.Wrap(err, "unable to write rootless-netns slirp4netns pid file")
}
defer func() {
@@ -529,43 +536,43 @@ func (r *Runtime) GetRootlessCNINetNs(new bool) (*RootlessCNI, error) {
dnsOptions := resolvconf.GetOptions(conf.Content)
nameServers := resolvconf.GetNameservers(conf.Content)
- _, err = resolvconf.Build(filepath.Join(cniDir, "resolv.conf"), append([]string{resolveIP.String()}, nameServers...), searchDomains, dnsOptions)
+ _, err = resolvconf.Build(filepath.Join(rootlessNetNsDir, "resolv.conf"), append([]string{resolveIP.String()}, nameServers...), searchDomains, dnsOptions)
if err != nil {
- return nil, errors.Wrap(err, "failed to create rootless cni resolv.conf")
+ return nil, errors.Wrap(err, "failed to create rootless netns resolv.conf")
}
// create cni directories to store files
// they will be bind mounted to the correct location in a extra mount ns
- err = os.MkdirAll(filepath.Join(cniDir, strings.TrimPrefix(persistentCNIDir, "/")), 0700)
+ err = os.MkdirAll(filepath.Join(rootlessNetNsDir, persistentCNIDir), 0700)
if err != nil {
- return nil, errors.Wrap(err, "could not create rootless-cni var directory")
+ return nil, errors.Wrap(err, "could not create rootless-netns var directory")
}
- runDir := filepath.Join(cniDir, "run")
+ runDir := filepath.Join(rootlessNetNsDir, "run")
err = os.MkdirAll(runDir, 0700)
if err != nil {
- return nil, errors.Wrap(err, "could not create rootless-cni run directory")
+ return nil, errors.Wrap(err, "could not create rootless-netns run directory")
}
// relabel the new run directory to the iptables /run label
// this is important, otherwise the iptables command will fail
err = label.Relabel(runDir, "system_u:object_r:iptables_var_run_t:s0", false)
if err != nil {
- return nil, errors.Wrap(err, "could not create relabel rootless-cni run directory")
+ return nil, errors.Wrap(err, "could not create relabel rootless-netns run directory")
}
// create systemd run directory
err = os.MkdirAll(filepath.Join(runDir, "systemd"), 0700)
if err != nil {
- return nil, errors.Wrap(err, "could not create rootless-cni systemd directory")
+ return nil, errors.Wrap(err, "could not create rootless-netns systemd directory")
}
// create the directory for the netns files at the same location
- // relative to the rootless-cni location
- err = os.MkdirAll(filepath.Join(cniDir, nsDir), 0700)
+ // relative to the rootless-netns location
+ err = os.MkdirAll(filepath.Join(rootlessNetNsDir, nsDir), 0700)
if err != nil {
- return nil, errors.Wrap(err, "could not create rootless-cni netns directory")
+ return nil, errors.Wrap(err, "could not create rootless-netns netns directory")
}
}
- // The CNI plugins need access to iptables in $PATH. As it turns out debian doesn't put
- // /usr/sbin in $PATH for rootless users. This will break rootless cni completely.
+ // The CNI plugins and netavark need access to iptables in $PATH. As it turns out debian doesn't put
+ // /usr/sbin in $PATH for rootless users. This will break rootless networking completely.
// We might break existing users and we cannot expect everyone to change their $PATH so
// lets add /usr/sbin to $PATH ourselves.
path = os.Getenv("PATH")
@@ -574,14 +581,14 @@ func (r *Runtime) GetRootlessCNINetNs(new bool) (*RootlessCNI, error) {
os.Setenv("PATH", path)
}
- // Important set rootlessCNINS as last step.
+ // Important set rootlessNetNS as last step.
// Do not return any errors after this.
- rootlessCNINS = &RootlessCNI{
+ rootlessNetNS = &RootlessNetNS{
ns: ns,
- dir: cniDir,
+ dir: rootlessNetNsDir,
Lock: lock,
}
- return rootlessCNINS, nil
+ return rootlessNetNS, nil
}
// setPrimaryMachineIP is used for podman-machine and it sets
@@ -603,14 +610,14 @@ func setPrimaryMachineIP() error {
}
// setUpNetwork will set up the the networks, on error it will also tear down the cni
-// networks. If rootless it will join/create the rootless cni namespace.
+// networks. If rootless it will join/create the rootless network namespace.
func (r *Runtime) setUpNetwork(ns string, opts types.NetworkOptions) (map[string]types.StatusBlock, error) {
if r.config.MachineEnabled() {
if err := setPrimaryMachineIP(); err != nil {
return nil, err
}
}
- rootlessCNINS, err := r.GetRootlessCNINetNs(true)
+ rootlessNetNS, err := r.GetRootlessNetNs(true)
if err != nil {
return nil, err
}
@@ -619,11 +626,11 @@ func (r *Runtime) setUpNetwork(ns string, opts types.NetworkOptions) (map[string
results, err = r.network.Setup(ns, types.SetupOptions{NetworkOptions: opts})
return err
}
- // rootlessCNINS is nil if we are root
- if rootlessCNINS != nil {
- // execute the cni setup in the rootless net ns
- err = rootlessCNINS.Do(setUpPod)
- rootlessCNINS.Lock.Unlock()
+ // rootlessNetNS is nil if we are root
+ if rootlessNetNS != nil {
+ // execute the setup in the rootless net ns
+ err = rootlessNetNS.Do(setUpPod)
+ rootlessNetNS.Lock.Unlock()
} else {
err = setUpPod()
}
@@ -697,10 +704,10 @@ func (r *Runtime) setupRootlessNetNS(ctr *Container) error {
return err
}
if len(networks) > 0 && len(ctr.config.PortMappings) > 0 {
- // set up port forwarder for CNI-in-slirp4netns
+ // set up port forwarder for rootless netns
netnsPath := ctr.state.NetNS.Path()
// TODO: support slirp4netns port forwarder as well
- // make sure to fix this container.handleRestartPolicy() as well
+ // make sure to fix this in container.handleRestartPolicy() as well
return r.setupRootlessPortMappingViaRLK(ctr, netnsPath)
}
return nil
@@ -719,7 +726,7 @@ func (r *Runtime) setupNetNS(ctr *Container) error {
if err != nil {
return err
}
- nsPath = filepath.Join(nsPath, fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]))
+ nsPath = filepath.Join(nsPath, fmt.Sprintf("netns-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]))
if err := os.MkdirAll(filepath.Dir(nsPath), 0711); err != nil {
return err
@@ -777,10 +784,10 @@ func (r *Runtime) closeNetNS(ctr *Container) error {
return nil
}
-// Tear down a container's CNI network configuration and joins the
+// Tear down a container's network configuration and joins the
// rootless net ns as rootless user
func (r *Runtime) teardownNetwork(ns string, opts types.NetworkOptions) error {
- rootlessCNINS, err := r.GetRootlessCNINetNs(false)
+ rootlessNetNS, err := r.GetRootlessNetNs(false)
if err != nil {
return err
}
@@ -789,13 +796,13 @@ func (r *Runtime) teardownNetwork(ns string, opts types.NetworkOptions) error {
return errors.Wrapf(err, "error tearing down network namespace configuration for container %s", opts.ContainerID)
}
- // rootlessCNINS is nil if we are root
- if rootlessCNINS != nil {
+ // rootlessNetNS is nil if we are root
+ if rootlessNetNS != nil {
// execute the cni setup in the rootless net ns
- err = rootlessCNINS.Do(tearDownPod)
- rootlessCNINS.Lock.Unlock()
+ err = rootlessNetNS.Do(tearDownPod)
+ rootlessNetNS.Lock.Unlock()
if err == nil {
- err = rootlessCNINS.Cleanup(r)
+ err = rootlessNetNS.Cleanup(r)
}
} else {
err = tearDownPod()
@@ -1200,9 +1207,7 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) erro
ContainerID: c.config.ID,
ContainerName: getCNIPodName(c),
}
- if len(c.config.PortMappings) > 0 {
- opts.PortMappings = ocicniPortsToNetTypesPorts(c.config.PortMappings)
- }
+ opts.PortMappings = c.config.PortMappings
eth, exists := c.state.NetInterfaceDescriptions.getInterfaceByName(netName)
if !exists {
return errors.Errorf("no network interface name for container %s on network %s", c.config.ID, netName)
@@ -1294,9 +1299,7 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e
ContainerID: c.config.ID,
ContainerName: getCNIPodName(c),
}
- if len(c.config.PortMappings) > 0 {
- opts.PortMappings = ocicniPortsToNetTypesPorts(c.config.PortMappings)
- }
+ opts.PortMappings = c.config.PortMappings
eth, exists := c.state.NetInterfaceDescriptions.getInterfaceByName(netName)
if !exists {
return errors.Errorf("no network interface name for container %s on network %s", c.config.ID, netName)
@@ -1364,16 +1367,67 @@ func (r *Runtime) normalizeNetworkName(nameOrID string) (string, error) {
return net.Name, nil
}
+// ocicniPortsToNetTypesPorts convert the old port format to the new one
+// while deduplicating ports into ranges
func ocicniPortsToNetTypesPorts(ports []types.OCICNIPortMapping) []types.PortMapping {
+ if len(ports) == 0 {
+ return nil
+ }
+
newPorts := make([]types.PortMapping, 0, len(ports))
- for _, port := range ports {
- newPorts = append(newPorts, types.PortMapping{
- HostIP: port.HostIP,
- HostPort: uint16(port.HostPort),
- ContainerPort: uint16(port.ContainerPort),
- Protocol: port.Protocol,
- Range: 1,
- })
+
+ // first sort the ports
+ sort.Slice(ports, func(i, j int) bool {
+ return compareOCICNIPorts(ports[i], ports[j])
+ })
+
+ // we already check if the slice is empty so we can use the first element
+ currentPort := types.PortMapping{
+ HostIP: ports[0].HostIP,
+ HostPort: uint16(ports[0].HostPort),
+ ContainerPort: uint16(ports[0].ContainerPort),
+ Protocol: ports[0].Protocol,
+ Range: 1,
+ }
+
+ for i := 1; i < len(ports); i++ {
+ if ports[i].HostIP == currentPort.HostIP &&
+ ports[i].Protocol == currentPort.Protocol &&
+ ports[i].HostPort-int32(currentPort.Range) == int32(currentPort.HostPort) &&
+ ports[i].ContainerPort-int32(currentPort.Range) == int32(currentPort.ContainerPort) {
+ currentPort.Range = currentPort.Range + 1
+ } else {
+ newPorts = append(newPorts, currentPort)
+ currentPort = types.PortMapping{
+ HostIP: ports[i].HostIP,
+ HostPort: uint16(ports[i].HostPort),
+ ContainerPort: uint16(ports[i].ContainerPort),
+ Protocol: ports[i].Protocol,
+ Range: 1,
+ }
+ }
}
+ newPorts = append(newPorts, currentPort)
return newPorts
}
+
+// compareOCICNIPorts will sort the ocicni ports by
+// 1) host ip
+// 2) protocol
+// 3) hostPort
+// 4) container port
+func compareOCICNIPorts(i, j types.OCICNIPortMapping) bool {
+ if i.HostIP != j.HostIP {
+ return i.HostIP < j.HostIP
+ }
+
+ if i.Protocol != j.Protocol {
+ return i.Protocol < j.Protocol
+ }
+
+ if i.HostPort != j.HostPort {
+ return i.HostPort < j.HostPort
+ }
+
+ return i.ContainerPort < j.ContainerPort
+}
diff --git a/libpod/networking_linux_test.go b/libpod/networking_linux_test.go
new file mode 100644
index 000000000..06bf05723
--- /dev/null
+++ b/libpod/networking_linux_test.go
@@ -0,0 +1,323 @@
+package libpod
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_ocicniPortsToNetTypesPorts(t *testing.T) {
+ tests := []struct {
+ name string
+ arg []types.OCICNIPortMapping
+ want []types.PortMapping
+ }{
+ {
+ name: "no ports",
+ arg: nil,
+ want: nil,
+ },
+ {
+ name: "empty ports",
+ arg: []types.OCICNIPortMapping{},
+ want: nil,
+ },
+ {
+ name: "single port",
+ arg: []types.OCICNIPortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "two separate ports",
+ arg: []types.OCICNIPortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 9000,
+ ContainerPort: 90,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 9000,
+ ContainerPort: 90,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "two ports joined",
+ arg: []types.OCICNIPortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 2,
+ },
+ },
+ },
+ {
+ name: "three ports with different container port are not joined",
+ arg: []types.OCICNIPortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 79,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 8082,
+ ContainerPort: 82,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 79,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 8082,
+ ContainerPort: 82,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "three ports joined (not sorted)",
+ arg: []types.OCICNIPortMapping{
+ {
+ HostPort: 8081,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 8082,
+ ContainerPort: 82,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 3,
+ },
+ },
+ },
+ {
+ name: "different protocols ports are not joined",
+ arg: []types.OCICNIPortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 81,
+ Protocol: "udp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 81,
+ Protocol: "udp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "different host ip ports are not joined",
+ arg: []types.OCICNIPortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ HostIP: "192.168.1.1",
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ HostIP: "192.168.1.2",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ HostIP: "192.168.1.1",
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ Range: 1,
+ HostIP: "192.168.1.2",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ result := ocicniPortsToNetTypesPorts(tt.arg)
+ assert.Equal(t, tt.want, result, "ports do not match")
+ })
+ }
+}
+
+func benchmarkOCICNIPortsToNetTypesPorts(b *testing.B, ports []types.OCICNIPortMapping) {
+ for n := 0; n < b.N; n++ {
+ ocicniPortsToNetTypesPorts(ports)
+ }
+}
+
+func Benchmark_ocicniPortsToNetTypesPortsNoPorts(b *testing.B) {
+ benchmarkOCICNIPortsToNetTypesPorts(b, nil)
+}
+
+func Benchmark_ocicniPortsToNetTypesPorts1(b *testing.B) {
+ benchmarkOCICNIPortsToNetTypesPorts(b, []types.OCICNIPortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ })
+}
+
+func Benchmark_ocicniPortsToNetTypesPorts10(b *testing.B) {
+ ports := make([]types.OCICNIPortMapping, 0, 10)
+ for i := int32(8080); i < 8090; i++ {
+ ports = append(ports, types.OCICNIPortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkOCICNIPortsToNetTypesPorts(b, ports)
+}
+
+func Benchmark_ocicniPortsToNetTypesPorts100(b *testing.B) {
+ ports := make([]types.OCICNIPortMapping, 0, 100)
+ for i := int32(8080); i < 8180; i++ {
+ ports = append(ports, types.OCICNIPortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkOCICNIPortsToNetTypesPorts(b, ports)
+}
+
+func Benchmark_ocicniPortsToNetTypesPorts1k(b *testing.B) {
+ ports := make([]types.OCICNIPortMapping, 0, 1000)
+ for i := int32(8080); i < 9080; i++ {
+ ports = append(ports, types.OCICNIPortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkOCICNIPortsToNetTypesPorts(b, ports)
+}
+
+func Benchmark_ocicniPortsToNetTypesPorts10k(b *testing.B) {
+ ports := make([]types.OCICNIPortMapping, 0, 30000)
+ for i := int32(8080); i < 18080; i++ {
+ ports = append(ports, types.OCICNIPortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkOCICNIPortsToNetTypesPorts(b, ports)
+}
+
+func Benchmark_ocicniPortsToNetTypesPorts1m(b *testing.B) {
+ ports := make([]types.OCICNIPortMapping, 0, 1000000)
+ for j := 0; j < 20; j++ {
+ for i := int32(1); i <= 50000; i++ {
+ ports = append(ports, types.OCICNIPortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ HostIP: fmt.Sprintf("192.168.1.%d", j),
+ })
+ }
+ }
+ b.ResetTimer()
+ benchmarkOCICNIPortsToNetTypesPorts(b, ports)
+}
diff --git a/libpod/networking_slirp4netns.go b/libpod/networking_slirp4netns.go
index 56e8eca99..760427f22 100644
--- a/libpod/networking_slirp4netns.go
+++ b/libpod/networking_slirp4netns.go
@@ -38,9 +38,9 @@ type slirpFeatures struct {
type slirp4netnsCmdArg struct {
Proto string `json:"proto,omitempty"`
HostAddr string `json:"host_addr"`
- HostPort int32 `json:"host_port"`
+ HostPort uint16 `json:"host_port"`
GuestAddr string `json:"guest_addr"`
- GuestPort int32 `json:"guest_port"`
+ GuestPort uint16 `json:"guest_port"`
}
type slirp4netnsCmd struct {
diff --git a/libpod/oci_util.go b/libpod/oci_util.go
index c1afc0d20..6d99d5836 100644
--- a/libpod/oci_util.go
+++ b/libpod/oci_util.go
@@ -32,93 +32,108 @@ func createUnitName(prefix string, name string) string {
}
// Bind ports to keep them closed on the host
-func bindPorts(ports []types.OCICNIPortMapping) ([]*os.File, error) {
+func bindPorts(ports []types.PortMapping) ([]*os.File, error) {
var files []*os.File
- notifySCTP := false
- for _, i := range ports {
- isV6 := net.ParseIP(i.HostIP).To4() == nil
- if i.HostIP == "" {
+ sctpWarning := true
+ for _, port := range ports {
+ isV6 := net.ParseIP(port.HostIP).To4() == nil
+ if port.HostIP == "" {
isV6 = false
}
- switch i.Protocol {
- case "udp":
- var (
- addr *net.UDPAddr
- err error
- )
- if isV6 {
- addr, err = net.ResolveUDPAddr("udp6", fmt.Sprintf("[%s]:%d", i.HostIP, i.HostPort))
- } else {
- addr, err = net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", i.HostIP, i.HostPort))
- }
- if err != nil {
- return nil, errors.Wrapf(err, "cannot resolve the UDP address")
+ protocols := strings.Split(port.Protocol, ",")
+ for _, protocol := range protocols {
+ for i := uint16(0); i < port.Range; i++ {
+ f, err := bindPort(protocol, port.HostIP, port.HostPort+i, isV6, &sctpWarning)
+ if err != nil {
+ return files, err
+ }
+ if f != nil {
+ files = append(files, f)
+ }
}
+ }
+ }
+ return files, nil
+}
- proto := "udp4"
- if isV6 {
- proto = "udp6"
- }
- server, err := net.ListenUDP(proto, addr)
- if err != nil {
- return nil, errors.Wrapf(err, "cannot listen on the UDP port")
- }
- f, err := server.File()
- if err != nil {
- return nil, errors.Wrapf(err, "cannot get file for UDP socket")
- }
- files = append(files, f)
- // close the listener
- // note that this does not affect the fd, see the godoc for server.File()
- err = server.Close()
- if err != nil {
- logrus.Warnf("Failed to close connection: %v", err)
- }
+func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool) (*os.File, error) {
+ var file *os.File
+ switch protocol {
+ case "udp":
+ var (
+ addr *net.UDPAddr
+ err error
+ )
+ if isV6 {
+ addr, err = net.ResolveUDPAddr("udp6", fmt.Sprintf("[%s]:%d", hostIP, port))
+ } else {
+ addr, err = net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", hostIP, port))
+ }
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot resolve the UDP address")
+ }
- case "tcp":
- var (
- addr *net.TCPAddr
- err error
- )
- if isV6 {
- addr, err = net.ResolveTCPAddr("tcp6", fmt.Sprintf("[%s]:%d", i.HostIP, i.HostPort))
- } else {
- addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", i.HostIP, i.HostPort))
- }
- if err != nil {
- return nil, errors.Wrapf(err, "cannot resolve the TCP address")
- }
+ proto := "udp4"
+ if isV6 {
+ proto = "udp6"
+ }
+ server, err := net.ListenUDP(proto, addr)
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot listen on the UDP port")
+ }
+ file, err = server.File()
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot get file for UDP socket")
+ }
+ // close the listener
+ // note that this does not affect the fd, see the godoc for server.File()
+ err = server.Close()
+ if err != nil {
+ logrus.Warnf("Failed to close connection: %v", err)
+ }
- proto := "tcp4"
- if isV6 {
- proto = "tcp6"
- }
- server, err := net.ListenTCP(proto, addr)
- if err != nil {
- return nil, errors.Wrapf(err, "cannot listen on the TCP port")
- }
- f, err := server.File()
- if err != nil {
- return nil, errors.Wrapf(err, "cannot get file for TCP socket")
- }
- files = append(files, f)
- // close the listener
- // note that this does not affect the fd, see the godoc for server.File()
- err = server.Close()
- if err != nil {
- logrus.Warnf("Failed to close connection: %v", err)
- }
+ case "tcp":
+ var (
+ addr *net.TCPAddr
+ err error
+ )
+ if isV6 {
+ addr, err = net.ResolveTCPAddr("tcp6", fmt.Sprintf("[%s]:%d", hostIP, port))
+ } else {
+ addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", hostIP, port))
+ }
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot resolve the TCP address")
+ }
- case "sctp":
- if !notifySCTP {
- notifySCTP = true
- logrus.Warnf("Port reservation for SCTP is not supported")
- }
- default:
- return nil, fmt.Errorf("unknown protocol %s", i.Protocol)
+ proto := "tcp4"
+ if isV6 {
+ proto = "tcp6"
+ }
+ server, err := net.ListenTCP(proto, addr)
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot listen on the TCP port")
+ }
+ file, err = server.File()
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot get file for TCP socket")
+ }
+ // close the listener
+ // note that this does not affect the fd, see the godoc for server.File()
+ err = server.Close()
+ if err != nil {
+ logrus.Warnf("Failed to close connection: %v", err)
}
+
+ case "sctp":
+ if *sctpWarning {
+ logrus.Info("Port reservation for SCTP is not supported")
+ *sctpWarning = false
+ }
+ default:
+ return nil, fmt.Errorf("unknown protocol %s", protocol)
}
- return files, nil
+ return file, nil
}
func getOCIRuntimeError(runtimeMsg string) error {
diff --git a/libpod/options.go b/libpod/options.go
index f1cf015f8..250b16556 100644
--- a/libpod/options.go
+++ b/libpod/options.go
@@ -1051,7 +1051,7 @@ func WithDependencyCtrs(ctrs []*Container) CtrCreateOption {
// namespace with a minimal configuration.
// An optional array of port mappings can be provided.
// Conflicts with WithNetNSFrom().
-func WithNetNS(portMappings []nettypes.OCICNIPortMapping, exposedPorts map[uint16][]string, postConfigureNetNS bool, netmode string, networks []string) CtrCreateOption {
+func WithNetNS(portMappings []nettypes.PortMapping, exposedPorts map[uint16][]string, postConfigureNetNS bool, netmode string, networks []string) CtrCreateOption {
return func(ctr *Container) error {
if ctr.valid {
return define.ErrCtrFinalized
@@ -2084,21 +2084,6 @@ func WithInfraContainer() PodCreateOption {
}
}
-// WithInfraContainerPorts tells the pod to add port bindings to the pause container
-func WithInfraContainerPorts(bindings []nettypes.OCICNIPortMapping, infraSpec *specgen.SpecGenerator) []nettypes.PortMapping {
- bindingSpec := []nettypes.PortMapping{}
- for _, bind := range bindings {
- currBind := nettypes.PortMapping{}
- currBind.ContainerPort = uint16(bind.ContainerPort)
- currBind.HostIP = bind.HostIP
- currBind.HostPort = uint16(bind.HostPort)
- currBind.Protocol = bind.Protocol
- bindingSpec = append(bindingSpec, currBind)
- }
- infraSpec.PortMappings = bindingSpec
- return infraSpec.PortMappings
-}
-
// WithVolatile sets the volatile flag for the container storage.
// The option can potentially cause data loss when used on a container that must survive a machine reboot.
func WithVolatile() CtrCreateOption {
diff --git a/libpod/state_test.go b/libpod/state_test.go
index 4799d7b8d..5c3b0d7f7 100644
--- a/libpod/state_test.go
+++ b/libpod/state_test.go
@@ -11,6 +11,7 @@ import (
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/libpod/lock"
+ "github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/storage"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -3705,3 +3706,42 @@ func TestGetContainerConfigNonExistentIDFails(t *testing.T) {
assert.Error(t, err)
})
}
+
+// Test that the state will convert the ports to the new format
+func TestConvertPortMapping(t *testing.T) {
+ runForAllStates(t, func(t *testing.T, state State, manager lock.Manager) {
+ testCtr, err := getTestCtr1(manager)
+ assert.NoError(t, err)
+
+ ports := testCtr.config.PortMappings
+
+ oldPorts := []types.OCICNIPortMapping{
+ {
+ HostPort: 80,
+ ContainerPort: 90,
+ Protocol: "tcp",
+ HostIP: "192.168.3.3",
+ },
+ {
+ HostPort: 100,
+ ContainerPort: 110,
+ Protocol: "udp",
+ HostIP: "192.168.4.4",
+ },
+ }
+
+ testCtr.config.OldPortMappings = oldPorts
+ testCtr.config.PortMappings = nil
+
+ err = state.AddContainer(testCtr)
+ assert.NoError(t, err)
+
+ retrievedCtr, err := state.Container(testCtr.ID())
+ assert.NoError(t, err)
+
+ // set values to expected ones
+ testCtr.config.PortMappings = ports
+
+ testContainersEqual(t, retrievedCtr, testCtr, true)
+ })
+}
diff --git a/libpod/util.go b/libpod/util.go
index 8f8303ff2..5154a261e 100644
--- a/libpod/util.go
+++ b/libpod/util.go
@@ -295,19 +295,21 @@ func writeHijackHeader(r *http.Request, conn io.Writer) {
}
// Convert OCICNI port bindings into Inspect-formatted port bindings.
-func makeInspectPortBindings(bindings []types.OCICNIPortMapping, expose map[uint16][]string) map[string][]define.InspectHostPort {
+func makeInspectPortBindings(bindings []types.PortMapping, expose map[uint16][]string) map[string][]define.InspectHostPort {
portBindings := make(map[string][]define.InspectHostPort)
for _, port := range bindings {
- key := fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol)
- hostPorts := portBindings[key]
- if hostPorts == nil {
- hostPorts = []define.InspectHostPort{}
+ protocols := strings.Split(port.Protocol, ",")
+ for _, protocol := range protocols {
+ for i := uint16(0); i < port.Range; i++ {
+ key := fmt.Sprintf("%d/%s", port.ContainerPort+i, protocol)
+ hostPorts := portBindings[key]
+ hostPorts = append(hostPorts, define.InspectHostPort{
+ HostIP: port.HostIP,
+ HostPort: fmt.Sprintf("%d", port.HostPort+i),
+ })
+ portBindings[key] = hostPorts
+ }
}
- hostPorts = append(hostPorts, define.InspectHostPort{
- HostIP: port.HostIP,
- HostPort: fmt.Sprintf("%d", port.HostPort),
- })
- portBindings[key] = hostPorts
}
// add exposed ports without host port information to match docker
for port, protocols := range expose {
diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go
index c63cf8f0e..35120a1a5 100644
--- a/pkg/api/handlers/types.go
+++ b/pkg/api/handlers/types.go
@@ -44,7 +44,13 @@ type ContainersPruneReport struct {
type LibpodContainersPruneReport struct {
ID string `json:"Id"`
SpaceReclaimed int64 `json:"Size"`
- PruneError string `json:"Err,omitempty"`
+ // Error which occurred during prune operation (if any).
+ // This field is optional and may be omitted if no error occurred.
+ //
+ // Extensions:
+ // x-omitempty: true
+ // x-nullable: true
+ PruneError string `json:"Err,omitempty"`
}
type Info struct {
diff --git a/pkg/api/server/handler_rid.go b/pkg/api/server/handler_rid.go
index b624b99a6..7dcf436f7 100644
--- a/pkg/api/server/handler_rid.go
+++ b/pkg/api/server/handler_rid.go
@@ -2,6 +2,7 @@ package server
import (
"fmt"
+ "io/ioutil"
"net/http"
"github.com/containers/podman/v3/pkg/api/types"
@@ -15,7 +16,13 @@ import (
// and Apache style request logging
func referenceIDHandler() mux.MiddlewareFunc {
return func(h http.Handler) http.Handler {
- return handlers.CombinedLoggingHandler(logrus.StandardLogger().Out,
+ // Only log Apache access_log-like entries at Info level or below
+ out := ioutil.Discard
+ if logrus.IsLevelEnabled(logrus.InfoLevel) {
+ out = logrus.StandardLogger().Out
+ }
+
+ return handlers.CombinedLoggingHandler(out,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rid := r.Header.Get("X-Reference-Id")
if rid == "" {
diff --git a/pkg/checkpoint/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go
index 46b47db98..da82c9745 100644
--- a/pkg/checkpoint/checkpoint_restore.go
+++ b/pkg/checkpoint/checkpoint_restore.go
@@ -195,7 +195,7 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt
}
if len(restoreOptions.PublishPorts) > 0 {
- ports, _, _, err := generate.ParsePortMapping(restoreOptions.PublishPorts)
+ ports, err := generate.ParsePortMapping(restoreOptions.PublishPorts, nil)
if err != nil {
return nil, err
}
diff --git a/pkg/domain/entities/container_ps.go b/pkg/domain/entities/container_ps.go
index 58f231a2f..d018d373f 100644
--- a/pkg/domain/entities/container_ps.go
+++ b/pkg/domain/entities/container_ps.go
@@ -54,7 +54,7 @@ type ListContainer struct {
// boolean to be set
PodName string
// Port mappings
- Ports []types.OCICNIPortMapping
+ Ports []types.PortMapping
// Size of the container rootfs. Requires the size boolean to be true
Size *define.ContainerSize
// Time when container started
diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go
index deae85fe1..869c616ea 100644
--- a/pkg/domain/entities/containers.go
+++ b/pkg/domain/entities/containers.go
@@ -422,7 +422,7 @@ type ContainerPortOptions struct {
// the CLI to output ports
type ContainerPortReport struct {
Id string //nolint
- Ports []nettypes.OCICNIPortMapping
+ Ports []nettypes.PortMapping
}
// ContainerCpOptions describes input options for cp.
diff --git a/pkg/domain/entities/system.go b/pkg/domain/entities/system.go
index fe041dec8..49f0c2323 100644
--- a/pkg/domain/entities/system.go
+++ b/pkg/domain/entities/system.go
@@ -100,7 +100,7 @@ type SystemVersionReport struct {
// SystemUnshareOptions describes the options for the unshare command
type SystemUnshareOptions struct {
- RootlessCNI bool
+ RootlessNetNS bool
}
type ComponentVersion struct {
diff --git a/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go
index e326f26a8..7da7754f2 100644
--- a/pkg/domain/infra/abi/system.go
+++ b/pkg/domain/infra/abi/system.go
@@ -360,15 +360,15 @@ func (ic *ContainerEngine) Unshare(ctx context.Context, args []string, options e
return cmd.Run()
}
- if options.RootlessCNI {
- rootlesscni, err := ic.Libpod.GetRootlessCNINetNs(true)
+ if options.RootlessNetNS {
+ rootlessNetNS, err := ic.Libpod.GetRootlessNetNs(true)
if err != nil {
return err
}
// make sure to unlock, unshare can run for a long time
- rootlesscni.Lock.Unlock()
- defer rootlesscni.Cleanup(ic.Libpod)
- return rootlesscni.Do(unshare)
+ rootlessNetNS.Lock.Unlock()
+ defer rootlessNetNS.Cleanup(ic.Libpod)
+ return rootlessNetNS.Do(unshare)
}
return unshare()
}
diff --git a/pkg/rootlessport/rootlessport_linux.go b/pkg/rootlessport/rootlessport_linux.go
index 7b9e5bbfa..7e6075789 100644
--- a/pkg/rootlessport/rootlessport_linux.go
+++ b/pkg/rootlessport/rootlessport_linux.go
@@ -23,7 +23,7 @@ const (
// Config needs to be provided to the process via stdin as a JSON string.
// stdin needs to be closed after the message has been written.
type Config struct {
- Mappings []types.OCICNIPortMapping
+ Mappings []types.PortMapping
NetNSPath string
ExitFD int
ReadyFD int
diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go
index 88ebc7ae3..501bce05d 100644
--- a/pkg/specgen/generate/pod_create.go
+++ b/pkg/specgen/generate/pod_create.go
@@ -204,11 +204,11 @@ func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime, infraSpec
// replacing necessary values with those specified in pod creation
func MapSpec(p *specgen.PodSpecGenerator) (*specgen.SpecGenerator, error) {
if len(p.PortMappings) > 0 {
- ports, _, _, err := ParsePortMapping(p.PortMappings)
+ ports, err := ParsePortMapping(p.PortMappings, nil)
if err != nil {
return nil, err
}
- p.InfraContainerSpec.PortMappings = libpod.WithInfraContainerPorts(ports, p.InfraContainerSpec)
+ p.InfraContainerSpec.PortMappings = ports
}
switch p.NetNS.NSMode {
case specgen.Default, "":
diff --git a/pkg/specgen/generate/ports.go b/pkg/specgen/generate/ports.go
index 992b4a8e9..53a5e5697 100644
--- a/pkg/specgen/generate/ports.go
+++ b/pkg/specgen/generate/ports.go
@@ -2,7 +2,9 @@ package generate
import (
"context"
+ "fmt"
"net"
+ "sort"
"strconv"
"strings"
@@ -11,6 +13,7 @@ import (
"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"
)
@@ -21,252 +24,323 @@ const (
protoSCTP = "sctp"
)
-// Parse port maps to OCICNI port mappings.
-// Returns a set of OCICNI port mappings, and maps of utilized container and
+// 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) ([]types.OCICNIPortMapping, 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 types.OCICNIPortMapping
- startOfRange bool
- isInRange bool
+func ParsePortMapping(portMappings []types.PortMapping, exposePorts map[uint16][]string) ([]types.PortMapping, error) {
+ if len(portMappings) == 0 && len(exposePorts) == 0 {
+ return nil, nil
}
- 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)
+
+ // tempMapping stores the ports without ip and protocol
+ type tempMapping struct {
+ hostPort uint16
+ containerPort uint16
+ rangePort uint16
}
- postAssignHostPort := false
+ // 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)
- // Iterate through all port mappings, generating OCICNI PortMapping
- // structs and validating there is no overlap.
+ // 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, nil, nil, err
- }
-
- // Validate host IP
- hostIP := port.HostIP
- if hostIP == "" {
- hostIP = "0.0.0.0"
+ return nil, err
}
- if ip := net.ParseIP(hostIP); ip == nil {
- return nil, nil, nil, errors.Errorf("invalid IP address %s in port mapping", port.HostIP)
+ 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.
- len := port.Range
- if len == 0 {
- len = 1
+ portRange := port.Range
+ if portRange == 0 {
+ portRange = 1
}
containerPort := port.ContainerPort
if containerPort == 0 {
- return nil, nil, nil, errors.Errorf("container port number must be non-0")
+ return 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(portRange-1)+uint32(containerPort) > 65535 {
+ return 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")
+ if uint32(portRange-1)+uint32(hostPort) > 65535 {
+ return 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
+ hostProtoMap, ok := portMap[port.HostIP]
+ if !ok {
+ hostProtoMap = make(map[string][]tempMapping)
+ for _, proto := range []string{protoTCP, protoUDP, protoSCTP} {
+ hostProtoMap[proto] = make([]tempMapping, 0)
}
- 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")
- }
+ portMap[port.HostIP] = hostProtoMap
+ }
- // 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
- }
- }
+ p := tempMapping{
+ hostPort: port.HostPort,
+ containerPort: port.ContainerPort,
+ rangePort: portRange,
+ }
- // 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 := types.OCICNIPortMapping{
- 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,
- },
- )
- }
+ for _, proto := range protocols {
+ hostProtoMap[proto] = append(hostProtoMap[proto], p)
}
}
- // Handle any 0 host ports now by setting random container ports.
- if postAssignHostPort {
- remadeMappings := make([]types.OCICNIPortMapping, 0, len(tempMappings))
-
- var (
- candidate int
- err error
- )
-
- // Iterate over all
- for _, tmp := range tempMappings {
- p := tmp.mapping
+ // 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]
- if p.HostPort != 0 {
- remadeMappings = append(remadeMappings, p)
+ for hostIP, protoMap := range portMap {
+ for protocol, ports := range protoMap {
+ ports := ports
+ if len(ports) == 0 {
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
+ // 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
+ }
}
- ctrPortMap, ok := ctrIPMap[p.HostIP]
- if !ok {
- ctrPortMap = make(map[uint16]uint16)
- ctrIPMap[p.HostIP] = ctrPortMap
+ if previousPort != nil {
+ addPortToUsedPorts(&portMappings, &allUsedHostPorts,
+ &allUsedContainerPorts, &usedHostPorts, previousPort)
}
- // See if container port has been used elsewhere
- if ctrPortMap[uint16(p.ContainerPort)] != 0 {
- // Duplicate definition. Let's not bother
- // including it.
- continue
+ // 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)
}
- // 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()
+ 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, nil, nil, errors.Wrapf(err, "error getting candidate host port for container port %d", p.ContainerPort)
+ return nil, err
}
+ portMappings = append(portMappings, p)
} 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
+ newProtocols = append(newProtocols, protocol)
}
}
- if p.HostPort == 0 {
- return nil, nil, nil, errors.Errorf("could not find open host port to map container port %d to", p.ContainerPort)
+ // 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
}
- remadeMappings = append(remadeMappings, p)
}
- return remadeMappings, containerPortValidate, hostPortValidate, nil
}
+ return portMappings, nil
+}
- finalMappings := []types.OCICNIPortMapping{}
- for _, m := range tempMappings {
- finalMappings = append(finalMappings, m.mapping)
+func appendProtocolsNoDuplicates(slice []string, protocols []string) []string {
+ for _, proto := range protocols {
+ if util.StringInSlice(proto, slice) {
+ continue
+ }
+ slice = append(slice, proto)
}
-
- return finalMappings, containerPortValidate, hostPortValidate, nil
+ return slice
}
// Make final port mappings for the container
-func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.OCICNIPortMapping, map[uint16][]string, error) {
- finalMappings, containerPortValidate, hostPortValidate, err := ParsePortMapping(s.PortMappings)
- if err != nil {
- return nil, nil, err
- }
-
- // No exposed ports so return the port mappings we've made so far.
- if len(s.Expose) == 0 && imageData == nil {
- return finalMappings, nil, nil
- }
-
- logrus.Debugf("Adding exposed ports")
-
+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 {
@@ -274,103 +348,30 @@ func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData
}
}
- // 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, nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port)
- }
-
- if port == 0 {
- return nil, 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
+ 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")
}
-
- 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
+ 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)
}
}
- // If not publishing exposed ports return mappings and exposed ports.
+ publishPorts := toExpose
if !s.PublishExposedPorts {
- return finalMappings, toExpose, nil
+ publishPorts = nil
}
- // 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, 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 := types.OCICNIPortMapping{
- 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, nil, errors.Errorf("failed to find an open port to expose container port %d on the host", port)
- }
- }
+ finalMappings, err := ParsePortMapping(s.PortMappings, publishPorts)
+ if err != nil {
+ return nil, nil, err
}
-
- return finalMappings, nil, nil
+ return finalMappings, toExpose, nil
}
// Check a string to ensure it is a comma-separated set of valid protocols
@@ -409,7 +410,7 @@ func checkProtocol(protocol string, allowSCTP bool) ([]string, error) {
}
func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error) {
- expose := make(map[uint16]string)
+ expose := make(map[uint16]string, len(exposedPorts))
for imgExpose := range exposedPorts {
// Expose format is portNumber[/protocol]
splitExpose := strings.SplitN(imgExpose, "/", 2)
@@ -420,12 +421,20 @@ func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error
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"
+
+ // 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 {
- expose[uint16(num)] = splitExpose[1]
+ proto = newProto
}
+ expose[uint16(num)] = proto
}
return expose, nil
}
diff --git a/pkg/specgen/generate/ports_bench_test.go b/pkg/specgen/generate/ports_bench_test.go
new file mode 100644
index 000000000..06f02acda
--- /dev/null
+++ b/pkg/specgen/generate/ports_bench_test.go
@@ -0,0 +1,197 @@
+package generate
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+)
+
+func benchmarkParsePortMapping(b *testing.B, ports []types.PortMapping) {
+ for n := 0; n < b.N; n++ {
+ ParsePortMapping(ports, nil)
+ }
+}
+
+func BenchmarkParsePortMappingNoPorts(b *testing.B) {
+ benchmarkParsePortMapping(b, nil)
+}
+
+func BenchmarkParsePortMapping1(b *testing.B) {
+ benchmarkParsePortMapping(b, []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ })
+}
+
+func BenchmarkParsePortMapping100(b *testing.B) {
+ ports := make([]types.PortMapping, 0, 100)
+ for i := uint16(8080); i < 8180; i++ {
+ ports = append(ports, types.PortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkParsePortMapping(b, ports)
+}
+
+func BenchmarkParsePortMapping1k(b *testing.B) {
+ ports := make([]types.PortMapping, 0, 1000)
+ for i := uint16(8080); i < 9080; i++ {
+ ports = append(ports, types.PortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkParsePortMapping(b, ports)
+}
+
+func BenchmarkParsePortMapping10k(b *testing.B) {
+ ports := make([]types.PortMapping, 0, 30000)
+ for i := uint16(8080); i < 18080; i++ {
+ ports = append(ports, types.PortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkParsePortMapping(b, ports)
+}
+
+func BenchmarkParsePortMapping1m(b *testing.B) {
+ ports := make([]types.PortMapping, 0, 1000000)
+ for j := 0; j < 20; j++ {
+ for i := uint16(1); i <= 50000; i++ {
+ ports = append(ports, types.PortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ HostIP: fmt.Sprintf("192.168.1.%d", j),
+ })
+ }
+ }
+ b.ResetTimer()
+ benchmarkParsePortMapping(b, ports)
+}
+
+func BenchmarkParsePortMappingReverse100(b *testing.B) {
+ ports := make([]types.PortMapping, 0, 100)
+ for i := uint16(8180); i > 8080; i-- {
+ ports = append(ports, types.PortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkParsePortMapping(b, ports)
+}
+
+func BenchmarkParsePortMappingReverse1k(b *testing.B) {
+ ports := make([]types.PortMapping, 0, 1000)
+ for i := uint16(9080); i > 8080; i-- {
+ ports = append(ports, types.PortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkParsePortMapping(b, ports)
+}
+
+func BenchmarkParsePortMappingReverse10k(b *testing.B) {
+ ports := make([]types.PortMapping, 0, 30000)
+ for i := uint16(18080); i > 8080; i-- {
+ ports = append(ports, types.PortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ })
+ }
+ b.ResetTimer()
+ benchmarkParsePortMapping(b, ports)
+}
+
+func BenchmarkParsePortMappingReverse1m(b *testing.B) {
+ ports := make([]types.PortMapping, 0, 1000000)
+ for j := 0; j < 20; j++ {
+ for i := uint16(50000); i > 0; i-- {
+ ports = append(ports, types.PortMapping{
+ HostPort: i,
+ ContainerPort: i,
+ Protocol: "tcp",
+ HostIP: fmt.Sprintf("192.168.1.%d", j),
+ })
+ }
+ }
+ b.ResetTimer()
+ benchmarkParsePortMapping(b, ports)
+}
+
+func BenchmarkParsePortMappingRange1(b *testing.B) {
+ benchmarkParsePortMapping(b, []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ })
+}
+
+func BenchmarkParsePortMappingRange100(b *testing.B) {
+ benchmarkParsePortMapping(b, []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 100,
+ },
+ })
+}
+
+func BenchmarkParsePortMappingRange1k(b *testing.B) {
+ benchmarkParsePortMapping(b, []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1000,
+ },
+ })
+}
+
+func BenchmarkParsePortMappingRange10k(b *testing.B) {
+ benchmarkParsePortMapping(b, []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 10000,
+ },
+ })
+}
+
+func BenchmarkParsePortMappingRange1m(b *testing.B) {
+ ports := make([]types.PortMapping, 0, 1000000)
+ for j := 0; j < 20; j++ {
+ ports = append(ports, types.PortMapping{
+ HostPort: 1,
+ ContainerPort: 1,
+ Protocol: "tcp",
+ Range: 50000,
+ HostIP: fmt.Sprintf("192.168.1.%d", j),
+ })
+ }
+ b.ResetTimer()
+ benchmarkParsePortMapping(b, ports)
+}
diff --git a/pkg/specgen/generate/ports_test.go b/pkg/specgen/generate/ports_test.go
new file mode 100644
index 000000000..20d5d0166
--- /dev/null
+++ b/pkg/specgen/generate/ports_test.go
@@ -0,0 +1,989 @@
+package generate
+
+import (
+ "testing"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParsePortMappingWithHostPort(t *testing.T) {
+ tests := []struct {
+ name string
+ arg []types.PortMapping
+ arg2 map[uint16][]string
+ want []types.PortMapping
+ }{
+ {
+ name: "no ports",
+ arg: nil,
+ want: nil,
+ },
+ {
+ name: "one tcp port",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "one tcp port no proto",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "one udp port",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "udp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "udp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "one sctp port",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "sctp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "sctp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "one port two protocols",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp,udp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "udp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "one port three protocols",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp,udp,sctp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "udp",
+ Range: 1,
+ },
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "sctp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "one port with range 1",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "one port with range 5",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 5,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 5,
+ },
+ },
+ },
+ {
+ name: "two ports joined",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 2,
+ },
+ },
+ },
+ {
+ name: "two ports joined with range",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 2,
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 2,
+ },
+ },
+ },
+ {
+ name: "two ports with no overlapping range",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 10,
+ },
+ {
+ HostPort: 9090,
+ ContainerPort: 9090,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 9090,
+ ContainerPort: 9090,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 10,
+ },
+ },
+ },
+ {
+ name: "four ports with two overlapping ranges",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 10,
+ },
+ {
+ HostPort: 8085,
+ ContainerPort: 85,
+ Protocol: "tcp",
+ Range: 10,
+ },
+ {
+ HostPort: 100,
+ ContainerPort: 5,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 101,
+ ContainerPort: 6,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 15,
+ },
+ {
+ HostPort: 100,
+ ContainerPort: 5,
+ Protocol: "tcp",
+ Range: 2,
+ },
+ },
+ },
+ {
+ name: "two overlapping ranges",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 10,
+ },
+ {
+ HostPort: 8085,
+ ContainerPort: 85,
+ Protocol: "tcp",
+ Range: 2,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 10,
+ },
+ },
+ },
+ {
+ name: "four overlapping ranges",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 10,
+ },
+ {
+ HostPort: 8085,
+ ContainerPort: 85,
+ Protocol: "tcp",
+ Range: 2,
+ },
+ {
+ HostPort: 8090,
+ ContainerPort: 90,
+ Protocol: "tcp",
+ Range: 7,
+ },
+ {
+ HostPort: 8095,
+ ContainerPort: 95,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 17,
+ },
+ },
+ },
+ {
+ name: "one port range overlaps 5 ports",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Range: 20,
+ },
+ {
+ HostPort: 8085,
+ ContainerPort: 85,
+ Range: 2,
+ },
+ {
+ HostPort: 8090,
+ ContainerPort: 90,
+ },
+ {
+ HostPort: 8095,
+ ContainerPort: 95,
+ },
+ {
+ HostPort: 8096,
+ ContainerPort: 96,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 20,
+ },
+ },
+ },
+ {
+ name: "different host ip same port",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ HostIP: "192.168.1.1",
+ },
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ HostIP: "192.168.2.1",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ HostIP: "192.168.1.1",
+ Range: 1,
+ },
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ HostIP: "192.168.2.1",
+ Range: 1,
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParsePortMapping(tt.arg, tt.arg2)
+ assert.NoError(t, err, "error is not nil")
+ // use ElementsMatch instead of Equal because the order is not consistent
+ assert.ElementsMatch(t, tt.want, got, "got unexpected port mapping")
+ })
+ }
+}
+
+func TestParsePortMappingWithoutHostPort(t *testing.T) {
+ tests := []struct {
+ name string
+ arg []types.PortMapping
+ arg2 map[uint16][]string
+ want []types.PortMapping
+ }{
+ {
+ name: "one tcp port",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "one port with two protocols",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp,udp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "udp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "same port twice",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "neighbor ports are not joined",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "overlapping range ports are joined",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 2,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 2,
+ },
+ },
+ },
+ {
+ name: "four overlapping range ports are joined",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 3,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 81,
+ Protocol: "tcp",
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 82,
+ Protocol: "tcp",
+ Range: 10,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 90,
+ Protocol: "tcp",
+ Range: 5,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 15,
+ },
+ },
+ },
+ {
+ name: "expose one tcp port",
+ arg2: map[uint16][]string{
+ 8080: {"tcp"},
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 8080,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "expose already defined port",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 8080,
+ Protocol: "tcp",
+ },
+ },
+ arg2: map[uint16][]string{
+ 8080: {"tcp"},
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 8080,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ },
+ {
+ name: "expose different proto",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 8080,
+ Protocol: "tcp",
+ },
+ },
+ arg2: map[uint16][]string{
+ 8080: {"udp"},
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 8080,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 8080,
+ Protocol: "udp",
+ Range: 1,
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParsePortMapping(tt.arg, tt.arg2)
+ assert.NoError(t, err, "error is not nil")
+
+ // because we always get random host ports when it is set to 0 we cannot check that exactly
+ // check if it is not 0 and set to to 0 afterwards
+ for i := range got {
+ assert.Greater(t, got[i].HostPort, uint16(0), "host port is zero")
+ got[i].HostPort = 0
+ }
+
+ // use ElementsMatch instead of Equal because the order is not consistent
+ assert.ElementsMatch(t, tt.want, got, "got unexpected port mapping")
+ })
+ }
+}
+
+func TestParsePortMappingMixedHostPort(t *testing.T) {
+ tests := []struct {
+ name string
+ arg []types.PortMapping
+ want []types.PortMapping
+ resetHostPorts []int
+ }{
+ {
+ name: "two ports one without a hostport set",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ },
+ {
+ HostPort: 8080,
+ ContainerPort: 8080,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 8080,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ resetHostPorts: []int{1},
+ },
+ {
+ name: "two ports one without a hostport set, inverted order",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 8080,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 8080,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ resetHostPorts: []int{1},
+ },
+ {
+ name: "three ports without host ports, one with a hostport set, , inverted order",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 85,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 90,
+ },
+ {
+ HostPort: 8080,
+ ContainerPort: 8080,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 8080,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 85,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 90,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ resetHostPorts: []int{1, 2, 3},
+ },
+ {
+ name: "three ports without host ports, one with a hostport set",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 8080,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 90,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 85,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ },
+ },
+ want: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 8080,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 85,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 90,
+ Protocol: "tcp",
+ Range: 1,
+ },
+ },
+ resetHostPorts: []int{1, 2, 3},
+ },
+ }
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParsePortMapping(tt.arg, nil)
+ assert.NoError(t, err, "error is not nil")
+
+ // because we always get random host ports when it is set to 0 we cannot check that exactly
+ // use resetHostPorts to know which port element is 0
+ for _, num := range tt.resetHostPorts {
+ assert.Greater(t, got[num].HostPort, uint16(0), "host port is zero")
+ got[num].HostPort = 0
+ }
+
+ assert.Equal(t, tt.want, got, "got unexpected port mapping")
+ })
+ }
+}
+
+func TestParsePortMappingError(t *testing.T) {
+ tests := []struct {
+ name string
+ arg []types.PortMapping
+ err string
+ }{
+ {
+ name: "container port is 0",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 0,
+ Protocol: "tcp",
+ },
+ },
+ err: "container port number must be non-0",
+ },
+ {
+ name: "container port range exceeds max",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 65000,
+ Protocol: "tcp",
+ Range: 10000,
+ },
+ },
+ err: "container port range exceeds maximum allowable port number",
+ },
+ {
+ name: "host port range exceeds max",
+ arg: []types.PortMapping{
+ {
+ HostPort: 60000,
+ ContainerPort: 1,
+ Protocol: "tcp",
+ Range: 10000,
+ },
+ },
+ err: "host port range exceeds maximum allowable port number",
+ },
+ {
+ name: "invalid protocol",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "1",
+ },
+ },
+ err: "unrecognized protocol \"1\" in port mapping",
+ },
+ {
+ name: "invalid protocol 2",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Protocol: "udp,u",
+ },
+ },
+ err: "unrecognized protocol \"u\" in port mapping",
+ },
+ {
+ name: "invalid ip address",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ HostIP: "blah",
+ },
+ },
+ err: "invalid IP address \"blah\" in port mapping",
+ },
+ {
+ name: "invalid overalpping range",
+ arg: []types.PortMapping{
+ {
+ HostPort: 8080,
+ ContainerPort: 80,
+ Range: 5,
+ },
+ {
+ HostPort: 8081,
+ ContainerPort: 60,
+ },
+ },
+ err: "conflicting port mappings for host port 8081 (protocol tcp)",
+ },
+ {
+ name: "big port range with host port zero does not fit",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 1,
+ Range: 65535,
+ },
+ },
+ err: "failed to find an open port to expose container port 1 with range 65535 on the host",
+ },
+ {
+ name: "big port range with host port zero does not fit",
+ arg: []types.PortMapping{
+ {
+ HostPort: 0,
+ ContainerPort: 80,
+ Range: 1,
+ },
+ {
+ HostPort: 0,
+ ContainerPort: 1000,
+ Range: 64535,
+ },
+ },
+ err: "failed to find an open port to expose container port 1000 with range 64535 on the host",
+ },
+ }
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := ParsePortMapping(tt.arg, nil)
+ assert.EqualError(t, err, tt.err, "error does not match")
+ })
+ }
+}
diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go
index 32d5be79a..948fb990c 100644
--- a/pkg/specgen/podspecgen.go
+++ b/pkg/specgen/podspecgen.go
@@ -99,6 +99,7 @@ type PodNetworkConfig struct {
// Only available if NetNS is set to Bridge (the default for root).
// As such, conflicts with NoInfra=true by proxy.
// Optional.
+ // swagger:strfmt string
StaticMAC *types.HardwareAddr `json:"static_mac,omitempty"`
// PortMappings is a set of ports to map into the infra container.
// As, by default, containers share their network with the infra
diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go
index 593d91c64..8a4497130 100644
--- a/pkg/specgen/specgen.go
+++ b/pkg/specgen/specgen.go
@@ -401,6 +401,7 @@ type ContainerNetworkConfig struct {
// StaticMAC is a static MAC address to set in the container.
// Only available if NetNS is set to bridge.
// Optional.
+ // swagger:strfmt string
StaticMAC *nettypes.HardwareAddr `json:"static_mac,omitempty"`
// PortBindings is a set of ports to map into the container.
// Only available if NetNS is set to bridge or slirp.
diff --git a/test/e2e/run_networking_test.go b/test/e2e/run_networking_test.go
index bdf3ce5d6..c64cfd2d5 100644
--- a/test/e2e/run_networking_test.go
+++ b/test/e2e/run_networking_test.go
@@ -494,6 +494,23 @@ var _ = Describe("Podman run networking", func() {
Expect(containerConfig[0].NetworkSettings.Ports["80/tcp"][0].HostPort).ToNot(Equal(80))
})
+ It("podman run forward sctp protocol", func() {
+ SkipIfRootless("sctp protocol only works as root")
+ session := podmanTest.Podman([]string{"--log-level=info", "run", "--name=test", "-p", "80/sctp", "-p", "81/sctp", ALPINE})
+ session.Wait(90)
+ Expect(session).Should(Exit(0))
+ // we can only check logrus on local podman
+ if !IsRemote() {
+ // check that the info message for sctp protocol is only displayed once
+ Expect(strings.Count(session.ErrorToString(), "Port reservation for SCTP is not supported")).To(Equal(1), "`Port reservation for SCTP is not supported` is not displayed exactly one time in the logrus logs")
+ }
+ results := podmanTest.Podman([]string{"inspect", "test"})
+ results.Wait(30)
+ Expect(results).Should(Exit(0))
+ Expect(results.OutputToString()).To(ContainSubstring(`"80/sctp":`))
+ Expect(results.OutputToString()).To(ContainSubstring(`"81/sctp":`))
+ })
+
It("podman run hostname test", func() {
session := podmanTest.Podman([]string{"run", "--rm", ALPINE, "printenv", "HOSTNAME"})
session.WaitWithDefaultTimeout()
diff --git a/test/e2e/unshare_test.go b/test/e2e/unshare_test.go
index 79ce68e89..cf1b8db53 100644
--- a/test/e2e/unshare_test.go
+++ b/test/e2e/unshare_test.go
@@ -51,7 +51,7 @@ var _ = Describe("Podman unshare", func() {
})
It("podman unshare --rootles-cni", func() {
- session := podmanTest.Podman([]string{"unshare", "--rootless-cni", "ip", "addr"})
+ session := podmanTest.Podman([]string{"unshare", "--rootless-netns", "ip", "addr"})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
Expect(session.OutputToString()).To(ContainSubstring("tap0"))
diff --git a/test/upgrade/test-upgrade.bats b/test/upgrade/test-upgrade.bats
index e9910f3d2..f6f32242b 100644
--- a/test/upgrade/test-upgrade.bats
+++ b/test/upgrade/test-upgrade.bats
@@ -97,6 +97,7 @@ podman \$opts run --name myfailedcontainer --label mylabel=$LABEL_FAILED \
podman \$opts run -d --name myrunningcontainer --label mylabel=$LABEL_RUNNING \
--network bridge \
-p $HOST_PORT:80 \
+ -p 127.0.0.1:8080-8082:8080-8082 \
-v $pmroot/var/www:/var/www \
-w /var/www \
--mac-address aa:bb:cc:dd:ee:ff \
@@ -186,7 +187,7 @@ EOF
is "${lines[1]}" "mycreatedcontainer--Created----$LABEL_CREATED" "created"
is "${lines[2]}" "mydonecontainer--Exited (0).*----<no value>" "done"
is "${lines[3]}" "myfailedcontainer--Exited (17) .*----$LABEL_FAILED" "fail"
- is "${lines[4]}" "myrunningcontainer--Up .*--0.0.0.0:$HOST_PORT->80/tcp--$LABEL_RUNNING" "running"
+ is "${lines[4]}" "myrunningcontainer--Up .*--0\.0\.0\.0:$HOST_PORT->80\/tcp, 127\.0\.0\.1\:8080-8082->8080-8082\/tcp--$LABEL_RUNNING" "running"
# For debugging: dump containers and IDs
if [[ -n "$PODMAN_UPGRADE_TEST_DEBUG" ]]; then