From 79975c3ed6e4323170cf97790022b9337bd63d3e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 7 Aug 2026 15:20:48 -0700 Subject: [PATCH 1/6] Virtualize broker TCP ports Give each broker session its own guest TCP port namespace, keep Linux backend endpoints private, route session-local connections through translated host tuples, and add explicit host-to-guest TCP port mappings with reserved endpoint lifecycle handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- dev_bench/src/main.rs | 9 +- litebox_broker_core/src/session.rs | 5 +- litebox_broker_core/src/socket.rs | 463 ++++- litebox_broker_host/src/lib.rs | 16 +- .../src/lib.rs | 2 +- .../src/socket.rs | 1526 +++++++++++++++-- litebox_broker_protocol/src/socket.rs | 8 +- litebox_broker_userland/src/main.rs | 75 +- litebox_runner_linux_userland/tests/run.rs | 35 +- .../tests/tcp_broker_server.c | 10 +- 10 files changed, 1970 insertions(+), 179 deletions(-) diff --git a/dev_bench/src/main.rs b/dev_bench/src/main.rs index a769af28d..63524fc16 100644 --- a/dev_bench/src/main.rs +++ b/dev_bench/src/main.rs @@ -712,15 +712,18 @@ fn run_rewritten_iperf3(ctx: BenchCtx<'_>) -> Result<()> { let client_sh = xshell::Shell::new()?; let max_server_attempts = 5; let max_client_attempts = 50; + let guest_port = "5201"; let mut last_failure = "sandboxed server did not become ready".to_owned(); for server_attempt in 1..=max_server_attempts { - let port = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))? + let host_port = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))? .local_addr()? .port() .to_string(); let mut server_command = std::process::Command::new(&broker); server_command + .arg("--publish-tcp") + .arg(format!("127.0.0.1:{host_port}:{guest_port}")) .arg("--runner") .arg(&runner) .arg("--") @@ -733,7 +736,7 @@ fn run_rewritten_iperf3(ctx: BenchCtx<'_>) -> Result<()> { ]) .arg(&tar_file) .arg(&iperf3_rewritten) - .args(["-s", "-1", "-B", "127.0.0.1", "-p", &port]); + .args(["-s", "-1", "-B", "127.0.0.1", "-p", guest_port]); if COMMAND_EXECUTION_IS_QUIET.load(Relaxed) { server_command .stdout(std::process::Stdio::null()) @@ -749,7 +752,7 @@ fn run_rewritten_iperf3(ctx: BenchCtx<'_>) -> Result<()> { for client_attempt in 1..=max_client_attempts { let result = cmd!( client_sh, - "{iperf3_host} -c 127.0.0.1 -p {port} --bytes 1G --connect-timeout 50" + "{iperf3_host} -c 127.0.0.1 -p {host_port} --bytes 1G --connect-timeout 50" ) .quiet() .ignore_stdout() diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index aa4a65f03..885579d8d 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -6,7 +6,7 @@ use core::sync::atomic::{AtomicUsize, Ordering}; use crate::event::EventObject; use crate::pipe::PipeObject; -use crate::socket::SocketObject; +use crate::socket::{SessionSocketPorts, SocketObject}; use crate::{BrokerCore, BrokerError, Result}; use hashbrown::HashMap; use litebox_broker_protocol::ObjectHandle; @@ -77,6 +77,8 @@ pub struct BrokerSession { references: Mutex, /// Socket quota held by pending, live, and closing in-flight resources. pub(crate) reserved_sockets: Arc, + /// Guest-visible TCP port namespace owned by this session. + pub(crate) socket_ports: SessionSocketPorts, } impl BrokerSession { @@ -95,6 +97,7 @@ impl BrokerSession { pending_handles: 0, }), reserved_sockets: Arc::new(AtomicUsize::new(0)), + socket_ports: SessionSocketPorts::default(), } } diff --git a/litebox_broker_core/src/socket.rs b/litebox_broker_core/src/socket.rs index 74a0f827b..8cdf0fcfb 100644 --- a/litebox_broker_core/src/socket.rs +++ b/litebox_broker_core/src/socket.rs @@ -7,6 +7,7 @@ use alloc::{sync::Arc, vec::Vec}; use core::net::{Ipv4Addr, SocketAddrV4}; use core::sync::atomic::{AtomicUsize, Ordering}; +use hashbrown::HashMap; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::socket::{ @@ -15,6 +16,7 @@ use litebox_broker_protocol::socket::{ SocketConnectionStatus, SocketError, SocketOutcome, SocketStatusResponse, SocketType, TcpOptionName, TcpOptionValue, }; +use spin::Mutex; use spin::Once; use crate::readiness::{ReadinessRegistration, ReadinessSink}; @@ -22,13 +24,110 @@ use crate::session::{ObjectEntry, ObjectRights}; use crate::{BrokerError, BrokerSession, Result, SessionId}; const DEFAULT_TCP_LISTEN_ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); +const DEFAULT_TCP_LOCAL_ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); +const FIRST_EPHEMERAL_PORT: u16 = 49152; + +#[derive(Default)] +struct SessionSocketPortState { + tcp: HashMap, + next_tcp_ephemeral: Option, +} + +/// Per-session authority for the guest-visible TCP port namespace. +/// +/// Guest TCP ports are allocated by the broker and are independent of the host +/// ports its socket backend uses, so two sessions may bind the same guest port. +#[derive(Clone, Default)] +pub(crate) struct SessionSocketPorts { + state: Arc>, +} + +impl SessionSocketPorts { + /// Reserves one guest TCP port for `requested_address`. + /// + /// A zero requested port is allocated from the ephemeral range, skipping + /// ports the provider reserved for an explicit host port mapping. The + /// reservation is released when the returned guard is dropped. + fn reserve( + &self, + request: CreateSocketRequest, + requested_address: SocketAddrV4, + mut port_is_reserved: impl FnMut(u16) -> bool, + ) -> Result> { + if !is_tcp(request) { + return Err(BrokerError::Internal); + } + let mut state = self.state.lock(); + let port = if requested_address.port() == 0 { + state.allocate_ephemeral(&mut port_is_reserved)? + } else if state.tcp.contains_key(&requested_address.port()) { + return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); + } else { + requested_address.port() + }; + let local_address = SocketAddrV4::new(*requested_address.ip(), port); + state + .tcp + .try_reserve(1) + .map_err(|_| BrokerError::OutOfMemory)?; + if state.tcp.insert(port, local_address).is_some() { + return Err(BrokerError::Internal); + } + drop(state); + Ok(SocketOutcome::Completed(( + local_address, + GuestPortReservation { + ports: self.clone(), + port, + }, + ))) + } +} + +impl SessionSocketPortState { + fn allocate_ephemeral( + &mut self, + port_is_reserved: &mut impl FnMut(u16) -> bool, + ) -> Result { + let start = self.next_tcp_ephemeral.unwrap_or(FIRST_EPHEMERAL_PORT); + let mut port = start; + loop { + if !self.tcp.contains_key(&port) && !port_is_reserved(port) { + self.next_tcp_ephemeral = Some(if port == u16::MAX { + FIRST_EPHEMERAL_PORT + } else { + port + 1 + }); + return Ok(port); + } + port = if port == u16::MAX { + FIRST_EPHEMERAL_PORT + } else { + port + 1 + }; + if port == start { + return Err(BrokerError::ResourceExhausted); + } + } + } +} + +/// Guard releasing one guest TCP port back to its session namespace. +struct GuestPortReservation { + ports: SessionSocketPorts, + port: u16, +} + +impl Drop for GuestPortReservation { + fn drop(&mut self) { + self.ports.state.lock().tcp.remove(&self.port); + } +} /// Platform socket and endpoint metadata returned by an accept operation. pub struct AcceptedPlatformSocket { /// Accepted nonblocking platform socket. pub socket: Arc, - /// Local endpoint of the accepted connection. - pub local_address: SocketAddrV4, /// Remote endpoint of the accepted connection. pub remote_address: SocketAddrV4, } @@ -42,6 +141,15 @@ pub enum PlatformConnectError { PeerIndeterminate(BrokerError), } +/// Whether a guest-local binding was requested explicitly or allocated implicitly. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PlatformBindKind { + /// The guest explicitly requested this nonzero local port through `bind`. + Explicit, + /// The broker allocated the local port for another socket operation. + Implicit, +} + /// Broker socket and endpoint metadata returned by an accept operation. pub struct AcceptedBrokerSocket { /// Broker handle naming the accepted socket. @@ -78,6 +186,15 @@ pub struct PlatformDatagramReceive { /// bookkeeping shared across the sockets of a broker session. Operations on an /// individual socket belong to [`PlatformSocket`], not this shared provider. pub trait SocketProvider: Send + Sync { + /// Returns whether a guest port is reserved for an explicit host port mapping. + /// + /// Reserved ports are skipped by implicit guest-port allocation but remain + /// available to an explicit nonzero `bind` request. + fn reserves_guest_port(&self, request: CreateSocketRequest, port: u16) -> bool { + let _ = (request, port); + false + } + /// Creates one nonblocking socket resource for an authenticated session. /// /// The returned socket must not retain authority beyond its `Arc` lifetime. @@ -98,10 +215,21 @@ pub trait SocketProvider: Send + Sync { /// in flight to finish after its object handle closes. Dropping the final `Arc` /// releases the platform socket. pub trait PlatformSocket: Send + Sync { - /// Binds this socket to a local address. - fn bind(&self, address: SocketAddrV4) -> Result>; + /// Binds this socket to a local address and echoes the assigned address. + /// + /// A stream socket receives a broker-reserved guest-local address with a + /// nonzero port and must echo it unchanged; the host endpoint backing the + /// socket is chosen privately by the platform. A datagram socket receives + /// the address requested by the guest and returns the host-assigned one. + fn bind( + &self, + address: SocketAddrV4, + kind: PlatformBindKind, + ) -> Result>; /// Makes this socket listen for incoming connections. + /// + /// The returned address is the socket's guest-local address. fn listen(&self, backlog: u32) -> Result>; /// Accepts one pending connection without waiting. @@ -220,6 +348,7 @@ pub fn create( platform_socket: Once::new(), readiness, _quota: quota, + port_reservation: Mutex::new(None), }); let platform_socket = match session.core.socket_provider.create( session.session_id, @@ -277,7 +406,7 @@ pub fn connect( return connect_datagram(&object, address); } - let resource = { + let (resource, needs_bind) = { let mut object = object.write(); let ObjectEntry::Socket(socket) = &mut *object else { return Err(BrokerError::InvalidRights); @@ -292,8 +421,47 @@ pub fn connect( return Ok(SocketOutcome::Completed(socket.connection_status)); } socket.connect_in_flight = true; - Arc::clone(&socket.resource) + (Arc::clone(&socket.resource), socket.local_address.is_none()) }; + if needs_bind { + match session.core.policy.authorize_socket_bind( + session.caller_credential, + create_request, + DEFAULT_TCP_LOCAL_ADDRESS, + ) { + Ok(()) => {} + Err(BrokerError::PolicyDenied) => { + finish_connect(&object, SocketConnectionStatus::Unconnected); + return Ok(SocketOutcome::Failed(SocketError::PolicyDenied)); + } + Err(error) => { + finish_connect(&object, SocketConnectionStatus::Unconnected); + return Err(error); + } + } + let binding = match reserve_and_bind( + session, + create_request, + &resource, + DEFAULT_TCP_LOCAL_ADDRESS, + PlatformBindKind::Implicit, + ) { + Ok(binding) => binding, + Err(error) => { + finish_connect(&object, SocketConnectionStatus::Unconnected); + return Err(error); + } + }; + match binding { + SocketOutcome::Completed((local_address, reservation)) => { + attach_binding(&object, local_address, reservation); + } + SocketOutcome::Failed(error) => { + finish_connect(&object, SocketConnectionStatus::Unconnected); + return Ok(SocketOutcome::Failed(error)); + } + } + } let status = match resource.connect(address) { Ok(SocketConnectionStatus::Unconnected) => { finish_connect(&object, SocketConnectionStatus::Failed(SocketError::Other)); @@ -343,29 +511,52 @@ pub fn bind( ) { Ok(()) => {} Err(BrokerError::PolicyDenied) => { - finish_configuration(&object, None, false); + finish_configuration(&object, None, None, false); return Ok(SocketOutcome::Failed(SocketError::PolicyDenied)); } Err(error) => { - finish_configuration(&object, None, false); + finish_configuration(&object, None, None, false); return Err(error); } } - let outcome = resource.bind(address); - match outcome { - Ok(SocketOutcome::Completed(local_address)) => { - finish_configuration(&object, Some(local_address), false); - Ok(SocketOutcome::Completed(local_address)) - } - Ok(SocketOutcome::Failed(error)) => { - finish_configuration(&object, None, false); - Ok(SocketOutcome::Failed(error)) - } + let bind_kind = if address.port() == 0 { + PlatformBindKind::Implicit + } else { + PlatformBindKind::Explicit + }; + if !is_tcp(create_request) { + // Datagram sockets remain backed directly by a host endpoint. + return match resource.bind(address, bind_kind) { + Ok(SocketOutcome::Completed(local_address)) => { + finish_configuration(&object, Some(local_address), None, false); + Ok(SocketOutcome::Completed(local_address)) + } + Ok(SocketOutcome::Failed(error)) => { + finish_configuration(&object, None, None, false); + Ok(SocketOutcome::Failed(error)) + } + Err(error) => { + finish_configuration(&object, None, None, false); + Err(error) + } + }; + } + let binding = match reserve_and_bind(session, create_request, &resource, address, bind_kind) { + Ok(binding) => binding, Err(error) => { - finish_configuration(&object, None, false); - Err(error) + finish_configuration(&object, None, None, false); + return Err(error); } - } + }; + let (local_address, reservation) = match binding { + SocketOutcome::Completed(binding) => binding, + SocketOutcome::Failed(error) => { + finish_configuration(&object, None, None, false); + return Ok(SocketOutcome::Failed(error)); + } + }; + finish_configuration(&object, Some(local_address), Some(reservation), false); + Ok(SocketOutcome::Completed(local_address)) } /// Makes a socket listen for incoming loopback connections. @@ -378,7 +569,7 @@ pub fn listen( return Err(BrokerError::UnsupportedOperation); } let object = session.authorized_object(handle, ObjectRights::WRITE)?; - let (resource, create_request, needs_bind) = { + let (resource, create_request, existing_local_address) = { let mut object = object.write(); let ObjectEntry::Socket(socket) = &mut *object else { return Err(BrokerError::InvalidRights); @@ -396,12 +587,13 @@ pub fn listen( ( Arc::clone(&socket.resource), socket.create_request, - socket.local_address.is_none(), + socket.local_address, ) }; - let mut local_address = None; - if needs_bind { + let mut local_address = existing_local_address; + let mut port_reservation = None; + if local_address.is_none() { match session.core.policy.authorize_socket_bind( session.caller_credential, create_request, @@ -409,39 +601,56 @@ pub fn listen( ) { Ok(()) => {} Err(BrokerError::PolicyDenied) => { - finish_configuration(&object, None, false); + finish_configuration(&object, None, None, false); return Ok(SocketOutcome::Failed(SocketError::PolicyDenied)); } Err(error) => { - finish_configuration(&object, None, false); + finish_configuration(&object, None, None, false); return Err(error); } } - match resource.bind(DEFAULT_TCP_LISTEN_ADDRESS) { - Ok(SocketOutcome::Completed(address)) => local_address = Some(address), - Ok(SocketOutcome::Failed(error)) => { - finish_configuration(&object, None, false); - return Ok(SocketOutcome::Failed(error)); - } + let binding = match reserve_and_bind( + session, + create_request, + &resource, + DEFAULT_TCP_LISTEN_ADDRESS, + PlatformBindKind::Implicit, + ) { + Ok(binding) => binding, Err(error) => { - finish_configuration(&object, None, false); + finish_configuration(&object, None, None, false); return Err(error); } + }; + match binding { + SocketOutcome::Completed((address, reservation)) => { + local_address = Some(address); + port_reservation = Some(reservation); + } + SocketOutcome::Failed(error) => { + finish_configuration(&object, None, None, false); + return Ok(SocketOutcome::Failed(error)); + } } } match resource.listen(backlog) { Ok(SocketOutcome::Completed(address)) => { - local_address = Some(address); - finish_configuration(&object, local_address, true); + // The guest-local address is broker-authoritative, so a platform + // that reports a different one is not trustworthy. + if local_address != Some(address) { + finish_configuration(&object, local_address, port_reservation, false); + return Err(BrokerError::Internal); + } + finish_configuration(&object, local_address, port_reservation, true); Ok(SocketOutcome::Completed(address)) } Ok(SocketOutcome::Failed(error)) => { - finish_configuration(&object, local_address, false); + finish_configuration(&object, local_address, port_reservation, false); Ok(SocketOutcome::Failed(error)) } Err(error) => { - finish_configuration(&object, local_address, false); + finish_configuration(&object, local_address, port_reservation, false); Err(error) } } @@ -454,7 +663,7 @@ pub fn accept( readiness_sink: Arc, ) -> Result> { let listener = session.authorized_object(handle, ObjectRights::WAIT)?; - let (listener_resource, create_request) = { + let (listener_resource, create_request, local_address) = { let listener = listener.read(); let ObjectEntry::Socket(socket) = &*listener else { return Err(BrokerError::InvalidRights); @@ -465,7 +674,11 @@ pub fn accept( if !socket.listening { return Ok(SocketOutcome::Failed(SocketError::NotConnected)); } - (Arc::clone(&socket.resource), socket.create_request) + ( + Arc::clone(&socket.resource), + socket.create_request, + socket.local_address.ok_or(BrokerError::Internal)?, + ) }; let rights = session .core @@ -482,6 +695,7 @@ pub fn accept( platform_socket: Once::new(), readiness, _quota: quota, + port_reservation: Mutex::new(None), }); let accepted = match listener_resource.accept(resource.readiness.clone()) { Ok(SocketOutcome::Completed(accepted)) => accepted, @@ -495,12 +709,11 @@ pub fn accept( } }; resource.platform_socket.call_once(|| accepted.socket); - let accepted_socket = - SocketObject::new_connected(resource, create_request, accepted.local_address); + let accepted_socket = SocketObject::new_connected(resource, create_request, local_address); let handle = reference.commit(ObjectEntry::Socket(accepted_socket))?; Ok(SocketOutcome::Completed(AcceptedBrokerSocket { handle, - local_address: accepted.local_address, + local_address, remote_address: accepted.remote_address, })) } @@ -756,7 +969,7 @@ pub fn status(session: &BrokerSession, handle: ObjectHandle) -> Result Result> { + let (local_address, reservation) = + match session + .socket_ports + .reserve(create_request, requested_address, |port| { + session + .core + .socket_provider + .reserves_guest_port(create_request, port) + })? { + SocketOutcome::Completed(binding) => binding, + SocketOutcome::Failed(error) => return Ok(SocketOutcome::Failed(error)), + }; + match resource.bind(local_address, kind)? { + SocketOutcome::Completed(bound_address) if bound_address == local_address => { + Ok(SocketOutcome::Completed((local_address, reservation))) + } + // The platform must echo the broker-reserved guest-local address. + SocketOutcome::Completed(_) => Err(BrokerError::Internal), + SocketOutcome::Failed(error) => Ok(SocketOutcome::Failed(error)), + } +} + fn connect_datagram( object: &spin::RwLock, address: SocketAddrV4, @@ -940,15 +1187,31 @@ fn finish_connect(object: &spin::RwLock, status: SocketConnectionSt } } +fn attach_binding( + object: &spin::RwLock, + local_address: SocketAddrV4, + port_reservation: GuestPortReservation, +) { + let mut object = object.write(); + if let ObjectEntry::Socket(socket) = &mut *object { + socket.local_address = Some(local_address); + socket.resource.set_port_reservation(port_reservation); + } +} + fn finish_configuration( object: &spin::RwLock, local_address: Option, + port_reservation: Option, listening: bool, ) { let mut object = object.write(); if let ObjectEntry::Socket(socket) = &mut *object { socket.configuration_in_flight = false; socket.local_address = socket.local_address.or(local_address); + if let Some(port_reservation) = port_reservation { + socket.resource.set_port_reservation(port_reservation); + } socket.listening |= listening; } } @@ -1004,9 +1267,22 @@ pub(crate) struct SocketResource { platform_socket: Once>, readiness: ReadinessRegistration, _quota: Arc, + port_reservation: Mutex>, } impl SocketResource { + /// Attaches the guest port reservation owning this socket's local port. + /// + /// The resource outlives its object handle, so an operation already in + /// flight keeps the port reserved until it finishes. + fn set_port_reservation(&self, reservation: GuestPortReservation) { + let mut slot = self.port_reservation.lock(); + debug_assert!(slot.is_none()); + if slot.is_none() { + *slot = Some(reservation); + } + } + fn platform_socket(&self) -> &dyn PlatformSocket { self.platform_socket .get() @@ -1021,8 +1297,12 @@ impl SocketResource { self.platform_socket().connect(address) } - fn bind(&self, address: SocketAddrV4) -> Result> { - self.platform_socket().bind(address) + fn bind( + &self, + address: SocketAddrV4, + kind: PlatformBindKind, + ) -> Result> { + self.platform_socket().bind(address, kind) } fn listen(&self, backlog: u32) -> Result> { @@ -1170,6 +1450,54 @@ pub(crate) mod tests { state: Arc, } + #[test] + fn guest_tcp_port_namespaces_are_per_session() { + let first_session = SessionSocketPorts::default(); + let second_session = SessionSocketPorts::default(); + let address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + + let SocketOutcome::Completed((_, first_reservation)) = first_session + .reserve(create_request(), address, |_| false) + .unwrap() + else { + panic!("first TCP reservation failed"); + }; + assert!(matches!( + first_session.reserve(create_request(), address, |_| false), + Ok(SocketOutcome::Failed(SocketError::AddressInUse)) + )); + assert!(matches!( + second_session.reserve(create_request(), address, |_| false), + Ok(SocketOutcome::Completed(_)) + )); + assert!(matches!( + first_session.reserve(create_udp_request(), address, |_| false), + Err(BrokerError::Internal) + )); + + drop(first_reservation); + assert!(matches!( + first_session.reserve(create_request(), address, |_| false), + Ok(SocketOutcome::Completed(_)) + )); + } + + #[test] + fn implicit_guest_port_allocation_skips_provider_reservations() { + let ports = SessionSocketPorts::default(); + let requested_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); + let SocketOutcome::Completed((address, _reservation)) = ports + .reserve(create_request(), requested_address, |port| { + port == FIRST_EPHEMERAL_PORT + }) + .unwrap() + else { + panic!("ephemeral TCP reservation failed"); + }; + + assert_eq!(address.port(), FIRST_EPHEMERAL_PORT + 1); + } + #[derive(Default)] struct TestSocketState { creates: StdMutex>, @@ -1235,6 +1563,7 @@ pub(crate) mod tests { readiness, create_request: request, tcp_options: StdMutex::new(TestTcpOptions::default()), + guest_local_address: StdMutex::new(None), })) } @@ -1248,6 +1577,7 @@ pub(crate) mod tests { readiness: ReadinessRegistration, create_request: CreateSocketRequest, tcp_options: StdMutex, + guest_local_address: StdMutex>, } #[derive(Default)] @@ -1257,8 +1587,16 @@ pub(crate) mod tests { } impl PlatformSocket for TestPlatformSocket { - fn bind(&self, address: SocketAddrV4) -> Result> { + fn bind( + &self, + address: SocketAddrV4, + _kind: PlatformBindKind, + ) -> Result> { self.state.binds.lock().unwrap().push(address); + if is_tcp(self.create_request) { + *self.guest_local_address.lock().unwrap() = Some(address); + return Ok(SocketOutcome::Completed(address)); + } let address = if address.port() == 0 { SocketAddrV4::new(*address.ip(), 49152) } else { @@ -1274,10 +1612,12 @@ pub(crate) mod tests { started.send(()).unwrap(); release.recv_timeout(Duration::from_secs(5)).unwrap(); } - Ok(SocketOutcome::Completed(SocketAddrV4::new( - Ipv4Addr::LOCALHOST, - 49152, - ))) + let local_address = self + .guest_local_address + .lock() + .unwrap() + .ok_or(BrokerError::Internal)?; + Ok(SocketOutcome::Completed(local_address)) } fn accept( @@ -1501,7 +1841,7 @@ pub(crate) mod tests { status(&session, handle), Ok(SocketStatusResponse { status: SocketConnectionStatus::Connected, - local_address: None, + local_address: Some(SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_EPHEMERAL_PORT)), pending_error: None, }) ); @@ -1835,13 +2175,14 @@ pub(crate) mod tests { assert_eq!(broker.reserved_sockets.load(Ordering::Relaxed), 0); let auto_bound = create(&session, create_request(), readiness).unwrap(); + let auto_bound_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_EPHEMERAL_PORT + 1); assert_eq!( listen(&session, auto_bound, 0), - Ok(SocketOutcome::Completed(local_address)) + Ok(SocketOutcome::Completed(auto_bound_address)) ); assert_eq!( provider.state.binds.lock().unwrap().last(), - Some(&DEFAULT_TCP_LISTEN_ADDRESS) + Some(&auto_bound_address) ); session.close_object_reference(auto_bound).unwrap(); } @@ -2175,7 +2516,10 @@ pub(crate) mod tests { status(&session, poisoned), Ok(SocketStatusResponse { status: SocketConnectionStatus::Failed(SocketError::Other), - local_address: None, + local_address: Some(SocketAddrV4::new( + Ipv4Addr::LOCALHOST, + FIRST_EPHEMERAL_PORT + 1, + )), pending_error: None, }) ); @@ -2263,7 +2607,7 @@ pub(crate) mod tests { Ok(SocketOutcome::Completed(SocketConnectionStatus::Connecting)) ); - let local_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 49153); + let platform_local_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 49153); provider .state .status_responses @@ -2271,13 +2615,16 @@ pub(crate) mod tests { .unwrap() .push_back(SocketStatusResponse { status: SocketConnectionStatus::Failed(SocketError::TimedOut), - local_address: Some(local_address), + local_address: Some(platform_local_address), pending_error: None, }); + // The guest-local address reserved by the broker outranks any address + // the platform reports for its private host endpoint. + let guest_local_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_EPHEMERAL_PORT); let expected = SocketStatusResponse { status: SocketConnectionStatus::Failed(SocketError::TimedOut), - local_address: Some(local_address), + local_address: Some(guest_local_address), pending_error: None, }; assert_eq!(status(&session, handle), Ok(expected)); diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 575cad89e..32d41f837 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -766,6 +766,7 @@ mod tests { Ok(Arc::new(TestPlatformSocket { readiness, create_request: request, + local_address: std::sync::Mutex::new(None), })) } @@ -775,18 +776,21 @@ mod tests { struct TestPlatformSocket { readiness: ReadinessRegistration, create_request: CreateSocketRequest, + local_address: std::sync::Mutex>, } impl PlatformSocket for TestPlatformSocket { fn bind( &self, address: SocketAddrV4, + _kind: litebox_broker_core::socket::PlatformBindKind, ) -> litebox_broker_core::Result> { let address = if address.port() == 0 { SocketAddrV4::new(*address.ip(), 49152) } else { address }; + *self.local_address.lock().unwrap() = Some(address); Ok(SocketOutcome::Completed(address)) } @@ -794,10 +798,12 @@ mod tests { &self, _backlog: u32, ) -> litebox_broker_core::Result> { - Ok(SocketOutcome::Completed(SocketAddrV4::new( - Ipv4Addr::LOCALHOST, - 49152, - ))) + let local_address = self + .local_address + .lock() + .unwrap() + .ok_or(litebox_broker_core::BrokerError::Internal)?; + Ok(SocketOutcome::Completed(local_address)) } fn accept( @@ -1363,7 +1369,7 @@ mod tests { ), BrokerResult::Socket(SocketResponse::Status(SocketStatusResponse { status: SocketConnectionStatus::Connected, - local_address: None, + local_address: Some(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 49152)), pending_error: None, })) ); diff --git a/litebox_broker_platform_linux_userland/src/lib.rs b/litebox_broker_platform_linux_userland/src/lib.rs index 7be7622a8..62f4b357b 100644 --- a/litebox_broker_platform_linux_userland/src/lib.rs +++ b/litebox_broker_platform_linux_userland/src/lib.rs @@ -11,4 +11,4 @@ mod socket; -pub use socket::LinuxSocketProvider; +pub use socket::{LinuxSocketProvider, SocketPortMapping}; diff --git a/litebox_broker_platform_linux_userland/src/socket.rs b/litebox_broker_platform_linux_userland/src/socket.rs index 184929c06..bdda3d291 100644 --- a/litebox_broker_platform_linux_userland/src/socket.rs +++ b/litebox_broker_platform_linux_userland/src/socket.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::fmt; use std::io::{Error, ErrorKind, Result as IoResult}; use std::mem::size_of; -use std::net::SocketAddrV4; +use std::net::{Ipv4Addr, SocketAddrV4}; use std::os::fd::OwnedFd; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError, sync_channel}; @@ -16,8 +16,8 @@ use std::thread::{self, JoinHandle}; use std::time::Duration; use litebox_broker_core::socket::{ - AcceptedPlatformSocket, PlatformConnectError, PlatformDatagramReceive, PlatformSocket, - PlatformStreamReceive, SocketProvider, + AcceptedPlatformSocket, PlatformBindKind, PlatformConnectError, PlatformDatagramReceive, + PlatformSocket, PlatformStreamReceive, SocketProvider, }; use litebox_broker_core::{BrokerError, Result as BrokerResult, SessionId}; use litebox_broker_protocol::readiness::ReadinessFlags; @@ -43,6 +43,8 @@ use litebox_broker_core::readiness::ReadinessRegistration; const WAKE_TOKEN: u64 = 0; const MAX_QUEUED_SOCKET_COMMANDS: usize = 64; const MAX_EPOLL_EVENTS: usize = 64; +const MAX_STALE_PUBLICATION_ITEMS: usize = 64; +const MAX_RETAINED_TRANSLATIONS: usize = 1 << 14; /// Linux-userland socket provider. /// @@ -51,21 +53,94 @@ const MAX_EPOLL_EVENTS: usize = 64; /// immediate nonblocking operation, never for network readiness. pub struct LinuxSocketProvider { reactor: Arc, + port_mappings: Vec, +} + +/// Explicit mapping from one host TCP endpoint to a guest-local TCP port. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SocketPortMapping { + /// Guest-local TCP port visible inside one broker session. + pub guest_port: u16, + /// Host TCP endpoint mapped to the guest port. + pub host_address: SocketAddrV4, +} + +struct PortMappingState { + mapping: SocketPortMapping, + reservation: Option, + claimed_by: Option, } impl LinuxSocketProvider { /// Starts a provider whose reactor tracks at most `max_sockets` resources. pub fn new(max_sockets: usize) -> IoResult { + Self::new_with_port_mappings(max_sockets, &[]) + } + + /// Starts a provider and reserves every mapped host endpoint immediately. + pub fn new_with_port_mappings( + max_sockets: usize, + port_mappings: &[SocketPortMapping], + ) -> IoResult { + for (index, mapping) in port_mappings.iter().enumerate() { + if mapping.guest_port == 0 || mapping.host_address.port() == 0 { + return Err(Error::new( + ErrorKind::InvalidInput, + "mapped guest and host ports must be nonzero", + )); + } + if port_mappings[..index].iter().any(|existing| { + existing.guest_port == mapping.guest_port + || existing.host_address == mapping.host_address + }) { + return Err(Error::new( + ErrorKind::InvalidInput, + "mapped guest ports and host endpoints must be unique", + )); + } + } + let port_mappings = port_mappings.to_vec(); Ok(Self { - reactor: Arc::new(ReactorClient::start(max_sockets)?), + reactor: Arc::new(ReactorClient::start(max_sockets, port_mappings.clone())?), + port_mappings, }) } } +fn create_port_mapping_reservation( + mapping: SocketPortMapping, + reuse_address: bool, + reuse_port: bool, +) -> core::result::Result { + let socket = socket_with( + LinuxAddressFamily::INET, + LinuxSocketType::STREAM, + LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, + Some(ipproto::TCP), + )?; + if reuse_address { + sockopt::set_socket_reuseaddr(&socket, true)?; + } + if reuse_port { + sockopt::set_socket_reuseport(&socket, true)?; + } + bind(&socket, &mapping.host_address)?; + Ok(socket) +} + impl SocketProvider for LinuxSocketProvider { + fn reserves_guest_port(&self, request: CreateSocketRequest, port: u16) -> bool { + if socket_kind(request) != Some(SocketKind::Tcp) { + return false; + } + self.port_mappings + .iter() + .any(|mapping| mapping.guest_port == port) + } + fn create( &self, - _session_id: SessionId, + session_id: SessionId, request: CreateSocketRequest, readiness: ReadinessRegistration, ) -> BrokerResult> { @@ -86,6 +161,7 @@ impl SocketProvider for LinuxSocketProvider { }); self.reactor.request(|response| ReactorCommand::Create { id, + session_id, request, readiness, snapshot, @@ -95,7 +171,9 @@ impl SocketProvider for LinuxSocketProvider { Ok(socket) } - fn close_session(&self, _session_id: SessionId) {} + fn close_session(&self, session_id: SessionId) { + self.reactor.close_session(session_id); + } } /// Broker-core-facing handle for a reactor-owned socket. @@ -110,10 +188,15 @@ struct LinuxSocket { } impl PlatformSocket for LinuxSocket { - fn bind(&self, address: SocketAddrV4) -> BrokerResult> { + fn bind( + &self, + address: SocketAddrV4, + kind: PlatformBindKind, + ) -> BrokerResult> { self.reactor.request(|response| ReactorCommand::Bind { id: self.id, address, + kind, response, }) } @@ -150,7 +233,6 @@ impl PlatformSocket for LinuxSocket { SocketOutcome::Completed(accepted) => { Ok(SocketOutcome::Completed(AcceptedPlatformSocket { socket, - local_address: accepted.local_address, remote_address: accepted.remote_address, })) } @@ -300,9 +382,19 @@ struct ReactorClient { } impl ReactorClient { - fn start(max_sockets: usize) -> IoResult { + fn start(max_sockets: usize, port_mappings: Vec) -> IoResult { let epoll_fd = epoll::create(epoll::CreateFlags::CLOEXEC)?; let wake = Arc::new(eventfd(0, EventfdFlags::CLOEXEC | EventfdFlags::NONBLOCK)?); + let port_mappings = port_mappings + .into_iter() + .map(|mapping| { + Ok(PortMappingState { + mapping, + reservation: Some(create_port_mapping_reservation(mapping, false, false)?), + claimed_by: None, + }) + }) + .collect::>>()?; epoll::add( &epoll_fd, wake.as_ref(), @@ -330,6 +422,8 @@ impl ReactorClient { wake: reactor_wake, commands: receiver, sockets, + sessions: HashMap::new(), + port_mappings, max_sockets, peek_cache: None, events, @@ -432,6 +526,36 @@ impl ReactorClient { let _ = receive.recv(); } + #[cfg(test)] + fn host_address(&self, kind: SocketKind, guest_port: u16) -> Option { + let (response, receive) = sync_channel(1); + self.commands + .send(ReactorCommand::HostAddress { + kind, + guest_port, + response, + }) + .unwrap(); + self.signal().unwrap(); + receive.recv().unwrap() + } + + fn close_session(&self, session_id: SessionId) { + let (response, receive) = sync_channel(1); + if self + .commands + .send(ReactorCommand::CloseSession { + session_id, + response, + }) + .is_err() + { + return; + } + let _ = self.signal(); + let _ = receive.recv(); + } + fn signal(&self) -> IoResult<()> { let value = 1_u64.to_ne_bytes(); loop { @@ -479,6 +603,7 @@ impl Drop for ReactorClient { enum ReactorCommand { Create { id: u64, + session_id: SessionId, request: CreateSocketRequest, readiness: ReadinessRegistration, snapshot: Arc>, @@ -493,6 +618,7 @@ enum ReactorCommand { Bind { id: u64, address: SocketAddrV4, + kind: PlatformBindKind, response: SyncSender>>, }, Listen { @@ -556,6 +682,16 @@ enum ReactorCommand { id: u64, response: SyncSender<()>, }, + CloseSession { + session_id: SessionId, + response: SyncSender<()>, + }, + #[cfg(test)] + HostAddress { + kind: SocketKind, + guest_port: u16, + response: SyncSender>, + }, Stop { response: SyncSender<()>, }, @@ -578,7 +714,6 @@ enum ReactorReceiveFromOutcome { } struct AcceptedEndpoints { - local_address: SocketAddrV4, remote_address: SocketAddrV4, } @@ -588,6 +723,8 @@ struct Reactor { wake: Arc, commands: Receiver, sockets: HashMap, + sessions: HashMap, + port_mappings: Vec, max_sockets: usize, peek_cache: Option, events: Vec, @@ -600,8 +737,10 @@ struct PeekCache { } /// Reactor-owned descriptor and its broker-facing readiness state. +#[allow(clippy::struct_excessive_bools)] // These are independent cached kernel attributes. struct SocketEntry { socket: OwnedFd, + session_id: SessionId, kind: SocketKind, readiness: ReadinessRegistration, snapshot: Arc>, @@ -609,6 +748,31 @@ struct SocketEntry { write_shutdown: bool, peek_waitall_threshold: Option, listening: bool, + guest_local_address: Option, + port_mapping_index: Option, + retain_port_mapping_on_close: bool, + tcp_no_delay: bool, + tcp_keep_alive: bool, +} + +/// Guest-visible TCP endpoints owned by one broker session. +#[derive(Default)] +struct SessionSocketNamespace { + tcp: HashMap, + // Pending accepts can outlive the connecting socket, so a translation is + // retained separately from its guest port binding until accept consumes it. + tcp_translations: HashMap<(SocketAddrV4, SocketAddrV4), SocketAddrV4>, + live_sockets: usize, + closing: bool, +} + +#[derive(Clone, Copy)] +struct GuestPortBinding { + socket_id: u64, + guest_address: SocketAddrV4, + host_address: Option, + host_peer_address: Option, + host_mapped: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -617,6 +781,148 @@ enum SocketKind { Udp, } +impl SessionSocketNamespace { + /// Returns the guest port bindings tracked for `kind`. + /// + /// Only TCP endpoints live in the guest namespace; UDP sockets remain + /// backed directly by a host endpoint. + fn bindings(&self, kind: SocketKind) -> Option<&HashMap> { + match kind { + SocketKind::Tcp => Some(&self.tcp), + SocketKind::Udp => None, + } + } + + fn insert_binding( + &mut self, + kind: SocketKind, + port: u16, + binding: GuestPortBinding, + ) -> BrokerResult<()> { + if kind != SocketKind::Tcp { + return Err(BrokerError::Internal); + } + let translation = binding + .host_address + .zip(binding.host_peer_address) + .map(|connection| (connection, binding.guest_address)); + if self.tcp.insert(port, binding).is_some() { + return Err(BrokerError::Internal); + } + if let Some((connection, guest_address)) = translation { + self.tcp_translations.insert(connection, guest_address); + } + Ok(()) + } + + fn reserve_binding(&mut self, kind: SocketKind) -> BrokerResult<()> { + if kind != SocketKind::Tcp { + return Err(BrokerError::Internal); + } + if self.tcp_translations.len() >= MAX_RETAINED_TRANSLATIONS { + return Err(BrokerError::ResourceExhausted); + } + self.tcp + .try_reserve(1) + .map_err(|_| BrokerError::OutOfMemory)?; + self.tcp_translations + .try_reserve(1) + .map_err(|_| BrokerError::OutOfMemory) + } + + fn remove_binding(&mut self, kind: SocketKind, port: u16, socket_id: u64) { + if kind != SocketKind::Tcp { + return; + } + if self + .tcp + .get(&port) + .is_some_and(|binding| binding.socket_id == socket_id) + { + self.tcp.remove(&port); + } + } + + fn guest_binding(&self, kind: SocketKind, address: SocketAddrV4) -> Option { + if !address.ip().is_loopback() { + return None; + } + let binding = self.bindings(kind)?.get(&address.port())?; + if binding.guest_address.ip().is_unspecified() || binding.guest_address.ip() == address.ip() + { + Some(*binding) + } else { + None + } + } + + fn set_host_address( + &mut self, + kind: SocketKind, + port: u16, + socket_id: u64, + host_address: SocketAddrV4, + host_mapped: bool, + ) -> BrokerResult<()> { + if kind != SocketKind::Tcp { + return Err(BrokerError::Internal); + } + let binding = self.tcp.get_mut(&port).ok_or(BrokerError::Internal)?; + if binding.socket_id != socket_id { + return Err(BrokerError::Internal); + } + binding.host_address = Some(host_address); + binding.host_mapped = host_mapped; + Ok(()) + } + + fn set_host_peer_address( + &mut self, + kind: SocketKind, + port: u16, + socket_id: u64, + host_peer_address: SocketAddrV4, + ) -> BrokerResult<()> { + if kind != SocketKind::Tcp { + return Err(BrokerError::Internal); + } + let (host_address, guest_address) = { + let binding = self.tcp.get_mut(&port).ok_or(BrokerError::Internal)?; + if binding.socket_id != socket_id { + return Err(BrokerError::Internal); + } + binding.host_peer_address = Some(host_peer_address); + ( + binding.host_address.ok_or(BrokerError::Internal)?, + binding.guest_address, + ) + }; + self.tcp_translations + .insert((host_address, host_peer_address), guest_address); + Ok(()) + } + + /// Maps one accepted host connection back to its guest-local source address. + /// + /// The complete host connection tuple is required because several guest + /// sockets may share one private host endpoint. + fn translate_tcp_peer( + &mut self, + remote_address: SocketAddrV4, + local_address: SocketAddrV4, + ) -> SocketAddrV4 { + self.tcp_translations + .remove(&(remote_address, local_address)) + .or_else(|| { + self.tcp_translations.remove(&( + remote_address, + SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, local_address.port()), + )) + }) + .unwrap_or(remote_address) + } +} + /// Cached connection and readiness state shared with the broker-facing handle. /// /// The reactor updates this snapshot whenever kernel state changes, allowing @@ -660,6 +966,489 @@ impl fmt::Display for ReactorFailure { } impl Reactor { + fn port_mapping_index(&self, kind: SocketKind, guest_port: u16) -> Option { + if kind != SocketKind::Tcp { + return None; + } + self.port_mappings + .iter() + .position(|state| state.mapping.guest_port == guest_port) + } + + /// Transfers one reserved host endpoint to the socket publishing it. + /// + /// The reservation descriptor keeps the mapped endpoint exclusively owned + /// between publications, so a claim replaces the socket's descriptor rather + /// than binding the endpoint again. + fn claim_port_mapping( + &mut self, + id: u64, + mapping_index: usize, + ) -> BrokerResult> { + let mapping = self + .port_mappings + .get(mapping_index) + .ok_or(BrokerError::Internal)? + .mapping; + if self + .port_mappings + .get(mapping_index) + .is_some_and(|state| state.claimed_by.is_none() && state.reservation.is_none()) + { + match create_port_mapping_reservation(mapping, true, false) { + Ok(reservation) => { + self.port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)? + .reservation = Some(reservation); + } + Err(error) => { + return Ok(SocketOutcome::Failed(socket_operation_error_from_errno( + error, + )?)); + } + } + } + let reservation = { + let state = self + .port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)?; + if state.claimed_by.is_some() { + return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); + } + let Some(reservation) = state.reservation.take() else { + return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); + }; + reservation + }; + let preparation = (|| { + sockopt::set_socket_reuseaddr(&reservation, true).map_err(broker_error_from_errno)?; + sockopt::set_socket_linger(&reservation, None).map_err(broker_error_from_errno)?; + drain_tcp_listener(&reservation) + })(); + let stale_state_drained = match preparation { + Ok(drained) => drained, + Err(error) => { + self.port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)? + .reservation = Some(reservation); + return Err(error); + } + }; + if !stale_state_drained { + self.port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)? + .reservation = Some(reservation); + return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); + } + let host_address = match self.replace_socket_descriptor(id, reservation) { + Ok(address) => address, + Err((error, reservation)) => { + self.port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)? + .reservation = Some(reservation); + return Err(error); + } + }; + self.port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)? + .claimed_by = Some(id); + Ok(SocketOutcome::Completed(host_address)) + } + + /// Replaces a socket's descriptor, returning ownership of the replacement on failure. + fn replace_socket_descriptor( + &mut self, + id: u64, + replacement: OwnedFd, + ) -> core::result::Result { + let host_address = match local_socket_address(&replacement) { + Ok(address) => address, + Err(error) => return Err((error, replacement)), + }; + let Some(socket) = self.sockets.get_mut(&id) else { + return Err((BrokerError::Internal, replacement)); + }; + if socket.kind == SocketKind::Tcp + && let Err(error) = + apply_tcp_options(&replacement, socket.tcp_no_delay, socket.tcp_keep_alive) + { + return Err((error, replacement)); + } + let events = if socket.kind == SocketKind::Tcp { + idle_epoll_events() + } else { + active_epoll_events() + }; + if let Err(error) = epoll::add( + &self.epoll, + &replacement, + epoll::EventData::new_u64(id), + events, + ) { + return Err((broker_error_from_errno(error), replacement)); + } + if let Err(error) = epoll::delete(&self.epoll, &socket.socket) { + let _ = epoll::delete(&self.epoll, &replacement); + return Err((broker_error_from_errno(error), replacement)); + } + let old_socket = core::mem::replace(&mut socket.socket, replacement); + drop(old_socket); + Ok(host_address) + } + + /// Returns whether `address` names a host endpoint backing a guest socket. + /// + /// Private backend endpoints are an implementation detail of the guest + /// namespace and must not be reachable as guest destinations. + fn is_private_host_endpoint( + &self, + kind: SocketKind, + address: SocketAddrV4, + ) -> BrokerResult { + for binding in self + .sessions + .values() + .filter_map(|namespace| namespace.bindings(kind)) + .flat_map(HashMap::values) + { + let Some(host_address) = binding.host_address else { + continue; + }; + if binding.host_mapped || host_address.port() != address.port() { + continue; + } + if host_address.ip() == address.ip() + || (host_address.ip().is_unspecified() + && host_ipv4_address_is_local(*address.ip())?) + { + return Ok(true); + } + } + Ok(false) + } + + /// Resolves a guest destination to the host endpoint that should receive it. + fn resolve_guest_destination( + &self, + session_id: SessionId, + kind: SocketKind, + mut address: SocketAddrV4, + ) -> BrokerResult> { + if address.ip().is_unspecified() { + address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, address.port()); + } + if let Some(binding) = self + .sessions + .get(&session_id) + .and_then(|namespace| namespace.guest_binding(kind, address)) + { + return match binding.host_address { + Some(host_address) if host_address.ip().is_unspecified() => { + Ok(SocketOutcome::Completed(( + SocketAddrV4::new(*address.ip(), host_address.port()), + true, + ))) + } + Some(host_address) => Ok(SocketOutcome::Completed((host_address, true))), + None => Ok(SocketOutcome::Failed(SocketError::ConnectionRefused)), + }; + } + if self.is_private_host_endpoint(kind, address)? { + Ok(SocketOutcome::Failed(SocketError::ConnectionRefused)) + } else { + Ok(SocketOutcome::Completed((address, false))) + } + } + + /// Binds a socket to its guest-local address. + /// + /// A stream socket reserves a guest port without consuming a host port; the + /// host endpoint is chosen when the socket listens or connects. A datagram + /// socket is still bound directly to the requested host endpoint. + fn bind_socket( + &mut self, + id: u64, + requested_address: SocketAddrV4, + bind_kind: PlatformBindKind, + ) -> BrokerResult> { + let (session_id, kind, already_bound) = { + let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; + ( + socket.session_id, + socket.kind, + socket.guest_local_address.is_some(), + ) + }; + if kind != SocketKind::Tcp { + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + return match bind_host_socket(socket, requested_address)? { + SocketOutcome::Completed(local_address) => { + socket + .snapshot + .lock() + .expect("Linux socket snapshot mutex poisoned") + .local_address = Some(local_address); + Ok(SocketOutcome::Completed(local_address)) + } + SocketOutcome::Failed(error) => Ok(SocketOutcome::Failed(error)), + }; + } + if already_bound { + return Ok(SocketOutcome::Failed(SocketError::InvalidArgument)); + } + let guest_port = requested_address.port(); + if guest_port == 0 { + return Err(BrokerError::Internal); + } + if self + .sessions + .get(&session_id) + .ok_or(BrokerError::Internal)? + .tcp + .contains_key(&guest_port) + { + return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); + } + self.sessions + .get_mut(&session_id) + .ok_or(BrokerError::Internal)? + .reserve_binding(kind)?; + let guest_address = SocketAddrV4::new(*requested_address.ip(), guest_port); + let port_mapping_index = (bind_kind == PlatformBindKind::Explicit) + .then(|| self.port_mapping_index(kind, guest_port)) + .flatten(); + self.sessions + .get_mut(&session_id) + .ok_or(BrokerError::Internal)? + .insert_binding( + kind, + guest_port, + GuestPortBinding { + socket_id: id, + guest_address, + host_address: None, + host_peer_address: None, + host_mapped: false, + }, + )?; + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + socket.guest_local_address = Some(guest_address); + socket.port_mapping_index = port_mapping_index; + socket + .snapshot + .lock() + .expect("Linux socket snapshot mutex poisoned") + .local_address = Some(guest_address); + Ok(SocketOutcome::Completed(guest_address)) + } + + fn listen_socket( + &mut self, + id: u64, + backlog: u32, + ) -> BrokerResult> { + let (session_id, kind, guest_address) = self + .sockets + .get(&id) + .map(|socket| (socket.session_id, socket.kind, socket.guest_local_address)) + .ok_or(BrokerError::Internal)?; + if kind != SocketKind::Tcp { + return Ok(SocketOutcome::Failed(SocketError::InvalidArgument)); + } + let guest_address = guest_address.ok_or(BrokerError::Internal)?; + let needs_host_bind = + local_socket_address(&self.sockets.get(&id).ok_or(BrokerError::Internal)?.socket)? + .port() + == 0; + if needs_host_bind { + let port_mapping_index = self + .sockets + .get(&id) + .ok_or(BrokerError::Internal)? + .port_mapping_index; + let (host_address, host_mapped) = if let Some(mapping_index) = port_mapping_index { + match self.claim_port_mapping(id, mapping_index)? { + SocketOutcome::Completed(address) => (address, true), + SocketOutcome::Failed(error) => return Ok(SocketOutcome::Failed(error)), + } + } else { + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + let address = + match bind_host_socket(socket, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))? { + SocketOutcome::Completed(address) => address, + SocketOutcome::Failed(error) => return Ok(SocketOutcome::Failed(error)), + }; + (address, false) + }; + self.sessions + .get_mut(&session_id) + .ok_or(BrokerError::Internal)? + .set_host_address(kind, guest_address.port(), id, host_address, host_mapped)?; + } + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + match listen_tcp_socket(&self.epoll, id, socket, backlog)? { + SocketOutcome::Completed(()) => Ok(SocketOutcome::Completed(guest_address)), + SocketOutcome::Failed(error) => Ok(SocketOutcome::Failed(error)), + } + } + + fn connect_socket( + &mut self, + id: u64, + address: SocketAddrV4, + ) -> core::result::Result { + let kind = self.sockets.get(&id).map(|socket| socket.kind).ok_or( + PlatformConnectError::PeerIndeterminate(BrokerError::Internal), + )?; + if kind != SocketKind::Tcp { + let socket = + self.sockets + .get_mut(&id) + .ok_or(PlatformConnectError::PeerIndeterminate( + BrokerError::Internal, + ))?; + return connect_datagram_socket(socket, address); + } + self.connect_guest_tcp_socket(id, address) + } + + fn connect_guest_tcp_socket( + &mut self, + id: u64, + guest_address: SocketAddrV4, + ) -> core::result::Result { + let session_id = { + let socket = self + .sockets + .get(&id) + .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))?; + if socket.guest_local_address.is_none() { + return Err(PlatformConnectError::PeerUnchanged(BrokerError::Internal)); + } + socket.session_id + }; + let (network_address, guest_routed) = match self + .resolve_guest_destination(session_id, SocketKind::Tcp, guest_address) + .map_err(PlatformConnectError::PeerUnchanged)? + { + SocketOutcome::Completed(destination) => destination, + SocketOutcome::Failed(error) => return Ok(SocketConnectionStatus::Failed(error)), + }; + let outcome = { + let socket = self + .sockets + .get_mut(&id) + .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))?; + connect_tcp_socket(&self.epoll, id, socket, network_address)? + }; + if matches!( + outcome, + SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected + ) { + let (local_guest_address, host_address) = + { + let socket = + self.sockets + .get(&id) + .ok_or(PlatformConnectError::PeerIndeterminate( + BrokerError::Internal, + ))?; + ( + socket.guest_local_address.ok_or( + PlatformConnectError::PeerIndeterminate(BrokerError::Internal), + )?, + local_socket_address(&socket.socket) + .map_err(PlatformConnectError::PeerIndeterminate)?, + ) + }; + let namespace = self.sessions.get_mut(&session_id).ok_or( + PlatformConnectError::PeerIndeterminate(BrokerError::Internal), + )?; + namespace + .set_host_address( + SocketKind::Tcp, + local_guest_address.port(), + id, + host_address, + false, + ) + .map_err(PlatformConnectError::PeerIndeterminate)?; + if guest_routed { + namespace + .set_host_peer_address( + SocketKind::Tcp, + local_guest_address.port(), + id, + network_address, + ) + .map_err(PlatformConnectError::PeerIndeterminate)?; + } + } + Ok(outcome) + } + + /// Drops a socket, releasing its guest port and any claimed host mapping. + fn remove_socket(&mut self, id: u64) { + let port_mapping = self + .sockets + .get(&id) + .and_then(|socket| socket.port_mapping_index) + .filter(|mapping_index| { + self.port_mappings + .get(*mapping_index) + .is_some_and(|state| state.claimed_by == Some(id)) + }) + .map(|mapping_index| (mapping_index, self.port_mappings[mapping_index].mapping)); + // A retired listener that stopped listening cannot be reused as a + // reservation, so its descriptor is dropped and the endpoint rebound. + let retain_original = port_mapping.is_some_and(|_| { + self.sockets.get(&id).is_some_and(|socket| { + socket.retain_port_mapping_on_close + && delete_epoll_registration(&self.epoll, &socket.socket) + }) + }); + let Some(socket) = self.sockets.remove(&id) else { + return; + }; + let session_id = socket.session_id; + let remove_namespace = if let Some(namespace) = self.sessions.get_mut(&session_id) { + if let Some(address) = socket.guest_local_address { + namespace.remove_binding(socket.kind, address.port(), id); + } + namespace.live_sockets = namespace + .live_sockets + .checked_sub(1) + .expect("session socket count underflow"); + namespace.closing && namespace.live_sockets == 0 + } else { + false + }; + if remove_namespace { + self.sessions.remove(&session_id); + } + let replacement_reservation = if retain_original { + let SocketEntry { socket, .. } = socket; + Some(socket) + } else { + drop(socket); + port_mapping + .and_then(|(_, mapping)| create_port_mapping_reservation(mapping, true, false).ok()) + }; + if let Some((mapping_index, _)) = port_mapping + && let Some(state) = self.port_mappings.get_mut(mapping_index) + && state.claimed_by == Some(id) + { + state.claimed_by = None; + state.reservation = replacement_reservation; + } + } + fn run(&mut self) -> core::result::Result<(), ReactorFailure> { loop { let mut events = core::mem::take(&mut self.events); @@ -717,19 +1506,20 @@ impl Reactor { match command { ReactorCommand::Create { id, + session_id, request, readiness, snapshot, active, response, } => { - let outcome = self.create_socket(id, request, readiness, snapshot); + let outcome = self.create_socket(id, session_id, request, readiness, snapshot); let created = outcome.is_ok(); if created { active.store(true, Ordering::Release); } if response.send(outcome).is_err() && created { - self.sockets.remove(&id); + self.remove_socket(id); } } ReactorCommand::Connect { @@ -737,24 +1527,16 @@ impl Reactor { address, response, } => { - let outcome = match self.sockets.get_mut(&id) { - Some(socket) => connect_socket(&self.epoll, id, socket, address), - None => Err(PlatformConnectError::PeerIndeterminate( - BrokerError::Internal, - )), - }; + let outcome = self.connect_socket(id, address); let _ = response.send(outcome); } ReactorCommand::Bind { id, address, + kind, response, } => { - let outcome = self - .sockets - .get_mut(&id) - .ok_or(BrokerError::Internal) - .and_then(|socket| bind_socket(socket, address)); + let outcome = self.bind_socket(id, address, kind); let _ = response.send(outcome); } ReactorCommand::Listen { @@ -762,11 +1544,7 @@ impl Reactor { backlog, response, } => { - let outcome = self - .sockets - .get_mut(&id) - .ok_or(BrokerError::Internal) - .and_then(|socket| listen_socket(&self.epoll, id, socket, backlog)); + let outcome = self.listen_socket(id, backlog); let _ = response.send(outcome); } ReactorCommand::Accept { @@ -786,7 +1564,7 @@ impl Reactor { active.store(true, Ordering::Release); } if response.send(outcome).is_err() && accepted { - self.sockets.remove(&accepted_id); + self.remove_socket(accepted_id); } } ReactorCommand::Send { id, data, response } => { @@ -867,7 +1645,7 @@ impl Reactor { } => { let outcome = self .sockets - .get(&id) + .get_mut(&id) .ok_or(BrokerError::Internal) .and_then(|socket| set_tcp_option(socket, value)); let _ = response.send(outcome); @@ -896,11 +1674,38 @@ impl Reactor { { self.peek_cache = None; } - self.sockets.remove(&id); + self.remove_socket(id); + let _ = response.send(()); + } + ReactorCommand::CloseSession { + session_id, + response, + } => { + if let Some(namespace) = self.sessions.get_mut(&session_id) { + namespace.closing = true; + if namespace.live_sockets == 0 { + self.sessions.remove(&session_id); + } + } let _ = response.send(()); } + #[cfg(test)] + ReactorCommand::HostAddress { + kind, + guest_port, + response, + } => { + let host_address = self.sessions.values().find_map(|namespace| { + namespace + .bindings(kind)? + .get(&guest_port) + .and_then(|binding| binding.host_address) + }); + let _ = response.send(host_address); + } ReactorCommand::Stop { response } => { self.sockets.clear(); + self.sessions.clear(); let _ = response.send(()); return true; } @@ -912,6 +1717,7 @@ impl Reactor { fn create_socket( &mut self, id: u64, + session_id: SessionId, request: CreateSocketRequest, readiness: ReadinessRegistration, snapshot: Arc>, @@ -923,6 +1729,10 @@ impl Reactor { if self.sockets.contains_key(&id) { return Err(BrokerError::Internal); } + let namespace = self.sessions.entry(session_id).or_default(); + if namespace.closing { + return Err(BrokerError::UnknownObject); + } let (linux_type, protocol, epoll_events, initial_readiness) = match kind { SocketKind::Tcp => ( LinuxSocketType::STREAM, @@ -962,6 +1772,7 @@ impl Reactor { id, SocketEntry { socket, + session_id, kind, readiness, snapshot, @@ -969,8 +1780,17 @@ impl Reactor { write_shutdown: false, peek_waitall_threshold: None, listening: false, + guest_local_address: None, + port_mapping_index: None, + retain_port_mapping_on_close: true, + tcp_no_delay: false, + tcp_keep_alive: false, }, ); + namespace.live_sockets = namespace + .live_sockets + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted)?; Ok(()) } @@ -994,6 +1814,12 @@ impl Reactor { if listener.kind != SocketKind::Tcp || !listener.listening { return Ok(SocketOutcome::Failed(SocketError::NotConnected)); } + let listener_session_id = listener.session_id; + let listener_tcp_no_delay = listener.tcp_no_delay; + let listener_tcp_keep_alive = listener.tcp_keep_alive; + // The accepted connection inherits the listener's trusted guest-local + // address rather than the private host endpoint behind it. + let local_address = listener.guest_local_address.ok_or(BrokerError::Internal)?; let (socket, remote_address) = loop { match acceptfrom_with( &listener.socket, @@ -1030,7 +1856,13 @@ impl Reactor { } let remote_address = SocketAddrV4::try_from(remote_address.ok_or(BrokerError::Internal)?) .map_err(|_| BrokerError::Internal)?; - let local_address = local_socket_address(&socket)?; + let host_local_address = local_socket_address(&socket)?; + let remote_address = self + .sessions + .get_mut(&listener_session_id) + .map_or(remote_address, |namespace| { + namespace.translate_tcp_peer(remote_address, host_local_address) + }); epoll::add( &self.epoll, &socket, @@ -1051,6 +1883,7 @@ impl Reactor { accepted_id, SocketEntry { socket, + session_id: listener_session_id, kind: SocketKind::Tcp, readiness, snapshot, @@ -1058,10 +1891,22 @@ impl Reactor { write_shutdown: false, peek_waitall_threshold: None, listening: false, + guest_local_address: Some(local_address), + port_mapping_index: None, + retain_port_mapping_on_close: true, + tcp_no_delay: listener_tcp_no_delay, + tcp_keep_alive: listener_tcp_keep_alive, }, ); + let namespace = self + .sessions + .get_mut(&listener_session_id) + .ok_or(BrokerError::Internal)?; + namespace.live_sockets = namespace + .live_sockets + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted)?; Ok(SocketOutcome::Completed(AcceptedEndpoints { - local_address, remote_address, })) } @@ -1081,18 +1926,16 @@ impl Reactor { let _ = socket.readiness.publish(ReadinessFlags::ERROR); } self.sockets.clear(); + self.sessions.clear(); } } -fn connect_socket( +fn connect_tcp_socket( epoll_fd: &OwnedFd, id: u64, socket: &mut SocketEntry, address: SocketAddrV4, ) -> core::result::Result { - if socket.kind == SocketKind::Udp { - return connect_datagram_socket(socket, address); - } if let Err(error) = epoll::modify( epoll_fd, &socket.socket, @@ -1129,13 +1972,13 @@ fn connect_socket( }; let readiness = match status { SocketConnectionStatus::Connected | SocketConnectionStatus::Connecting => { - let local_address = local_socket_address(&socket.socket) - .map_err(PlatformConnectError::PeerIndeterminate)?; - socket - .snapshot - .lock() - .expect("Linux socket snapshot mutex poisoned") - .local_address = Some(local_address); + // The guest-local address is assigned by bind; the private host + // endpoint behind it is never reported to the guest. + if socket.guest_local_address.is_none() { + return Err(PlatformConnectError::PeerIndeterminate( + BrokerError::Internal, + )); + } if status == SocketConnectionStatus::Connected { ReadinessFlags::WRITE } else { @@ -1194,7 +2037,7 @@ fn connect_datagram_socket( } } -fn bind_socket( +fn bind_host_socket( socket: &mut SocketEntry, address: SocketAddrV4, ) -> BrokerResult> { @@ -1202,11 +2045,6 @@ fn bind_socket( match bind(&socket.socket, &address) { Ok(()) => { let local_address = local_socket_address(&socket.socket)?; - socket - .snapshot - .lock() - .expect("Linux socket snapshot mutex poisoned") - .local_address = Some(local_address); return Ok(SocketOutcome::Completed(local_address)); } Err(Errno::INTR) => {} @@ -1219,12 +2057,59 @@ fn bind_socket( } } -fn listen_socket( +fn delete_epoll_registration(epoll_fd: &OwnedFd, socket: &OwnedFd) -> bool { + loop { + match epoll::delete(epoll_fd, socket) { + Ok(()) | Err(Errno::NOENT) => return true, + Err(Errno::INTR) => {} + Err(_) => return false, + } + } +} + +/// Returns whether a host IPv4 address names one of this machine's interfaces. +fn host_ipv4_address_is_local(address: Ipv4Addr) -> BrokerResult { + let socket = socket_with( + LinuxAddressFamily::INET, + LinuxSocketType::DGRAM, + LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, + Some(ipproto::UDP), + ) + .map_err(broker_error_from_errno)?; + match bind(&socket, &SocketAddrV4::new(address, 0)) { + Ok(()) => Ok(true), + Err(Errno::ADDRNOTAVAIL) => Ok(false), + Err(error) => Err(broker_error_from_errno(error)), + } +} + +/// Discards connections queued on a reserved host endpoint before publication. +fn drain_tcp_listener(socket: &OwnedFd) -> BrokerResult { + for _ in 0..MAX_STALE_PUBLICATION_ITEMS { + match acceptfrom_with( + socket, + LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, + ) { + Ok((accepted, _)) => drop(accepted), + Err(Errno::INTR) => {} + Err(Errno::AGAIN | Errno::INVAL) => return Ok(true), + Err(error) => { + // Linux reports pending per-connection network errors from + // accept. Skip that stale connection while preserving broker + // resource failures such as descriptor or memory exhaustion. + let _ = socket_operation_error_from_errno(error)?; + } + } + } + Ok(false) +} + +fn listen_tcp_socket( epoll_fd: &OwnedFd, id: u64, socket: &mut SocketEntry, backlog: u32, -) -> BrokerResult> { +) -> BrokerResult> { if socket.kind != SocketKind::Tcp { return Ok(SocketOutcome::Failed(SocketError::InvalidArgument)); } @@ -1260,18 +2145,14 @@ fn listen_socket( } } socket.listening = true; - // An unbound TCP socket is implicitly bound by listen(2), so query the assigned - // address only after listen succeeds. - let local_address = local_socket_address(&socket.socket)?; let mut snapshot = socket .snapshot .lock() .expect("Linux socket snapshot mutex poisoned"); - snapshot.local_address = Some(local_address); if !was_listening { snapshot.readiness = ReadinessFlags::default(); } - Ok(SocketOutcome::Completed(local_address)) + Ok(SocketOutcome::Completed(())) } fn send_socket(socket: &mut SocketEntry, data: &[u8]) -> BrokerResult> { @@ -1554,16 +2435,30 @@ fn receive_socket_once( } } -fn set_tcp_option(socket: &SocketEntry, value: TcpOptionValue) -> BrokerResult<()> { +fn set_tcp_option(socket: &mut SocketEntry, value: TcpOptionValue) -> BrokerResult<()> { if socket.kind != SocketKind::Tcp { return Err(BrokerError::UnsupportedOperation); } + // Cached values are reapplied when a published endpoint replaces this + // socket's descriptor. match value { - TcpOptionValue::NoDelay(value) => sockopt::set_tcp_nodelay(&socket.socket, value), - TcpOptionValue::KeepAlive(value) => sockopt::set_socket_keepalive(&socket.socket, value), + TcpOptionValue::NoDelay(value) => { + sockopt::set_tcp_nodelay(&socket.socket, value).map_err(broker_error_from_errno)?; + socket.tcp_no_delay = value; + } + TcpOptionValue::KeepAlive(value) => { + sockopt::set_socket_keepalive(&socket.socket, value) + .map_err(broker_error_from_errno)?; + socket.tcp_keep_alive = value; + } _ => return Err(BrokerError::UnsupportedOperation), } - .map_err(broker_error_from_errno) + Ok(()) +} + +fn apply_tcp_options(socket: &OwnedFd, no_delay: bool, keep_alive: bool) -> BrokerResult<()> { + sockopt::set_tcp_nodelay(socket, no_delay).map_err(broker_error_from_errno)?; + sockopt::set_socket_keepalive(socket, keep_alive).map_err(broker_error_from_errno) } fn get_tcp_option(socket: &SocketEntry, name: TcpOptionName) -> BrokerResult { @@ -1651,6 +2546,11 @@ fn shutdown_socket( } if stop_listening { socket.listening = false; + // A listener that stopped listening can no longer serve as the + // reservation descriptor for its published endpoint. + if socket.port_mapping_index.is_some() { + socket.retain_port_mapping_on_close = false; + } socket.read_shutdown = true; socket.peek_waitall_threshold = None; update_snapshot( @@ -1734,15 +2634,7 @@ fn handle_socket_event(socket: &mut SocketEntry, events: epoll::EventFlags) -> B fn complete_connect(socket: &mut SocketEntry, events: epoll::EventFlags) -> BrokerResult<()> { let status = match sockopt::socket_error(&socket.socket) { Ok(Ok(())) => match getpeername(&socket.socket) { - Ok(Some(_)) => { - let local_address = local_socket_address(&socket.socket)?; - socket - .snapshot - .lock() - .expect("Linux socket snapshot mutex poisoned") - .local_address = Some(local_address); - SocketConnectionStatus::Connected - } + Ok(Some(_)) => SocketConnectionStatus::Connected, Ok(None) | Err(Errno::NOTCONN) => SocketConnectionStatus::Connecting, Err(error) => SocketConnectionStatus::Failed(socket_error_from_errno(error)), }, @@ -2041,6 +2933,7 @@ mod tests { use litebox_broker_protocol::socket::{Ipv4Address, Port, ReceiveSocketResponse}; const TEST_TIMEOUT: Duration = Duration::from_secs(5); + const FIRST_GUEST_EPHEMERAL_PORT: u16 = 49152; #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct ReceivedPlatformDatagram { @@ -2178,6 +3071,375 @@ mod tests { } } + #[test] + fn port_mapping_reservation_is_close_on_exec() { + let host_address = unused_tcp_address(); + let retained = create_port_mapping_reservation( + SocketPortMapping { + guest_port: 80, + host_address, + }, + false, + false, + ) + .unwrap(); + + assert!( + rustix::io::fcntl_getfd(&retained) + .unwrap() + .contains(rustix::io::FdFlags::CLOEXEC) + ); + } + + #[test] + fn unavailable_publish_endpoint_rejects_provider_startup() { + let occupied = TcpListener::bind("127.0.0.1:0").unwrap(); + let host_address = socket_address_v4(occupied.local_addr().unwrap()); + let error = LinuxSocketProvider::new_with_port_mappings( + 1, + &[SocketPortMapping { + guest_port: 80, + host_address, + }], + ) + .err() + .unwrap(); + + assert_eq!(error.kind(), ErrorKind::AddrInUse); + } + + #[test] + fn private_backend_endpoints_are_not_guest_destinations() { + let provider = Arc::new(LinuxSocketProvider::new(2).unwrap()); + let all_destinations = DestinationRule::new( + CallerCredential::Unauthenticated, + Ipv4Cidr::new(Ipv4Address([0, 0, 0, 0]), 0).unwrap(), + DestinationPortRange::new(Port(1), Port(u16::MAX)).unwrap(), + ); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()).with_socket_policy( + SocketPolicy::from_tcp_udp_destination_rules( + &[all_destinations], + &[all_destinations], + ) + .unwrap(), + ), + BrokerCoreLimits::new_with_all_limits(4, 0, 2, 2), + provider.clone(), + ) + .unwrap(); + let first_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let second_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, _publications) = channel(); + let (retired, _retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + + let listener = create_socket(&first_session, readiness.clone()); + let guest_listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + assert_eq!( + litebox_broker_core::socket::bind(&first_session, listener, guest_listener_address,), + Ok(SocketOutcome::Completed(guest_listener_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&first_session, listener, 1), + Ok(SocketOutcome::Completed(guest_listener_address)) + ); + let private_tcp_address = provider + .reactor + .host_address(SocketKind::Tcp, guest_listener_address.port()) + .unwrap(); + let tcp_client = create_socket(&second_session, readiness); + let private_tcp_alias = + SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, private_tcp_address.port()); + + assert_eq!( + litebox_broker_core::socket::connect(&second_session, tcp_client, private_tcp_alias,), + Ok(SocketOutcome::Completed(SocketConnectionStatus::Failed( + SocketError::ConnectionRefused, + ))) + ); + } + + #[test] + fn tcp_peer_translation_uses_the_complete_connection_tuple() { + let mut namespace = SessionSocketNamespace::default(); + let shared_host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); + for (guest_port, host_peer_port) in [(1000, 5000), (1001, 5001)] { + namespace + .insert_binding( + SocketKind::Tcp, + guest_port, + GuestPortBinding { + socket_id: u64::from(guest_port), + guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), + host_address: Some(shared_host_address), + host_peer_address: Some(SocketAddrV4::new( + Ipv4Addr::LOCALHOST, + host_peer_port, + )), + host_mapped: false, + }, + ) + .unwrap(); + } + + assert_eq!( + namespace.translate_tcp_peer( + shared_host_address, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001), + ), + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1001) + ); + assert_eq!(namespace.tcp_translations.len(), 1); + assert_eq!( + namespace.translate_tcp_peer( + shared_host_address, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001), + ), + shared_host_address + ); + assert_eq!( + namespace.translate_tcp_peer( + shared_host_address, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000), + ), + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1000) + ); + assert!(namespace.tcp_translations.is_empty()); + } + + #[test] + fn guest_tcp_ports_are_session_scoped_and_do_not_bind_host_ports() { + let occupied_host_listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let occupied_host_address = socket_address_v4(occupied_host_listener.local_addr().unwrap()); + let provider = Arc::new(LinuxSocketProvider::new(4).unwrap()); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(8, 0, 4, 4), + provider, + ) + .unwrap(); + let first_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let second_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, _publications) = channel(); + let (retired, _retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let first = create_socket(&first_session, readiness.clone()); + let second = create_socket(&second_session, readiness.clone()); + let occupied_host_port = create_socket(&first_session, readiness); + let guest_port_80 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + + assert_eq!( + litebox_broker_core::socket::bind(&first_session, first, guest_port_80), + Ok(SocketOutcome::Completed(guest_port_80)) + ); + assert_eq!( + litebox_broker_core::socket::bind(&second_session, second, guest_port_80), + Ok(SocketOutcome::Completed(guest_port_80)) + ); + assert_eq!( + litebox_broker_core::socket::bind( + &first_session, + occupied_host_port, + occupied_host_address, + ), + Ok(SocketOutcome::Completed(occupied_host_address)) + ); + } + + #[test] + fn guest_tcp_loopback_routes_within_the_session_namespace() { + let provider = Arc::new(LinuxSocketProvider::new(3).unwrap()); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(4, 0, 3, 3), + provider, + ) + .unwrap(); + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, publications) = channel(); + let (retired, retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let listener = create_socket(&session, readiness.clone()); + let guest_listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + assert_eq!( + litebox_broker_core::socket::bind(&session, listener, guest_listener_address), + Ok(SocketOutcome::Completed(guest_listener_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&session, listener, 2), + Ok(SocketOutcome::Completed(guest_listener_address)) + ); + + let client = create_socket(&session, readiness.clone()); + let connect = + litebox_broker_core::socket::connect(&session, client, guest_listener_address).unwrap(); + assert!(matches!( + connect, + SocketOutcome::Completed( + SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected + ) + )); + wait_until_connected(&session, client, &publications); + let client_address = litebox_broker_core::socket::status(&session, client) + .unwrap() + .local_address + .expect("connected client must have a guest-local address"); + session.close_object_reference(client).unwrap(); + assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), client); + + let replacement = create_socket(&session, readiness.clone()); + let replacement_address = + SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_GUEST_EPHEMERAL_PORT + 1); + assert_eq!( + litebox_broker_core::socket::bind(&session, replacement, replacement_address), + Ok(SocketOutcome::Completed(replacement_address)) + ); + let connect = + litebox_broker_core::socket::connect(&session, replacement, guest_listener_address) + .unwrap(); + assert!(matches!( + connect, + SocketOutcome::Completed( + SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected + ) + )); + wait_until_connected(&session, replacement, &publications); + if !session + .check_readiness(listener) + .unwrap() + .contains(ReadinessFlags::READ) + { + wait_for_readiness(&publications, listener, ReadinessFlags::READ); + } + let accepted = + match litebox_broker_core::socket::accept(&session, listener, readiness.clone()) + .unwrap() + { + SocketOutcome::Completed(accepted) => accepted, + SocketOutcome::Failed(error) => panic!("accept failed: {error:?}"), + }; + assert_eq!(accepted.local_address, guest_listener_address); + assert_eq!(accepted.remote_address, client_address); + session.close_object_reference(accepted.handle).unwrap(); + assert_eq!( + retirements.recv_timeout(TEST_TIMEOUT).unwrap(), + accepted.handle + ); + + let accepted = + match litebox_broker_core::socket::accept(&session, listener, readiness).unwrap() { + SocketOutcome::Completed(accepted) => accepted, + SocketOutcome::Failed(error) => panic!("replacement accept failed: {error:?}"), + }; + assert_eq!(accepted.local_address, guest_listener_address); + assert_eq!(accepted.remote_address, replacement_address); + assert_eq!( + litebox_broker_core::socket::status(&session, replacement) + .unwrap() + .local_address, + Some(replacement_address) + ); + assert_eq!( + send_bytes(&session, replacement, b"x", SendFlags::NONE), + Ok(SocketOutcome::Completed(1)) + ); + wait_for_readiness(&publications, accepted.handle, ReadinessFlags::READ); + let mut byte = [0]; + assert_eq!( + receive_into( + &session, + accepted.handle, + &mut byte, + ReceiveFlags::NONE, + 0, + 0, + ), + Ok(SocketOutcome::Completed(ReceiveSocketResponse::Received(1))) + ); + assert_eq!(byte, *b"x"); + } + + #[test] + fn mapped_tcp_listener_can_be_replaced_while_an_accepted_socket_remains() { + let host_address = unused_tcp_address(); + let provider = Arc::new( + LinuxSocketProvider::new_with_port_mappings( + 2, + &[SocketPortMapping { + guest_port: 80, + host_address, + }], + ) + .unwrap(), + ); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(4, 0, 2, 2), + provider, + ) + .unwrap(); + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, publications) = channel(); + let (retired, retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let listener = create_socket(&session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::bind(&session, listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&session, listener, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + + let first_client = TcpStream::connect(host_address).unwrap(); + wait_for_readiness(&publications, listener, ReadinessFlags::READ); + let accepted = + match litebox_broker_core::socket::accept(&session, listener, readiness.clone()) + .unwrap() + { + SocketOutcome::Completed(accepted) => accepted, + SocketOutcome::Failed(error) => panic!("accept failed: {error:?}"), + }; + assert_eq!( + litebox_broker_core::socket::shutdown(&session, listener, ShutdownMode::StopListening,), + Ok(SocketOutcome::Completed(())) + ); + session.close_object_reference(listener).unwrap(); + assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), listener); + + let replacement = create_socket(&session, readiness); + assert_eq!( + litebox_broker_core::socket::bind(&session, replacement, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&session, replacement, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + let second_client = TcpStream::connect(host_address).unwrap(); + + drop((first_client, second_client)); + session.close_object_reference(accepted.handle).unwrap(); + } + #[test] fn reactor_drives_a_loopback_tcp_socket() { assert_eq!( @@ -2263,7 +3525,7 @@ mod tests { .local_address .expect("connected socket must expose its local address"); assert_eq!(*local_address.ip(), Ipv4Addr::LOCALHOST); - assert_ne!(local_address.port(), 0); + assert_eq!(local_address.port(), FIRST_GUEST_EPHEMERAL_PORT); assert_eq!(status.pending_error, None); assert_eq!( litebox_broker_core::socket::get_tcp_option(&session, handle, TcpOptionName::NoDelay,), @@ -2549,7 +3811,17 @@ mod tests { #[test] fn reactor_assigns_a_port_to_an_unbound_tcp_listener() { - let provider = Arc::new(LinuxSocketProvider::new(2).unwrap()); + let host_address = unused_tcp_address(); + let provider = Arc::new( + LinuxSocketProvider::new_with_port_mappings( + 2, + &[SocketPortMapping { + guest_port: FIRST_GUEST_EPHEMERAL_PORT, + host_address, + }], + ) + .unwrap(), + ); let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) .with_socket_policy(SocketPolicy::Ipv4Loopback), @@ -2560,7 +3832,7 @@ mod tests { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); - let (published, publications) = channel(); + let (published, _publications) = channel(); let (retired, _retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); let listener = create_socket(&session, readiness.clone()); @@ -2571,33 +3843,86 @@ mod tests { SocketOutcome::Completed(address) => address, SocketOutcome::Failed(error) => panic!("listen failed: {error:?}"), }; - assert_ne!(local_address.port(), 0); - - let connect_address = SocketAddrV4::new( - if local_address.ip().is_unspecified() { - Ipv4Addr::LOCALHOST - } else { - *local_address.ip() - }, - local_address.port(), + assert_eq!(local_address.port(), FIRST_GUEST_EPHEMERAL_PORT + 1); + assert_eq!( + TcpListener::bind(host_address).unwrap_err().kind(), + ErrorKind::AddrInUse ); - let client = TcpStream::connect(connect_address).unwrap(); - wait_for_readiness(&publications, listener, ReadinessFlags::READ); - let accepted = - match litebox_broker_core::socket::accept(&session, listener, readiness).unwrap() { - SocketOutcome::Completed(accepted) => accepted, - SocketOutcome::Failed(error) => panic!("accept failed: {error:?}"), - }; - assert_eq!(accepted.local_address, connect_address); + let mapped_listener = create_socket(&session, readiness); + let mapped_guest_address = + SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_GUEST_EPHEMERAL_PORT); assert_eq!( - accepted.remote_address, - socket_address_v4(client.local_addr().unwrap()) + litebox_broker_core::socket::set_tcp_option( + &session, + mapped_listener, + TcpOptionValue::NoDelay(true), + ), + Ok(()) + ); + assert_eq!( + litebox_broker_core::socket::set_tcp_option( + &session, + mapped_listener, + TcpOptionValue::KeepAlive(true), + ), + Ok(()) + ); + assert_eq!( + litebox_broker_core::socket::bind(&session, mapped_listener, mapped_guest_address,), + Ok(SocketOutcome::Completed(mapped_guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::get_tcp_option( + &session, + mapped_listener, + TcpOptionName::NoDelay, + ), + Ok(TcpOptionValue::NoDelay(true)) + ); + assert_eq!( + litebox_broker_core::socket::get_tcp_option( + &session, + mapped_listener, + TcpOptionName::KeepAlive, + ), + Ok(TcpOptionValue::KeepAlive(true)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&session, mapped_listener, 1), + Ok(SocketOutcome::Completed(mapped_guest_address)) + ); + assert_eq!( + TcpListener::bind(host_address).unwrap_err().kind(), + ErrorKind::AddrInUse + ); + assert_eq!( + create_port_mapping_reservation( + SocketPortMapping { + guest_port: FIRST_GUEST_EPHEMERAL_PORT, + host_address, + }, + true, + true, + ) + .unwrap_err(), + Errno::ADDRINUSE ); + TcpStream::connect(host_address).unwrap(); } #[test] fn reactor_drives_a_loopback_tcp_listener() { - let provider = Arc::new(LinuxSocketProvider::new(4).unwrap()); + let host_address = unused_tcp_address(); + let provider = Arc::new( + LinuxSocketProvider::new_with_port_mappings( + 4, + &[SocketPortMapping { + guest_port: FIRST_GUEST_EPHEMERAL_PORT, + host_address, + }], + ) + .unwrap(), + ); let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) .with_socket_policy(SocketPolicy::Ipv4Loopback), @@ -2612,7 +3937,7 @@ mod tests { let (retired, retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); let listener = create_socket(&session, readiness.clone()); - let requested_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); + let requested_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_GUEST_EPHEMERAL_PORT); let local_address = match litebox_broker_core::socket::bind(&session, listener, requested_address).unwrap() { @@ -2620,7 +3945,7 @@ mod tests { SocketOutcome::Failed(error) => panic!("bind failed: {error:?}"), }; assert_eq!(local_address.ip(), requested_address.ip()); - assert_ne!(local_address.port(), 0); + assert_eq!(local_address, requested_address); assert_eq!( litebox_broker_core::socket::listen(&session, listener, 8), Ok(SocketOutcome::Completed(local_address)) @@ -2642,8 +3967,8 @@ mod tests { .contains(ReadinessFlags::READ) ); - let mut first_client = TcpStream::connect(local_address).unwrap(); - let second_client = TcpStream::connect(local_address).unwrap(); + let mut first_client = TcpStream::connect(host_address).unwrap(); + let second_client = TcpStream::connect(host_address).unwrap(); first_client.set_read_timeout(Some(TEST_TIMEOUT)).unwrap(); first_client.set_write_timeout(Some(TEST_TIMEOUT)).unwrap(); wait_for_readiness(&publications, listener, ReadinessFlags::READ); @@ -3039,6 +4364,13 @@ mod tests { assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), handle); } + fn unused_tcp_address() -> SocketAddrV4 { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = socket_address_v4(listener.local_addr().unwrap()); + drop(listener); + address + } + fn socket_address_v4(address: std::net::SocketAddr) -> SocketAddrV4 { let std::net::SocketAddr::V4(address) = address else { panic!("loopback TCP test unexpectedly used IPv6"); diff --git a/litebox_broker_protocol/src/socket.rs b/litebox_broker_protocol/src/socket.rs index 36867db2f..1188e08ae 100644 --- a/litebox_broker_protocol/src/socket.rs +++ b/litebox_broker_protocol/src/socket.rs @@ -305,7 +305,7 @@ impl SocketError { pub enum SocketOutcome { /// The operation completed successfully. Completed(T), - /// The host network stack reported an ordinary socket failure. + /// The broker socket backend reported an ordinary socket failure. Failed(SocketError), } @@ -355,7 +355,7 @@ pub struct BindSocketRequest { /// Response to a socket bind request. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BindSocketResponse { - /// Local address assigned by the host network stack. + /// Guest-visible local address assigned to the socket. pub local_address: SocketAddrV4, } @@ -371,7 +371,7 @@ pub struct ListenSocketRequest { /// Response to a socket listen request. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ListenSocketResponse { - /// Local address assigned by the host network stack. + /// Local address reserved in the guest's broker-managed namespace. pub local_address: SocketAddrV4, } @@ -553,7 +553,7 @@ pub struct SocketStatusRequest { pub struct SocketStatusResponse { /// Current connection state. pub status: SocketConnectionStatus, - /// Local endpoint assigned by the host network stack, if any. + /// Guest-visible local endpoint assigned to the socket, if any. pub local_address: Option, /// Pending asynchronous socket error, consumed by this status query. pub pending_error: Option, diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 61db89ec2..ed76f3fb4 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -4,7 +4,7 @@ use std::error::Error; use std::ffi::OsString; use std::io::{Error as IoError, ErrorKind, Result as IoResult}; -use std::net::Ipv4Addr; +use std::net::{Ipv4Addr, SocketAddrV4}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; use std::process::{Child, Command}; @@ -22,7 +22,7 @@ use litebox_broker_core::{ Ipv4Cidr, ObjectRights, PolicyEngine, SocketPolicy, SocketPolicyError, }; use litebox_broker_host::{BrokerHostAssociation, ConnectionTermination, setup_connection}; -use litebox_broker_platform_linux_userland::LinuxSocketProvider; +use litebox_broker_platform_linux_userland::{LinuxSocketProvider, SocketPortMapping}; use litebox_broker_protocol::message::BrokerRequest; use litebox_broker_protocol::shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE}; use litebox_broker_protocol::socket::{Ipv4Address, Port}; @@ -85,6 +85,36 @@ impl FromStr for AllowedTcpDestination { } } +/// Command-line description of one host-to-guest TCP port mapping. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct PortMappingArgument { + host_address: SocketAddrV4, + guest_port: u16, +} + +impl FromStr for PortMappingArgument { + type Err = String; + + fn from_str(value: &str) -> Result { + let (host_address, guest_port) = value + .rsplit_once(':') + .ok_or_else(|| "expected HOST_IP:HOST_PORT:GUEST_PORT".to_owned())?; + let host_address = host_address + .parse::() + .map_err(|error| format!("invalid host IPv4 endpoint: {error}"))?; + let guest_port = guest_port + .parse::() + .map_err(|error| format!("invalid guest port: {error}"))?; + if host_address.port() == 0 || guest_port == 0 { + return Err("mapped host and guest ports must be nonzero".to_owned()); + } + Ok(Self { + host_address, + guest_port, + }) + } +} + #[derive(Parser, Debug)] struct CliArgs { /// Permit outbound TCP connections to a destination CIDR and port range. @@ -93,6 +123,9 @@ struct CliArgs { /// `0.0.0.0/0:1-65535` permits every nonzero IPv4 TCP destination. #[arg(long, value_name = "CIDR:PORT[-PORT]")] allow_tcp_destination: Vec, + /// Publish a host IPv4 TCP endpoint to a guest-local TCP port. + #[arg(long, value_name = "HOST_IP:HOST_PORT:GUEST_PORT")] + publish_tcp: Vec, /// Local runner executable to launch. #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::ExecutablePath)] runner: PathBuf, @@ -110,11 +143,15 @@ fn main() -> Result<(), Box> { let control_listener = UnixListener::bind(&control_socket_path)?; control_listener.set_nonblocking(true)?; let limits = BrokerCoreLimits::DEFAULT; + let port_mappings = configured_port_mappings(&args.publish_tcp); let broker = BrokerCore::new_with_limits( PolicyEngine::with_host_guaranteed_rights(ObjectRights::all()) .with_socket_policy(configured_socket_policy(&args.allow_tcp_destination)?), limits, - Arc::new(LinuxSocketProvider::new(limits.max_sockets)?), + Arc::new(LinuxSocketProvider::new_with_port_mappings( + limits.max_sockets, + &port_mappings, + )?), )?; let mut runner_command = Command::new(&args.runner); @@ -139,6 +176,15 @@ fn main() -> Result<(), Box> { Ok(()) } +fn configured_port_mappings(tcp: &[PortMappingArgument]) -> Vec { + tcp.iter() + .map(|mapping| SocketPortMapping { + guest_port: mapping.guest_port, + host_address: mapping.host_address, + }) + .collect() +} + fn configured_socket_policy( allowed_destinations: &[AllowedTcpDestination], ) -> Result { @@ -563,6 +609,29 @@ mod tests { assert!("203.0.113.0/24:0".parse::().is_err()); } + #[test] + fn socket_port_mapping_arguments_name_distinct_host_and_guest_ports() { + let mapping = "127.0.0.1:8080:80".parse::().unwrap(); + + assert_eq!( + mapping, + PortMappingArgument { + host_address: "127.0.0.1:8080".parse().unwrap(), + guest_port: 80, + } + ); + assert!("127.0.0.1:0:80".parse::().is_err()); + assert!("127.0.0.1:8080:0".parse::().is_err()); + assert!("8080:80".parse::().is_err()); + assert_eq!( + configured_port_mappings(&[mapping]), + vec![SocketPortMapping { + guest_port: 80, + host_address: "127.0.0.1:8080".parse().unwrap(), + }] + ); + } + #[test] fn tcp_destination_arguments_replace_the_loopback_default() { assert_eq!( diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 16da35fd4..02419bb6f 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -341,6 +341,16 @@ fn spawn_test_broker( control_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, connection_count: usize, +) -> TestBroker { + spawn_test_broker_with_port_mappings(control_socket_path, policy, connection_count, Vec::new()) +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +fn spawn_test_broker_with_port_mappings( + control_socket_path: &Path, + policy: litebox_broker_core::PolicyEngine, + connection_count: usize, + port_mappings: Vec, ) -> TestBroker { let _ = std::fs::remove_file(control_socket_path); @@ -359,8 +369,9 @@ fn spawn_test_broker( policy, limits, std::sync::Arc::new( - litebox_broker_platform_linux_userland::LinuxSocketProvider::new( + litebox_broker_platform_linux_userland::LinuxSocketProvider::new_with_port_mappings( limits.max_sockets, + &port_mappings, ) .expect("failed to create broker test socket provider"), ), @@ -739,23 +750,36 @@ fn test_runner_broker_tcp_server_with_rewriter() { use std::net::{Ipv4Addr, TcpStream}; use std::process::Stdio; + const GUEST_PORT: u16 = 18_080; + let target = common::compile( "./tests/tcp_broker_server.c", "broker_tcp_server_rewriter", false, false, ); + let host_listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let host_address = match host_listener.local_addr().unwrap() { + std::net::SocketAddr::V4(address) => address, + std::net::SocketAddr::V6(_) => unreachable!("IPv4 bind returned an IPv6 address"), + }; + drop(host_listener); let control_socket_path = unique_test_socket_path("runner-broker-tcp-server-control"); - let broker = spawn_test_broker( + let broker = spawn_test_broker_with_port_mappings( &control_socket_path, litebox_broker_core::PolicyEngine::with_host_guaranteed_rights( litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(litebox_broker_core::SocketPolicy::Ipv4Loopback), 1, + vec![litebox_broker_platform_linux_userland::SocketPortMapping { + guest_port: GUEST_PORT, + host_address, + }], ); let mut child = Runner::new(&target, "broker_tcp_server_rewriter") .broker_socket(&control_socket_path) + .arg(GUEST_PORT.to_string()) .spawn_with_stdio(Stdio::null(), Stdio::piped(), Stdio::inherit()); let stdout = child.stdout.take().unwrap(); let (line_sender, line_receiver) = std::sync::mpsc::channel(); @@ -786,8 +810,9 @@ fn test_runner_broker_tcp_server_with_rewriter() { }; let listen = next_marker("LISTEN "); - let port = listen.split_whitespace().nth(1).unwrap().parse().unwrap(); - let mut first = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap(); + let port: u16 = listen.split_whitespace().nth(1).unwrap().parse().unwrap(); + assert_eq!(port, GUEST_PORT); + let mut first = TcpStream::connect(host_address).unwrap(); first.set_read_timeout(Some(BROKER_HELPER_TIMEOUT)).unwrap(); first .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) @@ -813,7 +838,7 @@ fn test_runner_broker_tcp_server_with_rewriter() { child.try_wait().unwrap().is_none(), "blocking accept returned before a client connected" ); - let mut second = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap(); + let mut second = TcpStream::connect(host_address).unwrap(); second .set_read_timeout(Some(BROKER_HELPER_TIMEOUT)) .unwrap(); diff --git a/litebox_runner_linux_userland/tests/tcp_broker_server.c b/litebox_runner_linux_userland/tests/tcp_broker_server.c index 2d053e9c2..1e07b7dff 100644 --- a/litebox_runner_linux_userland/tests/tcp_broker_server.c +++ b/litebox_runner_linux_userland/tests/tcp_broker_server.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -58,8 +59,13 @@ static void *blocking_accept_thread(void *argument) { return NULL; } -int main(void) { +int main(int argc, char **argv) { setvbuf(stdout, NULL, _IONBF, 0); + assert(argc == 2); + char *end = NULL; + unsigned long guest_port = strtoul(argv[1], &end, 10); + assert(end != argv[1] && *end == '\0' && guest_port > 0 && + guest_port <= UINT16_MAX); int listener = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); @@ -67,7 +73,7 @@ int main(void) { struct sockaddr_in local = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK), - .sin_port = 0, + .sin_port = htons((uint16_t)guest_port), }; assert(bind(listener, (const struct sockaddr *)&local, sizeof(local)) == 0); assert(listen(listener, 8) == 0); From 8ea75e04598dc08be6836061b0616e545603014c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 7 Aug 2026 18:41:18 -0700 Subject: [PATCH 2/6] Harden TCP port mapping lifecycle Bound pending peer translations to live guest-routed connections, retire them with their listener, and preserve exclusive mapped endpoint ownership while clearing stale listener queues. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- .../src/socket.rs | 375 ++++++++++++++++-- 1 file changed, 333 insertions(+), 42 deletions(-) diff --git a/litebox_broker_platform_linux_userland/src/socket.rs b/litebox_broker_platform_linux_userland/src/socket.rs index bdda3d291..5983ca52d 100644 --- a/litebox_broker_platform_linux_userland/src/socket.rs +++ b/litebox_broker_platform_linux_userland/src/socket.rs @@ -23,9 +23,9 @@ use litebox_broker_core::{BrokerError, Result as BrokerResult, SessionId}; use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::socket::{ AddressFamily, CreateSocketRequest, IpProtocol, MAX_SOCKET_PEEK_SIZE, MAX_SOCKET_TRANSFER_SIZE, - MAX_UDP_DATAGRAM_SIZE, ReceiveFlags, ReceiveFromFlags, SendFlags, ShutdownMode, - SocketConnectionStatus, SocketError, SocketOutcome, SocketStatusResponse, SocketType, - TcpOptionName, TcpOptionValue, + MAX_TCP_LISTEN_BACKLOG, MAX_UDP_DATAGRAM_SIZE, ReceiveFlags, ReceiveFromFlags, SendFlags, + ShutdownMode, SocketConnectionStatus, SocketError, SocketOutcome, SocketStatusResponse, + SocketType, TcpOptionName, TcpOptionValue, }; use rustix::buffer::spare_capacity; use rustix::event::{EventfdFlags, PollFd, PollFlags, Timespec, epoll, eventfd, poll}; @@ -43,7 +43,7 @@ use litebox_broker_core::readiness::ReadinessRegistration; const WAKE_TOKEN: u64 = 0; const MAX_QUEUED_SOCKET_COMMANDS: usize = 64; const MAX_EPOLL_EVENTS: usize = 64; -const MAX_STALE_PUBLICATION_ITEMS: usize = 64; +const MAX_STALE_PORT_MAPPING_CONNECTIONS: usize = MAX_TCP_LISTEN_BACKLOG as usize + 3; const MAX_RETAINED_TRANSLATIONS: usize = 1 << 14; /// Linux-userland socket provider. @@ -128,6 +128,17 @@ fn create_port_mapping_reservation( Ok(socket) } +fn create_replacement_port_mapping_reservation( + mapping: SocketPortMapping, +) -> core::result::Result { + let socket = create_port_mapping_reservation(mapping, true, false)?; + // Reuse is needed only while replacing a listener that may still have + // accepted children. Clear it afterward so the bound reservation is + // exclusive between publications. + sockopt::set_socket_reuseaddr(&socket, false)?; + Ok(socket) +} + impl SocketProvider for LinuxSocketProvider { fn reserves_guest_port(&self, request: CreateSocketRequest, port: u16) -> bool { if socket_kind(request) != Some(SocketKind::Tcp) { @@ -750,7 +761,6 @@ struct SocketEntry { listening: bool, guest_local_address: Option, port_mapping_index: Option, - retain_port_mapping_on_close: bool, tcp_no_delay: bool, tcp_keep_alive: bool, } @@ -802,16 +812,9 @@ impl SessionSocketNamespace { if kind != SocketKind::Tcp { return Err(BrokerError::Internal); } - let translation = binding - .host_address - .zip(binding.host_peer_address) - .map(|connection| (connection, binding.guest_address)); if self.tcp.insert(port, binding).is_some() { return Err(BrokerError::Internal); } - if let Some((connection, guest_address)) = translation { - self.tcp_translations.insert(connection, guest_address); - } Ok(()) } @@ -819,12 +822,15 @@ impl SessionSocketNamespace { if kind != SocketKind::Tcp { return Err(BrokerError::Internal); } + self.tcp + .try_reserve(1) + .map_err(|_| BrokerError::OutOfMemory) + } + + fn reserve_tcp_translation(&mut self) -> BrokerResult<()> { if self.tcp_translations.len() >= MAX_RETAINED_TRANSLATIONS { return Err(BrokerError::ResourceExhausted); } - self.tcp - .try_reserve(1) - .map_err(|_| BrokerError::OutOfMemory)?; self.tcp_translations .try_reserve(1) .map_err(|_| BrokerError::OutOfMemory) @@ -921,6 +927,14 @@ impl SessionSocketNamespace { }) .unwrap_or(remote_address) } + + fn remove_tcp_translations_for_listener(&mut self, listener_address: SocketAddrV4) { + self.tcp_translations.retain(|(_, destination), _| { + destination.port() != listener_address.port() + || (!listener_address.ip().is_unspecified() + && destination.ip() != listener_address.ip()) + }); + } } /// Cached connection and readiness state shared with the broker-facing handle. @@ -995,7 +1009,7 @@ impl Reactor { .get(mapping_index) .is_some_and(|state| state.claimed_by.is_none() && state.reservation.is_none()) { - match create_port_mapping_reservation(mapping, true, false) { + match create_replacement_port_mapping_reservation(mapping) { Ok(reservation) => { self.port_mappings .get_mut(mapping_index) @@ -1023,7 +1037,6 @@ impl Reactor { reservation }; let preparation = (|| { - sockopt::set_socket_reuseaddr(&reservation, true).map_err(broker_error_from_errno)?; sockopt::set_socket_linger(&reservation, None).map_err(broker_error_from_errno)?; drain_tcp_listener(&reservation) })(); @@ -1061,6 +1074,88 @@ impl Reactor { Ok(SocketOutcome::Completed(host_address)) } + fn stop_listening_socket(&mut self, id: u64) -> BrokerResult> { + let mapping_index = { + let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; + if socket.kind != SocketKind::Tcp || !socket.listening { + return self + .sockets + .get_mut(&id) + .ok_or(BrokerError::Internal) + .and_then(|socket| shutdown_socket(socket, ShutdownMode::StopListening)); + } + socket.port_mapping_index.filter(|mapping_index| { + self.port_mappings + .get(*mapping_index) + .is_some_and(|state| state.claimed_by == Some(id)) + }) + }; + let Some(mapping_index) = mapping_index else { + return self + .sockets + .get_mut(&id) + .ok_or(BrokerError::Internal) + .and_then(|socket| shutdown_socket(socket, ShutdownMode::StopListening)); + }; + if !self + .port_mappings + .get(mapping_index) + .is_some_and(|state| state.claimed_by == Some(id) && state.reservation.is_none()) + { + return Err(BrokerError::Internal); + } + let (no_delay, keep_alive) = self + .sockets + .get(&id) + .map(|socket| (socket.tcp_no_delay, socket.tcp_keep_alive)) + .ok_or(BrokerError::Internal)?; + let replacement = socket_with( + LinuxAddressFamily::INET, + LinuxSocketType::STREAM, + LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, + Some(ipproto::TCP), + ) + .map_err(broker_error_from_errno)?; + apply_tcp_options(&replacement, no_delay, keep_alive)?; + epoll::add( + &self.epoll, + &replacement, + epoll::EventData::new_u64(id), + idle_epoll_events(), + ) + .map_err(broker_error_from_errno)?; + let old_socket = &self.sockets.get(&id).ok_or(BrokerError::Internal)?.socket; + if let Err(error) = quiesce_tcp_listener(old_socket) { + let _ = delete_epoll_registration(&self.epoll, &replacement); + return Err(error); + } + if !delete_epoll_registration(&self.epoll, old_socket) { + let _ = delete_epoll_registration(&self.epoll, &replacement); + return Err(BrokerError::Internal); + } + let reservation = { + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + let reservation = core::mem::replace(&mut socket.socket, replacement); + socket.listening = false; + socket.read_shutdown = true; + socket.peek_waitall_threshold = None; + socket.port_mapping_index = None; + reservation + }; + let state = self + .port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)?; + state.claimed_by = None; + state.reservation = Some(reservation); + update_snapshot( + self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?, + Some(SocketConnectionStatus::Failed(SocketError::NotConnected)), + ReadinessFlags::WRITE | ReadinessFlags::HANGUP, + )?; + Ok(SocketOutcome::Completed(())) + } + /// Replaces a socket's descriptor, returning ownership of the replacement on failure. fn replace_socket_descriptor( &mut self, @@ -1292,7 +1387,8 @@ impl Reactor { .set_host_address(kind, guest_address.port(), id, host_address, host_mapped)?; } let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; - match listen_tcp_socket(&self.epoll, id, socket, backlog)? { + let host_mapped = socket.port_mapping_index.is_some(); + match listen_tcp_socket(&self.epoll, id, socket, backlog, host_mapped)? { SocketOutcome::Completed(()) => Ok(SocketOutcome::Completed(guest_address)), SocketOutcome::Failed(error) => Ok(SocketOutcome::Failed(error)), } @@ -1340,6 +1436,13 @@ impl Reactor { SocketOutcome::Completed(destination) => destination, SocketOutcome::Failed(error) => return Ok(SocketConnectionStatus::Failed(error)), }; + if guest_routed { + self.sessions + .get_mut(&session_id) + .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))? + .reserve_tcp_translation() + .map_err(PlatformConnectError::PeerUnchanged)?; + } let outcome = { let socket = self .sockets @@ -1405,12 +1508,21 @@ impl Reactor { .is_some_and(|state| state.claimed_by == Some(id)) }) .map(|mapping_index| (mapping_index, self.port_mappings[mapping_index].mapping)); - // A retired listener that stopped listening cannot be reused as a - // reservation, so its descriptor is dropped and the endpoint rebound. + let retired_listener_address = self.sockets.get(&id).and_then(|socket| { + (socket.kind == SocketKind::Tcp && socket.listening) + .then(|| local_socket_address(&socket.socket).ok()) + .flatten() + }); + // Keep a published listener active with a zero backlog so the endpoint + // remains exclusive without accumulating an unbounded stale queue. let retain_original = port_mapping.is_some_and(|_| { self.sockets.get(&id).is_some_and(|socket| { - socket.retain_port_mapping_on_close - && delete_epoll_registration(&self.epoll, &socket.socket) + delete_epoll_registration(&self.epoll, &socket.socket) + && if socket.listening { + quiesce_tcp_listener(&socket.socket).is_ok() + } else { + sockopt::set_socket_reuseaddr(&socket.socket, false).is_ok() + } }) }); let Some(socket) = self.sockets.remove(&id) else { @@ -1418,6 +1530,9 @@ impl Reactor { }; let session_id = socket.session_id; let remove_namespace = if let Some(namespace) = self.sessions.get_mut(&session_id) { + if let Some(listener_address) = retired_listener_address { + namespace.remove_tcp_translations_for_listener(listener_address); + } if let Some(address) = socket.guest_local_address { namespace.remove_binding(socket.kind, address.port(), id); } @@ -1438,7 +1553,7 @@ impl Reactor { } else { drop(socket); port_mapping - .and_then(|(_, mapping)| create_port_mapping_reservation(mapping, true, false).ok()) + .and_then(|(_, mapping)| create_replacement_port_mapping_reservation(mapping).ok()) }; if let Some((mapping_index, _)) = port_mapping && let Some(state) = self.port_mappings.get_mut(mapping_index) @@ -1631,11 +1746,34 @@ impl Reactor { { self.peek_cache = None; } - let outcome = self - .sockets - .get_mut(&id) - .ok_or(BrokerError::Internal) - .and_then(|socket| shutdown_socket(socket, mode)); + let outcome = (|| { + let retired_listener = if mode == ShutdownMode::StopListening { + let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; + (socket.kind == SocketKind::Tcp && socket.listening) + .then(|| { + local_socket_address(&socket.socket) + .map(|address| (socket.session_id, address)) + }) + .transpose()? + } else { + None + }; + let outcome = if mode == ShutdownMode::StopListening { + self.stop_listening_socket(id)? + } else { + self.sockets + .get_mut(&id) + .ok_or(BrokerError::Internal) + .and_then(|socket| shutdown_socket(socket, mode))? + }; + if matches!(outcome, SocketOutcome::Completed(())) + && let Some((session_id, listener_address)) = retired_listener + && let Some(namespace) = self.sessions.get_mut(&session_id) + { + namespace.remove_tcp_translations_for_listener(listener_address); + } + Ok(outcome) + })(); let _ = response.send(outcome); } ReactorCommand::SetTcpOption { @@ -1782,7 +1920,6 @@ impl Reactor { listening: false, guest_local_address: None, port_mapping_index: None, - retain_port_mapping_on_close: true, tcp_no_delay: false, tcp_keep_alive: false, }, @@ -1893,7 +2030,6 @@ impl Reactor { listening: false, guest_local_address: Some(local_address), port_mapping_index: None, - retain_port_mapping_on_close: true, tcp_no_delay: listener_tcp_no_delay, tcp_keep_alive: listener_tcp_keep_alive, }, @@ -2067,6 +2203,16 @@ fn delete_epoll_registration(epoll_fd: &OwnedFd, socket: &OwnedFd) -> bool { } } +fn quiesce_tcp_listener(socket: &OwnedFd) -> BrokerResult<()> { + loop { + match listen(socket, 0) { + Ok(()) => return Ok(()), + Err(Errno::INTR) => {} + Err(error) => return Err(broker_error_from_errno(error)), + } + } +} + /// Returns whether a host IPv4 address names one of this machine's interfaces. fn host_ipv4_address_is_local(address: Ipv4Addr) -> BrokerResult { let socket = socket_with( @@ -2085,7 +2231,7 @@ fn host_ipv4_address_is_local(address: Ipv4Addr) -> BrokerResult { /// Discards connections queued on a reserved host endpoint before publication. fn drain_tcp_listener(socket: &OwnedFd) -> BrokerResult { - for _ in 0..MAX_STALE_PUBLICATION_ITEMS { + for _ in 0..MAX_STALE_PORT_MAPPING_CONNECTIONS { match acceptfrom_with( socket, LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, @@ -2109,6 +2255,7 @@ fn listen_tcp_socket( id: u64, socket: &mut SocketEntry, backlog: u32, + host_mapped: bool, ) -> BrokerResult> { if socket.kind != SocketKind::Tcp { return Ok(SocketOutcome::Failed(SocketError::InvalidArgument)); @@ -2124,19 +2271,40 @@ fn listen_tcp_socket( ) .map_err(broker_error_from_errno)?; } + if host_mapped && let Err(error) = sockopt::set_socket_reuseaddr(&socket.socket, true) { + if !was_listening { + epoll::modify( + epoll_fd, + &socket.socket, + epoll::EventData::new_u64(id), + idle_epoll_events(), + ) + .map_err(broker_error_from_errno)?; + } + return Err(broker_error_from_errno(error)); + } loop { match listen(&socket.socket, backlog) { Ok(()) => break, Err(Errno::INTR) => {} Err(error) => { - if !was_listening { + let reuse_error = host_mapped + .then(|| sockopt::set_socket_reuseaddr(&socket.socket, false)) + .transpose() + .err(); + let epoll_error = if was_listening { + None + } else { epoll::modify( epoll_fd, &socket.socket, epoll::EventData::new_u64(id), idle_epoll_events(), ) - .map_err(broker_error_from_errno)?; + .err() + }; + if let Some(error) = reuse_error.or(epoll_error) { + return Err(broker_error_from_errno(error)); } return Ok(SocketOutcome::Failed(socket_operation_error_from_errno( error, @@ -2546,11 +2714,6 @@ fn shutdown_socket( } if stop_listening { socket.listening = false; - // A listener that stopped listening can no longer serve as the - // reservation descriptor for its published endpoint. - if socket.port_mapping_index.is_some() { - socket.retain_port_mapping_on_close = false; - } socket.read_shutdown = true; socket.peek_waitall_threshold = None; update_snapshot( @@ -3169,6 +3332,7 @@ mod tests { let mut namespace = SessionSocketNamespace::default(); let shared_host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); for (guest_port, host_peer_port) in [(1000, 5000), (1001, 5001)] { + namespace.reserve_tcp_translation().unwrap(); namespace .insert_binding( SocketKind::Tcp, @@ -3177,14 +3341,19 @@ mod tests { socket_id: u64::from(guest_port), guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), host_address: Some(shared_host_address), - host_peer_address: Some(SocketAddrV4::new( - Ipv4Addr::LOCALHOST, - host_peer_port, - )), + host_peer_address: None, host_mapped: false, }, ) .unwrap(); + namespace + .set_host_peer_address( + SocketKind::Tcp, + guest_port, + u64::from(guest_port), + SocketAddrV4::new(Ipv4Addr::LOCALHOST, host_peer_port), + ) + .unwrap(); } assert_eq!( @@ -3212,6 +3381,47 @@ mod tests { assert!(namespace.tcp_translations.is_empty()); } + #[test] + fn retiring_listener_removes_its_pending_tcp_translations() { + let mut namespace = SessionSocketNamespace::default(); + let first_listener = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); + let second_listener = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001); + for (guest_port, listener) in [(1000, first_listener), (1001, second_listener)] { + namespace.reserve_tcp_translation().unwrap(); + namespace + .insert_binding( + SocketKind::Tcp, + guest_port, + GuestPortBinding { + socket_id: u64::from(guest_port), + guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), + host_address: Some(SocketAddrV4::new( + Ipv4Addr::LOCALHOST, + 40000 + guest_port, + )), + host_peer_address: None, + host_mapped: false, + }, + ) + .unwrap(); + namespace + .set_host_peer_address(SocketKind::Tcp, guest_port, u64::from(guest_port), listener) + .unwrap(); + } + + namespace.remove_tcp_translations_for_listener(first_listener); + + assert_eq!(namespace.tcp_translations.len(), 1); + assert_eq!( + namespace.translate_tcp_peer( + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 41001), + second_listener, + ), + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1001) + ); + assert!(namespace.tcp_translations.is_empty()); + } + #[test] fn guest_tcp_ports_are_session_scoped_and_do_not_bind_host_ports() { let occupied_host_listener = TcpListener::bind("127.0.0.1:0").unwrap(); @@ -3422,6 +3632,18 @@ mod tests { litebox_broker_core::socket::shutdown(&session, listener, ShutdownMode::StopListening,), Ok(SocketOutcome::Completed(())) ); + assert_eq!( + create_port_mapping_reservation( + SocketPortMapping { + guest_port: 80, + host_address, + }, + true, + true, + ) + .unwrap_err(), + Errno::ADDRINUSE + ); session.close_object_reference(listener).unwrap(); assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), listener); @@ -3440,6 +3662,75 @@ mod tests { session.close_object_reference(accepted.handle).unwrap(); } + #[test] + fn closing_mapped_listener_discards_more_than_sixty_four_queued_connections() { + let host_address = unused_tcp_address(); + let provider = Arc::new( + LinuxSocketProvider::new_with_port_mappings( + 2, + &[SocketPortMapping { + guest_port: 80, + host_address, + }], + ) + .unwrap(), + ); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(4, 0, 2, 2), + provider, + ) + .unwrap(); + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, _publications) = channel(); + let (retired, retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let listener = create_socket(&session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::bind(&session, listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&session, listener, 65), + Ok(SocketOutcome::Completed(guest_address)) + ); + + let clients = (0..65) + .map(|_| TcpStream::connect(host_address).unwrap()) + .collect::>(); + session.close_object_reference(listener).unwrap(); + assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), listener); + assert_eq!( + create_port_mapping_reservation( + SocketPortMapping { + guest_port: 80, + host_address, + }, + true, + true, + ) + .unwrap_err(), + Errno::ADDRINUSE + ); + + let replacement = create_socket(&session, readiness); + assert_eq!( + litebox_broker_core::socket::bind(&session, replacement, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&session, replacement, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + TcpStream::connect(host_address).unwrap(); + + drop(clients); + } + #[test] fn reactor_drives_a_loopback_tcp_socket() { assert_eq!( From 8dfb159b5b4edc25f9a2a2d25a66d8230b1e2ea8 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 7 Aug 2026 20:39:35 -0700 Subject: [PATCH 3/6] Retire stale TCP routing state Clear session-local mapped routes when listeners stop, and remove pending peer translations after confirmed connect failure or abortive connector close without changing graceful close-before-accept behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- .../src/socket.rs | 308 +++++++++++++++++- 1 file changed, 290 insertions(+), 18 deletions(-) diff --git a/litebox_broker_platform_linux_userland/src/socket.rs b/litebox_broker_platform_linux_userland/src/socket.rs index 5983ca52d..f5c3c3c4e 100644 --- a/litebox_broker_platform_linux_userland/src/socket.rs +++ b/litebox_broker_platform_linux_userland/src/socket.rs @@ -551,6 +551,16 @@ impl ReactorClient { receive.recv().unwrap() } + #[cfg(test)] + fn tcp_translation_count(&self) -> usize { + let (response, receive) = sync_channel(1); + self.commands + .send(ReactorCommand::TcpTranslationCount { response }) + .unwrap(); + self.signal().unwrap(); + receive.recv().unwrap() + } + fn close_session(&self, session_id: SessionId) { let (response, receive) = sync_channel(1); if self @@ -703,6 +713,10 @@ enum ReactorCommand { guest_port: u16, response: SyncSender>, }, + #[cfg(test)] + TcpTranslationCount { + response: SyncSender, + }, Stop { response: SyncSender<()>, }, @@ -759,6 +773,7 @@ struct SocketEntry { write_shutdown: bool, peek_waitall_threshold: Option, listening: bool, + abortive_close: bool, guest_local_address: Option, port_mapping_index: Option, tcp_no_delay: bool, @@ -908,6 +923,44 @@ impl SessionSocketNamespace { Ok(()) } + fn clear_host_address( + &mut self, + kind: SocketKind, + port: u16, + socket_id: u64, + ) -> BrokerResult<()> { + if kind != SocketKind::Tcp { + return Err(BrokerError::Internal); + } + let binding = self.tcp.get_mut(&port).ok_or(BrokerError::Internal)?; + if binding.socket_id != socket_id { + return Err(BrokerError::Internal); + } + binding.host_address = None; + binding.host_peer_address = None; + binding.host_mapped = false; + Ok(()) + } + + fn remove_tcp_translation_for_connector( + &mut self, + kind: SocketKind, + port: u16, + socket_id: u64, + ) { + if kind != SocketKind::Tcp { + return; + } + let connection = self.tcp.get_mut(&port).and_then(|binding| { + (binding.socket_id == socket_id) + .then(|| binding.host_address.zip(binding.host_peer_address.take())) + .flatten() + }); + if let Some(connection) = connection { + self.tcp_translations.remove(&connection); + } + } + /// Maps one accepted host connection back to its guest-local source address. /// /// The complete host connection tuple is required because several guest @@ -1104,11 +1157,28 @@ impl Reactor { { return Err(BrokerError::Internal); } - let (no_delay, keep_alive) = self + let (session_id, guest_port, no_delay, keep_alive) = self .sockets .get(&id) - .map(|socket| (socket.tcp_no_delay, socket.tcp_keep_alive)) + .and_then(|socket| { + socket.guest_local_address.map(|guest_address| { + ( + socket.session_id, + guest_address.port(), + socket.tcp_no_delay, + socket.tcp_keep_alive, + ) + }) + }) .ok_or(BrokerError::Internal)?; + if !self + .sessions + .get(&session_id) + .and_then(|namespace| namespace.tcp.get(&guest_port)) + .is_some_and(|binding| binding.socket_id == id && binding.host_mapped) + { + return Err(BrokerError::Internal); + } let replacement = socket_with( LinuxAddressFamily::INET, LinuxSocketType::STREAM, @@ -1148,6 +1218,10 @@ impl Reactor { .ok_or(BrokerError::Internal)?; state.claimed_by = None; state.reservation = Some(reservation); + self.sessions + .get_mut(&session_id) + .ok_or(BrokerError::Internal)? + .clear_host_address(SocketKind::Tcp, guest_port, id)?; update_snapshot( self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?, Some(SocketConnectionStatus::Failed(SocketError::NotConnected)), @@ -1534,6 +1608,9 @@ impl Reactor { namespace.remove_tcp_translations_for_listener(listener_address); } if let Some(address) = socket.guest_local_address { + if socket.abortive_close { + namespace.remove_tcp_translation_for_connector(socket.kind, address.port(), id); + } namespace.remove_binding(socket.kind, address.port(), id); } namespace.live_sockets = namespace @@ -1584,8 +1661,21 @@ impl Reactor { let id = event.data.u64(); if id == WAKE_TOKEN { wake = true; - } else if let Some(socket) = self.sockets.get_mut(&id) { - handle_socket_event(socket, event.flags).map_err(ReactorFailure::Broker)?; + } else { + let failed_connector = if let Some(socket) = self.sockets.get_mut(&id) { + let session_id = socket.session_id; + let guest_port = socket.guest_local_address.map(|address| address.port()); + handle_socket_event(socket, event.flags) + .map_err(ReactorFailure::Broker)? + .then_some((session_id, socket.kind, guest_port)) + } else { + None + }; + if let Some((session_id, kind, Some(guest_port))) = failed_connector + && let Some(namespace) = self.sessions.get_mut(&session_id) + { + namespace.remove_tcp_translation_for_connector(kind, guest_port, id); + } } } self.events = events; @@ -1841,6 +1931,15 @@ impl Reactor { }); let _ = response.send(host_address); } + #[cfg(test)] + ReactorCommand::TcpTranslationCount { response } => { + let count = self + .sessions + .values() + .map(|namespace| namespace.tcp_translations.len()) + .sum(); + let _ = response.send(count); + } ReactorCommand::Stop { response } => { self.sockets.clear(); self.sessions.clear(); @@ -1918,6 +2017,7 @@ impl Reactor { write_shutdown: false, peek_waitall_threshold: None, listening: false, + abortive_close: false, guest_local_address: None, port_mapping_index: None, tcp_no_delay: false, @@ -2028,6 +2128,7 @@ impl Reactor { write_shutdown: false, peek_waitall_threshold: None, listening: false, + abortive_close: false, guest_local_address: Some(local_address), port_mapping_index: None, tcp_no_delay: listener_tcp_no_delay, @@ -2656,6 +2757,7 @@ fn shutdown_socket( if mode == ShutdownMode::Abort { sockopt::set_socket_linger(&socket.socket, Some(Duration::ZERO)) .map_err(broker_error_from_errno)?; + socket.abortive_close = true; return Ok(SocketOutcome::Completed(())); } let stop_listening = mode == ShutdownMode::StopListening; @@ -2744,12 +2846,14 @@ fn shutdown_socket( } } -fn handle_socket_event(socket: &mut SocketEntry, events: epoll::EventFlags) -> BrokerResult<()> { +fn handle_socket_event(socket: &mut SocketEntry, events: epoll::EventFlags) -> BrokerResult { if socket.listening { - return update_snapshot(socket, None, readiness_from_epoll(socket, events)); + update_snapshot(socket, None, readiness_from_epoll(socket, events))?; + return Ok(false); } if socket.kind == SocketKind::Udp { - return update_snapshot(socket, None, readiness_from_epoll(socket, events)); + update_snapshot(socket, None, readiness_from_epoll(socket, events))?; + return Ok(false); } let republish_readiness = if events.contains(epoll::EventFlags::IN) && let Some(threshold) = socket.peek_waitall_threshold @@ -2770,19 +2874,28 @@ fn handle_socket_event(socket: &mut SocketEntry, events: epoll::EventFlags) -> B .lock() .expect("Linux socket snapshot mutex poisoned") .status; - let result = match status { - SocketConnectionStatus::Unconnected => Ok(()), - SocketConnectionStatus::Connecting => complete_connect(socket, events), + let failed_connector = match status { + SocketConnectionStatus::Unconnected => false, + SocketConnectionStatus::Connecting => { + matches!( + complete_connect(socket, events)?, + SocketConnectionStatus::Failed(_) + ) + } SocketConnectionStatus::Connected => { - update_snapshot(socket, None, readiness_from_epoll(socket, events)) + update_snapshot(socket, None, readiness_from_epoll(socket, events))?; + false } SocketConnectionStatus::Failed(SocketError::NotConnected) if socket.read_shutdown => { - update_snapshot(socket, None, ReadinessFlags::WRITE | ReadinessFlags::HANGUP) + update_snapshot(socket, None, ReadinessFlags::WRITE | ReadinessFlags::HANGUP)?; + false } - SocketConnectionStatus::Failed(_) => update_snapshot(socket, None, ReadinessFlags::ERROR), - _ => Err(BrokerError::Internal), + SocketConnectionStatus::Failed(_) => { + update_snapshot(socket, None, ReadinessFlags::ERROR)?; + false + } + _ => return Err(BrokerError::Internal), }; - result?; if republish_readiness { let readiness = socket .snapshot @@ -2791,10 +2904,13 @@ fn handle_socket_event(socket: &mut SocketEntry, events: epoll::EventFlags) -> B .readiness; socket.readiness.republish(readiness)?; } - Ok(()) + Ok(failed_connector) } -fn complete_connect(socket: &mut SocketEntry, events: epoll::EventFlags) -> BrokerResult<()> { +fn complete_connect( + socket: &mut SocketEntry, + events: epoll::EventFlags, +) -> BrokerResult { let status = match sockopt::socket_error(&socket.socket) { Ok(Ok(())) => match getpeername(&socket.socket) { Ok(Some(_)) => SocketConnectionStatus::Connected, @@ -2813,7 +2929,8 @@ fn complete_connect(socket: &mut SocketEntry, events: epoll::EventFlags) -> Brok SocketConnectionStatus::Failed(_) => ReadinessFlags::ERROR, _ => return Err(BrokerError::Internal), }; - update_snapshot(socket, Some(status), readiness) + update_snapshot(socket, Some(status), readiness)?; + Ok(status) } fn take_socket_error(socket: &SocketEntry) -> BrokerResult> { @@ -3422,6 +3539,40 @@ mod tests { assert!(namespace.tcp_translations.is_empty()); } + #[test] + fn retiring_failed_or_aborted_connector_removes_its_tcp_translation() { + let mut namespace = SessionSocketNamespace::default(); + let guest_port = 1000; + let socket_id = 1; + let host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); + let listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); + namespace.reserve_tcp_translation().unwrap(); + namespace + .insert_binding( + SocketKind::Tcp, + guest_port, + GuestPortBinding { + socket_id, + guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), + host_address: Some(host_address), + host_peer_address: None, + host_mapped: false, + }, + ) + .unwrap(); + namespace + .set_host_peer_address(SocketKind::Tcp, guest_port, socket_id, listener_address) + .unwrap(); + + namespace.remove_tcp_translation_for_connector(SocketKind::Tcp, guest_port, socket_id); + + assert!(namespace.tcp_translations.is_empty()); + assert_eq!( + namespace.tcp.get(&guest_port).unwrap().host_peer_address, + None + ); + } + #[test] fn guest_tcp_ports_are_session_scoped_and_do_not_bind_host_ports() { let occupied_host_listener = TcpListener::bind("127.0.0.1:0").unwrap(); @@ -3662,6 +3813,127 @@ mod tests { session.close_object_reference(accepted.handle).unwrap(); } + #[test] + fn stopped_mapped_listener_cannot_route_into_another_session() { + let host_address = unused_tcp_address(); + let provider = Arc::new( + LinuxSocketProvider::new_with_port_mappings( + 3, + &[SocketPortMapping { + guest_port: 80, + host_address, + }], + ) + .unwrap(), + ); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(6, 0, 3, 2), + provider, + ) + .unwrap(); + let first_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let second_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, _publications) = channel(); + let (retired, _retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let first_listener = create_socket(&first_session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::bind(&first_session, first_listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&first_session, first_listener, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::shutdown( + &first_session, + first_listener, + ShutdownMode::StopListening, + ), + Ok(SocketOutcome::Completed(())) + ); + + let second_listener = create_socket(&second_session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::bind(&second_session, second_listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&second_session, second_listener, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + let first_client = create_socket(&first_session, readiness); + assert_eq!( + litebox_broker_core::socket::connect(&first_session, first_client, guest_address), + Ok(SocketOutcome::Completed(SocketConnectionStatus::Failed( + SocketError::ConnectionRefused, + ))) + ); + + first_session.close_object_reference(first_client).unwrap(); + first_session + .close_object_reference(first_listener) + .unwrap(); + second_session + .close_object_reference(second_listener) + .unwrap(); + } + + #[test] + fn aborting_guest_routed_connector_releases_its_translation() { + let provider = Arc::new(LinuxSocketProvider::new(2).unwrap()); + let socket_provider: Arc = provider.clone(); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(4, 0, 2, 2), + socket_provider, + ) + .unwrap(); + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, publications) = channel(); + let (retired, _retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000); + let listener = create_socket(&session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::bind(&session, listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&session, listener, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + let client = create_socket(&session, readiness); + assert!(matches!( + litebox_broker_core::socket::connect(&session, client, guest_address), + Ok(SocketOutcome::Completed( + SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected + )) + )); + wait_until_connected(&session, client, &publications); + assert_eq!(provider.reactor.tcp_translation_count(), 1); + + assert_eq!( + litebox_broker_core::socket::shutdown(&session, client, ShutdownMode::Abort), + Ok(SocketOutcome::Completed(())) + ); + session.close_object_reference(client).unwrap(); + + assert_eq!(provider.reactor.tcp_translation_count(), 0); + session.close_object_reference(listener).unwrap(); + } + #[test] fn closing_mapped_listener_discards_more_than_sixty_four_queued_connections() { let host_address = unused_tcp_address(); From 8578e23c8ea359765f103d91f5f4065f282ced42 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 8 Aug 2026 01:24:16 -0700 Subject: [PATCH 4/6] Harden TCP handoff isolation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- .../src/socket.rs | 1703 +++++++++++++++-- litebox_broker_userland/src/main.rs | 1 + litebox_runner_linux_userland/tests/run.rs | 1 + 3 files changed, 1494 insertions(+), 211 deletions(-) diff --git a/litebox_broker_platform_linux_userland/src/socket.rs b/litebox_broker_platform_linux_userland/src/socket.rs index f5c3c3c4e..77bed6213 100644 --- a/litebox_broker_platform_linux_userland/src/socket.rs +++ b/litebox_broker_platform_linux_userland/src/socket.rs @@ -13,7 +13,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError, sync_channel}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; -use std::time::Duration; +use std::time::{Duration, Instant}; use litebox_broker_core::socket::{ AcceptedPlatformSocket, PlatformBindKind, PlatformConnectError, PlatformDatagramReceive, @@ -45,6 +45,7 @@ const MAX_QUEUED_SOCKET_COMMANDS: usize = 64; const MAX_EPOLL_EVENTS: usize = 64; const MAX_STALE_PORT_MAPPING_CONNECTIONS: usize = MAX_TCP_LISTEN_BACKLOG as usize + 3; const MAX_RETAINED_TRANSLATIONS: usize = 1 << 14; +const PENDING_CONNECT_DISCARD_LIFETIME: Duration = Duration::from_mins(5); /// Linux-userland socket provider. /// @@ -69,17 +70,25 @@ struct PortMappingState { mapping: SocketPortMapping, reservation: Option, claimed_by: Option, + stale_connections: HashMap<(SocketAddrV4, SocketAddrV4), StaleTcpConnection>, +} + +struct StaleTcpConnection { + session_id: SessionId, + deadline: Option, + retained_connector: Option, } impl LinuxSocketProvider { - /// Starts a provider whose reactor tracks at most `max_sockets` resources. - pub fn new(max_sockets: usize) -> IoResult { - Self::new_with_port_mappings(max_sockets, &[]) + /// Starts a provider with matching global and per-session socket limits. + pub fn new(max_sockets: usize, max_sockets_per_session: usize) -> IoResult { + Self::new_with_port_mappings(max_sockets, max_sockets_per_session, &[]) } - /// Starts a provider and reserves every mapped host endpoint immediately. + /// Starts a limited provider and reserves every mapped host endpoint immediately. pub fn new_with_port_mappings( max_sockets: usize, + max_sockets_per_session: usize, port_mappings: &[SocketPortMapping], ) -> IoResult { for (index, mapping) in port_mappings.iter().enumerate() { @@ -101,7 +110,11 @@ impl LinuxSocketProvider { } let port_mappings = port_mappings.to_vec(); Ok(Self { - reactor: Arc::new(ReactorClient::start(max_sockets, port_mappings.clone())?), + reactor: Arc::new(ReactorClient::start( + max_sockets, + max_sockets_per_session, + port_mappings.clone(), + )?), port_mappings, }) } @@ -393,16 +406,23 @@ struct ReactorClient { } impl ReactorClient { - fn start(max_sockets: usize, port_mappings: Vec) -> IoResult { + fn start( + max_sockets: usize, + max_sockets_per_session: usize, + port_mappings: Vec, + ) -> IoResult { let epoll_fd = epoll::create(epoll::CreateFlags::CLOEXEC)?; let wake = Arc::new(eventfd(0, EventfdFlags::CLOEXEC | EventfdFlags::NONBLOCK)?); let port_mappings = port_mappings .into_iter() .map(|mapping| { + let mut stale_connections = HashMap::new(); + stale_connections.try_reserve(MAX_RETAINED_TRANSLATIONS)?; Ok(PortMappingState { mapping, reservation: Some(create_port_mapping_reservation(mapping, false, false)?), claimed_by: None, + stale_connections, }) }) .collect::>>()?; @@ -436,6 +456,8 @@ impl ReactorClient { sessions: HashMap::new(), port_mappings, max_sockets, + max_sockets_per_session, + retained_connectors: 0, peek_cache: None, events, }; @@ -531,8 +553,8 @@ impl ReactorClient { { return; } - // Do not release the core's socket quota until the reactor has dropped - // the descriptor, even if the redundant wake write fails. + // Wait until the reactor has dropped the descriptor or transferred it + // into retained-connector accounting, even if the wake write fails. let _ = self.signal(); let _ = receive.recv(); } @@ -561,6 +583,26 @@ impl ReactorClient { receive.recv().unwrap() } + #[cfg(test)] + fn stale_tcp_translation_count(&self) -> usize { + let (response, receive) = sync_channel(1); + self.commands + .send(ReactorCommand::StaleTcpTranslationCount { response }) + .unwrap(); + self.signal().unwrap(); + receive.recv().unwrap() + } + + #[cfg(test)] + fn retained_connector_count(&self) -> usize { + let (response, receive) = sync_channel(1); + self.commands + .send(ReactorCommand::RetainedConnectorCount { response }) + .unwrap(); + self.signal().unwrap(); + receive.recv().unwrap() + } + fn close_session(&self, session_id: SessionId) { let (response, receive) = sync_channel(1); if self @@ -717,6 +759,14 @@ enum ReactorCommand { TcpTranslationCount { response: SyncSender, }, + #[cfg(test)] + StaleTcpTranslationCount { + response: SyncSender, + }, + #[cfg(test)] + RetainedConnectorCount { + response: SyncSender, + }, Stop { response: SyncSender<()>, }, @@ -742,6 +792,25 @@ struct AcceptedEndpoints { remote_address: SocketAddrV4, } +enum AcceptedTcpPeer { + Guest(SocketAddrV4), + Native(SocketAddrV4), + Stale, +} + +#[derive(Clone, Copy)] +enum ConnectorTranslationDisposition { + Retain, + Discard(Option), +} + +struct RetiredTcpConnector { + connection: (SocketAddrV4, SocketAddrV4), + mapping_index: Option, + retained_in_namespace: bool, + unplaced_connector: Option, +} + /// State owned and accessed exclusively by the socket reactor thread. struct Reactor { epoll: OwnedFd, @@ -751,6 +820,8 @@ struct Reactor { sessions: HashMap, port_mappings: Vec, max_sockets: usize, + max_sockets_per_session: usize, + retained_connectors: usize, peek_cache: Option, events: Vec, } @@ -786,17 +857,28 @@ struct SessionSocketNamespace { tcp: HashMap, // Pending accepts can outlive the connecting socket, so a translation is // retained separately from its guest port binding until accept consumes it. - tcp_translations: HashMap<(SocketAddrV4, SocketAddrV4), SocketAddrV4>, + tcp_translations: HashMap<(SocketAddrV4, SocketAddrV4), TcpPeerTranslation>, live_sockets: usize, + retained_connectors: usize, closing: bool, } +struct TcpPeerTranslation { + guest_address: SocketAddrV4, + listener_id: u64, + discard_on_accept: bool, + discard_deadline: Option, + // Delay an established abort until accept can identify and discard its peer. + retained_connector: Option, +} + #[derive(Clone, Copy)] struct GuestPortBinding { socket_id: u64, guest_address: SocketAddrV4, host_address: Option, host_peer_address: Option, + host_peer_mapping_index: Option, host_mapped: bool, } @@ -842,8 +924,23 @@ impl SessionSocketNamespace { .map_err(|_| BrokerError::OutOfMemory) } - fn reserve_tcp_translation(&mut self) -> BrokerResult<()> { - if self.tcp_translations.len() >= MAX_RETAINED_TRANSLATIONS { + fn reserve_tcp_translation( + &mut self, + session_stale: usize, + total_stale: usize, + max_session_translations: usize, + ) -> BrokerResult<()> { + if self + .tcp_translations + .len() + .checked_add(session_stale) + .is_none_or(|count| count >= max_session_translations) + || self + .tcp_translations + .len() + .checked_add(total_stale) + .is_none_or(|count| count >= MAX_RETAINED_TRANSLATIONS) + { return Err(BrokerError::ResourceExhausted); } self.tcp_translations @@ -903,6 +1000,8 @@ impl SessionSocketNamespace { port: u16, socket_id: u64, host_peer_address: SocketAddrV4, + listener_id: u64, + mapping_index: Option, ) -> BrokerResult<()> { if kind != SocketKind::Tcp { return Err(BrokerError::Internal); @@ -913,13 +1012,22 @@ impl SessionSocketNamespace { return Err(BrokerError::Internal); } binding.host_peer_address = Some(host_peer_address); + binding.host_peer_mapping_index = mapping_index; ( binding.host_address.ok_or(BrokerError::Internal)?, binding.guest_address, ) }; - self.tcp_translations - .insert((host_address, host_peer_address), guest_address); + self.tcp_translations.insert( + (host_address, host_peer_address), + TcpPeerTranslation { + guest_address, + listener_id, + discard_on_accept: false, + discard_deadline: None, + retained_connector: None, + }, + ); Ok(()) } @@ -938,6 +1046,7 @@ impl SessionSocketNamespace { } binding.host_address = None; binding.host_peer_address = None; + binding.host_peer_mapping_index = None; binding.host_mapped = false; Ok(()) } @@ -947,46 +1056,199 @@ impl SessionSocketNamespace { kind: SocketKind, port: u16, socket_id: u64, - ) { + ) -> Option { if kind != SocketKind::Tcp { - return; + return None; } - let connection = self.tcp.get_mut(&port).and_then(|binding| { + let retired = self.tcp.get_mut(&port).and_then(|binding| { (binding.socket_id == socket_id) - .then(|| binding.host_address.zip(binding.host_peer_address.take())) + .then(|| { + binding + .host_address + .zip(binding.host_peer_address.take()) + .map(|connection| (connection, binding.host_peer_mapping_index.take())) + }) .flatten() }); - if let Some(connection) = connection { - self.tcp_translations.remove(&connection); + retired.map(|(connection, mapping_index)| { + let retained_connector = self + .tcp_translations + .remove(&connection) + .is_some_and(|translation| translation.retained_connector.is_some()); + if retained_connector { + self.retained_connectors = self + .retained_connectors + .checked_sub(1) + .expect("session retained connector count underflow"); + } + RetiredTcpConnector { + connection, + mapping_index, + retained_in_namespace: retained_connector, + unplaced_connector: None, + } + }) + } + + fn retire_tcp_translation_for_connector( + &mut self, + kind: SocketKind, + port: u16, + socket_id: u64, + discard_on_accept: bool, + discard_deadline: Option, + mut retained_connector: Option, + ) -> Option { + if kind != SocketKind::Tcp { + return None; } + let retired = self.tcp.get_mut(&port).and_then(|binding| { + (binding.socket_id == socket_id) + .then(|| { + let connection = binding.host_address.zip(binding.host_peer_address.take()); + connection + .map(|connection| (connection, binding.host_peer_mapping_index.take())) + }) + .flatten() + }); + retired.map(|(connection, mapping_index)| { + let mut retained = false; + if let Some(translation) = self.tcp_translations.get_mut(&connection) { + translation.discard_on_accept = discard_on_accept; + translation.discard_deadline = discard_deadline; + retained = retained_connector.is_some(); + translation.retained_connector = retained_connector.take(); + self.retained_connectors = self + .retained_connectors + .checked_add(usize::from(retained)) + .expect("session retained connector count overflow"); + } + RetiredTcpConnector { + connection, + mapping_index, + retained_in_namespace: retained, + unplaced_connector: retained_connector, + } + }) } - /// Maps one accepted host connection back to its guest-local source address. - /// - /// The complete host connection tuple is required because several guest - /// sockets may share one private host endpoint. - fn translate_tcp_peer( + fn expire_tcp_discard_markers(&mut self, now: Instant) -> usize { + let mut released_connectors = 0; + self.tcp_translations.retain(|_, translation| { + let retain = !translation.discard_on_accept + || translation + .discard_deadline + .is_none_or(|deadline| deadline > now); + if !retain && translation.retained_connector.is_some() { + released_connectors += 1; + } + retain + }); + self.retained_connectors = self + .retained_connectors + .checked_sub(released_connectors) + .expect("session retained connector count underflow"); + released_connectors + } + + fn take_tcp_peer_translation( &mut self, remote_address: SocketAddrV4, local_address: SocketAddrV4, - ) -> SocketAddrV4 { - self.tcp_translations + ) -> Option { + let translation = self + .tcp_translations .remove(&(remote_address, local_address)) .or_else(|| { self.tcp_translations.remove(&( remote_address, SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, local_address.port()), )) - }) - .unwrap_or(remote_address) + }); + if translation + .as_ref() + .is_some_and(|translation| translation.retained_connector.is_some()) + { + self.retained_connectors = self + .retained_connectors + .checked_sub(1) + .expect("session retained connector count underflow"); + } + translation + } + + fn remove_tcp_translations_for_listener(&mut self, listener_address: SocketAddrV4) -> usize { + let mut released_connectors = 0; + self.tcp_translations + .retain(|(_, destination), translation| { + let retain = destination.port() != listener_address.port() + || (!listener_address.ip().is_unspecified() + && destination.ip() != listener_address.ip()); + if !retain && translation.retained_connector.is_some() { + released_connectors += 1; + } + retain + }); + self.retained_connectors = self + .retained_connectors + .checked_sub(released_connectors) + .expect("session retained connector count underflow"); + released_connectors } - fn remove_tcp_translations_for_listener(&mut self, listener_address: SocketAddrV4) { - self.tcp_translations.retain(|(_, destination), _| { - destination.port() != listener_address.port() - || (!listener_address.ip().is_unspecified() - && destination.ip() != listener_address.ip()) + fn move_tcp_translations_for_listener( + &mut self, + session_id: SessionId, + listener_address: SocketAddrV4, + stale_connections: &mut HashMap<(SocketAddrV4, SocketAddrV4), StaleTcpConnection>, + ) -> usize { + let retirement_deadline = Instant::now() + PENDING_CONNECT_DISCARD_LIFETIME; + let mut released_connectors = 0; + self.tcp_translations.retain(|connection, translation| { + let destination = connection.1; + let matches = destination.port() == listener_address.port() + && (listener_address.ip().is_unspecified() + || destination.ip() == listener_address.ip()); + if matches { + let mut deadline = translation + .discard_deadline + .map(|deadline| deadline.max(retirement_deadline)); + let mut retained_connector = translation.retained_connector.take(); + if retained_connector.is_some() { + deadline = None; + } + match stale_connections.entry(*connection) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(StaleTcpConnection { + session_id, + deadline, + retained_connector, + }); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + let existing = entry.get_mut(); + existing.deadline = match (existing.deadline, deadline) { + (None, _) | (_, None) => None, + (Some(existing), Some(deadline)) => Some(existing.max(deadline)), + }; + if existing.session_id == session_id + && existing.retained_connector.is_none() + { + existing.retained_connector = retained_connector.take(); + } + if retained_connector.is_some() { + released_connectors += 1; + } + } + } + } + !matches }); + self.retained_connectors = self + .retained_connectors + .checked_sub(released_connectors) + .expect("session retained connector count underflow"); + released_connectors } } @@ -1089,10 +1351,9 @@ impl Reactor { }; reservation }; - let preparation = (|| { - sockopt::set_socket_linger(&reservation, None).map_err(broker_error_from_errno)?; - drain_tcp_listener(&reservation) - })(); + let preparation = sockopt::set_socket_linger(&reservation, None) + .map_err(broker_error_from_errno) + .and_then(|()| self.drain_tcp_listener(&reservation, mapping_index)); let stale_state_drained = match preparation { Ok(drained) => drained, Err(error) => { @@ -1127,6 +1388,39 @@ impl Reactor { Ok(SocketOutcome::Completed(host_address)) } + fn drain_tcp_listener(&mut self, socket: &OwnedFd, mapping_index: usize) -> BrokerResult { + for _ in 0..MAX_STALE_PORT_MAPPING_CONNECTIONS { + match acceptfrom_with( + socket, + LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, + ) { + Ok((accepted, remote_address)) => { + if let Some(remote_address) = remote_address { + let remote_address = SocketAddrV4::try_from(remote_address) + .map_err(|_| BrokerError::Internal)?; + let local_address = local_socket_address(&accepted)?; + self.take_stale_tcp_connection( + mapping_index, + remote_address, + local_address, + ); + self.take_tcp_peer_translations_except(None, remote_address, local_address); + } + drop(accepted); + } + Err(Errno::INTR) => {} + Err(Errno::AGAIN | Errno::INVAL) => return Ok(true), + Err(error) => { + // Linux reports pending per-connection network errors from + // accept. Skip stale connection failures while preserving + // broker resource failures. + let _ = socket_operation_error_from_errno(error)?; + } + } + } + Ok(false) + } + fn stop_listening_socket(&mut self, id: u64) -> BrokerResult> { let mapping_index = { let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; @@ -1218,16 +1512,78 @@ impl Reactor { .ok_or(BrokerError::Internal)?; state.claimed_by = None; state.reservation = Some(reservation); + Ok(SocketOutcome::Completed(())) + } + + fn release_failed_port_mapping_claim( + &mut self, + id: u64, + mapping_index: usize, + ) -> BrokerResult<()> { + if !self + .port_mappings + .get(mapping_index) + .is_some_and(|state| state.claimed_by == Some(id) && state.reservation.is_none()) + { + return Err(BrokerError::Internal); + } + let (session_id, guest_port, no_delay, keep_alive) = self + .sockets + .get(&id) + .and_then(|socket| { + (!socket.listening).then_some(())?; + socket.guest_local_address.map(|guest_address| { + ( + socket.session_id, + guest_address.port(), + socket.tcp_no_delay, + socket.tcp_keep_alive, + ) + }) + }) + .ok_or(BrokerError::Internal)?; + if !self + .sessions + .get(&session_id) + .and_then(|namespace| namespace.tcp.get(&guest_port)) + .is_some_and(|binding| binding.socket_id == id && binding.host_mapped) + { + return Err(BrokerError::Internal); + } + let replacement = socket_with( + LinuxAddressFamily::INET, + LinuxSocketType::STREAM, + LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, + Some(ipproto::TCP), + ) + .map_err(broker_error_from_errno)?; + apply_tcp_options(&replacement, no_delay, keep_alive)?; + epoll::add( + &self.epoll, + &replacement, + epoll::EventData::new_u64(id), + idle_epoll_events(), + ) + .map_err(broker_error_from_errno)?; + let old_socket = &self.sockets.get(&id).ok_or(BrokerError::Internal)?.socket; + if !delete_epoll_registration(&self.epoll, old_socket) { + let _ = delete_epoll_registration(&self.epoll, &replacement); + return Err(BrokerError::Internal); + } + let reservation = { + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + core::mem::replace(&mut socket.socket, replacement) + }; + let state = self + .port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)?; + state.claimed_by = None; + state.reservation = Some(reservation); self.sessions .get_mut(&session_id) .ok_or(BrokerError::Internal)? - .clear_host_address(SocketKind::Tcp, guest_port, id)?; - update_snapshot( - self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?, - Some(SocketConnectionStatus::Failed(SocketError::NotConnected)), - ReadinessFlags::WRITE | ReadinessFlags::HANGUP, - )?; - Ok(SocketOutcome::Completed(())) + .clear_host_address(SocketKind::Tcp, guest_port, id) } /// Replaces a socket's descriptor, returning ownership of the replacement on failure. @@ -1308,7 +1664,7 @@ impl Reactor { session_id: SessionId, kind: SocketKind, mut address: SocketAddrV4, - ) -> BrokerResult> { + ) -> BrokerResult)>> { if address.ip().is_unspecified() { address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, address.port()); } @@ -1321,17 +1677,20 @@ impl Reactor { Some(host_address) if host_address.ip().is_unspecified() => { Ok(SocketOutcome::Completed(( SocketAddrV4::new(*address.ip(), host_address.port()), - true, + Some(binding.socket_id), ))) } - Some(host_address) => Ok(SocketOutcome::Completed((host_address, true))), + Some(host_address) => Ok(SocketOutcome::Completed(( + host_address, + Some(binding.socket_id), + ))), None => Ok(SocketOutcome::Failed(SocketError::ConnectionRefused)), }; } if self.is_private_host_endpoint(kind, address)? { Ok(SocketOutcome::Failed(SocketError::ConnectionRefused)) } else { - Ok(SocketOutcome::Completed((address, false))) + Ok(SocketOutcome::Completed((address, None))) } } @@ -1403,6 +1762,7 @@ impl Reactor { guest_address, host_address: None, host_peer_address: None, + host_peer_mapping_index: None, host_mapped: false, }, )?; @@ -1462,9 +1822,21 @@ impl Reactor { } let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; let host_mapped = socket.port_mapping_index.is_some(); - match listen_tcp_socket(&self.epoll, id, socket, backlog, host_mapped)? { + let was_listening = socket.listening; + let outcome = listen_tcp_socket(&self.epoll, id, socket, backlog, host_mapped)?; + match outcome { SocketOutcome::Completed(()) => Ok(SocketOutcome::Completed(guest_address)), - SocketOutcome::Failed(error) => Ok(SocketOutcome::Failed(error)), + SocketOutcome::Failed(error) => { + if host_mapped && !was_listening { + let mapping_index = self + .sockets + .get(&id) + .and_then(|socket| socket.port_mapping_index) + .ok_or(BrokerError::Internal)?; + self.release_failed_port_mapping_claim(id, mapping_index)?; + } + Ok(SocketOutcome::Failed(error)) + } } } @@ -1485,6 +1857,27 @@ impl Reactor { ))?; return connect_datagram_socket(socket, address); } + let claims_mapping = self + .sockets + .get(&id) + .and_then(|socket| socket.port_mapping_index) + .is_some_and(|mapping_index| { + self.port_mappings + .get(mapping_index) + .is_some_and(|state| state.claimed_by == Some(id)) + }); + if claims_mapping { + let status = SocketConnectionStatus::Failed(SocketError::InvalidArgument); + update_snapshot( + self.sockets + .get(&id) + .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))?, + Some(status), + ReadinessFlags::ERROR, + ) + .map_err(PlatformConnectError::PeerIndeterminate)?; + return Ok(status); + } self.connect_guest_tcp_socket(id, address) } @@ -1503,21 +1896,58 @@ impl Reactor { } socket.session_id }; - let (network_address, guest_routed) = match self + let (network_address, guest_listener_id) = match self .resolve_guest_destination(session_id, SocketKind::Tcp, guest_address) .map_err(PlatformConnectError::PeerUnchanged)? { SocketOutcome::Completed(destination) => destination, - SocketOutcome::Failed(error) => return Ok(SocketConnectionStatus::Failed(error)), + SocketOutcome::Failed(error) => { + let status = SocketConnectionStatus::Failed(error); + update_snapshot( + self.sockets + .get(&id) + .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))?, + Some(status), + ReadinessFlags::ERROR, + ) + .map_err(PlatformConnectError::PeerIndeterminate)?; + return Ok(status); + } }; - if guest_routed { - self.sessions + let guest_mapping_index = guest_listener_id + .and_then(|listener_id| self.sockets.get(&listener_id)) + .and_then(|listener| listener.port_mapping_index); + if guest_listener_id.is_some() { + let now = Instant::now(); + for state in &mut self.port_mappings { + state.stale_connections.retain(|_, stale| { + stale.retained_connector.is_some() + || stale.deadline.is_none_or(|deadline| deadline > now) + }); + } + let session_stale = count_session_stale_connections( + self.port_mappings + .iter() + .flat_map(|state| state.stale_connections.values()), + session_id, + ); + let total_stale = guest_mapping_index + .and_then(|mapping_index| self.port_mappings.get(mapping_index)) + .map_or(0, |state| state.stale_connections.len()); + let namespace = self + .sessions .get_mut(&session_id) - .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))? - .reserve_tcp_translation() + .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))?; + let released_connectors = namespace.expire_tcp_discard_markers(now); + self.retained_connectors = self + .retained_connectors + .checked_sub(released_connectors) + .expect("reactor retained connector count underflow"); + namespace + .reserve_tcp_translation(session_stale, total_stale, self.max_sockets_per_session) .map_err(PlatformConnectError::PeerUnchanged)?; } - let outcome = { + let (outcome, readiness) = { let socket = self .sockets .get_mut(&id) @@ -1544,6 +1974,25 @@ impl Reactor { .map_err(PlatformConnectError::PeerIndeterminate)?, ) }; + if guest_listener_id.is_some() { + if let Some(mapping_index) = guest_mapping_index { + self.port_mappings.get(mapping_index).ok_or( + PlatformConnectError::PeerIndeterminate(BrokerError::Internal), + )?; + self.take_stale_tcp_connection(mapping_index, host_address, network_address); + } + self.take_tcp_peer_translations_except( + Some(session_id), + host_address, + network_address, + ); + self.sessions.retain(|_, namespace| { + !namespace.closing + || namespace.live_sockets != 0 + || namespace.retained_connectors != 0 + || !namespace.tcp_translations.is_empty() + }); + } let namespace = self.sessions.get_mut(&session_id).ok_or( PlatformConnectError::PeerIndeterminate(BrokerError::Internal), )?; @@ -1556,20 +2005,55 @@ impl Reactor { false, ) .map_err(PlatformConnectError::PeerIndeterminate)?; - if guest_routed { + if let Some(listener_id) = guest_listener_id { namespace .set_host_peer_address( SocketKind::Tcp, local_guest_address.port(), id, network_address, + listener_id, + guest_mapping_index, ) .map_err(PlatformConnectError::PeerIndeterminate)?; } } + update_snapshot( + self.sockets + .get(&id) + .ok_or(PlatformConnectError::PeerIndeterminate( + BrokerError::Internal, + ))?, + Some(outcome), + readiness, + ) + .map_err(PlatformConnectError::PeerIndeterminate)?; Ok(outcome) } + fn remove_tcp_translation_for_connector( + &mut self, + session_id: SessionId, + kind: SocketKind, + guest_port: u16, + socket_id: u64, + ) { + let retired = self.sessions.get_mut(&session_id).and_then(|namespace| { + namespace.remove_tcp_translation_for_connector(kind, guest_port, socket_id) + }); + if let Some(retired) = retired { + self.retained_connectors = self + .retained_connectors + .checked_sub(usize::from(retired.retained_in_namespace)) + .expect("reactor retained connector count underflow"); + if let Some(mapping_index) = retired.mapping_index { + self.port_mappings[mapping_index] + .stale_connections + .remove(&retired.connection); + } + } + } + /// Drops a socket, releasing its guest port and any claimed host mapping. fn remove_socket(&mut self, id: u64) { let port_mapping = self @@ -1602,44 +2086,172 @@ impl Reactor { let Some(socket) = self.sockets.remove(&id) else { return; }; - let session_id = socket.session_id; - let remove_namespace = if let Some(namespace) = self.sessions.get_mut(&session_id) { - if let Some(listener_address) = retired_listener_address { - namespace.remove_tcp_translations_for_listener(listener_address); - } - if let Some(address) = socket.guest_local_address { - if socket.abortive_close { - namespace.remove_tcp_translation_for_connector(socket.kind, address.port(), id); + let SocketEntry { + socket, + session_id, + kind, + snapshot, + abortive_close, + guest_local_address, + .. + } = socket; + let mut socket = Some(socket); + let connecting = snapshot + .lock() + .expect("Linux socket snapshot mutex poisoned") + .status + == SocketConnectionStatus::Connecting; + let translation_disposition = + match getpeername(socket.as_ref().expect("removed socket descriptor missing")) { + Ok(Some(_)) if abortive_close => ConnectorTranslationDisposition::Discard(None), + Ok(Some(_)) => ConnectorTranslationDisposition::Retain, + Ok(None) | Err(_) => { + if abortive_close || connecting { + let _ = sockopt::set_socket_linger( + socket.as_ref().expect("removed socket descriptor missing"), + Some(Duration::ZERO), + ); + } + ConnectorTranslationDisposition::Discard(Some( + Instant::now() + PENDING_CONNECT_DISCARD_LIFETIME, + )) } - namespace.remove_binding(socket.kind, address.port(), id); - } - namespace.live_sockets = namespace - .live_sockets - .checked_sub(1) - .expect("session socket count underflow"); - namespace.closing && namespace.live_sockets == 0 - } else { - false - }; - if remove_namespace { - self.sessions.remove(&session_id); - } - let replacement_reservation = if retain_original { - let SocketEntry { socket, .. } = socket; - Some(socket) - } else { - drop(socket); - port_mapping - .and_then(|(_, mapping)| create_replacement_port_mapping_reservation(mapping).ok()) - }; - if let Some((mapping_index, _)) = port_mapping - && let Some(state) = self.port_mappings.get_mut(mapping_index) - && state.claimed_by == Some(id) + }; + if retired_listener_address.is_some() + && let Some((mapping_index, _)) = port_mapping + && !retain_original { - state.claimed_by = None; - state.reservation = replacement_reservation; + self.clear_stale_tcp_connections(mapping_index); } - } + let (discarded_connector, retained_before, retained_after) = + if let Some(namespace) = self.sessions.get_mut(&session_id) { + let retained_before = namespace.retained_connectors; + if let Some(listener_address) = retired_listener_address { + if let Some((mapping_index, _)) = port_mapping { + let state = &mut self.port_mappings[mapping_index]; + if retain_original { + namespace.move_tcp_translations_for_listener( + session_id, + listener_address, + &mut state.stale_connections, + ); + } else { + namespace.remove_tcp_translations_for_listener(listener_address); + } + } else { + namespace.remove_tcp_translations_for_listener(listener_address); + } + } + let mut discarded_connector = None; + if let Some(address) = guest_local_address { + match translation_disposition { + ConnectorTranslationDisposition::Retain => { + let retained_connector = + socket.take().expect("removed socket descriptor missing"); + discarded_connector = namespace.retire_tcp_translation_for_connector( + kind, + address.port(), + id, + false, + None, + Some(retained_connector), + ); + } + ConnectorTranslationDisposition::Discard(discard_deadline) => { + let retained_connector = discard_deadline + .is_none() + .then(|| socket.take().expect("removed socket descriptor missing")); + discarded_connector = namespace.retire_tcp_translation_for_connector( + kind, + address.port(), + id, + true, + discard_deadline, + retained_connector, + ); + } + } + if let Some(retired) = discarded_connector.as_mut() + && let Some(mapping_index) = retired.mapping_index + && let Some(stale) = self.port_mappings[mapping_index] + .stale_connections + .get_mut(&retired.connection) + && stale.session_id == session_id + { + stale.deadline = match (stale.deadline, translation_disposition) { + (existing, ConnectorTranslationDisposition::Discard(None)) => existing, + (_, ConnectorTranslationDisposition::Retain) => None, + (None, ConnectorTranslationDisposition::Discard(Some(deadline))) => { + Some(deadline) + } + ( + Some(existing), + ConnectorTranslationDisposition::Discard(Some(deadline)), + ) => Some(existing.max(deadline)), + }; + if stale.retained_connector.is_none() + && let Some(retained_connector) = retired.unplaced_connector.take() + { + stale.deadline = None; + stale.retained_connector = Some(retained_connector); + namespace.retained_connectors = namespace + .retained_connectors + .checked_add(1) + .expect("session retained connector count overflow"); + } + } + namespace.remove_binding(kind, address.port(), id); + } + namespace.live_sockets = namespace + .live_sockets + .checked_sub(1) + .expect("session socket count underflow"); + ( + discarded_connector, + retained_before, + namespace.retained_connectors, + ) + } else { + (None, 0, 0) + }; + self.retained_connectors = self + .retained_connectors + .checked_sub(retained_before) + .and_then(|count| count.checked_add(retained_after)) + .expect("reactor retained connector count invariant violated"); + drop(discarded_connector); + if self + .sessions + .get(&session_id) + .is_some_and(|namespace| namespace.closing) + { + self.retire_session_stale_connectors(session_id); + } + let remove_namespace = self.sessions.get(&session_id).is_some_and(|namespace| { + namespace.closing && namespace.live_sockets == 0 && namespace.retained_connectors == 0 + }); + if remove_namespace { + self.sessions.remove(&session_id); + } + let replacement_reservation = if retain_original { + Some( + socket + .take() + .expect("mapping claimant descriptor unexpectedly retained"), + ) + } else { + drop(socket.take()); + port_mapping + .and_then(|(_, mapping)| create_replacement_port_mapping_reservation(mapping).ok()) + }; + if let Some((mapping_index, _)) = port_mapping + && let Some(state) = self.port_mappings.get_mut(mapping_index) + && state.claimed_by == Some(id) + { + state.claimed_by = None; + state.reservation = replacement_reservation; + } + } fn run(&mut self) -> core::result::Result<(), ReactorFailure> { loop { @@ -1671,10 +2283,8 @@ impl Reactor { } else { None }; - if let Some((session_id, kind, Some(guest_port))) = failed_connector - && let Some(namespace) = self.sessions.get_mut(&session_id) - { - namespace.remove_tcp_translation_for_connector(kind, guest_port, id); + if let Some((session_id, kind, Some(guest_port))) = failed_connector { + self.remove_tcp_translation_for_connector(session_id, kind, guest_port, id); } } } @@ -1841,8 +2451,19 @@ impl Reactor { let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; (socket.kind == SocketKind::Tcp && socket.listening) .then(|| { - local_socket_address(&socket.socket) - .map(|address| (socket.session_id, address)) + local_socket_address(&socket.socket).and_then(|address| { + socket + .guest_local_address + .map(|guest_address| { + ( + socket.session_id, + address, + guest_address.port(), + socket.port_mapping_index, + ) + }) + .ok_or(BrokerError::Internal) + }) }) .transpose()? } else { @@ -1857,10 +2478,37 @@ impl Reactor { .and_then(|socket| shutdown_socket(socket, mode))? }; if matches!(outcome, SocketOutcome::Completed(())) - && let Some((session_id, listener_address)) = retired_listener - && let Some(namespace) = self.sessions.get_mut(&session_id) + && let Some((session_id, listener_address, guest_port, mapping_index)) = + retired_listener { - namespace.remove_tcp_translations_for_listener(listener_address); + let namespace = self + .sessions + .get_mut(&session_id) + .ok_or(BrokerError::Internal)?; + let released_connectors = if let Some(mapping_index) = mapping_index { + let stale_connections = &mut self + .port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)? + .stale_connections; + namespace.move_tcp_translations_for_listener( + session_id, + listener_address, + stale_connections, + ) + } else { + namespace.remove_tcp_translations_for_listener(listener_address) + }; + self.retained_connectors = self + .retained_connectors + .checked_sub(released_connectors) + .expect("reactor retained connector count underflow"); + namespace.clear_host_address(SocketKind::Tcp, guest_port, id)?; + update_snapshot( + self.sockets.get(&id).ok_or(BrokerError::Internal)?, + Some(SocketConnectionStatus::Failed(SocketError::NotConnected)), + ReadinessFlags::WRITE | ReadinessFlags::HANGUP, + )?; } Ok(outcome) })(); @@ -1911,9 +2559,12 @@ impl Reactor { } => { if let Some(namespace) = self.sessions.get_mut(&session_id) { namespace.closing = true; - if namespace.live_sockets == 0 { - self.sessions.remove(&session_id); - } + } + self.retire_session_stale_connectors(session_id); + if self.sessions.get(&session_id).is_some_and(|namespace| { + namespace.live_sockets == 0 && namespace.retained_connectors == 0 + }) { + self.sessions.remove(&session_id); } let _ = response.send(()); } @@ -1940,6 +2591,19 @@ impl Reactor { .sum(); let _ = response.send(count); } + #[cfg(test)] + ReactorCommand::StaleTcpTranslationCount { response } => { + let count = self + .port_mappings + .iter() + .map(|state| state.stale_connections.len()) + .sum(); + let _ = response.send(count); + } + #[cfg(test)] + ReactorCommand::RetainedConnectorCount { response } => { + let _ = response.send(self.retained_connectors); + } ReactorCommand::Stop { response } => { self.sockets.clear(); self.sessions.clear(); @@ -1959,7 +2623,12 @@ impl Reactor { readiness: ReadinessRegistration, snapshot: Arc>, ) -> BrokerResult<()> { - if self.sockets.len() >= self.max_sockets { + if self + .sockets + .len() + .checked_add(self.retained_connectors) + .is_none_or(|count| count >= self.max_sockets) + { return Err(BrokerError::ResourceExhausted); } let kind = socket_kind(request).ok_or(BrokerError::Internal)?; @@ -1970,6 +2639,13 @@ impl Reactor { if namespace.closing { return Err(BrokerError::UnknownObject); } + if namespace + .live_sockets + .checked_add(namespace.retained_connectors) + .is_none_or(|count| count >= self.max_sockets_per_session) + { + return Err(BrokerError::ResourceExhausted); + } let (linux_type, protocol, epoll_events, initial_readiness) = match kind { SocketKind::Tcp => ( LinuxSocketType::STREAM, @@ -2031,6 +2707,197 @@ impl Reactor { Ok(()) } + fn take_tcp_peer_translation( + &mut self, + listener_session_id: SessionId, + listener_id: u64, + mapping_index: Option, + remote_address: SocketAddrV4, + local_address: SocketAddrV4, + ) -> AcceptedTcpPeer { + let now = Instant::now(); + if let Some(mapping_index) = mapping_index { + let stale_connections = &mut self.port_mappings[mapping_index].stale_connections; + stale_connections.retain(|_, stale| { + stale.retained_connector.is_some() + || stale.deadline.is_none_or(|deadline| deadline > now) + }); + if self.take_stale_tcp_connection(mapping_index, remote_address, local_address) { + return AcceptedTcpPeer::Stale; + } + } + let released_connectors = self + .sessions + .values_mut() + .map(|namespace| namespace.expire_tcp_discard_markers(now)) + .sum::(); + self.retained_connectors = self + .retained_connectors + .checked_sub(released_connectors) + .expect("reactor retained connector count underflow"); + let current_translation = + self.sessions + .get_mut(&listener_session_id) + .and_then(|namespace| { + namespace.take_tcp_peer_translation(remote_address, local_address) + }); + if let Some(translation) = current_translation { + self.retained_connectors = self + .retained_connectors + .checked_sub(usize::from(translation.retained_connector.is_some())) + .expect("reactor retained connector count underflow"); + drop(translation.retained_connector); + if translation.discard_on_accept { + AcceptedTcpPeer::Stale + } else if translation.listener_id == listener_id { + AcceptedTcpPeer::Guest(translation.guest_address) + } else { + AcceptedTcpPeer::Stale + } + } else { + let stale = self.take_tcp_peer_translations_except( + Some(listener_session_id), + remote_address, + local_address, + ); + if stale { + AcceptedTcpPeer::Stale + } else { + AcceptedTcpPeer::Native(remote_address) + } + } + } + + fn take_stale_tcp_connection( + &mut self, + mapping_index: usize, + remote_address: SocketAddrV4, + local_address: SocketAddrV4, + ) -> bool { + let stale = { + let stale_connections = &mut self.port_mappings[mapping_index].stale_connections; + stale_connections + .remove(&(remote_address, local_address)) + .or_else(|| { + stale_connections.remove(&( + remote_address, + SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, local_address.port()), + )) + }) + }; + let Some(stale) = stale else { + return false; + }; + self.release_stale_tcp_connection(stale); + true + } + + fn release_stale_tcp_connection(&mut self, stale: StaleTcpConnection) { + if stale.retained_connector.is_some() { + self.retained_connectors = self + .retained_connectors + .checked_sub(1) + .expect("reactor retained connector count underflow"); + let remove_namespace = + self.sessions + .get_mut(&stale.session_id) + .is_some_and(|namespace| { + namespace.retained_connectors = namespace + .retained_connectors + .checked_sub(1) + .expect("session retained connector count underflow"); + namespace.closing + && namespace.live_sockets == 0 + && namespace.retained_connectors == 0 + }); + if remove_namespace { + self.sessions.remove(&stale.session_id); + } + } + drop(stale); + } + + fn clear_stale_tcp_connections(&mut self, mapping_index: usize) { + let Reactor { + port_mappings, + sessions, + retained_connectors, + .. + } = self; + for (_, stale) in port_mappings[mapping_index].stale_connections.drain() { + if stale.retained_connector.is_some() { + *retained_connectors = retained_connectors + .checked_sub(1) + .expect("reactor retained connector count underflow"); + if let Some(namespace) = sessions.get_mut(&stale.session_id) { + namespace.retained_connectors = namespace + .retained_connectors + .checked_sub(1) + .expect("session retained connector count underflow"); + } + } + } + sessions.retain(|_, namespace| { + !namespace.closing || namespace.live_sockets != 0 || namespace.retained_connectors != 0 + }); + } + + fn retire_session_stale_connectors(&mut self, session_id: SessionId) { + let deadline = Instant::now() + PENDING_CONNECT_DISCARD_LIFETIME; + let mut released_connectors = 0; + for stale in self + .port_mappings + .iter_mut() + .flat_map(|state| state.stale_connections.values_mut()) + .filter(|stale| stale.session_id == session_id) + { + if let Some(connector) = stale.retained_connector.take() { + let _ = sockopt::set_socket_linger(&connector, None); + drop(connector); + stale.deadline = Some( + stale + .deadline + .map_or(deadline, |existing| existing.max(deadline)), + ); + released_connectors += 1; + } + } + self.retained_connectors = self + .retained_connectors + .checked_sub(released_connectors) + .expect("reactor retained connector count underflow"); + if let Some(namespace) = self.sessions.get_mut(&session_id) { + namespace.retained_connectors = namespace + .retained_connectors + .checked_sub(released_connectors) + .expect("session retained connector count underflow"); + } + } + + fn take_tcp_peer_translations_except( + &mut self, + excluded_session_id: Option, + remote_address: SocketAddrV4, + local_address: SocketAddrV4, + ) -> bool { + let mut found = false; + let mut released_connectors = 0; + for (session_id, namespace) in &mut self.sessions { + if Some(*session_id) != excluded_session_id + && let Some(translation) = + namespace.take_tcp_peer_translation(remote_address, local_address) + { + released_connectors += usize::from(translation.retained_connector.is_some()); + found = true; + } + } + self.retained_connectors = self + .retained_connectors + .checked_sub(released_connectors) + .expect("reactor retained connector count underflow"); + found + } + fn accept_socket( &mut self, listener_id: u64, @@ -2038,43 +2905,94 @@ impl Reactor { readiness: ReadinessRegistration, snapshot: Arc>, ) -> BrokerResult> { - if self.sockets.len() >= self.max_sockets { - return Err(BrokerError::ResourceExhausted); - } if self.sockets.contains_key(&accepted_id) { return Err(BrokerError::Internal); } - let listener = self - .sockets - .get_mut(&listener_id) - .ok_or(BrokerError::Internal)?; - if listener.kind != SocketKind::Tcp || !listener.listening { - return Ok(SocketOutcome::Failed(SocketError::NotConnected)); - } - let listener_session_id = listener.session_id; - let listener_tcp_no_delay = listener.tcp_no_delay; - let listener_tcp_keep_alive = listener.tcp_keep_alive; - // The accepted connection inherits the listener's trusted guest-local - // address rather than the private host endpoint behind it. - let local_address = listener.guest_local_address.ok_or(BrokerError::Internal)?; + let ( + listener_session_id, + listener_tcp_no_delay, + listener_tcp_keep_alive, + local_address, + mapping_index, + ) = { + let listener = self + .sockets + .get(&listener_id) + .ok_or(BrokerError::Internal)?; + if listener.kind != SocketKind::Tcp || !listener.listening { + return Ok(SocketOutcome::Failed(SocketError::NotConnected)); + } + ( + listener.session_id, + listener.tcp_no_delay, + listener.tcp_keep_alive, + // Accepted connections inherit the trusted guest-local address, + // not the private host endpoint behind the listener. + listener.guest_local_address.ok_or(BrokerError::Internal)?, + listener.port_mapping_index, + ) + }; let (socket, remote_address) = loop { - match acceptfrom_with( - &listener.socket, - LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, - ) { - Ok((socket, address)) => break (socket, address), - Err(Errno::INTR) => {} - Err(Errno::AGAIN) => { - clear_readiness(listener, ReadinessFlags::READ)?; - return Err(BrokerError::WouldBlock); - } - Err(error) => { - return Ok(SocketOutcome::Failed(socket_operation_error_from_errno( - error, - )?)); + let (socket, remote_address) = loop { + let listener = self + .sockets + .get_mut(&listener_id) + .ok_or(BrokerError::Internal)?; + match acceptfrom_with( + &listener.socket, + LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, + ) { + Ok((socket, address)) => break (socket, address), + Err(Errno::INTR) => {} + Err(Errno::AGAIN) => { + clear_readiness(listener, ReadinessFlags::READ)?; + return Err(BrokerError::WouldBlock); + } + Err(error) => { + let _ = socket_operation_error_from_errno(error)?; + } } + }; + let remote_address = + SocketAddrV4::try_from(remote_address.ok_or(BrokerError::Internal)?) + .map_err(|_| BrokerError::Internal)?; + let host_local_address = local_socket_address(&socket)?; + match self.take_tcp_peer_translation( + listener_session_id, + listener_id, + mapping_index, + remote_address, + host_local_address, + ) { + AcceptedTcpPeer::Guest(guest_address) => break (socket, guest_address), + AcceptedTcpPeer::Native(host_address) => break (socket, host_address), + AcceptedTcpPeer::Stale => drop(socket), } }; + if self + .sockets + .len() + .checked_add(self.retained_connectors) + .is_none_or(|count| count >= self.max_sockets) + || self + .sessions + .get(&listener_session_id) + .ok_or(BrokerError::Internal)? + .live_sockets + .checked_add( + self.sessions + .get(&listener_session_id) + .ok_or(BrokerError::Internal)? + .retained_connectors, + ) + .is_none_or(|count| count >= self.max_sockets_per_session) + { + return Err(BrokerError::ResourceExhausted); + } + let listener = self + .sockets + .get_mut(&listener_id) + .ok_or(BrokerError::Internal)?; let no_wait = Timespec { tv_sec: 0, tv_nsec: 0, @@ -2091,15 +3009,6 @@ impl Reactor { Err(error) => return Err(broker_error_from_errno(error)), } } - let remote_address = SocketAddrV4::try_from(remote_address.ok_or(BrokerError::Internal)?) - .map_err(|_| BrokerError::Internal)?; - let host_local_address = local_socket_address(&socket)?; - let remote_address = self - .sessions - .get_mut(&listener_session_id) - .map_or(remote_address, |namespace| { - namespace.translate_tcp_peer(remote_address, host_local_address) - }); epoll::add( &self.epoll, &socket, @@ -2172,7 +3081,7 @@ fn connect_tcp_socket( id: u64, socket: &mut SocketEntry, address: SocketAddrV4, -) -> core::result::Result { +) -> core::result::Result<(SocketConnectionStatus, ReadinessFlags), PlatformConnectError> { if let Err(error) = epoll::modify( epoll_fd, &socket.socket, @@ -2230,9 +3139,7 @@ fn connect_tcp_socket( )); } }; - update_snapshot(socket, Some(status), readiness) - .map_err(PlatformConnectError::PeerIndeterminate)?; - Ok(status) + Ok((status, readiness)) } fn connect_datagram_socket( @@ -2330,27 +3237,6 @@ fn host_ipv4_address_is_local(address: Ipv4Addr) -> BrokerResult { } } -/// Discards connections queued on a reserved host endpoint before publication. -fn drain_tcp_listener(socket: &OwnedFd) -> BrokerResult { - for _ in 0..MAX_STALE_PORT_MAPPING_CONNECTIONS { - match acceptfrom_with( - socket, - LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, - ) { - Ok((accepted, _)) => drop(accepted), - Err(Errno::INTR) => {} - Err(Errno::AGAIN | Errno::INVAL) => return Ok(true), - Err(error) => { - // Linux reports pending per-connection network errors from - // accept. Skip that stale connection while preserving broker - // resource failures such as descriptor or memory exhaustion. - let _ = socket_operation_error_from_errno(error)?; - } - } - } - Ok(false) -} - fn listen_tcp_socket( epoll_fd: &OwnedFd, id: u64, @@ -2745,6 +3631,15 @@ fn get_tcp_option(socket: &SocketEntry, name: TcpOptionName) -> BrokerResult( + stale_connections: impl Iterator, + session_id: SessionId, +) -> usize { + stale_connections + .filter(|stale| stale.session_id == session_id) + .count() +} + fn shutdown_socket( socket: &mut SocketEntry, mode: ShutdownMode, @@ -2818,11 +3713,6 @@ fn shutdown_socket( socket.listening = false; socket.read_shutdown = true; socket.peek_waitall_threshold = None; - update_snapshot( - socket, - Some(SocketConnectionStatus::Failed(SocketError::NotConnected)), - ReadinessFlags::WRITE | ReadinessFlags::HANGUP, - )?; return Ok(SocketOutcome::Completed(())); } socket.read_shutdown |= shuts_down_read; @@ -3376,6 +4266,7 @@ mod tests { let occupied = TcpListener::bind("127.0.0.1:0").unwrap(); let host_address = socket_address_v4(occupied.local_addr().unwrap()); let error = LinuxSocketProvider::new_with_port_mappings( + 1, 1, &[SocketPortMapping { guest_port: 80, @@ -3390,7 +4281,7 @@ mod tests { #[test] fn private_backend_endpoints_are_not_guest_destinations() { - let provider = Arc::new(LinuxSocketProvider::new(2).unwrap()); + let provider = Arc::new(LinuxSocketProvider::new(2, 2).unwrap()); let all_destinations = DestinationRule::new( CallerCredential::Unauthenticated, Ipv4Cidr::new(Ipv4Address([0, 0, 0, 0]), 0).unwrap(), @@ -3449,7 +4340,9 @@ mod tests { let mut namespace = SessionSocketNamespace::default(); let shared_host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); for (guest_port, host_peer_port) in [(1000, 5000), (1001, 5001)] { - namespace.reserve_tcp_translation().unwrap(); + namespace + .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) + .unwrap(); namespace .insert_binding( SocketKind::Tcp, @@ -3459,6 +4352,7 @@ mod tests { guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), host_address: Some(shared_host_address), host_peer_address: None, + host_peer_mapping_index: None, host_mapped: false, }, ) @@ -3469,42 +4363,93 @@ mod tests { guest_port, u64::from(guest_port), SocketAddrV4::new(Ipv4Addr::LOCALHOST, host_peer_port), + u64::from(guest_port), + None, ) .unwrap(); } assert_eq!( - namespace.translate_tcp_peer( - shared_host_address, - SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001), - ), + namespace + .take_tcp_peer_translation( + shared_host_address, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001), + ) + .unwrap() + .guest_address, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1001) ); assert_eq!(namespace.tcp_translations.len(), 1); - assert_eq!( - namespace.translate_tcp_peer( - shared_host_address, - SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001), - ), - shared_host_address + assert!( + namespace + .take_tcp_peer_translation( + shared_host_address, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001), + ) + .is_none() ); assert_eq!( - namespace.translate_tcp_peer( - shared_host_address, - SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000), - ), + namespace + .take_tcp_peer_translation( + shared_host_address, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000), + ) + .unwrap() + .guest_address, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1000) ); assert!(namespace.tcp_translations.is_empty()); } + #[test] + fn tcp_translation_capacity_charges_only_session_owned_stale_state() { + let mut namespace = SessionSocketNamespace::default(); + + namespace.reserve_tcp_translation(0, 1, 1).unwrap(); + assert_eq!( + namespace.reserve_tcp_translation(1, 1, 1), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!( + namespace.reserve_tcp_translation(0, MAX_RETAINED_TRANSLATIONS, 1), + Err(BrokerError::ResourceExhausted) + ); + } + + #[test] + fn stale_translation_ownership_is_aggregated_across_mappings() { + let session_id = SessionId(7); + let foreign_session_id = SessionId(8); + let stale = [ + StaleTcpConnection { + session_id, + deadline: None, + retained_connector: None, + }, + StaleTcpConnection { + session_id, + deadline: None, + retained_connector: None, + }, + StaleTcpConnection { + session_id: foreign_session_id, + deadline: None, + retained_connector: None, + }, + ]; + + assert_eq!(count_session_stale_connections(stale.iter(), session_id), 2); + } + #[test] fn retiring_listener_removes_its_pending_tcp_translations() { let mut namespace = SessionSocketNamespace::default(); let first_listener = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); let second_listener = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001); for (guest_port, listener) in [(1000, first_listener), (1001, second_listener)] { - namespace.reserve_tcp_translation().unwrap(); + namespace + .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) + .unwrap(); namespace .insert_binding( SocketKind::Tcp, @@ -3517,12 +4462,20 @@ mod tests { 40000 + guest_port, )), host_peer_address: None, + host_peer_mapping_index: None, host_mapped: false, }, ) .unwrap(); namespace - .set_host_peer_address(SocketKind::Tcp, guest_port, u64::from(guest_port), listener) + .set_host_peer_address( + SocketKind::Tcp, + guest_port, + u64::from(guest_port), + listener, + u64::from(guest_port), + None, + ) .unwrap(); } @@ -3530,23 +4483,94 @@ mod tests { assert_eq!(namespace.tcp_translations.len(), 1); assert_eq!( - namespace.translate_tcp_peer( - SocketAddrV4::new(Ipv4Addr::LOCALHOST, 41001), - second_listener, - ), + namespace + .take_tcp_peer_translation( + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 41001), + second_listener, + ) + .unwrap() + .guest_address, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1001) ); assert!(namespace.tcp_translations.is_empty()); } #[test] - fn retiring_failed_or_aborted_connector_removes_its_tcp_translation() { + fn moving_live_translation_keeps_nonexpiring_session_identity() { + let mut namespace = SessionSocketNamespace::default(); + let session_id = SessionId(7); + let guest_port = 1000; + let socket_id = 1; + let host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); + let listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); + namespace + .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) + .unwrap(); + namespace + .insert_binding( + SocketKind::Tcp, + guest_port, + GuestPortBinding { + socket_id, + guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), + host_address: Some(host_address), + host_peer_address: None, + host_peer_mapping_index: None, + host_mapped: false, + }, + ) + .unwrap(); + namespace + .set_host_peer_address( + SocketKind::Tcp, + guest_port, + socket_id, + listener_address, + 2, + Some(0), + ) + .unwrap(); + namespace.retire_tcp_translation_for_connector( + SocketKind::Tcp, + guest_port, + socket_id, + false, + None, + Some(eventfd(0, EventfdFlags::CLOEXEC).unwrap()), + ); + let mut stale_connections = HashMap::new(); + stale_connections + .try_reserve(MAX_RETAINED_TRANSLATIONS) + .unwrap(); + + assert_eq!( + namespace.move_tcp_translations_for_listener( + session_id, + listener_address, + &mut stale_connections, + ), + 0 + ); + + let stale = stale_connections + .get(&(host_address, listener_address)) + .unwrap(); + assert_eq!(stale.session_id, session_id); + assert_eq!(stale.deadline, None); + assert!(stale.retained_connector.is_some()); + assert_eq!(namespace.retained_connectors, 1); + } + + #[test] + fn retiring_failed_connector_removes_its_tcp_translation() { let mut namespace = SessionSocketNamespace::default(); let guest_port = 1000; let socket_id = 1; let host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); let listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); - namespace.reserve_tcp_translation().unwrap(); + namespace + .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) + .unwrap(); namespace .insert_binding( SocketKind::Tcp, @@ -3556,15 +4580,24 @@ mod tests { guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), host_address: Some(host_address), host_peer_address: None, + host_peer_mapping_index: None, host_mapped: false, }, ) .unwrap(); namespace - .set_host_peer_address(SocketKind::Tcp, guest_port, socket_id, listener_address) + .set_host_peer_address( + SocketKind::Tcp, + guest_port, + socket_id, + listener_address, + 2, + None, + ) .unwrap(); - namespace.remove_tcp_translation_for_connector(SocketKind::Tcp, guest_port, socket_id); + let _ = + namespace.remove_tcp_translation_for_connector(SocketKind::Tcp, guest_port, socket_id); assert!(namespace.tcp_translations.is_empty()); assert_eq!( @@ -3573,11 +4606,62 @@ mod tests { ); } + #[test] + fn aborting_connector_marks_its_translation_for_discard() { + let mut namespace = SessionSocketNamespace::default(); + let guest_port = 1000; + let socket_id = 1; + let host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); + let listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); + namespace + .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) + .unwrap(); + namespace + .insert_binding( + SocketKind::Tcp, + guest_port, + GuestPortBinding { + socket_id, + guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), + host_address: Some(host_address), + host_peer_address: None, + host_peer_mapping_index: None, + host_mapped: false, + }, + ) + .unwrap(); + namespace + .set_host_peer_address( + SocketKind::Tcp, + guest_port, + socket_id, + listener_address, + 2, + None, + ) + .unwrap(); + + let _ = namespace.retire_tcp_translation_for_connector( + SocketKind::Tcp, + guest_port, + socket_id, + true, + None, + Some(eventfd(0, EventfdFlags::CLOEXEC).unwrap()), + ); + + let translation = namespace + .take_tcp_peer_translation(host_address, listener_address) + .unwrap(); + assert!(translation.discard_on_accept); + assert!(translation.retained_connector.is_some()); + } + #[test] fn guest_tcp_ports_are_session_scoped_and_do_not_bind_host_ports() { let occupied_host_listener = TcpListener::bind("127.0.0.1:0").unwrap(); let occupied_host_address = socket_address_v4(occupied_host_listener.local_addr().unwrap()); - let provider = Arc::new(LinuxSocketProvider::new(4).unwrap()); + let provider = Arc::new(LinuxSocketProvider::new(4, 4).unwrap()); let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) .with_socket_policy(SocketPolicy::Ipv4Loopback), @@ -3619,12 +4703,12 @@ mod tests { #[test] fn guest_tcp_loopback_routes_within_the_session_namespace() { - let provider = Arc::new(LinuxSocketProvider::new(3).unwrap()); + let provider = Arc::new(LinuxSocketProvider::new(3, 3).unwrap()); let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) .with_socket_policy(SocketPolicy::Ipv4Loopback), BrokerCoreLimits::new_with_all_limits(4, 0, 3, 3), - provider, + provider.clone(), ) .unwrap(); let session = broker @@ -3660,6 +4744,7 @@ mod tests { .expect("connected client must have a guest-local address"); session.close_object_reference(client).unwrap(); assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), client); + assert_eq!(provider.reactor.retained_connector_count(), 1); let replacement = create_socket(&session, readiness.clone()); let replacement_address = @@ -3694,6 +4779,7 @@ mod tests { }; assert_eq!(accepted.local_address, guest_listener_address); assert_eq!(accepted.remote_address, client_address); + assert_eq!(provider.reactor.retained_connector_count(), 0); session.close_object_reference(accepted.handle).unwrap(); assert_eq!( retirements.recv_timeout(TEST_TIMEOUT).unwrap(), @@ -3738,6 +4824,7 @@ mod tests { let host_address = unused_tcp_address(); let provider = Arc::new( LinuxSocketProvider::new_with_port_mappings( + 2, 2, &[SocketPortMapping { guest_port: 80, @@ -3798,7 +4885,7 @@ mod tests { session.close_object_reference(listener).unwrap(); assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), listener); - let replacement = create_socket(&session, readiness); + let replacement = create_socket(&session, readiness.clone()); assert_eq!( litebox_broker_core::socket::bind(&session, replacement, guest_address), Ok(SocketOutcome::Completed(guest_address)) @@ -3819,6 +4906,7 @@ mod tests { let provider = Arc::new( LinuxSocketProvider::new_with_port_mappings( 3, + 2, &[SocketPortMapping { guest_port: 80, host_address, @@ -3888,8 +4976,102 @@ mod tests { } #[test] - fn aborting_guest_routed_connector_releases_its_translation() { - let provider = Arc::new(LinuxSocketProvider::new(2).unwrap()); + fn mapped_listener_handoff_drains_bounded_stale_connections() { + let host_address = unused_tcp_address(); + let provider = Arc::new( + LinuxSocketProvider::new_with_port_mappings( + 3, + 2, + &[SocketPortMapping { + guest_port: 80, + host_address, + }], + ) + .unwrap(), + ); + let socket_provider: Arc = provider.clone(); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(6, 0, 3, 2), + socket_provider, + ) + .unwrap(); + let first_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let second_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, publications) = channel(); + let (retired, _retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let first_listener = create_socket(&first_session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::bind(&first_session, first_listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&first_session, first_listener, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + let first_client = create_socket(&first_session, readiness.clone()); + assert!(matches!( + litebox_broker_core::socket::connect(&first_session, first_client, guest_address,), + Ok(SocketOutcome::Completed( + SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected + )) + )); + wait_until_connected(&first_session, first_client, &publications); + assert_eq!(provider.reactor.tcp_translation_count(), 1); + assert_eq!( + litebox_broker_core::socket::shutdown( + &first_session, + first_listener, + ShutdownMode::StopListening, + ), + Ok(SocketOutcome::Completed(())) + ); + assert_eq!(provider.reactor.tcp_translation_count(), 0); + assert_eq!(provider.reactor.stale_tcp_translation_count(), 1); + assert_eq!( + litebox_broker_core::socket::shutdown( + &first_session, + first_client, + ShutdownMode::Abort, + ), + Ok(SocketOutcome::Completed(())) + ); + first_session.close_object_reference(first_client).unwrap(); + assert_eq!(provider.reactor.retained_connector_count(), 1); + first_session + .close_object_reference(first_listener) + .unwrap(); + drop(first_session); + assert_eq!(provider.reactor.retained_connector_count(), 0); + assert_eq!(provider.reactor.stale_tcp_translation_count(), 1); + + let second_listener = create_socket(&second_session, readiness); + assert_eq!( + litebox_broker_core::socket::bind(&second_session, second_listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&second_session, second_listener, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!(provider.reactor.stale_tcp_translation_count(), 0); + assert_eq!(provider.reactor.retained_connector_count(), 0); + + second_session + .close_object_reference(second_listener) + .unwrap(); + } + + #[test] + fn stopped_private_listener_refuses_new_guest_connections() { + let provider = Arc::new(LinuxSocketProvider::new(2, 2).unwrap()); let socket_provider: Arc = provider.clone(); let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) @@ -3901,6 +5083,60 @@ mod tests { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); + let (published, _publications) = channel(); + let (retired, _retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000); + let listener = create_socket(&session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::bind(&session, listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&session, listener, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::shutdown(&session, listener, ShutdownMode::StopListening,), + Ok(SocketOutcome::Completed(())) + ); + + let client = create_socket(&session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::connect(&session, client, guest_address), + Ok(SocketOutcome::Completed(SocketConnectionStatus::Failed( + SocketError::ConnectionRefused, + ))) + ); + assert!( + session + .check_readiness(client) + .unwrap() + .contains(ReadinessFlags::ERROR) + ); + assert_eq!(provider.reactor.tcp_translation_count(), 0); + + session.close_object_reference(client).unwrap(); + session.close_object_reference(listener).unwrap(); + } + + #[test] + fn aborting_guest_routed_connector_retains_a_discard_marker() { + let provider = Arc::new(LinuxSocketProvider::new(3, 2).unwrap()); + let socket_provider: Arc = provider.clone(); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(8, 0, 6, 4), + socket_provider, + ) + .unwrap(); + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let second_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); let (published, publications) = channel(); let (retired, _retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); @@ -3914,7 +5150,7 @@ mod tests { litebox_broker_core::socket::listen(&session, listener, 1), Ok(SocketOutcome::Completed(guest_address)) ); - let client = create_socket(&session, readiness); + let client = create_socket(&session, readiness.clone()); assert!(matches!( litebox_broker_core::socket::connect(&session, client, guest_address), Ok(SocketOutcome::Completed( @@ -3930,8 +5166,50 @@ mod tests { ); session.close_object_reference(client).unwrap(); + assert_eq!(provider.reactor.tcp_translation_count(), 1); + assert_eq!( + litebox_broker_core::socket::create( + &session, + CreateSocketRequest { + address_family: AddressFamily::Ipv4, + socket_type: SocketType::Stream, + protocol: IpProtocol::Tcp, + }, + readiness.clone(), + ), + Err(BrokerError::ResourceExhausted) + ); + let other_session_socket = create_socket(&second_session, readiness.clone()); + assert!(matches!( + litebox_broker_core::socket::accept(&session, listener, readiness.clone()), + Err(BrokerError::WouldBlock) + )); assert_eq!(provider.reactor.tcp_translation_count(), 0); + let replacement = create_socket(&session, readiness.clone()); + session.close_object_reference(replacement).unwrap(); + second_session + .close_object_reference(other_session_socket) + .unwrap(); + let retiring_client = create_socket(&session, readiness.clone()); + assert!(matches!( + litebox_broker_core::socket::connect(&session, retiring_client, guest_address), + Ok(SocketOutcome::Completed( + SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected + )) + )); + wait_until_connected(&session, retiring_client, &publications); + assert_eq!( + litebox_broker_core::socket::shutdown(&session, retiring_client, ShutdownMode::Abort,), + Ok(SocketOutcome::Completed(())) + ); + session.close_object_reference(retiring_client).unwrap(); + assert_eq!(provider.reactor.tcp_translation_count(), 1); session.close_object_reference(listener).unwrap(); + assert_eq!(provider.reactor.tcp_translation_count(), 0); + let after_retirement = create_socket(&second_session, readiness); + second_session + .close_object_reference(after_retirement) + .unwrap(); } #[test] @@ -3939,6 +5217,7 @@ mod tests { let host_address = unused_tcp_address(); let provider = Arc::new( LinuxSocketProvider::new_with_port_mappings( + 2, 2, &[SocketPortMapping { guest_port: 80, @@ -4057,7 +5336,7 @@ mod tests { assert_eq!(error.kind(), std::io::ErrorKind::ConnectionReset); }); - let provider = Arc::new(LinuxSocketProvider::new(8).unwrap()); + let provider = Arc::new(LinuxSocketProvider::new(8, 8).unwrap()); let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) .with_socket_policy(SocketPolicy::Ipv4Loopback), @@ -4377,6 +5656,7 @@ mod tests { let host_address = unused_tcp_address(); let provider = Arc::new( LinuxSocketProvider::new_with_port_mappings( + 2, 2, &[SocketPortMapping { guest_port: FIRST_GUEST_EPHEMERAL_PORT, @@ -4478,6 +5758,7 @@ mod tests { let host_address = unused_tcp_address(); let provider = Arc::new( LinuxSocketProvider::new_with_port_mappings( + 4, 4, &[SocketPortMapping { guest_port: FIRST_GUEST_EPHEMERAL_PORT, @@ -4648,7 +5929,7 @@ mod tests { let server = UdpSocket::bind("127.0.0.1:0").unwrap(); server.set_read_timeout(Some(TEST_TIMEOUT)).unwrap(); let server_address = socket_address_v4(server.local_addr().unwrap()); - let provider = Arc::new(LinuxSocketProvider::new(2).unwrap()); + let provider = Arc::new(LinuxSocketProvider::new(2, 2).unwrap()); let socket_policy = SocketPolicy::from_udp_destination_rules(&[ DestinationRule::new( CallerCredential::Unauthenticated, diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index ed76f3fb4..991141e8c 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -150,6 +150,7 @@ fn main() -> Result<(), Box> { limits, Arc::new(LinuxSocketProvider::new_with_port_mappings( limits.max_sockets, + limits.max_sockets_per_session, &port_mappings, )?), )?; diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 02419bb6f..e454c96e7 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -371,6 +371,7 @@ fn spawn_test_broker_with_port_mappings( std::sync::Arc::new( litebox_broker_platform_linux_userland::LinuxSocketProvider::new_with_port_mappings( limits.max_sockets, + limits.max_sockets_per_session, &port_mappings, ) .expect("failed to create broker test socket provider"), From 4b411bc0c47d100bb39da7f1ccbb67f520ab38c9 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 10 Aug 2026 10:52:36 -0700 Subject: [PATCH 5/6] Split TCP publication authority from platform Move publication declarations, guest-port reservation, claim ownership, and listener generations into broker core while keeping Linux endpoint realization in the platform provider. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- litebox_broker_core/src/lib.rs | 6 +- litebox_broker_core/src/socket.rs | 573 +++++++++++++++--- litebox_broker_host/src/lib.rs | 2 +- .../src/lib.rs | 2 +- .../src/socket.rs | 567 +++++++++-------- litebox_broker_userland/src/main.rs | 71 ++- litebox_runner_linux_userland/tests/run.rs | 21 +- 7 files changed, 877 insertions(+), 365 deletions(-) diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 6bf3567fe..da507e7b9 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -40,7 +40,7 @@ pub use policy::{ }; use session::ObjectReference; pub use session::{BrokerSession, CallerCredential, ObjectRights, SessionId}; -use socket::SocketProvider; +use socket::{SocketProvider, TcpPortPublicationAuthority}; /// BrokerCore result type. pub type Result = core::result::Result; @@ -117,6 +117,7 @@ pub struct BrokerCore { pub(crate) reserved_pipe_capacity: Arc, pub(crate) reserved_sockets: Arc, pub(crate) socket_provider: Arc, + pub(crate) tcp_port_publications: TcpPortPublicationAuthority, } static BROKER_CORE_CREATED: AtomicBool = AtomicBool::new(false); @@ -133,6 +134,8 @@ impl BrokerCore { limits: BrokerCoreLimits, socket_provider: Arc, ) -> Result { + let tcp_port_publications = + TcpPortPublicationAuthority::new(socket_provider.tcp_port_publications())?; BROKER_CORE_CREATED .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .map_err(|_| BrokerError::BrokerCoreAlreadyExists)?; @@ -147,6 +150,7 @@ impl BrokerCore { reserved_pipe_capacity: Arc::new(AtomicUsize::new(0)), reserved_sockets: Arc::new(AtomicUsize::new(0)), socket_provider, + tcp_port_publications, }) } diff --git a/litebox_broker_core/src/socket.rs b/litebox_broker_core/src/socket.rs index 8cdf0fcfb..7304c9381 100644 --- a/litebox_broker_core/src/socket.rs +++ b/litebox_broker_core/src/socket.rs @@ -27,6 +27,147 @@ const DEFAULT_TCP_LISTEN_ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::LOC const DEFAULT_TCP_LOCAL_ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); const FIRST_EPHEMERAL_PORT: u16 = 49152; +/// Portable declaration that publishes one external TCP endpoint as a guest port. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TcpPortPublication { + /// Guest-local TCP port eligible for explicit publication. + pub guest_port: u16, + /// Externally reachable endpoint realized by the platform provider. + pub external_address: SocketAddrV4, +} + +/// Broker-assigned identity of one configured TCP port publication. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct TcpPortPublicationId(u32); + +impl TcpPortPublicationId { + /// Returns the provider declaration index for this publication. + #[must_use] + pub const fn index(self) -> usize { + self.0 as usize + } +} + +/// Portable lease token authorizing one listener generation to realize a publication. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TcpPortPublicationClaim { + publication_id: TcpPortPublicationId, + generation: u64, +} + +impl TcpPortPublicationClaim { + /// Returns the configured publication being claimed. + #[must_use] + pub const fn publication_id(self) -> TcpPortPublicationId { + self.publication_id + } + + /// Returns the broker-assigned listener generation. + #[must_use] + pub const fn generation(self) -> u64 { + self.generation + } +} + +#[derive(Clone)] +pub(crate) struct TcpPortPublicationAuthority { + state: Arc>>, +} + +struct TcpPortPublicationState { + publication: TcpPortPublication, + next_generation: u64, + claimed_by: Option<(SessionId, u64)>, +} + +impl TcpPortPublicationAuthority { + pub(crate) fn new(publications: &[TcpPortPublication]) -> Result { + let mut state = Vec::new(); + state + .try_reserve_exact(publications.len()) + .map_err(|_| BrokerError::OutOfMemory)?; + for (index, publication) in publications.iter().copied().enumerate() { + if publication.guest_port == 0 + || publication.external_address.port() == 0 + || publications[..index].iter().any(|existing| { + existing.guest_port == publication.guest_port + || existing.external_address == publication.external_address + }) + { + return Err(BrokerError::Internal); + } + state.push(TcpPortPublicationState { + publication, + next_generation: 1, + claimed_by: None, + }); + } + Ok(Self { + state: Arc::new(Mutex::new(state)), + }) + } + + fn publication_id( + &self, + request: CreateSocketRequest, + guest_port: u16, + ) -> Option { + if !is_tcp(request) { + return None; + } + let state = self.state.lock(); + let index = state + .iter() + .position(|entry| entry.publication.guest_port == guest_port)?; + Some(TcpPortPublicationId(u32::try_from(index).ok()?)) + } + + fn claim( + &self, + session_id: SessionId, + publication_id: TcpPortPublicationId, + ) -> Result> { + let mut state = self.state.lock(); + let entry = state + .get_mut(publication_id.index()) + .ok_or(BrokerError::Internal)?; + if entry.claimed_by.is_some() { + return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); + } + let generation = entry.next_generation; + entry.next_generation = generation + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted)?; + entry.claimed_by = Some((session_id, generation)); + Ok(SocketOutcome::Completed(TcpPortPublicationReservation { + authority: self.clone(), + session_id, + claim: TcpPortPublicationClaim { + publication_id, + generation, + }, + })) + } +} + +struct TcpPortPublicationReservation { + authority: TcpPortPublicationAuthority, + session_id: SessionId, + claim: TcpPortPublicationClaim, +} + +impl Drop for TcpPortPublicationReservation { + fn drop(&mut self) { + let mut state = self.authority.state.lock(); + if let Some(entry) = state.get_mut(self.claim.publication_id.index()) + && entry.claimed_by == Some((self.session_id, self.claim.generation)) + { + entry.claimed_by = None; + } + } +} + #[derive(Default)] struct SessionSocketPortState { tcp: HashMap, @@ -46,7 +187,7 @@ impl SessionSocketPorts { /// Reserves one guest TCP port for `requested_address`. /// /// A zero requested port is allocated from the ephemeral range, skipping - /// ports the provider reserved for an explicit host port mapping. The + /// ports reserved for explicit TCP publication. The /// reservation is released when the returned guard is dropped. fn reserve( &self, @@ -141,15 +282,6 @@ pub enum PlatformConnectError { PeerIndeterminate(BrokerError), } -/// Whether a guest-local binding was requested explicitly or allocated implicitly. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum PlatformBindKind { - /// The guest explicitly requested this nonzero local port through `bind`. - Explicit, - /// The broker allocated the local port for another socket operation. - Implicit, -} - /// Broker socket and endpoint metadata returned by an accept operation. pub struct AcceptedBrokerSocket { /// Broker handle naming the accepted socket. @@ -186,13 +318,12 @@ pub struct PlatformDatagramReceive { /// bookkeeping shared across the sockets of a broker session. Operations on an /// individual socket belong to [`PlatformSocket`], not this shared provider. pub trait SocketProvider: Send + Sync { - /// Returns whether a guest port is reserved for an explicit host port mapping. + /// Returns the portable TCP publications realized by this provider. /// - /// Reserved ports are skipped by implicit guest-port allocation but remain - /// available to an explicit nonzero `bind` request. - fn reserves_guest_port(&self, request: CreateSocketRequest, port: u16) -> bool { - let _ = (request, port); - false + /// The slice and its order must remain stable for the provider's lifetime; + /// broker-issued publication IDs index this declaration list. + fn tcp_port_publications(&self) -> &[TcpPortPublication] { + &[] } /// Creates one nonblocking socket resource for an authenticated session. @@ -221,16 +352,20 @@ pub trait PlatformSocket: Send + Sync { /// nonzero port and must echo it unchanged; the host endpoint backing the /// socket is chosen privately by the platform. A datagram socket receives /// the address requested by the guest and returns the host-assigned one. - fn bind( - &self, - address: SocketAddrV4, - kind: PlatformBindKind, - ) -> Result>; + fn bind(&self, address: SocketAddrV4) -> Result>; /// Makes this socket listen for incoming connections. /// - /// The returned address is the socket's guest-local address. - fn listen(&self, backlog: u32) -> Result>; + /// The returned address is the socket's guest-local address. `publication` + /// authorizes realizing exactly one configured external endpoint for this + /// listener generation. The platform must retain and compare the complete + /// claim through rollback and teardown so stale work cannot detach a newer + /// generation. `None` requests an ordinary private listener. + fn listen( + &self, + backlog: u32, + publication: Option, + ) -> Result>; /// Accepts one pending connection without waiting. fn accept( @@ -349,6 +484,7 @@ pub fn create( readiness, _quota: quota, port_reservation: Mutex::new(None), + tcp_port_publication_reservation: Mutex::new(None), }); let platform_socket = match session.core.socket_provider.create( session.session_id, @@ -444,7 +580,6 @@ pub fn connect( create_request, &resource, DEFAULT_TCP_LOCAL_ADDRESS, - PlatformBindKind::Implicit, ) { Ok(binding) => binding, Err(error) => { @@ -511,51 +646,60 @@ pub fn bind( ) { Ok(()) => {} Err(BrokerError::PolicyDenied) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); return Ok(SocketOutcome::Failed(SocketError::PolicyDenied)); } Err(error) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); return Err(error); } } - let bind_kind = if address.port() == 0 { - PlatformBindKind::Implicit - } else { - PlatformBindKind::Explicit - }; if !is_tcp(create_request) { // Datagram sockets remain backed directly by a host endpoint. - return match resource.bind(address, bind_kind) { + return match resource.bind(address) { Ok(SocketOutcome::Completed(local_address)) => { - finish_configuration(&object, Some(local_address), None, false); + finish_configuration(&object, Some(local_address), None, None, false); Ok(SocketOutcome::Completed(local_address)) } Ok(SocketOutcome::Failed(error)) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); Ok(SocketOutcome::Failed(error)) } Err(error) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); Err(error) } }; } - let binding = match reserve_and_bind(session, create_request, &resource, address, bind_kind) { + let binding = match reserve_and_bind(session, create_request, &resource, address) { Ok(binding) => binding, Err(error) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); return Err(error); } }; let (local_address, reservation) = match binding { SocketOutcome::Completed(binding) => binding, SocketOutcome::Failed(error) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); return Ok(SocketOutcome::Failed(error)); } }; - finish_configuration(&object, Some(local_address), Some(reservation), false); + let publication_id = (address.port() != 0) + .then(|| { + session + .core + .tcp_port_publications + .publication_id(create_request, local_address.port()) + }) + .flatten(); + finish_configuration( + &object, + Some(local_address), + Some(reservation), + publication_id, + false, + ); Ok(SocketOutcome::Completed(local_address)) } @@ -569,7 +713,7 @@ pub fn listen( return Err(BrokerError::UnsupportedOperation); } let object = session.authorized_object(handle, ObjectRights::WRITE)?; - let (resource, create_request, existing_local_address) = { + let (resource, create_request, existing_local_address, publication_id, was_listening) = { let mut object = object.write(); let ObjectEntry::Socket(socket) = &mut *object else { return Err(BrokerError::InvalidRights); @@ -588,6 +732,8 @@ pub fn listen( Arc::clone(&socket.resource), socket.create_request, socket.local_address, + socket.tcp_port_publication_id, + socket.listening, ) }; @@ -601,11 +747,11 @@ pub fn listen( ) { Ok(()) => {} Err(BrokerError::PolicyDenied) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); return Ok(SocketOutcome::Failed(SocketError::PolicyDenied)); } Err(error) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); return Err(error); } } @@ -614,11 +760,10 @@ pub fn listen( create_request, &resource, DEFAULT_TCP_LISTEN_ADDRESS, - PlatformBindKind::Implicit, ) { Ok(binding) => binding, Err(error) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); return Err(error); } }; @@ -628,29 +773,60 @@ pub fn listen( port_reservation = Some(reservation); } SocketOutcome::Failed(error) => { - finish_configuration(&object, None, None, false); + finish_configuration(&object, None, None, None, false); return Ok(SocketOutcome::Failed(error)); } } } - match resource.listen(backlog) { + let existing_claim = resource.tcp_port_publication_claim(); + if existing_claim.is_some() != (was_listening && publication_id.is_some()) { + finish_configuration(&object, local_address, port_reservation, None, false); + return Err(BrokerError::Internal); + } + let mut new_publication_reservation = None; + let publication_claim = if let Some(claim) = existing_claim { + Some(claim) + } else if let Some(publication_id) = publication_id { + match session + .core + .tcp_port_publications + .claim(session.session_id, publication_id)? + { + SocketOutcome::Completed(reservation) => { + let claim = reservation.claim; + new_publication_reservation = Some(reservation); + Some(claim) + } + SocketOutcome::Failed(error) => { + finish_configuration(&object, local_address, port_reservation, None, false); + return Ok(SocketOutcome::Failed(error)); + } + } + } else { + None + }; + + match resource.listen(backlog, publication_claim) { Ok(SocketOutcome::Completed(address)) => { // The guest-local address is broker-authoritative, so a platform // that reports a different one is not trustworthy. if local_address != Some(address) { - finish_configuration(&object, local_address, port_reservation, false); + finish_configuration(&object, local_address, port_reservation, None, false); return Err(BrokerError::Internal); } - finish_configuration(&object, local_address, port_reservation, true); + if let Some(reservation) = new_publication_reservation { + resource.set_tcp_port_publication_reservation(reservation); + } + finish_configuration(&object, local_address, port_reservation, None, true); Ok(SocketOutcome::Completed(address)) } Ok(SocketOutcome::Failed(error)) => { - finish_configuration(&object, local_address, port_reservation, false); + finish_configuration(&object, local_address, port_reservation, None, false); Ok(SocketOutcome::Failed(error)) } Err(error) => { - finish_configuration(&object, local_address, port_reservation, false); + finish_configuration(&object, local_address, port_reservation, None, false); Err(error) } } @@ -696,6 +872,7 @@ pub fn accept( readiness, _quota: quota, port_reservation: Mutex::new(None), + tcp_port_publication_reservation: Mutex::new(None), }); let accepted = match listener_resource.accept(resource.readiness.clone()) { Ok(SocketOutcome::Completed(accepted)) => accepted, @@ -922,6 +1099,14 @@ pub fn shutdown( ) }; let outcome = resource.shutdown(mode); + if shuts_down_listener + && matches!( + &outcome, + Ok(SocketOutcome::Completed(()) | SocketOutcome::Failed(SocketError::NotConnected)) + ) + { + resource.release_tcp_port_publication(); + } if serializes_configuration { let mut object = object.write(); if let ObjectEntry::Socket(socket) = &mut *object { @@ -1089,7 +1274,6 @@ fn reserve_and_bind( create_request: CreateSocketRequest, resource: &SocketResource, requested_address: SocketAddrV4, - kind: PlatformBindKind, ) -> Result> { let (local_address, reservation) = match session @@ -1097,13 +1281,14 @@ fn reserve_and_bind( .reserve(create_request, requested_address, |port| { session .core - .socket_provider - .reserves_guest_port(create_request, port) + .tcp_port_publications + .publication_id(create_request, port) + .is_some() })? { SocketOutcome::Completed(binding) => binding, SocketOutcome::Failed(error) => return Ok(SocketOutcome::Failed(error)), }; - match resource.bind(local_address, kind)? { + match resource.bind(local_address)? { SocketOutcome::Completed(bound_address) if bound_address == local_address => { Ok(SocketOutcome::Completed((local_address, reservation))) } @@ -1203,6 +1388,7 @@ fn finish_configuration( object: &spin::RwLock, local_address: Option, port_reservation: Option, + tcp_port_publication_id: Option, listening: bool, ) { let mut object = object.write(); @@ -1212,6 +1398,7 @@ fn finish_configuration( if let Some(port_reservation) = port_reservation { socket.resource.set_port_reservation(port_reservation); } + socket.tcp_port_publication_id = socket.tcp_port_publication_id.or(tcp_port_publication_id); socket.listening |= listening; } } @@ -1224,6 +1411,7 @@ pub(crate) struct SocketObject { connect_in_flight: bool, configuration_in_flight: bool, listening: bool, + tcp_port_publication_id: Option, datagram_connect_generation: u64, } @@ -1237,6 +1425,7 @@ impl SocketObject { connect_in_flight: false, configuration_in_flight: false, listening: false, + tcp_port_publication_id: None, datagram_connect_generation: 0, } } @@ -1254,6 +1443,7 @@ impl SocketObject { connect_in_flight: false, configuration_in_flight: false, listening: false, + tcp_port_publication_id: None, datagram_connect_generation: 0, } } @@ -1268,6 +1458,7 @@ pub(crate) struct SocketResource { readiness: ReadinessRegistration, _quota: Arc, port_reservation: Mutex>, + tcp_port_publication_reservation: Mutex>, } impl SocketResource { @@ -1283,6 +1474,25 @@ impl SocketResource { } } + fn set_tcp_port_publication_reservation(&self, reservation: TcpPortPublicationReservation) { + let mut slot = self.tcp_port_publication_reservation.lock(); + debug_assert!(slot.is_none()); + if slot.is_none() { + *slot = Some(reservation); + } + } + + fn tcp_port_publication_claim(&self) -> Option { + self.tcp_port_publication_reservation + .lock() + .as_ref() + .map(|reservation| reservation.claim) + } + + fn release_tcp_port_publication(&self) { + self.tcp_port_publication_reservation.lock().take(); + } + fn platform_socket(&self) -> &dyn PlatformSocket { self.platform_socket .get() @@ -1297,16 +1507,16 @@ impl SocketResource { self.platform_socket().connect(address) } - fn bind( - &self, - address: SocketAddrV4, - kind: PlatformBindKind, - ) -> Result> { - self.platform_socket().bind(address, kind) + fn bind(&self, address: SocketAddrV4) -> Result> { + self.platform_socket().bind(address) } - fn listen(&self, backlog: u32) -> Result> { - self.platform_socket().listen(backlog) + fn listen( + &self, + backlog: u32, + publication: Option, + ) -> Result> { + self.platform_socket().listen(backlog, publication) } fn accept( @@ -1445,9 +1655,22 @@ pub(crate) mod tests { use std::time::Duration; use std::vec; - #[derive(Clone, Default)] + #[derive(Clone)] pub(crate) struct TestSocketProvider { state: Arc, + tcp_port_publications: Arc>, + } + + impl Default for TestSocketProvider { + fn default() -> Self { + Self { + state: Arc::default(), + tcp_port_publications: Arc::new(vec![TcpPortPublication { + guest_port: 80, + external_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080), + }]), + } + } } #[test] @@ -1498,6 +1721,49 @@ pub(crate) mod tests { assert_eq!(address.port(), FIRST_EPHEMERAL_PORT + 1); } + #[test] + fn tcp_port_publication_configuration_is_bounded_and_unique() { + let publication = TcpPortPublication { + guest_port: 80, + external_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080), + }; + assert!(TcpPortPublicationAuthority::new(&[publication]).is_ok()); + assert!(matches!( + TcpPortPublicationAuthority::new(&[TcpPortPublication { + guest_port: 0, + ..publication + }]), + Err(BrokerError::Internal) + )); + assert!(matches!( + TcpPortPublicationAuthority::new(&[TcpPortPublication { + external_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0), + ..publication + }]), + Err(BrokerError::Internal) + )); + assert!(matches!( + TcpPortPublicationAuthority::new(&[ + publication, + TcpPortPublication { + external_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8081), + ..publication + }, + ]), + Err(BrokerError::Internal) + )); + assert!(matches!( + TcpPortPublicationAuthority::new(&[ + publication, + TcpPortPublication { + guest_port: 81, + ..publication + }, + ]), + Err(BrokerError::Internal) + )); + } + #[derive(Default)] struct TestSocketState { creates: StdMutex>, @@ -1509,7 +1775,10 @@ pub(crate) mod tests { status_block: StdMutex, mpsc::Receiver<()>)>>, binds: StdMutex>, listens: StdMutex>, + listen_publications: StdMutex>>, listen_block: StdMutex, mpsc::Receiver<()>)>>, + fail_listen: core::sync::atomic::AtomicBool, + error_listen: core::sync::atomic::AtomicBool, shutdown_calls: AtomicUsize, dropped_sockets: AtomicUsize, fail_create: core::sync::atomic::AtomicBool, @@ -1539,9 +1808,21 @@ pub(crate) mod tests { fn fail_next_shutdown(&self) { self.state.fail_shutdown.store(true, Ordering::Relaxed); } + + fn fail_next_listen(&self) { + self.state.fail_listen.store(true, Ordering::Relaxed); + } + + fn error_next_listen(&self) { + self.state.error_listen.store(true, Ordering::Relaxed); + } } impl SocketProvider for TestSocketProvider { + fn tcp_port_publications(&self) -> &[TcpPortPublication] { + &self.tcp_port_publications + } + fn create( &self, session_id: SessionId, @@ -1587,11 +1868,7 @@ pub(crate) mod tests { } impl PlatformSocket for TestPlatformSocket { - fn bind( - &self, - address: SocketAddrV4, - _kind: PlatformBindKind, - ) -> Result> { + fn bind(&self, address: SocketAddrV4) -> Result> { self.state.binds.lock().unwrap().push(address); if is_tcp(self.create_request) { *self.guest_local_address.lock().unwrap() = Some(address); @@ -1605,13 +1882,28 @@ pub(crate) mod tests { Ok(SocketOutcome::Completed(address)) } - fn listen(&self, backlog: u32) -> Result> { + fn listen( + &self, + backlog: u32, + publication: Option, + ) -> Result> { self.state.listens.lock().unwrap().push(backlog); + self.state + .listen_publications + .lock() + .unwrap() + .push(publication); let listen_block = self.state.listen_block.lock().unwrap().take(); if let Some((started, release)) = listen_block { started.send(()).unwrap(); release.recv_timeout(Duration::from_secs(5)).unwrap(); } + if self.state.fail_listen.swap(false, Ordering::Relaxed) { + return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); + } + if self.state.error_listen.swap(false, Ordering::Relaxed) { + return Err(BrokerError::Internal); + } let local_address = self .guest_local_address .lock() @@ -1762,6 +2054,7 @@ pub(crate) mod tests { check_udp_socket_operations(broker, provider); check_concurrent_udp_status_does_not_regress_connection(broker, provider); check_server_socket_operations(broker, provider); + check_tcp_port_publication_lifecycle(broker, provider); check_failed_listener_shutdown_preserves_state(broker, provider); check_listener_shutdown_does_not_race_listen(broker, provider); check_connect_errors_classify_peer_state(broker, provider); @@ -2187,6 +2480,148 @@ pub(crate) mod tests { session.close_object_reference(auto_bound).unwrap(); } + fn check_tcp_port_publication_lifecycle(broker: &BrokerCore, provider: &TestSocketProvider) { + let first_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let second_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let readiness = Arc::new(TestReadinessSink::default()); + let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let first = create(&first_session, create_request(), readiness.clone()).unwrap(); + let second = create(&second_session, create_request(), readiness).unwrap(); + assert_eq!( + bind(&first_session, first, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + bind(&second_session, second, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + + let claims_before = provider.state.listen_publications.lock().unwrap().len(); + provider.fail_next_listen(); + assert_eq!( + listen(&first_session, first, 1), + Ok(SocketOutcome::Failed(SocketError::AddressInUse)) + ); + assert_eq!( + listen(&second_session, second, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + let claims = provider.state.listen_publications.lock().unwrap(); + let first_claim = claims[claims_before].expect("published listen must carry a claim"); + let second_claim = claims[claims_before + 1].expect("published listen must carry a claim"); + assert_eq!(first_claim.publication_id(), second_claim.publication_id()); + assert_eq!(second_claim.generation(), first_claim.generation() + 1); + drop(claims); + + let calls_before_conflict = provider.state.listens.lock().unwrap().len(); + assert_eq!( + listen(&first_session, first, 1), + Ok(SocketOutcome::Failed(SocketError::AddressInUse)) + ); + assert_eq!( + provider.state.listens.lock().unwrap().len(), + calls_before_conflict + ); + + assert_eq!( + shutdown(&second_session, second, ShutdownMode::StopListening), + Ok(SocketOutcome::Completed(())) + ); + second_session.close_object_reference(second).unwrap(); + assert_eq!( + listen(&first_session, first, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + let third_claim = provider + .state + .listen_publications + .lock() + .unwrap() + .last() + .copied() + .flatten() + .unwrap(); + assert_eq!(third_claim.generation(), second_claim.generation() + 1); + first_session.close_object_reference(first).unwrap(); + + let second = create( + &second_session, + create_request(), + Arc::new(TestReadinessSink::default()), + ) + .unwrap(); + assert_eq!( + bind(&second_session, second, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + + provider.error_next_listen(); + assert_eq!( + listen(&second_session, second, 1), + Err(BrokerError::Internal) + ); + let failed_claim = provider + .state + .listen_publications + .lock() + .unwrap() + .last() + .copied() + .flatten() + .unwrap(); + assert_eq!(failed_claim.generation(), third_claim.generation() + 1); + + let first = create( + &first_session, + create_request(), + Arc::new(TestReadinessSink::default()), + ) + .unwrap(); + assert_eq!( + bind(&first_session, first, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + listen(&first_session, first, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + let dropped_claim = provider + .state + .listen_publications + .lock() + .unwrap() + .last() + .copied() + .flatten() + .unwrap(); + assert_eq!(dropped_claim.generation(), failed_claim.generation() + 1); + first_session.close_object_reference(first).unwrap(); + + assert_eq!( + listen(&second_session, second, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + let final_claim = provider + .state + .listen_publications + .lock() + .unwrap() + .last() + .copied() + .flatten() + .unwrap(); + assert_eq!(final_claim.generation(), dropped_claim.generation() + 1); + assert_eq!( + shutdown(&second_session, second, ShutdownMode::StopListening), + Ok(SocketOutcome::Completed(())) + ); + second_session.close_object_reference(second).unwrap(); + } + fn check_concurrent_udp_status_does_not_regress_connection( broker: &BrokerCore, provider: &TestSocketProvider, diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 32d41f837..a1e5368f3 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -783,7 +783,6 @@ mod tests { fn bind( &self, address: SocketAddrV4, - _kind: litebox_broker_core::socket::PlatformBindKind, ) -> litebox_broker_core::Result> { let address = if address.port() == 0 { SocketAddrV4::new(*address.ip(), 49152) @@ -797,6 +796,7 @@ mod tests { fn listen( &self, _backlog: u32, + _publication: Option, ) -> litebox_broker_core::Result> { let local_address = self .local_address diff --git a/litebox_broker_platform_linux_userland/src/lib.rs b/litebox_broker_platform_linux_userland/src/lib.rs index 62f4b357b..7be7622a8 100644 --- a/litebox_broker_platform_linux_userland/src/lib.rs +++ b/litebox_broker_platform_linux_userland/src/lib.rs @@ -11,4 +11,4 @@ mod socket; -pub use socket::{LinuxSocketProvider, SocketPortMapping}; +pub use socket::LinuxSocketProvider; diff --git a/litebox_broker_platform_linux_userland/src/socket.rs b/litebox_broker_platform_linux_userland/src/socket.rs index 77bed6213..42defda54 100644 --- a/litebox_broker_platform_linux_userland/src/socket.rs +++ b/litebox_broker_platform_linux_userland/src/socket.rs @@ -16,8 +16,8 @@ use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; use litebox_broker_core::socket::{ - AcceptedPlatformSocket, PlatformBindKind, PlatformConnectError, PlatformDatagramReceive, - PlatformSocket, PlatformStreamReceive, SocketProvider, + AcceptedPlatformSocket, PlatformConnectError, PlatformDatagramReceive, PlatformSocket, + PlatformStreamReceive, SocketProvider, TcpPortPublication, TcpPortPublicationClaim, }; use litebox_broker_core::{BrokerError, Result as BrokerResult, SessionId}; use litebox_broker_protocol::readiness::ReadinessFlags; @@ -54,22 +54,14 @@ const PENDING_CONNECT_DISCARD_LIFETIME: Duration = Duration::from_mins(5); /// immediate nonblocking operation, never for network readiness. pub struct LinuxSocketProvider { reactor: Arc, - port_mappings: Vec, -} - -/// Explicit mapping from one host TCP endpoint to a guest-local TCP port. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct SocketPortMapping { - /// Guest-local TCP port visible inside one broker session. - pub guest_port: u16, - /// Host TCP endpoint mapped to the guest port. - pub host_address: SocketAddrV4, + tcp_port_publications: Vec, } struct PortMappingState { - mapping: SocketPortMapping, + mapping: TcpPortPublication, reservation: Option, - claimed_by: Option, + reservation_registered: bool, + claimed_by: Option<(u64, TcpPortPublicationClaim)>, stale_connections: HashMap<(SocketAddrV4, SocketAddrV4), StaleTcpConnection>, } @@ -82,46 +74,46 @@ struct StaleTcpConnection { impl LinuxSocketProvider { /// Starts a provider with matching global and per-session socket limits. pub fn new(max_sockets: usize, max_sockets_per_session: usize) -> IoResult { - Self::new_with_port_mappings(max_sockets, max_sockets_per_session, &[]) + Self::new_with_tcp_port_publications(max_sockets, max_sockets_per_session, &[]) } - /// Starts a limited provider and reserves every mapped host endpoint immediately. - pub fn new_with_port_mappings( + /// Starts a limited provider and realizes every TCP publication immediately. + pub fn new_with_tcp_port_publications( max_sockets: usize, max_sockets_per_session: usize, - port_mappings: &[SocketPortMapping], + tcp_port_publications: &[TcpPortPublication], ) -> IoResult { - for (index, mapping) in port_mappings.iter().enumerate() { - if mapping.guest_port == 0 || mapping.host_address.port() == 0 { + for (index, publication) in tcp_port_publications.iter().enumerate() { + if publication.guest_port == 0 || publication.external_address.port() == 0 { return Err(Error::new( ErrorKind::InvalidInput, - "mapped guest and host ports must be nonzero", + "published guest and external ports must be nonzero", )); } - if port_mappings[..index].iter().any(|existing| { - existing.guest_port == mapping.guest_port - || existing.host_address == mapping.host_address + if tcp_port_publications[..index].iter().any(|existing| { + existing.guest_port == publication.guest_port + || existing.external_address == publication.external_address }) { return Err(Error::new( ErrorKind::InvalidInput, - "mapped guest ports and host endpoints must be unique", + "published guest ports and external endpoints must be unique", )); } } - let port_mappings = port_mappings.to_vec(); + let tcp_port_publications = tcp_port_publications.to_vec(); Ok(Self { reactor: Arc::new(ReactorClient::start( max_sockets, max_sockets_per_session, - port_mappings.clone(), + tcp_port_publications.clone(), )?), - port_mappings, + tcp_port_publications, }) } } fn create_port_mapping_reservation( - mapping: SocketPortMapping, + mapping: TcpPortPublication, reuse_address: bool, reuse_port: bool, ) -> core::result::Result { @@ -137,12 +129,12 @@ fn create_port_mapping_reservation( if reuse_port { sockopt::set_socket_reuseport(&socket, true)?; } - bind(&socket, &mapping.host_address)?; + bind(&socket, &mapping.external_address)?; Ok(socket) } fn create_replacement_port_mapping_reservation( - mapping: SocketPortMapping, + mapping: TcpPortPublication, ) -> core::result::Result { let socket = create_port_mapping_reservation(mapping, true, false)?; // Reuse is needed only while replacing a listener that may still have @@ -153,13 +145,8 @@ fn create_replacement_port_mapping_reservation( } impl SocketProvider for LinuxSocketProvider { - fn reserves_guest_port(&self, request: CreateSocketRequest, port: u16) -> bool { - if socket_kind(request) != Some(SocketKind::Tcp) { - return false; - } - self.port_mappings - .iter() - .any(|mapping| mapping.guest_port == port) + fn tcp_port_publications(&self) -> &[TcpPortPublication] { + &self.tcp_port_publications } fn create( @@ -212,23 +199,23 @@ struct LinuxSocket { } impl PlatformSocket for LinuxSocket { - fn bind( - &self, - address: SocketAddrV4, - kind: PlatformBindKind, - ) -> BrokerResult> { + fn bind(&self, address: SocketAddrV4) -> BrokerResult> { self.reactor.request(|response| ReactorCommand::Bind { id: self.id, address, - kind, response, }) } - fn listen(&self, backlog: u32) -> BrokerResult> { + fn listen( + &self, + backlog: u32, + publication: Option, + ) -> BrokerResult> { self.reactor.request(|response| ReactorCommand::Listen { id: self.id, backlog, + publication, response, }) } @@ -409,11 +396,11 @@ impl ReactorClient { fn start( max_sockets: usize, max_sockets_per_session: usize, - port_mappings: Vec, + tcp_port_publications: Vec, ) -> IoResult { let epoll_fd = epoll::create(epoll::CreateFlags::CLOEXEC)?; let wake = Arc::new(eventfd(0, EventfdFlags::CLOEXEC | EventfdFlags::NONBLOCK)?); - let port_mappings = port_mappings + let port_mappings = tcp_port_publications .into_iter() .map(|mapping| { let mut stale_connections = HashMap::new(); @@ -421,6 +408,7 @@ impl ReactorClient { Ok(PortMappingState { mapping, reservation: Some(create_port_mapping_reservation(mapping, false, false)?), + reservation_registered: false, claimed_by: None, stale_connections, }) @@ -681,12 +669,12 @@ enum ReactorCommand { Bind { id: u64, address: SocketAddrV4, - kind: PlatformBindKind, response: SyncSender>>, }, Listen { id: u64, backlog: u32, + publication: Option, response: SyncSender>>, }, Accept { @@ -847,6 +835,8 @@ struct SocketEntry { abortive_close: bool, guest_local_address: Option, port_mapping_index: Option, + tcp_port_publication_claim: Option, + publication_fallback_socket: Option, tcp_no_delay: bool, tcp_keep_alive: bool, } @@ -1295,13 +1285,16 @@ impl fmt::Display for ReactorFailure { } impl Reactor { - fn port_mapping_index(&self, kind: SocketKind, guest_port: u16) -> Option { - if kind != SocketKind::Tcp { - return None; - } - self.port_mappings - .iter() - .position(|state| state.mapping.guest_port == guest_port) + fn claimed_tcp_port_publication(&self, id: u64) -> Option<(usize, TcpPortPublicationClaim)> { + let socket = self.sockets.get(&id)?; + let claim = socket.tcp_port_publication_claim?; + let mapping_index = claim.publication_id().index(); + (socket.port_mapping_index == Some(mapping_index) + && self + .port_mappings + .get(mapping_index) + .is_some_and(|state| state.claimed_by == Some((id, claim)))) + .then_some((mapping_index, claim)) } /// Transfers one reserved host endpoint to the socket publishing it. @@ -1309,16 +1302,26 @@ impl Reactor { /// The reservation descriptor keeps the mapped endpoint exclusively owned /// between publications, so a claim replaces the socket's descriptor rather /// than binding the endpoint again. - fn claim_port_mapping( + fn realize_tcp_port_publication( &mut self, id: u64, - mapping_index: usize, + claim: TcpPortPublicationClaim, ) -> BrokerResult> { + let mapping_index = claim.publication_id().index(); + let guest_port = self + .sockets + .get(&id) + .and_then(|socket| socket.guest_local_address) + .ok_or(BrokerError::Internal)? + .port(); let mapping = self .port_mappings .get(mapping_index) .ok_or(BrokerError::Internal)? .mapping; + if mapping.guest_port != guest_port { + return Err(BrokerError::Internal); + } if self .port_mappings .get(mapping_index) @@ -1326,10 +1329,12 @@ impl Reactor { { match create_replacement_port_mapping_reservation(mapping) { Ok(reservation) => { - self.port_mappings + let state = self + .port_mappings .get_mut(mapping_index) - .ok_or(BrokerError::Internal)? - .reservation = Some(reservation); + .ok_or(BrokerError::Internal)?; + state.reservation = Some(reservation); + state.reservation_registered = false; } Err(error) => { return Ok(SocketOutcome::Failed(socket_operation_error_from_errno( @@ -1338,7 +1343,7 @@ impl Reactor { } } } - let reservation = { + let (reservation, reservation_registered) = { let state = self .port_mappings .get_mut(mapping_index) @@ -1349,7 +1354,9 @@ impl Reactor { let Some(reservation) = state.reservation.take() else { return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); }; - reservation + let reservation_registered = state.reservation_registered; + state.reservation_registered = false; + (reservation, reservation_registered) }; let preparation = sockopt::set_socket_linger(&reservation, None) .map_err(broker_error_from_errno) @@ -1359,32 +1366,88 @@ impl Reactor { Err(error) => { self.port_mappings .get_mut(mapping_index) - .ok_or(BrokerError::Internal)? - .reservation = Some(reservation); + .ok_or(BrokerError::Internal) + .map(|state| { + state.reservation = Some(reservation); + state.reservation_registered = reservation_registered; + })?; return Err(error); } }; if !stale_state_drained { - self.port_mappings + let state = self + .port_mappings .get_mut(mapping_index) - .ok_or(BrokerError::Internal)? - .reservation = Some(reservation); + .ok_or(BrokerError::Internal)?; + state.reservation = Some(reservation); + state.reservation_registered = reservation_registered; return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); } - let host_address = match self.replace_socket_descriptor(id, reservation) { + let host_address = match local_socket_address(&reservation) { Ok(address) => address, - Err((error, reservation)) => { - self.port_mappings + Err(error) => { + let state = self + .port_mappings .get_mut(mapping_index) - .ok_or(BrokerError::Internal)? - .reservation = Some(reservation); + .ok_or(BrokerError::Internal)?; + state.reservation = Some(reservation); + state.reservation_registered = reservation_registered; return Err(error); } }; + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + if socket.kind != SocketKind::Tcp || socket.publication_fallback_socket.is_some() { + let state = self + .port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)?; + state.reservation = Some(reservation); + state.reservation_registered = reservation_registered; + return Err(BrokerError::Internal); + } + if let Err(error) = + apply_tcp_options(&reservation, socket.tcp_no_delay, socket.tcp_keep_alive) + { + let state = self + .port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)?; + state.reservation = Some(reservation); + state.reservation_registered = reservation_registered; + return Err(error); + } + let registration = if reservation_registered { + epoll::modify( + &self.epoll, + &reservation, + epoll::EventData::new_u64(id), + idle_epoll_events(), + ) + } else { + epoll::add( + &self.epoll, + &reservation, + epoll::EventData::new_u64(id), + idle_epoll_events(), + ) + }; + if let Err(error) = registration { + let state = self + .port_mappings + .get_mut(mapping_index) + .ok_or(BrokerError::Internal)?; + state.reservation = Some(reservation); + state.reservation_registered = reservation_registered; + return Err(broker_error_from_errno(error)); + } + let fallback = core::mem::replace(&mut socket.socket, reservation); + socket.publication_fallback_socket = Some(fallback); + socket.port_mapping_index = Some(mapping_index); + socket.tcp_port_publication_claim = Some(claim); self.port_mappings .get_mut(mapping_index) .ok_or(BrokerError::Internal)? - .claimed_by = Some(id); + .claimed_by = Some((id, claim)); Ok(SocketOutcome::Completed(host_address)) } @@ -1422,7 +1485,7 @@ impl Reactor { } fn stop_listening_socket(&mut self, id: u64) -> BrokerResult> { - let mapping_index = { + let claimed_publication = { let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; if socket.kind != SocketKind::Tcp || !socket.listening { return self @@ -1431,29 +1494,28 @@ impl Reactor { .ok_or(BrokerError::Internal) .and_then(|socket| shutdown_socket(socket, ShutdownMode::StopListening)); } - socket.port_mapping_index.filter(|mapping_index| { - self.port_mappings - .get(*mapping_index) - .is_some_and(|state| state.claimed_by == Some(id)) - }) + self.claimed_tcp_port_publication(id) }; - let Some(mapping_index) = mapping_index else { + let Some((mapping_index, claim)) = claimed_publication else { + let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; + if socket.port_mapping_index.is_some() || socket.tcp_port_publication_claim.is_some() { + return Err(BrokerError::Internal); + } return self .sockets .get_mut(&id) .ok_or(BrokerError::Internal) .and_then(|socket| shutdown_socket(socket, ShutdownMode::StopListening)); }; - if !self - .port_mappings - .get(mapping_index) - .is_some_and(|state| state.claimed_by == Some(id) && state.reservation.is_none()) - { + if !self.port_mappings.get(mapping_index).is_some_and(|state| { + state.claimed_by == Some((id, claim)) && state.reservation.is_none() + }) { return Err(BrokerError::Internal); } let (session_id, guest_port, no_delay, keep_alive) = self .sockets .get(&id) + .filter(|socket| socket.publication_fallback_socket.is_none()) .and_then(|socket| { socket.guest_local_address.map(|guest_address| { ( @@ -1504,6 +1566,7 @@ impl Reactor { socket.read_shutdown = true; socket.peek_waitall_threshold = None; socket.port_mapping_index = None; + socket.tcp_port_publication_claim = None; reservation }; let state = self @@ -1512,67 +1575,50 @@ impl Reactor { .ok_or(BrokerError::Internal)?; state.claimed_by = None; state.reservation = Some(reservation); + state.reservation_registered = false; Ok(SocketOutcome::Completed(())) } - fn release_failed_port_mapping_claim( + fn release_failed_tcp_port_publication( &mut self, id: u64, - mapping_index: usize, + claim: TcpPortPublicationClaim, ) -> BrokerResult<()> { - if !self - .port_mappings - .get(mapping_index) - .is_some_and(|state| state.claimed_by == Some(id) && state.reservation.is_none()) - { + let mapping_index = claim.publication_id().index(); + if !self.port_mappings.get(mapping_index).is_some_and(|state| { + state.claimed_by == Some((id, claim)) && state.reservation.is_none() + }) { return Err(BrokerError::Internal); } - let (session_id, guest_port, no_delay, keep_alive) = self + let (session_id, guest_port) = self .sockets .get(&id) .and_then(|socket| { - (!socket.listening).then_some(())?; - socket.guest_local_address.map(|guest_address| { - ( - socket.session_id, - guest_address.port(), - socket.tcp_no_delay, - socket.tcp_keep_alive, - ) - }) + (!socket.listening + && socket.port_mapping_index == Some(mapping_index) + && socket.tcp_port_publication_claim == Some(claim) + && socket.publication_fallback_socket.is_some()) + .then_some(())?; + socket + .guest_local_address + .map(|guest_address| (socket.session_id, guest_address.port())) }) .ok_or(BrokerError::Internal)?; - if !self + let host_address_is_set = self .sessions .get(&session_id) .and_then(|namespace| namespace.tcp.get(&guest_port)) - .is_some_and(|binding| binding.socket_id == id && binding.host_mapped) - { - return Err(BrokerError::Internal); - } - let replacement = socket_with( - LinuxAddressFamily::INET, - LinuxSocketType::STREAM, - LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, - Some(ipproto::TCP), - ) - .map_err(broker_error_from_errno)?; - apply_tcp_options(&replacement, no_delay, keep_alive)?; - epoll::add( - &self.epoll, - &replacement, - epoll::EventData::new_u64(id), - idle_epoll_events(), - ) - .map_err(broker_error_from_errno)?; - let old_socket = &self.sockets.get(&id).ok_or(BrokerError::Internal)?.socket; - if !delete_epoll_registration(&self.epoll, old_socket) { - let _ = delete_epoll_registration(&self.epoll, &replacement); - return Err(BrokerError::Internal); - } + .filter(|binding| binding.socket_id == id) + .map(|binding| binding.host_mapped); let reservation = { let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; - core::mem::replace(&mut socket.socket, replacement) + let fallback = socket + .publication_fallback_socket + .take() + .ok_or(BrokerError::Internal)?; + socket.port_mapping_index = None; + socket.tcp_port_publication_claim = None; + core::mem::replace(&mut socket.socket, fallback) }; let state = self .port_mappings @@ -1580,51 +1626,17 @@ impl Reactor { .ok_or(BrokerError::Internal)?; state.claimed_by = None; state.reservation = Some(reservation); - self.sessions - .get_mut(&session_id) - .ok_or(BrokerError::Internal)? - .clear_host_address(SocketKind::Tcp, guest_port, id) - } - - /// Replaces a socket's descriptor, returning ownership of the replacement on failure. - fn replace_socket_descriptor( - &mut self, - id: u64, - replacement: OwnedFd, - ) -> core::result::Result { - let host_address = match local_socket_address(&replacement) { - Ok(address) => address, - Err(error) => return Err((error, replacement)), - }; - let Some(socket) = self.sockets.get_mut(&id) else { - return Err((BrokerError::Internal, replacement)); - }; - if socket.kind == SocketKind::Tcp - && let Err(error) = - apply_tcp_options(&replacement, socket.tcp_no_delay, socket.tcp_keep_alive) - { - return Err((error, replacement)); - } - let events = if socket.kind == SocketKind::Tcp { - idle_epoll_events() - } else { - active_epoll_events() + state.reservation_registered = true; + let Some(host_address_is_set) = host_address_is_set else { + return Err(BrokerError::Internal); }; - if let Err(error) = epoll::add( - &self.epoll, - &replacement, - epoll::EventData::new_u64(id), - events, - ) { - return Err((broker_error_from_errno(error), replacement)); - } - if let Err(error) = epoll::delete(&self.epoll, &socket.socket) { - let _ = epoll::delete(&self.epoll, &replacement); - return Err((broker_error_from_errno(error), replacement)); + if host_address_is_set { + self.sessions + .get_mut(&session_id) + .ok_or(BrokerError::Internal)? + .clear_host_address(SocketKind::Tcp, guest_port, id)?; } - let old_socket = core::mem::replace(&mut socket.socket, replacement); - drop(old_socket); - Ok(host_address) + Ok(()) } /// Returns whether `address` names a host endpoint backing a guest socket. @@ -1703,7 +1715,6 @@ impl Reactor { &mut self, id: u64, requested_address: SocketAddrV4, - bind_kind: PlatformBindKind, ) -> BrokerResult> { let (session_id, kind, already_bound) = { let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; @@ -1748,9 +1759,6 @@ impl Reactor { .ok_or(BrokerError::Internal)? .reserve_binding(kind)?; let guest_address = SocketAddrV4::new(*requested_address.ip(), guest_port); - let port_mapping_index = (bind_kind == PlatformBindKind::Explicit) - .then(|| self.port_mapping_index(kind, guest_port)) - .flatten(); self.sessions .get_mut(&session_id) .ok_or(BrokerError::Internal)? @@ -1768,7 +1776,6 @@ impl Reactor { )?; let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; socket.guest_local_address = Some(guest_address); - socket.port_mapping_index = port_mapping_index; socket .snapshot .lock() @@ -1781,11 +1788,19 @@ impl Reactor { &mut self, id: u64, backlog: u32, + publication_claim: Option, ) -> BrokerResult> { - let (session_id, kind, guest_address) = self + let (session_id, kind, guest_address, current_claim) = self .sockets .get(&id) - .map(|socket| (socket.session_id, socket.kind, socket.guest_local_address)) + .map(|socket| { + ( + socket.session_id, + socket.kind, + socket.guest_local_address, + socket.tcp_port_publication_claim, + ) + }) .ok_or(BrokerError::Internal)?; if kind != SocketKind::Tcp { return Ok(SocketOutcome::Failed(SocketError::InvalidArgument)); @@ -1795,14 +1810,30 @@ impl Reactor { local_socket_address(&self.sockets.get(&id).ok_or(BrokerError::Internal)?.socket)? .port() == 0; + if let Some(current_claim) = current_claim { + if publication_claim != Some(current_claim) { + return Err(BrokerError::Internal); + } + let mapping_index = current_claim.publication_id().index(); + if !self.port_mappings.get(mapping_index).is_some_and(|state| { + state.claimed_by == Some((id, current_claim)) + && self + .sockets + .get(&id) + .is_some_and(|socket| socket.port_mapping_index == Some(mapping_index)) + }) { + return Err(BrokerError::Internal); + } + } else if publication_claim.is_some() && !needs_host_bind { + return Err(BrokerError::Internal); + } + let newly_claimed_publication = current_claim + .is_none() + .then_some(publication_claim) + .flatten(); if needs_host_bind { - let port_mapping_index = self - .sockets - .get(&id) - .ok_or(BrokerError::Internal)? - .port_mapping_index; - let (host_address, host_mapped) = if let Some(mapping_index) = port_mapping_index { - match self.claim_port_mapping(id, mapping_index)? { + let (host_address, host_mapped) = if let Some(claim) = publication_claim { + match self.realize_tcp_port_publication(id, claim)? { SocketOutcome::Completed(address) => (address, true), SocketOutcome::Failed(error) => return Ok(SocketOutcome::Failed(error)), } @@ -1815,28 +1846,57 @@ impl Reactor { }; (address, false) }; - self.sessions + if let Err(error) = self + .sessions .get_mut(&session_id) - .ok_or(BrokerError::Internal)? - .set_host_address(kind, guest_address.port(), id, host_address, host_mapped)?; + .ok_or(BrokerError::Internal) + .and_then(|namespace| { + namespace.set_host_address( + kind, + guest_address.port(), + id, + host_address, + host_mapped, + ) + }) + { + if let Some(claim) = newly_claimed_publication { + self.release_failed_tcp_port_publication(id, claim)?; + } + return Err(error); + } } let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; let host_mapped = socket.port_mapping_index.is_some(); - let was_listening = socket.listening; - let outcome = listen_tcp_socket(&self.epoll, id, socket, backlog, host_mapped)?; + let outcome = listen_tcp_socket(&self.epoll, id, socket, backlog, host_mapped); match outcome { - SocketOutcome::Completed(()) => Ok(SocketOutcome::Completed(guest_address)), - SocketOutcome::Failed(error) => { - if host_mapped && !was_listening { - let mapping_index = self - .sockets - .get(&id) - .and_then(|socket| socket.port_mapping_index) - .ok_or(BrokerError::Internal)?; - self.release_failed_port_mapping_claim(id, mapping_index)?; + Ok(SocketOutcome::Completed(())) => { + if let Some(claim) = newly_claimed_publication { + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + if socket.tcp_port_publication_claim != Some(claim) { + return Err(BrokerError::Internal); + } + drop( + socket + .publication_fallback_socket + .take() + .ok_or(BrokerError::Internal)?, + ); + } + Ok(SocketOutcome::Completed(guest_address)) + } + Ok(SocketOutcome::Failed(error)) => { + if let Some(claim) = newly_claimed_publication { + self.release_failed_tcp_port_publication(id, claim)?; } Ok(SocketOutcome::Failed(error)) } + Err(error) => { + if let Some(claim) = newly_claimed_publication { + self.release_failed_tcp_port_publication(id, claim)?; + } + Err(error) + } } } @@ -1857,15 +1917,7 @@ impl Reactor { ))?; return connect_datagram_socket(socket, address); } - let claims_mapping = self - .sockets - .get(&id) - .and_then(|socket| socket.port_mapping_index) - .is_some_and(|mapping_index| { - self.port_mappings - .get(mapping_index) - .is_some_and(|state| state.claimed_by == Some(id)) - }); + let claims_mapping = self.claimed_tcp_port_publication(id).is_some(); if claims_mapping { let status = SocketConnectionStatus::Failed(SocketError::InvalidArgument); update_snapshot( @@ -2057,15 +2109,14 @@ impl Reactor { /// Drops a socket, releasing its guest port and any claimed host mapping. fn remove_socket(&mut self, id: u64) { let port_mapping = self - .sockets - .get(&id) - .and_then(|socket| socket.port_mapping_index) - .filter(|mapping_index| { - self.port_mappings - .get(*mapping_index) - .is_some_and(|state| state.claimed_by == Some(id)) - }) - .map(|mapping_index| (mapping_index, self.port_mappings[mapping_index].mapping)); + .claimed_tcp_port_publication(id) + .map(|(mapping_index, claim)| { + ( + mapping_index, + claim, + self.port_mappings[mapping_index].mapping, + ) + }); let retired_listener_address = self.sockets.get(&id).and_then(|socket| { (socket.kind == SocketKind::Tcp && socket.listening) .then(|| local_socket_address(&socket.socket).ok()) @@ -2118,7 +2169,7 @@ impl Reactor { } }; if retired_listener_address.is_some() - && let Some((mapping_index, _)) = port_mapping + && let Some((mapping_index, _, _)) = port_mapping && !retain_original { self.clear_stale_tcp_connections(mapping_index); @@ -2127,7 +2178,7 @@ impl Reactor { if let Some(namespace) = self.sessions.get_mut(&session_id) { let retained_before = namespace.retained_connectors; if let Some(listener_address) = retired_listener_address { - if let Some((mapping_index, _)) = port_mapping { + if let Some((mapping_index, _, _)) = port_mapping { let state = &mut self.port_mappings[mapping_index]; if retain_original { namespace.move_tcp_translations_for_listener( @@ -2241,15 +2292,17 @@ impl Reactor { ) } else { drop(socket.take()); - port_mapping - .and_then(|(_, mapping)| create_replacement_port_mapping_reservation(mapping).ok()) + port_mapping.and_then(|(_, _, mapping)| { + create_replacement_port_mapping_reservation(mapping).ok() + }) }; - if let Some((mapping_index, _)) = port_mapping + if let Some((mapping_index, claim, _)) = port_mapping && let Some(state) = self.port_mappings.get_mut(mapping_index) - && state.claimed_by == Some(id) + && state.claimed_by == Some((id, claim)) { state.claimed_by = None; state.reservation = replacement_reservation; + state.reservation_registered = false; } } @@ -2348,18 +2401,18 @@ impl Reactor { ReactorCommand::Bind { id, address, - kind, response, } => { - let outcome = self.bind_socket(id, address, kind); + let outcome = self.bind_socket(id, address); let _ = response.send(outcome); } ReactorCommand::Listen { id, backlog, + publication, response, } => { - let outcome = self.listen_socket(id, backlog); + let outcome = self.listen_socket(id, backlog, publication); let _ = response.send(outcome); } ReactorCommand::Accept { @@ -2696,6 +2749,8 @@ impl Reactor { abortive_close: false, guest_local_address: None, port_mapping_index: None, + tcp_port_publication_claim: None, + publication_fallback_socket: None, tcp_no_delay: false, tcp_keep_alive: false, }, @@ -3040,6 +3095,8 @@ impl Reactor { abortive_close: false, guest_local_address: Some(local_address), port_mapping_index: None, + tcp_port_publication_claim: None, + publication_fallback_socket: None, tcp_no_delay: listener_tcp_no_delay, tcp_keep_alive: listener_tcp_keep_alive, }, @@ -4245,9 +4302,9 @@ mod tests { fn port_mapping_reservation_is_close_on_exec() { let host_address = unused_tcp_address(); let retained = create_port_mapping_reservation( - SocketPortMapping { + TcpPortPublication { guest_port: 80, - host_address, + external_address: host_address, }, false, false, @@ -4265,12 +4322,12 @@ mod tests { fn unavailable_publish_endpoint_rejects_provider_startup() { let occupied = TcpListener::bind("127.0.0.1:0").unwrap(); let host_address = socket_address_v4(occupied.local_addr().unwrap()); - let error = LinuxSocketProvider::new_with_port_mappings( + let error = LinuxSocketProvider::new_with_tcp_port_publications( 1, 1, - &[SocketPortMapping { + &[TcpPortPublication { guest_port: 80, - host_address, + external_address: host_address, }], ) .err() @@ -4823,12 +4880,12 @@ mod tests { fn mapped_tcp_listener_can_be_replaced_while_an_accepted_socket_remains() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_port_mappings( + LinuxSocketProvider::new_with_tcp_port_publications( 2, 2, - &[SocketPortMapping { + &[TcpPortPublication { guest_port: 80, - host_address, + external_address: host_address, }], ) .unwrap(), @@ -4872,9 +4929,9 @@ mod tests { ); assert_eq!( create_port_mapping_reservation( - SocketPortMapping { + TcpPortPublication { guest_port: 80, - host_address, + external_address: host_address, }, true, true, @@ -4904,12 +4961,12 @@ mod tests { fn stopped_mapped_listener_cannot_route_into_another_session() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_port_mappings( + LinuxSocketProvider::new_with_tcp_port_publications( 3, 2, - &[SocketPortMapping { + &[TcpPortPublication { guest_port: 80, - host_address, + external_address: host_address, }], ) .unwrap(), @@ -4979,12 +5036,12 @@ mod tests { fn mapped_listener_handoff_drains_bounded_stale_connections() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_port_mappings( + LinuxSocketProvider::new_with_tcp_port_publications( 3, 2, - &[SocketPortMapping { + &[TcpPortPublication { guest_port: 80, - host_address, + external_address: host_address, }], ) .unwrap(), @@ -5216,12 +5273,12 @@ mod tests { fn closing_mapped_listener_discards_more_than_sixty_four_queued_connections() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_port_mappings( + LinuxSocketProvider::new_with_tcp_port_publications( 2, 2, - &[SocketPortMapping { + &[TcpPortPublication { guest_port: 80, - host_address, + external_address: host_address, }], ) .unwrap(), @@ -5257,9 +5314,9 @@ mod tests { assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), listener); assert_eq!( create_port_mapping_reservation( - SocketPortMapping { + TcpPortPublication { guest_port: 80, - host_address, + external_address: host_address, }, true, true, @@ -5655,12 +5712,12 @@ mod tests { fn reactor_assigns_a_port_to_an_unbound_tcp_listener() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_port_mappings( + LinuxSocketProvider::new_with_tcp_port_publications( 2, 2, - &[SocketPortMapping { + &[TcpPortPublication { guest_port: FIRST_GUEST_EPHEMERAL_PORT, - host_address, + external_address: host_address, }], ) .unwrap(), @@ -5740,9 +5797,9 @@ mod tests { ); assert_eq!( create_port_mapping_reservation( - SocketPortMapping { + TcpPortPublication { guest_port: FIRST_GUEST_EPHEMERAL_PORT, - host_address, + external_address: host_address, }, true, true, @@ -5757,12 +5814,12 @@ mod tests { fn reactor_drives_a_loopback_tcp_listener() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_port_mappings( + LinuxSocketProvider::new_with_tcp_port_publications( 4, 4, - &[SocketPortMapping { + &[TcpPortPublication { guest_port: FIRST_GUEST_EPHEMERAL_PORT, - host_address, + external_address: host_address, }], ) .unwrap(), diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 991141e8c..92e104d40 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -20,9 +20,10 @@ use clap::Parser; use litebox_broker_core::{ BrokerCore, BrokerCoreLimits, CallerCredential, DestinationPortRange, DestinationRule, Ipv4Cidr, ObjectRights, PolicyEngine, SocketPolicy, SocketPolicyError, + socket::TcpPortPublication, }; use litebox_broker_host::{BrokerHostAssociation, ConnectionTermination, setup_connection}; -use litebox_broker_platform_linux_userland::{LinuxSocketProvider, SocketPortMapping}; +use litebox_broker_platform_linux_userland::LinuxSocketProvider; use litebox_broker_protocol::message::BrokerRequest; use litebox_broker_protocol::shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE}; use litebox_broker_protocol::socket::{Ipv4Address, Port}; @@ -85,31 +86,31 @@ impl FromStr for AllowedTcpDestination { } } -/// Command-line description of one host-to-guest TCP port mapping. +/// Command-line description of one external-to-guest TCP publication. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct PortMappingArgument { - host_address: SocketAddrV4, +struct TcpPortPublicationArgument { + external_address: SocketAddrV4, guest_port: u16, } -impl FromStr for PortMappingArgument { +impl FromStr for TcpPortPublicationArgument { type Err = String; fn from_str(value: &str) -> Result { - let (host_address, guest_port) = value + let (external_address, guest_port) = value .rsplit_once(':') .ok_or_else(|| "expected HOST_IP:HOST_PORT:GUEST_PORT".to_owned())?; - let host_address = host_address + let external_address = external_address .parse::() - .map_err(|error| format!("invalid host IPv4 endpoint: {error}"))?; + .map_err(|error| format!("invalid external IPv4 endpoint: {error}"))?; let guest_port = guest_port .parse::() .map_err(|error| format!("invalid guest port: {error}"))?; - if host_address.port() == 0 || guest_port == 0 { - return Err("mapped host and guest ports must be nonzero".to_owned()); + if external_address.port() == 0 || guest_port == 0 { + return Err("published external and guest ports must be nonzero".to_owned()); } Ok(Self { - host_address, + external_address, guest_port, }) } @@ -125,7 +126,7 @@ struct CliArgs { allow_tcp_destination: Vec, /// Publish a host IPv4 TCP endpoint to a guest-local TCP port. #[arg(long, value_name = "HOST_IP:HOST_PORT:GUEST_PORT")] - publish_tcp: Vec, + publish_tcp: Vec, /// Local runner executable to launch. #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::ExecutablePath)] runner: PathBuf, @@ -143,15 +144,15 @@ fn main() -> Result<(), Box> { let control_listener = UnixListener::bind(&control_socket_path)?; control_listener.set_nonblocking(true)?; let limits = BrokerCoreLimits::DEFAULT; - let port_mappings = configured_port_mappings(&args.publish_tcp); + let tcp_port_publications = configured_tcp_port_publications(&args.publish_tcp); let broker = BrokerCore::new_with_limits( PolicyEngine::with_host_guaranteed_rights(ObjectRights::all()) .with_socket_policy(configured_socket_policy(&args.allow_tcp_destination)?), limits, - Arc::new(LinuxSocketProvider::new_with_port_mappings( + Arc::new(LinuxSocketProvider::new_with_tcp_port_publications( limits.max_sockets, limits.max_sockets_per_session, - &port_mappings, + &tcp_port_publications, )?), )?; @@ -177,11 +178,11 @@ fn main() -> Result<(), Box> { Ok(()) } -fn configured_port_mappings(tcp: &[PortMappingArgument]) -> Vec { +fn configured_tcp_port_publications(tcp: &[TcpPortPublicationArgument]) -> Vec { tcp.iter() - .map(|mapping| SocketPortMapping { - guest_port: mapping.guest_port, - host_address: mapping.host_address, + .map(|publication| TcpPortPublication { + guest_port: publication.guest_port, + external_address: publication.external_address, }) .collect() } @@ -611,24 +612,34 @@ mod tests { } #[test] - fn socket_port_mapping_arguments_name_distinct_host_and_guest_ports() { - let mapping = "127.0.0.1:8080:80".parse::().unwrap(); + fn tcp_publication_arguments_name_distinct_external_and_guest_ports() { + let publication = "127.0.0.1:8080:80" + .parse::() + .unwrap(); assert_eq!( - mapping, - PortMappingArgument { - host_address: "127.0.0.1:8080".parse().unwrap(), + publication, + TcpPortPublicationArgument { + external_address: "127.0.0.1:8080".parse().unwrap(), guest_port: 80, } ); - assert!("127.0.0.1:0:80".parse::().is_err()); - assert!("127.0.0.1:8080:0".parse::().is_err()); - assert!("8080:80".parse::().is_err()); + assert!( + "127.0.0.1:0:80" + .parse::() + .is_err() + ); + assert!( + "127.0.0.1:8080:0" + .parse::() + .is_err() + ); + assert!("8080:80".parse::().is_err()); assert_eq!( - configured_port_mappings(&[mapping]), - vec![SocketPortMapping { + configured_tcp_port_publications(&[publication]), + vec![TcpPortPublication { guest_port: 80, - host_address: "127.0.0.1:8080".parse().unwrap(), + external_address: "127.0.0.1:8080".parse().unwrap(), }] ); } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index e454c96e7..0478d22d3 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -342,15 +342,20 @@ fn spawn_test_broker( policy: litebox_broker_core::PolicyEngine, connection_count: usize, ) -> TestBroker { - spawn_test_broker_with_port_mappings(control_socket_path, policy, connection_count, Vec::new()) + spawn_test_broker_with_tcp_port_publications( + control_socket_path, + policy, + connection_count, + Vec::new(), + ) } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -fn spawn_test_broker_with_port_mappings( +fn spawn_test_broker_with_tcp_port_publications( control_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, connection_count: usize, - port_mappings: Vec, + tcp_port_publications: Vec, ) -> TestBroker { let _ = std::fs::remove_file(control_socket_path); @@ -369,10 +374,10 @@ fn spawn_test_broker_with_port_mappings( policy, limits, std::sync::Arc::new( - litebox_broker_platform_linux_userland::LinuxSocketProvider::new_with_port_mappings( + litebox_broker_platform_linux_userland::LinuxSocketProvider::new_with_tcp_port_publications( limits.max_sockets, limits.max_sockets_per_session, - &port_mappings, + &tcp_port_publications, ) .expect("failed to create broker test socket provider"), ), @@ -766,16 +771,16 @@ fn test_runner_broker_tcp_server_with_rewriter() { }; drop(host_listener); let control_socket_path = unique_test_socket_path("runner-broker-tcp-server-control"); - let broker = spawn_test_broker_with_port_mappings( + let broker = spawn_test_broker_with_tcp_port_publications( &control_socket_path, litebox_broker_core::PolicyEngine::with_host_guaranteed_rights( litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(litebox_broker_core::SocketPolicy::Ipv4Loopback), 1, - vec![litebox_broker_platform_linux_userland::SocketPortMapping { + vec![litebox_broker_core::socket::TcpPortPublication { guest_port: GUEST_PORT, - host_address, + external_address: host_address, }], ); let mut child = Runner::new(&target, "broker_tcp_server_rewriter") From 5b27cd80939cd3f0c59afe643340c57d1a60114f Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 12 Aug 2026 09:24:07 -0700 Subject: [PATCH 6/6] Share broker TCP namespace across sessions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- dev_bench/src/main.rs | 6 +- litebox_broker_core/src/lib.rs | 12 +- litebox_broker_core/src/policy.rs | 29 +- litebox_broker_core/src/session.rs | 5 +- litebox_broker_core/src/socket.rs | 784 ++--- litebox_broker_host/src/lib.rs | 6 +- .../src/socket.rs | 2958 ++++++++++------- litebox_broker_userland/src/main.rs | 85 +- litebox_runner_linux_userland/tests/run.rs | 20 +- .../tests/tcp_broker.c | 4 +- .../tests/tcp_broker_server.c | 4 +- 11 files changed, 2246 insertions(+), 1667 deletions(-) diff --git a/dev_bench/src/main.rs b/dev_bench/src/main.rs index 63524fc16..d051f9818 100644 --- a/dev_bench/src/main.rs +++ b/dev_bench/src/main.rs @@ -722,8 +722,10 @@ fn run_rewritten_iperf3(ctx: BenchCtx<'_>) -> Result<()> { .to_string(); let mut server_command = std::process::Command::new(&broker); server_command - .arg("--publish-tcp") - .arg(format!("127.0.0.1:{host_port}:{guest_port}")) + .arg("--broker-ipv4-address") + .arg("127.0.0.1") + .arg("--tcp-port-mapping") + .arg(format!("{host_port}:{guest_port}")) .arg("--runner") .arg(&runner) .arg("--") diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index da507e7b9..277105e80 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -40,7 +40,7 @@ pub use policy::{ }; use session::ObjectReference; pub use session::{BrokerSession, CallerCredential, ObjectRights, SessionId}; -use socket::{SocketProvider, TcpPortPublicationAuthority}; +use socket::{BrokerSocketPorts, SocketProvider, TcpPortMappingConfig}; /// BrokerCore result type. pub type Result = core::result::Result; @@ -117,7 +117,8 @@ pub struct BrokerCore { pub(crate) reserved_pipe_capacity: Arc, pub(crate) reserved_sockets: Arc, pub(crate) socket_provider: Arc, - pub(crate) tcp_port_publications: TcpPortPublicationAuthority, + pub(crate) tcp_port_mapping_config: TcpPortMappingConfig, + pub(crate) socket_ports: BrokerSocketPorts, } static BROKER_CORE_CREATED: AtomicBool = AtomicBool::new(false); @@ -134,8 +135,8 @@ impl BrokerCore { limits: BrokerCoreLimits, socket_provider: Arc, ) -> Result { - let tcp_port_publications = - TcpPortPublicationAuthority::new(socket_provider.tcp_port_publications())?; + let tcp_port_mapping_config = + TcpPortMappingConfig::new(socket_provider.tcp_port_mappings())?; BROKER_CORE_CREATED .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .map_err(|_| BrokerError::BrokerCoreAlreadyExists)?; @@ -150,7 +151,8 @@ impl BrokerCore { reserved_pipe_capacity: Arc::new(AtomicUsize::new(0)), reserved_sockets: Arc::new(AtomicUsize::new(0)), socket_provider, - tcp_port_publications, + tcp_port_mapping_config, + socket_ports: BrokerSocketPorts::default(), }) } diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index f12a9b2b1..81656b2ad 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -466,16 +466,17 @@ impl PolicyEngine { address: SocketAddrV4, ) -> Result<(), BrokerError> { self.principal_object_rights(caller_credential)?; - // Egress rules do not describe local listener authority. Socket - // creation admission plus this fixed loopback boundary governs binds. - let supported_socket = matches!( - (request.socket_type, request.protocol), - (SocketType::Stream, IpProtocol::Tcp) | (SocketType::Datagram, IpProtocol::Udp) - ); - if request.address_family == AddressFamily::Ipv4 - && supported_socket - && address.ip().is_loopback() - { + // Egress rules do not describe local listener authority. TCP may bind + // loopback for a private endpoint or unspecified for the broker's + // configured external interface. UDP remains loopback-only. + let permitted_address = match (request.socket_type, request.protocol) { + (SocketType::Stream, IpProtocol::Tcp) => { + address.ip().is_loopback() || address.ip().is_unspecified() + } + (SocketType::Datagram, IpProtocol::Udp) => address.ip().is_loopback(), + _ => false, + }; + if request.address_family == AddressFamily::Ipv4 && permitted_address { Ok(()) } else { Err(BrokerError::PolicyDenied) @@ -787,5 +788,13 @@ mod tests { ), Err(BrokerError::PolicyDenied) ); + assert_eq!( + policy.authorize_socket_bind( + CallerCredential::Unauthenticated, + IPV4_TCP, + address([0, 0, 0, 0], 0), + ), + Ok(()) + ); } } diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index 885579d8d..aa4a65f03 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -6,7 +6,7 @@ use core::sync::atomic::{AtomicUsize, Ordering}; use crate::event::EventObject; use crate::pipe::PipeObject; -use crate::socket::{SessionSocketPorts, SocketObject}; +use crate::socket::SocketObject; use crate::{BrokerCore, BrokerError, Result}; use hashbrown::HashMap; use litebox_broker_protocol::ObjectHandle; @@ -77,8 +77,6 @@ pub struct BrokerSession { references: Mutex, /// Socket quota held by pending, live, and closing in-flight resources. pub(crate) reserved_sockets: Arc, - /// Guest-visible TCP port namespace owned by this session. - pub(crate) socket_ports: SessionSocketPorts, } impl BrokerSession { @@ -97,7 +95,6 @@ impl BrokerSession { pending_handles: 0, }), reserved_sockets: Arc::new(AtomicUsize::new(0)), - socket_ports: SessionSocketPorts::default(), } } diff --git a/litebox_broker_core/src/socket.rs b/litebox_broker_core/src/socket.rs index 7304c9381..0f0cdff83 100644 --- a/litebox_broker_core/src/socket.rs +++ b/litebox_broker_core/src/socket.rs @@ -7,7 +7,7 @@ use alloc::{sync::Arc, vec::Vec}; use core::net::{Ipv4Addr, SocketAddrV4}; use core::sync::atomic::{AtomicUsize, Ordering}; -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::socket::{ @@ -23,171 +23,94 @@ use crate::readiness::{ReadinessRegistration, ReadinessSink}; use crate::session::{ObjectEntry, ObjectRights}; use crate::{BrokerError, BrokerSession, Result, SessionId}; -const DEFAULT_TCP_LISTEN_ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); -const DEFAULT_TCP_LOCAL_ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); +const DEFAULT_TCP_LISTEN_ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0); +const DEFAULT_TCP_LOCAL_ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0); +/// First port in the IANA dynamic/private range used for guest-local allocation. const FIRST_EPHEMERAL_PORT: u16 = 49152; -/// Portable declaration that publishes one external TCP endpoint as a guest port. +/// Portable mapping from one broker TCP port to a guest-local TCP port. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct TcpPortPublication { - /// Guest-local TCP port eligible for explicit publication. +pub struct TcpPortMapping { + /// TCP port on the broker's configured host-facing IPv4 address. + pub broker_port: u16, + /// Guest-local TCP port eligible for the mapping. pub guest_port: u16, - /// Externally reachable endpoint realized by the platform provider. - pub external_address: SocketAddrV4, -} - -/// Broker-assigned identity of one configured TCP port publication. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct TcpPortPublicationId(u32); - -impl TcpPortPublicationId { - /// Returns the provider declaration index for this publication. - #[must_use] - pub const fn index(self) -> usize { - self.0 as usize - } -} - -/// Portable lease token authorizing one listener generation to realize a publication. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct TcpPortPublicationClaim { - publication_id: TcpPortPublicationId, - generation: u64, -} - -impl TcpPortPublicationClaim { - /// Returns the configured publication being claimed. - #[must_use] - pub const fn publication_id(self) -> TcpPortPublicationId { - self.publication_id - } - - /// Returns the broker-assigned listener generation. - #[must_use] - pub const fn generation(self) -> u64 { - self.generation - } } +/// Broker startup configuration overriding selected guest TCP ports. +/// +/// This table does not track active listeners or runtime port ownership. When +/// an external listener starts, an absent guest port uses the identity mapping +/// (`broker_port = guest_port`). [`BrokerSocketPorts`] owns live guest ports, +/// while the platform owns each realized broker endpoint. #[derive(Clone)] -pub(crate) struct TcpPortPublicationAuthority { - state: Arc>>, +pub(crate) struct TcpPortMappingConfig { + broker_ports: Arc>, } -struct TcpPortPublicationState { - publication: TcpPortPublication, - next_generation: u64, - claimed_by: Option<(SessionId, u64)>, -} - -impl TcpPortPublicationAuthority { - pub(crate) fn new(publications: &[TcpPortPublication]) -> Result { - let mut state = Vec::new(); - state - .try_reserve_exact(publications.len()) +impl TcpPortMappingConfig { + pub(crate) fn new(mappings: &[TcpPortMapping]) -> Result { + let mut broker_ports: HashMap = HashMap::new(); + broker_ports + .try_reserve(mappings.len()) .map_err(|_| BrokerError::OutOfMemory)?; - for (index, publication) in publications.iter().copied().enumerate() { - if publication.guest_port == 0 - || publication.external_address.port() == 0 - || publications[..index].iter().any(|existing| { - existing.guest_port == publication.guest_port - || existing.external_address == publication.external_address - }) + for mapping in mappings.iter().copied() { + if mapping.guest_port == 0 + || mapping.broker_port == 0 + || broker_ports.contains_key(&mapping.guest_port) + || broker_ports + .values() + .any(|existing| *existing == mapping.broker_port) + { + return Err(BrokerError::Internal); + } + if broker_ports + .insert(mapping.guest_port, mapping.broker_port) + .is_some() { return Err(BrokerError::Internal); } - state.push(TcpPortPublicationState { - publication, - next_generation: 1, - claimed_by: None, - }); } Ok(Self { - state: Arc::new(Mutex::new(state)), + broker_ports: Arc::new(broker_ports), }) } - fn publication_id( - &self, - request: CreateSocketRequest, - guest_port: u16, - ) -> Option { - if !is_tcp(request) { - return None; - } - let state = self.state.lock(); - let index = state - .iter() - .position(|entry| entry.publication.guest_port == guest_port)?; - Some(TcpPortPublicationId(u32::try_from(index).ok()?)) - } - - fn claim( - &self, - session_id: SessionId, - publication_id: TcpPortPublicationId, - ) -> Result> { - let mut state = self.state.lock(); - let entry = state - .get_mut(publication_id.index()) - .ok_or(BrokerError::Internal)?; - if entry.claimed_by.is_some() { - return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); - } - let generation = entry.next_generation; - entry.next_generation = generation - .checked_add(1) - .ok_or(BrokerError::ResourceExhausted)?; - entry.claimed_by = Some((session_id, generation)); - Ok(SocketOutcome::Completed(TcpPortPublicationReservation { - authority: self.clone(), - session_id, - claim: TcpPortPublicationClaim { - publication_id, - generation, - }, - })) + fn mapping_for_guest_port(&self, guest_port: u16) -> Option { + self.broker_ports + .get(&guest_port) + .copied() + .map(|broker_port| TcpPortMapping { + broker_port, + guest_port, + }) } -} -struct TcpPortPublicationReservation { - authority: TcpPortPublicationAuthority, - session_id: SessionId, - claim: TcpPortPublicationClaim, -} - -impl Drop for TcpPortPublicationReservation { - fn drop(&mut self) { - let mut state = self.authority.state.lock(); - if let Some(entry) = state.get_mut(self.claim.publication_id.index()) - && entry.claimed_by == Some((self.session_id, self.claim.generation)) - { - entry.claimed_by = None; - } + fn contains_guest_port(&self, guest_port: u16) -> bool { + self.broker_ports.contains_key(&guest_port) } } #[derive(Default)] -struct SessionSocketPortState { - tcp: HashMap, - next_tcp_ephemeral: Option, +struct BrokerSocketPortState { + guest_tcp_ports: HashSet, + next_guest_tcp_ephemeral: Option, } -/// Per-session authority for the guest-visible TCP port namespace. +/// Broker-wide authority for the guest-visible TCP port namespace. /// -/// Guest TCP ports are allocated by the broker and are independent of the host -/// ports its socket backend uses, so two sessions may bind the same guest port. +/// Sessions identify endpoint owners but do not define separate network +/// namespaces. Guest TCP ports remain independent of backend host ports. #[derive(Clone, Default)] -pub(crate) struct SessionSocketPorts { - state: Arc>, +pub(crate) struct BrokerSocketPorts { + state: Arc>, } -impl SessionSocketPorts { +impl BrokerSocketPorts { /// Reserves one guest TCP port for `requested_address`. /// /// A zero requested port is allocated from the ephemeral range, skipping - /// ports reserved for explicit TCP publication. The + /// ports reserved for explicit TCP mappings. The /// reservation is released when the returned guard is dropped. fn reserve( &self, @@ -201,17 +124,17 @@ impl SessionSocketPorts { let mut state = self.state.lock(); let port = if requested_address.port() == 0 { state.allocate_ephemeral(&mut port_is_reserved)? - } else if state.tcp.contains_key(&requested_address.port()) { + } else if state.guest_tcp_ports.contains(&requested_address.port()) { return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); } else { requested_address.port() }; let local_address = SocketAddrV4::new(*requested_address.ip(), port); state - .tcp + .guest_tcp_ports .try_reserve(1) .map_err(|_| BrokerError::OutOfMemory)?; - if state.tcp.insert(port, local_address).is_some() { + if !state.guest_tcp_ports.insert(port) { return Err(BrokerError::Internal); } drop(state); @@ -225,16 +148,18 @@ impl SessionSocketPorts { } } -impl SessionSocketPortState { +impl BrokerSocketPortState { fn allocate_ephemeral( &mut self, port_is_reserved: &mut impl FnMut(u16) -> bool, ) -> Result { - let start = self.next_tcp_ephemeral.unwrap_or(FIRST_EPHEMERAL_PORT); + let start = self + .next_guest_tcp_ephemeral + .unwrap_or(FIRST_EPHEMERAL_PORT); let mut port = start; loop { - if !self.tcp.contains_key(&port) && !port_is_reserved(port) { - self.next_tcp_ephemeral = Some(if port == u16::MAX { + if !self.guest_tcp_ports.contains(&port) && !port_is_reserved(port) { + self.next_guest_tcp_ephemeral = Some(if port == u16::MAX { FIRST_EPHEMERAL_PORT } else { port + 1 @@ -253,15 +178,15 @@ impl SessionSocketPortState { } } -/// Guard releasing one guest TCP port back to its session namespace. +/// Guard releasing one guest TCP port back to the broker-wide namespace. struct GuestPortReservation { - ports: SessionSocketPorts, + ports: BrokerSocketPorts, port: u16, } impl Drop for GuestPortReservation { fn drop(&mut self) { - self.ports.state.lock().tcp.remove(&self.port); + self.ports.state.lock().guest_tcp_ports.remove(&self.port); } } @@ -318,17 +243,18 @@ pub struct PlatformDatagramReceive { /// bookkeeping shared across the sockets of a broker session. Operations on an /// individual socket belong to [`PlatformSocket`], not this shared provider. pub trait SocketProvider: Send + Sync { - /// Returns the portable TCP publications realized by this provider. + /// Returns broker startup overrides from guest TCP ports to broker ports. /// - /// The slice and its order must remain stable for the provider's lifetime; - /// broker-issued publication IDs index this declaration list. - fn tcp_port_publications(&self) -> &[TcpPortPublication] { + /// BrokerCore copies and validates this configuration during construction. + /// Active listeners and realized platform endpoints are tracked elsewhere. + fn tcp_port_mappings(&self) -> &[TcpPortMapping] { &[] } /// Creates one nonblocking socket resource for an authenticated session. /// - /// The returned socket must not retain authority beyond its `Arc` lifetime. + /// Any provider-retained clones must become inert when + /// [`PlatformSocket::retire`] is called. fn create( &self, session_id: SessionId, @@ -343,8 +269,8 @@ pub trait SocketProvider: Send + Sync { /// One nonblocking socket resource created by [`SocketProvider`]. /// /// The broker retains this resource in an `Arc`, allowing an operation already -/// in flight to finish after its object handle closes. Dropping the final `Arc` -/// releases the platform socket. +/// in flight to finish after its object handle closes. Before releasing its +/// portable authority, core explicitly retires the platform socket. pub trait PlatformSocket: Send + Sync { /// Binds this socket to a local address and echoes the assigned address. /// @@ -356,15 +282,14 @@ pub trait PlatformSocket: Send + Sync { /// Makes this socket listen for incoming connections. /// - /// The returned address is the socket's guest-local address. `publication` - /// authorizes realizing exactly one configured external endpoint for this - /// listener generation. The platform must retain and compare the complete - /// claim through rollback and teardown so stale work cannot detach a newer - /// generation. `None` requests an ordinary private listener. + /// The returned address is the socket's guest-local address. `mapping` + /// identifies the broker-facing endpoint to realize. `None` requests an + /// ordinary private listener. The platform validates that the mapping's + /// guest port matches this socket's guest-local port. fn listen( &self, backlog: u32, - publication: Option, + mapping: Option, ) -> Result>; /// Accepts one pending connection without waiting. @@ -442,6 +367,9 @@ pub trait PlatformSocket: Send + Sync { /// `Connected` and may change between those states through peer updates. fn status(&self) -> Result; + /// Synchronously and idempotently ends this socket's platform authority. + fn retire(&self); + /// Returns the current readiness snapshot. fn readiness(&self) -> ReadinessFlags; } @@ -484,7 +412,6 @@ pub fn create( readiness, _quota: quota, port_reservation: Mutex::new(None), - tcp_port_publication_reservation: Mutex::new(None), }); let platform_socket = match session.core.socket_provider.create( session.session_id, @@ -616,7 +543,7 @@ pub fn connect( Ok(SocketOutcome::Completed(status)) } -/// Binds a socket to an authorized loopback address. +/// Binds a socket to an authorized guest-local address. pub fn bind( session: &BrokerSession, handle: ObjectHandle, @@ -646,11 +573,11 @@ pub fn bind( ) { Ok(()) => {} Err(BrokerError::PolicyDenied) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); return Ok(SocketOutcome::Failed(SocketError::PolicyDenied)); } Err(error) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); return Err(error); } } @@ -658,15 +585,15 @@ pub fn bind( // Datagram sockets remain backed directly by a host endpoint. return match resource.bind(address) { Ok(SocketOutcome::Completed(local_address)) => { - finish_configuration(&object, Some(local_address), None, None, false); + finish_configuration(&object, Some(local_address), None, false); Ok(SocketOutcome::Completed(local_address)) } Ok(SocketOutcome::Failed(error)) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); Ok(SocketOutcome::Failed(error)) } Err(error) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); Err(error) } }; @@ -674,36 +601,22 @@ pub fn bind( let binding = match reserve_and_bind(session, create_request, &resource, address) { Ok(binding) => binding, Err(error) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); return Err(error); } }; let (local_address, reservation) = match binding { SocketOutcome::Completed(binding) => binding, SocketOutcome::Failed(error) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); return Ok(SocketOutcome::Failed(error)); } }; - let publication_id = (address.port() != 0) - .then(|| { - session - .core - .tcp_port_publications - .publication_id(create_request, local_address.port()) - }) - .flatten(); - finish_configuration( - &object, - Some(local_address), - Some(reservation), - publication_id, - false, - ); + finish_configuration(&object, Some(local_address), Some(reservation), false); Ok(SocketOutcome::Completed(local_address)) } -/// Makes a socket listen for incoming loopback connections. +/// Makes a socket listen for incoming connections. pub fn listen( session: &BrokerSession, handle: ObjectHandle, @@ -713,7 +626,7 @@ pub fn listen( return Err(BrokerError::UnsupportedOperation); } let object = session.authorized_object(handle, ObjectRights::WRITE)?; - let (resource, create_request, existing_local_address, publication_id, was_listening) = { + let (resource, create_request, existing_local_address) = { let mut object = object.write(); let ObjectEntry::Socket(socket) = &mut *object else { return Err(BrokerError::InvalidRights); @@ -732,8 +645,6 @@ pub fn listen( Arc::clone(&socket.resource), socket.create_request, socket.local_address, - socket.tcp_port_publication_id, - socket.listening, ) }; @@ -747,11 +658,11 @@ pub fn listen( ) { Ok(()) => {} Err(BrokerError::PolicyDenied) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); return Ok(SocketOutcome::Failed(SocketError::PolicyDenied)); } Err(error) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); return Err(error); } } @@ -763,7 +674,7 @@ pub fn listen( ) { Ok(binding) => binding, Err(error) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); return Err(error); } }; @@ -773,60 +684,43 @@ pub fn listen( port_reservation = Some(reservation); } SocketOutcome::Failed(error) => { - finish_configuration(&object, None, None, None, false); + finish_configuration(&object, None, None, false); return Ok(SocketOutcome::Failed(error)); } } } - let existing_claim = resource.tcp_port_publication_claim(); - if existing_claim.is_some() != (was_listening && publication_id.is_some()) { - finish_configuration(&object, local_address, port_reservation, None, false); - return Err(BrokerError::Internal); - } - let mut new_publication_reservation = None; - let publication_claim = if let Some(claim) = existing_claim { - Some(claim) - } else if let Some(publication_id) = publication_id { - match session - .core - .tcp_port_publications - .claim(session.session_id, publication_id)? - { - SocketOutcome::Completed(reservation) => { - let claim = reservation.claim; - new_publication_reservation = Some(reservation); - Some(claim) - } - SocketOutcome::Failed(error) => { - finish_configuration(&object, local_address, port_reservation, None, false); - return Ok(SocketOutcome::Failed(error)); - } - } - } else { - None - }; + let mapping = local_address + .filter(|address| address.ip().is_unspecified()) + .map(|address| { + session + .core + .tcp_port_mapping_config + .mapping_for_guest_port(address.port()) + .unwrap_or(TcpPortMapping { + broker_port: address.port(), + guest_port: address.port(), + }) + }); - match resource.listen(backlog, publication_claim) { + match resource.listen(backlog, mapping) { Ok(SocketOutcome::Completed(address)) => { // The guest-local address is broker-authoritative, so a platform // that reports a different one is not trustworthy. if local_address != Some(address) { - finish_configuration(&object, local_address, port_reservation, None, false); + resource.retire(); + finish_retired_configuration(&object, local_address, port_reservation); return Err(BrokerError::Internal); } - if let Some(reservation) = new_publication_reservation { - resource.set_tcp_port_publication_reservation(reservation); - } - finish_configuration(&object, local_address, port_reservation, None, true); + finish_configuration(&object, local_address, port_reservation, true); Ok(SocketOutcome::Completed(address)) } Ok(SocketOutcome::Failed(error)) => { - finish_configuration(&object, local_address, port_reservation, None, false); + finish_configuration(&object, local_address, port_reservation, false); Ok(SocketOutcome::Failed(error)) } Err(error) => { - finish_configuration(&object, local_address, port_reservation, None, false); + finish_configuration(&object, local_address, port_reservation, false); Err(error) } } @@ -872,7 +766,6 @@ pub fn accept( readiness, _quota: quota, port_reservation: Mutex::new(None), - tcp_port_publication_reservation: Mutex::new(None), }); let accepted = match listener_resource.accept(resource.readiness.clone()) { Ok(SocketOutcome::Completed(accepted)) => accepted, @@ -1099,14 +992,6 @@ pub fn shutdown( ) }; let outcome = resource.shutdown(mode); - if shuts_down_listener - && matches!( - &outcome, - Ok(SocketOutcome::Completed(()) | SocketOutcome::Failed(SocketError::NotConnected)) - ) - { - resource.release_tcp_port_publication(); - } if serializes_configuration { let mut object = object.write(); if let ObjectEntry::Socket(socket) = &mut *object { @@ -1277,13 +1162,13 @@ fn reserve_and_bind( ) -> Result> { let (local_address, reservation) = match session + .core .socket_ports .reserve(create_request, requested_address, |port| { session .core - .tcp_port_publications - .publication_id(create_request, port) - .is_some() + .tcp_port_mapping_config + .contains_guest_port(port) })? { SocketOutcome::Completed(binding) => binding, SocketOutcome::Failed(error) => return Ok(SocketOutcome::Failed(error)), @@ -1388,7 +1273,6 @@ fn finish_configuration( object: &spin::RwLock, local_address: Option, port_reservation: Option, - tcp_port_publication_id: Option, listening: bool, ) { let mut object = object.write(); @@ -1398,11 +1282,27 @@ fn finish_configuration( if let Some(port_reservation) = port_reservation { socket.resource.set_port_reservation(port_reservation); } - socket.tcp_port_publication_id = socket.tcp_port_publication_id.or(tcp_port_publication_id); socket.listening |= listening; } } +fn finish_retired_configuration( + object: &spin::RwLock, + local_address: Option, + port_reservation: Option, +) { + let mut object = object.write(); + if let ObjectEntry::Socket(socket) = &mut *object { + socket.configuration_in_flight = false; + socket.local_address = socket.local_address.or(local_address); + if let Some(port_reservation) = port_reservation { + socket.resource.set_port_reservation(port_reservation); + } + socket.listening = false; + socket.connection_status = SocketConnectionStatus::Failed(SocketError::Other); + } +} + pub(crate) struct SocketObject { resource: Arc, create_request: CreateSocketRequest, @@ -1411,7 +1311,6 @@ pub(crate) struct SocketObject { connect_in_flight: bool, configuration_in_flight: bool, listening: bool, - tcp_port_publication_id: Option, datagram_connect_generation: u64, } @@ -1425,7 +1324,6 @@ impl SocketObject { connect_in_flight: false, configuration_in_flight: false, listening: false, - tcp_port_publication_id: None, datagram_connect_generation: 0, } } @@ -1443,7 +1341,6 @@ impl SocketObject { connect_in_flight: false, configuration_in_flight: false, listening: false, - tcp_port_publication_id: None, datagram_connect_generation: 0, } } @@ -1458,7 +1355,6 @@ pub(crate) struct SocketResource { readiness: ReadinessRegistration, _quota: Arc, port_reservation: Mutex>, - tcp_port_publication_reservation: Mutex>, } impl SocketResource { @@ -1474,25 +1370,6 @@ impl SocketResource { } } - fn set_tcp_port_publication_reservation(&self, reservation: TcpPortPublicationReservation) { - let mut slot = self.tcp_port_publication_reservation.lock(); - debug_assert!(slot.is_none()); - if slot.is_none() { - *slot = Some(reservation); - } - } - - fn tcp_port_publication_claim(&self) -> Option { - self.tcp_port_publication_reservation - .lock() - .as_ref() - .map(|reservation| reservation.claim) - } - - fn release_tcp_port_publication(&self) { - self.tcp_port_publication_reservation.lock().take(); - } - fn platform_socket(&self) -> &dyn PlatformSocket { self.platform_socket .get() @@ -1514,9 +1391,13 @@ impl SocketResource { fn listen( &self, backlog: u32, - publication: Option, + mapping: Option, ) -> Result> { - self.platform_socket().listen(backlog, publication) + self.platform_socket().listen(backlog, mapping) + } + + fn retire(&self) { + self.platform_socket().retire(); } fn accept( @@ -1595,6 +1476,9 @@ const fn is_udp(request: CreateSocketRequest) -> bool { impl Drop for SocketResource { fn drop(&mut self) { + if let Some(platform_socket) = self.platform_socket.get() { + platform_socket.retire(); + } self.readiness.retire(); } } @@ -1658,56 +1542,50 @@ pub(crate) mod tests { #[derive(Clone)] pub(crate) struct TestSocketProvider { state: Arc, - tcp_port_publications: Arc>, + tcp_port_mappings: Arc>, } impl Default for TestSocketProvider { fn default() -> Self { Self { state: Arc::default(), - tcp_port_publications: Arc::new(vec![TcpPortPublication { + tcp_port_mappings: Arc::new(vec![TcpPortMapping { + broker_port: 8080, guest_port: 80, - external_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080), }]), } } } #[test] - fn guest_tcp_port_namespaces_are_per_session() { - let first_session = SessionSocketPorts::default(); - let second_session = SessionSocketPorts::default(); + fn guest_tcp_port_namespace_is_broker_wide() { + let ports = BrokerSocketPorts::default(); let address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); - let SocketOutcome::Completed((_, first_reservation)) = first_session - .reserve(create_request(), address, |_| false) - .unwrap() + let SocketOutcome::Completed((_, first_reservation)) = + ports.reserve(create_request(), address, |_| false).unwrap() else { panic!("first TCP reservation failed"); }; assert!(matches!( - first_session.reserve(create_request(), address, |_| false), + ports.reserve(create_request(), address, |_| false), Ok(SocketOutcome::Failed(SocketError::AddressInUse)) )); assert!(matches!( - second_session.reserve(create_request(), address, |_| false), - Ok(SocketOutcome::Completed(_)) - )); - assert!(matches!( - first_session.reserve(create_udp_request(), address, |_| false), + ports.reserve(create_udp_request(), address, |_| false), Err(BrokerError::Internal) )); drop(first_reservation); assert!(matches!( - first_session.reserve(create_request(), address, |_| false), + ports.reserve(create_request(), address, |_| false), Ok(SocketOutcome::Completed(_)) )); } #[test] fn implicit_guest_port_allocation_skips_provider_reservations() { - let ports = SessionSocketPorts::default(); + let ports = BrokerSocketPorts::default(); let requested_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); let SocketOutcome::Completed((address, _reservation)) = ports .reserve(create_request(), requested_address, |port| { @@ -1722,42 +1600,42 @@ pub(crate) mod tests { } #[test] - fn tcp_port_publication_configuration_is_bounded_and_unique() { - let publication = TcpPortPublication { + fn tcp_port_mapping_configuration_is_bounded_and_unique() { + let mapping = TcpPortMapping { + broker_port: 8080, guest_port: 80, - external_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080), }; - assert!(TcpPortPublicationAuthority::new(&[publication]).is_ok()); + assert!(TcpPortMappingConfig::new(&[mapping]).is_ok()); assert!(matches!( - TcpPortPublicationAuthority::new(&[TcpPortPublication { + TcpPortMappingConfig::new(&[TcpPortMapping { guest_port: 0, - ..publication + ..mapping }]), Err(BrokerError::Internal) )); assert!(matches!( - TcpPortPublicationAuthority::new(&[TcpPortPublication { - external_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0), - ..publication + TcpPortMappingConfig::new(&[TcpPortMapping { + broker_port: 0, + ..mapping }]), Err(BrokerError::Internal) )); assert!(matches!( - TcpPortPublicationAuthority::new(&[ - publication, - TcpPortPublication { - external_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8081), - ..publication + TcpPortMappingConfig::new(&[ + mapping, + TcpPortMapping { + broker_port: 8081, + ..mapping }, ]), Err(BrokerError::Internal) )); assert!(matches!( - TcpPortPublicationAuthority::new(&[ - publication, - TcpPortPublication { + TcpPortMappingConfig::new(&[ + mapping, + TcpPortMapping { guest_port: 81, - ..publication + ..mapping }, ]), Err(BrokerError::Internal) @@ -1775,12 +1653,16 @@ pub(crate) mod tests { status_block: StdMutex, mpsc::Receiver<()>)>>, binds: StdMutex>, listens: StdMutex>, - listen_publications: StdMutex>>, + listen_mappings: StdMutex>>, listen_block: StdMutex, mpsc::Receiver<()>)>>, fail_listen: core::sync::atomic::AtomicBool, error_listen: core::sync::atomic::AtomicBool, + invalid_listen_address: core::sync::atomic::AtomicBool, shutdown_calls: AtomicUsize, + retired_sockets: AtomicUsize, dropped_sockets: AtomicUsize, + retained_platform_sockets: StdMutex>>, + retain_next_socket: core::sync::atomic::AtomicBool, fail_create: core::sync::atomic::AtomicBool, fail_connect: core::sync::atomic::AtomicBool, fail_connect_indeterminate: core::sync::atomic::AtomicBool, @@ -1816,11 +1698,21 @@ pub(crate) mod tests { fn error_next_listen(&self) { self.state.error_listen.store(true, Ordering::Relaxed); } + + fn invalidate_next_listen_address(&self) { + self.state + .invalid_listen_address + .store(true, Ordering::Relaxed); + } + + fn retain_next_socket(&self) { + self.state.retain_next_socket.store(true, Ordering::Relaxed); + } } impl SocketProvider for TestSocketProvider { - fn tcp_port_publications(&self) -> &[TcpPortPublication] { - &self.tcp_port_publications + fn tcp_port_mappings(&self) -> &[TcpPortMapping] { + &self.tcp_port_mappings } fn create( @@ -1839,13 +1731,22 @@ pub(crate) mod tests { return Err(BrokerError::OutOfMemory); } *self.state.live_readiness.lock().unwrap() = Some(readiness.clone()); - Ok(Arc::new(TestPlatformSocket { + let socket = Arc::new(TestPlatformSocket { state: Arc::clone(&self.state), readiness, create_request: request, tcp_options: StdMutex::new(TestTcpOptions::default()), guest_local_address: StdMutex::new(None), - })) + active: core::sync::atomic::AtomicBool::new(true), + }); + if self.state.retain_next_socket.swap(false, Ordering::Relaxed) { + self.state + .retained_platform_sockets + .lock() + .unwrap() + .push(Arc::clone(&socket)); + } + Ok(socket) } fn close_session(&self, session_id: SessionId) { @@ -1859,6 +1760,7 @@ pub(crate) mod tests { create_request: CreateSocketRequest, tcp_options: StdMutex, guest_local_address: StdMutex>, + active: core::sync::atomic::AtomicBool, } #[derive(Default)] @@ -1885,14 +1787,10 @@ pub(crate) mod tests { fn listen( &self, backlog: u32, - publication: Option, + mapping: Option, ) -> Result> { self.state.listens.lock().unwrap().push(backlog); - self.state - .listen_publications - .lock() - .unwrap() - .push(publication); + self.state.listen_mappings.lock().unwrap().push(mapping); let listen_block = self.state.listen_block.lock().unwrap().take(); if let Some((started, release)) = listen_block { started.send(()).unwrap(); @@ -1909,6 +1807,16 @@ pub(crate) mod tests { .lock() .unwrap() .ok_or(BrokerError::Internal)?; + if self + .state + .invalid_listen_address + .swap(false, Ordering::Relaxed) + { + return Ok(SocketOutcome::Completed(SocketAddrV4::new( + *local_address.ip(), + local_address.port().wrapping_add(1), + ))); + } Ok(SocketOutcome::Completed(local_address)) } @@ -2036,6 +1944,12 @@ pub(crate) mod tests { Ok(response) } + fn retire(&self) { + if self.active.swap(false, Ordering::AcqRel) { + self.state.retired_sockets.fetch_add(1, Ordering::Relaxed); + } + } + fn readiness(&self) -> ReadinessFlags { ReadinessFlags::READ | ReadinessFlags::WRITE } @@ -2043,6 +1957,7 @@ pub(crate) mod tests { impl Drop for TestPlatformSocket { fn drop(&mut self) { + self.retire(); self.state.dropped_sockets.fetch_add(1, Ordering::Relaxed); } } @@ -2054,7 +1969,9 @@ pub(crate) mod tests { check_udp_socket_operations(broker, provider); check_concurrent_udp_status_does_not_regress_connection(broker, provider); check_server_socket_operations(broker, provider); - check_tcp_port_publication_lifecycle(broker, provider); + check_tcp_port_mapping_lifecycle(broker, provider); + check_tcp_port_mapping_retirement(broker, provider); + check_invalid_listen_address_retires_mapping(broker, provider); check_failed_listener_shutdown_preserves_state(broker, provider); check_listener_shutdown_does_not_race_listen(broker, provider); check_connect_errors_classify_peer_state(broker, provider); @@ -2126,6 +2043,13 @@ pub(crate) mod tests { connect(&session, handle, loopback_address()), Ok(SocketOutcome::Completed(SocketConnectionStatus::Connecting)) ); + let local_address = *provider + .state + .binds + .lock() + .unwrap() + .last() + .expect("automatic TCP bind was not recorded"); assert_eq!( readiness.published.lock().unwrap().as_slice(), [(handle, ReadinessFlags::WRITE)] @@ -2134,7 +2058,7 @@ pub(crate) mod tests { status(&session, handle), Ok(SocketStatusResponse { status: SocketConnectionStatus::Connected, - local_address: Some(SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_EPHEMERAL_PORT)), + local_address: Some(local_address), pending_error: None, }) ); @@ -2406,11 +2330,13 @@ pub(crate) mod tests { assert_eq!(provider.state.binds.lock().unwrap().len(), binds_before); let requested_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); - let local_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 49152); - assert_eq!( - bind(&session, listener, requested_address), - Ok(SocketOutcome::Completed(local_address)) - ); + let SocketOutcome::Completed(local_address) = + bind(&session, listener, requested_address).unwrap() + else { + panic!("guest TCP bind failed"); + }; + assert!(local_address.ip().is_loopback()); + assert_ne!(local_address.port(), 0); assert_eq!( status(&session, listener), Ok(SocketStatusResponse { @@ -2423,6 +2349,10 @@ pub(crate) mod tests { listen(&session, listener, 128), Ok(SocketOutcome::Completed(local_address)) ); + assert_eq!( + provider.state.listen_mappings.lock().unwrap().last(), + Some(&None) + ); assert_eq!( listen(&session, listener, MAX_TCP_LISTEN_BACKLOG + 1), Err(BrokerError::UnsupportedOperation) @@ -2468,19 +2398,27 @@ pub(crate) mod tests { assert_eq!(broker.reserved_sockets.load(Ordering::Relaxed), 0); let auto_bound = create(&session, create_request(), readiness).unwrap(); - let auto_bound_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_EPHEMERAL_PORT + 1); - assert_eq!( - listen(&session, auto_bound, 0), - Ok(SocketOutcome::Completed(auto_bound_address)) - ); + let SocketOutcome::Completed(auto_bound_address) = listen(&session, auto_bound, 0).unwrap() + else { + panic!("automatic TCP listen failed"); + }; + assert!(auto_bound_address.ip().is_unspecified()); + assert_ne!(auto_bound_address.port(), 0); assert_eq!( provider.state.binds.lock().unwrap().last(), Some(&auto_bound_address) ); + assert_eq!( + provider.state.listen_mappings.lock().unwrap().last(), + Some(&Some(TcpPortMapping { + broker_port: auto_bound_address.port(), + guest_port: auto_bound_address.port(), + })) + ); session.close_object_reference(auto_bound).unwrap(); } - fn check_tcp_port_publication_lifecycle(broker: &BrokerCore, provider: &TestSocketProvider) { + fn check_tcp_port_mapping_lifecycle(broker: &BrokerCore, provider: &TestSocketProvider) { let first_session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); @@ -2488,7 +2426,7 @@ pub(crate) mod tests { .create_session(CallerCredential::Unauthenticated) .unwrap(); let readiness = Arc::new(TestReadinessSink::default()); - let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let guest_address = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 80); let first = create(&first_session, create_request(), readiness.clone()).unwrap(); let second = create(&second_session, create_request(), readiness).unwrap(); assert_eq!( @@ -2497,56 +2435,47 @@ pub(crate) mod tests { ); assert_eq!( bind(&second_session, second, guest_address), - Ok(SocketOutcome::Completed(guest_address)) + Ok(SocketOutcome::Failed(SocketError::AddressInUse)) ); - let claims_before = provider.state.listen_publications.lock().unwrap().len(); + let mappings_before = provider.state.listen_mappings.lock().unwrap().len(); provider.fail_next_listen(); assert_eq!( listen(&first_session, first, 1), Ok(SocketOutcome::Failed(SocketError::AddressInUse)) ); assert_eq!( - listen(&second_session, second, 1), - Ok(SocketOutcome::Completed(guest_address)) - ); - let claims = provider.state.listen_publications.lock().unwrap(); - let first_claim = claims[claims_before].expect("published listen must carry a claim"); - let second_claim = claims[claims_before + 1].expect("published listen must carry a claim"); - assert_eq!(first_claim.publication_id(), second_claim.publication_id()); - assert_eq!(second_claim.generation(), first_claim.generation() + 1); - drop(claims); - - let calls_before_conflict = provider.state.listens.lock().unwrap().len(); - assert_eq!( - listen(&first_session, first, 1), + bind(&second_session, second, guest_address), Ok(SocketOutcome::Failed(SocketError::AddressInUse)) ); assert_eq!( - provider.state.listens.lock().unwrap().len(), - calls_before_conflict + listen(&first_session, first, 1), + Ok(SocketOutcome::Completed(guest_address)) ); + let mappings = provider.state.listen_mappings.lock().unwrap(); + let mapping = provider.tcp_port_mappings[0]; + assert_eq!(mappings[mappings_before], Some(mapping)); + assert_eq!(mappings[mappings_before + 1], Some(mapping)); + drop(mappings); assert_eq!( - shutdown(&second_session, second, ShutdownMode::StopListening), + shutdown(&first_session, first, ShutdownMode::StopListening), Ok(SocketOutcome::Completed(())) ); - second_session.close_object_reference(second).unwrap(); + first_session.close_object_reference(first).unwrap(); assert_eq!( - listen(&first_session, first, 1), + bind(&second_session, second, guest_address), Ok(SocketOutcome::Completed(guest_address)) ); - let third_claim = provider - .state - .listen_publications - .lock() - .unwrap() - .last() - .copied() - .flatten() - .unwrap(); - assert_eq!(third_claim.generation(), second_claim.generation() + 1); - first_session.close_object_reference(first).unwrap(); + assert_eq!( + listen(&second_session, second, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + provider.state.listen_mappings.lock().unwrap().last(), + Some(&Some(mapping)) + ); + second_session.close_object_reference(second).unwrap(); let second = create( &second_session, @@ -2564,16 +2493,7 @@ pub(crate) mod tests { listen(&second_session, second, 1), Err(BrokerError::Internal) ); - let failed_claim = provider - .state - .listen_publications - .lock() - .unwrap() - .last() - .copied() - .flatten() - .unwrap(); - assert_eq!(failed_claim.generation(), third_claim.generation() + 1); + second_session.close_object_reference(second).unwrap(); let first = create( &first_session, @@ -2589,35 +2509,111 @@ pub(crate) mod tests { listen(&first_session, first, 1), Ok(SocketOutcome::Completed(guest_address)) ); - let dropped_claim = provider - .state - .listen_publications - .lock() - .unwrap() - .last() - .copied() - .flatten() + first_session.close_object_reference(first).unwrap(); + } + + fn check_tcp_port_mapping_retirement(broker: &BrokerCore, provider: &TestSocketProvider) { + let first_session = broker + .create_session(CallerCredential::Unauthenticated) .unwrap(); - assert_eq!(dropped_claim.generation(), failed_claim.generation() + 1); + let second_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let guest_address = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 80); + let retired_before = provider.state.retired_sockets.load(Ordering::Relaxed); + let dropped_before = provider.state.dropped_sockets.load(Ordering::Relaxed); + + provider.retain_next_socket(); + let first = create( + &first_session, + create_request(), + Arc::new(TestReadinessSink::default()), + ) + .unwrap(); + assert_eq!( + bind(&first_session, first, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + listen(&first_session, first, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); first_session.close_object_reference(first).unwrap(); + assert_eq!( + provider.state.retired_sockets.load(Ordering::Relaxed), + retired_before + 1 + ); + assert_eq!( + provider.state.dropped_sockets.load(Ordering::Relaxed), + dropped_before + ); + let second = create( + &second_session, + create_request(), + Arc::new(TestReadinessSink::default()), + ) + .unwrap(); + assert_eq!( + bind(&second_session, second, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); assert_eq!( listen(&second_session, second, 1), Ok(SocketOutcome::Completed(guest_address)) ); - let final_claim = provider + second_session.close_object_reference(second).unwrap(); + provider .state - .listen_publications + .retained_platform_sockets .lock() .unwrap() - .last() - .copied() - .flatten() + .clear(); + } + + fn check_invalid_listen_address_retires_mapping( + broker: &BrokerCore, + provider: &TestSocketProvider, + ) { + let first_session = broker + .create_session(CallerCredential::Unauthenticated) .unwrap(); - assert_eq!(final_claim.generation(), dropped_claim.generation() + 1); + let second_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let guest_address = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 80); + let retired_before = provider.state.retired_sockets.load(Ordering::Relaxed); + let first = create( + &first_session, + create_request(), + Arc::new(TestReadinessSink::default()), + ) + .unwrap(); assert_eq!( - shutdown(&second_session, second, ShutdownMode::StopListening), - Ok(SocketOutcome::Completed(())) + bind(&first_session, first, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + provider.invalidate_next_listen_address(); + assert_eq!(listen(&first_session, first, 1), Err(BrokerError::Internal)); + assert_eq!( + provider.state.retired_sockets.load(Ordering::Relaxed), + retired_before + 1 + ); + first_session.close_object_reference(first).unwrap(); + + let second = create( + &second_session, + create_request(), + Arc::new(TestReadinessSink::default()), + ) + .unwrap(); + assert_eq!( + bind(&second_session, second, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + listen(&second_session, second, 1), + Ok(SocketOutcome::Completed(guest_address)) ); second_session.close_object_reference(second).unwrap(); } @@ -2941,6 +2937,13 @@ pub(crate) mod tests { connect(&session, poisoned, loopback_address()), Err(BrokerError::Internal) ); + let poisoned_local_address = *provider + .state + .binds + .lock() + .unwrap() + .last() + .expect("automatic TCP bind was not recorded"); assert_eq!( connect(&session, poisoned, loopback_address()), Ok(SocketOutcome::Completed(SocketConnectionStatus::Failed( @@ -2951,10 +2954,7 @@ pub(crate) mod tests { status(&session, poisoned), Ok(SocketStatusResponse { status: SocketConnectionStatus::Failed(SocketError::Other), - local_address: Some(SocketAddrV4::new( - Ipv4Addr::LOCALHOST, - FIRST_EPHEMERAL_PORT + 1, - )), + local_address: Some(poisoned_local_address), pending_error: None, }) ); @@ -2984,7 +2984,13 @@ pub(crate) mod tests { Ok(SocketOutcome::Completed(SocketConnectionStatus::Connecting)) ); - let local_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 49152); + let local_address = *provider + .state + .binds + .lock() + .unwrap() + .last() + .expect("automatic TCP bind was not recorded"); *provider.state.status_responses.lock().unwrap() = std::collections::VecDeque::from([ SocketStatusResponse { status: SocketConnectionStatus::Connecting, @@ -3041,6 +3047,13 @@ pub(crate) mod tests { connect(&session, handle, loopback_address()), Ok(SocketOutcome::Completed(SocketConnectionStatus::Connecting)) ); + let guest_local_address = *provider + .state + .binds + .lock() + .unwrap() + .last() + .expect("automatic TCP bind was not recorded"); let platform_local_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 49153); provider @@ -3056,7 +3069,6 @@ pub(crate) mod tests { // The guest-local address reserved by the broker outranks any address // the platform reports for its private host endpoint. - let guest_local_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_EPHEMERAL_PORT); let expected = SocketStatusResponse { status: SocketConnectionStatus::Failed(SocketError::TimedOut), local_address: Some(guest_local_address), diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index a1e5368f3..8ea37931d 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -796,7 +796,7 @@ mod tests { fn listen( &self, _backlog: u32, - _publication: Option, + _mapping: Option, ) -> litebox_broker_core::Result> { let local_address = self .local_address @@ -897,6 +897,8 @@ mod tests { }) } + fn retire(&self) {} + fn readiness(&self) -> litebox_broker_protocol::readiness::ReadinessFlags { litebox_broker_protocol::readiness::ReadinessFlags::READ | litebox_broker_protocol::readiness::ReadinessFlags::WRITE @@ -1369,7 +1371,7 @@ mod tests { ), BrokerResult::Socket(SocketResponse::Status(SocketStatusResponse { status: SocketConnectionStatus::Connected, - local_address: Some(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 49152)), + local_address: Some(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 49152)), pending_error: None, })) ); diff --git a/litebox_broker_platform_linux_userland/src/socket.rs b/litebox_broker_platform_linux_userland/src/socket.rs index 42defda54..96d35c5fc 100644 --- a/litebox_broker_platform_linux_userland/src/socket.rs +++ b/litebox_broker_platform_linux_userland/src/socket.rs @@ -17,7 +17,7 @@ use std::time::{Duration, Instant}; use litebox_broker_core::socket::{ AcceptedPlatformSocket, PlatformConnectError, PlatformDatagramReceive, PlatformSocket, - PlatformStreamReceive, SocketProvider, TcpPortPublication, TcpPortPublicationClaim, + PlatformStreamReceive, SocketProvider, TcpPortMapping, }; use litebox_broker_core::{BrokerError, Result as BrokerResult, SessionId}; use litebox_broker_protocol::readiness::ReadinessFlags; @@ -44,7 +44,7 @@ const WAKE_TOKEN: u64 = 0; const MAX_QUEUED_SOCKET_COMMANDS: usize = 64; const MAX_EPOLL_EVENTS: usize = 64; const MAX_STALE_PORT_MAPPING_CONNECTIONS: usize = MAX_TCP_LISTEN_BACKLOG as usize + 3; -const MAX_RETAINED_TRANSLATIONS: usize = 1 << 14; +const MAX_TRACKED_GUEST_CONNECTIONS: usize = 1 << 14; const PENDING_CONNECT_DISCARD_LIFETIME: Duration = Duration::from_mins(5); /// Linux-userland socket provider. @@ -54,15 +54,14 @@ const PENDING_CONNECT_DISCARD_LIFETIME: Duration = Duration::from_mins(5); /// immediate nonblocking operation, never for network readiness. pub struct LinuxSocketProvider { reactor: Arc, - tcp_port_publications: Vec, + tcp_port_mappings: Vec, } struct PortMappingState { - mapping: TcpPortPublication, + mapping: TcpPortMapping, reservation: Option, reservation_registered: bool, - claimed_by: Option<(u64, TcpPortPublicationClaim)>, - stale_connections: HashMap<(SocketAddrV4, SocketAddrV4), StaleTcpConnection>, + owned_by: Option, } struct StaleTcpConnection { @@ -74,46 +73,54 @@ struct StaleTcpConnection { impl LinuxSocketProvider { /// Starts a provider with matching global and per-session socket limits. pub fn new(max_sockets: usize, max_sockets_per_session: usize) -> IoResult { - Self::new_with_tcp_port_publications(max_sockets, max_sockets_per_session, &[]) + Self::new_with_tcp_port_mappings( + max_sockets, + max_sockets_per_session, + Ipv4Addr::UNSPECIFIED, + &[], + ) } - /// Starts a limited provider and realizes every TCP publication immediately. - pub fn new_with_tcp_port_publications( + /// Starts a limited provider with broker-wide TCP port mapping overrides. + pub fn new_with_tcp_port_mappings( max_sockets: usize, max_sockets_per_session: usize, - tcp_port_publications: &[TcpPortPublication], + broker_ipv4_address: Ipv4Addr, + tcp_port_mappings: &[TcpPortMapping], ) -> IoResult { - for (index, publication) in tcp_port_publications.iter().enumerate() { - if publication.guest_port == 0 || publication.external_address.port() == 0 { + for (index, mapping) in tcp_port_mappings.iter().enumerate() { + if mapping.guest_port == 0 || mapping.broker_port == 0 { return Err(Error::new( ErrorKind::InvalidInput, - "published guest and external ports must be nonzero", + "mapped broker and guest ports must be nonzero", )); } - if tcp_port_publications[..index].iter().any(|existing| { - existing.guest_port == publication.guest_port - || existing.external_address == publication.external_address + if tcp_port_mappings[..index].iter().any(|existing| { + existing.guest_port == mapping.guest_port + || existing.broker_port == mapping.broker_port }) { return Err(Error::new( ErrorKind::InvalidInput, - "published guest ports and external endpoints must be unique", + "mapped broker and guest ports must be unique", )); } } - let tcp_port_publications = tcp_port_publications.to_vec(); + let tcp_port_mappings = tcp_port_mappings.to_vec(); Ok(Self { reactor: Arc::new(ReactorClient::start( max_sockets, max_sockets_per_session, - tcp_port_publications.clone(), + broker_ipv4_address, + tcp_port_mappings.clone(), )?), - tcp_port_publications, + tcp_port_mappings, }) } } fn create_port_mapping_reservation( - mapping: TcpPortPublication, + broker_ipv4_address: Ipv4Addr, + mapping: TcpPortMapping, reuse_address: bool, reuse_port: bool, ) -> core::result::Result { @@ -129,24 +136,28 @@ fn create_port_mapping_reservation( if reuse_port { sockopt::set_socket_reuseport(&socket, true)?; } - bind(&socket, &mapping.external_address)?; + bind( + &socket, + &SocketAddrV4::new(broker_ipv4_address, mapping.broker_port), + )?; Ok(socket) } fn create_replacement_port_mapping_reservation( - mapping: TcpPortPublication, + broker_ipv4_address: Ipv4Addr, + mapping: TcpPortMapping, ) -> core::result::Result { - let socket = create_port_mapping_reservation(mapping, true, false)?; + let socket = create_port_mapping_reservation(broker_ipv4_address, mapping, true, false)?; // Reuse is needed only while replacing a listener that may still have // accepted children. Clear it afterward so the bound reservation is - // exclusive between publications. + // exclusive between mapped listeners. sockopt::set_socket_reuseaddr(&socket, false)?; Ok(socket) } impl SocketProvider for LinuxSocketProvider { - fn tcp_port_publications(&self) -> &[TcpPortPublication] { - &self.tcp_port_publications + fn tcp_port_mappings(&self) -> &[TcpPortMapping] { + &self.tcp_port_mappings } fn create( @@ -210,12 +221,12 @@ impl PlatformSocket for LinuxSocket { fn listen( &self, backlog: u32, - publication: Option, + mapping: Option, ) -> BrokerResult> { self.reactor.request(|response| ReactorCommand::Listen { id: self.id, backlog, - publication, + mapping, response, }) } @@ -368,6 +379,12 @@ impl PlatformSocket for LinuxSocket { }) } + fn retire(&self) { + if self.active.swap(false, Ordering::AcqRel) { + self.reactor.close_socket(self.id); + } + } + fn readiness(&self) -> ReadinessFlags { self.snapshot .lock() @@ -378,9 +395,7 @@ impl PlatformSocket for LinuxSocket { impl Drop for LinuxSocket { fn drop(&mut self) { - if self.active.swap(false, Ordering::AcqRel) { - self.reactor.close_socket(self.id); - } + self.retire(); } } @@ -396,24 +411,29 @@ impl ReactorClient { fn start( max_sockets: usize, max_sockets_per_session: usize, - tcp_port_publications: Vec, + broker_ipv4_address: Ipv4Addr, + tcp_port_mappings: Vec, ) -> IoResult { let epoll_fd = epoll::create(epoll::CreateFlags::CLOEXEC)?; let wake = Arc::new(eventfd(0, EventfdFlags::CLOEXEC | EventfdFlags::NONBLOCK)?); - let port_mappings = tcp_port_publications + let port_mappings = tcp_port_mappings .into_iter() - .map(|mapping| { - let mut stale_connections = HashMap::new(); - stale_connections.try_reserve(MAX_RETAINED_TRANSLATIONS)?; - Ok(PortMappingState { - mapping, - reservation: Some(create_port_mapping_reservation(mapping, false, false)?), - reservation_registered: false, - claimed_by: None, - stale_connections, - }) + .map(|mapping| PortMappingState { + mapping, + reservation: None, + reservation_registered: false, + owned_by: None, }) - .collect::>>()?; + .collect::>(); + let mut tcp = BrokerTcpState::default(); + tcp.stale_mapped_connections + .try_reserve(MAX_TRACKED_GUEST_CONNECTIONS) + .map_err(|_| { + Error::new( + ErrorKind::OutOfMemory, + "stale guest connection table allocation failed", + ) + })?; epoll::add( &epoll_fd, wake.as_ref(), @@ -439,8 +459,10 @@ impl ReactorClient { let mut reactor = Reactor { epoll: epoll_fd, wake: reactor_wake, + broker_ipv4_address, commands: receiver, sockets, + tcp, sessions: HashMap::new(), port_mappings, max_sockets, @@ -497,7 +519,9 @@ impl ReactorClient { Err(TrySendError::Full(_)) => return Err(BrokerError::ResourceExhausted), Err(TrySendError::Disconnected(_)) => return Err(BrokerError::Internal), } - self.signal().map_err(|_| BrokerError::Internal)?; + // Once queued, wait for acknowledgement even if the wake write fails: + // the reactor may still execute the command after another event. + let _ = self.signal(); receive.recv().map_err(|_| BrokerError::Internal)? } @@ -525,8 +549,9 @@ impl ReactorClient { )); } } - self.signal() - .map_err(|_| PlatformConnectError::PeerIndeterminate(BrokerError::Internal))?; + // A queued connect has indeterminate peer state until the reactor + // acknowledges it, regardless of whether this wake write succeeds. + let _ = self.signal(); receive .recv() .map_err(|_| PlatformConnectError::PeerIndeterminate(BrokerError::Internal))? @@ -562,20 +587,20 @@ impl ReactorClient { } #[cfg(test)] - fn tcp_translation_count(&self) -> usize { + fn pending_guest_connection_count(&self) -> usize { let (response, receive) = sync_channel(1); self.commands - .send(ReactorCommand::TcpTranslationCount { response }) + .send(ReactorCommand::PendingGuestConnectionCount { response }) .unwrap(); self.signal().unwrap(); receive.recv().unwrap() } #[cfg(test)] - fn stale_tcp_translation_count(&self) -> usize { + fn stale_guest_connection_count(&self) -> usize { let (response, receive) = sync_channel(1); self.commands - .send(ReactorCommand::StaleTcpTranslationCount { response }) + .send(ReactorCommand::StaleGuestConnectionCount { response }) .unwrap(); self.signal().unwrap(); receive.recv().unwrap() @@ -674,7 +699,7 @@ enum ReactorCommand { Listen { id: u64, backlog: u32, - publication: Option, + mapping: Option, response: SyncSender>>, }, Accept { @@ -744,11 +769,11 @@ enum ReactorCommand { response: SyncSender>, }, #[cfg(test)] - TcpTranslationCount { + PendingGuestConnectionCount { response: SyncSender, }, #[cfg(test)] - StaleTcpTranslationCount { + StaleGuestConnectionCount { response: SyncSender, }, #[cfg(test)] @@ -787,7 +812,7 @@ enum AcceptedTcpPeer { } #[derive(Clone, Copy)] -enum ConnectorTranslationDisposition { +enum PendingGuestConnectionDisposition { Retain, Discard(Option), } @@ -795,7 +820,6 @@ enum ConnectorTranslationDisposition { struct RetiredTcpConnector { connection: (SocketAddrV4, SocketAddrV4), mapping_index: Option, - retained_in_namespace: bool, unplaced_connector: Option, } @@ -803,9 +827,11 @@ struct RetiredTcpConnector { struct Reactor { epoll: OwnedFd, wake: Arc, + broker_ipv4_address: Ipv4Addr, commands: Receiver, sockets: HashMap, - sessions: HashMap, + tcp: BrokerTcpState, + sessions: HashMap, port_mappings: Vec, max_sockets: usize, max_sockets_per_session: usize, @@ -835,25 +861,33 @@ struct SocketEntry { abortive_close: bool, guest_local_address: Option, port_mapping_index: Option, - tcp_port_publication_claim: Option, - publication_fallback_socket: Option, + mapping_fallback_socket: Option, tcp_no_delay: bool, tcp_keep_alive: bool, } -/// Guest-visible TCP endpoints owned by one broker session. +/// The broker-wide guest TCP namespace and pending guest-to-guest connections. +#[derive(Default)] +struct BrokerTcpState { + bindings: HashMap, + pending_guest_connections: HashMap<(SocketAddrV4, SocketAddrV4), PendingGuestTcpConnection>, + stale_mapped_connections: HashMap<(usize, SocketAddrV4, SocketAddrV4), StaleTcpConnection>, +} + +/// Per-session socket ownership, quota, and teardown state. +/// +/// Sessions do not define network namespaces; all guest TCP endpoints and +/// pending guest-to-guest connections live in the reactor-wide `BrokerTcpState`. #[derive(Default)] -struct SessionSocketNamespace { - tcp: HashMap, - // Pending accepts can outlive the connecting socket, so a translation is - // retained separately from its guest port binding until accept consumes it. - tcp_translations: HashMap<(SocketAddrV4, SocketAddrV4), TcpPeerTranslation>, +struct SessionSocketState { live_sockets: usize, + pending_guest_connections: usize, retained_connectors: usize, closing: bool, } -struct TcpPeerTranslation { +struct PendingGuestTcpConnection { + session_id: SessionId, guest_address: SocketAddrV4, listener_id: u64, discard_on_accept: bool, @@ -870,6 +904,7 @@ struct GuestPortBinding { host_peer_address: Option, host_peer_mapping_index: Option, host_mapped: bool, + listening: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -878,84 +913,38 @@ enum SocketKind { Udp, } -impl SessionSocketNamespace { - /// Returns the guest port bindings tracked for `kind`. - /// - /// Only TCP endpoints live in the guest namespace; UDP sockets remain - /// backed directly by a host endpoint. - fn bindings(&self, kind: SocketKind) -> Option<&HashMap> { - match kind { - SocketKind::Tcp => Some(&self.tcp), - SocketKind::Udp => None, - } - } - - fn insert_binding( - &mut self, - kind: SocketKind, - port: u16, - binding: GuestPortBinding, - ) -> BrokerResult<()> { - if kind != SocketKind::Tcp { +impl BrokerTcpState { + fn insert_binding(&mut self, port: u16, binding: GuestPortBinding) -> BrokerResult<()> { + if binding.guest_address.port() != port { return Err(BrokerError::Internal); } - if self.tcp.insert(port, binding).is_some() { + if self.bindings.insert(port, binding).is_some() { return Err(BrokerError::Internal); } Ok(()) } - fn reserve_binding(&mut self, kind: SocketKind) -> BrokerResult<()> { - if kind != SocketKind::Tcp { - return Err(BrokerError::Internal); - } - self.tcp - .try_reserve(1) - .map_err(|_| BrokerError::OutOfMemory) - } - - fn reserve_tcp_translation( - &mut self, - session_stale: usize, - total_stale: usize, - max_session_translations: usize, - ) -> BrokerResult<()> { - if self - .tcp_translations - .len() - .checked_add(session_stale) - .is_none_or(|count| count >= max_session_translations) - || self - .tcp_translations - .len() - .checked_add(total_stale) - .is_none_or(|count| count >= MAX_RETAINED_TRANSLATIONS) - { - return Err(BrokerError::ResourceExhausted); - } - self.tcp_translations + fn reserve_binding(&mut self) -> BrokerResult<()> { + self.bindings .try_reserve(1) .map_err(|_| BrokerError::OutOfMemory) } - fn remove_binding(&mut self, kind: SocketKind, port: u16, socket_id: u64) { - if kind != SocketKind::Tcp { - return; - } + fn remove_binding(&mut self, port: u16, socket_id: u64) { if self - .tcp + .bindings .get(&port) .is_some_and(|binding| binding.socket_id == socket_id) { - self.tcp.remove(&port); + self.bindings.remove(&port); } } - fn guest_binding(&self, kind: SocketKind, address: SocketAddrV4) -> Option { + fn guest_binding(&self, address: SocketAddrV4) -> Option { if !address.ip().is_loopback() { return None; } - let binding = self.bindings(kind)?.get(&address.port())?; + let binding = self.bindings.get(&address.port())?; if binding.guest_address.ip().is_unspecified() || binding.guest_address.ip() == address.ip() { Some(*binding) @@ -966,16 +955,12 @@ impl SessionSocketNamespace { fn set_host_address( &mut self, - kind: SocketKind, port: u16, socket_id: u64, host_address: SocketAddrV4, host_mapped: bool, ) -> BrokerResult<()> { - if kind != SocketKind::Tcp { - return Err(BrokerError::Internal); - } - let binding = self.tcp.get_mut(&port).ok_or(BrokerError::Internal)?; + let binding = self.bindings.get_mut(&port).ok_or(BrokerError::Internal)?; if binding.socket_id != socket_id { return Err(BrokerError::Internal); } @@ -986,18 +971,13 @@ impl SessionSocketNamespace { fn set_host_peer_address( &mut self, - kind: SocketKind, port: u16, socket_id: u64, host_peer_address: SocketAddrV4, - listener_id: u64, mapping_index: Option, - ) -> BrokerResult<()> { - if kind != SocketKind::Tcp { - return Err(BrokerError::Internal); - } + ) -> BrokerResult<(SocketAddrV4, SocketAddrV4)> { let (host_address, guest_address) = { - let binding = self.tcp.get_mut(&port).ok_or(BrokerError::Internal)?; + let binding = self.bindings.get_mut(&port).ok_or(BrokerError::Internal)?; if binding.socket_id != socket_id { return Err(BrokerError::Internal); } @@ -1008,29 +988,11 @@ impl SessionSocketNamespace { binding.guest_address, ) }; - self.tcp_translations.insert( - (host_address, host_peer_address), - TcpPeerTranslation { - guest_address, - listener_id, - discard_on_accept: false, - discard_deadline: None, - retained_connector: None, - }, - ); - Ok(()) + Ok((host_address, guest_address)) } - fn clear_host_address( - &mut self, - kind: SocketKind, - port: u16, - socket_id: u64, - ) -> BrokerResult<()> { - if kind != SocketKind::Tcp { - return Err(BrokerError::Internal); - } - let binding = self.tcp.get_mut(&port).ok_or(BrokerError::Internal)?; + fn clear_host_address(&mut self, port: u16, socket_id: u64) -> BrokerResult<()> { + let binding = self.bindings.get_mut(&port).ok_or(BrokerError::Internal)?; if binding.socket_id != socket_id { return Err(BrokerError::Internal); } @@ -1038,208 +1000,57 @@ impl SessionSocketNamespace { binding.host_peer_address = None; binding.host_peer_mapping_index = None; binding.host_mapped = false; + binding.listening = false; Ok(()) } - fn remove_tcp_translation_for_connector( - &mut self, - kind: SocketKind, - port: u16, - socket_id: u64, - ) -> Option { - if kind != SocketKind::Tcp { - return None; + fn mark_listening(&mut self, port: u16, socket_id: u64) -> BrokerResult<()> { + let binding = self.bindings.get_mut(&port).ok_or(BrokerError::Internal)?; + if binding.socket_id != socket_id || binding.host_address.is_none() { + return Err(BrokerError::Internal); } - let retired = self.tcp.get_mut(&port).and_then(|binding| { - (binding.socket_id == socket_id) - .then(|| { - binding - .host_address - .zip(binding.host_peer_address.take()) - .map(|connection| (connection, binding.host_peer_mapping_index.take())) - }) - .flatten() - }); - retired.map(|(connection, mapping_index)| { - let retained_connector = self - .tcp_translations - .remove(&connection) - .is_some_and(|translation| translation.retained_connector.is_some()); - if retained_connector { - self.retained_connectors = self - .retained_connectors - .checked_sub(1) - .expect("session retained connector count underflow"); - } - RetiredTcpConnector { - connection, - mapping_index, - retained_in_namespace: retained_connector, - unplaced_connector: None, - } - }) + binding.listening = true; + Ok(()) } - fn retire_tcp_translation_for_connector( + fn take_connector_connection( &mut self, - kind: SocketKind, port: u16, socket_id: u64, - discard_on_accept: bool, - discard_deadline: Option, - mut retained_connector: Option, - ) -> Option { - if kind != SocketKind::Tcp { - return None; - } - let retired = self.tcp.get_mut(&port).and_then(|binding| { + ) -> Option<((SocketAddrV4, SocketAddrV4), Option)> { + self.bindings.get_mut(&port).and_then(|binding| { (binding.socket_id == socket_id) .then(|| { - let connection = binding.host_address.zip(binding.host_peer_address.take()); - connection + binding + .host_address + .zip(binding.host_peer_address.take()) .map(|connection| (connection, binding.host_peer_mapping_index.take())) }) .flatten() - }); - retired.map(|(connection, mapping_index)| { - let mut retained = false; - if let Some(translation) = self.tcp_translations.get_mut(&connection) { - translation.discard_on_accept = discard_on_accept; - translation.discard_deadline = discard_deadline; - retained = retained_connector.is_some(); - translation.retained_connector = retained_connector.take(); - self.retained_connectors = self - .retained_connectors - .checked_add(usize::from(retained)) - .expect("session retained connector count overflow"); - } - RetiredTcpConnector { - connection, - mapping_index, - retained_in_namespace: retained, - unplaced_connector: retained_connector, - } }) } - fn expire_tcp_discard_markers(&mut self, now: Instant) -> usize { - let mut released_connectors = 0; - self.tcp_translations.retain(|_, translation| { - let retain = !translation.discard_on_accept - || translation - .discard_deadline - .is_none_or(|deadline| deadline > now); - if !retain && translation.retained_connector.is_some() { - released_connectors += 1; - } - retain - }); - self.retained_connectors = self - .retained_connectors - .checked_sub(released_connectors) - .expect("session retained connector count underflow"); - released_connectors - } - - fn take_tcp_peer_translation( + fn take_pending_guest_connection( &mut self, remote_address: SocketAddrV4, local_address: SocketAddrV4, - ) -> Option { - let translation = self - .tcp_translations + ) -> Option { + self.pending_guest_connections .remove(&(remote_address, local_address)) .or_else(|| { - self.tcp_translations.remove(&( + self.pending_guest_connections.remove(&( remote_address, SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, local_address.port()), )) - }); - if translation - .as_ref() - .is_some_and(|translation| translation.retained_connector.is_some()) - { - self.retained_connectors = self - .retained_connectors - .checked_sub(1) - .expect("session retained connector count underflow"); - } - translation - } - - fn remove_tcp_translations_for_listener(&mut self, listener_address: SocketAddrV4) -> usize { - let mut released_connectors = 0; - self.tcp_translations - .retain(|(_, destination), translation| { - let retain = destination.port() != listener_address.port() - || (!listener_address.ip().is_unspecified() - && destination.ip() != listener_address.ip()); - if !retain && translation.retained_connector.is_some() { - released_connectors += 1; - } - retain - }); - self.retained_connectors = self - .retained_connectors - .checked_sub(released_connectors) - .expect("session retained connector count underflow"); - released_connectors + }) } +} - fn move_tcp_translations_for_listener( - &mut self, - session_id: SessionId, - listener_address: SocketAddrV4, - stale_connections: &mut HashMap<(SocketAddrV4, SocketAddrV4), StaleTcpConnection>, - ) -> usize { - let retirement_deadline = Instant::now() + PENDING_CONNECT_DISCARD_LIFETIME; - let mut released_connectors = 0; - self.tcp_translations.retain(|connection, translation| { - let destination = connection.1; - let matches = destination.port() == listener_address.port() - && (listener_address.ip().is_unspecified() - || destination.ip() == listener_address.ip()); - if matches { - let mut deadline = translation - .discard_deadline - .map(|deadline| deadline.max(retirement_deadline)); - let mut retained_connector = translation.retained_connector.take(); - if retained_connector.is_some() { - deadline = None; - } - match stale_connections.entry(*connection) { - std::collections::hash_map::Entry::Vacant(entry) => { - entry.insert(StaleTcpConnection { - session_id, - deadline, - retained_connector, - }); - } - std::collections::hash_map::Entry::Occupied(mut entry) => { - let existing = entry.get_mut(); - existing.deadline = match (existing.deadline, deadline) { - (None, _) | (_, None) => None, - (Some(existing), Some(deadline)) => Some(existing.max(deadline)), - }; - if existing.session_id == session_id - && existing.retained_connector.is_none() - { - existing.retained_connector = retained_connector.take(); - } - if retained_connector.is_some() { - released_connectors += 1; - } - } - } - } - !matches - }); - self.retained_connectors = self - .retained_connectors - .checked_sub(released_connectors) - .expect("session retained connector count underflow"); - released_connectors - } +fn retain_session_state(state: &SessionSocketState) -> bool { + !state.closing + || state.live_sockets != 0 + || state.pending_guest_connections != 0 + || state.retained_connectors != 0 } /// Cached connection and readiness state shared with the broker-facing handle. @@ -1285,29 +1096,161 @@ impl fmt::Display for ReactorFailure { } impl Reactor { - fn claimed_tcp_port_publication(&self, id: u64) -> Option<(usize, TcpPortPublicationClaim)> { + fn reserve_pending_guest_connection(&mut self, session_id: SessionId) -> BrokerResult<()> { + let session = self + .sessions + .get(&session_id) + .ok_or(BrokerError::Internal)?; + let session_stale = + count_session_stale_connections(self.tcp.stale_mapped_connections.values(), session_id); + if session + .pending_guest_connections + .checked_add(session_stale) + .is_none_or(|count| count >= self.max_sockets_per_session) + || self + .tcp + .pending_guest_connections + .len() + .checked_add(self.tcp.stale_mapped_connections.len()) + .is_none_or(|count| count >= MAX_TRACKED_GUEST_CONNECTIONS) + { + return Err(BrokerError::ResourceExhausted); + } + self.tcp + .pending_guest_connections + .try_reserve(1) + .map_err(|_| BrokerError::OutOfMemory) + } + + fn decrement_pending_guest_connection_count(&mut self, session_id: SessionId) { + let session = self + .sessions + .get_mut(&session_id) + .expect("pending guest connection session state missing"); + session.pending_guest_connections = session + .pending_guest_connections + .checked_sub(1) + .expect("session pending guest connection count underflow"); + } + + fn release_pending_guest_connection_connector( + &mut self, + connection: &PendingGuestTcpConnection, + ) { + if connection.retained_connector.is_none() { + return; + } + self.retained_connectors = self + .retained_connectors + .checked_sub(1) + .expect("reactor retained connector count underflow"); + let session = self + .sessions + .get_mut(&connection.session_id) + .expect("pending guest connection session state missing"); + session.retained_connectors = session + .retained_connectors + .checked_sub(1) + .expect("session retained connector count underflow"); + } + + fn finish_removed_pending_guest_connection(&mut self, connection: &PendingGuestTcpConnection) { + self.decrement_pending_guest_connection_count(connection.session_id); + self.release_pending_guest_connection_connector(connection); + } + + fn insert_pending_guest_connection( + &mut self, + session_id: SessionId, + connection: (SocketAddrV4, SocketAddrV4), + guest_address: SocketAddrV4, + listener_id: u64, + ) -> BrokerResult<()> { + if let Some(previous) = self.tcp.pending_guest_connections.remove(&connection) { + self.finish_removed_pending_guest_connection(&previous); + } + let session = self + .sessions + .get_mut(&session_id) + .ok_or(BrokerError::Internal)?; + session.pending_guest_connections = session + .pending_guest_connections + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted)?; + self.tcp.pending_guest_connections.insert( + connection, + PendingGuestTcpConnection { + session_id, + guest_address, + listener_id, + discard_on_accept: false, + discard_deadline: None, + retained_connector: None, + }, + ); + Ok(()) + } + + fn retire_pending_guest_connection_for_connector( + &mut self, + session_id: SessionId, + guest_port: u16, + socket_id: u64, + discard_on_accept: bool, + discard_deadline: Option, + mut retained_connector: Option, + ) -> Option { + let (connection, mapping_index) = + self.tcp.take_connector_connection(guest_port, socket_id)?; + let mut retained_in_pending_connection = false; + if let Some(pending) = self.tcp.pending_guest_connections.get_mut(&connection) + && pending.session_id == session_id + { + pending.discard_on_accept = discard_on_accept; + pending.discard_deadline = discard_deadline; + retained_in_pending_connection = retained_connector.is_some(); + pending.retained_connector = retained_connector.take(); + } + if retained_in_pending_connection { + self.retained_connectors = self + .retained_connectors + .checked_add(1) + .expect("reactor retained connector count overflow"); + let session = self + .sessions + .get_mut(&session_id) + .expect("pending guest connection session state missing"); + session.retained_connectors = session + .retained_connectors + .checked_add(1) + .expect("session retained connector count overflow"); + } + Some(RetiredTcpConnector { + connection, + mapping_index, + unplaced_connector: retained_connector, + }) + } + + fn owned_tcp_port_mapping(&self, id: u64) -> Option { let socket = self.sockets.get(&id)?; - let claim = socket.tcp_port_publication_claim?; - let mapping_index = claim.publication_id().index(); - (socket.port_mapping_index == Some(mapping_index) - && self - .port_mappings - .get(mapping_index) - .is_some_and(|state| state.claimed_by == Some((id, claim)))) - .then_some((mapping_index, claim)) + let mapping_index = socket.port_mapping_index?; + self.port_mappings + .get(mapping_index) + .is_some_and(|state| state.owned_by == Some(id)) + .then_some(mapping_index) } - /// Transfers one reserved host endpoint to the socket publishing it. + /// Transfers one reserved broker endpoint to the socket using its mapping. /// /// The reservation descriptor keeps the mapped endpoint exclusively owned - /// between publications, so a claim replaces the socket's descriptor rather - /// than binding the endpoint again. - fn realize_tcp_port_publication( + /// between mapped listeners, so ownership replaces the socket's descriptor + /// rather than binding the endpoint again. + fn realize_tcp_port_mapping( &mut self, id: u64, - claim: TcpPortPublicationClaim, + mapping_index: usize, ) -> BrokerResult> { - let mapping_index = claim.publication_id().index(); let guest_port = self .sockets .get(&id) @@ -1325,9 +1268,9 @@ impl Reactor { if self .port_mappings .get(mapping_index) - .is_some_and(|state| state.claimed_by.is_none() && state.reservation.is_none()) + .is_some_and(|state| state.owned_by.is_none() && state.reservation.is_none()) { - match create_replacement_port_mapping_reservation(mapping) { + match create_replacement_port_mapping_reservation(self.broker_ipv4_address, mapping) { Ok(reservation) => { let state = self .port_mappings @@ -1348,7 +1291,7 @@ impl Reactor { .port_mappings .get_mut(mapping_index) .ok_or(BrokerError::Internal)?; - if state.claimed_by.is_some() { + if state.owned_by.is_some() { return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); } let Some(reservation) = state.reservation.take() else { @@ -1396,7 +1339,7 @@ impl Reactor { } }; let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; - if socket.kind != SocketKind::Tcp || socket.publication_fallback_socket.is_some() { + if socket.kind != SocketKind::Tcp || socket.mapping_fallback_socket.is_some() { let state = self .port_mappings .get_mut(mapping_index) @@ -1441,20 +1384,102 @@ impl Reactor { return Err(broker_error_from_errno(error)); } let fallback = core::mem::replace(&mut socket.socket, reservation); - socket.publication_fallback_socket = Some(fallback); + socket.mapping_fallback_socket = Some(fallback); socket.port_mapping_index = Some(mapping_index); - socket.tcp_port_publication_claim = Some(claim); self.port_mappings .get_mut(mapping_index) .ok_or(BrokerError::Internal)? - .claimed_by = Some((id, claim)); + .owned_by = Some(id); Ok(SocketOutcome::Completed(host_address)) } - fn drain_tcp_listener(&mut self, socket: &OwnedFd, mapping_index: usize) -> BrokerResult { - for _ in 0..MAX_STALE_PORT_MAPPING_CONNECTIONS { - match acceptfrom_with( - socket, + /// Binds an unconfigured identity mapping at listen time. + fn realize_identity_tcp_mapping( + &mut self, + id: u64, + mapping: TcpPortMapping, + ) -> BrokerResult> { + let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; + let guest_port = socket + .guest_local_address + .ok_or(BrokerError::Internal)? + .port(); + if socket.kind != SocketKind::Tcp + || mapping.guest_port != guest_port + || mapping.broker_port != guest_port + || socket.mapping_fallback_socket.is_some() + || socket.port_mapping_index.is_some() + { + return Err(BrokerError::Internal); + } + let replacement = match create_port_mapping_reservation( + self.broker_ipv4_address, + mapping, + false, + false, + ) { + Ok(socket) => socket, + Err(error) => { + return Ok(SocketOutcome::Failed(socket_operation_error_from_errno( + error, + )?)); + } + }; + apply_tcp_options(&replacement, socket.tcp_no_delay, socket.tcp_keep_alive)?; + epoll::add( + &self.epoll, + &replacement, + epoll::EventData::new_u64(id), + idle_epoll_events(), + ) + .map_err(broker_error_from_errno)?; + let host_address = local_socket_address(&replacement)?; + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + let fallback = core::mem::replace(&mut socket.socket, replacement); + socket.mapping_fallback_socket = Some(fallback); + Ok(SocketOutcome::Completed(host_address)) + } + + fn release_failed_identity_tcp_mapping(&mut self, id: u64) -> BrokerResult<()> { + let guest_port = self + .sockets + .get(&id) + .and_then(|socket| { + (!socket.listening + && socket.port_mapping_index.is_none() + && socket.mapping_fallback_socket.is_some()) + .then_some(())?; + socket.guest_local_address.map(|address| address.port()) + }) + .ok_or(BrokerError::Internal)?; + let host_address_is_set = self + .tcp + .bindings + .get(&guest_port) + .filter(|binding| binding.socket_id == id) + .map(|binding| binding.host_mapped); + let replacement = { + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + let fallback = socket + .mapping_fallback_socket + .take() + .ok_or(BrokerError::Internal)?; + core::mem::replace(&mut socket.socket, fallback) + }; + drop(replacement); + let Some(host_address_is_set) = host_address_is_set else { + return Err(BrokerError::Internal); + }; + if host_address_is_set { + self.tcp.clear_host_address(guest_port, id)?; + } + Ok(()) + } + + fn drain_tcp_listener(&mut self, socket: &OwnedFd, mapping_index: usize) -> BrokerResult { + for _ in 0..MAX_STALE_PORT_MAPPING_CONNECTIONS { + match acceptfrom_with( + socket, LinuxSocketFlags::CLOEXEC | LinuxSocketFlags::NONBLOCK, ) { Ok((accepted, remote_address)) => { @@ -1467,7 +1492,11 @@ impl Reactor { remote_address, local_address, ); - self.take_tcp_peer_translations_except(None, remote_address, local_address); + self.remove_pending_guest_connection_except( + None, + remote_address, + local_address, + ); } drop(accepted); } @@ -1485,7 +1514,7 @@ impl Reactor { } fn stop_listening_socket(&mut self, id: u64) -> BrokerResult> { - let claimed_publication = { + let owned_mapping = { let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; if socket.kind != SocketKind::Tcp || !socket.listening { return self @@ -1494,11 +1523,11 @@ impl Reactor { .ok_or(BrokerError::Internal) .and_then(|socket| shutdown_socket(socket, ShutdownMode::StopListening)); } - self.claimed_tcp_port_publication(id) + self.owned_tcp_port_mapping(id) }; - let Some((mapping_index, claim)) = claimed_publication else { + let Some(mapping_index) = owned_mapping else { let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; - if socket.port_mapping_index.is_some() || socket.tcp_port_publication_claim.is_some() { + if socket.port_mapping_index.is_some() { return Err(BrokerError::Internal); } return self @@ -1507,19 +1536,20 @@ impl Reactor { .ok_or(BrokerError::Internal) .and_then(|socket| shutdown_socket(socket, ShutdownMode::StopListening)); }; - if !self.port_mappings.get(mapping_index).is_some_and(|state| { - state.claimed_by == Some((id, claim)) && state.reservation.is_none() - }) { + if !self + .port_mappings + .get(mapping_index) + .is_some_and(|state| state.owned_by == Some(id) && state.reservation.is_none()) + { return Err(BrokerError::Internal); } - let (session_id, guest_port, no_delay, keep_alive) = self + let (guest_port, no_delay, keep_alive) = self .sockets .get(&id) - .filter(|socket| socket.publication_fallback_socket.is_none()) + .filter(|socket| socket.mapping_fallback_socket.is_none()) .and_then(|socket| { socket.guest_local_address.map(|guest_address| { ( - socket.session_id, guest_address.port(), socket.tcp_no_delay, socket.tcp_keep_alive, @@ -1528,9 +1558,9 @@ impl Reactor { }) .ok_or(BrokerError::Internal)?; if !self - .sessions - .get(&session_id) - .and_then(|namespace| namespace.tcp.get(&guest_port)) + .tcp + .bindings + .get(&guest_port) .is_some_and(|binding| binding.socket_id == id && binding.host_mapped) { return Err(BrokerError::Internal); @@ -1566,75 +1596,70 @@ impl Reactor { socket.read_shutdown = true; socket.peek_waitall_threshold = None; socket.port_mapping_index = None; - socket.tcp_port_publication_claim = None; reservation }; let state = self .port_mappings .get_mut(mapping_index) .ok_or(BrokerError::Internal)?; - state.claimed_by = None; + state.owned_by = None; state.reservation = Some(reservation); state.reservation_registered = false; Ok(SocketOutcome::Completed(())) } - fn release_failed_tcp_port_publication( + fn release_failed_tcp_port_mapping( &mut self, id: u64, - claim: TcpPortPublicationClaim, + mapping_index: usize, ) -> BrokerResult<()> { - let mapping_index = claim.publication_id().index(); - if !self.port_mappings.get(mapping_index).is_some_and(|state| { - state.claimed_by == Some((id, claim)) && state.reservation.is_none() - }) { + if !self + .port_mappings + .get(mapping_index) + .is_some_and(|state| state.owned_by == Some(id) && state.reservation.is_none()) + { return Err(BrokerError::Internal); } - let (session_id, guest_port) = self + let guest_port = self .sockets .get(&id) .and_then(|socket| { (!socket.listening && socket.port_mapping_index == Some(mapping_index) - && socket.tcp_port_publication_claim == Some(claim) - && socket.publication_fallback_socket.is_some()) + && socket.mapping_fallback_socket.is_some()) .then_some(())?; socket .guest_local_address - .map(|guest_address| (socket.session_id, guest_address.port())) + .map(|guest_address| guest_address.port()) }) .ok_or(BrokerError::Internal)?; let host_address_is_set = self - .sessions - .get(&session_id) - .and_then(|namespace| namespace.tcp.get(&guest_port)) + .tcp + .bindings + .get(&guest_port) .filter(|binding| binding.socket_id == id) .map(|binding| binding.host_mapped); let reservation = { let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; let fallback = socket - .publication_fallback_socket + .mapping_fallback_socket .take() .ok_or(BrokerError::Internal)?; socket.port_mapping_index = None; - socket.tcp_port_publication_claim = None; core::mem::replace(&mut socket.socket, fallback) }; let state = self .port_mappings .get_mut(mapping_index) .ok_or(BrokerError::Internal)?; - state.claimed_by = None; + state.owned_by = None; state.reservation = Some(reservation); state.reservation_registered = true; let Some(host_address_is_set) = host_address_is_set else { return Err(BrokerError::Internal); }; if host_address_is_set { - self.sessions - .get_mut(&session_id) - .ok_or(BrokerError::Internal)? - .clear_host_address(SocketKind::Tcp, guest_port, id)?; + self.tcp.clear_host_address(guest_port, id)?; } Ok(()) } @@ -1648,12 +1673,10 @@ impl Reactor { kind: SocketKind, address: SocketAddrV4, ) -> BrokerResult { - for binding in self - .sessions - .values() - .filter_map(|namespace| namespace.bindings(kind)) - .flat_map(HashMap::values) - { + if kind != SocketKind::Tcp { + return Ok(false); + } + for binding in self.tcp.bindings.values() { let Some(host_address) = binding.host_address else { continue; }; @@ -1673,18 +1696,18 @@ impl Reactor { /// Resolves a guest destination to the host endpoint that should receive it. fn resolve_guest_destination( &self, - session_id: SessionId, kind: SocketKind, mut address: SocketAddrV4, ) -> BrokerResult)>> { if address.ip().is_unspecified() { address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, address.port()); } - if let Some(binding) = self - .sessions - .get(&session_id) - .and_then(|namespace| namespace.guest_binding(kind, address)) + if kind == SocketKind::Tcp + && let Some(binding) = self.tcp.guest_binding(address) { + if !binding.listening { + return Ok(SocketOutcome::Failed(SocketError::ConnectionRefused)); + } return match binding.host_address { Some(host_address) if host_address.ip().is_unspecified() => { Ok(SocketOutcome::Completed(( @@ -1716,13 +1739,9 @@ impl Reactor { id: u64, requested_address: SocketAddrV4, ) -> BrokerResult> { - let (session_id, kind, already_bound) = { + let (kind, already_bound) = { let socket = self.sockets.get(&id).ok_or(BrokerError::Internal)?; - ( - socket.session_id, - socket.kind, - socket.guest_local_address.is_some(), - ) + (socket.kind, socket.guest_local_address.is_some()) }; if kind != SocketKind::Tcp { let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; @@ -1745,35 +1764,23 @@ impl Reactor { if guest_port == 0 { return Err(BrokerError::Internal); } - if self - .sessions - .get(&session_id) - .ok_or(BrokerError::Internal)? - .tcp - .contains_key(&guest_port) - { + if self.tcp.bindings.contains_key(&guest_port) { return Ok(SocketOutcome::Failed(SocketError::AddressInUse)); } - self.sessions - .get_mut(&session_id) - .ok_or(BrokerError::Internal)? - .reserve_binding(kind)?; + self.tcp.reserve_binding()?; let guest_address = SocketAddrV4::new(*requested_address.ip(), guest_port); - self.sessions - .get_mut(&session_id) - .ok_or(BrokerError::Internal)? - .insert_binding( - kind, - guest_port, - GuestPortBinding { - socket_id: id, - guest_address, - host_address: None, - host_peer_address: None, - host_peer_mapping_index: None, - host_mapped: false, - }, - )?; + self.tcp.insert_binding( + guest_port, + GuestPortBinding { + socket_id: id, + guest_address, + host_address: None, + host_peer_address: None, + host_peer_mapping_index: None, + host_mapped: false, + listening: false, + }, + )?; let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; socket.guest_local_address = Some(guest_address); socket @@ -1788,17 +1795,16 @@ impl Reactor { &mut self, id: u64, backlog: u32, - publication_claim: Option, + mapping: Option, ) -> BrokerResult> { - let (session_id, kind, guest_address, current_claim) = self + let (kind, guest_address, current_mapping_index) = self .sockets .get(&id) .map(|socket| { ( - socket.session_id, socket.kind, socket.guest_local_address, - socket.tcp_port_publication_claim, + socket.port_mapping_index, ) }) .ok_or(BrokerError::Internal)?; @@ -1810,30 +1816,63 @@ impl Reactor { local_socket_address(&self.sockets.get(&id).ok_or(BrokerError::Internal)?.socket)? .port() == 0; - if let Some(current_claim) = current_claim { - if publication_claim != Some(current_claim) { + if mapping.is_some_and(|mapping| mapping.guest_port != guest_address.port()) { + return Err(BrokerError::Internal); + } + let requested_mapping_index = mapping.and_then(|mapping| { + self.port_mappings + .iter() + .position(|state| state.mapping == mapping) + }); + let identity_mapping = mapping.filter(|_| requested_mapping_index.is_none()); + if identity_mapping.is_some_and(|mapping| mapping.broker_port != mapping.guest_port) { + return Err(BrokerError::Internal); + } + let currently_host_mapped = self + .tcp + .bindings + .get(&guest_address.port()) + .filter(|binding| binding.socket_id == id) + .map(|binding| binding.host_mapped) + .ok_or(BrokerError::Internal)?; + if let Some(current_mapping_index) = current_mapping_index { + if requested_mapping_index != Some(current_mapping_index) { return Err(BrokerError::Internal); } - let mapping_index = current_claim.publication_id().index(); - if !self.port_mappings.get(mapping_index).is_some_and(|state| { - state.claimed_by == Some((id, current_claim)) - && self - .sockets - .get(&id) - .is_some_and(|socket| socket.port_mapping_index == Some(mapping_index)) - }) { + if self + .port_mappings + .get(current_mapping_index) + .is_none_or(|state| state.owned_by != Some(id)) + { + return Err(BrokerError::Internal); + } + } else if !needs_host_bind { + if mapping.is_some() != currently_host_mapped { + return Err(BrokerError::Internal); + } + if let Some(mapping) = mapping + && local_socket_address( + &self.sockets.get(&id).ok_or(BrokerError::Internal)?.socket, + )? + .port() + != mapping.broker_port + { return Err(BrokerError::Internal); } - } else if publication_claim.is_some() && !needs_host_bind { - return Err(BrokerError::Internal); } - let newly_claimed_publication = current_claim + let new_mapping = current_mapping_index .is_none() - .then_some(publication_claim) + .then_some(requested_mapping_index) .flatten(); + let new_identity_mapping = needs_host_bind.then_some(identity_mapping).flatten(); if needs_host_bind { - let (host_address, host_mapped) = if let Some(claim) = publication_claim { - match self.realize_tcp_port_publication(id, claim)? { + let (host_address, host_mapped) = if let Some(mapping_index) = requested_mapping_index { + match self.realize_tcp_port_mapping(id, mapping_index)? { + SocketOutcome::Completed(address) => (address, true), + SocketOutcome::Failed(error) => return Ok(SocketOutcome::Failed(error)), + } + } else if let Some(mapping) = identity_mapping { + match self.realize_identity_tcp_mapping(id, mapping)? { SocketOutcome::Completed(address) => (address, true), SocketOutcome::Failed(error) => return Ok(SocketOutcome::Failed(error)), } @@ -1846,39 +1885,43 @@ impl Reactor { }; (address, false) }; - if let Err(error) = self - .sessions - .get_mut(&session_id) - .ok_or(BrokerError::Internal) - .and_then(|namespace| { - namespace.set_host_address( - kind, - guest_address.port(), - id, - host_address, - host_mapped, - ) - }) + if let Err(error) = + self.tcp + .set_host_address(guest_address.port(), id, host_address, host_mapped) { - if let Some(claim) = newly_claimed_publication { - self.release_failed_tcp_port_publication(id, claim)?; + if let Some(mapping_index) = new_mapping { + self.release_failed_tcp_port_mapping(id, mapping_index)?; + } else if new_identity_mapping.is_some() { + self.release_failed_identity_tcp_mapping(id)?; } return Err(error); } } let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; - let host_mapped = socket.port_mapping_index.is_some(); + let host_mapped = mapping.is_some(); let outcome = listen_tcp_socket(&self.epoll, id, socket, backlog, host_mapped); match outcome { Ok(SocketOutcome::Completed(())) => { - if let Some(claim) = newly_claimed_publication { + self.tcp.mark_listening(guest_address.port(), id)?; + if let Some(mapping_index) = new_mapping { let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; - if socket.tcp_port_publication_claim != Some(claim) { + if socket.port_mapping_index != Some(mapping_index) { return Err(BrokerError::Internal); } drop( socket - .publication_fallback_socket + .mapping_fallback_socket + .take() + .ok_or(BrokerError::Internal)?, + ); + } else if new_identity_mapping.is_some() { + let socket = self.sockets.get_mut(&id).ok_or(BrokerError::Internal)?; + if socket.port_mapping_index.is_some() { + return Err(BrokerError::Internal); + } + drop( + socket + .mapping_fallback_socket .take() .ok_or(BrokerError::Internal)?, ); @@ -1886,14 +1929,18 @@ impl Reactor { Ok(SocketOutcome::Completed(guest_address)) } Ok(SocketOutcome::Failed(error)) => { - if let Some(claim) = newly_claimed_publication { - self.release_failed_tcp_port_publication(id, claim)?; + if let Some(mapping_index) = new_mapping { + self.release_failed_tcp_port_mapping(id, mapping_index)?; + } else if new_identity_mapping.is_some() { + self.release_failed_identity_tcp_mapping(id)?; } Ok(SocketOutcome::Failed(error)) } Err(error) => { - if let Some(claim) = newly_claimed_publication { - self.release_failed_tcp_port_publication(id, claim)?; + if let Some(mapping_index) = new_mapping { + self.release_failed_tcp_port_mapping(id, mapping_index)?; + } else if new_identity_mapping.is_some() { + self.release_failed_identity_tcp_mapping(id)?; } Err(error) } @@ -1917,8 +1964,8 @@ impl Reactor { ))?; return connect_datagram_socket(socket, address); } - let claims_mapping = self.claimed_tcp_port_publication(id).is_some(); - if claims_mapping { + let owns_mapping = self.owned_tcp_port_mapping(id).is_some(); + if owns_mapping { let status = SocketConnectionStatus::Failed(SocketError::InvalidArgument); update_snapshot( self.sockets @@ -1949,7 +1996,7 @@ impl Reactor { socket.session_id }; let (network_address, guest_listener_id) = match self - .resolve_guest_destination(session_id, SocketKind::Tcp, guest_address) + .resolve_guest_destination(SocketKind::Tcp, guest_address) .map_err(PlatformConnectError::PeerUnchanged)? { SocketOutcome::Completed(destination) => destination, @@ -1966,37 +2013,48 @@ impl Reactor { return Ok(status); } }; + if guest_listener_id.is_none() { + let source_ip = self + .sockets + .get(&id) + .and_then(|socket| socket.guest_local_address) + .map(|address| { + if address.ip().is_unspecified() { + self.broker_ipv4_address + } else { + *address.ip() + } + }) + .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))?; + let socket = self + .sockets + .get_mut(&id) + .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))?; + if local_socket_address(&socket.socket) + .map_err(PlatformConnectError::PeerUnchanged)? + .port() + == 0 + { + match bind_host_socket(socket, SocketAddrV4::new(source_ip, 0)) + .map_err(PlatformConnectError::PeerUnchanged)? + { + SocketOutcome::Completed(_) => {} + SocketOutcome::Failed(error) => { + let status = SocketConnectionStatus::Failed(error); + update_snapshot(socket, Some(status), ReadinessFlags::ERROR) + .map_err(PlatformConnectError::PeerIndeterminate)?; + return Ok(status); + } + } + } + } let guest_mapping_index = guest_listener_id .and_then(|listener_id| self.sockets.get(&listener_id)) .and_then(|listener| listener.port_mapping_index); if guest_listener_id.is_some() { let now = Instant::now(); - for state in &mut self.port_mappings { - state.stale_connections.retain(|_, stale| { - stale.retained_connector.is_some() - || stale.deadline.is_none_or(|deadline| deadline > now) - }); - } - let session_stale = count_session_stale_connections( - self.port_mappings - .iter() - .flat_map(|state| state.stale_connections.values()), - session_id, - ); - let total_stale = guest_mapping_index - .and_then(|mapping_index| self.port_mappings.get(mapping_index)) - .map_or(0, |state| state.stale_connections.len()); - let namespace = self - .sessions - .get_mut(&session_id) - .ok_or(PlatformConnectError::PeerUnchanged(BrokerError::Internal))?; - let released_connectors = namespace.expire_tcp_discard_markers(now); - self.retained_connectors = self - .retained_connectors - .checked_sub(released_connectors) - .expect("reactor retained connector count underflow"); - namespace - .reserve_tcp_translation(session_stale, total_stale, self.max_sockets_per_session) + self.expire_deadlined_state(now); + self.reserve_pending_guest_connection(session_id) .map_err(PlatformConnectError::PeerUnchanged)?; } let (outcome, readiness) = { @@ -2033,41 +2091,32 @@ impl Reactor { )?; self.take_stale_tcp_connection(mapping_index, host_address, network_address); } - self.take_tcp_peer_translations_except( + self.remove_pending_guest_connection_except( Some(session_id), host_address, network_address, ); - self.sessions.retain(|_, namespace| { - !namespace.closing - || namespace.live_sockets != 0 - || namespace.retained_connectors != 0 - || !namespace.tcp_translations.is_empty() - }); } - let namespace = self.sessions.get_mut(&session_id).ok_or( - PlatformConnectError::PeerIndeterminate(BrokerError::Internal), - )?; - namespace - .set_host_address( - SocketKind::Tcp, - local_guest_address.port(), - id, - host_address, - false, - ) + self.tcp + .set_host_address(local_guest_address.port(), id, host_address, false) .map_err(PlatformConnectError::PeerIndeterminate)?; if let Some(listener_id) = guest_listener_id { - namespace + let (host_address, guest_address) = self + .tcp .set_host_peer_address( - SocketKind::Tcp, local_guest_address.port(), id, network_address, - listener_id, guest_mapping_index, ) .map_err(PlatformConnectError::PeerIndeterminate)?; + self.insert_pending_guest_connection( + session_id, + (host_address, network_address), + guest_address, + listener_id, + ) + .map_err(PlatformConnectError::PeerIndeterminate)?; } } update_snapshot( @@ -2083,46 +2132,46 @@ impl Reactor { Ok(outcome) } - fn remove_tcp_translation_for_connector( + fn remove_pending_guest_connection_for_connector( &mut self, session_id: SessionId, kind: SocketKind, guest_port: u16, socket_id: u64, ) { - let retired = self.sessions.get_mut(&session_id).and_then(|namespace| { - namespace.remove_tcp_translation_for_connector(kind, guest_port, socket_id) - }); - if let Some(retired) = retired { - self.retained_connectors = self - .retained_connectors - .checked_sub(usize::from(retired.retained_in_namespace)) - .expect("reactor retained connector count underflow"); - if let Some(mapping_index) = retired.mapping_index { - self.port_mappings[mapping_index] - .stale_connections - .remove(&retired.connection); + if kind != SocketKind::Tcp { + return; + } + if let Some((connection, mapping_index)) = + self.tcp.take_connector_connection(guest_port, socket_id) + { + if let Some(pending) = self.tcp.pending_guest_connections.remove(&connection) { + debug_assert_eq!(pending.session_id, session_id); + self.finish_removed_pending_guest_connection(&pending); + } + if let Some(mapping_index) = mapping_index + && let Some(stale) = self.tcp.stale_mapped_connections.remove(&( + mapping_index, + connection.0, + connection.1, + )) + { + self.release_stale_tcp_connection(stale); } } } - /// Drops a socket, releasing its guest port and any claimed host mapping. + /// Drops a socket, releasing its guest port and any owned host mapping. fn remove_socket(&mut self, id: u64) { let port_mapping = self - .claimed_tcp_port_publication(id) - .map(|(mapping_index, claim)| { - ( - mapping_index, - claim, - self.port_mappings[mapping_index].mapping, - ) - }); + .owned_tcp_port_mapping(id) + .map(|mapping_index| (mapping_index, self.port_mappings[mapping_index].mapping)); let retired_listener_address = self.sockets.get(&id).and_then(|socket| { (socket.kind == SocketKind::Tcp && socket.listening) .then(|| local_socket_address(&socket.socket).ok()) .flatten() }); - // Keep a published listener active with a zero backlog so the endpoint + // Keep a mapped listener active with a zero backlog so the endpoint // remains exclusive without accumulating an unbounded stale queue. let retain_original = port_mapping.is_some_and(|_| { self.sockets.get(&id).is_some_and(|socket| { @@ -2152,10 +2201,10 @@ impl Reactor { .expect("Linux socket snapshot mutex poisoned") .status == SocketConnectionStatus::Connecting; - let translation_disposition = + let pending_connection_disposition = match getpeername(socket.as_ref().expect("removed socket descriptor missing")) { - Ok(Some(_)) if abortive_close => ConnectorTranslationDisposition::Discard(None), - Ok(Some(_)) => ConnectorTranslationDisposition::Retain, + Ok(Some(_)) if abortive_close => PendingGuestConnectionDisposition::Discard(None), + Ok(Some(_)) => PendingGuestConnectionDisposition::Retain, Ok(None) | Err(_) => { if abortive_close || connecting { let _ = sockopt::set_socket_linger( @@ -2163,144 +2212,136 @@ impl Reactor { Some(Duration::ZERO), ); } - ConnectorTranslationDisposition::Discard(Some( + PendingGuestConnectionDisposition::Discard(Some( Instant::now() + PENDING_CONNECT_DISCARD_LIFETIME, )) } }; if retired_listener_address.is_some() - && let Some((mapping_index, _, _)) = port_mapping + && let Some((mapping_index, _)) = port_mapping && !retain_original { self.clear_stale_tcp_connections(mapping_index); } - let (discarded_connector, retained_before, retained_after) = - if let Some(namespace) = self.sessions.get_mut(&session_id) { - let retained_before = namespace.retained_connectors; - if let Some(listener_address) = retired_listener_address { - if let Some((mapping_index, _, _)) = port_mapping { - let state = &mut self.port_mappings[mapping_index]; - if retain_original { - namespace.move_tcp_translations_for_listener( - session_id, - listener_address, - &mut state.stale_connections, - ); - } else { - namespace.remove_tcp_translations_for_listener(listener_address); - } - } else { - namespace.remove_tcp_translations_for_listener(listener_address); - } + if let Some(listener_address) = retired_listener_address { + if let Some((mapping_index, _)) = port_mapping + && retain_original + { + self.move_pending_guest_connections_for_listener(listener_address, mapping_index); + } else { + self.remove_pending_guest_connections_for_listener(listener_address); + } + } + let mut discarded_connector = None; + if kind == SocketKind::Tcp + && let Some(address) = guest_local_address + { + match pending_connection_disposition { + PendingGuestConnectionDisposition::Retain => { + let retained_connector = + socket.take().expect("removed socket descriptor missing"); + discarded_connector = self.retire_pending_guest_connection_for_connector( + session_id, + address.port(), + id, + false, + None, + Some(retained_connector), + ); } - let mut discarded_connector = None; - if let Some(address) = guest_local_address { - match translation_disposition { - ConnectorTranslationDisposition::Retain => { - let retained_connector = - socket.take().expect("removed socket descriptor missing"); - discarded_connector = namespace.retire_tcp_translation_for_connector( - kind, - address.port(), - id, - false, - None, - Some(retained_connector), - ); - } - ConnectorTranslationDisposition::Discard(discard_deadline) => { - let retained_connector = discard_deadline - .is_none() - .then(|| socket.take().expect("removed socket descriptor missing")); - discarded_connector = namespace.retire_tcp_translation_for_connector( - kind, - address.port(), - id, - true, - discard_deadline, - retained_connector, - ); - } - } - if let Some(retired) = discarded_connector.as_mut() - && let Some(mapping_index) = retired.mapping_index - && let Some(stale) = self.port_mappings[mapping_index] - .stale_connections - .get_mut(&retired.connection) - && stale.session_id == session_id - { - stale.deadline = match (stale.deadline, translation_disposition) { - (existing, ConnectorTranslationDisposition::Discard(None)) => existing, - (_, ConnectorTranslationDisposition::Retain) => None, - (None, ConnectorTranslationDisposition::Discard(Some(deadline))) => { - Some(deadline) - } - ( - Some(existing), - ConnectorTranslationDisposition::Discard(Some(deadline)), - ) => Some(existing.max(deadline)), - }; - if stale.retained_connector.is_none() - && let Some(retained_connector) = retired.unplaced_connector.take() - { - stale.deadline = None; - stale.retained_connector = Some(retained_connector); - namespace.retained_connectors = namespace - .retained_connectors - .checked_add(1) - .expect("session retained connector count overflow"); - } + PendingGuestConnectionDisposition::Discard(discard_deadline) => { + let retained_connector = discard_deadline + .is_none() + .then(|| socket.take().expect("removed socket descriptor missing")); + discarded_connector = self.retire_pending_guest_connection_for_connector( + session_id, + address.port(), + id, + true, + discard_deadline, + retained_connector, + ); + } + } + if let Some(retired) = discarded_connector.as_mut() + && let Some(mapping_index) = retired.mapping_index + && let Some(stale) = self.tcp.stale_mapped_connections.get_mut(&( + mapping_index, + retired.connection.0, + retired.connection.1, + )) + && stale.session_id == session_id + { + stale.deadline = match (stale.deadline, pending_connection_disposition) { + (existing, PendingGuestConnectionDisposition::Discard(None)) => existing, + (_, PendingGuestConnectionDisposition::Retain) => None, + (None, PendingGuestConnectionDisposition::Discard(Some(deadline))) => { + Some(deadline) } - namespace.remove_binding(kind, address.port(), id); + ( + Some(existing), + PendingGuestConnectionDisposition::Discard(Some(deadline)), + ) => Some(existing.max(deadline)), + }; + if stale.retained_connector.is_none() + && let Some(retained_connector) = retired.unplaced_connector.take() + { + stale.deadline = None; + stale.retained_connector = Some(retained_connector); + self.retained_connectors = self + .retained_connectors + .checked_add(1) + .expect("reactor retained connector count overflow"); + let session = self + .sessions + .get_mut(&session_id) + .expect("socket session state missing"); + session.retained_connectors = session + .retained_connectors + .checked_add(1) + .expect("session retained connector count overflow"); } - namespace.live_sockets = namespace - .live_sockets - .checked_sub(1) - .expect("session socket count underflow"); - ( - discarded_connector, - retained_before, - namespace.retained_connectors, - ) - } else { - (None, 0, 0) - }; - self.retained_connectors = self - .retained_connectors - .checked_sub(retained_before) - .and_then(|count| count.checked_add(retained_after)) - .expect("reactor retained connector count invariant violated"); + } + self.tcp.remove_binding(address.port(), id); + } + if let Some(session) = self.sessions.get_mut(&session_id) { + session.live_sockets = session + .live_sockets + .checked_sub(1) + .expect("session socket count underflow"); + } drop(discarded_connector); if self .sessions .get(&session_id) - .is_some_and(|namespace| namespace.closing) + .is_some_and(|session| session.closing) { - self.retire_session_stale_connectors(session_id); + self.retire_session_connectors(session_id); } - let remove_namespace = self.sessions.get(&session_id).is_some_and(|namespace| { - namespace.closing && namespace.live_sockets == 0 && namespace.retained_connectors == 0 - }); - if remove_namespace { + let remove_session = self + .sessions + .get(&session_id) + .is_some_and(|session| !retain_session_state(session)); + if remove_session { self.sessions.remove(&session_id); } let replacement_reservation = if retain_original { Some( socket .take() - .expect("mapping claimant descriptor unexpectedly retained"), + .expect("mapping owner descriptor unexpectedly retained"), ) } else { drop(socket.take()); - port_mapping.and_then(|(_, _, mapping)| { - create_replacement_port_mapping_reservation(mapping).ok() + port_mapping.and_then(|(_, mapping)| { + create_replacement_port_mapping_reservation(self.broker_ipv4_address, mapping).ok() }) }; - if let Some((mapping_index, claim, _)) = port_mapping + if let Some((mapping_index, _)) = port_mapping && let Some(state) = self.port_mappings.get_mut(mapping_index) - && state.claimed_by == Some((id, claim)) + && state.owned_by == Some(id) { - state.claimed_by = None; + state.owned_by = None; state.reservation = replacement_reservation; state.reservation_registered = false; } @@ -2310,7 +2351,15 @@ impl Reactor { loop { let mut events = core::mem::take(&mut self.events); events.clear(); - match epoll::wait(&self.epoll, spare_capacity(&mut events), None) { + let now = Instant::now(); + let timeout = self.next_cleanup_deadline().map(|deadline| { + let duration = deadline.saturating_duration_since(now); + Timespec { + tv_sec: i64::try_from(duration.as_secs()).unwrap_or(i64::MAX), + tv_nsec: i64::from(duration.subsec_nanos()), + } + }); + match epoll::wait(&self.epoll, spare_capacity(&mut events), timeout.as_ref()) { Ok(_) => {} Err(Errno::INTR) => { self.events = events; @@ -2318,6 +2367,7 @@ impl Reactor { } Err(error) => return Err(ReactorFailure::Io(error)), } + self.expire_deadlined_state(Instant::now()); // Apply readiness observed by this wait before commands. A command // that then reaches EAGAIN records the newer authoritative state. @@ -2337,7 +2387,9 @@ impl Reactor { None }; if let Some((session_id, kind, Some(guest_port))) = failed_connector { - self.remove_tcp_translation_for_connector(session_id, kind, guest_port, id); + self.remove_pending_guest_connection_for_connector( + session_id, kind, guest_port, id, + ); } } } @@ -2351,6 +2403,59 @@ impl Reactor { } } + fn next_cleanup_deadline(&self) -> Option { + self.tcp + .pending_guest_connections + .values() + .filter_map(|connection| connection.discard_deadline) + .chain( + self.tcp + .stale_mapped_connections + .values() + .filter_map(|stale| stale.deadline), + ) + .min() + } + + fn expire_deadlined_state(&mut self, now: Instant) { + let Reactor { + tcp, + sessions, + retained_connectors, + .. + } = self; + tcp.pending_guest_connections.retain(|_, connection| { + let retain = !connection.discard_on_accept + || connection + .discard_deadline + .is_none_or(|deadline| deadline > now); + if !retain { + let session = sessions + .get_mut(&connection.session_id) + .expect("pending guest connection session state missing"); + session.pending_guest_connections = session + .pending_guest_connections + .checked_sub(1) + .expect("session pending guest connection count underflow"); + if connection.retained_connector.is_some() { + session.retained_connectors = session + .retained_connectors + .checked_sub(1) + .expect("session retained connector count underflow"); + *retained_connectors = retained_connectors + .checked_sub(1) + .expect("reactor retained connector count underflow"); + } + } + retain + }); + sessions.retain(|_, session| retain_session_state(session)); + tcp.stale_mapped_connections.retain(|_, stale| { + stale.retained_connector.is_some() + || stale.deadline.is_none_or(|deadline| deadline > now) + }); + } + fn drain_wake(&self) -> core::result::Result<(), ReactorFailure> { let mut value = [0_u8; size_of::()]; loop { @@ -2409,11 +2514,17 @@ impl Reactor { ReactorCommand::Listen { id, backlog, - publication, + mapping, response, } => { - let outcome = self.listen_socket(id, backlog, publication); - let _ = response.send(outcome); + let outcome = self.listen_socket(id, backlog, mapping); + if response.send(outcome).is_err() { + // An unacknowledged listen may have committed a + // mapping. Retire the socket before processing any + // later command that could observe released core + // authority. + self.remove_socket(id); + } } ReactorCommand::Accept { listener_id, @@ -2509,7 +2620,6 @@ impl Reactor { .guest_local_address .map(|guest_address| { ( - socket.session_id, address, guest_address.port(), socket.port_mapping_index, @@ -2531,32 +2641,20 @@ impl Reactor { .and_then(|socket| shutdown_socket(socket, mode))? }; if matches!(outcome, SocketOutcome::Completed(())) - && let Some((session_id, listener_address, guest_port, mapping_index)) = + && let Some((listener_address, guest_port, mapping_index)) = retired_listener { - let namespace = self - .sessions - .get_mut(&session_id) - .ok_or(BrokerError::Internal)?; - let released_connectors = if let Some(mapping_index) = mapping_index { - let stale_connections = &mut self - .port_mappings - .get_mut(mapping_index) - .ok_or(BrokerError::Internal)? - .stale_connections; - namespace.move_tcp_translations_for_listener( - session_id, + if let Some(mapping_index) = mapping_index { + self.move_pending_guest_connections_for_listener( listener_address, - stale_connections, - ) + mapping_index, + ); } else { - namespace.remove_tcp_translations_for_listener(listener_address) - }; - self.retained_connectors = self - .retained_connectors - .checked_sub(released_connectors) - .expect("reactor retained connector count underflow"); - namespace.clear_host_address(SocketKind::Tcp, guest_port, id)?; + self.remove_pending_guest_connections_for_listener( + listener_address, + ); + } + self.tcp.clear_host_address(guest_port, id)?; update_snapshot( self.sockets.get(&id).ok_or(BrokerError::Internal)?, Some(SocketConnectionStatus::Failed(SocketError::NotConnected)), @@ -2610,13 +2708,15 @@ impl Reactor { session_id, response, } => { - if let Some(namespace) = self.sessions.get_mut(&session_id) { - namespace.closing = true; + if let Some(session) = self.sessions.get_mut(&session_id) { + session.closing = true; } - self.retire_session_stale_connectors(session_id); - if self.sessions.get(&session_id).is_some_and(|namespace| { - namespace.live_sockets == 0 && namespace.retained_connectors == 0 - }) { + self.retire_session_connectors(session_id); + if self + .sessions + .get(&session_id) + .is_some_and(|session| !retain_session_state(session)) + { self.sessions.remove(&session_id); } let _ = response.send(()); @@ -2627,31 +2727,19 @@ impl Reactor { guest_port, response, } => { - let host_address = self.sessions.values().find_map(|namespace| { - namespace - .bindings(kind)? - .get(&guest_port) - .and_then(|binding| binding.host_address) - }); + let host_address = (kind == SocketKind::Tcp) + .then(|| self.tcp.bindings.get(&guest_port)) + .flatten() + .and_then(|binding| binding.host_address); let _ = response.send(host_address); } #[cfg(test)] - ReactorCommand::TcpTranslationCount { response } => { - let count = self - .sessions - .values() - .map(|namespace| namespace.tcp_translations.len()) - .sum(); - let _ = response.send(count); + ReactorCommand::PendingGuestConnectionCount { response } => { + let _ = response.send(self.tcp.pending_guest_connections.len()); } #[cfg(test)] - ReactorCommand::StaleTcpTranslationCount { response } => { - let count = self - .port_mappings - .iter() - .map(|state| state.stale_connections.len()) - .sum(); - let _ = response.send(count); + ReactorCommand::StaleGuestConnectionCount { response } => { + let _ = response.send(self.tcp.stale_mapped_connections.len()); } #[cfg(test)] ReactorCommand::RetainedConnectorCount { response } => { @@ -2659,6 +2747,8 @@ impl Reactor { } ReactorCommand::Stop { response } => { self.sockets.clear(); + self.tcp.bindings.clear(); + self.tcp.pending_guest_connections.clear(); self.sessions.clear(); let _ = response.send(()); return true; @@ -2688,13 +2778,13 @@ impl Reactor { if self.sockets.contains_key(&id) { return Err(BrokerError::Internal); } - let namespace = self.sessions.entry(session_id).or_default(); - if namespace.closing { + let session = self.sessions.entry(session_id).or_default(); + if session.closing { return Err(BrokerError::UnknownObject); } - if namespace + if session .live_sockets - .checked_add(namespace.retained_connectors) + .checked_add(session.retained_connectors) .is_none_or(|count| count >= self.max_sockets_per_session) { return Err(BrokerError::ResourceExhausted); @@ -2749,78 +2839,51 @@ impl Reactor { abortive_close: false, guest_local_address: None, port_mapping_index: None, - tcp_port_publication_claim: None, - publication_fallback_socket: None, + mapping_fallback_socket: None, tcp_no_delay: false, tcp_keep_alive: false, }, ); - namespace.live_sockets = namespace + session.live_sockets = session .live_sockets .checked_add(1) .ok_or(BrokerError::ResourceExhausted)?; Ok(()) } - fn take_tcp_peer_translation( + fn take_pending_guest_connection_for_accept( &mut self, - listener_session_id: SessionId, listener_id: u64, mapping_index: Option, remote_address: SocketAddrV4, local_address: SocketAddrV4, ) -> AcceptedTcpPeer { let now = Instant::now(); - if let Some(mapping_index) = mapping_index { - let stale_connections = &mut self.port_mappings[mapping_index].stale_connections; - stale_connections.retain(|_, stale| { - stale.retained_connector.is_some() - || stale.deadline.is_none_or(|deadline| deadline > now) - }); - if self.take_stale_tcp_connection(mapping_index, remote_address, local_address) { - return AcceptedTcpPeer::Stale; - } + self.expire_deadlined_state(now); + if let Some(mapping_index) = mapping_index + && self.take_stale_tcp_connection(mapping_index, remote_address, local_address) + { + return AcceptedTcpPeer::Stale; } - let released_connectors = self - .sessions - .values_mut() - .map(|namespace| namespace.expire_tcp_discard_markers(now)) - .sum::(); - self.retained_connectors = self - .retained_connectors - .checked_sub(released_connectors) - .expect("reactor retained connector count underflow"); - let current_translation = - self.sessions - .get_mut(&listener_session_id) - .and_then(|namespace| { - namespace.take_tcp_peer_translation(remote_address, local_address) - }); - if let Some(translation) = current_translation { - self.retained_connectors = self - .retained_connectors - .checked_sub(usize::from(translation.retained_connector.is_some())) - .expect("reactor retained connector count underflow"); - drop(translation.retained_connector); - if translation.discard_on_accept { + let pending_connection = self + .tcp + .take_pending_guest_connection(remote_address, local_address); + let peer = if let Some(connection) = pending_connection { + self.finish_removed_pending_guest_connection(&connection); + drop(connection.retained_connector); + if connection.discard_on_accept { AcceptedTcpPeer::Stale - } else if translation.listener_id == listener_id { - AcceptedTcpPeer::Guest(translation.guest_address) + } else if connection.listener_id == listener_id { + AcceptedTcpPeer::Guest(connection.guest_address) } else { AcceptedTcpPeer::Stale } } else { - let stale = self.take_tcp_peer_translations_except( - Some(listener_session_id), - remote_address, - local_address, - ); - if stale { - AcceptedTcpPeer::Stale - } else { - AcceptedTcpPeer::Native(remote_address) - } - } + AcceptedTcpPeer::Native(remote_address) + }; + self.sessions + .retain(|_, session| retain_session_state(session)); + peer } fn take_stale_tcp_connection( @@ -2829,17 +2892,17 @@ impl Reactor { remote_address: SocketAddrV4, local_address: SocketAddrV4, ) -> bool { - let stale = { - let stale_connections = &mut self.port_mappings[mapping_index].stale_connections; - stale_connections - .remove(&(remote_address, local_address)) - .or_else(|| { - stale_connections.remove(&( - remote_address, - SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, local_address.port()), - )) - }) - }; + let stale = self + .tcp + .stale_mapped_connections + .remove(&(mapping_index, remote_address, local_address)) + .or_else(|| { + self.tcp.stale_mapped_connections.remove(&( + mapping_index, + remote_address, + SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, local_address.port()), + )) + }); let Some(stale) = stale else { return false; }; @@ -2853,104 +2916,242 @@ impl Reactor { .retained_connectors .checked_sub(1) .expect("reactor retained connector count underflow"); - let remove_namespace = - self.sessions - .get_mut(&stale.session_id) - .is_some_and(|namespace| { - namespace.retained_connectors = namespace + let remove_session = self + .sessions + .get_mut(&stale.session_id) + .is_some_and(|session| { + session.retained_connectors = session + .retained_connectors + .checked_sub(1) + .expect("session retained connector count underflow"); + !retain_session_state(session) + }); + if remove_session { + self.sessions.remove(&stale.session_id); + } + } + drop(stale); + } + + fn clear_stale_tcp_connections(&mut self, mapping_index: usize) { + let Reactor { + tcp, + sessions, + retained_connectors, + .. + } = self; + tcp.stale_mapped_connections.retain(|(index, _, _), stale| { + let retain = *index != mapping_index; + if !retain && stale.retained_connector.is_some() { + *retained_connectors = retained_connectors + .checked_sub(1) + .expect("reactor retained connector count underflow"); + if let Some(session) = sessions.get_mut(&stale.session_id) { + session.retained_connectors = session + .retained_connectors + .checked_sub(1) + .expect("session retained connector count underflow"); + } + } + retain + }); + sessions.retain(|_, session| retain_session_state(session)); + } + + fn retire_session_connectors(&mut self, session_id: SessionId) { + let mut pending_connections_released = 0; + for connection in self + .tcp + .pending_guest_connections + .values_mut() + .filter(|connection| connection.session_id == session_id) + { + if let Some(connector) = connection.retained_connector.take() { + let _ = sockopt::set_socket_linger(&connector, None); + drop(connector); + connection.discard_on_accept = true; + // An established child can remain queued after its connector + // closes. Keep its discard marker until accept or listener + // teardown so it can never fall through as a native peer. + connection.discard_deadline = None; + pending_connections_released += 1; + } + } + let mut stale_released = 0; + for stale in self + .tcp + .stale_mapped_connections + .values_mut() + .filter(|stale| stale.session_id == session_id) + { + if let Some(connector) = stale.retained_connector.take() { + let _ = sockopt::set_socket_linger(&connector, None); + drop(connector); + stale.deadline = None; + stale_released += 1; + } + } + self.retained_connectors = self + .retained_connectors + .checked_sub(pending_connections_released + stale_released) + .expect("reactor retained connector count underflow"); + if let Some(session) = self.sessions.get_mut(&session_id) { + session.retained_connectors = session + .retained_connectors + .checked_sub(pending_connections_released + stale_released) + .expect("session retained connector count underflow"); + } + } + + fn remove_pending_guest_connection_except( + &mut self, + excluded_session_id: Option, + remote_address: SocketAddrV4, + local_address: SocketAddrV4, + ) -> bool { + let exact = (remote_address, local_address); + let unspecified = ( + remote_address, + SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, local_address.port()), + ); + let connection = if self.tcp.pending_guest_connections.contains_key(&exact) { + Some(exact) + } else if self + .tcp + .pending_guest_connections + .contains_key(&unspecified) + { + Some(unspecified) + } else { + None + }; + let removed = connection + .filter(|connection| { + self.tcp + .pending_guest_connections + .get(connection) + .is_some_and(|pending| Some(pending.session_id) != excluded_session_id) + }) + .and_then(|connection| self.tcp.pending_guest_connections.remove(&connection)); + if let Some(pending) = &removed { + self.finish_removed_pending_guest_connection(pending); + } + self.sessions + .retain(|_, session| retain_session_state(session)); + removed.is_some() + } + + fn remove_pending_guest_connections_for_listener(&mut self, listener_address: SocketAddrV4) { + let Reactor { + tcp, + sessions, + retained_connectors, + .. + } = self; + tcp.pending_guest_connections + .retain(|(_, destination), connection| { + let retain = destination.port() != listener_address.port() + || (!listener_address.ip().is_unspecified() + && destination.ip() != listener_address.ip()); + if !retain { + let session = sessions + .get_mut(&connection.session_id) + .expect("pending guest connection session state missing"); + session.pending_guest_connections = session + .pending_guest_connections + .checked_sub(1) + .expect("session pending guest connection count underflow"); + if connection.retained_connector.is_some() { + session.retained_connectors = session + .retained_connectors + .checked_sub(1) + .expect("session retained connector count underflow"); + *retained_connectors = retained_connectors + .checked_sub(1) + .expect("reactor retained connector count underflow"); + } + } + retain + }); + sessions.retain(|_, session| retain_session_state(session)); + } + + fn move_pending_guest_connections_for_listener( + &mut self, + listener_address: SocketAddrV4, + mapping_index: usize, + ) { + let retirement_deadline = Instant::now() + PENDING_CONNECT_DISCARD_LIFETIME; + let Reactor { + tcp, + sessions, + retained_connectors, + .. + } = self; + let BrokerTcpState { + pending_guest_connections, + stale_mapped_connections, + .. + } = tcp; + pending_guest_connections.retain(|connection_tuple, connection| { + let destination = connection_tuple.1; + let matches = destination.port() == listener_address.port() + && (listener_address.ip().is_unspecified() + || destination.ip() == listener_address.ip()); + if !matches { + return true; + } + let session = sessions + .get_mut(&connection.session_id) + .expect("pending guest connection session state missing"); + session.pending_guest_connections = session + .pending_guest_connections + .checked_sub(1) + .expect("session pending guest connection count underflow"); + let mut deadline = connection + .discard_deadline + .map(|deadline| deadline.max(retirement_deadline)); + let mut retained_connector = connection.retained_connector.take(); + if retained_connector.is_some() { + deadline = None; + } + match stale_mapped_connections.entry(( + mapping_index, + connection_tuple.0, + connection_tuple.1, + )) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(StaleTcpConnection { + session_id: connection.session_id, + deadline, + retained_connector, + }); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + let existing = entry.get_mut(); + existing.deadline = match (existing.deadline, deadline) { + (None, _) | (_, None) => None, + (Some(existing), Some(deadline)) => Some(existing.max(deadline)), + }; + if existing.session_id == connection.session_id + && existing.retained_connector.is_none() + { + existing.retained_connector = retained_connector.take(); + } + if retained_connector.is_some() { + session.retained_connectors = session .retained_connectors .checked_sub(1) .expect("session retained connector count underflow"); - namespace.closing - && namespace.live_sockets == 0 - && namespace.retained_connectors == 0 - }); - if remove_namespace { - self.sessions.remove(&stale.session_id); - } - } - drop(stale); - } - - fn clear_stale_tcp_connections(&mut self, mapping_index: usize) { - let Reactor { - port_mappings, - sessions, - retained_connectors, - .. - } = self; - for (_, stale) in port_mappings[mapping_index].stale_connections.drain() { - if stale.retained_connector.is_some() { - *retained_connectors = retained_connectors - .checked_sub(1) - .expect("reactor retained connector count underflow"); - if let Some(namespace) = sessions.get_mut(&stale.session_id) { - namespace.retained_connectors = namespace - .retained_connectors - .checked_sub(1) - .expect("session retained connector count underflow"); + *retained_connectors = retained_connectors + .checked_sub(1) + .expect("reactor retained connector count underflow"); + } } } - } - sessions.retain(|_, namespace| { - !namespace.closing || namespace.live_sockets != 0 || namespace.retained_connectors != 0 + false }); - } - - fn retire_session_stale_connectors(&mut self, session_id: SessionId) { - let deadline = Instant::now() + PENDING_CONNECT_DISCARD_LIFETIME; - let mut released_connectors = 0; - for stale in self - .port_mappings - .iter_mut() - .flat_map(|state| state.stale_connections.values_mut()) - .filter(|stale| stale.session_id == session_id) - { - if let Some(connector) = stale.retained_connector.take() { - let _ = sockopt::set_socket_linger(&connector, None); - drop(connector); - stale.deadline = Some( - stale - .deadline - .map_or(deadline, |existing| existing.max(deadline)), - ); - released_connectors += 1; - } - } - self.retained_connectors = self - .retained_connectors - .checked_sub(released_connectors) - .expect("reactor retained connector count underflow"); - if let Some(namespace) = self.sessions.get_mut(&session_id) { - namespace.retained_connectors = namespace - .retained_connectors - .checked_sub(released_connectors) - .expect("session retained connector count underflow"); - } - } - - fn take_tcp_peer_translations_except( - &mut self, - excluded_session_id: Option, - remote_address: SocketAddrV4, - local_address: SocketAddrV4, - ) -> bool { - let mut found = false; - let mut released_connectors = 0; - for (session_id, namespace) in &mut self.sessions { - if Some(*session_id) != excluded_session_id - && let Some(translation) = - namespace.take_tcp_peer_translation(remote_address, local_address) - { - released_connectors += usize::from(translation.retained_connector.is_some()); - found = true; - } - } - self.retained_connectors = self - .retained_connectors - .checked_sub(released_connectors) - .expect("reactor retained connector count underflow"); - found + sessions.retain(|_, session| retain_session_state(session)); } fn accept_socket( @@ -3012,8 +3213,7 @@ impl Reactor { SocketAddrV4::try_from(remote_address.ok_or(BrokerError::Internal)?) .map_err(|_| BrokerError::Internal)?; let host_local_address = local_socket_address(&socket)?; - match self.take_tcp_peer_translation( - listener_session_id, + match self.take_pending_guest_connection_for_accept( listener_id, mapping_index, remote_address, @@ -3095,17 +3295,16 @@ impl Reactor { abortive_close: false, guest_local_address: Some(local_address), port_mapping_index: None, - tcp_port_publication_claim: None, - publication_fallback_socket: None, + mapping_fallback_socket: None, tcp_no_delay: listener_tcp_no_delay, tcp_keep_alive: listener_tcp_keep_alive, }, ); - let namespace = self + let session = self .sessions .get_mut(&listener_session_id) .ok_or(BrokerError::Internal)?; - namespace.live_sockets = namespace + session.live_sockets = session .live_sockets .checked_add(1) .ok_or(BrokerError::ResourceExhausted)?; @@ -3124,7 +3323,7 @@ impl Reactor { snapshot.readiness = ReadinessFlags::ERROR; drop(snapshot); // The readiness path may itself be why the reactor is failing. The - // cached terminal snapshot remains authoritative if publication is + // cached terminal snapshot remains authoritative if mapping is // no longer available. let _ = socket.readiness.publish(ReadinessFlags::ERROR); } @@ -3651,7 +3850,7 @@ fn set_tcp_option(socket: &mut SocketEntry, value: TcpOptionValue) -> BrokerResu if socket.kind != SocketKind::Tcp { return Err(BrokerError::UnsupportedOperation); } - // Cached values are reapplied when a published endpoint replaces this + // Cached values are reapplied when a mapped endpoint replaces this // socket's descriptor. match value { TcpOptionValue::NoDelay(value) => { @@ -4162,6 +4361,83 @@ mod tests { const TEST_TIMEOUT: Duration = Duration::from_secs(5); const FIRST_GUEST_EPHEMERAL_PORT: u16 = 49152; + fn test_reactor( + max_sockets_per_session: usize, + tcp_port_mappings: &[TcpPortMapping], + ) -> Reactor { + let epoll = epoll::create(epoll::CreateFlags::CLOEXEC).unwrap(); + let wake = Arc::new(eventfd(0, EventfdFlags::CLOEXEC | EventfdFlags::NONBLOCK).unwrap()); + let (_, commands) = sync_channel(1); + Reactor { + epoll, + wake, + broker_ipv4_address: Ipv4Addr::LOCALHOST, + commands, + sockets: HashMap::new(), + tcp: BrokerTcpState::default(), + sessions: HashMap::new(), + port_mappings: tcp_port_mappings + .iter() + .copied() + .map(|mapping| PortMappingState { + mapping, + reservation: None, + reservation_registered: false, + owned_by: None, + }) + .collect(), + max_sockets: MAX_TRACKED_GUEST_CONNECTIONS, + max_sockets_per_session, + retained_connectors: 0, + peek_cache: None, + events: Vec::new(), + } + } + + #[allow(clippy::too_many_arguments)] + fn insert_test_pending_guest_connection( + reactor: &mut Reactor, + session_id: SessionId, + guest_port: u16, + socket_id: u64, + host_address: SocketAddrV4, + listener_address: SocketAddrV4, + listener_id: u64, + mapping_index: Option, + ) { + reactor.sessions.entry(session_id).or_default(); + reactor + .reserve_pending_guest_connection(session_id) + .unwrap(); + reactor + .tcp + .insert_binding( + guest_port, + GuestPortBinding { + socket_id, + guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), + host_address: Some(host_address), + host_peer_address: None, + host_peer_mapping_index: None, + host_mapped: false, + listening: false, + }, + ) + .unwrap(); + let (host_address, guest_address) = reactor + .tcp + .set_host_peer_address(guest_port, socket_id, listener_address, mapping_index) + .unwrap(); + reactor + .insert_pending_guest_connection( + session_id, + (host_address, listener_address), + guest_address, + listener_id, + ) + .unwrap(); + } + #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct ReceivedPlatformDatagram { received: usize, @@ -4302,9 +4578,10 @@ mod tests { fn port_mapping_reservation_is_close_on_exec() { let host_address = unused_tcp_address(); let retained = create_port_mapping_reservation( - TcpPortPublication { + *host_address.ip(), + TcpPortMapping { + broker_port: host_address.port(), guest_port: 80, - external_address: host_address, }, false, false, @@ -4319,21 +4596,44 @@ mod tests { } #[test] - fn unavailable_publish_endpoint_rejects_provider_startup() { + fn unavailable_mapped_port_rejects_listen() { let occupied = TcpListener::bind("127.0.0.1:0").unwrap(); let host_address = socket_address_v4(occupied.local_addr().unwrap()); - let error = LinuxSocketProvider::new_with_tcp_port_publications( - 1, - 1, - &[TcpPortPublication { - guest_port: 80, - external_address: host_address, - }], + let provider = Arc::new( + LinuxSocketProvider::new_with_tcp_port_mappings( + 1, + 1, + *host_address.ip(), + &[TcpPortMapping { + broker_port: host_address.port(), + guest_port: 80, + }], + ) + .unwrap(), + ); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(2, 0, 1, 1), + provider, ) - .err() .unwrap(); - - assert_eq!(error.kind(), ErrorKind::AddrInUse); + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, _publications) = channel(); + let (retired, _retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let listener = create_socket(&session, readiness); + let guest_address = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 80); + assert_eq!( + litebox_broker_core::socket::bind(&session, listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&session, listener, 1), + Ok(SocketOutcome::Failed(SocketError::AddressInUse)) + ); } #[test] @@ -4393,42 +4693,27 @@ mod tests { } #[test] - fn tcp_peer_translation_uses_the_complete_connection_tuple() { - let mut namespace = SessionSocketNamespace::default(); + fn pending_guest_tcp_connection_uses_the_complete_host_tuple() { + let mut reactor = test_reactor(MAX_TRACKED_GUEST_CONNECTIONS, &[]); + let session_id = SessionId(7); let shared_host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); for (guest_port, host_peer_port) in [(1000, 5000), (1001, 5001)] { - namespace - .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) - .unwrap(); - namespace - .insert_binding( - SocketKind::Tcp, - guest_port, - GuestPortBinding { - socket_id: u64::from(guest_port), - guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), - host_address: Some(shared_host_address), - host_peer_address: None, - host_peer_mapping_index: None, - host_mapped: false, - }, - ) - .unwrap(); - namespace - .set_host_peer_address( - SocketKind::Tcp, - guest_port, - u64::from(guest_port), - SocketAddrV4::new(Ipv4Addr::LOCALHOST, host_peer_port), - u64::from(guest_port), - None, - ) - .unwrap(); + insert_test_pending_guest_connection( + &mut reactor, + session_id, + guest_port, + u64::from(guest_port), + shared_host_address, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, host_peer_port), + u64::from(guest_port), + None, + ); } assert_eq!( - namespace - .take_tcp_peer_translation( + reactor + .tcp + .take_pending_guest_connection( shared_host_address, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001), ) @@ -4436,18 +4721,20 @@ mod tests { .guest_address, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1001) ); - assert_eq!(namespace.tcp_translations.len(), 1); + assert_eq!(reactor.tcp.pending_guest_connections.len(), 1); assert!( - namespace - .take_tcp_peer_translation( + reactor + .tcp + .take_pending_guest_connection( shared_host_address, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001), ) .is_none() ); assert_eq!( - namespace - .take_tcp_peer_translation( + reactor + .tcp + .take_pending_guest_connection( shared_host_address, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000), ) @@ -4455,26 +4742,105 @@ mod tests { .guest_address, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1000) ); - assert!(namespace.tcp_translations.is_empty()); + assert!(reactor.tcp.pending_guest_connections.is_empty()); + } + + #[test] + fn connector_binding_is_not_routable_as_a_guest_listener() { + let mut reactor = test_reactor(1, &[]); + let guest_port = 1000; + let socket_id = 1; + let host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); + reactor + .tcp + .insert_binding( + guest_port, + GuestPortBinding { + socket_id, + guest_address: SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, guest_port), + host_address: Some(host_address), + host_peer_address: None, + host_peer_mapping_index: None, + host_mapped: false, + listening: false, + }, + ) + .unwrap(); + + assert_eq!( + reactor.resolve_guest_destination( + SocketKind::Tcp, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), + ), + Ok(SocketOutcome::Failed(SocketError::ConnectionRefused)) + ); + + reactor.tcp.mark_listening(guest_port, socket_id).unwrap(); + assert_eq!( + reactor.resolve_guest_destination( + SocketKind::Tcp, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), + ), + Ok(SocketOutcome::Completed((host_address, Some(socket_id)))) + ); } #[test] - fn tcp_translation_capacity_charges_only_session_owned_stale_state() { - let mut namespace = SessionSocketNamespace::default(); + fn pending_guest_connection_capacity_counts_all_stale_mappings() { + let mut reactor = test_reactor(1, &[]); + let session_id = SessionId(7); + let foreign_session_id = SessionId(8); + reactor + .sessions + .insert(session_id, SessionSocketState::default()); - namespace.reserve_tcp_translation(0, 1, 1).unwrap(); + reactor + .reserve_pending_guest_connection(session_id) + .unwrap(); + reactor.tcp.stale_mapped_connections.insert( + ( + 0, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000), + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000), + ), + StaleTcpConnection { + session_id, + deadline: None, + retained_connector: None, + }, + ); assert_eq!( - namespace.reserve_tcp_translation(1, 1, 1), + reactor.reserve_pending_guest_connection(session_id), Err(BrokerError::ResourceExhausted) ); + reactor.tcp.stale_mapped_connections.clear(); + reactor + .tcp + .stale_mapped_connections + .try_reserve(MAX_TRACKED_GUEST_CONNECTIONS) + .unwrap(); + for mapping_index in 0..MAX_TRACKED_GUEST_CONNECTIONS { + reactor.tcp.stale_mapped_connections.insert( + ( + mapping_index, + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000), + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000), + ), + StaleTcpConnection { + session_id: foreign_session_id, + deadline: None, + retained_connector: None, + }, + ); + } assert_eq!( - namespace.reserve_tcp_translation(0, MAX_RETAINED_TRANSLATIONS, 1), + reactor.reserve_pending_guest_connection(session_id), Err(BrokerError::ResourceExhausted) ); } #[test] - fn stale_translation_ownership_is_aggregated_across_mappings() { + fn stale_guest_connection_ownership_is_aggregated_across_mappings() { let session_id = SessionId(7); let foreign_session_id = SessionId(8); let stale = [ @@ -4499,49 +4865,39 @@ mod tests { } #[test] - fn retiring_listener_removes_its_pending_tcp_translations() { - let mut namespace = SessionSocketNamespace::default(); + fn retiring_listener_removes_its_pending_guest_connections() { + let mut reactor = test_reactor(MAX_TRACKED_GUEST_CONNECTIONS, &[]); + let session_id = SessionId(7); let first_listener = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); let second_listener = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5001); for (guest_port, listener) in [(1000, first_listener), (1001, second_listener)] { - namespace - .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) - .unwrap(); - namespace - .insert_binding( - SocketKind::Tcp, - guest_port, - GuestPortBinding { - socket_id: u64::from(guest_port), - guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), - host_address: Some(SocketAddrV4::new( - Ipv4Addr::LOCALHOST, - 40000 + guest_port, - )), - host_peer_address: None, - host_peer_mapping_index: None, - host_mapped: false, - }, - ) - .unwrap(); - namespace - .set_host_peer_address( - SocketKind::Tcp, - guest_port, - u64::from(guest_port), - listener, - u64::from(guest_port), - None, - ) - .unwrap(); + insert_test_pending_guest_connection( + &mut reactor, + session_id, + guest_port, + u64::from(guest_port), + SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000 + guest_port), + listener, + u64::from(guest_port), + None, + ); } - namespace.remove_tcp_translations_for_listener(first_listener); + reactor.remove_pending_guest_connections_for_listener(first_listener); - assert_eq!(namespace.tcp_translations.len(), 1); + assert_eq!(reactor.tcp.pending_guest_connections.len(), 1); + assert_eq!( + reactor + .sessions + .get(&session_id) + .unwrap() + .pending_guest_connections, + 1 + ); assert_eq!( - namespace - .take_tcp_peer_translation( + reactor + .tcp + .take_pending_guest_connection( SocketAddrV4::new(Ipv4Addr::LOCALHOST, 41001), second_listener, ) @@ -4549,173 +4905,182 @@ mod tests { .guest_address, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1001) ); - assert!(namespace.tcp_translations.is_empty()); + assert!(reactor.tcp.pending_guest_connections.is_empty()); } #[test] - fn moving_live_translation_keeps_nonexpiring_session_identity() { - let mut namespace = SessionSocketNamespace::default(); + fn moving_live_pending_guest_connection_keeps_nonexpiring_session_identity() { + let mapping = TcpPortMapping { + guest_port: 5000, + broker_port: 5000, + }; + let mut reactor = test_reactor(MAX_TRACKED_GUEST_CONNECTIONS, &[mapping]); let session_id = SessionId(7); let guest_port = 1000; let socket_id = 1; let host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); let listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); - namespace - .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) - .unwrap(); - namespace - .insert_binding( - SocketKind::Tcp, - guest_port, - GuestPortBinding { - socket_id, - guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), - host_address: Some(host_address), - host_peer_address: None, - host_peer_mapping_index: None, - host_mapped: false, - }, - ) - .unwrap(); - namespace - .set_host_peer_address( - SocketKind::Tcp, - guest_port, - socket_id, - listener_address, - 2, - Some(0), - ) - .unwrap(); - namespace.retire_tcp_translation_for_connector( - SocketKind::Tcp, + insert_test_pending_guest_connection( + &mut reactor, + session_id, + guest_port, + socket_id, + host_address, + listener_address, + 2, + Some(0), + ); + reactor.retire_pending_guest_connection_for_connector( + session_id, guest_port, socket_id, false, None, Some(eventfd(0, EventfdFlags::CLOEXEC).unwrap()), ); - let mut stale_connections = HashMap::new(); - stale_connections - .try_reserve(MAX_RETAINED_TRANSLATIONS) + + reactor.move_pending_guest_connections_for_listener(listener_address, 0); + + let stale = reactor + .tcp + .stale_mapped_connections + .get(&(0, host_address, listener_address)) .unwrap(); + assert_eq!(stale.session_id, session_id); + assert_eq!(stale.deadline, None); + assert!(stale.retained_connector.is_some()); + let session = reactor.sessions.get(&session_id).unwrap(); + assert_eq!(session.pending_guest_connections, 0); + assert_eq!(session.retained_connectors, 1); + } + + #[test] + fn retiring_failed_connector_removes_its_pending_guest_connection() { + let mut reactor = test_reactor(MAX_TRACKED_GUEST_CONNECTIONS, &[]); + let session_id = SessionId(7); + let guest_port = 1000; + let socket_id = 1; + let host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); + let listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); + insert_test_pending_guest_connection( + &mut reactor, + session_id, + guest_port, + socket_id, + host_address, + listener_address, + 2, + None, + ); + + reactor.remove_pending_guest_connection_for_connector( + session_id, + SocketKind::Tcp, + guest_port, + socket_id, + ); + assert!(reactor.tcp.pending_guest_connections.is_empty()); + assert_eq!( + reactor + .tcp + .bindings + .get(&guest_port) + .unwrap() + .host_peer_address, + None + ); assert_eq!( - namespace.move_tcp_translations_for_listener( - session_id, - listener_address, - &mut stale_connections, - ), + reactor + .sessions + .get(&session_id) + .unwrap() + .pending_guest_connections, 0 ); - - let stale = stale_connections - .get(&(host_address, listener_address)) - .unwrap(); - assert_eq!(stale.session_id, session_id); - assert_eq!(stale.deadline, None); - assert!(stale.retained_connector.is_some()); - assert_eq!(namespace.retained_connectors, 1); } #[test] - fn retiring_failed_connector_removes_its_tcp_translation() { - let mut namespace = SessionSocketNamespace::default(); + fn aborting_connector_marks_its_pending_guest_connection_for_discard() { + let mut reactor = test_reactor(MAX_TRACKED_GUEST_CONNECTIONS, &[]); + let session_id = SessionId(7); let guest_port = 1000; let socket_id = 1; let host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); let listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); - namespace - .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) - .unwrap(); - namespace - .insert_binding( - SocketKind::Tcp, - guest_port, - GuestPortBinding { - socket_id, - guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), - host_address: Some(host_address), - host_peer_address: None, - host_peer_mapping_index: None, - host_mapped: false, - }, - ) - .unwrap(); - namespace - .set_host_peer_address( - SocketKind::Tcp, - guest_port, - socket_id, - listener_address, - 2, - None, - ) - .unwrap(); - - let _ = - namespace.remove_tcp_translation_for_connector(SocketKind::Tcp, guest_port, socket_id); + insert_test_pending_guest_connection( + &mut reactor, + session_id, + guest_port, + socket_id, + host_address, + listener_address, + 2, + None, + ); - assert!(namespace.tcp_translations.is_empty()); - assert_eq!( - namespace.tcp.get(&guest_port).unwrap().host_peer_address, - None + let _ = reactor.retire_pending_guest_connection_for_connector( + session_id, + guest_port, + socket_id, + true, + None, + Some(eventfd(0, EventfdFlags::CLOEXEC).unwrap()), ); + + let pending = reactor + .tcp + .take_pending_guest_connection(host_address, listener_address) + .unwrap(); + assert!(pending.discard_on_accept); + assert!(pending.retained_connector.is_some()); } #[test] - fn aborting_connector_marks_its_translation_for_discard() { - let mut namespace = SessionSocketNamespace::default(); + fn closing_session_keeps_established_discard_marker_until_accept() { + let mut reactor = test_reactor(MAX_TRACKED_GUEST_CONNECTIONS, &[]); + let session_id = SessionId(7); let guest_port = 1000; let socket_id = 1; let host_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 40000); let listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5000); - namespace - .reserve_tcp_translation(0, 0, MAX_RETAINED_TRANSLATIONS) - .unwrap(); - namespace - .insert_binding( - SocketKind::Tcp, - guest_port, - GuestPortBinding { - socket_id, - guest_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_port), - host_address: Some(host_address), - host_peer_address: None, - host_peer_mapping_index: None, - host_mapped: false, - }, - ) - .unwrap(); - namespace - .set_host_peer_address( - SocketKind::Tcp, - guest_port, - socket_id, - listener_address, - 2, - None, - ) - .unwrap(); - - let _ = namespace.retire_tcp_translation_for_connector( - SocketKind::Tcp, + insert_test_pending_guest_connection( + &mut reactor, + session_id, guest_port, socket_id, - true, + host_address, + listener_address, + 2, + None, + ); + reactor.retire_pending_guest_connection_for_connector( + session_id, + guest_port, + socket_id, + false, None, Some(eventfd(0, EventfdFlags::CLOEXEC).unwrap()), ); - let translation = namespace - .take_tcp_peer_translation(host_address, listener_address) + reactor.retire_session_connectors(session_id); + reactor.expire_deadlined_state( + Instant::now() + PENDING_CONNECT_DISCARD_LIFETIME + Duration::from_secs(1), + ); + + let pending = reactor + .tcp + .pending_guest_connections + .get(&(host_address, listener_address)) .unwrap(); - assert!(translation.discard_on_accept); - assert!(translation.retained_connector.is_some()); + assert!(pending.discard_on_accept); + assert_eq!(pending.discard_deadline, None); + assert!(pending.retained_connector.is_none()); + assert_eq!(reactor.retained_connectors, 0); } #[test] - fn guest_tcp_ports_are_session_scoped_and_do_not_bind_host_ports() { + fn guest_tcp_ports_are_broker_wide_and_do_not_bind_private_host_ports() { let occupied_host_listener = TcpListener::bind("127.0.0.1:0").unwrap(); let occupied_host_address = socket_address_v4(occupied_host_listener.local_addr().unwrap()); let provider = Arc::new(LinuxSocketProvider::new(4, 4).unwrap()); @@ -4746,7 +5111,7 @@ mod tests { ); assert_eq!( litebox_broker_core::socket::bind(&second_session, second, guest_port_80), - Ok(SocketOutcome::Completed(guest_port_80)) + Ok(SocketOutcome::Failed(SocketError::AddressInUse)) ); assert_eq!( litebox_broker_core::socket::bind( @@ -4759,7 +5124,7 @@ mod tests { } #[test] - fn guest_tcp_loopback_routes_within_the_session_namespace() { + fn guest_tcp_loopback_routes_across_sessions() { let provider = Arc::new(LinuxSocketProvider::new(3, 3).unwrap()); let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) @@ -4768,103 +5133,117 @@ mod tests { provider.clone(), ) .unwrap(); - let session = broker + let listener_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let client_session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); let (published, publications) = channel(); let (retired, retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); - let listener = create_socket(&session, readiness.clone()); + let listener = create_socket(&listener_session, readiness.clone()); let guest_listener_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); assert_eq!( - litebox_broker_core::socket::bind(&session, listener, guest_listener_address), + litebox_broker_core::socket::bind(&listener_session, listener, guest_listener_address,), Ok(SocketOutcome::Completed(guest_listener_address)) ); assert_eq!( - litebox_broker_core::socket::listen(&session, listener, 2), + litebox_broker_core::socket::listen(&listener_session, listener, 2), Ok(SocketOutcome::Completed(guest_listener_address)) ); - let client = create_socket(&session, readiness.clone()); + let client = create_socket(&client_session, readiness.clone()); let connect = - litebox_broker_core::socket::connect(&session, client, guest_listener_address).unwrap(); + litebox_broker_core::socket::connect(&client_session, client, guest_listener_address) + .unwrap(); assert!(matches!( connect, SocketOutcome::Completed( SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected ) )); - wait_until_connected(&session, client, &publications); - let client_address = litebox_broker_core::socket::status(&session, client) + wait_until_connected(&client_session, client, &publications); + let client_address = litebox_broker_core::socket::status(&client_session, client) .unwrap() .local_address .expect("connected client must have a guest-local address"); - session.close_object_reference(client).unwrap(); + client_session.close_object_reference(client).unwrap(); assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), client); assert_eq!(provider.reactor.retained_connector_count(), 1); - let replacement = create_socket(&session, readiness.clone()); + let replacement = create_socket(&client_session, readiness.clone()); let replacement_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_GUEST_EPHEMERAL_PORT + 1); assert_eq!( - litebox_broker_core::socket::bind(&session, replacement, replacement_address), + litebox_broker_core::socket::bind(&client_session, replacement, replacement_address,), Ok(SocketOutcome::Completed(replacement_address)) ); - let connect = - litebox_broker_core::socket::connect(&session, replacement, guest_listener_address) - .unwrap(); + let connect = litebox_broker_core::socket::connect( + &client_session, + replacement, + guest_listener_address, + ) + .unwrap(); assert!(matches!( connect, SocketOutcome::Completed( SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected ) )); - wait_until_connected(&session, replacement, &publications); - if !session + wait_until_connected(&client_session, replacement, &publications); + if !listener_session .check_readiness(listener) .unwrap() .contains(ReadinessFlags::READ) { wait_for_readiness(&publications, listener, ReadinessFlags::READ); } - let accepted = - match litebox_broker_core::socket::accept(&session, listener, readiness.clone()) - .unwrap() - { - SocketOutcome::Completed(accepted) => accepted, - SocketOutcome::Failed(error) => panic!("accept failed: {error:?}"), - }; + let accepted = match litebox_broker_core::socket::accept( + &listener_session, + listener, + readiness.clone(), + ) + .unwrap() + { + SocketOutcome::Completed(accepted) => accepted, + SocketOutcome::Failed(error) => panic!("accept failed: {error:?}"), + }; assert_eq!(accepted.local_address, guest_listener_address); assert_eq!(accepted.remote_address, client_address); assert_eq!(provider.reactor.retained_connector_count(), 0); - session.close_object_reference(accepted.handle).unwrap(); + listener_session + .close_object_reference(accepted.handle) + .unwrap(); assert_eq!( retirements.recv_timeout(TEST_TIMEOUT).unwrap(), accepted.handle ); let accepted = - match litebox_broker_core::socket::accept(&session, listener, readiness).unwrap() { + match litebox_broker_core::socket::accept(&listener_session, listener, readiness) + .unwrap() + { SocketOutcome::Completed(accepted) => accepted, SocketOutcome::Failed(error) => panic!("replacement accept failed: {error:?}"), }; assert_eq!(accepted.local_address, guest_listener_address); assert_eq!(accepted.remote_address, replacement_address); assert_eq!( - litebox_broker_core::socket::status(&session, replacement) + litebox_broker_core::socket::status(&client_session, replacement) .unwrap() .local_address, Some(replacement_address) ); assert_eq!( - send_bytes(&session, replacement, b"x", SendFlags::NONE), + send_bytes(&client_session, replacement, b"x", SendFlags::NONE), Ok(SocketOutcome::Completed(1)) ); wait_for_readiness(&publications, accepted.handle, ReadinessFlags::READ); let mut byte = [0]; assert_eq!( receive_into( - &session, + &listener_session, accepted.handle, &mut byte, ReceiveFlags::NONE, @@ -4876,16 +5255,76 @@ mod tests { assert_eq!(byte, *b"x"); } + #[test] + fn closing_connector_session_preserves_cross_session_discard_marker() { + let provider = Arc::new(LinuxSocketProvider::new(2, 2).unwrap()); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(4, 0, 2, 2), + provider.clone(), + ) + .unwrap(); + let listener_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let connector_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, publications) = channel(); + let (retired, _retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080); + let listener = create_socket(&listener_session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::bind(&listener_session, listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&listener_session, listener, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + let connector = create_socket(&connector_session, readiness.clone()); + assert!(matches!( + litebox_broker_core::socket::connect(&connector_session, connector, guest_address), + Ok(SocketOutcome::Completed( + SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected + )) + )); + wait_until_connected(&connector_session, connector, &publications); + connector_session.close_object_reference(connector).unwrap(); + assert_eq!(provider.reactor.pending_guest_connection_count(), 1); + assert_eq!(provider.reactor.retained_connector_count(), 1); + + drop(connector_session); + assert_eq!(provider.reactor.pending_guest_connection_count(), 1); + assert_eq!(provider.reactor.retained_connector_count(), 0); + if !listener_session + .check_readiness(listener) + .unwrap() + .contains(ReadinessFlags::READ) + { + wait_for_readiness(&publications, listener, ReadinessFlags::READ); + } + assert!(matches!( + litebox_broker_core::socket::accept(&listener_session, listener, readiness), + Err(BrokerError::WouldBlock) + )); + assert_eq!(provider.reactor.pending_guest_connection_count(), 0); + listener_session.close_object_reference(listener).unwrap(); + } + #[test] fn mapped_tcp_listener_can_be_replaced_while_an_accepted_socket_remains() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_tcp_port_publications( + LinuxSocketProvider::new_with_tcp_port_mappings( 2, 2, - &[TcpPortPublication { + *host_address.ip(), + &[TcpPortMapping { + broker_port: host_address.port(), guest_port: 80, - external_address: host_address, }], ) .unwrap(), @@ -4903,7 +5342,7 @@ mod tests { let (published, publications) = channel(); let (retired, retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); - let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let guest_address = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 80); let listener = create_socket(&session, readiness.clone()); assert_eq!( litebox_broker_core::socket::bind(&session, listener, guest_address), @@ -4929,9 +5368,10 @@ mod tests { ); assert_eq!( create_port_mapping_reservation( - TcpPortPublication { + *host_address.ip(), + TcpPortMapping { + broker_port: host_address.port(), guest_port: 80, - external_address: host_address, }, true, true, @@ -4958,15 +5398,87 @@ mod tests { } #[test] - fn stopped_mapped_listener_cannot_route_into_another_session() { + fn unacknowledged_mapped_listen_retires_platform_ownership() { + let host_address = unused_tcp_address(); + let mapping = TcpPortMapping { + broker_port: host_address.port(), + guest_port: 80, + }; + let provider = Arc::new( + LinuxSocketProvider::new_with_tcp_port_mappings(2, 1, *host_address.ip(), &[mapping]) + .unwrap(), + ); + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) + .with_socket_policy(SocketPolicy::Ipv4Loopback), + BrokerCoreLimits::new_with_all_limits(4, 0, 2, 1), + provider.clone(), + ) + .unwrap(); + let first_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let second_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, _publications) = channel(); + let (retired, _retirements) = channel(); + let readiness = Arc::new(TestReadinessSink { published, retired }); + let guest_address = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, mapping.guest_port); + let first = create_socket(&first_session, readiness.clone()); + assert_eq!( + litebox_broker_core::socket::bind(&first_session, first, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + let first_id = provider + .reactor + .next_socket_id + .load(Ordering::Relaxed) + .checked_sub(1) + .unwrap(); + let (response, receive) = sync_channel(1); + drop(receive); + provider + .reactor + .commands + .send(ReactorCommand::Listen { + id: first_id, + backlog: 1, + mapping: Some(mapping), + response, + }) + .unwrap(); + provider.reactor.signal().unwrap(); + assert_eq!( + provider + .reactor + .host_address(SocketKind::Tcp, mapping.guest_port), + None + ); + first_session.close_object_reference(first).unwrap(); + + let second = create_socket(&second_session, readiness); + assert_eq!( + litebox_broker_core::socket::bind(&second_session, second, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&second_session, second, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); + } + + #[test] + fn stopped_mapped_listener_keeps_global_guest_port_until_close() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_tcp_port_publications( + LinuxSocketProvider::new_with_tcp_port_mappings( 3, 2, - &[TcpPortPublication { + *host_address.ip(), + &[TcpPortMapping { + broker_port: host_address.port(), guest_port: 80, - external_address: host_address, }], ) .unwrap(), @@ -4987,7 +5499,7 @@ mod tests { let (published, _publications) = channel(); let (retired, _retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); - let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let guest_address = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 80); let first_listener = create_socket(&first_session, readiness.clone()); assert_eq!( litebox_broker_core::socket::bind(&first_session, first_listener, guest_address), @@ -5009,15 +5521,12 @@ mod tests { let second_listener = create_socket(&second_session, readiness.clone()); assert_eq!( litebox_broker_core::socket::bind(&second_session, second_listener, guest_address), - Ok(SocketOutcome::Completed(guest_address)) - ); - assert_eq!( - litebox_broker_core::socket::listen(&second_session, second_listener, 1), - Ok(SocketOutcome::Completed(guest_address)) + Ok(SocketOutcome::Failed(SocketError::AddressInUse)) ); let first_client = create_socket(&first_session, readiness); + let guest_destination = SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_address.port()); assert_eq!( - litebox_broker_core::socket::connect(&first_session, first_client, guest_address), + litebox_broker_core::socket::connect(&first_session, first_client, guest_destination,), Ok(SocketOutcome::Completed(SocketConnectionStatus::Failed( SocketError::ConnectionRefused, ))) @@ -5027,6 +5536,14 @@ mod tests { first_session .close_object_reference(first_listener) .unwrap(); + assert_eq!( + litebox_broker_core::socket::bind(&second_session, second_listener, guest_address), + Ok(SocketOutcome::Completed(guest_address)) + ); + assert_eq!( + litebox_broker_core::socket::listen(&second_session, second_listener, 1), + Ok(SocketOutcome::Completed(guest_address)) + ); second_session .close_object_reference(second_listener) .unwrap(); @@ -5036,12 +5553,13 @@ mod tests { fn mapped_listener_handoff_drains_bounded_stale_connections() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_tcp_port_publications( + LinuxSocketProvider::new_with_tcp_port_mappings( 3, 2, - &[TcpPortPublication { + *host_address.ip(), + &[TcpPortMapping { + broker_port: host_address.port(), guest_port: 80, - external_address: host_address, }], ) .unwrap(), @@ -5063,7 +5581,7 @@ mod tests { let (published, publications) = channel(); let (retired, _retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); - let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let guest_address = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 80); let first_listener = create_socket(&first_session, readiness.clone()); assert_eq!( litebox_broker_core::socket::bind(&first_session, first_listener, guest_address), @@ -5073,15 +5591,16 @@ mod tests { litebox_broker_core::socket::listen(&first_session, first_listener, 1), Ok(SocketOutcome::Completed(guest_address)) ); - let first_client = create_socket(&first_session, readiness.clone()); + let first_client = create_socket(&second_session, readiness.clone()); + let guest_destination = SocketAddrV4::new(Ipv4Addr::LOCALHOST, guest_address.port()); assert!(matches!( - litebox_broker_core::socket::connect(&first_session, first_client, guest_address,), + litebox_broker_core::socket::connect(&second_session, first_client, guest_destination,), Ok(SocketOutcome::Completed( SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected )) )); - wait_until_connected(&first_session, first_client, &publications); - assert_eq!(provider.reactor.tcp_translation_count(), 1); + wait_until_connected(&second_session, first_client, &publications); + assert_eq!(provider.reactor.pending_guest_connection_count(), 1); assert_eq!( litebox_broker_core::socket::shutdown( &first_session, @@ -5090,24 +5609,24 @@ mod tests { ), Ok(SocketOutcome::Completed(())) ); - assert_eq!(provider.reactor.tcp_translation_count(), 0); - assert_eq!(provider.reactor.stale_tcp_translation_count(), 1); + assert_eq!(provider.reactor.pending_guest_connection_count(), 0); + assert_eq!(provider.reactor.stale_guest_connection_count(), 1); assert_eq!( litebox_broker_core::socket::shutdown( - &first_session, + &second_session, first_client, ShutdownMode::Abort, ), Ok(SocketOutcome::Completed(())) ); - first_session.close_object_reference(first_client).unwrap(); + second_session.close_object_reference(first_client).unwrap(); assert_eq!(provider.reactor.retained_connector_count(), 1); first_session .close_object_reference(first_listener) .unwrap(); drop(first_session); - assert_eq!(provider.reactor.retained_connector_count(), 0); - assert_eq!(provider.reactor.stale_tcp_translation_count(), 1); + assert_eq!(provider.reactor.retained_connector_count(), 1); + assert_eq!(provider.reactor.stale_guest_connection_count(), 1); let second_listener = create_socket(&second_session, readiness); assert_eq!( @@ -5118,7 +5637,7 @@ mod tests { litebox_broker_core::socket::listen(&second_session, second_listener, 1), Ok(SocketOutcome::Completed(guest_address)) ); - assert_eq!(provider.reactor.stale_tcp_translation_count(), 0); + assert_eq!(provider.reactor.stale_guest_connection_count(), 0); assert_eq!(provider.reactor.retained_connector_count(), 0); second_session @@ -5127,54 +5646,74 @@ mod tests { } #[test] - fn stopped_private_listener_refuses_new_guest_connections() { - let provider = Arc::new(LinuxSocketProvider::new(2, 2).unwrap()); + fn stopped_private_listener_cleans_cross_session_pending_guest_connections() { + let provider = Arc::new(LinuxSocketProvider::new(3, 2).unwrap()); let socket_provider: Arc = provider.clone(); let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) .with_socket_policy(SocketPolicy::Ipv4Loopback), - BrokerCoreLimits::new_with_all_limits(4, 0, 2, 2), + BrokerCoreLimits::new_with_all_limits(6, 0, 3, 2), socket_provider, ) .unwrap(); - let session = broker + let listener_session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); - let (published, _publications) = channel(); + let client_session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (published, publications) = channel(); let (retired, _retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000); - let listener = create_socket(&session, readiness.clone()); + let listener = create_socket(&listener_session, readiness.clone()); assert_eq!( - litebox_broker_core::socket::bind(&session, listener, guest_address), + litebox_broker_core::socket::bind(&listener_session, listener, guest_address), Ok(SocketOutcome::Completed(guest_address)) ); assert_eq!( - litebox_broker_core::socket::listen(&session, listener, 1), + litebox_broker_core::socket::listen(&listener_session, listener, 1), Ok(SocketOutcome::Completed(guest_address)) ); + let connected_client = create_socket(&client_session, readiness.clone()); + assert!(matches!( + litebox_broker_core::socket::connect(&client_session, connected_client, guest_address,), + Ok(SocketOutcome::Completed( + SocketConnectionStatus::Connecting | SocketConnectionStatus::Connected + )) + )); + wait_until_connected(&client_session, connected_client, &publications); + assert_eq!(provider.reactor.pending_guest_connection_count(), 1); assert_eq!( - litebox_broker_core::socket::shutdown(&session, listener, ShutdownMode::StopListening,), + litebox_broker_core::socket::shutdown( + &listener_session, + listener, + ShutdownMode::StopListening, + ), Ok(SocketOutcome::Completed(())) ); + assert_eq!(provider.reactor.pending_guest_connection_count(), 0); - let client = create_socket(&session, readiness.clone()); + let client = create_socket(&client_session, readiness); assert_eq!( - litebox_broker_core::socket::connect(&session, client, guest_address), + litebox_broker_core::socket::connect(&client_session, client, guest_address), Ok(SocketOutcome::Completed(SocketConnectionStatus::Failed( SocketError::ConnectionRefused, ))) ); assert!( - session + client_session .check_readiness(client) .unwrap() .contains(ReadinessFlags::ERROR) ); - assert_eq!(provider.reactor.tcp_translation_count(), 0); + assert_eq!(provider.reactor.pending_guest_connection_count(), 0); - session.close_object_reference(client).unwrap(); - session.close_object_reference(listener).unwrap(); + client_session.close_object_reference(client).unwrap(); + client_session + .close_object_reference(connected_client) + .unwrap(); + listener_session.close_object_reference(listener).unwrap(); } #[test] @@ -5215,7 +5754,7 @@ mod tests { )) )); wait_until_connected(&session, client, &publications); - assert_eq!(provider.reactor.tcp_translation_count(), 1); + assert_eq!(provider.reactor.pending_guest_connection_count(), 1); assert_eq!( litebox_broker_core::socket::shutdown(&session, client, ShutdownMode::Abort), @@ -5223,7 +5762,7 @@ mod tests { ); session.close_object_reference(client).unwrap(); - assert_eq!(provider.reactor.tcp_translation_count(), 1); + assert_eq!(provider.reactor.pending_guest_connection_count(), 1); assert_eq!( litebox_broker_core::socket::create( &session, @@ -5241,7 +5780,7 @@ mod tests { litebox_broker_core::socket::accept(&session, listener, readiness.clone()), Err(BrokerError::WouldBlock) )); - assert_eq!(provider.reactor.tcp_translation_count(), 0); + assert_eq!(provider.reactor.pending_guest_connection_count(), 0); let replacement = create_socket(&session, readiness.clone()); session.close_object_reference(replacement).unwrap(); second_session @@ -5260,9 +5799,9 @@ mod tests { Ok(SocketOutcome::Completed(())) ); session.close_object_reference(retiring_client).unwrap(); - assert_eq!(provider.reactor.tcp_translation_count(), 1); + assert_eq!(provider.reactor.pending_guest_connection_count(), 1); session.close_object_reference(listener).unwrap(); - assert_eq!(provider.reactor.tcp_translation_count(), 0); + assert_eq!(provider.reactor.pending_guest_connection_count(), 0); let after_retirement = create_socket(&second_session, readiness); second_session .close_object_reference(after_retirement) @@ -5273,12 +5812,13 @@ mod tests { fn closing_mapped_listener_discards_more_than_sixty_four_queued_connections() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_tcp_port_publications( + LinuxSocketProvider::new_with_tcp_port_mappings( 2, 2, - &[TcpPortPublication { + *host_address.ip(), + &[TcpPortMapping { + broker_port: host_address.port(), guest_port: 80, - external_address: host_address, }], ) .unwrap(), @@ -5296,7 +5836,7 @@ mod tests { let (published, _publications) = channel(); let (retired, retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); - let guest_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80); + let guest_address = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 80); let listener = create_socket(&session, readiness.clone()); assert_eq!( litebox_broker_core::socket::bind(&session, listener, guest_address), @@ -5314,9 +5854,10 @@ mod tests { assert_eq!(retirements.recv_timeout(TEST_TIMEOUT).unwrap(), listener); assert_eq!( create_port_mapping_reservation( - TcpPortPublication { + *host_address.ip(), + TcpPortMapping { + broker_port: host_address.port(), guest_port: 80, - external_address: host_address, }, true, true, @@ -5354,8 +5895,10 @@ mod tests { let address = listener.local_addr().unwrap(); let (allow_response, response_allowed) = channel(); let (allow_end_of_stream, end_of_stream_allowed) = channel(); + let (peer_address, peer_addresses) = channel(); let server = thread::spawn(move || { - let (mut stream, _) = listener.accept().unwrap(); + let (mut stream, peer) = listener.accept().unwrap(); + peer_address.send(peer).unwrap(); stream.set_read_timeout(Some(TEST_TIMEOUT)).unwrap(); stream.set_write_timeout(Some(TEST_TIMEOUT)).unwrap(); let mut request = [0_u8; 4]; @@ -5393,7 +5936,11 @@ mod tests { assert_eq!(error.kind(), std::io::ErrorKind::ConnectionReset); }); - let provider = Arc::new(LinuxSocketProvider::new(8, 8).unwrap()); + let broker_ipv4_address = Ipv4Addr::new(127, 0, 0, 2); + let provider = Arc::new( + LinuxSocketProvider::new_with_tcp_port_mappings(8, 8, broker_ipv4_address, &[]) + .unwrap(), + ); let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()) .with_socket_policy(SocketPolicy::Ipv4Loopback), @@ -5418,12 +5965,16 @@ mod tests { ) )); wait_until_connected(&session, handle, &publications); + assert_eq!( + socket_address_v4(peer_addresses.recv_timeout(TEST_TIMEOUT).unwrap()).ip(), + &broker_ipv4_address + ); let status = litebox_broker_core::socket::status(&session, handle).unwrap(); assert_eq!(status.status, SocketConnectionStatus::Connected); let local_address = status .local_address .expect("connected socket must expose its local address"); - assert_eq!(*local_address.ip(), Ipv4Addr::LOCALHOST); + assert_eq!(*local_address.ip(), Ipv4Addr::UNSPECIFIED); assert_eq!(local_address.port(), FIRST_GUEST_EPHEMERAL_PORT); assert_eq!(status.pending_error, None); assert_eq!( @@ -5709,15 +6260,16 @@ mod tests { } #[test] - fn reactor_assigns_a_port_to_an_unbound_tcp_listener() { + fn unbound_listener_uses_identity_mapping_and_configured_override() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_tcp_port_publications( + LinuxSocketProvider::new_with_tcp_port_mappings( 2, 2, - &[TcpPortPublication { + *host_address.ip(), + &[TcpPortMapping { + broker_port: host_address.port(), guest_port: FIRST_GUEST_EPHEMERAL_PORT, - external_address: host_address, }], ) .unwrap(), @@ -5744,13 +6296,12 @@ mod tests { SocketOutcome::Failed(error) => panic!("listen failed: {error:?}"), }; assert_eq!(local_address.port(), FIRST_GUEST_EPHEMERAL_PORT + 1); - assert_eq!( - TcpListener::bind(host_address).unwrap_err().kind(), - ErrorKind::AddrInUse - ); + assert!(local_address.ip().is_unspecified()); + TcpStream::connect(SocketAddrV4::new(*host_address.ip(), local_address.port())).unwrap(); + drop(TcpListener::bind(host_address).unwrap()); let mapped_listener = create_socket(&session, readiness); let mapped_guest_address = - SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_GUEST_EPHEMERAL_PORT); + SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, FIRST_GUEST_EPHEMERAL_PORT); assert_eq!( litebox_broker_core::socket::set_tcp_option( &session, @@ -5797,9 +6348,10 @@ mod tests { ); assert_eq!( create_port_mapping_reservation( - TcpPortPublication { + *host_address.ip(), + TcpPortMapping { + broker_port: host_address.port(), guest_port: FIRST_GUEST_EPHEMERAL_PORT, - external_address: host_address, }, true, true, @@ -5811,15 +6363,16 @@ mod tests { } #[test] - fn reactor_drives_a_loopback_tcp_listener() { + fn reactor_drives_an_external_tcp_listener() { let host_address = unused_tcp_address(); let provider = Arc::new( - LinuxSocketProvider::new_with_tcp_port_publications( + LinuxSocketProvider::new_with_tcp_port_mappings( 4, 4, - &[TcpPortPublication { + *host_address.ip(), + &[TcpPortMapping { + broker_port: host_address.port(), guest_port: FIRST_GUEST_EPHEMERAL_PORT, - external_address: host_address, }], ) .unwrap(), @@ -5838,7 +6391,8 @@ mod tests { let (retired, retirements) = channel(); let readiness = Arc::new(TestReadinessSink { published, retired }); let listener = create_socket(&session, readiness.clone()); - let requested_address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, FIRST_GUEST_EPHEMERAL_PORT); + let requested_address = + SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, FIRST_GUEST_EPHEMERAL_PORT); let local_address = match litebox_broker_core::socket::bind(&session, listener, requested_address).unwrap() { diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 92e104d40..75ff37c4a 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -4,7 +4,7 @@ use std::error::Error; use std::ffi::OsString; use std::io::{Error as IoError, ErrorKind, Result as IoResult}; -use std::net::{Ipv4Addr, SocketAddrV4}; +use std::net::Ipv4Addr; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; use std::process::{Child, Command}; @@ -19,8 +19,7 @@ use std::time::{Duration, Instant}; use clap::Parser; use litebox_broker_core::{ BrokerCore, BrokerCoreLimits, CallerCredential, DestinationPortRange, DestinationRule, - Ipv4Cidr, ObjectRights, PolicyEngine, SocketPolicy, SocketPolicyError, - socket::TcpPortPublication, + Ipv4Cidr, ObjectRights, PolicyEngine, SocketPolicy, SocketPolicyError, socket::TcpPortMapping, }; use litebox_broker_host::{BrokerHostAssociation, ConnectionTermination, setup_connection}; use litebox_broker_platform_linux_userland::LinuxSocketProvider; @@ -86,31 +85,31 @@ impl FromStr for AllowedTcpDestination { } } -/// Command-line description of one external-to-guest TCP publication. +/// Command-line description of one broker-to-guest TCP port mapping. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct TcpPortPublicationArgument { - external_address: SocketAddrV4, +struct TcpPortMappingArgument { + broker_port: u16, guest_port: u16, } -impl FromStr for TcpPortPublicationArgument { +impl FromStr for TcpPortMappingArgument { type Err = String; fn from_str(value: &str) -> Result { - let (external_address, guest_port) = value - .rsplit_once(':') - .ok_or_else(|| "expected HOST_IP:HOST_PORT:GUEST_PORT".to_owned())?; - let external_address = external_address - .parse::() - .map_err(|error| format!("invalid external IPv4 endpoint: {error}"))?; + let (broker_port, guest_port) = value + .split_once(':') + .ok_or_else(|| "expected BROKER_PORT:GUEST_PORT".to_owned())?; + let broker_port = broker_port + .parse::() + .map_err(|error| format!("invalid broker port: {error}"))?; let guest_port = guest_port .parse::() .map_err(|error| format!("invalid guest port: {error}"))?; - if external_address.port() == 0 || guest_port == 0 { - return Err("published external and guest ports must be nonzero".to_owned()); + if broker_port == 0 || guest_port == 0 { + return Err("mapped broker and guest ports must be nonzero".to_owned()); } Ok(Self { - external_address, + broker_port, guest_port, }) } @@ -124,9 +123,12 @@ struct CliArgs { /// `0.0.0.0/0:1-65535` permits every nonzero IPv4 TCP destination. #[arg(long, value_name = "CIDR:PORT[-PORT]")] allow_tcp_destination: Vec, - /// Publish a host IPv4 TCP endpoint to a guest-local TCP port. - #[arg(long, value_name = "HOST_IP:HOST_PORT:GUEST_PORT")] - publish_tcp: Vec, + /// Host-facing IPv4 address used for TCP listeners and outbound TCP. + #[arg(long, default_value_t = Ipv4Addr::UNSPECIFIED, value_name = "IP")] + broker_ipv4_address: Ipv4Addr, + /// Map one TCP port on the broker IPv4 address to a guest-local TCP port. + #[arg(long, value_name = "BROKER_PORT:GUEST_PORT")] + tcp_port_mapping: Vec, /// Local runner executable to launch. #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::ExecutablePath)] runner: PathBuf, @@ -144,15 +146,16 @@ fn main() -> Result<(), Box> { let control_listener = UnixListener::bind(&control_socket_path)?; control_listener.set_nonblocking(true)?; let limits = BrokerCoreLimits::DEFAULT; - let tcp_port_publications = configured_tcp_port_publications(&args.publish_tcp); + let tcp_port_mappings = configured_tcp_port_mappings(&args.tcp_port_mapping); let broker = BrokerCore::new_with_limits( PolicyEngine::with_host_guaranteed_rights(ObjectRights::all()) .with_socket_policy(configured_socket_policy(&args.allow_tcp_destination)?), limits, - Arc::new(LinuxSocketProvider::new_with_tcp_port_publications( + Arc::new(LinuxSocketProvider::new_with_tcp_port_mappings( limits.max_sockets, limits.max_sockets_per_session, - &tcp_port_publications, + args.broker_ipv4_address, + &tcp_port_mappings, )?), )?; @@ -178,11 +181,11 @@ fn main() -> Result<(), Box> { Ok(()) } -fn configured_tcp_port_publications(tcp: &[TcpPortPublicationArgument]) -> Vec { +fn configured_tcp_port_mappings(tcp: &[TcpPortMappingArgument]) -> Vec { tcp.iter() - .map(|publication| TcpPortPublication { - guest_port: publication.guest_port, - external_address: publication.external_address, + .map(|mapping| TcpPortMapping { + broker_port: mapping.broker_port, + guest_port: mapping.guest_port, }) .collect() } @@ -612,34 +615,28 @@ mod tests { } #[test] - fn tcp_publication_arguments_name_distinct_external_and_guest_ports() { - let publication = "127.0.0.1:8080:80" - .parse::() - .unwrap(); + fn tcp_port_mapping_arguments_name_distinct_broker_and_guest_ports() { + let mapping = "8080:80".parse::().unwrap(); assert_eq!( - publication, - TcpPortPublicationArgument { - external_address: "127.0.0.1:8080".parse().unwrap(), + mapping, + TcpPortMappingArgument { + broker_port: 8080, guest_port: 80, } ); + assert!("0:80".parse::().is_err()); + assert!("8080:0".parse::().is_err()); assert!( - "127.0.0.1:0:80" - .parse::() - .is_err() - ); - assert!( - "127.0.0.1:8080:0" - .parse::() + "127.0.0.1:8080:80" + .parse::() .is_err() ); - assert!("8080:80".parse::().is_err()); assert_eq!( - configured_tcp_port_publications(&[publication]), - vec![TcpPortPublication { + configured_tcp_port_mappings(&[mapping]), + vec![TcpPortMapping { + broker_port: 8080, guest_port: 80, - external_address: "127.0.0.1:8080".parse().unwrap(), }] ); } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 0478d22d3..8b14b0593 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -342,20 +342,22 @@ fn spawn_test_broker( policy: litebox_broker_core::PolicyEngine, connection_count: usize, ) -> TestBroker { - spawn_test_broker_with_tcp_port_publications( + spawn_test_broker_with_tcp_port_mappings( control_socket_path, policy, connection_count, + std::net::Ipv4Addr::LOCALHOST, Vec::new(), ) } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -fn spawn_test_broker_with_tcp_port_publications( +fn spawn_test_broker_with_tcp_port_mappings( control_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, connection_count: usize, - tcp_port_publications: Vec, + broker_ipv4_address: std::net::Ipv4Addr, + tcp_port_mappings: Vec, ) -> TestBroker { let _ = std::fs::remove_file(control_socket_path); @@ -374,10 +376,11 @@ fn spawn_test_broker_with_tcp_port_publications( policy, limits, std::sync::Arc::new( - litebox_broker_platform_linux_userland::LinuxSocketProvider::new_with_tcp_port_publications( + litebox_broker_platform_linux_userland::LinuxSocketProvider::new_with_tcp_port_mappings( limits.max_sockets, limits.max_sockets_per_session, - &tcp_port_publications, + broker_ipv4_address, + &tcp_port_mappings, ) .expect("failed to create broker test socket provider"), ), @@ -771,16 +774,17 @@ fn test_runner_broker_tcp_server_with_rewriter() { }; drop(host_listener); let control_socket_path = unique_test_socket_path("runner-broker-tcp-server-control"); - let broker = spawn_test_broker_with_tcp_port_publications( + let broker = spawn_test_broker_with_tcp_port_mappings( &control_socket_path, litebox_broker_core::PolicyEngine::with_host_guaranteed_rights( litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(litebox_broker_core::SocketPolicy::Ipv4Loopback), 1, - vec![litebox_broker_core::socket::TcpPortPublication { + *host_address.ip(), + vec![litebox_broker_core::socket::TcpPortMapping { + broker_port: host_address.port(), guest_port: GUEST_PORT, - external_address: host_address, }], ); let mut child = Runner::new(&target, "broker_tcp_server_rewriter") diff --git a/litebox_runner_linux_userland/tests/tcp_broker.c b/litebox_runner_linux_userland/tests/tcp_broker.c index 2def56638..64abaa9cc 100644 --- a/litebox_runner_linux_userland/tests/tcp_broker.c +++ b/litebox_runner_linux_userland/tests/tcp_broker.c @@ -44,7 +44,7 @@ int main(int argc, char **argv) { socklen_t connecting_address_length = sizeof(connecting_address); assert(getsockname(fd, (struct sockaddr *)&connecting_address, &connecting_address_length) == 0); - assert(connecting_address.sin_addr.s_addr == htonl(INADDR_LOOPBACK)); + assert(connecting_address.sin_addr.s_addr == htonl(INADDR_ANY)); assert(connecting_address.sin_port != 0); int epoll_fd = epoll_create1(EPOLL_CLOEXEC); assert(epoll_fd >= 0); @@ -63,7 +63,7 @@ int main(int argc, char **argv) { socklen_t address_length = sizeof(local_address); assert(getsockname(fd, (struct sockaddr *)&local_address, &address_length) == 0); assert(local_address.sin_family == AF_INET); - assert(local_address.sin_addr.s_addr == htonl(INADDR_LOOPBACK)); + assert(local_address.sin_addr.s_addr == htonl(INADDR_ANY)); assert(local_address.sin_port != 0); assert(accept(fd, NULL, NULL) == -1); assert(errno == EINVAL); diff --git a/litebox_runner_linux_userland/tests/tcp_broker_server.c b/litebox_runner_linux_userland/tests/tcp_broker_server.c index 1e07b7dff..8d2fb95c8 100644 --- a/litebox_runner_linux_userland/tests/tcp_broker_server.c +++ b/litebox_runner_linux_userland/tests/tcp_broker_server.c @@ -72,7 +72,7 @@ int main(int argc, char **argv) { assert(listener >= 0); struct sockaddr_in local = { .sin_family = AF_INET, - .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + .sin_addr.s_addr = htonl(INADDR_ANY), .sin_port = htons((uint16_t)guest_port), }; assert(bind(listener, (const struct sockaddr *)&local, sizeof(local)) == 0); @@ -81,7 +81,7 @@ int main(int argc, char **argv) { socklen_t length = sizeof(local); assert(getsockname(listener, (struct sockaddr *)&local, &length) == 0); assert(length == sizeof(local)); - assert(local.sin_addr.s_addr == htonl(INADDR_LOOPBACK)); + assert(local.sin_addr.s_addr == htonl(INADDR_ANY)); assert(local.sin_port != 0); assert(accept(listener, NULL, NULL) == -1);