1
//! Configuration for the Arti command line application
2
//
3
// (This module is called `cfg` to avoid name clash with the `config` crate, which we use.)
4

            
5
use derive_deftly::Deftly;
6
use tor_basic_utils::ByteQty;
7
use tor_config_path::CfgPath;
8

            
9
#[cfg(feature = "onion-service-service")]
10
use crate::onion_proxy::{
11
    OnionServiceProxyConfigBuilder, OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder,
12
};
13
#[cfg(feature = "rpc")]
14
semipublic_use! {
15
    use crate::rpc::{
16
        RpcConfig, RpcConfigBuilder,
17
        listener::{RpcListenerSetConfig, RpcListenerSetConfigBuilder},
18
    };
19
}
20
use arti_client::TorClientConfig;
21
#[cfg(feature = "onion-service-service")]
22
use tor_config::define_list_builder_accessors;
23
use tor_config::derive::prelude::*;
24
pub(crate) use tor_config::{ConfigBuildError, Listen};
25
pub(crate) use tor_config_shared::metrics::{MetricsConfig, MetricsConfigBuilder};
26

            
27
use crate::{LoggingConfig, LoggingConfigBuilder};
28

            
29
/// Example file demonstrating our configuration and the default options.
30
///
31
/// The options in this example file are all commented out;
32
/// the actual defaults are done via builder attributes in all the Rust config structs.
33
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
34
pub(crate) const ARTI_EXAMPLE_CONFIG: &str = concat!(include_str!("./arti-example-config.toml"));
35

            
36
/// Test case file for the oldest version of the config we still support.
37
///
38
/// (When updating, copy `arti-example-config.toml` from the earliest version we want to
39
/// be compatible with.)
40
//
41
// Probably, in the long run, we will want to make this architecture more general: we'll want
42
// to have a larger number of examples to test, and we won't want to write a separate constant
43
// for each. Probably in that case, we'll want a directory of test examples, and we'll want to
44
// traverse the whole directory.
45
//
46
// Compare C tor, look at conf_examples and conf_failures - each of the subdirectories there is
47
// an example configuration situation that we wanted to validate.
48
//
49
// NB here in Arti the OLDEST_SUPPORTED_CONFIG and the ARTI_EXAMPLE_CONFIG are tested
50
// somewhat differently: we test that the current example is *exhaustive*, not just
51
// parsable.
52
#[cfg(test)]
53
const OLDEST_SUPPORTED_CONFIG: &str = concat!(include_str!("./oldest-supported-config.toml"),);
54

            
55
// Our proxy sockets will use a small-ish fixed kernel socket buffer size.
56
// Tor streams are slow relative to a pair of loopback sockets,
57
// so don't need socket buffers as large as what Linux provides by default
58
// (sometimes several MBs).
59
//
60
// This has a few advantages over the defaults:
61
// - Less buffer bloat.
62
// - Better ability to make congestion/flow control decisions.
63
// - Disables TCP autotuning, which means behaviour will better match Shadow sims.
64
// - Easier to reason about stream performance when the buffer size isn't dynamic.
65
//
66
// See https://gitlab.torproject.org/tpo/core/arti/-/work_items/2500.
67
/// See [`ProxyConfig::socket_send_buf_size`].
68
const DEFAULT_SEND_BUF_SIZE: usize = 128_000;
69
/// See [`ProxyConfig::socket_recv_buf_size`].
70
const DEFAULT_RECV_BUF_SIZE: usize = 128_000;
71

            
72
/// Replacement for rpc config when the rpc feature is disabled.
73
#[cfg(not(feature = "rpc"))]
74
type RpcConfig = ();
75

            
76
/// Replacement for onion service config when the onion service feature is disabled.
77
#[cfg(not(feature = "onion-service-service"))]
78
type OnionServiceProxyConfigMap = ();
79

            
80
/// Structure to hold our application configuration options
81
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
82
#[derive_deftly(TorConfig)]
83
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
84
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
85
pub(crate) struct ApplicationConfig {
86
    /// If true, we should watch our configuration files for changes, and reload
87
    /// our configuration when they change.
88
    ///
89
    /// Note that this feature may behave in unexpected ways if the path to the
90
    /// directory holding our configuration files changes its identity (because
91
    /// an intermediate symlink is changed, because the directory is removed and
92
    /// recreated, or for some other reason).
93
    #[deftly(tor_config(default))]
94
    pub(crate) watch_configuration: bool,
95

            
96
    /// If true, we should allow other applications not owned by the system
97
    /// administrator to monitor the Arti application and inspect its memory.
98
    ///
99
    /// Otherwise, we take various steps (including disabling core dumps) to
100
    /// make it harder for other programs to view our internal state.
101
    ///
102
    /// This option has no effect when arti is built without the `harden`
103
    /// feature.  When `harden` is not enabled, debugger attachment is permitted
104
    /// whether this option is set or not.
105
    #[deftly(tor_config(default))]
106
    pub(crate) permit_debugging: bool,
107

            
108
    /// If true, then we do not exit when we are running as `root`.
109
    ///
110
    /// This has no effect on Windows.
111
    #[deftly(tor_config(default))]
112
    pub(crate) allow_running_as_root: bool,
113

            
114
    /// If true, then we do not bootstrap a [`TorClient`](arti_client::TorClient) on startup.
115
    /// Instead, we defer bootstrapping until _either_ this option is false,
116
    /// or until an RPC-using application tells us to bootstrap.
117
    ///
118
    /// We will still bind to proxy ports at startup, but we won't make any connections
119
    /// to the network until after we are bootstrapping.
120
    #[deftly(tor_config(default))]
121
    pub(crate) defer_bootstrap: bool,
122
}
123

            
124
/// Configuration for one or more proxy listeners.
125
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
126
#[derive_deftly(TorConfig)]
127
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
128
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
129
pub(crate) struct ProxyConfig {
130
    /// Addresses to listen on for incoming SOCKS connections.
131
    //
132
    // TODO: Once http-connect is non-experimental, we should rename this option in a backward-compatible way.
133
    #[deftly(tor_config(default = "Listen::new_localhost(9150)"))]
134
    pub(crate) socks_listen: Listen,
135

            
136
    /// Addresses to listen on for incoming DNS connections.
137
    #[deftly(tor_config(default = "Listen::new_none()"))]
138
    pub(crate) dns_listen: Listen,
139

            
140
    /// If true, and the `http-connect` feature is enabled,
141
    /// all members of `socks_listen` also support HTTP CONNECT.
142
    //
143
    // TODO:
144
    // At some point in the future we might want per-port configuration, like Tor has.
145
    #[deftly(tor_config(
146
        cfg = r#" feature="http-connect" "#,
147
        cfg_desc = "with HTTP CONNECT support"
148
    ))]
149
    #[deftly(tor_config(default = "true"))]
150
    pub(crate) enable_http_connect: bool,
151

            
152
    /// The send buffer size (`SO_SNDBUF`) of proxy sockets.
153
    #[deftly(tor_config(default = "ByteQty(DEFAULT_SEND_BUF_SIZE)"))]
154
    pub(crate) socket_send_buf_size: ByteQty,
155

            
156
    /// The receive buffer size (`SO_RCVBUF`) of proxy sockets.
157
    #[deftly(tor_config(default = "ByteQty(DEFAULT_RECV_BUF_SIZE)"))]
158
    pub(crate) socket_recv_buf_size: ByteQty,
