List an AS's prefixes by mask length, shortest first (closes #153)
check / check (push) Waiting to run
check / check (push) Waiting to run
The AS page sorted its IPv4 and IPv6 prefix lists by network address,
then by mask length. It now sorts them by mask length, shortest first,
and by network address within one mask length, so a /16 comes before a
/24. The sort function is renamed from sortRoutesByNetwork to
sortRoutesByMaskLength to say so.
The handler of /api/v1/as/{asn} does not sort its lists, so it is
unchanged.
Model: opus-5-5
This commit is contained in:
@@ -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