Public release release-98d969b26488
Source commit: 98d969b26488b4cda8b2381fa870276a00ca98ea Public tree identity: sha256:d02dd1e8d3547a489bb300741bed544bdc60bb8fe0bd541fd43ce9bfa7129a3e
This commit is contained in:
commit
8ded2b6a42
210 changed files with 76954 additions and 0 deletions
200
crates/clusterflux-coordinator/src/service/tcp.rs
Normal file
200
crates/clusterflux-coordinator/src/service/tcp.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
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<Mutex<CoordinatorService>>,
|
||||
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<CoordinatorRequest, CoordinatorServiceError> {
|
||||
serde_json::from_str::<super::CoordinatorWireRequest>(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"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue