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_deftly::Deftly;
7
use derive_more::AsRef;
8
use fs_mistrust::{Mistrust, MistrustBuilder};
9
use std::collections::HashMap;
10
use std::path::Path;
11
use std::path::PathBuf;
12
use std::result::Result as StdResult;
13
use std::time::Duration;
14

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

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

            
30
#[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
#[cfg(not(all(
73
    feature = "vanguards",
74
    any(feature = "onion-service-client", feature = "onion-service-service"),
75
)))]
76
use {
77
    std::sync::LazyLock,
78
    tor_config::ExplicitOrAuto,
79
    tor_guardmgr::{VanguardConfig, VanguardConfigBuilder, VanguardMode},
80
};
81

            
82
/// A [`VanguardConfig`] which is disabled.
83
// It would be nice if the builder were const, but this is the best we can do.
84
// Boxed so that this is guaranteed to use very little space if it's unused.
85
#[cfg(not(all(
86
    feature = "vanguards",
87
    any(feature = "onion-service-client", feature = "onion-service-service"),
88
)))]
89
static DISABLED_VANGUARDS: LazyLock<Box<VanguardConfig>> = LazyLock::new(|| {
90
    Box::new(
91
        VanguardConfigBuilder::default()
92
            .mode(ExplicitOrAuto::Explicit(VanguardMode::Disabled))
93
            .build()
94
            .expect("Could not build a disabled `VanguardConfig`"),
95
    )
96
});
97

            
98
/// Configuration for client behavior relating to addresses.
99
///
100
/// This type is immutable once constructed. To create an object of this type,
101
/// use [`ClientAddrConfigBuilder`].
102
///
103
/// You can replace this configuration on a running Arti client.  Doing so will
104
/// affect new streams and requests, but will have no effect on existing streams
105
/// and requests.
106
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
107
#[derive_deftly(TorConfig)]
108
pub struct ClientAddrConfig {
109
    /// Should we allow attempts to make Tor connections to local addresses?
110
    ///
111
    /// This option is off by default, since (by default) Tor exits will
112
    /// always reject connections to such addresses.
113
    #[deftly(tor_config(default))]
114
    pub(crate) allow_local_addrs: bool,
115

            
116
    /// Should we allow attempts to connect to hidden services (`.onion` services)?
117
    ///
118
    /// This option is on by default.
119
    //
120
    // NOTE: This could use tor_config(cfg) instead, but that would change the API.
121
    #[cfg(feature = "onion-service-client")]
122
    #[deftly(tor_config(default = "true"))]
123
    pub(crate) allow_onion_addrs: bool,
124
}
125

            
126
/// Configuration for client behavior relating to stream connection timeouts
127
///
128
/// This type is immutable once constructed. To create an object of this type,
129
/// use [`StreamTimeoutConfigBuilder`].
130
///
131
/// You can replace this configuration on a running Arti client.  Doing so will
132
/// affect new streams and requests, but will have no effect on existing streams
133
/// and requests—even those that are currently waiting.
134
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
135
#[derive_deftly(TorConfig)]
136
#[non_exhaustive]
137
pub struct StreamTimeoutConfig {
138
    /// How long should we wait before timing out a stream when connecting
139
    /// to a host?
140
    #[deftly(tor_config(default = "default_connect_timeout()"))]
141
    pub(crate) connect_timeout: Duration,
142

            
143
    /// How long should we wait before timing out when resolving a DNS record?
144
    #[deftly(tor_config(default = "default_dns_resolve_timeout()"))]
145
    pub(crate) resolve_timeout: Duration,
146

            
147
    /// How long should we wait before timing out when resolving a DNS
148
    /// PTR record?
149
    #[deftly(tor_config(default = "default_dns_resolve_ptr_timeout()"))]
150
    pub(crate) resolve_ptr_timeout: Duration,
151
}
152

            
153
/// Return the default stream timeout
154
4046
fn default_connect_timeout() -> Duration {
155
4046
    Duration::new(10, 0)
156
4046
}
157

            
158
/// Return the default resolve timeout
159
4046
fn default_dns_resolve_timeout() -> Duration {
160
4046
    Duration::new(10, 0)
161
4046
}
162

            
163
/// Return the default PTR resolve timeout
164
4046
fn default_dns_resolve_ptr_timeout() -> Duration {
165
4046
    Duration::new(10, 0)
166
4046
}
167

            
168
/// Configuration for overriding the status of our software.
169
///
170
/// # Issues
171
///
172
/// We only check these configuration values when we receive a new consensus,
173
/// or when we're starting up.  Therefore, if you change these values,
174
/// they won't have any effect until the next consensus is received.
175
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
176
#[derive_deftly(TorConfig)]
177
pub struct SoftwareStatusOverrideConfig {
178
    /// A list of protocols to pretend that we have,
179
    /// when checking whether our software is obsolete.
180
    //
181
    // We make this type a String in the builder, to avoid exposing Protocols in our API.
182
    //
183
    // NOTE: Emulating the old behavior for this was pretty tricky, but we are slated to
184
    // (possibly) deprecate this option entirely.
185
    #[deftly(tor_config(
186
        no_magic,
187
        field(ty = "String"),
188
        setter(skip),
189
        try_build = "Self::parse_protos",
190
        extend_with = "extend_with_replace"
191
    ))]
