Initial commit: RouteWatch BGP stream monitor
- Connects to RIPE RIS Live stream to receive real-time BGP updates - Stores BGP data in SQLite database: - ASNs with first/last seen timestamps - Prefixes with IPv4/IPv6 classification - BGP announcements and withdrawals - AS-to-AS peering relationships from AS paths - Live routing table tracking active routes - HTTP server with statistics endpoints - Metrics tracking with go-metrics - Custom JSON unmarshaling to handle nested AS sets in paths - Dependency injection with uber/fx - Pure Go implementation (no CGO) - Includes streamdumper utility for debugging raw messages
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
package routewatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||
"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{
|
||||
ID: uuid.New(),
|
||||
Number: number,
|
||||
FirstSeen: timestamp,
|
||||
LastSeen: timestamp,
|
||||
}
|
||||
m.ASNs[number] = asn
|
||||
m.ASNCount++
|
||||
|
||||
return asn, 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(fromASNID, toASNID string, _ time.Time) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := fromASNID + "_" + toASNID
|
||||
if !m.Peerings[key] {
|
||||
m.Peerings[key] = true
|
||||
m.PeeringCount++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLiveRoute mock implementation
|
||||
func (m *mockStore) UpdateLiveRoute(prefixID, originASNID uuid.UUID, peerASN int, _ string, _ time.Time) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := prefixID.String() + "_" + originASNID.String() + "_" + string(rune(peerASN))
|
||||
if !m.Routes[key] {
|
||||
m.Routes[key] = true
|
||||
m.RouteCount++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithdrawLiveRoute mock implementation
|
||||
func (m *mockStore) WithdrawLiveRoute(_ uuid.UUID, _ int, _ time.Time) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.WithdrawalCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveLiveRoutes mock implementation
|
||||
func (m *mockStore) GetActiveLiveRoutes() ([]database.LiveRoute, error) {
|
||||
return []database.LiveRoute{}, 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,
|
||||
LiveRoutes: m.RouteCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestRouteWatchLiveFeed(t *testing.T) {
|
||||
// Create mock database
|
||||
mockDB := newMockStore()
|
||||
defer mockDB.Close()
|
||||
|
||||
logger := NewLogger()
|
||||
|
||||
// Create metrics tracker
|
||||
metricsTracker := metrics.New()
|
||||
|
||||
// Create streamer
|
||||
s := streamer.New(logger, metricsTracker)
|
||||
|
||||
// Create server
|
||||
srv := server.New(mockDB, s, logger)
|
||||
|
||||
// Create RouteWatch with 5 second limit
|
||||
deps := Dependencies{
|
||||
DB: mockDB,
|
||||
Streamer: s,
|
||||
Server: srv,
|
||||
Logger: logger,
|
||||
Config: Config{
|
||||
MaxRuntime: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
rw := New(deps)
|
||||
|
||||
// Run with context
|
||||
ctx := context.Background()
|
||||
go func() {
|
||||
_ = rw.Run(ctx)
|
||||
}()
|
||||
|
||||
// Wait for the configured duration
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// 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)
|
||||
|
||||
if stats.LiveRoutes == 0 {
|
||||
t.Error("Expected to have some active routes")
|
||||
}
|
||||
t.Logf("Active routes: %d", stats.LiveRoutes)
|
||||
}
|
||||
Reference in New Issue
Block a user