Go backend (miekg/dns) + Nuxt 3 frontend (Tailwind CSS v4). 8 check categories, 52 checks total: - Overview: @ record, WWW, MX with ASN/provider lookup - Domain Registration: expiry, registrar (RDAP + whois fallback) - Parent Delegation: NS records, glue, consistency - Nameservers: 17 checks (reachability, auth, recursion, TCP/UDP, AXFR, etc.) - SOA: serial consistency, timing values - Mail (MX): 11 checks (CNAME, PTR, public IPs, consistency) - Mail Auth: SPF, DKIM, DMARC - WWW: A record, CNAME Features: - SSE streaming (results appear as each category completes) - SQLite history (modernc.org/sqlite) - Rate limiting, CORS, request logging - Dark mode, responsive design
106 lines
2.6 KiB
Go
106 lines
2.6 KiB
Go
package checker
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/intodns/backend/internal/resolver"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// checkWWW runs the 2 WWW checks.
|
|
func checkWWW(domain string, r *resolver.Resolver) Category {
|
|
cat := Category{Name: "www", Title: "WWW"}
|
|
|
|
domain = dns.Fqdn(domain)
|
|
wwwName := "www." + domain
|
|
|
|
// 1. www-a-record
|
|
cat.Checks = append(cat.Checks, checkWWWARecord(wwwName, r))
|
|
|
|
// 2. www-cname
|
|
cat.Checks = append(cat.Checks, checkWWWCNAME(wwwName, r))
|
|
|
|
return cat
|
|
}
|
|
|
|
func checkWWWARecord(wwwName string, r *resolver.Resolver) CheckResult {
|
|
start := time.Now()
|
|
res := CheckResult{ID: "www-a-record", Title: "WWW A Record"}
|
|
defer func() { res.DurationMs = measureDuration(start) }()
|
|
|
|
resp, err := r.Query(wwwName, "8.8.8.8", dns.TypeA)
|
|
if err != nil {
|
|
res.Status = StatusWarn
|
|
res.Message = fmt.Sprintf("Failed to query A record for %s: %v", wwwName, err)
|
|
return res
|
|
}
|
|
|
|
var ips []string
|
|
for _, rr := range resp.Answer {
|
|
if a, ok := rr.(*dns.A); ok {
|
|
ips = append(ips, a.A.String())
|
|
}
|
|
}
|
|
|
|
if len(ips) == 0 {
|
|
res.Status = StatusInfo
|
|
res.Message = fmt.Sprintf("No A record found for %s", wwwName)
|
|
return res
|
|
}
|
|
|
|
allPublic := true
|
|
for _, ipStr := range ips {
|
|
ip := net.ParseIP(ipStr)
|
|
if isPublicIP(ip) {
|
|
res.Details = append(res.Details, fmt.Sprintf("%s -> %s (public)", wwwName, ipStr))
|
|
} else {
|
|
allPublic = false
|
|
res.Details = append(res.Details, fmt.Sprintf("%s -> %s (NOT public)", wwwName, ipStr))
|
|
}
|
|
}
|
|
|
|
if allPublic {
|
|
res.Status = StatusPass
|
|
res.Message = fmt.Sprintf("%s has A record(s) with public IP(s)", wwwName)
|
|
} else {
|
|
res.Status = StatusWarn
|
|
res.Message = fmt.Sprintf("%s has A record(s) but some IPs are not public", wwwName)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func checkWWWCNAME(wwwName string, r *resolver.Resolver) CheckResult {
|
|
start := time.Now()
|
|
res := CheckResult{ID: "www-cname", Title: "WWW CNAME"}
|
|
defer func() { res.DurationMs = measureDuration(start) }()
|
|
|
|
resp, err := r.Query(wwwName, "8.8.8.8", dns.TypeCNAME)
|
|
if err != nil {
|
|
res.Status = StatusInfo
|
|
res.Message = fmt.Sprintf("Could not check CNAME for %s", wwwName)
|
|
return res
|
|
}
|
|
|
|
var targets []string
|
|
for _, rr := range resp.Answer {
|
|
if cname, ok := rr.(*dns.CNAME); ok {
|
|
targets = append(targets, cname.Target)
|
|
}
|
|
}
|
|
|
|
if len(targets) > 0 {
|
|
res.Status = StatusInfo
|
|
res.Message = fmt.Sprintf("%s is a CNAME to %s", wwwName, strings.Join(targets, ", "))
|
|
for _, t := range targets {
|
|
res.Details = append(res.Details, fmt.Sprintf("CNAME target: %s", t))
|
|
}
|
|
} else {
|
|
res.Status = StatusInfo
|
|
res.Message = fmt.Sprintf("%s is not a CNAME (direct A/AAAA record)", wwwName)
|
|
}
|
|
return res
|
|
}
|