Skip to main content

ozzy_proto/
handshake.rs

1//! Bounded native HELLO/WELCOME properties. Negotiation is not authentication.
2
3use super::append::DataLimits;
4use super::{ENVELOPE_BYTES, Envelope, EnvelopeError, EnvelopeLimits, Opcode, Packet};
5use crate::layout::{LayoutError, Reader};
6use smallvec::SmallVec;
7
8/// Owner-append capability, including exact retry reconciliation.
9pub const OWNER_APPEND: u16 = 1;
10/// Consumer-group membership and source activation; requires durable progress.
11pub const OWNER_GROUPS: u16 = 1 << 7;
12/// Typed authority hints for fixed-group owner discovery and unchanged retries.
13pub const OWNER_ROUTING: u16 = 1 << 9;
14/// Single-partition reader delivery and receipt/processing observations.
15pub const OWNER_READ: u16 = 1 << 10;
16/// Durable group processing progress, with exact member/assignment fencing.
17pub const OWNER_PROGRESS: u16 = 1 << 11;
18/// Pipelined owner append with record-based retries and per-writer range replies.
19pub const OWNER_STREAM: u16 = 1 << 12;
20/// Reader role, not independent authorization.
21pub const CONSUMER: u32 = 2;
22/// Producer role. A role claim does not authorize a producer identity.
23pub const PRODUCER: u32 = 1;
24/// Log-owner role. Serving authority still comes from the active group.
25pub const OWNER: u32 = 4;
26// Capability 14 is unassigned. Required use must fail, never imply delivery.
27const KNOWN_CAPABILITIES: u16 = 0x1fff;
28const MAX_PROPERTIES: usize = 32;
29const NAMES: [&[u8]; 10] = [
30    b"versions",
31    b"capabilities",
32    b"required-capabilities",
33    b"max-metadata-bytes",
34    b"max-payload-bytes",
35    b"max-batch-records",
36    b"max-payload-parts",
37    b"roles",
38    b"max-record-bytes",
39    b"superseded-hello",
40];
41
42/// One endpoint's advertised receive limits and supported capability set.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Parameters {
45    /// Bit `id - 1` for each supported capability in the allocated 1..=13 range.
46    pub capabilities: u16,
47    /// Same bit encoding; each required capability must be selected.
48    pub required_capabilities: u16,
49    /// Directional command bounds. Never replace the opposite direction's limits.
50    pub receive: DataLimits,
51    /// Producer/consumer/owner/replica/directory occupy bits 0..=4.
52    pub roles: u32,
53}
54
55impl Parameters {
56    /// Pipelined append profile with directional packet bounds.
57    /// Both append and streaming support are mandatory; no legacy downgrade.
58    pub fn streaming(receive: DataLimits, roles: u32) -> Result<Self, HandshakeError> {
59        let parameters = Self {
60            capabilities: OWNER_APPEND | OWNER_STREAM,
61            required_capabilities: OWNER_APPEND | OWNER_STREAM,
62            receive,
63            roles,
64        };
65        parameters.validate()?;
66        Ok(parameters)
67    }
68    /// Reader profile with fixed receive bounds. Transport pressure bounds replay.
69    pub fn reader(receive: DataLimits, roles: u32) -> Result<Self, HandshakeError> {
70        let mut parameters = Self::append(receive, roles)?;
71        parameters.capabilities = OWNER_READ;
72        parameters.required_capabilities = OWNER_READ;
73        Ok(parameters)
74    }
75
76    /// Reader profile requiring durable progress in addition to record delivery.
77    pub fn processing(receive: DataLimits, roles: u32) -> Result<Self, HandshakeError> {
78        let mut parameters = Self::reader(receive, roles)?;
79        parameters.capabilities |= OWNER_PROGRESS;
80        parameters.required_capabilities |= OWNER_PROGRESS;
81        parameters.validate()?;
82        Ok(parameters)
83    }
84
85    /// Processing profile that also requires automatic group membership.
86    pub fn group(receive: DataLimits, roles: u32) -> Result<Self, HandshakeError> {
87        let mut parameters = Self::processing(receive, roles)?;
88        parameters.capabilities |= OWNER_GROUPS;
89        parameters.required_capabilities |= OWNER_GROUPS;
90        parameters.validate()?;
91        Ok(parameters)
92    }
93    /// Single-request append profile; other command families remain unadvertised.
94    pub fn append(receive: DataLimits, roles: u32) -> Result<Self, HandshakeError> {
95        let parameters = Self {
96            capabilities: OWNER_APPEND,
97            required_capabilities: OWNER_APPEND,
98            receive,
99            roles,
100        };
101        parameters.validate()?;
102        Ok(parameters)
103    }
104
105    /// Validate configuration before allocating handshake or command buffers.
106    pub fn validate(self) -> Result<(), HandshakeError> {
107        let sizes = [
108            self.receive.envelope.max_metadata_bytes,
109            self.receive.envelope.max_payload_bytes,
110            self.receive.max_records,
111            self.receive.max_parts,
112            self.receive.max_record_bytes,
113        ];
114        if sizes.iter().any(|&n| n == 0 || u32::try_from(n).is_err())
115            || self.receive.envelope.max_metadata_bytes < 512
116            || self.capabilities & !KNOWN_CAPABILITIES != 0
117            || (self.capabilities & OWNER_STREAM != 0 && self.capabilities & OWNER_APPEND == 0)
118            || (self.capabilities & OWNER_PROGRESS != 0 && self.capabilities & OWNER_READ == 0)
119            || (self.capabilities & OWNER_GROUPS != 0 && self.capabilities & OWNER_PROGRESS == 0)
120            || self.required_capabilities & !self.capabilities != 0
121            || self.roles == 0
122            || self.roles & !31 != 0
123        {
124            return Err(HandshakeError::Parameters);
125        }
126        Ok(())
127    }
128
129    /// Select the supported intersection, checking requirements in both directions.
130    /// Each endpoint retains its own directional receive limits.
131    pub fn select(self, remote: Self) -> Result<Self, HandshakeError> {
132        self.validate()?;
133        remote.validate()?;
134        let selected = self.capabilities & remote.capabilities;
135        if (self.required_capabilities | remote.required_capabilities) & !selected != 0 {
136            return Err(HandshakeError::Capabilities);
137        }
138        Ok(Self {
139            capabilities: selected,
140            ..self
141        })
142    }
143}
144
145/// HELLO properties or WELCOME properties with the echoed initiator nonce.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct Handshake {
148    /// WELCOME-only nonce of a simultaneous HELLO abandoned by this responder.
149    /// Lets the initiator ignore that HELLO even if it arrives after WELCOME.
150    pub superseded_hello: Option<u128>,
151    /// Fresh nonzero process incarnation; not the persistent node identity.
152    pub instance_id: u128,
153    /// Fresh nonzero HELLO attempt identity, echoed unchanged by WELCOME.
154    pub hello_nonce: u128,
155    /// Sender's receive bounds and capability advertisement/selection.
156    pub parameters: Parameters,
157}
158
159/// Encode RFC-style property names/values into reserved storage without growth.
160/// Validation failure leaves the previous metadata untouched. Payload is empty.
161pub fn encode(
162    envelope: Envelope,
163    handshake: Handshake,
164    metadata: &mut Vec<u8>,
165    limits: EnvelopeLimits,
166) -> Result<[u8; ENVELOPE_BYTES], HandshakeError> {
167    validate_command(envelope, handshake)?;
168    let p = handshake.parameters;
169    let caps = capabilities(p.capabilities);
170    let required = capabilities(p.required_capabilities);
171    let properties: [(&[u8], &[u8]); 9] = [
172        (b"versions", &[0, 0, 0, 1, super::VERSION]),
173        (b"capabilities", caps.as_slice()),
174        (b"required-capabilities", required.as_slice()),
175        (
176            b"max-metadata-bytes",
177            &(p.receive.envelope.max_metadata_bytes as u32).to_be_bytes(),
178        ),
179        (
180            b"max-payload-bytes",
181            &(p.receive.envelope.max_payload_bytes as u32).to_be_bytes(),
182        ),
183        (
184            b"max-batch-records",
185            &(p.receive.max_records as u32).to_be_bytes(),
186        ),
187        (
188            b"max-payload-parts",
189            &(p.receive.max_parts as u32).to_be_bytes(),
190        ),
191        (b"roles", &p.roles.to_be_bytes()),
192        (
193            b"max-record-bytes",
194            &(p.receive.max_record_bytes as u32).to_be_bytes(),
195        ),
196    ];
197    let superseded = handshake.superseded_hello.map(u128::to_be_bytes);
198    let properties = properties.into_iter().chain(
199        superseded
200            .as_ref()
201            .map(|nonce| (&b"superseded-hello"[..], &nonce[..])),
202    );
203    // Identity, then one length-prefixed name and value per property.
204    let size = properties
205        .clone()
206        .try_fold(32_usize, |size, (name, value)| {
207            size.checked_add(5)?
208                .checked_add(name.len())?
209                .checked_add(value.len())
210        })
211        .ok_or(HandshakeError::Length)?;
212    let header = envelope.encode_header(size, 0, limits)?;
213    if metadata.capacity() < size {
214        return Err(HandshakeError::Capacity);
215    }
216    metadata.clear();
217    metadata.extend_from_slice(&handshake.instance_id.to_be_bytes());
218    metadata.extend_from_slice(&handshake.hello_nonce.to_be_bytes());
219    for (name, value) in properties {
220        write_property(metadata, name, value)?;
221    }
222    Ok(header)
223}
224
225/// One length-prefixed property name and value.
226fn write_property(metadata: &mut Vec<u8>, name: &[u8], value: &[u8]) -> Result<(), HandshakeError> {
227    let name_bytes = u8::try_from(name.len()).map_err(|_| HandshakeError::Properties)?;
228    let value_bytes = u32::try_from(value.len()).map_err(|_| HandshakeError::Properties)?;
229    metadata.push(name_bytes);
230    metadata.extend_from_slice(name);
231    metadata.extend_from_slice(&value_bytes.to_be_bytes());
232    metadata.extend_from_slice(value);
233    Ok(())
234}
235
236/// Decode without allocating. Unknown optional properties/capabilities are ignored.
237/// All names, including unknown names, participate in case-insensitive duplicate
238/// rejection. Property count is bounded independently of the metadata byte limit.
239pub fn decode(packet: Packet<'_>, limits: EnvelopeLimits) -> Result<Handshake, HandshakeError> {
240    packet
241        .envelope
242        .validate_frames(packet.metadata.len(), packet.payload.len(), limits)?;
243    if !packet.payload.is_empty() {
244        return Err(HandshakeError::Command);
245    }
246    let mut reader = Reader::new(packet.metadata);
247    let instance_id = u128::from_be_bytes(reader.array()?);
248    let hello_nonce = u128::from_be_bytes(reader.array()?);
249    let values = read_properties(reader)?;
250    let mut v = [&[][..]; 9];
251    for (to, from) in v.iter_mut().zip(values) {
252        *to = from.ok_or(HandshakeError::Properties)?;
253    }
254    check_versions(v[0], packet.envelope.opcode)?;
255    let parameters = decode_parameters(&v)?;
256    let handshake = Handshake {
257        superseded_hello: values[9]
258            .map(|v| {
259                v.try_into()
260                    .map(u128::from_be_bytes)
261                    .map_err(|_| HandshakeError::Length)
262            })
263            .transpose()?,
264        instance_id,
265        hello_nonce,
266        parameters,
267    };
268    validate_command(packet.envelope, handshake)?;
269    Ok(handshake)
270}
271
272/// Known property values in [`NAMES`] order. Unknown names count only toward
273/// the property bound and duplicate rejection.
274fn read_properties(mut reader: Reader<'_>) -> Result<[Option<&[u8]>; NAMES.len()], HandshakeError> {
275    let mut names: SmallVec<[&[u8]; MAX_PROPERTIES]> = SmallVec::new();
276    let mut values = [None; NAMES.len()];
277    while !reader.rest().is_empty() {
278        if names.len() == MAX_PROPERTIES {
279            return Err(HandshakeError::Properties);
280        }
281        let length = usize::from(reader.u8()?);
282        let name = reader.take(length)?;
283        if name.is_empty()
284            || !name.is_ascii()
285            || names.iter().any(|old| old.eq_ignore_ascii_case(name))
286        {
287            return Err(HandshakeError::Properties);
288        }
289        names.push(name);
290        let length = reader.u32()? as usize;
291        let value = reader.take(length)?;
292        if let Some(slot) = NAMES
293            .iter()
294            .zip(&mut values)
295            .find_map(|(known, slot)| known.eq_ignore_ascii_case(name).then_some(slot))
296        {
297            *slot = Some(value);
298        }
299    }
300    Ok(values)
301}
302
303/// A HELLO offers this version among others; a WELCOME selects only it.
304fn check_versions(value: &[u8], opcode: Opcode) -> Result<(), HandshakeError> {
305    let mut versions = Reader::new(value);
306    let count = versions.u32()? as usize;
307    let versions = versions.rest();
308    if count == 0
309        || count > 256
310        || count != versions.len()
311        || !versions.contains(&super::VERSION)
312        || (opcode == Opcode::Welcome && versions != [super::VERSION])
313    {
314        return Err(HandshakeError::Version);
315    }
316    Ok(())
317}
318
319/// Parameters from the required property values, in [`NAMES`] order.
320fn decode_parameters(v: &[&[u8]; 9]) -> Result<Parameters, HandshakeError> {
321    Ok(Parameters {
322        capabilities: decode_capabilities(v[1], false)?,
323        required_capabilities: decode_capabilities(v[2], true)?,
324        receive: DataLimits {
325            envelope: EnvelopeLimits {
326                max_metadata_bytes: number(v[3])? as usize,
327                max_payload_bytes: number(v[4])? as usize,
328            },
329            max_records: number(v[5])? as usize,
330            max_parts: number(v[6])? as usize,
331            max_record_bytes: number(v[8])? as usize,
332        },
333        roles: number(v[7])?,
334    })
335}
336
337fn validate_command(envelope: Envelope, handshake: Handshake) -> Result<(), HandshakeError> {
338    if !matches!(envelope.opcode, Opcode::Hello | Opcode::Welcome)
339        || envelope.response != (envelope.opcode == Opcode::Welcome)
340        || handshake.instance_id == 0
341        || handshake.hello_nonce == 0
342        || handshake.superseded_hello == Some(0)
343        || (envelope.opcode == Opcode::Hello && handshake.superseded_hello.is_some())
344    {
345        return Err(HandshakeError::Command);
346    }
347    handshake.parameters.validate()
348}
349
350#[expect(
351    clippy::arithmetic_side_effects,
352    reason = "capability ids lie in 1..=14"
353)]
354fn capabilities(bits: u16) -> SmallVec<[u8; 32]> {
355    let mut bytes = SmallVec::new();
356    bytes.extend_from_slice(&bits.count_ones().to_be_bytes());
357    for id in (1..=14_u16).filter(|id| bits & (1 << (id - 1)) != 0) {
358        bytes.extend_from_slice(&id.to_be_bytes());
359    }
360    bytes
361}
362
363#[expect(
364    clippy::arithmetic_side_effects,
365    reason = "the count is capped at 64 first, and set ids lie in 1..=14"
366)]
367fn decode_capabilities(bytes: &[u8], required: bool) -> Result<u16, HandshakeError> {
368    let mut reader = Reader::new(bytes);
369    let count = reader.u32()? as usize;
370    let bytes = reader.rest();
371    if count > 64 || bytes.len() != count * 2 {
372        return Err(HandshakeError::Capabilities);
373    }
374    let mut bits = 0;
375    let mut previous = 0;
376    for chunk in bytes.as_chunks::<2>().0 {
377        let id = u16::from_be_bytes(*chunk);
378        if id <= previous || (required && id > 14) {
379            return Err(HandshakeError::Capabilities);
380        }
381        previous = id;
382        if id <= 14 {
383            bits |= 1 << (id - 1);
384        }
385    }
386    Ok(bits)
387}
388
389fn number(bytes: &[u8]) -> Result<u32, HandshakeError> {
390    Ok(u32::from_be_bytes(
391        bytes.try_into().map_err(|_| HandshakeError::Length)?,
392    ))
393}
394
395/// No handshake rejection is authority to admit application work.
396#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
397pub enum HandshakeError {
398    /// Invalid outer command/session shape or frame bound.
399    #[error(transparent)]
400    Envelope(#[from] EnvelopeError),
401    /// Wrong command direction or zero instance/nonce.
402    #[error("invalid handshake command or identity")]
403    Command,
404    /// Truncated field or inconsistent counted value.
405    #[error("invalid handshake field length")]
406    Length,
407    /// Missing, duplicate, invalid, or too many properties.
408    #[error("invalid handshake properties")]
409    Properties,
410    /// Unsupported version or invalid WELCOME selection.
411    #[error("no supported protocol version")]
412    Version,
413    /// Required capability unavailable or malformed capability set.
414    #[error("unsupported handshake capabilities")]
415    Capabilities,
416    /// Invalid advertised bounds, roles, or local capability configuration.
417    #[error("invalid handshake parameters")]
418    Parameters,
419    /// Caller did not reserve enough metadata space.
420    #[error("handshake encode buffer capacity exhausted")]
421    Capacity,
422}
423
424impl From<LayoutError> for HandshakeError {
425    fn from(_: LayoutError) -> Self {
426        Self::Length
427    }
428}