npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vates/iscsi

v0.2.0

Published

Minimal iSCSI target and initiator: expose or read a single LUN, with one-way CHAP

Downloads

113

Readme

@vates/iscsi

Package Version License PackagePhobia Node compatibility

Minimal iSCSI target and initiator: expose or read a single LUN, with one-way CHAP

Install

Installation of the npm package:

npm install --save @vates/iscsi

Usage

A small, dependency-light iSCSI target (server) written in TypeScript. It exposes exactly one read/write LUN backed by a pluggable byte-range BlockDevice, and interoperates with the standard Linux open-iscsi initiator (the same one XCP-ng/XAPI uses to attach an lvmoiscsi SR).

Scope

The target drives login negotiation onto a single, fixed code path:

  • single initiator, MaxConnections=1, ErrorRecoveryLevel=0
  • no header/data digests (HeaderDigest=None, DataDigest=None)
  • all writes are R2T-solicited (InitialR2T=Yes, ImmediateData=No)
  • one LUN (LUN 0)

It implements SendTargets discovery, login (AuthMethod=None or one-way CHAP — see Authentication), the ~10 SCSI commands a Linux initiator issues to attach and use a block device (INQUIRY + VPD 0x00/0x80/0x83, REPORT LUNS, READ CAPACITY 10/16, READ/WRITE 10/16, TEST UNIT READY, REQUEST SENSE, MODE SENSE, SYNCHRONIZE CACHE), the full R2T / Data-Out write path, NOP keepalive replies, and Logout. Commands may be outstanding concurrently (Data-Out PDUs are routed by Initiator Task Tag), so it does not stall under real guest I/O.

The package also ships a userspace initiator (IscsiInitiator) and an IscsiDisk adapter that exposes a remote LUN as an @xen-orchestra/disk-transform RandomAccessDisk — see Initiator.

Not implemented (yet): mutual (two-way) CHAP, Task Management (abort/reset), target-initiated NOP keepalives, multiple connections/LUNs.

CLI

The package ships a vates-iscsi binary that serves a file as a LUN:

> vates-iscsi /srv/lun.img
> vates-iscsi /srv/lun.img --serial MYSERIAL01 --port 3260
> vates-iscsi /srv/new.img --size 10G          # create/grow the backing file first
Usage: vates-iscsi <file> [options]

Options:
  --serial <serial>       LUN serial number (default: derived from the IQN).
  --size <bytes>          Create/grow the backing file (suffixes K, M, G, T).
  --iqn <iqn>             Target IQN (default: iqn.2024-01.tech.vates:<file>).
  --host <host>           Listen address (default: 0.0.0.0).
  --port <port>           Listen port (default: 3260).
  --block-size <bytes>    Logical block size (default: 512).
  -h, --help              Show this help.

Library

import { IscsiTarget, FileBlockDevice } from '@vates/iscsi'

// The backing file must already exist and be sized to the LUN capacity,
// e.g. `fs.truncate('/srv/lun.img', 10 * 1024 ** 3)`.
const target = new IscsiTarget({
  iqn: 'iqn.2024-01.tech.vates:lun0',
  host: '0.0.0.0',
  port: 3260,
  lun: new FileBlockDevice({ path: '/srv/lun.img', blockSize: 512 }),
})

await target.listen()
// ... target is now discoverable and attachable by open-iscsi ...
await target.close()

Adding a target backend

A "backend" is whatever stores the LUN's bytes. The protocol layer translates SCSI READ/WRITE CDBs (LBA + block count) into byte offsets/lengths against a single small interface, so a backend only has to provide flat random access — it never sees a PDU. FileBlockDevice (a sparse file) is the built-in one; implement BlockDevice to expose anything else (a raw device, an object store, an @xen-orchestra/disk-transform RandomAccessDisk, …):

interface BlockDevice {
  open?(): Promise<void> // optional, awaited once before serving I/O
  getSize(): number // total capacity in bytes; MUST be a multiple of getBlockSize()
  getBlockSize(): number // logical block size, typically 512
  read(offset: number, length: number): Promise<Buffer> // exactly `length` bytes at byte `offset`
  write(offset: number, data: Buffer): Promise<void> // write `data.length` bytes at byte `offset`
  flush(): Promise<void> // SYNCHRONIZE CACHE — persist any buffering
  close(): Promise<void> // release resources
}

Contract to respect:

  • getSize() must be a multiple of getBlockSize() and stable for the life of the device. open() is the place to compute it (e.g. stat the file).
  • read must return exactly length bytes — never short. Zero-fill a sparse or past-EOF tail rather than returning fewer bytes; the SCSI layer already bounds offset + length to the capacity.
  • offset and length are always block-aligned (multiples of getBlockSize()).
  • read/write may be called concurrently (commands are interleaved), so the backend must be safe under overlapping calls; honour back-pressure and don't buffer unboundedly.
  • Throwing from read/write/flush tears the connection down; fail fast and attach context to the error.

Minimal example — expose a read-only RandomAccessDisk (VHD / qcow2 / raw / a remote NBD or iSCSI LUN) as a target LUN by mapping byte reads onto block reads:

import { IscsiTarget } from '@vates/iscsi'

