atlas_rdma/
handshake.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//
3// The client side of the rail handshake as PURE byte functions, generic over
4// `Read`/`Write` and taking QP identities as plain `(qpn, psn, gid)` tuples —
5// no `Verbs` (and thus no ibverbs) required. `railset::RailSet` delegates to
6// these in production; tests drive them against a scripted fake stream to pin
7// the client's complete emitted byte sequence (`tests/transcript_golden.rs`),
8// which is the only test class that catches a write REORDER.
9//
10// Wire order per dialect (all little-endian, a frozen external contract):
11//   client: [u8 n_rails]                                   (`write_n_rails`)
12//   server: RO  → [u8 n]{VerbsServerParams}×n              (`wire::read_server_rails`)
13//           RW  → [u8 n echo]{CacheServerParams}×n         (`read_rw_server_params`)
14//   client: [u8 n_rails] {VerbsClientParams}×n             (`write_client_params`)
15//   server: [u8 ack] == STATUS_OK                          (`read_ack`)
16// The n_rails byte is deliberately sent TWICE by the client — that is the wire
17// format, not a redundancy to deduplicate.
18
19use anyhow::{Context, Result, bail};
20use std::io::{Read, Write};
21
22use crate::wire::{CacheServerParams, STATUS_OK, VerbsClientParams};
23
24/// Step 1: tell the peer how many rails we want to stripe across.
25pub fn write_n_rails<W: Write>(w: &mut W, n: usize) -> Result<()> {
26    w.write_all(&[n as u8]).context("send n_rails")?;
27    Ok(())
28}
29
30/// RW-blade dialect (KV overflow / snapshots): read the peer's `[u8 n]` echo
31/// (must equal the negotiated `want`; deliberately NOT bounded to 8 — the RO
32/// dialect's 1..=8 bound is its own, and unifying validation would change
33/// accepted wire inputs) followed by `n` `CacheServerParams`.
34pub fn read_rw_server_params<R: Read>(
35    r: &mut R,
36    want: usize,
37    peer: &str,
38) -> Result<Vec<CacheServerParams>> {
39    let mut b1 = [0u8; 1];
40    if let Err(e) = r.read_exact(&mut b1) {
41        // A clean EOF here is the peer REJECTING us, not a transport fault: it
42        // hangs up after logging its own reason (e.g. "paging blade cap") and
43        // the v2 wire has no error frame to carry that reason back. Bare
44        // `read_exact` context yields "failed to fill whole buffer", which
45        // sends operators hunting a network problem that does not exist.
46        if e.kind() == std::io::ErrorKind::UnexpectedEof {
47            bail!(
48                "{peer} closed the connection during the rail handshake without sending rail \
49                 params. The peer REJECTED this client — read ITS log for the reason. Most \
50                 common: the arena this client requested exceeds the peer's --max-blade-gb \
51                 cap (peer logs \"paging blade cap\"); also possible: a blob_bytes/kind \
52                 mismatch against an arena a prior client already fixed."
53            );
54        }
55        return Err(e).context("read peer n_rails");
56    }
57    if b1[0] as usize != want {
58        bail!("{peer} granted {} rails, wanted {want}", b1[0]);
59    }
60    let mut server = Vec::with_capacity(want);
61    for _ in 0..want {
62        server
63            .push(CacheServerParams::read_from(r).with_context(|| format!("read {peer} params"))?);
64    }
65    Ok(server)
66}
67
68/// Reply with our rail count (again — wire format) + each rail's QP identity.
69pub fn write_client_params<W: Write>(w: &mut W, ids: &[(u32, u32, [u8; 16])]) -> Result<()> {
70    w.write_all(&[ids.len() as u8])
71        .context("send client n_rails")?;
72    for &(qpn, psn, gid) in ids {
73        VerbsClientParams { qpn, psn, gid }
74            .write_to(w)
75            .context("send verbs client params")?;
76    }
77    Ok(())
78}
79
80/// Final step: the peer's one-byte ready ack, which must be `STATUS_OK`.
81pub fn read_ack<R: Read>(r: &mut R, peer: &str) -> Result<()> {
82    let mut ack = [0u8; 1];
83    r.read_exact(&mut ack).context("read verbs ready ack")?;
84    if ack[0] != STATUS_OK {
85        bail!("{peer} refused connection (ack {})", ack[0]);
86    }
87    Ok(())
88}