Rename blocklists to IP lists and add IP lookup flags (closes #233)
check / check (push) Failing after 1m7s

The answer field is now ip_lists, the package internal/iplist and the
state directory iplists. No compatibility is kept for the old names.

Every IP lookup result gains a flags object with is_tor and is_bogon.
The largest list file accepted is now 200 MiB.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-10 09:27:12 +00:00
1 parent e58c6635e7
commit 9e8f687511
12 files changed
+375 -176

No files matched your search

+102 -41
View File
@@ -19,9 +19,9 @@ import (
"testing"
"time"
"git.eeqj.de/sneak/routewatch/internal/blocklist"
"git.eeqj.de/sneak/routewatch/internal/config"
"git.eeqj.de/sneak/routewatch/internal/database"
"git.eeqj.de/sneak/routewatch/internal/iplist"
"git.eeqj.de/sneak/routewatch/internal/logger"
"git.eeqj.de/sneak/routewatch/internal/metrics"
"git.eeqj.de/sneak/routewatch/internal/ristypes"
@@ -1428,50 +1428,112 @@ func TestIPLookupKeepsRouteWhenPTRLookupDoesNotAnswer(t *testing.T) {
// newServerWithTestRoutes stores a route for.
const addrInTestPrefixV6 = "2001:db8::1"
// testBlocklistInterval is fakeBlocklists' shortest interval.
const testBlocklistInterval = 15 * time.Minute
// testIPListInterval is fakeIPLists' shortest interval.
const testIPListInterval = 15 * time.Minute
// ownAddressPath is the IP lookup of the caller's own address.
const ownAddressPath = "/ip/"
// listedUnroutedAddr is an address that fakeBlocklists lists, outside the
// routes newServerWithTestRoutes stores.
// listedUnroutedAddr is an address that fakeIPLists lists, outside the routes
// newServerWithTestRoutes stores.
const listedUnroutedAddr = "203.0.113.1"
// fakeBlocklists has one list, which lists addrInTestPrefixV4 and
// listedUnroutedAddr and no other address.
type fakeBlocklists struct{}
// torExitAddr is the address that fakeIPLists lists on the Tor exit list.
const torExitAddr = "203.0.113.2"
func testListing() blocklist.Listing {
return blocklist.Listing{
// fakeIPLists has two lists. The test list holds addrInTestPrefixV4 and
// listedUnroutedAddr, and the Tor exit list holds torExitAddr.
type fakeIPLists struct{}
func testListing() iplist.Listing {
return iplist.Listing{
Name: "test_list",
Description: "On the test list",
Updated: time.Date(2026, 10, 27, 4, 0, 0, 0, time.UTC),
}
}
func (fakeBlocklists) Lookup(addr netip.Addr) []blocklist.Listing {
func (fakeIPLists) Lookup(addr netip.Addr) []iplist.Listing {
if addr == netip.MustParseAddr(addrInTestPrefixV4) ||
addr == netip.MustParseAddr(listedUnroutedAddr) {
return []blocklist.Listing{testListing()}
return []iplist.Listing{testListing()}
}
return []blocklist.Listing{}
if addr == netip.MustParseAddr(torExitAddr) {
return []iplist.Listing{{Name: iplist.TorExitsList}}
}
return []iplist.Listing{}
}
func (fakeBlocklists) ShortestInterval() time.Duration {
return testBlocklistInterval
func (fakeIPLists) ShortestInterval() time.Duration {
return testIPListInterval
}
// TestIPLookupReportsBlocklists looks up two routed addresses, one of them on
// a blocklist. The listed address's result names the list, says what it means
// and when it was updated. The other's result has an empty list.
func TestIPLookupReportsBlocklists(t *testing.T) {
// TestIPLookupFlags looks up addresses with and without a route. Every result
// has both flags, under the JSON keys the API documents.
func TestIPLookupFlags(t *testing.T) {
t.Parallel()
s := newServerWithTestRoutes(t)
s.resolver = &fakeResolver{}
s.SetBlocklists(fakeBlocklists{})
s.SetIPLists(fakeIPLists{})
tests := []struct {
name string
addr string
tor bool
bogon bool
}{
{"on the Tor exit list", torExitAddr, true, true},
{"on another list", listedUnroutedAddr, false, true},
{"routed IPv4", addrInTestPrefixV4, false, true},
{"routed IPv6", addrInTestPrefixV6, false, true},
{"private IPv4", "10.0.0.1", false, true},
{"loopback IPv4", "127.0.0.1", false, true},
{"public IPv4", "1.1.1.1", false, false},
{"private IPv6", "fd00::1", false, true},
{"loopback IPv6", "::1", false, true},
{"public IPv6", "2606:4700:4700::1111", false, false},
{"public IPv4 in IPv6 form", "::ffff:1.1.1.1", false, false},
{"private IPv4 in IPv6 form", "::ffff:10.0.0.1", false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
path := "/api/v1/ip/" + tt.addr
results := resultFields(t, get(t, s, path).Body.Bytes())
if len(results) != 1 {
t.Fatalf("%s: %d results, want 1", path, len(results))
}
var got map[string]bool
err := json.Unmarshal(results[0]["flags"], &got)
if err != nil {
t.Fatalf("%s: decoding flags %s: %v", path, results[0]["flags"], err)
}
want := map[string]bool{"is_tor": tt.tor, "is_bogon": tt.bogon}
if !maps.Equal(got, want) {
t.Errorf("%s: flags %v, want %v", path, got, want)
}
})
}
}
// TestIPLookupReportsIPLists looks up two routed addresses, one of them on an
// IP list. The listed address's result names the list, says what it means and
// when it was updated. The other's result has an empty list.
func TestIPLookupReportsIPLists(t *testing.T) {
t.Parallel()
s := newServerWithTestRoutes(t)
s.resolver = &fakeResolver{}
s.SetIPLists(fakeIPLists{})
rec := httptest.NewRecorder()
s.router.ServeHTTP(rec, httptest.NewRequestWithContext(t.Context(),
@@ -1498,15 +1560,15 @@ func TestIPLookupReportsBlocklists(t *testing.T) {
want := testListing()
listed := results[0].Blocklists
listed := results[0].IPLists
if len(listed) != 1 || listed[0].Name != want.Name ||
listed[0].Description != want.Description ||
!listed[0].Updated.Equal(want.Updated) {
t.Errorf("%s: blocklists %+v, want [%+v]", results[0].IP, listed, want)
t.Errorf("%s: ip_lists %+v, want [%+v]", results[0].IP, listed, want)
}
if !strings.Contains(rec.Body.String(), `"blocklists":[]`) {
t.Errorf("answer %s, want an empty blocklists list for %s",
if !strings.Contains(rec.Body.String(), `"ip_lists":[]`) {
t.Errorf("answer %s, want an empty ip_lists list for %s",
rec.Body, addrInTestPrefixV6)
}
}
@@ -1536,20 +1598,19 @@ func resultFields(t *testing.T, answer []byte) []map[string]json.RawMessage {
return body.Data.Results
}
// TestIPLookupReportsBlocklistsOfAnAddressWithNoRoute looks up a listed
// address that has no live route, alone and beside a routed address. Each
// answer is 200 and has a result for the address, with its blocklists and
// every route field set to null. Before the fix the lookup of that address
// alone answered 404, and beside a routed address it was only named in
// errors, so its blocklists were never reported. After that fix the result
// still left out the route fields
// (https://git.eeqj.de/sneak/routewatch/issues/200).
func TestIPLookupReportsBlocklistsOfAnAddressWithNoRoute(t *testing.T) {
// TestIPLookupReportsIPListsOfAnAddressWithNoRoute looks up a listed address
// that has no live route, alone and beside a routed address. Each answer is
// 200 and has a result for the address, with its IP lists and every route
// field set to null. Before the fix the lookup of that address alone answered
// 404, and beside a routed address it was only named in errors, so its IP
// lists were never reported. After that fix the result still left out the
// route fields (https://git.eeqj.de/sneak/routewatch/issues/200).
func TestIPLookupReportsIPListsOfAnAddressWithNoRoute(t *testing.T) {
t.Parallel()
s := newServerWithTestRoutes(t)
s.resolver = &fakeResolver{}
s.SetBlocklists(fakeBlocklists{})
s.SetIPLists(fakeIPLists{})
// routeFields are the fields of a result that come from the address's
// route and that route's origin AS.
@@ -1605,10 +1666,10 @@ func TestIPLookupReportsBlocklistsOfAnAddressWithNoRoute(t *testing.T) {
}
result := body.Data.Results[i]
if len(result.Blocklists) != 1 ||
result.Blocklists[0].Name != testListing().Name {
t.Errorf("%s: blocklists %+v, want [%+v]",
target, result.Blocklists, testListing())
if len(result.IPLists) != 1 ||
result.IPLists[0].Name != testListing().Name {
t.Errorf("%s: ip_lists %+v, want [%+v]",
target, result.IPLists, testListing())
}
}
}
@@ -1629,7 +1690,7 @@ func TestIPLookupResultsHaveTheSameFields(t *testing.T) {
s.resolver = &fakeResolver{hosts: map[string][]string{
twoAddressHostName: {addrInTestPrefixV4, listedUnroutedAddr},
}}
s.SetBlocklists(fakeBlocklists{})
s.SetIPLists(fakeIPLists{})
// want is the fields of the first result.
var want []string
@@ -1684,7 +1745,7 @@ func TestIPLookupOfAnAddressWithoutPTRNamesOrStoredNetblock(t *testing.T) {
}
// TestIPLookupAnswersMayBeCached checks the Cache-Control header of the IP
// lookup answers. Caches may keep them for the blocklists' shortest interval,
// lookup answers. Caches may keep them for the IP lists' shortest interval,
// and only the caller's own browser may keep the answer about the caller's
// own address.
func TestIPLookupAnswersMayBeCached(t *testing.T) {
@@ -1692,7 +1753,7 @@ func TestIPLookupAnswersMayBeCached(t *testing.T) {
s := newServerWithTestRoutes(t)
s.resolver = &fakeResolver{}
s.SetBlocklists(fakeBlocklists{})
s.SetIPLists(fakeIPLists{})
tests := []struct {
path string