159
}
160

            
161
impl ProxyConfig {
162
    /// Return the stream proxy protocols we support according to this configuration.
163
    pub(crate) fn protocols(&self) -> crate::proxy::ListenProtocols {
164
        use crate::proxy::ListenProtocols::*;
165
        #[cfg(feature = "http-connect")]
166
        if self.enable_http_connect {
167
            return SocksAndHttpConnect;
168
        }
169

            
170
        SocksOnly
171
    }
172
}
173

            
174
/// Configuration for arti-specific storage locations.
175
///
176
/// See also [`arti_client::config::StorageConfig`].
177
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
178
#[derive_deftly(TorConfig)]
179
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
180
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
181
pub(crate) struct ArtiStorageConfig {
182
    /// A file in which to write information about the ports we're listening on.
183
    #[deftly(tor_config(setter(into), default = "default_port_info_file()"))]
184
    pub(crate) port_info_file: CfgPath,
185
}
186

            
187
/// Return the default ports_info_file location.
188
356
fn default_port_info_file() -> CfgPath {
189
356
    CfgPath::new("${ARTI_LOCAL_DATA}/public/port_info.json".to_owned())
190
356
}
191

            
192
/// Configuration for system resources used by Tor.
193
///
194
/// You cannot change *these variables* in this section on a running Arti client.
195
///
196
/// Note that there are other settings in this section,
197
/// in [`arti_client::config::SystemConfig`].
198
//
199
// These two structs exist because:
200
//
201
//  1. Our doctrine is that configuration structs live with the code that uses the info.
202
//  2. tor-memquota's configuration is used by the MemoryQuotaTracker in TorClient
203
//  3. File descriptor limits are enforced here in arti because it's done process-global
204
//  4. Nevertheless, logically, these things want to be in the same section of the file.
205
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
206
#[derive_deftly(TorConfig)]
207
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
208
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
209
#[non_exhaustive]
210
pub(crate) struct SystemConfig {
211
    /// Maximum number of file descriptors we should launch with
212
    #[deftly(tor_config(setter(into), default = "default_max_files()"))]
213
    pub(crate) max_files: u64,
214
}
215

            
216
/// Return the default maximum number of file descriptors to launch with.
217
354
fn default_max_files() -> u64 {
218
354
    16384
219
354
}
220

            
221
/// Structure to hold Arti's configuration options, whether from a
222
/// configuration file or the command line.
223
//
224
/// These options are declared in a public crate outside of `arti` so that other
225
/// applications can parse and use them, if desired.  If you're only embedding
226
/// arti via `arti-client`, and you don't want to use Arti's configuration
227
/// format, use [`arti_client::TorClientConfig`] instead.
228
///
229
/// By default, Arti will run using the default Tor network, store state and
230
/// cache information to a per-user set of directories shared by all
231
/// that user's applications, and run a SOCKS client on a local port.
232
///
233
/// NOTE: These are NOT the final options or their final layout. Expect NO
234
/// stability here.
235
#[derive(Debug, Deftly, Clone, Eq, PartialEq)]
236
#[derive_deftly(TorConfig)]
237
#[deftly(tor_config(post_build = "Self::post_build"))]
238
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
239
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
240
pub(crate) struct ArtiConfig {
241
    /// Configuration for application behavior.
242
    #[deftly(tor_config(sub_builder))]
243
    application: ApplicationConfig,
244

            
245
    /// Configuration for proxy listeners
246
    #[deftly(tor_config(sub_builder))]
247
    proxy: ProxyConfig,
248

            
249
    /// Logging configuration
250
    #[deftly(tor_config(sub_builder))]
251
    logging: LoggingConfig,
252

            
253
    /// Metrics configuration
254
    #[deftly(tor_config(sub_builder))]
255
    pub(crate) metrics: MetricsConfig,
256

            
257
    /// Configuration for RPC subsystem
258
    #[deftly(tor_config(
259
        sub_builder,
260
        cfg = r#" feature = "rpc" "#,
261
        cfg_desc = "with RPC support"
262
    ))]
263
    pub(crate) rpc: RpcConfig,
264

            
265
    /// Information on system resources used by Arti.
266
    ///
267
    /// Note that there are other settings in this section,
268
    /// in [`arti_client::config::SystemConfig`] -
269
    /// these two structs overlay here.
270
    #[deftly(tor_config(sub_builder))]
271
    pub(crate) system: SystemConfig,
272

            
273
    /// Information on where things are stored by Arti.
274
    ///
275
    /// Note that [`TorClientConfig`] also has a storage configuration;
276
    /// our configuration logic should merge them correctly.
277
    #[deftly(tor_config(sub_builder))]
278
    pub(crate) storage: ArtiStorageConfig,
279

            
280
    /// Configured list of proxied onion services.
281
    ///
282
    /// Note that this field is present unconditionally, but when onion service
283
    /// support is disabled, it is replaced with a stub type from
284
    /// `onion_proxy_disabled`, and its setter functions are not implemented.
285
    /// The purpose of this stub type is to give an error if somebody tries to
286
    /// configure onion services when the `onion-service-service` feature is
287
    /// disabled.
288
    #[deftly(tor_config(
289
        setter(skip),
290
        sub_builder,
291
        cfg = r#" feature = "onion-service-service" "#,
292
        cfg_reject,
293
        cfg_desc = "with onion service support"
294
    ))]
295
    pub(crate) onion_services: OnionServiceProxyConfigMap,
296
}
297

            
298
impl ArtiConfigBuilder {
299
    /// validate the [`ArtiConfig`] after building.
300
    #[allow(clippy::unnecessary_wraps)]
301
348
    fn post_build(config: ArtiConfig) -> Result<ArtiConfig, ConfigBuildError> {
302
        #[cfg_attr(not(feature = "onion-service-service"), allow(unused_mut))]
303
348
        let mut config = config;
304
        #[cfg(feature = "onion-service-service")]
305
348
        for svc in config.onion_services.values_mut() {
306
140
            // Pass the application-level watch_configuration to each restricted discovery config.
307
140
            *svc.svc_cfg
308
140
                .restricted_discovery_mut()
309
140
                .watch_configuration_mut() = config.application.watch_configuration;
310
140
        }
311

            
312
348
        Ok(config)
313
348
    }
314
}
315

            
316
impl tor_config::load::TopLevel for ArtiConfig {
317
    type Builder = ArtiConfigBuilder;
318
    // Some config options such as "proxy.socks_port" are no longer
319
    // just "deprecated" and have since been completely removed from Arti,
320
    // but there's no harm in informing the user that the options are still deprecated.
321
    // For these removed options, Arti will ignore them like it does for all unknown options.
322
    const DEPRECATED_KEYS: &'static [&'static str] = &["proxy.socks_port", "proxy.dns_port"];
