Skip to main content

libfcp_core/
connection.rs

1// Copyright Nixort <https://github.com/Nixort> 2026.
2//
3// License: GNU General Public License v3.0 only.
4// You can find the license file in the project root.
5//
6// Federated CFR Connect Protocol (FCP).
7
8//! Per-peer FCP connection state machine and engine actions.
9
10use alloc::boxed::Box;
11use alloc::collections::VecDeque;
12use alloc::vec;
13use alloc::vec::Vec;
14
15use crate::envelope::{Body, Envelope};
16use crate::error::Error;
17use crate::identity::{EndpointIdentity, EndpointSigner};
18use crate::types::{
19    AttemptId, CloseCode, EnvelopeId, FederationId, WebRtcBinding, MAX_SEEN_ENVELOPES, PROTOCOL_ID,
20};
21
22/// Connection lifecycle state.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Phase {
25    /// No offer or answer exists.
26    Idle,
27    /// This endpoint has sent an offer.
28    OfferSent,
29    /// A valid offer from the configured peer awaits a local answer.
30    OfferReceived,
31    /// This endpoint has sent an answer and awaits transport connection.
32    AnswerSent,
33    /// This endpoint has received an answer and awaits transport connection.
34    AnswerReceived,
35    /// The platform adapter reported the WebRTC control channel connected.
36    Established,
37    /// A local or remote close finished this attempt.
38    Closed,
39}
40
41#[derive(Debug, Clone, Copy)]
42struct NegotiationIds {
43    offer: EnvelopeId,
44    answer: Option<EnvelopeId>,
45}
46
47/// An action emitted by the core for an untrusted transport or platform adapter.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum Action {
50    /// Send a signed envelope through any configured signaling or data path.
51    Send(Box<Envelope>),
52    /// Give an opaque offer description to the selected WebRTC engine.
53    ApplyOffer {
54        /// Signed digest binding exact description and DTLS fingerprint bytes.
55        binding: WebRtcBinding,
56        /// Exact bounded opaque description to give the platform engine.
57        description: Vec<u8>,
58    },
59    /// Give an opaque answer description to the selected WebRTC engine.
60    ApplyAnswer {
61        /// Signed digest binding exact description and DTLS fingerprint bytes.
62        binding: WebRtcBinding,
63        /// Exact bounded opaque description to give the platform engine.
64        description: Vec<u8>,
65    },
66    /// Add one opaque ICE candidate to the selected WebRTC engine.
67    AddCandidate {
68        /// Sender-local diagnostic sequence number.
69        sequence: u32,
70        /// Exact bounded opaque candidate to give the platform engine.
71        candidate: Vec<u8>,
72    },
73    /// Ask the adapter to configure/open the fixed reliable ordered FCP control channel.
74    OpenControlChannel,
75    /// Deliver an exact raw CFR payload with its verified FCP transport origin.
76    DeliverCfr {
77        /// Stable identifier of the verified signed FCP envelope carrying this payload.
78        envelope_id: EnvelopeId,
79        /// Complete FCP endpoint identity that signed the carrying envelope.
80        remote_endpoint: Box<EndpointIdentity>,
81        /// Unmodified CFR wire payload.
82        payload: Vec<u8>,
83    },
84    /// Close the selected platform connection.
85    CloseTransport {
86        /// Application close code from a signed remote envelope.
87        reason: CloseCode,
88    },
89}
90
91/// Fixed FCP data-channel configuration that platform adapters must enforce.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct ControlChannelConfig {
94    /// Stable data channel label.
95    pub label: &'static str,
96    /// Stable subprotocol string.
97    pub protocol: &'static str,
98    /// FCP control is ordered.
99    pub ordered: bool,
100    /// FCP control is fully reliable.
101    pub reliable: bool,
102    /// FCP control uses binary messages.
103    pub binary: bool,
104}
105
106/// Required FCP data-channel configuration.
107pub const CONTROL_CHANNEL: ControlChannelConfig = ControlChannelConfig {
108    label: "org.nixort.cfr.fcp.control/1",
109    protocol: PROTOCOL_ID,
110    ordered: true,
111    reliable: true,
112    binary: true,
113};
114
115/// Per-peer state machine for one FCP connection attempt.
116#[derive(Debug)]
117pub struct Connection {
118    federation: FederationId,
119    attempt: AttemptId,
120    local: EndpointIdentity,
121    remote: EndpointIdentity,
122    phase: Phase,
123    negotiation: Option<NegotiationIds>,
124    seen: VecDeque<EnvelopeId>,
125}
126
127impl Connection {
128    /// Creates an idle connection pinned to one federation, attempt and complete remote identity.
129    pub fn new(
130        federation: FederationId,
131        attempt: AttemptId,
132        local: EndpointIdentity,
133        remote: EndpointIdentity,
134    ) -> Result<Self, Error> {
135        if local == remote {
136            return Err(Error::SameEndpoint);
137        }
138        Ok(Self {
139            federation,
140            attempt,
141            local,
142            remote,
143            phase: Phase::Idle,
144            negotiation: None,
145            seen: VecDeque::new(),
146        })
147    }
148
149    /// Returns the federation namespace pinned for this attempt.
150    pub const fn federation(&self) -> FederationId {
151        self.federation
152    }
153
154    /// Returns the application-provided attempt identifier pinned for this connection.
155    pub const fn attempt(&self) -> AttemptId {
156        self.attempt
157    }
158
159    /// Returns the complete local FCP endpoint identity pinned for this connection.
160    pub const fn local_endpoint(&self) -> EndpointIdentity {
161        self.local
162    }
163
164    /// Returns the complete remote FCP endpoint identity pinned for this connection.
165    pub const fn remote_endpoint(&self) -> EndpointIdentity {
166        self.remote
167    }
168
169    /// Returns the current lifecycle state.
170    pub const fn phase(&self) -> Phase {
171        self.phase
172    }
173
174    /// Starts a local offer from `Idle`.
175    pub fn begin_offer<S: EndpointSigner>(
176        &mut self,
177        signer: &S,
178        binding: WebRtcBinding,
179        description: Vec<u8>,
180    ) -> Result<Vec<Action>, Error> {
181        self.require_local_signer(signer)?;
182        if self.phase != Phase::Idle {
183            return Err(Error::InvalidState);
184        }
185        let offer = Envelope::sign(
186            signer,
187            self.federation,
188            self.attempt,
189            self.remote,
190            Body::Offer {
191                binding,
192                description,
193            },
194        )?;
195        self.negotiation = Some(NegotiationIds {
196            offer: offer.id()?,
197            answer: None,
198        });
199        self.phase = Phase::OfferSent;
200        Ok(vec![
201            Action::OpenControlChannel,
202            Action::Send(Box::new(offer)),
203        ])
204    }
205
206    /// Answers one accepted remote offer.
207    pub fn answer<S: EndpointSigner>(
208        &mut self,
209        signer: &S,
210        binding: WebRtcBinding,
211        description: Vec<u8>,
212    ) -> Result<Action, Error> {
213        self.require_local_signer(signer)?;
214        if self.phase != Phase::OfferReceived {
215            return Err(Error::InvalidState);
216        }
217        let offer_id = self.negotiation.ok_or(Error::InvalidState)?.offer;
218        let answer = Envelope::sign(
219            signer,
220            self.federation,
221            self.attempt,
222            self.remote,
223            Body::Answer {
224                offer_id,
225                binding,
226                description,
227            },
228        )?;
229        self.negotiation = Some(NegotiationIds {
230            offer: offer_id,
231            answer: Some(answer.id()?),
232        });
233        self.phase = Phase::AnswerSent;
234        Ok(Action::Send(Box::new(answer)))
235    }
236
237    /// Creates a candidate bound to the active negotiation.
238    pub fn candidate<S: EndpointSigner>(
239        &self,
240        signer: &S,
241        sequence: u32,
242        candidate: Vec<u8>,
243    ) -> Result<Action, Error> {
244        self.require_local_signer(signer)?;
245        if !matches!(
246            self.phase,
247            Phase::OfferSent
248                | Phase::OfferReceived
249                | Phase::AnswerSent
250                | Phase::AnswerReceived
251                | Phase::Established
252        ) {
253            return Err(Error::InvalidState);
254        }
255        let ids = self.negotiation.ok_or(Error::InvalidState)?;
256        let parent_id = ids.answer.unwrap_or(ids.offer);
257        let envelope = Envelope::sign(
258            signer,
259            self.federation,
260            self.attempt,
261            self.remote,
262            Body::Candidate {
263                parent_id,
264                sequence,
265                candidate,
266            },
267        )?;
268        Ok(Action::Send(Box::new(envelope)))
269    }
270
271    /// Emits one bounded raw CFR control envelope after WebRTC connection is established.
272    pub fn cfr_control<S: EndpointSigner>(
273        &self,
274        signer: &S,
275        payload: Vec<u8>,
276    ) -> Result<Action, Error> {
277        self.require_local_signer(signer)?;
278        if self.phase != Phase::Established {
279            return Err(Error::InvalidState);
280        }
281        Ok(Action::Send(Box::new(Envelope::sign(
282            signer,
283            self.federation,
284            self.attempt,
285            self.remote,
286            Body::CfrControl { payload },
287        )?)))
288    }
289
290    /// Emits a local close and closes the core attempt.
291    pub fn close<S: EndpointSigner>(
292        &mut self,
293        signer: &S,
294        reason: CloseCode,
295    ) -> Result<Action, Error> {
296        self.require_local_signer(signer)?;
297        if matches!(self.phase, Phase::Idle | Phase::Closed) {
298            return Err(Error::InvalidState);
299        }
300        self.phase = Phase::Closed;
301        Ok(Action::Send(Box::new(Envelope::sign(
302            signer,
303            self.federation,
304            self.attempt,
305            self.remote,
306            Body::Close { reason },
307        )?)))
308    }
309
310    /// Processes a received envelope only after exact binding and signature validation.
311    pub fn receive(&mut self, envelope: Envelope) -> Result<Vec<Action>, Error> {
312        self.validate_inbound(&envelope)?;
313        let id = envelope.id()?;
314        let remote_endpoint = envelope.sender;
315        if self.seen.iter().any(|known| known == &id) {
316            return Ok(Vec::new());
317        }
318        let actions = match envelope.body {
319            Body::Offer {
320                binding,
321                description,
322            } => {
323                if self.phase == Phase::OfferSent {
324                    return Err(Error::Glare);
325                }
326                if self.phase != Phase::Idle {
327                    return Err(Error::InvalidState);
328                }
329                self.negotiation = Some(NegotiationIds {
330                    offer: id,
331                    answer: None,
332                });
333                self.phase = Phase::OfferReceived;
334                vec![Action::ApplyOffer {
335                    binding,
336                    description,
337                }]
338            }
339            Body::Answer {
340                offer_id,
341                binding,
342                description,
343            } => {
344                if self.phase != Phase::OfferSent
345                    || self.negotiation.ok_or(Error::InvalidState)?.offer != offer_id
346                {
347                    return Err(Error::InvalidState);
348                }
349                let offer = self.negotiation.ok_or(Error::InvalidState)?.offer;
350                self.negotiation = Some(NegotiationIds {
351                    offer,
352                    answer: Some(id),
353                });
354                self.phase = Phase::AnswerReceived;
355                vec![Action::ApplyAnswer {
356                    binding,
357                    description,
358                }]
359            }
360            Body::Candidate {
361                parent_id,
362                sequence,
363                candidate,
364            } => {
365                self.validate_candidate_parent(parent_id)?;
366                vec![Action::AddCandidate {
367                    sequence,
368                    candidate,
369                }]
370            }
371            Body::Close { reason } => {
372                if matches!(self.phase, Phase::Idle | Phase::Closed) {
373                    return Err(Error::InvalidState);
374                }
375                self.phase = Phase::Closed;
376                vec![Action::CloseTransport { reason }]
377            }
378            Body::CfrControl { payload } => {
379                if self.phase != Phase::Established {
380                    return Err(Error::InvalidState);
381                }
382                vec![Action::DeliverCfr {
383                    envelope_id: id,
384                    remote_endpoint: Box::new(remote_endpoint),
385                    payload,
386                }]
387            }
388        };
389        self.note_seen(id);
390        Ok(actions)
391    }
392
393    /// Records that the platform adapter reached a connected WebRTC control channel.
394    pub fn transport_connected(&mut self) -> Result<Vec<Action>, Error> {
395        if !matches!(self.phase, Phase::AnswerSent | Phase::AnswerReceived) {
396            return Err(Error::InvalidState);
397        }
398        self.phase = Phase::Established;
399        Ok(Vec::new())
400    }
401
402    /// Records a local platform failure and stops this attempt without manufacturing a remote close.
403    pub fn transport_failed(&mut self) -> Result<(), Error> {
404        if matches!(self.phase, Phase::Idle | Phase::Closed) {
405            return Err(Error::InvalidState);
406        }
407        self.phase = Phase::Closed;
408        Ok(())
409    }
410
411    fn require_local_signer<S: EndpointSigner>(&self, signer: &S) -> Result<(), Error> {
412        if signer.endpoint() != self.local {
413            return Err(Error::WrongLocalSigner);
414        }
415        Ok(())
416    }
417
418    fn validate_inbound(&self, envelope: &Envelope) -> Result<(), Error> {
419        if envelope.federation != self.federation {
420            return Err(Error::WrongFederation);
421        }
422        if envelope.attempt != self.attempt {
423            return Err(Error::WrongAttempt);
424        }
425        if envelope.sender != self.remote {
426            return Err(Error::WrongSender);
427        }
428        if envelope.recipient != self.local {
429            return Err(Error::WrongRecipient);
430        }
431        envelope.verify()
432    }
433
434    fn validate_candidate_parent(&self, parent: EnvelopeId) -> Result<(), Error> {
435        let ids = self.negotiation.ok_or(Error::InvalidState)?;
436        if !matches!(
437            self.phase,
438            Phase::OfferSent
439                | Phase::OfferReceived
440                | Phase::AnswerSent
441                | Phase::AnswerReceived
442                | Phase::Established
443        ) {
444            return Err(Error::InvalidState);
445        }
446        if parent != ids.offer && ids.answer != Some(parent) {
447            return Err(Error::WrongCandidateParent);
448        }
449        Ok(())
450    }
451
452    fn note_seen(&mut self, id: EnvelopeId) {
453        if self.seen.len() == MAX_SEEN_ENVELOPES {
454            let _ = self.seen.pop_front();
455        }
456        self.seen.push_back(id);
457    }
458}