WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -31,6 +31,7 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tstun"
|
||||
@@ -84,6 +85,7 @@ func init() {
|
||||
})
|
||||
ipnlocal.RegisterPeerAPIHandler("/v0/connector/transit-ip", handleConnectorTransitIP)
|
||||
ipnlocal.HookReplyToDNSQueries.Add(handleHookReplyToDNSQueries)
|
||||
localapi.Register("conn25-state", serveStateGet)
|
||||
}
|
||||
|
||||
func handleConnectorTransitIP(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -5,6 +5,7 @@ package conn25
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/netip"
|
||||
|
||||
"go4.org/netipx"
|
||||
@@ -140,3 +141,37 @@ func (ipp *ippool) reconfig(ipSet *netipx.IPSet) *ippool {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package conn25
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
@@ -197,3 +198,85 @@ func TestIPPoolReconfig(t *testing.T) {
|
||||
ipp.returnAddr(netip.MustParseAddr("192.168.0.9"))
|
||||
expectAddrNext(t, ipp, "192.168.0.9")
|
||||
}
|
||||
|
||||
func TestIPPoolCapacity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
want int64
|
||||
}{
|
||||
{"ipv4-slash-30", "100.64.0.0/30", 4},
|
||||
{"ipv4-slash-24", "100.64.0.0/24", 256},
|
||||
{"ipv4-single", "100.64.0.1/32", 1},
|
||||
{"ipv6-slash-120", "fd7a::/120", 256},
|
||||
// 2^62 is the largest power of two below math.MaxInt64; not clamped.
|
||||
{"ipv6-slash-66-not-clamped", "fd7a::/66", 1 << 62},
|
||||
// 2^63 overflows int64, so it clamps.
|
||||
{"ipv6-slash-65-clamps", "fd7a::/65", math.MaxInt64},
|
||||
{"ipv6-slash-64-clamps", "fd7a::/64", math.MaxInt64},
|
||||
{"ipv6-default-clamps", "::/0", math.MaxInt64},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ipp := newIPPool(mustIPSetFromPrefix(tt.prefix))
|
||||
if got := ipp.capacity(); got != tt.want {
|
||||
t.Errorf("capacity(%s) = %d, want %d", tt.prefix, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("multi-prefix-sum-overflows-clamps", func(t *testing.T) {
|
||||
b := &netipx.IPSetBuilder{}
|
||||
b.AddPrefix(netip.MustParsePrefix("fd7a::/66")) // 2^62 +
|
||||
b.AddPrefix(netip.MustParsePrefix("fd7a:0:0:0:8000::/66")) // 2^62 = 2^63 (1 more than MaxInt64)
|
||||
set, err := b.IPSet()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := newIPPool(set).capacity(); got != math.MaxInt64 {
|
||||
t.Errorf("capacity() = %d, want %d", got, int64(math.MaxInt64))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil-and-uninitialized", func(t *testing.T) {
|
||||
var nilPool *ippool
|
||||
if got := nilPool.capacity(); got != 0 {
|
||||
t.Errorf("nil pool capacity() = %d, want 0", got)
|
||||
}
|
||||
// newIPPool(nil) returns a non-nil pool with a nil ipSet.
|
||||
if got := newIPPool(nil).capacity(); got != 0 {
|
||||
t.Errorf("uninitialized pool capacity() = %d, want 0", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIPPoolInUseCount(t *testing.T) {
|
||||
t.Run("counts-handed-out", func(t *testing.T) {
|
||||
ipp := newIPPool(mustIPSetFromPrefix("100.64.0.0/29")) // 8 addresses
|
||||
if got := ipp.inUseCount(); got != 0 {
|
||||
t.Fatalf("fresh pool inUseCount() = %d, want 0", got)
|
||||
}
|
||||
a1 := must.Get(ipp.next())
|
||||
must.Get(ipp.next())
|
||||
if got := ipp.inUseCount(); got != 2 {
|
||||
t.Fatalf("after 2 next() inUseCount() = %d, want 2", got)
|
||||
}
|
||||
if err := ipp.returnAddr(a1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := ipp.inUseCount(); got != 1 {
|
||||
t.Fatalf("after returnAddr inUseCount() = %d, want 1", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil-and-uninitialized", func(t *testing.T) {
|
||||
var nilPool *ippool
|
||||
if got := nilPool.inUseCount(); got != 0 {
|
||||
t.Errorf("nil pool inUseCount() = %d, want 0", got)
|
||||
}
|
||||
// newIPPool(nil) returns a non-nil pool with a nil inUse set.
|
||||
if got := newIPPool(nil).inUseCount(); got != 0 {
|
||||
t.Errorf("uninitialized pool inUseCount() = %d, want 0", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package conn25
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// serveStateGet serves the localapi endpoint /conn25-state.
|
||||
// See also [*Conn25.GetActiveState].
|
||||
func serveStateGet(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(tailscale/corp#39033): Remove for alpha release.
|
||||
if !envknob.UseWIPCode() && !testenv.InTest() {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "conn25-state access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.GET {
|
||||
http.Error(w, "GET required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ext, ok := ipnlocal.GetExt[*extension](h.LocalBackend())
|
||||
if !ok {
|
||||
http.Error(w, "miswired", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
state := ext.conn25.GetActiveState()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(state); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveState returns active state for the client and the connector,
|
||||
// including IP pool usage, address mappings, domains, and active flow counts.
|
||||
func (c *Conn25) GetActiveState() appctype.Conn25ActiveState {
|
||||
if !c.isConfigured() {
|
||||
return appctype.Conn25ActiveState{}
|
||||
}
|
||||
|
||||
return appctype.Conn25ActiveState{
|
||||
Configured: true,
|
||||
Client: c.client.getActiveState(),
|
||||
Connector: c.connector.getActiveState(),
|
||||
}
|
||||
}
|
||||
|
||||
// getActiveState gets active state from the client.
|
||||
// See [appctype.Conn25ClientState] for structure.
|
||||
func (c *client) getActiveState() appctype.Conn25ClientState {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
var appToDomains map[string]map[dnsname.FQDN][]appctype.Conn25ClientAddressState
|
||||
|
||||
// Pre-sort addresses by destination IP. They'll later be primary sorted
|
||||
// by active flow count.
|
||||
domainDstKeys := slices.Collect(maps.Keys(c.assignments.byDomainDst))
|
||||
slices.SortFunc(domainDstKeys, func(a, b domainDst) int {
|
||||
return a.dst.Compare(b.dst)
|
||||
})
|
||||
|
||||
for _, domainDstKey := range domainDstKeys {
|
||||
assignment := c.assignments.byDomainDst[domainDstKey]
|
||||
domainMap := appToDomains[assignment.app]
|
||||
addresses := domainMap[assignment.domain]
|
||||
addresses = append(addresses, appctype.Conn25ClientAddressState{
|
||||
ActiveFlowCount: assignment.activeFlowCount,
|
||||
DestinationIP: assignment.dst.String(),
|
||||
MagicIP: assignment.magic.String(),
|
||||
TransitIP: assignment.transit.String(),
|
||||
ExpiresAt: assignment.expiresAt,
|
||||
})
|
||||
|
||||
mak.Set(&domainMap, assignment.domain, addresses)
|
||||
mak.Set(&appToDomains, assignment.app, domainMap)
|
||||
}
|
||||
|
||||
var apps []appctype.Conn25ClientAppState
|
||||
for appName, domainMap := range appToDomains {
|
||||
var app appctype.Conn25ClientAppState
|
||||
app.App = appName
|
||||
for domain, addresses := range domainMap {
|
||||
var domainState appctype.Conn25ClientDomainState
|
||||
domainState.Domain = domain
|
||||
|
||||
// Sort address mappings by descending active flow count.
|
||||
slices.SortStableFunc(addresses, func(a, b appctype.Conn25ClientAddressState) int {
|
||||
if a.ActiveFlowCount > b.ActiveFlowCount {
|
||||
return -1
|
||||
}
|
||||
if a.ActiveFlowCount < b.ActiveFlowCount {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
domainState.Addresses = addresses
|
||||
app.Domains = append(app.Domains, domainState)
|
||||
}
|
||||
|
||||
// Sort domains by hierarchy, e.g. "example.com", "sub.example.com".
|
||||
slices.SortFunc(app.Domains, compareFQDNHierarchical)
|
||||
apps = append(apps, app)
|
||||
}
|
||||
|
||||
// Sort apps lexicographically.
|
||||
slices.SortFunc(apps, func(a, b appctype.Conn25ClientAppState) int {
|
||||
return strings.Compare(a.App, b.App)
|
||||
})
|
||||
|
||||
return appctype.Conn25ClientState{
|
||||
Apps: apps,
|
||||
IPPoolStats: appctype.Conn25ClientIPPoolStats{
|
||||
IPv4MagicIPsInUse: c.v4MagicIPPool.inUseCount(),
|
||||
IPv4MagicIPsCapacity: c.v4MagicIPPool.capacity(),
|
||||
IPv6MagicIPsInUse: c.v6MagicIPPool.inUseCount(),
|
||||
IPv6MagicIPsCapacity: c.v6MagicIPPool.capacity(),
|
||||
IPv4TransitIPsInUse: c.v4TransitIPPool.inUseCount(),
|
||||
IPv4TransitIPsCapacity: c.v4TransitIPPool.capacity(),
|
||||
IPv6TransitIPsInUse: c.v6TransitIPPool.inUseCount(),
|
||||
IPv6TransitIPsCapacity: c.v6TransitIPPool.capacity(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getActiveState gets active state from the connector.
|
||||
// See [appctype.Conn25ConnectorState] for structure.
|
||||
func (c *connector) getActiveState() appctype.Conn25ConnectorState {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
var peers []appctype.Conn25ConnectorPeerState
|
||||
// Sort peers by client IP.
|
||||
for _, clientIP := range slices.SortedFunc(maps.Keys(c.transitIPs), netip.Addr.Compare) {
|
||||
transitToAddr := c.transitIPs[clientIP]
|
||||
var appToAddrs map[string][]appctype.Conn25ConnectorAddressState
|
||||
|
||||
// Sort address mappings by transit IP.
|
||||
for _, transitIP := range slices.SortedFunc(maps.Keys(transitToAddr), netip.Addr.Compare) {
|
||||
addr := transitToAddr[transitIP]
|
||||
apiAddr := appctype.Conn25ConnectorAddressState{
|
||||
DestinationIP: addr.addr.String(),
|
||||
TransitIP: transitIP.String(),
|
||||
}
|
||||
mak.Set(&appToAddrs, addr.app, append(appToAddrs[addr.app], apiAddr))
|
||||
}
|
||||
|
||||
var apps []appctype.Conn25ConnectorAppState
|
||||
for appName, addrs := range appToAddrs {
|
||||
apps = append(apps, appctype.Conn25ConnectorAppState{
|
||||
App: appName,
|
||||
Addresses: addrs,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort apps lexicographically.
|
||||
slices.SortFunc(apps, func(a, b appctype.Conn25ConnectorAppState) int {
|
||||
return strings.Compare(a.App, b.App)
|
||||
})
|
||||
|
||||
peers = append(peers, appctype.Conn25ConnectorPeerState{
|
||||
ClientIP: clientIP.String(),
|
||||
Apps: apps,
|
||||
})
|
||||
}
|
||||
|
||||
return appctype.Conn25ConnectorState{Peers: peers}
|
||||
}
|
||||
|
||||
// compareFQDNHierarchical sorts [appctype.Conn25ClientDomainState] hierarchically
|
||||
// such that parents precede their children, e.g. "example.com" precedes
|
||||
// "sub.example.com". If two domains aren't related, they are sorted lexicographically
|
||||
// from the root and moving forward, e.g. "example.com" precedes "abc.org".
|
||||
func compareFQDNHierarchical(ad, bd appctype.Conn25ClientDomainState) int {
|
||||
a, b := ad.Domain, bd.Domain
|
||||
al := strings.Split(a.WithoutTrailingDot(), ".")
|
||||
bl := strings.Split(b.WithoutTrailingDot(), ".")
|
||||
for i := 1; i <= len(al) && i <= len(bl); i++ {
|
||||
if c := strings.Compare(al[len(al)-i], bl[len(bl)-i]); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return cmp.Compare(len(al), len(bl))
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package conn25
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
func TestGetActiveState(t *testing.T) {
|
||||
mustFQDN := func(s string) dnsname.FQDN { return must.Get(dnsname.ToFQDN(s)) }
|
||||
// Fixed, arbitrary expiry so the output is comparable.
|
||||
expires := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
t.Run("unconfigured", func(t *testing.T) {
|
||||
c := newConn25(logger.Discard)
|
||||
if diff := cmp.Diff(appctype.Conn25ActiveState{}, c.GetActiveState()); diff != "" {
|
||||
t.Fatalf("unconfigured Conn25ActiveState mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("configured", func(t *testing.T) {
|
||||
c := newConn25(logger.Discard)
|
||||
c.config.Store(&config{isConfigured: true})
|
||||
|
||||
client := c.client
|
||||
client.assignments.byDomainDst = map[domainDst]*addrs{}
|
||||
addClientAssignment := func(app, domain, dst, magic, transit string, flows int) {
|
||||
as := &addrs{
|
||||
app: app,
|
||||
domain: mustFQDN(domain),
|
||||
dst: netip.MustParseAddr(dst),
|
||||
magic: netip.MustParseAddr(magic),
|
||||
transit: netip.MustParseAddr(transit),
|
||||
activeFlowCount: flows,
|
||||
expiresAt: expires,
|
||||
}
|
||||
client.assignments.byDomainDst[domainDst{domain: as.domain, dst: as.dst}] = as
|
||||
}
|
||||
// Two addresses under app1/example.com: should sort by active flow
|
||||
// count descending (5 before 2).
|
||||
addClientAssignment("app1", "example.com", "10.0.0.1", "100.64.0.1", "169.254.0.1", 2)
|
||||
addClientAssignment("app1", "example.com", "10.0.0.2", "100.64.0.2", "169.254.0.2", 5)
|
||||
// example.com (2 labels) sorts before sub.example.com.
|
||||
addClientAssignment("app1", "sub.example.com", "10.0.0.3", "100.64.0.3", "169.254.0.3", 1)
|
||||
// A second app: apps sort by name, so app1 before zebra.
|
||||
addClientAssignment("zebra", "z.example.org", "10.0.0.4", "100.64.0.4", "169.254.0.4", 1)
|
||||
|
||||
// IP addresses should be sorted numerically, not lexigraphically.
|
||||
c.connector.transitIPs = map[netip.Addr]map[netip.Addr]appAddr{
|
||||
netip.MustParseAddr("100.64.0.1"): {
|
||||
netip.MustParseAddr("169.254.0.100"): {app: "app1", addr: netip.MustParseAddr("10.0.0.100")},
|
||||
netip.MustParseAddr("169.254.0.11"): {app: "app1", addr: netip.MustParseAddr("10.0.0.11")},
|
||||
},
|
||||
netip.MustParseAddr("11.0.0.1"): {
|
||||
netip.MustParseAddr("169.254.0.5"): {app: "zapp", addr: netip.MustParseAddr("10.0.0.5")},
|
||||
netip.MustParseAddr("169.254.0.6"): {app: "app1", addr: netip.MustParseAddr("10.0.0.6")},
|
||||
},
|
||||
}
|
||||
|
||||
// Configure the four IP pools with distinct capacities and hand out a
|
||||
// distinct number of addresses from each.
|
||||
mustPool := func(prefix string, handOut int) *ippool {
|
||||
p := newIPPool(mustIPSetFromPrefix(prefix))
|
||||
for range handOut {
|
||||
if _, err := p.next(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
client.v4MagicIPPool = mustPool("100.64.0.0/30", 1) // capacity 4
|
||||
client.v6MagicIPPool = mustPool("fd7a:1::/125", 2) // capacity 8
|
||||
client.v4TransitIPPool = mustPool("169.254.0.0/28", 3) // capacity 16
|
||||
client.v6TransitIPPool = mustPool("fd7a:2::/123", 4) // capacity 32
|
||||
|
||||
want := appctype.Conn25ActiveState{
|
||||
Configured: true,
|
||||
Client: appctype.Conn25ClientState{
|
||||
Apps: []appctype.Conn25ClientAppState{
|
||||
{
|
||||
App: "app1",
|
||||
Domains: []appctype.Conn25ClientDomainState{
|
||||
{
|
||||
Domain: mustFQDN("example.com"),
|
||||
Addresses: []appctype.Conn25ClientAddressState{
|
||||
{ActiveFlowCount: 5, DestinationIP: "10.0.0.2", MagicIP: "100.64.0.2", TransitIP: "169.254.0.2", ExpiresAt: expires},
|
||||
{ActiveFlowCount: 2, DestinationIP: "10.0.0.1", MagicIP: "100.64.0.1", TransitIP: "169.254.0.1", ExpiresAt: expires},
|
||||
},
|
||||
},
|
||||
{
|
||||
Domain: mustFQDN("sub.example.com"),
|
||||
Addresses: []appctype.Conn25ClientAddressState{
|
||||
{ActiveFlowCount: 1, DestinationIP: "10.0.0.3", MagicIP: "100.64.0.3", TransitIP: "169.254.0.3", ExpiresAt: expires},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
App: "zebra",
|
||||
Domains: []appctype.Conn25ClientDomainState{
|
||||
{
|
||||
Domain: mustFQDN("z.example.org"),
|
||||
Addresses: []appctype.Conn25ClientAddressState{
|
||||
{ActiveFlowCount: 1, DestinationIP: "10.0.0.4", MagicIP: "100.64.0.4", TransitIP: "169.254.0.4", ExpiresAt: expires},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
IPPoolStats: appctype.Conn25ClientIPPoolStats{
|
||||
IPv4MagicIPsInUse: 1,
|
||||
IPv4MagicIPsCapacity: 4,
|
||||
IPv6MagicIPsInUse: 2,
|
||||
IPv6MagicIPsCapacity: 8,
|
||||
IPv4TransitIPsInUse: 3,
|
||||
IPv4TransitIPsCapacity: 16,
|
||||
IPv6TransitIPsInUse: 4,
|
||||
IPv6TransitIPsCapacity: 32,
|
||||
},
|
||||
},
|
||||
Connector: appctype.Conn25ConnectorState{
|
||||
Peers: []appctype.Conn25ConnectorPeerState{
|
||||
{
|
||||
ClientIP: "11.0.0.1",
|
||||
Apps: []appctype.Conn25ConnectorAppState{
|
||||
{App: "app1", Addresses: []appctype.Conn25ConnectorAddressState{{DestinationIP: "10.0.0.6", TransitIP: "169.254.0.6"}}},
|
||||
{App: "zapp", Addresses: []appctype.Conn25ConnectorAddressState{{DestinationIP: "10.0.0.5", TransitIP: "169.254.0.5"}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
ClientIP: "100.64.0.1",
|
||||
Apps: []appctype.Conn25ConnectorAppState{
|
||||
{App: "app1", Addresses: []appctype.Conn25ConnectorAddressState{
|
||||
{DestinationIP: "10.0.0.11", TransitIP: "169.254.0.11"},
|
||||
{DestinationIP: "10.0.0.100", TransitIP: "169.254.0.100"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got := c.GetActiveState()
|
||||
if diff := cmp.Diff(want, got, cmpopts.EquateApproxTime(0)); diff != "" {
|
||||
t.Fatalf("Conn25ActiveState mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,9 +7,11 @@ package appctype
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// ConfigID is an opaque identifier for a configuration.
|
||||
@@ -113,3 +115,81 @@ type Conn25PoolsAttr struct {
|
||||
V6MagicIPPool []netipx.IPRange `json:"v6MagicIPPool,omitempty"`
|
||||
V6TransitIPPool []netipx.IPRange `json:"v6TransitIPPool,omitempty"`
|
||||
}
|
||||
|
||||
// Conn25ActiveState holds the active client and connector state.
|
||||
type Conn25ActiveState struct {
|
||||
Configured bool `json:"configured"`
|
||||
Client Conn25ClientState `json:"client,omitzero"`
|
||||
Connector Conn25ConnectorState `json:"connector,omitzero"`
|
||||
}
|
||||
|
||||
// Conn25ClientState holds the active client state.
|
||||
type Conn25ClientState struct {
|
||||
Apps []Conn25ClientAppState `json:"apps,omitempty"`
|
||||
IPPoolStats Conn25ClientIPPoolStats `json:"ipPoolStats,omitzero"`
|
||||
}
|
||||
|
||||
// Conn25ClientIPPoolStats holds the in-use and total capacity counts for the
|
||||
// client's magic and transit IP pools, split by address family. If a count
|
||||
// exceeds [math.MaxInt64], that maximum is used instead.
|
||||
type Conn25ClientIPPoolStats struct {
|
||||
IPv4MagicIPsInUse int64 `json:"ipv4MagicIPsInUse"`
|
||||
IPv4MagicIPsCapacity int64 `json:"ipv4MagicIPsCapacity"`
|
||||
IPv6MagicIPsInUse int64 `json:"ipv6MagicIPsInUse"`
|
||||
IPv6MagicIPsCapacity int64 `json:"ipv6MagicIPsCapacity"`
|
||||
IPv4TransitIPsInUse int64 `json:"ipv4TransitIPsInUse"`
|
||||
IPv4TransitIPsCapacity int64 `json:"ipv4TransitIPsCapacity"`
|
||||
IPv6TransitIPsInUse int64 `json:"ipv6TransitIPsInUse"`
|
||||
IPv6TransitIPsCapacity int64 `json:"ipv6TransitIPsCapacity"`
|
||||
}
|
||||
|
||||
// Conn25ClientAppState holds the active client state for a single app,
|
||||
// grouped by domain.
|
||||
type Conn25ClientAppState struct {
|
||||
App string `json:"app,omitempty"`
|
||||
Domains []Conn25ClientDomainState `json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
// Conn25ClientDomainState holds the address mappings the client has allocated
|
||||
// for a single domain.
|
||||
type Conn25ClientDomainState struct {
|
||||
Domain dnsname.FQDN `json:"domain"`
|
||||
Addresses []Conn25ClientAddressState `json:"addresses,omitempty"`
|
||||
}
|
||||
|
||||
// Conn25ClientAddressState describes a single address mapping the client has
|
||||
// allocated: the destination it resolves to, the magic and transit IPs handed
|
||||
// out for it, its active flow count, and when the mapping expires.
|
||||
type Conn25ClientAddressState struct {
|
||||
ActiveFlowCount int `json:"activeFlowCount"`
|
||||
DestinationIP string `json:"destinationIP,omitempty"`
|
||||
MagicIP string `json:"magicIP,omitempty"`
|
||||
TransitIP string `json:"transitIP,omitempty"`
|
||||
ExpiresAt time.Time `json:"expiresAt,omitzero"`
|
||||
}
|
||||
|
||||
// Conn25ConnectorState holds the active connector state.
|
||||
type Conn25ConnectorState struct {
|
||||
Peers []Conn25ConnectorPeerState `json:"peers,omitempty"`
|
||||
}
|
||||
|
||||
// Conn25ConnectorPeerState holds the active connector state for a single peer
|
||||
// (client) that has registered addresses with the connector, grouped by app.
|
||||
type Conn25ConnectorPeerState struct {
|
||||
ClientIP string `json:"clientIP,omitempty"`
|
||||
Apps []Conn25ConnectorAppState `json:"apps,omitempty"`
|
||||
}
|
||||
|
||||
// Conn25ConnectorAppState holds the address mappings a peer has registered
|
||||
// with the connector for a single app.
|
||||
type Conn25ConnectorAppState struct {
|
||||
App string `json:"app,omitempty"`
|
||||
Addresses []Conn25ConnectorAddressState `json:"addresses,omitempty"`
|
||||
}
|
||||
|
||||
// Conn25ConnectorAddressState describes a single transit-to-destination IP
|
||||
// mapping the connector routes on behalf of a peer.
|
||||
type Conn25ConnectorAddressState struct {
|
||||
DestinationIP string `json:"destinationIP,omitempty"`
|
||||
TransitIP string `json:"transitIP,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user