fix(ab-transport): RegisterSession carries a zero sender context (found by Task 17 controllogix)

ab_server's RegisterSession sanity check rejects any non-zero sender context
with encapsulation status 1 and a header-only reply, so every native session
against the simulator failed with BadCommunicationError. libplctag sends zero
(logix-register-session.hex); the codec test now matches that capture byte for
byte instead of excluding the context, and a client test pins the zero on the wire.
This commit is contained in:
Joseph Doherty
2026-10-07 17:20:06 -04:00
parent 72c73f525e
commit 82d011c7a8
4 changed files with 24 additions and 11 deletions
@@ -82,7 +82,7 @@ public sealed class EncapsulationClient : IAsyncDisposable
try
{
var (header, _) = await client.ExchangeAsync(connected: false,
(ctx, _) => EncapsulationCodec.RegisterSession(ctx), timeout, ct).ConfigureAwait(false);
(_, _) => EncapsulationCodec.RegisterSession(), timeout, ct).ConfigureAwait(false);
client.SessionHandle = header.SessionHandle;
return client;
}
@@ -30,9 +30,13 @@ public static class EncapsulationCodec
return w.ToArray();
}
/// <summary>RegisterSession: protocol version 1, option flags 0.</summary>
public static byte[] RegisterSession(ulong ctx) =>
Finish(Header(EncapCommand.RegisterSession, 0, ctx).U16(1).U16(0));
/// <summary>RegisterSession: protocol version 1, option flags 0, and a <b>zero</b> sender context.</summary>
/// <remarks>The sender context is always zero, as libplctag sends it (<c>logix-register-session.hex</c>):
/// ab_server's RegisterSession sanity check rejects any non-zero context with encapsulation status 1
/// and a header-only reply, so a per-request context here failed every native session against the
/// simulator. The reply is still matched — unconnected matching tolerates a zero context.</remarks>
public static byte[] RegisterSession() =>
Finish(Header(EncapCommand.RegisterSession, 0, 0).U16(1).U16(0));
/// <summary>UnRegisterSession: header only, no data.</summary>
public static byte[] UnRegisterSession(uint session, ulong ctx) =>
@@ -32,6 +32,16 @@ public sealed class EncapsulationClientTests
FakeEipServer.Command(server.Received[0]).ShouldBe(EncapCommand.RegisterSession);
}
[Fact]
public async Task RegisterSession_goes_on_the_wire_with_a_zero_sender_context()
{
// ab_server refuses RegisterSession with any non-zero sender context (status 1, header-only
// reply), so the client must not stamp its per-request context on it. Found by Task 17.
await using var server = new FakeEipServer();
await using var client = await Connect(server);
EncapsulationCodec.ReadHeader(server.Received[0]).SenderContext.ShouldBe(0UL);
}
[Fact]
public async Task SendRRData_returns_cip_payload()
{
@@ -10,8 +10,8 @@ public sealed class EncapsulationCodecTests
[Fact]
public void RegisterSession_frame_is_byte_exact() =>
Convert.ToHexString(EncapsulationCodec.RegisterSession(Ctx)).ShouldBe(
"6500" + "0400" + "00000000" + "00000000" + "0102030405060708" + "00000000" + "0100" + "0000");
Convert.ToHexString(EncapsulationCodec.RegisterSession()).ShouldBe(
"6500" + "0400" + "00000000" + "00000000" + "0000000000000000" + "00000000" + "0100" + "0000");
[Fact]
public void UnRegisterSession_frame_is_header_only() =>
@@ -43,13 +43,12 @@ public sealed class EncapsulationCodecTests
.Status.ShouldBe(AbStatus.TooLarge);
[Fact]
public void RegisterSession_matches_libplctag_capture_except_context()
public void RegisterSession_matches_libplctag_capture_byte_for_byte()
{
// Including the sender context: ab_server rejects a non-zero RegisterSession context with
// encapsulation status 1 (found by Task 17 controllogix — every native session failed).
var captured = CaptureFile.Load("logix-register-session.hex").First(f => f.FromClient).Bytes;
var ours = EncapsulationCodec.RegisterSession(Ctx);
ours.Length.ShouldBe(captured.Length);
ours.AsSpan(0, 12).ToArray().ShouldBe(captured.AsSpan(0, 12).ToArray()); // cmd,len,session,status
ours.AsSpan(20).ToArray().ShouldBe(captured.AsSpan(20).ToArray()); // options + data
Convert.ToHexString(EncapsulationCodec.RegisterSession()).ShouldBe(Convert.ToHexString(captured));
}
[Fact]