From 5cf71c60140cba9d018aed537c98358f86c59e1a Mon Sep 17 00:00:00 2001 From: Louis Dureuil Date: Wed, 28 Dec 2022 15:47:38 +0100 Subject: [PATCH] Add rate-limiting options --- config.toml | 63 ++++++ .../src/analytics/segment_analytics.rs | 37 +++- meilisearch/src/option.rs | 191 ++++++++++++++++++ meilisearch/tests/common/server.rs | 6 +- 4 files changed, 295 insertions(+), 2 deletions(-) diff --git a/config.toml b/config.toml index 6ef9b77f1..a1a52f3ca 100644 --- a/config.toml +++ b/config.toml @@ -73,6 +73,69 @@ ignore_dump_if_db_exists = false # https://docs.meilisearch.com/learn/configuration/instance_options.html#ignore-dump-if-db-exists +##################### +### RATE LIMITING ### +##################### + +rate_limiting_disable_all = false +# Prevents a Meilisearch instance from performing any rate limiting. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-all + +rate_limiting_disable_global = false +# Prevents a Meilisearch instance from performing rate limiting global to all queries. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-global + +rate_limiting_global_pool = 100000 +# The maximum pool of search requests that can be performed before they are rejected. +# +# The pool starts full at the provided value, then each search request diminishes the pool by 1. +# When the pool is empty the search request is rejected. +# The pool is replenished by 1 depending on the cooldown period. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-global-pool + +rate_limiting_global_cooldown_ns = 50000 +# The amount of time, in nanoseconds, before the pool of available search requests is replenished by 1 again. +# +# The maximum number of available search requests is given by `rate_limiting_global_pool`. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-global-cooldown-ns + +rate_limiting_disable_ip = false +# Prevents a Meilisearch instance from performing rate limiting per IP address. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-ip + +rate_limiting_ip_pool = 200 +# The maximum pool of search requests that can be performed from a specific IP before they are rejected. +# +# The pool starts full at the provided value, then each search request from the same IP address diminishes the pool by 1. +# When the pool is empty the search request is rejected. +# The pool is replenished by 1 depending on the cooldown period. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-ip-pool + +rate_limiting_ip_cooldown_ns = 50000000 +# The amount of time, in nanoseconds, before the pool of available search requests for a specific IP address is replenished by 1 again. +# +# The maximum number of available search requests for a specific IP address is given by `rate_limiting_ip_pool`. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-ip-cooldown-ns + +rate_limiting_disable_api_key = false +# Prevents a Meilisearch instance from performing rate limiting per API key. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-api-key + +rate_limiting_api_key_pool = 10000 +# The maximum pool of search requests that can be performed using a specific API key before they are rejected. +# +# The pool starts full at the provided value, then each search request using the same API key diminishes the pool by 1. +# When the pool is empty the search request is rejected. +# The pool is replenished by 1 depending on the cooldown period. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-api-key-pool + +rate_limiting_api_key_cooldown_ns = 500000 +# The amount of time, in nanoseconds, before the pool of available search requests using a specific API key is replenished by 1 again. +# +# The maximum number of available search requests using a specific API key is given by `rate_limiting_api_key_pool`. +# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-api-key-cooldown-ns + + ################# ### SNAPSHOTS ### ################# diff --git a/meilisearch/src/analytics/segment_analytics.rs b/meilisearch/src/analytics/segment_analytics.rs index 8bde71688..e11e14a00 100644 --- a/meilisearch/src/analytics/segment_analytics.rs +++ b/meilisearch/src/analytics/segment_analytics.rs @@ -25,7 +25,9 @@ use uuid::Uuid; use super::{config_user_id_path, DocumentDeletionKind, MEILISEARCH_CONFIG_PATH}; use crate::analytics::Analytics; -use crate::option::{default_http_addr, IndexerOpts, MaxMemory, MaxThreads, SchedulerConfig}; +use crate::option::{ + default_http_addr, IndexerOpts, MaxMemory, MaxThreads, RateLimiterConfig, SchedulerConfig, +}; use crate::routes::indexes::documents::UpdateDocumentsQuery; use crate::routes::tasks::TasksFilterQueryRaw; use crate::routes::{create_all_stats, Stats}; @@ -241,6 +243,16 @@ struct Infos { ssl_require_auth: bool, ssl_resumption: bool, ssl_tickets: bool, + rate_limiting_disable_all: bool, + rate_limiting_disable_global: bool, + rate_limiting_global_pool: u32, + rate_limiting_global_cooldown_ns: u64, + rate_limiting_disable_ip: bool, + rate_limiting_ip_pool: u32, + rate_limiting_ip_cooldown_ns: u64, + rate_limiting_disable_api_key: bool, + rate_limiting_api_key_pool: u32, + rate_limiting_api_key_cooldown_ns: u64, } impl From for Infos { @@ -278,6 +290,7 @@ impl From for Infos { scheduler_options, config_file_path, generate_master_key: _, + rate_limiter_options, #[cfg(all(not(debug_assertions), feature = "analytics"))] no_analytics: _, } = options; @@ -289,6 +302,18 @@ impl From for Infos { max_indexing_memory, max_indexing_threads, } = indexer_options; + let RateLimiterConfig { + rate_limiting_disable_all, + rate_limiting_disable_global, + rate_limiting_global_pool, + rate_limiting_global_cooldown_ns, + rate_limiting_disable_ip, + rate_limiting_ip_pool, + rate_limiting_ip_cooldown_ns, + rate_limiting_disable_api_key, + rate_limiting_api_key_pool, + rate_limiting_api_key_cooldown_ns, + } = rate_limiter_options; // We're going to override every sensible information. // We consider information sensible if it contains a path, an address, or a key. @@ -321,6 +346,16 @@ impl From for Infos { ssl_require_auth, ssl_resumption, ssl_tickets, + rate_limiting_disable_all, + rate_limiting_disable_global, + rate_limiting_global_pool, + rate_limiting_global_cooldown_ns, + rate_limiting_disable_ip, + rate_limiting_ip_pool, + rate_limiting_ip_cooldown_ns, + rate_limiting_disable_api_key, + rate_limiting_api_key_pool, + rate_limiting_api_key_cooldown_ns, } } } diff --git a/meilisearch/src/option.rs b/meilisearch/src/option.rs index cc8aeaf50..beca9589f 100644 --- a/meilisearch/src/option.rs +++ b/meilisearch/src/option.rs @@ -50,6 +50,22 @@ const MEILI_IGNORE_DUMP_IF_DB_EXISTS: &str = "MEILI_IGNORE_DUMP_IF_DB_EXISTS"; const MEILI_DUMP_DIR: &str = "MEILI_DUMP_DIR"; const MEILI_LOG_LEVEL: &str = "MEILI_LOG_LEVEL"; const MEILI_GENERATE_MASTER_KEY: &str = "MEILI_GENERATE_MASTER_KEY"; + +// rate limiting + +const MEILI_RATE_LIMITING_DISABLE_ALL: &str = "MEILI_RATE_LIMITING_DISABLE_ALL"; +const MEILI_RATE_LIMITING_DISABLE_GLOBAL: &str = "MEILI_RATE_LIMITING_DISABLE_GLOBAL"; +const MEILI_RATE_LIMITING_DISABLE_IP: &str = "MEILI_RATE_LIMITING_DISABLE_IP"; +const MEILI_RATE_LIMITING_DISABLE_API_KEY: &str = "MEILI_RATE_LIMITING_DISABLE_API_KEY"; + +const MEILI_RATE_LIMITING_GLOBAL_POOL: &str = "MEILI_RATE_LIMITING_GLOBAL_POOL"; +const MEILI_RATE_LIMITING_IP_POOL: &str = "MEILI_RATE_LIMITING_IP_POOL"; +const MEILI_RATE_LIMITING_API_KEY_POOL: &str = "MEILI_RATE_LIMITING_API_KEY_POOL"; + +const MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS: &str = "MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS"; +const MEILI_RATE_LIMITING_IP_COOLDOWN_NS: &str = "MEILI_RATE_LIMITING_IP_COOLDOWN_NS"; +const MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS: &str = "MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS"; + #[cfg(feature = "metrics")] const MEILI_ENABLE_METRICS_ROUTE: &str = "MEILI_ENABLE_METRICS_ROUTE"; @@ -70,6 +86,15 @@ const MEILI_MAX_INDEXING_THREADS: &str = "MEILI_MAX_INDEXING_THREADS"; const DISABLE_AUTO_BATCHING: &str = "DISABLE_AUTO_BATCHING"; const DEFAULT_LOG_EVERY_N: usize = 100000; +const DEFAULT_GLOBAL_RATE_LIMITING_POOL: u32 = 100_000; +const DEFAULT_GLOBAL_RATE_LIMITING_COOLDOWN_NS: u64 = 50_000; // pool replenishes in 5s + +const DEFAULT_IP_RATE_LIMITING_POOL: u32 = 200; +const DEFAULT_IP_RATE_LIMITING_COOLDOWN_NS: u64 = 50_000_000; // pool replenishes in 10s + +const DEFAULT_API_KEY_RATE_LIMITING_POOL: u32 = 10_000; +const DEFAULT_API_KEY_RATE_LIMITING_COOLDOWN_NS: u64 = 500_000; // pool replenishes in 10s + #[derive(Debug, Clone, Parser, Deserialize)] #[clap(version, next_display_order = None)] #[serde(rename_all = "snake_case", deny_unknown_fields)] @@ -252,6 +277,10 @@ pub struct Opt { #[clap(flatten)] pub scheduler_options: SchedulerConfig, + #[serde(flatten)] + #[clap(flatten)] + pub rate_limiter_options: RateLimiterConfig, + /// Set the path to a configuration file that should be used to setup the engine. /// Format must be TOML. #[clap(long)] @@ -340,6 +369,7 @@ impl Opt { ignore_missing_dump: _, ignore_dump_if_db_exists: _, config_file_path: _, + rate_limiter_options, #[cfg(all(not(debug_assertions), feature = "analytics"))] no_analytics, #[cfg(feature = "metrics")] @@ -393,6 +423,7 @@ impl Opt { } indexer_options.export_to_env(); scheduler_options.export_to_env(); + rate_limiter_options.export_to_env(); } pub fn get_ssl_config(&self) -> anyhow::Result> { @@ -537,6 +568,142 @@ impl Default for IndexerOpts { } } +/// Options related to the configuration of the rate limiters. +#[derive(Debug, Clone, Parser, Default, Deserialize)] +#[serde(rename_all = "snake_case", deny_unknown_fields)] +pub struct RateLimiterConfig { + /// When provided, completely disables all rate limiting. + #[clap(long, env = MEILI_RATE_LIMITING_DISABLE_ALL)] + #[serde(default)] + pub rate_limiting_disable_all: bool, + + /// When provided, disables the global rate limiting that applies to all search requests. + /// + /// Disabling the global rate limiting does not disable IP-based and API-key-based rate limitings. + /// To disable all rate limiting regardless of the origin use `--rate-limiting-disable-all`. + #[clap(long, env = MEILI_RATE_LIMITING_DISABLE_GLOBAL)] + #[serde(default)] + pub rate_limiting_disable_global: bool, + /// The maximum pool of search requests that can be performed before they are rejected. + /// + /// The pool starts full at the provided value, then each search request diminishes the pool by 1. + /// When the pool is empty the search request is rejected. + /// The pool is replenished by 1 depending on the cooldown period. + #[clap(long, env = MEILI_RATE_LIMITING_GLOBAL_POOL, default_value_t = default_rate_limiting_global_pool())] + #[serde(default = "default_rate_limiting_global_pool")] + pub rate_limiting_global_pool: u32, + /// The amount of time, in nanoseconds, before the pool of available search requests is replenished by 1 again. + /// + /// The maximum number of available search requests is given by `--rate-limiting-global-pool`. + #[clap(long, env = MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS, default_value_t = default_rate_limiting_global_cooldown_ns())] + #[serde(default = "default_rate_limiting_global_cooldown_ns")] + pub rate_limiting_global_cooldown_ns: u64, + + /// When provided, disables the rate limiting that applies to all search requests originating with a specific IP address. + /// + /// Disabling the IP rate limiting does not disable the rate limiting that applies to all requests ("global") nor the API-key-based rate limiting. + /// To disable all rate limiting regardless of the origin use `--rate-limiting-disable-all`. + #[clap(long, env = MEILI_RATE_LIMITING_DISABLE_IP)] + #[serde(default)] + pub rate_limiting_disable_ip: bool, + /// The maximum pool of search requests that can be performed from a specific IP before they are rejected. + /// + /// The pool starts full at the provided value, then each search request from the same IP address diminishes the pool by 1. + /// When the pool is empty the search request is rejected. + /// The pool is replenished by 1 depending on the cooldown period. + #[clap(long, env = MEILI_RATE_LIMITING_IP_POOL, default_value_t = default_rate_limiting_ip_pool())] + #[serde(default = "default_rate_limiting_ip_pool")] + pub rate_limiting_ip_pool: u32, + /// The amount of time, in nanoseconds, before the pool of available search requests for a specific IP address is replenished by 1 again. + /// + /// The maximum number of available search requests for a specific IP address is given by `--rate-limiting-ip-pool`. + #[clap(long, env = MEILI_RATE_LIMITING_IP_COOLDOWN_NS, default_value_t = default_rate_limiting_ip_cooldown_ns())] + #[serde(default = "default_rate_limiting_ip_cooldown_ns")] + pub rate_limiting_ip_cooldown_ns: u64, + + /// When provided, disables the rate limiting that applies to all search requests originating with a specific API key. + /// + /// Disabling the API key limiting does not disable the rate limiting that applies to all requests ("global") nor the IP-based rate limiting. + /// To disable all rate limiting regardless of the origin use `--rate-limiting-disable-all`. + #[clap(long, env = MEILI_RATE_LIMITING_DISABLE_API_KEY)] + #[serde(default)] + pub rate_limiting_disable_api_key: bool, + /// The maximum pool of search requests that can be performed using a specific API key before they are rejected. + /// + /// The pool starts full at the provided value, then each search request using the same API key diminishes the pool by 1. + /// When the pool is empty the search request is rejected. + /// The pool is replenished by 1 depending on the cooldown period. + #[clap(long, env = MEILI_RATE_LIMITING_API_KEY_POOL, default_value_t = default_rate_limiting_api_key_pool())] + #[serde(default = "default_rate_limiting_api_key_pool")] + pub rate_limiting_api_key_pool: u32, + /// The amount of time, in nanoseconds, before the pool of available search requests using a specific API key is replenished by 1 again. + /// + /// The maximum number of available search requests using a specific API key is given by `--rate-limiting-api-key-pool`. + #[clap(long, env = MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS, default_value_t = default_rate_limiting_api_key_cooldown_ns())] + #[serde(default = "default_rate_limiting_api_key_cooldown_ns")] + pub rate_limiting_api_key_cooldown_ns: u64, +} + +impl RateLimiterConfig { + /// Exports the values to their corresponding env vars if they are not set. + pub fn export_to_env(self) { + let RateLimiterConfig { + rate_limiting_disable_all: disable_rate_limiting, + rate_limiting_disable_global: disable_global_rate_limiting, + rate_limiting_global_pool: global_rate_limiting_pool, + rate_limiting_global_cooldown_ns: global_rate_limiting_cooldown_ns, + rate_limiting_disable_ip: disable_ip_rate_limiting, + rate_limiting_ip_pool: ip_rate_limiting_pool, + rate_limiting_ip_cooldown_ns: ip_rate_limiting_cooldown_ns, + rate_limiting_disable_api_key: disable_api_key_rate_limiting, + rate_limiting_api_key_pool: api_key_rate_limiting_pool, + rate_limiting_api_key_cooldown_ns: api_key_rate_limiting_cooldown_ns, + } = self; + export_to_env_if_not_present( + MEILI_RATE_LIMITING_DISABLE_ALL, + disable_rate_limiting.to_string(), + ); + export_to_env_if_not_present( + MEILI_RATE_LIMITING_DISABLE_GLOBAL, + disable_global_rate_limiting.to_string(), + ); + export_to_env_if_not_present( + MEILI_RATE_LIMITING_DISABLE_IP, + disable_ip_rate_limiting.to_string(), + ); + export_to_env_if_not_present( + MEILI_RATE_LIMITING_DISABLE_API_KEY, + disable_api_key_rate_limiting.to_string(), + ); + + export_to_env_if_not_present( + MEILI_RATE_LIMITING_GLOBAL_POOL, + global_rate_limiting_pool.to_string(), + ); + export_to_env_if_not_present( + MEILI_RATE_LIMITING_IP_POOL, + ip_rate_limiting_pool.to_string(), + ); + export_to_env_if_not_present( + MEILI_RATE_LIMITING_API_KEY_POOL, + api_key_rate_limiting_pool.to_string(), + ); + + export_to_env_if_not_present( + MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS, + global_rate_limiting_cooldown_ns.to_string(), + ); + export_to_env_if_not_present( + MEILI_RATE_LIMITING_IP_COOLDOWN_NS, + ip_rate_limiting_cooldown_ns.to_string(), + ); + export_to_env_if_not_present( + MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS, + api_key_rate_limiting_cooldown_ns.to_string(), + ); + } +} + /// A type used to detect the max memory available and use 2/3 of it. #[derive(Debug, Clone, Copy, Deserialize, Serialize)] pub struct MaxMemory(Option); @@ -729,6 +896,30 @@ fn default_log_every_n() -> usize { DEFAULT_LOG_EVERY_N } +fn default_rate_limiting_global_pool() -> u32 { + DEFAULT_GLOBAL_RATE_LIMITING_POOL +} + +fn default_rate_limiting_ip_pool() -> u32 { + DEFAULT_IP_RATE_LIMITING_POOL +} + +fn default_rate_limiting_api_key_pool() -> u32 { + DEFAULT_API_KEY_RATE_LIMITING_POOL +} + +fn default_rate_limiting_global_cooldown_ns() -> u64 { + DEFAULT_GLOBAL_RATE_LIMITING_COOLDOWN_NS +} + +fn default_rate_limiting_ip_cooldown_ns() -> u64 { + DEFAULT_IP_RATE_LIMITING_COOLDOWN_NS +} + +fn default_rate_limiting_api_key_cooldown_ns() -> u64 { + DEFAULT_API_KEY_RATE_LIMITING_COOLDOWN_NS +} + #[cfg(test)] mod test { diff --git a/meilisearch/tests/common/server.rs b/meilisearch/tests/common/server.rs index c3c9b7c60..d0ffbe3b7 100644 --- a/meilisearch/tests/common/server.rs +++ b/meilisearch/tests/common/server.rs @@ -8,7 +8,7 @@ use actix_web::dev::ServiceResponse; use actix_web::http::StatusCode; use byte_unit::{Byte, ByteUnit}; use clap::Parser; -use meilisearch::option::{IndexerOpts, MaxMemory, Opt}; +use meilisearch::option::{IndexerOpts, MaxMemory, Opt, RateLimiterConfig}; use meilisearch::{analytics, create_app, setup_meilisearch}; use once_cell::sync::Lazy; use serde_json::{json, Value}; @@ -192,6 +192,10 @@ pub fn default_settings(dir: impl AsRef) -> Opt { max_task_db_size: Byte::from_unit(1.0, ByteUnit::GiB).unwrap(), http_payload_size_limit: Byte::from_unit(10.0, ByteUnit::MiB).unwrap(), snapshot_dir: ".".into(), + rate_limiter_options: RateLimiterConfig { + rate_limiting_disable_all: true, + ..Parser::parse_from(None as Option<&str>) + }, indexer_options: IndexerOpts { // memory has to be unlimited because several meilisearch are running in test context. max_indexing_memory: MaxMemory::unlimited(),