Skip to main content

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#![forbid(unsafe_code)]
19#![deny(
20    missing_copy_implementations,
21    rustdoc::missing_crate_level_docs,
22    missing_debug_implementations,
23    missing_docs,
24    nonstandard_style,
25    unused_qualifications
26)]
27
28#[cfg(test)]
29#[doc = include_str!("../README.md")]
30mod readme {}
31
32mod forwarded;
33pub use forwarded::Forwarded;
34
35mod parse_utils;
36
37use std::{fmt::Debug, net::IpAddr, ops::Deref};
38use trillium::{Conn, Handler, Status, Transport};
39
40#[derive(Debug, Default)]
41#[non_exhaustive]
42enum TrustProxy {
43    Always,
44
45    #[default]
46    Never,
47
48    Cidr(Vec<cidr::AnyIpCidr>),
49
50    Function(TrustFn),
51}
52
53struct TrustFn(Box<dyn Fn(&IpAddr) -> bool + Send + Sync + 'static>);
54impl<F> From<F> for TrustFn
55where
56    F: Fn(&IpAddr) -> bool + Send + Sync + 'static,
57{
58    fn from(f: F) -> Self {
59        Self(Box::new(f))
60    }
61}
62impl Debug for TrustFn {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_tuple("TrustPredicate")
65            .field(&format_args!(".."))
66            .finish()
67    }
68}
69
70impl Deref for TrustFn {
71    type Target = dyn Fn(&IpAddr) -> bool + Send + Sync + 'static;
72
73    fn deref(&self) -> &Self::Target {
74        &self.0
75    }
76}
77
78impl TrustProxy {
79    fn is_trusted(&self, ip: Option<IpAddr>) -> bool {
80        match (self, ip) {
81            (TrustProxy::Always, _) => true,
82            (TrustProxy::Cidr(cidrs), Some(ip)) => cidrs.iter().any(|c| c.contains(&ip)),
83            (TrustProxy::Function(trust_predicate), Some(ip)) => trust_predicate(&ip),
84            _ => false,
85        }
86    }
87}
88
89/// Trillium handler for `forwarded`/`x-forwarded-*` headers
90///
91/// See crate-level docs for an explanation
92#[derive(Default, Debug)]
93pub struct Forwarding(TrustProxy);
94
95impl From<TrustProxy> for Forwarding {
96    fn from(tp: TrustProxy) -> Self {
97        Self(tp)
98    }
99}
100
101impl Forwarding {
102    /// builds a Forwarding handler that trusts a list of strings that represent either specific IPs
103    /// or a CIDR range.
104    ///
105    /// ```
106    /// # use trillium_forwarding::Forwarding;
107    /// let forwarding = Forwarding::trust_ips(["10.1.10.1"]);
108    /// let forwarding = Forwarding::trust_ips(["10.1.10.1", "192.168.0.0/16"]);
109    /// ```
110    pub fn trust_ips<'a>(ips: impl IntoIterator<Item = &'a str>) -> Self {
111        Self(TrustProxy::Cidr(
112            ips.into_iter().map(|ip| ip.parse().unwrap()).collect(),
113        ))
114    }
115
116    /// builds a Forwarding handler that trusts a peer ip based on the provided predicate function.
117    ///
118    /// ```
119    /// # use trillium_forwarding::Forwarding;
120    /// # use std::net::IpAddr;
121    /// let forwarding = Forwarding::trust_fn(IpAddr::is_loopback);
122    /// let forwarding = Forwarding::trust_fn(|ip| match ip {
123    ///     IpAddr::V6(_) => false,
124    ///     IpAddr::V4(ipv4) => ipv4.is_link_local(),
125    /// });
126    /// ```
127    pub fn trust_fn<F>(trust_predicate: F) -> Self
128    where
129        F: Fn(&IpAddr) -> bool + Send + Sync + 'static,
130    {
131        Self(TrustProxy::Function(TrustFn::from(trust_predicate)))
132    }
133
134    /// builds a Forwarding handler that expects that all http connections
135    /// will always come from a trusted and spec-compliant reverse
136    /// proxy. This should only be used in situations in which the
137    /// application is either running inside of a vpc and the reverse
138    /// proxy ip cannot be known. Using an overbroad trust rule such as
139    /// `trust_always` introduces security risk to an application, as it
140    /// allows any request to forge Forwarded headers.
141    pub fn trust_always() -> Self {
142        Self(TrustProxy::Always)
143    }
144}
145
146impl Handler for Forwarding {
147    async fn run(&self, mut conn: Conn) -> Conn {
148        if !self.0.is_trusted(conn.peer_ip()) {
149            return conn;
150        }
151
152        let forwarded = match Forwarded::from_headers(conn.request_headers()) {
153            Ok(Some(forwarded)) => forwarded.into_owned(),
154            Err(error) => {
155                log::error!("{error}");
156                return conn
157                    .halt()
158                    .with_state(error)
159                    .with_status(Status::BadRequest);
160            }
161            Ok(None) => return conn,
162        };
163
164        log::debug!("received trusted forwarded {:?}", &forwarded);
165
166        let inner_mut: &mut trillium_http::Conn<Box<dyn Transport>> = conn.as_mut();
167
168        if let Some(host) = forwarded.host() {
169            inner_mut.set_host(String::from(host));
170        }
171
172        if let Some(proto) = forwarded.proto() {
173            inner_mut.set_secure(proto == "https");
174        }
175
176        if let Some(ip) = forwarded.forwarded_for().first()
177            && let Ok(ip_addr) = ip
178                .trim_start_matches('[')
179                .trim_end_matches(']')
180                .parse::<IpAddr>()
181        {
182            inner_mut.set_peer_ip(Some(ip_addr));
183        }
184
185        conn.with_state(forwarded)
186    }
187}