From db1f9cf9cbf162d3c94244ef9b3c7b3abe9a250f Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 20 Oct 2026 01:30:51 +0000 Subject: [PATCH] Keep prefixes that do not parse out of the prefix tables (closes #114) UpdatePrefixesBatch now skips a prefix that does not parse, so it never becomes a row in prefixes_v4 or prefixes_v6 and never counts in the prefix total in the stats. Before, the prefix handler stored no route for such a prefix and logged it once per prefix and peer, but still passed it to UpdatePrefixesBatch, which stored it. Model: opus-5-5 --- internal/database/database.go | 7 ++- .../routewatch/prefixhandler_internal_test.go | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/internal/database/database.go b/internal/database/database.go index 270d7d1..ec6bded 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -855,7 +855,7 @@ func insertPrefixesInTable( // UpdatePrefixesBatch stores, in a single transaction, the prefixes that are // not stored yet, each first seen at its time in prefixes: the IPv4 prefixes -// first and then the IPv6 prefixes. +// first and then the IPv6 prefixes. A prefix that does not parse is not stored. func (d *Database) UpdatePrefixesBatch(prefixes map[string]time.Time) error { if len(prefixes) == 0 { return nil @@ -874,6 +874,11 @@ func (d *Database) UpdatePrefixesBatch(prefixes map[string]time.Time) error { prefixesV6 := make(map[string]time.Time) for prefix, timestamp := range prefixes { + _, err := netip.ParsePrefix(prefix) + if err != nil { + continue + } + if detectIPVersion(prefix) == ipVersionV4 { prefixesV4[prefix] = timestamp } else { diff --git a/internal/routewatch/prefixhandler_internal_test.go b/internal/routewatch/prefixhandler_internal_test.go index 81aa4c7..a5c69a6 100644 --- a/internal/routewatch/prefixhandler_internal_test.go +++ b/internal/routewatch/prefixhandler_internal_test.go @@ -440,6 +440,64 @@ func TestPrefixHandlerRemovesTheRouteOnAnAnnouncementWithoutAnOrigin( } } +// TestPrefixHandlerDoesNotStoreAPrefixThatDoesNotParse runs an announcement of +// "not-a-prefix" and a withdrawal of "also/bad" through the prefix handler into +// a real database. Neither is stored in the prefix tables: the prefix count in +// the stats stays 0, both as kept in memory and as counted from the tables when +// the database is opened again. +func TestPrefixHandlerDoesNotStoreAPrefixThatDoesNotParse(t *testing.T) { + t.Parallel() + + cfg := &config.Config{StateDir: t.TempDir()} + + db, err := database.New(cfg, logger.New()) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + + // Built without NewPrefixHandler's flush timer, so the test flushes itself. + h := &PrefixHandler{db: db, logger: logger.New()} + + withdrawal := &ristypes.RISMessage{ + Type: ristypes.MessageTypeUpdate, + Peer: testPeerIP, + ParsedTimestamp: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), + Withdrawals: []string{"also/bad"}, + } + h.HandleMessage(peerAnnouncement(testPeerIP, "not-a-prefix")) + h.HandleMessage(withdrawal) + h.flushBatch() + + checkNoPrefixes := func(when string) { + t.Helper() + + stats, err := db.GetStatsContext(context.Background()) + if err != nil { + t.Fatalf("GetStatsContext %s: %v", when, err) + } + + if stats.Prefixes != 0 { + t.Errorf("prefix count %s = %d, want 0", when, stats.Prefixes) + } + } + + checkNoPrefixes("after the flush") + + err = db.Close() + if err != nil { + t.Fatalf("Close: %v", err) + } + + db, err = database.New(cfg, logger.New()) + if err != nil { + t.Fatalf("failed to open the database again: %v", err) + } + + t.Cleanup(func() { _ = db.Close() }) + + checkNoPrefixes("after opening the database again") +} + // 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. -- 2.54.0