TurkeyDPI implements packet fragmentation through a transform pipeline that operates on outgoing network data. The system is designed to be modular, configurable, and verifiable.
Fragmentation behavior is controlled by FragmentParams:
// Defined in engine/src/config.rs (referenced in fragment.rs)pub struct FragmentParams { pub min_size: usize, // Minimum fragment size pub max_size: usize, // Maximum fragment size pub split_at_offset: Option<usize>, // Specific split point pub randomize: bool, // Randomize fragment sizes}
The split_at_offset parameter allows precise control over where data is split, enabling targeted fragmentation of protocol fields like SNI or Host headers.
Primary split at split_pos (SNI location or header boundary)
Secondary segmentation if max_segment_size is small
Example with split_pos=50, max_segment_size=10:
Original: [100 bytes]Step 1: Split at 50 Part A: [0..50] (50 bytes) Part B: [50..100] (50 bytes)Step 2: Segment Part A Fragment 1: [0..10] (10 bytes) Fragment 2: [10..20] (10 bytes) Fragment 3: [20..30] (10 bytes) Fragment 4: [30..40] (10 bytes) Fragment 5: [40..50] (10 bytes)Step 3: Keep Part B whole Fragment 6: [50..100] (50 bytes)Final: 6 fragments
This creates asymmetric fragmentation: heavy splitting before the SNI, minimal splitting after.
Why asymmetric? The critical data (SNI hostname) is in the first part. Once that’s fragmented, the rest can be sent efficiently.
The proxy layer (not shown in these files) respects inter_fragment_delay when sending fragments:
// Pseudocode from proxy layerfor fragment in result.fragments { send(fragment).await; if let Some(delay) = result.inter_fragment_delay { tokio::time::sleep(delay).await; // Delay before next fragment }}
Every fragmentation operation is verified to be lossless:
// From engine/src/transform/fragment.rs:213-238#[test]fn test_fragment_preserves_all_data() { let params = FragmentParams { min_size: 3, max_size: 7, split_at_offset: None, randomize: false, }; let transform = FragmentTransform::new(¶ms); let key = test_flow_key(); let mut state = FlowState::new(key); let mut ctx = test_context(&key, &mut state); let original = b"The quick brown fox jumps over the lazy dog"; let mut data = BytesMut::from(&original[..]); let _ = transform.apply(&mut ctx, &mut data); // Collect all fragments let mut all_data = data.to_vec(); for packet in &ctx.output_packets { all_data.extend_from_slice(packet); } // Verify byte-for-byte identity assert_eq!(all_data.as_slice(), original);}
Critical invariant: concat(fragments) == original_dataIf this fails, TLS handshakes will be rejected by servers.
The TLS parser handles truncated/malformed data gracefully:
// From engine/src/tls.rs:77-86if data.len() < 6 { return None; // Too short to be valid}let content_type = data[pos];if content_type != TLS_HANDSHAKE { return None; // Not a handshake}// ... bounds checking throughout ...if pos + 2 > data.len() { return Some(info); // Return partial info instead of crashing}
Returns None or partial ClientHelloInfo instead of panicking on bad input.
for fragment in fragments { socket.send(fragment).await; tokio::time::sleep(Duration::from_micros(10000)).await; // 10ms delay}Total delay: 4 * 10ms = 40ms added to handshake