summaryrefslogtreecommitdiff
path: root/libpod/network/cni
diff options
context:
space:
mode:
Diffstat (limited to 'libpod/network/cni')
-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
26 files changed, 4999 insertions, 0 deletions
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
+ }
+ }
+ ]
+}