Skip to main content

systemprompt_api/services/middleware/
client_addr.rs

1//! Client-address resolution that does not blindly trust hop headers.
2//!
3//! [`resolve_client_ip`] is the single helper every middleware that
4//! cares about the originating client (rate-limiter, IP banlist,
5//! bot-scoring, abuse heuristics) must use. The contract:
6//!
7//! 1. If the immediate socket peer (`ConnectInfo<SocketAddr>`) is not contained
8//!    in `trusted_proxies`, return the peer address. Hop headers are ignored
9//!    entirely — they are untrusted in this case.
10//! 2. If the peer is trusted, walk `X-Forwarded-For` right-to-left and take the
11//!    first hop that is itself outside `trusted_proxies`. That hop is the
12//!    closest entity our proxy chain still sees, and the earliest one a client
13//!    could have spoofed.
14//! 3. If the chain is empty or every hop is trusted, fall back to the peer
15//!    address.
16//!
17//! `X-Real-IP` and `CF-Connecting-IP` are honoured only under rule 2's
18//! trust gate; otherwise they are ignored.
19//!
20//! `parse_trusted_proxies` drops invalid CIDR entries with a `tracing::warn!`
21//! rather than failing bootstrap: a single typo in a profile must not take
22//! the whole replica offline.
23
24use std::net::{IpAddr, SocketAddr};
25
26use axum::extract::ConnectInfo;
27use axum::http::HeaderMap;
28use ipnet::IpNet;
29
30#[must_use]
31pub fn parse_trusted_proxies(raw: &[String]) -> Vec<IpNet> {
32    raw.iter()
33        .filter_map(|s| {
34            let trimmed = s.trim();
35            if trimmed.is_empty() {
36                return None;
37            }
38            if let Ok(net) = trimmed.parse::<IpNet>() {
39                return Some(net);
40            }
41            if let Ok(addr) = trimmed.parse::<IpAddr>() {
42                let prefix = match addr {
43                    IpAddr::V4(_) => 32,
44                    IpAddr::V6(_) => 128,
45                };
46                if let Ok(net) = IpNet::new(addr, prefix) {
47                    return Some(net);
48                }
49            }
50            tracing::warn!(entry = %trimmed, "ignoring invalid trusted_proxies entry");
51            None
52        })
53        .collect()
54}
55
56fn is_trusted(addr: IpAddr, trusted: &[IpNet]) -> bool {
57    trusted.iter().any(|net| net.contains(&addr))
58}
59
60#[must_use]
61pub fn resolve_client_ip(
62    headers: &HeaderMap,
63    connect_info: Option<&ConnectInfo<SocketAddr>>,
64    trusted: &[IpNet],
65) -> Option<IpAddr> {
66    let peer_ip = connect_info.map(|c| c.0.ip())?;
67
68    if !is_trusted(peer_ip, trusted) {
69        return Some(peer_ip);
70    }
71
72    if let Some(xff) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
73        let hops: Vec<&str> = xff
74            .split(',')
75            .map(str::trim)
76            .filter(|s| !s.is_empty())
77            .collect();
78        for hop in hops.iter().rev() {
79            if let Ok(addr) = hop.parse::<IpAddr>()
80                && !is_trusted(addr, trusted)
81            {
82                return Some(addr);
83            }
84        }
85    }
86
87    for header in ["x-real-ip", "cf-connecting-ip"] {
88        if let Some(raw) = headers.get(header).and_then(|v| v.to_str().ok())
89            && let Ok(addr) = raw.trim().parse::<IpAddr>()
90            && !is_trusted(addr, trusted)
91        {
92            return Some(addr);
93        }
94    }
95
96    Some(peer_ip)
97}