package defaultipam
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"github.com/containerd/log"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/v2/daemon/internal/netiputil"
"github.com/moby/moby/v2/daemon/libnetwork/internal/addrset"
"github.com/moby/moby/v2/daemon/libnetwork/ipamapi"
"github.com/moby/moby/v2/daemon/libnetwork/ipamutils"
"github.com/moby/moby/v2/daemon/libnetwork/types"
)
const (
// DriverName is the name of the built-in default IPAM driver.
DriverName = "default"
localAddressSpace = "LocalDefault"
globalAddressSpace = "GlobalDefault"
)
var _ ipamapi.PoolStatuser = &Allocator{}
// Register registers the default ipam driver with libnetwork. It takes
// two optional address pools respectively containing the list of user-defined
// address pools for 'local' and 'global' address spaces.
func Register(ic ipamapi.Registerer, lAddrPools, gAddrPools []*ipamutils.NetworkToSplit) error {
if len(gAddrPools) == 0 {
gAddrPools = ipamutils.GetGlobalScopeDefaultNetworks()
}
a, err := NewAllocator(lAddrPools, gAddrPools)
if err != nil {
return err
}
cps := &ipamapi.Capability{RequiresRequestReplay: true}
return ic.RegisterIpamDriverWithCapabilities(DriverName, a, cps)
}
// Allocator provides per address space ipv4/ipv6 bookkeeping
type Allocator struct {
// The address spaces
local4, local6, global4, global6 *addrSpace
}
// NewAllocator returns an instance of libnetwork ipam
func NewAllocator(lcAs, glAs []*ipamutils.NetworkToSplit) (*Allocator, error) {
var (
a Allocator
err error
lcAs4, lcAs6, glAs4, glAs6 []*ipamutils.NetworkToSplit
)
lcAs4, lcAs6, err = splitByIPFamily(lcAs)
if err != nil {
return nil, fmt.Errorf("could not construct local address space: %w", err)
}
glAs4, glAs6, err = splitByIPFamily(glAs)
if err != nil {
return nil, fmt.Errorf("could not construct global address space: %w", err)
}
a.local4, err = newAddrSpace(lcAs4)
if err != nil {
return nil, fmt.Errorf("could not construct local v4 address space: %w", err)
}
a.local6, err = newAddrSpace(lcAs6)
if err != nil {
return nil, fmt.Errorf("could not construct local v6 address space: %w", err)
}
a.global4, err = newAddrSpace(glAs4)
if err != nil {
return nil, fmt.Errorf("could not construct global v4 address space: %w", err)
}
a.global6, err = newAddrSpace(glAs6)
if err != nil {
return nil, fmt.Errorf("could not construct global v6 address space: %w", err)
}
return &a, nil
}
func splitByIPFamily(s []*ipamutils.NetworkToSplit) ([]*ipamutils.NetworkToSplit, []*ipamutils.NetworkToSplit, error) {
var v4, v6 []*ipamutils.NetworkToSplit
for i, n := range s {
if !n.Base.IsValid() || n.Size == 0 {
return []*ipamutils.NetworkToSplit{}, []*ipamutils.NetworkToSplit{}, fmt.Errorf("network at index %d (%v) is not in canonical form", i, n)
}
if n.Base.Bits() > n.Size {
return []*ipamutils.NetworkToSplit{}, []*ipamutils.NetworkToSplit{}, fmt.Errorf("network at index %d (%v) has a smaller prefix (/%d) than the target size of that pool (/%d)", i, n, n.Base.Bits(), n.Size)
}
n.Base, _ = n.Base.Addr().Unmap().Prefix(n.Base.Bits())
if n.Base.Addr().Is4() {
v4 = append(v4, n)
} else {
v6 = append(v6, n)
}
}
return v4, v6, nil
}
// GetDefaultAddressSpaces returns the local and global default address spaces
func (a *Allocator) GetDefaultAddressSpaces() (string, string, error) {
return localAddressSpace, globalAddressSpace, nil
}
// RequestPool returns an address pool along with its unique id.
// addressSpace must be a valid address space name and must not be the empty string.
// If requestedPool is the empty string then the default predefined pool for addressSpace will be used, otherwise pool must be a valid IP address and length in CIDR notation.
// If requestedSubPool is not empty, it must be a valid IP address and length in CIDR notation which is a sub-range of requestedPool.
// requestedSubPool must be empty if requestedPool is empty.
func (a *Allocator) RequestPool(req ipamapi.PoolRequest) (ipamapi.AllocatedPool, error) {
log.G(context.TODO()).Debugf("RequestPool: %+v", req)
parseErr := func(err error) error {
return types.InternalErrorf("failed to parse pool request for address space %q pool %q subpool %q: %v", req.AddressSpace, req.Pool, req.SubPool, err)
}
if req.AddressSpace == "" {
return ipamapi.AllocatedPool{}, parseErr(ipamapi.ErrInvalidAddressSpace)
}
aSpace, err := a.getAddrSpace(req.AddressSpace, req.V6)
if err != nil {
return ipamapi.AllocatedPool{}, err
}
if req.Pool == "" && req.SubPool != "" {
return ipamapi.AllocatedPool{}, parseErr(ipamapi.ErrInvalidSubPool)
}
k := PoolID{AddressSpace: req.AddressSpace}
if req.Pool == "" {
if k.Subnet, err = aSpace.allocatePredefinedPool(req.Exclude); err != nil {
return ipamapi.AllocatedPool{}, err
}
return ipamapi.AllocatedPool{PoolID: k.String(), Pool: k.Subnet}, nil
}
if k.Subnet, err = netip.ParsePrefix(req.Pool); err != nil {
return ipamapi.AllocatedPool{}, parseErr(ipamapi.ErrInvalidPool)
}
if req.SubPool != "" {
if k.ChildSubnet, err = netip.ParsePrefix(req.SubPool); err != nil {
return ipamapi.AllocatedPool{}, types.InternalErrorf("invalid pool request: %v", ipamapi.ErrInvalidSubPool)
}
}
// This is a new non-master pool (subPool)
if k.Subnet.IsValid() && k.ChildSubnet.IsValid() && k.Subnet.Addr().BitLen() != k.ChildSubnet.Addr().BitLen() {
return ipamapi.AllocatedPool{}, types.InvalidParameterErrorf("pool and subpool are of incompatible address families")
}
k.Subnet, k.ChildSubnet = k.Subnet.Masked(), k.ChildSubnet.Masked()
// Prior to https://github.com/moby/moby/pull/44968, libnetwork would happily accept a ChildSubnet with a bigger
// mask than its parent subnet. In such case, it was producing IP addresses based on the parent subnet, and the
// child subnet was not allocated from the address pool. Following condition take care of restoring this behavior
// for networks created before upgrading to v24.0.
if k.ChildSubnet.IsValid() && k.ChildSubnet.Bits() < k.Subnet.Bits() {
k.ChildSubnet = k.Subnet
}
err = aSpace.allocateSubnet(k.Subnet, k.ChildSubnet)
if err != nil {
return ipamapi.AllocatedPool{}, types.ForbiddenErrorf("invalid pool request: %v", err)
}
return ipamapi.AllocatedPool{PoolID: k.String(), Pool: k.Subnet}, nil
}
// ReleasePool releases the address pool identified by the passed id
func (a *Allocator) ReleasePool(poolID string) error {
log.G(context.TODO()).Debugf("ReleasePool(%s)", poolID)
k, err := PoolIDFromString(poolID)
if err != nil {
return types.InvalidParameterErrorf("invalid pool id: %s", poolID)
}
aSpace, err := a.getAddrSpace(k.AddressSpace, k.Is6())
if err != nil {
return err
}
return aSpace.releaseSubnet(k.Subnet, k.ChildSubnet)
}
// Given the address space, returns the local or global PoolConfig based on whether the
// address space is local or global. AddressSpace locality is registered with IPAM out of band.
func (a *Allocator) getAddrSpace(as string, v6 bool) (*addrSpace, error) {
switch as {
case localAddressSpace:
if v6 {
return a.local6, nil
}
return a.local4, nil
case globalAddressSpace:
if v6 {
return a.global6, nil
}
return a.global4, nil
}
return nil, types.InvalidParameterErrorf("cannot find address space %s", as)
}
func newPoolData(pool netip.Prefix) *PoolData {
h := addrset.New(pool)
// Reserve the first address in the range for the:
// - IPv4 network address
// - Except in a /31 point-to-point link, https://datatracker.ietf.org/doc/html/rfc3021
// - IPv6 Subnet-Router anycast address, https://datatracker.ietf.org/doc/html/rfc4291#section-2.6.1
bits := pool.Addr().BitLen() - pool.Bits()
if !pool.Addr().Is4() || bits > 1 {
h.Add(pool.Addr())
}
// For IPv4, reserve the broadcast address.
// - Except in a /31 point-to-point link, https://datatracker.ietf.org/doc/html/rfc3021
if pool.Addr().Is4() && bits > 1 {
h.Add(netiputil.LastAddr(pool))
}
return &PoolData{addrs: h, children: map[netip.Prefix]struct{}{}}
}
// RequestAddress returns an address from the specified pool ID
func (a *Allocator) RequestAddress(poolID string, prefAddress net.IP, opts map[string]string) (*net.IPNet, map[string]string, error) {
log.G(context.TODO()).Debugf("RequestAddress(%s, %v, %v)", poolID, prefAddress, opts)
k, err := PoolIDFromString(poolID)
if err != nil {
return nil, nil, types.InvalidParameterErrorf("invalid pool id: %s", poolID)
}
aSpace, err := a.getAddrSpace(k.AddressSpace, k.Is6())
if err != nil {
return nil, nil, err
}
var pref netip.Addr
if prefAddress != nil {
var ok bool
pref, ok = netip.AddrFromSlice(prefAddress)
if !ok {
return nil, nil, types.InvalidParameterErrorf("invalid preferred address: %v", prefAddress)
}
}
p, err := aSpace.requestAddress(k.Subnet, k.ChildSubnet, pref.Unmap(), opts)
if err != nil {
return nil, nil, err
}
return &net.IPNet{
IP: p.AsSlice(),
Mask: net.CIDRMask(k.Subnet.Bits(), k.Subnet.Addr().BitLen()),
}, nil, nil
}
// ReleaseAddress releases the address from the specified pool ID
func (a *Allocator) ReleaseAddress(poolID string, address net.IP) error {
log.G(context.TODO()).Debugf("ReleaseAddress(%s, %v)", poolID, address)
k, err := PoolIDFromString(poolID)
if err != nil {
return types.InvalidParameterErrorf("invalid pool id: %s", poolID)
}
aSpace, err := a.getAddrSpace(k.AddressSpace, k.Is6())
if err != nil {
return err
}
addr, ok := netip.AddrFromSlice(address)
if !ok {
return types.InvalidParameterErrorf("invalid address: %v", address)
}
return aSpace.releaseAddress(k.Subnet, k.ChildSubnet, addr.Unmap())
}
func getAddress(base netip.Prefix, addrSet *addrset.AddrSet, prefAddress netip.Addr, ipr netip.Prefix, serial bool) (netip.Addr, error) {
var (
addr netip.Addr
err error
)
log.G(context.TODO()).Debugf("Request address PoolID:%v %s Serial:%v PrefAddress:%v ", base, addrSet, serial, prefAddress)
if prefAddress.IsValid() {
err = addrSet.Add(prefAddress)
if err == nil {
addr = prefAddress
}
} else if ipr.IsValid() && ipr != base {
addr, err = addrSet.AddAnyInRange(ipr, serial)
} else {
addr, err = addrSet.AddAny(serial)
}
if err != nil {
if errors.Is(err, addrset.ErrAllocated) {
return netip.Addr{}, ipamapi.ErrIPAlreadyAllocated
}
if errors.Is(err, addrset.ErrNotAvailable) {
return netip.Addr{}, ipamapi.ErrNoAvailableIPs
}
return netip.Addr{}, err
}
return addr, nil
}
// PoolStatus returns the operational status of the specified IPAM pool.
func (a *Allocator) PoolStatus(poolID string) (network.SubnetStatus, error) {
k, err := PoolIDFromString(poolID)
if err != nil {
return network.SubnetStatus{}, types.InvalidParameterErrorf("invalid pool id: %s", poolID)
}
aSpace, err := a.getAddrSpace(k.AddressSpace, k.Is6())
if err != nil {
return network.SubnetStatus{}, err
}
return aSpace.allocationStatus(k.Subnet, k.ChildSubnet)
}
// IsBuiltIn returns true for builtin drivers
func (a *Allocator) IsBuiltIn() bool {
return true
}