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")]
17pub use crate::rpc::{RpcConfig, RpcConfigBuilder};
18use arti_client::TorClientConfig;
19#[cfg(feature = "onion-service-service")]
20use tor_config::define_list_builder_accessors;
21use tor_config::resolve_alternative_specs;
22pub(crate) use tor_config::{ConfigBuildError, Listen, impl_standard_builder};
23
24use crate::{LoggingConfig, LoggingConfigBuilder};
25
26pub const ARTI_EXAMPLE_CONFIG: &str = concat!(include_str!("./arti-example-config.toml"));
31
32#[cfg(test)]
49const OLDEST_SUPPORTED_CONFIG: &str = concat!(include_str!("./oldest-supported-config.toml"),);
50
51#[derive(Debug, Clone, Builder, Eq, PartialEq)]
53#[builder(build_fn(error = "ConfigBuildError"))]
54#[builder(derive(Debug, Serialize, Deserialize))]
55pub struct ApplicationConfig {
56 #[builder(default)]
64 pub(crate) watch_configuration: bool,
65
66 #[builder(default)]
76 pub(crate) permit_debugging: bool,
77
78 #[builder(default)]
82 pub(crate) allow_running_as_root: bool,
83}
84impl_standard_builder! { ApplicationConfig }
85
86#[deprecated = "This macro is only for supporting old _port options! Don't use it for new options."]
100macro_rules! resolve_listen_port {
101 { $self:expr, $field:ident, $def_port:expr } => { paste!{
102 resolve_alternative_specs(
103 [
104 (
105 concat!(stringify!($field), "_listen"),
106 $self.[<$field _listen>].clone(),
107 ),
108 (
109 concat!(stringify!($field), "_port"),
110 $self.[<$field _port>].map(Listen::new_localhost_optional),
111 ),
112 ],
113 || Listen::new_localhost($def_port),
114 )?
115 } }
116}
117
118#[derive(Debug, Clone, Builder, Eq, PartialEq)]
120#[builder(build_fn(error = "ConfigBuildError"))]
121#[builder(derive(Debug, Serialize, Deserialize))]
122#[allow(clippy::option_option)] pub struct ProxyConfig {
124 #[builder(field(build = r#"#[allow(deprecated)]
128 // We use this deprecated macro to instantiate the legacy socks_port option.
129 { resolve_listen_port!(self, socks, 9150) }
130 "#))]
131 pub(crate) socks_listen: Listen,
132
133 #[builder(
139 setter(strip_option),
140 field(type = "Option<Option<u16>>", build = "()")
141 )]
142 #[builder_setter_attr(deprecated)]
143 pub(crate) socks_port: (),
144
145 #[builder(field(build = r#"#[allow(deprecated)]
147 // We use this deprecated macro to instantiate the legacy dns_port option.
148 { resolve_listen_port!(self, dns, 0) }
149 "#))]
150 pub(crate) dns_listen: Listen,
151
152 #[builder(
158 setter(strip_option),
159 field(type = "Option<Option<u16>>", build = "()")
160 )]
161 #[builder_setter_attr(deprecated)]
162 pub(crate) dns_port: (),
163}
164impl_standard_builder! { ProxyConfig }
165
166#[derive(Debug, Clone, Builder, Eq, PartialEq)]
180#[builder(build_fn(error = "ConfigBuildError"))]
181#[builder(derive(Debug, Serialize, Deserialize))]
182#[non_exhaustive]
183pub struct SystemConfig {
184 #[builder(setter(into), default = "default_max_files()")]
186 pub(crate) max_files: u64,
187}
188impl_standard_builder! { SystemConfig }
189
190fn default_max_files() -> u64 {
192 16384
193}
194
195#[derive(Debug, Builder, Clone, Eq, PartialEq)]
210#[builder(derive(Serialize, Deserialize, Debug))]
211#[builder(build_fn(private, name = "build_unvalidated", error = "ConfigBuildError"))]
212pub struct ArtiConfig {
213 #[builder(sub_builder(fn_name = "build"))]
215 #[builder_field_attr(serde(default))]
216 application: ApplicationConfig,
217
218 #[builder(sub_builder(fn_name = "build"))]
220 #[builder_field_attr(serde(default))]
221 proxy: ProxyConfig,
222
223 #[builder(sub_builder(fn_name = "build"))]
225 #[builder_field_attr(serde(default))]
226 logging: LoggingConfig,
227
228 #[builder(sub_builder(fn_name = "build"))]
230 #[builder_field_attr(serde(default))]
231 pub(crate) metrics: MetricsConfig,
232
233 #[cfg(feature = "rpc")]
235 #[builder(sub_builder(fn_name = "build"))]
236 #[builder_field_attr(serde(default))]
237 pub(crate) rpc: RpcConfig,
238
239 #[cfg(not(feature = "rpc"))]
251 #[builder_field_attr(serde(default))]
252 #[builder(field(type = "Option<toml::Value>", build = "()"), private)]
253 rpc: (),
254
255 #[builder(sub_builder(fn_name = "build"))]
261 #[builder_field_attr(serde(default))]
262 pub(crate) system: SystemConfig,
263
264 #[builder(sub_builder(fn_name = "build"), setter(custom))]
273 #[builder_field_attr(serde(default))]
274 pub(crate) onion_services: OnionServiceProxyConfigMap,
275}
276
277impl_standard_builder! { ArtiConfig }
278
279impl ArtiConfigBuilder {
280 pub fn build(&self) -> Result<ArtiConfig, ConfigBuildError> {
282 #[cfg_attr(not(feature = "onion-service-service"), allow(unused_mut))]
283 let mut config = self.build_unvalidated()?;
284 #[cfg(feature = "onion-service-service")]
285 for svc in config.onion_services.values_mut() {
286 *svc.svc_cfg
288 .restricted_discovery_mut()
289 .watch_configuration_mut() = config.application.watch_configuration;
290 }
291
292 #[cfg(not(feature = "rpc"))]
293 if self.rpc.is_some() {
294 tracing::warn!("rpc options were set, but Arti was built without support for rpc.");
295 }
296
297 Ok(config)
298 }
299}
300
301impl tor_config::load::TopLevel for ArtiConfig {
302 type Builder = ArtiConfigBuilder;
303 const DEPRECATED_KEYS: &'static [&'static str] = &["proxy.socks_port", "proxy.dns_port"];
304}
305
306#[cfg(feature = "onion-service-service")]
307define_list_builder_accessors! {
308 struct ArtiConfigBuilder {
309 pub(crate) onion_services: [OnionServiceProxyConfigBuilder],
310 }
311}
312
313pub type ArtiCombinedConfig = (ArtiConfig, TorClientConfig);
317
318#[derive(Debug, Clone, Builder, Eq, PartialEq)]
320#[builder(build_fn(error = "ConfigBuildError"))]
321#[builder(derive(Debug, Serialize, Deserialize))]
322pub struct MetricsConfig {
323 #[builder(sub_builder(fn_name = "build"))]
325 #[builder_field_attr(serde(default))]
326 pub(crate) prometheus: PrometheusConfig,
327}
328impl_standard_builder! { MetricsConfig }
329
330#[derive(Debug, Clone, Builder, Eq, PartialEq)]
332#[builder(build_fn(error = "ConfigBuildError"))]
333#[builder(derive(Debug, Serialize, Deserialize))]
334#[allow(clippy::option_option)] pub struct PrometheusConfig {
336 #[builder(default)]
345 #[builder_field_attr(serde(default))]
346 pub(crate) listen: Listen,
347}
348impl_standard_builder! { PrometheusConfig }
349
350impl ArtiConfig {
351 pub fn application(&self) -> &ApplicationConfig {
353 &self.application
354 }
355
356 pub fn logging(&self) -> &LoggingConfig {
358 &self.logging
359 }
360
361 pub fn proxy(&self) -> &ProxyConfig {
363 &self.proxy
364 }
365
366 #[cfg(feature = "rpc")]
368 pub fn rpc(&self) -> &RpcConfig {
369 &self.rpc
370 }
371}
372
373#[cfg(test)]
374mod test {
375 #![allow(clippy::bool_assert_comparison)]
377 #![allow(clippy::clone_on_copy)]
378 #![allow(clippy::dbg_macro)]
379 #![allow(clippy::mixed_attributes_style)]
380 #![allow(clippy::print_stderr)]
381 #![allow(clippy::print_stdout)]
382 #![allow(clippy::single_char_pattern)]
383 #![allow(clippy::unwrap_used)]
384 #![allow(clippy::unchecked_time_subtraction)]
385 #![allow(clippy::useless_vec)]
386 #![allow(clippy::needless_pass_by_value)]
387 #![allow(clippy::iter_overeager_cloned)]
390 #![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)] 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 fn uncomment_example_settings(template: &str) -> String {
425 let re = Regex::new(r#"(?m)^\#([^ \n])"#).unwrap();
426 re.replace_all(template, |cap: ®ex::Captures<'_>| -> _ {
427 cap.get(1).unwrap().as_str().to_string()
428 })
429 .into()
430 }
431
432 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
441 enum InExample {
442 Absent,
443 Present,
444 }
445 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
451 enum WhichExample {
452 Old,
453 New,
454 }
455 #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
461 struct ConfigException {
462 key: String,
464 in_old_example: InExample,
466 in_new_example: InExample,
468 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 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 fn declared_config_exceptions() -> Vec<ConfigException> {
491 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
496 enum InCode {
497 Ignored,
499 FeatureDependent,
507 Recognized,
509 }
510 use InCode::*;
511
512 struct InOld;
514 struct InNew;
516
517 let mut out = vec![];
518
519 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)] 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 "application.allow_running_as_root",
565 "bridges",
566 "logging.time_granularity",
567 "path_rules.long_lived_ports",
568 "proxy.socks_listen",
569 "proxy.dns_listen",
570 "use_obsolete_software",
571 "circuit_timing.disused_circuit_timeout",
572 ],
573 );
574
575 declare_exceptions(
576 None,
577 None,
578 Recognized,
579 &[
580 "tor_network.authorities",
582 "tor_network.fallback_caches",
583 ],
584 );
585
586 declare_exceptions(
587 None,
588 None,
589 Recognized,
590 &[
591 "logging.opentelemetry",
593 ],
594 );
595
596 declare_exceptions(
597 Some(InOld),
598 Some(InNew),
599 if cfg!(target_family = "windows") {
600 Ignored
601 } else {
602 Recognized
603 },
604 &[
605 "storage.permissions.trust_group",
607 "storage.permissions.trust_user",
608 ],
609 );
610
611 declare_exceptions(
612 None,
613 None, FeatureDependent,
615 &[
616 "bridges.transports", ],
619 );
620
621 declare_exceptions(
622 None,
623 Some(InNew),
624 FeatureDependent,
625 &[
626 "storage.keystore",
628 ],
629 );
630
631 declare_exceptions(
632 None,
633 None, FeatureDependent,
635 &[
636 "logging.tokio_console",
638 "logging.tokio_console.enabled",
639 ],
640 );
641
642 declare_exceptions(
643 None,
644 None, Recognized,
646 &[
647 "system.memory",
649 "system.memory.max",
650 "system.memory.low_water",
651 ],
652 );
653
654 declare_exceptions(
655 None,
656 Some(InNew), Recognized,
658 &["metrics"],
659 );
660
661 declare_exceptions(
662 None,
663 None, Recognized,
665 &[
666 "metrics.prometheus",
668 "metrics.prometheus.listen",
669 ],
670 );
671
672 declare_exceptions(
673 None,
674 Some(InNew),
675 FeatureDependent,
676 &[
677 ],
679 );
680
681 declare_exceptions(
682 None,
683 Some(InNew),
684 FeatureDependent,
685 &[
686 "address_filter.allow_onion_addrs",
688 "circuit_timing.hs_desc_fetch_attempts",
689 "circuit_timing.hs_intro_rend_attempts",
690 ],
691 );
692
693 declare_exceptions(
694 None,
695 None, FeatureDependent,
697 &[
698 "rpc",
700 "rpc.rpc_listen",
701 ],
702 );
703
704 declare_exceptions(
706 None,
707 None,
708 FeatureDependent,
709 &[
710 "onion_services",
712 ],
713 );
714
715 declare_exceptions(
716 None,
717 Some(InNew),
718 FeatureDependent,
719 &[
720 "vanguards",
722 "vanguards.mode",
723 ],
724 );
725
726 declare_exceptions(
728 None,
729 None,
730 FeatureDependent,
731 &[
732 "storage.keystore.ctor",
733 "storage.keystore.ctor.services",
734 "storage.keystore.ctor.clients",
735 ],
736 );
737
738 out.sort();
739
740 let dupes = out.iter().map(|exc| &exc.key).duplicates().collect_vec();
741 assert!(
742 dupes.is_empty(),
743 "duplicate exceptions in configuration {dupes:?}"
744 );
745
746 eprintln!(
747 "declared config exceptions for this configuration:\n{:#?}",
748 &out
749 );
750 out
751 }
752
753 #[test]
754 fn default_config() {
755 use InExample::*;
756
757 let empty_config = tor_config::ConfigurationSources::new_empty()
758 .load()
759 .unwrap();
760 let empty_config: ArtiCombinedConfig = tor_config::resolve(empty_config).unwrap();
761
762 let default = (ArtiConfig::default(), TorClientConfig::default());
763 let exceptions = declared_config_exceptions();
764
765 #[allow(clippy::needless_pass_by_value)] fn analyse_joined_info(
776 which: WhichExample,
777 uncommented: bool,
778 eob: EitherOrBoth<&String, &ConfigException>,
779 ) -> Result<(), (String, String)> {
780 use EitherOrBoth::*;
781 let (key, err) = match eob {
782 Left(found) => (found, "found in example but not processed".into()),
784 Both(found, exc) => {
785 let but = match (exc.in_example(which), exc.in_code, uncommented) {
786 (Absent, _, _) => "but exception entry expected key to be absent",
787 (_, _, false) => "when processing still-commented-out file!",
788 (_, Some(true), _) => {
789 "but an exception entry says it should have been recognised"
790 }
791 (Present, Some(false), true) => return Ok(()), (Present, None, true) => return Ok(()), };
794 (
795 found,
796 format!("parser reported unrecognised config key, {but}"),
797 )
798 }
799 Right(exc) => {
800 let trouble = match (exc.in_example(which), exc.in_code, uncommented) {
805 (Absent, _, _) => return Ok(()), (_, _, false) => return Ok(()), (_, Some(true), _) => return Ok(()), (Present, Some(false), true) => {
809 "expected an 'unknown config key' report but didn't see one"
810 }
811 (Present, None, true) => return Ok(()), };
813 (&exc.key, trouble.into())
814 }
815 };
816 Err((key.clone(), err))
817 }
818
819 let parses_to_defaults = |example: &str, which: WhichExample, uncommented: bool| {
820 let cfg = {
821 let mut sources = tor_config::ConfigurationSources::new_empty();
822 sources.push_source(
823 tor_config::ConfigurationSource::from_verbatim(example.to_string()),
824 tor_config::sources::MustRead::MustRead,
825 );
826 sources.load().unwrap()
827 };
828
829 let results: ResolutionResults<ArtiCombinedConfig> =
831 tor_config::resolve_return_results(cfg).unwrap();
832
833 assert_eq!(&results.value, &default, "{which:?} {uncommented:?}");
834 assert_eq!(&results.value, &empty_config, "{which:?} {uncommented:?}");
835
836 let unrecognized = results
839 .unrecognized
840 .iter()
841 .map(|k| k.to_string())
842 .collect_vec();
843
844 eprintln!(
845 "parsing of {which:?} uncommented={uncommented:?}, unrecognized={unrecognized:#?}"
846 );
847
848 let reports =
849 Itertools::merge_join_by(unrecognized.iter(), exceptions.iter(), |u, e| {
850 u.as_str().cmp(&e.key)
851 })
852 .filter_map(|eob| analyse_joined_info(which, uncommented, eob).err())
853 .collect_vec();
854
855 if !reports.is_empty() {
856 let reports = reports.iter().fold(String::new(), |mut out, (k, s)| {
857 writeln!(out, " {}: {}", s, k).unwrap();
858 out
859 });
860
861 panic!(
862 r"
863mismatch: results of parsing example files (& vs declared exceptions):
864example config file {which:?}, uncommented={uncommented:?}
865{reports}
866"
867 );
868 }
869
870 results.value
871 };
872
873 let _ = parses_to_defaults(ARTI_EXAMPLE_CONFIG, WhichExample::New, false);
874 let _ = parses_to_defaults(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, false);
875
876 let built_default = (
877 ArtiConfigBuilder::default().build().unwrap(),
878 TorClientConfigBuilder::default().build().unwrap(),
879 );
880
881 let parsed = parses_to_defaults(
882 &uncomment_example_settings(ARTI_EXAMPLE_CONFIG),
883 WhichExample::New,
884 true,
885 );
886 let parsed_old = parses_to_defaults(
887 &uncomment_example_settings(OLDEST_SUPPORTED_CONFIG),
888 WhichExample::Old,
889 true,
890 );
891
892 assert_eq!(&parsed, &built_default);
893 assert_eq!(&parsed_old, &built_default);
894
895 assert_eq!(&default, &built_default);
896 }
897
898 fn exhaustive_1(example_file: &str, which: WhichExample, deprecated: &[String]) {
930 use InExample::*;
931 use serde_json::Value as JsValue;
932 use std::collections::BTreeSet;
933
934 let example = uncomment_example_settings(example_file);
935 let example: toml::Value = toml::from_str(&example).unwrap();
936 let example = serde_json::to_value(example).unwrap();
938 let exhausts = [
948 serde_json::to_value(TorClientConfig::builder()).unwrap(),
949 serde_json::to_value(ArtiConfig::builder()).unwrap(),
950 ];
951
952 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, derive_more::Display)]
955 enum ProblemKind {
956 #[display("recognised by serialisation, but missing from example config file")]
957 MissingFromExample,
958 #[display("expected that example config file should contain have this as a table")]
959 ExpectedTableInExample,
960 #[display(
961 "declared exception says this key should be recognised but not in file, but that doesn't seem to be the case"
962 )]
963 UnusedException,
964 }
965
966 #[derive(Default, Debug)]
967 struct Walk {
968 current_path: Vec<String>,
969 problems: Vec<(String, ProblemKind)>,
970 }
971
972 impl Walk {
973 fn bad(&mut self, kind: ProblemKind) {
975 self.problems.push((self.current_path.join("."), kind));
976 }
977
978 fn walk<const E: usize>(
985 &mut self,
986 example: Option<&JsValue>,
987 exhausts: [Option<&JsValue>; E],
988 ) {
989 assert! { exhausts.into_iter().any(|e| e.is_some()) }
990
991 let example = if let Some(e) = example {
992 e
993 } else {
994 self.bad(ProblemKind::MissingFromExample);
995 return;
996 };
997
998 let tables = exhausts.map(|e| e?.as_object());
999
1000 let table_keys = tables
1002 .iter()
1003 .flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten())
1004 .collect::<BTreeSet<String>>();
1005
1006 for key in table_keys {
1007 let example = if let Some(e) = example.as_object() {
1008 e
1009 } else {
1010 self.bad(ProblemKind::ExpectedTableInExample);
1013 continue;
1014 };
1015
1016 self.current_path.push(key.clone());
1018 self.walk(example.get(&key), tables.map(|t| t?.get(&key)));
1019 self.current_path.pop().unwrap();
1020 }
1021 }
1022 }
1023
1024 let exhausts = exhausts.iter().map(Some).collect_vec().try_into().unwrap();
1025
1026 let mut walk = Walk::default();
1027 walk.walk::<2>(Some(&example), exhausts);
1028 let mut problems = walk.problems;
1029
1030 #[derive(Debug, Copy, Clone)]
1032 struct DefinitelyRecognized;
1033
1034 let expect_missing = declared_config_exceptions()
1035 .iter()
1036 .filter_map(|exc| {
1037 let definitely = match (exc.in_example(which), exc.in_code) {
1038 (Present, _) => return None, (_, Some(false)) => return None, (Absent, Some(true)) => Some(DefinitelyRecognized),
1041 (Absent, None) => None, };
1043 Some((exc.key.clone(), definitely))
1044 })
1045 .collect_vec();
1046 dbg!(&expect_missing);
1047
1048 let expect_missing: Vec<(String, Option<DefinitelyRecognized>)> = expect_missing
1057 .iter()
1058 .cloned()
1059 .filter({
1060 let original: HashSet<_> = expect_missing.iter().map(|(k, _)| k.clone()).collect();
1061 move |(found, _)| {
1062 !found
1063 .match_indices('.')
1064 .any(|(doti, _)| original.contains(&found[0..doti]))
1065 }
1066 })
1067 .collect_vec();
1068 dbg!(&expect_missing);
1069
1070 for (exp, definitely) in expect_missing {
1071 let was = problems.len();
1072 problems.retain(|(path, _)| path != &exp);
1073 if problems.len() == was && definitely.is_some() {
1074 problems.push((exp, ProblemKind::UnusedException));
1075 }
1076 }
1077
1078 let problems = problems
1079 .into_iter()
1080 .filter(|(key, _kind)| !deprecated.iter().any(|dep| key == dep))
1081 .map(|(path, m)| format!(" config key {:?}: {}", path, m))
1082 .collect_vec();
1083
1084 assert!(
1087 problems.is_empty(),
1088 "example config {which:?} exhaustiveness check failed: {}\n-----8<-----\n{}\n-----8<-----\n",
1089 problems.join("\n"),
1090 example_file,
1091 );
1092 }
1093
1094 #[test]
1095 fn exhaustive() {
1096 let mut deprecated = vec![];
1097 <(ArtiConfig, TorClientConfig) as tor_config::load::Resolvable>::enumerate_deprecated_keys(
1098 &mut |l| {
1099 for k in l {
1100 deprecated.push(k.to_string());
1101 }
1102 },
1103 );
1104 let deprecated = deprecated.iter().cloned().collect_vec();
1105
1106 exhaustive_1(ARTI_EXAMPLE_CONFIG, WhichExample::New, &deprecated);
1111
1112 exhaustive_1(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, &deprecated);
1119 }
1120
1121 #[cfg_attr(feature = "pt-client", allow(dead_code))]
1123 fn expect_err_contains(err: ConfigResolveError, exp: &str) {
1124 use std::error::Error as StdError;
1125 let err: Box<dyn StdError> = Box::new(err);
1126 let err = tor_error::Report(err).to_string();
1127 assert!(
1128 err.contains(exp),
1129 "wrong message, got {:?}, exp {:?}",
1130 err,
1131 exp,
1132 );
1133 }
1134
1135 #[test]
1136 fn bridges() {
1137 let filter_examples = |#[allow(unused_mut)] mut examples: ExampleSectionLines| -> _ {
1151 if cfg!(all(feature = "bridge-client", not(feature = "pt-client"))) {
1153 let looks_like_addr =
1154 |l: &str| l.starts_with(|c: char| c.is_ascii_digit() || c == '[');
1155 examples.lines.retain(|l| looks_like_addr(l));
1156 }
1157
1158 examples
1159 };
1160
1161 let resolve_examples = |examples: &ExampleSectionLines| {
1166 #[cfg(all(feature = "bridge-client", not(feature = "pt-client")))]
1168 {
1169 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1170 expect_err_contains(err, "support disabled in cargo features");
1171 }
1172
1173 let examples = filter_examples(examples.clone());
1174
1175 #[cfg(feature = "bridge-client")]
1176 {
1177 examples.resolve::<TorClientConfig>().unwrap()
1178 }
1179
1180 #[cfg(not(feature = "bridge-client"))]
1181 {
1182 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1183 expect_err_contains(err, "support disabled in cargo features");
1184 ((),)
1186 }
1187 };
1188
1189 let mut examples = ExampleSectionLines::from_section("bridges");
1191 examples.narrow((r#"^# For example:"#, true), NARROW_NONE);
1192
1193 let compare = {
1194 let mut examples = examples.clone();
1196 examples.narrow((r#"^# bridges = '''"#, true), (r#"^# '''"#, true));
1197 examples.uncomment();
1198
1199 let parsed = resolve_examples(&examples);
1200
1201 examples.lines.remove(0);
1204 examples.lines.remove(examples.lines.len() - 1);
1205 examples.expect_lines(3);
1207
1208 #[cfg(feature = "bridge-client")]
1210 {
1211 let examples = filter_examples(examples);
1212 let mut built = TorClientConfig::builder();
1213 for l in &examples.lines {
1214 built.bridges().bridges().push(l.trim().parse().expect(l));
1215 }
1216 let built = built.build().unwrap();
1217
1218 assert_eq!(&parsed, &built);
1219 }
1220
1221 parsed
1222 };
1223
1224 {
1226 examples.narrow((r#"^# bridges = \["#, true), (r#"^# \]"#, true));
1227 examples.uncomment();
1228 let parsed = resolve_examples(&examples);
1229 assert_eq!(&parsed, &compare);
1230 }
1231 }
1232
1233 #[test]
1234 fn transports() {
1235 let mut file =
1241 ExampleSectionLines::from_markers("# An example managed pluggable transport", "[");
1242 file.lines.retain(|line| line.starts_with("# "));
1243 file.uncomment();
1244
1245 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1246 let cfg_got = result.unwrap();
1247
1248 #[cfg(feature = "pt-client")]
1249 {
1250 use arti_client::config::{BridgesConfig, pt::TransportConfig};
1251 use tor_config_path::CfgPath;
1252
1253 let bridges_got: &BridgesConfig = cfg_got.0.as_ref();
1254
1255 let mut bld = BridgesConfig::builder();
1257 {
1258 let mut b = TransportConfig::builder();
1259 b.protocols(vec!["obfs4".parse().unwrap(), "obfs5".parse().unwrap()]);
1260 b.path(CfgPath::new("/usr/bin/obfsproxy".to_string()));
1261 b.arguments(vec!["-obfs4".to_string(), "-obfs5".to_string()]);
1262 b.run_on_startup(true);
1263 bld.transports().push(b);
1264 }
1265 {
1266 let mut b = TransportConfig::builder();
1267 b.protocols(vec!["obfs4".parse().unwrap()]);
1268 b.proxy_addr("127.0.0.1:31337".parse().unwrap());
1269 bld.transports().push(b);
1270 }
1271
1272 let bridges_expected = bld.build().unwrap();
1273 assert_eq!(&bridges_expected, bridges_got);
1274 }
1275 }
1276
1277 #[test]
1278 fn memquota() {
1279 let mut file = ExampleSectionLines::from_section("system");
1282 file.lines.retain(|line| line.starts_with("# memory."));
1283 file.uncomment();
1284
1285 let result = file.resolve_return_results::<(TorClientConfig, ArtiConfig)>();
1286
1287 let result = result.unwrap();
1288
1289 assert_eq!(result.unrecognized, []);
1291 assert_eq!(result.deprecated, []);
1292
1293 let inner: &tor_memquota::testing::ConfigInner =
1294 result.value.0.system_memory().inner().unwrap();
1295
1296 let defaulted_low = tor_memquota::Config::builder()
1299 .max(*inner.max)
1300 .build()
1301 .unwrap();
1302 let inner_defaulted_low = defaulted_low.inner().unwrap();
1303 assert_eq!(inner, inner_defaulted_low);
1304 }
1305
1306 #[test]
1307 fn metrics() {
1308 let mut file = ExampleSectionLines::from_section("metrics");
1310 file.lines
1311 .retain(|line| line.starts_with("# prometheus."));
1312 file.uncomment();
1313
1314 let result = file
1315 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1316 .unwrap();
1317
1318 assert_eq!(result.unrecognized, []);
1320 assert_eq!(result.deprecated, []);
1321
1322 assert_eq!(
1324 result
1325 .value
1326 .1
1327 .metrics
1328 .prometheus
1329 .listen
1330 .single_address_legacy()
1331 .unwrap(),
1332 Some("127.0.0.1:9035".parse().unwrap()),
1333 );
1334
1335 }
1338
1339 #[test]
1340 fn onion_services() {
1341 let mut file = ExampleSectionLines::from_markers("##### ONION SERVICES", "##### RPC");
1345 file.lines.retain(|line| line.starts_with("# "));
1346 file.uncomment();
1347
1348 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1349 #[cfg(feature = "onion-service-service")]
1350 {
1351 let svc_expected = {
1352 use tor_hsrproxy::config::*;
1353 let mut b = OnionServiceProxyConfigBuilder::default();
1354 b.service().nickname("allium-cepa".parse().unwrap());
1355 b.proxy().proxy_ports().push(ProxyRule::new(
1356 ProxyPattern::one_port(80).unwrap(),
1357 ProxyAction::Forward(
1358 Encapsulation::Simple,
1359 TargetAddr::Inet("127.0.0.1:10080".parse().unwrap()),
1360 ),
1361 ));
1362 b.proxy().proxy_ports().push(ProxyRule::new(
1363 ProxyPattern::one_port(22).unwrap(),
1364 ProxyAction::DestroyCircuit,
1365 ));
1366 b.proxy().proxy_ports().push(ProxyRule::new(
1367 ProxyPattern::one_port(265).unwrap(),
1368 ProxyAction::IgnoreStream,
1369 ));
1370 b.proxy().proxy_ports().push(ProxyRule::new(
1380 ProxyPattern::one_port(443).unwrap(),
1381 ProxyAction::RejectStream,
1382 ));
1383 b.proxy().proxy_ports().push(ProxyRule::new(
1384 ProxyPattern::all_ports(),
1385 ProxyAction::DestroyCircuit,
1386 ));
1387
1388 #[cfg(feature = "restricted-discovery")]
1389 {
1390 const ALICE_KEY: &str =
1391 "descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA";
1392 const BOB_KEY: &str =
1393 "descriptor:x25519:b5zqgtpermmuda6vc63lhjuf5ihpokjmuk26ly2xksf7vg52aesq";
1394 for (nickname, key) in [("alice", ALICE_KEY), ("bob", BOB_KEY)] {
1395 b.service()
1396 .restricted_discovery()
1397 .enabled(true)
1398 .static_keys()
1399 .access()
1400 .push((
1401 HsClientNickname::from_str(nickname).unwrap(),
1402 HsClientDescEncKey::from_str(key).unwrap(),
1403 ));
1404 }
1405 let mut dir = DirectoryKeyProviderBuilder::default();
1406 dir.path(CfgPath::new(
1407 "/var/lib/tor/hidden_service/authorized_clients".to_string(),
1408 ));
1409
1410 b.service()
1411 .restricted_discovery()
1412 .key_dirs()
1413 .access()
1414 .push(dir);
1415 }
1416
1417 b.build().unwrap()
1418 };
1419
1420 cfg_if::cfg_if! {
1421 if #[cfg(feature = "restricted-discovery")] {
1422 let cfg = result.unwrap();
1423 let services = cfg.1.onion_services;
1424 assert_eq!(services.len(), 1);
1425 let svc = services.values().next().unwrap();
1426 assert_eq!(svc, &svc_expected);
1427 } else {
1428 expect_err_contains(
1429 result.unwrap_err(),
1430 "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
1431 );
1432 }
1433 }
1434 }
1435 #[cfg(not(feature = "onion-service-service"))]
1436 {
1437 expect_err_contains(result.unwrap_err(), "no support for running onion services");
1438 }
1439 }
1440
1441 #[cfg(feature = "rpc")]
1442 #[test]
1443 fn rpc_defaults() {
1444 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1445 file.lines
1449 .retain(|line| line.starts_with("# ") && !line.starts_with("# "));
1450 file.uncomment();
1451
1452 let parsed = file
1453 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1454 .unwrap();
1455 assert!(parsed.unrecognized.is_empty());
1456 assert!(parsed.deprecated.is_empty());
1457 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1458 let rpc_default = RpcConfig::default();
1459 assert_eq!(rpc_parsed, &rpc_default);
1460 }
1461
1462 #[cfg(feature = "rpc")]
1463 #[test]
1464 fn rpc_full() {
1465 use crate::rpc::listener::{ConnectPointOptionsBuilder, RpcListenerSetConfigBuilder};
1466
1467 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1469 file.lines
1471 .retain(|line| line.starts_with("# ") && !line.contains("file ="));
1472 file.uncomment();
1473
1474 let parsed = file
1475 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1476 .unwrap();
1477 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1478
1479 let expected = {
1480 let mut bld_opts = ConnectPointOptionsBuilder::default();
1481 bld_opts.enable(false);
1482
1483 let mut bld_set = RpcListenerSetConfigBuilder::default();
1484 bld_set.dir(CfgPath::new("${HOME}/.my_connect_files/".to_string()));
1485 bld_set.listener_options().enable(true);
1486 bld_set
1487 .file_options()
1488 .insert("bad_file.json".to_string(), bld_opts);
1489
1490 let mut bld = RpcConfigBuilder::default();
1491 bld.listen().insert("label".to_string(), bld_set);
1492 bld.build().unwrap()
1493 };
1494
1495 assert_eq!(&expected, rpc_parsed);
1496 }
1497
1498 #[derive(Debug, Clone)]
1506 struct ExampleSectionLines {
1507 section: String,
1510 lines: Vec<String>,
1512 }
1513
1514 type NarrowInstruction<'s> = (&'s str, bool);
1517 const NARROW_NONE: NarrowInstruction<'static> = ("?<none>", false);
1519
1520 impl ExampleSectionLines {
1521 fn from_section(section: &str) -> Self {
1525 Self::from_markers(format!("[{section}]"), "[")
1526 }
1527
1528 fn from_markers<S, E>(start: S, end: E) -> Self
1539 where
1540 S: AsRef<str>,
1541 E: AsRef<str>,
1542 {
1543 let (start, end) = (start.as_ref(), end.as_ref());
1544 let mut lines = ARTI_EXAMPLE_CONFIG
1545 .lines()
1546 .skip_while(|line| !line.starts_with(start))
1547 .peekable();
1548 let section = lines
1549 .next_if(|l0| l0.starts_with('['))
1550 .map(|section| section.to_owned())
1551 .unwrap_or_default();
1552 let lines = lines
1553 .take_while(|line| !line.starts_with(end))
1554 .map(|l| l.to_owned())
1555 .collect_vec();
1556
1557 Self { section, lines }
1558 }
1559
1560 fn narrow(&mut self, start: NarrowInstruction, end: NarrowInstruction) {
1563 let find_index = |(re, include), start_pos, exactly_one: bool, adjust: [isize; 2]| {
1564 if (re, include) == NARROW_NONE {
1565 return None;
1566 }
1567
1568 let re = Regex::new(re).expect(re);
1569 let i = self
1570 .lines
1571 .iter()
1572 .enumerate()
1573 .skip(start_pos)
1574 .filter(|(_, l)| re.is_match(l))
1575 .map(|(i, _)| i);
1576 let i = if exactly_one {
1577 i.clone().exactly_one().unwrap_or_else(|_| {
1578 panic!("RE={:?} I={:#?} L={:#?}", re, i.collect_vec(), &self.lines)
1579 })
1580 } else {
1581 i.clone().next()?
1582 };
1583
1584 let adjust = adjust[usize::from(include)];
1585 let i = (i as isize + adjust) as usize;
1586 Some(i)
1587 };
1588
1589 eprint!("narrow {:?} {:?}: ", start, end);
1590 let start = find_index(start, 0, true, [1, 0]).unwrap_or(0);
1591 let end = find_index(end, start + 1, false, [0, 1]).unwrap_or(self.lines.len());
1592 eprintln!("{:?} {:?}", start, end);
1593 assert!(start < end, "empty, from {:#?}", &self.lines);
1595 self.lines = self.lines.drain(..).take(end).skip(start).collect_vec();
1596 }
1597
1598 fn expect_lines(&self, n: usize) {
1600 assert_eq!(self.lines.len(), n);
1601 }
1602
1603 fn uncomment(&mut self) {
1605 self.strip_prefix("#");
1606 }
1607
1608 fn strip_prefix(&mut self, prefix: &str) {
1615 for l in &mut self.lines {
1616 if !l.starts_with('[') {
1617 *l = l.strip_prefix(prefix).expect(l).to_string();
1618 }
1619 }
1620 }
1621
1622 fn build_string(&self) -> String {
1624 chain!(iter::once(&self.section), self.lines.iter(),).join("\n")
1625 }
1626
1627 fn parse(&self) -> tor_config::ConfigurationTree {
1630 let s = self.build_string();
1631 eprintln!("parsing\n --\n{}\n --", &s);
1632 let mut sources = tor_config::ConfigurationSources::new_empty();
1633 sources.push_source(
1634 tor_config::ConfigurationSource::from_verbatim(s.clone()),
1635 tor_config::sources::MustRead::MustRead,
1636 );
1637 sources.load().expect(&s)
1638 }
1639
1640 fn resolve<R: tor_config::load::Resolvable>(&self) -> Result<R, ConfigResolveError> {
1641 tor_config::load::resolve(self.parse())
1642 }
1643
1644 fn resolve_return_results<R: tor_config::load::Resolvable>(
1645 &self,
1646 ) -> Result<ResolutionResults<R>, ConfigResolveError> {
1647 tor_config::load::resolve_return_results(self.parse())
1648 }
1649 }
1650
1651 #[test]
1654 fn builder() {
1655 use tor_config_path::CfgPath;
1656 let sec = std::time::Duration::from_secs(1);
1657
1658 let mut authorities = dir::AuthorityContacts::builder();
1659 authorities.v3idents().push([22; 20].into());
1660
1661 let mut fallback = dir::FallbackDir::builder();
1662 fallback
1663 .rsa_identity([23; 20].into())
1664 .ed_identity([99; 32].into())
1665 .orports()
1666 .push("127.0.0.7:7".parse().unwrap());
1667
1668 let mut bld = ArtiConfig::builder();
1669 let mut bld_tor = TorClientConfig::builder();
1670
1671 bld.proxy().socks_listen(Listen::new_localhost(9999));
1672 bld.logging().console("warn");
1673
1674 *bld_tor.tor_network().authorities() = authorities;
1675 bld_tor.tor_network().set_fallback_caches(vec![fallback]);
1676 bld_tor
1677 .storage()
1678 .cache_dir(CfgPath::new("/var/tmp/foo".to_owned()))
1679 .state_dir(CfgPath::new("/var/tmp/bar".to_owned()));
1680 bld_tor.download_schedule().retry_certs().attempts(10);
1681 bld_tor.download_schedule().retry_certs().initial_delay(sec);
1682 bld_tor.download_schedule().retry_certs().parallelism(3);
1683 bld_tor.download_schedule().retry_microdescs().attempts(30);
1684 bld_tor
1685 .download_schedule()
1686 .retry_microdescs()
1687 .initial_delay(10 * sec);
1688 bld_tor
1689 .download_schedule()
1690 .retry_microdescs()
1691 .parallelism(9);
1692 bld_tor
1693 .override_net_params()
1694 .insert("wombats-per-quokka".to_owned(), 7);
1695 bld_tor
1696 .path_rules()
1697 .ipv4_subnet_family_prefix(20)
1698 .ipv6_subnet_family_prefix(48);
1699 bld_tor.preemptive_circuits().disable_at_threshold(12);
1700 bld_tor
1701 .preemptive_circuits()
1702 .set_initial_predicted_ports(vec![80, 443]);
1703 bld_tor
1704 .preemptive_circuits()
1705 .prediction_lifetime(Duration::from_secs(3600))
1706 .min_exit_circs_for_port(2);
1707 bld_tor
1708 .circuit_timing()
1709 .max_dirtiness(90 * sec)
1710 .request_timeout(10 * sec)
1711 .request_max_retries(22)
1712 .request_loyalty(3600 * sec);
1713 bld_tor.address_filter().allow_local_addrs(true);
1714
1715 let val = bld.build().unwrap();
1716
1717 assert_ne!(val, ArtiConfig::default());
1718 }
1719
1720 #[test]
1721 fn articonfig_application() {
1722 let config = ArtiConfig::default();
1723
1724 let application = config.application();
1725 assert_eq!(&config.application, application);
1726 }
1727
1728 #[test]
1729 fn articonfig_logging() {
1730 let config = ArtiConfig::default();
1731
1732 let logging = config.logging();
1733 assert_eq!(&config.logging, logging);
1734 }
1735
1736 #[test]
1737 fn articonfig_proxy() {
1738 let config = ArtiConfig::default();
1739
1740 let proxy = config.proxy();
1741 assert_eq!(&config.proxy, proxy);
1742 }
1743
1744 fn compat_ports_listen(
1748 f: &str,
1749 get_listen: &dyn Fn(&ArtiConfig) -> &Listen,
1750 bld_get_port: &dyn Fn(&ArtiConfigBuilder) -> &Option<Option<u16>>,
1751 bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option<Listen>,
1752 setter_port: &dyn Fn(&mut ArtiConfigBuilder, Option<u16>) -> &mut ProxyConfigBuilder,
1753 setter_listen: &dyn Fn(&mut ArtiConfigBuilder, Listen) -> &mut ProxyConfigBuilder,
1754 ) {
1755 let from_toml = |s: &str| -> ArtiConfigBuilder {
1756 let cfg: toml::Value = toml::from_str(dbg!(s)).unwrap();
1757 let cfg: ArtiConfigBuilder = cfg.try_into().unwrap();
1758 cfg
1759 };
1760
1761 let conflicting_cfgs = [
1762 format!("proxy.{}_port = 0 \n proxy.{}_listen = 200", f, f),
1763 format!("proxy.{}_port = 100 \n proxy.{}_listen = 0", f, f),
1764 format!("proxy.{}_port = 100 \n proxy.{}_listen = 200", f, f),
1765 ];
1766
1767 let chk = |cfg: &ArtiConfigBuilder, expected: &Listen| {
1768 dbg!(bld_get_listen(cfg), bld_get_port(cfg));
1769 let cfg = cfg.build().unwrap();
1770 assert_eq!(get_listen(&cfg), expected);
1771 };
1772
1773 let check_setters = |port, expected: &_| {
1774 for cfg in chain!(
1775 iter::once(ArtiConfig::builder()),
1776 conflicting_cfgs.iter().map(|cfg| from_toml(cfg)),
1777 ) {
1778 for listen in match port {
1779 None => vec![Listen::new_none(), Listen::new_localhost(0)],
1780 Some(port) => vec![Listen::new_localhost(port)],
1781 } {
1782 let mut cfg = cfg.clone();
1783 setter_port(&mut cfg, dbg!(port));
1784 setter_listen(&mut cfg, dbg!(listen));
1785 chk(&cfg, expected);
1786 }
1787 }
1788 };
1789
1790 {
1791 let expected = Listen::new_localhost(100);
1792
1793 let cfg = from_toml(&format!("proxy.{}_port = 100", f));
1794 assert_eq!(bld_get_port(&cfg), &Some(Some(100)));
1795 chk(&cfg, &expected);
1796
1797 let cfg = from_toml(&format!("proxy.{}_listen = 100", f));
1798 assert_eq!(bld_get_listen(&cfg), &Some(Listen::new_localhost(100)));
1799 chk(&cfg, &expected);
1800
1801 let cfg = from_toml(&format!(
1802 "proxy.{}_port = 100\n proxy.{}_listen = 100",
1803 f, f
1804 ));
1805 chk(&cfg, &expected);
1806
1807 check_setters(Some(100), &expected);
1808 }
1809
1810 {
1811 let expected = Listen::new_none();
1812
1813 let cfg = from_toml(&format!("proxy.{}_port = 0", f));
1814 chk(&cfg, &expected);
1815
1816 let cfg = from_toml(&format!("proxy.{}_listen = 0", f));
1817 chk(&cfg, &expected);
1818
1819 let cfg = from_toml(&format!("proxy.{}_port = 0 \n proxy.{}_listen = 0", f, f));
1820 chk(&cfg, &expected);
1821
1822 check_setters(None, &expected);
1823 }
1824
1825 for cfg in &conflicting_cfgs {
1826 let cfg = from_toml(cfg);
1827 let err = dbg!(cfg.build()).unwrap_err();
1828 assert!(err.to_string().contains("specifying different values"));
1829 }
1830 }
1831
1832 #[test]
1833 #[allow(deprecated)]
1834 fn ports_listen_socks() {
1835 compat_ports_listen(
1836 "socks",
1837 &|cfg| &cfg.proxy.socks_listen,
1838 &|bld| &bld.proxy.socks_port,
1839 &|bld| &bld.proxy.socks_listen,
1840 &|bld, arg| bld.proxy.socks_port(arg),
1841 &|bld, arg| bld.proxy.socks_listen(arg),
1842 );
1843 }
1844
1845 #[test]
1846 #[allow(deprecated)]
1847 fn compat_ports_listen_dns() {
1848 compat_ports_listen(
1849 "dns",
1850 &|cfg| &cfg.proxy.dns_listen,
1851 &|bld| &bld.proxy.dns_port,
1852 &|bld| &bld.proxy.dns_listen,
1853 &|bld, arg| bld.proxy.dns_port(arg),
1854 &|bld, arg| bld.proxy.dns_listen(arg),
1855 );
1856 }
1857}