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