rocket_client_addr/lib.rs
1/*!
2# Client's IP Address Request Guard for Rocket Framework
3
4Resolve client IP addresses in `rocket` from trusted proxy headers with safe socket fallback.
5
6The [`ClientIp`] request guard reads a [`ClientIpConfig`] from Rocket's managed state. The config decides how much of a request is allowed to change the answer, and every answer falls back to the socket peer IP that Rocket records in [`rocket::request::Request::remote`].
7
8## Why the socket address is not enough
9
10The socket peer IP is the address the connection came from. When the service sits behind a proxy, that address belongs to the proxy, and the client is one or more hops further away.
11
12Proxies report the hops they hid in a forwarding header. A header is only text a client can also write, though, so the service has to decide which parts of it were written by something it trusts.
13
14## Forwarding headers
15
16### X-Forwarded-For
17
18`X-Forwarded-For` is a comma-separated list of addresses. Each proxy along the path appends the address it received the request from, so the list grows on the right.
19
20Take a client at `203.0.113.10` that reaches the service through two proxies.
21
22```text
23203.0.113.10 -> 198.51.100.7 -> 10.0.0.2 -> service
24 client proxy 1 proxy 2
25```
26
27The service then sees this request.
28
29```text
30X-Forwarded-For: 203.0.113.10, 198.51.100.7
31socket peer: 10.0.0.2
32```
33
34* The **leftmost** value is the oldest hop. It is the original client if every proxy on the path appended honestly, but it is also the part a client can write freely, because the first proxy appends to whatever the client sent instead of replacing it.
35* The **rightmost** value is the newest hop, written by the proxy closest to the service. It is the address that proxy received the request from, and it is the one value in the list that no client can choose.
36* The **socket peer IP is not in the header**. It belongs to the proxy that opened the connection, so the full path is the header values followed by the socket peer.
37
38Every hop is only as trustworthy as the proxy that wrote it. Everything left of the rightmost value was copied from what an earlier hop claimed.
39
40Several lines of the same header name are read as one list, joined in the order they arrived.
41
42### Forwarded
43
44`Forwarded` (RFC 7239) carries the same chain in a different syntax, with the same left-to-right meaning.
45
46```text
47Forwarded: for=203.0.113.10, for=198.51.100.7
48```
49
50Only the `for` parameter is read. An element may also carry `by`, `host`, and `proto`, which say nothing about the client address and are ignored. Parameter names are case-insensitive, so `For=` works too.
51
52### What one hop may look like
53
54A hop may carry a port, and an IPv6 hop may be bracketed and quoted.
55
56```text
57203.0.113.10
58192.0.2.43:47011
59[2001:db8::17]:4711
60for="[2001:db8::17]:4711"
61```
62
63A hop may also exist without revealing an address. `unknown` and an obfuscated identifier such as `_hidden` are hops with no usable IP address, and so is a `Forwarded` element without a `for` parameter. Such a hop still takes one place in the chain.
64
65A header value that cannot be read as visible ASCII hides an unknown number of hops, so that whole chain header becomes unusable.
66
67## How the client IP is chosen
68
69Two rules run through every trust model.
70
71* The socket peer IP is always the fallback. It is the answer whenever no header is trusted enough to change it.
72* A header is only read when a proxy the config trusts is known to have written it.
73
74Chain headers are configured as an ordered list, but they are alternatives rather than 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 and the socket peer IP is used, rather than falling back to a header that same proxy may never have touched.
75
76## Trust models
77
78A config uses exactly one trust model, chosen as the first step of [`ClientIpConfig::builder`].
79
80### Trust no proxy
81
82No header is read. Every request resolves to the socket peer IP. This is also what [`ClientIpConfig::default`] gives you.
83
84Use this when clients reach the service directly.
85
86```rust
87use std::net::IpAddr;
88
89use rocket_client_addr::{ClientIpConfig, ClientIpSource, HeaderMap};
90
91let config = ClientIpConfig::builder().trust_no_proxy();
92
93let mut headers = HeaderMap::new();
94headers.add_raw("x-forwarded-for", "203.0.113.10");
95
96// The header is never read, so the answer is the address the connection came from.
97let client_ip = config.resolve_client_ip(&headers, "198.51.100.7".parse::<IpAddr>().unwrap());
98
99assert_eq!("198.51.100.7".parse::<IpAddr>().unwrap(), client_ip.ip());
100assert_eq!(&ClientIpSource::Socket, client_ip.source());
101```
102
103### Trusted proxies
104
105Headers are read only when the socket peer IP falls inside one of the trusted CIDRs. Use this whenever the proxy addresses are known, because a client that reaches the service directly can never make it read forwarding headers.
106
107The answer is chosen in this order.
108
1091. If the socket peer IP matches no trusted proxy rule, the socket peer IP is the answer.
1102. If the matched rule names a client IP header, such as `X-Real-IP`, and the last value of that header is a plain IP address, that address is the answer.
1113. Otherwise the first chain header the request carries, in the configured order, which is `X-Forwarded-For` then `Forwarded` by default.
1124. Otherwise the socket peer IP is the answer.
113
114A chain header is scanned from the socket side toward the original client, and the first hop that is not itself a trusted proxy becomes the client IP. A hop with no usable IP address ends the scan, because it cannot be compared with the trusted proxy rules, and neither can anything further left that such a hop may have written.
115
116The example below resolves the request from the diagram above.
117
118```rust
119use std::net::IpAddr;
120
121use rocket_client_addr::{ClientIpConfig, ClientIpSource, HeaderMap, IpCidr, Uncased};
122
123let config = ClientIpConfig::builder()
124 .trusted_proxies()
125 .proxy("10.0.0.0/24".parse::<IpCidr>().unwrap())
126 .proxy("198.51.100.0/24".parse::<IpCidr>().unwrap())
127 .build()
128 .unwrap();
129
130let mut headers = HeaderMap::new();
131headers.add_raw("x-forwarded-for", "203.0.113.10, 198.51.100.7");
132
133// The socket peer 10.0.0.2 is a trusted proxy, so the chain is read, starting from the right.
134// 198.51.100.7 is a trusted proxy too, so the scan walks one hop further left.
135// 203.0.113.10 is not a trusted proxy, so it is the client.
136let client_ip = config.resolve_client_ip(&headers, "10.0.0.2".parse::<IpAddr>().unwrap());
137
138assert_eq!("203.0.113.10".parse::<IpAddr>().unwrap(), client_ip.ip());
139assert_eq!(
140 &ClientIpSource::ChainHeader(Uncased::from_borrowed("x-forwarded-for")),
141 client_ip.source(),
142);
143
144// The same header sent from an address outside the trusted CIDRs is never read.
145let direct = config.resolve_client_ip(&headers, "192.0.2.5".parse::<IpAddr>().unwrap());
146
147assert_eq!("192.0.2.5".parse::<IpAddr>().unwrap(), direct.ip());
148assert_eq!(&ClientIpSource::Socket, direct.source());
149```
150
151### Trust all proxies
152
153Every socket peer is treated as a trusted proxy. Use this only when the service can never be reached directly, because any client that can open a connection can then choose its own address.
154
155The answer is chosen in this order.
156
1571. If a client IP header is configured and holds a plain IP address, that address is the answer.
1582. Otherwise the first chain header the request carries, in the configured order, with one hop picked by [`TrustAllChainIpSelection`].
1593. Otherwise the socket peer IP is the answer.
160
161There are no CIDRs to compare against here, so the chain is not scanned. One hop is picked by position instead. If that hop carries no usable IP address, or the chain is shorter than the selection needs, the socket peer IP is the answer.
162
163```rust
164use std::net::IpAddr;
165
166use rocket_client_addr::{ClientIpConfig, HeaderMap, TrustAllChainIpSelection};
167
168// Rightmost is the default, and it fits one proxy that appends to the header.
169let one_proxy = ClientIpConfig::builder().trust_all_proxies().build();
170
171let mut headers = HeaderMap::new();
172// The client wrote 9.9.9.9 itself, and then the proxy appended the address it saw.
173headers.add_raw("x-forwarded-for", "9.9.9.9, 203.0.113.10");
174
175let client_ip = one_proxy.resolve_client_ip(&headers, "10.0.0.2".parse::<IpAddr>().unwrap());
176
177assert_eq!("203.0.113.10".parse::<IpAddr>().unwrap(), client_ip.ip());
178
179// With two proxies appending, the client is one more hop to the left.
180let two_proxies = ClientIpConfig::builder()
181 .trust_all_proxies()
182 .chain_ip_selection(TrustAllChainIpSelection::SkipRightmostHops(1))
183 .build();
184
185let mut headers = HeaderMap::new();
186headers.add_raw("x-forwarded-for", "203.0.113.10, 198.51.100.7, 10.0.0.2");
187
188let client_ip = two_proxies.resolve_client_ip(&headers, "10.0.0.3".parse::<IpAddr>().unwrap());
189
190assert_eq!("198.51.100.7".parse::<IpAddr>().unwrap(), client_ip.ip());
191```
192
193## Using the request guard
194
195```rust,no_run
196#[macro_use]
197extern crate rocket;
198
199use rocket_client_addr::{ClientIp, ClientIpConfig, IpCidr};
200
201#[get("/")]
202fn index(client_ip: &ClientIp) -> String {
203 format!("client_ip={} source={:?}\n", client_ip.ip(), client_ip.source())
204}
205
206#[rocket::main]
207async fn main() -> Result<(), rocket::Error> {
208 let config = ClientIpConfig::builder()
209 .trusted_proxies()
210 // Trust this proxy range, and read X-Real-IP only when the socket peer is inside it.
211 .proxy_with_x_real_ip("10.0.0.0/24".parse::<IpCidr>().unwrap())
212 .build()
213 .unwrap();
214
215 // The guard looks the config up by type, so it has to be managed here.
216 rocket::build().manage(config).mount("/", routes![index]).launch().await?;
217
218 Ok(())
219}
220```
221
222A route may take `&ClientIp` as above, or an owned [`ClientIp`]. The borrowed form resolves the address once per request and caches it, so prefer it when several guards on one route need the client address.
223
224Without a managed [`ClientIpConfig`] the guard fails with [`ClientIpRejection::MissingConfig`], and Rocket answers `500 Internal Server Error`. Both are server setup mistakes rather than bad requests, so neither is reported to the client.
225
226A route may also take `Option<ClientIp>`, which resolves to [`None`] instead of failing the request.
227
228## Rocket's own `ip_header`
229
230Rocket has a client address mechanism of its own. [`rocket::Config::ip_header`] names one header, `X-Real-IP` by default, and it feeds [`rocket::request::Request::real_ip`], [`rocket::request::Request::client_ip`], and Rocket's built-in `IpAddr` request guard. That header is trusted no matter which address the request came from.
231
232This crate ignores all of it. Nothing here reads `ip_header`, and a header only becomes the answer through a [`ClientIpConfig`] that trusts the proxy that sent it. Setting `ip_header = false` in `Rocket.toml` turns the built-in mechanism off, so that the two cannot disagree.
233
234## IPv4-mapped IPv6 addresses
235
236A dual-stack listener reports an IPv4 peer as `::ffff:203.0.113.10`, and some proxies write that form into their headers.
237
238Every address is rewritten to its IPv4 form first. This covers the socket peer IP as well as every header value, so a service on a dual-stack listener still matches trusted proxy CIDRs that are written in IPv4 form, and one client always resolves to one address. A trusted proxy CIDR written in the same form, such as `::ffff:10.0.0.0/120`, is rewritten to `10.0.0.0/24` while the config is built.
239
240Because the socket peer IP is rewritten first, an IPv6 CIDR such as `::/0` never matches an IPv4 peer, not even one that arrived as `::ffff:10.0.0.2`. Add a CIDR for each address family you want to trust.
241
242## Custom chain headers
243
244Some proxies use their own list header instead of `X-Forwarded-For`. Pass its name to [`ChainHeader::new`], and it is read as a comma-separated list of addresses.
245
246```rust
247use std::net::IpAddr;
248
249use rocket_client_addr::{ChainHeader, ClientIpConfig, HeaderMap, Uncased};
250
251let config = ClientIpConfig::builder()
252 .trust_all_proxies()
253 .chain_header_order([ChainHeader::new(Uncased::from_borrowed("x-client-chain"))])
254 .build();
255
256let mut headers = HeaderMap::new();
257headers.add_raw("x-client-chain", "9.9.9.9, 203.0.113.10");
258
259let client_ip = config.resolve_client_ip(&headers, "10.0.0.2".parse::<IpAddr>().unwrap());
260
261assert_eq!("203.0.113.10".parse::<IpAddr>().unwrap(), client_ip.ip());
262```
263
264The name `forwarded` keeps its RFC 7239 reading even when it goes through [`ChainHeader::new`].
265
266A proxy may also send the RFC 7239 syntax under a name of its own. [`ChainHeader::forwarded_style`] gives that reading to any header name.
267
268```rust
269use std::net::IpAddr;
270
271use rocket_client_addr::{ChainHeader, ClientIpConfig, HeaderMap, Uncased};
272
273let config = ClientIpConfig::builder()
274 .trust_all_proxies()
275 .chain_header_order([ChainHeader::forwarded_style(Uncased::from_borrowed("x-forwarded"))])
276 .build();
277
278let mut headers = HeaderMap::new();
279headers.add_raw("x-forwarded", "for=9.9.9.9, for=203.0.113.10");
280
281let client_ip = config.resolve_client_ip(&headers, "10.0.0.2".parse::<IpAddr>().unwrap());
282
283assert_eq!("203.0.113.10".parse::<IpAddr>().unwrap(), client_ip.ip());
284```
285
286## Security notes
287
288* A client IP header only means something if the proxy always overwrites it. If the proxy appends instead, the last value wins, because that is the one the client could not write.
289* A trusted proxy rule that names a client IP header still tries the chain headers when that header is missing or unusable, so the proxy should also clear the chain headers it does not set itself. Use `disable_chain_headers` if it cannot.
290* In trust-all proxy mode, [`TrustAllChainIpSelection::Leftmost`] returns the part of the chain that a client can write freely. It is safe only when the proxy overwrites the whole header.
291* Rocket drops a whole header line whose value is not valid UTF-8 before this crate ever sees the request, and only logs a warning. A chain header that lost a line that way looks one hop shorter than it was, which matters to [`TrustAllChainIpSelection::SkipRightmostHops`]. A proxy does not write such values, so this only comes up when a client can reach the service directly.
292*/
293
294mod canonical;
295mod cidr_merge;
296mod config;
297mod errors;
298mod guard;
299mod headers;
300mod resolve;
301
302pub use cidr::IpCidr;
303pub use config::{
304 ChainHeader, ClientIpConfig, ClientIpConfigBuilder, TrustAllChainIpSelection,
305 TrustAllProxiesBuilder, TrustAllProxyMode, TrustedProxiesBuilder, TrustedProxyRule,
306};
307pub use errors::{ClientIpConfigBuildError, ClientIpRejection};
308pub use guard::{ClientIp, ClientIpSource};
309pub use rocket::http::{HeaderMap, uncased::Uncased};