WIP: rebase for 2026-05-18 #7

Draft
codinget wants to merge 234 commits from rebase/2026-05-18 into webnet
4 changed files with 394 additions and 84 deletions
Showing only changes of commit 290a6cc03c - Show all commits
+19 -10
View File
@@ -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
}
+45
View File
@@ -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{}
+65 -39
View File
@@ -569,12 +569,13 @@ func emptyIPSets() ipSets {
// which includes the policy.
// config is not safe for concurrent use.
type config struct {
isConfigured bool
apps []appctype.Conn25Attr
appsByName map[string]appctype.Conn25Attr
appNamesByDomain map[dnsname.FQDN][]string
selfDomains set.Set[dnsname.FQDN]
ipSets ipSets
isConfigured bool
apps []appctype.Conn25Attr
appsByName map[string]appctype.Conn25Attr
appNamesByDomain map[dnsname.FQDN][]string
appNamesByWCDomain map[dnsname.FQDN][]string
selfAppNames set.Set[string]
ipSets ipSets
}
func configFromNodeView(n tailcfg.NodeView) (*config, error) {
@@ -587,26 +588,36 @@ func configFromNodeView(n tailcfg.NodeView) (*config, error) {
}
selfTags := set.SetOf(n.Tags().AsSlice())
cfg := &config{
isConfigured: true,
apps: apps,
appsByName: map[string]appctype.Conn25Attr{},
appNamesByDomain: map[dnsname.FQDN][]string{},
selfDomains: set.Set[dnsname.FQDN]{},
ipSets: emptyIPSets(),
isConfigured: true,
apps: apps,
appsByName: map[string]appctype.Conn25Attr{},
appNamesByDomain: map[dnsname.FQDN][]string{},
appNamesByWCDomain: map[dnsname.FQDN][]string{},
selfAppNames: set.Set[string]{},
ipSets: emptyIPSets(),
}
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 {
fqdn, err := normalizeDNSName(d)
domain, isWild := strings.CutPrefix(d, "*.")
fqdn, err := normalizeDNSName(domain)
if err != nil {
return &config{}, err
}
mak.Set(&cfg.appNamesByDomain, fqdn, append(cfg.appNamesByDomain[fqdn], app.Name))
if selfMatchesTags {
cfg.selfDomains.Add(fqdn)
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.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
@@ -702,24 +713,42 @@ func (c *client) reconfig() {
c.v6TransitIPPool = newIPPool(ipSets.v6Transit)
}
// isConnectorDomain returns true if the domain is expected
// to be routed through a peer connector, but returns false
// if the self node is a connector responsible for routing the
// domain, and false in all other cases.
func (cfg *config) isConnectorDomain(domain dnsname.FQDN, prefsAdvertiseConnector bool) bool {
if prefsAdvertiseConnector && cfg.selfDomains.Contains(domain) {
return false
// getAppsForConnectorDomain returns the slice of app names which match the
// provided domain. Apps which match the domain exactly are preferred,
// otherwise the list of apps comes from the wildcard domain which matches
// the longest suffix of the specified domain. A nil or empty slice is returned
// if no match is found or if the list of matching apps would contain an app
// which is being handled by the self-node's connector.
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]
return ok && len(appNames) > 0
// If we have a candidate match, make sure that no candidate app is pointing
// 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
// 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
// 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() {
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,
magic: mip,
transit: tip,
app: app,
app: appName,
domain: domain,
}
if err := c.assignments.insert(as); err != nil {
@@ -962,16 +991,13 @@ func (c *Conn25) mapDNSResponse(buf []byte) []byte {
return buf
}
if !cfg.isConnectorDomain(queriedDomain, c.prefsAdvertiseConnector.Load()) {
return buf
}
appNames, _ := cfg.appNamesByDomain[queriedDomain]
appNames := cfg.getAppsForConnectorDomain(queriedDomain, c.prefsAdvertiseConnector.Load())
if len(appNames) == 0 {
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
// currently means we will:
@@ -981,7 +1007,7 @@ func (c *Conn25) mapDNSResponse(buf []byte) []byte {
var answers []dnsResponseRewrite
if question.Type != dnsmessage.TypeA && question.Type != dnsmessage.TypeAAAA {
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 {
c.logf("error writing empty response for unsupported type: %v", err)
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 {
c.logf("error rewriting dns response: %v", err)
return makeServFail(c.logf, hdr, question)
@@ -1074,7 +1100,7 @@ func (c *Conn25) mapDNSResponse(buf []byte) []byte {
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.EnableCompression()
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
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 {
return nil, err
}
+265 -35
View File
@@ -393,13 +393,11 @@ func TestHandleConnectorTransitIPRequest(t *testing.T) {
func TestReserveIPs(t *testing.T) {
c := newConn25(logger.Discard)
app := "a"
const appName = "a"
domainStr := "example.com."
mbd := map[dnsname.FQDN][]string{}
mbd["example.com."] = []string{app}
cfg := &config{
isConfigured: true,
appNamesByDomain: mbd,
isConfigured: true,
appsByName: map[string]appctype.Conn25Attr{appName: {}},
ipSets: ipSets{
v4Magic: mustIPSetFromPrefix("100.64.0.0/24"),
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) {
addrs, err := c.client.reserveAddresses(app, domain, tt.dst)
addrs, err := c.client.reserveAddresses(appName, domain, tt.dst)
if err != nil {
t.Fatal(err)
}
@@ -443,8 +441,8 @@ func TestReserveIPs(t *testing.T) {
if tt.wantTransit != addrs.transit {
t.Errorf("want %v, got %v", tt.wantTransit, addrs.transit)
}
if app != addrs.app {
t.Errorf("want %s, got %s", app, addrs.app)
if appName != addrs.app {
t.Errorf("want %s, got %s", appName, addrs.app)
}
if domain != addrs.domain {
t.Errorf("want %s, got %s", domain, addrs.domain)
@@ -488,13 +486,14 @@ func TestReconfig(t *testing.T) {
func TestConfigFromNodeView(t *testing.T) {
for _, tt := range []struct {
name string
rawCfg string
cfg []appctype.Conn25Attr
tags []string
wantErr bool
wantAppsByDomain map[dnsname.FQDN][]string
wantSelfDomains set.Set[dnsname.FQDN]
name string
rawCfg string
cfg []appctype.Conn25Attr
tags []string
wantErr bool
wantAppsByDomain map[dnsname.FQDN][]string
wantAppsByWCDomain map[dnsname.FQDN][]string
wantSelfAppNames set.Set[string]
}{
{
name: "bad-config",
@@ -512,7 +511,8 @@ func TestConfigFromNodeView(t *testing.T) {
"a.example.com.": {"one"},
"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",
@@ -532,7 +532,8 @@ func TestConfigFromNodeView(t *testing.T) {
"4.b.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",
@@ -545,6 +546,49 @@ func TestConfigFromNodeView(t *testing.T) {
"a.example.com.": {"one"},
"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) {
@@ -574,8 +618,114 @@ func TestConfigFromNodeView(t *testing.T) {
if diff := cmp.Diff(tt.wantAppsByDomain, c.appNamesByDomain); diff != "" {
t.Errorf("appsByDomain diff (-want, +got):\n%s", diff)
}
if diff := cmp.Diff(tt.wantSelfDomains, c.selfDomains); diff != "" {
t.Errorf("selfDomains diff (-want, +got):\n%s", diff)
if diff := cmp.Diff(tt.wantAppsByWCDomain, c.appNamesByWCDomain); 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) {
for _, tt := range []struct {
name string
appDomains []string
domain string
v4Addrs []*dnsmessage.AResource
v6Addrs []*dnsmessage.AAAAResource
@@ -761,9 +912,10 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
wantByMagicIP map[netip.Addr]addrs
}{
{
name: "one-ip-matches",
domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
name: "one-ip-matches",
appDomains: []string{"example.com"},
domain: "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"): {
@@ -776,8 +928,9 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
},
},
{
name: "v6-ip-matches",
domain: "example.com.",
name: "v6-ip-matches",
appDomains: []string{"example.com"},
domain: "example.com.",
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, 2}},
@@ -800,8 +953,9 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
},
},
{
name: "multiple-ip-matches",
domain: "example.com.",
name: "multiple-ip-matches",
appDomains: []string{"example.com"},
domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{
{A: [4]byte{1, 0, 0, 0}},
{A: [4]byte{2, 0, 0, 0}},
@@ -824,8 +978,9 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
},
},
{
name: "no-domain-match",
domain: "x.example.com.",
name: "no-domain-match",
appDomains: []string{"foo.example.com"},
domain: "bad.example.com.",
v4Addrs: []*dnsmessage.AResource{
{A: [4]byte{1, 0, 0, 0}},
{A: [4]byte{2, 0, 0, 0}},
@@ -833,16 +988,18 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
},
{
name: "no-rewrite-self-routed-domain",
appDomains: []string{"example.com"},
domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
selfTags: []string{"tag:woo"},
isEligibleConnector: true,
},
{
name: "rewrite-tagged-but-not-eligible-connector",
domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
selfTags: []string{"tag:woo"},
name: "rewrite-tagged-but-not-eligible-connector",
appDomains: []string{"example.com"},
domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
selfTags: []string{"tag:woo"},
// isEligibleConnector is false: tag matches but prefs not set,
// so DNS response should be rewritten normally.
wantByMagicIP: map[netip.Addr]addrs{
@@ -857,6 +1014,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
},
{
name: "rewrite-eligible-connector-no-matching-tag",
appDomains: []string{"example.com"},
domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
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) {
var dnsResp []byte
@@ -884,7 +1090,7 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
sn := makeSelfNode(t, []appctype.Conn25Attr{{
Name: "app1",
Connectors: []string{"tag:woo"},
Domains: []string{"example.com"},
Domains: tt.appDomains,
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10"), v4RangeFrom("20", "30")},
V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10"), v6RangeFrom("20", "30")},
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) {
for _, tt := range []struct {
name string
@@ -925,6 +1154,7 @@ func TestReserveAddressesDeduplicated(t *testing.T) {
},
} {
t.Run(tt.name, func(t *testing.T) {
const appName = "a"
conn25 := newConn25(t.Logf)
c := conn25.client
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.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 {
t.Fatal(err)
}
second, err := c.reserveAddresses("a", "example.com.", tt.dst)
second, err := c.reserveAddresses(appName, "example.com.", tt.dst)
if err != nil {
t.Fatal(err)
}