This commit is contained in:
Racks 2026-03-01 17:01:47 +01:00
commit 1d167420c3
89 changed files with 10707 additions and 0 deletions

411
cmd/yggdrasilctl/main.go Normal file
View file

@ -0,0 +1,411 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net"
"net/url"
"os"
"sort"
"strings"
"time"
"suah.dev/protect"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast"
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
func main() {
// read config, speak DNS/TCP and/or over a UNIX socket
if err := protect.Pledge("stdio rpath inet unix dns"); err != nil {
panic(err)
}
// makes sure we can use defer and still return an error code to the OS
os.Exit(run())
}
func run() int {
logbuffer := &bytes.Buffer{}
logger := log.New(logbuffer, "", log.Flags())
defer func() int {
if r := recover(); r != nil {
logger.Println("Fatal error:", r)
fmt.Print(logbuffer)
return 1
}
return 0
}()
cmdLineEnv := newCmdLineEnv()
cmdLineEnv.parseFlagsAndArgs()
if cmdLineEnv.ver {
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
fmt.Println("To get the version number of the running Kaya node, run", os.Args[0], "getSelf")
return 0
}
if len(cmdLineEnv.args) == 0 {
flag.Usage()
return 0
}
cmdLineEnv.setEndpoint(logger)
var conn net.Conn
u, err := url.Parse(cmdLineEnv.endpoint)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
logger.Println("Connecting to UNIX socket", cmdLineEnv.endpoint[7:])
conn, err = net.Dial("unix", cmdLineEnv.endpoint[7:])
case "tcp":
logger.Println("Connecting to TCP socket", u.Host)
conn, err = net.Dial("tcp", u.Host)
default:
logger.Println("Unknown protocol or malformed address - check your endpoint")
err = errors.New("protocol not supported")
}
} else {
logger.Println("Connecting to TCP socket", u.Host)
conn, err = net.Dial("tcp", cmdLineEnv.endpoint)
}
if err != nil {
panic(err)
}
// config and socket are done, work without unprivileges
if err := protect.Pledge("stdio"); err != nil {
panic(err)
}
logger.Println("Connected")
defer conn.Close()
decoder := json.NewDecoder(conn)
encoder := json.NewEncoder(conn)
send := &admin.AdminSocketRequest{}
recv := &admin.AdminSocketResponse{}
args := map[string]string{}
for c, a := range cmdLineEnv.args {
if c == 0 {
if strings.HasPrefix(a, "-") {
logger.Printf("Ignoring flag %s as it should be specified before other parameters\n", a)
continue
}
logger.Printf("Sending request: %v\n", a)
send.Name = a
continue
}
tokens := strings.SplitN(a, "=", 2)
switch {
case len(tokens) == 1:
logger.Println("Ignoring invalid argument:", a)
default:
args[tokens[0]] = tokens[1]
}
}
if send.Arguments, err = json.Marshal(args); err != nil {
panic(err)
}
if err := encoder.Encode(&send); err != nil {
panic(err)
}
logger.Printf("Request sent")
if err := decoder.Decode(&recv); err != nil {
panic(err)
}
if recv.Status == "error" {
if err := recv.Error; err != "" {
fmt.Println("Admin socket returned an error:", err)
} else {
fmt.Println("Admin socket returned an error but didn't specify any error text")
}
return 1
}
if cmdLineEnv.injson {
if json, err := json.MarshalIndent(recv.Response, "", " "); err == nil {
fmt.Println(string(json))
}
return 0
}
opts := []tablewriter.Option{
tablewriter.WithRowAlignment(tw.AlignLeft),
tablewriter.WithHeaderAlignment(tw.AlignCenter),
tablewriter.WithHeaderAutoFormat(tw.Off),
tablewriter.WithDebug(false),
}
if !cmdLineEnv.borders {
opts = append(opts, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Borders: tw.BorderNone,
Settings: tw.Settings{
Lines: tw.LinesNone,
Separators: tw.SeparatorsNone,
},
})))
}
table := tablewriter.NewTable(os.Stdout, opts...)
switch strings.ToLower(send.Name) {
case "list":
var resp admin.ListResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.Header([]string{"Command", "Arguments", "Description"})
for _, entry := range resp.List {
for i := range entry.Fields {
entry.Fields[i] = entry.Fields[i] + "=..."
}
_ = table.Append([]string{entry.Command, strings.Join(entry.Fields, ", "), entry.Description})
}
_ = table.Render()
case "getself":
var resp admin.GetSelfResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
_ = table.Append([]string{"Build name:", resp.BuildName})
_ = table.Append([]string{"Build version:", resp.BuildVersion})
_ = table.Append([]string{"IPv6 address:", resp.IPAddress})
_ = table.Append([]string{"IPv6 subnet:", resp.Subnet})
_ = table.Append([]string{"Routing table size:", fmt.Sprintf("%d", resp.RoutingEntries)})
_ = table.Append([]string{"Public key:", resp.PublicKey})
_ = table.Render()
case "getpeers":
var resp admin.GetPeersResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.Header([]string{"URI", "Remote", "State", "Dir", "IP Address", "Uptime", "RTT", "RX", "TX", "Down", "Up", "Pr", "Cost", "Last Error"})
for _, peer := range resp.Peers {
state, lasterr, dir, rtt, rxr, txr := "Up", "-", "Out", "-", "-", "-"
if !peer.Up {
if state = "Down"; peer.LastError != "" {
lasterr = fmt.Sprintf("%s ago: %s", peer.LastErrorTime.Round(time.Second), peer.LastError)
}
} else if rttms := float64(peer.Latency.Microseconds()) / 1000; rttms > 0 {
rtt = fmt.Sprintf("%.02fms", rttms)
}
if peer.Inbound {
dir = "In"
}
parsedURI, parseErr := url.Parse(peer.URI)
uristring := peer.URI
if parseErr == nil {
parsedURI.RawQuery = ""
uristring = parsedURI.String()
}
if peer.RXRate > 0 {
rxr = peer.RXRate.String() + "/s"
}
if peer.TXRate > 0 {
txr = peer.TXRate.String() + "/s"
}
remote := peerRemoteHost(parsedURI, parseErr)
_ = table.Append([]string{
uristring,
remote,
state,
dir,
peer.IPAddress,
(time.Duration(peer.Uptime) * time.Second).String(),
rtt,
peer.RXBytes.String(),
peer.TXBytes.String(),
rxr,
txr,
fmt.Sprintf("%d", peer.Priority),
fmt.Sprintf("%d", peer.Cost),
lasterr,
})
}
_ = table.Render()
case "gettree":
var resp admin.GetTreeResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.Header([]string{"Tree", "IP Address", "Sequence"})
for _, line := range formatTreeRows(resp.Tree) {
_ = table.Append([]string{line.Branch, line.IPAddress, fmt.Sprintf("%d", line.Sequence)})
}
_ = table.Render()
case "getpaths":
var resp admin.GetPathsResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.Header([]string{"Public Key", "IP Address", "Path", "Seq"})
for _, p := range resp.Paths {
_ = table.Append([]string{
p.PublicKey,
p.IPAddress,
fmt.Sprintf("%v", p.Path),
fmt.Sprintf("%d", p.Sequence),
})
}
_ = table.Render()
case "getsessions":
var resp admin.GetSessionsResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.Header([]string{"Public Key", "IP Address", "Uptime", "RX", "TX"})
for _, p := range resp.Sessions {
_ = table.Append([]string{
p.PublicKey,
p.IPAddress,
(time.Duration(p.Uptime) * time.Second).String(),
p.RXBytes.String(),
p.TXBytes.String(),
})
}
_ = table.Render()
case "getnodeinfo":
var resp core.GetNodeInfoResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
for _, v := range resp {
fmt.Println(string(v))
break
}
case "getmulticastinterfaces":
var resp multicast.GetMulticastInterfacesResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
fmtBool := func(b bool) string {
if b {
return "Yes"
}
return "-"
}
table.Header([]string{"Name", "Listen Address", "Beacon", "Listen", "Password"})
for _, p := range resp.Interfaces {
_ = table.Append([]string{
p.Name,
p.Address,
fmtBool(p.Beacon),
fmtBool(p.Listen),
fmtBool(p.Password),
})
}
_ = table.Render()
case "gettun":
var resp tun.GetTUNResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
_ = table.Append([]string{"TUN enabled:", fmt.Sprintf("%#v", resp.Enabled)})
if resp.Enabled {
_ = table.Append([]string{"Interface name:", resp.Name})
_ = table.Append([]string{"Interface MTU:", fmt.Sprintf("%d", resp.MTU)})
}
_ = table.Render()
case "addpeer", "removepeer", "setpeertraffic":
default:
fmt.Println(string(recv.Response))
}
return 0
}
func peerRemoteHost(parsedURI *url.URL, parseErr error) string {
if parseErr != nil || parsedURI == nil {
return "-"
}
host := parsedURI.Host
if splitHost, _, err := net.SplitHostPort(host); err == nil {
return splitHost
}
if host == "" {
return "-"
}
return host
}
type treeLine struct {
Branch string
IPAddress string
Sequence uint64
}
func formatTreeRows(entries []admin.TreeEntry) []treeLine {
if len(entries) == 0 {
return nil
}
children := make(map[string][]admin.TreeEntry, len(entries))
byKey := make(map[string]admin.TreeEntry, len(entries))
for _, entry := range entries {
byKey[entry.PublicKey] = entry
}
roots := make([]admin.TreeEntry, 0, len(entries))
for _, entry := range entries {
if entry.Parent == "" || entry.Parent == entry.PublicKey || byKey[entry.Parent].PublicKey == "" {
roots = append(roots, entry)
continue
}
children[entry.Parent] = append(children[entry.Parent], entry)
}
sort.Slice(roots, func(i, j int) bool { return roots[i].PublicKey < roots[j].PublicKey })
for parent := range children {
sort.Slice(children[parent], func(i, j int) bool {
return children[parent][i].PublicKey < children[parent][j].PublicKey
})
}
rows := make([]treeLine, 0, len(entries))
var walk func(admin.TreeEntry, string, bool)
walk = func(node admin.TreeEntry, prefix string, last bool) {
branchPrefix := ""
nextPrefix := ""
if prefix != "" {
if last {
branchPrefix = prefix + "└─ "
nextPrefix = prefix + " "
} else {
branchPrefix = prefix + "├─ "
nextPrefix = prefix + "│ "
}
}
label := node.PublicKey
if len(label) > 16 {
label = label[:16] + "…"
}
rows = append(rows, treeLine{Branch: branchPrefix + label, IPAddress: node.IPAddress, Sequence: node.Sequence})
kids := children[node.PublicKey]
for i, child := range kids {
walk(child, nextPrefix, i == len(kids)-1)
}
}
for i, root := range roots {
walk(root, "", i == len(roots)-1)
}
return rows
}