List an AS's prefixes by mask length, shortest first (closes #153) #181
@@ -33,6 +33,9 @@ tested.
|
|||||||
|
|
||||||
# Completed Steps
|
# 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
|
- 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
|
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
|
not seen more messages for three refreshes shows "stale" and the time since
|
||||||
|
|||||||
+12
-21
@@ -1,7 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -1028,24 +1029,15 @@ func splitRoutesByIPVersion(
|
|||||||
return ipv4Routes, ipv6Routes
|
return ipv4Routes, ipv6Routes
|
||||||
}
|
}
|
||||||
|
|
||||||
// sortRoutesByNetwork sorts routes by network address, then by mask length.
|
// sortRoutesByMaskLength sorts routes by mask length, shortest first, and
|
||||||
func sortRoutesByNetwork(routes []database.LiveRoute) {
|
// routes with the same mask length by network address.
|
||||||
sort.Slice(routes, func(i, j int) bool {
|
func sortRoutesByMaskLength(routes []database.LiveRoute) {
|
||||||
// Parse the prefixes to compare network addresses
|
slices.SortFunc(routes, func(a, b database.LiveRoute) int {
|
||||||
ipI, netI, _ := net.ParseCIDR(routes[i].Prefix)
|
prefixA, _ := netip.ParsePrefix(a.Prefix)
|
||||||
ipJ, netJ, _ := net.ParseCIDR(routes[j].Prefix)
|
prefixB, _ := netip.ParsePrefix(b.Prefix)
|
||||||
|
|
||||||
// Compare by network address first
|
return cmp.Or(cmp.Compare(prefixA.Bits(), prefixB.Bits()),
|
||||||
cmp := bytes.Compare(ipI.To16(), ipJ.To16())
|
prefixA.Addr().Compare(prefixB.Addr()))
|
||||||
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1212,9 +1204,8 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
|||||||
// Group prefixes by IP version
|
// Group prefixes by IP version
|
||||||
ipv4Prefixes, ipv6Prefixes := splitRoutesByIPVersion(prefixes)
|
ipv4Prefixes, ipv6Prefixes := splitRoutesByIPVersion(prefixes)
|
||||||
|
|
||||||
// Sort prefixes by network address
|
sortRoutesByMaskLength(ipv4Prefixes)
|
||||||
sortRoutesByNetwork(ipv4Prefixes)
|
sortRoutesByMaskLength(ipv6Prefixes)
|
||||||
sortRoutesByNetwork(ipv6Prefixes)
|
|
||||||
|
|
||||||
// Prepare template data
|
// Prepare template data
|
||||||
data := struct {
|
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
|
// TestNoOriginHasNoASAnswers stores a route with no known origin, which has
|
||||||
// origin AS 0 (https://git.eeqj.de/sneak/routewatch/issues/75). It expects
|
// origin AS 0 (https://git.eeqj.de/sneak/routewatch/issues/75). It expects
|
||||||
// /as/0 and /api/v1/as/0 to answer 404.
|
// /as/0 and /api/v1/as/0 to answer 404.
|
||||||
|
|||||||
Reference in New Issue
Block a user