XMSG TAD terminal bring-up (MOTD) — the unsolved channel/sequence problem¶
Purpose of this document. A self-contained, precise problem statement for a fresh analyst
(human or LLM). It states what is VERIFIED, what is ASSUMED, and what is UNKNOWN, with
exact byte values from the packet captures and from live tests against a real SINTRAN III machine.
The goal is to determine the rule that decides the sub-protocol channel (Protocol ID) and the
datagram sequence (Flags 1) for the terminal-data phase of a SINTRAN connect-to session, so a
freshly-started responder node can send the MOTD/login without machine 100 rejecting or crashing.
Everything marked ASSUMPTION or IDEA is not established fact — treat with suspicion.
1. Context and goal¶
- System: Norsk Data SINTRAN III, the XMSG inter-node message system, over HDLC/LAPB.
- We are building a C# node (call it node 102) that connects to a real/emulated ND
machine (node 100) over a
nd100x --hdlcTCP bridge, and participates in XMSG well enough that a user on 100 can type@connect-to d102and get a terminal session hosted by our node (MOTD banner,PASSWORD:prompt, a menu). Node 102 is the responder/host; node 100 is the terminal/client. - What already works (VERIFIED live against 100, no crash):
- LAPB link establishment (balanced, both sides SABM).
- Reachability request/reply.
list-route(XSGSY) replies.- The
connect-tohandshake: 100 sends a connect, we reply with a connect-accept; 100 sends a session-setup, we reply with a port-assign. 100 delivery-ACKs both of our replies. No crash.
- What is BLOCKED: the very next step — the responder's terminal-data burst (a
DUMMno-op, thenRESE/control frames, then the MOTD asBDATtext). Every attempt to send the first terminal-data frame is either sequence-rejected (recoverable) or crashes 100's XMSG (fatal).
2. Wire format essentials (VERIFIED)¶
Reference: SINTRAN/XMSG/DOC/XMSG-PROTOCOL.md, and the FCS-verified peer spec
SINTRAN/XMSG/OLD/xmsg-hdlc-protocol.md.
2.1 SINTRAN header (13 bytes) — every XMSG information field starts with it¶
| Offset | Field | Notes |
|---|---|---|
| 0 | Marker 1 = 0x21 |
|
| 1 | Marker 2 = 0x13 (normal) / 0x12 (relay) |
|
| 2 | Packet type = 0x00 |
|
| 3 | Subtype | 0x0E Data, 0x03 Ack, 0x13 ReachReply, 0x19 ReachReq, 0x07 NetworkError |
| 4-5 | Destination node (big-endian) | e.g. 0x0064 = 100, 0x0066 = 102 |
| 6-7 | Source node | |
| 8-9 | Flags 1 | the per-direction datagram sequence, or 0xFFFF on broadcast |
| 10-11 | Flags 2 | frame-class word; for a NetworkError it is a negative XE* code |
| 12 | Protocol ID (channel) | 0xD8..0xDE — the crux of this problem |
2.2 XMSG sub-header (19 bytes) — present on Data (0x0E) frames, right after the header¶
| Offset (from sub-header start) | Field | Notes |
|---|---|---|
| 0 | Counter | per-direction byte, generally decrements |
| 1-2 | Marker = 0x21 0x00 |
constant |
| 3 | Frame flags | e.g. 0x86 setup, 0x92/0x96 data |
| 4 | Role | 0x40 responder setup, 0x00 data-phase, 0xE4/0x84 asker |
| 5-6 | XMDSY dest system | |
| 7-8 | XMDPT dest port | |
| 9-10 | XMSSY src system | |
| 11-12 | XMSPT src port | port = (logical << 7) | low7 (magic-number encoding) |
| 13-16 | XMCSM control/service word (32-bit) | its top byte classifies the frame |
| 17 | Pad = 0x00 |
|
| 18 | XMLEN user-data length (low byte) |
Then the payload (a TAD message chain, or XROUT letter).
2.3 XMCSM values seen (VERIFIED)¶
0x04000041— SYSTEM-TAD connect (the*TADADMletter).0x04000000— session-setup / port-negotiation.0x01080000— terminal data (BDAT, DUMM, RESE, MOTD…). This is the one that must land on DB.0x00080000— a terminal control class (the0x20/ESCAframes).
2.4 Protocol IDs (channels) observed¶
0xDE ROUTING, 0xDD TAD, 0xDC DC (terminal data), 0xDB DB (terminal data variant),
0xDA PAD, 0xD9, 0xD8. In a session these are used as a pool of per-stream channels.
2.5 Error codes (VERIFIED from the peer spec + memory)¶
Subtype 0x07 = network error/reject. Flags 2 = a negative 16-bit XE* code:
- 0xFFED = XEIMA −19 (invalid magic number)
- 0xFFDE = XENSE −34 (sequence error) ← the one we keep hitting
A 0x07 reject is recoverable (100's XMSG stays up). A separate failure mode, XXPER
(XMSG error 24B, physical address 134265B), is fatal — XMSG dies ("XMSG not running", XMFIDO
aborts) and must be restarted on the ND.
3. The envelope model (VERIFIED to fit captures; does NOT predict fresh-node behaviour)¶
Reference: XMSG-PROTOCOL.md section 18.5; implemented in
SINTRAN/XMSG/SRC/Xmsg.Protocol/Packet/XmsgEnvelope.cs.
Base = Flags1 + Counter (16-bit)
Channel = 0xDE − (XMCSM >> 24) − (Base >> 8)
Within one stream, Flags1 increments and Counter decrements in lockstep, holding Base (and therefore the channel) constant.
This formula reproduces EVERY frame in the captures — see section 5. It is verified by
Xmsg.Protocol.Tests/EnvelopeChannelTests.cs. BUT section 6 shows it does not predict what a
real 100 accepts from our freshly-started node, because the channel depends on the absolute
sequence value, which a fresh node cannot set the way a long-running ND can.
4. Reference material — where to look¶
| Artifact | Path | What it contains |
|---|---|---|
| Main protocol spec | SINTRAN/XMSG/DOC/XMSG-PROTOCOL.md |
wire format, envelope model (18.5) |
| Peer/friend spec (FCS-verified) | SINTRAN/XMSG/OLD/xmsg-hdlc-protocol.md |
independent decode, XE codes, port<<7 |
| Decode report | SINTRAN/XMSG/SRC/pcap-decode-report.txt |
every captured frame decoded (line refs below) |
| The seam plan + live findings | SINTRAN/XMSG/DOC/XMSG-TRANSPORT-SEAM-PLAN.md |
phase history, "Phase 6 live findings" |
| pcap files | pcap/*.pcapng (in the X25Emulator repo) |
raw captures |
| TAD message formats | SINTRAN/XMSG/TAD/ and SubProtocol/TadOpcodes.cs |
TAD opcode table |
Key capture: conn-to-d102-from-100.pcapng — a real connect-to d102 where 100 is the
client and 102 is the host, i.e. exactly our roles. Decode report lines 2012–2929:
- lines 2012–2456 = 100→102 direction (frames 1–49)
- lines 2457–2929 = 102→100 direction (frames 50–98) ← the responder side we must reproduce
5. The captured ground truth (VERIFIED byte values)¶
All from conn-to-d102-from-100.pcapng. In this capture the connect channel is 0xD9 and both
machines had been running (their sequences are already "high").
5.1 The responder (102→100) frames — what we must reproduce¶
| Frame | Subtype | Proto | Flags1 | Counter | Base | XMCSM | Role | Meaning |
|---|---|---|---|---|---|---|---|---|
| 50 | Ack 0x03 | DD | 0x00F8 | (trailing 0x26) | — | — | — | ACK of connect |
| 51 | Data | D8 | 0x012F | 0xE5 | 0x0214 | 04000041 | 40 | connect-accept |
| 53 | Data | D8 | 0x0130 | 0xE4 | 0x0214 | 04000000 | 40 | port-assign |
| 54 | Data | DB | 0x0131 | 0xDB | 0x020C | 01080000 | 00 | DUMM (first terminal-data) |
| 57 | Data | DC | 0x0132 | 0xDA | 0x020C | 00080000 | 00 | control 0x20 |
| 58 | Data | DB | 0x0133 | 0xD9 | 0x020C | 01080000 | 00 | RESE |
| 62 | Data | DB | 0x0135 | 0xD7 | 0x020C | 01080000 | 00 | MOTD (BDAT banner) |
| 66 | Data | DB | 0x0137 | 0xD5 | 0x020C | 01080000 | 00 | PASSWORD prompt |
Exact bytes (frame 54, DUMM): 2113000E0064006601310108DBDB21009200006402AB006604C20108000000021800
Exact bytes (frame 62, MOTD): begins
2113000E0064006601350108DBD721009600006402AB006604C201080000007C0004030100000003010101600D0A...
and the BDAT payload decodes to the ASCII banner
"…22.27.22 8 APRIL 1998 / SINTRAN III - VSX/500 L / --- RETROCORE EMULATED L ID:102 --- / ENTER ".
Check the envelope formula on these:
- Frame 51: Base 0x012F+0xE5 = 0x0214; 0xDE − 0x04 − 0x02 = 0xD8 ✓
- Frame 54: Base 0x0131+0xDB = 0x020C; 0xDE − 0x01 − 0x02 = 0xDB ✓
- Frame 57: Base 0x0132+0xDA = 0x020C; 0xDE − 0x00 − 0x02 = 0xDC ✓
KEY: Flags1 runs continuously 0x012F→0x0130→0x0131→0x0132… across accept, port-assign, and data, and the Counter jumps (0xE4→0xDB) between the control stream (Base 0x0214) and the data stream (Base 0x020C). So the responder maintains one monotonic Flags1 sequence but two Base values (0x0214 control, 0x020C data). The channel is fully determined by Base and XMCSM.
5.2 The client (100→102) data frames — 100's own data channel¶
| Frame | Proto | Flags1 | Counter | Base | XMCSM | Meaning |
|---|---|---|---|---|---|---|
| 6 | DC | 0x00FA | 0x12 | 0x010C | 01080000 | TMOD/TTYP/DESC/OPSV |
| 7 | DD | 0x00FB | 0x11 | 0x010C | 00080000 | ESCA |
| 10 | DC | 0x00FC | 0x10 | 0x010C | 01080000 | RECO |
- Frame 6: Base 0x00FA+0x12 = 0x010C;
0xDE − 0x01 − 0x01 = 0xDC✓ - Frame 7: Base 0x00FB+0x11 = 0x010C;
0xDE − 0x00 − 0x01 = 0xDD✓
Observation (VERIFIED): the two directions differ by Base high byte — 100→102 data has
Base >> 8 = 1 (→ DC/DD), 102→100 data has Base >> 8 = 2 (→ DB/DC). Because 100's Flags1 was
~0x00FA and 102's was ~0x0131, i.e. both sides simply had high running sequences, and the
different high bytes fall out of Base = Flags1 + Counter.
6. What we tried LIVE and what machine 100 answered (VERIFIED)¶
Our live node's sequence starts fresh (~0x0000) because it has no prior XMSG traffic. In each
live connect, 100's connect Flags1 was 0x0000, its counter ~0x14; 100's port varied per connect
(0x02BC, 0x02DD, 0x02F7, …). We assign our session port 0x0211.
The echo accept/port-assign (Flags1 echoes the connect's 0x0000/0x0001, channel echoes the
connect's DA) is accepted by 100 — it ACKs them and the session advances to the terminal phase.
The problem is purely the first terminal-data frame (the DUMM).
| # | DUMM Flags1 | Counter | Base | Derived channel | 100's response |
|---|---|---|---|---|---|
| 1 | 0x0131 (capture-absolute, a jump) | 0xDB | 0x020C | DB | XENSE 0xFFDE (recoverable). Reject frame: 2113000700660064 0131 FFDE DD 0B — subtype 0x07, echoes our Flags1, on channel DD, trailing 0x0B. No crash. |
| 2 | 0x000C (continuous, low) | 0x08 | 0x0014 | DD | XXPER fatal crash (XMSG dies) |
| 3 | 0x000C (continuous) | 0xFF | 0x0101 | DC | XXPER fatal crash |
| 4 | (accept itself) 0x012F on D8 | 0xE5 | 0x0214 | D8 | XENSE on the accept: 2113000700660064 012F FFDE DD 0D. 100 rejected our high-sequence accept. No crash, but the session never advanced. |
6.1 What these four results establish (VERIFIED)¶
- Terminal-data must be on channel DB. DC (probe 3) and DD (probe 2) both cause the fatal XXPER. DB (probe 1) produced only a recoverable XENSE — i.e. 100 parsed the DB frame as a valid terminal-data frame and rejected only its sequence. So DB is structurally correct; DC/DD are structurally invalid for a 102→100 terminal-data frame and crash the kernel.
- Flags1 must be continuous with our own prior frames (probe 1's jump → XENSE).
- 100 forces our sequence low. Probe 4 tried to start our whole sequence high (
0x012F) so DB would be reachable; 100 rejected the accept with XENSE. So we cannot simply adopt the capture's high sequence — 100 requires our datagram sequence to follow the low connect it sent.
6.2 The contradiction (the actual problem)¶
- To land on DB with
XMCSM 0x01080000:0xDE − 1 − (Base>>8) = 0xDB⟹Base>>8 = 2⟹Base ≈ 0x02xx⟹ Flags1 must be high (a one-byte Counter, max0xFF, cannot lift a low Flags1 to0x02xx— e.g.0x0002 + 0xFF = 0x0101, onlyBase>>8 = 1→ DC). - But 100 forces Flags1 low (must be in-sequence with the connect's
0x0000…; a high start is XENSE-rejected).
A fresh node cannot satisfy both. In the capture there was no conflict because 102 already had a
high running sequence (0x0131), so its 0x01080000 frames landed on DB for free. We start at
zero and 100 will not let us jump. Therefore the envelope formula, though it reproduces the
captures exactly, does not by itself tell us how a fresh responder reaches DB.
7. Code modules (for reference)¶
All under SINTRAN/XMSG/SRC/. C# / .NET 9. No LINQ, no foreach, named-delegate events.
| Module | Responsibility |
|---|---|
Xmsg.Protocol/Wire/SintranHeader.cs, XmsgSubHeader.cs, XmsgFrame.cs |
parse/serialise the wire frame |
Xmsg.Protocol/Packet/XmsgEnvelope.cs |
DeriveChannel(flags1, counter, xmcsm) — the formula in §3 |
Xmsg.Protocol/Packet/XmsgPacketBuilder.cs |
CreateData, CreateAck, CreateSessionData (derives the channel) |
Xmsg.Protocol/StateMachine/SecureDatagramReceiver.cs |
the subtype-0x03 secure ACK (channel = connect+4, trailing = connect-counter+0x0A) — VERIFIED working |
Xmsg.Live/Tad/TadTerminalResponder.cs |
the responder: OnConnect (accept), OnSessionSetup (port-assign + the disabled DUMM), BuildResponderFrame |
Xmsg.Live/Seam/XmsgLayer.cs |
L3 dispatch above the codec |
Xmsg.Live.Runner/Program.cs |
live composition; SendTerminalBringup is currently false (bring-up disabled to avoid the crash) |
Xmsg.Protocol.Tests/EnvelopeChannelTests.cs |
proves the formula reproduces the captured DUMM/MOTD byte-for-byte |
Xmsg.Live.Tests/TadConnectAcceptTests.cs |
proves accept/port-assign/DUMM byte values |
The secure ACK (subtype 0x03) is fully solved and VERIFIED live: channel = connect-channel + 4
(DA→DE, D9→DD); trailing byte seeded at connect-counter + 0x0A, decrementing. This is the model
that lets 100 accept our accept/port-assign — see SecureDatagramReceiver.cs. It is not the
blocker; the terminal-data channel/sequence is.
8. What we KNOW vs ASSUME vs DON'T KNOW¶
VERIFIED (KNOW)¶
- The wire format (§2), the envelope formula fits all captures (§3, §5).
- The handshake works live (accept/port-assign echo + secure ACK on connect+4).
- Terminal-data 102→100 must be on DB; DC/DD crash 100 fatally (§6.1).
- Flags1 must be continuous with our prior frames; 100 rejects out-of-order with XENSE.
- 100 rejects a high-sequence accept (we cannot just start high).
- In the working captures, both machines had high running datagram sequences.
ASSUMPTION (marked — NOT established)¶
- ASSUMPTION A: The datagram sequence (Flags1) is tracked per node-pair (all 102→100 traffic shares one sequence), not per-port-pair. Evidence: in the capture Flags1 is continuous across the TADADM port and the session port. Risk: if it is actually per-connection, the session-port data stream could have its own independent sequence that we could start where we like — which might dissolve the contradiction. This is the single most important thing to verify.
- ASSUMPTION B: The channel is derived from
Base(not independently allocated and then merely consistent-with-Base in the captures). Evidence: the formula fits. Risk: if the channel is actually allocated by XMSG during setup and announced to 100, then we must send on the allocated channel and our Base must be made to match it — different mental model. - ASSUMPTION C: 100 validates the incoming channel by recomputing the formula and comparing; a mismatch (or an out-of-family channel like DD-for-terminal-data) triggers XXPER. Unverified.
UNKNOWN (DON'T KNOW)¶
- The rule XMSG uses to pick the terminal-data channel and the expected Flags1 for a responder whose sequence starts near zero. This is the crux. The captures only show responders with high sequences.
- Whether a fresh responder is even expected to start at zero, or whether real XMSG seeds its
outgoing datagram sequence to some non-zero value at startup (which would explain the captures'
0x012F). - The exact meaning of the XXPER at physical address
134265B(which XMSG routine, which check). - Whether the session port value we assign (
0x0211, logical 4) matters — captures used0x04C2(logical 9). Not yet isolated. - What the counter's semantics are beyond feeding
Base(does 100 track/validate it independently?).
9. IDEAS to test (clearly marked as UNVERIFIED hypotheses)¶
- IDEA 1 (test ASSUMPTION A): capture a
connect-toto a freshly (re)started ND responder whose XMSG sequence is genuinely near zero. If such a responder still sends terminal-data on DB with a low Flags1, then the channel is notBase-derived (ASSUMPTION B is false) and is allocated some other way — decisive. If instead a fresh responder's sequence is not near zero (XMSG seeds it high at startup), then our node should seed its outgoing datagram sequence high at startup and the contradiction dissolves — but we must find what 100 expects that seed to be. - IDEA 2: the datagram sequence may be per-connection (per XMSPT/XMDPT port-pair). The
accept/port-assign ride the TADADM port; the terminal-data rides the session port
0x0211. If 100 tracks a separate sequence for the session-port connection, that stream can start where we choose — possibly high enough for DB — without violating the TADADM stream's continuity. Test: send the DUMM on the session port with a Flags1 that is the first frame of that port-pair (e.g. matching whatever 100 used when it opened the session), and see if XENSE disappears. - IDEA 3: maybe the terminal-data channel is allocated relative to the connect channel, not from Base. Capture: connect D9 → data DB (D9+2) and DC (D9+3). Our connect DA → data would be DC (DA+2)/DD (DA+3) — but we tried DC/DD and they crashed. So this simple offset is likely wrong, or the offset is anchored differently (e.g. always DB regardless of connect channel). Worth re-deriving the offset across all captures with different connect channels.
- IDEA 4: read the XMSG kernel source (XMSG/XROUT NPL symbols) for the routine at the XXPER
address and for where it computes/looks up the Protocol ID of an outbound terminal-data letter.
The repo has NPL sources under
SINTRAN/NPL-SOURCE/— the channel-allocation routine may be there.
10. The precise question for the analyst¶
For a SINTRAN III XMSG responder (node 102) whose XMSG has just started (outgoing datagram sequence near zero), answering a
connect-tofrom node 100: on which Protocol-ID channel, and with which Flags1 (datagram sequence) and sub-header Counter, must the first terminal-data frame (XMCSM 0x01080000, e.g. a DUMM) be sent so that 100 accepts it — given that (a) terminal-data must be on DB, (b) DB via the envelope formula needsBase = Flags1 + Counter ≈ 0x02xx, and (c) 100 XENSE-rejects any Flags1 that is not in-sequence with the low connect it sent?Equivalently: is the datagram sequence per-node-pair or per-connection, and does XMSG seed a responder's outgoing sequence to a non-zero value at startup? Resolving that resolves the MOTD.
Written 2026-07-03. All hex values are from conn-to-d102-from-100.pcapng (decode report lines
2012–2929) and from live console logs against machine 100. Verified/assumed/unknown are marked
throughout; do not trust the ASSUMPTION/IDEA items without independent confirmation.