use crate::transport::legs::TransportLeg;
use async_trait::async_trait;
use bytes::{Bytes, BytesMut};
use rand::rngs::OsRng;
use ring::aead::{self, LessSafeKey, Nonce, UnboundKey};
use std::io;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
#[repr(u8)]
#[derive(Debug, Clone, Copy)]
enum TlsContentType {
#[allow(dead_code)]
ChangeCipherSpec = 20,
#[allow(dead_code)]
Alert = 21,
Handshake = 22,
ApplicationData = 23,
}
#[derive(Debug, Clone)]
pub struct FakeTlsConfig {
pub sni: String,
pub version: u16,
}
impl Default for FakeTlsConfig {
fn default() -> Self {
Self {
sni: "www.google.com".to_string(),
version: 0x0303, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FakeTlsState {
Init = 0,
ClientHelloDone = 1,
ServerHelloDone = 2,
ApplicationData = 3,
}
pub struct FakeTlsLeg {
config: FakeTlsConfig,
stream: Mutex<Option<TcpStream>>,
remote_addr: Option<SocketAddr>,
rtt_ms: AtomicU32,
available: AtomicBool,
state: parking_lot::RwLock<FakeTlsState>,
read_buf: Mutex<BytesMut>,
send_key: LessSafeKey,
recv_key: LessSafeKey,
nonce_prefix: [u8; 4],
send_counter: AtomicU64,
recv_counter: AtomicU64,
#[allow(dead_code)]
is_server: bool,
}
fn derive_outer_keys(
sni: &str,
version: u16,
is_server: bool,
) -> io::Result<(LessSafeKey, LessSafeKey, [u8; 4])> {
let mut seed = Vec::with_capacity(64);
seed.extend_from_slice(b"phantom-faketls-outer-v1");
seed.extend_from_slice(&version.to_be_bytes());
seed.extend_from_slice(sni.as_bytes());
let key_c2s = crate::crypto::kdf::derive_key_32("phantom-faketls-c2s-v1", &seed);
let key_s2c = crate::crypto::kdf::derive_key_32("phantom-faketls-s2c-v1", &seed);
let pfx = crate::crypto::kdf::derive_key_32("phantom-faketls-pfx-v1", &seed);
let mut nonce_prefix = [0u8; 4];
nonce_prefix.copy_from_slice(&pfx[..4]);
let (send_bytes, recv_bytes) = if is_server {
(key_s2c, key_c2s)
} else {
(key_c2s, key_s2c)
};
let send_unbound = UnboundKey::new(&aead::AES_256_GCM, &send_bytes)
.map_err(|e| io::Error::other(format!("AES key init (send): {}", e)))?;
let recv_unbound = UnboundKey::new(&aead::AES_256_GCM, &recv_bytes)
.map_err(|e| io::Error::other(format!("AES key init (recv): {}", e)))?;
Ok((
LessSafeKey::new(send_unbound),
LessSafeKey::new(recv_unbound),
nonce_prefix,
))
}
impl FakeTlsLeg {
pub fn new() -> io::Result<Self> {
Self::with_config(FakeTlsConfig::default())
}
pub fn with_config(config: FakeTlsConfig) -> io::Result<Self> {
let (send_key, recv_key, nonce_prefix) =
derive_outer_keys(&config.sni, config.version, false)?;
Ok(Self {
config,
stream: Mutex::new(None),
remote_addr: None,
rtt_ms: AtomicU32::new(150),
available: AtomicBool::new(false),
state: parking_lot::RwLock::new(FakeTlsState::Init),
read_buf: Mutex::new(BytesMut::with_capacity(16384)),
send_key,
recv_key,
nonce_prefix,
send_counter: AtomicU64::new(0),
recv_counter: AtomicU64::new(0),
is_server: false,
})
}
pub async fn connect(addr: SocketAddr, config: FakeTlsConfig) -> io::Result<Self> {
let start = std::time::Instant::now();
let stream = TcpStream::connect(addr).await?;
let rtt = start.elapsed().as_millis() as u32;
stream.set_nodelay(true)?;
let (send_key, recv_key, nonce_prefix) =
derive_outer_keys(&config.sni, config.version, false)?;
let leg = Self {
config,
stream: Mutex::new(Some(stream)),
remote_addr: Some(addr),
rtt_ms: AtomicU32::new(rtt),
available: AtomicBool::new(true),
state: parking_lot::RwLock::new(FakeTlsState::Init),
read_buf: Mutex::new(BytesMut::with_capacity(16384)),
send_key,
recv_key,
nonce_prefix,
send_counter: AtomicU64::new(0),
recv_counter: AtomicU64::new(0),
is_server: false,
};
leg.do_client_handshake().await?;
Ok(leg)
}
pub async fn accept(stream: TcpStream, config: FakeTlsConfig) -> io::Result<Self> {
stream.set_nodelay(true)?;
let remote_addr = stream.peer_addr().ok();
let (send_key, recv_key, nonce_prefix) =
derive_outer_keys(&config.sni, config.version, true)?;
let leg = Self {
config,
stream: Mutex::new(Some(stream)),
remote_addr,
rtt_ms: AtomicU32::new(150),
available: AtomicBool::new(true),
state: parking_lot::RwLock::new(FakeTlsState::Init),
read_buf: Mutex::new(BytesMut::with_capacity(16384)),
send_key,
recv_key,
nonce_prefix,
send_counter: AtomicU64::new(0),
recv_counter: AtomicU64::new(0),
is_server: true,
};
leg.do_server_handshake().await?;
Ok(leg)
}
#[inline]
fn make_nonce(&self, counter: u64) -> Nonce {
let mut n = [0u8; 12];
n[..4].copy_from_slice(&self.nonce_prefix);
n[4..12].copy_from_slice(&counter.to_be_bytes());
Nonce::assume_unique_for_key(n)
}
async fn do_client_handshake(&self) -> io::Result<()> {
let mut stream_guard = self.stream.lock().await;
let stream = stream_guard
.as_mut()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "Not connected"))?;
let client_hello = self.build_fake_client_hello();
stream.write_all(&client_hello).await?;
*self.state.write() = FakeTlsState::ClientHelloDone;
let mut buf = [0u8; 4096];
let n = tokio::time::timeout(std::time::Duration::from_secs(5), stream.read(&mut buf))
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "ServerHello timeout"))??;
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"Connection closed by server",
));
}
*self.state.write() = FakeTlsState::ServerHelloDone;
*self.state.write() = FakeTlsState::ApplicationData;
Ok(())
}
async fn do_server_handshake(&self) -> io::Result<()> {
let mut stream_guard = self.stream.lock().await;
let stream = stream_guard
.as_mut()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "Not connected"))?;
let mut buf = [0u8; 4096];
let n = tokio::time::timeout(std::time::Duration::from_secs(5), stream.read(&mut buf))
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "ClientHello timeout"))??;
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"Connection closed by client",
));
}
*self.state.write() = FakeTlsState::ClientHelloDone;
let server_hello = self.build_fake_server_hello();
stream.write_all(&server_hello).await?;
*self.state.write() = FakeTlsState::ServerHelloDone;
*self.state.write() = FakeTlsState::ApplicationData;
Ok(())
}
fn build_fake_server_hello(&self) -> Vec<u8> {
let mut record = Vec::with_capacity(128);
record.push(TlsContentType::Handshake as u8);
record.extend_from_slice(&self.config.version.to_be_bytes());
let mut hs = Vec::new();
hs.push(0x02); hs.extend_from_slice(&[0, 0, 0]);
hs.extend_from_slice(&0x0303u16.to_be_bytes());
let mut random = [0u8; 32];
if getrandom::getrandom(&mut random).is_err() {
use rand::RngCore;
rand::thread_rng().fill_bytes(&mut random);
}
hs.extend_from_slice(&random);
hs.push(0);
hs.extend_from_slice(&0x1302u16.to_be_bytes());
hs.push(0);
hs.extend_from_slice(&[0, 8]); hs.extend_from_slice(&51u16.to_be_bytes());
hs.extend_from_slice(&4u16.to_be_bytes()); hs.extend_from_slice(&0x001du16.to_be_bytes()); hs.extend_from_slice(&0u16.to_be_bytes());
let hs_len = (hs.len() - 4) as u32;
hs[1] = ((hs_len >> 16) & 0xFF) as u8;
hs[2] = ((hs_len >> 8) & 0xFF) as u8;
hs[3] = (hs_len & 0xFF) as u8;
record.extend_from_slice(&(hs.len() as u16).to_be_bytes());
record.extend_from_slice(&hs);
record
}
fn build_fake_client_hello(&self) -> Vec<u8> {
let mut record = Vec::with_capacity(512);
let mut rng = OsRng;
record.push(TlsContentType::Handshake as u8);
record.extend_from_slice(&self.config.version.to_be_bytes());
let length_pos = record.len();
record.extend_from_slice(&[0u8; 2]);
record.push(0x01); let hs_length_pos = record.len();
record.extend_from_slice(&[0u8; 3]);
record.extend_from_slice(&0x0303u16.to_be_bytes());
let mut random = [0u8; 32];
if getrandom::getrandom(&mut random).is_err() {
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut random);
}
record.extend_from_slice(&random);
record.push(32);
let mut session_id = [0u8; 32];
if getrandom::getrandom(&mut session_id).is_err() {
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut session_id);
}
record.extend_from_slice(&session_id);
let grease = (rng.gen::<u8>() & 0xf0) as u16 + 0x0a0a;
let mut suites = vec![
0x1301, 0x1302, 0x1303, 0xc02b, 0xc02c, 0xc02f, 0xc030, 0xcca9, 0xcca8, ];
use rand::seq::SliceRandom;
use rand::Rng;
suites.shuffle(&mut rng);
let num_suites = rng.gen_range(3..=suites.len());
suites.truncate(num_suites);
suites.insert(rng.gen_range(0..=suites.len()), grease);
record.extend_from_slice(&((suites.len() * 2) as u16).to_be_bytes());
for s in suites {
record.extend_from_slice(&s.to_be_bytes());
}
record.push(1);
record.push(0);
let extensions_start = record.len();
record.extend_from_slice(&[0u8; 2]);
let supported_groups = self.make_supported_groups_body(&mut rng);
let key_share = self.make_key_share_body(&mut rng);
let supported_versions = self.make_supported_versions_body(&mut rng);
let mut exts: Vec<(u16, Vec<u8>)> = vec![
(0u16, self.make_sni_extension_body()),
(10u16, supported_groups),
(11u16, vec![1, 0]),
(13u16, self.make_signature_algorithms_body()),
(51u16, key_share),
(43u16, supported_versions),
];
if !exts.is_empty() {
let sni = exts.remove(0); exts.shuffle(&mut rng);
exts.insert(0, sni);
}
exts.push((21u16, vec![0u8; rng.gen_range(50..200)]));
for (etype, body) in exts {
record.extend_from_slice(&etype.to_be_bytes());
record.extend_from_slice(&(body.len() as u16).to_be_bytes());
record.extend_from_slice(&body);
}
let extensions_len = (record.len() - extensions_start - 2) as u16;
record[extensions_start..extensions_start + 2]
.copy_from_slice(&extensions_len.to_be_bytes());
let hs_len = (record.len() - hs_length_pos - 3) as u32;
record[hs_length_pos] = ((hs_len >> 16) & 0xFF) as u8;
record[hs_length_pos + 1] = ((hs_len >> 8) & 0xFF) as u8;
record[hs_length_pos + 2] = (hs_len & 0xFF) as u8;
let record_len = (record.len() - 5) as u16;
record[length_pos..length_pos + 2].copy_from_slice(&record_len.to_be_bytes());
record
}
fn make_sni_extension_body(&self) -> Vec<u8> {
let mut body = Vec::new();
let sni_bytes = self.config.sni.as_bytes();
body.extend_from_slice(&((3 + sni_bytes.len()) as u16).to_be_bytes()); body.push(0); body.extend_from_slice(&(sni_bytes.len() as u16).to_be_bytes());
body.extend_from_slice(sni_bytes);
body
}
fn make_supported_groups_body(&self, rng: &mut impl rand::Rng) -> Vec<u8> {
let mut body = Vec::new();
let grease = (rng.gen::<u8>() & 0xf0) as u16 + 0x0a0a;
let groups = [
grease, 0x001d, 0x0017, ];
let len = (groups.len() * 2) as u16;
body.extend_from_slice(&len.to_be_bytes());
for g in groups {
body.extend_from_slice(&g.to_be_bytes());
}
body
}
fn make_signature_algorithms_body(&self) -> Vec<u8> {
let mut body = Vec::new();
let algos: [u16; 8] = [
0x0403, 0x0804, 0x0401, 0x0503, 0x0805, 0x0501, 0x0806, 0x0601, ];
let len = (algos.len() * 2) as u16;
body.extend_from_slice(&len.to_be_bytes());
for a in algos {
body.extend_from_slice(&a.to_be_bytes());
}
body
}
fn make_key_share_body(&self, rng: &mut (impl rand::Rng + rand::CryptoRng)) -> Vec<u8> {
let mut body = Vec::new();
let grease = (rng.gen::<u8>() & 0xf0) as u16 + 0x0a0a;
let secret = StaticSecret::random_from_rng(rng);
let public = X25519PublicKey::from(&secret);
let x25519_share = public.as_bytes();
let client_shares_len = 4 + (2 + 32); body.extend_from_slice(&(client_shares_len as u16).to_be_bytes());
body.extend_from_slice(&grease.to_be_bytes());
body.extend_from_slice(&1u16.to_be_bytes()); body.push(0);
body.extend_from_slice(&0x001du16.to_be_bytes());
body.extend_from_slice(&(x25519_share.len() as u16).to_be_bytes());
body.extend_from_slice(x25519_share);
body
}
fn make_supported_versions_body(&self, rng: &mut impl rand::Rng) -> Vec<u8> {
let mut body = Vec::new();
let grease = (rng.gen::<u8>() & 0xf0) as u16 + 0x0a0a;
let versions = [
grease, 0x0304, 0x0303, ];
let len = (versions.len() * 2) as u16;
body.push((len & 0xFF) as u8); for v in versions {
body.extend_from_slice(&v.to_be_bytes());
}
body
}
fn wrap_as_tls_record(&self, data: &[u8]) -> io::Result<Vec<u8>> {
let tag_len = self.send_key.algorithm().tag_len();
let framed_len = data.len().saturating_add(1).saturating_add(tag_len);
if framed_len > u16::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"faketls record body would be {framed_len} bytes, exceeding the {}-byte u16 length field",
u16::MAX
),
));
}
let mut inner_plaintext = Vec::with_capacity(data.len() + 1);
inner_plaintext.extend_from_slice(data);
inner_plaintext.push(TlsContentType::ApplicationData as u8);
let mut in_out = inner_plaintext;
let counter = self.send_counter.fetch_add(1, Ordering::Relaxed);
let nonce = self.make_nonce(counter);
let aad = aead::Aad::empty();
self.send_key
.seal_in_place_append_tag(nonce, aad, &mut in_out)
.map_err(|_| io::Error::other("faketls AEAD seal failed"))?;
let mut record = Vec::with_capacity(5 + in_out.len());
record.push(TlsContentType::ApplicationData as u8);
record.extend_from_slice(&self.config.version.to_be_bytes());
record.extend_from_slice(&(in_out.len() as u16).to_be_bytes());
record.extend_from_slice(&in_out);
Ok(record)
}
fn unwrap_tls_record<'a>(&self, record: &'a mut [u8]) -> io::Result<&'a [u8]> {
if record.len() < 5 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Record too short",
));
}
let payload = &mut record[5..];
let counter = self.recv_counter.fetch_add(1, Ordering::Relaxed);
let nonce = self.make_nonce(counter);
let aad = aead::Aad::empty();
let decrypted = self
.recv_key
.open_in_place(nonce, aad, payload)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "AEAD decryption failed"))?;
if decrypted.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Empty inner plaintext",
));
}
let inner_len = decrypted.len() - 1;
Ok(&decrypted[..inner_len])
}
}
impl Default for FakeTlsLeg {
fn default() -> Self {
#[allow(clippy::expect_used)]
Self::new().expect("FakeTlsLeg::default: AES key init invariant violated")
}
}
#[async_trait]
impl TransportLeg for FakeTlsLeg {
async fn send(&self, data: Bytes) -> io::Result<()> {
if !self.is_available() {
return Err(io::Error::new(
io::ErrorKind::NotConnected,
"FakeTLS not connected",
));
}
if *self.state.read() != FakeTlsState::ApplicationData {
return Err(io::Error::other("Handshake not finished"));
}
let record = self.wrap_as_tls_record(&data)?;
let mut stream_guard = self.stream.lock().await;
let stream = stream_guard
.as_mut()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "Not connected"))?;
stream.write_all(&record).await?;
stream.flush().await
}
async fn recv(&self) -> io::Result<Bytes> {
if !self.is_available() {
return Err(io::Error::new(
io::ErrorKind::NotConnected,
"FakeTLS not connected",
));
}
if *self.state.read() != FakeTlsState::ApplicationData {
return Err(io::Error::other("Handshake not finished"));
}
let mut stream_guard = self.stream.lock().await;
let stream = stream_guard
.as_mut()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "Not connected"))?;
let mut read_buf = self.read_buf.lock().await;
while read_buf.len() < 5 {
let mut temp = [0u8; 4096];
let n = stream.read(&mut temp).await?;
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Connection closed",
));
}
read_buf.extend_from_slice(&temp[..n]);
}
let record_len = u16::from_be_bytes([read_buf[3], read_buf[4]]) as usize;
while read_buf.len() < 5 + record_len {
let mut temp = [0u8; 4096];
let n = stream.read(&mut temp).await?;
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Connection closed",
));
}
read_buf.extend_from_slice(&temp[..n]);
}
let mut record = read_buf.split_to(5 + record_len);
let payload_len = self.unwrap_tls_record(&mut record)?.len();
let mut out = BytesMut::with_capacity(payload_len);
out.extend_from_slice(&record[5..5 + payload_len]);
Ok(out.freeze())
}
fn is_available(&self) -> bool {
self.available.load(Ordering::Relaxed)
&& *self.state.read() == FakeTlsState::ApplicationData
}
fn rtt_ms(&self) -> u32 {
self.rtt_ms.load(Ordering::Relaxed)
}
fn loss_percent(&self) -> u8 {
0 }
fn remote_addr(&self) -> Option<SocketAddr> {
self.remote_addr
}
async fn close(&self) -> io::Result<()> {
self.available.store(false, Ordering::Relaxed);
if let Some(stream) = self.stream.lock().await.take() {
drop(stream);
}
log::info!("FakeTLS closed");
Ok(())
}
}
impl std::fmt::Debug for FakeTlsLeg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FakeTlsLeg")
.field("sni", &self.config.sni)
.field("remote", &self.remote_addr)
.field("rtt_ms", &self.rtt_ms.load(Ordering::Relaxed))
.field("available", &self.is_available())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_faketls_leg_creation() {
let leg = FakeTlsLeg::new().expect("FakeTlsLeg::new");
assert!(!leg.is_available());
assert_eq!(leg.config.sni, "www.google.com");
}
#[test]
fn test_fake_client_hello() {
let leg = FakeTlsLeg::new().expect("FakeTlsLeg::new");
let hello = leg.build_fake_client_hello();
assert_eq!(hello[0], TlsContentType::Handshake as u8);
assert!(hello.len() > 100); }
#[test]
fn test_wrap_tls_record() {
let leg = FakeTlsLeg::new().expect("FakeTlsLeg::new");
let data = b"test payload";
let record = leg.wrap_as_tls_record(data).expect("wrap_as_tls_record");
assert_eq!(record[0], TlsContentType::ApplicationData as u8);
assert_eq!(record.len(), 5 + data.len() + 1 + 16);
}
#[test]
fn oversized_record_payload_is_rejected_not_truncated() {
let leg = FakeTlsLeg::new().expect("FakeTlsLeg::new");
let ok = leg
.wrap_as_tls_record(&vec![0u8; 65_518])
.expect("boundary payload fits");
assert_eq!(ok.len(), 5 + u16::MAX as usize);
let err = leg
.wrap_as_tls_record(&vec![0u8; 65_519])
.expect_err("oversized payload must be rejected, not truncated");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
fn test_ja3_randomization() {
let leg = FakeTlsLeg::new().expect("FakeTlsLeg::new");
let hello1_ = leg.build_fake_client_hello();
let hello2 = leg.build_fake_client_hello();
assert_ne!(hello1_, hello2);
assert_eq!(hello1_[9], 0x03);
assert_eq!(hello1_[10], 0x03);
}
#[test]
fn test_wrap_unwrap_roundtrip_across_peers() {
let client =
FakeTlsLeg::with_config(FakeTlsConfig::default()).expect("FakeTlsLeg::with_config");
let cfg = FakeTlsConfig::default();
let (send_key, recv_key, nonce_prefix) =
derive_outer_keys(&cfg.sni, cfg.version, true).expect("derive_outer_keys");
let server = FakeTlsLeg {
config: cfg,
stream: Mutex::new(None),
remote_addr: None,
rtt_ms: AtomicU32::new(0),
available: AtomicBool::new(false),
state: parking_lot::RwLock::new(FakeTlsState::ApplicationData),
read_buf: Mutex::new(BytesMut::new()),
send_key,
recv_key,
nonce_prefix,
send_counter: AtomicU64::new(0),
recv_counter: AtomicU64::new(0),
is_server: true,
};
let plaintexts: &[&[u8]] = &[
b"first record",
b"second record",
b"third record (a bit longer to vary length)",
];
for pt in plaintexts {
let mut record = client.wrap_as_tls_record(pt).expect("wrap_as_tls_record");
let recovered_len = server.unwrap_tls_record(&mut record).unwrap().len();
assert_eq!(&record[5..5 + recovered_len], *pt);
}
}
#[test]
fn test_nonce_advances_per_record() {
let leg =
FakeTlsLeg::with_config(FakeTlsConfig::default()).expect("FakeTlsLeg::with_config");
let r1 = leg.wrap_as_tls_record(b"identical").expect("wrap r1");
let r2 = leg.wrap_as_tls_record(b"identical").expect("wrap r2");
assert_ne!(
r1, r2,
"counter must advance — identical plaintext must not produce identical ciphertext"
);
}
#[test]
fn test_client_hello_structure() {
let leg = FakeTlsLeg::new().expect("FakeTlsLeg::new");
let hello = leg.build_fake_client_hello();
assert_eq!(
hello[0],
TlsContentType::Handshake as u8,
"First byte should be Handshake (22)"
);
let record_version = u16::from_be_bytes([hello[1], hello[2]]);
assert_eq!(
record_version, 0x0303,
"Record version should be TLS 1.2 (0x0303)"
);
let record_len = u16::from_be_bytes([hello[3], hello[4]]) as usize;
assert_eq!(
hello.len(),
5 + record_len,
"Record length should match payload"
);
assert_eq!(hello[5], 0x01, "Handshake type should be ClientHello");
assert_eq!(hello[9], 0x03, "Legacy version major");
assert_eq!(hello[10], 0x03, "Legacy version minor");
let random = &hello[11..43];
assert!(
!random.iter().all(|&b| b == 0),
"Random should not be all zeros"
);
assert_eq!(hello[43], 32, "Session ID length should be 32");
}
}