From 6fa8ac8b5f16482576b57e120df546de1a73da01 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:45:31 +0200 Subject: [PATCH] [Fix] Limit hosted-user MTP sessions --- README.md | 2 +- iota-daemon/src/main.rs | 1 + iota-storage/src/util/config_util.rs | 10 +++++-- web-server/src/lib.rs | 40 +++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0e619af..039cc57 100644 --- a/README.md +++ b/README.md @@ -87,4 +87,4 @@ Replacement uploads temporarily count both old and new assets until commit. Signed relays expire after 30 days less five minutes; replay rows remain for 30 days. `max_relay_future_skew_millis` defaults to 300000, or five minutes, and can be reduced for deployment clock tolerance. Fixed five-minute margin keeps freshness within replay retention even if configuration changes. Timestamps use Unix milliseconds. -Asset and blob list responses contain at most 128 entries. Send the returned positive `Offset` as the next request cursor; `Offset: 0` ends pagination. `web.max_mtp_sessions` defaults to 256 and limits concurrent authenticated MTP sessions. Legacy web administration requires the non-default `legacy-web-admin` feature. +Asset and blob list responses contain at most 128 entries. Send the returned positive `Offset` as the next request cursor; `Offset: 0` ends pagination. `web.max_mtp_sessions` defaults to 256 and limits concurrent authenticated MTP sessions. `web.max_mtp_sessions_per_user` defaults to four sessions per hosted user. Legacy web administration requires the non-default `legacy-web-admin` feature. diff --git a/iota-daemon/src/main.rs b/iota-daemon/src/main.rs index d746017..6b0fa6a 100644 --- a/iota-daemon/src/main.rs +++ b/iota-daemon/src/main.rs @@ -577,6 +577,7 @@ async fn main() -> ExitCode { }), required: web.required, max_mtp_sessions: web.max_mtp_sessions, + max_mtp_sessions_per_user: web.max_mtp_sessions_per_user, authority_discovery: iota_identity::AuthorityDiscoveryDocument { version: 1, service: iota_identity::AuthorityKind::Iota, diff --git a/iota-storage/src/util/config_util.rs b/iota-storage/src/util/config_util.rs index b7040dd..51e2e5f 100644 --- a/iota-storage/src/util/config_util.rs +++ b/iota-storage/src/util/config_util.rs @@ -40,7 +40,7 @@ pub enum ConfigError { InvalidRelayRouterKey(String), #[error("relay router certificate path must not be empty")] MissingRelayRouterCertificate, - #[error("web.max_mtp_sessions must be greater than zero")] + #[error("web MTP session limits must be greater than zero")] InvalidMaxMtpSessions, #[error("max_ipc_clients must be greater than zero")] InvalidMaxIpcClients, @@ -155,6 +155,8 @@ pub struct WebSettings { pub required: bool, #[serde(default = "default_max_mtp_sessions")] pub max_mtp_sessions: usize, + #[serde(default = "default_max_mtp_sessions_per_user")] + pub max_mtp_sessions_per_user: usize, #[serde(default)] pub direct_endpoints: Vec, #[serde(default)] @@ -163,6 +165,9 @@ pub struct WebSettings { const fn default_max_mtp_sessions() -> usize { 256 } +const fn default_max_mtp_sessions_per_user() -> usize { + 4 +} fn default_web_bind() -> String { "127.0.0.1".into() } @@ -180,6 +185,7 @@ impl Default for WebSettings { key: None, required: false, max_mtp_sessions: default_max_mtp_sessions(), + max_mtp_sessions_per_user: default_max_mtp_sessions_per_user(), direct_endpoints: Vec::new(), relay_hints: Vec::new(), } @@ -270,7 +276,7 @@ pub fn validate_config(config: &IotaConfig) -> Result<(), ConfigError> { if config.storage_limits.max_user_blobs <= 0 { return Err(ConfigError::InvalidStorageLimit("max_user_blobs")); } - if config.web.max_mtp_sessions == 0 { + if config.web.max_mtp_sessions == 0 || config.web.max_mtp_sessions_per_user == 0 { return Err(ConfigError::InvalidMaxMtpSessions); } if config.max_ipc_clients == 0 { diff --git a/web-server/src/lib.rs b/web-server/src/lib.rs index d690df0..684da84 100644 --- a/web-server/src/lib.rs +++ b/web-server/src/lib.rs @@ -4,7 +4,12 @@ use iota_identity::{LocalDescriptorPublisher, LocalUserId, LocalUserStore, Signe use iota_logger::log; use mtp::host::HostConfig; use mtp::webserver::{HttpRequest, HttpResponse, MTPWebServer, WebServerConfig}; -use std::{net::IpAddr, path::PathBuf, sync::Arc}; +use std::{ + collections::HashMap, + net::IpAddr, + path::PathBuf, + sync::{Arc, Mutex as StdMutex}, +}; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -41,6 +46,7 @@ pub struct WebConfig { pub tls: Option, pub required: bool, pub max_mtp_sessions: usize, + pub max_mtp_sessions_per_user: usize, pub authority_discovery: iota_identity::AuthorityDiscoveryDocument, pub local_users: Arc, pub descriptor_publisher: Arc, @@ -64,6 +70,23 @@ impl std::fmt::Display for WebServerError { } impl std::error::Error for WebServerError {} +struct UserSessionGuard { + sessions: Arc>>, + user_id: u64, +} + +impl Drop for UserSessionGuard { + fn drop(&mut self) { + let mut sessions = self.sessions.lock().unwrap(); + if let Some(count) = sessions.get_mut(&self.user_id) { + *count -= 1; + if *count == 0 { + sessions.remove(&self.user_id); + } + } + } +} + pub struct WebServerHandle { cancellation: CancellationToken, join: Mutex>>, @@ -292,6 +315,8 @@ pub async fn start( let cancellation = parent.child_token(); let task_cancellation = cancellation.clone(); let session_limit = Arc::new(tokio::sync::Semaphore::new(config.max_mtp_sessions)); + let user_sessions = Arc::new(StdMutex::new(HashMap::::new())); + let max_user_sessions = config.max_mtp_sessions_per_user; let join = tokio::spawn(async move { loop { tokio::select! { @@ -301,9 +326,22 @@ pub async fn start( log!("Rejected MTP connection: session limit reached"); continue; }; + let user_guard = if connection.client_id & (1_u64 << 63) == 0 { + let mut sessions = user_sessions.lock().unwrap(); + let count = sessions.entry(connection.client_id).or_default(); + if *count >= max_user_sessions { + log!("Rejected MTP connection: per-user session limit reached"); + continue; + } + *count += 1; + Some(UserSessionGuard { sessions: user_sessions.clone(), user_id: connection.client_id }) + } else { + None + }; let handler = mtp_handler.clone(); tokio::spawn(async move { let _permit = permit; + let _user_guard = user_guard; handler.accept(connection).await; }); }