Add IPv4 range optimization for IP to AS lookups
- Add v4_ip_start and v4_ip_end columns to live_routes table - Calculate IPv4 CIDR ranges as 32-bit integers for fast lookups - Update PrefixHandler to populate IPv4 range fields - Add GetASInfoForIP method with optimized IPv4 queries - Add comprehensive tests for IP conversion functions
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -20,7 +21,15 @@ import (
|
||||
//go:embed schema.sql
|
||||
var dbSchema string
|
||||
|
||||
const dirPermissions = 0750 // rwxr-x---
|
||||
const (
|
||||
dirPermissions = 0750 // rwxr-x---
|
||||
ipVersionV4 = 4
|
||||
ipVersionV6 = 6
|
||||
ipv6Length = 16
|
||||
ipv4Offset = 12
|
||||
ipv4Bits = 32
|
||||
maxIPv4 = 0xFFFFFFFF
|
||||
)
|
||||
|
||||
// Database manages the SQLite database connection and operations.
|
||||
type Database struct {
|
||||
@@ -430,14 +439,17 @@ func (d *Database) GetStats() (Stats, error) {
|
||||
// UpsertLiveRoute inserts or updates a live route
|
||||
func (d *Database) UpsertLiveRoute(route *LiveRoute) error {
|
||||
query := `
|
||||
INSERT INTO live_routes (id, prefix, mask_length, ip_version, origin_asn, peer_ip, as_path, next_hop, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO live_routes (id, prefix, mask_length, ip_version, origin_asn, peer_ip, as_path, next_hop,
|
||||
last_updated, v4_ip_start, v4_ip_end)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(prefix, origin_asn, peer_ip) DO UPDATE SET
|
||||
mask_length = excluded.mask_length,
|
||||
ip_version = excluded.ip_version,
|
||||
as_path = excluded.as_path,
|
||||
next_hop = excluded.next_hop,
|
||||
last_updated = excluded.last_updated
|
||||
last_updated = excluded.last_updated,
|
||||
v4_ip_start = excluded.v4_ip_start,
|
||||
v4_ip_end = excluded.v4_ip_end
|
||||
`
|
||||
|
||||
// Encode AS path as JSON
|
||||
@@ -446,6 +458,15 @@ func (d *Database) UpsertLiveRoute(route *LiveRoute) error {
|
||||
return fmt.Errorf("failed to encode AS path: %w", err)
|
||||
}
|
||||
|
||||
// Convert v4_ip_start and v4_ip_end to interface{} for SQL NULL handling
|
||||
var v4Start, v4End interface{}
|
||||
if route.V4IPStart != nil {
|
||||
v4Start = *route.V4IPStart
|
||||
}
|
||||
if route.V4IPEnd != nil {
|
||||
v4End = *route.V4IPEnd
|
||||
}
|
||||
|
||||
_, err = d.db.Exec(query,
|
||||
route.ID.String(),
|
||||
route.Prefix,
|
||||
@@ -456,6 +477,8 @@ func (d *Database) UpsertLiveRoute(route *LiveRoute) error {
|
||||
string(pathJSON),
|
||||
route.NextHop,
|
||||
route.LastUpdated,
|
||||
v4Start,
|
||||
v4End,
|
||||
)
|
||||
|
||||
return err
|
||||
@@ -545,3 +568,156 @@ func (d *Database) GetLiveRouteCounts() (ipv4Count, ipv6Count int, err error) {
|
||||
|
||||
return ipv4Count, ipv6Count, nil
|
||||
}
|
||||
|
||||
// GetASInfoForIP returns AS information for the given IP address
|
||||
func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
||||
// Parse the IP to validate it
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return nil, fmt.Errorf("invalid IP address: %s", ip)
|
||||
}
|
||||
|
||||
// Determine IP version
|
||||
ipVersion := ipVersionV4
|
||||
ipv4 := parsedIP.To4()
|
||||
if ipv4 == nil {
|
||||
ipVersion = ipVersionV6
|
||||
}
|
||||
|
||||
// For IPv4, use optimized range query
|
||||
if ipVersion == ipVersionV4 {
|
||||
// Convert IP to 32-bit unsigned integer
|
||||
ipUint := ipToUint32(ipv4)
|
||||
|
||||
query := `
|
||||
SELECT DISTINCT lr.prefix, lr.mask_length, lr.origin_asn, a.handle, a.description
|
||||
FROM live_routes lr
|
||||
LEFT JOIN asns a ON a.number = lr.origin_asn
|
||||
WHERE lr.ip_version = ? AND lr.v4_ip_start <= ? AND lr.v4_ip_end >= ?
|
||||
ORDER BY lr.mask_length DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
var prefix string
|
||||
var maskLength, originASN int
|
||||
var handle, description sql.NullString
|
||||
|
||||
err := d.db.QueryRow(query, ipVersionV4, ipUint, ipUint).Scan(&prefix, &maskLength, &originASN, &handle, &description)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("no route found for IP %s", ip)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to query routes: %w", err)
|
||||
}
|
||||
|
||||
return &ASInfo{
|
||||
ASN: originASN,
|
||||
Handle: handle.String,
|
||||
Description: description.String,
|
||||
Prefix: prefix,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// For IPv6, use the original method since we don't have range optimization
|
||||
query := `
|
||||
SELECT DISTINCT lr.prefix, lr.mask_length, lr.origin_asn, a.handle, a.description
|
||||
FROM live_routes lr
|
||||
LEFT JOIN asns a ON a.number = lr.origin_asn
|
||||
WHERE lr.ip_version = ?
|
||||
ORDER BY lr.mask_length DESC
|
||||
`
|
||||
|
||||
rows, err := d.db.Query(query, ipVersionV6)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query routes: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
// Find the most specific matching prefix
|
||||
var bestMatch struct {
|
||||
prefix string
|
||||
maskLength int
|
||||
originASN int
|
||||
handle sql.NullString
|
||||
description sql.NullString
|
||||
}
|
||||
bestMaskLength := -1
|
||||
|
||||
for rows.Next() {
|
||||
var prefix string
|
||||
var maskLength, originASN int
|
||||
var handle, description sql.NullString
|
||||
|
||||
if err := rows.Scan(&prefix, &maskLength, &originASN, &handle, &description); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the prefix CIDR
|
||||
_, ipNet, err := net.ParseCIDR(prefix)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the IP is in this prefix
|
||||
if ipNet.Contains(parsedIP) && maskLength > bestMaskLength {
|
||||
bestMatch.prefix = prefix
|
||||
bestMatch.maskLength = maskLength
|
||||
bestMatch.originASN = originASN
|
||||
bestMatch.handle = handle
|
||||
bestMatch.description = description
|
||||
bestMaskLength = maskLength
|
||||
}
|
||||
}
|
||||
|
||||
if bestMaskLength == -1 {
|
||||
return nil, fmt.Errorf("no route found for IP %s", ip)
|
||||
}
|
||||
|
||||
return &ASInfo{
|
||||
ASN: bestMatch.originASN,
|
||||
Handle: bestMatch.handle.String,
|
||||
Description: bestMatch.description.String,
|
||||
Prefix: bestMatch.prefix,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ipToUint32 converts an IPv4 address to a 32-bit unsigned integer
|
||||
func ipToUint32(ip net.IP) uint32 {
|
||||
if len(ip) == ipv6Length {
|
||||
// Convert to 4-byte representation
|
||||
ip = ip[ipv4Offset:ipv6Length]
|
||||
}
|
||||
|
||||
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
|
||||
}
|
||||
|
||||
// CalculateIPv4Range calculates the start and end IP addresses for an IPv4 CIDR block
|
||||
func CalculateIPv4Range(cidr string) (start, end uint32, err error) {
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// Get the network address (start of range)
|
||||
ip := ipNet.IP.To4()
|
||||
if ip == nil {
|
||||
return 0, 0, fmt.Errorf("not an IPv4 address")
|
||||
}
|
||||
|
||||
start = ipToUint32(ip)
|
||||
|
||||
// Calculate the end of the range
|
||||
ones, bits := ipNet.Mask.Size()
|
||||
hostBits := bits - ones
|
||||
if hostBits >= ipv4Bits {
|
||||
// Special case for /0 - entire IPv4 space
|
||||
end = maxIPv4
|
||||
} else {
|
||||
// Safe to convert since we checked hostBits < 32
|
||||
//nolint:gosec // hostBits is guaranteed to be < 32 from the check above
|
||||
end = start | ((1 << uint(hostBits)) - 1)
|
||||
}
|
||||
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user