From a24a6a347360e4f7c2241061d6cbf59f0a0f4144 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Thu, 18 Jun 2026 10:41:09 +0300 Subject: [PATCH] Rewrite dissector: modular sources, dual-protocol, version-tested Single "Tarantool" dissector that auto-detects, per PDU, the modern MsgPack IPROTO (1.6-3.x) vs the legacy <=1.5 binary protocol, so a mixed capture decodes in one load. Structure: - src/ split into core, msgpack_ext, modern, legacy and per-build entries; amalgamate.sh (POSIX sh) inlines modules into three self-contained dist/ builds (all / modern-only / legacy-only), bundling MessagePack only where needed. Each build carries a private module registry instead of the global package.preload, so generic module names ("core", ...) don't collide with other Lua plugins and two builds can load in one session. - each build registers under its own protocol name -- tarantool (all), tarantool2 (modern), tarantool1 (legacy) -- created by core.init(slug, desc, default_port), so the split builds coexist. The per-build distinct names are borrowed from Dmitry Pankov's "Wireshark 4.0+ support for tarantool 1.5 proto" branch (PR tarantool/tarantool-dissector#5), which split into tarantool15 / tarantool2. Decoding: - modern: SQL, streams, id, watchers, structured MP_ERROR stack, replication (join/subscribe/raft/vclock), and MsgPack ext types (decimal, uuid, datetime, interval) decoded to real values; unsigned 64-bit rendered unsigned; pcall-guarded against malformed PDUs; 0xce framing guard so non-IPROTO bytes don't corrupt reassembly. - legacy: full 1.5 request/response set; direction detection from the configured server ports, falling back to the lower-port heuristic; typeless fields rendered as string / LE integer / blob, consistently across Wireshark versions. Preferences: - "Dissector enabled" (default on; borrowed from PR #5 -- toggles the dissector from the GUI and re-registers on change) and "TCP ports" -- a range, e.g. 3301,3311-3313 (default 3301; legacy build 33013). prefs_changed re-registers the port table and disabling unregisters the dissector. Distinct per-build defaults keep co-loaded split builds off the same tcp.port slot, since Wireshark binds one dissector per port. Tests: - tests/pcap/ holds real captures from Tarantool 1.5, 1.10, 2.11, 3.x, a merged 1.5+3.x, and 3-node master-master replication (async and sync); tests/run.sh asserts concrete decoded values (bodies, responses, error text, ext tuples, replication metadata), the enabled/disabled preference, the ports-range preference (binding a whole 3311-3313 mesh without Decode As), and that the legacy and modern builds co-load without colliding -- including that disabling one leaves the other working. CI runs it on Wireshark 3.x and 4.x, and checks dist/ is regenerated from src/. The capture generator test.lua guards with "if _TARANTOOL == nil then return end" (borrowed from PR #5), so it is a no-op when loaded outside a Tarantool runtime. Removes the old single-format tarantool.dissector.lua and tarantool15.dissector.lua. --- .github/workflows/dist.yml | 50 + .github/workflows/tests.yml | 59 + README.md | 148 +- TESTING.md | 40 + amalgamate.sh | 102 + dist/tarantool-legacy.dissector.lua | 413 ++++ dist/tarantool-modern.dissector.lua | 2192 +++++++++++++++++ dist/tarantool.dissector.lua | 2430 +++++++++++++++++++ src/core.lua | 143 ++ src/entry_all.lua | 20 + src/entry_legacy.lua | 12 + src/entry_modern.lua | 12 + src/legacy.lua | 226 ++ src/modern.lua | 700 ++++++ src/msgpack_ext.lua | 166 ++ tarantool.dissector.lua | 496 ---- tarantool15.dissector.lua | 382 --- test.lua | 48 +- tests/pcap/tarantool-1.10.pcap | Bin 0 -> 8701 bytes tests/pcap/tarantool-1.5.pcap | Bin 0 -> 11660 bytes tests/pcap/tarantool-2.11.pcap | Bin 0 -> 49306 bytes tests/pcap/tarantool-3.x.pcap | Bin 0 -> 50724 bytes tests/pcap/tarantool-combined.pcap | Bin 0 -> 66140 bytes tests/pcap/tarantool-replication-async.pcap | Bin 0 -> 702296 bytes tests/pcap/tarantool-replication-sync.pcap | Bin 0 -> 90864 bytes tests/run.sh | 208 ++ 26 files changed, 6942 insertions(+), 905 deletions(-) create mode 100644 .github/workflows/dist.yml create mode 100644 .github/workflows/tests.yml create mode 100644 TESTING.md create mode 100755 amalgamate.sh create mode 100644 dist/tarantool-legacy.dissector.lua create mode 100644 dist/tarantool-modern.dissector.lua create mode 100644 dist/tarantool.dissector.lua create mode 100644 src/core.lua create mode 100644 src/entry_all.lua create mode 100644 src/entry_legacy.lua create mode 100644 src/entry_modern.lua create mode 100644 src/legacy.lua create mode 100644 src/modern.lua create mode 100644 src/msgpack_ext.lua delete mode 100644 tarantool.dissector.lua delete mode 100644 tarantool15.dissector.lua create mode 100644 tests/pcap/tarantool-1.10.pcap create mode 100644 tests/pcap/tarantool-1.5.pcap create mode 100644 tests/pcap/tarantool-2.11.pcap create mode 100644 tests/pcap/tarantool-3.x.pcap create mode 100644 tests/pcap/tarantool-combined.pcap create mode 100644 tests/pcap/tarantool-replication-async.pcap create mode 100644 tests/pcap/tarantool-replication-sync.pcap create mode 100755 tests/run.sh diff --git a/.github/workflows/dist.yml b/.github/workflows/dist.yml new file mode 100644 index 0000000..441dc4e --- /dev/null +++ b/.github/workflows/dist.yml @@ -0,0 +1,50 @@ +name: dist + +# Verify the committed dist/ builds are exactly what amalgamate.sh produces from +# the current src/. Without this, an edit to src/ could ship while dist/ (what +# users actually load into Wireshark) silently lags behind. + +on: + push: + branches: [master] + paths: + - "src/**" + - "dist/**" + - "MessagePack.lua" + - "amalgamate.sh" + - ".github/workflows/dist.yml" + pull_request: + paths: + - "src/**" + - "dist/**" + - "MessagePack.lua" + - "amalgamate.sh" + - ".github/workflows/dist.yml" + +jobs: + check-dist: + name: dist/ is up to date + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Regenerate dist/ from src/ + run: sh ./amalgamate.sh + + - name: Fail if dist/ differs from the committed output + run: | + if [ -n "$(git status --porcelain -- dist)" ]; then + echo "::error::dist/ is stale — run ./amalgamate.sh and commit the result." + git --no-pager diff -- dist + git status --porcelain -- dist + exit 1 + fi + echo "dist/ matches the current src/." + + - name: Syntax-check the generated dissectors + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq lua5.4 + for f in dist/*.lua; do + luac5.4 -p "$f" && echo "ok: $f" + done diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..0a65f37 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,59 @@ +name: tests + +# Decode the captured fixtures (tests/pcap/) with the dissector and assert each +# decodes cleanly and covers the request types expected for its Tarantool +# version. Guards against regressions in either protocol path. + +on: + push: + branches: [master] + paths: + - "src/**" + - "dist/**" + - "tests/**" + - "MessagePack.lua" + - "amalgamate.sh" + - ".github/workflows/tests.yml" + pull_request: + paths: + - "src/**" + - "dist/**" + - "tests/**" + - "MessagePack.lua" + - "amalgamate.sh" + - ".github/workflows/tests.yml" + +jobs: + dissect: + name: ${{ matrix.name }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - name: Wireshark 4.x (noble) + jammy: false + - name: Wireshark 3.x (jammy) + jammy: true + steps: + - uses: actions/checkout@v4 + + - name: Install tshark + env: + DEBIAN_FRONTEND: noninteractive + run: | + if ${{ matrix.jammy }}; then + echo "deb http://archive.ubuntu.com/ubuntu jammy main universe" \ + | sudo tee /etc/apt/sources.list.d/jammy.list + sudo apt-get update -qq + sudo apt-get install -y -qq -t jammy tshark + else + sudo apt-get update -qq + sudo apt-get install -y -qq tshark + fi + + - name: Show tshark version + run: tshark --version | head -1 + + - name: Decode fixtures with the dissector + run: sh tests/run.sh diff --git a/README.md b/README.md index 0384c70..59e6737 100644 --- a/README.md +++ b/README.md @@ -10,32 +10,130 @@ dissector implemented for Tarantool binary protocol. ![Wireshark][screenshot] +The dissector auto-detects the wire format per PDU by inspecting the leading +bytes: the modern MsgPack IProto (Tarantool 1.6–3.x, always framed with a 5-byte +`0xce` length prefix) and the legacy ≤1.5 binary protocol (a fixed 12-byte +little-endian `` header). It loops over every PDU in a +segment, so it also decodes a mixed capture where old and new clients share one +port (e.g. legacy `box.dostring` calls alongside MsgPack `call_16`/`ping`). + +Three ready-to-use, self-contained single-file builds are generated under +`dist/` (MsgPack is bundled in; nothing else to copy). Pick the one matching your +traffic — load only one at a time, they share the protocol name: + +| File | Covers | +| --- | --- | +| [`dist/tarantool.dissector.lua`][dist-all] | **all versions** — modern + legacy, auto-detected (recommended) | +| [`dist/tarantool-modern.dissector.lua`][dist-modern] | modern MsgPack IProto only (Tarantool 1.6–3.x) | +| [`dist/tarantool-legacy.dissector.lua`][dist-legacy] | legacy pre-MsgPack only (Tarantool ≤1.5) | + +For the modern format it understands the current IProto request set, including +SQL (`execute`, `prepare`), interactive transactions over streams (`begin`, +`commit`, `rollback`), protocol negotiation (`id`), event watchers (`watch`, +`unwatch`, `event`, `watch_once`) and the structured (`MP_ERROR`) error format. +Tuple values encoded as MsgPack extensions are decoded too: `decimal`, `uuid`, +`datetime` and `interval` render as real values, while opaque extensions +(`error`, `compression`, `tuple`, `arrow`) show as a labelled blob. Unsigned +64-bit values are rendered unsigned, and it handles TCP reassembly of large +packets and several pipelined packets in a single segment. + +The following display filter fields are available: + +| Field | Description | +| --- | --- | +| `tnt.type` | request/response code, e.g. `tnt.type == 0x01` for selects | +| `tnt.request` | request name, e.g. `tnt.request == "call"` | +| `tnt.sync` | request id, handy to match a response to its request | +| `tnt.schema_version` | schema version reported in responses | +| `tnt.stream_id` | stream id of an interactive transaction | +| `tnt.response` | `true` for responses, `false` for requests | + +### Installation + +The `dist/` builds are self-contained — one file, no dependencies to copy. Put +it in Wireshark's **Personal Lua Plugins** folder. + +1. Find the folder. Run `tshark -G folders` (or *Help → About Wireshark → + Folders* in the GUI) and look for the `Personal Lua Plugins` line. Typical + locations: + + | OS | Path | + | --- | --- | + | Linux / macOS | `~/.local/lib/wireshark/plugins` | + | Windows | `%APPDATA%\Wireshark\plugins` | + +2. Copy the build you want there. **Rename it so the filename has no extra dot + before `.lua`.** Wireshark treats a dot in a plugin filename as a module path, + so `tarantool.dissector.lua` would be looked up as the module + `tarantool/dissector.lua` and fail to auto-load. Install it as `tarantool.lua`: + + ```sh + DEST=~/.local/lib/wireshark/plugins + mkdir -p "$DEST" + cp dist/tarantool.dissector.lua "$DEST/tarantool.lua" + ``` + + A second Tarantool dissector registering the same protocol name fails to load + with *"there cannot be two protocols with the same description"*, so keep only + one in that folder. + +3. Load it. Restart Wireshark, or in a running GUI reload plugins with + *Analyze → Reload Lua Plugins* (**Ctrl+Shift+L**, **⌘⇧L** on macOS). + +Alternatively, skip installation and pass the dissector ad-hoc on the command +line: + +```sh +wireshark -X lua_script:dist/tarantool.dissector.lua +tshark -X lua_script:dist/tarantool.dissector.lua -V -r capture.pcap +``` + +### Building from source + +The `dist/` files are generated; the real source lives under `src/` as small +modules, combined by `amalgamate.sh` (POSIX shell, no toolchain needed): + +| Path | Role | +| --- | --- | +| `src/core.lua` | Proto, ProtoFields, port pref, greeting, main loop, registration | +| `src/msgpack_ext.lua` | MsgPack ext decoding (decimal/uuid/datetime/interval) + unsigned-64 fix | +| `src/modern.lua` | modern MsgPack IProto constants + decoders | +| `src/legacy.lua` | legacy ≤1.5 framing + decoders | +| `src/entry_*.lua` | per-build entry points wiring the dispatch | +| `MessagePack.lua` | vendored pure-Lua MsgPack (inlined into the modern/all builds) | + +Each build inlines the modules it needs via `package.preload`, so the modules +stay independent (and `require`-able) while the output is one self-contained +file. Edit `src/` and regenerate all three builds with: + +```sh +./amalgamate.sh +``` + ### How to use -- Setup Wireshark. See chapter [Building and Installing - Wireshark][building-and-installing-wireshark] in documentation. -- Put a Lua file with dissector and `MessagePack.lua` to a directory with - plugins for Wireshark, directory depends on operating system, please refer to - chapter [Plugin folders][plugin-folders]. - Note that Wireshark requires root privileges, make sure you are using plugin - directory for a user that is used for running Wireshark. It possible to run - Wireshark in terminal and pass Lua extension explicitly: `wireshark -X - lua_script:tarantool.dissector.lua` or `tshark -X - lua_script:tarantool.dissector.lua -V`. -- If for some reason you still use Tarantool <= 1.5, use `tarantool15.dissector.lua` -- Run Wireshark. By default Tarantool protocol dissector decodes TCP packets - with port 3301. However one can change a port for dissector in Wireshark - settings, see chapter [Control Protocol dissection][control-protocol-dissection]. - -### How to test - -There is a script `test.lua` that uses Tarantool instance remotely via network -and covers most part of IProto commands. For testing one can run Wireshark on -local interface `lo0` with filtering by port 3301 and run script with command -`tarantool test.lua`. - -[box-protocol]: https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/ +By default the dissector decodes TCP packets on port 3301. The port is +configurable in *Edit → Preferences → Protocols → Tarantool*, or apply it to a +single conversation via *Decode As…*, see chapter [Control Protocol +dissection][control-protocol-dissection]. Capturing requires permission to read +from the network interface (root, or membership in a capture group such as +`access_bpf` on macOS / `wireshark` on Linux). + +Legacy ≤1.5 servers default to a different binary port (33013). Since the +dissector only auto-attaches to the configured port (3301), point it at the +legacy port too — set the Tarantool port preference to 33013, or use *Decode +As… → tcp.port 33013 → Tarantool*. (This is why the test suite passes +`-d tcp.port==33013,tarantool` for the 1.5 capture.) + +### Tests + +The dissector is verified against captures from real servers of several +Tarantool versions, and traffic can be regenerated by hand. See +[TESTING.md](TESTING.md). + +[box-protocol]: https://www.tarantool.io/en/doc/latest/reference/internals/box_protocol/ [screenshot]: screenshot.png -[building-and-installing-wireshark]: https://www.wireshark.org/docs/wsug_html_chunked/ChapterBuildInstall.html -[plugin-folders]: https://www.wireshark.org/docs/wsug_html_chunked/ChPluginFolders.html [control-protocol-dissection]: https://www.wireshark.org/docs/wsug_html_chunked/ChCustProtocolDissectionSection.html +[dist-all]: https://raw.githubusercontent.com/tarantool/tarantool-dissector/master/dist/tarantool.dissector.lua +[dist-modern]: https://raw.githubusercontent.com/tarantool/tarantool-dissector/master/dist/tarantool-modern.dissector.lua +[dist-legacy]: https://raw.githubusercontent.com/tarantool/tarantool-dissector/master/dist/tarantool-legacy.dissector.lua diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..8fd2d43 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,40 @@ +## Testing + +`tests/pcap/` holds captures from real servers of several Tarantool versions, +each exercising the functionality that version supports: + +| Fixture | Captured from | Covers | +| --- | --- | --- | +| `tarantool-1.5.pcap` | Tarantool 1.5 (legacy binary, port 33013) | insert, select, update, delete, call | +| `tarantool-1.10.pcap` | Tarantool 1.10 (MsgPack) | auth, ping, CRUD, call, eval | +| `tarantool-2.11.pcap` | Tarantool 2.11 (MsgPack) | the above + SQL, streams (begin/commit/rollback), `id`, watchers, error stack | +| `tarantool-3.x.pcap` | Tarantool 3.x (MsgPack) | the above + `watch_once` and MsgPack ext types (decimal, uuid, datetime, interval) | +| `tarantool-combined.pcap` | 1.5 + 3.x merged | both framings in one capture — exercises the per-PDU legacy/modern dispatch | +| `tarantool-replication-async.pcap` | 3-node master-master, ports 3311–3313 | bootstrap (`join`/`subscribe`) + asynchronous master-master writes: non-conflicting, conflicting and over a stream (`begin`/`commit`/`rollback`) | +| `tarantool-replication-sync.pcap` | same cluster, node 1 promoted | synchronous replication: `raft_promote` + quorum-acknowledged commits (`raft_confirm`) into a synchronous space | + +`tests/run.sh` decodes each fixture with `dist/tarantool.dissector.lua` and +asserts, per version: zero Lua errors; the expected request types; decoded +request bodies (e.g. `box.dostring`, `myfunc(2, 3)`, the SQL text); response +decoding (legacy return codes, SQL `row_count`/`metadata`, the structured error +stack `[1] ClientError (code 3)` vs. the 1.10 string error); header fields; and +the 3.x MsgPack ext values (decimal, uuid, datetime, interval). It needs `tshark` +on `PATH` (override with `TSHARK`; run as a non-root user — Wireshark disables +`-X lua_script` under root). CI runs it on every change +(`.github/workflows/tests.yml`) across **Wireshark 3.x and 4.x** — both on +`ubuntu-24.04`, the 3.x leg pulling Wireshark 3.6 from jammy's packages. A second +workflow (`dist.yml`) regenerates `dist/` from `src/` and fails if they differ, +so the committed builds can't drift from the sources. + +```sh +sh tests/run.sh +# macOS: +TSHARK=/Applications/Wireshark.app/Contents/MacOS/tshark sh tests/run.sh +``` + +### Generating traffic by hand + +`test.lua` drives a local Tarantool 3.x instance through most IProto commands — +CRUD, `call`/`eval`, SQL (`execute`/`prepare`), event watchers, interactive +transactions over streams and replication (`join`/`subscribe`). Capture loopback +on port 3301 (e.g. in Wireshark on `lo`/`lo0`) and run `tarantool test.lua`. diff --git a/amalgamate.sh b/amalgamate.sh new file mode 100755 index 0000000..89ebdd4 --- /dev/null +++ b/amalgamate.sh @@ -0,0 +1,102 @@ +#!/bin/sh +# +# Amalgamate the src/ modules into three self-contained dissectors under dist/: +# tarantool.dissector.lua all versions (modern MsgPack + legacy) +# tarantool-modern.dissector.lua Tarantool 1.6 .. 3.x +# tarantool-legacy.dissector.lua Tarantool <= 1.5 +# Each inlines the modules it needs via package.preload; MessagePack is bundled +# only into the builds that use it. Edit src/, then run ./amalgamate.sh. + +set -eu + +here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +dist="$here/dist" +mkdir -p "$dist" + +# print_header TITLE -- the banner prepended to every generated file. +print_header() { + cat < -r capture.pcap +-- +-- Protocol reference: +-- https://www.tarantool.io/en/doc/latest/reference/internals/box_protocol/ +-- +EOF +} + +# emit_prologue -- a private module loader prepended before the module bodies, +# so each amalgamated file keeps its own core/modern/legacy registry instead of +# sharing the process-global package.preload/package.loaded. This stops our +# generic module names from colliding with any other Lua plugin (or another copy +# of this dissector) loaded in the same Wireshark session. +emit_prologue() { + cat <<'EOF' +local _mods, _loaded = {}, {} +local _require = require -- real require, for string/math/jit/... +local function require(name) + if _loaded[name] ~= nil then return _loaded[name] end + local m = _mods[name] + if not m then return _require(name) end -- not one of ours: defer to stdlib + local r = m() + _loaded[name] = (r == nil) or r + return _loaded[name] +end + +EOF +} + +# emit_module NAME FILE -- register a module body in the private registry. +emit_module() { + printf "_mods['%s'] = function(...)\n" "$1" + cat "$2" + printf '\nend\n\n' +} + +# build OUTFILE TITLE ENTRY MODULESPEC... -- assemble one dissector; each +# MODULESPEC is "name:path_relative_to_repo_root", ENTRY is appended last. +build() { + out="$dist/$1" + title="$2" + entry="$here/$3" + shift 3 + { + print_header "$title" + emit_prologue + for spec in "$@"; do + emit_module "${spec%%:*}" "$here/${spec#*:}" + done + cat "$entry" + } > "$out" + printf 'wrote %s\n' "$out" +} + +build tarantool.dissector.lua \ + "all versions (modern MsgPack IPROTO + legacy <= 1.5)" \ + src/entry_all.lua \ + MessagePack:MessagePack.lua \ + msgpack_ext:src/msgpack_ext.lua \ + core:src/core.lua \ + modern:src/modern.lua \ + legacy:src/legacy.lua + +build tarantool-modern.dissector.lua \ + "modern MsgPack IPROTO (Tarantool 1.6 .. 3.x)" \ + src/entry_modern.lua \ + MessagePack:MessagePack.lua \ + msgpack_ext:src/msgpack_ext.lua \ + core:src/core.lua \ + modern:src/modern.lua + +build tarantool-legacy.dissector.lua \ + "legacy pre-MsgPack protocol (Tarantool <= 1.5)" \ + src/entry_legacy.lua \ + core:src/core.lua \ + legacy:src/legacy.lua diff --git a/dist/tarantool-legacy.dissector.lua b/dist/tarantool-legacy.dissector.lua new file mode 100644 index 0000000..fb505e2 --- /dev/null +++ b/dist/tarantool-legacy.dissector.lua @@ -0,0 +1,413 @@ +-- +-- Tarantool protocol dissector for Wireshark -- legacy pre-MsgPack protocol (Tarantool <= 1.5). +-- +-- GENERATED FILE -- do not edit. Built from src/ by amalgamate.sh; edit the +-- modules under src/ and re-run ./amalgamate.sh to regenerate. +-- +-- Install: copy this file into Wireshark's Personal Lua Plugins folder, renamed +-- so the filename has no extra dot before .lua (e.g. tarantool.lua), then reload +-- Lua plugins. Or run ad-hoc: tshark -X lua_script: -r capture.pcap +-- +-- Protocol reference: +-- https://www.tarantool.io/en/doc/latest/reference/internals/box_protocol/ +-- +local _mods, _loaded = {}, {} +local _require = require -- real require, for string/math/jit/... +local function require(name) + if _loaded[name] ~= nil then return _loaded[name] end + local m = _mods[name] + if not m then return _require(name) end -- not one of ours: defer to stdlib + local r = m() + _loaded[name] = (r == nil) or r + return _loaded[name] +end + +_mods['core'] = function(...) +-- Shared dissector core: the Proto, header fields, port/enabled preferences, +-- greeting, the main-loop factory and registration. Modern/legacy decoders and +-- the per-build entry points build on this. +-- +-- The Proto is created by M.init(slug, desc), which each entry point calls with +-- its own name (e.g. "tarantool1", "tarantool2", "tarantool") BEFORE requiring +-- the decoder modules -- they capture M.proto/M.pf at load time. + +local M = {} + +-- Set by M.init; the closures below capture these names as upvalues, so they +-- see the values init assigns. +local proto +local pf + +-- Request reassembly: returns nil so the caller stops and TCP redelivers more. +local function need_more(pinfo, offset, more) + if pinfo.can_desegment > 0 then + pinfo.desegment_offset = offset + pinfo.desegment_len = more + end + return nil +end +M.need_more = need_more + +local GREETING_SIZE = 128 +local GREETING_SALT_OFFSET = 64 +local GREETING_SALT_SIZE = 44 + +local function dissect_greeting(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + if available < GREETING_SIZE then + return need_more(pinfo, offset, GREETING_SIZE - available) + end + pinfo.cols.info:append('Greeting ') + local subtree = tree:add(proto, tvb(offset, GREETING_SIZE), "Tarantool greeting") + subtree:add(tvb(offset, GREETING_SALT_OFFSET), + "Server version: " .. tvb(offset, GREETING_SALT_OFFSET):string()) + subtree:add(tvb(offset + GREETING_SALT_OFFSET, GREETING_SALT_SIZE), + "Salt: " .. tvb(offset + GREETING_SALT_OFFSET, GREETING_SALT_SIZE):string()) + subtree:add(tvb(offset + GREETING_SALT_OFFSET + GREETING_SALT_SIZE, + GREETING_SIZE - GREETING_SALT_OFFSET - GREETING_SALT_SIZE), "Reserved") + return GREETING_SIZE +end +M.dissect_greeting = dissect_greeting + +-- `dispatch_pdu` decodes one non-greeting PDU (returns bytes consumed, nil for +-- reassembly, or false for "not ours"); which one is wired in distinguishes the +-- modern-only, legacy-only and combined builds. +function M.make_dissector(dispatch_pdu) + return function(tvb, pinfo, tree) + pinfo.cols.protocol = "Tarantool" + pinfo.cols.info:clear() + local n = tvb:len() + local offset = 0 + while offset < n do + local consumed + if n - offset >= 9 and tvb(offset, 9):string() == "Tarantool" then + consumed = dissect_greeting(tvb, pinfo, tree, offset) + else + consumed = dispatch_pdu(tvb, pinfo, tree, offset) + end + if consumed == nil then return -- reassembly requested + elseif not consumed then break end -- not decodable as our protocol + offset = offset + consumed + end + return offset + end +end + +local tcp_port_table = DissectorTable.get("tcp.port") +local registered_ports +local server_ports = {} + +-- Parse a Wireshark port range ("3301,3311-3313") into a lookup set, so the +-- legacy decoder can tell a server-side port from a client port for direction. +local function parse_ports(spec) + local set = {} + for part in tostring(spec):gmatch("[^,]+") do + local a, b = part:match("^%s*(%d+)%s*%-%s*(%d+)%s*$") + if a then + for p = tonumber(a), tonumber(b) do set[p] = true end + else + local n = part:match("^%s*(%d+)%s*$") + if n then set[tonumber(n)] = true end + end + end + return set +end + +-- True if `port` is one of the configured Tarantool server ports. +function M.is_server_port(port) return server_ports[port] == true end + +-- Sync the tcp.port registration with the current `enabled`/`ports` preferences. +-- `ports` is a range (e.g. "3301,3311-3313"); drop the previously registered +-- range and add the current one -- Wireshark expands the range and binds each +-- port. Idempotent: safe to call on every prefs change. +function M.register() + if registered_ports ~= nil then + tcp_port_table:remove(registered_ports, proto) + registered_ports = nil + end + server_ports = {} + if not proto.prefs.enabled then return end + tcp_port_table:add(proto.prefs.ports, proto) + registered_ports = proto.prefs.ports + server_ports = parse_ports(proto.prefs.ports) +end + +-- Create the protocol under `slug` (display name `desc`), register its header +-- fields and preferences, and wire prefs_changed. `default_port` seeds the +-- "TCP ports" range preference (3301 for modern; legacy <=1.5 used 33013) -- a +-- distinct default keeps co-loaded builds off the same port, since Wireshark's +-- tcp.port table binds one dissector per port. The user can widen it to a range +-- (e.g. "3301,3311-3313") to decode a whole cluster. Call once, before +-- requiring the decoder modules. +function M.init(slug, desc, default_port) + proto = Proto(slug, desc) + M.proto = proto + + -- Header fields, also usable as display filters (e.g. `tnt.type == 0x01`). + pf = { + type = ProtoField.uint16("tnt.type", "Request type", base.HEX), + request = ProtoField.string("tnt.request", "Request name"), + sync = ProtoField.uint64("tnt.sync", "Sync", base.DEC), + schema = ProtoField.uint64("tnt.schema_version", "Schema version", base.DEC), + stream = ProtoField.uint64("tnt.stream_id", "Stream id", base.DEC), + is_resp = ProtoField.bool("tnt.response", "Is response"), + } + M.pf = pf + proto.fields = { pf.type, pf.request, pf.sync, pf.schema, pf.stream, pf.is_resp } + + proto.prefs.enabled = Pref.bool("Dissector enabled", true, + "Whether the Tarantool dissector is enabled") + proto.prefs.ports = Pref.range("TCP ports", tostring(default_port or 3301), + "Ports to decode as Tarantool, e.g. 3301,3311-3313", 65535) + + function proto.prefs_changed() M.register() end + + return M +end + +return M + +end + +_mods['legacy'] = function(...) +-- Legacy pre-MsgPack decoder (Tarantool <= 1.5). Per doc/box-protocol.txt: +-- header ::= -- three int32, little-endian +-- tuple ::= + -- cardinality int32 LE +-- field ::= -- length is a VLQ (MSB-first) +-- Exports dissect(tvb, pinfo, tree, offset). Pure binary parsing, no MsgPack. + +local core = require 'core' + +local tarantool_proto = core.proto +local need_more = core.need_more +local pf_type = core.pf.type +local pf_request = core.pf.request +local pf_sync = core.pf.sync +local pf_is_resp = core.pf.is_resp + +-- Read a VLQ field length at `off`. Returns the value and bytes consumed. +local function legacy_varint(b, off) + local value, used = 0, 0 + while true do + local byte = b(off + used, 1):uint() + used = used + 1 + value = value * 128 + (byte % 128) + if byte < 128 then break end + end + return value, used +end + +-- Legacy fields are typeless on the wire, so guess from the bytes: printable +-- text that fills the whole field -> quoted string; otherwise a 4-/8-byte field +-- -> its little-endian unsigned integer (NUM/NUM64); else a byte count. The +-- "fills the whole field" check (#s == len) keeps this independent of how a +-- Wireshark version truncates :string() at embedded NUL bytes. +local function legacy_field_text(range) + local len = range:len() + local ok, s = pcall(function() return range:string() end) + if ok and #s == len and s:match('^[\32-\126]*$') then + return '"' .. s .. '"' + elseif len == 4 then + return tostring(range:le_uint()) + elseif len == 8 then + return tostring(range:le_uint64()) + end + return string.format('<%d bytes>', len) +end + +-- tuple ::= +. Returns bytes the tuple occupies. +local function legacy_add_tuple(b, subtree, num) + local card = b(0, 4):le_uint() + local off, parts = 4, {} + local node = subtree:add(b(0, 4), string.format('tuple #%d (cardinality %d)', num, card)) + for i = 1, card do + local flen, used = legacy_varint(b, off) + local text = legacy_field_text(b(off + used, flen)) + node:add(b(off, used + flen), string.format('[%d] %s', i, text)) + parts[#parts + 1] = text + off = off + used + flen + end + node:append_text(' {' .. table.concat(parts, ', ') .. '}') + return off +end + +local function legacy_call_req(b, subtree) + subtree:add(b(0, 4), string.format('flags: 0x%08x', b(0, 4):le_uint())) + local nlen, used = legacy_varint(b, 4) + local name = b(4 + used, nlen):string() + subtree:add(b(4, used + nlen), 'function: ' .. name) + local args_off = 4 + used + nlen + if b:len() > args_off then legacy_add_tuple(b(args_off), subtree, 0) end + subtree:append_text(string.format(' call %s(...)', name)) +end + +local function legacy_select_req(b, subtree) + local lim = b(12, 4):le_uint() + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), 'index: ' .. b(4, 4):le_uint()) + subtree:add(b(8, 4), 'offset: ' .. b(8, 4):le_uint()) + subtree:add(b(12, 4), 'limit: ' .. (lim == 4294967295 and 'unlimited' or lim)) + local count = b(16, 4):le_uint() + subtree:add(b(16, 4), 'keys: ' .. count) + local o = 20 + for i = 1, count do o = o + legacy_add_tuple(b(o), subtree, i) end +end + +local function legacy_insert_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), string.format('flags: 0x%08x', b(4, 4):le_uint())) + legacy_add_tuple(b(8), subtree, 0) +end + +local function legacy_delete_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), string.format('flags: 0x%08x', b(4, 4):le_uint())) + legacy_add_tuple(b(8), subtree, 0) +end + +-- Pre-1.5 obsolete DELETE (type 20): , no flags. +local function legacy_delete_v13_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + legacy_add_tuple(b(4), subtree, 0) +end + +-- UPDATE: +. Op encoding is +-- version-specific, so show the key, op count and the rest as a blob. +local function legacy_update_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), string.format('flags: 0x%08x', b(4, 4):le_uint())) + local off = 8 + legacy_add_tuple(b(8), subtree, 0) + if b:len() >= off + 4 then + subtree:add(b(off, 4), 'operations: ' .. b(off, 4):le_uint()) + if b:len() > off + 4 then + subtree:add(b(off + 4), string.format('ops payload: %d bytes', b:len() - off - 4)) + end + end +end + +-- fq_tuples: count-prefixed list, each tuple preceded by its u32 byte size. +local function legacy_add_fqtuples(b, subtree, count) + local o = 0 + for i = 1, count do + subtree:add(b(o, 4), string.format('tuple #%d size: %d', i, b(o, 4):le_uint())) + o = o + 4 + legacy_add_tuple(b(o + 4), subtree, i) + end +end + +-- response ::=
{}. return_code: low byte = status +-- (0 ok, 1 try again, 2 error), upper 3 bytes = error code. Body only on success. +local LEGACY_STATUS = { [0] = 'ok', [1] = 'try again', [2] = 'error' } +local function legacy_response(rtype, b, subtree) + local code = b(0, 4):le_uint() + local status, errcode = code % 256, math.floor(code / 256) + subtree:add(b(0, 4), string.format('return code: 0x%08x (%s%s)', code, + LEGACY_STATUS[status] or ('status ' .. status), + status ~= 0 and string.format(', error 0x%x', errcode) or '')) + if status ~= 0 then + if b:len() > 4 then subtree:add(b(4), 'error: ' .. b(4):string()) end + return + end + if b:len() > 4 then + local count = b(4, 4):le_uint() + subtree:add(b(4, 4), 'count: ' .. count) + if b:len() > 8 then legacy_add_fqtuples(b(8), subtree, count) end + end +end + +-- 1.5 request types (doc/box-protocol.txt), plus the pre-1.5 obsolete DELETE (20). +local LEGACY_NAME = { + [13] = 'insert', + [17] = 'select', + [19] = 'update', + [20] = 'delete_v13', + [21] = 'delete', + [22] = 'call', + [65280] = 'ping', +} +local LEGACY_REQ = { + [13] = legacy_insert_req, + [17] = legacy_select_req, + [19] = legacy_update_req, + [20] = legacy_delete_v13_req, + [21] = legacy_delete_req, + [22] = legacy_call_req, + -- 65280 (ping) has an empty body. +} + +local function dissect_legacy(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + if available < 4 then + return need_more(pinfo, offset, DESEGMENT_ONE_MORE_SEGMENT) + end + local rtype = tvb(offset, 4):le_uint() + local name = LEGACY_NAME[rtype] + if name == nil then + return false -- not a legacy header we recognise; leave it for Data + end + if available < 12 then + return need_more(pinfo, offset, 12 - available) + end + local body_len = tvb(offset + 4, 4):le_uint() + local req_id = tvb(offset + 8, 4):le_uint() + local total = 12 + body_len + if available < total then + return need_more(pinfo, offset, total - available) + end + + -- Request and response share the header (same type), so direction is the + -- only signal: match a configured server port if possible, else assume the + -- server is the lower (well-known) port. Keeps responses decoding even on a + -- non-default port via Decode As (e.g. legacy 33013). + local is_response + if core.is_server_port(pinfo.src_port) then + is_response = true + elseif core.is_server_port(pinfo.dst_port) then + is_response = false + else + is_response = pinfo.src_port < pinfo.dst_port + end + local subtree = tree:add(tarantool_proto, tvb(offset, total), + is_response and "Tarantool response (legacy <= 1.5)" + or "Tarantool request (legacy <= 1.5)") + subtree:add(pf_type, tvb(offset, 4), rtype) + subtree:add(pf_request, tvb(offset, 4), name) + subtree:add(pf_is_resp, tvb(offset, 4), is_response) + subtree:add(pf_sync, tvb(offset + 8, 4), UInt64(req_id)) + subtree:add(tvb(offset, 12), string.format( + 'legacy header: type %d (%s), body_len %d, req_id 0x%08x', + rtype, name, body_len, req_id)) + + if body_len > 0 then + local body = tvb(offset + 12, body_len) + local ok = pcall(function() + if is_response then + legacy_response(rtype, body, subtree) + else + local fn = LEGACY_REQ[rtype] + if fn then fn(body, subtree) + else subtree:add(body, string.format('body: %d bytes', body_len)) end + end + end) + if not ok then subtree:add(body, 'malformed legacy body') end + end + + pinfo.cols.info:append((is_response and 'resp ' or '') .. name .. ' ') + return total +end + +return { dissect = dissect_legacy } + +end + +-- Entry point: legacy only (Tarantool <= 1.5). No MsgPack. Unrecognised bytes +-- are left undissected (shown as Data). + +-- core.init must run before requiring the decoder: it captures core.proto at +-- load time. +local core = require 'core' +core.init('tarantool1', 'Tarantool 1.5', 33013) + +local legacy = require 'legacy' + +core.proto.dissector = core.make_dissector(legacy.dissect) +core.register() diff --git a/dist/tarantool-modern.dissector.lua b/dist/tarantool-modern.dissector.lua new file mode 100644 index 0000000..9df5173 --- /dev/null +++ b/dist/tarantool-modern.dissector.lua @@ -0,0 +1,2192 @@ +-- +-- Tarantool protocol dissector for Wireshark -- modern MsgPack IPROTO (Tarantool 1.6 .. 3.x). +-- +-- GENERATED FILE -- do not edit. Built from src/ by amalgamate.sh; edit the +-- modules under src/ and re-run ./amalgamate.sh to regenerate. +-- +-- Install: copy this file into Wireshark's Personal Lua Plugins folder, renamed +-- so the filename has no extra dot before .lua (e.g. tarantool.lua), then reload +-- Lua plugins. Or run ad-hoc: tshark -X lua_script: -r capture.pcap +-- +-- Protocol reference: +-- https://www.tarantool.io/en/doc/latest/reference/internals/box_protocol/ +-- +local _mods, _loaded = {}, {} +local _require = require -- real require, for string/math/jit/... +local function require(name) + if _loaded[name] ~= nil then return _loaded[name] end + local m = _mods[name] + if not m then return _require(name) end -- not one of ours: defer to stdlib + local r = m() + _loaded[name] = (r == nil) or r + return _loaded[name] +end + +_mods['MessagePack'] = function(...) +-- +-- lua-MessagePack : +-- + +local r, jit = pcall(require, 'jit') +if not r then + jit = nil +end + +local SIZEOF_NUMBER = string.pack and #string.pack('n', 0.0) or 8 +local NUMBER_INTEGRAL = math.type and (math.type(0.0) == math.type(0)) or false +if not jit and _VERSION < 'Lua 5.3' then + -- Lua 5.1 & 5.2 + local loadstring = loadstring or load + local luac = string.dump(loadstring "a = 1") + local header = { luac:sub(1, 12):byte(1, 12) } + SIZEOF_NUMBER = header[11] + NUMBER_INTEGRAL = 1 == header[12] +end + +local assert = assert +local error = error +local pairs = pairs +local pcall = pcall +local setmetatable = setmetatable +local tostring = tostring +local type = type +local char = require'string'.char +local floor = require'math'.floor +local tointeger = require'math'.tointeger or floor +local frexp = require'math'.frexp or require'mathx'.frexp +local ldexp = require'math'.ldexp or require'mathx'.ldexp +local huge = require'math'.huge +local tconcat = require'table'.concat + +--[[ debug only +local format = require'string'.format +local function hexadump (s) + return (s:gsub('.', function (c) return format('%02X ', c:byte()) end)) +end +--]] + +local _ENV = nil +local m = {} + +--[[ debug only +m.hexadump = hexadump +--]] + +local function argerror (caller, narg, extramsg) + error("bad argument #" .. tostring(narg) .. " to " + .. caller .. " (" .. extramsg .. ")") +end + +local function typeerror (caller, narg, arg, tname) + argerror(caller, narg, tname .. " expected, got " .. type(arg)) +end + +local function checktype (caller, narg, arg, tname) + if type(arg) ~= tname then + typeerror(caller, narg, arg, tname) + end +end + +local packers = setmetatable({}, { + __index = function (t, k) error("pack '" .. k .. "' is unimplemented") end +}) +m.packers = packers + +packers['nil'] = function (buffer) + buffer[#buffer+1] = char(0xC0) -- nil +end + +packers['boolean'] = function (buffer, bool) + if bool then + buffer[#buffer+1] = char(0xC3) -- true + else + buffer[#buffer+1] = char(0xC2) -- false + end +end + +packers['string_compat'] = function (buffer, str) + local n = #str + if n <= 0x1F then + buffer[#buffer+1] = char(0xA0 + n) -- fixstr + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xDA, -- str16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xDB, -- str32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'string_compat'" + end + buffer[#buffer+1] = str +end + +packers['_string'] = function (buffer, str) + local n = #str + if n <= 0x1F then + buffer[#buffer+1] = char(0xA0 + n) -- fixstr + elseif n <= 0xFF then + buffer[#buffer+1] = char(0xD9, -- str8 + n) + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xDA, -- str16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xDB, -- str32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'string'" + end + buffer[#buffer+1] = str +end + +packers['binary'] = function (buffer, str) + local n = #str + if n <= 0xFF then + buffer[#buffer+1] = char(0xC4, -- bin8 + n) + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xC5, -- bin16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xC6, -- bin32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'binary'" + end + buffer[#buffer+1] = str +end + +local set_string = function (str) + if str == 'string_compat' then + packers['string'] = packers['string_compat'] + elseif str == 'string' then + packers['string'] = packers['_string'] + elseif str == 'binary' then + packers['string'] = packers['binary'] + else + argerror('set_string', 1, "invalid option '" .. str .."'") + end +end +m.set_string = set_string + +packers['map'] = function (buffer, tbl, n) + if n <= 0x0F then + buffer[#buffer+1] = char(0x80 + n) -- fixmap + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xDE, -- map16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xDF, -- map32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'map'" + end + for k, v in pairs(tbl) do + packers[type(k)](buffer, k) + packers[type(v)](buffer, v) + end +end + +packers['array'] = function (buffer, tbl, n) + if n <= 0x0F then + buffer[#buffer+1] = char(0x90 + n) -- fixarray + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xDC, -- array16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xDD, -- array32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'array'" + end + for i = 1, n do + local v = tbl[i] + packers[type(v)](buffer, v) + end +end + +local set_array = function (array) + if array == 'without_hole' then + packers['_table'] = function (buffer, tbl) + local is_map, n, max = false, 0, 0 + for k in pairs(tbl) do + if type(k) == 'number' and k > 0 then + if k > max then + max = k + end + else + is_map = true + end + n = n + 1 + end + if max ~= n then -- there are holes + is_map = true + end + if is_map then + return packers['map'](buffer, tbl, n) + else + return packers['array'](buffer, tbl, n) + end + end + elseif array == 'with_hole' then + packers['_table'] = function (buffer, tbl) + local is_map, n, max = false, 0, 0 + for k in pairs(tbl) do + if type(k) == 'number' and k > 0 then + if k > max then + max = k + end + else + is_map = true + end + n = n + 1 + end + if is_map then + return packers['map'](buffer, tbl, n) + else + return packers['array'](buffer, tbl, max) + end + end + elseif array == 'always_as_map' then + packers['_table'] = function(buffer, tbl) + local n = 0 + for k in pairs(tbl) do + n = n + 1 + end + return packers['map'](buffer, tbl, n) + end + else + argerror('set_array', 1, "invalid option '" .. array .."'") + end +end +m.set_array = set_array + +packers['table'] = function (buffer, tbl) + return packers['_table'](buffer, tbl) +end + +packers['unsigned'] = function (buffer, n) + if n >= 0 then + if n <= 0x7F then + buffer[#buffer+1] = char(n) -- fixnum_pos + elseif n <= 0xFF then + buffer[#buffer+1] = char(0xCC, -- uint8 + n) + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xCD, -- uint16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xCE, -- uint32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + buffer[#buffer+1] = char(0xCF, -- uint64 + 0, -- only 53 bits from double + floor(n / 0x1000000000000) % 0x100, + floor(n / 0x10000000000) % 0x100, + floor(n / 0x100000000) % 0x100, + floor(n / 0x1000000) % 0x100, + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + end + else + if n >= -0x20 then + buffer[#buffer+1] = char(0x100 + n) -- fixnum_neg + elseif n >= -0x80 then + buffer[#buffer+1] = char(0xD0, -- int8 + 0x100 + n) + elseif n >= -0x8000 then + n = 0x10000 + n + buffer[#buffer+1] = char(0xD1, -- int16 + floor(n / 0x100), + n % 0x100) + elseif n >= -0x80000000 then + n = 4294967296.0 + n + buffer[#buffer+1] = char(0xD2, -- int32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + buffer[#buffer+1] = char(0xD3, -- int64 + 0xFF, -- only 53 bits from double + floor(n / 0x1000000000000) % 0x100, + floor(n / 0x10000000000) % 0x100, + floor(n / 0x100000000) % 0x100, + floor(n / 0x1000000) % 0x100, + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + end + end +end + +packers['signed'] = function (buffer, n) + if n >= 0 then + if n <= 0x7F then + buffer[#buffer+1] = char(n) -- fixnum_pos + elseif n <= 0x7FFF then + buffer[#buffer+1] = char(0xD1, -- int16 + floor(n / 0x100), + n % 0x100) + elseif n <= 0x7FFFFFFF then + buffer[#buffer+1] = char(0xD2, -- int32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + buffer[#buffer+1] = char(0xD3, -- int64 + 0, -- only 53 bits from double + floor(n / 0x1000000000000) % 0x100, + floor(n / 0x10000000000) % 0x100, + floor(n / 0x100000000) % 0x100, + floor(n / 0x1000000) % 0x100, + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + end + else + if n >= -0x20 then + buffer[#buffer+1] = char(0xE0 + 0x20 + n) -- fixnum_neg + elseif n >= -0x80 then + buffer[#buffer+1] = char(0xD0, -- int8 + 0x100 + n) + elseif n >= -0x8000 then + n = 0x10000 + n + buffer[#buffer+1] = char(0xD1, -- int16 + floor(n / 0x100), + n % 0x100) + elseif n >= -0x80000000 then + n = 4294967296.0 + n + buffer[#buffer+1] = char(0xD2, -- int32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + buffer[#buffer+1] = char(0xD3, -- int64 + 0xFF, -- only 53 bits from double + floor(n / 0x1000000000000) % 0x100, + floor(n / 0x10000000000) % 0x100, + floor(n / 0x100000000) % 0x100, + floor(n / 0x1000000) % 0x100, + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + end + end +end + +local set_integer = function (integer) + if integer == 'unsigned' then + packers['integer'] = packers['unsigned'] + elseif integer == 'signed' then + packers['integer'] = packers['signed'] + else + argerror('set_integer', 1, "invalid option '" .. integer .."'") + end +end +m.set_integer = set_integer + +packers['float'] = function (buffer, n) + local sign = 0 + if n < 0.0 then + sign = 0x80 + n = -n + end + local mant, expo = frexp(n) + if mant ~= mant then + buffer[#buffer+1] = char(0xCA, -- nan + 0xFF, 0x88, 0x00, 0x00) + elseif mant == huge or expo > 0x80 then + if sign == 0 then + buffer[#buffer+1] = char(0xCA, -- inf + 0x7F, 0x80, 0x00, 0x00) + else + buffer[#buffer+1] = char(0xCA, -- -inf + 0xFF, 0x80, 0x00, 0x00) + end + elseif (mant == 0.0 and expo == 0) or expo < -0x7E then + buffer[#buffer+1] = char(0xCA, -- zero + sign, 0x00, 0x00, 0x00) + else + expo = expo + 0x7E + mant = (mant * 2.0 - 1.0) * ldexp(0.5, 24) + buffer[#buffer+1] = char(0xCA, + sign + floor(expo / 0x2), + (expo % 0x2) * 0x80 + floor(mant / 0x10000), + floor(mant / 0x100) % 0x100, + mant % 0x100) + end +end + +packers['double'] = function (buffer, n) + local sign = 0 + if n < 0.0 then + sign = 0x80 + n = -n + end + local mant, expo = frexp(n) + if mant ~= mant then + buffer[#buffer+1] = char(0xCB, -- nan + 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + elseif mant == huge then + if sign == 0 then + buffer[#buffer+1] = char(0xCB, -- inf + 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + else + buffer[#buffer+1] = char(0xCB, -- -inf + 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + end + elseif mant == 0.0 and expo == 0 then + buffer[#buffer+1] = char(0xCB, -- zero + sign, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + else + expo = expo + 0x3FE + mant = (mant * 2.0 - 1.0) * ldexp(0.5, 53) + buffer[#buffer+1] = char(0xCB, + sign + floor(expo / 0x10), + (expo % 0x10) * 0x10 + floor(mant / 0x1000000000000), + floor(mant / 0x10000000000) % 0x100, + floor(mant / 0x100000000) % 0x100, + floor(mant / 0x1000000) % 0x100, + floor(mant / 0x10000) % 0x100, + floor(mant / 0x100) % 0x100, + mant % 0x100) + end +end + +local set_number = function (number) + if number == 'integer' then + packers['number'] = packers['signed'] + elseif number == 'float' then + packers['number'] = function (buffer, n) + if floor(n) ~= n or n ~= n or n > 3.40282347e+38 or n < -3.40282347e+38 then + return packers['float'](buffer, n) + else + return packers['integer'](buffer, n) + end + end + elseif number == 'double' then + packers['number'] = function (buffer, n) + if floor(n) ~= n or n ~= n or n == huge or n == -huge then + return packers['double'](buffer, n) + else + return packers['integer'](buffer, n) + end + end + else + argerror('set_number', 1, "invalid option '" .. number .."'") + end +end +m.set_number = set_number + +for k = 0, 4 do + local n = tointeger(2^k) + local fixext = 0xD4 + k + packers['fixext' .. tostring(n)] = function (buffer, tag, data) + assert(#data == n, "bad length for fixext" .. tostring(n)) + buffer[#buffer+1] = char(fixext, + tag < 0 and tag + 0x100 or tag) + buffer[#buffer+1] = data + end +end + +packers['ext'] = function (buffer, tag, data) + local n = #data + if n <= 0xFF then + buffer[#buffer+1] = char(0xC7, -- ext8 + n, + tag < 0 and tag + 0x100 or tag) + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xC8, -- ext16 + floor(n / 0x100), + n % 0x100, + tag < 0 and tag + 0x100 or tag) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xC9, -- ext&32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100, + tag < 0 and tag + 0x100 or tag) + else + error"overflow in pack 'ext'" + end + buffer[#buffer+1] = data +end + +function m.pack (data) + local buffer = {} + packers[type(data)](buffer, data) + return tconcat(buffer) +end + + +local types_map = setmetatable({ + [0xC0] = 'nil', + [0xC2] = 'false', + [0xC3] = 'true', + [0xC4] = 'bin8', + [0xC5] = 'bin16', + [0xC6] = 'bin32', + [0xC7] = 'ext8', + [0xC8] = 'ext16', + [0xC9] = 'ext32', + [0xCA] = 'float', + [0xCB] = 'double', + [0xCC] = 'uint8', + [0xCD] = 'uint16', + [0xCE] = 'uint32', + [0xCF] = 'uint64', + [0xD0] = 'int8', + [0xD1] = 'int16', + [0xD2] = 'int32', + [0xD3] = 'int64', + [0xD4] = 'fixext1', + [0xD5] = 'fixext2', + [0xD6] = 'fixext4', + [0xD7] = 'fixext8', + [0xD8] = 'fixext16', + [0xD9] = 'str8', + [0xDA] = 'str16', + [0xDB] = 'str32', + [0xDC] = 'array16', + [0xDD] = 'array32', + [0xDE] = 'map16', + [0xDF] = 'map32', +}, { __index = function (t, k) + if k < 0xC0 then + if k < 0x80 then + return 'fixnum_pos' + elseif k < 0x90 then + return 'fixmap' + elseif k < 0xA0 then + return 'fixarray' + else + return 'fixstr' + end + elseif k > 0xDF then + return 'fixnum_neg' + else + return 'reserved' .. tostring(k) + end +end }) +m.types_map = types_map + +local unpackers = setmetatable({}, { + __index = function (t, k) error("unpack '" .. k .. "' is unimplemented") end +}) +m.unpackers = unpackers + +local function unpack_array (c, n) + local t = {} + local decode = unpackers['any'] + for i = 1, n do + t[i] = decode(c) + end + return t +end + +local function unpack_map (c, n) + local t = {} + local decode = unpackers['any'] + for i = 1, n do + local k = decode(c) + local val = decode(c) + if k == nil then + k = m.sentinel + end + if k ~= nil then + t[k] = val + end + end + return t +end + +unpackers['any'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local val = s:sub(i, i):byte() + c.i = i+1 + return unpackers[types_map[val]](c, val) +end + +unpackers['nil'] = function () + return nil +end + +unpackers['false'] = function () + return false +end + +unpackers['true'] = function () + return true +end + +unpackers['float'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + local sign = b1 > 0x7F + local expo = (b1 % 0x80) * 0x2 + floor(b2 / 0x80) + local mant = ((b2 % 0x80) * 0x100 + b3) * 0x100 + b4 + if sign then + sign = -1 + else + sign = 1 + end + local n + if mant == 0 and expo == 0 then + n = sign * 0.0 + elseif expo == 0xFF then + if mant == 0 then + n = sign * huge + else + n = 0.0/0.0 + end + else + n = sign * ldexp(1.0 + mant / 0x800000, expo - 0x7F) + end + c.i = i+4 + return n +end + +unpackers['double'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+7 > j then + c:underflow(i+7) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4, b5, b6, b7, b8 = s:sub(i, i+7):byte(1, 8) + local sign = b1 > 0x7F + local expo = (b1 % 0x80) * 0x10 + floor(b2 / 0x10) + local mant = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + if sign then + sign = -1 + else + sign = 1 + end + local n + if mant == 0 and expo == 0 then + n = sign * 0.0 + elseif expo == 0x7FF then + if mant == 0 then + n = sign * huge + else + n = 0.0/0.0 + end + else + n = sign * ldexp(1.0 + mant / 4503599627370496.0, expo - 0x3FF) + end + c.i = i+8 + return n +end + +unpackers['fixnum_pos'] = function (c, val) + return val +end + +unpackers['uint8'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local b1 = s:sub(i, i):byte() + c.i = i+1 + return b1 +end + +unpackers['uint16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + c.i = i+2 + return b1 * 0x100 + b2 +end + +unpackers['uint32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + c.i = i+4 + return ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4 +end + +unpackers['uint64'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+7 > j then + c:underflow(i+7) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4, b5, b6, b7, b8 = s:sub(i, i+7):byte(1, 8) + c.i = i+8 + return ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 +end + +unpackers['fixnum_neg'] = function (c, val) + return val - 0x100 +end + +unpackers['int8'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local b1 = s:sub(i, i):byte() + c.i = i+1 + if b1 < 0x80 then + return b1 + else + return b1 - 0x100 + end +end + +unpackers['int16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + c.i = i+2 + if b1 < 0x80 then + return b1 * 0x100 + b2 + else + return ((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) - 1 + end +end + +unpackers['int32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + c.i = i+4 + if b1 < 0x80 then + return ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4 + else + return ((((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) * 0x100 + (b3 - 0xFF)) * 0x100 + (b4 - 0xFF)) - 1 + end +end + +unpackers['int64'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+7 > j then + c:underflow(i+7) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4, b5, b6, b7, b8 = s:sub(i, i+7):byte(1, 8) + c.i = i+8 + if b1 < 0x80 then + return ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + else + return ((((((((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) * 0x100 + (b3 - 0xFF)) * 0x100 + (b4 - 0xFF)) * 0x100 + (b5 - 0xFF)) * 0x100 + (b6 - 0xFF)) * 0x100 + (b7 - 0xFF)) * 0x100 + (b8 - 0xFF)) - 1 + end +end + +unpackers['fixstr'] = function (c, val) + local s, i, j = c.s, c.i, c.j + local n = val % 0x20 + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return s:sub(i, e) +end + +unpackers['str8'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local n = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return s:sub(i, e) +end + +unpackers['str16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + i = i+2 + c.i = i + local n = b1 * 0x100 + b2 + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return s:sub(i, e) +end + +unpackers['str32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + i = i+4 + c.i = i + local n = ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4 + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return s:sub(i, e) +end + +unpackers['bin8'] = unpackers['str8'] +unpackers['bin16'] = unpackers['str16'] +unpackers['bin32'] = unpackers['str32'] + +unpackers['fixarray'] = function (c, val) + return unpack_array(c, val % 0x10) +end + +unpackers['array16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + c.i = i+2 + return unpack_array(c, b1 * 0x100 + b2) +end + +unpackers['array32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + c.i = i+4 + return unpack_array(c, ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) +end + +unpackers['fixmap'] = function (c, val) + return unpack_map(c, val % 0x10) +end + +unpackers['map16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + c.i = i+2 + return unpack_map(c, b1 * 0x100 + b2) +end + +unpackers['map32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + c.i = i+4 + return unpack_map(c, ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) +end + +function m.build_ext (tag, data) + return nil +end + +for k = 0, 4 do + local n = tointeger(2^k) + unpackers['fixext' .. tostring(n)] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local tag = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return m.build_ext(tag < 0x80 and tag or tag - 0x100, s:sub(i, e)) + end +end + +unpackers['ext8'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local n = s:sub(i, i):byte() + i = i+1 + c.i = i + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local tag = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return m.build_ext(tag < 0x80 and tag or tag - 0x100, s:sub(i, e)) +end + +unpackers['ext16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + i = i+2 + c.i = i + local n = b1 * 0x100 + b2 + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local tag = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return m.build_ext(tag < 0x80 and tag or tag - 0x100, s:sub(i, e)) +end + +unpackers['ext32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + i = i+4 + c.i = i + local n = ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4 + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local tag = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return m.build_ext(tag < 0x80 and tag or tag - 0x100, s:sub(i, e)) +end + + +local function cursor_string (str) + return { + s = str, + i = 1, + j = #str, + underflow = function (self) + error "missing bytes" + end, + } +end + +local function cursor_loader (ld) + return { + s = '', + i = 1, + j = 0, + underflow = function (self, e) + self.s = self.s:sub(self.i) + e = e - self.i + 1 + self.i = 1 + self.j = 0 + while e > self.j do + local chunk = ld() + if not chunk then + error "missing bytes" + end + self.s = self.s .. chunk + self.j = #self.s + end + end, + } +end + +function m.unpack (s) + checktype('unpack', 1, s, 'string') + local cursor = cursor_string(s) + local data = unpackers['any'](cursor) + if cursor.i < cursor.j then + -- error "extra bytes" + end + -- j - strlen(s) + -- i - current position + -- print('unpack: ', cursor.j, cursor.i) + -- current position starts from zero + return data, cursor.i - 1 +end + +function m.unpacker (src) + if type(src) == 'string' then + local cursor = cursor_string(src) + return function () + if cursor.i <= cursor.j then + return cursor.i, unpackers['any'](cursor) + end + end + elseif type(src) == 'function' then + local cursor = cursor_loader(src) + return function () + if cursor.i > cursor.j then + pcall(cursor.underflow, cursor, cursor.i) + end + if cursor.i <= cursor.j then + return true, unpackers['any'](cursor) + end + end + else + argerror('unpacker', 1, "string or function expected, got " .. type(src)) + end +end + +set_string'string_compat' +set_integer'unsigned' +if NUMBER_INTEGRAL then + packers['double'] = packers['integer'] + packers['float'] = packers['integer'] + set_number'integer' +elseif SIZEOF_NUMBER == 4 then + packers['double'] = packers['float'] + m.small_lua = true + set_number'float' +else + set_number'double' +end +set_array'without_hole' + +m._VERSION = '0.3.3' +m._DESCRIPTION = "lua-MessagePack : a pure Lua implementation" +m._COPYRIGHT = "Copyright (c) 2012-2015 Francois Perrad" +return m +-- +-- This library is licensed under the terms of the MIT/X11 license, +-- like Lua itself. +-- + +end + +_mods['msgpack_ext'] = function(...) +-- Tarantool MsgPack ext (MP_EXT) decoding layered onto the bundled +-- MessagePack.lua, which by default drops ext values and renders uint64 >= 2^63 +-- as negative. Exports the configured msgpack module and the `ext_mt` marker. +-- Used only by the modern decoder (legacy <= 1.5 is not MsgPack-based). + +local msgpack = require 'MessagePack' + +local M = {} + +-- MP_EXT type ids (src/lib/core/mp_extension_types.h). +local MP_EXT_NAME = { + [0] = 'unknown', [1] = 'decimal', [2] = 'uuid', [3] = 'error', + [4] = 'datetime', [5] = 'compression', [6] = 'interval', + [7] = 'tuple', [8] = 'arrow', +} + +-- Marks a pre-formatted ext value; escape_call_arg renders its .text verbatim. +local ext_mt = {} + +local function le_uint(s, from, len) + local v = 0 + for i = len, 1, -1 do v = v * 256 + s:byte(from + i - 1) end + return v +end + +local function be_uint(s, from, len) + local v = 0 + for i = 0, len - 1 do v = v * 256 + s:byte(from + i) end + return v +end + +-- Read one MsgPack integer at `from`; returns the value and the next index. +local function read_mp_int(s, from) + local b = s:byte(from) + if b <= 0x7f then return b, from + 1 + elseif b >= 0xe0 then return b - 0x100, from + 1 + elseif b == 0xcc then return s:byte(from + 1), from + 2 + elseif b == 0xcd then return be_uint(s, from + 1, 2), from + 3 + elseif b == 0xce then return be_uint(s, from + 1, 4), from + 5 + elseif b == 0xd0 then local v = s:byte(from + 1) + return v >= 0x80 and v - 0x100 or v, from + 2 + elseif b == 0xd1 then local v = be_uint(s, from + 1, 2) + return v >= 0x8000 and v - 0x10000 or v, from + 3 + elseif b == 0xd2 then local v = be_uint(s, from + 1, 4) + return v >= 0x80000000 and v - 0x100000000 or v, from + 5 + elseif b == 0xcf then return be_uint(s, from + 1, 8), from + 9 + elseif b == 0xd3 then return be_uint(s, from + 1, 8), from + 9 + end + return 0, from + 1 +end + +-- MP_UUID (fixext16): 16 bytes -> canonical UUID string. +local function decode_uuid(data) + local h = {} + for i = 1, 16 do h[i] = string.format('%02x', data:byte(i)) end + return table.concat(h, '', 1, 4) .. '-' .. table.concat(h, '', 5, 6) .. '-' + .. table.concat(h, '', 7, 8) .. '-' .. table.concat(h, '', 9, 10) .. '-' + .. table.concat(h, '', 11, 16) +end + +-- MP_DATETIME (fixext8/16): int64 LE seconds [+ nsec, tzoffset, tzindex]. +local function decode_datetime(data) + local secs = le_uint(data, 1, 8) + local nsec, tzoffset = 0, 0 + if #data >= 16 then + nsec = le_uint(data, 9, 4) + tzoffset = le_uint(data, 13, 2) + if tzoffset >= 0x8000 then tzoffset = tzoffset - 0x10000 end + end + -- Shift the UTC instant by the stored offset so the printed wall clock + -- matches the appended timezone. + local out = 'epoch=' .. string.format('%d', secs) + if os and os.date then + local ok, formatted = pcall(os.date, '!%Y-%m-%dT%H:%M:%S', secs + tzoffset * 60) + if ok and formatted then out = formatted end + end + if nsec > 0 then + local frac = string.format('%09d', nsec):gsub('0+$', '') + out = out .. '.' .. frac + end + if tzoffset ~= 0 then + local m = math.abs(tzoffset) + out = out .. string.format('%s%02d:%02d', tzoffset < 0 and '-' or '+', + math.floor(m / 60), m % 60) + else + out = out .. 'Z' + end + return out +end + +-- MP_DECIMAL: MsgPack scale (-exponent) followed by packed-BCD coefficient. +local function decode_decimal(data) + local scale, pos = read_mp_int(data, 1) + local digits, sign, last = {}, '', #data + for i = pos, last do + local byte = data:byte(i) + digits[#digits + 1] = math.floor(byte / 16) + if i < last then + digits[#digits + 1] = byte % 16 + else + local nibble = byte % 16 -- last low nibble is the sign + sign = (nibble == 0x0b or nibble == 0x0d) and '-' or '' + end + end + local s = table.concat(digits):gsub('^0+(%d)', '%1') + if scale > 0 then + if #s <= scale then s = string.rep('0', scale - #s + 1) .. s end + s = s:sub(1, #s - scale) .. '.' .. s:sub(#s - scale + 1) + elseif scale < 0 then + s = s .. string.rep('0', -scale) + end + return sign .. s +end + +local INTERVAL_FIELD = { + [0] = 'year', [1] = 'month', [2] = 'week', [3] = 'day', [4] = 'hour', + [5] = 'min', [6] = 'sec', [7] = 'nsec', [8] = 'adjust', +} + +-- MP_INTERVAL: u8 count, then count (u8 field_id, MsgPack value) pairs. +local function decode_interval(data) + local count, pos = data:byte(1), 2 + local parts = {} + for _ = 1, count do + local fid = data:byte(pos) + local val + val, pos = read_mp_int(data, pos + 1) + parts[#parts + 1] = (INTERVAL_FIELD[fid] or ('f' .. fid)) .. '=' .. val + end + return '{' .. table.concat(parts, ', ') .. '}' +end + +local EXT_DECODER = { + [1] = decode_decimal, [2] = decode_uuid, + [4] = decode_datetime, [6] = decode_interval, +} + +-- Decode known scalar ext types; render opaque ones as a labelled blob. +function msgpack.build_ext(tag, data) + local decoder = EXT_DECODER[tag] + if decoder then + local ok, result = pcall(decoder, data) + if ok and result ~= nil then + return setmetatable({text = result}, ext_mt) + end + end + return setmetatable({text = string.format('<%s ext, %d byte%s>', + MP_EXT_NAME[tag] or ('type ' .. tag), #data, + #data == 1 and '' or 's')}, ext_mt) +end + +-- MessagePack.lua's uint64 decode wraps to a signed Lua integer, so values +-- >= 2^63 come back negative; re-render those as unsigned. int64 (genuinely +-- signed) and non-negative values pass through. Headers use exact_uint instead. +local raw_uint64 = msgpack.unpackers['uint64'] +msgpack.unpackers['uint64'] = function(c) + local n = raw_uint64(c) + if type(n) == 'number' and n < 0 then + return setmetatable({text = string.format('%u', n)}, ext_mt) + end + return n +end + +M.msgpack = msgpack +M.ext_mt = ext_mt +return M + +end + +_mods['core'] = function(...) +-- Shared dissector core: the Proto, header fields, port/enabled preferences, +-- greeting, the main-loop factory and registration. Modern/legacy decoders and +-- the per-build entry points build on this. +-- +-- The Proto is created by M.init(slug, desc), which each entry point calls with +-- its own name (e.g. "tarantool1", "tarantool2", "tarantool") BEFORE requiring +-- the decoder modules -- they capture M.proto/M.pf at load time. + +local M = {} + +-- Set by M.init; the closures below capture these names as upvalues, so they +-- see the values init assigns. +local proto +local pf + +-- Request reassembly: returns nil so the caller stops and TCP redelivers more. +local function need_more(pinfo, offset, more) + if pinfo.can_desegment > 0 then + pinfo.desegment_offset = offset + pinfo.desegment_len = more + end + return nil +end +M.need_more = need_more + +local GREETING_SIZE = 128 +local GREETING_SALT_OFFSET = 64 +local GREETING_SALT_SIZE = 44 + +local function dissect_greeting(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + if available < GREETING_SIZE then + return need_more(pinfo, offset, GREETING_SIZE - available) + end + pinfo.cols.info:append('Greeting ') + local subtree = tree:add(proto, tvb(offset, GREETING_SIZE), "Tarantool greeting") + subtree:add(tvb(offset, GREETING_SALT_OFFSET), + "Server version: " .. tvb(offset, GREETING_SALT_OFFSET):string()) + subtree:add(tvb(offset + GREETING_SALT_OFFSET, GREETING_SALT_SIZE), + "Salt: " .. tvb(offset + GREETING_SALT_OFFSET, GREETING_SALT_SIZE):string()) + subtree:add(tvb(offset + GREETING_SALT_OFFSET + GREETING_SALT_SIZE, + GREETING_SIZE - GREETING_SALT_OFFSET - GREETING_SALT_SIZE), "Reserved") + return GREETING_SIZE +end +M.dissect_greeting = dissect_greeting + +-- `dispatch_pdu` decodes one non-greeting PDU (returns bytes consumed, nil for +-- reassembly, or false for "not ours"); which one is wired in distinguishes the +-- modern-only, legacy-only and combined builds. +function M.make_dissector(dispatch_pdu) + return function(tvb, pinfo, tree) + pinfo.cols.protocol = "Tarantool" + pinfo.cols.info:clear() + local n = tvb:len() + local offset = 0 + while offset < n do + local consumed + if n - offset >= 9 and tvb(offset, 9):string() == "Tarantool" then + consumed = dissect_greeting(tvb, pinfo, tree, offset) + else + consumed = dispatch_pdu(tvb, pinfo, tree, offset) + end + if consumed == nil then return -- reassembly requested + elseif not consumed then break end -- not decodable as our protocol + offset = offset + consumed + end + return offset + end +end + +local tcp_port_table = DissectorTable.get("tcp.port") +local registered_ports +local server_ports = {} + +-- Parse a Wireshark port range ("3301,3311-3313") into a lookup set, so the +-- legacy decoder can tell a server-side port from a client port for direction. +local function parse_ports(spec) + local set = {} + for part in tostring(spec):gmatch("[^,]+") do + local a, b = part:match("^%s*(%d+)%s*%-%s*(%d+)%s*$") + if a then + for p = tonumber(a), tonumber(b) do set[p] = true end + else + local n = part:match("^%s*(%d+)%s*$") + if n then set[tonumber(n)] = true end + end + end + return set +end + +-- True if `port` is one of the configured Tarantool server ports. +function M.is_server_port(port) return server_ports[port] == true end + +-- Sync the tcp.port registration with the current `enabled`/`ports` preferences. +-- `ports` is a range (e.g. "3301,3311-3313"); drop the previously registered +-- range and add the current one -- Wireshark expands the range and binds each +-- port. Idempotent: safe to call on every prefs change. +function M.register() + if registered_ports ~= nil then + tcp_port_table:remove(registered_ports, proto) + registered_ports = nil + end + server_ports = {} + if not proto.prefs.enabled then return end + tcp_port_table:add(proto.prefs.ports, proto) + registered_ports = proto.prefs.ports + server_ports = parse_ports(proto.prefs.ports) +end + +-- Create the protocol under `slug` (display name `desc`), register its header +-- fields and preferences, and wire prefs_changed. `default_port` seeds the +-- "TCP ports" range preference (3301 for modern; legacy <=1.5 used 33013) -- a +-- distinct default keeps co-loaded builds off the same port, since Wireshark's +-- tcp.port table binds one dissector per port. The user can widen it to a range +-- (e.g. "3301,3311-3313") to decode a whole cluster. Call once, before +-- requiring the decoder modules. +function M.init(slug, desc, default_port) + proto = Proto(slug, desc) + M.proto = proto + + -- Header fields, also usable as display filters (e.g. `tnt.type == 0x01`). + pf = { + type = ProtoField.uint16("tnt.type", "Request type", base.HEX), + request = ProtoField.string("tnt.request", "Request name"), + sync = ProtoField.uint64("tnt.sync", "Sync", base.DEC), + schema = ProtoField.uint64("tnt.schema_version", "Schema version", base.DEC), + stream = ProtoField.uint64("tnt.stream_id", "Stream id", base.DEC), + is_resp = ProtoField.bool("tnt.response", "Is response"), + } + M.pf = pf + proto.fields = { pf.type, pf.request, pf.sync, pf.schema, pf.stream, pf.is_resp } + + proto.prefs.enabled = Pref.bool("Dissector enabled", true, + "Whether the Tarantool dissector is enabled") + proto.prefs.ports = Pref.range("TCP ports", tostring(default_port or 3301), + "Ports to decode as Tarantool, e.g. 3301,3311-3313", 65535) + + function proto.prefs_changed() M.register() end + + return M +end + +return M + +end + +_mods['modern'] = function(...) +-- Modern MsgPack IPROTO decoder (Tarantool 1.6 .. 3.x). A PDU is a 5-byte 0xce +-- uint32 length prefix, then a header map and an optional body map. Exports +-- dissect(tvb, pinfo, tree, offset). + +local core = require 'core' +local mpx = require 'msgpack_ext' + +local msgpack = mpx.msgpack +local ext_mt = mpx.ext_mt + +local tarantool_proto = core.proto +local need_more = core.need_more +local pf_type = core.pf.type +local pf_request = core.pf.request +local pf_sync = core.pf.sync +local pf_schema = core.pf.schema +local pf_stream = core.pf.stream +local pf_is_resp = core.pf.is_resp + +-- iproto_type: request/command codes (src/box/iproto_constants.h). +local OK = 0x00 +local SELECT = 0x01 +local INSERT = 0x02 +local REPLACE = 0x03 +local UPDATE = 0x04 +local DELETE = 0x05 +local CALL_16 = 0x06 -- 1.6-era call: coerced results into tuples; kept as call_16 +local AUTH = 0x07 +local EVAL = 0x08 +local UPSERT = 0x09 +local CALL = 0x0a -- modern call (1.7.2+): returns a plain array +local EXECUTE = 0x0b +local NOP = 0x0c +local PREPARE = 0x0d +local BEGIN = 0x0e +local COMMIT = 0x0f +local ROLLBACK = 0x10 +local INSERT_ARROW = 0x11 +local RAFT = 0x1e +local RAFT_PROMOTE = 0x1f +local RAFT_DEMOTE = 0x20 +local RAFT_CONFIRM = 0x28 +local RAFT_ROLLBACK = 0x29 +local PING = 0x40 +local JOIN = 0x41 +local SUBSCRIBE = 0x42 +local VOTE_DEPRECATED = 0x43 +local VOTE = 0x44 +local FETCH_SNAPSHOT = 0x45 +local REGISTER = 0x46 +local JOIN_META = 0x47 +local JOIN_SNAPSHOT = 0x48 +local ID = 0x49 +local WATCH = 0x4a +local UNWATCH = 0x4b +local EVENT = 0x4c +local WATCH_ONCE = 0x4d + +-- Response markers. +local CHUNK = 0x80 -- non-final response chunk (box.session.push) +local TYPE_ERROR = 0x8000 -- bit 15 set => error, low 15 bits = errcode + +-- iproto_key: header keys (0x00 .. 0x0b). +local TYPE = 0x00 -- IPROTO_REQUEST_TYPE +local SYNC = 0x01 +local REPLICA_ID = 0x02 +local LSN = 0x03 +local TIMESTAMP = 0x04 +local SCHEMA_VERSION = 0x05 +local SERVER_VERSION = 0x06 +local GROUP_ID = 0x07 +local TSN = 0x08 +local FLAGS = 0x09 +local STREAM_ID = 0x0a +local THREAD_ID = 0x0b + +-- iproto_key: DML body keys (0x10 .. 0x2f). +local SPACE_ID = 0x10 +local INDEX_ID = 0x11 +local LIMIT = 0x12 +local OFFSET = 0x13 +local ITERATOR = 0x14 +local INDEX_BASE = 0x15 +local FETCH_POSITION = 0x1f +local KEY = 0x20 +local TUPLE = 0x21 +local FUNCTION_NAME = 0x22 +local USER_NAME = 0x23 +local INSTANCE_UUID = 0x24 +local REPLICASET_UUID = 0x25 +local VCLOCK = 0x26 +local EXPRESSION = 0x27 +local OPS = 0x28 +local BALLOT = 0x29 +local OLD_TUPLE = 0x2c +local NEW_TUPLE = 0x2d +local AFTER_POSITION = 0x2e +local AFTER_TUPLE = 0x2f + +-- iproto_key: response keys (0x30 .. 0x35). +local DATA = 0x30 +local ERROR_24 = 0x31 -- legacy string error +local METADATA = 0x32 +local BIND_METADATA = 0x33 +local BIND_COUNT = 0x34 +local POSITION = 0x35 + +-- iproto_key: SQL keys (0x40 .. 0x43). +local SQL_TEXT = 0x40 +local SQL_BIND = 0x41 +local SQL_INFO = 0x42 +local STMT_ID = 0x43 + +-- Nested keys inside response sub-structures. +local FIELD_NAME = 0x00 -- column maps in METADATA +local FIELD_TYPE = 0x01 +local SQL_INFO_ROW_COUNT = 0x00 -- inside SQL_INFO map + +-- iproto_key: extended keys (0x50 .. 0x64). +local REPLICA_ANON = 0x50 +local ID_FILTER = 0x51 +local ERROR = 0x52 -- structured error stack (MP_MAP) +local TERM = 0x53 +local VERSION = 0x54 +local FEATURES = 0x55 +local TIMEOUT = 0x56 +local EVENT_KEY = 0x57 +local EVENT_DATA = 0x58 +local TXN_ISOLATION = 0x59 +local VCLOCK_SYNC = 0x5a +local AUTH_TYPE = 0x5b +local REPLICASET_NAME = 0x5c +local INSTANCE_NAME = 0x5d +local SPACE_NAME = 0x5e +local INDEX_NAME = 0x5f +local IS_SYNC = 0x61 + +-- iterator types (box.index iterator codes), for nicer SELECT output. +local ITERATOR_NAME = { + [0] = 'EQ', [1] = 'REQ', [2] = 'ALL', [3] = 'LT', [4] = 'LE', + [5] = 'GE', [6] = 'GT', [7] = 'BITS_ALL_SET', [8] = 'BITS_ANY_SET', + [9] = 'BITS_ALL_NOT_SET', [10] = 'OVERLAPS', [11] = 'NEIGHBOR', +} + +-- helpers --------------------------------------------------------------------- + +-- 0-based wire offset of each value in a top-level MsgPack map, keyed by map key, +-- so 64-bit header fields can be read exactly (see exact_uint) instead of via +-- MessagePack.lua's lossy decode. +local function map_value_offsets(raw, base_off) + local b = raw:byte(1) + local count, first + if b >= 0x80 and b <= 0x8f then count, first = b - 0x80, 2 + elseif b == 0xde then count, first = raw:byte(2) * 256 + raw:byte(3), 4 + elseif b == 0xdf then + count = ((raw:byte(2) * 256 + raw:byte(3)) * 256 + raw:byte(4)) * 256 + + raw:byte(5) + first = 6 + else + return {} + end + local offsets = {} + local iter = msgpack.unpacker(raw:sub(first)) + for _ = 1, count do + local _, key = iter() -- key element + local value_pos = iter() -- start of value element (1-based within sub) + if value_pos == nil then break end + offsets[key] = base_off + first + value_pos - 2 + end + return offsets +end + +-- Read the MsgPack uint at `off` as a full-precision Wireshark UInt64, or nil if +-- `off` is nil or the bytes are not a uint. +local function exact_uint(tvb, off) + if off == nil then return nil end + local b = tvb(off, 1):uint() + if b <= 0x7f then return UInt64(b) -- positive fixint + elseif b == 0xcc then return UInt64(tvb(off + 1, 1):uint()) + elseif b == 0xcd then return UInt64(tvb(off + 1, 2):uint()) + elseif b == 0xce then return UInt64(tvb(off + 1, 4):uint()) + elseif b == 0xcf then return tvb(off + 1, 8):uint64() + end + return nil +end + +local function map(tbl, callback) + local result = {} + if tbl == nil then return result end + for k, v in pairs(tbl) do + result[k] = callback(v) + end + return result +end + +local function table_kv_concat(tbl, sep) + local result = {} + local used_keys = {} + for i, v in ipairs(tbl) do + used_keys[i] = true + table.insert(result, v) + end + for k, v in pairs(tbl) do + if not used_keys[k] then + local key = (type(k) == 'table' and getmetatable(k) == ext_mt) + and k.text or tostring(k) + table.insert(result, key .. ' = ' .. tostring(v)) + end + end + return table.concat(result, sep) +end + +local function escape_call_arg(a) + if type(a) == 'table' and getmetatable(a) == ext_mt then + return a.text -- a decoded MP_EXT value (datetime, decimal, uuid, ...) + end + local t = type(a) + if t == 'number' or t == 'boolean' then + return tostring(a) + elseif t == 'string' then + return '"' .. a .. '"' + elseif t == 'table' then + return '{' .. table_kv_concat(map(a, escape_call_arg), ', ') .. '}' + elseif a == nil then + return 'nil' + end + return tostring(a) +end + +-- Concatenate an array (possibly nil) of msgpack values into a readable string. +local function join_args(arr) + return table.concat(map(arr, escape_call_arg), ', ') +end + +-- Add a "label: value" node only when the value is present. +local function add_opt(subtree, buffer, label, value) + if value ~= nil then + subtree:add(buffer, label .. ': ' .. escape_call_arg(value)) + end +end + +-- decoders -------------------------------------------------------------------- +-- Each receives (body_table, body_tvbrange, subtree); body_table may be empty +-- for body-less requests (PING, VOTE, ...). + +local function parse_call(tbl, buffer, subtree) + local name = tbl[FUNCTION_NAME] + local args = tbl[TUPLE] + subtree:add(buffer, string.format('%s(%s)', tostring(name), join_args(args))) +end + +local function parse_eval(tbl, buffer, subtree) + local expression = tbl[EXPRESSION] + local args = tbl[TUPLE] + subtree:add(buffer, string.format('eval %s with args (%s)', + tostring(expression), join_args(args))) +end + +local function parse_select(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + local index = tbl[INDEX_NAME] or tbl[INDEX_ID] or 0 + local limit = tbl[LIMIT] + local offset = tbl[OFFSET] or 0 + local iterator = tbl[ITERATOR] or 0 + + subtree:add(buffer, string.format( + 'SELECT FROM space %s WHERE index(%s) = (%s) LIMIT %s OFFSET %d ITERATOR %s', + tostring(space), tostring(index), join_args(tbl[KEY]), + tostring(limit), offset, + ITERATOR_NAME[iterator] or tostring(iterator))) + -- Pagination (request side). + add_opt(subtree, buffer, 'fetch_position', tbl[FETCH_POSITION]) + add_opt(subtree, buffer, 'after_position', tbl[AFTER_POSITION]) + if tbl[AFTER_TUPLE] ~= nil then + subtree:add(buffer, 'after_tuple: {' .. join_args(tbl[AFTER_TUPLE]) .. '}') + end +end + +local function parse_insert(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'tuple: {' .. join_args(tbl[TUPLE]) .. '}') + -- Before/after images carried by replicated DML rows. + add_opt(subtree, buffer, 'old_tuple', tbl[OLD_TUPLE]) + add_opt(subtree, buffer, 'new_tuple', tbl[NEW_TUPLE]) +end + +local function parse_delete(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + local index = tbl[INDEX_NAME] or tbl[INDEX_ID] or 0 + subtree:add(buffer, string.format('DELETE FROM space(%s) WHERE index(%s) = (%s)', + tostring(space), tostring(index), join_args(tbl[KEY]))) +end + +local function parse_upsert(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'tuple: {' .. join_args(tbl[TUPLE]) .. '}') + subtree:add(buffer, 'ops: {' .. join_args(tbl[OPS]) .. '}') + add_opt(subtree, buffer, 'index_base', tbl[INDEX_BASE]) +end + +local function parse_update(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + local index = tbl[INDEX_NAME] or tbl[INDEX_ID] or 0 + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'index: ' .. tostring(index)) + subtree:add(buffer, 'key: {' .. join_args(tbl[KEY]) .. '}') + subtree:add(buffer, 'ops: {' .. join_args(tbl[TUPLE]) .. '}') + add_opt(subtree, buffer, 'index_base', tbl[INDEX_BASE]) + add_opt(subtree, buffer, 'old_tuple', tbl[OLD_TUPLE]) + add_opt(subtree, buffer, 'new_tuple', tbl[NEW_TUPLE]) +end + +local function parse_auth(tbl, buffer, subtree) + local user = tbl[USER_NAME] + local tuple = tbl[TUPLE] or {} + subtree:add(buffer, string.format( + 'Authentication: user "%s", mechanism %s', + tostring(user), tostring(tuple[1]))) +end + +local function parse_id(tbl, buffer, subtree) + subtree:add(buffer, 'protocol version: ' .. tostring(tbl[VERSION])) + subtree:add(buffer, 'features: {' .. join_args(tbl[FEATURES]) .. '}') + if tbl[AUTH_TYPE] ~= nil then + subtree:add(buffer, 'auth_type: ' .. tostring(tbl[AUTH_TYPE])) + end +end + +local function parse_execute(tbl, buffer, subtree) + local stmt_id = tbl[STMT_ID] + local sql_text = tbl[SQL_TEXT] + local bind = join_args(tbl[SQL_BIND]) + if bind ~= '' then + bind = string.format(', with parameters (%s)', bind) + end + if stmt_id ~= nil then + subtree:add(buffer, string.format( + 'execute prepared statement id %s%s', tostring(stmt_id), bind)) + else + subtree:add(buffer, string.format( + 'execute SQL "%s"%s', tostring(sql_text), bind)) + end +end + +local function parse_prepare(tbl, buffer, subtree) + local stmt_id = tbl[STMT_ID] + local sql_text = tbl[SQL_TEXT] + if stmt_id ~= nil then + subtree:add(buffer, 'unprepare/prepare statement id ' .. tostring(stmt_id)) + else + subtree:add(buffer, string.format('prepare SQL "%s"', tostring(sql_text))) + end +end + +local function parse_begin(tbl, buffer, subtree) + add_opt(subtree, buffer, 'timeout', tbl[TIMEOUT]) + add_opt(subtree, buffer, 'txn_isolation', tbl[TXN_ISOLATION]) + add_opt(subtree, buffer, 'is_sync', tbl[IS_SYNC]) +end + +local function parse_commit(tbl, buffer, subtree) + add_opt(subtree, buffer, 'is_sync', tbl[IS_SYNC]) +end + +local function parse_insert_arrow(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'arrow: ') +end + +local function parse_watch(tbl, buffer, subtree) + subtree:add(buffer, 'event key: ' .. tostring(tbl[EVENT_KEY])) + if tbl[EVENT_DATA] ~= nil then + subtree:add(buffer, 'event data: ' .. escape_call_arg(tbl[EVENT_DATA])) + end +end + +local function parse_synchro(tbl, buffer, subtree) + subtree:add(buffer, string.format( + 'replica_id: %s, lsn: %s, term: %s', + tostring(tbl[REPLICA_ID]), tostring(tbl[LSN]), tostring(tbl[TERM]))) +end + +-- Render a vclock ({replica_id = lsn}) as "{0 = 2, 1 = 9}", iterating sorted keys +-- (a plain table_kv_concat would print id 1 positionally as "{9, 0 = 2}"). +local function vclock_str(vclock) + if type(vclock) ~= 'table' then return tostring(vclock) end + local keys = {} + for k in pairs(vclock) do keys[#keys + 1] = k end + table.sort(keys) + local parts = {} + for _, k in ipairs(keys) do + parts[#parts + 1] = tostring(k) .. ' = ' .. escape_call_arg(vclock[k]) + end + return '{' .. table.concat(parts, ', ') .. '}' +end + +local function parse_subscribe(tbl, buffer, subtree) + subtree:add(buffer, 'instance_uuid: ' .. tostring(tbl[INSTANCE_UUID])) + subtree:add(buffer, 'replicaset_uuid: ' .. tostring(tbl[REPLICASET_UUID])) + if tbl[VCLOCK] ~= nil then + subtree:add(buffer, 'vclock: ' .. vclock_str(tbl[VCLOCK])) + end + add_opt(subtree, buffer, 'instance_name', tbl[INSTANCE_NAME]) + add_opt(subtree, buffer, 'replicaset_name', tbl[REPLICASET_NAME]) + add_opt(subtree, buffer, 'server_version', tbl[SERVER_VERSION]) + add_opt(subtree, buffer, 'replica_anon', tbl[REPLICA_ANON]) + if tbl[ID_FILTER] ~= nil then + subtree:add(buffer, 'id_filter: {' .. join_args(tbl[ID_FILTER]) .. '}') + end +end + +-- Shared by JOIN, FETCH_SNAPSHOT and REGISTER. +local function parse_join(tbl, buffer, subtree) + subtree:add(buffer, 'instance_uuid: ' .. tostring(tbl[INSTANCE_UUID])) + add_opt(subtree, buffer, 'instance_name', tbl[INSTANCE_NAME]) + add_opt(subtree, buffer, 'server_version', tbl[SERVER_VERSION]) + if tbl[VCLOCK] ~= nil then + subtree:add(buffer, 'vclock: ' .. vclock_str(tbl[VCLOCK])) + end +end + +-- IPROTO_ERROR (0x52) is a map { MP_ERROR_STACK: [ frame, ... ] }; each frame is +-- a map keyed by these field ids (src/box/mp_error.cc). +local MP_ERROR_STACK = 0x00 +local MP_ERROR_TYPE = 0x00 +local MP_ERROR_FILE = 0x01 +local MP_ERROR_LINE = 0x02 +local MP_ERROR_MESSAGE = 0x03 +local MP_ERROR_ERRNO = 0x04 +local MP_ERROR_CODE = 0x05 +local MP_ERROR_FIELDS = 0x06 + +-- Render the structured error stack as named fields instead of a raw map dump. +local function add_error_stack(subtree, buffer, err) + local stack = (type(err) == 'table') and err[MP_ERROR_STACK] or nil + if type(stack) ~= 'table' then + subtree:add(buffer, 'error: ' .. escape_call_arg(err)) -- unexpected shape + return + end + local node = subtree:add(buffer, 'error stack') + for i, frame in ipairs(stack) do + if type(frame) == 'table' then + local head = string.format('[%d] %s', i, + tostring(frame[MP_ERROR_TYPE] or '?')) + if frame[MP_ERROR_CODE] ~= nil then + head = head .. string.format(' (code %s)', + tostring(frame[MP_ERROR_CODE])) + end + if frame[MP_ERROR_MESSAGE] ~= nil then + head = head .. ': ' .. tostring(frame[MP_ERROR_MESSAGE]) + end + local fnode = node:add(buffer, head) + add_opt(fnode, buffer, 'errno', frame[MP_ERROR_ERRNO]) + if frame[MP_ERROR_FILE] ~= nil then + fnode:add(buffer, string.format('at %s:%s', + tostring(frame[MP_ERROR_FILE]), + tostring(frame[MP_ERROR_LINE]))) + end + if frame[MP_ERROR_FIELDS] ~= nil then + fnode:add(buffer, 'fields: ' .. escape_call_arg(frame[MP_ERROR_FIELDS])) + end + end + end +end + +local function parse_error_response(tbl, buffer, subtree) + if tbl == nil then + subtree:add(buffer, '(empty response body)') + return + end + if tbl[ERROR_24] ~= nil then + subtree:add(buffer, 'message: ' .. tostring(tbl[ERROR_24])) + end + if tbl[ERROR] ~= nil then + add_error_stack(subtree, buffer, tbl[ERROR]) + end + if tbl[ERROR_24] == nil and tbl[ERROR] == nil then + subtree:add(buffer, '(empty response body)') + end +end + +-- Responses carry no request type, so surface whichever known response keys are +-- present (data, SQL metadata, PREPARE info, cursor, ballot, ID/SUBSCRIBE). +local function parse_response(tbl, buffer, subtree) + if tbl == nil or next(tbl) == nil then + subtree:add(buffer, '(empty response body)') + return + end + + if tbl[METADATA] ~= nil then + local node = subtree:add(buffer, 'metadata') + for _, col in ipairs(tbl[METADATA]) do + node:add(buffer, tostring(col[FIELD_NAME]) .. ' : ' .. + tostring(col[FIELD_TYPE])) + end + end + if tbl[DATA] ~= nil then + local node = subtree:add(buffer, 'data') + if type(tbl[DATA]) == 'table' and getmetatable(tbl[DATA]) ~= ext_mt then + for _, v in ipairs(map(tbl[DATA], escape_call_arg)) do + node:add(buffer, v) + end + else + node:add(buffer, escape_call_arg(tbl[DATA])) -- bare scalar/ext payload + end + end + if tbl[SQL_INFO] ~= nil then + add_opt(subtree, buffer, 'sql row_count', tbl[SQL_INFO][SQL_INFO_ROW_COUNT]) + end + add_opt(subtree, buffer, 'position', tbl[POSITION]) + add_opt(subtree, buffer, 'stmt_id', tbl[STMT_ID]) + add_opt(subtree, buffer, 'bind_count', tbl[BIND_COUNT]) + add_opt(subtree, buffer, 'bind_metadata', tbl[BIND_METADATA]) + add_opt(subtree, buffer, 'version', tbl[VERSION]) + if tbl[FEATURES] ~= nil then + subtree:add(buffer, 'features: {' .. join_args(tbl[FEATURES]) .. '}') + end + add_opt(subtree, buffer, 'auth_type', tbl[AUTH_TYPE]) + add_opt(subtree, buffer, 'ballot', tbl[BALLOT]) + add_opt(subtree, buffer, 'replicaset_uuid', tbl[REPLICASET_UUID]) + if tbl[VCLOCK] ~= nil then + subtree:add(buffer, 'vclock: ' .. vclock_str(tbl[VCLOCK])) + end +end + +local function parse_nop(tbl, buffer, subtree) + subtree:add(buffer, 'NOP (No Operation)') +end + +local function parse_empty(tbl, buffer, subtree) + -- Body-less request (PING, VOTE, ROLLBACK, UNWATCH, ...) — nothing to show. +end + +local function parser_not_implemented(tbl, buffer, subtree) + subtree:add(buffer, 'parser not yet implemented') +end + +local UNKNOWN_COMMAND = {name = 'UNKNOWN', decoder = parser_not_implemented} + +local COMMANDS = { + [SELECT] = {name = 'select', decoder = parse_select}, + [INSERT] = {name = 'insert', decoder = parse_insert}, + [REPLACE] = {name = 'replace', decoder = parse_insert}, + [UPDATE] = {name = 'update', decoder = parse_update}, + [DELETE] = {name = 'delete', decoder = parse_delete}, + [CALL] = {name = 'call', decoder = parse_call}, + [CALL_16] = {name = 'call_16', decoder = parse_call}, + [AUTH] = {name = 'auth', decoder = parse_auth}, + [EVAL] = {name = 'eval', decoder = parse_eval}, + [UPSERT] = {name = 'upsert', decoder = parse_upsert}, + [EXECUTE] = {name = 'execute', decoder = parse_execute}, + [NOP] = {name = 'nop', decoder = parse_nop}, + [PREPARE] = {name = 'prepare', decoder = parse_prepare}, + [BEGIN] = {name = 'begin', decoder = parse_begin}, + [COMMIT] = {name = 'commit', decoder = parse_commit}, + [ROLLBACK] = {name = 'rollback', decoder = parse_empty}, + [INSERT_ARROW] = {name = 'insert_arrow', decoder = parse_insert_arrow}, + [ID] = {name = 'id', decoder = parse_id}, + [WATCH] = {name = 'watch', decoder = parse_watch}, + [UNWATCH] = {name = 'unwatch', decoder = parse_watch}, + [EVENT] = {name = 'event', decoder = parse_watch}, + [WATCH_ONCE] = {name = 'watch_once', decoder = parse_watch}, + [JOIN] = {name = 'join', decoder = parse_join}, + [JOIN_META] = {name = 'join_meta', decoder = parse_empty}, + [JOIN_SNAPSHOT] = {name = 'join_snapshot', decoder = parse_empty}, + [SUBSCRIBE] = {name = 'subscribe', decoder = parse_subscribe}, + [VOTE] = {name = 'vote', decoder = parse_empty}, + [VOTE_DEPRECATED] = {name = 'vote_deprecated', decoder = parse_empty}, + [FETCH_SNAPSHOT] = {name = 'fetch_snapshot', decoder = parse_join}, + [REGISTER] = {name = 'register', decoder = parse_join}, + [RAFT] = {name = 'raft', decoder = parser_not_implemented}, + [RAFT_PROMOTE] = {name = 'raft_promote', decoder = parse_synchro}, + [RAFT_DEMOTE] = {name = 'raft_demote', decoder = parse_synchro}, + [RAFT_CONFIRM] = {name = 'raft_confirm', decoder = parse_synchro}, + [RAFT_ROLLBACK] = {name = 'raft_rollback', decoder = parse_synchro}, + + -- Administrative commands. + [PING] = {name = 'ping', decoder = parse_empty}, + + -- Responses. + [OK] = {name = 'OK', is_response = true, decoder = parse_response}, + [CHUNK] = {name = 'CHUNK', is_response = true, decoder = parse_response}, +} + +local function code_to_command(code) + -- A corrupt header can decode TYPE to a non-number (string/array/map, or a + -- uint64 >= 2^63 that msgpack_ext wraps into an ext marker). This runs before + -- the rendering pcall, so guard the comparison rather than let it throw. + if type(code) ~= 'number' then + return UNKNOWN_COMMAND + end + if code >= TYPE_ERROR then + return {name = string.format('ERROR(0x%x)', code - TYPE_ERROR), + is_response = true, decoder = parse_error_response} + end + return COMMANDS[code] or UNKNOWN_COMMAND +end + +-- Dissect one modern PDU at `offset`; returns bytes consumed, nil (reassembly) +-- or false (not a modern PDU). +local function dissect_modern(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + + -- Tarantool always frames the length as a 5-byte 0xce uint32. + if tvb(offset, 1):uint() ~= 0xce then + return false + end + local prefix_len = 5 + if available < prefix_len then + return need_more(pinfo, offset, DESEGMENT_ONE_MORE_SEGMENT) + end + + local _, packet_length = msgpack.unpacker(tvb:raw(offset, prefix_len))() + -- Reject an absurd length instead of asking TCP to reassemble ~4 GB. + if type(packet_length) ~= 'number' or packet_length < 0 + or packet_length > 0x10000000 then + return false + end + local total = prefix_len + packet_length + if available < total then + return need_more(pinfo, offset, total - available) + end + + -- Decode header + optional body; catch malformed inner MsgPack so one bad + -- PDU never aborts the whole segment (mirrors the legacy path's pcall). + local ok, header_data, body_start, body_data = pcall(function() + local iter = msgpack.unpacker(tvb:raw(offset, total)) + iter() -- skip the length prefix already decoded above + local _, hdr = iter() + local bstart, bdata = iter() + return hdr, bstart, bdata + end) + + if not ok or type(header_data) ~= 'table' then + local subtree = tree:add(tarantool_proto, tvb(offset, total), + "Tarantool PDU (undecodable)") + subtree:add(tvb(offset, total), 'malformed or truncated MsgPack') + return total -- consume it and carry on with the next PDU + end + + local command = code_to_command(header_data[TYPE] or 0) + local subtree = tree:add(tarantool_proto, tvb(offset, total), + command.is_response and "Tarantool response" or "Tarantool request") + + -- Guard the table indexing below so a non-conforming PDU renders a note + -- instead of throwing and aborting the rest of the segment. + local rendered = pcall(function() + local header_end = body_start and (body_start - 1) or total + local header_len = header_end - prefix_len + local body_off = offset + header_end + local body_len = total - header_end + local header_range = tvb(offset + prefix_len, header_len) + local voff = map_value_offsets(tvb:raw(offset + prefix_len, header_len), + offset + prefix_len) + + -- Add a header int, reading the exact wire value (see map_value_offsets); + -- nil `field` renders a "label: value" text node. + local function add_hdr_uint(field, label, key) + if header_data[key] == nil then return end + local value = exact_uint(tvb, voff[key]) or UInt64(header_data[key]) + if field ~= nil then + subtree:add(field, header_range, value) + else + subtree:add(header_range, label .. ': ' .. tostring(value)) + end + end + + subtree:add(pf_type, header_range, header_data[TYPE] or 0) + subtree:add(pf_request, header_range, command.name) + subtree:add(pf_is_resp, header_range, command.is_response and true or false) + add_hdr_uint(pf_sync, nil, SYNC) + add_hdr_uint(pf_schema, nil, SCHEMA_VERSION) + add_hdr_uint(pf_stream, nil, STREAM_ID) + -- Replication / transaction header fields (WAL rows, multi-stmt txns). + add_hdr_uint(nil, 'replica_id', REPLICA_ID) + add_hdr_uint(nil, 'lsn', LSN) + add_hdr_uint(nil, 'tsn', TSN) + add_hdr_uint(nil, 'flags', FLAGS) + add_opt(subtree, header_range, 'timestamp', header_data[TIMESTAMP]) + add_hdr_uint(nil, 'group_id', GROUP_ID) + add_hdr_uint(nil, 'thread_id', THREAD_ID) + add_hdr_uint(nil, 'vclock_sync', VCLOCK_SYNC) + + local body_range = (body_len > 0) and tvb(body_off, body_len) + or tvb(offset, total) + local decoder = command.decoder or parser_not_implemented + decoder(body_data or {}, body_range, subtree) -- empty table when body-less + end) + if not rendered then + subtree:add(tvb(offset, total), 'malformed or non-conforming body') + end + + pinfo.cols.info:append(command.name .. ' ') + return total +end + +return { dissect = dissect_modern } + +end + +-- Entry point: modern only (Tarantool 1.6 .. 3.x). Non-modern bytes are left +-- undissected (shown as Data). + +-- core.init must run before requiring the decoder: it captures core.proto at +-- load time. +local core = require 'core' +core.init('tarantool2', 'Tarantool 1.6+') + +local modern = require 'modern' + +core.proto.dissector = core.make_dissector(modern.dissect) +core.register() diff --git a/dist/tarantool.dissector.lua b/dist/tarantool.dissector.lua new file mode 100644 index 0000000..936179b --- /dev/null +++ b/dist/tarantool.dissector.lua @@ -0,0 +1,2430 @@ +-- +-- Tarantool protocol dissector for Wireshark -- all versions (modern MsgPack IPROTO + legacy <= 1.5). +-- +-- GENERATED FILE -- do not edit. Built from src/ by amalgamate.sh; edit the +-- modules under src/ and re-run ./amalgamate.sh to regenerate. +-- +-- Install: copy this file into Wireshark's Personal Lua Plugins folder, renamed +-- so the filename has no extra dot before .lua (e.g. tarantool.lua), then reload +-- Lua plugins. Or run ad-hoc: tshark -X lua_script: -r capture.pcap +-- +-- Protocol reference: +-- https://www.tarantool.io/en/doc/latest/reference/internals/box_protocol/ +-- +local _mods, _loaded = {}, {} +local _require = require -- real require, for string/math/jit/... +local function require(name) + if _loaded[name] ~= nil then return _loaded[name] end + local m = _mods[name] + if not m then return _require(name) end -- not one of ours: defer to stdlib + local r = m() + _loaded[name] = (r == nil) or r + return _loaded[name] +end + +_mods['MessagePack'] = function(...) +-- +-- lua-MessagePack : +-- + +local r, jit = pcall(require, 'jit') +if not r then + jit = nil +end + +local SIZEOF_NUMBER = string.pack and #string.pack('n', 0.0) or 8 +local NUMBER_INTEGRAL = math.type and (math.type(0.0) == math.type(0)) or false +if not jit and _VERSION < 'Lua 5.3' then + -- Lua 5.1 & 5.2 + local loadstring = loadstring or load + local luac = string.dump(loadstring "a = 1") + local header = { luac:sub(1, 12):byte(1, 12) } + SIZEOF_NUMBER = header[11] + NUMBER_INTEGRAL = 1 == header[12] +end + +local assert = assert +local error = error +local pairs = pairs +local pcall = pcall +local setmetatable = setmetatable +local tostring = tostring +local type = type +local char = require'string'.char +local floor = require'math'.floor +local tointeger = require'math'.tointeger or floor +local frexp = require'math'.frexp or require'mathx'.frexp +local ldexp = require'math'.ldexp or require'mathx'.ldexp +local huge = require'math'.huge +local tconcat = require'table'.concat + +--[[ debug only +local format = require'string'.format +local function hexadump (s) + return (s:gsub('.', function (c) return format('%02X ', c:byte()) end)) +end +--]] + +local _ENV = nil +local m = {} + +--[[ debug only +m.hexadump = hexadump +--]] + +local function argerror (caller, narg, extramsg) + error("bad argument #" .. tostring(narg) .. " to " + .. caller .. " (" .. extramsg .. ")") +end + +local function typeerror (caller, narg, arg, tname) + argerror(caller, narg, tname .. " expected, got " .. type(arg)) +end + +local function checktype (caller, narg, arg, tname) + if type(arg) ~= tname then + typeerror(caller, narg, arg, tname) + end +end + +local packers = setmetatable({}, { + __index = function (t, k) error("pack '" .. k .. "' is unimplemented") end +}) +m.packers = packers + +packers['nil'] = function (buffer) + buffer[#buffer+1] = char(0xC0) -- nil +end + +packers['boolean'] = function (buffer, bool) + if bool then + buffer[#buffer+1] = char(0xC3) -- true + else + buffer[#buffer+1] = char(0xC2) -- false + end +end + +packers['string_compat'] = function (buffer, str) + local n = #str + if n <= 0x1F then + buffer[#buffer+1] = char(0xA0 + n) -- fixstr + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xDA, -- str16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xDB, -- str32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'string_compat'" + end + buffer[#buffer+1] = str +end + +packers['_string'] = function (buffer, str) + local n = #str + if n <= 0x1F then + buffer[#buffer+1] = char(0xA0 + n) -- fixstr + elseif n <= 0xFF then + buffer[#buffer+1] = char(0xD9, -- str8 + n) + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xDA, -- str16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xDB, -- str32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'string'" + end + buffer[#buffer+1] = str +end + +packers['binary'] = function (buffer, str) + local n = #str + if n <= 0xFF then + buffer[#buffer+1] = char(0xC4, -- bin8 + n) + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xC5, -- bin16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xC6, -- bin32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'binary'" + end + buffer[#buffer+1] = str +end + +local set_string = function (str) + if str == 'string_compat' then + packers['string'] = packers['string_compat'] + elseif str == 'string' then + packers['string'] = packers['_string'] + elseif str == 'binary' then + packers['string'] = packers['binary'] + else + argerror('set_string', 1, "invalid option '" .. str .."'") + end +end +m.set_string = set_string + +packers['map'] = function (buffer, tbl, n) + if n <= 0x0F then + buffer[#buffer+1] = char(0x80 + n) -- fixmap + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xDE, -- map16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xDF, -- map32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'map'" + end + for k, v in pairs(tbl) do + packers[type(k)](buffer, k) + packers[type(v)](buffer, v) + end +end + +packers['array'] = function (buffer, tbl, n) + if n <= 0x0F then + buffer[#buffer+1] = char(0x90 + n) -- fixarray + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xDC, -- array16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xDD, -- array32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + error"overflow in pack 'array'" + end + for i = 1, n do + local v = tbl[i] + packers[type(v)](buffer, v) + end +end + +local set_array = function (array) + if array == 'without_hole' then + packers['_table'] = function (buffer, tbl) + local is_map, n, max = false, 0, 0 + for k in pairs(tbl) do + if type(k) == 'number' and k > 0 then + if k > max then + max = k + end + else + is_map = true + end + n = n + 1 + end + if max ~= n then -- there are holes + is_map = true + end + if is_map then + return packers['map'](buffer, tbl, n) + else + return packers['array'](buffer, tbl, n) + end + end + elseif array == 'with_hole' then + packers['_table'] = function (buffer, tbl) + local is_map, n, max = false, 0, 0 + for k in pairs(tbl) do + if type(k) == 'number' and k > 0 then + if k > max then + max = k + end + else + is_map = true + end + n = n + 1 + end + if is_map then + return packers['map'](buffer, tbl, n) + else + return packers['array'](buffer, tbl, max) + end + end + elseif array == 'always_as_map' then + packers['_table'] = function(buffer, tbl) + local n = 0 + for k in pairs(tbl) do + n = n + 1 + end + return packers['map'](buffer, tbl, n) + end + else + argerror('set_array', 1, "invalid option '" .. array .."'") + end +end +m.set_array = set_array + +packers['table'] = function (buffer, tbl) + return packers['_table'](buffer, tbl) +end + +packers['unsigned'] = function (buffer, n) + if n >= 0 then + if n <= 0x7F then + buffer[#buffer+1] = char(n) -- fixnum_pos + elseif n <= 0xFF then + buffer[#buffer+1] = char(0xCC, -- uint8 + n) + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xCD, -- uint16 + floor(n / 0x100), + n % 0x100) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xCE, -- uint32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + buffer[#buffer+1] = char(0xCF, -- uint64 + 0, -- only 53 bits from double + floor(n / 0x1000000000000) % 0x100, + floor(n / 0x10000000000) % 0x100, + floor(n / 0x100000000) % 0x100, + floor(n / 0x1000000) % 0x100, + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + end + else + if n >= -0x20 then + buffer[#buffer+1] = char(0x100 + n) -- fixnum_neg + elseif n >= -0x80 then + buffer[#buffer+1] = char(0xD0, -- int8 + 0x100 + n) + elseif n >= -0x8000 then + n = 0x10000 + n + buffer[#buffer+1] = char(0xD1, -- int16 + floor(n / 0x100), + n % 0x100) + elseif n >= -0x80000000 then + n = 4294967296.0 + n + buffer[#buffer+1] = char(0xD2, -- int32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + buffer[#buffer+1] = char(0xD3, -- int64 + 0xFF, -- only 53 bits from double + floor(n / 0x1000000000000) % 0x100, + floor(n / 0x10000000000) % 0x100, + floor(n / 0x100000000) % 0x100, + floor(n / 0x1000000) % 0x100, + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + end + end +end + +packers['signed'] = function (buffer, n) + if n >= 0 then + if n <= 0x7F then + buffer[#buffer+1] = char(n) -- fixnum_pos + elseif n <= 0x7FFF then + buffer[#buffer+1] = char(0xD1, -- int16 + floor(n / 0x100), + n % 0x100) + elseif n <= 0x7FFFFFFF then + buffer[#buffer+1] = char(0xD2, -- int32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + buffer[#buffer+1] = char(0xD3, -- int64 + 0, -- only 53 bits from double + floor(n / 0x1000000000000) % 0x100, + floor(n / 0x10000000000) % 0x100, + floor(n / 0x100000000) % 0x100, + floor(n / 0x1000000) % 0x100, + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + end + else + if n >= -0x20 then + buffer[#buffer+1] = char(0xE0 + 0x20 + n) -- fixnum_neg + elseif n >= -0x80 then + buffer[#buffer+1] = char(0xD0, -- int8 + 0x100 + n) + elseif n >= -0x8000 then + n = 0x10000 + n + buffer[#buffer+1] = char(0xD1, -- int16 + floor(n / 0x100), + n % 0x100) + elseif n >= -0x80000000 then + n = 4294967296.0 + n + buffer[#buffer+1] = char(0xD2, -- int32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + else + buffer[#buffer+1] = char(0xD3, -- int64 + 0xFF, -- only 53 bits from double + floor(n / 0x1000000000000) % 0x100, + floor(n / 0x10000000000) % 0x100, + floor(n / 0x100000000) % 0x100, + floor(n / 0x1000000) % 0x100, + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100) + end + end +end + +local set_integer = function (integer) + if integer == 'unsigned' then + packers['integer'] = packers['unsigned'] + elseif integer == 'signed' then + packers['integer'] = packers['signed'] + else + argerror('set_integer', 1, "invalid option '" .. integer .."'") + end +end +m.set_integer = set_integer + +packers['float'] = function (buffer, n) + local sign = 0 + if n < 0.0 then + sign = 0x80 + n = -n + end + local mant, expo = frexp(n) + if mant ~= mant then + buffer[#buffer+1] = char(0xCA, -- nan + 0xFF, 0x88, 0x00, 0x00) + elseif mant == huge or expo > 0x80 then + if sign == 0 then + buffer[#buffer+1] = char(0xCA, -- inf + 0x7F, 0x80, 0x00, 0x00) + else + buffer[#buffer+1] = char(0xCA, -- -inf + 0xFF, 0x80, 0x00, 0x00) + end + elseif (mant == 0.0 and expo == 0) or expo < -0x7E then + buffer[#buffer+1] = char(0xCA, -- zero + sign, 0x00, 0x00, 0x00) + else + expo = expo + 0x7E + mant = (mant * 2.0 - 1.0) * ldexp(0.5, 24) + buffer[#buffer+1] = char(0xCA, + sign + floor(expo / 0x2), + (expo % 0x2) * 0x80 + floor(mant / 0x10000), + floor(mant / 0x100) % 0x100, + mant % 0x100) + end +end + +packers['double'] = function (buffer, n) + local sign = 0 + if n < 0.0 then + sign = 0x80 + n = -n + end + local mant, expo = frexp(n) + if mant ~= mant then + buffer[#buffer+1] = char(0xCB, -- nan + 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + elseif mant == huge then + if sign == 0 then + buffer[#buffer+1] = char(0xCB, -- inf + 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + else + buffer[#buffer+1] = char(0xCB, -- -inf + 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + end + elseif mant == 0.0 and expo == 0 then + buffer[#buffer+1] = char(0xCB, -- zero + sign, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + else + expo = expo + 0x3FE + mant = (mant * 2.0 - 1.0) * ldexp(0.5, 53) + buffer[#buffer+1] = char(0xCB, + sign + floor(expo / 0x10), + (expo % 0x10) * 0x10 + floor(mant / 0x1000000000000), + floor(mant / 0x10000000000) % 0x100, + floor(mant / 0x100000000) % 0x100, + floor(mant / 0x1000000) % 0x100, + floor(mant / 0x10000) % 0x100, + floor(mant / 0x100) % 0x100, + mant % 0x100) + end +end + +local set_number = function (number) + if number == 'integer' then + packers['number'] = packers['signed'] + elseif number == 'float' then + packers['number'] = function (buffer, n) + if floor(n) ~= n or n ~= n or n > 3.40282347e+38 or n < -3.40282347e+38 then + return packers['float'](buffer, n) + else + return packers['integer'](buffer, n) + end + end + elseif number == 'double' then + packers['number'] = function (buffer, n) + if floor(n) ~= n or n ~= n or n == huge or n == -huge then + return packers['double'](buffer, n) + else + return packers['integer'](buffer, n) + end + end + else + argerror('set_number', 1, "invalid option '" .. number .."'") + end +end +m.set_number = set_number + +for k = 0, 4 do + local n = tointeger(2^k) + local fixext = 0xD4 + k + packers['fixext' .. tostring(n)] = function (buffer, tag, data) + assert(#data == n, "bad length for fixext" .. tostring(n)) + buffer[#buffer+1] = char(fixext, + tag < 0 and tag + 0x100 or tag) + buffer[#buffer+1] = data + end +end + +packers['ext'] = function (buffer, tag, data) + local n = #data + if n <= 0xFF then + buffer[#buffer+1] = char(0xC7, -- ext8 + n, + tag < 0 and tag + 0x100 or tag) + elseif n <= 0xFFFF then + buffer[#buffer+1] = char(0xC8, -- ext16 + floor(n / 0x100), + n % 0x100, + tag < 0 and tag + 0x100 or tag) + elseif n <= 4294967295.0 then + buffer[#buffer+1] = char(0xC9, -- ext&32 + floor(n / 0x1000000), + floor(n / 0x10000) % 0x100, + floor(n / 0x100) % 0x100, + n % 0x100, + tag < 0 and tag + 0x100 or tag) + else + error"overflow in pack 'ext'" + end + buffer[#buffer+1] = data +end + +function m.pack (data) + local buffer = {} + packers[type(data)](buffer, data) + return tconcat(buffer) +end + + +local types_map = setmetatable({ + [0xC0] = 'nil', + [0xC2] = 'false', + [0xC3] = 'true', + [0xC4] = 'bin8', + [0xC5] = 'bin16', + [0xC6] = 'bin32', + [0xC7] = 'ext8', + [0xC8] = 'ext16', + [0xC9] = 'ext32', + [0xCA] = 'float', + [0xCB] = 'double', + [0xCC] = 'uint8', + [0xCD] = 'uint16', + [0xCE] = 'uint32', + [0xCF] = 'uint64', + [0xD0] = 'int8', + [0xD1] = 'int16', + [0xD2] = 'int32', + [0xD3] = 'int64', + [0xD4] = 'fixext1', + [0xD5] = 'fixext2', + [0xD6] = 'fixext4', + [0xD7] = 'fixext8', + [0xD8] = 'fixext16', + [0xD9] = 'str8', + [0xDA] = 'str16', + [0xDB] = 'str32', + [0xDC] = 'array16', + [0xDD] = 'array32', + [0xDE] = 'map16', + [0xDF] = 'map32', +}, { __index = function (t, k) + if k < 0xC0 then + if k < 0x80 then + return 'fixnum_pos' + elseif k < 0x90 then + return 'fixmap' + elseif k < 0xA0 then + return 'fixarray' + else + return 'fixstr' + end + elseif k > 0xDF then + return 'fixnum_neg' + else + return 'reserved' .. tostring(k) + end +end }) +m.types_map = types_map + +local unpackers = setmetatable({}, { + __index = function (t, k) error("unpack '" .. k .. "' is unimplemented") end +}) +m.unpackers = unpackers + +local function unpack_array (c, n) + local t = {} + local decode = unpackers['any'] + for i = 1, n do + t[i] = decode(c) + end + return t +end + +local function unpack_map (c, n) + local t = {} + local decode = unpackers['any'] + for i = 1, n do + local k = decode(c) + local val = decode(c) + if k == nil then + k = m.sentinel + end + if k ~= nil then + t[k] = val + end + end + return t +end + +unpackers['any'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local val = s:sub(i, i):byte() + c.i = i+1 + return unpackers[types_map[val]](c, val) +end + +unpackers['nil'] = function () + return nil +end + +unpackers['false'] = function () + return false +end + +unpackers['true'] = function () + return true +end + +unpackers['float'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + local sign = b1 > 0x7F + local expo = (b1 % 0x80) * 0x2 + floor(b2 / 0x80) + local mant = ((b2 % 0x80) * 0x100 + b3) * 0x100 + b4 + if sign then + sign = -1 + else + sign = 1 + end + local n + if mant == 0 and expo == 0 then + n = sign * 0.0 + elseif expo == 0xFF then + if mant == 0 then + n = sign * huge + else + n = 0.0/0.0 + end + else + n = sign * ldexp(1.0 + mant / 0x800000, expo - 0x7F) + end + c.i = i+4 + return n +end + +unpackers['double'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+7 > j then + c:underflow(i+7) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4, b5, b6, b7, b8 = s:sub(i, i+7):byte(1, 8) + local sign = b1 > 0x7F + local expo = (b1 % 0x80) * 0x10 + floor(b2 / 0x10) + local mant = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + if sign then + sign = -1 + else + sign = 1 + end + local n + if mant == 0 and expo == 0 then + n = sign * 0.0 + elseif expo == 0x7FF then + if mant == 0 then + n = sign * huge + else + n = 0.0/0.0 + end + else + n = sign * ldexp(1.0 + mant / 4503599627370496.0, expo - 0x3FF) + end + c.i = i+8 + return n +end + +unpackers['fixnum_pos'] = function (c, val) + return val +end + +unpackers['uint8'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local b1 = s:sub(i, i):byte() + c.i = i+1 + return b1 +end + +unpackers['uint16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + c.i = i+2 + return b1 * 0x100 + b2 +end + +unpackers['uint32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + c.i = i+4 + return ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4 +end + +unpackers['uint64'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+7 > j then + c:underflow(i+7) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4, b5, b6, b7, b8 = s:sub(i, i+7):byte(1, 8) + c.i = i+8 + return ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 +end + +unpackers['fixnum_neg'] = function (c, val) + return val - 0x100 +end + +unpackers['int8'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local b1 = s:sub(i, i):byte() + c.i = i+1 + if b1 < 0x80 then + return b1 + else + return b1 - 0x100 + end +end + +unpackers['int16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + c.i = i+2 + if b1 < 0x80 then + return b1 * 0x100 + b2 + else + return ((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) - 1 + end +end + +unpackers['int32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + c.i = i+4 + if b1 < 0x80 then + return ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4 + else + return ((((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) * 0x100 + (b3 - 0xFF)) * 0x100 + (b4 - 0xFF)) - 1 + end +end + +unpackers['int64'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+7 > j then + c:underflow(i+7) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4, b5, b6, b7, b8 = s:sub(i, i+7):byte(1, 8) + c.i = i+8 + if b1 < 0x80 then + return ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 + else + return ((((((((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) * 0x100 + (b3 - 0xFF)) * 0x100 + (b4 - 0xFF)) * 0x100 + (b5 - 0xFF)) * 0x100 + (b6 - 0xFF)) * 0x100 + (b7 - 0xFF)) * 0x100 + (b8 - 0xFF)) - 1 + end +end + +unpackers['fixstr'] = function (c, val) + local s, i, j = c.s, c.i, c.j + local n = val % 0x20 + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return s:sub(i, e) +end + +unpackers['str8'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local n = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return s:sub(i, e) +end + +unpackers['str16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + i = i+2 + c.i = i + local n = b1 * 0x100 + b2 + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return s:sub(i, e) +end + +unpackers['str32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + i = i+4 + c.i = i + local n = ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4 + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return s:sub(i, e) +end + +unpackers['bin8'] = unpackers['str8'] +unpackers['bin16'] = unpackers['str16'] +unpackers['bin32'] = unpackers['str32'] + +unpackers['fixarray'] = function (c, val) + return unpack_array(c, val % 0x10) +end + +unpackers['array16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + c.i = i+2 + return unpack_array(c, b1 * 0x100 + b2) +end + +unpackers['array32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + c.i = i+4 + return unpack_array(c, ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) +end + +unpackers['fixmap'] = function (c, val) + return unpack_map(c, val % 0x10) +end + +unpackers['map16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + c.i = i+2 + return unpack_map(c, b1 * 0x100 + b2) +end + +unpackers['map32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + c.i = i+4 + return unpack_map(c, ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) +end + +function m.build_ext (tag, data) + return nil +end + +for k = 0, 4 do + local n = tointeger(2^k) + unpackers['fixext' .. tostring(n)] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local tag = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return m.build_ext(tag < 0x80 and tag or tag - 0x100, s:sub(i, e)) + end +end + +unpackers['ext8'] = function (c) + local s, i, j = c.s, c.i, c.j + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local n = s:sub(i, i):byte() + i = i+1 + c.i = i + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local tag = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return m.build_ext(tag < 0x80 and tag or tag - 0x100, s:sub(i, e)) +end + +unpackers['ext16'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+1 > j then + c:underflow(i+1) + s, i, j = c.s, c.i, c.j + end + local b1, b2 = s:sub(i, i+1):byte(1, 2) + i = i+2 + c.i = i + local n = b1 * 0x100 + b2 + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local tag = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return m.build_ext(tag < 0x80 and tag or tag - 0x100, s:sub(i, e)) +end + +unpackers['ext32'] = function (c) + local s, i, j = c.s, c.i, c.j + if i+3 > j then + c:underflow(i+3) + s, i, j = c.s, c.i, c.j + end + local b1, b2, b3, b4 = s:sub(i, i+3):byte(1, 4) + i = i+4 + c.i = i + local n = ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4 + if i > j then + c:underflow(i) + s, i, j = c.s, c.i, c.j + end + local tag = s:sub(i, i):byte() + i = i+1 + c.i = i + local e = i+n-1 + if e > j then + c:underflow(e) + s, i, j = c.s, c.i, c.j + e = i+n-1 + end + c.i = i+n + return m.build_ext(tag < 0x80 and tag or tag - 0x100, s:sub(i, e)) +end + + +local function cursor_string (str) + return { + s = str, + i = 1, + j = #str, + underflow = function (self) + error "missing bytes" + end, + } +end + +local function cursor_loader (ld) + return { + s = '', + i = 1, + j = 0, + underflow = function (self, e) + self.s = self.s:sub(self.i) + e = e - self.i + 1 + self.i = 1 + self.j = 0 + while e > self.j do + local chunk = ld() + if not chunk then + error "missing bytes" + end + self.s = self.s .. chunk + self.j = #self.s + end + end, + } +end + +function m.unpack (s) + checktype('unpack', 1, s, 'string') + local cursor = cursor_string(s) + local data = unpackers['any'](cursor) + if cursor.i < cursor.j then + -- error "extra bytes" + end + -- j - strlen(s) + -- i - current position + -- print('unpack: ', cursor.j, cursor.i) + -- current position starts from zero + return data, cursor.i - 1 +end + +function m.unpacker (src) + if type(src) == 'string' then + local cursor = cursor_string(src) + return function () + if cursor.i <= cursor.j then + return cursor.i, unpackers['any'](cursor) + end + end + elseif type(src) == 'function' then + local cursor = cursor_loader(src) + return function () + if cursor.i > cursor.j then + pcall(cursor.underflow, cursor, cursor.i) + end + if cursor.i <= cursor.j then + return true, unpackers['any'](cursor) + end + end + else + argerror('unpacker', 1, "string or function expected, got " .. type(src)) + end +end + +set_string'string_compat' +set_integer'unsigned' +if NUMBER_INTEGRAL then + packers['double'] = packers['integer'] + packers['float'] = packers['integer'] + set_number'integer' +elseif SIZEOF_NUMBER == 4 then + packers['double'] = packers['float'] + m.small_lua = true + set_number'float' +else + set_number'double' +end +set_array'without_hole' + +m._VERSION = '0.3.3' +m._DESCRIPTION = "lua-MessagePack : a pure Lua implementation" +m._COPYRIGHT = "Copyright (c) 2012-2015 Francois Perrad" +return m +-- +-- This library is licensed under the terms of the MIT/X11 license, +-- like Lua itself. +-- + +end + +_mods['msgpack_ext'] = function(...) +-- Tarantool MsgPack ext (MP_EXT) decoding layered onto the bundled +-- MessagePack.lua, which by default drops ext values and renders uint64 >= 2^63 +-- as negative. Exports the configured msgpack module and the `ext_mt` marker. +-- Used only by the modern decoder (legacy <= 1.5 is not MsgPack-based). + +local msgpack = require 'MessagePack' + +local M = {} + +-- MP_EXT type ids (src/lib/core/mp_extension_types.h). +local MP_EXT_NAME = { + [0] = 'unknown', [1] = 'decimal', [2] = 'uuid', [3] = 'error', + [4] = 'datetime', [5] = 'compression', [6] = 'interval', + [7] = 'tuple', [8] = 'arrow', +} + +-- Marks a pre-formatted ext value; escape_call_arg renders its .text verbatim. +local ext_mt = {} + +local function le_uint(s, from, len) + local v = 0 + for i = len, 1, -1 do v = v * 256 + s:byte(from + i - 1) end + return v +end + +local function be_uint(s, from, len) + local v = 0 + for i = 0, len - 1 do v = v * 256 + s:byte(from + i) end + return v +end + +-- Read one MsgPack integer at `from`; returns the value and the next index. +local function read_mp_int(s, from) + local b = s:byte(from) + if b <= 0x7f then return b, from + 1 + elseif b >= 0xe0 then return b - 0x100, from + 1 + elseif b == 0xcc then return s:byte(from + 1), from + 2 + elseif b == 0xcd then return be_uint(s, from + 1, 2), from + 3 + elseif b == 0xce then return be_uint(s, from + 1, 4), from + 5 + elseif b == 0xd0 then local v = s:byte(from + 1) + return v >= 0x80 and v - 0x100 or v, from + 2 + elseif b == 0xd1 then local v = be_uint(s, from + 1, 2) + return v >= 0x8000 and v - 0x10000 or v, from + 3 + elseif b == 0xd2 then local v = be_uint(s, from + 1, 4) + return v >= 0x80000000 and v - 0x100000000 or v, from + 5 + elseif b == 0xcf then return be_uint(s, from + 1, 8), from + 9 + elseif b == 0xd3 then return be_uint(s, from + 1, 8), from + 9 + end + return 0, from + 1 +end + +-- MP_UUID (fixext16): 16 bytes -> canonical UUID string. +local function decode_uuid(data) + local h = {} + for i = 1, 16 do h[i] = string.format('%02x', data:byte(i)) end + return table.concat(h, '', 1, 4) .. '-' .. table.concat(h, '', 5, 6) .. '-' + .. table.concat(h, '', 7, 8) .. '-' .. table.concat(h, '', 9, 10) .. '-' + .. table.concat(h, '', 11, 16) +end + +-- MP_DATETIME (fixext8/16): int64 LE seconds [+ nsec, tzoffset, tzindex]. +local function decode_datetime(data) + local secs = le_uint(data, 1, 8) + local nsec, tzoffset = 0, 0 + if #data >= 16 then + nsec = le_uint(data, 9, 4) + tzoffset = le_uint(data, 13, 2) + if tzoffset >= 0x8000 then tzoffset = tzoffset - 0x10000 end + end + -- Shift the UTC instant by the stored offset so the printed wall clock + -- matches the appended timezone. + local out = 'epoch=' .. string.format('%d', secs) + if os and os.date then + local ok, formatted = pcall(os.date, '!%Y-%m-%dT%H:%M:%S', secs + tzoffset * 60) + if ok and formatted then out = formatted end + end + if nsec > 0 then + local frac = string.format('%09d', nsec):gsub('0+$', '') + out = out .. '.' .. frac + end + if tzoffset ~= 0 then + local m = math.abs(tzoffset) + out = out .. string.format('%s%02d:%02d', tzoffset < 0 and '-' or '+', + math.floor(m / 60), m % 60) + else + out = out .. 'Z' + end + return out +end + +-- MP_DECIMAL: MsgPack scale (-exponent) followed by packed-BCD coefficient. +local function decode_decimal(data) + local scale, pos = read_mp_int(data, 1) + local digits, sign, last = {}, '', #data + for i = pos, last do + local byte = data:byte(i) + digits[#digits + 1] = math.floor(byte / 16) + if i < last then + digits[#digits + 1] = byte % 16 + else + local nibble = byte % 16 -- last low nibble is the sign + sign = (nibble == 0x0b or nibble == 0x0d) and '-' or '' + end + end + local s = table.concat(digits):gsub('^0+(%d)', '%1') + if scale > 0 then + if #s <= scale then s = string.rep('0', scale - #s + 1) .. s end + s = s:sub(1, #s - scale) .. '.' .. s:sub(#s - scale + 1) + elseif scale < 0 then + s = s .. string.rep('0', -scale) + end + return sign .. s +end + +local INTERVAL_FIELD = { + [0] = 'year', [1] = 'month', [2] = 'week', [3] = 'day', [4] = 'hour', + [5] = 'min', [6] = 'sec', [7] = 'nsec', [8] = 'adjust', +} + +-- MP_INTERVAL: u8 count, then count (u8 field_id, MsgPack value) pairs. +local function decode_interval(data) + local count, pos = data:byte(1), 2 + local parts = {} + for _ = 1, count do + local fid = data:byte(pos) + local val + val, pos = read_mp_int(data, pos + 1) + parts[#parts + 1] = (INTERVAL_FIELD[fid] or ('f' .. fid)) .. '=' .. val + end + return '{' .. table.concat(parts, ', ') .. '}' +end + +local EXT_DECODER = { + [1] = decode_decimal, [2] = decode_uuid, + [4] = decode_datetime, [6] = decode_interval, +} + +-- Decode known scalar ext types; render opaque ones as a labelled blob. +function msgpack.build_ext(tag, data) + local decoder = EXT_DECODER[tag] + if decoder then + local ok, result = pcall(decoder, data) + if ok and result ~= nil then + return setmetatable({text = result}, ext_mt) + end + end + return setmetatable({text = string.format('<%s ext, %d byte%s>', + MP_EXT_NAME[tag] or ('type ' .. tag), #data, + #data == 1 and '' or 's')}, ext_mt) +end + +-- MessagePack.lua's uint64 decode wraps to a signed Lua integer, so values +-- >= 2^63 come back negative; re-render those as unsigned. int64 (genuinely +-- signed) and non-negative values pass through. Headers use exact_uint instead. +local raw_uint64 = msgpack.unpackers['uint64'] +msgpack.unpackers['uint64'] = function(c) + local n = raw_uint64(c) + if type(n) == 'number' and n < 0 then + return setmetatable({text = string.format('%u', n)}, ext_mt) + end + return n +end + +M.msgpack = msgpack +M.ext_mt = ext_mt +return M + +end + +_mods['core'] = function(...) +-- Shared dissector core: the Proto, header fields, port/enabled preferences, +-- greeting, the main-loop factory and registration. Modern/legacy decoders and +-- the per-build entry points build on this. +-- +-- The Proto is created by M.init(slug, desc), which each entry point calls with +-- its own name (e.g. "tarantool1", "tarantool2", "tarantool") BEFORE requiring +-- the decoder modules -- they capture M.proto/M.pf at load time. + +local M = {} + +-- Set by M.init; the closures below capture these names as upvalues, so they +-- see the values init assigns. +local proto +local pf + +-- Request reassembly: returns nil so the caller stops and TCP redelivers more. +local function need_more(pinfo, offset, more) + if pinfo.can_desegment > 0 then + pinfo.desegment_offset = offset + pinfo.desegment_len = more + end + return nil +end +M.need_more = need_more + +local GREETING_SIZE = 128 +local GREETING_SALT_OFFSET = 64 +local GREETING_SALT_SIZE = 44 + +local function dissect_greeting(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + if available < GREETING_SIZE then + return need_more(pinfo, offset, GREETING_SIZE - available) + end + pinfo.cols.info:append('Greeting ') + local subtree = tree:add(proto, tvb(offset, GREETING_SIZE), "Tarantool greeting") + subtree:add(tvb(offset, GREETING_SALT_OFFSET), + "Server version: " .. tvb(offset, GREETING_SALT_OFFSET):string()) + subtree:add(tvb(offset + GREETING_SALT_OFFSET, GREETING_SALT_SIZE), + "Salt: " .. tvb(offset + GREETING_SALT_OFFSET, GREETING_SALT_SIZE):string()) + subtree:add(tvb(offset + GREETING_SALT_OFFSET + GREETING_SALT_SIZE, + GREETING_SIZE - GREETING_SALT_OFFSET - GREETING_SALT_SIZE), "Reserved") + return GREETING_SIZE +end +M.dissect_greeting = dissect_greeting + +-- `dispatch_pdu` decodes one non-greeting PDU (returns bytes consumed, nil for +-- reassembly, or false for "not ours"); which one is wired in distinguishes the +-- modern-only, legacy-only and combined builds. +function M.make_dissector(dispatch_pdu) + return function(tvb, pinfo, tree) + pinfo.cols.protocol = "Tarantool" + pinfo.cols.info:clear() + local n = tvb:len() + local offset = 0 + while offset < n do + local consumed + if n - offset >= 9 and tvb(offset, 9):string() == "Tarantool" then + consumed = dissect_greeting(tvb, pinfo, tree, offset) + else + consumed = dispatch_pdu(tvb, pinfo, tree, offset) + end + if consumed == nil then return -- reassembly requested + elseif not consumed then break end -- not decodable as our protocol + offset = offset + consumed + end + return offset + end +end + +local tcp_port_table = DissectorTable.get("tcp.port") +local registered_ports +local server_ports = {} + +-- Parse a Wireshark port range ("3301,3311-3313") into a lookup set, so the +-- legacy decoder can tell a server-side port from a client port for direction. +local function parse_ports(spec) + local set = {} + for part in tostring(spec):gmatch("[^,]+") do + local a, b = part:match("^%s*(%d+)%s*%-%s*(%d+)%s*$") + if a then + for p = tonumber(a), tonumber(b) do set[p] = true end + else + local n = part:match("^%s*(%d+)%s*$") + if n then set[tonumber(n)] = true end + end + end + return set +end + +-- True if `port` is one of the configured Tarantool server ports. +function M.is_server_port(port) return server_ports[port] == true end + +-- Sync the tcp.port registration with the current `enabled`/`ports` preferences. +-- `ports` is a range (e.g. "3301,3311-3313"); drop the previously registered +-- range and add the current one -- Wireshark expands the range and binds each +-- port. Idempotent: safe to call on every prefs change. +function M.register() + if registered_ports ~= nil then + tcp_port_table:remove(registered_ports, proto) + registered_ports = nil + end + server_ports = {} + if not proto.prefs.enabled then return end + tcp_port_table:add(proto.prefs.ports, proto) + registered_ports = proto.prefs.ports + server_ports = parse_ports(proto.prefs.ports) +end + +-- Create the protocol under `slug` (display name `desc`), register its header +-- fields and preferences, and wire prefs_changed. `default_port` seeds the +-- "TCP ports" range preference (3301 for modern; legacy <=1.5 used 33013) -- a +-- distinct default keeps co-loaded builds off the same port, since Wireshark's +-- tcp.port table binds one dissector per port. The user can widen it to a range +-- (e.g. "3301,3311-3313") to decode a whole cluster. Call once, before +-- requiring the decoder modules. +function M.init(slug, desc, default_port) + proto = Proto(slug, desc) + M.proto = proto + + -- Header fields, also usable as display filters (e.g. `tnt.type == 0x01`). + pf = { + type = ProtoField.uint16("tnt.type", "Request type", base.HEX), + request = ProtoField.string("tnt.request", "Request name"), + sync = ProtoField.uint64("tnt.sync", "Sync", base.DEC), + schema = ProtoField.uint64("tnt.schema_version", "Schema version", base.DEC), + stream = ProtoField.uint64("tnt.stream_id", "Stream id", base.DEC), + is_resp = ProtoField.bool("tnt.response", "Is response"), + } + M.pf = pf + proto.fields = { pf.type, pf.request, pf.sync, pf.schema, pf.stream, pf.is_resp } + + proto.prefs.enabled = Pref.bool("Dissector enabled", true, + "Whether the Tarantool dissector is enabled") + proto.prefs.ports = Pref.range("TCP ports", tostring(default_port or 3301), + "Ports to decode as Tarantool, e.g. 3301,3311-3313", 65535) + + function proto.prefs_changed() M.register() end + + return M +end + +return M + +end + +_mods['modern'] = function(...) +-- Modern MsgPack IPROTO decoder (Tarantool 1.6 .. 3.x). A PDU is a 5-byte 0xce +-- uint32 length prefix, then a header map and an optional body map. Exports +-- dissect(tvb, pinfo, tree, offset). + +local core = require 'core' +local mpx = require 'msgpack_ext' + +local msgpack = mpx.msgpack +local ext_mt = mpx.ext_mt + +local tarantool_proto = core.proto +local need_more = core.need_more +local pf_type = core.pf.type +local pf_request = core.pf.request +local pf_sync = core.pf.sync +local pf_schema = core.pf.schema +local pf_stream = core.pf.stream +local pf_is_resp = core.pf.is_resp + +-- iproto_type: request/command codes (src/box/iproto_constants.h). +local OK = 0x00 +local SELECT = 0x01 +local INSERT = 0x02 +local REPLACE = 0x03 +local UPDATE = 0x04 +local DELETE = 0x05 +local CALL_16 = 0x06 -- 1.6-era call: coerced results into tuples; kept as call_16 +local AUTH = 0x07 +local EVAL = 0x08 +local UPSERT = 0x09 +local CALL = 0x0a -- modern call (1.7.2+): returns a plain array +local EXECUTE = 0x0b +local NOP = 0x0c +local PREPARE = 0x0d +local BEGIN = 0x0e +local COMMIT = 0x0f +local ROLLBACK = 0x10 +local INSERT_ARROW = 0x11 +local RAFT = 0x1e +local RAFT_PROMOTE = 0x1f +local RAFT_DEMOTE = 0x20 +local RAFT_CONFIRM = 0x28 +local RAFT_ROLLBACK = 0x29 +local PING = 0x40 +local JOIN = 0x41 +local SUBSCRIBE = 0x42 +local VOTE_DEPRECATED = 0x43 +local VOTE = 0x44 +local FETCH_SNAPSHOT = 0x45 +local REGISTER = 0x46 +local JOIN_META = 0x47 +local JOIN_SNAPSHOT = 0x48 +local ID = 0x49 +local WATCH = 0x4a +local UNWATCH = 0x4b +local EVENT = 0x4c +local WATCH_ONCE = 0x4d + +-- Response markers. +local CHUNK = 0x80 -- non-final response chunk (box.session.push) +local TYPE_ERROR = 0x8000 -- bit 15 set => error, low 15 bits = errcode + +-- iproto_key: header keys (0x00 .. 0x0b). +local TYPE = 0x00 -- IPROTO_REQUEST_TYPE +local SYNC = 0x01 +local REPLICA_ID = 0x02 +local LSN = 0x03 +local TIMESTAMP = 0x04 +local SCHEMA_VERSION = 0x05 +local SERVER_VERSION = 0x06 +local GROUP_ID = 0x07 +local TSN = 0x08 +local FLAGS = 0x09 +local STREAM_ID = 0x0a +local THREAD_ID = 0x0b + +-- iproto_key: DML body keys (0x10 .. 0x2f). +local SPACE_ID = 0x10 +local INDEX_ID = 0x11 +local LIMIT = 0x12 +local OFFSET = 0x13 +local ITERATOR = 0x14 +local INDEX_BASE = 0x15 +local FETCH_POSITION = 0x1f +local KEY = 0x20 +local TUPLE = 0x21 +local FUNCTION_NAME = 0x22 +local USER_NAME = 0x23 +local INSTANCE_UUID = 0x24 +local REPLICASET_UUID = 0x25 +local VCLOCK = 0x26 +local EXPRESSION = 0x27 +local OPS = 0x28 +local BALLOT = 0x29 +local OLD_TUPLE = 0x2c +local NEW_TUPLE = 0x2d +local AFTER_POSITION = 0x2e +local AFTER_TUPLE = 0x2f + +-- iproto_key: response keys (0x30 .. 0x35). +local DATA = 0x30 +local ERROR_24 = 0x31 -- legacy string error +local METADATA = 0x32 +local BIND_METADATA = 0x33 +local BIND_COUNT = 0x34 +local POSITION = 0x35 + +-- iproto_key: SQL keys (0x40 .. 0x43). +local SQL_TEXT = 0x40 +local SQL_BIND = 0x41 +local SQL_INFO = 0x42 +local STMT_ID = 0x43 + +-- Nested keys inside response sub-structures. +local FIELD_NAME = 0x00 -- column maps in METADATA +local FIELD_TYPE = 0x01 +local SQL_INFO_ROW_COUNT = 0x00 -- inside SQL_INFO map + +-- iproto_key: extended keys (0x50 .. 0x64). +local REPLICA_ANON = 0x50 +local ID_FILTER = 0x51 +local ERROR = 0x52 -- structured error stack (MP_MAP) +local TERM = 0x53 +local VERSION = 0x54 +local FEATURES = 0x55 +local TIMEOUT = 0x56 +local EVENT_KEY = 0x57 +local EVENT_DATA = 0x58 +local TXN_ISOLATION = 0x59 +local VCLOCK_SYNC = 0x5a +local AUTH_TYPE = 0x5b +local REPLICASET_NAME = 0x5c +local INSTANCE_NAME = 0x5d +local SPACE_NAME = 0x5e +local INDEX_NAME = 0x5f +local IS_SYNC = 0x61 + +-- iterator types (box.index iterator codes), for nicer SELECT output. +local ITERATOR_NAME = { + [0] = 'EQ', [1] = 'REQ', [2] = 'ALL', [3] = 'LT', [4] = 'LE', + [5] = 'GE', [6] = 'GT', [7] = 'BITS_ALL_SET', [8] = 'BITS_ANY_SET', + [9] = 'BITS_ALL_NOT_SET', [10] = 'OVERLAPS', [11] = 'NEIGHBOR', +} + +-- helpers --------------------------------------------------------------------- + +-- 0-based wire offset of each value in a top-level MsgPack map, keyed by map key, +-- so 64-bit header fields can be read exactly (see exact_uint) instead of via +-- MessagePack.lua's lossy decode. +local function map_value_offsets(raw, base_off) + local b = raw:byte(1) + local count, first + if b >= 0x80 and b <= 0x8f then count, first = b - 0x80, 2 + elseif b == 0xde then count, first = raw:byte(2) * 256 + raw:byte(3), 4 + elseif b == 0xdf then + count = ((raw:byte(2) * 256 + raw:byte(3)) * 256 + raw:byte(4)) * 256 + + raw:byte(5) + first = 6 + else + return {} + end + local offsets = {} + local iter = msgpack.unpacker(raw:sub(first)) + for _ = 1, count do + local _, key = iter() -- key element + local value_pos = iter() -- start of value element (1-based within sub) + if value_pos == nil then break end + offsets[key] = base_off + first + value_pos - 2 + end + return offsets +end + +-- Read the MsgPack uint at `off` as a full-precision Wireshark UInt64, or nil if +-- `off` is nil or the bytes are not a uint. +local function exact_uint(tvb, off) + if off == nil then return nil end + local b = tvb(off, 1):uint() + if b <= 0x7f then return UInt64(b) -- positive fixint + elseif b == 0xcc then return UInt64(tvb(off + 1, 1):uint()) + elseif b == 0xcd then return UInt64(tvb(off + 1, 2):uint()) + elseif b == 0xce then return UInt64(tvb(off + 1, 4):uint()) + elseif b == 0xcf then return tvb(off + 1, 8):uint64() + end + return nil +end + +local function map(tbl, callback) + local result = {} + if tbl == nil then return result end + for k, v in pairs(tbl) do + result[k] = callback(v) + end + return result +end + +local function table_kv_concat(tbl, sep) + local result = {} + local used_keys = {} + for i, v in ipairs(tbl) do + used_keys[i] = true + table.insert(result, v) + end + for k, v in pairs(tbl) do + if not used_keys[k] then + local key = (type(k) == 'table' and getmetatable(k) == ext_mt) + and k.text or tostring(k) + table.insert(result, key .. ' = ' .. tostring(v)) + end + end + return table.concat(result, sep) +end + +local function escape_call_arg(a) + if type(a) == 'table' and getmetatable(a) == ext_mt then + return a.text -- a decoded MP_EXT value (datetime, decimal, uuid, ...) + end + local t = type(a) + if t == 'number' or t == 'boolean' then + return tostring(a) + elseif t == 'string' then + return '"' .. a .. '"' + elseif t == 'table' then + return '{' .. table_kv_concat(map(a, escape_call_arg), ', ') .. '}' + elseif a == nil then + return 'nil' + end + return tostring(a) +end + +-- Concatenate an array (possibly nil) of msgpack values into a readable string. +local function join_args(arr) + return table.concat(map(arr, escape_call_arg), ', ') +end + +-- Add a "label: value" node only when the value is present. +local function add_opt(subtree, buffer, label, value) + if value ~= nil then + subtree:add(buffer, label .. ': ' .. escape_call_arg(value)) + end +end + +-- decoders -------------------------------------------------------------------- +-- Each receives (body_table, body_tvbrange, subtree); body_table may be empty +-- for body-less requests (PING, VOTE, ...). + +local function parse_call(tbl, buffer, subtree) + local name = tbl[FUNCTION_NAME] + local args = tbl[TUPLE] + subtree:add(buffer, string.format('%s(%s)', tostring(name), join_args(args))) +end + +local function parse_eval(tbl, buffer, subtree) + local expression = tbl[EXPRESSION] + local args = tbl[TUPLE] + subtree:add(buffer, string.format('eval %s with args (%s)', + tostring(expression), join_args(args))) +end + +local function parse_select(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + local index = tbl[INDEX_NAME] or tbl[INDEX_ID] or 0 + local limit = tbl[LIMIT] + local offset = tbl[OFFSET] or 0 + local iterator = tbl[ITERATOR] or 0 + + subtree:add(buffer, string.format( + 'SELECT FROM space %s WHERE index(%s) = (%s) LIMIT %s OFFSET %d ITERATOR %s', + tostring(space), tostring(index), join_args(tbl[KEY]), + tostring(limit), offset, + ITERATOR_NAME[iterator] or tostring(iterator))) + -- Pagination (request side). + add_opt(subtree, buffer, 'fetch_position', tbl[FETCH_POSITION]) + add_opt(subtree, buffer, 'after_position', tbl[AFTER_POSITION]) + if tbl[AFTER_TUPLE] ~= nil then + subtree:add(buffer, 'after_tuple: {' .. join_args(tbl[AFTER_TUPLE]) .. '}') + end +end + +local function parse_insert(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'tuple: {' .. join_args(tbl[TUPLE]) .. '}') + -- Before/after images carried by replicated DML rows. + add_opt(subtree, buffer, 'old_tuple', tbl[OLD_TUPLE]) + add_opt(subtree, buffer, 'new_tuple', tbl[NEW_TUPLE]) +end + +local function parse_delete(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + local index = tbl[INDEX_NAME] or tbl[INDEX_ID] or 0 + subtree:add(buffer, string.format('DELETE FROM space(%s) WHERE index(%s) = (%s)', + tostring(space), tostring(index), join_args(tbl[KEY]))) +end + +local function parse_upsert(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'tuple: {' .. join_args(tbl[TUPLE]) .. '}') + subtree:add(buffer, 'ops: {' .. join_args(tbl[OPS]) .. '}') + add_opt(subtree, buffer, 'index_base', tbl[INDEX_BASE]) +end + +local function parse_update(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + local index = tbl[INDEX_NAME] or tbl[INDEX_ID] or 0 + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'index: ' .. tostring(index)) + subtree:add(buffer, 'key: {' .. join_args(tbl[KEY]) .. '}') + subtree:add(buffer, 'ops: {' .. join_args(tbl[TUPLE]) .. '}') + add_opt(subtree, buffer, 'index_base', tbl[INDEX_BASE]) + add_opt(subtree, buffer, 'old_tuple', tbl[OLD_TUPLE]) + add_opt(subtree, buffer, 'new_tuple', tbl[NEW_TUPLE]) +end + +local function parse_auth(tbl, buffer, subtree) + local user = tbl[USER_NAME] + local tuple = tbl[TUPLE] or {} + subtree:add(buffer, string.format( + 'Authentication: user "%s", mechanism %s', + tostring(user), tostring(tuple[1]))) +end + +local function parse_id(tbl, buffer, subtree) + subtree:add(buffer, 'protocol version: ' .. tostring(tbl[VERSION])) + subtree:add(buffer, 'features: {' .. join_args(tbl[FEATURES]) .. '}') + if tbl[AUTH_TYPE] ~= nil then + subtree:add(buffer, 'auth_type: ' .. tostring(tbl[AUTH_TYPE])) + end +end + +local function parse_execute(tbl, buffer, subtree) + local stmt_id = tbl[STMT_ID] + local sql_text = tbl[SQL_TEXT] + local bind = join_args(tbl[SQL_BIND]) + if bind ~= '' then + bind = string.format(', with parameters (%s)', bind) + end + if stmt_id ~= nil then + subtree:add(buffer, string.format( + 'execute prepared statement id %s%s', tostring(stmt_id), bind)) + else + subtree:add(buffer, string.format( + 'execute SQL "%s"%s', tostring(sql_text), bind)) + end +end + +local function parse_prepare(tbl, buffer, subtree) + local stmt_id = tbl[STMT_ID] + local sql_text = tbl[SQL_TEXT] + if stmt_id ~= nil then + subtree:add(buffer, 'unprepare/prepare statement id ' .. tostring(stmt_id)) + else + subtree:add(buffer, string.format('prepare SQL "%s"', tostring(sql_text))) + end +end + +local function parse_begin(tbl, buffer, subtree) + add_opt(subtree, buffer, 'timeout', tbl[TIMEOUT]) + add_opt(subtree, buffer, 'txn_isolation', tbl[TXN_ISOLATION]) + add_opt(subtree, buffer, 'is_sync', tbl[IS_SYNC]) +end + +local function parse_commit(tbl, buffer, subtree) + add_opt(subtree, buffer, 'is_sync', tbl[IS_SYNC]) +end + +local function parse_insert_arrow(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'arrow: ') +end + +local function parse_watch(tbl, buffer, subtree) + subtree:add(buffer, 'event key: ' .. tostring(tbl[EVENT_KEY])) + if tbl[EVENT_DATA] ~= nil then + subtree:add(buffer, 'event data: ' .. escape_call_arg(tbl[EVENT_DATA])) + end +end + +local function parse_synchro(tbl, buffer, subtree) + subtree:add(buffer, string.format( + 'replica_id: %s, lsn: %s, term: %s', + tostring(tbl[REPLICA_ID]), tostring(tbl[LSN]), tostring(tbl[TERM]))) +end + +-- Render a vclock ({replica_id = lsn}) as "{0 = 2, 1 = 9}", iterating sorted keys +-- (a plain table_kv_concat would print id 1 positionally as "{9, 0 = 2}"). +local function vclock_str(vclock) + if type(vclock) ~= 'table' then return tostring(vclock) end + local keys = {} + for k in pairs(vclock) do keys[#keys + 1] = k end + table.sort(keys) + local parts = {} + for _, k in ipairs(keys) do + parts[#parts + 1] = tostring(k) .. ' = ' .. escape_call_arg(vclock[k]) + end + return '{' .. table.concat(parts, ', ') .. '}' +end + +local function parse_subscribe(tbl, buffer, subtree) + subtree:add(buffer, 'instance_uuid: ' .. tostring(tbl[INSTANCE_UUID])) + subtree:add(buffer, 'replicaset_uuid: ' .. tostring(tbl[REPLICASET_UUID])) + if tbl[VCLOCK] ~= nil then + subtree:add(buffer, 'vclock: ' .. vclock_str(tbl[VCLOCK])) + end + add_opt(subtree, buffer, 'instance_name', tbl[INSTANCE_NAME]) + add_opt(subtree, buffer, 'replicaset_name', tbl[REPLICASET_NAME]) + add_opt(subtree, buffer, 'server_version', tbl[SERVER_VERSION]) + add_opt(subtree, buffer, 'replica_anon', tbl[REPLICA_ANON]) + if tbl[ID_FILTER] ~= nil then + subtree:add(buffer, 'id_filter: {' .. join_args(tbl[ID_FILTER]) .. '}') + end +end + +-- Shared by JOIN, FETCH_SNAPSHOT and REGISTER. +local function parse_join(tbl, buffer, subtree) + subtree:add(buffer, 'instance_uuid: ' .. tostring(tbl[INSTANCE_UUID])) + add_opt(subtree, buffer, 'instance_name', tbl[INSTANCE_NAME]) + add_opt(subtree, buffer, 'server_version', tbl[SERVER_VERSION]) + if tbl[VCLOCK] ~= nil then + subtree:add(buffer, 'vclock: ' .. vclock_str(tbl[VCLOCK])) + end +end + +-- IPROTO_ERROR (0x52) is a map { MP_ERROR_STACK: [ frame, ... ] }; each frame is +-- a map keyed by these field ids (src/box/mp_error.cc). +local MP_ERROR_STACK = 0x00 +local MP_ERROR_TYPE = 0x00 +local MP_ERROR_FILE = 0x01 +local MP_ERROR_LINE = 0x02 +local MP_ERROR_MESSAGE = 0x03 +local MP_ERROR_ERRNO = 0x04 +local MP_ERROR_CODE = 0x05 +local MP_ERROR_FIELDS = 0x06 + +-- Render the structured error stack as named fields instead of a raw map dump. +local function add_error_stack(subtree, buffer, err) + local stack = (type(err) == 'table') and err[MP_ERROR_STACK] or nil + if type(stack) ~= 'table' then + subtree:add(buffer, 'error: ' .. escape_call_arg(err)) -- unexpected shape + return + end + local node = subtree:add(buffer, 'error stack') + for i, frame in ipairs(stack) do + if type(frame) == 'table' then + local head = string.format('[%d] %s', i, + tostring(frame[MP_ERROR_TYPE] or '?')) + if frame[MP_ERROR_CODE] ~= nil then + head = head .. string.format(' (code %s)', + tostring(frame[MP_ERROR_CODE])) + end + if frame[MP_ERROR_MESSAGE] ~= nil then + head = head .. ': ' .. tostring(frame[MP_ERROR_MESSAGE]) + end + local fnode = node:add(buffer, head) + add_opt(fnode, buffer, 'errno', frame[MP_ERROR_ERRNO]) + if frame[MP_ERROR_FILE] ~= nil then + fnode:add(buffer, string.format('at %s:%s', + tostring(frame[MP_ERROR_FILE]), + tostring(frame[MP_ERROR_LINE]))) + end + if frame[MP_ERROR_FIELDS] ~= nil then + fnode:add(buffer, 'fields: ' .. escape_call_arg(frame[MP_ERROR_FIELDS])) + end + end + end +end + +local function parse_error_response(tbl, buffer, subtree) + if tbl == nil then + subtree:add(buffer, '(empty response body)') + return + end + if tbl[ERROR_24] ~= nil then + subtree:add(buffer, 'message: ' .. tostring(tbl[ERROR_24])) + end + if tbl[ERROR] ~= nil then + add_error_stack(subtree, buffer, tbl[ERROR]) + end + if tbl[ERROR_24] == nil and tbl[ERROR] == nil then + subtree:add(buffer, '(empty response body)') + end +end + +-- Responses carry no request type, so surface whichever known response keys are +-- present (data, SQL metadata, PREPARE info, cursor, ballot, ID/SUBSCRIBE). +local function parse_response(tbl, buffer, subtree) + if tbl == nil or next(tbl) == nil then + subtree:add(buffer, '(empty response body)') + return + end + + if tbl[METADATA] ~= nil then + local node = subtree:add(buffer, 'metadata') + for _, col in ipairs(tbl[METADATA]) do + node:add(buffer, tostring(col[FIELD_NAME]) .. ' : ' .. + tostring(col[FIELD_TYPE])) + end + end + if tbl[DATA] ~= nil then + local node = subtree:add(buffer, 'data') + if type(tbl[DATA]) == 'table' and getmetatable(tbl[DATA]) ~= ext_mt then + for _, v in ipairs(map(tbl[DATA], escape_call_arg)) do + node:add(buffer, v) + end + else + node:add(buffer, escape_call_arg(tbl[DATA])) -- bare scalar/ext payload + end + end + if tbl[SQL_INFO] ~= nil then + add_opt(subtree, buffer, 'sql row_count', tbl[SQL_INFO][SQL_INFO_ROW_COUNT]) + end + add_opt(subtree, buffer, 'position', tbl[POSITION]) + add_opt(subtree, buffer, 'stmt_id', tbl[STMT_ID]) + add_opt(subtree, buffer, 'bind_count', tbl[BIND_COUNT]) + add_opt(subtree, buffer, 'bind_metadata', tbl[BIND_METADATA]) + add_opt(subtree, buffer, 'version', tbl[VERSION]) + if tbl[FEATURES] ~= nil then + subtree:add(buffer, 'features: {' .. join_args(tbl[FEATURES]) .. '}') + end + add_opt(subtree, buffer, 'auth_type', tbl[AUTH_TYPE]) + add_opt(subtree, buffer, 'ballot', tbl[BALLOT]) + add_opt(subtree, buffer, 'replicaset_uuid', tbl[REPLICASET_UUID]) + if tbl[VCLOCK] ~= nil then + subtree:add(buffer, 'vclock: ' .. vclock_str(tbl[VCLOCK])) + end +end + +local function parse_nop(tbl, buffer, subtree) + subtree:add(buffer, 'NOP (No Operation)') +end + +local function parse_empty(tbl, buffer, subtree) + -- Body-less request (PING, VOTE, ROLLBACK, UNWATCH, ...) — nothing to show. +end + +local function parser_not_implemented(tbl, buffer, subtree) + subtree:add(buffer, 'parser not yet implemented') +end + +local UNKNOWN_COMMAND = {name = 'UNKNOWN', decoder = parser_not_implemented} + +local COMMANDS = { + [SELECT] = {name = 'select', decoder = parse_select}, + [INSERT] = {name = 'insert', decoder = parse_insert}, + [REPLACE] = {name = 'replace', decoder = parse_insert}, + [UPDATE] = {name = 'update', decoder = parse_update}, + [DELETE] = {name = 'delete', decoder = parse_delete}, + [CALL] = {name = 'call', decoder = parse_call}, + [CALL_16] = {name = 'call_16', decoder = parse_call}, + [AUTH] = {name = 'auth', decoder = parse_auth}, + [EVAL] = {name = 'eval', decoder = parse_eval}, + [UPSERT] = {name = 'upsert', decoder = parse_upsert}, + [EXECUTE] = {name = 'execute', decoder = parse_execute}, + [NOP] = {name = 'nop', decoder = parse_nop}, + [PREPARE] = {name = 'prepare', decoder = parse_prepare}, + [BEGIN] = {name = 'begin', decoder = parse_begin}, + [COMMIT] = {name = 'commit', decoder = parse_commit}, + [ROLLBACK] = {name = 'rollback', decoder = parse_empty}, + [INSERT_ARROW] = {name = 'insert_arrow', decoder = parse_insert_arrow}, + [ID] = {name = 'id', decoder = parse_id}, + [WATCH] = {name = 'watch', decoder = parse_watch}, + [UNWATCH] = {name = 'unwatch', decoder = parse_watch}, + [EVENT] = {name = 'event', decoder = parse_watch}, + [WATCH_ONCE] = {name = 'watch_once', decoder = parse_watch}, + [JOIN] = {name = 'join', decoder = parse_join}, + [JOIN_META] = {name = 'join_meta', decoder = parse_empty}, + [JOIN_SNAPSHOT] = {name = 'join_snapshot', decoder = parse_empty}, + [SUBSCRIBE] = {name = 'subscribe', decoder = parse_subscribe}, + [VOTE] = {name = 'vote', decoder = parse_empty}, + [VOTE_DEPRECATED] = {name = 'vote_deprecated', decoder = parse_empty}, + [FETCH_SNAPSHOT] = {name = 'fetch_snapshot', decoder = parse_join}, + [REGISTER] = {name = 'register', decoder = parse_join}, + [RAFT] = {name = 'raft', decoder = parser_not_implemented}, + [RAFT_PROMOTE] = {name = 'raft_promote', decoder = parse_synchro}, + [RAFT_DEMOTE] = {name = 'raft_demote', decoder = parse_synchro}, + [RAFT_CONFIRM] = {name = 'raft_confirm', decoder = parse_synchro}, + [RAFT_ROLLBACK] = {name = 'raft_rollback', decoder = parse_synchro}, + + -- Administrative commands. + [PING] = {name = 'ping', decoder = parse_empty}, + + -- Responses. + [OK] = {name = 'OK', is_response = true, decoder = parse_response}, + [CHUNK] = {name = 'CHUNK', is_response = true, decoder = parse_response}, +} + +local function code_to_command(code) + -- A corrupt header can decode TYPE to a non-number (string/array/map, or a + -- uint64 >= 2^63 that msgpack_ext wraps into an ext marker). This runs before + -- the rendering pcall, so guard the comparison rather than let it throw. + if type(code) ~= 'number' then + return UNKNOWN_COMMAND + end + if code >= TYPE_ERROR then + return {name = string.format('ERROR(0x%x)', code - TYPE_ERROR), + is_response = true, decoder = parse_error_response} + end + return COMMANDS[code] or UNKNOWN_COMMAND +end + +-- Dissect one modern PDU at `offset`; returns bytes consumed, nil (reassembly) +-- or false (not a modern PDU). +local function dissect_modern(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + + -- Tarantool always frames the length as a 5-byte 0xce uint32. + if tvb(offset, 1):uint() ~= 0xce then + return false + end + local prefix_len = 5 + if available < prefix_len then + return need_more(pinfo, offset, DESEGMENT_ONE_MORE_SEGMENT) + end + + local _, packet_length = msgpack.unpacker(tvb:raw(offset, prefix_len))() + -- Reject an absurd length instead of asking TCP to reassemble ~4 GB. + if type(packet_length) ~= 'number' or packet_length < 0 + or packet_length > 0x10000000 then + return false + end + local total = prefix_len + packet_length + if available < total then + return need_more(pinfo, offset, total - available) + end + + -- Decode header + optional body; catch malformed inner MsgPack so one bad + -- PDU never aborts the whole segment (mirrors the legacy path's pcall). + local ok, header_data, body_start, body_data = pcall(function() + local iter = msgpack.unpacker(tvb:raw(offset, total)) + iter() -- skip the length prefix already decoded above + local _, hdr = iter() + local bstart, bdata = iter() + return hdr, bstart, bdata + end) + + if not ok or type(header_data) ~= 'table' then + local subtree = tree:add(tarantool_proto, tvb(offset, total), + "Tarantool PDU (undecodable)") + subtree:add(tvb(offset, total), 'malformed or truncated MsgPack') + return total -- consume it and carry on with the next PDU + end + + local command = code_to_command(header_data[TYPE] or 0) + local subtree = tree:add(tarantool_proto, tvb(offset, total), + command.is_response and "Tarantool response" or "Tarantool request") + + -- Guard the table indexing below so a non-conforming PDU renders a note + -- instead of throwing and aborting the rest of the segment. + local rendered = pcall(function() + local header_end = body_start and (body_start - 1) or total + local header_len = header_end - prefix_len + local body_off = offset + header_end + local body_len = total - header_end + local header_range = tvb(offset + prefix_len, header_len) + local voff = map_value_offsets(tvb:raw(offset + prefix_len, header_len), + offset + prefix_len) + + -- Add a header int, reading the exact wire value (see map_value_offsets); + -- nil `field` renders a "label: value" text node. + local function add_hdr_uint(field, label, key) + if header_data[key] == nil then return end + local value = exact_uint(tvb, voff[key]) or UInt64(header_data[key]) + if field ~= nil then + subtree:add(field, header_range, value) + else + subtree:add(header_range, label .. ': ' .. tostring(value)) + end + end + + subtree:add(pf_type, header_range, header_data[TYPE] or 0) + subtree:add(pf_request, header_range, command.name) + subtree:add(pf_is_resp, header_range, command.is_response and true or false) + add_hdr_uint(pf_sync, nil, SYNC) + add_hdr_uint(pf_schema, nil, SCHEMA_VERSION) + add_hdr_uint(pf_stream, nil, STREAM_ID) + -- Replication / transaction header fields (WAL rows, multi-stmt txns). + add_hdr_uint(nil, 'replica_id', REPLICA_ID) + add_hdr_uint(nil, 'lsn', LSN) + add_hdr_uint(nil, 'tsn', TSN) + add_hdr_uint(nil, 'flags', FLAGS) + add_opt(subtree, header_range, 'timestamp', header_data[TIMESTAMP]) + add_hdr_uint(nil, 'group_id', GROUP_ID) + add_hdr_uint(nil, 'thread_id', THREAD_ID) + add_hdr_uint(nil, 'vclock_sync', VCLOCK_SYNC) + + local body_range = (body_len > 0) and tvb(body_off, body_len) + or tvb(offset, total) + local decoder = command.decoder or parser_not_implemented + decoder(body_data or {}, body_range, subtree) -- empty table when body-less + end) + if not rendered then + subtree:add(tvb(offset, total), 'malformed or non-conforming body') + end + + pinfo.cols.info:append(command.name .. ' ') + return total +end + +return { dissect = dissect_modern } + +end + +_mods['legacy'] = function(...) +-- Legacy pre-MsgPack decoder (Tarantool <= 1.5). Per doc/box-protocol.txt: +-- header ::= -- three int32, little-endian +-- tuple ::= + -- cardinality int32 LE +-- field ::= -- length is a VLQ (MSB-first) +-- Exports dissect(tvb, pinfo, tree, offset). Pure binary parsing, no MsgPack. + +local core = require 'core' + +local tarantool_proto = core.proto +local need_more = core.need_more +local pf_type = core.pf.type +local pf_request = core.pf.request +local pf_sync = core.pf.sync +local pf_is_resp = core.pf.is_resp + +-- Read a VLQ field length at `off`. Returns the value and bytes consumed. +local function legacy_varint(b, off) + local value, used = 0, 0 + while true do + local byte = b(off + used, 1):uint() + used = used + 1 + value = value * 128 + (byte % 128) + if byte < 128 then break end + end + return value, used +end + +-- Legacy fields are typeless on the wire, so guess from the bytes: printable +-- text that fills the whole field -> quoted string; otherwise a 4-/8-byte field +-- -> its little-endian unsigned integer (NUM/NUM64); else a byte count. The +-- "fills the whole field" check (#s == len) keeps this independent of how a +-- Wireshark version truncates :string() at embedded NUL bytes. +local function legacy_field_text(range) + local len = range:len() + local ok, s = pcall(function() return range:string() end) + if ok and #s == len and s:match('^[\32-\126]*$') then + return '"' .. s .. '"' + elseif len == 4 then + return tostring(range:le_uint()) + elseif len == 8 then + return tostring(range:le_uint64()) + end + return string.format('<%d bytes>', len) +end + +-- tuple ::= +. Returns bytes the tuple occupies. +local function legacy_add_tuple(b, subtree, num) + local card = b(0, 4):le_uint() + local off, parts = 4, {} + local node = subtree:add(b(0, 4), string.format('tuple #%d (cardinality %d)', num, card)) + for i = 1, card do + local flen, used = legacy_varint(b, off) + local text = legacy_field_text(b(off + used, flen)) + node:add(b(off, used + flen), string.format('[%d] %s', i, text)) + parts[#parts + 1] = text + off = off + used + flen + end + node:append_text(' {' .. table.concat(parts, ', ') .. '}') + return off +end + +local function legacy_call_req(b, subtree) + subtree:add(b(0, 4), string.format('flags: 0x%08x', b(0, 4):le_uint())) + local nlen, used = legacy_varint(b, 4) + local name = b(4 + used, nlen):string() + subtree:add(b(4, used + nlen), 'function: ' .. name) + local args_off = 4 + used + nlen + if b:len() > args_off then legacy_add_tuple(b(args_off), subtree, 0) end + subtree:append_text(string.format(' call %s(...)', name)) +end + +local function legacy_select_req(b, subtree) + local lim = b(12, 4):le_uint() + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), 'index: ' .. b(4, 4):le_uint()) + subtree:add(b(8, 4), 'offset: ' .. b(8, 4):le_uint()) + subtree:add(b(12, 4), 'limit: ' .. (lim == 4294967295 and 'unlimited' or lim)) + local count = b(16, 4):le_uint() + subtree:add(b(16, 4), 'keys: ' .. count) + local o = 20 + for i = 1, count do o = o + legacy_add_tuple(b(o), subtree, i) end +end + +local function legacy_insert_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), string.format('flags: 0x%08x', b(4, 4):le_uint())) + legacy_add_tuple(b(8), subtree, 0) +end + +local function legacy_delete_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), string.format('flags: 0x%08x', b(4, 4):le_uint())) + legacy_add_tuple(b(8), subtree, 0) +end + +-- Pre-1.5 obsolete DELETE (type 20): , no flags. +local function legacy_delete_v13_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + legacy_add_tuple(b(4), subtree, 0) +end + +-- UPDATE: +. Op encoding is +-- version-specific, so show the key, op count and the rest as a blob. +local function legacy_update_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), string.format('flags: 0x%08x', b(4, 4):le_uint())) + local off = 8 + legacy_add_tuple(b(8), subtree, 0) + if b:len() >= off + 4 then + subtree:add(b(off, 4), 'operations: ' .. b(off, 4):le_uint()) + if b:len() > off + 4 then + subtree:add(b(off + 4), string.format('ops payload: %d bytes', b:len() - off - 4)) + end + end +end + +-- fq_tuples: count-prefixed list, each tuple preceded by its u32 byte size. +local function legacy_add_fqtuples(b, subtree, count) + local o = 0 + for i = 1, count do + subtree:add(b(o, 4), string.format('tuple #%d size: %d', i, b(o, 4):le_uint())) + o = o + 4 + legacy_add_tuple(b(o + 4), subtree, i) + end +end + +-- response ::=
{}. return_code: low byte = status +-- (0 ok, 1 try again, 2 error), upper 3 bytes = error code. Body only on success. +local LEGACY_STATUS = { [0] = 'ok', [1] = 'try again', [2] = 'error' } +local function legacy_response(rtype, b, subtree) + local code = b(0, 4):le_uint() + local status, errcode = code % 256, math.floor(code / 256) + subtree:add(b(0, 4), string.format('return code: 0x%08x (%s%s)', code, + LEGACY_STATUS[status] or ('status ' .. status), + status ~= 0 and string.format(', error 0x%x', errcode) or '')) + if status ~= 0 then + if b:len() > 4 then subtree:add(b(4), 'error: ' .. b(4):string()) end + return + end + if b:len() > 4 then + local count = b(4, 4):le_uint() + subtree:add(b(4, 4), 'count: ' .. count) + if b:len() > 8 then legacy_add_fqtuples(b(8), subtree, count) end + end +end + +-- 1.5 request types (doc/box-protocol.txt), plus the pre-1.5 obsolete DELETE (20). +local LEGACY_NAME = { + [13] = 'insert', + [17] = 'select', + [19] = 'update', + [20] = 'delete_v13', + [21] = 'delete', + [22] = 'call', + [65280] = 'ping', +} +local LEGACY_REQ = { + [13] = legacy_insert_req, + [17] = legacy_select_req, + [19] = legacy_update_req, + [20] = legacy_delete_v13_req, + [21] = legacy_delete_req, + [22] = legacy_call_req, + -- 65280 (ping) has an empty body. +} + +local function dissect_legacy(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + if available < 4 then + return need_more(pinfo, offset, DESEGMENT_ONE_MORE_SEGMENT) + end + local rtype = tvb(offset, 4):le_uint() + local name = LEGACY_NAME[rtype] + if name == nil then + return false -- not a legacy header we recognise; leave it for Data + end + if available < 12 then + return need_more(pinfo, offset, 12 - available) + end + local body_len = tvb(offset + 4, 4):le_uint() + local req_id = tvb(offset + 8, 4):le_uint() + local total = 12 + body_len + if available < total then + return need_more(pinfo, offset, total - available) + end + + -- Request and response share the header (same type), so direction is the + -- only signal: match a configured server port if possible, else assume the + -- server is the lower (well-known) port. Keeps responses decoding even on a + -- non-default port via Decode As (e.g. legacy 33013). + local is_response + if core.is_server_port(pinfo.src_port) then + is_response = true + elseif core.is_server_port(pinfo.dst_port) then + is_response = false + else + is_response = pinfo.src_port < pinfo.dst_port + end + local subtree = tree:add(tarantool_proto, tvb(offset, total), + is_response and "Tarantool response (legacy <= 1.5)" + or "Tarantool request (legacy <= 1.5)") + subtree:add(pf_type, tvb(offset, 4), rtype) + subtree:add(pf_request, tvb(offset, 4), name) + subtree:add(pf_is_resp, tvb(offset, 4), is_response) + subtree:add(pf_sync, tvb(offset + 8, 4), UInt64(req_id)) + subtree:add(tvb(offset, 12), string.format( + 'legacy header: type %d (%s), body_len %d, req_id 0x%08x', + rtype, name, body_len, req_id)) + + if body_len > 0 then + local body = tvb(offset + 12, body_len) + local ok = pcall(function() + if is_response then + legacy_response(rtype, body, subtree) + else + local fn = LEGACY_REQ[rtype] + if fn then fn(body, subtree) + else subtree:add(body, string.format('body: %d bytes', body_len)) end + end + end) + if not ok then subtree:add(body, 'malformed legacy body') end + end + + pinfo.cols.info:append((is_response and 'resp ' or '') .. name .. ' ') + return total +end + +return { dissect = dissect_legacy } + +end + +-- Entry point: all versions. Modern PDUs start with the 0xce length prefix; +-- anything else is tried as legacy framing (the two are unambiguous). + +-- core.init must run before requiring the decoders: they capture core.proto at +-- load time. +local core = require 'core' +core.init('tarantool', 'Tarantool') + +local modern = require 'modern' +local legacy = require 'legacy' + +local function dispatch(tvb, pinfo, tree, offset) + if tvb(offset, 1):uint() == 0xce then + return modern.dissect(tvb, pinfo, tree, offset) + end + return legacy.dissect(tvb, pinfo, tree, offset) +end + +core.proto.dissector = core.make_dissector(dispatch) +core.register() diff --git a/src/core.lua b/src/core.lua new file mode 100644 index 0000000..4247743 --- /dev/null +++ b/src/core.lua @@ -0,0 +1,143 @@ +-- Shared dissector core: the Proto, header fields, port/enabled preferences, +-- greeting, the main-loop factory and registration. Modern/legacy decoders and +-- the per-build entry points build on this. +-- +-- The Proto is created by M.init(slug, desc), which each entry point calls with +-- its own name (e.g. "tarantool1", "tarantool2", "tarantool") BEFORE requiring +-- the decoder modules -- they capture M.proto/M.pf at load time. + +local M = {} + +-- Set by M.init; the closures below capture these names as upvalues, so they +-- see the values init assigns. +local proto +local pf + +-- Request reassembly: returns nil so the caller stops and TCP redelivers more. +local function need_more(pinfo, offset, more) + if pinfo.can_desegment > 0 then + pinfo.desegment_offset = offset + pinfo.desegment_len = more + end + return nil +end +M.need_more = need_more + +local GREETING_SIZE = 128 +local GREETING_SALT_OFFSET = 64 +local GREETING_SALT_SIZE = 44 + +local function dissect_greeting(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + if available < GREETING_SIZE then + return need_more(pinfo, offset, GREETING_SIZE - available) + end + pinfo.cols.info:append('Greeting ') + local subtree = tree:add(proto, tvb(offset, GREETING_SIZE), "Tarantool greeting") + subtree:add(tvb(offset, GREETING_SALT_OFFSET), + "Server version: " .. tvb(offset, GREETING_SALT_OFFSET):string()) + subtree:add(tvb(offset + GREETING_SALT_OFFSET, GREETING_SALT_SIZE), + "Salt: " .. tvb(offset + GREETING_SALT_OFFSET, GREETING_SALT_SIZE):string()) + subtree:add(tvb(offset + GREETING_SALT_OFFSET + GREETING_SALT_SIZE, + GREETING_SIZE - GREETING_SALT_OFFSET - GREETING_SALT_SIZE), "Reserved") + return GREETING_SIZE +end +M.dissect_greeting = dissect_greeting + +-- `dispatch_pdu` decodes one non-greeting PDU (returns bytes consumed, nil for +-- reassembly, or false for "not ours"); which one is wired in distinguishes the +-- modern-only, legacy-only and combined builds. +function M.make_dissector(dispatch_pdu) + return function(tvb, pinfo, tree) + pinfo.cols.protocol = "Tarantool" + pinfo.cols.info:clear() + local n = tvb:len() + local offset = 0 + while offset < n do + local consumed + if n - offset >= 9 and tvb(offset, 9):string() == "Tarantool" then + consumed = dissect_greeting(tvb, pinfo, tree, offset) + else + consumed = dispatch_pdu(tvb, pinfo, tree, offset) + end + if consumed == nil then return -- reassembly requested + elseif not consumed then break end -- not decodable as our protocol + offset = offset + consumed + end + return offset + end +end + +local tcp_port_table = DissectorTable.get("tcp.port") +local registered_ports +local server_ports = {} + +-- Parse a Wireshark port range ("3301,3311-3313") into a lookup set, so the +-- legacy decoder can tell a server-side port from a client port for direction. +local function parse_ports(spec) + local set = {} + for part in tostring(spec):gmatch("[^,]+") do + local a, b = part:match("^%s*(%d+)%s*%-%s*(%d+)%s*$") + if a then + for p = tonumber(a), tonumber(b) do set[p] = true end + else + local n = part:match("^%s*(%d+)%s*$") + if n then set[tonumber(n)] = true end + end + end + return set +end + +-- True if `port` is one of the configured Tarantool server ports. +function M.is_server_port(port) return server_ports[port] == true end + +-- Sync the tcp.port registration with the current `enabled`/`ports` preferences. +-- `ports` is a range (e.g. "3301,3311-3313"); drop the previously registered +-- range and add the current one -- Wireshark expands the range and binds each +-- port. Idempotent: safe to call on every prefs change. +function M.register() + if registered_ports ~= nil then + tcp_port_table:remove(registered_ports, proto) + registered_ports = nil + end + server_ports = {} + if not proto.prefs.enabled then return end + tcp_port_table:add(proto.prefs.ports, proto) + registered_ports = proto.prefs.ports + server_ports = parse_ports(proto.prefs.ports) +end + +-- Create the protocol under `slug` (display name `desc`), register its header +-- fields and preferences, and wire prefs_changed. `default_port` seeds the +-- "TCP ports" range preference (3301 for modern; legacy <=1.5 used 33013) -- a +-- distinct default keeps co-loaded builds off the same port, since Wireshark's +-- tcp.port table binds one dissector per port. The user can widen it to a range +-- (e.g. "3301,3311-3313") to decode a whole cluster. Call once, before +-- requiring the decoder modules. +function M.init(slug, desc, default_port) + proto = Proto(slug, desc) + M.proto = proto + + -- Header fields, also usable as display filters (e.g. `tnt.type == 0x01`). + pf = { + type = ProtoField.uint16("tnt.type", "Request type", base.HEX), + request = ProtoField.string("tnt.request", "Request name"), + sync = ProtoField.uint64("tnt.sync", "Sync", base.DEC), + schema = ProtoField.uint64("tnt.schema_version", "Schema version", base.DEC), + stream = ProtoField.uint64("tnt.stream_id", "Stream id", base.DEC), + is_resp = ProtoField.bool("tnt.response", "Is response"), + } + M.pf = pf + proto.fields = { pf.type, pf.request, pf.sync, pf.schema, pf.stream, pf.is_resp } + + proto.prefs.enabled = Pref.bool("Dissector enabled", true, + "Whether the Tarantool dissector is enabled") + proto.prefs.ports = Pref.range("TCP ports", tostring(default_port or 3301), + "Ports to decode as Tarantool, e.g. 3301,3311-3313", 65535) + + function proto.prefs_changed() M.register() end + + return M +end + +return M diff --git a/src/entry_all.lua b/src/entry_all.lua new file mode 100644 index 0000000..fb2b25f --- /dev/null +++ b/src/entry_all.lua @@ -0,0 +1,20 @@ +-- Entry point: all versions. Modern PDUs start with the 0xce length prefix; +-- anything else is tried as legacy framing (the two are unambiguous). + +-- core.init must run before requiring the decoders: they capture core.proto at +-- load time. +local core = require 'core' +core.init('tarantool', 'Tarantool') + +local modern = require 'modern' +local legacy = require 'legacy' + +local function dispatch(tvb, pinfo, tree, offset) + if tvb(offset, 1):uint() == 0xce then + return modern.dissect(tvb, pinfo, tree, offset) + end + return legacy.dissect(tvb, pinfo, tree, offset) +end + +core.proto.dissector = core.make_dissector(dispatch) +core.register() diff --git a/src/entry_legacy.lua b/src/entry_legacy.lua new file mode 100644 index 0000000..c701c16 --- /dev/null +++ b/src/entry_legacy.lua @@ -0,0 +1,12 @@ +-- Entry point: legacy only (Tarantool <= 1.5). No MsgPack. Unrecognised bytes +-- are left undissected (shown as Data). + +-- core.init must run before requiring the decoder: it captures core.proto at +-- load time. +local core = require 'core' +core.init('tarantool1', 'Tarantool 1.5', 33013) + +local legacy = require 'legacy' + +core.proto.dissector = core.make_dissector(legacy.dissect) +core.register() diff --git a/src/entry_modern.lua b/src/entry_modern.lua new file mode 100644 index 0000000..51ee2f3 --- /dev/null +++ b/src/entry_modern.lua @@ -0,0 +1,12 @@ +-- Entry point: modern only (Tarantool 1.6 .. 3.x). Non-modern bytes are left +-- undissected (shown as Data). + +-- core.init must run before requiring the decoder: it captures core.proto at +-- load time. +local core = require 'core' +core.init('tarantool2', 'Tarantool 1.6+') + +local modern = require 'modern' + +core.proto.dissector = core.make_dissector(modern.dissect) +core.register() diff --git a/src/legacy.lua b/src/legacy.lua new file mode 100644 index 0000000..3a90e14 --- /dev/null +++ b/src/legacy.lua @@ -0,0 +1,226 @@ +-- Legacy pre-MsgPack decoder (Tarantool <= 1.5). Per doc/box-protocol.txt: +-- header ::= -- three int32, little-endian +-- tuple ::= + -- cardinality int32 LE +-- field ::= -- length is a VLQ (MSB-first) +-- Exports dissect(tvb, pinfo, tree, offset). Pure binary parsing, no MsgPack. + +local core = require 'core' + +local tarantool_proto = core.proto +local need_more = core.need_more +local pf_type = core.pf.type +local pf_request = core.pf.request +local pf_sync = core.pf.sync +local pf_is_resp = core.pf.is_resp + +-- Read a VLQ field length at `off`. Returns the value and bytes consumed. +local function legacy_varint(b, off) + local value, used = 0, 0 + while true do + local byte = b(off + used, 1):uint() + used = used + 1 + value = value * 128 + (byte % 128) + if byte < 128 then break end + end + return value, used +end + +-- Legacy fields are typeless on the wire, so guess from the bytes: printable +-- text that fills the whole field -> quoted string; otherwise a 4-/8-byte field +-- -> its little-endian unsigned integer (NUM/NUM64); else a byte count. The +-- "fills the whole field" check (#s == len) keeps this independent of how a +-- Wireshark version truncates :string() at embedded NUL bytes. +local function legacy_field_text(range) + local len = range:len() + local ok, s = pcall(function() return range:string() end) + if ok and #s == len and s:match('^[\32-\126]*$') then + return '"' .. s .. '"' + elseif len == 4 then + return tostring(range:le_uint()) + elseif len == 8 then + return tostring(range:le_uint64()) + end + return string.format('<%d bytes>', len) +end + +-- tuple ::= +. Returns bytes the tuple occupies. +local function legacy_add_tuple(b, subtree, num) + local card = b(0, 4):le_uint() + local off, parts = 4, {} + local node = subtree:add(b(0, 4), string.format('tuple #%d (cardinality %d)', num, card)) + for i = 1, card do + local flen, used = legacy_varint(b, off) + local text = legacy_field_text(b(off + used, flen)) + node:add(b(off, used + flen), string.format('[%d] %s', i, text)) + parts[#parts + 1] = text + off = off + used + flen + end + node:append_text(' {' .. table.concat(parts, ', ') .. '}') + return off +end + +local function legacy_call_req(b, subtree) + subtree:add(b(0, 4), string.format('flags: 0x%08x', b(0, 4):le_uint())) + local nlen, used = legacy_varint(b, 4) + local name = b(4 + used, nlen):string() + subtree:add(b(4, used + nlen), 'function: ' .. name) + local args_off = 4 + used + nlen + if b:len() > args_off then legacy_add_tuple(b(args_off), subtree, 0) end + subtree:append_text(string.format(' call %s(...)', name)) +end + +local function legacy_select_req(b, subtree) + local lim = b(12, 4):le_uint() + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), 'index: ' .. b(4, 4):le_uint()) + subtree:add(b(8, 4), 'offset: ' .. b(8, 4):le_uint()) + subtree:add(b(12, 4), 'limit: ' .. (lim == 4294967295 and 'unlimited' or lim)) + local count = b(16, 4):le_uint() + subtree:add(b(16, 4), 'keys: ' .. count) + local o = 20 + for i = 1, count do o = o + legacy_add_tuple(b(o), subtree, i) end +end + +local function legacy_insert_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), string.format('flags: 0x%08x', b(4, 4):le_uint())) + legacy_add_tuple(b(8), subtree, 0) +end + +local function legacy_delete_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), string.format('flags: 0x%08x', b(4, 4):le_uint())) + legacy_add_tuple(b(8), subtree, 0) +end + +-- Pre-1.5 obsolete DELETE (type 20): , no flags. +local function legacy_delete_v13_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + legacy_add_tuple(b(4), subtree, 0) +end + +-- UPDATE: +. Op encoding is +-- version-specific, so show the key, op count and the rest as a blob. +local function legacy_update_req(b, subtree) + subtree:add(b(0, 4), 'space: ' .. b(0, 4):le_uint()) + subtree:add(b(4, 4), string.format('flags: 0x%08x', b(4, 4):le_uint())) + local off = 8 + legacy_add_tuple(b(8), subtree, 0) + if b:len() >= off + 4 then + subtree:add(b(off, 4), 'operations: ' .. b(off, 4):le_uint()) + if b:len() > off + 4 then + subtree:add(b(off + 4), string.format('ops payload: %d bytes', b:len() - off - 4)) + end + end +end + +-- fq_tuples: count-prefixed list, each tuple preceded by its u32 byte size. +local function legacy_add_fqtuples(b, subtree, count) + local o = 0 + for i = 1, count do + subtree:add(b(o, 4), string.format('tuple #%d size: %d', i, b(o, 4):le_uint())) + o = o + 4 + legacy_add_tuple(b(o + 4), subtree, i) + end +end + +-- response ::=
{}. return_code: low byte = status +-- (0 ok, 1 try again, 2 error), upper 3 bytes = error code. Body only on success. +local LEGACY_STATUS = { [0] = 'ok', [1] = 'try again', [2] = 'error' } +local function legacy_response(rtype, b, subtree) + local code = b(0, 4):le_uint() + local status, errcode = code % 256, math.floor(code / 256) + subtree:add(b(0, 4), string.format('return code: 0x%08x (%s%s)', code, + LEGACY_STATUS[status] or ('status ' .. status), + status ~= 0 and string.format(', error 0x%x', errcode) or '')) + if status ~= 0 then + if b:len() > 4 then subtree:add(b(4), 'error: ' .. b(4):string()) end + return + end + if b:len() > 4 then + local count = b(4, 4):le_uint() + subtree:add(b(4, 4), 'count: ' .. count) + if b:len() > 8 then legacy_add_fqtuples(b(8), subtree, count) end + end +end + +-- 1.5 request types (doc/box-protocol.txt), plus the pre-1.5 obsolete DELETE (20). +local LEGACY_NAME = { + [13] = 'insert', + [17] = 'select', + [19] = 'update', + [20] = 'delete_v13', + [21] = 'delete', + [22] = 'call', + [65280] = 'ping', +} +local LEGACY_REQ = { + [13] = legacy_insert_req, + [17] = legacy_select_req, + [19] = legacy_update_req, + [20] = legacy_delete_v13_req, + [21] = legacy_delete_req, + [22] = legacy_call_req, + -- 65280 (ping) has an empty body. +} + +local function dissect_legacy(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + if available < 4 then + return need_more(pinfo, offset, DESEGMENT_ONE_MORE_SEGMENT) + end + local rtype = tvb(offset, 4):le_uint() + local name = LEGACY_NAME[rtype] + if name == nil then + return false -- not a legacy header we recognise; leave it for Data + end + if available < 12 then + return need_more(pinfo, offset, 12 - available) + end + local body_len = tvb(offset + 4, 4):le_uint() + local req_id = tvb(offset + 8, 4):le_uint() + local total = 12 + body_len + if available < total then + return need_more(pinfo, offset, total - available) + end + + -- Request and response share the header (same type), so direction is the + -- only signal: match a configured server port if possible, else assume the + -- server is the lower (well-known) port. Keeps responses decoding even on a + -- non-default port via Decode As (e.g. legacy 33013). + local is_response + if core.is_server_port(pinfo.src_port) then + is_response = true + elseif core.is_server_port(pinfo.dst_port) then + is_response = false + else + is_response = pinfo.src_port < pinfo.dst_port + end + local subtree = tree:add(tarantool_proto, tvb(offset, total), + is_response and "Tarantool response (legacy <= 1.5)" + or "Tarantool request (legacy <= 1.5)") + subtree:add(pf_type, tvb(offset, 4), rtype) + subtree:add(pf_request, tvb(offset, 4), name) + subtree:add(pf_is_resp, tvb(offset, 4), is_response) + subtree:add(pf_sync, tvb(offset + 8, 4), UInt64(req_id)) + subtree:add(tvb(offset, 12), string.format( + 'legacy header: type %d (%s), body_len %d, req_id 0x%08x', + rtype, name, body_len, req_id)) + + if body_len > 0 then + local body = tvb(offset + 12, body_len) + local ok = pcall(function() + if is_response then + legacy_response(rtype, body, subtree) + else + local fn = LEGACY_REQ[rtype] + if fn then fn(body, subtree) + else subtree:add(body, string.format('body: %d bytes', body_len)) end + end + end) + if not ok then subtree:add(body, 'malformed legacy body') end + end + + pinfo.cols.info:append((is_response and 'resp ' or '') .. name .. ' ') + return total +end + +return { dissect = dissect_legacy } diff --git a/src/modern.lua b/src/modern.lua new file mode 100644 index 0000000..81d68dc --- /dev/null +++ b/src/modern.lua @@ -0,0 +1,700 @@ +-- Modern MsgPack IPROTO decoder (Tarantool 1.6 .. 3.x). A PDU is a 5-byte 0xce +-- uint32 length prefix, then a header map and an optional body map. Exports +-- dissect(tvb, pinfo, tree, offset). + +local core = require 'core' +local mpx = require 'msgpack_ext' + +local msgpack = mpx.msgpack +local ext_mt = mpx.ext_mt + +local tarantool_proto = core.proto +local need_more = core.need_more +local pf_type = core.pf.type +local pf_request = core.pf.request +local pf_sync = core.pf.sync +local pf_schema = core.pf.schema +local pf_stream = core.pf.stream +local pf_is_resp = core.pf.is_resp + +-- iproto_type: request/command codes (src/box/iproto_constants.h). +local OK = 0x00 +local SELECT = 0x01 +local INSERT = 0x02 +local REPLACE = 0x03 +local UPDATE = 0x04 +local DELETE = 0x05 +local CALL_16 = 0x06 -- 1.6-era call: coerced results into tuples; kept as call_16 +local AUTH = 0x07 +local EVAL = 0x08 +local UPSERT = 0x09 +local CALL = 0x0a -- modern call (1.7.2+): returns a plain array +local EXECUTE = 0x0b +local NOP = 0x0c +local PREPARE = 0x0d +local BEGIN = 0x0e +local COMMIT = 0x0f +local ROLLBACK = 0x10 +local INSERT_ARROW = 0x11 +local RAFT = 0x1e +local RAFT_PROMOTE = 0x1f +local RAFT_DEMOTE = 0x20 +local RAFT_CONFIRM = 0x28 +local RAFT_ROLLBACK = 0x29 +local PING = 0x40 +local JOIN = 0x41 +local SUBSCRIBE = 0x42 +local VOTE_DEPRECATED = 0x43 +local VOTE = 0x44 +local FETCH_SNAPSHOT = 0x45 +local REGISTER = 0x46 +local JOIN_META = 0x47 +local JOIN_SNAPSHOT = 0x48 +local ID = 0x49 +local WATCH = 0x4a +local UNWATCH = 0x4b +local EVENT = 0x4c +local WATCH_ONCE = 0x4d + +-- Response markers. +local CHUNK = 0x80 -- non-final response chunk (box.session.push) +local TYPE_ERROR = 0x8000 -- bit 15 set => error, low 15 bits = errcode + +-- iproto_key: header keys (0x00 .. 0x0b). +local TYPE = 0x00 -- IPROTO_REQUEST_TYPE +local SYNC = 0x01 +local REPLICA_ID = 0x02 +local LSN = 0x03 +local TIMESTAMP = 0x04 +local SCHEMA_VERSION = 0x05 +local SERVER_VERSION = 0x06 +local GROUP_ID = 0x07 +local TSN = 0x08 +local FLAGS = 0x09 +local STREAM_ID = 0x0a +local THREAD_ID = 0x0b + +-- iproto_key: DML body keys (0x10 .. 0x2f). +local SPACE_ID = 0x10 +local INDEX_ID = 0x11 +local LIMIT = 0x12 +local OFFSET = 0x13 +local ITERATOR = 0x14 +local INDEX_BASE = 0x15 +local FETCH_POSITION = 0x1f +local KEY = 0x20 +local TUPLE = 0x21 +local FUNCTION_NAME = 0x22 +local USER_NAME = 0x23 +local INSTANCE_UUID = 0x24 +local REPLICASET_UUID = 0x25 +local VCLOCK = 0x26 +local EXPRESSION = 0x27 +local OPS = 0x28 +local BALLOT = 0x29 +local OLD_TUPLE = 0x2c +local NEW_TUPLE = 0x2d +local AFTER_POSITION = 0x2e +local AFTER_TUPLE = 0x2f + +-- iproto_key: response keys (0x30 .. 0x35). +local DATA = 0x30 +local ERROR_24 = 0x31 -- legacy string error +local METADATA = 0x32 +local BIND_METADATA = 0x33 +local BIND_COUNT = 0x34 +local POSITION = 0x35 + +-- iproto_key: SQL keys (0x40 .. 0x43). +local SQL_TEXT = 0x40 +local SQL_BIND = 0x41 +local SQL_INFO = 0x42 +local STMT_ID = 0x43 + +-- Nested keys inside response sub-structures. +local FIELD_NAME = 0x00 -- column maps in METADATA +local FIELD_TYPE = 0x01 +local SQL_INFO_ROW_COUNT = 0x00 -- inside SQL_INFO map + +-- iproto_key: extended keys (0x50 .. 0x64). +local REPLICA_ANON = 0x50 +local ID_FILTER = 0x51 +local ERROR = 0x52 -- structured error stack (MP_MAP) +local TERM = 0x53 +local VERSION = 0x54 +local FEATURES = 0x55 +local TIMEOUT = 0x56 +local EVENT_KEY = 0x57 +local EVENT_DATA = 0x58 +local TXN_ISOLATION = 0x59 +local VCLOCK_SYNC = 0x5a +local AUTH_TYPE = 0x5b +local REPLICASET_NAME = 0x5c +local INSTANCE_NAME = 0x5d +local SPACE_NAME = 0x5e +local INDEX_NAME = 0x5f +local IS_SYNC = 0x61 + +-- iterator types (box.index iterator codes), for nicer SELECT output. +local ITERATOR_NAME = { + [0] = 'EQ', [1] = 'REQ', [2] = 'ALL', [3] = 'LT', [4] = 'LE', + [5] = 'GE', [6] = 'GT', [7] = 'BITS_ALL_SET', [8] = 'BITS_ANY_SET', + [9] = 'BITS_ALL_NOT_SET', [10] = 'OVERLAPS', [11] = 'NEIGHBOR', +} + +-- helpers --------------------------------------------------------------------- + +-- 0-based wire offset of each value in a top-level MsgPack map, keyed by map key, +-- so 64-bit header fields can be read exactly (see exact_uint) instead of via +-- MessagePack.lua's lossy decode. +local function map_value_offsets(raw, base_off) + local b = raw:byte(1) + local count, first + if b >= 0x80 and b <= 0x8f then count, first = b - 0x80, 2 + elseif b == 0xde then count, first = raw:byte(2) * 256 + raw:byte(3), 4 + elseif b == 0xdf then + count = ((raw:byte(2) * 256 + raw:byte(3)) * 256 + raw:byte(4)) * 256 + + raw:byte(5) + first = 6 + else + return {} + end + local offsets = {} + local iter = msgpack.unpacker(raw:sub(first)) + for _ = 1, count do + local _, key = iter() -- key element + local value_pos = iter() -- start of value element (1-based within sub) + if value_pos == nil then break end + offsets[key] = base_off + first + value_pos - 2 + end + return offsets +end + +-- Read the MsgPack uint at `off` as a full-precision Wireshark UInt64, or nil if +-- `off` is nil or the bytes are not a uint. +local function exact_uint(tvb, off) + if off == nil then return nil end + local b = tvb(off, 1):uint() + if b <= 0x7f then return UInt64(b) -- positive fixint + elseif b == 0xcc then return UInt64(tvb(off + 1, 1):uint()) + elseif b == 0xcd then return UInt64(tvb(off + 1, 2):uint()) + elseif b == 0xce then return UInt64(tvb(off + 1, 4):uint()) + elseif b == 0xcf then return tvb(off + 1, 8):uint64() + end + return nil +end + +local function map(tbl, callback) + local result = {} + if tbl == nil then return result end + for k, v in pairs(tbl) do + result[k] = callback(v) + end + return result +end + +local function table_kv_concat(tbl, sep) + local result = {} + local used_keys = {} + for i, v in ipairs(tbl) do + used_keys[i] = true + table.insert(result, v) + end + for k, v in pairs(tbl) do + if not used_keys[k] then + local key = (type(k) == 'table' and getmetatable(k) == ext_mt) + and k.text or tostring(k) + table.insert(result, key .. ' = ' .. tostring(v)) + end + end + return table.concat(result, sep) +end + +local function escape_call_arg(a) + if type(a) == 'table' and getmetatable(a) == ext_mt then + return a.text -- a decoded MP_EXT value (datetime, decimal, uuid, ...) + end + local t = type(a) + if t == 'number' or t == 'boolean' then + return tostring(a) + elseif t == 'string' then + return '"' .. a .. '"' + elseif t == 'table' then + return '{' .. table_kv_concat(map(a, escape_call_arg), ', ') .. '}' + elseif a == nil then + return 'nil' + end + return tostring(a) +end + +-- Concatenate an array (possibly nil) of msgpack values into a readable string. +local function join_args(arr) + return table.concat(map(arr, escape_call_arg), ', ') +end + +-- Add a "label: value" node only when the value is present. +local function add_opt(subtree, buffer, label, value) + if value ~= nil then + subtree:add(buffer, label .. ': ' .. escape_call_arg(value)) + end +end + +-- decoders -------------------------------------------------------------------- +-- Each receives (body_table, body_tvbrange, subtree); body_table may be empty +-- for body-less requests (PING, VOTE, ...). + +local function parse_call(tbl, buffer, subtree) + local name = tbl[FUNCTION_NAME] + local args = tbl[TUPLE] + subtree:add(buffer, string.format('%s(%s)', tostring(name), join_args(args))) +end + +local function parse_eval(tbl, buffer, subtree) + local expression = tbl[EXPRESSION] + local args = tbl[TUPLE] + subtree:add(buffer, string.format('eval %s with args (%s)', + tostring(expression), join_args(args))) +end + +local function parse_select(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + local index = tbl[INDEX_NAME] or tbl[INDEX_ID] or 0 + local limit = tbl[LIMIT] + local offset = tbl[OFFSET] or 0 + local iterator = tbl[ITERATOR] or 0 + + subtree:add(buffer, string.format( + 'SELECT FROM space %s WHERE index(%s) = (%s) LIMIT %s OFFSET %d ITERATOR %s', + tostring(space), tostring(index), join_args(tbl[KEY]), + tostring(limit), offset, + ITERATOR_NAME[iterator] or tostring(iterator))) + -- Pagination (request side). + add_opt(subtree, buffer, 'fetch_position', tbl[FETCH_POSITION]) + add_opt(subtree, buffer, 'after_position', tbl[AFTER_POSITION]) + if tbl[AFTER_TUPLE] ~= nil then + subtree:add(buffer, 'after_tuple: {' .. join_args(tbl[AFTER_TUPLE]) .. '}') + end +end + +local function parse_insert(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'tuple: {' .. join_args(tbl[TUPLE]) .. '}') + -- Before/after images carried by replicated DML rows. + add_opt(subtree, buffer, 'old_tuple', tbl[OLD_TUPLE]) + add_opt(subtree, buffer, 'new_tuple', tbl[NEW_TUPLE]) +end + +local function parse_delete(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + local index = tbl[INDEX_NAME] or tbl[INDEX_ID] or 0 + subtree:add(buffer, string.format('DELETE FROM space(%s) WHERE index(%s) = (%s)', + tostring(space), tostring(index), join_args(tbl[KEY]))) +end + +local function parse_upsert(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'tuple: {' .. join_args(tbl[TUPLE]) .. '}') + subtree:add(buffer, 'ops: {' .. join_args(tbl[OPS]) .. '}') + add_opt(subtree, buffer, 'index_base', tbl[INDEX_BASE]) +end + +local function parse_update(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + local index = tbl[INDEX_NAME] or tbl[INDEX_ID] or 0 + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'index: ' .. tostring(index)) + subtree:add(buffer, 'key: {' .. join_args(tbl[KEY]) .. '}') + subtree:add(buffer, 'ops: {' .. join_args(tbl[TUPLE]) .. '}') + add_opt(subtree, buffer, 'index_base', tbl[INDEX_BASE]) + add_opt(subtree, buffer, 'old_tuple', tbl[OLD_TUPLE]) + add_opt(subtree, buffer, 'new_tuple', tbl[NEW_TUPLE]) +end + +local function parse_auth(tbl, buffer, subtree) + local user = tbl[USER_NAME] + local tuple = tbl[TUPLE] or {} + subtree:add(buffer, string.format( + 'Authentication: user "%s", mechanism %s', + tostring(user), tostring(tuple[1]))) +end + +local function parse_id(tbl, buffer, subtree) + subtree:add(buffer, 'protocol version: ' .. tostring(tbl[VERSION])) + subtree:add(buffer, 'features: {' .. join_args(tbl[FEATURES]) .. '}') + if tbl[AUTH_TYPE] ~= nil then + subtree:add(buffer, 'auth_type: ' .. tostring(tbl[AUTH_TYPE])) + end +end + +local function parse_execute(tbl, buffer, subtree) + local stmt_id = tbl[STMT_ID] + local sql_text = tbl[SQL_TEXT] + local bind = join_args(tbl[SQL_BIND]) + if bind ~= '' then + bind = string.format(', with parameters (%s)', bind) + end + if stmt_id ~= nil then + subtree:add(buffer, string.format( + 'execute prepared statement id %s%s', tostring(stmt_id), bind)) + else + subtree:add(buffer, string.format( + 'execute SQL "%s"%s', tostring(sql_text), bind)) + end +end + +local function parse_prepare(tbl, buffer, subtree) + local stmt_id = tbl[STMT_ID] + local sql_text = tbl[SQL_TEXT] + if stmt_id ~= nil then + subtree:add(buffer, 'unprepare/prepare statement id ' .. tostring(stmt_id)) + else + subtree:add(buffer, string.format('prepare SQL "%s"', tostring(sql_text))) + end +end + +local function parse_begin(tbl, buffer, subtree) + add_opt(subtree, buffer, 'timeout', tbl[TIMEOUT]) + add_opt(subtree, buffer, 'txn_isolation', tbl[TXN_ISOLATION]) + add_opt(subtree, buffer, 'is_sync', tbl[IS_SYNC]) +end + +local function parse_commit(tbl, buffer, subtree) + add_opt(subtree, buffer, 'is_sync', tbl[IS_SYNC]) +end + +local function parse_insert_arrow(tbl, buffer, subtree) + local space = tbl[SPACE_NAME] or tbl[SPACE_ID] + subtree:add(buffer, 'space: ' .. tostring(space)) + subtree:add(buffer, 'arrow: ') +end + +local function parse_watch(tbl, buffer, subtree) + subtree:add(buffer, 'event key: ' .. tostring(tbl[EVENT_KEY])) + if tbl[EVENT_DATA] ~= nil then + subtree:add(buffer, 'event data: ' .. escape_call_arg(tbl[EVENT_DATA])) + end +end + +local function parse_synchro(tbl, buffer, subtree) + subtree:add(buffer, string.format( + 'replica_id: %s, lsn: %s, term: %s', + tostring(tbl[REPLICA_ID]), tostring(tbl[LSN]), tostring(tbl[TERM]))) +end + +-- Render a vclock ({replica_id = lsn}) as "{0 = 2, 1 = 9}", iterating sorted keys +-- (a plain table_kv_concat would print id 1 positionally as "{9, 0 = 2}"). +local function vclock_str(vclock) + if type(vclock) ~= 'table' then return tostring(vclock) end + local keys = {} + for k in pairs(vclock) do keys[#keys + 1] = k end + table.sort(keys) + local parts = {} + for _, k in ipairs(keys) do + parts[#parts + 1] = tostring(k) .. ' = ' .. escape_call_arg(vclock[k]) + end + return '{' .. table.concat(parts, ', ') .. '}' +end + +local function parse_subscribe(tbl, buffer, subtree) + subtree:add(buffer, 'instance_uuid: ' .. tostring(tbl[INSTANCE_UUID])) + subtree:add(buffer, 'replicaset_uuid: ' .. tostring(tbl[REPLICASET_UUID])) + if tbl[VCLOCK] ~= nil then + subtree:add(buffer, 'vclock: ' .. vclock_str(tbl[VCLOCK])) + end + add_opt(subtree, buffer, 'instance_name', tbl[INSTANCE_NAME]) + add_opt(subtree, buffer, 'replicaset_name', tbl[REPLICASET_NAME]) + add_opt(subtree, buffer, 'server_version', tbl[SERVER_VERSION]) + add_opt(subtree, buffer, 'replica_anon', tbl[REPLICA_ANON]) + if tbl[ID_FILTER] ~= nil then + subtree:add(buffer, 'id_filter: {' .. join_args(tbl[ID_FILTER]) .. '}') + end +end + +-- Shared by JOIN, FETCH_SNAPSHOT and REGISTER. +local function parse_join(tbl, buffer, subtree) + subtree:add(buffer, 'instance_uuid: ' .. tostring(tbl[INSTANCE_UUID])) + add_opt(subtree, buffer, 'instance_name', tbl[INSTANCE_NAME]) + add_opt(subtree, buffer, 'server_version', tbl[SERVER_VERSION]) + if tbl[VCLOCK] ~= nil then + subtree:add(buffer, 'vclock: ' .. vclock_str(tbl[VCLOCK])) + end +end + +-- IPROTO_ERROR (0x52) is a map { MP_ERROR_STACK: [ frame, ... ] }; each frame is +-- a map keyed by these field ids (src/box/mp_error.cc). +local MP_ERROR_STACK = 0x00 +local MP_ERROR_TYPE = 0x00 +local MP_ERROR_FILE = 0x01 +local MP_ERROR_LINE = 0x02 +local MP_ERROR_MESSAGE = 0x03 +local MP_ERROR_ERRNO = 0x04 +local MP_ERROR_CODE = 0x05 +local MP_ERROR_FIELDS = 0x06 + +-- Render the structured error stack as named fields instead of a raw map dump. +local function add_error_stack(subtree, buffer, err) + local stack = (type(err) == 'table') and err[MP_ERROR_STACK] or nil + if type(stack) ~= 'table' then + subtree:add(buffer, 'error: ' .. escape_call_arg(err)) -- unexpected shape + return + end + local node = subtree:add(buffer, 'error stack') + for i, frame in ipairs(stack) do + if type(frame) == 'table' then + local head = string.format('[%d] %s', i, + tostring(frame[MP_ERROR_TYPE] or '?')) + if frame[MP_ERROR_CODE] ~= nil then + head = head .. string.format(' (code %s)', + tostring(frame[MP_ERROR_CODE])) + end + if frame[MP_ERROR_MESSAGE] ~= nil then + head = head .. ': ' .. tostring(frame[MP_ERROR_MESSAGE]) + end + local fnode = node:add(buffer, head) + add_opt(fnode, buffer, 'errno', frame[MP_ERROR_ERRNO]) + if frame[MP_ERROR_FILE] ~= nil then + fnode:add(buffer, string.format('at %s:%s', + tostring(frame[MP_ERROR_FILE]), + tostring(frame[MP_ERROR_LINE]))) + end + if frame[MP_ERROR_FIELDS] ~= nil then + fnode:add(buffer, 'fields: ' .. escape_call_arg(frame[MP_ERROR_FIELDS])) + end + end + end +end + +local function parse_error_response(tbl, buffer, subtree) + if tbl == nil then + subtree:add(buffer, '(empty response body)') + return + end + if tbl[ERROR_24] ~= nil then + subtree:add(buffer, 'message: ' .. tostring(tbl[ERROR_24])) + end + if tbl[ERROR] ~= nil then + add_error_stack(subtree, buffer, tbl[ERROR]) + end + if tbl[ERROR_24] == nil and tbl[ERROR] == nil then + subtree:add(buffer, '(empty response body)') + end +end + +-- Responses carry no request type, so surface whichever known response keys are +-- present (data, SQL metadata, PREPARE info, cursor, ballot, ID/SUBSCRIBE). +local function parse_response(tbl, buffer, subtree) + if tbl == nil or next(tbl) == nil then + subtree:add(buffer, '(empty response body)') + return + end + + if tbl[METADATA] ~= nil then + local node = subtree:add(buffer, 'metadata') + for _, col in ipairs(tbl[METADATA]) do + node:add(buffer, tostring(col[FIELD_NAME]) .. ' : ' .. + tostring(col[FIELD_TYPE])) + end + end + if tbl[DATA] ~= nil then + local node = subtree:add(buffer, 'data') + if type(tbl[DATA]) == 'table' and getmetatable(tbl[DATA]) ~= ext_mt then + for _, v in ipairs(map(tbl[DATA], escape_call_arg)) do + node:add(buffer, v) + end + else + node:add(buffer, escape_call_arg(tbl[DATA])) -- bare scalar/ext payload + end + end + if tbl[SQL_INFO] ~= nil then + add_opt(subtree, buffer, 'sql row_count', tbl[SQL_INFO][SQL_INFO_ROW_COUNT]) + end + add_opt(subtree, buffer, 'position', tbl[POSITION]) + add_opt(subtree, buffer, 'stmt_id', tbl[STMT_ID]) + add_opt(subtree, buffer, 'bind_count', tbl[BIND_COUNT]) + add_opt(subtree, buffer, 'bind_metadata', tbl[BIND_METADATA]) + add_opt(subtree, buffer, 'version', tbl[VERSION]) + if tbl[FEATURES] ~= nil then + subtree:add(buffer, 'features: {' .. join_args(tbl[FEATURES]) .. '}') + end + add_opt(subtree, buffer, 'auth_type', tbl[AUTH_TYPE]) + add_opt(subtree, buffer, 'ballot', tbl[BALLOT]) + add_opt(subtree, buffer, 'replicaset_uuid', tbl[REPLICASET_UUID]) + if tbl[VCLOCK] ~= nil then + subtree:add(buffer, 'vclock: ' .. vclock_str(tbl[VCLOCK])) + end +end + +local function parse_nop(tbl, buffer, subtree) + subtree:add(buffer, 'NOP (No Operation)') +end + +local function parse_empty(tbl, buffer, subtree) + -- Body-less request (PING, VOTE, ROLLBACK, UNWATCH, ...) — nothing to show. +end + +local function parser_not_implemented(tbl, buffer, subtree) + subtree:add(buffer, 'parser not yet implemented') +end + +local UNKNOWN_COMMAND = {name = 'UNKNOWN', decoder = parser_not_implemented} + +local COMMANDS = { + [SELECT] = {name = 'select', decoder = parse_select}, + [INSERT] = {name = 'insert', decoder = parse_insert}, + [REPLACE] = {name = 'replace', decoder = parse_insert}, + [UPDATE] = {name = 'update', decoder = parse_update}, + [DELETE] = {name = 'delete', decoder = parse_delete}, + [CALL] = {name = 'call', decoder = parse_call}, + [CALL_16] = {name = 'call_16', decoder = parse_call}, + [AUTH] = {name = 'auth', decoder = parse_auth}, + [EVAL] = {name = 'eval', decoder = parse_eval}, + [UPSERT] = {name = 'upsert', decoder = parse_upsert}, + [EXECUTE] = {name = 'execute', decoder = parse_execute}, + [NOP] = {name = 'nop', decoder = parse_nop}, + [PREPARE] = {name = 'prepare', decoder = parse_prepare}, + [BEGIN] = {name = 'begin', decoder = parse_begin}, + [COMMIT] = {name = 'commit', decoder = parse_commit}, + [ROLLBACK] = {name = 'rollback', decoder = parse_empty}, + [INSERT_ARROW] = {name = 'insert_arrow', decoder = parse_insert_arrow}, + [ID] = {name = 'id', decoder = parse_id}, + [WATCH] = {name = 'watch', decoder = parse_watch}, + [UNWATCH] = {name = 'unwatch', decoder = parse_watch}, + [EVENT] = {name = 'event', decoder = parse_watch}, + [WATCH_ONCE] = {name = 'watch_once', decoder = parse_watch}, + [JOIN] = {name = 'join', decoder = parse_join}, + [JOIN_META] = {name = 'join_meta', decoder = parse_empty}, + [JOIN_SNAPSHOT] = {name = 'join_snapshot', decoder = parse_empty}, + [SUBSCRIBE] = {name = 'subscribe', decoder = parse_subscribe}, + [VOTE] = {name = 'vote', decoder = parse_empty}, + [VOTE_DEPRECATED] = {name = 'vote_deprecated', decoder = parse_empty}, + [FETCH_SNAPSHOT] = {name = 'fetch_snapshot', decoder = parse_join}, + [REGISTER] = {name = 'register', decoder = parse_join}, + [RAFT] = {name = 'raft', decoder = parser_not_implemented}, + [RAFT_PROMOTE] = {name = 'raft_promote', decoder = parse_synchro}, + [RAFT_DEMOTE] = {name = 'raft_demote', decoder = parse_synchro}, + [RAFT_CONFIRM] = {name = 'raft_confirm', decoder = parse_synchro}, + [RAFT_ROLLBACK] = {name = 'raft_rollback', decoder = parse_synchro}, + + -- Administrative commands. + [PING] = {name = 'ping', decoder = parse_empty}, + + -- Responses. + [OK] = {name = 'OK', is_response = true, decoder = parse_response}, + [CHUNK] = {name = 'CHUNK', is_response = true, decoder = parse_response}, +} + +local function code_to_command(code) + -- A corrupt header can decode TYPE to a non-number (string/array/map, or a + -- uint64 >= 2^63 that msgpack_ext wraps into an ext marker). This runs before + -- the rendering pcall, so guard the comparison rather than let it throw. + if type(code) ~= 'number' then + return UNKNOWN_COMMAND + end + if code >= TYPE_ERROR then + return {name = string.format('ERROR(0x%x)', code - TYPE_ERROR), + is_response = true, decoder = parse_error_response} + end + return COMMANDS[code] or UNKNOWN_COMMAND +end + +-- Dissect one modern PDU at `offset`; returns bytes consumed, nil (reassembly) +-- or false (not a modern PDU). +local function dissect_modern(tvb, pinfo, tree, offset) + local available = tvb:len() - offset + + -- Tarantool always frames the length as a 5-byte 0xce uint32. + if tvb(offset, 1):uint() ~= 0xce then + return false + end + local prefix_len = 5 + if available < prefix_len then + return need_more(pinfo, offset, DESEGMENT_ONE_MORE_SEGMENT) + end + + local _, packet_length = msgpack.unpacker(tvb:raw(offset, prefix_len))() + -- Reject an absurd length instead of asking TCP to reassemble ~4 GB. + if type(packet_length) ~= 'number' or packet_length < 0 + or packet_length > 0x10000000 then + return false + end + local total = prefix_len + packet_length + if available < total then + return need_more(pinfo, offset, total - available) + end + + -- Decode header + optional body; catch malformed inner MsgPack so one bad + -- PDU never aborts the whole segment (mirrors the legacy path's pcall). + local ok, header_data, body_start, body_data = pcall(function() + local iter = msgpack.unpacker(tvb:raw(offset, total)) + iter() -- skip the length prefix already decoded above + local _, hdr = iter() + local bstart, bdata = iter() + return hdr, bstart, bdata + end) + + if not ok or type(header_data) ~= 'table' then + local subtree = tree:add(tarantool_proto, tvb(offset, total), + "Tarantool PDU (undecodable)") + subtree:add(tvb(offset, total), 'malformed or truncated MsgPack') + return total -- consume it and carry on with the next PDU + end + + local command = code_to_command(header_data[TYPE] or 0) + local subtree = tree:add(tarantool_proto, tvb(offset, total), + command.is_response and "Tarantool response" or "Tarantool request") + + -- Guard the table indexing below so a non-conforming PDU renders a note + -- instead of throwing and aborting the rest of the segment. + local rendered = pcall(function() + local header_end = body_start and (body_start - 1) or total + local header_len = header_end - prefix_len + local body_off = offset + header_end + local body_len = total - header_end + local header_range = tvb(offset + prefix_len, header_len) + local voff = map_value_offsets(tvb:raw(offset + prefix_len, header_len), + offset + prefix_len) + + -- Add a header int, reading the exact wire value (see map_value_offsets); + -- nil `field` renders a "label: value" text node. + local function add_hdr_uint(field, label, key) + if header_data[key] == nil then return end + local value = exact_uint(tvb, voff[key]) or UInt64(header_data[key]) + if field ~= nil then + subtree:add(field, header_range, value) + else + subtree:add(header_range, label .. ': ' .. tostring(value)) + end + end + + subtree:add(pf_type, header_range, header_data[TYPE] or 0) + subtree:add(pf_request, header_range, command.name) + subtree:add(pf_is_resp, header_range, command.is_response and true or false) + add_hdr_uint(pf_sync, nil, SYNC) + add_hdr_uint(pf_schema, nil, SCHEMA_VERSION) + add_hdr_uint(pf_stream, nil, STREAM_ID) + -- Replication / transaction header fields (WAL rows, multi-stmt txns). + add_hdr_uint(nil, 'replica_id', REPLICA_ID) + add_hdr_uint(nil, 'lsn', LSN) + add_hdr_uint(nil, 'tsn', TSN) + add_hdr_uint(nil, 'flags', FLAGS) + add_opt(subtree, header_range, 'timestamp', header_data[TIMESTAMP]) + add_hdr_uint(nil, 'group_id', GROUP_ID) + add_hdr_uint(nil, 'thread_id', THREAD_ID) + add_hdr_uint(nil, 'vclock_sync', VCLOCK_SYNC) + + local body_range = (body_len > 0) and tvb(body_off, body_len) + or tvb(offset, total) + local decoder = command.decoder or parser_not_implemented + decoder(body_data or {}, body_range, subtree) -- empty table when body-less + end) + if not rendered then + subtree:add(tvb(offset, total), 'malformed or non-conforming body') + end + + pinfo.cols.info:append(command.name .. ' ') + return total +end + +return { dissect = dissect_modern } diff --git a/src/msgpack_ext.lua b/src/msgpack_ext.lua new file mode 100644 index 0000000..70cf8b9 --- /dev/null +++ b/src/msgpack_ext.lua @@ -0,0 +1,166 @@ +-- Tarantool MsgPack ext (MP_EXT) decoding layered onto the bundled +-- MessagePack.lua, which by default drops ext values and renders uint64 >= 2^63 +-- as negative. Exports the configured msgpack module and the `ext_mt` marker. +-- Used only by the modern decoder (legacy <= 1.5 is not MsgPack-based). + +local msgpack = require 'MessagePack' + +local M = {} + +-- MP_EXT type ids (src/lib/core/mp_extension_types.h). +local MP_EXT_NAME = { + [0] = 'unknown', [1] = 'decimal', [2] = 'uuid', [3] = 'error', + [4] = 'datetime', [5] = 'compression', [6] = 'interval', + [7] = 'tuple', [8] = 'arrow', +} + +-- Marks a pre-formatted ext value; escape_call_arg renders its .text verbatim. +local ext_mt = {} + +local function le_uint(s, from, len) + local v = 0 + for i = len, 1, -1 do v = v * 256 + s:byte(from + i - 1) end + return v +end + +local function be_uint(s, from, len) + local v = 0 + for i = 0, len - 1 do v = v * 256 + s:byte(from + i) end + return v +end + +-- Read one MsgPack integer at `from`; returns the value and the next index. +local function read_mp_int(s, from) + local b = s:byte(from) + if b <= 0x7f then return b, from + 1 + elseif b >= 0xe0 then return b - 0x100, from + 1 + elseif b == 0xcc then return s:byte(from + 1), from + 2 + elseif b == 0xcd then return be_uint(s, from + 1, 2), from + 3 + elseif b == 0xce then return be_uint(s, from + 1, 4), from + 5 + elseif b == 0xd0 then local v = s:byte(from + 1) + return v >= 0x80 and v - 0x100 or v, from + 2 + elseif b == 0xd1 then local v = be_uint(s, from + 1, 2) + return v >= 0x8000 and v - 0x10000 or v, from + 3 + elseif b == 0xd2 then local v = be_uint(s, from + 1, 4) + return v >= 0x80000000 and v - 0x100000000 or v, from + 5 + elseif b == 0xcf then return be_uint(s, from + 1, 8), from + 9 + elseif b == 0xd3 then return be_uint(s, from + 1, 8), from + 9 + end + return 0, from + 1 +end + +-- MP_UUID (fixext16): 16 bytes -> canonical UUID string. +local function decode_uuid(data) + local h = {} + for i = 1, 16 do h[i] = string.format('%02x', data:byte(i)) end + return table.concat(h, '', 1, 4) .. '-' .. table.concat(h, '', 5, 6) .. '-' + .. table.concat(h, '', 7, 8) .. '-' .. table.concat(h, '', 9, 10) .. '-' + .. table.concat(h, '', 11, 16) +end + +-- MP_DATETIME (fixext8/16): int64 LE seconds [+ nsec, tzoffset, tzindex]. +local function decode_datetime(data) + local secs = le_uint(data, 1, 8) + local nsec, tzoffset = 0, 0 + if #data >= 16 then + nsec = le_uint(data, 9, 4) + tzoffset = le_uint(data, 13, 2) + if tzoffset >= 0x8000 then tzoffset = tzoffset - 0x10000 end + end + -- Shift the UTC instant by the stored offset so the printed wall clock + -- matches the appended timezone. + local out = 'epoch=' .. string.format('%d', secs) + if os and os.date then + local ok, formatted = pcall(os.date, '!%Y-%m-%dT%H:%M:%S', secs + tzoffset * 60) + if ok and formatted then out = formatted end + end + if nsec > 0 then + local frac = string.format('%09d', nsec):gsub('0+$', '') + out = out .. '.' .. frac + end + if tzoffset ~= 0 then + local m = math.abs(tzoffset) + out = out .. string.format('%s%02d:%02d', tzoffset < 0 and '-' or '+', + math.floor(m / 60), m % 60) + else + out = out .. 'Z' + end + return out +end + +-- MP_DECIMAL: MsgPack scale (-exponent) followed by packed-BCD coefficient. +local function decode_decimal(data) + local scale, pos = read_mp_int(data, 1) + local digits, sign, last = {}, '', #data + for i = pos, last do + local byte = data:byte(i) + digits[#digits + 1] = math.floor(byte / 16) + if i < last then + digits[#digits + 1] = byte % 16 + else + local nibble = byte % 16 -- last low nibble is the sign + sign = (nibble == 0x0b or nibble == 0x0d) and '-' or '' + end + end + local s = table.concat(digits):gsub('^0+(%d)', '%1') + if scale > 0 then + if #s <= scale then s = string.rep('0', scale - #s + 1) .. s end + s = s:sub(1, #s - scale) .. '.' .. s:sub(#s - scale + 1) + elseif scale < 0 then + s = s .. string.rep('0', -scale) + end + return sign .. s +end + +local INTERVAL_FIELD = { + [0] = 'year', [1] = 'month', [2] = 'week', [3] = 'day', [4] = 'hour', + [5] = 'min', [6] = 'sec', [7] = 'nsec', [8] = 'adjust', +} + +-- MP_INTERVAL: u8 count, then count (u8 field_id, MsgPack value) pairs. +local function decode_interval(data) + local count, pos = data:byte(1), 2 + local parts = {} + for _ = 1, count do + local fid = data:byte(pos) + local val + val, pos = read_mp_int(data, pos + 1) + parts[#parts + 1] = (INTERVAL_FIELD[fid] or ('f' .. fid)) .. '=' .. val + end + return '{' .. table.concat(parts, ', ') .. '}' +end + +local EXT_DECODER = { + [1] = decode_decimal, [2] = decode_uuid, + [4] = decode_datetime, [6] = decode_interval, +} + +-- Decode known scalar ext types; render opaque ones as a labelled blob. +function msgpack.build_ext(tag, data) + local decoder = EXT_DECODER[tag] + if decoder then + local ok, result = pcall(decoder, data) + if ok and result ~= nil then + return setmetatable({text = result}, ext_mt) + end + end + return setmetatable({text = string.format('<%s ext, %d byte%s>', + MP_EXT_NAME[tag] or ('type ' .. tag), #data, + #data == 1 and '' or 's')}, ext_mt) +end + +-- MessagePack.lua's uint64 decode wraps to a signed Lua integer, so values +-- >= 2^63 come back negative; re-render those as unsigned. int64 (genuinely +-- signed) and non-negative values pass through. Headers use exact_uint instead. +local raw_uint64 = msgpack.unpackers['uint64'] +msgpack.unpackers['uint64'] = function(c) + local n = raw_uint64(c) + if type(n) == 'number' and n < 0 then + return setmetatable({text = string.format('%u', n)}, ext_mt) + end + return n +end + +M.msgpack = msgpack +M.ext_mt = ext_mt +return M diff --git a/tarantool.dissector.lua b/tarantool.dissector.lua deleted file mode 100644 index fd810f2..0000000 --- a/tarantool.dissector.lua +++ /dev/null @@ -1,496 +0,0 @@ - -local msgpack = require 'MessagePack' - --- constants --- common -local GREETING_SIZE = 128 -local GREETING_SALT_OFFSET = 64 -local GREETING_SALT_SIZE = 44 - --- packet codes -local OK = 0x00 -local SELECT = 0x01 -local INSERT = 0x02 -local REPLACE = 0x03 -local UPDATE = 0x04 -local DELETE = 0x05 -local CALL_16 = 0x06 -local AUTH = 0x07 -local EVAL = 0x08 -local UPSERT = 0x09 -local CALL = 0x0a -local EXECUTE = 0x0b -local NOP = 0x0c -local PREPARE = 0x0d -local CONFIRM = 0x28 -local ROLLBACK = 0x29 -local PING = 0x40 -local JOIN = 0x41 -local SUBSCRIBE = 0x42 -local VOTE_DEPRECATED = 0x43 -local VOTE = 0x44 -local FETCH_SNAPSHOT = 0x45 -local REGISTER = 0x46 - --- packet keys -local REQUEST_TYPE = 0x00 -local TYPE = 0x00 -local SYNC = 0x01 -local REPLICA_ID = 0x02 -local LSN = 0x03 -local TIMESTAMP = 0x04 -local SCHEMA_VERSION = 0x05 -local FLAGS = 0x09 -local SPACE_ID = 0x10 -local INDEX_ID = 0x11 -local LIMIT = 0x12 -local OFFSET = 0x13 -local ITERATOR = 0x14 -local INDEX_BASE = 0x15 -local KEY = 0x20 -local TUPLE = 0x21 -local FUNCTION_NAME = 0x22 -local USER_NAME = 0x23 -local INSTANCE_UUID = 0x24 -local CLUSTER_UUID = 0x25 -local VCLOCK = 0x26 -local EXPRESSION = 0x27 -local OPS = 0x28 -local BALLOT = 0x29 -local DATA = 0x30 -local ERROR = 0x31 - -local BALLOT_IS_RO_CFG = 0x01 -local BALLOT_VCLOCK = 0x02 -local BALLOT_GC_VCLOCK = 0x03 -local BALLOT_IS_RO = 0x04 -local BALLOT_IS_ANON = 0x05 -local BALLOT_IS_BOOTED = 0x06 -local TUPLE_META = 0x2a -local OPTIONS = 0x2b -local ERROR_24 = 0x31 -local METADATA = 0x32 -local BIND_METADATA = 0x33 -local BIND_COUNT = 0x34 -local SQL_TEXT = 0x40 -local SQL_BIND = 0x41 -local SQL_INFO = 0x42 -local STMT_ID = 0x43 -local ERROR = 0x52 -local FIELD_NAME = 0x00 -local FIELD_TYPE = 0x01 -local FIELD_COLL = 0x02 -local FIELD_IS_NULLABLE = 0x03 -local FIELD_IS_AUTOINCREMENT = 0x04 -local FIELD_SPAN = 0x05 - --- declare the protocol -tarantool_proto = Proto("tarantool","Tarantool") ---[[ -local tnt_field_sync = ProtoField.new('tnt.sync', 'tnt.sync', ftypes.UINT32) - -tarantool_proto.fields = { - tnt_field_sync -} -]] - --- extracts bytes from the buffer -function binary_string(buffer) - local result = {} - for i=0,buffer:len() - 1 do - table.insert(result, string.char(buffer(i, 1):le_uint())) - end - return table.concat(result, '') -end - - -local function map(tbl, callback) - local result = {} - for k,v in pairs(tbl) do - result[k] = callback(v) - end - return result -end - -local function table_kv_concat(tbl, sep) - local result = {} - local used_keys = {} - for i, v in ipairs(tbl) do - used_keys[i] = true - table.insert(result, v) - end - for k, v in pairs(tbl) do - if not used_keys[k] then - table.insert(result, k .. ' = ' .. v) - end - end - return table.concat(result, sep) -end - -local function escape_call_arg(a) - if type(a) == 'number' then - return a - elseif type(a) == 'string' then - return '"' .. a .. '"' - elseif type(a) == 'table' then - return '{' .. table_kv_concat(map(a, escape_call_arg), ', ') .. '}' - else - return a - end -end - -local function parse_call(tbl, buffer, subtree) - local name = tbl[FUNCTION_NAME] - local tuple = tbl[TUPLE] - - local argument_string = table.concat(map(tuple, escape_call_arg), ', ') - - local descr = string.format('%s(%s)', name, argument_string) - subtree:add(buffer, descr) -end - --- TODO: why do we need "tuple" in `eval' command? -local function parse_eval(tbl, buffer, subtree) - local expression = tbl[EXPRESSION] - local tuple = tbl[TUPLE] - - local argument_string = table.concat(map(tuple, escape_call_arg), ', ') - - local descr = string.format('%s(%s)', name, argument_string) - subtree:add(buffer, descr) -end - -local function parse_select(tbl, buffer, subtree) - local space_id = tbl[SPACE_ID] -- int - local index_id = tbl[INDEX_ID] -- int - local limit = tbl[LIMIT] -- int - local offset = tbl[OFFSET] -- int - local iterator = tbl[ITERATOR] -- int - local key = tbl[KEY] -- array - - local key_string = table.concat(map(key, escape_call_arg), ', ') - - local descr = string.format( - 'SELECT FROM space %d WHERE index(%d) = (%s) LIMIT %d OFFSET %d ITERATOR %s', - space_id, - index_id, - key_string, - limit, - offset, - iterator or ('null') - ) - subtree:add(buffer, descr) -end - -local function parse_insert(tbl, buffer, subtree) - local tuple = tbl[TUPLE] - local space_id = tbl[SPACE_ID] - - subtree:add(buffer, 'space_id: ' .. space_id) - local tuple_tree = subtree:add(buffer, 'tuple') - local tuple_str = table.concat(map(tuple, escape_call_arg), ', ') - - tuple_tree:add(buffer, tuple_str) -end - -local function parse_delete(tbl, buffer, subtree) - local key = tbl[KEY] - local space_id = tbl[SPACE_ID] - local index_id = tbl[INDEX_ID] - - local key_string = table.concat(map(key, escape_call_arg), ', ') - - local descr = string.format( - 'DELETE FROM space(%d) WHERE index(%d) = (%s)', - space_id, - index_id, - key_string - ) - subtree:add(buffer, descr) -end - -local function parse_upsert(tbl, buffer, subtree) - local space_id = tbl[SPACE_ID] -- int - local index_base = tbl[INDEX_BASE] -- int - local ops = tbl[OPS] -- int - local tuple = tbl[TUPLE] -- array - - subtree:add(buffer, 'space_id: ' .. space_id) - local tuple_tree = subtree:add(buffer, 'tuple') - local tuple_str = table.concat(map(tuple, escape_call_arg), ', ') - - tuple_tree:add(buffer, tuple_str) -end - -local function parse_auth(tbl, buffer, subtree) - local user_name = tbl[USER_NAME] -- str - local tuple = tbl[TUPLE] -- array - - -- chap-sha1 is the only supported mechanism (v. 2.10). - local proto = tuple[1] - local scramble = tuple[2] - - local descr = string.format( - 'Authentication with username "%s", protocol %s and scramble "%s"', - user_name, - proto, - scramble - ) - subtree:add(buffer, descr) -end - -local function parse_update(tbl, buffer, subtree) - local space_id = tbl[SPACE_ID] -- int - local index_id = tbl[INDEX_ID] -- int - local key = tbl[KEY] -- array - local tuple = tbl[TUPLE] -- array - - subtree:add(buffer, 'space_id: ' .. space_id) - local tuple_tree = subtree:add(buffer, 'tuple') - local tuple_str = table.concat(map(tuple, escape_call_arg), ', ') - - tuple_tree:add(buffer, tuple_str) - local key_string = table.concat(map(key, escape_call_arg), ', ') - subtree:add(buffer, 'key: ' .. key_string) -end - -local function parse_execute(tbl, buffer, subtree) - local stmt_id = tbl[STMT_ID] -- int - local sql_text = tbl[SQL_TEXT] -- str - local sql_bind = tbl[SQL_BIND] -- array - - local sql_bind_str = table.concat(map(sql_bind, escape_call_arg), ', ') - if sql_bind_str ~= '' then - sql_bind_str = string.format(', with parameter values "%s"', sql_bind_str) - end - - if stmt_id ~= nil then - local descr = string.format( - 'executing a prepared statement with id %d%s', - stmt_id, - sql_bind_str - ) - subtree:add(buffer, descr) - else - local descr = string.format( - 'executing an SQL string "%s"%s', - sql_text, - sql_bind_str - ) - subtree:add(buffer, descr) - end -end - -local function parse_prepare(tbl, buffer, subtree) - local stmt_id = tbl[STMT_ID] -- int - local sql_text = tbl[SQL_TEXT] -- str - - if stmt_id ~= nil then - local descr = string.format( - 'prepare a statement with id %d', - stmt_id - ) - subtree:add(buffer, descr) - else - local descr = string.format( - 'preparing an SQL string "%s"', - sql_text - ) - subtree:add(buffer, descr) - end -end - -local function parse_confirm(tbl, buffer, subtree) - local replica_id = tbl[REPLICA_ID] -- int - local lsn = tbl[LSN] -- int - - local descr = string.format( - [[transactions originated from the instance with id = "%d" - have achieved quorum and can be committed, up to and including lsn "%d".]], - replica_id, - lsn - ) - subtree:add(buffer, descr) -end - -local function parse_rollback(tbl, buffer, subtree) - local replica_id = tbl[REPLICA_ID] -- int - local lsn = tbl[LSN] -- int - - local descr = string.format( - [[transactions originated from the instance with id = "%d" - couldn't achieve quorum for some reason and should be rolled back, - down to lsn = "%d" and including it.]], - replica_id, - lsn - ) - subtree:add(buffer, descr) -end - -local function parse_subscribe(tbl, buffer, subtree) - local vclock = tbl[VCLOCK] - - local srv_id = vclock[1] - local srv_lsn = vclock[2] - - local descr = string.format( - 'Subscribe to server with id "%d" and lsn "%d"', - srv_id, - srv_lsn - ) - subtree:add(buffer, descr) -end - -local function parse_join(tbl, buffer, subtree) - local uuid = tbl[INSTANCE_UUID] - - local descr = string.format( - 'Initial join request with uuid = "%s"', - uuid - ) - subtree:add(buffer, descr) -end - -local function parse_error_response(tbl, buffer, subtree) - local data = tbl[ERROR] - if not data then - subtree:add(buffer, '(empty response body)') - else - subtree:add(buffer, data) - end -end - -local function parse_response(tbl, buffer, subtree) - local data = tbl[DATA] - if not data then - subtree:add(buffer, '(empty response body)') - else - local value = map(data, escape_call_arg) - local arguments_tree = subtree:add(buffer, 'tuple') - for k, v in pairs(value) do - arguments_tree:add(buffer, v) - end - end -end - -local function parse_nop(tbl, buffer, subtree) - subtree:add(buffer, 'NOP (No Operation') -end - -local function parser_not_implemented(tbl, buffer, subtree) - subtree:add(buffer, 'parser not yet implemented (or unknown packet?)') -end - -local function code_to_command(code) - - local codes = { - [SELECT] = {name = 'select', decoder = parse_select}, - [INSERT] = {name = 'insert', decoder = parse_insert}, - [REPLACE] = {name = 'replace', decoder = parse_insert}, - [UPDATE] = {name = 'update', decoder = parse_update}, - [DELETE] = {name = 'delete', decoder = parse_delete}, - [CALL] = {name = 'call', decoder = parse_call}, - [CALL_16] = {name = 'call_16', decoder = parser_not_implemented}, -- Deprecated. - [AUTH] = {name = 'auth', decoder = parse_auth}, - [EVAL] = {name = 'eval', decoder = parse_eval}, - [UPSERT] = {name = 'upsert', decoder = parser_upsert}, - [EXECUTE] = {name = 'execute', decoder = parse_execute}, - [NOP] = {name = 'nop', decoder = parse_nop}, - [PREPARE] = {name = 'prepare', decoder = parse_prepare}, - [CONFIRM] = {name = 'confirm', decoder = parse_confirm}, - [ROLLBACK] = {name = 'rollback', decoder = parse_rollback}, - [JOIN] = {name = 'join', decoder = parse_join}, - [VOTE] = {name = 'vote', decoder = parser_not_implemented}, - [VOTE_DEPRECATED] = {name = 'vote_deprecated', decoder = parser_not_implemented}, - [SUBSCRIBE] = {name = 'subscribe', decoder = parse_subscribe}, - [FETCH_SNAPSHOT] = {name = 'fetch_snapshot', decoder = parser_not_implemented}, - [REGISTER] = {name = 'register', decoder = parser_not_implemented}, - - -- Admin command codes - [PING] = {name = 'ping', decoder = parser_not_implemented}, - - -- Value for key in response can be: - [OK] = {name = 'OK', is_response = true, decoder = parse_response}, - --[0x8XXX] = {name = 'ERROR', is_response = true}, - }; - if code >= 0x8000 then - return {name = 'ERROR', is_response = true, decoder = parse_error_response} - end - - local unknown_code = {name = 'UNKNOWN', decoder = parser_not_implemented} - - return (codes[code] or unknown_code) -end - - --- create a function to dissect it -function tarantool_proto.dissector(buffer, pinfo, tree) - pinfo.cols.protocol = "Tarantool" - - if buffer(0, 9):string() == "Tarantool" then - pinfo.cols.info = 'Greeting packet. ' .. tostring(pinfo.cols.info) - - local subtree = tree:add(tarantool_proto, buffer(),"Tarantool greeting packet") - subtree:add(buffer(0, 64), "Server version: " .. buffer(0, 64):string()) - subtree:add(buffer(64, 44), "Salt: " .. buffer(64, 44):string()) - subtree:add(buffer(108), "Reserved space") - return buffer(0, 9):len() - end - - local iterator = msgpack.unpacker(binary_string(buffer)) - local _, packet_length = iterator() - - -- TODO: check bytes available - - local size_length, header_data = iterator() - size_length = size_length - 1; - - local packet_buffer = buffer(size_length) - - local request_length = packet_length + size_length - - if (buffer:len() < request_length) then - -- debug('reassemble required: ' .. (request_length - buffer:len()) ) - pinfo.desegment_len = request_length - buffer:len() - pinfo.desegment_offset = 0 - return DESEGMENT_ONE_MORE_SEGMENT - end - - local command = code_to_command(header_data[TYPE]) - - local header_length, body_data = iterator() - header_length = header_length - size_length - 1 - local body_buffer = packet_buffer(size_length + header_length) - - - if not command.is_response then - local subtree = tree:add(tarantool_proto, buffer(),"Tarantool protocol data") - -- subtree:add(tnt_field_sync, header_data[0x01]) - local header_descr = string.format('code: 0x%02x (%s), sync: 0x%04x', header_data[TYPE], command.name, header_data[SYNC]) - subtree:add(packet_buffer(0, header_length), header_descr) - - local decoder = command.decoder or parser_not_implemented - - decoder(body_data, body_buffer, subtree) - - pinfo.cols.info = command.name:gsub("^%l", string.upper) .. ' request. ' .. tostring(pinfo.cols.info) - --[[print(body_data, bytes_used) - for k,v in pairs(body_data) do - print(k,v) - end]] - -- subtree:add( buffer(0,4),"Request Type: " .. buffer(0,4):le_uint() .. ' ' .. requestName(buffer(0,4):le_uint()) ) - -- request(buffer, subtree) - else - local subtree = tree:add(tarantool_proto,buffer(),"Tarantool protocol data (response)") - local header_descr = string.format('code: 0x%02x (%s), sync: 0x%04x', header_data[TYPE], command.name, header_data[SYNC]) - subtree:add(packet_buffer(0, header_length), header_descr) - command.decoder(body_data, body_buffer, subtree) - pinfo.cols.info = 'Response. ' .. tostring(pinfo.cols.info) - end - - return request_length - -end - -tcp_table = DissectorTable.get("tcp.port") -tcp_table:add(3301,tarantool_proto) diff --git a/tarantool15.dissector.lua b/tarantool15.dissector.lua deleted file mode 100644 index 691814b..0000000 --- a/tarantool15.dissector.lua +++ /dev/null @@ -1,382 +0,0 @@ --- declare the protocol -tarantool_proto = Proto("tarantool","Tarantool") - -function leb128Unpack(buffer, offset) --- see http://en.wikipedia.org/wiki/LEB128#Decode_unsigned_integer - debug('-- leb128Unpack --') - local result = 0 - local shift = 0 - local used = 1 - - while true do - - local byte = buffer(offset, 1):le_uint(); - debug('byte: ' .. byte .. ' ' .. string.format('%04X', byte)) - local bit7 = buffer(offset, 1):bitfield(0, 1) - offset = offset + 1 - - local tmp = (bit7 == 0) and byte or (byte - 128) -- reset 7th bit (byte & 0x80) - - result = result * 128 + tmp -- result |= (low order 7 bits of byte << shift); - - if ( bit7 == 0) then - break - end - shift = shift + 7 - used = used + 1 - end - - return result, used -end - - - -function add_one_tuple(buffer, subtree, num) - debug('-- add_one_tuple --') - --[[ - ::= + - ::= - ::= - ::= + - ::= + - ]] - local data_length = 4 -- for cardinality - local cardinality = buffer(0,4):le_uint() - - local array = {} - - for i=1,cardinality do - debug('offset:'.. data_length) - local field_length, used = leb128Unpack(buffer, data_length) - debug('f,u:'.. field_length .. ' '..used) - array[i] = { - ['start'] = data_length + used, - ['length'] = field_length, - ['title'] = "Data (length: " .. field_length .. ')' - } - - data_length = data_length + field_length + used - end - - local tree = subtree:add( tarantool_proto, buffer(0, data_length),"Tuple #" .. num .. " (cardinality: "..cardinality..')') - for i,v in ipairs(array) do - tree:add(buffer(v.start, v.length), v.title) - end - - return data_length - -end - -function add_tuples(buffer, subtree, name, count) - -- local count = count_buffer(0,4):le_uint() - local tuples = subtree:add( tarantool_proto, buffer(), "Tuples") - - -- tuples:add( count_buffer(0,4), "Count: " .. count ) - - local offset = 0 - for i=1,count do - offset = offset + add_one_tuple( buffer(offset), tuples, i ) - end - -end - -function add_fqtuple(buffer, subtree, name, count) - - local tuples = subtree:add( tarantool_proto, buffer(), "fq_tuples (count: " .. count ..')' ) - - local offset = 0 - for i=1,count do - local size = buffer(0,4):le_uint() - tuples:add( buffer(offset,4), 'tuple size: ' .. size ) - offset = offset + add_one_tuple( buffer(offset + 4), tuples, i ) - offset = offset + 4 - end - -end - - -function select_request_body(buffer, subtree) - --[[ - ::= - + - ]] - - local tree = subtree:add( tarantool_proto, buffer(),"Select body") - - local namespace_no = buffer(0,4):le_uint() - local index_no = buffer(4, 4):le_uint() - local offset = buffer(8, 4):le_uint() - local limit = buffer(12,4):le_uint() - local count = buffer(16,4):le_uint() - if (limit == 4294967295) then - limit = limit .. ' (no limit)' - end - tree:add( buffer(0, 4), "Namespace # " .. namespace_no ) - tree:add( buffer(4, 4), "Index # " .. index_no ) - tree:add( buffer(8, 4), "Offset # " .. offset ) - tree:add( buffer(12,4), "Limit # " .. limit ) - - tree:add( buffer(16,4), "Tuples count: " .. count ) - add_tuples(buffer(20, buffer:len() - 20), tree, 'tuple', count) - -end - -function requestName(reqid) - local requests = { - [13] = "INSERT", - [17] = "SELECT", - [19] = "UPDATE", - [20] = "DELETE(obsolete)", - [21] = "DELETE", - [22] = "CALL", - [65280] = "PING", - } - return requests[reqid] or 'UNKNOWN' -end - - - -function decodeErrorCode(buf) - local completion_status = buf(0,1):le_uint() - local error_code = buf(1):le_uint() - - local result = "Code " .. completion_status - - if ( completion_status == 0 ) then - return "0 (Ok)" - elseif ( completion_status == 1 ) then -- try again - return "1 (Try again) " .. 'code: ' .. error_code - elseif ( completion_status == 2 ) then - return "2 (Error)" - else - return completion_status .. " (Unknown error code) " .. ' code: ' .. error_code - end -end - -function insert_request_body(buffer, subtree) - --[[ - ::= - ]] - local tree = subtree:add( tarantool_proto, buffer(),"Insert body") - - local namespace_no = buffer(0,4):le_uint() - local flags = buffer(4, 4):le_uint() - tree:add( buffer(0, 4), "Namespace # " .. namespace_no ) - tree:add( buffer(4, 4), "Flags # " .. flags ) - - add_one_tuple(buffer(8), tree, 0) - -end - -function update_request_body(buffer, subtree) - subtree:add( buffer,"Update data" ) -end - -function deletev13_request_body(buffer, subtree) - --[[ - ::= - ]] - local tree = subtree:add( tarantool_proto, buffer(),"Delete body (v1.3)") - - local namespace_no = buffer(0,4):le_uint() - tree:add( buffer(0, 4), "Namespace # " .. namespace_no ) - - add_one_tuple(buffer(4), tree, 1) - -end -function delete_request_body(buffer, subtree) - subtree:add( buffer,"Delete data" ) -end -function call_request_body(buffer, subtree) - --[[ - ::= - ]] - local tree = subtree:add( tarantool_proto, buffer,"Call data" ) - - local flags = buffer(0,4):le_uint() - tree:add( buffer(0, 4), "Namespace # " .. flags ) - - local field_length, used = leb128Unpack(buffer, 4) - local name = buffer(5, field_length):string() - tree:add( buffer(5, field_length), "name " .. name ) - - add_one_tuple(buffer(4 + 1 + field_length), tree, 0) -end - - -function ping_request_body(buffer, subtree) - subtree:add( buffer,"ping data" ) -end -function unknown_request_body(buffer, subtree) - subtree:add( buffer,"Unknown command data" ) -end -function unknown_response_body(buffer, subtree) - subtree:add( buffer,"Unknown response data" ) -end - -function insert_reponse_body(buffer, subtree) - --[[ - ::= | - ]] - local tree = subtree:add( tarantool_proto, buffer(),"Insert response") - local count = buffer(0,4):le_uint() - tree:add( buffer(0, 4), "Affected rows " .. count ) - - if ( buffer:len() > 4 ) then - -- subtree:add( buffer(4),"Insert response data" ) - add_fqtuple( buffer(4), subtree, "Select tuples", count) - end -end -function select_reponse_body(buffer, subtree) - --[[ - ::= * - ]] - local tree = subtree:add( tarantool_proto, buffer(),"Select response") - local count = buffer(0,4):le_uint() - tree:add( buffer(0, 4), "Count: " .. count ) - - if ( buffer:len() > 4 ) then - add_fqtuple( buffer(4), subtree, "Select tuples", count) - end -end -function call_reponse_body(buffer, subtree) - --[[ - ::= - ]] - local tree = subtree:add( tarantool_proto, buffer(),"Call response") - local count = buffer(0,4):le_uint() - tree:add( buffer(0, 4), "Count: " .. count ) - - if ( buffer:len() > 4 ) then - add_fqtuple( buffer(4), subtree, "Call tuples", count) - end -end -function requestfunction(reqid) - local requests = { - [13] = insert_request_body, - [17] = select_request_body, - [19] = update_request_body, - [20] = deletev13_request_body, -- old delete - [21] = delete_request_body, - [22] = call_request_body, - [65280] = ping_request_body, - } - if (requests[reqid] == nil) then - return unknown_request_body - else - return requests[reqid] - end - -end - -function responsefunction(reqid) - local requests = { - [13] = insert_response_body, - [17] = select_reponse_body, - [19] = unknown_response_body, - [20] = unknown_response_body, -- old delete - [21] = unknown_response_body, - [22] = call_reponse_body, - [65280] = unknown_response_body, - } - if (requests[reqid] == nil) then - return unknown_request_body - else - return requests[reqid] - end - -end - -function readHeader(buffer, subtree) - --[[ -
::= - ]] - local req_type = buffer(0,4):le_uint() - local length = buffer(4,4):le_uint() - local req_id = buffer(8,4):le_uint() - - local header = subtree:add( tarantool_proto, buffer(),"Header") - header:add( buffer(0,4),"Request Type: " .. req_type .. ' (' .. requestName(req_type) .. ')' ) - header:add( buffer(4,4),"Body length: " .. length ) - header:add( buffer(8,4),"Request ID: " .. req_id .. '[' .. string.format('%08X', req_id) .. ']' ) - - return buffer(12, buffer:len() - 12) -end - -function request(buffer, subtree) - --[[ - ::=
- ]] - - local req_type = buffer(0,4):le_uint() - - buffer = readHeader(buffer, subtree) - - local requestfunction = requestfunction(req_type) - requestfunction(buffer, subtree) -end - -function response(buffer, subtree) - --[[ - ::=
{ - ]] - local req_type = buffer(0,4):le_uint() - - buffer = readHeader(buffer, subtree) - if ( buffer:len() > 0 ) then - - local code = buffer(0,4):le_uint() - if (code == 0) then - subtree:add( buffer(0,4),"Return code: " .. decodeErrorCode(buffer(0,4)) ) - - local requestfunction = responsefunction(req_type) - - -- subtree:add( buffer(4),"Data" ) - requestfunction(buffer(4), subtree) - else - subtree:add( buffer(0,4),"Return code: " .. decodeErrorCode(buffer(0,4)) ) - end - end - -end - --- create a function to dissect it -function tarantool_proto.dissector(buffer, pinfo, tree) - pinfo.cols.protocol = "TARANTOOL" - - local body_length = buffer(4,4):le_uint() - local request_length = body_length + 12 -- 12 - header length - - if (pinfo.src_port == 33013) then - -- answer, should have a response code - -- request_length = request_length + 4 - end - - -- debug('buffer: ' .. buffer:len()) - -- debug('length: ' .. body_length) - if (buffer:len() < request_length) then - -- debug('reassemble required: ' .. (request_length - buffer:len()) ) - pinfo.desegment_len = request_length - buffer:len() - pinfo.desegment_offset = 0 - return 0 - end - if (pinfo.src_port ~= 33013) then - -- debug('parsing') - local subtree = tree:add(tarantool_proto,buffer(),"Tarantool protocol data") - - -- subtree:add( buffer(0,4),"Request Type: " .. buffer(0,4):le_uint() .. ' ' .. requestName(buffer(0,4):le_uint()) ) - request(buffer, subtree) - else - local subtree = tree:add(tarantool_proto,buffer(),"Tarantool protocol data (response)") - response(buffer, subtree) - end - - return request_length - -end - --- load the udp.port table -tcp_table = DissectorTable.get("tcp.port") --- register our protocol to handle udp port 7777 -tcp_table:add(33013,tarantool_proto) - diff --git a/test.lua b/test.lua index 1d034ff..8cbd126 100644 --- a/test.lua +++ b/test.lua @@ -4,10 +4,16 @@ -- IProto network packets for testing Wireshark dissector. -- -- IProto protocol description: --- https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/ +-- https://www.tarantool.io/en/doc/latest/reference/internals/box_protocol/ -- -- How to run: tarantool test.lua +-- Only run under a Tarantool runtime; loading this file from plain Lua tooling +-- (linters, require) is a no-op. +if _TARANTOOL == nil then + return +end + local netbox = require('net.box') local popen = require('popen') local fiber = require('fiber') @@ -18,8 +24,8 @@ local space_name = 'testspace' local test_dir = './test.data' local test_dir_replica = './test.data/replica' +-- Removing test_dir also drops the replica's data in test_dir_replica. os.execute('rm -rf ' .. test_dir) -os.execute('rm -rf replica') os.execute('mkdir ' .. test_dir) box.cfg{ @@ -29,6 +35,9 @@ box.cfg{ replication = 'replicator:password@localhost:3301', work_dir = test_dir, replication_synchro_quorum = 1, + -- Required for interactive (stream) transactions that yield, e.g. when + -- committing into a synchronous space. + memtx_use_mvcc_engine = true, } local s = box.schema.space.create(space_name, { @@ -111,6 +120,41 @@ conn:call('f1') conn:eval('function f2(x, y) return x, y end;') conn:call('f2', {1, 'B'}) +-- SQL: exercises IPROTO_EXECUTE (0x0b) and IPROTO_PREPARE (0x0d). +conn:execute([[SELECT 1 AS a, 'two' AS b]]) +local stmt = conn:prepare([[SELECT ? + ?]]) +conn:execute(stmt.stmt_id, {2, 3}) +conn:unprepare(stmt.stmt_id) + +-- Event watchers: exercises IPROTO_WATCH (0x4a), UNWATCH (0x4b), +-- EVENT (0x4c) and WATCH_ONCE (0x4d). +box.broadcast('test_event', 42) +local watcher = conn:watch('test_event', function(key, value) end) +fiber.sleep(0.1) +watcher:unregister() +conn:watch_once('test_event') + +-- Interactive transaction over a stream: exercises IPROTO_BEGIN (0x0e), +-- IPROTO_COMMIT (0x0f), IPROTO_ROLLBACK (0x10) and the IPROTO_STREAM_ID header. +local stream = conn:new_stream() +local stream_space = stream.space[space_name] + +stream:begin() +stream_space:insert({100, 'committed'}) +stream:commit() + +stream:begin() +stream_space:insert({101, 'rolled-back'}) +stream:rollback() + +-- Tuples carrying MsgPack extension types (MP_DECIMAL, MP_UUID, MP_DATETIME, +-- MP_INTERVAL) so the dissector's ext decoders are exercised over the wire. +local decimal = require('decimal') +local uuid = require('uuid') +local datetime = require('datetime') +space:replace({200, decimal.new('3.14'), uuid.new(), datetime.now()}) +space:replace({201, datetime.now() - datetime.new({year = 1})}) + conn:close() -- Teardown replica. diff --git a/tests/pcap/tarantool-1.10.pcap b/tests/pcap/tarantool-1.10.pcap new file mode 100644 index 0000000000000000000000000000000000000000..fe6bc5adc46ea9d32f86714c8500b1ce6e5e73a7 GIT binary patch literal 8701 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 DA&`4y literal 0 HcmV?d00001 diff --git a/tests/pcap/tarantool-2.11.pcap b/tests/pcap/tarantool-2.11.pcap new file mode 100644 index 0000000000000000000000000000000000000000..adef1b434c8b0d99e6957d66a7df463178b2b84c GIT binary patch literal 49306 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 literal 0 HcmV?d00001 diff --git a/tests/pcap/tarantool-3.x.pcap b/tests/pcap/tarantool-3.x.pcap new file mode 100644 index 0000000000000000000000000000000000000000..646072cbc56c332d1d7d8f29cec546e33f075c98 GIT binary patch literal 50724 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

XBW_)Laic7RNpZ}(~2@1G8zvn@I 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 F{uf8GnTY@Z literal 0 HcmV?d00001 diff --git a/tests/pcap/tarantool-replication-sync.pcap b/tests/pcap/tarantool-replication-sync.pcap new file mode 100644 index 0000000000000000000000000000000000000000..5ca910900b8f23aed303432d7ee9757a33846df6 GIT binary patch literal 90864 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 aeU<$Fd!K!9-w?Qe0zZ!#3is&^$M{cAz!gCN literal 0 HcmV?d00001 diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..2cf216d --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,208 @@ +#!/bin/sh +# +# Decode the tests/pcap/ fixtures with the all-versions dissector and assert, +# per Tarantool version, the exact decoded request/response bodies, header +# fields, error text and (3.x) ext values. Assertions pin concrete decoded +# strings; the uuid/datetime in the 3.x ext tuple are specific to that committed +# capture (re-capturing the fixture means updating them here). +# +# Usage: sh tests/run.sh (TSHARK overrides the binary; run as a non-root user +# -- Wireshark disables -X lua_script under root.) + +set -eu +here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +root=$(dirname "$here") +TSHARK=${TSHARK:-tshark} +DISS="$root/dist/tarantool.dissector.lua" +fail=0 + +# Preflight: confirm the dissector actually loaded (see the root note above). +if [ "$("$TSHARK" -r "$here/pcap/tarantool-2.11.pcap" -X lua_script:"$DISS" \ + -O tarantool 2>/dev/null | grep -c Tarantool)" -eq 0 ]; then + echo "ERROR: dissector produced no output. Either it failed to load, or tshark" >&2 + echo " is running as root (Wireshark disables -X lua_script then)." >&2 + exit 2 +fi + +load() { NAME="$1"; PCAP="$here/pcap/$2"; DEC="$3" + OUT=$("$TSHARK" -r "$PCAP" -X lua_script:"$DISS" $DEC -O tarantool 2>/dev/null || true); bad=0; } +lit() { printf '%s' "$OUT" | grep -Fq -- "$1" || { echo " FAIL $NAME: missing [$1]"; bad=1; }; } +re() { printf '%s' "$OUT" | grep -Eq -- "$1" || { echo " FAIL $NAME: missing /$1/"; bad=1; }; } +absent(){ printf '%s' "$OUT" | grep -Fq -- "$1" && { echo " FAIL $NAME: unexpected [$1]"; bad=1; } || true; } +req() { for r in "$@"; do lit "Request name: $r"; done; } +# Clean decode: no Lua errors, and every payload-bearing packet fully decoded -- +# nothing left as Wireshark's generic "Data" (catches gaps / undecoded bytes). +noerr(){ n=$(printf '%s' "$OUT" | grep -c 'Lua Error' || true); \ + [ "$n" -eq 0 ] || { echo " FAIL $NAME: $n Lua error(s)"; bad=1; }; \ + d=$("$TSHARK" -r "$PCAP" -X lua_script:"$DISS" $DEC -Y 'tcp.len>0 && data' \ + 2>/dev/null | wc -l | tr -d ' ' || true); \ + [ "${d:-0}" -eq 0 ] || { echo " FAIL $NAME: $d payload frame(s) left undecoded (Data)"; bad=1; }; } +done_(){ [ "$bad" -eq 0 ] && echo "ok $NAME" || fail=1; } + +# --- Tarantool 1.5 : legacy binary protocol (captured on 33013) -------------- +load tarantool-1.5.pcap tarantool-1.5.pcap "-d tcp.port==33013,tarantool" +noerr +req insert select update delete call +lit "Tarantool request (legacy <= 1.5)" +lit "Tarantool response (legacy <= 1.5)" # responses decode as responses +lit "legacy header: type 13 (insert)" +lit "legacy header: type 17 (select)" +lit "legacy header: type 19 (update)" +lit "legacy header: type 21 (delete)" +lit "legacy header: type 22 (call)" +lit "function: box.dostring" +lit '{1, "alpha", 100}' # exact decoded insert tuple (NUM + STR + NUM) +lit '{2, "beta", 200}' +lit '{3, "gamma", 300}' +lit '{1, "ALPHA", 111}' # REPLACE +lit '{"beta"}' # secondary-index SELECT key +lit '{"return box.space[0]:len()"}' # exact CALL argument +lit "return code: 0x00000000 (ok)" # exact response return code +lit "error: Procedure 'nonexistent_function' is not defined" +done_ + +# --- Tarantool 1.10 : MsgPack, no SQL/streams/watchers ----------------------- +load tarantool-1.10.pcap tarantool-1.10.pcap "" +noerr +req auth ping insert replace update upsert delete select call eval +re "Sync: [0-9]" # header field decoded +lit 'tuple: {1, "a", 10}' # exact insert body +lit 'tuple: {1, "b", 20}' # exact replace body +lit "myfunc(2, 3)" # exact CALL body +lit "eval return 1 + 1, box.info.version with args ()" +lit "message: Duplicate key exists in unique index 'pk' in space 'tester'" # 1.10 string error +done_ + +# --- Tarantool 2.11 : + SQL, streams, watchers, structured error stack ------- +load tarantool-2.11.pcap tarantool-2.11.pcap "" +noerr +req auth id ping insert replace update upsert delete select call eval \ + execute prepare begin commit rollback watch unwatch +lit 'tuple: {1, "a", 10}' +lit 'tuple: {100, "tx", 1}' # stream commit body +lit 'tuple: {101, "rb", 1}' # stream rollback body +lit 'execute SQL "CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v STRING)"' +lit 'prepare SQL "SELECT * FROM t WHERE id = ?"' +lit "ID : integer" # SQL metadata (2.11 upper-cases) +lit "V : string" +lit '{1, "one"}' # SQL data rows +lit '{2, "two"}' +lit "sql row_count: 2" +lit '[1] ClientError (code 3): Duplicate key exists in unique index "pk" in space "tester" with old tuple - [1, "b", 99] and new tuple - [1, "dup"]' +done_ + +# --- "enabled" preference : FALSE unregisters the port, nothing is decoded ---- +# Paired with a default-enabled control over the same capture so an empty result +# means "dissector disabled", not "script failed to load". +load tarantool-enabled tarantool-2.11.pcap "" +lit "Request name:" # control: decodes when enabled +done_ +load tarantool-disabled tarantool-2.11.pcap "-o tarantool.enabled:FALSE" +absent "Request name:" # disabled: no PDUs dissected +absent "Tarantool protocol data" +done_ + +# --- Tarantool 3.x : + watch_once + MsgPack ext types ------------------------ +load tarantool-3.x.pcap tarantool-3.x.pcap "" +noerr +req auth id ping insert replace update upsert delete select call eval \ + execute prepare begin commit rollback watch unwatch watch_once +lit 'tuple: {1, "a", 10}' +lit 'tuple: {100, "tx", 1}' +lit 'execute SQL "CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v STRING)"' +lit "id : integer" # 3.x preserves lower-case +lit "v : string" +lit '{1, "one"}' +lit "sql row_count: 2" +lit '[1] ClientError (code 3): Duplicate key exists in unique index "pk" in space "tester" with old tuple - [1, "b", 99] and new tuple - [1, "dup"]' +# Exact MsgPack ext decoding (decimal + uuid + datetime, then interval): +lit 'tuple: {200, -12.345, 55eca2de-8996-4375-9152-8fb5a4e7bb0a, 2026-06-16T21:04:20.672283Z}' +lit 'tuple: {201, {year=26, month=5, day=15, hour=21, min=4, sec=20, nsec=672379000, adjust=1}}' +done_ + +# --- combined 1.5 + 3.x : both framings decoded in one mixed capture --------- +load tarantool-combined.pcap tarantool-combined.pcap "-d tcp.port==33013,tarantool" +noerr +lit "Tarantool request (legacy <= 1.5)" # legacy present +lit "Tarantool response (legacy <= 1.5)" +lit "function: box.dostring" +lit '{1, "alpha", 100}' # legacy tuple, fully decoded +req execute prepare watch_once # modern-only request types +lit '[1] ClientError (code 3): Duplicate key exists in unique index "pk" in space "tester" with old tuple - [1, "b", 99] and new tuple - [1, "dup"]' +lit 'tuple: {200, -12.345, 55eca2de-8996-4375-9152-8fb5a4e7bb0a, 2026-06-16T21:04:20.672283Z}' +done_ + +# --- master-master replication, ASYNC : 3-node full mesh (ports 3311-3313) --- +# Bootstrap (join/subscribe), then asynchronous master-master writes -- non- +# conflicting, conflicting and over a stream -- replicated across the mesh. +RPORTS="-d tcp.port==3311,tarantool -d tcp.port==3312,tarantool -d tcp.port==3313,tarantool" +load tarantool-replication-async.pcap tarantool-replication-async.pcap "$RPORTS" +noerr +req join subscribe insert select begin commit rollback # bootstrap + replicated DML + txn +lit "instance_uuid:" # join/subscribe metadata +lit "replicaset_uuid:" +lit "vclock:" # replication position +lit 'tuple: {100, "node-1-row-0"}' # non-conflicting async write +lit 'tuple: {1, "inserted-on-node-2"}' # conflicting write from node 2 +lit 'tuple: {1, "inserted-on-node-3"}' # conflicting write from node 3 +lit 'tuple: {500, "tx-a"}' # streamed transaction body +lit 'tuple: {501, "tx-b"}' +done_ + +# --- master-master replication, SYNC : node 1 promoted to leader ------------- +# RAFT_PROMOTE then synchronous-space commits acknowledged with quorum +# (RAFT_CONFIRM carrying replica_id/lsn). +load tarantool-replication-sync.pcap tarantool-replication-sync.pcap "$RPORTS" +noerr +req raft raft_promote raft_confirm # leadership + quorum confirmation +lit "replica_id: 1, lsn:" # decoded synchro body +lit 'tuple: {1, "sync-quorum-commit"}' # synchronous insert +lit 'tuple: {1, "sync-updated"}' # synchronous replace +lit 'tuple: {2, "sync-tx-1"}' # synchronous transaction body +lit 'tuple: {3, "sync-tx-2"}' +done_ + +# --- co-load : the two split builds loaded together must not collide ---------- +# Distinct proto descriptions + the private per-file module registry (see +# amalgamate.sh) let legacy (tarantool1) and modern (tarantool2) coexist, and +# their distinct default ports (33013 vs 3301) keep them off the same tcp.port +# slot -- Wireshark binds one dissector per port, so a shared default would make +# one build's (un)registration evict the other. Assert no load error, that BOTH +# decode the mixed capture by their own default ports, and that disabling one +# leaves the other working. +LEG="$root/dist/tarantool-legacy.dissector.lua" +MOD="$root/dist/tarantool-modern.dissector.lua" +coload() { "$TSHARK" -r "$here/pcap/tarantool-combined.pcap" \ + -X lua_script:"$LEG" -X lua_script:"$MOD" \ + -O tarantool1,tarantool2 "$@" 2>&1 || true; } + +NAME=co-load; OUT=$(coload); bad=0 +absent "Error during loading" # no duplicate-Proto / shadowed require +absent "two protocols with the same" +lit "Tarantool request (legacy <= 1.5)" # legacy build, default port 33013 +lit "function: box.dostring" +lit "Request name: execute" # modern build, default port 3301 +lit "Request name: watch_once" +done_ + +NAME=co-load-disable-legacy; OUT=$(coload -o tarantool1.enabled:FALSE); bad=0 +absent "Tarantool request (legacy <= 1.5)" # legacy off ... +lit "Request name: execute" # ... modern unaffected +done_ + +NAME=co-load-disable-modern; OUT=$(coload -o tarantool2.enabled:FALSE); bad=0 +absent "Request name: execute" # modern off ... +lit "Tarantool request (legacy <= 1.5)" # ... legacy unaffected +done_ + +# --- ports range preference : one build binds a whole range of ports ---------- +# The combined build defaults to 3301; point its "ports" range at the +# replication mesh (3311-3313) and confirm it decodes there with no -d mapping. +load tarantool-ports-range tarantool-replication-async.pcap "-o tarantool.ports:3311-3313" +noerr +req join subscribe insert # decoded purely via the range pref +lit "vclock:" +done_ + +[ "$fail" -eq 0 ] && echo "all fixtures decoded cleanly" || echo "SOME FIXTURES FAILED" +exit $fail