Skip to content

Commit 78637ab

Browse files
authored
Merge pull request #23 from netsec-ethz/dns_domains_fallback
Integrate the DNS search domain fallbacks 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. Added information about DNS Search Domain fallback, use of the whoami service and public DNS resolver fallback to README.
2 parents c088ae1 + 3eb20e3 commit 78637ab

File tree

4 files changed

+152
-9
lines changed

4 files changed

+152
-9
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,34 @@ It uses the following hinting mechanisms:
1313
- DNS-SD: DNS service discovery [RFC6763]
1414
- mDNS: multicast DNS [RFC6762]
1515

16+
If the host being bootstrapped has no DNS search domain set, the rDNS functionality of DNS (as described in RFC1035)
17+
is used to obtain a hostname and derive a search domain.
18+
A query for the name `reversed-external-ip.in-addr-servers.arpa.` (or `reversed-external-ip.ip6.arpa` in the case of
19+
IPv6) is sent to the default DNS resolver and resolved according to the delegation hierarchy.
20+
21+
---
22+
**_NOTE:_**
23+
In case there is no DNS search domain set on the host being bootstrapped **and**
24+
that host has no public IP address, the *whoami* DNS service on `akamai.net` is used to resolve an
25+
external IP.
26+
As a further fallback, in case a nameserver for `akamai.net` cannot be resolved, the public DNS
27+
resolver `9.9.9.9` provided by Quad9, headquartered in Switzerland and subject to Swiss privacy law, is used
28+
to obtain the address of further nameservers.
29+
30+
The only information reaching those services are the external IP of the host and the information
31+
that this host is using the *whoami* service to obtain that address.
32+
33+
All this is only a further fallback to provide zero-configuration bootstrapping even in misconfigured networks.
34+
35+
Calls to these two third-party services can be disabled entirely for a host by null routing their IP with the
36+
`ip route add 9.9.9.9 via 127.0.0.1 dev lo` command and adding the entry `127.0.0.1 akamai.net` to the hosts file.
37+
On the network level, calls to those fallbacks can be prevented by providing a proper DNS search domain configuration to
38+
the endhost using DHCP(v6) or IPv6 RAs. In split-horizon DNS settings, the response to the nameserver query for
39+
`akamai.net` can be shadowed if required to prevent the fallback.
40+
Note that other services on your system might rely on those.
41+
42+
---
43+
1644
It integrates with SCION by using the same OpenAPI as the control service uses
1745
for exposing TRCs (serving as root certificate) and the topology file
1846
(describing the local SCION topology).

hinting/dns_search_domain_fallbacks.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,106 @@
11
package hinting
22

33
import (
4+
"net"
5+
"net/netip"
46
"slices"
57
"strings"
68
)
79

