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. 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. Model: opus-5-5
738 lines
22 KiB
Go
738 lines
22 KiB
Go
package routewatch
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/config"
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
|
)
|
|
|
|
const testPeerIP = "2001:db8:ffff::1"
|
|
|
|
// An IPv4 and an IPv6 prefix the tests announce.
|
|
const (
|
|
testV4Prefix = "192.0.2.0/24"
|
|
testV6Prefix = "2001:db8:1::/48"
|
|
)
|
|
|
|
// TestPrefixHandlerStoresPrefixesTheIPLookupFinds runs announcements and
|
|
// withdrawals through the prefix handler into a real database and looks the
|
|
// addresses up. The prefixes are written the way the feed sends them: IPv6
|
|
// announcements compressed, IPv6 withdrawals uncompressed. The withdrawal must
|
|
// remove the route the announcement stored, and the IP lookup must find the
|
|
// stored prefix.
|
|
func TestPrefixHandlerStoresPrefixesTheIPLookupFinds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := newTestDatabase(t)
|
|
|
|
// Built without NewPrefixHandler's flush timer, so the test flushes itself.
|
|
h := &PrefixHandler{db: db, logger: logger.New()}
|
|
|
|
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
|
h.HandleMessage(&ristypes.RISMessage{
|
|
Peer: testPeerIP,
|
|
Path: ristypes.ASPath{Sequence: []int{testASNA, testASNB}},
|
|
ParsedTimestamp: ts,
|
|
Announcements: []ristypes.RISAnnouncement{{
|
|
NextHop: testPeerIP,
|
|
Prefixes: []string{testV6Prefix, testV4Prefix},
|
|
}},
|
|
})
|
|
h.flushBatch()
|
|
|
|
for ip, want := range map[string]string{
|
|
"2001:db8:1::1": testV6Prefix,
|
|
"192.0.2.1": testV4Prefix,
|
|
} {
|
|
info, err := db.GetIPInfo(ip)
|
|
if err != nil {
|
|
t.Fatalf("GetIPInfo(%s) after announcement: %v", ip, err)
|
|
}
|
|
|
|
if info.Netblock != want || info.ASN != testASNB {
|
|
t.Errorf("GetIPInfo(%s) = %s AS%d, want %s AS%d",
|
|
ip, info.Netblock, info.ASN, want, testASNB)
|
|
}
|
|
}
|
|
|
|
h.HandleMessage(&ristypes.RISMessage{
|
|
Peer: testPeerIP,
|
|
ParsedTimestamp: ts.Add(time.Minute),
|
|
Withdrawals: []string{"2001:db8:1:0:0:0:0:0/48", testV4Prefix},
|
|
})
|
|
h.flushBatch()
|
|
|
|
for _, ip := range []string{"2001:db8:1::1", "192.0.2.1"} {
|
|
info, err := db.GetIPInfo(ip)
|
|
if !errors.Is(err, database.ErrNoRoute) {
|
|
t.Errorf("GetIPInfo(%s) after withdrawal = %+v, %v; want ErrNoRoute",
|
|
ip, info, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPrefixHandlerAppliesEachPeersLastUpdateInABatch runs updates for one
|
|
// prefix through the prefix handler into a real database. All updates carry the
|
|
// same timestamp, as updates within one second of the feed do. Within a batch,
|
|
// the last update each peer sent for the prefix decides whether that peer has a
|
|
// live route for it, and updates from different peers are all applied. The
|
|
// in-memory route count and prefix distribution must match the route table.
|
|
func TestPrefixHandlerAppliesEachPeersLastUpdateInABatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
prefix = "198.51.100.0/24"
|
|
prefixMaskLength = 24
|
|
otherPeerIP = "2001:db8:ffff::2"
|
|
)
|
|
|
|
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
|
announce := func(peer string) *ristypes.RISMessage {
|
|
return &ristypes.RISMessage{
|
|
Peer: peer,
|
|
Path: ristypes.ASPath{Sequence: []int{testASNA, testASNB}},
|
|
ParsedTimestamp: ts,
|
|
Announcements: []ristypes.RISAnnouncement{
|
|
{NextHop: peer, Prefixes: []string{prefix}},
|
|
},
|
|
}
|
|
}
|
|
withdraw := func(peer string) *ristypes.RISMessage {
|
|
return &ristypes.RISMessage{Peer: peer, ParsedTimestamp: ts,
|
|
Withdrawals: []string{prefix}}
|
|
}
|
|
announceAndWithdraw := func(peer string) *ristypes.RISMessage {
|
|
msg := announce(peer)
|
|
msg.Withdrawals = []string{prefix}
|
|
|
|
return msg
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
// Each inner list is handled and then flushed as one batch.
|
|
batches [][]*ristypes.RISMessage
|
|
// The peers left with a live route for the prefix, by peer IP.
|
|
wantPeers []string
|
|
}{
|
|
{
|
|
name: "two peers announce the prefix",
|
|
batches: [][]*ristypes.RISMessage{
|
|
{announce(testPeerIP), announce(otherPeerIP)},
|
|
},
|
|
wantPeers: []string{testPeerIP, otherPeerIP},
|
|
},
|
|
{
|
|
name: "two peers withdraw the prefix",
|
|
batches: [][]*ristypes.RISMessage{
|
|
{announce(testPeerIP)},
|
|
{announce(otherPeerIP)},
|
|
{withdraw(testPeerIP), withdraw(otherPeerIP)},
|
|
},
|
|
},
|
|
{
|
|
name: "a peer announces and then withdraws the prefix",
|
|
batches: [][]*ristypes.RISMessage{{announce(testPeerIP), withdraw(testPeerIP)}},
|
|
},
|
|
{
|
|
name: "a peer withdraws and then re-announces the prefix",
|
|
batches: [][]*ristypes.RISMessage{
|
|
{announce(testPeerIP)},
|
|
{withdraw(testPeerIP), announce(testPeerIP)},
|
|
},
|
|
wantPeers: []string{testPeerIP},
|
|
},
|
|
{
|
|
// RFC 4271 section 4.3: the announcement counts, not the withdrawal.
|
|
name: "one message announces and withdraws the prefix",
|
|
batches: [][]*ristypes.RISMessage{
|
|
{announce(testPeerIP)},
|
|
{announceAndWithdraw(testPeerIP)},
|
|
},
|
|
wantPeers: []string{testPeerIP},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := handleBatches(t, tt.batches)
|
|
checkLivePeers(t, db, prefix, prefixMaskLength, tt.wantPeers)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPrefixHandlerKeepsOneRoutePerPrefixAndPeer runs updates from one peer
|
|
// through the prefix handler into a real database, flushing each message as its
|
|
// own batch, once with IPv4 prefixes and once with IPv6 prefixes. A peer has at
|
|
// most one route for a prefix: a later announcement replaces it whatever its
|
|
// origin AS, and a withdrawal removes it whatever AS the message's AS path ends
|
|
// in. The in-memory route count and prefix distribution must match the route
|
|
// table.
|
|
func TestPrefixHandlerKeepsOneRoutePerPrefixAndPeer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
families := []struct {
|
|
ipVersion int
|
|
prefix, otherPrefix string
|
|
// The mask length of both prefixes.
|
|
maskLength int
|
|
}{
|
|
{
|
|
ipVersion: ipv4Version, maskLength: 24,
|
|
prefix: "198.51.100.0/24", otherPrefix: "203.0.113.0/24",
|
|
},
|
|
{
|
|
ipVersion: ipv6Version, maskLength: 48,
|
|
prefix: testV6Prefix, otherPrefix: "2001:db8:2::/48",
|
|
},
|
|
}
|
|
|
|
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
|
announce := func(path ristypes.ASPath, announced string) *ristypes.RISMessage {
|
|
return &ristypes.RISMessage{
|
|
Peer: testPeerIP,
|
|
Path: path,
|
|
ParsedTimestamp: ts,
|
|
Announcements: []ristypes.RISAnnouncement{
|
|
{NextHop: testPeerIP, Prefixes: []string{announced}},
|
|
},
|
|
}
|
|
}
|
|
|
|
for _, family := range families {
|
|
prefix, otherPrefix := family.prefix, family.otherPrefix
|
|
|
|
// One message that announces otherPrefix with origin AS testASNB and
|
|
// withdraws prefix.
|
|
announceOtherAndWithdraw := announce(
|
|
ristypes.ASPath{Sequence: []int{testASNA, testASNB}}, otherPrefix)
|
|
announceOtherAndWithdraw.Withdrawals = []string{prefix}
|
|
|
|
tests := []struct {
|
|
name string
|
|
// Each message is handled and then flushed as its own batch.
|
|
batches [][]*ristypes.RISMessage
|
|
// The origin AS of the peer's live route for each prefix. A prefix
|
|
// left out must have no live route.
|
|
wantOrigins map[string]int
|
|
}{
|
|
{
|
|
name: "the peer announces the prefix with a new origin AS",
|
|
batches: [][]*ristypes.RISMessage{
|
|
{announce(ristypes.ASPath{Sequence: []int{testASNA}}, prefix)},
|
|
{announce(ristypes.ASPath{Sequence: []int{testASNA, testASNB}}, prefix)},
|
|
},
|
|
wantOrigins: map[string]int{prefix: testASNB},
|
|
},
|
|
{
|
|
name: "one message announces a prefix with another origin AS " +
|
|
"and withdraws the prefix",
|
|
batches: [][]*ristypes.RISMessage{
|
|
{announce(ristypes.ASPath{Sequence: []int{testASNA}}, prefix)},
|
|
{announceOtherAndWithdraw},
|
|
},
|
|
wantOrigins: map[string]int{otherPrefix: testASNB},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
name := fmt.Sprintf("IPv%d %s", family.ipVersion, tt.name)
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := handleBatches(t, tt.batches)
|
|
checkLiveOrigins(t, db, tt.wantOrigins, prefix, otherPrefix)
|
|
|
|
wantDistribution := []database.PrefixDistribution{
|
|
{MaskLength: family.maskLength, Count: len(tt.wantOrigins)},
|
|
}
|
|
assertCounts(t, db, family.ipVersion, len(tt.wantOrigins),
|
|
wantDistribution)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func TestStateMessagesGoToThePrefixHandlerNotThePeerHandler(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if !(&PrefixHandler{}).WantsMessage(ristypes.MessageTypeState) {
|
|
t.Error("the prefix handler does not want STATE messages")
|
|
}
|
|
|
|
if (&PeerHandler{}).WantsMessage(ristypes.MessageTypeState) {
|
|
t.Error("the peer handler wants STATE messages")
|
|
}
|
|
|
|
if !(&PeerHandler{}).WantsMessage(ristypes.MessageTypeUpdate) {
|
|
t.Error("the peer handler does not want UPDATE messages")
|
|
}
|
|
}
|
|
|
|
// TestPrefixHandlerDeletesAPeersRoutesWhenItsSessionGoesDown runs
|
|
// announcements from two peers through the prefix handler into a real
|
|
// database, then a batch in which one of the peers announces a prefix and its
|
|
// session goes down. The down deletes every live route of that peer, IPv4 and
|
|
// IPv6, the one announced just before it included, and keeps the other peer's
|
|
// routes for the same prefixes. An announcement the peer sends after the down,
|
|
// in the same batch, is kept. The in-memory route counts and prefix
|
|
// distribution must match the route tables.
|
|
func TestPrefixHandlerDeletesAPeersRoutesWhenItsSessionGoesDown(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
otherPeerIP = "2001:db8:ffff::2"
|
|
// Both peers announce the shared prefixes, only testPeerIP the others.
|
|
sharedV4 = "198.51.100.0/24"
|
|
sharedV6 = "2001:db8:1::/48"
|
|
onlyV4 = "203.0.113.0/24"
|
|
onlyV6 = "2001:db8:2::/48"
|
|
beforeDown = "192.0.2.0/24"
|
|
afterDown = "2001:db8:3::/48"
|
|
// The mask lengths of the IPv4 and of the IPv6 prefixes.
|
|
maskLengthV4 = 24
|
|
maskLengthV6 = 48
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
// Handled and flushed as one batch after both peers' announcements.
|
|
batch []*ristypes.RISMessage
|
|
// Whether testPeerIP is left with a live route for afterDown.
|
|
wantAfterDown bool
|
|
}{
|
|
{
|
|
name: "the down ends the batch",
|
|
batch: []*ristypes.RISMessage{
|
|
peerAnnouncement(testPeerIP, beforeDown),
|
|
sessionDown(),
|
|
},
|
|
},
|
|
{
|
|
name: "the peer announces a prefix after the down",
|
|
batch: []*ristypes.RISMessage{
|
|
peerAnnouncement(testPeerIP, beforeDown),
|
|
sessionDown(),
|
|
peerAnnouncement(testPeerIP, afterDown),
|
|
},
|
|
wantAfterDown: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := handleBatches(t, [][]*ristypes.RISMessage{
|
|
{
|
|
peerAnnouncement(testPeerIP,
|
|
sharedV4, sharedV6, onlyV4, onlyV6),
|
|
peerAnnouncement(otherPeerIP, sharedV4, sharedV6),
|
|
},
|
|
tt.batch,
|
|
})
|
|
|
|
// The peers left with a live route for each prefix.
|
|
wantPeers := map[string][]string{
|
|
sharedV4: {otherPeerIP},
|
|
sharedV6: {otherPeerIP},
|
|
onlyV4: nil,
|
|
onlyV6: nil,
|
|
beforeDown: nil,
|
|
afterDown: nil,
|
|
}
|
|
wantRoutesV6 := 1
|
|
|
|
if tt.wantAfterDown {
|
|
wantPeers[afterDown] = []string{testPeerIP}
|
|
wantRoutesV6++
|
|
}
|
|
|
|
assertPeers(t, db, wantPeers)
|
|
assertCounts(t, db, ipv4Version, 1, []database.PrefixDistribution{
|
|
{MaskLength: maskLengthV4, Count: 1},
|
|
})
|
|
assertCounts(t, db, ipv6Version, wantRoutesV6,
|
|
[]database.PrefixDistribution{
|
|
{MaskLength: maskLengthV6, Count: wantRoutesV6},
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPrefixHandlerKeepsWhatAPeerAnnouncesAfterTheFlushOfItsDown runs, through
|
|
// the prefix handler into a real database, one flush after another: a peer's
|
|
// announcement, its session going down, a new announcement from the peer, and
|
|
// another peer's announcement. The down deletes the peer's routes on the flush
|
|
// that handles it and on no later flush, so the route the peer announced after
|
|
// it is kept.
|
|
func TestPrefixHandlerKeepsWhatAPeerAnnouncesAfterTheFlushOfItsDown(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
otherPeerIP = "2001:db8:ffff::2"
|
|
beforeDown = "192.0.2.0/24"
|
|
afterDown = "198.51.100.0/24"
|
|
otherPeers = "203.0.113.0/24"
|
|
maskLength = 24
|
|
)
|
|
|
|
// Each message is handled and then flushed as its own batch.
|
|
db := handleBatches(t, [][]*ristypes.RISMessage{
|
|
{peerAnnouncement(testPeerIP, beforeDown)},
|
|
{sessionDown()},
|
|
{peerAnnouncement(testPeerIP, afterDown)},
|
|
{peerAnnouncement(otherPeerIP, otherPeers)},
|
|
})
|
|
|
|
assertPeers(t, db, map[string][]string{
|
|
beforeDown: nil,
|
|
afterDown: {testPeerIP},
|
|
otherPeers: {otherPeerIP},
|
|
})
|
|
assertCounts(t, db, ipv4Version, 2, []database.PrefixDistribution{
|
|
{MaskLength: maskLength, Count: 2},
|
|
})
|
|
}
|
|
|
|
// firstWriteHeldStore is a real database whose first live route batch write
|
|
// waits until the test closes release. writeStarted is closed when it starts.
|
|
type firstWriteHeldStore struct {
|
|
*database.Database
|
|
|
|
writeStarted chan struct{}
|
|
release chan struct{}
|
|
once sync.Once
|
|
}
|
|
|
|
// UpsertLiveRouteBatch holds the first call until release is closed, then
|
|
// writes routes to the database.
|
|
func (s *firstWriteHeldStore) UpsertLiveRouteBatch(
|
|
routes []*database.LiveRoute,
|
|
) error {
|
|
s.once.Do(func() {
|
|
close(s.writeStarted)
|
|
<-s.release
|
|
})
|
|
|
|
return s.Database.UpsertLiveRouteBatch(routes)
|
|
}
|
|
|
|
// TestPrefixHandlerHandlesADownWhileAFlushWrites holds the write of a flush
|
|
// that stores a peer's route and meanwhile hands the prefix handler the peer's
|
|
// session going down, then a new announcement from the peer. The next flush
|
|
// must delete the route the held write stored and keep the one announced after
|
|
// the down.
|
|
func TestPrefixHandlerHandlesADownWhileAFlushWrites(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
beforeDown = "192.0.2.0/24"
|
|
afterDown = "198.51.100.0/24"
|
|
maskLength = 24
|
|
)
|
|
|
|
db := newTestDatabase(t)
|
|
store := &firstWriteHeldStore{
|
|
Database: db,
|
|
writeStarted: make(chan struct{}),
|
|
release: make(chan struct{}),
|
|
}
|
|
|
|
// Built without NewPrefixHandler's flush timer, so the test flushes itself.
|
|
h := &PrefixHandler{db: store, logger: logger.New()}
|
|
h.HandleMessage(peerAnnouncement(testPeerIP, beforeDown))
|
|
|
|
flushed := make(chan struct{})
|
|
|
|
go func() {
|
|
h.flushBatch()
|
|
close(flushed)
|
|
}()
|
|
|
|
select {
|
|
case <-store.writeStarted:
|
|
case <-time.After(handleMessageTimeout):
|
|
t.Fatal("the flush did not start its write")
|
|
}
|
|
|
|
if !returnsInTime(func() {
|
|
h.HandleMessage(sessionDown())
|
|
h.HandleMessage(peerAnnouncement(testPeerIP, afterDown))
|
|
}) {
|
|
t.Fatal("HandleMessage did not return while a database write was held")
|
|
}
|
|
|
|
close(store.release)
|
|
|
|
select {
|
|
case <-flushed:
|
|
case <-time.After(handleMessageTimeout):
|
|
t.Fatal("the flush did not finish after its write was let go")
|
|
}
|
|
|
|
h.flushBatch()
|
|
|
|
assertPeers(t, db, map[string][]string{
|
|
beforeDown: nil,
|
|
afterDown: {testPeerIP},
|
|
})
|
|
assertCounts(t, db, ipv4Version, 1, []database.PrefixDistribution{
|
|
{MaskLength: maskLength, Count: 1},
|
|
})
|
|
}
|
|
|
|
// newTestDatabase opens a database in a new temporary directory. It is closed
|
|
// when the test ends.
|
|
func newTestDatabase(t *testing.T) *database.Database {
|
|
t.Helper()
|
|
|
|
db, err := database.New(&config.Config{StateDir: t.TempDir()}, logger.New())
|
|
if err != nil {
|
|
t.Fatalf("failed to create database: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
return db
|
|
}
|
|
|
|
// handleBatches runs each batch of messages through a prefix handler into a
|
|
// new database, flushing after each batch, and returns the database.
|
|
func handleBatches(
|
|
t *testing.T, batches [][]*ristypes.RISMessage,
|
|
) *database.Database {
|
|
t.Helper()
|
|
|
|
db := newTestDatabase(t)
|
|
|
|
// Built without NewPrefixHandler's flush timer, so the test flushes itself.
|
|
h := &PrefixHandler{db: db, logger: logger.New()}
|
|
|
|
for _, batch := range batches {
|
|
for _, msg := range batch {
|
|
h.HandleMessage(msg)
|
|
}
|
|
|
|
h.flushBatch()
|
|
}
|
|
|
|
return db
|
|
}
|
|
|
|
// peerAnnouncement returns an UPDATE message in which peer announces prefixes
|
|
// with origin AS testASNB.
|
|
func peerAnnouncement(peer string, prefixes ...string) *ristypes.RISMessage {
|
|
return &ristypes.RISMessage{
|
|
Type: ristypes.MessageTypeUpdate,
|
|
Peer: peer,
|
|
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},
|
|
},
|
|
}
|
|
}
|
|
|
|
// sessionDown returns the STATE message that says the BGP session of the peer
|
|
// testPeerIP went down.
|
|
func sessionDown() *ristypes.RISMessage {
|
|
return &ristypes.RISMessage{
|
|
Type: ristypes.MessageTypeState,
|
|
Peer: testPeerIP,
|
|
State: ristypes.StateDown,
|
|
}
|
|
}
|
|
|
|
// checkLivePeers checks that exactly wantPeers have a live route for the IPv4
|
|
// prefix, and that the in-memory route count and prefix distribution match
|
|
// the route table.
|
|
func checkLivePeers(
|
|
t *testing.T, db *database.Database, prefix string, maskLength int,
|
|
wantPeers []string,
|
|
) {
|
|
t.Helper()
|
|
|
|
assertPeers(t, db, map[string][]string{prefix: wantPeers})
|
|
|
|
var wantDistribution []database.PrefixDistribution
|
|
if len(wantPeers) > 0 {
|
|
wantDistribution = []database.PrefixDistribution{
|
|
{MaskLength: maskLength, Count: 1},
|
|
}
|
|
}
|
|
|
|
assertCounts(t, db, ipv4Version, len(wantPeers), wantDistribution)
|
|
}
|
|
|
|
// assertPeers checks, for each prefix in wantPeers, that the peers with a live
|
|
// route for it are the ones listed, in peer IP order; nil means none.
|
|
func assertPeers(
|
|
t *testing.T, db *database.Database, wantPeers map[string][]string,
|
|
) {
|
|
t.Helper()
|
|
|
|
for prefix, want := range wantPeers {
|
|
routes, _, err := db.GetPrefixDetailsContext(context.Background(), prefix)
|
|
if err != nil && !errors.Is(err, database.ErrNoRoute) {
|
|
t.Fatalf("GetPrefixDetailsContext(%s): %v", prefix, err)
|
|
}
|
|
|
|
got := make([]string, 0, len(routes))
|
|
for _, route := range routes {
|
|
got = append(got, route.PeerIP)
|
|
}
|
|
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("peers with a live route for %s = %v, want %v",
|
|
prefix, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkLiveOrigins checks that each of prefixes has one live route, with the
|
|
// origin AS wantOrigins gives for it, or none when wantOrigins leaves it out.
|
|
func checkLiveOrigins(
|
|
t *testing.T, db *database.Database, wantOrigins map[string]int,
|
|
prefixes ...string,
|
|
) {
|
|
t.Helper()
|
|
|
|
for _, prefix := range prefixes {
|
|
routes, _, err := db.GetPrefixDetailsContext(context.Background(), prefix)
|
|
if err != nil && !errors.Is(err, database.ErrNoRoute) {
|
|
t.Fatalf("GetPrefixDetailsContext(%s): %v", prefix, err)
|
|
}
|
|
|
|
gotOrigins := make([]int, 0, len(routes))
|
|
for _, route := range routes {
|
|
gotOrigins = append(gotOrigins, route.OriginASN)
|
|
}
|
|
|
|
var want []int
|
|
if origin, ok := wantOrigins[prefix]; ok {
|
|
want = []int{origin}
|
|
}
|
|
|
|
if !slices.Equal(gotOrigins, want) {
|
|
t.Errorf("origin ASes of the live routes for %s = %v, want %v",
|
|
prefix, gotOrigins, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// assertCounts checks that the live route count and prefix distribution of one
|
|
// IP version are wantRoutes and wantDistribution, both as held in memory and as
|
|
// read from the route table.
|
|
func assertCounts(
|
|
t *testing.T, db *database.Database, ipVersion, wantRoutes int,
|
|
wantDistribution []database.PrefixDistribution,
|
|
) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
|
|
stats, err := db.GetStatsContext(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetStatsContext: %v", err)
|
|
}
|
|
|
|
tableRoutesV4, tableRoutesV6, err := db.GetLiveRouteCountsContext(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetLiveRouteCountsContext: %v", err)
|
|
}
|
|
|
|
queryDistributionV4, queryDistributionV6, err :=
|
|
db.GetPrefixDistributionContext(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetPrefixDistributionContext: %v", err)
|
|
}
|
|
|
|
memoryRoutes, tableRoutes := stats.IPv4Routes, tableRoutesV4
|
|
memoryDistribution := stats.IPv4PrefixDistribution
|
|
queryDistribution := queryDistributionV4
|
|
|
|
if ipVersion == ipv6Version {
|
|
memoryRoutes, tableRoutes = stats.IPv6Routes, tableRoutesV6
|
|
memoryDistribution = stats.IPv6PrefixDistribution
|
|
queryDistribution = queryDistributionV6
|
|
}
|
|
|
|
if memoryRoutes != wantRoutes || tableRoutes != wantRoutes {
|
|
t.Errorf("IPv%d live routes: %d in memory, %d in the table, want %d",
|
|
ipVersion, memoryRoutes, tableRoutes, wantRoutes)
|
|
}
|
|
|
|
if !slices.Equal(memoryDistribution, wantDistribution) ||
|
|
!slices.Equal(queryDistribution, wantDistribution) {
|
|
t.Errorf("IPv%d prefix distribution: %v in memory, %v from the table, "+
|
|
"want %v", ipVersion, memoryDistribution, queryDistribution,
|
|
wantDistribution)
|
|
}
|
|
}
|