1use derive_builder::Builder;
6use serde::{Deserialize, Serialize};
7use tor_config_path::CfgPath;
8
9#[cfg(feature = "onion-service-service")]
10use crate::onion_proxy::{
11 OnionServiceProxyConfigBuilder, OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder,
12};
13#[cfg(not(feature = "onion-service-service"))]
14use crate::onion_proxy_disabled::{OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder};
15#[cfg(feature = "rpc")]
16pub use crate::rpc::{RpcConfig, RpcConfigBuilder};
17use arti_client::TorClientConfig;
18#[cfg(feature = "onion-service-service")]
19use tor_config::define_list_builder_accessors;
20pub(crate) use tor_config::{ConfigBuildError, Listen, impl_standard_builder};
21
22use crate::{LoggingConfig, LoggingConfigBuilder};
23
24#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
29pub(crate) const ARTI_EXAMPLE_CONFIG: &str = concat!(include_str!("./arti-example-config.toml"));
30
31#[cfg(test)]
48const OLDEST_SUPPORTED_CONFIG: &str = concat!(include_str!("./oldest-supported-config.toml"),);
49
50#[derive(Debug, Clone, Builder, Eq, PartialEq)]
52#[builder(build_fn(error = "ConfigBuildError"))]
53#[builder(derive(Debug, Serialize, Deserialize))]
54#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
55#[cfg_attr(feature = "experimental-api", builder(public))]
56pub(crate) struct ApplicationConfig {
57 #[builder(default)]
65 pub(crate) watch_configuration: bool,
66
67 #[builder(default)]
77 pub(crate) permit_debugging: bool,
78
79 #[builder(default)]
83 pub(crate) allow_running_as_root: bool,
84}
85impl_standard_builder! { ApplicationConfig }
86
87#[derive(Debug, Clone, Builder, Eq, PartialEq)]
89#[builder(build_fn(error = "ConfigBuildError"))]
90#[builder(derive(Debug, Serialize, Deserialize))]
91#[allow(clippy::option_option)] #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
93#[cfg_attr(feature = "experimental-api", builder(public))]
94pub(crate) struct ProxyConfig {
95 #[builder(default = "Listen::new_localhost(9150)")]
99 pub(crate) socks_listen: Listen,
100
101 #[builder(default = "Listen::new_none()")]
103 pub(crate) dns_listen: Listen,
104}
105impl_standard_builder! { ProxyConfig }
106
107#[derive(Debug, Clone, Builder, Eq, PartialEq)]
111#[builder(build_fn(error = "ConfigBuildError"))]
112#[builder(derive(Debug, Serialize, Deserialize))]
113#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
114#[cfg_attr(feature = "experimental-api", builder(public))]
115pub(crate) struct ArtiStorageConfig {
116 #[builder(setter(into), default = "default_port_info_file()")]
118 pub(crate) port_info_file: CfgPath,
119}
120impl_standard_builder! { ArtiStorageConfig }
121
122fn default_port_info_file() -> CfgPath {
124 CfgPath::new("${ARTI_LOCAL_DATA}/public/port_info.json".to_owned())
125}
126
127#[derive(Debug, Clone, Builder, Eq, PartialEq)]
141#[builder(build_fn(error = "ConfigBuildError"))]
142#[builder(derive(Debug, Serialize, Deserialize))]
143#[non_exhaustive]
144#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
145#[cfg_attr(feature = "experimental-api", builder(public))]
146pub(crate) struct SystemConfig {
147 #[builder(setter(into), default = "default_max_files()")]
149 pub(crate) max_files: u64,
150}
151impl_standard_builder! { SystemConfig }
152
153fn default_max_files() -> u64 {
155 16384
156}
157
158#[derive(Debug, Builder, Clone, Eq, PartialEq)]
173#[builder(derive(Serialize, Deserialize, Debug))]
174#[builder(build_fn(private, name = "build_unvalidated", error = "ConfigBuildError"))]
175#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
176#[cfg_attr(feature = "experimental-api", builder(public))]
177pub(crate) struct ArtiConfig {
178 #[builder(sub_builder(fn_name = "build"))]
180 #[builder_field_attr(serde(default))]
181 application: ApplicationConfig,
182
183 #[builder(sub_builder(fn_name = "build"))]
185 #[builder_field_attr(serde(default))]
186 proxy: ProxyConfig,
187
188 #[builder(sub_builder(fn_name = "build"))]
190 #[builder_field_attr(serde(default))]
191 logging: LoggingConfig,
192
193 #[builder(sub_builder(fn_name = "build"))]
195 #[builder_field_attr(serde(default))]
196 pub(crate) metrics: MetricsConfig,
197
198 #[cfg(feature = "rpc")]
200 #[builder(sub_builder(fn_name = "build"))]
201 #[builder_field_attr(serde(default))]
202 pub(crate) rpc: RpcConfig,
203
204 #[cfg(not(feature = "rpc"))]
216 #[builder_field_attr(serde(default))]
217 #[builder(field(type = "Option<toml::Value>", build = "()"), private)]
218 rpc: (),
219
220 #[builder(sub_builder(fn_name = "build"))]
226 #[builder_field_attr(serde(default))]
227 pub(crate) system: SystemConfig,
228
229 #[builder(sub_builder(fn_name = "build"))]
234 #[builder_field_attr(serde(default))]
235 pub(crate) storage: ArtiStorageConfig,
236
237 #[builder(sub_builder(fn_name = "build"), setter(custom))]
246 #[builder_field_attr(serde(default))]
247 pub(crate) onion_services: OnionServiceProxyConfigMap,
248}
249
250impl_standard_builder! { ArtiConfig }
251
252impl ArtiConfigBuilder {
253 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
255 pub(crate) fn build(&self) -> Result<ArtiConfig, ConfigBuildError> {
256 #[cfg_attr(not(feature = "onion-service-service"), allow(unused_mut))]
257 let mut config = self.build_unvalidated()?;
258 #[cfg(feature = "onion-service-service")]
259 for svc in config.onion_services.values_mut() {
260 *svc.svc_cfg
262 .restricted_discovery_mut()
263 .watch_configuration_mut() = config.application.watch_configuration;
264 }
265
266 #[cfg(not(feature = "rpc"))]
267 if self.rpc.is_some() {
268 tracing::warn!("rpc options were set, but Arti was built without support for rpc.");
269 }
270
271 Ok(config)
272 }
273}
274
275impl tor_config::load::TopLevel for ArtiConfig {
276 type Builder = ArtiConfigBuilder;
277 const DEPRECATED_KEYS: &'static [&'static str] = &["proxy.socks_port", "proxy.dns_port"];
282}
283
284#[cfg(feature = "onion-service-service")]
285define_list_builder_accessors! {
286 struct ArtiConfigBuilder {
287 pub(crate) onion_services: [OnionServiceProxyConfigBuilder],
288 }
289}
290
291#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
295pub(crate) type ArtiCombinedConfig = (ArtiConfig, TorClientConfig);
296
297#[derive(Debug, Clone, Builder, Eq, PartialEq)]
299#[builder(build_fn(error = "ConfigBuildError"))]
300#[builder(derive(Debug, Serialize, Deserialize))]
301#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
302#[cfg_attr(feature = "experimental-api", builder(public))]
303pub(crate) struct MetricsConfig {
304 #[builder(sub_builder(fn_name = "build"))]
306 #[builder_field_attr(serde(default))]
307 pub(crate) prometheus: PrometheusConfig,
308}
309impl_standard_builder! { MetricsConfig }
310
311#[derive(Debug, Clone, Builder, Eq, PartialEq)]
313#[builder(build_fn(error = "ConfigBuildError"))]
314#[builder(derive(Debug, Serialize, Deserialize))]
315#[allow(clippy::option_option)] #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
317#[cfg_attr(feature = "experimental-api", builder(public))]
318pub(crate) struct PrometheusConfig {
319 #[builder(default)]
328 #[builder_field_attr(serde(default))]
329 pub(crate) listen: Listen,
330}
331impl_standard_builder! { PrometheusConfig }
332
333impl ArtiConfig {
334 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
336 pub(crate) fn application(&self) -> &ApplicationConfig {
337 &self.application
338 }
339
340 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
342 pub(crate) fn logging(&self) -> &LoggingConfig {
343 &self.logging
344 }
345
346 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
348 pub(crate) fn proxy(&self) -> &ProxyConfig {
349 &self.proxy
350 }
351
352 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
354 pub(crate) fn storage(&self) -> &ArtiStorageConfig {
356 &self.storage
357 }
358
359 #[cfg(feature = "rpc")]
361 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
362 pub(crate) fn rpc(&self) -> &RpcConfig {
363 &self.rpc
364 }
365}
366
367#[cfg(test)]
368mod test {
369 #![allow(clippy::bool_assert_comparison)]
371 #![allow(clippy::clone_on_copy)]
372 #![allow(clippy::dbg_macro)]
373 #![allow(clippy::mixed_attributes_style)]
374 #![allow(clippy::print_stderr)]
375 #![allow(clippy::print_stdout)]
376 #![allow(clippy::single_char_pattern)]
377 #![allow(clippy::unwrap_used)]
378 #![allow(clippy::unchecked_time_subtraction)]
379 #![allow(clippy::useless_vec)]
380 #![allow(clippy::needless_pass_by_value)]
381 #![allow(clippy::iter_overeager_cloned)]
384 #![cfg_attr(not(feature = "pt-client"), allow(dead_code))]
386
387 use arti_client::config::TorClientConfigBuilder;
388 use arti_client::config::dir;
389 use itertools::{EitherOrBoth, Itertools, chain};
390 use regex::Regex;
391 use std::collections::HashSet;
392 use std::fmt::Write as _;
393 use std::iter;
394 use std::time::Duration;
395 use tor_config::load::{ConfigResolveError, ResolutionResults};
396 use tor_config_path::CfgPath;
397
398 #[allow(unused_imports)] use tor_error::ErrorReport as _;
400
401 #[cfg(feature = "restricted-discovery")]
402 use {
403 arti_client::HsClientDescEncKey,
404 std::str::FromStr as _,
405 tor_hsservice::config::restricted_discovery::{
406 DirectoryKeyProviderBuilder, HsClientNickname,
407 },
408 };
409
410 use super::*;
411
412 fn uncomment_example_settings(template: &str) -> String {
419 let re = Regex::new(r#"(?m)^\#([^ \n])"#).unwrap();
420 re.replace_all(template, |cap: ®ex::Captures<'_>| -> _ {
421 cap.get(1).unwrap().as_str().to_string()
422 })
423 .into()
424 }
425
426 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
435 enum InExample {
436 Absent,
437 Present,
438 }
439 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
445 enum WhichExample {
446 Old,
447 New,
448 }
449 #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
455 struct ConfigException {
456 key: String,
458 in_old_example: InExample,
460 in_new_example: InExample,
462 in_code: Option<bool>,
464 }
465 impl ConfigException {
466 fn in_example(&self, which: WhichExample) -> InExample {
467 use WhichExample::*;
468 match which {
469 Old => self.in_old_example,
470 New => self.in_new_example,
471 }
472 }
473 }
474
475 const ALL_RELEVANT_FEATURES_ENABLED: bool = cfg!(all(
477 feature = "bridge-client",
478 feature = "pt-client",
479 feature = "onion-service-client",
480 feature = "rpc",
481 ));
482
483 fn declared_config_exceptions() -> Vec<ConfigException> {
485 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
490 enum InCode {
491 Ignored,
493 FeatureDependent,
501 Recognized,
503 }
504 use InCode::*;
505
506 struct InOld;
508 struct InNew;
510
511 let mut out = vec![];
512
513 let mut declare_exceptions = |in_old_example: Option<InOld>,
526 in_new_example: Option<InNew>,
527 in_code: InCode,
528 keys: &[&str]| {
529 let in_code = match in_code {
530 Ignored => Some(false),
531 Recognized => Some(true),
532 FeatureDependent if ALL_RELEVANT_FEATURES_ENABLED => Some(true),
533 FeatureDependent => None,
534 };
535 #[allow(clippy::needless_pass_by_value)] fn in_example<T>(spec: Option<T>) -> InExample {
537 match spec {
538 None => InExample::Absent,
539 Some(_) => InExample::Present,
540 }
541 }
542 let in_old_example = in_example(in_old_example);
543 let in_new_example = in_example(in_new_example);
544 out.extend(keys.iter().cloned().map(|key| ConfigException {
545 key: key.to_owned(),
546 in_old_example,
547 in_new_example,
548 in_code,
549 }));
550 };
551
552 declare_exceptions(
553 None,
554 Some(InNew),
555 Recognized,
556 &[
557 "application.allow_running_as_root",
559 "bridges",
560 "logging.time_granularity",
561 "path_rules.long_lived_ports",
562 "use_obsolete_software",
563 "circuit_timing.disused_circuit_timeout",
564 "storage.port_info_file",
565 ],
566 );
567
568 declare_exceptions(
569 None,
570 None,
571 Recognized,
572 &[
573 "tor_network.authorities",
575 "tor_network.fallback_caches",
576 ],
577 );
578
579 declare_exceptions(
580 None,
581 None,
582 Recognized,
583 &[
584 "logging.opentelemetry",
586 ],
587 );
588
589 declare_exceptions(
590 Some(InOld),
591 Some(InNew),
592 if cfg!(target_family = "windows") {
593 Ignored
594 } else {
595 Recognized
596 },
597 &[
598 "storage.permissions.trust_group",
600 "storage.permissions.trust_user",
601 ],
602 );
603
604 declare_exceptions(
605 None,
606 None, FeatureDependent,
608 &[
609 "bridges.transports", ],
612 );
613
614 declare_exceptions(
615 None,
616 Some(InNew),
617 FeatureDependent,
618 &[
619 "storage.keystore",
621 ],
622 );
623
624 declare_exceptions(
625 None,
626 None, FeatureDependent,
628 &[
629 "logging.tokio_console",
631 "logging.tokio_console.enabled",
632 ],
633 );
634
635 declare_exceptions(
636 None,
637 None, Recognized,
639 &[
640 "system.memory",
642 "system.memory.max",
643 "system.memory.low_water",
644 ],
645 );
646
647 declare_exceptions(
648 None,
649 Some(InNew), Recognized,
651 &["metrics"],
652 );
653
654 declare_exceptions(
655 None,
656 None, Recognized,
658 &[
659 "metrics.prometheus",
661 "metrics.prometheus.listen",
662 ],
663 );
664
665 declare_exceptions(
666 None,
667 Some(InNew),
668 FeatureDependent,
669 &[
670 ],
672 );
673
674 declare_exceptions(
675 None,
676 Some(InNew),
677 FeatureDependent,
678 &[
679 "address_filter.allow_onion_addrs",
681 "circuit_timing.hs_desc_fetch_attempts",
682 "circuit_timing.hs_intro_rend_attempts",
683 ],
684 );
685
686 declare_exceptions(
687 None,
688 None, FeatureDependent,
690 &[
691 "rpc",
693 "rpc.rpc_listen",
694 ],
695 );
696
697 declare_exceptions(
699 None,
700 None,
701 FeatureDependent,
702 &[
703 "onion_services",
705 ],
706 );
707
708 declare_exceptions(
709 None,
710 Some(InNew),
711 FeatureDependent,
712 &[
713 "vanguards",
715 "vanguards.mode",
716 ],
717 );
718
719 declare_exceptions(
721 None,
722 None,
723 FeatureDependent,
724 &[
725 "storage.keystore.ctor",
726 "storage.keystore.ctor.services",
727 "storage.keystore.ctor.clients",
728 ],
729 );
730
731 out.sort();
732
733 let dupes = out.iter().map(|exc| &exc.key).duplicates().collect_vec();
734 assert!(
735 dupes.is_empty(),
736 "duplicate exceptions in configuration {dupes:?}"
737 );
738
739 eprintln!(
740 "declared config exceptions for this configuration:\n{:#?}",
741 &out
742 );
743 out
744 }
745
746 #[test]
747 fn default_config() {
748 use InExample::*;
749
750 let empty_config = tor_config::ConfigurationSources::new_empty()
751 .load()
752 .unwrap();
753 let empty_config: ArtiCombinedConfig = tor_config::resolve(empty_config).unwrap();
754
755 let default = (ArtiConfig::default(), TorClientConfig::default());
756 let exceptions = declared_config_exceptions();
757
758 #[allow(clippy::needless_pass_by_value)] fn analyse_joined_info(
769 which: WhichExample,
770 uncommented: bool,
771 eob: EitherOrBoth<&String, &ConfigException>,
772 ) -> Result<(), (String, String)> {
773 use EitherOrBoth::*;
774 let (key, err) = match eob {
775 Left(found) => (found, "found in example but not processed".into()),
777 Both(found, exc) => {
778 let but = match (exc.in_example(which), exc.in_code, uncommented) {
779 (Absent, _, _) => "but exception entry expected key to be absent",
780 (_, _, false) => "when processing still-commented-out file!",
781 (_, Some(true), _) => {
782 "but an exception entry says it should have been recognised"
783 }
784 (Present, Some(false), true) => return Ok(()), (Present, None, true) => return Ok(()), };
787 (
788 found,
789 format!("parser reported unrecognised config key, {but}"),
790 )
791 }
792 Right(exc) => {
793 let trouble = match (exc.in_example(which), exc.in_code, uncommented) {
798 (Absent, _, _) => return Ok(()), (_, _, false) => return Ok(()), (_, Some(true), _) => return Ok(()), (Present, Some(false), true) => {
802 "expected an 'unknown config key' report but didn't see one"
803 }
804 (Present, None, true) => return Ok(()), };
806 (&exc.key, trouble.into())
807 }
808 };
809 Err((key.clone(), err))
810 }
811
812 let parses_to_defaults = |example: &str, which: WhichExample, uncommented: bool| {
813 let cfg = {
814 let mut sources = tor_config::ConfigurationSources::new_empty();
815 sources.push_source(
816 tor_config::ConfigurationSource::from_verbatim(example.to_string()),
817 tor_config::sources::MustRead::MustRead,
818 );
819 sources.load().unwrap()
820 };
821
822 let results: ResolutionResults<ArtiCombinedConfig> =
824 tor_config::resolve_return_results(cfg).unwrap();
825
826 assert_eq!(&results.value, &default, "{which:?} {uncommented:?}");
827 assert_eq!(&results.value, &empty_config, "{which:?} {uncommented:?}");
828
829 let unrecognized = results
832 .unrecognized
833 .iter()
834 .map(|k| k.to_string())
835 .collect_vec();
836
837 eprintln!(
838 "parsing of {which:?} uncommented={uncommented:?}, unrecognized={unrecognized:#?}"
839 );
840
841 let reports =
842 Itertools::merge_join_by(unrecognized.iter(), exceptions.iter(), |u, e| {
843 u.as_str().cmp(&e.key)
844 })
845 .filter_map(|eob| analyse_joined_info(which, uncommented, eob).err())
846 .collect_vec();
847
848 if !reports.is_empty() {
849 let reports = reports.iter().fold(String::new(), |mut out, (k, s)| {
850 writeln!(out, " {}: {}", s, k).unwrap();
851 out
852 });
853
854 panic!(
855 r"
856mismatch: results of parsing example files (& vs declared exceptions):
857example config file {which:?}, uncommented={uncommented:?}
858{reports}
859"
860 );
861 }
862
863 results.value
864 };
865
866 let _ = parses_to_defaults(ARTI_EXAMPLE_CONFIG, WhichExample::New, false);
867 let _ = parses_to_defaults(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, false);
868
869 let built_default = (
870 ArtiConfigBuilder::default().build().unwrap(),
871 TorClientConfigBuilder::default().build().unwrap(),
872 );
873
874 let parsed = parses_to_defaults(
875 &uncomment_example_settings(ARTI_EXAMPLE_CONFIG),
876 WhichExample::New,
877 true,
878 );
879 let parsed_old = parses_to_defaults(
880 &uncomment_example_settings(OLDEST_SUPPORTED_CONFIG),
881 WhichExample::Old,
882 true,
883 );
884
885 assert_eq!(&parsed, &built_default);
886 assert_eq!(&parsed_old, &built_default);
887
888 assert_eq!(&default, &built_default);
889 }
890
891 fn exhaustive_1(example_file: &str, which: WhichExample, deprecated: &[String]) {
923 use InExample::*;
924 use serde_json::Value as JsValue;
925 use std::collections::BTreeSet;
926
927 let example = uncomment_example_settings(example_file);
928 let example: toml::Value = toml::from_str(&example).unwrap();
929 let example = serde_json::to_value(example).unwrap();
931 let exhausts = [
941 serde_json::to_value(TorClientConfig::builder()).unwrap(),
942 serde_json::to_value(ArtiConfig::builder()).unwrap(),
943 ];
944
945 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, derive_more::Display)]
948 enum ProblemKind {
949 #[display("recognised by serialisation, but missing from example config file")]
950 MissingFromExample,
951 #[display("expected that example config file should contain have this as a table")]
952 ExpectedTableInExample,
953 #[display(
954 "declared exception says this key should be recognised but not in file, but that doesn't seem to be the case"
955 )]
956 UnusedException,
957 }
958
959 #[derive(Default, Debug)]
960 struct Walk {
961 current_path: Vec<String>,
962 problems: Vec<(String, ProblemKind)>,
963 }
964
965 impl Walk {
966 fn bad(&mut self, kind: ProblemKind) {
968 self.problems.push((self.current_path.join("."), kind));
969 }
970
971 fn walk<const E: usize>(
978 &mut self,
979 example: Option<&JsValue>,
980 exhausts: [Option<&JsValue>; E],
981 ) {
982 assert! { exhausts.into_iter().any(|e| e.is_some()) }
983
984 let example = if let Some(e) = example {
985 e
986 } else {
987 self.bad(ProblemKind::MissingFromExample);
988 return;
989 };
990
991 let tables = exhausts.map(|e| e?.as_object());
992
993 let table_keys = tables
995 .iter()
996 .flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten())
997 .collect::<BTreeSet<String>>();
998
999 for key in table_keys {
1000 let example = if let Some(e) = example.as_object() {
1001 e
1002 } else {
1003 self.bad(ProblemKind::ExpectedTableInExample);
1006 continue;
1007 };
1008
1009 self.current_path.push(key.clone());
1011 self.walk(example.get(&key), tables.map(|t| t?.get(&key)));
1012 self.current_path.pop().unwrap();
1013 }
1014 }
1015 }
1016
1017 let exhausts = exhausts.iter().map(Some).collect_vec().try_into().unwrap();
1018
1019 let mut walk = Walk::default();
1020 walk.walk::<2>(Some(&example), exhausts);
1021 let mut problems = walk.problems;
1022
1023 #[derive(Debug, Copy, Clone)]
1025 struct DefinitelyRecognized;
1026
1027 let expect_missing = declared_config_exceptions()
1028 .iter()
1029 .filter_map(|exc| {
1030 let definitely = match (exc.in_example(which), exc.in_code) {
1031 (Present, _) => return None, (_, Some(false)) => return None, (Absent, Some(true)) => Some(DefinitelyRecognized),
1034 (Absent, None) => None, };
1036 Some((exc.key.clone(), definitely))
1037 })
1038 .collect_vec();
1039 dbg!(&expect_missing);
1040
1041 let expect_missing: Vec<(String, Option<DefinitelyRecognized>)> = expect_missing
1050 .iter()
1051 .cloned()
1052 .filter({
1053 let original: HashSet<_> = expect_missing.iter().map(|(k, _)| k.clone()).collect();
1054 move |(found, _)| {
1055 !found
1056 .match_indices('.')
1057 .any(|(doti, _)| original.contains(&found[0..doti]))
1058 }
1059 })
1060 .collect_vec();
1061 dbg!(&expect_missing);
1062
1063 for (exp, definitely) in expect_missing {
1064 let was = problems.len();
1065 problems.retain(|(path, _)| path != &exp);
1066 if problems.len() == was && definitely.is_some() {
1067 problems.push((exp, ProblemKind::UnusedException));
1068 }
1069 }
1070
1071 let problems = problems
1072 .into_iter()
1073 .filter(|(key, _kind)| !deprecated.iter().any(|dep| key == dep))
1074 .map(|(path, m)| format!(" config key {:?}: {}", path, m))
1075 .collect_vec();
1076
1077 assert!(
1080 problems.is_empty(),
1081 "example config {which:?} exhaustiveness check failed: {}\n-----8<-----\n{}\n-----8<-----\n",
1082 problems.join("\n"),
1083 example_file,
1084 );
1085 }
1086
1087 #[test]
1088 fn exhaustive() {
1089 let mut deprecated = vec![];
1090 <(ArtiConfig, TorClientConfig) as tor_config::load::Resolvable>::enumerate_deprecated_keys(
1091 &mut |l| {
1092 for k in l {
1093 deprecated.push(k.to_string());
1094 }
1095 },
1096 );
1097 let deprecated = deprecated.iter().cloned().collect_vec();
1098
1099 exhaustive_1(ARTI_EXAMPLE_CONFIG, WhichExample::New, &deprecated);
1104
1105 exhaustive_1(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, &deprecated);
1112 }
1113
1114 #[cfg_attr(feature = "pt-client", allow(dead_code))]
1116 fn expect_err_contains(err: ConfigResolveError, exp: &str) {
1117 use std::error::Error as StdError;
1118 let err: Box<dyn StdError> = Box::new(err);
1119 let err = tor_error::Report(err).to_string();
1120 assert!(
1121 err.contains(exp),
1122 "wrong message, got {:?}, exp {:?}",
1123 err,
1124 exp,
1125 );
1126 }
1127
1128 #[test]
1129 fn bridges() {
1130 let filter_examples = |#[allow(unused_mut)] mut examples: ExampleSectionLines| -> _ {
1144 if cfg!(all(feature = "bridge-client", not(feature = "pt-client"))) {
1146 let looks_like_addr =
1147 |l: &str| l.starts_with(|c: char| c.is_ascii_digit() || c == '[');
1148 examples.lines.retain(|l| looks_like_addr(l));
1149 }
1150
1151 examples
1152 };
1153
1154 let resolve_examples = |examples: &ExampleSectionLines| {
1159 #[cfg(all(feature = "bridge-client", not(feature = "pt-client")))]
1161 {
1162 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1163 expect_err_contains(err, "support disabled in cargo features");
1164 }
1165
1166 let examples = filter_examples(examples.clone());
1167
1168 #[cfg(feature = "bridge-client")]
1169 {
1170 examples.resolve::<TorClientConfig>().unwrap()
1171 }
1172
1173 #[cfg(not(feature = "bridge-client"))]
1174 {
1175 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1176 expect_err_contains(err, "support disabled in cargo features");
1177 ((),)
1179 }
1180 };
1181
1182 let mut examples = ExampleSectionLines::from_section("bridges");
1184 examples.narrow((r#"^# For example:"#, true), NARROW_NONE);
1185
1186 let compare = {
1187 let mut examples = examples.clone();
1189 examples.narrow((r#"^# bridges = '''"#, true), (r#"^# '''"#, true));
1190 examples.uncomment();
1191
1192 let parsed = resolve_examples(&examples);
1193
1194 examples.lines.remove(0);
1197 examples.lines.remove(examples.lines.len() - 1);
1198 examples.expect_lines(3);
1200
1201 #[cfg(feature = "bridge-client")]
1203 {
1204 let examples = filter_examples(examples);
1205 let mut built = TorClientConfig::builder();
1206 for l in &examples.lines {
1207 built.bridges().bridges().push(l.trim().parse().expect(l));
1208 }
1209 let built = built.build().unwrap();
1210
1211 assert_eq!(&parsed, &built);
1212 }
1213
1214 parsed
1215 };
1216
1217 {
1219 examples.narrow((r#"^# bridges = \["#, true), (r#"^# \]"#, true));
1220 examples.uncomment();
1221 let parsed = resolve_examples(&examples);
1222 assert_eq!(&parsed, &compare);
1223 }
1224 }
1225
1226 #[test]
1227 fn transports() {
1228 let mut file =
1234 ExampleSectionLines::from_markers("# An example managed pluggable transport", "[");
1235 file.lines.retain(|line| line.starts_with("# "));
1236 file.uncomment();
1237
1238 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1239 let cfg_got = result.unwrap();
1240
1241 #[cfg(feature = "pt-client")]
1242 {
1243 use arti_client::config::{BridgesConfig, pt::TransportConfig};
1244 use tor_config_path::CfgPath;
1245
1246 let bridges_got: &BridgesConfig = cfg_got.0.as_ref();
1247
1248 let mut bld = BridgesConfig::builder();
1250 {
1251 let mut b = TransportConfig::builder();
1252 b.protocols(vec!["obfs4".parse().unwrap(), "obfs5".parse().unwrap()]);
1253 b.path(CfgPath::new("/usr/bin/obfsproxy".to_string()));
1254 b.arguments(vec!["-obfs4".to_string(), "-obfs5".to_string()]);
1255 b.run_on_startup(true);
1256 bld.transports().push(b);
1257 }
1258 {
1259 let mut b = TransportConfig::builder();
1260 b.protocols(vec!["obfs4".parse().unwrap()]);
1261 b.proxy_addr("127.0.0.1:31337".parse().unwrap());
1262 bld.transports().push(b);
1263 }
1264
1265 let bridges_expected = bld.build().unwrap();
1266 assert_eq!(&bridges_expected, bridges_got);
1267 }
1268 }
1269
1270 #[test]
1271 fn memquota() {
1272 let mut file = ExampleSectionLines::from_section("system");
1275 file.lines.retain(|line| line.starts_with("# memory."));
1276 file.uncomment();
1277
1278 let result = file.resolve_return_results::<(TorClientConfig, ArtiConfig)>();
1279
1280 let result = result.unwrap();
1281
1282 assert_eq!(result.unrecognized, []);
1284 assert_eq!(result.deprecated, []);
1285
1286 let inner: &tor_memquota::testing::ConfigInner =
1287 result.value.0.system_memory().inner().unwrap();
1288
1289 let defaulted_low = tor_memquota::Config::builder()
1292 .max(*inner.max)
1293 .build()
1294 .unwrap();
1295 let inner_defaulted_low = defaulted_low.inner().unwrap();
1296 assert_eq!(inner, inner_defaulted_low);
1297 }
1298
1299 #[test]
1300 fn metrics() {
1301 let mut file = ExampleSectionLines::from_section("metrics");
1303 file.lines
1304 .retain(|line| line.starts_with("# prometheus."));
1305 file.uncomment();
1306
1307 let result = file
1308 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1309 .unwrap();
1310
1311 assert_eq!(result.unrecognized, []);
1313 assert_eq!(result.deprecated, []);
1314
1315 assert_eq!(
1317 result
1318 .value
1319 .1
1320 .metrics
1321 .prometheus
1322 .listen
1323 .single_address_legacy()
1324 .unwrap(),
1325 Some("127.0.0.1:9035".parse().unwrap()),
1326 );
1327
1328 }
1331
1332 #[test]
1333 fn onion_services() {
1334 let mut file = ExampleSectionLines::from_markers("##### ONION SERVICES", "##### RPC");
1338 file.lines.retain(|line| line.starts_with("# "));
1339 file.uncomment();
1340
1341 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1342 #[cfg(feature = "onion-service-service")]
1343 {
1344 let svc_expected = {
1345 use tor_hsrproxy::config::*;
1346 let mut b = OnionServiceProxyConfigBuilder::default();
1347 b.service().nickname("allium-cepa".parse().unwrap());
1348 b.proxy().proxy_ports().push(ProxyRule::new(
1349 ProxyPattern::one_port(80).unwrap(),
1350 ProxyAction::Forward(
1351 Encapsulation::Simple,
1352 TargetAddr::Inet("127.0.0.1:10080".parse().unwrap()),
1353 ),
1354 ));
1355 b.proxy().proxy_ports().push(ProxyRule::new(
1356 ProxyPattern::one_port(22).unwrap(),
1357 ProxyAction::DestroyCircuit,
1358 ));
1359 b.proxy().proxy_ports().push(ProxyRule::new(
1360 ProxyPattern::one_port(265).unwrap(),
1361 ProxyAction::IgnoreStream,
1362 ));
1363 b.proxy().proxy_ports().push(ProxyRule::new(
1373 ProxyPattern::one_port(443).unwrap(),
1374 ProxyAction::RejectStream,
1375 ));
1376 b.proxy().proxy_ports().push(ProxyRule::new(
1377 ProxyPattern::all_ports(),
1378 ProxyAction::DestroyCircuit,
1379 ));
1380
1381 #[cfg(feature = "restricted-discovery")]
1382 {
1383 const ALICE_KEY: &str =
1384 "descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA";
1385 const BOB_KEY: &str =
1386 "descriptor:x25519:b5zqgtpermmuda6vc63lhjuf5ihpokjmuk26ly2xksf7vg52aesq";
1387 for (nickname, key) in [("alice", ALICE_KEY), ("bob", BOB_KEY)] {
1388 b.service()
1389 .restricted_discovery()
1390 .enabled(true)
1391 .static_keys()
1392 .access()
1393 .push((
1394 HsClientNickname::from_str(nickname).unwrap(),
1395 HsClientDescEncKey::from_str(key).unwrap(),
1396 ));
1397 }
1398 let mut dir = DirectoryKeyProviderBuilder::default();
1399 dir.path(CfgPath::new(
1400 "/var/lib/tor/hidden_service/authorized_clients".to_string(),
1401 ));
1402
1403 b.service()
1404 .restricted_discovery()
1405 .key_dirs()
1406 .access()
1407 .push(dir);
1408 }
1409
1410 b.build().unwrap()
1411 };
1412
1413 cfg_if::cfg_if! {
1414 if #[cfg(feature = "restricted-discovery")] {
1415 let cfg = result.unwrap();
1416 let services = cfg.1.onion_services;
1417 assert_eq!(services.len(), 1);
1418 let svc = services.values().next().unwrap();
1419 assert_eq!(svc, &svc_expected);
1420 } else {
1421 expect_err_contains(
1422 result.unwrap_err(),
1423 "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
1424 );
1425 }
1426 }
1427 }
1428 #[cfg(not(feature = "onion-service-service"))]
1429 {
1430 expect_err_contains(result.unwrap_err(), "no support for running onion services");
1431 }
1432 }
1433
1434 #[cfg(feature = "rpc")]
1435 #[test]
1436 fn rpc_defaults() {
1437 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1438 file.lines
1442 .retain(|line| line.starts_with("# ") && !line.starts_with("# "));
1443 file.uncomment();
1444
1445 let parsed = file
1446 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1447 .unwrap();
1448 assert!(parsed.unrecognized.is_empty());
1449 assert!(parsed.deprecated.is_empty());
1450 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1451 let rpc_default = RpcConfig::default();
1452 assert_eq!(rpc_parsed, &rpc_default);
1453 }
1454
1455 #[cfg(feature = "rpc")]
1456 #[test]
1457 fn rpc_full() {
1458 use crate::rpc::listener::{ConnectPointOptionsBuilder, RpcListenerSetConfigBuilder};
1459
1460 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1462 file.lines
1464 .retain(|line| line.starts_with("# ") && !line.contains("file ="));
1465 file.uncomment();
1466
1467 let parsed = file
1468 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1469 .unwrap();
1470 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1471
1472 let expected = {
1473 let mut bld_opts = ConnectPointOptionsBuilder::default();
1474 bld_opts.enable(false);
1475
1476 let mut bld_set = RpcListenerSetConfigBuilder::default();
1477 bld_set.dir(CfgPath::new("${HOME}/.my_connect_files/".to_string()));
1478 bld_set.listener_options().enable(true);
1479 bld_set
1480 .file_options()
1481 .insert("bad_file.json".to_string(), bld_opts);
1482
1483 let mut bld = RpcConfigBuilder::default();
1484 bld.listen().insert("label".to_string(), bld_set);
1485 bld.build().unwrap()
1486 };
1487
1488 assert_eq!(&expected, rpc_parsed);
1489 }
1490
1491 #[derive(Debug, Clone)]
1499 struct ExampleSectionLines {
1500 section: String,
1503 lines: Vec<String>,
1505 }
1506
1507 type NarrowInstruction<'s> = (&'s str, bool);
1510 const NARROW_NONE: NarrowInstruction<'static> = ("?<none>", false);
1512
1513 impl ExampleSectionLines {
1514 fn from_section(section: &str) -> Self {
1518 Self::from_markers(format!("[{section}]"), "[")
1519 }
1520
1521 fn from_markers<S, E>(start: S, end: E) -> Self
1532 where
1533 S: AsRef<str>,
1534 E: AsRef<str>,
1535 {
1536 let (start, end) = (start.as_ref(), end.as_ref());
1537 let mut lines = ARTI_EXAMPLE_CONFIG
1538 .lines()
1539 .skip_while(|line| !line.starts_with(start))
1540 .peekable();
1541 let section = lines
1542 .next_if(|l0| l0.starts_with('['))
1543 .map(|section| section.to_owned())
1544 .unwrap_or_default();
1545 let lines = lines
1546 .take_while(|line| !line.starts_with(end))
1547 .map(|l| l.to_owned())
1548 .collect_vec();
1549
1550 Self { section, lines }
1551 }
1552
1553 fn narrow(&mut self, start: NarrowInstruction, end: NarrowInstruction) {
1556 let find_index = |(re, include), start_pos, exactly_one: bool, adjust: [isize; 2]| {
1557 if (re, include) == NARROW_NONE {
1558 return None;
1559 }
1560
1561 let re = Regex::new(re).expect(re);
1562 let i = self
1563 .lines
1564 .iter()
1565 .enumerate()
1566 .skip(start_pos)
1567 .filter(|(_, l)| re.is_match(l))
1568 .map(|(i, _)| i);
1569 let i = if exactly_one {
1570 i.clone().exactly_one().unwrap_or_else(|_| {
1571 panic!("RE={:?} I={:#?} L={:#?}", re, i.collect_vec(), &self.lines)
1572 })
1573 } else {
1574 i.clone().next()?
1575 };
1576
1577 let adjust = adjust[usize::from(include)];
1578 let i = (i as isize + adjust) as usize;
1579 Some(i)
1580 };
1581
1582 eprint!("narrow {:?} {:?}: ", start, end);
1583 let start = find_index(start, 0, true, [1, 0]).unwrap_or(0);
1584 let end = find_index(end, start + 1, false, [0, 1]).unwrap_or(self.lines.len());
1585 eprintln!("{:?} {:?}", start, end);
1586 assert!(start < end, "empty, from {:#?}", &self.lines);
1588 self.lines = self.lines.drain(..).take(end).skip(start).collect_vec();
1589 }
1590
1591 fn expect_lines(&self, n: usize) {
1593 assert_eq!(self.lines.len(), n);
1594 }
1595
1596 fn uncomment(&mut self) {
1598 self.strip_prefix("#");
1599 }
1600
1601 fn strip_prefix(&mut self, prefix: &str) {
1608 for l in &mut self.lines {
1609 if !l.starts_with('[') {
1610 *l = l.strip_prefix(prefix).expect(l).to_string();
1611 }
1612 }
1613 }
1614
1615 fn build_string(&self) -> String {
1617 chain!(iter::once(&self.section), self.lines.iter(),).join("\n")
1618 }
1619
1620 fn parse(&self) -> tor_config::ConfigurationTree {
1623 let s = self.build_string();
1624 eprintln!("parsing\n --\n{}\n --", &s);
1625 let mut sources = tor_config::ConfigurationSources::new_empty();
1626 sources.push_source(
1627 tor_config::ConfigurationSource::from_verbatim(s.clone()),
1628 tor_config::sources::MustRead::MustRead,
1629 );
1630 sources.load().expect(&s)
1631 }
1632
1633 fn resolve<R: tor_config::load::Resolvable>(&self) -> Result<R, ConfigResolveError> {
1634 tor_config::load::resolve(self.parse())
1635 }
1636
1637 fn resolve_return_results<R: tor_config::load::Resolvable>(
1638 &self,
1639 ) -> Result<ResolutionResults<R>, ConfigResolveError> {
1640 tor_config::load::resolve_return_results(self.parse())
1641 }
1642 }
1643
1644 #[test]
1647 fn builder() {
1648 use tor_config_path::CfgPath;
1649 let sec = std::time::Duration::from_secs(1);
1650
1651 let mut authorities = dir::AuthorityContacts::builder();
1652 authorities.v3idents().push([22; 20].into());
1653
1654 let mut fallback = dir::FallbackDir::builder();
1655 fallback
1656 .rsa_identity([23; 20].into())
1657 .ed_identity([99; 32].into())
1658 .orports()
1659 .push("127.0.0.7:7".parse().unwrap());
1660
1661 let mut bld = ArtiConfig::builder();
1662 let mut bld_tor = TorClientConfig::builder();
1663
1664 bld.proxy().socks_listen(Listen::new_localhost(9999));
1665 bld.logging().console("warn");
1666
1667 *bld_tor.tor_network().authorities() = authorities;
1668 bld_tor.tor_network().set_fallback_caches(vec![fallback]);
1669 bld_tor
1670 .storage()
1671 .cache_dir(CfgPath::new("/var/tmp/foo".to_owned()))
1672 .state_dir(CfgPath::new("/var/tmp/bar".to_owned()));
1673 bld_tor.download_schedule().retry_certs().attempts(10);
1674 bld_tor.download_schedule().retry_certs().initial_delay(sec);
1675 bld_tor.download_schedule().retry_certs().parallelism(3);
1676 bld_tor.download_schedule().retry_microdescs().attempts(30);
1677 bld_tor
1678 .download_schedule()
1679 .retry_microdescs()
1680 .initial_delay(10 * sec);
1681 bld_tor
1682 .download_schedule()
1683 .retry_microdescs()
1684 .parallelism(9);
1685 bld_tor
1686 .override_net_params()
1687 .insert("wombats-per-quokka".to_owned(), 7);
1688 bld_tor
1689 .path_rules()
1690 .ipv4_subnet_family_prefix(20)
1691 .ipv6_subnet_family_prefix(48);
1692 bld_tor.preemptive_circuits().disable_at_threshold(12);
1693 bld_tor
1694 .preemptive_circuits()
1695 .set_initial_predicted_ports(vec![80, 443]);
1696 bld_tor
1697 .preemptive_circuits()
1698 .prediction_lifetime(Duration::from_secs(3600))
1699 .min_exit_circs_for_port(2);
1700 bld_tor
1701 .circuit_timing()
1702 .max_dirtiness(90 * sec)
1703 .request_timeout(10 * sec)
1704 .request_max_retries(22)
1705 .request_loyalty(3600 * sec);
1706 bld_tor.address_filter().allow_local_addrs(true);
1707
1708 let val = bld.build().unwrap();
1709
1710 assert_ne!(val, ArtiConfig::default());
1711 }
1712
1713 #[test]
1714 fn articonfig_application() {
1715 let config = ArtiConfig::default();
1716
1717 let application = config.application();
1718 assert_eq!(&config.application, application);
1719 }
1720
1721 #[test]
1722 fn articonfig_logging() {
1723 let config = ArtiConfig::default();
1724
1725 let logging = config.logging();
1726 assert_eq!(&config.logging, logging);
1727 }
1728
1729 #[test]
1730 fn articonfig_proxy() {
1731 let config = ArtiConfig::default();
1732
1733 let proxy = config.proxy();
1734 assert_eq!(&config.proxy, proxy);
1735 }
1736
1737 fn ports_listen(
1741 f: &str,
1742 get_listen: &dyn Fn(&ArtiConfig) -> &Listen,
1743 bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option<Listen>,
1744 setter_listen: &dyn Fn(&mut ArtiConfigBuilder, Listen) -> &mut ProxyConfigBuilder,
1745 ) {
1746 let from_toml = |s: &str| -> ArtiConfigBuilder {
1747 let cfg: toml::Value = toml::from_str(dbg!(s)).unwrap();
1748 let cfg: ArtiConfigBuilder = cfg.try_into().unwrap();
1749 cfg
1750 };
1751
1752 let chk = |cfg: &ArtiConfigBuilder, expected: &Listen| {
1753 dbg!(bld_get_listen(cfg));
1754 let cfg = cfg.build().unwrap();
1755 assert_eq!(get_listen(&cfg), expected);
1756 };
1757
1758 let check_setters = |port, expected: &_| {
1759 let cfg = ArtiConfig::builder();
1760 for listen in match port {
1761 None => vec![Listen::new_none(), Listen::new_localhost(0)],
1762 Some(port) => vec![Listen::new_localhost(port)],
1763 } {
1764 let mut cfg = cfg.clone();
1765 setter_listen(&mut cfg, dbg!(listen));
1766 chk(&cfg, expected);
1767 }
1768 };
1769
1770 {
1771 let expected = Listen::new_localhost(100);
1772
1773 let cfg = from_toml(&format!("proxy.{}_listen = 100", f));
1774 assert_eq!(bld_get_listen(&cfg), &Some(Listen::new_localhost(100)));
1775 chk(&cfg, &expected);
1776
1777 check_setters(Some(100), &expected);
1778 }
1779
1780 {
1781 let expected = Listen::new_none();
1782
1783 let cfg = from_toml(&format!("proxy.{}_listen = 0", f));
1784 chk(&cfg, &expected);
1785
1786 check_setters(None, &expected);
1787 }
1788 }
1789
1790 #[test]
1791 fn ports_listen_socks() {
1792 ports_listen(
1793 "socks",
1794 &|cfg| &cfg.proxy.socks_listen,
1795 &|bld| &bld.proxy.socks_listen,
1796 &|bld, arg| bld.proxy.socks_listen(arg),
1797 );
1798 }
1799
1800 #[test]
1801 fn ports_listen_dns() {
1802 ports_listen(
1803 "dns",
1804 &|cfg| &cfg.proxy.dns_listen,
1805 &|bld| &bld.proxy.dns_listen,
1806 &|bld, arg| bld.proxy.dns_listen(arg),
1807 );
1808 }
1809}