tka: keep the CompactionDefaults alongside the other limits #6

Merged
codinget merged 216 commits from upstream/2026-05-18 into main 2026-05-18 21:22:49 +02:00
4 changed files with 394 additions and 84 deletions
Showing only changes of commit 290a6cc03c - Show all commits
+18 -9
View File
@@ -6,6 +6,7 @@ package appc
import ( import (
"cmp" "cmp"
"slices" "slices"
"strings"
"tailscale.com/ipn/ipnext" "tailscale.com/ipn/ipnext"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@@ -64,20 +65,28 @@ func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.
if err != nil { if err != nil {
return m 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()) selfTags := set.SetOf(self.Tags().AsSlice())
selfRoutedDomains := set.Set[string]{} selfRoutedDomains := set.Set[string]{}
for _, app := range apps { 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 { for _, tag := range app.Connectors {
domains := tagToDomain[tag] if tagToDomain[tag] == nil {
domains = slices.Grow(domains, len(app.Domains)) tagToDomain[tag] = set.Set[string]{}
for _, d := range app.Domains { }
tagToDomain[tag].AddSet(domains)
if isSelfEligibleConnector && selfTags.Contains(tag) { if isSelfEligibleConnector && selfTags.Contains(tag) {
selfRoutedDomains.Add(d) selfRoutedDomains.AddSet(domains)
} }
domains = append(domains, d)
}
tagToDomain[tag] = domains
} }
} }
// NodeIDs are Comparable, and we have a map of NodeID to NodeView anyway, so // 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() { for _, t := range peer.Tags().All() {
domains := tagToDomain[t] domains := tagToDomain[t]
for _, domain := range domains { for domain := range domains {
if selfRoutedDomains.Contains(domain) { if selfRoutedDomains.Contains(domain) {
continue continue
} }
+45
View File
@@ -32,6 +32,8 @@ func TestPickSplitDNSPeers(t *testing.T) {
appTwoBytes := getBytesForAttr("app2", []string{"a.example.com"}, []string{"tag:two"}) 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"}) 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"}) 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 { makeNodeView := func(id tailcfg.NodeID, name string, tags []string) tailcfg.NodeView {
return (&tailcfg.Node{ return (&tailcfg.Node{
@@ -192,6 +194,49 @@ func TestPickSplitDNSPeers(t *testing.T) {
"c.example.com": {nvp2, nvp4}, "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) { t.Run(tt.name, func(t *testing.T) {
selfNode := &tailcfg.Node{} selfNode := &tailcfg.Node{}
+54 -28
View File
@@ -573,7 +573,8 @@ type config struct {
apps []appctype.Conn25Attr apps []appctype.Conn25Attr
appsByName map[string]appctype.Conn25Attr appsByName map[string]appctype.Conn25Attr
appNamesByDomain map[dnsname.FQDN][]string appNamesByDomain map[dnsname.FQDN][]string
selfDomains set.Set[dnsname.FQDN] appNamesByWCDomain map[dnsname.FQDN][]string
selfAppNames set.Set[string]
ipSets ipSets ipSets ipSets
} }
@@ -591,22 +592,32 @@ func configFromNodeView(n tailcfg.NodeView) (*config, error) {
apps: apps, apps: apps,
appsByName: map[string]appctype.Conn25Attr{}, appsByName: map[string]appctype.Conn25Attr{},
appNamesByDomain: map[dnsname.FQDN][]string{}, appNamesByDomain: map[dnsname.FQDN][]string{},
selfDomains: set.Set[dnsname.FQDN]{}, appNamesByWCDomain: map[dnsname.FQDN][]string{},
selfAppNames: set.Set[string]{},
ipSets: emptyIPSets(), ipSets: emptyIPSets(),
} }
for _, app := range apps { for _, app := range apps {
selfMatchesTags := slices.ContainsFunc(app.Connectors, selfTags.Contains) normalizedDomains := set.Set[dnsname.FQDN]{}
normalizedWCDomains := set.Set[dnsname.FQDN]{}
for _, d := range app.Domains { for _, d := range app.Domains {
fqdn, err := normalizeDNSName(d) domain, isWild := strings.CutPrefix(d, "*.")
fqdn, err := normalizeDNSName(domain)
if err != nil { if err != nil {
return &config{}, err return &config{}, err
} }
if isWild && !normalizedWCDomains.Contains(fqdn) {
normalizedWCDomains.Add(fqdn)
mak.Set(&cfg.appNamesByWCDomain, fqdn, append(cfg.appNamesByWCDomain[fqdn], app.Name))
} else if !isWild && !normalizedDomains.Contains(fqdn) {
normalizedDomains.Add(fqdn)
mak.Set(&cfg.appNamesByDomain, fqdn, append(cfg.appNamesByDomain[fqdn], app.Name)) mak.Set(&cfg.appNamesByDomain, fqdn, append(cfg.appNamesByDomain[fqdn], app.Name))
if selfMatchesTags {
cfg.selfDomains.Add(fqdn)
} }
} }
mak.Set(&cfg.appsByName, app.Name, app) mak.Set(&cfg.appsByName, app.Name, app)
if slices.ContainsFunc(app.Connectors, selfTags.Contains) {
cfg.selfAppNames.Add(app.Name)
}
} }
// TODO(fran) 2026-03-18 we don't yet have a proper way to communicate the // TODO(fran) 2026-03-18 we don't yet have a proper way to communicate the
@@ -702,24 +713,42 @@ func (c *client) reconfig() {
c.v6TransitIPPool = newIPPool(ipSets.v6Transit) c.v6TransitIPPool = newIPPool(ipSets.v6Transit)
} }
// isConnectorDomain returns true if the domain is expected // getAppsForConnectorDomain returns the slice of app names which match the
// to be routed through a peer connector, but returns false // provided domain. Apps which match the domain exactly are preferred,
// if the self node is a connector responsible for routing the // otherwise the list of apps comes from the wildcard domain which matches
// domain, and false in all other cases. // the longest suffix of the specified domain. A nil or empty slice is returned
func (cfg *config) isConnectorDomain(domain dnsname.FQDN, prefsAdvertiseConnector bool) bool { // if no match is found or if the list of matching apps would contain an app
if prefsAdvertiseConnector && cfg.selfDomains.Contains(domain) { // which is being handled by the self-node's connector.
return false func (cfg *config) getAppsForConnectorDomain(domain dnsname.FQDN, prefsAdvertiseConnector bool) []string {
// Lookup exact matches first
appNames := cfg.appNamesByDomain[domain]
if len(appNames) == 0 {
// No exact match, check wildcard domains
// We have made the decision that wildcards will match the base domain.
// So example.com will be a match for *.example.com, because we think that
// this is most likely what users will expect.
for d := domain; d != ""; d = d.Parent() {
if appNames = cfg.appNamesByWCDomain[d]; len(appNames) > 0 {
break
}
}
} }
appNames, ok := cfg.appNamesByDomain[domain] // If we have a candidate match, make sure that no candidate app is pointing
return ok && len(appNames) > 0 // at a connector on the self-node.
if len(appNames) == 0 || (prefsAdvertiseConnector && slices.ContainsFunc(appNames, cfg.selfAppNames.Contains)) {
return nil
}
return appNames
} }
// reserveAddresses tries to make an assignment of addrs from the address pools // reserveAddresses tries to make an assignment of addrs from the address pools
// for this domain+dst address, so that this client can use conn25 connectors. // for this domain+dst address, so that this client can use conn25 connectors.
// The name of the matching app is also provided, no validation is done to check whether or not
// the app name refers to a configured app.
// It checks that this domain should be routed and that this client is not itself a connector for the domain // It checks that this domain should be routed and that this client is not itself a connector for the domain
// and generally if it is valid to make the assignment. // and generally if it is valid to make the assignment.
func (c *client) reserveAddresses(app string, domain dnsname.FQDN, dst netip.Addr) (addrs, error) { func (c *client) reserveAddresses(appName string, domain dnsname.FQDN, dst netip.Addr) (addrs, error) {
if !dst.IsValid() { if !dst.IsValid() {
return addrs{}, errors.New("dst is not valid") return addrs{}, errors.New("dst is not valid")
} }
@@ -756,7 +785,7 @@ func (c *client) reserveAddresses(app string, domain dnsname.FQDN, dst netip.Add
dst: dst, dst: dst,
magic: mip, magic: mip,
transit: tip, transit: tip,
app: app, app: appName,
domain: domain, domain: domain,
} }
if err := c.assignments.insert(as); err != nil { if err := c.assignments.insert(as); err != nil {
@@ -962,16 +991,13 @@ func (c *Conn25) mapDNSResponse(buf []byte) []byte {
return buf return buf
} }
if !cfg.isConnectorDomain(queriedDomain, c.prefsAdvertiseConnector.Load()) { appNames := cfg.getAppsForConnectorDomain(queriedDomain, c.prefsAdvertiseConnector.Load())
return buf
}
appNames, _ := cfg.appNamesByDomain[queriedDomain]
if len(appNames) == 0 { if len(appNames) == 0 {
return buf return buf
} }
// only reserve for first app
app := appNames[0] // There is guaranteed to be at least one matching app, so just take the first one for now
appName := appNames[0]
// Now we know this is a dns response we think we should rewrite, we're going to provide our response which // 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: // currently means we will:
@@ -981,7 +1007,7 @@ func (c *Conn25) mapDNSResponse(buf []byte) []byte {
var answers []dnsResponseRewrite var answers []dnsResponseRewrite
if question.Type != dnsmessage.TypeA && question.Type != dnsmessage.TypeAAAA { if question.Type != dnsmessage.TypeA && question.Type != dnsmessage.TypeAAAA {
c.logf("mapping dns response for connector domain, unsupported type: %v", question.Type) c.logf("mapping dns response for connector domain, unsupported type: %v", question.Type)
newBuf, err := c.client.rewriteDNSResponse(app, hdr, questions, answers) newBuf, err := c.client.rewriteDNSResponse(appName, hdr, questions, answers)
if err != nil { if err != nil {
c.logf("error writing empty response for unsupported type: %v", err) c.logf("error writing empty response for unsupported type: %v", err)
return makeServFail(c.logf, hdr, question) return makeServFail(c.logf, hdr, question)
@@ -1066,7 +1092,7 @@ func (c *Conn25) mapDNSResponse(buf []byte) []byte {
} }
} }
} }
newBuf, err := c.client.rewriteDNSResponse(app, hdr, questions, answers) newBuf, err := c.client.rewriteDNSResponse(appName, hdr, questions, answers)
if err != nil { if err != nil {
c.logf("error rewriting dns response: %v", err) c.logf("error rewriting dns response: %v", err)
return makeServFail(c.logf, hdr, question) return makeServFail(c.logf, hdr, question)
@@ -1074,7 +1100,7 @@ func (c *Conn25) mapDNSResponse(buf []byte) []byte {
return newBuf return newBuf
} }
func (c *client) rewriteDNSResponse(app string, hdr dnsmessage.Header, questions []dnsmessage.Question, answers []dnsResponseRewrite) ([]byte, error) { func (c *client) rewriteDNSResponse(appName string, hdr dnsmessage.Header, questions []dnsmessage.Question, answers []dnsResponseRewrite) ([]byte, error) {
b := dnsmessage.NewBuilder(nil, hdr) b := dnsmessage.NewBuilder(nil, hdr)
b.EnableCompression() b.EnableCompression()
if err := b.StartQuestions(); err != nil { if err := b.StartQuestions(); err != nil {
@@ -1091,7 +1117,7 @@ func (c *client) rewriteDNSResponse(app string, hdr dnsmessage.Header, questions
// make an answer for each rewrite // make an answer for each rewrite
for _, rw := range answers { for _, rw := range answers {
as, err := c.reserveAddresses(app, rw.domain, rw.dst) as, err := c.reserveAddresses(appName, rw.domain, rw.dst)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+246 -16
View File
@@ -393,13 +393,11 @@ func TestHandleConnectorTransitIPRequest(t *testing.T) {
func TestReserveIPs(t *testing.T) { func TestReserveIPs(t *testing.T) {
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
app := "a" const appName = "a"
domainStr := "example.com." domainStr := "example.com."
mbd := map[dnsname.FQDN][]string{}
mbd["example.com."] = []string{app}
cfg := &config{ cfg := &config{
isConfigured: true, isConfigured: true,
appNamesByDomain: mbd, appsByName: map[string]appctype.Conn25Attr{appName: {}},
ipSets: ipSets{ ipSets: ipSets{
v4Magic: mustIPSetFromPrefix("100.64.0.0/24"), v4Magic: mustIPSetFromPrefix("100.64.0.0/24"),
v6Magic: mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"), v6Magic: mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"),
@@ -430,7 +428,7 @@ func TestReserveIPs(t *testing.T) {
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
addrs, err := c.client.reserveAddresses(app, domain, tt.dst) addrs, err := c.client.reserveAddresses(appName, domain, tt.dst)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -443,8 +441,8 @@ func TestReserveIPs(t *testing.T) {
if tt.wantTransit != addrs.transit { if tt.wantTransit != addrs.transit {
t.Errorf("want %v, got %v", tt.wantTransit, addrs.transit) t.Errorf("want %v, got %v", tt.wantTransit, addrs.transit)
} }
if app != addrs.app { if appName != addrs.app {
t.Errorf("want %s, got %s", app, addrs.app) t.Errorf("want %s, got %s", appName, addrs.app)
} }
if domain != addrs.domain { if domain != addrs.domain {
t.Errorf("want %s, got %s", domain, addrs.domain) t.Errorf("want %s, got %s", domain, addrs.domain)
@@ -494,7 +492,8 @@ func TestConfigFromNodeView(t *testing.T) {
tags []string tags []string
wantErr bool wantErr bool
wantAppsByDomain map[dnsname.FQDN][]string wantAppsByDomain map[dnsname.FQDN][]string
wantSelfDomains set.Set[dnsname.FQDN] wantAppsByWCDomain map[dnsname.FQDN][]string
wantSelfAppNames set.Set[string]
}{ }{
{ {
name: "bad-config", name: "bad-config",
@@ -512,7 +511,8 @@ func TestConfigFromNodeView(t *testing.T) {
"a.example.com.": {"one"}, "a.example.com.": {"one"},
"b.example.com.": {"two"}, "b.example.com.": {"two"},
}, },
wantSelfDomains: set.SetOf([]dnsname.FQDN{"a.example.com."}), wantAppsByWCDomain: map[dnsname.FQDN][]string{},
wantSelfAppNames: set.SetOf([]string{"one"}),
}, },
{ {
name: "more-complex-with-connector-self-domains", name: "more-complex-with-connector-self-domains",
@@ -532,7 +532,8 @@ func TestConfigFromNodeView(t *testing.T) {
"4.b.example.com.": {"four"}, "4.b.example.com.": {"four"},
"4.d.example.com.": {"four"}, "4.d.example.com.": {"four"},
}, },
wantSelfDomains: set.SetOf([]dnsname.FQDN{"1.a.example.com.", "1.b.example.com.", "4.b.example.com.", "4.d.example.com."}), wantAppsByWCDomain: map[dnsname.FQDN][]string{},
wantSelfAppNames: set.SetOf([]string{"one", "four"}),
}, },
{ {
name: "eligible-connector-no-matching-tag-no-self-domains", name: "eligible-connector-no-matching-tag-no-self-domains",
@@ -545,6 +546,49 @@ func TestConfigFromNodeView(t *testing.T) {
"a.example.com.": {"one"}, "a.example.com.": {"one"},
"b.example.com.": {"two"}, "b.example.com.": {"two"},
}, },
wantAppsByWCDomain: map[dnsname.FQDN][]string{}},
{
name: "wildcard-collapse-and-deduplication",
cfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"*.example.com", "example.com"}, Connectors: []string{"tag:one"}},
{Name: "two", Domains: []string{"example.com", "sub.example.com"}, Connectors: []string{"tag:two"}},
},
tags: []string{"tag:one", "tag:two"},
wantAppsByDomain: map[dnsname.FQDN][]string{
"example.com.": {"one", "two"},
"sub.example.com.": {"two"},
},
wantAppsByWCDomain: map[dnsname.FQDN][]string{
"example.com.": {"one"},
},
wantSelfAppNames: set.SetOf([]string{"one", "two"}),
},
{
// Domain names that differ only in case must be treated as the same
// domain and the app name must appear exactly once in appNamesByDomain,
// not once per case variant.
name: "case-variant-exact-domains-deduplicated-within-app",
cfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"EXAMPLE.com", "example.COM", "Example.COM"}, Connectors: []string{"tag:one"}},
},
tags: []string{"tag:one"},
wantAppsByDomain: map[dnsname.FQDN][]string{
"example.com.": {"one"},
},
wantAppsByWCDomain: map[dnsname.FQDN][]string{},
wantSelfAppNames: set.SetOf([]string{"one"}),
},
{
// Same as above but for wildcard domains: *.EXAMPLE.com and *.example.COM
// must collapse to a single entry in appNamesByWCDomain.
name: "case-variant-wildcard-domains-deduplicated-within-app",
cfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"*.EXAMPLE.com", "*.example.COM"}, Connectors: []string{"tag:one"}},
},
tags: []string{"tag:one"},
wantAppsByDomain: map[dnsname.FQDN][]string{},
wantAppsByWCDomain: map[dnsname.FQDN][]string{"example.com.": {"one"}},
wantSelfAppNames: set.SetOf([]string{"one"}),
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -574,8 +618,114 @@ func TestConfigFromNodeView(t *testing.T) {
if diff := cmp.Diff(tt.wantAppsByDomain, c.appNamesByDomain); diff != "" { if diff := cmp.Diff(tt.wantAppsByDomain, c.appNamesByDomain); diff != "" {
t.Errorf("appsByDomain diff (-want, +got):\n%s", diff) t.Errorf("appsByDomain diff (-want, +got):\n%s", diff)
} }
if diff := cmp.Diff(tt.wantSelfDomains, c.selfDomains); diff != "" { if diff := cmp.Diff(tt.wantAppsByWCDomain, c.appNamesByWCDomain); diff != "" {
t.Errorf("selfDomains diff (-want, +got):\n%s", diff) t.Errorf("appsByWCDomain diff (-want, +got):\n%s", diff)
}
if diff := cmp.Diff(tt.wantSelfAppNames, c.selfAppNames); diff != "" {
t.Errorf("selfAppNames diff (-want, +got):\n%s", diff)
}
})
}
}
func TestGetAppsForDomainName(t *testing.T) {
defaultSN := makeSelfNode(
t,
[]appctype.Conn25Attr{
{Name: "one", Domains: []string{"*.example.com", "example.com"}, Connectors: []string{"tag:one"}},
{Name: "two", Domains: []string{"sub.example.com", "example.com"}, Connectors: []string{"tag:two"}},
{Name: "three", Domains: []string{"*.sub.example.com"}, Connectors: []string{"tag:three"}},
{Name: "four", Domains: []string{"a.sub.example.com"}, Connectors: []string{"tag:four"}},
{Name: "self-routed", Domains: []string{"*.wildcard.com", "exact-match.com"}, Connectors: []string{"tag:self-routed"}},
},
[]string{"tag:self-routed"},
)
for _, tt := range []struct {
name string
isConnector bool
domain dnsname.FQDN
wantApps []string
}{
{
name: "no-match",
domain: "nomatch.com.",
wantApps: nil,
},
{
name: "exact-match",
domain: "example.com.",
wantApps: []string{"one", "two"},
},
{
name: "wildcard-subdomain-match",
domain: "a.example.com.",
wantApps: []string{"one"},
},
{
name: "exact-subdomain-match",
domain: "sub.example.com.",
wantApps: []string{"two"},
},
{
name: "wildcard-sub-of-subdomain-match",
domain: "b.sub.example.com.",
wantApps: []string{"three"},
},
{
name: "exact-sub-of-subdomain-match",
domain: "a.sub.example.com.",
wantApps: []string{"four"},
},
{
name: "exact-domain-matches-wildcard",
domain: "wildcard.com.",
wantApps: []string{"self-routed"},
},
{
name: "self-routed-exact-domain-suppressed",
isConnector: true,
domain: "exact-match.com.",
wantApps: nil,
},
{
// Self node is an eligible connector for "wildcard-self-app" via
// *.wildcard.com, so the wildcard match must also be suppressed.
name: "self-routed-wildcard-domain-suppressed",
isConnector: true,
domain: "sub.wildcard.com.",
wantApps: nil,
},
{
// "other-app" is not on a self-connector tag, so it must not be suppressed.
name: "non-self-routed-domain-not-suppressed",
isConnector: true,
domain: "example.com.",
wantApps: []string{"one", "two"},
},
{
// Even though the app's connector tag matches the self node's tags,
// if the node is not an eligible connector (Advertise=false) then
// isSelfRoutedApp returns false and the domain is forwarded normally.
name: "not-eligible-connector-not-suppressed",
domain: "exact-match.com.",
wantApps: []string{"self-routed"},
},
} {
t.Run(tt.name, func(t *testing.T) {
c := newConn25(logger.Discard)
if tt.isConnector {
c.prefsAdvertiseConnector.Store(true)
}
cfg := mustConfig(t, defaultSN)
c.reconfig(cfg)
cfg, ok := c.getConfig()
if !ok {
t.Fatal("could not get config")
}
gotApps := cfg.getAppsForConnectorDomain(tt.domain, tt.isConnector)
if diff := cmp.Diff(tt.wantApps, gotApps); diff != "" {
t.Errorf("unexpected appNames result: diff (-want, +got):\n%s", diff)
} }
}) })
} }
@@ -753,6 +903,7 @@ func makeDNSResponseForSections(t *testing.T, questions []dnsmessage.Question, a
func TestMapDNSResponseAssignsAddrs(t *testing.T) { func TestMapDNSResponseAssignsAddrs(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
appDomains []string
domain string domain string
v4Addrs []*dnsmessage.AResource v4Addrs []*dnsmessage.AResource
v6Addrs []*dnsmessage.AAAAResource v6Addrs []*dnsmessage.AAAAResource
@@ -762,6 +913,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
}{ }{
{ {
name: "one-ip-matches", name: "one-ip-matches",
appDomains: []string{"example.com"},
domain: "example.com.", domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}}, v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
// these are 'expected' because they are the beginning of the provided pools // these are 'expected' because they are the beginning of the provided pools
@@ -777,6 +929,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
}, },
{ {
name: "v6-ip-matches", name: "v6-ip-matches",
appDomains: []string{"example.com"},
domain: "example.com.", domain: "example.com.",
v6Addrs: []*dnsmessage.AAAAResource{ v6Addrs: []*dnsmessage.AAAAResource{
{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}, {AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
@@ -801,6 +954,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
}, },
{ {
name: "multiple-ip-matches", name: "multiple-ip-matches",
appDomains: []string{"example.com"},
domain: "example.com.", domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{ v4Addrs: []*dnsmessage.AResource{
{A: [4]byte{1, 0, 0, 0}}, {A: [4]byte{1, 0, 0, 0}},
@@ -825,7 +979,8 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
}, },
{ {
name: "no-domain-match", name: "no-domain-match",
domain: "x.example.com.", appDomains: []string{"foo.example.com"},
domain: "bad.example.com.",
v4Addrs: []*dnsmessage.AResource{ v4Addrs: []*dnsmessage.AResource{
{A: [4]byte{1, 0, 0, 0}}, {A: [4]byte{1, 0, 0, 0}},
{A: [4]byte{2, 0, 0, 0}}, {A: [4]byte{2, 0, 0, 0}},
@@ -833,6 +988,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
}, },
{ {
name: "no-rewrite-self-routed-domain", name: "no-rewrite-self-routed-domain",
appDomains: []string{"example.com"},
domain: "example.com.", domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}}, v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
selfTags: []string{"tag:woo"}, selfTags: []string{"tag:woo"},
@@ -840,6 +996,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
}, },
{ {
name: "rewrite-tagged-but-not-eligible-connector", name: "rewrite-tagged-but-not-eligible-connector",
appDomains: []string{"example.com"},
domain: "example.com.", domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}}, v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
selfTags: []string{"tag:woo"}, selfTags: []string{"tag:woo"},
@@ -857,6 +1014,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
}, },
{ {
name: "rewrite-eligible-connector-no-matching-tag", name: "rewrite-eligible-connector-no-matching-tag",
appDomains: []string{"example.com"},
domain: "example.com.", domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}}, v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
selfTags: []string{"tag:unrelated"}, selfTags: []string{"tag:unrelated"},
@@ -873,6 +1031,54 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
}, },
}, },
}, },
{
name: "subdomain-matches-wildcard",
appDomains: []string{"*.example.com"},
domain: "sub.example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
// these are 'expected' because they are the beginning of the provided pools
wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("100.64.0.0"): {
domain: "sub.example.com.",
dst: netip.MustParseAddr("1.0.0.0"),
magic: netip.MustParseAddr("100.64.0.0"),
transit: netip.MustParseAddr("100.64.0.40"),
app: "app1",
},
},
},
{
name: "exact-subdomain-matches",
appDomains: []string{"example.com", "sub.example.com"},
domain: "sub.example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
// these are 'expected' because they are the beginning of the provided pools
wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("100.64.0.0"): {
domain: "sub.example.com.",
dst: netip.MustParseAddr("1.0.0.0"),
magic: netip.MustParseAddr("100.64.0.0"),
transit: netip.MustParseAddr("100.64.0.40"),
app: "app1",
},
},
},
{
name: "wildcard-subdomain-matches-subdomain",
appDomains: []string{"example.com", "*.sub.example.com"},
domain: "a.sub.example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
// these are 'expected' because they are the beginning of the provided pools
wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("100.64.0.0"): {
domain: "a.sub.example.com.",
dst: netip.MustParseAddr("1.0.0.0"),
magic: netip.MustParseAddr("100.64.0.0"),
transit: netip.MustParseAddr("100.64.0.40"),
app: "app1",
},
},
},
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var dnsResp []byte var dnsResp []byte
@@ -884,7 +1090,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
sn := makeSelfNode(t, []appctype.Conn25Attr{{ sn := makeSelfNode(t, []appctype.Conn25Attr{{
Name: "app1", Name: "app1",
Connectors: []string{"tag:woo"}, Connectors: []string{"tag:woo"},
Domains: []string{"example.com"}, Domains: tt.appDomains,
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10"), v4RangeFrom("20", "30")}, V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10"), v4RangeFrom("20", "30")},
V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10"), v6RangeFrom("20", "30")}, V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10"), v6RangeFrom("20", "30")},
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")}, V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
@@ -910,6 +1116,29 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
} }
} }
func TestNormalizedDNSNames(t *testing.T) {
tests := []struct {
name string
domain string
want dnsname.FQDN
}{
{name: "no-change", domain: "example.com.", want: "example.com."},
{name: "mixed-case", domain: "eXAmPle.COM", want: "example.com."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := normalizeDNSName(tt.domain)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if got != tt.want {
t.Errorf("Unexpected result, want %q, got %q", tt.want, got)
}
})
}
}
func TestReserveAddressesDeduplicated(t *testing.T) { func TestReserveAddressesDeduplicated(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
@@ -925,6 +1154,7 @@ func TestReserveAddressesDeduplicated(t *testing.T) {
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
const appName = "a"
conn25 := newConn25(t.Logf) conn25 := newConn25(t.Logf)
c := conn25.client c := conn25.client
c.v4MagicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24")) c.v4MagicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24"))
@@ -932,12 +1162,12 @@ func TestReserveAddressesDeduplicated(t *testing.T) {
c.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24")) c.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
c.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80")) c.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
first, err := c.reserveAddresses("a", "example.com.", tt.dst) first, err := c.reserveAddresses(appName, "example.com.", tt.dst)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
second, err := c.reserveAddresses("a", "example.com.", tt.dst) second, err := c.reserveAddresses(appName, "example.com.", tt.dst)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }