Adds IPPool and moves all IP address management concerns behind that. Updates #14667 Signed-off-by: Fran Bull <fran@tailscale.com>main
parent
46505ca338
commit
603a1d3830
@ -0,0 +1,127 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// ippool implements IP address storage, creation, and retrieval for cmd/natc
|
||||
package ippool |
||||
|
||||
import ( |
||||
"errors" |
||||
"log" |
||||
"math/big" |
||||
"net/netip" |
||||
"sync" |
||||
|
||||
"github.com/gaissmai/bart" |
||||
"go4.org/netipx" |
||||
"tailscale.com/syncs" |
||||
"tailscale.com/tailcfg" |
||||
"tailscale.com/util/dnsname" |
||||
"tailscale.com/util/mak" |
||||
) |
||||
|
||||
var ErrNoIPsAvailable = errors.New("no IPs available") |
||||
|
||||
type IPPool struct { |
||||
perPeerMap syncs.Map[tailcfg.NodeID, *perPeerState] |
||||
IPSet *netipx.IPSet |
||||
V6ULA netip.Prefix |
||||
} |
||||
|
||||
func (ipp *IPPool) DomainForIP(from tailcfg.NodeID, addr netip.Addr) (string, bool) { |
||||
ps, ok := ipp.perPeerMap.Load(from) |
||||
if !ok { |
||||
log.Printf("handleTCPFlow: no perPeerState for %v", from) |
||||
return "", false |
||||
} |
||||
domain, ok := ps.domainForIP(addr) |
||||
if !ok { |
||||
log.Printf("handleTCPFlow: no domain for IP %v\n", addr) |
||||
return "", false |
||||
} |
||||
return domain, ok |
||||
} |
||||
|
||||
func (ipp *IPPool) IPForDomain(from tailcfg.NodeID, domain string) ([]netip.Addr, error) { |
||||
npps := &perPeerState{ |
||||
ipset: ipp.IPSet, |
||||
v6ULA: ipp.V6ULA, |
||||
} |
||||
ps, _ := ipp.perPeerMap.LoadOrStore(from, npps) |
||||
return ps.ipForDomain(domain) |
||||
} |
||||
|
||||
// perPeerState holds the state for a single peer.
|
||||
type perPeerState struct { |
||||
v6ULA netip.Prefix |
||||
ipset *netipx.IPSet |
||||
|
||||
mu sync.Mutex |
||||
addrInUse *big.Int |
||||
domainToAddr map[string][]netip.Addr |
||||
addrToDomain *bart.Table[string] |
||||
} |
||||
|
||||
// domainForIP returns the domain name assigned to the given IP address and
|
||||
// whether it was found.
|
||||
func (ps *perPeerState) domainForIP(ip netip.Addr) (_ string, ok bool) { |
||||
ps.mu.Lock() |
||||
defer ps.mu.Unlock() |
||||
if ps.addrToDomain == nil { |
||||
return "", false |
||||
} |
||||
return ps.addrToDomain.Lookup(ip) |
||||
} |
||||
|
||||
// ipForDomain assigns a pair of unique IP addresses for the given domain and
|
||||
// returns them. The first address is an IPv4 address and the second is an IPv6
|
||||
// address. If the domain already has assigned addresses, it returns them.
|
||||
func (ps *perPeerState) ipForDomain(domain string) ([]netip.Addr, error) { |
||||
fqdn, err := dnsname.ToFQDN(domain) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
domain = fqdn.WithoutTrailingDot() |
||||
|
||||
ps.mu.Lock() |
||||
defer ps.mu.Unlock() |
||||
if addrs, ok := ps.domainToAddr[domain]; ok { |
||||
return addrs, nil |
||||
} |
||||
addrs := ps.assignAddrsLocked(domain) |
||||
if addrs == nil { |
||||
return nil, ErrNoIPsAvailable |
||||
} |
||||
return addrs, nil |
||||
} |
||||
|
||||
// unusedIPv4Locked returns an unused IPv4 address from the available ranges.
|
||||
func (ps *perPeerState) unusedIPv4Locked() netip.Addr { |
||||
if ps.addrInUse == nil { |
||||
ps.addrInUse = big.NewInt(0) |
||||
} |
||||
return allocAddr(ps.ipset, ps.addrInUse) |
||||
} |
||||
|
||||
// assignAddrsLocked assigns a pair of unique IP addresses for the given domain
|
||||
// and returns them. The first address is an IPv4 address and the second is an
|
||||
// IPv6 address. It does not check if the domain already has assigned addresses.
|
||||
// ps.mu must be held.
|
||||
func (ps *perPeerState) assignAddrsLocked(domain string) []netip.Addr { |
||||
if ps.addrToDomain == nil { |
||||
ps.addrToDomain = &bart.Table[string]{} |
||||
} |
||||
v4 := ps.unusedIPv4Locked() |
||||
if !v4.IsValid() { |
||||
return nil |
||||
} |
||||
as16 := ps.v6ULA.Addr().As16() |
||||
as4 := v4.As4() |
||||
copy(as16[12:], as4[:]) |
||||
v6 := netip.AddrFrom16(as16) |
||||
addrs := []netip.Addr{v4, v6} |
||||
mak.Set(&ps.domainToAddr, domain, addrs) |
||||
for _, a := range addrs { |
||||
ps.addrToDomain.Insert(netip.PrefixFrom(a, a.BitLen()), domain) |
||||
} |
||||
return addrs |
||||
} |
||||
@ -0,0 +1,129 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ippool |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"net/netip" |
||||
"slices" |
||||
"testing" |
||||
|
||||
"go4.org/netipx" |
||||
"tailscale.com/tailcfg" |
||||
"tailscale.com/util/must" |
||||
) |
||||
|
||||
func TestIPPoolExhaustion(t *testing.T) { |
||||
smallPrefix := netip.MustParsePrefix("100.64.1.0/30") // Only 4 IPs: .0, .1, .2, .3
|
||||
var ipsb netipx.IPSetBuilder |
||||
ipsb.AddPrefix(smallPrefix) |
||||
addrPool := must.Get(ipsb.IPSet()) |
||||
v6ULA := netip.MustParsePrefix("fd7a:115c:a1e0:a99c:0001::/80") |
||||
pool := IPPool{V6ULA: v6ULA, IPSet: addrPool} |
||||
|
||||
assignedIPs := make(map[netip.Addr]string) |
||||
|
||||
domains := []string{"a.example.com", "b.example.com", "c.example.com", "d.example.com", "e.example.com"} |
||||
|
||||
var errs []error |
||||
|
||||
from := tailcfg.NodeID(12345) |
||||
|
||||
for i := 0; i < 5; i++ { |
||||
for _, domain := range domains { |
||||
addrs, err := pool.IPForDomain(from, domain) |
||||
if err != nil { |
||||
errs = append(errs, fmt.Errorf("failed to get IP for domain %q: %w", domain, err)) |
||||
continue |
||||
} |
||||
|
||||
for _, addr := range addrs { |
||||
if d, ok := assignedIPs[addr]; ok { |
||||
if d != domain { |
||||
t.Errorf("IP %s reused for domain %q, previously assigned to %q", addr, domain, d) |
||||
} |
||||
} else { |
||||
assignedIPs[addr] = domain |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
for addr, domain := range assignedIPs { |
||||
if addr.Is4() && !smallPrefix.Contains(addr) { |
||||
t.Errorf("IP %s for domain %q not in expected range %s", addr, domain, smallPrefix) |
||||
} |
||||
if addr.Is6() && !v6ULA.Contains(addr) { |
||||
t.Errorf("IP %s for domain %q not in expected range %s", addr, domain, v6ULA) |
||||
} |
||||
} |
||||
|
||||
// expect one error for each iteration with the 5th domain
|
||||
if len(errs) != 5 { |
||||
t.Errorf("Expected 5 errors, got %d: %v", len(errs), errs) |
||||
} |
||||
for _, err := range errs { |
||||
if !errors.Is(err, ErrNoIPsAvailable) { |
||||
t.Errorf("generateDNSResponse() error = %v, want ErrNoIPsAvailable", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestIPPool(t *testing.T) { |
||||
var ipsb netipx.IPSetBuilder |
||||
ipsb.AddPrefix(netip.MustParsePrefix("100.64.1.0/24")) |
||||
addrPool := must.Get(ipsb.IPSet()) |
||||
pool := IPPool{ |
||||
V6ULA: netip.MustParsePrefix("fd7a:115c:a1e0:a99c:0001::/80"), |
||||
IPSet: addrPool, |
||||
} |
||||
from := tailcfg.NodeID(12345) |
||||
addrs, err := pool.IPForDomain(from, "example.com") |
||||
if err != nil { |
||||
t.Fatalf("ipForDomain() error = %v", err) |
||||
} |
||||
|
||||
if len(addrs) != 2 { |
||||
t.Fatalf("ipForDomain() returned %d addresses, want 2", len(addrs)) |
||||
} |
||||
|
||||
v4 := addrs[0] |
||||
v6 := addrs[1] |
||||
|
||||
if !v4.Is4() { |
||||
t.Errorf("First address is not IPv4: %s", v4) |
||||
} |
||||
|
||||
if !v6.Is6() { |
||||
t.Errorf("Second address is not IPv6: %s", v6) |
||||
} |
||||
|
||||
if !addrPool.Contains(v4) { |
||||
t.Errorf("IPv4 address %s not in range %s", v4, addrPool) |
||||
} |
||||
|
||||
domain, ok := pool.DomainForIP(from, v4) |
||||
if !ok { |
||||
t.Errorf("domainForIP(%s) not found", v4) |
||||
} else if domain != "example.com" { |
||||
t.Errorf("domainForIP(%s) = %s, want %s", v4, domain, "example.com") |
||||
} |
||||
|
||||
domain, ok = pool.DomainForIP(from, v6) |
||||
if !ok { |
||||
t.Errorf("domainForIP(%s) not found", v6) |
||||
} else if domain != "example.com" { |
||||
t.Errorf("domainForIP(%s) = %s, want %s", v6, domain, "example.com") |
||||
} |
||||
|
||||
addrs2, err := pool.IPForDomain(from, "example.com") |
||||
if err != nil { |
||||
t.Fatalf("ipForDomain() second call error = %v", err) |
||||
} |
||||
|
||||
if !slices.Equal(addrs, addrs2) { |
||||
t.Errorf("ipForDomain() second call = %v, want %v", addrs2, addrs) |
||||
} |
||||
} |
||||
@ -1,7 +1,7 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main |
||||
package ippool |
||||
|
||||
import ( |
||||
"math/big" |
||||
@ -1,7 +1,7 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main |
||||
package ippool |
||||
|
||||
import ( |
||||
"math" |
||||
Loading…
Reference in new issue