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
use tor_config::derive::prelude::*;
9

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

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

            
114
derive_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
16
        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.
158
const DEFAULT_NUM_INTRO_POINTS: u8 = 3;
159

            
160
impl 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
16
    pub(crate) fn dos_extension(&self) -> Result<Option<est_intro::DosParams>, crate::FatalError> {
267
16
        Ok(self
268
16
            .rate_limit_at_intro
269
16
            .as_ref()
270
16
            .map(dos_params_from_token_bucket_config)
271
16
            .transpose()
272
16
            .map_err(into_internal!(
273
                "somehow built an un-validated rate-limit-at-intro"
274
            ))?)
275
16
    }
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

            
285
impl OnionServiceConfigBuilder {
286
    /// Builder helper: check whether the options in this builder are consistent.
287
1308
    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
1308
        if let Some(ipts) = self.num_intro_points {
299
37
            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
37
            }
308
1271
        }
309

            
310
        // Make sure that our rate_limit_at_intro is valid.
311
8
        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
1308
        }
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
1308
        Ok(())
326
1308
    }
327

            
328
    /// Return the configured nickname for this service, if it has one.
329
1073
    pub fn peek_nickname(&self) -> Option<&HsNickname> {
330
1073
        self.nickname.as_ref()
331
1073
    }
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)]
341
pub 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

            
348
impl 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.
364
fn 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
}