package checker import ( "fmt" "net" "sort" "strings" "time" "github.com/intodns/backend/internal/resolver" "github.com/miekg/dns" ) // isPublicIP returns true if the IP is a globally routable unicast address. func isPublicIP(ip net.IP) bool { if ip == nil { return false } if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() { return false } // Check private ranges. privateRanges := []string{ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7", "100.64.0.0/10", // Carrier-grade NAT "169.254.0.0/16", // Link-local "192.0.0.0/24", // IETF protocol assignments "198.18.0.0/15", // Benchmarking } for _, cidr := range privateRanges { _, network, err := net.ParseCIDR(cidr) if err != nil { continue } if network.Contains(ip) { return false } } return true } // reverseDNS builds the in-addr.arpa or ip6.arpa name for an IP. func reverseDNS(ipStr string) string { ip := net.ParseIP(ipStr) if ip == nil { return "" } if ip4 := ip.To4(); ip4 != nil { return fmt.Sprintf("%d.%d.%d.%d.in-addr.arpa.", ip4[3], ip4[2], ip4[1], ip4[0]) } // IPv6 full := ip.To16() if full == nil { return "" } var buf strings.Builder for i := len(full) - 1; i >= 0; i-- { b := full[i] buf.WriteString(fmt.Sprintf("%x.%x.", b&0x0f, (b>>4)&0x0f)) } buf.WriteString("ip6.arpa.") return buf.String() } // findParentNS locates the parent zone's nameservers for the given domain by // querying the TLD nameservers. Returns NS hostnames and their resolved IPs. func findParentNS(domain string, r *resolver.Resolver) ([]string, []string) { domain = dns.Fqdn(domain) parts := dns.SplitDomainName(domain) if len(parts) < 2 { return nil, nil } // Build the parent zone (e.g., for "example.com." -> "com.") parent := dns.Fqdn(strings.Join(parts[1:], ".")) // First, find the TLD/parent zone nameservers. parentNS := findNSForZone(parent, r) if len(parentNS) == 0 { return nil, nil } // Resolve a parent NS to an IP so we can query it. parentIPs := resolveNames(parentNS, r) if len(parentIPs) == 0 { return nil, nil } // Query a parent NS for the domain's NS records (non-recursive). var nsNames []string for _, pip := range parentIPs { resp, err := r.QueryNoRecurse(domain, pip, dns.TypeNS) if err != nil { continue } // The NS records might be in the authority section (delegation) or answer. for _, rr := range resp.Ns { if nsRR, ok := rr.(*dns.NS); ok { nsNames = appendUniqLower(nsNames, nsRR.Ns) } } for _, rr := range resp.Answer { if nsRR, ok := rr.(*dns.NS); ok { nsNames = appendUniqLower(nsNames, nsRR.Ns) } } if len(nsNames) > 0 { break } } // Collect glue / resolve NS IPs. var nsIPs []string for _, ns := range nsNames { ips := resolveNS(ns, r) nsIPs = append(nsIPs, ips...) } return nsNames, nsIPs } // findNSForZone returns the nameserver hostnames for a zone. func findNSForZone(zone string, r *resolver.Resolver) []string { // Use the system resolver (recursive) to find NS for the zone. resp, err := r.Query(zone, "8.8.8.8", dns.TypeNS) if err != nil { return nil } var names []string for _, rr := range resp.Answer { if ns, ok := rr.(*dns.NS); ok { names = appendUniqLower(names, ns.Ns) } } return names } // resolveNS returns IP addresses for a nameserver hostname. func resolveNS(name string, r *resolver.Resolver) []string { var ips []string resp, err := r.Query(name, "8.8.8.8", dns.TypeA) if err == nil { for _, rr := range resp.Answer { if a, ok := rr.(*dns.A); ok { ips = append(ips, a.A.String()) } } } resp, err = r.Query(name, "8.8.8.8", dns.TypeAAAA) if err == nil { for _, rr := range resp.Answer { if aaaa, ok := rr.(*dns.AAAA); ok { ips = append(ips, aaaa.AAAA.String()) } } } return ips } // resolveNames resolves a list of hostnames to IPs. func resolveNames(names []string, r *resolver.Resolver) []string { var ips []string for _, n := range names { ips = append(ips, resolveNS(n, r)...) } return ips } // appendUniqLower adds s (lowered, FQDN) to the slice if not already present. func appendUniqLower(slice []string, s string) []string { s = strings.ToLower(dns.Fqdn(s)) for _, v := range slice { if v == s { return slice } } return append(slice, s) } // measureDuration returns the elapsed time in milliseconds since start. func measureDuration(start time.Time) int64 { return time.Since(start).Milliseconds() } // sortedStrings returns a sorted copy. func sortedStrings(s []string) []string { c := make([]string, len(s)) copy(c, s) sort.Strings(c) return c } // nsNamesFromAuthNS queries the authoritative nameservers for the domain's NS // records directly. func nsNamesFromAuthNS(domain string, nsIP string, r *resolver.Resolver) []string { resp, err := r.QueryNoRecurse(domain, nsIP, dns.TypeNS) if err != nil { return nil } var names []string for _, rr := range resp.Answer { if ns, ok := rr.(*dns.NS); ok { names = appendUniqLower(names, ns.Ns) } } return names } // subnet24 returns the /24 prefix string for an IPv4 address. func subnet24(ip string) string { parsed := net.ParseIP(ip) if parsed == nil { return ip } if v4 := parsed.To4(); v4 != nil { return fmt.Sprintf("%d.%d.%d.0/24", v4[0], v4[1], v4[2]) } // For IPv6 just use the first 48 bits as a rough grouping. full := parsed.To16() return fmt.Sprintf("%x:%x:%x::/48", uint16(full[0])<<8|uint16(full[1]), uint16(full[2])<<8|uint16(full[3]), uint16(full[4])<<8|uint16(full[5])) } // subnet16 returns the /16 prefix string as a rough AS-diversity proxy. func subnet16(ip string) string { parsed := net.ParseIP(ip) if parsed == nil { return ip } if v4 := parsed.To4(); v4 != nil { return fmt.Sprintf("%d.%d.0.0/16", v4[0], v4[1]) } full := parsed.To16() return fmt.Sprintf("%x:%x::/32", uint16(full[0])<<8|uint16(full[1]), uint16(full[2])<<8|uint16(full[3])) } // isInBailiwick returns true if the NS hostname is under the domain. func isInBailiwick(nsName, domain string) bool { nsName = strings.ToLower(dns.Fqdn(nsName)) domain = strings.ToLower(dns.Fqdn(domain)) return dns.IsSubDomain(domain, nsName) }