Skip to main content

rustauth_core/utils/
ip.rs

1use std::net::{IpAddr, Ipv6Addr};
2
3/// IPv6 subnet prefix used for normalization.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Ipv6Subnet {
6    Prefix32,
7    Prefix48,
8    Prefix64,
9    Full,
10}
11
12impl Ipv6Subnet {
13    const fn bits(self) -> u8 {
14        match self {
15            Self::Prefix32 => 32,
16            Self::Prefix48 => 48,
17            Self::Prefix64 => 64,
18            Self::Full => 128,
19        }
20    }
21}
22
23/// Options for IP normalization.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct NormalizeIpOptions {
26    pub ipv6_subnet: Ipv6Subnet,
27}
28
29impl Default for NormalizeIpOptions {
30    fn default() -> Self {
31        Self {
32            ipv6_subnet: Ipv6Subnet::Prefix64,
33        }
34    }
35}
36
37/// Returns true when the value is a valid IPv4 or IPv6 literal.
38pub fn is_valid_ip(ip: &str) -> bool {
39    ip.parse::<IpAddr>().is_ok()
40}
41
42/// Normalize an IP address for rate limiting using RustAuth defaults.
43pub fn normalize_ip(ip: &str) -> String {
44    normalize_ip_with_options(ip, NormalizeIpOptions::default())
45}
46
47/// Normalize an IP address for rate limiting.
48pub fn normalize_ip_with_options(ip: &str, options: NormalizeIpOptions) -> String {
49    match ip.parse::<IpAddr>() {
50        Ok(IpAddr::V4(ip)) => ip.to_string(),
51        Ok(IpAddr::V6(ip)) => normalize_ipv6(ip, options.ipv6_subnet),
52        Err(_) => ip.to_ascii_lowercase(),
53    }
54}
55
56/// Create a rate limit key from a normalized IP and request path.
57pub fn create_rate_limit_key(ip: &str, path: &str) -> String {
58    format!("{ip}|{path}")
59}
60
61/// Create a rate limit key with an additional opaque scope segment.
62///
63/// The suffix must not contain raw secrets (for example a challenge cookie value);
64/// callers should pass a keyed digest such as [`hash_rate_limit_scope`].
65pub fn create_rate_limit_key_with_suffix(ip: &str, path: &str, suffix: &str) -> String {
66    format!("{}|{}", create_rate_limit_key(ip, path), suffix)
67}
68
69fn normalize_ipv6(ip: Ipv6Addr, subnet: Ipv6Subnet) -> String {
70    if let Some(mapped) = ip.to_ipv4_mapped() {
71        return mapped.to_string();
72    }
73
74    format_ipv6_segments(mask_ipv6_segments(ip.segments(), subnet.bits()))
75}
76
77fn mask_ipv6_segments(mut segments: [u16; 8], prefix_bits: u8) -> [u16; 8] {
78    let mut bits_remaining = prefix_bits;
79
80    for segment in &mut segments {
81        if bits_remaining >= 16 {
82            bits_remaining -= 16;
83            continue;
84        }
85
86        if bits_remaining == 0 {
87            *segment = 0;
88            continue;
89        }
90
91        let mask = u16::MAX << (16 - bits_remaining);
92        *segment &= mask;
93        bits_remaining = 0;
94    }
95
96    segments
97}
98
99fn format_ipv6_segments(segments: [u16; 8]) -> String {
100    segments
101        .iter()
102        .map(|segment| format!("{segment:04x}"))
103        .collect::<Vec<_>>()
104        .join(":")
105}