2026-07-15 17:54:46 +09:00
|
|
|
use std::fs::{File, OpenOptions};
|
|
|
|
|
use std::io::Write;
|
|
|
|
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
2026-07-16 17:49:33 +09:00
|
|
|
use std::sync::{Arc, Mutex};
|
2026-07-15 17:54:46 +09:00
|
|
|
use std::thread::{self, JoinHandle};
|
|
|
|
|
|
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
|
|
|
use lazaret_core::identity::{
|
|
|
|
|
ClientId, ConfigurationFingerprint, CoordinatorId, DomainId, GenerationId, HostPath,
|
2026-07-16 17:49:33 +09:00
|
|
|
NetworkSidecarId, SandboxId, SessionAnchorId, SessionId,
|
2026-07-15 17:54:46 +09:00
|
|
|
};
|
|
|
|
|
use lazaret_core::records::{
|
|
|
|
|
ClientEnrollmentResponse, ControlRequest, ControlResponse, Domain, DurableDecision, Generation,
|
2026-07-16 17:49:33 +09:00
|
|
|
GenerationIdentity, ProtocolVersion, Session, SessionBinding, SessionPhase,
|
|
|
|
|
SessionRegistrationRequest, SessionRegistrationResponse, TlsCredential,
|
2026-07-15 17:54:46 +09:00
|
|
|
};
|
|
|
|
|
use lazaret_host_coordinator::control::{
|
2026-07-16 17:29:23 +09:00
|
|
|
CONTROL_ALPN, ControlError, PendingClientEnrollment, PublishedControlFace, SessionRegistry,
|
|
|
|
|
StagedControlFace, TrustAnchor,
|
2026-07-15 17:54:46 +09:00
|
|
|
};
|
|
|
|
|
use lazaret_host_coordinator::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
|
2026-07-16 17:49:33 +09:00
|
|
|
use lazaret_host_coordinator::lifecycle::{
|
2026-07-16 17:53:54 +09:00
|
|
|
DomainPhase, HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter,
|
2026-07-16 17:49:33 +09:00
|
|
|
};
|
2026-07-15 17:54:46 +09:00
|
|
|
use lazaret_linux::filesystem::try_exclusive_flock;
|
|
|
|
|
use rustix::process::getuid;
|
|
|
|
|
use x509_parser::extensions::GeneralName;
|
|
|
|
|
use x509_parser::parse_x509_certificate;
|
|
|
|
|
|
|
|
|
|
static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0);
|
|
|
|
|
|
2026-07-16 17:49:33 +09:00
|
|
|
#[derive(Clone, Default)]
|
|
|
|
|
struct ControlLifecycleAdapter {
|
|
|
|
|
publication: Arc<Mutex<Option<Arc<PublishedControlFace>>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl LifecycleAdapter for ControlLifecycleAdapter {
|
|
|
|
|
type Error = ControlError;
|
|
|
|
|
|
|
|
|
|
fn execute(
|
|
|
|
|
&mut self,
|
|
|
|
|
action: LifecycleAction,
|
|
|
|
|
_generation: &Generation,
|
|
|
|
|
) -> Result<(), Self::Error> {
|
2026-07-16 17:53:54 +09:00
|
|
|
if action == LifecycleAction::BeginTeardown {
|
|
|
|
|
let mut publication = self.publication.lock().unwrap();
|
|
|
|
|
if let Some(face) = publication.as_ref() {
|
|
|
|
|
face.withdraw()?;
|
|
|
|
|
}
|
|
|
|
|
publication.take();
|
2026-07-16 17:49:33 +09:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:54:46 +09:00
|
|
|
struct TestDirectory(PathBuf);
|
|
|
|
|
|
|
|
|
|
impl TestDirectory {
|
|
|
|
|
fn new(label: &str) -> Self {
|
|
|
|
|
let sequence = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed);
|
2026-07-19 05:12:57 +09:00
|
|
|
let parent = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/t/control");
|
|
|
|
|
std::fs::create_dir_all(&parent).unwrap();
|
|
|
|
|
let parent = parent.canonicalize().unwrap();
|
|
|
|
|
let path = parent.join(format!(
|
2026-07-15 17:54:46 +09:00
|
|
|
"lazaret-control-{label}-{}-{sequence}",
|
|
|
|
|
std::process::id()
|
|
|
|
|
));
|
|
|
|
|
std::fs::create_dir(&path).unwrap();
|
|
|
|
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
|
|
|
|
|
Self(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn path(&self, name: &str) -> PathBuf {
|
|
|
|
|
self.0.join(name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for TestDirectory {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
std::fs::remove_dir_all(&self.0).unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn bytes(value: u8) -> [u8; 32] {
|
|
|
|
|
[value; 32]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn generation_identity(generation: u64) -> GenerationIdentity {
|
|
|
|
|
GenerationIdentity {
|
|
|
|
|
agent_profile: "trusted".into(),
|
|
|
|
|
domain_id: DomainId::new(bytes(1)),
|
|
|
|
|
generation_id: GenerationId::new(generation),
|
|
|
|
|
configuration_fingerprint: ConfigurationFingerprint::new(bytes(2)),
|
|
|
|
|
coordinator_id: CoordinatorId::new(bytes(u8::try_from(generation + 10).unwrap())),
|
|
|
|
|
sandbox_id: SandboxId::new(bytes(u8::try_from(generation + 20).unwrap())),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn domain() -> Domain {
|
|
|
|
|
Domain {
|
|
|
|
|
domain_id: DomainId::new(bytes(1)),
|
|
|
|
|
profile_name: "trusted".into(),
|
|
|
|
|
canonical_workspace: HostPath::new(b"/workspace/project".to_vec()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn generation(identity: &GenerationIdentity, decision: DurableDecision) -> Generation {
|
|
|
|
|
Generation {
|
|
|
|
|
domain_id: identity.domain_id,
|
|
|
|
|
generation_id: identity.generation_id,
|
|
|
|
|
configuration_fingerprint: identity.configuration_fingerprint,
|
|
|
|
|
coordinator_id: identity.coordinator_id,
|
|
|
|
|
network_sidecar_id: NetworkSidecarId::new(bytes(4)),
|
|
|
|
|
sandbox_id: identity.sandbox_id,
|
|
|
|
|
durable_decision: decision,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open_store(
|
|
|
|
|
directory: &TestDirectory,
|
|
|
|
|
identity: &GenerationIdentity,
|
|
|
|
|
) -> Arc<HostCoordinatorDomainStore> {
|
|
|
|
|
let lock_path = directory.path("domain.lock");
|
|
|
|
|
let lock = OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.create_new(true)
|
|
|
|
|
.open(lock_path)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let lock = try_exclusive_flock(lock).unwrap();
|
|
|
|
|
let store = Arc::new(
|
|
|
|
|
HostCoordinatorDomainStore::open(lock, &directory.path("domain.redb"), &domain()).unwrap(),
|
|
|
|
|
);
|
|
|
|
|
store
|
|
|
|
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
|
|
|
|
identity,
|
|
|
|
|
DurableDecision::None,
|
|
|
|
|
))])
|
|
|
|
|
.unwrap();
|
|
|
|
|
store
|
|
|
|
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
|
|
|
|
identity,
|
|
|
|
|
DurableDecision::Preparing,
|
|
|
|
|
))])
|
|
|
|
|
.unwrap();
|
|
|
|
|
store
|
|
|
|
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
|
|
|
|
identity,
|
|
|
|
|
DurableDecision::Published,
|
|
|
|
|
))])
|
|
|
|
|
.unwrap();
|
|
|
|
|
store
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open_preparing_store(
|
|
|
|
|
directory: &TestDirectory,
|
|
|
|
|
identity: &GenerationIdentity,
|
|
|
|
|
) -> Arc<HostCoordinatorDomainStore> {
|
|
|
|
|
let lock = OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.create_new(true)
|
|
|
|
|
.open(directory.path("domain.lock"))
|
|
|
|
|
.unwrap();
|
|
|
|
|
let store = Arc::new(
|
|
|
|
|
HostCoordinatorDomainStore::open(
|
|
|
|
|
try_exclusive_flock(lock).unwrap(),
|
|
|
|
|
&directory.path("domain.redb"),
|
|
|
|
|
&domain(),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
for decision in [DurableDecision::None, DurableDecision::Preparing] {
|
|
|
|
|
store
|
|
|
|
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
|
|
|
|
identity, decision,
|
|
|
|
|
))])
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
store
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 17:49:33 +09:00
|
|
|
fn open_empty_store(directory: &TestDirectory) -> Arc<HostCoordinatorDomainStore> {
|
|
|
|
|
let lock = OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.create_new(true)
|
|
|
|
|
.open(directory.path("domain.lock"))
|
|
|
|
|
.unwrap();
|
|
|
|
|
Arc::new(
|
|
|
|
|
HostCoordinatorDomainStore::open(
|
|
|
|
|
try_exclusive_flock(lock).unwrap(),
|
|
|
|
|
&directory.path("domain.redb"),
|
|
|
|
|
&domain(),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn start_sandbox(
|
|
|
|
|
lifecycle: &mut HostCoordinatorLifecycle<'_, ControlLifecycleAdapter, Arc<SessionRegistry>>,
|
|
|
|
|
identity: &GenerationIdentity,
|
|
|
|
|
) {
|
|
|
|
|
let owner = Session {
|
|
|
|
|
generation_id: identity.generation_id,
|
|
|
|
|
session_id: SessionId::new(bytes(80)),
|
|
|
|
|
cli_id: ClientId::new(bytes(81)),
|
|
|
|
|
session_anchor_id: SessionAnchorId::new(bytes(82)),
|
|
|
|
|
session_phase: SessionPhase::New,
|
|
|
|
|
};
|
|
|
|
|
lifecycle
|
|
|
|
|
.start_new_domain(generation(identity, DurableDecision::None), owner)
|
|
|
|
|
.unwrap();
|
|
|
|
|
lifecycle.create_trust_anchor().unwrap();
|
|
|
|
|
lifecycle.start_runtime_supervisor().unwrap();
|
|
|
|
|
lifecycle.lease_prepared_lower().unwrap();
|
|
|
|
|
lifecycle.mount_state_anchor().unwrap();
|
|
|
|
|
lifecycle.start_sidecar().unwrap();
|
|
|
|
|
lifecycle.start_connectivity_helper().unwrap();
|
|
|
|
|
lifecycle.mark_network_ready().unwrap();
|
|
|
|
|
lifecycle.arm_runtime_causal_edge().unwrap();
|
|
|
|
|
lifecycle.start_sandbox().unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:54:46 +09:00
|
|
|
fn publish(
|
|
|
|
|
directory: &TestDirectory,
|
|
|
|
|
identity: &GenerationIdentity,
|
|
|
|
|
) -> (Arc<PublishedControlFace>, Arc<HostCoordinatorDomainStore>) {
|
|
|
|
|
let store = open_store(directory, identity);
|
|
|
|
|
let face = PublishedControlFace::publish(
|
|
|
|
|
&directory.0,
|
|
|
|
|
getuid().as_raw(),
|
|
|
|
|
TrustAnchor::create(identity.clone()).unwrap(),
|
|
|
|
|
Arc::clone(&store),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
(Arc::new(face), store)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn server(face: &Arc<PublishedControlFace>) -> JoinHandle<Result<(), ControlError>> {
|
|
|
|
|
let face = Arc::clone(face);
|
|
|
|
|
thread::spawn(move || face.serve_next())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct EnrolledClient {
|
|
|
|
|
pending: PendingClientEnrollment,
|
|
|
|
|
credential: TlsCredential,
|
|
|
|
|
server_certificate: Vec<u8>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn enroll(face: &Arc<PublishedControlFace>, cli_id: ClientId) -> EnrolledClient {
|
|
|
|
|
let pending = PendingClientEnrollment::create(
|
|
|
|
|
face.publication_identity().generation_identity.clone(),
|
|
|
|
|
cli_id,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let serving = server(face);
|
|
|
|
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
|
|
|
let mut stream =
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
|
|
|
.unwrap();
|
|
|
|
|
PublishedControlFace::send(
|
|
|
|
|
&mut stream,
|
|
|
|
|
&ControlRequest::Enroll(pending.request().clone()),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let response: ControlResponse = PublishedControlFace::receive(&mut stream).unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
stream.conn.protocol_version(),
|
|
|
|
|
Some(rustls::ProtocolVersion::TLSv1_3)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(stream.conn.alpn_protocol(), Some(CONTROL_ALPN));
|
|
|
|
|
let server_certificate = stream.conn.peer_certificates().unwrap()[0].to_vec();
|
|
|
|
|
drop(stream);
|
|
|
|
|
serving.join().unwrap().unwrap();
|
|
|
|
|
let ControlResponse::Enrolled(ClientEnrollmentResponse { credential, .. }) = response else {
|
|
|
|
|
panic!("unexpected enrollment response")
|
|
|
|
|
};
|
|
|
|
|
EnrolledClient {
|
|
|
|
|
pending,
|
|
|
|
|
credential,
|
|
|
|
|
server_certificate,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn assert_certificate_profiles(face: &PublishedControlFace, client: &EnrolledClient) {
|
|
|
|
|
let (_, ca) =
|
|
|
|
|
parse_x509_certificate(&face.publication_identity().tls_ca_certificate_der).unwrap();
|
|
|
|
|
assert!(ca.is_ca());
|
|
|
|
|
assert!(ca.key_usage().unwrap().unwrap().value.key_cert_sign());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ca.signature_algorithm.algorithm.to_id_string(),
|
|
|
|
|
"1.3.101.112"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let (_, server_certificate) = parse_x509_certificate(&client.server_certificate).unwrap();
|
|
|
|
|
let server_usage = server_certificate.extended_key_usage().unwrap().unwrap();
|
|
|
|
|
assert!(server_usage.value.server_auth);
|
|
|
|
|
assert!(!server_usage.value.client_auth);
|
|
|
|
|
let server_names = server_certificate
|
|
|
|
|
.subject_alternative_name()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
server_names
|
|
|
|
|
.value
|
|
|
|
|
.general_names
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|name| { matches!(name, GeneralName::DNSName("coordinator.lazaret.invalid")) })
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
server_certificate.validity().not_after.timestamp()
|
|
|
|
|
- server_certificate.validity().not_before.timestamp()
|
|
|
|
|
<= 3_660
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let (_, client_certificate) =
|
|
|
|
|
parse_x509_certificate(&client.credential.certificate_der).unwrap();
|
|
|
|
|
let client_usage = client_certificate.extended_key_usage().unwrap().unwrap();
|
|
|
|
|
assert!(client_usage.value.client_auth);
|
|
|
|
|
assert!(!client_usage.value.server_auth);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
client_certificate
|
|
|
|
|
.signature_algorithm
|
|
|
|
|
.algorithm
|
|
|
|
|
.to_id_string(),
|
|
|
|
|
"1.3.101.112"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
client_certificate.validity().not_after.timestamp()
|
|
|
|
|
- client_certificate.validity().not_before.timestamp()
|
|
|
|
|
<= 3_660
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn registration_binding(
|
|
|
|
|
identity: &GenerationIdentity,
|
|
|
|
|
cli_id: ClientId,
|
|
|
|
|
session: u8,
|
|
|
|
|
anchor: u8,
|
|
|
|
|
) -> SessionBinding {
|
|
|
|
|
SessionBinding {
|
|
|
|
|
generation_identity: identity.clone(),
|
|
|
|
|
cli_id,
|
|
|
|
|
session_id: SessionId::new(bytes(session)),
|
|
|
|
|
session_anchor_public_key: SigningKey::from_bytes(&bytes(anchor))
|
|
|
|
|
.verifying_key()
|
|
|
|
|
.to_bytes()
|
|
|
|
|
.to_vec(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open_registration(
|
|
|
|
|
face: &Arc<PublishedControlFace>,
|
|
|
|
|
client: &EnrolledClient,
|
|
|
|
|
binding: SessionBinding,
|
|
|
|
|
) -> (
|
|
|
|
|
rustls::StreamOwned<rustls::ClientConnection, std::os::unix::net::UnixStream>,
|
|
|
|
|
JoinHandle<Result<(), ControlError>>,
|
|
|
|
|
SessionRegistrationResponse,
|
|
|
|
|
) {
|
|
|
|
|
let serving = server(face);
|
|
|
|
|
let config = PublishedControlFace::client_config(
|
|
|
|
|
face.publication_identity(),
|
|
|
|
|
Some((&client.credential, client.pending.private_key())),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut stream =
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
|
|
|
.unwrap();
|
|
|
|
|
PublishedControlFace::send(
|
|
|
|
|
&mut stream,
|
|
|
|
|
&ControlRequest::Register(SessionRegistrationRequest {
|
|
|
|
|
protocol_version: ProtocolVersion::V1,
|
|
|
|
|
session_binding: binding,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let response: ControlResponse = PublishedControlFace::receive(&mut stream).unwrap();
|
|
|
|
|
let ControlResponse::Registered(response) = response else {
|
|
|
|
|
panic!("unexpected registration response")
|
|
|
|
|
};
|
|
|
|
|
(stream, serving, response)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn refused_registration(
|
|
|
|
|
face: &Arc<PublishedControlFace>,
|
|
|
|
|
client: &EnrolledClient,
|
|
|
|
|
binding: SessionBinding,
|
|
|
|
|
) -> ControlError {
|
|
|
|
|
let serving = server(face);
|
|
|
|
|
let config = PublishedControlFace::client_config(
|
|
|
|
|
face.publication_identity(),
|
|
|
|
|
Some((&client.credential, client.pending.private_key())),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut stream =
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
|
|
|
.unwrap();
|
|
|
|
|
PublishedControlFace::send(
|
|
|
|
|
&mut stream,
|
|
|
|
|
&ControlRequest::Register(SessionRegistrationRequest {
|
|
|
|
|
protocol_version: ProtocolVersion::V1,
|
|
|
|
|
session_binding: binding,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
|
|
|
|
drop(stream);
|
|
|
|
|
serving.join().unwrap().unwrap_err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn real_uds_tls_enrollment_and_registration_preserve_exact_profiles_and_session_state() {
|
|
|
|
|
let directory = TestDirectory::new("complete");
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let (face, store) = publish(&directory, &identity);
|
|
|
|
|
|
|
|
|
|
let socket_metadata = std::fs::symlink_metadata(face.socket_path()).unwrap();
|
|
|
|
|
let identity_metadata = std::fs::symlink_metadata(face.identity_path()).unwrap();
|
|
|
|
|
assert_eq!(socket_metadata.uid(), getuid().as_raw());
|
|
|
|
|
assert_eq!(socket_metadata.mode() & 0o7777, 0o600);
|
|
|
|
|
assert_eq!(identity_metadata.uid(), getuid().as_raw());
|
|
|
|
|
assert_eq!(identity_metadata.mode() & 0o7777, 0o600);
|
2026-07-16 16:28:18 +09:00
|
|
|
assert_eq!(face.socket_path().file_name().unwrap(), "c.sock");
|
2026-07-15 17:54:46 +09:00
|
|
|
assert_eq!(
|
|
|
|
|
face.identity_path().file_name().unwrap(),
|
|
|
|
|
"protocol-identity.postcard"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let client = enroll(&face, ClientId::new(bytes(30)));
|
|
|
|
|
assert_certificate_profiles(&face, &client);
|
|
|
|
|
|
|
|
|
|
let mut wrong_leaf_pin = face.publication_identity().clone();
|
|
|
|
|
wrong_leaf_pin.coordinator_tls_certificate_sha256 =
|
|
|
|
|
lazaret_core::identity::CertificateDigest::new(bytes(99));
|
|
|
|
|
let serving = server(&face);
|
|
|
|
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
|
|
|
assert!(PublishedControlFace::connect(face.socket_path(), config, &wrong_leaf_pin).is_err());
|
|
|
|
|
assert!(serving.join().unwrap().is_err());
|
|
|
|
|
|
|
|
|
|
let serving = server(&face);
|
|
|
|
|
let config = PublishedControlFace::client_config(
|
|
|
|
|
face.publication_identity(),
|
|
|
|
|
Some((&client.credential, client.pending.private_key())),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut stream =
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
|
|
|
.unwrap();
|
|
|
|
|
PublishedControlFace::send(
|
|
|
|
|
&mut stream,
|
|
|
|
|
&ControlRequest::Enroll(client.pending.request().clone()),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
|
|
|
|
drop(stream);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
serving.join().unwrap().unwrap_err(),
|
|
|
|
|
ControlError::InvalidRequest
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let binding = registration_binding(&identity, ClientId::new(bytes(30)), 31, 32);
|
|
|
|
|
let (stream, serving, response) = open_registration(&face, &client, binding.clone());
|
|
|
|
|
assert_eq!(response.session_capability.session_binding, binding);
|
|
|
|
|
assert_eq!(face.registry().connected_sessions().unwrap().len(), 1);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
store.snapshot().unwrap().sessions[0].session_phase,
|
|
|
|
|
SessionPhase::Attached
|
|
|
|
|
);
|
|
|
|
|
drop(stream);
|
|
|
|
|
serving.join().unwrap().unwrap();
|
|
|
|
|
assert!(face.registry().connected_sessions().unwrap().is_empty());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
store.snapshot().unwrap().sessions[0].session_phase,
|
|
|
|
|
SessionPhase::Closed
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn duplicate_session_cli_and_anchor_identities_are_refused_while_registered() {
|
|
|
|
|
let directory = TestDirectory::new("duplicates");
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let (face, _) = publish(&directory, &identity);
|
|
|
|
|
let first = enroll(&face, ClientId::new(bytes(40)));
|
|
|
|
|
let second = enroll(&face, ClientId::new(bytes(41)));
|
|
|
|
|
let mismatched_cli = registration_binding(&identity, ClientId::new(bytes(41)), 39, 40);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
refused_registration(&face, &first, mismatched_cli),
|
|
|
|
|
ControlError::InvalidCertificateProfile
|
|
|
|
|
));
|
|
|
|
|
let first_binding = registration_binding(&identity, ClientId::new(bytes(40)), 42, 43);
|
|
|
|
|
let (first_stream, first_server, _) = open_registration(&face, &first, first_binding.clone());
|
|
|
|
|
|
|
|
|
|
let duplicate_session = registration_binding(&identity, ClientId::new(bytes(41)), 42, 44);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
refused_registration(&face, &second, duplicate_session),
|
|
|
|
|
ControlError::DuplicateIdentity
|
|
|
|
|
));
|
|
|
|
|
let duplicate_cli = registration_binding(&identity, ClientId::new(bytes(40)), 45, 46);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
refused_registration(&face, &first, duplicate_cli),
|
|
|
|
|
ControlError::DuplicateIdentity
|
|
|
|
|
));
|
|
|
|
|
let duplicate_anchor = SessionBinding {
|
|
|
|
|
generation_identity: identity,
|
|
|
|
|
cli_id: ClientId::new(bytes(41)),
|
|
|
|
|
session_id: SessionId::new(bytes(47)),
|
|
|
|
|
session_anchor_public_key: first_binding.session_anchor_public_key,
|
|
|
|
|
};
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
refused_registration(&face, &second, duplicate_anchor),
|
|
|
|
|
ControlError::DuplicateIdentity
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
drop(first_stream);
|
|
|
|
|
first_server.join().unwrap().unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 05:47:15 +09:00
|
|
|
#[test]
|
|
|
|
|
fn concurrent_connections_validate_publication_without_shared_file_offsets() {
|
|
|
|
|
let directory = TestDirectory::new("concurrent-publication");
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let (face, _store) = publish(&directory, &identity);
|
|
|
|
|
let clients = (40..56)
|
|
|
|
|
.map(|value| {
|
|
|
|
|
let face = Arc::clone(&face);
|
|
|
|
|
thread::spawn(move || enroll(&face, ClientId::new(bytes(value))))
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
for client in clients {
|
|
|
|
|
client.join().unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:54:46 +09:00
|
|
|
#[test]
|
|
|
|
|
fn anonymous_connections_are_enrollment_only_bounded_and_exact_alpn_is_required() {
|
|
|
|
|
let directory = TestDirectory::new("anonymous");
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let (face, _) = publish(&directory, &identity);
|
|
|
|
|
|
|
|
|
|
let serving = server(&face);
|
|
|
|
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
|
|
|
let mut stream =
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
|
|
|
.unwrap();
|
|
|
|
|
PublishedControlFace::send(
|
|
|
|
|
&mut stream,
|
|
|
|
|
&ControlRequest::Register(SessionRegistrationRequest {
|
|
|
|
|
protocol_version: ProtocolVersion::V1,
|
|
|
|
|
session_binding: registration_binding(&identity, ClientId::new(bytes(48)), 49, 50),
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
|
|
|
|
drop(stream);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
serving.join().unwrap().unwrap_err(),
|
|
|
|
|
ControlError::InvalidRequest
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let serving = server(&face);
|
|
|
|
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
|
|
|
let mut stream =
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
|
|
|
.unwrap();
|
|
|
|
|
stream.write_all(&8_193_u32.to_be_bytes()).unwrap();
|
|
|
|
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
|
|
|
|
drop(stream);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
serving.join().unwrap().unwrap_err(),
|
|
|
|
|
ControlError::OversizeRequest {
|
|
|
|
|
length: 8_193,
|
|
|
|
|
maximum: 8_192
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let mut tampered = PendingClientEnrollment::create(identity.clone(), ClientId::new(bytes(51)))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.request()
|
|
|
|
|
.clone();
|
|
|
|
|
*tampered.certificate_signing_request_der.last_mut().unwrap() ^= 1;
|
|
|
|
|
let serving = server(&face);
|
|
|
|
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
|
|
|
let mut stream =
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
|
|
|
.unwrap();
|
|
|
|
|
PublishedControlFace::send(&mut stream, &ControlRequest::Enroll(tampered)).unwrap();
|
|
|
|
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
|
|
|
|
drop(stream);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
serving.join().unwrap().unwrap_err(),
|
|
|
|
|
ControlError::Certificate(_)
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let serving = server(&face);
|
|
|
|
|
let mut config =
|
|
|
|
|
PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
|
|
|
config.alpn_protocols = vec![b"not-lazaret".to_vec()];
|
|
|
|
|
assert!(
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity(),)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
assert!(serving.join().unwrap().is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn stale_generation_and_publication_identity_are_refused() {
|
|
|
|
|
let directory = TestDirectory::new("stale");
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let (face, store) = publish(&directory, &identity);
|
|
|
|
|
|
|
|
|
|
let stale =
|
|
|
|
|
PendingClientEnrollment::create(generation_identity(6), ClientId::new(bytes(60))).unwrap();
|
|
|
|
|
let serving = server(&face);
|
|
|
|
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
|
|
|
let mut stream =
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
|
|
|
.unwrap();
|
|
|
|
|
PublishedControlFace::send(
|
|
|
|
|
&mut stream,
|
|
|
|
|
&ControlRequest::Enroll(stale.request().clone()),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(PublishedControlFace::receive::<ControlResponse>(&mut stream).is_err());
|
|
|
|
|
drop(stream);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
serving.join().unwrap().unwrap_err(),
|
|
|
|
|
ControlError::StaleGeneration
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let later = generation_identity(8);
|
|
|
|
|
for decision in [
|
|
|
|
|
DurableDecision::None,
|
|
|
|
|
DurableDecision::Preparing,
|
|
|
|
|
DurableDecision::Published,
|
|
|
|
|
] {
|
|
|
|
|
store
|
|
|
|
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
|
|
|
|
&later, decision,
|
|
|
|
|
))])
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
let serving = server(&face);
|
|
|
|
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity(),)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
serving.join().unwrap().unwrap_err(),
|
|
|
|
|
ControlError::StaleGeneration
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn host_coordinator_key_loss_never_reuses_old_tls_authority() {
|
|
|
|
|
let directory = TestDirectory::new("key-loss");
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let store = open_store(&directory, &identity);
|
|
|
|
|
let old_face = Arc::new(
|
|
|
|
|
PublishedControlFace::publish(
|
|
|
|
|
&directory.0,
|
|
|
|
|
getuid().as_raw(),
|
|
|
|
|
TrustAnchor::create(identity.clone()).unwrap(),
|
|
|
|
|
Arc::clone(&store),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
let old_client = enroll(&old_face, ClientId::new(bytes(70)));
|
|
|
|
|
let old_publication = old_face.publication_identity().clone();
|
|
|
|
|
drop(old_face);
|
|
|
|
|
|
|
|
|
|
let new_face = Arc::new(
|
|
|
|
|
PublishedControlFace::publish(
|
|
|
|
|
&directory.0,
|
|
|
|
|
getuid().as_raw(),
|
|
|
|
|
TrustAnchor::create(identity).unwrap(),
|
|
|
|
|
store,
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
assert_ne!(
|
|
|
|
|
new_face.publication_identity().tls_ca_certificate_der,
|
|
|
|
|
old_publication.tls_ca_certificate_der
|
|
|
|
|
);
|
|
|
|
|
let serving = server(&new_face);
|
|
|
|
|
let old_config = PublishedControlFace::client_config(
|
|
|
|
|
&old_publication,
|
|
|
|
|
Some((&old_client.credential, old_client.pending.private_key())),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
PublishedControlFace::connect(new_face.socket_path(), old_config, &old_publication,)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
assert!(serving.join().unwrap().is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn publication_refuses_wrong_root_path_mode_and_stale_identity_file() {
|
|
|
|
|
let mode_directory = TestDirectory::new("mode");
|
|
|
|
|
std::fs::set_permissions(&mode_directory.0, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let store = open_store(&mode_directory, &identity);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
PublishedControlFace::publish(
|
|
|
|
|
&mode_directory.0,
|
|
|
|
|
getuid().as_raw(),
|
|
|
|
|
TrustAnchor::create(identity.clone()).unwrap(),
|
|
|
|
|
store,
|
|
|
|
|
),
|
|
|
|
|
Err(ControlError::InvalidPath(_))
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let symlink_parent = TestDirectory::new("symlink-parent");
|
|
|
|
|
let target = symlink_parent.path("target");
|
|
|
|
|
std::fs::create_dir(&target).unwrap();
|
|
|
|
|
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o700)).unwrap();
|
|
|
|
|
let link = symlink_parent.path("link");
|
|
|
|
|
std::os::unix::fs::symlink(&target, &link).unwrap();
|
|
|
|
|
let store_directory = TestDirectory::new("symlink-store");
|
|
|
|
|
let store = open_store(&store_directory, &identity);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
PublishedControlFace::publish(
|
|
|
|
|
&link,
|
|
|
|
|
getuid().as_raw(),
|
|
|
|
|
TrustAnchor::create(identity.clone()).unwrap(),
|
|
|
|
|
store,
|
|
|
|
|
),
|
|
|
|
|
Err(ControlError::InvalidPath(_))
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let stale_directory = TestDirectory::new("stale-file");
|
|
|
|
|
let (face, _) = publish(&stale_directory, &identity);
|
|
|
|
|
let mut identity_file = OpenOptions::new()
|
|
|
|
|
.write(true)
|
|
|
|
|
.truncate(true)
|
|
|
|
|
.open(face.identity_path())
|
|
|
|
|
.unwrap();
|
|
|
|
|
identity_file.write_all(b"stale publication").unwrap();
|
|
|
|
|
identity_file.sync_all().unwrap();
|
|
|
|
|
let serving = server(&face);
|
|
|
|
|
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity(),)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
serving.join().unwrap().unwrap_err(),
|
|
|
|
|
ControlError::InvalidPublication(_)
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn publication_refuses_preexisting_fixed_control_path() {
|
|
|
|
|
let directory = TestDirectory::new("existing");
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let store = open_store(&directory, &identity);
|
2026-07-16 16:28:18 +09:00
|
|
|
File::create(directory.path("c.sock")).unwrap();
|
2026-07-15 17:54:46 +09:00
|
|
|
assert!(matches!(
|
|
|
|
|
PublishedControlFace::publish(
|
|
|
|
|
&directory.0,
|
|
|
|
|
getuid().as_raw(),
|
|
|
|
|
TrustAnchor::create(identity).unwrap(),
|
|
|
|
|
store,
|
|
|
|
|
),
|
|
|
|
|
Err(ControlError::InvalidPublication(_))
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn prepared_control_face_admits_only_after_generation_publication() {
|
|
|
|
|
let directory = TestDirectory::new("publication-gate");
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let store = open_preparing_store(&directory, &identity);
|
2026-07-16 17:29:23 +09:00
|
|
|
let registry = Arc::new(SessionRegistry::new(Arc::clone(&store), identity.clone()));
|
|
|
|
|
let staged = StagedControlFace::stage(
|
|
|
|
|
&directory.0,
|
|
|
|
|
getuid().as_raw(),
|
|
|
|
|
TrustAnchor::create(identity.clone()).unwrap(),
|
|
|
|
|
registry,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2026-07-16 16:28:18 +09:00
|
|
|
assert!(std::os::unix::net::UnixStream::connect(directory.path("c.sock")).is_err());
|
2026-07-15 17:54:46 +09:00
|
|
|
|
|
|
|
|
store
|
|
|
|
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
|
|
|
|
&identity,
|
|
|
|
|
DurableDecision::Published,
|
|
|
|
|
))])
|
|
|
|
|
.unwrap();
|
2026-07-16 17:29:23 +09:00
|
|
|
let face = Arc::new(staged.expose().unwrap());
|
2026-07-15 17:54:46 +09:00
|
|
|
let client = enroll(&face, ClientId::new(bytes(90)));
|
|
|
|
|
assert_eq!(client.credential.generation_identity, identity);
|
|
|
|
|
}
|
2026-07-16 17:49:33 +09:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn coordinated_control_session_tracks_lifecycle_and_withdraws_before_teardown() {
|
|
|
|
|
let directory = TestDirectory::new("coordinated-lifecycle");
|
|
|
|
|
let identity = generation_identity(7);
|
|
|
|
|
let store = open_empty_store(&directory);
|
|
|
|
|
let registry = Arc::new(SessionRegistry::new(Arc::clone(&store), identity.clone()));
|
|
|
|
|
let adapter = ControlLifecycleAdapter::default();
|
|
|
|
|
let publication_slot = Arc::clone(&adapter.publication);
|
|
|
|
|
let mut lifecycle = HostCoordinatorLifecycle::new(&store, adapter, Arc::clone(®istry));
|
|
|
|
|
start_sandbox(&mut lifecycle, &identity);
|
|
|
|
|
let staged = StagedControlFace::stage(
|
|
|
|
|
&directory.0,
|
|
|
|
|
getuid().as_raw(),
|
|
|
|
|
TrustAnchor::create(identity.clone()).unwrap(),
|
|
|
|
|
Arc::clone(®istry),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
lifecycle.publish_domain().unwrap();
|
|
|
|
|
lifecycle
|
|
|
|
|
.disconnect_session(SessionId::new(bytes(80)))
|
|
|
|
|
.unwrap();
|
|
|
|
|
let face = Arc::new(staged.expose().unwrap());
|
|
|
|
|
*publication_slot.lock().unwrap() = Some(Arc::clone(&face));
|
|
|
|
|
let client = enroll(&face, ClientId::new(bytes(90)));
|
|
|
|
|
let binding = registration_binding(&identity, ClientId::new(bytes(90)), 91, 92);
|
|
|
|
|
let session_id = binding.session_id;
|
|
|
|
|
let lifecycle = Arc::new(Mutex::new(lifecycle));
|
|
|
|
|
|
|
|
|
|
thread::scope(|scope| {
|
|
|
|
|
let serving_face = Arc::clone(&face);
|
|
|
|
|
let serving_lifecycle = Arc::clone(&lifecycle);
|
|
|
|
|
let serving = scope.spawn(move || {
|
|
|
|
|
serving_face.serve_next_coordinated(
|
|
|
|
|
|admission| {
|
|
|
|
|
serving_lifecycle
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.admit_session(admission)
|
|
|
|
|
.map_err(|error| ControlError::Lifecycle(error.to_string()))
|
|
|
|
|
},
|
|
|
|
|
|session_id| {
|
|
|
|
|
serving_lifecycle
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.disconnect_session(session_id)
|
|
|
|
|
.map_err(|error| ControlError::Lifecycle(error.to_string()))
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
let config = PublishedControlFace::client_config(
|
|
|
|
|
face.publication_identity(),
|
|
|
|
|
Some((&client.credential, client.pending.private_key())),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut stream =
|
|
|
|
|
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
|
|
|
.unwrap();
|
|
|
|
|
PublishedControlFace::send(
|
|
|
|
|
&mut stream,
|
|
|
|
|
&ControlRequest::Register(SessionRegistrationRequest {
|
|
|
|
|
protocol_version: ProtocolVersion::V1,
|
|
|
|
|
session_binding: binding,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let _: ControlResponse = PublishedControlFace::receive(&mut stream).unwrap();
|
|
|
|
|
{
|
|
|
|
|
let lifecycle = lifecycle.lock().unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
lifecycle.session_phase(session_id),
|
|
|
|
|
Some(SessionPhase::Attached)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(lifecycle.registered_sessions(), 1);
|
|
|
|
|
assert!(!lifecycle.is_teardown_ready());
|
|
|
|
|
}
|
|
|
|
|
drop(stream);
|
|
|
|
|
serving.join().unwrap().unwrap();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mut lifecycle = lifecycle.lock().unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
lifecycle.session_phase(session_id),
|
|
|
|
|
Some(SessionPhase::Closed)
|
|
|
|
|
);
|
|
|
|
|
assert!(lifecycle.is_teardown_ready());
|
2026-07-16 17:53:54 +09:00
|
|
|
std::fs::set_permissions(&directory.0, std::fs::Permissions::from_mode(0o500)).unwrap();
|
|
|
|
|
assert!(lifecycle.begin_teardown().is_err());
|
|
|
|
|
assert_eq!(lifecycle.phase(), DomainPhase::Ready);
|
|
|
|
|
assert!(lifecycle.is_published());
|
|
|
|
|
assert!(face.socket_path().exists());
|
|
|
|
|
assert!(face.identity_path().exists());
|
|
|
|
|
std::fs::set_permissions(&directory.0, std::fs::Permissions::from_mode(0o700)).unwrap();
|
2026-07-16 17:49:33 +09:00
|
|
|
lifecycle.begin_teardown().unwrap();
|
2026-07-16 17:53:54 +09:00
|
|
|
assert_eq!(lifecycle.phase(), DomainPhase::Stopping);
|
|
|
|
|
assert!(!lifecycle.is_published());
|
2026-07-16 17:49:33 +09:00
|
|
|
assert!(!face.socket_path().exists());
|
|
|
|
|
assert!(!face.identity_path().exists());
|
|
|
|
|
assert!(std::os::unix::net::UnixStream::connect(face.socket_path()).is_err());
|
|
|
|
|
}
|