1use rand::distributions::weighted::WeightedIndex;
6use rand::{distributions::Distribution, Rng};
7use serde::{Deserialize, Serialize};
8
9use crate::error::{Error, Result};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct BootstrapDescriptor {
14 pub descriptor_id: String,
15 pub version: u16,
16 pub created_at: u64,
17 pub expires_at: u64,
18 pub base_mask_ids: Vec<String>,
19 #[serde(default)]
20 pub embedded_masks: Vec<MaskProfile>,
21 pub candidate_count: u8,
22 #[serde(with = "serde_bytes")]
23 pub kdf_salt: [u8; 32],
24 #[serde(with = "serde_bytes")]
25 #[serde(default = "default_signature")]
26 pub signature: [u8; 64],
27}
28
29impl BootstrapDescriptor {
30 pub fn is_valid_at(&self, unix_secs: u64) -> bool {
31 unix_secs >= self.created_at && unix_secs <= self.expires_at
32 }
33
34 pub fn signing_bytes(&self) -> Vec<u8> {
35 let mut unsigned = self.clone();
36 unsigned.signature = [0u8; 64];
37 rmp_serde::to_vec(&unsigned).expect("bootstrap descriptor serializable")
38 }
39
40 pub fn verify_signature(&self, public_key: &[u8; 32]) -> Result<bool> {
42 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
43 let vk = VerifyingKey::from_bytes(public_key)
44 .map_err(|e| Error::Crypto(format!("Invalid Ed25519 public key: {}", e)))?;
45 let message = self.signing_bytes();
46 let sig = Signature::from_bytes(&self.signature);
47 match vk.verify(&message, &sig) {
48 Ok(()) => Ok(true),
49 Err(_) => Ok(false),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum BootstrapChannel {
60 CDN {
62 url: String,
64 provider: String,
66 },
67 Telegram {
69 bot_username: String,
71 token: Option<String>,
73 },
74 GitHub {
76 repo: String,
78 asset_name: String,
80 },
81 IPFS {
83 hash: String,
85 gateway: Option<String>,
87 },
88 Email {
90 address: String,
92 subject_pattern: String,
94 },
95}
96
97impl BootstrapChannel {
98 pub fn name(&self) -> &str {
100 match self {
101 BootstrapChannel::CDN { provider, .. } => provider.as_str(),
102 BootstrapChannel::Telegram { bot_username, .. } => bot_username.as_str(),
103 BootstrapChannel::GitHub { repo, .. } => repo.as_str(),
104 BootstrapChannel::IPFS { hash, .. } => hash.as_str(),
105 BootstrapChannel::Email { address, .. } => address.as_str(),
106 }
107 }
108
109 pub fn channel_type(&self) -> &str {
111 match self {
112 BootstrapChannel::CDN { .. } => "CDN",
113 BootstrapChannel::Telegram { .. } => "Telegram",
114 BootstrapChannel::GitHub { .. } => "GitHub",
115 BootstrapChannel::IPFS { .. } => "IPFS",
116 BootstrapChannel::Email { .. } => "Email",
117 }
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct BootstrapConfig {
124 pub channels: Vec<BootstrapChannel>,
126 pub max_descriptor_age: u64,
128 pub min_success_channels: usize,
130 pub refresh_interval: u64,
132 pub randomize_first_refresh: bool,
134}
135
136impl Default for BootstrapConfig {
137 fn default() -> Self {
138 Self {
139 channels: Vec::new(),
140 max_descriptor_age: 86400, min_success_channels: 1,
142 refresh_interval: 3600, randomize_first_refresh: true,
144 }
145 }
146}
147
148impl BootstrapConfig {
149 pub fn new(channels: Vec<BootstrapChannel>) -> Self {
151 Self {
152 channels,
153 ..Default::default()
154 }
155 }
156
157 pub fn with_cdn(mut self, url: impl Into<String>, provider: impl Into<String>) -> Self {
159 self.channels.push(BootstrapChannel::CDN {
160 url: url.into(),
161 provider: provider.into(),
162 });
163 self
164 }
165
166 pub fn with_telegram(mut self, bot_username: impl Into<String>) -> Self {
168 self.channels.push(BootstrapChannel::Telegram {
169 bot_username: bot_username.into(),
170 token: None,
171 });
172 self
173 }
174
175 pub fn with_github(mut self, repo: impl Into<String>, asset_name: impl Into<String>) -> Self {
177 self.channels.push(BootstrapChannel::GitHub {
178 repo: repo.into(),
179 asset_name: asset_name.into(),
180 });
181 self
182 }
183
184 pub fn with_ipfs(mut self, hash: impl Into<String>) -> Self {
186 self.channels.push(BootstrapChannel::IPFS {
187 hash: hash.into(),
188 gateway: None,
189 });
190 self
191 }
192}
193
194pub fn current_unix_secs() -> u64 {
195 crate::crypto::current_timestamp_ms() / 1000
196}
197
198fn derive_bootstrap_seed(
199 descriptor: &BootstrapDescriptor,
200 preshared_key: Option<&[u8; 32]>,
201 slot: u8,
202) -> [u8; 32] {
203 let mut hasher = blake3::Hasher::new();
204 hasher.update(&descriptor.kdf_salt);
205 hasher.update(descriptor.descriptor_id.as_bytes());
206 hasher.update(&[slot]);
207 match preshared_key {
208 Some(psk) => {
209 hasher.update(psk);
210 }
211 None => {
212 hasher.update(&[0u8; 32]);
213 }
214 };
215 let hash = hasher.finalize();
216 let mut seed = [0u8; 32];
217 seed.copy_from_slice(&hash.as_bytes()[..32]);
218 seed
219}
220
221pub fn derive_bootstrap_candidate(
222 descriptor: &BootstrapDescriptor,
223 preshared_key: Option<&[u8; 32]>,
224 slot: u8,
225) -> Option<MaskProfile> {
226 let embedded_masks = &descriptor.embedded_masks;
227 let base_ids = if descriptor.base_mask_ids.is_empty() && embedded_masks.is_empty() {
228 preset_masks::all()
229 .into_iter()
230 .map(|mask| mask.mask_id)
231 .collect::<Vec<_>>()
232 } else {
233 descriptor.base_mask_ids.clone()
234 };
235 if base_ids.is_empty() && embedded_masks.is_empty() {
236 return None;
237 }
238
239 let seed = derive_bootstrap_seed(descriptor, preshared_key, slot);
240 let selector_len = if !embedded_masks.is_empty() {
241 embedded_masks.len()
242 } else {
243 base_ids.len()
244 };
245 let base_index = (seed[0] as usize) % selector_len;
246 let mut mask = if !embedded_masks.is_empty() {
247 embedded_masks[base_index].clone()
248 } else {
249 preset_masks::by_id(&base_ids[base_index])?
250 };
251 let extra_gap_len = (seed[1] % 9) as usize;
252
253 if extra_gap_len > 0 {
254 let mut fields = mask
255 .header_spec
256 .as_ref()
257 .map(HeaderSpec::fields)
258 .unwrap_or_else(|| {
259 vec![HeaderField::Fixed {
260 bytes: mask.header_template.clone(),
261 }]
262 });
263 fields.push(HeaderField::Random { len: extra_gap_len });
264 mask.header_spec = Some(HeaderSpec::Structured { fields });
265 mask.eph_pub_offset = mask.eph_pub_offset.saturating_add(extra_gap_len as u16);
266 }
267
268 mask.mask_id = format!(
269 "bootstrap:{}:{}:{}:{:02x}{:02x}",
270 descriptor.descriptor_id,
271 if !embedded_masks.is_empty() {
272 &embedded_masks[base_index].mask_id
273 } else {
274 &base_ids[base_index]
275 },
276 slot,
277 seed[0],
278 seed[1]
279 );
280 Some(mask)
281}
282
283pub fn derive_bootstrap_candidates(
284 descriptor: &BootstrapDescriptor,
285 preshared_key: Option<&[u8; 32]>,
286) -> Vec<MaskProfile> {
287 (0..descriptor.candidate_count)
288 .filter_map(|slot| derive_bootstrap_candidate(descriptor, preshared_key, slot))
289 .collect()
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct MaskProfile {
295 pub mask_id: String,
297 pub version: u16,
299 pub created_at: u64,
301 pub expires_at: u64,
303
304 pub spoof_protocol: SpoofProtocol,
306 pub header_template: Vec<u8>,
308 pub eph_pub_offset: u16,
310 pub eph_pub_length: u16,
312
313 pub size_distribution: SizeDistribution,
315 pub iat_distribution: IATDistribution,
317 pub padding_strategy: PaddingStrategy,
319
320 pub fsm_states: Vec<FSMState>,
322 pub fsm_initial_state: u16,
324
325 pub signature_vector: Vec<f32>,
327
328 pub reverse_profile: Option<Box<MaskProfile>>,
330
331 #[serde(with = "serde_bytes")]
333 #[serde(default = "default_signature")]
334 pub signature: [u8; 64],
335
336 #[serde(default)]
341 pub header_spec: Option<HeaderSpec>,
342}
343
344fn default_signature() -> [u8; 64] {
345 [0u8; 64]
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
350#[allow(non_camel_case_types)]
351pub enum SpoofProtocol {
352 None,
353 QUIC,
354 WebRTC_STUN,
355 HTTPS_H2,
356 DNS_over_UDP,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct SizeDistribution {
362 pub dist_type: SizeDistType,
363 pub bins: Vec<(u16, u16, f32)>, pub parametric_type: Option<ParametricType>,
365 pub parametric_params: Option<Vec<f64>>,
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
369pub enum SizeDistType {
370 Histogram,
371 Parametric,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
375pub enum ParametricType {
376 LogNormal,
377 Gamma,
378 Bimodal,
379}
380
381impl SizeDistribution {
382 pub fn sample<R: Rng>(&self, rng: &mut R) -> u16 {
384 match self.dist_type {
385 SizeDistType::Histogram => {
386 if self.bins.is_empty() {
387 return 64; }
389
390 let weights: Vec<f32> = self.bins.iter().map(|(_, _, p)| *p).collect();
392 if let Ok(dist) = WeightedIndex::new(&weights) {
393 let bin_idx = dist.sample(rng);
394 let (min, max, _) = self.bins[bin_idx];
395 rng.gen_range(min..=max)
396 } else {
397 64
398 }
399 }
400 SizeDistType::Parametric => {
401 match self.parametric_type {
402 Some(ParametricType::LogNormal) => {
403 if let Some(params) = &self.parametric_params {
404 let mu: f64 = params[0];
405 let sigma: f64 = params[1];
406 let u1: f64 = rng.gen::<f64>().max(1e-10); let u2: f64 = rng.gen();
409 let z =
410 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
411 let sample = (mu + sigma * z).exp();
413 (sample as u16).max(1)
414 } else {
415 rng.gen_range(64..512)
416 }
417 }
418 _ => rng.gen_range(64..512),
419 }
420 }
421 }
422 }
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct IATDistribution {
428 pub dist_type: IATDistType,
429 pub params: Vec<f64>,
430 pub jitter_range_ms: (f64, f64),
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
434pub enum IATDistType {
435 Exponential,
436 LogNormal,
437 Gamma,
438 Empirical,
439}
440
441impl IATDistribution {
442 pub fn sample<R: Rng>(&self, rng: &mut R) -> f64 {
444 let base_iat = match self.dist_type {
445 IATDistType::Exponential => {
446 let lambda: f64 = self.params[0];
447 let val: f64 = rng.gen::<f64>().max(1e-10);
448 -(1.0 - val).ln() / lambda
449 }
450 IATDistType::LogNormal => {
451 let mu: f64 = self.params[0];
452 let sigma: f64 = self.params[1];
453 let u1: f64 = rng.gen::<f64>().max(1e-10);
455 let u2: f64 = rng.gen();
456 let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
457 (mu + sigma * z).exp()
458 }
459 IATDistType::Gamma => {
460 let k: f64 = self.params[0];
462 let theta: f64 = self.params[1];
463 let sum: f64 = (0..k.max(1.0) as i32)
464 .map(|_| {
465 let val: f64 = rng.gen::<f64>().max(1e-10);
466 -(1.0 - val).ln()
467 })
468 .sum();
469 sum * theta
470 }
471 IATDistType::Empirical => {
472 let idx = rng.gen_range(0..self.params.len());
473 self.params[idx]
474 }
475 };
476
477 let jitter = rng.gen_range(self.jitter_range_ms.0..=self.jitter_range_ms.1);
479 (base_iat + jitter).max(0.0)
480 }
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485pub enum PaddingStrategy {
486 RandomUniform { min: u16, max: u16 },
487 MatchDistribution,
488 Fixed { size: u16 },
489}
490
491impl PaddingStrategy {
492 pub fn calc_padding<R: Rng>(&self, payload_size: usize, target_size: u16, rng: &mut R) -> u16 {
494 match self {
495 Self::RandomUniform { min, max } => rng.gen_range(*min..=*max),
496 Self::MatchDistribution => {
497 if target_size as usize > payload_size {
498 (target_size as usize - payload_size) as u16
499 } else {
500 0
501 }
502 }
503 Self::Fixed { size } => *size,
504 }
505 }
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
514#[serde(tag = "type")]
515pub enum HeaderSpec {
516 Structured { fields: Vec<HeaderField> },
518 RawPrefix {
521 prefix_hex: String,
523 #[serde(default)]
525 randomize_indices: Vec<usize>,
526 },
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
530#[serde(tag = "kind")]
531pub enum HeaderField {
532 Fixed {
533 bytes: Vec<u8>,
534 },
535 Random {
536 len: usize,
537 },
538 Length {
539 len: usize,
540 endian: HeaderEndian,
541 },
542 Id {
543 len: usize,
544 mode: IdFieldMode,
545 },
546 CounterLike {
547 len: usize,
548 endian: HeaderEndian,
549 #[serde(default)]
550 start: u64,
551 #[serde(default = "default_counter_step")]
552 step: u64,
553 },
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
557pub enum HeaderEndian {
558 Big,
559 Little,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize, Default)]
563pub enum IdFieldMode {
564 #[default]
565 Random,
566 Zero,
567}
568
569fn default_counter_step() -> u64 {
570 1
571}
572
573impl HeaderSpec {
574 pub fn structured(fields: Vec<HeaderField>) -> Self {
575 Self::Structured { fields }
576 }
577
578 pub fn stun_binding() -> Self {
579 Self::stun_binding_with_cookie(true)
580 }
581
582 pub fn stun_binding_with_cookie(magic_cookie: bool) -> Self {
583 Self::structured(vec![
584 HeaderField::Fixed {
585 bytes: vec![0x00, 0x01],
586 },
587 HeaderField::Length {
588 len: 2,
589 endian: HeaderEndian::Big,
590 },
591 HeaderField::Fixed {
592 bytes: if magic_cookie {
593 vec![0x21, 0x12, 0xA4, 0x42]
594 } else {
595 vec![0x00, 0x00, 0x00, 0x00]
596 },
597 },
598 HeaderField::Id {
599 len: 12,
600 mode: IdFieldMode::Random,
601 },
602 ])
603 }
604
605 pub fn quic_initial(version: u32, dcid_len: u8) -> Self {
606 let dcid_len = dcid_len.clamp(8, 20);
607 Self::structured(vec![
608 HeaderField::Fixed { bytes: vec![0xC0] },
609 HeaderField::Fixed {
610 bytes: version.to_be_bytes().to_vec(),
611 },
612 HeaderField::Fixed {
613 bytes: vec![dcid_len],
614 },
615 HeaderField::Id {
616 len: dcid_len as usize,
617 mode: IdFieldMode::Random,
618 },
619 ])
620 }
621
622 pub fn dns_query(flags: u16) -> Self {
623 Self::structured(vec![
624 HeaderField::Id {
625 len: 2,
626 mode: IdFieldMode::Random,
627 },
628 HeaderField::Fixed {
629 bytes: flags.to_be_bytes().to_vec(),
630 },
631 HeaderField::Fixed {
632 bytes: vec![0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
633 },
634 ])
635 }
636
637 pub fn tls_record(content_type: u8, version: u16) -> Self {
638 Self::structured(vec![
639 HeaderField::Fixed {
640 bytes: vec![content_type],
641 },
642 HeaderField::Fixed {
643 bytes: version.to_be_bytes().to_vec(),
644 },
645 HeaderField::Length {
646 len: 2,
647 endian: HeaderEndian::Big,
648 },
649 ])
650 }
651
652 pub fn fields(&self) -> Vec<HeaderField> {
653 match self {
654 Self::Structured { fields } => fields.clone(),
655 Self::RawPrefix {
656 prefix_hex,
657 randomize_indices,
658 } => {
659 let bytes =
660 hex::decode(prefix_hex).unwrap_or_else(|_| vec![0x00, 0x01, 0x02, 0x03]);
661 if randomize_indices.is_empty() {
662 return vec![HeaderField::Fixed { bytes }];
663 }
664 let mut fields = Vec::new();
665 let mut current_fixed = Vec::new();
666 for (idx, byte) in bytes.iter().enumerate() {
667 if randomize_indices.contains(&idx) {
668 if !current_fixed.is_empty() {
669 fields.push(HeaderField::Fixed {
670 bytes: std::mem::take(&mut current_fixed),
671 });
672 }
673 fields.push(HeaderField::Random { len: 1 });
674 } else {
675 current_fixed.push(*byte);
676 }
677 }
678 if !current_fixed.is_empty() {
679 fields.push(HeaderField::Fixed {
680 bytes: current_fixed,
681 });
682 }
683 fields
684 }
685 }
686 }
687
688 pub fn generate<R: Rng>(&self, rng: &mut R) -> Vec<u8> {
691 let mut header = Vec::new();
692 for field in self.fields() {
693 match field {
694 HeaderField::Fixed { bytes } => header.extend_from_slice(&bytes),
695 HeaderField::Random { len } => {
696 let start = header.len();
697 header.resize(start + len, 0);
698 rng.fill_bytes(&mut header[start..start + len]);
699 }
700 HeaderField::Length { len, endian } => {
701 let bytes = encode_semantic_u64(0, len, endian);
702 header.extend_from_slice(&bytes);
703 }
704 HeaderField::Id { len, mode } => match mode {
705 IdFieldMode::Random => {
706 let start = header.len();
707 header.resize(start + len, 0);
708 rng.fill_bytes(&mut header[start..start + len]);
709 }
710 IdFieldMode::Zero => header.extend(std::iter::repeat_n(0u8, len)),
711 },
712 HeaderField::CounterLike {
713 len,
714 endian,
715 start,
716 step,
717 } => {
718 let raw = start.saturating_add(rng.gen_range(0..=step.max(1) * 1024));
719 let bytes = encode_semantic_u64(raw, len, endian);
720 header.extend_from_slice(&bytes);
721 }
722 }
723 }
724 header
725 }
726
727 pub fn min_length(&self) -> usize {
729 self.fields()
730 .into_iter()
731 .map(|field| match field {
732 HeaderField::Fixed { bytes } => bytes.len(),
733 HeaderField::Random { len }
734 | HeaderField::Length { len, .. }
735 | HeaderField::Id { len, .. }
736 | HeaderField::CounterLike { len, .. } => len,
737 })
738 .sum()
739 }
740
741 pub fn generate_static(&self) -> Vec<u8> {
744 use rand::SeedableRng;
745 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
746 self.generate(&mut rng)
747 }
748}
749
750fn encode_semantic_u64(value: u64, len: usize, endian: HeaderEndian) -> Vec<u8> {
751 let mut bytes = match endian {
752 HeaderEndian::Big => value.to_be_bytes().to_vec(),
753 HeaderEndian::Little => value.to_le_bytes().to_vec(),
754 };
755 if len < bytes.len() {
756 match endian {
757 HeaderEndian::Big => bytes = bytes[bytes.len() - len..].to_vec(),
758 HeaderEndian::Little => bytes.truncate(len),
759 }
760 } else if len > bytes.len() {
761 let mut out = vec![0u8; len - bytes.len()];
762 match endian {
763 HeaderEndian::Big => {
764 out.extend(bytes);
765 bytes = out;
766 }
767 HeaderEndian::Little => {
768 bytes.extend(out);
769 }
770 }
771 }
772 bytes
773}
774
775#[derive(Debug, Clone, Serialize, Deserialize)]
777pub struct FSMState {
778 pub state_id: u16,
779 pub transitions: Vec<FSMTransition>,
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize)]
784pub struct FSMTransition {
785 pub condition: TransitionCondition,
786 pub next_state: u16,
787 pub size_override: Option<SizeDistribution>,
788 pub iat_override: Option<IATDistribution>,
789 pub padding_override: Option<PaddingStrategy>,
790}
791
792#[derive(Debug, Clone, Serialize, Deserialize)]
794pub enum TransitionCondition {
795 AfterPackets(u32),
796 AfterDuration(u64), OnPayloadType(u8),
798 Random(f32), }
800
801impl MaskProfile {
802 pub fn verify_signature(&self, public_key: &[u8; 32]) -> Result<bool> {
804 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
805
806 let vk = VerifyingKey::from_bytes(public_key)
807 .map_err(|e| Error::Crypto(format!("Invalid Ed25519 public key: {}", e)))?;
808
809 let mut message = Vec::new();
811 message.extend_from_slice(self.mask_id.as_bytes());
812 message.extend_from_slice(&self.version.to_le_bytes());
813 message.extend_from_slice(&self.header_template);
814
815 let sig = Signature::from_bytes(&self.signature);
816 match vk.verify(&message, &sig) {
817 Ok(()) => Ok(true),
818 Err(_) => Ok(false),
819 }
820 }
821
822 pub fn initial_state(&self) -> u16 {
824 self.fsm_initial_state
825 }
826
827 pub fn process_transition(
829 &self,
830 current_state: u16,
831 packets_in_state: u32,
832 duration_in_state_ms: u64,
833 ) -> (
834 u16,
835 Option<SizeDistribution>,
836 Option<IATDistribution>,
837 Option<PaddingStrategy>,
838 ) {
839 let state = self.fsm_states.iter().find(|s| s.state_id == current_state);
840 if let Some(state) = state {
841 for transition in &state.transitions {
842 let should_transition = match &transition.condition {
843 TransitionCondition::AfterPackets(n) => packets_in_state >= *n,
844 TransitionCondition::AfterDuration(ms) => duration_in_state_ms >= *ms,
845 TransitionCondition::Random(prob) => {
846 rand::thread_rng().gen_range(0.0..1.0) < *prob
847 }
848 TransitionCondition::OnPayloadType(_) => false, };
850
851 if should_transition {
852 return (
853 transition.next_state,
854 transition.size_override.clone(),
855 transition.iat_override.clone(),
856 transition.padding_override.clone(),
857 );
858 }
859 }
860 }
861 (current_state, None, None, None)
862 }
863}
864
865#[cfg(test)]
866mod distribution_tests {
867 use super::{IATDistType, IATDistribution};
868 use rand::{rngs::StdRng, SeedableRng};
869
870 #[test]
871 fn iat_sampling_uses_symmetric_jitter_range() {
872 let dist = IATDistribution {
873 dist_type: IATDistType::Empirical,
874 params: vec![50.0],
875 jitter_range_ms: (-10.0, 10.0),
876 };
877 let mut rng = StdRng::seed_from_u64(7);
878 let samples: Vec<f64> = (0..256).map(|_| dist.sample(&mut rng)).collect();
879
880 assert!(samples.iter().any(|&value| value < 50.0));
881 assert!(samples.iter().any(|&value| value > 50.0));
882 }
883}
884
885pub mod preset_masks {
887 use super::*;
888 use std::sync::OnceLock;
889
890 static WEBRTC_ZOOM_V3: OnceLock<MaskProfile> = OnceLock::new();
891 static QUIC_HTTPS_V2: OnceLock<MaskProfile> = OnceLock::new();
892 static WEBRTC_YANDEX_TELEMOST_V1: OnceLock<MaskProfile> = OnceLock::new();
893 static WEBRTC_VK_TEAMS_V1: OnceLock<MaskProfile> = OnceLock::new();
894 static WEBRTC_SBERJAZZ_V1: OnceLock<MaskProfile> = OnceLock::new();
895
896 fn parse_mask(json: &str) -> MaskProfile {
897 serde_json::from_str(json).expect("valid preset mask asset")
898 }
899
900 fn load_webrtc_zoom_v3() -> MaskProfile {
901 parse_mask(include_str!("../mask-assets/webrtc_zoom_v3.json"))
902 }
903
904 fn load_quic_https_v2() -> MaskProfile {
905 parse_mask(include_str!("../mask-assets/quic_https_v2.json"))
906 }
907
908 fn load_webrtc_yandex_telemost_v1() -> MaskProfile {
909 parse_mask(include_str!(
910 "../mask-assets/webrtc_yandex_telemost_v1.json"
911 ))
912 }
913
914 fn load_webrtc_vk_teams_v1() -> MaskProfile {
915 parse_mask(include_str!("../mask-assets/webrtc_vk_teams_v1.json"))
916 }
917
918 fn load_webrtc_sberjazz_v1() -> MaskProfile {
919 parse_mask(include_str!("../mask-assets/webrtc_sberjazz_v1.json"))
920 }
921
922 pub fn webrtc_zoom_v3() -> MaskProfile {
923 WEBRTC_ZOOM_V3.get_or_init(load_webrtc_zoom_v3).clone()
924 }
925
926 pub fn quic_https_v2() -> MaskProfile {
927 QUIC_HTTPS_V2.get_or_init(load_quic_https_v2).clone()
928 }
929
930 pub fn webrtc_yandex_telemost_v1() -> MaskProfile {
931 WEBRTC_YANDEX_TELEMOST_V1
932 .get_or_init(load_webrtc_yandex_telemost_v1)
933 .clone()
934 }
935
936 pub fn webrtc_vk_teams_v1() -> MaskProfile {
937 WEBRTC_VK_TEAMS_V1
938 .get_or_init(load_webrtc_vk_teams_v1)
939 .clone()
940 }
941
942 pub fn webrtc_sberjazz_v1() -> MaskProfile {
943 WEBRTC_SBERJAZZ_V1
944 .get_or_init(load_webrtc_sberjazz_v1)
945 .clone()
946 }
947
948 pub fn all() -> Vec<MaskProfile> {
949 vec![
950 webrtc_zoom_v3(),
951 quic_https_v2(),
952 webrtc_yandex_telemost_v1(),
953 webrtc_vk_teams_v1(),
954 webrtc_sberjazz_v1(),
955 ]
956 }
957
958 pub fn by_id(mask_id: &str) -> Option<MaskProfile> {
959 match mask_id {
960 "webrtc_zoom_v3" => Some(webrtc_zoom_v3()),
961 "quic_https_v2" => Some(quic_https_v2()),
962 "webrtc_yandex_telemost_v1" => Some(webrtc_yandex_telemost_v1()),
963 "webrtc_vk_teams_v1" => Some(webrtc_vk_teams_v1()),
964 "webrtc_sberjazz_v1" => Some(webrtc_sberjazz_v1()),
965 _ => None,
966 }
967 }
968
969 pub fn bootstrap_default() -> MaskProfile {
970 webrtc_zoom_v3()
971 }
972}
973
974#[cfg(test)]
975mod tests {
976 use super::*;
977 use rand::rngs::StdRng;
978 use rand::SeedableRng;
979
980 #[test]
981 fn test_stun_binding_generation() {
982 let spec = HeaderSpec::stun_binding();
983
984 let mut rng = StdRng::seed_from_u64(42);
986 let header1 = spec.generate(&mut rng);
987 let header2 = spec.generate(&mut rng);
988
989 assert_eq!(header1.len(), 20);
990 assert_eq!(header2.len(), 20);
991
992 assert_eq!(&header1[0..2], &[0x00, 0x01]); assert_eq!(&header1[4..8], &[0x21, 0x12, 0xA4, 0x42]); assert_ne!(&header1[8..], &header2[8..]);
998 }
999
1000 #[test]
1001 fn test_quic_initial_generation() {
1002 let spec = HeaderSpec::quic_initial(0x00000001, 8);
1003
1004 let mut rng = StdRng::seed_from_u64(42);
1005 let header1 = spec.generate(&mut rng);
1006 let header2 = spec.generate(&mut rng);
1007
1008 assert_eq!(header1.len(), 14); assert_eq!(header2.len(), 14);
1010
1011 assert_eq!(header1[0], 0xC0);
1013
1014 assert_eq!(&header1[1..5], &0x00000001u32.to_be_bytes());
1016
1017 assert_eq!(header1[5], 8);
1019
1020 assert_ne!(&header1[6..], &header2[6..]);
1022 }
1023
1024 #[test]
1025 fn test_dns_query_generation() {
1026 let spec = HeaderSpec::dns_query(0x0100);
1027
1028 let mut rng = StdRng::seed_from_u64(42);
1029 let header1 = spec.generate(&mut rng);
1030 let header2 = spec.generate(&mut rng);
1031
1032 assert_eq!(header1.len(), 12);
1033 assert_eq!(header2.len(), 12);
1034
1035 assert_eq!(&header1[2..4], &[0x01, 0x00]);
1037 assert_eq!(&header2[2..4], &[0x01, 0x00]);
1038
1039 assert_ne!(&header1[0..2], &header2[0..2]);
1041
1042 assert_eq!(
1044 &header1[4..],
1045 &[0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
1046 );
1047 }
1048
1049 #[test]
1050 fn test_tls_record_generation() {
1051 let spec = HeaderSpec::tls_record(0x17, 0x0303);
1052
1053 let mut rng = StdRng::seed_from_u64(42);
1054 let header = spec.generate(&mut rng);
1055
1056 assert_eq!(header.len(), 5);
1057 assert_eq!(header[0], 0x17); assert_eq!(&header[1..3], &[0x03, 0x03]); assert_eq!(&header[3..5], &[0x00, 0x00]); }
1061
1062 #[test]
1063 fn test_raw_prefix_generation() {
1064 let spec = HeaderSpec::RawPrefix {
1065 prefix_hex: "010203040506".to_string(),
1066 randomize_indices: vec![2, 4],
1067 };
1068
1069 let mut rng = StdRng::seed_from_u64(42);
1070 let header1 = spec.generate(&mut rng);
1071 let header2 = spec.generate(&mut rng);
1072
1073 assert_eq!(header1.len(), 6);
1074 assert_eq!(header2.len(), 6);
1075
1076 assert_eq!(header1[0], header2[0]); assert_eq!(header1[1], header2[1]); assert_eq!(header1[3], header2[3]); assert_eq!(header1[5], header2[5]); assert_ne!(header1[2], header2[2]);
1084 assert_ne!(header1[4], header2[4]);
1085 }
1086
1087 #[test]
1088 fn test_header_spec_min_length() {
1089 let stun = HeaderSpec::stun_binding();
1090 assert_eq!(stun.min_length(), 20);
1091
1092 let quic = HeaderSpec::quic_initial(0x00000001, 8);
1093 assert_eq!(quic.min_length(), 14);
1095
1096 let dns = HeaderSpec::dns_query(0x0100);
1097 assert_eq!(dns.min_length(), 12);
1098
1099 let tls = HeaderSpec::tls_record(0x17, 0x0303);
1100 assert_eq!(tls.min_length(), 5);
1101 }
1102
1103 #[test]
1104 fn test_static_generation_deterministic() {
1105 let spec = HeaderSpec::stun_binding();
1106
1107 let static1 = spec.generate_static();
1108 let static2 = spec.generate_static();
1109
1110 assert_eq!(static1, static2);
1112 }
1113
1114 #[test]
1115 fn test_preset_masks_have_header_spec() {
1116 let mask = preset_masks::webrtc_zoom_v3();
1117 assert!(mask.header_spec.is_some());
1118 assert_eq!(mask.version, 2);
1119
1120 let mask2 = preset_masks::quic_https_v2();
1121 assert!(mask2.header_spec.is_some());
1122 assert_eq!(mask2.version, 2);
1123 }
1124
1125 #[test]
1126 fn bootstrap_derivation_is_deterministic() {
1127 let descriptor = BootstrapDescriptor {
1128 descriptor_id: "epoch-1".into(),
1129 version: 1,
1130 created_at: 0,
1131 expires_at: u64::MAX,
1132 base_mask_ids: vec!["webrtc_zoom_v3".into(), "quic_https_v2".into()],
1133 embedded_masks: Vec::new(),
1134 candidate_count: 4,
1135 kdf_salt: [7u8; 32],
1136 signature: [0u8; 64],
1137 };
1138 let psk = [3u8; 32];
1139 let left = derive_bootstrap_candidates(&descriptor, Some(&psk));
1140 let right = derive_bootstrap_candidates(&descriptor, Some(&psk));
1141
1142 assert_eq!(left.len(), right.len());
1143 for (lhs, rhs) in left.iter().zip(right.iter()) {
1144 assert_eq!(lhs.mask_id, rhs.mask_id);
1145 assert_eq!(lhs.eph_pub_offset, rhs.eph_pub_offset);
1146 assert_eq!(
1147 lhs.header_spec.as_ref().map(|s| s.min_length()),
1148 rhs.header_spec.as_ref().map(|s| s.min_length())
1149 );
1150 }
1151 }
1152
1153 #[test]
1154 fn bootstrap_derivation_varies_across_psks() {
1155 let descriptor = BootstrapDescriptor {
1156 descriptor_id: "epoch-2".into(),
1157 version: 1,
1158 created_at: 0,
1159 expires_at: u64::MAX,
1160 base_mask_ids: vec!["webrtc_zoom_v3".into(), "quic_https_v2".into()],
1161 embedded_masks: Vec::new(),
1162 candidate_count: 4,
1163 kdf_salt: [11u8; 32],
1164 signature: [0u8; 64],
1165 };
1166
1167 let first = derive_bootstrap_candidates(&descriptor, Some(&[1u8; 32]));
1168 let second = derive_bootstrap_candidates(&descriptor, Some(&[2u8; 32]));
1169
1170 assert_ne!(first[0].mask_id, second[0].mask_id);
1171 }
1172
1173 #[test]
1174 fn bootstrap_derivation_supports_embedded_masks() {
1175 let descriptor = BootstrapDescriptor {
1176 descriptor_id: "epoch-3".into(),
1177 version: 1,
1178 created_at: 0,
1179 expires_at: u64::MAX,
1180 base_mask_ids: Vec::new(),
1181 embedded_masks: vec![preset_masks::webrtc_zoom_v3()],
1182 candidate_count: 1,
1183 kdf_salt: [13u8; 32],
1184 signature: [0u8; 64],
1185 };
1186
1187 let masks = derive_bootstrap_candidates(&descriptor, Some(&[9u8; 32]));
1188 assert_eq!(masks.len(), 1);
1189 assert!(masks[0]
1190 .mask_id
1191 .starts_with("bootstrap:epoch-3:webrtc_zoom_v3:"));
1192 }
1193}