Format Go with gofmt, then golines -m 77 (closes #215)
check / check (push) Waiting to run

script/fmt now runs golines -m 77 after gofmt, repeating golines until
it changes nothing, since one pass can leave a line the next splits.
script/fmt-check fails when either would change a file.
script/bootstrap installs golines v0.13.0 with go install, pinned to
its commit, and checks the installed version with go version -m.

The tree is reformatted. The longer lines this makes are fixed by
hand: nolint directives moved onto their own line above the statement,
one string split, and 20 functions over funlen's 80 lines shortened or
split, RunWAF and limitPercentages among them.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 14:50:46 +00:00
1 parent f9bec5146b
commit a8b24a843b
117 files changed
+9441 -2670

No files matched your search

+22 -5
View File
@@ -260,7 +260,11 @@ func (a *AbuseIPDB) LoadChecksSpent(checks ChecksSpent) {
// the score as client's, and notes the check as no longer under way. A
// check that fails gives no score: it is counted, logged and raised as a
// source_failure alert, and no client is checked for failureDelay.
func (a *AbuseIPDB) check(ctx context.Context, client netip.Prefix, addr netip.Addr) {
func (a *AbuseIPDB) check(
ctx context.Context,
client netip.Prefix,
addr netip.Addr,
) {
score, err := a.ask(ctx, addr)
now := a.params.Now()
@@ -269,7 +273,10 @@ func (a *AbuseIPDB) check(ctx context.Context, client netip.Prefix, addr netip.A
delete(a.checking, client)
if err == nil {
a.scores.Add(client, Score{Client: client, Score: score, Fetched: now})
a.scores.Add(
client,
Score{Client: client, Score: score, Fetched: now},
)
} else {
a.failures++
a.retryAt = now.Add(failureDelay)
@@ -285,7 +292,10 @@ func (a *AbuseIPDB) check(ctx context.Context, client netip.Prefix, addr netip.A
// ask asks AbuseIPDB for addr's abuse confidence score, sending the key
// in the header Key. An answer other than 200, one that gives no score,
// and none within Timeout, fail.
func (a *AbuseIPDB) ask(ctx context.Context, addr netip.Addr) (int64, error) {
func (a *AbuseIPDB) ask(
ctx context.Context,
addr netip.Addr,
) (int64, error) {
ctx, cancel := context.WithTimeout(ctx, a.params.Timeout)
defer cancel()
@@ -321,7 +331,8 @@ func (a *AbuseIPDB) ask(ctx context.Context, addr netip.Addr) (int64, error) {
} `json:"data"`
}
err = json.NewDecoder(io.LimitReader(res.Body, maxAnswerBytes)).Decode(&answer)
err = json.NewDecoder(io.LimitReader(res.Body, maxAnswerBytes)).
Decode(&answer)
if err != nil {
return 0, fmt.Errorf("read the answer: %w", err)
}
@@ -339,5 +350,11 @@ func (a *AbuseIPDB) alert(reason string, err error) {
// Raised before it is logged, so that the alert is there once the log
// line is.
raiseFailure(a.params.Alerts, reason, AbuseIPDBSource, err)
a.params.ProcessLog.Warn(reason, "source", AbuseIPDBSource, "error", err.Error())
a.params.ProcessLog.Warn(
reason,
"source",
AbuseIPDBSource,
"error",
err.Error(),
)
}
+127 -37
View File
@@ -41,7 +41,9 @@ func TestOnlyAnOffenderWithoutAScoreIsChecked(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
abuseIPDB := &abuseIPDBStandIn{scores: map[string]int64{suspect: 100}}
abuseIPDB := &abuseIPDBStandIn{
scores: map[string]int64{suspect: 100},
}
checker := newAbuseIPDB(abuseIPDB, abuseIPDBParams())
// A client that has committed no offence is not checked.
@@ -60,7 +62,9 @@ func TestOnlyAnOffenderWithoutAScoreIsChecked(t *testing.T) {
})
}
func TestIPv6ClientIsCheckedOnceAndItsScoreUsedForEachOfItsAddresses(t *testing.T) {
func TestIPv6ClientIsCheckedOnceAndItsScoreUsedForEachOfItsAddresses(
t *testing.T,
) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
@@ -68,10 +72,15 @@ func TestIPv6ClientIsCheckedOnceAndItsScoreUsedForEachOfItsAddresses(t *testing.
// it of its own.
var addresses []string
for i := 1; i < 16; i++ {
addresses = append(addresses, fmt.Sprintf("2001:db8:1:2:%x::9", i<<12))
addresses = append(
addresses,
fmt.Sprintf("2001:db8:1:2:%x::9", i<<12),
)
}
abuseIPDB := &abuseIPDBStandIn{scores: map[string]int64{addresses[0]: 100}}
abuseIPDB := &abuseIPDBStandIn{
scores: map[string]int64{addresses[0]: 100},
}
checker := newAbuseIPDB(abuseIPDB, abuseIPDBParams())
// A request from each has the client checked once, by the first.
@@ -96,7 +105,11 @@ func TestScoreAtOrOverTheMinimumIsAHit(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
scores := map[string]int64{"192.0.2.74": 74, "192.0.2.75": 75, "192.0.2.100": 100}
scores := map[string]int64{
"192.0.2.74": 74,
"192.0.2.75": 75,
"192.0.2.100": 100,
}
p := abuseIPDBParams()
p.MinScore = 75
checker := newAbuseIPDB(&abuseIPDBStandIn{scores: scores}, p)
@@ -117,7 +130,9 @@ func TestScoreUsedUntilTheCacheTTLHasPassedSinceItWasFetched(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
abuseIPDB := &abuseIPDBStandIn{scores: map[string]int64{suspect: 100}}
abuseIPDB := &abuseIPDBStandIn{
scores: map[string]int64{suspect: 100},
}
checker := newAbuseIPDB(abuseIPDB, abuseIPDBParams())
hitFrom(t, checker, suspect, true)
@@ -141,7 +156,9 @@ func TestScoreUsedUntilTheCacheTTLHasPassedSinceItWasFetched(t *testing.T) {
})
}
func TestDailyBudgetKeptAcrossARestartAndWholeAgainAsTheDayEnds(t *testing.T) {
func TestDailyBudgetKeptAcrossARestartAndWholeAgainAsTheDayEnds(
t *testing.T,
) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
@@ -152,7 +169,9 @@ func TestDailyBudgetKeptAcrossARestartAndWholeAgainAsTheDayEnds(t *testing.T) {
p.DailyBudget = 3
p.Alerts = queue
p.ProcessLog = slog.New(slog.NewJSONHandler(&log, nil))
abuseIPDB := &abuseIPDBStandIn{scores: map[string]int64{suspect: 100}}
abuseIPDB := &abuseIPDBStandIn{
scores: map[string]int64{suspect: 100},
}
checker := newAbuseIPDB(abuseIPDB, p)
// At noon, the first three offenders spend the budget, and the
@@ -173,8 +192,14 @@ func TestDailyBudgetKeptAcrossARestartAndWholeAgainAsTheDayEnds(t *testing.T) {
// The check that used the budget up raised the alert, and logged it.
const usedUp = "the daily budget of AbuseIPDB checks is used up"
wantFailureAlert(t, queue, time.Now(), usedUp,
"3 checks spent; none is made until the day ends at 00:00 UTC", 0)
wantFailureAlert(
t,
queue,
time.Now(),
usedUp,
"3 checks spent; none is made until the day ends at 00:00 UTC",
0,
)
if !strings.Contains(log.String(), `"msg":"`+usedUp+`"`) {
t.Errorf("logged\n%s\nwant the budget used up", log.String())
@@ -214,7 +239,9 @@ func TestChecksAreCountedInUTCDaysWhateverTheClocksZone(t *testing.T) {
p := abuseIPDBParams()
p.DailyBudget = 1
p.Now = func() time.Time { return time.Now().In(ahead) }
abuseIPDB := &abuseIPDBStandIn{scores: map[string]int64{suspect: 100}}
abuseIPDB := &abuseIPDBStandIn{
scores: map[string]int64{suspect: 100},
}
checker := newAbuseIPDB(abuseIPDB, p)
// At 09:00 UTC the one check is spent.
@@ -238,7 +265,9 @@ func TestChecksAreCountedInUTCDaysWhateverTheClocksZone(t *testing.T) {
})
}
func TestFailedCheckGivesNoScoreAndNoClientIsCheckedForAMinute(t *testing.T) {
func TestFailedCheckGivesNoScoreAndNoClientIsCheckedForAMinute(
t *testing.T,
) {
t.Parallel()
for _, tc := range []struct {
@@ -282,7 +311,10 @@ func TestFailedCheckGivesNoScoreAndNoClientIsCheckedForAMinute(t *testing.T) {
p.Key = tc.key
p.Alerts = queue
p.ProcessLog = slog.New(slog.NewJSONHandler(&log, nil))
abuseIPDB := &abuseIPDBStandIn{status: tc.status, body: tc.body}
abuseIPDB := &abuseIPDBStandIn{
status: tc.status,
body: tc.body,
}
checker := newAbuseIPDB(abuseIPDB, p)
// The failure gives no score, and no client is checked within a
@@ -310,8 +342,11 @@ func TestFailedCheckGivesNoScoreAndNoClientIsCheckedForAMinute(t *testing.T) {
wantFailureAlert(t, queue, time.Now().Add(-time.Minute),
"checking a client with AbuseIPDB failed", tc.error, 1)
if !strings.Contains(log.String(), `"msg":"checking a client with `+
`AbuseIPDB failed","source":"abuseipdb","error":"`+tc.error) {
if !strings.Contains(
log.String(),
`"msg":"checking a client with `+
`AbuseIPDB failed","source":"abuseipdb","error":"`+tc.error,
) {
t.Errorf("logged\n%s\nwant the failures", log.String())
}
})
@@ -319,7 +354,9 @@ func TestFailedCheckGivesNoScoreAndNoClientIsCheckedForAMinute(t *testing.T) {
}
}
func TestCheckNotAnsweredWithinTheTimeoutFailsAndHitNeverWaits(t *testing.T) {
func TestCheckNotAnsweredWithinTheTimeoutFailsAndHitNeverWaits(
t *testing.T,
) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
@@ -346,8 +383,14 @@ func TestCheckNotAnsweredWithinTheTimeoutFailsAndHitNeverWaits(t *testing.T) {
time.Sleep(time.Nanosecond)
synctest.Wait()
wantFailures(t, checker, 1)
wantFailureAlert(t, queue, time.Now(), "checking a client with AbuseIPDB failed",
"check the client: context deadline exceeded", 0)
wantFailureAlert(
t,
queue,
time.Now(),
"checking a client with AbuseIPDB failed",
"check the client: context deadline exceeded",
0,
)
})
}
@@ -361,10 +404,15 @@ func TestKeyIsSentInTheKeyHeaderAndNeverShown(t *testing.T) {
p := abuseIPDBParams()
p.Alerts = queue
p.ProcessLog = slog.New(slog.NewJSONHandler(&log, nil))
abuseIPDB := &abuseIPDBStandIn{scores: map[string]int64{suspect: 100}}
abuseIPDB := &abuseIPDBStandIn{
scores: map[string]int64{suspect: 100},
}
checker := newAbuseIPDB(abuseIPDB, p)
m := metrics.New(1, "app")
m.AddReputation(reputation.New(params()), reputation.NewDNSBL(dnsblParams()))
m.AddReputation(
reputation.New(params()),
reputation.NewDNSBL(dnsblParams()),
)
m.AddAbuseIPDB(checker)
// One check that AbuseIPDB answers, and one it refuses with an answer
@@ -373,7 +421,10 @@ func TestKeyIsSentInTheKeyHeaderAndNeverShown(t *testing.T) {
synctest.Wait()
wantScore(t, checker, suspect, true, 100, true)
abuseIPDB.answerWith(http.StatusUnauthorized, `{"errors":[{"detail":"`+key+`"}]}`)
abuseIPDB.answerWith(
http.StatusUnauthorized,
`{"errors":[{"detail":"`+key+`"}]}`,
)
wantScore(t, checker, other, true, 0, false)
synctest.Wait()
wantFailures(t, checker, 1)
@@ -416,7 +467,10 @@ func TestMetricsCountTheChecksTheFailuresAndTheBudgetLeft(t *testing.T) {
p.DailyBudget = 5
checker := newAbuseIPDB(abuseIPDB, p)
m := metrics.New(1, "app")
m.AddReputation(reputation.New(params()), reputation.NewDNSBL(dnsblParams()))
m.AddReputation(
reputation.New(params()),
reputation.NewDNSBL(dnsblParams()),
)
m.AddAbuseIPDB(checker)
// One check that AbuseIPDB answers, and one that fails.
@@ -472,7 +526,9 @@ func TestScoreFetchedATTLAgoIsNeitherUsedNorKept(t *testing.T) {
}
}
func TestAtMost100000ScoresKeptTheOneFetchedLongestAgoDroppedFirst(t *testing.T) {
func TestAtMost100000ScoresKeptTheOneFetchedLongestAgoDroppedFirst(
t *testing.T,
) {
t.Parallel()
now := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC)
@@ -501,7 +557,10 @@ func TestAtMost100000ScoresKeptTheOneFetchedLongestAgoDroppedFirst(t *testing.T)
got := checker.Snapshot()
if len(got) != count-1 || !slices.Contains(got, scores[0]) ||
slices.Contains(got, scores[count-1]) {
t.Errorf("%d scores kept, want all but the one fetched longest ago", len(got))
t.Errorf(
"%d scores kept, want all but the one fetched longest ago",
len(got),
)
}
}
@@ -522,7 +581,9 @@ type abuseIPDBStandIn struct {
// RoundTrip has the stand-in answer req, in place of the network. A check
// abandoned before the stand-in answers fails, as over the network.
func (s *abuseIPDBStandIn) RoundTrip(req *http.Request) (*http.Response, error) {
func (s *abuseIPDBStandIn) RoundTrip(
req *http.Request,
) (*http.Response, error) {
client := req.URL.Query().Get("ipAddress")
sent := req.Header.Get("Key")
@@ -542,8 +603,11 @@ func (s *abuseIPDBStandIn) RoundTrip(req *http.Request) (*http.Response, error)
status = http.StatusUnauthorized
case status == 0:
status = http.StatusOK
body = fmt.Sprintf(`{"data":{"ipAddress":%q,"abuseConfidenceScore":%d}}`, client,
score)
body = fmt.Sprintf(
`{"data":{"ipAddress":%q,"abuseConfidenceScore":%d}}`,
client,
score,
)
}
return &http.Response{
@@ -603,15 +667,25 @@ func newAbuseIPDB(
// wantScore checks the score checker gives client, and whether it is a
// hit, as a request from client finds them, offender or not.
func wantScore(
t *testing.T, checker *reputation.AbuseIPDB, client string, offender bool,
score int64, hit bool,
t *testing.T,
checker *reputation.AbuseIPDB,
client string,
offender bool,
score int64,
hit bool,
) {
t.Helper()
gotScore, gotHit := hitFrom(t, checker, client, offender)
if gotScore != score || gotHit != hit {
t.Errorf("%s has the score %d, a hit %t, want %d, %t", client, gotScore, gotHit,
score, hit)
t.Errorf(
"%s has the score %d, a hit %t, want %d, %t",
client,
gotScore,
gotHit,
score,
hit,
)
}
}
@@ -619,7 +693,10 @@ func wantScore(
// Its client is address for an IPv4 address, and its /64 for an IPv6 one,
// as sww counts clients.
func hitFrom(
t *testing.T, checker *reputation.AbuseIPDB, address string, offender bool,
t *testing.T,
checker *reputation.AbuseIPDB,
address string,
offender bool,
) (int64, bool) {
t.Helper()
@@ -663,7 +740,10 @@ func wantFailures(t *testing.T, checker *reputation.AbuseIPDB, want int) {
// the error failure, and that the cooldown has held back held repeats of
// it.
func wantFailureAlert(
t *testing.T, queue *alerts.Queue, raised time.Time, reason, failure string,
t *testing.T,
queue *alerts.Queue,
raised time.Time,
reason, failure string,
held int64,
) {
t.Helper()
@@ -673,8 +753,15 @@ func wantFailureAlert(
got[0].Event != alerts.EventSourceFailure || got[0].Reason != reason ||
got[0].Detail["source"] != reputation.AbuseIPDBSource ||
got[0].Detail["error"] != failure || queue.Suppressed() != held {
t.Errorf("alerts waiting %+v, %d held back, want only AbuseIPDB's %q with %q, "+
"and %d", got, queue.Suppressed(), reason, failure, held)
t.Errorf(
"alerts waiting %+v, %d held back, want only AbuseIPDB's %q with %q, "+
"and %d",
got,
queue.Suppressed(),
reason,
failure,
held,
)
}
}
@@ -692,8 +779,11 @@ func scrapeMetrics(t *testing.T, m *metrics.Metrics) string {
t.Helper()
scraped := httptest.NewRecorder()
m.ServeHTTP(scraped, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/",
http.NoBody))
m.ServeHTTP(
scraped,
httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/",
http.NoBody),
)
return scraped.Body.String()
}
+146 -37
View File
@@ -37,7 +37,9 @@ const (
rangeScope = "Range"
)
func TestCrowdSecDecisionBansItsNetblockUntilItEndsEvenWithTheEngineDown(t *testing.T) {
func TestCrowdSecDecisionBansItsNetblockUntilItEndsEvenWithTheEngineDown(
t *testing.T,
) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
@@ -48,7 +50,13 @@ func TestCrowdSecDecisionBansItsNetblockUntilItEndsEvenWithTheEngineDown(t *test
// A shorter decision on the same address, which is not the one
// used.
{"Ip", suspect, ban, sshBF, began.Add(4 * time.Hour)},
{rangeScope, "198.51.100.0/24", ban, probing, began.Add(time.Hour)},
{
rangeScope,
"198.51.100.0/24",
ban,
probing,
began.Add(time.Hour),
},
{"Ip", "2001:db8::1", ban, sshBF, began.Add(2 * time.Hour)},
// Left out: a decision to show a captcha, and one on a country.
{"Ip", "192.0.2.50", "captcha", probing, began.Add(time.Hour)},
@@ -74,14 +82,28 @@ func TestCrowdSecDecisionBansItsNetblockUntilItEndsEvenWithTheEngineDown(t *test
e.set(func(e *engine) { e.failing = true })
time.Sleep(time.Hour - time.Nanosecond)
synctest.Wait()
wantDecision(t, lists, "198.51.100.7",
reputation.Decision{Expires: began.Add(time.Hour), Scenario: probing})
wantDecision(
t,
lists,
"198.51.100.7",
reputation.Decision{
Expires: began.Add(time.Hour),
Scenario: probing,
},
)
time.Sleep(time.Nanosecond)
synctest.Wait()
wantDecision(t, lists, "198.51.100.7", reputation.Decision{})
wantDecision(t, lists, suspect,
reputation.Decision{Expires: began.Add(6 * time.Hour), Scenario: manual})
wantDecision(
t,
lists,
suspect,
reputation.Decision{
Expires: began.Add(6 * time.Hour),
Scenario: manual,
},
)
})
}
@@ -96,7 +118,10 @@ func TestCrowdSecDecisionListFetchedAgainEveryMinute(t *testing.T) {
lists := start(t, e, crowdSecParams())
wantEngineFetches(t, e, 1)
added := reputation.Decision{Expires: began.Add(2 * time.Hour), Scenario: probing}
added := reputation.Decision{
Expires: began.Add(2 * time.Hour),
Scenario: probing,
}
e.set(func(e *engine) {
e.decisions = append(e.decisions,
@@ -125,23 +150,58 @@ func TestCrowdSecDecisionOnAClientIsTheOneThatEndsLast(t *testing.T) {
// 198.51.100.130 is held by a decision on its address that ends
// after the one on its netblock, and 192.0.2.20 by one that ends
// before.
{rangeScope, "198.51.100.128/25", ban, sshBF, began.Add(time.Hour)},
{
rangeScope,
"198.51.100.128/25",
ban,
sshBF,
began.Add(time.Hour),
},
{"Ip", "198.51.100.130", ban, probing, began.Add(3 * time.Hour)},
{rangeScope, "192.0.2.0/24", ban, probing, began.Add(5 * time.Hour)},
{
rangeScope,
"192.0.2.0/24",
ban,
probing,
began.Add(5 * time.Hour),
},
{"Ip", "192.0.2.20", ban, sshBF, began.Add(2 * time.Hour)},
}}
lists := start(t, e, crowdSecParams())
wantDecision(t, lists, suspect,
reputation.Decision{Expires: began.Add(4 * time.Hour), Scenario: probing})
wantDecision(t, lists, "198.51.100.130",
reputation.Decision{Expires: began.Add(3 * time.Hour), Scenario: probing})
wantDecision(t, lists, "192.0.2.20",
reputation.Decision{Expires: began.Add(5 * time.Hour), Scenario: probing})
wantDecision(
t,
lists,
suspect,
reputation.Decision{
Expires: began.Add(4 * time.Hour),
Scenario: probing,
},
)
wantDecision(
t,
lists,
"198.51.100.130",
reputation.Decision{
Expires: began.Add(3 * time.Hour),
Scenario: probing,
},
)
wantDecision(
t,
lists,
"192.0.2.20",
reputation.Decision{
Expires: began.Add(5 * time.Hour),
Scenario: probing,
},
)
})
}
func TestCrowdSecAnswerOfNoDecisionIsAGoodCopyThatListsNoClient(t *testing.T) {
func TestCrowdSecAnswerOfNoDecisionIsAGoodCopyThatListsNoClient(
t *testing.T,
) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
@@ -203,27 +263,43 @@ func TestCrowdSecFailureKeepsTheLastGoodCopyAlertsOncePerCooldownAndHidesTheKey(
}
wantEngineFetches(t, e, 3)
wantDecision(t, lists, suspect,
reputation.Decision{Expires: began.Add(4 * time.Hour), Scenario: sshBF})
wantDecision(
t,
lists,
suspect,
reputation.Decision{
Expires: began.Add(4 * time.Hour),
Scenario: sshBF,
},
)
want := kept[0]
want.Tried = time.Now()
if got := lists.Snapshot(); !reflect.DeepEqual(got, []reputation.List{want}) {
t.Errorf("lists %+v, want the first copy, last tried now, %+v", got, want)
got := lists.Snapshot()
if !reflect.DeepEqual(got, []reputation.List{want}) {
t.Errorf(
"lists %+v, want the first copy, last tried now, %+v",
got,
want,
)
}
if lists.Failures(decisionsURL) != 2 {
t.Errorf("%d failures, want 2", lists.Failures(decisionsURL))
if n := lists.Failures(decisionsURL); n != 2 {
t.Errorf("%d failures, want 2", n)
}
// One alert for the first failure; the cooldown holds back the
// second.
wantAlert(t, queue,
fetchFailure(time.Now().Add(-time.Minute), decisionsURL, tc.error))
failed := time.Now().Add(-time.Minute)
alert := fetchFailure(failed, decisionsURL, tc.error)
wantAlert(t, queue, alert)
if !strings.Contains(log.String(), `"msg":"fetching a list failed",`+
`"url":"`+decisionsURL+`","error":"`+tc.error) {
if !strings.Contains(
log.String(),
`"msg":"fetching a list failed",`+
`"url":"`+decisionsURL+`","error":"`+tc.error,
) {
t.Errorf("logged\n%s\nwant the failures", log.String())
}
@@ -293,7 +369,11 @@ func crowdSecFailures() []crowdSecFailure {
// log, the alerts waiting in queue, and the copies of lists, which
// lists.json keeps.
func wantKeyNotShown(
t *testing.T, e *engine, log string, lists *reputation.Lists, queue *alerts.Queue,
t *testing.T,
e *engine,
log string,
lists *reputation.Lists,
queue *alerts.Queue,
) {
t.Helper()
@@ -315,13 +395,21 @@ func wantKeyNotShown(
}
}
func TestCrowdSecDecisionListKeptAcrossARestartEndsWhenItsDecisionsDo(t *testing.T) {
func TestCrowdSecDecisionListKeptAcrossARestartEndsWhenItsDecisionsDo(
t *testing.T,
) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
began := time.Now()
e := &engine{key: engineKey, decisions: []decision{
{rangeScope, "198.51.100.0/24", ban, probing, began.Add(time.Hour)},
{
rangeScope,
"198.51.100.0/24",
ban,
probing,
began.Add(time.Hour),
},
}}
lists := start(t, e, crowdSecParams())
kept := lists.Snapshot()
@@ -342,7 +430,10 @@ func TestCrowdSecDecisionListKeptAcrossARestartEndsWhenItsDecisionsDo(t *testing
run(t, again)
want := reputation.Decision{Expires: began.Add(time.Hour), Scenario: probing}
want := reputation.Decision{
Expires: began.Add(time.Hour),
Scenario: probing,
}
wantDecision(t, again, "198.51.100.7", want)
time.Sleep(30*time.Minute - time.Nanosecond)
@@ -415,7 +506,8 @@ func (e *engine) RoundTrip(req *http.Request) (*http.Response, error) {
e.fetches++
if req.URL.String() != decisionsURL && req.Header.Get("X-Api-Key") != "" {
if req.URL.String() != decisionsURL &&
req.Header.Get("X-Api-Key") != "" {
e.keySentTo = append(e.keySentTo, req.URL.String())
}
@@ -425,7 +517,9 @@ func (e *engine) RoundTrip(req *http.Request) (*http.Response, error) {
case req.URL.String() != decisionsURL || req.Header.Get("X-Api-Key") != e.key:
status, body = http.StatusForbidden, `{"message":"access forbidden"}`
case e.redirect != "":
status, header = http.StatusFound, http.Header{"Location": {e.redirect}}
status, header = http.StatusFound, http.Header{
"Location": {e.redirect},
}
case e.failing:
status, body = http.StatusServiceUnavailable, ""
case body == "":
@@ -449,7 +543,9 @@ func (e *engine) inForce(now time.Time) string {
for _, d := range e.decisions {
if now.Before(d.expires) {
answer = append(answer, map[string]string{
"duration": d.expires.Sub(now).String(), "origin": "crowdsec",
"duration": d.expires.Sub(now).
String(),
"origin": "crowdsec",
"scenario": d.scenario, "scope": d.scope, "type": d.kind, "value": d.value,
})
}
@@ -501,13 +597,26 @@ func wantEngineFetches(t *testing.T, e *engine, want int) {
// wantDecision checks the decision lists says is in force on addr now,
// the zero Decision for none.
func wantDecision(
t *testing.T, lists *reputation.Lists, addr string, want reputation.Decision,
t *testing.T,
lists *reputation.Lists,
addr string,
want reputation.Decision,
) {
t.Helper()
got, listed := lists.CrowdSecDecision(netip.MustParseAddr(addr), time.Now())
got, listed := lists.CrowdSecDecision(
netip.MustParseAddr(addr),
time.Now(),
)
if listed != !want.Expires.IsZero() ||
listed && (!got.Expires.Equal(want.Expires) || got.Scenario != want.Scenario) {
t.Errorf("%s has the decision %+v in force %t, want %+v", addr, got, listed, want)
listed &&
(!got.Expires.Equal(want.Expires) || got.Scenario != want.Scenario) {
t.Errorf(
"%s has the decision %+v in force %t, want %+v",
addr,
got,
listed,
want,
)
}
}
+9 -2
View File
@@ -203,7 +203,10 @@ func (d *DNSBL) Snapshot() []Verdict {
d.mu.Unlock()
slices.SortFunc(verdicts, func(a, b Verdict) int {
return cmp.Or(a.Client.Compare(b.Client), strings.Compare(a.Zone, b.Zone))
return cmp.Or(
a.Client.Compare(b.Client),
strings.Compare(a.Zone, b.Zone),
)
})
return verdicts
@@ -275,7 +278,11 @@ func (d *DNSBL) lookUp(ctx context.Context, q query) (bool, error) {
ctx, cancel := context.WithTimeout(ctx, d.params.Timeout)
defer cancel()
answer, err := d.resolver.LookupNetIP(ctx, "ip4", queryName(q.zone, q.client))
answer, err := d.resolver.LookupNetIP(
ctx,
"ip4",
queryName(q.zone, q.client),
)
var dnsErr *net.DNSError
+116 -34
View File
@@ -73,7 +73,11 @@ func TestMain(m *testing.M) {
return nil, errNoNetwork
},
}
_, _ = resolver.LookupNetIP(context.Background(), "ip4", "warm-up.invalid.")
_, _ = resolver.LookupNetIP(
context.Background(),
"ip4",
"warm-up.invalid.",
)
m.Run()
}
@@ -91,8 +95,10 @@ func TestZonesListOrNotClientsByTheirIPv4AndIPv6Addresses(t *testing.T) {
)
resolver := &resolverStandIn{answers: map[string]answer{
"99.2.0.192." + zone + ".": {addrs: []string{listing}},
v6Name + otherZone + ".": {addrs: []string{"127.0.0.4", "127.0.0.10"}},
"99.2.0.192." + zone + ".": {addrs: []string{listing}},
v6Name + otherZone + ".": {
addrs: []string{"127.0.0.4", "127.0.0.10"},
},
"99.2.0.192." + otherZone + ".": {},
}}
dnsbl := newDNSBL(resolver, dnsblParams(zone, otherZone))
@@ -131,8 +137,11 @@ func TestListedByNeverWaitsForAQuery(t *testing.T) {
})
}
//
//nolint:paralleltest // one at a time, as the comment at the top of this file says
func TestVerdictUsedUntilTheCacheTTLHasPassedSinceItWasFetched(t *testing.T) {
func TestVerdictUsedUntilTheCacheTTLHasPassedSinceItWasFetched(
t *testing.T,
) {
synctest.Test(t, func(t *testing.T) {
resolver := &resolverStandIn{answers: map[string]answer{
listedName: {addrs: []string{listing}},
@@ -198,8 +207,11 @@ func TestQueryNotAnsweredWithinTheTimeoutFails(t *testing.T) {
})
}
//
//nolint:paralleltest // one at a time, as the comment at the top of this file says
func TestZoneThatFailsOrRefusesGivesNoVerdictAndIsLeftAloneForAMinute(t *testing.T) {
func TestZoneThatFailsOrRefusesGivesNoVerdictAndIsLeftAloneForAMinute(
t *testing.T,
) {
for _, tc := range []struct {
name string
answer answer
@@ -230,9 +242,12 @@ func TestZoneThatFailsOrRefusesGivesNoVerdictAndIsLeftAloneForAMinute(t *testing
p := dnsblParams(zone)
p.Alerts = queue
p.ProcessLog = slog.New(slog.NewJSONHandler(&log, nil))
dnsbl := newDNSBL(&resolverStandIn{answers: map[string]answer{
listedName: tc.answer,
}}, p)
dnsbl := newDNSBL(
&resolverStandIn{answers: map[string]answer{
listedName: tc.answer,
}},
p,
)
// The failure gives no verdict, and the zone is not asked
// again within a minute of it.
@@ -258,11 +273,17 @@ func TestZoneThatFailsOrRefusesGivesNoVerdictAndIsLeftAloneForAMinute(t *testing
Time: time.Now().Add(-time.Minute),
Event: alerts.EventSourceFailure,
Reason: "asking a DNSBL zone failed",
Detail: map[string]any{"source": zone, "error": tc.error},
Detail: map[string]any{
"source": zone,
"error": tc.error,
},
})
if !strings.Contains(log.String(), `"msg":"asking a DNSBL zone failed",`+
`"zone":"`+zone+`","error":"`+tc.error) {
if !strings.Contains(
log.String(),
`"msg":"asking a DNSBL zone failed",`+
`"zone":"`+zone+`","error":"`+tc.error,
) {
t.Errorf("logged\n%s\nwant the failures", log.String())
}
})
@@ -302,8 +323,11 @@ func TestMetricsCountEachZonesQueriesAndThoseThatFailed(t *testing.T) {
synctest.Wait()
scraped := httptest.NewRecorder()
m.ServeHTTP(scraped, httptest.NewRequestWithContext(t.Context(), http.MethodGet,
"/", http.NoBody))
m.ServeHTTP(
scraped,
httptest.NewRequestWithContext(t.Context(), http.MethodGet,
"/", http.NoBody),
)
for series, want := range map[string]string{
"queries_total" + `{instance="app",source="` + zone + `"}`: "1",
@@ -348,19 +372,32 @@ func TestZoneKeyIsMaskedInTheVerdictsTheFailuresAndTheMetrics(t *testing.T) {
synctest.Wait()
wantZones(t, dnsbl, listed, masked)
if got := waiting(queue); len(got) != 1 || got[0].Detail["source"] != masked {
t.Errorf("alerts waiting %+v, want the failure's, from %s", got, masked)
if got := waiting(queue); len(got) != 1 ||
got[0].Detail["source"] != masked {
t.Errorf(
"alerts waiting %+v, want the failure's, from %s",
got,
masked,
)
}
scraped := httptest.NewRecorder()
m.ServeHTTP(scraped, httptest.NewRequestWithContext(t.Context(), http.MethodGet,
"/", http.NoBody))
m.ServeHTTP(
scraped,
httptest.NewRequestWithContext(t.Context(), http.MethodGet,
"/", http.NoBody),
)
for name, shown := range map[string]string{
"the log": log.String(), "the metrics": scraped.Body.String(),
} {
if strings.Contains(shown, key) || !strings.Contains(shown, masked) {
t.Errorf("%s shows the key, or does not name the zone:\n%s", name, shown)
if strings.Contains(shown, key) ||
!strings.Contains(shown, masked) {
t.Errorf(
"%s shows the key, or does not name the zone:\n%s",
name,
shown,
)
}
}
})
@@ -382,8 +419,17 @@ func TestVerdictsKeptAcrossARestart(t *testing.T) {
kept := dnsbl.Snapshot()
want := []reputation.Verdict{
{Zone: zone, Client: netip.MustParseAddr(listed), Listed: true, Fetched: fetched},
{Zone: zone, Client: netip.MustParseAddr(unlisted), Fetched: fetched},
{
Zone: zone,
Client: netip.MustParseAddr(listed),
Listed: true,
Fetched: fetched,
},
{
Zone: zone,
Client: netip.MustParseAddr(unlisted),
Fetched: fetched,
},
}
if !reflect.DeepEqual(kept, want) {
t.Errorf("verdicts %+v, want %+v", kept, want)
@@ -433,12 +479,17 @@ func TestNeitherAVerdictOfAZoneNotNamedNorOnePastItsTTLIsKept(t *testing.T) {
dnsbl.Load([]reputation.Verdict{notNamed, stale, inUse})
if got := dnsbl.Snapshot(); !reflect.DeepEqual(got, []reputation.Verdict{inUse}) {
if got := dnsbl.Snapshot(); !reflect.DeepEqual(
got,
[]reputation.Verdict{inUse},
) {
t.Errorf("verdicts %+v, want only %+v", got, inUse)
}
}
func TestAtMost100000VerdictsKeptTheOneFetchedLongestAgoDroppedFirst(t *testing.T) {
func TestAtMost100000VerdictsKeptTheOneFetchedLongestAgoDroppedFirst(
t *testing.T,
) {
t.Parallel()
now := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC)
@@ -466,7 +517,10 @@ func TestAtMost100000VerdictsKeptTheOneFetchedLongestAgoDroppedFirst(t *testing.
got := dnsbl.Snapshot()
if len(got) != count-1 || !slices.Contains(got, verdicts[0]) ||
slices.Contains(got, verdicts[count-1]) {
t.Errorf("%d verdicts kept, want all but the one fetched longest ago", len(got))
t.Errorf(
"%d verdicts kept, want all but the one fetched longest ago",
len(got),
)
}
}
@@ -476,7 +530,11 @@ func TestQueriesGoToTheResolverSWWDNSBLResolverNames(t *testing.T) {
listedName: {addrs: []string{listing}},
}}
conn, err := (&net.ListenConfig{}).ListenPacket(t.Context(), "udp", "127.0.0.1:0")
conn, err := (&net.ListenConfig{}).ListenPacket(
t.Context(),
"udp",
"127.0.0.1:0",
)
if err != nil {
t.Fatalf("listen: %v", err)
}
@@ -500,7 +558,8 @@ func TestQueriesGoToTheResolverSWWDNSBLResolverNames(t *testing.T) {
// process held up for a whole minute would see the query fail.
p.Timeout = time.Minute
isListed, err := reputation.NewDNSBL(p).LookUp(zone, netip.MustParseAddr(listed))
isListed, err := reputation.NewDNSBL(p).
LookUp(zone, netip.MustParseAddr(listed))
if err != nil || !isListed {
t.Errorf("listed %t (%v), want true", isListed, err)
}
@@ -554,7 +613,11 @@ func (s *resolverStandIn) set(name string, given answer) {
// dial connects Go's resolver to the stand-in through an in-memory
// connection, on which it sends each query, and reads each reply, after
// its length, as over TCP.
func (s *resolverStandIn) dial(context.Context, string, string) (net.Conn, error) {
func (s *resolverStandIn) dial(
context.Context,
string,
string,
) (net.Conn, error) {
client, server := net.Pipe()
go s.serve(server)
@@ -590,8 +653,10 @@ func (s *resolverStandIn) serve(conn net.Conn) {
}
//nolint:gosec // a reply of a few dozen bytes
_, err = conn.Write(append(binary.BigEndian.AppendUint16(nil, uint16(len(reply))),
reply...))
_, err = conn.Write(
append(binary.BigEndian.AppendUint16(nil, uint16(len(reply))),
reply...),
)
if err != nil {
return
}
@@ -692,7 +757,10 @@ func waitForTheResolver() {
}
// newDNSBL returns the DNSBL of p, asking resolver.
func newDNSBL(resolver *resolverStandIn, p reputation.DNSBLParams) *reputation.DNSBL {
func newDNSBL(
resolver *resolverStandIn,
p reputation.DNSBLParams,
) *reputation.DNSBL {
dnsbl := reputation.NewDNSBL(p)
dnsbl.SetDial(resolver.dial)
@@ -701,7 +769,12 @@ func newDNSBL(resolver *resolverStandIn, p reputation.DNSBLParams) *reputation.D
// wantZones checks the zones whose verdict dnsbl says lists client, as a
// request from client finds them.
func wantZones(t *testing.T, dnsbl *reputation.DNSBL, client string, want ...string) {
func wantZones(
t *testing.T,
dnsbl *reputation.DNSBL,
client string,
want ...string,
) {
t.Helper()
got := dnsbl.ListedBy(t.Context(), netip.MustParseAddr(client))
@@ -712,12 +785,21 @@ func wantZones(t *testing.T, dnsbl *reputation.DNSBL, client string, want ...str
// wantQueries checks how many queries dnsbl made to zone, and how many of
// them failed.
func wantQueries(t *testing.T, dnsbl *reputation.DNSBL, queries, failures int) {
func wantQueries(
t *testing.T,
dnsbl *reputation.DNSBL,
queries, failures int,
) {
t.Helper()
if dnsbl.Queries(zone) != queries || dnsbl.Failures(zone) != failures {
t.Errorf("%d queries and %d failures, want %d and %d", dnsbl.Queries(zone),
dnsbl.Failures(zone), queries, failures)
t.Errorf(
"%d queries and %d failures, want %d and %d",
dnsbl.Queries(zone),
dnsbl.Failures(zone),
queries,
failures,
)
}
}
+46 -12
View File
@@ -45,13 +45,16 @@ const (
)
var (
errStatus = errors.New("the server answered")
errTooLong = errors.New("the list is longer than 16 MiB")
errNotNetblock = errors.New("is not an address or a netblock, such as 192.0.2.0/24")
errStatus = errors.New("the server answered")
errTooLong = errors.New("the list is longer than 16 MiB")
errNotNetblock = errors.New(
"is not an address or a netblock, such as 192.0.2.0/24",
)
errNotASNPercent = errors.New(
"is not an AS number, : and a percentage, such as AS64496:50")
errNotDecision = errors.New(
"does not give an address or a netblock and a duration, such as 4h0m0s")
"does not give an address or a netblock and a duration, such as 4h0m0s",
)
)
// List is a list as lists.json holds it: the URL it is fetched from,
@@ -226,7 +229,10 @@ func (l *Lists) ASNLimitPercent(asn string) (int64, bool) {
// decision list on a netblock that holds addr and that ends last, and
// whether it is still in force at now. A decision that has ended no
// longer bans, even before the next fetch drops it.
func (l *Lists) CrowdSecDecision(addr netip.Addr, now time.Time) (Decision, bool) {
func (l *Lists) CrowdSecDecision(
addr netip.Addr,
now time.Time,
) (Decision, bool) {
if l.params.CrowdSecDecisionsURL == "" {
return Decision{}, false
}
@@ -431,12 +437,24 @@ func (l *Lists) fetch(ctx context.Context, listURL string) {
// Raised before it is logged, so that the alert is there once the
// log line is.
raiseFailure(l.params.Alerts, failed, listURL, err)
l.params.ProcessLog.Warn(failed, "url", listURL, "error", err.Error())
l.params.ProcessLog.Warn(
failed,
"url",
listURL,
"error",
err.Error(),
)
return
}
l.params.ProcessLog.Info("fetched a list", "url", listURL, "lines", len(lines))
l.params.ProcessLog.Info(
"fetched a list",
"url",
listURL,
"lines",
len(lines),
)
}
// raiseFailure raises a source_failure alert into queue, with reason, and
@@ -459,7 +477,12 @@ func (l *Lists) get(ctx context.Context, listURL string) ([]string, error) {
ctx, cancel := context.WithTimeout(ctx, fetchTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, listURL, http.NoBody)
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
listURL,
http.NoBody,
)
if err != nil {
return nil, fmt.Errorf("make the request: %w", err)
}
@@ -570,7 +593,10 @@ func parseNetblock(text string) (netip.Prefix, bool) {
return netip.Prefix{}, false
}
return netip.PrefixFrom(netblock.Addr().Unmap(), netblock.Bits()-mappedBits), true
return netip.PrefixFrom(
netblock.Addr().Unmap(),
netblock.Bits()-mappedBits,
), true
}
// parsePercents reads the lines of the file of AS:percent lines, each an
@@ -628,7 +654,8 @@ func parseDecisions(lines []string, fetched time.Time) (entries, error) {
found := entries{decisions: map[netip.Prefix]Decision{}}
for i, decision := range answer {
if decision.Type != "ban" || (decision.Scope != "Ip" && decision.Scope != "Range") {
if decision.Type != "ban" ||
(decision.Scope != "Ip" && decision.Scope != "Range") {
continue
}
@@ -636,12 +663,19 @@ func parseDecisions(lines []string, fetched time.Time) (entries, error) {
duration, err := time.ParseDuration(decision.Duration)
if !ok || err != nil {
return entries{}, fmt.Errorf("decision %d %w", i+1, errNotDecision)
return entries{}, fmt.Errorf(
"decision %d %w",
i+1,
errNotDecision,
)
}
expires := fetched.Add(duration)
if expires.After(found.decisions[netblock].Expires) {
found.decisions[netblock] = Decision{Expires: expires, Scenario: decision.Scenario}
found.decisions[netblock] = Decision{
Expires: expires,
Scenario: decision.Scenario,
}
}
if !slices.Contains(found.lengths, netblock.Bits()) {
+133 -55
View File
@@ -72,7 +72,9 @@ func TestListedAddressesAndNetblocksWithTheCommentsLeftOut(t *testing.T) {
})
}
func TestIPv4MappedLineListsTheIPv4AddressOrNetblockItStandsFor(t *testing.T) {
func TestIPv4MappedLineListsTheIPv4AddressOrNetblockItStandsFor(
t *testing.T,
) {
t.Parallel()
now := time.Now()
@@ -126,7 +128,9 @@ func TestListFetchedAgainOnceRefreshHasPassed(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
servers := &standIn{lists: map[string]string{dropURL: "203.0.113.9\n"}}
servers := &standIn{
lists: map[string]string{dropURL: "203.0.113.9\n"},
}
lists := start(t, servers, params(dropURL))
began := time.Now()
@@ -142,41 +146,21 @@ func TestListFetchedAgainOnceRefreshHasPassed(t *testing.T) {
wantListedBy(t, lists, "203.0.113.9")
wantListedBy(t, lists, "203.0.113.10", dropURL)
if fetched := lists.Fetched(dropURL); !fetched.Equal(began.Add(refresh)) {
if fetched := lists.Fetched(dropURL); !fetched.Equal(
began.Add(refresh),
) {
t.Errorf("the copy in use was fetched at %s, want %s", fetched,
began.Add(refresh))
}
})
}
func TestFailedFetchKeepsTheLastGoodCopyAndAlertsOncePerCooldown(t *testing.T) {
func TestFailedFetchKeepsTheLastGoodCopyAndAlertsOncePerCooldown(
t *testing.T,
) {
t.Parallel()
for _, tc := range []struct {
name string
// fail has the stand-in answer the fetches after the first so that
// they fail with error.
fail func(servers *standIn)
error string
}{
{
"an answer other than 200",
func(servers *standIn) { servers.set(dropURL, "") },
"the server answered 503 Service Unavailable",
},
{
"a line that does not read",
func(servers *standIn) { servers.set(dropURL, "203.0.113.10\n<html>\n") },
"line 2 is not an address or a netblock, such as 192.0.2.0/24",
},
{
"a list longer than 16 MiB",
func(servers *standIn) {
servers.set(dropURL, strings.Repeat("#\n", 8<<20+1))
},
"the list is longer than 16 MiB",
},
} {
for _, tc := range listFailures() {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -205,8 +189,13 @@ func TestFailedFetchKeepsTheLastGoodCopyAndAlertsOncePerCooldown(t *testing.T) {
want := kept[0]
want.Tried = time.Now()
if got := lists.Snapshot(); !reflect.DeepEqual(got, []reputation.List{want}) {
t.Errorf("lists %+v, want the first copy, last tried now, %+v", got, want)
got := lists.Snapshot()
if !reflect.DeepEqual(got, []reputation.List{want}) {
t.Errorf(
"lists %+v, want the first copy, last tried now, %+v",
got,
want,
)
}
if lists.Failures(dropURL) != 2 {
@@ -215,10 +204,14 @@ func TestFailedFetchKeepsTheLastGoodCopyAndAlertsOncePerCooldown(t *testing.T) {
// One alert for the first failure; the cooldown holds back the
// second.
wantAlert(t, queue, fetchFailure(time.Now().Add(-refresh), dropURL, tc.error))
failed := time.Now().Add(-refresh)
wantAlert(t, queue, fetchFailure(failed, dropURL, tc.error))
if !strings.Contains(log.String(), `"msg":"fetching a list failed",`+
`"url":"`+dropURL+`","error":"`+tc.error) {
if !strings.Contains(
log.String(),
`"msg":"fetching a list failed",`+
`"url":"`+dropURL+`","error":"`+tc.error,
) {
t.Errorf("logged\n%s\nwant the failures", log.String())
}
})
@@ -226,6 +219,40 @@ func TestFailedFetchKeepsTheLastGoodCopyAndAlertsOncePerCooldown(t *testing.T) {
}
}
// listFailure is a way a list's fetch can fail.
type listFailure struct {
name string
// fail has the stand-in answer the fetches after the first so that
// they fail with error.
fail func(servers *standIn)
error string
}
// listFailures returns the ways a list's fetch can fail.
func listFailures() []listFailure {
return []listFailure{
{
"an answer other than 200",
func(servers *standIn) { servers.set(dropURL, "") },
"the server answered 503 Service Unavailable",
},
{
"a line that does not read",
func(servers *standIn) {
servers.set(dropURL, "203.0.113.10\n<html>\n")
},
"line 2 is not an address or a netblock, such as 192.0.2.0/24",
},
{
"a list longer than 16 MiB",
func(servers *standIn) {
servers.set(dropURL, strings.Repeat("#\n", 8<<20+1))
},
"the list is longer than 16 MiB",
},
}
}
func TestFetchNotDoneWithinAMinuteFails(t *testing.T) {
t.Parallel()
@@ -237,19 +264,27 @@ func TestFetchNotDoneWithinAMinuteFails(t *testing.T) {
synctest.Wait()
if lists.Failures(dropURL) != 0 {
t.Errorf("%d failures before a minute, want none", lists.Failures(dropURL))
t.Errorf(
"%d failures before a minute, want none",
lists.Failures(dropURL),
)
}
time.Sleep(time.Nanosecond)
synctest.Wait()
if lists.Failures(dropURL) != 1 {
t.Errorf("%d failures after a minute, want 1", lists.Failures(dropURL))
t.Errorf(
"%d failures after a minute, want 1",
lists.Failures(dropURL),
)
}
})
}
func TestKeptCopyIsFetchedAgainOnceRefreshHasPassedSinceItWasFetched(t *testing.T) {
func TestKeptCopyIsFetchedAgainOnceRefreshHasPassedSinceItWasFetched(
t *testing.T,
) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
@@ -262,8 +297,16 @@ func TestKeptCopyIsFetchedAgainOnceRefreshHasPassedSinceItWasFetched(t *testing.
// drop.txt was fetched an hour ago, and tor.txt a refresh ago, as
// lists.json says at start.
err := lists.Load([]reputation.List{
{URL: dropURL, Fetched: time.Now().Add(-time.Hour), Lines: []string{"203.0.113.9"}},
{URL: torURL, Fetched: time.Now().Add(-refresh), Lines: []string{"198.51.100.9"}},
{
URL: dropURL,
Fetched: time.Now().Add(-time.Hour),
Lines: []string{"203.0.113.9"},
},
{
URL: torURL,
Fetched: time.Now().Add(-refresh),
Lines: []string{"198.51.100.9"},
},
})
if err != nil {
t.Fatalf("load: %v", err)
@@ -290,7 +333,9 @@ func TestRestartWaitsRefreshAfterTheLastTryEvenOneThatFailed(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
// drop.txt is fetched, and a refresh later the fetch downloads it
// whole but fails on a line that does not read.
servers := &standIn{lists: map[string]string{dropURL: "198.51.100.1\n"}}
servers := &standIn{
lists: map[string]string{dropURL: "198.51.100.1\n"},
}
lists := start(t, servers, params(dropURL))
servers.set(dropURL, "198.51.100.2\n<html>\n")
@@ -299,7 +344,9 @@ func TestRestartWaitsRefreshAfterTheLastTryEvenOneThatFailed(t *testing.T) {
// Restarted with what lists.json keeps, it waits a refresh
// after the failed try, as it does while it runs.
restarted := &standIn{lists: map[string]string{dropURL: "198.51.100.2\n"}}
restarted := &standIn{
lists: map[string]string{dropURL: "198.51.100.2\n"},
}
again := reputation.New(params(dropURL))
again.SetTransport(restarted)
@@ -349,8 +396,11 @@ func TestFetchCutOffAsItStopsIsNoFailureButARestartWaitsForIt(t *testing.T) {
wantFetches(t, servers, 1)
if lists.Failures(dropURL) != 0 || len(waiting(queue)) != 0 {
t.Errorf("%d failures and alerts %+v, want none", lists.Failures(dropURL),
waiting(queue))
t.Errorf(
"%d failures and alerts %+v, want none",
lists.Failures(dropURL),
waiting(queue),
)
}
// Restarted an hour later with what lists.json keeps, it fetches
@@ -382,7 +432,9 @@ func TestFetchCutOffAsItStopsIsNoFailureButARestartWaitsForIt(t *testing.T) {
})
}
func TestASNLimitPercentFileGivesEachASNumberItsLowestPercentage(t *testing.T) {
func TestASNLimitPercentFileGivesEachASNumberItsLowestPercentage(
t *testing.T,
) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
@@ -397,7 +449,13 @@ func TestASNLimitPercentFileGivesEachASNumberItsLowestPercentage(t *testing.T) {
for asn, want := range map[string]int64{"AS14061": 10, "AS16276": 25} {
percent, listed := lists.ASNLimitPercent(asn)
if !listed || percent != want {
t.Errorf("%s has %d (listed %t), want %d", asn, percent, listed, want)
t.Errorf(
"%s has %d (listed %t), want %d",
asn,
percent,
listed,
want,
)
}
}
@@ -416,7 +474,9 @@ func TestASNLimitPercentFileGivesEachASNumberItsLowestPercentage(t *testing.T) {
})
}
func TestLoadDropsCopiesOfListsNotNamedAndRefusesOnesThatDoNotRead(t *testing.T) {
func TestLoadDropsCopiesOfListsNotNamedAndRefusesOnesThatDoNotRead(
t *testing.T,
) {
t.Parallel()
fetched := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
@@ -432,13 +492,18 @@ func TestLoadDropsCopiesOfListsNotNamedAndRefusesOnesThatDoNotRead(t *testing.T)
t.Fatalf("load: %v", err)
}
if got := lists.Snapshot(); !reflect.DeepEqual(got, []reputation.List{kept}) {
if got := lists.Snapshot(); !reflect.DeepEqual(
got,
[]reputation.List{kept},
) {
t.Errorf("copies %+v, want only %+v", got, kept)
}
err = lists.Load([]reputation.List{{URL: dropURL, Fetched: fetched, Lines: []string{
"; DROP", "203.0.113.300",
}}})
err = lists.Load(
[]reputation.List{{URL: dropURL, Fetched: fetched, Lines: []string{
"; DROP", "203.0.113.300",
}}},
)
const want = "the copy of " + dropURL +
": line 2 is not an address or a netblock, such as 192.0.2.0/24"
@@ -446,7 +511,10 @@ func TestLoadDropsCopiesOfListsNotNamedAndRefusesOnesThatDoNotRead(t *testing.T)
t.Errorf("error %v, want %s", err, want)
}
if got := lists.Snapshot(); !reflect.DeepEqual(got, []reputation.List{kept}) {
if got := lists.Snapshot(); !reflect.DeepEqual(
got,
[]reputation.List{kept},
) {
t.Errorf("copies %+v after the error, want %+v still", got, kept)
}
}
@@ -580,7 +648,12 @@ func wantFetches(t *testing.T, servers *standIn, want int) {
}
// wantListedBy checks the URLs of the blocklists lists says list addr.
func wantListedBy(t *testing.T, lists *reputation.Lists, addr string, want ...string) {
func wantListedBy(
t *testing.T,
lists *reputation.Lists,
addr string,
want ...string,
) {
t.Helper()
got := lists.ListedBy(netip.MustParseAddr(addr))
@@ -611,8 +684,13 @@ func wantAlert(t *testing.T, queue *alerts.Queue, want alerts.Alert) {
t.Helper()
got := waiting(queue)
if len(got) != 1 || !reflect.DeepEqual(got[0], want) || queue.Suppressed() != 1 {
t.Errorf("alerts waiting %+v, %d held back, want only %+v and 1", got,
queue.Suppressed(), want)
if len(got) != 1 || !reflect.DeepEqual(got[0], want) ||
queue.Suppressed() != 1 {
t.Errorf(
"alerts waiting %+v, %d held back, want only %+v and 1",
got,
queue.Suppressed(),
want,
)
}
}