Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
133d2ffc66 |
@@ -37,7 +37,8 @@ tested.
|
|||||||
LACNIC's answer for such an AS comes from NIC.br, Brazil's registry, with each
|
LACNIC's answer for such an AS comes from NIC.br, Brazil's registry, with each
|
||||||
accented letter replaced. The details now come from NIC.br's own server,
|
accented letter replaced. The details now come from NIC.br's own server,
|
||||||
`whois.registro.br`. When that query fails, the details from LACNIC's answer
|
`whois.registro.br`. When that query fails, the details from LACNIC's answer
|
||||||
are kept (closes #174)
|
are kept and a warning is logged. After NIC.br answers that RouteWatch is over
|
||||||
|
its query limit, RouteWatch stops asking NIC.br for an hour (closes #174)
|
||||||
- 2026-10-27: `make asupdate` stops with an error when the download of `as.csv`
|
- 2026-10-27: `make asupdate` stops with an error when the download of `as.csv`
|
||||||
breaks off. The generator used to retry the failed read forever. A record that
|
breaks off. The generator used to retry the failed read forever. A record that
|
||||||
does not parse is still logged and skipped (closes #177)
|
does not parse is still logged and skipped (closes #177)
|
||||||
|
|||||||
@@ -68,10 +68,12 @@ type ASNFetcher struct {
|
|||||||
|
|
||||||
// NewASNFetcher creates a new ASN fetcher.
|
// NewASNFetcher creates a new ASN fetcher.
|
||||||
func NewASNFetcher(db database.Store, logger *slog.Logger) *ASNFetcher {
|
func NewASNFetcher(db database.Store, logger *slog.Logger) *ASNFetcher {
|
||||||
|
logger = logger.With("component", "asn_fetcher")
|
||||||
|
|
||||||
return &ASNFetcher{
|
return &ASNFetcher{
|
||||||
db: db,
|
db: db,
|
||||||
whoisClient: whois.NewClient(),
|
whoisClient: whois.NewClient(logger),
|
||||||
logger: logger.With("component", "asn_fetcher"),
|
logger: logger,
|
||||||
immediateQueue: make(chan int, immediateQueueSize),
|
immediateQueue: make(chan int, immediateQueueSize),
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
|
|||||||
+69
-10
@@ -7,9 +7,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -68,6 +70,13 @@ const nicbrHeader = "% Copyright (c) Nic.br"
|
|||||||
// queries to stop the data being taken in bulk.
|
// queries to stop the data being taken in bulk.
|
||||||
const nicbrLimitReached = "Query rate limit exceeded"
|
const nicbrLimitReached = "Query rate limit exceeded"
|
||||||
|
|
||||||
|
// NICbrSkipTime is how long the client stops asking NIC.br after NIC.br
|
||||||
|
// answers that the client is over its query limit. NIC.br publishes
|
||||||
|
// neither the limit nor how long it lasts. An hour outlasts a limit
|
||||||
|
// counted per minute or per hour. The ASes looked up meanwhile get
|
||||||
|
// NIC.br's fields when they are next refreshed.
|
||||||
|
const NICbrSkipTime = time.Hour
|
||||||
|
|
||||||
// RIR identifiers.
|
// RIR identifiers.
|
||||||
const (
|
const (
|
||||||
RIRARIN = "ARIN"
|
RIRARIN = "ARIN"
|
||||||
@@ -140,18 +149,36 @@ type Client struct {
|
|||||||
// host:port. NewClient sets a net.Dialer's; tests replace it to reach
|
// host:port. NewClient sets a net.Dialer's; tests replace it to reach
|
||||||
// local servers instead.
|
// local servers instead.
|
||||||
DialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
DialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||||
|
|
||||||
|
// Now returns the current time. NewClient sets time.Now; tests replace
|
||||||
|
// it to move the clock.
|
||||||
|
Now func() time.Time
|
||||||
|
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
|
// mu guards nicbrSkipUntil.
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
// nicbrSkipUntil is when the client next asks NIC.br, after NIC.br
|
||||||
|
// answered that the client is over its query limit.
|
||||||
|
nicbrSkipUntil time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new WHOIS client.
|
// NewClient creates a new WHOIS client that logs to logger.
|
||||||
func NewClient() *Client {
|
func NewClient(logger *slog.Logger) *Client {
|
||||||
dialer := &net.Dialer{Timeout: dialTimeout}
|
dialer := &net.Dialer{Timeout: dialTimeout}
|
||||||
|
|
||||||
return &Client{DialContext: dialer.DialContext}
|
return &Client{
|
||||||
|
DialContext: dialer.DialContext,
|
||||||
|
Now: time.Now,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LookupASN asks IANA which RIR holds the given ASN, asks that RIR, and
|
// LookupASN asks IANA which RIR holds the given ASN, asks that RIR, and
|
||||||
// returns what the RIR's answer says. When LACNIC's answer is NIC.br's, it
|
// 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.
|
// returns what NIC.br's own server says instead, unless that query fails or
|
||||||
|
// is skipped, as askNICbr says.
|
||||||
//
|
//
|
||||||
// It returns ErrNoReferral when IANA refers the AS to no RIR, and another
|
// 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
|
// error when the query to IANA or to the RIR fails, when the RIR refuses
|
||||||
@@ -209,19 +236,51 @@ func (c *Client) LookupASN(ctx context.Context, asn int) (*ASNInfo, error) {
|
|||||||
// askNICbr asks NIC.br's server about an AS whose answer from LACNIC is
|
// 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
|
// 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
|
// 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,
|
// them, in Latin-1.
|
||||||
// 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.
|
// askNICbr returns lacnicInfo, what LACNIC's answer says, and logs a
|
||||||
|
// warning 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. After the reduced answer, it returns lacnicInfo without asking
|
||||||
|
// NIC.br until NICbrSkipTime has passed.
|
||||||
func (c *Client) askNICbr(
|
func (c *Client) askNICbr(
|
||||||
ctx context.Context, asQuery string, lacnicInfo *ASNInfo,
|
ctx context.Context, asQuery string, lacnicInfo *ASNInfo,
|
||||||
) *ASNInfo {
|
) *ASNInfo {
|
||||||
answer, err := c.query(ctx, whoisServerNICbr, asQuery)
|
asn := lacnicInfo.ASN
|
||||||
if err != nil || strings.Contains(answer, nicbrLimitReached) {
|
|
||||||
|
c.mu.Lock()
|
||||||
|
skipping := c.Now().Before(c.nicbrSkipUntil)
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if skipping {
|
||||||
return lacnicInfo
|
return lacnicInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
info := c.parseResponse(lacnicInfo.ASN, RIRLACNIC, answer)
|
answer, err := c.query(ctx, whoisServerNICbr, asQuery)
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Warn("NIC.br query failed, keeping LACNIC's fields",
|
||||||
|
"asn", asn, "error", err)
|
||||||
|
|
||||||
|
return lacnicInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(answer, nicbrLimitReached) {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.nicbrSkipUntil = c.Now().Add(NICbrSkipTime)
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
c.logger.Warn("NIC.br answered that the client is over its query "+
|
||||||
|
"limit, keeping LACNIC's fields and skipping NIC.br",
|
||||||
|
"asn", asn, "skip_for", NICbrSkipTime)
|
||||||
|
|
||||||
|
return lacnicInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
info := c.parseResponse(asn, RIRLACNIC, answer)
|
||||||
if info.isEmpty() {
|
if info.isEmpty() {
|
||||||
|
c.logger.Warn("NIC.br answered with no WHOIS fields, "+
|
||||||
|
"keeping LACNIC's fields", "asn", asn)
|
||||||
|
|
||||||
return lacnicInfo
|
return lacnicInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+149
-24
@@ -2,9 +2,12 @@ package whois_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"maps"
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -277,7 +280,7 @@ func lookUp(
|
|||||||
) (*whois.ASNInfo, error) {
|
) (*whois.ASNInfo, error) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
client := whois.NewClient()
|
client := whois.NewClient(slog.New(slog.DiscardHandler))
|
||||||
client.DialContext = fakeServers(t, answers)
|
client.DialContext = fakeServers(t, answers)
|
||||||
|
|
||||||
return client.LookupASN(t.Context(), testASN)
|
return client.LookupASN(t.Context(), testASN)
|
||||||
@@ -335,7 +338,7 @@ func TestLookupASNReadsTheRIRAnswer(t *testing.T) {
|
|||||||
func TestLookupASNReadsOnlyTheASAndItsOrganisation(t *testing.T) {
|
func TestLookupASNReadsOnlyTheASAndItsOrganisation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
client := whois.NewClient()
|
client := whois.NewClient(slog.New(slog.DiscardHandler))
|
||||||
client.DialContext = fakeServers(t, map[string]string{
|
client.DialContext = fakeServers(t, map[string]string{
|
||||||
apnicIANAQuery: "refer: whois.apnic.net\n",
|
apnicIANAQuery: "refer: whois.apnic.net\n",
|
||||||
apnicQuery: apnicRecord,
|
apnicQuery: apnicRecord,
|
||||||
@@ -539,6 +542,30 @@ created: 20120829
|
|||||||
changed: 20130306
|
changed: 20130306
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// newNICbrClient returns a client whose fake servers send answers and
|
||||||
|
// whose logger writes JSON lines to logs. It adds one to *nicbrAsks each
|
||||||
|
// time it connects to NIC.br's server.
|
||||||
|
func newNICbrClient(
|
||||||
|
t *testing.T, answers map[string]string, logs io.Writer, nicbrAsks *int,
|
||||||
|
) *whois.Client {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dial := fakeServers(t, answers)
|
||||||
|
|
||||||
|
client := whois.NewClient(slog.New(slog.NewJSONHandler(logs, nil)))
|
||||||
|
client.DialContext = func(
|
||||||
|
ctx context.Context, network, address string,
|
||||||
|
) (net.Conn, error) {
|
||||||
|
if address == nicbrServer {
|
||||||
|
*nicbrAsks++
|
||||||
|
}
|
||||||
|
|
||||||
|
return dial(ctx, network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
// lookUpBrazilianAS looks brazilASN up with fake servers sending answers,
|
// lookUpBrazilianAS looks brazilASN up with fake servers sending answers,
|
||||||
// and reports whether the client connected to NIC.br's server.
|
// and reports whether the client connected to NIC.br's server.
|
||||||
func lookUpBrazilianAS(
|
func lookUpBrazilianAS(
|
||||||
@@ -546,23 +573,12 @@ func lookUpBrazilianAS(
|
|||||||
) (*whois.ASNInfo, bool, error) {
|
) (*whois.ASNInfo, bool, error) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dial := fakeServers(t, answers)
|
nicbrAsks := 0
|
||||||
askedNICbr := false
|
client := newNICbrClient(t, answers, io.Discard, &nicbrAsks)
|
||||||
|
|
||||||
client := whois.NewClient()
|
|
||||||
client.DialContext = func(
|
|
||||||
ctx context.Context, network, address string,
|
|
||||||
) (net.Conn, error) {
|
|
||||||
if address == nicbrServer {
|
|
||||||
askedNICbr = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return dial(ctx, network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := client.LookupASN(t.Context(), brazilASN)
|
info, err := client.LookupASN(t.Context(), brazilASN)
|
||||||
|
|
||||||
return info, askedNICbr, err
|
return info, nicbrAsks > 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLookupASNTakesABrazilianASFromNICbr looks up an AS that NIC.br
|
// TestLookupASNTakesABrazilianASFromNICbr looks up an AS that NIC.br
|
||||||
@@ -599,17 +615,58 @@ func TestLookupASNTakesABrazilianASFromNICbr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLookupASNAsksNICbrOnlyForItsASes looks up an AS at LACNIC whose
|
||||||
|
// answer does not carry NIC.br's copyright notice, as for an AS that LACNIC
|
||||||
|
// registers itself. The client does not connect to NIC.br's server, which
|
||||||
|
// would answer with a record, and keeps LACNIC's fields.
|
||||||
|
func TestLookupASNAsksNICbrOnlyForItsASes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
info, askedNICbr, err := lookUpBrazilianAS(t, map[string]string{
|
||||||
|
brazilIANAQuery: lacnicReferral,
|
||||||
|
brazilLACNICQuery: "aut-num: AS52751\n" +
|
||||||
|
"owner: " + utf8OrgName + "\n",
|
||||||
|
brazilNICbrQuery: nicbrRecord,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LookupASN: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if askedNICbr {
|
||||||
|
t.Error("LookupASN asked NIC.br's server about an AS whose " +
|
||||||
|
"answer from LACNIC is not NIC.br's")
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.OrgName != utf8OrgName {
|
||||||
|
t.Errorf("LookupASN org name = %q, want LACNIC's %q",
|
||||||
|
info.OrgName, utf8OrgName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestLookupASNKeepsLACNICsAnswerWhenNICbrFails looks up an AS that NIC.br
|
// TestLookupASNKeepsLACNICsAnswerWhenNICbrFails looks up an AS that NIC.br
|
||||||
// registers when NIC.br's server refuses the connection, sends nothing, or
|
// registers when NIC.br's server refuses the connection, sends nothing, or
|
||||||
// sends the reduced answer for a client over its query limit. The client
|
// sends the reduced answer for a client over its query limit. The client
|
||||||
// asks NIC.br, and the lookup still succeeds with LACNIC's fields.
|
// asks NIC.br, and the lookup still succeeds with LACNIC's fields. The
|
||||||
|
// client logs one warning, which names the AS and the reason.
|
||||||
func TestLookupASNKeepsLACNICsAnswerWhenNICbrFails(t *testing.T) {
|
func TestLookupASNKeepsLACNICsAnswerWhenNICbrFails(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for name, nicbrAnswer := range map[string]map[string]string{
|
for name, test := range map[string]struct {
|
||||||
"connection refused": {},
|
nicbrAnswer map[string]string
|
||||||
"no answer": {brazilNICbrQuery: ""},
|
reason string
|
||||||
"over the query limit": {brazilNICbrQuery: nicbrOverLimit},
|
}{
|
||||||
|
"connection refused": {
|
||||||
|
nicbrAnswer: map[string]string{},
|
||||||
|
reason: syscall.ECONNREFUSED.Error(),
|
||||||
|
},
|
||||||
|
"no answer": {
|
||||||
|
nicbrAnswer: map[string]string{brazilNICbrQuery: ""},
|
||||||
|
reason: "no WHOIS fields",
|
||||||
|
},
|
||||||
|
"over the query limit": {
|
||||||
|
nicbrAnswer: map[string]string{brazilNICbrQuery: nicbrOverLimit},
|
||||||
|
reason: "over its query limit",
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -618,14 +675,19 @@ func TestLookupASNKeepsLACNICsAnswerWhenNICbrFails(t *testing.T) {
|
|||||||
brazilIANAQuery: lacnicReferral,
|
brazilIANAQuery: lacnicReferral,
|
||||||
brazilLACNICQuery: lacnicBrazilRecord,
|
brazilLACNICQuery: lacnicBrazilRecord,
|
||||||
}
|
}
|
||||||
maps.Copy(answers, nicbrAnswer)
|
maps.Copy(answers, test.nicbrAnswer)
|
||||||
|
|
||||||
info, askedNICbr, err := lookUpBrazilianAS(t, answers)
|
var logs bytes.Buffer
|
||||||
|
|
||||||
|
nicbrAsks := 0
|
||||||
|
client := newNICbrClient(t, answers, &logs, &nicbrAsks)
|
||||||
|
|
||||||
|
info, err := client.LookupASN(t.Context(), brazilASN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("LookupASN: %v", err)
|
t.Fatalf("LookupASN: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !askedNICbr {
|
if nicbrAsks == 0 {
|
||||||
t.Error("LookupASN did not ask NIC.br's server")
|
t.Error("LookupASN did not ask NIC.br's server")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,10 +695,73 @@ func TestLookupASNKeepsLACNICsAnswerWhenNICbrFails(t *testing.T) {
|
|||||||
t.Errorf("LookupASN org name = %q, want LACNIC's %q",
|
t.Errorf("LookupASN org name = %q, want LACNIC's %q",
|
||||||
info.OrgName, lacnicBrazilOrgName)
|
info.OrgName, lacnicBrazilOrgName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var logged struct {
|
||||||
|
Level string `json:"level"`
|
||||||
|
ASN int `json:"asn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(logs.Bytes(), &logged)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("want one log line, got %q: %v", logs.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logged.Level != slog.LevelWarn.String() ||
|
||||||
|
logged.ASN != brazilASN ||
|
||||||
|
!strings.Contains(logs.String(), test.reason) {
|
||||||
|
t.Errorf("logged %q, want a warning for AS%d holding %q",
|
||||||
|
logs.String(), brazilASN, test.reason)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLookupASNSkipsNICbrAfterItsLimit looks up an AS that NIC.br
|
||||||
|
// registers three times with one client, while NIC.br answers that the
|
||||||
|
// client is over its query limit. The first lookup asks NIC.br. The
|
||||||
|
// second, a second before NICbrSkipTime has passed, does not. The third,
|
||||||
|
// once NICbrSkipTime has passed, asks again. Each keeps LACNIC's fields.
|
||||||
|
func TestLookupASNSkipsNICbrAfterItsLimit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
nicbrAsks := 0
|
||||||
|
client := newNICbrClient(t, map[string]string{
|
||||||
|
brazilIANAQuery: lacnicReferral,
|
||||||
|
brazilLACNICQuery: lacnicBrazilRecord,
|
||||||
|
brazilNICbrQuery: nicbrOverLimit,
|
||||||
|
}, io.Discard, &nicbrAsks)
|
||||||
|
|
||||||
|
start := time.Date(2026, 10, 27, 0, 0, 0, 0, time.UTC)
|
||||||
|
now := start
|
||||||
|
client.Now = func() time.Time { return now }
|
||||||
|
|
||||||
|
for _, step := range []struct {
|
||||||
|
after time.Duration
|
||||||
|
wantAsks int
|
||||||
|
}{
|
||||||
|
{after: 0, wantAsks: 1},
|
||||||
|
{after: whois.NICbrSkipTime - time.Second, wantAsks: 1},
|
||||||
|
{after: whois.NICbrSkipTime, wantAsks: 2},
|
||||||
|
} {
|
||||||
|
now = start.Add(step.after)
|
||||||
|
|
||||||
|
info, err := client.LookupASN(t.Context(), brazilASN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LookupASN after %v: %v", step.after, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nicbrAsks != step.wantAsks {
|
||||||
|
t.Errorf("after %v NIC.br's server was asked %d times, want %d",
|
||||||
|
step.after, nicbrAsks, step.wantAsks)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.OrgName != lacnicBrazilOrgName {
|
||||||
|
t.Errorf("LookupASN after %v org name = %q, want LACNIC's %q",
|
||||||
|
step.after, info.OrgName, lacnicBrazilOrgName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestLookupASNReportsARefusal looks up an AS IANA refers to RIPE when RIPE
|
// TestLookupASNReportsARefusal looks up an AS IANA refers to RIPE when RIPE
|
||||||
// refuses the query, with each of the error lines RIPE refuses with. The
|
// refuses the query, with each of the error lines RIPE refuses with. The
|
||||||
// error holds RIPE's line, so the log says why the lookup failed.
|
// error holds RIPE's line, so the log says why the lookup failed.
|
||||||
|
|||||||
Reference in New Issue
Block a user