check / check (push) Canceled after 0s
The client kept every line a WHOIS server sent until the server closed the connection or 30 seconds passed, so a server that kept sending could fill memory. It now reads at most 1 MiB of an answer, and an answer that reaches that is an error. A "refer:" line in IANA's answer naming a server none of the five RIRs run was dialled as given, on any host and port; it is now an error, so the client only connects to IANA and the five RIR servers. Both errors are failed lookups, which the fetcher retries later. Model: opus-5-5
247 lines
7.0 KiB
Go
247 lines
7.0 KiB
Go
package whois_test
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"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.
|
|
`
|
|
|
|
// fillerLine is one of the lines a server that does not stop keeps sending.
|
|
const fillerLine = "remarks: and more\n"
|
|
|
|
// endlessAnswerSize is how many bytes of filler lines that server sends:
|
|
// four times the client's cap on an answer.
|
|
const endlessAnswerSize = 4 << 20
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// TestLookupASNFailsWhenTheRIRAnswerDoesNotEnd looks up an AS whose RIR
|
|
// server follows the record with filler lines far past any real answer.
|
|
// The client stops reading at its cap and fails instead of keeping them.
|
|
func TestLookupASNFailsWhenTheRIRAnswerDoesNotEnd(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
endless := ripeRecord +
|
|
strings.Repeat(fillerLine, endlessAnswerSize/len(fillerLine))
|
|
|
|
_, err := lookUp(t, map[string]string{
|
|
ianaServer: ianaReferral,
|
|
ripeServer: endless,
|
|
})
|
|
if err == nil || errors.Is(err, whois.ErrNoReferral) {
|
|
t.Errorf("LookupASN error = %v, want one that is not ErrNoReferral",
|
|
err)
|
|
}
|
|
}
|
|
|
|
// TestLookupASNFailsOnAReferralToAnotherServer looks up an AS when IANA's
|
|
// answer refers it to a server none of the five RIRs run, with a port and
|
|
// without. The client follows a referral only to an RIR's server, so it
|
|
// fails instead of asking that server, which would answer with a record.
|
|
func TestLookupASNFailsOnAReferralToAnotherServer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Each case maps the server on the "refer:" line to the address the
|
|
// client would dial to follow it. IANA's answer also names the AS's
|
|
// block, so a client that dropped the referral would report no RIR.
|
|
for refer, address := range map[string]string{
|
|
"whois.example.net:8080": "whois.example.net:8080",
|
|
"whois.example.net": "whois.example.net:43",
|
|
} {
|
|
t.Run(refer, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := lookUp(t, map[string]string{
|
|
ianaServer: "refer: " + refer + "\n\n" +
|
|
"as-block: 3154-3353\n",
|
|
address: ripeRecord,
|
|
})
|
|
if err == nil || errors.Is(err, whois.ErrNoReferral) {
|
|
t.Errorf("LookupASN error = %v, want one that is not "+
|
|
"ErrNoReferral", err)
|
|
}
|
|
})
|
|
}
|
|
}
|