mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
384 lines
13 KiB
Go
384 lines
13 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package rnsutil
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"quad4/reticulum-go/pkg/protect"
|
|
"quad4/reticulum-go/pkg/term"
|
|
"quad4/reticulum-go/pkg/transport"
|
|
)
|
|
|
|
// PrettyHex formats a hash as <aabb...ccdd>.
|
|
func PrettyHex(b []byte) string {
|
|
if len(b) == 0 {
|
|
return "<>"
|
|
}
|
|
h := hex.EncodeToString(b)
|
|
if len(h) <= 12 {
|
|
return "<" + h + ">"
|
|
}
|
|
return "<" + h[:6] + "..." + h[len(h)-6:] + ">"
|
|
}
|
|
|
|
// SizeString formats a byte count with SI prefixes.
|
|
func SizeString(num float64, suffix string) string {
|
|
units := [...]string{"", "K", "M", "G", "T", "P", "E", "Z"}
|
|
for _, unit := range units {
|
|
if abs64(num) < 1000 {
|
|
if unit == "" {
|
|
return fmt.Sprintf("%.0f %s%s", num, unit, suffix)
|
|
}
|
|
return fmt.Sprintf("%.2f %s%s", num, unit, suffix)
|
|
}
|
|
num /= 1000
|
|
}
|
|
return fmt.Sprintf("%.2f Y%s", num, suffix)
|
|
}
|
|
|
|
func abs64(v float64) float64 {
|
|
if v < 0 {
|
|
return -v
|
|
}
|
|
return v
|
|
}
|
|
|
|
// ModeName maps interface mode bytes to display labels.
|
|
func ModeName(mode byte) string {
|
|
switch mode {
|
|
case 0x03:
|
|
return "Access Point"
|
|
case 0x02:
|
|
return "Point-to-Point"
|
|
case 0x04:
|
|
return "Roaming"
|
|
case 0x05:
|
|
return "Boundary"
|
|
case 0x06:
|
|
return "Gateway"
|
|
case 0x07:
|
|
return "Internal"
|
|
default:
|
|
return "Full"
|
|
}
|
|
}
|
|
|
|
// StatusOptions controls human status rendering.
|
|
type StatusOptions struct {
|
|
NameFilter string
|
|
SortBy string
|
|
SortAsc bool
|
|
ShowAll bool
|
|
}
|
|
|
|
// SortInterfaceStats sorts interfaces in place by SortBy.
|
|
func SortInterfaceStats(stats *transport.InterfaceStatsResponse, sortBy string, asc bool) {
|
|
if stats == nil || sortBy == "" {
|
|
return
|
|
}
|
|
less := func(i, j int) bool { return false }
|
|
switch strings.ToLower(sortBy) {
|
|
case "rate", "bitrate":
|
|
less = func(i, j int) bool { return stats.Interfaces[i].Bitrate < stats.Interfaces[j].Bitrate }
|
|
case "rx":
|
|
less = func(i, j int) bool { return stats.Interfaces[i].RXB < stats.Interfaces[j].RXB }
|
|
case "tx":
|
|
less = func(i, j int) bool { return stats.Interfaces[i].TXB < stats.Interfaces[j].TXB }
|
|
case "rxs":
|
|
less = func(i, j int) bool { return stats.Interfaces[i].RXS < stats.Interfaces[j].RXS }
|
|
case "txs":
|
|
less = func(i, j int) bool { return stats.Interfaces[i].TXS < stats.Interfaces[j].TXS }
|
|
case "traffic":
|
|
less = func(i, j int) bool {
|
|
return stats.Interfaces[i].RXB+stats.Interfaces[i].TXB < stats.Interfaces[j].RXB+stats.Interfaces[j].TXB
|
|
}
|
|
case "announces", "announce":
|
|
less = func(i, j int) bool {
|
|
a := stats.Interfaces[i].IncomingAnnounceFrequency + stats.Interfaces[i].OutgoingAnnounceFrequency
|
|
b := stats.Interfaces[j].IncomingAnnounceFrequency + stats.Interfaces[j].OutgoingAnnounceFrequency
|
|
return a < b
|
|
}
|
|
case "arx":
|
|
less = func(i, j int) bool {
|
|
return stats.Interfaces[i].IncomingAnnounceFrequency < stats.Interfaces[j].IncomingAnnounceFrequency
|
|
}
|
|
case "atx":
|
|
less = func(i, j int) bool {
|
|
return stats.Interfaces[i].OutgoingAnnounceFrequency < stats.Interfaces[j].OutgoingAnnounceFrequency
|
|
}
|
|
case "prx":
|
|
less = func(i, j int) bool {
|
|
return stats.Interfaces[i].IncomingPRFrequency < stats.Interfaces[j].IncomingPRFrequency
|
|
}
|
|
case "ptx":
|
|
less = func(i, j int) bool {
|
|
return stats.Interfaces[i].OutgoingPRFrequency < stats.Interfaces[j].OutgoingPRFrequency
|
|
}
|
|
case "held":
|
|
less = func(i, j int) bool {
|
|
return stats.Interfaces[i].HeldAnnounces < stats.Interfaces[j].HeldAnnounces
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
sort.SliceStable(stats.Interfaces, func(i, j int) bool {
|
|
if asc {
|
|
return less(i, j)
|
|
}
|
|
return less(j, i)
|
|
})
|
|
}
|
|
|
|
func hideInterface(name string, showAll bool) bool {
|
|
if showAll {
|
|
return false
|
|
}
|
|
prefixes := []string{
|
|
"LocalInterface[",
|
|
"TCPInterface[Client",
|
|
"BackboneInterface[Client on",
|
|
"AutoInterfacePeer[",
|
|
"WeaveInterfacePeer[",
|
|
"I2PInterfacePeer[Connected peer",
|
|
}
|
|
for _, p := range prefixes {
|
|
if strings.HasPrefix(name, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// WriteStatusHuman writes a human-readable status report including announce rates.
|
|
func WriteStatusHuman(w io.Writer, stats transport.InterfaceStatsResponse, linkCount *int, opts StatusOptions) error {
|
|
filter := strings.ToLower(opts.NameFilter)
|
|
for i := range stats.Interfaces {
|
|
st := &stats.Interfaces[i]
|
|
if hideInterface(st.Name, opts.ShowAll) {
|
|
continue
|
|
}
|
|
if filter != "" && !strings.Contains(strings.ToLower(st.Name), filter) {
|
|
continue
|
|
}
|
|
state := "Down"
|
|
if st.Status {
|
|
state = "Up"
|
|
}
|
|
if st.Status {
|
|
state = term.GreenW(w, state)
|
|
} else {
|
|
state = term.RedW(w, state)
|
|
}
|
|
if _, err := fmt.Fprintf(w, "\n%s\n Status : %s\n Mode : %s\n RX : %s\n TX : %s\n",
|
|
st.Name, state, ModeName(st.Mode),
|
|
SizeString(float64(st.RXB), "B"),
|
|
SizeString(float64(st.TXB), "B"),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if st.RXS > 0 || st.TXS > 0 {
|
|
if _, err := fmt.Fprintf(w, " Rate : ↓%s ↑%s\n",
|
|
SizeString(st.RXS, "b")+"/s",
|
|
SizeString(st.TXS, "b")+"/s",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if st.Bitrate > 0 {
|
|
if _, err := fmt.Fprintf(w, " Bitrate : %s\n", SizeString(float64(st.Bitrate), "b")+"/s"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := fmt.Fprintf(w, " Announces : ↓%.2f/s ↑%.2f/s\n Path reqs : ↓%.2f/s ↑%.2f/s\n Held : %d\n",
|
|
st.IncomingAnnounceFrequency, st.OutgoingAnnounceFrequency,
|
|
st.IncomingPRFrequency, st.OutgoingPRFrequency,
|
|
st.HeldAnnounces,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if st.BurstActive || st.PRBurstActive {
|
|
if _, err := fmt.Fprintf(w, " Burst : announce=%v pathreq=%v\n", st.BurstActive, st.PRBurstActive); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
integrityFails := st.IFACFail + st.HMACFail + st.UnpackFail + st.PaddingFail + st.AnnounceSigFail
|
|
if integrityFails > 0 || st.StaleCloses > 0 || st.IntegrityFailRate > 0 {
|
|
if _, err := fmt.Fprintf(w, " Integrity : fail_rate=%.2f ifac=%d hmac=%d unpack=%d announce_sig=%d stale_closes=%d\n",
|
|
st.IntegrityFailRate, st.IFACFail, st.HMACFail, st.UnpackFail, st.AnnounceSigFail, st.StaleCloses,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
healthBits := st.BlackholeHit + st.PathReqDup + st.PathReqNoCache + st.PathRespSuppressed +
|
|
st.ResourceStall + st.LinkStaleClose + st.KeepaliveTimeout + st.AnnounceDup
|
|
if healthBits > 0 {
|
|
if _, err := fmt.Fprintf(w, " Health : blackhole=%d path_dup=%d path_nocache=%d path_suppress=%d resource_stall=%d link_stale=%d keepalive=%d announce_dup=%d\n",
|
|
st.BlackholeHit, st.PathReqDup, st.PathReqNoCache, st.PathRespSuppressed,
|
|
st.ResourceStall, st.LinkStaleClose, st.KeepaliveTimeout, st.AnnounceDup,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if st.Clients != nil {
|
|
if _, err := fmt.Fprintf(w, " Clients : %d\n", *st.Clients); err != nil {
|
|
return err
|
|
}
|
|
if st.BlockedIPs != nil && *st.BlockedIPs > 0 {
|
|
if _, err := fmt.Fprintf(w, " Blocked : %d IPs\n", *st.BlockedIPs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(stats.TransportID) > 0 {
|
|
if _, err := fmt.Fprintf(w, "\nTransport ID : %s\n", PrettyHex(stats.TransportID)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if stats.TransportUptime > 0 {
|
|
if _, err := fmt.Fprintf(w, "Uptime : %s\n", formatSeconds(stats.TransportUptime)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := fmt.Fprintf(w, "Traffic : RX %s TX %s\n",
|
|
SizeString(float64(stats.RXB), "B"),
|
|
SizeString(float64(stats.TXB), "B"),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if stats.RXS > 0 || stats.TXS > 0 {
|
|
if _, err := fmt.Fprintf(w, "Throughput : ↓%s ↑%s\n",
|
|
SizeString(stats.RXS, "b")+"/s",
|
|
SizeString(stats.TXS, "b")+"/s",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if linkCount != nil {
|
|
if _, err := fmt.Fprintf(w, "Links : %d\n", *linkCount); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func formatSeconds(s float64) string {
|
|
if s < 60 {
|
|
return strconv.FormatFloat(s, 'f', 1, 64) + "s"
|
|
}
|
|
if s < 3600 {
|
|
return strconv.FormatFloat(s/60, 'f', 1, 64) + "m"
|
|
}
|
|
if s < 86400 {
|
|
return strconv.FormatFloat(s/3600, 'f', 1, 64) + "h"
|
|
}
|
|
return strconv.FormatFloat(s/86400, 'f', 1, 64) + "d"
|
|
}
|
|
|
|
// WriteStatusJSON writes interface stats as JSON with bytes hex-encoded.
|
|
func WriteStatusJSON(w io.Writer, stats transport.InterfaceStatsResponse) error {
|
|
type ifaceJSON struct {
|
|
Name string `json:"name"`
|
|
ShortName string `json:"short_name"`
|
|
Hash string `json:"hash,omitempty"`
|
|
Type string `json:"type"`
|
|
RXB uint64 `json:"rxb"`
|
|
TXB uint64 `json:"txb"`
|
|
RXS float64 `json:"rxs"`
|
|
TXS float64 `json:"txs"`
|
|
Status bool `json:"status"`
|
|
Mode byte `json:"mode"`
|
|
Clients *int `json:"clients"`
|
|
BlockedIPs *int `json:"blocked_ips,omitempty"`
|
|
BlockedIPList []string `json:"blocked_ip_list,omitempty"`
|
|
Bitrate int64 `json:"bitrate"`
|
|
IncomingAnnounceFrequency float64 `json:"incoming_announce_frequency"`
|
|
OutgoingAnnounceFrequency float64 `json:"outgoing_announce_frequency"`
|
|
IncomingPRFrequency float64 `json:"incoming_pr_frequency"`
|
|
OutgoingPRFrequency float64 `json:"outgoing_pr_frequency"`
|
|
HeldAnnounces int `json:"held_announces"`
|
|
BurstActive bool `json:"burst_active"`
|
|
PRBurstActive bool `json:"pr_burst_active"`
|
|
IFACFail uint64 `json:"ifac_fail"`
|
|
HMACFail uint64 `json:"hmac_fail"`
|
|
AnnounceSigFail uint64 `json:"announce_sig_fail"`
|
|
UnpackFail uint64 `json:"unpack_fail"`
|
|
IntegrityFailRate float64 `json:"integrity_fail_rate"`
|
|
StaleCloses uint64 `json:"stale_closes"`
|
|
KeepaliveTimeout uint64 `json:"keepalive_timeout"`
|
|
I2PB32 *string `json:"i2p_b32,omitempty"`
|
|
I2PConnectable *bool `json:"i2p_connectable,omitempty"`
|
|
Tunnel *string `json:"tunnelstate,omitempty"`
|
|
I2PLastError *string `json:"i2p_last_error,omitempty"`
|
|
}
|
|
out := struct {
|
|
Interfaces []ifaceJSON `json:"interfaces"`
|
|
RXB uint64 `json:"rxb"`
|
|
TXB uint64 `json:"txb"`
|
|
RXS float64 `json:"rxs"`
|
|
TXS float64 `json:"txs"`
|
|
TransportID string `json:"transport_id"`
|
|
TransportUptime float64 `json:"transport_uptime"`
|
|
NetmonFlap uint64 `json:"netmon_flap"`
|
|
ActiveLinks int `json:"active_links"`
|
|
Protect protect.Snapshot `json:"protect"`
|
|
}{
|
|
Interfaces: make([]ifaceJSON, 0, len(stats.Interfaces)),
|
|
RXB: stats.RXB,
|
|
TXB: stats.TXB,
|
|
RXS: stats.RXS,
|
|
TXS: stats.TXS,
|
|
TransportID: hex.EncodeToString(stats.TransportID),
|
|
TransportUptime: stats.TransportUptime,
|
|
NetmonFlap: stats.NetmonFlap,
|
|
ActiveLinks: stats.ActiveLinks,
|
|
Protect: stats.Protect,
|
|
}
|
|
for i := range stats.Interfaces {
|
|
st := &stats.Interfaces[i]
|
|
out.Interfaces = append(out.Interfaces, ifaceJSON{
|
|
Name: st.Name,
|
|
ShortName: st.ShortName,
|
|
Hash: hex.EncodeToString(st.Hash),
|
|
Type: st.Type,
|
|
RXB: st.RXB,
|
|
TXB: st.TXB,
|
|
RXS: st.RXS,
|
|
TXS: st.TXS,
|
|
Status: st.Status,
|
|
Mode: st.Mode,
|
|
Clients: st.Clients,
|
|
BlockedIPs: st.BlockedIPs,
|
|
BlockedIPList: st.BlockedIPList,
|
|
Bitrate: st.Bitrate,
|
|
IncomingAnnounceFrequency: st.IncomingAnnounceFrequency,
|
|
OutgoingAnnounceFrequency: st.OutgoingAnnounceFrequency,
|
|
IncomingPRFrequency: st.IncomingPRFrequency,
|
|
OutgoingPRFrequency: st.OutgoingPRFrequency,
|
|
HeldAnnounces: st.HeldAnnounces,
|
|
BurstActive: st.BurstActive,
|
|
PRBurstActive: st.PRBurstActive,
|
|
IFACFail: st.IFACFail,
|
|
HMACFail: st.HMACFail,
|
|
AnnounceSigFail: st.AnnounceSigFail,
|
|
UnpackFail: st.UnpackFail,
|
|
IntegrityFailRate: st.IntegrityFailRate,
|
|
StaleCloses: st.StaleCloses,
|
|
KeepaliveTimeout: st.KeepaliveTimeout,
|
|
I2PB32: st.I2PB32,
|
|
I2PConnectable: st.I2PConnectable,
|
|
Tunnel: st.TunnelState,
|
|
I2PLastError: st.I2PLastError,
|
|
})
|
|
}
|
|
enc := json.NewEncoder(w)
|
|
return enc.Encode(out)
|
|
}
|