Skip to content

Commit c088ae1

Browse files
authored
Merge pull request #22 from netsec-ethz/dns_domains_fallback_reverse
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.
2 parents 414f9d0 + 1d3fda3 commit c088ae1

File tree

5 files changed

+271
-1
lines changed

5 files changed

+271
-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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
domain := ""
17+
slices.Reverse(labels)
18+
for _, label := range labels[:len(labels)-1] {
19+
if domain == "" {
20+
domain = label
21+
// do not add TLD to domains candidate list
22+
continue
23+
}
24+
domain = strings.Join([]string{label, domain}, ".")
25+
// Filter out Effective ccTLDs (of the form "co.uk"), as we are only interested in the ETLD+1 domains
26+
if 5 == len(domain) && // TODO: complete TLD specific exceptions, or directly use PSL
27+
(label == "co" ||
28+
label == "ac" ||
29+
label == "re" ||
30+
label == "ne") {
31+
// do not add country-code second-level ETLD domains to domains candidate list
32+
continue
33+
}
34+
if !slices.Contains(domains, domain) {
35+
domains = append(domains, domain)
36+
}
37+
}
38+
}
39+
// order search domains by hostname from most specific to least specific,
40+
// a more specific search domain of an earlier hostname might sort after
41+
// a search domain derived from a later hostname.
42+
slices.Reverse(domains)
43+
return
44+
}
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 "", err
46+
}
47+
if len(in.Answer) < 1 {
48+
err = errors.New("getAkaNS: No DNS RR answer")
49+
return "", err
50+
}
51+
if ns, ok := in.Answer[rand.Intn(len(in.Answer))].(*dns.NS); ok {
52+
return ns.Ns, nil
53+
}
54+
return "", 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 getExternalIP(nameserver string) (addr netip.Addr, err error) {
59+
if nameserver == "" {
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 netip.Addr{}, err
70+
}
71+
if len(in.Answer) < 1 {
72+
err = errors.New("getExternalIP: No DNS RR answer")
73+
return netip.Addr{}, 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 netip.Addr{}, &net.AddrError{Err: "invalid IP address", Addr: a.A.String()}
80+
}
81+
return netip.Addr{}, 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 = getExternalIP("")
88+
if err != nil {
89+
// try with looking up alternative NS
90+
ns, err := getAkaNS()
91+
if err == nil {
92+
addr, err = getExternalIP(ns)
93+
}
94+
}
95+
return
96+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package hinting
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"net/netip"
6+
"testing"
7+
)
8+
9+
func TestReverseLookupDomains(t *testing.T) {
10+
testCases := []struct {
11+
name string
12+
values []struct {
13+
ip netip.Addr
14+
domain string
15+
}
16+
}{
17+
{
18+
name: "ETHZ",
19+
values: []struct {
20+
ip netip.Addr
21+
domain string
22+
}{
23+
{ip: netip.MustParseAddr("129.132.19.216"), domain: "ethz.ch"},
24+
{ip: netip.MustParseAddr("82.130.64.0"), domain: "ethz.ch"},
25+
{ip: netip.MustParseAddr("148.187.192.0"), domain: "ethz.ch"},
26+
},
27+
},
28+
{
29+
name: "PU",
30+
values: []struct {
31+
ip netip.Addr
32+
domain string
33+
}{
34+
{ip: netip.MustParseAddr("66.180.178.131"), domain: "princeton.edu"},
35+
},
36+
},
37+
{
38+
name: "VU",
39+
values: []struct {
40+
ip netip.Addr
41+
domain string
42+
}{
43+
{ip: netip.MustParseAddr("128.143.33.137"), domain: "virginia.edu"},
44+
{ip: netip.MustParseAddr("128.143.33.144"), domain: "virginia.edu"},
45+
},
46+
},
47+
{
48+
name: "SWITCH",
49+
values: []struct {
50+
ip netip.Addr
51+
domain string
52+
}{
53+
{ip: netip.MustParseAddr("130.59.31.80"), domain: "switch.ch"},
54+
},
55+
},
56+
{
57+
name: "KREONET",
58+
values: []struct {
59+
ip netip.Addr
60+
domain string
61+
}{
62+
{ip: netip.MustParseAddr("134.75.254.11"), domain: "kreonet.net"},
63+
{ip: netip.MustParseAddr("134.75.254.12"), domain: "kreonet.net"},
64+
},
65+
},
66+
{
67+
name: "KU",
68+
values: []struct {
69+
ip netip.Addr
70+
domain string
71+
}{
72+
{ip: netip.MustParseAddr("163.152.6.10"), domain: "korea.ac.kr"},
73+
},
74+
},
75+
}
76+
77+
for _, tc := range testCases {
78+
t.Log(tc.name)
79+
for _, v := range tc.values {
80+
t.Log(v.ip)
81+
res := reverseLookupDomains(v.ip)
82+
t.Log(res)
83+
assert.Subset(t, res, []string{v.domain}, "")
84+
}
85+
}
86+
}
87+
88+
func TestDomainsFromHostnamesDerivation(t *testing.T) {
89+
testCases := []struct {
90+
name string
91+
values []struct {
92+
hostnames []string
93+
domains []string
94+
}
95+
}{
96+
{
97+
name: "ETHZ",
98+
values: []struct {
99+
hostnames []string
100+
domains []string
101+
}{
102+
{hostnames: []string{"82-130-64-0.net4.ethz.ch."}, domains: []string{"net4.ethz.ch", "ethz.ch"}},
103+
{hostnames: []string{"service-id-api-cd-dcz1-server-4-b.ethz.ch."}, domains: []string{"ethz.ch"}},
104+
},
105+
},
106+
{
107+
name: "ETHZ",
108+
values: []struct {
109+
hostnames []string
110+
domains []string
111+
}{
112+
{hostnames: []string{"60.korea.ac.kr.", "sub.korea.ac.kr."}, domains: []string{"korea.ac.kr"}},
113+
},
114+
},
115+
}
116+
117+
for _, tc := range testCases {
118+
t.Log(tc.name)
119+
for _, v := range tc.values {
120+
t.Log(v.hostnames)
121+
res := domainsFromHostnames(v.hostnames)
122+
t.Log(res)
123+
assert.EqualValues(t, v.domains, res, "")
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)