From 7cdd5c04d5e966b3c90a3d86720e0823b4f7ee2c Mon Sep 17 00:00:00 2001 From: bt Date: Tue, 10 Mar 2026 22:50:53 +0100 Subject: Update spec --- PROTOCOL.md | 74 ++----- README.md | 12 +- build_spec.sh | 2 +- solec.ksy | 54 ++++- solec.lua | 111 ++++++++-- solec.svg | 699 ++++++++++++++++++++++++++++++++-------------------------- 6 files changed, 544 insertions(+), 408 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index 895d1df..886161c 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -2,48 +2,41 @@ # SOLEC protocol specification +## Data format + * Payload can contain multiple numeric types or binary data in a single frame * Numeric types are big endian -* Numeric types names are same as in [Go](https://go.dev/ref/spec#Numeric_types) * See [test payload](#Test) for an example of complex data structure -## Frame +### Frame structure | Type | Description | |----------|----------------| | `uint8` | Payload type | | `uint16` | Payload length | -| `[]any` | Payload | - +| `any` | Payload | -## Data types -### Numeric types +### Special types -* `uint8` -* `uint16` -* `uint32` -* `uint64` +#### Strings and binary -### `binary` +Strings and binary data are prefixed wtith 2 bytes denoting their length. +Strings are UTF-8 encoded. -| Type | Description | -|--------|----------------| -| uint16 | Data length | -| binary | Data | +#### Timestamps -### `string` +Timestamps are stored as `uint64` in [Unix format](https://en.wikipedia.org/wiki/Unix_time). +Timezone is always set to UTC. -UTF-8 encoded string encapsulated in `bianry` type. +### Payloads -### `time` +All payload types are described in following files: -[Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) stored in `uint16`. -Timezone is always UTC. +* [Kaitai Struct](https://git.sr.ht/~rctt/solec/blob/main/solec.ksy) +* [SVG graph](https://git.sr.ht/~rctt/solec/blob/main/solec.svg) -# Payloads - -## Payload prefixes +### Payload types | Type | Name | |------|--------------| @@ -53,39 +46,4 @@ Timezone is always UTC. | 0x04 | Message | | 0xFF | Test | -## Payload types - -### Handshake - -| Type | Name | Description | -|--------|---------------------|----------------------------------------------------------| -| uint8 | ProtocolVersionMajor | Bumped if changes are incompatible with previous version | -| uint8 | PrococolVersionMinor | Bumber if changes are backwards compatible | - - -### Ping - -Can be send by client or server. Receiver shoudl reply with `Pong`. - -### Pong - -Reply to `ping`. - -| Type | Name | Description | -|--------|------|-------------------------| -| uint64 | Time | Set just before sending | - -### Test - -Used for testing purposes. Can change at any time. -| Type | Name | Description | -|--------|-------|-------------| -| uint8 | Num1 | | -| time | Time1 | | -| string | Str1 | | -| uint16 | Num2 | | -| binary | Bin1 | | -| uint32 | Num3 | | -| string | Str2 | | -| uint64 | Num4 | | diff --git a/README.md b/README.md index 24cb6c3..ee8e555 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ -# solecd +# solec -## Running +* [Protocol spec](https://git.sr.ht/~rctt/solec/tree/main/item/PROTOCOL.md) + +## Running the daemon You'll need Go toolchain. Get it [here](https://go.dev/dl/) ``` -git clone https://git.sr.ht/~rctt/solecd +git clone https://git.sr.ht/~rctt/solec cd solecd go run cmd/daemon/main.go ``` @@ -23,6 +25,7 @@ it into Wireshark plugins directory. - Protocol - Design handshake process - Design basic messaging + - Maybe put payload type and length into same bytes? - Server - Put server stuff into a package @@ -31,6 +34,3 @@ it into Wireshark plugins directory. - Network library - Handle partial TCP packets - Better error handling - -- Tools - - Generate Wireshark dissector from spec \ No newline at end of file diff --git a/build_spec.sh b/build_spec.sh index ae78b81..dfebbd2 100755 --- a/build_spec.sh +++ b/build_spec.sh @@ -13,7 +13,7 @@ rm solec.dot ~/bin/kaitai-struct-compiler-ws/bin/kaitai-struct-compiler -t wireshark --read-pos solec.ksy ${SED} -i -e '5s/.*/package.path = "kaitai_struct_lua_runtime\/?.lua" .. package.path/' solec.lua -${SED} -i -e 's/, self.payload_type//' solec.lua +${SED} -i -e 's/, self.type_payload//' solec.lua cat <> solec.lua local tcp_port = DissectorTable.get("tcp.port") diff --git a/solec.ksy b/solec.ksy index 82f3bf7..e7c24e1 100644 --- a/solec.ksy +++ b/solec.ksy @@ -3,32 +3,39 @@ meta: file-extension: hex endian: be -enums: - type: - 0x01: handshake - 0x02: ping - 0x03: pong - 0x04: message - 0xFF: test +doc: | + SOLEC protocol uses big endian encoding. Frames uses type-length-value format. + +doc-ref: https://git.sr.ht/~rctt/solec/blob/main/solec.svg seq: - - id: payload_type + - id: type_payload type: u1 enum: type - - id: payload_length + - id: len_payload type: u2 - id: payload + size: len_payload type: - switch-on: payload_type + switch-on: type_payload cases: 'type::handshake': handshake 'type::ping': ping 'type::pong': pong -# 'type::message': message + 'type::message': message 'type::test': test +enums: + type: + 0x01: handshake + 0x02: ping + 0x03: pong + 0x04: message + 0xFF: test + types: string: + doc: UTF-8 encoded string. seq: - id: len_payload type: u2 @@ -38,6 +45,7 @@ types: encoding: UTF-8 binary: + doc: Binary data of unspecifed type. seq: - id: len_payload type: u2 @@ -45,19 +53,41 @@ types: size: len_payload handshake: + doc: | + Handshake is sent by the client during connection initialization. + If protocol_ver_major is different than version used by the server + connection will be aborted. seq: - id: proto_ver_major type: u1 - id: proto_ver_minor type: u1 - ping: {} + ping: + doc: Ping does not contain any payload. pong: seq: - id: timestamp + doc: | + Timestamp is set just before sending the pong message. It can be used + to meassure packet's travel time between server and client. type: u8 + message: + doc: | + Source and target fields are addresses in user@server format. + seq: + - id: source + type: string + - id: target + type: string + - id: timestamp + doc: Set just before sending the message. + type: u8 + - id: content + type: string + test: doc: Test payload, used for parsers testing seq: diff --git a/solec.lua b/solec.lua index e4c8514..a06a429 100644 --- a/solec.lua +++ b/solec.lua @@ -9,10 +9,10 @@ require("kaitaistruct") local proto = Proto('solec', 'Solec') proto.fields = {} local enum = require("enum") -local Solec_payload_type = ProtoField.new('payload_type', 'Solec.payload_type', ftypes.BYTES) -table.insert(proto.fields, Solec_payload_type) -local Solec_payload_length = ProtoField.new('payload_length', 'Solec.payload_length', ftypes.UINT32) -table.insert(proto.fields, Solec_payload_length) +local Solec_type_payload = ProtoField.new('type_payload', 'Solec.type_payload', ftypes.BYTES) +table.insert(proto.fields, Solec_type_payload) +local Solec_len_payload = ProtoField.new('len_payload', 'Solec.len_payload', ftypes.UINT32) +table.insert(proto.fields, Solec_len_payload) local Solec_Test_num1 = ProtoField.new('num1', 'Solec.Test.num1', ftypes.UINT8) table.insert(proto.fields, Solec_Test_num1) local Solec_Test_time1 = ProtoField.new('time1', 'Solec.Test.time1', ftypes.UINT32) @@ -36,6 +36,8 @@ local Solec_Handshake_proto_ver_major = ProtoField.new('proto_ver_major', 'Solec table.insert(proto.fields, Solec_Handshake_proto_ver_major) local Solec_Handshake_proto_ver_minor = ProtoField.new('proto_ver_minor', 'Solec.Handshake.proto_ver_minor', ftypes.UINT8) table.insert(proto.fields, Solec_Handshake_proto_ver_minor) +local Solec_Message_timestamp = ProtoField.new('timestamp', 'Solec.Message.timestamp', ftypes.UINT32) +table.insert(proto.fields, Solec_Message_timestamp) local Solec_Pong_timestamp = ProtoField.new('timestamp', 'Solec.Pong.timestamp', ftypes.UINT32) table.insert(proto.fields, Solec_Pong_timestamp) @@ -46,6 +48,9 @@ function proto.dissector(tvb, pinfo, root) local obj = Solec(io, tree) end +-- +-- SOLEC protocol uses big endian encoding. Frames uses type-length-value format. +-- See also: Source (https://git.sr.ht/~rctt/solec/blob/main/solec.svg) Solec = class.class(KaitaiStruct) Solec.Type = enum.Enum { @@ -66,29 +71,45 @@ end function Solec:_read() local _offset = self._io:pos() - self.payload_type = Solec.Type(self._io:read_u1()) - self._tree:add(Solec_payload_type, self._io._io.tvb(_offset, self._io:pos() - _offset)) + self.type_payload = Solec.Type(self._io:read_u1()) + self._tree:add(Solec_type_payload, self._io._io.tvb(_offset, self._io:pos() - _offset)) local _offset = self._io:pos() - self.payload_length = self._io:read_u2be() - self._tree:add(Solec_payload_length, self._io._io.tvb(_offset, self._io:pos() - _offset), self.payload_length) + self.len_payload = self._io:read_u2be() + self._tree:add(Solec_len_payload, self._io._io.tvb(_offset, self._io:pos() - _offset), self.len_payload) local _offset = self._io:pos() - local _on = self.payload_type - if _on == Solec.Type.handshake then - local _tree = self._tree:add(self._io._io.tvb(_offset, 0), 'payload') - self.payload = Solec.Handshake(self._io, _tree, self, self._root) - _tree:set_len(self._io:pos() - _offset) + local _on = self.type_payload + if _on == Solec.Type.message then + self._raw_payload = self._io:read_bytes(self.len_payload) + local _tvb = self._io._io.tvb(_offset, self._io:pos() - _offset) + local _io = KaitaiStream(TVBStream(_tvb)) + local _tree = self._tree:add(_tvb, 'payload') + self.payload = Solec.Message(_io, _tree, self, self._root) elseif _on == Solec.Type.ping then - local _tree = self._tree:add(self._io._io.tvb(_offset, 0), 'payload') - self.payload = Solec.Ping(self._io, _tree, self, self._root) - _tree:set_len(self._io:pos() - _offset) + self._raw_payload = self._io:read_bytes(self.len_payload) + local _tvb = self._io._io.tvb(_offset, self._io:pos() - _offset) + local _io = KaitaiStream(TVBStream(_tvb)) + local _tree = self._tree:add(_tvb, 'payload') + self.payload = Solec.Ping(_io, _tree, self, self._root) elseif _on == Solec.Type.pong then - local _tree = self._tree:add(self._io._io.tvb(_offset, 0), 'payload') - self.payload = Solec.Pong(self._io, _tree, self, self._root) - _tree:set_len(self._io:pos() - _offset) + self._raw_payload = self._io:read_bytes(self.len_payload) + local _tvb = self._io._io.tvb(_offset, self._io:pos() - _offset) + local _io = KaitaiStream(TVBStream(_tvb)) + local _tree = self._tree:add(_tvb, 'payload') + self.payload = Solec.Pong(_io, _tree, self, self._root) elseif _on == Solec.Type.test then - local _tree = self._tree:add(self._io._io.tvb(_offset, 0), 'payload') - self.payload = Solec.Test(self._io, _tree, self, self._root) - _tree:set_len(self._io:pos() - _offset) + self._raw_payload = self._io:read_bytes(self.len_payload) + local _tvb = self._io._io.tvb(_offset, self._io:pos() - _offset) + local _io = KaitaiStream(TVBStream(_tvb)) + local _tree = self._tree:add(_tvb, 'payload') + self.payload = Solec.Test(_io, _tree, self, self._root) + elseif _on == Solec.Type.handshake then + self._raw_payload = self._io:read_bytes(self.len_payload) + local _tvb = self._io._io.tvb(_offset, self._io:pos() - _offset) + local _io = KaitaiStream(TVBStream(_tvb)) + local _tree = self._tree:add(_tvb, 'payload') + self.payload = Solec.Handshake(_io, _tree, self, self._root) + else + self.payload = self._io:read_bytes(self.len_payload) end end @@ -136,6 +157,8 @@ function Solec.Test:_read() end +-- +-- Binary data of unspecifed type. Solec.Binary = class.class(KaitaiStruct) function Solec.Binary:_init(io, tree, parent, root) @@ -156,6 +179,8 @@ function Solec.Binary:_read() end +-- +-- UTF-8 encoded string. Solec.String = class.class(KaitaiStruct) function Solec.String:_init(io, tree, parent, root) @@ -176,6 +201,8 @@ function Solec.String:_read() end +-- +-- Ping does not contain any payload. Solec.Ping = class.class(KaitaiStruct) function Solec.Ping:_init(io, tree, parent, root) @@ -190,6 +217,10 @@ function Solec.Ping:_read() end +-- +-- Handshake is sent by the client during connection initialization. +-- If protocol_ver_major is different than version used by the server +-- connection will be aborted. Solec.Handshake = class.class(KaitaiStruct) function Solec.Handshake:_init(io, tree, parent, root) @@ -210,6 +241,39 @@ function Solec.Handshake:_read() end +-- +-- Source and target fields are addresses in user@server format. +Solec.Message = class.class(KaitaiStruct) + +function Solec.Message:_init(io, tree, parent, root) + KaitaiStruct._init(self, io) + self._parent = parent + self._root = root or self + self._tree = tree + self:_read() +end + +function Solec.Message:_read() + local _offset = self._io:pos() + local _tree = self._tree:add(self._io._io.tvb(_offset, 0), 'source') + self.source = Solec.String(self._io, _tree, self, self._root) + _tree:set_len(self._io:pos() - _offset) + local _offset = self._io:pos() + local _tree = self._tree:add(self._io._io.tvb(_offset, 0), 'target') + self.target = Solec.String(self._io, _tree, self, self._root) + _tree:set_len(self._io:pos() - _offset) + local _offset = self._io:pos() + self.timestamp = self._io:read_u8be() + self._tree:add(Solec_Message_timestamp, self._io._io.tvb(_offset, self._io:pos() - _offset), self.timestamp) + local _offset = self._io:pos() + local _tree = self._tree:add(self._io._io.tvb(_offset, 0), 'content') + self.content = Solec.String(self._io, _tree, self, self._root) + _tree:set_len(self._io:pos() - _offset) +end + +-- +-- Set just before sending the message. + Solec.Pong = class.class(KaitaiStruct) function Solec.Pong:_init(io, tree, parent, root) @@ -226,6 +290,9 @@ function Solec.Pong:_read() self._tree:add(Solec_Pong_timestamp, self._io._io.tvb(_offset, self._io:pos() - _offset), self.timestamp) end +-- +-- Timestamp is set just before sending the pong message. It can be used +-- to meassure packet's travel time between server and client. local tcp_port = DissectorTable.get("tcp.port") tcp_port:add(9999, proto) diff --git a/solec.svg b/solec.svg index 360c759..63e66fe 100644 --- a/solec.svg +++ b/solec.svg @@ -4,391 +4,472 @@ - - - + + + cluster__solec - -Solec + +Solec cluster__binary - -Solec::Binary + +Solec::Binary cluster__handshake - -Solec::Handshake + +Solec::Handshake -cluster__ping - -Solec::Ping +cluster__message + +Solec::Message -cluster__pong - -Solec::Pong +cluster__ping + +Solec::Ping -cluster__string - -Solec::String +cluster__pong + +Solec::Pong +cluster__string + +Solec::String + + cluster__test - -Solec::Test + +Solec::Test solec__seq - - -pos - - -size - - -type - - -id - -0 - -1 - -u1→Type - -payload_type - -1 - -2 - -u2be - -payload_length - -3 - -... - -switch (payload_type) - -payload + + +pos + + +size + + +type + + +id + +0 + +1 + +u1→Type + +type_payload + +1 + +2 + +u2be + +len_payload + +3 + +... + +switch (type_payload) + +payload - -solec__seq:payload_type_type->solec__seq:payload_type - - + +solec__seq:type_payload_type->solec__seq:payload_type + + solec__seq_payload_switch - - -case - - -type - -:type_handshake - -Handshake - -:type_ping - -Ping - -:type_pong - -Pong - -:type_test - -Test + + +case + + +type + +:type_handshake + +Handshake + +:type_message + +Message + +:type_ping + +Ping + +:type_pong + +Pong + +:type_test + +Test solec__seq:payload_type->solec__seq_payload_switch - - + + handshake__seq - - -pos - - -size - - -type - - -id - -0 - -1 - -u1 - -proto_ver_major - -1 - -1 - -u1 - -proto_ver_minor + + +pos + + +size + + +type + + +id + +0 + +1 + +u1 + +proto_ver_major + +1 + +1 + +u1 + +proto_ver_minor solec__seq_payload_switch:case0->handshake__seq - - + + - + +message__seq + + +pos + + +size + + +type + + +id + +0 + +... + +String + +source + +... + +... + +String + +target + +... + +8 + +u8be + +timestamp + +... + +... + +String + +content + + + +solec__seq_payload_switch:case1->message__seq + + + + + ping__seq - - -pos - - -size - - -type - - -id + + +pos + + +size + + +type + + +id - -solec__seq_payload_switch:case1->ping__seq - - + +solec__seq_payload_switch:case2->ping__seq + + - + pong__seq - - -pos - - -size - - -type - - -id - -0 - -8 - -u8be - -timestamp + + +pos + + +size + + +type + + +id + +0 + +8 + +u8be + +timestamp - -solec__seq_payload_switch:case2->pong__seq - - + +solec__seq_payload_switch:case3->pong__seq + + - + test__seq - - -pos - - -size - - -type - - -id - -0 - -1 - -u1 - -num1 - -1 - -8 - -u8be - -time1 - -9 - -... - -String - -str1 - -... - -2 - -u2be - -num2 - -... - -... - -Binary - -bin1 - -... - -4 - -u4be - -num3 - -... - -... - -String - -str2 - -... - -8 - -u8be - -num4 + + +pos + + +size + + +type + + +id + +0 + +1 + +u1 + +num1 + +1 + +8 + +u8be + +time1 + +9 + +... + +String + +str1 + +... + +2 + +u2be + +num2 + +... + +... + +Binary + +bin1 + +... + +4 + +u4be + +num3 + +... + +... + +String + +str2 + +... + +8 + +u8be + +num4 - -solec__seq_payload_switch:case3->test__seq - - + +solec__seq_payload_switch:case4->test__seq + + binary__seq - - -pos - - -size - - -type - - -id - -0 - -2 - -u2be - -len_payload - -2 - -len_payload - - -payload + + +pos + + +size + + +type + + +id + +0 + +2 + +u2be + +len_payload + +2 + +len_payload + + +payload - + binary__seq:len_payload_type->binary__seq:payload_size - - + + - + string__seq - - -pos - - -size - - -type - - -id - -0 - -2 - -u2be - -len_payload - -2 - -len_payload - -str(UTF-8) - -payload + + +pos + + +size + + +type + + +id + +0 + +2 + +u2be + +len_payload + +2 + +len_payload + +str(UTF-8) + +payload + + + +message__seq:source_type->string__seq + + + + + +message__seq:target_type->string__seq + + + + + +message__seq:content_type->string__seq + + - + string__seq:len_payload_type->string__seq:payload_size - - + + - + test__seq:bin1_type->binary__seq - - + + - + test__seq:str1_type->string__seq - - + + - + test__seq:str2_type->string__seq - - + + -- cgit v1.2.3