use std::{
convert::Infallible,
fmt,
net::{IpAddr, SocketAddr},
str::FromStr,
};
use axum::{
extract::{ConnectInfo, FromRequestParts, Request, State},
http::{HeaderMap, HeaderName, request::Parts},
middleware::Next,
response::Response,
};
const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
const MAX_HOPS: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProxyNet {
network: IpAddr,
prefix: u8,
}
impl ProxyNet {
#[must_use]
pub fn contains(&self, address: IpAddr) -> bool {
match (self.network, address.to_canonical()) {
(IpAddr::V4(network), IpAddr::V4(address)) => {
masked(u128::from(network.to_bits()), 32, self.prefix)
== masked(u128::from(address.to_bits()), 32, self.prefix)
}
(IpAddr::V6(network), IpAddr::V6(address)) => {
masked(network.to_bits(), 128, self.prefix)
== masked(address.to_bits(), 128, self.prefix)
}
_ => false,
}
}
}
const fn masked(bits: u128, width: u8, prefix: u8) -> u128 {
if prefix == 0 {
0
} else {
bits >> (width - prefix)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidProxyNet;
impl fmt::Display for InvalidProxyNet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("not an address or CIDR range")
}
}
impl std::error::Error for InvalidProxyNet {}
impl FromStr for ProxyNet {
type Err = InvalidProxyNet;
fn from_str(text: &str) -> Result<Self, InvalidProxyNet> {
let (address, prefix) = text
.split_once('/')
.map_or((text, None), |(a, p)| (a, Some(p)));
let network = address
.parse::<IpAddr>()
.map_err(|_| InvalidProxyNet)?
.to_canonical();
let width = if network.is_ipv4() { 32 } else { 128 };
let prefix = match prefix {
Some(prefix) => prefix.parse::<u8>().map_err(|_| InvalidProxyNet)?,
None => width,
};
if prefix > width {
return Err(InvalidProxyNet);
}
Ok(Self { network, prefix })
}
}
impl fmt::Display for ProxyNet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.network, self.prefix)
}
}
#[must_use]
pub fn resolve(peer: IpAddr, headers: &HeaderMap, trusted: &[ProxyNet]) -> IpAddr {
let is_trusted = |address: IpAddr| trusted.iter().any(|net| net.contains(address));
let mut client = peer.to_canonical();
if !is_trusted(client) {
return client;
}
let values = headers.get_all(&X_FORWARDED_FOR);
let entries = values
.iter()
.rev()
.map(|value| value.to_str().unwrap_or(""))
.flat_map(|value| value.rsplit(','));
for entry in entries.take(MAX_HOPS) {
let Ok(address) = entry.trim().parse::<IpAddr>() else {
break;
};
client = address.to_canonical();
if !is_trusted(client) {
break;
}
}
client
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClientAddr(pub Option<IpAddr>);
impl<S: Send + Sync> FromRequestParts<S> for ClientAddr {
type Rejection = Infallible;
#[allow(unknown_lints, clippy::unused_async_trait_impl)]
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Infallible> {
Ok(Self(parts.extensions.get::<IpAddr>().copied()))
}
}
pub(crate) async fn record(
State(trusted): State<std::sync::Arc<Vec<ProxyNet>>>,
mut request: Request,
next: Next,
) -> Response {
let peer = request
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|info| info.0.ip());
if let Some(peer) = peer {
let client = resolve(peer, request.headers(), &trusted);
request.extensions_mut().insert(client);
}
next.run(request).await
}
#[cfg(test)]
mod tests {
use axum::http::HeaderValue;
use super::*;
const PEER: &str = "10.0.0.1";
fn trusted() -> Vec<ProxyNet> {
["10.0.0.0/8", "fd00::/8"]
.iter()
.map(|net| net.parse().unwrap())
.collect()
}
fn resolved(peer: &str, forwarded: &[&str]) -> String {
let mut headers = HeaderMap::new();
for value in forwarded {
headers.append(&X_FORWARDED_FOR, HeaderValue::from_str(value).unwrap());
}
resolve(peer.parse().unwrap(), &headers, &trusted()).to_string()
}
#[test]
fn with_no_proxies_the_header_is_ignored() {
let mut headers = HeaderMap::new();
headers.insert(&X_FORWARDED_FOR, HeaderValue::from_static("1.2.3.4"));
let peer = PEER.parse().unwrap();
assert_eq!(resolve(peer, &headers, &[]).to_string(), PEER);
}
#[test]
fn a_caller_that_is_no_proxy_cannot_name_an_address() {
assert_eq!(resolved("203.0.113.9", &["1.2.3.4"]), "203.0.113.9");
}
#[test]
fn the_rightmost_untrusted_entry_is_the_client() {
assert_eq!(resolved(PEER, &["198.51.100.7"]), "198.51.100.7");
assert_eq!(resolved(PEER, &["198.51.100.7, 10.0.0.2"]), "198.51.100.7");
}
#[test]
fn an_entry_the_caller_wrote_is_not_believed() {
assert_eq!(resolved(PEER, &["1.2.3.4, 198.51.100.7"]), "198.51.100.7");
}
#[test]
fn repeated_headers_read_as_one_list() {
assert_eq!(resolved(PEER, &["1.2.3.4", "198.51.100.7"]), "198.51.100.7");
}
#[test]
fn an_unreadable_entry_ends_the_walk() {
assert_eq!(resolved(PEER, &["1.2.3.4, junk, 10.0.0.2"]), "10.0.0.2");
assert_eq!(resolved(PEER, &["junk"]), PEER);
assert_eq!(resolved(PEER, &[""]), PEER);
}
#[test]
fn with_only_proxies_the_leftmost_is_the_client() {
assert_eq!(resolved(PEER, &["10.0.0.3, 10.0.0.2"]), "10.0.0.3");
}
#[test]
fn a_long_header_stops_at_the_limit() {
let long = vec!["10.0.0.2"; 100].join(",");
assert_eq!(resolved(PEER, &[&format!("1.2.3.4,{long}")]), "10.0.0.2");
}
#[test]
fn an_ipv4_address_inside_ipv6_is_the_same_address() {
assert_eq!(
resolved("::ffff:10.0.0.1", &["198.51.100.7"]),
"198.51.100.7"
);
assert_eq!(resolved("::ffff:203.0.113.9", &[]), "203.0.113.9");
}
#[test]
fn ranges_read_and_match() {
let net: ProxyNet = "192.168.1.0/24".parse().unwrap();
assert!(net.contains("192.168.1.200".parse().unwrap()));
assert!(!net.contains("192.168.2.1".parse().unwrap()));
assert!(!net.contains("fd00::1".parse().unwrap()));
let all: ProxyNet = "0.0.0.0/0".parse().unwrap();
assert!(all.contains("8.8.8.8".parse().unwrap()));
let one: ProxyNet = "2001:db8::1".parse().unwrap();
assert!(one.contains("2001:db8::1".parse().unwrap()));
assert!(!one.contains("2001:db8::2".parse().unwrap()));
assert_eq!(one.to_string(), "2001:db8::1/128");
}
#[test]
fn a_bad_range_is_refused() {
for text in ["", "junk", "10.0.0.0/33", "fd00::/129", "10.0.0.0/x", "/8"] {
assert_eq!(text.parse::<ProxyNet>(), Err(InvalidProxyNet), "{text}");
}
}
}