From 73de6a1a95f14a2cac4050a8a01d2873ef982ba3 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Wed, 8 Nov 2023 10:57:16 -0800 Subject: [PATCH] appc: add support for matching wildcard domains The app connector matches a configuration of "*.example.com" to mean any sub-domain of example.com. Updates #15437 Signed-off-by: James Tucker --- appc/appconnector.go | 48 ++++++++++++++++++++++++++++++++------- appc/appconnector_test.go | 16 +++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/appc/appconnector.go b/appc/appconnector.go index 17a642887..4c5c6bac6 100644 --- a/appc/appconnector.go +++ b/appc/appconnector.go @@ -19,6 +19,7 @@ import ( "golang.org/x/net/dns/dnsmessage" "tailscale.com/types/logger" "tailscale.com/types/views" + "tailscale.com/util/dnsname" ) // RouteAdvertiser is an interface that allows the AppConnector to advertise @@ -47,6 +48,9 @@ type AppConnector struct { // domains is a map of lower case domain names with no trailing dot, to a // list of resolved IP addresses. domains map[string][]netip.Addr + + // wildcards is the list of domain strings that match subdomains. + wildcards []string } // NewAppConnector creates a new AppConnector. @@ -59,18 +63,37 @@ func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser) *AppConn // UpdateDomains replaces the current set of configured domains with the // supplied set of domains. Domains must not contain a trailing dot, and should -// be lower case. +// be lower case. If the domain contains a leading '*' label it matches all +// subdomains of a domain. func (e *AppConnector) UpdateDomains(domains []string) { e.mu.Lock() defer e.mu.Unlock() - var old map[string][]netip.Addr - old, e.domains = e.domains, make(map[string][]netip.Addr, len(domains)) + var oldDomains map[string][]netip.Addr + oldDomains, e.domains = e.domains, make(map[string][]netip.Addr, len(domains)) for _, d := range domains { d = strings.ToLower(d) - e.domains[d] = old[d] + if len(d) == 0 { + continue + } + if strings.HasPrefix(d, "*.") { + e.wildcards = append(e.wildcards, d[2:]) + continue + } + e.domains[d] = oldDomains[d] + delete(oldDomains, d) + } + + // Ensure that still-live wildcards addresses are preserved as well. + for d, addrs := range oldDomains { + for _, wc := range e.wildcards { + if dnsname.HasSuffix(d, wc) { + e.domains[d] = addrs + break + } + } } - e.logf("handling domains: %v", xmaps.Keys(e.domains)) + e.logf("handling domains: %v and wildcards: %v", xmaps.Keys(e.domains), e.wildcards) } // Domains returns the currently configured domain list. @@ -134,15 +157,24 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) { if len(domain) == 0 { return } - if domain[len(domain)-1] == '.' { - domain = domain[:len(domain)-1] - } + domain = strings.TrimSuffix(domain, ".") domain = strings.ToLower(domain) e.logf("[v2] observed DNS response for %s", domain) e.mu.Lock() addrs, ok := e.domains[domain] + // match wildcard domains + if !ok { + for _, wc := range e.wildcards { + if dnsname.HasSuffix(domain, wc) { + e.domains[domain] = nil + ok = true + break + } + } + } e.mu.Unlock() + if !ok { if err := p.SkipAnswer(); err != nil { return diff --git a/appc/appconnector_test.go b/appc/appconnector_test.go index bf9f92859..fda7ae2b8 100644 --- a/appc/appconnector_test.go +++ b/appc/appconnector_test.go @@ -67,6 +67,22 @@ func TestObserveDNSResponse(t *testing.T) { } } +func TestWildcardDomains(t *testing.T) { + rc := &routeCollector{} + a := NewAppConnector(t.Logf, rc) + + a.UpdateDomains([]string{"*.example.com"}) + a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8")) + if got, want := rc.routes, []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}; !slices.Equal(got, want) { + t.Errorf("got %v; want %v", got, want) + } + + a.UpdateDomains([]string{"*.example.com", "example.com"}) + if _, ok := a.domains["foo.example.com"]; !ok { + t.Errorf("expected foo.example.com to be preserved in domains due to wildcard") + } +} + // dnsResponse is a test helper that creates a DNS response buffer for the given domain and address func dnsResponse(domain, address string) []byte { addr := netip.MustParseAddr(address)