Compare commits
1
Commits
prod
..
555f01920d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
555f01920d |
@@ -314,9 +314,7 @@ the routing table, and everything else from the database.
|
||||
3. **Message Processing Pipeline**
|
||||
|
||||
```
|
||||
RIS Live Stream → JSON Parser → Message Dispatcher → Handler Queues
|
||||
├→ Batch Writers → SQLite
|
||||
└→ PrefixHandler → Routing Table in Memory → SQLite, every second
|
||||
RIS Live Stream → JSON Parser → Message Dispatcher → Handler Queues → Batch Writers → SQLite
|
||||
```
|
||||
|
||||
- Streamer connects to `ris-live.ripe.net` via HTTP
|
||||
@@ -338,11 +336,11 @@ the routing table, and everything else from the database.
|
||||
AS path is empty or holds AS 0 anywhere.
|
||||
- A peer whose BGP session goes down leaves every prefix.
|
||||
|
||||
Every second it writes the changed prefix and origin AS pairs to the
|
||||
`routes` table. A write takes at most 20,000 pairs, and the rest follow
|
||||
in the next writes. It also stores each new prefix with the time it was
|
||||
first seen. Peers are known by their IP address. A peer IP can have
|
||||
sessions on two collectors. A session going down on one then also
|
||||
Every second it writes the prefix and origin AS pairs that changed to
|
||||
the `routes` table. A write takes at most 20,000 pairs, and the rest
|
||||
follow in the next writes. It also stores each new prefix with the time
|
||||
it was first seen. Peers are known by their IP address. A peer IP can
|
||||
have sessions on two collectors. A session going down on one then also
|
||||
removes the routes the other still carries, until they are announced
|
||||
again.
|
||||
|
||||
@@ -520,12 +518,12 @@ What happens at each limit:
|
||||
at ten times the batch size:
|
||||
- 300,000 updates for the AS handler (`asnBatchMaxSize`, about 10 MiB);
|
||||
- 100,000 for the peer handler (`peerBatchMaxSize`, about 10 MiB);
|
||||
- 250,000 for the prefix handler (`prefixBatchMaxSize`, about 15 MiB).
|
||||
- 250,000 for the prefix handler (`prefixBatchMaxSize`, about 60 MiB).
|
||||
|
||||
At that size the handler takes nothing more off its queue until the write
|
||||
ends. The queue then fills, and the streamer drops messages as above. With
|
||||
the batches being written, the three handlers' batches take at most about
|
||||
0.1 GiB of the Go heap.
|
||||
0.25 GiB of the Go heap.
|
||||
|
||||
With `DEBUG=routewatch` the daemon logs a `System stats` line every 60 seconds
|
||||
with the goroutine count and Go memory figures.
|
||||
@@ -564,8 +562,8 @@ What the [upaas](https://git.eeqj.de/sneak/upaas) app needs:
|
||||
- A lost withdrawal or peer down leaves its routes in the `routes` table,
|
||||
and the next start reads them back. They stay until the peer sends another
|
||||
update for the prefix, or its session goes down.
|
||||
- When the 8 seconds run out the database is not closed. When the database
|
||||
next opens, SQLite rolls back any write the exit cut off.
|
||||
- When the 8 seconds run out the database is not closed. SQLite rolls back a
|
||||
write the exit cut off when the database next opens.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -96,6 +96,12 @@ const (
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
// ErrInvalidIP says that an IP address is malformed. The server's IP
|
||||
// lookup returns it.
|
||||
ErrInvalidIP = errors.New("invalid IP address")
|
||||
// ErrNoRoute says that the routing table has no route for an address, a
|
||||
// prefix or an AS. The server's lookups return it.
|
||||
ErrNoRoute = errors.New("no route found")
|
||||
// ErrNoStaleASN is returned when no ASN needs WHOIS refresh
|
||||
ErrNoStaleASN = errors.New("no stale ASN found")
|
||||
// ErrCheckpointIncomplete is returned by Checkpoint when it left part of
|
||||
|
||||
@@ -66,6 +66,58 @@ type ASNPeering struct {
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
// LiveRoute represents a route in the live routing table. JSON leaves out an
|
||||
// empty AS path, next hop or time: the routing table in memory keeps them only
|
||||
// for the two newest routes of each prefix and origin AS.
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type LiveRoute struct {
|
||||
Prefix string `json:"prefix"`
|
||||
MaskLength int `json:"mask_length"`
|
||||
IPVersion int `json:"ip_version"`
|
||||
OriginASN int `json:"origin_asn"`
|
||||
PeerIP string `json:"peer_ip"`
|
||||
ASPath []int `json:"as_path,omitempty"`
|
||||
NextHop string `json:"next_hop,omitempty"`
|
||||
LastUpdated time.Time `json:"last_updated,omitzero"`
|
||||
}
|
||||
|
||||
// PrefixDistribution represents the distribution of prefixes by mask length
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type PrefixDistribution struct {
|
||||
MaskLength int `json:"mask_length"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// IPInfo represents comprehensive IP information for the /ip endpoint
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type IPInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Netblock string `json:"netblock"`
|
||||
MaskLength int `json:"mask_length"`
|
||||
IPVersion int `json:"ip_version"`
|
||||
NumPeers int `json:"num_peers"`
|
||||
// AS information
|
||||
ASN int `json:"asn"`
|
||||
ASName string `json:"as_name,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
OrgName string `json:"org_name,omitempty"`
|
||||
OrgID string `json:"org_id,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
CountryCode string `json:"country_code,omitempty"`
|
||||
AbuseEmail string `json:"abuse_email,omitempty"`
|
||||
RIR string `json:"rir,omitempty"`
|
||||
// Timestamps. LastSeen is zero, and left out of JSON, when the prefix
|
||||
// keeps no route.
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
LastSeen time.Time `json:"last_seen,omitzero"`
|
||||
// Indicates if WHOIS data needs refresh (not serialized)
|
||||
NeedsWHOISRefresh bool `json:"-"`
|
||||
}
|
||||
|
||||
// SeenTimes is the earliest and the latest time one batch of updates saw an AS,
|
||||
// a peer or a peering. A row the batch adds is stored first seen at First and
|
||||
// last seen at Last. A row already stored takes First as its first_seen only
|
||||
|
||||
@@ -26,7 +26,7 @@ const (
|
||||
prefixBatchSize = 25000
|
||||
|
||||
// prefixBatchMaxSize is the most prefix updates the batch holds while the
|
||||
// flush goroutine writes the previous one: ten batches, about 15 MiB
|
||||
// flush goroutine writes the previous one: ten batches, about 60 MiB
|
||||
// (README "Memory"). At this size HandleMessage waits until the flush
|
||||
// goroutine takes the batch.
|
||||
prefixBatchMaxSize = 250000
|
||||
@@ -65,7 +65,7 @@ type PrefixHandler struct {
|
||||
// mu, so HandleMessage waits for a database write only while batch is at
|
||||
// prefixBatchMaxSize.
|
||||
mu sync.Mutex
|
||||
batch []seenPrefix
|
||||
batch []prefixUpdate
|
||||
// writing is how many updates the flush goroutine took and has not
|
||||
// finished writing.
|
||||
writing int
|
||||
@@ -82,15 +82,6 @@ type PrefixHandler struct {
|
||||
done chan struct{} // closed when the flush goroutine returns
|
||||
}
|
||||
|
||||
// seenPrefix is what the batch keeps of an update: the prefix it names and its
|
||||
// time. A flush needs only these to store each new prefix's first-seen time.
|
||||
type seenPrefix struct {
|
||||
prefix string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// prefixUpdate is one prefix of an update message, as applyToTable applies it
|
||||
// to the routing table.
|
||||
type prefixUpdate struct {
|
||||
prefix string
|
||||
originASN int
|
||||
@@ -133,7 +124,7 @@ func newPrefixHandler(
|
||||
db: db,
|
||||
logger: logger,
|
||||
table: table,
|
||||
batch: make([]seenPrefix, 0, prefixBatchSize),
|
||||
batch: make([]prefixUpdate, 0, prefixBatchSize),
|
||||
batchFullCh: make(chan struct{}, 1),
|
||||
stopCh: make(chan context.Context, 1),
|
||||
done: make(chan struct{}),
|
||||
@@ -207,7 +198,7 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
messageType: "withdrawal",
|
||||
timestamp: timestamp,
|
||||
}
|
||||
h.batch = append(h.batch, seenPrefix{update.prefix, update.timestamp})
|
||||
h.batch = append(h.batch, update)
|
||||
h.applyToTable(update)
|
||||
|
||||
// Record withdrawal in metrics
|
||||
@@ -227,7 +218,7 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
path: path,
|
||||
endsInSet: len(msg.Path.Set) > 0,
|
||||
}
|
||||
h.batch = append(h.batch, seenPrefix{update.prefix, update.timestamp})
|
||||
h.batch = append(h.batch, update)
|
||||
h.applyToTable(update)
|
||||
|
||||
// Record announcement in metrics
|
||||
@@ -286,7 +277,7 @@ func (h *PrefixHandler) Stop(ctx context.Context) {
|
||||
// takeBatch returns the pending updates and leaves an empty batch in their
|
||||
// place for HandleMessage to fill. The updates count in writing until
|
||||
// flushBatch has written them.
|
||||
func (h *PrefixHandler) takeBatch() []seenPrefix {
|
||||
func (h *PrefixHandler) takeBatch() []prefixUpdate {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
@@ -296,7 +287,7 @@ func (h *PrefixHandler) takeBatch() []seenPrefix {
|
||||
|
||||
batch := h.batch
|
||||
h.writing = len(batch)
|
||||
h.batch = make([]seenPrefix, 0, prefixBatchSize)
|
||||
h.batch = make([]prefixUpdate, 0, prefixBatchSize)
|
||||
h.batchTaken.Broadcast()
|
||||
|
||||
return batch
|
||||
|
||||
+94
-94
@@ -83,7 +83,7 @@ const (
|
||||
maxASSearchResults = 50
|
||||
)
|
||||
|
||||
// IP versions as LiveRoute.IPVersion records them.
|
||||
// IP versions as database.LiveRoute.IPVersion records them.
|
||||
const (
|
||||
ipVersionV4 = 4
|
||||
ipVersionV6 = 6
|
||||
@@ -233,34 +233,34 @@ type WHOISStatsInfo struct {
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type Stats struct {
|
||||
Uptime string `json:"uptime"`
|
||||
TotalMessages uint64 `json:"total_messages"`
|
||||
TotalBytes uint64 `json:"total_bytes"`
|
||||
TotalWireBytes uint64 `json:"total_wire_bytes"`
|
||||
MessagesPerSec float64 `json:"messages_per_sec"`
|
||||
MbitsPerSec float64 `json:"mbits_per_sec"`
|
||||
WireMbitsPerSec float64 `json:"wire_mbits_per_sec"`
|
||||
Connected bool `json:"connected"`
|
||||
GoVersion string `json:"go_version"`
|
||||
Goroutines int `json:"goroutines"`
|
||||
MemoryUsage string `json:"memory_usage"`
|
||||
ASNs int `json:"asns"`
|
||||
Prefixes int `json:"prefixes"`
|
||||
IPv4Prefixes int `json:"ipv4_prefixes"`
|
||||
IPv6Prefixes int `json:"ipv6_prefixes"`
|
||||
Peerings int `json:"peerings"`
|
||||
Peers int `json:"peers"`
|
||||
DatabaseSizeBytes int64 `json:"database_size_bytes"`
|
||||
LiveRoutes int `json:"live_routes"`
|
||||
IPv4Routes int `json:"ipv4_routes"`
|
||||
IPv6Routes int `json:"ipv6_routes"`
|
||||
OldestRoute *time.Time `json:"oldest_route,omitempty"`
|
||||
NewestRoute *time.Time `json:"newest_route,omitempty"`
|
||||
IPv4UpdatesPerSec float64 `json:"ipv4_updates_per_sec"`
|
||||
IPv6UpdatesPerSec float64 `json:"ipv6_updates_per_sec"`
|
||||
IPv4PrefixDistribution []PrefixDistribution `json:"ipv4_prefix_distribution"`
|
||||
IPv6PrefixDistribution []PrefixDistribution `json:"ipv6_prefix_distribution"`
|
||||
WHOISStats *WHOISStatsInfo `json:"whois_stats,omitempty"`
|
||||
Uptime string `json:"uptime"`
|
||||
TotalMessages uint64 `json:"total_messages"`
|
||||
TotalBytes uint64 `json:"total_bytes"`
|
||||
TotalWireBytes uint64 `json:"total_wire_bytes"`
|
||||
MessagesPerSec float64 `json:"messages_per_sec"`
|
||||
MbitsPerSec float64 `json:"mbits_per_sec"`
|
||||
WireMbitsPerSec float64 `json:"wire_mbits_per_sec"`
|
||||
Connected bool `json:"connected"`
|
||||
GoVersion string `json:"go_version"`
|
||||
Goroutines int `json:"goroutines"`
|
||||
MemoryUsage string `json:"memory_usage"`
|
||||
ASNs int `json:"asns"`
|
||||
Prefixes int `json:"prefixes"`
|
||||
IPv4Prefixes int `json:"ipv4_prefixes"`
|
||||
IPv6Prefixes int `json:"ipv6_prefixes"`
|
||||
Peerings int `json:"peerings"`
|
||||
Peers int `json:"peers"`
|
||||
DatabaseSizeBytes int64 `json:"database_size_bytes"`
|
||||
LiveRoutes int `json:"live_routes"`
|
||||
IPv4Routes int `json:"ipv4_routes"`
|
||||
IPv6Routes int `json:"ipv6_routes"`
|
||||
OldestRoute *time.Time `json:"oldest_route,omitempty"`
|
||||
NewestRoute *time.Time `json:"newest_route,omitempty"`
|
||||
IPv4UpdatesPerSec float64 `json:"ipv4_updates_per_sec"`
|
||||
IPv6UpdatesPerSec float64 `json:"ipv6_updates_per_sec"`
|
||||
IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
|
||||
IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
|
||||
WHOISStats *WHOISStatsInfo `json:"whois_stats,omitempty"`
|
||||
|
||||
// OriginsWithoutKeptRoute counts the prefix and origin AS pairs that peers
|
||||
// carry but whose kept routes were all withdrawn.
|
||||
@@ -394,12 +394,12 @@ func (s *Server) statusStats(
|
||||
// prefixDistribution lists the mask lengths that have at least one prefix, in
|
||||
// ascending order, with their prefix counts. countsByLength[n] is the number
|
||||
// of prefixes of mask length n.
|
||||
func prefixDistribution(countsByLength []int) []PrefixDistribution {
|
||||
var distribution []PrefixDistribution
|
||||
func prefixDistribution(countsByLength []int) []database.PrefixDistribution {
|
||||
var distribution []database.PrefixDistribution
|
||||
|
||||
for maskLength, count := range countsByLength {
|
||||
if count > 0 {
|
||||
distribution = append(distribution, PrefixDistribution{
|
||||
distribution = append(distribution, database.PrefixDistribution{
|
||||
MaskLength: maskLength, Count: count,
|
||||
})
|
||||
}
|
||||
@@ -508,39 +508,39 @@ type StreamStats struct {
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type StatsResponse struct {
|
||||
Uptime string `json:"uptime"`
|
||||
TotalMessages uint64 `json:"total_messages"`
|
||||
TotalBytes uint64 `json:"total_bytes"`
|
||||
TotalWireBytes uint64 `json:"total_wire_bytes"`
|
||||
MessagesPerSec float64 `json:"messages_per_sec"`
|
||||
MbitsPerSec float64 `json:"mbits_per_sec"`
|
||||
WireMbitsPerSec float64 `json:"wire_mbits_per_sec"`
|
||||
Connected bool `json:"connected"`
|
||||
ConnectionDuration string `json:"connection_duration"`
|
||||
ReconnectCount uint64 `json:"reconnect_count"`
|
||||
GoVersion string `json:"go_version"`
|
||||
Goroutines int `json:"goroutines"`
|
||||
MemoryUsage string `json:"memory_usage"`
|
||||
GC GCStats `json:"gc"`
|
||||
Stream StreamStats `json:"stream"`
|
||||
ASNs int `json:"asns"`
|
||||
Prefixes int `json:"prefixes"`
|
||||
IPv4Prefixes int `json:"ipv4_prefixes"`
|
||||
IPv6Prefixes int `json:"ipv6_prefixes"`
|
||||
Peerings int `json:"peerings"`
|
||||
Peers int `json:"peers"`
|
||||
DatabaseSizeBytes int64 `json:"database_size_bytes"`
|
||||
LiveRoutes int `json:"live_routes"`
|
||||
IPv4Routes int `json:"ipv4_routes"`
|
||||
IPv6Routes int `json:"ipv6_routes"`
|
||||
OldestRoute *time.Time `json:"oldest_route,omitempty"`
|
||||
NewestRoute *time.Time `json:"newest_route,omitempty"`
|
||||
IPv4UpdatesPerSec float64 `json:"ipv4_updates_per_sec"`
|
||||
IPv6UpdatesPerSec float64 `json:"ipv6_updates_per_sec"`
|
||||
HandlerStats []HandlerStatsInfo `json:"handler_stats"`
|
||||
IPv4PrefixDistribution []PrefixDistribution `json:"ipv4_prefix_distribution"`
|
||||
IPv6PrefixDistribution []PrefixDistribution `json:"ipv6_prefix_distribution"`
|
||||
WHOISStats *WHOISStatsInfo `json:"whois_stats,omitempty"`
|
||||
Uptime string `json:"uptime"`
|
||||
TotalMessages uint64 `json:"total_messages"`
|
||||
TotalBytes uint64 `json:"total_bytes"`
|
||||
TotalWireBytes uint64 `json:"total_wire_bytes"`
|
||||
MessagesPerSec float64 `json:"messages_per_sec"`
|
||||
MbitsPerSec float64 `json:"mbits_per_sec"`
|
||||
WireMbitsPerSec float64 `json:"wire_mbits_per_sec"`
|
||||
Connected bool `json:"connected"`
|
||||
ConnectionDuration string `json:"connection_duration"`
|
||||
ReconnectCount uint64 `json:"reconnect_count"`
|
||||
GoVersion string `json:"go_version"`
|
||||
Goroutines int `json:"goroutines"`
|
||||
MemoryUsage string `json:"memory_usage"`
|
||||
GC GCStats `json:"gc"`
|
||||
Stream StreamStats `json:"stream"`
|
||||
ASNs int `json:"asns"`
|
||||
Prefixes int `json:"prefixes"`
|
||||
IPv4Prefixes int `json:"ipv4_prefixes"`
|
||||
IPv6Prefixes int `json:"ipv6_prefixes"`
|
||||
Peerings int `json:"peerings"`
|
||||
Peers int `json:"peers"`
|
||||
DatabaseSizeBytes int64 `json:"database_size_bytes"`
|
||||
LiveRoutes int `json:"live_routes"`
|
||||
IPv4Routes int `json:"ipv4_routes"`
|
||||
IPv6Routes int `json:"ipv6_routes"`
|
||||
OldestRoute *time.Time `json:"oldest_route,omitempty"`
|
||||
NewestRoute *time.Time `json:"newest_route,omitempty"`
|
||||
IPv4UpdatesPerSec float64 `json:"ipv4_updates_per_sec"`
|
||||
IPv6UpdatesPerSec float64 `json:"ipv6_updates_per_sec"`
|
||||
HandlerStats []HandlerStatsInfo `json:"handler_stats"`
|
||||
IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
|
||||
IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
|
||||
WHOISStats *WHOISStatsInfo `json:"whois_stats,omitempty"`
|
||||
|
||||
OriginsWithoutKeptRoute int `json:"origins_without_kept_route"`
|
||||
}
|
||||
@@ -753,7 +753,7 @@ type IPLookupResponse struct {
|
||||
// fields. IP hides the IPInfo field of the same name, so that an address with
|
||||
// no route has it too.
|
||||
type IPLookupResult struct {
|
||||
*IPInfo
|
||||
*database.IPInfo
|
||||
|
||||
IP string `json:"ip"`
|
||||
PTR []string `json:"ptr,omitempty"`
|
||||
@@ -813,10 +813,10 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Is(err, ErrNoRoute):
|
||||
case errors.Is(err, database.ErrNoRoute):
|
||||
// The address still gets a result, without route fields.
|
||||
ipInfo = nil
|
||||
case errors.Is(err, ErrInvalidIP):
|
||||
case errors.Is(err, database.ErrInvalidIP):
|
||||
response.Errors = append(response.Errors, ip+": "+err.Error())
|
||||
|
||||
continue
|
||||
@@ -860,15 +860,15 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||
// route. The database gives the prefix's first-seen time and the AS's WHOIS
|
||||
// details. It returns ErrNoRoute when no prefix holds the address, and
|
||||
// ErrInvalidIP when ip does not parse.
|
||||
func (s *Server) ipInfo(ctx context.Context, ip string) (*IPInfo, error) {
|
||||
func (s *Server) ipInfo(ctx context.Context, ip string) (*database.IPInfo, error) {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrInvalidIP, ip)
|
||||
return nil, fmt.Errorf("%w: %s", database.ErrInvalidIP, ip)
|
||||
}
|
||||
|
||||
summary, found := s.table.Lookup(addr)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%w for IP %s", ErrNoRoute, ip)
|
||||
return nil, fmt.Errorf("%w for IP %s", database.ErrNoRoute, ip)
|
||||
}
|
||||
|
||||
firstSeen, err := s.db.GetPrefixFirstSeen(ctx, summary.Prefix)
|
||||
@@ -881,7 +881,7 @@ func (s *Server) ipInfo(ctx context.Context, ip string) (*IPInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := &IPInfo{
|
||||
info := &database.IPInfo{
|
||||
IP: ip,
|
||||
Netblock: summary.Prefix.String(),
|
||||
MaskLength: summary.Prefix.Bits(),
|
||||
@@ -1165,7 +1165,7 @@ func (s *Server) handleASDetailJSON() http.HandlerFunc {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoRoute) {
|
||||
if errors.Is(err, database.ErrNoRoute) {
|
||||
s.writeJSONError(w, http.StatusNotFound, err.Error())
|
||||
} else {
|
||||
s.logger.Error("Failed to get AS details", "asn", asn, "error", err)
|
||||
@@ -1201,9 +1201,9 @@ func (s *Server) handleASDetailJSON() http.HandlerFunc {
|
||||
// routing table all lack.
|
||||
func (s *Server) asDetails(
|
||||
ctx context.Context, asn int,
|
||||
) (*database.ASN, []LiveRoute, error) {
|
||||
) (*database.ASN, []database.LiveRoute, error) {
|
||||
if asn == ristypes.NoOrigin {
|
||||
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
|
||||
return nil, nil, fmt.Errorf("%w: AS%d", database.ErrNoRoute, asn)
|
||||
}
|
||||
|
||||
asInfo, known, err := s.db.GetAS(ctx, asn)
|
||||
@@ -1213,13 +1213,13 @@ func (s *Server) asDetails(
|
||||
|
||||
originated := s.table.OriginatedPrefixes(asn)
|
||||
if !known && len(originated) == 0 {
|
||||
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
|
||||
return nil, nil, fmt.Errorf("%w: AS%d", database.ErrNoRoute, asn)
|
||||
}
|
||||
|
||||
prefixes := make([]LiveRoute, 0, len(originated))
|
||||
prefixes := make([]database.LiveRoute, 0, len(originated))
|
||||
|
||||
for _, p := range originated {
|
||||
route := LiveRoute{
|
||||
route := database.LiveRoute{
|
||||
Prefix: p.Prefix.String(),
|
||||
MaskLength: p.Prefix.Bits(),
|
||||
IPVersion: ipVersionOf(p.Prefix),
|
||||
@@ -1239,9 +1239,9 @@ func (s *Server) asDetails(
|
||||
// splitRoutesByIPVersion groups routes into the IPv4 and the IPv6 ones. Neither
|
||||
// group is nil, so that a JSON answer holds [] for an empty one and not null.
|
||||
func splitRoutesByIPVersion(
|
||||
routes []LiveRoute,
|
||||
) ([]LiveRoute, []LiveRoute) {
|
||||
ipv4Routes, ipv6Routes := []LiveRoute{}, []LiveRoute{}
|
||||
routes []database.LiveRoute,
|
||||
) ([]database.LiveRoute, []database.LiveRoute) {
|
||||
ipv4Routes, ipv6Routes := []database.LiveRoute{}, []database.LiveRoute{}
|
||||
|
||||
for _, route := range routes {
|
||||
if route.IPVersion == ipVersionV4 {
|
||||
@@ -1256,8 +1256,8 @@ func splitRoutesByIPVersion(
|
||||
|
||||
// sortRoutesByMaskLength sorts routes by mask length, shortest first, and
|
||||
// routes with the same mask length by network address.
|
||||
func sortRoutesByMaskLength(routes []LiveRoute) {
|
||||
slices.SortFunc(routes, func(a, b LiveRoute) int {
|
||||
func sortRoutesByMaskLength(routes []database.LiveRoute) {
|
||||
slices.SortFunc(routes, func(a, b database.LiveRoute) int {
|
||||
prefixA, _ := netip.ParsePrefix(a.Prefix)
|
||||
prefixB, _ := netip.ParsePrefix(b.Prefix)
|
||||
|
||||
@@ -1294,7 +1294,7 @@ func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
|
||||
origins := s.table.Origins(parsed)
|
||||
if len(origins) == 0 {
|
||||
s.writeJSONError(w, http.StatusNotFound,
|
||||
ErrNoRoute.Error()+": "+prefix)
|
||||
database.ErrNoRoute.Error()+": "+prefix)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1302,7 +1302,7 @@ func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
|
||||
routes := prefixRoutes(parsed, origins)
|
||||
|
||||
// Group by origin AS
|
||||
originMap := make(map[int][]LiveRoute)
|
||||
originMap := make(map[int][]database.LiveRoute)
|
||||
for _, route := range routes {
|
||||
originMap[route.OriginASN] = append(originMap[route.OriginASN], route)
|
||||
}
|
||||
@@ -1392,7 +1392,7 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoRoute) {
|
||||
if errors.Is(err, database.ErrNoRoute) {
|
||||
http.Error(w, "AS not found", http.StatusNotFound)
|
||||
} else {
|
||||
s.logger.Error("Failed to get AS details", "error", err)
|
||||
@@ -1425,8 +1425,8 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
||||
// Prepare template data
|
||||
data := struct {
|
||||
ASN *database.ASN
|
||||
IPv4Prefixes []LiveRoute
|
||||
IPv6Prefixes []LiveRoute
|
||||
IPv4Prefixes []database.LiveRoute
|
||||
IPv6Prefixes []database.LiveRoute
|
||||
TotalCount int
|
||||
IPv4Count int
|
||||
IPv6Count int
|
||||
@@ -1585,11 +1585,11 @@ func (s *Server) originASNs(
|
||||
// them, since the routing table keeps only their addresses.
|
||||
func prefixRoutes(
|
||||
prefix netip.Prefix, origins []routetable.Origin,
|
||||
) []LiveRoute {
|
||||
var routes []LiveRoute
|
||||
) []database.LiveRoute {
|
||||
var routes []database.LiveRoute
|
||||
|
||||
for _, origin := range origins {
|
||||
base := LiveRoute{
|
||||
base := database.LiveRoute{
|
||||
Prefix: prefix.String(),
|
||||
MaskLength: prefix.Bits(),
|
||||
IPVersion: ipVersionOf(prefix),
|
||||
@@ -1630,14 +1630,14 @@ type ASPathEntry struct {
|
||||
|
||||
// EnhancedRoute is a live route with the handle of each AS in its path.
|
||||
type EnhancedRoute struct {
|
||||
LiveRoute
|
||||
database.LiveRoute
|
||||
|
||||
ASPathWithHandle []ASPathEntry
|
||||
}
|
||||
|
||||
// routesWithASPathHandles looks up the handle of each AS in the path of each
|
||||
// route.
|
||||
func routesWithASPathHandles(routes []LiveRoute) []EnhancedRoute {
|
||||
func routesWithASPathHandles(routes []database.LiveRoute) []EnhancedRoute {
|
||||
enhancedRoutes := make([]EnhancedRoute, len(routes))
|
||||
|
||||
for i, route := range routes {
|
||||
@@ -1736,7 +1736,7 @@ func (s *Server) prefixLengthHandler(ipVersion, maxMaskLength int) http.HandlerF
|
||||
// EnhancedPrefix is a prefix on the prefix length page, with the origin AS
|
||||
// that most peers carry it with, that AS's description, and its age.
|
||||
type EnhancedPrefix struct {
|
||||
LiveRoute
|
||||
database.LiveRoute
|
||||
|
||||
OriginASDescription string
|
||||
Age string
|
||||
|
||||
@@ -158,8 +158,8 @@ type tableStats struct {
|
||||
OldestRoute time.Time `json:"oldest_route"`
|
||||
NewestRoute time.Time `json:"newest_route"`
|
||||
|
||||
IPv4Distribution []PrefixDistribution `json:"ipv4_prefix_distribution"`
|
||||
IPv6Distribution []PrefixDistribution `json:"ipv6_prefix_distribution"`
|
||||
IPv4Distribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
|
||||
IPv6Distribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
|
||||
}
|
||||
|
||||
// TestStatsHandlersAnswerFromMemory checks that both stats handlers answer 200
|
||||
@@ -211,8 +211,8 @@ func TestStatsHandlersAnswerFromMemory(t *testing.T) {
|
||||
OriginsWithoutKeptRoute: 1,
|
||||
OldestRoute: testRouteTime(0),
|
||||
NewestRoute: testRouteTime(4),
|
||||
IPv4Distribution: []PrefixDistribution{{MaskLength: 24, Count: 2}},
|
||||
IPv6Distribution: []PrefixDistribution{{MaskLength: 32, Count: 1}},
|
||||
IPv4Distribution: []database.PrefixDistribution{{MaskLength: 24, Count: 2}},
|
||||
IPv6Distribution: []database.PrefixDistribution{{MaskLength: 32, Count: 1}},
|
||||
}
|
||||
|
||||
for name, handler := range handlers {
|
||||
@@ -1698,9 +1698,9 @@ func TestASDetailJSONAnswersBuiltInAS(t *testing.T) {
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
var body struct {
|
||||
Data struct {
|
||||
ASN database.ASN `json:"asn"`
|
||||
IPv4Prefixes []LiveRoute `json:"ipv4_prefixes"`
|
||||
IPv6Prefixes []LiveRoute `json:"ipv6_prefixes"`
|
||||
ASN database.ASN `json:"asn"`
|
||||
IPv4Prefixes []database.LiveRoute `json:"ipv4_prefixes"`
|
||||
IPv6Prefixes []database.LiveRoute `json:"ipv6_prefixes"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
@@ -1845,9 +1845,9 @@ func TestSortRoutesByMaskLength(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
routes := make([]LiveRoute, 0, len(tt.prefixes))
|
||||
routes := make([]database.LiveRoute, 0, len(tt.prefixes))
|
||||
for _, prefix := range tt.prefixes {
|
||||
routes = append(routes, LiveRoute{Prefix: prefix})
|
||||
routes = append(routes, database.LiveRoute{Prefix: prefix})
|
||||
}
|
||||
|
||||
sortRoutesByMaskLength(routes)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidIP says that an IP address is malformed. The IP lookup returns
|
||||
// it.
|
||||
ErrInvalidIP = errors.New("invalid IP address")
|
||||
// ErrNoRoute says that the routing table has no route for an address, a
|
||||
// prefix or an AS. The lookups return it.
|
||||
ErrNoRoute = errors.New("no route found")
|
||||
)
|
||||
|
||||
// LiveRoute represents a route in the live routing table. JSON leaves out an
|
||||
// empty AS path, next hop or time: the routing table in memory keeps them only
|
||||
// for the two newest routes of each prefix and origin AS.
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type LiveRoute struct {
|
||||
Prefix string `json:"prefix"`
|
||||
MaskLength int `json:"mask_length"`
|
||||
IPVersion int `json:"ip_version"`
|
||||
OriginASN int `json:"origin_asn"`
|
||||
PeerIP string `json:"peer_ip"`
|
||||
ASPath []int `json:"as_path,omitempty"`
|
||||
NextHop string `json:"next_hop,omitempty"`
|
||||
LastUpdated time.Time `json:"last_updated,omitzero"`
|
||||
}
|
||||
|
||||
// PrefixDistribution represents the distribution of prefixes by mask length
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type PrefixDistribution struct {
|
||||
MaskLength int `json:"mask_length"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// IPInfo represents comprehensive IP information for the /ip endpoint
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type IPInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Netblock string `json:"netblock"`
|
||||
MaskLength int `json:"mask_length"`
|
||||
IPVersion int `json:"ip_version"`
|
||||
NumPeers int `json:"num_peers"`
|
||||
// AS information
|
||||
ASN int `json:"asn"`
|
||||
ASName string `json:"as_name,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
OrgName string `json:"org_name,omitempty"`
|
||||
OrgID string `json:"org_id,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
CountryCode string `json:"country_code,omitempty"`
|
||||
AbuseEmail string `json:"abuse_email,omitempty"`
|
||||
RIR string `json:"rir,omitempty"`
|
||||
// Timestamps. LastSeen is zero, and left out of JSON, when the prefix
|
||||
// keeps no route.
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
LastSeen time.Time `json:"last_seen,omitzero"`
|
||||
// Indicates if WHOIS data needs refresh (not serialized)
|
||||
NeedsWHOISRefresh bool `json:"-"`
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||
"git.eeqj.de/sneak/routewatch/internal/server"
|
||||
"git.eeqj.de/sneak/routewatch/internal/templates"
|
||||
)
|
||||
|
||||
@@ -115,14 +114,14 @@ func TestASDetailPrefixListsFold(t *testing.T) {
|
||||
prefixV6 = "2001:db8::/32"
|
||||
)
|
||||
|
||||
ipv4 := []server.LiveRoute{{Prefix: prefixV4}}
|
||||
ipv6 := []server.LiveRoute{{Prefix: prefixV6}}
|
||||
ipv4 := []database.LiveRoute{{Prefix: prefixV4}}
|
||||
ipv6 := []database.LiveRoute{{Prefix: prefixV6}}
|
||||
|
||||
// The fields the AS page handler gives the template.
|
||||
data := struct {
|
||||
ASN *database.ASN
|
||||
IPv4Prefixes []server.LiveRoute
|
||||
IPv6Prefixes []server.LiveRoute
|
||||
IPv4Prefixes []database.LiveRoute
|
||||
IPv6Prefixes []database.LiveRoute
|
||||
TotalCount int
|
||||
IPv4Count int
|
||||
IPv6Count int
|
||||
|
||||
Reference in New Issue
Block a user