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}