use bytes::{Bytes, BytesMut};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::Duration;
use super::record::{frame_message, RecordDeframer, CT_HANDSHAKE};
use super::server_hello::parse_client_hello;
use super::{client_hello, theater};
use crate::crypto::rng::OsRng;
use crate::errors::CoreError;
use crate::transport::session_transport::{FramePhase, SessionTransport};
const SEND_CAP: usize = 4 * 1024 * 1024;
const READ_SCRATCH: usize = 16 * 1024;
const PRELUDE_DEADLINE: Duration = Duration::from_secs(10);
const PROFILE_STALE_MONTHS: u32 = 6;
fn warn_if_profile_stale() {
let now = time::OffsetDateTime::now_utc();
let now_yyyymm = (now.year() as u32) * 100 + u8::from(now.month()) as u32;
if !client_hello::profile_is_fresh(now_yyyymm, PROFILE_STALE_MONTHS) {
log::warn!(
"MimicTlsLeg: the Chrome ClientHello profile (captured {}) is stale; refresh it \
from a live browser capture — a drifted JA3/JA4 is itself a fingerprint",
client_hello::PROFILE_CAPTURED
);
}
}
#[derive(Clone, Debug)]
pub struct MimicConfig {
pub sni: String,
}
impl MimicConfig {
pub fn new(sni: impl Into<String>) -> Self {
Self { sni: sni.into() }
}
}
struct ReadState {
reader: OwnedReadHalf,
deframer: RecordDeframer,
scratch: Vec<u8>,
}
pub struct MimicTlsLeg {
write_half: Mutex<OwnedWriteHalf>,
read: Mutex<ReadState>,
phase: AtomicU8,
}
fn net_err(e: std::io::Error) -> CoreError {
CoreError::NetworkError(e.to_string())
}
async fn read_one_record(
reader: &mut OwnedReadHalf,
buf: &mut BytesMut,
expected_type: u8,
) -> Result<Bytes, CoreError> {
let mut scratch = [0u8; 4096];
loop {
if let Some(frag) = theater::take_record(buf, expected_type)? {
return Ok(frag);
}
let n = reader.read(&mut scratch).await.map_err(net_err)?;
if n == 0 {
return Err(CoreError::ConnectionClosed);
}
buf.extend_from_slice(&scratch[..n]);
}
}
impl MimicTlsLeg {
fn from_halves(
reader: OwnedReadHalf,
writer: OwnedWriteHalf,
deframer: RecordDeframer,
) -> Self {
Self {
write_half: Mutex::new(writer),
read: Mutex::new(ReadState {
reader,
deframer,
scratch: vec![0u8; READ_SCRATCH],
}),
phase: AtomicU8::new(0),
}
}
pub async fn connect(stream: TcpStream, config: &MimicConfig) -> Result<Self, CoreError> {
warn_if_profile_stale();
let _ = stream.set_nodelay(true);
let (mut reader, mut writer) = stream.into_split();
let rng = OsRng;
let fut = async {
let ch = theater::client_hello_record(&config.sni, &rng)?;
writer.write_all(&ch).await.map_err(net_err)?;
writer.flush().await.map_err(net_err)?;
let mut buf = BytesMut::new();
for &t in &theater::SERVER_FLIGHT_TYPES {
read_one_record(&mut reader, &mut buf, t).await?;
}
let fin = theater::client_finished_flight(&rng)?;
writer.write_all(&fin).await.map_err(net_err)?;
writer.flush().await.map_err(net_err)?;
Ok::<BytesMut, CoreError>(buf)
};
let leftover = tokio::time::timeout(PRELUDE_DEADLINE, fut)
.await
.map_err(|_| CoreError::Timeout)??;
let mut deframer = RecordDeframer::new();
deframer.push(&leftover);
Ok(Self::from_halves(reader, writer, deframer))
}
pub async fn accept(stream: TcpStream, config: &MimicConfig) -> Result<Self, CoreError> {
let started = std::time::Instant::now();
let _ = stream.set_nodelay(true);
let _ = config; let (mut reader, mut writer) = stream.into_split();
let rng = OsRng;
let fut = async {
let mut buf = BytesMut::new();
let ch = read_one_record(&mut reader, &mut buf, CT_HANDSHAKE).await?;
let parsed = parse_client_hello(&ch)?;
let flight = theater::server_flight(&parsed, &rng)?;
writer.write_all(&flight).await.map_err(net_err)?;
writer.flush().await.map_err(net_err)?;
for &t in &theater::CLIENT_FINISHED_TYPES {
read_one_record(&mut reader, &mut buf, t).await?;
}
Ok::<BytesMut, CoreError>(buf)
};
match tokio::time::timeout(PRELUDE_DEADLINE, fut).await {
Ok(Ok(leftover)) => {
let mut deframer = RecordDeframer::new();
deframer.push(&leftover);
Ok(Self::from_halves(reader, writer, deframer))
}
Ok(Err(_)) | Err(_) => {
let remaining = PRELUDE_DEADLINE.saturating_sub(started.elapsed());
black_hole(reader, remaining).await;
Err(CoreError::ConnectionClosed)
}
}
}
fn current_phase(&self) -> FramePhase {
match self.phase.load(Ordering::Relaxed) {
0 => FramePhase::Handshake,
_ => FramePhase::Established,
}
}
}
async fn black_hole(mut reader: OwnedReadHalf, remaining: Duration) {
let mut scratch = [0u8; 4096];
let drain = async {
loop {
match reader.read(&mut scratch).await {
Ok(0) | Err(_) => break,
Ok(_) => continue,
}
}
};
let _ = tokio::time::timeout(remaining, drain).await;
}
impl SessionTransport for MimicTlsLeg {
async fn send_bytes(&self, data: &[u8]) -> Result<(), CoreError> {
if data.len() > SEND_CAP {
return Err(CoreError::NetworkError(format!(
"frame too large: {} > {}",
data.len(),
SEND_CAP
)));
}
let records = frame_message(data)?;
let mut w = self.write_half.lock().await;
w.write_all(&records).await.map_err(net_err)?;
w.flush().await.map_err(net_err)?;
Ok(())
}
async fn recv_bytes(&self) -> Result<Bytes, CoreError> {
let phase = self.current_phase();
let mut guard = self.read.lock().await;
let st = &mut *guard;
st.deframer.set_phase(phase);
loop {
if let Some(msg) = st.deframer.next_message()? {
return Ok(msg);
}
let n = st.reader.read(&mut st.scratch).await.map_err(net_err)?;
if n == 0 {
return Err(CoreError::ConnectionClosed);
}
let (deframer, scratch) = (&mut st.deframer, &st.scratch);
deframer.push(&scratch[..n]);
}
}
fn set_frame_phase(&self, phase: FramePhase) {
let v = match phase {
FramePhase::Handshake => 0,
FramePhase::Established => 1,
};
self.phase.store(v, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::net::TcpListener;
async fn mimic_pair() -> (MimicTlsLeg, MimicTlsLeg) {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("addr");
let server_fut = async {
let (stream, _) = listener.accept().await.expect("accept");
MimicTlsLeg::accept(stream, &MimicConfig::new("server-ignored"))
.await
.expect("server prelude")
};
let client_fut = async {
let stream = TcpStream::connect(addr).await.expect("connect");
MimicTlsLeg::connect(stream, &MimicConfig::new("cover.example.com"))
.await
.expect("client prelude")
};
let (server, client) = tokio::join!(server_fut, client_fut);
(client, server)
}
#[tokio::test]
#[ignore = "real TCP loopback"]
async fn prelude_completes_and_messages_round_trip() {
let (client, server) = mimic_pair().await;
client.set_frame_phase(FramePhase::Established);
server.set_frame_phase(FramePhase::Established);
let (s, r) = tokio::join!(client.send_bytes(b"hello from client"), server.recv_bytes());
s.expect("client send");
assert_eq!(&r.expect("server recv")[..], b"hello from client");
let (s, r) = tokio::join!(server.send_bytes(b"hello from server"), client.recv_bytes());
s.expect("server send");
assert_eq!(&r.expect("client recv")[..], b"hello from server");
let big = vec![0x5au8; 40_000];
let (s, r) = tokio::join!(client.send_bytes(&big), server.recv_bytes());
s.expect("client send big");
assert_eq!(r.expect("server recv big").len(), big.len());
}
#[tokio::test]
#[ignore = "real TCP loopback"]
async fn server_black_holes_a_garbage_prelude() {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("addr");
let server_fut = async {
let (stream, _) = listener.accept().await.expect("accept");
MimicTlsLeg::accept(stream, &MimicConfig::new("x")).await
};
let client_fut = async {
let mut stream = TcpStream::connect(addr).await.expect("connect");
stream
.write_all(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n")
.await
.expect("write garbage");
tokio::time::sleep(Duration::from_millis(50)).await;
};
let (server_res, ()) = tokio::join!(server_fut, client_fut);
assert!(
server_res.is_err(),
"a garbage prelude must be black-holed (accept returns Err)"
);
}
}