Skip to main content

rocket_client_addr/
config.rs

1use std::net::IpAddr;
2
3use cidr::IpCidr;
4use rocket::http::uncased::Uncased;
5
6use crate::{
7    ClientIpConfigBuildError,
8    canonical::{canonical_cidr, canonical_ip},
9    cidr_merge::{ensure_no_cross_metadata_overlap, merge_rules_by_metadata},
10};
11
12/// Settings that decide how a client IP is resolved from a request.
13///
14/// A config uses exactly one trust model, picked as the first step of [`ClientIpConfig::builder`].
15///
16/// * Trust no proxy: no header is read, and every request resolves to the socket peer IP.
17/// * Trusted proxies: headers are read only when the socket peer IP is inside one of the trusted CIDRs.
18/// * Trust all proxies: every socket peer is treated as a trusted proxy.
19///
20/// The default config trusts no proxy.
21///
22/// The [`crate::ClientIp`] request guard reads this config out of Rocket's managed state, so a built config has to be passed to `rocket::build().manage(config)`.
23#[derive(Clone, Debug, Eq, Hash, PartialEq)]
24pub struct ClientIpConfig {
25    pub(crate) trust: TrustModel,
26}
27
28/// The trust model that a [`ClientIpConfig`] was built with.
29#[derive(Clone, Debug, Eq, Hash, PartialEq)]
30pub(crate) enum TrustModel {
31    NoProxy,
32    TrustedProxies {
33        rules:              Vec<TrustedProxyRule>,
34        chain_header_order: Vec<ChainHeader>,
35    },
36    TrustAllProxies(TrustAllProxyMode),
37}
38
39impl ClientIpConfig {
40    /// Start building a config by choosing a trust model.
41    #[inline]
42    pub const fn builder() -> ClientIpConfigBuilder {
43        ClientIpConfigBuilder
44    }
45
46    /// Return the trusted proxy rules, as they look after build-time merging.
47    ///
48    /// This is empty unless the config was built with trusted proxy CIDRs.
49    #[inline]
50    pub fn trusted_proxy_rules(&self) -> &[TrustedProxyRule] {
51        match &self.trust {
52            TrustModel::TrustedProxies {
53                rules, ..
54            } => rules,
55            TrustModel::NoProxy | TrustModel::TrustAllProxies(_) => &[],
56        }
57    }
58
59    /// Return the chain headers in the order they are tried.
60    ///
61    /// This is empty when the config trusts no proxy, because no header is read at all.
62    #[inline]
63    pub fn chain_header_order(&self) -> &[ChainHeader] {
64        match &self.trust {
65            TrustModel::NoProxy => &[],
66            TrustModel::TrustedProxies {
67                chain_header_order, ..
68            } => chain_header_order,
69            TrustModel::TrustAllProxies(mode) => &mode.chain_header_order,
70        }
71    }
72
73    /// Return the trust-all proxy settings, if that trust model is in use.
74    #[inline]
75    pub const fn trust_all_proxy_mode(&self) -> Option<&TrustAllProxyMode> {
76        match &self.trust {
77            TrustModel::TrustAllProxies(mode) => Some(mode),
78            _ => None,
79        }
80    }
81
82    /// Check whether this config treats every socket peer as a trusted proxy.
83    #[inline]
84    pub const fn trusts_all_proxies(&self) -> bool {
85        matches!(self.trust, TrustModel::TrustAllProxies(_))
86    }
87
88    /// Check whether this config reads no header at all and always answers with the socket peer IP.
89    #[inline]
90    pub const fn trusts_no_proxy(&self) -> bool {
91        matches!(self.trust, TrustModel::NoProxy)
92    }
93
94    /// Check whether an address is treated as a trusted proxy.
95    ///
96    /// An IPv4-mapped IPv6 address is matched by its IPv4 form, on both sides of the comparison. A trusted proxy CIDR written as `::ffff:10.0.0.0/120` is rewritten to `10.0.0.0/24` while the config is built, so it matches the same addresses an IPv4 CIDR would. For the same reason an IPv6 CIDR such as `::/0` never matches an IPv4 peer, so both address families need their own CIDR.
97    ///
98    /// This is always true in trust-all proxy mode, and always false when no proxy is trusted.
99    #[inline]
100    pub fn is_trusted_proxy(&self, ip: IpAddr) -> bool {
101        match &self.trust {
102            TrustModel::NoProxy => false,
103            TrustModel::TrustedProxies {
104                ..
105            } => self.rule_for(ip).is_some(),
106            TrustModel::TrustAllProxies(_) => true,
107        }
108    }
109
110    /// Find the trusted proxy rule that covers an address.
111    #[inline]
112    pub(crate) fn rule_for(&self, ip: IpAddr) -> Option<&TrustedProxyRule> {
113        match &self.trust {
114            TrustModel::TrustedProxies {
115                rules, ..
116            } => {
117                let ip = canonical_ip(ip);
118
119                // The rules never overlap and are sorted by first address, so only the last rule that starts at or before this address can contain it.
120                let index = rules.partition_point(|rule| rule.cidr.first_address() <= ip);
121                let rule = &rules[index.checked_sub(1)?];
122
123                rule.cidr.contains(&ip).then_some(rule)
124            },
125            TrustModel::NoProxy | TrustModel::TrustAllProxies(_) => None,
126        }
127    }
128}
129
130impl Default for ClientIpConfig {
131    #[inline]
132    fn default() -> Self {
133        Self {
134            trust: TrustModel::NoProxy
135        }
136    }
137}
138
139/// The first step of building a [`ClientIpConfig`], where the trust model is chosen.
140///
141/// The chosen trust model decides which builder comes next, so a config can never mix trusted proxy CIDRs with trust-all proxy settings.
142#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
143pub struct ClientIpConfigBuilder;
144
145impl ClientIpConfigBuilder {
146    /// Create a new config builder.
147    #[inline]
148    pub const fn new() -> Self {
149        Self
150    }
151
152    /// Read no header and always answer with the socket peer IP.
153    ///
154    /// This is the same as [`ClientIpConfig::default`], and needs no further building.
155    #[inline]
156    pub const fn trust_no_proxy(self) -> ClientIpConfig {
157        ClientIpConfig {
158            trust: TrustModel::NoProxy
159        }
160    }
161
162    /// Trust proxies whose socket peer IP falls inside a known CIDR.
163    ///
164    /// Use this whenever the proxy addresses are known ahead of time. It is the safest choice, because a client that reaches the service directly can never make it read forwarding headers.
165    #[inline]
166    pub fn trusted_proxies(self) -> TrustedProxiesBuilder {
167        TrustedProxiesBuilder::new()
168    }
169
170    /// Treat every socket peer as a trusted proxy.
171    ///
172    /// Use this when the service can only be reached through a proxy, but the address of that proxy is not known ahead of time. Any socket peer may then choose its own address, so this must not be used on a service that clients can reach directly.
173    #[inline]
174    pub fn trust_all_proxies(self) -> TrustAllProxiesBuilder {
175        TrustAllProxiesBuilder::new()
176    }
177}
178
179/// Builder for a config that trusts proxies by CIDR.
180///
181/// A request is resolved from headers only when its socket peer IP matches one of the added CIDRs.
182#[derive(Clone, Debug, Eq, Hash, PartialEq)]
183pub struct TrustedProxiesBuilder {
184    rules:              Vec<TrustedProxyRule>,
185    chain_header_order: Vec<ChainHeader>,
186}
187
188impl TrustedProxiesBuilder {
189    /// Create a builder with the default chain header order.
190    #[inline]
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Add a trusted proxy CIDR without a client IP header.
196    ///
197    /// Requests from this CIDR are resolved from the chain headers.
198    #[must_use = "builder methods return an updated builder and do not mutate in place"]
199    #[inline]
200    pub fn proxy(self, cidr: IpCidr) -> Self {
201        self.proxy_rule(TrustedProxyRule::new(cidr))
202    }
203
204    /// Add a trusted proxy CIDR with a client IP header.
205    ///
206    /// Use this when the proxy writes the client address into one header that holds a single IP. Common examples are `X-Real-IP`, `CF-Connecting-IP`, and `True-Client-IP`.
207    ///
208    /// The header is read only when the socket peer IP is inside this CIDR. If it is missing or unusable, the chain headers are still tried, so the proxy should also clear or overwrite the chain headers it does not set itself.
209    #[must_use = "builder methods return an updated builder and do not mutate in place"]
210    #[inline]
211    pub fn proxy_with_client_ip_header(self, cidr: IpCidr, header: Uncased<'static>) -> Self {
212        self.proxy_rule(TrustedProxyRule::with_client_ip_header(cidr, header))
213    }
214
215    /// Add a trusted proxy CIDR that sends the `X-Real-IP` header.
216    ///
217    /// This is a shortcut for [`Self::proxy_with_client_ip_header`], and it fits Nginx-like setups that pass one client address in `X-Real-IP`.
218    #[must_use = "builder methods return an updated builder and do not mutate in place"]
219    #[inline]
220    pub fn proxy_with_x_real_ip(self, cidr: IpCidr) -> Self {
221        self.proxy_rule(TrustedProxyRule::with_x_real_ip(cidr))
222    }
223
224    /// Add one prepared trusted proxy rule.
225    #[must_use = "builder methods return an updated builder and do not mutate in place"]
226    #[inline]
227    pub fn proxy_rule(mut self, rule: TrustedProxyRule) -> Self {
228        self.rules.push(rule);
229        self
230    }
231
232    /// Add several prepared trusted proxy rules.
233    ///
234    /// Use this when the rules come from a config file or another runtime source.
235    #[must_use = "builder methods return an updated builder and do not mutate in place"]
236    #[inline]
237    pub fn proxies(mut self, rules: impl IntoIterator<Item = TrustedProxyRule>) -> Self {
238        self.rules.extend(rules);
239        self
240    }
241
242    /// Set the chain headers, and the order they are tried in.
243    ///
244    /// Use [`ChainHeader::new`] for a custom comma-separated IP list header, and an empty iterator to read no chain header at all.
245    #[must_use = "builder methods return an updated builder and do not mutate in place"]
246    #[inline]
247    pub fn chain_header_order(mut self, order: impl IntoIterator<Item = ChainHeader>) -> Self {
248        self.chain_header_order = order.into_iter().collect();
249        self
250    }
251
252    /// Read no chain header at all.
253    ///
254    /// Use this when the proxy sets a client IP header and cannot clear the chain headers a client may send.
255    #[must_use = "builder methods return an updated builder and do not mutate in place"]
256    #[inline]
257    pub fn disable_chain_headers(self) -> Self {
258        self.chain_header_order([])
259    }
260
261    /// Build an immutable config.
262    ///
263    /// CIDRs that use the same client IP header are merged into as few rules as possible. CIDRs that use different client IP headers must not overlap, because one socket peer IP would then mean two policies.
264    ///
265    /// An IPv4-mapped IPv6 CIDR, such as `::ffff:10.0.0.0/120`, is rewritten to its IPv4 form first, so [`TrustedProxyRule::cidr`] may report a different CIDR than the one that was added.
266    ///
267    /// # Errors
268    ///
269    /// Returns [`ClientIpConfigBuildError::OverlappingTrustedProxyRules`] when two rules cover a common address but do not agree on the client IP header.
270    pub fn build(self) -> Result<ClientIpConfig, ClientIpConfigBuildError> {
271        let mut rules = self.rules;
272
273        // Rewriting before the overlap check lets an IPv4-mapped CIDR clash with a plain IPv4 CIDR that covers the same addresses.
274        for rule in &mut rules {
275            rule.cidr = canonical_cidr(rule.cidr);
276        }
277
278        ensure_no_cross_metadata_overlap(&rules)?;
279
280        let mut rules = merge_rules_by_metadata(rules);
281
282        // Merging only joins networks of one metadata group, so it covers exactly the same addresses and cannot create a new cross-metadata overlap.
283        debug_assert!(ensure_no_cross_metadata_overlap(&rules).is_ok());
284
285        // Merging leaves the rules of one metadata group disjoint, and rules of different groups were already rejected if they overlapped, so no address can match two rules. Sorting by first address therefore only fixes the order that the metadata grouping left undefined, and it lets a lookup binary search instead of scan.
286        rules.sort_by_key(|rule| rule.cidr.first_address());
287
288        Ok(ClientIpConfig {
289            trust: TrustModel::TrustedProxies {
290                rules,
291                chain_header_order: self.chain_header_order,
292            },
293        })
294    }
295}
296
297impl Default for TrustedProxiesBuilder {
298    #[inline]
299    fn default() -> Self {
300        Self {
301            rules: Vec::new(), chain_header_order: default_chain_header_order()
302        }
303    }
304}
305
306/// Builder for a config that treats every socket peer as a trusted proxy.
307///
308/// This fits a service that is always behind a proxy whose address is not known. It is safe only when clients cannot reach the service directly and the proxy clears the forwarding headers it does not set itself.
309#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
310pub struct TrustAllProxiesBuilder {
311    mode: TrustAllProxyMode,
312}
313
314impl TrustAllProxiesBuilder {
315    /// Create a builder with the default trust-all proxy settings.
316    #[inline]
317    pub fn new() -> Self {
318        Self::default()
319    }
320
321    /// Set a header that holds a single client IP, checked before any chain header.
322    ///
323    /// It must contain one plain IP address, such as the value a proxy usually sends in `X-Real-IP`.
324    #[must_use = "builder methods return an updated builder and do not mutate in place"]
325    #[inline]
326    pub fn client_ip_header(mut self, header: Uncased<'static>) -> Self {
327        self.mode.client_ip_header = Some(header);
328        self
329    }
330
331    /// Set the chain headers, and the order they are tried in.
332    ///
333    /// Use [`ChainHeader::new`] for a custom comma-separated IP list header, and an empty iterator to read no chain header at all.
334    #[must_use = "builder methods return an updated builder and do not mutate in place"]
335    #[inline]
336    pub fn chain_header_order(mut self, order: impl IntoIterator<Item = ChainHeader>) -> Self {
337        self.mode.chain_header_order = order.into_iter().collect();
338        self
339    }
340
341    /// Read no chain header at all.
342    ///
343    /// Use this when the proxy sets a client IP header and cannot clear the chain headers a client may send.
344    #[must_use = "builder methods return an updated builder and do not mutate in place"]
345    #[inline]
346    pub fn disable_chain_headers(self) -> Self {
347        self.chain_header_order([])
348    }
349
350    /// Set which hop of a chain header becomes the client IP.
351    #[must_use = "builder methods return an updated builder and do not mutate in place"]
352    #[inline]
353    pub const fn chain_ip_selection(mut self, selection: TrustAllChainIpSelection) -> Self {
354        self.mode.chain_ip_selection = selection;
355        self
356    }
357
358    /// Build an immutable config.
359    #[inline]
360    pub fn build(self) -> ClientIpConfig {
361        ClientIpConfig {
362            trust: TrustModel::TrustAllProxies(self.mode)
363        }
364    }
365}
366
367/// Settings used when every socket peer is treated as a trusted proxy.
368#[derive(Clone, Debug, Eq, Hash, PartialEq)]
369pub struct TrustAllProxyMode {
370    pub(crate) client_ip_header:   Option<Uncased<'static>>,
371    pub(crate) chain_header_order: Vec<ChainHeader>,
372    pub(crate) chain_ip_selection: TrustAllChainIpSelection,
373}
374
375impl TrustAllProxyMode {
376    /// Return the header that holds a single client IP, checked before any chain header.
377    #[inline]
378    pub const fn client_ip_header(&self) -> Option<&Uncased<'static>> {
379        self.client_ip_header.as_ref()
380    }
381
382    /// Return the chain headers in the order they are tried.
383    #[inline]
384    pub fn chain_header_order(&self) -> &[ChainHeader] {
385        &self.chain_header_order
386    }
387
388    /// Return which hop of a chain header becomes the client IP.
389    #[inline]
390    pub const fn chain_ip_selection(&self) -> TrustAllChainIpSelection {
391        self.chain_ip_selection
392    }
393}
394
395impl Default for TrustAllProxyMode {
396    #[inline]
397    fn default() -> Self {
398        Self {
399            client_ip_header:   None,
400            chain_header_order: default_chain_header_order(),
401            chain_ip_selection: TrustAllChainIpSelection::Rightmost,
402        }
403    }
404}
405
406/// Which hop of a chain header becomes the client IP in trust-all proxy mode.
407///
408/// There are no CIDRs to compare against in this mode, so one hop is picked by position instead of by scanning the chain. If that hop carries no usable IP address, the search stops and the socket peer IP becomes the answer.
409#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
410pub enum TrustAllChainIpSelection {
411    /// Use the first hop of the chain.
412    ///
413    /// This is the value people usually mean by "the `X-Forwarded-For` address", but it is also the part a client can write freely. It is safe only when the proxy in front of the service overwrites or clears the header instead of appending to it.
414    Leftmost,
415
416    /// Use the last hop of the chain.
417    ///
418    /// A proxy that appends writes the address it received the request from, so the last hop is the one value in the chain that a client cannot choose. This is the default, and it is correct when exactly one proxy appends to the header.
419    Rightmost,
420
421    /// Skip a number of hops from the right, then use the next one.
422    ///
423    /// Use this when a fixed number of proxies append to the header, such as a CDN in front of a load balancer. `SkipRightmostHops(0)` is the same as [`Self::Rightmost`].
424    ///
425    /// Every hop counts, including one that carries no usable IP address. When the chain is shorter than this needs, the search stops and the socket peer IP becomes the answer.
426    SkipRightmostHops(usize),
427}
428
429/// Extra behavior attached to a trusted proxy rule.
430#[derive(Clone, Debug, Eq, Hash, PartialEq)]
431pub(crate) struct TrustedProxyMetadata {
432    pub(crate) client_ip_header: Option<Uncased<'static>>,
433}
434
435impl TrustedProxyMetadata {
436    #[inline]
437    pub(crate) const fn none() -> Self {
438        Self {
439            client_ip_header: None
440        }
441    }
442
443    #[inline]
444    pub(crate) const fn with_client_ip_header(header: Uncased<'static>) -> Self {
445        Self {
446            client_ip_header: Some(header)
447        }
448    }
449}
450
451/// A trusted proxy CIDR, and the client IP header that proxy is allowed to set.
452///
453/// The CIDR decides when this rule applies. If the rule names a client IP header, that header is read before any chain header.
454#[derive(Clone, Debug, Eq, Hash, PartialEq)]
455pub struct TrustedProxyRule {
456    pub(crate) cidr:     IpCidr,
457    pub(crate) metadata: TrustedProxyMetadata,
458}
459
460impl TrustedProxyRule {
461    /// Create a trusted proxy rule without a client IP header.
462    #[inline]
463    pub const fn new(cidr: IpCidr) -> Self {
464        Self {
465            cidr,
466            metadata: TrustedProxyMetadata::none(),
467        }
468    }
469
470    /// Create a trusted proxy rule with a client IP header.
471    ///
472    /// Use this when the proxy writes the client address into one header that holds a single IP. The header is read only for socket peers inside this CIDR. If it is missing or unusable, the chain headers are still tried, so the proxy should also clear or overwrite the chain headers it does not set itself.
473    #[inline]
474    pub const fn with_client_ip_header(cidr: IpCidr, header: Uncased<'static>) -> Self {
475        Self {
476            cidr,
477            metadata: TrustedProxyMetadata::with_client_ip_header(header),
478        }
479    }
480
481    /// Create a trusted proxy rule for the `X-Real-IP` header.
482    #[inline]
483    pub const fn with_x_real_ip(cidr: IpCidr) -> Self {
484        Self::with_client_ip_header(cidr, Uncased::from_borrowed("x-real-ip"))
485    }
486
487    /// Return the CIDR this rule matches.
488    #[inline]
489    pub const fn cidr(&self) -> &IpCidr {
490        &self.cidr
491    }
492
493    /// Return the client IP header of this rule, if it has one.
494    #[inline]
495    pub const fn client_ip_header(&self) -> Option<&Uncased<'static>> {
496        self.metadata.client_ip_header.as_ref()
497    }
498}
499
500/// A header that carries a chain of client and proxy addresses.
501///
502/// `Forwarded` is read with the RFC 7239 syntax. Every other header name, including `X-Forwarded-For`, is read as a comma-separated list of addresses.
503///
504/// The reading style is decided once, when the chain header is created. [`ChainHeader::new`] takes it from the header name, and [`ChainHeader::forwarded_style`] sets it to the RFC 7239 syntax whatever the name is.
505#[derive(Clone, Debug, Eq, Hash, PartialEq)]
506pub struct ChainHeader {
507    name: Uncased<'static>,
508    kind: ChainHeaderKind,
509}
510
511/// How the value of a chain header is read.
512#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
513pub(crate) enum ChainHeaderKind {
514    /// An `X-Forwarded-For` style comma-separated list, which is also how every custom chain header is read.
515    XForwardedFor,
516
517    /// The RFC 7239 `Forwarded` syntax.
518    Forwarded,
519}
520
521impl ChainHeader {
522    /// Create a chain header from a header name.
523    ///
524    /// Use this for a custom header that carries an `X-Forwarded-For` style comma-separated list. The name `forwarded` still gets its RFC 7239 reading, and [`Self::forwarded_style`] gives that reading to any other name.
525    #[inline]
526    pub fn new(header: Uncased<'static>) -> Self {
527        let kind = if header == "forwarded" {
528            ChainHeaderKind::Forwarded
529        } else {
530            ChainHeaderKind::XForwardedFor
531        };
532
533        Self {
534            name: header,
535            kind,
536        }
537    }
538
539    /// Create the `X-Forwarded-For` chain header.
540    #[inline]
541    pub const fn x_forwarded_for() -> Self {
542        Self {
543            name: Uncased::from_borrowed("x-forwarded-for"),
544            kind: ChainHeaderKind::XForwardedFor,
545        }
546    }
547
548    /// Create the `Forwarded` chain header.
549    #[inline]
550    pub const fn forwarded() -> Self {
551        Self {
552            name: Uncased::from_borrowed("forwarded"), kind: ChainHeaderKind::Forwarded
553        }
554    }
555
556    /// Create a chain header that is read with the RFC 7239 `Forwarded` syntax.
557    ///
558    /// Use this for a proxy that sends that syntax under a name of its own, because [`Self::new`] reads every name other than `forwarded` as a comma-separated list.
559    #[inline]
560    pub const fn forwarded_style(header: Uncased<'static>) -> Self {
561        Self {
562            name: header, kind: ChainHeaderKind::Forwarded
563        }
564    }
565
566    /// Return the wrapped header name.
567    #[inline]
568    pub const fn as_header_name(&self) -> &Uncased<'static> {
569        &self.name
570    }
571
572    /// Consume this chain header and return the wrapped header name.
573    #[inline]
574    pub fn into_header_name(self) -> Uncased<'static> {
575        self.name
576    }
577
578    #[inline]
579    pub(crate) const fn kind(&self) -> ChainHeaderKind {
580        self.kind
581    }
582}
583
584impl From<Uncased<'static>> for ChainHeader {
585    #[inline]
586    fn from(header: Uncased<'static>) -> Self {
587        Self::new(header)
588    }
589}
590
591/// The chain headers a config reads when none are named: `X-Forwarded-For`, then `Forwarded`.
592#[inline]
593fn default_chain_header_order() -> Vec<ChainHeader> {
594    vec![ChainHeader::x_forwarded_for(), ChainHeader::forwarded()]
595}