#[path = "../src/config.rs"] mod config; #[path = "../src/datagram.rs"] mod datagram; #[path = "../src/protocol/mod.rs"] mod protocol; #[path = "../src/stats.rs"] mod stats; #[path = "../src/transport/mod.rs"] mod transport; use std::{collections::HashMap, sync::Arc, time::Duration}; use base64::Engine; use bytes::Bytes; use quinn::{ClientConfig as QuinnClientConfig, Endpoint, ServerConfig as QuinnServerConfig}; use rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer}; use tokio::time::timeout; use transport::{ quic::{QuicClientConnector, QuicServer}, TransportError, TransportSession, }; const SECRET: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; // A test-only localhost certificate, valid for ten years. Keeping the DER // here avoids adding a certificate-generation crate to the application. const CERT_DER_B64: &str = "MIIDHDCCAgSgAwIBAgIUJnI/VlHMWu6j0dXr/t1OmMQrLgowDQYJKoZIhvcNAQELBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDkxNDIwNDEyMFoXDTM2MDkxMTIwNDEyMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm4d4r5Py4GdfQrLit2oytLv20L+y7oM/VTDzzJfFMPmewOh0XyS0BI4lyRbCgOuUh686UPvF/BU8uJq24OO7wt/2CrE3FfwYX7BDjIWMuFevHuv896imabbX4IJ7lzVugI3862QSW09QSEQk3ggYs3z/F16cnSRZm9aIURgdPQsLmMzOwcPtDGwxp2jf0Rky0kFRxhUnqfIWko4hny7GjWL8VG0WzYJY6aUevjPlN/Oq/akr9xUAr2QJenEesP77do4X31LIZmsvB6GqKwaSQ0AbwNHW7rDiaE4dkZMIk5l95YgYnrzyOC8IhUwaSZt0wODxk/euXPYr+RFJv2FuOQIDAQABo2YwZDAdBgNVHQ4EFgQUhViBEXLkp7YUVZaWzLi2SfRmfmYwHwYDVR0jBBgwFoAUhViBEXLkp7YUVZaWzLi2SfRmfmYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBAEFC7da0bK78IUL0k6lZ82pqFImX2bNteMh/r1IJG0xqB/O2dOLUDHybpF7HToommALgRu6zhlT0d1eHg0cMYEgrKyq4BAAXGHqOt3OdKKL5nAWmI04dlWsrG9aTU3hTQhe0++hnTbVm+YJnYAp+ufOxx858FeothjLWu1Lr81Nm01FnmZbXcFno3nosyl1GPxSRVZl2XH9NsfMSZJUbSU6mHXTO1pEH6mjYmgD362CeJc+0DOzevdLsDozxUoWmdEuqlldauESW6XVK0WxRSJ1i2/B/mtYViHr5R/j5NYgG5STnNnwaNwjYK+0AhxvTtH3jAp0Bsluw4d+xKdsV/YI="; const KEY_DER_B64: &str = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbh3ivk/LgZ19CsuK3ajK0u/bQv7Lugz9VMPPMl8Uw+Z7A6HRfJLQEjiXJFsKA65SHrzpQ+8X8FTy4mrbg47vC3/YKsTcV/BhfsEOMhYy4V68e6/z3qKZpttfggnuXNW6AjfzrZBJbT1BIRCTeCBizfP8XXpydJFmb1ohRGB09CwuYzM7Bw+0MbDGnaN/RGTLSQVHGFSep8haSjiGfLsaNYvxUbRbNgljppR6+M+U386r9qSv3FQCvZAl6cR6w/vt2jhffUshmay8HoaorBpJDQBvA0dbusOJoTh2RkwiTmX3liBievPI4LwiFTBpJm3TA4PGT965c9iv5EUm/YW45AgMBAAECggEAMs0EtCD4t6i8Ol6Q5EUZa+RrgZLkILkZEjwLnjvCqrUWUD99Z2hnLoe5ut2myZJpcQpg23vpluXe8BOFAZ8RQN2DkcmByzoNQhAieD9VEA+AAXMmrURWmvggulIUiXbn8bCvOLn2rwGQoRI1EpsFanc9IUaDlGZ/qIHiA88DGeFDuchbcgClpXAX4lBzqhLKv/CCIhhGxmyWc1RPlZUef7uTO1Zh7q2HxnO5iBKobvPfMq5VfvzE4rXeSQIHtW+yJFLU2sNjaYry2JTLEqGIZhWMROmzlkW8x/cj0GvjLbY4ltkIpLNSb+WRbY3zZNhQ81kosBZtiYRj3vgr/FzvSwKBgQDQ/dxghryGDqIIOfyZ59+2tpPrrLX4vy7tmAQg6zJIiQVHMZ/Av60QvY7lA0dvDfQmfJuNzTOXtwhgnYkHbg6jlFqpT7P1CZQOzbF0UuFCNvwzE/jgCEKUExckDa+Du7g/VOglB9B4XKqJnQLpZkzz4Hwmuvz4oUtZJCl796HTMwKBgQC+gyPKrNp6AthxZ1gAu+aIjf18vgGZjANQx3cRFQl5BqSyTwZpxIfuiKrT+sU65N2VCWpGH6oTzGruaXBXzG0rlNCMAL5XaJqJehvLAnejUqmXhyi8ouH174Q4azDngPOCFaSjuYJwpX4nUXwMExqs9hwkH9pV3tn6qzEpKJA44wKBgBa+SalHgGSmWwPCK7ckALiFjftrxDHZvGWm0WwzrDkwkB57TANR2KeluVM61B9FyXgWv2bjGeY8yPCatDrWw0kR7/vU7KSR8kbaI6GhE1p9WVzFo6PdwkETAsWtp5gSaa6asti0zP8MNpLbP2He/CjeGSSt3LHB+80rnkgbXsT5AoGAVaTytCxc0zjwQJrqzvmpu33JlQh+A58T5n6lIkLIzWWVKKuqAGZfjXZXBZDYlY9JpbRj2D5n4Rqmf6Na47183ni6ABZde0IFJwSD9hGX4w5IO1yBjbvTxRH16L8XbxtjXPmJ2CiPYUCko9xX3b3A4jyJjhvbtXrHl47aPNyFaW0CgYEAxDOcW5r5pXXkCF/mIqSklGa1jDO9TeMavLo1AguHaUlJEQU8DgGM9Flx3senFASN2EjwwTB3aScVuxe3X29LeZnCNsuTIzCVGpSNyFLUjZVU0fRaf6R9JIcI7dVoflPAPCdrqBVkezK5Eyu8f3NCwnVD+zv+iWA+RE1zUyKy4sM="; fn endpoint_and_client( max_datagram: usize, ) -> (Endpoint, QuinnClientConfig, CertificateDer<'static>) { let cert_bytes = base64::engine::general_purpose::STANDARD .decode(CERT_DER_B64) .unwrap(); let key_bytes = base64::engine::general_purpose::STANDARD .decode(KEY_DER_B64) .unwrap(); let cert = CertificateDer::from(cert_bytes); let key = PrivatePkcs8KeyDer::from(key_bytes); let mut server_config = QuinnServerConfig::with_single_cert(vec![cert.clone()], key.into()).unwrap(); let mut transport_config = quinn::TransportConfig::default(); transport_config.keep_alive_interval(Some(Duration::from_secs(1))); transport_config.datagram_send_buffer_size(max_datagram * 8); server_config.transport_config(Arc::new(transport_config)); let endpoint = Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap(); let mut roots = rustls::RootCertStore::empty(); roots.add(cert.clone()).unwrap(); let client = QuinnClientConfig::with_root_certificates(Arc::new(roots)).unwrap(); (endpoint, client, cert) } fn client_config(port: u16, secret: &str) -> config::ClientConfig { config::Config::from_str(&format!( "[client]\nserver = \"localhost\"\nid = \"test\"\nsecret = \"{secret}\"\n[transport.quic]\nport = {port}\n[transport.auto]\nprobe_timeout_ms = 2000\n[network]\nmax_datagram_size = 1200\n" )) .unwrap() .into_client() .unwrap() } #[tokio::test] async fn authenticated_datagram_round_trip_and_limit() { let (endpoint, client_tls, _cert) = endpoint_and_client(1200); let mut clients = HashMap::new(); clients.insert( "test".to_owned(), base64::engine::general_purpose::STANDARD .decode(SECRET) .unwrap(), ); let server = QuicServer::from_endpoint(endpoint, clients, 1200, Duration::from_secs(2)); let port = server.local_addr().unwrap().port(); let client = QuicClientConnector::with_client_config(client_config(port, SECRET), client_tls); let server_task = tokio::spawn(async move { server.accept().await }); let client_session = client.connect_session().await.unwrap(); let server_session = server_task.await.unwrap().unwrap(); client_session .send_datagram(Bytes::from_static(b"opaque")) .await .unwrap(); assert_eq!( timeout(Duration::from_secs(2), server_session.recv_datagram()) .await .unwrap() .unwrap(), Bytes::from_static(b"opaque") ); assert!(matches!( client_session .send_datagram(Bytes::from(vec![0; 1201])) .await, Err(TransportError::Oversized { maximum: 1200 }) )); client_session.close().await.unwrap(); server_session.close().await.unwrap(); } #[tokio::test] async fn bad_secret_is_rejected_as_authentication_error() { let (endpoint, client_tls, _cert) = endpoint_and_client(1200); let mut clients = HashMap::new(); clients.insert( "test".to_owned(), base64::engine::general_purpose::STANDARD .decode(SECRET) .unwrap(), ); let server = QuicServer::from_endpoint(endpoint, clients, 1200, Duration::from_secs(2)); let port = server.local_addr().unwrap().port(); let wrong = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; let client = QuicClientConnector::with_client_config(client_config(port, wrong), client_tls); let server_task = tokio::spawn(async move { server.accept().await }); let result = client.connect_session().await; assert!(matches!( result, Err(TransportError::AuthenticationRejected(_)) )); assert!(matches!( server_task.await.unwrap(), Err(TransportError::AuthenticationRejected(_)) )); } #[tokio::test] async fn certificate_hostname_mismatch_is_not_accepted() { let (endpoint, client_tls, _cert) = endpoint_and_client(1200); let mut clients = HashMap::new(); clients.insert( "test".to_owned(), base64::engine::general_purpose::STANDARD .decode(SECRET) .unwrap(), ); let server = QuicServer::from_endpoint(endpoint, clients, 1200, Duration::from_secs(2)); let port = server.local_addr().unwrap().port(); let server_task = tokio::spawn(async move { server.accept().await }); // The connector's configured hostname is localhost, which matches. This // test changes it to an unrelated name while retaining the localhost root. let mut mismatch = client_config(port, SECRET); mismatch.client.server = "not-localhost".to_owned(); let mismatch = QuicClientConnector::with_client_config(mismatch, client_tls); let result = mismatch.connect_session().await; assert!(result.is_err()); server_task.abort(); }