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 }