A high-performance, multi-protocol storage target implementation in Rust supporting iSCSI and NVMe over Fabrics (NVMe-oF) via TCP transport.
- Dual Protocol Support: Run iSCSI and NVMe-oF from the same codebase
- iSCSI Protocol Compliance: Full SCSI command router with MODE SENSE/SELECT, WRITE AND VERIFY, LOG SENSE, FORMAT UNIT, PERSISTENT RESERVE, UNMAP, WRITE SAME, COMPARE AND WRITE
- NVMe-oF/TCP Target: Discovery controller, Fabrics Connect/Disconnect, Identify, logs, features, reservations, dataset management, and read/write I/O paths
- Unified Management: Single admin API and metrics endpoint for both protocols
- Dynamic Configuration: Add and remove iSCSI IQNs and NVMe NQNs through the CLI or JSON-RPC Admin API
- Cluster Coordination: Built-in support for distributed deployments via etcd, Consul, or Redis
- ALUA/MPIO: Asymmetric Logical Unit Access for multi-path I/O
- Performance Optimized: TCP_NODELAY, SO_REUSEPORT, TCP Fast Open (Linux)
- Pluggable Backends: Memory, mmap, file-backed, and block-device backends
┌─────────────────────────────────────────┐
│ Unified Daemon │
│ (targets/unified) │
├─────────────────────────────────────────┤
│ Admin API (:5000) │ Metrics (:5001) │
├─────────────────────────────────────────┤
│ MultiProtocolRuntime │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ iSCSI │ │ NVMe-oF │ │
│ │ TargetManager│ │ NvmeRuntime │ │
│ └─────────────┘ └─────────────────┘ │
├─────────────────────────────────────────┤
│ iSCSI (:3260) NVMe-oF (:4420) │
└─────────────────────────────────────────┘
| Crate | Description |
|---|---|
target-core |
Protocol-agnostic abstractions: Backend, StorageRuntime, metrics, coordinator |
iscsi-target |
iSCSI implementation: session, connection, login, command routing |
nvme-target |
NVMe-oF implementation: controller, queue pair, capsule protocol |
network-tokio |
Tokio-based TCP frontend for iSCSI |
network-common |
Shared TCP listener optimizations |
admin-api |
JSON-RPC admin API for target management |
metrics |
Prometheus metrics export |
iscsi-protocol |
iSCSI protocol types and PDU codec |
nvme-protocol |
NVMe command and fabrics protocol types |
backend-block |
Block device backend |
backend-memory |
Memory and mmap backends |
# Development build, including libraries, binaries, examples, and tests
cargo build --all-targets
# Release binaries
cargo build --release
# Standard quality gates
cargo fmt --all -- --check
cargo test
cargo clippy --workspace --all-targets -- -D warningsDefault endpoints:
| Binary | Protocol bind | Admin API | Metrics | Health |
|---|---|---|---|---|
unified |
iSCSI 0.0.0.0:3260, NVMe-oF 0.0.0.0:4420 |
127.0.0.1:5000 |
127.0.0.1:5001 |
127.0.0.1:8080 |
iscsid |
iSCSI 0.0.0.0:3260 |
127.0.0.1:5000 |
127.0.0.1:5001 |
127.0.0.1:8080 |
nvmeofd |
NVMe-oF 0.0.0.0:4420 |
127.0.0.1:5002 |
127.0.0.1:5003 |
127.0.0.1:8082 |
Binding to well-known ports such as 3260 may require elevated privileges on
some systems. Use explicit --*-bind or config-file bind addresses for
unprivileged local testing.
cargo run --bin unified run
cargo run --bin unified run --config unified.tomlConfiguration file (unified.toml):
[server]
iscsi_bind = "0.0.0.0:3260"
nvme_bind = "0.0.0.0:4420"
admin_bind = "127.0.0.1:5000"
metrics_bind = "127.0.0.1:5001"
[[iscsi_targets]]
iqn = "iqn.2024-06.com.rust-iscsi:disk1"
alias = "Rust iSCSI Target"
[[iscsi_targets.luns]]
lun = 0
backend_type = "memory"
size = "1GiB"
block_size = 512
readonly = false
# Supported backend_type values: "memory", "mmap", "file", "block".
# For persistent iSCSI storage, use backend_type = "file" or "block".
# path = "/var/lib/rust-scsi/iscsi-disk1.img"
# Optional: per-target CHAP authentication
[iscsi_targets.auth]
username = "initiator1"
password = "secret123"
mutual = false
[nvme_target]
nqn = "nqn.2014-08.org.nvmexpress:uuid:rust-target-0001"
[nvme_target.backend]
type = "memory"
size = "1G"
block_size = 512
readonly = false
# Supported type values: "memory", "mmap", "file", "block".
# For persistent NVMe-oF storage, use type = "file" or "block".
# path = "/var/lib/rust-scsi/nvme-target.img"
[coordinator]
backend = "local"
node_id = "node-1"
# backend may also be "etcd", "consul", or "redis".
# endpoints = ["127.0.0.1:2379"]
# datacenter = "dc1" # Consul onlycargo run --bin iscsid run
cargo run --bin iscsid run --config config.tomlWith CHAP authentication:
cargo run --bin iscsid run --auth-user initiator1 --auth-pass secret123
cargo run --bin iscsid run --auth-user initiator1 --auth-pass secret123 --auth-mutualConfiguration file (config.toml):
[server]
bind_address = "0.0.0.0:3260"
threads = 4
[iscsi]
max_connections = 8
max_sessions = 256
max_queue_depth = 128
error_recovery_level = 0
initial_r2t = false
immediate_data = true
header_digest = "None"
data_digest = "None"
[[targets]]
iqn = "iqn.2024-06.com.rust-iscsi:disk1"
alias = "Rust iSCSI Target"
[[targets.luns]]
lun = 0
backend = "memory"
size = "1GiB"
readonly = false
# Supported backend values: "memory", "mmap", "file", "block".
# For persistent iSCSI storage, use backend = "file" or "block".
# path = "/var/lib/rust-scsi/iscsi-disk1.img"
[auth.chap]
username = "initiator1"
password = "secret123"cargo run --bin nvmeofd run
cargo run --bin nvmeofd run --config nvmeofd.tomlConfiguration file: /etc/nvmeofd/config.toml or ./nvmeofd.toml
Example file-backed NVMe-oF target:
nqn = "nqn.2014-08.org.nvmexpress:uuid:rust-target-0001"
[server]
bind_address = "0.0.0.0:4420"
[backend]
type = "file"
size = "1G"
block_size = 512
readonly = false
path = "/var/lib/rust-scsi/nvme-target.img"
[coordinator]
backend = "local"
node_id = "nvme-node-1"All daemons use the same coordinator options. local is the default
single-node backend. Distributed deployments may use etcd, consul, or
redis; unknown backend names fail daemon startup instead of silently falling
back to local.
[coordinator]
backend = "etcd" # local | etcd | consul | redis
node_id = "node-1"
endpoints = ["127.0.0.1:2379"]
datacenter = "dc1" # used by Consul; defaults to dc1Default endpoints are 127.0.0.1:2379 for etcd, 127.0.0.1:8500 for Consul,
and 127.0.0.1:6379 for Redis when endpoints is omitted.
All daemons accept the same backend names in CLI and dynamic Admin API paths:
| Backend | Use case | Notes |
|---|---|---|
memory |
Fast disposable test LUNs/namespaces | Data is lost when the daemon exits |
mmap |
Anonymous memory-mapped storage | Uses Linux mmap optimizations and falls back to in-memory storage on other platforms |
file |
Persistent file-backed storage | Prefer an explicit path; a deterministic temp path is used when omitted |
block |
Existing block device or block-device-like path | Prefer an explicit path; requires appropriate OS permissions and isolated test devices |
size accepts raw bytes or human-readable units such as 512MiB, 1GiB,
1G, and 2GB. block_size defaults to 512 bytes where the field exists.
readonly = true wraps the selected backend in a read-only adapter and rejects
write-affecting commands while preserving read paths.
iSCSI targets support CHAP one-way and mutual authentication:
- One-way: Target verifies the initiator's identity
- Mutual: Both target and initiator verify each other
Supported hash algorithms: MD5, SHA1, SHA256, SHA3-256.
Per-target ACL controls which initiators can connect:
allowed_initiators: IQN allow list (supports wildcards likeiqn.2024-01.com.test:*)denied_initiators: IQN deny list (takes precedence)allowed_ips: IP address allow list
If no ACL is configured, all initiators are allowed.
All daemons support graceful shutdown on SIGINT/SIGTERM/SIGHUP (Unix) or Ctrl+C:
# Stop accepting new connections, wait for active I/O to complete
cargo run --bin unified run
# Press Ctrl+C or send SIGTERMHTTP health check endpoints (default 127.0.0.1:8080):
curl http://127.0.0.1:8080/health # {"status":"healthy"}
curl http://127.0.0.1:8080/ready # {"status":"ready"} or 503Change health check bind address:
cargo run --bin unified run --health-bind 0.0.0.0:8080
cargo run --bin iscsid run --health-bind 127.0.0.1:8081
cargo run --bin nvmeofd run --health-bind 127.0.0.1:8082All binaries expose a CLI for dynamic target management without restarting the
daemon. Use --admin <addr> on any management command when the daemon was
started with a non-default Admin API bind address.
# iSCSI
cargo run --bin iscsid -- target list
cargo run --bin iscsid -- target add --iqn iqn.2024-06.com.rust-iscsi:disk2 --backend memory --size 2G
cargo run --bin iscsid -- target add --iqn iqn.2024-06.com.rust-iscsi:disk3 --backend file --size 2G --block-size 512 --path /var/lib/rust-scsi/iscsi-disk3.img
cargo run --bin iscsid -- target remove --iqn iqn.2024-06.com.rust-iscsi:disk2
cargo run --bin iscsid -- session list
cargo run --bin iscsid -- session disconnect --id <session_id>
cargo run --bin iscsid -- health
cargo run --bin iscsid -- metrics
cargo run --bin iscsid -- config validate config.toml
# Unified
cargo run --bin unified -- target list
cargo run --bin unified -- target add --iqn iqn.2024-06.com.rust-iscsi:disk2 --backend memory --size 2G
cargo run --bin unified -- target add --iqn iqn.2024-06.com.rust-iscsi:disk3 --backend file --size 2G --path /var/lib/rust-scsi/iscsi-disk3.img
cargo run --bin unified -- target add --protocol nvme --nqn nqn.2014-08.org.nvmexpress:uuid:disk2 --backend file --size 2G --path /var/lib/rust-scsi/nvme-disk2.img
cargo run --bin unified -- target remove --iqn iqn.2024-06.com.rust-iscsi:disk2
cargo run --bin unified -- target remove --nqn nqn.2014-08.org.nvmexpress:uuid:disk2
cargo run --bin unified -- session list
cargo run --bin unified -- session disconnect --id <session_id>
cargo run --bin unified -- health
cargo run --bin unified -- metrics
cargo run --bin unified -- config validate unified.toml
# NVMe-oF
cargo run --bin nvmeofd -- target list
cargo run --bin nvmeofd -- target add --nqn nqn.2014-08.org.nvmexpress:uuid:disk2 --backend file --size 2G --path /var/lib/rust-scsi/nvme-disk2.img
cargo run --bin nvmeofd -- target remove --nqn nqn.2014-08.org.nvmexpress:uuid:disk2
cargo run --bin nvmeofd -- health
cargo run --bin nvmeofd -- metrics
cargo run --bin nvmeofd -- config validate nvmeofd.tomlJSON-RPC 2.0 over newline-delimited TCP (default port 5000 for unified/iSCSI,
5002 for NVMe-oF). CLI commands use this API through the built-in AdminClient.
Example methods:
target_list— List all targetstarget_get— Get one target bytarget_id,id,iqn, ornqntarget_add— Dynamically add a target (rejects duplicate target IDs)target_remove— Remove a target bytarget_id,id,iqn, ornqnsession_list— List active sessionssession_disconnect— Disconnect a sessionstats— Get runtime statisticshealth_check— Cluster health statusget_metrics— Return runtime counters as JSONget_backend_status— Return backend size/block statusget_logs— Return recent in-memory log entries, with optionallimitget_alua_state— Return ALUA state, optionally filtered by targetcluster_members— Return coordinator cluster membershipset_tpg_state— Change ALUA TPG statefailover— Trigger LUN failover
For iSCSI target_add, the JSON config accepts iqn, optional alias,
luns, CHAP fields (auth_username/auth_password, chap_username/chap_password,
or nested auth.chap.username/auth.chap.password plus auth_mutual or
auth.chap.mutual), and ACL arrays (allowed_initiators, denied_initiators,
allowed_ips). Each LUN accepts backend_type or backend, optional path
for file/block backends, and size may be either bytes as a number or a
human-readable string such as "32MiB". If a file/block LUN omits path,
the daemon creates a deterministic file under the system temp directory using
both target IQN and LUN number.
For NVMe-oF target_add, the JSON config accepts nqn, backend_type,
backend, or type, optional readonly, block_size, optional path for
file/block backends, and size as either bytes as a number or a
human-readable string such as "32MiB". If a file/block target omits
path, the daemon creates a deterministic file under the system temp
directory for that NQN.
Direct TCP examples:
# List targets on a unified or iSCSI daemon.
printf '%s\n' '{"jsonrpc":"2.0","method":"target_list","id":1}' | nc 127.0.0.1 5000
# Add an iSCSI target with CHAP and ACL through Admin API.
printf '%s\n' '{
"jsonrpc":"2.0",
"method":"target_add",
"params":{
"iqn":"iqn.2024-06.com.rust-iscsi:api-disk",
"alias":"API iSCSI Target",
"auth":{"chap":{"username":"initiator1","password":"secret123","mutual":true}},
"allowed_initiators":["iqn.2024-01.com.test:*"],
"denied_initiators":[],
"allowed_ips":["127.0.0.1"],
"luns":[{"lun":0,"backend_type":"file","size":"32MiB","block_size":512,"path":"/tmp/rust-scsi-api-iscsi.img"}]
},
"id":2
}' | nc 127.0.0.1 5000
# Add an NVMe-oF target on nvmeofd.
printf '%s\n' '{
"jsonrpc":"2.0",
"method":"target_add",
"params":{
"nqn":"nqn.2014-08.org.nvmexpress:uuid:api-disk",
"backend_type":"file",
"size":"32MiB",
"block_size":512,
"path":"/tmp/rust-scsi-api-nvme.img"
},
"id":3
}' | nc 127.0.0.1 5002
# Remove by protocol-specific identifier.
printf '%s\n' '{"jsonrpc":"2.0","method":"target_remove","params":{"iqn":"iqn.2024-06.com.rust-iscsi:api-disk"},"id":4}' | nc 127.0.0.1 5000
printf '%s\n' '{"jsonrpc":"2.0","method":"target_remove","params":{"nqn":"nqn.2014-08.org.nvmexpress:uuid:api-disk"},"id":5}' | nc 127.0.0.1 5002# Workspace unit, integration, and doc tests
cargo test
# Build every target
cargo build --all-targets
# Warning gate
cargo clippy --workspace --all-targets -- -D warnings
# End-to-end mock test (in-process iSCSI server + client)
cargo test -p iscsi-target --test e2e_mock
# Quick mock startup for manual testing
cargo run --bin iscsid run --config scripts/mock-config.toml
# Portable userspace iSCSI daemon smoke test
scripts/iscsi_userspace_smoke.py
# Optional Linux nvme-cli smoke test for NVMe-oF/TCP
scripts/nvmeof_smoke.sh
# Portable userspace NVMe-oF/TCP daemon smoke test
scripts/nvmeof_userspace_smoke.py
# Optional libiscsi initiator/conformance run for iSCSI
tests/integration/run_tests.sh
# Local unified daemon smoke test (no external initiators required)
scripts/unified_smoke.sh
# Aggregated local verification
scripts/e2e_verify.shRecommended verification matrix:
| Command | Coverage |
|---|---|
cargo fmt --all -- --check |
Workspace formatting |
cargo test |
Rust unit, integration, and doc tests across protocol, backend, admin, and daemon crates |
cargo build --all-targets |
All binaries and test targets compile |
cargo clippy --workspace --all-targets -- -D warnings |
Warning-free workspace |
scripts/unified_smoke.sh |
Unified daemon startup, readiness, admin API, metrics, and dynamic iSCSI/NVMe target add/remove |
scripts/iscsi_userspace_smoke.py |
Portable iSCSI login, CHAP, discovery, SCSI commands, R2T/Data-Out, reservations, UNMAP, NOP, dynamic target API |
scripts/nvmeof_userspace_smoke.py |
Portable NVMe/TCP discovery, connect, admin/I/O queues, reads/writes, logs, reservations, dataset management, dynamic target API |
scripts/nvmeof_smoke.sh |
Optional Linux nvme-cli initiator smoke; falls back to the userspace NVMe-oF smoke unless strict mode is requested |
tests/integration/run_tests.sh |
Optional libiscsi discovery, initiator smoke, SCSI/iSCSI conformance, digest, CHAP, and mutual CHAP coverage |
The E2E mock test (crates/iscsi-target/tests/e2e_mock.rs) verifies the full iSCSI protocol stack:
- iSCSI Login (Security Negotiation → Operational Negotiation → Full Feature Phase)
- SCSI command responses: TEST UNIT READY, INQUIRY, MODE SENSE(6/10), READ CAPACITY(10), LOG SENSE
- Mode page validation: Caching (0x08), Control (0x0A), Error Recovery (0x01), Informational Exceptions (0x1C)
For automated CI verification:
./scripts/e2e_verify.sh # tests + e2e_mock + build + smoke checkThe optional scripts/nvmeof_smoke.sh check starts a memory-backed nvmeofd
instance and uses Linux nvme-cli to discover, connect, identify, and
disconnect. It skips when Linux, nvme-cli, or required privileges are missing;
set NVME_CMD_PREFIX=sudo instead of running the whole script as root. By
default it performs no namespace writes; set RUN_READ=1 or
RUN_WRITE_ZEROES=1 only for an isolated test host.
When nvme-cli prerequisites are missing, scripts/nvmeof_smoke.sh automatically
falls back to scripts/nvmeof_userspace_smoke.py. Set NVME_SMOKE_REQUIRE_NVMECLI=1
to preserve the old strict behavior and exit SKIP instead of running the fallback.
scripts/nvmeof_userspace_smoke.py starts a memory-backed nvmeofd instance
and verifies NVMe/TCP ICReq/ICResp, Discovery Controller connect plus Discovery
Log Page, Fabrics Connect, Get/Set Features, Keep Alive, Identify Controller,
and Identify Namespace, checks the Supported Log Pages and Command Effects logs,
then performs a one-block write/read round trip, Flush, Write Zeroes, and zero
read-back using only the Python standard library. It also verifies the
non-inline write path with R2T and H2CData, Verify, Compare, Copy, Write
Uncorrectable, Format NVM via a separate admin queue, Reservation
Register/Report/Acquire/Release, Dataset Management deallocate with zero
read-back, explicit Fabrics Disconnect, and dynamic Admin API add/remove of a
second NVMe-oF target with duplicate-add rejection, backend aliases,
human-readable sizes, discovery, and connect validation.
It is portable across macOS and Linux, requires no root privileges or kernel
NVMe initiator, and cleans up the temporary daemon.
scripts/iscsi_userspace_smoke.py starts a memory-backed iscsid instance and
verifies mutual CHAP Login, SendTargets, TEST UNIT READY, INQUIRY,
READ CAPACITY(10), MODE SENSE(6/10), MODE SELECT(6/10), LOG SENSE,
REPORT SUPPORTED OPERATION CODES, immediate and R2T/Data-Out WRITE(10)/READ(10)
round trips, PERSISTENT RESERVE IN/OUT, Task Management LUN RESET, and
UNMAP/GET LBA STATUS, plus NOP-Out/NOP-In and dynamic Admin API add/remove of a
second CHAP/ACL-configured iSCSI target with SendTargets discovery updates. The
dynamic target path also exercises duplicate-add rejection, backend aliases, and
human-readable size strings. File/block LUN path handling is covered by unit
tests and daemon smoke checks. It finishes with Logout using only the Python
standard library. It requires no root privileges, kernel iSCSI initiator,
libiscsi tools, or jq.
scripts/unified_smoke.sh starts a temporary unified daemon with one
memory-backed iSCSI target and one memory-backed NVMe-oF target, verifies
/ready, admin health, target list, metrics, and dynamic admin add/remove for
both protocols, then cleans up. It does not require root, nvme-cli, libiscsi,
or jq.
The optional tests/integration/run_tests.sh check builds iscsid, starts a
disposable memory-backed iSCSI LUN, then uses the vendored libiscsi tools for
discovery, INQUIRY, READ CAPACITY, SCSI conformance, iSCSI protocol checks, and
thin-provisioning coverage such as UNMAP, GET LBA STATUS, ORWRITE, and WRITE
SAME. It expects normal Rust build dependencies plus libiscsi build tooling such
as autoconf, automake, libtool, pkg-config, and CUnit. Conformance failures are
strict by default; set ALLOW_CONFORMANCE_FAILURES=1 only for exploratory
runs. Use SMOKE_ONLY=1 to run only discovery, INQUIRY, and READ CAPACITY when
CUnit is unavailable, or CHECK_ONLY=1 to preflight local build dependencies
without starting a target. On macOS the script disables libiscsi's -Werror
while building the vendored user-space tools, because current SDK headers emit
warnings for APIs used by libiscsi. Common overrides include TEST_PORT=13260,
ISCSID_BIN=/path/to/iscsid, and LIBISCSI_CONFIGURE_FLAGS="...".
- Linux is required for the kernel
nvme-cliinitiator path inscripts/nvmeof_smoke.sh; macOS and Linux can both run the userspace NVMe-oF smoke. - Kernel iSCSI initiators are not required for the default tests; the iSCSI userspace smoke and libiscsi integration tests use user-space clients.
- Binding to privileged ports, opening raw block devices, or using kernel initiators may require elevated permissions.
- File and block backends mutate their backing storage. Use temporary files or isolated test devices unless the data is disposable.
- The repository vendors libiscsi under
tests/libiscsi; the integration script can build those tools locally when the host has the C build dependencies.
MIT OR Apache-2.0