use std::net::{IpAddr, ToSocketAddrs, UdpSocket};
pub fn agent_is_local(ws_url: &str) -> bool {
let Ok(parsed) = url::Url::parse(ws_url) else {
return false;
};
match parsed.host() {
Some(url::Host::Ipv4(ip)) => ip_is_local(IpAddr::V4(ip)),
Some(url::Host::Ipv6(ip)) => ip_is_local(IpAddr::V6(ip)),
Some(url::Host::Domain(domain)) => {
let port = parsed.port_or_known_default().unwrap_or(0);
match (domain, port).to_socket_addrs() {
Ok(addrs) => {
let mut saw_any = false;
for addr in addrs {
saw_any = true;
if !ip_is_local(addr.ip()) {
return false;
}
}
saw_any
}
Err(_) => false,
}
}
None => false,
}
}
fn ip_is_local(ip: IpAddr) -> bool {
ip.is_loopback() || UdpSocket::bind((ip, 0)).is_ok()
}
#[cfg(test)]
mod tests {
use super::agent_is_local;
#[test]
fn loopback_hosts_are_local() {
assert!(agent_is_local("ws://127.0.0.1:3000/ws"));
assert!(agent_is_local("ws://localhost:3000/ws"));
assert!(agent_is_local("ws://[::1]:3000/ws"));
assert!(agent_is_local("wss://127.0.0.1/ws"));
}
#[test]
fn public_addresses_are_not_local() {
assert!(!agent_is_local("ws://8.8.8.8:3000/ws"));
assert!(!agent_is_local("ws://203.0.113.1:3000/ws"));
}
#[test]
fn garbage_fails_closed() {
assert!(!agent_is_local("not a url"));
assert!(!agent_is_local(""));
}
}