//! QUIC connection lifecycle management. //! //! Provides helpers for creating endpoints, connecting to peers, and accepting connections. use std::net::SocketAddr; use wzp_proto::TransportError; /// Create a QUIC endpoint bound to the given address. /// /// If `server_config` is provided, the endpoint can accept incoming connections. pub fn create_endpoint( bind_addr: SocketAddr, server_config: Option, ) -> Result { let endpoint = if let Some(sc) = server_config { quinn::Endpoint::server(sc, bind_addr)? } else { quinn::Endpoint::client(bind_addr)? }; Ok(endpoint) } /// Connect to a remote peer using the given client configuration. pub async fn connect( endpoint: &quinn::Endpoint, addr: SocketAddr, server_name: &str, config: quinn::ClientConfig, ) -> Result { let connecting = endpoint.connect_with(config, addr, server_name).map_err(|e| { TransportError::Internal(format!("connect error: {e}")) })?; let connection = connecting.await.map_err(|e| { TransportError::Internal(format!("connection failed: {e}")) })?; Ok(connection) } /// Create an IPv6-only QUIC endpoint with `IPV6_V6ONLY=1`. /// /// Tries `[::]:preferred_port` first (same port as the IPv4 signal /// endpoint — allowed on Linux/Android when the AFs differ and /// V6ONLY is set). Falls back to `[::]:0` (OS-assigned) if the /// preferred port is already taken. /// /// Must be called from within a tokio runtime (quinn needs the /// async runtime handle for its I/O driver). pub fn create_ipv6_endpoint( preferred_port: u16, server_config: Option, ) -> Result { use socket2::{Domain, Protocol, Socket, Type}; use std::net::{Ipv6Addr, SocketAddrV6}; let sock = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP)) .map_err(|e| TransportError::Internal(format!("ipv6 socket: {e}")))?; // Critical: IPv6-only so this socket never intercepts IPv4. // On Android some kernels default to V6ONLY=1 anyway, but we // set it explicitly for cross-platform consistency. sock.set_only_v6(true) .map_err(|e| TransportError::Internal(format!("set_only_v6: {e}")))?; sock.set_reuse_address(true) .map_err(|e| TransportError::Internal(format!("set_reuse_address: {e}")))?; // Try the preferred port (same as IPv4 signal endpoint), fall // back to ephemeral if the OS rejects it. let bind_addr = SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, preferred_port, 0, 0); if let Err(e) = sock.bind(&bind_addr.into()) { if preferred_port != 0 { tracing::debug!( preferred_port, error = %e, "ipv6 bind to preferred port failed, falling back to ephemeral" ); let fallback = SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0); sock.bind(&fallback.into()) .map_err(|e| TransportError::Internal(format!("ipv6 bind fallback: {e}")))?; } else { return Err(TransportError::Internal(format!("ipv6 bind: {e}"))); } } sock.set_nonblocking(true) .map_err(|e| TransportError::Internal(format!("set_nonblocking: {e}")))?; let udp_socket: std::net::UdpSocket = sock.into(); let runtime = quinn::default_runtime() .ok_or_else(|| TransportError::Internal("no async runtime for ipv6 endpoint".into()))?; let endpoint = quinn::Endpoint::new( quinn::EndpointConfig::default(), server_config, udp_socket, runtime, ) .map_err(|e| TransportError::Internal(format!("ipv6 endpoint: {e}")))?; Ok(endpoint) } /// Accept the next incoming connection on an endpoint. pub async fn accept(endpoint: &quinn::Endpoint) -> Result { let incoming = endpoint .accept() .await .ok_or(TransportError::ConnectionLost)?; let connection = incoming.await.map_err(|e| { TransportError::Internal(format!("accept failed: {e}")) })?; Ok(connection) }