Keep a trailing AS set out of the origin and the peerings (closes #75)
check / check (push) Canceled after 0s

A path that ends in an AS set of several members, such as
[64496, 64497, [64498, 64499]], now has no origin: its route is stored
with origin 0 (none), and no member of the set is in a peering. A set of
one member counts as that AS. An announcement with an empty path is
still not stored.

ASPath keeps such a set apart from the sequence and gives the origin.
The prefix and AS handlers take the origin from it; the peering handler
uses the sequence only. The stored AS path still lists the set's members
at the end.

The prefix and prefix length pages show origin 0 as no known origin, the
IP lookup asks no WHOIS lookup for it, and the README says what it means.

Model: opus-5-5
This commit is contained in:
2026-10-19 10:35:31 +00:00
parent 3667d09800
commit ac338d8f10
14 changed files with 395 additions and 60 deletions
+44 -21
View File
@@ -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