@ -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 ( )
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
}
}
domain := strings . TrimSuffix ( strings . ToLower ( h . Name . String ( ) ) , "." )
if len ( domain ) == 0 {
continue
}
e . mu . Unlock ( )
if ! ok {
if err := p . SkipAnswer ( ) ; err != nil {
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
}
var addr netip . Addr
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 ) {
}
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
}
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
// 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 )
continue
return
}
e . logf ( "[v2] advertised route for %v: %v" , domain , addr )
e . mu . Lock ( )
e . domains [ domain ] = append ( addrs , addr )
e . mu . Unlock ( )
}
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 )
}
} )
}