package remote import ( "context" "fmt" "net" "net/netip" "github.com/containerd/log" "github.com/moby/moby/v2/daemon/libnetwork/ipamapi" "github.com/moby/moby/v2/daemon/libnetwork/ipams/remote/api" "github.com/moby/moby/v2/daemon/libnetwork/types" "github.com/moby/moby/v2/pkg/plugingetter" "github.com/moby/moby/v2/pkg/plugins" "github.com/pkg/errors" ) type allocator struct { endpoint *plugins.Client name string } // PluginResponse is the interface for the plugin request responses type PluginResponse interface { IsSuccess() bool GetError() string } func newAllocator(name string, client *plugins.Client) ipamapi.Ipam { a := &allocator{name: name, endpoint: client} return a } // Register registers a remote ipam when its plugin is activated. func Register(cb ipamapi.Registerer, pg plugingetter.PluginGetter) error { newPluginHandler := func(name string, client *plugins.Client) { a := newAllocator(name, client) if cps, err := a.(*allocator).getCapabilities(); err == nil { if err := cb.RegisterIpamDriverWithCapabilities(name, a, cps); err != nil { log.G(context.TODO()).Errorf("error registering remote ipam driver %s due to %v", name, err) } } else { log.G(context.TODO()).Infof("remote ipam driver %s does not support capabilities", name) log.G(context.TODO()).Debug(err) if err := cb.RegisterIpamDriver(name, a); err != nil { log.G(context.TODO()).Errorf("error registering remote ipam driver %s due to %v", name, err) } } } // Unit test code is unaware of a true PluginStore. So we fall back to v1 plugins. handleFunc := plugins.Handle if pg != nil { handleFunc = pg.Handle activePlugins := pg.GetAllManagedPluginsByCap(ipamapi.PluginEndpointType) for _, ap := range activePlugins { client, err := getPluginClient(ap) if err != nil { return err } newPluginHandler(ap.Name(), client) } } handleFunc(ipamapi.PluginEndpointType, newPluginHandler) return nil } func getPluginClient(p plugingetter.CompatPlugin) (*plugins.Client, error) { if v1, ok := p.(plugingetter.PluginWithV1Client); ok { return v1.Client(), nil } pa, ok := p.(plugingetter.PluginAddr) if !ok { return nil, errors.Errorf("unknown plugin type %T", p) } if pa.Protocol() != plugins.ProtocolSchemeHTTPV1 { return nil, errors.Errorf("unsupported plugin protocol %s", pa.Protocol()) } addr := pa.Addr() client, err := plugins.NewClientWithTimeout(addr.Network()+"://"+addr.String(), nil, pa.Timeout()) if err != nil { return nil, errors.Wrap(err, "error creating plugin client") } return client, nil } func (a *allocator) call(methodName string, arg any, retVal PluginResponse) error { method := ipamapi.PluginEndpointType + "." + methodName err := a.endpoint.Call(method, arg, retVal) if err != nil { return err } if !retVal.IsSuccess() { return fmt.Errorf("remote: %s", retVal.GetError()) } return nil } func (a *allocator) getCapabilities() (*ipamapi.Capability, error) { var res api.GetCapabilityResponse if err := a.call("GetCapabilities", nil, &res); err != nil { return nil, err } return res.ToCapability(), nil } // GetDefaultAddressSpaces returns the local and global default address spaces func (a *allocator) GetDefaultAddressSpaces() (string, string, error) { res := &api.GetAddressSpacesResponse{} if err := a.call("GetDefaultAddressSpaces", nil, res); err != nil { return "", "", err } return res.LocalDefaultAddressSpace, res.GlobalDefaultAddressSpace, nil } // RequestPool requests an address pool in the specified address space. // // This is a bug-for-bug re-implementation of the logic originally found in // requestPoolHelper prior to v27. See https://github.com/moby/moby/blob/faf84d7f0a1f2e6badff6f720a3e1e559c356fff/libnetwork/network.go#L1518-L1570 func (a *allocator) RequestPool(req ipamapi.PoolRequest) (ipamapi.AllocatedPool, error) { var tmpPoolLeases []string defer func() { // Release all pools we held on to. for _, pID := range tmpPoolLeases { if err := a.ReleasePool(pID); err != nil { log.G(context.TODO()).Warnf("Failed to release overlapping pool") } } }() _, globalSpace, err := a.GetDefaultAddressSpaces() if err != nil { return ipamapi.AllocatedPool{}, err } remoteReq := &api.RequestPoolRequest{ AddressSpace: req.AddressSpace, Pool: req.Pool, SubPool: req.SubPool, Options: req.Options, V6: req.V6, } for { alloc, err := a.requestPool(remoteReq) if err != nil { return alloc, err } // If the network pool was explicitly chosen, the network belongs to // global address space, or it is invalid ("0.0.0.0/0"), then we don't // perform check for overlaps. // // FIXME(thaJeztah): why are we ignoring invalid pools here? // // The "invalid" conditions was added in [libnetwork#1095][1], which // moved code to reduce os-specific dependencies in the ipam package, // but also introduced a types.IsIPNetValid() function, which considers // "0.0.0.0/0" invalid, and added it to the conditions below. // // Unfortunately review does not mention this change, so there's no // context why. Possibly this was done to prevent errors further down // the line (when checking for overlaps), but returning an error here // instead would likely have avoided that as well, so we can only guess. // // [1]: https://github.com/moby/libnetwork/commit/5ca79d6b87873264516323a7b76f0af7d0298492#diff-bdcd879439d041827d334846f9aba01de6e3683ed8fdd01e63917dae6df23846 if req.Pool != "" || req.AddressSpace == globalSpace || alloc.Pool.String() == "0.0.0.0/0" { return alloc, nil } // Check for overlap and if none found, we have found the right pool. if !checkOverlaps(alloc, req.Exclude) { return alloc, nil } // Pool obtained in this iteration is overlapping. Hold onto the pool // and don't release it yet, because we don't want IPAM to give us back // the same pool over again. But make sure we still do a deferred release // when we have either obtained a non-overlapping pool or ran out of // pre-defined pools. tmpPoolLeases = append(tmpPoolLeases, alloc.PoolID) } } func (a *allocator) requestPool(req *api.RequestPoolRequest) (ipamapi.AllocatedPool, error) { res := &api.RequestPoolResponse{} if err := a.call("RequestPool", req, res); err != nil { return ipamapi.AllocatedPool{}, err } retPool, err := netip.ParsePrefix(res.Pool) return ipamapi.AllocatedPool{ PoolID: res.PoolID, Pool: retPool, Meta: res.Data, }, err } // checkOverlaps returns true if the 'pool' overlaps with some prefix in 'reserved'. func checkOverlaps(pool ipamapi.AllocatedPool, reserved []netip.Prefix) bool { for _, r := range reserved { if r.Overlaps(pool.Pool) { return true } } return false } // ReleasePool removes an address pool from the specified address space func (a *allocator) ReleasePool(poolID string) error { req := &api.ReleasePoolRequest{PoolID: poolID} res := &api.ReleasePoolResponse{} return a.call("ReleasePool", req, res) } // RequestAddress requests an address from the address pool func (a *allocator) RequestAddress(poolID string, address net.IP, options map[string]string) (*net.IPNet, map[string]string, error) { var ( prefAddress string retAddress *net.IPNet err error ) if address != nil { prefAddress = address.String() } req := &api.RequestAddressRequest{PoolID: poolID, Address: prefAddress, Options: options} res := &api.RequestAddressResponse{} if err := a.call("RequestAddress", req, res); err != nil { return nil, nil, err } if res.Address != "" { retAddress, err = types.ParseCIDR(res.Address) } else { return nil, nil, ipamapi.ErrNoIPReturned } return retAddress, res.Data, err } // ReleaseAddress releases the address from the specified address pool func (a *allocator) ReleaseAddress(poolID string, address net.IP) error { var relAddress string if address != nil { relAddress = address.String() } req := &api.ReleaseAddressRequest{PoolID: poolID, Address: relAddress} res := &api.ReleaseAddressResponse{} return a.call("ReleaseAddress", req, res) } func (a *allocator) IsBuiltIn() bool { return false }