#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressVerdict {
pub allowed: bool,
pub reason: String,
}
const LOOPBACK_NAMES: [&str; 4] = ["localhost", "127.0.0.1", "::1", "[::1]"];
const WILDCARDS: [&str; 4] = ["0.0.0.0", "::", "[::]", "*"];
fn parse_ipv4(host: &str) -> Option<[u8; 4]> {
let parts: Vec<&str> = host.split('.').collect();
if parts.len() != 4 {
return None;
}
let mut octets = [0u8; 4];
for (slot, part) in octets.iter_mut().zip(&parts) {
if part.is_empty() || part.len() > 3 || !part.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let value: u32 = part.parse().ok()?;
if value > 255 {
return None;
}
#[expect(clippy::cast_possible_truncation)]
{
*slot = value as u8;
}
}
Some(octets)
}
fn is_private_ipv4(octets: [u8; 4]) -> bool {
let [a, b, ..] = octets;
if a == 127 {
return true; }
if a == 10 {
return true; }
if a == 192 && b == 168 {
return true; }
if a == 172 && (16..=31).contains(&b) {
return true; }
if a == 169 && b == 254 {
return true; }
if a == 100 && (64..=127).contains(&b) {
return true;
}
false
}
fn is_private_ipv6(host: &str) -> bool {
let bare = host.trim_matches(|c| c == '[' || c == ']').to_lowercase();
if bare == "::1" {
return true;
}
if bare.starts_with("fe80:") {
return true; }
bare.starts_with("fc") || bare.starts_with("fd")
}
pub fn check_bind_address(host: &str) -> AddressVerdict {
let trimmed = host.trim().to_lowercase();
if trimmed.is_empty() {
return AddressVerdict {
allowed: false,
reason: "no address was given".to_owned(),
};
}
if WILDCARDS.contains(&trimmed.as_str()) {
return AddressVerdict {
allowed: false,
reason: format!(
"{host} listens on every interface, including public ones. Bind to a loopback or private address instead."
),
};
}
if LOOPBACK_NAMES.contains(&trimmed.as_str()) {
return AddressVerdict {
allowed: true,
reason: "loopback".to_owned(),
};
}
if let Some(octets) = parse_ipv4(&trimmed) {
return if is_private_ipv4(octets) {
AddressVerdict {
allowed: true,
reason: "private address".to_owned(),
}
} else {
AddressVerdict {
allowed: false,
reason: format!(
"{host} is a public address, and the interface has no login. Bind to a loopback, private, or tailnet address instead."
),
}
};
}
if trimmed.contains(':') {
return if is_private_ipv6(&trimmed) {
AddressVerdict {
allowed: true,
reason: "private address".to_owned(),
}
} else {
AddressVerdict {
allowed: false,
reason: format!(
"{host} is a public address, and the interface has no login. Bind to a loopback, private, or tailnet address instead."
),
}
};
}
AddressVerdict {
allowed: false,
reason: format!(
"{host} is a name, not an address. Give the address to bind to, so what is reachable does not depend on what a name resolves to."
),
}
}
#[cfg(test)]
mod tests;