From 90c0d65e1b255cd61ba3079cff1d1c87836564d4 Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Sun, 23 Aug 2026 09:04:53 +0300 Subject: [PATCH] WEB Carrier: https-lanes Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> --- src/config/load/runtime_web.rs | 1 + src/config/load/strict_keys.rs | 2 +- src/config/load/validate_web.rs | 5 + .../tests/load_basic_tests/web_tests.rs | 20 +- src/config/types.rs | 2 +- src/config/types/web.rs | 26 + src/web/bridge.rs | 115 +++- src/web/frame.rs | 2 + src/web/http.rs | 63 ++- src/web/http/tests.rs | 89 ++- src/web/manager.rs | 24 +- src/web/manager/state.rs | 1 + src/web/session.rs | 64 ++- src/web/session/backend.rs | 12 +- src/web/session/downlink.rs | 29 + src/web/session/lanes.rs | 525 ++++++++++++++++++ src/web/session/lanes/tests.rs | 129 +++++ src/web/session/uplink.rs | 28 +- 18 files changed, 1063 insertions(+), 74 deletions(-) create mode 100644 src/web/session/lanes.rs create mode 100644 src/web/session/lanes/tests.rs diff --git a/src/config/load/runtime_web.rs b/src/config/load/runtime_web.rs index 24be591..a85175c 100644 --- a/src/config/load/runtime_web.rs +++ b/src/config/load/runtime_web.rs @@ -62,6 +62,7 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> { public_addr: vhost.public_addr, user: profile.user.clone(), secret_mode: profile.secret_mode, + carrier: config.web.carrier, capability, max_sessions: profile .max_sessions diff --git a/src/config/load/strict_keys.rs b/src/config/load/strict_keys.rs index 807de7f..d047920 100644 --- a/src/config/load/strict_keys.rs +++ b/src/config/load/strict_keys.rs @@ -259,7 +259,7 @@ const LISTENER_CONFIG_KEYS: &[&str] = &[ "web_trusted_proxy_cidrs", ]; -const WEB_CONFIG_KEYS: &[&str] = &["enabled", "limits", "timeouts", "vhosts"]; +const WEB_CONFIG_KEYS: &[&str] = &["enabled", "carrier", "limits", "timeouts", "vhosts"]; const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[ "max_header_bytes", diff --git a/src/config/load/validate_web.rs b/src/config/load/validate_web.rs index 1f64a99..ba202f6 100644 --- a/src/config/load/validate_web.rs +++ b/src/config/load/validate_web.rs @@ -59,6 +59,11 @@ pub(super) fn validate(config: &mut ProxyConfig) -> Result<()> { } validate_limits(&config.web.limits)?; + if config.web.carrier == WebCarrier::HttpsLanes + && config.web.limits.max_http_handlers < 2 + { + return config_error("web.carrier=https-lanes requires web.limits.max_http_handlers >= 2"); + } validate_timeouts(&config.web.timeouts)?; validate_vhosts(config)?; Ok(()) diff --git a/src/config/tests/load_basic_tests/web_tests.rs b/src/config/tests/load_basic_tests/web_tests.rs index b781601..e803cda 100644 --- a/src/config/tests/load_basic_tests/web_tests.rs +++ b/src/config/tests/load_basic_tests/web_tests.rs @@ -14,6 +14,7 @@ web_trusted_proxy_cidrs = ["127.0.0.1/32"] [web] enabled = true +carrier = "https-lanes" [[web.vhosts]] host = "Proxy.Example.COM" @@ -42,11 +43,22 @@ fn web_config_builds_canonical_runtime_snapshot() { assert_eq!(vhost.profiles.len(), 1); assert_eq!(vhost.profiles[0].user, "alice"); assert_eq!(vhost.profiles[0].secret_mode, WebSecretMode::Dd); + assert_eq!(vhost.profiles[0].carrier, WebCarrier::HttpsLanes); assert_eq!(vhost.profiles[0].max_sessions, 4); assert_eq!(vhost.profiles[0].max_streams, 64); assert_eq!(vhost.profiles[0].max_streams_per_session, 16); } +#[test] +fn https_lanes_requires_separate_poll_and_control_handler_capacity() { + let invalid = WEB_CONFIG.replace( + "carrier = \"https-lanes\"", + "carrier = \"https-lanes\"\n\n[web.limits]\nmax_http_handlers = 1\nmax_body_readers = 1", + ); + let error = load_config_error_from_temp_toml(&invalid); + assert!(error.contains("web.carrier=https-lanes requires")); +} + #[test] fn web_listener_requires_an_explicit_trusted_proxy() { let invalid = WEB_CONFIG.replace( @@ -60,8 +72,8 @@ fn web_listener_requires_an_explicit_trusted_proxy() { #[test] fn web_queue_limits_preserve_control_and_uplink_progress() { let invalid = WEB_CONFIG.replace( - "[web]\nenabled = true", - "[web]\nenabled = true\n\n[web.limits]\ncontrol_bytes_per_session = 1", + "carrier = \"https-lanes\"", + "carrier = \"https-lanes\"\n\n[web.limits]\ncontrol_bytes_per_session = 1", ); let error = load_config_error_from_temp_toml(&invalid); assert!(error.contains("control reserves must cover bounded control frames")); @@ -70,9 +82,9 @@ fn web_queue_limits_preserve_control_and_uplink_progress() { #[test] fn web_semaphore_limits_are_rejected_before_runtime_construction() { let invalid = WEB_CONFIG.replace( - "[web]\nenabled = true", + "carrier = \"https-lanes\"", &format!( - "[web]\nenabled = true\n\n[web.limits]\nmax_http_connections = {}", + "carrier = \"https-lanes\"\n\n[web.limits]\nmax_http_connections = {}", tokio::sync::Semaphore::MAX_PERMITS + 1, ), ); diff --git a/src/config/types.rs b/src/config/types.rs index ecdea93..89f9be7 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -49,7 +49,7 @@ pub use server::{ }; #[allow(unused_imports)] pub use web::{ - WebConfig, WebDecoyConfig, WebLimitsConfig, WebProfileConfig, WebSecretMode, + WebCarrier, WebConfig, WebDecoyConfig, WebLimitsConfig, WebProfileConfig, WebSecretMode, WebTimeoutsConfig, WebVhostConfig, }; pub(crate) use web::{ diff --git a/src/config/types/web.rs b/src/config/types/web.rs index c246b58..d8396a5 100644 --- a/src/config/types/web.rs +++ b/src/config/types/web.rs @@ -16,6 +16,27 @@ pub enum WebSecretMode { Dd, } +/// HTTP carrier selected for newly issued WEB bridge sessions. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WebCarrier { + /// Serialize all logical streams through one uplink and one downlink sequence. + #[default] + Https, + /// Give every logical stream independent HTTPS sequencing and polling state. + HttpsLanes, +} + +impl WebCarrier { + /// Returns the exact carrier token advertised to the browser bridge. + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::Https => "https", + Self::HttpsLanes => "https-lanes", + } + } +} + /// One access user explicitly exposed through a WEB virtual host. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WebProfileConfig { @@ -273,6 +294,9 @@ pub struct WebConfig { /// Enables issuance of new WEB bridge and session credentials. #[serde(default)] pub enabled: bool, + /// Carrier selected for newly issued WEB bridge sessions. + #[serde(default)] + pub carrier: WebCarrier, /// Hard process and protocol limits. #[serde(default)] pub limits: WebLimitsConfig, @@ -320,6 +344,8 @@ pub(crate) struct WebRuntimeProfile { pub(crate) user: String, /// Client secret representation and inner protocol policy. pub(crate) secret_mode: WebSecretMode, + /// Carrier frozen into bridge and session state at issuance time. + pub(crate) carrier: WebCarrier, /// HMAC-derived bridge capability. pub(crate) capability: [u8; 32], /// Per-profile live session ceiling. diff --git a/src/web/bridge.rs b/src/web/bridge.rs index 575715b..e640ef4 100644 --- a/src/web/bridge.rs +++ b/src/web/bridge.rs @@ -1,5 +1,6 @@ use base64::Engine as _; +use crate::config::WebCarrier; use crate::crypto::SecureRandom; /// Browser security policy for the transient Telegram Desktop bridge page. @@ -13,13 +14,14 @@ pub(crate) struct BridgePage { pub(crate) content_security_policy: String, } -/// Renders the HTTPS-only WEB carrier bridge with a fresh CSP nonce. +/// Renders the selected HTTPS WEB carrier bridge with a fresh CSP nonce. pub(crate) fn render( host: &str, bootstrap: &str, batch_limit: usize, queue_limit: usize, queue_items: usize, + carrier: WebCarrier, rng: &SecureRandom, ) -> BridgePage { let mut nonce = [0u8; 18]; @@ -31,7 +33,8 @@ pub(crate) fn render( .replace("__BOOTSTRAP__", bootstrap) .replace("__BATCH_LIMIT__", &batch_limit.to_string()) .replace("__QUEUE_LIMIT__", &queue_limit.to_string()) - .replace("__QUEUE_ITEMS__", &queue_items.to_string()); + .replace("__QUEUE_ITEMS__", &queue_items.to_string()) + .replace("__CARRIER__", carrier.as_str()); BridgePage { body, content_security_policy: format!( @@ -51,24 +54,26 @@ const DOCUMENT: &str = r##"