mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 04:06:35 +02:00
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:
parent
bdf3419e7d
commit
290a6cc03c
@ -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{}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user