//! Configuration information for onion services. use crate::internal_prelude::*; use amplify::Getters; use derive_deftly::derive_deftly_adhoc; use tor_cell::relaycell::hs::est_intro; use tor_config::derive::prelude::*; use crate::config::restricted_discovery::{ RestrictedDiscoveryConfig, RestrictedDiscoveryConfigBuilder, }; #[cfg(feature = "restricted-discovery")] pub mod restricted_discovery; // Only exported with pub visibility if the restricted-discovery feature is enabled. #[cfg(not(feature = "restricted-discovery"))] // Use cfg(true) to prevent this from being documented as // "Available on non-crate feature `restricted-discovery` only" #[cfg_attr(docsrs, doc(cfg(true)))] pub(crate) mod restricted_discovery; /// Configuration for one onion service. #[derive(Debug, Clone, Eq, PartialEq, Deftly, Getters)] #[derive_deftly(TorConfig)] #[derive_deftly_adhoc] #[deftly(tor_config(no_default_trait, pre_build = "Self::validate"))] pub struct OnionServiceConfig { /// The nickname used to look up this service's keys, state, configuration, etc. #[deftly(publisher_view)] #[deftly(tor_config(no_default))] pub(crate) nickname: HsNickname, /// If true, this service will be started. It should be available to /// commands that don't require it to start regardless. #[deftly(tor_config(default = "true"))] pub(crate) enabled: bool, /// Number of intro points; defaults to 3; max 20. #[deftly(tor_config(default = "DEFAULT_NUM_INTRO_POINTS"))] pub(crate) num_intro_points: u8, /// A rate-limit on the acceptable rate of introduction requests. /// /// We send this to the introduction point to configure how many /// introduction requests it sends us. /// If this is not set, the introduction point chooses a default based on /// the current consensus. /// /// We do not enforce this limit ourselves. /// /// This configuration is sent as a `DOS_PARAMS` extension, as documented in /// . #[deftly(tor_config(default))] rate_limit_at_intro: Option, /// How many streams will we allow to be open at once for a single circuit on /// this service? /// /// If a client attempts to open more than this many streams on a rendezvous circuit, /// the circuit will be torn down. /// /// Equivalent to C Tor's HiddenServiceMaxStreamsCloseCircuit option. #[deftly(tor_config(default = "65535"))] max_concurrent_streams_per_circuit: u32, /// If true, we will require proof-of-work when we're under heavy load. #[deftly(tor_config(default = "false"))] #[deftly(publisher_view)] pub(crate) enable_pow: bool, /// The maximum number of entries allowed in the rendezvous request queue when PoW is enabled. /// /// If you are seeing dropped requests, have a bursty traffic pattern, and have some memory to /// spare, you may want to increase this. /// /// Each request will take a few KB, the default queue is expected to take 32MB at most. // The "a few KB" measurement was done by using the get_size crate to // measure the size of the RendRequest object, but due to limitations in // that crate (and in my willingness to go implement ways of checking the // size of external types), it might be somewhat off. The ~32MB value is // based on the idea that each RendRequest is 4KB. #[deftly(tor_config(default = "8192"))] pub(crate) pow_rend_queue_depth: usize, /// Configure restricted discovery mode. /// /// When this is enabled, we encrypt our list of introduction point and keys /// so that only clients holding one of the listed keys can decrypt it. #[deftly(tor_config(sub_builder))] #[deftly(publisher_view)] #[getter(as_mut)] pub(crate) restricted_discovery: RestrictedDiscoveryConfig, // TODO(#727): add support for single onion services // // TODO: Perhaps this belongs at a higher level. Perhaps we don't need it // at all. // // enabled: bool, // /// Whether we want this to be a non-anonymous "single onion service". // /// We could skip this in v1. We should make sure that our state // /// is built to make it hard to accidentally set this. // #[builder(default)] // #[deftly(publisher_view)] // pub(crate) anonymity: crate::Anonymity, /// Whether to use the compiled backend for proof-of-work. // TODO: Consider making this a global option instead? #[deftly(tor_config(default = "false"))] disable_pow_compilation: bool, } derive_deftly_adhoc! { OnionServiceConfig expect items: ${defcond PUBLISHER_VIEW fmeta(publisher_view)} #[doc = concat!("Descriptor publisher's view of [`", stringify!($tname), "`]")] #[derive(PartialEq, Clone, Debug)] pub(crate) struct $<$tname PublisherView><$tdefgens> where $twheres ${vdefbody $vname $( ${when PUBLISHER_VIEW} ${fattrs doc} $fvis $fname: $ftype, ) } impl<$tgens> From<$tname> for $<$tname PublisherView><$tdefgens> where $twheres { fn from(config: $tname) -> $<$tname PublisherView><$tdefgens> { Self { $( ${when PUBLISHER_VIEW} $fname: config.$fname, ) } } } impl<$tgens> From<&$tname> for $<$tname PublisherView><$tdefgens> where $twheres { fn from(config: &$tname) -> $<$tname PublisherView><$tdefgens> { Self { $( ${when PUBLISHER_VIEW} #[allow(clippy::clone_on_copy)] // some fields are Copy $fname: config.$fname.clone(), ) } } } } /// Default number of introduction points. const DEFAULT_NUM_INTRO_POINTS: u8 = 3; impl OnionServiceConfig { /// Check whether an onion service running with this configuration can /// switch over `other` according to the rules of `how`. /// // Return an error if it can't; otherwise return the new config that we // should change to. pub(crate) fn for_transition_to( &self, mut other: OnionServiceConfig, how: tor_config::Reconfigure, ) -> Result { /// Arguments to a handler for a field /// /// The handler must: /// * check whether this field can be updated /// * if necessary, throw an error (in which case `*other` may be wrong) /// * if it doesn't throw an error, ensure that `*other` /// is appropriately updated. // // We could have a trait but that seems overkill. #[allow(clippy::missing_docs_in_private_items)] // avoid otiosity struct HandlerInput<'i, 'o, T> { how: tor_config::Reconfigure, self_: &'i T, other: &'o mut T, field_name: &'i str, } /// Convenience alias type HandlerResult = Result<(), tor_config::ReconfigureError>; /// Handler for config fields that cannot be changed #[allow(clippy::needless_pass_by_value)] fn unchangeable(i: HandlerInput) -> HandlerResult { if i.self_ != i.other { i.how.cannot_change(i.field_name)?; // If we reach here, then `how` is WarnOnFailures, so we keep the // original value. *i.other = i.self_.clone(); } Ok(()) } /// Handler for config fields that can be freely changed #[allow(clippy::unnecessary_wraps)] fn simply_update(_: HandlerInput) -> HandlerResult { Ok(()) } /// Check all the fields. Input maps fields to handlers. macro_rules! fields { { $( $field:ident: $handler:expr ),* $(,)? } => { // prove that we have handled every field let OnionServiceConfig { $( $field: _, )* } = self; $( $handler(HandlerInput { how, self_: &self.$field, other: &mut other.$field, field_name: stringify!($field), })?; )* } } fields! { nickname: unchangeable, // TODO: allow starting/stopping onion services while the client is // running enabled: unchangeable, // IPT manager will respond by adding or removing IPTs as desired. // (Old IPTs are not proactively removed, but they will not be replaced // as they are rotated out.) num_intro_points: simply_update, // IPT manager's "new configuration" select arm handles this, // by replacing IPTs if necessary. rate_limit_at_intro: simply_update, // We extract this on every introduction request. max_concurrent_streams_per_circuit: simply_update, // The descriptor publisher responds by generating and publishing a new descriptor. restricted_discovery: simply_update, // TODO (#2082): allow changing enable_pow while the client is running enable_pow: unchangeable, // Do note that if the depth of the queue is decreased at runtime to a value smaller // than the number of items in the queue, that will prevent new requests from coming in // until the queue is smaller than the new size, but if will not trim the existing // queue. pow_rend_queue_depth: simply_update, // This is a little too much effort to allow to by dynamically changeable for what it's // worth. disable_pow_compilation: unchangeable, } Ok(other) } /// Return the DosParams extension we should send for this configuration, if any. pub(crate) fn dos_extension(&self) -> Result, crate::FatalError> { Ok(self .rate_limit_at_intro .as_ref() .map(dos_params_from_token_bucket_config) .transpose() .map_err(into_internal!( "somehow built an un-validated rate-limit-at-intro" ))?) } /// Return a RequestFilter based on this configuration. pub(crate) fn filter_settings(&self) -> crate::rend_handshake::RequestFilter { crate::rend_handshake::RequestFilter { max_concurrent_streams: self.max_concurrent_streams_per_circuit as usize, } } } impl OnionServiceConfigBuilder { /// Builder helper: check whether the options in this builder are consistent. fn validate(&self) -> Result<(), ConfigBuildError> { /// Largest number of introduction points supported. /// /// (This is not a very principled value; it's just copied from the C /// implementation.) const MAX_NUM_INTRO_POINTS: u8 = 20; /// Supported range of numbers of intro points. const ALLOWED_NUM_INTRO_POINTS: std::ops::RangeInclusive = DEFAULT_NUM_INTRO_POINTS..=MAX_NUM_INTRO_POINTS; // Make sure MAX_INTRO_POINTS is in range. if let Some(ipts) = self.num_intro_points { if !ALLOWED_NUM_INTRO_POINTS.contains(&ipts) { return Err(ConfigBuildError::Invalid { field: "num_intro_points".into(), problem: format!( "out of range {}-{}", DEFAULT_NUM_INTRO_POINTS, MAX_NUM_INTRO_POINTS ), }); } } // Make sure that our rate_limit_at_intro is valid. if let Some(Some(ref rate_limit)) = self.rate_limit_at_intro { let _ignore_extension: est_intro::DosParams = dos_params_from_token_bucket_config(rate_limit)?; } cfg_if::cfg_if! { if #[cfg(not(feature = "hs-pow-full"))] { if self.enable_pow == Some(true) { // TODO (#2020) is it correct for this to raise a error? return Err(ConfigBuildError::NoCompileTimeSupport { field: "enable_pow".into(), problem: "Arti was built without hs-pow-full feature!".into() }); } } } Ok(()) } /// Return the configured nickname for this service, if it has one. pub fn peek_nickname(&self) -> Option<&HsNickname> { self.nickname.as_ref() } } /// Configure a token-bucket style limit on some process. // // TODO: Someday we may wish to lower this; it will be used in far more places. // // TODO: Do we want to parameterize this, or make it always u32? Do we want to // specify "per second"? #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct TokenBucketConfig { /// The maximum number of items to process per second. rate: u32, /// The maximum number of items to process in a single burst. burst: u32, } impl TokenBucketConfig { /// Create a new token-bucket configuration to rate-limit some action. /// /// The "bucket" will have a maximum capacity of `burst`, and will fill at a /// rate of `rate` per second. New actions are permitted if the bucket is nonempty; /// each action removes one token from the bucket. pub fn new(rate: u32, burst: u32) -> Self { Self { rate, burst } } } /// Helper: Try to create a DosParams from a given token bucket configuration. /// Give an error if the value is out of range. /// /// This is a separate function so we can use the same logic when validating /// and when making the extension object. fn dos_params_from_token_bucket_config( c: &TokenBucketConfig, ) -> Result { let err = || ConfigBuildError::Invalid { field: "rate_limit_at_intro".into(), problem: "out of range".into(), }; let cast = |n| i32::try_from(n).map_err(|_| err()); est_intro::DosParams::new(Some(cast(c.rate)?), Some(cast(c.burst)?)).map_err(|_| err()) }