use std::net::{IpAddr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::task::{Context, Poll};
use axum::extract::ConnectInfo;
use http::header::{HeaderName, FORWARDED};
use http::{HeaderMap, HeaderValue};
use ipnet::IpNet;
use tower::{Layer, Service};
use crate::config::{ClientAddressConfig, ForwardingHeader, XForwardedFor};
pub(crate) const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
pub(crate) const X_REAL_IP: HeaderName = HeaderName::from_static("x-real-ip");
pub(crate) fn owns(name: &str) -> bool {
name.eq_ignore_ascii_case("x-forwarded-for")
|| name.eq_ignore_ascii_case("x-real-ip")
|| name.eq_ignore_ascii_case("forwarded")
}
const MAX_HOPS: usize = 32;
const MAX_ELEMENT: usize = 128;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClientAddress {
peer: Option<SocketAddr>,
resolution: Resolution,
value: Option<HeaderValue>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Resolution {
Peer(IpAddr),
Forwarded(IpAddr),
Invalid(InvalidForwarding),
Unavailable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidForwarding {
Malformed,
TooManyHops,
Repeated,
Conflicting,
}
impl std::fmt::Display for InvalidForwarding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Malformed => "malformed",
Self::TooManyHops => "beyond too many trusted hops",
Self::Repeated => "repeated",
Self::Conflicting => "in conflict with itself",
})
}
}
impl ClientAddress {
pub fn from_peer(peer: Option<SocketAddr>) -> Self {
let resolution = match peer {
Some(peer) => Resolution::Peer(peer.ip().to_canonical()),
None => Resolution::Unavailable,
};
Self::new(peer, resolution)
}
fn new(peer: Option<SocketAddr>, resolution: Resolution) -> Self {
let value = match resolution {
Resolution::Peer(ip) | Resolution::Forwarded(ip) => Some(header_value(ip)),
Resolution::Invalid(_) | Resolution::Unavailable => None,
};
Self {
peer,
resolution,
value,
}
}
pub fn ip(&self) -> Option<IpAddr> {
match self.resolution {
Resolution::Peer(ip) | Resolution::Forwarded(ip) => Some(ip),
Resolution::Invalid(_) | Resolution::Unavailable => None,
}
}
pub fn peer(&self) -> Option<SocketAddr> {
self.peer
}
pub fn resolution(&self) -> Resolution {
self.resolution
}
pub(crate) fn header_value(&self) -> Option<&HeaderValue> {
self.value.as_ref()
}
}
fn header_value(ip: IpAddr) -> HeaderValue {
use std::io::Write;
let mut buf = [0u8; 48];
let mut rest: &mut [u8] = &mut buf;
write!(rest, "{ip}").expect("an IP address fits in 48 bytes");
let len = 48 - rest.len();
HeaderValue::from_bytes(&buf[..len]).expect("an IP address is a header value")
}
#[derive(Debug, Default)]
pub(crate) struct Resolver {
trusted: Box<[IpNet]>,
header: ForwardingHeader,
required: bool,
forward: XForwardedFor,
client_header: Option<HeaderName>,
audit_header: Option<HeaderName>,
}
impl Resolver {
pub(crate) fn build(config: &ClientAddressConfig) -> Result<Self, String> {
let trusted = config
.trusted_proxies
.iter()
.map(|entry| parse_trusted(entry))
.collect::<Result<_, _>>()?;
let forward = &config.forward;
let client_header = forward
.client_header
.as_deref()
.map(|name| forward_header("client_header", name, &["x-forwarded-for", "forwarded"]))
.transpose()?;
let audit_header = forward
.audit_header
.as_deref()
.map(|name| {
let mut taken = vec!["x-forwarded-for", "x-real-ip", "forwarded"];
taken.extend(client_header.as_ref().map(HeaderName::as_str));
forward_header("audit_header", name, &taken)
})
.transpose()?;
Ok(Self {
trusted,
header: config.header,
required: config.required,
forward: forward.x_forwarded_for,
client_header,
audit_header,
})
}
pub(crate) fn required(&self) -> bool {
self.required
}
pub(crate) fn reserves(&self, name: &str) -> bool {
owns(name)
|| self
.configured_headers()
.any(|own| name.eq_ignore_ascii_case(own.as_str()))
}
pub(crate) fn configured_headers(&self) -> impl Iterator<Item = &HeaderName> {
self.client_header.iter().chain(&self.audit_header)
}
pub(crate) fn forwarded_headers(&self) -> impl Iterator<Item = &str> {
["x-forwarded-for", "x-real-ip", "forwarded"]
.into_iter()
.chain(self.configured_headers().map(HeaderName::as_str))
}
fn trusts(&self, ip: IpAddr) -> bool {
self.trusted.iter().any(|net| net.contains(&ip))
}
#[cfg(test)]
pub(crate) fn resolve(&self, peer: Option<SocketAddr>, headers: &HeaderMap) -> ClientAddress {
self.resolve_noting(peer, headers, None)
}
fn resolve_noting(
&self,
peer: Option<SocketAddr>,
headers: &HeaderMap,
hops: Option<&mut Vec<IpAddr>>,
) -> ClientAddress {
let Some(peer) = peer else {
return ClientAddress::new(None, Resolution::Unavailable);
};
let peer_ip = peer.ip().to_canonical();
if !self.trusts(peer_ip) {
return ClientAddress::new(Some(peer), Resolution::Peer(peer_ip));
}
let forwarded = match self.header {
ForwardingHeader::XForwardedFor => {
self.walk(headers.get_all(X_FORWARDED_FOR).iter(), hops)
}
ForwardingHeader::XRealIp => real_ip(headers.get_all(X_REAL_IP).iter()),
ForwardingHeader::CleanedReport => cleaned_report(headers),
};
let resolution = match forwarded {
Ok(Some(ip)) => Resolution::Forwarded(ip),
Ok(None) if self.required => Resolution::Unavailable,
Ok(None) => Resolution::Peer(peer_ip),
Err(invalid) => Resolution::Invalid(invalid),
};
ClientAddress::new(Some(peer), resolution)
}
pub(crate) fn apply<B>(&self, request: &mut http::Request<B>, peer: Option<SocketAddr>) {
let mut hops = Vec::new();
let noting = (self.forward == XForwardedFor::Verified).then_some(&mut hops);
let client = self.resolve_noting(peer, request.headers(), noting);
let headers = request.headers_mut();
if let Some(audit) = &self.audit_header {
let arrived: Vec<HeaderValue> =
headers.get_all(X_FORWARDED_FOR).iter().cloned().collect();
headers.remove(audit);
for line in arrived {
headers.append(audit.clone(), line);
}
}
let xff = match self.forward {
XForwardedFor::Verified => verified_chain(&client, &hops),
XForwardedFor::Resolved => client.header_value().cloned(),
XForwardedFor::Append => appended_chain(headers, client.peer),
XForwardedFor::Preserve => None,
XForwardedFor::Remove => None,
};
if self.forward != XForwardedFor::Preserve {
headers.remove(X_FORWARDED_FOR);
headers.remove(X_REAL_IP);
headers.remove(FORWARDED);
if let Some(xff) = xff {
headers.insert(X_FORWARDED_FOR, xff);
}
}
if let Some(name) = &self.client_header {
headers.remove(name);
if let Some(value) = client.header_value() {
headers.insert(name.clone(), value.clone());
}
}
request.extensions_mut().insert(client);
}
fn walk<'a>(
&self,
lines: impl DoubleEndedIterator<Item = &'a HeaderValue>,
mut hops: Option<&mut Vec<IpAddr>>,
) -> Result<Option<IpAddr>, InvalidForwarding> {
let mut read = 0;
let mut leftmost = None;
for line in lines.rev() {
let mut rest = line.as_bytes();
loop {
read += 1;
if read > MAX_HOPS {
return Err(InvalidForwarding::TooManyHops);
}
let (element, before) = last_element(rest)?;
if !element.is_empty() {
let ip = parse_address(element)
.ok_or(InvalidForwarding::Malformed)?
.to_canonical();
if !self.trusts(ip) {
return Ok(Some(ip));
}
if let Some(hops) = hops.as_deref_mut() {
hops.push(ip);
}
leftmost = Some(ip);
}
match before {
Some(before) => rest = before,
None => break,
}
}
}
if leftmost.is_some() {
if let Some(hops) = hops {
hops.pop();
}
}
Ok(leftmost)
}
}
fn verified_chain(client: &ClientAddress, hops: &[IpAddr]) -> Option<HeaderValue> {
use std::fmt::Write;
let value = client.header_value()?;
let Resolution::Forwarded(ip) = client.resolution else {
return Some(value.clone());
};
let peer = client.peer?.ip().to_canonical();
let mut chain = ip.to_string();
for hop in hops.iter().rev().chain([&peer]) {
write!(chain, ", {hop}").expect("writing to a String cannot fail");
}
Some(HeaderValue::try_from(chain).expect("addresses and commas are a header value"))
}
fn appended_chain(headers: &HeaderMap, peer: Option<SocketAddr>) -> Option<HeaderValue> {
use std::io::Write;
let mut lines = headers.get_all(X_FORWARDED_FOR).iter();
let Some(peer) = peer else {
let first = lines.next()?;
let mut joined = first.as_bytes().to_vec();
for line in lines {
joined.extend_from_slice(b", ");
joined.extend_from_slice(line.as_bytes());
}
return Some(
HeaderValue::from_maybe_shared(bytes::Bytes::from(joined))
.expect("joined header values are a header value"),
);
};
let mut chain = Vec::new();
for line in lines {
chain.extend_from_slice(line.as_bytes());
chain.extend_from_slice(b", ");
}
write!(chain, "{}", peer.ip().to_canonical()).expect("writing to a Vec cannot fail");
Some(
HeaderValue::from_maybe_shared(bytes::Bytes::from(chain))
.expect("header values, commas and an address are a header value"),
)
}
fn forward_header(setting: &str, name: &str, taken: &[&str]) -> Result<HeaderName, String> {
let header = HeaderName::from_bytes(name.as_bytes())
.ok()
.filter(|header| crate::transcode::metadata::is_grpc_key(header.as_str()))
.ok_or_else(|| {
format!("forward.{setting} {name:?} is not a gRPC metadata key (letters, digits, '_', '-' and '.')")
})?;
if header.as_str().ends_with("-bin") {
return Err(format!(
"forward.{setting} {name:?} is a binary metadata key, whose values must be base64; \
the address is text"
));
}
if taken.contains(&header.as_str()) {
return Err(format!(
"forward.{setting} {name:?} names a header the forwarding already writes"
));
}
Ok(header)
}
fn last_element(bytes: &[u8]) -> Result<(&[u8], Option<&[u8]>), InvalidForwarding> {
let window = bytes.len().saturating_sub(MAX_ELEMENT + 1);
let (element, before) = match bytes[window..].iter().rposition(|&b| b == b',') {
Some(comma) => (&bytes[window + comma + 1..], Some(&bytes[..window + comma])),
None if window == 0 => (bytes, None),
None => return Err(InvalidForwarding::Malformed),
};
Ok((trim_whitespace(element), before))
}
fn trim_whitespace(bytes: &[u8]) -> &[u8] {
let is_ows = |b: &u8| matches!(b, b' ' | b'\t');
let start = bytes.iter().position(|b| !is_ows(b)).unwrap_or(bytes.len());
let end = bytes
.iter()
.rposition(|b| !is_ows(b))
.map_or(start, |end| end + 1);
&bytes[start..end]
}
fn real_ip<'a>(
mut lines: impl Iterator<Item = &'a HeaderValue>,
) -> Result<Option<IpAddr>, InvalidForwarding> {
let Some(line) = lines.next() else {
return Ok(None);
};
if lines.next().is_some() {
return Err(InvalidForwarding::Repeated);
}
let raw = line.as_bytes();
if raw.len() > MAX_ELEMENT {
return Err(InvalidForwarding::Malformed);
}
parse_address(trim_whitespace(raw))
.map(|ip| Some(ip.to_canonical()))
.ok_or(InvalidForwarding::Malformed)
}
fn cleaned_report(headers: &HeaderMap) -> Result<Option<IpAddr>, InvalidForwarding> {
let reported = real_ip(headers.get_all(X_REAL_IP).iter())?;
let mut chain = headers.get_all(X_FORWARDED_FOR).iter();
let (first, second) = (chain.next(), chain.next());
let agrees = !headers.contains_key(FORWARDED)
&& match (reported, first, second) {
(_, _, Some(_)) | (None, Some(_), None) => false,
(_, None, None) => true,
(Some(ip), Some(line), None) => {
real_ip(std::iter::once(line)).ok().flatten() == Some(ip)
}
};
if !agrees {
return Err(InvalidForwarding::Conflicting);
}
Ok(reported)
}
fn parse_address(bytes: &[u8]) -> Option<IpAddr> {
let text = std::str::from_utf8(bytes).ok()?;
if let Ok(ip) = text.parse::<IpAddr>() {
return Some(ip);
}
if let Ok(socket) = text.parse::<SocketAddr>() {
return Some(socket.ip());
}
let v6 = text.strip_prefix('[')?.strip_suffix(']')?;
v6.parse::<Ipv6Addr>().ok().map(IpAddr::V6)
}
fn parse_trusted(entry: &str) -> Result<IpNet, String> {
let net = match entry.parse::<IpNet>() {
Ok(net) => net,
Err(_) => match entry.parse::<IpAddr>() {
Ok(ip) => IpNet::from(ip),
Err(_) => {
return Err(format!(
"trusted_proxies entry {entry:?} is neither a CIDR range nor an IP address"
))
}
},
};
Ok(canonical_net(net))
}
fn canonical_net(net: IpNet) -> IpNet {
if let IpNet::V6(v6) = net {
if let (Some(v4), Some(prefix)) = (
v6.network().to_ipv4_mapped(),
v6.prefix_len().checked_sub(96),
) {
return ipnet::Ipv4Net::new(v4, prefix)
.expect("a prefix of at most 128 - 96 fits IPv4")
.into();
}
}
net
}
#[derive(Clone, Debug)]
pub struct ClientAddressLayer {
resolver: Arc<Resolver>,
}
impl ClientAddressLayer {
pub fn new(config: &ClientAddressConfig) -> anyhow::Result<Self> {
let resolver = Resolver::build(config)
.map_err(|e| anyhow::anyhow!("invalid client_address config: {e}"))?;
Ok(Self::with(Arc::new(resolver)))
}
pub(crate) fn with(resolver: Arc<Resolver>) -> Self {
Self { resolver }
}
}
impl<S> Layer<S> for ClientAddressLayer {
type Service = ClientAddressService<S>;
fn layer(&self, inner: S) -> ClientAddressService<S> {
ClientAddressService {
inner,
resolver: self.resolver.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct ClientAddressService<S> {
inner: S,
resolver: Arc<Resolver>,
}
impl<S, B> Service<http::Request<B>> for ClientAddressService<S>
where
S: Service<http::Request<B>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut request: http::Request<B>) -> Self::Future {
let peer = request
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|ConnectInfo(peer)| *peer)
.or_else(|| {
use crate::service::TlsConnectInfo;
use tonic::transport::server::TcpConnectInfo;
let extensions = request.extensions();
extensions
.get::<TcpConnectInfo>()
.or_else(|| extensions.get::<TlsConnectInfo>().map(|tls| tls.get_ref()))
.and_then(TcpConnectInfo::remote_addr)
});
self.resolver.apply(&mut request, peer);
self.inner.call(request)
}
}
#[cfg(test)]
mod tests;