mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-21 07:24:25 +00:00
Windows iSCSI Initiator compatibility: - Add TargetPortalGroupTag to login response (RFC 7143 S13.9) - Add REQUEST_SENSE, START_STOP_UNIT, MODE_SELECT(6/10) handlers - Add PERSISTENT_RESERVE_IN/OUT, MAINTENANCE_IN (REPORT SUPPORTED OPCODES) - Implement MODE SENSE caching (page 0x08) and control (page 0x0A) pages - Fix Data-In residual underflow/overflow flags (U/O bits on final PDU) - Rename ScsiReadCapacity16 -> ScsiServiceActionIn16 for correctness Instrumentation and tooling: - Add instrumentedAdapter with periodic PERF stats logging - Add pprof endpoints on admin HTTP server (/debug/pprof/*) - Add blockbench CLI tool for standalone block device benchmarking - Add SCSI CDB debug logging in session dispatch HA integration fixes: - Move HA test replica ports to 9011-9014 to avoid conflicts - Add QA adversarial tests for Phase 4A CP4b-4 (755 lines) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
306 lines
8.2 KiB
Go
306 lines
8.2 KiB
Go
package iscsi
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
)
|
|
|
|
var (
|
|
ErrDataSNOrder = errors.New("iscsi: Data-Out DataSN out of order")
|
|
ErrDataOffsetOrder = errors.New("iscsi: Data-Out buffer offset out of order")
|
|
ErrDataOverflow = errors.New("iscsi: data exceeds expected transfer length")
|
|
ErrDataIncomplete = errors.New("iscsi: data transfer incomplete")
|
|
)
|
|
|
|
// DataInWriter splits a read response into multiple Data-In PDUs, respecting
|
|
// MaxRecvDataSegmentLength. The final PDU carries the S-bit (status) and F-bit.
|
|
type DataInWriter struct {
|
|
maxSegLen uint32 // negotiated MaxRecvDataSegmentLength
|
|
}
|
|
|
|
// NewDataInWriter creates a writer with the given max segment length.
|
|
func NewDataInWriter(maxSegLen uint32) *DataInWriter {
|
|
if maxSegLen == 0 {
|
|
maxSegLen = 8192 // sensible default
|
|
}
|
|
return &DataInWriter{maxSegLen: maxSegLen}
|
|
}
|
|
|
|
// WriteDataIn splits data into Data-In PDUs and writes them to w.
|
|
// itt is the initiator task tag, edtl is expected data transfer length.
|
|
// statSN is the current StatSN (incremented when S-bit is set).
|
|
// Returns the number of PDUs written.
|
|
func (d *DataInWriter) WriteDataIn(w io.Writer, data []byte, itt, edtl uint32, expCmdSN, maxCmdSN uint32, statSN *uint32) (int, error) {
|
|
totalLen := uint32(len(data))
|
|
if totalLen == 0 {
|
|
// Zero-length read -- send single Data-In with S-bit, no data
|
|
pdu := &PDU{}
|
|
pdu.SetOpcode(OpSCSIDataIn)
|
|
flags := uint8(FlagF | FlagS)
|
|
if edtl > 0 {
|
|
flags |= FlagU
|
|
pdu.SetResidualCount(edtl)
|
|
}
|
|
pdu.SetOpSpecific1(flags)
|
|
pdu.SetInitiatorTaskTag(itt)
|
|
pdu.SetTargetTransferTag(0xFFFFFFFF)
|
|
pdu.SetStatSN(*statSN)
|
|
*statSN++
|
|
pdu.SetExpCmdSN(expCmdSN)
|
|
pdu.SetMaxCmdSN(maxCmdSN)
|
|
pdu.SetDataSN(0)
|
|
pdu.SetSCSIStatus(SCSIStatusGood)
|
|
if err := WritePDU(w, pdu); err != nil {
|
|
return 0, err
|
|
}
|
|
return 1, nil
|
|
}
|
|
|
|
var offset uint32
|
|
var dataSN uint32
|
|
count := 0
|
|
|
|
for offset < totalLen {
|
|
segLen := d.maxSegLen
|
|
if offset+segLen > totalLen {
|
|
segLen = totalLen - offset
|
|
}
|
|
isFinal := (offset + segLen) >= totalLen
|
|
|
|
pdu := &PDU{}
|
|
pdu.SetOpcode(OpSCSIDataIn)
|
|
pdu.SetInitiatorTaskTag(itt)
|
|
pdu.SetTargetTransferTag(0xFFFFFFFF)
|
|
pdu.SetExpCmdSN(expCmdSN)
|
|
pdu.SetMaxCmdSN(maxCmdSN)
|
|
pdu.SetDataSN(dataSN)
|
|
pdu.SetBufferOffset(offset)
|
|
|
|
pdu.DataSegment = data[offset : offset+segLen]
|
|
|
|
if isFinal {
|
|
flags := uint8(FlagF | FlagS)
|
|
if totalLen < edtl {
|
|
flags |= FlagU
|
|
pdu.SetResidualCount(edtl - totalLen)
|
|
} else if totalLen > edtl {
|
|
flags |= FlagO
|
|
pdu.SetResidualCount(totalLen - edtl)
|
|
}
|
|
pdu.SetOpSpecific1(flags)
|
|
pdu.SetStatSN(*statSN)
|
|
*statSN++
|
|
pdu.SetSCSIStatus(SCSIStatusGood)
|
|
} else {
|
|
pdu.SetOpSpecific1(0) // no flags
|
|
}
|
|
|
|
if err := WritePDU(w, pdu); err != nil {
|
|
return count, err
|
|
}
|
|
count++
|
|
dataSN++
|
|
offset += segLen
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// DataOutCollector collects Data-Out PDUs for a single write command,
|
|
// assembling the full data buffer from potentially multiple PDUs.
|
|
// It handles both immediate data and R2T-solicited data.
|
|
type DataOutCollector struct {
|
|
expectedLen uint32
|
|
buf []byte
|
|
received uint32
|
|
nextDataSN uint32
|
|
done bool
|
|
}
|
|
|
|
// NewDataOutCollector creates a collector expecting the given total transfer length.
|
|
func NewDataOutCollector(expectedLen uint32) *DataOutCollector {
|
|
return &DataOutCollector{
|
|
expectedLen: expectedLen,
|
|
buf: make([]byte, expectedLen),
|
|
}
|
|
}
|
|
|
|
// AddImmediateData adds the data from the SCSI Command PDU (immediate data).
|
|
func (c *DataOutCollector) AddImmediateData(data []byte) error {
|
|
if uint32(len(data)) > c.expectedLen {
|
|
return ErrDataOverflow
|
|
}
|
|
copy(c.buf, data)
|
|
c.received += uint32(len(data))
|
|
if c.received >= c.expectedLen {
|
|
c.done = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddDataOut processes a Data-Out PDU and adds its data to the buffer.
|
|
func (c *DataOutCollector) AddDataOut(pdu *PDU) error {
|
|
dataSN := pdu.DataSN()
|
|
if dataSN != c.nextDataSN {
|
|
return ErrDataSNOrder
|
|
}
|
|
c.nextDataSN++
|
|
|
|
offset := pdu.BufferOffset()
|
|
data := pdu.DataSegment
|
|
end := offset + uint32(len(data))
|
|
|
|
if end > c.expectedLen {
|
|
return ErrDataOverflow
|
|
}
|
|
|
|
// Validate buffer offset matches received position (DataPDUInOrder/DataSequenceInOrder)
|
|
if offset != c.received {
|
|
return ErrDataOffsetOrder
|
|
}
|
|
|
|
copy(c.buf[offset:], data)
|
|
c.received += uint32(len(data))
|
|
|
|
// Check F-bit
|
|
if pdu.OpSpecific1()&FlagF != 0 {
|
|
c.done = true
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Done returns true if all expected data has been received.
|
|
func (c *DataOutCollector) Done() bool { return c.done }
|
|
|
|
// Data returns the assembled data buffer.
|
|
func (c *DataOutCollector) Data() []byte { return c.buf }
|
|
|
|
// Remaining returns how many bytes are still needed.
|
|
func (c *DataOutCollector) Remaining() uint32 {
|
|
if c.received >= c.expectedLen {
|
|
return 0
|
|
}
|
|
return c.expectedLen - c.received
|
|
}
|
|
|
|
// BuildR2T creates an R2T PDU requesting more data from the initiator.
|
|
// StatSN is NOT set here -- txLoop assigns it (statSNCopy mode, no increment).
|
|
func BuildR2T(itt, ttt uint32, r2tSN uint32, bufferOffset, desiredLen uint32, expCmdSN, maxCmdSN uint32) *PDU {
|
|
pdu := &PDU{}
|
|
pdu.SetOpcode(OpR2T)
|
|
pdu.SetOpSpecific1(FlagF) // always Final for R2T
|
|
pdu.SetInitiatorTaskTag(itt)
|
|
pdu.SetTargetTransferTag(ttt)
|
|
pdu.SetExpCmdSN(expCmdSN)
|
|
pdu.SetMaxCmdSN(maxCmdSN)
|
|
pdu.SetR2TSN(r2tSN)
|
|
pdu.SetBufferOffset(bufferOffset)
|
|
pdu.SetDesiredDataLength(desiredLen)
|
|
return pdu
|
|
}
|
|
|
|
// SendSCSIResponse sends a SCSI Response PDU with optional sense data.
|
|
func SendSCSIResponse(w io.Writer, result SCSIResult, itt uint32, statSN *uint32, expCmdSN, maxCmdSN uint32) error {
|
|
pdu := BuildSCSIResponse(result, itt, expCmdSN, maxCmdSN)
|
|
pdu.SetStatSN(*statSN)
|
|
*statSN++
|
|
return WritePDU(w, pdu)
|
|
}
|
|
|
|
// BuildSCSIResponse creates a SCSI Response PDU without writing it.
|
|
// StatSN is NOT set -- the caller (or txLoop) assigns it.
|
|
func BuildSCSIResponse(result SCSIResult, itt uint32, expCmdSN, maxCmdSN uint32) *PDU {
|
|
pdu := &PDU{}
|
|
pdu.SetOpcode(OpSCSIResp)
|
|
pdu.SetOpSpecific1(FlagF) // Final
|
|
pdu.SetSCSIResponse(ISCSIRespCompleted)
|
|
pdu.SetSCSIStatus(result.Status)
|
|
pdu.SetInitiatorTaskTag(itt)
|
|
pdu.SetExpCmdSN(expCmdSN)
|
|
pdu.SetMaxCmdSN(maxCmdSN)
|
|
|
|
if result.Status == SCSIStatusCheckCond {
|
|
senseData := BuildSenseData(result.SenseKey, result.SenseASC, result.SenseASCQ)
|
|
pdu.DataSegment = make([]byte, 2+len(senseData))
|
|
pdu.DataSegment[0] = byte(len(senseData) >> 8)
|
|
pdu.DataSegment[1] = byte(len(senseData))
|
|
copy(pdu.DataSegment[2:], senseData)
|
|
}
|
|
|
|
return pdu
|
|
}
|
|
|
|
// BuildDataInPDUs splits data into Data-In PDUs and returns them.
|
|
// edtl is the Expected Data Transfer Length from the SCSI command PDU.
|
|
// StatSN is NOT set on the final PDU -- the txLoop assigns it.
|
|
// Intermediate PDUs (without S-bit) never carry StatSN.
|
|
func (d *DataInWriter) BuildDataInPDUs(data []byte, itt, edtl uint32, expCmdSN, maxCmdSN uint32) []*PDU {
|
|
totalLen := uint32(len(data))
|
|
if totalLen == 0 {
|
|
pdu := &PDU{}
|
|
pdu.SetOpcode(OpSCSIDataIn)
|
|
flags := uint8(FlagF | FlagS)
|
|
if edtl > 0 {
|
|
flags |= FlagU // underflow: expected data but got none
|
|
pdu.SetResidualCount(edtl)
|
|
}
|
|
pdu.SetOpSpecific1(flags)
|
|
pdu.SetInitiatorTaskTag(itt)
|
|
pdu.SetTargetTransferTag(0xFFFFFFFF)
|
|
pdu.SetExpCmdSN(expCmdSN)
|
|
pdu.SetMaxCmdSN(maxCmdSN)
|
|
pdu.SetDataSN(0)
|
|
pdu.SetSCSIStatus(SCSIStatusGood)
|
|
return []*PDU{pdu}
|
|
}
|
|
|
|
var pdus []*PDU
|
|
var offset uint32
|
|
var dataSN uint32
|
|
|
|
for offset < totalLen {
|
|
segLen := d.maxSegLen
|
|
if offset+segLen > totalLen {
|
|
segLen = totalLen - offset
|
|
}
|
|
isFinal := (offset + segLen) >= totalLen
|
|
|
|
pdu := &PDU{}
|
|
pdu.SetOpcode(OpSCSIDataIn)
|
|
pdu.SetInitiatorTaskTag(itt)
|
|
pdu.SetTargetTransferTag(0xFFFFFFFF)
|
|
pdu.SetExpCmdSN(expCmdSN)
|
|
pdu.SetMaxCmdSN(maxCmdSN)
|
|
pdu.SetDataSN(dataSN)
|
|
pdu.SetBufferOffset(offset)
|
|
|
|
// Copy data segment (don't share slice with caller).
|
|
seg := make([]byte, segLen)
|
|
copy(seg, data[offset:offset+segLen])
|
|
pdu.DataSegment = seg
|
|
|
|
if isFinal {
|
|
flags := uint8(FlagF | FlagS)
|
|
// RFC 7143: set underflow/overflow residual on final Data-In with S-bit.
|
|
if totalLen < edtl {
|
|
flags |= FlagU
|
|
pdu.SetResidualCount(edtl - totalLen)
|
|
} else if totalLen > edtl {
|
|
flags |= FlagO
|
|
pdu.SetResidualCount(totalLen - edtl)
|
|
}
|
|
pdu.SetOpSpecific1(flags)
|
|
pdu.SetSCSIStatus(SCSIStatusGood)
|
|
} else {
|
|
pdu.SetOpSpecific1(0)
|
|
}
|
|
|
|
pdus = append(pdus, pdu)
|
|
dataSN++
|
|
offset += segLen
|
|
}
|
|
|
|
return pdus
|
|
}
|