DNS blocklists asked in the background, verdicts kept (closes #104)
check / check (push) Waiting to run
check / check (push) Waiting to run
Zones in SWWAF_DNSBL_ZONES are asked about each client (RFC 5782 names) in the background, through the host's resolver or SWWAF_DNSBL_RESOLVER; no request waits. Verdicts last SWWAF_REPUTATION_CACHE_TTL and are kept in reputation.json, at most 100,000. After the blocklists, SWWAF_REPUTATION_ACTION (limit:25) denies, limits or logs a listed client; the log line names the zones, each raises reputation_hit, with metrics by zone. A failed, timed-out or refused query gives no verdict, raises source_failure, and pauses the zone a minute. 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. 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 is contained in:
+41
-8
@@ -2,7 +2,8 @@
|
||||
// SWWAF_STATE_DIR, as the "Persistent state" section of SPEC.md describes:
|
||||
// bans.json holds the bans, clients.json each client's counters and
|
||||
// history, lookups.json GeoJS's answers, reputation.json the last try and
|
||||
// last good copy of each list fetched from a URL, and alerts.json the
|
||||
// last good copy of each list fetched from a URL and the DNSBL zones'
|
||||
// verdicts, and alerts.json the
|
||||
// cooldowns, the hour under way, the alerts waiting for each destination
|
||||
// and the anomaly counters. Load
|
||||
// reads them at start, Watch takes in an admin's edit of one while
|
||||
@@ -76,13 +77,14 @@ type Params struct {
|
||||
// is (SWWAF_STATE_COUNTER_INTERVAL).
|
||||
WriteDelay time.Duration
|
||||
CounterInterval time.Duration
|
||||
// Ledger, Limiter, GeoJS, Lists, Alerts and Anomalies hold the state.
|
||||
// Alerts also receive a file_error alert for an edit set aside, and for
|
||||
// a write that fails while smallwebwaf runs.
|
||||
// Ledger, Limiter, GeoJS, Lists, DNSBL, Alerts and Anomalies hold the
|
||||
// state. Alerts also receive a file_error alert for an edit set aside,
|
||||
// and for a write that fails while smallwebwaf runs.
|
||||
Ledger *bans.Ledger
|
||||
Limiter *ratelimit.Limiter
|
||||
GeoJS *lookup.GeoJS
|
||||
Lists *reputation.Lists
|
||||
DNSBL *reputation.DNSBL
|
||||
Alerts *alerts.Queue
|
||||
Anomalies *anomaly.Counters
|
||||
// Now tells the time by which the counters' buckets run out, normally
|
||||
@@ -145,8 +147,9 @@ type lookupsFile struct {
|
||||
// reputationFile is reputation.json, indented for an admin to read and
|
||||
// edit, so that each line of a list's copy is on a line of its own.
|
||||
type reputationFile struct {
|
||||
Version int `json:"version"`
|
||||
Lists []reputation.List `json:"lists"`
|
||||
Version int `json:"version"`
|
||||
Lists []reputation.List `json:"lists"`
|
||||
Verdicts []reputation.Verdict `json:"verdicts"`
|
||||
}
|
||||
|
||||
// alertsFile is alerts.json, indented for an admin to read and edit.
|
||||
@@ -432,6 +435,7 @@ func (f *Files) takeIn(name string, data []byte, edit bool) (int, error) {
|
||||
return 0, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
f.params.DNSBL.Load(file.Verdicts)
|
||||
entries = len(file.Lists)
|
||||
case alertsJSON:
|
||||
waiting, err := f.takeInAlerts(path, data)
|
||||
@@ -567,6 +571,7 @@ func (f *Files) encode(name string) ([]byte, error) {
|
||||
case reputationJSON:
|
||||
return encodeIndented(reputationFile{
|
||||
Version: version, Lists: f.params.Lists.Snapshot(),
|
||||
Verdicts: f.params.DNSBL.Snapshot(),
|
||||
})
|
||||
default: // alerts.json
|
||||
held := f.params.Alerts.Snapshot()
|
||||
@@ -729,8 +734,12 @@ func (f *lookupsFile) check(data []byte) error {
|
||||
// check refuses a list without its URL, which would name no list, or the
|
||||
// time it was last tried, which would have it fetched at once, and a copy
|
||||
// of it without the time it was fetched, or without its lines, which hold
|
||||
// the list.
|
||||
func (f *reputationFile) check([]byte) error {
|
||||
// the list. It refuses a verdict without its zone or its client, which
|
||||
// would be about no one, whether the zone lists the client, or the time
|
||||
// it was fetched, which would drop it. A verdict's listed is false for a
|
||||
// client the zone does not list, which Verdicts cannot tell from a
|
||||
// missing one, so each listed is read again as written.
|
||||
func (f *reputationFile) check(data []byte) error {
|
||||
for i, kept := range f.Lists {
|
||||
switch {
|
||||
case kept.URL == "":
|
||||
@@ -744,6 +753,30 @@ func (f *reputationFile) check([]byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
var written struct {
|
||||
Verdicts []struct {
|
||||
Listed *bool `json:"listed"`
|
||||
} `json:"verdicts"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(data, &written)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, verdict := range f.Verdicts {
|
||||
switch {
|
||||
case verdict.Zone == "":
|
||||
return fmt.Errorf("verdicts %w", missing(i, "zone"))
|
||||
case !verdict.Client.IsValid():
|
||||
return fmt.Errorf("verdicts %w", missing(i, "client"))
|
||||
case written.Verdicts[i].Listed == nil:
|
||||
return fmt.Errorf("verdicts %w", missing(i, "listed"))
|
||||
case verdict.Fetched.IsZero():
|
||||
return fmt.Errorf("verdicts %w", missing(i, "fetched"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user