mirror of
https://github.com/tailscale/tailscale.git
synced 2026-04-03 06:02:30 -04:00
check and populate self-routed domains
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"cmp"
|
||||
"slices"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/appctype"
|
||||
@@ -55,7 +56,7 @@ func PickConnector(nb ipnext.NodeBackend, app appctype.Conn25Attr) []tailcfg.Nod
|
||||
|
||||
// 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 {
|
||||
func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.NodeView, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView) map[string][]tailcfg.NodeView {
|
||||
var m map[string][]tailcfg.NodeView
|
||||
if !hasCap(AppConnectorsExperimentalAttrName) {
|
||||
return m
|
||||
@@ -64,12 +65,34 @@ func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.
|
||||
if err != nil {
|
||||
return m
|
||||
}
|
||||
|
||||
selfIsEligibleConnector := prefs.AppConnector().Advertise
|
||||
var selfRoutedDomains set.Set[string]
|
||||
|
||||
if selfIsEligibleConnector {
|
||||
for _, app := range apps {
|
||||
for _, tag := range app.Connectors {
|
||||
if self.Tags().ContainsFunc(func(t string) bool {
|
||||
return t == tag
|
||||
}) {
|
||||
if len(app.Domains) > 0 {
|
||||
if selfRoutedDomains == nil {
|
||||
selfRoutedDomains.Make()
|
||||
}
|
||||
selfRoutedDomains.AddSlice(app.Domains)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tagToDomain := make(map[string][]string)
|
||||
for _, app := range apps {
|
||||
for _, tag := range app.Connectors {
|
||||
tagToDomain[tag] = append(tagToDomain[tag], app.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]
|
||||
@@ -80,6 +103,9 @@ func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.
|
||||
for _, t := range peer.Tags().All() {
|
||||
domains := tagToDomain[t]
|
||||
for _, domain := range domains {
|
||||
if selfIsEligibleConnector && selfRoutedDomains.Contains(domain) {
|
||||
continue
|
||||
}
|
||||
if work[domain] == nil {
|
||||
mak.Set(&work, domain, set.Set[tailcfg.NodeID]{})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/appctype"
|
||||
@@ -47,10 +48,12 @@ func TestPickSplitDNSPeers(t *testing.T) {
|
||||
nvp4 := makeNodeView(4, "p4", []string{"tag:two", "tag:three2", "tag:four2"})
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
want map[string][]tailcfg.NodeView
|
||||
peers []tailcfg.NodeView
|
||||
config []tailcfg.RawMessage
|
||||
name string
|
||||
peers []tailcfg.NodeView
|
||||
config []tailcfg.RawMessage
|
||||
selfIsConnector bool
|
||||
selfTags []string
|
||||
want map[string][]tailcfg.NodeView
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
@@ -111,6 +114,59 @@ func TestPickSplitDNSPeers(t *testing.T) {
|
||||
"c.example.com": {nvp2, nvp4},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "self-connector-exclude-self-domains",
|
||||
config: []tailcfg.RawMessage{
|
||||
tailcfg.RawMessage(appOneBytes),
|
||||
tailcfg.RawMessage(appTwoBytes),
|
||||
tailcfg.RawMessage(appThreeBytes),
|
||||
tailcfg.RawMessage(appFourBytes),
|
||||
},
|
||||
peers: []tailcfg.NodeView{
|
||||
nvp1,
|
||||
nvp2,
|
||||
nvp3,
|
||||
nvp4,
|
||||
},
|
||||
selfIsConnector: true,
|
||||
selfTags: []string{"tag:three1"},
|
||||
want: map[string][]tailcfg.NodeView{
|
||||
// woo.b.example.com and hoo.b.example.com are covered
|
||||
// by tag:three1, and so is this self-node.
|
||||
// So those domains should not be routed to peers.
|
||||
// woo.b.example.com is also covered by another tag,
|
||||
// but still not included since this connector can route to it.
|
||||
"example.com": {nvp1},
|
||||
"a.example.com": {nvp3, nvp4},
|
||||
"c.example.com": {nvp2, nvp4},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "self-not-connector-but-tagged-include-self-domains",
|
||||
config: []tailcfg.RawMessage{
|
||||
tailcfg.RawMessage(appOneBytes),
|
||||
tailcfg.RawMessage(appTwoBytes),
|
||||
tailcfg.RawMessage(appThreeBytes),
|
||||
tailcfg.RawMessage(appFourBytes),
|
||||
},
|
||||
peers: []tailcfg.NodeView{
|
||||
nvp1,
|
||||
nvp2,
|
||||
nvp3,
|
||||
nvp4,
|
||||
},
|
||||
selfTags: []string{"tag:three1"},
|
||||
want: map[string][]tailcfg.NodeView{
|
||||
// Even though this self node has a tag for an app
|
||||
// it doesn't have Hostinfo.AppConnector == true, so
|
||||
// should still route through other connectors.
|
||||
"example.com": {nvp1},
|
||||
"a.example.com": {nvp3, nvp4},
|
||||
"woo.b.example.com": {nvp2, nvp3, nvp4},
|
||||
"hoo.b.example.com": {nvp3, nvp4},
|
||||
"c.example.com": {nvp2, nvp4},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selfNode := &tailcfg.Node{}
|
||||
@@ -119,6 +175,11 @@ func TestPickSplitDNSPeers(t *testing.T) {
|
||||
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): tt.config,
|
||||
}
|
||||
}
|
||||
prefs := ipn.Prefs{}
|
||||
if tt.selfIsConnector {
|
||||
prefs.AppConnector.Advertise = true
|
||||
}
|
||||
selfNode.Tags = append(selfNode.Tags, tt.selfTags...)
|
||||
selfView := selfNode.View()
|
||||
peers := map[tailcfg.NodeID]tailcfg.NodeView{}
|
||||
for _, p := range tt.peers {
|
||||
@@ -126,7 +187,7 @@ func TestPickSplitDNSPeers(t *testing.T) {
|
||||
}
|
||||
got := PickSplitDNSPeers(func(_ tailcfg.NodeCapability) bool {
|
||||
return true
|
||||
}, selfView, peers)
|
||||
}, selfView, peers, prefs.View())
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
@@ -521,7 +521,7 @@ func configFromNodeView(n tailcfg.NodeView) (config, error) {
|
||||
return config{}, err
|
||||
}
|
||||
mak.Set(&cfg.appNamesByDomain, fqdn, append(cfg.appNamesByDomain[fqdn], app.Name))
|
||||
if selfMatchesTags {
|
||||
if n.Hostinfo().AppConnector().EqualBool(true) && selfMatchesTags {
|
||||
cfg.selfRoutedDomains.Add(fqdn)
|
||||
}
|
||||
}
|
||||
@@ -850,6 +850,10 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
|
||||
return buf
|
||||
}
|
||||
|
||||
if c.config.selfRoutedDomains.Contains(queriedDomain) {
|
||||
return buf
|
||||
}
|
||||
|
||||
// Now we know this is a dns response we think we should rewrite, we're going to provide our response which
|
||||
// currently means we will:
|
||||
// * write the questions through as they are
|
||||
@@ -1023,6 +1027,7 @@ func (c *connector) lookupBySrcIPAndTransitIP(srcIP, transitIP netip.Addr) (appA
|
||||
return appAddr{}, false
|
||||
}
|
||||
v, ok := m[transitIP]
|
||||
|
||||
return v, ok
|
||||
}
|
||||
|
||||
|
||||
@@ -433,9 +433,11 @@ func TestReconfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
var hostInfo tailcfg.Hostinfo
|
||||
c := newConn25(logger.Discard)
|
||||
sn := (&tailcfg.Node{
|
||||
CapMap: capMap,
|
||||
CapMap: capMap,
|
||||
Hostinfo: hostInfo.View(),
|
||||
}).View()
|
||||
|
||||
err := c.reconfig(sn)
|
||||
@@ -454,6 +456,7 @@ func TestConfigReconfig(t *testing.T) {
|
||||
rawCfg string
|
||||
cfg []appctype.Conn25Attr
|
||||
tags []string
|
||||
isEligibleConnector bool
|
||||
wantErr bool
|
||||
wantAppsByDomain map[dnsname.FQDN][]string
|
||||
wantSelfRoutedDomains set.Set[dnsname.FQDN]
|
||||
@@ -469,7 +472,8 @@ func TestConfigReconfig(t *testing.T) {
|
||||
{Name: "one", Domains: []string{"a.example.com"}, Connectors: []string{"tag:one"}},
|
||||
{Name: "two", Domains: []string{"b.example.com"}, Connectors: []string{"tag:two"}},
|
||||
},
|
||||
tags: []string{"tag:one"},
|
||||
tags: []string{"tag:one"},
|
||||
isEligibleConnector: true,
|
||||
wantAppsByDomain: map[dnsname.FQDN][]string{
|
||||
"a.example.com.": {"one"},
|
||||
"b.example.com.": {"two"},
|
||||
@@ -477,7 +481,28 @@ func TestConfigReconfig(t *testing.T) {
|
||||
wantSelfRoutedDomains: set.SetOf([]dnsname.FQDN{"a.example.com."}),
|
||||
},
|
||||
{
|
||||
name: "more-complex",
|
||||
name: "more-complex-with-connector-self-routed-domains",
|
||||
cfg: []appctype.Conn25Attr{
|
||||
{Name: "one", Domains: []string{"1.a.example.com", "1.b.example.com"}, Connectors: []string{"tag:one", "tag:onea"}},
|
||||
{Name: "two", Domains: []string{"2.b.example.com", "2.c.example.com"}, Connectors: []string{"tag:two", "tag:twoa"}},
|
||||
{Name: "three", Domains: []string{"1.b.example.com", "1.c.example.com"}, Connectors: []string{}},
|
||||
{Name: "four", Domains: []string{"4.b.example.com", "4.d.example.com"}, Connectors: []string{"tag:four"}},
|
||||
},
|
||||
tags: []string{"tag:onea", "tag:four", "tag:unrelated"},
|
||||
isEligibleConnector: true,
|
||||
wantAppsByDomain: map[dnsname.FQDN][]string{
|
||||
"1.a.example.com.": {"one"},
|
||||
"1.b.example.com.": {"one", "three"},
|
||||
"1.c.example.com.": {"three"},
|
||||
"2.b.example.com.": {"two"},
|
||||
"2.c.example.com.": {"two"},
|
||||
"4.b.example.com.": {"four"},
|
||||
"4.d.example.com.": {"four"},
|
||||
},
|
||||
wantSelfRoutedDomains: set.SetOf([]dnsname.FQDN{"1.a.example.com.", "1.b.example.com.", "4.b.example.com.", "4.d.example.com."}),
|
||||
},
|
||||
{
|
||||
name: "more-complex-non-connector-no-self-routed-domains",
|
||||
cfg: []appctype.Conn25Attr{
|
||||
{Name: "one", Domains: []string{"1.a.example.com", "1.b.example.com"}, Connectors: []string{"tag:one", "tag:onea"}},
|
||||
{Name: "two", Domains: []string{"2.b.example.com", "2.c.example.com"}, Connectors: []string{"tag:two", "tag:twoa"}},
|
||||
@@ -494,7 +519,6 @@ func TestConfigReconfig(t *testing.T) {
|
||||
"4.b.example.com.": {"four"},
|
||||
"4.d.example.com.": {"four"},
|
||||
},
|
||||
wantSelfRoutedDomains: set.SetOf([]dnsname.FQDN{"1.a.example.com.", "1.b.example.com.", "4.b.example.com.", "4.d.example.com."}),
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -512,9 +536,14 @@ func TestConfigReconfig(t *testing.T) {
|
||||
capMap := tailcfg.NodeCapMap{
|
||||
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg,
|
||||
}
|
||||
var hostInfo tailcfg.Hostinfo
|
||||
if tt.isEligibleConnector {
|
||||
hostInfo.AppConnector.Set(true)
|
||||
}
|
||||
sn := (&tailcfg.Node{
|
||||
CapMap: capMap,
|
||||
Tags: tt.tags,
|
||||
CapMap: capMap,
|
||||
Tags: tt.tags,
|
||||
Hostinfo: hostInfo.View(),
|
||||
}).View()
|
||||
c, err := configFromNodeView(sn)
|
||||
if (err != nil) != tt.wantErr {
|
||||
@@ -530,7 +559,7 @@ func TestConfigReconfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, tags []string) tailcfg.NodeView {
|
||||
func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, tags []string, isConnector bool) tailcfg.NodeView {
|
||||
t.Helper()
|
||||
cfg := make([]tailcfg.RawMessage, 0, len(attrs))
|
||||
for i, attr := range attrs {
|
||||
@@ -543,9 +572,14 @@ func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, tags []string) tail
|
||||
capMap := tailcfg.NodeCapMap{
|
||||
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg,
|
||||
}
|
||||
|
||||
var hostInfo tailcfg.Hostinfo
|
||||
hostInfo.AppConnector.Set(isConnector)
|
||||
|
||||
return (&tailcfg.Node{
|
||||
CapMap: capMap,
|
||||
Tags: tags,
|
||||
CapMap: capMap,
|
||||
Tags: tags,
|
||||
Hostinfo: hostInfo.View(),
|
||||
}).View()
|
||||
}
|
||||
|
||||
@@ -683,6 +717,8 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
|
||||
name string
|
||||
domain string
|
||||
addrs []*dnsmessage.AResource
|
||||
selfTags []string
|
||||
isConnector bool
|
||||
wantByMagicIP map[netip.Addr]addrs
|
||||
}{
|
||||
{
|
||||
@@ -732,6 +768,13 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
|
||||
{A: [4]byte{2, 0, 0, 0}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exclude-connector-self-routed-domain",
|
||||
domain: "example.com.",
|
||||
addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
|
||||
selfTags: []string{"tag:woo"},
|
||||
isConnector: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dnsResp := makeDNSResponse(t, tt.domain, tt.addrs)
|
||||
@@ -741,7 +784,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
|
||||
Domains: []string{"example.com"},
|
||||
MagicIPPool: []netipx.IPRange{rangeFrom("0", "10"), rangeFrom("20", "30")},
|
||||
TransitIPPool: []netipx.IPRange{rangeFrom("40", "50")},
|
||||
}}, []string{})
|
||||
}}, tt.selfTags, tt.isConnector)
|
||||
c := newConn25(logger.Discard)
|
||||
c.reconfig(sn)
|
||||
|
||||
@@ -886,7 +929,7 @@ func TestAddressAssignmentIsHandled(t *testing.T) {
|
||||
Name: "app1",
|
||||
Connectors: []string{"tag:woo"},
|
||||
Domains: []string{"example.com"},
|
||||
}}, []string{})
|
||||
}}, []string{}, false)
|
||||
err := ext.conn25.reconfig(sn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -966,7 +1009,7 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) {
|
||||
Domains: []string{configuredDomain},
|
||||
MagicIPPool: []netipx.IPRange{rangeFrom("0", "10")},
|
||||
TransitIPPool: []netipx.IPRange{rangeFrom("40", "50")},
|
||||
}}, []string{})
|
||||
}}, []string{}, false)
|
||||
|
||||
compareToRecords := func(t *testing.T, resources []dnsmessage.Resource, want []netip.Addr) {
|
||||
t.Helper()
|
||||
@@ -1280,7 +1323,7 @@ func TestHandleAddressAssignmentStoresTransitIPs(t *testing.T) {
|
||||
Connectors: []string{"tag:hoo"},
|
||||
Domains: []string{"hoo.example.com"},
|
||||
},
|
||||
}, []string{})
|
||||
}, []string{}, false)
|
||||
err := ext.conn25.reconfig(sn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -1459,7 +1502,7 @@ func TestTransitIPConnMapping(t *testing.T) {
|
||||
func TestClientTransitIPForMagicIP(t *testing.T) {
|
||||
sn := makeSelfNode(t, []appctype.Conn25Attr{{
|
||||
MagicIPPool: []netipx.IPRange{rangeFrom("0", "10")}, // 100.64.0.0 - 100.64.0.10
|
||||
}}, []string{})
|
||||
}}, []string{}, false)
|
||||
mappedMip := netip.MustParseAddr("100.64.0.0")
|
||||
mappedTip := netip.MustParseAddr("169.0.0.0")
|
||||
unmappedMip := netip.MustParseAddr("100.64.0.1")
|
||||
@@ -1512,7 +1555,7 @@ func TestClientTransitIPForMagicIP(t *testing.T) {
|
||||
func TestConnectorRealIPForTransitIPConnection(t *testing.T) {
|
||||
sn := makeSelfNode(t, []appctype.Conn25Attr{{
|
||||
TransitIPPool: []netipx.IPRange{rangeFrom("40", "50")}, // 100.64.0.40 - 100.64.0.50
|
||||
}}, []string{})
|
||||
}}, []string{}, true)
|
||||
mappedSrc := netip.MustParseAddr("100.0.0.1")
|
||||
unmappedSrc := netip.MustParseAddr("100.0.0.2")
|
||||
mappedTip := netip.MustParseAddr("100.64.0.41")
|
||||
|
||||
@@ -864,7 +864,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
|
||||
addSplitDNSRoutes(nm.DNS.Routes)
|
||||
|
||||
// Add split DNS routes for conn25
|
||||
conn25DNSTargets := appc.PickSplitDNSPeers(nm.HasCap, nm.SelfNode, peers)
|
||||
conn25DNSTargets := appc.PickSplitDNSPeers(nm.HasCap, nm.SelfNode, peers, prefs)
|
||||
if conn25DNSTargets != nil {
|
||||
var m map[string][]*dnstype.Resolver
|
||||
for domain, candidateSplitDNSPeers := range conn25DNSTargets {
|
||||
|
||||
Reference in New Issue
Block a user