Skip to main content

vcl_protocol/
obfuscation.rs

1//! # VCL Traffic Obfuscation
2//!
3//! Makes VCL Protocol traffic indistinguishable from regular HTTPS/TLS
4//! to bypass Deep Packet Inspection (DPI) used by ISPs like МТС, Beeline.
5//!
6//! ## Techniques
7//!
8//! ```text
9//! 1. Packet Padding    — random padding to disguise payload size patterns
10//! 2. Timing Jitter     — random delays to disguise traffic timing patterns
11//! 3. TLS Mimicry       — wrap packets to look like TLS 1.3 records
12//! 4. HTTP/2 Mimicry    — wrap packets to look like HTTP/2 DATA frames
13//! 5. Size Normalization— normalize packet sizes to common HTTPS sizes
14//! ```
15//!
16//! ## Example
17//!
18//! ```rust
19//! use vcl_protocol::obfuscation::{Obfuscator, ObfuscationConfig, ObfuscationMode};
20//!
21//! let config = ObfuscationConfig::tls_mimicry();
22//! let mut obf = Obfuscator::new(config);
23//!
24//! let data = b"secret VCL packet";
25//! let obfuscated = obf.obfuscate(data);
26//! let restored = obf.deobfuscate(&obfuscated).unwrap();
27//! assert_eq!(restored, data);
28//! ```
29
30use crate::error::VCLError;
31use tracing::trace;
32
33/// Magic bytes that look like a TLS 1.3 record header.
34/// Content-Type: Application Data (23), Version: TLS 1.2 compat (0x0303)
35const TLS_RECORD_HEADER: [u8; 3] = [0x17, 0x03, 0x03];
36
37/// Magic bytes for HTTP/2 DATA frame header prefix.
38const HTTP2_DATA_FRAME_TYPE: u8 = 0x00;
39
40/// Common HTTPS packet sizes — normalizing to these avoids size fingerprinting.
41const COMMON_SIZES: &[usize] = &[64, 128, 256, 512, 1024, 1280, 1400, 1460];
42
43/// Obfuscation mode — how to disguise VCL traffic.
44#[derive(Debug, Clone, PartialEq)]
45pub enum ObfuscationMode {
46    /// No obfuscation — raw VCL packets.
47    None,
48    /// Add random padding to disguise payload size.
49    Padding,
50    /// Normalize packet sizes to common HTTPS sizes.
51    SizeNormalization,
52    /// Wrap packets in fake TLS 1.3 Application Data records.
53    TlsMimicry,
54    /// Wrap packets in fake HTTP/2 DATA frames.
55    Http2Mimicry,
56    /// Full obfuscation: TLS mimicry + size normalization.
57    Full,
58}
59
60/// Configuration for traffic obfuscation.
61#[derive(Debug, Clone)]
62pub struct ObfuscationConfig {
63    pub mode: ObfuscationMode,
64    pub jitter_max_ms: u64,
65    pub min_packet_size: usize,
66    pub max_packet_size: usize,
67    pub xor_key: u8,
68}
69
70impl ObfuscationConfig {
71    /// No obfuscation.
72    pub fn none() -> Self {
73        ObfuscationConfig {
74            mode: ObfuscationMode::None,
75            jitter_max_ms: 0,
76            min_packet_size: 0,
77            max_packet_size: 65535,
78            xor_key: 0,
79        }
80    }
81
82    /// Basic padding only — low overhead.
83    pub fn padding() -> Self {
84        ObfuscationConfig {
85            mode: ObfuscationMode::Padding,
86            jitter_max_ms: 0,
87            min_packet_size: 64,
88            max_packet_size: 1460,
89            xor_key: 0xAB,
90        }
91    }
92
93    /// TLS 1.3 mimicry — looks like HTTPS to DPI.
94    pub fn tls_mimicry() -> Self {
95        ObfuscationConfig {
96            mode: ObfuscationMode::TlsMimicry,
97            jitter_max_ms: 5,
98            min_packet_size: 0,
99            max_packet_size: 16384,
100            xor_key: 0x5A,
101        }
102    }
103
104    /// HTTP/2 mimicry — looks like web traffic.
105    pub fn http2_mimicry() -> Self {
106        ObfuscationConfig {
107            mode: ObfuscationMode::Http2Mimicry,
108            jitter_max_ms: 10,
109            min_packet_size: 0,
110            max_packet_size: 16384,
111            xor_key: 0x3C,
112        }
113    }
114
115    /// Size normalization — normalizes to common HTTPS packet sizes.
116    pub fn size_normalization() -> Self {
117        ObfuscationConfig {
118            mode: ObfuscationMode::SizeNormalization,
119            jitter_max_ms: 0,
120            min_packet_size: 0,
121            max_packet_size: 1460,
122            xor_key: 0x77,
123        }
124    }
125
126    /// Full obfuscation — maximum stealth, recommended for МТС/censored networks.
127    pub fn full() -> Self {
128        ObfuscationConfig {
129            mode: ObfuscationMode::Full,
130            jitter_max_ms: 15,
131            min_packet_size: 128,
132            max_packet_size: 16384,
133            xor_key: 0xF3,
134        }
135    }
136}
137
138impl Default for ObfuscationConfig {
139    fn default() -> Self {
140        Self::tls_mimicry()
141    }
142}
143
144/// Traffic obfuscator — wraps and unwraps VCL packets.
145pub struct Obfuscator {
146    config: ObfuscationConfig,
147    counter: u64,
148    total_obfuscated: u64,
149    total_deobfuscated: u64,
150    total_overhead: u64,
151}
152
153impl Obfuscator {
154    /// Create a new obfuscator with the given config.
155    pub fn new(config: ObfuscationConfig) -> Self {
156        Obfuscator {
157            config,
158            counter: 0,
159            total_obfuscated: 0,
160            total_deobfuscated: 0,
161            total_overhead: 0,
162        }
163    }
164
165    /// Obfuscate a VCL packet payload.
166    pub fn obfuscate(&mut self, data: &[u8]) -> Vec<u8> {
167        self.counter += 1;
168        let original_len = data.len();
169
170        let result = match &self.config.mode {
171            ObfuscationMode::None             => data.to_vec(),
172            ObfuscationMode::Padding          => self.apply_padding(data),
173            ObfuscationMode::SizeNormalization => self.apply_size_normalization(data),
174            ObfuscationMode::TlsMimicry       => self.apply_tls_mimicry(data),
175            ObfuscationMode::Http2Mimicry     => self.apply_http2_mimicry(data),
176            ObfuscationMode::Full             => {
177                let normed = self.apply_size_normalization(data);
178                self.apply_tls_mimicry(&normed)
179            }
180        };
181
182        let overhead = result.len().saturating_sub(original_len);
183        self.total_overhead += overhead as u64;
184        self.total_obfuscated += original_len as u64;
185
186        trace!(
187            mode = ?self.config.mode,
188            original = original_len,
189            obfuscated = result.len(),
190            overhead,
191            "Packet obfuscated"
192        );
193
194        result
195    }
196
197    /// Deobfuscate a received packet back to raw VCL data.
198    pub fn deobfuscate(&mut self, data: &[u8]) -> Result<Vec<u8>, VCLError> {
199        if data.is_empty() {
200            return Err(VCLError::InvalidPacket("Empty obfuscated packet".to_string()));
201        }
202
203        let result = match &self.config.mode {
204            ObfuscationMode::None             => data.to_vec(),
205            ObfuscationMode::Padding          => self.strip_padding(data)?,
206            ObfuscationMode::SizeNormalization => self.strip_size_normalization(data)?,
207            ObfuscationMode::TlsMimicry       => self.strip_tls_mimicry(data)?,
208            ObfuscationMode::Http2Mimicry     => self.strip_http2_mimicry(data)?,
209            ObfuscationMode::Full             => {
210                let stripped_tls = self.strip_tls_mimicry(data)?;
211                self.strip_size_normalization(&stripped_tls)?
212            }
213        };
214
215        self.total_deobfuscated += result.len() as u64;
216
217        trace!(
218            mode = ?self.config.mode,
219            received = data.len(),
220            restored = result.len(),
221            "Packet deobfuscated"
222        );
223
224        Ok(result)
225    }
226
227    /// Returns the jitter delay in milliseconds to wait before sending.
228    pub fn jitter_ms(&self) -> u64 {
229        if self.config.jitter_max_ms == 0 {
230            return 0;
231        }
232        let r = (self.counter.wrapping_mul(6364136223846793005)
233            .wrapping_add(1442695040888963407)) >> 33;
234        r % (self.config.jitter_max_ms + 1)
235    }
236
237    // ─── Padding ──────────────────────────────────────────────────────────────
238
239    fn apply_padding(&self, data: &[u8]) -> Vec<u8> {
240        let target = self.config.min_packet_size;
241        let padding_needed = if data.len() + 1 < target {
242            target - data.len() - 1
243        } else {
244            self.counter as usize % 16
245        };
246        let padding_len = padding_needed.min(255);
247
248        let mut result = Vec::with_capacity(1 + data.len() + padding_len);
249        result.push(padding_len as u8);
250
251        if self.config.xor_key != 0 {
252            result.extend(data.iter().map(|&b| b ^ self.config.xor_key));
253        } else {
254            result.extend_from_slice(data);
255        }
256
257        for i in 0..padding_len {
258            result.push(((i as u64).wrapping_mul(self.counter).wrapping_add(0x5A) & 0xFF) as u8);
259        }
260
261        result
262    }
263
264    fn strip_padding(&self, data: &[u8]) -> Result<Vec<u8>, VCLError> {
265        if data.is_empty() {
266            return Err(VCLError::InvalidPacket("Padding: empty packet".to_string()));
267        }
268        let padding_len = data[0] as usize;
269        let payload_end = data.len().saturating_sub(padding_len);
270        if payload_end < 1 {
271            return Err(VCLError::InvalidPacket("Padding: invalid length".to_string()));
272        }
273        let payload = &data[1..payload_end];
274
275        if self.config.xor_key != 0 {
276            Ok(payload.iter().map(|&b| b ^ self.config.xor_key).collect())
277        } else {
278            Ok(payload.to_vec())
279        }
280    }
281
282    // ─── Size normalization ───────────────────────────────────────────────────
283
284    fn apply_size_normalization(&self, data: &[u8]) -> Vec<u8> {
285        // Header: [0xCC][0xC0][padding_len u8] then payload then padding
286        let target = COMMON_SIZES.iter()
287            .find(|&&s| s >= data.len() + 3)
288            .copied()
289            .unwrap_or(data.len() + 3);
290
291        let padding_needed = target.saturating_sub(data.len() + 3);
292        let padding_len = padding_needed.min(255);
293        let mut result = Vec::with_capacity(target);
294
295        result.push(0xCC);
296        result.push(0xC0);
297        result.push(padding_len as u8);
298
299        if self.config.xor_key != 0 {
300            result.extend(data.iter().map(|&b| b ^ self.config.xor_key));
301        } else {
302            result.extend_from_slice(data);
303        }
304
305        for i in 0..padding_len {
306            result.push((i ^ 0x5A) as u8);
307        }
308
309        result
310    }
311
312    fn strip_size_normalization(&self, data: &[u8]) -> Result<Vec<u8>, VCLError> {
313        if data.len() < 3 {
314            return Err(VCLError::InvalidPacket("SizeNorm: too short".to_string()));
315        }
316        if data[0] != 0xCC || data[1] != 0xC0 {
317            return Err(VCLError::InvalidPacket("SizeNorm: invalid header".to_string()));
318        }
319        let padding_len = data[2] as usize;
320        let payload_end = data.len().saturating_sub(padding_len);
321        if payload_end < 3 {
322            return Err(VCLError::InvalidPacket("SizeNorm: invalid length".to_string()));
323        }
324        let payload = &data[3..payload_end];
325
326        if self.config.xor_key != 0 {
327            Ok(payload.iter().map(|&b| b ^ self.config.xor_key).collect())
328        } else {
329            Ok(payload.to_vec())
330        }
331    }
332
333    // ─── TLS 1.3 mimicry ──────────────────────────────────────────────────────
334
335    fn apply_tls_mimicry(&self, data: &[u8]) -> Vec<u8> {
336        let xored: Vec<u8> = if self.config.xor_key != 0 {
337            data.iter().map(|&b| b ^ self.config.xor_key).collect()
338        } else {
339            data.to_vec()
340        };
341
342        let len = xored.len() as u16;
343        let mut result = Vec::with_capacity(5 + xored.len());
344        result.extend_from_slice(&TLS_RECORD_HEADER);
345        result.extend_from_slice(&len.to_be_bytes());
346        result.extend_from_slice(&xored);
347        result
348    }
349
350    fn strip_tls_mimicry(&self, data: &[u8]) -> Result<Vec<u8>, VCLError> {
351        if data.len() < 5 {
352            return Err(VCLError::InvalidPacket(
353                "TLS mimicry: packet too short".to_string()
354            ));
355        }
356        if data[0] != TLS_RECORD_HEADER[0]
357            || data[1] != TLS_RECORD_HEADER[1]
358            || data[2] != TLS_RECORD_HEADER[2]
359        {
360            return Err(VCLError::InvalidPacket(
361                "TLS mimicry: invalid header".to_string()
362            ));
363        }
364        let payload_len = u16::from_be_bytes([data[3], data[4]]) as usize;
365        if data.len() < 5 + payload_len {
366            return Err(VCLError::InvalidPacket(
367                "TLS mimicry: truncated payload".to_string()
368            ));
369        }
370        let payload = &data[5..5 + payload_len];
371
372        if self.config.xor_key != 0 {
373            Ok(payload.iter().map(|&b| b ^ self.config.xor_key).collect())
374        } else {
375            Ok(payload.to_vec())
376        }
377    }
378
379    // ─── HTTP/2 mimicry ───────────────────────────────────────────────────────
380
381    fn apply_http2_mimicry(&self, data: &[u8]) -> Vec<u8> {
382        let xored: Vec<u8> = if self.config.xor_key != 0 {
383            data.iter().map(|&b| b ^ self.config.xor_key).collect()
384        } else {
385            data.to_vec()
386        };
387
388        let len = xored.len() as u32;
389        let mut result = Vec::with_capacity(9 + xored.len());
390
391        result.push(((len >> 16) & 0xFF) as u8);
392        result.push(((len >> 8)  & 0xFF) as u8);
393        result.push((len & 0xFF) as u8);
394        result.push(HTTP2_DATA_FRAME_TYPE);
395        result.push(0x00);
396
397        let stream_id = (self.counter % 100 + 1) as u32;
398        result.extend_from_slice(&stream_id.to_be_bytes());
399        result.extend_from_slice(&xored);
400        result
401    }
402
403    fn strip_http2_mimicry(&self, data: &[u8]) -> Result<Vec<u8>, VCLError> {
404        if data.len() < 9 {
405            return Err(VCLError::InvalidPacket(
406                "HTTP/2 mimicry: packet too short".to_string()
407            ));
408        }
409        if data[3] != HTTP2_DATA_FRAME_TYPE {
410            return Err(VCLError::InvalidPacket(
411                "HTTP/2 mimicry: invalid frame type".to_string()
412            ));
413        }
414        let payload_len = ((data[0] as usize) << 16)
415            | ((data[1] as usize) << 8)
416            | (data[2] as usize);
417
418        if data.len() < 9 + payload_len {
419            return Err(VCLError::InvalidPacket(
420                "HTTP/2 mimicry: truncated payload".to_string()
421            ));
422        }
423        let payload = &data[9..9 + payload_len];
424
425        if self.config.xor_key != 0 {
426            Ok(payload.iter().map(|&b| b ^ self.config.xor_key).collect())
427        } else {
428            Ok(payload.to_vec())
429        }
430    }
431
432    // ─── Stats ────────────────────────────────────────────────────────────────
433
434    /// Returns the overhead ratio: overhead_bytes / original_bytes.
435    pub fn overhead_ratio(&self) -> f64 {
436        if self.total_obfuscated == 0 {
437            return 0.0;
438        }
439        self.total_overhead as f64 / self.total_obfuscated as f64
440    }
441
442    /// Returns total bytes of original data obfuscated.
443    pub fn total_obfuscated(&self) -> u64 {
444        self.total_obfuscated
445    }
446
447    /// Returns total overhead bytes added.
448    pub fn total_overhead(&self) -> u64 {
449        self.total_overhead
450    }
451
452    /// Returns a reference to the config.
453    pub fn config(&self) -> &ObfuscationConfig {
454        &self.config
455    }
456
457    /// Returns the current obfuscation mode.
458    pub fn mode(&self) -> &ObfuscationMode {
459        &self.config.mode
460    }
461}
462
463/// Check if raw bytes look like a TLS Application Data record.
464pub fn looks_like_tls(data: &[u8]) -> bool {
465    data.len() >= 5
466        && data[0] == TLS_RECORD_HEADER[0]
467        && data[1] == TLS_RECORD_HEADER[1]
468        && data[2] == TLS_RECORD_HEADER[2]
469}
470
471/// Check if raw bytes look like an HTTP/2 DATA frame.
472pub fn looks_like_http2(data: &[u8]) -> bool {
473    data.len() >= 9
474        && data[3] == HTTP2_DATA_FRAME_TYPE
475        && data[0] != TLS_RECORD_HEADER[0]
476}
477
478/// Returns the recommended [`ObfuscationMode`] for a given network environment.
479pub fn recommended_mode(network_hint: &str) -> ObfuscationMode {
480    match network_hint.to_lowercase().as_str() {
481        "mobile" | "mts" | "beeline" | "megafon" | "tele2" => ObfuscationMode::Full,
482        "corporate" | "office" | "work"                     => ObfuscationMode::Http2Mimicry,
483        "home" | "broadband"                                => ObfuscationMode::TlsMimicry,
484        _                                                   => ObfuscationMode::Padding,
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    fn roundtrip(config: ObfuscationConfig, data: &[u8]) {
493        let mut obf = Obfuscator::new(config);
494        let obfuscated = obf.obfuscate(data);
495        let restored = obf.deobfuscate(&obfuscated).unwrap();
496        assert_eq!(restored, data, "Roundtrip failed");
497    }
498
499    #[test]
500    fn test_none_roundtrip() {
501        roundtrip(ObfuscationConfig::none(), b"hello vcl");
502    }
503
504    #[test]
505    fn test_padding_roundtrip() {
506        roundtrip(ObfuscationConfig::padding(), b"hello vcl padding");
507    }
508
509    #[test]
510    fn test_padding_empty() {
511        roundtrip(ObfuscationConfig::padding(), b"");
512    }
513
514    #[test]
515    fn test_tls_mimicry_roundtrip() {
516        roundtrip(ObfuscationConfig::tls_mimicry(), b"secret vpn packet");
517    }
518
519    #[test]
520    fn test_tls_mimicry_empty() {
521        roundtrip(ObfuscationConfig::tls_mimicry(), b"");
522    }
523
524    #[test]
525    fn test_tls_mimicry_large() {
526        let data = vec![0xAB_u8; 4096];
527        roundtrip(ObfuscationConfig::tls_mimicry(), &data);
528    }
529
530    #[test]
531    fn test_http2_mimicry_roundtrip() {
532        roundtrip(ObfuscationConfig::http2_mimicry(), b"http2 framed data");
533    }
534
535    #[test]
536    fn test_http2_mimicry_large() {
537        let data = vec![0xFF_u8; 2048];
538        roundtrip(ObfuscationConfig::http2_mimicry(), &data);
539    }
540
541    #[test]
542    fn test_size_normalization_roundtrip() {
543        roundtrip(ObfuscationConfig::size_normalization(), b"normalize me");
544    }
545
546    #[test]
547    fn test_full_roundtrip() {
548        roundtrip(ObfuscationConfig::full(), b"maximum stealth mode");
549    }
550
551    #[test]
552    fn test_full_large() {
553        let data = vec![0x42_u8; 1000];
554        roundtrip(ObfuscationConfig::full(), &data);
555    }
556
557    #[test]
558    fn test_tls_mimicry_looks_like_tls() {
559        let mut obf = Obfuscator::new(ObfuscationConfig::tls_mimicry());
560        let obfuscated = obf.obfuscate(b"data");
561        assert!(looks_like_tls(&obfuscated));
562        assert!(!looks_like_http2(&obfuscated));
563    }
564
565    #[test]
566    fn test_http2_mimicry_looks_like_http2() {
567        let mut obf = Obfuscator::new(ObfuscationConfig::http2_mimicry());
568        let obfuscated = obf.obfuscate(b"data");
569        assert!(looks_like_http2(&obfuscated));
570        assert!(!looks_like_tls(&obfuscated));
571    }
572
573    #[test]
574    fn test_tls_invalid_header() {
575        let mut obf = Obfuscator::new(ObfuscationConfig::tls_mimicry());
576        let bad = vec![0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04];
577        assert!(obf.deobfuscate(&bad).is_err());
578    }
579
580    #[test]
581    fn test_http2_invalid_type() {
582        let mut obf = Obfuscator::new(ObfuscationConfig::http2_mimicry());
583        let mut bad = vec![0u8; 12];
584        bad[3] = 0xFF;
585        assert!(obf.deobfuscate(&bad).is_err());
586    }
587
588    #[test]
589    fn test_deobfuscate_empty() {
590        let mut obf = Obfuscator::new(ObfuscationConfig::tls_mimicry());
591        assert!(obf.deobfuscate(&[]).is_err());
592    }
593
594    #[test]
595    fn test_jitter_zero_when_disabled() {
596        let obf = Obfuscator::new(ObfuscationConfig::none());
597        assert_eq!(obf.jitter_ms(), 0);
598    }
599
600    #[test]
601    fn test_jitter_within_range() {
602        let obf = Obfuscator::new(ObfuscationConfig::tls_mimicry());
603        assert!(obf.jitter_ms() <= obf.config().jitter_max_ms);
604    }
605
606    #[test]
607    fn test_overhead_ratio() {
608        let mut obf = Obfuscator::new(ObfuscationConfig::tls_mimicry());
609        obf.obfuscate(b"data");
610        assert!(obf.overhead_ratio() > 0.0);
611    }
612
613    #[test]
614    fn test_overhead_ratio_none_mode() {
615        let mut obf = Obfuscator::new(ObfuscationConfig::none());
616        obf.obfuscate(b"data");
617        assert_eq!(obf.overhead_ratio(), 0.0);
618    }
619
620    #[test]
621    fn test_recommended_mode_mobile() {
622        assert_eq!(recommended_mode("mobile"), ObfuscationMode::Full);
623        assert_eq!(recommended_mode("mts"),    ObfuscationMode::Full);
624        assert_eq!(recommended_mode("MTS"),    ObfuscationMode::Full);
625    }
626
627    #[test]
628    fn test_recommended_mode_corporate() {
629        assert_eq!(recommended_mode("corporate"), ObfuscationMode::Http2Mimicry);
630        assert_eq!(recommended_mode("office"),    ObfuscationMode::Http2Mimicry);
631    }
632
633    #[test]
634    fn test_recommended_mode_home() {
635        assert_eq!(recommended_mode("home"), ObfuscationMode::TlsMimicry);
636    }
637
638    #[test]
639    fn test_recommended_mode_unknown() {
640        assert_eq!(recommended_mode("unknown"), ObfuscationMode::Padding);
641    }
642
643    #[test]
644    fn test_xor_key_zero_no_scramble() {
645        let config = ObfuscationConfig {
646            xor_key: 0,
647            ..ObfuscationConfig::padding()
648        };
649        roundtrip(config, b"no xor test");
650    }
651
652    #[test]
653    fn test_size_normalization_output_size() {
654        let mut obf = Obfuscator::new(ObfuscationConfig::size_normalization());
655        let data = b"tiny";
656        let out = obf.obfuscate(data);
657        assert!(COMMON_SIZES.iter().any(|&s| s <= out.len()) || out.len() >= data.len());
658    }
659
660    #[test]
661    fn test_multiple_packets_different_jitter() {
662        let mut obf = Obfuscator::new(ObfuscationConfig::full());
663        obf.obfuscate(b"packet1");
664        let j1 = obf.jitter_ms();
665        obf.obfuscate(b"packet2");
666        let j2 = obf.jitter_ms();
667        assert!(j1 <= obf.config().jitter_max_ms);
668        assert!(j2 <= obf.config().jitter_max_ms);
669    }
670
671    #[test]
672    fn test_stats_tracking() {
673        let mut obf = Obfuscator::new(ObfuscationConfig::tls_mimicry());
674        obf.obfuscate(b"hello");
675        obf.obfuscate(b"world");
676        assert_eq!(obf.total_obfuscated(), 10);
677        assert!(obf.total_overhead() > 0);
678    }
679}