Skip to main content

rocket_client_addr/
resolve.rs

1use std::net::IpAddr;
2
3use rocket::http::HeaderMap;
4
5use crate::{
6    ChainHeader, ClientIp, ClientIpConfig, ClientIpSource, TrustAllChainIpSelection,
7    TrustAllProxyMode,
8    canonical::canonical_ip,
9    config::{ChainHeaderKind, TrustModel},
10    headers::{
11        ChainEntry, configured_client_ip_header, forwarded_entries, header_lines,
12        list_header_entries,
13    },
14};
15
16impl ClientIpConfig {
17    /// Resolve the client IP of a request from its headers and its socket peer IP.
18    ///
19    /// Use this when the request does not come from Rocket, or when the [`ClientIp`] request guard is not convenient. The crate-level docs describe the full order in which the answer is chosen.
20    pub fn resolve_client_ip(&self, headers: &HeaderMap<'_>, peer_ip: IpAddr) -> ClientIp {
21        let peer_ip = canonical_ip(peer_ip);
22
23        match &self.trust {
24            TrustModel::NoProxy => ClientIp::new(peer_ip, ClientIpSource::Socket),
25            TrustModel::TrustedProxies {
26                chain_header_order, ..
27            } => resolve_client_ip_from_trusted_proxies(headers, peer_ip, self, chain_header_order),
28            TrustModel::TrustAllProxies(mode) => {
29                resolve_client_ip_trusting_all_proxies(headers, peer_ip, mode)
30            },
31        }
32    }
33}
34
35/// Resolve a request when proxies are trusted by CIDR.
36fn resolve_client_ip_from_trusted_proxies(
37    headers: &HeaderMap<'_>,
38    socket_ip: IpAddr,
39    config: &ClientIpConfig,
40    chain_header_order: &[ChainHeader],
41) -> ClientIp {
42    // A peer that is not a trusted proxy wrote its own headers, so none of them may be read.
43    let Some(socket_proxy_rule) = config.rule_for(socket_ip) else {
44        return ClientIp::new(socket_ip, ClientIpSource::Socket);
45    };
46
47    if let Some(header) = socket_proxy_rule.client_ip_header()
48        && let Some(ip) = configured_client_ip_header(headers, header)
49    {
50        return ClientIp::new(ip, ClientIpSource::ConfiguredHeader(header.clone()));
51    }
52
53    // Scan from the socket side toward the original client.
54    if let Some((ip, source)) =
55        client_ip_from_chain_headers(headers, chain_header_order, |entries| {
56            first_non_trusted_from_right(entries, config)
57        })
58    {
59        return ClientIp::new(ip, source);
60    }
61
62    ClientIp::new(socket_ip, ClientIpSource::Socket)
63}
64
65/// Resolve a request when every socket peer is treated as a trusted proxy.
66fn resolve_client_ip_trusting_all_proxies(
67    headers: &HeaderMap<'_>,
68    socket_ip: IpAddr,
69    mode: &TrustAllProxyMode,
70) -> ClientIp {
71    if let Some(header) = mode.client_ip_header()
72        && let Some(ip) = configured_client_ip_header(headers, header)
73    {
74        return ClientIp::new(ip, ClientIpSource::ConfiguredHeader(header.clone()));
75    }
76
77    if let Some((ip, source)) =
78        client_ip_from_chain_headers(headers, mode.chain_header_order(), |entries| {
79            select_trust_all_chain_ip(entries, mode.chain_ip_selection())
80        })
81    {
82        return ClientIp::new(ip, source);
83    }
84
85    ClientIp::new(socket_ip, ClientIpSource::Socket)
86}
87
88/// Return the IP that `pick` accepts in the first chain header the request carries.
89///
90/// `pick` receives the hops of one chain header in left-to-right order.
91///
92/// The configured chain headers are alternatives, not a search list. Only a chain header that the request does not carry at all moves the search on to the next one. A header the request does carry was written by whichever proxy handled it, so if it yields no answer the search stops there instead of falling back to a header that same proxy may never have touched.
93fn client_ip_from_chain_headers(
94    headers: &HeaderMap<'_>,
95    chain_header_order: &[ChainHeader],
96    mut pick: impl FnMut(&mut dyn DoubleEndedIterator<Item = ChainEntry>) -> Option<IpAddr>,
97) -> Option<(IpAddr, ClientIpSource)> {
98    for chain_header in chain_header_order {
99        let header = chain_header.as_header_name();
100        let lines = header_lines(headers, header);
101
102        if lines.is_empty() {
103            continue;
104        }
105
106        let ip = match chain_header.kind() {
107            ChainHeaderKind::Forwarded => {
108                let entries = forwarded_entries(lines)?;
109
110                pick(&mut entries.into_iter())
111            },
112            ChainHeaderKind::XForwardedFor => {
113                let mut entries = list_header_entries(lines)?;
114
115                pick(&mut entries)
116            },
117        };
118
119        // The request carries this header, so the answer comes from it or from nowhere.
120        return Some((ip?, ClientIpSource::ChainHeader(header.clone())));
121    }
122
123    None
124}
125
126/// Pick one hop of a chain by position, for trust-all proxy mode.
127fn select_trust_all_chain_ip(
128    entries: &mut dyn DoubleEndedIterator<Item = ChainEntry>,
129    selection: TrustAllChainIpSelection,
130) -> Option<IpAddr> {
131    let entry = match selection {
132        TrustAllChainIpSelection::Leftmost => entries.next(),
133        TrustAllChainIpSelection::Rightmost => entries.next_back(),
134        // Counting is positional, so a hop without a usable IP address still takes one place.
135        TrustAllChainIpSelection::SkipRightmostHops(hops) => entries.nth_back(hops),
136    }?;
137
138    match entry {
139        ChainEntry::Ip(ip) => Some(ip),
140        ChainEntry::Opaque => None,
141    }
142}
143
144/// Walk a chain from the socket side toward the original client, and stop at the first hop that is not a trusted proxy.
145fn first_non_trusted_from_right(
146    entries: &mut dyn DoubleEndedIterator<Item = ChainEntry>,
147    config: &ClientIpConfig,
148) -> Option<IpAddr> {
149    while let Some(entry) = entries.next_back() {
150        match entry {
151            // Keep walking left past hops that are known trusted proxies.
152            ChainEntry::Ip(ip) if config.is_trusted_proxy(ip) => continue,
153            ChainEntry::Ip(ip) => return Some(ip),
154            // This hop cannot be compared with the trusted proxy rules, so it is unknown whether it is a proxy. Everything further left was written by a hop that is unknown too, so the scan cannot go on.
155            ChainEntry::Opaque => return None,
156        }
157    }
158
159    None
160}