From d9ce3912ed3b5eb235715fa3283bf5e35407d805 Mon Sep 17 00:00:00 2001
From: lemmi
Date: Tue, 21 Jan 2020 22:44:28 +0100
Subject: [PATCH] init
---
.gitignore | 26 +++++
main.go | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 311 insertions(+)
create mode 100644 .gitignore
create mode 100644 main.go
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..06ee5b5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+rmon
+# Created by https://www.gitignore.io/api/go
+# Edit at https://www.gitignore.io/?templates=go
+
+### Go ###
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+### Go Patch ###
+/vendor/
+/Godeps/
+
+# End of https://www.gitignore.io/api/go
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..0682670
--- /dev/null
+++ b/main.go
@@ -0,0 +1,285 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "sort"
+ "sync"
+ "time"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/vishvananda/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()) && r.Downtime(now) < 0.01
+}
+
+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 (rs *routeStats) update(ru netlink.RouteUpdate) {
+ rs.Lock()
+ defer rs.Unlock()
+
+ switch ru.Type {
+ case unix.RTM_NEWROUTE:
+ rs.add(ru)
+ case unix.RTM_DELROUTE:
+ rs.del(ru)
+ default:
+ fmt.Fprintf(os.Stderr, "Unknown route type %d\n", ru.Type)
+ }
+}
+
+func (rs *routeStats) add(ru netlink.RouteUpdate) {
+ key := ru.Route.Dst.String()
+ r := rs.stats[key]
+ if r == nil {
+ r = &route{Dst: ru.Route.Dst}
+ }
+ r.Counter++
+ if r.UnreachableSince == nil {
+ t := time.Now()
+ r.UnreachableSince = &t
+ }
+ rs.stats[key] = r
+}
+
+func (rs *routeStats) del(ru netlink.RouteUpdate) {
+ key := ru.Route.Dst.String()
+ r := rs.stats[key]
+ if r == nil {
+ r = &route{Dst: ru.Route.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 {
+ return ret[i].DurationUntil(t) > ret[j].DurationUntil(t)
+ })
+
+ return ret
+}
+
+func monitor(done <-chan struct{}, rs *routeStats) {
+ rups := make(chan netlink.RouteUpdate, 64)
+ opts := netlink.RouteSubscribeOptions{
+ ErrorCallback: func(err error) {
+ fmt.Fprintf(os.Stderr, "%s\n", err, err)
+ },
+ }
+ if err := netlink.RouteSubscribeWithOptions(rups, done, opts); err != nil {
+ log.Fatal(err)
+ }
+
+ for ru := range rups {
+ if ru.Route.Type != unix.RTN_UNREACHABLE {
+ continue
+ }
+ // show unly babel routes
+ if ru.Route.Protocol != 42 {
+ continue
+ }
+ rs.update(ru)
+ }
+}
+
+func render(w io.Writer, rs []route) error {
+ const tmplHTML = `
+
+
+
+
+ FFF Wall-of-not-so-reachable-networks
+
+
+
+ FFF Wall-of-not-so-reachable-networks
+
+
+
+ Start: | {{ .Start.Format "2006-01-02 15:04:05" }} |
+ Update: | {{ .Now.Format "2006-01-02 15:04:05" }} |
+
+
+
+ Show All
+
+
+
+
+ Net |
+ Downtime |
+
+
+ |
+ Counts |
+ rel |
+ abs |
+
+
+
+
|
+ {{- $now := .Now -}}
+ {{- range .R }}
+
+ {{.Dst}} |
+ {{.Counter}} |
+ {{.Downtime $now | printf "%.3f%%"}} |
+ {{.DurationUntilRounded $now}} |
+
+ {{- end}}
+
+
+
+
+`
+
+ 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)))
+}