Skip to main content

zlayer_proxy/
trust.rs

1//! Trusted-proxy predicate.
2//!
3//! Combines user-configured trusted-proxy CIDRs with an optional Cloudflare IP
4//! cache into a single `is_trusted(peer_ip) -> bool` predicate. The
5//! reverse-proxy service uses this to decide whether to honor
6//! `CF-Connecting-IP` / `X-Forwarded-For` request headers from a given peer.
7
8use std::net::IpAddr;
9use std::sync::Arc;
10
11use ipnet::IpNet;
12
13use crate::cf_ip_list::CloudflareIpCache;
14
15/// A list of trusted upstream proxies, expressed as a set of user CIDRs plus
16/// an optional Cloudflare IP cache.
17#[derive(Clone)]
18pub struct TrustedProxyList {
19    user_cidrs: Vec<IpNet>,
20    cf_cache: Option<Arc<CloudflareIpCache>>,
21}
22
23impl TrustedProxyList {
24    /// Build a trusted-proxy list from user CIDRs and an optional Cloudflare
25    /// IP cache.
26    #[must_use]
27    pub fn new(user_cidrs: Vec<IpNet>, cf_cache: Option<Arc<CloudflareIpCache>>) -> Self {
28        Self {
29            user_cidrs,
30            cf_cache,
31        }
32    }
33
34    /// Shortcut: localhost-only, no CF. Safe default for origins that are NOT
35    /// behind any upstream proxy.
36    ///
37    /// # Panics
38    ///
39    /// This function contains `.expect()` calls on CIDR parsing, but in
40    /// practice it never panics: the CIDR strings (`127.0.0.0/8` and
41    /// `::1/128`) are hardcoded constants that are guaranteed to be valid
42    /// `IpNet` values. The expects exist only to satisfy the `FromStr`
43    /// signature.
44    #[must_use]
45    pub fn localhost_only() -> Self {
46        Self::new(
47            vec![
48                "127.0.0.0/8"
49                    .parse()
50                    .expect("hardcoded loopback CIDR is valid"),
51                "::1/128"
52                    .parse()
53                    .expect("hardcoded IPv6 loopback CIDR is valid"),
54            ],
55            None,
56        )
57    }
58
59    /// True if `peer` is in `user_cidrs` OR in the CF cache (when present).
60    #[must_use]
61    pub fn is_trusted(&self, peer: IpAddr) -> bool {
62        if self.user_cidrs.iter().any(|net| net.contains(&peer)) {
63            return true;
64        }
65
66        if let Some(cache) = &self.cf_cache {
67            if cache.contains(peer) {
68                return true;
69            }
70        }
71
72        false
73    }
74
75    /// Returns the configured user CIDRs (for diagnostics / inspection).
76    #[must_use]
77    pub fn user_cidrs(&self) -> &[IpNet] {
78        &self.user_cidrs
79    }
80
81    /// True if a CF cache is attached.
82    #[must_use]
83    pub fn has_cloudflare_trust(&self) -> bool {
84        self.cf_cache.is_some()
85    }
86}
87
88impl std::fmt::Debug for TrustedProxyList {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("TrustedProxyList")
91            .field("user_cidrs", &self.user_cidrs)
92            .field("cf_cache_attached", &self.cf_cache.is_some())
93            .finish()
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn localhost_only_trusts_127_0_0_1() {
103        let list = TrustedProxyList::localhost_only();
104        assert!(list.is_trusted("127.0.0.1".parse().unwrap()));
105    }
106
107    #[test]
108    fn localhost_only_rejects_public_ip() {
109        let list = TrustedProxyList::localhost_only();
110        assert!(!list.is_trusted("8.8.8.8".parse().unwrap()));
111    }
112
113    #[test]
114    fn user_cidr_trusts_in_range_rejects_out_of_range() {
115        let list = TrustedProxyList::new(vec!["10.0.0.0/24".parse().unwrap()], None);
116        assert!(list.is_trusted("10.0.0.5".parse().unwrap()));
117        assert!(!list.is_trusted("10.0.1.5".parse().unwrap()));
118    }
119
120    #[test]
121    fn cf_cache_consulted_when_attached() {
122        let cache = CloudflareIpCache::new_with_fallback();
123        let list = TrustedProxyList::new(vec![], Some(cache));
124        assert!(list.is_trusted("104.16.0.1".parse().unwrap()));
125    }
126
127    #[test]
128    fn empty_list_rejects_everything() {
129        let list = TrustedProxyList::new(vec![], None);
130        assert!(!list.is_trusted("1.2.3.4".parse().unwrap()));
131    }
132}