1
//! Configuration information for onion services.
2

            
3
use crate::internal_prelude::*;
4

            
5
use amplify::Getters;
6
use derive_deftly::derive_deftly_adhoc;
7
use tor_cell::relaycell::hs::est_intro;
8

            
9
use crate::config::restricted_discovery::{
10
    RestrictedDiscoveryConfig, RestrictedDiscoveryConfigBuilder,
11
};
12

            
13
#[cfg(feature = "restricted-discovery")]
14
pub 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())))]
21
pub(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]
29
pub 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

            
103
derive_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
16
        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.
147
const DEFAULT_NUM_INTRO_POINTS: u8 = 3;
148

            
149
impl 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
16
    pub(crate) fn dos_extension(&self) -> Result<Option<est_intro::DosParams>, crate::FatalError> {
252
16
        Ok(self
253
16
            .rate_limit_at_intro
254
16
            .as_ref()
255
16
            .map(dos_params_from_token_bucket_config)
256
16
            .transpose()
257
16
            .map_err(into_internal!(
258
                "somehow built an un-validated rate-limit-at-intro"
259
            ))?)
260
16
    }
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

            
270
impl OnionServiceConfigBuilder {
271
    /// Builder helper: check whether the options in this builder are consistent.
272
1274
    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
1274
        if let Some(ipts) = self.num_intro_points {
284
36
            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
36
            }
293
1238
        }
294

            
295
        // Make sure that our rate_limit_at_intro is valid.
296
8
        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
1274
        }
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
1274
        Ok(())
311
1274
    }
312

            
313
    /// Return the configured nickname for this service, if it has one.
314
1044
    pub fn peek_nickname(&self) -> Option<&HsNickname> {
315
1044
        self.nickname.as_ref()
316
1044
    }
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)]
326
pub 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

            
333
impl 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.
349
fn 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
}