323
}
324

            
325
#[cfg(feature = "onion-service-service")]
326
define_list_builder_accessors! {
327
    struct ArtiConfigBuilder {
328
        pub(crate) onion_services: [OnionServiceProxyConfigBuilder],
329
    }
330
}
331

            
332
/// Convenience alias for the config for a whole `arti` program
333
///
334
/// Used primarily as a type parameter on calls to [`tor_config::resolve`]
335
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
336
pub(crate) type ArtiCombinedConfig = (ArtiConfig, TorClientConfig);
337

            
338
impl ArtiConfig {
339
    /// Return the [`ApplicationConfig`] for this configuration.
340
560
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
341
560
    pub(crate) fn application(&self) -> &ApplicationConfig {
342
560
        &self.application
343
560
    }
344

            
345
    /// Return the [`LoggingConfig`] for this configuration.
346
278
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
347
278
    pub(crate) fn logging(&self) -> &LoggingConfig {
348
278
        &self.logging
349
278
    }
350

            
351
    /// Return the [`ProxyConfig`] for this configuration.
352
2
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
353
2
    pub(crate) fn proxy(&self) -> &ProxyConfig {
354
2
        &self.proxy
355
2
    }
356

            
357
    /// Return the [`ArtiStorageConfig`] for this configuration.
358
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
359
    ///
360
    pub(crate) fn storage(&self) -> &ArtiStorageConfig {
361
        &self.storage
362
    }
363

            
364
    /// Return the [`RpcConfig`] for this configuration.
365
    #[cfg(feature = "rpc")]
366
4
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
367
4
    pub(crate) fn rpc(&self) -> &RpcConfig {
368
4
        &self.rpc
369
4
    }
370
}
371

            
372
#[cfg(test)]
373
mod test {
374
    // @@ begin test lint list maintained by maint/add_warning @@
375
    #![allow(clippy::bool_assert_comparison)]
376
    #![allow(clippy::clone_on_copy)]
377
    #![allow(clippy::dbg_macro)]
378
    #![allow(clippy::mixed_attributes_style)]
379
    #![allow(clippy::print_stderr)]
380
    #![allow(clippy::print_stdout)]
381
    #![allow(clippy::single_char_pattern)]
382
    #![allow(clippy::unwrap_used)]
383
    #![allow(clippy::unchecked_time_subtraction)]
384
    #![allow(clippy::useless_vec)]
385
    #![allow(clippy::needless_pass_by_value)]
386
    #![allow(clippy::string_slice)] // See arti#2571
387
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
388
    // TODO add this next lint to maint/add_warning, for all tests
389
    #![allow(clippy::iter_overeager_cloned)]
390
    // Saves adding many individual #[cfg], or a sub-module
391
    #![cfg_attr(not(feature = "pt-client"), allow(dead_code))]
392

            
393
    use arti_client::config::TorClientConfigBuilder;
394
    use arti_client::config::dir;
395
    use itertools::{EitherOrBoth, Itertools, chain};
396
    use regex::Regex;
397
    use std::collections::HashSet;
398
    use std::fmt::Write as _;
399
    use std::iter;
400
    use std::time::Duration;
401
    use tor_config::load::{ConfigResolveError, ResolutionResults};
402
    use tor_config_path::CfgPath;
403

            
404
    #[allow(unused_imports)] // depends on features
405
    use tor_error::ErrorReport as _;
406

            
407
    #[cfg(feature = "restricted-discovery")]
408
    use {
409
        arti_client::HsClientDescEncKey,
410
        std::str::FromStr as _,
411
        tor_hsservice::config::restricted_discovery::{
412
            DirectoryKeyProviderBuilder, HsClientNickname,
413
        },
414
    };
415

            
416
    use super::*;
417

            
418
    //---------- tests that rely on the provided example config file ----------
419
    //
420
    // These are quite complex.  They uncomment the file, parse bits of it,
421
    // and do tests via serde and via the normal config machinery,
422
    // to see that everything is documented as expected.
423

            
424
    fn uncomment_example_settings(template: &str) -> String {
425
        let re = Regex::new(r#"(?m)^\#([^ \n])"#).unwrap();
426
        re.replace_all(template, |cap: &regex::Captures<'_>| -> _ {
427
            cap.get(1).unwrap().as_str().to_string()
428
        })
429
        .into()
430
    }
431

            
432
    /// Is this key present or absent in the examples in one of the example files ?
433
    ///
434
    /// Depending on which variable this is in, it refers to presence in other the
435
    /// old or the new example file.
436
    ///
437
    /// This type is *not* used in declarations in `declared_config_exceptions`;
438
    /// it is used by the actual checking code.
439
    /// The declarations use types in that function.
440
    #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
441
    enum InExample {
442
        Absent,
443
        Present,
444
    }
445
    /// Which of the two example files?
446
    ///
447
    /// This type is *not* used in declarations in `declared_config_exceptions`;
448
    /// it is used by the actual checking code.
449
    /// The declarations use types in that function.
450
    #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
451
    enum WhichExample {
452
        Old,
453
        New,
454
    }
455
    /// An exception to the usual expectations about configuration example files
456
    ///
457
    /// This type is *not* used in declarations in `declared_config_exceptions`;
458
    /// it is used by the actual checking code.
459
    /// The declarations use types in that function.
460
    #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
461
    struct ConfigException {
462
        /// The actual config key
463
        key: String,
464
        /// Does it appear in the oldest supported example file?
465
        in_old_example: InExample,
466
        /// Does it appear in the current example file?
467
        in_new_example: InExample,
468
        /// Does our code recognise it ?  `None` means "don't know"
469
        in_code: Option<bool>,
470
    }
471
    impl ConfigException {
472
        fn in_example(&self, which: WhichExample) -> InExample {
473
            use WhichExample::*;
474
            match which {
475
                Old => self.in_old_example,
476
                New => self.in_new_example,
477
            }
478
        }
479
    }
480

            
481
    /// *every* feature that's listed as `InCode::FeatureDependent`
482
    const ALL_RELEVANT_FEATURES_ENABLED: bool = cfg!(all(
483
        feature = "bridge-client",
484
        feature = "pt-client",
485
        feature = "onion-service-client",
486
        feature = "rpc",
487
    ));
488

            
489
    /// Return the expected exceptions to the usual expectations about config and examples
490
    fn declared_config_exceptions() -> Vec<ConfigException> {
491
        /// Is this key recognised by the parsing code ?
492
        ///
493
        /// (This can be feature-dependent, so literal values of this type
494
        /// are often feature-qualified.)
495
        #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
496
        enum InCode {
497
            /// No configuration of this codebase knows about this option
498
            Ignored,
499
            /// *Some* configuration of this codebase know about this option
500
            ///
501
            /// This means:
502
            ///   - If *every* feature in `ALL_RELEVANT_FEATURES_ENABLED` is enabled,
503
            ///     the config key is expected to be `Recognised`
504
            ///   - Otherwise we're not sure (because cargo features are additive,
505
            ///     dependency crates' features might be *en*abled willy-nilly).
506
            FeatureDependent,
507
            /// All configurations of this codebase know about this option
508
            Recognized,
509
        }
510
        use InCode::*;
511

            
512
        /// Marker.  `Some(InOld)` means presence of this config key in the oldest-supported file
513
        struct InOld;
514
        /// Marker.  `Some(InNew)` means presence of this config key in the current example file
515
        struct InNew;
516

            
517
        let mut out = vec![];
518

            
519
        // Declare some keys which aren't "normal", eg they aren't documented in the usual
520
        // way, are configurable, aren't in the oldest supported file, etc.
521
        //
522
        // `in_old_example` and `in_new_example` are whether the key appears in
523
        // `arti-example-config.toml` and `oldest-supported-config.toml` respectively.
524
        // (in each case, only a line like `#example.key = ...` counts.)
525
        //
526
        // `whether_supported` tells is if the key is supposed to be
527
        // recognised by the code.
528
        //
529
        // `keys` is the list of keys.  Add a // comment at the start of the list
530
        // so that rustfmt retains the consistent formatting.
531
        let mut declare_exceptions = |in_old_example: Option<InOld>,
532
                                      in_new_example: Option<InNew>,
533
                                      in_code: InCode,
534
                                      keys: &[&str]| {
535
            let in_code = match in_code {
536
                Ignored => Some(false),
537
                Recognized => Some(true),
538
                FeatureDependent if ALL_RELEVANT_FEATURES_ENABLED => Some(true),
539
                FeatureDependent => None,
540
            };
541
            #[allow(clippy::needless_pass_by_value)] // pass by value defends against a->a b->a
542
            fn in_example<T>(spec: Option<T>) -> InExample {
543
                match spec {
544
                    None => InExample::Absent,
545
                    Some(_) => InExample::Present,
546
                }
547
            }
548
            let in_old_example = in_example(in_old_example);
549
            let in_new_example = in_example(in_new_example);
550
            out.extend(keys.iter().cloned().map(|key| ConfigException {
551
                key: key.to_owned(),
552
                in_old_example,
553
                in_new_example,
554
                in_code,
555
            }));
556
        };
557

            
558
        declare_exceptions(
559
            None,
560
            Some(InNew),
561
            Recognized,
562
            &[
563
                // Keys that are newer than the oldest-supported example, but otherwise normal.
564
                "application.allow_running_as_root",
565
                "bridges",
566
                "logging.syslog",
567
                "logging.time_granularity",
568
                "path_rules.long_lived_ports",
569
                "circuit_timing.disused_circuit_timeout",
570
                "storage.port_info_file",
571
                "proxy.socket_send_buf_size",
572
                "proxy.socket_recv_buf_size",
573
                "application.defer_bootstrap",
574
            ],
575
        );
576

            
577
        declare_exceptions(
578
            None,
579
            None,
580
            Recognized,
581
            &[
582
                // Examples exist but are not auto-testable
583
                "tor_network.authorities",
584
                "tor_network.fallback_caches",
585
            ],
586
        );
587

            
588
        declare_exceptions(
589
            None,
590
            None,
591
            Recognized,
592
            &[
593
                // Examples exist but are not auto-testable
594
                "logging.opentelemetry",
595
            ],
596
        );
597

            
598
        declare_exceptions(
599
            Some(InOld),
600
            Some(InNew),
601
            if cfg!(target_family = "windows") {
602
                Ignored
603
            } else {
604
                Recognized
605
            },
606
            &[
607
                // Unix-only mistrust settings
608
                "storage.permissions.trust_group",
609
                "storage.permissions.trust_user",
610
            ],
611
        );
612

            
613
        declare_exceptions(
614
            None,
615
            None, // TODO: Make examples for bridges settings!
616
            FeatureDependent,
617
            &[
618
                // Settings only available with bridge support
619
                "bridges.transports", // we recognise this so we can reject it
620
            ],
621
        );
622

            
623
        declare_exceptions(
624
            None,
625
            Some(InNew),
626
            FeatureDependent,
627
            &[
628
                // Settings only available with experimental-api support
629
                "storage.keystore",
630
            ],
631
        );
632

            
633
        declare_exceptions(
634
            None,
635
            None, // it's there, but not formatted for auto-testing
636
            FeatureDependent,
637
            &[
638
                // Settings only available with tokio-console support
639
                "logging.tokio_console",
640
                "logging.tokio_console.enabled",
641
            ],
642
        );
643

            
644
        declare_exceptions(
645
            None,
646
            None, // it's there, but not formatted for auto-testing
647
            Recognized,
648
            &[
649
                // Memory quota, tested by fn memquota (below)
650
                "system.memory",
651
                "system.memory.max",
652
                "system.memory.low_water",
653
            ],
654
        );
655

            
656
        declare_exceptions(
657
            None,
658
            Some(InNew), // The top-level section is in the new file (only).
659
            Recognized,
660
            &["metrics"],
661
        );
662

            
663
        declare_exceptions(
664
            None,
665
            None, // The inner information is not formatted for auto-testing
666
            Recognized,
667
            &[
668
                // Prometheus metrics exporter, tested by fn metrics (below)
669
                "metrics.prometheus",
670
                "metrics.prometheus.listen",
671
            ],
672
        );
673

            
674
        declare_exceptions(
675
            None,
676
            Some(InNew),
677
            FeatureDependent,
678
            &[
679
                // PT-only settings
680
            ],
681
        );
682

            
683
        declare_exceptions(
684
            None,
685
            Some(InNew),
686
            FeatureDependent,
687
            &[
688
                // HS client settings
689
                "address_filter.allow_onion_addrs",
690
                "circuit_timing.hs_desc_fetch_attempts",
691
                "circuit_timing.hs_intro_rend_attempts",
692
                "circuit_timing.hs_dir_requery_interval",
693
            ],
694
        );
695

            
696
        declare_exceptions(
697
            None,
698
            Some(InNew),
699
            FeatureDependent,
700
            &[
701
                // HTTP Connect settings
702
                "proxy.enable_http_connect",
703
            ],
704
        );
705

            
706
        declare_exceptions(
707
            None,
708
            None, // TODO RPC, these should actually appear in the example config
709
            FeatureDependent,
710
            &[
711
                // RPC-only settings
712
                "rpc",
713
                "rpc.rpc_listen",
714
            ],
715
        );
716

            
717
        // These are commented-out by default, and tested with test::onion_services().
718
        declare_exceptions(
719
            None,
720
            None,
721
            FeatureDependent,
722
            &[
723
                // onion-service only settings.
724
                "onion_services",
725
            ],
726
        );
727

            
728
        declare_exceptions(
729
            None,
730
            Some(InNew),
731
            FeatureDependent,
732
            &[
733
                // Vanguards-specific settings
734
                "vanguards",
735
                "vanguards.mode",
736
            ],
737
        );
738

            
739
        // These are commented-out by default
740
        declare_exceptions(
741
            None,
742
            None,
743
            FeatureDependent,
744
            &[
745
                "storage.keystore.ctor",
746
                "storage.keystore.ctor.services",
747
                "storage.keystore.ctor.clients",
748
            ],
749
        );
750

            
751
        out.sort();
752

            
753
        let dupes = out.iter().map(|exc| &exc.key).duplicates().collect_vec();
754
        assert!(
755
            dupes.is_empty(),
756
            "duplicate exceptions in configuration {dupes:?}"
757
        );
758

            
759
        eprintln!(
760
            "declared config exceptions for this configuration:\n{:#?}",
761
            out
762
        );
763
        out
764
    }
765

            
766
    #[test]
767
    fn default_config() {
768
        use InExample::*;
769

            
770
        let empty_config = tor_config::ConfigurationSources::new_empty()
771
            .load()
772
            .unwrap();
773
        let empty_config: ArtiCombinedConfig = tor_config::resolve(empty_config).unwrap();
774

            
775
        let default = (ArtiConfig::default(), TorClientConfig::default());
776
        let exceptions = declared_config_exceptions();
777

            
778
        /// Helper to decide what to do about a possible discrepancy
779
        ///
780
        /// Provided with `EitherOrBoth` of:
781
        ///   - the config key that the config parser reported it found, but didn't recognise
782
        ///   - the declared exception entry
783
        ///     (for the same config key)
784
        ///
785
        /// Decides whether this is something that should fail the test.
786
        /// If so it returns `Err((key, error_message))`, otherwise `Ok`.
787
        #[allow(clippy::needless_pass_by_value)] // clippy is IMO wrong about eob
788
        fn analyse_joined_info(
789
            which: WhichExample,
790
            uncommented: bool,
791
            eob: EitherOrBoth<&String, &ConfigException>,
792
        ) -> Result<(), (String, String)> {
793
            use EitherOrBoth::*;
794
            let (key, err) = match eob {
795
                // Unrecognised entry, no exception
796
                Left(found) => (found, "found in example but not processed".into()),
797
                Both(found, exc) => {
798
                    let but = match (exc.in_example(which), exc.in_code, uncommented) {
799
                        (Absent, _, _) => "but exception entry expected key to be absent",
800
                        (_, _, false) => "when processing still-commented-out file!",
801
                        (_, Some(true), _) => {
802
                            "but an exception entry says it should have been recognised"
803
                        }
804
                        (Present, Some(false), true) => return Ok(()), // that's as expected
805
                        (Present, None, true) => return Ok(()), // that's could be as expected
806
                    };
807
                    (
808
                        found,
809
                        format!("parser reported unrecognised config key, {but}"),
810
                    )
811
                }
812
                Right(exc) => {
813
                    // An exception entry exists.  The actual situation is either
814
                    //   - not found in file (so no "unrecognised" report)
815
                    //   - processed successfully (found in file and in code)
816
                    // but we don't know which.
817
                    let trouble = match (exc.in_example(which), exc.in_code, uncommented) {
818
                        (Absent, _, _) => return Ok(()), // not in file, no report expected
819
                        (_, _, false) => return Ok(()),  // not uncommented, no report expected
820
                        (_, Some(true), _) => return Ok(()), // code likes it, no report expected
821
                        (Present, Some(false), true) => {
822
                            "expected an 'unknown config key' report but didn't see one"
823
                        }
824
                        (Present, None, true) => return Ok(()), // not sure, have to just allow it
825
                    };
826
                    (&exc.key, trouble.into())
827
                }
828
            };
829
            Err((key.clone(), err))
830
        }
831

            
832
        let parses_to_defaults = |example: &str, which: WhichExample, uncommented: bool| {
833
            let cfg = {
834
                let mut sources = tor_config::ConfigurationSources::new_empty();
835
                sources.push_source(
836
                    tor_config::ConfigurationSource::from_verbatim(example.to_string()),
837
                    tor_config::sources::MustRead::MustRead,
838
                );
839
                sources.load().unwrap()
840
            };
841

            
842
            // This tests that the example settings do not *contradict* the defaults.
843
            let results: ResolutionResults<ArtiCombinedConfig> =
844
                tor_config::resolve_return_results(cfg, &Default::default()).unwrap();
845

            
846
            assert_eq!(&results.value, &default, "{which:?} {uncommented:?}");
847
            assert_eq!(&results.value, &empty_config, "{which:?} {uncommented:?}");
848

            
849
            // We serialize the DisfavouredKey entries to strings to compare them against
850
            // `known_unrecognized_options`.
851
            let unrecognized = results
852
                .unrecognized
853
                .iter()
854
                .map(|k| k.to_string())
855
                .collect_vec();
856

            
857
            eprintln!(
858
                "parsing of {which:?} uncommented={uncommented:?}, unrecognized={unrecognized:#?}"
859
            );
860

            
861
            let reports =
862
                Itertools::merge_join_by(unrecognized.iter(), exceptions.iter(), |u, e| {
863
                    u.as_str().cmp(&e.key)
864
                })
865
                .filter_map(|eob| analyse_joined_info(which, uncommented, eob).err())
866
                .collect_vec();
867

            
868
            if !reports.is_empty() {
869
                let reports = reports.iter().fold(String::new(), |mut out, (k, s)| {
870
                    writeln!(out, "  {}: {}", s, k).unwrap();
871
                    out
872
                });
873

            
874
                panic!(
875
                    r"
876
mismatch: results of parsing example files (& vs declared exceptions):
877
example config file {which:?}, uncommented={uncommented:?}
878
{reports}
879
"
880
                );
881
            }
882

            
883
            results.value
884
        };
885

            
886
        let _ = parses_to_defaults(ARTI_EXAMPLE_CONFIG, WhichExample::New, false);
887
        let _ = parses_to_defaults(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, false);
888

            
889
        let built_default = (
890
            ArtiConfigBuilder::default().build().unwrap(),
891
            TorClientConfigBuilder::default().build().unwrap(),
892
        );
893

            
894
        let parsed = parses_to_defaults(
895
            &uncomment_example_settings(ARTI_EXAMPLE_CONFIG),
896
            WhichExample::New,
897
            true,
898
        );
899
        let parsed_old = parses_to_defaults(
900
            &uncomment_example_settings(OLDEST_SUPPORTED_CONFIG),
901
            WhichExample::Old,
902
            true,
903
        );
904

            
905
        assert_eq!(&parsed, &built_default);
906
        assert_eq!(&parsed_old, &built_default);
907

            
908
        assert_eq!(&default, &built_default);
909
    }
910

            
911
    /// Config file exhaustiveness and default checking
912
    ///
913
    /// `example_file` is a putative configuration file text.
914
    /// It is expected to contain "example lines",
915
    /// which are lines in start with `#` *not followed by whitespace*.
916
    ///
917
    /// This function checks that:
918
    ///
919
    /// Positive check on the example lines that are present.
920
    ///  * `example_file`, when example lines are uncommented, can be parsed.
921
    ///  * The example values are the same as the default values.
922
    ///
923
    /// Check for missing examples:
924
    ///  * Every key `in `TorClientConfig` or `ArtiConfig` has a corresponding example value.
925
    ///  * Except as declared in [`declared_config_exceptions`]
926
    ///  * And also, tolerating absence in the example files of `deprecated` keys
927
    ///
928
    /// It handles straightforward cases, where the example line is in a `[section]`
929
    /// and is something like `#key = value`.
930
    ///
931
    /// More complex keys, eg those which don't appear in "example lines" starting with just `#`,
932
    /// must be dealt with ad-hoc and mentioned in `declared_config_exceptions`.
933
    ///
934
    /// For complex config keys, it may not be sufficient to simply write the default value in
935
    /// the example files (along with perhaps some other information).  In that case,
936
    ///   1. Write a bespoke example (with lines starting `# `) in the config file.
937
    ///   2. Write a bespoke test, to test the parsing of the bespoke example.
938
    ///      This will probably involve using `ExampleSectionLines` and may be quite ad-hoc.
939
    ///      The test function bridges(), below, is a complex worked example.
940
    ///   3. Either add a trivial example for the affected key(s) (starting with just `#`)
941
    ///      or add the affected key(s) to `declared_config_exceptions`
942
    fn exhaustive_1(example_file: &str, which: WhichExample, deprecated: &[String]) {
943
        use InExample::*;
944
        use serde_json::Value as JsValue;
945
        use std::collections::BTreeSet;
946

            
947
        let example = uncomment_example_settings(example_file);
948
        let example: toml::Value = toml::from_str(&example).unwrap();
949
        // dbg!(&example);
950
        let example = serde_json::to_value(example).unwrap();
951
        // dbg!(&example);
952

            
953
        // "Exhaustive" taxonomy of the recognized configuration keys
954
        //
955
        // We use the JSON serialization of the default builders, because Rust's toml
956
        // implementation likes to omit more things, that we want to see.
957
        //
958
        // I'm not sure this is quite perfect but it is pretty good,
959
        // and has found a number of un-exampled config keys.
960
        let exhausts = [
961
            serde_json::to_value(TorClientConfig::builder()).unwrap(),
962
            serde_json::to_value(ArtiConfig::builder()).unwrap(),
963
        ];
964

            
965
        /// This code does *not* record a problem for keys *in* the example file
966
        /// that are unrecognized.  That is handled by the `default_config` test.
967
        #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, derive_more::Display)]
968
        enum ProblemKind {
969
            #[display("recognised by serialisation, but missing from example config file")]
970
            MissingFromExample,
971
            #[display("expected that example config file should contain have this as a table")]
972
            ExpectedTableInExample,
973
            #[display(
974
                "declared exception says this key should be recognised but not in file, but that doesn't seem to be the case"
975
            )]
976
            UnusedException,
977
        }
978

            
979
        #[derive(Default, Debug)]
980
        struct Walk {
981
            current_path: Vec<String>,
982
            problems: Vec<(String, ProblemKind)>,
983
        }
984

            
985
        impl Walk {
986
            /// Records a problem
987
            fn bad(&mut self, kind: ProblemKind) {
988
                self.problems.push((self.current_path.join("."), kind));
989
            }
990

            
991
            /// Recurses, looking for problems
992
            ///
993
            /// Visited for every node in either or both of the starting `exhausts`.
994
            ///
995
            /// `E` is the number of elements in `exhausts`, ie the number of different
996
            /// top-level config types that Arti uses.  Ie, 2.
997
            fn walk<const E: usize>(
998
                &mut self,
999
                example: Option<&JsValue>,
                exhausts: [Option<&JsValue>; E],
            ) {
                assert! { exhausts.into_iter().any(|e| e.is_some()) }
                let example = if let Some(e) = example {
                    e
                } else {
                    self.bad(ProblemKind::MissingFromExample);
                    return;
                };
                let tables = exhausts.map(|e| e?.as_object());
                // Union of the keys of both exhausts' tables (insofar as they *are* tables)
                let table_keys = tables
                    .iter()
                    .flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten())
                    .collect::<BTreeSet<String>>();
                for key in table_keys {
                    let example = if let Some(e) = example.as_object() {
                        e
                    } else {
                        // At least one of the exhausts was a nonempty table,
                        // but the corresponding example node isn't a table.
                        self.bad(ProblemKind::ExpectedTableInExample);
                        continue;
                    };
                    // Descend the same key in all the places.
                    self.current_path.push(key.clone());
                    self.walk(example.get(&key), tables.map(|t| t?.get(&key)));
                    self.current_path.pop().unwrap();
                }
            }
        }
        let exhausts = exhausts.iter().map(Some).collect_vec().try_into().unwrap();
        let mut walk = Walk::default();
        walk.walk::<2>(Some(&example), exhausts);
        let mut problems = walk.problems;
        /// Marker present in `expect_missing` to say we *definitely* expect it
        #[derive(Debug, Copy, Clone)]
        struct DefinitelyRecognized;
        let expect_missing = declared_config_exceptions()
            .iter()
            .filter_map(|exc| {
                let definitely = match (exc.in_example(which), exc.in_code) {
                    (Present, _) => return None, // in file, don't expect "non-exhaustive" notice
                    (_, Some(false)) => return None, // code hasn't heard of it, likewise
                    (Absent, Some(true)) => Some(DefinitelyRecognized),
                    (Absent, None) => None, // allow this exception but don't mind if not known
                };
                Some((exc.key.clone(), definitely))
            })
            .collect_vec();
        dbg!(&expect_missing);
        // Things might appear in expect_missing for different reasons, and sometimes
        // at different levels.  For example, `bridges.transports` is expected to be
        // missing because we document that a different way in the example; but
        // `bridges` is expected to be missing from the OLDEST_SUPPORTED_CONFIG,
        // because that config predates bridge support.
        //
        // When this happens, we need to remove `bridges.transports` in favour of
        // the over-arching `bridges`.
        let expect_missing: Vec<(String, Option<DefinitelyRecognized>)> = expect_missing
            .iter()
            .cloned()
            .filter({
                let original: HashSet<_> = expect_missing.iter().map(|(k, _)| k.clone()).collect();
                move |(found, _)| {
                    !found
                        .match_indices('.')
                        .any(|(doti, _)| original.contains(&found[0..doti]))
                }
            })
            .collect_vec();
        dbg!(&expect_missing);
        for (exp, definitely) in expect_missing {
            let was = problems.len();
            problems.retain(|(path, _)| path != &exp);
            if problems.len() == was && definitely.is_some() {
                problems.push((exp, ProblemKind::UnusedException));
            }
        }
        let problems = problems
            .into_iter()
            .filter(|(key, _kind)| !deprecated.iter().any(|dep| key == dep))
            .map(|(path, m)| format!("    config key {:?}: {}", path, m))
            .collect_vec();
        // If this assert fails, it might be because in `fn exhaustive`, below,
        // a newly-defined config item has not been added to the list for OLDEST_SUPPORTED_CONFIG.
        assert!(
            problems.is_empty(),
            "example config {which:?} exhaustiveness check failed: {}\n-----8<-----\n{}\n-----8<-----\n",
            problems.join("\n"),
            example_file,
        );
    }
    #[test]
    fn exhaustive() {
        let mut deprecated = vec![];
        <(ArtiConfig, TorClientConfig) as tor_config::load::Resolvable>::enumerate_deprecated_keys(
            &mut |l| {
                for k in l {
                    deprecated.push(k.to_string());
                }
            },
        );
        let deprecated = deprecated.iter().cloned().collect_vec();
        // Check that:
        //  - The primary example config file has good examples for everything
        //  - Except for deprecated config keys
        //  - (And, except for those that we never expect: CONFIG_KEYS_EXPECT_NO_EXAMPLE.)
        exhaustive_1(ARTI_EXAMPLE_CONFIG, WhichExample::New, &deprecated);
        // Check that:
        //  - That oldest supported example config file has good examples for everything
        //  - Except for keys that we have introduced since that file was written
        //  - (And, except for those that we never expect: CONFIG_KEYS_EXPECT_NO_EXAMPLE.)
        // We *tolerate* entries in this table that don't actually occur in the oldest-supported
        // example.  This avoids having to feature-annotate them.
        exhaustive_1(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, &deprecated);
    }
    /// Check that the `Report` of `err` contains the string `exp`, and otherwise panic
    #[cfg_attr(feature = "pt-client", allow(dead_code))]
    fn expect_err_contains(err: ConfigResolveError, exp: &str) {
        use std::error::Error as StdError;
        let err: Box<dyn StdError> = Box::new(err);
        let err = tor_error::Report(err).to_string();
        assert!(
            err.contains(exp),
            "wrong message, got {:?}, exp {:?}",
            err,
            exp,
        );
    }
    #[test]
    fn bridges() {
        // We make assumptions about the contents of `arti-example-config.toml` !
        //
        // 1. There are nontrivial, non-default examples of `bridges.bridges`.
        // 2. These are in the `[bridges]` section, after a line `# For example:`
        // 3. There's precisely one ``` example, with conventional TOML formatting.
        // 4. There's precisely one [ ] example, with conventional TOML formatting.
        // 5. Both these examples specify the same set of bridges.
        // 6. There are three bridges.
        // 7. Lines starting with a digit or `[` are direct bridges; others are PT.
        //
        // Below, we annotate with `[1]` etc. where these assumptions are made.
        // Filter examples that we don't want to test in this configuration
        let filter_examples = |#[allow(unused_mut)] mut examples: ExampleSectionLines| -> _ {
            // [7], filter out the PTs
            if cfg!(all(feature = "bridge-client", not(feature = "pt-client"))) {
                let looks_like_addr =
                    |l: &str| l.starts_with(|c: char| c.is_ascii_digit() || c == '[');
                examples.lines.retain(|l| looks_like_addr(l));
            }
            examples
        };
        // Tests that one example parses, and returns what it parsed.
        // If bridge support is completely disabled, checks that this configuration
        // is rejected, as it should be, and returns a dummy value `((),)`
        // (so that the rest of the test has something to "compare that we parsed it the same").
        let resolve_examples = |examples: &ExampleSectionLines| {
            // [7], check that the PT bridge is properly rejected
            #[cfg(all(feature = "bridge-client", not(feature = "pt-client")))]
            {
                let err = examples.resolve::<TorClientConfig>().unwrap_err();
                expect_err_contains(err, "support disabled in cargo features");
            }
            let examples = filter_examples(examples.clone());
            #[cfg(feature = "bridge-client")]
            {
                examples.resolve::<TorClientConfig>().unwrap()
            }
            #[cfg(not(feature = "bridge-client"))]
            {
                let err = examples.resolve::<TorClientConfig>().unwrap_err();
                expect_err_contains(err, "support disabled in cargo features");
                // Use ((),) as the dummy unit value because () gives clippy conniptions
                ((),)
            }
        };
        // [1], [2], narrow to just the nontrivial, non-default, examples
        let mut examples = ExampleSectionLines::from_section("bridges");
        examples.narrow((r#"^# For example:"#, true), NARROW_NONE);
        let compare = {
            // [3], narrow to the multi-line string
            let mut examples = examples.clone();
            examples.narrow((r#"^#  bridges = '''"#, true), (r#"^#  '''"#, true));
            examples.uncomment();
            let parsed = resolve_examples(&examples);
            // Now we fish out the lines ourselves as a double-check
            // We must strip off the bridges = ''' and ''' lines.
            examples.lines.remove(0);
            examples.lines.remove(examples.lines.len() - 1);
            // [6], check we got the number of examples we expected
            examples.expect_lines(3);
            // If we have the bridge API, try parsing each line and using the API to insert it
            #[cfg(feature = "bridge-client")]
            {
                let examples = filter_examples(examples);
                let mut built = TorClientConfig::builder();
                for l in &examples.lines {
                    built.bridges().bridges().push(l.trim().parse().expect(l));
                }
                let built = built.build().unwrap();
                assert_eq!(&parsed, &built);
            }
            parsed
        };
        // [4], [5], narrow to the [ ] section, parse again, and compare
        {
            examples.narrow((r#"^#  bridges = \["#, true), (r#"^#  \]"#, true));
            examples.uncomment();
            let parsed = resolve_examples(&examples);
            assert_eq!(&parsed, &compare);
        }
    }
    #[test]
    fn transports() {
        // Extract and uncomment our transports lines.
        //
        // (They're everything from  `# An example managed pluggable transport`
        // through the start of the next
        // section.  They start with "#    ".)
        let mut file =
            ExampleSectionLines::from_markers("# An example managed pluggable transport", "[");
        file.lines.retain(|line| line.starts_with("#    "));
        file.uncomment();
        let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
        let cfg_got = result.unwrap();
        #[cfg(feature = "pt-client")]
        {
            use arti_client::config::{BridgesConfig, pt::TransportConfig};
            use tor_config_path::CfgPath;
            let bridges_got: &BridgesConfig = cfg_got.0.as_ref();
            // Build the expected configuration.
            let mut bld = BridgesConfig::builder();
            {
                let mut b = TransportConfig::builder();
                b.protocols(vec!["obfs4".parse().unwrap(), "obfs5".parse().unwrap()]);
                b.path(CfgPath::new("/usr/bin/obfsproxy".to_string()));
                b.arguments(vec!["-obfs4".to_string(), "-obfs5".to_string()]);
                b.run_on_startup(true);
                bld.transports().push(b);
            }
            {
                let mut b = TransportConfig::builder();
                b.protocols(vec!["obfs4".parse().unwrap()]);
                b.proxy_addr("127.0.0.1:31337".parse().unwrap());
                bld.transports().push(b);
            }
            let bridges_expected = bld.build().unwrap();
            assert_eq!(&bridges_expected, bridges_got);
        }
    }
    #[test]
    fn memquota() {
        // Test that uncommenting the example generates a config
        // with tracking enabled, iff support is compiled in.
        let mut file = ExampleSectionLines::from_section("system");
        file.lines.retain(|line| line.starts_with("#    memory."));
        file.uncomment();
        let result = file.resolve_return_results::<(TorClientConfig, ArtiConfig)>();
        let result = result.unwrap();
        // Test that the example config doesn't have any unrecognised keys
        assert_eq!(result.unrecognized, []);
        assert_eq!(result.deprecated, []);
        let inner: &tor_memquota::testing::ConfigInner =
            result.value.0.system_memory().inner().unwrap();
        // Test that the example low_water is the default
        // value for the example max.
        let defaulted_low = tor_memquota::Config::builder()
            .max(*inner.max)
            .build()
            .unwrap();
        let inner_defaulted_low = defaulted_low.inner().unwrap();
        assert_eq!(inner, inner_defaulted_low);
    }
    #[test]
    fn metrics() {
        // Test that uncommenting the example generates a config with prometheus enabled.
        let mut file = ExampleSectionLines::from_section("metrics");
        file.lines
            .retain(|line| line.starts_with("#    prometheus."));
        file.uncomment();
        let result = file
            .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
            .unwrap();
        // Test that the example config doesn't have any unrecognised keys
        assert_eq!(result.unrecognized, []);
        assert_eq!(result.deprecated, []);
        // Check that the example is as we expected
        assert_eq!(
            result
                .value
                .1
                .metrics
                .prometheus
                .listen
                .single_address_legacy()
                .unwrap(),
            Some("127.0.0.1:9035".parse().unwrap()),
        );
        // We don't test "compiled out but not used" here.
        // That case is handled in proxy.rs at startup time.
    }
    #[test]
    fn onion_services() {
        // Here we require that the onion services configuration is between a line labeled
        // with `##### ONION SERVICES` and a line labeled with `##### RPC`, and that each
        // line of _real_ configuration in that section begins with `#    `.
        let mut file = ExampleSectionLines::from_markers("##### ONION SERVICES", "##### RPC");
        file.lines.retain(|line| line.starts_with("#    "));
        file.uncomment();
        let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
        #[cfg(feature = "onion-service-service")]
        {
            let svc_expected = {
                use tor_hsrproxy::config::*;
                let mut b = OnionServiceProxyConfigBuilder::default();
                b.service().nickname("allium-cepa".parse().unwrap());
                b.proxy().proxy_ports().push(ProxyRule::new(
                    ProxyPattern::one_port(80).unwrap(),
                    ProxyAction::Forward(
                        Encapsulation::Simple,
                        TargetAddr::Inet("127.0.0.1:10080".parse().unwrap()),
                    ),
                ));
                b.proxy().proxy_ports().push(ProxyRule::new(
                    ProxyPattern::one_port(22).unwrap(),
                    ProxyAction::DestroyCircuit,
                ));
                b.proxy().proxy_ports().push(ProxyRule::new(
                    ProxyPattern::one_port(265).unwrap(),
                    ProxyAction::IgnoreStream,
                ));
                /* TODO (#1246)
                b.proxy().proxy_ports().push(ProxyRule::new(
                    ProxyPattern::port_range(1, 1024).unwrap(),
                    ProxyAction::Forward(
                        Encapsulation::Simple,
                        TargetAddr::Unix("/var/run/allium-cepa/socket".into()),
                    ),
                ));
                */
                b.proxy().proxy_ports().push(ProxyRule::new(
                    ProxyPattern::one_port(443).unwrap(),
                    ProxyAction::RejectStream,
                ));
                b.proxy().proxy_ports().push(ProxyRule::new(
                    ProxyPattern::all_ports(),
                    ProxyAction::DestroyCircuit,
                ));
                #[cfg(feature = "restricted-discovery")]
                {
                    const ALICE_KEY: &str =
                        "descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA";
                    const BOB_KEY: &str =
                        "descriptor:x25519:b5zqgtpermmuda6vc63lhjuf5ihpokjmuk26ly2xksf7vg52aesq";
                    for (nickname, key) in [("alice", ALICE_KEY), ("bob", BOB_KEY)] {
                        b.service()
                            .restricted_discovery()
                            .enabled(true)
                            .static_keys()
                            .access()
                            .push((
                                HsClientNickname::from_str(nickname).unwrap(),
                                HsClientDescEncKey::from_str(key).unwrap(),
                            ));
                    }
                    let mut dir = DirectoryKeyProviderBuilder::default();
                    dir.path(CfgPath::new(
                        "/var/lib/tor/hidden_service/authorized_clients".to_string(),
                    ));
                    b.service()
                        .restricted_discovery()
                        .key_dirs()
                        .access()
                        .push(dir);
                }
                b.build().unwrap()
            };
            cfg_if::cfg_if! {
                if #[cfg(feature = "restricted-discovery")] {
                    let cfg = result.unwrap();
                    let services = cfg.1.onion_services;
                    assert_eq!(services.len(), 1);
                    let svc = services.values().next().unwrap();
                    assert_eq!(svc, &svc_expected);
                } else {
                    expect_err_contains(
                        result.unwrap_err(),
                        "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
                    );
                }
            }
        }
        #[cfg(not(feature = "onion-service-service"))]
        {
            expect_err_contains(result.unwrap_err(), "not built with onion service support");
        }
    }
    #[cfg(feature = "rpc")]
    #[test]
    fn rpc_defaults() {
        let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
        // This will get us all the RPC entries that correspond to our defaults.
        //
        // The examples that _aren't_ in our defaults have '#      ' at the start.
        file.lines
            .retain(|line| line.starts_with("#    ") && !line.starts_with("#      "));
        file.uncomment();
        let parsed = file
            .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
            .unwrap();
        assert!(parsed.unrecognized.is_empty());
        assert!(parsed.deprecated.is_empty());
        let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
        let rpc_default = RpcConfig::default();
        assert_eq!(rpc_parsed, &rpc_default);
    }
    #[cfg(feature = "rpc")]
    #[test]
    fn rpc_full() {
        use crate::rpc::listener::{ConnectPointOptionsBuilder, RpcListenerSetConfigBuilder};
        // This will get us all the RPC entries, including those that _don't_ correspond to our defaults.
        let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
        // We skip the "file" item because it conflicts with "dir" and "file_options"
        file.lines
            .retain(|line| line.starts_with("#    ") && !line.contains("file ="));
        file.uncomment();
        let parsed = file
            .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
            .unwrap();
        let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
        let expected = {
            let mut bld_opts = ConnectPointOptionsBuilder::default();
            bld_opts.enable(false);
            let mut bld_set = RpcListenerSetConfigBuilder::default();
            bld_set.dir(CfgPath::new("${HOME}/.my_connect_files/".to_string()));
            bld_set.listener_options().enable(true);
            bld_set
                .file_options()
                .insert("bad_file.json".to_string(), bld_opts);
            let mut bld = RpcConfigBuilder::default();
            bld.listen().insert("label".to_string(), bld_set);
            bld.build().unwrap()
        };
        assert_eq!(&expected, rpc_parsed);
    }
    /// Helper for fishing out parts of the config file and uncommenting them.
    ///
    /// It represents a part of a configuration file.
    ///
    /// This can be used to find part of the config file by ad-hoc regexp matching,
    /// uncomment it, and parse it.  This is useful as part of a test to check
    /// that we can parse more complex config.
    #[derive(Debug, Clone)]
    struct ExampleSectionLines {
        /// The header for the section that we are parsing.  It is
        /// prepended to the lines before parsing them.
        section: String,
        /// The lines in the section.
        lines: Vec<String>,
    }
    /// A 2-tuple of a regular expression and a flag describing whether the line
    /// containing the expression should be included in the result of `narrow()`.
    type NarrowInstruction<'s> = (&'s str, bool);
    /// A NarrowInstruction that does not match anything.
    const NARROW_NONE: NarrowInstruction<'static> = ("?<none>", false);
    impl ExampleSectionLines {
        /// Construct a new `ExampleSectionLines` from `ARTI_EXAMPLE_CONFIG`, containing
        /// everything that starts with `[section]`, up to but not including the
        /// next line that begins with a `[`.
        fn from_section(section: &str) -> Self {
            Self::from_markers(format!("[{section}]"), "[")
        }
        /// Construct a new `ExampleSectionLines` from `ARTI_EXAMPLE_CONFIG`,
        /// containing everything that starts with `start`, up to but not
        /// including the next line that begins with `end`.
        ///
        /// If `start` is a configuration section header it will be put in the
        /// `section` field of the returned `ExampleSectionLines`, otherwise
        /// at the beginning of the `lines` field.
        ///
        /// `start` will be perceived as a configuration section header if it
        /// starts with `[` and ends with `]`.
        fn from_markers<S, E>(start: S, end: E) -> Self
        where
            S: AsRef<str>,
            E: AsRef<str>,
        {
            let (start, end) = (start.as_ref(), end.as_ref());
            let mut lines = ARTI_EXAMPLE_CONFIG
                .lines()
                .skip_while(|line| !line.starts_with(start))
                .peekable();
            let section = lines
                .next_if(|l0| l0.starts_with('['))
                .map(|section| section.to_owned())
                .unwrap_or_default();
            let lines = lines
                .take_while(|line| !line.starts_with(end))
                .map(|l| l.to_owned())
                .collect_vec();
            Self { section, lines }
        }
        /// Remove all lines from this section, except those between the (unique) line matching
        /// "start" and the next line matching "end" (or the end of the file).
        fn narrow(&mut self, start: NarrowInstruction, end: NarrowInstruction) {
            let find_index = |(re, include), start_pos, exactly_one: bool, adjust: [isize; 2]| {
                if (re, include) == NARROW_NONE {
                    return None;
                }
                let re = Regex::new(re).expect(re);
                let i = self
                    .lines
                    .iter()
                    .enumerate()
                    .skip(start_pos)
                    .filter(|(_, l)| re.is_match(l))
                    .map(|(i, _)| i);
                let i = if exactly_one {
                    i.clone().exactly_one().unwrap_or_else(|_| {
                        panic!("RE={:?} I={:#?} L={:#?}", re, i.collect_vec(), self.lines)
                    })
                } else {
                    i.clone().next()?
                };
                let adjust = adjust[usize::from(include)];
                let i = (i as isize + adjust) as usize;
                Some(i)
            };
            eprint!("narrow {:?} {:?}: ", start, end);
            let start = find_index(start, 0, true, [1, 0]).unwrap_or(0);
            let end = find_index(end, start + 1, false, [0, 1]).unwrap_or(self.lines.len());
            eprintln!("{:?} {:?}", start, end);
            // don't tolerate empty
            assert!(start < end, "empty, from {:#?}", self.lines);
            self.lines = self.lines.drain(..).take(end).skip(start).collect_vec();
        }
        /// Assert that this section contains exactly `n` lines.
        fn expect_lines(&self, n: usize) {
            assert_eq!(self.lines.len(), n);
        }
        /// Remove `#` from the start of every line that begins with it.
        fn uncomment(&mut self) {
            self.strip_prefix("#");
        }
        /// Remove `prefix` from the start of every line.
        ///
        /// If there are lines that *don't* start with `prefix`, crash.
        ///
        /// But, lines starting with `[` are left unchanged, in any case.
        /// (These are TOML section markers; changing them would change the TOML structure.)
        fn strip_prefix(&mut self, prefix: &str) {
            for l in &mut self.lines {
                if !l.starts_with('[') {
                    *l = l.strip_prefix(prefix).expect(l).to_string();
                }
            }
        }
        /// Join the parts of this object together into a single string.
        fn build_string(&self) -> String {
            chain!(iter::once(&self.section), self.lines.iter(),).join("\n")
        }
        /// Make a TOML document of this section and parse it as a complete configuration.
        /// Panic if the section cannot be parsed.
        fn parse(&self) -> tor_config::ConfigurationTree {
            let s = self.build_string();
            eprintln!("parsing\n  --\n{}\n  --", s);
            let mut sources = tor_config::ConfigurationSources::new_empty();
            sources.push_source(
                tor_config::ConfigurationSource::from_verbatim(s.clone()),
                tor_config::sources::MustRead::MustRead,
            );
            sources.load().expect(&s)
        }
        fn resolve<R: tor_config::load::Resolvable>(&self) -> Result<R, ConfigResolveError> {
            tor_config::load::resolve(self.parse())
        }
        fn resolve_return_results<R: tor_config::load::Resolvable>(
            &self,
        ) -> Result<ResolutionResults<R>, ConfigResolveError> {
            tor_config::load::resolve_return_results(self.parse(), &Default::default())
        }
    }
    // More normal config tests
    #[test]
    fn builder() {
        use tor_config_path::CfgPath;
        let sec = std::time::Duration::from_secs(1);
        let mut authorities = dir::AuthorityContacts::builder();
        authorities.v3idents().push([22; 20].into());
        let mut fallback = dir::FallbackDir::builder();
        fallback
            .rsa_identity([23; 20].into())
            .ed_identity([99; 32].into())
            .orports()
            .push("127.0.0.7:7".parse().unwrap());
        let mut bld = ArtiConfig::builder();
        let mut bld_tor = TorClientConfig::builder();
        bld.proxy().socks_listen(Listen::new_localhost(9999));
        bld.logging().console("warn");
        *bld_tor.tor_network().authorities() = authorities;
        bld_tor.tor_network().set_fallback_caches(vec![fallback]);
        bld_tor
            .storage()
            .cache_dir(CfgPath::new("/var/tmp/foo".to_owned()))
            .state_dir(CfgPath::new("/var/tmp/bar".to_owned()));
        bld_tor.download_schedule().retry_certs().attempts(10);
        bld_tor.download_schedule().retry_certs().initial_delay(sec);
        bld_tor.download_schedule().retry_certs().parallelism(3);
        bld_tor.download_schedule().retry_microdescs().attempts(30);
        bld_tor
            .download_schedule()
            .retry_microdescs()
            .initial_delay(10 * sec);
        bld_tor
            .download_schedule()
            .retry_microdescs()
            .parallelism(9);
        bld_tor
            .override_net_params()
            .insert("wombats-per-quokka".to_owned(), 7);
        bld_tor
            .path_rules()
            .ipv4_subnet_family_prefix(20)
            .ipv6_subnet_family_prefix(48);
        bld_tor.preemptive_circuits().disable_at_threshold(12);
        bld_tor
            .preemptive_circuits()
            .set_initial_predicted_ports(vec![80, 443]);
        bld_tor
            .preemptive_circuits()
            .prediction_lifetime(Duration::from_secs(3600))
            .min_exit_circs_for_port(2);
        bld_tor
            .circuit_timing()
            .max_dirtiness(90 * sec)
            .request_timeout(10 * sec)
            .request_max_retries(22)
            .request_loyalty(3600 * sec);
        bld_tor.address_filter().allow_local_addrs(true);
        let val = bld.build().unwrap();
        assert_ne!(val, ArtiConfig::default());
    }
    #[test]
    fn articonfig_application() {
        let config = ArtiConfig::default();
        let application = config.application();
        assert_eq!(&config.application, application);
    }
    #[test]
    fn articonfig_logging() {
        let config = ArtiConfig::default();
        let logging = config.logging();
        assert_eq!(&config.logging, logging);
    }
    #[test]
    fn articonfig_proxy() {
        let config = ArtiConfig::default();
        let proxy = config.proxy();
        assert_eq!(&config.proxy, proxy);
    }
    /// Comprehensive tests for `proxy.socks_listen` and `proxy.dns_listen`.
    ///
    /// The "this isn't set at all, just use the default" cases are tested elsewhere.
    fn ports_listen(
        f: &str,
        get_listen: &dyn Fn(&ArtiConfig) -> &Listen,
        bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option<Listen>,
        setter_listen: &dyn Fn(&mut ArtiConfigBuilder, Listen) -> &mut ProxyConfigBuilder,
    ) {
        let from_toml = |s: &str| -> ArtiConfigBuilder {
            let cfg: toml::Value = toml::from_str(dbg!(s)).unwrap();
            let cfg: ArtiConfigBuilder = cfg.try_into().unwrap();
            cfg
        };
        let chk = |cfg: &ArtiConfigBuilder, expected: &Listen| {
            dbg!(bld_get_listen(cfg));
            let cfg = cfg.build().unwrap();
            assert_eq!(get_listen(&cfg), expected);
        };
        let check_setters = |port, expected: &_| {
            let cfg = ArtiConfig::builder();
            for listen in match port {
                None => vec![Listen::new_none(), Listen::new_localhost(0)],
                Some(port) => vec![Listen::new_localhost(port)],
            } {
                let mut cfg = cfg.clone();
                setter_listen(&mut cfg, dbg!(listen));
                chk(&cfg, expected);
            }
        };
        {
            let expected = Listen::new_localhost(100);
            let cfg = from_toml(&format!("proxy.{}_listen = 100", f));
            assert_eq!(bld_get_listen(&cfg), &Some(Listen::new_localhost(100)));
            chk(&cfg, &expected);
            check_setters(Some(100), &expected);
        }
        {
            let expected = Listen::new_none();
            let cfg = from_toml(&format!("proxy.{}_listen = 0", f));
            chk(&cfg, &expected);
            check_setters(None, &expected);
        }
    }
    #[test]
    fn ports_listen_socks() {
        ports_listen(
            "socks",
            &|cfg| &cfg.proxy.socks_listen,
            &|bld| &bld.proxy.socks_listen,
            &|bld, arg| bld.proxy.socks_listen(arg),
        );
    }
    #[test]
    fn ports_listen_dns() {
        ports_listen(
            "dns",
            &|cfg| &cfg.proxy.dns_listen,
            &|bld| &bld.proxy.dns_listen,
            &|bld, arg| bld.proxy.dns_listen(arg),
        );
    }
}