Skip to main content

tor_hsservice/
config.rs

1//! Configuration information for onion services.
2
3use crate::internal_prelude::*;
4
5use amplify::Getters;
6use derive_deftly::derive_deftly_adhoc;
7use tor_cell::relaycell::hs::est_intro;
8use tor_config::derive::prelude::*;
9
10use crate::config::restricted_discovery::{
11    RestrictedDiscoveryConfig, RestrictedDiscoveryConfigBuilder,
12};
13
14#[cfg(feature = "restricted-discovery")]
15pub mod restricted_discovery;
16
17// Only exported with pub visibility if the restricted-discovery feature is enabled.
18#[cfg(not(feature = "restricted-discovery"))]
19// Use cfg(all()) to prevent this from being documented as
20// "Available on non-crate feature `restricted-discovery` only"
21#[cfg_attr(docsrs, doc(cfg(all())))]
22pub(crate) mod restricted_discovery;
23
24/// Configuration for one onion service.
25#[derive(Debug, Clone, Eq, PartialEq, Deftly, Getters)]
26#[derive_deftly(TorConfig)]
27#[derive_deftly_adhoc]
28#[deftly(tor_config(no_default_trait, pre_build = "Self::validate"))]
29pub struct OnionServiceConfig {
30    /// The nickname used to look up this service's keys, state, configuration, etc.
31    #[deftly(publisher_view)]
32    #[deftly(tor_config(no_default))]
33    pub(crate) nickname: HsNickname,
34
35    /// If true, this service will be started. It should be available to
36    /// commands that don't require it to start regardless.
37    #[deftly(tor_config(default = "true"))]
38    pub(crate) enabled: bool,
39
40    /// Number of intro points; defaults to 3; max 20.
41    #[deftly(tor_config(default = "DEFAULT_NUM_INTRO_POINTS"))]
42    pub(crate) num_intro_points: u8,
43
44    /// A rate-limit on the acceptable rate of introduction requests.
45    ///
46    /// We send this to the introduction point to configure how many
47    /// introduction requests it sends us.
48    /// If this is not set, the introduction point chooses a default based on
49    /// the current consensus.
50    ///
51    /// We do not enforce this limit ourselves.
52    ///
53    /// This configuration is sent as a `DOS_PARAMS` extension, as documented in
54    /// <https://spec.torproject.org/rend-spec/introduction-protocol.html#EST_INTRO_DOS_EXT>.
55    #[deftly(tor_config(default))]
56    rate_limit_at_intro: Option<TokenBucketConfig>,
57
58    /// How many streams will we allow to be open at once for a single circuit on
59    /// this service?
60    ///
61    /// If a client attempts to open more than this many streams on a rendezvous circuit,
62    /// the circuit will be torn down.
63    ///
64    /// Equivalent to C Tor's HiddenServiceMaxStreamsCloseCircuit option.
65    #[deftly(tor_config(default = "65535"))]
66    max_concurrent_streams_per_circuit: u32,
67
68    /// If true, we will require proof-of-work when we're under heavy load.
69    #[deftly(tor_config(default = "false"))]
70    #[deftly(publisher_view)]
71    pub(crate) enable_pow: bool,
72
73    /// The maximum number of entries allowed in the rendezvous request queue when PoW is enabled.
74    ///
75    /// If you are seeing dropped requests, have a bursty traffic pattern, and have some memory to
76    /// spare, you may want to increase this.
77    ///
78    /// Each request will take a few KB, the default queue is expected to take 32MB at most.
79    // The "a few KB" measurement was done by using the get_size crate to
80    // measure the size of the RendRequest object, but due to limitations in
81    // that crate (and in my willingness to go implement ways of checking the
82    // size of external types), it might be somewhat off. The ~32MB value is
83    // based on the idea that each RendRequest is 4KB.
84    #[deftly(tor_config(default = "8192"))]
85    pub(crate) pow_rend_queue_depth: usize,
86
87    /// Configure restricted discovery mode.
88    ///
89    /// When this is enabled, we encrypt our list of introduction point and keys
90    /// so that only clients holding one of the listed keys can decrypt it.
91    #[deftly(tor_config(sub_builder))]
92    #[deftly(publisher_view)]
93    #[getter(as_mut)]
94    pub(crate) restricted_discovery: RestrictedDiscoveryConfig,
95
96    // TODO(#727): add support for single onion services
97    //
98    // TODO: Perhaps this belongs at a higher level.  Perhaps we don't need it
99    // at all.
100    //
101    // enabled: bool,
102    // /// Whether we want this to be a non-anonymous "single onion service".
103    // /// We could skip this in v1.  We should make sure that our state
104    // /// is built to make it hard to accidentally set this.
105    // #[builder(default)]
106    // #[deftly(publisher_view)]
107    // pub(crate) anonymity: crate::Anonymity,
108    /// Whether to use the compiled backend for proof-of-work.
109    // TODO: Consider making this a global option instead?
110    #[deftly(tor_config(default = "false"))]
111    disable_pow_compilation: bool,
112}
113
114derive_deftly_adhoc! {
115    OnionServiceConfig expect items:
116
117    ${defcond PUBLISHER_VIEW fmeta(publisher_view)}
118
119    #[doc = concat!("Descriptor publisher's view of [`", stringify!($tname), "`]")]
120    #[derive(PartialEq, Clone, Debug)]
121    pub(crate) struct $<$tname PublisherView><$tdefgens>
122    where $twheres
123    ${vdefbody $vname $(
124        ${when PUBLISHER_VIEW}
125        ${fattrs doc}
126        $fvis $fname: $ftype,
127    ) }
128
129    impl<$tgens> From<$tname> for $<$tname PublisherView><$tdefgens>
130    where $twheres
131    {
132        fn from(config: $tname) -> $<$tname PublisherView><$tdefgens> {
133            Self {
134                $(
135                    ${when PUBLISHER_VIEW}
136                    $fname: config.$fname,
137                )
138            }
139        }
140    }
141
142    impl<$tgens> From<&$tname> for $<$tname PublisherView><$tdefgens>
143    where $twheres
144    {
145        fn from(config: &$tname) -> $<$tname PublisherView><$tdefgens> {
146            Self {
147                $(
148                    ${when PUBLISHER_VIEW}
149                    #[allow(clippy::clone_on_copy)] // some fields are Copy
150                    $fname: config.$fname.clone(),
151                )
152            }
153        }
154    }
155}
156
157/// Default number of introduction points.
158const DEFAULT_NUM_INTRO_POINTS: u8 = 3;
159
160impl OnionServiceConfig {
161    /// Check whether an onion service running with this configuration can
162    /// switch over `other` according to the rules of `how`.
163    ///
164    //  Return an error if it can't; otherwise return the new config that we
165    //  should change to.
166    pub(crate) fn for_transition_to(
167        &self,
168        mut other: OnionServiceConfig,
169        how: tor_config::Reconfigure,
170    ) -> Result<OnionServiceConfig, tor_config::ReconfigureError> {
171        /// Arguments to a handler for a field
172        ///
173        /// The handler must:
174        ///  * check whether this field can be updated
175        ///  * if necessary, throw an error (in which case `*other` may be wrong)
176        ///  * if it doesn't throw an error, ensure that `*other`
177        ///    is appropriately updated.
178        //
179        // We could have a trait but that seems overkill.
180        #[allow(clippy::missing_docs_in_private_items)] // avoid otiosity
181        struct HandlerInput<'i, 'o, T> {
182            how: tor_config::Reconfigure,
183            self_: &'i T,
184            other: &'o mut T,
185            field_name: &'i str,
186        }
187        /// Convenience alias
188        type HandlerResult = Result<(), tor_config::ReconfigureError>;
189
190        /// Handler for config fields that cannot be changed
191        #[allow(clippy::needless_pass_by_value)]
192        fn unchangeable<T: Clone + PartialEq>(i: HandlerInput<T>) -> HandlerResult {
193            if i.self_ != i.other {
194                i.how.cannot_change(i.field_name)?;
195                // If we reach here, then `how` is WarnOnFailures, so we keep the
196                // original value.
197                *i.other = i.self_.clone();
198            }
199            Ok(())
200        }
201        /// Handler for config fields that can be freely changed
202        #[allow(clippy::unnecessary_wraps)]
203        fn simply_update<T>(_: HandlerInput<T>) -> HandlerResult {
204            Ok(())
205        }
206
207        /// Check all the fields.  Input maps fields to handlers.
208        macro_rules! fields { {
209            $(
210                $field:ident: $handler:expr
211            ),* $(,)?
212        } => {
213            // prove that we have handled every field
214            let OnionServiceConfig { $( $field: _, )* } = self;
215
216            $(
217                $handler(HandlerInput {
218                    how,
219                    self_: &self.$field,
220                    other: &mut other.$field,
221                    field_name: stringify!($field),
222                })?;
223            )*
224        } }
225
226        fields! {
227            nickname: unchangeable,
228
229            // TODO: allow starting/stopping onion services while the client is
230            // running
231            enabled: unchangeable,
232
233            // IPT manager will respond by adding or removing IPTs as desired.
234            // (Old IPTs are not proactively removed, but they will not be replaced
235            // as they are rotated out.)
236            num_intro_points: simply_update,
237
238            // IPT manager's "new configuration" select arm handles this,
239            // by replacing IPTs if necessary.
240            rate_limit_at_intro: simply_update,
241
242            // We extract this on every introduction request.
243            max_concurrent_streams_per_circuit: simply_update,
244
245            // The descriptor publisher responds by generating and publishing a new descriptor.
246            restricted_discovery: simply_update,
247
248            // TODO (#2082): allow changing enable_pow while the client is running
249            enable_pow: unchangeable,
250
251            // Do note that if the depth of the queue is decreased at runtime to a value smaller
252            // than the number of items in the queue, that will prevent new requests from coming in
253            // until the queue is smaller than the new size, but if will not trim the existing
254            // queue.
255            pow_rend_queue_depth: simply_update,
256
257            // This is a little too much effort to allow to by dynamically changeable for what it's
258            // worth.
259            disable_pow_compilation: unchangeable,
260        }
261
262        Ok(other)
263    }
264
265    /// Return the DosParams extension we should send for this configuration, if any.
266    pub(crate) fn dos_extension(&self) -> Result<Option<est_intro::DosParams>, crate::FatalError> {
267        Ok(self
268            .rate_limit_at_intro
269            .as_ref()
270            .map(dos_params_from_token_bucket_config)
271            .transpose()
272            .map_err(into_internal!(
273                "somehow built an un-validated rate-limit-at-intro"
274            ))?)
275    }
276
277    /// Return a RequestFilter based on this configuration.
278    pub(crate) fn filter_settings(&self) -> crate::rend_handshake::RequestFilter {
279        crate::rend_handshake::RequestFilter {
280            max_concurrent_streams: self.max_concurrent_streams_per_circuit as usize,
281        }
282    }
283}
284
285impl OnionServiceConfigBuilder {
286    /// Builder helper: check whether the options in this builder are consistent.
287    fn validate(&self) -> Result<(), ConfigBuildError> {
288        /// Largest number of introduction points supported.
289        ///
290        /// (This is not a very principled value; it's just copied from the C
291        /// implementation.)
292        const MAX_NUM_INTRO_POINTS: u8 = 20;
293        /// Supported range of numbers of intro points.
294        const ALLOWED_NUM_INTRO_POINTS: std::ops::RangeInclusive<u8> =
295            DEFAULT_NUM_INTRO_POINTS..=MAX_NUM_INTRO_POINTS;
296
297        // Make sure MAX_INTRO_POINTS is in range.
298        if let Some(ipts) = self.num_intro_points {
299            if !ALLOWED_NUM_INTRO_POINTS.contains(&ipts) {
300                return Err(ConfigBuildError::Invalid {
301                    field: "num_intro_points".into(),
302                    problem: format!(
303                        "out of range {}-{}",
304                        DEFAULT_NUM_INTRO_POINTS, MAX_NUM_INTRO_POINTS
305                    ),
306                });
307            }
308        }
309
310        // Make sure that our rate_limit_at_intro is valid.
311        if let Some(Some(ref rate_limit)) = self.rate_limit_at_intro {
312            let _ignore_extension: est_intro::DosParams =
313                dos_params_from_token_bucket_config(rate_limit)?;
314        }
315
316        cfg_if::cfg_if! {
317            if #[cfg(not(feature = "hs-pow-full"))] {
318                if self.enable_pow == Some(true) {
319                    // TODO (#2020) is it correct for this to raise a error?
320                    return Err(ConfigBuildError::NoCompileTimeSupport { field: "enable_pow".into(), problem: "Arti was built without hs-pow-full feature!".into() });
321                }
322            }
323        }
324
325        Ok(())
326    }
327
328    /// Return the configured nickname for this service, if it has one.
329    pub fn peek_nickname(&self) -> Option<&HsNickname> {
330        self.nickname.as_ref()
331    }
332}
333
334/// Configure a token-bucket style limit on some process.
335//
336// TODO: Someday we may wish to lower this; it will be used in far more places.
337//
338// TODO: Do we want to parameterize this, or make it always u32?  Do we want to
339// specify "per second"?
340#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
341pub struct TokenBucketConfig {
342    /// The maximum number of items to process per second.
343    rate: u32,
344    /// The maximum number of items to process in a single burst.
345    burst: u32,
346}
347
348impl TokenBucketConfig {
349    /// Create a new token-bucket configuration to rate-limit some action.
350    ///
351    /// The "bucket" will have a maximum capacity of `burst`, and will fill at a
352    /// rate of `rate` per second.  New actions are permitted if the bucket is nonempty;
353    /// each action removes one token from the bucket.
354    pub fn new(rate: u32, burst: u32) -> Self {
355        Self { rate, burst }
356    }
357}
358
359/// Helper: Try to create a DosParams from a given token bucket configuration.
360/// Give an error if the value is out of range.
361///
362/// This is a separate function so we can use the same logic when validating
363/// and when making the extension object.
364fn dos_params_from_token_bucket_config(
365    c: &TokenBucketConfig,
366) -> Result<est_intro::DosParams, ConfigBuildError> {
367    let err = || ConfigBuildError::Invalid {
368        field: "rate_limit_at_intro".into(),
369        problem: "out of range".into(),
370    };
371    let cast = |n| i32::try_from(n).map_err(|_| err());
372    est_intro::DosParams::new(Some(cast(c.rate)?), Some(cast(c.burst)?)).map_err(|_| err())
373}