Skip to main content

aivpn_common/
mask.rs

1//! Mask System (Traffic Mimicry Profiles)
2//!
3//! Implements Mask profiles that define traffic shaping behavior
4
5use rand::distributions::weighted::WeightedIndex;
6use rand::{distributions::Distribution, Rng};
7use serde::{Deserialize, Serialize};
8
9use crate::error::{Error, Result};
10
11/// Rotating bootstrap descriptor.
12#[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    /// Verify the ed25519 signature of this descriptor against an operator signing key.
41    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/// Multi-channel bootstrap descriptor distribution.
55///
56/// Each channel represents a different method to fetch bootstrap descriptors.
57/// Using multiple channels makes it harder for censors to block all distribution points.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum BootstrapChannel {
60    /// CDN-based distribution (e.g., Cloudflare, AWS CloudFront)
61    CDN {
62        /// CDN URL endpoint
63        url: String,
64        /// CDN provider name for logging/analytics
65        provider: String,
66    },
67    /// Telegram bot-based distribution
68    Telegram {
69        /// Telegram bot username
70        bot_username: String,
71        /// Bot token (optional, can use public bot)
72        token: Option<String>,
73    },
74    /// GitHub releases/assets distribution
75    GitHub {
76        /// Repository in format "owner/repo"
77        repo: String,
78        /// Asset name pattern (e.g., "bootstrap-descriptors-v*.json")
79        asset_name: String,
80    },
81    /// IPFS-based distribution (content-addressed)
82    IPFS {
83        /// IPFS content hash (CID)
84        hash: String,
85        /// Gateway URL (optional, uses default gateway if None)
86        gateway: Option<String>,
87    },
88    /// Email-based distribution (for enterprise deployments)
89    Email {
90        /// Email address to request descriptors from
91        address: String,
92        /// Subject line pattern for automated requests
93        subject_pattern: String,
94    },
95}
96
97impl BootstrapChannel {
98    /// Get a human-readable name for this channel
99    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    /// Get the channel type name
110    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/// Configuration for multi-channel bootstrap descriptor distribution.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct BootstrapConfig {
124    /// List of channels to try (in order of preference)
125    pub channels: Vec<BootstrapChannel>,
126    /// Maximum age of descriptors to accept (seconds)
127    pub max_descriptor_age: u64,
128    /// Minimum number of channels that must succeed
129    pub min_success_channels: usize,
130    /// Background refresh interval (seconds)
131    pub refresh_interval: u64,
132    /// Whether to use random delay before first refresh
133    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, // 24 hours
141            min_success_channels: 1,
142            refresh_interval: 3600, // 1 hour
143            randomize_first_refresh: true,
144        }
145    }
146}
147
148impl BootstrapConfig {
149    /// Create a new bootstrap config with the given channels
150    pub fn new(channels: Vec<BootstrapChannel>) -> Self {
151        Self {
152            channels,
153            ..Default::default()
154        }
155    }
156
157    /// Add a CDN channel
158    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    /// Add a Telegram channel
167    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    /// Add a GitHub channel
176    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    /// Add an IPFS channel
185    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/// Mask profile for traffic mimicry
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct MaskProfile {
295    /// Unique identifier
296    pub mask_id: String,
297    /// Profile version
298    pub version: u16,
299    /// Creation timestamp
300    pub created_at: u64,
301    /// Expiration timestamp
302    pub expires_at: u64,
303
304    /// Protocol to spoof
305    pub spoof_protocol: SpoofProtocol,
306    /// Header template bytes (static, for legacy compatibility)
307    pub header_template: Vec<u8>,
308    /// Offset for ephemeral public key in header
309    pub eph_pub_offset: u16,
310    /// Length of ephemeral public key (always 32)
311    pub eph_pub_length: u16,
312
313    /// Packet size distribution
314    pub size_distribution: SizeDistribution,
315    /// Inter-arrival time distribution
316    pub iat_distribution: IATDistribution,
317    /// Padding strategy
318    pub padding_strategy: PaddingStrategy,
319
320    /// FSM states for behavioral mimicry
321    pub fsm_states: Vec<FSMState>,
322    /// Initial FSM state
323    pub fsm_initial_state: u16,
324
325    /// Neural resonance signature (64 floats)
326    pub signature_vector: Vec<f32>,
327
328    /// Reverse profile for server->client traffic
329    pub reverse_profile: Option<Box<MaskProfile>>,
330
331    /// Ed25519 signature (64 bytes)
332    #[serde(with = "serde_bytes")]
333    #[serde(default = "default_signature")]
334    pub signature: [u8; 64],
335
336    /// Dynamic header specification (Issue #30 fix)
337    /// If present, clients should use this for per-packet header generation
338    /// instead of the static header_template.
339    /// Added in version 2, legacy clients ignore this field.
340    #[serde(default)]
341    pub header_spec: Option<HeaderSpec>,
342}
343
344fn default_signature() -> [u8; 64] {
345    [0u8; 64]
346}
347
348/// Protocol spoofing types
349#[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/// Packet size distribution
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct SizeDistribution {
362    pub dist_type: SizeDistType,
363    pub bins: Vec<(u16, u16, f32)>, // (min, max, probability)
364    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    /// Sample a packet size from the distribution
383    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; // Default
388                }
389
390                // Weighted random selection of bin
391                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                            // Box-Muller transform: generate standard normal from two uniform samples
407                            let u1: f64 = rng.gen::<f64>().max(1e-10); // avoid ln(0)
408                            let u2: f64 = rng.gen();
409                            let z =
410                                (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
411                            // LogNormal: exp(mu + sigma * z)
412                            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/// Inter-arrival time distribution
426#[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    /// Sample an inter-arrival time in milliseconds
443    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                // Box-Muller transform for proper normal distribution
454                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                // Simplified gamma sampling (sum of k exponentials for integer k)
461                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        // Add jitter
478        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/// Padding strategy
484#[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    /// Calculate padding length for a given payload
493    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/// Header Specification for dynamic per-packet header generation
509///
510/// Instead of storing fixed header bytes, HeaderSpec declares how to generate
511/// headers dynamically. This solves Issue #30 (WireGuard detection) by ensuring
512/// each packet has a unique but protocol-valid header.
513#[derive(Debug, Clone, Serialize, Deserialize)]
514#[serde(tag = "type")]
515pub enum HeaderSpec {
516    /// Structured header semantics expressed as typed fields.
517    Structured { fields: Vec<HeaderField> },
518    /// Raw prefix with per-packet randomization
519    /// Uses fixed bytes with optional random positions
520    RawPrefix {
521        /// Fixed prefix bytes (hex string)
522        prefix_hex: String,
523        /// Indices of bytes to randomize on each packet (0-indexed)
524        #[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    /// Generate a header from this specification
689    /// Returns different bytes on each call for randomizable fields
690    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    /// Get the minimum header length for this spec
728    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    /// Generate a static header template for legacy compatibility
742    /// Uses a seeded RNG for deterministic output
743    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/// FSM state for behavioral mimicry
776#[derive(Debug, Clone, Serialize, Deserialize)]
777pub struct FSMState {
778    pub state_id: u16,
779    pub transitions: Vec<FSMTransition>,
780}
781
782/// FSM transition
783#[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/// Transition condition
793#[derive(Debug, Clone, Serialize, Deserialize)]
794pub enum TransitionCondition {
795    AfterPackets(u32),
796    AfterDuration(u64), // milliseconds
797    OnPayloadType(u8),
798    Random(f32), // probability per packet
799}
800
801impl MaskProfile {
802    /// Verify Ed25519 signature over all profile fields except the signature itself
803    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        // Build canonical message: mask_id || version || header_template
810        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    /// Get initial FSM state
823    pub fn initial_state(&self) -> u16 {
824        self.fsm_initial_state
825    }
826
827    /// Process FSM transition
828    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, // Handled separately
849                };
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
885/// File-backed preset mask catalog
886pub 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        // Generate two headers - they should differ in transaction_id
985        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        // First 8 bytes should be the same (type + length + magic cookie)
993        assert_eq!(&header1[0..2], &[0x00, 0x01]); // Binding Request
994        assert_eq!(&header1[4..8], &[0x21, 0x12, 0xA4, 0x42]); // Magic cookie
995
996        // Transaction IDs should differ
997        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); // 1 + 4 + 1 + 8
1009        assert_eq!(header2.len(), 14);
1010
1011        // First byte should be 0xC0 (long packet)
1012        assert_eq!(header1[0], 0xC0);
1013
1014        // Version bytes
1015        assert_eq!(&header1[1..5], &0x00000001u32.to_be_bytes());
1016
1017        // DCID length
1018        assert_eq!(header1[5], 8);
1019
1020        // DCID should differ between generations
1021        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        // Flags should be consistent
1036        assert_eq!(&header1[2..4], &[0x01, 0x00]);
1037        assert_eq!(&header2[2..4], &[0x01, 0x00]);
1038
1039        // Transaction ID should differ
1040        assert_ne!(&header1[0..2], &header2[0..2]);
1041
1042        // Counts should be standard DNS query
1043        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); // Application data
1058        assert_eq!(&header[1..3], &[0x03, 0x03]); // TLS 1.2
1059        assert_eq!(&header[3..5], &[0x00, 0x00]); // Length (to be filled)
1060    }
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        // Fixed bytes should be the same
1077        assert_eq!(header1[0], header2[0]); // 0x01
1078        assert_eq!(header1[1], header2[1]); // 0x02
1079        assert_eq!(header1[3], header2[3]); // 0x04
1080        assert_eq!(header1[5], header2[5]); // 0x06
1081
1082        // Randomized bytes should differ
1083        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        // 1 (header_form) + 4 (version) + 1 (dcid_len) + 8 (dcid) = 14
1094        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        // Static generation should be deterministic
1111        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}