fix: rate limit per real client IP behind reverse proxy
Replace PeerIpKeyExtractor with custom RealIpKeyExtractor that extracts the real client IP from X-Real-IP/X-Forwarded-For headers when the request comes from a trusted proxy (default 127.0.0.1). This fixes rate limiting being applied to the proxy IP instead of individual clients when deployed behind Nginx.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
use actix_governor::{Governor, GovernorConfigBuilder};
|
use actix_governor::{Governor, GovernorConfigBuilder, KeyExtractor, SimpleKeyExtractionError};
|
||||||
|
use actix_web::dev::ServiceRequest;
|
||||||
use actix_web::middleware;
|
use actix_web::middleware;
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
use actix_web::{App, HttpResponse, HttpServer, Responder, web};
|
use actix_web::{App, HttpResponse, HttpServer, Responder, web};
|
||||||
@@ -10,6 +11,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use bal_server::db::{
|
use bal_server::db::{
|
||||||
DatabasePool, InsertInpData, InsertOutData, InsertTxData, check_duplicate_txids,
|
DatabasePool, InsertInpData, InsertOutData, InsertTxData, check_duplicate_txids,
|
||||||
@@ -130,6 +133,7 @@ struct ActixConfig {
|
|||||||
rate_limit_default: (u64, u32),
|
rate_limit_default: (u64, u32),
|
||||||
workers: usize,
|
workers: usize,
|
||||||
max_connections: usize,
|
max_connections: usize,
|
||||||
|
trusted_proxy: IpAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_actix_config() -> ActixConfig {
|
fn parse_actix_config() -> ActixConfig {
|
||||||
@@ -190,6 +194,10 @@ fn parse_actix_config() -> ActixConfig {
|
|||||||
.unwrap_or("100".to_string())
|
.unwrap_or("100".to_string())
|
||||||
.parse::<usize>()
|
.parse::<usize>()
|
||||||
.unwrap_or(100),
|
.unwrap_or(100),
|
||||||
|
trusted_proxy: env::var("BAL_SERVER_TRUSTED_PROXY")
|
||||||
|
.unwrap_or_else(|_| "127.0.0.1".to_string())
|
||||||
|
.parse::<IpAddr>()
|
||||||
|
.unwrap_or(IpAddr::from_str("127.0.0.1").unwrap()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +231,51 @@ fn is_valid_ip(ip: &str) -> bool {
|
|||||||
ip.parse::<std::net::IpAddr>().is_ok()
|
ip.parse::<std::net::IpAddr>().is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
struct RealIpKeyExtractor;
|
||||||
|
|
||||||
|
impl KeyExtractor for RealIpKeyExtractor {
|
||||||
|
type Key = IpAddr;
|
||||||
|
type KeyExtractionError = SimpleKeyExtractionError<&'static str>;
|
||||||
|
|
||||||
|
fn extract(&self, req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
|
||||||
|
let proxy_ip = req
|
||||||
|
.app_data::<web::Data<IpAddr>>()
|
||||||
|
.map(|ip| *ip.get_ref())
|
||||||
|
.unwrap_or_else(|| IpAddr::from_str("0.0.0.0").unwrap());
|
||||||
|
|
||||||
|
let peer_ip = req.peer_addr().map(|socket| socket.ip());
|
||||||
|
let connection_info = req.connection_info();
|
||||||
|
|
||||||
|
match peer_ip {
|
||||||
|
Some(peer) if peer == proxy_ip => connection_info
|
||||||
|
.realip_remote_addr()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
SimpleKeyExtractionError::new("Could not extract real IP address from request")
|
||||||
|
})
|
||||||
|
.and_then(|str| {
|
||||||
|
str.parse::<IpAddr>().map_err(|_| {
|
||||||
|
SimpleKeyExtractionError::new(
|
||||||
|
"Could not extract real IP address from request",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
_ => connection_info
|
||||||
|
.peer_addr()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
SimpleKeyExtractionError::new("Could not extract peer IP address from request")
|
||||||
|
})
|
||||||
|
.and_then(|str| {
|
||||||
|
str.parse::<IpAddr>().map_err(|_| {
|
||||||
|
SimpleKeyExtractionError::new(
|
||||||
|
"Could not extract peer IP address from request",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn extract_client_ip(req: &actix_web::HttpRequest) -> String {
|
fn extract_client_ip(req: &actix_web::HttpRequest) -> String {
|
||||||
if let Some(val) = req.headers().get("X-Real-IP")
|
if let Some(val) = req.headers().get("X-Real-IP")
|
||||||
&& let Ok(s) = val.to_str()
|
&& let Ok(s) = val.to_str()
|
||||||
@@ -776,9 +829,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
let bind_address = data.cfg.bind_address.clone();
|
let bind_address = data.cfg.bind_address.clone();
|
||||||
let bind_port = data.cfg.bind_port;
|
let bind_port = data.cfg.bind_port;
|
||||||
|
|
||||||
let governor_conf = GovernorConfigBuilder::const_default()
|
let governor_conf = GovernorConfigBuilder::default()
|
||||||
.seconds_per_request(actix_cfg.rate_limit_pushtxs.0)
|
.seconds_per_request(actix_cfg.rate_limit_pushtxs.0)
|
||||||
.burst_size(actix_cfg.rate_limit_pushtxs.1)
|
.burst_size(actix_cfg.rate_limit_pushtxs.1)
|
||||||
|
.key_extractor(RealIpKeyExtractor)
|
||||||
.finish()
|
.finish()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -788,6 +842,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
App::new()
|
App::new()
|
||||||
.app_data(web::PayloadConfig::default().limit(actix_cfg.max_body_size))
|
.app_data(web::PayloadConfig::default().limit(actix_cfg.max_body_size))
|
||||||
.app_data(data.clone())
|
.app_data(data.clone())
|
||||||
|
.app_data(web::Data::new(actix_cfg.trusted_proxy))
|
||||||
.wrap(middleware::Logger::default())
|
.wrap(middleware::Logger::default())
|
||||||
.wrap(middleware::Compress::default())
|
.wrap(middleware::Compress::default())
|
||||||
.wrap(Governor::new(&governor_conf))
|
.wrap(Governor::new(&governor_conf))
|
||||||
|
|||||||
Reference in New Issue
Block a user