Skip to main content

openipc_core/
wfb.rs

1use std::collections::BTreeMap;
2
3use crypto_box::aead::Aead;
4use crypto_box::{Nonce as BoxNonce, PublicKey, SalsaBox, SecretKey};
5
6use crate::channel::ChannelId;
7use crate::crypto::decrypt_chacha20poly1305_legacy_into;
8use crate::fec::FecCode;
9
10/// WFB WiFi MTU used by OpenIPC forwarder packets.
11pub const WIFI_MTU: usize = 4045;
12/// 802.11 header length subtracted from WFB packet capacity.
13pub const IEEE80211_HEADER_LEN: usize = 24;
14/// crypto_box secret key length.
15pub const CRYPTO_BOX_SECRETKEY_LEN: usize = 32;
16/// crypto_box public key length.
17pub const CRYPTO_BOX_PUBLICKEY_LEN: usize = 32;
18/// crypto_box nonce length.
19pub const CRYPTO_BOX_NONCE_LEN: usize = 24;
20/// crypto_box authentication tag length.
21pub const CRYPTO_BOX_TAG_LEN: usize = 16;
22/// WFB session packet header length.
23pub const WSESSION_HDR_LEN: usize = 1 + CRYPTO_BOX_NONCE_LEN;
24/// Plain WFB session body length before crypto_box encryption.
25pub const WSESSION_DATA_LEN: usize = 8 + 4 + 1 + 1 + 1 + CHACHA20_POLY1305_KEY_LEN;
26/// WFB data-block header length.
27pub const WBLOCK_HDR_LEN: usize = 9;
28/// Plain WFB payload-fragment header length.
29pub const WPACKET_HDR_LEN: usize = 3;
30/// WFB session ChaCha20-Poly1305 key length.
31pub const CHACHA20_POLY1305_KEY_LEN: usize = 32;
32/// WFB session ChaCha20-Poly1305 authentication tag length.
33pub const CHACHA20_POLY1305_TAG_LEN: usize = 16;
34/// Maximum encrypted FEC fragment payload carried by one WFB data packet.
35pub const MAX_FEC_PAYLOAD: usize =
36    WIFI_MTU - IEEE80211_HEADER_LEN - WBLOCK_HDR_LEN - CHACHA20_POLY1305_TAG_LEN;
37/// Maximum application payload before WFB fragment headers are added.
38pub const MAX_PAYLOAD_SIZE: usize = MAX_FEC_PAYLOAD - WPACKET_HDR_LEN;
39/// Maximum WFB forwarder packet payload after the 802.11 header.
40pub const MAX_FORWARDER_PACKET_SIZE: usize = WIFI_MTU - IEEE80211_HEADER_LEN;
41/// Largest WFB block index before a transmitter must rotate session keys.
42pub const MAX_BLOCK_IDX: u64 = (1u64 << 55) - 1;
43
44/// WFB packet type for encrypted data fragments.
45pub const WFB_PACKET_DATA: u8 = 0x01;
46/// WFB packet type for encrypted session-key packets.
47pub const WFB_PACKET_KEY: u8 = 0x02;
48/// FEC type used by WFB's Vandermonde Reed-Solomon blocks.
49pub const WFB_FEC_VDM_RS: u8 = 0x01;
50/// Flag marking a WFB packet as parity-only FEC data.
51pub const WFB_PACKET_FEC_ONLY: u8 = 0x01;
52
53/// Error returned while parsing, decrypting, or assembling WFB packets.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum WfbError {
56    /// Packet buffer is empty.
57    Empty,
58    /// Packet exceeds WFB forwarder size.
59    TooLong,
60    /// Data packet is too short.
61    ShortDataPacket,
62    /// Session packet is too short.
63    ShortSessionPacket,
64    /// WFB keypair is not the expected 64-byte file shape.
65    InvalidKeypair,
66    /// Session-key encryption failed.
67    SessionEncryptFailed,
68    /// Session-key decryption failed.
69    SessionDecryptFailed,
70    /// Data encryption failed.
71    DataEncryptFailed,
72    /// Data decryption failed.
73    DataDecryptFailed,
74    /// Session epoch was older than the configured minimum.
75    SessionEpochTooOld {
76        /// Epoch from the received session packet.
77        session_epoch: u64,
78        /// Minimum epoch accepted by the receiver.
79        minimum_epoch: u64,
80    },
81    /// Session packet was for a different WFB channel.
82    SessionChannelMismatch {
83        /// Expected channel id.
84        expected: u32,
85        /// Actual channel id in the session packet.
86        actual: u32,
87    },
88    /// FEC type is not the supported VDM Reed-Solomon mode.
89    UnsupportedFecType(u8),
90    /// Forwarder packet type is unknown.
91    UnknownPacketType(u8),
92    /// FEC parameters are invalid.
93    InvalidFecParameters,
94    /// Fragment index is outside the current FEC block.
95    InvalidFragmentIndex,
96    /// Data nonce encoded a block index beyond the supported range.
97    BlockIndexOverflow,
98    /// Decrypted plain packet is malformed.
99    InvalidPlainPacket,
100    /// Plain payload exceeds the WFB maximum.
101    PayloadTooLarge,
102    /// Encrypted data packet arrived before a session key.
103    MissingSession,
104    /// FEC recovery failed.
105    FecRecoveryFailed,
106}
107
108impl std::fmt::Display for WfbError {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self {
111            Self::Empty => write!(f, "empty WFB packet"),
112            Self::TooLong => write!(f, "WFB packet exceeds maximum forwarder size"),
113            Self::ShortDataPacket => write!(f, "short WFB data packet"),
114            Self::ShortSessionPacket => write!(f, "invalid WFB session packet size"),
115            Self::InvalidKeypair => write!(f, "WFB keypair must be 64 bytes"),
116            Self::SessionEncryptFailed => write!(f, "unable to encrypt WFB session key"),
117            Self::SessionDecryptFailed => write!(f, "unable to decrypt WFB session key"),
118            Self::DataEncryptFailed => write!(f, "unable to encrypt WFB data packet"),
119            Self::DataDecryptFailed => write!(f, "unable to decrypt WFB data packet"),
120            Self::SessionEpochTooOld {
121                session_epoch,
122                minimum_epoch,
123            } => write!(
124                f,
125                "WFB session epoch {session_epoch} is older than minimum {minimum_epoch}"
126            ),
127            Self::SessionChannelMismatch { expected, actual } => write!(
128                f,
129                "WFB session channel mismatch: expected 0x{expected:08x}, got 0x{actual:08x}"
130            ),
131            Self::UnsupportedFecType(fec_type) => {
132                write!(f, "unsupported WFB FEC type {fec_type}")
133            }
134            Self::UnknownPacketType(packet_type) => {
135                write!(f, "unknown WFB packet type 0x{packet_type:02x}")
136            }
137            Self::InvalidFecParameters => write!(f, "invalid WFB FEC parameters"),
138            Self::InvalidFragmentIndex => write!(f, "invalid WFB fragment index"),
139            Self::BlockIndexOverflow => write!(f, "WFB block index overflow"),
140            Self::InvalidPlainPacket => write!(f, "invalid decrypted WFB packet"),
141            Self::PayloadTooLarge => write!(f, "decrypted WFB payload is too large"),
142            Self::MissingSession => write!(f, "WFB data packet arrived before session key"),
143            Self::FecRecoveryFailed => write!(f, "WFB FEC recovery failed"),
144        }
145    }
146}
147
148impl std::error::Error for WfbError {}
149
150/// Borrowed WFB forwarder packet.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum WfbPacket<'a> {
153    /// Encrypted WFB data/FEC fragment packet.
154    Data {
155        /// Data nonce; high bits are block index and low byte is fragment index.
156        data_nonce: u64,
157        /// Encrypted fragment payload plus authentication tag.
158        encrypted_payload: &'a [u8],
159        /// Associated data used for WFB data authentication.
160        associated_data: &'a [u8],
161    },
162    /// Encrypted WFB session-key packet.
163    SessionKey {
164        /// crypto_box session nonce.
165        session_nonce: &'a [u8],
166        /// Encrypted session data.
167        encrypted_session: &'a [u8],
168    },
169}
170
171/// Ground-station WFB keypair file contents.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct WfbKeypair {
174    /// Ground-station receive secret key.
175    pub rx_secretkey: [u8; CRYPTO_BOX_SECRETKEY_LEN],
176    /// Air-unit transmit public key.
177    pub tx_publickey: [u8; CRYPTO_BOX_PUBLICKEY_LEN],
178}
179
180impl WfbKeypair {
181    /// Parse the 64-byte `gs.key` style keypair.
182    pub fn from_bytes(bytes: &[u8]) -> Result<Self, WfbError> {
183        if bytes.len() != CRYPTO_BOX_SECRETKEY_LEN + CRYPTO_BOX_PUBLICKEY_LEN {
184            return Err(WfbError::InvalidKeypair);
185        }
186        let mut rx_secretkey = [0; CRYPTO_BOX_SECRETKEY_LEN];
187        let mut tx_publickey = [0; CRYPTO_BOX_PUBLICKEY_LEN];
188        rx_secretkey.copy_from_slice(&bytes[..CRYPTO_BOX_SECRETKEY_LEN]);
189        tx_publickey.copy_from_slice(&bytes[CRYPTO_BOX_SECRETKEY_LEN..]);
190        Ok(Self {
191            rx_secretkey,
192            tx_publickey,
193        })
194    }
195}
196
197/// Cumulative FEC counters for a WFB receiver or assembler.
198#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
199pub struct FecCounters {
200    /// Total data fragments observed.
201    pub total_packets: u64,
202    /// Primary fragments recovered by FEC.
203    pub recovered_packets: u64,
204    /// Primary fragments considered lost.
205    pub lost_packets: u64,
206    /// Malformed or unrecoverable fragments.
207    pub bad_packets: u64,
208}
209
210/// Decrypted WFB session parameters.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct WfbSession {
213    /// Session epoch.
214    pub epoch: u64,
215    /// Channel id this session applies to.
216    pub channel_id: ChannelId,
217    /// WFB FEC type.
218    pub fec_type: u8,
219    /// Primary fragment count.
220    pub fec_k: usize,
221    /// Total primary plus parity fragment count.
222    pub fec_n: usize,
223    /// Symmetric key used for WFB data packets.
224    pub session_key: [u8; CHACHA20_POLY1305_KEY_LEN],
225}
226
227impl WfbSession {
228    fn parse(
229        plaintext: &[u8],
230        expected_channel_id: ChannelId,
231        minimum_epoch: u64,
232    ) -> Result<Self, WfbError> {
233        if plaintext.len() < WSESSION_DATA_LEN {
234            return Err(WfbError::SessionDecryptFailed);
235        }
236        let epoch = u64::from_be_bytes(plaintext[0..8].try_into().expect("checked length"));
237        if epoch < minimum_epoch {
238            return Err(WfbError::SessionEpochTooOld {
239                session_epoch: epoch,
240                minimum_epoch,
241            });
242        }
243
244        let raw_channel = u32::from_be_bytes(plaintext[8..12].try_into().expect("checked length"));
245        let channel_id = ChannelId::new(raw_channel);
246        if channel_id != expected_channel_id {
247            return Err(WfbError::SessionChannelMismatch {
248                expected: expected_channel_id.raw(),
249                actual: raw_channel,
250            });
251        }
252
253        let fec_type = plaintext[12];
254        if fec_type != WFB_FEC_VDM_RS {
255            return Err(WfbError::UnsupportedFecType(fec_type));
256        }
257        let fec_k = plaintext[13] as usize;
258        let fec_n = plaintext[14] as usize;
259        if fec_k == 0 || fec_n == 0 || fec_k > fec_n || fec_n >= 256 {
260            return Err(WfbError::InvalidFecParameters);
261        }
262
263        let mut session_key = [0; CHACHA20_POLY1305_KEY_LEN];
264        session_key.copy_from_slice(&plaintext[15..47]);
265        Ok(Self {
266            epoch,
267            channel_id,
268            fec_type,
269            fec_k,
270            fec_n,
271            session_key,
272        })
273    }
274}
275
276/// Event emitted by an encrypted WFB receiver.
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub enum WfbEvent {
279    /// A session key was accepted.
280    Session(WfbSession),
281    /// One recovered payload was emitted.
282    Payload(WfbOutput),
283}
284
285/// Encrypted WFB receiver for one channel.
286#[derive(Debug, Clone)]
287pub struct WfbReceiver {
288    channel_id: ChannelId,
289    minimum_epoch: u64,
290    keypair: WfbKeypair,
291    session: Option<WfbSession>,
292    assembler: Option<PlainAssembler>,
293    decrypt_scratch: Vec<u8>,
294    incoming_packets: u64,
295    session_packets: u64,
296    data_packets: u64,
297}
298
299impl WfbReceiver {
300    /// Create a receiver for one channel and keypair.
301    pub fn new(channel_id: ChannelId, keypair: WfbKeypair, minimum_epoch: u64) -> Self {
302        Self {
303            channel_id,
304            minimum_epoch,
305            keypair,
306            session: None,
307            assembler: None,
308            decrypt_scratch: Vec::with_capacity(MAX_FEC_PAYLOAD),
309            incoming_packets: 0,
310            session_packets: 0,
311            data_packets: 0,
312        }
313    }
314
315    /// Return the currently accepted WFB session, if any.
316    pub fn session(&self) -> Option<&WfbSession> {
317        self.session.as_ref()
318    }
319
320    /// Return cumulative receive/FEC counters.
321    pub fn counters(&self) -> FecCounters {
322        let assembler = self
323            .assembler
324            .as_ref()
325            .map(PlainAssembler::counters)
326            .unwrap_or_default();
327        FecCounters {
328            total_packets: self.incoming_packets,
329            recovered_packets: assembler.recovered_packets,
330            lost_packets: assembler.lost_packets,
331            bad_packets: assembler.bad_packets,
332        }
333    }
334
335    /// Push one WFB forwarder packet payload.
336    pub fn push_forwarder_packet(&mut self, buf: &[u8]) -> Result<Vec<WfbEvent>, WfbError> {
337        let mut events = Vec::new();
338        self.push_forwarder_packet_with(buf, &mut |event| events.push(event))?;
339        Ok(events)
340    }
341
342    pub(crate) fn push_forwarder_packet_with(
343        &mut self,
344        buf: &[u8],
345        emit: &mut impl FnMut(WfbEvent),
346    ) -> Result<(), WfbError> {
347        log::trace!(target: "openipc_core::wfb", "received WFB forwarder packet bytes={}", buf.len());
348        match parse_forwarder_packet(buf)? {
349            WfbPacket::SessionKey {
350                session_nonce,
351                encrypted_session,
352            } => {
353                self.incoming_packets += 1;
354                self.session_packets += 1;
355                let session = self.decrypt_session(session_nonce, encrypted_session)?;
356                let changed = self
357                    .session
358                    .as_ref()
359                    .map(|current| current.session_key != session.session_key)
360                    .unwrap_or(true);
361                if changed {
362                    log::info!(
363                        target: "openipc_core::wfb",
364                        "accepted WFB session epoch={} channel=0x{:08x} fec={}/{}",
365                        session.epoch,
366                        session.channel_id.raw(),
367                        session.fec_k,
368                        session.fec_n
369                    );
370                    self.assembler = Some(PlainAssembler::new(session.fec_k, session.fec_n)?);
371                    self.session = Some(session.clone());
372                    emit(WfbEvent::Session(session));
373                }
374                Ok(())
375            }
376            WfbPacket::Data {
377                data_nonce,
378                encrypted_payload,
379                associated_data,
380            } => {
381                self.incoming_packets += 1;
382                self.data_packets += 1;
383                let session = self.session.as_ref().ok_or(WfbError::MissingSession)?;
384                let nonce = &associated_data[1..WBLOCK_HDR_LEN];
385                decrypt_chacha20poly1305_legacy_into(
386                    &session.session_key,
387                    nonce,
388                    associated_data,
389                    encrypted_payload,
390                    &mut self.decrypt_scratch,
391                )
392                .map_err(|_| WfbError::DataDecryptFailed)?;
393                let assembler = self.assembler.as_mut().ok_or(WfbError::MissingSession)?;
394                let mut payload_count = 0usize;
395                assembler.push_decrypted_fragment_with(
396                    data_nonce,
397                    &self.decrypt_scratch,
398                    &mut |payload| {
399                        payload_count += 1;
400                        emit(WfbEvent::Payload(payload));
401                    },
402                )?;
403                log::trace!(
404                    target: "openipc_core::wfb",
405                    "processed encrypted WFB data fragment nonce={} payloads={}",
406                    data_nonce,
407                    payload_count
408                );
409                Ok(())
410            }
411        }
412    }
413
414    fn decrypt_session(
415        &self,
416        session_nonce: &[u8],
417        encrypted_session: &[u8],
418    ) -> Result<WfbSession, WfbError> {
419        let nonce: [u8; CRYPTO_BOX_NONCE_LEN] = session_nonce
420            .try_into()
421            .map_err(|_| WfbError::ShortSessionPacket)?;
422        let rx_secret = SecretKey::from(self.keypair.rx_secretkey);
423        let tx_public = PublicKey::from(self.keypair.tx_publickey);
424        let cipher = SalsaBox::new(&tx_public, &rx_secret);
425        let plaintext = cipher
426            .decrypt(BoxNonce::from_slice(&nonce), encrypted_session)
427            .map_err(|_| WfbError::SessionDecryptFailed)?;
428        let minimum_epoch = self
429            .session
430            .as_ref()
431            .map(|session| session.epoch.max(self.minimum_epoch))
432            .unwrap_or(self.minimum_epoch);
433        WfbSession::parse(&plaintext, self.channel_id, minimum_epoch)
434    }
435}
436
437/// Parse a WFB forwarder packet as data or session-key payload.
438pub fn parse_forwarder_packet(buf: &[u8]) -> Result<WfbPacket<'_>, WfbError> {
439    if buf.is_empty() {
440        return Err(WfbError::Empty);
441    }
442    if buf.len() > MAX_FORWARDER_PACKET_SIZE {
443        return Err(WfbError::TooLong);
444    }
445
446    match buf[0] {
447        WFB_PACKET_DATA => {
448            if buf.len() < WBLOCK_HDR_LEN + WPACKET_HDR_LEN + CHACHA20_POLY1305_TAG_LEN {
449                return Err(WfbError::ShortDataPacket);
450            }
451            let mut nonce = [0; 8];
452            nonce.copy_from_slice(&buf[1..9]);
453            Ok(WfbPacket::Data {
454                data_nonce: u64::from_be_bytes(nonce),
455                encrypted_payload: &buf[WBLOCK_HDR_LEN..],
456                associated_data: &buf[..WBLOCK_HDR_LEN],
457            })
458        }
459        WFB_PACKET_KEY => {
460            if buf.len() < WSESSION_HDR_LEN + WSESSION_DATA_LEN + CRYPTO_BOX_TAG_LEN {
461                return Err(WfbError::ShortSessionPacket);
462            }
463            Ok(WfbPacket::SessionKey {
464                session_nonce: &buf[1..WSESSION_HDR_LEN],
465                encrypted_session: &buf[WSESSION_HDR_LEN..],
466            })
467        }
468        other => Err(WfbError::UnknownPacketType(other)),
469    }
470}
471
472/// Recovered WFB application payload.
473#[derive(Debug, Clone, PartialEq, Eq)]
474pub struct WfbOutput {
475    /// Recovered packet sequence number.
476    pub packet_seq: u64,
477    /// Raw application payload bytes.
478    pub payload: Vec<u8>,
479}
480
481#[derive(Debug, Clone)]
482struct Block {
483    fragments: Vec<u8>,
484    present: Vec<bool>,
485    received: usize,
486    next_fragment: usize,
487}
488
489impl Block {
490    fn new(n: usize) -> Self {
491        Self {
492            fragments: vec![0; n * MAX_FEC_PAYLOAD],
493            present: vec![false; n],
494            received: 0,
495            next_fragment: 0,
496        }
497    }
498
499    fn reset(&mut self) {
500        self.present.fill(false);
501        self.received = 0;
502        self.next_fragment = 0;
503    }
504}
505
506/// Plain WFB FEC assembler.
507///
508/// This is used after data decryption, or directly for tests/pre-decrypted
509/// captures. It accepts primary and parity fragments and emits recovered
510/// application payloads in order.
511#[derive(Debug, Clone)]
512pub struct PlainAssembler {
513    fec_k: usize,
514    fec_n: usize,
515    fec: FecCode,
516    blocks: BTreeMap<u64, Block>,
517    spare_blocks: Vec<Block>,
518    next_block: Option<u64>,
519    /// Total fragments observed.
520    pub total_packets: u64,
521    /// Primary fragments considered lost.
522    pub lost_packets: u64,
523    /// Primary fragments recovered by FEC.
524    pub recovered_packets: u64,
525    /// Malformed or unrecoverable fragments.
526    pub bad_packets: u64,
527}
528
529impl PlainAssembler {
530    /// Create a plain assembler for `fec_k` primary and `fec_n` total fragments.
531    pub fn new(fec_k: usize, fec_n: usize) -> Result<Self, WfbError> {
532        if fec_k == 0 || fec_n == 0 || fec_k > fec_n || fec_n > 255 {
533            return Err(WfbError::InvalidFecParameters);
534        }
535        let fec = FecCode::new(fec_k, fec_n).map_err(|_| WfbError::InvalidFecParameters)?;
536        Ok(Self {
537            fec_k,
538            fec_n,
539            fec,
540            blocks: BTreeMap::new(),
541            spare_blocks: Vec::with_capacity(2),
542            next_block: None,
543            total_packets: 0,
544            lost_packets: 0,
545            recovered_packets: 0,
546            bad_packets: 0,
547        })
548    }
549
550    /// Return the primary fragment count.
551    pub const fn fec_k(&self) -> usize {
552        self.fec_k
553    }
554
555    /// Return the total primary plus parity fragment count.
556    pub const fn fec_n(&self) -> usize {
557        self.fec_n
558    }
559
560    /// Reset assembler state and FEC parameters.
561    pub fn reset_fec(&mut self, fec_k: usize, fec_n: usize) -> Result<(), WfbError> {
562        *self = Self::new(fec_k, fec_n)?;
563        Ok(())
564    }
565
566    /// Return cumulative FEC counters.
567    pub fn counters(&self) -> FecCounters {
568        FecCounters {
569            total_packets: self.total_packets,
570            recovered_packets: self.recovered_packets,
571            lost_packets: self.lost_packets,
572            bad_packets: self.bad_packets,
573        }
574    }
575
576    /// Push a decrypted WFB FEC fragment.
577    pub fn push_decrypted_fragment(
578        &mut self,
579        data_nonce: u64,
580        fragment: &[u8],
581    ) -> Result<Vec<WfbOutput>, WfbError> {
582        let mut outputs = Vec::new();
583        self.push_decrypted_fragment_with(data_nonce, fragment, &mut |output| {
584            outputs.push(output);
585        })?;
586        Ok(outputs)
587    }
588
589    pub(crate) fn push_decrypted_fragment_with(
590        &mut self,
591        data_nonce: u64,
592        fragment: &[u8],
593        emit: &mut impl FnMut(WfbOutput),
594    ) -> Result<(), WfbError> {
595        let block_idx = data_nonce >> 8;
596        let fragment_idx = (data_nonce & 0xff) as usize;
597
598        if block_idx > MAX_BLOCK_IDX {
599            return Err(WfbError::BlockIndexOverflow);
600        }
601        if fragment_idx >= self.fec_n {
602            return Err(WfbError::InvalidFragmentIndex);
603        }
604        self.total_packets += 1;
605
606        if self.next_block.is_none() {
607            self.next_block = Some(block_idx);
608        }
609        if self
610            .next_block
611            .map(|next_block| block_idx < next_block)
612            .unwrap_or(false)
613        {
614            return Ok(());
615        }
616
617        if !self.blocks.contains_key(&block_idx) {
618            let mut block = self
619                .spare_blocks
620                .pop()
621                .unwrap_or_else(|| Block::new(self.fec_n));
622            block.reset();
623            self.blocks.insert(block_idx, block);
624        }
625        let block = self
626            .blocks
627            .get_mut(&block_idx)
628            .expect("block was inserted above");
629        if !block.present[fragment_idx] {
630            let len = fragment.len().min(MAX_FEC_PAYLOAD);
631            let start = fragment_idx * MAX_FEC_PAYLOAD;
632            let slot = &mut block.fragments[start..start + MAX_FEC_PAYLOAD];
633            slot.fill(0);
634            slot[..len].copy_from_slice(&fragment[..len]);
635            block.present[fragment_idx] = true;
636            block.received += 1;
637        }
638
639        self.drain_ready_blocks(emit);
640        Ok(())
641    }
642
643    fn drain_ready_blocks(&mut self, emit: &mut impl FnMut(WfbOutput)) {
644        while let Some(block_idx) = self.next_block {
645            if !self.blocks.contains_key(&block_idx) {
646                if self.should_skip_missing_block(block_idx) {
647                    self.lost_packets += self.fec_k as u64;
648                    self.next_block = Some(block_idx + 1);
649                    continue;
650                }
651                break;
652            }
653
654            self.emit_contiguous_primary(block_idx, emit);
655            let complete = self
656                .blocks
657                .get(&block_idx)
658                .map(|block| block.next_fragment == self.fec_k)
659                .unwrap_or(false);
660            if complete {
661                if let Some(block) = self.blocks.remove(&block_idx) {
662                    self.recycle_block(block);
663                }
664                self.next_block = Some(block_idx + 1);
665                continue;
666            }
667
668            let can_recover = self
669                .blocks
670                .get(&block_idx)
671                .map(|block| block.received >= self.fec_k)
672                .unwrap_or(false);
673            if can_recover {
674                if let Some(block) = self.blocks.get_mut(&block_idx) {
675                    match self.fec.recover_primary_into(
676                        &mut block.fragments,
677                        &mut block.present,
678                        MAX_FEC_PAYLOAD,
679                    ) {
680                        Ok(recovered) => {
681                            if recovered > 0 {
682                                log::debug!(
683                                    target: "openipc_core::fec",
684                                    "recovered missing primary WFB fragments block={} recovered={}",
685                                    block_idx,
686                                    recovered
687                                );
688                            }
689                            self.recovered_packets += recovered as u64;
690                        }
691                        Err(error) => {
692                            log::warn!(
693                                target: "openipc_core::fec",
694                                "FEC recovery failed block={block_idx}: {error}"
695                            );
696                            self.bad_packets += 1;
697                            self.force_flush_block(block_idx, emit);
698                            continue;
699                        }
700                    }
701                }
702                self.emit_contiguous_primary(block_idx, emit);
703                if let Some(block) = self.blocks.remove(&block_idx) {
704                    self.recycle_block(block);
705                }
706                self.next_block = Some(block_idx + 1);
707                continue;
708            }
709
710            if self.should_force_flush(block_idx) {
711                self.force_flush_block(block_idx, emit);
712                continue;
713            }
714
715            break;
716        }
717    }
718
719    fn should_skip_missing_block(&self, block_idx: u64) -> bool {
720        let Some((&next_present_block, block)) = self.blocks.range((block_idx + 1)..).next() else {
721            return false;
722        };
723
724        block.received >= self.fec_k
725            || self.blocks.len() > 40
726            || next_present_block.saturating_sub(block_idx) >= 40
727    }
728
729    fn emit_contiguous_primary(&mut self, block_idx: u64, emit: &mut impl FnMut(WfbOutput)) {
730        let Some(block) = self.blocks.get_mut(&block_idx) else {
731            return;
732        };
733        while block.next_fragment < self.fec_k {
734            let fragment_idx = block.next_fragment;
735            if !block.present[fragment_idx] {
736                break;
737            }
738            let start = fragment_idx * MAX_FEC_PAYLOAD;
739            let fragment = &block.fragments[start..start + MAX_FEC_PAYLOAD];
740            let packet_seq = block_idx * self.fec_k as u64 + fragment_idx as u64;
741            match parse_plain_packet(fragment) {
742                Ok(Some(payload)) => emit(WfbOutput {
743                    packet_seq,
744                    payload: payload.to_vec(),
745                }),
746                Ok(None) => {}
747                Err(_) => {
748                    self.bad_packets += 1;
749                }
750            }
751            block.next_fragment += 1;
752        }
753    }
754
755    fn should_force_flush(&self, block_idx: u64) -> bool {
756        if self.blocks.len() > 40 {
757            return true;
758        }
759        self.blocks
760            .range((block_idx + 1)..)
761            .any(|(_, block)| block.received >= self.fec_k)
762    }
763
764    fn force_flush_block(&mut self, block_idx: u64, emit: &mut impl FnMut(WfbOutput)) {
765        if let Some(block) = self.blocks.remove(&block_idx) {
766            for fragment_idx in block.next_fragment..self.fec_k {
767                let packet_seq = block_idx * self.fec_k as u64 + fragment_idx as u64;
768                match block.present[fragment_idx] {
769                    true => {
770                        let start = fragment_idx * MAX_FEC_PAYLOAD;
771                        match parse_plain_packet(&block.fragments[start..start + MAX_FEC_PAYLOAD]) {
772                            Ok(Some(payload)) => emit(WfbOutput {
773                                packet_seq,
774                                payload: payload.to_vec(),
775                            }),
776                            Ok(None) => {}
777                            Err(_) => {
778                                self.bad_packets += 1;
779                            }
780                        }
781                    }
782                    false => {
783                        self.lost_packets += 1;
784                    }
785                }
786            }
787            self.next_block = Some(block_idx + 1);
788            self.recycle_block(block);
789        }
790    }
791
792    fn recycle_block(&mut self, mut block: Block) {
793        const MAX_SPARE_BLOCKS: usize = 4;
794        if self.spare_blocks.len() < MAX_SPARE_BLOCKS {
795            block.reset();
796            self.spare_blocks.push(block);
797        }
798    }
799}
800
801/// Parse a decrypted WFB plain packet and return payload bytes when present.
802pub fn parse_plain_packet(fragment: &[u8]) -> Result<Option<&[u8]>, WfbError> {
803    if fragment.len() < WPACKET_HDR_LEN {
804        return Err(WfbError::InvalidPlainPacket);
805    }
806    let flags = fragment[0];
807    let packet_size = u16::from_be_bytes([fragment[1], fragment[2]]) as usize;
808    if packet_size > MAX_PAYLOAD_SIZE || WPACKET_HDR_LEN + packet_size > fragment.len() {
809        return Err(WfbError::PayloadTooLarge);
810    }
811    if flags & WFB_PACKET_FEC_ONLY != 0 {
812        return Ok(None);
813    }
814    Ok(Some(
815        &fragment[WPACKET_HDR_LEN..WPACKET_HDR_LEN + packet_size],
816    ))
817}
818
819#[cfg(test)]
820mod tests {
821    use super::*;
822    use crate::crypto::encrypt_chacha20poly1305_legacy;
823    use crypto_box::aead::Aead;
824
825    fn plain(payload: &[u8]) -> Vec<u8> {
826        let mut out = Vec::new();
827        out.push(0);
828        out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
829        out.extend_from_slice(payload);
830        out
831    }
832
833    fn padded(fragment: &[u8]) -> Vec<u8> {
834        let mut out = vec![0; MAX_FEC_PAYLOAD];
835        out[..fragment.len()].copy_from_slice(fragment);
836        out
837    }
838
839    #[test]
840    fn parses_forwarder_data_packet() {
841        let mut packet = vec![WFB_PACKET_DATA];
842        packet.extend_from_slice(&0x0102_0304_0506_0708u64.to_be_bytes());
843        let encrypted = [9; WPACKET_HDR_LEN + CHACHA20_POLY1305_TAG_LEN];
844        packet.extend_from_slice(&encrypted);
845
846        let parsed = parse_forwarder_packet(&packet).unwrap();
847        match parsed {
848            WfbPacket::Data {
849                data_nonce,
850                encrypted_payload,
851                associated_data,
852            } => {
853                assert_eq!(data_nonce, 0x0102_0304_0506_0708);
854                assert_eq!(encrypted_payload, encrypted);
855                assert_eq!(associated_data.len(), WBLOCK_HDR_LEN);
856            }
857            WfbPacket::SessionKey { .. } => panic!("expected data"),
858        }
859    }
860
861    #[test]
862    fn rejects_data_packets_without_encrypted_plain_header_and_tag() {
863        let mut packet = vec![WFB_PACKET_DATA];
864        packet.extend_from_slice(&0x0102_0304_0506_0708u64.to_be_bytes());
865        packet.extend_from_slice(&[0; WPACKET_HDR_LEN + CHACHA20_POLY1305_TAG_LEN - 1]);
866
867        assert_eq!(
868            parse_forwarder_packet(&packet),
869            Err(WfbError::ShortDataPacket)
870        );
871    }
872
873    #[test]
874    fn emits_primary_fragments_in_order() {
875        let mut assembler = PlainAssembler::new(2, 4).unwrap();
876        let first = assembler
877            .push_decrypted_fragment(0, &plain(b"first"))
878            .unwrap();
879        assert_eq!(first.len(), 1);
880        assert_eq!(first[0].payload, b"first");
881        let out = assembler
882            .push_decrypted_fragment(1, &plain(b"second"))
883            .unwrap();
884        assert_eq!(out.len(), 1);
885        assert_eq!(out[0].payload, b"second");
886    }
887
888    #[test]
889    fn reuses_completed_fec_block_storage() {
890        let mut assembler = PlainAssembler::new(1, 1).unwrap();
891        let first = padded(&plain(b"first"));
892        assert_eq!(
893            assembler.push_decrypted_fragment(0, &first).unwrap()[0].payload,
894            b"first"
895        );
896        assert_eq!(assembler.spare_blocks.len(), 1);
897
898        let allocation = assembler.spare_blocks[0].fragments.as_ptr();
899        let second = plain(b"second");
900        assert_eq!(
901            assembler.push_decrypted_fragment(1 << 8, &second).unwrap()[0].payload,
902            b"second"
903        );
904        assert_eq!(assembler.spare_blocks.len(), 1);
905        assert_eq!(assembler.spare_blocks[0].fragments.as_ptr(), allocation);
906    }
907
908    #[test]
909    fn recovers_missing_primary_fragment_from_fec() {
910        let fec = FecCode::new(3, 5).unwrap();
911        let primary = vec![
912            padded(&plain(b"first")),
913            padded(&plain(b"second")),
914            padded(&plain(b"third")),
915        ];
916        let parity = fec.encode(&primary, MAX_FEC_PAYLOAD).unwrap();
917
918        let mut assembler = PlainAssembler::new(3, 5).unwrap();
919        let first = assembler.push_decrypted_fragment(0, &primary[0]).unwrap();
920        assert_eq!(first[0].payload, b"first");
921        assert!(assembler
922            .push_decrypted_fragment(2, &primary[2])
923            .unwrap()
924            .is_empty());
925        let recovered = assembler.push_decrypted_fragment(3, &parity[0]).unwrap();
926        assert_eq!(recovered.len(), 2);
927        assert_eq!(recovered[0].payload, b"second");
928        assert_eq!(recovered[1].payload, b"third");
929        assert_eq!(assembler.recovered_packets, 1);
930    }
931
932    #[test]
933    fn skips_fully_missing_blocks_when_later_block_is_ready() {
934        let mut assembler = PlainAssembler::new(2, 2).unwrap();
935
936        let first = assembler
937            .push_decrypted_fragment(0, &plain(b"b0-f0"))
938            .unwrap();
939        assert_eq!(first[0].payload, b"b0-f0");
940
941        assert!(assembler
942            .push_decrypted_fragment(2 << 8, &plain(b"b2-f0"))
943            .unwrap()
944            .is_empty());
945        let out = assembler
946            .push_decrypted_fragment((2 << 8) | 1, &plain(b"b2-f1"))
947            .unwrap();
948
949        assert_eq!(out.len(), 2);
950        assert_eq!(out[0].payload, b"b2-f0");
951        assert_eq!(out[1].payload, b"b2-f1");
952        assert_eq!(assembler.lost_packets, 3);
953    }
954
955    #[test]
956    fn ignores_late_fragments_from_already_flushed_blocks() {
957        let mut assembler = PlainAssembler::new(2, 2).unwrap();
958
959        assembler
960            .push_decrypted_fragment(0, &plain(b"b0-f0"))
961            .unwrap();
962        assembler
963            .push_decrypted_fragment(2 << 8, &plain(b"b2-f0"))
964            .unwrap();
965        assembler
966            .push_decrypted_fragment((2 << 8) | 1, &plain(b"b2-f1"))
967            .unwrap();
968
969        let late = assembler
970            .push_decrypted_fragment(1 << 8, &plain(b"late-b1-f0"))
971            .unwrap();
972        assert!(late.is_empty());
973    }
974
975    #[test]
976    fn skips_fec_only_plain_packets() {
977        let mut fragment = vec![WFB_PACKET_FEC_ONLY];
978        fragment.extend_from_slice(&4u16.to_be_bytes());
979        fragment.extend_from_slice(b"skip");
980        assert!(parse_plain_packet(&fragment).unwrap().is_none());
981    }
982
983    #[test]
984    fn receiver_decrypts_session_and_data_packet() {
985        let rx_secret = SecretKey::from([1; CRYPTO_BOX_SECRETKEY_LEN]);
986        let tx_secret = SecretKey::from([2; CRYPTO_BOX_SECRETKEY_LEN]);
987        let keypair = WfbKeypair {
988            rx_secretkey: rx_secret.to_bytes(),
989            tx_publickey: *tx_secret.public_key().as_bytes(),
990        };
991        let channel_id = ChannelId::default_video();
992        let session_key = [7; CHACHA20_POLY1305_KEY_LEN];
993
994        let mut session_plain = Vec::new();
995        session_plain.extend_from_slice(&1u64.to_be_bytes());
996        session_plain.extend_from_slice(&channel_id.raw().to_be_bytes());
997        session_plain.push(WFB_FEC_VDM_RS);
998        session_plain.push(1);
999        session_plain.push(1);
1000        session_plain.extend_from_slice(&session_key);
1001        assert_eq!(session_plain.len(), WSESSION_DATA_LEN);
1002        // wfb-ng allows encrypted optional session TLVs after the fixed fields.
1003        session_plain.extend_from_slice(&[0x42, 0x00, 0x01, 0x99]);
1004
1005        let session_nonce = [3; CRYPTO_BOX_NONCE_LEN];
1006        let tx_box = SalsaBox::new(&rx_secret.public_key(), &tx_secret);
1007        let encrypted_session = tx_box
1008            .encrypt(
1009                BoxNonce::from_slice(&session_nonce),
1010                session_plain.as_slice(),
1011            )
1012            .unwrap();
1013        let mut session_packet = vec![WFB_PACKET_KEY];
1014        session_packet.extend_from_slice(&session_nonce);
1015        session_packet.extend_from_slice(&encrypted_session);
1016
1017        let mut receiver = WfbReceiver::new(channel_id, keypair, 0);
1018        let session_events = receiver.push_forwarder_packet(&session_packet).unwrap();
1019        assert!(matches!(session_events.as_slice(), [WfbEvent::Session(_)]));
1020
1021        let data_nonce = 0u64;
1022        let mut block_header = vec![WFB_PACKET_DATA];
1023        block_header.extend_from_slice(&data_nonce.to_be_bytes());
1024        let encrypted_data = encrypt_chacha20poly1305_legacy(
1025            &session_key,
1026            &block_header[1..WBLOCK_HDR_LEN],
1027            &block_header,
1028            &plain(b"rtp payload"),
1029        )
1030        .unwrap();
1031        let mut data_packet = block_header;
1032        data_packet.extend_from_slice(&encrypted_data);
1033
1034        let payload_events = receiver.push_forwarder_packet(&data_packet).unwrap();
1035        match payload_events.as_slice() {
1036            [WfbEvent::Payload(payload)] => assert_eq!(payload.payload, b"rtp payload"),
1037            other => panic!("unexpected events: {other:?}"),
1038        }
1039
1040        let mut older_session_plain = Vec::new();
1041        older_session_plain.extend_from_slice(&0u64.to_be_bytes());
1042        older_session_plain.extend_from_slice(&channel_id.raw().to_be_bytes());
1043        older_session_plain.push(WFB_FEC_VDM_RS);
1044        older_session_plain.push(1);
1045        older_session_plain.push(1);
1046        older_session_plain.extend_from_slice(&[8; CHACHA20_POLY1305_KEY_LEN]);
1047        let older_session_nonce = [4; CRYPTO_BOX_NONCE_LEN];
1048        let encrypted_older_session = tx_box
1049            .encrypt(
1050                BoxNonce::from_slice(&older_session_nonce),
1051                older_session_plain.as_slice(),
1052            )
1053            .unwrap();
1054        let mut older_session_packet = vec![WFB_PACKET_KEY];
1055        older_session_packet.extend_from_slice(&older_session_nonce);
1056        older_session_packet.extend_from_slice(&encrypted_older_session);
1057
1058        assert_eq!(
1059            receiver.push_forwarder_packet(&older_session_packet),
1060            Err(WfbError::SessionEpochTooOld {
1061                session_epoch: 0,
1062                minimum_epoch: 1,
1063            })
1064        );
1065    }
1066}