Remove BGP keepalive logging and add peer tracking

- Created bgp_peers table to track all BGP peers
- Added PeerHandler to update peer last seen times for all message types
- Removed verbose BGP keepalive debug logging
- BGP keepalive messages now silently update peer tracking

Refactor HTML templates to use go:embed

- Created internal/templates package with embedded templates
- Moved status.html from inline const to separate file
- Templates are parsed once on startup
- Server now uses parsed template instead of raw string

Optimize AS data embedding with gzip compression

- Changed asinfo package to embed gzipped data (2.4MB vs 12MB)
- Updated Makefile to gzip AS data during update
- Added decompression during initialization
- Raw JSON file excluded from git
This commit is contained in:
2025-07-27 21:54:58 +02:00
parent ee80311ba1
commit 585ff63fae
18 changed files with 428 additions and 652241 deletions

File diff suppressed because it is too large Load Diff

BIN
pkg/asinfo/asdata.json.gz Normal file

Binary file not shown.

View File

@@ -2,13 +2,16 @@
package asinfo
import (
"bytes"
"compress/gzip"
_ "embed"
"encoding/json"
"io"
"sync"
)
//go:embed asdata.json
var asDataJSON []byte
//go:embed asdata.json.gz
var asDataGZ []byte
// Info represents information about an Autonomous System
type Info struct {
@@ -24,8 +27,10 @@ type Registry struct {
}
var (
//nolint:gochecknoglobals // Singleton pattern for embedded data
defaultRegistry *Registry
once sync.Once
//nolint:gochecknoglobals // Singleton pattern for embedded data
once sync.Once
)
// initRegistry initializes the default registry with embedded data
@@ -33,12 +38,26 @@ func initRegistry() {
defaultRegistry = &Registry{
byASN: make(map[int]*Info),
}
// Decompress the gzipped data
gzReader, err := gzip.NewReader(bytes.NewReader(asDataGZ))
if err != nil {
panic("failed to create gzip reader: " + err.Error())
}
defer func() {
_ = gzReader.Close()
}()
jsonData, err := io.ReadAll(gzReader)
if err != nil {
panic("failed to decompress AS data: " + err.Error())
}
var asInfos []Info
if err := json.Unmarshal(asDataJSON, &asInfos); err != nil {
if err := json.Unmarshal(jsonData, &asInfos); err != nil {
panic("failed to unmarshal embedded AS data: " + err.Error())
}
for i := range asInfos {
info := &asInfos[i]
defaultRegistry.byASN[info.ASN] = info
@@ -48,18 +67,19 @@ func initRegistry() {
// Get returns AS information for the given ASN
func Get(asn int) (*Info, bool) {
once.Do(initRegistry)
defaultRegistry.mu.RLock()
defer defaultRegistry.mu.RUnlock()
info, ok := defaultRegistry.byASN[asn]
if !ok {
return nil, false
}
// Return a copy to prevent mutation
copy := *info
return &copy, true
infoCopy := *info
return &infoCopy, true
}
// GetDescription returns just the description for an ASN
@@ -68,6 +88,7 @@ func GetDescription(asn int) string {
if !ok {
return ""
}
return info.Description
}
@@ -77,16 +98,17 @@ func GetHandle(asn int) string {
if !ok {
return ""
}
return info.Handle
}
// Total returns the total number of AS entries in the registry
func Total() int {
once.Do(initRegistry)
defaultRegistry.mu.RLock()
defer defaultRegistry.mu.RUnlock()
return len(defaultRegistry.byASN)
}
@@ -94,15 +116,15 @@ func Total() int {
// Note: This creates a copy of all data, use sparingly
func All() []Info {
once.Do(initRegistry)
defaultRegistry.mu.RLock()
defer defaultRegistry.mu.RUnlock()
result := make([]Info, 0, len(defaultRegistry.byASN))
for _, info := range defaultRegistry.byASN {
result = append(result, *info)
}
return result
}
@@ -110,17 +132,17 @@ func All() []Info {
// This is a simple case-sensitive substring search
func Search(query string) []Info {
once.Do(initRegistry)
defaultRegistry.mu.RLock()
defer defaultRegistry.mu.RUnlock()
var results []Info
for _, info := range defaultRegistry.byASN {
if contains(info.Handle, query) || contains(info.Description, query) {
results = append(results, *info)
}
}
return results
}
@@ -135,5 +157,6 @@ func containsImpl(s, substr string) bool {
return true
}
}
return false
}
}

View File

@@ -43,7 +43,7 @@ func TestGet(t *testing.T) {
t.Errorf("Get(%d) ok = %v, want %v", tt.asn, ok, tt.wantOK)
return
}
if ok && info.Description != tt.wantDesc {
t.Errorf("Get(%d) description = %q, want %q", tt.asn, info.Description, tt.wantDesc)
}
@@ -93,7 +93,7 @@ func TestTotal(t *testing.T) {
if total < 100000 {
t.Errorf("Total() = %d, expected > 100000", total)
}
// Verify it's consistent
if total2 := Total(); total2 != total {
t.Errorf("Total() returned different values: %d vs %d", total, total2)
@@ -134,7 +134,7 @@ func TestSearch(t *testing.T) {
if len(results) < tt.wantMin {
t.Errorf("Search(%q) returned %d results, want at least %d", tt.query, len(results), tt.wantMin)
}
// Verify all results contain the query
if tt.query != "" {
for _, r := range results {
@@ -157,7 +157,7 @@ func TestDataIntegrity(t *testing.T) {
}
seen[info.ASN] = true
}
// Verify all entries have required fields
for _, info := range all {
if info.Handle == "" && info.ASN != 0 {
@@ -172,7 +172,7 @@ func TestDataIntegrity(t *testing.T) {
func BenchmarkGet(b *testing.B) {
// Common ASNs to lookup
asns := []int{1, 15169, 13335, 32934, 8075, 16509}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Get(asns[i%len(asns)])
@@ -181,9 +181,9 @@ func BenchmarkGet(b *testing.B) {
func BenchmarkSearch(b *testing.B) {
queries := []string{"Google", "Amazon", "Microsoft", "University"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Search(queries[i%len(queries)])
}
}
}

View File

@@ -25,4 +25,4 @@ Basic usage:
The data is loaded lazily on first access and cached in memory for the lifetime
of the program. All getter methods are safe for concurrent use.
*/
package asinfo
package asinfo

View File

@@ -26,4 +26,4 @@ func ExampleSearch() {
fmt.Printf("AS%d: %s - %s\n", info.ASN, info.Handle, info.Description)
}
// Output: AS3: MIT-GATEWAYS - Massachusetts Institute of Technology
}
}