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
962
#[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
    // TODO POW: If this is set to true but the pow feature is disabled we should error.
59
    #[builder(default = "false")]
60
    #[deftly(publisher_view)]
61
    pub(crate) enable_pow: bool,
62

            
63
    /// Configure restricted discovery mode.
64
    ///
65
    /// When this is enabled, we encrypt our list of introduction point and keys
66
    /// so that only clients holding one of the listed keys can decrypt it.
67
    #[builder(sub_builder)]
68
    #[builder_field_attr(serde(default))]
69
    #[deftly(publisher_view)]
70
    #[getter(as_mut)]
71
    pub(crate) restricted_discovery: RestrictedDiscoveryConfig,
72
    // TODO(#727): add support for single onion services
73
    //
74
    // TODO: Perhaps this belongs at a higher level.  Perhaps we don't need it
75
    // at all.
76
    //
77
    // enabled: bool,
78
    // /// Whether we want this to be a non-anonymous "single onion service".
79
    // /// We could skip this in v1.  We should make sure that our state
80
    // /// is built to make it hard to accidentally set this.
81
    // #[builder(default)]
82
    // #[deftly(publisher_view)]
83
    // pub(crate) anonymity: crate::Anonymity,
84

            
85
    // TODO POW: The POW items are disabled for now, since they aren't implemented.
86
    // /// Disable the compiled backend for proof-of-work.
87
    // // disable_pow_compilation: bool,
88

            
89
    // TODO POW: C tor has this, but I don't know if we want it.
90
    //
91
    // TODO POW: It's possible that we want this to relate, somehow, to our
92
    // rate_limit_at_intro settings.
93
    //
94
    // /// A rate-limit on dispatching requests from the request queue when
95
    // /// our proof-of-work defense is enabled.
96
    // pow_queue_rate: TokenBucketConfig,
97
    // ...
