fix: replace broken DashMap rate limiting with tower-governor middleware
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 4s
CI Pipeline / Clippy Lints (push) Successful in 1m1s
CI Pipeline / Rust Unit Tests (push) Successful in 1m21s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 16s
CI Pipeline / Build .deb & Release (push) Has been skipped
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 4s
CI Pipeline / Clippy Lints (push) Successful in 1m1s
CI Pipeline / Rust Unit Tests (push) Successful in 1m21s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 16s
CI Pipeline / Build .deb & Release (push) Has been skipped
- Replace custom DashMap<IpAddr, Instant> rate limiting in enrollment.rs that fell back to 0.0.0.0 when X-Forwarded-For was missing, causing ALL enrollment traffic to share a single global rate limit bucket - Use tower_governor with SmartIpKeyExtractor for proper per-IP rate limiting that respects X-Forwarded-For headers (critical behind HAProxy) - Add three configurable rate limit tiers via config.toml: * Enrollment: 5 req/min per IP, burst 3 (strict) * Auth: 20 req/min per IP, burst 10 (moderate) * API: 120 req/min per IP, burst 30 (normal) - Remove enrollment_rate_limits from AppState and cleanup task - Remove manual rate limit code from enrollment.rs (headers param, IP extraction) - Add into_make_service_with_connect_info for ConnectInfo fallback - Add RateLimitConfig to AppConfig with sensible defaults Fixes: #1
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
@ -16,8 +16,6 @@ use pm_core::{
|
||||
};
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use serde::Serialize;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct HostConflict {
|
||||
@ -34,43 +32,12 @@ pub fn router() -> Router<AppState> {
|
||||
|
||||
/// POST /api/v1/enroll
|
||||
/// Initiates host self-enrollment.
|
||||
/// Rate limiting is handled by tower-governor middleware (per-IP, configurable).
|
||||
async fn enroll_host(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<CreateEnrollmentRequest>,
|
||||
) -> Result<Response, (StatusCode, Json<serde_json::Value>)> {
|
||||
// 1. IP-based Rate Limiting
|
||||
// Prefer real IP from headers if behind proxy (e.g., X-Forwarded-For)
|
||||
let ip = headers
|
||||
.get("x-forwarded-for")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.split(',').next())
|
||||
.and_then(|h| h.trim().parse::<IpAddr>().ok())
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
"No X-Forwarded-For header found for enrollment request from public endpoint"
|
||||
);
|
||||
// Default to a placeholder IP since we can't extract the socket addr without the ConnectInfo layer
|
||||
"0.0.0.0".parse().unwrap()
|
||||
});
|
||||
|
||||
{
|
||||
let mut rate_limits = state
|
||||
.enrollment_rate_limits
|
||||
.entry(ip)
|
||||
.or_insert(Instant::now() - std::time::Duration::from_secs(3600));
|
||||
let last_request = rate_limits.value();
|
||||
if last_request.elapsed().as_secs() < 60 {
|
||||
// 1 request per minute per IP
|
||||
return Err((
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
Json(serde_json::json!({ "error": "Rate limit exceeded. Try again in a minute." })),
|
||||
));
|
||||
}
|
||||
*rate_limits = Instant::now();
|
||||
}
|
||||
|
||||
// 2. Generate secure random polling token
|
||||
// Generate secure random polling token
|
||||
let polling_token: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(64)
|
||||
|
||||
Reference in New Issue
Block a user