Skip to main content

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}