use crate::handler::BoxFuture;
use crate::middleware::{Middleware, Next};
use crate::request::Request;
use crate::response::Response;
use rustlavel_core::Config;
use std::net::IpAddr;
#[derive(Debug, Clone, Default)]
pub struct Forwarded {
pub ip: Option<String>,
pub scheme: Option<String>,
pub host: Option<String>,
pub port: Option<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Trusted {
Any,
Address(IpAddr),
Network { base: IpAddr, prefix: u8 },
}
impl Trusted {
fn parse(entry: &str) -> Option<Trusted> {
let entry = entry.trim();
if entry == "*" || entry.eq_ignore_ascii_case("any") {
return Some(Trusted::Any);
}
match entry.split_once('/') {
None => entry.parse().ok().map(Trusted::Address),
Some((base, prefix)) => {
let base: IpAddr = base.trim().parse().ok()?;
let prefix: u8 = prefix.trim().parse().ok()?;
let width = if base.is_ipv4() { 32 } else { 128 };
(prefix <= width).then_some(Trusted::Network { base, prefix })
}
}
}
fn contains(&self, address: IpAddr) -> bool {
match self {
Trusted::Any => true,
Trusted::Address(trusted) => *trusted == address,
Trusted::Network { base, prefix } => in_network(*base, *prefix, address),
}
}
}
fn in_network(base: IpAddr, prefix: u8, address: IpAddr) -> bool {
let (base, address) = match (base, address) {
(IpAddr::V4(base), IpAddr::V4(address)) => (base.octets().to_vec(), address.octets().to_vec()),
(IpAddr::V6(base), IpAddr::V6(address)) => (base.octets().to_vec(), address.octets().to_vec()),
_ => return false,
};
let whole_bytes = (prefix / 8) as usize;
if base[..whole_bytes] != address[..whole_bytes] {
return false;
}
let leftover = prefix % 8;
if leftover == 0 {
return true;
}
let mask = 0xFFu8 << (8 - leftover);
base[whole_bytes] & mask == address[whole_bytes] & mask
}
#[derive(Debug, Clone, Default)]
pub struct TrustProxies {
proxies: Vec<Trusted>,
}
impl TrustProxies {
pub fn none() -> Self {
TrustProxies::default()
}
pub fn at<I, S>(proxies: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
TrustProxies {
proxies: proxies.into_iter().filter_map(|p| Trusted::parse(p.as_ref())).collect(),
}
}
pub fn any() -> Self {
TrustProxies { proxies: vec![Trusted::Any] }
}
pub fn from_config(config: &Config) -> Self {
TrustProxies::at(config.list("trustedproxy.proxies"))
}
fn trusts(&self, address: IpAddr) -> bool {
self.proxies.iter().any(|proxy| proxy.contains(address))
}
fn client_from(&self, chain: &str) -> Option<String> {
let hops: Vec<&str> = chain.split(',').map(str::trim).filter(|h| !h.is_empty()).collect();
for hop in hops.iter().rev() {
let address = strip_port(hop).parse::<IpAddr>().ok()?;
if !self.trusts(address) {
return Some(address.to_string());
}
}
hops.first().map(|hop| strip_port(hop).to_string())
}
}
fn strip_port(hop: &str) -> &str {
let hop = hop.trim();
if let Some(rest) = hop.strip_prefix('[') {
return rest.split(']').next().unwrap_or(hop);
}
match hop.rsplit_once(':') {
Some((address, _)) if !address.contains(':') => address,
_ => hop,
}
}
impl Middleware for TrustProxies {
fn handle(&self, mut request: Request, next: Next) -> BoxFuture<Response> {
let peer = request.peer_addr().map(|addr| addr.ip());
if !peer.is_some_and(|peer| self.trusts(peer)) {
return next.run(request);
}
let mut forwarded = Forwarded::default();
if let Some(chain) = request.header("x-forwarded-for") {
forwarded.ip = self.client_from(chain);
}
if let Some(scheme) = request.header("x-forwarded-proto") {
let scheme = scheme.split(',').next().unwrap_or("").trim().to_ascii_lowercase();
if scheme == "http" || scheme == "https" {
forwarded.scheme = Some(scheme);
}
}
if let Some(host) = request.header("x-forwarded-host")
&& let Some(host) = host.split(',').next().map(str::trim).filter(|h| !h.is_empty())
{
forwarded.host = Some(host.to_string());
}
if let Some(port) = request.header("x-forwarded-port") {
forwarded.port = port.split(',').next().and_then(|p| p.trim().parse().ok());
}
request.extend(forwarded);
next.run(request)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::method::Method;
use crate::router::Router;
use crate::testing::TestClient;
fn client(trust: TrustProxies) -> TestClient {
let mut router = Router::new();
router.middleware(trust);
router.get("/", |req: Request| async move {
Response::text(format!(
"{}|{}|{}",
req.ip().unwrap_or_else(|| "none".into()),
req.scheme(),
req.forwarded_host().unwrap_or("none")
))
});
TestClient::new(router)
}
fn from(peer: &str, chain: &str) -> Request {
Request::new(Method::Get, "/")
.with_peer(format!("{peer}:44321").parse().expect("an address"))
.with_header("x-forwarded-for", chain)
}
#[tokio::test]
async fn without_a_trust_list_the_header_is_ignored_entirely() {
let response = client(TrustProxies::none()).send(from("203.0.113.9", "1.2.3.4")).await;
assert_eq!(response.body(), "203.0.113.9|http|none", "the peer, not what it claimed");
}
#[tokio::test]
async fn an_untrusted_peer_cannot_choose_its_own_address() {
let trust = TrustProxies::at(["10.0.0.0/8"]);
let response = client(trust).send(from("203.0.113.9", "9.9.9.9")).await;
assert_eq!(response.body(), "203.0.113.9|http|none");
}
#[tokio::test]
async fn a_trusted_proxy_is_believed() {
let trust = TrustProxies::at(["10.0.0.0/8"]);
let response = client(trust).send(from("10.1.2.3", "203.0.113.9")).await;
assert_eq!(response.body(), "203.0.113.9|http|none");
}
#[tokio::test]
async fn trusted_hops_are_stripped_from_the_right() {
let trust = TrustProxies::at(["10.0.0.0/8"]);
let request = from("10.0.0.2", "203.0.113.9, 10.0.0.1, 10.0.0.2");
assert_eq!(client(trust).send(request).await.body(), "203.0.113.9|http|none");
}
#[tokio::test]
async fn a_spoofed_prefix_before_the_real_client_is_not_believed() {
let trust = TrustProxies::at(["10.0.0.0/8"]);
let request = from("10.0.0.1", "9.9.9.9, 203.0.113.9");
assert_eq!(client(trust).send(request).await.body(), "203.0.113.9|http|none");
}
#[tokio::test]
async fn ports_are_stripped_from_forwarded_addresses() {
let trust = TrustProxies::at(["10.0.0.0/8"]);
let request = from("10.0.0.1", "203.0.113.9:51234");
assert_eq!(client(trust).send(request).await.body(), "203.0.113.9|http|none");
}
#[tokio::test]
async fn the_scheme_and_host_come_from_a_trusted_proxy_only() {
let trust = TrustProxies::at(["10.0.0.0/8"]);
let trusted = from("10.0.0.1", "203.0.113.9")
.with_header("x-forwarded-proto", "https")
.with_header("x-forwarded-host", "app.example.com");
assert_eq!(client(trust.clone()).send(trusted).await.body(), "203.0.113.9|https|app.example.com");
let spoofed = from("198.51.100.7", "1.2.3.4")
.with_header("x-forwarded-proto", "https")
.with_header("x-forwarded-host", "evil.example");
assert_eq!(client(trust).send(spoofed).await.body(), "198.51.100.7|http|none");
}
#[tokio::test]
async fn any_trusts_whoever_connected() {
let response = client(TrustProxies::any()).send(from("203.0.113.9", "1.2.3.4")).await;
assert_eq!(response.body(), "1.2.3.4|http|none");
}
#[test]
fn cidr_matching_covers_both_families_and_odd_prefixes() {
let ten = Trusted::parse("10.0.0.0/8").unwrap();
assert!(ten.contains("10.255.255.255".parse().unwrap()));
assert!(!ten.contains("11.0.0.1".parse().unwrap()));
let odd = Trusted::parse("192.168.4.0/22").unwrap();
assert!(odd.contains("192.168.7.255".parse().unwrap()));
assert!(!odd.contains("192.168.8.1".parse().unwrap()));
let v6 = Trusted::parse("2001:db8::/32").unwrap();
assert!(v6.contains("2001:db8:1234::1".parse().unwrap()));
assert!(!v6.contains("2001:db9::1".parse().unwrap()));
assert!(!v6.contains("10.0.0.1".parse().unwrap()), "families never match");
assert_eq!(Trusted::parse("not an address"), None);
assert_eq!(Trusted::parse("10.0.0.0/33"), None);
assert_eq!(Trusted::parse("*"), Some(Trusted::Any));
}
#[test]
fn a_typo_in_the_list_is_dropped_rather_than_widening_it() {
let trust = TrustProxies::at(["10.0.0.0/8", "hello", ""]);
assert_eq!(trust.proxies.len(), 1);
assert!(!trust.trusts("203.0.113.9".parse().unwrap()));
}
#[test]
fn from_config_reads_a_comma_separated_env_value() {
let config = Config::new();
config.set("trustedproxy.proxies", "10.0.0.0/8, 192.168.1.7");
let trust = TrustProxies::from_config(&config);
assert!(trust.trusts("10.9.9.9".parse().unwrap()));
assert!(trust.trusts("192.168.1.7".parse().unwrap()));
assert!(!trust.trusts("192.168.1.8".parse().unwrap()));
assert!(!TrustProxies::from_config(&Config::new()).trusts("10.0.0.1".parse().unwrap()));
}
#[test]
fn ipv6_hops_keep_their_colons() {
assert_eq!(strip_port("[2001:db8::1]:443"), "2001:db8::1");
assert_eq!(strip_port("2001:db8::1"), "2001:db8::1");
assert_eq!(strip_port("1.2.3.4:80"), "1.2.3.4");
assert_eq!(strip_port("1.2.3.4"), "1.2.3.4");
}
}