tailscale/appc/conn25.go
Fran Bull c9333854fb appc,feature/conn25: use custom scheme resolvers for conn25
Currently we are picking a peer for the split dns routes when we get a
netmap. Use the new custom scheme resolvers, installed per app in the
config in the netmap, to allow us to choose which connector peer should
handle a DNS request at the time the request is made.

Fixes tailscale/corp#39858

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-05-29 12:23:47 -07:00

87 lines
2.6 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)}}
}
return m
}