Captures and traces
Two kinds of files describe what happened on a Modbus link, and OT Fabric has tools for both:
| A capture (MBCAP) | A packet trace (pcap, pcapng) | |
|---|---|---|
| Holds | What a device answered, and when: request and response PDUs per read | Every packet a capture point saw, of every protocol |
| Made by | modbusctl client record and client scan, or exported from a trace |
tcpdump, dumpcap, tshark or Wireshark, without touching the devices |
| Read with | mbcapctl, modbusctl server, Modbus Trace Explorer |
mbtracectl, Modbus Trace Explorer |
| Good for | Serving a device again, as a simulator or a digital twin | Finding what goes wrong between a client and its devices |
The usual path goes from a trace, or a recording, to a capture, to a simulated device:
pcap / pcapng ──mbtracectl export mbcap──▶ site.mbcap ──modbusctl server replay──▶ simulated device (or Modbus Trace Explorer) ▲modbusctl client record ───────────────────────┘MBCAP, the capture format
Section titled “MBCAP, the capture format”MBCAP is defined by go-modbus-capture, which is its Go library and the home of the mbcapctl tool. A capture is built from four kinds of records:
| Record | Is | Example |
|---|---|---|
| Header | The capture: tick size, wall-clock start, producer, flags, tags, end | Recorded on 2026-10-14 at 13:14:24 UTC, 1 µs ticks |
| Target | A device: an endpoint plus a unit ID | Inverter A at 192.168.2.201:502, unit 21 |
| Stream | One way a target’s data was acquired | “poll every second”, “identity probe” |
| Sample | One answered request: stream, time, request PDU, response PDU | At 1.00045 s, 03 1000 0002 → 03 04 0001 01a7 |
Here is a whole capture in its JSON Lines encoding: two holding registers polled every second, and one device identification answer.
{"t":"header","version":"0.1","tick_ns":1000,"wall_start":"2026-10-14T13:14:24.701149Z","producer":"modbusctl 3.0.0"}{"t":"target","id":1,"label":"Inverter A","transport":"tcp","host":"192.168.2.201","port":502,"unit":21}{"t":"stream","id":1,"target":1,"role":"poll","label":"holding 4096 x2"}{"t":"stream","id":2,"target":1,"role":"identity"}{"t":"s","s":1,"at":0,"req":"0310000002","resp":"0304000101a7"}{"t":"s","s":1,"at":1000450,"req":"0310000002","resp":"0304000101a7"}{"t":"s","s":1,"at":2001100,"req":"0310000002","resp":"0304000101a8"}{"t":"s","s":2,"at":3500000,"req":"2b0e0100","resp":"2b0e0101000003000441434d45010258310203312e30"}{"t":"end","at":4000800}Three rules give the data its meaning:
- Sample and hold. A value holds from its sample until the next sample of the same address or request. Nothing is interpolated.
- Only what was recorded exists. An address or function without a sample is absent; the replayer decides how to answer it.
- Metadata is advisory, values are not. A replayer may serve a target on another address or unit ID, but never changes a recorded value or time. Edits produce a new file.
Times are integer ticks since the start, from a monotonic clock, so a capture replays at exactly its original pace or any multiple of it. Captures hold the read functions of the Modbus specification only: FC01 to FC04, FC07, FC08 (sub-functions that return data), FC11, FC12, FC17, FC20, FC24 and FC43. Vendor and unknown function codes are rejected by every reader and writer.
| Encoding | Extension | Lossless | For |
|---|---|---|---|
| MBCAP binary | .mbcap |
yes | Recording and archiving: compact, append-only, checksummed, readable after a crash |
| JSON Lines | .mbcap.jsonl |
yes | Reading, diffing, scripting, test fixtures |
| Long CSV | .mbcap.csv |
yes | Spreadsheets and dataframes |
| Wide CSV | .wide.csv |
view only | Charting raw values per address over time |
The binary encoding stores each distinct request once, codes times as small deltas and responses as “same as before” or only the words that changed, in deflate-compressed, checksummed blocks with an index at the end. A typical meter takes about 28 bytes per sample. The three lossless encodings convert into each other and back to the identical file.
mbcapctl
Section titled “mbcapctl”go install github.com/otfabric/go-modbus-capture/cmd/mbcapctl@latestBinaries are also on the releases page. mbcapctl never opens a Modbus connection; it works on files:
mbcapctl info capture.mbcap # targets, streams, samples, functions, spanmbcapctl verify capture.mbcap # checksums, structure and the rules of the formatmbcapctl convert capture.mbcap capture.mbcap.jsonl # lossless, encoding from the file namembcapctl filter capture.mbcap first-ten.mbcap --fc 3 --to 10mIt also dumps, merges, splits, trims and repairs captures; a file cut short by a crash reads up to its last complete block. Every command is in the CLI reference.
From Go
Section titled “From Go”go get github.com/otfabric/go-modbus-capturer, err := mbcap.NewReader(f) // or jsonl.NewReader, mbcsv.NewReaderif err != nil { return err}for { rec, err := r.Next() if err == io.EOF { break } if err != nil { return err } if s, ok := rec.(capture.Sample); ok { fmt.Printf("%v: % x -> % x\n", r.Header().Duration(s.At), s.Request, s.Response) }}The packages are capture (the model, the Reader and Writer interfaces and the rules), mbcap, jsonl and mbcsv (one per encoding) and edit (filter, trim, split and merge, each writing a new capture with provenance). The library uses the standard library only, needs Go 1.23 or later, and every reader is fuzzed against hostile input. A recorder built on go-modbus gets the exact PDUs from the client’s PDUTap (see the Go library).
Packet traces
Section titled “Packet traces”go-modbus-trace reads pcap and pcapng files, finds the Modbus/TCP traffic in them and gives a typed, time-ordered view of what happened: connections, requests paired with their responses, exceptions and unanswered requests. It analyses devices, units and poll patterns, exports a timeline, extracts a sub-trace for Wireshark, and exports an MBCAP capture. It is offline only: it never captures live and never sends anything, needs no libpcap and is bounded in memory for any trace size.
Capturing a useful trace
Section titled “Capturing a useful trace”- Capture where both directions pass: on the client (SCADA host, gateway), on a mirrored switch port (SPAN) or with a network tap. An ordinary switch port sees only its own traffic.
- Keep whole packets (
-s 0, the default in current tools), and filter by port when capturing, not by protocol afterwards: saving only what Wireshark decodes asmodbusdrops the handshakes and retransmissions the reassembly needs. - Start before the connections open, then start or restart the poller. The handshake shows which side is the server; without it the role is inferred from the traffic and reported as guessed.
sudo tcpdump -i eth0 -s 0 -n -w site.pcap 'tcp port 502'dumpcap -i eth0 -f "tcp port 502" -b filesize:100000 -b files:50 -w site.pcapng # ring bufferModbus/TCP Security (TLS, usually port 802) is encrypted and cannot be decoded; the tools name such flows instead of guessing.
mbtracectl
Section titled “mbtracectl”Download a static binary for Linux (amd64, arm64, armv7), macOS or Windows from the releases page, or:
go install github.com/otfabric/go-modbus-trace/cmd/mbtracectl@latest| Command | What it tells you |
|---|---|
info |
Format, interfaces, capture drops, snap length, connections and quality counters |
flows |
The TCP flows that carry Modbus/TCP: client, server, score and how the roles were decided |
summary |
Per server and unit: transactions by status and function, clients, latency, poll patterns, addresses read and written |
timeline |
Every transaction in time order, filtered, as text, CSV or JSON Lines |
extract |
The packets of chosen connections, as a pcapng for Wireshark |
export mbcap |
A capture that modbusctl replays as a virtual device, with a report |
# Every exception and unanswered request of unit 3, with the request that caused itmbtracectl timeline site.pcapng --unit 3 --status exception,unanswered
# Responses slower than 200 ms, as CSV for a spreadsheetmbtracectl export timeline site.pcapng --slow 200ms --flag slow -o slow.csv
# Straight from a gateway, without storing the trace theressh gw 'tcpdump -i eth0 -U -w - port 502' | mbtracectl export mbcap - -o site.mbcapSeveral files are read as one trace (mbtracectl summary ring_*.pcapng), --json gives machine output with a JSON Schema per document, and the exit codes are 0, 1 and 2.
From a trace to a simulated device
Section titled “From a trace to a simulated device”mbtracectl export mbcap site.pcapng -o site.mbcap --name 10.0.0.2:502/1=Inverter --report site.report.jsonmbcapctl verify site.mbcapmodbusctl server replay --input site.mbcap --listen 127.0.0.1 --port 1502 --before-first absent --missing exceptionOnly answered reads that were paired with certainty become samples. Writes, vendor functions and anything uncertain are counted in the report by reason and never replayed. Read the warnings the export prints, because they name what a replay cannot reproduce:
- Writes are not replayed. A value written after its last read keeps its old value in the replay (
stale_after_write). - Only what was read exists. A passive trace holds exactly what the observed clients asked for;
--missing exceptionmakes the rest visible. - Unit collisions. Two servers that both use unit 1 are two targets that one listener cannot serve: run one replay per target (
--target N --port P) or remap one with--unit. - Times are when the capture point saw each response, not device timestamps; latency is not replayed.
- Memory. modbusctl loads the whole capture, about 12 MiB plus 750 bytes per sample, which the report estimates. For a long trace, export a window with
--fromand--to.
In a window
Section titled “In a window”Modbus Trace Explorer is the desktop application on go-modbus-trace: open a trace and see it per device, per poll and per register, filter the timeline, decode any transaction down to the bytes, and export one device as MBCAP with a preview of what goes in and why the rest stays out. It opens MBCAP captures too, to see what a replay will serve.