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