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 0000000..fe6bc5a Binary files /dev/null and b/tests/pcap/tarantool-1.10.pcap differ diff --git a/tests/pcap/tarantool-1.5.pcap b/tests/pcap/tarantool-1.5.pcap new file mode 100644 index 0000000..16bf479 Binary files /dev/null and b/tests/pcap/tarantool-1.5.pcap differ diff --git a/tests/pcap/tarantool-2.11.pcap b/tests/pcap/tarantool-2.11.pcap new file mode 100644 index 0000000..adef1b4 Binary files /dev/null and b/tests/pcap/tarantool-2.11.pcap differ diff --git a/tests/pcap/tarantool-3.x.pcap b/tests/pcap/tarantool-3.x.pcap new file mode 100644 index 0000000..646072c Binary files /dev/null and b/tests/pcap/tarantool-3.x.pcap differ diff --git a/tests/pcap/tarantool-combined.pcap b/tests/pcap/tarantool-combined.pcap new file mode 100644 index 0000000..9cfa98a Binary files /dev/null and b/tests/pcap/tarantool-combined.pcap differ diff --git a/tests/pcap/tarantool-replication-async.pcap b/tests/pcap/tarantool-replication-async.pcap new file mode 100644 index 0000000..a1569f4 Binary files /dev/null and b/tests/pcap/tarantool-replication-async.pcap differ diff --git a/tests/pcap/tarantool-replication-sync.pcap b/tests/pcap/tarantool-replication-sync.pcap new file mode 100644 index 0000000..5ca9109 Binary files /dev/null and b/tests/pcap/tarantool-replication-sync.pcap differ 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