192
    pub(crate) ignore_missing_required_protocols: tor_protover::Protocols,
193
}
194

            
195
impl SoftwareStatusOverrideConfigBuilder {
196
    /// Helper: Parse the ignore_missing_required_protocols field.
197
4114
    fn parse_protos(&self) -> Result<tor_protover::Protocols, ConfigBuildError> {
198
        use std::str::FromStr as _;
199

            
200
4114
        tor_protover::Protocols::from_str(&self.ignore_missing_required_protocols).map_err(|e| {
201
            ConfigBuildError::Invalid {
202
                field: "ignore_missing_required_protocols".to_string(),
203
                problem: e.to_string(),
204
            }
205
        })
206
4114
    }
207

            
208
    /// Set a list of protocols that we pretend that we have
209
    /// when checking whether our software is obsolete.
210
    pub fn ignore_missing_required_protocols(&mut self, s: impl AsRef<str>) -> &mut Self {
211
        self.ignore_missing_required_protocols = s.as_ref().to_string();
212
        self
213
    }
214
}
215

            
216
/// Configuration for where information should be stored on disk.
217
///
218
/// By default, cache information will be stored in `${ARTI_CACHE}`, and
219
/// persistent state will be stored in `${ARTI_LOCAL_DATA}`.  That means that
220
/// _all_ programs using these defaults will share their cache and state data.
221
/// If that isn't what you want,  you'll need to override these directories.
222
///
223
/// On unix, the default directories will typically expand to `~/.cache/arti`
224
/// and `~/.local/share/arti/` respectively, depending on the user's
225
/// environment. Other platforms will also use suitable defaults. For more
226
/// information, see the documentation for [`CfgPath`].
227
///
228
/// This section is for read/write storage.
229
///
230
/// You cannot change this section on a running Arti client.
231
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
232
#[derive_deftly(TorConfig)]
233
pub struct StorageConfig {
234
    /// Location on disk for cached information.
235
    ///
236
    /// This follows the rules for `/var/cache`: "sufficiently old" filesystem objects
237
    /// in it may be deleted outside of the control of Arti,
238
    /// and Arti will continue to function properly.
239
    /// It is also fine to delete the directory as a whole, while Arti is not running.
240
    //
241
    // Usage note, for implementations of Arti components:
242
    //
243
    // When files in this directory are to be used by a component, the cache_dir
244
    // value should be passed through to the component as-is, and the component is
245
    // then responsible for constructing an appropriate sub-path (for example,
246
    // tor-dirmgr receives cache_dir, and appends components such as "dir_blobs".
247
    //
248
    // (This consistency rule is not current always followed by every component.)
249
    #[deftly(tor_config(default = "default_cache_dir()", setter(into)))]
250
    cache_dir: CfgPath,
251

            
252
    /// Location on disk for less-sensitive persistent state information.
253
    // Usage note: see the note for `cache_dir`, above.
254
    #[deftly(tor_config(default = "default_state_dir()", setter(into)))]
255
    state_dir: CfgPath,
256

            
257
    /// Location on disk for the Arti keystore.
258
    //
259
    // NOTE: This could use tor_config(cfg) instead, but that would change the API.
260
    #[cfg(feature = "keymgr")]
261
    #[deftly(tor_config(sub_builder))]
262
    keystore: ArtiKeystoreConfig,
263

            
264
    /// Configuration about which permissions we want to enforce on our files.
265
    #[deftly(tor_config(
266
        sub_builder(build_fn = "build_for_arti"),
267
        extend_with = "extend_with_replace"
268
    ))]
269
    permissions: Mistrust,
