Skip to main content

net_io/
trusted.rs

1//! The one list of proxies whose claims about the client address are believed.
2
3use std::net::IpAddr;
4
5use crate::addr::{AddrError, Cidr, normalize};
6
7/// The proxies whose claims about the client address are believed: PROXY
8/// headers here, and `X-Forwarded-For` / `Forwarded` in the web layer, which
9/// should use the same value. Keep it minimal: a trusted proxy can claim any
10/// client address.
11///
12/// Sorted, deduplicated, at most [`TrustedProxies::MAX`] blocks. `contains` is a
13/// binary search over the blocks merged into disjoint ranges, IPv4 and IPv6 kept
14/// apart; it allocates nothing.
15///
16/// # Examples
17///
18/// ```
19/// use net_io::TrustedProxies;
20/// let proxies = TrustedProxies::parse("10.0.0.0/8, 192.168.1.10 ::1")?;
21/// assert!(proxies.contains("10.4.5.6".parse().unwrap()));
22/// assert!(!proxies.contains("8.8.8.8".parse().unwrap()));
23/// # Ok::<(), net_io::AddrError>(())
24/// ```
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct TrustedProxies {
27    blocks: Vec<Cidr>,
28    v4: Vec<(u32, u32)>,
29    v6: Vec<(u128, u128)>,
30}
31
32impl TrustedProxies {
33    /// The most blocks a list holds.
34    pub const MAX: usize = 1024;
35
36    /// A list of these blocks (duplicates removed).
37    ///
38    /// # Errors
39    ///
40    /// [`AddrError::TooMany`] above [`TrustedProxies::MAX`] blocks.
41    ///
42    /// ```
43    /// use net_io::{Cidr, TrustedProxies};
44    /// let p = TrustedProxies::new(&[Cidr::parse("10.0.0.0/8")?, Cidr::parse("10.0.0.0/8")?])?;
45    /// assert_eq!(p.len(), 1);
46    /// # Ok::<(), net_io::AddrError>(())
47    /// ```
48    pub fn new(blocks: &[Cidr]) -> Result<TrustedProxies, AddrError> {
49        if blocks.len() > Self::MAX {
50            return Err(AddrError::TooMany { max: Self::MAX });
51        }
52        let mut sorted = blocks.to_vec();
53        sorted.sort_by_key(|c| sort_key(*c));
54        sorted.dedup();
55        let mut v4 = Vec::new();
56        let mut v6 = Vec::new();
57        for c in &sorted {
58            match c.network() {
59                IpAddr::V4(a) => {
60                    let start = u32::from(a);
61                    let span = if c.prefix_len() == 0 {
62                        u32::MAX
63                    } else {
64                        (1u32 << (32 - u32::from(c.prefix_len()))) - 1
65                    };
66                    v4.push((start, start | span));
67                }
68                IpAddr::V6(a) => {
69                    let start = u128::from(a);
70                    let span = if c.prefix_len() == 0 {
71                        u128::MAX
72                    } else {
73                        (1u128 << (128 - u32::from(c.prefix_len()))) - 1
74                    };
75                    v6.push((start, start | span));
76                }
77            }
78        }
79        Ok(TrustedProxies {
80            blocks: sorted,
81            v4: merge(v4),
82            v6: merge(v6),
83        })
84    }
85
86    /// Parses blocks or addresses separated by commas or whitespace (a bare
87    /// address is a /32 or /128).
88    ///
89    /// # Errors
90    ///
91    /// As [`Cidr::parse`] (with `Syntax { at }` counted from the start of `text`),
92    /// [`AddrError::Empty`] when there is no entry, [`AddrError::TooMany`].
93    ///
94    /// ```
95    /// let p = net_io::TrustedProxies::parse("127.0.0.1, ::1")?;
96    /// assert_eq!(p.len(), 2);
97    /// assert!(net_io::TrustedProxies::parse("10.0.0.1/8").is_err()); // host bits set
98    /// # Ok::<(), net_io::AddrError>(())
99    /// ```
100    pub fn parse(text: &str) -> Result<TrustedProxies, AddrError> {
101        let mut blocks = Vec::new();
102        let mut start = None;
103        for (i, ch) in text
104            .char_indices()
105            .chain(std::iter::once((text.len(), ' ')))
106        {
107            let separator = ch == ',' || ch.is_whitespace();
108            match (start, separator) {
109                (None, false) => start = Some(i),
110                (Some(s), true) => {
111                    if blocks.len() == Self::MAX {
112                        return Err(AddrError::TooMany { max: Self::MAX });
113                    }
114                    let block = Cidr::parse(&text[s..i]).map_err(|e| match e {
115                        AddrError::Syntax { at } => AddrError::Syntax { at: s + at },
116                        other => other,
117                    })?;
118                    blocks.push(block);
119                    start = None;
120                }
121                _ => {}
122            }
123        }
124        if blocks.is_empty() {
125            return Err(AddrError::Empty);
126        }
127        TrustedProxies::new(&blocks)
128    }
129
130    /// Trusts nobody.
131    ///
132    /// ```
133    /// assert!(net_io::TrustedProxies::none().is_empty());
134    /// ```
135    #[must_use]
136    pub fn none() -> TrustedProxies {
137        TrustedProxies {
138            blocks: Vec::new(),
139            v4: Vec::new(),
140            v6: Vec::new(),
141        }
142    }
143
144    /// `127.0.0.0/8` and `::1/128`: a proxy on the same host.
145    ///
146    /// ```
147    /// assert!(net_io::TrustedProxies::loopback().contains("127.0.0.1".parse().unwrap()));
148    /// ```
149    #[must_use]
150    pub fn loopback() -> TrustedProxies {
151        let blocks = [
152            Cidr::new(IpAddr::from([127, 0, 0, 0]), 8),
153            Cidr::new(IpAddr::from([0u16, 0, 0, 0, 0, 0, 0, 1]), 128),
154        ];
155        let blocks: Vec<Cidr> = blocks.into_iter().flatten().collect();
156        TrustedProxies::new(&blocks).unwrap_or_else(|_| TrustedProxies::none())
157    }
158
159    /// Whether `ip` (normalized first, so an IPv4-mapped address matches IPv4
160    /// blocks) belongs to a trusted proxy. No allocation.
161    ///
162    /// ```
163    /// let p = net_io::TrustedProxies::parse("10.0.0.0/8")?;
164    /// assert!(p.contains("::ffff:10.1.2.3".parse().unwrap()));
165    /// # Ok::<(), net_io::AddrError>(())
166    /// ```
167    #[inline]
168    #[must_use]
169    pub fn contains(&self, ip: IpAddr) -> bool {
170        match normalize(ip) {
171            IpAddr::V4(a) => in_ranges(&self.v4, u32::from(a)),
172            IpAddr::V6(a) => in_ranges(&self.v6, u128::from(a)),
173        }
174    }
175
176    /// The number of blocks.
177    #[must_use]
178    pub fn len(&self) -> usize {
179        self.blocks.len()
180    }
181
182    /// Whether the list is empty (trusts nobody).
183    #[must_use]
184    pub fn is_empty(&self) -> bool {
185        self.blocks.is_empty()
186    }
187
188    /// The blocks, sorted (IPv4 first, then by network and prefix).
189    pub fn iter(&self) -> impl Iterator<Item = Cidr> + '_ {
190        self.blocks.iter().copied()
191    }
192}
193
194fn sort_key(c: Cidr) -> (u8, u128, u8) {
195    match c.network() {
196        IpAddr::V4(a) => (4, u128::from(u32::from(a)), c.prefix_len()),
197        IpAddr::V6(a) => (6, u128::from(a), c.prefix_len()),
198    }
199}
200
201/// Sorted, inclusive ranges merged so they are disjoint and not adjacent.
202fn merge<T: Copy + Ord + Step>(mut ranges: Vec<(T, T)>) -> Vec<(T, T)> {
203    ranges.sort_unstable();
204    let mut out: Vec<(T, T)> = Vec::with_capacity(ranges.len());
205    for (start, end) in ranges {
206        match out.last_mut() {
207            Some(last) if start <= last.1.next_or_max() => last.1 = last.1.max(end),
208            _ => out.push((start, end)),
209        }
210    }
211    out.shrink_to_fit();
212    out
213}
214
215fn in_ranges<T: Copy + Ord>(ranges: &[(T, T)], x: T) -> bool {
216    // The last range starting at or before `x`.
217    let i = ranges.partition_point(|r| r.0 <= x);
218    i > 0 && x <= ranges[i - 1].1
219}
220
221/// The successor of a range end, saturating, for merging adjacent ranges.
222trait Step {
223    fn next_or_max(self) -> Self;
224}
225
226impl Step for u32 {
227    fn next_or_max(self) -> Self {
228        self.saturating_add(1)
229    }
230}
231
232impl Step for u128 {
233    fn next_or_max(self) -> Self {
234        self.saturating_add(1)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    fn ip(s: &str) -> IpAddr {
243        s.parse().unwrap()
244    }
245
246    #[test]
247    fn test_trusted_parse_mixed_separators_and_bare_addresses() {
248        let p = TrustedProxies::parse(" 10.0.0.0/8,192.168.1.10\t::1 ,, 2001:db8::/32 ").unwrap();
249        assert_eq!(p.len(), 4);
250        assert!(p.contains(ip("10.255.255.255")));
251        assert!(p.contains(ip("192.168.1.10")));
252        assert!(!p.contains(ip("192.168.1.11")));
253        assert!(p.contains(ip("::1")));
254        assert!(p.contains(ip("2001:db8:ffff::1")));
255        assert!(!p.contains(ip("2001:db9::1")));
256        assert!(!p.contains(ip("11.0.0.0")));
257    }
258
259    #[test]
260    fn test_trusted_parse_errors() {
261        assert_eq!(TrustedProxies::parse(" , "), Err(AddrError::Empty));
262        assert_eq!(
263            TrustedProxies::parse("10.0.0.0/8, nope"),
264            Err(AddrError::Syntax { at: 12 })
265        );
266        assert_eq!(
267            TrustedProxies::parse("10.0.0.1/8"),
268            Err(AddrError::HostBitsSet)
269        );
270        let mut many = String::new();
271        for i in 0..1025u32 {
272            use std::fmt::Write;
273            let _ = write!(many, "10.{}.{}.0/24 ", i / 256, i % 256);
274        }
275        assert_eq!(
276            TrustedProxies::parse(&many),
277            Err(AddrError::TooMany { max: 1024 })
278        );
279    }
280
281    #[test]
282    fn test_trusted_nested_and_adjacent_blocks_merge() {
283        let p = TrustedProxies::parse("10.0.0.0/8 10.1.0.0/16 11.0.0.0/8 0.0.0.0/1").unwrap();
284        assert_eq!(p.len(), 4, "iter keeps every distinct block");
285        assert_eq!(
286            p.v4,
287            vec![(0, u32::from(std::net::Ipv4Addr::new(127, 255, 255, 255)))]
288        );
289        assert!(p.contains(ip("11.200.0.1")));
290        assert!(!p.contains(ip("128.0.0.0")));
291    }
292
293    #[test]
294    fn test_trusted_whole_space_and_none() {
295        let all = TrustedProxies::parse("0.0.0.0/0 ::/0").unwrap();
296        assert!(all.contains(ip("255.255.255.255")));
297        assert!(all.contains(ip("ffff::1")));
298        let none = TrustedProxies::none();
299        assert!(!none.contains(ip("127.0.0.1")));
300        let lo = TrustedProxies::loopback();
301        assert_eq!(lo.len(), 2);
302        assert!(lo.contains(ip("127.9.9.9")) && lo.contains(ip("::1")) && !lo.contains(ip("::2")));
303    }
304
305    /// Property: `contains` agrees with checking every block one by one.
306    #[test]
307    fn prop_trusted_contains_matches_a_linear_scan() {
308        use crate::testing::Gen;
309        let mut g = Gen::new(0x7275_5354);
310        for _ in 0..if cfg!(miri) { 20 } else { 2000 } {
311            let n = 1 + g.below(12) as usize;
312            let blocks: Vec<Cidr> = (0..n)
313                .map(|_| {
314                    let a = std::net::Ipv4Addr::from(g.next_u64() as u32 & 0x0F0F_FFFF);
315                    Cidr::new_masked(IpAddr::V4(a), 8 + g.below(25) as u8).unwrap()
316                })
317                .collect();
318            let p = TrustedProxies::new(&blocks).unwrap();
319            for _ in 0..50 {
320                let probe = IpAddr::V4(std::net::Ipv4Addr::from(g.next_u64() as u32 & 0x0F0F_FFFF));
321                assert_eq!(
322                    p.contains(probe),
323                    blocks.iter().any(|b| b.contains(probe)),
324                    "{blocks:?} {probe}"
325                );
326            }
327        }
328    }
329}