WIP: rebase for 2026-05-18 #7
+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{}
|
||||
|
||||
+65
-39
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user