1use std::net::IpAddr;
54use std::str::FromStr;
55
56use crate::ip_gate::IpNet;
57
58pub const UNKNOWN_CLIENT_IDENTITY: &str = "unknown";
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum ClientIpSource {
66 Connecting,
68 ForwardedChain,
70 Unknown,
73}
74
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
78pub struct ChainWarnings {
79 pub chain_too_short: bool,
82 pub depth_overcounts: Option<usize>,
86 pub selected_entry_trusted: bool,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ClientIpExtraction {
95 pub client_ip: String,
98 pub source: ClientIpSource,
100 pub untrusted_forwarded_header: bool,
104 pub warnings: ChainWarnings,
107}
108
109#[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#[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
147fn parse_ip_literal(value: &str) -> Option<(IpAddr, Option<&str>)> {
151 match value.split_once('%') {
152 Some((address, zone)) if !zone.is_empty() && address.contains(':') => {
153 let addr = IpAddr::from_str(address).ok()?;
154 addr.is_ipv6().then_some((addr, Some(zone)))
155 }
156 Some(_) => None,
157 None => IpAddr::from_str(value).ok().map(|addr| (addr, None)),
158 }
159}
160
161fn literal_text(addr: IpAddr, zone: Option<&str>) -> String {
165 zone.map_or_else(|| canonical_ip_text(addr), |zone| format!("{addr}%{zone}"))
166}
167
168#[must_use]
171pub fn canonicalize_ip(value: &str) -> String {
172 parse_ip_literal(strip_ip_brackets(value))
173 .map_or_else(|| value.to_owned(), |(addr, zone)| literal_text(addr, zone))
174}
175
176#[must_use]
178pub fn canonical_ip_text(addr: IpAddr) -> String {
179 match addr {
180 IpAddr::V6(v6) => v6
181 .to_ipv4_mapped()
182 .map_or_else(|| v6.to_string(), |mapped| mapped.to_string()),
183 IpAddr::V4(v4) => v4.to_string(),
184 }
185}
186
187#[must_use]
189pub fn candidate_has_metachar(candidate: &str) -> bool {
190 candidate
191 .chars()
192 .any(|c| matches!(c, '*' | '?' | '[' | ']' | '\\'))
193}
194
195#[must_use]
197pub fn forwarded_header_ips(forwarded_for: &str) -> Vec<String> {
198 forwarded_for
199 .split(',')
200 .map(|entry| strip_forwarded_entry_port(entry.trim()).to_owned())
201 .collect()
202}
203
204#[must_use]
211pub fn is_trusted_proxy(connecting: &str, trusted_proxies: &[String]) -> bool {
212 let Some((addr, _zone)) = parse_ip_literal(connecting) else {
213 return false;
214 };
215 trusted_proxies.iter().any(|proxy| {
216 if proxy.contains('/') {
217 IpNet::parse(proxy).is_some_and(|network| network.contains(addr))
218 } else {
219 connecting == proxy
220 }
221 })
222}
223
224#[must_use]
226fn candidate_addr(candidate: &str) -> Option<(IpAddr, Option<&str>)> {
227 parse_ip_literal(candidate)
228}
229
230#[must_use]
232fn right_side_unlisted_count(
233 ips: &[String],
234 proxy_depth: u32,
235 trusted_proxies: &[String],
236) -> usize {
237 let depth = usize::try_from(proxy_depth).unwrap_or(ips.len());
238 let start = ips.len().saturating_sub(depth.saturating_sub(1));
239 ips[start..]
240 .iter()
241 .filter(|entry| !is_trusted_proxy(&canonicalize_ip(entry), trusted_proxies))
242 .count()
243}
244
245#[must_use]
249fn resolve_chain_right_to_left(ips: &[String], trusted_proxies: &[String]) -> Option<String> {
250 ips.iter()
251 .rev()
252 .find(|entry| !is_trusted_proxy(&canonicalize_ip(entry), trusted_proxies))
253 .and_then(|entry| {
254 if candidate_has_metachar(entry) {
259 return None;
260 }
261 candidate_addr(entry).map(|_| canonicalize_ip(entry))
262 })
263}
264
265#[must_use]
269pub fn resolve_client_ip_from_forwarded_chain(
270 canonical_connecting: &str,
271 forwarded_for: Option<&str>,
272 proxy_depth: u32,
273 trusted_proxies: &[String],
274) -> ClientIpExtraction {
275 let mut warnings = ChainWarnings::default();
276 let Some(forwarded_for) = forwarded_for.filter(|value| !value.is_empty()) else {
277 return ClientIpExtraction {
278 client_ip: canonical_connecting.to_owned(),
279 source: ClientIpSource::Connecting,
280 untrusted_forwarded_header: false,
281 warnings,
282 };
283 };
284
285 let ips = forwarded_header_ips(forwarded_for);
286 let chain_length = ips.len();
287
288 let depth = usize::try_from(proxy_depth).unwrap_or(0);
289 if depth == 0 || chain_length < depth {
290 warnings.chain_too_short = true;
291 return ClientIpExtraction {
292 client_ip: canonical_connecting.to_owned(),
293 source: ClientIpSource::Connecting,
294 untrusted_forwarded_header: false,
295 warnings,
296 };
297 }
298
299 if !trusted_proxies.is_empty() {
300 let unlisted = right_side_unlisted_count(&ips, proxy_depth, trusted_proxies);
301 if unlisted > 0 {
302 warnings.depth_overcounts = Some(unlisted);
303 let Some(resolved) = resolve_chain_right_to_left(&ips, trusted_proxies) else {
304 return ClientIpExtraction {
305 client_ip: canonical_connecting.to_owned(),
306 source: ClientIpSource::Connecting,
307 untrusted_forwarded_header: false,
308 warnings,
309 };
310 };
311 return ClientIpExtraction {
312 client_ip: resolved,
313 source: ClientIpSource::ForwardedChain,
314 untrusted_forwarded_header: false,
315 warnings,
316 };
317 }
318 }
319
320 let candidate = ips[chain_length - depth].as_str();
322 if candidate_has_metachar(candidate) || candidate_addr(candidate).is_none() {
325 return ClientIpExtraction {
326 client_ip: canonical_connecting.to_owned(),
327 source: ClientIpSource::Connecting,
328 untrusted_forwarded_header: false,
329 warnings,
330 };
331 }
332
333 let client_ip = canonicalize_ip(candidate);
334 warnings.selected_entry_trusted = is_trusted_proxy(&client_ip, trusted_proxies);
335 ClientIpExtraction {
336 client_ip,
337 source: ClientIpSource::ForwardedChain,
338 untrusted_forwarded_header: false,
339 warnings,
340 }
341}
342
343#[must_use]
347pub fn extract_client_ip(
348 client_host: Option<&str>,
349 forwarded_for: Option<&str>,
350 trusted_proxies: &[String],
351 proxy_depth: u32,
352) -> ClientIpExtraction {
353 let Some(client_host) = client_host.filter(|host| !host.is_empty()) else {
355 if trusted_proxies.iter().any(|proxy| proxy == "unix") {
358 let mut extraction = resolve_client_ip_from_forwarded_chain(
359 UNKNOWN_CLIENT_IDENTITY,
360 forwarded_for,
361 proxy_depth,
362 trusted_proxies,
363 );
364 if extraction.source == ClientIpSource::Connecting {
365 UNKNOWN_CLIENT_IDENTITY.clone_into(&mut extraction.client_ip);
366 extraction.source = ClientIpSource::Unknown;
367 }
368 return extraction;
369 }
370 return ClientIpExtraction {
371 client_ip: UNKNOWN_CLIENT_IDENTITY.to_owned(),
372 source: ClientIpSource::Unknown,
373 untrusted_forwarded_header: false,
374 warnings: ChainWarnings::default(),
375 };
376 };
377
378 let canonical_connecting = canonicalize_ip(client_host);
379
380 if trusted_proxies.is_empty() {
381 return ClientIpExtraction {
383 client_ip: canonical_connecting,
384 source: ClientIpSource::Connecting,
385 untrusted_forwarded_header: forwarded_for.is_some_and(|value| !value.is_empty()),
386 warnings: ChainWarnings::default(),
387 };
388 }
389
390 if !is_trusted_proxy(client_host, trusted_proxies) {
391 return ClientIpExtraction {
392 client_ip: canonical_connecting,
393 source: ClientIpSource::Connecting,
394 untrusted_forwarded_header: forwarded_for.is_some_and(|value| !value.is_empty()),
395 warnings: ChainWarnings::default(),
396 };
397 }
398
399 resolve_client_ip_from_forwarded_chain(
400 &canonical_connecting,
401 forwarded_for,
402 proxy_depth,
403 trusted_proxies,
404 )
405}
406
407pub const UNRESOLVABLE_CLIENT_ADDRESS_STATUS: u16 = 403;
410
411pub const UNRESOLVABLE_CLIENT_ADDRESS_MESSAGE: &str = "Client address could not be determined";
413
414pub const UNRESOLVABLE_CLIENT_ADDRESS_CHECK_NAME: &str = "client_address_unresolved";
417
418#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct ClientResolution {
422 pub extraction: ClientIpExtraction,
424 pub ip: Option<IpAddr>,
427 pub reject: bool,
431}
432
433#[must_use]
459pub fn resolve_client(
460 peer: Option<IpAddr>,
461 forwarded_for: Option<&str>,
462 trusted_proxies: &[String],
463 proxy_depth: u32,
464 fail_secure: bool,
465) -> ClientResolution {
466 let peer_text = peer.map(|addr| addr.to_string());
467 let extraction = extract_client_ip(
468 peer_text.as_deref(),
469 forwarded_for,
470 trusted_proxies,
471 proxy_depth,
472 );
473 let unknown = extraction.client_ip == UNKNOWN_CLIENT_IDENTITY;
474 let ip = if unknown {
475 None
476 } else {
477 parse_ip_literal(&extraction.client_ip)
479 .and_then(|(addr, zone)| zone.is_none().then_some(addr))
480 };
481 ClientResolution {
482 reject: peer.is_none() && unknown && fail_secure,
483 extraction,
484 ip,
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491
492 fn proxies(entries: &[&str]) -> Vec<String> {
493 entries.iter().map(ToString::to_string).collect()
494 }
495
496 #[test]
497 fn the_depth_selects_the_entry_from_the_right() {
498 let extraction = extract_client_ip(
500 Some("10.0.0.1"),
501 Some("203.0.113.7"),
502 &proxies(&["10.0.0.0/8"]),
503 1,
504 );
505 assert_eq!(extraction.client_ip, "203.0.113.7");
506 assert_eq!(extraction.source, ClientIpSource::ForwardedChain);
507 assert!(!extraction.warnings.selected_entry_trusted);
508
509 let extraction = extract_client_ip(
512 Some("10.0.0.1"),
513 Some("203.0.113.7, 198.51.100.4, 10.0.0.1"),
514 &proxies(&["10.0.0.0/8", "198.51.100.0/24"]),
515 3,
516 );
517 assert_eq!(extraction.client_ip, "203.0.113.7");
518
519 let extraction = extract_client_ip(
523 Some("10.0.0.1"),
524 Some("203.0.113.7, 10.0.0.1"),
525 &proxies(&["10.0.0.0/8"]),
526 1,
527 );
528 assert_eq!(extraction.client_ip, "10.0.0.1");
529 assert!(extraction.warnings.selected_entry_trusted);
530 }
531
532 #[test]
533 fn a_short_chain_falls_back_to_the_connecting_peer() {
534 let extraction = extract_client_ip(
535 Some("10.0.0.1"),
536 Some("203.0.113.7"),
537 &proxies(&["10.0.0.0/8"]),
538 3,
539 );
540 assert_eq!(extraction.client_ip, "10.0.0.1");
541 assert_eq!(extraction.source, ClientIpSource::Connecting);
542 assert!(extraction.warnings.chain_too_short);
543 }
544
545 #[test]
546 fn an_over_counting_depth_walks_right_to_left() {
547 let extraction = extract_client_ip(
551 Some("10.0.0.1"),
552 Some("203.0.113.7, 198.51.100.4, 10.0.0.1"),
553 &proxies(&["10.0.0.0/8"]),
554 3,
555 );
556 assert_eq!(extraction.client_ip, "198.51.100.4");
557 assert_eq!(extraction.warnings.depth_overcounts, Some(1));
558
559 let extraction = extract_client_ip(
561 Some("10.0.0.1"),
562 Some("10.0.0.2, 10.0.0.1"),
563 &proxies(&["10.0.0.0/8"]),
564 1,
565 );
566 assert_eq!(extraction.client_ip, "10.0.0.1");
567 assert_eq!(extraction.source, ClientIpSource::ForwardedChain);
568 assert_eq!(extraction.warnings.depth_overcounts, None);
569 }
570
571 #[test]
572 fn unparseable_and_metachar_candidates_degrade_to_connecting() {
573 let extraction = extract_client_ip(
575 Some("10.0.0.1"),
576 Some("not-an-ip"),
577 &proxies(&["10.0.0.0/8"]),
578 1,
579 );
580 assert_eq!(extraction.client_ip, "10.0.0.1");
581
582 let extraction = extract_client_ip(
584 Some("10.0.0.1"),
585 Some("*.evil.example"),
586 &proxies(&["10.0.0.0/8"]),
587 1,
588 );
589 assert_eq!(extraction.client_ip, "10.0.0.1");
590
591 let extraction = extract_client_ip(
595 Some("10.0.0.1"),
596 Some("203.0.113.7, *.evil.example, 10.0.0.1"),
597 &proxies(&["10.0.0.0/8"]),
598 3,
599 );
600 assert_eq!(extraction.client_ip, "10.0.0.1");
601 assert_eq!(extraction.warnings.depth_overcounts, Some(1));
602 }
603
604 #[test]
605 fn a_selected_trusted_entry_warns() {
606 let extraction = extract_client_ip(
607 Some("10.0.0.1"),
608 Some("10.0.0.9, 10.0.0.1"),
609 &proxies(&["10.0.0.0/8"]),
610 2,
611 );
612 assert_eq!(extraction.client_ip, "10.0.0.9");
613 assert!(extraction.warnings.selected_entry_trusted);
614 }
615
616 #[test]
617 fn untrusted_peers_and_absent_proxies_ignore_the_header() {
618 let extraction = extract_client_ip(
619 Some("192.0.2.9"),
620 Some("203.0.113.7"),
621 &proxies(&["10.0.0.0/8"]),
622 1,
623 );
624 assert_eq!(extraction.client_ip, "192.0.2.9");
625 assert_eq!(extraction.source, ClientIpSource::Connecting);
626 assert!(extraction.untrusted_forwarded_header);
627
628 let extraction = extract_client_ip(Some("192.0.2.9"), Some("203.0.113.7"), &[], 1);
629 assert_eq!(extraction.client_ip, "192.0.2.9");
630 assert!(extraction.untrusted_forwarded_header);
631
632 let extraction = extract_client_ip(Some("192.0.2.9"), None, &proxies(&["10.0.0.0/8"]), 1);
634 assert!(!extraction.untrusted_forwarded_header);
635 }
636
637 #[test]
638 fn canonicalization_strips_brackets_ports_and_v4_mapping() {
639 assert_eq!(
640 strip_forwarded_entry_port("[2001:db8::1]:443"),
641 "2001:db8::1"
642 );
643 assert_eq!(strip_forwarded_entry_port("2001:db8::1"), "2001:db8::1");
644 assert_eq!(
645 strip_forwarded_entry_port("203.0.113.7:8443"),
646 "203.0.113.7"
647 );
648 assert_eq!(strip_forwarded_entry_port("203.0.113.7"), "203.0.113.7");
649 assert_eq!(strip_forwarded_entry_port("2001:db8::1"), "2001:db8::1");
651 assert_eq!(canonicalize_ip("[2001:db8::1]"), "2001:db8::1");
652 assert_eq!(canonicalize_ip("::ffff:203.0.113.7"), "203.0.113.7");
653 assert_eq!(canonicalize_ip("not-an-ip"), "not-an-ip");
654 }
655
656 #[test]
657 fn the_unknown_arm_walks_only_for_unix_proxies() {
658 let extraction = extract_client_ip(None, Some("203.0.113.7"), &proxies(&["unix"]), 1);
659 assert_eq!(extraction.client_ip, "203.0.113.7");
660 assert_eq!(extraction.source, ClientIpSource::ForwardedChain);
661
662 let extraction = extract_client_ip(None, Some("203.0.113.7"), &[], 1);
663 assert_eq!(extraction.client_ip, UNKNOWN_CLIENT_IDENTITY);
664 assert_eq!(extraction.source, ClientIpSource::Unknown);
665
666 let extraction = extract_client_ip(None, None, &proxies(&["unix"]), 1);
668 assert_eq!(extraction.client_ip, UNKNOWN_CLIENT_IDENTITY);
669 assert_eq!(extraction.source, ClientIpSource::Unknown);
670 }
671
672 #[test]
673 fn malformed_bracket_entries_pass_through_untouched() {
674 assert_eq!(strip_forwarded_entry_port("[2001:db8::1"), "[2001:db8::1");
676 assert_eq!(
679 strip_forwarded_entry_port("[2001:db8::1]junk"),
680 "[2001:db8::1]junk"
681 );
682 assert_eq!(strip_forwarded_entry_port("[2001:db8::1]"), "2001:db8::1");
684 assert_eq!(
685 strip_forwarded_entry_port("[2001:db8::1]:443"),
686 "2001:db8::1"
687 );
688 }
689
690 #[test]
691 fn an_empty_trusted_list_still_walks_at_the_declared_depth() {
692 let extraction =
695 resolve_client_ip_from_forwarded_chain("10.0.0.1", Some("203.0.113.7"), 1, &[]);
696 assert_eq!(extraction.client_ip, "203.0.113.7");
697 assert_eq!(extraction.source, ClientIpSource::ForwardedChain);
698 assert_eq!(extraction.warnings, ChainWarnings::default());
699 }
700
701 #[test]
702 fn an_unparseable_rightmost_untrusted_entry_ends_the_walk() {
703 let extraction = extract_client_ip(
706 Some("10.0.0.1"),
707 Some("203.0.113.7, not-an-ip, 10.0.0.1"),
708 &proxies(&["10.0.0.0/8"]),
709 3,
710 );
711 assert_eq!(extraction.client_ip, "10.0.0.1");
712 assert_eq!(extraction.warnings.depth_overcounts, Some(1));
713 }
714
715 #[test]
716 fn the_unix_token_never_matches_a_real_peer() {
717 assert!(!is_trusted_proxy("127.0.0.1", &proxies(&["unix"])));
718 assert!(is_trusted_proxy(
719 "10.1.2.3",
720 &proxies(&["unix", "10.0.0.0/8"])
721 ));
722 assert!(is_trusted_proxy("10.1.2.3", &proxies(&["10.1.2.3"])));
723 }
724
725 #[allow(clippy::type_complexity)]
728 const REFERENCE: &[(Option<&str>, Option<&str>, &[&str], u32, &str)] = &[
729 (
730 Some("::ffff:10.0.0.1"),
731 Some("203.0.113.7"),
732 &["10.0.0.0/8"],
733 1,
734 "10.0.0.1",
735 ),
736 (
737 Some("::ffff:10.0.0.1"),
738 Some("203.0.113.7"),
739 &["10.0.0.1"],
740 1,
741 "10.0.0.1",
742 ),
743 (
744 Some("10.0.0.1"),
745 Some("203.0.113.7"),
746 &["10.0.0.0/8"],
747 1,
748 "203.0.113.7",
749 ),
750 (
751 Some("::FFFF:10.0.0.1"),
752 Some("203.0.113.7"),
753 &[],
754 1,
755 "10.0.0.1",
756 ),
757 (Some("FE80::1%eth0"), None, &[], 1, "fe80::1%eth0"),
758 (Some("fe80::1%eth0"), None, &[], 1, "fe80::1%eth0"),
759 (Some("2001:DB8::1"), None, &[], 1, "2001:db8::1"),
760 (
761 Some("10.0.0.1"),
762 Some("[2001:db8::1]:443"),
763 &["10.0.0.1"],
764 1,
765 "2001:db8::1",
766 ),
767 (None, Some("203.0.113.7"), &["unix"], 1, "203.0.113.7"),
768 (None, Some("203.0.113.7"), &[], 1, "unknown"),
769 (None, None, &["unix"], 1, "unknown"),
770 (Some(""), Some("203.0.113.7"), &["unix"], 1, "203.0.113.7"),
771 (Some("not-an-ip"), None, &[], 1, "not-an-ip"),
772 (
773 Some("10.0.0.1"),
774 Some("1.1.1.1, 203.0.113.7"),
775 &["10.0.0.1"],
776 2,
777 "203.0.113.7",
778 ),
779 (
780 Some("10.0.0.1"),
781 Some("203.0.113.7"),
782 &["10.0.0.1"],
783 3,
784 "10.0.0.1",
785 ),
786 (Some("::1"), Some("203.0.113.7"), &["::1"], 1, "203.0.113.7"),
787 (
788 Some("0:0:0:0:0:ffff:a00:1"),
789 Some("9.9.9.9"),
790 &["10.0.0.1"],
791 1,
792 "10.0.0.1",
793 ),
794 ];
795
796 #[test]
797 fn identities_follow_the_reference_table() {
798 for (index, (peer, forwarded, proxy_list, depth, expected)) in REFERENCE.iter().enumerate()
799 {
800 let extraction = extract_client_ip(*peer, *forwarded, &proxies(proxy_list), *depth);
801 assert_eq!(extraction.client_ip, *expected, "reference row {index}");
802 }
803 }
804
805 #[test]
806 fn a_peerless_request_is_rejected_only_when_unresolved_and_fail_secure() {
807 let rejected = resolve_client(None, Some("203.0.113.7"), &[], 1, true);
808 assert!(rejected.reject);
809 assert_eq!(rejected.ip, None);
810 assert_eq!(rejected.extraction.source, ClientIpSource::Unknown);
811
812 let open = resolve_client(None, Some("203.0.113.7"), &[], 1, false);
813 assert!(!open.reject);
814 assert_eq!(open.ip, None);
815
816 let unix = resolve_client(None, Some("203.0.113.7"), &proxies(&["unix"]), 1, true);
817 assert!(!unix.reject);
818 assert_eq!(unix.ip, Some("203.0.113.7".parse().unwrap()));
819 }
820
821 #[test]
822 fn a_peer_resolves_through_the_trusted_chain_and_never_rejects() {
823 let resolution = resolve_client(
824 Some("10.0.0.1".parse().unwrap()),
825 Some("203.0.113.7"),
826 &proxies(&["10.0.0.0/8"]),
827 1,
828 true,
829 );
830 assert!(!resolution.reject);
831 assert_eq!(resolution.ip, Some("203.0.113.7".parse().unwrap()));
832
833 let untrusted = resolve_client(
834 Some("192.0.2.9".parse().unwrap()),
835 Some("203.0.113.7"),
836 &proxies(&["10.0.0.0/8"]),
837 1,
838 true,
839 );
840 assert_eq!(untrusted.ip, Some("192.0.2.9".parse().unwrap()));
841 assert!(untrusted.extraction.untrusted_forwarded_header);
842
843 let mapped = resolve_client(
845 Some("::ffff:192.0.2.9".parse().unwrap()),
846 None,
847 &[],
848 1,
849 true,
850 );
851 assert_eq!(mapped.ip, Some("192.0.2.9".parse().unwrap()));
852 }
853
854 #[test]
855 fn a_zoned_chain_entry_keeps_its_text_but_has_no_address() {
856 let resolution = resolve_client(None, Some("fe80::1%eth0"), &proxies(&["unix"]), 1, true);
857 assert_eq!(resolution.extraction.client_ip, "fe80::1%eth0");
858 assert_eq!(resolution.ip, None);
859 assert!(!resolution.reject);
860 }
861
862 #[test]
863 fn a_malformed_zone_literal_is_not_an_address() {
864 for raw in ["1.2.3.4%eth0", "fe80::1%", "not:an:ip%eth0"] {
867 assert_eq!(canonicalize_ip(raw), raw);
868 assert!(!is_trusted_proxy(raw, &proxies(&["fe80::/10", raw])));
869 }
870 assert!(is_trusted_proxy("fe80::1%eth0", &proxies(&["fe80::/10"])));
872 }
873}