diff options
author | Přemysl Eric Janouch <p@janouch.name> | 2025-05-15 13:59:05 +0200 |
---|---|---|
committer | Přemysl Eric Janouch <p@janouch.name> | 2025-05-15 14:14:53 +0200 |
commit | 80af5c22d62f6df11c63a807ebf34ff69a4af754 (patch) | |
tree | 418804e057f1d85188edd8e6b083780a3776bb20 | |
parent | 7ba17a016140f578bccca3ffd320c1663cf66c6c (diff) | |
download | xK-origin/master.tar.gz xK-origin/master.tar.xz xK-origin/master.zip |
Add an xC relay protocol analyzerorigin/master
-rw-r--r-- | xN/xN.go | 10 | ||||
-rw-r--r-- | xR/.gitignore | 2 | ||||
-rw-r--r-- | xR/Makefile | 17 | ||||
-rw-r--r-- | xR/go.mod | 5 | ||||
-rw-r--r-- | xR/xR.adoc | 41 | ||||
-rw-r--r-- | xR/xR.go | 134 |
6 files changed, 204 insertions, 5 deletions
@@ -247,16 +247,16 @@ func main() { flag.PrintDefaults() } flag.Parse() - if flag.NArg() < 1 { - flag.Usage() - os.Exit(2) - } - if *version { fmt.Printf("%s %s\n", projectName, projectVersion) return } + if flag.NArg() < 1 { + flag.Usage() + os.Exit(2) + } + text, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalln(err) diff --git a/xR/.gitignore b/xR/.gitignore new file mode 100644 index 0000000..a9766d8 --- /dev/null +++ b/xR/.gitignore @@ -0,0 +1,2 @@ +/xR +/proto.go diff --git a/xR/Makefile b/xR/Makefile new file mode 100644 index 0000000..7fb55c5 --- /dev/null +++ b/xR/Makefile @@ -0,0 +1,17 @@ +.POSIX: +AWK = env LC_ALL=C awk + +tools = ../liberty/tools +generated = proto.go +outputs = xR $(generated) +all: $(outputs) +generate: $(generated) + +proto.go: $(tools)/lxdrgen.awk $(tools)/lxdrgen-go.awk ../xC.lxdr + $(AWK) -f $(tools)/lxdrgen.awk -f $(tools)/lxdrgen-go.awk \ + -v PrefixCamel=Relay ../xC.lxdr > $@ +xR: xR.go ../xK-version $(generated) + go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@ \ + -gcflags=all="-N -l" +clean: + rm -f $(outputs) diff --git a/xR/go.mod b/xR/go.mod new file mode 100644 index 0000000..998a18f --- /dev/null +++ b/xR/go.mod @@ -0,0 +1,5 @@ +module janouch.name/xK/xR + +go 1.23.0 + +toolchain go1.24.0 diff --git a/xR/xR.adoc b/xR/xR.adoc new file mode 100644 index 0000000..c3215bd --- /dev/null +++ b/xR/xR.adoc @@ -0,0 +1,41 @@ +xR(1) +===== +:doctype: manpage +:manmanual: xK Manual +:mansource: xK {release-version} + +Name +---- +xR - xC relay protocol analyzer + +Synopsis +-------- +*xR* [_OPTION_]... RELAY-ADDRESS... + +Description +----------- +*xR* connects to an *xC* relay and prints all incoming events one per line +in JSON format. The JSON objects have two additional fields: + +when:: + The time of reception (or sending) as a nanosecond precision + RFC 3339 UTC timestamp. +raw:: + The incoming event (or outgoing command) in raw binary form. + +Options +------- +*-debug*:: + Print any outgoing commands as well, which may help in debugging any issues. + +*-version*:: + Output version information and exit. + +Reporting bugs +-------------- +Use https://git.janouch.name/p/xK to report bugs, request features, +or submit pull requests. + +See also +-------- +*xC*(1) diff --git a/xR/xR.go b/xR/xR.go new file mode 100644 index 0000000..a26832d --- /dev/null +++ b/xR/xR.go @@ -0,0 +1,134 @@ +// Copyright (c) 2025, Přemysl Eric Janouch <p@janouch.name> +// SPDX-License-Identifier: 0BSD + +package main + +import ( + "encoding/binary" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "log" + "net" + "os" + "time" +) + +var ( + debug = flag.Bool("debug", false, "enable debug output") + version = flag.Bool("version", false, "show version and exit") + projectName = "xR" + projectVersion = "?" +) + +func now() string { + return time.Now().UTC().Format(time.RFC3339Nano) +} + +func relayReadFrame(r io.Reader) bool { + var length uint32 + if err := binary.Read( + r, binary.BigEndian, &length); errors.Is(err, io.EOF) { + return false + } else if err != nil { + log.Fatalln("Event receive failed: " + err.Error()) + } + b := make([]byte, length) + if _, err := io.ReadFull(r, b); errors.Is(err, io.EOF) { + return false + } else if err != nil { + log.Fatalln("Event receive failed: " + err.Error()) + } + + m := struct { + When string `json:"when"` + Binary []byte `json:"raw"` + RelayEventMessage + }{ + When: now(), + Binary: b, + } + + if after, ok := m.RelayEventMessage.ConsumeFrom(b); !ok { + log.Println("Event deserialization failed") + } else if len(after) != 0 { + log.Println("Event deserialization failed: trailing data") + return true + } + + j, err := json.Marshal(m) + if err != nil { + log.Fatalln("Event marshalling failed: " + err.Error()) + } + fmt.Printf("%s\n", j) + return true +} + +func run(addressConnect string) { + conn, err := net.Dial("tcp", addressConnect) + if err != nil { + log.Println("Connection failed: " + err.Error()) + return + } + defer conn.Close() + + // We can only support this one protocol version + // that proto.go has been generated for. + m := RelayCommandMessage{CommandSeq: 0, Data: RelayCommandData{ + Variant: &RelayCommandDataHello{Version: RelayVersion}, + }} + + b, ok := m.AppendTo(make([]byte, 4)) + if !ok { + log.Fatalln("Command serialization failed") + } + binary.BigEndian.PutUint32(b[:4], uint32(len(b)-4)) + if _, err := conn.Write(b); err != nil { + log.Fatalln("Command send failed: " + err.Error()) + } + + // You can differentiate the direction by the presence + // of .data.command or .data.event. + if *debug { + j, err := json.Marshal(struct { + When string `json:"when"` + Binary []byte `json:"raw"` + RelayCommandMessage + }{ + When: now(), + Binary: b, + RelayCommandMessage: m, + }) + if err != nil { + log.Fatalln("Command marshalling failed: " + err.Error()) + } + fmt.Printf("%s\n", j) + } + + for relayReadFrame(conn) { + } +} + +func main() { + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), + "Usage: %s [OPTION...] CONNECT\n\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + if *version { + fmt.Printf("%s %s (relay protocol version %d)\n", + projectName, projectVersion, RelayVersion) + return + } + + if flag.NArg() != 1 { + flag.Usage() + os.Exit(1) + } + + // TODO(p): This program should be able to run as a filter as well. + run(flag.Arg(0)) +} |