use std::net::IpAddr;
use rainier_http::{ClientIp, Request, Response};
use crate::pipeline::{Middleware, Next};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cidr {
address: IpAddr,
prefix: u8,
}
impl Cidr {
pub fn parse(value: &str) -> Option<Self> {
let value = value.trim();
let (address, prefix) = match value.split_once('/') {
Some((address, prefix)) => (address, Some(prefix)),
None => (value, None),
};
let address: IpAddr = address.parse().ok()?;
let full = if address.is_ipv4() { 32 } else { 128 };
let prefix = match prefix {
Some(prefix) => {
let prefix: u8 = prefix.parse().ok()?;
if prefix > full {
return None;
}
prefix
}
None => full,
};
Some(Self { address, prefix })
}
pub fn contains(&self, candidate: IpAddr) -> bool {
match (self.address, candidate) {
(IpAddr::V4(network), IpAddr::V4(candidate)) => {
matches(&network.octets(), &candidate.octets(), self.prefix)
}
(IpAddr::V6(network), IpAddr::V6(candidate)) => {
matches(&network.octets(), &candidate.octets(), self.prefix)
}
_ => false,
}
}
}
fn matches(network: &[u8], candidate: &[u8], prefix: u8) -> bool {
let whole = (prefix / 8) as usize;
if network[..whole] != candidate[..whole] {
return false;
}
let remaining = prefix % 8;
if remaining == 0 {
return true;
}
let mask = 0xffu8 << (8 - remaining);
network[whole] & mask == candidate[whole] & mask
}
#[derive(Debug, Clone, Default)]
pub enum Trusted {
#[default]
None,
All,
These(Vec<Cidr>),
}
impl Trusted {
pub fn accepts(&self, peer: IpAddr) -> bool {
match self {
Trusted::None => false,
Trusted::All => true,
Trusted::These(networks) => networks.iter().any(|network| network.contains(peer)),
}
}
pub fn is_known_hop(&self, address: IpAddr) -> bool {
match self {
Trusted::None | Trusted::All => false,
Trusted::These(networks) => networks.iter().any(|network| network.contains(address)),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TrustProxies {
trusted: Trusted,
header: String,
}
impl TrustProxies {
pub fn new() -> Self {
Self { trusted: Trusted::None, header: "x-forwarded-for".to_string() }
}
pub fn all() -> Self {
Self { trusted: Trusted::All, ..Self::new() }
}
pub fn these(networks: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
let parsed = networks
.into_iter()
.filter_map(|network| {
let network = network.as_ref();
let parsed = Cidr::parse(network);
if parsed.is_none() {
tracing::warn!(%network, "ignoring an unparseable trusted proxy");
}
parsed
})
.collect();
Self { trusted: Trusted::These(parsed), ..Self::new() }
}
pub fn header(mut self, header: impl Into<String>) -> Self {
self.header = header.into().to_ascii_lowercase();
self
}
pub fn trusts_anything(&self) -> bool {
!matches!(self.trusted, Trusted::None)
}
pub fn resolve(&self, request: &Request, peer: IpAddr) -> IpAddr {
if !self.trusted.accepts(peer) {
return peer;
}
let Some(header) = request.header(&self.header) else {
return peer;
};
header
.split(',')
.rev()
.filter_map(|entry| entry.trim().parse::<IpAddr>().ok())
.find(|address| !self.trusted.is_known_hop(*address))
.unwrap_or(peer)
}
}
#[async_trait::async_trait]
impl Middleware for TrustProxies {
async fn handle(&self, mut request: Request, next: Next) -> Response {
if let Some(peer) = request.ip() {
let client = self.resolve(&request, peer);
if client != peer {
request.extensions_mut().insert(ClientIp(client));
}
}
next.run(request).await
}
fn name(&self) -> &'static str {
"TrustProxies"
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ip(value: &str) -> IpAddr {
value.parse().unwrap()
}
fn request(chain: &str) -> Request {
Request::builder().header("x-forwarded-for", chain).build()
}
#[test]
fn a_bare_address_is_a_single_host() {
let cidr = Cidr::parse("192.168.1.7").unwrap();
assert!(cidr.contains(ip("192.168.1.7")));
assert!(!cidr.contains(ip("192.168.1.8")));
}
#[test]
fn a_prefix_matches_its_network() {
let cidr = Cidr::parse("10.0.0.0/8").unwrap();
assert!(cidr.contains(ip("10.0.0.1")));
assert!(cidr.contains(ip("10.255.255.255")));
assert!(!cidr.contains(ip("11.0.0.1")));
}
#[test]
fn a_prefix_that_is_not_a_whole_byte_still_works() {
let cidr = Cidr::parse("172.16.0.0/12").unwrap();
assert!(cidr.contains(ip("172.16.0.1")));
assert!(cidr.contains(ip("172.31.255.255")));
assert!(!cidr.contains(ip("172.32.0.1")), "just outside the /12");
assert!(!cidr.contains(ip("172.15.255.255")));
}
#[test]
fn ipv6_works_too() {
assert!(Cidr::parse("::1").unwrap().contains(ip("::1")));
assert!(Cidr::parse("fd00::/8").unwrap().contains(ip("fd12:3456::1")));
assert!(!Cidr::parse("fd00::/8").unwrap().contains(ip("fe80::1")));
}
#[test]
fn families_do_not_cross() {
assert!(!Cidr::parse("10.0.0.0/8").unwrap().contains(ip("::ffff:10.0.0.1")));
assert!(!Cidr::parse("::/0").unwrap().contains(ip("10.0.0.1")));
}
#[test]
fn rubbish_does_not_parse() {
for bad in ["", "nonsense", "10.0.0.0/33", "10.0.0.0/-1", "::1/129", "10.0.0.0/x"] {
assert!(Cidr::parse(bad).is_none(), "{bad}");
}
}
#[test]
fn trusting_nobody_ignores_the_header() {
let middleware = TrustProxies::new();
assert!(!middleware.trusts_anything());
assert_eq!(middleware.resolve(&request("1.2.3.4"), ip("10.0.0.9")), ip("10.0.0.9"));
}
#[test]
fn an_untrusted_peer_is_believed_over_its_own_header() {
let middleware = TrustProxies::these(["10.0.0.0/8"]);
assert_eq!(middleware.resolve(&request("1.2.3.4"), ip("203.0.113.9")), ip("203.0.113.9"));
}
#[test]
fn a_trusted_peer_yields_the_client() {
let middleware = TrustProxies::these(["10.0.0.0/8"]);
assert_eq!(middleware.resolve(&request("203.0.113.9"), ip("10.0.0.1")), ip("203.0.113.9"));
}
#[test]
fn trusted_hops_are_walked_past_from_the_right() {
let middleware = TrustProxies::these(["10.0.0.0/8"]);
let chain = request("203.0.113.9, 10.0.0.5, 10.0.0.6");
assert_eq!(middleware.resolve(&chain, ip("10.0.0.1")), ip("203.0.113.9"));
}
#[test]
fn a_client_cannot_forge_an_address_by_prepending_one() {
let middleware = TrustProxies::these(["10.0.0.0/8"]);
let chain = request("1.2.3.4, 203.0.113.9, 10.0.0.5");
assert_eq!(
middleware.resolve(&chain, ip("10.0.0.1")),
ip("203.0.113.9"),
"the rightmost untrusted entry is the only one our own proxy vouched for"
);
}
#[test]
fn a_chain_of_only_trusted_hops_falls_back_to_the_peer() {
let middleware = TrustProxies::these(["10.0.0.0/8"]);
let chain = request("10.0.0.5, 10.0.0.6");
assert_eq!(middleware.resolve(&chain, ip("10.0.0.1")), ip("10.0.0.1"));
}
#[test]
fn rubbish_entries_are_skipped() {
let middleware = TrustProxies::these(["10.0.0.0/8"]);
let chain = request("203.0.113.9, unknown, 10.0.0.5");
assert_eq!(middleware.resolve(&chain, ip("10.0.0.1")), ip("203.0.113.9"));
}
#[test]
fn no_header_leaves_the_peer_alone() {
let middleware = TrustProxies::these(["10.0.0.0/8"]);
assert_eq!(middleware.resolve(&Request::builder().build(), ip("10.0.0.1")), ip("10.0.0.1"));
}
#[test]
fn trusting_everything_believes_the_chain() {
let middleware = TrustProxies::all();
assert!(middleware.trusts_anything());
assert_eq!(middleware.resolve(&request("203.0.113.9"), ip("10.0.0.1")), ip("203.0.113.9"));
}
#[test]
fn trusting_everything_takes_the_rightmost_entry() {
let middleware = TrustProxies::all();
let chain = request("1.2.3.4, 203.0.113.9");
assert_eq!(middleware.resolve(&chain, ip("10.0.0.1")), ip("203.0.113.9"));
}
#[test]
fn an_alternative_header_can_be_read() {
let middleware = TrustProxies::all().header("CF-Connecting-IP");
let request = Request::builder().header("cf-connecting-ip", "203.0.113.9").build();
assert_eq!(middleware.resolve(&request, ip("10.0.0.1")), ip("203.0.113.9"));
}
#[tokio::test]
async fn the_middleware_rewrites_the_recorded_address() {
use crate::pipeline::Pipeline;
let request = Request::builder()
.header("x-forwarded-for", "203.0.113.9, 10.0.0.5")
.build()
.with_extension(ClientIp(ip("10.0.0.1")));
let response = Pipeline::new()
.through(TrustProxies::these(["10.0.0.0/8"]))
.then(|request: Request| async move {
Response::text(request.ip().map(|ip| ip.to_string()).unwrap_or_default())
})
.run(request)
.await;
let body = response.into_http().into_body().collect().await.unwrap();
assert_eq!(body, "203.0.113.9");
}
#[tokio::test]
async fn a_request_with_no_recorded_address_passes_through() {
use crate::pipeline::Pipeline;
let response = Pipeline::new()
.through(TrustProxies::all())
.then(|request: Request| async move {
Response::text(format!("{}", request.ip().is_none()))
})
.run(request("203.0.113.9"))
.await;
let body = response.into_http().into_body().collect().await.unwrap();
assert_eq!(body, "true");
}
}