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) } }