use arcbox_connect::sandbox_v1::InspectSandboxRequest;
use arcbox_connect::v1::{SandboxPortForwardRemoveRequest, SandboxPortForwardRequest};
use arcbox_engine::EngineError;
use crate::host::SandboxHost;
pub struct SandboxPortExposure {
pub sandbox_id: String,
pub sandbox_port: u16,
pub protocol: String,
pub host_port: u16,
pub guest_port: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SandboxPortMapping {
pub sandbox_port: u16,
pub host_port: u16,
pub protocol: SandboxPortProtocol,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SandboxPortProtocol {
Tcp,
Udp,
}
impl SandboxPortProtocol {
#[must_use]
pub const fn key(self) -> &'static str {
match self {
Self::Tcp => "tcp",
Self::Udp => "udp",
}
}
#[must_use]
const fn wire(self) -> arcbox_connect::v1::SandboxPortProtocol {
match self {
Self::Tcp => arcbox_connect::v1::SandboxPortProtocol::Tcp,
Self::Udp => arcbox_connect::v1::SandboxPortProtocol::Udp,
}
}
}
pub struct ExposedPortPair {
pub host_port: u16,
pub guest_port: u16,
}
#[derive(Debug)]
pub enum ExposePortError {
Raced,
Engine(EngineError),
}
impl From<EngineError> for ExposePortError {
fn from(err: EngineError) -> Self {
Self::Engine(err)
}
}
pub async fn expose<H: SandboxHost>(
host: &H,
machine: &str,
sandbox_id: &str,
sandbox_port: u16,
requested_host_port: u16,
protocol: SandboxPortProtocol,
) -> Result<ExposedPortPair, ExposePortError> {
let host_generation = host.host_state_generation().await;
let mut agent = host.agent(machine)?;
let forwarded = agent
.sandbox_port_forward(SandboxPortForwardRequest {
id: sandbox_id.to_owned(),
sandbox_port: u32::from(sandbox_port),
protocol: protocol.wire().into(),
..Default::default()
})
.await?;
let guest_port = u16::try_from(forwarded.guest_port).map_err(|_| {
ExposePortError::Engine(EngineError::Machine(
"agent returned an invalid guest port".into(),
))
})?;
let rollback_request = || SandboxPortForwardRemoveRequest {
id: sandbox_id.to_owned(),
sandbox_port: u32::from(sandbox_port),
protocol: protocol.wire().into(),
..Default::default()
};
let host_state = host.lock_host_state().await;
if *host_state != host_generation {
if let Err(rollback) = agent.sandbox_port_forward_remove(rollback_request()).await {
tracing::warn!(
sandbox_id,
error = %rollback,
"failed to roll back guest DNAT after host cleanup race"
);
}
return Err(ExposePortError::Raced);
}
let host_port = if requested_host_port == 0 {
guest_port
} else {
requested_host_port
};
let exposure = SandboxPortExposure {
sandbox_id: sandbox_id.to_owned(),
sandbox_port,
protocol: protocol.key().to_owned(),
host_port,
guest_port,
};
if let Err(primary) = host.expose_port(machine, &exposure).await {
if let Err(rollback) = agent.sandbox_port_forward_remove(rollback_request()).await {
tracing::warn!(
sandbox_id,
error = %rollback,
"failed to roll back guest DNAT after host bind failure"
);
}
return Err(ExposePortError::Engine(primary));
}
Ok(ExposedPortPair {
host_port,
guest_port,
})
}
#[derive(Debug)]
pub enum ListExposedPortsError {
Sandbox(EngineError),
Unavailable(EngineError),
Unstable,
}
pub async fn list<H: SandboxHost>(
host: &H,
machine: &str,
sandbox_id: &str,
) -> Result<Vec<SandboxPortMapping>, ListExposedPortsError> {
let mut agent = host
.agent(machine)
.map_err(ListExposedPortsError::Unavailable)?;
for _ in 0..2 {
let host_generation = host.host_state_generation().await;
agent
.sandbox_inspect(InspectSandboxRequest {
id: sandbox_id.to_owned(),
..Default::default()
})
.await
.map_err(|error| match error {
error @ EngineError::Agent { code: 404, .. } => {
ListExposedPortsError::Sandbox(error)
}
error => ListExposedPortsError::Unavailable(error),
})?;
if let Some(mappings) = host
.port_mappings_if_unchanged(sandbox_id, host_generation)
.await
{
return Ok(mappings);
}
}
Err(ListExposedPortsError::Unstable)
}
pub async fn unexpose<H: SandboxHost>(
host: &H,
machine: &str,
sandbox_id: &str,
sandbox_port: u16,
protocol: SandboxPortProtocol,
) -> arcbox_engine::Result<()> {
host.unexpose_port(sandbox_id, sandbox_port, protocol.key())
.await;
let mut agent = host.agent(machine)?;
agent
.sandbox_port_forward_remove(SandboxPortForwardRemoveRequest {
id: sandbox_id.to_owned(),
sandbox_port: u32::from(sandbox_port),
protocol: protocol.wire().into(),
..Default::default()
})
.await?;
Ok(())
}