github.com/datadog/cilium@v1.6.12/pkg/ipam/allocator.go (about)

     1  // Copyright 2016-2020 Authors of Cilium
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ipam
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"net"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/cilium/cilium/pkg/metrics"
    25  	"github.com/cilium/cilium/pkg/uuid"
    26  
    27  	"github.com/sirupsen/logrus"
    28  )
    29  
    30  const (
    31  	metricAllocate = "allocate"
    32  	metricRelease  = "release"
    33  	familyIPv4     = "ipv4"
    34  	familyIPv6     = "ipv6"
    35  )
    36  
    37  type owner string
    38  
    39  // Error definitions
    40  var (
    41  	// ErrIPv4Disabled is returned when IPv4 allocation is disabled
    42  	ErrIPv4Disabled = errors.New("IPv4 allocation disabled")
    43  
    44  	// ErrIPv6Disabled is returned when Ipv6 allocation is disabled
    45  	ErrIPv6Disabled = errors.New("IPv6 allocation disabled")
    46  )
    47  
    48  func (ipam *IPAM) lookupIPsByOwner(owner string) (ips []net.IP) {
    49  	ipam.allocatorMutex.RLock()
    50  	defer ipam.allocatorMutex.RUnlock()
    51  
    52  	for ip, o := range ipam.owner {
    53  		if o == owner {
    54  			if parsedIP := net.ParseIP(ip); parsedIP != nil {
    55  				ips = append(ips, parsedIP)
    56  			}
    57  		}
    58  	}
    59  
    60  	return
    61  }
    62  
    63  // AllocateIP allocates a IP address.
    64  func (ipam *IPAM) AllocateIP(ip net.IP, owner string) (err error) {
    65  	ipam.allocatorMutex.Lock()
    66  	defer ipam.allocatorMutex.Unlock()
    67  
    68  	if ipam.blacklist.Contains(ip) {
    69  		err = fmt.Errorf("IP %s is blacklisted, owned by %s", ip.String(), owner)
    70  		return
    71  	}
    72  
    73  	family := familyIPv4
    74  	if ip.To4() != nil {
    75  		if ipam.IPv4Allocator == nil {
    76  			err = ErrIPv4Disabled
    77  			return
    78  		}
    79  
    80  		if _, err = ipam.IPv4Allocator.Allocate(ip, owner); err != nil {
    81  			return
    82  		}
    83  	} else {
    84  		family = familyIPv6
    85  		if ipam.IPv6Allocator == nil {
    86  			err = ErrIPv6Disabled
    87  			return
    88  		}
    89  
    90  		if _, err = ipam.IPv6Allocator.Allocate(ip, owner); err != nil {
    91  			return
    92  		}
    93  	}
    94  
    95  	log.WithFields(logrus.Fields{
    96  		"ip":    ip.String(),
    97  		"owner": owner,
    98  	}).Debugf("Allocated specific IP")
    99  
   100  	ipam.owner[ip.String()] = owner
   101  	metrics.IpamEvent.WithLabelValues(metricAllocate, family).Inc()
   102  	return
   103  }
   104  
   105  // AllocateIPString is identical to AllocateIP but takes a string
   106  func (ipam *IPAM) AllocateIPString(ipAddr, owner string) error {
   107  	ip := net.ParseIP(ipAddr)
   108  	if ip == nil {
   109  		return fmt.Errorf("Invalid IP address: %s", ipAddr)
   110  	}
   111  
   112  	return ipam.AllocateIP(ip, owner)
   113  }
   114  
   115  func (ipam *IPAM) allocateNextFamily(family Family, allocator Allocator, owner string) (result *AllocationResult, err error) {
   116  	if allocator == nil {
   117  		err = fmt.Errorf("%s allocator not available", family)
   118  		return
   119  	}
   120  
   121  	for {
   122  		result, err = allocator.AllocateNext(owner)
   123  		if err != nil {
   124  			return
   125  		}
   126  
   127  		if !ipam.blacklist.Contains(result.IP) {
   128  			log.WithFields(logrus.Fields{
   129  				"ip":    result.IP.String(),
   130  				"owner": owner,
   131  			}).Debugf("Allocated random IP")
   132  			ipam.owner[result.IP.String()] = owner
   133  			metrics.IpamEvent.WithLabelValues(metricAllocate, string(family)).Inc()
   134  			return
   135  		}
   136  
   137  		// The allocated IP is blacklisted, do not use it. The
   138  		// blacklisted IP is now allocated so it won't be allocated in
   139  		// the next iteration.
   140  		ipam.owner[result.IP.String()] = fmt.Sprintf("%s (blacklisted)", owner)
   141  	}
   142  }
   143  
   144  // AllocateNextFamily allocates the next IP of the requested address family
   145  func (ipam *IPAM) AllocateNextFamily(family Family, owner string) (result *AllocationResult, err error) {
   146  	ipam.allocatorMutex.Lock()
   147  	defer ipam.allocatorMutex.Unlock()
   148  
   149  	switch family {
   150  	case IPv6:
   151  		result, err = ipam.allocateNextFamily(family, ipam.IPv6Allocator, owner)
   152  	case IPv4:
   153  		result, err = ipam.allocateNextFamily(family, ipam.IPv4Allocator, owner)
   154  
   155  	default:
   156  		err = fmt.Errorf("unknown address \"%s\" family requested", family)
   157  	}
   158  	return
   159  }
   160  
   161  // AllocateNext allocates the next available IPv4 and IPv6 address out of the
   162  // configured address pool. If family is set to "ipv4" or "ipv6", then
   163  // allocation is limited to the specified address family. If the pool has been
   164  // drained of addresses, an error will be returned.
   165  func (ipam *IPAM) AllocateNext(family, owner string) (ipv4Result, ipv6Result *AllocationResult, err error) {
   166  	if (family == "ipv6" || family == "") && ipam.IPv6Allocator != nil {
   167  		ipv6Result, err = ipam.AllocateNextFamily(IPv6, owner)
   168  		if err != nil {
   169  			return
   170  		}
   171  
   172  	}
   173  
   174  	if (family == "ipv4" || family == "") && ipam.IPv4Allocator != nil {
   175  		ipv4Result, err = ipam.AllocateNextFamily(IPv4, owner)
   176  		if err != nil {
   177  			if ipv6Result != nil {
   178  				ipam.ReleaseIP(ipv6Result.IP)
   179  			}
   180  			return
   181  		}
   182  	}
   183  
   184  	return
   185  }
   186  
   187  // AllocateNextWithExpiration is identical to AllocateNext but registers an
   188  // expiration timer as well. This is identical to using AllocateNext() in
   189  // combination with StartExpirationTimer()
   190  func (ipam *IPAM) AllocateNextWithExpiration(family, owner string, timeout time.Duration) (ipv4Result, ipv6Result *AllocationResult, err error) {
   191  	ipv4Result, ipv6Result, err = ipam.AllocateNext(family, owner)
   192  	if err != nil {
   193  		return nil, nil, err
   194  	}
   195  
   196  	if timeout != time.Duration(0) {
   197  		for _, result := range []*AllocationResult{ipv4Result, ipv6Result} {
   198  			if result != nil {
   199  				result.ExpirationUUID, err = ipam.StartExpirationTimer(result.IP, timeout)
   200  				if err != nil {
   201  					if ipv4Result != nil {
   202  						ipam.ReleaseIP(ipv4Result.IP)
   203  					}
   204  					if ipv6Result != nil {
   205  						ipam.ReleaseIP(ipv6Result.IP)
   206  					}
   207  					return
   208  				}
   209  			}
   210  		}
   211  	}
   212  
   213  	return
   214  }
   215  
   216  func (ipam *IPAM) releaseIPLocked(ip net.IP) error {
   217  	family := familyIPv4
   218  	if ip.To4() != nil {
   219  		if ipam.IPv4Allocator == nil {
   220  			return ErrIPv4Disabled
   221  		}
   222  
   223  		if err := ipam.IPv4Allocator.Release(ip); err != nil {
   224  			return err
   225  		}
   226  	} else {
   227  		family = familyIPv6
   228  		if ipam.IPv6Allocator == nil {
   229  			return ErrIPv6Disabled
   230  		}
   231  
   232  		if err := ipam.IPv6Allocator.Release(ip); err != nil {
   233  			return err
   234  		}
   235  	}
   236  
   237  	owner := ipam.owner[ip.String()]
   238  	log.WithFields(logrus.Fields{
   239  		"ip":    ip.String(),
   240  		"owner": owner,
   241  	}).Debugf("Released IP")
   242  	delete(ipam.owner, ip.String())
   243  	delete(ipam.expirationTimers, ip.String())
   244  
   245  	metrics.IpamEvent.WithLabelValues(metricRelease, family).Inc()
   246  	return nil
   247  }
   248  
   249  // ReleaseIP release a IP address.
   250  func (ipam *IPAM) ReleaseIP(ip net.IP) error {
   251  	ipam.allocatorMutex.Lock()
   252  	defer ipam.allocatorMutex.Unlock()
   253  	return ipam.releaseIPLocked(ip)
   254  }
   255  
   256  // ReleaseIPString is identical to ReleaseIP but takes a string and supports
   257  // referring to the IPs to be released with the IP itself or the owner name
   258  // used during allocation. If the owner can be referred to multiple IPs, then
   259  // all IPs are being released.
   260  func (ipam *IPAM) ReleaseIPString(releaseArg string) (err error) {
   261  	var ips []net.IP
   262  
   263  	ip := net.ParseIP(releaseArg)
   264  	if ip == nil {
   265  		ips = ipam.lookupIPsByOwner(releaseArg)
   266  		if len(ips) == 0 {
   267  			return fmt.Errorf("Invalid IP address or owner name: %s", releaseArg)
   268  		}
   269  	} else {
   270  		ips = append(ips, ip)
   271  	}
   272  
   273  	for _, parsedIP := range ips {
   274  		// If any of the releases fail, report the failure
   275  		if err2 := ipam.ReleaseIP(parsedIP); err2 != nil {
   276  			err = err2
   277  		}
   278  	}
   279  	return
   280  }
   281  
   282  // Dump dumps the list of allocated IP addresses
   283  func (ipam *IPAM) Dump() (allocv4 map[string]string, allocv6 map[string]string, status string) {
   284  	var st4, st6 string
   285  
   286  	ipam.allocatorMutex.RLock()
   287  	defer ipam.allocatorMutex.RUnlock()
   288  
   289  	if ipam.IPv4Allocator != nil {
   290  		allocv4, st4 = ipam.IPv4Allocator.Dump()
   291  		st4 = "IPv4: " + st4
   292  		for ip := range allocv4 {
   293  			owner, _ := ipam.owner[ip]
   294  			// If owner is not available, report IP but leave owner empty
   295  			allocv4[ip] = owner
   296  		}
   297  	}
   298  
   299  	if ipam.IPv6Allocator != nil {
   300  		allocv6, st6 = ipam.IPv6Allocator.Dump()
   301  		st6 = "IPv6: " + st6
   302  		for ip := range allocv6 {
   303  			owner, _ := ipam.owner[ip]
   304  			// If owner is not available, report IP but leave owner empty
   305  			allocv6[ip] = owner
   306  		}
   307  	}
   308  
   309  	status = strings.Join([]string{st4, st6}, ", ")
   310  	if status == "" {
   311  		status = "Not running"
   312  	}
   313  
   314  	return
   315  }
   316  
   317  // StartExpirationTimer installs an expiration timer for a previously allocated
   318  // IP. Unless StopExpirationTimer is called in time, the IP will be released
   319  // again after expiration of the specified timeout. The function will return a
   320  // UUID representing the unique allocation attempt. The same UUID must be
   321  // passed into StopExpirationTimer again.
   322  //
   323  // This function is to be used as allocation and use of an IP can be controlled
   324  // by an external entity and that external entity can disappear. Therefore such
   325  // users should register an expiration timer before returning the IP and then
   326  // stop the expiration timer when the IP has been used.
   327  func (ipam *IPAM) StartExpirationTimer(ip net.IP, timeout time.Duration) (string, error) {
   328  	ipam.allocatorMutex.Lock()
   329  	defer ipam.allocatorMutex.Unlock()
   330  
   331  	ipString := ip.String()
   332  	if _, ok := ipam.expirationTimers[ipString]; ok {
   333  		return "", fmt.Errorf("expiration timer already registered")
   334  	}
   335  
   336  	allocationUUID := uuid.NewUUID().String()
   337  	ipam.expirationTimers[ipString] = allocationUUID
   338  
   339  	go func(ip net.IP, allocationUUID string, timeout time.Duration) {
   340  		ipString := ip.String()
   341  		time.Sleep(timeout)
   342  
   343  		ipam.allocatorMutex.Lock()
   344  		defer ipam.allocatorMutex.Unlock()
   345  
   346  		if currentUUID, ok := ipam.expirationTimers[ipString]; ok {
   347  			if currentUUID == allocationUUID {
   348  				scopedLog := log.WithFields(logrus.Fields{"ip": ipString, "uuid": allocationUUID})
   349  				if err := ipam.releaseIPLocked(ip); err != nil {
   350  					scopedLog.WithError(err).Warning("Unable to release IP after expiration")
   351  				} else {
   352  					scopedLog.Warning("Released IP after expiration")
   353  				}
   354  			} else {
   355  				// This is an obsolete expiration timer. The IP
   356  				// was reused and a new expiration timer is
   357  				// already attached
   358  			}
   359  		} else {
   360  			// Expiration timer was removed. No action is required
   361  		}
   362  	}(ip, allocationUUID, timeout)
   363  
   364  	return allocationUUID, nil
   365  }
   366  
   367  // StopExpirationTimer will remove the expiration timer for a particular IP.
   368  // The UUID returned by the symmetric StartExpirationTimer must be provided.
   369  // The expiration timer will only be removed if the UUIDs match. Releasing an
   370  // IP will also stop the expiration timer.
   371  func (ipam *IPAM) StopExpirationTimer(ip net.IP, allocationUUID string) error {
   372  	ipam.allocatorMutex.Lock()
   373  	defer ipam.allocatorMutex.Unlock()
   374  
   375  	ipString := ip.String()
   376  	if currentUUID, ok := ipam.expirationTimers[ipString]; !ok {
   377  		return fmt.Errorf("no expiration timer registered")
   378  	} else if currentUUID != allocationUUID {
   379  		return fmt.Errorf("UUID mismatch, not stopping expiration timer")
   380  	}
   381  
   382  	delete(ipam.expirationTimers, ipString)
   383  
   384  	return nil
   385  }