check / check (push) Canceled after 0s
A lookup whose RIR query failed, or whose RIR answer carried none of the WHOIS fields (a refusal of service, for one), returned IANA's answer as a success. The fetcher stored it as complete with blank fields, did not look the AS up again for 30 days, and shortened its interval. Such a lookup is now an error: the fetcher backs off and leaves the row as it was. The fields are read from the RIR's answer alone. An AS IANA refers to no RIR is stored with no WHOIS data, so it is not looked up again until it is stale, counting as neither a success nor a failure. Tests reach fake WHOIS servers through the client's DialContext. Model: opus-5-5
190 lines
5.0 KiB
Go
190 lines
5.0 KiB
Go
package whois_test
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/whois"
|
|
)
|
|
|
|
// The servers LookupASN asks: IANA first, then the RIR IANA refers it to.
|
|
const (
|
|
ianaServer = "whois.iana.org:43"
|
|
ripeServer = "whois.ripe.net:43"
|
|
)
|
|
|
|
// testASN is the AS the tests look up.
|
|
const testASN = 3333
|
|
|
|
// ianaReferral is IANA's answer for an AS in a block it gave to RIPE.
|
|
const ianaReferral = `% IANA WHOIS server
|
|
|
|
refer: whois.ripe.net
|
|
|
|
as-block: 3154-3353
|
|
organisation: Assigned by RIPE NCC
|
|
whois: whois.ripe.net
|
|
changed: 2002-09-01
|
|
source: IANA
|
|
`
|
|
|
|
// ripeRecord is RIPE's answer for the AS.
|
|
const ripeRecord = `% This is the RIPE Database query service.
|
|
|
|
aut-num: AS3333
|
|
as-name: RIPE-NCC-AS
|
|
org: ORG-RIEN1-RIPE
|
|
created: 2002-09-17T13:08:32Z
|
|
last-modified: 2024-01-09T08:55:41Z
|
|
source: RIPE
|
|
`
|
|
|
|
// ripeRefusal is RIPE's answer to a host it no longer serves.
|
|
const ripeRefusal = `% This is the RIPE Database query service.
|
|
% The objects are in RPSL format.
|
|
%
|
|
%ERROR:201: access denied for 192.0.2.1
|
|
%
|
|
% Sorry, access from your host has been permanently
|
|
% denied because of a repeated excessive querying.
|
|
`
|
|
|
|
// ianaPrivateUse is IANA's answer for a private-use AS: its block and no
|
|
// referral.
|
|
const ianaPrivateUse = `% IANA WHOIS server
|
|
|
|
as-block: 64512-65534
|
|
organisation: Reserved for Private Use
|
|
descr: Reserved for Private Use
|
|
source: IANA
|
|
`
|
|
|
|
// ianaNoObject is IANA's answer when it has nothing for the query.
|
|
const ianaNoObject = `% IANA WHOIS server
|
|
% This query returned 0 objects.
|
|
`
|
|
|
|
// fakeServers returns a DialContext for a whois.Client that connects to a
|
|
// fake server for each address in answers, which reads the query and sends
|
|
// answers[address]. Connecting to any other address is refused.
|
|
func fakeServers(
|
|
t *testing.T, answers map[string]string,
|
|
) func(context.Context, string, string) (net.Conn, error) {
|
|
t.Helper()
|
|
|
|
var servers sync.WaitGroup
|
|
|
|
t.Cleanup(servers.Wait)
|
|
|
|
return func(_ context.Context, _, address string) (net.Conn, error) {
|
|
answer, ok := answers[address]
|
|
if !ok {
|
|
return nil, syscall.ECONNREFUSED
|
|
}
|
|
|
|
client, server := net.Pipe()
|
|
|
|
servers.Go(func() {
|
|
defer func() { _ = server.Close() }()
|
|
|
|
_, _ = bufio.NewReader(server).ReadString('\n')
|
|
_, _ = io.WriteString(server, answer)
|
|
})
|
|
|
|
return client, nil
|
|
}
|
|
}
|
|
|
|
// lookUp looks testASN up with fake servers sending answers.
|
|
func lookUp(
|
|
t *testing.T, answers map[string]string,
|
|
) (*whois.ASNInfo, error) {
|
|
t.Helper()
|
|
|
|
client := whois.NewClient()
|
|
client.DialContext = fakeServers(t, answers)
|
|
|
|
return client.LookupASN(t.Context(), testASN)
|
|
}
|
|
|
|
// TestLookupASNReadsTheRIRAnswer looks up an AS IANA refers to RIPE. The
|
|
// fields come from RIPE's answer, the last-modified date included: IANA's
|
|
// "changed:" line dates its block, not the AS.
|
|
func TestLookupASNReadsTheRIRAnswer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
info, err := lookUp(t, map[string]string{
|
|
ianaServer: ianaReferral,
|
|
ripeServer: ripeRecord,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LookupASN: %v", err)
|
|
}
|
|
|
|
if info.ASName != "RIPE-NCC-AS" || info.OrgID != "ORG-RIEN1-RIPE" ||
|
|
info.RIR != whois.RIRRIPE {
|
|
t.Errorf("LookupASN = AS name %q, org %q, RIR %q; "+
|
|
"want RIPE-NCC-AS, ORG-RIEN1-RIPE, RIPE",
|
|
info.ASName, info.OrgID, info.RIR)
|
|
}
|
|
|
|
wantLastMod := time.Date(2024, 1, 9, 8, 55, 41, 0, time.UTC)
|
|
if info.LastMod == nil || !info.LastMod.Equal(wantLastMod) {
|
|
t.Errorf("LookupASN last modified = %v, want %v",
|
|
info.LastMod, wantLastMod)
|
|
}
|
|
}
|
|
|
|
// TestLookupASNFailsWhenTheRIRGivesNoAnswer looks up an AS IANA refers to
|
|
// RIPE when RIPE's server refuses the connection, and when it answers only
|
|
// with an error. Neither is a lookup that found the AS.
|
|
func TestLookupASNFailsWhenTheRIRGivesNoAnswer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for name, answers := range map[string]map[string]string{
|
|
"connection refused": {ianaServer: ianaReferral},
|
|
"only an error": {ianaServer: ianaReferral, ripeServer: ripeRefusal},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := lookUp(t, answers)
|
|
if err == nil || errors.Is(err, whois.ErrNoReferral) {
|
|
t.Errorf("LookupASN error = %v, want one that is not "+
|
|
"ErrNoReferral", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLookupASNReportsNoReferral looks up a private-use AS, which IANA
|
|
// refers to no RIR.
|
|
func TestLookupASNReportsNoReferral(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := lookUp(t, map[string]string{ianaServer: ianaPrivateUse})
|
|
if !errors.Is(err, whois.ErrNoReferral) {
|
|
t.Errorf("LookupASN error = %v, want ErrNoReferral", err)
|
|
}
|
|
}
|
|
|
|
// TestLookupASNFailsWhenIANAAnswersNoBlock looks up an AS when IANA answers
|
|
// with neither a referral nor the AS's block. That is not IANA saying no
|
|
// RIR holds the AS.
|
|
func TestLookupASNFailsWhenIANAAnswersNoBlock(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := lookUp(t, map[string]string{ianaServer: ianaNoObject})
|
|
if err == nil || errors.Is(err, whois.ErrNoReferral) {
|
|
t.Errorf("LookupASN error = %v, want one that is not ErrNoReferral",
|
|
err)
|
|
}
|
|
}
|