feature/conn25: keep addrAssignments through pool reconfig

Fixes tailscale/corp#40250

Signed-off-by: Fran Bull <fran@tailscale.com>
This commit is contained in:
Fran Bull
2026-05-11 14:58:08 -07:00
committed by franbull
parent e4e59a2af0
commit 3a6261b79b
4 changed files with 193 additions and 38 deletions
+41 -7
View File
@@ -20,6 +20,9 @@ var errNotOurAddress = errors.New("not our address")
// errAddrExists is returned if a returned address is already in the returned pool.
var errAddrExists = errors.New("address already returned")
// errUninitializedIPPool is returned if the pool is used when it's not initialized
var errUninitializedIPPool = errors.New("uninitialized ippool")
// ipSetIterator allows for round robin iteration over all the addresses within a netipx.IPSet.
// netipx.IPSet has a Ranges call that returns the "minimum and sorted set of IP ranges that covers [the set]".
// netipx.IPRange is "an inclusive range of IP addresses from the same address family.". So we can iterate over
@@ -72,12 +75,23 @@ func newIPPool(ipset *netipx.IPSet) *ippool {
}
type ippool struct {
ipSet *netipx.IPSet
// ipSet defines the addresses within the ippool, it is configured by the user.
ipSet *netipx.IPSet
// ipSetIterator keeps track of iteration through the ippool.
ipSetIterator *ipSetIterator
inUse *set.Set[netip.Addr]
// inUse is a set of addresses that have been handed out and not yet returned.
// Addresses in inUse won't be returned from next.
// Addresses in inUse may no longer be in the ipSet definition of the pool bounds
// if the ippool has been reconfigured.
inUse *set.Set[netip.Addr]
}
// next returns the next available address from within the ippool.
// next will return errPoolExhausted if there are no more unused addresses.
func (ipp *ippool) next() (netip.Addr, error) {
if ipp == nil || ipp.ipSetIterator == nil {
return netip.Addr{}, errUninitializedIPPool
}
a, err := ipp.ipSetIterator.next()
if err != nil {
return netip.Addr{}, err
@@ -96,13 +110,33 @@ func (ipp *ippool) next() (netip.Addr, error) {
return a, nil
}
// returnAddr puts an address back into the ippool, that address will
// now be available to be handed out when we iterate back around to it.
// returnAddr will return an error if the provided address is not one
// that's currently in inUse.
func (ipp *ippool) returnAddr(a netip.Addr) error {
if ipp.inUse.Contains(a) {
ipp.inUse.Delete(a)
return nil
}
if !ipp.ipSet.Contains(a) {
return errNotOurAddress
}
if !ipp.inUse.Contains(a) {
return errAddrExists
}
ipp.inUse.Delete(a)
return nil
return errAddrExists
}
// reconfig changes the definition of the addresses that are in the ippool
// while keeping track of the addresses that are currently in inUse.
func (ipp *ippool) reconfig(ipSet *netipx.IPSet) *ippool {
if ipp != nil && ipSet != nil && ipSet.Equal(ipp.ipSet) {
// in the common case that the definition has not changed, do nothing.
return ipp
}
newPool := newIPPool(ipSet)
if ipp != nil {
// even if the definition of which addresses are in the pool has changed
// we don't want to lose track of which addresses are currently in use
newPool.inUse = ipp.inUse
}
return newPool
}