checker-delegation/checker/dns.go

277 lines
8.4 KiB
Go

package checker
import (
"context"
"fmt"
"net"
"strings"
"time"
"github.com/miekg/dns"
)
// year68 mirrors the constant from miekg/dns used to wrap RRSIG validity
// periods around 2^32 seconds (≈68 years).
const year68 = int64(1 << 31)
// dnsTimeout is the per-query deadline used by every helper here.
const dnsTimeout = 5 * time.Second
// dnsExchange sends a single query to the given server using the requested
// transport ("" for UDP, "tcp"). The server address must already include a
// port. RecursionDesired is forced off, this checker only talks to
// authoritative servers.
func dnsExchange(ctx context.Context, proto, server string, q dns.Question, edns bool) (*dns.Msg, error) {
client := dns.Client{Net: proto, Timeout: dnsTimeout}
m := new(dns.Msg)
m.Id = dns.Id()
m.Question = []dns.Question{q}
m.RecursionDesired = false
if edns {
m.SetEdns0(4096, true)
}
deadline, ok := ctx.Deadline()
if ok {
if d := time.Until(deadline); d > 0 && d < client.Timeout {
client.Timeout = d
}
}
r, _, err := client.Exchange(m, server)
if err != nil {
return nil, err
}
if r == nil {
return nil, fmt.Errorf("nil response from %s", server)
}
return r, nil
}
// hostPort returns "host:port", correctly bracketing IPv6 literals.
func hostPort(host, port string) string {
if ip := net.ParseIP(host); ip != nil && ip.To4() == nil {
return "[" + host + "]:" + port
}
host = strings.TrimSuffix(host, ".")
return host + ":" + port
}
// resolveHost resolves an NS hostname to its A and AAAA addresses using the
// system resolver. It is used as a fallback when no glue is provided by the
// parent for an out-of-bailiwick NS.
func resolveHost(ctx context.Context, host string) ([]string, error) {
var resolver net.Resolver
addrs, err := resolver.LookupHost(ctx, strings.TrimSuffix(host, "."))
if err != nil {
return nil, err
}
return addrs, nil
}
// findParentZone walks up the labels of fqdn until it finds the closest
// enclosing zone (the one that has its own SOA), and returns the FQDN of
// that zone along with its authoritative server addresses (resolved from
// its NS RRset). The walk stops as soon as a SOA query at the system
// resolver returns NOERROR with an answer.
//
// If hintParent is non-empty, it is used as the assumed parent and we only
// resolve its NS, this matches happyDomain's data model where the parent
// zone is known.
func findParentZone(ctx context.Context, fqdn, hintParent string) (zone string, servers []string, err error) {
zone = dns.Fqdn(hintParent)
if zone == "" || zone == "." {
// Walk up.
labels := dns.SplitDomainName(fqdn)
if len(labels) == 0 {
return "", nil, fmt.Errorf("cannot derive parent of %q", fqdn)
}
zone = dns.Fqdn(strings.Join(labels[1:], "."))
}
servers, err = resolveZoneNSAddrs(ctx, zone)
if err != nil {
return "", nil, fmt.Errorf("resolving NS of parent zone %q: %w", zone, err)
}
if len(servers) == 0 {
return "", nil, fmt.Errorf("parent zone %q has no resolvable NS", zone)
}
return zone, servers, nil
}
// resolveZoneNSAddrs returns the list of "host:53" entries for every NS of
// the given zone, as seen by the system resolver. It is used to discover the
// parent's authoritative servers.
func resolveZoneNSAddrs(ctx context.Context, zone string) ([]string, error) {
var resolver net.Resolver
nss, err := resolver.LookupNS(ctx, strings.TrimSuffix(zone, "."))
if err != nil {
return nil, err
}
var out []string
for _, ns := range nss {
addrs, err := resolver.LookupHost(ctx, strings.TrimSuffix(ns.Host, "."))
if err != nil || len(addrs) == 0 {
continue
}
for _, a := range addrs {
out = append(out, hostPort(a, "53"))
}
}
return out, nil
}
// queryDelegation queries the given parent server for the NS RRset of fqdn
// and extracts the advertised NS names plus any glue records found in the
// Additional section. The query is sent without RD; the response is the
// classical "referral" packet.
func queryDelegation(ctx context.Context, parentServer, fqdn string) (ns []string, glue map[string][]string, msg *dns.Msg, err error) {
q := dns.Question{Name: dns.Fqdn(fqdn), Qtype: dns.TypeNS, Qclass: dns.ClassINET}
msg, err = dnsExchange(ctx, "", parentServer, q, true)
if err != nil {
return nil, nil, nil, err
}
if msg.Rcode != dns.RcodeSuccess {
return nil, nil, msg, fmt.Errorf("parent answered %s", dns.RcodeToString[msg.Rcode])
}
glue = map[string][]string{}
collect := func(records []dns.RR) {
for _, rr := range records {
switch t := rr.(type) {
case *dns.NS:
if strings.EqualFold(strings.TrimSuffix(t.Header().Name, "."), strings.TrimSuffix(fqdn, ".")) {
ns = append(ns, strings.ToLower(dns.Fqdn(t.Ns)))
}
case *dns.A:
name := strings.ToLower(dns.Fqdn(t.Header().Name))
glue[name] = append(glue[name], t.A.String())
case *dns.AAAA:
name := strings.ToLower(dns.Fqdn(t.Header().Name))
glue[name] = append(glue[name], t.AAAA.String())
}
}
}
collect(msg.Answer)
collect(msg.Ns)
collect(msg.Extra)
return
}
// queryDS asks the parent server for the DS RRset of fqdn and returns the
// DS records plus any RRSIGs found in the same section.
func queryDS(ctx context.Context, parentServer, fqdn string) (ds []*dns.DS, sigs []*dns.RRSIG, err error) {
q := dns.Question{Name: dns.Fqdn(fqdn), Qtype: dns.TypeDS, Qclass: dns.ClassINET}
r, err := dnsExchange(ctx, "tcp", parentServer, q, true)
if err != nil {
return nil, nil, err
}
if r.Rcode != dns.RcodeSuccess {
return nil, nil, fmt.Errorf("parent answered %s for DS", dns.RcodeToString[r.Rcode])
}
for _, rr := range r.Answer {
switch t := rr.(type) {
case *dns.DS:
ds = append(ds, t)
case *dns.RRSIG:
sigs = append(sigs, t)
}
}
return
}
// querySOA asks the given authoritative server for the SOA of fqdn and
// returns the SOA record plus the AA flag from the response header.
func querySOA(ctx context.Context, proto, server, fqdn string) (soa *dns.SOA, aa bool, err error) {
q := dns.Question{Name: dns.Fqdn(fqdn), Qtype: dns.TypeSOA, Qclass: dns.ClassINET}
r, err := dnsExchange(ctx, proto, server, q, false)
if err != nil {
return nil, false, err
}
if r.Rcode != dns.RcodeSuccess {
return nil, r.Authoritative, fmt.Errorf("server answered %s", dns.RcodeToString[r.Rcode])
}
for _, rr := range r.Answer {
if t, ok := rr.(*dns.SOA); ok {
return t, r.Authoritative, nil
}
}
return nil, r.Authoritative, fmt.Errorf("no SOA in answer section")
}
// queryNSAt asks the given authoritative server for the NS RRset of fqdn.
func queryNSAt(ctx context.Context, server, fqdn string) ([]string, error) {
q := dns.Question{Name: dns.Fqdn(fqdn), Qtype: dns.TypeNS, Qclass: dns.ClassINET}
r, err := dnsExchange(ctx, "", server, q, false)
if err != nil {
return nil, err
}
if r.Rcode != dns.RcodeSuccess {
return nil, fmt.Errorf("server answered %s", dns.RcodeToString[r.Rcode])
}
var out []string
for _, rr := range r.Answer {
if t, ok := rr.(*dns.NS); ok {
out = append(out, strings.ToLower(dns.Fqdn(t.Ns)))
}
}
return out, nil
}
// queryAddrsAt asks an authoritative server for the A and AAAA records of
// host (typically an in-bailiwick NS hostname).
func queryAddrsAt(ctx context.Context, server, host string) ([]string, error) {
var out []string
for _, qt := range []uint16{dns.TypeA, dns.TypeAAAA} {
r, err := dnsExchange(ctx, "", server, dns.Question{Name: dns.Fqdn(host), Qtype: qt, Qclass: dns.ClassINET}, false)
if err != nil {
continue
}
if r.Rcode != dns.RcodeSuccess {
continue
}
for _, rr := range r.Answer {
switch t := rr.(type) {
case *dns.A:
out = append(out, t.A.String())
case *dns.AAAA:
out = append(out, t.AAAA.String())
}
}
}
return out, nil
}
// queryDNSKEY asks the given child server for the DNSKEY RRset of fqdn.
func queryDNSKEY(ctx context.Context, server, fqdn string) ([]*dns.DNSKEY, error) {
q := dns.Question{Name: dns.Fqdn(fqdn), Qtype: dns.TypeDNSKEY, Qclass: dns.ClassINET}
r, err := dnsExchange(ctx, "tcp", server, q, true)
if err != nil {
return nil, err
}
if r.Rcode != dns.RcodeSuccess {
return nil, fmt.Errorf("server answered %s for DNSKEY", dns.RcodeToString[r.Rcode])
}
var out []*dns.DNSKEY
for _, rr := range r.Answer {
if t, ok := rr.(*dns.DNSKEY); ok {
out = append(out, t)
}
}
return out, nil
}
// dsEqual returns true when two DS records refer to the same key material.
func dsEqual(a, b *dns.DS) bool {
return a.KeyTag == b.KeyTag &&
a.Algorithm == b.Algorithm &&
a.DigestType == b.DigestType &&
strings.EqualFold(a.Digest, b.Digest)
}