And rename serveStateGet to serveLocalAPIStateGet to prepare for adding a c2n handler that is backed by the same methods as the LocalAPI handler. Updates tailscale/corp#40125 Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
205 lines
6.4 KiB
Go
205 lines
6.4 KiB
Go
// 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"
|
|
)
|
|
|
|
// serveLocalAPIStateGet serves the localapi endpoint /conn25-state.
|
|
// See also [*Conn25.GetActiveState].
|
|
func serveLocalAPIStateGet(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))
|
|
}
|