| Crates.io | basic-axum-rate-limit |
| lib.rs | basic-axum-rate-limit |
| version | 0.2.2 |
| created_at | 2025-11-28 02:20:54.133003+00 |
| updated_at | 2025-11-30 02:09:03.229496+00 |
| description | Simple rate limiting middleware for Axum with callback-based extensibility |
| homepage | https://grant.cavebatsofware.com/ |
| repository | https://github.com/cavebatsofware/rate-limiter |
| max_upload_size | |
| id | 1954697 |
| size | 112,858 |
Rate limiting middleware for Axum using a callback pattern for (optional) database operations.
use basic_axum_rate_limit::{OnBlocked, ActionChecker, SecurityContext};
use async_trait::async_trait;
use std::time::Duration;
#[derive(Clone)]
pub struct MyCallbacks {
db: DatabaseConnection,
}
#[async_trait]
impl OnBlocked for MyCallbacks {
async fn on_blocked(&self, ip: &str, path: &str, context: &SecurityContext) {
// Log the blocked attempt
}
}
#[async_trait]
impl ActionChecker for MyCallbacks {
async fn check_recent_action(
&self,
ip: &str,
action: &str,
within: Duration,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
// Query database for recent actions
Ok(false)
}
}
use basic_axum_rate_limit::{RateLimiter, RateLimitConfig};
use std::time::Duration;
let config = RateLimitConfig::new(
30, // 30 requests per minute
Duration::from_secs(15 * 60), // 15 minute block duration
);
let callbacks = MyCallbacks { db };
let rate_limiter = RateLimiter::new(config, callbacks);
The screener immediately blocks requests matching malicious patterns before they consume rate limit tokens:
use basic_axum_rate_limit::{RequestScreener, ScreeningConfig};
let screening_config = ScreeningConfig::new()
.with_path_patterns(vec![
// PHP attacks
r"\.php\d?$".to_string(),
r"/vendor/".to_string(),
// Git/config exposure
r"/\.git/".to_string(),
r"/\.env".to_string(),
// WordPress
r"/wp-admin".to_string(),
r"/wp-content".to_string(),
])
.with_user_agent_patterns(vec![
"zgrab".to_string(),
"masscan".to_string(),
"nuclei".to_string(),
"sqlmap".to_string(),
]);
let screener = RequestScreener::new(&screening_config)
.expect("Failed to compile screening patterns");
let rate_limiter = RateLimiter::new(config, callbacks)
.with_screener(screener);
By default, the middleware uses X-Forwarded-For and expects exactly one IP (from your trusted proxy). For other setups, configure the extraction strategy:
use basic_axum_rate_limit::{SecurityContextConfig, IpExtractionStrategy};
// Cloudflare CF-Connecting-IP header
let config = SecurityContextConfig::new()
.with_ip_extraction(IpExtractionStrategy::cloudflare());
// nginx with X-Real-IP
let config = SecurityContextConfig::new()
.with_ip_extraction(IpExtractionStrategy::x_real_ip());
// Direct connections (no proxy)
let config = SecurityContextConfig::new()
.with_ip_extraction(IpExtractionStrategy::SocketAddr);
// Custom header
let config = SecurityContextConfig::new()
.with_ip_extraction(IpExtractionStrategy::custom_header("X-Client-IP", 1));
use axum::Router;
use basic_axum_rate_limit::{security_context_middleware, rate_limit_middleware};
let app = Router::new()
.route("/api/endpoint", post(handler))
.layer(axum::middleware::from_fn_with_state(
rate_limiter,
rate_limit_middleware,
))
/* Your application middleware should be placed in between these layers.
* This allows the security_context_middleware to handle the post processing,
* refunding tokens, or docking extra tokens after requests have been handled.
*/
.layer(axum::middleware::from_fn(security_context_middleware));
For custom IP extraction strategies, use security_context_middleware_with_config:
use basic_axum_rate_limit::{
security_context_middleware_with_config, SecurityContextConfig, IpExtractionStrategy,
};
let security_config = SecurityContextConfig::new()
.with_ip_extraction(IpExtractionStrategy::cloudflare());
// ...
.layer(axum::middleware::from_fn_with_state(
security_config,
security_context_middleware_with_config,
));
use axum::Extension;
use basic_axum_rate_limit::SecurityContext;
async fn handler(Extension(ctx): Extension<SecurityContext>) {
let ip = ctx.ip_address;
let user_agent = ctx.user_agent;
}
This crate uses a token bucket algorithm for efficient rate limiting:
rate_limit_per_minute)rate_limit_per_minute / 60 per secondTo handle legitimate bursts (e.g., loading a page with many assets), new IP addresses get a grace period:
This allows a browser to load 25+ assets quickly on initial page load without triggering rate limits, since assets are then cached.
let config = RateLimitConfig::new(
50, // Max requests per minute
Duration::from_secs(15 * 60), // Block duration when limit exceeded
)
.with_grace_period(1) // Grace period in seconds (default: 1)
.with_cache_refund_ratio(0.5) // Refund 90% for cache hits (default: 0.5)
.with_error_penalty(2.0); // Extra tokens for errors (default: 2.0)
Defaults:
rate_limit_per_minute: 50block_duration: 15 minutes (900 seconds)grace_period_seconds: 1cache_refund_ratio: 0.5 (50% refund for 304 responses)error_penalty_tokens: 2.0 (additional token cost for 4xx/5xx)impl RateLimitConfig {
// Create with custom rate limit and block duration
pub fn new(rate_limit_per_minute: u32, block_duration: Duration) -> Self;
// Set grace period in seconds
pub fn with_grace_period(self, seconds: u64) -> Self;
// Set cache refund ratio (0.0 to 1.0)
pub fn with_cache_refund_ratio(self, ratio: f64) -> Self;
// Set error penalty in tokens (>= 0.0)
pub fn with_error_penalty(self, penalty: f64) -> Self;
// Get maximum tokens (equals rate_limit_per_minute)
pub fn max_tokens(&self) -> f64;
// Get token refill rate per second
pub fn refill_rate_per_second(&self) -> f64;
}
pub struct SecurityContext {
pub ip_address: String,
pub user_agent: String,
}
#[async_trait]
pub trait OnBlocked: Send + Sync {
async fn on_blocked(&self, ip: &str, path: &str, context: &SecurityContext);
}
#[async_trait]
pub trait ActionChecker: Send + Sync {
async fn check_recent_action(
&self,
ip: &str,
action: &str,
within: Duration,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>>;
}
Rate limits are applied per IP address globally, not per endpoint. If an IP makes 30 (or whatever the limit is) requests to different endpoints, they will be rate limited. There is support for custom per-action limiting with callbacks but that is a different than the token bucket limiting.
The token bucket algorithm naturally allows bursts:
HTTP cache validation requests (304 Not Modified) consume reduced tokens:
How it works:
304 Not Modified → refunds 0.5 tokens (default)If-None-Match headersExample:
Full requests: 30 requests/minute (1.0 token each)
Cache requests: 60 requests/minute (0.5 token effective cost)
This naturally handles browser cache validation without creating security holes.
Failed requests (4xx and 5xx status codes) consume additional tokens to penalize malicious behavior:
How it works:
4xx or 5xx → consumes 1.0 additional token (default)Why this helps:
Example token costs:
200 OK: 1.0 token
304 Not Modified: 0.5 token (with refund)
404 Not Found: 2.0 tokens (1.0 + 1.0 penalty)
403 Forbidden: 2.0 tokens (1.0 + 1.0 penalty)
500 Server Error: 2.0 tokens (1.0 + 1.0 penalty)
Impact with 50 token bucket:
Legitimate traffic (mostly 2xx): ~50 requests/min
Scanner (all 404s): 25 requests/min (50 tokens / 2.0 cost)
Mixed (40 success, 10 failures): ~45 requests/min
This creates a reputation-based system where well-behaved clients get more capacity while malicious traffic is throttled more aggressively.
The RequestScreener identifies obviously malicious requests (vulnerability scanners, path enumeration). Screened requests consume exactly 1 token regardless of response status, bypassing error penalties.
pub struct ScreeningConfig {
/// Regex patterns that match malicious paths
pub path_patterns: Vec<String>,
/// Regex patterns that match malicious user agents (case-insensitive)
pub user_agent_patterns: Vec<String>,
}
Both pattern sets are compiled into a RegexSet for efficient single-pass matching. User agent patterns are automatically made case-insensitive.
impl ScreeningConfig {
pub fn new() -> Self;
pub fn with_path_pattern(self, pattern: &str) -> Self;
pub fn with_path_patterns(self, patterns: Vec<String>) -> Self;
pub fn with_user_agent_pattern(self, pattern: &str) -> Self;
pub fn with_user_agent_patterns(self, patterns: Vec<String>) -> Self;
}
The metrics feature enables the metrics endpoint and the metrics logging methods. These are used for load testing with prometheus logging outside of production environments. They probably could be used in production environments but you would want to secure the endpoint or change the implementation to use a flat file for metrics rather than an api endpoint. The Cargo.toml looks like this for my setup.
[features]
default = []
loadtest = ["basic-axum-rate-limit/metrics"]
[dependencies]
basic-axum-rate-limit = "0.2.1"
let config = ScreeningConfig::new()
.with_path_patterns(vec![
r"\.php\d?$".to_string(), // PHP files
r"/\.git/".to_string(), // Git exposure
r"/\.env".to_string(), // Environment files
r"/wp-admin".to_string(), // WordPress admin
])
.with_user_agent_patterns(vec![
"zgrab".to_string(),
"nuclei".to_string(),
"sqlmap".to_string(),
]);
RegexSetRegexSetGNU Lesser General Public License v3.0 or later.