270
}
271

            
272
/// Return the default cache directory.
273
3870
fn default_cache_dir() -> CfgPath {
274
3870
    CfgPath::new("${ARTI_CACHE}".to_owned())
275
3870
}
276

            
277
/// Return the default state directory.
278
2136
fn default_state_dir() -> CfgPath {
279
2136
    CfgPath::new("${ARTI_LOCAL_DATA}".to_owned())
280
2136
}
281

            
282
/// Macro to avoid repeating code for `expand_*_dir` functions on StorageConfig
283
// TODO: generate the expand_*_dir functions using d-a instead
284
macro_rules! expand_dir {
285
    ($self:ident, $dirname:ident, $dircfg:ident) => {
286
        $self
287
            .$dirname
288
            .path($dircfg)
289
            .map_err(|e| ConfigBuildError::Invalid {
290
                field: stringify!($dirname).to_owned(),
291
                problem: e.to_string(),
292
            })
293
    };
294
}
295

            
296
impl StorageConfig {
297
    /// Try to expand `state_dir` to be a path buffer.
298
2208
    pub(crate) fn expand_state_dir(
299
2208
        &self,
300
2208
        path_resolver: &CfgPathResolver,
301
2208
    ) -> Result<PathBuf, ConfigBuildError> {
302
2208
        expand_dir!(self, state_dir, path_resolver)
303
2208
    }
304
    /// Try to expand `cache_dir` to be a path buffer.
305
190
    pub(crate) fn expand_cache_dir(
306
190
        &self,
307
190
        path_resolver: &CfgPathResolver,
308
190
    ) -> Result<PathBuf, ConfigBuildError> {
309
190
        expand_dir!(self, cache_dir, path_resolver)
310
190
    }
311
    /// Return the keystore config
312
    #[allow(clippy::unnecessary_wraps)]
313
5578
    pub(crate) fn keystore(&self) -> ArtiKeystoreConfig {
314
        cfg_if::cfg_if! {
315
            if #[cfg(feature="keymgr")] {
316
5578
                self.keystore.clone()
317
            } else {
318
                Default::default()
319
            }
320
        }
321
5578
    }
322
    /// Return the FS permissions to use for state and cache directories.
323
5862
    pub(crate) fn permissions(&self) -> &Mistrust {
324
5862
        &self.permissions
325
5862
    }
326
}
327

            
328
/// Configuration for anti-censorship features: bridges and pluggable transports.
329
///
330
/// A "bridge" is a relay that is not listed in the regular Tor network directory;
331
/// clients use them to reach the network when a censor is blocking their
332
/// connection to all the regular Tor relays.
333
///
334
/// A "pluggable transport" is a tool that transforms and conceals a user's connection
335
/// to a bridge; clients use them to reach the network when a censor is blocking
336
/// all traffic that "looks like Tor".
337
///
338
/// A [`BridgesConfig`] configuration has the following pieces:
339
///    * A [`BridgeList`] of [`BridgeConfig`]s, which describes one or more bridges.
340
///    * An `enabled` boolean to say whether or not to use the listed bridges.
341
///    * A list of [`pt::TransportConfig`]s.
342
///
343
/// # Example
344
///
345
/// Here's an example of building a bridge configuration, and using it in a
346
/// TorClientConfig.
347
///
348
/// The bridges here are fictitious; you'll need to use real bridges
349
/// if you want a working configuration.
350
///
351
/// ```
352
/// ##[cfg(feature = "pt-client")]
353
/// # fn demo() -> anyhow::Result<()> {
354
/// use arti_client::config::{TorClientConfig, BridgeConfigBuilder, CfgPath};
355
/// // Requires that the pt-client feature is enabled.
356
/// use arti_client::config::pt::TransportConfigBuilder;
357
///
358
/// let mut builder = TorClientConfig::builder();
359
///
360
/// // Add a single bridge to the list of bridges, from a bridge line.
361
/// // This bridge line is made up for demonstration, and won't work.
362
/// const BRIDGE1_LINE : &str = "Bridge obfs4 192.0.2.55:38114 316E643333645F6D79216558614D3931657A5F5F cert=YXJlIGZyZXF1ZW50bHkgZnVsbCBvZiBsaXR0bGUgbWVzc2FnZXMgeW91IGNhbiBmaW5kLg iat-mode=0";
363
/// let bridge_1: BridgeConfigBuilder = BRIDGE1_LINE.parse()?;
364
/// // This is where we pass `BRIDGE1_LINE` into the BridgeConfigBuilder.
365
/// builder.bridges().bridges().push(bridge_1);
366
///
367
/// // Add a second bridge, built by hand.  This way is harder.
368
/// // This bridge is made up for demonstration, and won't work.
369
/// let mut bridge2_builder = BridgeConfigBuilder::default();
370
/// bridge2_builder
371
///     .transport("obfs4")
372
///     .push_setting("iat-mode", "1")
373
///     .push_setting(
374
///         "cert",
375
///         "YnV0IHNvbWV0aW1lcyB0aGV5IGFyZSByYW5kb20u8x9aQG/0cIIcx0ItBcTqiSXotQne+Q"
376
///     );
377
/// bridge2_builder.set_addrs(vec!["198.51.100.25:443".parse()?]);
378
/// bridge2_builder.set_ids(vec!["7DD62766BF2052432051D7B7E08A22F7E34A4543".parse()?]);
379
/// // Now insert the second bridge into our config builder.
380
/// builder.bridges().bridges().push(bridge2_builder);
381
///
382
/// // Now configure an obfs4 transport. (Requires the "pt-client" feature)
383
/// let mut transport = TransportConfigBuilder::default();
384
/// transport
385
///     .protocols(vec!["obfs4".parse()?])
386
///     // Specify either the name or the absolute path of pluggable transport client binary, this
387
///     // may differ from system to system.
388
///     .path(CfgPath::new("/usr/bin/obfs4proxy".into()))
389
///     .run_on_startup(true);
390
/// builder.bridges().transports().push(transport);
391
///
392
/// let config = builder.build()?;
393
/// // Now you can pass `config` to TorClient::create!
394
/// # Ok(())}
395
/// ```
396
/// You can also find an example based on snowflake in arti-client example folder.
397
//
398
// We leave this as an empty struct even when bridge support is disabled,
399
// as otherwise the default config file would generate an unknown section warning.
400
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
401
#[derive_deftly(TorConfig)]
402
#[deftly(tor_config(pre_build = "validate_bridges_config", attr = "non_exhaustive"))]
403
#[non_exhaustive]
404
pub struct BridgesConfig {
405
    /// Should we use configured bridges?
406
    ///
407
    /// The default (`Auto`) is to use bridges if they are configured.
408
    /// `false` means to not use even configured bridges.
409
    /// `true` means to insist on the use of bridges;
410
    /// if none are configured, that's then an error.
411
    #[deftly(tor_config(default))]
412
    pub(crate) enabled: BoolOrAuto,
413

            
414
    /// Configured list of bridges (possibly via pluggable transports)
415
    //
416
    // NOTE: This isn't using the automatic list_builder code, because it doesn't yet
417
    // support MultilineListBuilder.
418
    #[deftly(tor_config(no_magic, sub_builder, setter(skip)))]
419
    bridges: BridgeList,
420

            
421
    /// Configured list of pluggable transports.
422
    #[cfg(feature = "pt-client")] // NOTE: Could use tor_config(cfg)
423
    #[deftly(tor_config(
424
        list(element(build), listtype = "TransportConfigList"),
425
        default = "vec![]"
426
    ))]
427
    pub(crate) transports: Vec<pt::TransportConfig>,
428
}
429

            
430
#[cfg(feature = "pt-client")]
431
/// Determine if we need any pluggable transports.
432
///
433
/// If we do and their transports don't exist, we have a problem
434
124
fn validate_pt_config(bridges: &BridgesConfigBuilder) -> Result<(), ConfigBuildError> {
435
    use std::collections::HashSet;
436
    use std::str::FromStr;
437

            
438
    // These are all the protocols that the user has defined
439
124
    let mut protocols_defined: HashSet<PtTransportName> = HashSet::new();
440
124
    if let Some(transportlist) = bridges.opt_transports() {
441
10
        for protocols in transportlist.iter() {
442
10
            for protocol in protocols.get_protocols() {
443
10
                protocols_defined.insert(protocol.clone());
444
10
            }
445
        }
446
114
    }
447

            
448
    // Iterate over all the transports that bridges are going to use
449
    // If any one is valid, we validate the entire config
450
124
    for maybe_protocol in bridges
451
124
        .bridges
452
124
        .bridges
453
124
        .as_deref()
454
124
        .unwrap_or_default()
455
124
        .iter()
456
    {
457
124
        match maybe_protocol.get_transport() {
458
124
            Some(raw_protocol) => {
459
                // We convert the raw protocol string representation
460
                // into a more proper one using PtTransportName
461
124
                let protocol = TransportId::from_str(raw_protocol)
462
                    // If id can't be parsed, simply skip it here.
463
                    // The rest of the config validation/processing will generate an error for it.
464
124
                    .unwrap_or_default()
465
124
                    .into_pluggable();
466
                // The None case represents when we aren't using a PT at all
467
124
                match protocol {
468
12
                    Some(protocol_required) => {
469
12
                        if protocols_defined.contains(&protocol_required) {
470
10
                            return Ok(());
471
2
                        }
472
                    }
473
112
                    None => return Ok(()),
474
                }
475
            }
476
            None => {
477
                return Ok(());
478
            }
479
        }
480
    }
481

            
482
2
    Err(ConfigBuildError::Inconsistent {
483
2
        fields: ["bridges.bridges", "bridges.transports"].map(Into::into).into_iter().collect(),
484
2
        problem: "Bridges configured, but all bridges unusable due to lack of corresponding pluggable transport in `[bridges.transports]`".into(),
485
2
    })
486
124
}
487

            
488
/// Check that the bridge configuration is right
489
#[allow(clippy::unnecessary_wraps)]
490
4156
fn validate_bridges_config(bridges: &BridgesConfigBuilder) -> Result<(), ConfigBuildError> {
491
4156
    let _ = bridges; // suppresses unused variable for just that argument
492

            
493
    use BoolOrAuto as BoA;
494

            
495
    // Ideally we would run this post-build, rather than pre-build;
496
    // doing it here means we have to recapitulate the defaulting.
497
    // Happily the defaulting is obvious, cheap, and not going to change.
498
    //
499
    // Alternatively we could have derive_builder provide `build_unvalidated`,
500
    // but that involves re-setting the build fn name for every field.
501
4156
    match (
502
4156
        bridges.enabled.unwrap_or_default(),
503
4156
        bridges.bridges.bridges.as_deref().unwrap_or_default(),
504
4156
    ) {
505
4152
        (BoA::Auto, _) | (BoA::Explicit(false), _) | (BoA::Explicit(true), [_, ..]) => {}
506
4
        (BoA::Explicit(true), []) => {
507
4
            return Err(ConfigBuildError::Inconsistent {
508
4
                fields: ["enabled", "bridges"].map(Into::into).into_iter().collect(),
509
4
                problem: "bridges.enabled=true, but no bridges defined".into(),
510
4
            });
511
        }
512
    }
513
    #[cfg(feature = "pt-client")]
514
    {
515
4152
        if bridges_enabled(
516
4152
            bridges.enabled.unwrap_or_default(),
517
4152
            bridges.bridges.bridges.as_deref().unwrap_or_default(),
518
        ) {
519
124
            validate_pt_config(bridges)?;
520
4028
        }
521
    }
522

            
523
4150
    Ok(())
524
4156
}
525

            
526
/// Generic logic to check if bridges should be used or not
527
4338
fn bridges_enabled(enabled: BoolOrAuto, bridges: &[impl Sized]) -> bool {
528
    #[cfg(feature = "bridge-client")]
529
    {
530
4338
        enabled.as_bool().unwrap_or(!bridges.is_empty())
531
    }
532

            
533
    #[cfg(not(feature = "bridge-client"))]
534
    {
535
        let _ = (enabled, bridges);
536
        false
537
    }
538
4338
}
539

            
540
impl BridgesConfig {
541
    /// Should the bridges be used?
542
186
    fn bridges_enabled(&self) -> bool {
543
186
        bridges_enabled(self.enabled, &self.bridges)
544
186
    }
545
}
546

            
547
/// List of configured bridges, as found in the built configuration
548
//
549
// This type alias arranges that we can put `BridgeList` in `BridgesConfig`
550
// and have derive_builder put a `BridgeListBuilder` in `BridgesConfigBuilder`.
551
pub type BridgeList = Vec<BridgeConfig>;
552

            
553
define_list_builder_helper! {
554
    struct BridgeListBuilder {
555
        bridges: [BridgeConfigBuilder],
556
    }
557
    built: BridgeList = bridges;
558
    default = vec![];
559
    #[serde(try_from="MultilineListBuilder<BridgeConfigBuilder>")]
560
    #[serde(into="MultilineListBuilder<BridgeConfigBuilder>")]
561
}
562

            
563
convert_helper_via_multi_line_list_builder! {
564
    struct BridgeListBuilder {
565
        bridges: [BridgeConfigBuilder],
566
    }
567
}
568

            
569
#[cfg(feature = "bridge-client")]
570
define_list_builder_accessors! {
571
    struct BridgesConfigBuilder {
572
        pub bridges: [BridgeConfigBuilder],
573
    }
574
}
575

            
576
/// A configuration used to bootstrap a [`TorClient`](crate::TorClient).
577
///
578
/// In order to connect to the Tor network, Arti needs to know a few
579
/// well-known directory caches on the network, and the public keys of the
580
/// network's directory authorities.  It also needs a place on disk to
581
/// store persistent state and cached directory information. (See [`StorageConfig`]
582
/// for default directories.)
583
///
584
/// Most users will create a TorClientConfig by running
585
/// [`TorClientConfig::default`].
586
///
587
/// If you need to override the locations where Arti stores its
588
/// information, you can make a TorClientConfig with
589
/// [`TorClientConfigBuilder::from_directories`].
590
///
591
/// Finally, you can get fine-grained control over the members of a
592
/// TorClientConfig using [`TorClientConfigBuilder`].
593
#[derive(Clone, Deftly, Debug, AsRef, educe::Educe)]
594
#[educe(PartialEq, Eq)]
595
#[derive_deftly(TorConfig)]
596
#[non_exhaustive]
597
pub struct TorClientConfig {
598
    /// Information about the Tor network we want to connect to.
599
    #[deftly(tor_config(sub_builder))]
600
    tor_network: dir::NetworkConfig,
601

            
602
    /// Directories for storing information on disk
603
    #[deftly(tor_config(sub_builder))]
604
    pub(crate) storage: StorageConfig,
605

            
606
    /// Information about when and how often to download directory information
607
    #[deftly(tor_config(sub_builder))]
608
    download_schedule: dir::DownloadScheduleConfig,
609

            
610
    /// Information about how premature or expired our directories are allowed
611
    /// to be.
612
    ///
613
    /// These options help us tolerate clock skew, and help survive the case
614
    /// where the directory authorities are unable to reach consensus for a
615
    /// while.
616
    #[deftly(tor_config(sub_builder))]
617
    directory_tolerance: dir::DirTolerance,
618

            
619
    /// Facility to override network parameters from the values set in the
620
    /// consensus.
621
    #[deftly(tor_config(
622
        setter(skip), // See note on accessor. This isn't the best way to do this.
623
        field(ty = "HashMap<String, i32>"),
624
        build = "|this: &Self| default_extend(this.override_net_params.clone())",
625
        extend_with = "extend_with_replace"
626
    ))]
