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