Skip to main content

libcfd_rpc/
rpc.rs

1use crate::error::{Result, RpcError};
2use crate::io::{AsyncStream, read_message};
3use crate::rpc_capnp;
4
5/// A minimal Cap'n Proto RPC client for tunnel registration.
6///
7/// Implements the exact wire sequence cloudflared uses on its control
8/// stream: bootstrap to obtain the peer's main interface, then
9/// `call`/`return`/`finish` for each registration method. Question ids are
10/// allocated monotonically from 0 and the bootstrapped capability is
11/// tracked as import id 0.
12///
13/// All capnp message construction and decoding happens in synchronous
14/// helpers so no non-`Send` capnp state is held across an await, keeping
15/// every future `Send`.
16pub struct RpcClient<S> {
17    stream: S,
18    next_question: u32,
19    has_bootstrap: bool,
20}
21
22enum BootstrapOutcome {
23    Capability,
24    Exception(String),
25    NoCapability,
26    UnexpectedKind,
27}
28
29enum CallOutcome<T> {
30    Value(T),
31    Exception(String),
32    UnexpectedKind,
33}
34
35impl<S: AsyncStream + Unpin> RpcClient<S> {
36    /// Wraps a bidirectional stream as an RPC client.
37    pub fn new(stream: S) -> Self {
38        Self {
39            stream,
40            next_question: 0,
41            has_bootstrap: false,
42        }
43    }
44
45    /// Returns the underlying stream (the connection stays open).
46    pub fn into_inner(self) -> S {
47        self.stream
48    }
49
50    /// Sends `Message::bootstrap` and waits for the return carrying the
51    /// peer's main interface. Returns the import id to use for calls.
52    pub async fn bootstrap(&mut self) -> Result<u32> {
53        if self.has_bootstrap {
54            return Err(RpcError::Protocol("bootstrap called twice".into()));
55        }
56        let question = self.next_question;
57        self.next_question += 1;
58
59        tracing::trace!(question, "sending bootstrap");
60        let bytes = build_bootstrap(question)?;
61        crate::io::write_raw(&mut self.stream, &bytes).await?;
62        let reader = read_message(&mut self.stream).await?;
63        let outcome = decode_bootstrap(&reader, question)?;
64        drop(reader);
65        match outcome {
66            BootstrapOutcome::Capability => {
67                self.has_bootstrap = true;
68                self.send_finish(question, false).await?;
69                Ok(0)
70            }
71            BootstrapOutcome::Exception(reason) => {
72                self.send_finish(question, true).await?;
73                Err(RpcError::RemoteCall(reason))
74            }
75            BootstrapOutcome::NoCapability => {
76                self.send_finish(question, true).await?;
77                Err(RpcError::Protocol(
78                    "bootstrap return carried no capability".into(),
79                ))
80            }
81            BootstrapOutcome::UnexpectedKind => {
82                self.send_finish(question, true).await?;
83                Err(RpcError::Protocol(
84                    "bootstrap got unexpected return kind".into(),
85                ))
86            }
87        }
88    }
89
90    /// Performs a method call on an imported capability, decodes the results
91    /// payload with `decode`, then sends `finish` for the question.
92    pub async fn call<T>(
93        &mut self,
94        import_identifier: u32,
95        interface_identifier: u64,
96        method_identifier: u16,
97        fill_parameters: impl FnOnce(&mut rpc_capnp::payload::Builder<'_>) -> Result<()>,
98        decode: impl FnOnce(rpc_capnp::payload::Reader<'_>) -> Result<T>,
99    ) -> Result<T> {
100        if !self.has_bootstrap {
101            return Err(RpcError::Protocol("call before bootstrap".into()));
102        }
103        let question = self.next_question;
104        self.next_question += 1;
105
106        tracing::trace!(
107            question,
108            interface_identifier,
109            method_identifier,
110            "sending call"
111        );
112        let bytes = build_call(
113            question,
114            import_identifier,
115            interface_identifier,
116            method_identifier,
117            fill_parameters,
118        )?;
119        crate::io::write_raw(&mut self.stream, &bytes).await?;
120        let reader = read_message(&mut self.stream).await?;
121        let outcome = decode_call(&reader, question, decode)?;
122        drop(reader);
123        match outcome {
124            CallOutcome::Value(value) => {
125                self.send_finish(question, false).await?;
126                Ok(value)
127            }
128            CallOutcome::Exception(reason) => {
129                self.send_finish(question, true).await?;
130                Err(RpcError::RemoteCall(reason))
131            }
132            CallOutcome::UnexpectedKind => {
133                self.send_finish(question, true).await?;
134                Err(RpcError::Protocol("call got unexpected return kind".into()))
135            }
136        }
137    }
138
139    /// Sends `Message::finish` for a resolved or failed question. Capnp-go
140    /// sends `releaseResultCaps: true` when the return was an exception.
141    async fn send_finish(&mut self, question: u32, release_result_caps: bool) -> Result<()> {
142        let bytes = build_finish(question, release_result_caps)?;
143        crate::io::write_raw(&mut self.stream, &bytes).await
144    }
145
146    /// Releases the bootstrapped capability and returns the underlying
147    /// stream. Sends `Message::release` for import id 0, mirroring what
148    /// capnp-go sends when a registration client is closed.
149    pub async fn close(mut self) -> Result<S> {
150        if self.has_bootstrap {
151            let bytes = build_release(0, 1)?;
152            crate::io::write_raw(&mut self.stream, &bytes).await?;
153            self.has_bootstrap = false;
154        }
155        Ok(self.stream)
156    }
157}
158
159fn decode_bootstrap(
160    reader: &capnp::message::Reader<capnp::serialize::OwnedSegments>,
161    question: u32,
162) -> Result<BootstrapOutcome> {
163    let root = reader.get_root::<rpc_capnp::message::Reader>()?;
164    let answer = expect_return(&root, question)?;
165    let payload = match answer.reborrow().which()? {
166        rpc_capnp::return_::Results(r) => r?,
167        rpc_capnp::return_::Exception(e) => {
168            return Ok(BootstrapOutcome::Exception(
169                e?.get_reason()?.to_str()?.to_string(),
170            ));
171        }
172        _ => return Ok(BootstrapOutcome::UnexpectedKind),
173    };
174    let ctab = payload.get_cap_table()?;
175    if ctab.is_empty() {
176        return Ok(BootstrapOutcome::NoCapability);
177    }
178    let desc = ctab.get(0);
179    match desc.reborrow().which()? {
180        rpc_capnp::cap_descriptor::SenderHosted(_)
181        | rpc_capnp::cap_descriptor::SenderPromise(_) => Ok(BootstrapOutcome::Capability),
182        _ => Ok(BootstrapOutcome::UnexpectedKind),
183    }
184}
185
186fn decode_call<T>(
187    reader: &capnp::message::Reader<capnp::serialize::OwnedSegments>,
188    question: u32,
189    decode: impl FnOnce(rpc_capnp::payload::Reader<'_>) -> Result<T>,
190) -> Result<CallOutcome<T>> {
191    let root = reader.get_root::<rpc_capnp::message::Reader>()?;
192    let answer = expect_return(&root, question)?;
193    match answer.reborrow().which()? {
194        rpc_capnp::return_::Results(r) => Ok(CallOutcome::Value(decode(r?)?)),
195        rpc_capnp::return_::Exception(e) => Ok(CallOutcome::Exception(
196            e?.get_reason()?.to_str()?.to_string(),
197        )),
198        _ => Ok(CallOutcome::UnexpectedKind),
199    }
200}
201
202fn expect_return<'a>(
203    root: &'a rpc_capnp::message::Reader<'a>,
204    question: u32,
205) -> Result<rpc_capnp::return_::Reader<'a>> {
206    match root.reborrow().which()? {
207        rpc_capnp::message::Return(ret) => {
208            let ret = ret?;
209            if ret.reborrow().get_answer_id() != question {
210                return Err(RpcError::Protocol(format!(
211                    "answer id {} does not match question {}",
212                    ret.reborrow().get_answer_id(),
213                    question
214                )));
215            }
216            Ok(ret)
217        }
218        rpc_capnp::message::Abort(exc) => {
219            let exc = exc?;
220            let reason = exc.get_reason()?.to_str()?.to_string();
221            let error_type = exc.get_type()? as u16;
222            Err(RpcError::Abort { reason, error_type })
223        }
224        _ => Err(RpcError::Protocol("expected return message".into())),
225    }
226}
227
228fn build_bootstrap(question: u32) -> Result<Vec<u8>> {
229    let mut message = capnp::message::Builder::new_default();
230    let root = message.init_root::<rpc_capnp::message::Builder>();
231    let mut bs = root.init_bootstrap();
232    bs.set_question_id(question);
233    Ok(crate::io::serialize_message(&message))
234}
235
236fn build_call<F>(
237    question: u32,
238    import_identifier: u32,
239    interface_identifier: u64,
240    method_identifier: u16,
241    fill_parameters: F,
242) -> Result<Vec<u8>>
243where
244    F: FnOnce(&mut rpc_capnp::payload::Builder<'_>) -> Result<()>,
245{
246    let mut message = capnp::message::Builder::new_default();
247    let root = message.init_root::<rpc_capnp::message::Builder>();
248    let mut call = root.init_call();
249    call.set_question_id(question);
250    let mut target = call.reborrow().init_target();
251    target.set_imported_cap(import_identifier);
252    call.reborrow().set_interface_id(interface_identifier);
253    call.reborrow().set_method_id(method_identifier);
254    call.reborrow().init_send_results_to().set_caller(());
255    let mut payload = call.reborrow().init_params();
256    fill_parameters(&mut payload)?;
257    Ok(crate::io::serialize_message(&message))
258}
259
260fn build_finish(question: u32, release_result_caps: bool) -> Result<Vec<u8>> {
261    let mut finish = capnp::message::Builder::new_default();
262    let froot = finish.init_root::<rpc_capnp::message::Builder>();
263    let mut f = froot.init_finish();
264    f.set_question_id(question);
265    f.set_release_result_caps(release_result_caps);
266    Ok(crate::io::serialize_message(&finish))
267}
268
269fn build_release(identifier: u32, reference_count: u32) -> Result<Vec<u8>> {
270    let mut message = capnp::message::Builder::new_default();
271    let root = message.init_root::<rpc_capnp::message::Builder>();
272    let mut rel = root.init_release();
273    rel.set_id(identifier);
274    rel.set_reference_count(reference_count);
275    Ok(crate::io::serialize_message(&message))
276}
277
278/// Replies to a call with an `unimplemented` exception, mirroring what
279/// capnp-go sends when a server does not implement a method.
280pub async fn send_exception<S: AsyncStream + Unpin>(
281    stream: &mut S,
282    question_identifier: u32,
283    reason: &str,
284) -> Result<()> {
285    let bytes = build_exception(question_identifier, reason)?;
286    crate::io::write_raw(stream, &bytes).await
287}
288
289/// Builds the framed bytes for an `unimplemented` exception return,
290/// mirroring what capnp-go sends when a server does not implement a method.
291pub(crate) fn build_exception(question_identifier: u32, reason: &str) -> Result<Vec<u8>> {
292    let mut message = capnp::message::Builder::new_default();
293    let root = message.init_root::<rpc_capnp::message::Builder>();
294    let mut ret = root.init_return();
295    ret.set_answer_id(question_identifier);
296    let mut exc = ret.init_exception();
297    exc.set_reason(reason);
298    exc.set_type(rpc_capnp::exception::Type::Unimplemented);
299    Ok(crate::io::serialize_message(&message))
300}
301
302/// A summary of an incoming RPC message, sufficient for a server to dispatch.
303#[derive(Debug, Clone, PartialEq)]
304pub enum Incoming {
305    /// A bootstrap request for the peer's main interface.
306    Bootstrap {
307        /// The question id to answer.
308        question_identifier: u32,
309    },
310    /// A method call.
311    Call {
312        /// The question id to answer.
313        question_identifier: u32,
314        /// The called interface.
315        interface_identifier: u64,
316        /// The called method.
317        method_identifier: u16,
318    },
319    /// A finish for a resolved question.
320    Finish {
321        /// The question id being released.
322        question_identifier: u32,
323    },
324    /// A capability release.
325    Release,
326    /// Any other message kind (kept so the enum is exhaustive).
327    Other,
328}
329
330/// Reads and classifies the next RPC message on a stream without exposing
331/// Cap'n Proto types. Returns `None` when the stream ends.
332pub async fn read_incoming<S: AsyncStream + Unpin>(stream: &mut S) -> Result<Option<Incoming>> {
333    let reader = match read_message(stream).await {
334        Ok(r) => r,
335        Err(RpcError::Eof) => return Ok(None),
336        Err(e) => return Err(e),
337    };
338    let root = reader.get_root::<rpc_capnp::message::Reader>()?;
339    match root.reborrow().which()? {
340        rpc_capnp::message::Bootstrap(b) => Ok(Some(Incoming::Bootstrap {
341            question_identifier: b?.get_question_id(),
342        })),
343        rpc_capnp::message::Call(c) => {
344            let c = c?;
345            Ok(Some(Incoming::Call {
346                question_identifier: c.reborrow().get_question_id(),
347                interface_identifier: c.reborrow().get_interface_id(),
348                method_identifier: c.reborrow().get_method_id(),
349            }))
350        }
351        rpc_capnp::message::Finish(f) => Ok(Some(Incoming::Finish {
352            question_identifier: f?.get_question_id(),
353        })),
354        rpc_capnp::message::Release(_) => Ok(Some(Incoming::Release)),
355        _ => Ok(Some(Incoming::Other)),
356    }
357}