// SPDX-License-Identifier: Apache-2.0 // Copyright (c) 2024-2026 Quad4.io package cli import ( "flag" "fmt" "io" "time" "quad4/reticulum-go/pkg/common" "quad4/reticulum-go/pkg/node" "quad4/reticulum-go/pkg/rnsutil" ) func RunPath(args []string, opt ...Options) int { stdout, stderr := cliIO(opt) fs := flag.NewFlagSet("rgopath", flag.ContinueOnError) fs.SetOutput(stderr) configDir := fs.String("config", "", "path to config directory") table := fs.Bool("t", false, "show path table") jsonOut := fs.Bool("json", false, "emit JSON") maxHops := fs.Int("m", -1, "max hops filter for path table (-1 = no filter)") rates := fs.Bool("r", false, "show announce rate info") drop := fs.Bool("d", false, "drop path to destination") dropVia := fs.Bool("D", false, "drop all paths via transport hash") dropQueues := fs.Bool("q", false, "drop announce queues") timeoutSec := fs.Float64("w", 0, "path request timeout in seconds (0 = adaptive from interface bitrate)") rpcTimeout := fs.Duration("timeout", 10*time.Second, "RPC timeout") remoteHex := fs.String("R", "", "transport identity hash of remote instance") mgmtIdent := fs.String("i", "", "identity file for remote management") remoteTimeoutSec := fs.Float64("W", 0, "timeout for remote queries in seconds (0 = adaptive from interface bitrate)") blackholed := fs.Bool("blackholed", false, "list blackholed identities") blackhole := fs.Bool("blackhole", false, "blackhole identity") unblackhole := fs.Bool("unblackhole", false, "lift blackhole for identity") bhHours := fs.Float64("for", 0, "blackhole duration in hours (0 = indefinite)") bhReason := fs.String("reason", "", "blackhole reason string") filter := fs.String("filter", "", "substring filter for blackhole list") if err := fs.Parse(args); err != nil { return 2 } cfg, err := rnsutil.LoadConfigDir(*configDir) if err != nil { fmt.Fprintf(stderr, "config: %v\n", err) return 1 } var destHash []byte if fs.NArg() > 0 { destHash, err = rnsutil.ParseDestHash(fs.Arg(0)) if err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } } modeCount := 0 for _, b := range []bool{*table, *rates, *drop, *dropVia, *dropQueues, *blackholed, *blackhole, *unblackhole} { if b { modeCount++ } } if modeCount > 1 { fmt.Fprintln(stderr, "specify only one of -t -r -d -D -q -blackholed -blackhole -unblackhole") return 2 } if *remoteHex != "" { return runPathRemote(cfg, destHash, pathRemoteOpts{ table: *table, rates: *rates, jsonOut: *jsonOut, maxHops: *maxHops, drop: *drop, dropVia: *dropVia, dropQueues: *dropQueues, blackholed: *blackholed, blackhole: *blackhole, unblackhole: *unblackhole, remoteHex: *remoteHex, identPath: *mgmtIdent, timeoutSec: *remoteTimeoutSec, stdout: stdout, stderr: stderr, }) } needsRPC := *table || *rates || *drop || *dropVia || *dropQueues || *blackholed || *blackhole || *unblackhole if needsRPC { client, err := rnsutil.DialRPC(cfg, nil) if err != nil { fmt.Fprintf(stderr, "rpc: %v\n", err) return 1 } client.SetTimeout(*rpcTimeout) switch { case *table: var mh *int if *maxHops >= 0 { mh = maxHops } paths, err := client.GetPathTable(mh) if err != nil { fmt.Fprintf(stderr, "path table: %v\n", err) return 1 } if *jsonOut { if err := rnsutil.WritePathTableJSON(stdout, paths); err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } return 0 } n, err := rnsutil.WritePathTableHuman(stdout, paths, destHash) if err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } if len(destHash) > 0 && n == 0 { fmt.Fprintln(stdout, warnMsg(stdout, "No path known")) return 1 } return 0 case *rates: table, err := client.GetRateTable() if err != nil { fmt.Fprintf(stderr, "rate table: %v\n", err) return 1 } if *jsonOut { if err := rnsutil.WriteRateTableJSON(stdout, table); err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } return 0 } n, err := rnsutil.WriteRateTableHuman(stdout, table, destHash) if err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } if len(destHash) > 0 && n == 0 { fmt.Fprintln(stdout, warnMsg(stdout, "No information available")) return 1 } return 0 case *drop: if len(destHash) == 0 { fmt.Fprintln(stderr, "destination hash required for -d") return 2 } ok, err := client.DropPath(destHash) if err != nil { fmt.Fprintf(stderr, "drop path: %v\n", err) return 1 } if !ok { fmt.Fprintln(stdout, warnMsg(stdout, fmt.Sprintf("Unable to drop path to %s. Does it exist?", rnsutil.PrettyHex(destHash)))) return 1 } fmt.Fprintln(stdout, okMsg(stdout, fmt.Sprintf("Dropped path to %s", rnsutil.PrettyHex(destHash)))) return 0 case *dropVia: if len(destHash) == 0 { fmt.Fprintln(stderr, "transport hash required for -D") return 2 } n, err := client.DropAllVia(destHash) if err != nil { fmt.Fprintf(stderr, "drop via: %v\n", err) return 1 } if n == 0 { fmt.Fprintln(stdout, warnMsg(stdout, fmt.Sprintf("Unable to drop paths via %s. Does the transport instance exist?", rnsutil.PrettyHex(destHash)))) return 1 } fmt.Fprintln(stdout, okMsg(stdout, fmt.Sprintf("Dropped all paths via %s (%d)", rnsutil.PrettyHex(destHash), n))) return 0 case *dropQueues: n, err := client.DropAnnounceQueues() if err != nil { fmt.Fprintf(stderr, "drop queues: %v\n", err) return 1 } fmt.Fprintln(stdout, okMsg(stdout, fmt.Sprintf("Dropping announce queues on all interfaces... (%d cleared)", n))) return 0 case *blackholed: raw, err := client.GetBlackholedIdentities() if err != nil { fmt.Fprintf(stderr, "blackhole list: %v\n", err) return 1 } entries := rnsutil.NormalizeBlackholeRPC(raw) if *jsonOut { if err := rnsutil.WriteBlackholeJSON(stdout, entries); err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } return 0 } filt := *filter if filt == "" && len(destHash) > 0 { filt = rnsutil.HexHash(destHash) } if err := rnsutil.WriteBlackholeHuman(stdout, entries, filt); err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } if len(entries) == 0 { fmt.Fprintln(stdout, "No blackholed identity data available") } return 0 case *blackhole: if len(destHash) == 0 { fmt.Fprintln(stderr, "identity hash required for -blackhole") return 2 } var until float64 if *bhHours > 0 { until = float64(time.Now().Unix()) + (*bhHours)*3600 } ok, err := client.BlackholeIdentity(destHash, until, *bhReason) if err != nil { fmt.Fprintf(stderr, "blackhole: %v\n", err) return 1 } if ok { fmt.Fprintln(stdout, okMsg(stdout, fmt.Sprintf("Blackholed identity %s", rnsutil.HexHash(destHash)))) } else { fmt.Fprintln(stdout, warnMsg(stdout, fmt.Sprintf("Identity %s already blackholed", rnsutil.HexHash(destHash)))) } return 0 case *unblackhole: if len(destHash) == 0 { fmt.Fprintln(stderr, "identity hash required for -unblackhole") return 2 } ok, err := client.UnblackholeIdentity(destHash) if err != nil { fmt.Fprintf(stderr, "unblackhole: %v\n", err) return 1 } if ok { fmt.Fprintln(stdout, okMsg(stdout, fmt.Sprintf("Lifted blackhole for identity %s", rnsutil.HexHash(destHash)))) } else { fmt.Fprintln(stdout, warnMsg(stdout, fmt.Sprintf("Identity %s not blackholed", rnsutil.HexHash(destHash)))) } return 0 } } // Default: request path (node-attached, like rnpath without -t). if len(destHash) == 0 { fmt.Fprintln(stderr, "usage: rgopath [flags] ") fmt.Fprintln(stderr, " -t path table -r rates -d drop -D drop via -q drop queues") fmt.Fprintln(stderr, " -R transport_id -i identity -W seconds") fmt.Fprintln(stderr, " -blackholed / -blackhole / -unblackhole") return 2 } n, err := node.New(cfg) if err != nil { fmt.Fprintf(stderr, "node: %v\n", err) return 1 } if err := n.Start(); err != nil { fmt.Fprintf(stderr, "start: %v\n", err) return 1 } defer n.Stop() tr := n.Transport() timeout := time.Duration(*timeoutSec * float64(time.Second)) fmt.Fprintln(stdout, infoMsg(stdout, fmt.Sprintf("Path to %s requested", rnsutil.PrettyHex(destHash)))) ctx, cancel := rnsutil.CLIWaitContext(timeout) defer cancel() if err := rnsutil.WaitPath(ctx, tr, destHash); err != nil { fmt.Fprintln(stdout, errMsg(stdout, "Path request timed out")) report := rnsutil.DiagnoseReachability(tr, destHash) _ = rnsutil.WriteReachReportHuman(stdout, report) return 12 } hops := tr.HopsTo(destHash) via := tr.NextHop(destHash) iface := tr.NextHopInterface(destHash) hopWord := "hops" if hops == 1 { hopWord = "hop" } fmt.Fprintln(stdout, okMsg(stdout, fmt.Sprintf("Path found: %d %s via %s on %s", hops, hopWord, rnsutil.PrettyHex(via), iface))) return 0 } type pathRemoteOpts struct { table bool rates bool jsonOut bool maxHops int drop bool dropVia bool dropQueues bool blackholed bool blackhole bool unblackhole bool remoteHex string identPath string timeoutSec float64 stdout io.Writer stderr io.Writer } func runPathRemote(cfg *common.ReticulumConfig, destHash []byte, opts pathRemoteOpts) int { stdout, stderr := opts.stdout, opts.stderr if opts.drop { fmt.Fprintln(stdout, "Dropping path on remote instances not yet implemented") return 255 } if opts.dropVia { fmt.Fprintln(stdout, "Dropping all paths via specific transport instance on remote instances yet not implemented") return 255 } if opts.dropQueues { fmt.Fprintln(stdout, "Dropping announce queues on remote instances not yet implemented") return 255 } if opts.blackholed || opts.blackhole || opts.unblackhole { fmt.Fprintln(stdout, "Blackholing identity on remote instances not yet implemented") return 255 } if !opts.table && !opts.rates { fmt.Fprintln(stdout, "Requesting paths on remote instances not implemented") return 255 } tid, err := rnsutil.ParseDestHash(opts.remoteHex) if err != nil { fmt.Fprintf(stderr, "%v\n", err) return 20 } auth, err := rnsutil.LoadManagementIdentity(opts.identPath) if err != nil { fmt.Fprintf(stderr, "%v\n", err) return 20 } n, err := node.New(cfg) if err != nil { fmt.Fprintf(stderr, "node: %v\n", err) return 1 } if err := n.Start(); err != nil { fmt.Fprintf(stderr, "start: %v\n", err) return 1 } defer n.Stop() timeout := time.Duration(opts.timeoutSec * float64(time.Second)) ctx, cancel := rnsutil.CLIWaitContext(timeout) defer cancel() fmt.Fprintln(stdout, infoMsg(stdout, "Establishing link with remote transport instance...")) l, err := rnsutil.EstablishRemoteManagementLink(ctx, n.Transport(), tid, auth) if err != nil { fmt.Fprintln(stdout, errMsg(stdout, err.Error())) return 12 } defer l.Teardown() fmt.Fprintln(stdout, infoMsg(stdout, "Sending request...")) var mh *int if opts.maxHops >= 0 { mh = &opts.maxHops } cmd := "table" if opts.rates { cmd = "rates" } raw, err := rnsutil.RemotePathRequest(ctx, l, cmd, destHash, mh) if err != nil { fmt.Fprintln(stdout, errMsg(stdout, "The remote request failed. Likely authentication failure.")) return 10 } if opts.rates { table := rnsutil.RateTableFromResponse(raw) if opts.jsonOut { if err := rnsutil.WriteRateTableJSON(stdout, table); err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } return 0 } nshown, err := rnsutil.WriteRateTableHuman(stdout, table, destHash) if err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } if len(destHash) > 0 && nshown == 0 { fmt.Fprintln(stdout, warnMsg(stdout, "No information available")) return 1 } return 0 } table := rnsutil.PathTableFromResponse(raw) if opts.jsonOut { if err := rnsutil.WritePathTableJSON(stdout, table); err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } return 0 } nshown, err := rnsutil.WritePathTableHuman(stdout, table, destHash) if err != nil { fmt.Fprintf(stderr, "%v\n", err) return 1 } if len(destHash) > 0 && nshown == 0 { fmt.Fprintln(stdout, warnMsg(stdout, "No path known")) return 1 } return 0 }