mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 20:26:47 +02:00
modifying DNS responses for domains they are also connectors for For Connectors 2025, determine if a client is configured as a connector and what domains it is a connector for. When acting as a client, don't install Split DNS routes to other connectors for those domains, and don't alter DNS responses for those domains. The responses are forwarded back to the original client, which in turn does the alteration, swapping the real IP for a Magic IP. A client is also a connector for a domain if it has tags that overlap with tags in the configured policy, and --advertise-connector=true in the prefs (not in the self-node Hostinfo from the netmap). We use the prefs as the source of truth because control only gets a copy from the prefs, and may drift. And the AppConnector field is currently zeroed out in the self-node Hostinfo from control. The extension adds a ProfileStateChange hook to process prefs changes, and the config type is split into prefs and nodeview sub-configs. Fixes tailscale/corp#39317 Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
116 lines
3.4 KiB
Go
116 lines
3.4 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package appc
|
|
|
|
import (
|
|
"cmp"
|
|
"slices"
|
|
|
|
"tailscale.com/ipn/ipnext"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/appctype"
|
|
"tailscale.com/util/mak"
|
|
"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
|
|
}
|
|
|
|
// PickSplitDNSPeers looks at the netmap peers capabilities and finds which peers
|
|
// want to be connectors for which domains.
|
|
func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.NodeView, peers map[tailcfg.NodeID]tailcfg.NodeView, isSelfEligibleConnector bool) map[string][]tailcfg.NodeView {
|
|
var m map[string][]tailcfg.NodeView
|
|
if !hasCap(AppConnectorsExperimentalAttrName) {
|
|
return m
|
|
}
|
|
apps, err := tailcfg.UnmarshalNodeCapViewJSON[appctype.AppConnectorAttr](self.CapMap(), AppConnectorsExperimentalAttrName)
|
|
if err != nil {
|
|
return m
|
|
}
|
|
tagToDomain := make(map[string][]string)
|
|
selfTags := set.SetOf(self.Tags().AsSlice())
|
|
selfRoutedDomains := set.Set[string]{}
|
|
for _, app := range apps {
|
|
for _, tag := range app.Connectors {
|
|
domains := tagToDomain[tag]
|
|
domains = slices.Grow(domains, len(app.Domains))
|
|
for _, d := range app.Domains {
|
|
if isSelfEligibleConnector && selfTags.Contains(tag) {
|
|
selfRoutedDomains.Add(d)
|
|
}
|
|
domains = append(domains, d)
|
|
}
|
|
tagToDomain[tag] = domains
|
|
}
|
|
}
|
|
// NodeIDs are Comparable, and we have a map of NodeID to NodeView anyway, so
|
|
// use a Set of NodeIDs to deduplicate, and populate into a []NodeView later.
|
|
var work map[string]set.Set[tailcfg.NodeID]
|
|
for _, peer := range peers {
|
|
if !isPeerEligibleConnector(peer) {
|
|
continue
|
|
}
|
|
for _, t := range peer.Tags().All() {
|
|
domains := tagToDomain[t]
|
|
for _, domain := range domains {
|
|
if selfRoutedDomains.Contains(domain) {
|
|
continue
|
|
}
|
|
if work[domain] == nil {
|
|
mak.Set(&work, domain, set.Set[tailcfg.NodeID]{})
|
|
}
|
|
work[domain].Add(peer.ID())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Populate m. Make a []tailcfg.NodeView from []tailcfg.NodeID using the peers map.
|
|
// And sort it to our preference.
|
|
for domain, ids := range work {
|
|
nodes := make([]tailcfg.NodeView, 0, ids.Len())
|
|
for id := range ids {
|
|
nodes = append(nodes, peers[id])
|
|
}
|
|
sortByPreference(nodes)
|
|
mak.Set(&m, domain, nodes)
|
|
}
|
|
return m
|
|
}
|