Keep a trailing AS set out of the origin and the peerings (closes #75)
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:
2026-10-19 12:50:05 +02:00
parent 8742f1b44d
commit 2712183f21
14 changed files with 395 additions and 60 deletions
@@ -40,7 +40,7 @@ func TestPrefixHandlerStoresPrefixesTheIPLookupFinds(t *testing.T) {
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
h.HandleMessage(&ristypes.RISMessage{
Peer: testPeerIP,
Path: ristypes.ASPath{testASNA, testASNB},
Path: ristypes.ASPath{Sequence: []int{testASNA, testASNB}},
ParsedTimestamp: ts,
Announcements: []ristypes.RISAnnouncement{{
NextHop: testPeerIP,
@@ -99,7 +99,7 @@ func TestPrefixHandlerAppliesEachPeersLastUpdateInABatch(t *testing.T) {
announce := func(peer string) *ristypes.RISMessage {
return &ristypes.RISMessage{
Peer: peer,
Path: ristypes.ASPath{testASNA, testASNB},
Path: ristypes.ASPath{Sequence: []int{testASNA, testASNB}},
ParsedTimestamp: ts,
Announcements: []ristypes.RISAnnouncement{
{NextHop: peer, Prefixes: []string{prefix}},
@@ -216,7 +216,7 @@ func TestPrefixHandlerKeepsOneRoutePerPrefixAndPeer(t *testing.T) {
// One message that announces otherPrefix with origin AS testASNB and
// withdraws prefix.
announceOtherAndWithdraw := announce(
ristypes.ASPath{testASNA, testASNB}, otherPrefix)
ristypes.ASPath{Sequence: []int{testASNA, testASNB}}, otherPrefix)
announceOtherAndWithdraw.Withdrawals = []string{prefix}
tests := []struct {
@@ -230,8 +230,8 @@ func TestPrefixHandlerKeepsOneRoutePerPrefixAndPeer(t *testing.T) {
{
name: "the peer announces the prefix with a new origin AS",
batches: [][]*ristypes.RISMessage{
{announce(ristypes.ASPath{testASNA}, prefix)},
{announce(ristypes.ASPath{testASNA, testASNB}, prefix)},
{announce(ristypes.ASPath{Sequence: []int{testASNA}}, prefix)},
{announce(ristypes.ASPath{Sequence: []int{testASNA, testASNB}}, prefix)},
},
wantOrigins: map[string]int{prefix: testASNB},
},
@@ -239,7 +239,7 @@ func TestPrefixHandlerKeepsOneRoutePerPrefixAndPeer(t *testing.T) {
name: "one message announces a prefix with another origin AS " +
"and withdraws the prefix",
batches: [][]*ristypes.RISMessage{
{announce(ristypes.ASPath{testASNA}, prefix)},
{announce(ristypes.ASPath{Sequence: []int{testASNA}}, prefix)},
{announceOtherAndWithdraw},
},
wantOrigins: map[string]int{otherPrefix: testASNB},
@@ -264,6 +264,70 @@ func TestPrefixHandlerKeepsOneRoutePerPrefixAndPeer(t *testing.T) {
}
}
// TestPrefixHandlerStoresNoOriginForAPathEndingInASet announces a prefix with
// AS paths that end in an AS set through the prefix handler into a real
// database. Any member of a set of several may be the origin, so the route is
// stored with origin 0 (none); a set of one member counts as that AS. The
// stored AS path lists the set's members after the rest of the path.
func TestPrefixHandlerStoresNoOriginForAPathEndingInASet(t *testing.T) {
t.Parallel()
tests := []struct {
name string
path []any
wantOrigin int
wantPath []int
}{
{
name: "a set of several members",
path: []any{testASNA, testASNB, []int{testASNC, testASND}},
wantPath: []int{testASNA, testASNB, testASNC, testASND},
},
{
name: "a set of one member",
path: []any{testASNA, testASNB, []int{testASNC}},
wantOrigin: testASNC,
wantPath: []int{testASNA, testASNB, testASNC},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
msg := peerAnnouncement(testPeerIP, testV4Prefix)
msg.Path = decodePath(t, tt.path...)
db := handleBatches(t, [][]*ristypes.RISMessage{{msg}})
routes, _, err := db.GetPrefixDetailsContext(
context.Background(), testV4Prefix)
if err != nil {
t.Fatalf("GetPrefixDetailsContext(%s): %v", testV4Prefix, err)
}
if len(routes) != 1 || routes[0].OriginASN != tt.wantOrigin ||
!slices.Equal(routes[0].ASPath, tt.wantPath) {
t.Errorf("live routes for %s = %+v, "+
"want one with origin AS%d and AS path %v",
testV4Prefix, routes, tt.wantOrigin, tt.wantPath)
}
})
}
}
// TestPrefixHandlerDoesNotStoreAnAnnouncementWithAnEmptyPath announces a
// prefix with an empty AS path, which gives no origin and does not end in an
// AS set, so no live route is stored.
func TestPrefixHandlerDoesNotStoreAnAnnouncementWithAnEmptyPath(t *testing.T) {
t.Parallel()
msg := peerAnnouncement(testPeerIP, testV4Prefix)
msg.Path = ristypes.ASPath{}
db := handleBatches(t, [][]*ristypes.RISMessage{{msg}})
assertPeers(t, db, map[string][]string{testV4Prefix: nil})
}
// TestStateMessagesGoToThePrefixHandlerNotThePeerHandler checks that STATE
// messages go to the prefix handler, which acts on a down, and not to the peer
// handler, which would record a peer whose session never comes up as active.
@@ -540,7 +604,7 @@ func peerAnnouncement(peer string, prefixes ...string) *ristypes.RISMessage {
return &ristypes.RISMessage{
Type: ristypes.MessageTypeUpdate,
Peer: peer,
Path: ristypes.ASPath{testASNA, testASNB},
Path: ristypes.ASPath{Sequence: []int{testASNA, testASNB}},
ParsedTimestamp: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC),
Announcements: []ristypes.RISAnnouncement{
{NextHop: peer, Prefixes: prefixes},