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}