class RandomAccessDiskLun {
  // `disk` is an initialized @xen-orchestra/disk-transform RandomAccessDisk.
  constructor(disk) {
    this.disk = disk
    this.blockSize = disk.getBlockSize()
  }
  getSize() {
    return this.disk.getVirtualSize()
  }
  getBlockSize() {
    return this.blockSize
  }
  async read(offset, length) {
    const out = Buffer.alloc(length) // pre-zeroed: sparse/holes read as zeros
    const first = Math.floor(offset / this.blockSize)
    const last = Math.floor((offset + length - 1) / this.blockSize)
    for (let index = first; index <= last; index++) {
      if (!this.disk.hasBlock(index)) {
        continue // unallocated: leave zeros
      }
      const { data } = await this.disk.readBlock(index)
      const blockStart = index * this.blockSize
      const from = Math.max(0, offset - blockStart)
      const to = Math.min(this.blockSize, offset + length - blockStart)
      data.copy(out, blockStart + from - offset, from, to)
    }
    return out
  }
  write() {
    throw new Error('read-only LUN')
  }
  async flush() {}
  async close() {
    await this.disk.close()
  }
}

const target = new IscsiTarget({ iqn: 'iqn.2024-01.tech.vates:disk0', lun: new RandomAccessDiskLun(disk) })
await target.listen()

For a writable backend, implement write(offset, data) symmetrically and make flush() durable (e.g. fsync), as FileBlockDevice does.

Initiator

IscsiInitiator is a minimal userspace iSCSI client: it connects to one target portal, logs in, and reads the LUN. IscsiDisk wraps it as an @xen-orchestra/disk-transform RandomAccessDisk, so a remote LUN drops straight into the backup/transform pipeline (the iSCSI counterpart of @vates/nbd-client's NbdDisk). It is read-only, single-connection, digests off.

import { IscsiInitiator, IscsiDisk } from '@vates/iscsi'

// Low-level byte reads:
const initiator = new IscsiInitiator({
  host: '10.0.0.10',
  port: 3260,
  targetIqn: 'iqn.2024-01.tech.vates:lun0',
  chap: { user: 'alice', secret: 's3cr3t' }, // optional, see Authentication
})
await initiator.connect()
console.log(initiator.getSize(), initiator.getBlockSize())
const bytes = await initiator.read(/* offset */ 0, /* length */ 1024 * 1024)
await initiator.close()

// …or as a RandomAccessDisk (blockSize must be a multiple of the LUN block size):
const disk = new IscsiDisk(
  { host: '10.0.0.10', targetIqn: 'iqn.2024-01.tech.vates:lun0' },
  { blockSize: 2 * 1024 * 1024 }
)
await disk.init()
// disk.getVirtualSize(), disk.getBlockIndexes(), await disk.readBlock(i), …
await disk.close()

First-cut IscsiDisk treats the LUN as fully allocated (every block reported as present, like RawDisk); a changed-block map (e.g. a Pure "Volume Difference") can narrow it later.

Authentication (CHAP)

The target and initiator support one-way CHAP (MD5) in opposite roles; mutual (two-way) CHAP is not yet implemented.

Target as authenticator. Pass chap to require each data session to prove a credential; the target challenges the initiator and rejects the login (Login Response status-class Initiator Error / authentication failure) on a mismatch:

const target = new IscsiTarget({
  iqn: 'iqn.2024-01.tech.vates:lun0',
  lun,
  chap: { user: 'alice', secret: 's3cr3t' },
})

This interoperates with the Linux open-iscsi initiator (the one XCP-ng uses); configure its node record's incoming credential:

iscsiadm -m node -T <iqn> -p <portal> -o update -n node.session.auth.authmethod -v CHAP
iscsiadm -m node -T <iqn> -p <portal> -o update -n node.session.auth.username   -v alice
iscsiadm -m node -T <iqn> -p <portal> -o update -n node.session.auth.password   -v s3cr3t

CHAP is enforced only on Normal (data) sessions; Discovery sessions (which can only run SendTargets and never reach the LUN) stay unauthenticated, so plain iscsiadm -m discovery keeps working. open-iscsi requires the secret to be 12–16 characters.

Initiator as responder. Pass chap to IscsiInitiator/IscsiDisk to answer a target's challenge (e.g. a Pure array acting as authenticator). If the target requires mutual CHAP the login is cleanly rejected (we never challenge back).

Security

iSCSI has no data-path encryption of its own. CHAP authenticates the login only; with no digests, CDBs and data remain cleartext. Anyone who can reach an unauthenticated port can read/write the LUN, and a CHAP-protected one is still exposed to eavesdropping and tampering on the wire. Expose targets only on a trusted, isolated storage network, and use IPsec (the RFC-specified mechanism) or equivalent if confidentiality is required.

Testing

  • yarn test — fast unit tests (PDU framing, SCSI encoders/decoders, login negotiation, sequence-number arithmetic).
  • yarn test-integration — a self-contained loopback test (an in-process minimal initiator drives discovery, identity, write/read-back, and command interleaving), plus an open-iscsi interop test that requires root and iscsiadm and skips gracefully otherwise.

Contributions

Contributions are very welcomed, either on the documentation or on the code.

You may:

  • report any issue you've encountered;
  • fork and create a pull request.

License

ISC © Vates SAS