Ignore routes shorter than /8 or /16 in the IP lookup (closes #57)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The IP lookup tried every mask length down to /0, so a default route or a very short route such as 2000::/3 was reported as the owner of any address it covered until a more specific route was seen. Such a route only says where a peer sends traffic it has no better route for. The lookup now stops at /8 for IPv4 and /16 for IPv6; an address covered only by shorter routes gets "no route found". The live tables still store those routes. README states the limit. Model: opus-5-5
This commit is contained in:
@@ -144,8 +144,9 @@ routewatch/
|
|||||||
- **Reconnection**: Exponential backoff (5s-320s) with reset after 30s of stable connection
|
- **Reconnection**: Exponential backoff (5s-320s) with reset after 30s of stable connection
|
||||||
- **IP Lookup**: the most specific live route is found by looking up the
|
- **IP Lookup**: the most specific live route is found by looking up the
|
||||||
address's prefix at each mask length, longest first, in the prefix index
|
address's prefix at each mask length, longest first, in the prefix index
|
||||||
(at most 33 lookups for IPv4, 129 for IPv6); prefixes are stored in the
|
(at most 25 lookups for IPv4, 113 for IPv6); prefixes are stored in the
|
||||||
text form Go's `net/netip` prints
|
text form Go's `net/netip` prints. Routes shorter than /8 for IPv4 or /16
|
||||||
|
for IPv6, such as a default route, are not used.
|
||||||
|
|
||||||
### Database Schema
|
### Database Schema
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ const (
|
|||||||
ipv6Bits = 128
|
ipv6Bits = 128
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The IP lookup does not use routes shorter than these mask lengths. A default
|
||||||
|
// route or a route this short only says where a peer sends traffic it has no
|
||||||
|
// better route for; it does not say who holds the address.
|
||||||
|
const (
|
||||||
|
minLookupMaskLengthV4 = 8
|
||||||
|
minLookupMaskLengthV6 = 16
|
||||||
|
)
|
||||||
|
|
||||||
// SQLite memory tuning. cache_size and busy_timeout go in the DSN so every
|
// SQLite memory tuning. cache_size and busy_timeout go in the DSN so every
|
||||||
// pooled connection gets them; the heap limits are process-wide and set once.
|
// pooled connection gets them; the heap limits are process-wide and set once.
|
||||||
const (
|
const (
|
||||||
@@ -1876,11 +1884,12 @@ func (d *Database) GetIPInfo(ip string) (*IPInfo, error) {
|
|||||||
// GetIPInfoContext returns comprehensive IP information with context support.
|
// GetIPInfoContext returns comprehensive IP information with context support.
|
||||||
//
|
//
|
||||||
// It finds the most specific live route for the address with point lookups on
|
// It finds the most specific live route for the address with point lookups on
|
||||||
// the prefix index: for each mask length from the longest (32 or 128) down to 0
|
// the prefix index: for each mask length from the longest (32 or 128) down to
|
||||||
// it looks up the address's network prefix at that length, and the first length
|
// minLookupMaskLengthV4 or minLookupMaskLengthV6 it looks up the address's
|
||||||
// with a live route wins. That is at most 33 or 129 indexed lookups, however
|
// network prefix at that length, and the first length with a live route wins.
|
||||||
// many routes are live. The prefix is looked up in the text form net/netip
|
// That is at most 25 or 113 indexed lookups, however many routes are live. The
|
||||||
// prints, which is the form the prefix handler stores.
|
// prefix is looked up in the text form net/netip prints, which is the form the
|
||||||
|
// prefix handler stores.
|
||||||
func (d *Database) GetIPInfoContext(ctx context.Context, ip string) (*IPInfo, error) {
|
func (d *Database) GetIPInfoContext(ctx context.Context, ip string) (*IPInfo, error) {
|
||||||
addr, err := netip.ParseAddr(ip)
|
addr, err := netip.ParseAddr(ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1891,9 +1900,11 @@ func (d *Database) GetIPInfoContext(ctx context.Context, ip string) (*IPInfo, er
|
|||||||
|
|
||||||
info := &IPInfo{IP: ip, IPVersion: ipVersionV6}
|
info := &IPInfo{IP: ip, IPVersion: ipVersionV6}
|
||||||
routesTable, prefixesTable := "live_routes_v6", "prefixes_v6"
|
routesTable, prefixesTable := "live_routes_v6", "prefixes_v6"
|
||||||
|
minMaskLength := minLookupMaskLengthV6
|
||||||
if addr.Is4() {
|
if addr.Is4() {
|
||||||
info.IPVersion = ipVersionV4
|
info.IPVersion = ipVersionV4
|
||||||
routesTable, prefixesTable = "live_routes_v4", "prefixes_v4"
|
routesTable, prefixesTable = "live_routes_v4", "prefixes_v4"
|
||||||
|
minMaskLength = minLookupMaskLengthV4
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route info with peer count and prefix first_seen
|
// Route info with peer count and prefix first_seen
|
||||||
@@ -1934,7 +1945,7 @@ func (d *Database) GetIPInfoContext(ctx context.Context, ip string) (*IPInfo, er
|
|||||||
var whoisUpdatedAt sql.NullTime
|
var whoisUpdatedAt sql.NullTime
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for bits := addr.BitLen(); bits >= 0; bits-- {
|
for bits := addr.BitLen(); bits >= minMaskLength; bits-- {
|
||||||
prefix := netip.PrefixFrom(addr, bits).Masked().String()
|
prefix := netip.PrefixFrom(addr, bits).Masked().String()
|
||||||
err := stmt.QueryRowContext(ctx, prefix).Scan(
|
err := stmt.QueryRowContext(ctx, prefix).Scan(
|
||||||
&info.Netblock,
|
&info.Netblock,
|
||||||
|
|||||||
@@ -133,6 +133,58 @@ func TestGetIPInfoFindsMostSpecificLiveRoute(t *testing.T) {
|
|||||||
lookup("2001:db8:1:2::3", "2001:db8:1::/48")
|
lookup("2001:db8:1:2::3", "2001:db8:1::/48")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetIPInfoIgnoresShortRoutes checks that a default route or a route
|
||||||
|
// shorter than the lookup's minimum mask length is not reported as the route
|
||||||
|
// for an address, and that a longer route covering the address still is.
|
||||||
|
func TestGetIPInfoIgnoresShortRoutes(t *testing.T) {
|
||||||
|
cfg := &config.Config{StateDir: t.TempDir()}
|
||||||
|
|
||||||
|
db, err := New(cfg, logger.New())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create database: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = db.Close() }()
|
||||||
|
|
||||||
|
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||||
|
announce := func(prefix string, asn int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
err := db.UpsertLiveRouteBatch([]*LiveRoute{{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Prefix: prefix,
|
||||||
|
MaskLength: netip.MustParsePrefix(prefix).Bits(),
|
||||||
|
IPVersion: detectIPVersion(prefix),
|
||||||
|
OriginASN: asn,
|
||||||
|
PeerIP: testPeerIP,
|
||||||
|
ASPath: []int{asn},
|
||||||
|
NextHop: testPeerIP,
|
||||||
|
LastUpdated: ts,
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpsertLiveRouteBatch(%s): %v", prefix, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
announce("2000::/3", 64500)
|
||||||
|
announce("0.0.0.0/0", 64500)
|
||||||
|
|
||||||
|
for _, ip := range []string{"2001:db8::1", "192.0.2.1"} {
|
||||||
|
if info, err := db.GetIPInfo(ip); !errors.Is(err, ErrNoRoute) {
|
||||||
|
t.Errorf("GetIPInfo(%s) with only short routes = %+v, %v; want ErrNoRoute", ip, info, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
announce("2001:db8::/32", 64501)
|
||||||
|
|
||||||
|
info, err := db.GetIPInfo("2001:db8::1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetIPInfo(2001:db8::1) after a /32 is added: %v", err)
|
||||||
|
}
|
||||||
|
if info.Netblock != "2001:db8::/32" || info.ASN != 64501 {
|
||||||
|
t.Errorf("GetIPInfo(2001:db8::1) = %s AS%d, want 2001:db8::/32 AS64501", info.Netblock, info.ASN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestConnectionPoolPragmas holds several pooled connections open at once and
|
// TestConnectionPoolPragmas holds several pooled connections open at once and
|
||||||
// checks each one carries the per-connection settings from the DSN, plus the
|
// checks each one carries the per-connection settings from the DSN, plus the
|
||||||
// process-wide hard heap limit, and sees the new file with auto_vacuum
|
// process-wide hard heap limit, and sees the new file with auto_vacuum
|
||||||
|
|||||||
Reference in New Issue
Block a user