[Fix] Limit hosted-user MTP sessions

This commit is contained in:
Alex Emmet 2026-09-24 18:45:31 +02:00
commit 6fa8ac8b5f
4 changed files with 49 additions and 4 deletions

View file

@ -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. 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.

View file

@ -577,6 +577,7 @@ async fn main() -> ExitCode {
}), }),
required: web.required, required: web.required,
max_mtp_sessions: web.max_mtp_sessions, max_mtp_sessions: web.max_mtp_sessions,
max_mtp_sessions_per_user: web.max_mtp_sessions_per_user,
authority_discovery: iota_identity::AuthorityDiscoveryDocument { authority_discovery: iota_identity::AuthorityDiscoveryDocument {
version: 1, version: 1,
service: iota_identity::AuthorityKind::Iota, service: iota_identity::AuthorityKind::Iota,

View file

@ -40,7 +40,7 @@ pub enum ConfigError {
InvalidRelayRouterKey(String), InvalidRelayRouterKey(String),
#[error("relay router certificate path must not be empty")] #[error("relay router certificate path must not be empty")]
MissingRelayRouterCertificate, MissingRelayRouterCertificate,
#[error("web.max_mtp_sessions must be greater than zero")] #[error("web MTP session limits must be greater than zero")]
InvalidMaxMtpSessions, InvalidMaxMtpSessions,
#[error("max_ipc_clients must be greater than zero")] #[error("max_ipc_clients must be greater than zero")]
InvalidMaxIpcClients, InvalidMaxIpcClients,
@ -155,6 +155,8 @@ pub struct WebSettings {
pub required: bool, pub required: bool,
#[serde(default = "default_max_mtp_sessions")] #[serde(default = "default_max_mtp_sessions")]
pub max_mtp_sessions: usize, pub max_mtp_sessions: usize,
#[serde(default = "default_max_mtp_sessions_per_user")]
pub max_mtp_sessions_per_user: usize,
#[serde(default)] #[serde(default)]
pub direct_endpoints: Vec<String>, pub direct_endpoints: Vec<String>,
#[serde(default)] #[serde(default)]
@ -163,6 +165,9 @@ pub struct WebSettings {
const fn default_max_mtp_sessions() -> usize { const fn default_max_mtp_sessions() -> usize {
256 256
} }
const fn default_max_mtp_sessions_per_user() -> usize {
4
}
fn default_web_bind() -> String { fn default_web_bind() -> String {
"127.0.0.1".into() "127.0.0.1".into()
} }
@ -180,6 +185,7 @@ impl Default for WebSettings {
key: None, key: None,
required: false, required: false,
max_mtp_sessions: default_max_mtp_sessions(), max_mtp_sessions: default_max_mtp_sessions(),
max_mtp_sessions_per_user: default_max_mtp_sessions_per_user(),
direct_endpoints: Vec::new(), direct_endpoints: Vec::new(),
relay_hints: 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 { if config.storage_limits.max_user_blobs <= 0 {
return Err(ConfigError::InvalidStorageLimit("max_user_blobs")); 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); return Err(ConfigError::InvalidMaxMtpSessions);
} }
if config.max_ipc_clients == 0 { if config.max_ipc_clients == 0 {

View file

@ -4,7 +4,12 @@ use iota_identity::{LocalDescriptorPublisher, LocalUserId, LocalUserStore, Signe
use iota_logger::log; use iota_logger::log;
use mtp::host::HostConfig; use mtp::host::HostConfig;
use mtp::webserver::{HttpRequest, HttpResponse, MTPWebServer, WebServerConfig}; 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::sync::Mutex;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@ -41,6 +46,7 @@ pub struct WebConfig {
pub tls: Option<TlsConfig>, pub tls: Option<TlsConfig>,
pub required: bool, pub required: bool,
pub max_mtp_sessions: usize, pub max_mtp_sessions: usize,
pub max_mtp_sessions_per_user: usize,
pub authority_discovery: iota_identity::AuthorityDiscoveryDocument, pub authority_discovery: iota_identity::AuthorityDiscoveryDocument,
pub local_users: Arc<dyn LocalUserStore>, pub local_users: Arc<dyn LocalUserStore>,
pub descriptor_publisher: Arc<dyn LocalDescriptorPublisher>, pub descriptor_publisher: Arc<dyn LocalDescriptorPublisher>,
@ -64,6 +70,23 @@ impl std::fmt::Display for WebServerError {
} }
impl std::error::Error for WebServerError {} impl std::error::Error for WebServerError {}
struct UserSessionGuard {
sessions: Arc<StdMutex<HashMap<u64, usize>>>,
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 { pub struct WebServerHandle {
cancellation: CancellationToken, cancellation: CancellationToken,
join: Mutex<Option<JoinHandle<()>>>, join: Mutex<Option<JoinHandle<()>>>,
@ -292,6 +315,8 @@ pub async fn start(
let cancellation = parent.child_token(); let cancellation = parent.child_token();
let task_cancellation = cancellation.clone(); let task_cancellation = cancellation.clone();
let session_limit = Arc::new(tokio::sync::Semaphore::new(config.max_mtp_sessions)); let session_limit = Arc::new(tokio::sync::Semaphore::new(config.max_mtp_sessions));
let user_sessions = Arc::new(StdMutex::new(HashMap::<u64, usize>::new()));
let max_user_sessions = config.max_mtp_sessions_per_user;
let join = tokio::spawn(async move { let join = tokio::spawn(async move {
loop { loop {
tokio::select! { tokio::select! {
@ -301,9 +326,22 @@ pub async fn start(
log!("Rejected MTP connection: session limit reached"); log!("Rejected MTP connection: session limit reached");
continue; 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(); let handler = mtp_handler.clone();
tokio::spawn(async move { tokio::spawn(async move {
let _permit = permit; let _permit = permit;
let _user_guard = user_guard;
handler.accept(connection).await; handler.accept(connection).await;
}); });
} }