appc, feature/conn25: handle exact and wildcard domains correctly (#19202)
Installed SplitDNS routes are always treated as wildcard domains, so the domains that we pass to the local resolver should be normalized and have any leading *. wildcard prefix removed. When looking at DNS responses to see if the domain matches, we need to consider both exact matches and wildcard matches. We now keep separate maps of exact-match domains and wildcard domains, and when we match we check to see if there's a match in the exact-match map, otherwise we check against the wild card match map until we find a match, removing a label after each check. Rather than looking for matching self-hosted domains (domains serviced by the connector being run on the self-node), the apps that are being serviced by the connector on the self-node are tracked instead. When checking to see if a DNS response should be rewritten, it is ignored if any of the matching apps for the domain are in the self-hosted apps set. Fixes tailscale/corp#39272 Signed-off-by: George Jones <george@tailscale.com>
This commit is contained in:
+19
-10
@@ -6,6 +6,7 @@ package appc
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -64,20 +65,28 @@ func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.
|
||||
if err != nil {
|
||||
return m
|
||||
}
|
||||
tagToDomain := make(map[string][]string)
|
||||
|
||||
// We strip the leading *. from any domains because the OS treats all domains
|
||||
// that we pass to it as wildcard domains, and the OS would treat the * character
|
||||
// as a literal domain component instead of treating it as a wildcard.
|
||||
// We also use a Set to deduplicate the domains we pass to the OS in case removing
|
||||
// the *. prefix resulted in duplicate entries.
|
||||
tagToDomain := make(map[string]set.Set[string])
|
||||
selfTags := set.SetOf(self.Tags().AsSlice())
|
||||
selfRoutedDomains := set.Set[string]{}
|
||||
for _, app := range apps {
|
||||
domains := make(set.Set[string])
|
||||
for _, domain := range app.Domains {
|
||||
domains.Add(strings.ToLower(strings.TrimPrefix(domain, "*.")))
|
||||
}
|
||||
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)
|
||||
if tagToDomain[tag] == nil {
|
||||
tagToDomain[tag] = set.Set[string]{}
|
||||
}
|
||||
tagToDomain[tag].AddSet(domains)
|
||||
if isSelfEligibleConnector && selfTags.Contains(tag) {
|
||||
selfRoutedDomains.AddSet(domains)
|
||||
}
|
||||
tagToDomain[tag] = domains
|
||||
}
|
||||
}
|
||||
// NodeIDs are Comparable, and we have a map of NodeID to NodeView anyway, so
|
||||
@@ -89,7 +98,7 @@ func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.
|
||||
}
|
||||
for _, t := range peer.Tags().All() {
|
||||
domains := tagToDomain[t]
|
||||
for _, domain := range domains {
|
||||
for domain := range domains {
|
||||
if selfRoutedDomains.Contains(domain) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ func TestPickSplitDNSPeers(t *testing.T) {
|
||||
appTwoBytes := getBytesForAttr("app2", []string{"a.example.com"}, []string{"tag:two"})
|
||||
appThreeBytes := getBytesForAttr("app3", []string{"woo.b.example.com", "hoo.b.example.com"}, []string{"tag:three1", "tag:three2"})
|
||||
appFourBytes := getBytesForAttr("app4", []string{"woo.b.example.com", "c.example.com"}, []string{"tag:four1", "tag:four2"})
|
||||
appFiveBytes := getBytesForAttr("app5", []string{"*.example.com", "example.com"}, []string{"tag:one"})
|
||||
appSixBytes := getBytesForAttr("app6", []string{"*.Example.com", "EXAMPLE.com", "EXAMPLE.COM"}, []string{"tag:one"})
|
||||
|
||||
makeNodeView := func(id tailcfg.NodeID, name string, tags []string) tailcfg.NodeView {
|
||||
return (&tailcfg.Node{
|
||||
@@ -192,6 +194,49 @@ func TestPickSplitDNSPeers(t *testing.T) {
|
||||
"c.example.com": {nvp2, nvp4},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcards-are-stripped-and-deduped",
|
||||
config: []tailcfg.RawMessage{
|
||||
tailcfg.RawMessage(appOneBytes),
|
||||
tailcfg.RawMessage(appFiveBytes),
|
||||
},
|
||||
peers: []tailcfg.NodeView{
|
||||
nvp1,
|
||||
},
|
||||
want: map[string][]tailcfg.NodeView{
|
||||
// All the domains should be normalized to example.com
|
||||
"example.com": {nvp1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "domains-are-normalized-and-deduped",
|
||||
config: []tailcfg.RawMessage{
|
||||
tailcfg.RawMessage(appSixBytes),
|
||||
},
|
||||
peers: []tailcfg.NodeView{
|
||||
nvp1,
|
||||
},
|
||||
want: map[string][]tailcfg.NodeView{
|
||||
// All the domains should be normalized to example.com
|
||||
"example.com": {nvp1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sub-domains-and-top-domains-do-not-collide",
|
||||
config: []tailcfg.RawMessage{
|
||||
tailcfg.RawMessage(appTwoBytes),
|
||||
tailcfg.RawMessage(appFiveBytes),
|
||||
},
|
||||
peers: []tailcfg.NodeView{
|
||||
nvp1,
|
||||
nvp3,
|
||||
},
|
||||
want: map[string][]tailcfg.NodeView{
|
||||
// The sub.example.com should remain distinct from example.com
|
||||
"example.com": {nvp1},
|
||||
"a.example.com": {nvp3},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selfNode := &tailcfg.Node{}
|
||||
|
||||
Reference in New Issue
Block a user