Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1831384be |
No files matched your search
@@ -37,8 +37,7 @@ 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 and a warning is logged. After NIC.br answers that RouteWatch is over
|
are kept (closes #174)
|
||||||
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,12 +68,10 @@ 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(logger),
|
whoisClient: whois.NewClient(),
|
||||||
logger: logger,
|
logger: logger.With("component", "asn_fetcher"),
|
||||||
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{}),
|
||||||
|
|||||||
+9
-68
@@ -7,11 +7,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"net"
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -70,13 +68,6 @@ 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"
|
||||||
@@ -149,36 +140,18 @@ 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 that logs to logger.
|
// NewClient creates a new WHOIS client.
|
||||||
func NewClient(logger *slog.Logger) *Client {
|
func NewClient() *Client {
|
||||||
dialer := &net.Dialer{Timeout: dialTimeout}
|
dialer := &net.Dialer{Timeout: dialTimeout}
|
||||||
|
|
||||||
return &Client{
|
return &Client{DialContext: dialer.DialContext}
|
||||||
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 or
|
// returns what NIC.br's own server says instead, unless that query fails.
|
||||||
// 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
|
||||||
@@ -236,51 +209,19 @@ 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.
|
// 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
|
||||||
// askNICbr returns lacnicInfo, what LACNIC's answer says, and logs a
|
// over its limit, or when NIC.br's answer carries none of ASNInfo's fields.
|
||||||
// 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 {
|
||||||
asn := lacnicInfo.ASN
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
skipping := c.Now().Before(c.nicbrSkipUntil)
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
if skipping {
|
|
||||||
return lacnicInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
answer, err := c.query(ctx, whoisServerNICbr, asQuery)
|
answer, err := c.query(ctx, whoisServerNICbr, asQuery)
|
||||||
if err != nil {
|
if err != nil || strings.Contains(answer, nicbrLimitReached) {
|
||||||
c.logger.Warn("NIC.br query failed, keeping LACNIC's fields",
|
|
||||||
"asn", asn, "error", err)
|
|
||||||
|
|
||||||
return lacnicInfo
|
return lacnicInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(answer, nicbrLimitReached) {
|
info := c.parseResponse(lacnicInfo.ASN, RIRLACNIC, answer)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-149
@@ -2,12 +2,9 @@ package whois_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"maps"
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -280,7 +277,7 @@ func lookUp(
|
|||||||
) (*whois.ASNInfo, error) {
|
) (*whois.ASNInfo, error) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
client := whois.NewClient(slog.New(slog.DiscardHandler))
|
client := whois.NewClient()
|
||||||
client.DialContext = fakeServers(t, answers)
|
client.DialContext = fakeServers(t, answers)
|
||||||
|
|
||||||
return client.LookupASN(t.Context(), testASN)
|
return client.LookupASN(t.Context(), testASN)
|
||||||
@@ -338,7 +335,7 @@ func TestLookupASNReadsTheRIRAnswer(t *testing.T) {
|
|||||||
func TestLookupASNReadsOnlyTheASAndItsOrganisation(t *testing.T) {
|
func TestLookupASNReadsOnlyTheASAndItsOrganisation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
client := whois.NewClient(slog.New(slog.DiscardHandler))
|
client := whois.NewClient()
|
||||||
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,
|
||||||
@@ -542,30 +539,6 @@ 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(
|
||||||
@@ -573,12 +546,23 @@ func lookUpBrazilianAS(
|
|||||||
) (*whois.ASNInfo, bool, error) {
|
) (*whois.ASNInfo, bool, error) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
nicbrAsks := 0
|
dial := fakeServers(t, answers)
|
||||||
client := newNICbrClient(t, answers, io.Discard, &nicbrAsks)
|
askedNICbr := false
|
||||||
|
|
||||||
|
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, nicbrAsks > 0, err
|
return info, askedNICbr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLookupASNTakesABrazilianASFromNICbr looks up an AS that NIC.br
|
// TestLookupASNTakesABrazilianASFromNICbr looks up an AS that NIC.br
|
||||||
@@ -615,58 +599,17 @@ 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. The
|
// asks NIC.br, and the lookup still succeeds with LACNIC's fields.
|
||||||
// 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, test := range map[string]struct {
|
for name, nicbrAnswer := range map[string]map[string]string{
|
||||||
nicbrAnswer map[string]string
|
"connection refused": {},
|
||||||
reason string
|
"no answer": {brazilNICbrQuery: ""},
|
||||||
}{
|
"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()
|
||||||
@@ -675,19 +618,14 @@ func TestLookupASNKeepsLACNICsAnswerWhenNICbrFails(t *testing.T) {
|
|||||||
brazilIANAQuery: lacnicReferral,
|
brazilIANAQuery: lacnicReferral,
|
||||||
brazilLACNICQuery: lacnicBrazilRecord,
|
brazilLACNICQuery: lacnicBrazilRecord,
|
||||||
}
|
}
|
||||||
maps.Copy(answers, test.nicbrAnswer)
|
maps.Copy(answers, nicbrAnswer)
|
||||||
|
|
||||||
var logs bytes.Buffer
|
info, askedNICbr, err := lookUpBrazilianAS(t, answers)
|
||||||
|
|
||||||
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 nicbrAsks == 0 {
|
if !askedNICbr {
|
||||||
t.Error("LookupASN did not ask NIC.br's server")
|
t.Error("LookupASN did not ask NIC.br's server")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,73 +633,10 @@ 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