Skip to main content

arti/
cfg.rs

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
5use derive_builder::Builder;
6use serde::{Deserialize, Serialize};
7use tor_config_path::CfgPath;
8
9#[cfg(feature = "onion-service-service")]
10use crate::onion_proxy::{
11    OnionServiceProxyConfigBuilder, OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder,
12};
13#[cfg(not(feature = "onion-service-service"))]
14use crate::onion_proxy_disabled::{OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder};
15#[cfg(feature = "rpc")]
16pub use crate::rpc::{RpcConfig, RpcConfigBuilder};
17use arti_client::TorClientConfig;
18#[cfg(feature = "onion-service-service")]
19use tor_config::define_list_builder_accessors;
20pub(crate) use tor_config::{ConfigBuildError, Listen, impl_standard_builder};
21
22use crate::{LoggingConfig, LoggingConfigBuilder};
23
24/// Example file demonstrating our configuration and the default options.
25///
26/// The options in this example file are all commented out;
27/// the actual defaults are done via builder attributes in all the Rust config structs.
28#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
29pub(crate) const ARTI_EXAMPLE_CONFIG: &str = concat!(include_str!("./arti-example-config.toml"));
30
31/// Test case file for the oldest version of the config we still support.
32///
33/// (When updating, copy `arti-example-config.toml` from the earliest version we want to
34/// be compatible with.)
35//
36// Probably, in the long run, we will want to make this architecture more general: we'll want
37// to have a larger number of examples to test, and we won't want to write a separate constant
38// for each. Probably in that case, we'll want a directory of test examples, and we'll want to
39// traverse the whole directory.
40//
41// Compare C tor, look at conf_examples and conf_failures - each of the subdirectories there is
42// an example configuration situation that we wanted to validate.
43//
44// NB here in Arti the OLDEST_SUPPORTED_CONFIG and the ARTI_EXAMPLE_CONFIG are tested
45// somewhat differently: we test that the current example is *exhaustive*, not just
46// parsable.
47#[cfg(test)]
48const OLDEST_SUPPORTED_CONFIG: &str = concat!(include_str!("./oldest-supported-config.toml"),);
49
50/// Structure to hold our application configuration options
51#[derive(Debug, Clone, Builder, Eq, PartialEq)]
52#[builder(build_fn(error = "ConfigBuildError"))]
53#[builder(derive(Debug, Serialize, Deserialize))]
54#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
55#[cfg_attr(feature = "experimental-api", builder(public))]
56pub(crate) struct ApplicationConfig {
57    /// If true, we should watch our configuration files for changes, and reload
58    /// our configuration when they change.
59    ///
60    /// Note that this feature may behave in unexpected ways if the path to the
61    /// directory holding our configuration files changes its identity (because
62    /// an intermediate symlink is changed, because the directory is removed and
63    /// recreated, or for some other reason).
64    #[builder(default)]
65    pub(crate) watch_configuration: bool,
66
67    /// If true, we should allow other applications not owned by the system
68    /// administrator to monitor the Arti application and inspect its memory.
69    ///
70    /// Otherwise, we take various steps (including disabling core dumps) to
71    /// make it harder for other programs to view our internal state.
72    ///
73    /// This option has no effect when arti is built without the `harden`
74    /// feature.  When `harden` is not enabled, debugger attachment is permitted
75    /// whether this option is set or not.
76    #[builder(default)]
77    pub(crate) permit_debugging: bool,
78
79    /// If true, then we do not exit when we are running as `root`.
80    ///
81    /// This has no effect on Windows.
82    #[builder(default)]
83    pub(crate) allow_running_as_root: bool,
84}
85impl_standard_builder! { ApplicationConfig }
86
87/// Configuration for one or more proxy listeners.
88#[derive(Debug, Clone, Builder, Eq, PartialEq)]
89#[builder(build_fn(error = "ConfigBuildError"))]
90#[builder(derive(Debug, Serialize, Deserialize))]
91#[allow(clippy::option_option)] // Builder port fields: Some(None) = specified to disable
92#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
93#[cfg_attr(feature = "experimental-api", builder(public))]
94pub(crate) struct ProxyConfig {
95    /// Addresses to listen on for incoming SOCKS connections.
96    //
97    // TODO: Once http-connect is non-experimental, we should rename this option in a backward-compatible way.
98    #[builder(default = "Listen::new_localhost(9150)")]
99    pub(crate) socks_listen: Listen,
100
101    /// Addresses to listen on for incoming DNS connections.
102    #[builder(default = "Listen::new_none()")]
103    pub(crate) dns_listen: Listen,
104}
105impl_standard_builder! { ProxyConfig }
106
107/// Configuration for arti-specific storage locations.
108///
109/// See also [`arti_client::config::StorageConfig`].
110#[derive(Debug, Clone, Builder, Eq, PartialEq)]
111#[builder(build_fn(error = "ConfigBuildError"))]
112#[builder(derive(Debug, Serialize, Deserialize))]
113#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
114#[cfg_attr(feature = "experimental-api", builder(public))]
115pub(crate) struct ArtiStorageConfig {
116    /// A file in which to write information about the ports we're listening on.
117    #[builder(setter(into), default = "default_port_info_file()")]
118    pub(crate) port_info_file: CfgPath,
119}
120impl_standard_builder! { ArtiStorageConfig }
121
122/// Return the default ports_info_file location.
123fn default_port_info_file() -> CfgPath {
124    CfgPath::new("${ARTI_LOCAL_DATA}/public/port_info.json".to_owned())
125}
126
127/// Configuration for system resources used by Tor.
128///
129/// You cannot change *these variables* in this section on a running Arti client.
130///
131/// Note that there are other settings in this section,
132/// in [`arti_client::config::SystemConfig`].
133//
134// These two structs exist because:
135//
136//  1. Our doctrine is that configuration structs live with the code that uses the info.
137//  2. tor-memquota's configuration is used by the MemoryQuotaTracker in TorClient
138//  3. File descriptor limits are enforced here in arti because it's done process-global
139//  4. Nevertheless, logically, these things want to be in the same section of the file.
140#[derive(Debug, Clone, Builder, Eq, PartialEq)]
141#[builder(build_fn(error = "ConfigBuildError"))]
142#[builder(derive(Debug, Serialize, Deserialize))]
143#[non_exhaustive]
144#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
145#[cfg_attr(feature = "experimental-api", builder(public))]
146pub(crate) struct SystemConfig {
147    /// Maximum number of file descriptors we should launch with
148    #[builder(setter(into), default = "default_max_files()")]
149    pub(crate) max_files: u64,
150}
151impl_standard_builder! { SystemConfig }
152
153/// Return the default maximum number of file descriptors to launch with.
154fn default_max_files() -> u64 {
155    16384
156}
157
158/// Structure to hold Arti's configuration options, whether from a
159/// configuration file or the command line.
160//
161/// These options are declared in a public crate outside of `arti` so that other
162/// applications can parse and use them, if desired.  If you're only embedding
163/// arti via `arti-client`, and you don't want to use Arti's configuration
164/// format, use [`arti_client::TorClientConfig`] instead.
165///
166/// By default, Arti will run using the default Tor network, store state and
167/// cache information to a per-user set of directories shared by all
168/// that user's applications, and run a SOCKS client on a local port.
169///
170/// NOTE: These are NOT the final options or their final layout. Expect NO
171/// stability here.
172#[derive(Debug, Builder, Clone, Eq, PartialEq)]
173#[builder(derive(Serialize, Deserialize, Debug))]
174#[builder(build_fn(private, name = "build_unvalidated", error = "ConfigBuildError"))]
175#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
176#[cfg_attr(feature = "experimental-api", builder(public))]
177pub(crate) struct ArtiConfig {
178    /// Configuration for application behavior.
179    #[builder(sub_builder(fn_name = "build"))]
180    #[builder_field_attr(serde(default))]
181    application: ApplicationConfig,
182
183    /// Configuration for proxy listeners
184    #[builder(sub_builder(fn_name = "build"))]
185    #[builder_field_attr(serde(default))]
186    proxy: ProxyConfig,
187
188    /// Logging configuration
189    #[builder(sub_builder(fn_name = "build"))]
190    #[builder_field_attr(serde(default))]
191    logging: LoggingConfig,
192
193    /// Metrics configuration
194    #[builder(sub_builder(fn_name = "build"))]
195    #[builder_field_attr(serde(default))]
196    pub(crate) metrics: MetricsConfig,
197
198    /// Configuration for RPC subsystem
199    #[cfg(feature = "rpc")]
200    #[builder(sub_builder(fn_name = "build"))]
201    #[builder_field_attr(serde(default))]
202    pub(crate) rpc: RpcConfig,
203
204    /// Configuration for the RPC subsystem (disabled)
205    //
206    // This set of options allows us to detect and warn
207    // when anything is set under "rpc" in the config.
208    //
209    // The incantations are a bit subtle: we use an Option<toml::Value> in the builder,
210    // to ensure that our configuration will continue to round-trip thorough serde.
211    // We use () in the configuration type, since toml::Value isn't Eq,
212    // and since we don't want to expose whatever spurious options were in the config.
213    // We use builder(private), since using builder(setter(skip))
214    // would (apparently) override the type of the field in builder and make it a PhantomData.
215    #[cfg(not(feature = "rpc"))]
216    #[builder_field_attr(serde(default))]
217    #[builder(field(type = "Option<toml::Value>", build = "()"), private)]
218    rpc: (),
219
220    /// Information on system resources used by Arti.
221    ///
222    /// Note that there are other settings in this section,
223    /// in [`arti_client::config::SystemConfig`] -
224    /// these two structs overlay here.
225    #[builder(sub_builder(fn_name = "build"))]
226    #[builder_field_attr(serde(default))]
227    pub(crate) system: SystemConfig,
228
229    /// Information on where things are stored by Arti.
230    ///
231    /// Note that [`TorClientConfig`] also has a storage configuration;
232    /// our configuration logic should merge them correctly.
233    #[builder(sub_builder(fn_name = "build"))]
234    #[builder_field_attr(serde(default))]
235    pub(crate) storage: ArtiStorageConfig,
236
237    /// Configured list of proxied onion services.
238    ///
239    /// Note that this field is present unconditionally, but when onion service
240    /// support is disabled, it is replaced with a stub type from
241    /// `onion_proxy_disabled`, and its setter functions are not implemented.
242    /// The purpose of this stub type is to give an error if somebody tries to
243    /// configure onion services when the `onion-service-service` feature is
244    /// disabled.
245    #[builder(sub_builder(fn_name = "build"), setter(custom))]
246    #[builder_field_attr(serde(default))]
247    pub(crate) onion_services: OnionServiceProxyConfigMap,
248}
249
250impl_standard_builder! { ArtiConfig }
251
252impl ArtiConfigBuilder {
253    /// Build the [`ArtiConfig`].
254    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
255    pub(crate) fn build(&self) -> Result<ArtiConfig, ConfigBuildError> {
256        #[cfg_attr(not(feature = "onion-service-service"), allow(unused_mut))]
257        let mut config = self.build_unvalidated()?;
258        #[cfg(feature = "onion-service-service")]
259        for svc in config.onion_services.values_mut() {
260            // Pass the application-level watch_configuration to each restricted discovery config.
261            *svc.svc_cfg
262                .restricted_discovery_mut()
263                .watch_configuration_mut() = config.application.watch_configuration;
264        }
265
266        #[cfg(not(feature = "rpc"))]
267        if self.rpc.is_some() {
268            tracing::warn!("rpc options were set, but Arti was built without support for rpc.");
269        }
270
271        Ok(config)
272    }
273}
274
275impl tor_config::load::TopLevel for ArtiConfig {
276    type Builder = ArtiConfigBuilder;
277    // Some config options such as "proxy.socks_port" are no longer
278    // just "deprecated" and have since been completely removed from Arti,
279    // but there's no harm in informing the user that the options are still deprecated.
280    // For these removed options, Arti will ignore them like it does for all unknown options.
281    const DEPRECATED_KEYS: &'static [&'static str] = &["proxy.socks_port", "proxy.dns_port"];
282}
283
284#[cfg(feature = "onion-service-service")]
285define_list_builder_accessors! {
286    struct ArtiConfigBuilder {
287        pub(crate) onion_services: [OnionServiceProxyConfigBuilder],
288    }
289}
290
291/// Convenience alias for the config for a whole `arti` program
292///
293/// Used primarily as a type parameter on calls to [`tor_config::resolve`]
294#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
295pub(crate) type ArtiCombinedConfig = (ArtiConfig, TorClientConfig);
296
297/// Configuration for exporting metrics (eg, perf data)
298#[derive(Debug, Clone, Builder, Eq, PartialEq)]
299#[builder(build_fn(error = "ConfigBuildError"))]
300#[builder(derive(Debug, Serialize, Deserialize))]
301#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
302#[cfg_attr(feature = "experimental-api", builder(public))]
303pub(crate) struct MetricsConfig {
304    /// Where to listen for incoming HTTP connections.
305    #[builder(sub_builder(fn_name = "build"))]
306    #[builder_field_attr(serde(default))]
307    pub(crate) prometheus: PrometheusConfig,
308}
309impl_standard_builder! { MetricsConfig }
310
311/// Configuration for one or more proxy listeners.
312#[derive(Debug, Clone, Builder, Eq, PartialEq)]
313#[builder(build_fn(error = "ConfigBuildError"))]
314#[builder(derive(Debug, Serialize, Deserialize))]
315#[allow(clippy::option_option)] // Builder port fields: Some(None) = specified to disable
316#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
317#[cfg_attr(feature = "experimental-api", builder(public))]
318pub(crate) struct PrometheusConfig {
319    /// Port on which to establish a Prometheus scrape endpoint
320    ///
321    /// We listen here for incoming HTTP connections.
322    ///
323    /// If just a port is provided, we don't support IPv6.
324    /// Alternatively, (only) a single address and port can be specified.
325    /// These restrictions are due to upstream limitations:
326    /// <https://github.com/metrics-rs/metrics/issues/567>.
327    #[builder(default)]
328    #[builder_field_attr(serde(default))]
329    pub(crate) listen: Listen,
330}
331impl_standard_builder! { PrometheusConfig }
332
333impl ArtiConfig {
334    /// Return the [`ApplicationConfig`] for this configuration.
335    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
336    pub(crate) fn application(&self) -> &ApplicationConfig {
337        &self.application
338    }
339
340    /// Return the [`LoggingConfig`] for this configuration.
341    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
342    pub(crate) fn logging(&self) -> &LoggingConfig {
343        &self.logging
344    }
345
346    /// Return the [`ProxyConfig`] for this configuration.
347    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
348    pub(crate) fn proxy(&self) -> &ProxyConfig {
349        &self.proxy
350    }
351
352    /// Return the [`ArtiStorageConfig`] for this configuration.
353    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
354    ///
355    pub(crate) fn storage(&self) -> &ArtiStorageConfig {
356        &self.storage
357    }
358
359    /// Return the [`RpcConfig`] for this configuration.
360    #[cfg(feature = "rpc")]
361    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
362    pub(crate) fn rpc(&self) -> &RpcConfig {
363        &self.rpc
364    }
365}
366
367#[cfg(test)]
368mod test {
369    // @@ begin test lint list maintained by maint/add_warning @@
370    #![allow(clippy::bool_assert_comparison)]
371    #![allow(clippy::clone_on_copy)]
372    #![allow(clippy::dbg_macro)]
373    #![allow(clippy::mixed_attributes_style)]
374    #![allow(clippy::print_stderr)]
375    #![allow(clippy::print_stdout)]
376    #![allow(clippy::single_char_pattern)]
377    #![allow(clippy::unwrap_used)]
378    #![allow(clippy::unchecked_time_subtraction)]
379    #![allow(clippy::useless_vec)]
380    #![allow(clippy::needless_pass_by_value)]
381    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
382    // TODO add this next lint to maint/add_warning, for all tests
383    #![allow(clippy::iter_overeager_cloned)]
384    // Saves adding many individual #[cfg], or a sub-module
385    #![cfg_attr(not(feature = "pt-client"), allow(dead_code))]
386
387    use arti_client::config::TorClientConfigBuilder;
388    use arti_client::config::dir;
389    use itertools::{EitherOrBoth, Itertools, chain};
390    use regex::Regex;
391    use std::collections::HashSet;
392    use std::fmt::Write as _;
393    use std::iter;
394    use std::time::Duration;
395    use tor_config::load::{ConfigResolveError, ResolutionResults};
396    use tor_config_path::CfgPath;
397
398    #[allow(unused_imports)] // depends on features
399    use tor_error::ErrorReport as _;
400
401    #[cfg(feature = "restricted-discovery")]
402    use {
403        arti_client::HsClientDescEncKey,
404        std::str::FromStr as _,
405        tor_hsservice::config::restricted_discovery::{
406            DirectoryKeyProviderBuilder, HsClientNickname,
407        },
408    };
409
410    use super::*;
411
412    //---------- tests that rely on the provided example config file ----------
413    //
414    // These are quite complex.  They uncomment the file, parse bits of it,
415    // and do tests via serde and via the normal config machinery,
416    // to see that everything is documented as expected.
417
418    fn uncomment_example_settings(template: &str) -> String {
419        let re = Regex::new(r#"(?m)^\#([^ \n])"#).unwrap();
420        re.replace_all(template, |cap: &regex::Captures<'_>| -> _ {
421            cap.get(1).unwrap().as_str().to_string()
422        })
423        .into()
424    }
425
426    /// Is this key present or absent in the examples in one of the example files ?
427    ///
428    /// Depending on which variable this is in, it refers to presence in other the
429    /// old or the new example file.
430    ///
431    /// This type is *not* used in declarations in `declared_config_exceptions`;
432    /// it is used by the actual checking code.
433    /// The declarations use types in that function.
434    #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
435    enum InExample {
436        Absent,
437        Present,
438    }
439    /// Which of the two example files?
440    ///
441    /// This type is *not* used in declarations in `declared_config_exceptions`;
442    /// it is used by the actual checking code.
443    /// The declarations use types in that function.
444    #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
445    enum WhichExample {
446        Old,
447        New,
448    }
449    /// An exception to the usual expectations about configuration example files
450    ///
451    /// This type is *not* used in declarations in `declared_config_exceptions`;
452    /// it is used by the actual checking code.
453    /// The declarations use types in that function.
454    #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
455    struct ConfigException {
456        /// The actual config key
457        key: String,
458        /// Does it appear in the oldest supported example file?
459        in_old_example: InExample,
460        /// Does it appear in the current example file?
461        in_new_example: InExample,
462        /// Does our code recognise it ?  `None` means "don't know"
463        in_code: Option<bool>,
464    }
465    impl ConfigException {
466        fn in_example(&self, which: WhichExample) -> InExample {
467            use WhichExample::*;
468            match which {
469                Old => self.in_old_example,
470                New => self.in_new_example,
471            }
472        }
473    }
474
475    /// *every* feature that's listed as `InCode::FeatureDependent`
476    const ALL_RELEVANT_FEATURES_ENABLED: bool = cfg!(all(
477        feature = "bridge-client",
478        feature = "pt-client",
479        feature = "onion-service-client",
480        feature = "rpc",
481    ));
482
483    /// Return the expected exceptions to the usual expectations about config and examples
484    fn declared_config_exceptions() -> Vec<ConfigException> {
485        /// Is this key recognised by the parsing code ?
486        ///
487        /// (This can be feature-dependent, so literal values of this type
488        /// are often feature-qualified.)
489        #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
490        enum InCode {
491            /// No configuration of this codebase knows about this option
492            Ignored,
493            /// *Some* configuration of this codebase know about this option
494            ///
495            /// This means:
496            ///   - If *every* feature in `ALL_RELEVANT_FEATURES_ENABLED` is enabled,
497            ///     the config key is expected to be `Recognised`
498            ///   - Otherwise we're not sure (because cargo features are additive,
499            ///     dependency crates' features might be *en*abled willy-nilly).
500            FeatureDependent,
501            /// All configurations of this codebase know about this option
502            Recognized,
503        }
504        use InCode::*;
505
506        /// Marker.  `Some(InOld)` means presence of this config key in the oldest-supported file
507        struct InOld;
508        /// Marker.  `Some(InNew)` means presence of this config key in the current example file
509        struct InNew;
510
511        let mut out = vec![];
512
513        // Declare some keys which aren't "normal", eg they aren't documented in the usual
514        // way, are configurable, aren't in the oldest supported file, etc.
515        //
516        // `in_old_example` and `in_new_example` are whether the key appears in
517        // `arti-example-config.toml` and `oldest-supported-config.toml` respectively.
518        // (in each case, only a line like `#example.key = ...` counts.)
519        //
520        // `whether_supported` tells is if the key is supposed to be
521        // recognised by the code.
522        //
523        // `keys` is the list of keys.  Add a // comment at the start of the list
524        // so that rustfmt retains the consistent formatting.
525        let mut declare_exceptions = |in_old_example: Option<InOld>,
526                                      in_new_example: Option<InNew>,
527                                      in_code: InCode,
528                                      keys: &[&str]| {
529            let in_code = match in_code {
530                Ignored => Some(false),
531                Recognized => Some(true),
532                FeatureDependent if ALL_RELEVANT_FEATURES_ENABLED => Some(true),
533                FeatureDependent => None,
534            };
535            #[allow(clippy::needless_pass_by_value)] // pass by value defends against a->a b->a
536            fn in_example<T>(spec: Option<T>) -> InExample {
537                match spec {
538                    None => InExample::Absent,
539                    Some(_) => InExample::Present,
540                }
541            }
542            let in_old_example = in_example(in_old_example);
543            let in_new_example = in_example(in_new_example);
544            out.extend(keys.iter().cloned().map(|key| ConfigException {
545                key: key.to_owned(),
546                in_old_example,
547                in_new_example,
548                in_code,
549            }));
550        };
551
552        declare_exceptions(
553            None,
554            Some(InNew),
555            Recognized,
556            &[
557                // Keys that are newer than the oldest-supported example, but otherwise normal.
558                "application.allow_running_as_root",
559                "bridges",
560                "logging.time_granularity",
561                "path_rules.long_lived_ports",
562                "use_obsolete_software",
563                "circuit_timing.disused_circuit_timeout",
564                "storage.port_info_file",
565            ],
566        );
567
568        declare_exceptions(
569            None,
570            None,
571            Recognized,
572            &[
573                // Examples exist but are not auto-testable
574                "tor_network.authorities",
575                "tor_network.fallback_caches",
576            ],
577        );
578
579        declare_exceptions(
580            None,
581            None,
582            Recognized,
583            &[
584                // Examples exist but are not auto-testable
585                "logging.opentelemetry",
586            ],
587        );
588
589        declare_exceptions(
590            Some(InOld),
591            Some(InNew),
592            if cfg!(target_family = "windows") {
593                Ignored
594            } else {
595                Recognized
596            },
597            &[
598                // Unix-only mistrust settings
599                "storage.permissions.trust_group",
600                "storage.permissions.trust_user",
601            ],
602        );
603
604        declare_exceptions(
605            None,
606            None, // TODO: Make examples for bridges settings!
607            FeatureDependent,
608            &[
609                // Settings only available with bridge support
610                "bridges.transports", // we recognise this so we can reject it
611            ],
612        );
613
614        declare_exceptions(
615            None,
616            Some(InNew),
617            FeatureDependent,
618            &[
619                // Settings only available with experimental-api support
620                "storage.keystore",
621            ],
622        );
623
624        declare_exceptions(
625            None,
626            None, // it's there, but not formatted for auto-testing
627            FeatureDependent,
628            &[
629                // Settings only available with tokio-console support
630                "logging.tokio_console",
631                "logging.tokio_console.enabled",
632            ],
633        );
634
635        declare_exceptions(
636            None,
637            None, // it's there, but not formatted for auto-testing
638            Recognized,
639            &[
640                // Memory quota, tested by fn memquota (below)
641                "system.memory",
642                "system.memory.max",
643                "system.memory.low_water",
644            ],
645        );
646
647        declare_exceptions(
648            None,
649            Some(InNew), // The top-level section is in the new file (only).
650            Recognized,
651            &["metrics"],
652        );
653
654        declare_exceptions(
655            None,
656            None, // The inner information is not formatted for auto-testing
657            Recognized,
658            &[
659                // Prometheus metrics exporter, tested by fn metrics (below)
660                "metrics.prometheus",
661                "metrics.prometheus.listen",
662            ],
663        );
664
665        declare_exceptions(
666            None,
667            Some(InNew),
668            FeatureDependent,
669            &[
670                // PT-only settings
671            ],
672        );
673
674        declare_exceptions(
675            None,
676            Some(InNew),
677            FeatureDependent,
678            &[
679                // HS client settings
680                "address_filter.allow_onion_addrs",
681                "circuit_timing.hs_desc_fetch_attempts",
682                "circuit_timing.hs_intro_rend_attempts",
683            ],
684        );
685
686        declare_exceptions(
687            None,
688            None, // TODO RPC, these should actually appear in the example config
689            FeatureDependent,
690            &[
691                // RPC-only settings
692                "rpc",
693                "rpc.rpc_listen",
694            ],
695        );
696
697        // These are commented-out by default, and tested with test::onion_services().
698        declare_exceptions(
699            None,
700            None,
701            FeatureDependent,
702            &[
703                // onion-service only settings.
704                "onion_services",
705            ],
706        );
707
708        declare_exceptions(
709            None,
710            Some(InNew),
711            FeatureDependent,
712            &[
713                // Vanguards-specific settings
714                "vanguards",
715                "vanguards.mode",
716            ],
717        );
718
719        // These are commented-out by default
720        declare_exceptions(
721            None,
722            None,
723            FeatureDependent,
724            &[
725                "storage.keystore.ctor",
726                "storage.keystore.ctor.services",
727                "storage.keystore.ctor.clients",
728            ],
729        );
730
731        out.sort();
732
733        let dupes = out.iter().map(|exc| &exc.key).duplicates().collect_vec();
734        assert!(
735            dupes.is_empty(),
736            "duplicate exceptions in configuration {dupes:?}"
737        );
738
739        eprintln!(
740            "declared config exceptions for this configuration:\n{:#?}",
741            &out
742        );
743        out
744    }
745
746    #[test]
747    fn default_config() {
748        use InExample::*;
749
750        let empty_config = tor_config::ConfigurationSources::new_empty()
751            .load()
752            .unwrap();
753        let empty_config: ArtiCombinedConfig = tor_config::resolve(empty_config).unwrap();
754
755        let default = (ArtiConfig::default(), TorClientConfig::default());
756        let exceptions = declared_config_exceptions();
757
758        /// Helper to decide what to do about a possible discrepancy
759        ///
760        /// Provided with `EitherOrBoth` of:
761        ///   - the config key that the config parser reported it found, but didn't recognise
762        ///   - the declared exception entry
763        ///     (for the same config key)
764        ///
765        /// Decides whether this is something that should fail the test.
766        /// If so it returns `Err((key, error_message))`, otherwise `Ok`.
767        #[allow(clippy::needless_pass_by_value)] // clippy is IMO wrong about eob
768        fn analyse_joined_info(
769            which: WhichExample,
770            uncommented: bool,
771            eob: EitherOrBoth<&String, &ConfigException>,
772        ) -> Result<(), (String, String)> {
773            use EitherOrBoth::*;
774            let (key, err) = match eob {
775                // Unrecognised entry, no exception
776                Left(found) => (found, "found in example but not processed".into()),
777                Both(found, exc) => {
778                    let but = match (exc.in_example(which), exc.in_code, uncommented) {
779                        (Absent, _, _) => "but exception entry expected key to be absent",
780                        (_, _, false) => "when processing still-commented-out file!",
781                        (_, Some(true), _) => {
782                            "but an exception entry says it should have been recognised"
783                        }
784                        (Present, Some(false), true) => return Ok(()), // that's as expected
785                        (Present, None, true) => return Ok(()), // that's could be as expected
786                    };
787                    (
788                        found,
789                        format!("parser reported unrecognised config key, {but}"),
790                    )
791                }
792                Right(exc) => {
793                    // An exception entry exists.  The actual situation is either
794                    //   - not found in file (so no "unrecognised" report)
795                    //   - processed successfully (found in file and in code)
796                    // but we don't know which.
797                    let trouble = match (exc.in_example(which), exc.in_code, uncommented) {
798                        (Absent, _, _) => return Ok(()), // not in file, no report expected
799                        (_, _, false) => return Ok(()),  // not uncommented, no report expected
800                        (_, Some(true), _) => return Ok(()), // code likes it, no report expected
801                        (Present, Some(false), true) => {
802                            "expected an 'unknown config key' report but didn't see one"
803                        }
804                        (Present, None, true) => return Ok(()), // not sure, have to just allow it
805                    };
806                    (&exc.key, trouble.into())
807                }
808            };
809            Err((key.clone(), err))
810        }
811
812        let parses_to_defaults = |example: &str, which: WhichExample, uncommented: bool| {
813            let cfg = {
814                let mut sources = tor_config::ConfigurationSources::new_empty();
815                sources.push_source(
816                    tor_config::ConfigurationSource::from_verbatim(example.to_string()),
817                    tor_config::sources::MustRead::MustRead,
818                );
819                sources.load().unwrap()
820            };
821
822            // This tests that the example settings do not *contradict* the defaults.
823            let results: ResolutionResults<ArtiCombinedConfig> =
824                tor_config::resolve_return_results(cfg).unwrap();
825
826            assert_eq!(&results.value, &default, "{which:?} {uncommented:?}");
827            assert_eq!(&results.value, &empty_config, "{which:?} {uncommented:?}");
828
829            // We serialize the DisfavouredKey entries to strings to compare them against
830            // `known_unrecognized_options`.
831            let unrecognized = results
832                .unrecognized
833                .iter()
834                .map(|k| k.to_string())
835                .collect_vec();
836
837            eprintln!(
838                "parsing of {which:?} uncommented={uncommented:?}, unrecognized={unrecognized:#?}"
839            );
840
841            let reports =
842                Itertools::merge_join_by(unrecognized.iter(), exceptions.iter(), |u, e| {
843                    u.as_str().cmp(&e.key)
844                })
845                .filter_map(|eob| analyse_joined_info(which, uncommented, eob).err())
846                .collect_vec();
847
848            if !reports.is_empty() {
849                let reports = reports.iter().fold(String::new(), |mut out, (k, s)| {
850                    writeln!(out, "  {}: {}", s, k).unwrap();
851                    out
852                });
853
854                panic!(
855                    r"
856mismatch: results of parsing example files (& vs declared exceptions):
857example config file {which:?}, uncommented={uncommented:?}
858{reports}
859"
860                );
861            }
862
863            results.value
864        };
865
866        let _ = parses_to_defaults(ARTI_EXAMPLE_CONFIG, WhichExample::New, false);
867        let _ = parses_to_defaults(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, false);
868
869        let built_default = (
870            ArtiConfigBuilder::default().build().unwrap(),
871            TorClientConfigBuilder::default().build().unwrap(),
872        );
873
874        let parsed = parses_to_defaults(
875            &uncomment_example_settings(ARTI_EXAMPLE_CONFIG),
876            WhichExample::New,
877            true,
878        );
879        let parsed_old = parses_to_defaults(
880            &uncomment_example_settings(OLDEST_SUPPORTED_CONFIG),
881            WhichExample::Old,
882            true,
883        );
884
885        assert_eq!(&parsed, &built_default);
886        assert_eq!(&parsed_old, &built_default);
887
888        assert_eq!(&default, &built_default);
889    }
890
891    /// Config file exhaustiveness and default checking
892    ///
893    /// `example_file` is a putative configuration file text.
894    /// It is expected to contain "example lines",
895    /// which are lines in start with `#` *not followed by whitespace*.
896    ///
897    /// This function checks that:
898    ///
899    /// Positive check on the example lines that are present.
900    ///  * `example_file`, when example lines are uncommented, can be parsed.
901    ///  * The example values are the same as the default values.
902    ///
903    /// Check for missing examples:
904    ///  * Every key `in `TorClientConfig` or `ArtiConfig` has a corresponding example value.
905    ///  * Except as declared in [`declared_config_exceptions`]
906    ///  * And also, tolerating absence in the example files of `deprecated` keys
907    ///
908    /// It handles straightforward cases, where the example line is in a `[section]`
909    /// and is something like `#key = value`.
910    ///
911    /// More complex keys, eg those which don't appear in "example lines" starting with just `#`,
912    /// must be dealt with ad-hoc and mentioned in `declared_config_exceptions`.
913    ///
914    /// For complex config keys, it may not be sufficient to simply write the default value in
915    /// the example files (along with perhaps some other information).  In that case,
916    ///   1. Write a bespoke example (with lines starting `# `) in the config file.
917    ///   2. Write a bespoke test, to test the parsing of the bespoke example.
918    ///      This will probably involve using `ExampleSectionLines` and may be quite ad-hoc.
919    ///      The test function bridges(), below, is a complex worked example.
920    ///   3. Either add a trivial example for the affected key(s) (starting with just `#`)
921    ///      or add the affected key(s) to `declared_config_exceptions`
922    fn exhaustive_1(example_file: &str, which: WhichExample, deprecated: &[String]) {
923        use InExample::*;
924        use serde_json::Value as JsValue;
925        use std::collections::BTreeSet;
926
927        let example = uncomment_example_settings(example_file);
928        let example: toml::Value = toml::from_str(&example).unwrap();
929        // dbg!(&example);
930        let example = serde_json::to_value(example).unwrap();
931        // dbg!(&example);
932
933        // "Exhaustive" taxonomy of the recognized configuration keys
934        //
935        // We use the JSON serialization of the default builders, because Rust's toml
936        // implementation likes to omit more things, that we want to see.
937        //
938        // I'm not sure this is quite perfect but it is pretty good,
939        // and has found a number of un-exampled config keys.
940        let exhausts = [
941            serde_json::to_value(TorClientConfig::builder()).unwrap(),
942            serde_json::to_value(ArtiConfig::builder()).unwrap(),
943        ];
944
945        /// This code does *not* record a problem for keys *in* the example file
946        /// that are unrecognized.  That is handled by the `default_config` test.
947        #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, derive_more::Display)]
948        enum ProblemKind {
949            #[display("recognised by serialisation, but missing from example config file")]
950            MissingFromExample,
951            #[display("expected that example config file should contain have this as a table")]
952            ExpectedTableInExample,
953            #[display(
954                "declared exception says this key should be recognised but not in file, but that doesn't seem to be the case"
955            )]
956            UnusedException,
957        }
958
959        #[derive(Default, Debug)]
960        struct Walk {
961            current_path: Vec<String>,
962            problems: Vec<(String, ProblemKind)>,
963        }
964
965        impl Walk {
966            /// Records a problem
967            fn bad(&mut self, kind: ProblemKind) {
968                self.problems.push((self.current_path.join("."), kind));
969            }
970
971            /// Recurses, looking for problems
972            ///
973            /// Visited for every node in either or both of the starting `exhausts`.
974            ///
975            /// `E` is the number of elements in `exhausts`, ie the number of different
976            /// top-level config types that Arti uses.  Ie, 2.
977            fn walk<const E: usize>(
978                &mut self,
979                example: Option<&JsValue>,
980                exhausts: [Option<&JsValue>; E],
981            ) {
982                assert! { exhausts.into_iter().any(|e| e.is_some()) }
983
984                let example = if let Some(e) = example {
985                    e
986                } else {
987                    self.bad(ProblemKind::MissingFromExample);
988                    return;
989                };
990
991                let tables = exhausts.map(|e| e?.as_object());
992
993                // Union of the keys of both exhausts' tables (insofar as they *are* tables)
994                let table_keys = tables
995                    .iter()
996                    .flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten())
997                    .collect::<BTreeSet<String>>();
998
999                for key in table_keys {
1000                    let example = if let Some(e) = example.as_object() {
1001                        e
1002                    } else {
1003                        // At least one of the exhausts was a nonempty table,
1004                        // but the corresponding example node isn't a table.
1005                        self.bad(ProblemKind::ExpectedTableInExample);
1006                        continue;
1007                    };
1008
1009                    // Descend the same key in all the places.
1010                    self.current_path.push(key.clone());
1011                    self.walk(example.get(&key), tables.map(|t| t?.get(&key)));
1012                    self.current_path.pop().unwrap();
1013                }
1014            }
1015        }
1016
1017        let exhausts = exhausts.iter().map(Some).collect_vec().try_into().unwrap();
1018
1019        let mut walk = Walk::default();
1020        walk.walk::<2>(Some(&example), exhausts);
1021        let mut problems = walk.problems;
1022
1023        /// Marker present in `expect_missing` to say we *definitely* expect it
1024        #[derive(Debug, Copy, Clone)]
1025        struct DefinitelyRecognized;
1026
1027        let expect_missing = declared_config_exceptions()
1028            .iter()
1029            .filter_map(|exc| {
1030                let definitely = match (exc.in_example(which), exc.in_code) {
1031                    (Present, _) => return None, // in file, don't expect "non-exhaustive" notice
1032                    (_, Some(false)) => return None, // code hasn't heard of it, likewise
1033                    (Absent, Some(true)) => Some(DefinitelyRecognized),
1034                    (Absent, None) => None, // allow this exception but don't mind if not known
1035                };
1036                Some((exc.key.clone(), definitely))
1037            })
1038            .collect_vec();
1039        dbg!(&expect_missing);
1040
1041        // Things might appear in expect_missing for different reasons, and sometimes
1042        // at different levels.  For example, `bridges.transports` is expected to be
1043        // missing because we document that a different way in the example; but
1044        // `bridges` is expected to be missing from the OLDEST_SUPPORTED_CONFIG,
1045        // because that config predates bridge support.
1046        //
1047        // When this happens, we need to remove `bridges.transports` in favour of
1048        // the over-arching `bridges`.
1049        let expect_missing: Vec<(String, Option<DefinitelyRecognized>)> = expect_missing
1050            .iter()
1051            .cloned()
1052            .filter({
1053                let original: HashSet<_> = expect_missing.iter().map(|(k, _)| k.clone()).collect();
1054                move |(found, _)| {
1055                    !found
1056                        .match_indices('.')
1057                        .any(|(doti, _)| original.contains(&found[0..doti]))
1058                }
1059            })
1060            .collect_vec();
1061        dbg!(&expect_missing);
1062
1063        for (exp, definitely) in expect_missing {
1064            let was = problems.len();
1065            problems.retain(|(path, _)| path != &exp);
1066            if problems.len() == was && definitely.is_some() {
1067                problems.push((exp, ProblemKind::UnusedException));
1068            }
1069        }
1070
1071        let problems = problems
1072            .into_iter()
1073            .filter(|(key, _kind)| !deprecated.iter().any(|dep| key == dep))
1074            .map(|(path, m)| format!("    config key {:?}: {}", path, m))
1075            .collect_vec();
1076
1077        // If this assert fails, it might be because in `fn exhaustive`, below,
1078        // a newly-defined config item has not been added to the list for OLDEST_SUPPORTED_CONFIG.
1079        assert!(
1080            problems.is_empty(),
1081            "example config {which:?} exhaustiveness check failed: {}\n-----8<-----\n{}\n-----8<-----\n",
1082            problems.join("\n"),
1083            example_file,
1084        );
1085    }
1086
1087    #[test]
1088    fn exhaustive() {
1089        let mut deprecated = vec![];
1090        <(ArtiConfig, TorClientConfig) as tor_config::load::Resolvable>::enumerate_deprecated_keys(
1091            &mut |l| {
1092                for k in l {
1093                    deprecated.push(k.to_string());
1094                }
1095            },
1096        );
1097        let deprecated = deprecated.iter().cloned().collect_vec();
1098
1099        // Check that:
1100        //  - The primary example config file has good examples for everything
1101        //  - Except for deprecated config keys
1102        //  - (And, except for those that we never expect: CONFIG_KEYS_EXPECT_NO_EXAMPLE.)
1103        exhaustive_1(ARTI_EXAMPLE_CONFIG, WhichExample::New, &deprecated);
1104
1105        // Check that:
1106        //  - That oldest supported example config file has good examples for everything
1107        //  - Except for keys that we have introduced since that file was written
1108        //  - (And, except for those that we never expect: CONFIG_KEYS_EXPECT_NO_EXAMPLE.)
1109        // We *tolerate* entries in this table that don't actually occur in the oldest-supported
1110        // example.  This avoids having to feature-annotate them.
1111        exhaustive_1(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, &deprecated);
1112    }
1113
1114    /// Check that the `Report` of `err` contains the string `exp`, and otherwise panic
1115    #[cfg_attr(feature = "pt-client", allow(dead_code))]
1116    fn expect_err_contains(err: ConfigResolveError, exp: &str) {
1117        use std::error::Error as StdError;
1118        let err: Box<dyn StdError> = Box::new(err);
1119        let err = tor_error::Report(err).to_string();
1120        assert!(
1121            err.contains(exp),
1122            "wrong message, got {:?}, exp {:?}",
1123            err,
1124            exp,
1125        );
1126    }
1127
1128    #[test]
1129    fn bridges() {
1130        // We make assumptions about the contents of `arti-example-config.toml` !
1131        //
1132        // 1. There are nontrivial, non-default examples of `bridges.bridges`.
1133        // 2. These are in the `[bridges]` section, after a line `# For example:`
1134        // 3. There's precisely one ``` example, with conventional TOML formatting.
1135        // 4. There's precisely one [ ] example, with conventional TOML formatting.
1136        // 5. Both these examples specify the same set of bridges.
1137        // 6. There are three bridges.
1138        // 7. Lines starting with a digit or `[` are direct bridges; others are PT.
1139        //
1140        // Below, we annotate with `[1]` etc. where these assumptions are made.
1141
1142        // Filter examples that we don't want to test in this configuration
1143        let filter_examples = |#[allow(unused_mut)] mut examples: ExampleSectionLines| -> _ {
1144            // [7], filter out the PTs
1145            if cfg!(all(feature = "bridge-client", not(feature = "pt-client"))) {
1146                let looks_like_addr =
1147                    |l: &str| l.starts_with(|c: char| c.is_ascii_digit() || c == '[');
1148                examples.lines.retain(|l| looks_like_addr(l));
1149            }
1150
1151            examples
1152        };
1153
1154        // Tests that one example parses, and returns what it parsed.
1155        // If bridge support is completely disabled, checks that this configuration
1156        // is rejected, as it should be, and returns a dummy value `((),)`
1157        // (so that the rest of the test has something to "compare that we parsed it the same").
1158        let resolve_examples = |examples: &ExampleSectionLines| {
1159            // [7], check that the PT bridge is properly rejected
1160            #[cfg(all(feature = "bridge-client", not(feature = "pt-client")))]
1161            {
1162                let err = examples.resolve::<TorClientConfig>().unwrap_err();
1163                expect_err_contains(err, "support disabled in cargo features");
1164            }
1165
1166            let examples = filter_examples(examples.clone());
1167
1168            #[cfg(feature = "bridge-client")]
1169            {
1170                examples.resolve::<TorClientConfig>().unwrap()
1171            }
1172
1173            #[cfg(not(feature = "bridge-client"))]
1174            {
1175                let err = examples.resolve::<TorClientConfig>().unwrap_err();
1176                expect_err_contains(err, "support disabled in cargo features");
1177                // Use ((),) as the dummy unit value because () gives clippy conniptions
1178                ((),)
1179            }
1180        };
1181
1182        // [1], [2], narrow to just the nontrivial, non-default, examples
1183        let mut examples = ExampleSectionLines::from_section("bridges");
1184        examples.narrow((r#"^# For example:"#, true), NARROW_NONE);
1185
1186        let compare = {
1187            // [3], narrow to the multi-line string
1188            let mut examples = examples.clone();
1189            examples.narrow((r#"^#  bridges = '''"#, true), (r#"^#  '''"#, true));
1190            examples.uncomment();
1191
1192            let parsed = resolve_examples(&examples);
1193
1194            // Now we fish out the lines ourselves as a double-check
1195            // We must strip off the bridges = ''' and ''' lines.
1196            examples.lines.remove(0);
1197            examples.lines.remove(examples.lines.len() - 1);
1198            // [6], check we got the number of examples we expected
1199            examples.expect_lines(3);
1200
1201            // If we have the bridge API, try parsing each line and using the API to insert it
1202            #[cfg(feature = "bridge-client")]
1203            {
1204                let examples = filter_examples(examples);
1205                let mut built = TorClientConfig::builder();
1206                for l in &examples.lines {
1207                    built.bridges().bridges().push(l.trim().parse().expect(l));
1208                }
1209                let built = built.build().unwrap();
1210
1211                assert_eq!(&parsed, &built);
1212            }
1213
1214            parsed
1215        };
1216
1217        // [4], [5], narrow to the [ ] section, parse again, and compare
1218        {
1219            examples.narrow((r#"^#  bridges = \["#, true), (r#"^#  \]"#, true));
1220            examples.uncomment();
1221            let parsed = resolve_examples(&examples);
1222            assert_eq!(&parsed, &compare);
1223        }
1224    }
1225
1226    #[test]
1227    fn transports() {
1228        // Extract and uncomment our transports lines.
1229        //
1230        // (They're everything from  `# An example managed pluggable transport`
1231        // through the start of the next
1232        // section.  They start with "#    ".)
1233        let mut file =
1234            ExampleSectionLines::from_markers("# An example managed pluggable transport", "[");
1235        file.lines.retain(|line| line.starts_with("#    "));
1236        file.uncomment();
1237
1238        let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1239        let cfg_got = result.unwrap();
1240
1241        #[cfg(feature = "pt-client")]
1242        {
1243            use arti_client::config::{BridgesConfig, pt::TransportConfig};
1244            use tor_config_path::CfgPath;
1245
1246            let bridges_got: &BridgesConfig = cfg_got.0.as_ref();
1247
1248            // Build the expected configuration.
1249            let mut bld = BridgesConfig::builder();
1250            {
1251                let mut b = TransportConfig::builder();
1252                b.protocols(vec!["obfs4".parse().unwrap(), "obfs5".parse().unwrap()]);
1253                b.path(CfgPath::new("/usr/bin/obfsproxy".to_string()));
1254                b.arguments(vec!["-obfs4".to_string(), "-obfs5".to_string()]);
1255                b.run_on_startup(true);
1256                bld.transports().push(b);
1257            }
1258            {
1259                let mut b = TransportConfig::builder();
1260                b.protocols(vec!["obfs4".parse().unwrap()]);
1261                b.proxy_addr("127.0.0.1:31337".parse().unwrap());
1262                bld.transports().push(b);
1263            }
1264
1265            let bridges_expected = bld.build().unwrap();
1266            assert_eq!(&bridges_expected, bridges_got);
1267        }
1268    }
1269
1270    #[test]
1271    fn memquota() {
1272        // Test that uncommenting the example generates a config
1273        // with tracking enabled, iff support is compiled in.
1274        let mut file = ExampleSectionLines::from_section("system");
1275        file.lines.retain(|line| line.starts_with("#    memory."));
1276        file.uncomment();
1277
1278        let result = file.resolve_return_results::<(TorClientConfig, ArtiConfig)>();
1279
1280        let result = result.unwrap();
1281
1282        // Test that the example config doesn't have any unrecognised keys
1283        assert_eq!(result.unrecognized, []);
1284        assert_eq!(result.deprecated, []);
1285
1286        let inner: &tor_memquota::testing::ConfigInner =
1287            result.value.0.system_memory().inner().unwrap();
1288
1289        // Test that the example low_water is the default
1290        // value for the example max.
1291        let defaulted_low = tor_memquota::Config::builder()
1292            .max(*inner.max)
1293            .build()
1294            .unwrap();
1295        let inner_defaulted_low = defaulted_low.inner().unwrap();
1296        assert_eq!(inner, inner_defaulted_low);
1297    }
1298
1299    #[test]
1300    fn metrics() {
1301        // Test that uncommenting the example generates a config with prometheus enabled.
1302        let mut file = ExampleSectionLines::from_section("metrics");
1303        file.lines
1304            .retain(|line| line.starts_with("#    prometheus."));
1305        file.uncomment();
1306
1307        let result = file
1308            .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1309            .unwrap();
1310
1311        // Test that the example config doesn't have any unrecognised keys
1312        assert_eq!(result.unrecognized, []);
1313        assert_eq!(result.deprecated, []);
1314
1315        // Check that the example is as we expected
1316        assert_eq!(
1317            result
1318                .value
1319                .1
1320                .metrics
1321                .prometheus
1322                .listen
1323                .single_address_legacy()
1324                .unwrap(),
1325            Some("127.0.0.1:9035".parse().unwrap()),
1326        );
1327
1328        // We don't test "compiled out but not used" here.
1329        // That case is handled in proxy.rs at startup time.
1330    }
1331
1332    #[test]
1333    fn onion_services() {
1334        // Here we require that the onion services configuration is between a line labeled
1335        // with `##### ONION SERVICES` and a line labeled with `##### RPC`, and that each
1336        // line of _real_ configuration in that section begins with `#    `.
1337        let mut file = ExampleSectionLines::from_markers("##### ONION SERVICES", "##### RPC");
1338        file.lines.retain(|line| line.starts_with("#    "));
1339        file.uncomment();
1340
1341        let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1342        #[cfg(feature = "onion-service-service")]
1343        {
1344            let svc_expected = {
1345                use tor_hsrproxy::config::*;
1346                let mut b = OnionServiceProxyConfigBuilder::default();
1347                b.service().nickname("allium-cepa".parse().unwrap());
1348                b.proxy().proxy_ports().push(ProxyRule::new(
1349                    ProxyPattern::one_port(80).unwrap(),
1350                    ProxyAction::Forward(
1351                        Encapsulation::Simple,
1352                        TargetAddr::Inet("127.0.0.1:10080".parse().unwrap()),
1353                    ),
1354                ));
1355                b.proxy().proxy_ports().push(ProxyRule::new(
1356                    ProxyPattern::one_port(22).unwrap(),
1357                    ProxyAction::DestroyCircuit,
1358                ));
1359                b.proxy().proxy_ports().push(ProxyRule::new(
1360                    ProxyPattern::one_port(265).unwrap(),
1361                    ProxyAction::IgnoreStream,
1362                ));
1363                /* TODO (#1246)
1364                b.proxy().proxy_ports().push(ProxyRule::new(
1365                    ProxyPattern::port_range(1, 1024).unwrap(),
1366                    ProxyAction::Forward(
1367                        Encapsulation::Simple,
1368                        TargetAddr::Unix("/var/run/allium-cepa/socket".into()),
1369                    ),
1370                ));
1371                */
1372                b.proxy().proxy_ports().push(ProxyRule::new(
1373                    ProxyPattern::one_port(443).unwrap(),
1374                    ProxyAction::RejectStream,
1375                ));
1376                b.proxy().proxy_ports().push(ProxyRule::new(
1377                    ProxyPattern::all_ports(),
1378                    ProxyAction::DestroyCircuit,
1379                ));
1380
1381                #[cfg(feature = "restricted-discovery")]
1382                {
1383                    const ALICE_KEY: &str =
1384                        "descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA";
1385                    const BOB_KEY: &str =
1386                        "descriptor:x25519:b5zqgtpermmuda6vc63lhjuf5ihpokjmuk26ly2xksf7vg52aesq";
1387                    for (nickname, key) in [("alice", ALICE_KEY), ("bob", BOB_KEY)] {
1388                        b.service()
1389                            .restricted_discovery()
1390                            .enabled(true)
1391                            .static_keys()
1392                            .access()
1393                            .push((
1394                                HsClientNickname::from_str(nickname).unwrap(),
1395                                HsClientDescEncKey::from_str(key).unwrap(),
1396                            ));
1397                    }
1398                    let mut dir = DirectoryKeyProviderBuilder::default();
1399                    dir.path(CfgPath::new(
1400                        "/var/lib/tor/hidden_service/authorized_clients".to_string(),
1401                    ));
1402
1403                    b.service()
1404                        .restricted_discovery()
1405                        .key_dirs()
1406                        .access()
1407                        .push(dir);
1408                }
1409
1410                b.build().unwrap()
1411            };
1412
1413            cfg_if::cfg_if! {
1414                if #[cfg(feature = "restricted-discovery")] {
1415                    let cfg = result.unwrap();
1416                    let services = cfg.1.onion_services;
1417                    assert_eq!(services.len(), 1);
1418                    let svc = services.values().next().unwrap();
1419                    assert_eq!(svc, &svc_expected);
1420                } else {
1421                    expect_err_contains(
1422                        result.unwrap_err(),
1423                        "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
1424                    );
1425                }
1426            }
1427        }
1428        #[cfg(not(feature = "onion-service-service"))]
1429        {
1430            expect_err_contains(result.unwrap_err(), "no support for running onion services");
1431        }
1432    }
1433
1434    #[cfg(feature = "rpc")]
1435    #[test]
1436    fn rpc_defaults() {
1437        let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1438        // This will get us all the RPC entries that correspond to our defaults.
1439        //
1440        // The examples that _aren't_ in our defaults have '#      ' at the start.
1441        file.lines
1442            .retain(|line| line.starts_with("#    ") && !line.starts_with("#      "));
1443        file.uncomment();
1444
1445        let parsed = file
1446            .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1447            .unwrap();
1448        assert!(parsed.unrecognized.is_empty());
1449        assert!(parsed.deprecated.is_empty());
1450        let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1451        let rpc_default = RpcConfig::default();
1452        assert_eq!(rpc_parsed, &rpc_default);
1453    }
1454
1455    #[cfg(feature = "rpc")]
1456    #[test]
1457    fn rpc_full() {
1458        use crate::rpc::listener::{ConnectPointOptionsBuilder, RpcListenerSetConfigBuilder};
1459
1460        // This will get us all the RPC entries, including those that _don't_ correspond to our defaults.
1461        let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1462        // We skip the "file" item because it conflicts with "dir" and "file_options"
1463        file.lines
1464            .retain(|line| line.starts_with("#    ") && !line.contains("file ="));
1465        file.uncomment();
1466
1467        let parsed = file
1468            .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1469            .unwrap();
1470        let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1471
1472        let expected = {
1473            let mut bld_opts = ConnectPointOptionsBuilder::default();
1474            bld_opts.enable(false);
1475
1476            let mut bld_set = RpcListenerSetConfigBuilder::default();
1477            bld_set.dir(CfgPath::new("${HOME}/.my_connect_files/".to_string()));
1478            bld_set.listener_options().enable(true);
1479            bld_set
1480                .file_options()
1481                .insert("bad_file.json".to_string(), bld_opts);
1482
1483            let mut bld = RpcConfigBuilder::default();
1484            bld.listen().insert("label".to_string(), bld_set);
1485            bld.build().unwrap()
1486        };
1487
1488        assert_eq!(&expected, rpc_parsed);
1489    }
1490
1491    /// Helper for fishing out parts of the config file and uncommenting them.
1492    ///
1493    /// It represents a part of a configuration file.
1494    ///
1495    /// This can be used to find part of the config file by ad-hoc regexp matching,
1496    /// uncomment it, and parse it.  This is useful as part of a test to check
1497    /// that we can parse more complex config.
1498    #[derive(Debug, Clone)]
1499    struct ExampleSectionLines {
1500        /// The header for the section that we are parsing.  It is
1501        /// prepended to the lines before parsing them.
1502        section: String,
1503        /// The lines in the section.
1504        lines: Vec<String>,
1505    }
1506
1507    /// A 2-tuple of a regular expression and a flag describing whether the line
1508    /// containing the expression should be included in the result of `narrow()`.
1509    type NarrowInstruction<'s> = (&'s str, bool);
1510    /// A NarrowInstruction that does not match anything.
1511    const NARROW_NONE: NarrowInstruction<'static> = ("?<none>", false);
1512
1513    impl ExampleSectionLines {
1514        /// Construct a new `ExampleSectionLines` from `ARTI_EXAMPLE_CONFIG`, containing
1515        /// everything that starts with `[section]`, up to but not including the
1516        /// next line that begins with a `[`.
1517        fn from_section(section: &str) -> Self {
1518            Self::from_markers(format!("[{section}]"), "[")
1519        }
1520
1521        /// Construct a new `ExampleSectionLines` from `ARTI_EXAMPLE_CONFIG`,
1522        /// containing everything that starts with `start`, up to but not
1523        /// including the next line that begins with `end`.
1524        ///
1525        /// If `start` is a configuration section header it will be put in the
1526        /// `section` field of the returned `ExampleSectionLines`, otherwise
1527        /// at the beginning of the `lines` field.
1528        ///
1529        /// `start` will be perceived as a configuration section header if it
1530        /// starts with `[` and ends with `]`.
1531        fn from_markers<S, E>(start: S, end: E) -> Self
1532        where
1533            S: AsRef<str>,
1534            E: AsRef<str>,
1535        {
1536            let (start, end) = (start.as_ref(), end.as_ref());
1537            let mut lines = ARTI_EXAMPLE_CONFIG
1538                .lines()
1539                .skip_while(|line| !line.starts_with(start))
1540                .peekable();
1541            let section = lines
1542                .next_if(|l0| l0.starts_with('['))
1543                .map(|section| section.to_owned())
1544                .unwrap_or_default();
1545            let lines = lines
1546                .take_while(|line| !line.starts_with(end))
1547                .map(|l| l.to_owned())
1548                .collect_vec();
1549
1550            Self { section, lines }
1551        }
1552
1553        /// Remove all lines from this section, except those between the (unique) line matching
1554        /// "start" and the next line matching "end" (or the end of the file).
1555        fn narrow(&mut self, start: NarrowInstruction, end: NarrowInstruction) {
1556            let find_index = |(re, include), start_pos, exactly_one: bool, adjust: [isize; 2]| {
1557                if (re, include) == NARROW_NONE {
1558                    return None;
1559                }
1560
1561                let re = Regex::new(re).expect(re);
1562                let i = self
1563                    .lines
1564                    .iter()
1565                    .enumerate()
1566                    .skip(start_pos)
1567                    .filter(|(_, l)| re.is_match(l))
1568                    .map(|(i, _)| i);
1569                let i = if exactly_one {
1570                    i.clone().exactly_one().unwrap_or_else(|_| {
1571                        panic!("RE={:?} I={:#?} L={:#?}", re, i.collect_vec(), &self.lines)
1572                    })
1573                } else {
1574                    i.clone().next()?
1575                };
1576
1577                let adjust = adjust[usize::from(include)];
1578                let i = (i as isize + adjust) as usize;
1579                Some(i)
1580            };
1581
1582            eprint!("narrow {:?} {:?}: ", start, end);
1583            let start = find_index(start, 0, true, [1, 0]).unwrap_or(0);
1584            let end = find_index(end, start + 1, false, [0, 1]).unwrap_or(self.lines.len());
1585            eprintln!("{:?} {:?}", start, end);
1586            // don't tolerate empty
1587            assert!(start < end, "empty, from {:#?}", &self.lines);
1588            self.lines = self.lines.drain(..).take(end).skip(start).collect_vec();
1589        }
1590
1591        /// Assert that this section contains exactly `n` lines.
1592        fn expect_lines(&self, n: usize) {
1593            assert_eq!(self.lines.len(), n);
1594        }
1595
1596        /// Remove `#` from the start of every line that begins with it.
1597        fn uncomment(&mut self) {
1598            self.strip_prefix("#");
1599        }
1600
1601        /// Remove `prefix` from the start of every line.
1602        ///
1603        /// If there are lines that *don't* start with `prefix`, crash.
1604        ///
1605        /// But, lines starting with `[` are left unchanged, in any case.
1606        /// (These are TOML section markers; changing them would change the TOML structure.)
1607        fn strip_prefix(&mut self, prefix: &str) {
1608            for l in &mut self.lines {
1609                if !l.starts_with('[') {
1610                    *l = l.strip_prefix(prefix).expect(l).to_string();
1611                }
1612            }
1613        }
1614
1615        /// Join the parts of this object together into a single string.
1616        fn build_string(&self) -> String {
1617            chain!(iter::once(&self.section), self.lines.iter(),).join("\n")
1618        }
1619
1620        /// Make a TOML document of this section and parse it as a complete configuration.
1621        /// Panic if the section cannot be parsed.
1622        fn parse(&self) -> tor_config::ConfigurationTree {
1623            let s = self.build_string();
1624            eprintln!("parsing\n  --\n{}\n  --", &s);
1625            let mut sources = tor_config::ConfigurationSources::new_empty();
1626            sources.push_source(
1627                tor_config::ConfigurationSource::from_verbatim(s.clone()),
1628                tor_config::sources::MustRead::MustRead,
1629            );
1630            sources.load().expect(&s)
1631        }
1632
1633        fn resolve<R: tor_config::load::Resolvable>(&self) -> Result<R, ConfigResolveError> {
1634            tor_config::load::resolve(self.parse())
1635        }
1636
1637        fn resolve_return_results<R: tor_config::load::Resolvable>(
1638            &self,
1639        ) -> Result<ResolutionResults<R>, ConfigResolveError> {
1640            tor_config::load::resolve_return_results(self.parse())
1641        }
1642    }
1643
1644    // More normal config tests
1645
1646    #[test]
1647    fn builder() {
1648        use tor_config_path::CfgPath;
1649        let sec = std::time::Duration::from_secs(1);
1650
1651        let mut authorities = dir::AuthorityContacts::builder();
1652        authorities.v3idents().push([22; 20].into());
1653
1654        let mut fallback = dir::FallbackDir::builder();
1655        fallback
1656            .rsa_identity([23; 20].into())
1657            .ed_identity([99; 32].into())
1658            .orports()
1659            .push("127.0.0.7:7".parse().unwrap());
1660
1661        let mut bld = ArtiConfig::builder();
1662        let mut bld_tor = TorClientConfig::builder();
1663
1664        bld.proxy().socks_listen(Listen::new_localhost(9999));
1665        bld.logging().console("warn");
1666
1667        *bld_tor.tor_network().authorities() = authorities;
1668        bld_tor.tor_network().set_fallback_caches(vec![fallback]);
1669        bld_tor
1670            .storage()
1671            .cache_dir(CfgPath::new("/var/tmp/foo".to_owned()))
1672            .state_dir(CfgPath::new("/var/tmp/bar".to_owned()));
1673        bld_tor.download_schedule().retry_certs().attempts(10);
1674        bld_tor.download_schedule().retry_certs().initial_delay(sec);
1675        bld_tor.download_schedule().retry_certs().parallelism(3);
1676        bld_tor.download_schedule().retry_microdescs().attempts(30);
1677        bld_tor
1678            .download_schedule()
1679            .retry_microdescs()
1680            .initial_delay(10 * sec);
1681        bld_tor
1682            .download_schedule()
1683            .retry_microdescs()
1684            .parallelism(9);
1685        bld_tor
1686            .override_net_params()
1687            .insert("wombats-per-quokka".to_owned(), 7);
1688        bld_tor
1689            .path_rules()
1690            .ipv4_subnet_family_prefix(20)
1691            .ipv6_subnet_family_prefix(48);
1692        bld_tor.preemptive_circuits().disable_at_threshold(12);
1693        bld_tor
1694            .preemptive_circuits()
1695            .set_initial_predicted_ports(vec![80, 443]);
1696        bld_tor
1697            .preemptive_circuits()
1698            .prediction_lifetime(Duration::from_secs(3600))
1699            .min_exit_circs_for_port(2);
1700        bld_tor
1701            .circuit_timing()
1702            .max_dirtiness(90 * sec)
1703            .request_timeout(10 * sec)
1704            .request_max_retries(22)
1705            .request_loyalty(3600 * sec);
1706        bld_tor.address_filter().allow_local_addrs(true);
1707
1708        let val = bld.build().unwrap();
1709
1710        assert_ne!(val, ArtiConfig::default());
1711    }
1712
1713    #[test]
1714    fn articonfig_application() {
1715        let config = ArtiConfig::default();
1716
1717        let application = config.application();
1718        assert_eq!(&config.application, application);
1719    }
1720
1721    #[test]
1722    fn articonfig_logging() {
1723        let config = ArtiConfig::default();
1724
1725        let logging = config.logging();
1726        assert_eq!(&config.logging, logging);
1727    }
1728
1729    #[test]
1730    fn articonfig_proxy() {
1731        let config = ArtiConfig::default();
1732
1733        let proxy = config.proxy();
1734        assert_eq!(&config.proxy, proxy);
1735    }
1736
1737    /// Comprehensive tests for `proxy.socks_listen` and `proxy.dns_listen`.
1738    ///
1739    /// The "this isn't set at all, just use the default" cases are tested elsewhere.
1740    fn ports_listen(
1741        f: &str,
1742        get_listen: &dyn Fn(&ArtiConfig) -> &Listen,
1743        bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option<Listen>,
1744        setter_listen: &dyn Fn(&mut ArtiConfigBuilder, Listen) -> &mut ProxyConfigBuilder,
1745    ) {
1746        let from_toml = |s: &str| -> ArtiConfigBuilder {
1747            let cfg: toml::Value = toml::from_str(dbg!(s)).unwrap();
1748            let cfg: ArtiConfigBuilder = cfg.try_into().unwrap();
1749            cfg
1750        };
1751
1752        let chk = |cfg: &ArtiConfigBuilder, expected: &Listen| {
1753            dbg!(bld_get_listen(cfg));
1754            let cfg = cfg.build().unwrap();
1755            assert_eq!(get_listen(&cfg), expected);
1756        };
1757
1758        let check_setters = |port, expected: &_| {
1759            let cfg = ArtiConfig::builder();
1760            for listen in match port {
1761                None => vec![Listen::new_none(), Listen::new_localhost(0)],
1762                Some(port) => vec![Listen::new_localhost(port)],
1763            } {
1764                let mut cfg = cfg.clone();
1765                setter_listen(&mut cfg, dbg!(listen));
1766                chk(&cfg, expected);
1767            }
1768        };
1769
1770        {
1771            let expected = Listen::new_localhost(100);
1772
1773            let cfg = from_toml(&format!("proxy.{}_listen = 100", f));
1774            assert_eq!(bld_get_listen(&cfg), &Some(Listen::new_localhost(100)));
1775            chk(&cfg, &expected);
1776
1777            check_setters(Some(100), &expected);
1778        }
1779
1780        {
1781            let expected = Listen::new_none();
1782
1783            let cfg = from_toml(&format!("proxy.{}_listen = 0", f));
1784            chk(&cfg, &expected);
1785
1786            check_setters(None, &expected);
1787        }
1788    }
1789
1790    #[test]
1791    fn ports_listen_socks() {
1792        ports_listen(
1793            "socks",
1794            &|cfg| &cfg.proxy.socks_listen,
1795            &|bld| &bld.proxy.socks_listen,
1796            &|bld, arg| bld.proxy.socks_listen(arg),
1797        );
1798    }
1799
1800    #[test]
1801    fn ports_listen_dns() {
1802        ports_listen(
1803            "dns",
1804            &|cfg| &cfg.proxy.dns_listen,
1805            &|bld| &bld.proxy.dns_listen,
1806            &|bld, arg| bld.proxy.dns_listen(arg),
1807        );
1808    }
1809}