iscsi-client-rs
A pure‑Rust iSCSI initiator library for interacting with iSCSI targets over TCP. Build/parse PDUs, perform login (plain or CHAP), and exchange SCSI commands asynchronously.
⚠️ Status: tested against Linux
tgt/targetclionly. Other targets may behave differently. Use with care.
Features
- iSCSI Login across Security → Operational → Full‑Feature phases
- CHAP (MD5) authentication (challenge parsing + response building)
- Plain login (no authentication)
- Async networking via Tokio
- Support CancellationToken and Graceful shutdown
- High‑level state machines for:
- Login (plain & CHAP)
- NOP (Nop-Out / Nop-In)
- SCSI READ(10/16) (Data-In)
- SCSI WRITE(10/16) (Data-Out; ImmediateData path)
- SCSI READ CAPACITY(16)
- SCSI TEST UNIT READY(6)
- SCSI MODE SENSE(6/10)
- SCSI REPORT LUNS(12)
- SCSI REQUEST SENSE(6)
- SCSI INQUIRY EVPD/VPD (6)
- Logout
- Zero C dependencies
⚠️ IMPORTANT: Always use the Pool
All SCSI commands must be executed via the session Pool.
Why this matters:
- Correct counters & ordering. Pool allocates and wires
ITT,CmdSN,ExpStatSNfor each task and binds them to the right connection (CID) in a session (TSIH). - Channel lifecycle. Pool creates and removes per‑ITT channels exactly once, avoiding stuck in‑flight requests.
- Keep‑alive & unsolicited NOP‑In. Pool auto‑replies and manages background plumbing; direct use can break this.
- Graceful shutdown. Pool can quiesce writers and drain in‑flight tasks cleanly.
Do not call
ClientConnection::send_request/read_response*directly in application code. Always wrap your state machine intopool.execute_with(tsih, cid, |conn, itt, cmd_sn, exp_stat_sn| { … }).
Quick start (via Pool)
use Result;
use Arc;
use CancellationToken;
use ;
async
If you build sessions by hand, ensure the connection is bound:
ClientConnection::bind_pool_session(pool_weak, tsih, cid)before any I/O. This enables unsolicited NOP‑In auto‑replies and other internals.
Examples (Pool‑first)
NOP (keep‑alive)
use ;
let lun = 1u64 << 48;
pool.execute_with
.await?;
READ(10)
use ;
let lun = 1u64 << 48;
let blocks = 64u32;
let block_size = 4096u32;
let read_len = blocks * block_size;
let mut cdb = ;
build_read10;
let read_outcome = pool.execute_with
.await?;
// println!("read {} bytes", read_outcome.data.len());
WRITE(10) — R2T path
use ;
let lun = 1u64 << 48;
let blocks = 64u32;
let block_size = 4096u32;
let bytes = as usize;
let mut payload = vec!;
// fill payload …
let mut cdb = ;
build_write10;
pool.execute_with
.await?;
// println!("write ok");
Notes:
- If
ImmediateData=Yesandlen ≤ FirstBurstLength,WriteCtxmay send the payload in the initialScsiCommandRequest. Otherwise it honors R2T windows and segmentsData‑Outbymin(MRDSL, remaining_in_burst); the last PDU in a burst hasF=1. ReadyToTransferPDUs are never final; the Pool keeps the ITT open until the finalScsiCommandResponse.
Anti‑pattern (don’t do this)
// ❌ Bypassing the Pool: sending requests and reading replies manually.
// This will desynchronize per‑ITT channels and leak in‑flight tasks.
let conn = /* … */;
conn.send_request.await?;
let rsp = conn.>.await?;
Always route through the Pool:
// ✅ Correct
pool.execute_with
.await?;
Concurrency pattern
Want to parallelize I/O? Launch several execute_with calls (different ITTs or LBAs). The Pool handles sequencing and counters.
use try_join_all;
let tasks = .map;
try_join_all.await?;
Troubleshooting
-
Stuck ITTs (e.g.,
left [ … ]). Ensure finality semantics are consistent in both the low‑level parser andSendingDataimpls:- ScsiDataIn: channel is final only when
F=1 && S=1(status carried in Data‑In). IfS=0, expect a separateScsiCommandResponseand keep the channel open. - ScsiCommandResponse: always final.
- ReadyToTransfer (R2T): never final.
These rules must match in both
parse::Pdu::get_final_bit()andScsiDataIn’sSendingData::get_final_bit().
- ScsiDataIn: channel is final only when
-
Unsolicited NOP‑In (TTT ≠ 0xffffffff) needs auto‑reply. Make sure the connection is bound to the Pool (done automatically by
open_session_and_login).
Architecture overview
Connection & PDU framing
Connection wraps a Tokio TCP stream and frames iSCSI PDUs by their 48‑byte BHS. It:
- writes PDUs via
ToBytes - reads headers + payload with a timeout
- coalesces multi‑segment payloads using
Continue/Finalbits - completes a pending request by Initiator Task Tag (ITT) and delivers a fully reconstructed
PDUWithData<T>to the caller
Sequence & task numbers
- ITT (Initiator Task Tag): request correlation per command/flow
- CmdSN / ExpStatSN: maintained by caller; on responses we bump
ExpStatSN = stat_sn + 1
Utility builders produce PDUs with correct fields; state machines update counters for you at the right moments.
Roadmap
A high‑level plan, tracked as Now → Next → Later with checkboxes. Pool‑first API is the baseline assumption.
Now
-
Core protocol & plumbing
- CRC32C digests (Header/Data; opt‑in)
- Unified state machines (Login, NOP, READ/WRITE)
- Opcode‑aware finality semantics (Data‑In: final iff F=1 && S=1; R2T: never final; ScsiCommandResponse: always final)
- Pool‑first execution path (per‑ITT channels; auto NOP‑In reply)
- Discovery: SendTargets (Text)
-
Reliability & ergonomics
- Structured errors with retry hints
- Timeouts & cancellation tokens per I/O and login
- Back‑pressure & graceful shutdown (quiesce writers, drain in‑flights)
-
Testing & CI
- Multi‑target matrix: tgt, LIO/targetcli, SCST
- Byte‑exact fixtures for login/PDUs
- Fuzzing (cargo‑fuzz / proptest) for PDUs & text keys
Next
- Sessions & recovery
- Multi‑connection sessions (MC/S)
- Reinstatement & session recovery
- ERL1/ERL2: SNACKs, retransmit, CmdSN/StatSN windowing
- Security
- Mutual CHAP (bi‑dir), strict key parsing/normalization
- Optional TLS/TCP (when target supports it)
- SCSI coverage
- REPORT LUNS, INQUIRY VPD, MODE SENSE/SELECT
- UNMAP, WRITE SAME, COMPARE‑AND‑WRITE
- TMFs: ABORT TASK, LUN RESET, CLEAR TASK SET
- AEN / Unit Attention flow
- Performance
- Zero‑copy build/parse; fewer allocs
- Pipelining & outstanding‑cmd windows
- Auto‑tune: MaxBurstLength, FirstBurstLength
- Scatter‑gather for large Data‑Out
- Benchmarks (throughput/latency) with reproducible profiles
How we track: create issues with labels epic, proto, perf, api, testing, docs. Link them here under the matching section.
Contributing
We use DCO (Signed-off-by on each commit) and require a CLA (individual/entity) before the first PR. This allows us to keep the project AGPL-only today and offer a commercial license later without recontacting contributors.
- See
CONTRIBUTING.md,legal/CLA-INDIVIDUAL.md,legal/CLA-ENTITY.md.
Issues and PRs are welcome. Please run:
License
AGPL-3.0-or-later. See LICENSE-AGPL-3.0.md.
© 2012-2025 Andrei Maltsev
Commercial licensing: not available yet; if you need a proprietary license, contact u7743837492@gmail.com to be notified when dual licensing launches.