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
10pub const WIFI_MTU: usize = 4045;
12pub const IEEE80211_HEADER_LEN: usize = 24;
14pub const CRYPTO_BOX_SECRETKEY_LEN: usize = 32;
16pub const CRYPTO_BOX_PUBLICKEY_LEN: usize = 32;
18pub const CRYPTO_BOX_NONCE_LEN: usize = 24;
20pub const CRYPTO_BOX_TAG_LEN: usize = 16;
22pub const WSESSION_HDR_LEN: usize = 1 + CRYPTO_BOX_NONCE_LEN;
24pub const WSESSION_DATA_LEN: usize = 8 + 4 + 1 + 1 + 1 + CHACHA20_POLY1305_KEY_LEN;
26pub const WBLOCK_HDR_LEN: usize = 9;
28pub const WPACKET_HDR_LEN: usize = 3;
30pub const CHACHA20_POLY1305_KEY_LEN: usize = 32;
32pub const CHACHA20_POLY1305_TAG_LEN: usize = 16;
34pub const MAX_FEC_PAYLOAD: usize =
36 WIFI_MTU - IEEE80211_HEADER_LEN - WBLOCK_HDR_LEN - CHACHA20_POLY1305_TAG_LEN;
37pub const MAX_PAYLOAD_SIZE: usize = MAX_FEC_PAYLOAD - WPACKET_HDR_LEN;
39pub const MAX_FORWARDER_PACKET_SIZE: usize = WIFI_MTU - IEEE80211_HEADER_LEN;
41pub const MAX_BLOCK_IDX: u64 = (1u64 << 55) - 1;
43
44pub const WFB_PACKET_DATA: u8 = 0x01;
46pub const WFB_PACKET_KEY: u8 = 0x02;
48pub const WFB_FEC_VDM_RS: u8 = 0x01;
50pub const WFB_PACKET_FEC_ONLY: u8 = 0x01;
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum WfbError {
56 Empty,
58 TooLong,
60 ShortDataPacket,
62 ShortSessionPacket,
64 InvalidKeypair,
66 SessionEncryptFailed,
68 SessionDecryptFailed,
70 DataEncryptFailed,
72 DataDecryptFailed,
74 SessionEpochTooOld {
76 session_epoch: u64,
78 minimum_epoch: u64,
80 },
81 SessionChannelMismatch {
83 expected: u32,
85 actual: u32,
87 },
88 UnsupportedFecType(u8),
90 UnknownPacketType(u8),
92 InvalidFecParameters,
94 InvalidFragmentIndex,
96 BlockIndexOverflow,
98 InvalidPlainPacket,
100 PayloadTooLarge,
102 MissingSession,
104 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum WfbPacket<'a> {
153 Data {
155 data_nonce: u64,
157 encrypted_payload: &'a [u8],
159 associated_data: &'a [u8],
161 },
162 SessionKey {
164 session_nonce: &'a [u8],
166 encrypted_session: &'a [u8],
168 },
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct WfbKeypair {
174 pub rx_secretkey: [u8; CRYPTO_BOX_SECRETKEY_LEN],
176 pub tx_publickey: [u8; CRYPTO_BOX_PUBLICKEY_LEN],
178}
179
180impl WfbKeypair {
181 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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
199pub struct FecCounters {
200 pub total_packets: u64,
202 pub recovered_packets: u64,
204 pub lost_packets: u64,
206 pub bad_packets: u64,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct WfbSession {
213 pub epoch: u64,
215 pub channel_id: ChannelId,
217 pub fec_type: u8,
219 pub fec_k: usize,
221 pub fec_n: usize,
223 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#[derive(Debug, Clone, PartialEq, Eq)]
278pub enum WfbEvent {
279 Session(WfbSession),
281 Payload(WfbOutput),
283}
284
285#[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 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 pub fn session(&self) -> Option<&WfbSession> {
317 self.session.as_ref()
318 }
319
320 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 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
437pub 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#[derive(Debug, Clone, PartialEq, Eq)]
474pub struct WfbOutput {
475 pub packet_seq: u64,
477 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#[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 pub total_packets: u64,
521 pub lost_packets: u64,
523 pub recovered_packets: u64,
525 pub bad_packets: u64,
527}
528
529impl PlainAssembler {
530 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 pub const fn fec_k(&self) -> usize {
552 self.fec_k
553 }
554
555 pub const fn fec_n(&self) -> usize {
557 self.fec_n
558 }
559
560 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 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 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
801pub 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 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}