checker-reverse-zone/checker/dns.go

152 lines
3.5 KiB
Go

package checker
import (
"context"
"net"
"strconv"
"strings"
"time"
"github.com/miekg/dns"
)
const dnsTimeout = 5 * time.Second
// FallbackResolver is the resolver used when /etc/resolv.conf is missing or
// empty.
var FallbackResolver = net.JoinHostPort("1.1.1.1", "53")
func systemResolver() string {
cfg, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil || len(cfg.Servers) == 0 {
return FallbackResolver
}
return net.JoinHostPort(cfg.Servers[0], cfg.Port)
}
func dnsExchange(ctx context.Context, server string, q dns.Question) (*dns.Msg, error) {
client := dns.Client{Timeout: dnsTimeout}
m := new(dns.Msg)
m.Id = dns.Id()
m.Question = []dns.Question{q}
m.RecursionDesired = true
m.SetEdns0(4096, true)
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 && r.Truncated {
if ctx.Err() != nil {
return r, nil
}
tcpClient := dns.Client{Net: "tcp", Timeout: client.Timeout}
if r2, _, err2 := tcpClient.Exchange(m, server); err2 == nil && r2 != nil {
return r2, nil
}
}
return r, nil
}
func lowerFQDN(name string) string {
return strings.ToLower(dns.Fqdn(name))
}
func isReverseArpa(name string) bool {
n := lowerFQDN(name)
return strings.HasSuffix(n, ".in-addr.arpa.") || n == "in-addr.arpa." ||
strings.HasSuffix(n, ".ip6.arpa.") || n == "ip6.arpa."
}
func isIPv6Arpa(name string) bool {
n := lowerFQDN(name)
return strings.HasSuffix(n, ".ip6.arpa.") || n == "ip6.arpa."
}
// reverseNameToIP returns nil for partial zone apexes (e.g. covering only part of an octet).
func reverseNameToIP(name string) net.IP {
n := strings.ToLower(strings.TrimSuffix(dns.Fqdn(name), "."))
switch {
case strings.HasSuffix(n, ".in-addr.arpa"):
labels := strings.Split(strings.TrimSuffix(n, ".in-addr.arpa"), ".")
if len(labels) != 4 {
return nil
}
out := make([]string, 4)
for i, l := range labels {
if _, err := strconv.Atoi(l); err != nil {
return nil
}
out[3-i] = l
}
return net.ParseIP(strings.Join(out, "."))
case strings.HasSuffix(n, ".ip6.arpa"):
labels := strings.Split(strings.TrimSuffix(n, ".ip6.arpa"), ".")
if len(labels) != 32 {
return nil
}
var sb strings.Builder
for i := len(labels) - 1; i >= 0; i-- {
if len(labels[i]) != 1 {
return nil
}
sb.WriteString(labels[i])
if i > 0 && (len(labels)-i)%4 == 0 {
sb.WriteByte(':')
}
}
return net.ParseIP(sb.String())
}
return nil
}
func resolveForward(ctx context.Context, name string) ([]ForwardAddress, string) {
resolver := systemResolver()
var out []ForwardAddress
var lastErr string
anySuccess := false
for _, qt := range []uint16{dns.TypeA, dns.TypeAAAA} {
if ctx.Err() != nil {
break
}
q := dns.Question{Name: dns.Fqdn(name), Qtype: qt, Qclass: dns.ClassINET}
r, err := dnsExchange(ctx, resolver, q)
if err != nil {
lastErr = err.Error()
continue
}
anySuccess = true
if r == nil {
continue
}
for _, rr := range r.Answer {
switch v := rr.(type) {
case *dns.A:
out = append(out, ForwardAddress{Type: "A", Address: v.A.String(), TTL: v.Hdr.Ttl})
case *dns.AAAA:
out = append(out, ForwardAddress{Type: "AAAA", Address: v.AAAA.String(), TTL: v.Hdr.Ttl})
}
}
}
if len(out) > 0 || anySuccess {
return out, ""
}
return out, lastErr
}
func ipEqual(addr string, ip net.IP) bool {
parsed := net.ParseIP(addr)
if parsed == nil {
return false
}
return parsed.Equal(ip)
}