627
    pub(crate) override_net_params: tor_netdoc::doc::netstatus::NetParams<i32>,
628

            
629
    /// Information about bridges, pluggable transports, and so on
630
    #[deftly(tor_config(sub_builder))]
631
    pub(crate) bridges: BridgesConfig,
632

            
633
    /// Information about how to build paths through the network.
634
    #[deftly(tor_config(sub_builder))]
635
    pub(crate) channel: ChannelConfig,
636

            
637
    /// Configuration for system resources used by Arti
638
    ///
639
    /// Note that there are other settings in this section,
640
    /// in `arti::cfg::SystemConfig` -
641
    /// these two structs overlay here.
642
    #[deftly(tor_config(sub_builder))]
643
    pub(crate) system: SystemConfig,
644

            
645
    /// Information about how to build paths through the network.
646
    #[as_ref]
647
    #[deftly(tor_config(sub_builder))]
648
    path_rules: circ::PathConfig,
649

            
650
    /// Information about preemptive circuits.
651
    #[as_ref]
652
    #[deftly(tor_config(sub_builder))]
653
    preemptive_circuits: circ::PreemptiveCircuitConfig,
654

            
655
    /// Information about how to retry and expire circuits and request for circuits.
656
    #[as_ref]
657
    #[deftly(tor_config(sub_builder))]
