check / check (push) Successful in 3m13s
Looking up an address on /ip/ and /api/v1/ip/ read every IPv6 live route and about half of the IPv4 range index, so on a day-sized database it passed the 30-second request timeout. The lookup is now one function for both families: from the longest mask length down, it looks up the address's network prefix on the existing prefix index, and the first live route wins. The feed sends IPv6 withdrawals uncompressed, so they never matched the stored compressed prefix and never removed a route. Prefixes are now stored in the text form net/netip prints, so IPv6 withdrawals take effect. ip_start, ip_end and GetASInfoForIP are removed; nothing read them. A database from before this change must be deleted. Model: opus-5-5
482 lines
12 KiB
Go
482 lines
12 KiB
Go
package routewatch
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/config"
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"git.eeqj.de/sneak/routewatch/internal/metrics"
|
|
"git.eeqj.de/sneak/routewatch/internal/server"
|
|
"git.eeqj.de/sneak/routewatch/internal/streamer"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// mockStore is a mock implementation of database.Store for testing
|
|
type mockStore struct {
|
|
mu sync.Mutex
|
|
|
|
// Counters for tracking calls
|
|
ASNCount int
|
|
PrefixCount int
|
|
PeeringCount int
|
|
RouteCount int
|
|
WithdrawalCount int
|
|
|
|
// Track unique items
|
|
ASNs map[int]*database.ASN
|
|
Prefixes map[string]*database.Prefix
|
|
Peerings map[string]bool // key is "from_to"
|
|
Routes map[string]bool // key is "prefix_origin_peer"
|
|
|
|
// Track IP versions
|
|
IPv4Prefixes int
|
|
IPv6Prefixes int
|
|
}
|
|
|
|
// newMockStore creates a new mock store
|
|
func newMockStore() *mockStore {
|
|
return &mockStore{
|
|
ASNs: make(map[int]*database.ASN),
|
|
Prefixes: make(map[string]*database.Prefix),
|
|
Peerings: make(map[string]bool),
|
|
Routes: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
// GetOrCreateASN mock implementation
|
|
func (m *mockStore) GetOrCreateASN(number int, timestamp time.Time) (*database.ASN, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if asn, exists := m.ASNs[number]; exists {
|
|
asn.LastSeen = timestamp
|
|
|
|
return asn, nil
|
|
}
|
|
|
|
asn := &database.ASN{
|
|
ASN: number,
|
|
FirstSeen: timestamp,
|
|
LastSeen: timestamp,
|
|
}
|
|
m.ASNs[number] = asn
|
|
m.ASNCount++
|
|
|
|
return asn, nil
|
|
}
|
|
|
|
// UpdatePrefixesBatch mock implementation
|
|
func (m *mockStore) UpdatePrefixesBatch(prefixes map[string]time.Time) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
for prefix, timestamp := range prefixes {
|
|
if p, exists := m.Prefixes[prefix]; exists {
|
|
p.LastSeen = timestamp
|
|
} else {
|
|
const (
|
|
ipVersionV4 = 4
|
|
ipVersionV6 = 6
|
|
)
|
|
|
|
ipVersion := ipVersionV4
|
|
if strings.Contains(prefix, ":") {
|
|
ipVersion = ipVersionV6
|
|
}
|
|
|
|
m.Prefixes[prefix] = &database.Prefix{
|
|
ID: uuid.New(),
|
|
Prefix: prefix,
|
|
IPVersion: ipVersion,
|
|
FirstSeen: timestamp,
|
|
LastSeen: timestamp,
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetOrCreatePrefix mock implementation
|
|
func (m *mockStore) GetOrCreatePrefix(prefix string, timestamp time.Time) (*database.Prefix, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if p, exists := m.Prefixes[prefix]; exists {
|
|
p.LastSeen = timestamp
|
|
|
|
return p, nil
|
|
}
|
|
|
|
const (
|
|
ipVersionV4 = 4
|
|
ipVersionV6 = 6
|
|
)
|
|
|
|
ipVersion := ipVersionV4
|
|
if strings.Contains(prefix, ":") {
|
|
ipVersion = ipVersionV6
|
|
}
|
|
|
|
p := &database.Prefix{
|
|
ID: uuid.New(),
|
|
Prefix: prefix,
|
|
IPVersion: ipVersion,
|
|
FirstSeen: timestamp,
|
|
LastSeen: timestamp,
|
|
}
|
|
m.Prefixes[prefix] = p
|
|
m.PrefixCount++
|
|
|
|
if ipVersion == ipVersionV4 {
|
|
m.IPv4Prefixes++
|
|
} else {
|
|
m.IPv6Prefixes++
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// RecordAnnouncement mock implementation
|
|
func (m *mockStore) RecordAnnouncement(_ *database.Announcement) error {
|
|
// Not tracking announcements in detail for now
|
|
return nil
|
|
}
|
|
|
|
// RecordPeering mock implementation
|
|
func (m *mockStore) RecordPeering(asA, asB int, _ time.Time) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Normalize
|
|
if asA > asB {
|
|
asA, asB = asB, asA
|
|
}
|
|
|
|
key := fmt.Sprintf("%d_%d", asA, asB)
|
|
if !m.Peerings[key] {
|
|
m.Peerings[key] = true
|
|
m.PeeringCount++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdatePeer mock implementation
|
|
func (m *mockStore) UpdatePeer(peerIP string, peerASN int, messageType string, timestamp time.Time) error {
|
|
// Simple mock - just return nil
|
|
return nil
|
|
}
|
|
|
|
// Close mock implementation
|
|
func (m *mockStore) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// GetStats returns statistics about the mock store
|
|
func (m *mockStore) GetStats() (database.Stats, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
return database.Stats{
|
|
ASNs: len(m.ASNs),
|
|
Prefixes: len(m.Prefixes),
|
|
IPv4Prefixes: m.IPv4Prefixes,
|
|
IPv6Prefixes: m.IPv6Prefixes,
|
|
Peerings: m.PeeringCount,
|
|
Peers: 10, // Mock peer count
|
|
}, nil
|
|
}
|
|
|
|
// GetStatsContext returns statistics about the mock store with context support
|
|
func (m *mockStore) GetStatsContext(ctx context.Context) (database.Stats, error) {
|
|
return m.GetStats()
|
|
}
|
|
|
|
// UpsertLiveRoute mock implementation
|
|
func (m *mockStore) UpsertLiveRoute(route *database.LiveRoute) error {
|
|
// Simple mock - just return nil
|
|
return nil
|
|
}
|
|
|
|
// DeleteLiveRoute mock implementation
|
|
func (m *mockStore) DeleteLiveRoute(prefix string, originASN int, peerIP string) error {
|
|
// Simple mock - just return nil
|
|
return nil
|
|
}
|
|
|
|
// GetPrefixDistribution mock implementation
|
|
func (m *mockStore) GetPrefixDistribution() (ipv4 []database.PrefixDistribution, ipv6 []database.PrefixDistribution, err error) {
|
|
// Return empty distributions for now
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// GetPrefixDistributionContext mock implementation with context support
|
|
func (m *mockStore) GetPrefixDistributionContext(ctx context.Context) (ipv4 []database.PrefixDistribution, ipv6 []database.PrefixDistribution, err error) {
|
|
return m.GetPrefixDistribution()
|
|
}
|
|
|
|
// GetLiveRouteCounts mock implementation
|
|
func (m *mockStore) GetLiveRouteCounts() (ipv4Count, ipv6Count int, err error) {
|
|
// Return mock counts
|
|
return m.RouteCount / 2, m.RouteCount / 2, nil
|
|
}
|
|
|
|
// GetLiveRouteCountsContext mock implementation with context support
|
|
func (m *mockStore) GetLiveRouteCountsContext(ctx context.Context) (ipv4Count, ipv6Count int, err error) {
|
|
return m.GetLiveRouteCounts()
|
|
}
|
|
|
|
// GetASDetails mock implementation
|
|
func (m *mockStore) GetASDetails(asn int) (*database.ASN, []database.LiveRoute, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Check if ASN exists
|
|
if asnInfo, exists := m.ASNs[asn]; exists {
|
|
// Return empty prefixes for now
|
|
return asnInfo, []database.LiveRoute{}, nil
|
|
}
|
|
|
|
return nil, nil, database.ErrNoRoute
|
|
}
|
|
|
|
// GetASDetailsContext mock implementation with context support
|
|
func (m *mockStore) GetASDetailsContext(ctx context.Context, asn int) (*database.ASN, []database.LiveRoute, error) {
|
|
return m.GetASDetails(asn)
|
|
}
|
|
|
|
// GetPrefixDetails mock implementation
|
|
func (m *mockStore) GetPrefixDetails(prefix string) ([]database.LiveRoute, error) {
|
|
// Return empty routes for now
|
|
return []database.LiveRoute{}, nil
|
|
}
|
|
|
|
// GetPrefixDetailsContext mock implementation with context support
|
|
func (m *mockStore) GetPrefixDetailsContext(ctx context.Context, prefix string) ([]database.LiveRoute, error) {
|
|
return m.GetPrefixDetails(prefix)
|
|
}
|
|
|
|
func (m *mockStore) GetRandomPrefixesByLength(maskLength, ipVersion, limit int) ([]database.LiveRoute, error) {
|
|
// Return empty routes for now
|
|
return []database.LiveRoute{}, nil
|
|
}
|
|
|
|
// GetRandomPrefixesByLengthContext mock implementation with context support
|
|
func (m *mockStore) GetRandomPrefixesByLengthContext(ctx context.Context, maskLength, ipVersion, limit int) ([]database.LiveRoute, error) {
|
|
return m.GetRandomPrefixesByLength(maskLength, ipVersion, limit)
|
|
}
|
|
|
|
// GetASPeers mock implementation
|
|
func (m *mockStore) GetASPeers(asn int) ([]database.ASPeer, error) {
|
|
// Return empty peers for now
|
|
return []database.ASPeer{}, nil
|
|
}
|
|
|
|
// GetASPeersContext mock implementation with context support
|
|
func (m *mockStore) GetASPeersContext(ctx context.Context, asn int) ([]database.ASPeer, error) {
|
|
return m.GetASPeers(asn)
|
|
}
|
|
|
|
// GetIPInfo mock implementation
|
|
func (m *mockStore) GetIPInfo(ip string) (*database.IPInfo, error) {
|
|
return m.GetIPInfoContext(context.Background(), ip)
|
|
}
|
|
|
|
// GetIPInfoContext mock implementation with context support
|
|
func (m *mockStore) GetIPInfoContext(ctx context.Context, ip string) (*database.IPInfo, error) {
|
|
now := time.Now()
|
|
return &database.IPInfo{
|
|
IP: ip,
|
|
Netblock: "8.8.8.0/24",
|
|
MaskLength: 24,
|
|
IPVersion: 4,
|
|
NumPeers: 3,
|
|
ASN: 15169,
|
|
Handle: "GOOGLE",
|
|
Description: "Google LLC",
|
|
CountryCode: "US",
|
|
FirstSeen: now.Add(-24 * time.Hour),
|
|
LastSeen: now,
|
|
}, nil
|
|
}
|
|
|
|
// GetNextStaleASN mock implementation
|
|
func (m *mockStore) GetNextStaleASN(ctx context.Context, staleThreshold time.Duration) (int, error) {
|
|
return 0, database.ErrNoStaleASN
|
|
}
|
|
|
|
// UpdateASNWHOIS mock implementation
|
|
func (m *mockStore) UpdateASNWHOIS(ctx context.Context, update *database.ASNWHOISUpdate) error {
|
|
return nil
|
|
}
|
|
|
|
// GetWHOISStats mock implementation
|
|
func (m *mockStore) GetWHOISStats(ctx context.Context, staleThreshold time.Duration) (*database.WHOISStats, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
return &database.WHOISStats{
|
|
TotalASNs: len(m.ASNs),
|
|
FreshASNs: 0,
|
|
StaleASNs: 0,
|
|
NeverFetched: len(m.ASNs),
|
|
}, nil
|
|
}
|
|
|
|
// UpsertLiveRouteBatch mock implementation
|
|
func (m *mockStore) UpsertLiveRouteBatch(routes []*database.LiveRoute) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
for _, route := range routes {
|
|
// Track prefix
|
|
if _, exists := m.Prefixes[route.Prefix]; !exists {
|
|
m.Prefixes[route.Prefix] = &database.Prefix{
|
|
ID: uuid.New(),
|
|
Prefix: route.Prefix,
|
|
IPVersion: route.IPVersion,
|
|
FirstSeen: route.LastUpdated,
|
|
LastSeen: route.LastUpdated,
|
|
}
|
|
m.PrefixCount++
|
|
if route.IPVersion == 4 {
|
|
m.IPv4Prefixes++
|
|
} else {
|
|
m.IPv6Prefixes++
|
|
}
|
|
}
|
|
m.RouteCount++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteLiveRouteBatch mock implementation
|
|
func (m *mockStore) DeleteLiveRouteBatch(deletions []database.LiveRouteDeletion) error {
|
|
// Simple mock - just return nil
|
|
return nil
|
|
}
|
|
|
|
// GetOrCreateASNBatch mock implementation
|
|
func (m *mockStore) GetOrCreateASNBatch(asns map[int]time.Time) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
for number, timestamp := range asns {
|
|
if _, exists := m.ASNs[number]; !exists {
|
|
m.ASNs[number] = &database.ASN{
|
|
ASN: number,
|
|
FirstSeen: timestamp,
|
|
LastSeen: timestamp,
|
|
}
|
|
m.ASNCount++
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdatePeerBatch mock implementation
|
|
func (m *mockStore) UpdatePeerBatch(peers map[string]database.PeerUpdate) error {
|
|
// Simple mock - just return nil
|
|
return nil
|
|
}
|
|
|
|
// Vacuum mock implementation
|
|
func (m *mockStore) Vacuum(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// Analyze mock implementation
|
|
func (m *mockStore) Analyze(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// Checkpoint mock implementation
|
|
func (m *mockStore) Checkpoint(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// Ping mock implementation
|
|
func (m *mockStore) Ping(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func TestRouteWatchLiveFeed(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping live RIPE RIS network feed test in short mode; run without -short to include it")
|
|
}
|
|
|
|
// Create mock database
|
|
mockDB := newMockStore()
|
|
defer mockDB.Close()
|
|
|
|
logger := logger.New()
|
|
|
|
// Create metrics tracker
|
|
metricsTracker := metrics.New()
|
|
|
|
// Create streamer
|
|
s := streamer.New(logger, metricsTracker)
|
|
|
|
// Create test config with empty state dir (no snapshot loading)
|
|
cfg := &config.Config{
|
|
StateDir: "",
|
|
MaxRuntime: 5 * time.Second,
|
|
EnableBatchedDatabaseWrites: true,
|
|
}
|
|
|
|
// Create server
|
|
srv := server.New(mockDB, s, logger, cfg)
|
|
|
|
// Create RouteWatch with 5 second limit
|
|
deps := Dependencies{
|
|
DB: mockDB,
|
|
Streamer: s,
|
|
Server: srv,
|
|
Logger: logger,
|
|
Config: cfg,
|
|
}
|
|
rw := New(deps)
|
|
|
|
// Run with context
|
|
ctx := context.Background()
|
|
go func() {
|
|
_ = rw.Run(ctx)
|
|
}()
|
|
|
|
// Wait for the configured duration
|
|
time.Sleep(5 * time.Second)
|
|
|
|
// Force peering processing for test
|
|
if rw.peeringHandler != nil {
|
|
rw.peeringHandler.ProcessPeeringsNow()
|
|
}
|
|
|
|
// Get statistics
|
|
stats, err := mockDB.GetStats()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get stats: %v", err)
|
|
}
|
|
|
|
if stats.ASNs == 0 {
|
|
t.Error("Expected to receive some ASNs from live feed")
|
|
}
|
|
t.Logf("Received %d unique ASNs in 5 seconds", stats.ASNs)
|
|
|
|
if stats.Prefixes == 0 {
|
|
t.Error("Expected to receive some prefixes from live feed")
|
|
}
|
|
t.Logf("Received %d unique prefixes (%d IPv4, %d IPv6) in 5 seconds", stats.Prefixes, stats.IPv4Prefixes, stats.IPv6Prefixes)
|
|
|
|
if stats.Peerings == 0 {
|
|
t.Error("Expected to receive some peerings from live feed")
|
|
}
|
|
t.Logf("Recorded %d AS peering relationships in 5 seconds", stats.Peerings)
|
|
|
|
}
|