TLS Record (5 bytes)└─ Handshake Protocol ├─ Type (1 byte): 0x01 = ClientHello ├─ Length (3 bytes) ├─ Version (2 bytes) ├─ Random (32 bytes) ├─ Session ID (variable) ├─ Cipher Suites (variable) ├─ Compression Methods (variable) └─ Extensions (variable) └─ Server Name (type 0x0000) ├─ Extension length ├─ Server Name list length ├─ Name type: 0x00 = hostname ├─ Name length └─ Name data ← Target for fragmentation
We must parse the entire structure to find the SNI:
// From engine/src/tls.rs:74-196 (abbreviated)pub fn parse_client_hello(data: &[u8]) -> Option<ClientHelloInfo> { let mut info = ClientHelloInfo::default(); let mut pos = 0; // Parse TLS record header if data[0] != TLS_HANDSHAKE { return None; } pos += 1; info.record_version = (data[pos], data[pos + 1]); pos += 2; let record_length = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize; pos += 2; // Parse handshake header if data[pos] != HANDSHAKE_CLIENT_HELLO { return None; } pos += 1; let _handshake_length = u32::from_be_bytes( [0, data[pos], data[pos + 1], data[pos + 2]] ) as usize; pos += 3; // Parse ClientHello fields info.client_version = (data[pos], data[pos + 1]); pos += 2; pos += 32; // Random let session_id_len = data[pos] as usize; pos += 1 + session_id_len; let cipher_suites_len = u16::from_be_bytes( [data[pos], data[pos + 1]] ) as usize; pos += 2 + cipher_suites_len; let compression_len = data[pos] as usize; pos += 1 + compression_len; let extensions_len = u16::from_be_bytes( [data[pos], data[pos + 1]] ) as usize; pos += 2; // Parse extensions to find SNI let extensions_end = pos + extensions_len; while pos + 4 <= data.len() && pos < extensions_end { let ext_type = u16::from_be_bytes([data[pos], data[pos + 1]]); pos += 2; let ext_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize; pos += 2; if ext_type == EXT_SERVER_NAME { // 0x0000 let _sni_list_len = u16::from_be_bytes( [data[pos], data[pos + 1]] ) as usize; let name_type = data[pos + 2]; let name_len = u16::from_be_bytes( [data[pos + 3], data[pos + 4]] ) as usize; if name_type == SNI_HOST_NAME { // 0x00 let name_offset = pos + 5; info.sni_offset = Some(name_offset); info.sni_length = Some(name_len); if name_offset + name_len <= data.len() { if let Ok(hostname) = std::str::from_utf8( &data[name_offset..name_offset + name_len] ) { info.sni_hostname = Some(hostname.to_string()); } } } break; } pos += ext_len; } Some(info)}
Works against: Simple stateless DPI Used by: Turk Telekom, Superonline presetsFragments are sent immediately. Effective when DPI doesn’t attempt reassembly.
Works against: DPI with <1ms buffer timeout Used by: Vodafone TR presetImperceptible latency to users, but exceeds typical DPI buffer windows.
Works against: DPI with moderate buffering Used by: Aggressive presetNoticeable but acceptable latency. Defeats most buffering-capable DPI systems.
Delays only apply to the initial handshake packets. Once the TLS connection is established, data packets flow normally without artificial delays.
The codebase includes infrastructure for sending decoy packets:
// From engine/src/bypass.rs:268-288fn generate_fake_tls_packet(&self, original: &[u8]) -> Bytes { let mut fake = BytesMut::with_capacity(original.len()); fake.extend_from_slice(original); // Corrupt the SNI field if let Some(info) = parse_client_hello(original) { if let (Some(offset), Some(len)) = (info.sni_offset, info.sni_length) { if offset + len <= fake.len() { for i in 0..len { fake[offset + i] = b'x'; // "xxxxxxxx.xxx" } } } } fake.freeze()}
DPI boxes must inspect millions of packets per second. They have nanosecond-scale budgets per packet.Stateful reassembly is expensive:
Allocate memory per flow
Track sequence numbers
Buffer out-of-order packets
Implement timeouts
Most inline DPI systems avoid this complexity.
Memory Limitations
Buffering requires RAM. At 1 million concurrent connections:
1KB per connection = 1GB RAM
10KB per connection = 10GB RAM
DPI boxes can’t buffer significant amounts per flow.
False Positive Avoidance
Censorship systems must minimize false positives (blocking legitimate traffic).Conservative approach: If a packet can’t be conclusively identified, let it through.Fragmentation creates ambiguity that triggers this conservatism.
TLS libraries like OpenSSL parse ClientHello incrementally and handle split records transparently.HTTP parsers are line-buffered and don’t care about packet boundaries.