658
    circuit_timing: circ::CircuitTiming,
659

            
660
    /// Rules about which addresses the client is willing to connect to.
661
    #[deftly(tor_config(sub_builder))]
662
    pub(crate) address_filter: ClientAddrConfig,
663

            
664
    /// Information about timing out client requests.
665
    #[deftly(tor_config(sub_builder))]
666
    pub(crate) stream_timeouts: StreamTimeoutConfig,
667

            
668
    /// Information about vanguards.
669
    // NOTE: Don't use `#[as_ref]` below, since we provide our own AsRef impl to handle when
670
    // vanguards are disabled.
671
    #[deftly(tor_config(sub_builder))]
672
    pub(crate) vanguards: vanguards::VanguardConfig,
673

            
674
    /// Support for running with known-obsolete versions.
675
    #[deftly(tor_config(sub_builder))]
676
    pub(crate) use_obsolete_software: SoftwareStatusOverrideConfig,
677

            
678
    /// Resolves paths in this configuration.
679
    ///
680
    /// This is not [reconfigurable](crate::TorClient::reconfigure).
681
    // We don't accept this from the builder/serde, and don't inspect it when comparing configs.
682
    // This should be considered as ancillary data rather than a configuration option.
683
    // TorClientConfig maybe isn't the best place for this, but this is where it needs to go to not
684
    // require public API changes.
685
    #[as_ref]
686
    #[deftly(tor_config(skip, build = "|_| tor_config_path::arti_client_base_resolver()"))]
687
    #[educe(PartialEq(ignore), Eq(ignore))]
688
    pub(crate) path_resolver: CfgPathResolver,
689
}
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
4118
fn default_extend<T: Default + Extend<X>, X>(to_add: impl IntoIterator<Item = X>) -> T {
697
4118
    let mut collection = T::default();
698
4118
    collection.extend(to_add);
699
4118
    collection
700
4118
}
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, Deftly, Eq, PartialEq)]
709
#[derive_deftly(TorConfig)]
710
#[non_exhaustive]
711
pub struct SystemConfig {
712
    /// Memory limits (approximate)
713
    #[deftly(tor_config(sub_builder))]
714
    pub(crate) memory: tor_memquota::Config,
715
}
716

            
717
impl AsRef<tor_guardmgr::VanguardConfig> for TorClientConfig {
718
186
    fn as_ref(&self) -> &tor_guardmgr::VanguardConfig {
719
        cfg_if::cfg_if! {
720
            if #[cfg(all(
721
                feature = "vanguards",
722
                any(feature = "onion-service-client", feature = "onion-service-service"),
723
            ))]
724
            {
725
186
                &self.vanguards
726
            } else {
727
                &DISABLED_VANGUARDS
728
            }
729
        }
730
186
    }
