add the diagnostics daemon
This commit is contained in:
parent
7164b27041
commit
518b9f843c
63
cmd/diagd/diagd.go
Normal file
63
cmd/diagd/diagd.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Binary diagd provides automated network diagnostics.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"router7/internal/diag"
|
||||
)
|
||||
|
||||
func dump(w io.Writer, re *diag.EvalResult) {
|
||||
symbol := "✔"
|
||||
if re.Error {
|
||||
symbol = "✘"
|
||||
}
|
||||
fmt.Fprintf(w, "<li>%s %s: %s<ul>", symbol, html.EscapeString(re.Name), html.EscapeString(re.Status))
|
||||
for _, ch := range re.Children {
|
||||
dump(w, ch)
|
||||
}
|
||||
fmt.Fprintf(w, "</ul></li>")
|
||||
}
|
||||
|
||||
func logic() error {
|
||||
const (
|
||||
uplink = "uplink0" /* enp0s31f6 */
|
||||
ip6allrouters = "ff02::2" // no /etc/hosts on gokrazy
|
||||
)
|
||||
m := diag.NewMonitor(diag.Link(uplink).
|
||||
Then(diag.DHCPv4().
|
||||
Then(diag.Ping4Gateway().
|
||||
Then(diag.Ping4("google.ch").
|
||||
Then(diag.TCP4("www.google.ch:80"))))).
|
||||
Then(diag.DHCPv6().
|
||||
Then(diag.Ping6("lan0", "google.ch"))).
|
||||
Then(diag.RouterAdvertisments(uplink).
|
||||
Then(diag.Ping6Gateway().
|
||||
Then(diag.Ping6(uplink, "google.ch").
|
||||
Then(diag.TCP6("www.google.ch:80"))))).
|
||||
Then(diag.Ping6("", ip6allrouters+"%"+uplink)))
|
||||
var mu sync.Mutex
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
re := m.Evaluate()
|
||||
mu.Unlock()
|
||||
fmt.Fprintf(w, `<!DOCTYPE html><style type="text/css">ul { list-style-type: none; }</style><ul>`)
|
||||
dump(w, re)
|
||||
})
|
||||
// TODO: only listen on private IP addresses
|
||||
return http.ListenAndServe(":7733", nil)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if err := logic(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
79
internal/diag/dhcp.go
Normal file
79
internal/diag/dhcp.go
Normal file
@ -0,0 +1,79 @@
|
||||
package diag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
)
|
||||
|
||||
func leaseValid(fn string) (status string, _ error) {
|
||||
var lease struct {
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
}
|
||||
b, err := ioutil.ReadFile(fn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := json.Unmarshal(b, &lease); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if time.Now().After(lease.ValidUntil) {
|
||||
return "", fmt.Errorf("lease expired at %v", lease.ValidUntil)
|
||||
}
|
||||
return fmt.Sprintf("lease valid until %v", lease.ValidUntil), nil
|
||||
}
|
||||
|
||||
type dhcpv4 struct {
|
||||
children []Node
|
||||
}
|
||||
|
||||
func (d *dhcpv4) String() string {
|
||||
return "dhcp4"
|
||||
}
|
||||
|
||||
func (d *dhcpv4) Then(t Node) Node {
|
||||
d.children = append(d.children, t)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *dhcpv4) Children() []Node {
|
||||
return d.children
|
||||
}
|
||||
|
||||
func (d *dhcpv4) Evaluate() (string, error) {
|
||||
return leaseValid("/perm/dhcp4/wire/lease.json")
|
||||
}
|
||||
|
||||
// DHCPv4 returns a Node which succeeds if /perm/dhcp4/wire/lease.json contains
|
||||
// a non-expired DHCPv4 lease.
|
||||
func DHCPv4() Node {
|
||||
return &dhcpv4{}
|
||||
}
|
||||
|
||||
type dhcpv6 struct {
|
||||
children []Node
|
||||
}
|
||||
|
||||
func (d *dhcpv6) String() string {
|
||||
return "dhcp6"
|
||||
}
|
||||
|
||||
func (d *dhcpv6) Then(t Node) Node {
|
||||
d.children = append(d.children, t)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *dhcpv6) Children() []Node {
|
||||
return d.children
|
||||
}
|
||||
|
||||
func (d *dhcpv6) Evaluate() (string, error) {
|
||||
return leaseValid("/perm/dhcp6/wire/lease.json")
|
||||
}
|
||||
|
||||
// DHCPv6 returns a Node which succeeds if /perm/dhcp6/wire/lease.json contains
|
||||
// a non-expired DHCPv6 lease.
|
||||
func DHCPv6() Node {
|
||||
return &dhcpv6{}
|
||||
}
|
54
internal/diag/diag.go
Normal file
54
internal/diag/diag.go
Normal file
@ -0,0 +1,54 @@
|
||||
// Package diag implements network diagnostics.
|
||||
package diag
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Node interface {
|
||||
Then(t Node) Node
|
||||
Children() []Node
|
||||
Evaluate() (status string, _ error)
|
||||
}
|
||||
|
||||
type Monitor struct {
|
||||
root Node
|
||||
}
|
||||
|
||||
func NewMonitor(n Node) *Monitor {
|
||||
return &Monitor{root: n}
|
||||
}
|
||||
|
||||
type EvalResult struct {
|
||||
Name string
|
||||
Error bool
|
||||
Status string
|
||||
Children []*EvalResult
|
||||
}
|
||||
|
||||
func evaluate(n Node, err string) *EvalResult {
|
||||
r := EvalResult{
|
||||
Name: fmt.Sprintf("%s", n),
|
||||
Status: err,
|
||||
Error: err != "",
|
||||
}
|
||||
if r.Status == "" {
|
||||
status, err := n.Evaluate()
|
||||
if err != nil {
|
||||
r.Error = true
|
||||
r.Status = err.Error()
|
||||
} else {
|
||||
r.Status = status
|
||||
}
|
||||
}
|
||||
var childErr string
|
||||
if r.Error {
|
||||
childErr = fmt.Sprintf("dependency %s failed", r.Name)
|
||||
}
|
||||
for _, n := range n.Children() {
|
||||
r.Children = append(r.Children, evaluate(n, childErr))
|
||||
}
|
||||
return &r
|
||||
}
|
||||
|
||||
func (m *Monitor) Evaluate() *EvalResult {
|
||||
return evaluate(m.root, "")
|
||||
}
|
39
internal/diag/diag_test.go
Normal file
39
internal/diag/diag_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
package diag_test
|
||||
|
||||
import (
|
||||
"router7/internal/diag"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestDiagLink(t *testing.T) {
|
||||
if _, err := diag.Link("nonexistant").Evaluate(); err == nil {
|
||||
t.Errorf("Link(nonexistant).Evaluate = nil, want non-nil")
|
||||
}
|
||||
|
||||
if _, err := diag.Link("lo").Evaluate(); err != nil {
|
||||
t.Errorf("Link(lo).Evaluate = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagMonitor(t *testing.T) {
|
||||
m := diag.NewMonitor(diag.Link("nonexistant").
|
||||
Then(diag.DHCPv4()))
|
||||
got := m.Evaluate()
|
||||
want := &diag.EvalResult{
|
||||
Name: "link/nonexistant",
|
||||
Error: true,
|
||||
Status: "Link not found",
|
||||
Children: []*diag.EvalResult{
|
||||
{
|
||||
Name: "dhcp4",
|
||||
Error: true,
|
||||
Status: "dependency link/nonexistant failed",
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("Evaluate(): unexpected result: diff (-got +want):\n%s", diff)
|
||||
}
|
||||
}
|
12
internal/diag/ipv6.go
Normal file
12
internal/diag/ipv6.go
Normal file
@ -0,0 +1,12 @@
|
||||
package diag
|
||||
|
||||
import "net"
|
||||
|
||||
var (
|
||||
global = mustParseCIDR("2000::/3") // RFC 4291
|
||||
)
|
||||
|
||||
func mustParseCIDR(s string) *net.IPNet {
|
||||
_, ipnet, _ := net.ParseCIDR(s)
|
||||
return ipnet
|
||||
}
|
47
internal/diag/link.go
Normal file
47
internal/diag/link.go
Normal file
@ -0,0 +1,47 @@
|
||||
package diag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
type link struct {
|
||||
children []Node
|
||||
ifname string
|
||||
}
|
||||
|
||||
func (l *link) String() string {
|
||||
return "link/" + l.ifname
|
||||
}
|
||||
|
||||
func (l *link) Then(t Node) Node {
|
||||
l.children = append(l.children, t)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *link) Children() []Node {
|
||||
return l.children
|
||||
}
|
||||
|
||||
func (l *link) Evaluate() (string, error) {
|
||||
link, err := netlink.LinkByName(l.ifname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
attrs := link.Attrs()
|
||||
|
||||
// TODO: check RUNNING as well?
|
||||
if attrs.Flags&net.FlagUp == 0 {
|
||||
return "", fmt.Errorf("link %s not UP", l.ifname)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d rx, %d tx", attrs.Statistics.RxPackets, attrs.Statistics.TxPackets), nil
|
||||
}
|
||||
|
||||
// Link returns a Node which succeeds when the specified network interface is in
|
||||
// state UP and RUNNING.
|
||||
func Link(ifname string) Node {
|
||||
return &link{ifname: ifname}
|
||||
}
|
254
internal/diag/ping.go
Normal file
254
internal/diag/ping.go
Normal file
@ -0,0 +1,254 @@
|
||||
package diag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/digineo/go-ping"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func formatRTT(rtt time.Duration) string {
|
||||
return fmt.Sprintf("%.2fms", float64(rtt)/float64(time.Millisecond))
|
||||
}
|
||||
|
||||
type ping4gw struct {
|
||||
children []Node
|
||||
}
|
||||
|
||||
func (d *ping4gw) String() string {
|
||||
return "ping4/<default-gateway>"
|
||||
}
|
||||
|
||||
func (d *ping4gw) Then(t Node) Node {
|
||||
d.children = append(d.children, t)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *ping4gw) Children() []Node {
|
||||
return d.children
|
||||
}
|
||||
|
||||
func defaultIPv4Gateway() (string, error) {
|
||||
rl, err := netlink.RouteGet(net.ParseIP("8.8.8.8"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if got, want := len(rl), 1; got != want {
|
||||
return "", fmt.Errorf("unexpected number of default routes: got %d, want %d", got, want)
|
||||
}
|
||||
r := rl[0]
|
||||
|
||||
return r.Gw.String(), nil
|
||||
}
|
||||
|
||||
func (d *ping4gw) Evaluate() (string, error) {
|
||||
const timeout = 1 * time.Second
|
||||
gw, err := defaultIPv4Gateway()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
addr, err := net.ResolveIPAddr("ip4", gw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p, err := ping.New("0.0.0.0", "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rtt, err := p.Ping(addr, timeout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
//return fmt.Errorf("%s did not respond within %v", gw, timeout)
|
||||
}
|
||||
return formatRTT(rtt), nil
|
||||
}
|
||||
|
||||
// Ping4Gateway returns a Node which succeeds when the default gateway responds
|
||||
// to an ICMPv4 ping.
|
||||
func Ping4Gateway() Node {
|
||||
return &ping4gw{}
|
||||
}
|
||||
|
||||
type ping4 struct {
|
||||
children []Node
|
||||
addr string
|
||||
}
|
||||
|
||||
func (d *ping4) String() string {
|
||||
return "ping4/" + d.addr
|
||||
}
|
||||
|
||||
func (d *ping4) Then(t Node) Node {
|
||||
d.children = append(d.children, t)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *ping4) Children() []Node {
|
||||
return d.children
|
||||
}
|
||||
|
||||
func (d *ping4) Evaluate() (string, error) {
|
||||
const timeout = 1 * time.Second
|
||||
addr, err := net.ResolveIPAddr("ip4", d.addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p, err := ping.New("0.0.0.0", "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rtt, err := p.Ping(addr, timeout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
//return fmt.Errorf("%s did not respond within %v", gw, timeout)
|
||||
}
|
||||
return formatRTT(rtt), nil
|
||||
}
|
||||
|
||||
// Ping4 returns a Node which succeeds when the specified address responds to an
|
||||
// ICMPv4 ping.
|
||||
func Ping4(addr string) Node {
|
||||
return &ping4{addr: addr}
|
||||
}
|
||||
|
||||
type ping6gw struct {
|
||||
children []Node
|
||||
}
|
||||
|
||||
func (d *ping6gw) String() string {
|
||||
return "ping6gw/<default-gateway>"
|
||||
}
|
||||
|
||||
func (d *ping6gw) Then(t Node) Node {
|
||||
d.children = append(d.children, t)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *ping6gw) Children() []Node {
|
||||
return d.children
|
||||
}
|
||||
|
||||
func defaultIPv6Gateway() (string, error) {
|
||||
rl, err := netlink.RouteGet(net.IPv6zero)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if got, want := len(rl), 1; got != want {
|
||||
return "", fmt.Errorf("unexpected number of default routes: got %d, want %d", got, want)
|
||||
}
|
||||
r := rl[0]
|
||||
|
||||
iface, err := net.InterfaceByIndex(r.LinkIndex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.Gw.String() + "%" + iface.Name, nil
|
||||
}
|
||||
|
||||
func (d *ping6gw) Evaluate() (string, error) {
|
||||
const timeout = 1 * time.Second
|
||||
gw, err := defaultIPv6Gateway()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
addr, err := net.ResolveIPAddr("ip6", gw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p, err := ping.New("", "::")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rtt, err := p.Ping(addr, timeout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
//return fmt.Errorf("%s did not respond within %v", gw, timeout)
|
||||
}
|
||||
return formatRTT(rtt), nil
|
||||
}
|
||||
|
||||
// Ping6Gateway returns a Node which succeeds when the default gateway responds
|
||||
// to an ICMPv6 ping.
|
||||
func Ping6Gateway() Node {
|
||||
return &ping6gw{}
|
||||
}
|
||||
|
||||
type ping6 struct {
|
||||
children []Node
|
||||
addr string
|
||||
ifname string
|
||||
}
|
||||
|
||||
func (d *ping6) String() string {
|
||||
if d.ifname == "" {
|
||||
return "ping6/" + d.addr
|
||||
}
|
||||
return fmt.Sprintf("ping6/%s→%s", d.ifname, d.addr)
|
||||
}
|
||||
|
||||
func (d *ping6) Then(t Node) Node {
|
||||
d.children = append(d.children, t)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *ping6) Children() []Node {
|
||||
return d.children
|
||||
}
|
||||
|
||||
func (d *ping6) Evaluate() (string, error) {
|
||||
const timeout = 1 * time.Second
|
||||
addr, err := net.ResolveIPAddr("ip6", d.addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bind6 := "::"
|
||||
if d.ifname != "" {
|
||||
iface, err := net.InterfaceByName(d.ifname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if ipnet.IP.To4() != nil {
|
||||
continue // skip IPv4 addresses
|
||||
}
|
||||
|
||||
if !global.Contains(ipnet.IP) {
|
||||
continue // skip local IPv6 addresses
|
||||
}
|
||||
|
||||
bind6 = ipnet.IP.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
p, err := ping.New("", bind6)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rtt, err := p.Ping(addr, timeout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
//return fmt.Errorf("%s did not respond within %v", gw, timeout)
|
||||
}
|
||||
return formatRTT(rtt), nil
|
||||
}
|
||||
|
||||
// Ping6 returns a Node which succeeds when the specified address responds to an
|
||||
// ICMPv6 ping.
|
||||
func Ping6(ifname, addr string) Node {
|
||||
return &ping6{
|
||||
ifname: ifname,
|
||||
addr: addr,
|
||||
}
|
||||
}
|
71
internal/diag/ra6.go
Normal file
71
internal/diag/ra6.go
Normal file
@ -0,0 +1,71 @@
|
||||
package diag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type ra6 struct {
|
||||
children []Node
|
||||
ifname string
|
||||
}
|
||||
|
||||
func (d *ra6) String() string {
|
||||
return "ra6/" + d.ifname
|
||||
}
|
||||
|
||||
func (d *ra6) Then(t Node) Node {
|
||||
d.children = append(d.children, t)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *ra6) Children() []Node {
|
||||
return d.children
|
||||
}
|
||||
|
||||
func isEUI64(ip net.IP) bool {
|
||||
if ip.To16() == nil {
|
||||
return false
|
||||
}
|
||||
ip = ip.To16()
|
||||
return ip[11] == 0xff && ip[12] == 0xfe
|
||||
}
|
||||
|
||||
func (d *ra6) Evaluate() (string, error) {
|
||||
iface, err := net.InterfaceByName(d.ifname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var first string
|
||||
for _, addr := range addrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ones, _ := ipnet.Mask.Size(); ones != 64 {
|
||||
continue
|
||||
}
|
||||
if !global.Contains(ipnet.IP) {
|
||||
continue // skip local IPv6 addresses
|
||||
}
|
||||
if !isEUI64(ipnet.IP) {
|
||||
continue // skip non-autoconf addresses (e.g. DHCPv6 temporary IP)
|
||||
}
|
||||
|
||||
first = ipnet.String()
|
||||
}
|
||||
if first == "" {
|
||||
return "", fmt.Errorf("no SLAAC address found")
|
||||
}
|
||||
return first, nil
|
||||
}
|
||||
|
||||
// RouterAdvertisments returns a Node which succeeds if the specified interface
|
||||
// obtained at least one address from IPv6 router advertisments.
|
||||
func RouterAdvertisments(ifname string) Node {
|
||||
return &ra6{ifname: ifname}
|
||||
}
|
71
internal/diag/tcp.go
Normal file
71
internal/diag/tcp.go
Normal file
@ -0,0 +1,71 @@
|
||||
package diag
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type tcp4 struct {
|
||||
children []Node
|
||||
addr string
|
||||
}
|
||||
|
||||
func (d *tcp4) String() string {
|
||||
return "tcp4/" + d.addr
|
||||
}
|
||||
|
||||
func (d *tcp4) Then(t Node) Node {
|
||||
d.children = append(d.children, t)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *tcp4) Children() []Node {
|
||||
return d.children
|
||||
}
|
||||
|
||||
func (d *tcp4) Evaluate() (string, error) {
|
||||
conn, err := net.Dial("tcp4", d.addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
return "connection established", nil
|
||||
}
|
||||
|
||||
// TCP4 returns a Node which succeeds when the specified address accepts a TCPv4
|
||||
// connection.
|
||||
func TCP4(addr string) Node {
|
||||
return &tcp4{addr: addr}
|
||||
}
|
||||
|
||||
type tcp6 struct {
|
||||
children []Node
|
||||
addr string
|
||||
}
|
||||
|
||||
func (d *tcp6) String() string {
|
||||
return "tcp6/" + d.addr
|
||||
}
|
||||
|
||||
func (d *tcp6) Then(t Node) Node {
|
||||
d.children = append(d.children, t)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *tcp6) Children() []Node {
|
||||
return d.children
|
||||
}
|
||||
|
||||
func (d *tcp6) Evaluate() (string, error) {
|
||||
conn, err := net.Dial("tcp6", d.addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
return "connection established", nil
|
||||
}
|
||||
|
||||
// TCP6 returns a Node which succeeds when the specified address accepts a TCPv6
|
||||
// connection.
|
||||
func TCP6(addr string) Node {
|
||||
return &tcp6{addr: addr}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user