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