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 <james@tailscale.com>
pull/10189/head
James Tucker 1 year ago committed by James Tucker
parent 12d5c99b04
commit 73de6a1a95

@ -19,6 +19,7 @@ import (
"golang.org/x/net/dns/dnsmessage" "golang.org/x/net/dns/dnsmessage"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/views" "tailscale.com/types/views"
"tailscale.com/util/dnsname"
) )
// RouteAdvertiser is an interface that allows the AppConnector to advertise // 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 // domains is a map of lower case domain names with no trailing dot, to a
// list of resolved IP addresses. // list of resolved IP addresses.
domains map[string][]netip.Addr domains map[string][]netip.Addr
// wildcards is the list of domain strings that match subdomains.
wildcards []string
} }
// NewAppConnector creates a new AppConnector. // 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 // UpdateDomains replaces the current set of configured domains with the
// supplied set of domains. Domains must not contain a trailing dot, and should // 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) { func (e *AppConnector) UpdateDomains(domains []string) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
var old map[string][]netip.Addr var oldDomains map[string][]netip.Addr
old, e.domains = e.domains, make(map[string][]netip.Addr, len(domains)) oldDomains, e.domains = e.domains, make(map[string][]netip.Addr, len(domains))
for _, d := range domains { for _, d := range domains {
d = strings.ToLower(d) d = strings.ToLower(d)
e.domains[d] = old[d] if len(d) == 0 {
continue
} }
e.logf("handling domains: %v", xmaps.Keys(e.domains)) 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 and wildcards: %v", xmaps.Keys(e.domains), e.wildcards)
} }
// Domains returns the currently configured domain list. // Domains returns the currently configured domain list.
@ -134,15 +157,24 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
if len(domain) == 0 { if len(domain) == 0 {
return return
} }
if domain[len(domain)-1] == '.' { domain = strings.TrimSuffix(domain, ".")
domain = domain[:len(domain)-1]
}
domain = strings.ToLower(domain) domain = strings.ToLower(domain)
e.logf("[v2] observed DNS response for %s", domain) e.logf("[v2] observed DNS response for %s", domain)
e.mu.Lock() e.mu.Lock()
addrs, ok := e.domains[domain] 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() e.mu.Unlock()
if !ok { if !ok {
if err := p.SkipAnswer(); err != nil { if err := p.SkipAnswer(); err != nil {
return return

@ -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 // dnsResponse is a test helper that creates a DNS response buffer for the given domain and address
func dnsResponse(domain, address string) []byte { func dnsResponse(domain, address string) []byte {
addr := netip.MustParseAddr(address) addr := netip.MustParseAddr(address)

Loading…
Cancel
Save