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