diff --git a/Library/DiscUtils.Iscsi/Connection.cs b/Library/DiscUtils.Iscsi/Connection.cs
index 0b4abff8..173c82ee 100644
--- a/Library/DiscUtils.Iscsi/Connection.cs
+++ b/Library/DiscUtils.Iscsi/Connection.cs
@@ -26,6 +26,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
@@ -46,6 +47,17 @@ internal sealed class Connection : IDisposable
private readonly NetworkStream _stream;
+ ///
+ /// Semaphore to synchronize access to the network stream. NOP-Out responses from the
+ /// keepalive timer and normal Send/ReadPdu traffic must not interleave.
+ ///
+ private readonly SemaphoreSlim _streamSemaphore = new(1, 1);
+
+ ///
+ /// Timer that periodically sends NOP-Out pings to keep the iSCSI session alive.
+ ///
+ private Timer _keepAliveTimer;
+
public Connection(Session session, TargetAddress address, Authenticator[] authenticators)
{
Session = session;
@@ -80,6 +92,14 @@ public Connection(Session session, TargetAddress address, Authenticator[] authen
_negotiatedParameters = [];
NegotiateSecurity();
NegotiateFeatures();
+
+ // Start keepalive timer after login completes.
+ // Send a NOP-Out every 10 seconds to prevent the target from
+ // timing out the session during idle periods.
+ _keepAliveTimer = new Timer(callback: KeepAliveCallback,
+ state: null,
+ dueTime: TimeSpan.FromSeconds(10),
+ period: TimeSpan.FromSeconds(10));
}
internal LoginStages CurrentLoginStage { get; private set; } = LoginStages.SecurityNegotiation;
@@ -106,17 +126,30 @@ public void Close(LogoutReason reason)
{
try
{
- var req = new LogoutRequest(this);
- var packet = req.GetBytes(reason);
- _stream.Write(packet, 0, packet.Length);
- _stream.Flush();
+ _streamSemaphore.Wait();
+ try
+ {
+ // Stop the keepalive timer while holding the semaphore
+ // to ensure no callback is in-flight.
+ _keepAliveTimer?.Dispose();
+ _keepAliveTimer = null;
+
+ var req = new LogoutRequest(this);
+ var packet = req.GetBytes(reason);
+ _stream.Write(packet, 0, packet.Length);
+ _stream.Flush();
- var pdu = ReadPdu();
- var resp = ParseResponse(pdu);
+ var pdu = ReadPdu();
+ var resp = ParseResponse(pdu);
- if (resp.Response != LogoutResponseCode.ClosedSuccessfully)
+ if (resp.Response != LogoutResponseCode.ClosedSuccessfully)
+ {
+ throw new InvalidProtocolException($"Target indicated failure during logout: {resp.Response}");
+ }
+ }
+ finally
{
- throw new InvalidProtocolException($"Target indicated failure during logout: {resp.Response}");
+ _streamSemaphore.Release();
}
}
catch (EndOfStreamException)
@@ -133,6 +166,73 @@ public void Close(LogoutReason reason)
}
}
+ ///
+ /// Timer callback: sends a NOP-Out to keep the iSCSI session alive.
+ /// Uses ITT=0xFFFFFFFF and TTT=0xFFFFFFFF so the target does not send a NOP-In response
+ /// (RFC 3720 §10.18), avoiding read-side contention with the main Send path.
+ ///
+ private async void KeepAliveCallback(object _)
+ {
+ if (!_streamSemaphore.Wait(0))
+ {
+ // A Send or Close is in progress; skip this ping and retry on the next timer tick.
+ return;
+ }
+
+ try
+ {
+#if DEBUG
+ Trace.WriteLine("Sending keep-alive...");
+#endif
+
+ await SendNopOutAsync().ConfigureAwait(false);
+ }
+ catch (IOException)
+ {
+ // Connection is dead — the next Send will surface the error.
+ }
+ catch (ObjectDisposedException)
+ {
+ // Stream was disposed — connection is shutting down.
+ }
+ finally
+ {
+ _streamSemaphore.Release();
+ }
+ }
+
+ private byte[] nopOut;
+
+ ///
+ /// Sends an initiator-initiated NOP-Out with ITT=0xFFFFFFFF and TTT=0xFFFFFFFF.
+ /// This is a "ping" that keeps the iSCSI session alive without requiring a response.
+ ///
+ private async ValueTask SendNopOutAsync()
+ {
+ if (nopOut is null)
+ {
+ nopOut = new byte[48];
+
+ // Byte 0: Immediate (0x40) | OpCode NopOut (0x00)
+ nopOut[0] = 0x40;
+ // Byte 1: Final bit
+ nopOut[1] = 0x80;
+ // Bytes 16-19: ITT = 0xFFFFFFFF (no response expected)
+ EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 16);
+ // Bytes 20-23: TTT = 0xFFFFFFFF
+ EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 20);
+ }
+
+ // Bytes 24-27: CmdSN (not incremented for immediate PDUs)
+ EndianUtilities.WriteBytesBigEndian(Session.CommandSequenceNumber, nopOut, 24);
+ // Bytes 28-31: ExpStatSN
+ EndianUtilities.WriteBytesBigEndian(ExpectedStatusSequenceNumber, nopOut, 28);
+
+ await _stream.WriteAsync(nopOut).ConfigureAwait(false);
+
+ await _stream.FlushAsync().ConfigureAwait(false);
+ }
+
///
/// Sends an SCSI command (aka task) to a LUN via the connected target.
///
@@ -142,145 +242,154 @@ public void Close(LogoutReason reason)
/// The number of bytes received.
public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuffer)
{
- for (var i = 0; ; i++)
+ _streamSemaphore.Wait();
+
+ try
{
- try
+ for (var i = 0; ; i++)
{
- // RFC 3720 Bug #8a: Allocate new Task Tag for this command
- // Originally added because we thought each command must have a unique ITT
- // TESTING RESULT: NOT NECESSARY - Test passes without this call
- // The Session already maintains ITT correctly without explicit increment here
- // Leaving commented for reference in case unique ITT per command is needed in future
- //Session.NextTaskTag();
+ try
+ {
+ // RFC 3720 Bug #8a: Allocate new Task Tag for this command
+ // Originally added because we thought each command must have a unique ITT
+ // TESTING RESULT: NOT NECESSARY - Test passes without this call
+ // The Session already maintains ITT correctly without explicit increment here
+ // Leaving commented for reference in case unique ITT per command is needed in future
+ //Session.NextTaskTag();
- // RFC 3720: DataSN starts at 0 for each new command
- var expectedDataSN = 0u;
+ // RFC 3720: DataSN starts at 0 for each new command
+ var expectedDataSN = 0u;
- var req = new CommandRequest(this, cmd.TargetLun);
+ var req = new CommandRequest(this, cmd.TargetLun);
- var outBufferCount = outBuffer.Length;
- var inBufferMax = inBuffer.Length;
+ var outBufferCount = outBuffer.Length;
+ var inBufferMax = inBuffer.Length;
- var toSend = Math.Min(outBufferCount, Session.ImmediateData ? Session.FirstBurstLength : 0);
+ var toSend = Math.Min(outBufferCount, Session.ImmediateData ? Session.FirstBurstLength : 0);
- // F bit (isFinalData) = true means "no more unsolicited Data-Out PDUs will follow".
- // Even if we're sending < outBufferCount, we set F=1 because any remaining data
- // will be sent via solicited Data-Out (after R2T), not more unsolicited Data-Out.
+ // F bit (isFinalData) = true means "no more unsolicited Data-Out PDUs will follow".
+ // Even if we're sending < outBufferCount, we set F=1 because any remaining data
+ // will be sent via solicited Data-Out (after R2T), not more unsolicited Data-Out.
- // Simpler logic: Use buffer sizes directly
- var expectedTransferLength = outBufferCount != 0 ? (uint)outBufferCount : (uint)inBufferMax;
+ // Simpler logic: Use buffer sizes directly
+ var expectedTransferLength = outBufferCount != 0 ? (uint)outBufferCount : (uint)inBufferMax;
- var packet = req.GetBytes(cmd,
- immediateData: outBuffer.Slice(0, toSend),
- isFinalData: true,
- willRead: inBufferMax != 0,
- willWrite: outBufferCount != 0,
- expected: expectedTransferLength);
+ var packet = req.GetBytes(cmd,
+ immediateData: outBuffer.Slice(0, toSend),
+ isFinalData: true,
+ willRead: inBufferMax != 0,
+ willWrite: outBufferCount != 0,
+ expected: expectedTransferLength);
- _stream.Write(packet, 0, packet.Length);
- _stream.Flush();
+ _stream.Write(packet, 0, packet.Length);
+ _stream.Flush();
- var numSent = toSend;
- while (numSent < outBufferCount)
- {
- var pdu = ReadPdu();
+ var numSent = toSend;
+ while (numSent < outBufferCount)
+ {
+ var pdu = ReadPdu();
- var resp = ParseResponse(pdu);
- var numApproved = (int)resp.DesiredTransferLength;
- var targetTransferTag = resp.TargetTransferTag;
+ var resp = ParseResponse(pdu);
+ var numApproved = (int)resp.DesiredTransferLength;
+ var targetTransferTag = resp.TargetTransferTag;
- // DataSN resets to 0 for each R2T (not continuous across multiple R2Ts)
- var pktsSent = 0;
- while (numApproved > 0)
- {
- toSend = Math.Min(Math.Min(outBufferCount - numSent, numApproved), MaxTargetReceiveDataSegmentLength ?? MaxInitiatorTransmitDataSegmentLength);
-
- var pkt = new DataOutPacket(this, cmd.TargetLun);
- var currentDataSN = pktsSent++;
-
- packet = pkt.GetBytes(data: outBuffer.Slice(numSent, toSend),
- isFinalData: toSend == numApproved,
- dataSeqNumber: currentDataSN,
- bufferOffset: (uint)numSent,
- targetTransferTag: targetTransferTag);
-
- _stream.Write(packet, 0, packet.Length);
- _stream.Flush();
-
- numApproved -= toSend;
- numSent += toSend;
- }
- }
+ // DataSN resets to 0 for each R2T (not continuous across multiple R2Ts)
+ var pktsSent = 0;
+ while (numApproved > 0)
+ {
+ toSend = Math.Min(Math.Min(outBufferCount - numSent, numApproved), MaxTargetReceiveDataSegmentLength ?? MaxInitiatorTransmitDataSegmentLength);
- var isFinal = false;
- var numRead = 0;
- while (!isFinal)
- {
- var pdu = ReadPdu();
+ var pkt = new DataOutPacket(this, cmd.TargetLun);
+ var currentDataSN = pktsSent++;
+
+ packet = pkt.GetBytes(data: outBuffer.Slice(numSent, toSend),
+ isFinalData: toSend == numApproved,
+ dataSeqNumber: currentDataSN,
+ bufferOffset: (uint)numSent,
+ targetTransferTag: targetTransferTag);
+
+ _stream.Write(packet, 0, packet.Length);
+ _stream.Flush();
- if (pdu.OpCode == OpCode.ScsiResponse)
+ numApproved -= toSend;
+ numSent += toSend;
+ }
+ }
+
+ var isFinal = false;
+ var numRead = 0;
+ while (!isFinal)
{
- var resp = ParseResponse(pdu);
+ var pdu = ReadPdu();
- if (resp.StatusPresent && resp.Status == ScsiStatus.CheckCondition)
+ if (pdu.OpCode == OpCode.ScsiResponse)
{
- var senseLength = EndianUtilities.ToUInt16BigEndian(pdu.ContentData, 0);
- var senseData = pdu.ContentData.AsSpan(2, senseLength).ToArray();
+ var resp = ParseResponse(pdu);
- if (i == 0 && ScsiSenseParser.TryParse(senseData, out var sense) && sense.IndicatesRetryRequired)
+ if (resp.StatusPresent && resp.Status == ScsiStatus.CheckCondition)
{
- goto retry;
- }
+ var senseLength = EndianUtilities.ToUInt16BigEndian(pdu.ContentData, 0);
+ var senseData = pdu.ContentData.AsSpan(2, senseLength).ToArray();
- throw new ScsiCommandException(resp.Status, senseData);
- }
+ if (i == 0 && ScsiSenseParser.TryParse(senseData, out var sense) && sense.IndicatesRetryRequired)
+ {
+ goto retry;
+ }
- if (resp.StatusPresent && resp.Status != ScsiStatus.Good)
- {
- throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure");
- }
+ throw new ScsiCommandException(resp.Status, senseData);
+ }
- isFinal = resp.Header.FinalPdu;
- }
- else if (pdu.OpCode == OpCode.ScsiDataIn)
- {
- var resp = ParseResponse(pdu);
+ if (resp.StatusPresent && resp.Status != ScsiStatus.Good)
+ {
+ throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure");
+ }
- // RFC 3720 Section 10.7.4: Validate DataSN sequence
- if (resp.DataSequenceNumber != expectedDataSN)
- {
- throw new InvalidProtocolException($"DataSN mismatch: received {resp.DataSequenceNumber}, expected {expectedDataSN}");
+ isFinal = resp.Header.FinalPdu;
}
-
- if (resp.StatusPresent && resp.Status != ScsiStatus.Good)
+ else if (pdu.OpCode == OpCode.ScsiDataIn)
{
- throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure");
- }
+ var resp = ParseResponse(pdu);
- if (resp.ReadData != null)
- {
- resp.ReadData.AsSpan().CopyTo(inBuffer.Slice((int)resp.BufferOffset));
- numRead += resp.ReadData.Length;
- }
+ // RFC 3720 Section 10.7.4: Validate DataSN sequence
+ if (resp.DataSequenceNumber != expectedDataSN)
+ {
+ throw new InvalidProtocolException($"DataSN mismatch: received {resp.DataSequenceNumber}, expected {expectedDataSN}");
+ }
- isFinal = resp.Header.FinalPdu;
+ if (resp.StatusPresent && resp.Status != ScsiStatus.Good)
+ {
+ throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure");
+ }
- expectedDataSN++;
+ if (resp.ReadData != null)
+ {
+ resp.ReadData.AsSpan().CopyTo(inBuffer.Slice((int)resp.BufferOffset));
+ numRead += resp.ReadData.Length;
+ }
+
+ isFinal = resp.Header.FinalPdu;
+
+ expectedDataSN++;
+ }
}
- }
- return Math.Max(numRead, numSent);
- }
- finally
- {
- Session.NextTaskTag();
- Session.NextCommandSequenceNumber();
- }
+ return Math.Max(numRead, numSent);
+ }
+ finally
+ {
+ Session.NextTaskTag();
+ Session.NextCommandSequenceNumber();
+ }
- retry:
- {
+ retry:
+ {
+ }
}
}
+ finally
+ {
+ _streamSemaphore.Release();
+ }
}
///
@@ -293,123 +402,132 @@ public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuff
/// The number of bytes received.
public async ValueTask SendAsync(ScsiCommand cmd, ReadOnlyMemory outBuffer, Memory inBuffer, CancellationToken cancellationToken)
{
- for (var i = 0; ; i++)
+ await _streamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
{
- try
+ for (var i = 0; ; i++)
{
- // RFC 3720: DataSN starts at 0 for each new command
- var expectedDataSN = 0u;
+ try
+ {
+ // RFC 3720: DataSN starts at 0 for each new command
+ var expectedDataSN = 0u;
- var req = new CommandRequest(this, cmd.TargetLun);
+ var req = new CommandRequest(this, cmd.TargetLun);
- var outBufferCount = outBuffer.Length;
- var inBufferMax = inBuffer.Length;
+ var outBufferCount = outBuffer.Length;
+ var inBufferMax = inBuffer.Length;
- var toSend = Math.Min(outBufferCount, Session.ImmediateData ? Session.FirstBurstLength : 0);
+ var toSend = Math.Min(outBufferCount, Session.ImmediateData ? Session.FirstBurstLength : 0);
- // F bit (isFinalData) = true means "no more unsolicited Data-Out PDUs will follow".
- // Even if we're sending < outBufferCount, we set F=1 because any remaining data
- // will be sent via solicited Data-Out (after R2T), not more unsolicited Data-Out.
+ // F bit (isFinalData) = true means "no more unsolicited Data-Out PDUs will follow".
+ // Even if we're sending < outBufferCount, we set F=1 because any remaining data
+ // will be sent via solicited Data-Out (after R2T), not more unsolicited Data-Out.
- // Simpler logic: Use buffer sizes directly
- var expectedTransferLength = outBufferCount != 0 ? (uint)outBufferCount : (uint)inBufferMax;
+ // Simpler logic: Use buffer sizes directly
+ var expectedTransferLength = outBufferCount != 0 ? (uint)outBufferCount : (uint)inBufferMax;
- var packet = req.GetBytes(cmd, outBuffer.Span.Slice(0, toSend), true, inBufferMax != 0, outBufferCount != 0, expectedTransferLength);
- await _stream.WriteAsync(packet, cancellationToken).ConfigureAwait(false);
- await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
- var numSent = toSend;
- var pktsSent = 0;
- while (numSent < outBufferCount)
- {
- var pdu = await ReadPduAsync(cancellationToken).ConfigureAwait(false);
+ var packet = req.GetBytes(cmd, outBuffer.Span.Slice(0, toSend), true, inBufferMax != 0, outBufferCount != 0, expectedTransferLength);
+ await _stream.WriteAsync(packet, cancellationToken).ConfigureAwait(false);
+ await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
+ var numSent = toSend;
+ var pktsSent = 0;
+ while (numSent < outBufferCount)
+ {
+ var pdu = await ReadPduAsync(cancellationToken).ConfigureAwait(false);
- var resp = ParseResponse(pdu);
- var numApproved = (int)resp.DesiredTransferLength;
- var targetTransferTag = resp.TargetTransferTag;
+ var resp = ParseResponse(pdu);
+ var numApproved = (int)resp.DesiredTransferLength;
+ var targetTransferTag = resp.TargetTransferTag;
- while (numApproved > 0)
- {
- toSend = Math.Min(Math.Min(outBufferCount - numSent, numApproved), MaxTargetReceiveDataSegmentLength ?? MaxInitiatorTransmitDataSegmentLength);
+ while (numApproved > 0)
+ {
+ toSend = Math.Min(Math.Min(outBufferCount - numSent, numApproved), MaxTargetReceiveDataSegmentLength ?? MaxInitiatorTransmitDataSegmentLength);
- var pkt = new DataOutPacket(this, cmd.TargetLun);
- var currentDataSN = pktsSent++;
- packet = pkt.GetBytes(outBuffer.Span.Slice(numSent, toSend), toSend == numApproved, currentDataSN, (uint)numSent, targetTransferTag);
- await _stream.WriteAsync(packet, cancellationToken).ConfigureAwait(false);
- await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
+ var pkt = new DataOutPacket(this, cmd.TargetLun);
+ var currentDataSN = pktsSent++;
+ packet = pkt.GetBytes(outBuffer.Span.Slice(numSent, toSend), toSend == numApproved, currentDataSN, (uint)numSent, targetTransferTag);
+ await _stream.WriteAsync(packet, cancellationToken).ConfigureAwait(false);
+ await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
- numApproved -= toSend;
- numSent += toSend;
+ numApproved -= toSend;
+ numSent += toSend;
+ }
}
- }
- var isFinal = false;
- var numRead = 0;
- while (!isFinal)
- {
- var pdu = await ReadPduAsync(cancellationToken).ConfigureAwait(false);
-
- if (pdu.OpCode == OpCode.ScsiResponse)
+ var isFinal = false;
+ var numRead = 0;
+ while (!isFinal)
{
- var resp = ParseResponse(pdu);
+ var pdu = await ReadPduAsync(cancellationToken).ConfigureAwait(false);
- if (resp.StatusPresent && resp.Status == ScsiStatus.CheckCondition)
+ if (pdu.OpCode == OpCode.ScsiResponse)
{
- var senseLength = EndianUtilities.ToUInt16BigEndian(pdu.ContentData, 0);
- var senseData = pdu.ContentData.AsSpan(2, senseLength).ToArray();
+ var resp = ParseResponse(pdu);
- if (i == 0 && ScsiSenseParser.TryParse(senseData, out var sense) && sense.IndicatesRetryRequired)
+ if (resp.StatusPresent && resp.Status == ScsiStatus.CheckCondition)
{
- goto retry;
+ var senseLength = EndianUtilities.ToUInt16BigEndian(pdu.ContentData, 0);
+ var senseData = pdu.ContentData.AsSpan(2, senseLength).ToArray();
+
+ if (i == 0 && ScsiSenseParser.TryParse(senseData, out var sense) && sense.IndicatesRetryRequired)
+ {
+ goto retry;
+ }
+
+ throw new ScsiCommandException(resp.Status, senseData);
}
- throw new ScsiCommandException(resp.Status, senseData);
- }
+ if (resp.StatusPresent && resp.Status != ScsiStatus.Good)
+ {
+ throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure");
+ }
- if (resp.StatusPresent && resp.Status != ScsiStatus.Good)
- {
- throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure");
+ isFinal = resp.Header.FinalPdu;
}
+ else if (pdu.OpCode == OpCode.ScsiDataIn)
+ {
+ var resp = ParseResponse(pdu);
- isFinal = resp.Header.FinalPdu;
- }
- else if (pdu.OpCode == OpCode.ScsiDataIn)
- {
- var resp = ParseResponse(pdu);
+ // RFC 3720 Section 10.7.4: Validate DataSN sequence
+ if (resp.DataSequenceNumber != expectedDataSN)
+ {
+ throw new InvalidProtocolException($"DataSN mismatch: received {resp.DataSequenceNumber}, expected {expectedDataSN}");
+ }
+ expectedDataSN++;
- // RFC 3720 Section 10.7.4: Validate DataSN sequence
- if (resp.DataSequenceNumber != expectedDataSN)
- {
- throw new InvalidProtocolException($"DataSN mismatch: received {resp.DataSequenceNumber}, expected {expectedDataSN}");
- }
- expectedDataSN++;
+ if (resp.StatusPresent && resp.Status != ScsiStatus.Good)
+ {
+ throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure");
+ }
- if (resp.StatusPresent && resp.Status != ScsiStatus.Good)
- {
- throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure");
- }
+ if (resp.ReadData != null)
+ {
+ resp.ReadData.CopyTo(inBuffer.Slice((int)resp.BufferOffset));
+ numRead += resp.ReadData.Length;
+ }
- if (resp.ReadData != null)
- {
- resp.ReadData.CopyTo(inBuffer.Slice((int)resp.BufferOffset));
- numRead += resp.ReadData.Length;
+ isFinal = resp.Header.FinalPdu;
}
-
- isFinal = resp.Header.FinalPdu;
}
- }
- return Math.Max(numRead, numSent);
- }
- finally
- {
- Session.NextTaskTag();
- Session.NextCommandSequenceNumber();
- }
+ return Math.Max(numRead, numSent);
+ }
+ finally
+ {
+ Session.NextTaskTag();
+ Session.NextCommandSequenceNumber();
+ }
- retry:
- {
+ retry:
+ {
+ }
}
}
+ finally
+ {
+ _streamSemaphore.Release();
+ }
}
public T Send(ScsiCommand cmd, ReadOnlySpan buffer, int expected)
@@ -816,32 +934,115 @@ private void NegotiateFeatures()
private ProtocolDataUnit ReadPdu()
{
- var pdu = ProtocolDataUnit.ReadFrom(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None);
-
- if (pdu.OpCode == OpCode.Reject)
+ const int MaxNopInRetries = 16;
+ for (var nopCount = 0; ; nopCount++)
{
- var pkt = new RejectPacket();
- pkt.Parse(pdu);
+ var pdu = ProtocolDataUnit.ReadFrom(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None);
- throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}");
- }
+ if (pdu.OpCode == OpCode.Reject)
+ {
+ var pkt = new RejectPacket();
+ pkt.Parse(pdu);
+
+ throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}");
+ }
+
+ // RFC 3720 §10.19: Handle target-initiated NOP-In (ping).
+ // Respond with NOP-Out echoing the Target Transfer Tag, then
+ // continue reading the next PDU.
+ if (pdu.OpCode == OpCode.NopIn)
+ {
+ if (nopCount >= MaxNopInRetries)
+ {
+ throw new InvalidProtocolException($"Received {nopCount} consecutive NOP-In PDUs without a command response");
+ }
+
+ HandleNopIn(pdu);
+ continue;
+ }
- return pdu;
+ return pdu;
+ }
}
private async ValueTask ReadPduAsync(CancellationToken cancellationToken)
{
- var pdu = await ProtocolDataUnit.ReadFromAsync(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None, cancellationToken).ConfigureAwait(false);
-
- if (pdu.OpCode == OpCode.Reject)
+ const int MaxNopInRetries = 16;
+ for (var nopCount = 0; ; nopCount++)
{
- var pkt = new RejectPacket();
- pkt.Parse(pdu);
+ var pdu = await ProtocolDataUnit.ReadFromAsync(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None, cancellationToken).ConfigureAwait(false);
+
+ if (pdu.OpCode == OpCode.Reject)
+ {
+ var pkt = new RejectPacket();
+ pkt.Parse(pdu);
+
+ throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}");
+ }
+
+ if (pdu.OpCode == OpCode.NopIn)
+ {
+ if (nopCount >= MaxNopInRetries)
+ {
+ throw new InvalidProtocolException($"Received {nopCount} consecutive NOP-In PDUs without a command response");
+ }
- throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}");
+ HandleNopIn(pdu);
+ continue;
+ }
+
+ return pdu;
}
+ }
+
+ ///
+ /// Responds to a target-initiated NOP-In with a NOP-Out (RFC 3720 §10.18/10.19).
+ /// Target-initiated NOP-In has TTT != 0xFFFFFFFF and requires a NOP-Out reply.
+ /// The StatSN in a target-initiated NOP-In is informational and does NOT consume
+ /// a sequence number, so we must NOT call SeenStatusSequenceNumber here.
+ ///
+ private void HandleNopIn(ProtocolDataUnit pdu)
+ {
+ var headerData = pdu.HeaderData;
- return pdu;
+ // Target Transfer Tag at offset 20
+ var targetTransferTag = EndianUtilities.ToUInt32BigEndian(headerData.AsSpan(20));
+
+ // If TTT is 0xFFFFFFFF, this is a response to our own NOP-Out (unsolicited);
+ // no reply needed.
+ if (targetTransferTag == 0xFFFFFFFF)
+ {
+ return;
+ }
+
+ // LUN at offset 8
+ var lun = EndianUtilities.ToUInt64BigEndian(headerData.AsSpan(8));
+
+ // NOTE: We intentionally do NOT call SeenStatusSequenceNumber() here.
+ // RFC 3720 §10.19: A target-initiated NOP-In carries a StatSN, but this
+ // StatSN is NOT consumed — it's the same value the target will use for the
+ // next real command response. Advancing ExpectedStatusSequenceNumber here
+ // would cause the next ParseResponse to fail with a sequence mismatch.
+
+ // Build NOP-Out response (RFC 3720 §10.18)
+ var nopOut = new byte[48];
+ // Byte 0: Immediate bit (0x40) | OpCode NopOut (0x00)
+ nopOut[0] = 0x40;
+ // Byte 1: Final bit (0x80)
+ nopOut[1] = 0x80;
+ // Bytes 8-15: LUN (copy from NOP-In)
+ EndianUtilities.WriteBytesBigEndian(lun, nopOut, 8);
+ // Bytes 16-19: Initiator Task Tag = 0xFFFFFFFF (response to target ping)
+ EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 16);
+ // Bytes 20-23: Target Transfer Tag (echo from NOP-In)
+ EndianUtilities.WriteBytesBigEndian(targetTransferTag, nopOut, 20);
+ // Bytes 24-27: CmdSN (not consumed for immediate PDUs with ITT=0xFFFFFFFF)
+ EndianUtilities.WriteBytesBigEndian(Session.CommandSequenceNumber, nopOut, 24);
+ // Bytes 28-31: ExpStatSN
+ EndianUtilities.WriteBytesBigEndian(ExpectedStatusSequenceNumber, nopOut, 28);
+
+ _stream.Write(nopOut, 0, nopOut.Length);
+ _stream.Flush();
}
private void GetParametersToNegotiate(TextBuffer parameters, KeyUsagePhase phase, SessionType sessionType)