use crate::bytes::{Bytes, BytesMut};
use crate::cx::Cx;
use crate::net::atp::protocol::quic_frames::QuicFrame;
use std::task::{Context as TaskContext, Poll};
use super::connection::{
NativeQuicConnection, NativeQuicConnectionConfig, NativeQuicConnectionError,
};
use super::streams::{StreamId, StreamRole};
use super::transport::{PacketNumberSpace, QuicConnectionState};
macro_rules! apitrace {
($($arg:tt)*) => {
if std::env::var_os("ATP_QUIC_TRACE").is_some() {
eprintln!("[atp-quic-api] {}", format!($($arg)*));
}
};
}
pub const DEFAULT_MAX_PACKET_BYTES: usize = 1350;
const PUMP_ITERATION_CAP: usize = 16_384;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct QuicPathStats {
pub smoothed_rtt_micros: Option<u64>,
pub latest_rtt_micros: Option<u64>,
pub rttvar_micros: Option<u64>,
pub congestion_window_bytes: u64,
pub bytes_in_flight: u64,
pub pto_count: u32,
pub packets_acked: u64,
pub packets_lost: u64,
pub loss_rate: f64,
}
#[derive(Debug)]
pub struct QuicConnection {
inner: NativeQuicConnection,
role: StreamRole,
next_app_pn: u64,
}
impl QuicConnection {
fn from_config(mut config: NativeQuicConnectionConfig, role: StreamRole) -> Self {
config.role = role;
Self {
inner: NativeQuicConnection::new(config),
role,
next_app_pn: 0,
}
}
#[must_use]
pub fn client(config: NativeQuicConnectionConfig) -> Self {
apitrace!("event=conn_new role=client");
Self::from_config(config, StreamRole::Client)
}
#[must_use]
pub fn server(config: NativeQuicConnectionConfig) -> Self {
apitrace!("event=conn_new role=server");
Self::from_config(config, StreamRole::Server)
}
#[must_use]
pub fn role(&self) -> StreamRole {
self.role
}
#[must_use]
pub fn state(&self) -> QuicConnectionState {
self.inner.state()
}
#[must_use]
pub fn can_send_app_data(&self) -> bool {
self.inner.can_send_1rtt()
}
#[must_use]
pub fn inner(&self) -> &NativeQuicConnection {
&self.inner
}
pub fn begin_handshake(&mut self, cx: &Cx) -> Result<(), NativeQuicConnectionError> {
apitrace!("event=begin_handshake role={:?}", self.role);
self.inner.begin_handshake(cx)
}
pub fn mark_handshake_keys_available(
&mut self,
cx: &Cx,
) -> Result<(), NativeQuicConnectionError> {
self.inner.on_handshake_keys_available(cx)
}
pub fn mark_app_keys_available(&mut self, cx: &Cx) -> Result<(), NativeQuicConnectionError> {
self.inner.on_1rtt_keys_available(cx)
}
pub fn record_verified_server_identity(&mut self) {
apitrace!("event=server_identity_recorded role={:?}", self.role);
self.inner.record_verified_server_identity();
}
pub fn confirm_handshake(&mut self, cx: &Cx) -> Result<(), NativeQuicConnectionError> {
let result = self.inner.on_handshake_confirmed(cx);
match &result {
Ok(()) => apitrace!("event=handshake_confirmed role={:?}", self.role),
Err(err) => apitrace!(
"event=handshake_confirm_failed role={:?} err={err:?}",
self.role
),
}
result
}
pub fn send_datagram(
&mut self,
cx: &Cx,
payload: Bytes,
) -> Result<(), NativeQuicConnectionError> {
if !self.inner.can_send_1rtt() {
apitrace!(
"event=datagram_send_reject reason=not_established role={:?}",
self.role
);
return Err(NativeQuicConnectionError::InvalidState(
"send_datagram requires an established 1-RTT connection",
));
}
let len = payload.len();
let result = self.inner.send_datagram(cx, payload);
if result.is_ok() {
apitrace!("event=datagram_queued role={:?} len={len}", self.role);
}
result
}
#[must_use]
pub fn recv_datagram(&mut self) -> Option<Bytes> {
self.inner.recv_datagram()
}
pub fn poll_recv_datagram(
&mut self,
cx: &Cx,
task_cx: &mut TaskContext<'_>,
) -> Poll<Result<Bytes, NativeQuicConnectionError>> {
self.inner.poll_recv_datagram(cx, task_cx)
}
#[must_use]
pub fn pending_datagram_count(&self) -> usize {
self.inner.pending_datagram_count()
}
#[must_use]
pub fn datagrams_sent(&self) -> u64 {
self.inner.datagrams_sent()
}
#[must_use]
pub fn datagrams_received(&self) -> u64 {
self.inner.datagrams_received()
}
pub fn open_control_stream(&mut self, cx: &Cx) -> Result<StreamId, NativeQuicConnectionError> {
let id = self.inner.open_local_bidi(cx)?;
apitrace!(
"event=control_stream_open role={:?} stream={}",
self.role,
id.0
);
Ok(id)
}
pub fn write_control(
&mut self,
cx: &Cx,
stream: StreamId,
data: Bytes,
fin: bool,
) -> Result<(), NativeQuicConnectionError> {
self.inner.write_stream_bytes(cx, stream, data, fin)
}
pub fn read_control(
&mut self,
cx: &Cx,
stream: StreamId,
max: usize,
) -> Result<Bytes, NativeQuicConnectionError> {
self.inner.read_stream_bytes(cx, stream, max)
}
pub fn is_control_eof(&self, stream: StreamId) -> Result<bool, NativeQuicConnectionError> {
self.inner.is_stream_read_eof(stream)
}
#[must_use]
pub fn path_stats(&self) -> QuicPathStats {
let transport = self.inner.transport();
let rtt = transport.rtt();
QuicPathStats {
smoothed_rtt_micros: rtt.smoothed_rtt_micros(),
latest_rtt_micros: rtt.latest_rtt_micros(),
rttvar_micros: rtt.rttvar_micros(),
congestion_window_bytes: transport.congestion_window_bytes(),
bytes_in_flight: transport.bytes_in_flight(),
pto_count: transport.pto_count(),
packets_acked: transport.packets_acked_total(),
packets_lost: transport.packets_lost_total(),
loss_rate: transport.packet_loss_rate(),
}
}
pub fn begin_close(
&mut self,
cx: &Cx,
now_micros: u64,
app_error_code: u64,
) -> Result<(), NativeQuicConnectionError> {
apitrace!(
"event=begin_close role={:?} code={app_error_code}",
self.role
);
self.inner.begin_close(cx, now_micros, app_error_code)
}
}
pub fn establish_loopback(
cx: &Cx,
client: &mut QuicConnection,
server: &mut QuicConnection,
) -> Result<(), NativeQuicConnectionError> {
client.begin_handshake(cx)?;
server.begin_handshake(cx)?;
client.mark_handshake_keys_available(cx)?;
server.mark_handshake_keys_available(cx)?;
client.mark_app_keys_available(cx)?;
server.mark_app_keys_available(cx)?;
server.confirm_handshake(cx)?;
client.confirm_handshake(cx)?;
apitrace!("event=loopback_established");
Ok(())
}
pub fn pump_app_data(
cx: &Cx,
from: &mut QuicConnection,
to: &mut QuicConnection,
max_packet_bytes: usize,
now_micros: u64,
) -> Result<usize, NativeQuicConnectionError> {
let frames: Vec<QuicFrame> =
from.inner
.generate_frames(cx, PacketNumberSpace::ApplicationData, max_packet_bytes)?;
if frames.is_empty() {
return Ok(0);
}
let mut payload = BytesMut::new();
for frame in &frames {
frame.encode(&mut payload)?;
}
let packet_number = from.next_app_pn;
from.next_app_pn = from.next_app_pn.saturating_add(1);
to.inner.process_packet_payload(
cx,
PacketNumberSpace::ApplicationData,
packet_number,
&payload,
now_micros,
)?;
apitrace!(
"event=pump frames={} bytes={} pn={packet_number}",
frames.len(),
payload.len()
);
Ok(frames.len())
}
pub fn pump_until_idle(
cx: &Cx,
from: &mut QuicConnection,
to: &mut QuicConnection,
max_packet_bytes: usize,
now_micros: u64,
) -> Result<usize, NativeQuicConnectionError> {
let mut total = 0;
for _ in 0..PUMP_ITERATION_CAP {
let moved = pump_app_data(cx, from, to, max_packet_bytes, now_micros)?;
if moved == 0 {
return Ok(total);
}
total += moved;
}
Err(NativeQuicConnectionError::InvalidState(
"pump_until_idle exceeded its iteration cap without draining",
))
}
#[cfg(test)]
mod tests {
#![allow(clippy::cast_possible_truncation)]
use super::*;
fn test_cx() -> Cx<crate::cx::cap::All> {
Cx::for_testing()
}
fn pair() -> (QuicConnection, QuicConnection) {
let cfg = NativeQuicConnectionConfig::default();
(QuicConnection::client(cfg), QuicConnection::server(cfg))
}
fn established_pair(cx: &Cx) -> (QuicConnection, QuicConnection) {
let (mut client, mut server) = pair();
client.record_verified_server_identity();
establish_loopback(cx, &mut client, &mut server).expect("loopback establishes");
(client, server)
}
#[test]
fn loopback_reaches_established_on_both_sides() {
let cx = test_cx();
let (client, server) = established_pair(&cx);
assert_eq!(client.state(), QuicConnectionState::Established);
assert_eq!(server.state(), QuicConnectionState::Established);
assert!(client.can_send_app_data());
assert!(server.can_send_app_data());
assert_eq!(client.role(), StreamRole::Client);
assert_eq!(server.role(), StreamRole::Server);
}
#[test]
fn client_handshake_fails_closed_without_verified_identity() {
let cx = test_cx();
let (mut client, mut server) = pair();
let err = establish_loopback(&cx, &mut client, &mut server)
.expect_err("client must fail closed without verified identity");
assert!(
matches!(err, NativeQuicConnectionError::Tls(_)),
"expected a TLS fail-closed error, got {err:?}"
);
assert_ne!(client.state(), QuicConnectionState::Established);
}
#[test]
fn datagram_roundtrip_exact_and_fifo() {
let cx = test_cx();
let (mut client, mut server) = established_pair(&cx);
let payloads: [&[u8]; 3] = [b"first-symbol", b"second", b"third-datagram-payload"];
for p in payloads {
client
.send_datagram(&cx, Bytes::copy_from_slice(p))
.expect("queue datagram");
}
let moved = pump_until_idle(
&cx,
&mut client,
&mut server,
DEFAULT_MAX_PACKET_BYTES,
1000,
)
.expect("pump");
assert!(
moved >= payloads.len(),
"expected >= {} frames",
payloads.len()
);
for expected in payloads {
let got = server.recv_datagram().expect("a datagram arrived");
assert_eq!(got.as_ref(), expected, "exact payload, FIFO order");
}
assert!(server.recv_datagram().is_none(), "no extra datagrams");
assert_eq!(client.datagrams_sent(), payloads.len() as u64);
assert_eq!(server.datagrams_received(), payloads.len() as u64);
}
#[test]
fn datagram_send_before_established_is_rejected() {
let cx = test_cx();
let (mut client, _server) = pair();
let err = client
.send_datagram(&cx, Bytes::from_static(b"too early"))
.expect_err("must reject before established");
assert!(matches!(err, NativeQuicConnectionError::InvalidState(_)));
}
#[test]
fn oversize_datagram_is_rejected_fail_closed() {
let cx = test_cx();
let (mut client, _server) = established_pair(&cx);
let huge = Bytes::from(vec![0xABu8; 4096]);
let err = client
.send_datagram(&cx, huge)
.expect_err("oversize datagram must be rejected");
assert!(matches!(
err,
NativeQuicConnectionError::DatagramTooLarge { .. }
));
}
#[test]
fn control_stream_roundtrip_multi_packet_reassembly() {
let cx = test_cx();
let (mut client, mut server) = established_pair(&cx);
let stream = client
.open_control_stream(&cx)
.expect("open control stream");
let body: Vec<u8> = (0..2048u32).map(|i| (i % 251) as u8).collect();
client
.write_control(&cx, stream, Bytes::copy_from_slice(&body), true)
.expect("write control bytes + FIN");
let moved = pump_until_idle(&cx, &mut client, &mut server, 256, 2000).expect("pump");
assert!(
moved > 1,
"large payload should span multiple packets, moved {moved}"
);
let mut received = Vec::new();
loop {
let chunk = server
.read_control(&cx, stream, 4096)
.expect("read control");
if chunk.is_empty() {
break;
}
received.extend_from_slice(&chunk);
}
assert_eq!(received, body, "stream bytes reassembled in order");
assert!(
server.is_control_eof(stream).expect("eof query"),
"FIN should be observed after consuming all bytes"
);
}
#[test]
fn poll_recv_datagram_pending_then_ready() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::Wake;
struct CountingWaker(AtomicUsize);
impl Wake for CountingWaker {
fn wake(self: Arc<Self>) {
self.0.fetch_add(1, Ordering::SeqCst);
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let cx = test_cx();
let (mut client, mut server) = established_pair(&cx);
let counter = Arc::new(CountingWaker(AtomicUsize::new(0)));
let waker = counter.clone().into();
let mut task_cx = TaskContext::from_waker(&waker);
assert!(matches!(
server.poll_recv_datagram(&cx, &mut task_cx),
Poll::Pending
));
client
.send_datagram(&cx, Bytes::from_static(b"wakeup"))
.expect("queue datagram");
pump_until_idle(
&cx,
&mut client,
&mut server,
DEFAULT_MAX_PACKET_BYTES,
3000,
)
.expect("pump");
assert!(
counter.0.load(Ordering::SeqCst) >= 1,
"arrival must wake the registered task"
);
match server.poll_recv_datagram(&cx, &mut task_cx) {
Poll::Ready(Ok(got)) => assert_eq!(got.as_ref(), b"wakeup"),
other => panic!("expected Ready(Ok), got {other:?}"),
}
}
#[test]
fn path_stats_are_exposed() {
let cx = test_cx();
let (client, _server) = established_pair(&cx);
let stats = client.path_stats();
assert!(stats.congestion_window_bytes > 0);
assert_eq!(stats.bytes_in_flight, 0);
assert_eq!(stats.pto_count, 0);
assert_eq!(stats.packets_acked, 0);
assert_eq!(stats.packets_lost, 0);
assert_eq!(stats.loss_rate, 0.0);
}
#[test]
fn graceful_close_transitions_out_of_established() {
let cx = test_cx();
let (mut client, _server) = established_pair(&cx);
client.begin_close(&cx, 5000, 0).expect("begin close");
assert_ne!(client.state(), QuicConnectionState::Established);
}
}