push
This commit is contained in:
commit
1d167420c3
89 changed files with 10707 additions and 0 deletions
329
cmd/yggdrasil/dashboard.go
Normal file
329
cmd/yggdrasil/dashboard.go
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gologme/log"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/address"
|
||||
)
|
||||
|
||||
//go:embed dashboard.html
|
||||
var dashboardHTML embed.FS
|
||||
|
||||
type dashboardServer struct {
|
||||
node *node
|
||||
logger *log.Logger
|
||||
username string
|
||||
password string
|
||||
readOnly bool
|
||||
banned map[string]struct{}
|
||||
mu sync.RWMutex
|
||||
http *http.Server
|
||||
}
|
||||
|
||||
type dashboardStatus struct {
|
||||
Self any `json:"self"`
|
||||
Peers []dashboardPeer `json:"peers"`
|
||||
Sessions []dashboardFlow `json:"sessions"`
|
||||
Paths []dashboardPath `json:"paths"`
|
||||
Tree []dashboardTree `json:"tree"`
|
||||
Banned []string `json:"banned"`
|
||||
Now time.Time `json:"now"`
|
||||
}
|
||||
|
||||
type dashboardPeer struct {
|
||||
URI string `json:"uri"`
|
||||
Remote string `json:"remote"`
|
||||
Up bool `json:"up"`
|
||||
Inbound bool `json:"inbound"`
|
||||
IP string `json:"ip"`
|
||||
Uptime string `json:"uptime"`
|
||||
RTT string `json:"rtt"`
|
||||
RX uint64 `json:"rx"`
|
||||
TX uint64 `json:"tx"`
|
||||
RXRate uint64 `json:"rx_rate"`
|
||||
TXRate uint64 `json:"tx_rate"`
|
||||
Priority uint8 `json:"priority"`
|
||||
Cost uint64 `json:"cost"`
|
||||
Banned bool `json:"banned"`
|
||||
}
|
||||
|
||||
type dashboardFlow struct {
|
||||
IP string `json:"ip"`
|
||||
RX uint64 `json:"rx"`
|
||||
TX uint64 `json:"tx"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
type dashboardPath struct {
|
||||
IP string `json:"ip"`
|
||||
Path []uint64 `json:"path"`
|
||||
}
|
||||
|
||||
type dashboardTree struct {
|
||||
IP string `json:"ip"`
|
||||
Parent string `json:"parent"`
|
||||
}
|
||||
|
||||
func startDashboard(n *node, logger *log.Logger, listenAddr, username, password string, readOnly bool) (*dashboardServer, error) {
|
||||
if strings.TrimSpace(listenAddr) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
d := &dashboardServer{
|
||||
node: n,
|
||||
logger: logger,
|
||||
username: username,
|
||||
password: password,
|
||||
readOnly: readOnly,
|
||||
banned: make(map[string]struct{}),
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", d.handleIndex)
|
||||
mux.HandleFunc("/api/status", d.handleStatus)
|
||||
mux.HandleFunc("/api/peer/traffic", d.handlePeerTraffic)
|
||||
mux.HandleFunc("/api/peer/ban", d.handlePeerBan)
|
||||
|
||||
d.http = &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: d.withAuth(mux),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
ln, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
if err := d.http.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
d.logger.Errorf("dashboard failed: %v", err)
|
||||
}
|
||||
}()
|
||||
if readOnly {
|
||||
logger.Infof("Public dashboard listening on http://%s", ln.Addr().String())
|
||||
return d, nil
|
||||
}
|
||||
logger.Infof("Dashboard listening on http://%s", ln.Addr().String())
|
||||
if username == "" || password == "" {
|
||||
logger.Warnln("Dashboard authentication disabled; set both -dashboard-user and -dashboard-password to secure it")
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dashboardServer) stop() {
|
||||
if d == nil || d.http == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = d.http.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) withAuth(next http.Handler) http.Handler {
|
||||
if d.username == "" || d.password == "" {
|
||||
return next
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
u, p, ok := r.BasicAuth()
|
||||
if !ok || subtle.ConstantTimeCompare([]byte(u), []byte(d.username)) != 1 || subtle.ConstantTimeCompare([]byte(p), []byte(d.password)) != 1 {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="kaya"`)
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *dashboardServer) handleIndex(w http.ResponseWriter, _ *http.Request) {
|
||||
bs, err := dashboardHTML.ReadFile("dashboard.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write(bs)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) handleStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
status := dashboardStatus{Now: time.Now()}
|
||||
self := d.node.core.GetSelf()
|
||||
subnet := d.node.core.Subnet()
|
||||
status.Self = map[string]any{
|
||||
"public_key": hex.EncodeToString(self.Key),
|
||||
"ip_address": d.node.core.Address().String(),
|
||||
"subnet": (&subnet).String(),
|
||||
"routing_entries": self.RoutingEntries,
|
||||
}
|
||||
|
||||
for _, p := range d.node.core.GetPeers() {
|
||||
uriKey := canonicalPeerKey(p.URI, "")
|
||||
status.Peers = append(status.Peers, dashboardPeer{
|
||||
URI: p.URI,
|
||||
Remote: peerHostFromURI(p.URI),
|
||||
Up: p.Up,
|
||||
Inbound: p.Inbound,
|
||||
IP: keyToIP(p.Key),
|
||||
Uptime: p.Uptime.String(),
|
||||
RTT: p.Latency.String(),
|
||||
RX: p.RXBytes,
|
||||
TX: p.TXBytes,
|
||||
RXRate: p.RXRate,
|
||||
TXRate: p.TXRate,
|
||||
Priority: p.Priority,
|
||||
Cost: p.Cost,
|
||||
Banned: d.isBanned(uriKey),
|
||||
})
|
||||
}
|
||||
sort.Slice(status.Peers, func(i, j int) bool { return status.Peers[i].URI < status.Peers[j].URI })
|
||||
for _, s := range d.node.core.GetSessions() {
|
||||
status.Sessions = append(status.Sessions, dashboardFlow{
|
||||
IP: keyToIP(s.Key),
|
||||
RX: s.RXBytes,
|
||||
TX: s.TXBytes,
|
||||
Uptime: s.Uptime.String(),
|
||||
})
|
||||
}
|
||||
for _, p := range d.node.core.GetPaths() {
|
||||
status.Paths = append(status.Paths, dashboardPath{IP: keyToIP(p.Key), Path: p.Path})
|
||||
}
|
||||
for _, t := range d.node.core.GetTree() {
|
||||
status.Tree = append(status.Tree, dashboardTree{IP: keyToIP(t.Key), Parent: keyToIP(t.Parent)})
|
||||
}
|
||||
status.Banned = d.bannedList()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) handlePeerTraffic(w http.ResponseWriter, r *http.Request) {
|
||||
if d.readOnly {
|
||||
http.Error(w, "public dashboard is read-only", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
URI string `json:"uri"`
|
||||
Interface string `json:"interface"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
uri, err := url.Parse(req.URI)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
key := canonicalPeerKey(req.URI, req.Interface)
|
||||
if req.Enabled {
|
||||
if d.isBanned(key) {
|
||||
http.Error(w, "peer is banned", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
err = d.node.core.AddPeer(uri, req.Interface)
|
||||
} else {
|
||||
err = d.node.core.RemovePeer(uri, req.Interface)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) handlePeerBan(w http.ResponseWriter, r *http.Request) {
|
||||
if d.readOnly {
|
||||
http.Error(w, "public dashboard is read-only", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
URI string `json:"uri"`
|
||||
Interface string `json:"interface"`
|
||||
Banned bool `json:"banned"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
uri, err := url.Parse(req.URI)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
key := canonicalPeerKey(req.URI, req.Interface)
|
||||
if req.Banned {
|
||||
d.mu.Lock()
|
||||
d.banned[key] = struct{}{}
|
||||
d.mu.Unlock()
|
||||
_ = d.node.core.RemovePeer(uri, req.Interface)
|
||||
} else {
|
||||
d.mu.Lock()
|
||||
delete(d.banned, key)
|
||||
d.mu.Unlock()
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) isBanned(k string) bool {
|
||||
d.mu.RLock()
|
||||
_, ok := d.banned[k]
|
||||
d.mu.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func (d *dashboardServer) bannedList() []string {
|
||||
d.mu.RLock()
|
||||
out := make([]string, 0, len(d.banned))
|
||||
for k := range d.banned {
|
||||
out = append(out, k)
|
||||
}
|
||||
d.mu.RUnlock()
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func canonicalPeerKey(uri, intf string) string { return uri + "|" + intf }
|
||||
|
||||
func peerHostFromURI(u string) string {
|
||||
pu, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return "-"
|
||||
}
|
||||
host := pu.Host
|
||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
return h
|
||||
}
|
||||
if host == "" {
|
||||
return "-"
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func keyToIP(k []byte) string {
|
||||
if len(k) == 0 {
|
||||
return "-"
|
||||
}
|
||||
var key [32]byte
|
||||
copy(key[:], k)
|
||||
ip := net.IP(address.AddrForKey(key[:])[:])
|
||||
if ip == nil {
|
||||
return "-"
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue