use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use ipnet::Ipv4Net;
use super::errors::RpcError;
pub(super) const ALLOW_PUBLIC_ENV: &str = "NETSCLI_MCP_ALLOW_PUBLIC_TARGETS";
fn public_targets_allowed() -> bool {
match std::env::var(ALLOW_PUBLIC_ENV) {
Ok(value) => {
let value = value.trim().to_ascii_lowercase();
matches!(value.as_str(), "1" | "true" | "yes" | "on")
}
Err(_) => false,
}
}
pub(super) fn is_local_scope(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => is_local_v4(v4),
IpAddr::V6(v6) => is_local_v6(v6),
}
}
fn is_local_v4(ip: Ipv4Addr) -> bool {
let is_cgnat = ip.octets()[0] == 100 && (64..128).contains(&ip.octets()[1]);
ip.is_private() || ip.is_loopback() || ip.is_link_local() || ip.is_unspecified() || is_cgnat
}
fn is_local_v6(ip: Ipv6Addr) -> bool {
let segments = ip.segments();
let is_unique_local = (segments[0] & 0xfe00) == 0xfc00;
let is_link_local = (segments[0] & 0xffc0) == 0xfe80;
ip.is_loopback() || ip.is_unspecified() || is_unique_local || is_link_local
}
fn refusal(target: &str) -> RpcError {
RpcError::InvalidParams(format!(
"{target} is outside the local network. This server only scans local \
targets unless it is started with {ALLOW_PUBLIC_ENV}=1."
))
}
pub(super) fn ensure_ip_allowed(ip: IpAddr, shown_as: &str) -> Result<(), RpcError> {
if public_targets_allowed() || is_local_scope(ip) {
return Ok(());
}
Err(refusal(shown_as))
}
pub(super) fn ensure_subnet_allowed(net: &Ipv4Net, shown_as: &str) -> Result<(), RpcError> {
ensure_ip_allowed(IpAddr::V4(net.network()), shown_as)
}
pub(super) async fn ensure_host_allowed(
host: &str,
dns_timeout_ms: u64,
) -> Result<IpAddr, RpcError> {
let host = host.trim();
let (ip, shown_as) = match host.parse::<IpAddr>() {
Ok(ip) => (ip, host.to_string()),
Err(_) => {
let ip = netscli_core::resolve_host_ip_with_timeout(host, dns_timeout_ms)
.await
.map_err(|e| RpcError::ToolError(e.to_string()))?;
(ip, format!("{host} ({ip})"))
}
};
ensure_ip_allowed(ip, &shown_as)?;
Ok(ip)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_ranges_are_allowed_without_the_opt_in() {
for ip in [
"10.0.0.1",
"172.16.5.4",
"192.168.1.1",
"127.0.0.1",
"169.254.1.1",
"100.100.1.1", "::1",
"fd00::1",
"fe80::1",
] {
let parsed: IpAddr = ip.parse().unwrap();
assert!(is_local_scope(parsed), "{ip} should be local");
}
}
#[test]
fn public_addresses_are_not_local() {
for ip in [
"8.8.8.8",
"198.51.100.7", "172.32.0.1", "100.128.0.1", "2606:4700::1", ] {
let parsed: IpAddr = ip.parse().unwrap();
assert!(!is_local_scope(parsed), "{ip} should not be local");
}
}
#[tokio::test]
async fn an_allowed_literal_comes_back_as_the_address_to_scan() {
let ip = ensure_host_allowed("192.168.1.1", 500)
.await
.expect("a private literal is allowed");
assert_eq!(ip, "192.168.1.1".parse::<IpAddr>().unwrap());
}
#[tokio::test]
async fn a_public_literal_is_refused_and_yields_no_address() {
assert!(ensure_host_allowed("198.51.100.7", 500).await.is_err());
}
#[test]
fn a_public_subnet_is_refused_by_the_network_address() {
let net: Ipv4Net = "198.51.100.0/24".parse().unwrap();
assert!(ensure_subnet_allowed(&net, "198.51.100.0/24").is_err());
let net: Ipv4Net = "192.168.1.0/24".parse().unwrap();
assert!(ensure_subnet_allowed(&net, "192.168.1.0/24").is_ok());
}
}