Keep a trailing AS set out of the origin and the peerings (closes #75)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
A path ending in an AS set, such as [64496, 64497, [64498, 64499]], was flattened into the sequence, so the route was stored as originated by the last set member and the peering handler recorded peerings the path does not show. ASPath now keeps the sequence and a trailing set of several members apart; Origin() gives the last AS of the sequence, or 0 (no known origin) for such a set. The handlers use it; peerings come from the sequence only. The pages show origin 0 as no known origin, and the IP lookup asks no WHOIS lookup for it. Empty paths are unchanged. Model: opus-5-5
This commit was merged in pull request #91.
This commit is contained in:
+44
-21
@@ -23,26 +23,43 @@ const (
|
||||
StateDown = "down"
|
||||
)
|
||||
|
||||
// ASPath represents a BGP AS path as a slice of AS numbers.
|
||||
// It handles JSON unmarshaling of both simple arrays and nested AS sets,
|
||||
// flattening any nested structures into a single sequence of AS numbers.
|
||||
type ASPath []int
|
||||
// ASPath is a BGP AS path as the feed writes it: an array of AS numbers in
|
||||
// which an AS set is an array of its members, such as [1, 2, [3, 4]].
|
||||
type ASPath struct {
|
||||
// Sequence is the path's AS numbers in order. A set of one member that
|
||||
// ends the path counts as that AS and is its last element. The members of
|
||||
// a set anywhere else are flattened into it.
|
||||
Sequence []int
|
||||
|
||||
// Set holds the members of a set of several members that ends the path.
|
||||
// Any of them may be the origin, and they are not neighbours of each
|
||||
// other or of the AS before them.
|
||||
Set []int
|
||||
}
|
||||
|
||||
// Origin returns the AS that originated the route: the last AS of the
|
||||
// sequence, or 0 (none) when the path ends in a set of several members or is
|
||||
// empty.
|
||||
func (p *ASPath) Origin() int {
|
||||
if len(p.Set) > 0 || len(p.Sequence) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return p.Sequence[len(p.Sequence)-1]
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for ASPath.
|
||||
// It handles both simple integer arrays [1, 2, 3] and nested AS sets
|
||||
// like [1, [2, 3], 4], flattening them into a single slice of integers.
|
||||
func (p *ASPath) UnmarshalJSON(data []byte) error {
|
||||
// First try to unmarshal as a simple array of integers
|
||||
var simple []int
|
||||
// Most paths have no AS set and decode as a plain array of integers.
|
||||
var sequence []int
|
||||
|
||||
err := json.Unmarshal(data, &simple)
|
||||
err := json.Unmarshal(data, &sequence)
|
||||
if err == nil {
|
||||
*p = ASPath(simple)
|
||||
*p = ASPath{Sequence: sequence}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If that fails, unmarshal as array of interfaces and flatten
|
||||
var raw []any
|
||||
|
||||
err = json.Unmarshal(data, &raw)
|
||||
@@ -50,24 +67,30 @@ func (p *ASPath) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Flatten the array
|
||||
result := make([]int, 0)
|
||||
path := ASPath{Sequence: make([]int, 0, len(raw))}
|
||||
|
||||
for _, item := range raw {
|
||||
for i, item := range raw {
|
||||
switch v := item.(type) {
|
||||
case float64:
|
||||
result = append(result, int(v))
|
||||
path.Sequence = append(path.Sequence, int(v))
|
||||
case []any:
|
||||
// Nested array - flatten it
|
||||
for _, nested := range v {
|
||||
if num, ok := nested.(float64); ok {
|
||||
result = append(result, int(num))
|
||||
set := make([]int, 0, len(v))
|
||||
|
||||
for _, member := range v {
|
||||
if num, ok := member.(float64); ok {
|
||||
set = append(set, int(num))
|
||||
}
|
||||
}
|
||||
|
||||
if i == len(raw)-1 && len(set) > 1 {
|
||||
path.Set = set
|
||||
} else {
|
||||
path.Sequence = append(path.Sequence, set...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*p = ASPath(result)
|
||||
*p = path
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -97,7 +120,7 @@ type RISMessage struct {
|
||||
RRC string `json:"rrc,omitempty"`
|
||||
MrtTime float64 `json:"mrt_time,omitempty"`
|
||||
SocketTime float64 `json:"socket_time,omitempty"`
|
||||
Path ASPath `json:"path,omitempty"`
|
||||
Path ASPath `json:"path,omitzero"`
|
||||
// Community and Raw are present in the feed but read by no handler.
|
||||
// They are the largest fields on a message that lives in up to four
|
||||
// handler queues, so json:"-" keeps them out of the decoded message
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
||||
@@ -58,7 +59,7 @@ func TestDecodeDropsCommunityAndRaw(t *testing.T) {
|
||||
t.Errorf("message %d: Raw decoded, want empty", messages)
|
||||
}
|
||||
|
||||
if len(msg.Path) > 0 {
|
||||
if len(msg.Path.Sequence) > 0 {
|
||||
withPath++
|
||||
}
|
||||
|
||||
@@ -82,3 +83,65 @@ func TestDecodeDropsCommunityAndRaw(t *testing.T) {
|
||||
withPath, withAnnouncements)
|
||||
}
|
||||
}
|
||||
|
||||
// TestASPathKeepsATrailingSetApart decodes AS paths as the feed writes them, an
|
||||
// AS set as an array inside the path. A set of several members that ends the
|
||||
// path is kept apart from the sequence, and the path has no origin, since any
|
||||
// member may be it. A set of one member counts as that AS, and a set anywhere
|
||||
// else is flattened into the sequence.
|
||||
func TestASPathKeepsATrailingSetApart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
wantSequence []int
|
||||
wantSet []int
|
||||
wantOrigin int
|
||||
}{
|
||||
{
|
||||
path: "[64496, 64497]",
|
||||
wantSequence: []int{64496, 64497},
|
||||
wantOrigin: 64497,
|
||||
},
|
||||
{
|
||||
path: "[64496, 64497, [64498, 64499]]",
|
||||
wantSequence: []int{64496, 64497},
|
||||
wantSet: []int{64498, 64499},
|
||||
},
|
||||
{
|
||||
path: "[64496, 64497, [64498]]",
|
||||
wantSequence: []int{64496, 64497, 64498},
|
||||
wantOrigin: 64498,
|
||||
},
|
||||
{
|
||||
path: "[64496, [64497, 64498], 64499]",
|
||||
wantSequence: []int{64496, 64497, 64498, 64499},
|
||||
wantOrigin: 64499,
|
||||
},
|
||||
{path: "[]"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var got ristypes.ASPath
|
||||
|
||||
err := json.Unmarshal([]byte(tt.path), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal %s: %v", tt.path, err)
|
||||
}
|
||||
|
||||
if !slices.Equal(got.Sequence, tt.wantSequence) ||
|
||||
!slices.Equal(got.Set, tt.wantSet) {
|
||||
t.Errorf("%s decoded to sequence %v and set %v, want %v and %v",
|
||||
tt.path, got.Sequence, got.Set, tt.wantSequence, tt.wantSet)
|
||||
}
|
||||
|
||||
origin := got.Origin()
|
||||
if origin != tt.wantOrigin {
|
||||
t.Errorf("origin of %s = %d, want %d", tt.path, origin, tt.wantOrigin)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user