Look up A and AAAA in parallel (#1760)
This allows validating IPv6-only hosts. Fixes #593.
This commit is contained in:
parent
a41b1dd091
commit
339405bcb9
99
bdns/dns.go
99
bdns/dns.go
|
@ -10,6 +10,7 @@ import (
|
|||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jmhodges/clock"
|
||||
|
@ -18,6 +19,14 @@ import (
|
|||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func parseCidr(network string, comment string) net.IPNet {
|
||||
_, net, err := net.ParseCIDR(network)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error parsing %s (%s): %s", network, comment, err))
|
||||
}
|
||||
return *net
|
||||
}
|
||||
|
||||
var (
|
||||
// Private CIDRs to ignore
|
||||
privateNetworks = []net.IPNet{
|
||||
|
@ -114,6 +123,21 @@ var (
|
|||
Mask: []byte{255, 192, 0, 0},
|
||||
},
|
||||
}
|
||||
// Sourced from https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
|
||||
// where Global, Source, or Destination is False
|
||||
privateV6Networks = []net.IPNet{
|
||||
parseCidr("::/128", "RFC 4291: Unspecified Address"),
|
||||
parseCidr("::1/128", "RFC 4291: Loopback Address"),
|
||||
parseCidr("::ffff:0:0/96", "RFC 4291: IPv4-mapped Address"),
|
||||
parseCidr("100::/64", "RFC 6666: Discard Address Block"),
|
||||
parseCidr("2001::/23", "RFC 2928: IETF Protocol Assignments"),
|
||||
parseCidr("2001:2::/48", "RFC 5180: Benchmarking"),
|
||||
parseCidr("2001:db8::/32", "RFC 3849: Documentation"),
|
||||
parseCidr("2001::/32", "RFC 4380: TEREDO"),
|
||||
parseCidr("fc00::/7", "RFC 4193: Unique-Local"),
|
||||
parseCidr("fe80::/10", "RFC 4291: Section 2.5.6 Link-Scoped Unicast"),
|
||||
parseCidr("ff00::/8", "RFC 4291: Section 2.7"),
|
||||
}
|
||||
)
|
||||
|
||||
// DNSResolver queries for DNS records
|
||||
|
@ -130,10 +154,12 @@ type DNSResolverImpl struct {
|
|||
servers []string
|
||||
allowRestrictedAddresses bool
|
||||
maxTries int
|
||||
LookupIPv6 bool
|
||||
clk clock.Clock
|
||||
stats metrics.Scope
|
||||
txtStats metrics.Scope
|
||||
aStats metrics.Scope
|
||||
aaaaStats metrics.Scope
|
||||
caaStats metrics.Scope
|
||||
mxStats metrics.Scope
|
||||
}
|
||||
|
@ -163,6 +189,7 @@ func NewDNSResolverImpl(readTimeout time.Duration, servers []string, stats metri
|
|||
stats: stats,
|
||||
txtStats: stats.NewScope("TXT"),
|
||||
aStats: stats.NewScope("A"),
|
||||
aaaaStats: stats.NewScope("AAAA"),
|
||||
caaStats: stats.NewScope("CAA"),
|
||||
mxStats: stats.NewScope("MX"),
|
||||
}
|
||||
|
@ -283,29 +310,71 @@ func isPrivateV4(ip net.IP) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// LookupHost sends a DNS query to find all A records associated with the
|
||||
// provided hostname. This method assumes that the external resolver will chase
|
||||
// CNAME/DNAME aliases and return relevant A records. It will retry requests in
|
||||
// the case of temporary network errors. It can return net package,
|
||||
// context.Canceled, and context.DeadlineExceeded errors.
|
||||
func (dnsResolver *DNSResolverImpl) LookupHost(ctx context.Context, hostname string) ([]net.IP, error) {
|
||||
var addrs []net.IP
|
||||
dnsType := dns.TypeA
|
||||
r, err := dnsResolver.exchangeOne(ctx, hostname, dnsType, dnsResolver.aStats)
|
||||
if err != nil {
|
||||
return addrs, &DNSError{dnsType, hostname, err, -1}
|
||||
func isPrivateV6(ip net.IP) bool {
|
||||
for _, net := range privateV6Networks {
|
||||
if net.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
return nil, &DNSError{dnsType, hostname, nil, r.Rcode}
|
||||
return false
|
||||
}
|
||||
|
||||
func (dnsResolver *DNSResolverImpl) lookupIP(ctx context.Context, hostname string, ipType uint16, stats metrics.Scope) ([]dns.RR, error) {
|
||||
resp, err := dnsResolver.exchangeOne(ctx, hostname, ipType, stats)
|
||||
if err != nil {
|
||||
return nil, &DNSError{ipType, hostname, err, -1}
|
||||
}
|
||||
if resp.Rcode != dns.RcodeSuccess {
|
||||
return nil, &DNSError{ipType, hostname, nil, resp.Rcode}
|
||||
}
|
||||
return resp.Answer, nil
|
||||
}
|
||||
|
||||
// LookupHost sends a DNS query to find all A and AAAA records associated with
|
||||
// the provided hostname. This method assumes that the external resolver will
|
||||
// chase CNAME/DNAME aliases and return relevant records. It will retry
|
||||
// requests in the case of temporary network errors. It can return net package,
|
||||
// context.Canceled, and context.DeadlineExceeded errors, all wrapped in the
|
||||
// DNSError type.
|
||||
func (dnsResolver *DNSResolverImpl) LookupHost(ctx context.Context, hostname string) ([]net.IP, error) {
|
||||
var recordsA, recordsAAAA []dns.RR
|
||||
var errA, errAAAA error
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
recordsA, errA = dnsResolver.lookupIP(ctx, hostname, dns.TypeA, dnsResolver.aStats)
|
||||
}()
|
||||
if dnsResolver.LookupIPv6 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
recordsAAAA, errAAAA = dnsResolver.lookupIP(ctx, hostname, dns.TypeAAAA, dnsResolver.aaaaStats)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if errA != nil && (errAAAA != nil || !dnsResolver.LookupIPv6) {
|
||||
return nil, errA
|
||||
}
|
||||
|
||||
for _, answer := range r.Answer {
|
||||
if answer.Header().Rrtype == dnsType {
|
||||
var addrs []net.IP
|
||||
|
||||
for _, answer := range recordsA {
|
||||
if answer.Header().Rrtype == dns.TypeA {
|
||||
if a, ok := answer.(*dns.A); ok && a.A.To4() != nil && (!isPrivateV4(a.A) || dnsResolver.allowRestrictedAddresses) {
|
||||
addrs = append(addrs, a.A)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, answer := range recordsAAAA {
|
||||
if answer.Header().Rrtype == dns.TypeAAAA {
|
||||
if aaaa, ok := answer.(*dns.AAAA); ok && aaaa.AAAA.To16() != nil && (!isPrivateV6(aaaa.AAAA) || dnsResolver.allowRestrictedAddresses) {
|
||||
addrs = append(addrs, aaaa.AAAA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
|
140
bdns/dns_test.go
140
bdns/dns_test.go
|
@ -59,6 +59,27 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
|
|||
record.AAAA = net.ParseIP("::1")
|
||||
appendAnswer(record)
|
||||
}
|
||||
if q.Name == "dualstack.letsencrypt.org." {
|
||||
record := new(dns.AAAA)
|
||||
record.Hdr = dns.RR_Header{Name: "dualstack.letsencrypt.org.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 0}
|
||||
record.AAAA = net.ParseIP("::1")
|
||||
appendAnswer(record)
|
||||
}
|
||||
if q.Name == "v4error.letsencrypt.org." {
|
||||
record := new(dns.AAAA)
|
||||
record.Hdr = dns.RR_Header{Name: "v4error.letsencrypt.org.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 0}
|
||||
record.AAAA = net.ParseIP("::1")
|
||||
appendAnswer(record)
|
||||
}
|
||||
if q.Name == "v6error.letsencrypt.org." {
|
||||
m.SetRcode(r, dns.RcodeNotImplemented)
|
||||
}
|
||||
if q.Name == "nxdomain.letsencrypt.org." {
|
||||
m.SetRcode(r, dns.RcodeNameError)
|
||||
}
|
||||
if q.Name == "dualstackerror.letsencrypt.org." {
|
||||
m.SetRcode(r, dns.RcodeNotImplemented)
|
||||
}
|
||||
case dns.TypeA:
|
||||
if q.Name == "cps.letsencrypt.org." {
|
||||
record := new(dns.A)
|
||||
|
@ -66,9 +87,27 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
|
|||
record.A = net.ParseIP("127.0.0.1")
|
||||
appendAnswer(record)
|
||||
}
|
||||
if q.Name == "dualstack.letsencrypt.org." {
|
||||
record := new(dns.A)
|
||||
record.Hdr = dns.RR_Header{Name: "dualstack.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}
|
||||
record.A = net.ParseIP("127.0.0.1")
|
||||
appendAnswer(record)
|
||||
}
|
||||
if q.Name == "v6error.letsencrypt.org." {
|
||||
record := new(dns.A)
|
||||
record.Hdr = dns.RR_Header{Name: "dualstack.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}
|
||||
record.A = net.ParseIP("127.0.0.1")
|
||||
appendAnswer(record)
|
||||
}
|
||||
if q.Name == "v4error.letsencrypt.org." {
|
||||
m.SetRcode(r, dns.RcodeNotImplemented)
|
||||
}
|
||||
if q.Name == "nxdomain.letsencrypt.org." {
|
||||
m.SetRcode(r, dns.RcodeNameError)
|
||||
}
|
||||
if q.Name == "dualstackerror.letsencrypt.org." {
|
||||
m.SetRcode(r, dns.RcodeRefused)
|
||||
}
|
||||
case dns.TypeCNAME:
|
||||
if q.Name == "cname.letsencrypt.org." {
|
||||
record := new(dns.CNAME)
|
||||
|
@ -246,6 +285,8 @@ func TestDNSLookupTXT(t *testing.T) {
|
|||
func TestDNSLookupHost(t *testing.T) {
|
||||
obj := NewTestDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr}, testStats, clock.NewFake(), 1)
|
||||
|
||||
obj.LookupIPv6 = true
|
||||
|
||||
ip, err := obj.LookupHost(context.Background(), "servfail.com")
|
||||
t.Logf("servfail.com - IP: %s, Err: %s", ip, err)
|
||||
test.AssertError(t, err, "Server failure")
|
||||
|
@ -266,11 +307,75 @@ func TestDNSLookupHost(t *testing.T) {
|
|||
test.AssertNotError(t, err, "Not an error to exist")
|
||||
test.Assert(t, len(ip) == 1, "Should have IP")
|
||||
|
||||
// No IPv6
|
||||
// Single IPv6 address
|
||||
ip, err = obj.LookupHost(context.Background(), "v6.letsencrypt.org")
|
||||
t.Logf("v6.letsencrypt.org - IP: %s, Err: %s", ip, err)
|
||||
test.AssertNotError(t, err, "Not an error to exist")
|
||||
test.Assert(t, len(ip) == 0, "Should not have IPs")
|
||||
test.Assert(t, len(ip) == 1, "Should not have IPs")
|
||||
|
||||
// Both IPv6 and IPv4 address
|
||||
ip, err = obj.LookupHost(context.Background(), "dualstack.letsencrypt.org")
|
||||
t.Logf("dualstack.letsencrypt.org - IP: %s, Err: %s", ip, err)
|
||||
test.AssertNotError(t, err, "Not an error to exist")
|
||||
test.Assert(t, len(ip) == 2, "Should have 2 IPs")
|
||||
expected := net.ParseIP("127.0.0.1")
|
||||
test.Assert(t, ip[0].To4().Equal(expected), "wrong ipv4 address")
|
||||
expected = net.ParseIP("::1")
|
||||
test.Assert(t, ip[1].To16().Equal(expected), "wrong ipv6 address")
|
||||
|
||||
// IPv6 error, IPv4 success
|
||||
ip, err = obj.LookupHost(context.Background(), "v6error.letsencrypt.org")
|
||||
t.Logf("v6error.letsencrypt.org - IP: %s, Err: %s", ip, err)
|
||||
test.AssertNotError(t, err, "Not an error to exist")
|
||||
test.Assert(t, len(ip) == 1, "Should have 1 IP")
|
||||
expected = net.ParseIP("127.0.0.1")
|
||||
test.Assert(t, ip[0].To4().Equal(expected), "wrong ipv4 address")
|
||||
|
||||
// IPv6 success, IPv4 error
|
||||
ip, err = obj.LookupHost(context.Background(), "v4error.letsencrypt.org")
|
||||
t.Logf("v4error.letsencrypt.org - IP: %s, Err: %s", ip, err)
|
||||
test.AssertNotError(t, err, "Not an error to exist")
|
||||
test.Assert(t, len(ip) == 1, "Should have 1 IP")
|
||||
expected = net.ParseIP("::1")
|
||||
test.Assert(t, ip[0].To16().Equal(expected), "wrong ipv6 address")
|
||||
|
||||
// IPv6 error, IPv4 error
|
||||
// Should return the IPv4 error (Refused) and not IPv6 error (NotImplemented)
|
||||
hostname := "dualstackerror.letsencrypt.org"
|
||||
ip, err = obj.LookupHost(context.Background(), hostname)
|
||||
t.Logf("%s - IP: %s, Err: %s", hostname, ip, err)
|
||||
test.AssertError(t, err, "Should be an error")
|
||||
expectedErr := DNSError{dns.TypeA, hostname, nil, dns.RcodeRefused}
|
||||
if err, ok := err.(*DNSError); !ok || *err != expectedErr {
|
||||
t.Errorf("Looking up %s, got %#v, expected %#v", hostname, err, expectedErr)
|
||||
}
|
||||
|
||||
obj.LookupIPv6 = false
|
||||
|
||||
// Single IPv6 address
|
||||
ip, err = obj.LookupHost(context.Background(), "v6.letsencrypt.org")
|
||||
t.Logf("v6.letsencrypt.org - IP: %s, Err: %s", ip, err)
|
||||
test.AssertNotError(t, err, "Should not be error")
|
||||
test.Assert(t, len(ip) == 0, "Should have no IPs")
|
||||
|
||||
// Both IPv6 and IPv4 address
|
||||
ip, err = obj.LookupHost(context.Background(), "dualstack.letsencrypt.org")
|
||||
t.Logf("dualstack.letsencrypt.org - IP: %s, Err: %s", ip, err)
|
||||
test.AssertNotError(t, err, "Should not be error")
|
||||
test.Assert(t, len(ip) == 1, "Should have 1 IP")
|
||||
expected = net.ParseIP("127.0.0.1")
|
||||
test.Assert(t, ip[0].To4().Equal(expected), "wrong ipv4 address")
|
||||
|
||||
// IPv6 success, IPv4 error
|
||||
hostname = "v4error.letsencrypt.org"
|
||||
ip, err = obj.LookupHost(context.Background(), hostname)
|
||||
t.Logf("v4error.letsencrypt.org - IP: %s, Err: %s", ip, err)
|
||||
test.AssertError(t, err, "Should be error")
|
||||
test.Assert(t, len(ip) == 0, "Should have 0 IPs")
|
||||
expectedErr = DNSError{dns.TypeA, hostname, nil, dns.RcodeNotImplemented}
|
||||
if err, ok := err.(*DNSError); !ok || *err != expectedErr {
|
||||
t.Errorf("Looking up %s, got %#v, expected %#v", hostname, err, expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSNXDOMAIN(t *testing.T) {
|
||||
|
@ -316,6 +421,37 @@ func TestDNSTXTAuthorities(t *testing.T) {
|
|||
test.AssertEquals(t, auths[0], "letsencrypt.org. 0 IN SOA ns.letsencrypt.org. master.letsencrypt.org. 1 1 1 1 1")
|
||||
}
|
||||
|
||||
func TestIsPrivateIP(t *testing.T) {
|
||||
test.Assert(t, isPrivateV4(net.ParseIP("127.0.0.1")), "should be private")
|
||||
test.Assert(t, isPrivateV4(net.ParseIP("192.168.254.254")), "should be private")
|
||||
test.Assert(t, isPrivateV4(net.ParseIP("10.255.0.3")), "should be private")
|
||||
test.Assert(t, isPrivateV4(net.ParseIP("172.16.255.255")), "should be private")
|
||||
test.Assert(t, isPrivateV4(net.ParseIP("172.31.255.255")), "should be private")
|
||||
test.Assert(t, !isPrivateV4(net.ParseIP("128.0.0.1")), "should be private")
|
||||
test.Assert(t, !isPrivateV4(net.ParseIP("192.169.255.255")), "should not be private")
|
||||
test.Assert(t, !isPrivateV4(net.ParseIP("9.255.0.255")), "should not be private")
|
||||
test.Assert(t, !isPrivateV4(net.ParseIP("172.32.255.255")), "should not be private")
|
||||
|
||||
test.Assert(t, isPrivateV6(net.ParseIP("::0")), "should be private")
|
||||
test.Assert(t, isPrivateV6(net.ParseIP("::1")), "should be private")
|
||||
test.Assert(t, !isPrivateV6(net.ParseIP("::2")), "should not be private")
|
||||
|
||||
test.Assert(t, isPrivateV6(net.ParseIP("fe80::1")), "should be private")
|
||||
test.Assert(t, isPrivateV6(net.ParseIP("febf::1")), "should be private")
|
||||
test.Assert(t, !isPrivateV6(net.ParseIP("fec0::1")), "should not be private")
|
||||
test.Assert(t, !isPrivateV6(net.ParseIP("feff::1")), "should not be private")
|
||||
|
||||
test.Assert(t, isPrivateV6(net.ParseIP("ff00::1")), "should be private")
|
||||
test.Assert(t, isPrivateV6(net.ParseIP("ff10::1")), "should be private")
|
||||
test.Assert(t, isPrivateV6(net.ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), "should be private")
|
||||
|
||||
test.Assert(t, !isPrivateV6(net.ParseIP("2002::")), "should not be private")
|
||||
test.Assert(t, !isPrivateV6(net.ParseIP("2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), "should not be private")
|
||||
test.Assert(t, isPrivateV6(net.ParseIP("0100::")), "should be private")
|
||||
test.Assert(t, isPrivateV6(net.ParseIP("0100::0000:ffff:ffff:ffff:ffff")), "should be private")
|
||||
test.Assert(t, !isPrivateV6(net.ParseIP("0100::0001:0000:0000:0000:0000")), "should be private")
|
||||
}
|
||||
|
||||
type testExchanger struct {
|
||||
sync.Mutex
|
||||
count int
|
||||
|
|
|
@ -74,9 +74,14 @@ func main() {
|
|||
dnsTries = 1
|
||||
}
|
||||
if !c.Common.DNSAllowLoopbackAddresses {
|
||||
vai.DNSResolver = bdns.NewDNSResolverImpl(dnsTimeout, []string{c.Common.DNSResolver}, scoped, clk, dnsTries)
|
||||
resolver := bdns.NewDNSResolverImpl(dnsTimeout, []string{c.Common.DNSResolver}, scoped, clk, dnsTries)
|
||||
resolver.LookupIPv6 = c.VA.LookupIPv6
|
||||
vai.DNSResolver = resolver
|
||||
|
||||
} else {
|
||||
vai.DNSResolver = bdns.NewTestDNSResolverImpl(dnsTimeout, []string{c.Common.DNSResolver}, scoped, clk, dnsTries)
|
||||
resolver := bdns.NewTestDNSResolverImpl(dnsTimeout, []string{c.Common.DNSResolver}, scoped, clk, dnsTries)
|
||||
resolver.LookupIPv6 = c.VA.LookupIPv6
|
||||
vai.DNSResolver = resolver
|
||||
}
|
||||
vai.UserAgent = c.VA.UserAgent
|
||||
|
||||
|
|
|
@ -87,6 +87,8 @@ type Config struct {
|
|||
|
||||
MaxConcurrentRPCServerRequests int64
|
||||
|
||||
LookupIPv6 bool
|
||||
|
||||
GoogleSafeBrowsing *GoogleSafeBrowsingConfig
|
||||
|
||||
CAAService *GRPCClientConfig
|
||||
|
|
|
@ -206,6 +206,7 @@
|
|||
"httpsPort": 5001,
|
||||
"tlsPort": 5001
|
||||
},
|
||||
"lookupIPV6": true,
|
||||
"maxConcurrentRPCServerRequests": 16,
|
||||
"dnsTries": 3,
|
||||
"issuerDomain": "happy-hacker-ca.invalid",
|
||||
|
|
Loading…
Reference in New Issue