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.
117 lines
3.3 KiB
Go
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,
|
|
}
|
|
}
|