Show figures of the routing table on the home page (closes #154)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The home page gets cards for the newest and oldest kept route, the newest and oldest updated AS, the AS with the most prefixes, the ASes with a single prefix and fifteen well-known networks. Two more cards count the bogon announcements and the prefixes with several origin ASes. They link to the new pages /bogons and /multiple-origins, which list up to 1,000 entries each. The new package internal/bogon tells reserved prefixes and AS numbers, from the IANA special-purpose and address space registries. Table.Overview walks the table and lets the read lock go every 10,000 prefixes. The server works the cards out once a minute and serves that copy. A new index on asns(last_seen) finds the newest and oldest updated AS. Model: opus-5-5
This commit was merged in pull request #196.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bogon Announcements - RouteWatch</title>
|
||||
<style nonce="{{.CSPNonce}}">
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Navbar styles */
|
||||
.navbar {
|
||||
background: #2c3e50;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.navbar-brand a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
.navbar-brand a:hover {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
.navbar-brand .by {
|
||||
font-weight: normal;
|
||||
color: #95a5a6;
|
||||
font-size: 14px;
|
||||
}
|
||||
.navbar-brand .author {
|
||||
color: #3498db;
|
||||
font-weight: normal;
|
||||
}
|
||||
.navbar-brand .author:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.navbar-links {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.navbar-links a {
|
||||
color: #ecf0f1;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.navbar-links a:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.main-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.prefix-link {
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Mono', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<a href="/">routewatch</a>
|
||||
<span class="by">by</span>
|
||||
<a href="https://sneak.berlin" class="author">@sneak</a>
|
||||
</div>
|
||||
<div class="navbar-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/status">Status</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<h1>Bogon Announcements</h1>
|
||||
{{ with .Cards }}
|
||||
<p class="subtitle">
|
||||
Bogon announcements: {{ .BogonCount }}.
|
||||
A bogon announcement is a route that announces a reserved prefix, or that comes from a reserved AS number.
|
||||
{{ if lt (len .Bogons) .BogonCount }}The first {{ len .Bogons }} are listed, in prefix order.{{ end }}
|
||||
The list is worked out once a minute.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Prefix</th>
|
||||
<th>Origin AS</th>
|
||||
<th>Peers</th>
|
||||
<th>Why it is a bogon</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Bogons }}
|
||||
<tr>
|
||||
<td><a href="{{ prefixURL .Prefix.String }}" class="prefix-link">{{ .Prefix }}</a></td>
|
||||
<td>{{ if .OriginASN }}<a href="/as/{{ .OriginASN }}">AS{{ .OriginASN }}</a>{{ else }}No known origin{{ end }}</td>
|
||||
<td>{{ .Peers }}</td>
|
||||
<td>{{ range $i, $reason := .Reasons }}{{ if $i }}; {{ end }}{{ $reason }}{{ end }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ else }}
|
||||
<p class="subtitle">The list is being worked out. Reload the page in a minute.</p>
|
||||
{{ end }}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -276,6 +276,52 @@
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* Cards with figures of the routing table, worked out once a minute */
|
||||
#table-cards p,
|
||||
#well-known p {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
#table-cards a,
|
||||
#well-known a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
#table-cards a:hover,
|
||||
#well-known a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
#table-cards .card-figure {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#table-cards .card-detail,
|
||||
#table-cards .card-time {
|
||||
color: #7f8c8d;
|
||||
font-size: 12px;
|
||||
}
|
||||
#table-cards .prefix {
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Mono', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
#well-known {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
#well-known table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
#well-known th,
|
||||
#well-known td {
|
||||
text-align: left;
|
||||
padding: 8px 4px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
#well-known th {
|
||||
color: #7f8c8d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
@@ -381,6 +427,109 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ with .Cards }}
|
||||
<div class="search-section" id="table-cards">
|
||||
<div class="search-card">
|
||||
<h2>Newest Updated Route</h2>
|
||||
{{ with .NewestRoute }}
|
||||
<p><a href="{{ prefixURL .Prefix.String }}" class="prefix">{{ .Prefix }}</a> from {{ if .OriginASN }}<a href="/as/{{ .OriginASN }}">AS{{ .OriginASN }}</a>{{ else }}no known origin{{ end }}</p>
|
||||
<p class="card-time">{{ .Time.UTC.Format "2006-01-02 15:04:05" }} UTC, {{ timeSince .Time }}</p>
|
||||
{{ else }}
|
||||
<p>The routing table keeps no route yet.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="search-card">
|
||||
<h2>Oldest Updated Route</h2>
|
||||
{{ with .OldestRoute }}
|
||||
<p><a href="{{ prefixURL .Prefix.String }}" class="prefix">{{ .Prefix }}</a> from {{ if .OriginASN }}<a href="/as/{{ .OriginASN }}">AS{{ .OriginASN }}</a>{{ else }}no known origin{{ end }}</p>
|
||||
<p class="card-time">{{ .Time.UTC.Format "2006-01-02 15:04:05" }} UTC, {{ timeSince .Time }}</p>
|
||||
{{ else }}
|
||||
<p>The routing table keeps no route yet.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="search-card">
|
||||
<h2>Newest Updated AS</h2>
|
||||
{{ with .NewestAS }}
|
||||
<p><a href="/as/{{ .ASN }}">AS{{ .ASN }}</a> {{ .Handle }}</p>
|
||||
<p class="card-detail">{{ .Description }}</p>
|
||||
<p class="card-time">Last seen {{ .LastSeen.UTC.Format "2006-01-02 15:04:05" }} UTC, {{ timeSince .LastSeen }}</p>
|
||||
{{ else }}
|
||||
<p>No AS has been seen yet.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="search-card">
|
||||
<h2>Oldest Updated AS</h2>
|
||||
{{ with .OldestAS }}
|
||||
<p><a href="/as/{{ .ASN }}">AS{{ .ASN }}</a> {{ .Handle }}</p>
|
||||
<p class="card-detail">{{ .Description }}</p>
|
||||
<p class="card-time">Last seen {{ .LastSeen.UTC.Format "2006-01-02 15:04:05" }} UTC, {{ timeSince .LastSeen }}</p>
|
||||
{{ else }}
|
||||
<p>No AS has been seen yet.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="search-card">
|
||||
<h2>AS with the Most Prefixes</h2>
|
||||
{{ if .MostPrefixes }}
|
||||
<p><a href="/as/{{ .MostPrefixesASN }}">AS{{ .MostPrefixesASN }}</a></p>
|
||||
<p>Prefixes it originates: {{ .MostPrefixes }}</p>
|
||||
{{ else }}
|
||||
<p>No AS originates a prefix yet.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="search-card">
|
||||
<h2>ASes with the Fewest Prefixes</h2>
|
||||
<p>ASes that originate a single prefix: {{ .SinglePrefixASes }}</p>
|
||||
{{ with .NewestSinglePrefix }}
|
||||
<p>The most recently updated of them: <a href="/as/{{ .OriginASN }}">AS{{ .OriginASN }}</a>, with <a href="{{ prefixURL .Prefix.String }}" class="prefix">{{ .Prefix }}</a></p>
|
||||
<p class="card-time">{{ .Time.UTC.Format "2006-01-02 15:04:05" }} UTC, {{ timeSince .Time }}</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="search-card">
|
||||
<h2>Bogon Announcements</h2>
|
||||
<p class="card-figure"><a href="/bogons">{{ .BogonCount }}</a></p>
|
||||
<p>Routes that announce a reserved prefix, or that come from a reserved AS number</p>
|
||||
</div>
|
||||
|
||||
<div class="search-card">
|
||||
<h2>Prefixes with Several Origins</h2>
|
||||
<p class="card-figure"><a href="/multiple-origins">{{ .MultipleOriginCount }}</a></p>
|
||||
<p>Prefixes announced by more than one origin AS</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-card" id="well-known">
|
||||
<h2>Well-Known Networks</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Network</th>
|
||||
<th>Prefixes</th>
|
||||
<th>Last Update</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .WellKnownNetworks }}
|
||||
<tr>
|
||||
<td><a href="/as/{{ .ASN }}">AS{{ .ASN }}</a> {{ .Name }}</td>
|
||||
<td>{{ .Prefixes }}</td>
|
||||
<td>{{ with .Newest }}{{ timeSince . }}{{ else }}no route{{ end }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="search-card" id="well-known">
|
||||
<p>The figures of the routing table are being worked out. Reload the page in a minute.</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="search-card" id="ip-api">
|
||||
<h2>IP Information API</h2>
|
||||
<p>The API answers what RouteWatch knows about an IP address, such as its AS number and country. <code>/ip/</code> with no address answers for the caller's own address.</p>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Prefixes with Several Origins - RouteWatch</title>
|
||||
<style nonce="{{.CSPNonce}}">
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Navbar styles */
|
||||
.navbar {
|
||||
background: #2c3e50;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.navbar-brand a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
.navbar-brand a:hover {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
.navbar-brand .by {
|
||||
font-weight: normal;
|
||||
color: #95a5a6;
|
||||
font-size: 14px;
|
||||
}
|
||||
.navbar-brand .author {
|
||||
color: #3498db;
|
||||
font-weight: normal;
|
||||
}
|
||||
.navbar-brand .author:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.navbar-links {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.navbar-links a {
|
||||
color: #ecf0f1;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.navbar-links a:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.main-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
vertical-align: top;
|
||||
}
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.prefix-link {
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Mono', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
.origins {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.peers {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<a href="/">routewatch</a>
|
||||
<span class="by">by</span>
|
||||
<a href="https://sneak.berlin" class="author">@sneak</a>
|
||||
</div>
|
||||
<div class="navbar-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/status">Status</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<h1>Prefixes with Several Origin ASes</h1>
|
||||
{{ with .Cards }}
|
||||
<p class="subtitle">
|
||||
Prefixes announced by more than one origin AS: {{ .MultipleOriginCount }}.
|
||||
Such a prefix can be a hijack.
|
||||
{{ if lt (len .MultipleOrigins) .MultipleOriginCount }}The first {{ len .MultipleOrigins }} are listed, in prefix order.{{ end }}
|
||||
The list is worked out once a minute.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Prefix</th>
|
||||
<th>Origin ASes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .MultipleOrigins }}
|
||||
<tr>
|
||||
<td><a href="{{ prefixURL .Prefix.String }}" class="prefix-link">{{ .Prefix }}</a></td>
|
||||
<td>
|
||||
<ul class="origins">
|
||||
{{ range .Origins }}
|
||||
<li>{{ if .ASN }}<a href="/as/{{ .ASN }}">AS{{ .ASN }}</a>{{ else }}No known origin{{ end }} <span class="peers">{{ .Peers }} {{ if eq .Peers 1 }}peer{{ else }}peers{{ end }}</span></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ else }}
|
||||
<p class="subtitle">The list is being worked out. Reload the page in a minute.</p>
|
||||
{{ end }}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -27,6 +27,12 @@ var prefixDetailHTML string
|
||||
//go:embed prefix_length.html
|
||||
var prefixLengthHTML string
|
||||
|
||||
//go:embed bogons.html
|
||||
var bogonsHTML string
|
||||
|
||||
//go:embed multiple_origins.html
|
||||
var multipleOriginsHTML string
|
||||
|
||||
// Templates contains all parsed templates
|
||||
type Templates struct {
|
||||
// Index is the template for the home page
|
||||
@@ -39,6 +45,11 @@ type Templates struct {
|
||||
PrefixDetail *template.Template
|
||||
// PrefixLength is the template for displaying prefixes by length
|
||||
PrefixLength *template.Template
|
||||
// Bogons is the template of the page that lists the bogon announcements
|
||||
Bogons *template.Template
|
||||
// MultipleOrigins is the template of the page that lists the prefixes
|
||||
// announced by more than one origin AS
|
||||
MultipleOrigins *template.Template
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -116,6 +127,18 @@ func initTemplates() {
|
||||
if err != nil {
|
||||
panic("failed to parse prefix length template: " + err.Error())
|
||||
}
|
||||
|
||||
defaultTemplates.Bogons, err = template.New("bogons").
|
||||
Funcs(funcs).Parse(bogonsHTML)
|
||||
if err != nil {
|
||||
panic("failed to parse bogons template: " + err.Error())
|
||||
}
|
||||
|
||||
defaultTemplates.MultipleOrigins, err = template.New("multipleOrigins").
|
||||
Funcs(funcs).Parse(multipleOriginsHTML)
|
||||
if err != nil {
|
||||
panic("failed to parse multiple origins template: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the singleton Templates instance
|
||||
@@ -149,3 +172,14 @@ func PrefixDetailTemplate() *template.Template {
|
||||
func PrefixLengthTemplate() *template.Template {
|
||||
return Get().PrefixLength
|
||||
}
|
||||
|
||||
// BogonsTemplate returns the parsed template of the bogon announcements page
|
||||
func BogonsTemplate() *template.Template {
|
||||
return Get().Bogons
|
||||
}
|
||||
|
||||
// MultipleOriginsTemplate returns the parsed template of the page of prefixes
|
||||
// announced by more than one origin AS
|
||||
func MultipleOriginsTemplate() *template.Template {
|
||||
return Get().MultipleOrigins
|
||||
}
|
||||
|
||||
@@ -12,13 +12,17 @@ import (
|
||||
)
|
||||
|
||||
// renderPage renders a page with the data every page needs, and the visitor's
|
||||
// address, which the home page also needs.
|
||||
// address and the routing table's cards, which the home page also needs. The
|
||||
// cards are left out, as before the server first works them out.
|
||||
func renderPage(t *testing.T, tmpl *template.Template) string {
|
||||
t.Helper()
|
||||
|
||||
var page bytes.Buffer
|
||||
|
||||
err := tmpl.Execute(&page, struct{ CSPNonce, ClientIP string }{})
|
||||
err := tmpl.Execute(&page, struct {
|
||||
CSPNonce, ClientIP string
|
||||
Cards any
|
||||
}{})
|
||||
if err != nil {
|
||||
t.Fatalf("rendering the page: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user