use crate::Error;
use std::net::Ipv4Addr;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Namespace(String);
impl Namespace {
pub fn new(value: impl Into<String>) -> Result<Self, Error> {
identity(value.into(), 39, "invalid network namespace identity").map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Bridge(String);
impl Bridge {
pub fn new(value: impl Into<String>) -> Result<Self, Error> {
identity(value.into(), 40, "invalid network bridge identity").map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn remove(&self) -> Result<(), Error> {
let path = std::path::PathBuf::from(format!("/tmp/.hl-bridge-{}", self.0));
match std::fs::remove_dir_all(path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error.into()),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Interface {
bridge: Bridge,
address: Ipv4Addr,
prefix: u8,
}
impl Interface {
pub fn new(bridge: Bridge, address: Ipv4Addr, prefix: u8) -> Result<Self, Error> {
if prefix > 32 {
Err(Error::InvalidConfig("IPv4 interface prefix exceeds 32"))
} else {
Ok(Self {
bridge,
address,
prefix,
})
}
}
#[must_use]
pub fn bridge(&self) -> &Bridge {
&self.bridge
}
#[must_use]
pub const fn address(&self) -> Ipv4Addr {
self.address
}
#[must_use]
pub const fn prefix(&self) -> u8 {
self.prefix
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Rule {
host: u16,
guest: u16,
address: Ipv4Addr,
}
impl Rule {
pub const fn new(host: u16, guest: u16) -> Result<Self, Error> {
if host == 0 || guest == 0 {
Err(Error::InvalidConfig(
"published host and guest ports must be nonzero",
))
} else {
Ok(Self {
host,
guest,
address: Ipv4Addr::UNSPECIFIED,
})
}
}
#[must_use]
pub const fn address(mut self, value: Ipv4Addr) -> Self {
self.address = value;
self
}
#[must_use]
pub const fn host(self) -> u16 {
self.host
}
#[must_use]
pub const fn guest(self) -> u16 {
self.guest
}
#[must_use]
pub const fn host_address(self) -> Ipv4Addr {
self.address
}
}
fn identity(value: String, maximum: usize, message: &'static str) -> Result<String, Error> {
if value.is_empty()
|| value.len() > maximum
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
{
Err(Error::InvalidConfig(message))
} else {
Ok(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identities_match_the_c_abi_alphabet_and_limits() {
assert_eq!(
Namespace::new("net.alpha-1").unwrap().as_str(),
"net.alpha-1"
);
assert_eq!(Bridge::new("_bridge.1").unwrap().as_str(), "_bridge.1");
assert!(Namespace::new("").is_err());
assert!(Namespace::new("a".repeat(40)).is_err());
assert!(Bridge::new("a".repeat(41)).is_err());
assert!(Bridge::new("bridge/one").is_err());
assert!(Bridge::new("bridge:one").is_err());
}
#[test]
fn publication_ports_are_nonzero() {
let rule = Rule::new(8_080, 80).unwrap();
assert_eq!((rule.host(), rule.guest()), (8_080, 80));
assert_eq!(rule.host_address(), Ipv4Addr::UNSPECIFIED);
assert_eq!(
rule.address(Ipv4Addr::LOCALHOST).host_address(),
Ipv4Addr::LOCALHOST
);
assert!(Rule::new(0, 80).is_err());
assert!(Rule::new(8_080, 0).is_err());
}
#[test]
fn bridge_removal_is_idempotent() {
let bridge = Bridge::new(format!("test-{}", std::process::id())).unwrap();
let path = std::path::PathBuf::from(format!("/tmp/.hl-bridge-{}", bridge.as_str()));
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path.join("state"), b"stale").unwrap();
bridge.remove().unwrap();
bridge.remove().unwrap();
assert!(!path.exists());
}
}