Lines
88.3 %
Functions
54.17 %
Branches
100 %
//! Deciding which address a request really came from.
//!
//! `X-Forwarded-For` is a claim, not a fact: anyone who can reach the port can
//! set it. It is trustworthy only on the hop written by a proxy we control, so
//! the header is consulted **only** when the immediate peer is a configured
//! trusted proxy, and ignored entirely otherwise. Without that check a per-IP
//! throttle keyed on the header is bypassed by changing one header value.
use axum::http::HeaderMap;
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
#[cfg(test)]
mod tests;
/// An address range trusted to speak for its clients.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cidr {
network: IpAddr,
prefix: u8,
}
/// Network bits kept when throttling an IPv6 client.
///
/// A single customer is routinely handed a whole /64, so keying on the full
/// address would give one attacker 2^64 independent budgets — the limit would
/// exist but never bind. /64 is the smallest block a client is assumed to
/// control entirely.
const IPV6_CLIENT_PREFIX: u32 = 64;
/// Collapses an address to the identity a throttle should count.
/// IPv4 is used as-is; IPv6 is truncated to [`IPV6_CLIENT_PREFIX`] so host
/// rotation inside one allocation cannot mint fresh budgets.
#[must_use]
pub fn throttle_key(addr: IpAddr) -> IpAddr {
match addr.to_canonical() {
IpAddr::V4(v4) => IpAddr::V4(v4),
IpAddr::V6(v6) => {
let masked = u128::from(v6) & (u128::MAX << (128 - IPV6_CLIENT_PREFIX));
IpAddr::V6(Ipv6Addr::from(masked))
/// Joins every `X-Forwarded-For` value on the request, in order.
/// A conforming proxy sends one comma-separated header, but nothing stops a
/// client from sending its own and a proxy from appending a second line. Only
/// the rightmost hop across all of them is evidence, so they must be read as
/// one list.
pub fn forwarded_for(headers: &HeaderMap) -> Option<String> {
let hops: Vec<&str> = headers
.get_all("x-forwarded-for")
.iter()
.filter_map(|value| value.to_str().ok())
.collect();
(!hops.is_empty()).then(|| hops.join(","))
impl Cidr {
/// Parses `10.104.0.0/16`, or a bare address as a single-host range.
pub fn parse(spec: &str) -> Option<Self> {
let (addr, prefix) = match spec.split_once('/') {
Some((addr, prefix)) => (addr, prefix.parse().ok()?),
None => (spec, 0),
};
let network: IpAddr = addr.trim().parse().ok()?;
let full = if network.is_ipv4() { 32 } else { 128 };
let prefix = if spec.contains('/') { prefix } else { full };
(prefix <= full).then_some(Self { network, prefix })
pub fn contains(&self, addr: IpAddr) -> bool {
match (self.network, addr) {
(IpAddr::V4(net), IpAddr::V4(other)) => {
masked(&net.octets(), &other.octets(), self.prefix)
(IpAddr::V6(net), IpAddr::V6(other)) => {
// A v4-mapped peer must not match a v6 range, or vice versa.
_ => false,
/// Compares the first `prefix` bits of two addresses.
fn masked(network: &[u8], addr: &[u8], prefix: u8) -> bool {
let whole = usize::from(prefix / 8);
let bits = prefix % 8;
if network[..whole] != addr[..whole] {
return false;
if bits == 0 {
return true;
let mask = 0xffu8 << (8 - bits);
network[whole] & mask == addr[whole] & mask
/// Why a trusted-proxy configuration was rejected at startup.
#[derive(Debug)]
pub enum TrustedProxyError {
Unset,
NoUsableEntry(String),
Unparseable(String),
impl std::fmt::Display for TrustedProxyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unset => write!(
f,
"TRUSTED_PROXIES is not set. Give the CIDRs of the proxies in \
front of this server, or `none` if it is directly exposed"
),
Self::NoUsableEntry(spec) => write!(
"TRUSTED_PROXIES ({spec:?}) contains no usable CIDR; use `none` \
to trust no proxy"
Self::Unparseable(spec) => write!(
"TRUSTED_PROXIES ({spec:?}) has an entry that is not a CIDR. Every \
entry must parse: one bad range silently un-trusts an ingress \
path and puts its users on a shared login budget"
impl std::error::Error for TrustedProxyError {}
/// The proxies whose `X-Forwarded-For` we believe.
#[derive(Debug, Clone, Default)]
pub struct TrustedProxies(Vec<Cidr>);
impl TrustedProxies {
/// Reads a comma-separated list of CIDRs, skipping anything unparseable.
/// Trusts nothing when the list is empty, so the peer address is always
/// used. Prefer [`Self::from_env`], which refuses both to guess and to
/// tolerate a typo.
pub fn from_spec(spec: &str) -> Self {
Self(Self::entries(spec).into_iter().flatten().collect())
/// Each entry, `None` where it did not parse.
fn entries(spec: &str) -> Vec<Option<Cidr>> {
spec.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(Cidr::parse)
.collect()
/// Reads `TRUSTED_PROXIES`, which must be set explicitly.
/// Neither default is safe to assume, which is why there is none. Trusting
/// nothing while actually behind a proxy keys *every* user on the ingress
/// address, so one attacker exhausts the shared budget and no one can log
/// in. Trusting something while directly exposed lets anyone forge their
/// identity. Set a CIDR list, or the literal `none` for a directly-exposed
/// deployment.
/// EVERY entry must parse. Dropping a bad one with a warning would leave a
/// typo in one range silently un-trusting one ingress path, collapsing that
/// path's users onto a single shared login budget — the failure this
/// variable exists to prevent, reintroduced quietly.
/// # Errors
/// If the variable is unset, has no entries, or any entry does not parse.
pub fn from_env() -> Result<Self, TrustedProxyError> {
let spec = std::env::var("TRUSTED_PROXIES").map_err(|_| TrustedProxyError::Unset)?;
if spec.trim() == "none" {
return Ok(Self(Vec::new()));
let entries = Self::entries(&spec);
if entries.is_empty() {
return Err(TrustedProxyError::NoUsableEntry(spec));
if entries.iter().any(Option::is_none) {
return Err(TrustedProxyError::Unparseable(spec));
Ok(Self::from_spec(&spec))
pub fn trusts(&self, addr: IpAddr) -> bool {
let addr = addr.to_canonical();
self.0.iter().any(|cidr| cidr.contains(addr))
/// Resolves the client address for `peer`, given the request's
/// `X-Forwarded-For` value.
/// Walks the hops right-to-left, skipping ones that are themselves trusted,
/// and returns the first address that is not. That entry is the one our own
/// proxy observed; everything to its left was supplied by the client and is
/// worthless. Falls back to the peer whenever the peer is not trusted, the
/// header is absent, or every entry is unusable.
/// `forwarded_for` must carry *every* `X-Forwarded-For` value the request
/// held, in order — see [`forwarded_for`]. Reading only the first would let
/// a client that sends its own header push the proxy's observed hop into a
/// second line this never looks at.
/// Addresses are canonicalized, so `::ffff:203.0.113.7` and `203.0.113.7`
/// are one client rather than two budgets.
pub fn client_ip(&self, peer: SocketAddr, forwarded_for: Option<&str>) -> IpAddr {
let peer = peer.ip().to_canonical();
if !self.trusts(peer) {
return peer;
forwarded_for
.unwrap_or_default()
.rsplit(',')
.filter_map(|hop| hop.trim().parse::<IpAddr>().ok())
.map(|hop| hop.to_canonical())
.find(|hop| !self.trusts(*hop))
.unwrap_or(peer)