Skip to content

Commit 8ae3fd0

Browse files
committed
search domain discovery: add reverse DNS lookup fallback
Add fallback feature for discovering a valid search domain for the current host using DNS, by performing a reverse DNS lookup and deriving candidate search domains from the returned hostname. Add a fallback for the fallback: In case the current host does not have a public IP configured, obtain the externally visible (NAT, proxy) IP used for DNS by leveraging the Akamai whoami DNS service. https://www.akamai.com/blog/developers/introducing-new-whoami-tool-dns-resolver-information Add additional layers of fallbacks: In case there is no working DNS resolver configured, use the Quad9 public DNS resolver to obtain the IP of a default Akamai authoritative NS, or query for additional fallback authoritative NS. The number of external requests is minimized and the fallback paths involving additional 3rd parties are only taken if local DNS resolution is not working.
1 parent 7123af7 commit 8ae3fd0

File tree

4 files changed

+136
-1
lines changed

4 files changed

+136
-1
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ require (
77
github.com/mdlayher/ndp v0.10.0
88
github.com/miekg/dns v1.1.27
99
github.com/pelletier/go-toml v1.8.1-0.20200708110244-34de94e6a887
10+
github.com/stretchr/testify v1.6.1
1011
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
1112
golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf
1213
)
1314

1415
require (
1516
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
17+
github.com/davecgh/go-spew v1.1.1 // indirect
1618
github.com/go-stack/stack v1.8.0 // indirect
1719
github.com/mattn/go-colorable v0.1.8 // indirect
1820
github.com/mattn/go-isatty v0.0.12 // indirect
19-
github.com/stretchr/testify v1.6.1 // indirect
21+
github.com/pmezard/go-difflib v1.0.0 // indirect
2022
github.com/u-root/u-root v7.0.0+incompatible // indirect
2123
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f // indirect
2224
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
25+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
2326
)
2427

2528
replace github.com/insomniacslk/dhcp => github.com/stapelberg/dhcp v0.0.0-20190429172946-5244c0daddf0

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBc
5959
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
6060
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
6161
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
62+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
6263
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
6364
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
6465
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package hinting
2+
3+
import (
4+
"slices"
5+
"strings"
6+
)
7+
8+
func domainsFromHostnames(hostnames []string) (domains []string) {
9+
for _, hostname := range hostnames {
10+
labels := strings.Split(strings.TrimRight(hostname, "."), ".")
11+
// skip hostname label, not part of the search domain
12+
if len(labels) == 1 {
13+
domains = append(domains, "local")
14+
continue
15+
}
16+
domainString := ""
17+
slices.Reverse(labels)
18+
for _, label := range labels[:len(labels)-1] {
19+
if domainString == "" {
20+
domainString = label
21+
// do not add TLD to domains candidate list
22+
continue
23+
}
24+
domainString = strings.Join([]string{label, domainString}, ".")
25+
if !slices.Contains(domains, domainString) {
26+
domains = append(domains, domainString)
27+
}
28+
}
29+
}
30+
// order search domains by hostname from most specific to least specific,
31+
// a more specific search domains of an earlier hostname might sort after
32+
// a search domain derived from a later hostname.
33+
slices.Reverse(domains)
34+
return
35+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package hinting
2+
3+
import (
4+
"context"
5+
"errors"
6+
"github.com/miekg/dns"
7+
"math/rand"
8+
"net"
9+
"net/netip"
10+
)
11+
12+
var (
13+
akamaiDomain = "akamai.net."
14+
akamaiNameserver = "zh.akamaitech.net"
15+
quad9DNSResolver = "9.9.9.9:53"
16+
)
17+
18+
// reverseLookupDomains obtains the reverse DNS entries for IP addr to derive DNS search domain candidates.
19+
// Uses in-addr.arpa and ip6.arpa domains to lookup reverse pointer records.
20+
func reverseLookupDomains(addr netip.Addr) (domains []string) {
21+
hostnames, err := net.LookupAddr(addr.String())
22+
if err != nil {
23+
return
24+
}
25+
return domainsFromHostnames(hostnames)
26+
}
27+
28+
// Fallbacks to obtain an external public IP address using DNS.
29+
30+
// getAkaNS returns one random authoritative nameserver for akamai.net.
31+
func getAkaNS() (nameserver *string, err error) {
32+
// try default resolver
33+
resolver := net.Resolver{}
34+
ctx := context.TODO()
35+
nameservers, err := resolver.LookupNS(ctx, akamaiDomain)
36+
if err == nil {
37+
return &nameservers[rand.Intn(len(nameservers))].Host, err
38+
}
39+
40+
m := new(dns.Msg)
41+
m.SetQuestion(akamaiDomain, dns.TypeNS)
42+
// try Quad9
43+
in, err := dns.Exchange(m, quad9DNSResolver)
44+
if err != nil {
45+
return nil, err
46+
}
47+
if len(in.Answer) < 1 {
48+
err = errors.New("getAkaNS: No DNS RR answer")
49+
return nil, err
50+
}
51+
if ns, ok := in.Answer[rand.Intn(len(in.Answer))].(*dns.NS); ok {
52+
return &ns.Ns, nil
53+
}
54+
return nil, errors.New("getAkaNS: Invalid NS record")
55+
}
56+
57+
// getExternalIP returns the external IP used for DNS resolution of the executing host using nameserver.
58+
func getExternalIPbyNS(nameserver *string) (addr *netip.Addr, err error) {
59+
if nameserver == nil {
60+
// Default external authoritative nameserver
61+
nameserver = &akamaiNameserver
62+
}
63+
m := new(dns.Msg)
64+
// The akamai.net nameservers reply to queries for the name `whoami`
65+
// with the IP address of the host sending the query.
66+
m.SetQuestion("whoami.akamai.net.", dns.TypeA)
67+
in, err := dns.Exchange(m, net.JoinHostPort(*nameserver, "53"))
68+
if err != nil {
69+
return nil, err
70+
}
71+
if len(in.Answer) < 1 {
72+
err = errors.New("getExternalIP: No DNS RR answer")
73+
return nil, err
74+
}
75+
if a, ok := in.Answer[0].(*dns.A); ok {
76+
if addr, ok := netip.AddrFromSlice(a.A); ok {
77+
return &addr, nil
78+
}
79+
return nil, &net.AddrError{Err: "invalid IP address", Addr: a.A.String()}
80+
}
81+
return nil, errors.New("getExternalIP: Invalid A record")
82+
}
83+
84+
// queryExternalIP returns the external IP used for DNS resolution of the executing host.
85+
func queryExternalIP() (addr *netip.Addr, err error) {
86+
// Try with default NS
87+
addr, err = getExternalIPbyNS(nil)
88+
if err != nil {
89+
// try with looking up alternative NS
90+
ns, err := getAkaNS()
91+
if err == nil {
92+
addr, err = getExternalIPbyNS(ns)
93+
}
94+
}
95+
return
96+
}

0 commit comments

Comments
 (0)