Handle an announcement without an origin like a replacement (closes #113)
check / check (push) Canceled after 0s
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:
@@ -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**
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user