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 was merged in pull request #363.
This commit is contained in:
1 parent
63e58e326b
commit
0b23352767
118 files changed
+9447
-2670
No files matched your search
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user