Fix: rate-limiter (tower_governor) считал весь трафик одним клиентом
CI / fmt + clippy + test (push) Has been cancelled
Build & Push Image / Build and push to Gitea Registry (push) Successful in 3m23s
Build & Push Image / Build and push Caddy (Cloudflare DNS) image (push) Successful in 20s

Дефолтный PeerIpKeyExtractor брал IP из TCP-peer'а, а не заголовков — а
nginx проксирует локально, так что peer всегда 127.0.0.1. В итоге лимит
"3 запроса/5с" на /auth и "10/100мс" на остальной API/v1 был не per-visitor,
а глобальным на весь прод: несколько попыток логина от кого угодно — и все
остальные посетители ловили 429 на /auth (это и мешало зайти в свежесозданную
админку — 401 на пробное неверное дело быстро съедал бюджет, дальше только
429). Добавлен SmartIpKeyExtractor (x-forwarded-for/x-real-ip/forwarded,
именно то, что реально шлёт nginx) в оба GovernorConfigBuilder.
This commit is contained in:
2026-07-11 03:30:52 +07:00
parent 74aab70a29
commit dca8d067f3
+11 -1
View File
@@ -17,7 +17,9 @@ use axum::{
use metrics_exporter_prometheus::{Matcher, PrometheusBuilder};
use serde_json::json;
use std::{sync::Arc, time::Instant};
use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer};
use tower_governor::{
governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, GovernorLayer,
};
use tower_http::{
cors::CorsLayer,
services::{ServeDir, ServeFile},
@@ -111,8 +113,15 @@ pub fn create_router(state: ApiState, frontend_dir: &str) -> Router {
let static_path = std::path::PathBuf::from(frontend_dir);
let index_path = static_path.join("index.html");
// SmartIpKeyExtractor читает x-forwarded-for/x-real-ip/forwarded (в этом
// порядке), падая на peer IP только если их нет. Без этого — дефолтный
// PeerIpKeyExtractor видел бы просто 127.0.0.1 (nginx проксирует локально)
// ОДИН И ТОТ ЖЕ для абсолютно всех посетителей сразу, и лимит превращался
// бы в глобальный на весь сервис, а не per-visitor (см. incident: 3 попытки
// логина от кого угодно — и /auth 429 для всех остальных до истечения окна).
let gov_conf = Arc::new(
GovernorConfigBuilder::default()
.key_extractor(SmartIpKeyExtractor)
.per_millisecond(100)
.burst_size(10)
.finish()
@@ -121,6 +130,7 @@ pub fn create_router(state: ApiState, frontend_dir: &str) -> Router {
let seed_limit_conf = Arc::new(
GovernorConfigBuilder::default()
.key_extractor(SmartIpKeyExtractor)
.per_millisecond(5000)
.burst_size(3)
.finish()