#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))] #![doc = include_str!("../README.md")] // @@ begin lint list maintained by maint/add_warning @@ #![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable) #![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly) #![warn(missing_docs)] #![warn(noop_method_call)] #![warn(unreachable_pub)] #![warn(clippy::all)] #![deny(clippy::await_holding_lock)] #![deny(clippy::cargo_common_metadata)] #![deny(clippy::cast_lossless)] #![deny(clippy::checked_conversions)] #![warn(clippy::cognitive_complexity)] #![deny(clippy::debug_assert_with_mut_call)] #![deny(clippy::exhaustive_enums)] #![deny(clippy::exhaustive_structs)] #![deny(clippy::expl_impl_clone_on_copy)] #![deny(clippy::fallible_impl_from)] #![deny(clippy::implicit_clone)] #![deny(clippy::large_stack_arrays)] #![warn(clippy::manual_ok_or)] #![deny(clippy::missing_docs_in_private_items)] #![warn(clippy::needless_borrow)] #![warn(clippy::needless_pass_by_value)] #![warn(clippy::option_option)] #![deny(clippy::print_stderr)] #![deny(clippy::print_stdout)] #![warn(clippy::rc_buffer)] #![deny(clippy::ref_option_ref)] #![warn(clippy::semicolon_if_nothing_returned)] #![warn(clippy::trait_duplication_in_bounds)] #![deny(clippy::unchecked_duration_subtraction)] #![deny(clippy::unnecessary_wraps)] #![warn(clippy::unseparated_literal_suffix)] #![deny(clippy::unwrap_used)] #![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness #![allow(clippy::uninlined_format_args)] #![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945 #![allow(clippy::result_large_err)] // temporary workaround for arti#587 #![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best //! // TODO #1645 (either remove this, or decide to have it everywhere) #![cfg_attr( not(all(feature = "full", feature = "experimental")), allow(unused, unreachable_pub) )] #[macro_use] // SerdeStringOrTransparent mod time_store; mod internal_prelude; mod anon_level; pub mod config; mod err; mod helpers; mod ipt_establish; mod ipt_lid; mod ipt_mgr; mod ipt_set; mod keys; mod publish; mod rend_handshake; mod replay; mod req; pub mod status; mod timeout_track; // rustdoc doctests can't use crate-public APIs, so are broken if provided for private items. // So we export the whole module again under this name. // Supports the Example in timeout_track.rs's module-level docs. // // Any out-of-crate user needs to write this ludicrous name in their code, // so we don't need to put any warnings in the docs for the individual items.) // // (`#[doc(hidden)] pub mod timeout_track;` would work for the test but it would // completely suppress the actual documentation, which is not what we want.) #[doc(hidden)] pub mod timeout_track_for_doctests_unstable_no_semver_guarantees { pub use crate::timeout_track::*; } #[doc(hidden)] pub mod time_store_for_doctests_unstable_no_semver_guarantees { pub use crate::time_store::*; } use internal_prelude::*; // ---------- public exports ---------- pub use anon_level::Anonymity; pub use config::OnionServiceConfig; pub use err::{ClientError, EstablishSessionError, FatalError, IntroRequestError, StartupError}; pub use ipt_mgr::IptError; pub use keys::{ BlindIdKeypairSpecifier, BlindIdPublicKeySpecifier, DescSigningKeypairSpecifier, HsIdKeypairSpecifier, HsIdPublicKeySpecifier, }; pub use publish::UploadError as DescUploadError; pub use req::{RendRequest, StreamRequest}; pub use tor_hscrypto::pk::HsId; pub use tor_persist::hsnickname::{HsNickname, InvalidNickname}; pub use helpers::handle_rend_requests; //---------- top-level service implementation (types and methods) ---------- /// Convenience alias for link specifiers of an intro point pub(crate) type LinkSpecs = Vec; /// Convenient type alias for an ntor public key // TODO (#1022) maybe this should be // `tor_proto::crypto::handshake::ntor::NtorPublicKey`, // or a unified OnionKey type. pub(crate) type NtorPublicKey = curve25519::PublicKey; /// A handle to a running instance of an onion service. // // TODO (#1228): Write more. // TODO (#1247): Choose a better name for this struct // // (APIs should return Arc) #[must_use = "a hidden service object will terminate the service when dropped"] pub struct RunningOnionService { /// The mutable implementation details of this onion service. inner: Mutex, /// The nickname of this service. nickname: HsNickname, /// The key manager, used for accessing the underlying key stores. keymgr: Arc, } /// Implementation details for an onion service. struct SvcInner { /// Configuration information about this service. config_tx: postage::watch::Sender>, /// A oneshot that will be dropped when this object is dropped. _shutdown_tx: postage::broadcast::Sender, /// Postage sender, used to tell subscribers about changes in the status of /// this onion service. status_tx: StatusSender, /// Handles that we'll take ownership of when launching the service. unlaunched: Option<( mpsc::Receiver, Box, )>, } /// Objects and handles needed to launch an onion service. struct ForLaunch { /// An unlaunched handle for the HsDesc publisher. /// /// This publisher is responsible for determining when we need to upload a /// new set of HsDescs, building them, and publishing them at the correct /// HsDirs. publisher: Publisher>, /// Our handler for the introduction point manager. /// /// This manager is responsible for selecting introduction points, /// maintaining our connections to them, and telling the publisher which ones /// are publicly available. ipt_mgr: IptManager>, /// A handle used by the ipt manager to send Ipts to the publisher. /// /// ipt_mgr_view: IptsManagerView, } /// Private trait used to type-erase `ForLaunch`, so that we don't need to /// parameterize OnionService on ``. trait Launchable: Send + Sync { /// Launch fn launch(self: Box) -> Result<(), StartupError>; } impl Launchable for ForLaunch { fn launch(self: Box) -> Result<(), StartupError> { self.ipt_mgr.launch_background_tasks(self.ipt_mgr_view)?; self.publisher.launch()?; Ok(()) } } /// Return value from one call to the main loop iteration /// /// Used by the publisher reactor and by the [`IptManager`]. #[derive(PartialEq)] #[must_use] pub(crate) enum ShutdownStatus { /// We should continue to operate this component Continue, /// We should shut down: the service, or maybe the whole process, is shutting down Terminate, } impl From for ShutdownStatus { fn from(_: oneshot::Canceled) -> ShutdownStatus { ShutdownStatus::Terminate } } /// A handle to an instance of an onion service, which may or may not be running. /// /// To construct an `OnionService`, use [`OnionServiceBuilder`]. /// It will not start handling requests until you call its /// [``.launch()``](OnionService::launch) method. /// /// Note: the identity key (HsId) of the service is not generated until /// [``.launch()``](OnionService::launch) is called. #[derive(Builder)] #[builder(build_fn(private, name = "build_unvalidated", error = "FatalError"))] pub struct OnionService { /// The current configuration. config: OnionServiceConfig, /// The key manager, used for accessing the underlying key stores. keymgr: Arc, /// The location on disk where the persistent data is stored. state_dir: StateDirectory, } impl OnionService { /// Create an [`OnionServiceBuilder`]. pub fn builder() -> OnionServiceBuilder { OnionServiceBuilder::default() } /// Tell this onion service to begin running, and return a /// [`RunningOnionService`] and its stream of rendezvous requests. /// /// You can turn the resulting stream into a stream of [`StreamRequest`] /// using the [`handle_rend_requests`] helper function. /// /// Once the `RunningOnionService` is dropped, the onion service will stop /// publishing, and stop accepting new introduction requests. Existing /// streams and rendezvous circuits will remain open. pub fn launch( self, runtime: R, netdir_provider: Arc, circ_pool: Arc>, path_resolver: Arc, ) -> Result<(Arc, impl Stream), StartupError> where R: Runtime, { let OnionService { config, keymgr, state_dir, } = self; let nickname = config.nickname.clone(); // TODO (#1194): add a config option for specifying whether to expect the KS_hsid to be stored // offline //let offline_hsid = config.offline_hsid; let offline_hsid = false; // TODO (#1106): make this configurable let selector = KeystoreSelector::Primary; maybe_generate_hsid(&keymgr, &config.nickname, offline_hsid, selector)?; if config.restricted_discovery.enabled { info!( nickname=%nickname, "Launching onion service in restricted discovery mode" ); } else { info!( nickname=%nickname, "Launching onion service" ); } let state_handle = state_dir .acquire_instance(&config.nickname) .map_err(StartupError::StateDirectoryInaccessible)?; // We pass the "cooked" handle, with the storage key embedded, to ipt_set, // since the ipt_set code doesn't otherwise have access to the HS nickname. let iptpub_storage_handle = state_handle .storage_handle("iptpub") .map_err(StartupError::StateDirectoryInaccessible)?; // If the HS implementation is stalled somehow, this is a local problem. // We shouldn't kill the HS even if this is the oldest data in the system. let (rend_req_tx, rend_req_rx) = mpsc_channel_no_memquota(32); let (shutdown_tx, shutdown_rx) = broadcast::channel(0); let (config_tx, config_rx) = postage::watch::channel_with(Arc::new(config)); let (ipt_mgr_view, publisher_view) = crate::ipt_set::ipts_channel(&runtime, iptpub_storage_handle)?; let status_tx = StatusSender::new(OnionServiceStatus::new_shutdown()); let ipt_mgr = IptManager::new( runtime.clone(), netdir_provider.clone(), nickname.clone(), config_rx.clone(), rend_req_tx, shutdown_rx.clone(), &state_handle, crate::ipt_mgr::Real { circ_pool: circ_pool.clone(), }, keymgr.clone(), status_tx.clone().into(), )?; let publisher: Publisher> = Publisher::new( runtime, nickname.clone(), netdir_provider, circ_pool, publisher_view, config_rx, status_tx.clone().into(), Arc::clone(&keymgr), path_resolver, ); let svc = Arc::new(RunningOnionService { nickname, keymgr, inner: Mutex::new(SvcInner { config_tx, _shutdown_tx: shutdown_tx, status_tx, unlaunched: Some(( rend_req_rx, Box::new(ForLaunch { publisher, ipt_mgr, ipt_mgr_view, }), )), }), }); let stream = svc.launch()?; Ok((svc, stream)) } /// Return the onion address of this service. /// /// Clients must know the service's onion address in order to discover or /// connect to it. /// /// Returns `None` if the HsId of the service could not be found in any of the configured /// keystores. pub fn onion_name(&self) -> Option { onion_name(&self.keymgr, &self.config.nickname) } /// Generate an identity key (KP_hs_id) for this service. /// /// If the keystore specified by `selector` contains an entry for the identity key /// of this service, it will be returned. Otherwise, a new key will be generated. /// /// Most users do not need to call this function: on [`launch`](`OnionService::launch`), /// the service will automatically generate its identity key if needed. /// You should only use this function if you need to know the KP_hs_id of the service /// before launching it. /// /// The `selector` argument is used for choosing the keystore in which to generate the keypair. /// While most users will want to write to the [`Primary`](KeystoreSelector::Primary), if you /// have configured this `TorClient` with a non-default keystore and wish to generate the /// keypair in it, you can do so by calling this function with a [KeystoreSelector::Id] /// specifying the keystore ID of your keystore. /// // Note: the selector argument exists for future-proofing reasons. We don't currently support // configuring custom or non-default keystores (see #1106). pub fn generate_identity_key(&self, selector: KeystoreSelector) -> Result { // TODO (#1194): add a config option for specifying whether to expect the KS_hsid to be stored // offline //let offline_hsid = config.offline_hsid; let offline_hsid = false; maybe_generate_hsid(&self.keymgr, &self.config.nickname, offline_hsid, selector) } } impl OnionServiceBuilder { /// Build the [`OnionService`] pub fn build(&self) -> Result { let svc = self.build_unvalidated()?; Ok(svc) } } impl RunningOnionService { /// Change the configuration of this onion service. /// /// (Not everything can be changed here. At the very least we'll need to say /// that the identity of a service is fixed. We might want to make the /// storage backing this, and the anonymity status, unchangeable.) pub fn reconfigure( &self, new_config: OnionServiceConfig, how: Reconfigure, ) -> Result<(), ReconfigureError> { let mut inner = self.inner.lock().expect("lock poisoned"); inner.config_tx.try_maybe_send(|cur_config| { let new_config = cur_config.for_transition_to(new_config, how)?; Ok(match how { // We're only checking, so return the current configuration. tor_config::Reconfigure::CheckAllOrNothing => Arc::clone(cur_config), // We're replacing the configuration, and we didn't get an error. _ => Arc::new(new_config), }) }) // TODO (#1153, #1209): We need to make sure that the various tasks listening on // config_rx actually enforce the configuration, not only on new // connections, but existing ones. } /* /// Tell this onion service about some new short-term keys it can use. pub fn add_keys(&self, keys: ()) -> Result<(), Bug> { todo!() // TODO #1194 } */ /// Return the current status of this onion service. pub fn status(&self) -> OnionServiceStatus { self.inner.lock().expect("poisoned lock").status_tx.get() } /// Return a stream of events that will receive notifications of changes in /// this onion service's status. pub fn status_events(&self) -> OnionServiceStatusStream { self.inner .lock() .expect("poisoned lock") .status_tx .subscribe() } /// Tell this onion service to begin running, and return a /// stream of rendezvous requests on the service. /// /// You can turn the resulting stream into a stream of [`StreamRequest`] /// using the [`handle_rend_requests`] helper function. fn launch(self: &Arc) -> Result, StartupError> { let (rend_req_rx, launch) = { let mut inner = self.inner.lock().expect("poisoned lock"); inner .unlaunched .take() .ok_or(StartupError::AlreadyLaunched)? }; match launch.launch() { Ok(()) => {} Err(e) => { return Err(e); } } // This needs to launch at least the following tasks: // // TODO (#1194) If we decide to use separate disk-based key // provisioning, we need a task to monitor our keys directory. Ok(rend_req_rx) } /* /// Tell this onion service to stop running. /// /// It can be restarted with launch(). /// /// You can also shut down an onion service completely by dropping the last /// Clone of it. pub fn pause(&self) { todo!() // TODO (#1231) } */ /// Return the onion address of this service. /// /// Clients must know the service's onion address in order to discover or /// connect to it. /// /// Returns `None` if the HsId of the service could not be found in any of the configured /// keystores. pub fn onion_name(&self) -> Option { onion_name(&self.keymgr, &self.nickname) } } /// Generate the identity key of the service, unless it already exists or `offline_hsid` is `true`. // // TODO (#1194): we don't support offline_hsid yet. fn maybe_generate_hsid( keymgr: &Arc, nickname: &HsNickname, offline_hsid: bool, selector: KeystoreSelector, ) -> Result { if offline_hsid { unimplemented!("offline hsid mode"); } let hsid_spec = HsIdPublicKeySpecifier::new(nickname.clone()); let kp = keymgr .get::(&hsid_spec) .map_err(|cause| StartupError::Keystore { action: "read", cause, })?; let mut rng = rand::thread_rng(); let (hsid, generated) = match kp { Some(kp) => (kp.id(), false), None => { // Note: there is a race here. If the HsId is generated through some other means // (e.g. via the CLI) at some point between the time we looked up the keypair and // now, we will return an error. let hsid_spec = HsIdKeypairSpecifier::new(nickname.clone()); let kp = keymgr .generate::(&hsid_spec, selector, &mut rng, false /* overwrite */) .map_err(|cause| StartupError::Keystore { action: "generate", cause, })?; (HsIdKey::from(&kp).id(), true) } }; if generated { info!( "Generated a new identity for service {nickname}: {}", sensitive(hsid) ); } else { // TODO: We may want to downgrade this to trace once we have a CLI // for extracting it. info!( "Using existing identity for service {nickname}: {}", sensitive(hsid) ); } Ok(hsid) } /// Return the onion address of this service. /// /// Clients must know the service's onion address in order to discover or /// connect to it. /// /// Returns `None` if the HsId of the service could not be found in any of the configured /// keystores. // // TODO: instead of duplicating RunningOnionService::onion_name, maybe we should make this a // method on an ArtiHss type, and make both OnionService and RunningOnionService deref to // ArtiHss. fn onion_name(keymgr: &KeyMgr, nickname: &HsNickname) -> Option { let hsid_spec = HsIdPublicKeySpecifier::new(nickname.clone()); keymgr .get::(&hsid_spec) .ok()? .map(|hsid| hsid.id()) } #[cfg(test)] pub(crate) mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::mixed_attributes_style)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_duration_subtraction)] #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] //! use super::*; use std::fmt::Display; use std::path::Path; use fs_mistrust::Mistrust; use test_temp_dir::{test_temp_dir, TestTempDir, TestTempDirGuard}; use tor_basic_utils::test_rng::testing_rng; use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder}; use tor_llcrypto::pk::ed25519; use tor_persist::state_dir::InstanceStateHandle; use crate::config::OnionServiceConfigBuilder; use crate::ipt_set::IptSetStorageHandle; use crate::{HsIdKeypairSpecifier, HsIdPublicKeySpecifier}; /// The nickname of the test service. const TEST_SVC_NICKNAME: &str = "test-svc"; /// Make a fresh `KeyMgr` (containing no keys) using files in `temp_dir` pub(crate) fn create_keymgr(temp_dir: &TestTempDir) -> TestTempDirGuard> { temp_dir.subdir_used_by("keystore", |keystore_dir| { let keystore = ArtiNativeKeystore::from_path_and_mistrust( keystore_dir, &Mistrust::new_dangerously_trust_everyone(), ) .unwrap(); Arc::new( KeyMgrBuilder::default() .primary_store(Box::new(keystore)) .build() .unwrap(), ) }) } #[allow(clippy::let_and_return)] // clearer and more regular pub(crate) fn mk_state_instance(dir: &Path, nick: impl Display) -> InstanceStateHandle { let nick = HsNickname::new(nick.to_string()).unwrap(); let mistrust = fs_mistrust::Mistrust::new_dangerously_trust_everyone(); let state_dir = StateDirectory::new(dir, &mistrust).unwrap(); let instance = state_dir.acquire_instance(&nick).unwrap(); instance } pub(crate) fn create_storage_handles( dir: &Path, ) -> ( tor_persist::state_dir::InstanceStateHandle, IptSetStorageHandle, ) { let nick = HsNickname::try_from("allium".to_owned()).unwrap(); create_storage_handles_from_state_dir(dir, &nick) } pub(crate) fn create_storage_handles_from_state_dir( state_dir: &Path, nick: &HsNickname, ) -> ( tor_persist::state_dir::InstanceStateHandle, IptSetStorageHandle, ) { let instance = mk_state_instance(state_dir, nick); let iptpub_state_handle = instance.storage_handle("iptpub").unwrap(); (instance, iptpub_state_handle) } macro_rules! maybe_generate_hsid { ($keymgr:expr, $offline_hsid:expr) => {{ let nickname = HsNickname::try_from(TEST_SVC_NICKNAME.to_string()).unwrap(); let hsid_spec = HsIdKeypairSpecifier::new(nickname.clone()); let pub_hsid_spec = HsIdPublicKeySpecifier::new(nickname.clone()); assert!($keymgr.get::(&pub_hsid_spec).unwrap().is_none()); assert!($keymgr.get::(&hsid_spec).unwrap().is_none()); maybe_generate_hsid(&$keymgr, &nickname, $offline_hsid, Default::default()).unwrap(); }}; } /// Create a test hsid keypair. fn create_hsid() -> (HsIdKeypair, HsIdKey) { let mut rng = testing_rng(); let keypair = ed25519::Keypair::generate(&mut rng); let id_pub = HsIdKey::from(keypair.verifying_key()); let id_keypair = HsIdKeypair::from(ed25519::ExpandedKeypair::from(&keypair)); (id_keypair, id_pub) } #[test] fn generate_hsid() { let temp_dir = test_temp_dir!(); let keymgr = create_keymgr(&temp_dir); let nickname = HsNickname::try_from(TEST_SVC_NICKNAME.to_string()).unwrap(); let hsid_spec = HsIdKeypairSpecifier::new(nickname.clone()); assert!(keymgr.get::(&hsid_spec).unwrap().is_none()); maybe_generate_hsid!(keymgr, false /* offline_hsid */); assert!(keymgr.get::(&hsid_spec).unwrap().is_some()); } #[test] fn hsid_keypair_already_exists() { let temp_dir = test_temp_dir!(); let nickname = HsNickname::try_from(TEST_SVC_NICKNAME.to_string()).unwrap(); let hsid_spec = HsIdKeypairSpecifier::new(nickname.clone()); let keymgr = create_keymgr(&temp_dir); // Insert the preexisting hsid keypair. let (existing_hsid_keypair, existing_hsid_public) = create_hsid(); let existing_keypair: ed25519::ExpandedKeypair = existing_hsid_keypair.into(); let existing_hsid_keypair = HsIdKeypair::from(existing_keypair); keymgr .insert( existing_hsid_keypair, &hsid_spec, KeystoreSelector::Primary, true, ) .unwrap(); maybe_generate_hsid( &keymgr, &nickname, false, /* offline_hsid */ Default::default(), ) .unwrap(); let keypair = keymgr.get::(&hsid_spec).unwrap().unwrap(); let pk: HsIdKey = (&keypair).into(); assert_eq!(pk.as_ref(), existing_hsid_public.as_ref()); } #[test] #[ignore] // TODO (#1194): Revisit when we add support for offline hsid mode fn generate_hsid_offline_hsid() { let temp_dir = test_temp_dir!(); let keymgr = create_keymgr(&temp_dir); let nickname = HsNickname::try_from(TEST_SVC_NICKNAME.to_string()).unwrap(); let hsid_spec = HsIdKeypairSpecifier::new(nickname.clone()); let pub_hsid_spec = HsIdPublicKeySpecifier::new(nickname.clone()); maybe_generate_hsid!(keymgr, true /* offline_hsid */); assert!(keymgr.get::(&pub_hsid_spec).unwrap().is_none()); assert!(keymgr.get::(&hsid_spec).unwrap().is_none()); } #[test] #[ignore] // TODO (#1194): Revisit when we add support for offline hsid mode fn generate_hsid_corrupt_keystore() { let temp_dir = test_temp_dir!(); let nickname = HsNickname::try_from(TEST_SVC_NICKNAME.to_string()).unwrap(); let hsid_spec = HsIdKeypairSpecifier::new(nickname.clone()); let pub_hsid_spec = HsIdPublicKeySpecifier::new(nickname.clone()); let keymgr = create_keymgr(&temp_dir); let (hsid_keypair, _hsid_public) = create_hsid(); let (_hsid_keypair, hsid_public) = create_hsid(); keymgr .insert(hsid_keypair, &hsid_spec, KeystoreSelector::Primary, true) .unwrap(); // Insert a mismatched public key keymgr .insert(hsid_public, &pub_hsid_spec, KeystoreSelector::Primary, true) .unwrap(); assert!(maybe_generate_hsid( &keymgr, &nickname, false, /* offline_hsid */ Default::default() ) .is_err()); } #[test] fn onion_name() { let temp_dir = test_temp_dir!(); let nickname = HsNickname::try_from(TEST_SVC_NICKNAME.to_string()).unwrap(); let hsid_spec = HsIdKeypairSpecifier::new(nickname.clone()); let keymgr = create_keymgr(&temp_dir); let (hsid_keypair, hsid_public) = create_hsid(); // Insert the hsid into the keystore keymgr .insert(hsid_keypair, &hsid_spec, KeystoreSelector::Primary, true) .unwrap(); let config = OnionServiceConfigBuilder::default() .nickname(nickname) .build() .unwrap(); let state_dir = StateDirectory::new( temp_dir.as_path_untracked(), &fs_mistrust::Mistrust::new_dangerously_trust_everyone(), ) .unwrap(); let service = OnionService::builder() .config(config) .keymgr(Arc::clone(&*keymgr)) .state_dir(state_dir) .build() .unwrap(); let hsid = HsId::from(hsid_public); assert_eq!(service.onion_name().unwrap(), hsid); drop(temp_dir); // prove that this is still live } }