List an AS's prefixes by mask length, shortest first (closes #153) #181
@@ -33,6 +33,9 @@ tested.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-27: the AS page lists its IPv4 and its IPv6 prefixes by mask length,
|
||||
shortest first, and prefixes of one mask length by address. They used to be
|
||||
listed by address (closes #153)
|
||||
- 2026-10-27: on `/` and `/status`, a spinner beside the status turns one frame
|
||||
each time the stats show more messages than the time before. A page that has
|
||||
not seen more messages for three refreshes shows "stale" and the time since
|
||||
|
||||
+12
-21
@@ -1,7 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -1028,24 +1029,15 @@ func splitRoutesByIPVersion(
|
||||
return ipv4Routes, ipv6Routes
|
||||
}
|
||||
|
||||
// sortRoutesByNetwork sorts routes by network address, then by mask length.
|
||||
func sortRoutesByNetwork(routes []database.LiveRoute) {
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
// Parse the prefixes to compare network addresses
|
||||
ipI, netI, _ := net.ParseCIDR(routes[i].Prefix)
|
||||
ipJ, netJ, _ := net.ParseCIDR(routes[j].Prefix)
|
||||
// sortRoutesByMaskLength sorts routes by mask length, shortest first, and
|
||||
// routes with the same mask length by network address.
|
||||
func sortRoutesByMaskLength(routes []database.LiveRoute) {
|
||||
slices.SortFunc(routes, func(a, b database.LiveRoute) int {
|
||||
prefixA, _ := netip.ParsePrefix(a.Prefix)
|
||||
prefixB, _ := netip.ParsePrefix(b.Prefix)
|
||||
|
||||
// Compare by network address first
|
||||
cmp := bytes.Compare(ipI.To16(), ipJ.To16())
|
||||
if cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
|
||||
// If network addresses are equal, compare by mask length
|
||||
onesI, _ := netI.Mask.Size()
|
||||
onesJ, _ := netJ.Mask.Size()
|
||||
|
||||
return onesI < onesJ
|
||||
return cmp.Or(cmp.Compare(prefixA.Bits(), prefixB.Bits()),
|
||||
prefixA.Addr().Compare(prefixB.Addr()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1212,9 +1204,8 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
||||
// Group prefixes by IP version
|
||||
ipv4Prefixes, ipv6Prefixes := splitRoutesByIPVersion(prefixes)
|
||||
|
||||
// Sort prefixes by network address
|
||||
sortRoutesByNetwork(ipv4Prefixes)
|
||||
sortRoutesByNetwork(ipv6Prefixes)
|
||||
sortRoutesByMaskLength(ipv4Prefixes)
|
||||
sortRoutesByMaskLength(ipv6Prefixes)
|
||||
|
||||
// Prepare template data
|
||||
data := struct {
|
||||
|
||||
@@ -1586,6 +1586,61 @@ func TestASDetailPageShowsBuiltInAS(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSortRoutesByMaskLength expects the AS page's prefix lists in mask length
|
||||
// order, shortest first, and in address order within one mask length. Before
|
||||
// the fix they were in address order, so 10.1.0.0/16 came before 11.0.0.0/8
|
||||
// (https://git.eeqj.de/sneak/routewatch/issues/153).
|
||||
func TestSortRoutesByMaskLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prefixes []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "IPv4",
|
||||
prefixes: []string{
|
||||
"100.64.1.0/24", "11.0.0.0/8", "10.1.0.0/16",
|
||||
"20.0.1.0/24", "172.16.0.0/12", "9.0.0.0/8",
|
||||
},
|
||||
want: []string{
|
||||
"9.0.0.0/8", "11.0.0.0/8", "172.16.0.0/12",
|
||||
"10.1.0.0/16", "20.0.1.0/24", "100.64.1.0/24",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6",
|
||||
prefixes: []string{
|
||||
"2001:db8:1::/48", "2001:db8:8000::/33", "2001:db8:0:1::/64",
|
||||
"2001:db8::/48", "2001:db8::/33",
|
||||
},
|
||||
want: []string{
|
||||
"2001:db8::/33", "2001:db8:8000::/33", "2001:db8::/48",
|
||||
"2001:db8:1::/48", "2001:db8:0:1::/64",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
routes := make([]database.LiveRoute, 0, len(tt.prefixes))
|
||||
for _, prefix := range tt.prefixes {
|
||||
routes = append(routes, database.LiveRoute{Prefix: prefix})
|
||||
}
|
||||
|
||||
sortRoutesByMaskLength(routes)
|
||||
|
||||
got := make([]string, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
got = append(got, route.Prefix)
|
||||
}
|
||||
|
||||
if !slices.Equal(got, tt.want) {
|
||||
t.Errorf("%s: order %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoOriginHasNoASAnswers stores a route with no known origin, which has
|
||||
// origin AS 0 (https://git.eeqj.de/sneak/routewatch/issues/75). It expects
|
||||
// /as/0 and /api/v1/as/0 to answer 404.
|
||||
|
||||
Reference in New Issue
Block a user