Conn25 hands out dummy IP addresses for use in the connector flow from limited address pools. When the addresses are no longer in use we expire the corresponding entry from our table of address mappings and return the addresses to their pools for reuse. We currently expire addresses after the DNS TTL for the DNS response that caused the mappings to be created. Stop expiring mappings when there are active packet flows for the addresses in the mappings. Fixes tailscale/corp#43180 Co-authored-by: Fran Bull <fran@tailscale.com> Co-authored-by: Michael Ben-Ami <mzb@tailscale.com> Signed-off-by: Fran Bull <fran@tailscale.com> Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
189 lines
5.7 KiB
Go
189 lines
5.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package conn25
|
|
|
|
import (
|
|
"container/heap"
|
|
"errors"
|
|
"net/netip"
|
|
"time"
|
|
|
|
"tailscale.com/tstime"
|
|
"tailscale.com/util/dnsname"
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
// domainDst is a key for looking up an existing address assignment by the
|
|
// DNS response domain and destination IP pair.
|
|
type domainDst struct {
|
|
domain dnsname.FQDN
|
|
dst netip.Addr
|
|
}
|
|
|
|
// addrAssignments is the collection of addrs assigned by this client
|
|
// supporting lookup by magic IP, transit IP or domain+dst, or to lookup all
|
|
// transit IPs associated with a given connector (identified by its node key).
|
|
type addrAssignments struct {
|
|
byMagicIP map[netip.Addr]*addrs
|
|
byTransitIP map[netip.Addr]*addrs
|
|
byDomainDst map[domainDst]*addrs
|
|
byExpiresAt addrsHeap
|
|
clock tstime.Clock
|
|
}
|
|
|
|
const defaultExpiry = 48 * time.Hour
|
|
|
|
func clampExpiryFromTTL(ttl time.Duration) time.Duration {
|
|
const minTTL = time.Minute * 1
|
|
const maxTTL = time.Hour * 72
|
|
expiry := max(minTTL, ttl)
|
|
return min(maxTTL, expiry)
|
|
}
|
|
|
|
func (a *addrAssignments) insert(as *addrs) error {
|
|
return a.insertWithExpiry(as, defaultExpiry)
|
|
}
|
|
|
|
func (a *addrAssignments) insertFromTTL(as *addrs, ttl time.Duration) error {
|
|
return a.insertWithExpiry(as, clampExpiryFromTTL(ttl))
|
|
}
|
|
|
|
func (a *addrAssignments) updateFromTTL(as *addrs, ttl time.Duration) {
|
|
a.updateExpiry(as, clampExpiryFromTTL(ttl))
|
|
}
|
|
|
|
func (a *addrAssignments) insertWithExpiry(as *addrs, d time.Duration) error {
|
|
now := a.clock.Now()
|
|
if !as.expiresAt.IsZero() && !as.expiresAt.Before(now) {
|
|
return errors.New("expiresAt already set")
|
|
}
|
|
// addresses must be removed (eg by popExpired) before they can be reused
|
|
if _, ok := a.byMagicIP[as.magic]; ok {
|
|
return errors.New("byMagicIP key exists")
|
|
}
|
|
ddst := domainDst{domain: as.domain, dst: as.dst}
|
|
if _, ok := a.byDomainDst[ddst]; ok {
|
|
return errors.New("byDomainDst key exists")
|
|
}
|
|
if _, ok := a.byTransitIP[as.transit]; ok {
|
|
return errors.New("byTransitIP key exists")
|
|
}
|
|
as.expiresAt = now.Add(d)
|
|
mak.Set(&a.byMagicIP, as.magic, as)
|
|
mak.Set(&a.byTransitIP, as.transit, as)
|
|
mak.Set(&a.byDomainDst, ddst, as)
|
|
heap.Push(&a.byExpiresAt, as)
|
|
return nil
|
|
}
|
|
|
|
func (a *addrAssignments) lookupByDomainDst(domain dnsname.FQDN, dst netip.Addr) (*addrs, bool) {
|
|
v, ok := a.byDomainDst[domainDst{domain: domain, dst: dst}]
|
|
if !ok {
|
|
return &addrs{}, false
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
func (a *addrAssignments) lookupByMagicIP(mip netip.Addr) (*addrs, bool) {
|
|
v, ok := a.byMagicIP[mip]
|
|
if !ok {
|
|
return &addrs{}, false
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
func (a *addrAssignments) lookupByTransitIP(tip netip.Addr) (*addrs, bool) {
|
|
v, ok := a.byTransitIP[tip]
|
|
if !ok {
|
|
return &addrs{}, false
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
const (
|
|
// deadFlowWaitTimeout is the minimum time after the active flow count
|
|
// drops to zero that we keep an address mapping in our table of address
|
|
// mappings.
|
|
deadFlowWaitTimeout = 2 * time.Minute
|
|
|
|
// extendForActiveFlowDuration is the minimum time we will wait to recheck
|
|
// an address mapping with a positive active flow count for removal from
|
|
// the table of address mappings.
|
|
extendForActiveFlowDuration = 24 * time.Hour
|
|
)
|
|
|
|
// popExpired attempts to remove from all the indexes one address
|
|
// mapping, and return that mapping, or nil if there were no eligible mappings.
|
|
// An address mapping is eligible for removal if:
|
|
// - the current time is past the expiresAt time on the mapping
|
|
// - and, the active flow count is 0
|
|
// - and, it's been long enough since the active flow count dropped to 0
|
|
// We're using a heap on expiresAt to efficiently find addresses that
|
|
// are eligible for removal. expiresAt is initially set according to the
|
|
// TTL on the DNS response. If the current time is past the expiresAt, but
|
|
// there are active flows, we extend the expiresAt time into the future.
|
|
func (a *addrAssignments) popExpired(now time.Time) *addrs {
|
|
if a.byExpiresAt.Len() == 0 {
|
|
return nil
|
|
}
|
|
var v *addrs
|
|
// Look for an address we can remove.
|
|
for {
|
|
if !a.byExpiresAt.peek().expiresAt.Before(now) {
|
|
// There's no longer anything outside the expiry window.
|
|
return nil
|
|
}
|
|
candidate := heap.Pop(&a.byExpiresAt).(*addrs)
|
|
if candidate.activeFlowCount == 0 && candidate.zeroFlowTime.Add(deadFlowWaitTimeout).Before(now) {
|
|
// Found one.
|
|
v = candidate
|
|
break
|
|
}
|
|
// Candidate can't be removed due to active flows. Extend expiresAt, and put it back in the heap.
|
|
candidate.expiresAt = now.Add(extendForActiveFlowDuration)
|
|
// TODO(mzb/fran): This is an expensive operation we could consider optimizing.
|
|
heap.Push(&a.byExpiresAt, candidate)
|
|
}
|
|
delete(a.byMagicIP, v.magic)
|
|
delete(a.byTransitIP, v.transit)
|
|
dd := domainDst{domain: v.domain, dst: v.dst}
|
|
delete(a.byDomainDst, dd)
|
|
return v
|
|
}
|
|
|
|
func (a *addrAssignments) updateExpiry(as *addrs, expiresIn time.Duration) {
|
|
now := a.clock.Now()
|
|
as.expiresAt = now.Add(expiresIn)
|
|
// TODO(fran) 2026-05-26 We can make this perform better.
|
|
// * With a bit of extra effort, we can track the index so that heap.Fix can
|
|
// be used.
|
|
// * Alternatively, marking the heap dirty and waiting until the next
|
|
// operation that requires it to be in the correct order would mean a
|
|
// whole slew of updates can accumulate before paying for a heap.Init.
|
|
heap.Init(&a.byExpiresAt)
|
|
}
|
|
|
|
type addrsHeap []*addrs
|
|
|
|
func (h addrsHeap) Len() int { return len(h) }
|
|
func (h addrsHeap) Less(i, j int) bool { return h[i].expiresAt.Before(h[j].expiresAt) }
|
|
func (h addrsHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
|
func (h *addrsHeap) Push(x any) {
|
|
as, ok := x.(*addrs)
|
|
if !ok {
|
|
panic("unexpected not an addrs")
|
|
}
|
|
*h = append(*h, as)
|
|
}
|
|
func (h *addrsHeap) Pop() any {
|
|
old := *h
|
|
n := len(old)
|
|
x := old[n-1]
|
|
*h = old[0 : n-1]
|
|
return x
|
|
}
|
|
func (h addrsHeap) peek() *addrs {
|
|
return (h)[0]
|
|
}
|