We were early-returning when the node was using an exit node, before Connectors 2025 split DNS routes were calculated and installed. Now we assemble the routes first, then install them in both exit node and non-exit-node contexts. The returned resolvers set UseWithExitNode to true even though as of today, we believe they should be installed in all cases without regard to that boolean value. With the boolean, we preserve the flexibility to toggle behavior without touching ipnlocal. We also add a TODO to turn the extra split DNS route gathering into a feature hook (tailscale/corp#37125). This does not affect appc connectors, which receive split DNS routes, and the UseWithExitNode value directly from control. Updates #16384 Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
87 lines
2.7 KiB
Go
87 lines
2.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package appc
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"tailscale.com/ipn/ipnext"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/appctype"
|
|
"tailscale.com/types/dnstype"
|
|
"tailscale.com/util/set"
|
|
)
|
|
|
|
const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental"
|
|
|
|
func isPeerEligibleConnector(peer tailcfg.NodeView) bool {
|
|
if !peer.Valid() || !peer.Hostinfo().Valid() {
|
|
return false
|
|
}
|
|
isConn, _ := peer.Hostinfo().AppConnector().Get()
|
|
return isConn
|
|
}
|
|
|
|
func sortByPreference(ns []tailcfg.NodeView) {
|
|
// The ordering of the nodes is semantic (callers use the first node they can
|
|
// get a peer api url for). We don't (currently 2026-02-27) have any
|
|
// preference over which node is chosen as long as it's consistent. In the
|
|
// future we anticipate integrating with traffic steering.
|
|
slices.SortFunc(ns, func(a, b tailcfg.NodeView) int {
|
|
return cmp.Compare(a.ID(), b.ID())
|
|
})
|
|
}
|
|
|
|
// PickConnector returns peers the backend knows about that match the app, in order of preference to use as
|
|
// a connector.
|
|
func PickConnector(nb ipnext.NodeBackend, app appctype.Conn25Attr) []tailcfg.NodeView {
|
|
appTagsSet := set.SetOf(app.Connectors)
|
|
matches := nb.AppendMatchingPeers(nil, func(n tailcfg.NodeView) bool {
|
|
if !isPeerEligibleConnector(n) {
|
|
return false
|
|
}
|
|
for _, t := range n.Tags().All() {
|
|
if appTagsSet.Contains(t) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
sortByPreference(matches)
|
|
return matches
|
|
}
|
|
|
|
// 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"
|
|
|
|
func AppDNSRoutes(hasCap func(c tailcfg.NodeCapability) 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
|
|
}
|
|
}
|
|
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}}
|
|
}
|
|
return m
|
|
}
|