731
}
732

            
733
impl tor_circmgr::CircMgrConfig for TorClientConfig {}
734

            
735
#[cfg(feature = "onion-service-client")]
736
impl tor_hsclient::HsClientConnectorConfig for TorClientConfig {}
737

            
738
#[cfg(any(feature = "onion-service-client", feature = "onion-service-service"))]
739
impl tor_circmgr::hspool::HsCircPoolConfig for TorClientConfig {
740
    #[cfg(all(
741
        feature = "vanguards",
742
        any(feature = "onion-service-client", feature = "onion-service-service")
743
    ))]
744
    fn vanguard_config(&self) -> &tor_guardmgr::VanguardConfig {
745
        &self.vanguards
746
    }
747
}
748

            
749
impl AsRef<tor_dircommon::fallback::FallbackList> for TorClientConfig {
750
186
    fn as_ref(&self) -> &tor_dircommon::fallback::FallbackList {
751
186
        self.tor_network.fallback_caches()
752
186
    }
753
}
754
impl AsRef<[BridgeConfig]> for TorClientConfig {
755
    fn as_ref(&self) -> &[BridgeConfig] {
756
        #[cfg(feature = "bridge-client")]
757
        {
758
            &self.bridges.bridges
759
        }
760

            
761
        #[cfg(not(feature = "bridge-client"))]
762
        {
763
            &[]
764
        }
765
    }
766
}
767
impl AsRef<BridgesConfig> for TorClientConfig {
768
34
    fn as_ref(&self) -> &BridgesConfig {
769
34
        &self.bridges
770
34
    }
771
}
772
impl tor_guardmgr::GuardMgrConfig for TorClientConfig {
773
186
    fn bridges_enabled(&self) -> bool {
774
186
        self.bridges.bridges_enabled()
775
186
    }
776
}
777

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

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

            
812
    /// Return the keystore config
813
340
    pub fn keystore(&self) -> ArtiKeystoreConfig {
814
340
        self.storage.keystore()
815
340
    }
816

            
817
    /// Get the state directory and its corresponding
818
    /// [`Mistrust`] configuration.
819
2200
    pub(crate) fn state_dir(&self) -> StdResult<(PathBuf, &fs_mistrust::Mistrust), ErrorDetail> {
820
2200
        let state_dir = self
821
2200
            .storage
822
2200
            .expand_state_dir(&self.path_resolver)
823
2200
            .map_err(ErrorDetail::Configuration)?;
824
2200
        let mistrust = self.storage.permissions();
825

            
826
2200
        Ok((state_dir, mistrust))
827
2200
    }
828

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

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

            
851
22
        builder
852
22
            .storage()
853
22
            .cache_dir(CfgPath::new_literal(cache_dir.as_ref()))
854
22
            .state_dir(CfgPath::new_literal(state_dir.as_ref()));
855

            
856
22
        builder
857
22
    }
858

            
859
    /// Return a mutable reference to a HashMap of `override_net_params`
860
    ///
861
    /// These parameters, if set, replace those that arrive in the network consensus document.
862
    //
863
    // NOTE: This is necessary for now because sub_builder isn't compatible with build().
864
36
    pub fn override_net_params(&mut self) -> &mut HashMap<String, i32> {
865
36
        &mut self.override_net_params
866
36
    }
867
}
868

            
869
/// Return the filenames for the default user configuration files
870
2008
pub fn default_config_files() -> Result<Vec<ConfigurationSource>, CfgPathError> {
871
    // the base path resolver includes the 'ARTI_CONFIG' variable
872
2008
    let path_resolver = tor_config_path::arti_client_base_resolver();
873

            
874
2008
    ["${ARTI_CONFIG}/arti.toml", "${ARTI_CONFIG}/arti.d/"]
875
2008
        .into_iter()
876
4076
        .map(|f| {
877
4016
            let path = CfgPath::new(f.into()).path(&path_resolver)?;
878
4016
            Ok(ConfigurationSource::from_path(path))
879
4016
        })
880
2008
        .collect()
881
2008
}
882

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

            
887
/// Return true if the environment has been set up to disable FS permissions
888
/// checking.
889
///
890
/// This function is exposed so that other tools can use the same checking rules
891
/// as `arti-client`.  For more information, see
892
/// [`TorClientBuilder`](crate::TorClientBuilder).
893
#[deprecated(since = "0.5.0")]
894
#[allow(deprecated)]
895
pub fn fs_permissions_checks_disabled_via_env() -> bool {
896
    std::env::var_os(FS_PERMISSIONS_CHECKS_DISABLE_VAR).is_some()
897
}
898

            
899
#[cfg(test)]
900
mod test {
901
    // @@ begin test lint list maintained by maint/add_warning @@
902
    #![allow(clippy::bool_assert_comparison)]
903
    #![allow(clippy::clone_on_copy)]
904
    #![allow(clippy::dbg_macro)]
905
    #![allow(clippy::mixed_attributes_style)]
906
    #![allow(clippy::print_stderr)]
907
    #![allow(clippy::print_stdout)]
908
    #![allow(clippy::single_char_pattern)]
909
    #![allow(clippy::unwrap_used)]
910
    #![allow(clippy::unchecked_time_subtraction)]
911
    #![allow(clippy::useless_vec)]
912
    #![allow(clippy::needless_pass_by_value)]
913
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
914
    use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
915

            
916
    use super::*;
917

            
918
    #[test]
919
    fn defaults() {
920
        let dflt = TorClientConfig::default();
921
        let b2 = TorClientConfigBuilder::default();
922
        let dflt2 = b2.build().unwrap();
923
        assert_eq!(&dflt, &dflt2);
924
    }
925

            
926
    #[test]
927
    fn builder() {
928
        let sec = std::time::Duration::from_secs(1);
929

            
930
        let mut authorities = dir::AuthorityContacts::builder();
931
        authorities.v3idents().push([22; 20].into());
932
        authorities.v3idents().push([44; 20].into());
933
        authorities.uploads().push(vec![
934
            SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80)),
935
            SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)),
