// SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Cilium package ipam import ( "errors" "fmt" "net" "strings" "time" "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/cilium/cilium/pkg/metrics" ) const ( metricAllocate = "allocate" metricRelease = "release" familyIPv4 = "ipv4" familyIPv6 = "ipv6" ) // Error definitions var ( // ErrIPv4Disabled is returned when IPv4 allocation is disabled ErrIPv4Disabled = errors.New("IPv4 allocation disabled") // ErrIPv6Disabled is returned when Ipv6 allocation is disabled ErrIPv6Disabled = errors.New("IPv6 allocation disabled") ) func (ipam *IPAM) lookupIPsByOwner(owner string) (ips []net.IP) { ipam.allocatorMutex.RLock() defer ipam.allocatorMutex.RUnlock() for ip, o := range ipam.owner { if o == owner { if parsedIP := net.ParseIP(ip); parsedIP != nil { ips = append(ips, parsedIP) } } } return } // AllocateIP allocates a IP address. func (ipam *IPAM) AllocateIP(ip net.IP, owner string) (err error) { needSyncUpstream := true _, err = ipam.allocateIP(ip, owner, needSyncUpstream) return } // AllocateIPWithAllocationResult allocates an IP address, and returns the // allocation result. func (ipam *IPAM) AllocateIPWithAllocationResult(ip net.IP, owner string) (result *AllocationResult, err error) { needSyncUpstream := true return ipam.allocateIP(ip, owner, needSyncUpstream) } // AllocateIPWithoutSyncUpstream allocates a IP address without syncing upstream. func (ipam *IPAM) AllocateIPWithoutSyncUpstream(ip net.IP, owner string) (result *AllocationResult, err error) { needSyncUpstream := false return ipam.allocateIP(ip, owner, needSyncUpstream) } // AllocateIPString is identical to AllocateIP but takes a string func (ipam *IPAM) AllocateIPString(ipAddr, owner string) error { ip := net.ParseIP(ipAddr) if ip == nil { return fmt.Errorf("Invalid IP address: %s", ipAddr) } return ipam.AllocateIP(ip, owner) } func (ipam *IPAM) allocateIP(ip net.IP, owner string, needSyncUpstream bool) (result *AllocationResult, err error) { ipam.allocatorMutex.Lock() defer ipam.allocatorMutex.Unlock() if ipam.blacklist.Contains(ip) { err = fmt.Errorf("IP %s is blacklisted, owned by %s", ip.String(), owner) return } family := familyIPv4 if ip.To4() != nil { if ipam.IPv4Allocator == nil { err = ErrIPv4Disabled return } if needSyncUpstream { if result, err = ipam.IPv4Allocator.Allocate(ip, owner); err != nil { return } } else { if result, err = ipam.IPv4Allocator.AllocateWithoutSyncUpstream(ip, owner); err != nil { return } } } else { family = familyIPv6 if ipam.IPv6Allocator == nil { err = ErrIPv6Disabled return } if needSyncUpstream { if _, err = ipam.IPv6Allocator.Allocate(ip, owner); err != nil { return } } else { if _, err = ipam.IPv6Allocator.AllocateWithoutSyncUpstream(ip, owner); err != nil { return } } } log.WithFields(logrus.Fields{ "ip": ip.String(), "owner": owner, }).Debugf("Allocated specific IP") ipam.owner[ip.String()] = owner metrics.IpamEvent.WithLabelValues(metricAllocate, family).Inc() return } func (ipam *IPAM) allocateNextFamily(family Family, owner string, needSyncUpstream bool) (result *AllocationResult, err error) { var allocator Allocator switch family { case IPv6: allocator = ipam.IPv6Allocator case IPv4: allocator = ipam.IPv4Allocator default: err = fmt.Errorf("unknown address \"%s\" family requested", family) return } if allocator == nil { err = fmt.Errorf("%s allocator not available", family) return } for { if needSyncUpstream { result, err = allocator.AllocateNext(owner) } else { result, err = allocator.AllocateNextWithoutSyncUpstream(owner) } if err != nil { return } if !ipam.blacklist.Contains(result.IP) { log.WithFields(logrus.Fields{ "ip": result.IP.String(), "owner": owner, }).Debugf("Allocated random IP") ipam.owner[result.IP.String()] = owner metrics.IpamEvent.WithLabelValues(metricAllocate, string(family)).Inc() return } // The allocated IP is blacklisted, do not use it. The // blacklisted IP is now allocated so it won't be allocated in // the next iteration. ipam.owner[result.IP.String()] = fmt.Sprintf("%s (blacklisted)", owner) } } // AllocateNextFamily allocates the next IP of the requested address family func (ipam *IPAM) AllocateNextFamily(family Family, owner string) (result *AllocationResult, err error) { ipam.allocatorMutex.Lock() defer ipam.allocatorMutex.Unlock() needSyncUpstream := true return ipam.allocateNextFamily(family, owner, needSyncUpstream) } // AllocateNextFamilyWithoutSyncUpstream allocates the next IP of the requested address family // without syncing upstream func (ipam *IPAM) AllocateNextFamilyWithoutSyncUpstream(family Family, owner string) (result *AllocationResult, err error) { ipam.allocatorMutex.Lock() defer ipam.allocatorMutex.Unlock() needSyncUpstream := false return ipam.allocateNextFamily(family, owner, needSyncUpstream) } // AllocateNext allocates the next available IPv4 and IPv6 address out of the // configured address pool. If family is set to "ipv4" or "ipv6", then // allocation is limited to the specified address family. If the pool has been // drained of addresses, an error will be returned. func (ipam *IPAM) AllocateNext(family, owner string) (ipv4Result, ipv6Result *AllocationResult, err error) { if (family == "ipv6" || family == "") && ipam.IPv6Allocator != nil { ipv6Result, err = ipam.AllocateNextFamily(IPv6, owner) if err != nil { return } } if (family == "ipv4" || family == "") && ipam.IPv4Allocator != nil { ipv4Result, err = ipam.AllocateNextFamily(IPv4, owner) if err != nil { if ipv6Result != nil { ipam.ReleaseIP(ipv6Result.IP) } return } } return } // AllocateNextWithExpiration is identical to AllocateNext but registers an // expiration timer as well. This is identical to using AllocateNext() in // combination with StartExpirationTimer() func (ipam *IPAM) AllocateNextWithExpiration(family, owner string, timeout time.Duration) (ipv4Result, ipv6Result *AllocationResult, err error) { ipv4Result, ipv6Result, err = ipam.AllocateNext(family, owner) if err != nil { return nil, nil, err } if timeout != time.Duration(0) { for _, result := range []*AllocationResult{ipv4Result, ipv6Result} { if result != nil { result.ExpirationUUID, err = ipam.StartExpirationTimer(result.IP, timeout) if err != nil { if ipv4Result != nil { ipam.ReleaseIP(ipv4Result.IP) } if ipv6Result != nil { ipam.ReleaseIP(ipv6Result.IP) } return } } } } return } func (ipam *IPAM) releaseIPLocked(ip net.IP) error { family := familyIPv4 if ip.To4() != nil { if ipam.IPv4Allocator == nil { return ErrIPv4Disabled } if err := ipam.IPv4Allocator.Release(ip); err != nil { return err } } else { family = familyIPv6 if ipam.IPv6Allocator == nil { return ErrIPv6Disabled } if err := ipam.IPv6Allocator.Release(ip); err != nil { return err } } owner := ipam.owner[ip.String()] log.WithFields(logrus.Fields{ "ip": ip.String(), "owner": owner, }).Debugf("Released IP") delete(ipam.owner, ip.String()) delete(ipam.expirationTimers, ip.String()) metrics.IpamEvent.WithLabelValues(metricRelease, family).Inc() return nil } // ReleaseIP release a IP address. func (ipam *IPAM) ReleaseIP(ip net.IP) error { ipam.allocatorMutex.Lock() defer ipam.allocatorMutex.Unlock() return ipam.releaseIPLocked(ip) } // ReleaseIPString is identical to ReleaseIP but takes a string and supports // referring to the IPs to be released with the IP itself or the owner name // used during allocation. If the owner can be referred to multiple IPs, then // all IPs are being released. func (ipam *IPAM) ReleaseIPString(releaseArg string) (err error) { var ips []net.IP ip := net.ParseIP(releaseArg) if ip == nil { ips = ipam.lookupIPsByOwner(releaseArg) if len(ips) == 0 { return fmt.Errorf("Invalid IP address or owner name: %s", releaseArg) } } else { ips = append(ips, ip) } for _, parsedIP := range ips { // If any of the releases fail, report the failure if err2 := ipam.ReleaseIP(parsedIP); err2 != nil { err = err2 } } return } // Dump dumps the list of allocated IP addresses func (ipam *IPAM) Dump() (allocv4 map[string]string, allocv6 map[string]string, status string) { var st4, st6 string ipam.allocatorMutex.RLock() defer ipam.allocatorMutex.RUnlock() if ipam.IPv4Allocator != nil { allocv4, st4 = ipam.IPv4Allocator.Dump() st4 = "IPv4: " + st4 for ip := range allocv4 { owner, _ := ipam.owner[ip] // If owner is not available, report IP but leave owner empty allocv4[ip] = owner } } if ipam.IPv6Allocator != nil { allocv6, st6 = ipam.IPv6Allocator.Dump() st6 = "IPv6: " + st6 for ip := range allocv6 { owner, _ := ipam.owner[ip] // If owner is not available, report IP but leave owner empty allocv6[ip] = owner } } status = strings.Join([]string{st4, st6}, ", ") if status == "" { status = "Not running" } return } // StartExpirationTimer installs an expiration timer for a previously allocated // IP. Unless StopExpirationTimer is called in time, the IP will be released // again after expiration of the specified timeout. The function will return a // UUID representing the unique allocation attempt. The same UUID must be // passed into StopExpirationTimer again. // // This function is to be used as allocation and use of an IP can be controlled // by an external entity and that external entity can disappear. Therefore such // users should register an expiration timer before returning the IP and then // stop the expiration timer when the IP has been used. func (ipam *IPAM) StartExpirationTimer(ip net.IP, timeout time.Duration) (string, error) { ipam.allocatorMutex.Lock() defer ipam.allocatorMutex.Unlock() ipString := ip.String() if _, ok := ipam.expirationTimers[ipString]; ok { return "", fmt.Errorf("expiration timer already registered") } allocationUUID := uuid.New().String() ipam.expirationTimers[ipString] = allocationUUID go func(ip net.IP, allocationUUID string, timeout time.Duration) { ipString := ip.String() time.Sleep(timeout) ipam.allocatorMutex.Lock() defer ipam.allocatorMutex.Unlock() if currentUUID, ok := ipam.expirationTimers[ipString]; ok { if currentUUID == allocationUUID { scopedLog := log.WithFields(logrus.Fields{"ip": ipString, "uuid": allocationUUID}) if err := ipam.releaseIPLocked(ip); err != nil { scopedLog.WithError(err).Warning("Unable to release IP after expiration") } else { scopedLog.Warning("Released IP after expiration") } } else { // This is an obsolete expiration timer. The IP // was reused and a new expiration timer is // already attached } } else { // Expiration timer was removed. No action is required } }(ip, allocationUUID, timeout) return allocationUUID, nil } // StopExpirationTimer will remove the expiration timer for a particular IP. // The UUID returned by the symmetric StartExpirationTimer must be provided. // The expiration timer will only be removed if the UUIDs match. Releasing an // IP will also stop the expiration timer. func (ipam *IPAM) StopExpirationTimer(ip net.IP, allocationUUID string) error { ipam.allocatorMutex.Lock() defer ipam.allocatorMutex.Unlock() ipString := ip.String() if currentUUID, ok := ipam.expirationTimers[ipString]; !ok { return fmt.Errorf("no expiration timer registered") } else if currentUUID != allocationUUID { return fmt.Errorf("UUID mismatch, not stopping expiration timer") } delete(ipam.expirationTimers, ipString) return nil }