Look up A and AAAA in parallel (#1760)

This allows validating IPv6-only hosts.

Fixes #593.
This commit is contained in:
Kane York 2016-05-09 08:38:23 -07:00 committed by Jacob Hoffman-Andrews
parent a41b1dd091
commit 339405bcb9
5 changed files with 232 additions and 19 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -87,6 +87,8 @@ type Config struct {
MaxConcurrentRPCServerRequests int64
LookupIPv6 bool
GoogleSafeBrowsing *GoogleSafeBrowsingConfig
CAAService *GRPCClientConfig

View File

@ -206,6 +206,7 @@
"httpsPort": 5001,
"tlsPort": 5001
},
"lookupIPV6": true,
"maxConcurrentRPCServerRequests": 16,
"dnsTries": 3,
"issuerDomain": "happy-hacker-ca.invalid",