#![allow(dead_code)]
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
use std::sync::Arc;
use crate::utils::http::get_user_agent;
#[derive(Debug, Clone)]
pub struct LookupAddress {
pub address: String,
pub family: u8, }
pub fn is_blocked_address(address: &str) -> bool {
if let Ok(ipv4) = address.parse::<Ipv4Addr>() {
return is_blocked_v4(&ipv4);
}
if let Ok(ipv6) = address.parse::<Ipv6Addr>() {
return is_blocked_v6(&ipv6);
}
false
}
fn is_blocked_v4(addr: &Ipv4Addr) -> bool {
let octets = addr.octets();
let a = octets[0] as u32;
let b = octets[1] as u32;
if a == 127 {
return false;
}
if a == 0 {
return true;
}
if a == 10 {
return true;
}
if a == 169 && b == 254 {
return true;
}
if a == 172 && b >= 16 && b <= 31 {
return true;
}
if a == 100 && b >= 64 && b <= 127 {
return true;
}
if a == 192 && b == 168 {
return true;
}
false
}
fn is_blocked_v6(addr: &Ipv6Addr) -> bool {
let segments = addr.segments();
if *addr == Ipv6Addr::LOCALHOST {
return false;
}
if *addr == Ipv6Addr::UNSPECIFIED {
return true;
}
if let Some(mapped_v4) = extract_mapped_ipv4(addr) {
return is_blocked_v4(&mapped_v4);
}
let first_segment = segments[0];
if first_segment >= 0xfc00 && first_segment <= 0xfdff {
return true;
}
if first_segment >= 0xfe80 && first_segment <= 0xfebf {
return true;
}
false
}
fn extract_mapped_ipv4(addr: &Ipv6Addr) -> Option<Ipv4Addr> {
let segments = addr.segments();
if segments[0] == 0
&& segments[1] == 0
&& segments[2] == 0
&& segments[3] == 0
&& segments[4] == 0
&& segments[5] == 0xffff
{
let hi = segments[6];
let lo = segments[7];
let ipv4 = Ipv4Addr::new(
((hi >> 8) & 0xff) as u8,
(hi & 0xff) as u8,
((lo >> 8) & 0xff) as u8,
(lo & 0xff) as u8,
);
return Some(ipv4);
}
None
}
#[derive(Debug)]
pub struct DnsLookupResult {
pub addresses: Vec<LookupAddress>,
}
pub fn ssrf_guarded_lookup(hostname: &str) -> Result<DnsLookupResult, SsrfError> {
if let Ok(ipv4) = hostname.parse::<Ipv4Addr>() {
if is_blocked_v4(&ipv4) {
return Err(ssrf_error(hostname, &ipv4.to_string()));
}
return Ok(DnsLookupResult {
addresses: vec![LookupAddress {
address: hostname.to_string(),
family: 4,
}],
});
}
if let Ok(ipv6) = hostname.parse::<Ipv6Addr>() {
if is_blocked_v6(&ipv6) {
return Err(ssrf_error(hostname, &ipv6.to_string()));
}
return Ok(DnsLookupResult {
addresses: vec![LookupAddress {
address: hostname.to_string(),
family: 6,
}],
});
}
let socket_addrs = format!("{}:0", hostname)
.to_socket_addrs()
.map_err(|e| SsrfError {
code: "ENOTFOUND".to_string(),
hostname: hostname.to_string(),
address: String::new(),
message: e.to_string(),
})?;
let mut addresses = Vec::new();
for socket_addr in socket_addrs {
let ip = socket_addr.ip();
match ip {
IpAddr::V4(v4) => {
if is_blocked_v4(&v4) {
return Err(ssrf_error(hostname, &v4.to_string()));
}
addresses.push(LookupAddress {
address: v4.to_string(),
family: 4,
});
}
IpAddr::V6(v6) => {
if is_blocked_v6(&v6) {
return Err(ssrf_error(hostname, &v6.to_string()));
}
addresses.push(LookupAddress {
address: v6.to_string(),
family: 6,
});
}
}
}
if addresses.is_empty() {
return Err(SsrfError {
code: "ENOTFOUND".to_string(),
hostname: hostname.to_string(),
address: String::new(),
message: format!("No addresses found for {}", hostname),
});
}
Ok(DnsLookupResult { addresses })
}
#[derive(Debug)]
pub struct SsrfError {
pub code: String,
pub hostname: String,
pub address: String,
pub message: String,
}
impl std::fmt::Display for SsrfError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"HTTP hook blocked: {} resolves to {} (private/link-local address). Loopback (127.0.0.1, ::1) is allowed for local dev.",
self.hostname, self.address
)
}
}
impl std::error::Error for SsrfError {}
fn ssrf_error(hostname: &str, address: &str) -> SsrfError {
SsrfError {
code: "ERR_HTTP_HOOK_BLOCKED_ADDRESS".to_string(),
hostname: hostname.to_string(),
address: address.to_string(),
message: format!(
"HTTP hook blocked: {} resolves to {} (private/link-local address). Loopback (127.0.0.1, ::1) is allowed for local dev.",
hostname, address
),
}
}
pub async fn ssrf_guarded_lookup_async(hostname: &str) -> Result<DnsLookupResult, SsrfError> {
let hostname_owned = hostname.to_string();
let hostname_for_err = hostname_owned.clone();
let result = tokio::task::spawn_blocking(move || ssrf_guarded_lookup(&hostname_owned))
.await
.map_err(|e| SsrfError {
code: "INTERNAL_ERROR".to_string(),
hostname: hostname_for_err,
address: String::new(),
message: e.to_string(),
})?;
result
}
pub fn create_ssrf_protected_connector() -> Arc<reqwest::Client> {
reqwest::Client::builder()
.user_agent(get_user_agent())
.danger_accept_invalid_certs(false)
.build()
.unwrap_or_else(|_| reqwest::Client::new())
.into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_blocked_address_loopback_allowed() {
assert!(!is_blocked_address("127.0.0.1"));
assert!(!is_blocked_address("127.0.0.255"));
assert!(!is_blocked_address("::1"));
}
#[test]
fn test_is_blocked_address_private_ipv4() {
assert!(is_blocked_address("10.0.0.1"));
assert!(is_blocked_address("192.168.1.1"));
assert!(is_blocked_address("172.16.0.1"));
assert!(is_blocked_address("172.31.255.255"));
}
#[test]
fn test_is_blocked_address_link_local() {
assert!(is_blocked_address("169.254.169.254")); assert!(is_blocked_address("169.254.0.1"));
}
#[test]
fn test_is_blocked_address_cgnat() {
assert!(is_blocked_address("100.100.100.200")); assert!(is_blocked_address("100.64.0.1"));
assert!(is_blocked_address("100.127.255.255"));
}
#[test]
fn test_is_blocked_address_this_network() {
assert!(is_blocked_address("0.0.0.0"));
assert!(is_blocked_address("0.255.255.255"));
}
#[test]
fn test_is_blocked_address_public_allowed() {
assert!(!is_blocked_address("8.8.8.8"));
assert!(!is_blocked_address("1.1.1.1"));
assert!(!is_blocked_address("192.0.2.1")); }
#[test]
fn test_is_blocked_address_ipv6() {
assert!(!is_blocked_address("::1")); assert!(is_blocked_address("::")); assert!(is_blocked_address("fc00::1")); assert!(is_blocked_address("fd00::1")); assert!(is_blocked_address("fe80::1")); }
#[test]
fn test_is_blocked_address_ipv4_mapped_ipv6() {
assert!(is_blocked_address("::ffff:169.254.169.254"));
assert!(!is_blocked_address("::ffff:127.0.0.1"));
assert!(is_blocked_address("::ffff:10.0.0.1"));
}
#[test]
fn test_ssrf_guarded_lookup_loopback() {
let result = ssrf_guarded_lookup("127.0.0.1");
assert!(result.is_ok());
}
#[test]
fn test_ssrf_guarded_lookup_blocked_private() {
let result = ssrf_guarded_lookup("10.0.0.1");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code, "ERR_HTTP_HOOK_BLOCKED_ADDRESS");
}
}