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>
178 lines
5.6 KiB
Go
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
|
|
}
|