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 Drop for ContainerNetworkLease {
fn drop(&mut self) {
if unsafe { libc::flock(self.file.as_raw_fd(), libc::LOCK_UN) } != 0 {
tracing::warn!(
error = %std::io::Error::last_os_error(),
network = %self.network,
"failed to unlock container network lease"
);
}
}
}
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,
});
}
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();
}
#[test]
fn release_does_not_wait_for_a_child_spawned_while_the_lease_was_held() {
use std::io::{Read as _, Write as _};
use std::os::unix::process::CommandExt as _;
let directory = tempfile::tempdir().unwrap();
let network: ContainerNetwork = "10.64.0.0/20".parse().unwrap();
let lease = ContainerNetworkLease::acquire_in(network, directory.path()).unwrap();
let (mut forked_rx, forked_tx) = std::io::pipe().unwrap();
let (go_rx, mut go_tx) = std::io::pipe().unwrap();
let spawner = std::thread::spawn(move || {
let mut command = std::process::Command::new("true");
unsafe {
command.pre_exec(move || {
(&forked_tx).write_all(b"f")?;
(&go_rx).read_exact(&mut [0])
});
}
command.status()
});
forked_rx.read_exact(&mut [0]).unwrap();
drop(lease);
let reacquired = ContainerNetworkLease::acquire_in(network, directory.path());
go_tx.write_all(b"g").unwrap();
assert!(spawner.join().unwrap().unwrap().success());
reacquired.unwrap();
}
}