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 }