AS number and country looked up for every client (closes #95)
check / check (push) Waiting to run
check / check (push) Waiting to run
GeoJS's geo.json is asked about every new visitor unless SWWAF_LOOKUP_SOURCE is off. A request waits for its client's first answer only while a country list or SWWAF_ADD_LOOKUP_HEADERS needs it; otherwise the answer reaches the client's history and ban notes when it comes. The AS number and name go beside the country in the request log, history, ban notes, alerts and lookups.json, with metrics by AS number; 64512 counts as unknown. A client's own X-Client-ASN and X-Client-Country never reach the app, whatever the setting says, and make example-app sends no address to GeoJS. Judgement call: AS numbers are written AS64496, as SPEC's settings write them. Judgement call: SWWAF_LOOKUP_TIMEOUT is added, default 1s, and cannot be off. Model: opus-5-5
This commit is contained in:
+183
-12
@@ -23,9 +23,13 @@ import (
|
||||
|
||||
const (
|
||||
// germany is where the stand-in for GeoJS places every address but
|
||||
// unplaced.
|
||||
germany = "DE"
|
||||
// unplaced is the address it cannot place.
|
||||
// unplaced, and asNumber, kept as asn, and asName the AS it gives them.
|
||||
germany = "DE"
|
||||
asNumber = 64496
|
||||
asn = "AS64496"
|
||||
asName = "Example Net"
|
||||
// unplaced is the address it cannot place, for which it gives the AS
|
||||
// number 64512 and the AS name Unknown, as GeoJS does.
|
||||
unplaced = "192.0.2.1"
|
||||
// leftOut is the address it leaves out of its answer when
|
||||
// answeringWithoutLeftOut.
|
||||
@@ -83,7 +87,7 @@ func TestNewClientWaitsAtMostOneSecondThenCountsAsNotFound(t *testing.T) {
|
||||
|
||||
var earlier sync.WaitGroup
|
||||
|
||||
earlier.Go(func() { g.Country(t.Context(), netip.MustParsePrefix("203.0.113.1/32")) })
|
||||
earlier.Go(func() { g.LookUp(t.Context(), netip.MustParsePrefix("203.0.113.1/32")) })
|
||||
defer earlier.Wait()
|
||||
|
||||
waitForRequests(t, geojs, 1)
|
||||
@@ -115,6 +119,58 @@ func TestNewClientWaitsAtMostOneSecondThenCountsAsNotFound(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRequestWaitsAsLongAsTheTimeoutSaysAndGeoJSIsAbandonedAfterIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
// A timeout longer than the default second, and a GeoJS that does
|
||||
// not answer.
|
||||
const longerTimeout = 3 * time.Second
|
||||
|
||||
m := metrics.New(1, "app")
|
||||
g := lookup.New(lookup.Params{
|
||||
URL: lookup.URL,
|
||||
Timeout: longerTimeout,
|
||||
Wait: true,
|
||||
Now: time.Now,
|
||||
ProcessLog: slog.New(slog.DiscardHandler),
|
||||
Metrics: m,
|
||||
Alerts: alerts.New(alerts.Params{}),
|
||||
})
|
||||
g.SetTransport(&standIn{answers: hanging})
|
||||
|
||||
var (
|
||||
request sync.WaitGroup
|
||||
waited time.Duration
|
||||
)
|
||||
|
||||
request.Go(func() {
|
||||
began := time.Now()
|
||||
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.9/32"), "")
|
||||
|
||||
waited = time.Since(began)
|
||||
})
|
||||
|
||||
// A moment before the timeout runs out, GeoJS is still being asked:
|
||||
// the request to it has not failed.
|
||||
time.Sleep(longerTimeout - time.Millisecond)
|
||||
synctest.Wait()
|
||||
wantFailures(t, m, 0)
|
||||
|
||||
// As it runs out, the client's request goes on, and the request to
|
||||
// GeoJS is abandoned, which counts as a failure.
|
||||
request.Wait()
|
||||
synctest.Wait()
|
||||
|
||||
if waited != longerTimeout {
|
||||
t.Errorf("waited %s for the answer, want %s", waited, longerTimeout)
|
||||
}
|
||||
|
||||
wantFailures(t, m, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddressLeftOutOfAnAnswerIsAskedAboutAgain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -187,6 +243,96 @@ func TestCountryIsKeptInCapitals(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnswerHoldsTheASNumberTheASNameAndTheCountry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
_, clock, g := start()
|
||||
placed := netip.MustParsePrefix("203.0.113.9/32")
|
||||
notPlaced := netip.MustParsePrefix(unplaced + "/32")
|
||||
now := clock.Now()
|
||||
|
||||
// For the client it cannot place, GeoJS gives the AS number 64512
|
||||
// and the AS name Unknown, which count as unknown.
|
||||
for client, want := range map[netip.Prefix]lookup.Answer{
|
||||
placed: {
|
||||
Client: placed, ASN: asn, ASName: asName, Country: germany,
|
||||
Answered: now, Used: now,
|
||||
},
|
||||
notPlaced: {Client: notPlaced, Answered: now, Used: now},
|
||||
} {
|
||||
got := g.LookUp(t.Context(), client)
|
||||
if got != want {
|
||||
t.Errorf("answer for %s\n%+v\nwant\n%+v", client, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithoutWaitTheRequestGoesOnAtOnceAndTheAnswerIsGivenWhenItComes(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
var (
|
||||
mu sync.Mutex
|
||||
given []lookup.Answer
|
||||
)
|
||||
|
||||
geojs := &standIn{answers: answeringSlowly}
|
||||
clock := newClock()
|
||||
m := metrics.New(1, "app")
|
||||
g := lookup.New(lookup.Params{
|
||||
URL: lookup.URL,
|
||||
Timeout: timeout,
|
||||
Answered: func(answer lookup.Answer) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
given = append(given, answer)
|
||||
},
|
||||
Now: clock.Now,
|
||||
ProcessLog: slog.New(slog.DiscardHandler),
|
||||
Metrics: m,
|
||||
Alerts: alerts.New(alerts.Params{}),
|
||||
})
|
||||
g.SetTransport(geojs)
|
||||
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
|
||||
// The request goes on at once, without an answer, and GeoJS is asked
|
||||
// about the client, which it answers most of a second later.
|
||||
began := time.Now()
|
||||
|
||||
got := g.LookUp(t.Context(), client)
|
||||
if took := time.Since(began); took != 0 || got != (lookup.Answer{}) {
|
||||
t.Errorf("waited %s for %+v, want no wait and no answer", took, got)
|
||||
}
|
||||
|
||||
waitForRequests(t, geojs, 1)
|
||||
wantAsked(t, geojs, 0, "203.0.113.9")
|
||||
|
||||
time.Sleep(timeout)
|
||||
synctest.Wait()
|
||||
|
||||
now := clock.Now()
|
||||
want := lookup.Answer{
|
||||
Client: client, ASN: asn, ASName: asName, Country: germany,
|
||||
Answered: now, Used: now,
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
if !slices.Equal(given, []lookup.Answer{want}) {
|
||||
t.Errorf("answers given %+v, want only %+v", given, want)
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
wantCountry(t, g, client, germany)
|
||||
wantUnanswered(t, m, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailureIsLoggedWithoutTheAddressesAskedAbout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -197,6 +343,8 @@ func TestFailureIsLoggedWithoutTheAddressesAskedAbout(t *testing.T) {
|
||||
geojs := &standIn{answers: hanging}
|
||||
g := lookup.New(lookup.Params{
|
||||
URL: lookup.URL,
|
||||
Timeout: timeout,
|
||||
Wait: true,
|
||||
Now: time.Now,
|
||||
ProcessLog: slog.New(slog.NewTextHandler(&log, nil)),
|
||||
Metrics: metrics.New(1, "app"),
|
||||
@@ -401,6 +549,8 @@ func TestClientsWithoutAnAnswerAreCounted(t *testing.T) {
|
||||
m := metrics.New(1, "app")
|
||||
g := lookup.New(lookup.Params{
|
||||
URL: lookup.URL,
|
||||
Timeout: timeout,
|
||||
Wait: true,
|
||||
Now: time.Now,
|
||||
ProcessLog: slog.New(slog.DiscardHandler),
|
||||
Metrics: m,
|
||||
@@ -494,21 +644,24 @@ func (s *standIn) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
list := make([]map[string]string, 0, len(addrs))
|
||||
list := make([]map[string]any, 0, len(addrs))
|
||||
|
||||
for _, addr := range addrs {
|
||||
country := germany
|
||||
item := map[string]any{
|
||||
"ip": addr, "asn": asNumber, "organization_name": asName,
|
||||
"country_code": germany,
|
||||
}
|
||||
|
||||
switch {
|
||||
case addr == unplaced:
|
||||
country = ""
|
||||
item = map[string]any{"ip": addr, "asn": 64512, "organization_name": "Unknown"}
|
||||
case addr == leftOut && answers == answeringWithoutLeftOut:
|
||||
continue
|
||||
case answers == answeringInLowerCase:
|
||||
country = strings.ToLower(germany)
|
||||
item["country_code"] = strings.ToLower(germany)
|
||||
}
|
||||
|
||||
list = append(list, map[string]string{"ip": addr, "country": country})
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
var answer any = list
|
||||
@@ -566,7 +719,8 @@ func (c *testClock) advance(d time.Duration) {
|
||||
}
|
||||
|
||||
// start returns a stand-in for GeoJS that answers, a clock, and a GeoJS
|
||||
// asking the stand-in by that clock.
|
||||
// asking the stand-in by that clock, for which a request waits for its
|
||||
// client's first answer.
|
||||
func start() (*standIn, *testClock, *lookup.GeoJS) {
|
||||
geojs, clock, g, _ := startWithAlerts()
|
||||
|
||||
@@ -578,7 +732,7 @@ func start() (*standIn, *testClock, *lookup.GeoJS) {
|
||||
// cooldown, by the same clock.
|
||||
func startWithAlerts() (*standIn, *testClock, *lookup.GeoJS, *alerts.Queue) {
|
||||
geojs := &standIn{}
|
||||
clock := &testClock{now: time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC)}
|
||||
clock := newClock()
|
||||
queue := alerts.New(alerts.Params{
|
||||
WebhookURL: &url.URL{Scheme: "https", Host: "alerts.example"},
|
||||
Events: alerts.Events(),
|
||||
@@ -587,6 +741,8 @@ func startWithAlerts() (*standIn, *testClock, *lookup.GeoJS, *alerts.Queue) {
|
||||
})
|
||||
g := lookup.New(lookup.Params{
|
||||
URL: lookup.URL,
|
||||
Timeout: timeout,
|
||||
Wait: true,
|
||||
Now: clock.Now,
|
||||
ProcessLog: slog.New(slog.DiscardHandler),
|
||||
Metrics: metrics.New(1, "app"),
|
||||
@@ -597,6 +753,11 @@ func startWithAlerts() (*standIn, *testClock, *lookup.GeoJS, *alerts.Queue) {
|
||||
return geojs, clock, g, queue
|
||||
}
|
||||
|
||||
// newClock returns a clock set to the start of a day.
|
||||
func newClock() *testClock {
|
||||
return &testClock{now: time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC)}
|
||||
}
|
||||
|
||||
// newClients returns what returns a new IPv4 client each time it is
|
||||
// called.
|
||||
func newClients() func() netip.Prefix {
|
||||
@@ -613,7 +774,7 @@ func newClients() func() netip.Prefix {
|
||||
func wantCountry(t *testing.T, g *lookup.GeoJS, client netip.Prefix, want string) {
|
||||
t.Helper()
|
||||
|
||||
got := g.Country(t.Context(), client)
|
||||
got := g.LookUp(t.Context(), client).Country
|
||||
if got != want {
|
||||
t.Errorf("%s is in %q, want %q", client, got, want)
|
||||
}
|
||||
@@ -658,6 +819,16 @@ func wantUnanswered(t *testing.T, m *metrics.Metrics, want float64) {
|
||||
}
|
||||
}
|
||||
|
||||
// wantFailures checks how many requests to GeoJS m counts as failed.
|
||||
func wantFailures(t *testing.T, m *metrics.Metrics, want float64) {
|
||||
t.Helper()
|
||||
|
||||
got := testutil.ToFloat64(m.GeoJSFailures)
|
||||
if got != want {
|
||||
t.Errorf("%v requests to GeoJS failed, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// waitForRequests waits until g has done all it can before time passes,
|
||||
// checks that GeoJS has had count requests, and returns the addresses each
|
||||
// asked about.
|
||||
|
||||
Reference in New Issue
Block a user