1use std::net::IpAddr;
9use std::sync::Arc;
10
11use ipnet::IpNet;
12
13use crate::cf_ip_list::CloudflareIpCache;
14
15#[derive(Clone)]
18pub struct TrustedProxyList {
19 user_cidrs: Vec<IpNet>,
20 cf_cache: Option<Arc<CloudflareIpCache>>,
21}
22
23impl TrustedProxyList {
24 #[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 #[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 #[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 #[must_use]
77 pub fn user_cidrs(&self) -> &[IpNet] {
78 &self.user_cidrs
79 }
80
81 #[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}