use std::collections::BTreeMap; use std::sync::Arc; use arc_swap::ArcSwap; use base64::Engine as _; use bytes::Bytes; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio_util::sync::CancellationToken; use super::serve_connection; use crate::config::{ ProxyConfig, WebCarrier, WebCarriers, WebClientIpSource, WebRuntimeConfig, WebRuntimeDecoy, WebRuntimeProfile, WebRuntimeVhost, WebSecretMode, WebStaticAsset, WebStaticSite, }; use crate::maestro::generation::test_runtime_generation; use crate::web::frame::{self, FrameType}; use crate::web::manager::{ CloseOperationSelector, ControlError, SessionDetail, SessionFilter, SessionListRequest, SessionRefError, WebProcessRuntime, }; #[path = "legacy_tests.rs"] mod legacy_tests; #[path = "negotiation_tests.rs"] mod negotiation_tests; // Reload-stability tests for session-owned timeout policy. #[path = "session_policy_tests.rs"] mod session_policy_tests; // Runtime control integration stays separate from carrier protocol scenarios. #[path = "control_tests.rs"] mod control_tests; const TEST_CARRIER_DEADLINES_SECS: [u64; 4] = [3, 5, 8, 12]; pub(super) fn runtime_config(capability: [u8; 32], carrier: WebCarrier) -> ProxyConfig { runtime_config_with_carriers(capability, carrier, false, true, Arc::from([carrier])) } pub(super) fn negotiation_runtime_config( capability: [u8; 32], carrier: WebCarrier, carrier_learning: bool, carriers: Arc<[WebCarrier]>, ) -> ProxyConfig { runtime_config_with_carriers(capability, carrier, true, carrier_learning, carriers) } fn runtime_config_with_carriers( capability: [u8; 32], carrier: WebCarrier, carrier_negotiation_enabled: bool, carrier_learning: bool, carriers: Arc<[WebCarrier]>, ) -> ProxyConfig { runtime_config_with_carriers_and_deadlines( capability, carrier, carrier_negotiation_enabled, carrier_learning, carriers, TEST_CARRIER_DEADLINES_SECS, ) } pub(super) fn negotiation_runtime_config_with_deadlines( capability: [u8; 32], carrier: WebCarrier, carrier_learning: bool, carriers: Arc<[WebCarrier]>, carrier_negotiation_deadlines_secs: [u64; 4], ) -> ProxyConfig { runtime_config_with_carriers_and_deadlines( capability, carrier, true, carrier_learning, carriers, carrier_negotiation_deadlines_secs, ) } fn runtime_config_with_carriers_and_deadlines( capability: [u8; 32], carrier: WebCarrier, carrier_negotiation_enabled: bool, carrier_learning: bool, carriers: Arc<[WebCarrier]>, carrier_negotiation_deadlines_secs: [u64; 4], ) -> ProxyConfig { let profile = Arc::new(WebRuntimeProfile { host: "proxy.example.com".to_string(), public_addr: "203.0.113.10:443".parse().unwrap(), user: "alice".to_string(), secret_mode: WebSecretMode::Plain, carrier, carrier_negotiation_enabled, carrier_learning, carriers: Arc::clone(&carriers), carrier_negotiation_deadlines_secs, capability, key_fingerprint: "0000000000000000".to_string(), max_sessions: 4, max_streams: 16, max_streams_per_session: 4, }); let mut assets = BTreeMap::new(); assets.insert( "/index.html".to_string(), WebStaticAsset { body: Bytes::from_static(b"decoy"), content_type: "text/html; charset=utf-8", etag: "\"test\"".to_string(), }, ); let site = Arc::new(WebStaticSite { assets, index: "index.html".to_string(), }); let vhost = Arc::new(WebRuntimeVhost { host: "proxy.example.com".to_string(), decoy: WebRuntimeDecoy::StaticDirectory(Arc::clone(&site)), decoy_header_secs: 1, profiles: vec![Arc::clone(&profile)], }); let mut vhosts = BTreeMap::new(); vhosts.insert("proxy.example.com".to_string(), vhost); vhosts.insert( "other.example.com".to_string(), Arc::new(WebRuntimeVhost { host: "other.example.com".to_string(), decoy: WebRuntimeDecoy::StaticDirectory(site), decoy_header_secs: 1, profiles: Vec::new(), }), ); let mut config = ProxyConfig::default(); config.web.enabled = true; config.web.carrier = carrier; config.web.carriers = if carrier_negotiation_enabled { WebCarriers::Enabled(carriers.to_vec()) } else { WebCarriers::Disabled }; config.web.carrier_learning = carrier_learning; config.web.timeouts.carrier_negotiation_deadlines_secs = carrier_negotiation_deadlines_secs; config.web.limits.max_bootstraps_per_ip = 1; config.web.timeouts.shutdown_secs = 1; config.web.runtime = Some(Arc::new(WebRuntimeConfig { vhosts, profiles: vec![profile], })); config } pub(super) async fn request( listener: &TcpListener, runtime: &Arc, request: Vec, ) -> Vec { let addr = listener.local_addr().unwrap(); let (accepted, client) = tokio::join!(listener.accept(), TcpStream::connect(addr)); let (server, peer) = accepted.unwrap(); let mut client = client.unwrap(); let permit = runtime.try_http_connection().unwrap(); let task = tokio::spawn(serve_connection( server, peer, WebClientIpSource::XForwardedFor, Arc::from(["127.0.0.1/32".parse().unwrap()]), Arc::clone(runtime), CancellationToken::new(), permit, )); client.write_all(&request).await.unwrap(); let mut response = Vec::new(); client.read_to_end(&mut response).await.unwrap(); task.await.unwrap(); response } pub(super) fn split_response(response: &[u8]) -> (&[u8], &[u8]) { let separator = response .windows(4) .position(|window| window == b"\r\n\r\n") .unwrap(); (&response[..separator], &response[separator + 4..]) } pub(super) fn response_header<'a>(headers: &'a [u8], name: &str) -> &'a str { std::str::from_utf8(headers) .unwrap() .lines() .filter_map(|line| line.split_once(':')) .find_map(|(header, value)| header.eq_ignore_ascii_case(name).then_some(value.trim())) .unwrap() } #[tokio::test] async fn https_carrier_bootstraps_and_closes_one_session() { let capability = [7u8; 32]; let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https)); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability); let root = format!( "GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n" ) .into_bytes(); let root_response = request(&listener, &runtime, root).await; let (root_headers, root_body) = split_response(&root_response); assert!(root_headers.starts_with(b"HTTP/1.1 200")); let root_body = std::str::from_utf8(root_body).unwrap(); let bootstrap = root_body .split_once("bootstrap=\"") .and_then(|(_, suffix)| suffix.split_once('"')) .map(|(token, _)| token) .unwrap(); assert_eq!(bootstrap.len(), 43); let hello = frame::encode(FrameType::Hello, 0, &[1]); let mut wrong_host = format!( "POST /api/v1/session HTTP/1.1\r\nHost: other.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", hello.len() ) .into_bytes(); wrong_host.extend_from_slice(&hello); let wrong_host_response = request(&listener, &runtime, wrong_host).await; assert!(wrong_host_response.starts_with(b"HTTP/1.1 404")); let mut create = format!( "POST /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", hello.len() ) .into_bytes(); let create_retry = create.clone(); create.extend_from_slice(&hello); let mut create_retry = create_retry; create_retry.extend_from_slice(&hello); let create_response = request(&listener, &runtime, create).await; let (create_headers, create_body) = split_response(&create_response); assert!(create_headers.starts_with(b"HTTP/1.1 200")); assert_eq!(response_header(create_headers, "x-carrier-mode"), "https"); assert_eq!(create_body, frame::encode(FrameType::Welcome, 0, &[])); let session = response_header(create_headers, "x-session-token"); assert_eq!(session.len(), 43); let replacement = test_runtime_generation(2, runtime_config(capability, WebCarrier::HttpsLanes)); active_runtime.store(Arc::clone(&replacement)); tokio::time::sleep(std::time::Duration::from_millis(1100)).await; let retry_response = request(&listener, &runtime, create_retry).await; let (retry_headers, retry_body) = split_response(&retry_response); assert!(retry_headers.starts_with(b"HTTP/1.1 200")); assert_eq!(response_header(retry_headers, "x-session-token"), session); assert_eq!(response_header(retry_headers, "x-carrier-mode"), "https"); assert_eq!(retry_body, frame::encode(FrameType::Welcome, 0, &[])); let next_root = format!( "GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n" ) .into_bytes(); let next_root_response = request(&listener, &runtime, next_root).await; let (_, next_root_body) = split_response(&next_root_response); assert!( next_root_body .windows(11) .any(|value| value == b"bootstrap=\"") ); assert!( next_root_body .windows(b"const negotiationEnabled=false".len()) .any(|value| value == b"const negotiationEnabled=false") ); let close = format!( "DELETE /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" ) .into_bytes(); let close_retry = close.clone(); let close_response = request(&listener, &runtime, close).await; assert!(close_response.starts_with(b"HTTP/1.1 204")); let close_retry_response = request(&listener, &runtime, close_retry).await; assert!(close_retry_response.starts_with(b"HTTP/1.1 204")); runtime.shutdown().await; generation.stop_sessions().await; generation.stop_background_tasks().await; replacement.stop_sessions().await; replacement.stop_background_tasks().await; } #[tokio::test] async fn rejected_bridge_bootstrap_falls_back_to_uncacheable_static_index() { let capability = [15u8; 32]; let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https)); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); let runtime = WebProcessRuntime::start(active_runtime); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability); let bridge_request = || { format!( "GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n" ) .into_bytes() }; let first_response = request(&listener, &runtime, bridge_request()).await; let (_, first_body) = split_response(&first_response); assert!(first_body.windows(11).any(|value| value == b"bootstrap=\"")); let fallback_response = request(&listener, &runtime, bridge_request()).await; let (fallback_headers, fallback_body) = split_response(&fallback_response); assert!(fallback_headers.starts_with(b"HTTP/1.1 200")); assert_eq!( response_header(fallback_headers, "cache-control"), "no-store" ); assert_eq!(fallback_body, b"decoy"); runtime.shutdown().await; generation.stop_sessions().await; generation.stop_background_tasks().await; } #[tokio::test] async fn bootstrap_survives_client_address_family_change() { let capability = [8u8; 32]; let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https)); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); let runtime = WebProcessRuntime::start(active_runtime); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability); let root = format!( "GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 2001:db8::10\r\nConnection: close\r\n\r\n" ) .into_bytes(); let root_response = request(&listener, &runtime, root).await; let (_, root_body) = split_response(&root_response); let root_body = std::str::from_utf8(root_body).unwrap(); let bootstrap = root_body .split_once("bootstrap=\"") .and_then(|(_, suffix)| suffix.split_once('"')) .map(|(token, _)| token) .unwrap(); let hello = frame::encode(FrameType::Hello, 0, &[1]); let mut create = format!( "POST /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", hello.len() ) .into_bytes(); create.extend_from_slice(&hello); let create_response = request(&listener, &runtime, create).await; assert!(create_response.starts_with(b"HTTP/1.1 200")); runtime.shutdown().await; generation.stop_sessions().await; generation.stop_background_tasks().await; } #[tokio::test] async fn unused_bootstrap_survives_equivalent_runtime_generation_swap() { let capability = [10u8; 32]; let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https)); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability); let root = format!( "GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n" ) .into_bytes(); let root_response = request(&listener, &runtime, root).await; let (_, root_body) = split_response(&root_response); let root_body = std::str::from_utf8(root_body).unwrap(); let bootstrap = root_body .split_once("bootstrap=\"") .and_then(|(_, suffix)| suffix.split_once('"')) .map(|(token, _)| token) .unwrap(); let replacement = test_runtime_generation(2, runtime_config(capability, WebCarrier::Https)); active_runtime.store(Arc::clone(&replacement)); let hello = frame::encode(FrameType::Hello, 0, &[1]); let mut create = format!( "POST /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", hello.len() ) .into_bytes(); create.extend_from_slice(&hello); let create_response = request(&listener, &runtime, create).await; assert!(create_response.starts_with(b"HTTP/1.1 200")); runtime.shutdown().await; generation.stop_sessions().await; generation.stop_background_tasks().await; replacement.stop_sessions().await; replacement.stop_background_tasks().await; } #[tokio::test] async fn bridge_bootstrap_uses_the_generation_that_selected_its_profile() { let initial = test_runtime_generation(1, runtime_config([21; 32], WebCarrier::Https)); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&initial))); let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime)); let profile = initial.config().web.runtime.as_ref().unwrap().profiles[0].clone(); let replacement = test_runtime_generation(2, runtime_config([22; 32], WebCarrier::HttpsLanes)); active_runtime.store(Arc::clone(&replacement)); let result = runtime.issue_bootstrap_for_generation(&initial, profile, "192.0.2.10".parse().unwrap()); assert!(result.is_ok()); runtime.shutdown().await; initial.stop_sessions().await; initial.stop_background_tasks().await; replacement.stop_sessions().await; replacement.stop_background_tasks().await; } #[tokio::test] async fn unused_bootstrap_is_rejected_after_profile_identity_change() { let capability = [11u8; 32]; let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https)); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability); let root = format!( "GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n" ) .into_bytes(); let root_response = request(&listener, &runtime, root).await; let (_, root_body) = split_response(&root_response); let root_body = std::str::from_utf8(root_body).unwrap(); let bootstrap = root_body .split_once("bootstrap=\"") .and_then(|(_, suffix)| suffix.split_once('"')) .map(|(token, _)| token) .unwrap(); let replacement = test_runtime_generation(2, runtime_config(capability, WebCarrier::HttpsLanes)); active_runtime.store(Arc::clone(&replacement)); let hello = frame::encode(FrameType::Hello, 0, &[1]); let mut create = format!( "POST /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", hello.len() ) .into_bytes(); create.extend_from_slice(&hello); let create_response = request(&listener, &runtime, create).await; assert!(!create_response.starts_with(b"HTTP/1.1 200")); runtime.shutdown().await; generation.stop_sessions().await; generation.stop_background_tasks().await; replacement.stop_sessions().await; replacement.stop_background_tasks().await; } #[tokio::test] async fn https_lanes_is_advertised_and_requires_canonical_lane_headers() { let capability = [9u8; 32]; let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::HttpsLanes)); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); let runtime = WebProcessRuntime::start(active_runtime); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability); let root = format!( "GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n" ) .into_bytes(); let root_response = request(&listener, &runtime, root).await; let (_, root_body) = split_response(&root_response); let root_body = std::str::from_utf8(root_body).unwrap(); assert!(root_body.contains("const negotiationEnabled=false")); let bootstrap = root_body .split_once("bootstrap=\"") .and_then(|(_, suffix)| suffix.split_once('"')) .map(|(token, _)| token) .unwrap(); let hello = frame::encode(FrameType::Hello, 0, &[1]); let mut create = format!( "POST /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", hello.len() ) .into_bytes(); create.extend_from_slice(&hello); let create_response = request(&listener, &runtime, create).await; let (create_headers, _) = split_response(&create_response); assert_eq!( response_header(create_headers, "x-carrier-mode"), "https-lanes" ); let session = response_header(create_headers, "x-session-token").to_string(); let pong = frame::encode(FrameType::Pong, 0, &[]); let mut uplink = format!( "POST /api/v1/up HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nContent-Type: application/octet-stream\r\nX-Up-Seq: 1\r\nX-Lane-ID: 0\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", pong.len() ) .into_bytes(); uplink.extend_from_slice(&pong); let uplink_response = request(&listener, &runtime, uplink).await; let (uplink_headers, _) = split_response(&uplink_response); assert!(uplink_headers.starts_with(b"HTTP/1.1 204")); assert_eq!(response_header(uplink_headers, "x-up-ack"), "1"); assert!( !std::str::from_utf8(uplink_headers) .unwrap() .lines() .any(|line| line.to_ascii_lowercase().starts_with("content-length:")) ); let mut missing_lane = format!( "POST /api/v1/up HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nContent-Type: application/octet-stream\r\nX-Up-Seq: 2\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", pong.len() ) .into_bytes(); missing_lane.extend_from_slice(&pong); let missing_lane_response = request(&listener, &runtime, missing_lane).await; assert!(!missing_lane_response.starts_with(b"HTTP/1.1 204")); let mut aliased_lane = format!( "POST /api/v1/up HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nContent-Type: application/octet-stream\r\nX-Up-Seq: 2\r\nX-Lane-ID: 00\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", pong.len() ) .into_bytes(); aliased_lane.extend_from_slice(&pong); let aliased_lane_response = request(&listener, &runtime, aliased_lane).await; assert!(!aliased_lane_response.starts_with(b"HTTP/1.1 204")); runtime.shutdown().await; generation.stop_sessions().await; generation.stop_background_tasks().await; }