Add ASN info lookup and periodic routing table statistics
- Add handle and description columns to asns table - Look up ASN info using asinfo package when creating new ASNs - Remove noisy debug logging for individual route updates - Add IPv4/IPv6 route counters and update rate tracking - Log routing table statistics every 15 seconds with IPv4/IPv6 breakdown - Track updates per second for both IPv4 and IPv6 routes separately
This commit is contained in:
parent
a555a1dee2
commit
76ec9f68b7
@ -11,6 +11,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/routewatch/pkg/asinfo"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
_ "github.com/mattn/go-sqlite3" // CGO SQLite driver
|
_ "github.com/mattn/go-sqlite3" // CGO SQLite driver
|
||||||
)
|
)
|
||||||
@ -174,12 +175,15 @@ func (d *Database) GetOrCreateASN(number int, timestamp time.Time) (*ASN, error)
|
|||||||
|
|
||||||
var asn ASN
|
var asn ASN
|
||||||
var idStr string
|
var idStr string
|
||||||
err = tx.QueryRow("SELECT id, number, first_seen, last_seen FROM asns WHERE number = ?", number).
|
var handle, description sql.NullString
|
||||||
Scan(&idStr, &asn.Number, &asn.FirstSeen, &asn.LastSeen)
|
err = tx.QueryRow("SELECT id, number, handle, description, first_seen, last_seen FROM asns WHERE number = ?", number).
|
||||||
|
Scan(&idStr, &asn.Number, &handle, &description, &asn.FirstSeen, &asn.LastSeen)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// ASN exists, update last_seen
|
// ASN exists, update last_seen
|
||||||
asn.ID, _ = uuid.Parse(idStr)
|
asn.ID, _ = uuid.Parse(idStr)
|
||||||
|
asn.Handle = handle.String
|
||||||
|
asn.Description = description.String
|
||||||
_, err = tx.Exec("UPDATE asns SET last_seen = ? WHERE id = ?", timestamp, asn.ID.String())
|
_, err = tx.Exec("UPDATE asns SET last_seen = ? WHERE id = ?", timestamp, asn.ID.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -199,15 +203,22 @@ func (d *Database) GetOrCreateASN(number int, timestamp time.Time) (*ASN, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ASN doesn't exist, create it
|
// ASN doesn't exist, create it with ASN info lookup
|
||||||
asn = ASN{
|
asn = ASN{
|
||||||
ID: generateUUID(),
|
ID: generateUUID(),
|
||||||
Number: number,
|
Number: number,
|
||||||
FirstSeen: timestamp,
|
FirstSeen: timestamp,
|
||||||
LastSeen: timestamp,
|
LastSeen: timestamp,
|
||||||
}
|
}
|
||||||
_, err = tx.Exec("INSERT INTO asns (id, number, first_seen, last_seen) VALUES (?, ?, ?, ?)",
|
|
||||||
asn.ID.String(), asn.Number, asn.FirstSeen, asn.LastSeen)
|
// Look up ASN info
|
||||||
|
if info, ok := asinfo.Get(number); ok {
|
||||||
|
asn.Handle = info.Handle
|
||||||
|
asn.Description = info.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec("INSERT INTO asns (id, number, handle, description, first_seen, last_seen) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
asn.ID.String(), asn.Number, asn.Handle, asn.Description, asn.FirstSeen, asn.LastSeen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,12 @@ import (
|
|||||||
|
|
||||||
// ASN represents an Autonomous System Number
|
// ASN represents an Autonomous System Number
|
||||||
type ASN struct {
|
type ASN struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Number int `json:"number"`
|
Number int `json:"number"`
|
||||||
FirstSeen time.Time `json:"first_seen"`
|
Handle string `json:"handle"`
|
||||||
LastSeen time.Time `json:"last_seen"`
|
Description string `json:"description"`
|
||||||
|
FirstSeen time.Time `json:"first_seen"`
|
||||||
|
LastSeen time.Time `json:"last_seen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefix represents an IP prefix (CIDR block)
|
// Prefix represents an IP prefix (CIDR block)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
CREATE TABLE IF NOT EXISTS asns (
|
CREATE TABLE IF NOT EXISTS asns (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
number INTEGER UNIQUE NOT NULL,
|
number INTEGER UNIQUE NOT NULL,
|
||||||
|
handle TEXT,
|
||||||
|
description TEXT,
|
||||||
first_seen DATETIME NOT NULL,
|
first_seen DATETIME NOT NULL,
|
||||||
last_seen DATETIME NOT NULL
|
last_seen DATETIME NOT NULL
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,7 @@ package routewatch
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@ -23,6 +24,11 @@ type Config struct {
|
|||||||
MaxRuntime time.Duration // Maximum runtime (0 = run forever)
|
MaxRuntime time.Duration // Maximum runtime (0 = run forever)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// routingTableStatsInterval is how often we log routing table statistics
|
||||||
|
routingTableStatsInterval = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
// NewConfig provides default configuration
|
// NewConfig provides default configuration
|
||||||
func NewConfig() Config {
|
func NewConfig() Config {
|
||||||
return Config{
|
return Config{
|
||||||
@ -88,6 +94,9 @@ func (rw *RouteWatch) Run(ctx context.Context) error {
|
|||||||
peerHandler := NewPeerHandler(rw.db, rw.logger)
|
peerHandler := NewPeerHandler(rw.db, rw.logger)
|
||||||
rw.streamer.RegisterHandler(peerHandler)
|
rw.streamer.RegisterHandler(peerHandler)
|
||||||
|
|
||||||
|
// Start periodic routing table stats logging
|
||||||
|
go rw.logRoutingTableStats(ctx)
|
||||||
|
|
||||||
// Start streaming
|
// Start streaming
|
||||||
if err := rw.streamer.Start(); err != nil {
|
if err := rw.streamer.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -125,6 +134,32 @@ func (rw *RouteWatch) Run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logRoutingTableStats periodically logs routing table statistics
|
||||||
|
func (rw *RouteWatch) logRoutingTableStats(ctx context.Context) {
|
||||||
|
// Log stats periodically
|
||||||
|
ticker := time.NewTicker(routingTableStatsInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
stats := rw.routingTable.GetDetailedStats()
|
||||||
|
rw.logger.Info("Routing table statistics",
|
||||||
|
"ipv4_routes", stats.IPv4Routes,
|
||||||
|
"ipv6_routes", stats.IPv6Routes,
|
||||||
|
"ipv4_updates_per_sec", fmt.Sprintf("%.2f", stats.IPv4UpdatesRate),
|
||||||
|
"ipv6_updates_per_sec", fmt.Sprintf("%.2f", stats.IPv6UpdatesRate),
|
||||||
|
"total_routes", stats.TotalRoutes,
|
||||||
|
"unique_prefixes", stats.UniquePrefixes,
|
||||||
|
"unique_origins", stats.UniqueOrigins,
|
||||||
|
"unique_peers", stats.UniquePeers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewLogger creates a structured logger
|
// NewLogger creates a structured logger
|
||||||
func NewLogger() *slog.Logger {
|
func NewLogger() *slog.Logger {
|
||||||
level := slog.LevelInfo
|
level := slog.LevelInfo
|
||||||
|
@ -70,13 +70,6 @@ func (h *RoutingTableHandler) HandleMessage(msg *ristypes.RISMessage) {
|
|||||||
|
|
||||||
// Add route to routing table
|
// Add route to routing table
|
||||||
h.rt.AddRoute(route)
|
h.rt.AddRoute(route)
|
||||||
|
|
||||||
h.logger.Debug("Added route to routing table",
|
|
||||||
"prefix", prefix,
|
|
||||||
"origin_asn", originASN,
|
|
||||||
"peer_asn", peerASN,
|
|
||||||
"path", msg.Path,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,13 +79,7 @@ func (h *RoutingTableHandler) HandleMessage(msg *ristypes.RISMessage) {
|
|||||||
prefixID := uuid.NewSHA1(uuid.NameSpaceURL, []byte(prefix))
|
prefixID := uuid.NewSHA1(uuid.NameSpaceURL, []byte(prefix))
|
||||||
|
|
||||||
// Withdraw all routes for this prefix from this peer
|
// Withdraw all routes for this prefix from this peer
|
||||||
count := h.rt.WithdrawRoutesByPrefixAndPeer(prefixID, peerASN)
|
h.rt.WithdrawRoutesByPrefixAndPeer(prefixID, peerASN)
|
||||||
|
|
||||||
h.logger.Debug("Withdrew routes from routing table",
|
|
||||||
"prefix", prefix,
|
|
||||||
"peer_asn", peerASN,
|
|
||||||
"routes_removed", count,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,9 @@ package routingtable
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -37,15 +39,23 @@ type RoutingTable struct {
|
|||||||
byPrefix map[uuid.UUID]map[RouteKey]*Route // Routes indexed by prefix ID
|
byPrefix map[uuid.UUID]map[RouteKey]*Route // Routes indexed by prefix ID
|
||||||
byOriginASN map[uuid.UUID]map[RouteKey]*Route // Routes indexed by origin ASN ID
|
byOriginASN map[uuid.UUID]map[RouteKey]*Route // Routes indexed by origin ASN ID
|
||||||
byPeerASN map[int]map[RouteKey]*Route // Routes indexed by peer ASN
|
byPeerASN map[int]map[RouteKey]*Route // Routes indexed by peer ASN
|
||||||
|
|
||||||
|
// Metrics tracking
|
||||||
|
ipv4Routes int
|
||||||
|
ipv6Routes int
|
||||||
|
ipv4Updates uint64 // Updates counter for rate calculation
|
||||||
|
ipv6Updates uint64 // Updates counter for rate calculation
|
||||||
|
lastMetricsReset time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new empty routing table
|
// New creates a new empty routing table
|
||||||
func New() *RoutingTable {
|
func New() *RoutingTable {
|
||||||
return &RoutingTable{
|
return &RoutingTable{
|
||||||
routes: make(map[RouteKey]*Route),
|
routes: make(map[RouteKey]*Route),
|
||||||
byPrefix: make(map[uuid.UUID]map[RouteKey]*Route),
|
byPrefix: make(map[uuid.UUID]map[RouteKey]*Route),
|
||||||
byOriginASN: make(map[uuid.UUID]map[RouteKey]*Route),
|
byOriginASN: make(map[uuid.UUID]map[RouteKey]*Route),
|
||||||
byPeerASN: make(map[int]map[RouteKey]*Route),
|
byPeerASN: make(map[int]map[RouteKey]*Route),
|
||||||
|
lastMetricsReset: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +73,12 @@ func (rt *RoutingTable) AddRoute(route *Route) {
|
|||||||
// If route already exists, remove it from indexes first
|
// If route already exists, remove it from indexes first
|
||||||
if existingRoute, exists := rt.routes[key]; exists {
|
if existingRoute, exists := rt.routes[key]; exists {
|
||||||
rt.removeFromIndexes(key, existingRoute)
|
rt.removeFromIndexes(key, existingRoute)
|
||||||
|
// Decrement counter for existing route
|
||||||
|
if isIPv6(existingRoute.Prefix) {
|
||||||
|
rt.ipv6Routes--
|
||||||
|
} else {
|
||||||
|
rt.ipv4Routes--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to main map
|
// Add to main map
|
||||||
@ -70,6 +86,15 @@ func (rt *RoutingTable) AddRoute(route *Route) {
|
|||||||
|
|
||||||
// Update indexes
|
// Update indexes
|
||||||
rt.addToIndexes(key, route)
|
rt.addToIndexes(key, route)
|
||||||
|
|
||||||
|
// Update metrics
|
||||||
|
if isIPv6(route.Prefix) {
|
||||||
|
rt.ipv6Routes++
|
||||||
|
atomic.AddUint64(&rt.ipv6Updates, 1)
|
||||||
|
} else {
|
||||||
|
rt.ipv4Routes++
|
||||||
|
atomic.AddUint64(&rt.ipv4Updates, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveRoute removes a route from the routing table
|
// RemoveRoute removes a route from the routing table
|
||||||
@ -94,6 +119,15 @@ func (rt *RoutingTable) RemoveRoute(prefixID, originASNID uuid.UUID, peerASN int
|
|||||||
// Remove from main map
|
// Remove from main map
|
||||||
delete(rt.routes, key)
|
delete(rt.routes, key)
|
||||||
|
|
||||||
|
// Update metrics
|
||||||
|
if isIPv6(route.Prefix) {
|
||||||
|
rt.ipv6Routes--
|
||||||
|
atomic.AddUint64(&rt.ipv6Updates, 1)
|
||||||
|
} else {
|
||||||
|
rt.ipv4Routes--
|
||||||
|
atomic.AddUint64(&rt.ipv4Updates, 1)
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,25 +136,38 @@ func (rt *RoutingTable) WithdrawRoutesByPrefixAndPeer(prefixID uuid.UUID, peerAS
|
|||||||
rt.mu.Lock()
|
rt.mu.Lock()
|
||||||
defer rt.mu.Unlock()
|
defer rt.mu.Unlock()
|
||||||
|
|
||||||
count := 0
|
prefixRoutes, exists := rt.byPrefix[prefixID]
|
||||||
|
if !exists {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// Find all routes for this prefix
|
// Collect keys to delete (can't delete while iterating)
|
||||||
if prefixRoutes, exists := rt.byPrefix[prefixID]; exists {
|
var keysToDelete []RouteKey
|
||||||
// Collect keys to delete (can't delete while iterating)
|
for key, route := range prefixRoutes {
|
||||||
var keysToDelete []RouteKey
|
if route.PeerASN == peerASN {
|
||||||
for key, route := range prefixRoutes {
|
keysToDelete = append(keysToDelete, key)
|
||||||
if route.PeerASN == peerASN {
|
}
|
||||||
keysToDelete = append(keysToDelete, key)
|
}
|
||||||
}
|
|
||||||
|
// Delete the routes
|
||||||
|
count := 0
|
||||||
|
for _, key := range keysToDelete {
|
||||||
|
route, exists := rt.routes[key]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the routes
|
rt.removeFromIndexes(key, route)
|
||||||
for _, key := range keysToDelete {
|
delete(rt.routes, key)
|
||||||
if route, exists := rt.routes[key]; exists {
|
count++
|
||||||
rt.removeFromIndexes(key, route)
|
|
||||||
delete(rt.routes, key)
|
// Update metrics
|
||||||
count++
|
if isIPv6(route.Prefix) {
|
||||||
}
|
rt.ipv6Routes--
|
||||||
|
atomic.AddUint64(&rt.ipv6Updates, 1)
|
||||||
|
} else {
|
||||||
|
rt.ipv4Routes--
|
||||||
|
atomic.AddUint64(&rt.ipv4Updates, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,6 +281,47 @@ func (rt *RoutingTable) Stats() map[string]int {
|
|||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DetailedStats contains detailed routing table statistics
|
||||||
|
type DetailedStats struct {
|
||||||
|
IPv4Routes int
|
||||||
|
IPv6Routes int
|
||||||
|
IPv4UpdatesRate float64
|
||||||
|
IPv6UpdatesRate float64
|
||||||
|
TotalRoutes int
|
||||||
|
UniquePrefixes int
|
||||||
|
UniqueOrigins int
|
||||||
|
UniquePeers int
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDetailedStats returns detailed statistics including IPv4/IPv6 breakdown and update rates
|
||||||
|
func (rt *RoutingTable) GetDetailedStats() DetailedStats {
|
||||||
|
rt.mu.Lock()
|
||||||
|
defer rt.mu.Unlock()
|
||||||
|
|
||||||
|
// Calculate update rates
|
||||||
|
elapsed := time.Since(rt.lastMetricsReset).Seconds()
|
||||||
|
ipv4Updates := atomic.LoadUint64(&rt.ipv4Updates)
|
||||||
|
ipv6Updates := atomic.LoadUint64(&rt.ipv6Updates)
|
||||||
|
|
||||||
|
stats := DetailedStats{
|
||||||
|
IPv4Routes: rt.ipv4Routes,
|
||||||
|
IPv6Routes: rt.ipv6Routes,
|
||||||
|
IPv4UpdatesRate: float64(ipv4Updates) / elapsed,
|
||||||
|
IPv6UpdatesRate: float64(ipv6Updates) / elapsed,
|
||||||
|
TotalRoutes: len(rt.routes),
|
||||||
|
UniquePrefixes: len(rt.byPrefix),
|
||||||
|
UniqueOrigins: len(rt.byOriginASN),
|
||||||
|
UniquePeers: len(rt.byPeerASN),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset counters for next period
|
||||||
|
atomic.StoreUint64(&rt.ipv4Updates, 0)
|
||||||
|
atomic.StoreUint64(&rt.ipv6Updates, 0)
|
||||||
|
rt.lastMetricsReset = time.Now()
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
// Clear removes all routes from the routing table
|
// Clear removes all routes from the routing table
|
||||||
func (rt *RoutingTable) Clear() {
|
func (rt *RoutingTable) Clear() {
|
||||||
rt.mu.Lock()
|
rt.mu.Lock()
|
||||||
@ -243,6 +331,11 @@ func (rt *RoutingTable) Clear() {
|
|||||||
rt.byPrefix = make(map[uuid.UUID]map[RouteKey]*Route)
|
rt.byPrefix = make(map[uuid.UUID]map[RouteKey]*Route)
|
||||||
rt.byOriginASN = make(map[uuid.UUID]map[RouteKey]*Route)
|
rt.byOriginASN = make(map[uuid.UUID]map[RouteKey]*Route)
|
||||||
rt.byPeerASN = make(map[int]map[RouteKey]*Route)
|
rt.byPeerASN = make(map[int]map[RouteKey]*Route)
|
||||||
|
rt.ipv4Routes = 0
|
||||||
|
rt.ipv6Routes = 0
|
||||||
|
atomic.StoreUint64(&rt.ipv4Updates, 0)
|
||||||
|
atomic.StoreUint64(&rt.ipv6Updates, 0)
|
||||||
|
rt.lastMetricsReset = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods for index management
|
// Helper methods for index management
|
||||||
@ -297,3 +390,8 @@ func (rt *RoutingTable) removeFromIndexes(key RouteKey, route *Route) {
|
|||||||
func (k RouteKey) String() string {
|
func (k RouteKey) String() string {
|
||||||
return fmt.Sprintf("%s/%s/%d", k.PrefixID, k.OriginASNID, k.PeerASN)
|
return fmt.Sprintf("%s/%s/%d", k.PrefixID, k.OriginASNID, k.PeerASN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isIPv6 returns true if the prefix is an IPv6 address
|
||||||
|
func isIPv6(prefix string) bool {
|
||||||
|
return strings.Contains(prefix, ":")
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user