Give every IP lookup result the same fields (closes #200)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Every result of /ip/, /ip/{addr} and /api/v1/ip/{ip} now has every
field. A field without a value is null instead of left out, so an
address with no live route has all its route and AS fields set to null.
ptr is an empty list when there are no PTR names.
Two new fields, num_origins and origin_asns, give how many origin ASes
peers carry the netblock with, and which. The routing table's prefix
summary now lists them. asn still names the origin that most peers
carry.
first_seen is null, no longer the zero time, when the database has not
stored the netblock yet.
Model: opus-5-5
This commit was merged in pull request #206.
This commit is contained in:
+58
-45
@@ -752,15 +752,14 @@ type IPLookupResponse struct {
|
||||
}
|
||||
|
||||
// IPLookupResult is the answer for one address: its PTR names, its live route
|
||||
// and that route's AS, and the blocklists that list it. IPInfo is nil when
|
||||
// the address has no live route, and the answer then has no route or AS
|
||||
// fields. IP hides the IPInfo field of the same name, so that an address with
|
||||
// no route has it too.
|
||||
// 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.
|
||||
type IPLookupResult struct {
|
||||
*IPInfo
|
||||
IPInfo
|
||||
|
||||
IP string `json:"ip"`
|
||||
PTR []string `json:"ptr,omitempty"`
|
||||
IP string `json:"ip"`
|
||||
// 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"`
|
||||
@@ -818,8 +817,8 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
|
||||
switch {
|
||||
case errors.Is(err, ErrNoRoute):
|
||||
// The address still gets a result, without route fields.
|
||||
ipInfo = nil
|
||||
// The address still gets a result, with every field of
|
||||
// ipInfo null.
|
||||
case errors.Is(err, ErrInvalidIP):
|
||||
response.Errors = append(response.Errors, ip+": "+err.Error())
|
||||
|
||||
@@ -846,8 +845,8 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
Blocklists: s.blocklistListings(ip),
|
||||
})
|
||||
|
||||
if ipInfo != nil && ipInfo.NeedsWHOISRefresh {
|
||||
refreshASNs[ipInfo.ASN] = true
|
||||
if ipInfo.NeedsWHOISRefresh {
|
||||
refreshASNs[*ipInfo.ASN] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,60 +857,72 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ipInfo returns the IP lookup's answer for ip. The routing table gives the
|
||||
// most specific prefix that holds the address, the number of peers that carry
|
||||
// it, the origin AS that most of them carry, and the time of its newest kept
|
||||
// route. The database gives the prefix's first-seen time and the AS's WHOIS
|
||||
// details. It returns ErrNoRoute when no prefix holds the address, and
|
||||
// ErrInvalidIP when ip does not parse.
|
||||
func (s *Server) ipInfo(ctx context.Context, ip string) (*IPInfo, error) {
|
||||
// ipInfo returns what the IP lookup knows about the route of ip. The routing
|
||||
// table gives the most specific prefix that holds the address, the number of
|
||||
// peers that carry it, its origin ASes and the one that most peers carry, and
|
||||
// the time of its newest kept route. The database gives the prefix's
|
||||
// first-seen time and the AS's WHOIS details. It returns ErrNoRoute when no
|
||||
// prefix holds the address, and ErrInvalidIP when ip does not parse. With an
|
||||
// error, every field of the IPInfo it returns is nil.
|
||||
func (s *Server) ipInfo(ctx context.Context, ip string) (IPInfo, error) {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrInvalidIP, ip)
|
||||
return IPInfo{}, fmt.Errorf("%w: %s", ErrInvalidIP, ip)
|
||||
}
|
||||
|
||||
summary, found := s.table.Lookup(addr)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%w for IP %s", ErrNoRoute, ip)
|
||||
return IPInfo{}, fmt.Errorf("%w for IP %s", ErrNoRoute, ip)
|
||||
}
|
||||
|
||||
firstSeen, err := s.db.GetPrefixFirstSeen(ctx, summary.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return IPInfo{}, err
|
||||
}
|
||||
|
||||
asInfo, err := s.db.GetASWHOIS(ctx, summary.OriginASN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return IPInfo{}, err
|
||||
}
|
||||
|
||||
info := &IPInfo{
|
||||
IP: ip,
|
||||
Netblock: summary.Prefix.String(),
|
||||
MaskLength: summary.Prefix.Bits(),
|
||||
IPVersion: ipVersionOf(summary.Prefix),
|
||||
NumPeers: summary.Peers,
|
||||
ASN: summary.OriginASN,
|
||||
ASName: asInfo.ASName,
|
||||
Handle: asInfo.Handle,
|
||||
Description: asInfo.Description,
|
||||
OrgName: asInfo.OrgName,
|
||||
OrgID: asInfo.OrgID,
|
||||
Address: asInfo.Address,
|
||||
CountryCode: asInfo.CountryCode,
|
||||
AbuseEmail: asInfo.AbuseEmail,
|
||||
RIR: asInfo.RIR,
|
||||
FirstSeen: firstSeen,
|
||||
info := IPInfo{
|
||||
Netblock: new(summary.Prefix.String()),
|
||||
MaskLength: new(summary.Prefix.Bits()),
|
||||
IPVersion: new(ipVersionOf(summary.Prefix)),
|
||||
NumPeers: new(summary.Peers),
|
||||
ASN: new(summary.OriginASN),
|
||||
NumOrigins: new(len(summary.OriginASNs)),
|
||||
OriginASNs: summary.OriginASNs,
|
||||
ASName: nilIfEmpty(asInfo.ASName),
|
||||
Handle: nilIfEmpty(asInfo.Handle),
|
||||
Description: nilIfEmpty(asInfo.Description),
|
||||
OrgName: nilIfEmpty(asInfo.OrgName),
|
||||
OrgID: nilIfEmpty(asInfo.OrgID),
|
||||
Address: nilIfEmpty(asInfo.Address),
|
||||
CountryCode: nilIfEmpty(asInfo.CountryCode),
|
||||
AbuseEmail: nilIfEmpty(asInfo.AbuseEmail),
|
||||
RIR: nilIfEmpty(asInfo.RIR),
|
||||
LastSeen: summary.Newest,
|
||||
NeedsWHOISRefresh: needsWHOISRefresh(asInfo),
|
||||
}
|
||||
|
||||
if summary.Newest != nil {
|
||||
info.LastSeen = *summary.Newest
|
||||
if !firstSeen.IsZero() {
|
||||
info.FirstSeen = &firstSeen
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// nilIfEmpty returns a pointer to s, or nil when s is empty, so that JSON
|
||||
// gives a detail the database does not hold as null.
|
||||
func nilIfEmpty(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
// needsWHOISRefresh says whether the WHOIS details of asInfo were never
|
||||
// fetched or are older than whoisStaleThreshold. AS 0, no known origin, has
|
||||
// none to fetch.
|
||||
@@ -1117,15 +1128,17 @@ func publicAddresses(addrs []string) []string {
|
||||
return public
|
||||
}
|
||||
|
||||
// lookupPTRNames returns the PTR records of ip without their trailing dots, or
|
||||
// nil when the lookup fails or runs past ptrLookupTimeout.
|
||||
// lookupPTRNames returns the PTR records of ip without their trailing dots. It
|
||||
// returns an empty list, never nil, when there are none, when the lookup fails
|
||||
// and when it runs past ptrLookupTimeout, so that an answer's ptr is [] rather
|
||||
// than null.
|
||||
func (s *Server) lookupPTRNames(ctx context.Context, ip string) []string {
|
||||
ctx, cancel := context.WithTimeout(ctx, ptrLookupTimeout)
|
||||
defer cancel()
|
||||
|
||||
ptrs, err := s.resolver.LookupAddr(ctx, ip)
|
||||
if err != nil {
|
||||
return nil
|
||||
if err != nil || ptrs == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Remove trailing dots from PTR records
|
||||
|
||||
Reference in New Issue
Block a user