use std::fs::{File, OpenOptions};
use std::os::fd::AsRawFd;
use std::path::Path;
use std::sync::Mutex;
use anyhow::{Context, Result, bail};
use arcbox_constants::container_network::ContainerNetwork;
pub struct ContainerNetworkLease {
network: ContainerNetwork,
route_bridge: Mutex<Option<String>>,
_file: File,
}
impl ContainerNetworkLease {
pub fn acquire(network: ContainerNetwork) -> Result<Option<Self>> {
if network == ContainerNetwork::default() {
return Ok(None);
}
let lease_dir = dirs::data_dir()
.context("user data directory is required for container network leases")?
.join("ArcBox/RuntimeCIDRLeases");
Self::acquire_in(network, &lease_dir).map(Some)
}
fn acquire_in(network: ContainerNetwork, lease_dir: &Path) -> Result<Self> {
std::fs::create_dir_all(lease_dir).with_context(|| {
format!(
"creating container network lease directory {}",
lease_dir.display()
)
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(lease_dir, std::fs::Permissions::from_mode(0o700))
.with_context(|| {
format!(
"restricting container network lease directory {}",
lease_dir.display()
)
})?;
}
let lock_path = lease_dir.join(format!("{}-{}.lock", network.addr(), network.prefix()));
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)
.with_context(|| format!("opening container network lease {}", lock_path.display()))?;
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if result == 0 {
return Ok(Self {
network,
route_bridge: Mutex::new(None),
_file: file,
});
}
let error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::WouldBlock {
bail!("container CIDR {network} is already in use by another ArcBox daemon");
}
Err(error).with_context(|| format!("locking container network lease for {network}"))
}
pub fn record_route(&self, bridge: String) -> Result<()> {
*self
.route_bridge
.lock()
.map_err(|_| anyhow::anyhow!("container route lease is poisoned"))? = Some(bridge);
Ok(())
}
pub async fn cleanup_route(&self) -> Result<()> {
let bridge = {
self.route_bridge
.lock()
.map_err(|_| anyhow::anyhow!("container route lease is poisoned"))?
.clone()
};
let Some(bridge) = bridge else {
return Ok(());
};
let client = arcbox_helper::client::Client::connect().await?;
client
.route_remove_if_owned(&self.network.to_string(), &bridge)
.await?;
let mut recorded = self
.route_bridge
.lock()
.map_err(|_| anyhow::anyhow!("container route lease is poisoned"))?;
if recorded.as_deref() == Some(bridge.as_str()) {
*recorded = None;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn concurrent_leases_reject_the_same_network_and_allow_release() {
let directory = tempfile::tempdir().unwrap();
let network: ContainerNetwork = "10.64.0.0/20".parse().unwrap();
let other: ContainerNetwork = "10.64.16.0/20".parse().unwrap();
let first = ContainerNetworkLease::acquire_in(network, directory.path()).unwrap();
assert!(ContainerNetworkLease::acquire_in(network, directory.path()).is_err());
let _other = ContainerNetworkLease::acquire_in(other, directory.path()).unwrap();
drop(first);
ContainerNetworkLease::acquire_in(network, directory.path()).unwrap();
}
}