Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+14
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package appc implements App Connectors.
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/execqueue"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
@@ -364,18 +365,20 @@ func (e *AppConnector) updateRoutes(routes []netip.Prefix) {
|
||||
toRemove = routesWithout(e.controlRoutes, routes)
|
||||
}
|
||||
|
||||
nextRoute:
|
||||
obsoleted := set.Set[netip.Addr]{}
|
||||
for _, r := range routes {
|
||||
for _, addr := range e.domains {
|
||||
for _, a := range addr {
|
||||
if r.Contains(a) && netip.PrefixFrom(a, a.BitLen()) != r {
|
||||
pfx := netip.PrefixFrom(a, a.BitLen())
|
||||
toRemove = append(toRemove, pfx)
|
||||
continue nextRoute
|
||||
obsoleted.Add(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for a := range obsoleted.All() {
|
||||
pfx := netip.PrefixFrom(a, a.BitLen())
|
||||
toRemove = append(toRemove, pfx)
|
||||
}
|
||||
|
||||
if e.routeAdvertiser != nil {
|
||||
e.queue.Add(func() {
|
||||
@@ -425,6 +428,7 @@ func (e *AppConnector) DomainRoutes() map[string][]netip.Addr {
|
||||
// e.mu must be held.
|
||||
func (e *AppConnector) findRoutedDomainLocked(domain string, cnameChain map[string]string) (string, bool) {
|
||||
var isRouted bool
|
||||
var seen set.Set[string]
|
||||
for {
|
||||
_, isRouted = e.domains[domain]
|
||||
if isRouted {
|
||||
@@ -444,7 +448,12 @@ func (e *AppConnector) findRoutedDomainLocked(domain string, cnameChain map[stri
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if seen.Contains(next) {
|
||||
break
|
||||
}
|
||||
domain = next
|
||||
seen.Make()
|
||||
seen.Add(domain)
|
||||
}
|
||||
return domain, isRouted
|
||||
}
|
||||
|
||||
+31
-95
@@ -1,110 +1,46 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package appc
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"sync"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailcfg/nodecap"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/types/dnstype"
|
||||
)
|
||||
|
||||
// Conn25 holds the developing state for the as yet nascent next generation app connector.
|
||||
// There is currently (2025-12-08) no actual app connecting functionality.
|
||||
type Conn25 struct {
|
||||
mu sync.Mutex
|
||||
transitIPs map[tailcfg.NodeID]map[netip.Addr]netip.Addr
|
||||
}
|
||||
const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental"
|
||||
|
||||
const dupeTransitIPMessage = "Duplicate transit address in ConnectorTransitIPRequest"
|
||||
// DNSAddrScheme is the custom URI scheme used for conn25-managed split DNS
|
||||
// entries to determine the destination at query time rather than configuration
|
||||
// time.
|
||||
const DNSAddrScheme = "tailscale-app"
|
||||
|
||||
// HandleConnectorTransitIPRequest creates a ConnectorTransitIPResponse in response to a ConnectorTransitIPRequest.
|
||||
// It updates the connectors mapping of TransitIP->DestinationIP per peer (tailcfg.NodeID).
|
||||
// If a peer has stored this mapping in the connector Conn25 will route traffic to TransitIPs to DestinationIPs for that peer.
|
||||
func (c *Conn25) HandleConnectorTransitIPRequest(nid tailcfg.NodeID, ctipr ConnectorTransitIPRequest) ConnectorTransitIPResponse {
|
||||
resp := ConnectorTransitIPResponse{}
|
||||
seen := map[netip.Addr]bool{}
|
||||
for _, each := range ctipr.TransitIPs {
|
||||
if seen[each.TransitIP] {
|
||||
resp.TransitIPs = append(resp.TransitIPs, TransitIPResponse{
|
||||
Code: OtherFailure,
|
||||
Message: dupeTransitIPMessage,
|
||||
})
|
||||
continue
|
||||
func AppDNSRoutes(hasCap func(c nodecap.Cap) bool, self tailcfg.NodeView) map[string][]*dnstype.Resolver {
|
||||
if !hasCap(AppConnectorsExperimentalAttrName) {
|
||||
return nil
|
||||
}
|
||||
apps, err := tailcfg.UnmarshalNodeCapViewJSON[appctype.AppConnectorAttr](self.CapMap(), AppConnectorsExperimentalAttrName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
appNamesByDomain := map[string]string{}
|
||||
for _, app := range apps {
|
||||
for _, domain := range app.Domains {
|
||||
domain, _ = strings.CutPrefix(domain, "*.")
|
||||
domain = strings.ToLower(domain)
|
||||
// in the case of multiple apps specifying the same domain (which is misconfiguration
|
||||
// that should be validated at point of input) last write wins.
|
||||
appNamesByDomain[domain] = app.Name
|
||||
}
|
||||
tipresp := c.handleTransitIPRequest(nid, each)
|
||||
seen[each.TransitIP] = true
|
||||
resp.TransitIPs = append(resp.TransitIPs, tipresp)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func (c *Conn25) handleTransitIPRequest(nid tailcfg.NodeID, tipr TransitIPRequest) TransitIPResponse {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.transitIPs == nil {
|
||||
c.transitIPs = make(map[tailcfg.NodeID]map[netip.Addr]netip.Addr)
|
||||
m := make(map[string][]*dnstype.Resolver, len(appNamesByDomain))
|
||||
for domain, appName := range appNamesByDomain {
|
||||
m[domain] = []*dnstype.Resolver{{Addr: fmt.Sprintf("%s:%s", DNSAddrScheme, appName), UseWithExitNode: true}}
|
||||
}
|
||||
peerMap, ok := c.transitIPs[nid]
|
||||
if !ok {
|
||||
peerMap = make(map[netip.Addr]netip.Addr)
|
||||
c.transitIPs[nid] = peerMap
|
||||
}
|
||||
peerMap[tipr.TransitIP] = tipr.DestinationIP
|
||||
return TransitIPResponse{}
|
||||
}
|
||||
|
||||
func (c *Conn25) transitIPTarget(nid tailcfg.NodeID, tip netip.Addr) netip.Addr {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.transitIPs[nid][tip]
|
||||
}
|
||||
|
||||
// TransitIPRequest details a single TransitIP allocation request from a client to a
|
||||
// connector.
|
||||
type TransitIPRequest struct {
|
||||
// TransitIP is the intermediate destination IP that will be received at this
|
||||
// connector and will be replaced by DestinationIP when performing DNAT.
|
||||
TransitIP netip.Addr `json:"transitIP,omitzero"`
|
||||
|
||||
// DestinationIP is the final destination IP that connections to the TransitIP
|
||||
// should be mapped to when performing DNAT.
|
||||
DestinationIP netip.Addr `json:"destinationIP,omitzero"`
|
||||
}
|
||||
|
||||
// ConnectorTransitIPRequest is the request body for a PeerAPI request to
|
||||
// /connector/transit-ip and can include zero or more TransitIP allocation requests.
|
||||
type ConnectorTransitIPRequest struct {
|
||||
// TransitIPs is the list of requested mappings.
|
||||
TransitIPs []TransitIPRequest `json:"transitIPs,omitempty"`
|
||||
}
|
||||
|
||||
// TransitIPResponseCode appears in TransitIPResponse and signifies success or failure status.
|
||||
type TransitIPResponseCode int
|
||||
|
||||
const (
|
||||
// OK indicates that the mapping was created as requested.
|
||||
OK TransitIPResponseCode = 0
|
||||
|
||||
// OtherFailure indicates that the mapping failed for a reason that does not have
|
||||
// another relevant [TransitIPResponsecode].
|
||||
OtherFailure TransitIPResponseCode = 1
|
||||
)
|
||||
|
||||
// TransitIPResponse is the response to a TransitIPRequest
|
||||
type TransitIPResponse struct {
|
||||
// Code is an error code indicating success or failure of the [TransitIPRequest].
|
||||
Code TransitIPResponseCode `json:"code,omitzero"`
|
||||
// Message is an error message explaining what happened, suitable for logging but
|
||||
// not necessarily suitable for displaying in a UI to non-technical users. It
|
||||
// should be empty when [Code] is [OK].
|
||||
Message string `json:"message,omitzero"`
|
||||
}
|
||||
|
||||
// ConnectorTransitIPResponse is the response to a ConnectorTransitIPRequest
|
||||
type ConnectorTransitIPResponse struct {
|
||||
// TransitIPs is the list of outcomes for each requested mapping. Elements
|
||||
// correspond to the order of [ConnectorTransitIPRequest.TransitIPs].
|
||||
TransitIPs []TransitIPResponse `json:"transitIPs,omitempty"`
|
||||
return m
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package appc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
// errPoolExhausted is returned when there are no more addresses to iterate over.
|
||||
var errPoolExhausted = errors.New("ip pool exhausted")
|
||||
|
||||
// ippool allows for 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.
|
||||
type ippool 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
|
||||
}
|
||||
|
||||
func newIPPool(ipset *netipx.IPSet) *ippool {
|
||||
if ipset == nil {
|
||||
return &ippool{}
|
||||
}
|
||||
return &ippool{ranges: ipset.Ranges()}
|
||||
}
|
||||
|
||||
// next returns the next address from the set, or errPoolExhausted if we have
|
||||
// iterated over the whole set.
|
||||
func (ipp *ippool) next() (netip.Addr, error) {
|
||||
if ipp.rangeIdx >= len(ipp.ranges) {
|
||||
// ipset is empty or we have iterated off the end
|
||||
return netip.Addr{}, errPoolExhausted
|
||||
}
|
||||
if !ipp.last.IsValid() {
|
||||
// not initialized yet
|
||||
ipp.last = ipp.ranges[0].From()
|
||||
return ipp.last, nil
|
||||
}
|
||||
currRange := ipp.ranges[ipp.rangeIdx]
|
||||
if ipp.last == currRange.To() {
|
||||
// then we need to move to the next range
|
||||
ipp.rangeIdx++
|
||||
if ipp.rangeIdx >= len(ipp.ranges) {
|
||||
return netip.Addr{}, errPoolExhausted
|
||||
}
|
||||
ipp.last = ipp.ranges[ipp.rangeIdx].From()
|
||||
return ipp.last, nil
|
||||
}
|
||||
ipp.last = ipp.last.Next()
|
||||
return ipp.last, nil
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_appconnectors
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_appconnectors
|
||||
|
||||
Reference in new issue
Block a user