Initial commit
This commit is contained in:
commit
eea7e4e459
22 changed files with 2520 additions and 0 deletions
155
checker/dns.go
Normal file
155
checker/dns.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const dnsTimeout = 5 * time.Second
|
||||
|
||||
// dnsExchange sends a single query. dnssec=true requests DNSSEC RRs (DO bit);
|
||||
// pass false for plain chain walks to keep responses small.
|
||||
func dnsExchange(ctx context.Context, proto, server string, q dns.Question, rd, dnssec 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 = rd
|
||||
m.SetEdns0(4096, dnssec)
|
||||
|
||||
if deadline, ok := ctx.Deadline(); 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
|
||||
}
|
||||
|
||||
func recursiveExchange(ctx context.Context, server string, q dns.Question) (*dns.Msg, error) {
|
||||
return dnsExchange(ctx, "", server, q, true, false)
|
||||
}
|
||||
|
||||
// systemResolver reads /etc/resolv.conf, falling back to 1.1.1.1 in scratch
|
||||
// containers where the file is absent. The fallback leaks queries to
|
||||
// Cloudflare; operators that care should mount a resolv.conf.
|
||||
func systemResolver() string {
|
||||
cfg, err := dns.ClientConfigFromFile("/etc/resolv.conf")
|
||||
if err != nil || len(cfg.Servers) == 0 {
|
||||
return net.JoinHostPort("1.1.1.1", "53")
|
||||
}
|
||||
return net.JoinHostPort(cfg.Servers[0], cfg.Port)
|
||||
}
|
||||
|
||||
func hostPort(host, port string) string {
|
||||
return net.JoinHostPort(strings.TrimSuffix(host, "."), port)
|
||||
}
|
||||
|
||||
func findApex(ctx context.Context, fqdn, resolver string) (apex string, servers []string, err error) {
|
||||
labels := dns.SplitDomainName(fqdn)
|
||||
for i := range labels {
|
||||
candidate := dns.Fqdn(strings.Join(labels[i:], "."))
|
||||
q := dns.Question{Name: candidate, Qtype: dns.TypeSOA, Qclass: dns.ClassINET}
|
||||
r, rerr := recursiveExchange(ctx, resolver, q)
|
||||
if rerr != nil {
|
||||
continue
|
||||
}
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
continue
|
||||
}
|
||||
hasSOA := false
|
||||
for _, rr := range r.Answer {
|
||||
if _, ok := rr.(*dns.SOA); ok {
|
||||
hasSOA = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSOA {
|
||||
continue
|
||||
}
|
||||
apex = candidate
|
||||
servers, err = resolveZoneNSAddrs(ctx, apex)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
return "", nil, fmt.Errorf("apex %s has no resolvable NS", apex)
|
||||
}
|
||||
return apex, servers, nil
|
||||
}
|
||||
return "", nil, fmt.Errorf("could not locate apex of %s", fqdn)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
results := make([][]string, len(nss))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(nss))
|
||||
for i, ns := range nss {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
addrs, err := resolver.LookupHost(ctx, strings.TrimSuffix(ns.Host, "."))
|
||||
if err != nil || len(addrs) == 0 {
|
||||
return
|
||||
}
|
||||
r := make([]string, len(addrs))
|
||||
for j, a := range addrs {
|
||||
r[j] = hostPort(a, "53")
|
||||
}
|
||||
results[i] = r
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
var out []string
|
||||
for _, r := range results {
|
||||
out = append(out, r...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// queryAtAuth tries each server in order and returns the first usable answer.
|
||||
// dnssec=true sets the DO bit; only the DNSSEC probes need it.
|
||||
func queryAtAuth(ctx context.Context, proto string, servers []string, q dns.Question, dnssec bool) (*dns.Msg, string, error) {
|
||||
var lastErr error
|
||||
for _, s := range servers {
|
||||
r, err := dnsExchange(ctx, proto, s, q, false, dnssec)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return r, s, nil
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no servers provided")
|
||||
}
|
||||
return nil, "", lastErr
|
||||
}
|
||||
|
||||
func rcodeText(r int) string {
|
||||
if s, ok := dns.RcodeToString[r]; ok {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("RCODE(%d)", r)
|
||||
}
|
||||
|
||||
func lowerFQDN(name string) string {
|
||||
return strings.ToLower(dns.Fqdn(name))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue