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:
Fran Bull
2026-02-27 14:31:43 -08:00
committed by franbull
parent 6a19995f13
commit a4614d7d17
10 changed files with 486 additions and 37 deletions
+156
View File
@@ -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)
}
})
}
}