Skip to main content

guard_core_engine/
ip_extraction.rs

1//! The client-IP extraction from the forwarded chain: the reference
2//! `_utils/ip_extraction.py`'s depth-limited `X-Forwarded-For` walk, driven
3//! by the `trusted_proxies` / `trusted_proxy_depth` knobs.
4//!
5//! Semantics, mirroring `_resolve_client_ip_from_forwarded_chain` exactly:
6//!
7//! ```text
8//! no header:                        the connecting peer is the client
9//! chain shorter than the depth:     the connecting peer (warn once)
10//! right side holds unlisted peers:  walk right to left, skip trusted
11//!                                   entries, first untrusted wins (warn)
12//! otherwise:                        the entry `depth` from the right,
13//!                                   port-stripped and canonicalized
14//! ```
15//!
16//! A selected entry that is itself on the trusted list surfaces the
17//! over-declared-depth warning as data ([`ChainWarnings`]) - the engine
18//! has no logger, so the once-only warnings ride the result for the
19//! caller to log. Unparseable entries and metacharacter candidates
20//! (`*?[]\`) resolve back to the connecting peer, never to a spoof.
21//!
22//! # Example
23//!
24//! ```
25//! use guard_core_engine::ip_extraction::{extract_client_ip, ClientIpSource};
26//!
27//! // A trusted proxy's claim is honored one hop deep.
28//! let extraction = extract_client_ip(
29//!     Some("10.0.0.1"),
30//!     Some("203.0.113.7"),
31//!     &["10.0.0.0/8".to_owned()],
32//!     1,
33//! );
34//! assert_eq!(extraction.client_ip, "203.0.113.7");
35//! assert_eq!(extraction.source, ClientIpSource::ForwardedChain);
36//!
37//! // An untrusted peer's header is ignored (the spoof signal rides along).
38//! let extraction = extract_client_ip(
39//!     Some("192.0.2.9"),
40//!     Some("203.0.113.7"),
41//!     &["10.0.0.0/8".to_owned()],
42//!     1,
43//! );
44//! assert_eq!(extraction.client_ip, "192.0.2.9");
45//! assert!(extraction.untrusted_forwarded_header);
46//!
47//! // No trusted proxies at all: the header never decides.
48//! let extraction = extract_client_ip(Some("192.0.2.9"), Some("203.0.113.7"), &[], 1);
49//! assert_eq!(extraction.client_ip, "192.0.2.9");
50//! assert_eq!(extraction.source, ClientIpSource::Connecting);
51//! ```
52
53use std::net::IpAddr;
54use std::str::FromStr;
55
56use crate::ip_gate::{canonical, parse_network_entry};
57
58/// The reference `UNKNOWN_CLIENT_IDENTITY`: the identity a request with no
59/// connecting peer carries.
60pub const UNKNOWN_CLIENT_IDENTITY: &str = "unknown";
61
62/// Where the extracted client identity came from (the reference
63/// `extract_client_ip`'s arms).
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum ClientIpSource {
66    /// The connecting peer itself (not a trusted proxy, or nothing to walk).
67    Connecting,
68    /// Resolved from the `X-Forwarded-For` chain at the declared depth.
69    ForwardedChain,
70    /// No connecting peer at all (the `unknown` identity, or the `unix`
71    /// proxy arm walking from unknown).
72    Unknown,
73}
74
75/// The once-only warnings the reference logs, surfaced as data (the engine
76/// has no logger; the caller logs and de-duplicates).
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
78pub struct ChainWarnings {
79    /// The chain had fewer entries than `trusted_proxy_depth` (the
80    /// reference `_warn_forwarded_header_chain_too_short`).
81    pub chain_too_short: bool,
82    /// The declared depth over-counts the real hops: `Some(count)` of
83    /// right-side entries are not on the trusted list (the reference
84    /// `_warn_forwarded_header_depth_overcounts_hops`).
85    pub depth_overcounts: Option<usize>,
86    /// The selected entry is itself a trusted proxy (the reference
87    /// `_warn_forwarded_header_selected_entry_trusted_proxy`).
88    pub selected_entry_trusted: bool,
89}
90
91/// One extraction: the identity plus where it came from and what the
92/// caller should log.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ClientIpExtraction {
95    /// The resolved client identity (`UNKNOWN_CLIENT_IDENTITY` when the
96    /// request has no peer and the `unix` arm does not apply).
97    pub client_ip: String,
98    /// Which arm produced the identity.
99    pub source: ClientIpSource,
100    /// An untrusted peer sent an `X-Forwarded-For` header (the reference's
101    /// spoof-attempt signal: the header is ignored, the event is the
102    /// caller's to emit).
103    pub untrusted_forwarded_header: bool,
104    /// The chain-walk warnings (empty unless [`ClientIpSource::
105    /// ForwardedChain`] or the too-short arm ran).
106    pub warnings: ChainWarnings,
107}
108
109/// `_strip_ip_brackets`.
110#[must_use]
111fn strip_ip_brackets(value: &str) -> &str {
112    value
113        .strip_prefix('[')
114        .and_then(|rest| rest.strip_suffix(']'))
115        .unwrap_or(value)
116}
117
118/// `_strip_forwarded_entry_port`: `[v6]:port` and `host:port` lose the
119/// port; anything else passes through untouched.
120#[must_use]
121pub fn strip_forwarded_entry_port(value: &str) -> &str {
122    if let Some(rest) = value.strip_prefix('[') {
123        let Some(closing) = rest.find(']') else {
124            return value;
125        };
126        let remainder = &rest[closing + 1..];
127        let port_form = remainder.len() > 1
128            && remainder.starts_with(':')
129            && remainder[1..].chars().all(|c| c.is_ascii_digit());
130        if remainder.is_empty() || port_form {
131            return &rest[..closing];
132        }
133        return value;
134    }
135    match value.split_once(':') {
136        Some((host, port))
137            if !host.contains(':')
138                && !port.is_empty()
139                && port.chars().all(|c| c.is_ascii_digit()) =>
140        {
141            host
142        }
143        _ => value,
144    }
145}
146
147/// `_canonicalize_ip`: canonical text for a parseable address (an
148/// IPv4-mapped IPv6 collapses to its IPv4 text), the raw value otherwise.
149#[must_use]
150pub fn canonicalize_ip(value: &str) -> String {
151    IpAddr::from_str(strip_ip_brackets(value)).map_or_else(|_| value.to_owned(), canonical_ip_text)
152}
153
154/// `_canonical_ip_text`.
155#[must_use]
156pub fn canonical_ip_text(addr: IpAddr) -> String {
157    match addr {
158        IpAddr::V6(v6) => v6
159            .to_ipv4_mapped()
160            .map_or_else(|| v6.to_string(), |mapped| mapped.to_string()),
161        IpAddr::V4(v4) => v4.to_string(),
162    }
163}
164
165/// `_forwarded_header_candidate_has_metachar`.
166#[must_use]
167pub fn candidate_has_metachar(candidate: &str) -> bool {
168    candidate
169        .chars()
170        .any(|c| matches!(c, '*' | '?' | '[' | ']' | '\\'))
171}
172
173/// `_forwarded_header_ips`: the entries, comma-split, port-stripped.
174#[must_use]
175pub fn forwarded_header_ips(forwarded_for: &str) -> Vec<String> {
176    forwarded_for
177        .split(',')
178        .map(|entry| strip_forwarded_entry_port(entry.trim()).to_owned())
179        .collect()
180}
181
182/// `_proxy_matches` / `_is_trusted_proxy`: a bare entry compares exactly
183/// (the `unix` token never matches a real peer), a CIDR entry contains the
184/// canonicalized address.
185#[must_use]
186pub fn is_trusted_proxy(connecting: &str, trusted_proxies: &[String]) -> bool {
187    trusted_proxies.iter().any(|proxy| {
188        parse_network_entry(proxy).map_or_else(
189            || connecting == proxy,
190            |network| {
191                IpAddr::from_str(connecting)
192                    .is_ok_and(|addr| network.contains(canonical(addr)) || network.contains(addr))
193            },
194        )
195    })
196}
197
198/// `_forwarded_header_candidate_addr`.
199#[must_use]
200fn candidate_addr(candidate: &str) -> Option<IpAddr> {
201    IpAddr::from_str(candidate).ok()
202}
203
204/// `_forwarded_header_right_side_unlisted_count`.
205#[must_use]
206fn right_side_unlisted_count(
207    ips: &[String],
208    proxy_depth: u32,
209    trusted_proxies: &[String],
210) -> usize {
211    let depth = usize::try_from(proxy_depth).unwrap_or(ips.len());
212    let start = ips.len().saturating_sub(depth.saturating_sub(1));
213    ips[start..]
214        .iter()
215        .filter(|entry| !is_trusted_proxy(&canonicalize_ip(entry), trusted_proxies))
216        .count()
217}
218
219/// `_resolve_forwarded_chain_right_to_left`: the first untrusted entry
220/// from the right, `None` when only trusted entries remain or a candidate
221/// is unparseable or carries a metacharacter.
222#[must_use]
223fn resolve_chain_right_to_left(ips: &[String], trusted_proxies: &[String]) -> Option<String> {
224    ips.iter()
225        .rev()
226        .find(|entry| !is_trusted_proxy(&canonicalize_ip(entry), trusted_proxies))
227        .and_then(|entry| {
228            // Metachar first: a metachar candidate never parses as an
229            // address, so the reference's parse-then-metachar pair has
230            // this exact truth table (the reference keeps the order only
231            // for readability).
232            if candidate_has_metachar(entry) {
233                return None;
234            }
235            candidate_addr(entry).map(|_| canonicalize_ip(entry))
236        })
237}
238
239/// `_resolve_client_ip_from_forwarded_chain`: the depth-limited walk with
240/// the warnings as data. Failures and short chains degrade to the
241/// connecting identity, never to a header claim.
242#[must_use]
243pub fn resolve_client_ip_from_forwarded_chain(
244    canonical_connecting: &str,
245    forwarded_for: Option<&str>,
246    proxy_depth: u32,
247    trusted_proxies: &[String],
248) -> ClientIpExtraction {
249    let mut warnings = ChainWarnings::default();
250    let Some(forwarded_for) = forwarded_for.filter(|value| !value.is_empty()) else {
251        return ClientIpExtraction {
252            client_ip: canonical_connecting.to_owned(),
253            source: ClientIpSource::Connecting,
254            untrusted_forwarded_header: false,
255            warnings,
256        };
257    };
258
259    let ips = forwarded_header_ips(forwarded_for);
260    let chain_length = ips.len();
261
262    let depth = usize::try_from(proxy_depth).unwrap_or(0);
263    if depth == 0 || chain_length < depth {
264        warnings.chain_too_short = true;
265        return ClientIpExtraction {
266            client_ip: canonical_connecting.to_owned(),
267            source: ClientIpSource::Connecting,
268            untrusted_forwarded_header: false,
269            warnings,
270        };
271    }
272
273    if !trusted_proxies.is_empty() {
274        let unlisted = right_side_unlisted_count(&ips, proxy_depth, trusted_proxies);
275        if unlisted > 0 {
276            warnings.depth_overcounts = Some(unlisted);
277            let Some(resolved) = resolve_chain_right_to_left(&ips, trusted_proxies) else {
278                return ClientIpExtraction {
279                    client_ip: canonical_connecting.to_owned(),
280                    source: ClientIpSource::Connecting,
281                    untrusted_forwarded_header: false,
282                    warnings,
283                };
284            };
285            return ClientIpExtraction {
286                client_ip: resolved,
287                source: ClientIpSource::ForwardedChain,
288                untrusted_forwarded_header: false,
289                warnings,
290            };
291        }
292    }
293
294    // The depth gate above guarantees an entry at `depth` from the right.
295    let candidate = ips[chain_length - depth].as_str();
296    // Metachar first: a metachar candidate never parses as an address, so
297    // the reference's parse-then-metachar pair has this exact truth table.
298    if candidate_has_metachar(candidate) || candidate_addr(candidate).is_none() {
299        return ClientIpExtraction {
300            client_ip: canonical_connecting.to_owned(),
301            source: ClientIpSource::Connecting,
302            untrusted_forwarded_header: false,
303            warnings,
304        };
305    }
306
307    let client_ip = canonicalize_ip(candidate);
308    warnings.selected_entry_trusted = is_trusted_proxy(&client_ip, trusted_proxies);
309    ClientIpExtraction {
310        client_ip,
311        source: ClientIpSource::ForwardedChain,
312        untrusted_forwarded_header: false,
313        warnings,
314    }
315}
316
317/// `extract_client_ip`: the reference entry point. `client_host` is the
318/// connecting peer (`None` for a socket connection); `forwarded_for` the
319/// raw `X-Forwarded-For` header.
320#[must_use]
321pub fn extract_client_ip(
322    client_host: Option<&str>,
323    forwarded_for: Option<&str>,
324    trusted_proxies: &[String],
325    proxy_depth: u32,
326) -> ClientIpExtraction {
327    let Some(client_host) = client_host else {
328        // The socket arm: with `unix` on the proxy list the chain is still
329        // walked, from the unknown identity (the reference `_handle` arm).
330        if trusted_proxies.iter().any(|proxy| proxy == "unix") {
331            let mut extraction = resolve_client_ip_from_forwarded_chain(
332                UNKNOWN_CLIENT_IDENTITY,
333                forwarded_for,
334                proxy_depth,
335                trusted_proxies,
336            );
337            if extraction.source == ClientIpSource::Connecting {
338                UNKNOWN_CLIENT_IDENTITY.clone_into(&mut extraction.client_ip);
339                extraction.source = ClientIpSource::Unknown;
340            }
341            return extraction;
342        }
343        return ClientIpExtraction {
344            client_ip: UNKNOWN_CLIENT_IDENTITY.to_owned(),
345            source: ClientIpSource::Unknown,
346            untrusted_forwarded_header: false,
347            warnings: ChainWarnings::default(),
348        };
349    };
350
351    let canonical_connecting = canonicalize_ip(client_host);
352
353    if trusted_proxies.is_empty() {
354        // The preempted-header warning is the caller's (it rides the flag).
355        return ClientIpExtraction {
356            client_ip: canonical_connecting,
357            source: ClientIpSource::Connecting,
358            untrusted_forwarded_header: forwarded_for.is_some_and(|value| !value.is_empty()),
359            warnings: ChainWarnings::default(),
360        };
361    }
362
363    if !is_trusted_proxy(&canonical_connecting, trusted_proxies) {
364        return ClientIpExtraction {
365            client_ip: canonical_connecting,
366            source: ClientIpSource::Connecting,
367            untrusted_forwarded_header: forwarded_for.is_some_and(|value| !value.is_empty()),
368            warnings: ChainWarnings::default(),
369        };
370    }
371
372    resolve_client_ip_from_forwarded_chain(
373        &canonical_connecting,
374        forwarded_for,
375        proxy_depth,
376        trusted_proxies,
377    )
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    fn proxies(entries: &[&str]) -> Vec<String> {
385        entries.iter().map(ToString::to_string).collect()
386    }
387
388    #[test]
389    fn the_depth_selects_the_entry_from_the_right() {
390        // One appending proxy (depth 1): the client is the last entry.
391        let extraction = extract_client_ip(
392            Some("10.0.0.1"),
393            Some("203.0.113.7"),
394            &proxies(&["10.0.0.0/8"]),
395            1,
396        );
397        assert_eq!(extraction.client_ip, "203.0.113.7");
398        assert_eq!(extraction.source, ClientIpSource::ForwardedChain);
399        assert!(!extraction.warnings.selected_entry_trusted);
400
401        // Two appending proxies (depth 3, both hops trusted): the client
402        // is third from the right.
403        let extraction = extract_client_ip(
404            Some("10.0.0.1"),
405            Some("203.0.113.7, 198.51.100.4, 10.0.0.1"),
406            &proxies(&["10.0.0.0/8", "198.51.100.0/24"]),
407            3,
408        );
409        assert_eq!(extraction.client_ip, "203.0.113.7");
410
411        // A depth that lands on a trusted entry still answers it, with
412        // the over-declared-depth warning (the reference warns and
413        // returns the selected entry).
414        let extraction = extract_client_ip(
415            Some("10.0.0.1"),
416            Some("203.0.113.7, 10.0.0.1"),
417            &proxies(&["10.0.0.0/8"]),
418            1,
419        );
420        assert_eq!(extraction.client_ip, "10.0.0.1");
421        assert!(extraction.warnings.selected_entry_trusted);
422    }
423
424    #[test]
425    fn a_short_chain_falls_back_to_the_connecting_peer() {
426        let extraction = extract_client_ip(
427            Some("10.0.0.1"),
428            Some("203.0.113.7"),
429            &proxies(&["10.0.0.0/8"]),
430            3,
431        );
432        assert_eq!(extraction.client_ip, "10.0.0.1");
433        assert_eq!(extraction.source, ClientIpSource::Connecting);
434        assert!(extraction.warnings.chain_too_short);
435    }
436
437    #[test]
438    fn an_over_counting_depth_walks_right_to_left() {
439        // The declared depth counts a hop the trusted list does not
440        // cover: an entry to the right of the selection is unlisted, the
441        // walk skips trusted entries and answers the first untrusted one.
442        let extraction = extract_client_ip(
443            Some("10.0.0.1"),
444            Some("203.0.113.7, 198.51.100.4, 10.0.0.1"),
445            &proxies(&["10.0.0.0/8"]),
446            3,
447        );
448        assert_eq!(extraction.client_ip, "198.51.100.4");
449        assert_eq!(extraction.warnings.depth_overcounts, Some(1));
450
451        // A depth whose right side is fully trusted never walks.
452        let extraction = extract_client_ip(
453            Some("10.0.0.1"),
454            Some("10.0.0.2, 10.0.0.1"),
455            &proxies(&["10.0.0.0/8"]),
456            1,
457        );
458        assert_eq!(extraction.client_ip, "10.0.0.1");
459        assert_eq!(extraction.source, ClientIpSource::ForwardedChain);
460        assert_eq!(extraction.warnings.depth_overcounts, None);
461    }
462
463    #[test]
464    fn unparseable_and_metachar_candidates_degrade_to_connecting() {
465        // The depth-selected entry is unparseable: connecting wins.
466        let extraction = extract_client_ip(
467            Some("10.0.0.1"),
468            Some("not-an-ip"),
469            &proxies(&["10.0.0.0/8"]),
470            1,
471        );
472        assert_eq!(extraction.client_ip, "10.0.0.1");
473
474        // The depth-selected entry carries a metacharacter: connecting.
475        let extraction = extract_client_ip(
476            Some("10.0.0.1"),
477            Some("*.evil.example"),
478            &proxies(&["10.0.0.0/8"]),
479            1,
480        );
481        assert_eq!(extraction.client_ip, "10.0.0.1");
482
483        // The right-to-left walk refuses a metachar candidate too: the
484        // rightmost untrusted entry is malformed, so the walk has no
485        // answer and the connecting identity stands.
486        let extraction = extract_client_ip(
487            Some("10.0.0.1"),
488            Some("203.0.113.7, *.evil.example, 10.0.0.1"),
489            &proxies(&["10.0.0.0/8"]),
490            3,
491        );
492        assert_eq!(extraction.client_ip, "10.0.0.1");
493        assert_eq!(extraction.warnings.depth_overcounts, Some(1));
494    }
495
496    #[test]
497    fn a_selected_trusted_entry_warns() {
498        let extraction = extract_client_ip(
499            Some("10.0.0.1"),
500            Some("10.0.0.9, 10.0.0.1"),
501            &proxies(&["10.0.0.0/8"]),
502            2,
503        );
504        assert_eq!(extraction.client_ip, "10.0.0.9");
505        assert!(extraction.warnings.selected_entry_trusted);
506    }
507
508    #[test]
509    fn untrusted_peers_and_absent_proxies_ignore_the_header() {
510        let extraction = extract_client_ip(
511            Some("192.0.2.9"),
512            Some("203.0.113.7"),
513            &proxies(&["10.0.0.0/8"]),
514            1,
515        );
516        assert_eq!(extraction.client_ip, "192.0.2.9");
517        assert_eq!(extraction.source, ClientIpSource::Connecting);
518        assert!(extraction.untrusted_forwarded_header);
519
520        let extraction = extract_client_ip(Some("192.0.2.9"), Some("203.0.113.7"), &[], 1);
521        assert_eq!(extraction.client_ip, "192.0.2.9");
522        assert!(extraction.untrusted_forwarded_header);
523
524        // No header at all: no spoof signal.
525        let extraction = extract_client_ip(Some("192.0.2.9"), None, &proxies(&["10.0.0.0/8"]), 1);
526        assert!(!extraction.untrusted_forwarded_header);
527    }
528
529    #[test]
530    fn canonicalization_strips_brackets_ports_and_v4_mapping() {
531        assert_eq!(
532            strip_forwarded_entry_port("[2001:db8::1]:443"),
533            "2001:db8::1"
534        );
535        assert_eq!(strip_forwarded_entry_port("2001:db8::1"), "2001:db8::1");
536        assert_eq!(
537            strip_forwarded_entry_port("203.0.113.7:8443"),
538            "203.0.113.7"
539        );
540        assert_eq!(strip_forwarded_entry_port("203.0.113.7"), "203.0.113.7");
541        // A bare v6 with one colon-free host:port shape stays intact.
542        assert_eq!(strip_forwarded_entry_port("2001:db8::1"), "2001:db8::1");
543        assert_eq!(canonicalize_ip("[2001:db8::1]"), "2001:db8::1");
544        assert_eq!(canonicalize_ip("::ffff:203.0.113.7"), "203.0.113.7");
545        assert_eq!(canonicalize_ip("not-an-ip"), "not-an-ip");
546    }
547
548    #[test]
549    fn the_unknown_arm_walks_only_for_unix_proxies() {
550        let extraction = extract_client_ip(None, Some("203.0.113.7"), &proxies(&["unix"]), 1);
551        assert_eq!(extraction.client_ip, "203.0.113.7");
552        assert_eq!(extraction.source, ClientIpSource::ForwardedChain);
553
554        let extraction = extract_client_ip(None, Some("203.0.113.7"), &[], 1);
555        assert_eq!(extraction.client_ip, UNKNOWN_CLIENT_IDENTITY);
556        assert_eq!(extraction.source, ClientIpSource::Unknown);
557
558        // The unix walk degrading to connecting reports the unknown identity.
559        let extraction = extract_client_ip(None, None, &proxies(&["unix"]), 1);
560        assert_eq!(extraction.client_ip, UNKNOWN_CLIENT_IDENTITY);
561        assert_eq!(extraction.source, ClientIpSource::Unknown);
562    }
563
564    #[test]
565    fn malformed_bracket_entries_pass_through_untouched() {
566        // An unterminated bracket form is not an address.
567        assert_eq!(strip_forwarded_entry_port("[2001:db8::1"), "[2001:db8::1");
568        // A bracket form with trailing junk past the closing bracket is
569        // not `host]:port` either.
570        assert_eq!(
571            strip_forwarded_entry_port("[2001:db8::1]junk"),
572            "[2001:db8::1]junk"
573        );
574        // The clean forms still strip.
575        assert_eq!(strip_forwarded_entry_port("[2001:db8::1]"), "2001:db8::1");
576        assert_eq!(
577            strip_forwarded_entry_port("[2001:db8::1]:443"),
578            "2001:db8::1"
579        );
580    }
581
582    #[test]
583    fn an_empty_trusted_list_still_walks_at_the_declared_depth() {
584        // Direct resolve with no proxy list: the depth arm answers the
585        // entry (the caller decided trust before calling).
586        let extraction =
587            resolve_client_ip_from_forwarded_chain("10.0.0.1", Some("203.0.113.7"), 1, &[]);
588        assert_eq!(extraction.client_ip, "203.0.113.7");
589        assert_eq!(extraction.source, ClientIpSource::ForwardedChain);
590        assert_eq!(extraction.warnings, ChainWarnings::default());
591    }
592
593    #[test]
594    fn an_unparseable_rightmost_untrusted_entry_ends_the_walk() {
595        // The rightmost untrusted entry does not parse: the walk has no
596        // answer and the connecting identity stands.
597        let extraction = extract_client_ip(
598            Some("10.0.0.1"),
599            Some("203.0.113.7, not-an-ip, 10.0.0.1"),
600            &proxies(&["10.0.0.0/8"]),
601            3,
602        );
603        assert_eq!(extraction.client_ip, "10.0.0.1");
604        assert_eq!(extraction.warnings.depth_overcounts, Some(1));
605    }
606
607    #[test]
608    fn the_unix_token_never_matches_a_real_peer() {
609        assert!(!is_trusted_proxy("127.0.0.1", &proxies(&["unix"])));
610        assert!(is_trusted_proxy(
611            "10.1.2.3",
612            &proxies(&["unix", "10.0.0.0/8"])
613        ));
614        assert!(is_trusted_proxy("10.1.2.3", &proxies(&["10.1.2.3"])));
615    }
616}