use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
#[must_use]
pub fn is_blocked_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => is_blocked_ipv4(v4),
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
return is_blocked_ipv4(v4);
}
is_blocked_ipv6(v6)
}
}
}
fn is_blocked_ipv4(v4: Ipv4Addr) -> bool {
v4.is_loopback() || v4.is_private() || v4.is_link_local() || v4.is_broadcast() || v4.is_documentation() || v4.is_unspecified() || v4.is_multicast() || is_cgnat(v4) }
fn is_cgnat(v4: Ipv4Addr) -> bool {
let [a, b, _, _] = v4.octets();
a == 100 && (64..=127).contains(&b)
}
fn is_blocked_ipv6(v6: Ipv6Addr) -> bool {
v6.is_loopback() || v6.is_multicast() || v6.is_unspecified() || is_ipv6_link_local(v6) || is_ipv6_unique_local(v6) }
fn is_ipv6_link_local(v6: Ipv6Addr) -> bool {
(v6.segments()[0] & 0xffc0) == 0xfe80
}
fn is_ipv6_unique_local(v6: Ipv6Addr) -> bool {
(v6.segments()[0] & 0xfe00) == 0xfc00
}
#[derive(Debug, thiserror::Error)]
#[error("SSRF protection rejected all IPs for host: {host}")]
pub struct SsrfRejected {
pub host: String,
}
#[derive(Debug, Default)]
pub struct SafeDnsResolver;
impl Resolve for SafeDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let host = name.as_str().to_owned();
Box::pin(async move {
let addrs = tokio::net::lookup_host(format!("{host}:0"))
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?;
let safe: Vec<SocketAddr> = addrs.filter(|sa| !is_blocked_ip(sa.ip())).collect();
if safe.is_empty() {
return Err(
Box::new(SsrfRejected { host }) as Box<dyn std::error::Error + Send + Sync>
);
}
Ok(Box::new(safe.into_iter()) as Addrs)
})
}
}
impl SafeDnsResolver {
pub fn arc() -> Arc<Self> {
Arc::new(Self)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v4(s: &str) -> IpAddr {
IpAddr::V4(s.parse().unwrap())
}
fn v6(s: &str) -> IpAddr {
IpAddr::V6(s.parse().unwrap())
}
#[test]
fn blocks_loopback_v4() {
assert!(is_blocked_ip(v4("127.0.0.1")));
assert!(is_blocked_ip(v4("127.255.255.255")));
}
#[test]
fn blocks_private_ranges() {
assert!(is_blocked_ip(v4("10.0.0.1")));
assert!(is_blocked_ip(v4("10.255.255.255")));
assert!(is_blocked_ip(v4("172.16.0.1")));
assert!(is_blocked_ip(v4("172.31.255.255")));
assert!(is_blocked_ip(v4("192.168.1.1")));
}
#[test]
fn blocks_link_local_including_metadata() {
assert!(is_blocked_ip(v4("169.254.169.254")));
assert!(is_blocked_ip(v4("169.254.0.1")));
assert!(is_blocked_ip(v4("169.254.255.254")));
}
#[test]
fn blocks_cgnat() {
assert!(is_blocked_ip(v4("100.64.0.1")));
assert!(is_blocked_ip(v4("100.127.255.254")));
}
#[test]
fn blocks_broadcast_and_unspecified_and_multicast() {
assert!(is_blocked_ip(v4("0.0.0.0")));
assert!(is_blocked_ip(v4("255.255.255.255")));
assert!(is_blocked_ip(v4("224.0.0.1")));
}
#[test]
fn blocks_ipv6_loopback_link_local_ula_multicast_unspecified() {
assert!(is_blocked_ip(v6("::1")));
assert!(is_blocked_ip(v6("::")));
assert!(is_blocked_ip(v6("fe80::1")));
assert!(is_blocked_ip(v6("fc00::1"))); assert!(is_blocked_ip(v6("fd00::1"))); assert!(is_blocked_ip(v6("ff02::1"))); }
#[test]
fn blocks_ipv4_mapped_v6() {
assert!(is_blocked_ip(v6("::ffff:10.0.0.1")));
assert!(is_blocked_ip(v6("::ffff:169.254.169.254")));
assert!(is_blocked_ip(v6("::ffff:127.0.0.1")));
}
#[test]
fn allows_public_addresses() {
assert!(!is_blocked_ip(v4("1.1.1.1")));
assert!(!is_blocked_ip(v4("8.8.8.8")));
assert!(!is_blocked_ip(v4("140.82.121.3")));
assert!(!is_blocked_ip(v6("2606:4700:4700::1111"))); assert!(!is_blocked_ip(v6("2001:4860:4860::8888"))); }
}