summaryrefslogtreecommitdiff
path: root/libpod
diff options
context:
space:
mode:
Diffstat (limited to 'libpod')
-rw-r--r--libpod/network/cni/cni_conversion.go2
-rw-r--r--libpod/network/internal/util/bridge.go56
-rw-r--r--libpod/network/internal/util/ip.go8
-rw-r--r--libpod/network/internal/util/validate.go6
-rw-r--r--libpod/network/netavark/ipam.go368
-rw-r--r--libpod/network/netavark/ipam_test.go433
-rw-r--r--libpod/network/netavark/netavark_suite_test.go1
-rw-r--r--libpod/network/netavark/network.go26
-rw-r--r--libpod/network/netavark/run.go43
-rw-r--r--libpod/network/netavark/run_test.go10
-rw-r--r--libpod/network/util/ip.go12
-rw-r--r--libpod/network/util/ip_calc.go53
12 files changed, 964 insertions, 54 deletions
diff --git a/libpod/network/cni/cni_conversion.go b/libpod/network/cni/cni_conversion.go
index 14cab2573..70d259b60 100644
--- a/libpod/network/cni/cni_conversion.go
+++ b/libpod/network/cni/cni_conversion.go
@@ -157,7 +157,7 @@ func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath
return errors.Errorf("failed to parse gateway ip %s", ipam.Gateway)
}
// convert to 4 byte if ipv4
- internalutil.NormalizeIP(&gateway)
+ util.NormalizeIP(&gateway)
} else if !network.Internal {
// only add a gateway address if the network is not internal
gateway, err = util.FirstIPInSubnet(sub)
diff --git a/libpod/network/internal/util/bridge.go b/libpod/network/internal/util/bridge.go
index c054c7d4e..476557050 100644
--- a/libpod/network/internal/util/bridge.go
+++ b/libpod/network/internal/util/bridge.go
@@ -27,41 +27,43 @@ func CreateBridge(n NetUtil, network *types.Network, usedNetworks []*net.IPNet)
}
}
- if len(network.Subnets) == 0 {
- freeSubnet, err := GetFreeIPv4NetworkSubnet(usedNetworks)
- 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 {
+ if network.IPAMOptions["driver"] != types.DHCPIPAMDriver {
+ if len(network.Subnets) == 0 {
freeSubnet, err := GetFreeIPv4NetworkSubnet(usedNetworks)
if err != nil {
return err
}
network.Subnets = append(network.Subnets, *freeSubnet)
}
- if !ipv6 {
- freeSubnet, err := GetFreeIPv6NetworkSubnet(usedNetworks)
- if err != nil {
- return err
+ // 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 := GetFreeIPv4NetworkSubnet(usedNetworks)
+ if err != nil {
+ return err
+ }
+ network.Subnets = append(network.Subnets, *freeSubnet)
+ }
+ if !ipv6 {
+ freeSubnet, err := GetFreeIPv6NetworkSubnet(usedNetworks)
+ if err != nil {
+ return err
+ }
+ network.Subnets = append(network.Subnets, *freeSubnet)
}
- network.Subnets = append(network.Subnets, *freeSubnet)
}
+ network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
}
- network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
return nil
}
diff --git a/libpod/network/internal/util/ip.go b/libpod/network/internal/util/ip.go
index ee759fd65..7fe35d3d4 100644
--- a/libpod/network/internal/util/ip.go
+++ b/libpod/network/internal/util/ip.go
@@ -68,11 +68,3 @@ func getRandomIPv6Subnet() (net.IPNet, error) {
ip = append(ip, make([]byte, 8)...)
return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil
}
-
-// NormalizeIP will transform the given ip to the 4 byte len ipv4 if possible
-func NormalizeIP(ip *net.IP) {
- ipv4 := ip.To4()
- if ipv4 != nil {
- *ip = ipv4
- }
-}
diff --git a/libpod/network/internal/util/validate.go b/libpod/network/internal/util/validate.go
index 4dced8631..62c3f3951 100644
--- a/libpod/network/internal/util/validate.go
+++ b/libpod/network/internal/util/validate.go
@@ -38,7 +38,7 @@ func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet)
if !s.Subnet.Contains(s.Gateway) {
return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet)
}
- NormalizeIP(&s.Gateway)
+ util.NormalizeIP(&s.Gateway)
} else if addGateway {
ip, err := util.FirstIPInSubnet(net)
if err != nil {
@@ -52,13 +52,13 @@ func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet)
if !s.Subnet.Contains(s.LeaseRange.StartIP) {
return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet)
}
- NormalizeIP(&s.LeaseRange.StartIP)
+ util.NormalizeIP(&s.LeaseRange.StartIP)
}
if s.LeaseRange.EndIP != nil {
if !s.Subnet.Contains(s.LeaseRange.EndIP) {
return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet)
}
- NormalizeIP(&s.LeaseRange.EndIP)
+ util.NormalizeIP(&s.LeaseRange.EndIP)
}
}
return nil
diff --git a/libpod/network/netavark/ipam.go b/libpod/network/netavark/ipam.go
new file mode 100644
index 000000000..db46ee652
--- /dev/null
+++ b/libpod/network/netavark/ipam.go
@@ -0,0 +1,368 @@
+package netavark
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/libpod/network/util"
+ "github.com/pkg/errors"
+ "go.etcd.io/bbolt"
+)
+
+// IPAM boltdb structure
+// Each network has their own bucket with the network name as bucket key.
+// Inside the network bucket there is an ID bucket which maps the container ID (key)
+// to a json array of ip addresses (value).
+// The network bucket also has a bucket for each subnet, the subnet is used as key.
+// Inside the subnet bucket an ip is used as key and the container ID as value.
+
+const (
+ idBucket = "ids"
+ // lastIP this is used as key to store the last allocated ip
+ // note that this string should not be 4 or 16 byte long
+ lastIP = "lastIP"
+)
+
+var (
+ idBucketKey = []byte(idBucket)
+ lastIPKey = []byte(lastIP)
+)
+
+type ipamError struct {
+ msg string
+ cause error
+}
+
+func (e *ipamError) Error() string {
+ msg := "IPAM error"
+ if e.msg != "" {
+ msg += ": " + e.msg
+ }
+ if e.cause != nil {
+ msg += ": " + e.cause.Error()
+ }
+ return msg
+}
+
+func newIPAMError(cause error, msg string, args ...interface{}) *ipamError {
+ return &ipamError{
+ msg: fmt.Sprintf(msg, args...),
+ cause: cause,
+ }
+}
+
+// openDB will open the ipam database
+// Note that the caller has to Close it.
+func (n *netavarkNetwork) openDB() (*bbolt.DB, error) {
+ db, err := bbolt.Open(n.ipamDBPath, 0600, nil)
+ if err != nil {
+ return nil, newIPAMError(err, "failed to open database %s", n.ipamDBPath)
+ }
+ return db, nil
+}
+
+// allocIPs will allocate ips for the the container. It will change the
+// NetworkOptions in place. When static ips are given it will validate
+// that these are free to use and will allocate them to the container.
+func (n *netavarkNetwork) allocIPs(opts *types.NetworkOptions) error {
+ db, err := n.openDB()
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ err = db.Update(func(tx *bbolt.Tx) error {
+ for netName, netOpts := range opts.Networks {
+ network := n.networks[netName]
+ if network == nil {
+ return newIPAMError(nil, "could not find network %q", netName)
+ }
+
+ // check if we have to alloc ips
+ if !requiresIPAMAlloc(network) {
+ continue
+ }
+
+ // create/get network bucket
+ netBkt, err := tx.CreateBucketIfNotExists([]byte(netName))
+ if err != nil {
+ return newIPAMError(err, "failed to create/get network bucket for network %s", netName)
+ }
+
+ // requestIPs is the list of ips which should be used for this container
+ requestIPs := make([]net.IP, 0, len(network.Subnets))
+
+ for _, subnet := range network.Subnets {
+ subnetBkt, err := netBkt.CreateBucketIfNotExists([]byte(subnet.Subnet.String()))
+ if err != nil {
+ return newIPAMError(err, "failed to create/get subnet bucket for network %s", netName)
+ }
+
+ // search for a static ip which matches the current subnet
+ // in this case the user wants this one and we should not assign a free one
+ var ip net.IP
+ for _, staticIP := range netOpts.StaticIPs {
+ if subnet.Subnet.Contains(staticIP) {
+ ip = staticIP
+ break
+ }
+ }
+
+ // when static ip is requested for this network
+ if ip != nil {
+ // convert to 4 byte ipv4 if needed
+ util.NormalizeIP(&ip)
+ id := subnetBkt.Get(ip)
+ if id != nil {
+ return newIPAMError(nil, "requested ip address %s is already allocated to container ID %s", ip.String(), string(id))
+ }
+ } else {
+ ip, err = getFreeIPFromBucket(subnetBkt, subnet)
+ if err != nil {
+ return err
+ }
+ err = subnetBkt.Put(lastIPKey, ip)
+ if err != nil {
+ return newIPAMError(err, "failed to store last ip in database")
+ }
+ }
+
+ err = subnetBkt.Put(ip, []byte(opts.ContainerID))
+ if err != nil {
+ return newIPAMError(err, "failed to store ip in database")
+ }
+
+ requestIPs = append(requestIPs, ip)
+ }
+
+ idsBucket, err := netBkt.CreateBucketIfNotExists(idBucketKey)
+ if err != nil {
+ return newIPAMError(err, "failed to create/get id bucket for network %s", netName)
+ }
+
+ ipsBytes, err := json.Marshal(requestIPs)
+ if err != nil {
+ return newIPAMError(err, "failed to marshal ips")
+ }
+
+ err = idsBucket.Put([]byte(opts.ContainerID), ipsBytes)
+ if err != nil {
+ return newIPAMError(err, "failed to store ips in database")
+ }
+
+ netOpts.StaticIPs = requestIPs
+ opts.Networks[netName] = netOpts
+ }
+ return nil
+ })
+ return err
+}
+
+func getFreeIPFromBucket(bucket *bbolt.Bucket, subnet types.Subnet) (net.IP, error) {
+ var rangeStart net.IP
+ var rangeEnd net.IP
+ if subnet.LeaseRange != nil {
+ rangeStart = subnet.LeaseRange.StartIP
+ rangeEnd = subnet.LeaseRange.EndIP
+ }
+
+ if rangeStart == nil {
+ // let start with the first ip in subnet
+ rangeStart = util.NextIP(subnet.Subnet.IP)
+ }
+
+ if rangeEnd == nil {
+ lastIP, err := util.LastIPInSubnet(&subnet.Subnet.IPNet)
+ // this error should never happen but lets check anyways to prevent panics
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to get lastIP")
+ }
+ // ipv4 uses the last ip in a subnet for broadcast so we cannot use it
+ if util.IsIPv4(lastIP) {
+ lastIP = util.PrevIP(lastIP)
+ }
+ rangeEnd = lastIP
+ }
+
+ lastIPByte := bucket.Get(lastIPKey)
+ curIP := net.IP(lastIPByte)
+ if curIP == nil {
+ curIP = rangeStart
+ } else {
+ curIP = util.NextIP(curIP)
+ }
+
+ // store the start ip to make sure we know when we looped over all available ips
+ startIP := curIP
+
+ for {
+ // skip the gateway
+ if subnet.Gateway != nil {
+ if util.Cmp(curIP, subnet.Gateway) == 0 {
+ curIP = util.NextIP(curIP)
+ continue
+ }
+ }
+
+ // if we are at the end we need to jump back to the start ip
+ if util.Cmp(curIP, rangeEnd) > 0 {
+ if util.Cmp(rangeStart, startIP) == 0 {
+ return nil, newIPAMError(nil, "failed to find free IP in range: %s - %s", rangeStart.String(), rangeEnd.String())
+ }
+ curIP = rangeStart
+ continue
+ }
+
+ // check if ip is already used by another container
+ // if not return it
+ if bucket.Get(curIP) == nil {
+ return curIP, nil
+ }
+
+ curIP = util.NextIP(curIP)
+
+ if util.Cmp(curIP, startIP) == 0 {
+ return nil, newIPAMError(nil, "failed to find free IP in range: %s - %s", rangeStart.String(), rangeEnd.String())
+ }
+ }
+}
+
+// getAssignedIPs will read the ipam database and will fill in the used ips for this container.
+// It will change the NetworkOptions in place.
+func (n *netavarkNetwork) getAssignedIPs(opts *types.NetworkOptions) error {
+ db, err := n.openDB()
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ err = db.View(func(tx *bbolt.Tx) error {
+ for netName, netOpts := range opts.Networks {
+ network := n.networks[netName]
+ if network == nil {
+ return newIPAMError(nil, "could not find network %q", netName)
+ }
+
+ // check if we have to alloc ips
+ if !requiresIPAMAlloc(network) {
+ continue
+ }
+ // get network bucket
+ netBkt := tx.Bucket([]byte(netName))
+ if netBkt == nil {
+ return newIPAMError(nil, "failed to get network bucket for network %s", netName)
+ }
+
+ idBkt := netBkt.Bucket(idBucketKey)
+ if idBkt == nil {
+ return newIPAMError(nil, "failed to get id bucket for network %s", netName)
+ }
+
+ ipJSON := idBkt.Get([]byte(opts.ContainerID))
+ if ipJSON == nil {
+ return newIPAMError(nil, "failed to get ips for container ID %s on network %s", opts.ContainerID, netName)
+ }
+
+ // assignedIPs is the list of ips which should be used for this container
+ assignedIPs := make([]net.IP, 0, len(network.Subnets))
+
+ err = json.Unmarshal(ipJSON, &assignedIPs)
+ if err != nil {
+ return newIPAMError(err, "failed to unmarshal ips from database")
+ }
+
+ for i := range assignedIPs {
+ util.NormalizeIP(&assignedIPs[i])
+ }
+
+ netOpts.StaticIPs = assignedIPs
+ opts.Networks[netName] = netOpts
+ }
+ return nil
+ })
+ return err
+}
+
+// deallocIPs will release the ips in the network options from the DB so that
+// they can be reused by other containers. It expects that the network options
+// are already filled with the correct ips. Use getAssignedIPs() for this.
+func (n *netavarkNetwork) deallocIPs(opts *types.NetworkOptions) error {
+ db, err := n.openDB()
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ err = db.Update(func(tx *bbolt.Tx) error {
+ for netName, netOpts := range opts.Networks {
+ network := n.networks[netName]
+ if network == nil {
+ return newIPAMError(nil, "could not find network %q", netName)
+ }
+
+ // check if we have to alloc ips
+ if !requiresIPAMAlloc(network) {
+ continue
+ }
+ // get network bucket
+ netBkt := tx.Bucket([]byte(netName))
+ if netBkt == nil {
+ return newIPAMError(nil, "failed to get network bucket for network %s", netName)
+ }
+
+ for _, subnet := range network.Subnets {
+ subnetBkt := netBkt.Bucket([]byte(subnet.Subnet.String()))
+ if subnetBkt == nil {
+ return newIPAMError(nil, "failed to get subnet bucket for network %s", netName)
+ }
+
+ // search for a static ip which matches the current subnet
+ // in this case the user wants this one and we should not assign a free one
+ var ip net.IP
+ for _, staticIP := range netOpts.StaticIPs {
+ if subnet.Subnet.Contains(staticIP) {
+ ip = staticIP
+ break
+ }
+ }
+ if ip == nil {
+ return newIPAMError(nil, "failed to find ip for subnet %s on network %s", subnet.Subnet.String(), netName)
+ }
+ util.NormalizeIP(&ip)
+
+ err = subnetBkt.Delete(ip)
+ if err != nil {
+ return newIPAMError(err, "failed to remove ip %s from subnet bucket for network %s", ip.String(), netName)
+ }
+ }
+
+ idBkt := netBkt.Bucket(idBucketKey)
+ if idBkt == nil {
+ return newIPAMError(nil, "failed to get id bucket for network %s", netName)
+ }
+
+ err = idBkt.Delete([]byte(opts.ContainerID))
+ if err != nil {
+ return newIPAMError(err, "failed to remove allocated ips for container ID %s on network %s", opts.ContainerID, netName)
+ }
+ }
+ return nil
+ })
+ return err
+}
+
+// requiresIPAMAlloc return true when we have to allocate ips for this network
+// it checks the ipam driver and if subnets are set
+func requiresIPAMAlloc(network *types.Network) bool {
+ // only do host allocation when driver is set to HostLocalIPAMDriver or unset
+ switch network.IPAMOptions["driver"] {
+ case "", types.HostLocalIPAMDriver:
+ default:
+ return false
+ }
+
+ // no subnets == no ips to assign
+ return len(network.Subnets) > 0
+}
diff --git a/libpod/network/netavark/ipam_test.go b/libpod/network/netavark/ipam_test.go
new file mode 100644
index 000000000..4b3947501
--- /dev/null
+++ b/libpod/network/netavark/ipam_test.go
@@ -0,0 +1,433 @@
+package netavark
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "github.com/sirupsen/logrus"
+)
+
+var _ = Describe("IPAM", func() {
+ var (
+ networkInterface *netavarkNetwork
+ networkConfDir string
+ logBuffer bytes.Buffer
+ )
+
+ BeforeEach(func() {
+ var err error
+ networkConfDir, err = ioutil.TempDir("", "podman_netavark_test")
+ if err != nil {
+ Fail("Failed to create tmpdir")
+
+ }
+ logBuffer = bytes.Buffer{}
+ logrus.SetOutput(&logBuffer)
+ })
+
+ JustBeforeEach(func() {
+ libpodNet, err := NewNetworkInterface(InitConfig{
+ NetworkConfigDir: networkConfDir,
+ IPAMDBPath: filepath.Join(networkConfDir, "ipam.db"),
+ LockFile: filepath.Join(networkConfDir, "netavark.lock"),
+ })
+ if err != nil {
+ Fail("Failed to create NewCNINetworkInterface")
+ }
+
+ networkInterface = libpodNet.(*netavarkNetwork)
+ // run network list to force a network load
+ networkInterface.NetworkList()
+ })
+
+ AfterEach(func() {
+ os.RemoveAll(networkConfDir)
+ })
+
+ It("simple ipam alloc", func() {
+ netName := types.DefaultNetworkName
+ for i := 2; i < 100; i++ {
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err := networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.88.0.%d", i)).To4()))
+ }
+ })
+
+ It("ipam try to alloc same ip", func() {
+ netName := types.DefaultNetworkName
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err := networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.88.0.2").To4()))
+
+ opts = &types.NetworkOptions{
+ ContainerID: "otherID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {StaticIPs: []net.IP{net.ParseIP("10.88.0.2")}},
+ },
+ }
+ err = networkInterface.allocIPs(opts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: requested ip address 10.88.0.2 is already allocated to container ID someContainerID"))
+ })
+
+ It("ipam try to alloc more ips as in range", func() {
+ s, _ := types.ParseCIDR("10.0.0.1/24")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ LeaseRange: &types.LeaseRange{
+ StartIP: net.ParseIP("10.0.0.10"),
+ EndIP: net.ParseIP("10.0.0.20"),
+ },
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ for i := 10; i < 21; i++ {
+ opts := &types.NetworkOptions{
+ ContainerID: fmt.Sprintf("someContainerID-%d", i),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", i)).To4()))
+ }
+
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID-22",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ // now this should fail because all free ips are already assigned
+ err = networkInterface.allocIPs(opts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: failed to find free IP in range: 10.0.0.10 - 10.0.0.20"))
+ })
+
+ It("ipam basic setup", func() {
+ netName := types.DefaultNetworkName
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ expectedIP := net.ParseIP("10.88.0.2").To4()
+
+ err := networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(expectedIP))
+
+ // remove static ips from opts
+ netOpts := opts.Networks[netName]
+ netOpts.StaticIPs = nil
+ opts.Networks[netName] = netOpts
+
+ err = networkInterface.getAssignedIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(expectedIP))
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: requested ip address 10.88.0.2 is already allocated to container ID someContainerID"))
+
+ // dealloc the ip
+ err = networkInterface.deallocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(expectedIP))
+ })
+
+ It("ipam dual stack", func() {
+ s1, _ := types.ParseCIDR("10.0.0.0/26")
+ s2, _ := types.ParseCIDR("fd80::/24")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s1,
+ },
+ {
+ Subnet: s2,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(2))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks[netName].StaticIPs[1]).To(Equal(net.ParseIP("fd80::2")))
+
+ // remove static ips from opts
+ netOpts := opts.Networks[netName]
+ netOpts.StaticIPs = nil
+ opts.Networks[netName] = netOpts
+
+ err = networkInterface.getAssignedIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(2))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks[netName].StaticIPs[1]).To(Equal(net.ParseIP("fd80::2")))
+
+ err = networkInterface.deallocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+
+ // try to alloc the same again
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(2))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks[netName].StaticIPs[1]).To(Equal(net.ParseIP("fd80::2")))
+ })
+
+ It("ipam with two networks", func() {
+ s, _ := types.ParseCIDR("10.0.0.0/24")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName1 := network.Name
+
+ s, _ = types.ParseCIDR("10.0.1.0/24")
+ network, err = networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName2 := network.Name
+
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName1: {},
+ netName2: {},
+ },
+ }
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName1))
+ Expect(opts.Networks[netName1].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName1].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks).To(HaveKey(netName2))
+ Expect(opts.Networks[netName2].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName2].StaticIPs[0]).To(Equal(net.ParseIP("10.0.1.2").To4()))
+
+ // remove static ips from opts
+ netOpts := opts.Networks[netName1]
+ netOpts.StaticIPs = nil
+ opts.Networks[netName1] = netOpts
+ netOpts = opts.Networks[netName2]
+ netOpts.StaticIPs = nil
+ opts.Networks[netName2] = netOpts
+
+ err = networkInterface.getAssignedIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName1))
+ Expect(opts.Networks[netName1].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName1].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks).To(HaveKey(netName2))
+ Expect(opts.Networks[netName2].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName2].StaticIPs[0]).To(Equal(net.ParseIP("10.0.1.2").To4()))
+
+ err = networkInterface.deallocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+
+ // try to alloc the same again
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName1))
+ Expect(opts.Networks[netName1].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName1].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks).To(HaveKey(netName2))
+ Expect(opts.Networks[netName2].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName2].StaticIPs[0]).To(Equal(net.ParseIP("10.0.1.2").To4()))
+ })
+
+ It("ipam alloc more ips as in subnet", func() {
+ s, _ := types.ParseCIDR("10.0.0.0/26")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ for i := 2; i < 64; i++ {
+ opts := &types.NetworkOptions{
+ ContainerID: fmt.Sprintf("id-%d", i),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+ err = networkInterface.allocIPs(opts)
+ if i < 63 {
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", i)).To4()))
+ } else {
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: failed to find free IP in range: 10.0.0.1 - 10.0.0.62"))
+ }
+ }
+ })
+
+ It("ipam alloc -> dealloc -> alloc", func() {
+ s, _ := types.ParseCIDR("10.0.0.0/27")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ for i := 2; i < 10; i++ {
+ opts := types.NetworkOptions{
+ ContainerID: fmt.Sprintf("id-%d", i),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+ err = networkInterface.allocIPs(&opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", i)).To4()))
+
+ err = networkInterface.deallocIPs(&opts)
+ Expect(err).ToNot(HaveOccurred())
+ }
+
+ for i := 0; i < 30; i++ {
+ opts := types.NetworkOptions{
+ ContainerID: fmt.Sprintf("id-%d", i),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+ err = networkInterface.allocIPs(&opts)
+ if i < 29 {
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ // The (i+8)%29+2 part looks cryptic but it is actually simple, we already have 8 ips allocated above
+ // so we expect the 8 available ip. We have 29 assignable ip addresses in this subnet because "i"+8 can
+ // be greater than 30 we have to modulo by 29 to go back to the beginning. Also the first free ip is
+ // network address + 2, so we have to add 2 to the result
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", (i+8)%29+2)).To4()))
+ } else {
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: failed to find free IP in range: 10.0.0.1 - 10.0.0.30"))
+ }
+ }
+ })
+
+ It("ipam with dhcp driver should not set ips", func() {
+ network, err := networkInterface.NetworkCreate(types.Network{
+ IPAMOptions: map[string]string{
+ "driver": types.DHCPIPAMDriver,
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(0))
+
+ err = networkInterface.getAssignedIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(0))
+
+ // dealloc the ip
+ err = networkInterface.deallocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ })
+
+})
diff --git a/libpod/network/netavark/netavark_suite_test.go b/libpod/network/netavark/netavark_suite_test.go
index b24f880e0..4d0c04634 100644
--- a/libpod/network/netavark/netavark_suite_test.go
+++ b/libpod/network/netavark/netavark_suite_test.go
@@ -32,6 +32,7 @@ func getNetworkInterface(confDir string, machine bool) (types.ContainerNetwork,
NetworkConfigDir: confDir,
IsMachine: machine,
NetavarkBinary: netavarkBinary,
+ IPAMDBPath: filepath.Join(confDir, "ipam.db"),
LockFile: filepath.Join(confDir, "netavark.lock"),
})
}
diff --git a/libpod/network/netavark/network.go b/libpod/network/netavark/network.go
index aa7dc8183..cd6db4f58 100644
--- a/libpod/network/netavark/network.go
+++ b/libpod/network/netavark/network.go
@@ -13,6 +13,7 @@ import (
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/libpod/network/internal/util"
"github.com/containers/podman/v3/libpod/network/types"
+ pkgutil "github.com/containers/podman/v3/pkg/util"
"github.com/containers/storage/pkg/lockfile"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -30,6 +31,9 @@ type netavarkNetwork struct {
// defaultSubnet is the default subnet for the default network.
defaultSubnet types.IPNet
+ // ipamDBPath is the path to the ip allocation bolt db
+ ipamDBPath string
+
// isMachine describes whenever podman runs in a podman machine environment.
isMachine bool
@@ -50,6 +54,10 @@ type InitConfig struct {
// NetavarkBinary is the path to the netavark binary.
NetavarkBinary string
+ // IPAMDBPath is the path to the ipam database. This should be on a tmpfs.
+ // If empty defaults to XDG_RUNTIME_DIR/netavark/ipam.db or /run/netavark/ipam.db as root.
+ IPAMDBPath string
+
// DefaultNetwork is the name for the default network.
DefaultNetwork string
// DefaultSubnet is the default subnet for the default network.
@@ -85,9 +93,27 @@ func NewNetworkInterface(conf InitConfig) (types.ContainerNetwork, error) {
return nil, errors.Wrap(err, "failed to parse default subnet")
}
+ ipamdbPath := conf.IPAMDBPath
+ if ipamdbPath == "" {
+ runDir, err := pkgutil.GetRuntimeDir()
+ if err != nil {
+ return nil, err
+ }
+ // as root runtimeDir is empty so use /run
+ if runDir == "" {
+ runDir = "/run"
+ }
+ ipamdbPath = filepath.Join(runDir, "netavark")
+ if err := os.MkdirAll(ipamdbPath, 0700); err != nil {
+ return nil, errors.Wrap(err, "failed to create ipam db path")
+ }
+ ipamdbPath = filepath.Join(ipamdbPath, "ipam.db")
+ }
+
n := &netavarkNetwork{
networkConfigDir: conf.NetworkConfigDir,
netavarkBinary: conf.NetavarkBinary,
+ ipamDBPath: ipamdbPath,
defaultNetwork: defaultNetworkName,
defaultSubnet: defaultNet,
isMachine: conf.IsMachine,
diff --git a/libpod/network/netavark/run.go b/libpod/network/netavark/run.go
index bd26e957e..2f839151e 100644
--- a/libpod/network/netavark/run.go
+++ b/libpod/network/netavark/run.go
@@ -32,21 +32,29 @@ func (n *netavarkNetwork) Setup(namespacePath string, options types.SetupOptions
return nil, err
}
- // TODO IP address assignment
+ // allocate IPs in the IPAM db
+ err = n.allocIPs(&options.NetworkOptions)
+ if err != nil {
+ return nil, err
+ }
netavarkOpts, err := n.convertNetOpts(options.NetworkOptions)
if err != nil {
return nil, errors.Wrap(err, "failed to convert net opts")
}
- b, err := json.Marshal(&netavarkOpts)
- if err != nil {
- return nil, err
+ // trace output to get the json
+ if logrus.IsLevelEnabled(logrus.TraceLevel) {
+ b, err := json.Marshal(&netavarkOpts)
+ if err != nil {
+ return nil, err
+ }
+ // show the full netavark command so we can easily reproduce errors from the cli
+ logrus.Tracef("netavark command: printf '%s' | %s setup %s", string(b), n.netavarkBinary, namespacePath)
}
- fmt.Println(string(b))
result := map[string]types.StatusBlock{}
- err = execNetavark(n.netavarkBinary, []string{"setup", namespacePath}, netavarkOpts, result)
+ err = execNetavark(n.netavarkBinary, []string{"setup", namespacePath}, netavarkOpts, &result)
if len(result) != len(options.Networks) {
logrus.Errorf("unexpected netavark result: %v", result)
@@ -65,12 +73,33 @@ func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownO
return err
}
+ // get IPs from the IPAM db
+ err = n.getAssignedIPs(&options.NetworkOptions)
+ if err != nil {
+ // when there is an error getting the ips we should still continue
+ // to call teardown for netavark to prevent leaking network interfaces
+ logrus.Error(err)
+ }
+
netavarkOpts, err := n.convertNetOpts(options.NetworkOptions)
if err != nil {
return errors.Wrap(err, "failed to convert net opts")
}
- return execNetavark(n.netavarkBinary, []string{"teardown", namespacePath}, netavarkOpts, nil)
+ retErr := execNetavark(n.netavarkBinary, []string{"teardown", namespacePath}, netavarkOpts, nil)
+
+ // when netavark returned an error we still free the used ips
+ // otherwise we could end up in a state where block the ips forever
+ err = n.deallocIPs(&netavarkOpts.NetworkOptions)
+ if err != nil {
+ if retErr != nil {
+ logrus.Error(err)
+ } else {
+ retErr = err
+ }
+ }
+
+ return retErr
}
func (n *netavarkNetwork) convertNetOpts(opts types.NetworkOptions) (*netavarkOptions, error) {
diff --git a/libpod/network/netavark/run_test.go b/libpod/network/netavark/run_test.go
index 6c3b4d970..84db89b49 100644
--- a/libpod/network/netavark/run_test.go
+++ b/libpod/network/netavark/run_test.go
@@ -14,8 +14,6 @@ package netavark_test
// })
import (
- "bytes"
- "fmt"
"io/ioutil"
"net"
"os"
@@ -39,7 +37,6 @@ var _ = Describe("run netavark", func() {
var (
libpodNet types.ContainerNetwork
confDir string
- logBuffer bytes.Buffer
netNSTest ns.NetNS
netNSContainer ns.NetNS
)
@@ -60,8 +57,11 @@ var _ = Describe("run netavark", func() {
}
BeforeEach(func() {
+ // set the logrus settings
logrus.SetLevel(logrus.TraceLevel)
+ // disable extra quotes so we can easily copy the netavark command
logrus.SetFormatter(&logrus.TextFormatter{DisableQuote: true})
+ logrus.SetOutput(os.Stderr)
// 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.
@@ -74,8 +74,6 @@ var _ = Describe("run netavark", func() {
if err != nil {
Fail("Failed to create tmpdir")
}
- logBuffer = bytes.Buffer{}
- logrus.SetOutput(&logBuffer)
netNSTest, err = netns.NewNS()
if err != nil {
@@ -106,8 +104,6 @@ var _ = Describe("run netavark", func() {
netns.UnmountNS(netNSContainer)
netNSContainer.Close()
-
- fmt.Println(logBuffer.String())
})
It("test basic setup", func() {
diff --git a/libpod/network/util/ip.go b/libpod/network/util/ip.go
index e75107a1c..e82b4a781 100644
--- a/libpod/network/util/ip.go
+++ b/libpod/network/util/ip.go
@@ -1,6 +1,8 @@
package util
-import "net"
+import (
+ "net"
+)
// IsIPv6 returns true if netIP is IPv6.
func IsIPv6(netIP net.IP) bool {
@@ -44,3 +46,11 @@ func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer
cidr.IP[len(cidr.IP)-1]++
return cidr.IP, nil
}
+
+// NormalizeIP will transform the given ip to the 4 byte len ipv4 if possible
+func NormalizeIP(ip *net.IP) {
+ ipv4 := ip.To4()
+ if ipv4 != nil {
+ *ip = ipv4
+ }
+}
diff --git a/libpod/network/util/ip_calc.go b/libpod/network/util/ip_calc.go
new file mode 100644
index 000000000..a27ddf78b
--- /dev/null
+++ b/libpod/network/util/ip_calc.go
@@ -0,0 +1,53 @@
+// Copyright 2015 CNI authors
+//
+// 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.
+
+package util
+
+import (
+ "math/big"
+ "net"
+)
+
+// NextIP returns IP incremented by 1
+func NextIP(ip net.IP) net.IP {
+ i := ipToInt(ip)
+ return intToIP(i.Add(i, big.NewInt(1)))
+}
+
+// PrevIP returns IP decremented by 1
+func PrevIP(ip net.IP) net.IP {
+ i := ipToInt(ip)
+ return intToIP(i.Sub(i, big.NewInt(1)))
+}
+
+// Cmp compares two IPs, returning the usual ordering:
+// a < b : -1
+// a == b : 0
+// a > b : 1
+func Cmp(a, b net.IP) int {
+ aa := ipToInt(a)
+ bb := ipToInt(b)
+ return aa.Cmp(bb)
+}
+
+func ipToInt(ip net.IP) *big.Int {
+ if v := ip.To4(); v != nil {
+ return big.NewInt(0).SetBytes(v)
+ }
+ return big.NewInt(0).SetBytes(ip.To16())
+}
+
+func intToIP(i *big.Int) net.IP {
+ return net.IP(i.Bytes())
+}