checker-blacklist/checker/quad9.go
Pierre-Olivier Mercier c3cda1f104
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Replace per-source enable booleans with SourcePrecheck and bump SDK to v1.9.0
Sources that always work (botvrij, disconnect, oisd, openphish, phishtank, quad9) drop their user-facing enable_* option; the rule's enabled/disabled state is now solely controlled by the SDK rule toggle. Sources that require credentials (criminalip, malwarebazaar, otx, pulsedive, safebrowsing, threatfox, urlhaus, virustotal) instead implement the new SourcePrecheck interface so the host UI can surface "not configured" before attempting a query.
2026-05-20 14:26:42 +08:00

117 lines
3.3 KiB
Go

package checker
import (
"context"
"fmt"
"net"
"sync"
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
)
func init() {
Register(&quad9Source{
secure: newDNSResolver("9.9.9.9:53"),
unsecured: newDNSResolver("9.9.9.10:53"),
})
}
type quad9Source struct {
secure *net.Resolver
unsecured *net.Resolver
}
func newDNSResolver(addr string) *net.Resolver {
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
d := &net.Dialer{Timeout: 5 * time.Second}
return d.DialContext(ctx, "udp", addr)
},
}
}
func (*quad9Source) ID() string { return "quad9" }
func (*quad9Source) Name() string { return "Quad9 secure DNS" }
func (*quad9Source) Options() SourceOptions {
return SourceOptions{}
}
func (s *quad9Source) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
if registered == "" {
return disabledResult(s.ID(), s.Name())
}
res := SourceResult{
SourceID: s.ID(),
SourceName: s.Name(),
Enabled: true,
LookupURL: "https://www.quad9.net/result/?domain=" + registered,
}
var (
secureAddrs, unsecuredAddrs []net.IPAddr
secureErr, unsecuredErr error
wg sync.WaitGroup
)
queryCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
wg.Add(2)
go func() {
defer wg.Done()
secureAddrs, secureErr = s.secure.LookupIPAddr(queryCtx, registered)
}()
go func() {
defer wg.Done()
unsecuredAddrs, unsecuredErr = s.unsecured.LookupIPAddr(queryCtx, registered)
}()
wg.Wait()
// Suppress the unused variable warning — secureAddrs is only used as an
// existence check; the actual IPs are not surfaced.
_ = secureAddrs
// If the unsecured resolver can't resolve the domain either, it simply
// doesn't exist — not a Quad9 block.
if unsecuredErr != nil || len(unsecuredAddrs) == 0 {
if unsecuredErr != nil {
if dnsErr, ok := unsecuredErr.(*net.DNSError); !ok || !dnsErr.IsNotFound {
res.Error = "unsecured resolver: " + unsecuredErr.Error()
}
}
return []SourceResult{res}
}
// Domain resolves on unsecured. Check whether the secure resolver blocked it.
if dnsErr, ok := secureErr.(*net.DNSError); ok && dnsErr.IsNotFound {
res.Reasons = []string{"Blocked by Quad9 threat intelligence"}
res.Evidence = append(res.Evidence, Evidence{
Label: "Secure resolver (9.9.9.9)",
Value: "NXDOMAIN",
})
} else if secureErr != nil {
res.Error = "secure resolver: " + secureErr.Error()
}
return []SourceResult{res}
}
func (*quad9Source) Evaluate(r SourceResult) (bool, string) {
return evidenceEval(r, SeverityCrit)
}
func (*quad9Source) Diagnose(res SourceResult) Diagnosis {
return Diagnosis{
Severity: SeverityCrit,
Title: "Blocked by Quad9 secure DNS resolver",
Detail: fmt.Sprintf(
"This domain is on Quad9's threat intelligence blocklist: the secure resolver (9.9.9.9) returns NXDOMAIN while the unsecured peer (9.9.9.10) resolves normally. Quad9 aggregates feeds from 18+ threat intel partners (abuse.ch, Bambenek, CINS, DShield, etc.). Visitors whose ISP or device uses Quad9 cannot reach this domain. Submit a false-positive report at the Quad9 contact page if you believe the listing is incorrect.",
),
Fix: "https://www.quad9.net/support/contact/",
FixIsURL: true,
LookupURL: res.LookupURL,
}
}