use crate::crypto::hybrid_sign::HybridVerifyingKey;
use crate::errors::CoreError;
use crate::observability::attrs::{AeadAlgorithm, ReplayReason};
use crate::observability::{Observability, ObservabilityConfig};
use crate::runtime::{Runtime, TokioRuntime};
use crate::transport::handshake::{HandshakeClient, ServerReject, ServerReply, EARLY_DATA_MAX_LEN};
use crate::transport::multiplexer::StreamDemultiplexer;
use crate::transport::packet_coalescer_codec::unwrap_coalesced_packet;
use crate::transport::path_validation_codec::build_path_validation_packet;
use crate::transport::session::{Session, SessionState};
use crate::transport::shaping::{self, PaddingPolicy};
use crate::transport::stream::Stream;
use crate::transport::types::{
LegType, PacketFlags, PacketHeader, PhantomPacket, SessionId, StreamId as TransportStreamId,
WIRE_VERSION,
};
use bytes::Bytes;
use dashmap::DashMap;
use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot, Mutex};
fn new_session_id() -> String {
let bytes: [u8; 16] = rand::random();
format!("phantom-{}", hex::encode(bytes))
}
#[cfg_attr(feature = "bindings", derive(uniffi::Enum))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
#[non_exhaustive]
pub enum ConnectionState {
Connecting = 0,
ClassicalReady = 1,
PqcUpgrading = 2,
PqcReady = 3,
Connected = 4,
Failed = 5,
Closed = 6,
Migrating = 7,
Dead = 8,
}
#[cfg_attr(feature = "bindings", derive(uniffi::Record))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TrafficShapingConfig {
pub padding: PaddingPolicy,
pub jitter_ms: u32,
pub cover_interval_ms: u32,
}
fn apply_shaping(session: &Session, cfg: TrafficShapingConfig) {
session.set_padding_policy(cfg.padding);
session.set_jitter_ms(cfg.jitter_ms);
session.set_cover_interval_ms(cfg.cover_interval_ms);
}
impl ConnectionState {
fn from_u8(v: u8) -> Self {
match v {
0 => Self::Connecting,
1 => Self::ClassicalReady,
2 => Self::PqcUpgrading,
3 => Self::PqcReady,
4 => Self::Connected,
5 => Self::Failed,
6 => Self::Closed,
7 => Self::Migrating,
8 => Self::Dead,
_ => Self::Failed,
}
}
pub fn is_data_ready(&self) -> bool {
matches!(
self,
Self::ClassicalReady
| Self::PqcUpgrading
| Self::PqcReady
| Self::Connected
| Self::Migrating
)
}
}
#[cfg_attr(feature = "bindings", derive(uniffi::Record))]
#[derive(Clone)]
#[non_exhaustive]
pub struct ResumptionHint {
pub session_id: Vec<u8>,
pub resumption_secret: Vec<u8>,
}
impl std::fmt::Debug for ResumptionHint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResumptionHint")
.field(
"session_id",
&format_args!("<{} bytes>", self.session_id.len()),
)
.field("resumption_secret", &"REDACTED")
.finish()
}
}
pub use crate::transport::session_transport::{FramePhase, SessionTransport};
struct ObservedTransport<T> {
inner: T,
observability: Arc<Observability>,
leg: LegType,
}
impl<T> ObservedTransport<T> {
fn new(inner: T, observability: Arc<Observability>, leg: LegType) -> Self {
Self {
inner,
observability,
leg,
}
}
}
impl<T: SessionTransport> SessionTransport for ObservedTransport<T> {
async fn send_bytes(&self, data: &[u8]) -> Result<(), CoreError> {
let result = self.inner.send_bytes(data).await;
if result.is_ok() {
self.observability.record_send(data.len(), self.leg);
}
result
}
async fn recv_bytes(&self) -> Result<Bytes, CoreError> {
let result = self.inner.recv_bytes().await;
if let Ok(ref bytes) = result {
self.observability.record_recv(bytes.len(), self.leg);
}
result
}
fn set_frame_phase(&self, phase: FramePhase) {
self.inner.set_frame_phase(phase);
}
fn set_outbound_cid(&self, cid: [u8; 8]) {
self.inner.set_outbound_cid(cid);
}
fn has_migration_candidate(&self) -> bool {
self.inner.has_migration_candidate()
}
fn send_to_candidate(
&self,
data: &[u8],
) -> impl core::future::Future<Output = Result<bool, CoreError>> + Send {
self.inner.send_to_candidate(data)
}
fn confirm_authenticated_source(&self) {
self.inner.confirm_authenticated_source();
}
fn promote_candidate(&self) -> bool {
self.inner.promote_candidate()
}
fn migrate(
&self,
local_addr: String,
) -> impl core::future::Future<Output = Result<(), CoreError>> + Send {
self.inner.migrate(local_addr)
}
fn migrate_server(
&self,
local_addr: String,
) -> impl core::future::Future<Output = Result<(), CoreError>> + Send {
self.inner.migrate_server(local_addr)
}
}
#[cfg_attr(feature = "bindings", derive(uniffi::Object))]
pub struct PhantomSession {
id: String,
peer_addr: String,
state: Arc<AtomicU8>,
send_queue: Arc<Mutex<Vec<Vec<u8>>>>,
cmd_tx: mpsc::Sender<SessionCommand>,
#[allow(dead_code)]
cmd_rx: Mutex<Option<mpsc::Receiver<SessionCommand>>>,
recv_rx: Mutex<mpsc::Receiver<Bytes>>,
demux: Arc<StreamDemultiplexer>,
streams: Arc<DashMap<u32, Arc<Stream>>>,
inner_session: Arc<Mutex<Option<Arc<Session>>>>,
early_data_accepted: Arc<Mutex<Option<bool>>>,
shaping: Arc<parking_lot::Mutex<TrafficShapingConfig>>,
observability: Arc<Observability>,
}
pub enum SessionCommand {
Send(Vec<u8>),
SendStreamReliable { stream_id: u32, data: bytes::Bytes },
SendStreamUnreliable { stream_id: u32, data: bytes::Bytes },
CloseStream { stream_id: u32 },
Migrate(String),
MigrateServer(String),
Close,
}
impl PhantomSession {
pub fn connect_with_transport<T: SessionTransport>(
peer_addr: &str,
transport: T,
expected_server_key: HybridVerifyingKey,
) -> Self {
Self::connect_with_transport_with_runtime(
peer_addr,
transport,
expected_server_key,
Arc::new(TokioRuntime),
)
}
pub fn connect_with_transport_with_runtime<T: SessionTransport>(
peer_addr: &str,
transport: T,
expected_server_key: HybridVerifyingKey,
runtime: Arc<dyn Runtime>,
) -> Self {
Self::spawn_client(peer_addr, transport, expected_server_key, runtime, None)
}
pub fn connect_with_resumption<T: SessionTransport>(
peer_addr: &str,
transport: T,
expected_server_key: HybridVerifyingKey,
resumption_hint: ([u8; 32], [u8; 32]),
early_data: Vec<u8>,
) -> Result<Self, CoreError> {
#[cfg(feature = "fips")]
crate::crypto::self_tests::ensure_post_passed()
.map_err(|e| CoreError::FipsSelfTestFailure(format!("{e:?}")))?;
if early_data.len() > EARLY_DATA_MAX_LEN {
return Err(CoreError::ValidationError(format!(
"early_data is {} bytes, exceeds the {}-byte 0-RTT cap",
early_data.len(),
EARLY_DATA_MAX_LEN
)));
}
let (resume_id, resume_secret) = resumption_hint;
Ok(Self::spawn_client(
peer_addr,
transport,
expected_server_key,
Arc::new(TokioRuntime),
Some((resume_id, resume_secret, early_data)),
))
}
fn spawn_client<T: SessionTransport>(
peer_addr: &str,
transport: T,
expected_server_key: HybridVerifyingKey,
runtime: Arc<dyn Runtime>,
resumption_request: Option<([u8; 32], [u8; 32], Vec<u8>)>,
) -> Self {
let (cmd_tx, cmd_rx) = mpsc::channel(256);
let (recv_tx, recv_rx) = mpsc::channel(256);
let state = Arc::new(AtomicU8::new(ConnectionState::Connecting as u8));
let send_queue = Arc::new(Mutex::new(Vec::new()));
let peer = peer_addr.to_string();
let (demux, _ctrl_rx) = StreamDemultiplexer::new(256);
let demux = Arc::new(demux);
let streams = Arc::new(DashMap::new());
let inner_session: Arc<Mutex<Option<Arc<Session>>>> = Arc::new(Mutex::new(None));
let early_data_accepted: Arc<Mutex<Option<bool>>> = Arc::new(Mutex::new(None));
let shaping = Arc::new(parking_lot::Mutex::new(TrafficShapingConfig::default()));
let observability = Observability::new(ObservabilityConfig::default());
let session = Self {
id: new_session_id(),
peer_addr: peer.clone(),
state: state.clone(),
send_queue: send_queue.clone(),
cmd_tx: cmd_tx.clone(),
cmd_rx: Mutex::new(None), recv_rx: Mutex::new(recv_rx),
demux: demux.clone(),
streams: streams.clone(),
inner_session: inner_session.clone(),
early_data_accepted: early_data_accepted.clone(),
shaping: shaping.clone(),
observability: observability.clone(),
};
let runtime_for_pump = runtime.clone();
let _detached = runtime.spawn(Box::pin(Self::background_task(
state,
send_queue,
cmd_tx,
cmd_rx,
recv_tx,
transport,
peer,
demux,
streams,
expected_server_key,
runtime_for_pump,
inner_session,
early_data_accepted,
shaping,
resumption_request,
observability,
)));
session
}
#[allow(dead_code)]
pub(crate) fn from_accepted_server_session<T: SessionTransport>(
peer_addr: String,
transport: T,
server_session: Arc<Session>,
) -> Arc<Self> {
Self::from_accepted_server_session_with_runtime(
peer_addr,
transport,
server_session,
Arc::new(TokioRuntime),
Observability::new(ObservabilityConfig::default()),
LegType::Tcp,
)
}
pub(crate) fn from_accepted_server_session_with_runtime<T: SessionTransport>(
peer_addr: String,
transport: T,
server_session: Arc<Session>,
runtime: Arc<dyn Runtime>,
observability: Arc<Observability>,
leg: LegType,
) -> Arc<Self> {
let (cmd_tx, cmd_rx) = mpsc::channel(256);
let (recv_tx, recv_rx) = mpsc::channel(256);
let state = Arc::new(AtomicU8::new(ConnectionState::Connected as u8));
let send_queue = Arc::new(Mutex::new(Vec::new()));
let (demux, _ctrl_rx) = StreamDemultiplexer::new(256);
let demux = Arc::new(demux);
let streams = Arc::new(DashMap::new());
let inner_session: Arc<Mutex<Option<Arc<Session>>>> =
Arc::new(Mutex::new(Some(server_session.clone())));
let session = Arc::new(Self {
id: new_session_id(),
peer_addr: peer_addr.clone(),
state: state.clone(),
send_queue: send_queue.clone(),
cmd_tx,
cmd_rx: Mutex::new(None),
recv_rx: Mutex::new(recv_rx),
demux: demux.clone(),
streams: streams.clone(),
inner_session,
early_data_accepted: Arc::new(Mutex::new(None)),
shaping: Arc::new(parking_lot::Mutex::new(TrafficShapingConfig::default())),
observability: observability.clone(),
});
let session_id = *server_session.id();
let runtime_for_pump = runtime.clone();
transport.set_frame_phase(FramePhase::Established);
transport.set_outbound_cid(server_session.current_outbound_cid());
let observed = Arc::new(ObservedTransport::new(
transport,
observability.clone(),
leg,
));
let _detached = runtime.spawn(Box::pin(run_data_pump(
server_session,
session_id,
observed,
state,
send_queue,
cmd_rx,
recv_tx,
demux,
streams,
runtime_for_pump,
observability,
leg,
)));
session
}
#[allow(clippy::too_many_arguments)]
async fn background_task<T: SessionTransport>(
state: Arc<AtomicU8>,
send_queue: Arc<Mutex<Vec<Vec<u8>>>>,
_cmd_tx: mpsc::Sender<SessionCommand>,
cmd_rx: mpsc::Receiver<SessionCommand>,
recv_tx: mpsc::Sender<Bytes>,
transport: T,
peer: String,
demux: Arc<StreamDemultiplexer>,
streams: Arc<DashMap<u32, Arc<Stream>>>,
expected_server_key: HybridVerifyingKey,
runtime: Arc<dyn Runtime>,
inner_session: Arc<Mutex<Option<Arc<Session>>>>,
early_data_accepted: Arc<Mutex<Option<bool>>>,
shaping: Arc<parking_lot::Mutex<TrafficShapingConfig>>,
resumption_request: Option<([u8; 32], [u8; 32], Vec<u8>)>,
observability: Arc<Observability>,
) {
log::debug!("PhantomSession: starting handshake with {}", peer);
#[cfg(feature = "fips")]
if let Err(e) = crate::crypto::self_tests::ensure_post_passed() {
log::error!(
"PhantomSession: FIPS POST self-test failed; refusing to handshake: {:?}",
e
);
state.store(ConnectionState::Failed as u8, Ordering::Relaxed);
return;
}
let pending_early_data: Option<Vec<u8>> = resumption_request
.as_ref()
.and_then(|(_, _, ed)| (!ed.is_empty()).then(|| ed.clone()));
const CLIENT_HANDSHAKE_DEADLINE: std::time::Duration = std::time::Duration::from_secs(10);
let handshake_result = {
let handshake_fut =
run_client_handshake(&transport, &expected_server_key, resumption_request);
let handshake_timeout = runtime.sleep(CLIENT_HANDSHAKE_DEADLINE);
tokio::pin!(handshake_fut);
tokio::select! {
r = &mut handshake_fut => r,
_ = handshake_timeout => Err(CoreError::Timeout),
}
};
let (crypto_session, ed_accepted) = match handshake_result {
Ok((session, accepted)) => (Arc::new(session), accepted),
Err(e) => {
log::error!("PhantomSession: handshake failed: {}", e);
state.store(ConnectionState::Failed as u8, Ordering::Relaxed);
return;
}
};
log::info!("PhantomSession: Handshake complete — hybrid channel ready");
{
let mut guard = inner_session.lock().await;
*guard = Some(crypto_session.clone());
}
apply_shaping(&crypto_session, *shaping.lock());
*early_data_accepted.lock().await = ed_accepted;
if ed_accepted == Some(false) {
if let Some(ed) = pending_early_data {
send_queue.lock().await.insert(0, ed);
log::debug!(
"PhantomSession: 0-RTT early-data rejected; re-queued for 1-RTT delivery"
);
}
}
let session_id = *crypto_session.id();
state.store(ConnectionState::Connected as u8, Ordering::Relaxed);
log::debug!("PhantomSession: fully connected to {}", peer);
transport.set_frame_phase(FramePhase::Established);
transport.set_outbound_cid(crypto_session.current_outbound_cid());
let observed = Arc::new(ObservedTransport::new(
transport,
observability.clone(),
LegType::Tcp,
));
run_data_pump(
crypto_session,
session_id,
observed,
state,
send_queue,
cmd_rx,
recv_tx,
demux,
streams,
runtime,
observability,
LegType::Tcp,
)
.await;
}
}
async fn run_client_handshake<T: SessionTransport>(
transport: &T,
expected_server_key: &HybridVerifyingKey,
resumption: Option<([u8; 32], [u8; 32], Vec<u8>)>,
) -> Result<(Session, Option<bool>), CoreError> {
let handshake = HandshakeClient::new()?;
let mut hello = match &resumption {
Some((resume_id, resume_secret, early_data)) => {
let ed: Option<&[u8]> = if early_data.is_empty() {
None
} else {
Some(early_data.as_slice())
};
handshake.create_client_hello_with_resume(*resume_id, resume_secret, ed)
}
None => handshake.create_client_hello(),
};
const MAX_CLIENT_RETRY_ROUNDS: u32 = 3;
const MAX_CLIENT_REJECT_ROUNDS: u32 = 3;
let mut retry_rounds: u32 = 0;
let mut reject_rounds: u32 = 0;
let mut remembered_reject: Option<ServerReject> = None;
loop {
let bytes = borsh::to_vec(&hello).map_err(|e| {
CoreError::SerializationError(format!("ClientHello encode failed: {}", e))
})?;
transport.send_bytes(&bytes).await?;
loop {
let resp = match transport.recv_bytes().await {
Ok(r) => r,
Err(e) => {
return match &remembered_reject {
Some(r) => Err(CoreError::HandshakeError(format!(
"server rejected the handshake: unsupported protocol version \
(client speaks v{}, server speaks v{})",
hello.version, r.supported_version
))),
None => Err(e),
};
}
};
match ServerReply::from_wire(&resp) {
Ok(ServerReply::Hello(sh)) => {
let (session, accepted) =
handshake.process_server_hello(&hello, &sh, Some(expected_server_key))?;
return Ok((session, accepted));
}
Ok(ServerReply::Reject(reject)) => {
if reject.has_marker() {
reject_rounds += 1;
if reject_rounds > MAX_CLIENT_REJECT_ROUNDS {
return Err(CoreError::HandshakeError(format!(
"server rejected the handshake: unsupported protocol version \
(client speaks v{}, server speaks v{})",
hello.version, reject.supported_version
)));
}
remembered_reject = Some(reject);
continue;
}
return Err(CoreError::HandshakeError(
"server reject missing marker".into(),
));
}
Ok(ServerReply::Retry(retry)) => {
retry_rounds += 1;
if retry_rounds > MAX_CLIENT_RETRY_ROUNDS {
return Err(CoreError::HandshakeError(format!(
"server demanded more than {MAX_CLIENT_RETRY_ROUNDS} HelloRetryRequest rounds"
)));
}
log::info!("PhantomSession: Received HelloRetryRequest, retrying...");
hello.cookie = retry.cookie;
if let Some(challenge) = retry.challenge {
log::info!("PhantomSession: Solving PoW challenge...");
hello.pow_solution = Some(
challenge
.solve_capped(crate::crypto::pow::MAX_CLIENT_POW_DIFFICULTY)
.map_err(|e| CoreError::HandshakeError(e.to_string()))?,
);
}
break; }
Err(e) => {
return Err(CoreError::HandshakeError(format!(
"invalid server reply: {e}"
)));
}
}
}
}
}
const RAW_APP_STREAM_ID: u32 = 1;
#[allow(clippy::too_many_arguments)]
async fn run_data_pump<T: SessionTransport>(
crypto_session: Arc<Session>,
session_id: SessionId,
transport: Arc<T>,
state: Arc<AtomicU8>,
send_queue: Arc<Mutex<Vec<Vec<u8>>>>,
mut cmd_rx: mpsc::Receiver<SessionCommand>,
recv_tx: mpsc::Sender<Bytes>,
demux: Arc<StreamDemultiplexer>,
streams: Arc<DashMap<u32, Arc<Stream>>>,
runtime: Arc<dyn Runtime>,
observability: Arc<Observability>,
leg: LegType,
) {
observability.session_opened(leg);
crypto_session.update_activity();
let raw_stream = Arc::new(Stream::new(RAW_APP_STREAM_ID as TransportStreamId));
streams.insert(RAW_APP_STREAM_ID, raw_stream.clone());
{
let mut queue = send_queue.lock().await;
let count = queue.len();
'flush: for msg in queue.drain(..) {
for chunk in msg.chunks(TRANSPORT_MTU) {
if let Err(e) = raw_stream
.send_reliable(Bytes::copy_from_slice(chunk))
.await
{
log::error!("PhantomSession: early-data flush aborted — {e}");
break 'flush;
}
}
}
if count > 0 {
log::info!(
"PhantomSession: queued {} early-data message(s) onto the raw-app stream",
count
);
crypto_session.notify_outbound_ready();
}
}
let (deliver_tx, mut deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered_bytes = Arc::new(AtomicU64::new(0));
{
let recv_tx_deliver = recv_tx; let demux_deliver = demux.clone();
let streams_deliver = streams.clone();
let crypto_deliver = crypto_session.clone();
let undelivered_deliver = undelivered_bytes.clone();
runtime.spawn(Box::pin(async move {
while let Some((stream_id, bytes)) = deliver_rx.recv().await {
let len = bytes.len() as u64;
demux_deliver.route_data(stream_id, bytes.clone());
undelivered_deliver.fetch_sub(len, Ordering::AcqRel);
if let Some(stream) = streams_deliver.get(&stream_id) {
if let Some(credit) = stream.record_app_consumed(len as u32) {
stream.stage_window_update_credit(credit);
crypto_deliver.notify_outbound_ready();
}
}
if recv_tx_deliver.send(bytes).await.is_err() {
break;
}
}
}));
}
let transport_recv = transport.clone();
let transport_send_ack = transport.clone();
let crypto_recv = crypto_session.clone();
let demux_recv = demux.clone();
let streams_recv = streams.clone();
let undelivered_reader = undelivered_bytes.clone();
let observability_recv = observability.clone();
let (recv_done_tx, mut recv_done_rx) = oneshot::channel::<()>();
let transport_for_path = transport.clone();
let recv_handle = runtime.spawn(Box::pin(async move {
let mut ack_buf: Vec<u8> = Vec::with_capacity(256);
const RECV_DELIVERY_HARD_CAP: u64 = 4 * 1024 * 1024;
loop {
if undelivered_reader.load(Ordering::Acquire) > RECV_DELIVERY_HARD_CAP {
log::warn!(
"PhantomSession: receive backlog {} B exceeds cap — peer ignoring flow \
control; closing session",
undelivered_reader.load(Ordering::Acquire)
);
break;
}
let data = match transport_recv.recv_bytes().await {
Ok(b) => b,
Err(_) => break,
};
let packet = match crypto_recv.parse_protected(&data) {
Ok(v) => v,
Err(_) => continue,
};
if packet.header.version != WIRE_VERSION {
continue;
}
handle_packet(
packet,
session_id,
&crypto_recv,
&streams_recv,
&demux_recv,
&transport_send_ack,
&transport_for_path,
&deliver_tx,
&undelivered_reader,
&mut ack_buf,
&observability_recv,
leg,
)
.await;
}
drop(deliver_tx);
let _ = recv_done_tx.send(());
}));
const TRANSPORT_MTU: usize = 1300;
let mut poll_interval = tokio::time::interval(std::time::Duration::from_millis(10));
let send_notify = crypto_session.send_notifier();
let mut migrating_since: Option<std::time::Instant> = None;
let mut died = false;
let mut last_keepalive = std::time::Instant::now();
let mut last_outbound_pn = crypto_session.peek_send_pn();
let mut last_outbound_at = std::time::Instant::now();
loop {
tokio::select! {
_ = poll_interval.tick() => {
flush_pending_window_updates(
&transport, &crypto_session, session_id, &streams,
)
.await;
drain_streams_priority_ordered(
&transport,
&crypto_session,
session_id,
&streams,
)
.await;
maybe_send_keepalive(
&transport, &crypto_session, session_id, &mut last_keepalive,
)
.await;
maybe_send_cover(
&transport,
&crypto_session,
session_id,
&mut last_outbound_pn,
&mut last_outbound_at,
)
.await;
if apply_liveness(&crypto_session, &state, &mut migrating_since) {
died = true;
break;
}
}
_ = send_notify.notified() => {
flush_pending_window_updates(
&transport, &crypto_session, session_id, &streams,
)
.await;
drain_streams_priority_ordered(
&transport,
&crypto_session,
session_id,
&streams,
)
.await;
}
cmd_opt = cmd_rx.recv() => {
match cmd_opt {
Some(SessionCommand::Send(data)) => {
for chunk in data.chunks(TRANSPORT_MTU) {
if let Err(e) = raw_stream
.send_reliable(Bytes::copy_from_slice(chunk))
.await
{
log::error!("PhantomSession: send aborted — {e}");
break;
}
}
crypto_session.notify_outbound_ready();
}
Some(SessionCommand::SendStreamReliable { stream_id, data }) => {
if let Some(stream) = streams.get(&stream_id) {
for chunk in data.chunks(TRANSPORT_MTU) {
if let Err(e) =
stream.send_reliable(Bytes::copy_from_slice(chunk)).await
{
log::error!("PhantomSession: stream send aborted — {e}");
break;
}
}
}
}
Some(SessionCommand::SendStreamUnreliable { stream_id, data }) => {
if let Some(stream) = streams.get(&stream_id) {
for chunk in data.chunks(TRANSPORT_MTU) {
stream.send_unreliable(Bytes::copy_from_slice(chunk)).await;
}
}
}
Some(SessionCommand::CloseStream { stream_id }) => {
if let Some(stream) = streams.get(&stream_id) {
stream.finish().await;
let _ = send_app_data(
&transport,
&crypto_session,
session_id,
stream_id as TransportStreamId,
&[],
PacketFlags::FIN,
None, ).await;
}
streams.remove(&stream_id);
demux.close_stream(stream_id);
}
Some(SessionCommand::Migrate(local_addr)) => {
match transport.migrate(local_addr).await {
Ok(()) => {
let new_path = crypto_session.next_migration_path_id();
transport.set_outbound_cid(crypto_session.advance_outbound_cid());
log::info!(
"PhantomSession: migrated send path -> path_id {}, CID rotated",
new_path
);
crypto_session.notify_outbound_ready();
}
Err(e) => {
log::warn!(
"PhantomSession: migrate rebind failed (staying on the old path): {}",
e
);
}
}
}
Some(SessionCommand::MigrateServer(local_addr)) => {
match transport.migrate_server(local_addr).await {
Ok(()) => {
let new_path = crypto_session.next_migration_path_id();
transport.set_outbound_cid(crypto_session.advance_outbound_cid());
log::info!(
"PhantomSession: migrated server send path -> path_id {}, s2c CID rotated",
new_path
);
crypto_session.notify_outbound_ready();
}
Err(e) => {
log::warn!(
"PhantomSession: server migrate rebind failed (staying on the old send socket): {}",
e
);
}
}
}
Some(SessionCommand::Close) => {
log::info!("PhantomSession: closing");
flush_pending_window_updates(
&transport, &crypto_session, session_id, &streams,
)
.await;
drain_streams_priority_ordered(
&transport, &crypto_session, session_id, &streams,
)
.await;
break;
}
None => {
log::info!("PhantomSession: command channel dropped");
flush_pending_window_updates(
&transport, &crypto_session, session_id, &streams,
)
.await;
drain_streams_priority_ordered(
&transport, &crypto_session, session_id, &streams,
)
.await;
break;
}
}
}
_ = &mut recv_done_rx => {
log::error!("PhantomSession: receive task ended unexpectedly (transport closed)");
break;
}
}
}
recv_handle.abort();
if !died {
state.store(ConnectionState::Closed as u8, Ordering::Relaxed);
}
observability.session_closed(leg);
}
fn apply_liveness(
crypto_session: &Arc<Session>,
state: &Arc<AtomicU8>,
migrating_since: &mut Option<std::time::Instant>,
) -> bool {
use crate::transport::liveness::{liveness_verdict, LivenessVerdict};
let cfg = crypto_session.liveness_config();
let snap = crypto_session.bandwidth_snapshot();
let silence = crypto_session.last_activity_elapsed();
let in_migrating = migrating_since.is_some();
let migrating_for = migrating_since
.map(|t| t.elapsed())
.unwrap_or(std::time::Duration::ZERO);
let effective_inflight = if crypto_session.keepalive_outstanding() {
snap.inflight_bytes.max(1)
} else {
snap.inflight_bytes
};
match liveness_verdict(
silence,
effective_inflight,
snap.min_rtt,
in_migrating,
migrating_for,
&cfg,
) {
LivenessVerdict::PathDown => {
*migrating_since = Some(std::time::Instant::now());
crypto_session.set_state(SessionState::Migrating);
state.store(ConnectionState::Migrating as u8, Ordering::Relaxed);
log::info!(
"PhantomSession: path down (no inbound for {silence:?} with data in flight) \
— entering Migrating; the embedder should migrate()"
);
false
}
LivenessVerdict::Recovered => {
*migrating_since = None;
crypto_session.set_state(SessionState::Connected);
state.store(ConnectionState::Connected as u8, Ordering::Relaxed);
log::info!("PhantomSession: path recovered — back to Connected");
false
}
LivenessVerdict::Dead => {
crypto_session.set_state(SessionState::Closed);
state.store(ConnectionState::Dead as u8, Ordering::Relaxed);
log::warn!("PhantomSession: migration idle-timeout elapsed — session dead");
true
}
LivenessVerdict::Unchanged => false,
}
}
async fn maybe_send_keepalive<T: SessionTransport>(
transport: &Arc<T>,
crypto_session: &Arc<Session>,
session_id: SessionId,
last_keepalive: &mut std::time::Instant,
) {
use crate::transport::liveness::should_send_keepalive;
let cfg = crypto_session.liveness_config();
if cfg.keepalive_interval.is_none() {
return;
}
let connected = crypto_session.state() == SessionState::Connected;
let snap = crypto_session.bandwidth_snapshot();
let inflight = if crypto_session.keepalive_outstanding() {
snap.inflight_bytes.max(1)
} else {
snap.inflight_bytes
};
if !should_send_keepalive(
connected,
inflight,
crypto_session.last_activity_elapsed(),
last_keepalive.elapsed(),
&cfg,
) {
return;
}
if send_keepalive(transport, crypto_session, session_id, false).await {
crypto_session.mark_keepalive_outstanding();
*last_keepalive = std::time::Instant::now();
}
}
async fn flush_pending_window_updates<T: SessionTransport>(
transport: &Arc<T>,
crypto_session: &Arc<Session>,
session_id: SessionId,
streams: &Arc<DashMap<u32, Arc<Stream>>>,
) {
let pending: Vec<(u32, u32, Arc<Stream>)> = streams
.iter()
.filter_map(|e| {
e.value()
.take_pending_window_update()
.map(|c| (*e.key(), c, e.value().clone()))
})
.collect();
for (stream_id, credit, stream) in pending {
if !send_window_update(
transport,
crypto_session,
session_id,
stream_id as TransportStreamId,
credit,
)
.await
{
stream.stage_window_update_credit(credit);
}
}
}
async fn drain_streams_priority_ordered<T: SessionTransport>(
transport: &Arc<T>,
crypto_session: &Arc<Session>,
session_id: SessionId,
streams: &Arc<DashMap<u32, Arc<Stream>>>,
) {
let mut snapshot: Vec<(u32, u32, Arc<Stream>)> = streams
.iter()
.map(|e| (e.value().priority(), *e.key(), e.value().clone()))
.collect();
snapshot.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
for (_priority, stream_id, stream) in snapshot {
loop {
let snap = crypto_session.bandwidth_snapshot();
let budget = snap.cwnd_bytes.saturating_sub(snap.inflight_bytes);
let Some(seg) = stream.poll_send(budget).await else {
break;
};
if seg.retransmit {
crypto_session.on_packet_lost(seg.data.len() as u64);
}
let base = if seg.reliable {
PacketFlags::RELIABLE
} else {
PacketFlags::UNRELIABLE
};
let reliable_offset = if seg.reliable {
Some(seg.stream_offset)
} else {
None
};
if !send_app_data(
transport,
crypto_session,
session_id,
stream_id as TransportStreamId,
&seg.data,
base,
reliable_offset,
)
.await
{
log::error!("PhantomSession: priority-ordered drain send failed");
if seg.reliable {
stream.mark_unsent(seg.stream_offset).await;
}
break;
}
}
}
}
fn feed_bbr_on_ack(
crypto_session: &Arc<Session>,
sent_at: tokio::time::Instant,
packet_bytes: u64,
ack_delay_us: u64,
) {
let sample = crate::transport::bandwidth_estimator::DeliverySample {
delivered_bytes: 0, sent_at: sent_at.into_std(),
acked_at: std::time::Instant::now(),
packet_bytes,
is_app_limited: false,
ack_delay_us,
};
let _ = crypto_session.on_packet_acked(sample);
}
async fn pace_send(crypto_session: &Arc<Session>, bytes: u64) {
let jitter_max = crypto_session.send_jitter();
if !jitter_max.is_zero() {
let delay = shaping::random_jitter(jitter_max.as_millis() as u32);
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
}
let pacer = crypto_session.pacer();
if !pacer.is_enabled() {
return;
}
loop {
if pacer.try_consume(bytes) {
return;
}
let wait = pacer.time_until_available(bytes);
if wait.is_zero() {
continue;
}
let cap = std::time::Duration::from_millis(50);
let wait = wait.min(cap);
tokio::time::sleep(wait).await;
}
}
fn rekey_before_stamp(crypto_session: &Arc<Session>) -> Option<u16> {
if crypto_session.send_needs_rekey() {
if let Err(e) = crypto_session.rekey() {
log::error!("PhantomSession: mid-session rekey failed: {}", e);
return None;
}
}
Some(if crypto_session.rekey_unconfirmed() {
PacketFlags::REKEY
} else {
0
})
}
async fn send_app_data<T: SessionTransport>(
transport: &Arc<T>,
crypto_session: &Arc<Session>,
session_id: SessionId,
stream_id: TransportStreamId,
payload: &[u8],
base_flags: u16,
reliable_offset: Option<u32>,
) -> bool {
let mut flag_bits = base_flags | PacketFlags::ENCRYPTED;
match rekey_before_stamp(crypto_session) {
Some(extra) => flag_bits |= extra,
None => return false,
}
let packet_number = crypto_session.next_send_pn();
let mut plaintext: Vec<u8> = match reliable_offset {
Some(off) => {
let mut v = Vec::with_capacity(4 + payload.len());
v.extend_from_slice(&off.to_be_bytes());
v.extend_from_slice(payload);
v
}
None => payload.to_vec(),
};
let trailer = shaping::padding_trailer_len(plaintext.len(), crypto_session.padding_policy());
if trailer > 0 {
shaping::append_padding(&mut plaintext, trailer);
flag_bits |= PacketFlags::PADDED;
}
let header = PacketHeader::new(
session_id,
stream_id,
packet_number,
PacketFlags::new(flag_bits),
)
.with_epoch(crypto_session.current_epoch())
.with_path_id(crypto_session.current_send_path_id());
let ciphertext = match crypto_session.encrypt_packet(&header, &plaintext, &[]) {
Ok(c) => c,
Err(e) => {
log::error!("PhantomSession: encrypt_packet failed: {}", e);
return false;
}
};
let packet = PhantomPacket::new(header, ciphertext);
let buf = match crypto_session.protect_packet(&packet) {
Ok(b) => b,
Err(e) => {
log::error!("PhantomSession: header protection failed: {}", e);
return false;
}
};
let size = buf.len();
pace_send(crypto_session, size as u64).await;
if let Err(e) = transport.send_bytes(&buf[..size]).await {
log::error!("PhantomSession: transport send failed: {}", e);
return false;
}
crypto_session.on_packet_sent(payload.len() as u64);
true
}
async fn send_window_update<T: SessionTransport>(
transport: &Arc<T>,
crypto_session: &Arc<Session>,
session_id: SessionId,
stream_id: TransportStreamId,
new_window: u32,
) -> bool {
let mut flag_bits = PacketFlags::ENCRYPTED | PacketFlags::WINDOW_UPDATE;
match rekey_before_stamp(crypto_session) {
Some(extra) => flag_bits |= extra,
None => return false,
}
let packet_number = crypto_session.next_send_pn();
let header = PacketHeader::new(
session_id,
stream_id,
packet_number,
PacketFlags::new(flag_bits),
)
.with_epoch(crypto_session.current_epoch());
let payload = new_window.to_be_bytes();
let ciphertext = match crypto_session.encrypt_packet(&header, &payload, &[]) {
Ok(c) => c,
Err(e) => {
log::error!("PhantomSession: WINDOW_UPDATE encrypt failed: {}", e);
return false;
}
};
let packet = PhantomPacket::new(header, ciphertext);
let buf = match crypto_session.protect_packet(&packet) {
Ok(b) => b,
Err(e) => {
log::error!(
"PhantomSession: WINDOW_UPDATE header protection failed: {}",
e
);
return false;
}
};
if let Err(e) = transport.send_bytes(&buf).await {
log::error!("PhantomSession: WINDOW_UPDATE send failed: {}", e);
return false;
}
true
}
async fn send_keepalive<T: SessionTransport>(
transport: &Arc<T>,
crypto_session: &Arc<Session>,
session_id: SessionId,
is_pong: bool,
) -> bool {
let mut flag_bits = PacketFlags::ENCRYPTED | PacketFlags::KEEPALIVE;
if is_pong {
flag_bits |= PacketFlags::ACK;
}
match rekey_before_stamp(crypto_session) {
Some(extra) => flag_bits |= extra,
None => return false,
}
let packet_number = crypto_session.next_send_pn();
let header = PacketHeader::new(
session_id,
RAW_APP_STREAM_ID as TransportStreamId,
packet_number,
PacketFlags::new(flag_bits),
)
.with_epoch(crypto_session.current_epoch())
.with_path_id(crypto_session.current_send_path_id());
let ciphertext = match crypto_session.encrypt_packet(&header, &[], &[]) {
Ok(c) => c,
Err(e) => {
log::error!("PhantomSession: keep-alive encrypt failed: {}", e);
return false;
}
};
let packet = PhantomPacket::new(header, ciphertext);
let buf = match crypto_session.protect_packet(&packet) {
Ok(b) => b,
Err(e) => {
log::error!("PhantomSession: keep-alive header protection failed: {}", e);
return false;
}
};
if let Err(e) = transport.send_bytes(&buf).await {
log::error!("PhantomSession: keep-alive send failed: {}", e);
return false;
}
true
}
async fn send_cover<T: SessionTransport>(
transport: &Arc<T>,
crypto_session: &Arc<Session>,
session_id: SessionId,
) -> bool {
let mut flag_bits = PacketFlags::ENCRYPTED | PacketFlags::COVER;
match rekey_before_stamp(crypto_session) {
Some(extra) => flag_bits |= extra,
None => return false,
}
let mut plaintext = Vec::new();
let trailer = shaping::padding_trailer_len(0, PaddingPolicy::Padme);
if trailer > 0 {
shaping::append_padding(&mut plaintext, trailer);
flag_bits |= PacketFlags::PADDED;
}
let packet_number = crypto_session.next_send_pn();
let header = PacketHeader::new(
session_id,
RAW_APP_STREAM_ID as TransportStreamId,
packet_number,
PacketFlags::new(flag_bits),
)
.with_epoch(crypto_session.current_epoch())
.with_path_id(crypto_session.current_send_path_id());
let ciphertext = match crypto_session.encrypt_packet(&header, &plaintext, &[]) {
Ok(c) => c,
Err(e) => {
log::error!("PhantomSession: cover encrypt failed: {}", e);
return false;
}
};
let packet = PhantomPacket::new(header, ciphertext);
let buf = match crypto_session.protect_packet(&packet) {
Ok(b) => b,
Err(e) => {
log::error!("PhantomSession: cover header protection failed: {}", e);
return false;
}
};
if let Err(e) = transport.send_bytes(&buf).await {
log::error!("PhantomSession: cover send failed: {}", e);
return false;
}
true
}
async fn maybe_send_cover<T: SessionTransport>(
transport: &Arc<T>,
crypto_session: &Arc<Session>,
session_id: SessionId,
last_pn: &mut u64,
last_at: &mut std::time::Instant,
) {
let interval = crypto_session.cover_interval();
if interval.is_zero() {
return;
}
if crypto_session.state() != SessionState::Connected {
return;
}
let pn = crypto_session.peek_send_pn();
if pn != *last_pn {
*last_pn = pn;
*last_at = std::time::Instant::now();
return;
}
if last_at.elapsed() >= interval && send_cover(transport, crypto_session, session_id).await {
*last_pn = crypto_session.peek_send_pn();
*last_at = std::time::Instant::now();
}
}
fn encrypt_path_validation(
crypto_session: &Arc<Session>,
session_id: SessionId,
path_id: u8,
payload: [u8; crate::transport::path::PATH_CHALLENGE_LEN],
) -> Option<Vec<u8>> {
let packet_number = crypto_session.next_send_pn();
let mut packet = build_path_validation_packet(session_id, path_id, packet_number, payload);
let flag_bits = packet.header.flags.0 | PacketFlags::ENCRYPTED;
packet.header.flags = PacketFlags::new(flag_bits);
packet.header.epoch = crypto_session.current_epoch();
let plaintext = std::mem::take(&mut packet.payload);
let ciphertext = match crypto_session.encrypt_packet(&packet.header, &plaintext, &[]) {
Ok(c) => c,
Err(e) => {
log::error!("PhantomSession: PATH_VALIDATION encrypt failed: {}", e);
return None;
}
};
packet.payload = ciphertext;
match crypto_session.protect_packet(&packet) {
Ok(buf) => Some(buf),
Err(e) => {
log::error!(
"PhantomSession: PATH_VALIDATION header protection failed: {}",
e
);
None
}
}
}
async fn send_path_validation<T: SessionTransport>(
transport: &Arc<T>,
crypto_session: &Arc<Session>,
session_id: SessionId,
path_id: u8,
payload: [u8; crate::transport::path::PATH_CHALLENGE_LEN],
) -> bool {
let buf = match encrypt_path_validation(crypto_session, session_id, path_id, payload) {
Some(b) => b,
None => return false,
};
if let Err(e) = transport.send_bytes(&buf).await {
log::error!("PhantomSession: PATH_VALIDATION send failed: {}", e);
return false;
}
true
}
const MAX_STREAMS: usize = 256;
fn apply_eps02_peer_migration_rotation<T: SessionTransport>(crypto: &Session, transport: &T) {
if crypto.is_server() {
transport.set_outbound_cid(crypto.advance_outbound_cid());
} else {
crypto.next_migration_path_id();
transport.set_outbound_cid(crypto.advance_outbound_cid());
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_packet<T: SessionTransport>(
packet: PhantomPacket,
session_id: SessionId,
crypto_recv: &Arc<Session>,
streams_recv: &Arc<DashMap<u32, Arc<Stream>>>,
demux_recv: &Arc<StreamDemultiplexer>,
transport_send_ack: &Arc<T>,
transport_for_path: &Arc<T>,
deliver_tx: &mpsc::UnboundedSender<(u32, Bytes)>,
undelivered_bytes: &AtomicU64,
ack_buf: &mut Vec<u8>,
observability: &Observability,
leg: LegType,
) {
let stream_id: u32 = packet.header.stream_id.into();
let path_id = packet.header.path_id;
if packet.header.session_id != session_id {
return;
}
crypto_recv.mark_path_seen(path_id);
let plaintext: Vec<u8> = if packet.header.flags.contains(PacketFlags::ENCRYPTED) {
match crypto_recv.decrypt_packet_accepting_rekey(
&packet.header,
&packet.payload,
&packet.extensions,
) {
Ok(pt) => pt,
Err(e) => {
if matches!(e, CoreError::ReplayDetected(_)) {
observability.record_replay_rejected(ReplayReason::Duplicate);
} else {
observability.record_aead_failure(leg, AeadAlgorithm::Aes256Gcm);
}
log::warn!("PhantomSession: V2 decrypt failed (dropping packet): {}", e);
return;
}
}
} else {
observability.record_unencrypted_dropped(leg);
log::warn!(
"PhantomSession: dropping unencrypted post-handshake packet (downgrade / forged FIN?)"
);
return;
};
let plaintext: Vec<u8> = if packet.header.flags.contains(PacketFlags::PADDED) {
match shaping::strip_padding(&plaintext) {
Ok(inner) => inner.to_vec(),
Err(_) => {
log::warn!("PhantomSession: dropping packet with malformed padding trailer");
return;
}
}
} else {
plaintext
};
if packet.header.flags.contains(PacketFlags::ENCRYPTED) {
crypto_recv.update_activity();
transport_for_path.confirm_authenticated_source();
if let Some(slide) = crypto_recv.note_migration_path(packet.header.path_id) {
crypto_recv.signal_cid_slide(slide);
apply_eps02_peer_migration_rotation(crypto_recv, transport_for_path.as_ref());
}
}
if packet.header.flags.contains(PacketFlags::KEEPALIVE) {
if !packet.header.flags.contains(PacketFlags::ACK) {
let _ = send_keepalive(transport_send_ack, crypto_recv, session_id, true).await;
}
return;
}
if packet.header.flags.contains(PacketFlags::COVER) {
return;
}
if packet.header.flags.contains(PacketFlags::ACK) {
let sack = match crate::transport::sack::Sack::from_wire(&plaintext) {
Ok(s) => s,
Err(e) => {
log::debug!(
"PhantomSession: dropping malformed SACK ({} B): {}",
plaintext.len(),
e
);
return;
}
};
if let Some(stream) = streams_recv.get(&stream_id) {
let result = stream.on_sack(&sack).await;
for retired in result.retired {
if let Some(sent_at) = retired.sent_at {
feed_bbr_on_ack(crypto_recv, sent_at, retired.size, sack.ack_delay_us as u64);
}
}
if !result.lost.is_empty() {
crypto_recv.notify_outbound_ready();
}
}
demux_recv.route_ack(stream_id, sack.largest_acked);
if packet.header.flags.contains(PacketFlags::FIN) {
demux_recv.route_close(stream_id);
}
return;
}
if packet.header.flags.contains(PacketFlags::WINDOW_UPDATE) {
if plaintext.len() != 4 {
log::warn!(
"PhantomSession: WINDOW_UPDATE payload length {} (expected 4)",
plaintext.len()
);
return;
}
let credit = u32::from_be_bytes([plaintext[0], plaintext[1], plaintext[2], plaintext[3]]);
if let Some(stream) = streams_recv.get(&stream_id) {
stream.apply_peer_window_update(credit);
crypto_recv.notify_outbound_ready();
}
return;
}
if packet.header.flags.contains(PacketFlags::PATH_VALIDATION) {
if plaintext.len() != crate::transport::path::PATH_CHALLENGE_LEN {
log::warn!(
"PhantomSession: PATH_VALIDATION plaintext length {} (expected {})",
plaintext.len(),
crate::transport::path::PATH_CHALLENGE_LEN
);
return;
}
let mut payload_buf = [0u8; crate::transport::path::PATH_CHALLENGE_LEN];
payload_buf.copy_from_slice(&plaintext);
match crypto_recv.path_state(path_id) {
Some(crate::transport::path::PathStateKind::Validating) => {
if crypto_recv.complete_path_validation(path_id, &payload_buf)
&& transport_for_path.promote_candidate()
{
crypto_recv.reset_congestion();
for s in streams_recv.iter() {
s.value().reset_rto();
}
if path_id == crate::transport::session::REBIND_VALIDATION_PATH_ID {
crypto_recv.retire_path(path_id);
}
}
return;
}
Some(crate::transport::path::PathStateKind::Validated)
| Some(crate::transport::path::PathStateKind::Failed) => {
return;
}
_ => {
let _ = send_path_validation(
transport_for_path,
crypto_recv,
session_id,
path_id,
payload_buf,
)
.await;
return;
}
}
}
if !matches!(
crypto_recv.path_state(path_id),
Some(crate::transport::path::PathStateKind::Validated)
) {
if transport_for_path.has_migration_candidate() {
if let Some(challenge) = crypto_recv.begin_path_validation(path_id) {
if let Some(buf) =
encrypt_path_validation(crypto_recv, session_id, path_id, challenge)
{
let _ = transport_for_path.send_to_candidate(&buf).await;
}
}
} else {
crypto_recv.register_unvalidated_path(path_id);
}
} else if transport_for_path.has_migration_candidate() {
let rebind_path = crate::transport::session::REBIND_VALIDATION_PATH_ID;
if let Some(challenge) = crypto_recv.begin_path_validation(rebind_path) {
if let Some(buf) =
encrypt_path_validation(crypto_recv, session_id, rebind_path, challenge)
{
let _ = transport_for_path.send_to_candidate(&buf).await;
}
}
}
if packet.header.flags.contains(PacketFlags::COALESCED) {
let inner_for_codec = PhantomPacket {
header: packet.header,
payload: plaintext,
extensions: Vec::new(),
};
match unwrap_coalesced_packet(&inner_for_codec) {
Ok(Some(subs)) => {
let payloads: Vec<Bytes> = subs
.into_iter()
.filter(|s| !s.is_empty())
.map(Bytes::from)
.collect();
deliver_in_order_run(payloads, stream_id, deliver_tx, undelivered_bytes);
}
Ok(None) => {
log::warn!("PhantomSession: COALESCED flag set but bundle didn't parse");
}
Err(e) => {
log::warn!("PhantomSession: COALESCED parse error: {}", e);
}
}
return;
}
if packet.header.flags.contains(PacketFlags::RELIABLE) {
if plaintext.len() < 4 {
log::warn!(
"PhantomSession: reliable frame missing stream-offset prefix ({} B)",
plaintext.len()
);
return;
}
let pt = Bytes::from(plaintext);
let stream_offset = u32::from_be_bytes([pt[0], pt[1], pt[2], pt[3]]);
let data = pt.slice(4..);
let existing = streams_recv.get(&stream_id).map(|s| s.clone());
let local = match existing {
Some(s) => s,
None => {
if streams_recv.len() >= MAX_STREAMS {
log::warn!(
"PhantomSession: refusing new receive stream {stream_id}: \
MAX_STREAMS ({MAX_STREAMS}) reached"
);
return;
}
streams_recv
.entry(stream_id)
.or_insert_with(|| Arc::new(Stream::new(stream_id as TransportStreamId)))
.clone()
}
};
let delivered = local.accept_in_order(stream_offset, vec![data]).await;
let Some(sack) = local.received_sack(0).await else {
return;
};
let mut ack_flag_bits = PacketFlags::ENCRYPTED | PacketFlags::ACK;
match rekey_before_stamp(crypto_recv) {
Some(extra) => ack_flag_bits |= extra,
None => return,
}
let ack_pn = crypto_recv.next_send_pn();
let ack_header = PacketHeader::new(
session_id,
stream_id as TransportStreamId,
ack_pn,
PacketFlags::new(ack_flag_bits),
)
.with_epoch(crypto_recv.current_epoch())
.with_path_id(path_id);
let ack_payload = sack.to_wire();
match crypto_recv.encrypt_packet(&ack_header, &ack_payload, &[]) {
Ok(ct) => {
let ack_packet = PhantomPacket::new(ack_header, ct);
match crypto_recv.protect_packet(&ack_packet) {
Ok(buf) => {
ack_buf.clear();
ack_buf.extend_from_slice(&buf);
let size = ack_buf.len();
let _ = transport_send_ack.send_bytes(&ack_buf[..size]).await;
}
Err(e) => {
log::error!("PhantomSession: ACK header protection failed: {}", e)
}
}
}
Err(e) => log::error!("PhantomSession: ACK encrypt failed: {}", e),
}
deliver_in_order_run(delivered, stream_id, deliver_tx, undelivered_bytes);
if packet.header.flags.contains(PacketFlags::FIN) {
demux_recv.route_close(stream_id);
}
return;
}
if !plaintext.is_empty() {
let len = plaintext.len() as u64;
if deliver_tx.send((stream_id, Bytes::from(plaintext))).is_ok() {
undelivered_bytes.fetch_add(len, Ordering::AcqRel);
}
}
if packet.header.flags.contains(PacketFlags::FIN) {
demux_recv.route_close(stream_id);
}
}
fn deliver_in_order_run(
run: Vec<Bytes>,
stream_id: u32,
deliver_tx: &mpsc::UnboundedSender<(u32, Bytes)>,
undelivered_bytes: &AtomicU64,
) {
for chunk in run {
if chunk.is_empty() {
continue;
}
let len = chunk.len() as u64;
if deliver_tx.send((stream_id, chunk)).is_ok() {
undelivered_bytes.fetch_add(len, Ordering::AcqRel);
}
}
}
impl PhantomSession {
pub(crate) fn set_state(&self, new_state: ConnectionState) {
self.state.store(new_state as u8, Ordering::Relaxed);
}
pub fn observability(&self) -> Arc<Observability> {
self.observability.clone()
}
}
#[cfg_attr(feature = "bindings", uniffi::export(async_runtime = "tokio"))]
impl PhantomSession {
#[cfg_attr(feature = "bindings", uniffi::constructor)]
pub fn connect(peer_addr: String) -> Arc<Self> {
let (cmd_tx, cmd_rx) = mpsc::channel(256);
let (_recv_tx, recv_rx) = mpsc::channel(256);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(256);
let streams = Arc::new(DashMap::new());
Arc::new(Self {
id: new_session_id(),
peer_addr,
state: Arc::new(AtomicU8::new(ConnectionState::Connecting as u8)),
send_queue: Arc::new(Mutex::new(Vec::new())),
cmd_tx,
cmd_rx: Mutex::new(Some(cmd_rx)),
recv_rx: Mutex::new(recv_rx),
demux: Arc::new(demux),
streams,
inner_session: Arc::new(Mutex::new(None)),
early_data_accepted: Arc::new(Mutex::new(None)),
shaping: Arc::new(parking_lot::Mutex::new(TrafficShapingConfig::default())),
observability: Observability::new(ObservabilityConfig::default()),
})
}
pub fn open_stream(&self) -> Arc<crate::api::stream::PhantomStream> {
let handle = self.demux.open_stream(1024);
let stream_id = handle.stream_id;
let transport_stream = Arc::new(Stream::new(stream_id as TransportStreamId));
self.streams.insert(stream_id, transport_stream);
Arc::new(crate::api::stream::PhantomStream::new(
handle,
self.cmd_tx.clone(),
))
}
pub async fn send(&self, data: Vec<u8>) -> Result<(), CoreError> {
let state = self.connection_state();
if state.is_data_ready() {
self.cmd_tx
.send(SessionCommand::Send(data))
.await
.map_err(|_| CoreError::NetworkError("Session closed".into()))?;
} else if state == ConnectionState::Connecting {
self.send_queue.lock().await.push(data);
} else {
return Err(CoreError::NetworkError(format!(
"Cannot send in state {:?}",
state
)));
}
Ok(())
}
pub async fn recv(&self) -> Result<Vec<u8>, CoreError> {
let mut rx = self.recv_rx.lock().await;
let bytes = rx
.recv()
.await
.ok_or_else(|| CoreError::NetworkError("Session closed".into()))?;
Ok(bytes.to_vec())
}
pub fn connection_state(&self) -> ConnectionState {
ConnectionState::from_u8(self.state.load(Ordering::Relaxed))
}
pub fn is_data_ready(&self) -> bool {
self.connection_state().is_data_ready()
}
pub fn is_pqc_ready(&self) -> bool {
matches!(
self.connection_state(),
ConnectionState::PqcReady | ConnectionState::Connected
)
}
pub async fn flush_queue(&self) -> Result<u32, CoreError> {
let mut queue = self.send_queue.lock().await;
let count = queue.len() as u32;
for msg in queue.drain(..) {
self.cmd_tx
.send(SessionCommand::Send(msg))
.await
.map_err(|_| CoreError::NetworkError("Session closed during flush".into()))?;
}
Ok(count)
}
pub async fn queued_count(&self) -> u32 {
self.send_queue.lock().await.len() as u32
}
pub fn id(&self) -> String {
self.id.clone()
}
pub fn peer_addr(&self) -> String {
self.peer_addr.clone()
}
pub async fn early_data_accepted(&self) -> Option<bool> {
*self.early_data_accepted.lock().await
}
pub async fn resumption_hint(&self) -> Option<ResumptionHint> {
let guard = self.inner_session.lock().await;
guard
.as_ref()
.and_then(|s| s.resumption_hint())
.map(|(session_id, resumption_secret)| ResumptionHint {
session_id: session_id.to_vec(),
resumption_secret: resumption_secret.to_vec(),
})
}
pub async fn current_epoch(&self) -> Option<u8> {
self.inner_session
.lock()
.await
.as_ref()
.map(|s| s.current_epoch())
}
pub async fn set_rekey_threshold(&self, n: u64) -> bool {
match self.inner_session.lock().await.as_ref() {
Some(s) => {
s.set_rekey_threshold(n);
true
}
None => false,
}
}
pub async fn set_traffic_shaping(&self, config: TrafficShapingConfig) -> bool {
*self.shaping.lock() = config;
if let Some(s) = self.inner_session.lock().await.as_ref() {
apply_shaping(s, config);
}
true
}
pub async fn traffic_shaping(&self) -> Option<TrafficShapingConfig> {
self.inner_session
.lock()
.await
.as_ref()
.map(|s| TrafficShapingConfig {
padding: s.padding_policy(),
jitter_ms: s.send_jitter().as_millis() as u32,
cover_interval_ms: s.cover_interval().as_millis() as u32,
})
}
pub async fn migrate(&self, local_addr: String) -> Result<(), CoreError> {
self.cmd_tx
.send(SessionCommand::Migrate(local_addr))
.await
.map_err(|_| CoreError::NetworkError("Session closed".into()))
}
pub async fn disconnect(&self) -> Result<(), CoreError> {
self.set_state(ConnectionState::Closed);
let _ = self.cmd_tx.send(SessionCommand::Close).await;
Ok(())
}
}
impl PhantomSession {
pub fn demux(&self) -> Arc<StreamDemultiplexer> {
self.demux.clone()
}
pub async fn set_liveness_config(
&self,
cfg: crate::transport::liveness::LivenessConfig,
) -> bool {
match self.inner_session.lock().await.as_ref() {
Some(s) => {
s.set_liveness_config(cfg);
true
}
None => false,
}
}
pub async fn migrate_server(&self, local_addr: String) -> Result<(), CoreError> {
self.cmd_tx
.send(SessionCommand::MigrateServer(local_addr))
.await
.map_err(|_| CoreError::NetworkError("Session closed".into()))
}
}
impl std::fmt::Debug for PhantomSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PhantomSession")
.field("id", &self.id)
.field("peer", &self.peer_addr)
.field("state", &self.connection_state())
.finish()
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(feature = "bindings", uniffi::export(async_runtime = "tokio"))]
pub async fn connect_pinned(
host: String,
port: u16,
pinned_key: Vec<u8>,
) -> Result<Arc<PhantomSession>, CoreError> {
#[cfg(feature = "fips")]
crate::crypto::self_tests::ensure_post_passed()
.map_err(|e| CoreError::FipsSelfTestFailure(format!("{e:?}")))?;
let expected_server_key = HybridVerifyingKey::from_bytes(&pinned_key)
.map_err(|e| CoreError::CryptoError(format!("invalid pinned key: {}", e)))?;
let addr = format!("{}:{}", host, port);
let stream = tokio::net::TcpStream::connect(&addr)
.await
.map_err(|e| CoreError::NetworkError(format!("connect {}: {}", addr, e)))?;
let transport = crate::api::tcp_transport::TcpSessionTransport::new(stream);
let session = PhantomSession::connect_with_transport(&addr, transport, expected_server_key);
Ok(Arc::new(session))
}
#[cfg(all(not(target_arch = "wasm32"), feature = "mimicry"))]
pub async fn connect_pinned_mimic(
host: String,
port: u16,
pinned_key: Vec<u8>,
sni: String,
) -> Result<Arc<PhantomSession>, CoreError> {
#[cfg(feature = "fips")]
crate::crypto::self_tests::ensure_post_passed()
.map_err(|e| CoreError::FipsSelfTestFailure(format!("{e:?}")))?;
let expected_server_key = HybridVerifyingKey::from_bytes(&pinned_key)
.map_err(|e| CoreError::CryptoError(format!("invalid pinned key: {}", e)))?;
let addr = format!("{}:{}", host, port);
let stream = tokio::net::TcpStream::connect(&addr)
.await
.map_err(|e| CoreError::NetworkError(format!("connect {}: {}", addr, e)))?;
let config = crate::transport::legs::mimic_tls::MimicConfig::new(sni);
let transport =
crate::transport::legs::mimic_tls::MimicTlsLeg::connect(stream, &config).await?;
let session = PhantomSession::connect_with_transport(&addr, transport, expected_server_key);
Ok(Arc::new(session))
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(feature = "bindings", uniffi::export(async_runtime = "tokio"))]
pub async fn connect_pinned_with_resumption(
host: String,
port: u16,
pinned_key: Vec<u8>,
hint: ResumptionHint,
early_data: Vec<u8>,
) -> Result<Arc<PhantomSession>, CoreError> {
#[cfg(feature = "fips")]
crate::crypto::self_tests::ensure_post_passed()
.map_err(|e| CoreError::FipsSelfTestFailure(format!("{e:?}")))?;
let expected_server_key = HybridVerifyingKey::from_bytes(&pinned_key)
.map_err(|e| CoreError::CryptoError(format!("invalid pinned key: {}", e)))?;
let session_id: [u8; 32] = hint.session_id.as_slice().try_into().map_err(|_| {
CoreError::ValidationError(format!(
"resumption hint session_id must be 32 bytes, got {}",
hint.session_id.len()
))
})?;
let resumption_secret: [u8; 32] =
hint.resumption_secret.as_slice().try_into().map_err(|_| {
CoreError::ValidationError(format!(
"resumption hint resumption_secret must be 32 bytes, got {}",
hint.resumption_secret.len()
))
})?;
if early_data.len() > EARLY_DATA_MAX_LEN {
return Err(CoreError::ValidationError(format!(
"early_data is {} bytes, exceeds the {}-byte 0-RTT cap",
early_data.len(),
EARLY_DATA_MAX_LEN
)));
}
let addr = format!("{}:{}", host, port);
let stream = tokio::net::TcpStream::connect(&addr)
.await
.map_err(|e| CoreError::NetworkError(format!("connect {}: {}", addr, e)))?;
let transport = crate::api::tcp_transport::TcpSessionTransport::new(stream);
let session = PhantomSession::connect_with_resumption(
&addr,
transport,
expected_server_key,
(session_id, resumption_secret),
early_data,
)?;
Ok(Arc::new(session))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::handshake::{ClientHello, HandshakeResponse, HandshakeServer};
struct ChannelTransport {
tx: mpsc::Sender<Vec<u8>>,
rx: Mutex<mpsc::Receiver<Vec<u8>>>,
}
impl ChannelTransport {
fn pair() -> (Self, Self) {
let (a_tx, b_rx) = mpsc::channel(64);
let (b_tx, a_rx) = mpsc::channel(64);
(
Self {
tx: a_tx,
rx: Mutex::new(a_rx),
},
Self {
tx: b_tx,
rx: Mutex::new(b_rx),
},
)
}
}
impl SessionTransport for ChannelTransport {
async fn send_bytes(&self, data: &[u8]) -> Result<(), CoreError> {
self.tx
.send(data.to_vec())
.await
.map_err(|_| CoreError::NetworkError("channel closed".into()))
}
async fn recv_bytes(&self) -> Result<Bytes, CoreError> {
let mut rx = self.rx.lock().await;
let v = rx
.recv()
.await
.ok_or_else(|| CoreError::NetworkError("channel closed".into()))?;
Ok(Bytes::from(v))
}
}
#[test]
fn rekey_before_stamp_re_advertises_rekey_until_peer_confirms() {
use crate::transport::session::{CryptoState, Session};
use crate::transport::types::{SchedulerMode, SessionId};
let shared = [0x55u8; 32];
let id = SessionId::from_bytes([7u8; 32]);
let crypto = CryptoState::new(&shared, false).expect("crypto");
let session = Arc::new(Session::from_derived(
id,
crypto,
SchedulerMode::LowLatency,
shared,
false,
));
session.set_rekey_threshold(2);
assert_eq!(
rekey_before_stamp(&session),
Some(0),
"below threshold: no flag"
);
let h = PacketHeader::new(
*session.id(),
1,
0,
PacketFlags::new(PacketFlags::ENCRYPTED),
);
for i in 0..2u64 {
session
.encrypt_packet(
&PacketHeader {
packet_number: i,
..h
},
b"x",
&[],
)
.expect("encrypt");
}
assert!(session.send_needs_rekey());
assert_eq!(rekey_before_stamp(&session), Some(PacketFlags::REKEY));
assert_eq!(session.current_epoch(), 1);
assert!(session.rekey_unconfirmed());
assert_eq!(rekey_before_stamp(&session), Some(PacketFlags::REKEY));
assert_eq!(
session.current_epoch(),
1,
"no second rekey — only a re-advertise"
);
}
#[tokio::test]
async fn client_surfaces_server_reject_as_version_error() {
use crate::transport::handshake::{ServerReject, ServerReply};
let (client_transport, server_transport) = ChannelTransport::pair();
let (_sk, expected_vk) = crate::crypto::hybrid_sign::HybridSigningKey::generate();
let server = tokio::spawn(async move {
let _hello = server_transport.recv_bytes().await.unwrap();
let reject = ServerReply::Reject(ServerReject::unsupported_version())
.to_wire()
.unwrap();
server_transport.send_bytes(&reject).await.unwrap();
});
let result = run_client_handshake(&client_transport, &expected_vk, None).await;
server.await.unwrap();
let err = result.expect_err("client must surface the reject as an error");
let msg = format!("{err:?}");
assert!(
msg.contains("unsupported protocol version"),
"expected a version-mismatch error, got: {msg}"
);
}
#[tokio::test]
async fn injected_server_reject_does_not_abort_a_healthy_handshake() {
use crate::transport::handshake::{ServerReject, ServerReply};
let (client_transport, server_transport) = ChannelTransport::pair();
let server_hs = HandshakeServer::new().unwrap();
let expected_vk = server_hs.verifying_key().clone();
let server = tokio::spawn(async move {
let Ok(hello_bytes) = server_transport.recv_bytes().await else {
return;
};
let Ok(client_hello) = borsh::from_slice::<ClientHello>(&hello_bytes) else {
return;
};
let reject = ServerReply::Reject(ServerReject::unsupported_version())
.to_wire()
.unwrap();
if server_transport.send_bytes(&reject).await.is_err() {
return;
}
let ip = "127.0.0.1".parse().unwrap();
let sh = match server_hs.process_client_hello(&client_hello, 0, ip) {
HandshakeResponse::Retry(retry) => {
if server_transport
.send_bytes(&ServerReply::Retry(retry).to_wire().unwrap())
.await
.is_err()
{
return;
}
let Ok(h2) = server_transport.recv_bytes().await else {
return;
};
let Ok(next) = borsh::from_slice::<ClientHello>(&h2) else {
return;
};
match server_hs.process_client_hello(&next, 0, ip) {
HandshakeResponse::Success(sh, _, _) => sh,
_ => return,
}
}
HandshakeResponse::Success(sh, _, _) => sh,
_ => return,
};
let _ = server_transport
.send_bytes(&ServerReply::Hello(sh).to_wire().unwrap())
.await;
});
let result = run_client_handshake(&client_transport, &expected_vk, None).await;
drop(client_transport);
let _ = server.await;
assert!(
result.is_ok(),
"an injected ServerReject ahead of the real ServerHello must not abort a healthy \
handshake; got {result:?}"
);
}
#[tokio::test]
async fn client_handshake_caps_retry_rounds() {
use crate::transport::handshake::HelloRetryRequest;
let (client_transport, server_transport) = ChannelTransport::pair();
let (_sk, expected_vk) = crate::crypto::hybrid_sign::HybridSigningKey::generate();
let server = tokio::spawn(async move {
loop {
if server_transport.recv_bytes().await.is_err() {
break;
}
let retry = borsh::to_vec(&HelloRetryRequest {
challenge: None,
cookie: None,
})
.expect("encode retry");
if server_transport.send_bytes(&retry).await.is_err() {
break;
}
}
});
let result = run_client_handshake(&client_transport, &expected_vk, None).await;
drop(client_transport); let _ = server.await;
assert!(
matches!(result, Err(CoreError::HandshakeError(_))),
"client must error after the retry-round cap, not loop forever; got {result:?}"
);
}
#[test]
fn resumption_hint_debug_redacts_secret() {
let hint = ResumptionHint {
session_id: vec![0xAB; 32],
resumption_secret: vec![0xCD; 32],
};
let dbg = format!("{hint:?}");
assert!(dbg.contains("REDACTED"), "secret must be redacted: {dbg}");
assert!(
!dbg.contains("205"),
"no decimal secret bytes in Debug: {dbg}"
);
assert!(
!dbg.to_lowercase().contains("cd, cd"),
"no hex secret bytes: {dbg}"
);
}
#[tokio::test]
async fn test_phantom_session_instant_connect() {
let session = PhantomSession::connect("example.com:443".to_string());
assert_eq!(session.connection_state(), ConnectionState::Connecting);
assert!(!session.is_data_ready());
assert_eq!(session.peer_addr(), "example.com:443");
}
#[tokio::test]
async fn deprecated_connect_is_inert_and_sends_no_bytes() {
let session = PhantomSession::connect("example.com:443".to_string());
assert_eq!(session.connection_state(), ConnectionState::Connecting);
assert!(!session.is_data_ready());
assert!(!session.is_pqc_ready());
session.send(b"first".to_vec()).await.unwrap();
session.send(b"second".to_vec()).await.unwrap();
assert_eq!(
session.queued_count().await,
2,
"inert connect() must buffer sends in memory, never flush them to a wire"
);
assert_eq!(session.connection_state(), ConnectionState::Connecting);
let recv = tokio::time::timeout(std::time::Duration::from_millis(50), session.recv()).await;
match recv {
Ok(Err(_)) => { }
Ok(Ok(bytes)) => panic!(
"inert connect() must never deliver received data, got {} bytes",
bytes.len()
),
Err(_elapsed) => { }
}
}
#[tokio::test]
async fn test_phantom_session_send_queue() {
let session = PhantomSession::connect("example.com:443".to_string());
session.send(vec![1, 2, 3]).await.unwrap();
session.send(vec![4, 5, 6]).await.unwrap();
assert_eq!(session.queued_count().await, 2);
session.set_state(ConnectionState::ClassicalReady);
assert!(session.is_data_ready());
let flushed = session.flush_queue().await.unwrap();
assert_eq!(flushed, 2);
assert_eq!(session.queued_count().await, 0);
}
#[tokio::test]
async fn test_phantom_session_state_progression() {
let session = PhantomSession::connect("example.com:443".to_string());
assert_eq!(session.connection_state(), ConnectionState::Connecting);
assert!(!session.is_data_ready());
session.set_state(ConnectionState::ClassicalReady);
assert!(session.is_data_ready());
assert!(!session.is_pqc_ready());
session.set_state(ConnectionState::PqcUpgrading);
assert!(session.is_data_ready());
assert!(!session.is_pqc_ready());
session.set_state(ConnectionState::PqcReady);
assert!(session.is_data_ready());
assert!(session.is_pqc_ready());
session.set_state(ConnectionState::Connected);
assert!(session.is_data_ready());
assert!(session.is_pqc_ready());
}
#[tokio::test]
async fn test_phantom_session_close() {
let session = PhantomSession::connect("example.com:443".to_string());
session.disconnect().await.unwrap();
assert_eq!(session.connection_state(), ConnectionState::Closed);
assert!(!session.is_data_ready());
}
fn decrypt_incoming(
server_session: &crate::transport::session::Session,
bytes: &[u8],
) -> Vec<u8> {
let pkt = server_session
.parse_protected(bytes)
.expect("parse header-protected PhantomPacket");
assert!(
pkt.header.flags.contains(PacketFlags::ENCRYPTED),
"expected ENCRYPTED flag on application data"
);
let plain = server_session
.decrypt_packet(&pkt.header, &pkt.payload, &[])
.expect("decrypt application data");
if pkt.header.flags.contains(PacketFlags::RELIABLE) && plain.len() >= 4 {
plain[4..].to_vec()
} else {
plain
}
}
fn encrypt_outgoing(
server_session: &crate::transport::session::Session,
session_id: SessionId,
stream_id: TransportStreamId,
sequence: u32,
payload: &[u8],
) -> Vec<u8> {
let flag_bits = PacketFlags::RELIABLE | PacketFlags::ENCRYPTED;
let header = PacketHeader::new(
session_id,
stream_id,
sequence as u64,
PacketFlags::new(flag_bits),
)
.with_epoch(server_session.current_epoch());
let mut pt = Vec::with_capacity(4 + payload.len());
pt.extend_from_slice(&sequence.to_be_bytes());
pt.extend_from_slice(payload);
let ct = server_session
.encrypt_packet(&header, &pt, &[])
.expect("encrypt reply");
let packet = PhantomPacket::new(header, ct);
server_session
.protect_packet(&packet)
.expect("header protection")
}
#[tokio::test]
async fn test_phantom_session_handshake_via_transport() {
let (client_transport, server_transport) = ChannelTransport::pair();
let server_hs = HandshakeServer::new().unwrap();
let server_pinned_key = server_hs.verifying_key().clone();
let session = PhantomSession::connect_with_transport(
"test-server:9000",
client_transport,
server_pinned_key,
);
session.send(b"early-data".to_vec()).await.unwrap();
let server_handle = tokio::spawn(async move {
let client_ip = "127.0.0.1".parse().unwrap();
let client_hello_bytes = server_transport.recv_bytes().await.unwrap();
let client_hello = borsh::from_slice::<ClientHello>(&client_hello_bytes).unwrap();
let server_session = loop {
let response = server_hs.process_client_hello(&client_hello, 0, client_ip);
match response {
HandshakeResponse::Retry(retry) => {
let retry_bytes = ServerReply::Retry(retry).to_wire().unwrap();
server_transport.send_bytes(&retry_bytes).await.unwrap();
let next_bytes = server_transport.recv_bytes().await.unwrap();
let next_hello = borsh::from_slice::<ClientHello>(&next_bytes).unwrap();
let resp2 = server_hs.process_client_hello(&next_hello, 0, client_ip);
match resp2 {
HandshakeResponse::Success(server_hello, session, _) => {
let server_hello_bytes =
ServerReply::Hello(server_hello).to_wire().unwrap();
server_transport
.send_bytes(&server_hello_bytes)
.await
.unwrap();
break session;
}
_ => panic!("Expected success after retry"),
}
}
HandshakeResponse::Success(server_hello, session, _) => {
let server_hello_bytes =
ServerReply::Hello(server_hello).to_wire().unwrap();
server_transport
.send_bytes(&server_hello_bytes)
.await
.unwrap();
break session;
}
HandshakeResponse::Reject(r) => panic!("unexpected reject: {:?}", r),
HandshakeResponse::Fail(e) => panic!("handshake failed: {:?}", e),
}
};
let session_id = *server_session.id();
let early_frame = server_transport.recv_bytes().await.unwrap();
assert!(
!early_frame
.windows(b"early-data".len())
.any(|w| w == b"early-data"),
"encrypted frame must not contain plaintext early-data"
);
let early_plain = decrypt_incoming(&server_session, &early_frame);
assert_eq!(early_plain, b"early-data");
let post_frame = server_transport.recv_bytes().await.unwrap();
let post_plain = decrypt_incoming(&server_session, &post_frame);
assert_eq!(post_plain, b"after-handshake");
let reply = encrypt_outgoing(&server_session, session_id, 1, 0, b"server-reply");
server_transport.send_bytes(&reply).await.unwrap();
});
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
assert_eq!(session.connection_state(), ConnectionState::Connected);
session.send(b"after-handshake".to_vec()).await.unwrap();
let reply = session.recv().await.unwrap();
assert_eq!(reply, b"server-reply");
server_handle.await.unwrap();
session.disconnect().await.unwrap();
}
#[tokio::test]
async fn reliable_send_survives_a_dropped_data_frame() {
use crate::test_harness::fault_transport::{FaultControl, LossyTransport};
let (client_transport, server_transport) = ChannelTransport::pair();
let faults = FaultControl::new();
let lossy_client = LossyTransport::new(client_transport, faults.clone());
let server_hs = HandshakeServer::new().unwrap();
let server_pinned_key = server_hs.verifying_key().clone();
let session = PhantomSession::connect_with_transport(
"test-server:9000",
lossy_client,
server_pinned_key,
);
let server_handle = tokio::spawn(async move {
let client_ip = "127.0.0.1".parse().unwrap();
let client_hello_bytes = server_transport.recv_bytes().await.unwrap();
let client_hello = borsh::from_slice::<ClientHello>(&client_hello_bytes).unwrap();
let server_session = loop {
match server_hs.process_client_hello(&client_hello, 0, client_ip) {
HandshakeResponse::Retry(retry) => {
let retry_bytes = ServerReply::Retry(retry).to_wire().unwrap();
server_transport.send_bytes(&retry_bytes).await.unwrap();
let next_bytes = server_transport.recv_bytes().await.unwrap();
let next_hello = borsh::from_slice::<ClientHello>(&next_bytes).unwrap();
match server_hs.process_client_hello(&next_hello, 0, client_ip) {
HandshakeResponse::Success(server_hello, session, _) => {
let b = ServerReply::Hello(server_hello).to_wire().unwrap();
server_transport.send_bytes(&b).await.unwrap();
break session;
}
_ => panic!("expected success after retry"),
}
}
HandshakeResponse::Success(server_hello, session, _) => {
let b = ServerReply::Hello(server_hello).to_wire().unwrap();
server_transport.send_bytes(&b).await.unwrap();
break session;
}
HandshakeResponse::Reject(r) => panic!("unexpected reject: {:?}", r),
HandshakeResponse::Fail(e) => panic!("handshake failed: {:?}", e),
}
};
let data_frame = tokio::time::timeout(
std::time::Duration::from_secs(3),
server_transport.recv_bytes(),
)
.await
.expect(
"reliable payload never arrived within 3s — the dropped data frame was not \
retransmitted (loss-recovery regression)",
)
.unwrap();
let plain = decrypt_incoming(&server_session, &data_frame);
assert_eq!(plain, b"reliable-payload");
});
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
assert_eq!(session.connection_state(), ConnectionState::Connected);
faults.arm_drop_next(1);
session.send(b"reliable-payload".to_vec()).await.unwrap();
server_handle.await.unwrap();
session.disconnect().await.unwrap();
}
#[tokio::test]
async fn drain_reports_a_retransmit_as_loss_to_bbr() {
use crate::transport::bandwidth_estimator::BbrState;
tokio::time::pause();
let sid = fixed_session_id();
let (client, _server) = paired_sessions(sid);
let stream = Arc::new(TransportStream::new(1));
stream.send_reliable(Bytes::from("payload")).await.unwrap();
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
streams.insert(1u32, stream);
let (client_t, _server_t) = ChannelTransport::pair();
let transport = Arc::new(client_t);
drain_streams_priority_ordered(&transport, &client, sid, &streams).await;
assert_ne!(client.bbr_state(), BbrState::FastRecovery);
tokio::time::advance(std::time::Duration::from_millis(1100)).await;
drain_streams_priority_ordered(&transport, &client, sid, &streams).await;
assert_eq!(
client.bbr_state(),
BbrState::FastRecovery,
"a retransmit must be reported to BBR as a loss"
);
}
#[tokio::test]
async fn drain_withholds_new_data_when_inflight_exceeds_cwnd() {
let sid = fixed_session_id();
let (client, _server) = paired_sessions(sid);
client.on_packet_sent(100_000_000);
let inflight_before = client.bandwidth_snapshot().inflight_bytes;
let stream = Arc::new(TransportStream::new(1));
stream.send_reliable(Bytes::from("new-data")).await.unwrap();
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
streams.insert(1u32, stream);
let (client_t, _server_t) = ChannelTransport::pair();
let transport = Arc::new(client_t);
drain_streams_priority_ordered(&transport, &client, sid, &streams).await;
assert_eq!(
client.bandwidth_snapshot().inflight_bytes,
inflight_before,
"no new data should be sent when inflight >= cwnd"
);
}
use crate::transport::multiplexer::StreamDemultiplexer;
use crate::transport::session::Session as InnerSession;
use crate::transport::stream::Stream as TransportStream;
fn paired_sessions(session_id: SessionId) -> (Arc<InnerSession>, Arc<InnerSession>) {
let secret = [0x11u8; 32];
let client = Arc::new(InnerSession::new(session_id, &secret, false).unwrap());
let server = Arc::new(InnerSession::new(session_id, &secret, true).unwrap());
(client, server)
}
fn fixed_session_id() -> SessionId {
SessionId::from_bytes([0x88; 32])
}
fn build_app_frame(
client_session: &InnerSession,
session_id: SessionId,
stream_id: TransportStreamId,
sequence: u32,
payload: &[u8],
) -> Vec<u8> {
build_app_frame_with_offset(
client_session,
session_id,
stream_id,
sequence,
sequence,
payload,
)
}
fn build_app_frame_with_offset(
client_session: &InnerSession,
session_id: SessionId,
stream_id: TransportStreamId,
sequence: u32,
stream_offset: u32,
payload: &[u8],
) -> Vec<u8> {
let flag_bits = PacketFlags::RELIABLE | PacketFlags::ENCRYPTED;
let header = PacketHeader::new(
session_id,
stream_id,
sequence as u64,
PacketFlags::new(flag_bits),
)
.with_epoch(client_session.current_epoch());
let mut pt = Vec::with_capacity(4 + payload.len());
pt.extend_from_slice(&stream_offset.to_be_bytes());
pt.extend_from_slice(payload);
let ciphertext = client_session
.encrypt_packet(&header, &pt, &[])
.expect("encrypt_packet");
PhantomPacket::new(header, ciphertext).to_wire()
}
fn decode_recv_frame(frame: &[u8], session_id: SessionId) -> PhantomPacket {
let mut packet = PhantomPacket::from_wire(frame).expect("decode test recv frame");
packet.header.session_id = session_id;
packet
}
#[tokio::test]
async fn v2_recv_routes_encrypted_app_data_through_recv_channel() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let frame = build_app_frame(&client_session, session_id, stream_id, 0, b"hello-v2");
let v2 = decode_recv_frame(&frame, session_id);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, mut deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(4);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
handle_packet(
v2,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
let (sid, received) = deliver_rx.recv().await.expect("delivery hand-off");
assert_eq!(sid, stream_id as u32);
assert_eq!(&received[..], b"hello-v2");
assert_eq!(
undelivered.load(Ordering::Acquire),
b"hello-v2".len() as u64
);
}
#[tokio::test]
async fn recv_path_caps_concurrent_streams_at_max_streams() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, _deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let attempts = MAX_STREAMS as u32 + 64;
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(attempts as usize + 16);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
for sid in 0..attempts {
let frame = build_app_frame_with_offset(
&client_session,
session_id,
sid as TransportStreamId,
sid, 0, b"x",
);
let v2 = decode_recv_frame(&frame, session_id);
handle_packet(
v2,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
}
assert!(
streams.len() <= MAX_STREAMS,
"recv path must cap concurrent receive streams at MAX_STREAMS ({MAX_STREAMS}); have {}",
streams.len()
);
}
#[tokio::test]
async fn forged_unencrypted_fin_does_not_close_a_stream() {
let session_id = fixed_session_id();
let (_client_session, server_session) = paired_sessions(session_id);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let mut handle = demux.register_stream(2, 8);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, _deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(4);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(64);
let obs = Observability::new(ObservabilityConfig::default());
let header = PacketHeader::new(session_id, 2, 0, PacketFlags::new(PacketFlags::FIN));
let forged = PhantomPacket::new(header, Vec::new());
handle_packet(
forged,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
assert!(
handle.rx.try_recv().is_err(),
"a forged unencrypted FIN must not close an open_stream() stream"
);
}
fn build_app_frame_on_path(
client_session: &InnerSession,
session_id: SessionId,
stream_id: TransportStreamId,
sequence: u32,
stream_offset: u32,
path_id: u8,
payload: &[u8],
) -> Vec<u8> {
let flag_bits = PacketFlags::RELIABLE | PacketFlags::ENCRYPTED;
let header = PacketHeader::new(
session_id,
stream_id,
sequence as u64,
PacketFlags::new(flag_bits),
)
.with_epoch(client_session.current_epoch())
.with_path_id(path_id);
let mut pt = Vec::with_capacity(4 + payload.len());
pt.extend_from_slice(&stream_offset.to_be_bytes());
pt.extend_from_slice(payload);
let ciphertext = client_session
.encrypt_packet(&header, &pt, &[])
.expect("encrypt_packet");
PhantomPacket::new(header, ciphertext).to_wire()
}
#[test]
fn send_path_id_starts_at_zero_then_bumps_per_migration() {
let session_id = fixed_session_id();
let (client_session, _server_session) = paired_sessions(session_id);
assert_eq!(client_session.current_send_path_id(), 0);
assert_eq!(client_session.next_migration_path_id(), 1);
assert_eq!(client_session.current_send_path_id(), 1);
assert_eq!(client_session.next_migration_path_id(), 2);
assert_eq!(client_session.current_send_path_id(), 2);
}
#[tokio::test]
async fn eps_slide_requires_aead_success() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (slide_tx, mut slide_rx) = mpsc::unbounded_channel();
server_session.set_cid_slide_tx(slide_tx);
let window_before = server_session.inbound_window_cids();
let stream_id: TransportStreamId = 1;
let frame =
build_app_frame_on_path(&client_session, session_id, stream_id, 0, 0, 1, b"forged");
let mut pkt = decode_recv_frame(&frame, session_id);
assert!(!pkt.payload.is_empty(), "ciphertext present to corrupt");
pkt.payload[0] ^= 0xFF;
run_recv(pkt, session_id, &server_session, &streams).await;
assert!(
slide_rx.try_recv().is_err(),
"a forged (AEAD-failing) packet must emit no CidSlide"
);
assert_eq!(
server_session.inbound_window_cids(),
window_before,
"the inbound CID window must not advance on a forged packet (slide is post-AEAD)"
);
}
use std::sync::atomic::AtomicBool;
#[derive(Default)]
struct ControlRecorder {
set_frame_phase: AtomicBool,
set_outbound_cid: std::sync::Mutex<Option<[u8; 8]>>,
has_migration_candidate: AtomicBool,
send_to_candidate: AtomicBool,
confirm_authenticated_source: AtomicBool,
promote_candidate: AtomicBool,
migrate: std::sync::Mutex<Option<String>>,
migrate_server: std::sync::Mutex<Option<String>>,
}
struct RecordingTransport {
rec: Arc<ControlRecorder>,
}
impl SessionTransport for RecordingTransport {
async fn send_bytes(&self, _data: &[u8]) -> Result<(), CoreError> {
Ok(())
}
async fn recv_bytes(&self) -> Result<Bytes, CoreError> {
Ok(Bytes::new())
}
fn set_frame_phase(&self, _phase: FramePhase) {
self.rec.set_frame_phase.store(true, Ordering::SeqCst);
}
fn set_outbound_cid(&self, cid: [u8; 8]) {
*self.rec.set_outbound_cid.lock().unwrap() = Some(cid);
}
fn has_migration_candidate(&self) -> bool {
self.rec
.has_migration_candidate
.store(true, Ordering::SeqCst);
true
}
async fn send_to_candidate(&self, _data: &[u8]) -> Result<bool, CoreError> {
self.rec.send_to_candidate.store(true, Ordering::SeqCst);
Ok(true)
}
fn confirm_authenticated_source(&self) {
self.rec
.confirm_authenticated_source
.store(true, Ordering::SeqCst);
}
fn promote_candidate(&self) -> bool {
self.rec.promote_candidate.store(true, Ordering::SeqCst);
true
}
async fn migrate(&self, local_addr: String) -> Result<(), CoreError> {
*self.rec.migrate.lock().unwrap() = Some(local_addr);
Ok(())
}
async fn migrate_server(&self, local_addr: String) -> Result<(), CoreError> {
*self.rec.migrate_server.lock().unwrap() = Some(local_addr);
Ok(())
}
}
#[test]
fn eps02_rotation_branch_is_role_correct() {
let session_id = fixed_session_id();
let (client, server) = paired_sessions(session_id);
let s_path_before = server.current_send_path_id();
let s_cid_before = server.current_outbound_cid();
let rec_s = Arc::new(ControlRecorder::default());
apply_eps02_peer_migration_rotation(&server, &RecordingTransport { rec: rec_s.clone() });
assert_ne!(
server.current_outbound_cid(),
s_cid_before,
"server must rotate its s2c CID on a peer (client) migration"
);
assert_eq!(
*rec_s.set_outbound_cid.lock().unwrap(),
Some(server.current_outbound_cid()),
"server stamps the rotated CID onto its transport"
);
assert_eq!(
server.current_send_path_id(),
s_path_before,
"server rotation is path-id-SILENT (bumping it would ping-pong the client)"
);
let c_path_before = client.current_send_path_id();
let c_cid_before = client.current_outbound_cid();
let rec_c = Arc::new(ControlRecorder::default());
apply_eps02_peer_migration_rotation(&client, &RecordingTransport { rec: rec_c.clone() });
assert_ne!(
client.current_outbound_cid(),
c_cid_before,
"client must rotate its c2s CID on a peer (server) migration"
);
assert_eq!(
*rec_c.set_outbound_cid.lock().unwrap(),
Some(client.current_outbound_cid()),
"client stamps the rotated CID onto its transport"
);
assert_eq!(
client.current_send_path_id(),
c_path_before + 1,
"client bumps its send path_id (slides the server's c2s demux window — no stranding)"
);
}
#[tokio::test]
async fn observed_transport_forwards_all_control_methods() {
let rec = Arc::new(ControlRecorder::default());
let observed = ObservedTransport::new(
RecordingTransport { rec: rec.clone() },
Observability::new(ObservabilityConfig::default()),
LegType::Udp,
);
observed.set_frame_phase(FramePhase::Established);
observed.set_outbound_cid([7u8; 8]);
assert!(observed.has_migration_candidate());
assert!(observed
.send_to_candidate(b"challenge")
.await
.expect("send_to_candidate"));
observed.confirm_authenticated_source();
assert!(observed.promote_candidate());
observed
.migrate("127.0.0.1:0".to_string())
.await
.expect("migrate");
observed
.migrate_server("127.0.0.1:0".to_string())
.await
.expect("migrate_server");
assert!(
rec.set_frame_phase.load(Ordering::SeqCst),
"set_frame_phase not forwarded"
);
assert_eq!(
*rec.set_outbound_cid.lock().unwrap(),
Some([7u8; 8]),
"set_outbound_cid not forwarded"
);
assert!(
rec.has_migration_candidate.load(Ordering::SeqCst),
"has_migration_candidate not forwarded"
);
assert!(
rec.send_to_candidate.load(Ordering::SeqCst),
"send_to_candidate not forwarded"
);
assert!(
rec.confirm_authenticated_source.load(Ordering::SeqCst),
"confirm_authenticated_source not forwarded"
);
assert!(
rec.promote_candidate.load(Ordering::SeqCst),
"promote_candidate not forwarded"
);
assert_eq!(
rec.migrate.lock().unwrap().as_deref(),
Some("127.0.0.1:0"),
"migrate not forwarded"
);
assert_eq!(
rec.migrate_server.lock().unwrap().as_deref(),
Some("127.0.0.1:0"),
"migrate_server not forwarded"
);
}
#[tokio::test]
async fn eps02_server_rotates_s2c_cid_on_client_migration() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
assert!(server_session.is_server(), "server side");
let s2c_cid_before = server_session.current_outbound_cid();
let stream_id: TransportStreamId = 1;
let frame =
build_app_frame_on_path(&client_session, session_id, stream_id, 0, 0, 1, b"migrated");
let pkt = decode_recv_frame(&frame, session_id);
run_recv(pkt, session_id, &server_session, &streams).await;
assert_ne!(
server_session.current_outbound_cid(),
s2c_cid_before,
"the server must rotate its server->client CID when the client migrates (EPS-02)"
);
}
#[tokio::test]
async fn eps02_client_rotates_c2s_on_server_migration() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
assert!(!client_session.is_server(), "client side");
let c2s_cid_before = client_session.current_outbound_cid();
let send_path_before = client_session.current_send_path_id();
let stream_id: TransportStreamId = 1;
let frame = build_app_frame_on_path(
&server_session,
session_id,
stream_id,
0,
0,
1,
b"srv-moved",
);
let pkt = decode_recv_frame(&frame, session_id);
run_recv(pkt, session_id, &client_session, &streams).await;
assert_ne!(
client_session.current_outbound_cid(),
c2s_cid_before,
"the client must rotate its c2s CID on detecting a server migration (EPS-02 closure)"
);
assert_ne!(
client_session.current_send_path_id(),
send_path_before,
"the client must bump its send path_id so the server slides its c2s window (no stranding)"
);
}
#[tokio::test]
async fn eps02_client_reflects_once_for_a_multi_step_server_migration() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
assert!(!client_session.is_server(), "client side");
let stream_id: TransportStreamId = 1;
let frame = build_app_frame_on_path(
&server_session,
session_id,
stream_id,
0,
0,
2,
b"srv-moved-2x",
);
let pkt = decode_recv_frame(&frame, session_id);
run_recv(pkt, session_id, &client_session, &streams).await;
assert_eq!(
client_session.current_send_path_id(),
1,
"client reflects ONCE (not d = 2) — its c2s rotation is decoupled from the server's migration count"
);
assert_eq!(
client_session.outbound_cid_index(),
1,
"outbound CID index stays 1:1 with send_path_id, so the server (which slides its c2s window by the CLIENT's path_id delta) routes the rotated c2s CID — no desync on a d>1 server migration"
);
}
#[test]
fn migration_path_id_never_collides_with_the_handshake_path() {
let session_id = fixed_session_id();
let (client_session, _server_session) = paired_sessions(session_id);
for _ in 0..600 {
assert_ne!(client_session.next_migration_path_id(), 0);
}
}
#[tokio::test]
async fn send_app_data_stamps_the_current_send_path_id() {
let session_id = fixed_session_id();
let (client_session, _server_session) = paired_sessions(session_id);
let (client_t, server_t) = ChannelTransport::pair();
let client_t = Arc::new(client_t);
assert!(
send_app_data(
&client_t,
&client_session,
session_id,
1,
b"pre-migration",
PacketFlags::RELIABLE,
Some(0),
)
.await
);
let wire = server_t.recv_bytes().await.unwrap();
let pkt = _server_session.parse_protected(&wire).unwrap();
assert_eq!(
pkt.header.path_id, 0,
"default send path is the implicit path 0"
);
assert_eq!(client_session.next_migration_path_id(), 1);
assert!(
send_app_data(
&client_t,
&client_session,
session_id,
1,
b"post-migration",
PacketFlags::RELIABLE,
Some(13),
)
.await
);
let wire2 = server_t.recv_bytes().await.unwrap();
let pkt2 = _server_session.parse_protected(&wire2).unwrap();
assert_eq!(
pkt2.header.path_id, 1,
"after migrate(), app data must carry the bumped send path_id"
);
}
#[tokio::test]
async fn app_data_on_non_validated_path_is_delivered_recv_relax() {
use crate::transport::path::PathStateKind;
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let frame = build_app_frame_on_path(
&client_session,
session_id,
stream_id,
0,
0, 7, b"on-new-path",
);
let frame = decode_recv_frame(&frame, session_id);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, mut deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(4);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
handle_packet(
frame,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
let (sid, received) =
tokio::time::timeout(std::time::Duration::from_secs(1), deliver_rx.recv())
.await
.expect("recv-relax must deliver promptly (no drop / hang)")
.expect("delivery channel open");
assert_eq!(sid, stream_id as u32);
assert_eq!(&received[..], b"on-new-path");
assert_eq!(
server_session.path_state(7),
Some(PathStateKind::Unvalidated),
"the new path id must be registered for a later challenge"
);
}
#[tokio::test]
async fn server_challenges_a_migration_candidate() {
use crate::api::udp_transport::UdpServerTransport;
use crate::transport::path::PathStateKind;
use crate::transport::phantom_udp::datagram::{push_datagram, FragmentAssembler};
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let server_sock = Arc::new(tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap());
let peer: std::net::SocketAddr = "127.0.0.1:9".parse().unwrap(); let cand_sock = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
let cand_addr = cand_sock.local_addr().unwrap();
let (tx, rx) = mpsc::channel(8);
let ust = Arc::new(UdpServerTransport::new(
server_sock.clone(),
peer,
[5u8; 8],
tx.clone(),
rx,
));
tx.send((Bytes::from(vec![0u8; 256]), cand_addr))
.await
.unwrap();
let _ = ust.recv_bytes().await.unwrap();
ust.confirm_authenticated_source();
assert!(
ust.has_migration_candidate(),
"new source must set a candidate"
);
let frame =
build_app_frame_on_path(&client_session, session_id, stream_id, 0, 0, 1, b"migrated");
let frame = decode_recv_frame(&frame, session_id);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, _deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
handle_packet(
frame,
session_id,
&server_session,
&streams,
&demux,
&ust,
&ust,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Udp,
)
.await;
assert_eq!(
server_session.path_state(1),
Some(PathStateKind::Validating),
"an unvalidated path on a candidate source must be challenged"
);
let mut buf = vec![0u8; 2048];
let (n, _from) = tokio::time::timeout(
std::time::Duration::from_secs(1),
cand_sock.recv_from(&mut buf),
)
.await
.expect("challenge must reach the candidate")
.unwrap();
let mut asm = FragmentAssembler::new();
let (_hdr, inner) = push_datagram(&mut asm, &buf[..n]).expect("decode envelope");
let inner = inner.expect("single-datagram challenge");
let pkt = client_session
.parse_protected(&inner)
.expect("inner packet");
assert!(
pkt.header.flags.contains(PacketFlags::PATH_VALIDATION),
"the candidate must receive a PATH_VALIDATION challenge"
);
assert_eq!(pkt.header.path_id, 1, "challenge must be on the new path");
}
#[tokio::test]
async fn server_challenges_a_passive_rebind_on_path_zero() {
use crate::api::udp_transport::UdpServerTransport;
use crate::transport::path::PathStateKind;
use crate::transport::phantom_udp::datagram::{push_datagram, FragmentAssembler};
use crate::transport::session::REBIND_VALIDATION_PATH_ID;
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let server_sock = Arc::new(tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap());
let peer: std::net::SocketAddr = "127.0.0.1:9".parse().unwrap(); let cand_sock = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
let cand_addr = cand_sock.local_addr().unwrap();
let (tx, rx) = mpsc::channel(8);
let ust = Arc::new(UdpServerTransport::new(
server_sock.clone(),
peer,
[5u8; 8],
tx.clone(),
rx,
));
tx.send((Bytes::from(vec![0u8; 256]), cand_addr))
.await
.unwrap();
let _ = ust.recv_bytes().await.unwrap();
ust.confirm_authenticated_source();
assert!(
ust.has_migration_candidate(),
"new source must set a candidate"
);
assert_eq!(
server_session.path_state(REBIND_VALIDATION_PATH_ID),
None,
"the rebind validation path must not exist before the rebind is observed"
);
let frame =
build_app_frame_on_path(&client_session, session_id, stream_id, 0, 0, 0, b"rebound");
let frame = decode_recv_frame(&frame, session_id);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, _deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
handle_packet(
frame,
session_id,
&server_session,
&streams,
&demux,
&ust,
&ust,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Udp,
)
.await;
assert_eq!(
server_session.path_state(REBIND_VALIDATION_PATH_ID),
Some(PathStateKind::Validating),
"a path-0 rebind on a candidate source must be challenged on the reserved id"
);
let mut buf = vec![0u8; 2048];
let (n, _from) = tokio::time::timeout(
std::time::Duration::from_secs(1),
cand_sock.recv_from(&mut buf),
)
.await
.expect("rebind challenge must reach the candidate")
.unwrap();
let mut asm = FragmentAssembler::new();
let (_hdr, inner) = push_datagram(&mut asm, &buf[..n]).expect("decode envelope");
let inner = inner.expect("single-datagram challenge");
let pkt = client_session
.parse_protected(&inner)
.expect("inner packet");
assert!(
pkt.header.flags.contains(PacketFlags::PATH_VALIDATION),
"the candidate must receive a PATH_VALIDATION challenge"
);
assert_eq!(
pkt.header.path_id, REBIND_VALIDATION_PATH_ID,
"the passive-rebind challenge must be stamped on the reserved validation path-id"
);
}
#[test]
fn migration_path_id_never_collides_with_the_rebind_validation_path() {
use crate::transport::session::REBIND_VALIDATION_PATH_ID;
let session_id = fixed_session_id();
let (client_session, _server_session) = paired_sessions(session_id);
for _ in 0..600 {
let id = client_session.next_migration_path_id();
assert_ne!(id, 0, "must never reuse the handshake path");
assert_ne!(
id, REBIND_VALIDATION_PATH_ID,
"must never reuse the reserved rebind validation path"
);
}
}
fn build_encrypted_ack(
acker_session: &InnerSession,
session_id: SessionId,
stream_id: TransportStreamId,
ack_header_seq: u32,
acked_seq: u32,
) -> Vec<u8> {
let sack = crate::transport::sack::Sack::from_received(&[acked_seq], 0)
.expect("single-seq sack")
.to_wire();
build_encrypted_ack_with_payload(
acker_session,
session_id,
stream_id,
ack_header_seq,
&sack,
)
}
fn build_encrypted_ack_with_payload(
acker_session: &InnerSession,
session_id: SessionId,
stream_id: TransportStreamId,
ack_header_seq: u32,
payload: &[u8],
) -> Vec<u8> {
let flag_bits = PacketFlags::ENCRYPTED | PacketFlags::ACK;
let header = PacketHeader::new(
session_id,
stream_id,
ack_header_seq as u64,
PacketFlags::new(flag_bits),
)
.with_epoch(acker_session.current_epoch());
let ct = acker_session
.encrypt_packet(&header, payload, &[])
.expect("encrypt ack");
PhantomPacket::new(header, ct).to_wire()
}
async fn run_recv(
pkt: PhantomPacket,
session_id: SessionId,
server_session: &Arc<InnerSession>,
streams: &Arc<DashMap<u32, Arc<TransportStream>>>,
) {
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let (deliver_tx, _deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(4);
let transport: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(64);
let obs = Observability::new(ObservabilityConfig::default());
handle_packet(
pkt,
session_id,
server_session,
streams,
&demux,
&transport,
&transport,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
}
async fn staged_pending_segment() -> (
Arc<TransportStream>,
Arc<DashMap<u32, Arc<TransportStream>>>,
u32,
) {
let stream_id: TransportStreamId = 1;
let stream = Arc::new(TransportStream::new(stream_id));
let seq = stream
.send_reliable(Bytes::from_static(b"reliable-payload"))
.await
.unwrap();
let _ = stream.poll_send(u64::MAX).await.expect("segment in-flight");
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
streams.insert(stream_id as u32, stream.clone());
(stream, streams, seq)
}
#[tokio::test]
async fn forged_plaintext_ack_does_not_retire_pending_segment() {
let session_id = fixed_session_id();
let (_client, server_session) = paired_sessions(session_id);
let (stream, streams, seq) = staged_pending_segment().await;
let stream_id: TransportStreamId = 1;
run_recv(
PhantomPacket::new(
PacketHeader::new(
session_id,
stream_id,
seq as u64,
PacketFlags::new(PacketFlags::ACK),
),
Vec::new(),
),
session_id,
&server_session,
&streams,
)
.await;
run_recv(
PhantomPacket::new(
PacketHeader::new(
session_id,
stream_id,
999,
PacketFlags::new(PacketFlags::ACK),
),
seq.to_be_bytes().to_vec(),
),
session_id,
&server_session,
&streams,
)
.await;
assert!(
stream.ack(seq).await.is_some(),
"a forged unauthenticated ACK must not retire the pending reliable segment"
);
}
#[tokio::test]
async fn forged_sack_neither_retires_nor_fast_retransmits() {
let session_id = fixed_session_id();
let (_client, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let stream = Arc::new(TransportStream::new(stream_id));
for _ in 0..6u32 {
stream
.send_reliable(Bytes::from_static(b"x"))
.await
.unwrap();
let _ = stream.poll_send(u64::MAX).await.expect("in-flight");
}
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
streams.insert(stream_id as u32, stream.clone());
assert_eq!(stream.pending_send_count().await, 6);
let forged_sack = crate::transport::sack::Sack::from_received(&[5], 0)
.expect("sack")
.to_wire();
run_recv(
PhantomPacket::new(
PacketHeader::new(
session_id,
stream_id,
4242,
PacketFlags::new(PacketFlags::ACK),
),
forged_sack, ),
session_id,
&server_session,
&streams,
)
.await;
assert_eq!(
stream.pending_send_count().await,
6,
"a forged unauthenticated SACK must not retire any segment (H1)"
);
assert!(
stream.poll_send(u64::MAX).await.is_none(),
"a forged SACK must not trigger a fast-retransmit (no segment flagged lost)"
);
}
#[tokio::test]
async fn loss_declaring_sack_does_not_feed_bbr_loss_at_detection() {
tokio::time::pause();
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let stream = Arc::new(TransportStream::new(stream_id));
let mut seg_size = 0u64;
for _ in 0..6u32 {
stream
.send_reliable(Bytes::from_static(b"x"))
.await
.unwrap();
let seg = stream.poll_send(u64::MAX).await.expect("in-flight");
seg_size = seg.data.len() as u64;
server_session.on_packet_sent(seg_size);
}
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
streams.insert(stream_id as u32, stream.clone());
let inflight_before = server_session.bandwidth_snapshot().inflight_bytes;
assert_eq!(inflight_before, 6 * seg_size, "six segments in flight");
let ack = build_encrypted_ack(&client_session, session_id, stream_id, 4242, 5);
let pkt = decode_recv_frame(&ack, session_id);
run_recv(pkt, session_id, &server_session, &streams).await;
let inflight_after = server_session.bandwidth_snapshot().inflight_bytes;
assert_eq!(
inflight_after,
inflight_before - seg_size,
"inflight must drop by ONLY the retired (acked) segment; double-feeding loss at \
SACK-gap detection would over-decrement it by the three flagged-lost segments (#7)"
);
}
#[tokio::test]
async fn authenticated_ack_retires_pending_segment() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let (stream, streams, seq) = staged_pending_segment().await;
let stream_id: TransportStreamId = 1;
let ack_header_seq = seq.wrapping_add(54_321);
let frame =
build_encrypted_ack(&client_session, session_id, stream_id, ack_header_seq, seq);
let ack_pkt = decode_recv_frame(&frame, session_id);
run_recv(ack_pkt, session_id, &server_session, &streams).await;
assert!(
stream.ack(seq).await.is_none(),
"an authenticated ACK must retire the acked pending segment"
);
}
#[tokio::test]
async fn authenticated_sack_retires_all_covered_segments_skipping_gap() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let stream = Arc::new(TransportStream::new(stream_id));
for i in 0..6u32 {
let seq = stream
.send_reliable(Bytes::from(format!("seg-{i}")))
.await
.unwrap();
assert_eq!(seq, i);
let _ = stream.poll_send(u64::MAX).await.expect("in-flight");
}
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
streams.insert(stream_id as u32, stream.clone());
assert_eq!(stream.pending_send_count().await, 6);
let sack = crate::transport::sack::Sack::from_received(&[0, 1, 2, 4, 5], 777)
.expect("sack")
.to_wire();
let frame = build_encrypted_ack_with_payload(
&client_session,
session_id,
stream_id,
9_999, &sack,
);
let ack_pkt = decode_recv_frame(&frame, session_id);
run_recv(ack_pkt, session_id, &server_session, &streams).await;
assert_eq!(
stream.pending_send_count().await,
1,
"SACK must retire all five covered segments at once"
);
for retired in [0u32, 1, 2, 4, 5] {
assert!(
stream.ack(retired).await.is_none(),
"seq {retired} should have been retired by the SACK"
);
}
assert!(
stream.ack(3).await.is_some(),
"the gap segment 3 must remain buffered"
);
}
#[tokio::test]
async fn malformed_sack_is_dropped_and_retires_nothing() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let (stream, streams, seq) = staged_pending_segment().await;
let stream_id: TransportStreamId = 1;
let bad_payload = vec![0u8; 5];
let frame = build_encrypted_ack_with_payload(
&client_session,
session_id,
stream_id,
1234,
&bad_payload,
);
let ack_pkt = decode_recv_frame(&frame, session_id);
run_recv(ack_pkt, session_id, &server_session, &streams).await;
assert!(
stream.ack(seq).await.is_some(),
"a malformed SACK must retire nothing — the pending segment stays buffered"
);
}
#[tokio::test]
async fn receiver_emits_sack_with_populated_ack_delay() {
let session_id = fixed_session_id();
let (sender_session, receiver_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let data_seq = 7u32;
let data_pkt = decode_recv_frame(
&build_app_frame(
&sender_session,
session_id,
stream_id,
data_seq,
b"hello-reliable",
),
session_id,
);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, _deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(4);
let transport: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(64);
let obs = Observability::new(ObservabilityConfig::default());
handle_packet(
data_pkt,
session_id,
&receiver_session,
&streams,
&demux,
&transport,
&transport,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
let ack_frame = transport
.rx
.lock()
.await
.recv()
.await
.expect("an ACK frame must have been emitted");
let ack_pkt = sender_session
.parse_protected(&ack_frame)
.expect("parse emitted ack");
assert!(ack_pkt.header.flags.contains(PacketFlags::ACK));
let plain = sender_session
.decrypt_packet(&ack_pkt.header, &ack_pkt.payload, &[])
.expect("decrypt emitted ack");
let sack = crate::transport::sack::Sack::from_wire(&plain).expect("decode emitted sack");
assert_eq!(sack.largest_acked, data_seq, "SACK must ack the data seq");
assert!(sack.acks(data_seq));
let _ = sack.ack_delay_us;
}
#[tokio::test]
async fn ack_with_wrong_session_id_is_dropped() {
let session_id = fixed_session_id();
let (_client, server_session) = paired_sessions(session_id);
let (stream, streams, seq) = staged_pending_segment().await;
let stream_id: TransportStreamId = 1;
let wrong_id = SessionId::from_bytes([0x11; 32]);
run_recv(
PhantomPacket::new(
PacketHeader::new(
wrong_id,
stream_id,
seq as u64,
PacketFlags::new(PacketFlags::ACK),
),
Vec::new(),
),
session_id,
&server_session,
&streams,
)
.await;
assert!(
stream.ack(seq).await.is_some(),
"an ACK for a different session id must not retire the segment"
);
}
#[tokio::test]
async fn v2_recv_drops_unencrypted_non_empty_post_handshake_payload() {
let session_id = fixed_session_id();
let (_, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 2;
let bad_header = PacketHeader::new(
session_id,
stream_id,
0,
PacketFlags::new(PacketFlags::RELIABLE), );
let bad_packet = PhantomPacket::new(bad_header, b"leaked-cleartext".to_vec());
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, mut deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(4);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
handle_packet(
bad_packet,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
assert!(
deliver_rx.try_recv().is_err(),
"unencrypted post-handshake payload must NOT be handed off for delivery"
);
assert_eq!(undelivered.load(Ordering::Acquire), 0);
}
#[tokio::test]
async fn v2_recv_handles_coalesced_bundle_and_routes_each_subpayload() {
use crate::transport::packet_coalescer::{CoalescerConfig, PacketCoalescer};
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let mut coalescer = PacketCoalescer::new(CoalescerConfig::default());
coalescer.push(b"alpha");
coalescer.push(b"bravo");
coalescer.push(b"charlie");
let bundle = coalescer.flush().expect("bundle");
let stream_id: TransportStreamId = 3;
let flag_bits = PacketFlags::ENCRYPTED | PacketFlags::COALESCED;
let header = PacketHeader::new(session_id, stream_id, 0, PacketFlags::new(flag_bits))
.with_epoch(client_session.current_epoch());
let ciphertext = client_session
.encrypt_packet(&header, &bundle, &[])
.expect("encrypt bundle");
let v2 = PhantomPacket::new(header, ciphertext);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, mut deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(4);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
handle_packet(
v2,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
let (sa, a) = deliver_rx.recv().await.expect("alpha");
let (sb, b) = deliver_rx.recv().await.expect("bravo");
let (sc, c) = deliver_rx.recv().await.expect("charlie");
assert_eq!(
(sa, sb, sc),
(stream_id as u32, stream_id as u32, stream_id as u32)
);
assert_eq!(&a[..], b"alpha");
assert_eq!(&b[..], b"bravo");
assert_eq!(&c[..], b"charlie");
assert_eq!(undelivered.load(Ordering::Acquire), (5 + 5 + 7) as u64);
}
#[tokio::test]
async fn delivery_preserves_order_across_coalesced_then_normal_frame() {
use crate::transport::packet_coalescer::{CoalescerConfig, PacketCoalescer};
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let build_bundle = |seq: u32, items: &[&[u8]]| -> PhantomPacket {
let mut coalescer = PacketCoalescer::new(CoalescerConfig::default());
for it in items {
coalescer.push(it);
}
let bundle = coalescer.flush().expect("bundle");
let flag_bits = PacketFlags::ENCRYPTED | PacketFlags::COALESCED;
let h = PacketHeader::new(
session_id,
stream_id,
seq as u64,
PacketFlags::new(flag_bits),
)
.with_epoch(client_session.current_epoch());
let ct = client_session
.encrypt_packet(&h, &bundle, &[])
.expect("encrypt bundle");
PhantomPacket::new(h, ct)
};
let coalesced = build_bundle(0, &[b"A", b"B", b"C"]);
let normal = build_bundle(1, &[b"D"]);
let (demux, _ctrl) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, mut deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(8);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
for pkt in [coalesced, normal] {
handle_packet(
pkt,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
}
let mut got: Vec<Bytes> = Vec::new();
while let Ok((_sid, b)) = deliver_rx.try_recv() {
got.push(b);
}
let seen: Vec<&[u8]> = got.iter().map(|b| &b[..]).collect();
assert_eq!(seen, vec![&b"A"[..], b"B", b"C", b"D"]);
}
#[tokio::test]
async fn reliable_frames_delivered_in_sequence_order_despite_arrival_order() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let f0 = decode_recv_frame(
&build_app_frame(&client_session, session_id, stream_id, 0, b"zero"),
session_id,
);
let f1 = decode_recv_frame(
&build_app_frame(&client_session, session_id, stream_id, 1, b"one"),
session_id,
);
let (demux, _ctrl) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, mut deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(8);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
for pkt in [f1, f0] {
handle_packet(
pkt,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
}
let mut got: Vec<Bytes> = Vec::new();
while let Ok((_sid, b)) = deliver_rx.try_recv() {
got.push(b);
}
let seen: Vec<&[u8]> = got.iter().map(|b| &b[..]).collect();
assert_eq!(
seen,
vec![&b"zero"[..], b"one"],
"reliable data must be delivered in sequence order, not arrival order"
);
}
#[tokio::test]
async fn reliable_delivery_skips_control_frame_sequence_holes() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 1;
let a = decode_recv_frame(
&build_app_frame_with_offset(&client_session, session_id, stream_id, 0, 0, b"a"),
session_id,
);
let b = decode_recv_frame(
&build_app_frame_with_offset(&client_session, session_id, stream_id, 2, 1, b"b"),
session_id,
);
let (demux, _ctrl) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, mut deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (ack_a, ack_b) = mpsc::channel::<Vec<u8>>(8);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: ack_a,
rx: Mutex::new(ack_b),
});
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
for pkt in [a, b] {
handle_packet(
pkt,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
}
let mut got: Vec<Bytes> = Vec::new();
while let Ok((_sid, x)) = deliver_rx.try_recv() {
got.push(x);
}
let seen: Vec<&[u8]> = got.iter().map(|b| &b[..]).collect();
assert_eq!(
seen,
vec![&b"a"[..], b"b"],
"reliable data must deliver in stream_offset order, skipping control-frame \
sequence holes (not stall on them)"
);
}
#[tokio::test]
async fn peer_ignoring_flow_control_trips_delivery_hard_cap_and_closes_session() {
let session_id = fixed_session_id();
let (client_inner, server_inner) = paired_sessions(session_id);
let (client_t, server_t) = ChannelTransport::pair();
let client_t = Arc::new(client_t);
let server = PhantomSession::from_accepted_server_session(
"flooder".to_string(),
server_t,
server_inner,
);
let drain_t = client_t.clone();
let drainer = tokio::spawn(async move { while drain_t.recv_bytes().await.is_ok() {} });
let payload = vec![0xABu8; 64 * 1024];
let mut seq: u32 = 0;
let mut torn_down = false;
for _ in 0..4000 {
if server.connection_state() == ConnectionState::Closed {
torn_down = true;
break;
}
let flag_bits = PacketFlags::RELIABLE | PacketFlags::ENCRYPTED;
let header = PacketHeader::new(session_id, 1, seq as u64, PacketFlags::new(flag_bits))
.with_epoch(client_inner.current_epoch());
let mut pt = Vec::with_capacity(4 + payload.len());
pt.extend_from_slice(&seq.to_be_bytes());
pt.extend_from_slice(&payload);
let ct = client_inner
.encrypt_packet(&header, &pt, &[])
.expect("encrypt");
let packet = PhantomPacket::new(header, ct);
let wire = client_inner
.protect_packet(&packet)
.expect("header protection");
match tokio::time::timeout(
std::time::Duration::from_secs(5),
client_t.send_bytes(&wire),
)
.await
{
Ok(Ok(())) => {}
_ => {
torn_down = true;
break;
}
}
seq = seq.wrapping_add(1);
tokio::task::yield_now().await;
}
assert!(
torn_down,
"a peer flooding past the delivery hard cap must get its session torn down"
);
let mut closed = false;
for _ in 0..200 {
if server.connection_state() == ConnectionState::Closed {
closed = true;
break;
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
drainer.abort();
assert!(
closed,
"session state must be Closed after the hard cap trips"
);
}
#[tokio::test]
async fn bbr_on_ack_drives_pacer_rate() {
use crate::transport::bandwidth_estimator::DeliverySample;
use std::time::{Duration, Instant};
let session_id = fixed_session_id();
let (client_session, _server_session) = paired_sessions(session_id);
assert!(!client_session.pacer().is_enabled());
let now = Instant::now();
for i in 0..16 {
let sent_at = now - Duration::from_millis(20 + i * 5);
let acked_at = now - Duration::from_millis(i * 5);
let sample = DeliverySample {
delivered_bytes: 0,
sent_at,
acked_at,
packet_bytes: 1500,
is_app_limited: false,
ack_delay_us: 100,
};
client_session.on_packet_sent(1500);
let _ = client_session.on_packet_acked(sample);
}
let snap = client_session.bandwidth_snapshot();
assert!(
snap.pacing_rate_bps > 0,
"expected pacing_rate to be non-zero, got {}",
snap.pacing_rate_bps,
);
assert_eq!(client_session.pacer().rate(), snap.pacing_rate_bps);
}
#[tokio::test]
async fn flow_control_window_update_round_trip() {
use crate::transport::stream::INITIAL_STREAM_WINDOW;
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let stream_id: TransportStreamId = 9;
let server_streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let server_stream = Arc::new(TransportStream::new(stream_id));
server_streams.insert(stream_id as u32, server_stream.clone());
let client_stream = Arc::new(TransportStream::new(stream_id));
let drain = INITIAL_STREAM_WINDOW - 1000;
assert!(client_stream.try_consume_send_window(drain));
assert_eq!(client_stream.peer_send_window(), 1000);
let consumed = INITIAL_STREAM_WINDOW / 2 + 1;
let credit = server_stream
.record_app_consumed(consumed)
.expect("threshold crossed → credit granted");
server_stream.stage_window_update_credit(credit);
let (out_tx, mut out_rx) = mpsc::channel::<Vec<u8>>(4);
let (back_tx, back_rx) = mpsc::channel::<Vec<u8>>(4);
let server_outbound: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: out_tx,
rx: Mutex::new(back_rx),
});
let _keep = back_tx;
flush_pending_window_updates(
&server_outbound,
&server_session,
session_id,
&server_streams,
)
.await;
let frame = tokio::time::timeout(std::time::Duration::from_millis(100), out_rx.recv())
.await
.expect("expected a WINDOW_UPDATE frame")
.expect("channel open");
let pv2 = client_session.parse_protected(&frame).unwrap();
assert!(pv2.header.flags.contains(PacketFlags::WINDOW_UPDATE));
let pt = client_session
.decrypt_packet(&pv2.header, &pv2.payload, &[])
.expect("decrypt WINDOW_UPDATE");
assert_eq!(pt.len(), 4);
let announced = u32::from_be_bytes([pt[0], pt[1], pt[2], pt[3]]);
assert_eq!(
announced, credit,
"WINDOW_UPDATE carries the relative credit (bytes consumed since last update)"
);
assert!(
out_rx.try_recv().is_err(),
"exactly one WINDOW_UPDATE must be emitted"
);
flush_pending_window_updates(
&server_outbound,
&server_session,
session_id,
&server_streams,
)
.await;
assert!(
out_rx.try_recv().is_err(),
"no spurious second WINDOW_UPDATE after the credit was already flushed"
);
client_stream.apply_peer_window_update(announced);
assert_eq!(client_stream.peer_send_window(), 1000 + credit);
}
#[tokio::test]
async fn priority_scheduler_drains_higher_priority_stream_first() {
let session_id = fixed_session_id();
let (client_session, _server_session) = paired_sessions(session_id);
let (tx_a, mut rx_a) = mpsc::channel::<Vec<u8>>(32);
let (tx_b, rx_b) = mpsc::channel::<Vec<u8>>(32);
let transport: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: tx_a,
rx: Mutex::new(rx_b),
});
let _keep = tx_b;
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let low = Arc::new(TransportStream::new(11));
low.set_priority(1);
low.send_reliable(Bytes::from_static(b"L0")).await.unwrap();
low.send_reliable(Bytes::from_static(b"L1")).await.unwrap();
low.send_reliable(Bytes::from_static(b"L2")).await.unwrap();
streams.insert(11, low);
let hi = Arc::new(TransportStream::new(22));
hi.set_priority(100);
hi.send_reliable(Bytes::from_static(b"H0")).await.unwrap();
hi.send_reliable(Bytes::from_static(b"H1")).await.unwrap();
hi.send_reliable(Bytes::from_static(b"H2")).await.unwrap();
streams.insert(22, hi);
drain_streams_priority_ordered(&transport, &client_session, session_id, &streams).await;
let mut order: Vec<&'static str> = Vec::new();
while let Ok(frame) =
tokio::time::timeout(std::time::Duration::from_millis(50), rx_a.recv()).await
{
let bytes = match frame {
Some(b) => b,
None => break,
};
let v2 = _server_session.parse_protected(&bytes).unwrap();
let plaintext = _server_session
.decrypt_packet(&v2.header, &v2.payload, &[])
.expect("decrypt");
let tag: &'static str = match &plaintext[4..] {
b"H0" => "H0",
b"H1" => "H1",
b"H2" => "H2",
b"L0" => "L0",
b"L1" => "L1",
b"L2" => "L2",
other => panic!("unexpected payload {:?}", other),
};
order.push(tag);
}
let first_low = order
.iter()
.position(|s| s.starts_with('L'))
.unwrap_or(order.len());
let last_high = order.iter().rposition(|s| s.starts_with('H')).unwrap();
assert!(
last_high < first_low,
"strict priority violated: order = {:?}",
order
);
}
#[tokio::test]
async fn v2_recv_echoes_path_validation_challenge_back_as_response() {
let session_id = fixed_session_id();
let (client_session, server_session) = paired_sessions(session_id);
let path_id: u8 = 7;
let payload = [0xDEu8; crate::transport::path::PATH_CHALLENGE_LEN];
let flag_bits = PacketFlags::ENCRYPTED | PacketFlags::PATH_VALIDATION;
let header = PacketHeader::new(session_id, 0, 0, PacketFlags::new(flag_bits))
.with_epoch(client_session.current_epoch())
.with_path_id(path_id);
let ciphertext = client_session
.encrypt_packet(&header, &payload, &[])
.expect("encrypt challenge");
let v2 = PhantomPacket::new(header, ciphertext);
let (demux, _ctrl_rx) = StreamDemultiplexer::new(16);
let demux = Arc::new(demux);
let streams: Arc<DashMap<u32, Arc<TransportStream>>> = Arc::new(DashMap::new());
let (deliver_tx, _deliver_rx) = mpsc::unbounded_channel::<(u32, Bytes)>();
let undelivered = AtomicU64::new(0);
let (echo_tx, mut echo_rx) = mpsc::channel::<Vec<u8>>(4);
let (back_tx, back_rx) = mpsc::channel::<Vec<u8>>(4);
let transport_send: Arc<ChannelTransport> = Arc::new(ChannelTransport {
tx: echo_tx,
rx: Mutex::new(back_rx),
});
let _back_tx_keepalive = back_tx;
let mut ack_buf = Vec::with_capacity(256);
let obs = Observability::new(ObservabilityConfig::default());
handle_packet(
v2,
session_id,
&server_session,
&streams,
&demux,
&transport_send,
&transport_send,
&deliver_tx,
&undelivered,
&mut ack_buf,
&obs,
LegType::Tcp,
)
.await;
let echo_bytes =
tokio::time::timeout(std::time::Duration::from_millis(200), echo_rx.recv())
.await
.expect("echo should arrive")
.expect("channel open");
let echo_v2 = client_session.parse_protected(&echo_bytes).unwrap();
assert!(echo_v2.header.flags.contains(PacketFlags::PATH_VALIDATION));
assert_eq!(echo_v2.header.path_id, path_id);
}
#[tokio::test]
async fn zero_rtt_early_data_full_round_trip() {
let server_hs = HandshakeServer::new().unwrap();
let server_pinned_key = server_hs.verifying_key().clone();
let client_ip: std::net::IpAddr = "127.0.0.1".parse().unwrap();
let (c1, s1) = ChannelTransport::pair();
let phase1_session =
PhantomSession::connect_with_transport("test:9000", c1, server_pinned_key.clone());
let hello_bytes = s1.recv_bytes().await.unwrap();
let ch = borsh::from_slice::<ClientHello>(&hello_bytes).unwrap();
let retry = match server_hs.process_client_hello(&ch, 0, client_ip) {
HandshakeResponse::Retry(r) => r,
_ => panic!("expected Retry"),
};
s1.send_bytes(&ServerReply::Retry(retry).to_wire().unwrap())
.await
.unwrap();
let next = s1.recv_bytes().await.unwrap();
let ch2 = borsh::from_slice::<ClientHello>(&next).unwrap();
match server_hs.process_client_hello(&ch2, 0, client_ip) {
HandshakeResponse::Success(sh, _session, _) => {
s1.send_bytes(&ServerReply::Hello(sh).to_wire().unwrap())
.await
.unwrap();
}
_ => panic!("expected Success"),
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
assert_eq!(
phase1_session.connection_state(),
ConnectionState::Connected
);
let hint = phase1_session
.resumption_hint()
.await
.expect("phase 1 produced a resumption hint");
let hint = (
<[u8; 32]>::try_from(hint.session_id.as_slice()).expect("session_id is 32 bytes"),
<[u8; 32]>::try_from(hint.resumption_secret.as_slice())
.expect("resumption_secret is 32 bytes"),
);
let early_payload = b"zero-rtt application bytes".to_vec();
let (c2, s2) = ChannelTransport::pair();
let phase2_session = PhantomSession::connect_with_resumption(
"test:9000",
c2,
server_pinned_key.clone(),
hint,
early_payload.clone(),
)
.expect("early_data is within the size cap");
let hello_bytes = s2.recv_bytes().await.unwrap();
let ch3 = borsh::from_slice::<ClientHello>(&hello_bytes).unwrap();
assert!(
ch3.early_data.is_some(),
"phase 2 hello carries sealed 0-RTT early-data"
);
match server_hs.process_client_hello(&ch3, 0, client_ip) {
HandshakeResponse::Success(sh, _session, early_data) => {
assert_eq!(early_data.as_deref(), Some(&early_payload[..]));
assert!(sh.early_data_accepted);
s2.send_bytes(&ServerReply::Hello(sh).to_wire().unwrap())
.await
.unwrap();
}
_ => {
panic!("expected Success with accepted early-data — the resumption ticket is fresh")
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
assert_eq!(
phase2_session.connection_state(),
ConnectionState::Connected
);
assert_eq!(
phase2_session.early_data_accepted().await,
Some(true),
"client must see the server accepted its 0-RTT early-data"
);
drop((s1, s2));
}
#[tokio::test]
async fn connect_pinned_with_resumption_rejects_malformed_hint() {
let server_hs = HandshakeServer::new().unwrap();
let pinned = server_hs.verifying_key().to_bytes();
let bad_hint = ResumptionHint {
session_id: vec![0u8; 5], resumption_secret: vec![0u8; 32],
};
let err = connect_pinned_with_resumption(
"127.0.0.1".to_string(),
9,
pinned,
bad_hint,
Vec::new(),
)
.await
.expect_err("a 5-byte session_id must be rejected");
assert!(
matches!(err, CoreError::ValidationError(_)),
"expected ValidationError, got {err:?}"
);
}
}