appc,feature/conn25: conn25: send address assignments to connector
After we intercept a DNS response and assign magic and transit addresses we must communicate the assignment to our connector so that it can direct traffic when it arrives. Use the recently added peerapi endpoint to send the addresses. Updates tailscale/corp#34258 Signed-off-by: Fran Bull <fran@tailscale.com>
This commit is contained in:
+40
-10
@@ -7,6 +7,7 @@ import (
|
||||
"cmp"
|
||||
"slices"
|
||||
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -15,6 +16,43 @@ import (
|
||||
|
||||
const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental"
|
||||
|
||||
func isEligibleConnector(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 !isEligibleConnector(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) map[string][]tailcfg.NodeView {
|
||||
@@ -36,10 +74,7 @@ func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.
|
||||
// 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 !peer.Valid() || !peer.Hostinfo().Valid() {
|
||||
continue
|
||||
}
|
||||
if isConn, _ := peer.Hostinfo().AppConnector().Get(); !isConn {
|
||||
if !isEligibleConnector(peer) {
|
||||
continue
|
||||
}
|
||||
for _, t := range peer.Tags().All() {
|
||||
@@ -60,12 +95,7 @@ func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.
|
||||
for id := range ids {
|
||||
nodes = append(nodes, peers[id])
|
||||
}
|
||||
// The ordering of the nodes in the map vals is semantic (dnsConfigForNetmap uses the first node it can
|
||||
// get a peer api url for as its split dns target). We can think of it as a preference order, except that
|
||||
// we don't (currently 2026-01-14) have any preference over which node is chosen.
|
||||
slices.SortFunc(nodes, func(a, b tailcfg.NodeView) int {
|
||||
return cmp.Compare(a.ID(), b.ID())
|
||||
})
|
||||
sortByPreference(nodes)
|
||||
mak.Set(&m, domain, nodes)
|
||||
}
|
||||
return m
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/types/opt"
|
||||
@@ -131,3 +133,157 @@ func TestPickSplitDNSPeers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testNodeBackend struct {
|
||||
ipnext.NodeBackend
|
||||
peers []tailcfg.NodeView
|
||||
}
|
||||
|
||||
func (nb *testNodeBackend) AppendMatchingPeers(base []tailcfg.NodeView, pred func(tailcfg.NodeView) bool) []tailcfg.NodeView {
|
||||
for _, p := range nb.peers {
|
||||
if pred(p) {
|
||||
base = append(base, p)
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func (nb *testNodeBackend) PeerHasPeerAPI(p tailcfg.NodeView) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestPickConnector(t *testing.T) {
|
||||
exampleApp := appctype.Conn25Attr{
|
||||
Name: "example",
|
||||
Connectors: []string{"tag:example"},
|
||||
Domains: []string{"example.com"},
|
||||
}
|
||||
|
||||
nvWithConnectorSet := func(id tailcfg.NodeID, isConnector bool, tags ...string) tailcfg.NodeView {
|
||||
return (&tailcfg.Node{
|
||||
ID: id,
|
||||
Tags: tags,
|
||||
Hostinfo: (&tailcfg.Hostinfo{AppConnector: opt.NewBool(isConnector)}).View(),
|
||||
}).View()
|
||||
}
|
||||
|
||||
nv := func(id tailcfg.NodeID, tags ...string) tailcfg.NodeView {
|
||||
return nvWithConnectorSet(id, true, tags...)
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
candidates []tailcfg.NodeView
|
||||
app appctype.Conn25Attr
|
||||
want []tailcfg.NodeView
|
||||
}{
|
||||
{
|
||||
name: "empty-everything",
|
||||
candidates: []tailcfg.NodeView{},
|
||||
app: appctype.Conn25Attr{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty-candidates",
|
||||
candidates: []tailcfg.NodeView{},
|
||||
app: exampleApp,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty-app",
|
||||
candidates: []tailcfg.NodeView{nv(1, "tag:example")},
|
||||
app: appctype.Conn25Attr{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "one-matches",
|
||||
candidates: []tailcfg.NodeView{nv(1, "tag:example")},
|
||||
app: exampleApp,
|
||||
want: []tailcfg.NodeView{nv(1, "tag:example")},
|
||||
},
|
||||
{
|
||||
name: "invalid-candidate",
|
||||
candidates: []tailcfg.NodeView{
|
||||
{},
|
||||
nv(1, "tag:example"),
|
||||
},
|
||||
app: exampleApp,
|
||||
want: []tailcfg.NodeView{
|
||||
nv(1, "tag:example"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-host-info",
|
||||
candidates: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 1,
|
||||
Tags: []string{"tag:example"},
|
||||
}).View(),
|
||||
nv(2, "tag:example"),
|
||||
},
|
||||
app: exampleApp,
|
||||
want: []tailcfg.NodeView{nv(2, "tag:example")},
|
||||
},
|
||||
{
|
||||
name: "not-a-connector",
|
||||
candidates: []tailcfg.NodeView{nvWithConnectorSet(1, false, "tag:example.com"), nv(2, "tag:example")},
|
||||
app: exampleApp,
|
||||
want: []tailcfg.NodeView{nv(2, "tag:example")},
|
||||
},
|
||||
{
|
||||
name: "without-matches",
|
||||
candidates: []tailcfg.NodeView{nv(1, "tag:woo"), nv(2, "tag:example")},
|
||||
app: exampleApp,
|
||||
want: []tailcfg.NodeView{nv(2, "tag:example")},
|
||||
},
|
||||
{
|
||||
name: "multi-tags",
|
||||
candidates: []tailcfg.NodeView{nv(1, "tag:woo", "tag:hoo"), nv(2, "tag:woo", "tag:example")},
|
||||
app: exampleApp,
|
||||
want: []tailcfg.NodeView{nv(2, "tag:woo", "tag:example")},
|
||||
},
|
||||
{
|
||||
name: "multi-matches",
|
||||
candidates: []tailcfg.NodeView{nv(1, "tag:woo", "tag:hoo"), nv(2, "tag:woo", "tag:example"), nv(3, "tag:example1", "tag:example")},
|
||||
app: appctype.Conn25Attr{
|
||||
Name: "example2",
|
||||
Connectors: []string{"tag:example1", "tag:example"},
|
||||
Domains: []string{"example.com"},
|
||||
},
|
||||
want: []tailcfg.NodeView{nv(2, "tag:woo", "tag:example"), nv(3, "tag:example1", "tag:example")},
|
||||
},
|
||||
{
|
||||
name: "bit-of-everything",
|
||||
candidates: []tailcfg.NodeView{
|
||||
nv(3, "tag:woo", "tag:hoo"),
|
||||
{},
|
||||
nv(2, "tag:woo", "tag:example"),
|
||||
nvWithConnectorSet(4, false, "tag:example"),
|
||||
nv(1, "tag:example1", "tag:example"),
|
||||
nv(7, "tag:example1", "tag:example"),
|
||||
nvWithConnectorSet(5, false),
|
||||
nv(6),
|
||||
nvWithConnectorSet(8, false, "tag:example"),
|
||||
nvWithConnectorSet(9, false),
|
||||
nvWithConnectorSet(10, false),
|
||||
},
|
||||
app: appctype.Conn25Attr{
|
||||
Name: "example2",
|
||||
Connectors: []string{"tag:example1", "tag:example", "tag:example2"},
|
||||
Domains: []string{"example.com"},
|
||||
},
|
||||
want: []tailcfg.NodeView{
|
||||
nv(1, "tag:example1", "tag:example"),
|
||||
nv(2, "tag:woo", "tag:example"),
|
||||
nv(7, "tag:example1", "tag:example"),
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := PickConnector(&testNodeBackend{peers: tt.candidates}, tt.app)
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Fatalf("PickConnectors (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user