Pause a registry's WHOIS server after it refuses a query (closes #218)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The fetcher kept one interval for every registry, so a registry that refused a query was asked again within seconds. RIPE blocked the test host's address that way. The WHOIS client now pauses an RIR's or NIC.br's server for an hour after the server refuses a query. A refused or reset connection is a refusal, and so is an %ERROR:201: or %ERROR:202: line. Lookups at the other servers go on. A lookup whose RIR is paused leaves the AS stale, and counts as neither a success nor a failure. IANA is never paused, because a pause of IANA would stop every lookup. NIC.br's hour-long skip after its over-limit answer is now the same pause. Model: opus-5-5
This commit was merged in pull request #221.
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||
"git.eeqj.de/sneak/routewatch/internal/logger"
|
||||
"git.eeqj.de/sneak/routewatch/internal/whois"
|
||||
)
|
||||
|
||||
// The WHOIS servers a lookup asks: IANA first, then the RIR IANA refers it
|
||||
@@ -131,6 +132,109 @@ func TestFetchAndUpdateKeepsTheRowWhenTheLookupFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFetchAndUpdateWaitsOutAPause looks up an AS at RIPE while RIPE refuses
|
||||
// the query. The test sets the WHOIS client's clock. The next lookup comes a
|
||||
// second before whois.PauseTime has passed. It sends RIPE nothing and leaves
|
||||
// the AS never looked up. It counts as neither a success nor a failure. A
|
||||
// lookup at whois.PauseTime asks RIPE again.
|
||||
func TestFetchAndUpdateWaitsOutAPause(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, db := newFetcherWithWHOISAnswers(t, map[string]string{
|
||||
ianaWHOISServer: ianaRefersToRIPE,
|
||||
ripeWHOISServer: ripeAccessDenied,
|
||||
})
|
||||
|
||||
ripeAsks := 0
|
||||
dial := f.whoisClient.DialContext
|
||||
f.whoisClient.DialContext = func(
|
||||
ctx context.Context, network, address string,
|
||||
) (net.Conn, error) {
|
||||
if address == ripeWHOISServer {
|
||||
ripeAsks++
|
||||
}
|
||||
|
||||
return dial(ctx, network, address)
|
||||
}
|
||||
|
||||
start := time.Date(2026, 11, 2, 0, 0, 0, 0, time.UTC)
|
||||
now := start
|
||||
f.whoisClient.Now = func() time.Time { return now }
|
||||
|
||||
f.fetchAndUpdate(t.Context(), testASNA)
|
||||
|
||||
now = start.Add(whois.PauseTime - time.Second)
|
||||
|
||||
f.fetchAndUpdate(t.Context(), testASNA)
|
||||
|
||||
if ripeAsks != 1 {
|
||||
t.Errorf("RIPE was asked %d times during its pause, want 1", ripeAsks)
|
||||
}
|
||||
|
||||
stats, err := db.GetWHOISStats(t.Context(), whoisStaleThreshold)
|
||||
if err != nil {
|
||||
t.Fatalf("GetWHOISStats: %v", err)
|
||||
}
|
||||
|
||||
if stats.NeverFetched != 1 {
|
||||
t.Errorf("never looked up = %d, want 1", stats.NeverFetched)
|
||||
}
|
||||
|
||||
got := f.GetStats()
|
||||
if got.ConsecutiveFails != 1 || got.ErrorsLastHour != 1 ||
|
||||
got.SuccessesLastHour != 0 {
|
||||
t.Errorf("%d failures in a row, %d errors, %d successes; want 1, 1, 0",
|
||||
got.ConsecutiveFails, got.ErrorsLastHour, got.SuccessesLastHour)
|
||||
}
|
||||
|
||||
now = start.Add(whois.PauseTime)
|
||||
|
||||
f.fetchAndUpdate(t.Context(), testASNA)
|
||||
|
||||
if ripeAsks != 2 {
|
||||
t.Errorf("RIPE was asked %d times after its pause, want 2", ripeAsks)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFetchAndUpdateKeepsAskingIANA looks up an AS while IANA's server
|
||||
// refuses the connection. The test sets the WHOIS client's clock. The next
|
||||
// lookup comes a second before whois.PauseTime has passed. IANA is never
|
||||
// paused, so that lookup asks IANA again. Each refusal counts as a failure.
|
||||
func TestFetchAndUpdateKeepsAskingIANA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// With no answers, every server refuses the connection.
|
||||
f, _ := newFetcherWithWHOISAnswers(t, map[string]string{})
|
||||
|
||||
ianaAsks := 0
|
||||
dial := f.whoisClient.DialContext
|
||||
f.whoisClient.DialContext = func(
|
||||
ctx context.Context, network, address string,
|
||||
) (net.Conn, error) {
|
||||
if address == ianaWHOISServer {
|
||||
ianaAsks++
|
||||
}
|
||||
|
||||
return dial(ctx, network, address)
|
||||
}
|
||||
|
||||
start := time.Date(2026, 11, 2, 0, 0, 0, 0, time.UTC)
|
||||
now := start
|
||||
f.whoisClient.Now = func() time.Time { return now }
|
||||
|
||||
f.fetchAndUpdate(t.Context(), testASNA)
|
||||
|
||||
now = start.Add(whois.PauseTime - time.Second)
|
||||
|
||||
f.fetchAndUpdate(t.Context(), testASNA)
|
||||
|
||||
got := f.GetStats()
|
||||
if ianaAsks != 2 || got.ConsecutiveFails != 2 {
|
||||
t.Errorf("IANA was asked %d times, with %d failures in a row; "+
|
||||
"want 2 and 2", ianaAsks, got.ConsecutiveFails)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRecordStepsTheInterval has the fetcher fail from its base interval of
|
||||
// 15 seconds, then succeed. Each failure makes the interval 1.5 times as
|
||||
// long, up to one minute, and each success halves it, down to one second.
|
||||
|
||||
Reference in New Issue
Block a user