use std::io::{BufRead, BufReader, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::sync::{Arc, Mutex}; use super::{CoordinatorRequest, CoordinatorResponse, CoordinatorService, CoordinatorServiceError}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ClientAuthorityMode { Strict, LocalTrustedLoopback, } impl CoordinatorService { pub fn serve_tcp(self, listener: TcpListener) -> Result<(), CoordinatorServiceError> { if !listener.local_addr()?.ip().is_loopback() { return Err(CoordinatorServiceError::Protocol( "the native coordinator transport is plaintext and restricted to loopback; expose a remote coordinator only through a secure transport" .to_owned(), )); } self.serve_tcp_with_authority(listener, ClientAuthorityMode::Strict) } pub fn serve_tcp_local_trusted( self, listener: TcpListener, ) -> Result<(), CoordinatorServiceError> { if !listener.local_addr()?.ip().is_loopback() { return Err(CoordinatorServiceError::Protocol( "local trusted request mode is restricted to a loopback listener".to_owned(), )); } self.serve_tcp_with_authority(listener, ClientAuthorityMode::LocalTrustedLoopback) } fn serve_tcp_with_authority( self, listener: TcpListener, authority_mode: ClientAuthorityMode, ) -> Result<(), CoordinatorServiceError> { let shared = Arc::new(Mutex::new(self)); for stream in listener.incoming() { let stream = stream?; let service = Arc::clone(&shared); std::thread::spawn(move || { if let Err(err) = handle_shared_stream(service, stream, authority_mode) { eprintln!("coordinator stream failed: {err}"); } }); } Ok(()) } pub fn handle_stream(&mut self, stream: TcpStream) -> Result<(), CoordinatorServiceError> { self.handle_stream_with_authority(stream, ClientAuthorityMode::Strict) } #[cfg(test)] pub(super) fn handle_stream_local_trusted( &mut self, stream: TcpStream, ) -> Result<(), CoordinatorServiceError> { self.handle_stream_with_authority(stream, ClientAuthorityMode::LocalTrustedLoopback) } fn handle_stream_with_authority( &mut self, stream: TcpStream, authority_mode: ClientAuthorityMode, ) -> Result<(), CoordinatorServiceError> { let mut reader = BufReader::new(stream.try_clone()?); let mut writer = stream; loop { let mut line = String::new(); if reader.read_line(&mut line)? == 0 { return Ok(()); } if line.trim().is_empty() { continue; } let response = match decode_wire_request(&line) { Ok(request) => match authorize_client_request(&request, authority_mode) .and_then(|()| self.handle_request(request)) { Ok(response) => response, Err(err) => CoordinatorResponse::Error { message: err.to_string(), }, }, Err(err) => CoordinatorResponse::Error { message: err.to_string(), }, }; serde_json::to_writer(&mut writer, &response)?; writer.write_all(b"\n")?; writer.flush()?; } } } fn handle_shared_stream( service: Arc>, stream: TcpStream, authority_mode: ClientAuthorityMode, ) -> Result<(), CoordinatorServiceError> { let mut reader = BufReader::new(stream.try_clone()?); let mut writer = stream; loop { let mut line = String::new(); if reader.read_line(&mut line)? == 0 { return Ok(()); } if line.trim().is_empty() { continue; } let response = match decode_wire_request(&line) { Ok(request) => match authorize_client_request(&request, authority_mode) { Ok(()) => match service.lock() { Ok(mut service) => match service.handle_request(request) { Ok(response) => response, Err(err) => CoordinatorResponse::Error { message: err.to_string(), }, }, Err(_) => CoordinatorResponse::Error { message: "coordinator service lock poisoned".to_owned(), }, }, Err(err) => CoordinatorResponse::Error { message: err.to_string(), }, }, Err(err) => CoordinatorResponse::Error { message: err.to_string(), }, }; serde_json::to_writer(&mut writer, &response)?; writer.write_all(b"\n")?; writer.flush()?; } } pub fn bind_listener(addr: &str) -> Result<(TcpListener, SocketAddr), CoordinatorServiceError> { let listener = TcpListener::bind(addr)?; let addr = listener.local_addr()?; Ok((listener, addr)) } fn decode_wire_request(line: &str) -> Result { serde_json::from_str::(line)? .into_request() .map_err(CoordinatorServiceError::Protocol) } fn authorize_client_request( request: &CoordinatorRequest, authority_mode: ClientAuthorityMode, ) -> Result<(), CoordinatorServiceError> { if authority_mode == ClientAuthorityMode::LocalTrustedLoopback { return Ok(()); } match request { CoordinatorRequest::Ping | CoordinatorRequest::Authenticated { .. } | CoordinatorRequest::ExchangeNodeEnrollmentGrant { .. } | CoordinatorRequest::SignedNode { .. } | CoordinatorRequest::NodeHeartbeat { node_signature: Some(_), .. } | CoordinatorRequest::StartProcess { actor_agent: Some(_), agent_signature: Some(_), .. } | CoordinatorRequest::LaunchTask { actor_agent: Some(_), agent_signature: Some(_), .. } | CoordinatorRequest::AdminStatus { .. } | CoordinatorRequest::SuspendTenant { .. } => Ok(()), _ => Err(CoordinatorServiceError::Protocol( "strict Core Client authority requires an authenticated CLI session, signed Agent, signed Node, enrollment grant exchange, or admin credential; request-body identity fields are not authority" .to_owned(), )), } } #[cfg(test)] mod transport_boundary_tests { use super::*; #[test] fn native_plaintext_service_refuses_non_loopback_listener() { let (listener, _) = bind_listener("0.0.0.0:0").unwrap(); let error = CoordinatorService::new(1).serve_tcp(listener).unwrap_err(); assert!(error.to_string().contains("restricted to loopback")); } }