Back off WHOIS lookups more slowly and stop asking for personal data (closes #159)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
A failed lookup now makes the fetcher's wait 1.5 times as long, up to one minute instead of five. A success still halves it. RIPE counts the person and role objects an answer holds against a daily limit per address, and refuses an address for good that passes it too often. RIPE is now asked with --no-personal, which keeps the organisation object. APNIC and AFRINIC document only -r, which also leaves out the organisation object, so the client asks for that object on its own. An answer with RIPE's access-denied or limit-reached error line is now reported as a refusal holding that line, not as an answer with no fields. Model: opus-5-5
This commit is contained in:
+85
-5
@@ -49,6 +49,34 @@ const (
|
||||
RIRAFRNIC = "AFRINIC"
|
||||
)
|
||||
|
||||
// Query flags that keep person and role objects out of an answer. RIPE
|
||||
// counts each one it returns against a daily limit per address, and refuses
|
||||
// an address for good that passes the limit too often:
|
||||
// https://docs.db.ripe.net/Types-of-Queries/Access-Control-for-Queries/
|
||||
// APNIC and AFRINIC run the same database software.
|
||||
const (
|
||||
// noPersonalFlag leaves out person and role objects only, so the answer
|
||||
// keeps the organisation object:
|
||||
// https://docs.db.ripe.net/Types-of-Queries/Referenced-Objects-in-Query-Response/
|
||||
noPersonalFlag = "--no-personal"
|
||||
|
||||
// noReferencedFlag leaves out every object the answer's objects refer
|
||||
// to, the organisation object too. It is the only such flag APNIC and
|
||||
// AFRINIC document:
|
||||
// https://www.apnic.net/db/search/web-help.html
|
||||
// https://afrinic.net/whois-getting-started
|
||||
noReferencedFlag = "-r"
|
||||
)
|
||||
|
||||
// The error lines with which RIPE's database software refuses a query:
|
||||
// access denied to the address, and the address's limit reached. They are
|
||||
// listed under "Access Errors" in Appendix C of
|
||||
// https://docs.db.ripe.net/Appendices/
|
||||
const (
|
||||
accessDeniedError = "%ERROR:201:"
|
||||
accessLimitError = "%ERROR:202:"
|
||||
)
|
||||
|
||||
// Errors LookupASN returns when a WHOIS answer has nothing to store.
|
||||
var (
|
||||
// ErrNoReferral is returned when IANA refers the AS to no RIR, as it
|
||||
@@ -58,6 +86,7 @@ var (
|
||||
errNoASBlock = errors.New("IANA answered with neither a referral " +
|
||||
"nor an AS block")
|
||||
errNoFields = errors.New("RIR answered with no WHOIS fields")
|
||||
errRefused = errors.New("RIR refused the query")
|
||||
errUnknownServer = errors.New("IANA refers the AS to a server no RIR runs")
|
||||
errAnswerTooLarge = errors.New("WHOIS answer reached the size cap")
|
||||
)
|
||||
@@ -97,10 +126,10 @@ func NewClient() *Client {
|
||||
|
||||
// LookupASN asks IANA which RIR holds the given ASN, asks that RIR, and
|
||||
// returns what the RIR's answer says. It returns ErrNoReferral when IANA
|
||||
// refers the AS to no RIR, and another error when a query fails, when IANA's
|
||||
// answer has neither a referral nor the AS's block, when IANA refers the AS
|
||||
// to a server none of the five RIRs run, or when the RIR's answer carries
|
||||
// none of ASNInfo's fields, as a refusal of service does.
|
||||
// refers the AS to no RIR, and another error when a query fails, when the
|
||||
// RIR refuses it, when IANA's answer has neither a referral nor the AS's
|
||||
// block, when IANA refers the AS to a server none of the five RIRs run, or
|
||||
// when the RIR's answer carries none of ASNInfo's fields.
|
||||
func (c *Client) LookupASN(ctx context.Context, asn int) (*ASNInfo, error) {
|
||||
// Query IANA first to find the authoritative RIR
|
||||
query := fmt.Sprintf("AS%d", asn)
|
||||
@@ -127,7 +156,7 @@ func (c *Client) LookupASN(ctx context.Context, asn int) (*ASNInfo, error) {
|
||||
}
|
||||
|
||||
// Query the authoritative RIR
|
||||
rirResp, err := c.query(ctx, whoisServer, query)
|
||||
rirResp, err := c.queryRIR(ctx, whoisServer, rirQuery(rir, query))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RIR query failed: %w", err)
|
||||
}
|
||||
@@ -135,6 +164,22 @@ func (c *Client) LookupASN(ctx context.Context, asn int) (*ASNInfo, error) {
|
||||
// The fields come from the RIR's answer alone: IANA's describes the
|
||||
// block the AS is in.
|
||||
info := c.parseResponse(asn, rir, rirResp)
|
||||
|
||||
// The answers of APNIC and AFRINIC leave out the AS's organisation
|
||||
// object, which holds the organisation's name, country and address. It
|
||||
// is asked for on its own and read after the AS's object, whose fields
|
||||
// come first.
|
||||
if (rir == RIRAPNIC || rir == RIRAFRNIC) && info.OrgID != "" {
|
||||
orgResp, orgErr := c.queryRIR(ctx, whoisServer,
|
||||
rirQuery(rir, info.OrgID))
|
||||
if orgErr != nil {
|
||||
return nil, fmt.Errorf("organisation query failed: %w", orgErr)
|
||||
}
|
||||
|
||||
rirResp += "\n" + orgResp
|
||||
info = c.parseResponse(asn, rir, rirResp)
|
||||
}
|
||||
|
||||
if info.isEmpty() {
|
||||
return nil, fmt.Errorf("%s from %s: %w", query, whoisServer, errNoFields)
|
||||
}
|
||||
@@ -154,6 +199,41 @@ func (info *ASNInfo) isEmpty() bool {
|
||||
info.RegDate == nil && info.LastMod == nil
|
||||
}
|
||||
|
||||
// rirQuery returns the query sent to rir's server for key, an AS number
|
||||
// such as "AS3333" or an organisation ID. RIPE, APNIC and AFRINIC get a
|
||||
// flag that keeps person and role objects out of the answer.
|
||||
func rirQuery(rir, key string) string {
|
||||
switch rir {
|
||||
case RIRRIPE:
|
||||
return noPersonalFlag + " " + key
|
||||
case RIRAPNIC, RIRAFRNIC:
|
||||
return noReferencedFlag + " " + key
|
||||
default:
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
// queryRIR sends query to an RIR's server and returns the answer. An answer
|
||||
// holding an error line with which the server refuses the query is an
|
||||
// error, and the error holds that line.
|
||||
func (c *Client) queryRIR(
|
||||
ctx context.Context, server, query string,
|
||||
) (string, error) {
|
||||
answer, err := c.query(ctx, server, query)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for line := range strings.SplitSeq(answer, "\n") {
|
||||
if strings.HasPrefix(line, accessDeniedError) ||
|
||||
strings.HasPrefix(line, accessLimitError) {
|
||||
return "", fmt.Errorf("%w: %s", errRefused, line)
|
||||
}
|
||||
}
|
||||
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
// query performs a raw WHOIS query to the specified server. An answer that
|
||||
// reaches maxAnswerSize is an error.
|
||||
func (c *Client) query(ctx context.Context, server, query string) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user