LAPB / SINTRAN Wireshark dissector (hdlc_tcp.lua)¶
A Wireshark/tshark Lua dissector for SINTRAN III traffic on both COSMOS transports. One file, three protocols:
hdlc_lapb— HDLC carried over thend100x --hdlcTCP bridge (ports 10362 / 10364 and friends). It de-frames byte-stuffed HDLC, checks the FCS, and decodes LAPB, the SINTRAN header, the XMSG envelope (seed/epoch/channel with built-in validation), the secure ACK, reachability/resync, XROUT routing letters, and the full TAD terminal protocol (login ladder, opcodes, ports).ndlink— the COSMOS Ethernet hub on TCP 5010 (added 2026-08-11). COSMOS does not use LLC1's connectionless service on Ethernet: it carries its own sequenced, acknowledged link protocol, and that is whatndlinkdecodes, along with the hub's length prefix, the 802.3 header and the LLC header above it. The SINTRAN datagram inside is handed to exactly the same code the HDLC path uses, so everything above the link layer decodes identically on both transports.
ndlink_eth— the same ND link layer on a raw capture of the Ethernet segment (added 2026-08-24). A file written byxmsghub --captureis classic pcap, link type Ethernet, with no TCP and no hub length prefix: Wireshark reads the 802.3 header and the LLC header itself and hands the rest over on DSAP 0xA8. Before this existed, such a capture decoded as nothing at all. It shares every line of the ND link and SINTRAN decode withndlink— only the framing below it differs.
The three never appear in the same stream. LAPB is HDLC only; the ND link header is Ethernet only, either inside the hub's TCP stream or straight off the segment.
Constants come from the protocol registry, not from this file¶
The frame kinds, the XROUT services and return codes, the file-server message types
and operations, the QFORM classes, the TAD opcodes and the chat message kinds are
generated into hdlc_tcp.lua from ../../../XMSG/DOC/protocols/*.json:
python E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\DOC\protocols\generate_lua.py
python E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\DOC\protocols\generate_lua.py --check
The generator rewrites only the region between
-- @@BEGIN GENERATED FROM DOC/protocols BY generate_lua.py - DO NOT EDIT @@ and
-- @@END GENERATED@@. Everything else in the file is hand-written. --check writes
nothing and fails when the block is stale, so it can go in a build.
It writes INTO the dissector rather than beside it because a Wireshark Lua script is
loaded as one file with -X lua_script: and its own directory is not on Lua's search
path, so a second file it could require would not be found.
Every generated name carries its registry status. Anything that is not MEASURED
is shown with the status in brackets — XSGNI [INFERRED], Class4 [UNKNOWN] — so a
value we are guessing at is never displayed with the same confidence as one that was
read off the wire.
Protocol details live in one place. This README covers only how to run the dissector. The wire format it implements is documented in ../../../XMSG/DOC/XMSG-PROTOCOL.md (framing, LAPB, header, envelope §18.5, ACK §6, ports §7.1, routing §9.1, scenarios §18.8) and ../../../TAD/TAD-Message-Formats.md (TAD opcodes, login ladder, connect-to session). The HDLC hardware/framing layer beneath it is in ../HDLC-Frame-Format-Reference.md.
Install — where to copy the file¶
-
Find your Wireshark Lua plugin directory: Help → About Wireshark → Folders, then use either:
- "Personal Lua Plugins" — e.g.
C:\Users\<you>\AppData\Roaming\Wireshark\plugins(per-user, no admin needed — RECOMMENDED), or - "Global Lua Plugins" — e.g.
C:\Program Files\Wireshark\plugins(all users; needs Administrator to write here).
Use only one of these. If a copy exists in BOTH, Wireshark loads both and the duplicate
Protoregistration aborts the plugin — delete the one you are not using. - "Personal Lua Plugins" — e.g.
-
Copy
hdlc_tcp.lua(from this directory) into the chosen folder. -
Load it:
- If Wireshark is already open: Analyze → Reload Lua Plugins (
Ctrl+Shift+L). - Or just restart Wireshark.
- Copying the file while Wireshark is open does nothing until you reload — Wireshark reads plugins once at startup.
- If Wireshark is already open: Analyze → Reload Lua Plugins (
Keep the installed copy in sync with this repo copy whenever the dissector is
updated — both must match. (Quick check: right sizes/hashes; or re-copy after
every git pull that touched this file.)
Verify a repo edit WITHOUT installing it (no admin needed)¶
Loading the repo copy with -X lua_script: while an installed copy exists aborts with
Proto_new: there cannot be two protocols with the same description. Overriding the
plugin directory to an empty folder stops the installed copy loading, so the repo copy
is the only one and can be tested directly:
mkdir -p /tmp/noplugins
WIRESHARK_PLUGIN_DIR=/tmp/noplugins tshark \
-X lua_script:"<repo path>/hdlc_tcp.lua" -r capture.pcapng -Y hdlc_lapb -V
(WIRESHARK_CONFIG_DIR does NOT work for this - it only moves the personal config
directory, and the installed copy lives in the global one.)
The acceptance check before installing: run it over several captures and require
zero Lua errors and no NEW expert warnings versus the installed copy. Count
them with grep -c "Expert Info" on both and compare - the numbers must match.
Two things make that comparison lie, so check them first:
- The installed copy may be old. Compare against
git show HEAD:<path>rather than against whatever is in the plugins folder - the installed copy here was found to be several revisions behind, which made every count look like a regression. - Compare like for like when a change adds a transport. Adding the
ndlinkpath makes frames decode that previously decoded as nothing, so their expert items are new by definition. Filter them out to check the HDLC path alone:-Y "_ws.expert && hdlc_lapb && !ndlink".
Use — the display filter you type at the top of the window¶
The nd100x --hdlc bridge uses a configurable TCP port, and Wireshark
captures pick up unrelated background traffic (mDNS, SSDP, NBNS, your own SSH,
etc.) that clutters the list. So after opening a capture, type a display
filter in the bar at the top of the Wireshark window and press Enter:
tcp.port == 10362
If your ND-100 emulator is configured with a second HDLC line (or you run two ports), OR them together:
tcp.port == 10362 || tcp.port == 10364
10362/10364are the DEFAULT HDLC ports — change the numbers to whatever you configurednd100x --hdlcto listen on. These two ports (plus a few others) are also pre-bound in the dissector, so their streams auto-decode without needing "Decode As".-
To see only the decoded protocol and hide the raw TCP acks, filter on the protocol name instead:
hdlc_lapb(that is the dissector's protocol name — use it in the filter bar or in
tshark -Y hdlc_lapb). - Useful field filters once decoded:hdlc_lapb(all frames),xmsg/tad/routingsub-trees, or specific fields such astad.sycn,sintran.flags1,lapb.addr.
Command-line (tshark)¶
tshark -r capture.pcapng -Y hdlc_lapb
tshark -r capture.pcapng -Y "tcp.port == 10362" -V
If a capture uses a port the dissector doesn't recognise and the stream is not
auto-decoded, force it: tshark -r cap.pcapng -d tcp.port==<port>,hdlc_lapb.
Troubleshooting¶
- "Nothing is decoded / frames show as TCP." In order:
- Reload the plugin (
Ctrl+Shift+L) or restart Wireshark — the most common cause is an edit/copy that Wireshark hasn't picked up yet. - Check the plugin is enabled: Analyze → Enabled Protocols (
Ctrl+Shift+E), typehdlc, ensure HDLC_LAPB is ticked. - Add the display filter
tcp.port == <your HDLC port>so the HDLC stream isn't lost among background traffic. - If one specific stream still won't decode (capture started mid-frame), right- click a packet in it → Decode As… → set the TCP port to HDLC_LAPB.
- Reload the plugin (
- A red error bar on reload means a Lua error — copy the exact text; it names the line.
- Every frame shows FCS BAD: Edit → Preferences → Protocols → TCP → enable "Allow subdissector to reassemble TCP streams" and disable TCP checksum validation.
Reading a COSMOS Ethernet hub capture (TCP 5010)¶
All the lab machines join one hub, so a single loopback capture sees every node:
& "C:\Program Files\Wireshark\tshark.exe" -i "\Device\NPF_Loopback" -f "tcp port 5010" -a duration:120 -w out.pcapng
Then filter on ndlink. Wireshark's own table claims 5010 as IPSICTL, so without
this dissector a hub capture shows nothing useful.
Do NOT instead force
-d tcp.port==5010,hdlc_lapb. It looks like it works — thousands of frames turn into LAPB — but the decode is misaligned and the output is noise that reads like data: node numbers come out as53306 → 1instead of 100/102/103.
Useful fields: ndlink.kind, ndlink.seq, ndlink.backlog, ndlink.dupof,
ndlink.srclink / ndlink.dstlink, ndlink.sysno.
The hub is a BROADCAST hub, so one frame sent once is forwarded to every other
member and the same bytes appear on several TCP streams. The dissector counts each
frame exactly once by binding every MAC-to-MAC direction to the first TCP stream it
was seen on; the other copies are marked hub fan-out copy and carry no backlog
line. Do not count all the copies by hand — that is what made one earlier set of
block-size numbers unusable.
Reading a RAW segment capture (classic pcap, link type Ethernet)¶
xmsghub --capture writes a plain pcap of everything on the segment - no TCP, no hub
length prefix. Just open it; the binding on llc.dsap == 0xA8 claims the frames on its
own and nothing has to be forced with "Decode As".
& "C:\Program Files\Wireshark\tshark.exe" `
-X lua_script:"E:\Dev\Ronny\NDInsight\SINTRAN\Devices\HDLC\WireShark\hdlc_tcp.lua" `
-r segment.pcap -V
Everything the hub path shows, this shows: the frame kind and sequence, the backlog,
the retransmission warning, the SINTRAN header with its checksum, the XMSG envelope
and the message body. The same field names work, so ndlink.kind, ndlink.seq,
ndlink.backlog and ndlink.dupof all filter a segment capture too.
Counting what is in a capture, which is usually the first question:
tshark ... -r segment.pcap -T fields -e ndlink.kind # then group them
tshark ... -r segment.pcap -Y "ndlink.kind==0x6f" -T fields -e frame.number -e frame.time_relative -e eth.src
tshark ... -r segment.pcap -Y "fa.msgtype" # every file-server message
tshark ... -r segment.pcap -Y "chat.kind" # every chat message
The machines name themselves in the MAC: 08:00:26 | system number, low byte first |
physical user, so 08:00:26:64:00:00 is system 100 and 08:00:26:66:00:00 is 102.
The dissector shows the decoded number as a generated ndlink.sysno.
A kind it does not know is shown as UNKNOWN and nothing is invented for it.
DOC/captures/XMSG-DEGRADE-2026-08-24/segment.pcap holds two frames of kind 0x70
that no registry entry covers; they read as
?? kind 0x70 UNKNOWN - not in the protocol registry. The only thing said about such
a frame is the NPDU index in its high nibble, and that table carries its own caveat.
Reading the message body¶
Since 2026-08-24 the dissector decodes the XMSG message body at its measured offset, absolute 28 - fourteen bytes of SINTRAN header plus fourteen of XMSG sub-header. It recognises three families and says plainly when it recognises none:
| Body | Recognised by | Fields |
|---|---|---|
| file server (FA) | body word 0 is a known FA message type | fa.msgtype, fa.conversation, fa.session, fa.operation, and a full walk of the QFORM tag encoding (qform.*) |
| XROUT | one end of the message is port 0, which is XROUT, and body word 0's low byte is a known service (bit 6 set) or return code (bit 6 clear) | xrout.service, xrout.status |
| CHAT | the SHAPE fits: a known kind byte and two length fields that land exactly on the end of the body | chat.kind, chat.name, chat.text |
Two of those need a caveat, and both are in the file beside the code:
- CHAT is recognised by shape, not by a tag. Nothing on the wire says "this is chat" - it is ordinary XMSG user data sent to a port the server claimed by name, and a dissector cannot know which port that is. The shape test is strong and it runs last, but the tree says it is a shape test.
- The XROUT test is gated on port 0 on purpose. Without that gate a return status
of
0x00is XRSOK, so every TAD terminal-data frame came out labelled "XROUT reply [XRSOK]" - confidently wrong output, which is the one thing this dissector must not produce.
The older fields xmsg.xmcsm and xmsg.xmlen keep their filter names but were
relabelled at the same time. The carved sub-header is 14 bytes, so those four bytes
at absolute 26-29 are not one field: 26-27 is the whole 16-bit XMCSM (always equal to
Flags 2) and 28-29 is the application's first word. xmsg.xmlen at 30-31 is body word
1, which is a length on TAD and XROUT traffic and the CONVERSATION NUMBER on a
file-server message - not a general user-data length.
Validation the dissector performs¶
On the HDLC path, for each data frame it derives the link seed, epoch and
expected channel (spec §18.5) and flags a Wireshark expert warning when the
on-wire Protocol-ID, Flags2, secure-ACK channel, or the LAPB odd-length address
bit disagree with the model — so a malformed or mis-sequenced frame stands out in
the packet list. A clean capture shows zero expert warnings.
On the SINTRAN header, on both transports, it recomputes word 6 (the ones-complement checksum over the other six words) and warns when it disagrees.
On the ND link layer it carries the three checks that came out of the 2026-08-11 investigation, where two nights went into the file-access protocol for a fault one layer below it. All three are the cheap tells that would have found it in minutes:
| Check | Warns when | Why it matters |
|---|---|---|
| seven-bit sequence | a data frame's sequence has bit 7 set | the sequence is 7 bits; the highest value in three real captures is 0x7F and the wrap 0x7F → 0x00 is visible. No real ND emits a 0x80. |
| send window | more than 6 frames are unacknowledged from one sender | a real ND has been seen to send six before waiting. A backlog that climbs and never comes down is THE symptom: the peer starts retransmitting everything, and that reads as a pile of application defects one layer up. This row said 4 until 2026-08-24; the code has said 6 since 2026-08-11, and the code is right. The number moved three times in one day and a capture can only ever give a FLOOR - see the long note beside ND_SEND_WINDOW. |
| retransmission | a sender repeats a sequence still unacknowledged | a peer repeating a data frame is never normal — it means it has not seen an acknowledgement. The warning says whether the bytes were byte-for-byte identical. |
The acknowledgement carries the next expected sequence, not the one being
acknowledged, and its trailing word is 0000 on every captured acknowledgement from
both machines — there is no credit field, so the window cannot be negotiated. The
two constants live in hdlc_tcp.lua as ND_SEQ_MODULUS and ND_SEND_WINDOW, next
to the measurement that produced them; the same two numbers are pinned in the C#
reference implementation (SINTRAN/XMSG/SRC/Xmsg.Ethernet/) and held to the
captures by NdLinkCaptureConformanceTests.
Measure the window on a READ capture, never a listing. A listing tops out at two frames — the short acknowledgement and the reply — because it never sends a content message, and reading only a listing gives a window half the real size.
What this looks like in practice, measured over the shipped captures: every capture of two real ND machines stays at a backlog of 1–4 with zero retransmissions, while every capture of a run made before the send window existed shows a backlog of 17 to 125 and dozens of retransmissions. The two are told apart at a glance by one column.
Field reference¶
Filter fields are namespaced lapb.*, sintran.*, xmsg.*, tad.*,
routing.*, xm.*, env.*, ack.*, reach.* for the HDLC path and ndlink.*
for both Ethernet paths (the hub's TCP stream and a raw segment capture share the
field names, because they share the decode). The message-body decoders added on
2026-08-24 use fa.*, qform.*, chat.* and xrout.*, and their value strings come
from the protocol registry with the status attached. See the ProtoField definitions at the top of
hdlc_tcp.lua (HDLC) and in the ND link section near the bottom; field meanings are
explained in the two protocol documents linked above, and the ND link header field
by field in ../../../XMSG/SRC/Xmsg.Ethernet/NdLinkHeader.cs.
Test captures: the separate sibling X25Emulator repository (pcap/ directory)
holds both HDLC and hub captures, and ../../../XMSG/DOC/captures/ holds the
machine-to-machine hub captures.
The captures in
../../../XMSG/DOC/captures/FA-READ-WRITE-2026-08-04/are text hex dumps, not pcap — one frame per line astime length hex, raw 802.3 frames with no hub length prefix. tshark cannot open them directly. To run the dissector over them, prefix each frame with its own 2-byte big-endian length and feed the result totext2pcap -t "%H:%M:%S." -T <anyport>,5010. Doing that reproduces both measured constants from the dissector's own output: the highest sequence in all three is 127, and the backlog peaks at 2 forcapture-list-files, 4 forcapture-readand 2 forcapture-write— the read capture being the only one that shows the real window.