Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
080b641ce6 |
@@ -36,8 +36,7 @@ tested.
|
|||||||
- 2026-10-26: the WHOIS fetcher backs off more slowly. A failure makes its wait
|
- 2026-10-26: the WHOIS fetcher backs off more slowly. A failure makes its wait
|
||||||
1.5 times as long, up to one minute instead of five. Its queries to RIPE,
|
1.5 times as long, up to one minute instead of five. Its queries to RIPE,
|
||||||
APNIC and AFRINIC leave out person and role objects, which RIPE counts against
|
APNIC and AFRINIC leave out person and role objects, which RIPE counts against
|
||||||
a daily limit per address. APNIC and AFRINIC are asked for the AS's
|
a daily limit per address. A refused query is logged with the RIR's error line
|
||||||
organisation on its own. A refused query is logged with the RIR's error line
|
|
||||||
(closes #159)
|
(closes #159)
|
||||||
- 2026-10-26: the home page's AS name search lists the ASes whose handle or
|
- 2026-10-26: the home page's AS name search lists the ASes whose handle or
|
||||||
description holds the text, as links to their pages, from the new
|
description holds the text, as links to their pages, from the new
|
||||||
|
|||||||
+23
-44
@@ -49,29 +49,26 @@ const (
|
|||||||
RIRAFRNIC = "AFRINIC"
|
RIRAFRNIC = "AFRINIC"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Query flags that keep person and role objects out of an answer. RIPE
|
// noPersonalFlag keeps person and role objects out of an answer. The
|
||||||
// counts each one it returns against a daily limit per address, and refuses
|
// organisation object stays in it. RIPE counts the person and role objects
|
||||||
// an address for good that passes the limit too often:
|
// it returns against a daily limit per IP address. Role objects for abuse
|
||||||
|
// contacts do not count. RIPE refuses an address for good that passes the
|
||||||
|
// limit too often:
|
||||||
// https://docs.db.ripe.net/Types-of-Queries/Access-Control-for-Queries/
|
// https://docs.db.ripe.net/Types-of-Queries/Access-Control-for-Queries/
|
||||||
// APNIC and AFRINIC run the same database software.
|
//
|
||||||
const (
|
// RIPE documents the flag here:
|
||||||
// noPersonalFlag leaves out person and role objects only, so the answer
|
// https://docs.db.ripe.net/Types-of-Queries/Referenced-Objects-in-Query-Response/
|
||||||
// keeps the organisation object:
|
// APNIC and AFRINIC list it in their answers to the query "help". AFRINIC
|
||||||
// https://docs.db.ripe.net/Types-of-Queries/Referenced-Objects-in-Query-Response/
|
// sends readers to that answer for its full list of flags:
|
||||||
noPersonalFlag = "--no-personal"
|
// https://afrinic.net/whois-getting-started
|
||||||
|
const 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:
|
// 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
|
// access denied to the client's IP address, and that address's limit
|
||||||
// listed under "Access Errors" in Appendix C of
|
// reached. APNIC and AFRINIC run the same software. The lines are listed
|
||||||
|
// under "Access Errors" on this page, whose URL is split over two lines:
|
||||||
// https://docs.db.ripe.net/Appendices/
|
// https://docs.db.ripe.net/Appendices/
|
||||||
|
// Appendix-C--RIPE-Database-Query-Server-Response-Codes-and-Messages/
|
||||||
const (
|
const (
|
||||||
accessDeniedError = "%ERROR:201:"
|
accessDeniedError = "%ERROR:201:"
|
||||||
accessLimitError = "%ERROR:202:"
|
accessLimitError = "%ERROR:202:"
|
||||||
@@ -164,22 +161,6 @@ func (c *Client) LookupASN(ctx context.Context, asn int) (*ASNInfo, error) {
|
|||||||
// The fields come from the RIR's answer alone: IANA's describes the
|
// The fields come from the RIR's answer alone: IANA's describes the
|
||||||
// block the AS is in.
|
// block the AS is in.
|
||||||
info := c.parseResponse(asn, rir, rirResp)
|
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() {
|
if info.isEmpty() {
|
||||||
return nil, fmt.Errorf("%s from %s: %w", query, whoisServer, errNoFields)
|
return nil, fmt.Errorf("%s from %s: %w", query, whoisServer, errNoFields)
|
||||||
}
|
}
|
||||||
@@ -199,17 +180,15 @@ func (info *ASNInfo) isEmpty() bool {
|
|||||||
info.RegDate == nil && info.LastMod == nil
|
info.RegDate == nil && info.LastMod == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// rirQuery returns the query sent to rir's server for key, an AS number
|
// rirQuery returns the query sent to rir's server for asQuery, an AS
|
||||||
// such as "AS3333" or an organisation ID. RIPE, APNIC and AFRINIC get a
|
// number such as "AS3333". RIPE, APNIC and AFRINIC are asked to keep person
|
||||||
// flag that keeps person and role objects out of the answer.
|
// and role objects out of the answer.
|
||||||
func rirQuery(rir, key string) string {
|
func rirQuery(rir, asQuery string) string {
|
||||||
switch rir {
|
switch rir {
|
||||||
case RIRRIPE:
|
case RIRRIPE, RIRAPNIC, RIRAFRNIC:
|
||||||
return noPersonalFlag + " " + key
|
return noPersonalFlag + " " + asQuery
|
||||||
case RIRAPNIC, RIRAFRNIC:
|
|
||||||
return noReferencedFlag + " " + key
|
|
||||||
default:
|
default:
|
||||||
return key
|
return asQuery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,24 +101,22 @@ const ripeRefusal = `% This is the RIPE Database query service.
|
|||||||
% denied because of a repeated excessive querying.
|
% denied because of a repeated excessive querying.
|
||||||
`
|
`
|
||||||
|
|
||||||
// The answers of APNIC and AFRINIC to an "-r" query, which leaves out every
|
// apnicAfrinicRecord is the answer of APNIC or AFRINIC for the AS when asked
|
||||||
// object the AS's object refers to: first for testASN, then for the
|
// to leave out person and role objects. Like RIPE's, it holds the abuse
|
||||||
// organisation that AS's object names.
|
// address as a comment, the AS's object and the organisation object.
|
||||||
const (
|
const apnicAfrinicRecord = `% Abuse contact for 'AS3333' is 'abuse@example.net'
|
||||||
answerWithoutOrganisation = `% Abuse contact for 'AS3333' is 'abuse@example.net'
|
|
||||||
|
|
||||||
aut-num: AS3333
|
aut-num: AS3333
|
||||||
as-name: EXAMPLE-AS
|
as-name: EXAMPLE-AS
|
||||||
org: ORG-EXAMPLE1
|
org: ORG-EXAMPLE1
|
||||||
admin-c: EX1-TEST
|
admin-c: EX1-TEST
|
||||||
last-modified: 2026-08-26T00:40:00Z
|
last-modified: 2026-08-26T00:40:00Z
|
||||||
`
|
|
||||||
organisationAnswer = `organisation: ORG-EXAMPLE1
|
organisation: ORG-EXAMPLE1
|
||||||
org-name: Example Networks
|
org-name: Example Networks
|
||||||
country: ZA
|
country: ZA
|
||||||
address: 1 Example Road
|
address: 1 Example Road
|
||||||
`
|
`
|
||||||
)
|
|
||||||
|
|
||||||
// ianaPrivateUse is IANA's answer for a private-use AS: its block and no
|
// ianaPrivateUse is IANA's answer for a private-use AS: its block and no
|
||||||
// referral.
|
// referral.
|
||||||
@@ -239,11 +237,11 @@ func TestLookupASNReadsTheRIRAnswer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLookupASNAsksForTheOrganisationOnItsOwn looks up an AS at APNIC and
|
// TestLookupASNAsksAPNICAndAFRINICToLeaveOutPersonalData looks up an AS at
|
||||||
// at AFRINIC. Each answers only "-r" queries, whose answer for the AS
|
// APNIC and at AFRINIC. Each answers only a query that leaves out person
|
||||||
// leaves out its organisation object, so the client asks for that object
|
// and role objects, as RIPE does. That one answer still holds the
|
||||||
// too. The organisation's name and country come from that second answer.
|
// organisation object, so the organisation's name and country are filled.
|
||||||
func TestLookupASNAsksForTheOrganisationOnItsOwn(t *testing.T) {
|
func TestLookupASNAsksAPNICAndAFRINICToLeaveOutPersonalData(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, server := range []string{"whois.apnic.net", "whois.afrinic.net"} {
|
for _, server := range []string{"whois.apnic.net", "whois.afrinic.net"} {
|
||||||
@@ -253,9 +251,8 @@ func TestLookupASNAsksForTheOrganisationOnItsOwn(t *testing.T) {
|
|||||||
address := server + ":43"
|
address := server + ":43"
|
||||||
|
|
||||||
info, err := lookUp(t, map[string]string{
|
info, err := lookUp(t, map[string]string{
|
||||||
ianaQuery: "refer: " + server + "\n",
|
ianaQuery: "refer: " + server + "\n",
|
||||||
address + " -r " + asQuery: answerWithoutOrganisation,
|
address + " --no-personal " + asQuery: apnicAfrinicRecord,
|
||||||
address + " -r ORG-EXAMPLE1": organisationAnswer,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("LookupASN: %v", err)
|
t.Fatalf("LookupASN: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user