10+
// getFallbackSearchDomains provides DNS search domain candidates to dnsChanWritable.
11+
// The results are only retrieved and returned if no DNS search domains were provided by
12+
// the dispatcher DNSInfo channel. The number of external entities contacted is minimized.
13+
// The domains are obtained from reverse DNS lookups and alternatively whois contact info,
14+
// and returned along with the resolvers learned from the dispatcher DNSInfo channel.
15+
func getFallbackSearchDomains(dnsChanWritable chan<- DNSInfo) {
16+
resolverSet := make(map[netip.Addr]struct{})
17+
searchDomainSet := make(map[string]struct{})
18+
// fallback for DNS search domains was started, so dnsInfoDispatcher hinting.dispatcher
19+
// was already started.
20+
dnsChanReadable := dispatcher.getDNSConfig()
21+
Fallback:
22+
for {
23+
select {
24+
case dnsInfo := <-dnsChanReadable:
25+
// collect info from happy path
26+
for _, resolver := range dnsInfo.resolvers {
27+
resolverSet[resolver] = struct{}{}
28+
}
29+
for _, searchDomain := range dnsInfo.searchDomains {
30+
searchDomainSet[searchDomain] = struct{}{}
31+
}
32+
case <-dnsInfoDone:
33+
// start with fallback
34+
break Fallback
35+
}
36+
}
37+
if len(searchDomainSet) > 0 {
38+
// do not attempt fallback as authoritative locally configured
39+
// search domains were found.
40+
return
41+
}
42+
43+
// Collect domain information from DNS reverse lookup
44+
ips := getPublicAddresses()
45+
if len(ips) == 0 {
46+
// attempt fallback to reverse lookup of externally observed IP,
47+
// if all configured IPs are private.
48+
ip, err := queryExternalIP()
49+
if err == nil {
50+
ips = append(ips, *ip)
51+
}
52+
}
53+
54+
for _, ip := range ips {
55+
domains := reverseLookupDomains(ip)
56+
for _, searchDomain := range domains {
57+
searchDomainSet[searchDomain] = struct{}{}
58+
}
59+
}
60+
61+
resolvers := make([]netip.Addr, 0, len(resolverSet))
62+
for k := range resolverSet {
63+
resolvers = append(resolvers, k)
64+
}
65+
searchDomains := make([]string, 0, len(searchDomainSet))
66+
for k := range searchDomainSet {
67+
searchDomains = append(searchDomains, k)
68+
}
69+
dnsInfo := DNSInfo{resolvers: resolvers, searchDomains: searchDomains}
70+
dnsInfoWriters.Add(1)
71+
select {
72+
case <-dnsInfoFallbackDone:
73+
// Ignore dnsInfo value, done publishing
74+
default:
75+
dnsChanWritable <- dnsInfo
76+
}
77+
dnsInfoWriters.Done()
78+
return
79+
}
80+
81+
func getPublicAddresses() (ips []netip.Addr) {
82+
ifaces, err := net.Interfaces()
83+
if err != nil {
84+
return
85+
}
86+
for _, iface := range ifaces {
87+
addrs, err := iface.Addrs()
88+
if err != nil {
89+
continue
90+
}
91+
for _, addr := range addrs {
92+
ip, err := netip.ParseAddr(addr.String())
93+
if err != nil {
94+
continue
95+
}
96+
if !ip.IsPrivate() {
97+
ips = append(ips, ip)
98+
}
99+
}
100+
}
101+
return
102+
}
103+
8104
func domainsFromHostnames(hostnames []string) (domains []string) {
9105
for _, hostname := range hostnames {
10106
labels := strings.Split(strings.TrimRight(hostname, "."), ".")

hinting/dns_search_domain_fallbacks_reverse.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package hinting
33
import (
44
"context"
55
"errors"
6-
"github.com/miekg/dns"
76
"math/rand"
87
"net"
98
"net/netip"
9+
"time"
10+
11+
"github.com/miekg/dns"
1012
)
1113

1214
var (
@@ -31,11 +33,18 @@ func reverseLookupDomains(addr netip.Addr) (domains []string) {
3133
func getAkaNS() (nameserver string, err error) {
3234
// try default resolver
3335
resolver := net.Resolver{}
34-
ctx := context.TODO()
36+
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(DNSInfoTimeoutFallback-DNSInfoTimeout))
37+
defer cancel()
3538
nameservers, err := resolver.LookupNS(ctx, akamaiDomain)
3639
if err == nil {
3740
return nameservers[rand.Intn(len(nameservers))].Host, err
3841
}
42+
if err, ok := err.(*net.DNSError); ok && err.IsNotFound {
43+
// Do not attempt further fallback to a public resolver.
44+
// We got a NXDOMAIN response or no NS type response. Since we know the NS record exists,
45+
// it must have been intentionally shadowed by the system default resolver.
46+
return nil, err
47+
}
3948

4049
m := new(dns.Msg)
4150
m.SetQuestion(akamaiDomain, dns.TypeNS)

hinting/hinting.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,17 @@ import (
2525
)
2626

2727
const (
28-
anapayaPEN = 55324 // Anapaya Systems Private Enterprise Number
29-
DiscoveryPort uint16 = 8041
30-
DNSInfoTimeout = 10 * time.Second
28+
anapayaPEN = 55324 // Anapaya Systems Private Enterprise Number
29+
DiscoveryPort uint16 = 8041
30+
DNSInfoTimeout = 8 * time.Second
31+
DNSInfoTimeoutFallback = 10 * time.Second
3132
)
3233

3334
var (
34-
dnsInfoChan = make(chan DNSInfo)
35-
dnsInfoDone = make(chan struct{})
36-
dnsInfoWriters sync.WaitGroup
35+
dnsInfoChan = make(chan DNSInfo)
36+
dnsInfoDone = make(chan struct{})
37+
dnsInfoFallbackDone = make(chan struct{})
38+
dnsInfoWriters sync.WaitGroup
3739

3840
dispatcher *dnsInfoDispatcher
3941
singleDispatcher = &sync.Mutex{}
@@ -161,15 +163,23 @@ func initDispatcher() (dnsChan <-chan DNSInfo) {
161163
}
162164
dispatcher = &dnsInfoDispatcher{}
163165
dnsChan = dispatcher.subscribe()
166+
// Start search domain fallback routine, listens for resolver IPs
167+
go getFallbackSearchDomains(dnsInfoChan)
164168
// Only start dispatcher when we have subscribers
165169
go dispatcher.publish()
166170
// Signal dnsInfoChan senders after timeout
167171
dnsInfoTimeout := time.After(DNSInfoTimeout)
172+
// Signal dnsInfoChan fallback senders after timeout
173+
dnsInfoTimeoutFallback := time.After(DNSInfoTimeoutFallback)
168174
go func() {
169175
select {
170176
case <-dnsInfoTimeout:
171-
// Signal senders
177+
// Signal senders about timeout
172178
close(dnsInfoDone)
179+
case <-dnsInfoTimeoutFallback:
180+
// Signal fallback about timeout
181+
close(dnsInfoFallbackDone)
182+
// Wait for remaining senders
173183
dnsInfoWriters.Wait()
174184
// Stop publishing new DNSInfo
175185
close(dnsInfoChan)

0 commit comments

Comments
 (0)