Skip to main content

iscsi_client_rs/state_machine/login/
common.rs

1//! This module defines common structures and enums for the iSCSI Login state
2//! machine. It provides the context and state definitions for handling the
3//! login process.
4
5use std::{collections::HashMap, marker::PhantomData, sync::Arc};
6
7use anyhow::{Context, Result, anyhow, bail};
8use tokio_util::sync::CancellationToken;
9use tracing::{debug, warn};
10
11use crate::{
12    cfg::config::{Config, login_keys_operational},
13    client::client::ClientConnection,
14    models::{
15        common::HEADER_LEN,
16        data_fromat::PduResponse,
17        login::{common::Stage, response::LoginResponse},
18    },
19    state_machine::{
20        common::{StateMachine, StateMachineCtx, Transition},
21        login::{
22            login_chap::{ChapA, ChapAnswer, ChapOpToFull, ChapSecurity},
23            login_plain::PlainStart,
24        },
25    },
26};
27
28/// This structure represents the context for a Login command.
29#[derive(Debug)]
30pub struct LoginCtx<'a> {
31    _lt: PhantomData<&'a ()>,
32
33    /// The client connection.
34    pub conn: Arc<ClientConnection>,
35    /// The Initiator Session ID.
36    pub isid: [u8; 6],
37    /// The Connection ID.
38    pub cid: u16,
39    /// The Target Session Identifying Handle.
40    pub tsih: u16,
41    /// The Initiator Task Tag.
42    pub itt: u32,
43    /// A buffer for the BHS.
44    pub buf: [u8; HEADER_LEN],
45
46    /// The last received login response.
47    pub last_response: Option<PduResponse<LoginResponse>>,
48
49    state: Option<LoginStates>,
50}
51
52impl<'a> LoginCtx<'a> {
53    /// Creates a new `LoginCtx` for a login operation.
54    pub fn new(conn: Arc<ClientConnection>, isid: [u8; 6], cid: u16, tsih: u16) -> Self {
55        Self {
56            conn,
57            isid,
58            cid,
59            tsih,
60            itt: 0,
61            buf: [0u8; HEADER_LEN],
62            last_response: None,
63            state: None,
64            _lt: PhantomData,
65        }
66    }
67
68    /// Sets the login state to use plain authentication.
69    pub fn set_plain_login(&mut self) {
70        self.state = Some(LoginStates::PlainStart(PlainStart));
71    }
72
73    /// Sets the login state to use CHAP authentication.
74    pub fn set_chap_login(&mut self) {
75        self.state = Some(LoginStates::ChapSecurity(ChapSecurity));
76    }
77
78    /// Validates and returns the header of the last login response.
79    pub fn validate_last_response_header(&self) -> Result<&LoginResponse> {
80        match &self.last_response {
81            Some(l) => match l.header_view() {
82                Ok(last) => Ok(last),
83                Err(e) => Err(e),
84            },
85            None => Err(anyhow!("no last response in ctx")),
86        }
87    }
88
89    /// Validates and returns the last login response PDU.
90    pub fn validate_last_response_pdu(&self) -> Result<&PduResponse<LoginResponse>> {
91        match &self.last_response {
92            Some(l) => Ok(l),
93            None => Err(anyhow!("no last response in ctx")),
94        }
95    }
96}
97
98/// A type alias for the output of a login state machine step.
99pub type LoginStepOut = Transition<LoginStates, Result<()>>;
100
101/// Defines the possible states for a Login operation state machine.
102#[derive(Debug)]
103pub enum LoginStates {
104    /// The initial state for plain authentication.
105    PlainStart(PlainStart),
106    /// The initial state for CHAP authentication.
107    ChapSecurity(ChapSecurity),
108    /// The state for sending the CHAP algorithm.
109    ChapA(ChapA),
110    /// The state for sending the CHAP answer.
111    ChapAnswer(ChapAnswer),
112    /// The state for transitioning from operational to full feature phase.
113    ChapOpToFull(ChapOpToFull),
114}
115
116impl<'ctx> StateMachineCtx<LoginCtx<'ctx>, PduResponse<LoginResponse>>
117    for LoginCtx<'ctx>
118{
119    async fn execute(
120        &mut self,
121        _cancel: &CancellationToken,
122    ) -> Result<PduResponse<LoginResponse>> {
123        debug!("Loop login");
124        loop {
125            let state = self.state.take().context("state must be set LoginCtx")?;
126            let tr = match state {
127                LoginStates::PlainStart(s) => s.step(self).await,
128                LoginStates::ChapSecurity(s) => s.step(self).await,
129                LoginStates::ChapA(s) => s.step(self).await,
130                LoginStates::ChapAnswer(s) => s.step(self).await,
131                LoginStates::ChapOpToFull(s) => s.step(self).await,
132            };
133
134            match tr {
135                Transition::Next(next_state, _r) => {
136                    self.state = Some(next_state);
137                },
138                Transition::Stay(Ok(_)) => {},
139                Transition::Stay(Err(e)) => return Err(e),
140                Transition::Done(r) => {
141                    r?;
142                    return self
143                        .last_response
144                        .take()
145                        .ok_or_else(|| anyhow!("no last response in ctx"));
146                },
147            }
148        }
149    }
150}
151
152fn parse_login_text_map(data: &[u8]) -> Result<HashMap<String, Vec<String>>> {
153    let mut map: HashMap<String, Vec<String>> = HashMap::new();
154    for entry in data.split(|b| *b == 0) {
155        if entry.is_empty() {
156            continue;
157        }
158        let entry_str = std::str::from_utf8(entry)
159            .context("login response contains invalid UTF-8 text")?;
160        let (key, value) = entry_str.split_once('=').ok_or_else(|| {
161            anyhow!("login response entry '{entry_str}' is missing '=' separator")
162        })?;
163        let value = value.trim().to_string();
164        if value == "NotUnderstood" {
165            warn!("{}={}", key.to_string(), value);
166            continue;
167        }
168
169        map.entry(key.to_string())
170            .or_default()
171            .push(value.trim().to_string());
172    }
173    Ok(map)
174}
175
176/// Ensures that the target accepted every operational key/value requested in
177/// the configuration.
178pub(crate) fn verify_operational_negotiation(
179    cfg: &Config,
180    rsp: &PduResponse<LoginResponse>,
181) -> Result<()> {
182    let header = rsp.header_view()?;
183    match header.flags.nsg() {
184        Some(Stage::FullFeature) => {},
185        other => bail!(
186            "login response NSG={other:?} (expected {:?}), status={:?}/{:?}",
187            Stage::FullFeature,
188            header.status_class,
189            header.status_detail,
190        ),
191    }
192
193    let data = rsp
194        .data()
195        .context("login response missing negotiation payload")?;
196    if data.is_empty() {
197        bail!("login response negotiation payload is empty");
198    }
199
200    let server = parse_login_text_map(data)?;
201    let expected_bytes = login_keys_operational(cfg);
202    let expected = parse_login_text_map(expected_bytes.as_slice())?;
203
204    let mut mismatches = Vec::new();
205    for (key, exp_values) in expected {
206        for expected_value in exp_values {
207            match server.get(&key) {
208                Some(values) if values.iter().any(|v| v == &expected_value) => {},
209                Some(values) => mismatches.push(format!(
210                    "{key}: expected '{expected_value}' but target replied '{}'",
211                    values.join(", ")
212                )),
213                None => {},
214            }
215        }
216    }
217
218    if mismatches.is_empty() {
219        Ok(())
220    } else {
221        bail!("login negotiation mismatch: {}", mismatches.join("; "))
222    }
223}