/// QUIC relay client — used when P2P hole punch fails use anyhow::{anyhow, Result}; use quinn::Connection; use std::net::SocketAddr; use std::time::Duration; use tokio::time::timeout; use uuid::Uuid; use crate::network::quic::open_endpoint; use crate::network::relay_selector::DEFAULT_RELAY_PORT; /// Connect to a relay server's QUIC endpoint pub async fn connect_relay( relay_url: &str, room_id: Uuid, access_token: &str, ) -> Result { let (host, port) = parse_relay_url(relay_url); let relay_addr: SocketAddr = { let addrs: Vec = tokio::net::lookup_host(format!("{}:{}", host, port)) .await? .collect(); addrs.into_iter().next().ok_or_else(|| anyhow!("relay DNS failed"))? }; let ep = open_endpoint("0.0.0.0:0".parse()?)?; tracing::info!("Connecting to relay at {}", relay_addr); let connecting = ep.connect(relay_addr, &host)?; let conn = timeout(Duration::from_secs(10), connecting).await .map_err(|_| anyhow!("relay connection timed out"))? .map_err(|e| anyhow!("relay QUIC error: {}", e))?; // Send auth handshake on a unidirectional stream using write_chunk let mut stream = conn.open_uni().await?; let handshake = serde_json::json!({ "token": access_token, "room_id": room_id.to_string(), }); let msg = serde_json::to_vec(&handshake)?; let len = (msg.len() as u32).to_be_bytes(); stream.write_chunk(bytes::Bytes::copy_from_slice(&len)).await .map_err(|e| anyhow!("{}", e))?; stream.write_chunk(bytes::Bytes::from(msg)).await .map_err(|e| anyhow!("{}", e))?; let _ = stream.finish(); let _ = stream.stopped().await; tracing::info!("Relay handshake sent for room {}", room_id); Ok(conn) } fn parse_relay_url(url: &str) -> (String, u16) { let cleaned = url .trim_start_matches("https://") .trim_start_matches("http://") .trim_start_matches("quic://"); if let Some((host, port_str)) = cleaned.rsplit_once(':') { if let Ok(port) = port_str.parse::() { return (host.to_string(), port); } } (cleaned.to_string(), DEFAULT_RELAY_PORT) }