1use paste::paste;
6
7use derive_builder::Builder;
8use serde::{Deserialize, Serialize};
9
10#[cfg(feature = "onion-service-service")]
11use crate::onion_proxy::{
12 OnionServiceProxyConfigBuilder, OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder,
13};
14#[cfg(not(feature = "onion-service-service"))]
15use crate::onion_proxy_disabled::{OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder};
16#[cfg(feature = "rpc")]
17#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
18pub use crate::rpc::{RpcConfig, RpcConfigBuilder};
19use arti_client::TorClientConfig;
20#[cfg(feature = "onion-service-service")]
21use tor_config::define_list_builder_accessors;
22use tor_config::resolve_alternative_specs;
23pub(crate) use tor_config::{impl_standard_builder, ConfigBuildError, Listen};
24
25use crate::{LoggingConfig, LoggingConfigBuilder};
26
27pub const ARTI_EXAMPLE_CONFIG: &str = concat!(include_str!("./arti-example-config.toml"));
32
33#[cfg(test)]
50const OLDEST_SUPPORTED_CONFIG: &str = concat!(include_str!("./oldest-supported-config.toml"),);
51
52#[derive(Debug, Clone, Builder, Eq, PartialEq)]
54#[builder(build_fn(error = "ConfigBuildError"))]
55#[builder(derive(Debug, Serialize, Deserialize))]
56pub 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#[deprecated = "This macro is only for supporting old _port options! Don't use it for new options."]
101macro_rules! resolve_listen_port {
102 { $self:expr, $field:ident, $def_port:expr } => { paste!{
103 resolve_alternative_specs(
104 [
105 (
106 concat!(stringify!($field), "_listen"),
107 $self.[<$field _listen>].clone(),
108 ),
109 (
110 concat!(stringify!($field), "_port"),
111 $self.[<$field _port>].map(Listen::new_localhost_optional),
112 ),
113 ],
114 || Listen::new_localhost($def_port),
115 )?
116 } }
117}
118
119#[derive(Debug, Clone, Builder, Eq, PartialEq)]
121#[builder(build_fn(error = "ConfigBuildError"))]
122#[builder(derive(Debug, Serialize, Deserialize))]
123#[allow(clippy::option_option)] pub struct ProxyConfig {
125 #[builder(field(build = r#"#[allow(deprecated)]
127 // We use this deprecated macro to instantiate the legacy socks_port option.
128 { resolve_listen_port!(self, socks, 9150) }
129 "#))]
130 pub(crate) socks_listen: Listen,
131
132 #[builder(
138 setter(strip_option),
139 field(type = "Option<Option<u16>>", build = "()")
140 )]
141 #[builder_setter_attr(deprecated)]
142 pub(crate) socks_port: (),
143
144 #[builder(field(build = r#"#[allow(deprecated)]
146 // We use this deprecated macro to instantiate the legacy dns_port option.
147 { resolve_listen_port!(self, dns, 0) }
148 "#))]
149 pub(crate) dns_listen: Listen,
150
151 #[builder(
157 setter(strip_option),
158 field(type = "Option<Option<u16>>", build = "()")
159 )]
160 #[builder_setter_attr(deprecated)]
161 pub(crate) dns_port: (),
162}
163impl_standard_builder! { ProxyConfig }
164
165#[derive(Debug, Clone, Builder, Eq, PartialEq)]
179#[builder(build_fn(error = "ConfigBuildError"))]
180#[builder(derive(Debug, Serialize, Deserialize))]
181#[non_exhaustive]
182pub struct SystemConfig {
183 #[builder(setter(into), default = "default_max_files()")]
185 pub(crate) max_files: u64,
186}
187impl_standard_builder! { SystemConfig }
188
189fn default_max_files() -> u64 {
191 16384
192}
193
194#[derive(Debug, Builder, Clone, Eq, PartialEq)]
209#[builder(derive(Serialize, Deserialize, Debug))]
210#[builder(build_fn(private, name = "build_unvalidated", error = "ConfigBuildError"))]
211pub struct ArtiConfig {
212 #[builder(sub_builder(fn_name = "build"))]
214 #[builder_field_attr(serde(default))]
215 application: ApplicationConfig,
216
217 #[builder(sub_builder(fn_name = "build"))]
219 #[builder_field_attr(serde(default))]
220 proxy: ProxyConfig,
221
222 #[builder(sub_builder(fn_name = "build"))]
224 #[builder_field_attr(serde(default))]
225 logging: LoggingConfig,
226
227 #[builder(sub_builder(fn_name = "build"))]
229 #[builder_field_attr(serde(default))]
230 pub(crate) metrics: MetricsConfig,
231
232 #[cfg(feature = "rpc")]
234 #[builder(sub_builder(fn_name = "build"))]
235 #[builder_field_attr(serde(default))]
236 pub(crate) rpc: RpcConfig,
237
238 #[cfg(not(feature = "rpc"))]
250 #[builder_field_attr(serde(default))]
251 #[builder(field(type = "Option<toml::Value>", build = "()"), private)]
252 rpc: (),
253
254 #[builder(sub_builder(fn_name = "build"))]
260 #[builder_field_attr(serde(default))]
261 pub(crate) system: SystemConfig,
262
263 #[builder(sub_builder(fn_name = "build"), setter(custom))]
272 #[builder_field_attr(serde(default))]
273 pub(crate) onion_services: OnionServiceProxyConfigMap,
274}
275
276impl_standard_builder! { ArtiConfig }
277
278impl ArtiConfigBuilder {
279 pub fn build(&self) -> Result<ArtiConfig, ConfigBuildError> {
281 #[cfg_attr(not(feature = "onion-service-service"), allow(unused_mut))]
282 let mut config = self.build_unvalidated()?;
283 #[cfg(feature = "onion-service-service")]
284 for svc in config.onion_services.values_mut() {
285 *svc.svc_cfg
287 .restricted_discovery_mut()
288 .watch_configuration_mut() = config.application.watch_configuration;
289 }
290
291 #[cfg(not(feature = "rpc"))]
292 if self.rpc.is_some() {
293 tracing::warn!("rpc options were set, but Arti was built without support for rpc.");
294 }
295
296 Ok(config)
297 }
298}
299
300impl tor_config::load::TopLevel for ArtiConfig {
301 type Builder = ArtiConfigBuilder;
302 const DEPRECATED_KEYS: &'static [&'static str] = &["proxy.socks_port", "proxy.dns_port"];
303}
304
305#[cfg(feature = "onion-service-service")]
306define_list_builder_accessors! {
307 struct ArtiConfigBuilder {
308 pub(crate) onion_services: [OnionServiceProxyConfigBuilder],
309 }
310}
311
312pub type ArtiCombinedConfig = (ArtiConfig, TorClientConfig);
316
317#[derive(Debug, Clone, Builder, Eq, PartialEq)]
319#[builder(build_fn(error = "ConfigBuildError"))]
320#[builder(derive(Debug, Serialize, Deserialize))]
321pub struct MetricsConfig {
322 #[builder(sub_builder(fn_name = "build"))]
324 #[builder_field_attr(serde(default))]
325 pub(crate) prometheus: PrometheusConfig,
326}
327impl_standard_builder! { MetricsConfig }
328
329#[derive(Debug, Clone, Builder, Eq, PartialEq)]
331#[builder(build_fn(error = "ConfigBuildError"))]
332#[builder(derive(Debug, Serialize, Deserialize))]
333#[allow(clippy::option_option)] pub struct PrometheusConfig {
335 #[builder(default)]
344 #[builder_field_attr(serde(default))]
345 pub(crate) listen: Listen,
346}
347impl_standard_builder! { PrometheusConfig }
348
349impl ArtiConfig {
350 pub fn application(&self) -> &ApplicationConfig {
352 &self.application
353 }
354
355 pub fn logging(&self) -> &LoggingConfig {
357 &self.logging
358 }
359
360 pub fn proxy(&self) -> &ProxyConfig {
362 &self.proxy
363 }
364
365 #[cfg(feature = "rpc")]
367 pub fn rpc(&self) -> &RpcConfig {
368 &self.rpc
369 }
370}
371
372#[cfg(test)]
373mod test {
374 #![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_duration_subtraction)]
384 #![allow(clippy::useless_vec)]
385 #![allow(clippy::needless_pass_by_value)]
386 #![allow(clippy::iter_overeager_cloned)]
389 #![cfg_attr(not(feature = "pt-client"), allow(dead_code))]
391
392 use arti_client::config::dir;
393 use arti_client::config::TorClientConfigBuilder;
394 use itertools::{chain, EitherOrBoth, Itertools};
395 use regex::Regex;
396 use std::collections::HashSet;
397 use std::fmt::Write as _;
398 use std::iter;
399 use std::time::Duration;
400 use tor_config::load::{ConfigResolveError, ResolutionResults};
401 use tor_config_path::CfgPath;
402
403 #[allow(unused_imports)] use tor_error::ErrorReport as _;
405
406 #[cfg(feature = "restricted-discovery")]
407 use {
408 arti_client::HsClientDescEncKey,
409 std::str::FromStr as _,
410 tor_hsservice::config::restricted_discovery::{
411 DirectoryKeyProviderBuilder, HsClientNickname,
412 },
413 };
414
415 use super::*;
416
417 fn uncomment_example_settings(template: &str) -> String {
424 let re = Regex::new(r#"(?m)^\#([^ \n])"#).unwrap();
425 re.replace_all(template, |cap: ®ex::Captures<'_>| -> _ {
426 cap.get(1).unwrap().as_str().to_string()
427 })
428 .into()
429 }
430
431 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
440 enum InExample {
441 Absent,
442 Present,
443 }
444 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
450 enum WhichExample {
451 Old,
452 New,
453 }
454 #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
460 struct ConfigException {
461 key: String,
463 in_old_example: InExample,
465 in_new_example: InExample,
467 in_code: Option<bool>,
469 }
470 impl ConfigException {
471 fn in_example(&self, which: WhichExample) -> InExample {
472 use WhichExample::*;
473 match which {
474 Old => self.in_old_example,
475 New => self.in_new_example,
476 }
477 }
478 }
479
480 const ALL_RELEVANT_FEATURES_ENABLED: bool = cfg!(all(
482 feature = "bridge-client",
483 feature = "pt-client",
484 feature = "onion-service-client",
485 feature = "rpc",
486 ));
487
488 fn declared_config_exceptions() -> Vec<ConfigException> {
490 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
495 enum InCode {
496 Ignored,
498 FeatureDependent,
506 Recognized,
508 }
509 use InCode::*;
510
511 struct InOld;
513 struct InNew;
515
516 let mut out = vec![];
517
518 let mut declare_exceptions = |in_old_example: Option<InOld>,
531 in_new_example: Option<InNew>,
532 in_code: InCode,
533 keys: &[&str]| {
534 let in_code = match in_code {
535 Ignored => Some(false),
536 Recognized => Some(true),
537 FeatureDependent if ALL_RELEVANT_FEATURES_ENABLED => Some(true),
538 FeatureDependent => None,
539 };
540 #[allow(clippy::needless_pass_by_value)] fn in_example<T>(spec: Option<T>) -> InExample {
542 match spec {
543 None => InExample::Absent,
544 Some(_) => InExample::Present,
545 }
546 }
547 let in_old_example = in_example(in_old_example);
548 let in_new_example = in_example(in_new_example);
549 out.extend(keys.iter().cloned().map(|key| ConfigException {
550 key: key.to_owned(),
551 in_old_example,
552 in_new_example,
553 in_code,
554 }));
555 };
556
557 declare_exceptions(
558 None,
559 Some(InNew),
560 Recognized,
561 &[
562 "application.allow_running_as_root",
564 "bridges",
565 "logging.time_granularity",
566 "path_rules.long_lived_ports",
567 "proxy.socks_listen",
568 "proxy.dns_listen",
569 "use_obsolete_software",
570 ],
571 );
572
573 declare_exceptions(
574 None,
575 None,
576 Recognized,
577 &[
578 "tor_network.authorities",
580 "tor_network.fallback_caches",
581 ],
582 );
583
584 declare_exceptions(
585 Some(InOld),
586 Some(InNew),
587 if cfg!(target_family = "windows") {
588 Ignored
589 } else {
590 Recognized
591 },
592 &[
593 "storage.permissions.trust_group",
595 "storage.permissions.trust_user",
596 ],
597 );
598
599 declare_exceptions(
600 None,
601 None, FeatureDependent,
603 &[
604 "bridges.transports", ],
607 );
608
609 declare_exceptions(
610 None,
611 Some(InNew),
612 FeatureDependent,
613 &[
614 "storage.keystore",
616 ],
617 );
618
619 declare_exceptions(
620 None,
621 None, Recognized,
623 &[
624 "system.memory",
626 "system.memory.max",
627 "system.memory.low_water",
628 ],
629 );
630
631 declare_exceptions(
632 None,
633 Some(InNew), Recognized,
635 &["metrics"],
636 );
637
638 declare_exceptions(
639 None,
640 None, Recognized,
642 &[
643 "metrics.prometheus",
645 "metrics.prometheus.listen",
646 ],
647 );
648
649 declare_exceptions(
650 None,
651 Some(InNew),
652 FeatureDependent,
653 &[
654 ],
656 );
657
658 declare_exceptions(
659 None,
660 Some(InNew),
661 FeatureDependent,
662 &[
663 "address_filter.allow_onion_addrs",
665 "circuit_timing.hs_desc_fetch_attempts",
666 "circuit_timing.hs_intro_rend_attempts",
667 ],
668 );
669
670 declare_exceptions(
671 None,
672 None, FeatureDependent,
674 &[
675 "rpc",
677 "rpc.rpc_listen",
678 ],
679 );
680
681 declare_exceptions(
683 None,
684 None,
685 FeatureDependent,
686 &[
687 "onion_services",
689 ],
690 );
691
692 declare_exceptions(
693 None,
694 Some(InNew),
695 FeatureDependent,
696 &[
697 "vanguards",
699 "vanguards.mode",
700 ],
701 );
702
703 declare_exceptions(
705 None,
706 None,
707 FeatureDependent,
708 &[
709 "storage.keystore.ctor",
710 "storage.keystore.ctor.services",
711 "storage.keystore.ctor.clients",
712 ],
713 );
714
715 out.sort();
716
717 let dupes = out.iter().map(|exc| &exc.key).duplicates().collect_vec();
718 assert!(
719 dupes.is_empty(),
720 "duplicate exceptions in configuration {dupes:?}"
721 );
722
723 eprintln!(
724 "declared config exceptions for this configuration:\n{:#?}",
725 &out
726 );
727 out
728 }
729
730 #[test]
731 fn default_config() {
732 use InExample::*;
733
734 let empty_config = tor_config::ConfigurationSources::new_empty()
735 .load()
736 .unwrap();
737 let empty_config: ArtiCombinedConfig = tor_config::resolve(empty_config).unwrap();
738
739 let default = (ArtiConfig::default(), TorClientConfig::default());
740 let exceptions = declared_config_exceptions();
741
742 #[allow(clippy::needless_pass_by_value)] fn analyse_joined_info(
753 which: WhichExample,
754 uncommented: bool,
755 eob: EitherOrBoth<&String, &ConfigException>,
756 ) -> Result<(), (String, String)> {
757 use EitherOrBoth::*;
758 let (key, err) = match eob {
759 Left(found) => (found, "found in example but not processed".into()),
761 Both(found, exc) => {
762 let but = match (exc.in_example(which), exc.in_code, uncommented) {
763 (Absent, _, _) => "but exception entry expected key to be absent",
764 (_, _, false) => "when processing still-commented-out file!",
765 (_, Some(true), _) => {
766 "but an exception entry says it should have been recognised"
767 }
768 (Present, Some(false), true) => return Ok(()), (Present, None, true) => return Ok(()), };
771 (
772 found,
773 format!("parser reported unrecognised config key, {but}"),
774 )
775 }
776 Right(exc) => {
777 let trouble = match (exc.in_example(which), exc.in_code, uncommented) {
782 (Absent, _, _) => return Ok(()), (_, _, false) => return Ok(()), (_, Some(true), _) => return Ok(()), (Present, Some(false), true) => {
786 "expected an 'unknown config key' report but didn't see one"
787 }
788 (Present, None, true) => return Ok(()), };
790 (&exc.key, trouble.into())
791 }
792 };
793 Err((key.clone(), err))
794 }
795
796 let parses_to_defaults = |example: &str, which: WhichExample, uncommented: bool| {
797 let cfg = {
798 let mut sources = tor_config::ConfigurationSources::new_empty();
799 sources.push_source(
800 tor_config::ConfigurationSource::from_verbatim(example.to_string()),
801 tor_config::sources::MustRead::MustRead,
802 );
803 sources.load().unwrap()
804 };
805
806 let results: ResolutionResults<ArtiCombinedConfig> =
808 tor_config::resolve_return_results(cfg).unwrap();
809
810 assert_eq!(&results.value, &default, "{which:?} {uncommented:?}");
811 assert_eq!(&results.value, &empty_config, "{which:?} {uncommented:?}");
812
813 let unrecognized = results
816 .unrecognized
817 .iter()
818 .map(|k| k.to_string())
819 .collect_vec();
820
821 eprintln!(
822 "parsing of {which:?} uncommented={uncommented:?}, unrecognized={unrecognized:#?}"
823 );
824
825 let reports =
826 Itertools::merge_join_by(unrecognized.iter(), exceptions.iter(), |u, e| {
827 u.as_str().cmp(&e.key)
828 })
829 .filter_map(|eob| analyse_joined_info(which, uncommented, eob).err())
830 .collect_vec();
831
832 if !reports.is_empty() {
833 let reports = reports.iter().fold(String::new(), |mut out, (k, s)| {
834 writeln!(out, " {}: {}", s, k).unwrap();
835 out
836 });
837
838 panic!(
839 r"
840mismatch: results of parsing example files (& vs declared exceptions):
841example config file {which:?}, uncommented={uncommented:?}
842{reports}
843"
844 );
845 }
846
847 results.value
848 };
849
850 let _ = parses_to_defaults(ARTI_EXAMPLE_CONFIG, WhichExample::New, false);
851 let _ = parses_to_defaults(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, false);
852
853 let built_default = (
854 ArtiConfigBuilder::default().build().unwrap(),
855 TorClientConfigBuilder::default().build().unwrap(),
856 );
857
858 let parsed = parses_to_defaults(
859 &uncomment_example_settings(ARTI_EXAMPLE_CONFIG),
860 WhichExample::New,
861 true,
862 );
863 let parsed_old = parses_to_defaults(
864 &uncomment_example_settings(OLDEST_SUPPORTED_CONFIG),
865 WhichExample::Old,
866 true,
867 );
868
869 assert_eq!(&parsed, &built_default);
870 assert_eq!(&parsed_old, &built_default);
871
872 assert_eq!(&default, &built_default);
873 }
874
875 fn exhaustive_1(example_file: &str, which: WhichExample, deprecated: &[String]) {
907 use serde_json::Value as JsValue;
908 use std::collections::BTreeSet;
909 use InExample::*;
910
911 let example = uncomment_example_settings(example_file);
912 let example: toml::Value = toml::from_str(&example).unwrap();
913 let example = serde_json::to_value(example).unwrap();
915 let exhausts = [
925 serde_json::to_value(TorClientConfig::builder()).unwrap(),
926 serde_json::to_value(ArtiConfig::builder()).unwrap(),
927 ];
928
929 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, derive_more::Display)]
932 enum ProblemKind {
933 #[display("recognised by serialisation, but missing from example config file")]
934 MissingFromExample,
935 #[display("expected that example config file should contain have this as a table")]
936 ExpectedTableInExample,
937 #[display(
938 "declared exception says this key should be recognised but not in file, but that doesn't seem to be the case"
939 )]
940 UnusedException,
941 }
942
943 #[derive(Default, Debug)]
944 struct Walk {
945 current_path: Vec<String>,
946 problems: Vec<(String, ProblemKind)>,
947 }
948
949 impl Walk {
950 fn bad(&mut self, kind: ProblemKind) {
952 self.problems.push((self.current_path.join("."), kind));
953 }
954
955 fn walk<const E: usize>(
962 &mut self,
963 example: Option<&JsValue>,
964 exhausts: [Option<&JsValue>; E],
965 ) {
966 assert! { exhausts.into_iter().any(|e| e.is_some()) }
967
968 let example = if let Some(e) = example {
969 e
970 } else {
971 self.bad(ProblemKind::MissingFromExample);
972 return;
973 };
974
975 let tables = exhausts.map(|e| e?.as_object());
976
977 let table_keys = tables
979 .iter()
980 .flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten())
981 .collect::<BTreeSet<String>>();
982
983 for key in table_keys {
984 let example = if let Some(e) = example.as_object() {
985 e
986 } else {
987 self.bad(ProblemKind::ExpectedTableInExample);
990 continue;
991 };
992
993 self.current_path.push(key.clone());
995 self.walk(example.get(&key), tables.map(|t| t?.get(&key)));
996 self.current_path.pop().unwrap();
997 }
998 }
999 }
1000
1001 let exhausts = exhausts.iter().map(Some).collect_vec().try_into().unwrap();
1002
1003 let mut walk = Walk::default();
1004 walk.walk::<2>(Some(&example), exhausts);
1005 let mut problems = walk.problems;
1006
1007 #[derive(Debug, Copy, Clone)]
1009 struct DefinitelyRecognized;
1010
1011 let expect_missing = declared_config_exceptions()
1012 .iter()
1013 .filter_map(|exc| {
1014 let definitely = match (exc.in_example(which), exc.in_code) {
1015 (Present, _) => return None, (_, Some(false)) => return None, (Absent, Some(true)) => Some(DefinitelyRecognized),
1018 (Absent, None) => None, };
1020 Some((exc.key.clone(), definitely))
1021 })
1022 .collect_vec();
1023 dbg!(&expect_missing);
1024
1025 let expect_missing: Vec<(String, Option<DefinitelyRecognized>)> = expect_missing
1034 .iter()
1035 .cloned()
1036 .filter({
1037 let original: HashSet<_> = expect_missing.iter().map(|(k, _)| k.clone()).collect();
1038 move |(found, _)| {
1039 !found
1040 .match_indices('.')
1041 .any(|(doti, _)| original.contains(&found[0..doti]))
1042 }
1043 })
1044 .collect_vec();
1045 dbg!(&expect_missing);
1046
1047 for (exp, definitely) in expect_missing {
1048 let was = problems.len();
1049 problems.retain(|(path, _)| path != &exp);
1050 if problems.len() == was && definitely.is_some() {
1051 problems.push((exp, ProblemKind::UnusedException));
1052 }
1053 }
1054
1055 let problems = problems
1056 .into_iter()
1057 .filter(|(key, _kind)| !deprecated.iter().any(|dep| key == dep))
1058 .map(|(path, m)| format!(" config key {:?}: {}", path, m))
1059 .collect_vec();
1060
1061 assert!(
1064 problems.is_empty(),
1065 "example config {which:?} exhaustiveness check failed: {}\n-----8<-----\n{}\n-----8<-----\n",
1066 problems.join("\n"),
1067 example_file,
1068 );
1069 }
1070
1071 #[test]
1072 fn exhaustive() {
1073 let mut deprecated = vec![];
1074 <(ArtiConfig, TorClientConfig) as tor_config::load::Resolvable>::enumerate_deprecated_keys(
1075 &mut |l| {
1076 for k in l {
1077 deprecated.push(k.to_string());
1078 }
1079 },
1080 );
1081 let deprecated = deprecated.iter().cloned().collect_vec();
1082
1083 exhaustive_1(ARTI_EXAMPLE_CONFIG, WhichExample::New, &deprecated);
1088
1089 exhaustive_1(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, &deprecated);
1096 }
1097
1098 #[cfg_attr(feature = "pt-client", allow(dead_code))]
1100 fn expect_err_contains(err: ConfigResolveError, exp: &str) {
1101 use std::error::Error as StdError;
1102 let err: Box<dyn StdError> = Box::new(err);
1103 let err = tor_error::Report(err).to_string();
1104 assert!(
1105 err.contains(exp),
1106 "wrong message, got {:?}, exp {:?}",
1107 err,
1108 exp,
1109 );
1110 }
1111
1112 #[test]
1113 fn bridges() {
1114 let filter_examples = |#[allow(unused_mut)] mut examples: ExampleSectionLines| -> _ {
1128 if cfg!(all(feature = "bridge-client", not(feature = "pt-client"))) {
1130 let looks_like_addr =
1131 |l: &str| l.starts_with(|c: char| c.is_ascii_digit() || c == '[');
1132 examples.lines.retain(|l| looks_like_addr(l));
1133 }
1134
1135 examples
1136 };
1137
1138 let resolve_examples = |examples: &ExampleSectionLines| {
1143 #[cfg(all(feature = "bridge-client", not(feature = "pt-client")))]
1145 {
1146 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1147 expect_err_contains(err, "support disabled in cargo features");
1148 }
1149
1150 let examples = filter_examples(examples.clone());
1151
1152 #[cfg(feature = "bridge-client")]
1153 {
1154 examples.resolve::<TorClientConfig>().unwrap()
1155 }
1156
1157 #[cfg(not(feature = "bridge-client"))]
1158 {
1159 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1160 expect_err_contains(err, "support disabled in cargo features");
1161 ((),)
1163 }
1164 };
1165
1166 let mut examples = ExampleSectionLines::from_section("bridges");
1168 examples.narrow((r#"^# For example:"#, true), NARROW_NONE);
1169
1170 let compare = {
1171 let mut examples = examples.clone();
1173 examples.narrow((r#"^# bridges = '''"#, true), (r#"^# '''"#, true));
1174 examples.uncomment();
1175
1176 let parsed = resolve_examples(&examples);
1177
1178 examples.lines.remove(0);
1181 examples.lines.remove(examples.lines.len() - 1);
1182 examples.expect_lines(3);
1184
1185 #[cfg(feature = "bridge-client")]
1187 {
1188 let examples = filter_examples(examples);
1189 let mut built = TorClientConfig::builder();
1190 for l in &examples.lines {
1191 built.bridges().bridges().push(l.trim().parse().expect(l));
1192 }
1193 let built = built.build().unwrap();
1194
1195 assert_eq!(&parsed, &built);
1196 }
1197
1198 parsed
1199 };
1200
1201 {
1203 examples.narrow((r#"^# bridges = \["#, true), (r#"^# \]"#, true));
1204 examples.uncomment();
1205 let parsed = resolve_examples(&examples);
1206 assert_eq!(&parsed, &compare);
1207 }
1208 }
1209
1210 #[test]
1211 fn transports() {
1212 let mut file =
1218 ExampleSectionLines::from_markers("# An example managed pluggable transport", "[");
1219 file.lines.retain(|line| line.starts_with("# "));
1220 file.uncomment();
1221
1222 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1223 let cfg_got = result.unwrap();
1224
1225 #[cfg(feature = "pt-client")]
1226 {
1227 use arti_client::config::{pt::TransportConfig, BridgesConfig};
1228 use tor_config_path::CfgPath;
1229
1230 let bridges_got: &BridgesConfig = cfg_got.0.as_ref();
1231
1232 let mut bld = BridgesConfig::builder();
1234 {
1235 let mut b = TransportConfig::builder();
1236 b.protocols(vec!["obfs4".parse().unwrap(), "obfs5".parse().unwrap()]);
1237 b.path(CfgPath::new("/usr/bin/obfsproxy".to_string()));
1238 b.arguments(vec!["-obfs4".to_string(), "-obfs5".to_string()]);
1239 b.run_on_startup(true);
1240 bld.transports().push(b);
1241 }
1242 {
1243 let mut b = TransportConfig::builder();
1244 b.protocols(vec!["obfs4".parse().unwrap()]);
1245 b.proxy_addr("127.0.0.1:31337".parse().unwrap());
1246 bld.transports().push(b);
1247 }
1248
1249 let bridges_expected = bld.build().unwrap();
1250 assert_eq!(&bridges_expected, bridges_got);
1251 }
1252 }
1253
1254 #[test]
1255 fn memquota() {
1256 let mut file = ExampleSectionLines::from_section("system");
1259 file.lines.retain(|line| line.starts_with("# memory."));
1260 file.uncomment();
1261
1262 let result = file.resolve_return_results::<(TorClientConfig, ArtiConfig)>();
1263
1264 cfg_if::cfg_if! {
1265 if #[cfg(feature = "memquota")] {
1266 let result = result.unwrap();
1267
1268 assert_eq!(result.unrecognized, []);
1270 assert_eq!(result.deprecated, []);
1271
1272 let inner: &tor_memquota::testing::ConfigInner =
1273 result.value.0.system_memory().inner().unwrap();
1274
1275 let defaulted_low = tor_memquota::Config::builder()
1278 .max(*inner.max)
1279 .build()
1280 .unwrap();
1281 let inner_defaulted_low = defaulted_low.inner().unwrap();
1282 assert_eq!(inner, inner_defaulted_low);
1283 } else if #[cfg(arti_features_precise)] {
1284 let m = result.unwrap_err().report().to_string();
1287 assert!(m.contains("cargo feature `memquota` disabled"), "{m:?}");
1288 } else {
1289 println!("not testing memquota config, cannot figure out if it's enabled");
1293 }
1294 }
1295 }
1296
1297 #[test]
1298 fn metrics() {
1299 let mut file = ExampleSectionLines::from_section("metrics");
1301 file.lines
1302 .retain(|line| line.starts_with("# prometheus."));
1303 file.uncomment();
1304
1305 let result = file
1306 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1307 .unwrap();
1308
1309 assert_eq!(result.unrecognized, []);
1311 assert_eq!(result.deprecated, []);
1312
1313 assert_eq!(
1315 result
1316 .value
1317 .1
1318 .metrics
1319 .prometheus
1320 .listen
1321 .single_address_legacy()
1322 .unwrap(),
1323 Some("127.0.0.1:9035".parse().unwrap()),
1324 );
1325
1326 }
1329
1330 #[test]
1331 fn onion_services() {
1332 let mut file = ExampleSectionLines::from_markers("##### ONION SERVICES", "##### RPC");
1336 file.lines.retain(|line| line.starts_with("# "));
1337 file.uncomment();
1338
1339 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1340 #[cfg(feature = "onion-service-service")]
1341 {
1342 let svc_expected = {
1343 use tor_hsrproxy::config::*;
1344 let mut b = OnionServiceProxyConfigBuilder::default();
1345 b.service().nickname("allium-cepa".parse().unwrap());
1346 b.proxy().proxy_ports().push(ProxyRule::new(
1347 ProxyPattern::one_port(80).unwrap(),
1348 ProxyAction::Forward(
1349 Encapsulation::Simple,
1350 TargetAddr::Inet("127.0.0.1:10080".parse().unwrap()),
1351 ),
1352 ));
1353 b.proxy().proxy_ports().push(ProxyRule::new(
1354 ProxyPattern::one_port(22).unwrap(),
1355 ProxyAction::DestroyCircuit,
1356 ));
1357 b.proxy().proxy_ports().push(ProxyRule::new(
1358 ProxyPattern::one_port(265).unwrap(),
1359 ProxyAction::IgnoreStream,
1360 ));
1361 b.proxy().proxy_ports().push(ProxyRule::new(
1371 ProxyPattern::one_port(443).unwrap(),
1372 ProxyAction::RejectStream,
1373 ));
1374 b.proxy().proxy_ports().push(ProxyRule::new(
1375 ProxyPattern::all_ports(),
1376 ProxyAction::DestroyCircuit,
1377 ));
1378
1379 #[cfg(feature = "restricted-discovery")]
1380 {
1381 const ALICE_KEY: &str =
1382 "descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA";
1383 const BOB_KEY: &str =
1384 "descriptor:x25519:b5zqgtpermmuda6vc63lhjuf5ihpokjmuk26ly2xksf7vg52aesq";
1385 for (nickname, key) in [("alice", ALICE_KEY), ("bob", BOB_KEY)] {
1386 b.service()
1387 .restricted_discovery()
1388 .enabled(true)
1389 .static_keys()
1390 .access()
1391 .push((
1392 HsClientNickname::from_str(nickname).unwrap(),
1393 HsClientDescEncKey::from_str(key).unwrap(),
1394 ));
1395 }
1396 let mut dir = DirectoryKeyProviderBuilder::default();
1397 dir.path(CfgPath::new(
1398 "/var/lib/tor/hidden_service/authorized_clients".to_string(),
1399 ));
1400
1401 b.service()
1402 .restricted_discovery()
1403 .key_dirs()
1404 .access()
1405 .push(dir);
1406 }
1407
1408 b.build().unwrap()
1409 };
1410
1411 cfg_if::cfg_if! {
1412 if #[cfg(feature = "restricted-discovery")] {
1413 let cfg = result.unwrap();
1414 let services = cfg.1.onion_services;
1415 assert_eq!(services.len(), 1);
1416 let svc = services.values().next().unwrap();
1417 assert_eq!(svc, &svc_expected);
1418 } else {
1419 expect_err_contains(
1420 result.unwrap_err(),
1421 "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
1422 );
1423 }
1424 }
1425 }
1426 #[cfg(not(feature = "onion-service-service"))]
1427 {
1428 expect_err_contains(result.unwrap_err(), "no support for running onion services");
1429 }
1430 }
1431
1432 #[cfg(feature = "rpc")]
1433 #[test]
1434 fn rpc_defaults() {
1435 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1436 file.lines
1440 .retain(|line| line.starts_with("# ") && !line.starts_with("# "));
1441 file.uncomment();
1442
1443 let parsed = file
1444 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1445 .unwrap();
1446 assert!(parsed.unrecognized.is_empty());
1447 assert!(parsed.deprecated.is_empty());
1448 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1449 let rpc_default = RpcConfig::default();
1450 assert_eq!(rpc_parsed, &rpc_default);
1451 }
1452
1453 #[cfg(feature = "rpc")]
1454 #[test]
1455 fn rpc_full() {
1456 use crate::rpc::listener::{ConnectPointOptionsBuilder, RpcListenerSetConfigBuilder};
1457
1458 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1460 file.lines
1462 .retain(|line| line.starts_with("# ") && !line.contains("file ="));
1463 file.uncomment();
1464
1465 let parsed = file
1466 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1467 .unwrap();
1468 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1469
1470 let expected = {
1471 let mut bld_opts = ConnectPointOptionsBuilder::default();
1472 bld_opts.enable(false);
1473
1474 let mut bld_set = RpcListenerSetConfigBuilder::default();
1475 bld_set.dir(CfgPath::new("${HOME}/.my_connect_files/".to_string()));
1476 bld_set.listener_options().enable(true);
1477 bld_set
1478 .file_options()
1479 .insert("bad_file.json".to_string(), bld_opts);
1480
1481 let mut bld = RpcConfigBuilder::default();
1482 bld.listen().insert("label".to_string(), bld_set);
1483 bld.build().unwrap()
1484 };
1485
1486 assert_eq!(&expected, rpc_parsed);
1487 }
1488
1489 #[derive(Debug, Clone)]
1497 struct ExampleSectionLines {
1498 section: String,
1501 lines: Vec<String>,
1503 }
1504
1505 type NarrowInstruction<'s> = (&'s str, bool);
1508 const NARROW_NONE: NarrowInstruction<'static> = ("?<none>", false);
1510
1511 impl ExampleSectionLines {
1512 fn from_section(section: &str) -> Self {
1516 Self::from_markers(format!("[{section}]"), "[")
1517 }
1518
1519 fn from_markers<S, E>(start: S, end: E) -> Self
1530 where
1531 S: AsRef<str>,
1532 E: AsRef<str>,
1533 {
1534 let (start, end) = (start.as_ref(), end.as_ref());
1535 let mut lines = ARTI_EXAMPLE_CONFIG
1536 .lines()
1537 .skip_while(|line| !line.starts_with(start))
1538 .peekable();
1539 let section = lines
1540 .next_if(|l0| l0.starts_with('['))
1541 .map(|section| section.to_owned())
1542 .unwrap_or_default();
1543 let lines = lines
1544 .take_while(|line| !line.starts_with(end))
1545 .map(|l| l.to_owned())
1546 .collect_vec();
1547
1548 Self { section, lines }
1549 }
1550
1551 fn narrow(&mut self, start: NarrowInstruction, end: NarrowInstruction) {
1554 let find_index = |(re, include), start_pos, exactly_one: bool, adjust: [isize; 2]| {
1555 if (re, include) == NARROW_NONE {
1556 return None;
1557 }
1558
1559 let re = Regex::new(re).expect(re);
1560 let i = self
1561 .lines
1562 .iter()
1563 .enumerate()
1564 .skip(start_pos)
1565 .filter(|(_, l)| re.is_match(l))
1566 .map(|(i, _)| i);
1567 let i = if exactly_one {
1568 i.clone().exactly_one().unwrap_or_else(|_| {
1569 panic!("RE={:?} I={:#?} L={:#?}", re, i.collect_vec(), &self.lines)
1570 })
1571 } else {
1572 i.clone().next()?
1573 };
1574
1575 let adjust = adjust[usize::from(include)];
1576 let i = (i as isize + adjust) as usize;
1577 Some(i)
1578 };
1579
1580 eprint!("narrow {:?} {:?}: ", start, end);
1581 let start = find_index(start, 0, true, [1, 0]).unwrap_or(0);
1582 let end = find_index(end, start + 1, false, [0, 1]).unwrap_or(self.lines.len());
1583 eprintln!("{:?} {:?}", start, end);
1584 assert!(start < end, "empty, from {:#?}", &self.lines);
1586 self.lines = self.lines.drain(..).take(end).skip(start).collect_vec();
1587 }
1588
1589 fn expect_lines(&self, n: usize) {
1591 assert_eq!(self.lines.len(), n);
1592 }
1593
1594 fn uncomment(&mut self) {
1596 self.strip_prefix("#");
1597 }
1598
1599 fn strip_prefix(&mut self, prefix: &str) {
1606 for l in &mut self.lines {
1607 if !l.starts_with('[') {
1608 *l = l.strip_prefix(prefix).expect(l).to_string();
1609 }
1610 }
1611 }
1612
1613 fn build_string(&self) -> String {
1615 chain!(iter::once(&self.section), self.lines.iter(),).join("\n")
1616 }
1617
1618 fn parse(&self) -> tor_config::ConfigurationTree {
1621 let s = self.build_string();
1622 eprintln!("parsing\n --\n{}\n --", &s);
1623 let mut sources = tor_config::ConfigurationSources::new_empty();
1624 sources.push_source(
1625 tor_config::ConfigurationSource::from_verbatim(s.to_string()),
1626 tor_config::sources::MustRead::MustRead,
1627 );
1628 sources.load().expect(&s)
1629 }
1630
1631 fn resolve<R: tor_config::load::Resolvable>(&self) -> Result<R, ConfigResolveError> {
1632 tor_config::load::resolve(self.parse())
1633 }
1634
1635 fn resolve_return_results<R: tor_config::load::Resolvable>(
1636 &self,
1637 ) -> Result<ResolutionResults<R>, ConfigResolveError> {
1638 tor_config::load::resolve_return_results(self.parse())
1639 }
1640 }
1641
1642 #[test]
1645 fn builder() {
1646 use tor_config_path::CfgPath;
1647 let sec = std::time::Duration::from_secs(1);
1648
1649 let auth = dir::Authority::builder()
1650 .name("Fred")
1651 .v3ident([22; 20].into())
1652 .clone();
1653 let mut fallback = dir::FallbackDir::builder();
1654 fallback
1655 .rsa_identity([23; 20].into())
1656 .ed_identity([99; 32].into())
1657 .orports()
1658 .push("127.0.0.7:7".parse().unwrap());
1659
1660 let mut bld = ArtiConfig::builder();
1661 let mut bld_tor = TorClientConfig::builder();
1662
1663 bld.proxy().socks_listen(Listen::new_localhost(9999));
1664 bld.logging().console("warn");
1665
1666 bld_tor.tor_network().set_authorities(vec![auth]);
1667 bld_tor.tor_network().set_fallback_caches(vec![fallback]);
1668 bld_tor
1669 .storage()
1670 .cache_dir(CfgPath::new("/var/tmp/foo".to_owned()))
1671 .state_dir(CfgPath::new("/var/tmp/bar".to_owned()));
1672 bld_tor.download_schedule().retry_certs().attempts(10);
1673 bld_tor.download_schedule().retry_certs().initial_delay(sec);
1674 bld_tor.download_schedule().retry_certs().parallelism(3);
1675 bld_tor.download_schedule().retry_microdescs().attempts(30);
1676 bld_tor
1677 .download_schedule()
1678 .retry_microdescs()
1679 .initial_delay(10 * sec);
1680 bld_tor
1681 .download_schedule()
1682 .retry_microdescs()
1683 .parallelism(9);
1684 bld_tor
1685 .override_net_params()
1686 .insert("wombats-per-quokka".to_owned(), 7);
1687 bld_tor
1688 .path_rules()
1689 .ipv4_subnet_family_prefix(20)
1690 .ipv6_subnet_family_prefix(48);
1691 bld_tor.preemptive_circuits().disable_at_threshold(12);
1692 bld_tor
1693 .preemptive_circuits()
1694 .set_initial_predicted_ports(vec![80, 443]);
1695 bld_tor
1696 .preemptive_circuits()
1697 .prediction_lifetime(Duration::from_secs(3600))
1698 .min_exit_circs_for_port(2);
1699 bld_tor
1700 .circuit_timing()
1701 .max_dirtiness(90 * sec)
1702 .request_timeout(10 * sec)
1703 .request_max_retries(22)
1704 .request_loyalty(3600 * sec);
1705 bld_tor.address_filter().allow_local_addrs(true);
1706
1707 let val = bld.build().unwrap();
1708
1709 assert_ne!(val, ArtiConfig::default());
1710 }
1711
1712 #[test]
1713 fn articonfig_application() {
1714 let config = ArtiConfig::default();
1715
1716 let application = config.application();
1717 assert_eq!(&config.application, application);
1718 }
1719
1720 #[test]
1721 fn articonfig_logging() {
1722 let config = ArtiConfig::default();
1723
1724 let logging = config.logging();
1725 assert_eq!(&config.logging, logging);
1726 }
1727
1728 #[test]
1729 fn articonfig_proxy() {
1730 let config = ArtiConfig::default();
1731
1732 let proxy = config.proxy();
1733 assert_eq!(&config.proxy, proxy);
1734 }
1735
1736 fn compat_ports_listen(
1740 f: &str,
1741 get_listen: &dyn Fn(&ArtiConfig) -> &Listen,
1742 bld_get_port: &dyn Fn(&ArtiConfigBuilder) -> &Option<Option<u16>>,
1743 bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option<Listen>,
1744 setter_port: &dyn Fn(&mut ArtiConfigBuilder, Option<u16>) -> &mut ProxyConfigBuilder,
1745 setter_listen: &dyn Fn(&mut ArtiConfigBuilder, Listen) -> &mut ProxyConfigBuilder,
1746 ) {
1747 let from_toml = |s: &str| -> ArtiConfigBuilder {
1748 let cfg: toml::Value = toml::from_str(dbg!(s)).unwrap();
1749 let cfg: ArtiConfigBuilder = cfg.try_into().unwrap();
1750 cfg
1751 };
1752
1753 let conflicting_cfgs = [
1754 format!("proxy.{}_port = 0 \n proxy.{}_listen = 200", f, f),
1755 format!("proxy.{}_port = 100 \n proxy.{}_listen = 0", f, f),
1756 format!("proxy.{}_port = 100 \n proxy.{}_listen = 200", f, f),
1757 ];
1758
1759 let chk = |cfg: &ArtiConfigBuilder, expected: &Listen| {
1760 dbg!(bld_get_listen(cfg), bld_get_port(cfg));
1761 let cfg = cfg.build().unwrap();
1762 assert_eq!(get_listen(&cfg), expected);
1763 };
1764
1765 let check_setters = |port, expected: &_| {
1766 for cfg in chain!(
1767 iter::once(ArtiConfig::builder()),
1768 conflicting_cfgs.iter().map(|cfg| from_toml(cfg)),
1769 ) {
1770 for listen in match port {
1771 None => vec![Listen::new_none(), Listen::new_localhost(0)],
1772 Some(port) => vec![Listen::new_localhost(port)],
1773 } {
1774 let mut cfg = cfg.clone();
1775 setter_port(&mut cfg, dbg!(port));
1776 setter_listen(&mut cfg, dbg!(listen));
1777 chk(&cfg, expected);
1778 }
1779 }
1780 };
1781
1782 {
1783 let expected = Listen::new_localhost(100);
1784
1785 let cfg = from_toml(&format!("proxy.{}_port = 100", f));
1786 assert_eq!(bld_get_port(&cfg), &Some(Some(100)));
1787 chk(&cfg, &expected);
1788
1789 let cfg = from_toml(&format!("proxy.{}_listen = 100", f));
1790 assert_eq!(bld_get_listen(&cfg), &Some(Listen::new_localhost(100)));
1791 chk(&cfg, &expected);
1792
1793 let cfg = from_toml(&format!(
1794 "proxy.{}_port = 100\n proxy.{}_listen = 100",
1795 f, f
1796 ));
1797 chk(&cfg, &expected);
1798
1799 check_setters(Some(100), &expected);
1800 }
1801
1802 {
1803 let expected = Listen::new_none();
1804
1805 let cfg = from_toml(&format!("proxy.{}_port = 0", f));
1806 chk(&cfg, &expected);
1807
1808 let cfg = from_toml(&format!("proxy.{}_listen = 0", f));
1809 chk(&cfg, &expected);
1810
1811 let cfg = from_toml(&format!("proxy.{}_port = 0 \n proxy.{}_listen = 0", f, f));
1812 chk(&cfg, &expected);
1813
1814 check_setters(None, &expected);
1815 }
1816
1817 for cfg in &conflicting_cfgs {
1818 let cfg = from_toml(cfg);
1819 let err = dbg!(cfg.build()).unwrap_err();
1820 assert!(err.to_string().contains("specifying different values"));
1821 }
1822 }
1823
1824 #[test]
1825 #[allow(deprecated)]
1826 fn ports_listen_socks() {
1827 compat_ports_listen(
1828 "socks",
1829 &|cfg| &cfg.proxy.socks_listen,
1830 &|bld| &bld.proxy.socks_port,
1831 &|bld| &bld.proxy.socks_listen,
1832 &|bld, arg| bld.proxy.socks_port(arg),
1833 &|bld, arg| bld.proxy.socks_listen(arg),
1834 );
1835 }
1836
1837 #[test]
1838 #[allow(deprecated)]
1839 fn compat_ports_listen_dns() {
1840 compat_ports_listen(
1841 "dns",
1842 &|cfg| &cfg.proxy.dns_listen,
1843 &|bld| &bld.proxy.dns_port,
1844 &|bld| &bld.proxy.dns_listen,
1845 &|bld, arg| bld.proxy.dns_port(arg),
1846 &|bld, arg| bld.proxy.dns_listen(arg),
1847 );
1848 }
1849}