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
|
|
@ -28,6 +28,7 @@ widely-used reputation systems.
|
||||||
| AlienVault OTX | HTTPS lookup | free (admin) | admin |
|
| AlienVault OTX | HTTPS lookup | free (admin) | admin |
|
||||||
| Pulsedive | HTTPS lookup | free (admin) | admin |
|
| Pulsedive | HTTPS lookup | free (admin) | admin |
|
||||||
| Criminal IP | HTTPS lookup | yes (admin) | admin |
|
| Criminal IP | HTTPS lookup | yes (admin) | admin |
|
||||||
|
| Quad9 secure DNS | DNS comparison | no | user (default on) |
|
||||||
|
|
||||||
### Obtaining API keys
|
### Obtaining API keys
|
||||||
|
|
||||||
|
|
|
||||||
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ func TestRegisteredSourcesAreSane(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// At least the built-in sources are present.
|
// At least the built-in sources are present.
|
||||||
for _, want := range []string{"dnsbl", "google_safe_browsing", "openphish", "pulsedive", "urlhaus", "virustotal"} {
|
for _, want := range []string{"dnsbl", "google_safe_browsing", "openphish", "pulsedive", "quad9", "urlhaus", "virustotal"} {
|
||||||
if !seen[want] {
|
if !seen[want] {
|
||||||
t.Errorf("missing built-in source %q", want)
|
t.Errorf("missing built-in source %q", want)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue