1
//! Types and functions to configure a Tor client.
2
//!
3
//! Some of these are re-exported from lower-level crates.
4

            
5
use crate::err::ErrorDetail;
6
use derive_builder::Builder;
7
use derive_more::AsRef;
8
use fs_mistrust::{Mistrust, MistrustBuilder};
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashMap;
11
use std::path::Path;
12
use std::path::PathBuf;
13
use std::result::Result as StdResult;
14
use std::time::Duration;
15

            
16
pub use tor_chanmgr::{ChannelConfig, ChannelConfigBuilder};
17
pub use tor_config::convert_helper_via_multi_line_list_builder;
18
pub use tor_config::impl_standard_builder;
19
pub use tor_config::list_builder::{MultilineListBuilder, MultilineListBuilderError};
20
pub use tor_config::mistrust::BuilderExt as _;
21
pub use tor_config::{BoolOrAuto, ConfigError};
22
pub use tor_config::{ConfigBuildError, ConfigurationSource, ConfigurationSources, Reconfigure};
23
pub use tor_config::{define_list_builder_accessors, define_list_builder_helper};
24
pub use tor_config_path::{CfgPath, CfgPathError, CfgPathResolver};
25
pub use tor_linkspec::{ChannelMethod, HasChanMethod, PtTransportName, TransportId};
26

            
27
pub use tor_guardmgr::bridge::BridgeConfigBuilder;
28

            
29
#[cfg(feature = "bridge-client")]
30
#[cfg_attr(docsrs, doc(cfg(feature = "bridge-client")))]
31
pub use tor_guardmgr::bridge::BridgeParseError;
32

            
33
use tor_guardmgr::bridge::BridgeConfig;
34
use tor_keymgr::config::{ArtiKeystoreConfig, ArtiKeystoreConfigBuilder};
35

            
36
/// Types for configuring how Tor circuits are built.
37
pub mod circ {
38
    pub use tor_circmgr::{
39
        CircMgrConfig, CircuitTiming, CircuitTimingBuilder, PathConfig, PathConfigBuilder,
40
        PreemptiveCircuitConfig, PreemptiveCircuitConfigBuilder,
41
    };
42
}
43

            
44
/// Types for configuring how Tor accesses its directory information.
45
pub mod dir {
46
    pub use tor_dircommon::authority::{AuthorityContacts, AuthorityContactsBuilder};
47
    pub use tor_dircommon::config::{
48
        DirTolerance, DirToleranceBuilder, DownloadScheduleConfig, DownloadScheduleConfigBuilder,
49
        NetworkConfig, NetworkConfigBuilder,
50
    };
51
    pub use tor_dircommon::retry::{DownloadSchedule, DownloadScheduleBuilder};
52
    pub use tor_dirmgr::{DirMgrConfig, FallbackDir, FallbackDirBuilder};
53
}
54

            
55
/// Types for configuring pluggable transports.
56
#[cfg(feature = "pt-client")]
57
pub mod pt {
58
    pub use tor_ptmgr::config::{TransportConfig, TransportConfigBuilder};
59
}
60

            
61
/// Types for configuring onion services.
62
#[cfg(feature = "onion-service-service")]
63
pub mod onion_service {
64
    pub use tor_hsservice::config::{OnionServiceConfig, OnionServiceConfigBuilder};
65
}
66

            
67
/// Types for configuring vanguards.
68
pub mod vanguards {
69
    pub use tor_guardmgr::{VanguardConfig, VanguardConfigBuilder};
70
}
71

            
72
/// Configuration for client behavior relating to addresses.
73
///
74
/// This type is immutable once constructed. To create an object of this type,
75
/// use [`ClientAddrConfigBuilder`].
76
///
77
/// You can replace this configuration on a running Arti client.  Doing so will
78
/// affect new streams and requests, but will have no effect on existing streams
79
/// and requests.
80
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
81
#[builder(build_fn(error = "ConfigBuildError"))]
82
#[builder(derive(Debug, Serialize, Deserialize))]
83
pub struct ClientAddrConfig {
84
    /// Should we allow attempts to make Tor connections to local addresses?
85
    ///
86
    /// This option is off by default, since (by default) Tor exits will
87
    /// always reject connections to such addresses.
88
    #[builder(default)]
89
    pub(crate) allow_local_addrs: bool,
90

            
91
    /// Should we allow attempts to connect to hidden services (`.onion` services)?
92
    ///
93
    /// This option is on by default.
94
    #[cfg(feature = "onion-service-client")]
95
    #[builder(default = "true")]
96
    pub(crate) allow_onion_addrs: bool,
97
}
98
impl_standard_builder! { ClientAddrConfig }
99

            
100
/// Configuration for client behavior relating to stream connection timeouts
101
///
102
/// This type is immutable once constructed. To create an object of this type,
103
/// use [`StreamTimeoutConfigBuilder`].
104
///
105
/// You can replace this configuration on a running Arti client.  Doing so will
106
/// affect new streams and requests, but will have no effect on existing streams
107
/// and requests—even those that are currently waiting.
108
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
109
#[builder(build_fn(error = "ConfigBuildError"))]
110
#[builder(derive(Debug, Serialize, Deserialize))]
111
#[non_exhaustive]
112
pub struct StreamTimeoutConfig {
113
    /// How long should we wait before timing out a stream when connecting
114
    /// to a host?
115
    #[builder(default = "default_connect_timeout()")]
116
    #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
117
    pub(crate) connect_timeout: Duration,
118

            
119
    /// How long should we wait before timing out when resolving a DNS record?
120
    #[builder(default = "default_dns_resolve_timeout()")]
121
    #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
122
    pub(crate) resolve_timeout: Duration,
123

            
124
    /// How long should we wait before timing out when resolving a DNS
125
    /// PTR record?
126
    #[builder(default = "default_dns_resolve_ptr_timeout()")]
127
    #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
128
    pub(crate) resolve_ptr_timeout: Duration,
129
}
130
impl_standard_builder! { StreamTimeoutConfig }
131

            
132
/// Return the default stream timeout
133
2754
fn default_connect_timeout() -> Duration {
134
2754
    Duration::new(10, 0)
135
2754
}
136

            
137
/// Return the default resolve timeout
138
2754
fn default_dns_resolve_timeout() -> Duration {
139
2754
    Duration::new(10, 0)
140
2754
}
141

            
142
/// Return the default PTR resolve timeout
143
2754
fn default_dns_resolve_ptr_timeout() -> Duration {
144
2754
    Duration::new(10, 0)
145
2754
}
146

            
147
/// Configuration for overriding the status of our software.
148
///
149
/// # Issues
150
///
151
/// We only check these configuration values when we receive a new consensus,
152
/// or when we're starting up.  Therefore, if you change these values,
153
/// they won't have any effect until the next consensus is received.
154
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
155
#[builder(build_fn(error = "ConfigBuildError"))]
156
#[builder(derive(Debug, Serialize, Deserialize))]
157
pub struct SoftwareStatusOverrideConfig {
158
    /// A list of protocols to pretend that we have,
159
    /// when checking whether our software is obsolete.
160
    //
161
    // We make this type a String in the builder, to avoid exposing Protocols in our API.
162
    #[builder(field(type = "String", build = "self.parse_protos()?"))]
163
    pub(crate) ignore_missing_required_protocols: tor_protover::Protocols,
164
}
165

            
166
impl SoftwareStatusOverrideConfigBuilder {
167
    /// Helper: Parse the ignore_missing_required_protocols field.
168
2818
    fn parse_protos(&self) -> Result<tor_protover::Protocols, ConfigBuildError> {
169
        use std::str::FromStr as _;
170
2818
        tor_protover::Protocols::from_str(&self.ignore_missing_required_protocols).map_err(|e| {
171
            ConfigBuildError::Invalid {
172
                field: "ignore_missing_required_protocols".to_string(),
173
                problem: e.to_string(),
174
            }
175
        })
176
2818
    }
177
}
178

            
179
/// Configuration for where information should be stored on disk.
180
///
181
/// By default, cache information will be stored in `${ARTI_CACHE}`, and
182
/// persistent state will be stored in `${ARTI_LOCAL_DATA}`.  That means that
183
/// _all_ programs using these defaults will share their cache and state data.
184
/// If that isn't what you want,  you'll need to override these directories.
185
///
186
/// On unix, the default directories will typically expand to `~/.cache/arti`
187
/// and `~/.local/share/arti/` respectively, depending on the user's
188
/// environment. Other platforms will also use suitable defaults. For more
189
/// information, see the documentation for [`CfgPath`].
190
///
191
/// This section is for read/write storage.
192
///
193
/// You cannot change this section on a running Arti client.
194
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
195
#[builder(build_fn(error = "ConfigBuildError"))]
196
#[builder(derive(Debug, Serialize, Deserialize))]
197
#[non_exhaustive]
198
pub struct StorageConfig {
199
    /// Location on disk for cached information.
200
    ///
201
    /// This follows the rules for `/var/cache`: "sufficiently old" filesystem objects
202
    /// in it may be deleted outside of the control of Arti,
203
    /// and Arti will continue to function properly.
204
    /// It is also fine to delete the directory as a whole, while Arti is not running.
205
    //
206
    // Usage note, for implementations of Arti components:
207
    //
208
    // When files in this directory are to be used by a component, the cache_dir
209
    // value should be passed through to the component as-is, and the component is
210
    // then responsible for constructing an appropriate sub-path (for example,
211
    // tor-dirmgr receives cache_dir, and appends components such as "dir_blobs".
212
    //
213
    // (This consistency rule is not current always followed by every component.)
214
    #[builder(setter(into), default = "default_cache_dir()")]
215
    cache_dir: CfgPath,
216

            
217
    /// Location on disk for less-sensitive persistent state information.
218
    // Usage note: see the note for `cache_dir`, above.
219
    #[builder(setter(into), default = "default_state_dir()")]
220
    state_dir: CfgPath,
221

            
222
    /// Location on disk for the Arti keystore.
223
    #[cfg(feature = "keymgr")]
224
    #[builder(sub_builder)]
225
    #[builder_field_attr(serde(default))]
226
    keystore: ArtiKeystoreConfig,
227

            
228
    /// Configuration about which permissions we want to enforce on our files.
229
    #[builder(sub_builder(fn_name = "build_for_arti"))]
230
    #[builder_field_attr(serde(default))]
231
    permissions: Mistrust,
232
}
233
impl_standard_builder! { StorageConfig }
234

            
235
/// Return the default cache directory.
236
2714
fn default_cache_dir() -> CfgPath {
237
2714
    CfgPath::new("${ARTI_CACHE}".to_owned())
238
2714
}
239

            
240
/// Return the default state directory.
241
1524
fn default_state_dir() -> CfgPath {
242
1524
    CfgPath::new("${ARTI_LOCAL_DATA}".to_owned())
243
1524
}
244

            
245
/// Macro to avoid repeating code for `expand_*_dir` functions on StorageConfig
246
// TODO: generate the expand_*_dir functions using d-a instead
247
macro_rules! expand_dir {
248
    ($self:ident, $dirname:ident, $dircfg:ident) => {
249
        $self
250
            .$dirname
251
            .path($dircfg)
252
            .map_err(|e| ConfigBuildError::Invalid {
253
                field: stringify!($dirname).to_owned(),
254
                problem: e.to_string(),
255
            })
256
    };
257
}
258

            
259
impl StorageConfig {
260
    /// Try to expand `state_dir` to be a path buffer.
261
1392
    pub(crate) fn expand_state_dir(
262
1392
        &self,
263
1392
        path_resolver: &CfgPathResolver,
264
1392
    ) -> Result<PathBuf, ConfigBuildError> {
265
1392
        expand_dir!(self, state_dir, path_resolver)
266
1392
    }
267
    /// Try to expand `cache_dir` to be a path buffer.
268
54
    pub(crate) fn expand_cache_dir(
269
54
        &self,
270
54
        path_resolver: &CfgPathResolver,
271
54
    ) -> Result<PathBuf, ConfigBuildError> {
272
54
        expand_dir!(self, cache_dir, path_resolver)
273
54
    }
274
    /// Return the keystore config
275
    #[allow(clippy::unnecessary_wraps)]
276
3538
    pub(crate) fn keystore(&self) -> ArtiKeystoreConfig {
277
        cfg_if::cfg_if! {
278
            if #[cfg(feature="keymgr")] {
279
3538
                self.keystore.clone()
280
            } else {
281
                Default::default()
282
            }
283
        }
284
3538
    }
285
    /// Return the FS permissions to use for state and cache directories.
286
3686
    pub(crate) fn permissions(&self) -> &Mistrust {
287
3686
        &self.permissions
288
3686
    }
289
}
290

            
291
/// Configuration for anti-censorship features: bridges and pluggable transports.
292
///
293
/// A "bridge" is a relay that is not listed in the regular Tor network directory;
294
/// clients use them to reach the network when a censor is blocking their
295
/// connection to all the regular Tor relays.
296
///
297
/// A "pluggable transport" is a tool that transforms and conceals a user's connection
298
/// to a bridge; clients use them to reach the network when a censor is blocking
299
/// all traffic that "looks like Tor".
300
///
301
/// A [`BridgesConfig`] configuration has the following pieces:
302
///    * A [`BridgeList`] of [`BridgeConfig`]s, which describes one or more bridges.
303
///    * An `enabled` boolean to say whether or not to use the listed bridges.
304
///    * A list of [`pt::TransportConfig`]s.
305
///
306
/// # Example
307
///
308
/// Here's an example of building a bridge configuration, and using it in a
309
/// TorClientConfig.
310
///
311
/// The bridges here are fictitious; you'll need to use real bridges
312
/// if you want a working configuration.
313
///
314
/// ```
315
/// ##[cfg(feature = "pt-client")]
316
/// # fn demo() -> anyhow::Result<()> {
317
/// use arti_client::config::{TorClientConfig, BridgeConfigBuilder, CfgPath};
318
/// // Requires that the pt-client feature is enabled.
319
/// use arti_client::config::pt::TransportConfigBuilder;
320
///
321
/// let mut builder = TorClientConfig::builder();
322
///
323
/// // Add a single bridge to the list of bridges, from a bridge line.
324
/// // This bridge line is made up for demonstration, and won't work.
325
/// const BRIDGE1_LINE : &str = "Bridge obfs4 192.0.2.55:38114 316E643333645F6D79216558614D3931657A5F5F cert=YXJlIGZyZXF1ZW50bHkgZnVsbCBvZiBsaXR0bGUgbWVzc2FnZXMgeW91IGNhbiBmaW5kLg iat-mode=0";
326
/// let bridge_1: BridgeConfigBuilder = BRIDGE1_LINE.parse()?;
327
/// // This is where we pass `BRIDGE1_LINE` into the BridgeConfigBuilder.
328
/// builder.bridges().bridges().push(bridge_1);
329
///
330
/// // Add a second bridge, built by hand.  This way is harder.
331
/// // This bridge is made up for demonstration, and won't work.
332
/// let mut bridge2_builder = BridgeConfigBuilder::default();
333
/// bridge2_builder
334
///     .transport("obfs4")
335
///     .push_setting("iat-mode", "1")
336
///     .push_setting(
337
///         "cert",
338
///         "YnV0IHNvbWV0aW1lcyB0aGV5IGFyZSByYW5kb20u8x9aQG/0cIIcx0ItBcTqiSXotQne+Q"
339
///     );
340
/// bridge2_builder.set_addrs(vec!["198.51.100.25:443".parse()?]);
341
/// bridge2_builder.set_ids(vec!["7DD62766BF2052432051D7B7E08A22F7E34A4543".parse()?]);
342
/// // Now insert the second bridge into our config builder.
343
/// builder.bridges().bridges().push(bridge2_builder);
344
///
345
/// // Now configure an obfs4 transport. (Requires the "pt-client" feature)
346
/// let mut transport = TransportConfigBuilder::default();
347
/// transport
348
///     .protocols(vec!["obfs4".parse()?])
349
///     // Specify either the name or the absolute path of pluggable transport client binary, this
350
///     // may differ from system to system.
351
///     .path(CfgPath::new("/usr/bin/obfs4proxy".into()))
352
///     .run_on_startup(true);
353
/// builder.bridges().transports().push(transport);
354
///
355
/// let config = builder.build()?;
356
/// // Now you can pass `config` to TorClient::create!
357
/// # Ok(())}
358
/// ```
359
/// You can also find an example based on snowflake in arti-client example folder.
360
//
361
// We leave this as an empty struct even when bridge support is disabled,
362
// as otherwise the default config file would generate an unknown section warning.
363
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
364
#[builder(build_fn(validate = "validate_bridges_config", error = "ConfigBuildError"))]
365
#[builder(derive(Debug, Serialize, Deserialize))]
366
#[non_exhaustive]
367
#[builder_struct_attr(non_exhaustive)] // This struct can be empty.
368
pub struct BridgesConfig {
369
    /// Should we use configured bridges?
370
    ///
371
    /// The default (`Auto`) is to use bridges if they are configured.
372
    /// `false` means to not use even configured bridges.
373
    /// `true` means to insist on the use of bridges;
374
    /// if none are configured, that's then an error.
375
    #[builder(default)]
376
    pub(crate) enabled: BoolOrAuto,
377

            
378
    /// Configured list of bridges (possibly via pluggable transports)
379
    #[builder(sub_builder, setter(custom))]
380
    #[builder_field_attr(serde(default))]
381
    bridges: BridgeList,
382

            
383
    /// Configured list of pluggable transports.
384
    #[builder(sub_builder, setter(custom))]
385
    #[builder_field_attr(serde(default))]
386
    #[cfg(feature = "pt-client")]
387
    pub(crate) transports: TransportConfigList,
388
}
389

            
390
/// A list of configured transport binaries (type alias for macrology).
391
#[cfg(feature = "pt-client")]
392
type TransportConfigList = Vec<pt::TransportConfig>;
393

            
394
#[cfg(feature = "pt-client")]
395
define_list_builder_helper! {
396
    pub struct TransportConfigListBuilder {
397
        transports: [pt::TransportConfigBuilder],
398
    }
399
    built: TransportConfigList = transports;
400
    default = vec![];
401
}
402

            
403
#[cfg(feature = "pt-client")]
404
define_list_builder_accessors! {
405
    struct BridgesConfigBuilder {
406
        pub transports: [pt::TransportConfigBuilder],
407
    }
408
}
409

            
410
impl_standard_builder! { BridgesConfig }
411

            
412
#[cfg(feature = "pt-client")]
413
/// Determine if we need any pluggable transports.
414
///
415
/// If we do and their transports don't exist, we have a problem
416
124
fn validate_pt_config(bridges: &BridgesConfigBuilder) -> Result<(), ConfigBuildError> {
417
    use std::collections::HashSet;
418
    use std::str::FromStr;
419

            
420
    // These are all the protocols that the user has defined
421
124
    let mut protocols_defined: HashSet<PtTransportName> = HashSet::new();
422
124
    if let Some(transportlist) = bridges.opt_transports() {
423
10
        for protocols in transportlist.iter() {
424
10
            for protocol in protocols.get_protocols() {
425
10
                protocols_defined.insert(protocol.clone());
426
10
            }
427
        }
428
114
    }
429

            
430
    // Iterate over all the transports that bridges are going to use
431
    // If any one is valid, we validate the entire config
432
124
    for maybe_protocol in bridges
433
124
        .bridges
434
124
        .bridges
435
124
        .as_deref()
436
124
        .unwrap_or_default()
437
124
        .iter()
438
    {
439
124
        match maybe_protocol.get_transport() {
440
124
            Some(raw_protocol) => {
441
                // We convert the raw protocol string representation
442
                // into a more proper one using PtTransportName
443
124
                let protocol = TransportId::from_str(raw_protocol)
444
                    // If id can't be parsed, simply skip it here.
445
                    // The rest of the config validation/processing will generate an error for it.
446
124
                    .unwrap_or_default()
447
124
                    .into_pluggable();
448
                // The None case represents when we aren't using a PT at all
449
124
                match protocol {
450
12
                    Some(protocol_required) => {
451
12
                        if protocols_defined.contains(&protocol_required) {
452
10
                            return Ok(());
453
2
                        }
454
                    }
455
112
                    None => return Ok(()),
456
                }
457
            }
458
            None => {
459
                return Ok(());
460
            }
461
        }
462
    }
463

            
464
2
    Err(ConfigBuildError::Inconsistent {
465
2
        fields: ["bridges.bridges", "bridges.transports"].map(Into::into).into_iter().collect(),
466
2
        problem: "Bridges configured, but all bridges unusable due to lack of corresponding pluggable transport in `[bridges.transports]`".into(),
467
2
    })
468
124
}
469

            
470
/// Check that the bridge configuration is right
471
#[allow(clippy::unnecessary_wraps)]
472
2864
fn validate_bridges_config(bridges: &BridgesConfigBuilder) -> Result<(), ConfigBuildError> {
473
2864
    let _ = bridges; // suppresses unused variable for just that argument
474

            
475
    use BoolOrAuto as BoA;
476

            
477
    // Ideally we would run this post-build, rather than pre-build;
478
    // doing it here means we have to recapitulate the defaulting.
479
    // Happily the defaulting is obvious, cheap, and not going to change.
480
    //
481
    // Alternatively we could have derive_builder provide `build_unvalidated`,
482
    // but that involves re-setting the build fn name for every field.
483
2864
    match (
484
2864
        bridges.enabled.unwrap_or_default(),
485
2864
        bridges.bridges.bridges.as_deref().unwrap_or_default(),
486
2864
    ) {
487
2860
        (BoA::Auto, _) | (BoA::Explicit(false), _) | (BoA::Explicit(true), [_, ..]) => {}
488
4
        (BoA::Explicit(true), []) => {
489
4
            return Err(ConfigBuildError::Inconsistent {
490
4
                fields: ["enabled", "bridges"].map(Into::into).into_iter().collect(),
491
4
                problem: "bridges.enabled=true, but no bridges defined".into(),
492
4
            });
493
        }
494
    }
495
    #[cfg(feature = "pt-client")]
496
    {
497
2860
        if bridges_enabled(
498
2860
            bridges.enabled.unwrap_or_default(),
499
2860
            bridges.bridges.bridges.as_deref().unwrap_or_default(),
500
        ) {
501
124
            validate_pt_config(bridges)?;
502
2736
        }
503
    }
504

            
505
2858
    Ok(())
506
2864
}
507

            
508
/// Generic logic to check if bridges should be used or not
509
2910
fn bridges_enabled(enabled: BoolOrAuto, bridges: &[impl Sized]) -> bool {
510
    #[cfg(feature = "bridge-client")]
511
    {
512
2910
        enabled.as_bool().unwrap_or(!bridges.is_empty())
513
    }
514

            
515
    #[cfg(not(feature = "bridge-client"))]
516
    {
517
        let _ = (enabled, bridges);
518
        false
519
    }
520
2910
}
521

            
522
impl BridgesConfig {
523
    /// Should the bridges be used?
524
50
    fn bridges_enabled(&self) -> bool {
525
50
        bridges_enabled(self.enabled, &self.bridges)
526
50
    }
527
}
528

            
529
/// List of configured bridges, as found in the built configuration
530
//
531
// This type alias arranges that we can put `BridgeList` in `BridgesConfig`
532
// and have derive_builder put a `BridgeListBuilder` in `BridgesConfigBuilder`.
533
pub type BridgeList = Vec<BridgeConfig>;
534

            
535
define_list_builder_helper! {
536
    struct BridgeListBuilder {
537
        bridges: [BridgeConfigBuilder],
538
    }
539
    built: BridgeList = bridges;
540
    default = vec![];
541
    #[serde(try_from="MultilineListBuilder<BridgeConfigBuilder>")]
542
    #[serde(into="MultilineListBuilder<BridgeConfigBuilder>")]
543
}
544

            
545
convert_helper_via_multi_line_list_builder! {
546
    struct BridgeListBuilder {
547
        bridges: [BridgeConfigBuilder],
548
    }
549
}
550

            
551
#[cfg(feature = "bridge-client")]
552
define_list_builder_accessors! {
553
    struct BridgesConfigBuilder {
554
        pub bridges: [BridgeConfigBuilder],
555
    }
556
}
557

            
558
/// A configuration used to bootstrap a [`TorClient`](crate::TorClient).
559
///
560
/// In order to connect to the Tor network, Arti needs to know a few
561
/// well-known directory caches on the network, and the public keys of the
562
/// network's directory authorities.  It also needs a place on disk to
563
/// store persistent state and cached directory information. (See [`StorageConfig`]
564
/// for default directories.)
565
///
566
/// Most users will create a TorClientConfig by running
567
/// [`TorClientConfig::default`].
568
///
569
/// If you need to override the locations where Arti stores its
570
/// information, you can make a TorClientConfig with
571
/// [`TorClientConfigBuilder::from_directories`].
572
///
573
/// Finally, you can get fine-grained control over the members of a
574
/// TorClientConfig using [`TorClientConfigBuilder`].
575
#[derive(Clone, Builder, Debug, AsRef, educe::Educe)]
576
#[educe(PartialEq, Eq)]
577
#[builder(build_fn(error = "ConfigBuildError"))]
578
#[builder(derive(Serialize, Deserialize, Debug))]
579
#[non_exhaustive]
580
pub struct TorClientConfig {
581
    /// Information about the Tor network we want to connect to.
582
    #[builder(sub_builder)]
583
    #[builder_field_attr(serde(default))]
584
    tor_network: dir::NetworkConfig,
585

            
586
    /// Directories for storing information on disk
587
    #[builder(sub_builder)]
588
    #[builder_field_attr(serde(default))]
589
    pub(crate) storage: StorageConfig,
590

            
591
    /// Information about when and how often to download directory information
592
    #[builder(sub_builder)]
593
    #[builder_field_attr(serde(default))]
594
    download_schedule: dir::DownloadScheduleConfig,
595

            
596
    /// Information about how premature or expired our directories are allowed
597
    /// to be.
598
    ///
599
    /// These options help us tolerate clock skew, and help survive the case
600
    /// where the directory authorities are unable to reach consensus for a
601
    /// while.
602
    #[builder(sub_builder)]
603
    #[builder_field_attr(serde(default))]
604
    directory_tolerance: dir::DirTolerance,
605

            
606
    /// Facility to override network parameters from the values set in the
607
    /// consensus.
608
    #[builder(
609
        sub_builder,
610
        field(
611
            type = "HashMap<String, i32>",
612
            build = "default_extend(self.override_net_params.clone())"
613
        )
614
    )]
615
    #[builder_field_attr(serde(default))]
616
    pub(crate) override_net_params: tor_netdoc::doc::netstatus::NetParams<i32>,
617

            
618
    /// Information about bridges, pluggable transports, and so on
619
    #[builder(sub_builder)]
620
    #[builder_field_attr(serde(default))]
621
    pub(crate) bridges: BridgesConfig,
622

            
623
    /// Information about how to build paths through the network.
624
    #[builder(sub_builder)]
625
    #[builder_field_attr(serde(default))]
626
    pub(crate) channel: ChannelConfig,
627

            
628
    /// Configuration for system resources used by Arti
629
    ///
630
    /// Note that there are other settings in this section,
631
    /// in `arti::cfg::SystemConfig` -
632
    /// these two structs overlay here.
633
    #[builder(sub_builder)]
634
    #[builder_field_attr(serde(default))]
635
    pub(crate) system: SystemConfig,
636

            
637
    /// Information about how to build paths through the network.
638
    #[as_ref]
639
    #[builder(sub_builder)]
640
    #[builder_field_attr(serde(default))]
641
    path_rules: circ::PathConfig,
642

            
643
    /// Information about preemptive circuits.
644
    #[as_ref]
645
    #[builder(sub_builder)]
646
    #[builder_field_attr(serde(default))]
647
    preemptive_circuits: circ::PreemptiveCircuitConfig,
648

            
649
    /// Information about how to retry and expire circuits and request for circuits.
650
    #[as_ref]
651
    #[builder(sub_builder)]
652
    #[builder_field_attr(serde(default))]
653
    circuit_timing: circ::CircuitTiming,
654

            
655
    /// Rules about which addresses the client is willing to connect to.
656
    #[builder(sub_builder)]
657
    #[builder_field_attr(serde(default))]
658
    pub(crate) address_filter: ClientAddrConfig,
659

            
660
    /// Information about timing out client requests.
661
    #[builder(sub_builder)]
662
    #[builder_field_attr(serde(default))]
663
    pub(crate) stream_timeouts: StreamTimeoutConfig,
664

            
665
    /// Information about vanguards.
666
    #[builder(sub_builder)]
667
    #[builder_field_attr(serde(default))]
668
    pub(crate) vanguards: vanguards::VanguardConfig,
669

            
670
    /// Support for running with known-obsolete versions.
671
    #[builder(sub_builder)]
672
    #[builder_field_attr(serde(default))]
673
    pub(crate) use_obsolete_software: SoftwareStatusOverrideConfig,
674

            
675
    /// Resolves paths in this configuration.
676
    ///
677
    /// This is not [reconfigurable](crate::TorClient::reconfigure).
678
    // We don't accept this from the builder/serde, and don't inspect it when comparing configs.
679
    // This should be considered as ancillary data rather than a configuration option.
680
    // TorClientConfig maybe isn't the best place for this, but this is where it needs to go to not
681
    // require public API changes.
682
    #[as_ref]
683
    #[builder(setter(skip))]
684
    #[builder_field_attr(serde(skip))]
685
    #[educe(PartialEq(ignore), Eq(ignore))]
686
    #[builder(default = "tor_config_path::arti_client_base_resolver()")]
687
    pub(crate) path_resolver: CfgPathResolver,
688
}
689
impl_standard_builder! { TorClientConfig }
690

            
691
impl tor_config::load::TopLevel for TorClientConfig {
692
    type Builder = TorClientConfigBuilder;
693
}
694

            
695
/// Helper to add overrides to a default collection.
696
2826
fn default_extend<T: Default + Extend<X>, X>(to_add: impl IntoIterator<Item = X>) -> T {
697
2826
    let mut collection = T::default();
698
2826
    collection.extend(to_add);
699
2826
    collection
700
2826
}
701

            
702
/// Configuration for system resources used by Tor.
703
///
704
/// You cannot change this section on a running Arti client.
705
///
706
/// Note that there are other settings in this section,
707
/// in `arti_client::config::SystemConfig`.
708
#[derive(Debug, Clone, Builder, Eq, PartialEq)]
709
#[builder(build_fn(error = "ConfigBuildError"))]
710
#[builder(derive(Debug, Serialize, Deserialize))]
711
#[non_exhaustive]
712
pub struct SystemConfig {
713
    /// Memory limits (approximate)
714
    #[builder(sub_builder(fn_name = "build"))]
715
    #[builder_field_attr(serde(default))]
716
    pub(crate) memory: tor_memquota::Config,
717
}
718
impl_standard_builder! { SystemConfig }
719

            
720
impl tor_circmgr::CircMgrConfig for TorClientConfig {
721
    #[cfg(all(
722
        feature = "vanguards",
723
        any(feature = "onion-service-client", feature = "onion-service-service")
724
    ))]
725
50
    fn vanguard_config(&self) -> &tor_guardmgr::VanguardConfig {
726
50
        &self.vanguards
727
50
    }
728
}
729
#[cfg(feature = "onion-service-client")]
730
impl tor_hsclient::HsClientConnectorConfig for TorClientConfig {}
731

            
732
#[cfg(any(feature = "onion-service-client", feature = "onion-service-service"))]
733
impl tor_circmgr::hspool::HsCircPoolConfig for TorClientConfig {
734
    #[cfg(all(
735
        feature = "vanguards",
736
        any(feature = "onion-service-client", feature = "onion-service-service")
737
    ))]
738
    fn vanguard_config(&self) -> &tor_guardmgr::VanguardConfig {
739
        &self.vanguards
740
    }
741
}
742

            
743
impl AsRef<tor_dircommon::fallback::FallbackList> for TorClientConfig {
744
50
    fn as_ref(&self) -> &tor_dircommon::fallback::FallbackList {
745
50
        self.tor_network.fallback_caches()
746
50
    }
747
}
748
impl AsRef<[BridgeConfig]> for TorClientConfig {
749
    fn as_ref(&self) -> &[BridgeConfig] {
750
        #[cfg(feature = "bridge-client")]
751
        {
752
            &self.bridges.bridges
753
        }
754

            
755
        #[cfg(not(feature = "bridge-client"))]
756
        {
757
            &[]
758
        }
759
    }
760
}
761
impl AsRef<BridgesConfig> for TorClientConfig {
762
34
    fn as_ref(&self) -> &BridgesConfig {
763
34
        &self.bridges
764
34
    }
765
}
766
impl tor_guardmgr::GuardMgrConfig for TorClientConfig {
767
50
    fn bridges_enabled(&self) -> bool {
768
50
        self.bridges.bridges_enabled()
769
50
    }
770
}
771

            
772
impl TorClientConfig {
773
    /// Try to create a DirMgrConfig corresponding to this object.
774
    #[rustfmt::skip]
775
54
    pub fn dir_mgr_config(&self) -> Result<dir::DirMgrConfig, ConfigBuildError> {
776
        Ok(dir::DirMgrConfig {
777
54
            network:             self.tor_network        .clone(),
778
54
            schedule:            self.download_schedule  .clone(),
779
54
            tolerance:           self.directory_tolerance.clone(),
780
54
            cache_dir:           self.storage.expand_cache_dir(&self.path_resolver)?,
781
54
            cache_trust:         self.storage.permissions.clone(),
782
54
            override_net_params: self.override_net_params.clone(),
783
54
            extensions:          Default::default(),
784
        })
785
54
    }
786

            
787
    /// Return a reference to the [`fs_mistrust::Mistrust`] object that we'll
788
    /// use to check permissions on files and directories by default.
789
    ///
790
    /// # Usage notes
791
    ///
792
    /// In the future, specific files or directories may have stricter or looser
793
    /// permissions checks applied to them than this default.  Callers shouldn't
794
    /// use this [`Mistrust`] to predict what Arti will accept for a specific
795
    /// file or directory.  Rather, you should use this if you have some file or
796
    /// directory of your own on which you'd like to enforce the same rules as
797
    /// Arti uses.
798
    //
799
    // NOTE: The presence of this accessor is _NOT_ in any form a commitment to
800
    // expose every field from the configuration as an accessor.  We explicitly
801
    // reject that slippery slope argument.
802
1190
    pub fn fs_mistrust(&self) -> &Mistrust {
803
1190
        self.storage.permissions()
804
1190
    }
805

            
806
    /// Return the keystore config
807
340
    pub fn keystore(&self) -> ArtiKeystoreConfig {
808
340
        self.storage.keystore()
809
340
    }
810

            
811
    /// Get the state directory and its corresponding
812
    /// [`Mistrust`] configuration.
813
1384
    pub(crate) fn state_dir(&self) -> StdResult<(PathBuf, &fs_mistrust::Mistrust), ErrorDetail> {
814
1384
        let state_dir = self
815
1384
            .storage
816
1384
            .expand_state_dir(&self.path_resolver)
817
1384
            .map_err(ErrorDetail::Configuration)?;
818
1384
        let mistrust = self.storage.permissions();
819

            
820
1384
        Ok((state_dir, mistrust))
821
1384
    }
822

            
823
    /// Access the `tor_memquota` configuration
824
    ///
825
    /// Ad-hoc accessor for testing purposes.
826
    /// (ideally we'd use `visibility` to make fields `pub`, but that doesn't work.)
827
    #[cfg(feature = "testing")]
828
34
    pub fn system_memory(&self) -> &tor_memquota::Config {
829
34
        &self.system.memory
830
34
    }
831
}
832

            
833
impl TorClientConfigBuilder {
834
    /// Returns a `TorClientConfigBuilder` using the specified state and cache directories.
835
    ///
836
    /// All other configuration options are set to their defaults, except `storage.keystore.path`,
837
    /// which is derived from the specified state directory.
838
14
    pub fn from_directories<P, Q>(state_dir: P, cache_dir: Q) -> Self
839
14
    where
840
14
        P: AsRef<Path>,
841
14
        Q: AsRef<Path>,
842
    {
843
14
        let mut builder = Self::default();
844

            
845
14
        builder
846
14
            .storage()
847
14
            .cache_dir(CfgPath::new_literal(cache_dir.as_ref()))
848
14
            .state_dir(CfgPath::new_literal(state_dir.as_ref()));
849

            
850
14
        builder
851
14
    }
852
}
853

            
854
/// Return the filenames for the default user configuration files
855
1464
pub fn default_config_files() -> Result<Vec<ConfigurationSource>, CfgPathError> {
856
    // the base path resolver includes the 'ARTI_CONFIG' variable
857
1464
    let path_resolver = tor_config_path::arti_client_base_resolver();
858

            
859
1464
    ["${ARTI_CONFIG}/arti.toml", "${ARTI_CONFIG}/arti.d/"]
860
1464
        .into_iter()
861
2972
        .map(|f| {
862
2928
            let path = CfgPath::new(f.into()).path(&path_resolver)?;
863
2928
            Ok(ConfigurationSource::from_path(path))
864
2928
        })
865
1464
        .collect()
866
1464
}
867

            
868
/// The environment variable we look at when deciding whether to disable FS permissions checking.
869
#[deprecated = "use tor-config::mistrust::ARTI_FS_DISABLE_PERMISSION_CHECKS instead"]
870
pub const FS_PERMISSIONS_CHECKS_DISABLE_VAR: &str = "ARTI_FS_DISABLE_PERMISSION_CHECKS";
871

            
872
/// Return true if the environment has been set up to disable FS permissions
873
/// checking.
874
///
875
/// This function is exposed so that other tools can use the same checking rules
876
/// as `arti-client`.  For more information, see
877
/// [`TorClientBuilder`](crate::TorClientBuilder).
878
#[deprecated(since = "0.5.0")]
879
#[allow(deprecated)]
880
pub fn fs_permissions_checks_disabled_via_env() -> bool {
881
    std::env::var_os(FS_PERMISSIONS_CHECKS_DISABLE_VAR).is_some()
882
}
883

            
884
#[cfg(test)]
885
mod test {
886
    // @@ begin test lint list maintained by maint/add_warning @@
887
    #![allow(clippy::bool_assert_comparison)]
888
    #![allow(clippy::clone_on_copy)]
889
    #![allow(clippy::dbg_macro)]
890
    #![allow(clippy::mixed_attributes_style)]
891
    #![allow(clippy::print_stderr)]
892
    #![allow(clippy::print_stdout)]
893
    #![allow(clippy::single_char_pattern)]
894
    #![allow(clippy::unwrap_used)]
895
    #![allow(clippy::unchecked_duration_subtraction)]
896
    #![allow(clippy::useless_vec)]
897
    #![allow(clippy::needless_pass_by_value)]
898
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
899
    use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
900

            
901
    use super::*;
902

            
903
    #[test]
904
    fn defaults() {
905
        let dflt = TorClientConfig::default();
906
        let b2 = TorClientConfigBuilder::default();
907
        let dflt2 = b2.build().unwrap();
908
        assert_eq!(&dflt, &dflt2);
909
    }
910

            
911
    #[test]
912
    fn builder() {
913
        let sec = std::time::Duration::from_secs(1);
914

            
915
        let mut authorities = dir::AuthorityContacts::builder();
916
        authorities.v3idents().push([22; 20].into());
917
        authorities.v3idents().push([44; 20].into());
918
        authorities.uploads().push(vec![
919
            SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80)),
920
            SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)),
921
        ]);
922

            
923
        let mut fallback = dir::FallbackDir::builder();
924
        fallback
925
            .rsa_identity([23; 20].into())
926
            .ed_identity([99; 32].into())
927
            .orports()
928
            .push("127.0.0.7:7".parse().unwrap());
929

            
930
        let mut bld = TorClientConfig::builder();
931
        *bld.tor_network().authorities() = authorities;
932
        bld.tor_network().set_fallback_caches(vec![fallback]);
933
        bld.storage()
934
            .cache_dir(CfgPath::new("/var/tmp/foo".to_owned()))
935
            .state_dir(CfgPath::new("/var/tmp/bar".to_owned()));
936
        bld.download_schedule().retry_certs().attempts(10);
937
        bld.download_schedule().retry_certs().initial_delay(sec);
938
        bld.download_schedule().retry_certs().parallelism(3);
939
        bld.download_schedule().retry_microdescs().attempts(30);
940
        bld.download_schedule()
941
            .retry_microdescs()
942
            .initial_delay(10 * sec);
943
        bld.download_schedule().retry_microdescs().parallelism(9);
944
        bld.override_net_params()
945
            .insert("wombats-per-quokka".to_owned(), 7);
946
        bld.path_rules()
947
            .ipv4_subnet_family_prefix(20)
948
            .ipv6_subnet_family_prefix(48);
949
        bld.circuit_timing()
950
            .max_dirtiness(90 * sec)
951
            .request_timeout(10 * sec)
952
            .request_max_retries(22)
953
            .request_loyalty(3600 * sec);
954
        bld.address_filter().allow_local_addrs(true);
955

            
956
        let val = bld.build().unwrap();
957

            
958
        assert_ne!(val, TorClientConfig::default());
959
    }
960

            
961
    #[test]
