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}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use ed25519_dalek::SigningKey; use lazaret_core::identity::{ ClientId, ConfigurationFingerprint, CoordinatorId, DomainId, GenerationId, HostPath, NetworkSidecarId, SandboxId, SessionAnchorId, SessionId, }; use lazaret_core::records::{ ClientEnrollmentResponse, ControlRequest, ControlResponse, Domain, DurableDecision, Generation, GenerationIdentity, ProtocolVersion, Session, SessionBinding, SessionPhase, SessionRegistrationRequest, SessionRegistrationResponse, TlsCredential, }; use lazaret_host_coordinator::control::{ CONTROL_ALPN, ControlError, PendingClientEnrollment, PublishedControlFace, SessionRegistry, StagedControlFace, TrustAnchor, }; use lazaret_host_coordinator::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore}; use lazaret_host_coordinator::lifecycle::{ DomainPhase, HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, }; 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); #[derive(Clone, Default)] struct ControlLifecycleAdapter { publication: Arc>>>, } impl LifecycleAdapter for ControlLifecycleAdapter { type Error = ControlError; fn execute( &mut self, action: LifecycleAction, _generation: &Generation, ) -> Result<(), Self::Error> { if action == LifecycleAction::BeginTeardown { let mut publication = self.publication.lock().unwrap(); if let Some(face) = publication.as_ref() { face.withdraw()?; } publication.take(); } Ok(()) } } struct TestDirectory(PathBuf); impl TestDirectory { fn new(label: &str) -> Self { let sequence = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed); let parent = PathBuf::from("/tmp"); let path = parent.join(format!( "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 { 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 { 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 } fn open_empty_store(directory: &TestDirectory) -> Arc { 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>, 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(); } fn publish( directory: &TestDirectory, identity: &GenerationIdentity, ) -> (Arc, Arc) { 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) -> JoinHandle> { let face = Arc::clone(face); thread::spawn(move || face.serve_next()) } struct EnrolledClient { pending: PendingClientEnrollment, credential: TlsCredential, server_certificate: Vec, } fn enroll(face: &Arc, 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, client: &EnrolledClient, binding: SessionBinding, ) -> ( rustls::StreamOwned, JoinHandle>, 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, 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::(&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); assert_eq!(face.socket_path().file_name().unwrap(), "c.sock"); 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::(&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(); } #[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::>(); for client in clients { client.join().unwrap(); } } #[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::(&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::(&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::(&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::(&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); File::create(directory.path("c.sock")).unwrap(); 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); 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(); assert!(std::os::unix::net::UnixStream::connect(directory.path("c.sock")).is_err()); store .apply(&[DomainStoreMutation::PutGeneration(generation( &identity, DurableDecision::Published, ))]) .unwrap(); let face = Arc::new(staged.expose().unwrap()); let client = enroll(&face, ClientId::new(bytes(90))); assert_eq!(client.credential.generation_identity, identity); } #[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()); 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(); lifecycle.begin_teardown().unwrap(); assert_eq!(lifecycle.phase(), DomainPhase::Stopping); assert!(!lifecycle.is_published()); assert!(!face.socket_path().exists()); assert!(!face.identity_path().exists()); assert!(std::os::unix::net::UnixStream::connect(face.socket_path()).is_err()); }