Handle an announcement without an origin like a replacement (closes #113)
check / check (push) Canceled after 0s

An announcement whose AS path is empty or ends in AS 0 now deletes the
peer's route for the prefix, as a withdrawal does. Before, it was neither
stored nor applied: the peer's old route stayed live, and within one
batch it cancelled the peer's earlier withdrawal of the prefix.

Model: opus-5-5
This commit is contained in:
2026-10-20 01:04:42 +00:00
parent 444457bcf8
commit dfca772035
3 changed files with 76 additions and 8 deletions
+5 -4
View File
@@ -190,10 +190,11 @@ routewatch/
- **ASHandler**: Tracks all ASNs seen in AS paths - **ASHandler**: Tracks all ASNs seen in AS paths
- **PeerHandler**: Records BGP peer information - **PeerHandler**: Records BGP peer information
- **PrefixHandler**: Maintains live routing table (upserts on announcement, - **PrefixHandler**: Maintains live routing table (upserts on announcement,
deletes on withdrawal, deletes all of a peer's routes when its BGP session deletes on withdrawal or on an announcement whose AS path is empty or ends
goes down). Routes are kept per peer IP, so when one peer IP has sessions in AS 0, deletes all of a peer's routes when its BGP session goes down).
on two collectors, a session going down on one also removes the routes the Routes are kept per peer IP, so when one peer IP has sessions on two
other still carries, until they are announced again. collectors, a session going down on one also removes the routes the other
still carries, until they are announced again.
- **PeeringHandler**: Extracts AS peering relationships from AS paths - **PeeringHandler**: Extracts AS peering relationships from AS paths
5. **HTTP Request Flow** 5. **HTTP Request Flow**
+9 -4
View File
@@ -44,8 +44,9 @@ const (
) )
// PrefixHandler tracks BGP prefixes and maintains a live routing table in the // PrefixHandler tracks BGP prefixes and maintains a live routing table in the
// database. Routes are added on announcement and deleted on withdrawal, and all // database. Routes are added on announcement and deleted on withdrawal or on an
// routes of a peer are deleted when its BGP session goes down. // announcement whose AS path is empty or ends in AS 0, and all routes of a peer
// are deleted when its BGP session goes down.
type PrefixHandler struct { type PrefixHandler struct {
db database.Store db database.Store
logger *logger.Logger logger *logger.Logger
@@ -371,7 +372,11 @@ func (h *PrefixHandler) flushBatch() {
for _, update := range lastUpdates { for _, update := range lastUpdates {
// An announcement is stored when its path gives an origin, or when it // An announcement is stored when its path gives an origin, or when it
// ends in an AS set of several members: that route is stored with // ends in an AS set of several members: that route is stored with
// origin 0 (none). An announcement with an empty path is not stored. // origin 0 (none). Any other update deletes the peer's route for the
// prefix: a withdrawal, or an announcement whose path is empty or ends
// in AS 0. Such an announcement replaces the peer's route, as any
// announcement does, with none (RFC 7607 treats AS 0 in a path as a
// withdrawal).
if update.messageType == "announcement" && if update.messageType == "announcement" &&
(update.originASN > 0 || update.endsInSet) { (update.originASN > 0 || update.endsInSet) {
// Create live route for batch upsert // Create live route for batch upsert
@@ -379,7 +384,7 @@ func (h *PrefixHandler) flushBatch() {
if route != nil { if route != nil {
routesToUpsert = append(routesToUpsert, route) routesToUpsert = append(routesToUpsert, route)
} }
} else if update.messageType == "withdrawal" { } else {
// Parse CIDR to get IP version // Parse CIDR to get IP version
_, ipVersion, err := parseCIDR(update.prefix) _, ipVersion, err := parseCIDR(update.prefix)
if err != nil { if err != nil {
@@ -378,6 +378,68 @@ func TestPrefixHandlerDoesNotStoreAnAnnouncementWithAnEmptyPath(t *testing.T) {
assertPeers(t, db, map[string][]string{testV4Prefix: nil}) assertPeers(t, db, map[string][]string{testV4Prefix: nil})
} }
// TestPrefixHandlerRemovesTheRouteOnAnAnnouncementWithoutAnOrigin runs, through
// the prefix handler into a real database, a peer's announcement of a prefix
// with origin AS testASNB and then the peer's announcement of the prefix with
// an AS path that gives no origin: one whose last AS is 0, or an empty one. As
// any later announcement from the peer does, the second replaces the peer's
// route, here with none, whether it comes in a later batch or follows a
// withdrawal in one batch. The in-memory route count and prefix distribution
// must match the route table.
func TestPrefixHandlerRemovesTheRouteOnAnAnnouncementWithoutAnOrigin(
t *testing.T,
) {
t.Parallel()
const prefixMaskLength = 24
noOrigin := func(path ristypes.ASPath) *ristypes.RISMessage {
msg := peerAnnouncement(testPeerIP, testV4Prefix)
msg.Path = path
return msg
}
endsInZero := ristypes.ASPath{Sequence: []int{testASNA, 0}}
withdrawal := &ristypes.RISMessage{
Type: ristypes.MessageTypeUpdate,
Peer: testPeerIP,
ParsedTimestamp: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC),
Withdrawals: []string{testV4Prefix},
}
tests := []struct {
name string
// Handled and flushed as one batch after the announcement with origin
// AS testASNB.
batch []*ristypes.RISMessage
}{
{
name: "a path whose last AS is 0",
batch: []*ristypes.RISMessage{noOrigin(endsInZero)},
},
{
name: "an empty path",
batch: []*ristypes.RISMessage{noOrigin(ristypes.ASPath{})},
},
{
name: "a withdrawal and then a path whose last AS is 0",
batch: []*ristypes.RISMessage{withdrawal, noOrigin(endsInZero)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
db := handleBatches(t, [][]*ristypes.RISMessage{
{peerAnnouncement(testPeerIP, testV4Prefix)},
tt.batch,
})
checkLivePeers(t, db, testV4Prefix, prefixMaskLength, nil)
})
}
}
// TestStateMessagesGoToThePrefixHandlerNotThePeerHandler checks that STATE // TestStateMessagesGoToThePrefixHandlerNotThePeerHandler checks that STATE
// messages go to the prefix handler, which acts on a down, and not to the peer // 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. // handler, which would record a peer whose session never comes up as active.