trillium_forwarding/lib.rs
1//! # Trillium handler for `x-forwarded-*` / `forwarded`
2//!
3//! This simple handler rewrites the request's host, secure setting, and
4//! peer ip based on headers added by a trusted reverse proxy.
5//!
6//! The specific headers that are understood by this handler are:
7//!
8//! [`Forwarded`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded)
9//! or some combination of the following
10//! - [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
11//! - [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto)
12//! - [`X-Forwarded-Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host)
13//!
14//! There are several ways of specifying when to trust a peer ip address,
15//! and the narrowest possible trust rules should be used for a given
16//! deployment so as to decrease the chance for a threat actor to generate
17//! a request with forwarded headers that we mistakenly trust.
18//!
19//! Because the forwarded-for chain is append-only, only its trusted suffix is meaningful: the
20//! peer ip is taken from the rightmost entry that is not itself a trusted proxy, walking right to
21//! left. Everything to the left of that is under the control of whoever sent the request.
22#![forbid(unsafe_code)]
23#![deny(
24 missing_copy_implementations,
25 rustdoc::missing_crate_level_docs,
26 missing_debug_implementations,
27 missing_docs,
28 nonstandard_style,
29 unused_qualifications
30)]
31
32#[cfg(test)]
33#[doc = include_str!("../README.md")]
34mod readme {}
35
36mod forwarded;
37pub use forwarded::Forwarded;
38
39mod parse_utils;
40
41use std::{fmt::Debug, net::IpAddr, ops::Deref};
42use trillium::{Conn, Handler, Status, Transport};
43
44#[derive(Debug, Default)]
45#[non_exhaustive]
46enum TrustProxy {
47 Always,
48
49 #[default]
50 Never,
51
52 Cidr(Vec<cidr::AnyIpCidr>),
53
54 Function(TrustFn),
55}
56
57struct TrustFn(Box<dyn Fn(&IpAddr) -> bool + Send + Sync + 'static>);
58impl<F> From<F> for TrustFn
59where
60 F: Fn(&IpAddr) -> bool + Send + Sync + 'static,
61{
62 fn from(f: F) -> Self {
63 Self(Box::new(f))
64 }
65}
66impl Debug for TrustFn {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.debug_tuple("TrustPredicate")
69 .field(&format_args!(".."))
70 .finish()
71 }
72}
73
74impl Deref for TrustFn {
75 type Target = dyn Fn(&IpAddr) -> bool + Send + Sync + 'static;
76
77 fn deref(&self) -> &Self::Target {
78 &self.0
79 }
80}
81
82impl TrustProxy {
83 fn is_trusted(&self, ip: Option<IpAddr>) -> bool {
84 match (self, ip) {
85 (TrustProxy::Always, _) => true,
86 (TrustProxy::Cidr(cidrs), Some(ip)) => cidrs.iter().any(|c| c.contains(&ip)),
87 (TrustProxy::Function(trust_predicate), Some(ip)) => trust_predicate(&ip),
88 _ => false,
89 }
90 }
91
92 /// Walks the append-only forwarded-for chain from right to left, adopting each entry in turn
93 /// and stopping at the first one that is not itself a trusted proxy.
94 ///
95 /// Everything to the left of the entry appended by the outermost trusted proxy is under the
96 /// control of whoever sent the request, so only the trusted suffix of the chain may be
97 /// traversed. Entries that do not parse as ip addresses (obfuscated identifiers, `unknown`)
98 /// end the walk.
99 fn rightmost_untrusted(
100 &self,
101 forwarded_for: &[&str],
102 peer_ip: Option<IpAddr>,
103 ) -> Option<IpAddr> {
104 let mut peer_ip = peer_ip;
105 for entry in forwarded_for.iter().rev() {
106 let Some(ip) = parse_node(entry) else { break };
107 peer_ip = Some(ip);
108 if !self.is_trusted(peer_ip) {
109 break;
110 }
111 }
112 peer_ip
113 }
114}
115
116/// Parses an RFC 7239 node identifier such as `192.0.2.60`, `192.0.2.60:8080`,
117/// `[2001:db8::17]`, or `[2001:db8::17]:4711` as an ip address, discarding any port.
118fn parse_node(node: &str) -> Option<IpAddr> {
119 let node = node.trim();
120 if let Some(rest) = node.strip_prefix('[') {
121 return rest.split_once(']')?.0.parse().ok();
122 }
123
124 node.parse()
125 .ok()
126 .or_else(|| node.split_once(':')?.0.parse().ok())
127}
128
129/// Trillium handler for `forwarded`/`x-forwarded-*` headers
130///
131/// See crate-level docs for an explanation
132#[derive(Default, Debug)]
133pub struct Forwarding(TrustProxy);
134
135impl From<TrustProxy> for Forwarding {
136 fn from(tp: TrustProxy) -> Self {
137 Self(tp)
138 }
139}
140
141impl Forwarding {
142 /// builds a Forwarding handler that trusts a list of strings that represent either specific IPs
143 /// or a CIDR range.
144 ///
145 /// ```
146 /// # use trillium_forwarding::Forwarding;
147 /// let forwarding = Forwarding::trust_ips(["10.1.10.1"]);
148 /// let forwarding = Forwarding::trust_ips(["10.1.10.1", "192.168.0.0/16"]);
149 /// ```
150 ///
151 /// # Panics
152 ///
153 /// Panics if any of the provided strings is neither an ip address nor a CIDR range.
154 pub fn trust_ips<'a>(ips: impl IntoIterator<Item = &'a str>) -> Self {
155 Self(TrustProxy::Cidr(
156 ips.into_iter()
157 .map(|ip| {
158 ip.parse()
159 .unwrap_or_else(|_| panic!("could not parse `{ip}` as an ip or cidr range"))
160 })
161 .collect(),
162 ))
163 }
164
165 /// builds a Forwarding handler that trusts a peer ip based on the provided predicate function.
166 ///
167 /// ```
168 /// # use trillium_forwarding::Forwarding;
169 /// # use std::net::IpAddr;
170 /// let forwarding = Forwarding::trust_fn(IpAddr::is_loopback);
171 /// let forwarding = Forwarding::trust_fn(|ip| match ip {
172 /// IpAddr::V6(_) => false,
173 /// IpAddr::V4(ipv4) => ipv4.is_link_local(),
174 /// });
175 /// ```
176 pub fn trust_fn<F>(trust_predicate: F) -> Self
177 where
178 F: Fn(&IpAddr) -> bool + Send + Sync + 'static,
179 {
180 Self(TrustProxy::Function(TrustFn::from(trust_predicate)))
181 }
182
183 /// builds a Forwarding handler that expects that all http connections
184 /// will always come from a trusted and spec-compliant reverse
185 /// proxy. This should only be used in situations in which the
186 /// application is either running inside of a vpc and the reverse
187 /// proxy ip cannot be known. Using an overbroad trust rule such as
188 /// `trust_always` introduces security risk to an application, as it
189 /// allows any request to forge Forwarded headers.
190 pub fn trust_always() -> Self {
191 Self(TrustProxy::Always)
192 }
193}
194
195impl Handler for Forwarding {
196 async fn run(&self, mut conn: Conn) -> Conn {
197 if !self.0.is_trusted(conn.peer_ip()) {
198 return conn;
199 }
200
201 let forwarded = match Forwarded::from_headers(conn.request_headers()) {
202 Ok(Some(forwarded)) => forwarded.into_owned(),
203 Err(error) => {
204 log::error!("{error}");
205 return conn
206 .halt()
207 .with_state(error)
208 .with_status(Status::BadRequest);
209 }
210 Ok(None) => return conn,
211 };
212
213 log::debug!("received trusted forwarded {:?}", forwarded);
214
215 let inner_mut: &mut trillium_http::Conn<Box<dyn Transport>> = conn.as_mut();
216
217 if let Some(host) = forwarded.host() {
218 inner_mut.set_host(String::from(host));
219 }
220
221 if let Some(proto) = forwarded.proto() {
222 inner_mut.set_secure(proto.eq_ignore_ascii_case("https"));
223 }
224
225 let peer_ip = self
226 .0
227 .rightmost_untrusted(&forwarded.forwarded_for(), inner_mut.peer_ip());
228 inner_mut.set_peer_ip(peer_ip);
229
230 conn.with_state(forwarded)
231 }
232}