tka: keep the CompactionDefaults alongside the other limits #6
+18
-9
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user