DNS blocklists asked in the background, verdicts kept (closes #104)
check / check (push) Waiting to run

Zones in SWWAF_DNSBL_ZONES are asked about each client in the background,
through SWWAF_DNSBL_RESOLVER or the host's resolver; no request waits.
Verdicts last SWWAF_REPUTATION_CACHE_TTL, kept in reputation.json.
SWWAF_REPUTATION_ACTION (limit:25) denies, limits or logs a listed
client; each zone listing it raises reputation_hit. A failed query gives
no verdict, raises source_failure, and pauses the zone a minute. A
zone's key, its first label under dq.spamhaus.net, is masked everywhere
but reputation.json. Zones compare without regard to case.

Judgement call: answers in 127.255.255.0/24 or outside 127.0.0.0/8 are failures.
Judgement call: the minute's pause after a failure; at most 1,000 queries at once.
Judgement call: one zone given with two keys stops the start as listed twice.
Rule suppressed: paralleltest on the DNSBL tests (Go's resolver shares state across synctest bubbles), funlen on the test of every logged setting.

Model: opus-5-5
This commit was merged in pull request #110.
This commit is contained in:
2026-10-07 21:09:51 +02:00
parent 2b8c98ba1f
commit 0b0f207423
18 changed files with 2330 additions and 236 deletions
+42 -12
View File
@@ -14,6 +14,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/remotelog"
"sneak.berlin/go/smallwebwaf/internal/reputation"
@@ -256,24 +257,53 @@ func (m *Metrics) AddLookupFile(lastRead func() time.Time, readFailures func() i
)
}
// AddReputation adds the metrics of the lists fetched from URLs, by
// source, each list's URL: the requests whose client a blocklist lists,
// which ReputationHit counts, and, read from lists as the metrics are
// asked for, the fetches that failed and when the copy in use was fetched.
// It is called once, before ReputationHit.
func (m *Metrics) AddReputation(lists *reputation.Lists) {
// AddReputation adds the metrics of the lists fetched from URLs and of the
// DNSBL zones, by source, each list's URL or each zone, its key masked as
// config.MaskZoneKey masks it: the requests whose client a blocklist or a
// zone's verdict lists, which ReputationHit counts, and, read from lists
// and dnsbl as the metrics are asked for, for a list, the fetches that
// failed and when the copy in use was fetched, and for a zone, the queries
// made and those that failed. It is called once, before ReputationHit.
func (m *Metrics) AddReputation(lists *reputation.Lists, dnsbl *reputation.DNSBL) {
const (
sourceLabel = "source"
failuresHelp = "Fetches of the list, or queries to the DNSBL zone, that failed."
)
m.reputationHits = counterVec("smallwebwaf_reputation_hits_total",
"Requests whose client a blocklist lists, by the blocklist's URL.",
[]string{"source"})
"Requests whose client a blocklist or a DNSBL zone lists, by the "+
"blocklist's URL or the zone.",
[]string{sourceLabel})
m.registry.MustRegister(m.reputationHits)
for _, zone := range dnsbl.Zones() {
source := prometheus.Labels{sourceLabel: config.MaskZoneKey(zone)}
m.registry.MustRegister(
prometheus.NewCounterFunc(prometheus.CounterOpts{
Name: "smallwebwaf_reputation_queries_total",
Help: "Queries to the DNSBL zone.",
ConstLabels: source,
}, func() float64 {
return float64(dnsbl.Queries(zone))
}),
prometheus.NewCounterFunc(prometheus.CounterOpts{
Name: "smallwebwaf_reputation_failures_total",
Help: failuresHelp,
ConstLabels: source,
}, func() float64 {
return float64(dnsbl.Failures(zone))
}),
)
}
for _, listURL := range lists.URLs() {
source := prometheus.Labels{"source": listURL}
source := prometheus.Labels{sourceLabel: listURL}
m.registry.MustRegister(
prometheus.NewCounterFunc(prometheus.CounterOpts{
Name: "smallwebwaf_reputation_failures_total",
Help: "Fetches of the list that failed.",
Help: failuresHelp,
ConstLabels: source,
}, func() float64 {
return float64(lists.Failures(listURL))
@@ -295,8 +325,8 @@ func (m *Metrics) AddReputation(lists *reputation.Lists) {
}
}
// ReputationHit counts a request whose client the blocklist at source, its
// URL, lists.
// ReputationHit counts a request whose client source lists: a blocklist,
// by its URL, or a DNSBL zone, its key masked.
func (m *Metrics) ReputationHit(source string) {
m.reputationHits.WithLabelValues(source).Inc()
}