halldyll_core/security/
ipblock.rs

1//! IP Block - Blocking private/local IPs
2
3use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4use url::Url;
5
6/// Private IP blocker
7pub struct IpBlocker {
8    /// Block private IPs
9    block_private: bool,
10    /// Block loopback IPs
11    block_loopback: bool,
12    /// Block link-local IPs
13    block_link_local: bool,
14    /// Block multicast IPs
15    block_multicast: bool,
16}
17
18impl Default for IpBlocker {
19    fn default() -> Self {
20        Self {
21            block_private: true,
22            block_loopback: true,
23            block_link_local: true,
24            block_multicast: true,
25        }
26    }
27}
28
29impl IpBlocker {
30    /// New blocker
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Configure blocking
36    pub fn configure(
37        block_private: bool,
38        block_loopback: bool,
39        block_link_local: bool,
40        block_multicast: bool,
41    ) -> Self {
42        Self {
43            block_private,
44            block_loopback,
45            block_link_local,
46            block_multicast,
47        }
48    }
49
50    /// Check if an IP is blocked
51    pub fn is_blocked(&self, ip: &IpAddr) -> bool {
52        match ip {
53            IpAddr::V4(ipv4) => self.is_blocked_ipv4(ipv4),
54            IpAddr::V6(ipv6) => self.is_blocked_ipv6(ipv6),
55        }
56    }
57
58    /// Check an IPv4
59    fn is_blocked_ipv4(&self, ip: &Ipv4Addr) -> bool {
60        // Loopback (127.0.0.0/8)
61        if self.block_loopback && ip.is_loopback() {
62            return true;
63        }
64
65        // Private ranges
66        if self.block_private && ip.is_private() {
67            return true;
68        }
69
70        // Link-local (169.254.0.0/16)
71        if self.block_link_local && ip.is_link_local() {
72            return true;
73        }
74
75        // Multicast (224.0.0.0/4)
76        if self.block_multicast && ip.is_multicast() {
77            return true;
78        }
79
80        // Broadcast
81        if ip.is_broadcast() {
82            return true;
83        }
84
85        // Unspecified (0.0.0.0)
86        if ip.is_unspecified() {
87            return true;
88        }
89
90        // Documentation ranges (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24)
91        if ip.is_documentation() {
92            return true;
93        }
94
95        false
96    }
97
98    /// Check an IPv6
99    fn is_blocked_ipv6(&self, ip: &Ipv6Addr) -> bool {
100        // Loopback (::1)
101        if self.block_loopback && ip.is_loopback() {
102            return true;
103        }
104
105        // Link-local (fe80::/10)
106        // Note: is_unicast_link_local is not stable, we check it manually
107        if self.block_link_local {
108            let segments = ip.segments();
109            if (segments[0] & 0xffc0) == 0xfe80 {
110                return true;
111            }
112        }
113
114        // Multicast (ff00::/8)
115        if self.block_multicast && ip.is_multicast() {
116            return true;
117        }
118
119        // Unspecified (::)
120        if ip.is_unspecified() {
121            return true;
122        }
123
124        // Unique local (fc00::/7) - IPv6 equivalent of private addresses
125        if self.block_private {
126            let segments = ip.segments();
127            if (segments[0] & 0xfe00) == 0xfc00 {
128                return true;
129            }
130        }
131
132        false
133    }
134
135    /// Check if a URL points to a blocked IP (by hostname)
136    pub fn is_url_hostname_blocked(&self, url: &Url) -> bool {
137        // Check if the host is a direct IP
138        if let Some(host) = url.host_str() {
139            // Try to parse as IP
140            if let Ok(ip) = host.parse::<IpAddr>() {
141                return self.is_blocked(&ip);
142            }
143
144            // Check local hostnames
145            let host_lower = host.to_lowercase();
146            if host_lower == "localhost" 
147                || host_lower == "localhost.localdomain"
148                || host_lower.ends_with(".local")
149                || host_lower.ends_with(".localhost")
150            {
151                return self.block_loopback;
152            }
153        }
154
155        false
156    }
157
158    /// Description of why an IP is blocked
159    pub fn block_reason(&self, ip: &IpAddr) -> Option<String> {
160        match ip {
161            IpAddr::V4(ipv4) => {
162                if self.block_loopback && ipv4.is_loopback() {
163                    return Some("loopback address".to_string());
164                }
165                if self.block_private && ipv4.is_private() {
166                    return Some("private address".to_string());
167                }
168                if self.block_link_local && ipv4.is_link_local() {
169                    return Some("link-local address".to_string());
170                }
171                if self.block_multicast && ipv4.is_multicast() {
172                    return Some("multicast address".to_string());
173                }
174                if ipv4.is_broadcast() {
175                    return Some("broadcast address".to_string());
176                }
177                if ipv4.is_unspecified() {
178                    return Some("unspecified address".to_string());
179                }
180            }
181            IpAddr::V6(ipv6) => {
182                if self.block_loopback && ipv6.is_loopback() {
183                    return Some("loopback address".to_string());
184                }
185                if self.block_multicast && ipv6.is_multicast() {
186                    return Some("multicast address".to_string());
187                }
188                if ipv6.is_unspecified() {
189                    return Some("unspecified address".to_string());
190                }
191            }
192        }
193        None
194    }
195}