mirror of
https://github.com/oneclickvirt/backtrace.git
synced 2026-07-23 03:20:10 +08:00
253 lines
8.6 KiB
Go
253 lines
8.6 KiB
Go
package bgptools
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestQueryIPBGPReportOfflineFixtures(t *testing.T) {
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasPrefix(r.URL.Path, "/rdap/"):
|
|
if got := strings.TrimPrefix(r.URL.Path, "/rdap/"); got != "192.0.2.1" {
|
|
t.Errorf("RDAP IP = %q", got)
|
|
}
|
|
w.Header().Set("Content-Type", "application/rdap+json")
|
|
_, _ = fmt.Fprintf(w, `{"handle":"NET-ARIN-TEST","port43":"whois.arin.net","cidr0_cidrs":[{"v4prefix":"192.0.2.42","length":24}],"events":[{"eventAction":"registration","eventDate":"2020-01-02T03:04:05Z"}],"links":[{"rel":"geofeed","href":%q}]}`, server.URL+"/geofeed.csv")
|
|
case r.URL.Path == "/geofeed.csv":
|
|
_, _ = io.WriteString(w, "192.0.2.0/24,US,US-CA,Example City,\n")
|
|
case r.URL.Path == "/ripe":
|
|
if got := r.URL.Query().Get("resource"); got != "64500" {
|
|
t.Errorf("resource query = %q", got)
|
|
}
|
|
_, _ = io.WriteString(w, `{"data":{"neighbours":[{"asn":64501,"name":"Transit","relationship":"upstream"},{"asn":64502,"name":"Peer","relationship":"peer"}]}}`)
|
|
case r.URL.Path == "/peering":
|
|
_, _ = io.WriteString(w, `{"data":[{"ix_id":7,"name":"Example IX"}]}`)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
report, err := QueryIPBGPReport(context.Background(), "192.0.2.1", IPBGPReportConfig{
|
|
Timeout: time.Second,
|
|
RDAPClient: server.Client(),
|
|
RDAPBaseURL: server.URL + "/rdap",
|
|
FetchGeofeed: true,
|
|
GeofeedClient: server.Client(),
|
|
ResolveASN: func(_ context.Context, ip string) (string, error) {
|
|
if ip != "192.0.2.1" {
|
|
t.Fatalf("resolver IP = %q", ip)
|
|
}
|
|
return "AS64500", nil
|
|
},
|
|
Relationships: RelationshipConfig{
|
|
Client: server.Client(),
|
|
RIPEstatURL: server.URL + "/ripe",
|
|
PeeringDBURL: server.URL + "/peering",
|
|
Timeout: time.Second,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if report.Status != ReportAvailable || report.IP != "192.0.2.1" || report.ASN != "64500" {
|
|
t.Fatalf("unexpected report identity/status: %+v", report)
|
|
}
|
|
if len(report.Prefixes) != 1 || report.Prefixes[0] != "192.0.2.0/24" || report.PrefixSource != "rdap" {
|
|
t.Fatalf("unexpected prefixes: %+v source=%q", report.Prefixes, report.PrefixSource)
|
|
}
|
|
if report.RIR.Name != "ARIN" || report.RIR.Status != ReportAvailable || report.RegistrationDate == nil {
|
|
t.Fatalf("unexpected registry fields: RIR=%+v registration=%v", report.RIR, report.RegistrationDate)
|
|
}
|
|
if len(report.Geofeeds) != 1 || report.Geofeeds[0].Status != ReportAvailable || report.Geofeeds[0].Bytes == 0 {
|
|
t.Fatalf("unexpected geofeed result: %+v", report.Geofeeds)
|
|
}
|
|
if report.Relationships == nil || len(report.Relationships.Upstreams) != 1 || len(report.Relationships.Peers) != 1 || len(report.Relationships.IXPs) != 1 {
|
|
t.Fatalf("unexpected relationships: %+v", report.Relationships)
|
|
}
|
|
}
|
|
|
|
func TestQueryIPBGPReportWHOISFallbackTCPFixture(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
tcpErr := make(chan error, 1)
|
|
go func() {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
tcpErr <- err
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
query, err := bufio.NewReader(conn).ReadString('\n')
|
|
if err != nil {
|
|
tcpErr <- err
|
|
return
|
|
}
|
|
if strings.TrimSpace(query) != "203.0.113.9" {
|
|
tcpErr <- fmt.Errorf("WHOIS query = %q", query)
|
|
return
|
|
}
|
|
_, err = io.WriteString(conn, "CIDR: 203.0.113.99/24\r\nRegDate: 2021-02-03\r\nSource: ARIN\r\nGeofeed: https://example.test/geofeed.csv\r\n")
|
|
tcpErr <- err
|
|
}()
|
|
|
|
rdapServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
http.Error(w, "unavailable", http.StatusServiceUnavailable)
|
|
}))
|
|
defer rdapServer.Close()
|
|
|
|
report, err := QueryIPBGPReport(context.Background(), "203.0.113.9", IPBGPReportConfig{
|
|
Timeout: time.Second,
|
|
RDAPClient: rdapServer.Client(),
|
|
RDAPBaseURL: rdapServer.URL,
|
|
EnableWHOISFallback: true,
|
|
WHOISServer: listener.Addr().String(),
|
|
WHOISTimeout: 250 * time.Millisecond,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := <-tcpErr; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if report.Status != ReportPartial || report.WHOIS == nil || report.WHOIS.Status != ReportAvailable {
|
|
t.Fatalf("unexpected fallback status: %+v", report)
|
|
}
|
|
if report.PrefixSource != "whois" || len(report.Prefixes) != 1 || report.Prefixes[0] != "203.0.113.0/24" {
|
|
t.Fatalf("unexpected WHOIS prefixes: %+v", report.Prefixes)
|
|
}
|
|
if report.RIR.Name != "ARIN" || report.RIR.Source != "whois" || report.RegistrationDate == nil {
|
|
t.Fatalf("unexpected WHOIS metadata: RIR=%+v registration=%v", report.RIR, report.RegistrationDate)
|
|
}
|
|
if len(report.Geofeeds) != 1 || report.Geofeeds[0].Status != ReportUnsupported {
|
|
t.Fatalf("disabled geofeed fetch was not explicit: %+v", report.Geofeeds)
|
|
}
|
|
}
|
|
|
|
func TestQueryIPBGPReportWHOISTimeoutIsBounded(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
_, _ = bufio.NewReader(conn).ReadString('\n')
|
|
_, _ = io.Copy(io.Discard, conn)
|
|
}()
|
|
|
|
rdapServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
http.Error(w, "unavailable", http.StatusServiceUnavailable)
|
|
}))
|
|
defer rdapServer.Close()
|
|
|
|
started := time.Now()
|
|
report, err := QueryIPBGPReport(context.Background(), "198.51.100.1", IPBGPReportConfig{
|
|
Timeout: time.Second,
|
|
RDAPClient: rdapServer.Client(),
|
|
RDAPBaseURL: rdapServer.URL,
|
|
EnableWHOISFallback: true,
|
|
WHOISServer: listener.Addr().String(),
|
|
WHOISTimeout: 30 * time.Millisecond,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if elapsed := time.Since(started); elapsed > 500*time.Millisecond {
|
|
t.Fatalf("WHOIS timeout took %v", elapsed)
|
|
}
|
|
if source := unifiedSourceByName(report, "whois"); source.Status != ReportTimeout {
|
|
t.Fatalf("WHOIS source = %+v, want timeout", source)
|
|
}
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("TCP fixture did not observe connection cleanup")
|
|
}
|
|
}
|
|
|
|
func TestQueryIPBGPReportResolvesWHOISFromIANABootstrap(t *testing.T) {
|
|
bootstrap := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = io.WriteString(w, `{"services":[[["203.0.113.0/24"],["https://rdap.arin.net/registry/"]]]}`)
|
|
}))
|
|
defer bootstrap.Close()
|
|
rdapServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
http.Error(w, "unavailable", http.StatusServiceUnavailable)
|
|
}))
|
|
defer rdapServer.Close()
|
|
|
|
dialed := make(chan string, 1)
|
|
report, err := QueryIPBGPReport(context.Background(), "203.0.113.9", IPBGPReportConfig{
|
|
Timeout: time.Second,
|
|
RDAPClient: rdapServer.Client(),
|
|
RDAPBaseURL: rdapServer.URL,
|
|
EnableWHOISFallback: true,
|
|
WHOISBootstrapClient: bootstrap.Client(),
|
|
WHOISBootstrapURL: bootstrap.URL,
|
|
WHOISTimeout: 250 * time.Millisecond,
|
|
WHOISDialContext: func(_ context.Context, _, address string) (net.Conn, error) {
|
|
dialed <- address
|
|
client, server := net.Pipe()
|
|
go func() {
|
|
defer server.Close()
|
|
_, _ = bufio.NewReader(server).ReadString('\n')
|
|
_, _ = io.WriteString(server, "CIDR: 203.0.113.0/24\r\nSource: ARIN\r\n")
|
|
}()
|
|
return client, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := <-dialed; got != "whois.arin.net:43" {
|
|
t.Fatalf("WHOIS address = %q", got)
|
|
}
|
|
if report.WHOIS == nil || report.WHOIS.Status != ReportAvailable || unifiedSourceByName(report, "whois_bootstrap").Status != ReportAvailable {
|
|
t.Fatalf("bootstrap WHOIS evidence missing: %+v", report)
|
|
}
|
|
}
|
|
|
|
func TestQueryIPBGPReportRejectsInvalidInput(t *testing.T) {
|
|
if _, err := QueryIPBGPReport(context.Background(), "", IPBGPReportConfig{}); !errors.Is(err, ErrInvalidIPAddress) {
|
|
t.Fatalf("error = %v, want %v", err, ErrInvalidIPAddress)
|
|
}
|
|
}
|
|
|
|
func TestRIRInferenceRequiresTokenBoundary(t *testing.T) {
|
|
if got := rirNameFromText("https://example.test/stripe-network"); got != "" {
|
|
t.Fatalf("false RIR inference = %q", got)
|
|
}
|
|
if got := rirNameFromText("whois.ripe.net"); got != "RIPE NCC" {
|
|
t.Fatalf("RIR inference = %q, want RIPE NCC", got)
|
|
}
|
|
}
|
|
|
|
func unifiedSourceByName(report *IPBGPReport, name string) IPBGPSourceStatus {
|
|
for _, source := range report.Sources {
|
|
if source.Source == name {
|
|
return source
|
|
}
|
|
}
|
|
return IPBGPSourceStatus{Source: name, Status: ReportError, Error: "not found"}
|
|
}
|