Skip to main content

running_process/broker/server/handoff/
wire.rs

1//! Production wire-frame handoff delivery over a broker↔backend control
2//! connection (#354, slice 6).
3//!
4//! Earlier slices abstracted delivery of the `(handle value, token)` pair
5//! behind [`HandoffDelivery`] because the v1
6//! envelope reserved no backend→broker ACK frame. This module closes that
7//! gap with two envelope messages riding the existing v1 frame layout on a
8//! framed broker↔backend connection:
9//!
10//! - [`HandoffOffer`] (broker → backend, `FRAME_KIND_REQUEST`): the
11//!   duplicated handle value (Windows; zero on Unix where the fd travels
12//!   via `SCM_RIGHTS`), the 16-byte one-time token, the service name, and
13//!   a correlation id.
14//! - [`HandoffAck`] (backend → broker, `FRAME_KIND_RESPONSE`): the token
15//!   echo, an accepted flag plus error detail, and the correlation id echo.
16//!
17//! Both ride `Frame.payload` under [`HANDOFF_PAYLOAD_PROTOCOL`], mirroring
18//! how Hello (`0x00`), admin verbs (`0xAD01`), and endpoint probes
19//! (`0xB232`) share the envelope.
20//!
21//! [`WireHandoffDelivery`] implements [`HandoffDelivery`] over any framed
22//! `Read + Write` stream (the same local-socket framing used
23//! by every other broker connection). Any malformed frame, token-echo
24//! mismatch, correlation-id mismatch, refused ACK, or overdue ACK is
25//! reported as a delivery error; the orchestration in
26//! [`super::orchestrate`] then revokes the token and falls back to the
27//! `backend_pipe` reconnect path. This module never panics on wire input.
28//!
29//! # Deadline enforcement
30//!
31//! [`WireHandoffDelivery::await_backend_ack`] wraps its framed read in a
32//! wall-clock deadline. Production local sockets enter nonblocking mode when
33//! the delivery is constructed, before offer/ACK traffic begins, and restore
34//! blocking behavior afterward. A closed/erroring stream surfaces
35//! immediately. Independently, an ACK observed after `deadline` is rejected
36//! here, and the
37//! [`HandoffAckRegistry`](super::HandoffAckRegistry) re-validates the
38//! observation instant against the deadline registered at issuance, so a
39//! slow stream can never complete an overdue handoff.
40
41use std::io::{Read, Write};
42use std::time::Instant;
43
44use prost::Message;
45
46use crate::broker::protocol::{
47    read_frame, validate_frame_envelope, write_frame, Frame, FrameKind, FrameValidationError,
48    HandoffAck, HandoffOffer, PayloadEncoding, PROTOCOL_VERSION,
49};
50use crate::broker::server::handoff::handoff_token::HandoffToken;
51use crate::broker::server::handoff::orchestrate::{HandoffDelivery, HandoffDeliveryError};
52use crate::broker::server::handoff::windows::WindowsHandleValue;
53use crate::broker::server::deadline_stream::DeadlineStream;
54
55/// Payload protocol reserved for broker↔backend handoff offer/ACK frames.
56///
57/// Re-exported from the authoritative
58/// [`registry`](crate::broker::protocol::registry), which owns every v1
59/// payload-protocol ID (#375). Lives in the same envelope-multiplexing
60/// space as the control plane (`0x00`), admin verbs (`0xAD01`), and
61/// backend-handle endpoint probes (`0xB232`).
62pub use crate::broker::protocol::registry::HANDOFF_PAYLOAD_PROTOCOL;
63
64/// Build the v1 frame carrying one broker→backend [`HandoffOffer`].
65pub fn handoff_offer_frame(offer: &HandoffOffer) -> Frame {
66    let mut payload = Vec::with_capacity(64);
67    offer.encode(&mut payload).expect(
68        "prost encoding HandoffOffer into Vec cannot fail because Vec writes are infallible",
69    );
70    Frame {
71        envelope_version: PROTOCOL_VERSION,
72        kind: FrameKind::Request as i32,
73        payload_protocol: HANDOFF_PAYLOAD_PROTOCOL,
74        payload,
75        request_id: offer.correlation_id,
76        payload_encoding: PayloadEncoding::None as i32,
77        deadline_unix_ms: 0,
78        traceparent: String::new(),
79        tracestate: String::new(),
80    }
81}
82
83/// Build the v1 frame carrying one backend→broker [`HandoffAck`].
84pub fn handoff_ack_frame(ack: &HandoffAck) -> Frame {
85    let mut payload = Vec::with_capacity(64);
86    ack.encode(&mut payload)
87        .expect("prost encoding HandoffAck into Vec cannot fail because Vec writes are infallible");
88    Frame {
89        envelope_version: PROTOCOL_VERSION,
90        kind: FrameKind::Response as i32,
91        payload_protocol: HANDOFF_PAYLOAD_PROTOCOL,
92        payload,
93        request_id: ack.correlation_id,
94        payload_encoding: PayloadEncoding::None as i32,
95        deadline_unix_ms: 0,
96        traceparent: String::new(),
97        tracestate: String::new(),
98    }
99}
100
101/// Build the v1 frame relaying one completed handoff to the CLIENT (#354,
102/// slice 7).
103///
104/// After the backend ACKs a [`HandoffOffer`], the broker relays the
105/// backend's [`HandoffAck`] verbatim to the waiting client on the client's
106/// original broker connection — the same socket that carried Hello and is
107/// now backend-served. The relay rides the envelope as a broker→client push
108/// (`FRAME_KIND_EVENT`) under [`HANDOFF_PAYLOAD_PROTOCOL`], mirroring how
109/// the offer/ACK pair rides the broker↔backend control connection. The
110/// client matches the relay by the one-time token echo (the only handoff
111/// secret it knows); the correlation id is broker↔backend bookkeeping that
112/// the client does not validate.
113pub fn handoff_ready_frame(ack: &HandoffAck) -> Frame {
114    let mut payload = Vec::with_capacity(64);
115    ack.encode(&mut payload)
116        .expect("prost encoding HandoffAck into Vec cannot fail because Vec writes are infallible");
117    Frame {
118        envelope_version: PROTOCOL_VERSION,
119        kind: FrameKind::Event as i32,
120        payload_protocol: HANDOFF_PAYLOAD_PROTOCOL,
121        payload,
122        request_id: ack.correlation_id,
123        payload_encoding: PayloadEncoding::None as i32,
124        deadline_unix_ms: 0,
125        traceparent: String::new(),
126        tracestate: String::new(),
127    }
128}
129
130/// Validate the envelope fields shared by both handoff frame directions.
131///
132/// Returns the expected-vs-actual mismatch as a static description so both
133/// the broker and backend sides report wire violations uniformly.
134pub fn validate_handoff_frame(frame: &Frame, expected_kind: FrameKind) -> Result<(), &'static str> {
135    validate_frame_envelope(frame, expected_kind, HANDOFF_PAYLOAD_PROTOCOL).map_err(|error| {
136        match error {
137            FrameValidationError::EnvelopeVersion { .. } => "envelope_version is not v1",
138            FrameValidationError::Kind { .. } => match expected_kind {
139                FrameKind::Request => "kind is not REQUEST",
140                FrameKind::Event => "kind is not EVENT",
141                _ => "kind is not RESPONSE",
142            },
143            FrameValidationError::PayloadProtocol { .. } => "payload_protocol is not handoff",
144            FrameValidationError::PayloadEncoding { .. } => "payload is compressed",
145        }
146    })
147}
148
149/// [`HandoffDelivery`] implementation that sends [`HandoffOffer`] frames to
150/// the backend over a framed control connection and waits for the matching
151/// [`HandoffAck`].
152///
153/// `deliver` writes one offer frame; `await_backend_ack` reads one response
154/// frame and requires the token echo, the correlation id, and the accepted
155/// flag to all match. Every violation is a delivery error — the
156/// orchestration falls back to reconnect and revokes the token.
157#[derive(Debug)]
158pub struct WireHandoffDelivery<S> {
159    stream: S,
160    service_name: String,
161    correlation_id: u64,
162    configure_ack_bounded_io: Option<fn(&S, bool) -> std::io::Result<()>>,
163    ack_setup_error: Option<String>,
164    io_deadline: Instant,
165}
166
167impl<S> WireHandoffDelivery<S> {
168    /// Wrap a framed broker↔backend connection for one handoff.
169    ///
170    /// `correlation_id` ties the offer to its ACK; reuse the request or
171    /// connection id of the client Hello that triggered the handoff.
172    pub fn new_preconfigured_nonblocking(
173        stream: S,
174        service_name: impl Into<String>,
175        correlation_id: u64,
176        io_deadline: Instant,
177    ) -> Self {
178        Self {
179            stream,
180            service_name: service_name.into(),
181            correlation_id,
182            configure_ack_bounded_io: None,
183            ack_setup_error: None,
184            io_deadline,
185        }
186    }
187
188    /// Legacy constructor for arbitrary framed transports.
189    ///
190    /// This preserves the original API but cannot arrange nonblocking I/O for
191    /// an unknown stream type. New generic callers should use
192    /// [`Self::new_preconfigured_nonblocking`].
193    #[deprecated(
194        note = "use new_preconfigured_nonblocking, or new_local_socket for production sockets"
195    )]
196    pub fn new(stream: S, service_name: impl Into<String>, correlation_id: u64) -> Self {
197        Self {
198            stream,
199            service_name: service_name.into(),
200            correlation_id,
201            configure_ack_bounded_io: None,
202            ack_setup_error: None,
203            io_deadline: Instant::now() + std::time::Duration::from_secs(30),
204        }
205    }
206
207    /// Return the correlation id stamped on the offer and required on the ACK.
208    pub fn correlation_id(&self) -> u64 {
209        self.correlation_id
210    }
211
212    /// Borrow the underlying connection (e.g. to read its raw fd for the
213    /// Unix `SCM_RIGHTS` send that precedes the offer frame).
214    pub fn stream(&self) -> &S {
215        &self.stream
216    }
217
218    /// Unwrap the underlying connection (e.g. to keep using it after a
219    /// completed handoff).
220    pub fn into_stream(self) -> S {
221        self.stream
222    }
223}
224
225impl WireHandoffDelivery<interprocess::local_socket::Stream> {
226    /// Wrap a production local socket and enforce ACK deadlines with the
227    /// platform's bounded-read mechanism.
228    pub fn new_local_socket(
229        stream: interprocess::local_socket::Stream,
230        service_name: impl Into<String>,
231        correlation_id: u64,
232        io_deadline: Instant,
233    ) -> Self {
234        use interprocess::local_socket::traits::Stream as _;
235        let ack_setup_error = stream
236            .set_nonblocking(true)
237            .err()
238            .map(|error| format!("failed to enable bounded HandoffAck reads: {error}"));
239        Self {
240            stream,
241            service_name: service_name.into(),
242            correlation_id,
243            configure_ack_bounded_io: Some(|stream, bounded| {
244                use interprocess::local_socket::traits::Stream as _;
245                stream.set_nonblocking(bounded)
246            }),
247            ack_setup_error,
248            io_deadline,
249        }
250    }
251}
252
253impl<S: Read + Write> HandoffDelivery for WireHandoffDelivery<S> {
254    fn deliver(
255        &mut self,
256        handle: WindowsHandleValue,
257        token: &HandoffToken,
258    ) -> Result<(), HandoffDeliveryError> {
259        if let Some(error) = self.ack_setup_error.as_ref() {
260            return Err(HandoffDeliveryError::DeliveryFailed {
261                detail: error.clone(),
262            });
263        }
264        let offer = HandoffOffer {
265            handle_value: handle.get() as u64,
266            token: token.as_bytes().to_vec(),
267            service_name: self.service_name.clone(),
268            correlation_id: self.correlation_id,
269        };
270        let frame = handoff_offer_frame(&offer);
271        let mut bytes = Vec::with_capacity(64);
272        frame
273            .encode(&mut bytes)
274            .expect("prost encoding Frame into Vec cannot fail because Vec writes are infallible");
275        let mut deadline_stream = DeadlineStream::new(&mut self.stream, self.io_deadline);
276        if let Err(write_error) = write_frame(&mut deadline_stream, &bytes) {
277            let restore_error = self
278                .configure_ack_bounded_io
279                .and_then(|configure| configure(&self.stream, false).err());
280            let detail = match restore_error {
281                Some(restore_error) => format!(
282                    "failed to write HandoffOffer frame: {write_error}; additionally failed to \
283                     restore blocking mode: {restore_error}"
284                ),
285                None => format!("failed to write HandoffOffer frame: {write_error}"),
286            };
287            return Err(HandoffDeliveryError::DeliveryFailed { detail });
288        }
289        Ok(())
290    }
291
292    fn await_backend_ack(
293        &mut self,
294        token: &HandoffToken,
295        deadline: Instant,
296    ) -> Result<Instant, HandoffDeliveryError> {
297        if let Some(error) = self.ack_setup_error.take() {
298            return Err(ack_not_observed(error));
299        }
300        let read_result = {
301            let mut deadline_stream = DeadlineStream::new(&mut self.stream, deadline);
302            read_frame(&mut deadline_stream)
303        };
304        let restore_result = self
305            .configure_ack_bounded_io
306            .map(|configure| configure(&self.stream, false))
307            .unwrap_or(Ok(()));
308        let bytes = match (read_result, restore_result) {
309            (Ok(bytes), Ok(())) => bytes,
310            (Err(read_error), Ok(())) => {
311                return Err(ack_not_observed(format!(
312                    "failed to read HandoffAck frame: {read_error}"
313                )));
314            }
315            (Ok(_), Err(restore_error)) => {
316                return Err(ack_not_observed(format!(
317                    "failed to restore blocking HandoffAck stream: {restore_error}"
318                )));
319            }
320            (Err(read_error), Err(restore_error)) => {
321                return Err(ack_not_observed(format!(
322                    "failed to read HandoffAck frame: {read_error}; additionally failed to \
323                     restore blocking mode: {restore_error}"
324                )));
325            }
326        };
327        let observed_at = Instant::now();
328        let frame = Frame::decode(bytes.as_slice()).map_err(|error| {
329            ack_not_observed(format!("failed to decode HandoffAck Frame: {error}"))
330        })?;
331        validate_handoff_frame(&frame, FrameKind::Response)
332            .map_err(|detail| ack_not_observed(format!("unexpected HandoffAck frame: {detail}")))?;
333        if frame.request_id != self.correlation_id {
334            return Err(ack_not_observed(format!(
335                "HandoffAck frame request_id {} does not match correlation id {}",
336                frame.request_id, self.correlation_id
337            )));
338        }
339        let ack = HandoffAck::decode(frame.payload.as_slice()).map_err(|error| {
340            ack_not_observed(format!("failed to decode HandoffAck payload: {error}"))
341        })?;
342        if ack.correlation_id != self.correlation_id {
343            return Err(ack_not_observed(format!(
344                "HandoffAck correlation id {} does not match offer correlation id {}",
345                ack.correlation_id, self.correlation_id
346            )));
347        }
348        if ack.token != token.as_bytes() {
349            return Err(ack_not_observed(
350                "HandoffAck token echo does not match the offered token".to_string(),
351            ));
352        }
353        if !ack.accepted {
354            return Err(ack_not_observed(format!(
355                "backend refused the handoff: {}",
356                if ack.error_detail.is_empty() {
357                    "no detail provided"
358                } else {
359                    ack.error_detail.as_str()
360                }
361            )));
362        }
363        if observed_at > deadline {
364            return Err(ack_not_observed(
365                "backend HandoffAck arrived after the ACK deadline".to_string(),
366            ));
367        }
368        Ok(observed_at)
369    }
370}
371
372fn ack_not_observed(detail: String) -> HandoffDeliveryError {
373    HandoffDeliveryError::AckNotObserved { detail }
374}