Files
tailscale/feature/conn25/ippool.go
T
Michael Ben-Amiandmzbenami b2de420e3d feature/conn25,types/appctype: serve active Conn25 state over localapi
At /v0/conn25-state.

State includes whether the node is configured for Connectors 2025, as
well as client-specific and connector-specific state, if the node is
acting in those contexts.

Client-specific state includes the reserved Magic IPs and Transit IPs on
the client that have not been returned to their IP pools, and their
associated apps, domains, real destination IPs, and active flow counts.

We also report IP pool utilization: the number of magic and transit IPs
in use versus each pool's capacity, split by IP family.

Connector-specific state includes a peer list of clients that have
registered Transit IPs with the connector, and the apps are real
destination IPs the Transit IPs map to.

Updates tailscale/corp#40125

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-07-16 16:35:38 -04:00

178 lines
5.6 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package conn25
import (
"errors"
"math"
"net/netip"
"go4.org/netipx"
"tailscale.com/util/set"
)
// errPoolExhausted is returned when there are no more addresses to iterate over.
var errPoolExhausted = errors.New("ip pool exhausted")
// errNotOurAddress is returned if a provided address is not from our pool
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
// all the addresses in the set by keeping a track of the last address we returned, calling Next on the last address
// to get the new one, and if we run off the edge of the current range, starting on the next one, or back at the beginning.
type ipSetIterator struct {
// ranges defines the addresses in the pool
ranges []netipx.IPRange
// last is internal tracking of which the last address provided was.
last netip.Addr
// rangeIdx is internal tracking of which netipx.IPRange from the IPSet we are currently on.
rangeIdx int
}
// next returns the next address from the set.
func (ipsi *ipSetIterator) next() (netip.Addr, error) {
if len(ipsi.ranges) == 0 {
// ipset is empty
return netip.Addr{}, errPoolExhausted
}
if !ipsi.last.IsValid() {
// not initialized yet
ipsi.last = ipsi.ranges[0].From()
return ipsi.last, nil
}
currRange := ipsi.ranges[ipsi.rangeIdx]
if ipsi.last == currRange.To() {
// then we need to move to the next range
ipsi.rangeIdx++
if ipsi.rangeIdx >= len(ipsi.ranges) {
// back to the beginning
ipsi.rangeIdx = 0
}
ipsi.last = ipsi.ranges[ipsi.rangeIdx].From()
return ipsi.last, nil
}
ipsi.last = ipsi.last.Next()
return ipsi.last, nil
}
func newIPPool(ipset *netipx.IPSet) *ippool {
if ipset == nil {
return &ippool{}
}
return &ippool{
ipSet: ipset,
ipSetIterator: &ipSetIterator{ranges: ipset.Ranges()},
inUse: &set.Set[netip.Addr]{},
}
}
type ippool struct {
// 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 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
}
startedAt := a
for ipp.inUse.Contains(a) {
a, err = ipp.ipSetIterator.next()
if err != nil {
return a, err
}
if a == startedAt {
return netip.Addr{}, errPoolExhausted
}
}
ipp.inUse.Add(a)
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
}
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
}
// inUseCount returns the number of addresses currently handed out from the
// pool. It is safe to call on a nil or uninitialized pool, returning 0.
func (ipp *ippool) inUseCount() int64 {
if ipp == nil || ipp.inUse == nil {
return 0
}
return int64(ipp.inUse.Len())
}
// capacity returns the total number of addresses defined by the pool's ipSet.
// It is safe to call on a nil or uninitialized pool, returning 0. The count is
// clamped to [math.MaxInt64] because an IP pool (particularly IPv6) can define
// far more addresses than fit in an int64.
func (ipp *ippool) capacity() int64 {
if ipp == nil || ipp.ipSet == nil {
return 0
}
var count int64
for _, pfx := range ipp.ipSet.Prefixes() {
bits := pfx.Addr().BitLen() - pfx.Bits()
// 1<<bits can exceed an int64 (e.g. a /64 IPv6 pool), so clamp.
if bits >= 63 {
return math.MaxInt64
}
addend := int64(1) << bits
if count > math.MaxInt64-addend {
// Summing multiple large prefixes would overflow.
return math.MaxInt64
}
count += addend
}
return count
}