diff options
Diffstat (limited to 'libpod')
36 files changed, 608 insertions, 2421 deletions
diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go index 5df3e8961..56b4bafd3 100644 --- a/libpod/boltdb_state.go +++ b/libpod/boltdb_state.go @@ -1223,7 +1223,7 @@ func (s *BoltState) NetworkConnect(ctr *Container, network string, aliases []str } ctrNetworks := ctr.config.Networks if len(ctrNetworks) == 0 { - ctrNetworks = []string{ctr.runtime.netPlugin.GetDefaultNetworkName()} + ctrNetworks = []string{ctr.runtime.config.Network.DefaultNetwork} } // Copy in all the container's CNI networks for _, net := range ctrNetworks { diff --git a/libpod/common_test.go b/libpod/common_test.go index 4c419cfa8..4662a33bd 100644 --- a/libpod/common_test.go +++ b/libpod/common_test.go @@ -10,7 +10,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/lock" - "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/containers/podman/v3/libpod/network/types" "github.com/opencontainers/runtime-tools/generate" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -41,7 +41,7 @@ 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: []ocicni.PortMapping{ + PortMappings: []types.OCICNIPortMapping{ { HostPort: 80, ContainerPort: 90, diff --git a/libpod/container.go b/libpod/container.go index a4bbb5dd0..cf727926c 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -8,14 +8,14 @@ import ( "os" "time" - "github.com/containernetworking/cni/pkg/types" cnitypes "github.com/containernetworking/cni/pkg/types/current" "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/manifest" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/lock" + "github.com/containers/podman/v3/libpod/network/cni" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/storage" - "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -114,14 +114,11 @@ type Container struct { rootlessPortSyncR *os.File rootlessPortSyncW *os.File - // A restored container should have the same IP address as before - // being checkpointed. If requestedIP is set it will be used instead - // of config.StaticIP. - requestedIP net.IP - // A restored container should have the same MAC address as before - // being checkpointed. If requestedMAC is set it will be used instead - // of config.StaticMAC. - requestedMAC net.HardwareAddr + // perNetworkOpts should be set when you want to use special network + // options when calling network setup/teardown. This should be used for + // container restore or network reload for example. Leave this nil if + // the settings from the container config should be used. + perNetworkOpts map[string]types.PerNetworkOptions // This is true if a container is restored from a checkpoint. restoreFromCheckpoint bool @@ -173,11 +170,20 @@ type ContainerState struct { // Podman. // These are DEPRECATED and will be removed in a future release. LegacyExecSessions map[string]*legacyExecSession `json:"execSessions,omitempty"` - // NetworkStatus contains the configuration results for all networks + // NetworkStatusOld contains the configuration results for all networks // the pod is attached to. Only populated if we created a network // namespace for the container, and the network namespace is currently - // active - NetworkStatus []*cnitypes.Result `json:"networkResults,omitempty"` + // active. + // These are DEPRECATED and will be removed in a future release. + // This field is only used for backwarts compatibility. + NetworkStatusOld []*cnitypes.Result `json:"networkResults,omitempty"` + // NetworkStatus contains the network Status for all networks + // the container is attached to. Only populated if we created a network + // namespace for the container, and the network namespace is currently + // active. + // To read this field use container.getNetworkStatus() instead, this will + // take care of migrating the old DEPRECATED network status to the new format. + NetworkStatus map[string]types.StatusBlock `json:"networkStatus,omitempty"` // BindMounts contains files that will be bind-mounted into the // container when it is mounted. // These include /etc/hosts and /etc/resolv.conf @@ -454,7 +460,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() ([]ocicni.PortMapping, error) { +func (c *Container) PortMappings() ([]types.OCICNIPortMapping, 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) @@ -788,66 +794,6 @@ func (c *Container) ExecSession(id string) (*ExecSession, error) { return returnSession, nil } -// IPs retrieves a container's IP address(es) -// This will only be populated if the container is configured to created a new -// network namespace, and that namespace is presently active -func (c *Container) IPs() ([]net.IPNet, error) { - if !c.batched { - c.lock.Lock() - defer c.lock.Unlock() - - if err := c.syncContainer(); err != nil { - return nil, err - } - } - - if !c.config.CreateNetNS { - return nil, errors.Wrapf(define.ErrInvalidArg, "container %s network namespace is not managed by libpod", c.ID()) - } - - ips := make([]net.IPNet, 0) - - for _, r := range c.state.NetworkStatus { - for _, ip := range r.IPs { - ips = append(ips, ip.Address) - } - } - - return ips, nil -} - -// Routes retrieves a container's routes -// This will only be populated if the container is configured to created a new -// network namespace, and that namespace is presently active -func (c *Container) Routes() ([]types.Route, error) { - if !c.batched { - c.lock.Lock() - defer c.lock.Unlock() - - if err := c.syncContainer(); err != nil { - return nil, err - } - } - - if !c.config.CreateNetNS { - return nil, errors.Wrapf(define.ErrInvalidArg, "container %s network namespace is not managed by libpod", c.ID()) - } - - routes := make([]types.Route, 0) - - for _, r := range c.state.NetworkStatus { - for _, route := range r.Routes { - newRoute := types.Route{ - Dst: route.Dst, - GW: route.GW, - } - routes = append(routes, newRoute) - } - } - - return routes, nil -} - // BindMounts retrieves bind mounts that were created by libpod and will be // added to the container // All these mounts except /dev/shm are ignored if a mount in the given spec has @@ -1230,7 +1176,7 @@ func (c *Container) networks() ([]string, bool, error) { networks, err := c.runtime.state.GetNetworks(c) if err != nil && errors.Cause(err) == define.ErrNoSuchNetwork { if len(c.config.Networks) == 0 && c.config.NetMode.IsBridge() { - return []string{c.runtime.netPlugin.GetDefaultNetworkName()}, true, nil + return []string{c.runtime.config.Network.DefaultNetwork}, true, nil } return c.config.Networks, false, nil } @@ -1267,3 +1213,37 @@ func (d ContainerNetworkDescriptions) getInterfaceByName(networkName string) (st } return fmt.Sprintf("eth%d", val), exists } + +// getNetworkStatus get the current network status from the state. If the container +// still uses the old network status it is converted to the new format. This function +// should be used instead of reading c.state.NetworkStatus directly. +func (c *Container) getNetworkStatus() map[string]types.StatusBlock { + if c.state.NetworkStatus != nil { + return c.state.NetworkStatus + } + if c.state.NetworkStatusOld != nil { + // Note: NetworkStatusOld does not contain the network names so we get them extra + // Generally the order should be the same + networks, _, err := c.networks() + if err != nil { + return nil + } + if len(networks) != len(c.state.NetworkStatusOld) { + return nil + } + result := make(map[string]types.StatusBlock, len(c.state.NetworkStatusOld)) + for i := range c.state.NetworkStatusOld { + status, err := cni.CNIResultToStatus(c.state.NetworkStatusOld[i]) + if err != nil { + return nil + } + result[networks[i]] = status + } + c.state.NetworkStatus = result + _ = c.save() + // TODO remove debug for final version + logrus.Debugf("converted old network result to new result %v", result) + return result + } + return nil +} diff --git a/libpod/container_config.go b/libpod/container_config.go index a2c989a1a..0374c25fe 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -6,9 +6,9 @@ import ( "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/manifest" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/namespaces" "github.com/containers/storage" - "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" ) @@ -230,7 +230,7 @@ type ContainerNetworkConfig struct { // PortMappings are the ports forwarded to the container's network // namespace // These are not used unless CreateNetNS is true - PortMappings []ocicni.PortMapping `json:"portMappings,omitempty"` + PortMappings []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 1033729ae..9ac2cd5bd 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -644,17 +644,8 @@ func (c *Container) refresh() error { } c.lock = lock - // Try to delete any lingering IP allocations. - // If this fails, just log and ignore. - // I'm a little concerned that this is so far down in refresh() and we - // could fail before getting to it - but the worst that would happen is - // that Inspect() would return info on IPs we no longer own. - if len(c.state.NetworkStatus) > 0 { - if err := c.removeIPv4Allocations(); err != nil { - logrus.Errorf("Error removing IP allocations for container %s: %v", c.ID(), err) - } - } c.state.NetworkStatus = nil + c.state.NetworkStatusOld = nil if err := c.save(); err != nil { return errors.Wrapf(err, "error refreshing state for container %s", c.ID()) @@ -668,57 +659,6 @@ func (c *Container) refresh() error { return nil } -// Try and remove IP address allocations. Presently IPv4 only. -// Should be safe as rootless because NetworkStatus should only be populated if -// CNI is running. -func (c *Container) removeIPv4Allocations() error { - cniNetworksDir, err := getCNINetworksDir() - if err != nil { - return err - } - - if len(c.state.NetworkStatus) == 0 { - return nil - } - - cniDefaultNetwork := "" - if c.runtime.netPlugin != nil { - cniDefaultNetwork = c.runtime.netPlugin.GetDefaultNetworkName() - } - - networks, _, err := c.networks() - if err != nil { - return err - } - - if len(networks) != len(c.state.NetworkStatus) { - return errors.Wrapf(define.ErrInternal, "network mismatch: asked to join %d CNI networks but got %d CNI results", len(networks), len(c.state.NetworkStatus)) - } - - for index, result := range c.state.NetworkStatus { - for _, ctrIP := range result.IPs { - if ctrIP.Version != "4" { - continue - } - candidate := "" - if len(networks) > 0 { - // CNI returns networks in order we passed them. - // So our index into results should be our index - // into networks. - candidate = filepath.Join(cniNetworksDir, networks[index], ctrIP.Address.IP.String()) - } else { - candidate = filepath.Join(cniNetworksDir, cniDefaultNetwork, ctrIP.Address.IP.String()) - } - logrus.Debugf("Going to try removing IP address reservation file %q for container %s", candidate, c.ID()) - if err := os.Remove(candidate); err != nil && !os.IsNotExist(err) { - return errors.Wrapf(err, "error removing CNI IP reservation file %q for container %s", candidate, c.ID()) - } - } - } - - return nil -} - // Remove conmon attach socket and terminal resize FIFO // This is necessary for restarting containers func (c *Container) removeConmonFiles() error { @@ -1017,11 +957,9 @@ func (c *Container) completeNetworkSetup() error { } state := c.state // collect any dns servers that cni tells us to use (dnsname) - for _, cni := range state.NetworkStatus { - if cni.DNS.Nameservers != nil { - for _, server := range cni.DNS.Nameservers { - outResolvConf = append(outResolvConf, fmt.Sprintf("nameserver %s", server)) - } + for _, status := range c.getNetworkStatus() { + for _, server := range status.DNSServerIPs { + outResolvConf = append(outResolvConf, fmt.Sprintf("nameserver %s", server)) } } // check if we have a bindmount for /etc/hosts @@ -1062,9 +1000,12 @@ func (c *Container) completeNetworkSetup() error { func (c *Container) cniHosts() string { var hosts string - if len(c.state.NetworkStatus) > 0 && len(c.state.NetworkStatus[0].IPs) > 0 { - ipAddress := strings.Split(c.state.NetworkStatus[0].IPs[0].Address.String(), "/")[0] - hosts += fmt.Sprintf("%s\t%s %s\n", ipAddress, c.Hostname(), c.Config().Name) + for _, status := range c.getNetworkStatus() { + for _, netInt := range status.Interfaces { + for _, netAddress := range netInt.Networks { + hosts += fmt.Sprintf("%s\t%s %s\n", netAddress.Subnet.IP.String(), c.Hostname(), c.Config().Name) + } + } } return hosts } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 4194a0d93..0557b30d0 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -22,7 +22,6 @@ import ( metadata "github.com/checkpoint-restore/checkpointctl/lib" cdi "github.com/container-orchestrated-devices/container-device-interface/pkg" - cnitypes "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/buildah/pkg/chrootuser" "github.com/containers/buildah/pkg/overlay" @@ -34,6 +33,7 @@ import ( "github.com/containers/common/pkg/umask" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/events" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/annotations" "github.com/containers/podman/v3/pkg/cgroups" "github.com/containers/podman/v3/pkg/checkpoint/crutils" @@ -81,7 +81,7 @@ func (c *Container) prepare() error { var ( wg sync.WaitGroup netNS ns.NetNS - networkStatus []*cnitypes.Result + networkStatus map[string]types.StatusBlock createNetNSErr, mountStorageErr error mountPoint string tmpStateLock sync.Mutex @@ -263,6 +263,7 @@ func (c *Container) cleanupNetwork() error { c.state.NetNS = nil c.state.NetworkStatus = nil + c.state.NetworkStatusOld = nil if c.valid { return c.save() @@ -1121,7 +1122,8 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO // Save network.status. This is needed to restore the container with // the same IP. Currently limited to one IP address in a container // with one interface. - if _, err := metadata.WriteJSONFile(c.state.NetworkStatus, c.bundlePath(), metadata.NetworkStatusFile); err != nil { + // FIXME: will this break something? + if _, err := metadata.WriteJSONFile(c.getNetworkStatus(), c.bundlePath(), metadata.NetworkStatusFile); err != nil { return err } @@ -1261,8 +1263,11 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti } // Read network configuration from checkpoint - // Currently only one interface with one IP is supported. - networkStatus, _, err := metadata.ReadContainerCheckpointNetworkStatus(c.bundlePath()) + var netStatus map[string]types.StatusBlock + _, err := metadata.ReadJSONFile(&netStatus, c.bundlePath(), metadata.NetworkStatusFile) + if err != nil { + logrus.Infof("failed to unmarshal network status, cannot restore the same ip/mac: %v", err) + } // If the restored container should get a new name, the IP address of // the container will not be restored. This assumes that if a new name is // specified, the container is restored multiple times. @@ -1271,19 +1276,41 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti // best solution. if err == nil && options.Name == "" && (!options.IgnoreStaticIP || !options.IgnoreStaticMAC) { // The file with the network.status does exist. Let's restore the - // container with the same IP address / MAC address as during checkpointing. - if !options.IgnoreStaticIP { - if IP := metadata.GetIPFromNetworkStatus(networkStatus); IP != nil { - // Tell CNI which IP address we want. - c.requestedIP = IP - } + // container with the same networks settings as during checkpointing. + aliases, err := c.runtime.state.GetAllNetworkAliases(c) + if err != nil { + return err } - if !options.IgnoreStaticMAC { - if MAC := metadata.GetMACFromNetworkStatus(networkStatus); MAC != nil { - // Tell CNI which MAC address we want. - c.requestedMAC = MAC + netOpts := make(map[string]types.PerNetworkOptions, len(netStatus)) + for network, status := range netStatus { + perNetOpts := types.PerNetworkOptions{} + for name, netInt := range status.Interfaces { + perNetOpts = types.PerNetworkOptions{ + InterfaceName: name, + Aliases: aliases[network], + } + if !options.IgnoreStaticMAC { + perNetOpts.StaticMAC = netInt.MacAddress + } + if !options.IgnoreStaticIP { + for _, netAddress := range netInt.Networks { + perNetOpts.StaticIPs = append(perNetOpts.StaticIPs, netAddress.Subnet.IP) + } + } + // Normally interfaces have a length of 1, only for some special cni configs we could get more. + // For now just use the first interface to get the ips this should be good enough for most cases. + break + } + if perNetOpts.InterfaceName == "" { + eth, exists := c.state.NetInterfaceDescriptions.getInterfaceByName(network) + if !exists { + return errors.Errorf("no network interface name for container %s on network %s", c.config.ID, network) + } + perNetOpts.InterfaceName = eth } + netOpts[network] = perNetOpts } + c.perNetworkOpts = netOpts } defer func() { @@ -1785,9 +1812,9 @@ rootless=%d // generateResolvConf generates a containers resolv.conf func (c *Container) generateResolvConf() (string, error) { var ( - nameservers []string - cniNameServers []string - cniSearchDomains []string + nameservers []string + networkNameServers []string + networkSearchDomains []string ) resolvConf := "/etc/resolv.conf" @@ -1827,22 +1854,27 @@ func (c *Container) generateResolvConf() (string, error) { } ipv6 := false - // Check if CNI gave back and DNS servers for us to add in - cniResponse := c.state.NetworkStatus - for _, i := range cniResponse { - for _, ip := range i.IPs { - // Note: only using To16() does not work since it also returns a valid ip for ipv4 - if ip.Address.IP.To4() == nil && ip.Address.IP.To16() != nil { - ipv6 = true + // If network status is set check for ipv6 and dns namesevers + netStatus := c.getNetworkStatus() + for _, status := range netStatus { + for _, netInt := range status.Interfaces { + for _, netAddress := range netInt.Networks { + // Note: only using To16() does not work since it also returns a valid ip for ipv4 + if netAddress.Subnet.IP.To4() == nil && netAddress.Subnet.IP.To16() != nil { + ipv6 = true + } } } - if i.DNS.Nameservers != nil { - cniNameServers = append(cniNameServers, i.DNS.Nameservers...) - logrus.Debugf("adding nameserver(s) from cni response of '%q'", i.DNS.Nameservers) + + if status.DNSServerIPs != nil { + for _, nsIP := range status.DNSServerIPs { + networkNameServers = append(networkNameServers, nsIP.String()) + } + logrus.Debugf("adding nameserver(s) from network status of '%q'", status.DNSServerIPs) } - if i.DNS.Search != nil { - cniSearchDomains = append(cniSearchDomains, i.DNS.Search...) - logrus.Debugf("adding search domain(s) from cni response of '%q'", i.DNS.Search) + if status.DNSSearchDomains != nil { + networkSearchDomains = append(networkSearchDomains, status.DNSSearchDomains...) + logrus.Debugf("adding search domain(s) from network status of '%q'", status.DNSSearchDomains) } } @@ -1882,8 +1914,8 @@ func (c *Container) generateResolvConf() (string, error) { for _, server := range dnsServers { nameservers = append(nameservers, server.String()) } - case len(cniNameServers) > 0: - nameservers = append(nameservers, cniNameServers...) + case len(networkNameServers) > 0: + nameservers = append(nameservers, networkNameServers...) default: // Make a new resolv.conf nameservers = resolvconf.GetNameservers(resolv.Content) @@ -1899,11 +1931,11 @@ func (c *Container) generateResolvConf() (string, error) { } var search []string - if len(c.config.DNSSearch) > 0 || len(c.runtime.config.Containers.DNSSearches) > 0 || len(cniSearchDomains) > 0 { + if len(c.config.DNSSearch) > 0 || len(c.runtime.config.Containers.DNSSearches) > 0 || len(networkSearchDomains) > 0 { if !util.StringInSlice(".", c.config.DNSSearch) { search = c.runtime.config.Containers.DNSSearches search = append(search, c.config.DNSSearch...) - search = append(search, cniSearchDomains...) + search = append(search, networkSearchDomains...) } } else { search = resolvconf.GetSearchDomains(resolv.Content) @@ -2019,20 +2051,22 @@ func (c *Container) getHosts() string { // Add gateway entry var depCtr *Container + netStatus := c.getNetworkStatus() if c.config.NetNsCtr != "" { // ignoring the error because there isn't anything to do depCtr, _ = c.getRootNetNsDepCtr() - } else if len(c.state.NetworkStatus) != 0 { + } else if len(netStatus) != 0 { depCtr = c - } else { - depCtr = nil } if depCtr != nil { - for _, pluginResultsRaw := range depCtr.state.NetworkStatus { - pluginResult, _ := cnitypes.GetResult(pluginResultsRaw) - for _, ip := range pluginResult.IPs { - hosts += fmt.Sprintf("%s host.containers.internal\n", ip.Gateway) + for _, status := range depCtr.getNetworkStatus() { + for _, netInt := range status.Interfaces { + for _, netAddress := range netInt.Networks { + if netAddress.Gateway != nil { + hosts += fmt.Sprintf("%s host.containers.internal\n", netAddress.Gateway.String()) + } + } } } } else if c.config.NetMode.IsSlirp4netns() { diff --git a/libpod/info.go b/libpod/info.go index 7b60ee46f..36dc8bc2a 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -18,7 +18,6 @@ import ( "github.com/containers/image/v5/pkg/sysregistriesv2" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/linkmode" - "github.com/containers/podman/v3/libpod/network" "github.com/containers/podman/v3/pkg/cgroups" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/storage" @@ -73,8 +72,7 @@ func (r *Runtime) info() (*define.Info, error) { volumePlugins = append(volumePlugins, plugin) } info.Plugins.Volume = volumePlugins - // TODO move this into the new network interface - info.Plugins.Network = []string{network.BridgeNetworkDriver, network.MacVLANNetworkDriver} + info.Plugins.Network = r.network.Drivers() info.Plugins.Log = logDrivers info.Registries = registries diff --git a/libpod/kube.go b/libpod/kube.go index 812bb101b..54e8a7c50 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -10,11 +10,11 @@ import ( "time" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/lookup" "github.com/containers/podman/v3/pkg/namespaces" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/util" - "github.com/cri-o/ocicni/pkg/ocicni" "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" @@ -544,7 +544,7 @@ func containerToV1Container(c *Container) (v1.Container, []v1.Volume, *v1.PodDNS // ocicniPortMappingToContainerPort takes an ocicni portmapping and converts // it to a v1.ContainerPort format for kube output -func ocicniPortMappingToContainerPort(portMappings []ocicni.PortMapping) ([]v1.ContainerPort, error) { +func ocicniPortMappingToContainerPort(portMappings []types.OCICNIPortMapping) ([]v1.ContainerPort, error) { containerPorts := make([]v1.ContainerPort, 0, len(portMappings)) for _, p := range portMappings { var protocol v1.Protocol diff --git a/libpod/network/cni/cni_conversion.go b/libpod/network/cni/cni_conversion.go index 09943948b..060794ebe 100644 --- a/libpod/network/cni/cni_conversion.go +++ b/libpod/network/cni/cni_conversion.go @@ -185,6 +185,9 @@ func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath s.LeaseRange.StartIP = rangeStart s.LeaseRange.EndIP = rangeEnd } + if util.IsIPv6(s.Subnet.IP) { + network.IPv6Enabled = true + } network.Subnets = append(network.Subnets, s) } } diff --git a/libpod/network/cni/config.go b/libpod/network/cni/config.go index ee203f80d..d31cd3002 100644 --- a/libpod/network/cni/config.go +++ b/libpod/network/cni/config.go @@ -24,7 +24,7 @@ func (n *cniNetwork) NetworkCreate(net types.Network) (types.Network, error) { if err != nil { return types.Network{}, err } - network, err := n.networkCreate(net, true) + network, err := n.networkCreate(net, false) if err != nil { return types.Network{}, err } @@ -33,89 +33,106 @@ func (n *cniNetwork) NetworkCreate(net types.Network) (types.Network, error) { return *network.libpodNet, nil } -func (n *cniNetwork) networkCreate(net types.Network, writeToDisk bool) (*network, error) { +// networkCreate will fill out the given network struct and return the new network entry. +// If defaultNet is true it will not validate against used subnets and it will not write the cni config to disk. +func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (*network, error) { // if no driver is set use the default one - if net.Driver == "" { - net.Driver = types.DefaultNetworkDriver + if newNetwork.Driver == "" { + newNetwork.Driver = types.DefaultNetworkDriver } // FIXME: Should we use a different type for network create without the ID field? // the caller is not allowed to set a specific ID - if net.ID != "" { + if newNetwork.ID != "" { return nil, errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create") } - if net.Labels == nil { - net.Labels = map[string]string{} + if newNetwork.Labels == nil { + newNetwork.Labels = map[string]string{} } - if net.Options == nil { - net.Options = map[string]string{} + if newNetwork.Options == nil { + newNetwork.Options = map[string]string{} } - if net.IPAMOptions == nil { - net.IPAMOptions = map[string]string{} + if newNetwork.IPAMOptions == nil { + newNetwork.IPAMOptions = map[string]string{} } var name string var err error // validate the name when given - if net.Name != "" { - if !define.NameRegex.MatchString(net.Name) { - return nil, errors.Wrapf(define.RegexError, "network name %s invalid", net.Name) + if newNetwork.Name != "" { + if !define.NameRegex.MatchString(newNetwork.Name) { + return nil, errors.Wrapf(define.RegexError, "network name %s invalid", newNetwork.Name) } - if _, ok := n.networks[net.Name]; ok { - return nil, errors.Wrapf(define.ErrNetworkExists, "network name %s already used", net.Name) + if _, ok := n.networks[newNetwork.Name]; ok { + return nil, errors.Wrapf(define.ErrNetworkExists, "network name %s already used", newNetwork.Name) } } else { name, err = n.getFreeDeviceName() if err != nil { return nil, err } - net.Name = name + newNetwork.Name = name } - switch net.Driver { + // Only get the used networks for validation if we do not create the default network. + // The default network should not be validated against used subnets, we have to ensure + // that this network can always be created even when a subnet is already used on the host. + // This could happen if you run a container on this net, then the cni interface will be + // created on the host and "block" this subnet from being used again. + // Therefore the next podman command tries to create the default net again and it would + // fail because it thinks the network is used on the host. + var usedNetworks []*net.IPNet + if !defaultNet { + usedNetworks, err = n.getUsedSubnets() + if err != nil { + return nil, err + } + } + + switch newNetwork.Driver { case types.BridgeNetworkDriver: // if the name was created with getFreeDeviceName set the interface to it as well - if name != "" && net.NetworkInterface == "" { - net.NetworkInterface = name + if name != "" && newNetwork.NetworkInterface == "" { + newNetwork.NetworkInterface = name } - err = n.createBridge(&net) + err = n.createBridge(&newNetwork, usedNetworks) if err != nil { return nil, err } case types.MacVLANNetworkDriver: - err = createMacVLAN(&net) + err = createMacVLAN(&newNetwork) if err != nil { return nil, err } default: - return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", net.Driver) + return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", newNetwork.Driver) } - for i := range net.Subnets { - err := validateSubnet(&net.Subnets[i], !net.Internal) + for i := range newNetwork.Subnets { + err := validateSubnet(&newNetwork.Subnets[i], !newNetwork.Internal, usedNetworks) if err != nil { return nil, err } - if util.IsIPv6(net.Subnets[i].Subnet.IP) { - net.IPv6Enabled = true + if util.IsIPv6(newNetwork.Subnets[i].Subnet.IP) { + newNetwork.IPv6Enabled = true } } // generate the network ID - net.ID = getNetworkIDFromName(net.Name) + newNetwork.ID = getNetworkIDFromName(newNetwork.Name) // FIXME: Should this be a hard error? - if net.DNSEnabled && net.Internal && hasDNSNamePlugin(n.cniPluginDirs) { - logrus.Warnf("dnsname and internal networks are incompatible. dnsname plugin not configured for network %s", net.Name) - net.DNSEnabled = false + if newNetwork.DNSEnabled && newNetwork.Internal && hasDNSNamePlugin(n.cniPluginDirs) { + logrus.Warnf("dnsname and internal networks are incompatible. dnsname plugin not configured for network %s", newNetwork.Name) + newNetwork.DNSEnabled = false } - cniConf, path, err := n.createCNIConfigListFromNetwork(&net, writeToDisk) + cniConf, path, err := n.createCNIConfigListFromNetwork(&newNetwork, !defaultNet) if err != nil { return nil, err } - return &network{cniNet: cniConf, libpodNet: &net, filename: path}, nil + return &network{cniNet: cniConf, libpodNet: &newNetwork, filename: path}, nil } // NetworkRemove will remove the Network with the given name or ID. @@ -218,7 +235,7 @@ func createMacVLAN(network *types.Network) error { return nil } -func (n *cniNetwork) createBridge(network *types.Network) error { +func (n *cniNetwork) createBridge(network *types.Network, usedNetworks []*net.IPNet) error { if network.NetworkInterface != "" { bridges := n.getBridgeInterfaceNames() if pkgutil.StringInSlice(network.NetworkInterface, bridges) { @@ -236,7 +253,7 @@ func (n *cniNetwork) createBridge(network *types.Network) error { } if len(network.Subnets) == 0 { - freeSubnet, err := n.getFreeIPv4NetworkSubnet() + freeSubnet, err := n.getFreeIPv4NetworkSubnet(usedNetworks) if err != nil { return err } @@ -256,14 +273,14 @@ func (n *cniNetwork) createBridge(network *types.Network) error { } } if !ipv4 { - freeSubnet, err := n.getFreeIPv4NetworkSubnet() + freeSubnet, err := n.getFreeIPv4NetworkSubnet(usedNetworks) if err != nil { return err } network.Subnets = append(network.Subnets, *freeSubnet) } if !ipv6 { - freeSubnet, err := n.getFreeIPv6NetworkSubnet() + freeSubnet, err := n.getFreeIPv6NetworkSubnet(usedNetworks) if err != nil { return err } @@ -278,10 +295,14 @@ func (n *cniNetwork) createBridge(network *types.Network) error { // given gateway and lease range are part of this subnet. If the // gateway is empty and addGateway is true it will get the first // available ip in the subnet assigned. -func validateSubnet(s *types.Subnet, addGateway bool) error { +func validateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet) error { if s == nil { return errors.New("subnet is nil") } + if s.Subnet.IP == nil { + return errors.New("subnet ip is nil") + } + // Reparse to ensure subnet is valid. // Do not use types.ParseCIDR() because we want the ip to be // the network address and not a random ip in the subnet. @@ -289,6 +310,12 @@ func validateSubnet(s *types.Subnet, addGateway bool) error { if err != nil { return errors.Wrap(err, "subnet invalid") } + + // check that the new subnet does not conflict with existing ones + if util.NetworkIntersectsWithNetworks(net, usedNetworks) { + return errors.Errorf("subnet %s is already used on the host or by another config", net.String()) + } + s.Subnet = types.IPNet{IPNet: *net} if s.Gateway != nil { if !s.Subnet.Contains(s.Gateway) { diff --git a/libpod/network/cni/config_test.go b/libpod/network/cni/config_test.go index f67402657..11ad71870 100644 --- a/libpod/network/cni/config_test.go +++ b/libpod/network/cni/config_test.go @@ -313,6 +313,14 @@ var _ = Describe("Config", func() { Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) Expect(network1.Subnets[0].Gateway.String()).To(Equal("fdcc::1")) Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + + // reload configs from disk + libpodNet, err = getNetworkInterface(cniConfDir, false) + Expect(err).To(BeNil()) + // check the the networks are identical + network2, err := libpodNet.NetworkInspect(network1.Name) + Expect(err).To(BeNil()) + Expect(network1).To(Equal(network2)) }) It("create bridge with ipv6 enabled", func() { @@ -508,6 +516,9 @@ var _ = Describe("Config", func() { Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) Expect(network1.Subnets[0].LeaseRange.StartIP.String()).To(Equal(startIP)) + err = libpodNet.NetworkRemove(network1.Name) + Expect(err).To(BeNil()) + endIP := "10.0.0.10" network = types.Network{ Driver: "bridge", @@ -529,6 +540,9 @@ var _ = Describe("Config", func() { Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) Expect(network1.Subnets[0].LeaseRange.EndIP.String()).To(Equal(endIP)) + err = libpodNet.NetworkRemove(network1.Name) + Expect(err).To(BeNil()) + network = types.Network{ Driver: "bridge", Subnets: []types.Subnet{ @@ -590,7 +604,7 @@ var _ = Describe("Config", func() { } _, err := libpodNet.NetworkCreate(network) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("subnet invalid")) + Expect(err.Error()).To(ContainSubstring("subnet ip is nil")) }) It("create network with name", func() { @@ -886,6 +900,25 @@ var _ = Describe("Config", func() { Expect(err.Error()).To(Equal("default network podman cannot be removed")) }) + It("network create with same subnet", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + subnet2 := "10.10.0.0/24" + n2, _ := types.ParseCIDR(subnet2) + network := types.Network{Subnets: []types.Subnet{{Subnet: n}, {Subnet: n2}}} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Subnets).To(HaveLen(2)) + network = types.Network{Subnets: []types.Subnet{{Subnet: n}}} + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("subnet 10.0.0.0/24 is already used on the host or by another config")) + network = types.Network{Subnets: []types.Subnet{{Subnet: n2}}} + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("subnet 10.10.0.0/24 is already used on the host or by another config")) + }) + }) Context("network load valid existing ones", func() { diff --git a/libpod/network/cni/network.go b/libpod/network/cni/network.go index fde08a0c6..46e07f780 100644 --- a/libpod/network/cni/network.go +++ b/libpod/network/cni/network.go @@ -106,6 +106,12 @@ func NewCNINetworkInterface(conf InitConfig) (types.ContainerNetwork, error) { return n, nil } +// Drivers will return the list of supported network drivers +// for this interface. +func (n *cniNetwork) Drivers() []string { + return []string{types.BridgeNetworkDriver, types.MacVLANNetworkDriver} +} + func (n *cniNetwork) loadNetworks() error { // skip loading networks if they are already loaded if n.networks != nil { @@ -179,7 +185,7 @@ func (n *cniNetwork) createDefaultNetwork() (*network, error) { {Subnet: n.defaultSubnet}, }, } - return n.networkCreate(net, false) + return n.networkCreate(net, true) } // getNetwork will lookup a network by name or ID. It returns an @@ -221,12 +227,7 @@ func getNetworkIDFromName(name string) string { } // getFreeIPv6NetworkSubnet returns a unused ipv4 subnet -func (n *cniNetwork) getFreeIPv4NetworkSubnet() (*types.Subnet, error) { - networks, err := n.getUsedSubnets() - if err != nil { - return nil, err - } - +func (n *cniNetwork) getFreeIPv4NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { // the default podman network is 10.88.0.0/16 // start locking for free /24 networks network := &net.IPNet{ @@ -236,12 +237,13 @@ func (n *cniNetwork) getFreeIPv4NetworkSubnet() (*types.Subnet, error) { // TODO: make sure to not use public subnets for { - if intersectsConfig := util.NetworkIntersectsWithNetworks(network, networks); !intersectsConfig { + if intersectsConfig := util.NetworkIntersectsWithNetworks(network, usedNetworks); !intersectsConfig { logrus.Debugf("found free ipv4 network subnet %s", network.String()) return &types.Subnet{ Subnet: types.IPNet{IPNet: *network}, }, nil } + var err error network, err = util.NextSubnet(network) if err != nil { return nil, err @@ -250,12 +252,7 @@ func (n *cniNetwork) getFreeIPv4NetworkSubnet() (*types.Subnet, error) { } // getFreeIPv6NetworkSubnet returns a unused ipv6 subnet -func (n *cniNetwork) getFreeIPv6NetworkSubnet() (*types.Subnet, error) { - networks, err := n.getUsedSubnets() - if err != nil { - return nil, err - } - +func (n *cniNetwork) getFreeIPv6NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { // FIXME: Is 10000 fine as limit? We should prevent an endless loop. for i := 0; i < 10000; i++ { // RFC4193: Choose the ipv6 subnet random and NOT sequentially. @@ -263,7 +260,7 @@ func (n *cniNetwork) getFreeIPv6NetworkSubnet() (*types.Subnet, error) { if err != nil { return nil, err } - if intersectsConfig := util.NetworkIntersectsWithNetworks(&network, networks); !intersectsConfig { + if intersectsConfig := util.NetworkIntersectsWithNetworks(&network, usedNetworks); !intersectsConfig { logrus.Debugf("found free ipv6 network subnet %s", network.String()) return &types.Subnet{ Subnet: types.IPNet{IPNet: network}, @@ -279,9 +276,8 @@ func (n *cniNetwork) getUsedSubnets() ([]*net.IPNet, error) { // first, load all used subnets from network configs subnets := make([]*net.IPNet, 0, len(n.networks)) for _, val := range n.networks { - for _, subnet := range val.libpodNet.Subnets { - // nolint:exportloopref - subnets = append(subnets, &subnet.Subnet.IPNet) + for i := range val.libpodNet.Subnets { + subnets = append(subnets, &val.libpodNet.Subnets[i].Subnet.IPNet) } } // second, load networks from the current system diff --git a/libpod/network/cni/run.go b/libpod/network/cni/run.go index 14634262c..b69953c4b 100644 --- a/libpod/network/cni/run.go +++ b/libpod/network/cni/run.go @@ -114,7 +114,7 @@ func (n *cniNetwork) Setup(namespacePath string, options types.SetupOptions) (ma } logrus.Debugf("cni result for container %s network %s: %v", options.ContainerID, name, cnires) var status types.StatusBlock - status, retErr = cniResultToStatus(cnires) + status, retErr = CNIResultToStatus(cnires) if retErr != nil { return nil, retErr } @@ -123,8 +123,9 @@ func (n *cniNetwork) Setup(namespacePath string, options types.SetupOptions) (ma return results, nil } -// cniResultToStatus convert the cni result to status block -func cniResultToStatus(cniResult *current.Result) (types.StatusBlock, error) { +// CNIResultToStatus convert the cni result to status block +// nolint:golint +func CNIResultToStatus(cniResult *current.Result) (types.StatusBlock, error) { result := types.StatusBlock{} nameservers := make([]net.IP, 0, len(cniResult.DNS.Nameservers)) for _, nameserver := range cniResult.DNS.Nameservers { diff --git a/libpod/network/cni/run_test.go b/libpod/network/cni/run_test.go index 32e88ca61..f6da22a76 100644 --- a/libpod/network/cni/run_test.go +++ b/libpod/network/cni/run_test.go @@ -140,6 +140,10 @@ var _ = Describe("run CNI", func() { Expect(res[defNet].DNSServerIPs).To(BeEmpty()) Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) + // reload the interface so the networks are reload from disk + libpodNet, err := getNetworkInterface(cniConfDir, false) + Expect(err).To(BeNil()) + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) Expect(err).To(BeNil()) }) diff --git a/libpod/network/config.go b/libpod/network/config.go deleted file mode 100644 index 9a3bc4763..000000000 --- a/libpod/network/config.go +++ /dev/null @@ -1,159 +0,0 @@ -package network - -import ( - "encoding/json" - "net" - - "github.com/containers/storage/pkg/lockfile" -) - -// TODO once the containers.conf file stuff is worked out, this should be modified -// to honor defines in the containers.conf as well as overrides? - -const ( - // CNIConfigDir is the path where CNI config files exist - CNIConfigDir = "/etc/cni/net.d" - // CNIDeviceName is the default network device name and in - // reality should have an int appended to it (cni-podman4) - CNIDeviceName = "cni-podman" - // DefaultPodmanDomainName is used for the dnsname plugin to define - // a localized domain name for a created network - DefaultPodmanDomainName = "dns.podman" - // LockFileName is used for obtaining a lock and is appended - // to libpod's tmpdir in practice - LockFileName = "cni.lock" -) - -// CNILock is for preventing name collision and -// unpredictable results when doing some CNI operations. -type CNILock struct { - lockfile.Locker -} - -// GetDefaultPodmanNetwork outputs the default network for podman -func GetDefaultPodmanNetwork() (*net.IPNet, error) { - _, n, err := net.ParseCIDR("10.88.1.0/24") - return n, err -} - -// CNIPlugins is a way of marshalling a CNI network configuration to disk -type CNIPlugins interface { - Bytes() ([]byte, error) -} - -// HostLocalBridge describes a configuration for a bridge plugin -// https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge#network-configuration-reference -type HostLocalBridge struct { - PluginType string `json:"type"` - BrName string `json:"bridge,omitempty"` - IsGW bool `json:"isGateway"` - IsDefaultGW bool `json:"isDefaultGateway,omitempty"` - ForceAddress bool `json:"forceAddress,omitempty"` - IPMasq bool `json:"ipMasq,omitempty"` - MTU int `json:"mtu,omitempty"` - HairpinMode bool `json:"hairpinMode,omitempty"` - PromiscMode bool `json:"promiscMode,omitempty"` - Vlan int `json:"vlan,omitempty"` - IPAM IPAMConfig `json:"ipam"` -} - -// Bytes outputs []byte -func (h *HostLocalBridge) Bytes() ([]byte, error) { - return json.MarshalIndent(h, "", "\t") -} - -// IPAMConfig describes an IPAM configuration -// https://github.com/containernetworking/plugins/tree/master/plugins/ipam/host-local#network-configuration-reference -type IPAMConfig struct { - PluginType string `json:"type"` - Routes []IPAMRoute `json:"routes,omitempty"` - ResolveConf string `json:"resolveConf,omitempty"` - DataDir string `json:"dataDir,omitempty"` - Ranges [][]IPAMLocalHostRangeConf `json:"ranges,omitempty"` -} - -// IPAMLocalHostRangeConf describes the new style IPAM ranges -type IPAMLocalHostRangeConf struct { - Subnet string `json:"subnet"` - RangeStart string `json:"rangeStart,omitempty"` - RangeEnd string `json:"rangeEnd,omitempty"` - Gateway string `json:"gateway,omitempty"` -} - -// Bytes outputs the configuration as []byte -func (i IPAMConfig) Bytes() ([]byte, error) { - return json.MarshalIndent(i, "", "\t") -} - -// IPAMRoute describes a route in an ipam config -type IPAMRoute struct { - Dest string `json:"dst"` -} - -// PortMapConfig describes the default portmapping config -type PortMapConfig struct { - PluginType string `json:"type"` - Capabilities map[string]bool `json:"capabilities"` -} - -// Bytes outputs the configuration as []byte -func (p PortMapConfig) Bytes() ([]byte, error) { - return json.MarshalIndent(p, "", "\t") -} - -// MacVLANConfig describes the macvlan config -type MacVLANConfig struct { - PluginType string `json:"type"` - Master string `json:"master"` - IPAM IPAMConfig `json:"ipam"` - MTU int `json:"mtu,omitempty"` -} - -// Bytes outputs the configuration as []byte -func (p MacVLANConfig) Bytes() ([]byte, error) { - return json.MarshalIndent(p, "", "\t") -} - -// FirewallConfig describes the firewall plugin -type FirewallConfig struct { - PluginType string `json:"type"` - Backend string `json:"backend"` -} - -// Bytes outputs the configuration as []byte -func (f FirewallConfig) Bytes() ([]byte, error) { - return json.MarshalIndent(f, "", "\t") -} - -// TuningConfig describes the tuning plugin -type TuningConfig struct { - PluginType string `json:"type"` -} - -// Bytes outputs the configuration as []byte -func (f TuningConfig) Bytes() ([]byte, error) { - return json.MarshalIndent(f, "", "\t") -} - -// DNSNameConfig describes the dns container name resolution plugin config -type DNSNameConfig struct { - PluginType string `json:"type"` - DomainName string `json:"domainName"` - Capabilities map[string]bool `json:"capabilities"` -} - -// PodmanMachineConfig enables port handling on the host OS -type PodmanMachineConfig struct { - PluginType string `json:"type"` - Capabilities map[string]bool `json:"capabilities"` -} - -// Bytes outputs the configuration as []byte -func (d DNSNameConfig) Bytes() ([]byte, error) { - return json.MarshalIndent(d, "", "\t") -} - -// Bytes outputs the configuration as []byte -func (p PodmanMachineConfig) Bytes() ([]byte, error) { - return json.MarshalIndent(p, "", "\t") -} diff --git a/libpod/network/create.go b/libpod/network/create.go deleted file mode 100644 index aca8150b5..000000000 --- a/libpod/network/create.go +++ /dev/null @@ -1,310 +0,0 @@ -package network - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strconv" - - "github.com/containernetworking/cni/pkg/version" - "github.com/containers/common/pkg/config" - "github.com/containers/podman/v3/pkg/domain/entities" - "github.com/containers/podman/v3/pkg/util" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// Create the CNI network -func Create(name string, options entities.NetworkCreateOptions, runtimeConfig *config.Config) (*entities.NetworkCreateReport, error) { - var fileName string - if err := isSupportedDriver(options.Driver); err != nil { - return nil, err - } - // Acquire a lock for CNI - l, err := acquireCNILock(runtimeConfig) - if err != nil { - return nil, err - } - defer l.releaseCNILock() - if len(options.MacVLAN) > 0 || options.Driver == MacVLANNetworkDriver { - fileName, err = createMacVLAN(name, options, runtimeConfig) - } else { - fileName, err = createBridge(name, options, runtimeConfig) - } - if err != nil { - return nil, err - } - return &entities.NetworkCreateReport{Filename: fileName}, nil -} - -// validateBridgeOptions validate the bridge networking options -func validateBridgeOptions(options entities.NetworkCreateOptions) error { - subnet := &options.Subnet - ipRange := &options.Range - gateway := options.Gateway - // if IPv6 is set an IPv6 subnet MUST be specified - if options.IPv6 && ((subnet.IP == nil) || (subnet.IP != nil && !IsIPv6(subnet.IP))) { - return errors.Errorf("ipv6 option requires an IPv6 --subnet to be provided") - } - // range and gateway depend on subnet - if subnet.IP == nil && (ipRange.IP != nil || gateway != nil) { - return errors.Errorf("every ip-range or gateway must have a corresponding subnet") - } - - // if a range is given, we need to ensure it is "in" the network range. - if ipRange.IP != nil { - firstIP, err := FirstIPInSubnet(ipRange) - if err != nil { - return errors.Wrapf(err, "failed to get first IP address from ip-range") - } - lastIP, err := LastIPInSubnet(ipRange) - if err != nil { - return errors.Wrapf(err, "failed to get last IP address from ip-range") - } - if !subnet.Contains(firstIP) || !subnet.Contains(lastIP) { - return errors.Errorf("the ip range %s does not fall within the subnet range %s", ipRange.String(), subnet.String()) - } - } - - // if network is provided and if gateway is provided, make sure it is "in" network - if gateway != nil && !subnet.Contains(gateway) { - return errors.Errorf("gateway %s is not in valid for subnet %s", gateway.String(), subnet.String()) - } - - return nil -} - -// parseMTU parses the mtu option -func parseMTU(mtu string) (int, error) { - if mtu == "" { - return 0, nil // default - } - m, err := strconv.Atoi(mtu) - if err != nil { - return 0, err - } - if m < 0 { - return 0, errors.Errorf("the value %d for mtu is less than zero", m) - } - return m, nil -} - -// parseVlan parses the vlan option -func parseVlan(vlan string) (int, error) { - if vlan == "" { - return 0, nil // default - } - return strconv.Atoi(vlan) -} - -// createBridge creates a CNI network -func createBridge(name string, options entities.NetworkCreateOptions, runtimeConfig *config.Config) (string, error) { - var ( - ipamRanges [][]IPAMLocalHostRangeConf - err error - routes []IPAMRoute - ) - isGateway := true - ipMasq := true - - // validate options - if err := validateBridgeOptions(options); err != nil { - return "", err - } - - // For compatibility with the docker implementation: - // if IPv6 is enabled (it really means dual-stack) then an IPv6 subnet has to be provided, and one free network is allocated for IPv4 - // if IPv6 is not specified the subnet may be specified and can be either IPv4 or IPv6 (podman, unlike docker, allows IPv6 only networks) - // If not subnet is specified an IPv4 subnet will be allocated - subnet := &options.Subnet - ipRange := &options.Range - gateway := options.Gateway - if subnet.IP != nil { - // if network is provided, does it conflict with existing CNI or live networks - err = ValidateUserNetworkIsAvailable(runtimeConfig, subnet) - if err != nil { - return "", err - } - // obtain CNI subnet default route - defaultRoute, err := NewIPAMDefaultRoute(IsIPv6(subnet.IP)) - if err != nil { - return "", err - } - routes = append(routes, defaultRoute) - // obtain CNI range - ipamRange, err := NewIPAMLocalHostRange(subnet, ipRange, gateway) - if err != nil { - return "", err - } - ipamRanges = append(ipamRanges, ipamRange) - } - // if no network is provided or IPv6 flag used, figure out the IPv4 network - if options.IPv6 || len(routes) == 0 { - subnetV4, err := GetFreeNetwork(runtimeConfig) - if err != nil { - return "", err - } - // obtain IPv4 default route - defaultRoute, err := NewIPAMDefaultRoute(false) - if err != nil { - return "", err - } - routes = append(routes, defaultRoute) - // the CNI bridge plugin does not need to set - // the range or gateway options explicitly - ipamRange, err := NewIPAMLocalHostRange(subnetV4, nil, nil) - if err != nil { - return "", err - } - ipamRanges = append(ipamRanges, ipamRange) - } - - // create CNI config - ipamConfig, err := NewIPAMHostLocalConf(routes, ipamRanges) - if err != nil { - return "", err - } - - if options.Internal { - isGateway = false - ipMasq = false - } - - var mtu int - var vlan int - for k, v := range options.Options { - var err error - switch k { - case "mtu": - mtu, err = parseMTU(v) - if err != nil { - return "", err - } - - case "vlan": - vlan, err = parseVlan(v) - if err != nil { - return "", err - } - - default: - return "", errors.Errorf("unsupported option %s", k) - } - } - - // obtain host bridge name - bridgeDeviceName, err := GetFreeDeviceName(runtimeConfig) - if err != nil { - return "", err - } - - if len(name) > 0 { - netNames, err := GetNetworkNamesFromFileSystem(runtimeConfig) - if err != nil { - return "", err - } - if util.StringInSlice(name, netNames) { - return "", errors.Errorf("the network name %s is already used", name) - } - } else { - // If no name is given, we give the name of the bridge device - name = bridgeDeviceName - } - - // create CNI plugin configuration - ncList := NewNcList(name, version.Current(), options.Labels) - var plugins []CNIPlugins - // TODO need to iron out the role of isDefaultGW and IPMasq - bridge := NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, mtu, vlan, ipamConfig) - plugins = append(plugins, bridge) - plugins = append(plugins, NewPortMapPlugin()) - plugins = append(plugins, NewFirewallPlugin()) - plugins = append(plugins, NewTuningPlugin()) - // if we find the dnsname plugin we add configuration for it - if HasDNSNamePlugin(runtimeConfig.Network.CNIPluginDirs) && !options.DisableDNS { - if options.Internal { - logrus.Warnf("dnsname and --internal networks are incompatible. dnsname plugin not configured for network %s", name) - } else { - // Note: in the future we might like to allow for dynamic domain names - plugins = append(plugins, NewDNSNamePlugin(DefaultPodmanDomainName)) - } - } - // Add the podman-machine CNI plugin if we are in a machine - if runtimeConfig.MachineEnabled() { // check if we are in a machine vm - plugins = append(plugins, NewPodmanMachinePlugin()) - } - ncList["plugins"] = plugins - b, err := json.MarshalIndent(ncList, "", " ") - if err != nil { - return "", err - } - if err := os.MkdirAll(GetCNIConfDir(runtimeConfig), 0755); err != nil { - return "", err - } - cniPathName := filepath.Join(GetCNIConfDir(runtimeConfig), fmt.Sprintf("%s.conflist", name)) - err = ioutil.WriteFile(cniPathName, b, 0644) - return cniPathName, err -} - -func createMacVLAN(name string, options entities.NetworkCreateOptions, runtimeConfig *config.Config) (string, error) { - var ( - mtu int - plugins []CNIPlugins - ) - liveNetNames, err := GetLiveNetworkNames() - if err != nil { - return "", err - } - - // The parent can be defined with --macvlan or as an option (-o parent:device) - parentNetworkDevice := options.MacVLAN - if len(parentNetworkDevice) < 1 { - if parent, ok := options.Options["parent"]; ok { - parentNetworkDevice = parent - } - } - - // Make sure the host-device exists if provided - if len(parentNetworkDevice) > 0 && !util.StringInSlice(parentNetworkDevice, liveNetNames) { - return "", errors.Errorf("failed to find network interface %q", parentNetworkDevice) - } - if len(name) > 0 { - netNames, err := GetNetworkNamesFromFileSystem(runtimeConfig) - if err != nil { - return "", err - } - if util.StringInSlice(name, netNames) { - return "", errors.Errorf("the network name %s is already used", name) - } - } else { - name, err = GetFreeDeviceName(runtimeConfig) - if err != nil { - return "", err - } - } - ncList := NewNcList(name, version.Current(), options.Labels) - if val, ok := options.Options["mtu"]; ok { - intVal, err := strconv.Atoi(val) - if err != nil { - return "", err - } - if intVal > 0 { - mtu = intVal - } - } - macvlan, err := NewMacVLANPlugin(parentNetworkDevice, options.Gateway, &options.Range, &options.Subnet, mtu) - if err != nil { - return "", err - } - plugins = append(plugins, macvlan) - ncList["plugins"] = plugins - b, err := json.MarshalIndent(ncList, "", " ") - if err != nil { - return "", err - } - cniPathName := filepath.Join(GetCNIConfDir(runtimeConfig), fmt.Sprintf("%s.conflist", name)) - err = ioutil.WriteFile(cniPathName, b, 0644) - return cniPathName, err -} diff --git a/libpod/network/create_test.go b/libpod/network/create_test.go deleted file mode 100644 index c3824bd91..000000000 --- a/libpod/network/create_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package network - -import ( - "net" - "testing" - - "github.com/containers/podman/v3/pkg/domain/entities" -) - -func Test_validateBridgeOptions(t *testing.T) { - tests := []struct { - name string - subnet net.IPNet - ipRange net.IPNet - gateway net.IP - isIPv6 bool - wantErr bool - }{ - { - name: "IPv4 subnet only", - subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, - }, - { - name: "IPv4 subnet and range", - subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, - ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, - }, - { - name: "IPv4 subnet and gateway", - subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, - gateway: net.ParseIP("192.168.0.10"), - }, - { - name: "IPv4 subnet, range and gateway", - subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, - ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, - gateway: net.ParseIP("192.168.0.10"), - }, - { - name: "IPv6 subnet only", - subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, - }, - { - name: "IPv6 subnet and range", - subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, - ipRange: net.IPNet{IP: net.ParseIP("2001:DB8:0:0:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, - isIPv6: true, - }, - { - name: "IPv6 subnet and gateway", - subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, - gateway: net.ParseIP("2001:DB8::2"), - isIPv6: true, - }, - { - name: "IPv6 subnet, range and gateway", - subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, - ipRange: net.IPNet{IP: net.ParseIP("2001:DB8:0:0:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, - gateway: net.ParseIP("2001:DB8::2"), - isIPv6: true, - }, - { - name: "IPv6 subnet, range and gateway without IPv6 option (PODMAN SUPPORTS IT UNLIKE DOCKER)", - subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, - ipRange: net.IPNet{IP: net.ParseIP("2001:DB8:0:0:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, - gateway: net.ParseIP("2001:DB8::2"), - isIPv6: false, - }, - { - name: "range provided but not subnet", - ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, - wantErr: true, - }, - { - name: "gateway provided but not subnet", - gateway: net.ParseIP("192.168.0.10"), - wantErr: true, - }, - { - name: "IPv4 subnet but IPv6 required", - subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, - ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, - gateway: net.ParseIP("192.168.0.10"), - isIPv6: true, - wantErr: true, - }, - { - name: "IPv6 required but IPv4 options used", - subnet: net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, - ipRange: net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, - gateway: net.ParseIP("192.168.0.10"), - isIPv6: true, - wantErr: true, - }, - { - name: "IPv6 required but not subnet provided", - isIPv6: true, - wantErr: true, - }, - { - name: "range out of the subnet", - subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, - ipRange: net.IPNet{IP: net.ParseIP("2001:1:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, - gateway: net.ParseIP("2001:DB8::2"), - isIPv6: true, - wantErr: true, - }, - { - name: "gateway out of the subnet", - subnet: net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, - gateway: net.ParseIP("2001::2"), - isIPv6: true, - wantErr: true, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - options := entities.NetworkCreateOptions{ - Subnet: tt.subnet, - Range: tt.ipRange, - Gateway: tt.gateway, - IPv6: tt.isIPv6, - } - if err := validateBridgeOptions(options); (err != nil) != tt.wantErr { - t.Errorf("validateBridgeOptions() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/libpod/network/devices.go b/libpod/network/devices.go deleted file mode 100644 index fc9aff337..000000000 --- a/libpod/network/devices.go +++ /dev/null @@ -1,59 +0,0 @@ -package network - -import ( - "fmt" - - "github.com/containers/common/pkg/config" - "github.com/containers/podman/v3/pkg/util" - "github.com/sirupsen/logrus" - "github.com/vishvananda/netlink" -) - -// GetFreeDeviceName returns a device name that is unused; used when no network -// name is provided by user -func GetFreeDeviceName(config *config.Config) (string, error) { - var ( - deviceNum uint - deviceName string - ) - networkNames, err := GetNetworkNamesFromFileSystem(config) - if err != nil { - return "", err - } - liveNetworksNames, err := GetLiveNetworkNames() - if err != nil { - return "", err - } - bridgeNames, err := GetBridgeNamesFromFileSystem(config) - if err != nil { - return "", err - } - for { - deviceName = fmt.Sprintf("%s%d", CNIDeviceName, deviceNum) - logrus.Debugf("checking if device name %q exists in other cni networks", deviceName) - if util.StringInSlice(deviceName, networkNames) { - deviceNum++ - continue - } - logrus.Debugf("checking if device name %q exists in live networks", deviceName) - if util.StringInSlice(deviceName, liveNetworksNames) { - deviceNum++ - continue - } - logrus.Debugf("checking if device name %q already exists as a bridge name ", deviceName) - if !util.StringInSlice(deviceName, bridgeNames) { - break - } - deviceNum++ - } - return deviceName, nil -} - -// RemoveInterface removes an interface by the given name -func RemoveInterface(interfaceName string) error { - link, err := netlink.LinkByName(interfaceName) - if err != nil { - return err - } - return netlink.LinkDel(link) -} diff --git a/libpod/network/files.go b/libpod/network/files.go deleted file mode 100644 index d876113f9..000000000 --- a/libpod/network/files.go +++ /dev/null @@ -1,211 +0,0 @@ -package network - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "sort" - "strings" - - "github.com/containernetworking/cni/libcni" - "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" - "github.com/containers/common/pkg/config" - "github.com/containers/podman/v3/libpod/define" - "github.com/containers/podman/v3/pkg/network" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// ErrNoSuchNetworkInterface indicates that no network interface exists -var ErrNoSuchNetworkInterface = errors.New("unable to find interface name for network") - -// GetCNIConfDir get CNI configuration directory -func GetCNIConfDir(configArg *config.Config) string { - if len(configArg.Network.NetworkConfigDir) < 1 { - dc, err := config.DefaultConfig() - if err != nil { - // Fallback to hard-coded dir - return CNIConfigDir - } - return dc.Network.NetworkConfigDir - } - return configArg.Network.NetworkConfigDir -} - -// LoadCNIConfsFromDir loads all the CNI configurations from a dir -func LoadCNIConfsFromDir(dir string) ([]*libcni.NetworkConfigList, error) { - files, err := libcni.ConfFiles(dir, []string{".conflist"}) - if err != nil { - return nil, err - } - sort.Strings(files) - - configs := make([]*libcni.NetworkConfigList, 0, len(files)) - for _, confFile := range files { - conf, err := libcni.ConfListFromFile(confFile) - if err != nil { - return nil, errors.Wrapf(err, "in %s", confFile) - } - configs = append(configs, conf) - } - return configs, nil -} - -// GetCNIConfigPathByNameOrID finds a CNI network by name and -// returns its configuration file path -func GetCNIConfigPathByNameOrID(config *config.Config, name string) (string, error) { - files, err := libcni.ConfFiles(GetCNIConfDir(config), []string{".conflist"}) - if err != nil { - return "", err - } - idMatch := 0 - file := "" - for _, confFile := range files { - conf, err := libcni.ConfListFromFile(confFile) - if err != nil { - return "", errors.Wrapf(err, "in %s", confFile) - } - if conf.Name == name { - return confFile, nil - } - if strings.HasPrefix(network.GetNetworkID(conf.Name), name) { - idMatch++ - file = confFile - } - } - if idMatch == 1 { - return file, nil - } - if idMatch > 1 { - return "", errors.Errorf("more than one result for network ID %s", name) - } - return "", errors.Wrap(define.ErrNoSuchNetwork, fmt.Sprintf("unable to find network configuration for %s", name)) -} - -// ReadRawCNIConfByNameOrID reads the raw CNI configuration for a CNI -// network by name -func ReadRawCNIConfByNameOrID(config *config.Config, name string) ([]byte, error) { - confFile, err := GetCNIConfigPathByNameOrID(config, name) - if err != nil { - return nil, err - } - b, err := ioutil.ReadFile(confFile) - return b, err -} - -// GetNetworkLabels returns a list of labels as a string -func GetNetworkLabels(list *libcni.NetworkConfigList) NcLabels { - cniJSON := make(map[string]interface{}) - err := json.Unmarshal(list.Bytes, &cniJSON) - if err != nil { - logrus.Errorf("failed to unmarshal network config %v %v", cniJSON["name"], err) - return nil - } - if args, ok := cniJSON["args"]; ok { - if key, ok := args.(map[string]interface{}); ok { - if labels, ok := key[PodmanLabelKey]; ok { - if labels, ok := labels.(map[string]interface{}); ok { - result := make(NcLabels, len(labels)) - for k, v := range labels { - if v, ok := v.(string); ok { - result[k] = v - } else { - logrus.Errorf("network config %v invalid label value type %T should be string", cniJSON["name"], labels) - } - } - return result - } - logrus.Errorf("network config %v invalid label type %T should be map[string]string", cniJSON["name"], labels) - } - } - } - return nil -} - -// GetNetworksFromFilesystem gets all the networks from the cni configuration -// files -func GetNetworksFromFilesystem(config *config.Config) ([]*allocator.Net, error) { - var cniNetworks []*allocator.Net - - networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config)) - if err != nil { - return nil, err - } - for _, n := range networks { - for _, cniplugin := range n.Plugins { - if cniplugin.Network.Type == "bridge" { - ipamConf := allocator.Net{} - if err := json.Unmarshal(cniplugin.Bytes, &ipamConf); err != nil { - return nil, err - } - cniNetworks = append(cniNetworks, &ipamConf) - break - } - } - } - return cniNetworks, nil -} - -// GetNetworkNamesFromFileSystem gets all the names from the cni network -// configuration files -func GetNetworkNamesFromFileSystem(config *config.Config) ([]string, error) { - networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config)) - if err != nil { - return nil, err - } - networkNames := []string{} - for _, n := range networks { - networkNames = append(networkNames, n.Name) - } - return networkNames, nil -} - -// GetInterfaceNameFromConfig returns the interface name for the bridge plugin -func GetInterfaceNameFromConfig(path string) (string, error) { - var name string - conf, err := libcni.ConfListFromFile(path) - if err != nil { - return "", err - } - for _, cniplugin := range conf.Plugins { - if cniplugin.Network.Type == "bridge" { - plugin := make(map[string]interface{}) - if err := json.Unmarshal(cniplugin.Bytes, &plugin); err != nil { - return "", err - } - name = plugin["bridge"].(string) - break - } - } - if len(name) == 0 { - return "", ErrNoSuchNetworkInterface - } - return name, nil -} - -// GetBridgeNamesFromFileSystem is a convenience function to get all the bridge -// names from the configured networks -func GetBridgeNamesFromFileSystem(config *config.Config) ([]string, error) { - networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config)) - if err != nil { - return nil, err - } - - bridgeNames := []string{} - for _, n := range networks { - var name string - // iterate network conflists - for _, cniplugin := range n.Plugins { - // iterate plugins - if cniplugin.Network.Type == "bridge" { - plugin := make(map[string]interface{}) - if err := json.Unmarshal(cniplugin.Bytes, &plugin); err != nil { - continue - } - name = plugin["bridge"].(string) - } - } - bridgeNames = append(bridgeNames, name) - } - return bridgeNames, nil -} diff --git a/libpod/network/ip.go b/libpod/network/ip.go deleted file mode 100644 index ba93a0d05..000000000 --- a/libpod/network/ip.go +++ /dev/null @@ -1,19 +0,0 @@ -package network - -import ( - "net" - - "github.com/containernetworking/plugins/pkg/ip" -) - -// CalcGatewayIP takes a network and returns the first IP in it. -func CalcGatewayIP(ipn *net.IPNet) net.IP { - // taken from cni bridge plugin as it is not exported - nid := ipn.IP.Mask(ipn.Mask) - return ip.NextIP(nid) -} - -// IsIPv6 returns if netIP is IPv6. -func IsIPv6(netIP net.IP) bool { - return netIP != nil && netIP.To4() == nil -} diff --git a/libpod/network/lock.go b/libpod/network/lock.go deleted file mode 100644 index 037f41efa..000000000 --- a/libpod/network/lock.go +++ /dev/null @@ -1,35 +0,0 @@ -package network - -import ( - "os" - "path/filepath" - - "github.com/containers/common/pkg/config" - "github.com/containers/storage" -) - -// acquireCNILock gets a lock that should be used in create and -// delete cases to avoid unwanted collisions in network names. -// TODO this uses a file lock and should be converted to shared memory -// when we have a more general shared memory lock in libpod -func acquireCNILock(config *config.Config) (*CNILock, error) { - cniDir := GetCNIConfDir(config) - err := os.MkdirAll(cniDir, 0755) - if err != nil { - return nil, err - } - l, err := storage.GetLockfile(filepath.Join(cniDir, LockFileName)) - if err != nil { - return nil, err - } - l.Lock() - cnilock := CNILock{ - Locker: l, - } - return &cnilock, nil -} - -// ReleaseCNILock unlocks the previously held lock -func (l *CNILock) releaseCNILock() { - l.Unlock() -} diff --git a/libpod/network/netconflist.go b/libpod/network/netconflist.go deleted file mode 100644 index d6c33740e..000000000 --- a/libpod/network/netconflist.go +++ /dev/null @@ -1,312 +0,0 @@ -package network - -import ( - "net" - "os" - "path/filepath" - "strings" - "syscall" - "time" - - "github.com/containernetworking/cni/libcni" - "github.com/containers/common/pkg/config" - "github.com/containers/podman/v3/pkg/network" - "github.com/containers/podman/v3/pkg/util" - "github.com/pkg/errors" -) - -const ( - defaultIPv4Route = "0.0.0.0/0" - defaultIPv6Route = "::/0" -) - -// NcList describes a generic map -type NcList map[string]interface{} - -// NcArgs describes the cni args field -type NcArgs map[string]NcLabels - -// NcLabels describes the label map -type NcLabels map[string]string - -// PodmanLabelKey key used to store the podman network label in a cni config -const PodmanLabelKey = "podman_labels" - -// NewNcList creates a generic map of values with string -// keys and adds in version and network name -func NewNcList(name, version string, labels NcLabels) NcList { - n := NcList{} - n["cniVersion"] = version - n["name"] = name - if len(labels) > 0 { - n["args"] = NcArgs{PodmanLabelKey: labels} - } - return n -} - -// NewHostLocalBridge creates a new LocalBridge for host-local -func NewHostLocalBridge(name string, isGateWay, isDefaultGW, ipMasq bool, mtu int, vlan int, ipamConf IPAMConfig) *HostLocalBridge { - hostLocalBridge := HostLocalBridge{ - PluginType: "bridge", - BrName: name, - IPMasq: ipMasq, - MTU: mtu, - HairpinMode: true, - Vlan: vlan, - IPAM: ipamConf, - } - if isGateWay { - hostLocalBridge.IsGW = true - } - if isDefaultGW { - hostLocalBridge.IsDefaultGW = true - } - return &hostLocalBridge -} - -// NewIPAMHostLocalConf creates a new IPAMHostLocal configuration -func NewIPAMHostLocalConf(routes []IPAMRoute, ipamRanges [][]IPAMLocalHostRangeConf) (IPAMConfig, error) { - ipamConf := IPAMConfig{ - PluginType: "host-local", - Routes: routes, - // Possible future support ? Leaving for clues - //ResolveConf: "", - //DataDir: "" - } - - ipamConf.Ranges = ipamRanges - return ipamConf, nil -} - -// NewIPAMLocalHostRange create a new IPAM range -func NewIPAMLocalHostRange(subnet *net.IPNet, ipRange *net.IPNet, gw net.IP) ([]IPAMLocalHostRangeConf, error) { //nolint:interfacer - var ranges []IPAMLocalHostRangeConf - hostRange := IPAMLocalHostRangeConf{ - Subnet: subnet.String(), - } - // an user provided a range, we add it here - if ipRange != nil && ipRange.IP != nil { - first, err := FirstIPInSubnet(ipRange) - if err != nil { - return nil, err - } - last, err := LastIPInSubnet(ipRange) - if err != nil { - return nil, err - } - hostRange.RangeStart = first.String() - hostRange.RangeEnd = last.String() - } - if gw != nil { - hostRange.Gateway = gw.String() - } else { - // Add first ip in subnet as gateway. It is not required - // by cni but should be included because of network inspect. - hostRange.Gateway = CalcGatewayIP(subnet).String() - } - ranges = append(ranges, hostRange) - return ranges, nil -} - -// NewIPAMRoute creates a new IPAM route configuration -func NewIPAMRoute(r *net.IPNet) IPAMRoute { //nolint:interfacer - return IPAMRoute{Dest: r.String()} -} - -// NewIPAMDefaultRoute creates a new IPAMDefault route of -// 0.0.0.0/0 for IPv4 or ::/0 for IPv6 -func NewIPAMDefaultRoute(isIPv6 bool) (IPAMRoute, error) { - route := defaultIPv4Route - if isIPv6 { - route = defaultIPv6Route - } - _, n, err := net.ParseCIDR(route) - if err != nil { - return IPAMRoute{}, err - } - return NewIPAMRoute(n), nil -} - -// NewPortMapPlugin creates a predefined, default portmapping -// configuration -func NewPortMapPlugin() PortMapConfig { - caps := make(map[string]bool) - caps["portMappings"] = true - p := PortMapConfig{ - PluginType: "portmap", - Capabilities: caps, - } - return p -} - -// NewFirewallPlugin creates a generic firewall plugin -func NewFirewallPlugin() FirewallConfig { - return FirewallConfig{ - PluginType: "firewall", - } -} - -// NewTuningPlugin creates a generic tuning section -func NewTuningPlugin() TuningConfig { - return TuningConfig{ - PluginType: "tuning", - } -} - -// NewDNSNamePlugin creates the dnsname config with a given -// domainname -func NewDNSNamePlugin(domainName string) DNSNameConfig { - caps := make(map[string]bool, 1) - caps["aliases"] = true - return DNSNameConfig{ - PluginType: "dnsname", - DomainName: domainName, - Capabilities: caps, - } -} - -// HasDNSNamePlugin looks to see if the dnsname cni plugin is present -func HasDNSNamePlugin(paths []string) bool { - for _, p := range paths { - if _, err := os.Stat(filepath.Join(p, "dnsname")); err == nil { - return true - } - } - return false -} - -// NewMacVLANPlugin creates a macvlanconfig with a given device name -func NewMacVLANPlugin(device string, gateway net.IP, ipRange *net.IPNet, subnet *net.IPNet, mtu int) (MacVLANConfig, error) { - i := IPAMConfig{PluginType: "dhcp"} - if gateway != nil || - (ipRange != nil && ipRange.IP != nil && ipRange.Mask != nil) || - (subnet != nil && subnet.IP != nil && subnet.Mask != nil) { - ipam, err := NewIPAMLocalHostRange(subnet, ipRange, gateway) - if err != nil { - return MacVLANConfig{}, err - } - ranges := make([][]IPAMLocalHostRangeConf, 0) - ranges = append(ranges, ipam) - i.Ranges = ranges - route, err := NewIPAMDefaultRoute(IsIPv6(subnet.IP)) - if err != nil { - return MacVLANConfig{}, err - } - i.Routes = []IPAMRoute{route} - i.PluginType = "host-local" - } - - m := MacVLANConfig{ - PluginType: "macvlan", - IPAM: i, - } - if mtu > 0 { - m.MTU = mtu - } - // CNI is supposed to use the default route if a - // parent device is not provided - if len(device) > 0 { - m.Master = device - } - return m, nil -} - -// IfPassesFilter filters NetworkListReport and returns true if the filter match the given config -func IfPassesFilter(netconf *libcni.NetworkConfigList, filters map[string][]string) (bool, error) { - result := true - for key, filterValues := range filters { - result = false - switch strings.ToLower(key) { - case "name": - // matches one name, regex allowed - result = util.StringMatchRegexSlice(netconf.Name, filterValues) - - case "plugin": - // match one plugin - plugins := network.GetCNIPlugins(netconf) - for _, val := range filterValues { - if strings.Contains(plugins, val) { - result = true - break - } - } - - case "label": - // matches all labels - result = util.MatchLabelFilters(filterValues, GetNetworkLabels(netconf)) - - case "driver": - // matches only for the DefaultNetworkDriver - for _, filterValue := range filterValues { - plugins := network.GetCNIPlugins(netconf) - if filterValue == DefaultNetworkDriver && - strings.Contains(plugins, DefaultNetworkDriver) { - result = true - } - } - - case "id": - // matches part of one id - for _, filterValue := range filterValues { - if strings.Contains(network.GetNetworkID(netconf.Name), filterValue) { - result = true - break - } - } - - // TODO: add dangling filter - - default: - return false, errors.Errorf("invalid filter %q", key) - } - } - return result, nil -} - -// IfPassesPruneFilter filters NetworkListReport and returns true if the prune filter match the given config -func IfPassesPruneFilter(config *config.Config, netconf *libcni.NetworkConfigList, f map[string][]string) (bool, error) { - for key, filterValues := range f { - switch strings.ToLower(key) { - case "label": - return util.MatchLabelFilters(filterValues, GetNetworkLabels(netconf)), nil - case "until": - until, err := util.ComputeUntilTimestamp(filterValues) - if err != nil { - return false, err - } - created, err := getCreatedTimestamp(config, netconf) - if err != nil { - return false, err - } - if created.Before(until) { - return true, nil - } - default: - return false, errors.Errorf("invalid filter %q", key) - } - } - return false, nil -} - -func getCreatedTimestamp(config *config.Config, netconf *libcni.NetworkConfigList) (*time.Time, error) { - networkConfigPath, err := GetCNIConfigPathByNameOrID(config, netconf.Name) - if err != nil { - return nil, err - } - f, err := os.Stat(networkConfigPath) - if err != nil { - return nil, err - } - stat := f.Sys().(*syscall.Stat_t) - created := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) // nolint: unconvert - return &created, nil -} - -func NewPodmanMachinePlugin() PodmanMachineConfig { - caps := make(map[string]bool, 1) - caps["portMappings"] = true - return PodmanMachineConfig{ - PluginType: "podman-machine", - Capabilities: caps, - } -} diff --git a/libpod/network/netconflist_test.go b/libpod/network/netconflist_test.go deleted file mode 100644 index 161764ed9..000000000 --- a/libpod/network/netconflist_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package network - -import ( - "net" - "reflect" - "testing" -) - -func TestNewIPAMDefaultRoute(t *testing.T) { - tests := []struct { - name string - isIPv6 bool - want IPAMRoute - }{ - { - name: "IPv4 default route", - isIPv6: false, - want: IPAMRoute{defaultIPv4Route}, - }, - { - name: "IPv6 default route", - isIPv6: true, - want: IPAMRoute{defaultIPv6Route}, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - got, err := NewIPAMDefaultRoute(tt.isIPv6) - if err != nil { - t.Errorf("no error expected: %v", err) - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("NewIPAMDefaultRoute() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestNewIPAMLocalHostRange(t *testing.T) { - tests := []struct { - name string - subnet *net.IPNet - ipRange *net.IPNet - gw net.IP - want []IPAMLocalHostRangeConf - }{ - { - name: "IPv4 subnet", - subnet: &net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, - want: []IPAMLocalHostRangeConf{ - { - Subnet: "192.168.0.0/24", - Gateway: "192.168.0.1", - }, - }, - }, - { - name: "IPv4 subnet, range and gateway", - subnet: &net.IPNet{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}, - ipRange: &net.IPNet{IP: net.IPv4(192, 168, 0, 128), Mask: net.IPv4Mask(255, 255, 255, 128)}, - gw: net.ParseIP("192.168.0.10"), - want: []IPAMLocalHostRangeConf{ - { - Subnet: "192.168.0.0/24", - RangeStart: "192.168.0.129", - RangeEnd: "192.168.0.255", - Gateway: "192.168.0.10", - }, - }, - }, - { - name: "IPv6 subnet", - subnet: &net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, - want: []IPAMLocalHostRangeConf{ - { - Subnet: "2001:db8::/48", - Gateway: "2001:db8::1", - }, - }, - }, - { - name: "IPv6 subnet, range and gateway", - subnet: &net.IPNet{IP: net.ParseIP("2001:DB8::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff::"))}, - ipRange: &net.IPNet{IP: net.ParseIP("2001:DB8:1:1::"), Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))}, - gw: net.ParseIP("2001:DB8::2"), - want: []IPAMLocalHostRangeConf{ - { - Subnet: "2001:db8::/48", - RangeStart: "2001:db8:1:1::1", - RangeEnd: "2001:db8:1:1:ffff:ffff:ffff:ffff", - Gateway: "2001:db8::2", - }, - }, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - got, err := NewIPAMLocalHostRange(tt.subnet, tt.ipRange, tt.gw) - if err != nil { - t.Errorf("no error expected: %v", err) - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("NewIPAMLocalHostRange() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/libpod/network/network.go b/libpod/network/network.go deleted file mode 100644 index 3b81ce776..000000000 --- a/libpod/network/network.go +++ /dev/null @@ -1,288 +0,0 @@ -package network - -import ( - "encoding/json" - "net" - "os" - - "github.com/containernetworking/cni/libcni" - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" - "github.com/containers/common/pkg/config" - "github.com/containers/podman/v3/libpod/define" - "github.com/containers/podman/v3/pkg/domain/entities" - "github.com/containers/podman/v3/pkg/rootless" - "github.com/containers/podman/v3/pkg/util" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -var ( - // BridgeNetworkDriver defines the bridge cni driver - BridgeNetworkDriver = "bridge" - // DefaultNetworkDriver is the default network type used - DefaultNetworkDriver = BridgeNetworkDriver - // MacVLANNetworkDriver defines the macvlan cni driver - MacVLANNetworkDriver = "macvlan" -) - -// SupportedNetworkDrivers describes the list of supported drivers -var SupportedNetworkDrivers = []string{BridgeNetworkDriver, MacVLANNetworkDriver} - -// isSupportedDriver checks if the user provided driver is supported -func isSupportedDriver(driver string) error { - if util.StringInSlice(driver, SupportedNetworkDrivers) { - return nil - } - return errors.Errorf("driver '%s' is not supported", driver) -} - -// GetLiveNetworks returns a slice of networks representing what the system -// has defined as network interfaces -func GetLiveNetworks() ([]*net.IPNet, error) { - addrs, err := net.InterfaceAddrs() - if err != nil { - return nil, err - } - nets := make([]*net.IPNet, 0, len(addrs)) - for _, address := range addrs { - _, n, err := net.ParseCIDR(address.String()) - if err != nil { - return nil, err - } - nets = append(nets, n) - } - return nets, nil -} - -// GetLiveNetworkNames returns a list of network interfaces on the system -func GetLiveNetworkNames() ([]string, error) { - liveInterfaces, err := net.Interfaces() - if err != nil { - return nil, err - } - interfaceNames := make([]string, 0, len(liveInterfaces)) - for _, i := range liveInterfaces { - interfaceNames = append(interfaceNames, i.Name) - } - return interfaceNames, nil -} - -// GetFreeNetwork looks for a free network according to existing cni configuration -// files and network interfaces. -func GetFreeNetwork(config *config.Config) (*net.IPNet, error) { - networks, err := GetNetworksFromFilesystem(config) - if err != nil { - return nil, err - } - liveNetworks, err := GetLiveNetworks() - if err != nil { - return nil, err - } - nextNetwork, err := GetDefaultPodmanNetwork() - if err != nil { - return nil, err - } - logrus.Debugf("default network is %s", nextNetwork.String()) - for { - newNetwork, err := NextSubnet(nextNetwork) - if err != nil { - return nil, err - } - logrus.Debugf("checking if network %s intersects with other cni networks", nextNetwork.String()) - if intersectsConfig, _ := networkIntersectsWithNetworks(newNetwork, allocatorToIPNets(networks)); intersectsConfig { - logrus.Debugf("network %s is already being used by a cni configuration", nextNetwork.String()) - nextNetwork = newNetwork - continue - } - logrus.Debugf("checking if network %s intersects with any network interfaces", nextNetwork.String()) - if intersectsLive, _ := networkIntersectsWithNetworks(newNetwork, liveNetworks); !intersectsLive { - break - } - logrus.Debugf("network %s is being used by a network interface", nextNetwork.String()) - nextNetwork = newNetwork - } - return nextNetwork, nil -} - -func allocatorToIPNets(networks []*allocator.Net) []*net.IPNet { - var nets []*net.IPNet - for _, network := range networks { - if len(network.IPAM.Ranges) > 0 { - // this is the new IPAM range style - // append each subnet from ipam the rangeset - for _, allocatorRange := range network.IPAM.Ranges { - for _, r := range allocatorRange { - nets = append(nets, newIPNetFromSubnet(r.Subnet)) - } - } - } else { - // looks like the old, deprecated style - nets = append(nets, newIPNetFromSubnet(network.IPAM.Subnet)) - } - } - return nets -} - -func newIPNetFromSubnet(subnet types.IPNet) *net.IPNet { - n := net.IPNet{ - IP: subnet.IP, - Mask: subnet.Mask, - } - return &n -} - -func networkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) (bool, *net.IPNet) { - for _, nw := range networklist { - if networkIntersect(n, nw) { - return true, nw - } - } - return false, nil -} - -func networkIntersect(n1, n2 *net.IPNet) bool { - return n2.Contains(n1.IP) || n1.Contains(n2.IP) -} - -// ValidateUserNetworkIsAvailable returns via an error if a network is available -// to be used -func ValidateUserNetworkIsAvailable(config *config.Config, userNet *net.IPNet) error { - if len(userNet.IP) == 0 || len(userNet.Mask) == 0 { - return errors.Errorf("network %s's ip or mask cannot be empty", userNet.String()) - } - - ones, bit := userNet.Mask.Size() - if ones == 0 || bit == 0 { - return errors.Errorf("network %s's mask is invalid", userNet.String()) - } - - networks, err := GetNetworksFromFilesystem(config) - if err != nil { - return err - } - liveNetworks, err := GetLiveNetworks() - if err != nil { - return err - } - logrus.Debugf("checking if network %s exists in cni networks", userNet.String()) - if intersectsConfig, _ := networkIntersectsWithNetworks(userNet, allocatorToIPNets(networks)); intersectsConfig { - return errors.Errorf("network %s is already being used by a cni configuration", userNet.String()) - } - logrus.Debugf("checking if network %s exists in any network interfaces", userNet.String()) - if intersectsLive, _ := networkIntersectsWithNetworks(userNet, liveNetworks); intersectsLive { - return errors.Errorf("network %s is being used by a network interface", userNet.String()) - } - return nil -} - -// removeNetwork is removes a cni network without a lock and should only be called -// when a lock was otherwise acquired. -func removeNetwork(config *config.Config, name string) error { - cniPath, err := GetCNIConfigPathByNameOrID(config, name) - if err != nil { - return err - } - // Before we delete the configuration file, we need to make sure we can read and parse - // it to get the network interface name so we can remove that too - interfaceName, err := GetInterfaceNameFromConfig(cniPath) - if err == nil { - // Don't try to remove the network interface if we are not root - if !rootless.IsRootless() { - liveNetworkNames, err := GetLiveNetworkNames() - if err != nil { - return errors.Wrapf(err, "failed to get live network names") - } - if util.StringInSlice(interfaceName, liveNetworkNames) { - if err = RemoveInterface(interfaceName); err != nil { - // only log the error, it is not fatal - logrus.Infof("failed to remove network interface %s: %v", interfaceName, err) - } - } - } - } else if err != ErrNoSuchNetworkInterface { - // Don't error if we couldn't find the network interface name - return err - } - // Remove the configuration file - if err := os.Remove(cniPath); err != nil { - return errors.Wrap(err, "failed to remove network configuration") - } - return nil -} - -// RemoveNetwork removes a given network by name. If the network has container associated with it, that -// must be handled outside the context of this. -func RemoveNetwork(config *config.Config, name string) error { - l, err := acquireCNILock(config) - if err != nil { - return err - } - defer l.releaseCNILock() - return removeNetwork(config, name) -} - -// InspectNetwork reads a CNI config and returns its configuration -func InspectNetwork(config *config.Config, name string) (map[string]interface{}, error) { - b, err := ReadRawCNIConfByNameOrID(config, name) - if err != nil { - return nil, err - } - rawList := make(map[string]interface{}) - err = json.Unmarshal(b, &rawList) - return rawList, err -} - -// Exists says whether a given network exists or not; it meant -// specifically for restful responses so 404s can be used -func Exists(config *config.Config, name string) (bool, error) { - _, err := ReadRawCNIConfByNameOrID(config, name) - if err != nil { - if errors.Cause(err) == define.ErrNoSuchNetwork { - return false, nil - } - return false, err - } - return true, nil -} - -// PruneNetworks removes networks that are not being used and that is not the default -// network. To keep proper fencing for imports, you must provide the used networks -// to this function as a map. the key is meaningful in the map, the book is a no-op -func PruneNetworks(rtc *config.Config, usedNetworks map[string]bool) ([]*entities.NetworkPruneReport, error) { - var reports []*entities.NetworkPruneReport - lock, err := acquireCNILock(rtc) - if err != nil { - return nil, err - } - defer lock.releaseCNILock() - nets, err := GetNetworkNamesFromFileSystem(rtc) - if err != nil { - return nil, err - } - for _, n := range nets { - _, found := usedNetworks[n] - // Remove is not default network and not found in the used list - if n != rtc.Network.DefaultNetwork && !found { - reports = append(reports, &entities.NetworkPruneReport{ - Name: n, - Error: removeNetwork(rtc, n), - }) - } - } - return reports, nil -} - -// NormalizeName translates a network ID into a name. -// If the input is a name the name is returned. -func NormalizeName(config *config.Config, nameOrID string) (string, error) { - path, err := GetCNIConfigPathByNameOrID(config, nameOrID) - if err != nil { - return "", err - } - conf, err := libcni.ConfListFromFile(path) - if err != nil { - return "", err - } - return conf.Name, nil -} diff --git a/libpod/network/network_test.go b/libpod/network/network_test.go deleted file mode 100644 index 1969e792c..000000000 --- a/libpod/network/network_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package network - -import ( - "net" - "testing" -) - -func parseCIDR(n string) *net.IPNet { - _, parsedNet, _ := net.ParseCIDR(n) - return parsedNet -} - -func Test_networkIntersect(t *testing.T) { - type args struct { - n1 *net.IPNet - n2 *net.IPNet - } - tests := []struct { - name string - args args - want bool - }{ - {"16 and 24 intersects", args{n1: parseCIDR("192.168.0.0/16"), n2: parseCIDR("192.168.1.0/24")}, true}, - {"24 and 25 intersects", args{n1: parseCIDR("192.168.1.0/24"), n2: parseCIDR("192.168.1.0/25")}, true}, - {"Two 24s", args{n1: parseCIDR("192.168.1.0/24"), n2: parseCIDR("192.168.2.0/24")}, false}, - } - for _, tt := range tests { - test := tt - t.Run(tt.name, func(t *testing.T) { - if got := networkIntersect(test.args.n1, test.args.n2); got != test.want { - t.Errorf("networkIntersect() = %v, want %v", got, test.want) - } - }) - } -} diff --git a/libpod/network/subnet.go b/libpod/network/subnet.go deleted file mode 100644 index 120038e57..000000000 --- a/libpod/network/subnet.go +++ /dev/null @@ -1,78 +0,0 @@ -package network - -/* - The code in this was kindly contributed by Dan Williams(dcbw@redhat.com). Many thanks - for his contributions. -*/ - -import ( - "fmt" - "net" -) - -func incByte(subnet *net.IPNet, idx int, shift uint) error { - if idx < 0 { - return fmt.Errorf("no more subnets left") - } - if subnet.IP[idx] == 255 { - subnet.IP[idx] = 0 - return incByte(subnet, idx-1, 0) - } - subnet.IP[idx] += 1 << shift - return nil -} - -// NextSubnet returns subnet incremented by 1 -func NextSubnet(subnet *net.IPNet) (*net.IPNet, error) { - newSubnet := &net.IPNet{ - IP: subnet.IP, - Mask: subnet.Mask, - } - ones, bits := newSubnet.Mask.Size() - if ones == 0 { - return nil, fmt.Errorf("%s has only one subnet", subnet.String()) - } - zeroes := uint(bits - ones) - shift := zeroes % 8 - idx := ones/8 - 1 - if idx < 0 { - idx = 0 - } - if err := incByte(newSubnet, idx, shift); err != nil { - return nil, err - } - return newSubnet, nil -} - -// LastIPInSubnet gets the last IP in a subnet -func LastIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer - // re-parse to ensure clean network address - _, cidr, err := net.ParseCIDR(addr.String()) - if err != nil { - return nil, err - } - - ones, bits := cidr.Mask.Size() - if ones == bits { - return cidr.IP, nil - } - for i := range cidr.IP { - cidr.IP[i] = cidr.IP[i] | ^cidr.Mask[i] - } - return cidr.IP, nil -} - -// FirstIPInSubnet gets the first IP in a subnet -func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer - // re-parse to ensure clean network address - _, cidr, err := net.ParseCIDR(addr.String()) - if err != nil { - return nil, err - } - ones, bits := cidr.Mask.Size() - if ones == bits { - return cidr.IP, nil - } - cidr.IP[len(cidr.IP)-1]++ - return cidr.IP, nil -} diff --git a/libpod/network/subnet_test.go b/libpod/network/subnet_test.go deleted file mode 100644 index 55b2443bd..000000000 --- a/libpod/network/subnet_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package network - -import ( - "net" - "reflect" - "testing" -) - -func TestNextSubnet(t *testing.T) { - type args struct { - subnet *net.IPNet - } - tests := []struct { - name string - args args - want *net.IPNet - wantErr bool - }{ - {"class b", args{subnet: parseCIDR("192.168.0.0/16")}, parseCIDR("192.169.0.0/16"), false}, - {"class c", args{subnet: parseCIDR("192.168.1.0/24")}, parseCIDR("192.168.2.0/24"), false}, - } - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - got, err := NextSubnet(test.args.subnet) - if (err != nil) != test.wantErr { - t.Errorf("NextSubnet() error = %v, wantErr %v", err, test.wantErr) - return - } - if !reflect.DeepEqual(got, test.want) { - t.Errorf("NextSubnet() got = %v, want %v", got, test.want) - } - }) - } -} - -func TestFirstIPInSubnet(t *testing.T) { - tests := []struct { - name string - args *net.IPNet - want net.IP - wantErr bool - }{ - {"class b", parseCIDR("192.168.0.0/16"), net.ParseIP("192.168.0.1"), false}, - {"class c", parseCIDR("192.168.1.0/24"), net.ParseIP("192.168.1.1"), false}, - {"cidr /23", parseCIDR("192.168.0.0/23"), net.ParseIP("192.168.0.1"), false}, - {"cidr /25", parseCIDR("192.168.1.0/25"), net.ParseIP("192.168.1.1"), false}, - {"cidr /26", parseCIDR("172.16.1.128/26"), net.ParseIP("172.16.1.129"), false}, - {"class a", parseCIDR("10.0.0.0/8"), net.ParseIP("10.0.0.1"), false}, - {"cidr /32", parseCIDR("192.168.255.4/32"), net.ParseIP("192.168.255.4"), false}, - {"cidr /31", parseCIDR("192.168.255.4/31"), net.ParseIP("192.168.255.5"), false}, - } - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - got, err := FirstIPInSubnet(test.args) - if (err != nil) != test.wantErr { - t.Errorf("FirstIPInSubnet() error = %v, wantErr %v", err, test.wantErr) - return - } - if !got.Equal(test.want) { - t.Errorf("FirstIPInSubnet() got = %v, want %v", got, test.want) - } - }) - } -} - -func TestLastIPInSubnet(t *testing.T) { - tests := []struct { - name string - args *net.IPNet - want net.IP - wantErr bool - }{ - {"class b", parseCIDR("192.168.0.0/16"), net.ParseIP("192.168.255.255"), false}, - {"class c", parseCIDR("192.168.1.0/24"), net.ParseIP("192.168.1.255"), false}, - {"cidr /23", parseCIDR("192.168.0.0/23"), net.ParseIP("192.168.1.255"), false}, - {"cidr /25", parseCIDR("192.168.1.0/25"), net.ParseIP("192.168.1.127"), false}, - {"cidr /26", parseCIDR("172.16.1.128/26"), net.ParseIP("172.16.1.191"), false}, - {"class a", parseCIDR("10.0.0.0/8"), net.ParseIP("10.255.255.255"), false}, - {"cidr /32", parseCIDR("192.168.255.4/32"), net.ParseIP("192.168.255.4"), false}, - {"cidr /31", parseCIDR("192.168.255.4/31"), net.ParseIP("192.168.255.5"), false}, - } - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - got, err := LastIPInSubnet(test.args) - if (err != nil) != test.wantErr { - t.Errorf("LastIPInSubnet() error = %v, wantErr %v", err, test.wantErr) - return - } - if !got.Equal(test.want) { - t.Errorf("LastIPInSubnet() got = %v, want %v", got, test.want) - } - }) - } -} diff --git a/libpod/network/types/network.go b/libpod/network/types/network.go index c2c598f46..6053ceb29 100644 --- a/libpod/network/types/network.go +++ b/libpod/network/types/network.go @@ -23,6 +23,10 @@ type ContainerNetwork interface { Setup(namespacePath string, options SetupOptions) (map[string]StatusBlock, error) // Teardown will teardown the container network namespace. Teardown(namespacePath string, options TeardownOptions) error + + // Drivers will return the list of supported network drivers + // for this interface. + Drivers() []string } // Network describes the Network attributes. @@ -36,8 +40,7 @@ type Network struct { // InterfaceName is the network interface name on the host. NetworkInterface string `json:"network_interface,omitempty"` // Created contains the timestamp when this network was created. - // This is not guaranteed to stay exactly the same. - Created time.Time + Created time.Time `json:"created,omitempty"` // Subnets to use. Subnets []Subnet `json:"subnets,omitempty"` // IPv6Enabled if set to true an ipv6 subnet should be created for this net. @@ -92,9 +95,11 @@ func (n *IPNet) UnmarshalText(text []byte) error { } type Subnet struct { - // Subnet for this Network. + // Subnet for this Network in CIDR form. + // swagger:strfmt string Subnet IPNet `json:"subnet,omitempty"` // Gateway IP for this Network. + // swagger:strfmt string Gateway net.IP `json:"gateway,omitempty"` // LeaseRange contains the range where IP are leased. Optional. LeaseRange *LeaseRange `json:"lease_range,omitempty"` @@ -103,8 +108,10 @@ type Subnet struct { // LeaseRange contains the range where IP are leased. type LeaseRange struct { // StartIP first IP in the subnet which should be used to assign ips. + // swagger:strfmt string StartIP net.IP `json:"start_ip,omitempty"` // EndIP last IP in the subnet which should be used to assign ips. + // swagger:strfmt string EndIP net.IP `json:"end_ip,omitempty"` } @@ -196,6 +203,20 @@ type PortMapping struct { Protocol string `json:"protocol,omitempty"` } +// OCICNIPortMapping maps to the standard CNI portmapping Capability. +// Deprecated, do not use this struct for new fields. This only exists +// for backwards compatibility. +type OCICNIPortMapping struct { + // HostPort is the port number on the host. + HostPort int32 `json:"hostPort"` + // ContainerPort is the port number inside the sandbox. + ContainerPort int32 `json:"containerPort"` + // Protocol is the protocol of the port mapping. + Protocol string `json:"protocol"` + // HostIP is the host ip to use. + HostIP string `json:"hostIP"` +} + type SetupOptions struct { NetworkOptions } diff --git a/libpod/network/util/filters.go b/libpod/network/util/filters.go index 48e769196..c3c80b352 100644 --- a/libpod/network/util/filters.go +++ b/libpod/network/util/filters.go @@ -28,12 +28,6 @@ func createFilterFuncs(key string, filterValues []string) (types.FilterFunc, err return util.StringMatchRegexSlice(net.Name, filterValues) }, nil - case "label": - // matches all labels - return func(net types.Network) bool { - return util.MatchLabelFilters(filterValues, net.Labels) - }, nil - case "driver": // matches network driver return func(net types.Network) bool { @@ -46,9 +40,39 @@ func createFilterFuncs(key string, filterValues []string) (types.FilterFunc, err return util.StringMatchRegexSlice(net.ID, filterValues) }, nil - // FIXME: What should we do with the old plugin filter - // TODO: add dangling, dns enabled, internal filter + // TODO: add dns enabled, internal filter + } + return createPruneFilterFuncs(key, filterValues) +} + +func GenerateNetworkPruneFilters(filters map[string][]string) ([]types.FilterFunc, error) { + filterFuncs := make([]types.FilterFunc, 0, len(filters)) + for key, filterValues := range filters { + filterFunc, err := createPruneFilterFuncs(key, filterValues) + if err != nil { + return nil, err + } + filterFuncs = append(filterFuncs, filterFunc) + } + return filterFuncs, nil +} + +func createPruneFilterFuncs(key string, filterValues []string) (types.FilterFunc, error) { + switch strings.ToLower(key) { + case "label": + // matches all labels + return func(net types.Network) bool { + return util.MatchLabelFilters(filterValues, net.Labels) + }, nil + case "until": + until, err := util.ComputeUntilTimestamp(filterValues) + if err != nil { + return nil, err + } + return func(net types.Network) bool { + return net.Created.Before(until) + }, nil default: return nil, errors.Errorf("invalid filter %q", key) } diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index b0d4e0b2d..96b6fb298 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -11,17 +11,15 @@ import ( "os/exec" "path/filepath" "regexp" - "sort" "strconv" "strings" "syscall" "time" - cnitypes "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/events" - "github.com/containers/podman/v3/libpod/network" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/errorhandling" "github.com/containers/podman/v3/pkg/namespaces" "github.com/containers/podman/v3/pkg/netns" @@ -29,7 +27,6 @@ import ( "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/util" "github.com/containers/storage/pkg/lockfile" - "github.com/cri-o/ocicni/pkg/ocicni" "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -51,58 +48,57 @@ const ( persistentCNIDir = "/var/lib/cni" ) -// Get an OCICNI network config -func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, ports []ocicni.PortMapping, staticIP net.IP, staticMAC net.HardwareAddr, netDescriptions ContainerNetworkDescriptions) ocicni.PodNetwork { - var networkKey string - if len(networks) > 0 { - // This is inconsistent for >1 ctrNetwork, but it's probably the - // best we can do. - networkKey = networks[0] - } else { - networkKey = r.netPlugin.GetDefaultNetworkName() - } - ctrNetwork := ocicni.PodNetwork{ - Name: name, - Namespace: name, // TODO is there something else we should put here? We don't know about Kube namespaces - ID: id, - NetNS: nsPath, - RuntimeConfig: map[string]ocicni.RuntimeConfig{ - networkKey: {PortMappings: ports}, - }, +func (c *Container) getNetworkOptions() (types.NetworkOptions, error) { + opts := types.NetworkOptions{ + 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) + } + networks, _, err := c.networks() + if err != nil { + return opts, err + } + aliases, err := c.runtime.state.GetAllNetworkAliases(c) + if err != nil { + return opts, err } - // If we have extra networks, add them - if len(networks) > 0 { - ctrNetwork.Networks = make([]ocicni.NetAttachment, len(networks)) - for i, netName := range networks { - ctrNetwork.Networks[i].Name = netName - if eth, exists := netDescriptions.getInterfaceByName(netName); exists { - ctrNetwork.Networks[i].Ifname = eth - } - } + // If the container requested special network options use this instead of the config. + // This is the case for container restore or network reload. + if c.perNetworkOpts != nil { + opts.Networks = c.perNetworkOpts + return opts, nil } - if staticIP != nil || staticMAC != nil { - // For static IP or MAC, we need to populate networks even if - // it's just the default. - if len(networks) == 0 { - // If len(networks) == 0 this is guaranteed to be the - // default ctrNetwork. - ctrNetwork.Networks = []ocicni.NetAttachment{{Name: networkKey}} - } - var rt ocicni.RuntimeConfig = ocicni.RuntimeConfig{PortMappings: ports} - if staticIP != nil { - rt.IP = staticIP.String() + // Update container map of interface descriptions + if err := c.setupNetworkDescriptions(networks); err != nil { + return opts, err + } + + nets := make(map[string]types.PerNetworkOptions, len(networks)) + for i, network := range networks { + eth, exists := c.state.NetInterfaceDescriptions.getInterfaceByName(network) + if !exists { + return opts, errors.Errorf("no network interface name for container %s on network %s", c.config.ID, network) } - if staticMAC != nil { - rt.MAC = staticMAC.String() + netOpts := types.PerNetworkOptions{ + InterfaceName: eth, + Aliases: aliases[network], } - ctrNetwork.RuntimeConfig = map[string]ocicni.RuntimeConfig{ - networkKey: rt, + // only set the static ip/mac on the first network + if i == 0 { + if c.config.StaticIP != nil { + netOpts.StaticIPs = []net.IP{c.config.StaticIP} + } + netOpts.StaticMAC = c.config.StaticMAC } + nets[network] = netOpts } - - return ctrNetwork + opts.Networks = nets + return opts, nil } type RootlessCNI struct { @@ -571,9 +567,9 @@ func setPrimaryMachineIP() error { return os.Setenv("PODMAN_MACHINE_HOST", addr.IP.String()) } -// setUpOCICNIPod will set up the cni networks, on error it will also tear down the cni +// 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. -func (r *Runtime) setUpOCICNIPod(podNetwork ocicni.PodNetwork) ([]ocicni.NetResult, error) { +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 @@ -583,16 +579,10 @@ func (r *Runtime) setUpOCICNIPod(podNetwork ocicni.PodNetwork) ([]ocicni.NetResu if err != nil { return nil, err } - var results []ocicni.NetResult + var results map[string]types.StatusBlock setUpPod := func() error { - results, err = r.netPlugin.SetUpPod(podNetwork) - if err != nil { - if err2 := r.netPlugin.TearDownPod(podNetwork); err2 != nil { - logrus.Errorf("Error tearing down partially created network namespace for container %s: %v", podNetwork.ID, err2) - } - return errors.Wrapf(err, "error configuring network namespace for container %s", podNetwork.ID) - } - return nil + results, err = r.network.Setup(ns, types.SetupOptions{NetworkOptions: opts}) + return err } // rootlessCNINS is nil if we are root if rootlessCNINS != nil { @@ -609,7 +599,7 @@ func (r *Runtime) setUpOCICNIPod(podNetwork ocicni.PodNetwork) ([]ocicni.NetResu // If we are in the pod network namespace use the pod name otherwise the container name func getCNIPodName(c *Container) string { if c.config.NetMode.IsPod() || c.IsInfra() { - pod, err := c.runtime.GetPod(c.PodID()) + pod, err := c.runtime.state.Pod(c.PodID()) if err == nil { return pod.Name() } @@ -618,26 +608,7 @@ func getCNIPodName(c *Container) string { } // Create and configure a new network namespace for a container -func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) ([]*cnitypes.Result, error) { - var requestedIP net.IP - if ctr.requestedIP != nil { - requestedIP = ctr.requestedIP - // cancel request for a specific IP in case the container is reused later - ctr.requestedIP = nil - } else { - requestedIP = ctr.config.StaticIP - } - - var requestedMAC net.HardwareAddr - if ctr.requestedMAC != nil { - requestedMAC = ctr.requestedMAC - // cancel request for a specific MAC in case the container is reused later - ctr.requestedMAC = nil - } else { - requestedMAC = ctr.config.StaticMAC - } - - podName := getCNIPodName(ctr) +func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) (map[string]types.StatusBlock, error) { networks, _, err := ctr.networks() if err != nil { return nil, err @@ -648,39 +619,15 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) ([]*cnitypes.Re return nil, nil } - // Update container map of interface descriptions - if err := ctr.setupNetworkDescriptions(networks); err != nil { - return nil, err - } - podNetwork := r.getPodNetwork(ctr.ID(), podName, ctrNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC, ctr.state.NetInterfaceDescriptions) - aliases, err := ctr.runtime.state.GetAllNetworkAliases(ctr) + netOpts, err := ctr.getNetworkOptions() if err != nil { return nil, err } - if len(aliases) > 0 { - podNetwork.Aliases = aliases - } - - results, err := r.setUpOCICNIPod(podNetwork) - if err != nil { - return nil, err - } - - networkStatus := make([]*cnitypes.Result, 0) - for idx, r := range results { - logrus.Debugf("[%d] CNI result: %v", idx, r.Result) - resultCurrent, err := cnitypes.GetResult(r.Result) - if err != nil { - return nil, errors.Wrapf(err, "error parsing CNI plugin result %q: %v", r.Result, err) - } - networkStatus = append(networkStatus, resultCurrent) - } - - return networkStatus, nil + return r.setUpNetwork(ctrNS.Path(), netOpts) } // Create and configure a new network namespace for a container -func (r *Runtime) createNetNS(ctr *Container) (n ns.NetNS, q []*cnitypes.Result, retErr error) { +func (r *Runtime) createNetNS(ctr *Container) (n ns.NetNS, q map[string]types.StatusBlock, retErr error) { ctrNS, err := netns.NewNS() if err != nil { return nil, nil, errors.Wrapf(err, "error creating network namespace for container %s", ctr.ID()) @@ -698,7 +645,7 @@ func (r *Runtime) createNetNS(ctr *Container) (n ns.NetNS, q []*cnitypes.Result, logrus.Debugf("Made network namespace at %s for container %s", ctrNS.Path(), ctr.ID()) - networkStatus := []*cnitypes.Result{} + var networkStatus map[string]types.StatusBlock if !ctr.config.NetMode.IsSlirp4netns() { networkStatus, err = r.configureNetNS(ctr, ctrNS) } @@ -797,14 +744,14 @@ func (r *Runtime) closeNetNS(ctr *Container) error { // Tear down a container's CNI network configuration and joins the // rootless net ns as rootless user -func (r *Runtime) teardownOCICNIPod(podNetwork ocicni.PodNetwork) error { +func (r *Runtime) teardownNetwork(ns string, opts types.NetworkOptions) error { rootlessCNINS, err := r.GetRootlessCNINetNs(false) if err != nil { return err } tearDownPod := func() error { - err := r.netPlugin.TearDownPod(podNetwork) - return errors.Wrapf(err, "error tearing down CNI namespace configuration for container %s", podNetwork.ID) + err := r.network.Teardown(ns, types.TeardownOptions{NetworkOptions: opts}) + return errors.Wrapf(err, "error tearing down network namespace configuration for container %s", opts.ContainerID) } // rootlessCNINS is nil if we are root @@ -837,27 +784,11 @@ func (r *Runtime) teardownCNI(ctr *Container) error { } if !ctr.config.NetMode.IsSlirp4netns() && len(networks) > 0 { - var requestedIP net.IP - if ctr.requestedIP != nil { - requestedIP = ctr.requestedIP - // cancel request for a specific IP in case the container is reused later - ctr.requestedIP = nil - } else { - requestedIP = ctr.config.StaticIP - } - - var requestedMAC net.HardwareAddr - if ctr.requestedMAC != nil { - requestedMAC = ctr.requestedMAC - // cancel request for a specific MAC in case the container is reused later - ctr.requestedMAC = nil - } else { - requestedMAC = ctr.config.StaticMAC + netOpts, err := ctr.getNetworkOptions() + if err != nil { + return err } - - podNetwork := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC, ctr.state.NetInterfaceDescriptions) - err = r.teardownOCICNIPod(podNetwork) - return err + return r.teardownNetwork(ctr.state.NetNS.Path(), netOpts) } return nil } @@ -918,40 +849,15 @@ func isBridgeNetMode(n namespaces.NetworkMode) error { // single MAC or IP. // Only works on root containers at present, though in the future we could // extend this to stop + restart slirp4netns -func (r *Runtime) reloadContainerNetwork(ctr *Container) ([]*cnitypes.Result, error) { +func (r *Runtime) reloadContainerNetwork(ctr *Container) (map[string]types.StatusBlock, error) { if ctr.state.NetNS == nil { return nil, errors.Wrapf(define.ErrCtrStateInvalid, "container %s network is not configured, refusing to reload", ctr.ID()) } if err := isBridgeNetMode(ctr.config.NetMode); err != nil { return nil, err } - logrus.Infof("Going to reload container %s network", ctr.ID()) - var requestedIP net.IP - var requestedMAC net.HardwareAddr - // Set requested IP and MAC address, if possible. - if len(ctr.state.NetworkStatus) == 1 { - result := ctr.state.NetworkStatus[0] - if len(result.IPs) == 1 { - resIP := result.IPs[0] - - requestedIP = resIP.Address.IP - ctr.requestedIP = requestedIP - logrus.Debugf("Going to preserve container %s IP address %s", ctr.ID(), ctr.requestedIP.String()) - - if resIP.Interface != nil && *resIP.Interface < len(result.Interfaces) && *resIP.Interface >= 0 { - var err error - requestedMAC, err = net.ParseMAC(result.Interfaces[*resIP.Interface].Mac) - if err != nil { - return nil, errors.Wrapf(err, "error parsing container %s MAC address %s", ctr.ID(), result.Interfaces[*resIP.Interface].Mac) - } - ctr.requestedMAC = requestedMAC - logrus.Debugf("Going to preserve container %s MAC address %s", ctr.ID(), ctr.requestedMAC.String()) - } - } - } - err := r.teardownCNI(ctr) if err != nil { // teardownCNI will error if the iptables rules do not exists and this is the case after @@ -966,9 +872,40 @@ func (r *Runtime) reloadContainerNetwork(ctr *Container) ([]*cnitypes.Result, er } } - // teardownCNI will clean the requested IP and MAC so we need to set them again - ctr.requestedIP = requestedIP - ctr.requestedMAC = requestedMAC + aliases, err := ctr.runtime.state.GetAllNetworkAliases(ctr) + if err != nil { + return nil, err + } + + // Set the same network settings as before.. + netStatus := ctr.getNetworkStatus() + netOpts := make(map[string]types.PerNetworkOptions, len(netStatus)) + for network, status := range netStatus { + perNetOpts := types.PerNetworkOptions{} + for name, netInt := range status.Interfaces { + perNetOpts = types.PerNetworkOptions{ + InterfaceName: name, + Aliases: aliases[network], + StaticMAC: netInt.MacAddress, + } + for _, netAddress := range netInt.Networks { + perNetOpts.StaticIPs = append(perNetOpts.StaticIPs, netAddress.Subnet.IP) + } + // Normally interfaces have a length of 1, only for some special cni configs we could get more. + // For now just use the first interface to get the ips this should be good enough for most cases. + break + } + if perNetOpts.InterfaceName == "" { + eth, exists := ctr.state.NetInterfaceDescriptions.getInterfaceByName(network) + if !exists { + return nil, errors.Errorf("no network interface name for container %s on network %s", ctr.config.ID, network) + } + perNetOpts.InterfaceName = eth + } + netOpts[network] = perNetOpts + } + ctr.perNetworkOpts = netOpts + return r.configureNetNS(ctr, ctr.state.NetNS) } @@ -989,7 +926,8 @@ func getContainerNetIO(ctr *Container) (*netlink.LinkStatistics, error) { return nil, nil } err := ns.WithNetNSPath(netNSPath, func(_ ns.NetNS) error { - link, err := netlink.LinkByName(ocicni.DefaultInterfaceName) + // FIXME get the interface from the container netstatus + link, err := netlink.LinkByName("eth0") if err != nil { return err } @@ -1047,27 +985,26 @@ func (c *Container) getContainerNetworkInfo() (*define.InspectNetworkSettings, e // Set network namespace path settings.SandboxKey = c.state.NetNS.Path() + netStatus := c.getNetworkStatus() // If this is empty, we're probably slirp4netns - if len(c.state.NetworkStatus) == 0 { + if len(netStatus) == 0 { return settings, nil } - // If we have CNI networks - handle that here + // If we have networks - handle that here if len(networks) > 0 { - if len(networks) != len(c.state.NetworkStatus) { - return nil, errors.Wrapf(define.ErrInternal, "network inspection mismatch: asked to join %d CNI network(s) %v, but have information on %d network(s)", len(networks), networks, len(c.state.NetworkStatus)) + if len(networks) != len(netStatus) { + return nil, errors.Wrapf(define.ErrInternal, "network inspection mismatch: asked to join %d network(s) %v, but have information on %d network(s)", len(networks), networks, len(netStatus)) } settings.Networks = make(map[string]*define.InspectAdditionalNetwork) - // CNI results should be in the same order as the list of - // networks we pass into CNI. - for index, name := range networks { - cniResult := c.state.NetworkStatus[index] + for _, name := range networks { + result := netStatus[name] addedNet := new(define.InspectAdditionalNetwork) addedNet.NetworkID = name - basicConfig, err := resultToBasicNetworkConfig(cniResult) + basicConfig, err := resultToBasicNetworkConfig(result) if err != nil { return nil, err } @@ -1089,19 +1026,19 @@ func (c *Container) getContainerNetworkInfo() (*define.InspectNetworkSettings, e } // If not joining networks, we should have at most 1 result - if len(c.state.NetworkStatus) > 1 { - return nil, errors.Wrapf(define.ErrInternal, "should have at most 1 CNI result if not joining networks, instead got %d", len(c.state.NetworkStatus)) + if len(netStatus) > 1 { + return nil, errors.Wrapf(define.ErrInternal, "should have at most 1 network status result if not joining networks, instead got %d", len(netStatus)) } - if len(c.state.NetworkStatus) == 1 { - basicConfig, err := resultToBasicNetworkConfig(c.state.NetworkStatus[0]) - if err != nil { - return nil, err + if len(netStatus) == 1 { + for _, status := range netStatus { + basicConfig, err := resultToBasicNetworkConfig(status) + if err != nil { + return nil, err + } + settings.InspectBasicNetworkConfig = basicConfig } - - settings.InspectBasicNetworkConfig = basicConfig } - return settings, nil } @@ -1130,45 +1067,40 @@ func (c *Container) setupNetworkDescriptions(networks []string) error { // resultToBasicNetworkConfig produces an InspectBasicNetworkConfig from a CNI // result -func resultToBasicNetworkConfig(result *cnitypes.Result) (define.InspectBasicNetworkConfig, error) { +func resultToBasicNetworkConfig(result types.StatusBlock) (define.InspectBasicNetworkConfig, error) { config := define.InspectBasicNetworkConfig{} - - for _, ctrIP := range result.IPs { - size, _ := ctrIP.Address.Mask.Size() - switch { - case ctrIP.Version == "4" && config.IPAddress == "": - config.IPAddress = ctrIP.Address.IP.String() - config.IPPrefixLen = size - config.Gateway = ctrIP.Gateway.String() - if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface >= 0 { - config.MacAddress = result.Interfaces[*ctrIP.Interface].Mac - } - case ctrIP.Version == "4" && config.IPAddress != "": - config.SecondaryIPAddresses = append(config.SecondaryIPAddresses, ctrIP.Address.String()) - if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface >= 0 { - config.AdditionalMacAddresses = append(config.AdditionalMacAddresses, result.Interfaces[*ctrIP.Interface].Mac) + for _, netInt := range result.Interfaces { + for _, netAddress := range netInt.Networks { + size, _ := netAddress.Subnet.Mask.Size() + if netAddress.Subnet.IP.To4() != nil { + //ipv4 + if config.IPAddress == "" { + config.IPAddress = netAddress.Subnet.IP.String() + config.IPPrefixLen = size + config.Gateway = netAddress.Gateway.String() + } else { + config.SecondaryIPAddresses = append(config.SecondaryIPAddresses, netAddress.Subnet.IP.String()) + } + } else { + //ipv6 + if config.GlobalIPv6Address == "" { + config.GlobalIPv6Address = netAddress.Subnet.IP.String() + config.GlobalIPv6PrefixLen = size + config.IPv6Gateway = netAddress.Gateway.String() + } else { + config.SecondaryIPv6Addresses = append(config.SecondaryIPv6Addresses, netAddress.Subnet.IP.String()) + } } - case ctrIP.Version == "6" && config.IPAddress == "": - config.GlobalIPv6Address = ctrIP.Address.IP.String() - config.GlobalIPv6PrefixLen = size - config.IPv6Gateway = ctrIP.Gateway.String() - case ctrIP.Version == "6" && config.IPAddress != "": - config.SecondaryIPv6Addresses = append(config.SecondaryIPv6Addresses, ctrIP.Address.String()) - default: - return config, errors.Wrapf(define.ErrInternal, "unrecognized IP version %q", ctrIP.Version) + } + if config.MacAddress == "" { + config.MacAddress = netInt.MacAddress.String() + } else { + config.AdditionalMacAddresses = append(config.AdditionalMacAddresses, netInt.MacAddress.String()) } } - return config, nil } -// This is a horrible hack, necessary because CNI does not properly clean up -// after itself on an unclean reboot. Return what we're pretty sure is the path -// to CNI's internal files (it's not really exposed to us). -func getCNINetworksDir() (string, error) { - return filepath.Join(persistentCNIDir, "networks"), nil -} - type logrusDebugWriter struct { prefix string } @@ -1185,28 +1117,31 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) erro return err } + c.lock.Lock() + defer c.lock.Unlock() + networks, err := c.networksByNameIndex() if err != nil { return err } // check if network exists and if the input is a ID we get the name - // ocicni only uses names so it is important that we only use the name - netName, err = network.NormalizeName(c.runtime.config, netName) + // CNI only uses names so it is important that we only use the name + netName, err = c.runtime.normalizeNetworkName(netName) if err != nil { return err } - index, nameExists := networks[netName] + _, nameExists := networks[netName] if !nameExists && len(networks) > 0 { return errors.Errorf("container %s is not connected to network %s", nameOrID, netName) } - c.lock.Lock() - defer c.lock.Unlock() if err := c.syncContainer(); err != nil { return err } + // get network status before we disconnect + networkStatus := c.getNetworkStatus() if err := c.runtime.state.NetworkDisconnect(c, netName); err != nil { return err @@ -1221,41 +1156,38 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) erro return errors.Wrapf(define.ErrNoNetwork, "unable to disconnect %s from %s", nameOrID, netName) } - podConfig := c.runtime.getPodNetwork(c.ID(), c.Name(), c.state.NetNS.Path(), []string{netName}, c.config.PortMappings, nil, nil, c.state.NetInterfaceDescriptions) - if err := c.runtime.teardownOCICNIPod(podConfig); err != nil { - return err + opts := types.NetworkOptions{ + ContainerID: c.config.ID, + ContainerName: getCNIPodName(c), + } + if len(c.config.PortMappings) > 0 { + opts.PortMappings = ocicniPortsToNetTypesPorts(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) + } + opts.Networks = map[string]types.PerNetworkOptions{ + netName: { + InterfaceName: eth, + }, } - // update network status if container is not running - networkStatus := c.state.NetworkStatus - // clip out the index of the network - tmpNetworkStatus := make([]*cnitypes.Result, 0, len(networkStatus)-1) - for k, v := range networkStatus { - if index != k { - tmpNetworkStatus = append(tmpNetworkStatus, v) - } + if err := c.runtime.teardownNetwork(c.state.NetNS.Path(), opts); err != nil { + return err } - c.state.NetworkStatus = tmpNetworkStatus + + // update network status if container is running + delete(networkStatus, netName) + c.state.NetworkStatus = networkStatus err = c.save() if err != nil { return err } - // OCICNI will set the loopback adapter down on teardown so we should set it up again - err = c.state.NetNS.Do(func(_ ns.NetNS) error { - link, err := netlink.LinkByName("lo") - if err != nil { - return err - } - err = netlink.LinkSetUp(link) - return err - }) - if err != nil { - logrus.Warnf("failed to set loopback adapter up in the container: %v", err) - } // Reload ports when there are still connected networks, maybe we removed the network interface with the child ip. // Reloading without connected networks does not make sense, so we can skip this step. - if rootless.IsRootless() && len(tmpNetworkStatus) > 0 { + if rootless.IsRootless() && len(networkStatus) > 0 { return c.reloadRootlessRLKPortMapping() } return nil @@ -1268,24 +1200,28 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e return err } + c.lock.Lock() + defer c.lock.Unlock() + networks, err := c.networksByNameIndex() if err != nil { return err } // check if network exists and if the input is a ID we get the name - // ocicni only uses names so it is important that we only use the name - netName, err = network.NormalizeName(c.runtime.config, netName) + // CNI only uses names so it is important that we only use the name + netName, err = c.runtime.normalizeNetworkName(netName) if err != nil { return err } - c.lock.Lock() - defer c.lock.Unlock() if err := c.syncContainer(); err != nil { return err } + // get network status before we connect + networkStatus := c.getNetworkStatus() + if err := c.runtime.state.NetworkConnect(c, netName, aliases); err != nil { return err } @@ -1305,10 +1241,26 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e if err := c.setupNetworkDescriptions(ctrNetworks); err != nil { return err } - podConfig := c.runtime.getPodNetwork(c.ID(), c.Name(), c.state.NetNS.Path(), []string{netName}, c.config.PortMappings, nil, nil, c.state.NetInterfaceDescriptions) - podConfig.Aliases = make(map[string][]string, 1) - podConfig.Aliases[netName] = aliases - results, err := c.runtime.setUpOCICNIPod(podConfig) + + opts := types.NetworkOptions{ + ContainerID: c.config.ID, + ContainerName: getCNIPodName(c), + } + if len(c.config.PortMappings) > 0 { + opts.PortMappings = ocicniPortsToNetTypesPorts(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) + } + opts.Networks = map[string]types.PerNetworkOptions{ + netName: { + Aliases: aliases, + InterfaceName: eth, + }, + } + + results, err := c.runtime.setUpNetwork(c.state.NetNS.Path(), opts) if err != nil { return err } @@ -1316,40 +1268,13 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e return errors.New("when adding aliases, results must be of length 1") } - networkResults := make([]*cnitypes.Result, 0) - for _, r := range results { - resultCurrent, err := cnitypes.GetResult(r.Result) - if err != nil { - return errors.Wrapf(err, "error parsing CNI plugin result %q: %v", r.Result, err) - } - networkResults = append(networkResults, resultCurrent) - } - // update network status - networkStatus := c.state.NetworkStatus - // if len is one and we confirmed earlier that the container is in - // fact connected to the network, then just return an empty slice - if len(networkStatus) == 0 { - c.state.NetworkStatus = append(c.state.NetworkStatus, networkResults...) - } else { - // build a list of network names so we can sort and - // get the new name's index - var networkNames []string - for name := range networks { - networkNames = append(networkNames, name) - } - networkNames = append(networkNames, netName) - // sort - sort.Strings(networkNames) - // get index of new network name - index := sort.SearchStrings(networkNames, netName) - // Append a zero value to to the slice - networkStatus = append(networkStatus, &cnitypes.Result{}) - // populate network status - copy(networkStatus[index+1:], networkStatus[index:]) - networkStatus[index] = networkResults[0] - c.state.NetworkStatus = networkStatus + if networkStatus == nil { + networkStatus = make(map[string]types.StatusBlock, 1) } + networkStatus[netName] = results[netName] + c.state.NetworkStatus = networkStatus + err = c.save() if err != nil { return err @@ -1379,3 +1304,27 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases [] } return ctr.NetworkConnect(nameOrID, netName, aliases) } + +// normalizeNetworkName takes a network name, a partial or a full network ID and returns the network name. +// If the network is not found a errors is returned. +func (r *Runtime) normalizeNetworkName(nameOrID string) (string, error) { + net, err := r.network.NetworkInspect(nameOrID) + if err != nil { + return "", err + } + return net.Name, nil +} + +func ocicniPortsToNetTypesPorts(ports []types.OCICNIPortMapping) []types.PortMapping { + 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, + }) + } + return newPorts +} diff --git a/libpod/networking_slirp4netns.go b/libpod/networking_slirp4netns.go index a09027b72..a5c035757 100644 --- a/libpod/networking_slirp4netns.go +++ b/libpod/networking_slirp4netns.go @@ -615,14 +615,21 @@ func getRootlessPortChildIP(c *Container) string { return slirp4netnsIP.String() } - for _, r := range c.state.NetworkStatus { - for _, i := range r.IPs { - ipv4 := i.Address.IP.To4() - if ipv4 != nil { - return ipv4.String() + var ipv6 net.IP + for _, status := range c.getNetworkStatus() { + for _, netInt := range status.Interfaces { + for _, netAddress := range netInt.Networks { + ipv4 := netAddress.Subnet.IP.To4() + if ipv4 != nil { + return ipv4.String() + } + ipv6 = netAddress.Subnet.IP } } } + if ipv6 != nil { + return ipv6.String() + } return "" } diff --git a/libpod/oci_util.go b/libpod/oci_util.go index f2843b09b..7db267915 100644 --- a/libpod/oci_util.go +++ b/libpod/oci_util.go @@ -9,7 +9,7 @@ import ( "time" "github.com/containers/podman/v3/libpod/define" - "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/containers/podman/v3/libpod/network/types" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -32,7 +32,7 @@ func createUnitName(prefix string, name string) string { } // Bind ports to keep them closed on the host -func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) { +func bindPorts(ports []types.OCICNIPortMapping) ([]*os.File, error) { var files []*os.File notifySCTP := false for _, i := range ports { diff --git a/libpod/options.go b/libpod/options.go index 7b0c6641a..3f6ccf1cb 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -14,14 +14,13 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/events" - netTypes "github.com/containers/podman/v3/libpod/network/types" + nettypes "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/namespaces" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/util" "github.com/containers/storage" "github.com/containers/storage/pkg/idtools" - "github.com/cri-o/ocicni/pkg/ocicni" "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -1040,7 +1039,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 []ocicni.PortMapping, exposedPorts map[uint16][]string, postConfigureNetNS bool, netmode string, networks []string) CtrCreateOption { +func WithNetNS(portMappings []nettypes.OCICNIPortMapping, exposedPorts map[uint16][]string, postConfigureNetNS bool, netmode string, networks []string) CtrCreateOption { return func(ctr *Container) error { if ctr.valid { return define.ErrCtrFinalized @@ -2063,10 +2062,10 @@ func WithInfraContainer() PodCreateOption { } // WithInfraContainerPorts tells the pod to add port bindings to the pause container -func WithInfraContainerPorts(bindings []ocicni.PortMapping, infraSpec *specgen.SpecGenerator) []netTypes.PortMapping { - bindingSpec := []netTypes.PortMapping{} +func WithInfraContainerPorts(bindings []nettypes.OCICNIPortMapping, infraSpec *specgen.SpecGenerator) []nettypes.PortMapping { + bindingSpec := []nettypes.PortMapping{} for _, bind := range bindings { - currBind := netTypes.PortMapping{} + currBind := nettypes.PortMapping{} currBind.ContainerPort = uint16(bind.ContainerPort) currBind.HostIP = bind.HostIP currBind.HostPort = uint16(bind.HostPort) diff --git a/libpod/runtime.go b/libpod/runtime.go index 761fa08a2..d2b3d36da 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -20,7 +20,6 @@ import ( "github.com/containers/buildah/pkg/parse" "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" - "github.com/containers/common/pkg/defaultnet" "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/pkg/sysregistriesv2" is "github.com/containers/image/v5/storage" @@ -28,6 +27,8 @@ import ( "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/events" "github.com/containers/podman/v3/libpod/lock" + "github.com/containers/podman/v3/libpod/network/cni" + nettypes "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/libpod/plugin" "github.com/containers/podman/v3/libpod/shutdown" "github.com/containers/podman/v3/pkg/cgroups" @@ -36,7 +37,6 @@ import ( "github.com/containers/podman/v3/pkg/util" "github.com/containers/storage" "github.com/containers/storage/pkg/unshare" - "github.com/cri-o/ocicni/pkg/ocicni" "github.com/docker/docker/pkg/namesgenerator" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -80,7 +80,7 @@ type Runtime struct { defaultOCIRuntime OCIRuntime ociRuntimes map[string]OCIRuntime runtimeFlags []string - netPlugin ocicni.CNIPlugin + network nettypes.ContainerNetwork conmonPath string libimageRuntime *libimage.Runtime libimageEventsShutdown chan bool @@ -482,17 +482,20 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { } } - // If we need to make a default network - do so now. - if err := defaultnet.Create(runtime.config.Network.DefaultNetwork, runtime.config.Network.DefaultSubnet, runtime.config.Network.NetworkConfigDir, runtime.config.Engine.StaticDir, runtime.config.Engine.MachineEnabled); err != nil { - logrus.Errorf("Failed to created default CNI network: %v", err) - } - - // Set up the CNI net plugin - netPlugin, err := ocicni.InitCNINoInotify(runtime.config.Network.DefaultNetwork, runtime.config.Network.NetworkConfigDir, "", runtime.config.Network.CNIPluginDirs...) + netInterface, err := cni.NewCNINetworkInterface(cni.InitConfig{ + CNIConfigDir: runtime.config.Network.NetworkConfigDir, + CNIPluginDirs: runtime.config.Network.CNIPluginDirs, + DefaultNetwork: runtime.config.Network.DefaultNetwork, + DefaultSubnet: runtime.config.Network.DefaultSubnet, + IsMachine: runtime.config.Engine.MachineEnabled, + // TODO use cni.lock + LockFile: filepath.Join(runtime.config.Network.NetworkConfigDir, "cni1.lock"), + }) if err != nil { - return errors.Wrapf(err, "error configuring CNI network plugin") + return errors.Wrapf(err, "could not create network interface") } - runtime.netPlugin = netPlugin + + runtime.network = netInterface // We now need to see if the system has restarted // We check for the presence of a file in our tmp directory to verify this @@ -1166,3 +1169,13 @@ func (r *Runtime) graphRootMountedFlag(mounts []spec.Mount) string { } return "" } + +// Network returns the network interface which is used by the runtime +func (r *Runtime) Network() nettypes.ContainerNetwork { + return r.network +} + +// Network returns the network interface which is used by the runtime +func (r *Runtime) GetDefaultNetworkName() string { + return r.config.Network.DefaultNetwork +} diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 7d3891f6e..d4f67a115 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -12,7 +12,6 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/events" - "github.com/containers/podman/v3/libpod/network" "github.com/containers/podman/v3/libpod/shutdown" "github.com/containers/podman/v3/pkg/cgroups" "github.com/containers/podman/v3/pkg/domain/entities/reports" @@ -248,7 +247,7 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai if len(ctr.config.Networks) > 0 { netNames := make([]string, 0, len(ctr.config.Networks)) for _, nameOrID := range ctr.config.Networks { - netName, err := network.NormalizeName(r.config, nameOrID) + netName, err := r.normalizeNetworkName(nameOrID) if err != nil { return nil, err } @@ -262,7 +261,7 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai if len(ctr.config.NetworkAliases) > 0 { netAliases := make(map[string][]string, len(ctr.config.NetworkAliases)) for nameOrID, aliases := range ctr.config.NetworkAliases { - netName, err := network.NormalizeName(r.config, nameOrID) + netName, err := r.normalizeNetworkName(nameOrID) if err != nil { return nil, err } diff --git a/libpod/util.go b/libpod/util.go index ed5c4e6c6..d3f7da91e 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -15,8 +15,8 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/utils" - "github.com/cri-o/ocicni/pkg/ocicni" "github.com/fsnotify/fsnotify" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux/label" @@ -295,8 +295,8 @@ func writeHijackHeader(r *http.Request, conn io.Writer) { } // Convert OCICNI port bindings into Inspect-formatted port bindings. -func makeInspectPortBindings(bindings []ocicni.PortMapping, expose map[uint16][]string) map[string][]define.InspectHostPort { - portBindings := make(map[string][]define.InspectHostPort, len(bindings)) +func makeInspectPortBindings(bindings []types.OCICNIPortMapping, 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] |