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 // 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
100derive_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 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.
144const DEFAULT_NUM_INTRO_POINTS: u8 = 3;
145
146impl 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 pub(crate) fn dos_extension(&self) -> Result<Option<est_intro::DosParams>, crate::FatalError> {
239 Ok(self
240 .rate_limit_at_intro
241 .as_ref()
242 .map(dos_params_from_token_bucket_config)
243 .transpose()
244 .map_err(into_internal!(
245 "somehow built an un-validated rate-limit-at-intro"
246 ))?)
247 }
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
257impl OnionServiceConfigBuilder {
258 /// Builder helper: check whether the options in this builder are consistent.
259 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 if let Some(ipts) = self.num_intro_points {
271 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 }
280 }
281
282 // Make sure that our rate_limit_at_intro is valid.
283 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 }
287
288 Ok(())
289 }
290
291 /// Return the configured nickname for this service, if it has one.
292 pub fn peek_nickname(&self) -> Option<&HsNickname> {
293 self.nickname.as_ref()
294 }
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)]
304pub 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
311impl 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.
327fn 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}