srt_runtime/rendezvous.rs
1//! Rendezvous handshake engine — `draft-sharabayko-srt-01` §4.3.2 (Rendezvous
2//! Handshake), curated at `specs/rules/srt-rendezvous.md`. Line cites below
3//! (`LNNNN`) are the source draft's line numbers, exactly as recorded in that
4//! curation.
5//!
6//! [`RendezvousHandshake`] is symmetric: both peers run the *same* engine
7//! (unlike [`crate::caller::CallerHandshake`] / [`crate::listener::ListenerHandshake`],
8//! which run different code for different roles). Which of the two logical
9//! roles — **Initiator** or **Responder** — a given instance ends up playing
10//! is decided at runtime by the **cookie contest** (§4.3.2, L2107-2135):
11//! each side supplies its own 32-bit cookie to [`RendezvousHandshake::new`]
12//! (this crate never reads a clock or a socket address — see
13//! [`crate::handshake_sm::derive_cookie`] for a ready-made, non-standardized
14//! derivation helper, exactly as for [`crate::listener::ListenerHandshake`]),
15//! and the greater cookie value wins ("becomes Initiator", L2133-2135).
16//!
17//! # State machine
18//!
19//! States are named exactly as the draft's Parallel Handshake Flow diagram
20//! (§4.3.2.2, L2280, quoted verbatim): **Waving → Attention → Initiated →
21//! Connected** (plus `Idle` before [`RendezvousHandshake::start`] is called —
22//! not spec-named, mirrors [`crate::caller::CallerHandshakeState::Idle`] — and
23//! `Rejected`/`TimedOut` terminal states, also not spec-named). Every
24//! transition below is the Initiator table (L2301-2334) or Responder table
25//! (L2336-2382), or a missing-packet recovery rule (L2383-2432).
26//!
27//! ## Serial vs Parallel flow: one engine, not two
28//!
29//! The draft narrates two "flows" (§4.3.2.1 Serial, L2140-2269; §4.3.2.2
30//! Parallel, L2270-2432) that differ only in *message interleaving*, not in
31//! any new transition rule: the Parallel flow's Initiator/Responder tables are
32//! complete state × received-message tables, driven purely by message
33//! content — so they already cover the Serial flow's crossing case. That case
34//! is: a peer still in `Waving` (having sent its own WAVEAHAND but never
35//! having received the other side's) receives a **CONCLUSION** directly
36//! instead of a WAVEAHAND (§4.3.2.1 step 3, L2203-2219 — the draft calls the
37//! resulting state "fine"). Tracing the draft's own worked example confirms
38//! the action taken is identical to applying the Parallel Attention row to
39//! that CONCLUSION: an Initiator receiving an extension-less CONCLUSION here
40//! behaves exactly like the Initiator-Attention-row "no extensions" case
41//! (L2312-2318); a Responder receiving a CONCLUSION+HSREQ here behaves
42//! exactly like the Responder-Attention-row HSREQ case (L2360-2364) — because
43//! by this point in the exchange the peer's role-appropriate first CONCLUSION
44//! already carries whatever extension its role dictates (Initiator: HSREQ
45//! immediately, L2286-2287; Responder: none until it has seen HSREQ,
46//! L2357-2360/L2287-2288). This implementation therefore has **no separate
47//! "Fine" state** — a received CONCLUSION while still in `Waving` dispatches
48//! straight into the same Attention-row logic used when genuinely in
49//! `Attention`, rather than inventing a fourth, behaviourally-divergent state
50//! the tables do not define.
51//!
52//! ## Resolved ambiguities (not explicit in the curated rules)
53//!
54//! - **Cookie collision → rejection.** The draft says only "the connection
55//! will not be made until new, unique cookies are generated" (L2119-2124),
56//! describing an out-of-band retry, not a wire action. This engine surfaces
57//! it as [`RejectionReason::RdvCookie`] (Table 7 code `1009`, "rendezvous
58//! cookie collision" — an exact fit) rather than blocking or retrying
59//! internally; a driver that wants the "regenerate and retry" behaviour
60//! builds a fresh [`RendezvousHandshake`] with a new cookie.
61//! - **CONCLUSION `SYN Cookie` field.** The draft specifies WAVEAHAND's SYN
62//! Cookie (L2156-2165) but not what later CONCLUSION/AGREEMENT messages
63//! carry in that field. Every message this engine sends after WAVEAHAND
64//! continues to carry *this side's own* cookie (never an echo of the peer's,
65//! unlike the Caller-Listener flood-protection cookie) — consistent with
66//! the cookie's role here being mutual identification/contest, not a
67//! flood-protection echo-token.
68//! - **No Stream ID / Group Membership exchange.** Neither extension is
69//! mentioned anywhere in §4.3.2; unlike
70//! [`crate::handshake_sm::NegotiatedParams`] from the Caller-Listener flow,
71//! this engine never sends them and always reports `stream_id: None`,
72//! `group: None` — flagged rather than fabricated.
73//! - **Latency/flags reconciliation.** §4.3.2 does not restate the
74//! greater-latency / AND-flags rule from §4.3.1.2, but it is a property of
75//! the Handshake Extension Message itself (§3.2.1.1), not of the flow that
76//! carried it, so the same shared `handshake_sm` reconciliation helper used
77//! by the Caller-Listener flow is reused unchanged.
78//! - **Duplicate WAVEAHAND while already `Attention`.** Not covered by either
79//! table (which only fire on the *first* WAVEAHAND). Treated as a benign
80//! duplicate: resend the last message, no state change.
81//! - **Recovery rule 3 (data packet promotes a stuck Responder, L2413-2422)**
82//! is exposed as [`RendezvousHandshake::on_recovery_trigger`], since this
83//! engine's `feed` takes a [`ControlPacket`] (matching
84//! [`crate::caller::CallerHandshake`] / [`crate::listener::ListenerHandshake`]),
85//! not the data-plane [`crate::packet::SrtPacket`]; a driver that receives a
86//! `SrtPacket::Data` (or any Control packet normally only sent between
87//! connected parties — [`RendezvousHandshake::feed`] already treats any
88//! *inbound* non-Handshake `ControlPacket` this way for exactly this
89//! reason) calls it directly.
90//!
91//! Explicit non-goals, unchanged from the crate root: ARQ/loss, TSBPD
92//! delivery, congestion control, AES key-wrap/unwrap crypto, a `tokio` socket
93//! adapter, and the Version-4 legacy Rendezvous path (L2101-2105, out of
94//! scope of the draft excerpt this crate implements against).
95
96use alloc::vec;
97use alloc::vec::Vec;
98
99use crate::error::{Error, Result};
100use crate::handshake_sm::{
101 self, HANDSHAKE_VERSION_5, HandshakeConfig, HandshakeOutput, NegotiatedParams, RejectionReason,
102};
103use crate::packet::{
104 ControlPacket, EncryptionField, ExtensionType, HandshakeExtensionFlags,
105 HandshakeExtensionMessageFlags, HandshakeExtensions, HandshakePacket, HandshakeType,
106 HsExtMessage,
107};
108
109/// Rendezvous handshake lifecycle state (`draft-sharabayko-srt-01` §4.3.2).
110/// State names are exactly the Parallel Handshake Flow diagram (§4.3.2.2,
111/// L2280): `Waving -> Attention -> Initiated -> Connected`. See the module
112/// doc "Serial vs Parallel flow" note for why there is no separate state for
113/// the Serial flow's "fine".
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize))]
116#[non_exhaustive]
117pub enum RendezvousHandshakeState {
118 /// No handshake message sent yet. Not spec-named — mirrors
119 /// [`crate::caller::CallerHandshakeState::Idle`].
120 Idle,
121 /// L2100/L2153/L2280 "waving"/"Waving": both parties' initial state.
122 Waving,
123 /// L2172/L2280 "attention"/"Attention": a WAVEAHAND was received, the
124 /// cookie contest is resolved, awaiting the peer's CONCLUSION.
125 Attention,
126 /// L2234/L2280 "initiated"/"Initiated": role-appropriate extension
127 /// content has been seen at least once; awaiting the peer's confirmation.
128 Initiated,
129 /// L2224/L2251/L2280 "connected"/"Connected": the handshake completed;
130 /// [`NegotiatedParams`] are available.
131 Connected,
132 /// The handshake was rejected (locally, or by an explicit peer Table 7
133 /// code). Not spec-named.
134 Rejected,
135 /// No response arrived after the configured retry budget. Not spec-named.
136 TimedOut,
137}
138
139impl RendezvousHandshakeState {
140 /// A short label for this state.
141 pub fn name(&self) -> &'static str {
142 match self {
143 RendezvousHandshakeState::Idle => "Idle",
144 RendezvousHandshakeState::Waving => "Waving",
145 RendezvousHandshakeState::Attention => "Attention",
146 RendezvousHandshakeState::Initiated => "Initiated",
147 RendezvousHandshakeState::Connected => "Connected",
148 RendezvousHandshakeState::Rejected => "Rejected",
149 RendezvousHandshakeState::TimedOut => "TimedOut",
150 }
151 }
152}
153
154impl core::fmt::Display for RendezvousHandshakeState {
155 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
156 f.write_str(self.name())
157 }
158}
159
160/// The role a [`RendezvousHandshake`] plays, resolved by the cookie contest
161/// (`draft-sharabayko-srt-01` §4.3.2, L2107-2135): "When one party's cookie
162/// value is greater than its peer's, it wins the cookie contest and becomes
163/// Initiator (the other party becomes the Responder)."
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize))]
166#[non_exhaustive]
167pub enum RendezvousRole {
168 /// Wins the cookie contest (the greater cookie value, L2133-2135). MUST
169 /// attach the HSREQ extension (L2286-2287).
170 Initiator,
171 /// Loses the cookie contest. MUST attach the HSRSP extension
172 /// (L2287-2288).
173 Responder,
174}
175
176impl RendezvousRole {
177 /// A short label for this role.
178 pub fn name(&self) -> &'static str {
179 match self {
180 RendezvousRole::Initiator => "Initiator",
181 RendezvousRole::Responder => "Responder",
182 }
183 }
184}
185
186broadcast_common::impl_spec_display!(RendezvousRole);
187
188/// The Handshake Extension content found on one received CONCLUSION, keyed by
189/// which side sends which (§4.3.2.2, L2282-2288). Internal to this module —
190/// [`crate::handshake_sm::parse_peer_extensions`] does not distinguish
191/// HSREQ from HSRSP, which the Rendezvous tables need to.
192enum PeerHsExt {
193 /// No Handshake Extension block present.
194 None,
195 /// An `HSREQ` block was present (only an Initiator sends this).
196 HsReq(HsExtMessage),
197 /// An `HSRSP` block was present (only a Responder sends this).
198 HsRsp(HsExtMessage),
199}
200
201/// Walks `hp`'s extension blocks looking for an HSREQ or HSRSP Handshake
202/// Extension Message. Returns [`Error::InvalidField`] on any malformed block
203/// or malformed HSREQ/HSRSP contents — never panics on untrusted input.
204fn parse_peer_hs_ext(hp: &HandshakePacket<'_>) -> Result<PeerHsExt> {
205 let mut found = PeerHsExt::None;
206 for block in hp.extensions.iter() {
207 let block = block.map_err(|_| Error::InvalidField {
208 what: "rendezvous handshake extensions",
209 reason: "malformed extension block",
210 })?;
211 match block.ext_type {
212 ExtensionType::HsReq => {
213 let msg = block.as_hs_ext_message().map_err(|_| Error::InvalidField {
214 what: "HSREQ extension message",
215 reason: "malformed contents",
216 })?;
217 found = PeerHsExt::HsReq(msg);
218 }
219 ExtensionType::HsRsp => {
220 let msg = block.as_hs_ext_message().map_err(|_| Error::InvalidField {
221 what: "HSRSP extension message",
222 reason: "malformed contents",
223 })?;
224 found = PeerHsExt::HsRsp(msg);
225 }
226 _ => {}
227 }
228 }
229 Ok(found)
230}
231
232/// A driveable, symmetric SRT Rendezvous handshake
233/// (`draft-sharabayko-srt-01` §4.3.2). See the module docs for the state
234/// machine and the design decisions not explicit in the curated rules.
235#[derive(Debug)]
236pub struct RendezvousHandshake {
237 own_socket_id: u32,
238 own_cookie: u32,
239 config: HandshakeConfig,
240 state: RendezvousHandshakeState,
241 role: Option<RendezvousRole>,
242 peer_socket_id: u32,
243 peer_cookie: u32,
244 peer_hs_msg: Option<HsExtMessage>,
245 last_sent: Option<Vec<u8>>,
246 ticks_since_send: u32,
247 retries: u32,
248 negotiated: Option<NegotiatedParams>,
249}
250
251impl RendezvousHandshake {
252 /// Creates a fresh Rendezvous handshake in
253 /// [`RendezvousHandshakeState::Idle`], with the 32-bit cookie this side
254 /// will offer in the cookie contest (§4.3.2, L2107-2135). This crate never
255 /// reads a clock or a socket address — see
256 /// [`crate::handshake_sm::derive_cookie`] for a ready-made,
257 /// non-standardized derivation helper.
258 pub fn new(own_socket_id: u32, own_cookie: u32, config: HandshakeConfig) -> Self {
259 RendezvousHandshake {
260 own_socket_id,
261 own_cookie,
262 config,
263 state: RendezvousHandshakeState::Idle,
264 role: None,
265 peer_socket_id: 0,
266 peer_cookie: 0,
267 peer_hs_msg: None,
268 last_sent: None,
269 ticks_since_send: 0,
270 retries: 0,
271 negotiated: None,
272 }
273 }
274
275 /// The current state.
276 pub fn state(&self) -> RendezvousHandshakeState {
277 self.state
278 }
279
280 /// The role resolved by the cookie contest, once past
281 /// [`RendezvousHandshakeState::Waving`].
282 pub fn role(&self) -> Option<RendezvousRole> {
283 self.role
284 }
285
286 /// The negotiated parameters, once [`RendezvousHandshakeState::Connected`].
287 pub fn negotiated(&self) -> Option<&NegotiatedParams> {
288 self.negotiated.as_ref()
289 }
290
291 /// Builds the initial WAVEAHAND handshake (§4.3.2, L2100/L2153-2170:
292 /// Version 5, this side's own cookie, no extensions) and transitions to
293 /// [`RendezvousHandshakeState::Waving`].
294 ///
295 /// # Errors
296 /// [`Error::HandshakeOutOfSequence`] if called more than once.
297 pub fn start(&mut self) -> Result<Vec<u8>> {
298 if self.state != RendezvousHandshakeState::Idle {
299 return Err(Error::HandshakeOutOfSequence {
300 state: self.state.name(),
301 reason: "start() called after the handshake already began",
302 });
303 }
304 let hp = HandshakePacket {
305 timestamp: 0,
306 dest_socket_id: 0, // Unknown yet — mirrors CallerHandshake::start's INDUCTION.
307 version: HANDSHAKE_VERSION_5,
308 encryption_field: self.config.encryption_field,
309 extension_field: HandshakeExtensionFlags(0),
310 initial_seq_number: self.config.initial_seq_number,
311 mtu: self.config.mtu,
312 max_flow_window_size: self.config.max_flow_window_size,
313 handshake_type: HandshakeType::Wavehand,
314 srt_socket_id: self.own_socket_id,
315 syn_cookie: self.own_cookie,
316 peer_ip: self.config.local_ip,
317 extensions: HandshakeExtensions(&[]),
318 };
319 let bytes = handshake_sm::build_bytes(hp)?;
320 self.last_sent = Some(bytes.clone());
321 self.ticks_since_send = 0;
322 self.state = RendezvousHandshakeState::Waving;
323 Ok(bytes)
324 }
325
326 /// Feeds an inbound control packet.
327 ///
328 /// A non-Handshake `ControlPacket` fed while this side is a Responder
329 /// stuck in [`RendezvousHandshakeState::Initiated`] is treated as
330 /// missing-packet recovery rule 3 (L2413-2422: "any control packet
331 /// normally only sent between connected parties") rather than an error —
332 /// see [`Self::on_recovery_trigger`] for the data-packet analogue.
333 ///
334 /// # Errors
335 /// [`Error::UnexpectedControlPacket`] for a non-Handshake packet outside
336 /// that one recovery case; [`Error::HandshakeOutOfSequence`] if fed before
337 /// [`Self::start`] or after a terminal state (a driver bug, not a peer
338 /// protocol failure).
339 pub fn feed(&mut self, packet: &ControlPacket<'_>) -> Result<Vec<HandshakeOutput>> {
340 let hp = match packet {
341 ControlPacket::Handshake(hp) => hp,
342 other => {
343 if self.state == RendezvousHandshakeState::Initiated
344 && self.role == Some(RendezvousRole::Responder)
345 {
346 return self.enter_connected();
347 }
348 return Err(Error::UnexpectedControlPacket {
349 actual: other.control_type().name(),
350 });
351 }
352 };
353 if let Some(reason) = RejectionReason::from_handshake_type(hp.handshake_type) {
354 return Ok(self.reject(reason));
355 }
356 if hp.version != HANDSHAKE_VERSION_5 {
357 // §4.3.2, L2101-2105: Version-4 legacy Rendezvous is out of scope.
358 return Ok(self.reject(RejectionReason::Version));
359 }
360 match self.state {
361 RendezvousHandshakeState::Idle => Err(Error::HandshakeOutOfSequence {
362 state: self.state.name(),
363 reason: "feed() called before start()",
364 }),
365 RendezvousHandshakeState::Waving => self.on_waving(hp),
366 RendezvousHandshakeState::Attention => self.on_attention(hp),
367 RendezvousHandshakeState::Initiated => self.on_initiated(hp),
368 RendezvousHandshakeState::Connected => self.on_connected(hp),
369 RendezvousHandshakeState::Rejected | RendezvousHandshakeState::TimedOut => {
370 Err(Error::HandshakeOutOfSequence {
371 state: self.state.name(),
372 reason: "handshake already reached a terminal state",
373 })
374 }
375 }
376 }
377
378 /// Convenience wrapper: parses `bytes` as a [`ControlPacket`] then
379 /// [`Self::feed`]s it.
380 pub fn feed_bytes(&mut self, bytes: &[u8]) -> Result<Vec<HandshakeOutput>> {
381 let packet = ControlPacket::parse(bytes)?;
382 self.feed(&packet)
383 }
384
385 /// Missing-packet recovery rule 3 (§4.3.2.2, L2413-2422): call this when
386 /// the driver receives a data packet (`SrtPacket::Data`) while this side
387 /// has not yet reached [`RendezvousHandshakeState::Connected`]. A no-op
388 /// (`Ok(Vec::new())`) unless this side is a Responder stuck in
389 /// [`RendezvousHandshakeState::Initiated`] — the one case the draft says
390 /// is "exceptionally allowed" to promote to Connected "as if it had
391 /// received AGREEMENT".
392 pub fn on_recovery_trigger(&mut self) -> Result<Vec<HandshakeOutput>> {
393 if self.state == RendezvousHandshakeState::Initiated
394 && self.role == Some(RendezvousRole::Responder)
395 {
396 self.enter_connected()
397 } else {
398 Ok(Vec::new())
399 }
400 }
401
402 /// Advances retransmit timing by one caller-defined tick, mirroring
403 /// [`crate::caller::CallerHandshake::tick`] /
404 /// [`crate::listener::ListenerHandshake::tick`].
405 pub fn tick(&mut self) -> Vec<HandshakeOutput> {
406 if matches!(
407 self.state,
408 RendezvousHandshakeState::Idle
409 | RendezvousHandshakeState::Connected
410 | RendezvousHandshakeState::Rejected
411 | RendezvousHandshakeState::TimedOut
412 ) {
413 return Vec::new();
414 }
415 self.ticks_since_send += 1;
416 if self.ticks_since_send < self.config.retransmit_after_ticks {
417 return Vec::new();
418 }
419 self.ticks_since_send = 0;
420 self.retries += 1;
421 if self.retries > self.config.max_retries {
422 self.state = RendezvousHandshakeState::TimedOut;
423 return vec![HandshakeOutput::TimedOut];
424 }
425 match self.last_sent.clone() {
426 Some(bytes) => vec![HandshakeOutput::Send(bytes)],
427 None => Vec::new(),
428 }
429 }
430
431 /// [`RendezvousHandshakeState::Waving`]: the first message ever received
432 /// from the peer — either a genuine WAVEAHAND (§4.3.2.2 Waving row,
433 /// L2301-2306 Initiator / L2336-2342 Responder) or, in the Serial flow's
434 /// crossing case, a CONCLUSION directly (see module docs). Either way the
435 /// cookie contest is resolved here, from `hp.syn_cookie`.
436 fn on_waving(&mut self, hp: &HandshakePacket<'_>) -> Result<Vec<HandshakeOutput>> {
437 if !matches!(
438 hp.handshake_type,
439 HandshakeType::Wavehand | HandshakeType::Conclusion
440 ) {
441 return Ok(self.reject(RejectionReason::Rogue));
442 }
443 self.peer_socket_id = hp.srt_socket_id;
444 let role = match self.resolve_role(hp.syn_cookie) {
445 Ok(r) => r,
446 Err(_) => return Ok(self.reject(RejectionReason::RdvCookie)),
447 };
448 self.peer_cookie = hp.syn_cookie;
449 self.role = Some(role);
450
451 if hp.handshake_type == HandshakeType::Wavehand {
452 self.state = RendezvousHandshakeState::Attention;
453 match role {
454 // Initiator Waving row (L2301-2306): send CONCLUSION+HSREQ.
455 RendezvousRole::Initiator => self.send_conclusion(Some(ExtensionType::HsReq)),
456 // Responder Waving row (L2336-2342): send CONCLUSION, no
457 // extensions (it has not seen the peer's HSREQ yet).
458 RendezvousRole::Responder => self.send_conclusion(None),
459 }
460 } else {
461 // Serial-flow crossing (§4.3.2.1 step 3, L2203-2219): apply the
462 // same content-driven Attention-row logic (module docs).
463 self.on_attention_conclusion(hp, role)
464 }
465 }
466
467 /// [`RendezvousHandshakeState::Attention`]: a WAVEAHAND was already seen
468 /// and processed; only a CONCLUSION (Attention row) or a duplicate
469 /// WAVEAHAND (not covered by either table — treated as a benign resend,
470 /// module docs) is expected here.
471 fn on_attention(&mut self, hp: &HandshakePacket<'_>) -> Result<Vec<HandshakeOutput>> {
472 let role = self
473 .role
474 .expect("role is always resolved before Attention is reached");
475 if hp.handshake_type == HandshakeType::Conclusion {
476 return self.on_attention_conclusion(hp, role);
477 }
478 if hp.handshake_type == HandshakeType::Wavehand {
479 return Ok(self.resend());
480 }
481 Ok(self.reject(RejectionReason::Rogue))
482 }
483
484 /// The Attention row's CONCLUSION-handling logic (§4.3.2.2, L2307-2320
485 /// Initiator / L2343-2364 Responder), shared between a genuine
486 /// [`RendezvousHandshakeState::Attention`] and the Serial-flow crossing
487 /// case fed straight from [`RendezvousHandshakeState::Waving`].
488 fn on_attention_conclusion(
489 &mut self,
490 hp: &HandshakePacket<'_>,
491 role: RendezvousRole,
492 ) -> Result<Vec<HandshakeOutput>> {
493 self.peer_socket_id = hp.srt_socket_id;
494 let ext = match parse_peer_hs_ext(hp) {
495 Ok(e) => e,
496 Err(_) => return Ok(self.reject(RejectionReason::Rogue)),
497 };
498 match (role, ext) {
499 (RendezvousRole::Initiator, PeerHsExt::None) => {
500 // L2312-2318: no extensions -> Initiated, still send
501 // CONCLUSION+HSREQ.
502 self.state = RendezvousHandshakeState::Initiated;
503 self.send_conclusion(Some(ExtensionType::HsReq))
504 }
505 (RendezvousRole::Initiator, PeerHsExt::HsRsp(msg)) => {
506 // L2318-2320: contains HSRSP -> Connected, send AGREEMENT.
507 self.peer_hs_msg = Some(msg);
508 self.enter_connected()
509 }
510 (RendezvousRole::Responder, PeerHsExt::None) => {
511 // L2357-2360: no extensions yet -> resend the empty
512 // CONCLUSION, remain in Attention.
513 self.state = RendezvousHandshakeState::Attention;
514 self.send_conclusion(None)
515 }
516 (RendezvousRole::Responder, PeerHsExt::HsReq(msg)) => {
517 // L2360-2364: HSREQ present -> Initiated, send
518 // CONCLUSION+HSRSP.
519 self.peer_hs_msg = Some(msg);
520 self.state = RendezvousHandshakeState::Initiated;
521 self.send_conclusion(Some(ExtensionType::HsRsp))
522 }
523 // A peer sending the extension kind only its own role should ever
524 // send (HSREQ into an Initiator, HSRSP into a Responder) is not a
525 // transition either table defines — reject rather than guess.
526 _ => Ok(self.reject(RejectionReason::Rogue)),
527 }
528 }
529
530 /// [`RendezvousHandshakeState::Initiated`] (§4.3.2.2, L2321-2334 Initiator
531 /// / L2365-2382 Responder), including the idempotent-resend recovery
532 /// rules (L2383-2422).
533 fn on_initiated(&mut self, hp: &HandshakePacket<'_>) -> Result<Vec<HandshakeOutput>> {
534 let role = self
535 .role
536 .expect("role is always resolved before Initiated is reached");
537 match role {
538 RendezvousRole::Initiator => {
539 if hp.handshake_type != HandshakeType::Conclusion {
540 return Ok(self.reject(RejectionReason::Rogue));
541 }
542 let ext = match parse_peer_hs_ext(hp) {
543 Ok(e) => e,
544 Err(_) => return Ok(self.reject(RejectionReason::Rogue)),
545 };
546 match ext {
547 // L2325: "REMAINS IN THIS STATE" — still resend
548 // CONCLUSION+HSREQ.
549 PeerHsExt::None => self.send_conclusion(Some(ExtensionType::HsReq)),
550 // L2325-2334ish: contains HSRSP -> Connected, AGREEMENT.
551 PeerHsExt::HsRsp(msg) => {
552 self.peer_hs_msg = Some(msg);
553 self.enter_connected()
554 }
555 PeerHsExt::HsReq(_) => Ok(self.reject(RejectionReason::Rogue)),
556 }
557 }
558 RendezvousRole::Responder => {
559 if hp.handshake_type == HandshakeType::Agreement {
560 // Responder Initiated row: AGREEMENT -> respond AGREEMENT,
561 // switch to Connected.
562 return self.enter_connected();
563 }
564 if hp.handshake_type != HandshakeType::Conclusion {
565 return Ok(self.reject(RejectionReason::Rogue));
566 }
567 let ext = match parse_peer_hs_ext(hp) {
568 Ok(e) => e,
569 Err(_) => return Ok(self.reject(RejectionReason::Rogue)),
570 };
571 match ext {
572 // Recovery rule 2 (L2391-2395): MUST always resend HSRSP,
573 // even if this HSREQ was already seen and processed once.
574 PeerHsExt::HsReq(msg) => {
575 self.peer_hs_msg = Some(msg);
576 self.send_conclusion(Some(ExtensionType::HsRsp))
577 }
578 _ => Ok(self.reject(RejectionReason::Rogue)),
579 }
580 }
581 }
582 }
583
584 /// [`RendezvousHandshakeState::Connected`] (Initiator row item 4,
585 /// L2331-2334; Responder row item 4, L2377-2381): normally no more
586 /// handshake traffic, but a repeated CONCLUSION is answered with another
587 /// AGREEMENT (recovery rule 4, L2424-2432, from the other side's point of
588 /// view); anything else is disregarded rather than regressing a completed
589 /// handshake.
590 fn on_connected(&mut self, hp: &HandshakePacket<'_>) -> Result<Vec<HandshakeOutput>> {
591 if hp.handshake_type == HandshakeType::Conclusion {
592 return self.send_agreement();
593 }
594 Ok(Vec::new())
595 }
596
597 /// The cookie contest (§4.3.2, L2107-2135). Identical cookies are a
598 /// collision the draft says must not connect (L2119-2124) — surfaced by
599 /// the caller as [`RejectionReason::RdvCookie`].
600 fn resolve_role(&self, peer_cookie: u32) -> Result<RendezvousRole> {
601 if peer_cookie == self.own_cookie {
602 return Err(Error::InvalidField {
603 what: "rendezvous cookie",
604 reason: "identical to the peer's cookie (collision, L2119-2124)",
605 });
606 }
607 Ok(if self.own_cookie > peer_cookie {
608 RendezvousRole::Initiator
609 } else {
610 RendezvousRole::Responder
611 })
612 }
613
614 fn reject(&mut self, reason: RejectionReason) -> Vec<HandshakeOutput> {
615 self.state = RendezvousHandshakeState::Rejected;
616 vec![HandshakeOutput::Rejected(reason)]
617 }
618
619 fn resend(&mut self) -> Vec<HandshakeOutput> {
620 match &self.last_sent {
621 Some(bytes) => vec![HandshakeOutput::Send(bytes.clone())],
622 None => Vec::new(),
623 }
624 }
625
626 /// Builds and sends a CONCLUSION: `ext_type` picks HSREQ/HSRSP, or `None`
627 /// for the extension-less greeting (§4.3.2.2, L2286-2288: no Stream ID /
628 /// Group Membership is ever attached — see module docs).
629 fn send_conclusion(&mut self, ext_type: Option<ExtensionType>) -> Result<Vec<HandshakeOutput>> {
630 let (ext_bytes, ext_flags): (Vec<u8>, u16) = match ext_type {
631 None => (Vec::new(), 0),
632 Some(t) => {
633 let hs_msg = HsExtMessage {
634 srt_version: self.config.srt_version,
635 srt_flags: self.config.flags,
636 receiver_tsbpd_delay_ms: self.config.latency_ms,
637 sender_tsbpd_delay_ms: self.config.latency_ms,
638 };
639 handshake_sm::build_conclusion_extensions(t, &hs_msg, None, None)?
640 }
641 };
642 let hp = HandshakePacket {
643 timestamp: 0,
644 dest_socket_id: self.peer_socket_id,
645 version: HANDSHAKE_VERSION_5,
646 encryption_field: self.config.encryption_field,
647 extension_field: HandshakeExtensionFlags(ext_flags),
648 initial_seq_number: self.config.initial_seq_number,
649 mtu: self.config.mtu,
650 max_flow_window_size: self.config.max_flow_window_size,
651 handshake_type: HandshakeType::Conclusion,
652 srt_socket_id: self.own_socket_id,
653 syn_cookie: self.own_cookie,
654 peer_ip: self.config.local_ip,
655 extensions: HandshakeExtensions(&ext_bytes),
656 };
657 let bytes = handshake_sm::build_bytes(hp)?;
658 self.last_sent = Some(bytes.clone());
659 self.ticks_since_send = 0;
660 self.retries = 0;
661 Ok(vec![HandshakeOutput::Send(bytes)])
662 }
663
664 /// Builds and sends an AGREEMENT: no extensions (§4.3.2.1, L2224-2230).
665 fn send_agreement(&mut self) -> Result<Vec<HandshakeOutput>> {
666 let hp = HandshakePacket {
667 timestamp: 0,
668 dest_socket_id: self.peer_socket_id,
669 version: HANDSHAKE_VERSION_5,
670 encryption_field: EncryptionField::NoEncryption,
671 extension_field: HandshakeExtensionFlags(0),
672 initial_seq_number: self.config.initial_seq_number,
673 mtu: self.config.mtu,
674 max_flow_window_size: self.config.max_flow_window_size,
675 handshake_type: HandshakeType::Agreement,
676 srt_socket_id: self.own_socket_id,
677 syn_cookie: self.own_cookie,
678 peer_ip: self.config.local_ip,
679 extensions: HandshakeExtensions(&[]),
680 };
681 let bytes = handshake_sm::build_bytes(hp)?;
682 self.last_sent = Some(bytes.clone());
683 self.ticks_since_send = 0;
684 self.retries = 0;
685 Ok(vec![HandshakeOutput::Send(bytes)])
686 }
687
688 /// Reaches [`RendezvousHandshakeState::Connected`]: builds
689 /// [`NegotiatedParams`] from `self.peer_hs_msg` (always captured by every
690 /// caller of this method before it is called) and sends the AGREEMENT
691 /// every documented transition into Connected requires.
692 fn enter_connected(&mut self) -> Result<Vec<HandshakeOutput>> {
693 let negotiated = self.build_negotiated();
694 self.negotiated = Some(negotiated.clone());
695 self.state = RendezvousHandshakeState::Connected;
696 let mut out = self.send_agreement()?;
697 out.push(HandshakeOutput::Connected(negotiated));
698 Ok(out)
699 }
700
701 fn build_negotiated(&self) -> NegotiatedParams {
702 let peer_msg = self
703 .peer_hs_msg
704 .expect("peer_hs_msg is always captured before any transition reaches Connected");
705 NegotiatedParams {
706 version: HANDSHAKE_VERSION_5,
707 flags: HandshakeExtensionMessageFlags(self.config.flags.0 & peer_msg.srt_flags.0),
708 latency_ms: handshake_sm::negotiate_latency_ms(self.config.latency_ms, &peer_msg),
709 own_socket_id: self.own_socket_id,
710 peer_socket_id: self.peer_socket_id,
711 // §4.3.2 never mentions a Stream ID / Group Membership exchange —
712 // module docs "Resolved ambiguities". The same applies to §6.1.5
713 // Key Material Exchange: this engine does not implement it (see
714 // the crate root docs' explicit-follow-ups list), so encryption
715 // is never negotiated over Rendezvous.
716 stream_id: None,
717 group: None,
718 #[cfg(feature = "crypto")]
719 sek: None,
720 #[cfg(feature = "crypto")]
721 salt: None,
722 }
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use crate::packet::handshake::HS_EXT_FLAG_HSREQ;
730
731 #[test]
732 fn start_is_idempotent_guard() {
733 let mut r = RendezvousHandshake::new(1, 500, HandshakeConfig::default());
734 assert!(r.start().is_ok());
735 assert!(r.start().is_err());
736 }
737
738 #[test]
739 fn wavehand_wire_values_match_draft_4_3_2() {
740 let mut r = RendezvousHandshake::new(0xAAAA_BBBB, 0xC0FF_EE00, HandshakeConfig::default());
741 let bytes = r.start().unwrap();
742 let pkt = ControlPacket::parse(&bytes).unwrap();
743 match pkt {
744 ControlPacket::Handshake(hp) => {
745 assert_eq!(hp.version, HANDSHAKE_VERSION_5);
746 assert_eq!(hp.handshake_type, HandshakeType::Wavehand);
747 assert_eq!(hp.srt_socket_id, 0xAAAA_BBBB);
748 assert_eq!(hp.syn_cookie, 0xC0FF_EE00);
749 assert_eq!(hp.extension_field.0, 0);
750 }
751 _ => panic!("expected handshake"),
752 }
753 assert_eq!(r.state(), RendezvousHandshakeState::Waving);
754 }
755
756 fn wavehand(socket_id: u32, cookie: u32) -> ControlPacket<'static> {
757 ControlPacket::Handshake(HandshakePacket {
758 timestamp: 0,
759 dest_socket_id: 0,
760 version: HANDSHAKE_VERSION_5,
761 encryption_field: EncryptionField::NoEncryption,
762 extension_field: HandshakeExtensionFlags(0),
763 initial_seq_number: 0,
764 mtu: 1500,
765 max_flow_window_size: 8192,
766 handshake_type: HandshakeType::Wavehand,
767 srt_socket_id: socket_id,
768 syn_cookie: cookie,
769 peer_ip: [0; 4],
770 extensions: HandshakeExtensions(&[]),
771 })
772 }
773
774 #[test]
775 fn greater_cookie_wins_initiator() {
776 let mut a = RendezvousHandshake::new(1, 500, HandshakeConfig::default());
777 let mut b = RendezvousHandshake::new(2, 100, HandshakeConfig::default());
778 a.start().unwrap();
779 b.start().unwrap();
780
781 a.feed(&wavehand(2, 100)).unwrap();
782 b.feed(&wavehand(1, 500)).unwrap();
783
784 assert_eq!(a.role(), Some(RendezvousRole::Initiator));
785 assert_eq!(b.role(), Some(RendezvousRole::Responder));
786 assert_eq!(a.state(), RendezvousHandshakeState::Attention);
787 assert_eq!(b.state(), RendezvousHandshakeState::Attention);
788 }
789
790 #[test]
791 fn identical_cookies_are_rejected_as_a_collision() {
792 let mut a = RendezvousHandshake::new(1, 0x00C0_FFEE, HandshakeConfig::default());
793 a.start().unwrap();
794 let outputs = a.feed(&wavehand(2, 0x00C0_FFEE)).unwrap();
795 assert_eq!(
796 outputs,
797 vec![HandshakeOutput::Rejected(RejectionReason::RdvCookie)]
798 );
799 assert_eq!(a.state(), RendezvousHandshakeState::Rejected);
800 }
801
802 #[test]
803 fn initiator_attention_entry_sends_hsreq() {
804 let mut a = RendezvousHandshake::new(1, 500, HandshakeConfig::default());
805 a.start().unwrap();
806 let outputs = a.feed(&wavehand(2, 100)).unwrap();
807 assert_eq!(outputs.len(), 1);
808 let bytes = match &outputs[0] {
809 HandshakeOutput::Send(b) => b.clone(),
810 other => panic!("expected Send, got {other:?}"),
811 };
812 let pkt = ControlPacket::parse(&bytes).unwrap();
813 match pkt {
814 ControlPacket::Handshake(hp) => {
815 assert_eq!(hp.handshake_type, HandshakeType::Conclusion);
816 assert_eq!(hp.extension_field.0 & HS_EXT_FLAG_HSREQ, HS_EXT_FLAG_HSREQ);
817 let blocks: Vec<_> = hp.extensions.iter().map(|b| b.unwrap()).collect();
818 assert_eq!(blocks.len(), 1);
819 assert_eq!(blocks[0].ext_type, ExtensionType::HsReq);
820 }
821 _ => panic!("expected handshake"),
822 }
823 }
824
825 #[test]
826 fn responder_attention_entry_sends_empty_conclusion() {
827 let mut b = RendezvousHandshake::new(2, 100, HandshakeConfig::default());
828 b.start().unwrap();
829 let outputs = b.feed(&wavehand(1, 500)).unwrap();
830 assert_eq!(outputs.len(), 1);
831 let bytes = match &outputs[0] {
832 HandshakeOutput::Send(b) => b.clone(),
833 other => panic!("expected Send, got {other:?}"),
834 };
835 let pkt = ControlPacket::parse(&bytes).unwrap();
836 match pkt {
837 ControlPacket::Handshake(hp) => {
838 assert_eq!(hp.handshake_type, HandshakeType::Conclusion);
839 assert_eq!(hp.extension_field.0, 0);
840 assert_eq!(hp.extensions.iter().count(), 0);
841 }
842 _ => panic!("expected handshake"),
843 }
844 }
845
846 #[test]
847 fn malformed_extension_mid_flow_is_rejected_not_panicking() {
848 let mut a = RendezvousHandshake::new(1, 500, HandshakeConfig::default());
849 a.start().unwrap();
850 a.feed(&wavehand(2, 100)).unwrap();
851 assert_eq!(a.state(), RendezvousHandshakeState::Attention);
852
853 // A CONCLUSION whose extension block declares a length far larger
854 // than the bytes actually present.
855 let bad_ext: &'static [u8] = &[0x00, 0x01, 0xFF, 0xFF];
856 let bad = ControlPacket::Handshake(HandshakePacket {
857 timestamp: 0,
858 dest_socket_id: 1,
859 version: HANDSHAKE_VERSION_5,
860 encryption_field: EncryptionField::NoEncryption,
861 extension_field: HandshakeExtensionFlags(HS_EXT_FLAG_HSREQ),
862 initial_seq_number: 0,
863 mtu: 1500,
864 max_flow_window_size: 8192,
865 handshake_type: HandshakeType::Conclusion,
866 srt_socket_id: 2,
867 syn_cookie: 100,
868 peer_ip: [0; 4],
869 extensions: HandshakeExtensions(bad_ext),
870 });
871 let outputs = a.feed(&bad).unwrap();
872 assert_eq!(
873 outputs,
874 vec![HandshakeOutput::Rejected(RejectionReason::Rogue)]
875 );
876 assert_eq!(a.state(), RendezvousHandshakeState::Rejected);
877 }
878
879 #[test]
880 fn feed_before_start_is_out_of_sequence() {
881 let mut r = RendezvousHandshake::new(1, 500, HandshakeConfig::default());
882 assert!(matches!(
883 r.feed(&wavehand(2, 100)),
884 Err(Error::HandshakeOutOfSequence { .. })
885 ));
886 }
887
888 #[test]
889 fn feed_rejects_non_handshake_packets_outside_recovery_case() {
890 use crate::packet::misc::KeepAlivePacket;
891 let mut r = RendezvousHandshake::new(1, 500, HandshakeConfig::default());
892 r.start().unwrap();
893 let ka = ControlPacket::KeepAlive(KeepAlivePacket {
894 timestamp: 0,
895 dest_socket_id: 0,
896 });
897 assert!(matches!(
898 r.feed(&ka),
899 Err(Error::UnexpectedControlPacket { .. })
900 ));
901 }
902}