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()) && r.Downtime(now) < 0.1 } 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 { return ret[i].DurationUntil(t) > ret[j].DurationUntil(t) }) 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.Println(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 = ` 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 {{- $now := .Now -}} {{- range .R }} {{- end}}
Net Downtime
Counts rel abs

{{.Dst}} {{.Counter}} {{.Downtime $now | printf "%.3f%%"}} {{.DurationUntilRounded $now}}
` 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))) }