appc,ipn/ipnlocal: add app connector routes if any part of a CNAME chain is routed
If any domain along a CNAME chain matches any of the routed domains, add routes for the discovered domains. Fixes tailscale/corp#16928 Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:

committed by
James Tucker

parent
2aeef4e610
commit
e1a4b89dbe
@ -22,6 +22,7 @@ import (
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/execqueue"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// RouteAdvertiser is an interface that allows the AppConnector to advertise
|
||||
@ -206,7 +207,16 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
nextAnswer:
|
||||
// cnameChain tracks a chain of CNAMEs for a given query in order to reverse
|
||||
// a CNAME chain back to the original query for flattening. The keys are
|
||||
// CNAME record targets, and the value is the name the record answers, so
|
||||
// for www.example.com CNAME example.com, the map would contain
|
||||
// ["example.com"] = "www.example.com".
|
||||
var cnameChain map[string]string
|
||||
|
||||
// addressRecords is a list of address records found in the response.
|
||||
var addressRecords map[string][]netip.Addr
|
||||
|
||||
for {
|
||||
h, err := p.AnswerHeader()
|
||||
if err == dnsmessage.ErrSectionDone {
|
||||
@ -222,83 +232,147 @@ nextAnswer:
|
||||
}
|
||||
continue
|
||||
}
|
||||
if h.Type != dnsmessage.TypeA && h.Type != dnsmessage.TypeAAAA {
|
||||
|
||||
switch h.Type {
|
||||
case dnsmessage.TypeCNAME, dnsmessage.TypeA, dnsmessage.TypeAAAA:
|
||||
default:
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
domain := h.Name.String()
|
||||
domain := strings.TrimSuffix(strings.ToLower(h.Name.String()), ".")
|
||||
if len(domain) == 0 {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var addr netip.Addr
|
||||
if h.Type == dnsmessage.TypeCNAME {
|
||||
res, err := p.CNAMEResource()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cname := strings.TrimSuffix(strings.ToLower(res.CNAME.String()), ".")
|
||||
if len(cname) == 0 {
|
||||
continue
|
||||
}
|
||||
mak.Set(&cnameChain, cname, domain)
|
||||
continue
|
||||
}
|
||||
|
||||
switch h.Type {
|
||||
case dnsmessage.TypeA:
|
||||
r, err := p.AResource()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
addr = netip.AddrFrom4(r.A)
|
||||
addr := netip.AddrFrom4(r.A)
|
||||
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
|
||||
case dnsmessage.TypeAAAA:
|
||||
r, err := p.AAAAResource()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
addr = netip.AddrFrom16(r.AAAA)
|
||||
addr := netip.AddrFrom16(r.AAAA)
|
||||
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
|
||||
default:
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if slices.Contains(addrs, addr) {
|
||||
continue
|
||||
}
|
||||
for _, route := range e.controlRoutes {
|
||||
if route.Contains(addr) {
|
||||
// record the new address associated with the domain for faster matching in subsequent
|
||||
// requests and for diagnostic records.
|
||||
e.mu.Lock()
|
||||
e.domains[domain] = append(addrs, addr)
|
||||
e.mu.Unlock()
|
||||
continue nextAnswer
|
||||
}
|
||||
}
|
||||
if err := e.routeAdvertiser.AdvertiseRoute(netip.PrefixFrom(addr, addr.BitLen())); err != nil {
|
||||
e.logf("failed to advertise route for %s: %v: %v", domain, addr, err)
|
||||
continue
|
||||
}
|
||||
e.logf("[v2] advertised route for %v: %v", domain, addr)
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.domains[domain] = append(addrs, addr)
|
||||
e.mu.Unlock()
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
for domain, addrs := range addressRecords {
|
||||
domain, isRouted := e.findRoutedDomainLocked(domain, cnameChain)
|
||||
|
||||
// domain and none of the CNAMEs in the chain are routed
|
||||
if !isRouted {
|
||||
continue
|
||||
}
|
||||
|
||||
// advertise each address we have learned for the routed domain, that
|
||||
// was not already known.
|
||||
for _, addr := range addrs {
|
||||
e.logf("[v2] observed routed DNS response for %s: %s", domain, addr)
|
||||
if e.isAddrKnownLocked(domain, addr) {
|
||||
continue
|
||||
}
|
||||
|
||||
e.scheduleAdvertisement(domain, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// starting from the given domain that resolved to an address, find it, or any
|
||||
// of the domains in the CNAME chain toward resolving it, that are routed
|
||||
// domains, returning the routed domain name and a bool indicating whether a
|
||||
// routed domain was found.
|
||||
// e.mu must be held.
|
||||
func (e *AppConnector) findRoutedDomainLocked(domain string, cnameChain map[string]string) (string, bool) {
|
||||
var isRouted bool
|
||||
for {
|
||||
_, isRouted = e.domains[domain]
|
||||
if isRouted {
|
||||
break
|
||||
}
|
||||
|
||||
// match wildcard domains
|
||||
for _, wc := range e.wildcards {
|
||||
if dnsname.HasSuffix(domain, wc) {
|
||||
e.domains[domain] = nil
|
||||
isRouted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
next, ok := cnameChain[domain]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
domain = next
|
||||
}
|
||||
return domain, isRouted
|
||||
}
|
||||
|
||||
// isAddrKnownLocked returns true if the address is known to be associated with
|
||||
// the given domain. Known domain tables are updated for covered routes to speed
|
||||
// up future matches.
|
||||
// e.mu must be held.
|
||||
func (e *AppConnector) isAddrKnownLocked(domain string, addr netip.Addr) bool {
|
||||
if slices.Contains(e.domains[domain], addr) {
|
||||
return true
|
||||
}
|
||||
for _, route := range e.controlRoutes {
|
||||
if route.Contains(addr) {
|
||||
// record the new address associated with the domain for faster matching in subsequent
|
||||
// requests and for diagnostic records.
|
||||
e.domains[domain] = append(e.domains[domain], addr)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// scheduleAdvertisement schedules an advertisement of the given address
|
||||
// associated with the given domain.
|
||||
func (e *AppConnector) scheduleAdvertisement(domain string, addr netip.Addr) {
|
||||
e.queue.Add(func() {
|
||||
if err := e.routeAdvertiser.AdvertiseRoute(netip.PrefixFrom(addr, addr.BitLen())); err != nil {
|
||||
e.logf("failed to advertise route for %s: %v: %v", domain, addr, err)
|
||||
return
|
||||
}
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if !slices.Contains(e.domains[domain], addr) {
|
||||
e.logf("[v2] advertised route for %v: %v", domain, addr)
|
||||
e.domains[domain] = append(e.domains[domain], addr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user