mod error;
use blake2::digest::{Update, VariableOutput};
use blake2::Blake2bVar;
use ipnetwork::Ipv6Network;
use std::net::{IpAddr, Ipv6Addr};
use std::str::FromStr;
pub use error::Error;
pub type Result<T> = std::result::Result<T, Error>;
const IP4_PREFIX: u8 = 32;
const IP6_PREFIX: u8 = 128;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct IpNetwork(ipnetwork::IpNetwork);
impl FromStr for IpNetwork {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Ok(Self(s.parse()?))
}
}
pub fn ip(name: &str, net: IpNetwork) -> Result<IpAddr> {
match net.0 {
ipnetwork::IpNetwork::V6(net6) => {
if net6.prefix() == IP6_PREFIX {
return Err(Error::PrefixTooBig(net));
}
ip6(name, net6).map(IpAddr::V6)
}
ipnetwork::IpNetwork::V4(net4) => {
if net4.prefix() == IP4_PREFIX {
return Err(Error::PrefixTooBig(net));
}
let prefix = IP6_PREFIX - IP4_PREFIX + net4.prefix();
let net6 = format!("::{}/{prefix}", net4.ip()).parse::<Ipv6Network>()?;
let ipv6_addr = ip6(name, net6)?.to_string();
let ip_addr = ipv6_addr
.strip_prefix("::")
.ok_or_else(|| Error::InvalidIpNetwork(format!("[BUG] the generated IPv6 address `{ipv6_addr}` does not start with the expected prefix `::`")))?
.parse()
.map_err(|_| Error::InvalidIpNetwork(format!("[BUG] failed to parse the generated IP address `{}` as IPv4", ipv6_addr.trim_start_matches(':'))))
?;
Ok(IpAddr::V4(ip_addr))
}
}
}
fn ip6(name: &str, net: Ipv6Network) -> Result<Ipv6Addr> {
let network_len = net.prefix() as usize / 4;
let ip = net.ip().segments();
let ip_parts: Vec<String> = ip.iter().map(|b| format!("{b:04x}")).collect();
let ip_hash = ip_parts.join("");
let network_hash = &ip_hash[..network_len];
let address_len = 32 - network_len;
let blake_len = (address_len / 2) + (address_len % 2);
let address_hash = hash(name.as_bytes(), blake_len)?;
let ip_hash = format!("{}{}", network_hash, address_hash);
let ip_str = format!(
"{}:{}:{}:{}:{}:{}:{}:{}",
&ip_hash[..4],
&ip_hash[4..8],
&ip_hash[8..12],
&ip_hash[12..16],
&ip_hash[16..20],
&ip_hash[20..24],
&ip_hash[24..28],
&ip_hash[28..32]
);
let ip_addr = ip_str.parse().map_err(|_| {
Error::InvalidIpNetwork(format!(
"[BUG] failed to parse the generated IP string `{ip_str}` as IPv6",
))
})?;
Ok(ip_addr)
}
pub fn subnet(name: &str) -> Result<String> {
hash(name.as_bytes(), 2)
}
fn hash(name: &[u8], len: usize) -> Result<String> {
let mut hasher = Blake2bVar::new(len)
.map_err(|_| {
Error::InvalidIpNetwork(format!(
"[BUG] output length of {len} resulted in an error in hash generation",
))
})?;
hasher.update(name);
let mut buf = vec![0u8; len];
hasher.finalize_variable(&mut buf).map_err(|_| {
Error::InvalidIpNetwork(format!(
"[BUG] buffer size of {len} resulted in an error in hash generation",
))
})?;
Ok(buf.iter().map(|v| format!("{:02x}", v)).collect())
}
#[cfg(test)]
mod tests {
#[test]
fn ip_generation() {
let ip = crate::ip("cassandra.1", "fd9d:bb35:94bf::/48".parse().unwrap())
.unwrap()
.to_string();
assert_eq!(ip, "fd9d:bb35:94bf:c38a:ee1:c75d:8df3:c909");
let ip = crate::ip("postgresql.host1", "10.0.0.0/8".parse().unwrap())
.unwrap()
.to_string();
assert_eq!(ip, "10.102.194.34");
let ip = crate::ip("", "fd9d:bb35:94bf::/48".parse().unwrap())
.unwrap()
.to_string();
assert_eq!(ip, "fd9d:bb35:94bf:6fa1:d8fc:fd71:9046:d762");
let ip = crate::ip("", "fd9d:bb35:94bf::/48".parse().unwrap())
.unwrap()
.to_string();
assert_eq!(ip, "fd9d:bb35:94bf:6fa1:d8fc:fd71:9046:d762");
}
#[test]
fn subnet_generation() {
let subnet = crate::subnet("consul").unwrap();
assert_eq!(subnet, "1211");
}
}