Add Quad9 secure DNS blocklist source
Detects domains blocked by Quad9's threat intelligence by comparing the secure resolver (9.9.9.9) against the unsecured peer (9.9.9.10). No API key required; enabled by default via the enable_quad9 user option.
This commit is contained in:
parent
661e67d9c2
commit
219d9353c3
3 changed files with 129 additions and 1 deletions
127
checker/quad9.go
Normal file
127
checker/quad9.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
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{
|
||||
User: []sdk.CheckerOptionField{
|
||||
{
|
||||
Id: "enable_quad9",
|
||||
Type: "bool",
|
||||
Label: "Use Quad9 secure DNS check",
|
||||
Description: "Compare Quad9's secure resolver (9.9.9.9) against its unsecured peer (9.9.9.10). A domain that resolves on the unsecured but returns NXDOMAIN on the secure resolver is blocked by Quad9's threat intelligence.",
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *quad9Source) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
||||
if !sdk.GetBoolOption(opts, "enable_quad9", true) || 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,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue