Take Brazilian AS details from NIC.br's WHOIS server (closes #174)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
LACNIC's answer for an AS that NIC.br registers is NIC.br's answer with each accented letter replaced by U+FFFD, so a name such as AS52751's cannot be recovered from it. When LACNIC's answer carries NIC.br's copyright notice, the client now also asks whois.registro.br for the AS and takes the fields from that answer, which is Latin-1 and decoded as before. When that query fails, when the answer has no fields, or when it is NIC.br's reduced answer for a client over its query limit, LACNIC's fields are kept and the lookup still succeeds. The tests use recorded answers for AS52751. Model: opus-5-5
This commit is contained in:
+58
-11
@@ -49,8 +49,25 @@ const (
|
||||
whoisServerAPNIC = "whois.apnic.net:43"
|
||||
whoisServerLACNIC = "whois.lacnic.net:43"
|
||||
whoisServerAFRINIC = "whois.afrinic.net:43"
|
||||
|
||||
// NIC.br registers Brazil's ASes under LACNIC.
|
||||
whoisServerNICbr = "whois.registro.br:43"
|
||||
)
|
||||
|
||||
// nicbrHeader starts the copyright notice at the top of NIC.br's WHOIS
|
||||
// answers. LACNIC's answer for an AS that NIC.br registers is NIC.br's, and
|
||||
// carries the same notice:
|
||||
//
|
||||
// % Copyright (c) Nic.br - Use of this data is governed by the Use and
|
||||
// % Privacy Policy at https://registro.br/upp . Distribution,
|
||||
const nicbrHeader = "% Copyright (c) Nic.br"
|
||||
|
||||
// nicbrLimitReached is in the line that marks NIC.br's reduced answer to a
|
||||
// client over its query limit. NIC.br publishes no limit. Its Use and
|
||||
// Privacy Policy, https://registro.br/upp, says only that it restricts
|
||||
// queries to stop the data being taken in bulk.
|
||||
const nicbrLimitReached = "Query rate limit exceeded"
|
||||
|
||||
// RIR identifiers.
|
||||
const (
|
||||
RIRARIN = "ARIN"
|
||||
@@ -133,11 +150,14 @@ 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 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.
|
||||
// returns what the RIR's answer says. When LACNIC's answer is NIC.br's, it
|
||||
// returns what NIC.br's own server says instead, unless that query fails.
|
||||
//
|
||||
// It returns ErrNoReferral when IANA refers the AS to no RIR, and another
|
||||
// error when the query to IANA or to the RIR 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
|
||||
// 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)
|
||||
@@ -169,18 +189,45 @@ func (c *Client) LookupASN(ctx context.Context, asn int) (*ASNInfo, error) {
|
||||
return nil, fmt.Errorf("RIR query failed: %w", err)
|
||||
}
|
||||
|
||||
// The fields come from the RIR's answer alone: IANA's describes the
|
||||
// block the AS is in.
|
||||
// The fields never come from IANA's answer, which describes the block
|
||||
// the AS is in.
|
||||
info := c.parseResponse(asn, rir, rirResp)
|
||||
|
||||
if rir == RIRLACNIC && strings.Contains(rirResp, nicbrHeader) {
|
||||
info = c.askNICbr(ctx, query, info)
|
||||
}
|
||||
|
||||
if info.isEmpty() {
|
||||
return nil, fmt.Errorf("%s from %s: %w", query, whoisServer, errNoFields)
|
||||
}
|
||||
|
||||
info.RawResponse = ianaResp + "\n" + rirResp
|
||||
info.RawResponse = ianaResp + "\n" + info.RawResponse
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// askNICbr asks NIC.br's server about an AS whose answer from LACNIC is
|
||||
// NIC.br's, and returns what NIC.br's answer says. LACNIC replaces each
|
||||
// accented letter in such an answer with U+FFFD. NIC.br's own server keeps
|
||||
// them, in Latin-1. askNICbr returns lacnicInfo, what LACNIC's answer says,
|
||||
// when the query fails, when NIC.br sends the reduced answer for a client
|
||||
// over its limit, or when NIC.br's answer carries none of ASNInfo's fields.
|
||||
func (c *Client) askNICbr(
|
||||
ctx context.Context, asQuery string, lacnicInfo *ASNInfo,
|
||||
) *ASNInfo {
|
||||
answer, err := c.query(ctx, whoisServerNICbr, asQuery)
|
||||
if err != nil || strings.Contains(answer, nicbrLimitReached) {
|
||||
return lacnicInfo
|
||||
}
|
||||
|
||||
info := c.parseResponse(lacnicInfo.ASN, RIRLACNIC, answer)
|
||||
if info.isEmpty() {
|
||||
return lacnicInfo
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// isEmpty reports whether info holds none of the fields a WHOIS answer
|
||||
// fills, as when the answer is only comment and error lines.
|
||||
func (info *ASNInfo) isEmpty() bool {
|
||||
@@ -276,9 +323,9 @@ func (c *Client) query(ctx context.Context, server, query string) (string, error
|
||||
}
|
||||
|
||||
// toUTF8 returns a WHOIS answer in UTF-8. An answer that is valid UTF-8 is
|
||||
// returned as it is. Any other answer is decoded as Latin-1, which RIPE and
|
||||
// APNIC send. LACNIC, ARIN and AFRINIC send UTF-8. RIPE's database working
|
||||
// group lists the five here:
|
||||
// returned as it is. Any other answer is decoded as Latin-1, which RIPE,
|
||||
// APNIC and NIC.br send. LACNIC, ARIN and AFRINIC send UTF-8. RIPE's
|
||||
// database working group lists the five RIRs here:
|
||||
// https://www.ripe.net/ripe/mail/archives/db-wg/2024-May/008075.html
|
||||
func toUTF8(answer string) (string, error) {
|
||||
if utf8.ValidString(answer) {
|
||||
|
||||
Reference in New Issue
Block a user