use std::borrow::Cow;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use http::{HeaderMap, HeaderName};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ClientIpError {
#[error("invalid IP address: {0}")]
InvalidIp(String),
#[error("invalid prefix length: {0}")]
InvalidPrefix(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IpNet {
V4 {
addr: Ipv4Addr,
prefix: u8,
},
V6 {
addr: Ipv6Addr,
prefix: u8,
},
}
impl IpNet {
pub fn parse(s: &str) -> Result<Self, ClientIpError> {
let (addr_str, prefix) = match s.split_once('/') {
Some((addr, prefix)) => {
let prefix = prefix
.trim()
.parse::<u8>()
.map_err(|_| ClientIpError::InvalidPrefix(prefix.to_string()))?;
(addr, Some(prefix))
}
None => (s, None),
};
let addr: IpAddr = addr_str
.trim()
.parse()
.map_err(|_| ClientIpError::InvalidIp(addr_str.to_string()))?;
let addr = normalize_ip(addr);
match (addr, prefix) {
(IpAddr::V4(addr), None) => Ok(IpNet::V4 { addr, prefix: 32 }),
(IpAddr::V4(addr), Some(prefix)) if prefix <= 32 => Ok(IpNet::V4 {
addr: mask_v4(addr, prefix),
prefix,
}),
(IpAddr::V4(_), Some(prefix)) => Err(ClientIpError::InvalidPrefix(prefix.to_string())),
(IpAddr::V6(addr), None) => Ok(IpNet::V6 { addr, prefix: 128 }),
(IpAddr::V6(addr), Some(prefix)) if prefix <= 128 => Ok(IpNet::V6 {
addr: mask_v6(addr, prefix),
prefix,
}),
(IpAddr::V6(_), Some(prefix)) => Err(ClientIpError::InvalidPrefix(prefix.to_string())),
}
}
pub fn contains(&self, ip: IpAddr) -> bool {
let ip = normalize_ip(ip);
match (self, ip) {
(IpNet::V4 { addr, prefix }, IpAddr::V4(other)) => {
let mask = v4_mask(*prefix);
mask & u32::from(*addr) == mask & u32::from(other)
}
(IpNet::V6 { addr, prefix }, IpAddr::V6(other)) => {
let mask = v6_mask(*prefix);
mask & u128::from(*addr) == mask & u128::from(other)
}
_ => false,
}
}
}
fn v4_mask(prefix: u8) -> u32 {
if prefix == 0 {
0
} else if prefix >= 32 {
u32::MAX
} else {
u32::MAX << (32 - prefix)
}
}
fn v6_mask(prefix: u8) -> u128 {
if prefix == 0 {
0
} else if prefix >= 128 {
u128::MAX
} else {
u128::MAX << (128 - prefix)
}
}
fn mask_v4(addr: Ipv4Addr, prefix: u8) -> Ipv4Addr {
Ipv4Addr::from(v4_mask(prefix) & u32::from(addr))
}
fn mask_v6(addr: Ipv6Addr, prefix: u8) -> Ipv6Addr {
Ipv6Addr::from(v6_mask(prefix) & u128::from(addr))
}
fn normalize_ip(ip: IpAddr) -> IpAddr {
match ip {
IpAddr::V6(v6) => match v6.to_ipv4_mapped() {
Some(v4) => IpAddr::V4(v4),
None => IpAddr::V6(v6),
},
other => other,
}
}
#[derive(Debug, Clone, Default)]
pub struct ClientIpConfig {
pub trusted_proxies: Vec<IpNet>,
pub num_trusted_hops: usize,
pub trusted_header: Option<HeaderName>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum MissingClientPolicy {
#[default]
Reject,
FallbackKey(Cow<'static, str>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientIpSource {
PeerSocket,
ForwardedHeader,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedClient {
pub ip: IpAddr,
pub source: ClientIpSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MissingClientIdentity;
pub fn resolve_client_identity(
headers: &HeaderMap,
peer: Option<IpAddr>,
config: &ClientIpConfig,
) -> Result<ResolvedClient, MissingClientIdentity> {
let peer = peer.ok_or(MissingClientIdentity)?;
let peer_trusted = !config.trusted_proxies.is_empty()
&& config.trusted_proxies.iter().any(|net| net.contains(peer));
if !peer_trusted {
return Ok(ResolvedClient {
ip: normalize_ip(peer),
source: ClientIpSource::PeerSocket,
});
}
let header_value = match config.trusted_header.as_ref() {
Some(name) => headers.get(name),
None => headers.get("x-forwarded-for"),
};
let resolved = header_value
.and_then(|value| value.to_str().ok())
.and_then(|value| value.split(',').rev().nth(config.num_trusted_hops))
.map(str::trim)
.and_then(|entry| entry.parse().ok());
match resolved {
Some(ip) => Ok(ResolvedClient {
ip: normalize_ip(ip),
source: ClientIpSource::ForwardedHeader,
}),
None => {
tracing::warn!(
target: "throttle_kit::client_ip",
"malformed or too-short forwarded header; falling back to peer address"
);
Ok(ResolvedClient {
ip: normalize_ip(peer),
source: ClientIpSource::PeerSocket,
})
}
}
}
pub fn peer_ip_from_extensions(extensions: &http::Extensions) -> Option<IpAddr> {
extensions
.get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
.map(|connect_info| connect_info.0.ip())
}
pub fn resolve_from_request<B>(
request: &http::Request<B>,
config: &ClientIpConfig,
) -> Result<ResolvedClient, MissingClientIdentity> {
resolve_client_identity(
request.headers(),
peer_ip_from_extensions(request.extensions()),
config,
)
}