98
}
99

            
100
derive_deftly_adhoc! {
101
    OnionServiceConfig expect items:
102

            
103
    ${defcond PUBLISHER_VIEW fmeta(publisher_view)}
104

            
105
    #[doc = concat!("Descriptor publisher's view of [`", stringify!($tname), "`]")]
106
    #[derive(PartialEq, Clone, Debug)]
107
    pub(crate) struct $<$tname PublisherView><$tdefgens>
108
    where $twheres
109
    ${vdefbody $vname $(
110
        ${when PUBLISHER_VIEW}
111
        ${fattrs doc}
112
        $fvis $fname: $ftype,
113
    ) }
114

            
115
    impl<$tgens> From<$tname> for $<$tname PublisherView><$tdefgens>
116
    where $twheres
117
    {
118
        fn from(config: $tname) -> $<$tname PublisherView><$tdefgens> {
119
            Self {
120
                $(
121
                    ${when PUBLISHER_VIEW}
122
                    $fname: config.$fname,
123
                )
124
            }
125
        }
126
    }
127

            
128
    impl<$tgens> From<&$tname> for $<$tname PublisherView><$tdefgens>
129
    where $twheres
130
    {
131
16
        fn from(config: &$tname) -> $<$tname PublisherView><$tdefgens> {
132
            Self {
133
                $(
134
                    ${when PUBLISHER_VIEW}
135
                    #[allow(clippy::clone_on_copy)] // some fields are Copy
136
                    $fname: config.$fname.clone(),
137
                )
138
            }
139
        }
140
    }
141
}
142

            
143
/// Default number of introduction points.
144
const DEFAULT_NUM_INTRO_POINTS: u8 = 3;
145

            
146
impl OnionServiceConfig {
147
    /// Check whether an onion service running with this configuration can
148
    /// switch over `other` according to the rules of `how`.
149
    ///
150
    //  Return an error if it can't; otherwise return the new config that we
151
    //  should change to.
152
    pub(crate) fn for_transition_to(
153
        &self,
154
        mut other: OnionServiceConfig,
155
        how: tor_config::Reconfigure,
156
    ) -> Result<OnionServiceConfig, tor_config::ReconfigureError> {
157
        /// Arguments to a handler for a field
158
        ///
159
        /// The handler must:
160
        ///  * check whether this field can be updated
161
        ///  * if necessary, throw an error (in which case `*other` may be wrong)
162
        ///  * if it doesn't throw an error, ensure that `*other`
163
        ///    is appropriately updated.
164
        //
165
        // We could have a trait but that seems overkill.
166
        #[allow(clippy::missing_docs_in_private_items)] // avoid otiosity
167
        struct HandlerInput<'i, 'o, T> {
168
            how: tor_config::Reconfigure,
169
            self_: &'i T,
170
            other: &'o mut T,
171
            field_name: &'i str,
172
        }
173
        /// Convenience alias
174
        type HandlerResult = Result<(), tor_config::ReconfigureError>;
175

            
176
        /// Handler for config fields that cannot be changed
177
        #[allow(clippy::needless_pass_by_value)]
178
        fn unchangeable<T: Clone + PartialEq>(i: HandlerInput<T>) -> HandlerResult {
179
            if i.self_ != i.other {
180
                i.how.cannot_change(i.field_name)?;
181
                // If we reach here, then `how` is WarnOnFailures, so we keep the
182
                // original value.
183
                *i.other = i.self_.clone();
184
            }
185
            Ok(())
186
        }
187
        /// Handler for config fields that can be freely changed
188
        #[allow(clippy::unnecessary_wraps)]
189
        fn simply_update<T>(_: HandlerInput<T>) -> HandlerResult {
190
            Ok(())
191
        }
192

            
193
        /// Check all the fields.  Input maps fields to handlers.
194
        macro_rules! fields { {
195
            $(
196
                $field:ident: $handler:expr
197
            ),* $(,)?
198
        } => {
199
            // prove that we have handled every field
200
            let OnionServiceConfig { $( $field: _, )* } = self;
201

            
202
            $(
203
                $handler(HandlerInput {
204
                    how,
205
                    self_: &self.$field,
206
                    other: &mut other.$field,
207
                    field_name: stringify!($field),
208
                })?;
209
            )*
210
        } }
211

            
212
        fields! {
213
            nickname: unchangeable,
214

            
215
            // IPT manager will respond by adding or removing IPTs as desired.
216
            // (Old IPTs are not proactively removed, but they will not be replaced
217
            // as they are rotated out.)
218
            num_intro_points: simply_update,
219

            
220
            // IPT manager's "new configuration" select arm handles this,
221
            // by replacing IPTs if necessary.
222
            rate_limit_at_intro: simply_update,
223

            
224
            // We extract this on every introduction request.
225
            max_concurrent_streams_per_circuit: simply_update,
226

            
227
            // The descriptor publisher responds by generating and publishing a new descriptor.
228
            restricted_discovery: simply_update,
229

            
230
            // TODO POW: Verify that simply_update has correct behaviour here.
231
            enable_pow: simply_update,
232
        }
233

            
234
        Ok(other)
235
    }
236

            
237
    /// Return the DosParams extension we should send for this configuration, if any.
238
16
    pub(crate) fn dos_extension(&self) -> Result<Option<est_intro::DosParams>, crate::FatalError> {
239
16
        Ok(self
240
16
            .rate_limit_at_intro
241
16
            .as_ref()
242
16
            .map(dos_params_from_token_bucket_config)
243
16
            .transpose()
244
16
            .map_err(into_internal!(
245
16
                "somehow built an un-validated rate-limit-at-intro"
246
16
            ))?)
247
16
    }
248

            
249
    /// Return a RequestFilter based on this configuration.
250
    pub(crate) fn filter_settings(&self) -> crate::rend_handshake::RequestFilter {
251
        crate::rend_handshake::RequestFilter {
252
            max_concurrent_streams: self.max_concurrent_streams_per_circuit as usize,
253
        }
254
    }
255
}
256

            
257
impl OnionServiceConfigBuilder {
258
    /// Builder helper: check whether the options in this builder are consistent.
259
450
    fn validate(&self) -> Result<(), ConfigBuildError> {
260
        /// Largest number of introduction points supported.
261
        ///
262
        /// (This is not a very principled value; it's just copied from the C
263
        /// implementation.)
264
        const MAX_NUM_INTRO_POINTS: u8 = 20;
265
        /// Supported range of numbers of intro points.
266
        const ALLOWED_NUM_INTRO_POINTS: std::ops::RangeInclusive<u8> =
267
            DEFAULT_NUM_INTRO_POINTS..=MAX_NUM_INTRO_POINTS;
268

            
269
        // Make sure MAX_INTRO_POINTS is in range.
270
450
        if let Some(ipts) = self.num_intro_points {
271
36
            if !ALLOWED_NUM_INTRO_POINTS.contains(&ipts) {
272
                return Err(ConfigBuildError::Invalid {
273
                    field: "num_intro_points".into(),
274
                    problem: format!(
275
                        "out of range {}-{}",
276
                        DEFAULT_NUM_INTRO_POINTS, MAX_NUM_INTRO_POINTS
277
                    ),
278
                });
279
36
            }
280
414
        }
281

            
282
        // Make sure that our rate_limit_at_intro is valid.
283
8
        if let Some(Some(ref rate_limit)) = self.rate_limit_at_intro {
284
            let _ignore_extension: est_intro::DosParams =
285
                dos_params_from_token_bucket_config(rate_limit)?;
286
450
        }
287

            
288
450
        Ok(())
289
450
    }
290

            
291
    /// Return the configured nickname for this service, if it has one.
292
252
    pub fn peek_nickname(&self) -> Option<&HsNickname> {
293
252
        self.nickname.as_ref()
294
252
    }
295
}
296

            
297
/// Configure a token-bucket style limit on some process.
298
//
299
// TODO: Someday we may wish to lower this; it will be used in far more places.
300
//
301
// TODO: Do we want to parameterize this, or make it always u32?  Do we want to
302
// specify "per second"?
303
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
304
pub struct TokenBucketConfig {
305
    /// The maximum number of items to process per second.
306
    rate: u32,
307
    /// The maximum number of items to process in a single burst.
308
    burst: u32,
309
}
310

            
311
impl TokenBucketConfig {
312
    /// Create a new token-bucket configuration to rate-limit some action.
313
    ///
314
    /// The "bucket" will have a maximum capacity of `burst`, and will fill at a
315
    /// rate of `rate` per second.  New actions are permitted if the bucket is nonempty;
316
    /// each action removes one token from the bucket.
317
    pub fn new(rate: u32, burst: u32) -> Self {
318
        Self { rate, burst }
319
    }
320
}
321

            
322
/// Helper: Try to create a DosParams from a given token bucket configuration.
323
/// Give an error if the value is out of range.
324
///
325
/// This is a separate function so we can use the same logic when validating
326
/// and when making the extension object.
327
fn dos_params_from_token_bucket_config(
328
    c: &TokenBucketConfig,
329
) -> Result<est_intro::DosParams, ConfigBuildError> {
330
    let err = || ConfigBuildError::Invalid {
331
        field: "rate_limit_at_intro".into(),
332
        problem: "out of range".into(),
333
    };
334
    let cast = |n| i32::try_from(n).map_err(|_| err());
335
    est_intro::DosParams::new(Some(cast(c.rate)?), Some(cast(c.burst)?)).map_err(|_| err())
336
}