summaryrefslogtreecommitdiff
path: root/pkg/network
diff options
context:
space:
mode:
authorbaude <bbaude@redhat.com>2019-08-08 06:01:00 -0500
committerbaude <bbaude@redhat.com>2019-09-09 09:32:43 -0500
commitee432cf2792c5dbe81953007f1fd5c87beb3ebd5 (patch)
treedc0646e4b2faeadf9cca58bf80f1e90d98c50165 /pkg/network
parent30cbb0091515a7f802f0f3f3ee486be6ff98f645 (diff)
downloadpodman-ee432cf2792c5dbe81953007f1fd5c87beb3ebd5.tar.gz
podman-ee432cf2792c5dbe81953007f1fd5c87beb3ebd5.tar.bz2
podman-ee432cf2792c5dbe81953007f1fd5c87beb3ebd5.zip
podman network create
initial implementation of network create. we only support bridging networks with this first pass. Signed-off-by: baude <bbaude@redhat.com>
Diffstat (limited to 'pkg/network')
-rw-r--r--pkg/network/config.go99
-rw-r--r--pkg/network/devices.go41
-rw-r--r--pkg/network/files.go107
-rw-r--r--pkg/network/ip.go14
-rw-r--r--pkg/network/netconflist.go113
-rw-r--r--pkg/network/network.go146
-rw-r--r--pkg/network/network_test.go34
-rw-r--r--pkg/network/subnet.go78
-rw-r--r--pkg/network/subnet_test.go34
9 files changed, 653 insertions, 13 deletions
diff --git a/pkg/network/config.go b/pkg/network/config.go
index d282f66b6..7eaa83833 100644
--- a/pkg/network/config.go
+++ b/pkg/network/config.go
@@ -1,4 +1,99 @@
package network
-// CNIConfigDir is the path where CNI config files exist
-const CNIConfigDir = "/etc/cni/net.d"
+import (
+ "encoding/json"
+ "net"
+)
+
+// TODO once the libpod.conf file stuff is worked out, this should be modified
+// to honor defines in the libpod.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"
+)
+
+// 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")
+}
+
+// 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")
+}
diff --git a/pkg/network/devices.go b/pkg/network/devices.go
new file mode 100644
index 000000000..26101b6f7
--- /dev/null
+++ b/pkg/network/devices.go
@@ -0,0 +1,41 @@
+package network
+
+import (
+ "fmt"
+ "github.com/containers/libpod/pkg/util"
+
+ "github.com/sirupsen/logrus"
+)
+
+// GetFreeDeviceName returns a device name that is unused; used when no network
+// name is provided by user
+func GetFreeDeviceName() (string, error) {
+ var (
+ deviceNum uint
+ deviceName string
+ )
+ networkNames, err := GetNetworkNamesFromFileSystem()
+ if err != nil {
+ return "", err
+ }
+ liveNetworksNames, err := GetLiveNetworkNames()
+ if err != nil {
+ return "", err
+ }
+ for {
+ deviceName = fmt.Sprintf("%s%d", CNIDeviceName, deviceNum)
+ logrus.Debugf("checking if device name %s exists in other cni networks", deviceName)
+ if util.StringInSlice(deviceName, networkNames) {
+ deviceNum++
+ continue
+ }
+ logrus.Debugf("checking if device name %s exists in live networks", deviceName)
+ if !util.StringInSlice(deviceName, liveNetworksNames) {
+ break
+ }
+ // TODO Still need to check the bridge names for a conflict but I dont know
+ // how to get them yet!
+ deviceNum++
+ }
+ return deviceName, nil
+}
diff --git a/pkg/network/files.go b/pkg/network/files.go
new file mode 100644
index 000000000..80fde5e17
--- /dev/null
+++ b/pkg/network/files.go
@@ -0,0 +1,107 @@
+package network
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "sort"
+ "strings"
+
+ "github.com/containernetworking/cni/libcni"
+ "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
+ "github.com/pkg/errors"
+)
+
+// LoadCNIConfsFromDir loads all the CNI configurations from a dir
+func LoadCNIConfsFromDir(dir string) ([]*libcni.NetworkConfigList, error) {
+ var configs []*libcni.NetworkConfigList
+ files, err := libcni.ConfFiles(dir, []string{".conflist"})
+ if err != nil {
+ return nil, err
+ }
+ sort.Strings(files)
+
+ for _, confFile := range files {
+ conf, err := libcni.ConfListFromFile(confFile)
+ if err != nil {
+ return nil, err
+ }
+ configs = append(configs, conf)
+ }
+ return configs, nil
+}
+
+// GetCNIConfigPathByName finds a CNI network by name and
+// returns its configuration file path
+func GetCNIConfigPathByName(name string) (string, error) {
+ files, err := libcni.ConfFiles(CNIConfigDir, []string{".conflist"})
+ if err != nil {
+ return "", err
+ }
+ for _, confFile := range files {
+ conf, err := libcni.ConfListFromFile(confFile)
+ if err != nil {
+ return "", err
+ }
+ if conf.Name == name {
+ return confFile, nil
+ }
+ }
+ return "", errors.Errorf("unable to find network configuration for %s", name)
+}
+
+// ReadRawCNIConfByName reads the raw CNI configuration for a CNI
+// network by name
+func ReadRawCNIConfByName(name string) ([]byte, error) {
+ confFile, err := GetCNIConfigPathByName(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 {
+ var plugins []string
+ 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() ([]*allocator.Net, error) {
+ var cniNetworks []*allocator.Net
+ networks, err := LoadCNIConfsFromDir(CNIConfigDir)
+ 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)
+ }
+ }
+ }
+ return cniNetworks, nil
+}
+
+// GetNetworkNamesFromFileSystem gets all the names from the cni network
+// configuration files
+func GetNetworkNamesFromFileSystem() ([]string, error) {
+ var networkNames []string
+ networks, err := LoadCNIConfsFromDir(CNIConfigDir)
+ if err != nil {
+ return nil, err
+ }
+ for _, n := range networks {
+ networkNames = append(networkNames, n.Name)
+ }
+ return networkNames, nil
+}
diff --git a/pkg/network/ip.go b/pkg/network/ip.go
new file mode 100644
index 000000000..1798cd939
--- /dev/null
+++ b/pkg/network/ip.go
@@ -0,0 +1,14 @@
+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)
+}
diff --git a/pkg/network/netconflist.go b/pkg/network/netconflist.go
new file mode 100644
index 000000000..c3b11b409
--- /dev/null
+++ b/pkg/network/netconflist.go
@@ -0,0 +1,113 @@
+package network
+
+import (
+ "net"
+)
+
+// 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,
+ 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
+func NewIPAMDefaultRoute() (IPAMRoute, error) {
+ _, n, err := net.ParseCIDR("0.0.0.0/0")
+ 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",
+ Backend: "iptables",
+ }
+}
diff --git a/pkg/network/network.go b/pkg/network/network.go
index 9d04340a3..b241a66c0 100644
--- a/pkg/network/network.go
+++ b/pkg/network/network.go
@@ -1,26 +1,150 @@
package network
import (
- "sort"
+ "github.com/containers/libpod/pkg/util"
+ "net"
- "github.com/containernetworking/cni/libcni"
+ "github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
)
-// LoadCNIConfsFromDir loads all the CNI configurations from a dir
-func LoadCNIConfsFromDir(dir string) ([]*libcni.NetworkConfigList, error) {
- var configs []*libcni.NetworkConfigList
- files, err := libcni.ConfFiles(dir, []string{".conflist"})
+// SupportedNetworkDrivers describes the list of supported drivers
+var SupportedNetworkDrivers = []string{"bridge"}
+
+// 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) {
+ var nets []*net.IPNet
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ return nil, err
+ }
+ 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) {
+ var interfaceNames []string
+ liveInterfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
- sort.Strings(files)
+ for _, i := range liveInterfaces {
+ interfaceNames = append(interfaceNames, i.Name)
+ }
+ return interfaceNames, nil
+}
- for _, confFile := range files {
- conf, err := libcni.ConfListFromFile(confFile)
+// GetFreeNetwork looks for a free network according to existing cni configuration
+// files and network interfaces.
+func GetFreeNetwork() (*net.IPNet, error) {
+ networks, err := GetNetworksFromFilesystem()
+ 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
}
- configs = append(configs, conf)
+ 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(userNet *net.IPNet) error {
+ networks, err := GetNetworksFromFilesystem()
+ 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 configs, nil
+ return nil
}
diff --git a/pkg/network/network_test.go b/pkg/network/network_test.go
new file mode 100644
index 000000000..dbffc33ad
--- /dev/null
+++ b/pkg/network/network_test.go
@@ -0,0 +1,34 @@
+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 {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := networkIntersect(tt.args.n1, tt.args.n2); got != tt.want {
+ t.Errorf("networkIntersect() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/pkg/network/subnet.go b/pkg/network/subnet.go
new file mode 100644
index 000000000..82ab9a8c8
--- /dev/null
+++ b/pkg/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/pkg/network/subnet_test.go b/pkg/network/subnet_test.go
new file mode 100644
index 000000000..6ecfd2d17
--- /dev/null
+++ b/pkg/network/subnet_test.go
@@ -0,0 +1,34 @@
+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 {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := NextSubnet(tt.args.subnet)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("NextSubnet() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("NextSubnet() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}