use microsandbox_utils::PORTAL_PORTS_FILE;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
path::{Path, PathBuf},
};
use tokio::{fs, sync::Mutex};
use tracing::{debug, info, warn};
use crate::{MicrosandboxServerError, MicrosandboxServerResult};
pub const LOCALHOST_IP: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
static PORT_ASSIGNMENT_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
#[derive(Debug, Clone, Default)]
pub struct BiPortMapping {
sandbox_to_port: HashMap<String, u16>,
port_to_sandbox: HashMap<u16, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PortMapping {
pub mappings: HashMap<String, u16>,
}
#[derive(Debug)]
pub struct PortManager {
mappings: BiPortMapping,
file_path: PathBuf,
}
impl BiPortMapping {
pub fn new() -> Self {
Self {
sandbox_to_port: HashMap::new(),
port_to_sandbox: HashMap::new(),
}
}
pub fn insert(&mut self, sandbox_key: String, port: u16) {
if let Some(existing_sandbox) = self.port_to_sandbox.get(&port) {
if existing_sandbox != &sandbox_key {
warn!(
"Port {} was already assigned to sandbox {}, reassigning to {}",
port, existing_sandbox, sandbox_key
);
self.sandbox_to_port.remove(existing_sandbox);
}
}
if let Some(existing_port) = self.sandbox_to_port.get(&sandbox_key) {
if *existing_port != port {
self.port_to_sandbox.remove(existing_port);
}
}
self.sandbox_to_port.insert(sandbox_key.clone(), port);
self.port_to_sandbox.insert(port, sandbox_key);
}
pub fn remove_by_sandbox(&mut self, sandbox_key: &str) -> Option<u16> {
if let Some(port) = self.sandbox_to_port.remove(sandbox_key) {
self.port_to_sandbox.remove(&port);
Some(port)
} else {
None
}
}
pub fn remove_by_port(&mut self, port: u16) -> Option<String> {
if let Some(sandbox_key) = self.port_to_sandbox.remove(&port) {
self.sandbox_to_port.remove(&sandbox_key);
Some(sandbox_key)
} else {
None
}
}
pub fn get_port(&self, sandbox_key: &str) -> Option<u16> {
self.sandbox_to_port.get(sandbox_key).copied()
}
pub fn get_sandbox(&self, port: u16) -> Option<&String> {
self.port_to_sandbox.get(&port)
}
pub fn to_port_mapping(&self) -> PortMapping {
PortMapping {
mappings: self.sandbox_to_port.clone(),
}
}
pub fn from_port_mapping(mapping: PortMapping) -> Self {
let mut result = Self::new();
for (sandbox_key, port) in mapping.mappings {
result.insert(sandbox_key, port);
}
result
}
}
impl PortManager {
pub async fn new(namespace_dir: impl AsRef<Path>) -> MicrosandboxServerResult<Self> {
let file_path = namespace_dir.as_ref().join(PORTAL_PORTS_FILE);
let mappings = Self::load_mappings(&file_path).await?;
Ok(Self {
mappings,
file_path,
})
}
async fn load_mappings(file_path: &Path) -> MicrosandboxServerResult<BiPortMapping> {
if file_path.exists() {
let contents = fs::read_to_string(file_path).await.map_err(|e| {
MicrosandboxServerError::ConfigError(format!(
"Failed to read port mappings file: {}",
e
))
})?;
let port_mapping: PortMapping = serde_json::from_str(&contents).map_err(|e| {
MicrosandboxServerError::ConfigError(format!(
"Failed to parse port mappings file: {}",
e
))
})?;
Ok(BiPortMapping::from_port_mapping(port_mapping))
} else {
debug!("No port mappings file found, creating a new one");
Ok(BiPortMapping::new())
}
}
async fn save_mappings(&self) -> MicrosandboxServerResult<()> {
let port_mapping = self.mappings.to_port_mapping();
let contents = serde_json::to_string_pretty(&port_mapping).map_err(|e| {
MicrosandboxServerError::ConfigError(format!(
"Failed to serialize port mappings: {}",
e
))
})?;
if let Some(parent) = self.file_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent).await.map_err(|e| {
MicrosandboxServerError::ConfigError(format!(
"Failed to create directory for port mappings file: {}",
e
))
})?;
}
}
fs::write(&self.file_path, contents).await.map_err(|e| {
MicrosandboxServerError::ConfigError(format!(
"Failed to write port mappings file: {}",
e
))
})
}
pub async fn assign_port(&mut self, key: &str) -> MicrosandboxServerResult<u16> {
if let Some(port) = self.mappings.get_port(key) {
if self.verify_port_availability(port) {
return Ok(port);
} else {
warn!("Previously assigned port {} for sandbox {} is no longer available, reassigning", port, key);
self.mappings.remove_by_sandbox(key);
}
}
let _lock = PORT_ASSIGNMENT_LOCK.lock().await;
let port = self.get_available_port_from_os()?;
self.mappings.insert(key.to_string(), port);
self.save_mappings().await?;
info!("Assigned port {} to sandbox {}", port, key);
Ok(port)
}
pub async fn release_port(&mut self, key: &str) -> MicrosandboxServerResult<()> {
if self.mappings.remove_by_sandbox(key).is_some() {
self.save_mappings().await?;
info!("Released port for sandbox {}", key);
}
Ok(())
}
pub fn get_port(&self, key: &str) -> Option<u16> {
self.mappings.get_port(key)
}
fn verify_port_availability(&self, port: u16) -> bool {
let addr = SocketAddr::new(LOCALHOST_IP, port);
match TcpListener::bind(addr) {
Ok(_) => true, Err(_) => false, }
}
fn get_available_port_from_os(&self) -> MicrosandboxServerResult<u16> {
let addr = SocketAddr::new(LOCALHOST_IP, 0);
let listener = TcpListener::bind(addr).map_err(|e| {
MicrosandboxServerError::ConfigError(format!(
"Failed to bind to address to get available port: {}",
e
))
})?;
let port = listener
.local_addr()
.map_err(|e| {
MicrosandboxServerError::ConfigError(format!(
"Failed to get local address from socket: {}",
e
))
})?
.port();
debug!("OS assigned port {}", port);
Ok(port)
}
}