mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-16 10:38:39 +00:00
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>
This commit is contained in:
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
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(d, "*.") {
|
||||||
|
e.wildcards = append(e.wildcards, d[2:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.domains[d] = oldDomains[d]
|
||||||
|
delete(oldDomains, d)
|
||||||
}
|
}
|
||||||
e.logf("handling domains: %v", xmaps.Keys(e.domains))
|
|
||||||
|
// 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…
x
Reference in New Issue
Block a user