1use crate::error::VCLError;
31use tracing::trace;
32
33const TLS_RECORD_HEADER: [u8; 3] = [0x17, 0x03, 0x03];
36
37const HTTP2_DATA_FRAME_TYPE: u8 = 0x00;
39
40const COMMON_SIZES: &[usize] = &[64, 128, 256, 512, 1024, 1280, 1400, 1460];
42
43#[derive(Debug, Clone, PartialEq)]
45pub enum ObfuscationMode {
46 None,
48 Padding,
50 SizeNormalization,
52 TlsMimicry,
54 Http2Mimicry,
56 Full,
58}
59
60#[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 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 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 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 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 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 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
144pub 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 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 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 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 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 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 fn apply_size_normalization(&self, data: &[u8]) -> Vec<u8> {
285 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 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 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 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 pub fn total_obfuscated(&self) -> u64 {
444 self.total_obfuscated
445 }
446
447 pub fn total_overhead(&self) -> u64 {
449 self.total_overhead
450 }
451
452 pub fn config(&self) -> &ObfuscationConfig {
454 &self.config
455 }
456
457 pub fn mode(&self) -> &ObfuscationMode {
459 &self.config.mode
460 }
461}
462
463pub 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
471pub 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
478pub 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}