XMSG / TAD connect-to — MASTER handoff for an expert reviewer¶
Goal: a C# node ("node 102") connects to a real/emulated Norsk Data SINTRAN III
machine ("node 100") over an nd100x --hdlc TCP bridge and serves an interactive
SINTRAN terminal when a user on 100 types @connect-to d102 — banner, prompt, menu,
and (goal) a login. It works fully when 100's XMSG is freshly restarted, but two
failures remain that we have not been able to root-cause ourselves. This document
gathers everything we have verified and states the exact open questions.
Every claim is tagged VERIFIED (observed live against machine 100, or in a capture), HYPOTHESIS (a lead, not confirmed), or UNKNOWN. Where we don't know, we say so.
These are OUR node's bugs. The task is NOT to fix SINTRAN, XROUT or XMSG — those are the ground truth. The task is to find which frame OUR node emits (or fails to emit / retransmit) that the real machine cannot process, by comparing our output to the captured real-responder frames.
0. Environment & artifacts¶
- Link:
nd100x --hdlcpresents machine 100 as a raw HDLC-over-TCP stream. Our node is a TCP client to127.0.0.1:10362(the bridge). Lossless, ordered byte stream. - Decoded captures:
SINTRAN/XMSG/SRC/pcap-decode-report.txt— 13 real captures fully decoded (SINTRAN header, XMSG sub-header, TAD/XROUT/ROUTING chains, raw hex). The raw.pcapngfiles are atE:\Dev\Ronny\X25Emulator\pcap(outside this repo). - Our code:
SINTRAN/XMSG/SRC/Xmsg.Protocol/Packet/XmsgEnvelope.cs— the seed/epoch/channel model.Xmsg.Live/Tad/TadTerminalResponder.cs— builds every reply frame (BuildResponderFrame).Xmsg.Live/LapbLink.cs— the LAPB (link) layer.Xmsg.Live/XmsgNode.cs— frame dispatch.Xmsg.Live.Runner/Program.cs— the runnable node (timestamped logs).- 110 unit tests green (64 protocol + 46 live).
- Companion docs (context):
XMSG-PROTOCOL.md(wire format, section 18.5 = seed model),XMSG-CHANNEL-SEQUENCE-ANALYSIS-2026-07-03.md,XMSG-SEQUENCE-RESTART-ANSWER-2026-07-03.md,XMSG-RECONNECT-CRASH-QUESTION-2026-07-03.md,TAD-CONNECT-FIELD-ANALYSIS.md.
1. Frame format (VERIFIED from all 13 captures)¶
Three stacked layers. Byte offsets below are into the SINTRAN payload (after LAPB de-stuffing / FCS strip).
1.1 LAPB (link) layer¶
- Modulo-8 ABM, balanced (both stations send SABM). Byte-stuffed
0x7Eframing, FCS-16 poly0x8408. - Control: SABM
0x3F, UA0x73, RR0x01 | (N(R)<<5), I-frame(N(R)<<5) | (N(S)<<1). - Address:
0x01for link setup (SABM/UA),0x09for data (RR/I). - Each link-management frame carries the sender's node number as a 2-byte info field:
00 64= 100,00 66= 102,00 67= 103. This is the physical neighbour on the link — always 100↔102 on our link, even when the logical traffic is for node 103 relayed through 100 (see 1.3).
1.2 SINTRAN header (12 bytes)¶
off 0-1 : markers 0x21 0x13 (0x12 relay variant seen; 0x13 normal)
off 2 : packetType 0x00
off 3 : (length/aux) 0x0E on Data
off 4-5 : DEST node big-endian word <-- 102 = 0x0066
off 6-7 : SRC node big-endian word <-- 100 = 0x0064
off 8-9 : Flags1 datagram sequence (per direction, +1 per Data frame)
off 10-11: Flags2 = XMCSM >> 16 (0x0400 on connect-data, 0x0001 on ACK, 0x0108 on TAD)
off 12 : proto/channel D9..DE (the "channel", epoch-dependent — see section 2)
1.3 Node IDs — the two-node distinction (VERIFIED)¶
A node ID is just the decimal node number as a 16-bit big-endian word
(100=0x0064, 102=0x0066, 103=0x0067). It appears four times per Data frame:
SINTRAN header dst (off 4-5) + src (off 6-7), and the XMSG sub-header dstNode:dstPort
+ srcNode:srcPort.
- Direct (
conn-to-d102-from-100.pcapng, report line 2012): header100 -> 102, sub-headersrc 100:683 -> dst 102:0. - Relayed (
conn-to-102-from103-via100.pcapng, report line 6): header103 -> 102, sub-headersrc 103:581 -> dst 102:0. The source is the logical originator (103), NOT the relay (100). On the LAPB link it is still 100↔102.
| Layer | Whose ID | 103-via-100 shows |
|---|---|---|
| LAPB info field | physical neighbour | 100 |
| SINTRAN + XMSG endpoints | logical endpoints | 103 ↔ 102 |
Our responder mirrors this correctly (TadTerminalResponder.cs:525):
DestinationNode = request.Header.SourceNode (echoes 100 or 103),
SourceNode = _nodeNumber (102). VERIFIED correct — not hardcoded to 100.
1.4 XMSG sub-header (Data frames), after the proto byte¶
counter (1) frameFlags (1) role (1) dstNode(2) dstPort(2) srcNode(2) srcPort(2)
XMCSM (4) pad (1) XMLEN (1) <payload: TAD / XROUT / ROUTING chain>
XMCSM is the 32-bit control word. Flags2 == XMCSM>>16. The high two bytes drive the
channel (section 2).
2. The seed / epoch / channel model (VERIFIED against all 601 captured Data frames)¶
Per link there is one seed byte, learned from 100's connect frame:
seed = (Counter + Flags1 + (Flags2 & 0xFF)) & 0xFF ; 100<->102 = 0x14, 100<->103 = 0x13
baseLow = (seed - (Flags2 & 0xFF)) & 0xFF ; Flags2 == XMCSM >> 16
Counter = (baseLow - Flags1) & 0xFF
epoch = (Flags1 - baseLow + 0xFF) >> 8 ; # of Counter wraps
Channel = 0xDE - (XMCSM >> 24) - epoch
XMCSM 0x01080000): channel DD at epoch 0, DC at epoch 1, DB at epoch 2.
A fresh responder (epoch 0) sends terminal data on DD (an earlier DB reading was an
epoch-2 capture artifact).
HYPOTHESIS (2 data points): the seed encodes the node number —
seed = 122 − responderNode (122−102=0x14, 122−103=0x13). Unconfirmed; we currently
learn the seed live so correctness doesn't depend on it. A 100↔104 capture would
confirm or kill this.
Sequence persistence (VERIFIED)¶
- Our outgoing
Flags1(102→100) is one counter, +1 per Data frame, and persists per remote node across our process restarts. It advances only on 100's delivery ACK (subtype 0x03 echoing the Flags1 it received) — never on send — so it can never run ahead of what 100 actually received. File:FileResponderSequenceStore. - 100's expected-from-us (XSRSQ) is persistent on 100's side and independent of 100's own send counter.
- Behind sequence ⇒ 100 silently drops the frame. Ahead ⇒ recoverable XENSE
(subtype 0x07, Flags2
0xFFDE= −34). We recover by stepping our accept Flags1 down one per XENSE until 100 accepts (ResyncAcceptDown). - A ReachabilityRequest from 100 (subtype 0x19, Flags1
0xFFFF) = 100's XMSG-restart signal ⇒ its sequences zero ⇒ we reset our stored Flags1 to 0. A bare HDLC link restart does NOT send one.
3. What WORKS — epoch 0, VERIFIED live end-to-end¶
With 100's XMSG freshly (re)started, a full session runs:
1. @connect-to d102 → connect handshake → === CONNECTION ESTABLISHED / TAD LOGICAL
UNIT NO: 768 ===.
2. Our accept (proto D8/DA per epoch, role 0x40) is accepted; we send session-setup
(port-assign) + DUMM; 100 then drives TMOD/TTYP.
3. We answer the terminal-setup with the MOTD burst (ctrl 0x20, RESE, RESE, MOTD) →
the RETROCORE MOTD banner renders on 100's console.
4. ENTER → # prompt; menu commands reply live: 1→time, 2→date, 3→echo. Each
reply appends an RFI (ready-for-input, TAD 0x02) flow-control credit; without it
100 sits idle after one line.
5. After ~1 minute SINTRAN ends it ("TAD not logged in") with a DCON (TAD 0x09),
which we receive and close cleanly.
Every success had 100's connect at a LOW sequence (Flags1 0x0000–0x0009,
Counter ~0x14, connect channel DA) = epoch 0.
4. OPEN PROBLEM A — terminal reply stalls (LAPB), recovered but root cause UNKNOWN¶
VERIFIED symptom: during an epoch-0 session, some terminal replies (reliably the
help menu and the echo for command 3) stalled. Trace:
[TX] a=0x09 I ns=0 nr=0 body=<help menu> <- we send the reply
[rx-raw] a=0x09 RR nr=0 <- 100 asks for ns=0 again
[rx-raw] a=0x09 RR nr=0 <- ...and again (stuck)
(we never retransmitted) <- DEADLOCK
RR N(R)=0 to request the frame it did
not accept. Our LAPB had no retransmit (RR-only; no REJ/RNR; timer-based retransmit
but the runner passes no tick), so we never resent → both sides wait forever.
FIX applied (commit b832cdd): in LapbLink.HandleSupervisory, when an RR's N(R)
is behind V(S) and repeats (peer genuinely stuck, _lastBehindNr guard), retransmit
_lastUnackedBody. This RECOVERS the deadlock (VERIFIED locally by test; awaiting a
live re-run).
OPEN QUESTION A — the root cause we do NOT know:
Why does 100 fail to accept our reply I-frame the first time, on a lossless ordered TCP stream? Candidates we cannot distinguish: (a) an FCS/byte-stuffing error our encoder makes specifically on the larger reply frames (menu > single echo); (b) a mod-8 window edge (do we violate a k=window limit / send while 100's window is closed?); (c) 100 legitimately REJs and our RR-only handler misreads it. The retransmit masks the symptom; it does not explain the loss. We need the frame that 100 rejects decoded and compared to a captured real-responder frame of the same class.
Our LAPB is also NON-STANDARD in one place (prime suspect): on the FIRST I-frame after
(re)establishing we adopt the peer's sequence (V(R)=frame.N(S), V(S)=frame.N(R))
instead of resetting to 0, on the theory that 100 keeps its V(S)/V(R) across our TCP
reconnect. Standard LAPB resets both sides to 0 on SABM. If that assumption is wrong, we
ack/deliver at the wrong sequence — a sliding-window desync.
5. OPEN PROBLEM B — reconnect crashes 100's XMSG at epoch ≥ 1 (cause UNKNOWN)¶
VERIFIED symptom: reconnecting without restarting 100 (so 100's sequence has climbed) makes 100's connect arrive at a HIGH sequence:
100 connect: Flags1=0x0015 Counter=0xFF Flags2=0x0400 proto=D9 XMCSM=0x04000041 port=0x02B5
-> baseLow low, Flags1 > baseLow => EPOCH 1, connect channel D9
secure ACK : 2113000300640066 0015 0001 DD 09 (channel DD = connect D9 + 4)
accept : 2113000E00640066 0015 0400 D9 FF 2100 8640 0064 02B5 0066 0156 04000041 00 08 01020000 0202000A
SYSTEM MALFUNCTION / PERF_CONNCT:3, then
XMSG fatal error … code 24B … Physical address 134265B, XMFIDO: ABORTS, XMSG not
running. Sometimes preceded by XROUT: Link restarted, LUN 1360B.
The one hard fact: every SUCCESS was epoch 0; every crash-on-reconnect is epoch ≥ 1. We have never validated our epoch-≥1 frame generation against the live machine — we produce those channels/counters purely from the (capture-verified) formula.
Reference for validation: conn-to-d102-from-100.pcapng (report lines 2012–2929)
contains real responder frames at epoch 2 (102→100, Base 0x0214/0x020C) and 100's
frames at epoch 1 (Base 0x010C). Compare our epoch-1 output to these.
OPEN QUESTIONS B:
1. Accept channel at epoch ≥ 1. In the capture the connect is D9 and the real
responder's accept is on D8 (Base 0x0214, epoch 2). Our epoch-1 accept comes out
on D9 (Base 0x0114). Is the accept expected on the connect's own channel, on
connect−1, or strictly on the formula's epoch-derived channel? Does a Counter=0xFF
(epoch-1 wrap) accept crash a real 100?
2. Terminal-data channels/counters at epoch 1 (DC, 0xF* counters) — do they match
what a real responder sends when its Base is 0x01xx?
3. Off-by-one at the epoch boundary — our formula puts Flags1=0x0014→epoch 0
(Counter 0x00) and Flags1=0x0015→epoch 1 (Counter 0xFF). Is the boundary correct
against the three live counter-wrap events in the captures?
6. OPEN PROBLEM C — login flow NOT built (needed to beat the 1-minute timeout)¶
SINTRAN policy: "a TAD not logged in cannot be held for more than 1 minute" → it
DCONs us. To keep the session we must complete the login handshake so SINTRAN considers
the session logged in. We have the opcodes (from captures / TAD-CONNECT-FIELD-ANALYSIS.md)
but have not built the state machine:
- SYCN (0x13) sync steps 0002 → 0003 → 0006 → 000A (000A = logged in).
- CESC (0x0E) 00 → 01.
- ECKM (0x03) echo control: FF = echo off (password entry) then 01.
- RFI (0x02) = ready-for-input credit — already appended to every input-expecting reply.
OPEN QUESTION C: the exact ordering/interleave of SYCN/CESC/ECKM with the PASSWORD
prompt, and which side initiates each step. Needs validation against a login capture
(li-syst-tad-103.pcapng may contain a login sequence — report line ~7771).
7. What we need from the reviewer (priorities)¶
- Problem A root cause — decode the reply I-frame 100 rejects; is it FCS/stuffing on larger frames, a window violation, or a misread REJ? Is the peer-sequence adoption (section 4) correct on a reconnect where 100's link never went down?
- Problem B — the exact accept channel/counter rule at epoch ≥ 1, and any deviation of our epoch-1 frames from the captured real-responder frames (that deviation is the crash).
- Problem C — the login handshake ordering.
All raw evidence is in SRC/pcap-decode-report.txt; our exact frame generation is in
XmsgEnvelope.cs + TadTerminalResponder.BuildResponderFrame + LapbLink.cs.
Written 2026-07-03. Consolidates: XMSG-RECONNECT-CRASH-QUESTION, XMSG-CHANNEL-SEQUENCE- ANALYSIS, XMSG-SEQUENCE-RESTART-ANSWER, TAD-CONNECT-FIELD-ANALYSIS, XMSG-PROTOCOL section 18.5. Every failure documented here is a defect in OUR node, to be found by comparing our output to the captured real-responder frames — not by changing SINTRAN/XROUT/XMSG.