use pb_mapper_core::{checksum::Credential, config::ResolvedAddrs};
pub use pb_mapper_protocol::transport::RelayTransport;
use pb_mapper_protocol::{
command::{MessageSerializer, PbConnRequest, PbConnResponse, PbErrorResponse, QuicEndpoint},
secure::ClientHeaderSession,
transport::CONNECT_TIMEOUT,
};
use serde::Serialize;
use std::{io, net::SocketAddr};
#[cfg(feature = "relay-quic")]
pub(crate) use quic::CarrierId;
#[cfg(not(feature = "relay-quic"))]
#[derive(Clone, Copy, Debug)]
pub(crate) enum CarrierId {}
#[derive(Debug, snafu::Snafu)]
pub(crate) enum ConnectError {
#[snafu(display("{source}"))]
Transport { source: std::io::Error },
#[snafu(display("{}: {}", response.code, response.message))]
Rejected { response: PbErrorResponse },
}
impl From<std::io::Error> for ConnectError {
fn from(source: std::io::Error) -> Self {
Self::Transport { source }
}
}
impl From<ConnectError> for std::io::Error {
fn from(error: ConnectError) -> Self {
match error {
ConnectError::Transport { source } => source,
error @ ConnectError::Rejected { .. } => {
Self::new(std::io::ErrorKind::PermissionDenied, error)
}
}
}
}
pub const QUIC_UNAVAILABLE: &str = "quic_unavailable";
const REQUEST_DECODE_FAILED: &str = "request_decode_failed";
fn quic_unavailable() -> ConnectError {
ConnectError::Rejected {
response: PbErrorResponse {
code: QUIC_UNAVAILABLE.into(),
message: "the relay does not offer QUIC; select TCP to use it".into(),
retryable: false,
server_time: 0,
},
}
}
pub(crate) async fn discover(
addr: SocketAddr,
credential: Credential,
) -> Result<(SocketAddr, QuicEndpoint), ConnectError> {
let mut tcp = tokio::net::TcpStream::connect(addr).await?;
tcp.set_nodelay(true)?;
let relay_addr = tcp.peer_addr()?;
let session = ClientHeaderSession::new_v2(&credential).map_err(io::Error::other)?;
let response = session
.exchange(
&mut tcp,
&PbConnRequest::QuicEndpoint
.encode()
.map_err(io::Error::other)?,
CONNECT_TIMEOUT,
)
.await
.map_err(io::Error::other)?;
match PbConnResponse::decode(&response).map_err(io::Error::other)? {
PbConnResponse::QuicEndpoint(Some(endpoint))
if endpoint.port != 0
&& !endpoint.certificate.is_empty()
&& endpoint.certificate.len() <= 4096 =>
{
Ok((relay_addr, endpoint))
}
PbConnResponse::QuicEndpoint(Some(endpoint)) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"the relay advertised an unusable QUIC endpoint: UDP port {}, \
a {}-byte certificate",
endpoint.port,
endpoint.certificate.len()
),
)
.into()),
PbConnResponse::QuicEndpoint(None) => Err(quic_unavailable()),
PbConnResponse::Error(error) if error.code == REQUEST_DECODE_FAILED => {
Err(quic_unavailable())
}
PbConnResponse::Error(response) => Err(ConnectError::Rejected { response }),
other => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unexpected QUIC discovery response: {other:?}"),
)
.into()),
}
}
pub(crate) async fn offered_quic_port(
addrs: &ResolvedAddrs,
credential: Credential,
) -> Result<Option<u16>, ConnectError> {
match crate::addr::race_candidates(addrs.as_slice(), CONNECT_TIMEOUT, move |addr| {
discover(addr, credential)
})
.await
{
Ok((_, endpoint)) => Ok(Some(endpoint.port)),
Err(ConnectError::Rejected { response }) if response.code == QUIC_UNAVAILABLE => Ok(None),
Err(error) => Err(error),
}
}
#[cfg(feature = "relay-quic")]
pub(crate) mod quic;
#[derive(Clone, Debug, Serialize)]
#[non_exhaustive]
pub struct RelayDiagnostics {
pub transport: RelayTransport,
pub connections_created: u64,
pub streams_opened: u64,
pub streams_reused: u64,
pub last_error: Option<String>,
pub connections: Vec<QuicConnectionDiagnostics>,
}
impl RelayDiagnostics {
pub(crate) fn empty(transport: RelayTransport) -> Self {
Self {
transport,
connections_created: 0,
streams_opened: 0,
streams_reused: 0,
last_error: None,
connections: Vec::new(),
}
}
}
#[derive(Clone, Debug, Serialize)]
#[non_exhaustive]
pub struct QuicConnectionDiagnostics {
pub connection_id: u64,
pub peer_addr: std::net::SocketAddr,
pub pooled: bool,
pub rtt_us: u64,
pub congestion_window: u64,
pub mtu: u16,
pub sent_packets: u64,
pub lost_packets: u64,
pub congestion_events: u64,
pub sent_bytes: u64,
pub received_bytes: u64,
}