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

+73 -22
View File
@@ -15,8 +15,9 @@ import (
"strings"
"time"
"git.eeqj.de/sneak/routewatch/internal/blocklist"
"git.eeqj.de/sneak/routewatch/internal/bogon"
"git.eeqj.de/sneak/routewatch/internal/database"
"git.eeqj.de/sneak/routewatch/internal/iplist"
"git.eeqj.de/sneak/routewatch/internal/metrics"
"git.eeqj.de/sneak/routewatch/internal/ristypes"
"git.eeqj.de/sneak/routewatch/internal/routetable"
@@ -765,9 +766,12 @@ type IPLookupResponse struct {
Errors []string `json:"errors,omitempty"`
}
// IPLookupResult is the answer for one address: its PTR names, its live route
// and that route's AS, and the blocklists that list it. Every result has every
// field. The fields of IPInfo are null when the address has no live route.
// IPLookupResult is the answer for one address: its PTR names, its flags, its
// live route and that route's AS, and the IP lists that contain it. Every
// result has every field. The fields of IPInfo are null when the address has
// no live route.
//
//nolint:tagliatelle // snake_case keys, as the Go style guide asks
type IPLookupResult struct {
IPInfo
@@ -775,8 +779,22 @@ type IPLookupResult struct {
// PTR is empty, never nil, when the address has no PTR names.
PTR []string `json:"ptr"`
// Blocklists has one entry for each blocklist the address is on.
Blocklists []blocklist.Listing `json:"blocklists"`
Flags IPLookupFlags `json:"flags"`
// IPLists has one entry for each IP list the address is on.
IPLists []iplist.Listing `json:"ip_lists"`
}
// IPLookupFlags are the yes-or-no facts about an address. Every result has
// them, whether or not the address has a live route.
//
//nolint:tagliatelle // snake_case keys, as the Go style guide asks
type IPLookupFlags struct {
// IsTor says that the address is on the Tor exit list.
IsTor bool `json:"is_tor"`
// IsBogon says that the address is reserved, as package bogon tells.
IsBogon bool `json:"is_bogon"`
}
// handleIPInfo returns a handler that provides comprehensive IP information.
@@ -852,12 +870,7 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
return
}
response.Results = append(response.Results, IPLookupResult{
IP: ip,
PTR: ptrs,
IPInfo: ipInfo,
Blocklists: s.blocklistListings(ip),
})
response.Results = append(response.Results, s.ipLookupResult(ip, ptrs, ipInfo))
if ipInfo.NeedsWHOISRefresh {
refreshASNs[*ipInfo.ASN] = true
@@ -958,23 +971,61 @@ func ipVersionOf(prefix netip.Prefix) int {
return ipVersionV6
}
// blocklistListings returns the blocklists that list ip. It returns an empty
// list, never nil, so that an answer's blocklists is [] rather than null.
func (s *Server) blocklistListings(ip string) []blocklist.Listing {
// ipLookupResult returns the result for ip, adding its flags and IP lists to
// its PTR names and route.
func (s *Server) ipLookupResult(ip string, ptrs []string, info IPInfo) IPLookupResult {
listings := s.ipListListings(ip)
return IPLookupResult{
IP: ip,
PTR: ptrs,
IPInfo: info,
Flags: ipLookupFlags(ip, listings),
IPLists: listings,
}
}
// ipListListings returns the IP lists that contain ip. It returns an empty
// list, never nil, so that an answer's ip_lists is [] rather than null.
func (s *Server) ipListListings(ip string) []iplist.Listing {
addr, err := netip.ParseAddr(ip)
if s.blocklists == nil || err != nil {
return []blocklist.Listing{}
if s.ipLists == nil || err != nil {
return []iplist.Listing{}
}
return s.blocklists.Lookup(addr)
return s.ipLists.Lookup(addr)
}
// ipLookupFlags returns the flags of ip, whose IP list entries are listings.
func ipLookupFlags(ip string, listings []iplist.Listing) IPLookupFlags {
return IPLookupFlags{
IsTor: slices.ContainsFunc(listings, func(l iplist.Listing) bool {
return l.Name == iplist.TorExitsList
}),
IsBogon: isBogon(ip),
}
}
// isBogon says whether the address ip is reserved. An IPv4 address written in
// IPv6 form counts as the IPv4 address it holds.
func isBogon(ip string) bool {
addr, err := netip.ParseAddr(ip)
if err != nil {
return false
}
addr = addr.Unmap()
_, reserved := bogon.Prefix(netip.PrefixFrom(addr, addr.BitLen()))
return reserved
}
// setIPLookupCacheControl lets caches keep an IP lookup answer for the
// shortest blocklist interval, the soonest a blocklist copy can change. The
// shortest IP list interval, the soonest an IP list copy can change. The
// answer about the caller's own address is private, so that a shared cache
// does not give it to another caller. Without blocklists it sets nothing.
// does not give it to another caller. Without IP lists it sets nothing.
func (s *Server) setIPLookupCacheControl(w http.ResponseWriter, ownAddress bool) {
if s.blocklists == nil {
if s.ipLists == nil {
return
}
@@ -983,7 +1034,7 @@ func (s *Server) setIPLookupCacheControl(w http.ResponseWriter, ownAddress bool)
visibility = cacheControlPrivate
}
maxAgeSeconds := int(s.blocklists.ShortestInterval().Seconds())
maxAgeSeconds := int(s.ipLists.ShortestInterval().Seconds())
w.Header().Set(cacheControlHeader,
visibility+cacheControlMaxAge+strconv.Itoa(maxAgeSeconds))
}