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