rmon/main.go

322 lines
6.0 KiB
Go

package main
import (
"bytes"
"fmt"
"html/template"
"io"
"log"
"net"
"net/http"
"os"
"sort"
"sync"
"time"
"golang.org/x/sys/unix"
"github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink"
)
var (
startTime = time.Now()
)
type route struct {
Dst *net.IPNet
Counter uint
UnreachableDuration time.Duration
UnreachableSince *time.Time
}
func (r route) DurationUntilRounded(t time.Time) time.Duration {
return r.DurationUntil(t).Round(time.Second)
}
func (r route) DurationUntil(t time.Time) time.Duration {
ret := r.UnreachableDuration
if r.UnreachableSince != nil {
ret += t.Sub(*r.UnreachableSince)
}
return ret
}
func (r route) Downtime(now time.Time) float64 {
return float64(r.DurationUntil(now)) / float64(now.Sub(startTime)) * 100
}
func (r route) Noise(now time.Time) bool {
return r.Counter <= uint(now.Sub(startTime).Hours())+10 && r.Downtime(now) < 0.5
}
func (r route) String() string {
now := time.Now()
return fmt.Sprintf("%44s %5d %2.3f%% %s", r.Dst, r.Counter, r.Downtime(now), r.DurationUntil(now))
}
type routeStats struct {
sync.Mutex
stats map[string]*route
}
func newRouteStats() *routeStats {
return &routeStats{
stats: make(map[string]*route),
}
}
func DstIPNet(rm rtnetlink.RouteMessage) *net.IPNet {
var zeros int
switch rm.Family {
case unix.AF_INET:
zeros = 32
case unix.AF_INET6:
zeros = 128
default:
fmt.Fprintf(os.Stderr, "unexpected family %q", rm.Family)
}
return &net.IPNet{
IP: rm.Attributes.Dst,
Mask: net.CIDRMask(int(rm.DstLength), zeros),
}
}
func (rs *routeStats) add(rm rtnetlink.RouteMessage) {
rs.Lock()
defer rs.Unlock()
dst := DstIPNet(rm)
key := dst.String()
r := rs.stats[key]
if r == nil {
r = &route{Dst: dst}
}
r.Counter++
if r.UnreachableSince == nil {
t := time.Now()
r.UnreachableSince = &t
}
rs.stats[key] = r
}
func (rs *routeStats) del(rm rtnetlink.RouteMessage) {
rs.Lock()
defer rs.Unlock()
dst := DstIPNet(rm)
key := dst.String()
r := rs.stats[key]
if r == nil {
r = &route{Dst: dst}
return
}
r.UnreachableDuration += time.Since(*r.UnreachableSince)
r.UnreachableSince = nil
rs.stats[key] = r
}
func (rs *routeStats) getAll() []route {
ret := make([]route, 0, len(rs.stats))
rs.Lock()
defer rs.Unlock()
for _, v := range rs.stats {
ret = append(ret, *v)
}
return ret
}
func (rs *routeStats) getLongest() []route {
ret := rs.getAll()
t := time.Now()
sort.Slice(ret, func(i, j int) bool {
id := ret[i].Downtime(t)
jd := ret[j].Downtime(t)
if id == jd ||
(id < 0.5 && jd < 0.5) {
return ret[i].Counter > ret[j].Counter
}
return id > jd
})
return ret
}
func monitor(done <-chan struct{}, rs *routeStats) {
c, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
Groups: 1<<(unix.RTNLGRP_IPV4_ROUTE-1) | 1<<(unix.RTNLGRP_IPV6_ROUTE-1),
})
if err != nil {
log.Fatal(err)
}
go func() {
<-done
c.Close()
}()
if err = c.SetReadBuffer(16 * 1048576); err != nil {
log.Fatal(err)
}
for {
ms, err := c.Receive()
if err != nil {
log.Fatal(err)
}
for _, m := range ms {
rm := rtnetlink.RouteMessage{}
if err = rm.UnmarshalBinary(m.Data); err != nil {
log.Fatal(err)
}
if rm.Type != unix.RTN_UNREACHABLE ||
rm.Protocol != 42 {
continue
}
switch m.Header.Type {
case unix.RTM_NEWROUTE:
rs.add(rm)
case unix.RTM_DELROUTE:
rs.del(rm)
}
}
}
}
func render(w io.Writer, rs []route) error {
const tmplHTML = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FFF Wall-of-not-so-reachable-networks</title>
<style>
html {
margin: 0 auto;
padding: 0;
font-size: 11pt;
}
body {
font-family: sans-serif;
text-align: center;
}
table {
border-spacing: 0;
margin: auto;
}
td {
text-align: right;
padding: 0.1em 2em;
font-family: monospace;
}
.dst {
text-align: left;
}
.unreachable {
color: darkred;
background-color: lightgray;
}
table tr.noise {
display: none;
}
tbody tr:hover {
background-color: silver;
}
#showAll:checked ~ table tr.noise {
color: midnightblue;
display: table-row;
}
</style>
</head>
<body>
<h1>FFF Wall-of-not-so-reachable-networks</h1>
<p>
<table>
<tr><td> Start:</td><td>{{ .Start.Format "2006-01-02 15:04:05" }}</td></tr>
<tr><td>Update:</td><td>{{ .Now.Format "2006-01-02 15:04:05" }}</td></tr>
</table>
</p>
Show All <input id="showAll" type="checkbox">
<table>
<thead>
<tr>
<th>Net</th>
<th colspan="3">Downtime</th>
</tr>
<tr>
<th></th>
<th>Counts</th>
<th>rel</th>
<th>abs</th>
</tr>
</thead>
<tbody>
<tr><td colspan="4"><hr/></td></tr>
{{- $now := .Now -}}
{{- range .R }}
<tr class="
{{- if .UnreachableSince }}unreachable{{end -}}
{{- if .Noise $now }} noise{{end -}}
">
<td class="dst">{{.Dst}}</td>
<td class="counter">{{.Counter}}</td>
<td class="downtime">{{.Downtime $now | printf "%.3f%%"}}</td>
<td class="duration">{{.DurationUntilRounded $now}}</td>
</tr>
{{- end}}
</tbody>
</table>
</body>
</html>
`
data := struct {
Now time.Time
Start time.Time
R []route
}{
time.Now(),
startTime,
rs,
}
t := template.Must(template.New("main").Parse(tmplHTML))
return t.Execute(w, data)
}
func cachedRender(rs *routeStats) http.HandlerFunc {
var m sync.Mutex
var timestamp time.Time
var content []byte
return func(w http.ResponseWriter, r *http.Request) {
m.Lock()
if time.Since(timestamp) > 5*time.Second {
var buf bytes.Buffer
if err := render(&buf, rs.getLongest()); err != nil {
fmt.Fprintln(os.Stderr, err)
}
content = buf.Bytes()
timestamp = time.Now()
}
m.Unlock()
w.Header().Add("Cache-Control", "max-age=5")
if _, err := w.Write(content); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
}
func main() {
rs := newRouteStats()
done := make(chan struct{})
go monitor(done, rs)
log.Fatal(http.ListenAndServe(":8080", cachedRender(rs)))
}