check / check (push) Successful in 3m13s
Looking up an address on /ip/ and /api/v1/ip/ read every IPv6 live route and about half of the IPv4 range index, so on a day-sized database it passed the 30-second request timeout. The lookup is now one function for both families: from the longest mask length down, it looks up the address's network prefix on the existing prefix index, and the first live route wins. The feed sends IPv6 withdrawals uncompressed, so they never matched the stored compressed prefix and never removed a route. Prefixes are now stored in the text form net/netip prints, so IPv6 withdrawals take effect. ip_start, ip_end and GetASInfoForIP are removed; nothing read them. A database from before this change must be deleted. Model: opus-5-5
75 lines
2.3 KiB
Go
75 lines
2.3 KiB
Go
package routewatch
|
|
|
|
import (
|
|
"errors"
|
|
"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"
|
|
|
|
// 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) {
|
|
db, err := database.New(&config.Config{StateDir: t.TempDir()}, logger.New())
|
|
if err != nil {
|
|
t.Fatalf("failed to create database: %v", err)
|
|
}
|
|
defer func() { _ = db.Close() }()
|
|
|
|
// Built without NewPrefixHandler's flush timer, so the test flushes itself.
|
|
h := &PrefixHandler{db: db, logger: logger.New()}
|
|
flush := func() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.flushBatchLocked()
|
|
}
|
|
|
|
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
|
h.HandleMessage(&ristypes.RISMessage{
|
|
Peer: testPeerIP,
|
|
Path: ristypes.ASPath{testASNA, testASNB},
|
|
ParsedTimestamp: ts,
|
|
Announcements: []ristypes.RISAnnouncement{{
|
|
NextHop: testPeerIP,
|
|
Prefixes: []string{"2001:db8:1::/48", "192.0.2.0/24"},
|
|
}},
|
|
})
|
|
flush()
|
|
|
|
for ip, want := range map[string]string{
|
|
"2001:db8:1::1": "2001:db8:1::/48",
|
|
"192.0.2.1": "192.0.2.0/24",
|
|
} {
|
|
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", "192.0.2.0/24"},
|
|
})
|
|
flush()
|
|
|
|
for _, ip := range []string{"2001:db8:1::1", "192.0.2.1"} {
|
|
if info, err := db.GetIPInfo(ip); !errors.Is(err, database.ErrNoRoute) {
|
|
t.Errorf("GetIPInfo(%s) after withdrawal = %+v, %v; want ErrNoRoute", ip, info, err)
|
|
}
|
|
}
|
|
}
|