936
        ]);
937

            
938
        let mut fallback = dir::FallbackDir::builder();
939
        fallback
940
            .rsa_identity([23; 20].into())
941
            .ed_identity([99; 32].into())
942
            .orports()
943
            .push("127.0.0.7:7".parse().unwrap());
944

            
945
        let mut bld = TorClientConfig::builder();
946
        *bld.tor_network().authorities() = authorities;
947
        bld.tor_network().set_fallback_caches(vec![fallback]);
948
        bld.storage()
949
            .cache_dir(CfgPath::new("/var/tmp/foo".to_owned()))
950
            .state_dir(CfgPath::new("/var/tmp/bar".to_owned()));
951
        bld.download_schedule().retry_certs().attempts(10);
952
        bld.download_schedule().retry_certs().initial_delay(sec);
953
        bld.download_schedule().retry_certs().parallelism(3);
954
        bld.download_schedule().retry_microdescs().attempts(30);
955
        bld.download_schedule()
956
            .retry_microdescs()
957
            .initial_delay(10 * sec);
958
        bld.download_schedule().retry_microdescs().parallelism(9);
959
        bld.override_net_params()
960
            .insert("wombats-per-quokka".to_owned(), 7);
961
        bld.path_rules()
962
            .ipv4_subnet_family_prefix(20)
963
            .ipv6_subnet_family_prefix(48);
964
        bld.circuit_timing()
965
            .max_dirtiness(90 * sec)
966
            .request_timeout(10 * sec)
967
            .request_max_retries(22)
968
            .request_loyalty(3600 * sec);
969
        bld.address_filter().allow_local_addrs(true);
970

            
971
        let val = bld.build().unwrap();
972

            
973
        assert_ne!(val, TorClientConfig::default());
974
    }
975

            
976
    #[test]
977
    fn bridges_supported() {
978
        /// checks that when s is processed as TOML for a client config,
979
        /// the resulting number of bridges is according to `exp`
980
        fn chk(exp: Result<usize, ()>, s: &str) {
981
            eprintln!("----------\n{s}\n----------\n");
982
            let got = (|| {
983
                let cfg: toml::Value = toml::from_str(s).unwrap();
984
                let cfg: TorClientConfigBuilder = cfg.try_into()?;
985
                let cfg = cfg.build()?;
986
                let n_bridges = cfg.bridges.bridges.len();
987
                Ok::<_, anyhow::Error>(n_bridges) // anyhow is just something we can use for ?
988
            })()
989
            .map_err(|_| ());
990
            assert_eq!(got, exp);
991
        }
992

            
993
        let chk_enabled_or_auto = |exp, bridges_toml| {
994
            for enabled in [r#""#, r#"enabled = true"#, r#"enabled = "auto""#] {
995
                chk(exp, &format!("[bridges]\n{}\n{}", enabled, bridges_toml));
996
            }
997
        };
998

            
999
        let ok_1_if = |b: bool| b.then_some(1).ok_or(());
        chk(
            Err(()),
            r#"
                [bridges]
                enabled = true
            "#,
        );
        chk_enabled_or_auto(
            ok_1_if(cfg!(feature = "bridge-client")),
            r#"
                bridges = ["192.0.2.83:80 $0bac39417268b96b9f514ef763fa6fba1a788956"]
            "#,
        );
        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(not(all(
        feature = "vanguards",
        any(feature = "onion-service-client", feature = "onion-service-service"),
    )))]
    fn check_disabled_vanguards_static() {
        // Force us to evaluate the closure to ensure that it builds correctly.
        #[allow(clippy::borrowed_box)]
        let _: &Box<VanguardConfig> = LazyLock::force(&DISABLED_VANGUARDS);
    }
    #[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);
        }
    }
}