Add best-effort geolocation to the IP lookup from geojs and DB-IP City Lite (closes #239)
check / check (push) Successful in 1m50s
check / check (push) Successful in 1m50s
Each result gains `geo`. One request to geojs carries every address, 1 second in all. On a timeout or error the lookup reads DB-IP City Lite, downloaded monthly into the state directory. geojs answers stay in memory (250,000, least recently used dropped) and in a new `cache.db` table for 7 days. Judgement call: a reserved address goes to neither source, and its `geo` is null. Judgement call: an answer from geojs with every part unknown is still an answer. Judgement call: `updated` for DB-IP is when the copy was downloaded. Deviation: the cache is SQLite, by sneak's words. Model: opus-5-5
This commit is contained in:
1 parent
0cbd9cd943
commit
6e903d511f
16 files changed
+2244
-22
No files matched your search
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/bogon"
|
||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||
"git.eeqj.de/sneak/routewatch/internal/geo"
|
||||
"git.eeqj.de/sneak/routewatch/internal/iplist"
|
||||
"git.eeqj.de/sneak/routewatch/internal/metrics"
|
||||
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
||||
@@ -764,9 +765,9 @@ type IPLookupResponse struct {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// live route and that route's AS, the IP lists that contain it, and where it
|
||||
// is. 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 {
|
||||
@@ -784,6 +785,9 @@ type IPLookupResult struct {
|
||||
|
||||
// IPLists has one entry for each IP list the address is on.
|
||||
IPLists []iplist.Listing `json:"ip_lists"`
|
||||
|
||||
// Geo is where the address is. It is nil when no source answers.
|
||||
Geo *geo.Location `json:"geo"`
|
||||
}
|
||||
|
||||
// IPLookupFlags are the yes-or-no facts about an address. Every result has
|
||||
@@ -839,6 +843,8 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
locations := s.geoLocations(ctx, ipsToLookup)
|
||||
|
||||
// Track ASNs that need WHOIS refresh
|
||||
refreshASNs := make(map[int]bool)
|
||||
|
||||
@@ -874,7 +880,9 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
response.Results = append(response.Results, s.ipLookupResult(ip, ptrs, ipInfo))
|
||||
result := s.ipLookupResult(ip, ptrs, ipInfo)
|
||||
result.Geo = locations[ip]
|
||||
response.Results = append(response.Results, result)
|
||||
|
||||
if ipInfo.NeedsWHOISRefresh {
|
||||
refreshASNs[*ipInfo.ASN] = true
|
||||
@@ -1029,6 +1037,45 @@ func ipLookupFlags(ip string, listings []iplist.Listing) IPLookupFlags {
|
||||
}
|
||||
}
|
||||
|
||||
// geoLocations returns where ips are, by address. An address that does not
|
||||
// parse has no entry. Nor has a reserved one, which is sent to no outside
|
||||
// source and has no location.
|
||||
func (s *Server) geoLocations(
|
||||
ctx context.Context, ips []string,
|
||||
) map[string]*geo.Location {
|
||||
if s.geoLocator == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
kept []string
|
||||
addrs []netip.Addr
|
||||
)
|
||||
|
||||
for _, ip := range ips {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil || isBogon(ip) {
|
||||
continue
|
||||
}
|
||||
|
||||
kept = append(kept, ip)
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
|
||||
if len(addrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
answers := s.geoLocator.Lookup(ctx, addrs)
|
||||
locations := make(map[string]*geo.Location, len(kept))
|
||||
|
||||
for i, ip := range kept {
|
||||
locations[ip] = answers[i]
|
||||
}
|
||||
|
||||
return locations
|
||||
}
|
||||
|
||||
// isBogon says whether the address ip is reserved, as package bogon tells.
|
||||
func isBogon(ip string) bool {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
|
||||
Reference in new issue
Block a user