962
    fn bridges_supported() {
963
        /// checks that when s is processed as TOML for a client config,
964
        /// the resulting number of bridges is according to `exp`
965
        fn chk(exp: Result<usize, ()>, s: &str) {
966
            eprintln!("----------\n{s}\n----------\n");
967
            let got = (|| {
968
                let cfg: toml::Value = toml::from_str(s).unwrap();
969
                let cfg: TorClientConfigBuilder = cfg.try_into()?;
970
                let cfg = cfg.build()?;
971
                let n_bridges = cfg.bridges.bridges.len();
972
                Ok::<_, anyhow::Error>(n_bridges) // anyhow is just something we can use for ?
973
            })()
974
            .map_err(|_| ());
975
            assert_eq!(got, exp);
976
        }
977

            
978
        let chk_enabled_or_auto = |exp, bridges_toml| {
979
            for enabled in [r#""#, r#"enabled = true"#, r#"enabled = "auto""#] {
980
                chk(exp, &format!("[bridges]\n{}\n{}", enabled, bridges_toml));
981
            }
982
        };
983

            
984
        let ok_1_if = |b: bool| b.then_some(1).ok_or(());
985

            
986
        chk(
987
            Err(()),
988
            r#"
989
                [bridges]
990
                enabled = true
991
            "#,
992
        );
993

            
994
        chk_enabled_or_auto(
995
            ok_1_if(cfg!(feature = "bridge-client")),
996
            r#"
997
                bridges = ["192.0.2.83:80 $0bac39417268b96b9f514ef763fa6fba1a788956"]
998
            "#,
999
        );
        chk_enabled_or_auto(
            ok_1_if(cfg!(feature = "pt-client")),
            r#"
                bridges = ["obfs4 bridge.example.net:80 $0bac39417268b69b9f514e7f63fa6fba1a788958 ed25519:dGhpcyBpcyBbpmNyZWRpYmx5IHNpbGx5ISEhISEhISA iat-mode=1"]
                [[bridges.transports]]
                protocols = ["obfs4"]
                path = "obfs4proxy"
            "#,
        );
    }
    #[test]
    fn check_default() {
        // We don't want to second-guess the directories crate too much
        // here, so we'll just make sure it does _something_ plausible.
        let dflt = default_config_files().unwrap();
        assert!(dflt[0].as_path().unwrap().ends_with("arti.toml"));
        assert!(dflt[1].as_path().unwrap().ends_with("arti.d"));
        assert_eq!(dflt.len(), 2);
    }
    #[test]
    #[cfg(feature = "pt-client")]
    fn check_bridge_pt() {
        let from_toml = |s: &str| -> TorClientConfigBuilder {
            let cfg: toml::Value = toml::from_str(dbg!(s)).unwrap();
            let cfg: TorClientConfigBuilder = cfg.try_into().unwrap();
            cfg
        };
        let chk = |cfg: &TorClientConfigBuilder, expected: Result<(), &str>| match (
            cfg.build(),
            expected,
        ) {
            (Ok(_), Ok(())) => {}
            (Err(e), Err(ex)) => {
                if !e.to_string().contains(ex) {
                    panic!("\"{e}\" did not contain {ex}");
                }
            }
            (Ok(_), Err(ex)) => {
                panic!("Expected {ex} but cfg succeeded");
            }
            (Err(e), Ok(())) => {
                panic!("Expected success but got error {e}")
            }
        };
        let test_cases = [
            ("# No bridges", Ok(())),
            (
                r#"
                    # No bridges but we still enabled bridges
                    [bridges]
                    enabled = true
                    bridges = []
                "#,
                Err("bridges.enabled=true, but no bridges defined"),
            ),
            (
                r#"
                    # One non-PT bridge
                    [bridges]
                    enabled = true
                    bridges = [
                        "192.0.2.83:80 $0bac39417268b96b9f514ef763fa6fba1a788956",
                    ]
                "#,
                Ok(()),
            ),
            (
                r#"
                    # One obfs4 bridge
                    [bridges]
                    enabled = true
                    bridges = [
                        "obfs4 bridge.example.net:80 $0bac39417268b69b9f514e7f63fa6fba1a788958 ed25519:dGhpcyBpcyBbpmNyZWRpYmx5IHNpbGx5ISEhISEhISA iat-mode=1",
                    ]
                    [[bridges.transports]]
                    protocols = ["obfs4"]
                    path = "obfs4proxy"
                "#,
                Ok(()),
            ),
            (
                r#"
                    # One obfs4 bridge with unmanaged transport.
                    [bridges]
                    enabled = true
                    bridges = [
                        "obfs4 bridge.example.net:80 $0bac39417268b69b9f514e7f63fa6fba1a788958 ed25519:dGhpcyBpcyBbpmNyZWRpYmx5IHNpbGx5ISEhISEhISA iat-mode=1",
                    ]
                    [[bridges.transports]]
                    protocols = ["obfs4"]
                    proxy_addr = "127.0.0.1:31337"
                "#,
                Ok(()),
            ),
            (
                r#"
                    # Transport is both managed and unmanaged.
                    [[bridges.transports]]
                    protocols = ["obfs4"]
                    path = "obfsproxy"
                    proxy_addr = "127.0.0.1:9999"
                "#,
                Err("Cannot provide both path and proxy_addr"),
            ),
            (
                r#"
                    # One obfs4 bridge and non-PT bridge
                    [bridges]
                    enabled = false
                    bridges = [
                        "192.0.2.83:80 $0bac39417268b96b9f514ef763fa6fba1a788956",
                        "obfs4 bridge.example.net:80 $0bac39417268b69b9f514e7f63fa6fba1a788958 ed25519:dGhpcyBpcyBbpmNyZWRpYmx5IHNpbGx5ISEhISEhISA iat-mode=1",
                    ]
                    [[bridges.transports]]
                    protocols = ["obfs4"]
                    path = "obfs4proxy"
                "#,
                Ok(()),
            ),
            (
                r#"
                    # One obfs4 and non-PT bridge with no transport
                    [bridges]
                    enabled = true
                    bridges = [
                        "192.0.2.83:80 $0bac39417268b96b9f514ef763fa6fba1a788956",
                        "obfs4 bridge.example.net:80 $0bac39417268b69b9f514e7f63fa6fba1a788958 ed25519:dGhpcyBpcyBbpmNyZWRpYmx5IHNpbGx5ISEhISEhISA iat-mode=1",
                    ]
                "#,
                Ok(()),
            ),
            (
                r#"
                    # One obfs4 bridge with no transport
                    [bridges]
                    enabled = true
                    bridges = [
                        "obfs4 bridge.example.net:80 $0bac39417268b69b9f514e7f63fa6fba1a788958 ed25519:dGhpcyBpcyBbpmNyZWRpYmx5IHNpbGx5ISEhISEhISA iat-mode=1",
                    ]
                "#,
                Err("all bridges unusable due to lack of corresponding pluggable transport"),
            ),
            (
                r#"
                    # One obfs4 bridge with no transport but bridges are disabled
                    [bridges]
                    enabled = false
                    bridges = [
                        "obfs4 bridge.example.net:80 $0bac39417268b69b9f514e7f63fa6fba1a788958 ed25519:dGhpcyBpcyBbpmNyZWRpYmx5IHNpbGx5ISEhISEhISA iat-mode=1",
                    ]
                "#,
                Ok(()),
            ),
            (
                r#"
                        # One non-PT bridge with a redundant transports section
                        [bridges]
                        enabled = false
                        bridges = [
                            "192.0.2.83:80 $0bac39417268b96b9f514ef763fa6fba1a788956",
                        ]
                        [[bridges.transports]]
                        protocols = ["obfs4"]
                        path = "obfs4proxy"
                "#,
                Ok(()),
            ),
        ];
        for (test_case, expected) in test_cases.iter() {
            chk(&from_toml(test_case), *expected);
        }
    }
}