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