iscsi-client-rs 0.0.4

A pure-Rust iSCSI initiator library and CLI
Documentation
use anyhow::{Context, Result, bail};
use md5::{Digest, Md5};

use crate::{
    cfg::config::{
        AuthConfig, Config, login_keys_chap_response, login_keys_operational,
        login_keys_security,
    },
    client::client::Connection,
    models::{
        common::Builder as _,
        data_fromat::PDUWithData,
        login::{common::Stage, request::LoginRequestBuilder, response::LoginResponse},
    },
};

fn calc_chap_r_hex(id: u8, secret: &[u8], challenge: &[u8]) -> String {
    let mut hasher = Md5::new();
    hasher.update([id]);
    hasher.update(secret);
    hasher.update(challenge);
    let d = hasher.finalize();
    let mut s = String::with_capacity(2 + d.len() * 2);
    s.push_str("0x");
    for b in d {
        use core::fmt::Write;
        write!(&mut s, "{b:02X}").unwrap();
    }
    s
}

fn parse_chap_challenge(txt_bytes: &[u8]) -> Result<(u8, Vec<u8>)> {
    let txt = String::from_utf8(txt_bytes.to_vec())?;
    let mut chap_i: Option<u8> = None;
    let mut chap_c_hex: Option<String> = None;

    for kv in txt.split_terminator('\x00') {
        let mut parts = kv.splitn(2, '=');
        match (parts.next(), parts.next()) {
            (Some("CHAP_I"), Some(v)) => chap_i = Some(v.trim().parse()?),
            (Some("CHAP_C"), Some(s)) => {
                let s = s.trim();
                let s = s
                    .strip_prefix("0x")
                    .or_else(|| s.strip_prefix("0X"))
                    .unwrap_or(s);
                chap_c_hex = Some(s.to_string());
            },
            _ => {},
        }
    }

    let id = chap_i.context("missing CHAP_I")?;
    let hex = chap_c_hex.context("missing CHAP_C")?;
    if hex.len() % 2 != 0 {
        bail!("CHAP_C hex length must be even, got {}", hex.len());
    }
    let chal =
        hex::decode(&hex).with_context(|| format!("failed to decode CHAP_C: {hex}"))?;
    Ok((id, chal))
}

async fn step_security(
    conn: &Connection,
    cfg: &Config,
    isid: [u8; 6],
    itt: u32,
    cid: u16,
) -> Result<PDUWithData<LoginResponse>> {
    let req = LoginRequestBuilder::new(isid, 0)
        .csg(Stage::Security)
        .nsg(Stage::Security)
        .initiator_task_tag(itt)
        .connection_id(cid)
        .cmd_sn(0)
        .exp_stat_sn(0)
        .header;

    let mut pdu = PDUWithData::from_header(req);
    pdu.append_data(login_keys_security(cfg));
    conn.send_request(itt, pdu).await?;
    conn.read_response::<LoginResponse>(itt).await
}

async fn step_chap_a(
    conn: &Connection,
    isid: [u8; 6],
    prev: &PDUWithData<LoginResponse>,
    cid: u16,
) -> Result<PDUWithData<LoginResponse>> {
    let req = LoginRequestBuilder::new(isid, prev.header.tsih)
        .csg(Stage::Security)
        .nsg(Stage::Security)
        .initiator_task_tag(prev.header.initiator_task_tag)
        .connection_id(cid)
        .cmd_sn(prev.header.exp_cmd_sn)
        .exp_stat_sn(prev.header.stat_sn.wrapping_add(1))
        .header;

    let mut pdu = PDUWithData::from_header(req);
    pdu.append_data(b"CHAP_A=5\x00".to_vec());
    conn.send_request(prev.header.initiator_task_tag, pdu)
        .await?;
    conn.read_response::<LoginResponse>(prev.header.initiator_task_tag)
        .await
}

async fn step_chap_answer(
    conn: &Connection,
    cfg: &Config,
    isid: [u8; 6],
    prev: &PDUWithData<LoginResponse>,
    cid: u16,
) -> Result<PDUWithData<LoginResponse>> {
    let (id, chal) = parse_chap_challenge(&prev.data)?;
    let (user, secret) = match &cfg.login.auth {
        AuthConfig::Chap(c) => (c.username.as_str(), c.secret.as_bytes()),
        AuthConfig::None => bail!("Target requires CHAP but config has no credentials"),
    };
    let chap_r = calc_chap_r_hex(id, secret, &chal);

    let req = LoginRequestBuilder::new(isid, prev.header.tsih)
        .transit()
        .csg(Stage::Security)
        .nsg(Stage::Operational)
        .initiator_task_tag(prev.header.initiator_task_tag)
        .connection_id(cid)
        .cmd_sn(prev.header.exp_cmd_sn)
        .exp_stat_sn(prev.header.stat_sn.wrapping_add(1))
        .header;

    let mut pdu = PDUWithData::from_header(req);
    pdu.append_data(login_keys_chap_response(user, &chap_r));
    conn.send_request(prev.header.initiator_task_tag, pdu)
        .await?;
    conn.read_response::<LoginResponse>(prev.header.initiator_task_tag)
        .await
}

async fn step_operational_to_ff(
    conn: &Connection,
    cfg: &Config,
    isid: [u8; 6],
    cid: u16,
    prev: &PDUWithData<LoginResponse>,
) -> Result<PDUWithData<LoginResponse>> {
    let req = LoginRequestBuilder::new(isid, prev.header.tsih)
        .transit()
        .csg(Stage::Operational)
        .nsg(Stage::FullFeature)
        .versions(prev.header.version_max, prev.header.version_active)
        .initiator_task_tag(prev.header.initiator_task_tag)
        .connection_id(cid)
        .cmd_sn(prev.header.exp_cmd_sn)
        .exp_stat_sn(prev.header.stat_sn.wrapping_add(1))
        .header;

    let mut pdu = PDUWithData::from_header(req);
    pdu.append_data(login_keys_operational(cfg));
    conn.send_request(prev.header.initiator_task_tag, pdu)
        .await?;
    conn.read_response::<LoginResponse>(prev.header.initiator_task_tag)
        .await
}

pub async fn login_chap(
    conn: &Connection,
    cfg: &Config,
    isid: [u8; 6],
) -> Result<PDUWithData<LoginResponse>> {
    let itt = 0u32;
    let cid = 1u16;

    let r1 = step_security(conn, cfg, isid, itt, cid).await?;
    let r1b = step_chap_a(conn, isid, &r1, cid).await?;
    let r2 = step_chap_answer(conn, cfg, isid, &r1b, cid).await?;
    let r4 = step_operational_to_ff(conn, cfg, isid, cid, &r2).await?;
    Ok(r4)
}