summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/common/create_opts.go5
-rw-r--r--cmd/podman/common/util.go10
-rw-r--r--libpod/network/cni/README.md10
-rw-r--r--libpod/network/cni/cni_conversion.go375
-rw-r--r--libpod/network/cni/cni_exec.go98
-rw-r--r--libpod/network/cni/cni_suite_test.go53
-rw-r--r--libpod/network/cni/cni_types.go292
-rw-r--r--libpod/network/cni/config.go313
-rw-r--r--libpod/network/cni/config_test.go1241
-rw-r--r--libpod/network/cni/network.go340
-rw-r--r--libpod/network/cni/run.go309
-rw-r--r--libpod/network/cni/run_test.go1326
-rw-r--r--libpod/network/cni/testfiles/invalid/broken.conflist25
-rw-r--r--libpod/network/cni/testfiles/invalid/invalid_gateway.conflist51
-rw-r--r--libpod/network/cni/testfiles/invalid/invalidname.conflist49
-rw-r--r--libpod/network/cni/testfiles/invalid/noname.conflist48
-rw-r--r--libpod/network/cni/testfiles/invalid/noplugin.conflist5
-rw-r--r--libpod/network/cni/testfiles/invalid/samename1.conflist49
-rw-r--r--libpod/network/cni/testfiles/invalid/samename2.conflist49
-rw-r--r--libpod/network/cni/testfiles/valid/87-podman.conflist37
-rw-r--r--libpod/network/cni/testfiles/valid/bridge.conflist51
-rw-r--r--libpod/network/cni/testfiles/valid/dualstack.conflist58
-rw-r--r--libpod/network/cni/testfiles/valid/internal.conflist40
-rw-r--r--libpod/network/cni/testfiles/valid/label.conflist54
-rw-r--r--libpod/network/cni/testfiles/valid/macvlan.conflist13
-rw-r--r--libpod/network/cni/testfiles/valid/macvlan_mtu.conflist14
-rw-r--r--libpod/network/cni/testfiles/valid/mtu.conflist49
-rw-r--r--libpod/network/cni/testfiles/valid/vlan.conflist50
-rw-r--r--libpod/network/types/const.go21
-rw-r--r--libpod/network/types/network.go208
-rw-r--r--libpod/network/util/filters.go55
-rw-r--r--libpod/network/util/interfaces.go34
-rw-r--r--libpod/network/util/ip.go113
-rw-r--r--libpod/network/util/ip_test.go125
-rw-r--r--pkg/domain/entities/containers.go3
-rw-r--r--pkg/domain/entities/types.go3
-rw-r--r--pkg/specgen/generate/kube/kube.go7
-rw-r--r--pkg/specgen/generate/ports.go3
-rw-r--r--pkg/specgen/podspecgen.go3
-rw-r--r--pkg/specgen/specgen.go33
-rw-r--r--pkg/systemd/generate/common.go5
-rw-r--r--pkg/systemd/generate/common_test.go4
-rw-r--r--pkg/systemd/generate/containers.go8
-rw-r--r--pkg/systemd/generate/containers_test.go75
-rw-r--r--test/system/250-systemd.bats17
-rw-r--r--utils/utils_supported.go2
46 files changed, 5662 insertions, 71 deletions
diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go
index e046e5a19..c94f46cf2 100644
--- a/cmd/podman/common/create_opts.go
+++ b/cmd/podman/common/create_opts.go
@@ -10,6 +10,7 @@ import (
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v3/cmd/podman/registry"
+ "github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/api/handlers"
"github.com/containers/podman/v3/pkg/cgroups"
"github.com/containers/podman/v3/pkg/domain/entities"
@@ -150,7 +151,7 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c
cappDrop []string
entrypoint *string
init bool
- specPorts []specgen.PortMapping
+ specPorts []types.PortMapping
)
if cc.HostConfig.Init != nil {
@@ -240,7 +241,7 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c
if err != nil {
return nil, nil, err
}
- tmpPort := specgen.PortMapping{
+ tmpPort := types.PortMapping{
HostIP: pb.HostIP,
ContainerPort: uint16(port.Int()),
HostPort: uint16(hostport),
diff --git a/cmd/podman/common/util.go b/cmd/podman/common/util.go
index 6a0af4dff..cdfff9d6f 100644
--- a/cmd/podman/common/util.go
+++ b/cmd/podman/common/util.go
@@ -6,7 +6,7 @@ import (
"strconv"
"strings"
- "github.com/containers/podman/v3/pkg/specgen"
+ "github.com/containers/podman/v3/libpod/network/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@@ -90,10 +90,10 @@ func createExpose(expose []string) (map[uint16]string, error) {
}
// CreatePortBindings iterates ports mappings into SpecGen format.
-func CreatePortBindings(ports []string) ([]specgen.PortMapping, error) {
+func CreatePortBindings(ports []string) ([]types.PortMapping, error) {
// --publish is formatted as follows:
// [[hostip:]hostport[-endPort]:]containerport[-endPort][/protocol]
- toReturn := make([]specgen.PortMapping, 0, len(ports))
+ toReturn := make([]types.PortMapping, 0, len(ports))
for _, p := range ports {
var (
@@ -169,8 +169,8 @@ func CreatePortBindings(ports []string) ([]specgen.PortMapping, error) {
// parseSplitPort parses individual components of the --publish flag to produce
// a single port mapping in SpecGen format.
-func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (specgen.PortMapping, error) {
- newPort := specgen.PortMapping{}
+func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (types.PortMapping, error) {
+ newPort := types.PortMapping{}
if ctrPort == "" {
return newPort, errors.Errorf("must provide a non-empty container port to publish")
}
diff --git a/libpod/network/cni/README.md b/libpod/network/cni/README.md
new file mode 100644
index 000000000..6f57feff5
--- /dev/null
+++ b/libpod/network/cni/README.md
@@ -0,0 +1,10 @@
+This package abstracts CNI from libpod.
+It implements the `ContainerNetwork` interface defined in [libpod/network/types/network.go](../types/network.go) for the CNI backend.
+
+
+## Testing
+Run the tests with:
+```
+go test -v -mod=vendor -cover ./libpod/network/cni/
+```
+Run the tests as root to also test setup/teardown. This will execute CNI and therefore the cni plugins have to be installed.
diff --git a/libpod/network/cni/cni_conversion.go b/libpod/network/cni/cni_conversion.go
new file mode 100644
index 000000000..09943948b
--- /dev/null
+++ b/libpod/network/cni/cni_conversion.go
@@ -0,0 +1,375 @@
+// +build linux
+
+package cni
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/containernetworking/cni/libcni"
+ "github.com/containernetworking/cni/pkg/version"
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/libpod/network/util"
+ pkgutil "github.com/containers/podman/v3/pkg/util"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+func createNetworkFromCNIConfigList(conf *libcni.NetworkConfigList, confPath string) (*types.Network, error) {
+ network := types.Network{
+ Name: conf.Name,
+ ID: getNetworkIDFromName(conf.Name),
+ Labels: map[string]string{},
+ Options: map[string]string{},
+ IPAMOptions: map[string]string{},
+ }
+
+ cniJSON := make(map[string]interface{})
+ err := json.Unmarshal(conf.Bytes, &cniJSON)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to unmarshal network config %s", conf.Name)
+ }
+ if args, ok := cniJSON["args"]; ok {
+ if key, ok := args.(map[string]interface{}); ok {
+ // read network labels and options from the conf file
+ network.Labels = getNetworkArgsFromConfList(key, podmanLabelKey)
+ network.Options = getNetworkArgsFromConfList(key, podmanOptionsKey)
+ }
+ }
+
+ f, err := os.Stat(confPath)
+ if err != nil {
+ return nil, err
+ }
+ stat := f.Sys().(*syscall.Stat_t)
+ network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
+
+ firstPlugin := conf.Plugins[0]
+ network.Driver = firstPlugin.Network.Type
+
+ switch firstPlugin.Network.Type {
+ case types.BridgeNetworkDriver:
+ var bridge hostLocalBridge
+ err := json.Unmarshal(firstPlugin.Bytes, &bridge)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to unmarshal the bridge plugin config in %s", confPath)
+ }
+ network.NetworkInterface = bridge.BrName
+
+ // if isGateway is false we have an internal network
+ if !bridge.IsGW {
+ network.Internal = true
+ }
+
+ // set network options
+ if bridge.MTU != 0 {
+ network.Options["mtu"] = strconv.Itoa(bridge.MTU)
+ }
+ if bridge.Vlan != 0 {
+ network.Options["vlan"] = strconv.Itoa(bridge.Vlan)
+ }
+
+ err = convertIPAMConfToNetwork(&network, bridge.IPAM, confPath)
+ if err != nil {
+ return nil, err
+ }
+
+ case types.MacVLANNetworkDriver:
+ var macvlan macVLANConfig
+ err := json.Unmarshal(firstPlugin.Bytes, &macvlan)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to unmarshal the macvlan plugin config in %s", confPath)
+ }
+ network.NetworkInterface = macvlan.Master
+
+ // set network options
+ if macvlan.MTU != 0 {
+ network.Options["mtu"] = strconv.Itoa(macvlan.MTU)
+ }
+
+ err = convertIPAMConfToNetwork(&network, macvlan.IPAM, confPath)
+ if err != nil {
+ return nil, err
+ }
+
+ default:
+ // A warning would be good but users would get this warning everytime so keep this at info level.
+ logrus.Infof("unsupported CNI config type %s in %s, this network can still be used but inspect or list cannot show all information",
+ firstPlugin.Network.Type, confPath)
+ }
+
+ // check if the dnsname plugin is configured
+ network.DNSEnabled = findPluginByName(conf.Plugins, "dnsname")
+
+ return &network, nil
+}
+
+func findPluginByName(plugins []*libcni.NetworkConfig, name string) bool {
+ for _, plugin := range plugins {
+ if plugin.Network.Type == name {
+ return true
+ }
+ }
+ return false
+}
+
+// convertIPAMConfToNetwork converts A cni IPAMConfig to libpod network subnets.
+// It returns an array of subnets and an extra bool if dhcp is configured.
+func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath string) error {
+ if ipam.PluginType == types.DHCPIPAMDriver {
+ network.IPAMOptions["driver"] = types.DHCPIPAMDriver
+ return nil
+ }
+
+ if ipam.PluginType != types.HostLocalIPAMDriver {
+ return errors.Errorf("unsupported ipam plugin %s in %s", ipam.PluginType, confPath)
+ }
+
+ network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
+ for _, r := range ipam.Ranges {
+ for _, ipam := range r {
+ s := types.Subnet{}
+
+ // Do not use types.ParseCIDR() because we want the ip to be
+ // the network address and not a random ip in the sub.
+ _, sub, err := net.ParseCIDR(ipam.Subnet)
+ if err != nil {
+ return err
+ }
+ s.Subnet = types.IPNet{IPNet: *sub}
+
+ // gateway
+ var gateway net.IP
+ if ipam.Gateway != "" {
+ gateway = net.ParseIP(ipam.Gateway)
+ if gateway == nil {
+ return errors.Errorf("failed to parse gateway ip %s", ipam.Gateway)
+ }
+ // convert to 4 byte if ipv4
+ ipv4 := gateway.To4()
+ if ipv4 != nil {
+ gateway = ipv4
+ }
+ } else if !network.Internal {
+ // only add a gateway address if the network is not internal
+ gateway, err = util.FirstIPInSubnet(sub)
+ if err != nil {
+ return errors.Errorf("failed to get first ip in subnet %s", sub.String())
+ }
+ }
+ s.Gateway = gateway
+
+ var rangeStart net.IP
+ var rangeEnd net.IP
+ if ipam.RangeStart != "" {
+ rangeStart = net.ParseIP(ipam.RangeStart)
+ if rangeStart == nil {
+ return errors.Errorf("failed to parse range start ip %s", ipam.RangeStart)
+ }
+ }
+ if ipam.RangeEnd != "" {
+ rangeEnd = net.ParseIP(ipam.RangeEnd)
+ if rangeEnd == nil {
+ return errors.Errorf("failed to parse range end ip %s", ipam.RangeEnd)
+ }
+ }
+ if rangeStart != nil || rangeEnd != nil {
+ s.LeaseRange = &types.LeaseRange{}
+ s.LeaseRange.StartIP = rangeStart
+ s.LeaseRange.EndIP = rangeEnd
+ }
+ network.Subnets = append(network.Subnets, s)
+ }
+ }
+ return nil
+}
+
+// getNetworkArgsFromConfList returns the map of args in a conflist, argType should be labels or options
+func getNetworkArgsFromConfList(args map[string]interface{}, argType string) map[string]string {
+ if args, ok := args[argType]; ok {
+ if labels, ok := args.(map[string]interface{}); ok {
+ result := make(map[string]string, len(labels))
+ for k, v := range labels {
+ if v, ok := v.(string); ok {
+ result[k] = v
+ }
+ }
+ return result
+ }
+ }
+ return nil
+}
+
+// createCNIConfigListFromNetwork will create a cni config file from the given network.
+// It returns the cni config and the path to the file where the config was written.
+// Set writeToDisk to false to only add this network into memory.
+func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writeToDisk bool) (*libcni.NetworkConfigList, string, error) {
+ var (
+ routes []ipamRoute
+ ipamRanges [][]ipamLocalHostRangeConf
+ ipamConf ipamConfig
+ err error
+ )
+ if len(network.Subnets) > 0 {
+ for _, subnet := range network.Subnets {
+ route, err := newIPAMDefaultRoute(util.IsIPv6(subnet.Subnet.IP))
+ if err != nil {
+ return nil, "", err
+ }
+ routes = append(routes, route)
+ ipam := newIPAMLocalHostRange(subnet.Subnet, subnet.LeaseRange, subnet.Gateway)
+ ipamRanges = append(ipamRanges, []ipamLocalHostRangeConf{*ipam})
+ }
+ ipamConf = newIPAMHostLocalConf(routes, ipamRanges)
+ } else {
+ ipamConf = ipamConfig{PluginType: "dhcp"}
+ }
+
+ vlan := 0
+ mtu := 0
+ for k, v := range network.Options {
+ switch k {
+ case "mtu":
+ mtu, err = parseMTU(v)
+ if err != nil {
+ return nil, "", err
+ }
+
+ case "vlan":
+ vlan, err = parseVlan(v)
+ if err != nil {
+ return nil, "", err
+ }
+
+ default:
+ return nil, "", errors.Errorf("unsupported network option %s", k)
+ }
+ }
+
+ isGateway := true
+ ipMasq := true
+ if network.Internal {
+ isGateway = false
+ ipMasq = false
+ }
+ // create CNI plugin configuration
+ ncList := newNcList(network.Name, version.Current(), network.Labels, network.Options)
+ var plugins []interface{}
+
+ switch network.Driver {
+ case types.BridgeNetworkDriver:
+ bridge := newHostLocalBridge(network.NetworkInterface, isGateway, ipMasq, mtu, vlan, ipamConf)
+ plugins = append(plugins, bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin())
+ // if we find the dnsname plugin we add configuration for it
+ if hasDNSNamePlugin(n.cniPluginDirs) && network.DNSEnabled {
+ // 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 n.isMachine {
+ plugins = append(plugins, newPodmanMachinePlugin())
+ }
+
+ case types.MacVLANNetworkDriver:
+ plugins = append(plugins, newMacVLANPlugin(network.NetworkInterface, mtu, ipamConf))
+
+ default:
+ return nil, "", errors.Errorf("driver %q is not supported by cni", network.Driver)
+ }
+ ncList["plugins"] = plugins
+ b, err := json.MarshalIndent(ncList, "", " ")
+ if err != nil {
+ return nil, "", err
+ }
+ cniPathName := ""
+ if writeToDisk {
+ cniPathName = filepath.Join(n.cniConfigDir, network.Name+".conflist")
+ err = ioutil.WriteFile(cniPathName, b, 0644)
+ if err != nil {
+ return nil, "", err
+ }
+ f, err := os.Stat(cniPathName)
+ if err != nil {
+ return nil, "", err
+ }
+ stat := f.Sys().(*syscall.Stat_t)
+ network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
+ } else {
+ network.Created = time.Now()
+ }
+ config, err := libcni.ConfListFromBytes(b)
+ if err != nil {
+ return nil, "", err
+ }
+ return config, cniPathName, 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("mtu %d 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
+ }
+ v, err := strconv.Atoi(vlan)
+ if err != nil {
+ return 0, err
+ }
+ if v < 0 || v > 4094 {
+ return 0, errors.Errorf("vlan ID %d must be between 0 and 4094", v)
+ }
+ return v, nil
+}
+
+func convertSpecgenPortsToCNIPorts(ports []types.PortMapping) ([]cniPortMapEntry, error) {
+ cniPorts := make([]cniPortMapEntry, 0, len(ports))
+ for _, port := range ports {
+ if port.Protocol == "" {
+ return nil, errors.New("port protocol should not be empty")
+ }
+ protocols := strings.Split(port.Protocol, ",")
+
+ for _, protocol := range protocols {
+ if !pkgutil.StringInSlice(protocol, []string{"tcp", "udp", "sctp"}) {
+ return nil, errors.Errorf("unknown port protocol %s", protocol)
+ }
+ cniPort := cniPortMapEntry{
+ HostPort: int(port.HostPort),
+ ContainerPort: int(port.ContainerPort),
+ HostIP: port.HostIP,
+ Protocol: protocol,
+ }
+ cniPorts = append(cniPorts, cniPort)
+ for i := 1; i < int(port.Range); i++ {
+ cniPort := cniPortMapEntry{
+ HostPort: int(port.HostPort) + i,
+ ContainerPort: int(port.ContainerPort) + i,
+ HostIP: port.HostIP,
+ Protocol: protocol,
+ }
+ cniPorts = append(cniPorts, cniPort)
+ }
+ }
+ }
+ return cniPorts, nil
+}
diff --git a/libpod/network/cni/cni_exec.go b/libpod/network/cni/cni_exec.go
new file mode 100644
index 000000000..0aec3d4f1
--- /dev/null
+++ b/libpod/network/cni/cni_exec.go
@@ -0,0 +1,98 @@
+// Copyright 2016 CNI authors
+// Copyright 2021 Podman authors
+//
+// This code has been originally copied from github.com/containernetworking/cni
+// but has been changed to better fit the Podman use case.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build linux
+
+package cni
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/containernetworking/cni/pkg/invoke"
+ "github.com/containernetworking/cni/pkg/version"
+)
+
+type cniExec struct {
+ version.PluginDecoder
+}
+
+type cniPluginError struct {
+ plugin string
+ Code uint `json:"code"`
+ Msg string `json:"msg"`
+ Details string `json:"details,omitempty"`
+}
+
+// Error returns a nicely formatted error message for the cni plugin errors.
+func (e *cniPluginError) Error() string {
+ err := fmt.Sprintf("cni plugin %s failed", e.plugin)
+ if e.Msg != "" {
+ err = fmt.Sprintf("%s: %s", err, e.Msg)
+ } else if e.Code > 0 {
+ err = fmt.Sprintf("%s with error code %d", err, e.Code)
+ }
+ if e.Details != "" {
+ err = fmt.Sprintf("%s: %s", err, e.Details)
+ }
+ return err
+}
+
+// ExecPlugin execute the cni plugin. Returns the stdout of the plugin or an error.
+func (e *cniExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
+ stdout := &bytes.Buffer{}
+ stderr := &bytes.Buffer{}
+ c := exec.CommandContext(ctx, pluginPath)
+ c.Env = environ
+ c.Stdin = bytes.NewBuffer(stdinData)
+ c.Stdout = stdout
+ c.Stderr = stderr
+
+ err := c.Run()
+ if err != nil {
+ return nil, annotatePluginError(err, pluginPath, stdout.Bytes(), stderr.Bytes())
+ }
+ return stdout.Bytes(), nil
+}
+
+// annotatePluginError parses the common cni plugin error json.
+func annotatePluginError(err error, plugin string, stdout []byte, stderr []byte) error {
+ pluginName := filepath.Base(plugin)
+ emsg := cniPluginError{
+ plugin: pluginName,
+ }
+ if len(stdout) == 0 {
+ if len(stderr) == 0 {
+ emsg.Msg = err.Error()
+ } else {
+ emsg.Msg = string(stderr)
+ }
+ } else if perr := json.Unmarshal(stdout, &emsg); perr != nil {
+ emsg.Msg = fmt.Sprintf("failed to unmarshal error message %q: %v", string(stdout), perr)
+ }
+ return &emsg
+}
+
+// FindInPath finds the plugin in the given paths.
+func (e *cniExec) FindInPath(plugin string, paths []string) (string, error) {
+ return invoke.FindInPath(plugin, paths)
+}
diff --git a/libpod/network/cni/cni_suite_test.go b/libpod/network/cni/cni_suite_test.go
new file mode 100644
index 000000000..f98869c96
--- /dev/null
+++ b/libpod/network/cni/cni_suite_test.go
@@ -0,0 +1,53 @@
+// +build linux
+
+package cni_test
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/containers/podman/v3/libpod/network/cni"
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/test/utils"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var cniPluginDirs = []string{
+ "/usr/libexec/cni",
+ "/usr/lib/cni",
+ "/usr/local/lib/cni",
+ "/opt/cni/bin",
+}
+
+func TestCni(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "CNI Suite")
+}
+
+func getNetworkInterface(cniConfDir string, machine bool) (types.ContainerNetwork, error) {
+ return cni.NewCNINetworkInterface(cni.InitConfig{
+ CNIConfigDir: cniConfDir,
+ CNIPluginDirs: cniPluginDirs,
+ IsMachine: machine,
+ LockFile: filepath.Join(cniConfDir, "cni.lock"),
+ })
+}
+
+func SkipIfNoDnsname() {
+ for _, path := range cniPluginDirs {
+ f, err := os.Stat(filepath.Join(path, "dnsname"))
+ if err == nil && f.Mode().IsRegular() {
+ return
+ }
+ }
+ Skip("dnsname cni plugin needs to be installed for this test")
+}
+
+func SkipIfNotFedora(msg string) {
+ info := utils.GetHostDistributionInfo()
+ if info.Distribution != "fedora" {
+ Skip("Test can only run on Fedora: " + msg)
+ }
+}
diff --git a/libpod/network/cni/cni_types.go b/libpod/network/cni/cni_types.go
new file mode 100644
index 000000000..91fd1c27b
--- /dev/null
+++ b/libpod/network/cni/cni_types.go
@@ -0,0 +1,292 @@
+// +build linux
+
+package cni
+
+import (
+ "net"
+ "os"
+ "path/filepath"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+)
+
+const (
+ defaultIPv4Route = "0.0.0.0/0"
+ defaultIPv6Route = "::/0"
+ // defaultPodmanDomainName is used for the dnsname plugin to define
+ // a localized domain name for a created network
+ defaultPodmanDomainName = "dns.podman"
+
+ // cniDeviceName is the default name for a new bridge, it should be suffixed with an integer
+ cniDeviceName = "cni-podman"
+
+ // podmanLabelKey key used to store the podman network label in a cni config
+ podmanLabelKey = "podman_labels"
+
+ // podmanOptionsKey key used to store the podman network options in a cni config
+ podmanOptionsKey = "podman_options"
+)
+
+// cniPortMapEntry struct is used by the portmap plugin
+// https://github.com/containernetworking/plugins/blob/649e0181fe7b3a61e708f3e4249a798f57f25cc5/plugins/meta/portmap/main.go#L43-L50
+type cniPortMapEntry struct {
+ HostPort int `json:"hostPort"`
+ ContainerPort int `json:"containerPort"`
+ Protocol string `json:"protocol"`
+ HostIP string `json:"hostIP,omitempty"`
+}
+
+// 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"`
+ Capabilities map[string]bool `json:"capabilities"`
+}
+
+// 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"`
+}
+
+// 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"`
+}
+
+// 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"`
+ Capabilities map[string]bool `json:"capabilities"`
+}
+
+// firewallConfig describes the firewall plugin
+type firewallConfig struct {
+ PluginType string `json:"type"`
+ Backend string `json:"backend"`
+}
+
+// tuningConfig describes the tuning plugin
+type tuningConfig struct {
+ PluginType string `json:"type"`
+}
+
+// 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"`
+}
+
+// ncList describes a generic map
+type ncList map[string]interface{}
+
+// newNcList creates a generic map of values with string
+// keys and adds in version and network name
+func newNcList(name, version string, labels, options map[string]string) ncList {
+ n := ncList{}
+ n["cniVersion"] = version
+ n["name"] = name
+ args := map[string]map[string]string{}
+ if len(labels) > 0 {
+ args[podmanLabelKey] = labels
+ }
+ if len(options) > 0 {
+ args[podmanOptionsKey] = options
+ }
+ if len(args) > 0 {
+ n["args"] = args
+ }
+ return n
+}
+
+// newHostLocalBridge creates a new LocalBridge for host-local
+func newHostLocalBridge(name string, isGateWay, ipMasq bool, mtu int, vlan int, ipamConf ipamConfig) *hostLocalBridge {
+ caps := make(map[string]bool)
+ caps["ips"] = true
+ bridge := hostLocalBridge{
+ PluginType: "bridge",
+ BrName: name,
+ IsGW: isGateWay,
+ IPMasq: ipMasq,
+ MTU: mtu,
+ HairpinMode: true,
+ Vlan: vlan,
+ IPAM: ipamConf,
+ }
+ // if we use host-local set the ips cap to ensure we can set static ips via runtime config
+ if ipamConf.PluginType == types.HostLocalIPAMDriver {
+ bridge.Capabilities = caps
+ }
+ return &bridge
+}
+
+// newIPAMHostLocalConf creates a new IPAMHostLocal configuration
+func newIPAMHostLocalConf(routes []ipamRoute, ipamRanges [][]ipamLocalHostRangeConf) ipamConfig {
+ ipamConf := ipamConfig{
+ PluginType: "host-local",
+ Routes: routes,
+ }
+
+ ipamConf.Ranges = ipamRanges
+ return ipamConf
+}
+
+// newIPAMLocalHostRange create a new IPAM range
+func newIPAMLocalHostRange(subnet types.IPNet, leaseRange *types.LeaseRange, gw net.IP) *ipamLocalHostRangeConf {
+ hostRange := &ipamLocalHostRangeConf{
+ Subnet: subnet.String(),
+ }
+
+ // an user provided a range, we add it here
+ if leaseRange != nil {
+ if leaseRange.StartIP != nil {
+ hostRange.RangeStart = leaseRange.StartIP.String()
+ }
+ if leaseRange.EndIP != nil {
+ hostRange.RangeStart = leaseRange.EndIP.String()
+ }
+ }
+
+ if gw != nil {
+ hostRange.Gateway = gw.String()
+ }
+ return hostRange
+}
+
+// newIPAMRoute creates a new IPAM route configuration
+// nolint:interfacer
+func newIPAMRoute(r *net.IPNet) ipamRoute {
+ 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, mtu int, ipam ipamConfig) macVLANConfig {
+ m := macVLANConfig{
+ PluginType: "macvlan",
+ IPAM: ipam,
+ }
+ 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
+ }
+ caps := make(map[string]bool)
+ caps["ips"] = true
+ // if we use host-local set the ips cap to ensure we can set static ips via runtime config
+ if ipam.PluginType == types.HostLocalIPAMDriver {
+ m.Capabilities = caps
+ }
+ return m
+}
+
+func newPodmanMachinePlugin() podmanMachineConfig {
+ caps := make(map[string]bool, 1)
+ caps["portMappings"] = true
+ return podmanMachineConfig{
+ PluginType: "podman-machine",
+ Capabilities: caps,
+ }
+}
diff --git a/libpod/network/cni/config.go b/libpod/network/cni/config.go
new file mode 100644
index 000000000..ee203f80d
--- /dev/null
+++ b/libpod/network/cni/config.go
@@ -0,0 +1,313 @@
+// +build linux
+
+package cni
+
+import (
+ "net"
+ "os"
+
+ "github.com/containers/podman/v3/libpod/define"
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/libpod/network/util"
+ pkgutil "github.com/containers/podman/v3/pkg/util"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "github.com/vishvananda/netlink"
+)
+
+// NetworkCreate will take a partial filled Network and fill the
+// missing fields. It creates the Network and returns the full Network.
+func (n *cniNetwork) NetworkCreate(net types.Network) (types.Network, error) {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return types.Network{}, err
+ }
+ network, err := n.networkCreate(net, true)
+ if err != nil {
+ return types.Network{}, err
+ }
+ // add the new network to the map
+ n.networks[network.libpodNet.Name] = network
+ return *network.libpodNet, nil
+}
+
+func (n *cniNetwork) networkCreate(net types.Network, writeToDisk bool) (*network, error) {
+ // if no driver is set use the default one
+ if net.Driver == "" {
+ net.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 != "" {
+ return nil, errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create")
+ }
+
+ if net.Labels == nil {
+ net.Labels = map[string]string{}
+ }
+ if net.Options == nil {
+ net.Options = map[string]string{}
+ }
+ if net.IPAMOptions == nil {
+ net.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 _, ok := n.networks[net.Name]; ok {
+ return nil, errors.Wrapf(define.ErrNetworkExists, "network name %s already used", net.Name)
+ }
+ } else {
+ name, err = n.getFreeDeviceName()
+ if err != nil {
+ return nil, err
+ }
+ net.Name = name
+ }
+
+ switch net.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
+ }
+ err = n.createBridge(&net)
+ if err != nil {
+ return nil, err
+ }
+ case types.MacVLANNetworkDriver:
+ err = createMacVLAN(&net)
+ if err != nil {
+ return nil, err
+ }
+ default:
+ return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", net.Driver)
+ }
+
+ for i := range net.Subnets {
+ err := validateSubnet(&net.Subnets[i], !net.Internal)
+ if err != nil {
+ return nil, err
+ }
+ if util.IsIPv6(net.Subnets[i].Subnet.IP) {
+ net.IPv6Enabled = true
+ }
+ }
+
+ // generate the network ID
+ net.ID = getNetworkIDFromName(net.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
+ }
+
+ cniConf, path, err := n.createCNIConfigListFromNetwork(&net, writeToDisk)
+ if err != nil {
+ return nil, err
+ }
+ return &network{cniNet: cniConf, libpodNet: &net, filename: path}, nil
+}
+
+// NetworkRemove will remove the Network with the given name or ID.
+// It does not ensure that the network is unused.
+func (n *cniNetwork) NetworkRemove(nameOrID string) error {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return err
+ }
+
+ network, err := n.getNetwork(nameOrID)
+ if err != nil {
+ return err
+ }
+
+ // Removing the default network is not allowed.
+ if network.libpodNet.Name == n.defaultNetwork {
+ return errors.Errorf("default network %s cannot be removed", n.defaultNetwork)
+ }
+
+ // Remove the bridge network interface on the host.
+ if network.libpodNet.Driver == types.BridgeNetworkDriver {
+ link, err := netlink.LinkByName(network.libpodNet.NetworkInterface)
+ if err == nil {
+ err = netlink.LinkDel(link)
+ // only log the error, it is not fatal
+ if err != nil {
+ logrus.Infof("failed to remove network interface %s: %v", network.libpodNet.NetworkInterface, err)
+ }
+ }
+ }
+
+ file := network.filename
+ delete(n.networks, network.libpodNet.Name)
+
+ return os.Remove(file)
+}
+
+// NetworkList will return all known Networks. Optionally you can
+// supply a list of filter functions. Only if a network matches all
+// functions it is returned.
+func (n *cniNetwork) NetworkList(filters ...types.FilterFunc) ([]types.Network, error) {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return nil, err
+ }
+
+ networks := make([]types.Network, 0, len(n.networks))
+outer:
+ for _, net := range n.networks {
+ for _, filter := range filters {
+ // All filters have to match, if one does not match we can skip to the next network.
+ if !filter(*net.libpodNet) {
+ continue outer
+ }
+ }
+ networks = append(networks, *net.libpodNet)
+ }
+ return networks, nil
+}
+
+// NetworkInspect will return the Network with the given name or ID.
+func (n *cniNetwork) NetworkInspect(nameOrID string) (types.Network, error) {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return types.Network{}, err
+ }
+
+ network, err := n.getNetwork(nameOrID)
+ if err != nil {
+ return types.Network{}, err
+ }
+ return *network.libpodNet, nil
+}
+
+func createMacVLAN(network *types.Network) error {
+ if network.Internal {
+ return errors.New("internal is not supported with macvlan")
+ }
+ if network.NetworkInterface != "" {
+ interfaceNames, err := util.GetLiveNetworkNames()
+ if err != nil {
+ return err
+ }
+ if !pkgutil.StringInSlice(network.NetworkInterface, interfaceNames) {
+ return errors.Errorf("parent interface %s does not exists", network.NetworkInterface)
+ }
+ }
+ if len(network.Subnets) == 0 {
+ network.IPAMOptions["driver"] = types.DHCPIPAMDriver
+ } else {
+ network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
+ }
+ return nil
+}
+
+func (n *cniNetwork) createBridge(network *types.Network) error {
+ if network.NetworkInterface != "" {
+ bridges := n.getBridgeInterfaceNames()
+ if pkgutil.StringInSlice(network.NetworkInterface, bridges) {
+ return errors.Errorf("bridge name %s already in use", network.NetworkInterface)
+ }
+ if !define.NameRegex.MatchString(network.NetworkInterface) {
+ return errors.Wrapf(define.RegexError, "bridge name %s invalid", network.NetworkInterface)
+ }
+ } else {
+ var err error
+ network.NetworkInterface, err = n.getFreeDeviceName()
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(network.Subnets) == 0 {
+ freeSubnet, err := n.getFreeIPv4NetworkSubnet()
+ if err != nil {
+ return err
+ }
+ network.Subnets = append(network.Subnets, *freeSubnet)
+ }
+ // ipv6 enabled means dual stack, check if we already have
+ // a ipv4 or ipv6 subnet and add one if not.
+ if network.IPv6Enabled {
+ ipv4 := false
+ ipv6 := false
+ for _, subnet := range network.Subnets {
+ if util.IsIPv6(subnet.Subnet.IP) {
+ ipv6 = true
+ }
+ if util.IsIPv4(subnet.Subnet.IP) {
+ ipv4 = true
+ }
+ }
+ if !ipv4 {
+ freeSubnet, err := n.getFreeIPv4NetworkSubnet()
+ if err != nil {
+ return err
+ }
+ network.Subnets = append(network.Subnets, *freeSubnet)
+ }
+ if !ipv6 {
+ freeSubnet, err := n.getFreeIPv6NetworkSubnet()
+ if err != nil {
+ return err
+ }
+ network.Subnets = append(network.Subnets, *freeSubnet)
+ }
+ }
+ network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
+ return nil
+}
+
+// validateSubnet will validate a given Subnet. It checks if the
+// 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 {
+ if s == nil {
+ return errors.New("subnet 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.
+ _, net, err := net.ParseCIDR(s.Subnet.String())
+ if err != nil {
+ return errors.Wrap(err, "subnet invalid")
+ }
+ s.Subnet = types.IPNet{IPNet: *net}
+ if s.Gateway != nil {
+ if !s.Subnet.Contains(s.Gateway) {
+ return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet)
+ }
+ } else if addGateway {
+ ip, err := util.FirstIPInSubnet(net)
+ if err != nil {
+ return err
+ }
+ s.Gateway = ip
+ }
+ if s.LeaseRange != nil {
+ if s.LeaseRange.StartIP != nil && !s.Subnet.Contains(s.LeaseRange.StartIP) {
+ return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet)
+ }
+ if s.LeaseRange.EndIP != nil && !s.Subnet.Contains(s.LeaseRange.EndIP) {
+ return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet)
+ }
+ }
+ return nil
+}
diff --git a/libpod/network/cni/config_test.go b/libpod/network/cni/config_test.go
new file mode 100644
index 000000000..f67402657
--- /dev/null
+++ b/libpod/network/cni/config_test.go
@@ -0,0 +1,1241 @@
+// +build linux
+
+package cni_test
+
+import (
+ "bytes"
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+ "time"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ gomegaTypes "github.com/onsi/gomega/types"
+ "github.com/sirupsen/logrus"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/libpod/network/util"
+)
+
+var _ = Describe("Config", func() {
+ var (
+ libpodNet types.ContainerNetwork
+ cniConfDir string
+ logBuffer bytes.Buffer
+ )
+
+ BeforeEach(func() {
+ var err error
+ cniConfDir, err = ioutil.TempDir("", "podman_cni_test")
+ if err != nil {
+ Fail("Failed to create tmpdir")
+
+ }
+ logBuffer = bytes.Buffer{}
+ logrus.SetOutput(&logBuffer)
+ })
+
+ JustBeforeEach(func() {
+ var err error
+ libpodNet, err = getNetworkInterface(cniConfDir, false)
+ if err != nil {
+ Fail("Failed to create NewCNINetworkInterface")
+ }
+ })
+
+ AfterEach(func() {
+ os.RemoveAll(cniConfDir)
+ })
+
+ Context("basic network config tests", func() {
+
+ It("check default network config exists", func() {
+ networks, err := libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(1))
+ Expect(networks[0].Name).To(Equal("podman"))
+ Expect(networks[0].Driver).To(Equal("bridge"))
+ Expect(networks[0].NetworkInterface).To(Equal("cni-podman0"))
+ Expect(networks[0].Created.Before(time.Now())).To(BeTrue())
+ Expect(networks[0].Subnets).To(HaveLen(1))
+ Expect(networks[0].Subnets[0].Subnet.String()).To(Equal("10.88.0.0/16"))
+ Expect(networks[0].Subnets[0].Gateway.String()).To(Equal("10.88.0.1"))
+ Expect(networks[0].Subnets[0].LeaseRange).To(BeNil())
+ Expect(networks[0].IPAMOptions).To(HaveKeyWithValue("driver", "host-local"))
+ Expect(networks[0].Options).To(BeEmpty())
+ Expect(networks[0].Labels).To(BeEmpty())
+ Expect(networks[0].DNSEnabled).To(BeFalse())
+ Expect(networks[0].Internal).To(BeFalse())
+ })
+
+ It("basic network create, inspect and remove", func() {
+ // Because we get the time from the file create timestamp there is small precision
+ // loss so lets remove 500 milliseconds to make sure this test does not flake.
+ now := time.Now().Add(-500 * time.Millisecond)
+ network := types.Network{}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ path := filepath.Join(cniConfDir, network1.Name+".conflist")
+ Expect(path).To(BeARegularFile())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Labels).To(BeEmpty())
+ Expect(network1.Options).To(BeEmpty())
+ Expect(network1.IPAMOptions).ToNot(BeEmpty())
+ Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "host-local"))
+ Expect(network1.Created.After(now)).To(BeTrue())
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.89.0.0/24"))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.89.0.1"))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.DNSEnabled).To(BeFalse())
+ Expect(network1.Internal).To(BeFalse())
+
+ // inspect by name
+ network2, err := libpodNet.NetworkInspect(network1.Name)
+ Expect(err).To(BeNil())
+ Expect(network2).To(Equal(network1))
+
+ // inspect by ID
+ network2, err = libpodNet.NetworkInspect(network1.ID)
+ Expect(err).To(BeNil())
+ Expect(network2).To(Equal(network1))
+
+ // inspect by partial ID
+ network2, err = libpodNet.NetworkInspect(network1.ID[:10])
+ Expect(err).To(BeNil())
+ Expect(network2).To(Equal(network1))
+
+ // create a new interface to force a config load from disk
+ libpodNet, err = getNetworkInterface(cniConfDir, false)
+ Expect(err).To(BeNil())
+
+ network2, err = libpodNet.NetworkInspect(network1.Name)
+ Expect(err).To(BeNil())
+ Expect(network2).To(Equal(network1))
+
+ err = libpodNet.NetworkRemove(network1.Name)
+ Expect(err).To(BeNil())
+ Expect(path).ToNot(BeARegularFile())
+
+ _, err = libpodNet.NetworkInspect(network1.Name)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("network not found"))
+ })
+
+ It("create two networks", func() {
+ network := types.Network{}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.Subnets).To(HaveLen(1))
+
+ network = types.Network{}
+ network2, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network2.Name).ToNot(Equal(network1.Name))
+ Expect(network2.ID).ToNot(Equal(network1.ID))
+ Expect(network2.NetworkInterface).ToNot(Equal(network1.NetworkInterface))
+ Expect(network2.Subnets).To(HaveLen(1))
+ Expect(network2.Subnets[0].Subnet.Contains(network1.Subnets[0].Subnet.IP)).To(BeFalse())
+ })
+
+ It("create bridge config", func() {
+ network := types.Network{Driver: "bridge"}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(filepath.Join(cniConfDir, network1.Name+".conflist")).To(BeARegularFile())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Labels).To(BeEmpty())
+ Expect(network1.Options).To(BeEmpty())
+ Expect(network1.IPAMOptions).ToNot(BeEmpty())
+ Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "host-local"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.89.0.0/24"))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.89.0.1"))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.DNSEnabled).To(BeFalse())
+ Expect(network1.Internal).To(BeFalse())
+ })
+
+ It("create bridge with same name should fail", func() {
+ network := types.Network{
+ Driver: "bridge",
+ NetworkInterface: "cni-podman2",
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).To(Equal("cni-podman2"))
+ Expect(network1.Driver).To(Equal("bridge"))
+
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("bridge name cni-podman2 already in use"))
+ })
+
+ It("create macvlan config", func() {
+ network := types.Network{Driver: "macvlan"}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(filepath.Join(cniConfDir, network1.Name+".conflist")).To(BeARegularFile())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("macvlan"))
+ Expect(network1.Labels).To(BeEmpty())
+ Expect(network1.Options).To(BeEmpty())
+ Expect(network1.IPAMOptions).ToNot(BeEmpty())
+ Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "dhcp"))
+ Expect(network1.Subnets).To(HaveLen(0))
+ Expect(network1.DNSEnabled).To(BeFalse())
+ Expect(network1.Internal).To(BeFalse())
+ })
+
+ It("create macvlan config with device", func() {
+ network := types.Network{
+ Driver: "macvlan",
+ NetworkInterface: "lo",
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ path := filepath.Join(cniConfDir, network1.Name+".conflist")
+ Expect(path).To(BeARegularFile())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("macvlan"))
+ Expect(network1.Labels).To(BeEmpty())
+ Expect(network1.Options).To(BeEmpty())
+ Expect(network1.Subnets).To(HaveLen(0))
+ Expect(network1.DNSEnabled).To(BeFalse())
+ Expect(network1.Internal).To(BeFalse())
+ Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "dhcp"))
+ grepInFile(path, `"type": "macvlan"`)
+ grepInFile(path, `"master": "lo"`)
+ grepInFile(path, `"type": "dhcp"`)
+ })
+
+ It("create macvlan config with subnet", func() {
+ subnet := "10.1.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+ network := types.Network{
+ Driver: "macvlan",
+ Subnets: []types.Subnet{
+ {Subnet: n},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ path := filepath.Join(cniConfDir, network1.Name+".conflist")
+ Expect(path).To(BeARegularFile())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("macvlan"))
+ Expect(network1.Labels).To(BeEmpty())
+ Expect(network1.Options).To(BeEmpty())
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.1.0.1"))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.DNSEnabled).To(BeFalse())
+ Expect(network1.Internal).To(BeFalse())
+ Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "host-local"))
+ grepInFile(path, `"type": "host-local"`)
+ })
+
+ It("create macvlan config with invalid device", func() {
+ network := types.Network{
+ Driver: "macvlan",
+ NetworkInterface: "idonotexists",
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("parent interface idonotexists does not exists"))
+ })
+
+ It("create macvlan config with internal should fail", func() {
+ network := types.Network{
+ Driver: "macvlan",
+ Internal: true,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("internal is not supported with macvlan"))
+ })
+
+ It("create bridge with subnet", func() {
+ subnet := "10.0.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1"))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 subnet", func() {
+ subnet := "fdcc::/64"
+ n, _ := types.ParseCIDR(subnet)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.IPv6Enabled).To(BeTrue())
+ Expect(network1.Subnets).To(HaveLen(1))
+ 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())
+ })
+
+ It("create bridge with ipv6 enabled", func() {
+ network := types.Network{
+ Driver: "bridge",
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(2))
+ Expect(network1.Subnets[0].Subnet.String()).To(ContainSubstring(".0/24"))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring("::/64"))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 enabled and ipv4 subnet", func() {
+ subnet := "10.100.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n},
+ },
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(2))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring("::/64"))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 enabled and ipv6 subnet", func() {
+ subnet := "fd66::/64"
+ n, _ := types.ParseCIDR(subnet)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n},
+ },
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(2))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring(".0/24"))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 enabled and ipv4+ipv6 subnet", func() {
+ subnet1 := "10.100.0.0/24"
+ n1, _ := types.ParseCIDR(subnet1)
+ subnet2 := "fd66::/64"
+ n2, _ := types.ParseCIDR(subnet2)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n1}, {Subnet: n2},
+ },
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(2))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet1))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(Equal(subnet2))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 enabled and two ipv4 subnets", func() {
+ subnet1 := "10.100.0.0/24"
+ n1, _ := types.ParseCIDR(subnet1)
+ subnet2 := "10.200.0.0/24"
+ n2, _ := types.ParseCIDR(subnet2)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n1}, {Subnet: n2},
+ },
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(3))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet1))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(Equal(subnet2))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[2].Subnet.String()).To(ContainSubstring("::/64"))
+ Expect(network1.Subnets[2].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[2].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with subnet and gateway", func() {
+ subnet := "10.0.0.5/24"
+ n, _ := types.ParseCIDR(subnet)
+ gateway := "10.0.0.50"
+ g := net.ParseIP(gateway)
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, Gateway: g},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.0.0.0/24"))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal(gateway))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with subnet and gateway not in the same subnet", func() {
+ subnet := "10.0.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+ gateway := "10.10.0.50"
+ g := net.ParseIP(gateway)
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, Gateway: g},
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("not in subnet"))
+ })
+
+ It("create bridge with subnet and lease range", func() {
+ subnet := "10.0.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+ startIP := "10.0.0.10"
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ StartIP: net.ParseIP(startIP),
+ }},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1"))
+ Expect(network1.Subnets[0].LeaseRange.StartIP.String()).To(Equal(startIP))
+
+ endIP := "10.0.0.10"
+ network = types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ EndIP: net.ParseIP(endIP),
+ }},
+ },
+ }
+ network1, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(filepath.Join(cniConfDir, network1.Name+".conflist")).To(BeARegularFile())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1"))
+ Expect(network1.Subnets[0].LeaseRange.EndIP.String()).To(Equal(endIP))
+
+ network = types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ StartIP: net.ParseIP(startIP),
+ EndIP: net.ParseIP(endIP),
+ }},
+ },
+ }
+ network1, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1"))
+ Expect(network1.Subnets[0].LeaseRange.StartIP.String()).To(Equal(startIP))
+ Expect(network1.Subnets[0].LeaseRange.EndIP.String()).To(Equal(endIP))
+ })
+
+ It("create bridge with subnet and invalid lease range", func() {
+ subnet := "10.0.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+ startIP := "10.0.1.2"
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ StartIP: net.ParseIP(startIP),
+ }},
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("not in subnet"))
+
+ endIP := "10.1.1.1"
+ network = types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ EndIP: net.ParseIP(endIP),
+ }},
+ },
+ }
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("not in subnet"))
+ })
+
+ It("create bridge with broken subnet", func() {
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: types.IPNet{}},
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("subnet invalid"))
+ })
+
+ It("create network with name", func() {
+ name := "myname"
+ network := types.Network{
+ Name: name,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).To(Equal(name))
+ Expect(network1.NetworkInterface).ToNot(Equal(name))
+ Expect(network1.Driver).To(Equal("bridge"))
+ })
+
+ It("create network with invalid name", func() {
+ name := "myname@some"
+ network := types.Network{
+ Name: name,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("create network with name", func() {
+ name := "myname"
+ network := types.Network{
+ Name: name,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).To(Equal(name))
+ Expect(network1.NetworkInterface).ToNot(Equal(name))
+ Expect(network1.Driver).To(Equal("bridge"))
+ })
+
+ It("create network with invalid name", func() {
+ name := "myname@some"
+ network := types.Network{
+ Name: name,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("create network with interface name", func() {
+ name := "myname"
+ network := types.Network{
+ NetworkInterface: name,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(Equal(name))
+ Expect(network1.NetworkInterface).To(Equal(name))
+ Expect(network1.Driver).To(Equal("bridge"))
+ })
+
+ It("create network with invalid interface name", func() {
+ name := "myname@some"
+ network := types.Network{
+ NetworkInterface: name,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("create network with ID should fail", func() {
+ id := "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"
+ network := types.Network{
+ ID: id,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("ID can not be set for network create"))
+ })
+
+ It("create bridge with dns", func() {
+ network := types.Network{
+ Driver: "bridge",
+ DNSEnabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.DNSEnabled).To(BeTrue())
+ path := filepath.Join(cniConfDir, network1.Name+".conflist")
+ Expect(path).To(BeARegularFile())
+ grepInFile(path, `"type": "dnsname"`)
+ })
+
+ It("create bridge with internal", func() {
+ network := types.Network{
+ Driver: "bridge",
+ Internal: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).ToNot(BeEmpty())
+ Expect(network1.Subnets[0].Gateway).To(BeNil())
+ Expect(network1.Internal).To(BeTrue())
+ })
+
+ It("create network with labels", func() {
+ network := types.Network{
+ Labels: map[string]string{
+ "key": "value",
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Labels).ToNot(BeNil())
+ Expect(network1.Labels).To(ContainElement("value"))
+ })
+
+ It("create network with mtu option", func() {
+ network := types.Network{
+ Options: map[string]string{
+ "mtu": "1500",
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Options).ToNot(BeNil())
+ path := filepath.Join(cniConfDir, network1.Name+".conflist")
+ Expect(path).To(BeARegularFile())
+ grepInFile(path, `"mtu": 1500,`)
+ Expect(network1.Options).To(HaveKeyWithValue("mtu", "1500"))
+ })
+
+ It("create network with invalid mtu option", func() {
+ network := types.Network{
+ Options: map[string]string{
+ "mtu": "abc",
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(`parsing "abc": invalid syntax`))
+
+ network = types.Network{
+ Options: map[string]string{
+ "mtu": "-1",
+ },
+ }
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(`mtu -1 is less than zero`))
+ })
+
+ It("create macvlan network with mtu option", func() {
+ network := types.Network{
+ Driver: "macvlan",
+ Options: map[string]string{
+ "mtu": "1500",
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("macvlan"))
+ Expect(network1.Options).ToNot(BeNil())
+ path := filepath.Join(cniConfDir, network1.Name+".conflist")
+ Expect(path).To(BeARegularFile())
+ grepInFile(path, `"mtu": 1500`)
+ Expect(network1.Options).To(HaveKeyWithValue("mtu", "1500"))
+ })
+
+ It("create network with vlan option", func() {
+ network := types.Network{
+ Options: map[string]string{
+ "vlan": "5",
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Options).ToNot(BeNil())
+ path := filepath.Join(cniConfDir, network1.Name+".conflist")
+ Expect(path).To(BeARegularFile())
+ grepInFile(path, `"vlan": 5,`)
+ Expect(network1.Options).To(HaveKeyWithValue("vlan", "5"))
+ })
+
+ It("create network with invalid vlan option", func() {
+ network := types.Network{
+ Options: map[string]string{
+ "vlan": "abc",
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(`parsing "abc": invalid syntax`))
+
+ network = types.Network{
+ Options: map[string]string{
+ "vlan": "-1",
+ },
+ }
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(`vlan ID -1 must be between 0 and 4094`))
+ })
+
+ It("network create unsupported option", func() {
+ network := types.Network{Options: map[string]string{
+ "someopt": "",
+ }}
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("unsupported network option someopt"))
+ })
+
+ It("network create unsupported driver", func() {
+ network := types.Network{
+ Driver: "someDriver",
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("unsupported driver someDriver"))
+ })
+
+ It("network create internal and dns", func() {
+ network := types.Network{
+ Driver: "bridge",
+ Internal: true,
+ DNSEnabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).ToNot(BeEmpty())
+ Expect(network1.Subnets[0].Gateway).To(BeNil())
+ Expect(network1.Internal).To(BeTrue())
+ // internal and dns does not work, dns should be disabled
+ Expect(network1.DNSEnabled).To(BeFalse())
+ logString := logBuffer.String()
+ Expect(logString).To(ContainSubstring("dnsname and internal networks are incompatible"))
+ })
+
+ It("create config with podman machine plugin", func() {
+ libpodNet, err := getNetworkInterface(cniConfDir, true)
+ Expect(err).To(BeNil())
+
+ network := types.Network{}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ path := filepath.Join(cniConfDir, network1.Name+".conflist")
+ Expect(path).To(BeARegularFile())
+ grepInFile(path, `"type": "podman-machine",`)
+ })
+
+ It("network inspect partial ID", func() {
+ network := types.Network{Name: "net4"}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.ID).To(Equal("b44b7426c006839e7fe6f15d1faf64db58079d5233cba09b43be2257c1652cf5"))
+ network = types.Network{Name: "net5"}
+ network1, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.ID).To(Equal("b67e86fb039828ad686aa13667975b9e51f192eb617044faf06cded9d31602af"))
+
+ // Note ID is the sha256 from the name
+ // both net4 and net5 have an ID starting with b...
+ _, err = libpodNet.NetworkInspect("b")
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("more than one result for network ID"))
+ })
+
+ It("network create two with same name", func() {
+ network := types.Network{Name: "net"}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).To(Equal("net"))
+ network = types.Network{Name: "net"}
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("network name net already used"))
+ })
+
+ It("remove default network config should fail", func() {
+ err := libpodNet.NetworkRemove("podman")
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("default network podman cannot be removed"))
+
+ network, err := libpodNet.NetworkInspect("podman")
+ Expect(err).To(BeNil())
+ err = libpodNet.NetworkRemove(network.ID)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("default network podman cannot be removed"))
+ })
+
+ })
+
+ Context("network load valid existing ones", func() {
+
+ BeforeEach(func() {
+ dir := "testfiles/valid"
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ Fail("Failed to read test directory")
+ }
+ for _, file := range files {
+ filename := file.Name()
+ data, err := ioutil.ReadFile(filepath.Join(dir, filename))
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ err = ioutil.WriteFile(filepath.Join(cniConfDir, filename), data, 0700)
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ }
+ })
+
+ It("load networks from disk", func() {
+ nets, err := libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(nets).To(HaveLen(9))
+ // test the we do not show logrus warnings/errors
+ logString := logBuffer.String()
+ Expect(logString).To(BeEmpty())
+ })
+
+ It("change network struct fields should not affect network struct in the backend", func() {
+ nets, err := libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(nets).To(HaveLen(9))
+
+ nets[0].Name = "myname"
+ nets, err = libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(nets).To(HaveLen(9))
+ Expect(nets).ToNot(ContainElement(HaveNetworkName("myname")))
+
+ network, err := libpodNet.NetworkInspect("bridge")
+ Expect(err).To(BeNil())
+ network.NetworkInterface = "abc"
+
+ network, err = libpodNet.NetworkInspect("bridge")
+ Expect(err).To(BeNil())
+ Expect(network.NetworkInterface).ToNot(Equal("abc"))
+ })
+
+ It("bridge network", func() {
+ network, err := libpodNet.NetworkInspect("bridge")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("bridge"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("cni-podman9"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.8.0/24"))
+ Expect(network.Subnets[0].Gateway.String()).To(Equal("10.89.8.1"))
+ Expect(network.Subnets[0].LeaseRange).ToNot(BeNil())
+ Expect(network.Subnets[0].LeaseRange.StartIP.String()).To(Equal("10.89.8.20"))
+ Expect(network.Subnets[0].LeaseRange.EndIP.String()).To(Equal("10.89.8.50"))
+ Expect(network.Internal).To(BeFalse())
+ })
+
+ It("macvlan network", func() {
+ network, err := libpodNet.NetworkInspect("macvlan")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("macvlan"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("lo"))
+ Expect(network.Driver).To(Equal("macvlan"))
+ Expect(network.Subnets).To(HaveLen(0))
+ // DHCP
+ })
+
+ It("internal network", func() {
+ network, err := libpodNet.NetworkInspect("internal")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("internal"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("cni-podman8"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.7.0/24"))
+ Expect(network.Subnets[0].Gateway).To(BeNil())
+ Expect(network.Internal).To(BeTrue())
+ })
+
+ It("bridge network with mtu", func() {
+ network, err := libpodNet.NetworkInspect("mtu")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("mtu"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("cni-podman13"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.11.0/24"))
+ Expect(network.Subnets[0].Gateway.String()).To(Equal("10.89.11.1"))
+ Expect(network.Internal).To(BeFalse())
+ Expect(network.Options).To(HaveLen(1))
+ Expect(network.Options).To(HaveKeyWithValue("mtu", "1500"))
+ })
+
+ It("macvlan network with mtu", func() {
+ network, err := libpodNet.NetworkInspect("macvlan_mtu")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("macvlan_mtu"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("lo"))
+ Expect(network.Driver).To(Equal("macvlan"))
+ Expect(network.Subnets).To(HaveLen(0))
+ Expect(network.Internal).To(BeFalse())
+ Expect(network.Options).To(HaveLen(1))
+ Expect(network.Options).To(HaveKeyWithValue("mtu", "1300"))
+ Expect(network.IPAMOptions).To(HaveLen(1))
+ Expect(network.IPAMOptions).To(HaveKeyWithValue("driver", "dhcp"))
+ })
+
+ It("bridge network with vlan", func() {
+ network, err := libpodNet.NetworkInspect("vlan")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("vlan"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("cni-podman14"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Options).To(HaveLen(1))
+ Expect(network.Options).To(HaveKeyWithValue("vlan", "5"))
+ })
+
+ It("bridge network with labels", func() {
+ network, err := libpodNet.NetworkInspect("label")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("label"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("cni-podman15"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Labels).To(HaveLen(1))
+ Expect(network.Labels).To(HaveKeyWithValue("mykey", "value"))
+ })
+
+ It("dual stack network", func() {
+ network, err := libpodNet.NetworkInspect("dualstack")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("dualstack"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("cni-podman21"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(2))
+
+ sub1, _ := types.ParseCIDR("fd10:88:a::/64")
+ sub2, _ := types.ParseCIDR("10.89.19.0/24")
+ Expect(network.Subnets).To(ContainElements(
+ types.Subnet{Subnet: sub1, Gateway: net.ParseIP("fd10:88:a::1")},
+ types.Subnet{Subnet: sub2, Gateway: net.ParseIP("10.89.19.10").To4()},
+ ))
+ })
+
+ It("network list with filters (name)", func() {
+ filters := map[string][]string{
+ "name": {"internal", "bridge"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (partial name)", func() {
+ filters := map[string][]string{
+ "name": {"inte", "bri"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (id)", func() {
+ filters := map[string][]string{
+ "id": {"3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (id)", func() {
+ filters := map[string][]string{
+ "id": {"3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (partial id)", func() {
+ filters := map[string][]string{
+ "id": {"3bed2cb3a3acf7b6a8ef408420", "17f29b073143d8cd97b5bbe492bde"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (driver)", func() {
+ filters := map[string][]string{
+ "driver": {"bridge", "macvlan"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(9))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"),
+ HaveNetworkName("mtu"), HaveNetworkName("vlan"), HaveNetworkName("podman"),
+ HaveNetworkName("label"), HaveNetworkName("macvlan"), HaveNetworkName("macvlan_mtu"), HaveNetworkName("dualstack")))
+ })
+
+ It("network list with filters (label)", func() {
+ filters := map[string][]string{
+ "label": {"mykey"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(1))
+ Expect(networks).To(ConsistOf(HaveNetworkName("label")))
+
+ filters = map[string][]string{
+ "label": {"mykey=value"},
+ }
+ filterFuncs, err = util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err = libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(1))
+ Expect(networks).To(ConsistOf(HaveNetworkName("label")))
+ })
+
+ It("network list with filters", func() {
+ filters := map[string][]string{
+ "driver": {"bridge"},
+ "label": {"mykey"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+ Expect(filterFuncs).To(HaveLen(2))
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(1))
+ Expect(networks).To(ConsistOf(HaveNetworkName("label")))
+
+ filters = map[string][]string{
+ "driver": {"macvlan"},
+ "label": {"mykey"},
+ }
+ filterFuncs, err = util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err = libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(0))
+ })
+
+ It("crate bridge network with used interface name", func() {
+ network := types.Network{
+ NetworkInterface: "cni-podman9",
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("bridge name cni-podman9 already in use"))
+ })
+ })
+
+ Context("network load invalid existing ones", func() {
+
+ BeforeEach(func() {
+ dir := "testfiles/invalid"
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ Fail("Failed to read test directory")
+ }
+ for _, file := range files {
+ filename := file.Name()
+ data, err := ioutil.ReadFile(filepath.Join(dir, filename))
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ err = ioutil.WriteFile(filepath.Join(cniConfDir, filename), data, 0700)
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ }
+ })
+
+ It("load invalid networks from disk", func() {
+ nets, err := libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(nets).To(HaveLen(2))
+ logString := logBuffer.String()
+ Expect(logString).To(ContainSubstring("noname.conflist: error parsing configuration list: no name"))
+ Expect(logString).To(ContainSubstring("noplugin.conflist: error parsing configuration list: no plugins in list"))
+ Expect(logString).To(ContainSubstring("invalidname.conflist has invalid name, skipping: names must match"))
+ Expect(logString).To(ContainSubstring("has the same network name as"))
+ Expect(logString).To(ContainSubstring("broken.conflist: error parsing configuration list"))
+ Expect(logString).To(ContainSubstring("invalid_gateway.conflist could not be converted to a libpod config, skipping: failed to parse gateway ip 10.89.8"))
+ })
+
+ })
+
+})
+
+func grepInFile(path string, match string) {
+ data, err := ioutil.ReadFile(path)
+ ExpectWithOffset(1, err).To(BeNil())
+ ExpectWithOffset(1, string(data)).To(ContainSubstring(match))
+}
+
+// HaveNetworkName is a custom GomegaMatcher to match a network name
+func HaveNetworkName(name string) gomegaTypes.GomegaMatcher {
+ return WithTransform(func(e types.Network) string {
+ return e.Name
+ }, Equal(name))
+}
diff --git a/libpod/network/cni/network.go b/libpod/network/cni/network.go
new file mode 100644
index 000000000..fde08a0c6
--- /dev/null
+++ b/libpod/network/cni/network.go
@@ -0,0 +1,340 @@
+// +build linux
+
+package cni
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "net"
+ "os"
+ "strings"
+
+ "github.com/containernetworking/cni/libcni"
+ "github.com/containers/podman/v3/libpod/define"
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/libpod/network/util"
+ pkgutil "github.com/containers/podman/v3/pkg/util"
+ "github.com/containers/storage/pkg/lockfile"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+type cniNetwork struct {
+ // cniConfigDir is directory where the cni config files are stored.
+ cniConfigDir string
+ // cniPluginDirs is a list of directories where cni should look for the plugins.
+ cniPluginDirs []string
+
+ cniConf *libcni.CNIConfig
+
+ // defaultNetwork is the name for the default network.
+ defaultNetwork string
+ // defaultSubnet is the default subnet for the default network.
+ defaultSubnet types.IPNet
+
+ // isMachine describes whenever podman runs in a podman machine environment.
+ isMachine bool
+
+ // lock is a internal lock for critical operations
+ lock lockfile.Locker
+
+ // networks is a map with loaded networks, the key is the network name
+ networks map[string]*network
+}
+
+type network struct {
+ // filename is the full path to the cni config file on disk
+ filename string
+ libpodNet *types.Network
+ cniNet *libcni.NetworkConfigList
+}
+
+type InitConfig struct {
+ // CNIConfigDir is directory where the cni config files are stored.
+ CNIConfigDir string
+ // CNIPluginDirs is a list of directories where cni should look for the plugins.
+ CNIPluginDirs []string
+
+ // DefaultNetwork is the name for the default network.
+ DefaultNetwork string
+ // DefaultSubnet is the default subnet for the default network.
+ DefaultSubnet string
+
+ // IsMachine describes whenever podman runs in a podman machine environment.
+ IsMachine bool
+
+ // LockFile is the path to lock file.
+ LockFile string
+}
+
+// NewCNINetworkInterface creates the ContainerNetwork interface for the CNI backend.
+// Note: The networks are not loaded from disk until a method is called.
+func NewCNINetworkInterface(conf InitConfig) (types.ContainerNetwork, error) {
+ // TODO: consider using a shared memory lock
+ lock, err := lockfile.GetLockfile(conf.LockFile)
+ if err != nil {
+ return nil, err
+ }
+
+ defaultNetworkName := conf.DefaultNetwork
+ if defaultNetworkName == "" {
+ defaultNetworkName = types.DefaultNetworkName
+ }
+
+ defaultSubnet := conf.DefaultSubnet
+ if defaultSubnet == "" {
+ defaultSubnet = types.DefaultSubnet
+ }
+ defaultNet, err := types.ParseCIDR(defaultSubnet)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to parse default subnet")
+ }
+
+ cni := libcni.NewCNIConfig(conf.CNIPluginDirs, &cniExec{})
+ n := &cniNetwork{
+ cniConfigDir: conf.CNIConfigDir,
+ cniPluginDirs: conf.CNIPluginDirs,
+ cniConf: cni,
+ defaultNetwork: defaultNetworkName,
+ defaultSubnet: defaultNet,
+ isMachine: conf.IsMachine,
+ lock: lock,
+ }
+
+ return n, nil
+}
+
+func (n *cniNetwork) loadNetworks() error {
+ // skip loading networks if they are already loaded
+ if n.networks != nil {
+ return nil
+ }
+ // FIXME: do we have to support other file types as well, e.g. .conf?
+ files, err := libcni.ConfFiles(n.cniConfigDir, []string{".conflist"})
+ if err != nil {
+ return err
+ }
+ networks := make(map[string]*network, len(files))
+ for _, file := range files {
+ conf, err := libcni.ConfListFromFile(file)
+ if err != nil {
+ // do not log ENOENT errors
+ if !os.IsNotExist(err) {
+ logrus.Warnf("Error loading CNI config file %s: %v", file, err)
+ }
+ continue
+ }
+
+ if !define.NameRegex.MatchString(conf.Name) {
+ logrus.Warnf("CNI config list %s has invalid name, skipping: %v", file, define.RegexError)
+ continue
+ }
+
+ if _, err := n.cniConf.ValidateNetworkList(context.Background(), conf); err != nil {
+ logrus.Warnf("Error validating CNI config file %s: %v", file, err)
+ continue
+ }
+
+ if val, ok := networks[conf.Name]; ok {
+ logrus.Warnf("CNI config list %s has the same network name as %s, skipping", file, val.filename)
+ continue
+ }
+
+ net, err := createNetworkFromCNIConfigList(conf, file)
+ if err != nil {
+ logrus.Errorf("CNI config list %s could not be converted to a libpod config, skipping: %v", file, err)
+ continue
+ }
+ logrus.Tracef("Successfully loaded network %s: %v", net.Name, net)
+ networkInfo := network{
+ filename: file,
+ cniNet: conf,
+ libpodNet: net,
+ }
+ networks[net.Name] = &networkInfo
+ }
+
+ // create the default network in memory if it did not exists on disk
+ if networks[n.defaultNetwork] == nil {
+ networkInfo, err := n.createDefaultNetwork()
+ if err != nil {
+ return errors.Wrapf(err, "failed to create default network %s", n.defaultNetwork)
+ }
+ networks[n.defaultNetwork] = networkInfo
+ }
+
+ logrus.Debugf("Successfully loaded %d networks", len(networks))
+ n.networks = networks
+ return nil
+}
+
+func (n *cniNetwork) createDefaultNetwork() (*network, error) {
+ net := types.Network{
+ Name: n.defaultNetwork,
+ NetworkInterface: "cni-podman0",
+ Driver: types.BridgeNetworkDriver,
+ Subnets: []types.Subnet{
+ {Subnet: n.defaultSubnet},
+ },
+ }
+ return n.networkCreate(net, false)
+}
+
+// getNetwork will lookup a network by name or ID. It returns an
+// error when no network was found or when more than one network
+// with the given (partial) ID exists.
+// getNetwork will read from the networks map, therefore the caller
+// must ensure that n.lock is locked before using it.
+func (n *cniNetwork) getNetwork(nameOrID string) (*network, error) {
+ // fast path check the map key, this will only work for names
+ if val, ok := n.networks[nameOrID]; ok {
+ return val, nil
+ }
+ // If there was no match we might got a full or partial ID.
+ var net *network
+ for _, val := range n.networks {
+ // This should not happen because we already looked up the map by name but check anyway.
+ if val.libpodNet.Name == nameOrID {
+ return val, nil
+ }
+
+ if strings.HasPrefix(val.libpodNet.ID, nameOrID) {
+ if net != nil {
+ return nil, errors.Errorf("more than one result for network ID %s", nameOrID)
+ }
+ net = val
+ }
+ }
+ if net != nil {
+ return net, nil
+ }
+ return nil, errors.Wrapf(define.ErrNoSuchNetwork, "unable to find network with name or ID %s", nameOrID)
+}
+
+// getNetworkIDFromName creates a network ID from the name. It is just the
+// sha256 hash so it is not safe but it should be safe enough for our use case.
+func getNetworkIDFromName(name string) string {
+ hash := sha256.Sum256([]byte(name))
+ return hex.EncodeToString(hash[:])
+}
+
+// getFreeIPv6NetworkSubnet returns a unused ipv4 subnet
+func (n *cniNetwork) getFreeIPv4NetworkSubnet() (*types.Subnet, error) {
+ networks, err := n.getUsedSubnets()
+ if err != nil {
+ return nil, err
+ }
+
+ // the default podman network is 10.88.0.0/16
+ // start locking for free /24 networks
+ network := &net.IPNet{
+ IP: net.IP{10, 89, 0, 0},
+ Mask: net.IPMask{255, 255, 255, 0},
+ }
+
+ // TODO: make sure to not use public subnets
+ for {
+ if intersectsConfig := util.NetworkIntersectsWithNetworks(network, networks); !intersectsConfig {
+ logrus.Debugf("found free ipv4 network subnet %s", network.String())
+ return &types.Subnet{
+ Subnet: types.IPNet{IPNet: *network},
+ }, nil
+ }
+ network, err = util.NextSubnet(network)
+ if err != nil {
+ return nil, err
+ }
+ }
+}
+
+// getFreeIPv6NetworkSubnet returns a unused ipv6 subnet
+func (n *cniNetwork) getFreeIPv6NetworkSubnet() (*types.Subnet, error) {
+ networks, err := n.getUsedSubnets()
+ if err != nil {
+ return nil, err
+ }
+
+ // 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.
+ network, err := util.GetRandomIPv6Subnet()
+ if err != nil {
+ return nil, err
+ }
+ if intersectsConfig := util.NetworkIntersectsWithNetworks(&network, networks); !intersectsConfig {
+ logrus.Debugf("found free ipv6 network subnet %s", network.String())
+ return &types.Subnet{
+ Subnet: types.IPNet{IPNet: network},
+ }, nil
+ }
+ }
+ return nil, errors.New("failed to get random ipv6 subnet")
+}
+
+// getUsedSubnets returns a list of all used subnets by network
+// configs and interfaces on the host.
+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)
+ }
+ }
+ // second, load networks from the current system
+ liveSubnets, err := util.GetLiveNetworkSubnets()
+ if err != nil {
+ return nil, err
+ }
+ return append(subnets, liveSubnets...), nil
+}
+
+// getFreeDeviceName returns a free device name which can
+// be used for new configs as name and bridge interface name
+func (n *cniNetwork) getFreeDeviceName() (string, error) {
+ bridgeNames := n.getBridgeInterfaceNames()
+ netNames := n.getUsedNetworkNames()
+ liveInterfaces, err := util.GetLiveNetworkNames()
+ if err != nil {
+ return "", nil
+ }
+ names := make([]string, 0, len(bridgeNames)+len(netNames)+len(liveInterfaces))
+ names = append(names, bridgeNames...)
+ names = append(names, netNames...)
+ names = append(names, liveInterfaces...)
+ // FIXME: Is a limit fine?
+ // Start by 1, 0 is reserved for the default network
+ for i := 1; i < 1000000; i++ {
+ deviceName := fmt.Sprintf("%s%d", cniDeviceName, i)
+ if !pkgutil.StringInSlice(deviceName, names) {
+ logrus.Debugf("found free device name %s", deviceName)
+ return deviceName, nil
+ }
+ }
+ return "", errors.New("could not find free device name, to many iterations")
+}
+
+// getUsedNetworkNames returns all network names already used
+// by network configs
+func (n *cniNetwork) getUsedNetworkNames() []string {
+ names := make([]string, 0, len(n.networks))
+ for _, val := range n.networks {
+ names = append(names, val.libpodNet.Name)
+ }
+ return names
+}
+
+// getUsedNetworkNames returns all bridge device names already used
+// by network configs
+func (n *cniNetwork) getBridgeInterfaceNames() []string {
+ names := make([]string, 0, len(n.networks))
+ for _, val := range n.networks {
+ if val.libpodNet.Driver == types.BridgeNetworkDriver {
+ names = append(names, val.libpodNet.NetworkInterface)
+ }
+ }
+ return names
+}
diff --git a/libpod/network/cni/run.go b/libpod/network/cni/run.go
new file mode 100644
index 000000000..14634262c
--- /dev/null
+++ b/libpod/network/cni/run.go
@@ -0,0 +1,309 @@
+// +build linux
+
+package cni
+
+import (
+ "context"
+ "net"
+ "os"
+ "strings"
+
+ "github.com/containernetworking/cni/libcni"
+ cnitypes "github.com/containernetworking/cni/pkg/types"
+ "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/network/types"
+ "github.com/hashicorp/go-multierror"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "github.com/vishvananda/netlink"
+)
+
+// Setup will setup the container network namespace. It returns
+// a map of StatusBlocks, the key is the network name.
+func (n *cniNetwork) Setup(namespacePath string, options types.SetupOptions) (map[string]types.StatusBlock, error) {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return nil, err
+ }
+
+ if namespacePath == "" {
+ return nil, errors.New("namespacePath is empty")
+ }
+ if options.ContainerID == "" {
+ return nil, errors.New("ContainerID is empty")
+ }
+ if len(options.Networks) == 0 {
+ return nil, errors.New("must specify at least one network")
+ }
+ for name, netOpts := range options.Networks {
+ network := n.networks[name]
+ if network == nil {
+ return nil, errors.Wrapf(define.ErrNoSuchNetwork, "network %s", name)
+ }
+ err := validatePerNetworkOpts(network, netOpts)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // set the loopback adapter up in the container netns
+ err = ns.WithNetNSPath(namespacePath, func(_ ns.NetNS) error {
+ link, err := netlink.LinkByName("lo")
+ if err == nil {
+ err = netlink.LinkSetUp(link)
+ }
+ return err
+ })
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to set the loopback adapter up")
+ }
+
+ var retErr error
+ teardownOpts := options
+ teardownOpts.Networks = map[string]types.PerNetworkOptions{}
+ // make sure to teardown the already connected networks on error
+ defer func() {
+ if retErr != nil {
+ if len(teardownOpts.Networks) > 0 {
+ err := n.teardown(namespacePath, types.TeardownOptions(teardownOpts))
+ if err != nil {
+ logrus.Warn(err)
+ }
+ }
+ }
+ }()
+
+ ports, err := convertSpecgenPortsToCNIPorts(options.PortMappings)
+ if err != nil {
+ return nil, err
+ }
+
+ results := make(map[string]types.StatusBlock, len(options.Networks))
+ for name, netOpts := range options.Networks {
+ network := n.networks[name]
+ rt := getRuntimeConfig(namespacePath, options.ContainerName, options.ContainerID, name, ports, netOpts)
+
+ // If we have more than one static ip we need parse the ips via runtime config,
+ // make sure to add the ips capability to the first plugin otherwise it doesn't get the ips
+ if len(netOpts.StaticIPs) > 0 && !network.cniNet.Plugins[0].Network.Capabilities["ips"] {
+ caps := make(map[string]interface{})
+ caps["capabilities"] = map[string]bool{"ips": true}
+ network.cniNet.Plugins[0], retErr = libcni.InjectConf(network.cniNet.Plugins[0], caps)
+ if retErr != nil {
+ return nil, retErr
+ }
+ }
+
+ var res cnitypes.Result
+ res, retErr = n.cniConf.AddNetworkList(context.Background(), network.cniNet, rt)
+ // Add this network to teardown opts since it is now connected.
+ // Also add this if an errors was returned since we want to call teardown on this regardless.
+ teardownOpts.Networks[name] = netOpts
+ if retErr != nil {
+ return nil, retErr
+ }
+
+ var cnires *current.Result
+ cnires, retErr = current.GetResult(res)
+ if retErr != nil {
+ return nil, retErr
+ }
+ logrus.Debugf("cni result for container %s network %s: %v", options.ContainerID, name, cnires)
+ var status types.StatusBlock
+ status, retErr = cniResultToStatus(cnires)
+ if retErr != nil {
+ return nil, retErr
+ }
+ results[name] = status
+ }
+ return results, nil
+}
+
+// cniResultToStatus convert the cni result to status block
+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 {
+ ip := net.ParseIP(nameserver)
+ if ip == nil {
+ return result, errors.Errorf("failed to parse cni nameserver ip %s", nameserver)
+ }
+ nameservers = append(nameservers, ip)
+ }
+ result.DNSServerIPs = nameservers
+ result.DNSSearchDomains = cniResult.DNS.Search
+
+ interfaces := make(map[string]types.NetInterface)
+ for _, ip := range cniResult.IPs {
+ if ip.Interface == nil {
+ // we do no expect ips without an interface
+ continue
+ }
+ if len(cniResult.Interfaces) <= *ip.Interface {
+ return result, errors.Errorf("invalid cni result, interface index %d out of range", *ip.Interface)
+ }
+ cniInt := cniResult.Interfaces[*ip.Interface]
+ netInt, ok := interfaces[cniInt.Name]
+ if ok {
+ netInt.Networks = append(netInt.Networks, types.NetAddress{
+ Subnet: types.IPNet{IPNet: ip.Address},
+ Gateway: ip.Gateway,
+ })
+ interfaces[cniInt.Name] = netInt
+ } else {
+ mac, err := net.ParseMAC(cniInt.Mac)
+ if err != nil {
+ return result, err
+ }
+ interfaces[cniInt.Name] = types.NetInterface{
+ MacAddress: mac,
+ Networks: []types.NetAddress{{
+ Subnet: types.IPNet{IPNet: ip.Address},
+ Gateway: ip.Gateway,
+ }},
+ }
+ }
+ }
+ result.Interfaces = interfaces
+ return result, nil
+}
+
+// validatePerNetworkOpts checks that all given static ips are in a subnet on this network
+func validatePerNetworkOpts(network *network, netOpts types.PerNetworkOptions) error {
+ if netOpts.InterfaceName == "" {
+ return errors.Errorf("interface name on network %s is empty", network.libpodNet.Name)
+ }
+outer:
+ for _, ip := range netOpts.StaticIPs {
+ for _, s := range network.libpodNet.Subnets {
+ if s.Subnet.Contains(ip) {
+ continue outer
+ }
+ }
+ return errors.Errorf("requested static ip %s not in any subnet on network %s", ip.String(), network.libpodNet.Name)
+ }
+ if len(netOpts.Aliases) > 0 && !network.libpodNet.DNSEnabled {
+ return errors.New("cannot set aliases on a network without dns enabled")
+ }
+ return nil
+}
+
+func getRuntimeConfig(netns, conName, conID, networkName string, ports []cniPortMapEntry, opts types.PerNetworkOptions) *libcni.RuntimeConf {
+ rt := &libcni.RuntimeConf{
+ ContainerID: conID,
+ NetNS: netns,
+ IfName: opts.InterfaceName,
+ Args: [][2]string{
+ {"IgnoreUnknown", "1"},
+ // FIXME: Should we set the K8S args?
+ //{"K8S_POD_NAMESPACE", conName},
+ //{"K8S_POD_INFRA_CONTAINER_ID", conID},
+ // K8S_POD_NAME is used by dnsname to get the container name
+ {"K8S_POD_NAME", conName},
+ },
+ CapabilityArgs: map[string]interface{}{},
+ }
+
+ // Propagate environment CNI_ARGS
+ for _, kvpairs := range strings.Split(os.Getenv("CNI_ARGS"), ";") {
+ if keyval := strings.SplitN(kvpairs, "=", 2); len(keyval) == 2 {
+ rt.Args = append(rt.Args, [2]string{keyval[0], keyval[1]})
+ }
+ }
+
+ // Add mac address to cni args
+ if len(opts.StaticMAC) > 0 {
+ rt.Args = append(rt.Args, [2]string{"MAC", opts.StaticMAC.String()})
+ }
+
+ if len(opts.StaticIPs) == 1 {
+ // Add a single IP to the args field. CNI plugins < 1.0.0
+ // do not support multiple ips via capability args.
+ rt.Args = append(rt.Args, [2]string{"IP", opts.StaticIPs[0].String()})
+ } else if len(opts.StaticIPs) > 1 {
+ // Set the static ips in the capability args
+ // to support more than one static ip per network.
+ rt.CapabilityArgs["ips"] = opts.StaticIPs
+ }
+
+ // Set network aliases for the dnsname plugin.
+ if len(opts.Aliases) > 0 {
+ rt.CapabilityArgs["aliases"] = map[string][]string{
+ networkName: opts.Aliases,
+ }
+ }
+
+ // Set PortMappings in Capabilities
+ if len(ports) > 0 {
+ rt.CapabilityArgs["portMappings"] = ports
+ }
+
+ return rt
+}
+
+// Teardown will teardown the container network namespace.
+func (n *cniNetwork) Teardown(namespacePath string, options types.TeardownOptions) error {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return err
+ }
+ return n.teardown(namespacePath, options)
+}
+
+func (n *cniNetwork) teardown(namespacePath string, options types.TeardownOptions) error {
+ // Note: An empty namespacePath is allowed because some plugins
+ // still need teardown, for example ipam should remove used ip allocations.
+
+ ports, err := convertSpecgenPortsToCNIPorts(options.PortMappings)
+ if err != nil {
+ return err
+ }
+
+ var multiErr *multierror.Error
+ for name, netOpts := range options.Networks {
+ rt := getRuntimeConfig(namespacePath, options.ContainerName, options.ContainerID, name, ports, netOpts)
+
+ cniConfList, newRt, err := getCachedNetworkConfig(n.cniConf, name, rt)
+ if err == nil {
+ rt = newRt
+ } else {
+ logrus.Warnf("failed to load cached network config: %v, falling back to loading network %s from disk", err, name)
+ network := n.networks[name]
+ if network == nil {
+ multiErr = multierror.Append(multiErr, errors.Wrapf(define.ErrNoSuchNetwork, "network %s", name))
+ continue
+ }
+ cniConfList = network.cniNet
+ }
+
+ err = n.cniConf.DelNetworkList(context.Background(), cniConfList, rt)
+ if err != nil {
+ multiErr = multierror.Append(multiErr, err)
+ }
+ }
+ return multiErr.ErrorOrNil()
+}
+
+func getCachedNetworkConfig(cniConf *libcni.CNIConfig, name string, rt *libcni.RuntimeConf) (*libcni.NetworkConfigList, *libcni.RuntimeConf, error) {
+ cniConfList := &libcni.NetworkConfigList{
+ Name: name,
+ }
+ confBytes, rt, err := cniConf.GetNetworkListCachedConfig(cniConfList, rt)
+ if err != nil {
+ return nil, nil, err
+ } else if confBytes == nil {
+ return nil, nil, errors.Errorf("network %s not found in CNI cache", name)
+ }
+
+ cniConfList, err = libcni.ConfListFromBytes(confBytes)
+ if err != nil {
+ return nil, nil, err
+ }
+ return cniConfList, rt, nil
+}
diff --git a/libpod/network/cni/run_test.go b/libpod/network/cni/run_test.go
new file mode 100644
index 000000000..32e88ca61
--- /dev/null
+++ b/libpod/network/cni/run_test.go
@@ -0,0 +1,1326 @@
+// +build linux
+
+package cni_test
+
+// The tests have to be run as root.
+// For each test there will be two network namespaces created,
+// netNSTest and netNSContainer. Each test must be run inside
+// netNSTest to prevent leakage in the host netns, therefore
+// it should use the following structure:
+// It("test name", func() {
+// runTest(func() {
+// // add test logic here
+// })
+// })
+
+import (
+ "bytes"
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/containernetworking/plugins/pkg/ns"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "github.com/sirupsen/logrus"
+ "github.com/vishvananda/netlink"
+ "golang.org/x/sys/unix"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/pkg/netns"
+ "github.com/containers/podman/v3/pkg/rootless"
+ "github.com/containers/storage/pkg/stringid"
+)
+
+var _ = Describe("run CNI", func() {
+ var (
+ libpodNet types.ContainerNetwork
+ cniConfDir string
+ logBuffer bytes.Buffer
+ netNSTest ns.NetNS
+ netNSContainer ns.NetNS
+ )
+ const cniVarDir = "/var/lib/cni"
+
+ // runTest is a helper function to run a test. It ensures that each test
+ // is run in its own netns. It also creates a mountns to mount a tmpfs to /var/lib/cni.
+ runTest := func(run func()) {
+ netNSTest.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ err := os.MkdirAll(cniVarDir, 0755)
+ Expect(err).To(BeNil(), "Failed to create cniVarDir")
+ err = unix.Unshare(unix.CLONE_NEWNS)
+ Expect(err).To(BeNil(), "Failed to create new mountns")
+ err = unix.Mount("tmpfs", cniVarDir, "tmpfs", unix.MS_NOEXEC|unix.MS_NOSUID|unix.MS_NODEV, "")
+ Expect(err).To(BeNil(), "Failed to mount tmpfs for cniVarDir")
+ defer unix.Unmount(cniVarDir, 0)
+
+ // we have to setup the loopback adapter in this netns to use port forwarding
+ link, err := netlink.LinkByName("lo")
+ Expect(err).To(BeNil(), "Failed to get loopback adapter")
+ err = netlink.LinkSetUp(link)
+ Expect(err).To(BeNil(), "Failed to set loopback adapter up")
+ run()
+ return nil
+ })
+ }
+
+ BeforeEach(func() {
+ // The tests need root privileges.
+ // Technically we could work around that by using user namespaces and
+ // the rootless cni code but this is to much work to get it right for a unit test.
+ if rootless.IsRootless() {
+ Skip("this test needs to be run as root")
+ }
+
+ var err error
+ cniConfDir, err = ioutil.TempDir("", "podman_cni_test")
+ if err != nil {
+ Fail("Failed to create tmpdir")
+ }
+ logBuffer = bytes.Buffer{}
+ logrus.SetOutput(&logBuffer)
+
+ netNSTest, err = netns.NewNS()
+ if err != nil {
+ Fail("Failed to create netns")
+ }
+
+ netNSContainer, err = netns.NewNS()
+ if err != nil {
+ Fail("Failed to create netns")
+ }
+ })
+
+ JustBeforeEach(func() {
+ var err error
+ libpodNet, err = getNetworkInterface(cniConfDir, false)
+ if err != nil {
+ Fail("Failed to create NewCNINetworkInterface")
+ }
+ })
+
+ AfterEach(func() {
+ os.RemoveAll(cniConfDir)
+
+ netns.UnmountNS(netNSTest)
+ netNSTest.Close()
+
+ netns.UnmountNS(netNSContainer)
+ netNSContainer.Close()
+ })
+
+ Context("network setup test", func() {
+
+ It("run with default config", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {InterfaceName: intName},
+ },
+ },
+ }
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+ Expect(res).To(HaveKey(defNet))
+ Expect(res[defNet].Interfaces).To(HaveKey(intName))
+ Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1))
+ Expect(res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String()).To(ContainSubstring("10.88.0."))
+ Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6))
+ // default network has no dns
+ Expect(res[defNet].DNSServerIPs).To(BeEmpty())
+ Expect(res[defNet].DNSSearchDomains).To(BeEmpty())
+
+ err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
+ Expect(err).To(BeNil())
+ })
+ })
+
+ It("run with default config and static ip", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ ip := net.ParseIP("10.88.5.5")
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {
+ InterfaceName: intName,
+ StaticIPs: []net.IP{ip},
+ },
+ },
+ },
+ }
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+ Expect(res).To(HaveKey(defNet))
+ Expect(res[defNet].Interfaces).To(HaveKey(intName))
+ Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1))
+ Expect(res[defNet].Interfaces[intName].Networks[0].Subnet.IP).To(Equal(ip))
+ Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6))
+ // default network has no dns
+ Expect(res[defNet].DNSServerIPs).To(BeEmpty())
+ Expect(res[defNet].DNSSearchDomains).To(BeEmpty())
+
+ err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
+ Expect(err).To(BeNil())
+ })
+ })
+
+ for _, proto := range []string{"tcp", "udp"} {
+ // copy proto to extra var to keep correct references in the goroutines
+ protocol := proto
+ It("run with exposed ports protocol "+protocol, func() {
+ runTest(func() {
+ testdata := stringid.GenerateNonCryptoID()
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ PortMappings: []types.PortMapping{{
+ Protocol: protocol,
+ HostIP: "127.0.0.1",
+ HostPort: 5000,
+ ContainerPort: 5000,
+ }},
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {InterfaceName: intName},
+ },
+ },
+ }
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+ Expect(res).To(HaveKey(defNet))
+ Expect(res[defNet].Interfaces).To(HaveKey(intName))
+ Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1))
+ Expect(res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String()).To(ContainSubstring("10.88.0."))
+ Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6))
+ // default network has no dns
+ Expect(res[defNet].DNSServerIPs).To(BeEmpty())
+ Expect(res[defNet].DNSSearchDomains).To(BeEmpty())
+ var wg sync.WaitGroup
+ wg.Add(1)
+ // start a listener in the container ns
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ runNetListener(&wg, protocol, "0.0.0.0", 5000, testdata)
+ return nil
+ })
+ Expect(err).To(BeNil())
+
+ conn, err := net.Dial(protocol, "127.0.0.1:5000")
+ Expect(err).To(BeNil())
+ _, err = conn.Write([]byte(testdata))
+ Expect(err).To(BeNil())
+ conn.Close()
+
+ // wait for the listener to finish
+ wg.Wait()
+
+ err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
+ Expect(err).To(BeNil())
+ })
+ })
+
+ It("run with range ports protocol "+protocol, func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ PortMappings: []types.PortMapping{{
+ Protocol: protocol,
+ HostIP: "127.0.0.1",
+ HostPort: 5001,
+ ContainerPort: 5000,
+ Range: 3,
+ }},
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {InterfaceName: intName},
+ },
+ },
+ }
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+ Expect(res).To(HaveKey(defNet))
+ Expect(res[defNet].Interfaces).To(HaveKey(intName))
+ Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1))
+ containerIP := res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String()
+ Expect(containerIP).To(ContainSubstring("10.88.0."))
+ Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6))
+ // default network has no dns
+ Expect(res[defNet].DNSServerIPs).To(BeEmpty())
+ Expect(res[defNet].DNSSearchDomains).To(BeEmpty())
+
+ // loop over all ports
+ for p := 5001; p < 5004; p++ {
+ port := p
+ var wg sync.WaitGroup
+ wg.Add(1)
+ testdata := stringid.GenerateNonCryptoID()
+ // start a listener in the container ns
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ runNetListener(&wg, protocol, containerIP, port-1, testdata)
+ return nil
+ })
+ Expect(err).To(BeNil())
+
+ conn, err := net.Dial(protocol, net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
+ Expect(err).To(BeNil())
+ _, err = conn.Write([]byte(testdata))
+ Expect(err).To(BeNil())
+ conn.Close()
+
+ // wait for the listener to finish
+ wg.Wait()
+ }
+
+ err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
+ Expect(err).To(BeNil())
+ })
+ })
+ }
+
+ It("run with comma separated port protocol", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ PortMappings: []types.PortMapping{{
+ Protocol: "tcp,udp",
+ HostIP: "127.0.0.1",
+ HostPort: 5000,
+ ContainerPort: 5000,
+ }},
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {InterfaceName: intName},
+ },
+ },
+ }
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+ Expect(res).To(HaveKey(defNet))
+ Expect(res[defNet].Interfaces).To(HaveKey(intName))
+ Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1))
+ Expect(res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String()).To(ContainSubstring("10.88.0."))
+ Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6))
+
+ for _, proto := range []string{"tcp", "udp"} {
+ // copy proto to extra var to keep correct references in the goroutines
+ protocol := proto
+
+ testdata := stringid.GenerateNonCryptoID()
+ var wg sync.WaitGroup
+ wg.Add(1)
+ // start tcp listener in the container ns
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ runNetListener(&wg, protocol, "0.0.0.0", 5000, testdata)
+ return nil
+ })
+ Expect(err).To(BeNil())
+
+ conn, err := net.Dial(protocol, "127.0.0.1:5000")
+ Expect(err).To(BeNil())
+ _, err = conn.Write([]byte(testdata))
+ Expect(err).To(BeNil())
+ conn.Close()
+
+ // wait for the listener to finish
+ wg.Wait()
+ }
+
+ err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
+ Expect(err).To(BeNil())
+ })
+ })
+
+ It("call setup twice", func() {
+ runTest(func() {
+ network := types.Network{}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+
+ intName1 := "eth0"
+ netName1 := network1.Name
+
+ containerID := stringid.GenerateNonCryptoID()
+
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: containerID,
+ Networks: map[string]types.PerNetworkOptions{
+ netName1: {
+ InterfaceName: intName1,
+ },
+ },
+ },
+ }
+
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+
+ Expect(res).To(HaveKey(netName1))
+ Expect(res[netName1].Interfaces).To(HaveKey(intName1))
+ Expect(res[netName1].Interfaces[intName1].Networks).To(HaveLen(1))
+ ipInt1 := res[netName1].Interfaces[intName1].Networks[0].Subnet.IP
+ Expect(ipInt1).ToNot(BeEmpty())
+ macInt1 := res[netName1].Interfaces[intName1].MacAddress
+ Expect(macInt1).To(HaveLen(6))
+
+ // check in the container namespace if the settings are applied
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ i, err := net.InterfaceByName(intName1)
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal(intName1))
+ Expect(i.HardwareAddr).To(Equal(macInt1))
+ addrs, err := i.Addrs()
+ Expect(err).To(BeNil())
+ subnet := &net.IPNet{
+ IP: ipInt1,
+ Mask: net.CIDRMask(24, 32),
+ }
+ Expect(addrs).To(ContainElements(subnet))
+
+ // check loopback adapter
+ i, err = net.InterfaceByName("lo")
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal("lo"))
+ Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+ return nil
+ })
+ Expect(err).To(BeNil())
+
+ network = types.Network{}
+ network2, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+
+ intName2 := "eth1"
+ netName2 := network2.Name
+
+ setupOpts.Networks = map[string]types.PerNetworkOptions{
+ netName2: {
+ InterfaceName: intName2,
+ },
+ }
+
+ res, err = libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+
+ Expect(res).To(HaveKey(netName2))
+ Expect(res[netName2].Interfaces).To(HaveKey(intName2))
+ Expect(res[netName2].Interfaces[intName2].Networks).To(HaveLen(1))
+ ipInt2 := res[netName2].Interfaces[intName2].Networks[0].Subnet.IP
+ Expect(ipInt2).ToNot(BeEmpty())
+ macInt2 := res[netName2].Interfaces[intName2].MacAddress
+ Expect(macInt2).To(HaveLen(6))
+
+ // check in the container namespace if the settings are applied
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ i, err := net.InterfaceByName(intName1)
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal(intName1))
+ Expect(i.HardwareAddr).To(Equal(macInt1))
+ addrs, err := i.Addrs()
+ Expect(err).To(BeNil())
+ subnet := &net.IPNet{
+ IP: ipInt1,
+ Mask: net.CIDRMask(24, 32),
+ }
+ Expect(addrs).To(ContainElements(subnet))
+
+ i, err = net.InterfaceByName(intName2)
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal(intName2))
+ Expect(i.HardwareAddr).To(Equal(macInt2))
+ addrs, err = i.Addrs()
+ Expect(err).To(BeNil())
+ subnet = &net.IPNet{
+ IP: ipInt2,
+ Mask: net.CIDRMask(24, 32),
+ }
+ Expect(addrs).To(ContainElements(subnet))
+
+ // check loopback adapter
+ i, err = net.InterfaceByName("lo")
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal("lo"))
+ Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+ return nil
+ })
+ Expect(err).To(BeNil())
+
+ teatdownOpts := types.TeardownOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: containerID,
+ Networks: map[string]types.PerNetworkOptions{
+ netName1: {
+ InterfaceName: intName1,
+ },
+ netName2: {
+ InterfaceName: intName2,
+ },
+ },
+ },
+ }
+
+ err = libpodNet.Teardown(netNSContainer.Path(), teatdownOpts)
+ Expect(err).To(BeNil())
+ logString := logBuffer.String()
+ Expect(logString).To(BeEmpty())
+
+ // check in the container namespace that the interface is removed
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ _, err := net.InterfaceByName(intName1)
+ Expect(err).To(HaveOccurred())
+ _, err = net.InterfaceByName(intName2)
+ Expect(err).To(HaveOccurred())
+
+ // check that only the loopback adapter is left
+ ints, err := net.Interfaces()
+ Expect(err).To(BeNil())
+ Expect(ints).To(HaveLen(1))
+ Expect(ints[0].Name).To(Equal("lo"))
+ Expect(ints[0].Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(ints[0].Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+
+ return nil
+ })
+ Expect(err).To(BeNil())
+
+ err = libpodNet.NetworkRemove(netName1)
+ Expect(err).To(BeNil())
+ err = libpodNet.NetworkRemove(netName2)
+ Expect(err).To(BeNil())
+
+ // check that the interfaces are removed in the host ns
+ _, err = net.InterfaceByName(network1.NetworkInterface)
+ Expect(err).To(HaveOccurred())
+ _, err = net.InterfaceByName(network2.NetworkInterface)
+ Expect(err).To(HaveOccurred())
+ })
+ })
+
+ It("setup two networks with one setup call", func() {
+ runTest(func() {
+ subnet1, _ := types.ParseCIDR("192.168.0.0/24")
+ subnet2, _ := types.ParseCIDR("192.168.1.0/24")
+ network := types.Network{
+ Subnets: []types.Subnet{
+ {Subnet: subnet1},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+
+ network = types.Network{
+ Subnets: []types.Subnet{
+ {Subnet: subnet2},
+ },
+ }
+ network2, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+
+ intName1 := "eth0"
+ intName2 := "eth1"
+ netName1 := network1.Name
+ netName2 := network2.Name
+
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ netName1: {
+ InterfaceName: intName1,
+ },
+ netName2: {
+ InterfaceName: intName2,
+ },
+ },
+ },
+ }
+
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(2))
+
+ Expect(res).To(HaveKey(netName1))
+ Expect(res[netName1].Interfaces).To(HaveKey(intName1))
+ Expect(res[netName1].Interfaces[intName1].Networks).To(HaveLen(1))
+ ipInt1 := res[netName1].Interfaces[intName1].Networks[0].Subnet.IP
+ Expect(ipInt1.String()).To(ContainSubstring("192.168.0."))
+ macInt1 := res[netName1].Interfaces[intName1].MacAddress
+ Expect(macInt1).To(HaveLen(6))
+
+ Expect(res).To(HaveKey(netName2))
+ Expect(res[netName2].Interfaces).To(HaveKey(intName2))
+ Expect(res[netName2].Interfaces[intName2].Networks).To(HaveLen(1))
+ ipInt2 := res[netName2].Interfaces[intName2].Networks[0].Subnet.IP
+ Expect(ipInt2.String()).To(ContainSubstring("192.168.1."))
+ macInt2 := res[netName2].Interfaces[intName2].MacAddress
+ Expect(macInt2).To(HaveLen(6))
+
+ // default network has no dns
+ Expect(res[netName1].DNSServerIPs).To(BeEmpty())
+ Expect(res[netName1].DNSSearchDomains).To(BeEmpty())
+
+ // check in the container namespace if the settings are applied
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ i, err := net.InterfaceByName(intName1)
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal(intName1))
+ Expect(i.HardwareAddr).To(Equal(macInt1))
+ addrs, err := i.Addrs()
+ Expect(err).To(BeNil())
+ subnet := &net.IPNet{
+ IP: ipInt1,
+ Mask: net.CIDRMask(24, 32),
+ }
+ Expect(addrs).To(ContainElements(subnet))
+
+ i, err = net.InterfaceByName(intName2)
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal(intName2))
+ Expect(i.HardwareAddr).To(Equal(macInt2))
+ addrs, err = i.Addrs()
+ Expect(err).To(BeNil())
+ subnet = &net.IPNet{
+ IP: ipInt2,
+ Mask: net.CIDRMask(24, 32),
+ }
+ Expect(addrs).To(ContainElements(subnet))
+
+ // check loopback adapter
+ i, err = net.InterfaceByName("lo")
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal("lo"))
+ Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+ return nil
+ })
+ Expect(err).To(BeNil())
+
+ err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
+ Expect(err).To(BeNil())
+ logString := logBuffer.String()
+ Expect(logString).To(BeEmpty())
+
+ // check in the container namespace that the interface is removed
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ _, err := net.InterfaceByName(intName1)
+ Expect(err).To(HaveOccurred())
+ _, err = net.InterfaceByName(intName2)
+ Expect(err).To(HaveOccurred())
+
+ // check that only the loopback adapter is left
+ ints, err := net.Interfaces()
+ Expect(err).To(BeNil())
+ Expect(ints).To(HaveLen(1))
+ Expect(ints[0].Name).To(Equal("lo"))
+ Expect(ints[0].Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(ints[0].Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+
+ return nil
+ })
+ Expect(err).To(BeNil())
+ })
+
+ })
+
+ It("dual stack network with static ips", func() {
+ // Version checks for cni plugins are not possible, the plugins do not output
+ // version information and using the package manager does not work across distros.
+ // Fedora has the right version so we use this for now.
+ SkipIfNotFedora("requires cni plugins 1.0.0 or newer for multiple static ips")
+ runTest(func() {
+ subnet1, _ := types.ParseCIDR("192.168.0.0/24")
+ subnet2, _ := types.ParseCIDR("fd41:0a75:2ca0:48a9::/64")
+ network := types.Network{
+ Subnets: []types.Subnet{
+ {Subnet: subnet1}, {Subnet: subnet2},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+
+ mac, _ := net.ParseMAC("40:15:2f:d8:42:36")
+ interfaceName := "eth0"
+
+ ip1 := net.ParseIP("192.168.0.5")
+ ip2 := net.ParseIP("fd41:0a75:2ca0:48a9::5")
+
+ netName := network1.Name
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerName: "mycon",
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {
+ InterfaceName: interfaceName,
+ StaticIPs: []net.IP{ip1, ip2},
+ StaticMAC: mac,
+ },
+ },
+ },
+ }
+
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+ Expect(res).To(HaveKey(netName))
+ Expect(res[netName].Interfaces).To(HaveKey(interfaceName))
+ Expect(res[netName].Interfaces[interfaceName].Networks).To(HaveLen(2))
+ Expect(res[netName].Interfaces[interfaceName].Networks[0].Subnet.IP.String()).To(Equal(ip1.String()))
+ Expect(res[netName].Interfaces[interfaceName].Networks[0].Subnet.Mask).To(Equal(subnet1.Mask))
+ Expect(res[netName].Interfaces[interfaceName].Networks[0].Gateway).To(Equal(net.ParseIP("192.168.0.1")))
+ Expect(res[netName].Interfaces[interfaceName].Networks[1].Subnet.IP.String()).To(Equal(ip2.String()))
+ Expect(res[netName].Interfaces[interfaceName].Networks[1].Subnet.Mask).To(Equal(subnet2.Mask))
+ Expect(res[netName].Interfaces[interfaceName].Networks[1].Gateway).To(Equal(net.ParseIP("fd41:0a75:2ca0:48a9::1")))
+ Expect(res[netName].Interfaces[interfaceName].MacAddress).To(Equal(mac))
+ // default network has no dns
+ Expect(res[netName].DNSServerIPs).To(BeEmpty())
+ Expect(res[netName].DNSSearchDomains).To(BeEmpty())
+
+ // check in the container namespace if the settings are applied
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ i, err := net.InterfaceByName(interfaceName)
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal(interfaceName))
+ Expect(i.HardwareAddr).To(Equal(mac))
+ addrs, err := i.Addrs()
+ Expect(err).To(BeNil())
+ subnet1 := &net.IPNet{
+ IP: ip1,
+ Mask: net.CIDRMask(24, 32),
+ }
+ subnet2 := &net.IPNet{
+ IP: ip2,
+ Mask: net.CIDRMask(64, 128),
+ }
+ Expect(addrs).To(ContainElements(subnet1, subnet2))
+
+ // check loopback adapter
+ i, err = net.InterfaceByName("lo")
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal("lo"))
+ Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+ return nil
+ })
+ Expect(err).To(BeNil())
+
+ err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
+ Expect(err).To(BeNil())
+ logString := logBuffer.String()
+ Expect(logString).To(BeEmpty())
+
+ // check in the container namespace that the interface is removed
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ _, err := net.InterfaceByName(interfaceName)
+ Expect(err).To(HaveOccurred())
+
+ // check that only the loopback adapter is left
+ ints, err := net.Interfaces()
+ Expect(err).To(BeNil())
+ Expect(ints).To(HaveLen(1))
+ Expect(ints[0].Name).To(Equal("lo"))
+ Expect(ints[0].Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(ints[0].Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+
+ return nil
+ })
+ Expect(err).To(BeNil())
+ })
+ })
+
+ It("CNI_ARGS from environment variable", func() {
+ runTest(func() {
+ subnet1, _ := types.ParseCIDR("172.16.1.0/24")
+ ip := "172.16.1.5"
+ network := types.Network{
+ Subnets: []types.Subnet{
+ {Subnet: subnet1},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ netName := network1.Name
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {
+ InterfaceName: intName,
+ },
+ },
+ },
+ }
+
+ os.Setenv("CNI_ARGS", "IP="+ip)
+ defer os.Unsetenv("CNI_ARGS")
+
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+ Expect(res).To(HaveKey(netName))
+ Expect(res[netName].Interfaces).To(HaveKey(intName))
+ Expect(res[netName].Interfaces[intName].Networks).To(HaveLen(1))
+ Expect(res[netName].Interfaces[intName].Networks[0].Subnet.IP.String()).To(Equal(ip))
+ Expect(res[netName].Interfaces[intName].Networks[0].Subnet.Mask).To(Equal(net.CIDRMask(24, 32)))
+
+ // check in the container namespace if the settings are applied
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ i, err := net.InterfaceByName(intName)
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal(intName))
+ addrs, err := i.Addrs()
+ Expect(err).To(BeNil())
+ subnet := &net.IPNet{
+ IP: net.ParseIP(ip),
+ Mask: net.CIDRMask(24, 32),
+ }
+ Expect(addrs).To(ContainElements(subnet))
+
+ // check loopback adapter
+ i, err = net.InterfaceByName("lo")
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal("lo"))
+ Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+ return nil
+ })
+ Expect(err).To(BeNil())
+ })
+ })
+ })
+
+ Context("network setup test with networks from disk", func() {
+
+ BeforeEach(func() {
+ dir := "testfiles/valid"
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ Fail("Failed to read test directory")
+ }
+ for _, file := range files {
+ filename := file.Name()
+ data, err := ioutil.ReadFile(filepath.Join(dir, filename))
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ err = ioutil.WriteFile(filepath.Join(cniConfDir, filename), data, 0700)
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ }
+ })
+
+ It("dualstack setup with static ip and dns", func() {
+ SkipIfNoDnsname()
+ // Version checks for cni plugins are not possible, the plugins do not output
+ // version information and using the package manager does not work across distros.
+ // Fedora has the right version so we use this for now.
+ SkipIfNotFedora("requires cni plugins 1.0.0 or newer for multiple static ips")
+ runTest(func() {
+ interfaceName := "eth0"
+
+ ip1 := net.ParseIP("fd10:88:a::11")
+ ip2 := net.ParseIP("10.89.19.15")
+
+ containerName := "myname"
+ aliases := []string{"aliasname"}
+
+ netName := "dualstack"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ ContainerName: containerName,
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {
+ InterfaceName: interfaceName,
+ StaticIPs: []net.IP{ip1, ip2},
+ Aliases: aliases,
+ },
+ },
+ },
+ }
+
+ network, err := libpodNet.NetworkInspect(netName)
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal(netName))
+ Expect(network.DNSEnabled).To(BeTrue())
+ Expect(network.Subnets).To(HaveLen(2))
+ gw1 := network.Subnets[0].Gateway
+ Expect(gw1).To(HaveLen(16))
+ mask1 := network.Subnets[0].Subnet.Mask
+ Expect(mask1).To(HaveLen(16))
+ gw2 := network.Subnets[1].Gateway
+ Expect(gw2).To(HaveLen(4))
+ mask2 := network.Subnets[1].Subnet.Mask
+ Expect(mask2).To(HaveLen(4))
+
+ // because this net has dns we should always teardown otherwise we leak a dnsmasq process
+ defer libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
+ res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(BeNil())
+ Expect(res).To(HaveLen(1))
+ Expect(res).To(HaveKey(netName))
+ Expect(res[netName].Interfaces).To(HaveKey(interfaceName))
+ Expect(res[netName].Interfaces[interfaceName].Networks).To(HaveLen(2))
+ Expect(res[netName].Interfaces[interfaceName].Networks[0].Subnet.IP.String()).To(Equal(ip1.String()))
+ Expect(res[netName].Interfaces[interfaceName].Networks[0].Subnet.Mask).To(Equal(mask1))
+ Expect(res[netName].Interfaces[interfaceName].Networks[1].Subnet.IP.String()).To(Equal(ip2.String()))
+ Expect(res[netName].Interfaces[interfaceName].Networks[1].Subnet.Mask).To(Equal(mask2))
+ // dualstack network dns
+ Expect(res[netName].DNSServerIPs).To(HaveLen(2))
+ Expect(res[netName].DNSSearchDomains).To(HaveLen(1))
+ Expect(res[netName].DNSSearchDomains).To(ConsistOf("dns.podman"))
+
+ // check in the container namespace if the settings are applied
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ i, err := net.InterfaceByName(interfaceName)
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal(interfaceName))
+ addrs, err := i.Addrs()
+ Expect(err).To(BeNil())
+ subnet1 := &net.IPNet{
+ IP: ip1,
+ Mask: net.CIDRMask(64, 128),
+ }
+ subnet2 := &net.IPNet{
+ IP: ip2,
+ Mask: net.CIDRMask(24, 32),
+ }
+ Expect(addrs).To(ContainElements(subnet1, subnet2))
+
+ // check loopback adapter
+ i, err = net.InterfaceByName("lo")
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal("lo"))
+ Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+
+ return nil
+ })
+ Expect(err).To(BeNil())
+
+ err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
+ Expect(err).To(BeNil())
+ logString := logBuffer.String()
+ Expect(logString).To(BeEmpty())
+
+ // check in the container namespace that the interface is removed
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ _, err := net.InterfaceByName(interfaceName)
+ Expect(err).To(HaveOccurred())
+
+ // check that only the loopback adapter is left
+ ints, err := net.Interfaces()
+ Expect(err).To(BeNil())
+ Expect(ints).To(HaveLen(1))
+ Expect(ints[0].Name).To(Equal("lo"))
+ Expect(ints[0].Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(ints[0].Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+
+ return nil
+ })
+ Expect(err).To(BeNil())
+ })
+ })
+
+ })
+
+ Context("invalid network setup test", func() {
+
+ It("static ip not in subnet", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ ip := "1.1.1.1"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {
+ InterfaceName: intName,
+ StaticIPs: []net.IP{net.ParseIP(ip)},
+ },
+ },
+ },
+ }
+ _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("requested static ip %s not in any subnet on network %s", ip, defNet))
+ })
+ })
+
+ It("setup without namespace path", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {
+ InterfaceName: intName,
+ },
+ },
+ },
+ }
+ _, err := libpodNet.Setup("", setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("namespacePath is empty"))
+ })
+ })
+
+ It("setup with invalid namespace path", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {
+ InterfaceName: intName,
+ },
+ },
+ },
+ }
+ _, err := libpodNet.Setup("some path", setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(`"some path": no such file or directory`))
+ })
+ })
+
+ It("setup without container ID", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: "",
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {
+ InterfaceName: intName,
+ },
+ },
+ },
+ }
+ _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("ContainerID is empty"))
+ })
+ })
+
+ It("setup with aliases but dns disabled", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {
+ InterfaceName: intName,
+ Aliases: []string{"somealias"},
+ },
+ },
+ },
+ }
+ _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("cannot set aliases on a network without dns enabled"))
+ })
+ })
+
+ It("setup without networks", func() {
+ runTest(func() {
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ },
+ }
+ _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("must specify at least one network"))
+ })
+ })
+
+ It("setup without interface name", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {
+ InterfaceName: "",
+ },
+ },
+ },
+ }
+ _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("interface name on network %s is empty", defNet))
+ })
+ })
+
+ It("setup does teardown on failure", func() {
+ runTest(func() {
+ subnet1, _ := types.ParseCIDR("192.168.0.0/24")
+ network := types.Network{
+ Subnets: []types.Subnet{
+ {Subnet: subnet1},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+
+ subnet2, _ := types.ParseCIDR("192.168.1.0/31")
+ network = types.Network{
+ Subnets: []types.Subnet{
+ {Subnet: subnet2},
+ },
+ }
+ network2, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+
+ intName1 := "eth0"
+ intName2 := "eth1"
+ netName1 := network1.Name
+ netName2 := network2.Name
+
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ netName1: {
+ InterfaceName: intName1,
+ },
+ netName2: {
+ InterfaceName: intName2,
+ },
+ },
+ },
+ }
+ _, err = libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("Network 192.168.1.0/31 too small to allocate from"))
+ // Note: we call teardown on the failing net and log the error, it should be the same.
+ logString := logBuffer.String()
+ Expect(logString).To(ContainSubstring("Network 192.168.1.0/31 too small to allocate from"))
+
+ // check in the container namespace that no interface is there
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ defer GinkgoRecover()
+ _, err := net.InterfaceByName(intName1)
+ Expect(err).To(HaveOccurred())
+
+ // Note: We can check if intName2 is removed because
+ // the cni plugin fails before it removes the interface
+
+ // check loopback adapter
+ i, err := net.InterfaceByName("lo")
+ Expect(err).To(BeNil())
+ Expect(i.Name).To(Equal("lo"))
+ Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
+ Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
+ return nil
+ })
+ Expect(err).To(BeNil())
+ })
+ })
+
+ It("setup with exposed invalid port protocol", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ PortMappings: []types.PortMapping{{
+ Protocol: "someproto",
+ HostIP: "127.0.0.1",
+ HostPort: 5000,
+ ContainerPort: 5000,
+ }},
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {InterfaceName: intName},
+ },
+ },
+ }
+ _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("unknown port protocol someproto"))
+ })
+ })
+
+ It("setup with exposed empty port protocol", func() {
+ runTest(func() {
+ defNet := types.DefaultNetworkName
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ PortMappings: []types.PortMapping{{
+ Protocol: "",
+ HostIP: "127.0.0.1",
+ HostPort: 5000,
+ ContainerPort: 5000,
+ }},
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {InterfaceName: intName},
+ },
+ },
+ }
+ _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("port protocol should not be empty"))
+ })
+ })
+
+ It("setup with unknown network", func() {
+ runTest(func() {
+ defNet := "somenet"
+ intName := "eth0"
+ setupOpts := types.SetupOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ defNet: {InterfaceName: intName},
+ },
+ },
+ }
+ _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("network somenet: network not found"))
+ })
+ })
+
+ It("teardown with unknown network", func() {
+ runTest(func() {
+ interfaceName := "eth0"
+ netName := "somenet"
+ teardownOpts := types.TeardownOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {
+ InterfaceName: interfaceName,
+ },
+ },
+ },
+ }
+
+ err := libpodNet.Teardown(netNSContainer.Path(), teardownOpts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("network somenet: network not found"))
+ logString := logBuffer.String()
+ Expect(logString).To(ContainSubstring("failed to load cached network config"))
+ })
+ })
+
+ It("teardown on not connected network", func() {
+ runTest(func() {
+ network := types.Network{}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+
+ interfaceName := "eth0"
+ netName := network1.Name
+ teardownOpts := types.TeardownOptions{
+ NetworkOptions: types.NetworkOptions{
+ ContainerID: stringid.GenerateNonCryptoID(),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {
+ InterfaceName: interfaceName,
+ },
+ },
+ },
+ }
+
+ // Most CNI plugins do not error on teardown when there is nothing to do.
+ err = libpodNet.Teardown(netNSContainer.Path(), teardownOpts)
+ Expect(err).To(BeNil())
+ logString := logBuffer.String()
+ Expect(logString).To(ContainSubstring("failed to load cached network config"))
+ })
+ })
+ })
+})
+
+func runNetListener(wg *sync.WaitGroup, protocol, ip string, port int, expectedData string) {
+ switch protocol {
+ case "tcp":
+ ln, err := net.Listen(protocol, net.JoinHostPort(ip, strconv.Itoa(port)))
+ Expect(err).To(BeNil())
+ // make sure to read in a separate goroutine to not block
+ go func() {
+ defer GinkgoRecover()
+ defer wg.Done()
+ conn, err := ln.Accept()
+ Expect(err).To(BeNil())
+ conn.SetDeadline(time.Now().Add(1 * time.Second))
+ data, err := ioutil.ReadAll(conn)
+ Expect(err).To(BeNil())
+ Expect(string(data)).To(Equal(expectedData))
+ conn.Close()
+ ln.Close()
+ }()
+ case "udp":
+ conn, err := net.ListenUDP("udp", &net.UDPAddr{
+ IP: net.ParseIP(ip),
+ Port: port,
+ })
+ Expect(err).To(BeNil())
+ conn.SetDeadline(time.Now().Add(1 * time.Second))
+ go func() {
+ defer GinkgoRecover()
+ defer wg.Done()
+ data := make([]byte, len(expectedData))
+ i, err := conn.Read(data)
+ Expect(err).To(BeNil())
+ Expect(i).To(Equal(len(expectedData)))
+ Expect(string(data)).To(Equal(expectedData))
+ conn.Close()
+ }()
+ default:
+ Fail("unsupported protocol")
+ }
+}
diff --git a/libpod/network/cni/testfiles/invalid/broken.conflist b/libpod/network/cni/testfiles/invalid/broken.conflist
new file mode 100644
index 000000000..e5bf48b39
--- /dev/null
+++ b/libpod/network/cni/testfiles/invalid/broken.conflist
@@ -0,0 +1,25 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "bridge",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman9",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8.1"
+ }
+ ]
+ ]
diff --git a/libpod/network/cni/testfiles/invalid/invalid_gateway.conflist b/libpod/network/cni/testfiles/invalid/invalid_gateway.conflist
new file mode 100644
index 000000000..f03c1fde4
--- /dev/null
+++ b/libpod/network/cni/testfiles/invalid/invalid_gateway.conflist
@@ -0,0 +1,51 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "invalidgw",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman8",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8",
+ "rangeStart": "10.89.8.20",
+ "rangeEnd": "10.89.8.50"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/invalid/invalidname.conflist b/libpod/network/cni/testfiles/invalid/invalidname.conflist
new file mode 100644
index 000000000..e35be69db
--- /dev/null
+++ b/libpod/network/cni/testfiles/invalid/invalidname.conflist
@@ -0,0 +1,49 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "bridge@123",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman9",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8.1"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/invalid/noname.conflist b/libpod/network/cni/testfiles/invalid/noname.conflist
new file mode 100644
index 000000000..865abadf8
--- /dev/null
+++ b/libpod/network/cni/testfiles/invalid/noname.conflist
@@ -0,0 +1,48 @@
+{
+ "cniVersion": "0.4.0",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman9",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8.1"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/invalid/noplugin.conflist b/libpod/network/cni/testfiles/invalid/noplugin.conflist
new file mode 100644
index 000000000..af192adca
--- /dev/null
+++ b/libpod/network/cni/testfiles/invalid/noplugin.conflist
@@ -0,0 +1,5 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "bridge",
+ "plugins": []
+}
diff --git a/libpod/network/cni/testfiles/invalid/samename1.conflist b/libpod/network/cni/testfiles/invalid/samename1.conflist
new file mode 100644
index 000000000..57b325264
--- /dev/null
+++ b/libpod/network/cni/testfiles/invalid/samename1.conflist
@@ -0,0 +1,49 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "bridge",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman9",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8.1"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/invalid/samename2.conflist b/libpod/network/cni/testfiles/invalid/samename2.conflist
new file mode 100644
index 000000000..57b325264
--- /dev/null
+++ b/libpod/network/cni/testfiles/invalid/samename2.conflist
@@ -0,0 +1,49 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "bridge",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman9",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8.1"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/valid/87-podman.conflist b/libpod/network/cni/testfiles/valid/87-podman.conflist
new file mode 100644
index 000000000..ef760a61b
--- /dev/null
+++ b/libpod/network/cni/testfiles/valid/87-podman.conflist
@@ -0,0 +1,37 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "podman",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman0",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [{ "dst": "0.0.0.0/0" }],
+ "ranges": [
+ [
+ {
+ "subnet": "10.88.0.0/16",
+ "gateway": "10.88.0.1"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall"
+ },
+ {
+ "type": "tuning"
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/valid/bridge.conflist b/libpod/network/cni/testfiles/valid/bridge.conflist
new file mode 100644
index 000000000..8952b50b7
--- /dev/null
+++ b/libpod/network/cni/testfiles/valid/bridge.conflist
@@ -0,0 +1,51 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "bridge",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman9",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8.1",
+ "rangeStart": "10.89.8.20",
+ "rangeEnd": "10.89.8.50"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/valid/dualstack.conflist b/libpod/network/cni/testfiles/valid/dualstack.conflist
new file mode 100644
index 000000000..dd08382f0
--- /dev/null
+++ b/libpod/network/cni/testfiles/valid/dualstack.conflist
@@ -0,0 +1,58 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "dualstack",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman21",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "::/0"
+ },
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "fd10:88:a::/64",
+ "gateway": "fd10:88:a::1"
+ }
+ ],
+ [
+ {
+ "subnet": "10.89.19.0/24",
+ "gateway": "10.89.19.10"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/valid/internal.conflist b/libpod/network/cni/testfiles/valid/internal.conflist
new file mode 100644
index 000000000..1b6f15a96
--- /dev/null
+++ b/libpod/network/cni/testfiles/valid/internal.conflist
@@ -0,0 +1,40 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "internal",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman8",
+ "isGateway": false,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.7.0/24"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/valid/label.conflist b/libpod/network/cni/testfiles/valid/label.conflist
new file mode 100644
index 000000000..1501f9bd7
--- /dev/null
+++ b/libpod/network/cni/testfiles/valid/label.conflist
@@ -0,0 +1,54 @@
+{
+ "args": {
+ "podman_labels": {
+ "mykey": "value"
+ }
+ },
+ "cniVersion": "0.4.0",
+ "name": "label",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman15",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.13.0/24",
+ "gateway": "10.89.13.1"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/valid/macvlan.conflist b/libpod/network/cni/testfiles/valid/macvlan.conflist
new file mode 100644
index 000000000..8f3692334
--- /dev/null
+++ b/libpod/network/cni/testfiles/valid/macvlan.conflist
@@ -0,0 +1,13 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "macvlan",
+ "plugins": [
+ {
+ "type": "macvlan",
+ "master": "lo",
+ "ipam": {
+ "type": "dhcp"
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/valid/macvlan_mtu.conflist b/libpod/network/cni/testfiles/valid/macvlan_mtu.conflist
new file mode 100644
index 000000000..2fd259117
--- /dev/null
+++ b/libpod/network/cni/testfiles/valid/macvlan_mtu.conflist
@@ -0,0 +1,14 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "macvlan_mtu",
+ "plugins": [
+ {
+ "type": "macvlan",
+ "master": "lo",
+ "ipam": {
+ "type": "dhcp"
+ },
+ "mtu": 1300
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/valid/mtu.conflist b/libpod/network/cni/testfiles/valid/mtu.conflist
new file mode 100644
index 000000000..db5f7e194
--- /dev/null
+++ b/libpod/network/cni/testfiles/valid/mtu.conflist
@@ -0,0 +1,49 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "mtu",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman13",
+ "isGateway": true,
+ "ipMasq": true,
+ "mtu": 1500,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.11.0/24"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/cni/testfiles/valid/vlan.conflist b/libpod/network/cni/testfiles/valid/vlan.conflist
new file mode 100644
index 000000000..75e8967f1
--- /dev/null
+++ b/libpod/network/cni/testfiles/valid/vlan.conflist
@@ -0,0 +1,50 @@
+{
+ "cniVersion": "0.4.0",
+ "name": "vlan",
+ "plugins": [
+ {
+ "type": "bridge",
+ "bridge": "cni-podman14",
+ "isGateway": true,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "vlan": 5,
+ "ipam": {
+ "type": "host-local",
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "ranges": [
+ [
+ {
+ "subnet": "10.89.12.0/24",
+ "gateway": "10.89.12.1"
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ },
+ {
+ "type": "firewall",
+ "backend": ""
+ },
+ {
+ "type": "tuning"
+ },
+ {
+ "type": "dnsname",
+ "domainName": "dns.podman",
+ "capabilities": {
+ "aliases": true
+ }
+ }
+ ]
+}
diff --git a/libpod/network/types/const.go b/libpod/network/types/const.go
new file mode 100644
index 000000000..be7ef03cf
--- /dev/null
+++ b/libpod/network/types/const.go
@@ -0,0 +1,21 @@
+package types
+
+const (
+ // BridgeNetworkDriver defines the bridge driver
+ BridgeNetworkDriver = "bridge"
+ // DefaultNetworkDriver is the default network type used
+ DefaultNetworkDriver = BridgeNetworkDriver
+ // MacVLANNetworkDriver defines the macvlan driver
+ MacVLANNetworkDriver = "macvlan"
+
+ // IPAM drivers
+ // HostLocalIPAMDriver store the ip
+ HostLocalIPAMDriver = "host-local"
+ // DHCPIPAMDriver get subnet and ip from dhcp server
+ DHCPIPAMDriver = "dhcp"
+
+ // DefaultSubnet is the name that will be used for the default CNI network.
+ DefaultNetworkName = "podman"
+ // DefaultSubnet is the subnet that will be used for the default CNI network.
+ DefaultSubnet = "10.88.0.0/16"
+)
diff --git a/libpod/network/types/network.go b/libpod/network/types/network.go
new file mode 100644
index 000000000..c2c598f46
--- /dev/null
+++ b/libpod/network/types/network.go
@@ -0,0 +1,208 @@
+package types
+
+import (
+ "net"
+ "time"
+)
+
+type ContainerNetwork interface {
+ // NetworkCreate will take a partial filled Network and fill the
+ // missing fields. It creates the Network and returns the full Network.
+ NetworkCreate(Network) (Network, error)
+ // NetworkRemove will remove the Network with the given name or ID.
+ NetworkRemove(nameOrID string) error
+ // NetworkList will return all known Networks. Optionally you can
+ // supply a list of filter functions. Only if a network matches all
+ // functions it is returned.
+ NetworkList(...FilterFunc) ([]Network, error)
+ // NetworkInspect will return the Network with the given name or ID.
+ NetworkInspect(nameOrID string) (Network, error)
+
+ // Setup will setup the container network namespace. It returns
+ // a map of StatusBlocks, the key is the network name.
+ Setup(namespacePath string, options SetupOptions) (map[string]StatusBlock, error)
+ // Teardown will teardown the container network namespace.
+ Teardown(namespacePath string, options TeardownOptions) error
+}
+
+// Network describes the Network attributes.
+type Network struct {
+ // Name of the Network.
+ Name string `json:"name,omitempty"`
+ // ID of the Network.
+ ID string `json:"id,omitempty"`
+ // Driver for this Network, e.g. bridge, macvlan...
+ Driver string `json:"driver,omitempty"`
+ // 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
+ // Subnets to use.
+ Subnets []Subnet `json:"subnets,omitempty"`
+ // IPv6Enabled if set to true an ipv6 subnet should be created for this net.
+ IPv6Enabled bool `json:"ipv6_enabled"`
+ // Internal is whether the Network should not have external routes
+ // to public or other Networks.
+ Internal bool `json:"internal"`
+ // DNSEnabled is whether name resolution is active for container on
+ // this Network.
+ DNSEnabled bool `json:"dns_enabled"`
+ // Labels is a set of key-value labels that have been applied to the
+ // Network.
+ Labels map[string]string `json:"labels,omitempty"`
+ // Options is a set of key-value options that have been applied to
+ // the Network.
+ Options map[string]string `json:"options,omitempty"`
+ // IPAMOptions contains options used for the ip assignment.
+ IPAMOptions map[string]string `json:"ipam_options,omitempty"`
+}
+
+// IPNet is used as custom net.IPNet type to add Marshal/Unmarshal methods.
+type IPNet struct {
+ net.IPNet
+}
+
+// ParseCIDR parse a string to IPNet
+func ParseCIDR(cidr string) (IPNet, error) {
+ ip, net, err := net.ParseCIDR(cidr)
+ if err != nil {
+ return IPNet{}, err
+ }
+ // convert to 4 bytes if ipv4
+ ipv4 := ip.To4()
+ if ipv4 != nil {
+ ip = ipv4
+ }
+ net.IP = ip
+ return IPNet{*net}, err
+}
+
+func (n *IPNet) MarshalText() ([]byte, error) {
+ return []byte(n.String()), nil
+}
+
+func (n *IPNet) UnmarshalText(text []byte) error {
+ net, err := ParseCIDR(string(text))
+ if err != nil {
+ return err
+ }
+ *n = net
+ return nil
+}
+
+type Subnet struct {
+ // Subnet for this Network.
+ Subnet IPNet `json:"subnet,omitempty"`
+ // Gateway IP for this Network.
+ Gateway net.IP `json:"gateway,omitempty"`
+ // LeaseRange contains the range where IP are leased. Optional.
+ LeaseRange *LeaseRange `json:"lease_range,omitempty"`
+}
+
+// LeaseRange contains the range where IP are leased.
+type LeaseRange struct {
+ // StartIP first IP in the subnet which should be used to assign ips.
+ StartIP net.IP `json:"start_ip,omitempty"`
+ // EndIP last IP in the subnet which should be used to assign ips.
+ EndIP net.IP `json:"end_ip,omitempty"`
+}
+
+// StatusBlock contains the network information about a container
+// connected to one Network.
+type StatusBlock struct {
+ // Interfaces contains the created network interface in the container.
+ // The map key is the interface name.
+ Interfaces map[string]NetInterface `json:"interfaces,omitempty"`
+ // DNSServerIPs nameserver addresses which should be added to
+ // the containers resolv.conf file.
+ DNSServerIPs []net.IP `json:"dns_server_ips,omitempty"`
+ // DNSSearchDomains search domains which should be added to
+ // the containers resolv.conf file.
+ DNSSearchDomains []string `json:"dns_search_domains,omitempty"`
+}
+
+// NetInterface contains the settings for a given network interface.
+type NetInterface struct {
+ // Networks list of assigned subnets with their gateway.
+ Networks []NetAddress `json:"networks,omitempty"`
+ // MacAddress for this Interface.
+ MacAddress net.HardwareAddr `json:"mac_address,omitempty"`
+}
+
+// NetAddress contains the subnet and gatway.
+type NetAddress struct {
+ // Subnet of this NetAddress. Note that the subnet contains the
+ // actual ip of the net interface and not the network address.
+ Subnet IPNet `json:"subnet,omitempty"`
+ // Gateway for the Subnet. This can be nil if there is no gateway, e.g. internal network.
+ Gateway net.IP `json:"gateway,omitempty"`
+}
+
+// PerNetworkOptions are options which should be set on a per network basis.
+type PerNetworkOptions struct {
+ // StaticIPv4 for this container. Optional.
+ StaticIPs []net.IP `json:"static_ips,omitempty"`
+ // Aliases contains a list of names which the dns server should resolve
+ // to this container. Can only be set when DNSEnabled is true on the Network.
+ // Optional.
+ Aliases []string `json:"aliases,omitempty"`
+ // StaticMac for this container. Optional.
+ StaticMAC net.HardwareAddr `json:"static_mac,omitempty"`
+ // InterfaceName for this container. Required.
+ InterfaceName string `json:"interface_name,omitempty"`
+}
+
+// NetworkOptions for a given container.
+type NetworkOptions struct {
+ // ContainerID is the container id, used for iptables comments and ipam allocation.
+ ContainerID string `json:"container_id,omitempty"`
+ // ContainerName is the container name, used as dns name.
+ ContainerName string `json:"container_name,omitempty"`
+ // PortMappings contains the port mappings for this container
+ PortMappings []PortMapping `json:"port_mappings,omitempty"`
+ // Networks contains all networks with the PerNetworkOptions.
+ // The map should contain at least one element.
+ Networks map[string]PerNetworkOptions `json:"networks,omitempty"`
+}
+
+// PortMapping is one or more ports that will be mapped into the container.
+type PortMapping struct {
+ // HostIP is the IP that we will bind to on the host.
+ // If unset, assumed to be 0.0.0.0 (all interfaces).
+ HostIP string `json:"host_ip,omitempty"`
+ // ContainerPort is the port number that will be exposed from the
+ // container.
+ // Mandatory.
+ ContainerPort uint16 `json:"container_port"`
+ // HostPort is the port number that will be forwarded from the host into
+ // the container.
+ // If omitted, a random port on the host (guaranteed to be over 1024)
+ // will be assigned.
+ HostPort uint16 `json:"host_port,omitempty"`
+ // Range is the number of ports that will be forwarded, starting at
+ // HostPort and ContainerPort and counting up.
+ // This is 1-indexed, so 1 is assumed to be a single port (only the
+ // Hostport:Containerport mapping will be added), 2 is two ports (both
+ // Hostport:Containerport and Hostport+1:Containerport+1), etc.
+ // If unset, assumed to be 1 (a single port).
+ // Both hostport + range and containerport + range must be less than
+ // 65536.
+ Range uint16 `json:"range,omitempty"`
+ // Protocol is the protocol forward.
+ // Must be either "tcp", "udp", and "sctp", or some combination of these
+ // separated by commas.
+ // If unset, assumed to be TCP.
+ Protocol string `json:"protocol,omitempty"`
+}
+
+type SetupOptions struct {
+ NetworkOptions
+}
+
+type TeardownOptions struct {
+ NetworkOptions
+}
+
+// FilterFunc can be passed to NetworkList to filter the networks.
+type FilterFunc func(Network) bool
diff --git a/libpod/network/util/filters.go b/libpod/network/util/filters.go
new file mode 100644
index 000000000..48e769196
--- /dev/null
+++ b/libpod/network/util/filters.go
@@ -0,0 +1,55 @@
+package util
+
+import (
+ "strings"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/pkg/util"
+ "github.com/pkg/errors"
+)
+
+func GenerateNetworkFilters(filters map[string][]string) ([]types.FilterFunc, error) {
+ filterFuncs := make([]types.FilterFunc, 0, len(filters))
+ for key, filterValues := range filters {
+ filterFunc, err := createFilterFuncs(key, filterValues)
+ if err != nil {
+ return nil, err
+ }
+ filterFuncs = append(filterFuncs, filterFunc)
+ }
+ return filterFuncs, nil
+}
+
+func createFilterFuncs(key string, filterValues []string) (types.FilterFunc, error) {
+ switch strings.ToLower(key) {
+ case "name":
+ // matches one name, regex allowed
+ return func(net types.Network) bool {
+ 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 {
+ return util.StringInSlice(net.Driver, filterValues)
+ }, nil
+
+ case "id":
+ // matches part of one id
+ return func(net types.Network) bool {
+ return util.StringMatchRegexSlice(net.ID, filterValues)
+ }, nil
+
+ // FIXME: What should we do with the old plugin filter
+ // TODO: add dangling, dns enabled, internal filter
+
+ default:
+ return nil, errors.Errorf("invalid filter %q", key)
+ }
+}
diff --git a/libpod/network/util/interfaces.go b/libpod/network/util/interfaces.go
new file mode 100644
index 000000000..dc2bd601d
--- /dev/null
+++ b/libpod/network/util/interfaces.go
@@ -0,0 +1,34 @@
+package util
+
+import "net"
+
+// GetLiveNetworkSubnets returns a slice of subnets representing what the system
+// has defined as network interfaces
+func GetLiveNetworkSubnets() ([]*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 interface names 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
+}
diff --git a/libpod/network/util/ip.go b/libpod/network/util/ip.go
new file mode 100644
index 000000000..b2ba92735
--- /dev/null
+++ b/libpod/network/util/ip.go
@@ -0,0 +1,113 @@
+package util
+
+import (
+ "crypto/rand"
+ "net"
+
+ "github.com/pkg/errors"
+)
+
+// IsIPv6 returns true if netIP is IPv6.
+func IsIPv6(netIP net.IP) bool {
+ return netIP != nil && netIP.To4() == nil
+}
+
+// IsIPv4 returns true if netIP is IPv4.
+func IsIPv4(netIP net.IP) bool {
+ return netIP != nil && netIP.To4() != nil
+}
+
+func incByte(subnet *net.IPNet, idx int, shift uint) error {
+ if idx < 0 {
+ return errors.New("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, errors.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
+}
+
+func NetworkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) bool {
+ for _, nw := range networklist {
+ if networkIntersect(n, nw) {
+ return true
+ }
+ }
+ return false
+}
+
+func networkIntersect(n1, n2 *net.IPNet) bool {
+ return n2.Contains(n1.IP) || n1.Contains(n2.IP)
+}
+
+// GetRandomIPv6Subnet returns a random internal ipv6 subnet as described in RFC3879.
+func GetRandomIPv6Subnet() (net.IPNet, error) {
+ ip := make(net.IP, 8, net.IPv6len)
+ // read 8 random bytes
+ _, err := rand.Read(ip)
+ if err != nil {
+ return net.IPNet{}, nil
+ }
+ // first byte must be FD as per RFC3879
+ ip[0] = 0xfd
+ // add 8 zero bytes
+ ip = append(ip, make([]byte, 8)...)
+ return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil
+}
diff --git a/libpod/network/util/ip_test.go b/libpod/network/util/ip_test.go
new file mode 100644
index 000000000..c26ad140a
--- /dev/null
+++ b/libpod/network/util/ip_test.go
@@ -0,0 +1,125 @@
+package util
+
+import (
+ "fmt"
+ "net"
+ "reflect"
+ "testing"
+)
+
+func parseCIDR(n string) *net.IPNet {
+ _, parsedNet, _ := net.ParseCIDR(n)
+ return parsedNet
+}
+
+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)
+ }
+ })
+ }
+}
+
+func TestGetRandomIPv6Subnet(t *testing.T) {
+ for i := 0; i < 1000; i++ {
+ t.Run(fmt.Sprintf("GetRandomIPv6Subnet %d", i), func(t *testing.T) {
+ sub, err := GetRandomIPv6Subnet()
+ if err != nil {
+ t.Errorf("GetRandomIPv6Subnet() error should be nil: %v", err)
+ return
+ }
+ if sub.IP.To4() != nil {
+ t.Errorf("ip %s is not an ipv6 address", sub.IP)
+ }
+ if sub.IP[0] != 0xfd {
+ t.Errorf("ipv6 %s does not start with fd", sub.IP)
+ }
+ ones, bytes := sub.Mask.Size()
+ if ones != 64 || bytes != 128 {
+ t.Errorf("wrong network mask %v, it should be /64", sub.Mask)
+ }
+ })
+ }
+}
diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go
index d2a7505a8..607e68256 100644
--- a/pkg/domain/entities/containers.go
+++ b/pkg/domain/entities/containers.go
@@ -8,6 +8,7 @@ import (
"github.com/containers/image/v5/types"
"github.com/containers/podman/v3/libpod/define"
+ nettypes "github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/containers/storage/pkg/archive"
"github.com/cri-o/ocicni/pkg/ocicni"
@@ -208,7 +209,7 @@ type RestoreOptions struct {
Name string
TCPEstablished bool
ImportPrevious string
- PublishPorts []specgen.PortMapping
+ PublishPorts []nettypes.PortMapping
Pod string
}
diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go
index 9e25b7bf8..db4c6bb8a 100644
--- a/pkg/domain/entities/types.go
+++ b/pkg/domain/entities/types.go
@@ -6,6 +6,7 @@ import (
buildahDefine "github.com/containers/buildah/define"
"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/specgen"
"github.com/containers/storage/pkg/archive"
)
@@ -40,7 +41,7 @@ type NetOptions struct {
DNSServers []net.IP
Network specgen.Namespace
NoHosts bool
- PublishPorts []specgen.PortMapping
+ PublishPorts []types.PortMapping
StaticIP *net.IP
StaticMAC *net.HardwareAddr
// NetworkOptions are additional options for each network
diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go
index d6d479c67..04b4e5ab3 100644
--- a/pkg/specgen/generate/kube/kube.go
+++ b/pkg/specgen/generate/kube/kube.go
@@ -12,6 +12,7 @@ import (
"github.com/containers/common/pkg/parse"
"github.com/containers/common/pkg/secrets"
"github.com/containers/image/v5/manifest"
+ "github.com/containers/podman/v3/libpod/network/types"
ann "github.com/containers/podman/v3/pkg/annotations"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/containers/podman/v3/pkg/specgen/generate"
@@ -588,8 +589,8 @@ func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (string, error) {
// getPodPorts converts a slice of kube container descriptions to an
// array of portmapping
-func getPodPorts(containers []v1.Container) []specgen.PortMapping {
- var infraPorts []specgen.PortMapping
+func getPodPorts(containers []v1.Container) []types.PortMapping {
+ var infraPorts []types.PortMapping
for _, container := range containers {
for _, p := range container.Ports {
if p.HostPort != 0 && p.ContainerPort == 0 {
@@ -598,7 +599,7 @@ func getPodPorts(containers []v1.Container) []specgen.PortMapping {
if p.Protocol == "" {
p.Protocol = "tcp"
}
- portBinding := specgen.PortMapping{
+ portBinding := types.PortMapping{
HostPort: uint16(p.HostPort),
ContainerPort: uint16(p.ContainerPort),
Protocol: strings.ToLower(string(p.Protocol)),
diff --git a/pkg/specgen/generate/ports.go b/pkg/specgen/generate/ports.go
index c00ad19fb..bd64080b1 100644
--- a/pkg/specgen/generate/ports.go
+++ b/pkg/specgen/generate/ports.go
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/containers/common/libimage"
+ "github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/utils"
"github.com/containers/podman/v3/pkg/specgen"
@@ -24,7 +25,7 @@ const (
// Parse port maps to OCICNI port mappings.
// Returns a set of OCICNI port mappings, and maps of utilized container and
// host ports.
-func ParsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping, map[string]map[string]map[uint16]uint16, map[string]map[string]map[uint16]uint16, error) {
+func ParsePortMapping(portMappings []types.PortMapping) ([]ocicni.PortMapping, map[string]map[string]map[uint16]uint16, map[string]map[string]map[uint16]uint16, error) {
// First, we need to validate the ports passed in the specgen, and then
// convert them into CNI port mappings.
type tempMapping struct {
diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go
index 893ebf675..386571d11 100644
--- a/pkg/specgen/podspecgen.go
+++ b/pkg/specgen/podspecgen.go
@@ -3,6 +3,7 @@ package specgen
import (
"net"
+ "github.com/containers/podman/v3/libpod/network/types"
spec "github.com/opencontainers/runtime-spec/specs-go"
)
@@ -102,7 +103,7 @@ type PodNetworkConfig struct {
// container, this will forward the ports to the entire pod.
// Only available if NetNS is set to Bridge or Slirp.
// Optional.
- PortMappings []PortMapping `json:"portmappings,omitempty"`
+ PortMappings []types.PortMapping `json:"portmappings,omitempty"`
// CNINetworks is a list of CNI networks that the infra container will
// join. As, by default, containers share their network with the infra
// container, these networks will effectively be joined by the
diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go
index 2252ef405..0c30c498a 100644
--- a/pkg/specgen/specgen.go
+++ b/pkg/specgen/specgen.go
@@ -5,6 +5,7 @@ import (
"syscall"
"github.com/containers/image/v5/manifest"
+ nettypes "github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/storage/types"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
@@ -393,7 +394,7 @@ type ContainerNetworkConfig struct {
// PortBindings is a set of ports to map into the container.
// Only available if NetNS is set to bridge or slirp.
// Optional.
- PortMappings []PortMapping `json:"portmappings,omitempty"`
+ PortMappings []nettypes.PortMapping `json:"portmappings,omitempty"`
// PublishExposedPorts will publish ports specified in the image to
// random unused ports (guaranteed to be above 1024) on the host.
// This is based on ports set in Expose below, and any ports specified
@@ -506,36 +507,6 @@ type SpecGenerator struct {
ContainerHealthCheckConfig
}
-// PortMapping is one or more ports that will be mapped into the container.
-type PortMapping struct {
- // HostIP is the IP that we will bind to on the host.
- // If unset, assumed to be 0.0.0.0 (all interfaces).
- HostIP string `json:"host_ip,omitempty"`
- // ContainerPort is the port number that will be exposed from the
- // container.
- // Mandatory.
- ContainerPort uint16 `json:"container_port"`
- // HostPort is the port number that will be forwarded from the host into
- // the container.
- // If omitted, a random port on the host (guaranteed to be over 1024)
- // will be assigned.
- HostPort uint16 `json:"host_port,omitempty"`
- // Range is the number of ports that will be forwarded, starting at
- // HostPort and ContainerPort and counting up.
- // This is 1-indexed, so 1 is assumed to be a single port (only the
- // Hostport:Containerport mapping will be added), 2 is two ports (both
- // Hostport:Containerport and Hostport+1:Containerport+1), etc.
- // If unset, assumed to be 1 (a single port).
- // Both hostport + range and containerport + range must be less than
- // 65536.
- Range uint16 `json:"range,omitempty"`
- // Protocol is the protocol forward.
- // Must be either "tcp", "udp", and "sctp", or some combination of these
- // separated by commas.
- // If unset, assumed to be TCP.
- Protocol string `json:"protocol,omitempty"`
-}
-
type Secret struct {
Source string
UID uint32
diff --git a/pkg/systemd/generate/common.go b/pkg/systemd/generate/common.go
index 45e12014a..49465fb30 100644
--- a/pkg/systemd/generate/common.go
+++ b/pkg/systemd/generate/common.go
@@ -71,11 +71,12 @@ func filterCommonContainerFlags(command []string, argCount int) []string {
case s == "--rm":
// Boolean flags support --flag and --flag={true,false}.
continue
- case s == "--sdnotify", s == "--cgroups":
+ case s == "--sdnotify", s == "--cgroups", s == "--cidfile":
i++
continue
case strings.HasPrefix(s, "--rm="),
- strings.HasPrefix(s, "--cgroups="):
+ strings.HasPrefix(s, "--cgroups="),
+ strings.HasPrefix(s, "--cidfile="):
continue
}
processed = append(processed, s)
diff --git a/pkg/systemd/generate/common_test.go b/pkg/systemd/generate/common_test.go
index 3e2ac015f..80abebb26 100644
--- a/pkg/systemd/generate/common_test.go
+++ b/pkg/systemd/generate/common_test.go
@@ -103,12 +103,12 @@ func TestFilterCommonContainerFlags(t *testing.T) {
},
{
[]string{"podman", "run", "--cidfile", "foo", "alpine"},
- []string{"podman", "run", "--cidfile", "foo", "alpine"},
+ []string{"podman", "run", "alpine"},
1,
},
{
[]string{"podman", "run", "--cidfile=foo", "alpine"},
- []string{"podman", "run", "--cidfile=foo", "alpine"},
+ []string{"podman", "run", "alpine"},
1,
},
{
diff --git a/pkg/systemd/generate/containers.go b/pkg/systemd/generate/containers.go
index 78b81b54b..931f13972 100644
--- a/pkg/systemd/generate/containers.go
+++ b/pkg/systemd/generate/containers.go
@@ -233,9 +233,10 @@ func executeContainerTemplate(info *containerInfo, options entities.GenerateSyst
info.Type = "notify"
info.NotifyAccess = "all"
info.PIDFile = ""
- info.ContainerIDFile = ""
- info.ExecStop = ""
- info.ExecStopPost = ""
+ info.ContainerIDFile = "%t/%n.ctr-id"
+ info.ExecStartPre = "/bin/rm -f {{{{.ContainerIDFile}}}}"
+ info.ExecStop = "{{{{.Executable}}}} stop --ignore --cidfile={{{{.ContainerIDFile}}}}"
+ info.ExecStopPost = "{{{{.Executable}}}} rm -f --ignore --cidfile={{{{.ContainerIDFile}}}}"
// The create command must at least have three arguments:
// /usr/bin/podman run $IMAGE
index := 0
@@ -258,6 +259,7 @@ func executeContainerTemplate(info *containerInfo, options entities.GenerateSyst
}
startCommand = append(startCommand,
"run",
+ "--cidfile={{{{.ContainerIDFile}}}}",
"--cgroups=no-conmon",
"--rm",
)
diff --git a/pkg/systemd/generate/containers_test.go b/pkg/systemd/generate/containers_test.go
index 6141950d0..c60c301cc 100644
--- a/pkg/systemd/generate/containers_test.go
+++ b/pkg/systemd/generate/containers_test.go
@@ -130,7 +130,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman container run --cgroups=no-conmon --rm --sdnotify=conmon -d --replace --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN "foo=arg \"with \" space"
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman container run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d --replace --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN "foo=arg \"with \" space"
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -152,7 +155,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman container run --cgroups=no-conmon --rm -d --replace --sdnotify=container --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN "foo=arg \"with \" space"
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman container run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm -d --replace --sdnotify=container --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN "foo=arg \"with \" space"
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -174,7 +180,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --sdnotify=conmon --replace -d --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon --replace -d --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -196,7 +205,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --pod-id-file %t/pod-foobar.pod-id-file --sdnotify=conmon --replace -d --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --pod-id-file %t/pod-foobar.pod-id-file --sdnotify=conmon --replace -d --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -218,7 +230,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --sdnotify=conmon --replace --detach --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon --replace --detach --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -240,7 +255,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --sdnotify=conmon -d awesome-image:latest
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d awesome-image:latest
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -263,9 +281,12 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=102
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --sdnotify=conmon ` +
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon ` +
detachparam +
` awesome-image:latest
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -289,7 +310,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=102
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --sdnotify=conmon -d --replace --name test -p 80:80 awesome-image:latest somecmd --detach=false
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d --replace --name test -p 80:80 awesome-image:latest somecmd --detach=false
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -311,7 +335,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=102
-ExecStart=/usr/bin/podman --events-backend none --runroot /root run --cgroups=no-conmon --rm --sdnotify=conmon -d awesome-image:latest
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman --events-backend none --runroot /root run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d awesome-image:latest
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -333,7 +360,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman container run --cgroups=no-conmon --rm --sdnotify=conmon -d awesome-image:latest
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman container run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d awesome-image:latest
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -355,7 +385,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --sdnotify=conmon -d --replace --name test --log-driver=journald --log-opt=tag={{.Name}} awesome-image:latest
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d --replace --name test --log-driver=journald --log-opt=tag={{.Name}} awesome-image:latest
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -377,7 +410,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --sdnotify=conmon -d --replace --name test awesome-image:latest sh -c "kill $$$$ && echo %%\\"
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d --replace --name test awesome-image:latest sh -c "kill $$$$ && echo %%\\"
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -399,7 +435,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --sdnotify=conmon -d --conmon-pidfile=foo --cidfile=foo awesome-image:latest podman run --cgroups=foo --conmon-pidfile=foo --cidfile=foo alpine
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d --conmon-pidfile=foo awesome-image:latest podman run --cgroups=foo --conmon-pidfile=foo --cidfile=foo alpine
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -421,7 +460,10 @@ RequiresMountsFor=/var/run/containers/storage
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --pod-id-file %t/pod-foobar.pod-id-file --sdnotify=conmon -d --conmon-pidfile=foo --cidfile=foo awesome-image:latest podman run --cgroups=foo --conmon-pidfile=foo --cidfile=foo --pod-id-file /tmp/pod-foobar.pod-id-file alpine
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --pod-id-file %t/pod-foobar.pod-id-file --sdnotify=conmon -d --conmon-pidfile=foo awesome-image:latest podman run --cgroups=foo --conmon-pidfile=foo --cidfile=foo --pod-id-file /tmp/pod-foobar.pod-id-file alpine
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
@@ -444,7 +486,10 @@ Environment=PODMAN_SYSTEMD_UNIT=%n
Environment=FOO=abc "BAR=my test" USER=%%a
Restart=always
TimeoutStopSec=70
-ExecStart=/usr/bin/podman run --cgroups=no-conmon --rm --sdnotify=conmon -d --env FOO --env=BAR --env=MYENV=2 -e USER awesome-image:latest
+ExecStartPre=/bin/rm -f %t/%n.ctr-id
+ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon -d --env FOO --env=BAR --env=MYENV=2 -e USER awesome-image:latest
+ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
+ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
diff --git a/test/system/250-systemd.bats b/test/system/250-systemd.bats
index ee951ff21..5d4ae4cb1 100644
--- a/test/system/250-systemd.bats
+++ b/test/system/250-systemd.bats
@@ -46,11 +46,20 @@ function service_setup() {
# Helper to stop a systemd service running a container
function service_cleanup() {
+ local status=$1
run systemctl stop "$SERVICE_NAME"
if [ $status -ne 0 ]; then
die "Error stopping systemd unit $SERVICE_NAME, output: $output"
fi
+ if [[ -z "$status" ]]; then
+ run systemctl is-active "$SERVICE_NAME"
+ if [ $status -ne 0 ]; then
+ die "Error checking stauts of systemd unit $SERVICE_NAME, output: $output"
+ fi
+ is "$output" "$status" "$SERVICE_NAME not in expected state"
+ fi
+
rm -f "$UNIT_FILE"
systemctl daemon-reload
}
@@ -60,7 +69,8 @@ function service_cleanup() {
@test "podman generate - systemd - basic" {
cname=$(random_string)
# See #7407 for --pull=always.
- run_podman create --pull=always --name $cname --label "io.containers.autoupdate=registry" $IMAGE top
+ run_podman create --pull=always --name $cname --label "io.containers.autoupdate=registry" $IMAGE \
+ sh -c "trap 'echo Received SIGTERM, finishing; exit' SIGTERM; echo WAITING; while :; do sleep 0.1; done"
# Start systemd service to run this container
service_setup
@@ -68,7 +78,7 @@ function service_cleanup() {
# Give container time to start; make sure output looks top-like
sleep 2
run_podman logs $cname
- is "$output" ".*Load average:.*" "running container 'top'-like output"
+ is "$output" ".*WAITING.*" "running is waiting for signal"
# Exercise `podman auto-update`.
# TODO: this will at least run auto-update code but won't perform an update
@@ -77,7 +87,8 @@ function service_cleanup() {
run_podman auto-update
# All good. Stop service, clean up.
- service_cleanup
+ # Also make sure the service is in the `inactive` state (see #11304).
+ service_cleanup inactive
}
@test "podman autoupdate local" {
diff --git a/utils/utils_supported.go b/utils/utils_supported.go
index 8668e3fba..ebc870d26 100644
--- a/utils/utils_supported.go
+++ b/utils/utils_supported.go
@@ -36,6 +36,7 @@ func RunUnderSystemdScope(pid int, slice string, unitName string) error {
return err
}
}
+ defer conn.Close()
properties = append(properties, systemdDbus.PropSlice(slice))
properties = append(properties, newProp("PIDs", []uint32{uint32(pid)}))
properties = append(properties, newProp("Delegate", true))
@@ -54,7 +55,6 @@ func RunUnderSystemdScope(pid int, slice string, unitName string) error {
}
return err
}
- defer conn.Close()
// Block until job is started
<-ch