Skip to content

Commit

Permalink
Integrate the DNS search domain fallbacks
Browse files Browse the repository at this point in the history
Make use of the DNS search domain fallbacks:
If no DNS search domain is discovered by the other hinters before the timeout is about to expire,
use the public IP(s) of the host and reverse DNS to discover candidate DNS search domains.
If there are no public IPs, use external DNS servers to discover publicly reachable IPs.
  • Loading branch information
FR4NK-W committed Oct 15, 2024
1 parent 6334048 commit f3ccde0
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 9 deletions.
96 changes: 96 additions & 0 deletions hinting/dns_search_domain_fallbacks.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,106 @@
package hinting

import (
"net"
"net/netip"
"slices"
"strings"
)

// getFallbackSearchDomains provides DNS search domain candidates to dnsChanWritable.
// The results are only retrieved and returned if no DNS search domains were provided by
// the dispatcher DNSInfo channel. The number of external entities contacted is minimized.
// The domains are obtained from reverse DNS lookups and alternatively whois contact info,
// and returned along with the resolvers learned from the dispatcher DNSInfo channel.
func getFallbackSearchDomains(dnsChanWritable chan<- DNSInfo) {
resolverSet := make(map[netip.Addr]struct{})
searchDomainSet := make(map[string]struct{})
// fallback for DNS search domains was started, so dnsInfoDispatcher hinting.dispatcher
// was already started.
dnsChanReadable := dispatcher.getDNSConfig()
Fallback:
for {
select {
case dnsInfo := <-dnsChanReadable:
// collect info from happy path
for _, resolver := range dnsInfo.resolvers {
resolverSet[resolver] = struct{}{}
}
for _, searchDomain := range dnsInfo.searchDomains {
searchDomainSet[searchDomain] = struct{}{}
}
case <-dnsInfoDone:
// start with fallback
break Fallback
}
}
if len(searchDomainSet) > 0 {
// do not attempt fallback as authoritative locally configured
// search domains were found.
return
}

// Collect domain information from DNS reverse lookup
ips := getPublicAddresses()
if len(ips) == 0 {
// attempt fallback to reverse lookup of externally observed IP,
// if all configured IPs are private.
ip, err := queryExternalIP()
if err == nil {
ips = append(ips, *ip)
}
}

for _, ip := range ips {
domains := reverseLookupDomains(ip)
for _, searchDomain := range domains {
searchDomainSet[searchDomain] = struct{}{}
}
}

resolvers := make([]netip.Addr, 0, len(resolverSet))
for k := range resolverSet {
resolvers = append(resolvers, k)
}
searchDomains := make([]string, 0, len(searchDomainSet))
for k := range searchDomainSet {
searchDomains = append(searchDomains, k)
}
dnsInfo := DNSInfo{resolvers: resolvers, searchDomains: searchDomains}
dnsInfoWriters.Add(1)
select {
case <-dnsInfoFallbackDone:
// Ignore dnsInfo value, done publishing
default:
dnsChanWritable <- dnsInfo
}
dnsInfoWriters.Done()
return
}

func getPublicAddresses() (ips []netip.Addr) {
ifaces, err := net.Interfaces()
if err != nil {
return
}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
ip, err := netip.ParseAddr(addr.String())
if err != nil {
continue
}
if !ip.IsPrivate() {
ips = append(ips, ip)
}
}
}
return
}

func domainsFromHostnames(hostnames []string) (domains []string) {
for _, hostname := range hostnames {
labels := strings.Split(strings.TrimRight(hostname, "."), ".")
Expand Down
7 changes: 5 additions & 2 deletions hinting/dns_search_domain_fallbacks_reverse.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package hinting
import (
"context"
"errors"
"github.com/miekg/dns"
"math/rand"
"net"
"net/netip"
"time"

"github.com/miekg/dns"
)

var (
Expand All @@ -31,7 +33,8 @@ func reverseLookupDomains(addr netip.Addr) (domains []string) {
func getAkaNS() (nameserver *string, err error) {
// try default resolver
resolver := net.Resolver{}
ctx := context.TODO()
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(DNSInfoTimeoutFallback-DNSInfoTimeout))
defer cancel()
nameservers, err := resolver.LookupNS(ctx, akamaiDomain)
if err == nil {
return &nameservers[rand.Intn(len(nameservers))].Host, err
Expand Down
24 changes: 17 additions & 7 deletions hinting/hinting.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ import (
)

const (
anapayaPEN = 55324 // Anapaya Systems Private Enterprise Number
DiscoveryPort uint16 = 8041
DNSInfoTimeout = 10 * time.Second
anapayaPEN = 55324 // Anapaya Systems Private Enterprise Number
DiscoveryPort uint16 = 8041
DNSInfoTimeout = 8 * time.Second
DNSInfoTimeoutFallback = 10 * time.Second
)

var (
dnsInfoChan = make(chan DNSInfo)
dnsInfoDone = make(chan struct{})
dnsInfoWriters sync.WaitGroup
dnsInfoChan = make(chan DNSInfo)
dnsInfoDone = make(chan struct{})
dnsInfoFallbackDone = make(chan struct{})
dnsInfoWriters sync.WaitGroup

dispatcher *dnsInfoDispatcher
singleDispatcher = &sync.Mutex{}
Expand Down Expand Up @@ -161,15 +163,23 @@ func initDispatcher() (dnsChan <-chan DNSInfo) {
}
dispatcher = &dnsInfoDispatcher{}
dnsChan = dispatcher.subscribe()
// Start search domain fallback routine, listens for resolver IPs
go getFallbackSearchDomains(dnsInfoChan)
// Only start dispatcher when we have subscribers
go dispatcher.publish()
// Signal dnsInfoChan senders after timeout
dnsInfoTimeout := time.After(DNSInfoTimeout)
// Signal dnsInfoChan fallback senders after timeout
dnsInfoTimeoutFallback := time.After(DNSInfoTimeoutFallback)
go func() {
select {
case <-dnsInfoTimeout:
// Signal senders
// Signal senders about timeout
close(dnsInfoDone)
case <-dnsInfoTimeoutFallback:
// Signal fallback about timeout
close(dnsInfoFallbackDone)
// Wait for remaining senders
dnsInfoWriters.Wait()
// Stop publishing new DNSInfo
close(dnsInfoChan)
Expand Down

0 comments on commit f3ccde0

Please sign in to comment.