diff options
author | baude <bbaude@redhat.com> | 2020-10-06 12:24:21 -0500 |
---|---|---|
committer | baude <bbaude@redhat.com> | 2020-10-07 10:03:21 -0500 |
commit | fe3faa517e1bbc3b2e82afaae32d8712c844fdae (patch) | |
tree | 3b4a74edc98a2861d2e1b6bb1d9769e078b9ba3c /libpod/network | |
parent | defb754945b3f99c1d786dac95d9b17b24f55e59 (diff) | |
download | podman-fe3faa517e1bbc3b2e82afaae32d8712c844fdae.tar.gz podman-fe3faa517e1bbc3b2e82afaae32d8712c844fdae.tar.bz2 podman-fe3faa517e1bbc3b2e82afaae32d8712c844fdae.zip |
prevent unpredictable results with network create|remove
due to a lack of "locking" on cni operations, we could get ourselves in trouble when doing rapid creation or removal of networks. added a simple file lock to deal with the collision and because it is not considered a performent path, use of the file lock should be ok. if proven otherwise in the future, some generic shared memory lock should be implemented for libpod and also used here.
moved pkog/network to libpod/network because libpod is now being pulled into the package and it has therefore lost its generic nature. this will make it easier to absorb into libpod as we try to make the network closer to core operations.
Fixes: #7807
Signed-off-by: baude <bbaude@redhat.com>
Diffstat (limited to 'libpod/network')
-rw-r--r-- | libpod/network/config.go | 141 | ||||
-rw-r--r-- | libpod/network/create.go | 195 | ||||
-rw-r--r-- | libpod/network/devices.go | 63 | ||||
-rw-r--r-- | libpod/network/files.go | 174 | ||||
-rw-r--r-- | libpod/network/ip.go | 19 | ||||
-rw-r--r-- | libpod/network/lock.go | 26 | ||||
-rw-r--r-- | libpod/network/netconflist.go | 155 | ||||
-rw-r--r-- | libpod/network/netconflist_test.go | 38 | ||||
-rw-r--r-- | libpod/network/network.go | 225 | ||||
-rw-r--r-- | libpod/network/network_test.go | 35 | ||||
-rw-r--r-- | libpod/network/subnet.go | 78 | ||||
-rw-r--r-- | libpod/network/subnet_test.go | 35 |
12 files changed, 1184 insertions, 0 deletions
diff --git a/libpod/network/config.go b/libpod/network/config.go new file mode 100644 index 000000000..a08e684d8 --- /dev/null +++ b/libpod/network/config.go @@ -0,0 +1,141 @@ +package network + +import ( + "encoding/json" + "net" + + "github.com/containers/storage/pkg/lockfile" +) + +// TODO once the containers.conf file stuff is worked out, this should be modified +// to honor defines in the containers.conf as well as overrides? + +const ( + // CNIConfigDir is the path where CNI config files exist + CNIConfigDir = "/etc/cni/net.d" + // CNIDeviceName is the default network device name and in + // reality should have an int appended to it (cni-podman4) + CNIDeviceName = "cni-podman" + // DefaultPodmanDomainName is used for the dnsname plugin to define + // a localized domain name for a created network + DefaultPodmanDomainName = "dns.podman" + // LockFileName is used for obtaining a lock and is appended + // to libpod's tmpdir in practice + LockFileName = "cni.lock" +) + +// CNILock is for preventing name collision and +// unpredictable results when doing some CNI operations. +type CNILock struct { + lockfile.Locker +} + +// GetDefaultPodmanNetwork outputs the default network for podman +func GetDefaultPodmanNetwork() (*net.IPNet, error) { + _, n, err := net.ParseCIDR("10.88.1.0/24") + return n, err +} + +// CNIPlugins is a way of marshalling a CNI network configuration to disk +type CNIPlugins interface { + Bytes() ([]byte, error) +} + +// HostLocalBridge describes a configuration for a bridge plugin +// https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge#network-configuration-reference +type HostLocalBridge struct { + PluginType string `json:"type"` + BrName string `json:"bridge,omitempty"` + IsGW bool `json:"isGateway"` + IsDefaultGW bool `json:"isDefaultGateway,omitempty"` + ForceAddress bool `json:"forceAddress,omitempty"` + IPMasq bool `json:"ipMasq,omitempty"` + MTU int `json:"mtu,omitempty"` + HairpinMode bool `json:"hairpinMode,omitempty"` + PromiscMode bool `json:"promiscMode,omitempty"` + Vlan int `json:"vlan,omitempty"` + IPAM IPAMHostLocalConf `json:"ipam"` +} + +// Bytes outputs []byte +func (h *HostLocalBridge) Bytes() ([]byte, error) { + return json.MarshalIndent(h, "", "\t") +} + +// IPAMHostLocalConf describes an IPAM configuration +// https://github.com/containernetworking/plugins/tree/master/plugins/ipam/host-local#network-configuration-reference +type IPAMHostLocalConf struct { + PluginType string `json:"type"` + Routes []IPAMRoute `json:"routes,omitempty"` + ResolveConf string `json:"resolveConf,omitempty"` + DataDir string `json:"dataDir,omitempty"` + Ranges [][]IPAMLocalHostRangeConf `json:"ranges,omitempty"` +} + +// IPAMLocalHostRangeConf describes the new style IPAM ranges +type IPAMLocalHostRangeConf struct { + Subnet string `json:"subnet"` + RangeStart string `json:"rangeStart,omitempty"` + RangeEnd string `json:"rangeEnd,omitempty"` + Gateway string `json:"gateway,omitempty"` +} + +// Bytes outputs the configuration as []byte +func (i IPAMHostLocalConf) Bytes() ([]byte, error) { + return json.MarshalIndent(i, "", "\t") +} + +// IPAMRoute describes a route in an ipam config +type IPAMRoute struct { + Dest string `json:"dst"` +} + +// PortMapConfig describes the default portmapping config +type PortMapConfig struct { + PluginType string `json:"type"` + Capabilities map[string]bool `json:"capabilities"` +} + +// Bytes outputs the configuration as []byte +func (p PortMapConfig) Bytes() ([]byte, error) { + return json.MarshalIndent(p, "", "\t") +} + +// IPAMDHCP describes the ipamdhcp config +type IPAMDHCP struct { + DHCP string `json:"type"` +} + +// MacVLANConfig describes the macvlan config +type MacVLANConfig struct { + PluginType string `json:"type"` + Master string `json:"master"` + IPAM IPAMDHCP `json:"ipam"` +} + +// Bytes outputs the configuration as []byte +func (p MacVLANConfig) Bytes() ([]byte, error) { + return json.MarshalIndent(p, "", "\t") +} + +// FirewallConfig describes the firewall plugin +type FirewallConfig struct { + PluginType string `json:"type"` + Backend string `json:"backend"` +} + +// Bytes outputs the configuration as []byte +func (f FirewallConfig) Bytes() ([]byte, error) { + return json.MarshalIndent(f, "", "\t") +} + +// DNSNameConfig describes the dns container name resolution plugin config +type DNSNameConfig struct { + PluginType string `json:"type"` + DomainName string `json:"domainName"` +} + +// Bytes outputs the configuration as []byte +func (d DNSNameConfig) Bytes() ([]byte, error) { + return json.MarshalIndent(d, "", "\t") +} diff --git a/libpod/network/create.go b/libpod/network/create.go new file mode 100644 index 000000000..a9ed4c4ef --- /dev/null +++ b/libpod/network/create.go @@ -0,0 +1,195 @@ +package network + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/containernetworking/cni/pkg/version" + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/podman/v2/pkg/util" + "github.com/pkg/errors" +) + +func Create(name string, options entities.NetworkCreateOptions, r *libpod.Runtime) (*entities.NetworkCreateReport, error) { + var fileName string + if err := isSupportedDriver(options.Driver); err != nil { + return nil, err + } + config, err := r.GetConfig() + if err != nil { + return nil, err + } + // Acquire a lock for CNI + l, err := acquireCNILock(filepath.Join(config.Engine.TmpDir, LockFileName)) + if err != nil { + return nil, err + } + defer l.releaseCNILock() + if len(options.MacVLAN) > 0 { + fileName, err = createMacVLAN(r, name, options) + } else { + fileName, err = createBridge(r, name, options) + } + if err != nil { + return nil, err + } + return &entities.NetworkCreateReport{Filename: fileName}, nil +} + +// createBridge creates a CNI network +func createBridge(r *libpod.Runtime, name string, options entities.NetworkCreateOptions) (string, error) { + isGateway := true + ipMasq := true + subnet := &options.Subnet + ipRange := options.Range + runtimeConfig, err := r.GetConfig() + if err != nil { + return "", err + } + // if range is provided, make sure it is "in" network + if subnet.IP != nil { + // if network is provided, does it conflict with existing CNI or live networks + err = ValidateUserNetworkIsAvailable(runtimeConfig, subnet) + } else { + // if no network is provided, figure out network + subnet, err = GetFreeNetwork(runtimeConfig) + } + if err != nil { + return "", err + } + gateway := options.Gateway + if gateway == nil { + // if no gateway is provided, provide it as first ip of network + gateway = CalcGatewayIP(subnet) + } + // if network is provided and if gateway is provided, make sure it is "in" network + if options.Subnet.IP != nil && options.Gateway != nil { + if !subnet.Contains(gateway) { + return "", errors.Errorf("gateway %s is not in valid for subnet %s", gateway.String(), subnet.String()) + } + } + if options.Internal { + isGateway = false + ipMasq = false + } + + // if a range is given, we need to ensure it is "in" the network range. + if options.Range.IP != nil { + if options.Subnet.IP == nil { + return "", errors.New("you must define a subnet range to define an ip-range") + } + firstIP, err := FirstIPInSubnet(&options.Range) + if err != nil { + return "", err + } + lastIP, err := LastIPInSubnet(&options.Range) + if err != nil { + return "", err + } + if !subnet.Contains(firstIP) || !subnet.Contains(lastIP) { + return "", errors.Errorf("the ip range %s does not fall within the subnet range %s", options.Range.String(), subnet.String()) + } + } + bridgeDeviceName, err := GetFreeDeviceName(runtimeConfig) + if err != nil { + return "", err + } + + if len(name) > 0 { + netNames, err := GetNetworkNamesFromFileSystem(runtimeConfig) + if err != nil { + return "", err + } + if util.StringInSlice(name, netNames) { + return "", errors.Errorf("the network name %s is already used", name) + } + } else { + // If no name is given, we give the name of the bridge device + name = bridgeDeviceName + } + + ncList := NewNcList(name, version.Current()) + var plugins []CNIPlugins + var routes []IPAMRoute + + defaultRoute, err := NewIPAMDefaultRoute(IsIPv6(subnet.IP)) + if err != nil { + return "", err + } + routes = append(routes, defaultRoute) + ipamConfig, err := NewIPAMHostLocalConf(subnet, routes, ipRange, gateway) + if err != nil { + return "", err + } + + // TODO need to iron out the role of isDefaultGW and IPMasq + bridge := NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, ipamConfig) + plugins = append(plugins, bridge) + plugins = append(plugins, NewPortMapPlugin()) + plugins = append(plugins, NewFirewallPlugin()) + // if we find the dnsname plugin, we add configuration for it + if HasDNSNamePlugin(runtimeConfig.Network.CNIPluginDirs) && !options.DisableDNS { + // Note: in the future we might like to allow for dynamic domain names + plugins = append(plugins, NewDNSNamePlugin(DefaultPodmanDomainName)) + } + ncList["plugins"] = plugins + b, err := json.MarshalIndent(ncList, "", " ") + if err != nil { + return "", err + } + if err := os.MkdirAll(GetCNIConfDir(runtimeConfig), 0755); err != nil { + return "", err + } + cniPathName := filepath.Join(GetCNIConfDir(runtimeConfig), fmt.Sprintf("%s.conflist", name)) + err = ioutil.WriteFile(cniPathName, b, 0644) + return cniPathName, err +} + +func createMacVLAN(r *libpod.Runtime, name string, options entities.NetworkCreateOptions) (string, error) { + var ( + plugins []CNIPlugins + ) + liveNetNames, err := GetLiveNetworkNames() + if err != nil { + return "", err + } + + config, err := r.GetConfig() + if err != nil { + return "", err + } + + // Make sure the host-device exists + if !util.StringInSlice(options.MacVLAN, liveNetNames) { + return "", errors.Errorf("failed to find network interface %q", options.MacVLAN) + } + if len(name) > 0 { + netNames, err := GetNetworkNamesFromFileSystem(config) + if err != nil { + return "", err + } + if util.StringInSlice(name, netNames) { + return "", errors.Errorf("the network name %s is already used", name) + } + } else { + name, err = GetFreeDeviceName(config) + if err != nil { + return "", err + } + } + ncList := NewNcList(name, version.Current()) + macvlan := NewMacVLANPlugin(options.MacVLAN) + plugins = append(plugins, macvlan) + ncList["plugins"] = plugins + b, err := json.MarshalIndent(ncList, "", " ") + if err != nil { + return "", err + } + cniPathName := filepath.Join(GetCNIConfDir(config), fmt.Sprintf("%s.conflist", name)) + err = ioutil.WriteFile(cniPathName, b, 0644) + return cniPathName, err +} diff --git a/libpod/network/devices.go b/libpod/network/devices.go new file mode 100644 index 000000000..a5d23fae4 --- /dev/null +++ b/libpod/network/devices.go @@ -0,0 +1,63 @@ +package network + +import ( + "fmt" + "os/exec" + + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v2/pkg/util" + "github.com/containers/podman/v2/utils" + "github.com/sirupsen/logrus" +) + +// GetFreeDeviceName returns a device name that is unused; used when no network +// name is provided by user +func GetFreeDeviceName(config *config.Config) (string, error) { + var ( + deviceNum uint + deviceName string + ) + networkNames, err := GetNetworkNamesFromFileSystem(config) + if err != nil { + return "", err + } + liveNetworksNames, err := GetLiveNetworkNames() + if err != nil { + return "", err + } + bridgeNames, err := GetBridgeNamesFromFileSystem(config) + if err != nil { + return "", err + } + for { + deviceName = fmt.Sprintf("%s%d", CNIDeviceName, deviceNum) + logrus.Debugf("checking if device name %q exists in other cni networks", deviceName) + if util.StringInSlice(deviceName, networkNames) { + deviceNum++ + continue + } + logrus.Debugf("checking if device name %q exists in live networks", deviceName) + if util.StringInSlice(deviceName, liveNetworksNames) { + deviceNum++ + continue + } + logrus.Debugf("checking if device name %q already exists as a bridge name ", deviceName) + if !util.StringInSlice(deviceName, bridgeNames) { + break + } + deviceNum++ + } + return deviceName, nil +} + +// RemoveInterface removes an interface by the given name +func RemoveInterface(interfaceName string) error { + // Make sure we have the ip command on the system + ipPath, err := exec.LookPath("ip") + if err != nil { + return err + } + // Delete the network interface + _, err = utils.ExecCmd(ipPath, []string{"link", "del", interfaceName}...) + return err +} diff --git a/libpod/network/files.go b/libpod/network/files.go new file mode 100644 index 000000000..a2090491f --- /dev/null +++ b/libpod/network/files.go @@ -0,0 +1,174 @@ +package network + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "sort" + "strings" + + "github.com/containernetworking/cni/libcni" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v2/libpod/define" + "github.com/pkg/errors" +) + +func GetCNIConfDir(configArg *config.Config) string { + if len(configArg.Network.NetworkConfigDir) < 1 { + dc, err := config.DefaultConfig() + if err != nil { + // Fallback to hard-coded dir + return CNIConfigDir + } + return dc.Network.NetworkConfigDir + } + return configArg.Network.NetworkConfigDir +} + +// LoadCNIConfsFromDir loads all the CNI configurations from a dir +func LoadCNIConfsFromDir(dir string) ([]*libcni.NetworkConfigList, error) { + files, err := libcni.ConfFiles(dir, []string{".conflist"}) + if err != nil { + return nil, err + } + sort.Strings(files) + + configs := make([]*libcni.NetworkConfigList, 0, len(files)) + for _, confFile := range files { + conf, err := libcni.ConfListFromFile(confFile) + if err != nil { + return nil, errors.Wrapf(err, "in %s", confFile) + } + configs = append(configs, conf) + } + return configs, nil +} + +// GetCNIConfigPathByName finds a CNI network by name and +// returns its configuration file path +func GetCNIConfigPathByName(config *config.Config, name string) (string, error) { + files, err := libcni.ConfFiles(GetCNIConfDir(config), []string{".conflist"}) + if err != nil { + return "", err + } + for _, confFile := range files { + conf, err := libcni.ConfListFromFile(confFile) + if err != nil { + return "", errors.Wrapf(err, "in %s", confFile) + } + if conf.Name == name { + return confFile, nil + } + } + return "", errors.Wrap(define.ErrNoSuchNetwork, fmt.Sprintf("unable to find network configuration for %s", name)) +} + +// ReadRawCNIConfByName reads the raw CNI configuration for a CNI +// network by name +func ReadRawCNIConfByName(config *config.Config, name string) ([]byte, error) { + confFile, err := GetCNIConfigPathByName(config, name) + if err != nil { + return nil, err + } + b, err := ioutil.ReadFile(confFile) + return b, err +} + +// GetCNIPlugins returns a list of plugins that a given network +// has in the form of a string +func GetCNIPlugins(list *libcni.NetworkConfigList) string { + plugins := make([]string, 0, len(list.Plugins)) + for _, plug := range list.Plugins { + plugins = append(plugins, plug.Network.Type) + } + return strings.Join(plugins, ",") +} + +// GetNetworksFromFilesystem gets all the networks from the cni configuration +// files +func GetNetworksFromFilesystem(config *config.Config) ([]*allocator.Net, error) { + var cniNetworks []*allocator.Net + + networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config)) + if err != nil { + return nil, err + } + for _, n := range networks { + for _, cniplugin := range n.Plugins { + if cniplugin.Network.Type == "bridge" { + ipamConf := allocator.Net{} + if err := json.Unmarshal(cniplugin.Bytes, &ipamConf); err != nil { + return nil, err + } + cniNetworks = append(cniNetworks, &ipamConf) + break + } + } + } + return cniNetworks, nil +} + +// GetNetworkNamesFromFileSystem gets all the names from the cni network +// configuration files +func GetNetworkNamesFromFileSystem(config *config.Config) ([]string, error) { + networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config)) + if err != nil { + return nil, err + } + networkNames := []string{} + for _, n := range networks { + networkNames = append(networkNames, n.Name) + } + return networkNames, nil +} + +// GetInterfaceNameFromConfig returns the interface name for the bridge plugin +func GetInterfaceNameFromConfig(path string) (string, error) { + var name string + conf, err := libcni.ConfListFromFile(path) + if err != nil { + return "", err + } + for _, cniplugin := range conf.Plugins { + if cniplugin.Network.Type == "bridge" { + plugin := make(map[string]interface{}) + if err := json.Unmarshal(cniplugin.Bytes, &plugin); err != nil { + return "", err + } + name = plugin["bridge"].(string) + break + } + } + if len(name) == 0 { + return "", errors.New("unable to find interface name for network") + } + return name, nil +} + +// GetBridgeNamesFromFileSystem is a convenience function to get all the bridge +// names from the configured networks +func GetBridgeNamesFromFileSystem(config *config.Config) ([]string, error) { + networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config)) + if err != nil { + return nil, err + } + + bridgeNames := []string{} + for _, n := range networks { + var name string + // iterate network conflists + for _, cniplugin := range n.Plugins { + // iterate plugins + if cniplugin.Network.Type == "bridge" { + plugin := make(map[string]interface{}) + if err := json.Unmarshal(cniplugin.Bytes, &plugin); err != nil { + continue + } + name = plugin["bridge"].(string) + } + } + bridgeNames = append(bridgeNames, name) + } + return bridgeNames, nil +} diff --git a/libpod/network/ip.go b/libpod/network/ip.go new file mode 100644 index 000000000..ba93a0d05 --- /dev/null +++ b/libpod/network/ip.go @@ -0,0 +1,19 @@ +package network + +import ( + "net" + + "github.com/containernetworking/plugins/pkg/ip" +) + +// CalcGatewayIP takes a network and returns the first IP in it. +func CalcGatewayIP(ipn *net.IPNet) net.IP { + // taken from cni bridge plugin as it is not exported + nid := ipn.IP.Mask(ipn.Mask) + return ip.NextIP(nid) +} + +// IsIPv6 returns if netIP is IPv6. +func IsIPv6(netIP net.IP) bool { + return netIP != nil && netIP.To4() == nil +} diff --git a/libpod/network/lock.go b/libpod/network/lock.go new file mode 100644 index 000000000..0395359eb --- /dev/null +++ b/libpod/network/lock.go @@ -0,0 +1,26 @@ +package network + +import ( + "github.com/containers/storage" +) + +// acquireCNILock gets a lock that should be used in create and +// delete cases to avoid unwanted collisions in network names. +// TODO this uses a file lock and should be converted to shared memory +// when we have a more general shared memory lock in libpod +func acquireCNILock(lockPath string) (*CNILock, error) { + l, err := storage.GetLockfile(lockPath) + if err != nil { + return nil, err + } + l.Lock() + cnilock := CNILock{ + Locker: l, + } + return &cnilock, nil +} + +// ReleaseCNILock unlocks the previously held lock +func (l *CNILock) releaseCNILock() { + l.Unlock() +} diff --git a/libpod/network/netconflist.go b/libpod/network/netconflist.go new file mode 100644 index 000000000..8187fdb39 --- /dev/null +++ b/libpod/network/netconflist.go @@ -0,0 +1,155 @@ +package network + +import ( + "net" + "os" + "path/filepath" +) + +const ( + defaultIPv4Route = "0.0.0.0/0" + defaultIPv6Route = "::/0" +) + +// 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) NcList { + n := NcList{} + n["cniVersion"] = version + n["name"] = name + return n +} + +// NewHostLocalBridge creates a new LocalBridge for host-local +func NewHostLocalBridge(name string, isGateWay, isDefaultGW, ipMasq bool, ipamConf IPAMHostLocalConf) *HostLocalBridge { + hostLocalBridge := HostLocalBridge{ + PluginType: "bridge", + BrName: name, + IPMasq: ipMasq, + HairpinMode: true, + IPAM: ipamConf, + } + if isGateWay { + hostLocalBridge.IsGW = true + } + if isDefaultGW { + hostLocalBridge.IsDefaultGW = true + } + return &hostLocalBridge +} + +// NewIPAMHostLocalConf creates a new IPAMHostLocal configfuration +func NewIPAMHostLocalConf(subnet *net.IPNet, routes []IPAMRoute, ipRange net.IPNet, gw net.IP) (IPAMHostLocalConf, error) { + var ipamRanges [][]IPAMLocalHostRangeConf + ipamConf := IPAMHostLocalConf{ + PluginType: "host-local", + Routes: routes, + // Possible future support ? Leaving for clues + //ResolveConf: "", + //DataDir: "" + } + IPAMRange, err := newIPAMLocalHostRange(subnet, &ipRange, &gw) + if err != nil { + return ipamConf, err + } + ipamRanges = append(ipamRanges, IPAMRange) + ipamConf.Ranges = ipamRanges + return ipamConf, nil +} + +func newIPAMLocalHostRange(subnet *net.IPNet, ipRange *net.IPNet, gw *net.IP) ([]IPAMLocalHostRangeConf, error) { //nolint:interfacer + var ranges []IPAMLocalHostRangeConf + hostRange := IPAMLocalHostRangeConf{ + Subnet: subnet.String(), + } + // an user provided a range, we add it here + if ipRange.IP != nil { + first, err := FirstIPInSubnet(ipRange) + if err != nil { + return nil, err + } + last, err := LastIPInSubnet(ipRange) + if err != nil { + return nil, err + } + hostRange.RangeStart = first.String() + hostRange.RangeEnd = last.String() + } + if gw != nil { + hostRange.Gateway = gw.String() + } + ranges = append(ranges, hostRange) + return ranges, nil +} + +// NewIPAMRoute creates a new IPAM route configuration +func NewIPAMRoute(r *net.IPNet) IPAMRoute { //nolint:interfacer + return IPAMRoute{Dest: r.String()} +} + +// NewIPAMDefaultRoute creates a new IPAMDefault route of +// 0.0.0.0/0 for IPv4 or ::/0 for IPv6 +func NewIPAMDefaultRoute(isIPv6 bool) (IPAMRoute, error) { + route := defaultIPv4Route + if isIPv6 { + route = defaultIPv6Route + } + _, n, err := net.ParseCIDR(route) + if err != nil { + return IPAMRoute{}, err + } + return NewIPAMRoute(n), nil +} + +// NewPortMapPlugin creates a predefined, default portmapping +// configuration +func NewPortMapPlugin() PortMapConfig { + caps := make(map[string]bool) + caps["portMappings"] = true + p := PortMapConfig{ + PluginType: "portmap", + Capabilities: caps, + } + return p +} + +// NewFirewallPlugin creates a generic firewall plugin +func NewFirewallPlugin() FirewallConfig { + return FirewallConfig{ + PluginType: "firewall", + } +} + +// NewDNSNamePlugin creates the dnsname config with a given +// domainname +func NewDNSNamePlugin(domainName string) DNSNameConfig { + return DNSNameConfig{ + PluginType: "dnsname", + DomainName: domainName, + } +} + +// 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) MacVLANConfig { + i := IPAMDHCP{DHCP: "dhcp"} + + m := MacVLANConfig{ + PluginType: "macvlan", + Master: device, + IPAM: i, + } + return m +} diff --git a/libpod/network/netconflist_test.go b/libpod/network/netconflist_test.go new file mode 100644 index 000000000..5893bf985 --- /dev/null +++ b/libpod/network/netconflist_test.go @@ -0,0 +1,38 @@ +package network + +import ( + "reflect" + "testing" +) + +func TestNewIPAMDefaultRoute(t *testing.T) { + + tests := []struct { + name string + isIPv6 bool + want IPAMRoute + }{ + { + name: "IPv4 default route", + isIPv6: false, + want: IPAMRoute{defaultIPv4Route}, + }, + { + name: "IPv6 default route", + isIPv6: true, + want: IPAMRoute{defaultIPv6Route}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := NewIPAMDefaultRoute(tt.isIPv6) + if err != nil { + t.Errorf("no error expected: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewIPAMDefaultRoute() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/libpod/network/network.go b/libpod/network/network.go new file mode 100644 index 000000000..7327a1a7d --- /dev/null +++ b/libpod/network/network.go @@ -0,0 +1,225 @@ +package network + +import ( + "encoding/json" + "net" + "os" + "path/filepath" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v2/libpod/define" + "github.com/containers/podman/v2/pkg/util" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// DefaultNetworkDriver is the default network type used +var DefaultNetworkDriver = "bridge" + +// SupportedNetworkDrivers describes the list of supported drivers +var SupportedNetworkDrivers = []string{DefaultNetworkDriver} + +// isSupportedDriver checks if the user provided driver is supported +func isSupportedDriver(driver string) error { + if util.StringInSlice(driver, SupportedNetworkDrivers) { + return nil + } + return errors.Errorf("driver '%s' is not supported", driver) +} + +// GetLiveNetworks returns a slice of networks representing what the system +// has defined as network interfaces +func GetLiveNetworks() ([]*net.IPNet, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return nil, err + } + nets := make([]*net.IPNet, 0, len(addrs)) + for _, address := range addrs { + _, n, err := net.ParseCIDR(address.String()) + if err != nil { + return nil, err + } + nets = append(nets, n) + } + return nets, nil +} + +// GetLiveNetworkNames returns a list of network interfaces on the system +func GetLiveNetworkNames() ([]string, error) { + liveInterfaces, err := net.Interfaces() + if err != nil { + return nil, err + } + interfaceNames := make([]string, 0, len(liveInterfaces)) + for _, i := range liveInterfaces { + interfaceNames = append(interfaceNames, i.Name) + } + return interfaceNames, nil +} + +// GetFreeNetwork looks for a free network according to existing cni configuration +// files and network interfaces. +func GetFreeNetwork(config *config.Config) (*net.IPNet, error) { + networks, err := GetNetworksFromFilesystem(config) + if err != nil { + return nil, err + } + liveNetworks, err := GetLiveNetworks() + if err != nil { + return nil, err + } + nextNetwork, err := GetDefaultPodmanNetwork() + if err != nil { + return nil, err + } + logrus.Debugf("default network is %s", nextNetwork.String()) + for { + newNetwork, err := NextSubnet(nextNetwork) + if err != nil { + return nil, err + } + logrus.Debugf("checking if network %s intersects with other cni networks", nextNetwork.String()) + if intersectsConfig, _ := networkIntersectsWithNetworks(newNetwork, allocatorToIPNets(networks)); intersectsConfig { + logrus.Debugf("network %s is already being used by a cni configuration", nextNetwork.String()) + nextNetwork = newNetwork + continue + } + logrus.Debugf("checking if network %s intersects with any network interfaces", nextNetwork.String()) + if intersectsLive, _ := networkIntersectsWithNetworks(newNetwork, liveNetworks); !intersectsLive { + break + } + logrus.Debugf("network %s is being used by a network interface", nextNetwork.String()) + nextNetwork = newNetwork + } + return nextNetwork, nil +} + +func allocatorToIPNets(networks []*allocator.Net) []*net.IPNet { + var nets []*net.IPNet + for _, network := range networks { + if len(network.IPAM.Ranges) > 0 { + // this is the new IPAM range style + // append each subnet from ipam the rangeset + for _, r := range network.IPAM.Ranges[0] { + nets = append(nets, newIPNetFromSubnet(r.Subnet)) + } + } else { + // looks like the old, deprecated style + nets = append(nets, newIPNetFromSubnet(network.IPAM.Subnet)) + } + } + return nets +} + +func newIPNetFromSubnet(subnet types.IPNet) *net.IPNet { + n := net.IPNet{ + IP: subnet.IP, + Mask: subnet.Mask, + } + return &n +} + +func networkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) (bool, *net.IPNet) { + for _, nw := range networklist { + if networkIntersect(n, nw) { + return true, nw + } + } + return false, nil +} + +func networkIntersect(n1, n2 *net.IPNet) bool { + return n2.Contains(n1.IP) || n1.Contains(n2.IP) +} + +// ValidateUserNetworkIsAvailable returns via an error if a network is available +// to be used +func ValidateUserNetworkIsAvailable(config *config.Config, userNet *net.IPNet) error { + if len(userNet.IP) == 0 || len(userNet.Mask) == 0 { + return errors.Errorf("network %s's ip or mask cannot be empty", userNet.String()) + } + + ones, bit := userNet.Mask.Size() + if ones == 0 || bit == 0 { + return errors.Errorf("network %s's mask is invalid", userNet.String()) + } + + networks, err := GetNetworksFromFilesystem(config) + if err != nil { + return err + } + liveNetworks, err := GetLiveNetworks() + if err != nil { + return err + } + logrus.Debugf("checking if network %s exists in cni networks", userNet.String()) + if intersectsConfig, _ := networkIntersectsWithNetworks(userNet, allocatorToIPNets(networks)); intersectsConfig { + return errors.Errorf("network %s is already being used by a cni configuration", userNet.String()) + } + logrus.Debugf("checking if network %s exists in any network interfaces", userNet.String()) + if intersectsLive, _ := networkIntersectsWithNetworks(userNet, liveNetworks); intersectsLive { + return errors.Errorf("network %s is being used by a network interface", userNet.String()) + } + return nil +} + +// RemoveNetwork removes a given network by name. If the network has container associated with it, that +// must be handled outside the context of this. +func RemoveNetwork(config *config.Config, name string) error { + l, err := acquireCNILock(filepath.Join(config.Engine.TmpDir, LockFileName)) + if err != nil { + return err + } + defer l.releaseCNILock() + cniPath, err := GetCNIConfigPathByName(config, name) + if err != nil { + return err + } + // Before we delete the configuration file, we need to make sure we can read and parse + // it to get the network interface name so we can remove that too + interfaceName, err := GetInterfaceNameFromConfig(cniPath) + if err != nil { + return errors.Wrapf(err, "failed to find network interface name in %q", cniPath) + } + liveNetworkNames, err := GetLiveNetworkNames() + if err != nil { + return errors.Wrapf(err, "failed to get live network names") + } + if util.StringInSlice(interfaceName, liveNetworkNames) { + if err := RemoveInterface(interfaceName); err != nil { + return errors.Wrapf(err, "failed to delete the network interface %q", interfaceName) + } + } + // Remove the configuration file + if err := os.Remove(cniPath); err != nil { + return errors.Wrapf(err, "failed to remove network configuration file %q", cniPath) + } + return nil +} + +// InspectNetwork reads a CNI config and returns its configuration +func InspectNetwork(config *config.Config, name string) (map[string]interface{}, error) { + b, err := ReadRawCNIConfByName(config, name) + if err != nil { + return nil, err + } + rawList := make(map[string]interface{}) + err = json.Unmarshal(b, &rawList) + return rawList, err +} + +// Exists says whether a given network exists or not; it meant +// specifically for restful responses so 404s can be used +func Exists(config *config.Config, name string) (bool, error) { + _, err := ReadRawCNIConfByName(config, name) + if err != nil { + if errors.Cause(err) == define.ErrNoSuchNetwork { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/libpod/network/network_test.go b/libpod/network/network_test.go new file mode 100644 index 000000000..1969e792c --- /dev/null +++ b/libpod/network/network_test.go @@ -0,0 +1,35 @@ +package network + +import ( + "net" + "testing" +) + +func parseCIDR(n string) *net.IPNet { + _, parsedNet, _ := net.ParseCIDR(n) + return parsedNet +} + +func Test_networkIntersect(t *testing.T) { + type args struct { + n1 *net.IPNet + n2 *net.IPNet + } + tests := []struct { + name string + args args + want bool + }{ + {"16 and 24 intersects", args{n1: parseCIDR("192.168.0.0/16"), n2: parseCIDR("192.168.1.0/24")}, true}, + {"24 and 25 intersects", args{n1: parseCIDR("192.168.1.0/24"), n2: parseCIDR("192.168.1.0/25")}, true}, + {"Two 24s", args{n1: parseCIDR("192.168.1.0/24"), n2: parseCIDR("192.168.2.0/24")}, false}, + } + for _, tt := range tests { + test := tt + t.Run(tt.name, func(t *testing.T) { + if got := networkIntersect(test.args.n1, test.args.n2); got != test.want { + t.Errorf("networkIntersect() = %v, want %v", got, test.want) + } + }) + } +} diff --git a/libpod/network/subnet.go b/libpod/network/subnet.go new file mode 100644 index 000000000..90f0cdfce --- /dev/null +++ b/libpod/network/subnet.go @@ -0,0 +1,78 @@ +package network + +/* + The code in this was kindly contributed by Dan Williams(dcbw@redhat.com). Many thanks + for his contributions. +*/ + +import ( + "fmt" + "net" +) + +func incByte(subnet *net.IPNet, idx int, shift uint) error { + if idx < 0 { + return fmt.Errorf("no more subnets left") + } + if subnet.IP[idx] == 255 { + subnet.IP[idx] = 0 + return incByte(subnet, idx-1, 0) + } + subnet.IP[idx] += 1 << shift + return nil +} + +// NextSubnet returns subnet incremented by 1 +func NextSubnet(subnet *net.IPNet) (*net.IPNet, error) { + newSubnet := &net.IPNet{ + IP: subnet.IP, + Mask: subnet.Mask, + } + ones, bits := newSubnet.Mask.Size() + if ones == 0 { + return nil, fmt.Errorf("%s has only one subnet", subnet.String()) + } + zeroes := uint(bits - ones) + shift := zeroes % 8 + idx := ones/8 - 1 + if idx < 0 { + idx = 0 + } + if err := incByte(newSubnet, idx, shift); err != nil { + return nil, err + } + return newSubnet, nil +} + +// LastIPInSubnet gets the last IP in a subnet +func LastIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer + // re-parse to ensure clean network address + _, cidr, err := net.ParseCIDR(addr.String()) + if err != nil { + return nil, err + } + + ones, bits := cidr.Mask.Size() + if ones == bits { + return FirstIPInSubnet(cidr) + } + hostStart := ones / 8 + // Handle the first host byte + cidr.IP[hostStart] |= 0xff & cidr.Mask[hostStart] + // Fill the rest with ones + for i := hostStart; i < len(cidr.IP); i++ { + cidr.IP[i] = 0xff + } + return cidr.IP, nil +} + +// FirstIPInSubnet gets the first IP in a subnet +func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer + // re-parse to ensure clean network address + _, cidr, err := net.ParseCIDR(addr.String()) + if err != nil { + return nil, err + } + cidr.IP[len(cidr.IP)-1]++ + return cidr.IP, nil +} diff --git a/libpod/network/subnet_test.go b/libpod/network/subnet_test.go new file mode 100644 index 000000000..917c3be88 --- /dev/null +++ b/libpod/network/subnet_test.go @@ -0,0 +1,35 @@ +package network + +import ( + "net" + "reflect" + "testing" +) + +func TestNextSubnet(t *testing.T) { + type args struct { + subnet *net.IPNet + } + tests := []struct { + name string + args args + want *net.IPNet + wantErr bool + }{ + {"class b", args{subnet: parseCIDR("192.168.0.0/16")}, parseCIDR("192.169.0.0/16"), false}, + {"class c", args{subnet: parseCIDR("192.168.1.0/24")}, parseCIDR("192.168.2.0/24"), false}, + } + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + got, err := NextSubnet(test.args.subnet) + if (err != nil) != test.wantErr { + t.Errorf("NextSubnet() error = %v, wantErr %v", err, test.wantErr) + return + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("NextSubnet() got = %v, want %v", got, test.want) + } + }) + } +} |