1use derive_deftly::Deftly;
6use tor_config_path::CfgPath;
7
8#[cfg(feature = "onion-service-service")]
9use crate::onion_proxy::{
10 OnionServiceProxyConfigBuilder, OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder,
11};
12#[cfg(feature = "rpc")]
13semipublic_use! {
14 use crate::rpc::{
15 RpcConfig, RpcConfigBuilder,
16 listener::{RpcListenerSetConfig, RpcListenerSetConfigBuilder},
17 };
18}
19use arti_client::TorClientConfig;
20#[cfg(feature = "onion-service-service")]
21use tor_config::define_list_builder_accessors;
22use tor_config::derive::prelude::*;
23pub(crate) use tor_config::{ConfigBuildError, Listen};
24
25use crate::{LoggingConfig, LoggingConfigBuilder};
26
27#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
32pub(crate) const ARTI_EXAMPLE_CONFIG: &str = concat!(include_str!("./arti-example-config.toml"));
33
34#[cfg(test)]
51const OLDEST_SUPPORTED_CONFIG: &str = concat!(include_str!("./oldest-supported-config.toml"),);
52
53#[cfg(not(feature = "rpc"))]
55type RpcConfig = ();
56
57#[cfg(not(feature = "onion-service-service"))]
59type OnionServiceProxyConfigMap = ();
60
61#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
63#[derive_deftly(TorConfig)]
64#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
65#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
66pub(crate) struct ApplicationConfig {
67 #[deftly(tor_config(default))]
75 pub(crate) watch_configuration: bool,
76
77 #[deftly(tor_config(default))]
87 pub(crate) permit_debugging: bool,
88
89 #[deftly(tor_config(default))]
93 pub(crate) allow_running_as_root: bool,
94}
95
96#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
98#[derive_deftly(TorConfig)]
99#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
100#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
101pub(crate) struct ProxyConfig {
102 #[deftly(tor_config(default = "Listen::new_localhost(9150)"))]
106 pub(crate) socks_listen: Listen,
107
108 #[deftly(tor_config(default = "Listen::new_none()"))]
110 pub(crate) dns_listen: Listen,
111
112 #[deftly(tor_config(
118 cfg = r#" feature="http-connect" "#,
119 cfg_desc = "with HTTP CONNECT support"
120 ))]
121 #[deftly(tor_config(default = "true"))]
122 pub(crate) enable_http_connect: bool,
123}
124
125impl ProxyConfig {
126 pub(crate) fn protocols(&self) -> crate::proxy::ListenProtocols {
128 use crate::proxy::ListenProtocols::*;
129 #[cfg(feature = "http-connect")]
130 if self.enable_http_connect {
131 return SocksAndHttpConnect;
132 }
133
134 SocksOnly
135 }
136}
137
138#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
142#[derive_deftly(TorConfig)]
143#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
144#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
145pub(crate) struct ArtiStorageConfig {
146 #[deftly(tor_config(setter(into), default = "default_port_info_file()"))]
148 pub(crate) port_info_file: CfgPath,
149}
150
151fn default_port_info_file() -> CfgPath {
153 CfgPath::new("${ARTI_LOCAL_DATA}/public/port_info.json".to_owned())
154}
155
156#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
170#[derive_deftly(TorConfig)]
171#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
172#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
173#[non_exhaustive]
174pub(crate) struct SystemConfig {
175 #[deftly(tor_config(setter(into), default = "default_max_files()"))]
177 pub(crate) max_files: u64,
178}
179
180fn default_max_files() -> u64 {
182 16384
183}
184
185#[derive(Debug, Deftly, Clone, Eq, PartialEq)]
200#[derive_deftly(TorConfig)]
201#[deftly(tor_config(post_build = "Self::post_build"))]
202#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
203#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
204pub(crate) struct ArtiConfig {
205 #[deftly(tor_config(sub_builder))]
207 application: ApplicationConfig,
208
209 #[deftly(tor_config(sub_builder))]
211 proxy: ProxyConfig,
212
213 #[deftly(tor_config(sub_builder))]
215 logging: LoggingConfig,
216
217 #[deftly(tor_config(sub_builder))]
219 pub(crate) metrics: MetricsConfig,
220
221 #[deftly(tor_config(
223 sub_builder,
224 cfg = r#" feature = "rpc" "#,
225 cfg_desc = "with RPC support"
226 ))]
227 pub(crate) rpc: RpcConfig,
228
229 #[deftly(tor_config(sub_builder))]
235 pub(crate) system: SystemConfig,
236
237 #[deftly(tor_config(sub_builder))]
242 pub(crate) storage: ArtiStorageConfig,
243
244 #[deftly(tor_config(
253 setter(skip),
254 sub_builder,
255 cfg = r#" feature = "onion-service-service" "#,
256 cfg_reject,
257 cfg_desc = "with onion service support"
258 ))]
259 pub(crate) onion_services: OnionServiceProxyConfigMap,
260}
261
262impl ArtiConfigBuilder {
263 #[allow(clippy::unnecessary_wraps)]
265 fn post_build(config: ArtiConfig) -> Result<ArtiConfig, ConfigBuildError> {
266 #[cfg_attr(not(feature = "onion-service-service"), allow(unused_mut))]
267 let mut config = config;
268 #[cfg(feature = "onion-service-service")]
269 for svc in config.onion_services.values_mut() {
270 *svc.svc_cfg
272 .restricted_discovery_mut()
273 .watch_configuration_mut() = config.application.watch_configuration;
274 }
275
276 Ok(config)
277 }
278}
279
280impl tor_config::load::TopLevel for ArtiConfig {
281 type Builder = ArtiConfigBuilder;
282 const DEPRECATED_KEYS: &'static [&'static str] = &["proxy.socks_port", "proxy.dns_port"];
287}
288
289#[cfg(feature = "onion-service-service")]
290define_list_builder_accessors! {
291 struct ArtiConfigBuilder {
292 pub(crate) onion_services: [OnionServiceProxyConfigBuilder],
293 }
294}
295
296#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
300pub(crate) type ArtiCombinedConfig = (ArtiConfig, TorClientConfig);
301
302#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
304#[derive_deftly(TorConfig)]
305#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
306#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
307pub(crate) struct MetricsConfig {
308 #[deftly(tor_config(sub_builder))]
310 pub(crate) prometheus: PrometheusConfig,
311}
312
313#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
315#[derive_deftly(TorConfig)]
316#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
317#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
318pub(crate) struct PrometheusConfig {
319 #[deftly(tor_config(default))]
328 pub(crate) listen: Listen,
329}
330
331impl ArtiConfig {
332 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
334 pub(crate) fn application(&self) -> &ApplicationConfig {
335 &self.application
336 }
337
338 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
340 pub(crate) fn logging(&self) -> &LoggingConfig {
341 &self.logging
342 }
343
344 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
346 pub(crate) fn proxy(&self) -> &ProxyConfig {
347 &self.proxy
348 }
349
350 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
352 pub(crate) fn storage(&self) -> &ArtiStorageConfig {
354 &self.storage
355 }
356
357 #[cfg(feature = "rpc")]
359 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
360 pub(crate) fn rpc(&self) -> &RpcConfig {
361 &self.rpc
362 }
363}
364
365#[cfg(test)]
366mod test {
367 #![allow(clippy::bool_assert_comparison)]
369 #![allow(clippy::clone_on_copy)]
370 #![allow(clippy::dbg_macro)]
371 #![allow(clippy::mixed_attributes_style)]
372 #![allow(clippy::print_stderr)]
373 #![allow(clippy::print_stdout)]
374 #![allow(clippy::single_char_pattern)]
375 #![allow(clippy::unwrap_used)]
376 #![allow(clippy::unchecked_time_subtraction)]
377 #![allow(clippy::useless_vec)]
378 #![allow(clippy::needless_pass_by_value)]
379 #![allow(clippy::iter_overeager_cloned)]
382 #![cfg_attr(not(feature = "pt-client"), allow(dead_code))]
384
385 use arti_client::config::dir;
386 use arti_client::config::TorClientConfigBuilder;
387 use itertools::{chain, EitherOrBoth, Itertools};
388 use regex::Regex;
389 use std::collections::HashSet;
390 use std::fmt::Write as _;
391 use std::iter;
392 use std::time::Duration;
393 use tor_config::load::{ConfigResolveError, ResolutionResults};
394 use tor_config_path::CfgPath;
395
396 #[allow(unused_imports)] use tor_error::ErrorReport as _;
398
399 #[cfg(feature = "restricted-discovery")]
400 use {
401 arti_client::HsClientDescEncKey,
402 std::str::FromStr as _,
403 tor_hsservice::config::restricted_discovery::{
404 DirectoryKeyProviderBuilder, HsClientNickname,
405 },
406 };
407
408 use super::*;
409
410 fn uncomment_example_settings(template: &str) -> String {
417 let re = Regex::new(r#"(?m)^\#([^ \n])"#).unwrap();
418 re.replace_all(template, |cap: ®ex::Captures<'_>| -> _ {
419 cap.get(1).unwrap().as_str().to_string()
420 })
421 .into()
422 }
423
424 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
433 enum InExample {
434 Absent,
435 Present,
436 }
437 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
443 enum WhichExample {
444 Old,
445 New,
446 }
447 #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
453 struct ConfigException {
454 key: String,
456 in_old_example: InExample,
458 in_new_example: InExample,
460 in_code: Option<bool>,
462 }
463 impl ConfigException {
464 fn in_example(&self, which: WhichExample) -> InExample {
465 use WhichExample::*;
466 match which {
467 Old => self.in_old_example,
468 New => self.in_new_example,
469 }
470 }
471 }
472
473 const ALL_RELEVANT_FEATURES_ENABLED: bool = cfg!(all(
475 feature = "bridge-client",
476 feature = "pt-client",
477 feature = "onion-service-client",
478 feature = "rpc",
479 ));
480
481 fn declared_config_exceptions() -> Vec<ConfigException> {
483 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
488 enum InCode {
489 Ignored,
491 FeatureDependent,
499 Recognized,
501 }
502 use InCode::*;
503
504 struct InOld;
506 struct InNew;
508
509 let mut out = vec![];
510
511 let mut declare_exceptions = |in_old_example: Option<InOld>,
524 in_new_example: Option<InNew>,
525 in_code: InCode,
526 keys: &[&str]| {
527 let in_code = match in_code {
528 Ignored => Some(false),
529 Recognized => Some(true),
530 FeatureDependent if ALL_RELEVANT_FEATURES_ENABLED => Some(true),
531 FeatureDependent => None,
532 };
533 #[allow(clippy::needless_pass_by_value)] fn in_example<T>(spec: Option<T>) -> InExample {
535 match spec {
536 None => InExample::Absent,
537 Some(_) => InExample::Present,
538 }
539 }
540 let in_old_example = in_example(in_old_example);
541 let in_new_example = in_example(in_new_example);
542 out.extend(keys.iter().cloned().map(|key| ConfigException {
543 key: key.to_owned(),
544 in_old_example,
545 in_new_example,
546 in_code,
547 }));
548 };
549
550 declare_exceptions(
551 None,
552 Some(InNew),
553 Recognized,
554 &[
555 "application.allow_running_as_root",
557 "bridges",
558 "logging.syslog",
559 "logging.time_granularity",
560 "path_rules.long_lived_ports",
561 "use_obsolete_software",
562 "circuit_timing.disused_circuit_timeout",
563 "storage.port_info_file",
564 ],
565 );
566
567 declare_exceptions(
568 None,
569 None,
570 Recognized,
571 &[
572 "tor_network.authorities",
574 "tor_network.fallback_caches",
575 ],
576 );
577
578 declare_exceptions(
579 None,
580 None,
581 Recognized,
582 &[
583 "logging.opentelemetry",
585 ],
586 );
587
588 declare_exceptions(
589 Some(InOld),
590 Some(InNew),
591 if cfg!(target_family = "windows") {
592 Ignored
593 } else {
594 Recognized
595 },
596 &[
597 "storage.permissions.trust_group",
599 "storage.permissions.trust_user",
600 ],
601 );
602
603 declare_exceptions(
604 None,
605 None, FeatureDependent,
607 &[
608 "bridges.transports", ],
611 );
612
613 declare_exceptions(
614 None,
615 Some(InNew),
616 FeatureDependent,
617 &[
618 "storage.keystore",
620 ],
621 );
622
623 declare_exceptions(
624 None,
625 None, FeatureDependent,
627 &[
628 "logging.tokio_console",
630 "logging.tokio_console.enabled",
631 ],
632 );
633
634 declare_exceptions(
635 None,
636 None, Recognized,
638 &[
639 "system.memory",
641 "system.memory.max",
642 "system.memory.low_water",
643 ],
644 );
645
646 declare_exceptions(
647 None,
648 Some(InNew), Recognized,
650 &["metrics"],
651 );
652
653 declare_exceptions(
654 None,
655 None, Recognized,
657 &[
658 "metrics.prometheus",
660 "metrics.prometheus.listen",
661 ],
662 );
663
664 declare_exceptions(
665 None,
666 Some(InNew),
667 FeatureDependent,
668 &[
669 ],
671 );
672
673 declare_exceptions(
674 None,
675 Some(InNew),
676 FeatureDependent,
677 &[
678 "address_filter.allow_onion_addrs",
680 "circuit_timing.hs_desc_fetch_attempts",
681 "circuit_timing.hs_intro_rend_attempts",
682 ],
683 );
684
685 declare_exceptions(
686 None,
687 Some(InNew),
688 FeatureDependent,
689 &[
690 "proxy.enable_http_connect",
692 ],
693 );
694
695 declare_exceptions(
696 None,
697 None, FeatureDependent,
699 &[
700 "rpc",
702 "rpc.rpc_listen",
703 ],
704 );
705
706 declare_exceptions(
708 None,
709 None,
710 FeatureDependent,
711 &[
712 "onion_services",
714 ],
715 );
716
717 declare_exceptions(
718 None,
719 Some(InNew),
720 FeatureDependent,
721 &[
722 "vanguards",
724 "vanguards.mode",
725 ],
726 );
727
728 declare_exceptions(
730 None,
731 None,
732 FeatureDependent,
733 &[
734 "storage.keystore.ctor",
735 "storage.keystore.ctor.services",
736 "storage.keystore.ctor.clients",
737 ],
738 );
739
740 out.sort();
741
742 let dupes = out.iter().map(|exc| &exc.key).duplicates().collect_vec();
743 assert!(
744 dupes.is_empty(),
745 "duplicate exceptions in configuration {dupes:?}"
746 );
747
748 eprintln!(
749 "declared config exceptions for this configuration:\n{:#?}",
750 &out
751 );
752 out
753 }
754
755 #[test]
756 fn default_config() {
757 use InExample::*;
758
759 let empty_config = tor_config::ConfigurationSources::new_empty()
760 .load()
761 .unwrap();
762 let empty_config: ArtiCombinedConfig = tor_config::resolve(empty_config).unwrap();
763
764 let default = (ArtiConfig::default(), TorClientConfig::default());
765 let exceptions = declared_config_exceptions();
766
767 #[allow(clippy::needless_pass_by_value)] fn analyse_joined_info(
778 which: WhichExample,
779 uncommented: bool,
780 eob: EitherOrBoth<&String, &ConfigException>,
781 ) -> Result<(), (String, String)> {
782 use EitherOrBoth::*;
783 let (key, err) = match eob {
784 Left(found) => (found, "found in example but not processed".into()),
786 Both(found, exc) => {
787 let but = match (exc.in_example(which), exc.in_code, uncommented) {
788 (Absent, _, _) => "but exception entry expected key to be absent",
789 (_, _, false) => "when processing still-commented-out file!",
790 (_, Some(true), _) => {
791 "but an exception entry says it should have been recognised"
792 }
793 (Present, Some(false), true) => return Ok(()), (Present, None, true) => return Ok(()), };
796 (
797 found,
798 format!("parser reported unrecognised config key, {but}"),
799 )
800 }
801 Right(exc) => {
802 let trouble = match (exc.in_example(which), exc.in_code, uncommented) {
807 (Absent, _, _) => return Ok(()), (_, _, false) => return Ok(()), (_, Some(true), _) => return Ok(()), (Present, Some(false), true) => {
811 "expected an 'unknown config key' report but didn't see one"
812 }
813 (Present, None, true) => return Ok(()), };
815 (&exc.key, trouble.into())
816 }
817 };
818 Err((key.clone(), err))
819 }
820
821 let parses_to_defaults = |example: &str, which: WhichExample, uncommented: bool| {
822 let cfg = {
823 let mut sources = tor_config::ConfigurationSources::new_empty();
824 sources.push_source(
825 tor_config::ConfigurationSource::from_verbatim(example.to_string()),
826 tor_config::sources::MustRead::MustRead,
827 );
828 sources.load().unwrap()
829 };
830
831 let results: ResolutionResults<ArtiCombinedConfig> =
833 tor_config::resolve_return_results(cfg).unwrap();
834
835 assert_eq!(&results.value, &default, "{which:?} {uncommented:?}");
836 assert_eq!(&results.value, &empty_config, "{which:?} {uncommented:?}");
837
838 let unrecognized = results
841 .unrecognized
842 .iter()
843 .map(|k| k.to_string())
844 .collect_vec();
845
846 eprintln!(
847 "parsing of {which:?} uncommented={uncommented:?}, unrecognized={unrecognized:#?}"
848 );
849
850 let reports =
851 Itertools::merge_join_by(unrecognized.iter(), exceptions.iter(), |u, e| {
852 u.as_str().cmp(&e.key)
853 })
854 .filter_map(|eob| analyse_joined_info(which, uncommented, eob).err())
855 .collect_vec();
856
857 if !reports.is_empty() {
858 let reports = reports.iter().fold(String::new(), |mut out, (k, s)| {
859 writeln!(out, " {}: {}", s, k).unwrap();
860 out
861 });
862
863 panic!(
864 r"
865mismatch: results of parsing example files (& vs declared exceptions):
866example config file {which:?}, uncommented={uncommented:?}
867{reports}
868"
869 );
870 }
871
872 results.value
873 };
874
875 let _ = parses_to_defaults(ARTI_EXAMPLE_CONFIG, WhichExample::New, false);
876 let _ = parses_to_defaults(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, false);
877
878 let built_default = (
879 ArtiConfigBuilder::default().build().unwrap(),
880 TorClientConfigBuilder::default().build().unwrap(),
881 );
882
883 let parsed = parses_to_defaults(
884 &uncomment_example_settings(ARTI_EXAMPLE_CONFIG),
885 WhichExample::New,
886 true,
887 );
888 let parsed_old = parses_to_defaults(
889 &uncomment_example_settings(OLDEST_SUPPORTED_CONFIG),
890 WhichExample::Old,
891 true,
892 );
893
894 assert_eq!(&parsed, &built_default);
895 assert_eq!(&parsed_old, &built_default);
896
897 assert_eq!(&default, &built_default);
898 }
899
900 fn exhaustive_1(example_file: &str, which: WhichExample, deprecated: &[String]) {
932 use serde_json::Value as JsValue;
933 use std::collections::BTreeSet;
934 use InExample::*;
935
936 let example = uncomment_example_settings(example_file);
937 let example: toml::Value = toml::from_str(&example).unwrap();
938 let example = serde_json::to_value(example).unwrap();
940 let exhausts = [
950 serde_json::to_value(TorClientConfig::builder()).unwrap(),
951 serde_json::to_value(ArtiConfig::builder()).unwrap(),
952 ];
953
954 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, derive_more::Display)]
957 enum ProblemKind {
958 #[display("recognised by serialisation, but missing from example config file")]
959 MissingFromExample,
960 #[display("expected that example config file should contain have this as a table")]
961 ExpectedTableInExample,
962 #[display(
963 "declared exception says this key should be recognised but not in file, but that doesn't seem to be the case"
964 )]
965 UnusedException,
966 }
967
968 #[derive(Default, Debug)]
969 struct Walk {
970 current_path: Vec<String>,
971 problems: Vec<(String, ProblemKind)>,
972 }
973
974 impl Walk {
975 fn bad(&mut self, kind: ProblemKind) {
977 self.problems.push((self.current_path.join("."), kind));
978 }
979
980 fn walk<const E: usize>(
987 &mut self,
988 example: Option<&JsValue>,
989 exhausts: [Option<&JsValue>; E],
990 ) {
991 assert! { exhausts.into_iter().any(|e| e.is_some()) }
992
993 let example = if let Some(e) = example {
994 e
995 } else {
996 self.bad(ProblemKind::MissingFromExample);
997 return;
998 };
999
1000 let tables = exhausts.map(|e| e?.as_object());
1001
1002 let table_keys = tables
1004 .iter()
1005 .flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten())
1006 .collect::<BTreeSet<String>>();
1007
1008 for key in table_keys {
1009 let example = if let Some(e) = example.as_object() {
1010 e
1011 } else {
1012 self.bad(ProblemKind::ExpectedTableInExample);
1015 continue;
1016 };
1017
1018 self.current_path.push(key.clone());
1020 self.walk(example.get(&key), tables.map(|t| t?.get(&key)));
1021 self.current_path.pop().unwrap();
1022 }
1023 }
1024 }
1025
1026 let exhausts = exhausts.iter().map(Some).collect_vec().try_into().unwrap();
1027
1028 let mut walk = Walk::default();
1029 walk.walk::<2>(Some(&example), exhausts);
1030 let mut problems = walk.problems;
1031
1032 #[derive(Debug, Copy, Clone)]
1034 struct DefinitelyRecognized;
1035
1036 let expect_missing = declared_config_exceptions()
1037 .iter()
1038 .filter_map(|exc| {
1039 let definitely = match (exc.in_example(which), exc.in_code) {
1040 (Present, _) => return None, (_, Some(false)) => return None, (Absent, Some(true)) => Some(DefinitelyRecognized),
1043 (Absent, None) => None, };
1045 Some((exc.key.clone(), definitely))
1046 })
1047 .collect_vec();
1048 dbg!(&expect_missing);
1049
1050 let expect_missing: Vec<(String, Option<DefinitelyRecognized>)> = expect_missing
1059 .iter()
1060 .cloned()
1061 .filter({
1062 let original: HashSet<_> = expect_missing.iter().map(|(k, _)| k.clone()).collect();
1063 move |(found, _)| {
1064 !found
1065 .match_indices('.')
1066 .any(|(doti, _)| original.contains(&found[0..doti]))
1067 }
1068 })
1069 .collect_vec();
1070 dbg!(&expect_missing);
1071
1072 for (exp, definitely) in expect_missing {
1073 let was = problems.len();
1074 problems.retain(|(path, _)| path != &exp);
1075 if problems.len() == was && definitely.is_some() {
1076 problems.push((exp, ProblemKind::UnusedException));
1077 }
1078 }
1079
1080 let problems = problems
1081 .into_iter()
1082 .filter(|(key, _kind)| !deprecated.iter().any(|dep| key == dep))
1083 .map(|(path, m)| format!(" config key {:?}: {}", path, m))
1084 .collect_vec();
1085
1086 assert!(
1089 problems.is_empty(),
1090 "example config {which:?} exhaustiveness check failed: {}\n-----8<-----\n{}\n-----8<-----\n",
1091 problems.join("\n"),
1092 example_file,
1093 );
1094 }
1095
1096 #[test]
1097 fn exhaustive() {
1098 let mut deprecated = vec![];
1099 <(ArtiConfig, TorClientConfig) as tor_config::load::Resolvable>::enumerate_deprecated_keys(
1100 &mut |l| {
1101 for k in l {
1102 deprecated.push(k.to_string());
1103 }
1104 },
1105 );
1106 let deprecated = deprecated.iter().cloned().collect_vec();
1107
1108 exhaustive_1(ARTI_EXAMPLE_CONFIG, WhichExample::New, &deprecated);
1113
1114 exhaustive_1(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, &deprecated);
1121 }
1122
1123 #[cfg_attr(feature = "pt-client", allow(dead_code))]
1125 fn expect_err_contains(err: ConfigResolveError, exp: &str) {
1126 use std::error::Error as StdError;
1127 let err: Box<dyn StdError> = Box::new(err);
1128 let err = tor_error::Report(err).to_string();
1129 assert!(
1130 err.contains(exp),
1131 "wrong message, got {:?}, exp {:?}",
1132 err,
1133 exp,
1134 );
1135 }
1136
1137 #[test]
1138 fn bridges() {
1139 let filter_examples = |#[allow(unused_mut)] mut examples: ExampleSectionLines| -> _ {
1153 if cfg!(all(feature = "bridge-client", not(feature = "pt-client"))) {
1155 let looks_like_addr =
1156 |l: &str| l.starts_with(|c: char| c.is_ascii_digit() || c == '[');
1157 examples.lines.retain(|l| looks_like_addr(l));
1158 }
1159
1160 examples
1161 };
1162
1163 let resolve_examples = |examples: &ExampleSectionLines| {
1168 #[cfg(all(feature = "bridge-client", not(feature = "pt-client")))]
1170 {
1171 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1172 expect_err_contains(err, "support disabled in cargo features");
1173 }
1174
1175 let examples = filter_examples(examples.clone());
1176
1177 #[cfg(feature = "bridge-client")]
1178 {
1179 examples.resolve::<TorClientConfig>().unwrap()
1180 }
1181
1182 #[cfg(not(feature = "bridge-client"))]
1183 {
1184 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1185 expect_err_contains(err, "support disabled in cargo features");
1186 ((),)
1188 }
1189 };
1190
1191 let mut examples = ExampleSectionLines::from_section("bridges");
1193 examples.narrow((r#"^# For example:"#, true), NARROW_NONE);
1194
1195 let compare = {
1196 let mut examples = examples.clone();
1198 examples.narrow((r#"^# bridges = '''"#, true), (r#"^# '''"#, true));
1199 examples.uncomment();
1200
1201 let parsed = resolve_examples(&examples);
1202
1203 examples.lines.remove(0);
1206 examples.lines.remove(examples.lines.len() - 1);
1207 examples.expect_lines(3);
1209
1210 #[cfg(feature = "bridge-client")]
1212 {
1213 let examples = filter_examples(examples);
1214 let mut built = TorClientConfig::builder();
1215 for l in &examples.lines {
1216 built.bridges().bridges().push(l.trim().parse().expect(l));
1217 }
1218 let built = built.build().unwrap();
1219
1220 assert_eq!(&parsed, &built);
1221 }
1222
1223 parsed
1224 };
1225
1226 {
1228 examples.narrow((r#"^# bridges = \["#, true), (r#"^# \]"#, true));
1229 examples.uncomment();
1230 let parsed = resolve_examples(&examples);
1231 assert_eq!(&parsed, &compare);
1232 }
1233 }
1234
1235 #[test]
1236 fn transports() {
1237 let mut file =
1243 ExampleSectionLines::from_markers("# An example managed pluggable transport", "[");
1244 file.lines.retain(|line| line.starts_with("# "));
1245 file.uncomment();
1246
1247 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1248 let cfg_got = result.unwrap();
1249
1250 #[cfg(feature = "pt-client")]
1251 {
1252 use arti_client::config::{pt::TransportConfig, BridgesConfig};
1253 use tor_config_path::CfgPath;
1254
1255 let bridges_got: &BridgesConfig = cfg_got.0.as_ref();
1256
1257 let mut bld = BridgesConfig::builder();
1259 {
1260 let mut b = TransportConfig::builder();
1261 b.protocols(vec!["obfs4".parse().unwrap(), "obfs5".parse().unwrap()]);
1262 b.path(CfgPath::new("/usr/bin/obfsproxy".to_string()));
1263 b.arguments(vec!["-obfs4".to_string(), "-obfs5".to_string()]);
1264 b.run_on_startup(true);
1265 bld.transports().push(b);
1266 }
1267 {
1268 let mut b = TransportConfig::builder();
1269 b.protocols(vec!["obfs4".parse().unwrap()]);
1270 b.proxy_addr("127.0.0.1:31337".parse().unwrap());
1271 bld.transports().push(b);
1272 }
1273
1274 let bridges_expected = bld.build().unwrap();
1275 assert_eq!(&bridges_expected, bridges_got);
1276 }
1277 }
1278
1279 #[test]
1280 fn memquota() {
1281 let mut file = ExampleSectionLines::from_section("system");
1284 file.lines.retain(|line| line.starts_with("# memory."));
1285 file.uncomment();
1286
1287 let result = file.resolve_return_results::<(TorClientConfig, ArtiConfig)>();
1288
1289 let result = result.unwrap();
1290
1291 assert_eq!(result.unrecognized, []);
1293 assert_eq!(result.deprecated, []);
1294
1295 let inner: &tor_memquota::testing::ConfigInner =
1296 result.value.0.system_memory().inner().unwrap();
1297
1298 let defaulted_low = tor_memquota::Config::builder()
1301 .max(*inner.max)
1302 .build()
1303 .unwrap();
1304 let inner_defaulted_low = defaulted_low.inner().unwrap();
1305 assert_eq!(inner, inner_defaulted_low);
1306 }
1307
1308 #[test]
1309 fn metrics() {
1310 let mut file = ExampleSectionLines::from_section("metrics");
1312 file.lines
1313 .retain(|line| line.starts_with("# prometheus."));
1314 file.uncomment();
1315
1316 let result = file
1317 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1318 .unwrap();
1319
1320 assert_eq!(result.unrecognized, []);
1322 assert_eq!(result.deprecated, []);
1323
1324 assert_eq!(
1326 result
1327 .value
1328 .1
1329 .metrics
1330 .prometheus
1331 .listen
1332 .single_address_legacy()
1333 .unwrap(),
1334 Some("127.0.0.1:9035".parse().unwrap()),
1335 );
1336
1337 }
1340
1341 #[test]
1342 fn onion_services() {
1343 let mut file = ExampleSectionLines::from_markers("##### ONION SERVICES", "##### RPC");
1347 file.lines.retain(|line| line.starts_with("# "));
1348 file.uncomment();
1349
1350 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1351 #[cfg(feature = "onion-service-service")]
1352 {
1353 let svc_expected = {
1354 use tor_hsrproxy::config::*;
1355 let mut b = OnionServiceProxyConfigBuilder::default();
1356 b.service().nickname("allium-cepa".parse().unwrap());
1357 b.proxy().proxy_ports().push(ProxyRule::new(
1358 ProxyPattern::one_port(80).unwrap(),
1359 ProxyAction::Forward(
1360 Encapsulation::Simple,
1361 TargetAddr::Inet("127.0.0.1:10080".parse().unwrap()),
1362 ),
1363 ));
1364 b.proxy().proxy_ports().push(ProxyRule::new(
1365 ProxyPattern::one_port(22).unwrap(),
1366 ProxyAction::DestroyCircuit,
1367 ));
1368 b.proxy().proxy_ports().push(ProxyRule::new(
1369 ProxyPattern::one_port(265).unwrap(),
1370 ProxyAction::IgnoreStream,
1371 ));
1372 b.proxy().proxy_ports().push(ProxyRule::new(
1382 ProxyPattern::one_port(443).unwrap(),
1383 ProxyAction::RejectStream,
1384 ));
1385 b.proxy().proxy_ports().push(ProxyRule::new(
1386 ProxyPattern::all_ports(),
1387 ProxyAction::DestroyCircuit,
1388 ));
1389
1390 #[cfg(feature = "restricted-discovery")]
1391 {
1392 const ALICE_KEY: &str =
1393 "descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA";
1394 const BOB_KEY: &str =
1395 "descriptor:x25519:b5zqgtpermmuda6vc63lhjuf5ihpokjmuk26ly2xksf7vg52aesq";
1396 for (nickname, key) in [("alice", ALICE_KEY), ("bob", BOB_KEY)] {
1397 b.service()
1398 .restricted_discovery()
1399 .enabled(true)
1400 .static_keys()
1401 .access()
1402 .push((
1403 HsClientNickname::from_str(nickname).unwrap(),
1404 HsClientDescEncKey::from_str(key).unwrap(),
1405 ));
1406 }
1407 let mut dir = DirectoryKeyProviderBuilder::default();
1408 dir.path(CfgPath::new(
1409 "/var/lib/tor/hidden_service/authorized_clients".to_string(),
1410 ));
1411
1412 b.service()
1413 .restricted_discovery()
1414 .key_dirs()
1415 .access()
1416 .push(dir);
1417 }
1418
1419 b.build().unwrap()
1420 };
1421
1422 cfg_if::cfg_if! {
1423 if #[cfg(feature = "restricted-discovery")] {
1424 let cfg = result.unwrap();
1425 let services = cfg.1.onion_services;
1426 assert_eq!(services.len(), 1);
1427 let svc = services.values().next().unwrap();
1428 assert_eq!(svc, &svc_expected);
1429 } else {
1430 expect_err_contains(
1431 result.unwrap_err(),
1432 "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
1433 );
1434 }
1435 }
1436 }
1437 #[cfg(not(feature = "onion-service-service"))]
1438 {
1439 expect_err_contains(result.unwrap_err(), "not built with onion service support");
1440 }
1441 }
1442
1443 #[cfg(feature = "rpc")]
1444 #[test]
1445 fn rpc_defaults() {
1446 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1447 file.lines
1451 .retain(|line| line.starts_with("# ") && !line.starts_with("# "));
1452 file.uncomment();
1453
1454 let parsed = file
1455 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1456 .unwrap();
1457 assert!(parsed.unrecognized.is_empty());
1458 assert!(parsed.deprecated.is_empty());
1459 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1460 let rpc_default = RpcConfig::default();
1461 assert_eq!(rpc_parsed, &rpc_default);
1462 }
1463
1464 #[cfg(feature = "rpc")]
1465 #[test]
1466 fn rpc_full() {
1467 use crate::rpc::listener::{ConnectPointOptionsBuilder, RpcListenerSetConfigBuilder};
1468
1469 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1471 file.lines
1473 .retain(|line| line.starts_with("# ") && !line.contains("file ="));
1474 file.uncomment();
1475
1476 let parsed = file
1477 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1478 .unwrap();
1479 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1480
1481 let expected = {
1482 let mut bld_opts = ConnectPointOptionsBuilder::default();
1483 bld_opts.enable(false);
1484
1485 let mut bld_set = RpcListenerSetConfigBuilder::default();
1486 bld_set.dir(CfgPath::new("${HOME}/.my_connect_files/".to_string()));
1487 bld_set.listener_options().enable(true);
1488 bld_set
1489 .file_options()
1490 .insert("bad_file.json".to_string(), bld_opts);
1491
1492 let mut bld = RpcConfigBuilder::default();
1493 bld.listen().insert("label".to_string(), bld_set);
1494 bld.build().unwrap()
1495 };
1496
1497 assert_eq!(&expected, rpc_parsed);
1498 }
1499
1500 #[derive(Debug, Clone)]
1508 struct ExampleSectionLines {
1509 section: String,
1512 lines: Vec<String>,
1514 }
1515
1516 type NarrowInstruction<'s> = (&'s str, bool);
1519 const NARROW_NONE: NarrowInstruction<'static> = ("?<none>", false);
1521
1522 impl ExampleSectionLines {
1523 fn from_section(section: &str) -> Self {
1527 Self::from_markers(format!("[{section}]"), "[")
1528 }
1529
1530 fn from_markers<S, E>(start: S, end: E) -> Self
1541 where
1542 S: AsRef<str>,
1543 E: AsRef<str>,
1544 {
1545 let (start, end) = (start.as_ref(), end.as_ref());
1546 let mut lines = ARTI_EXAMPLE_CONFIG
1547 .lines()
1548 .skip_while(|line| !line.starts_with(start))
1549 .peekable();
1550 let section = lines
1551 .next_if(|l0| l0.starts_with('['))
1552 .map(|section| section.to_owned())
1553 .unwrap_or_default();
1554 let lines = lines
1555 .take_while(|line| !line.starts_with(end))
1556 .map(|l| l.to_owned())
1557 .collect_vec();
1558
1559 Self { section, lines }
1560 }
1561
1562 fn narrow(&mut self, start: NarrowInstruction, end: NarrowInstruction) {
1565 let find_index = |(re, include), start_pos, exactly_one: bool, adjust: [isize; 2]| {
1566 if (re, include) == NARROW_NONE {
1567 return None;
1568 }
1569
1570 let re = Regex::new(re).expect(re);
1571 let i = self
1572 .lines
1573 .iter()
1574 .enumerate()
1575 .skip(start_pos)
1576 .filter(|(_, l)| re.is_match(l))
1577 .map(|(i, _)| i);
1578 let i = if exactly_one {
1579 i.clone().exactly_one().unwrap_or_else(|_| {
1580 panic!("RE={:?} I={:#?} L={:#?}", re, i.collect_vec(), &self.lines)
1581 })
1582 } else {
1583 i.clone().next()?
1584 };
1585
1586 let adjust = adjust[usize::from(include)];
1587 let i = (i as isize + adjust) as usize;
1588 Some(i)
1589 };
1590
1591 eprint!("narrow {:?} {:?}: ", start, end);
1592 let start = find_index(start, 0, true, [1, 0]).unwrap_or(0);
1593 let end = find_index(end, start + 1, false, [0, 1]).unwrap_or(self.lines.len());
1594 eprintln!("{:?} {:?}", start, end);
1595 assert!(start < end, "empty, from {:#?}", &self.lines);
1597 self.lines = self.lines.drain(..).take(end).skip(start).collect_vec();
1598 }
1599
1600 fn expect_lines(&self, n: usize) {
1602 assert_eq!(self.lines.len(), n);
1603 }
1604
1605 fn uncomment(&mut self) {
1607 self.strip_prefix("#");
1608 }
1609
1610 fn strip_prefix(&mut self, prefix: &str) {
1617 for l in &mut self.lines {
1618 if !l.starts_with('[') {
1619 *l = l.strip_prefix(prefix).expect(l).to_string();
1620 }
1621 }
1622 }
1623
1624 fn build_string(&self) -> String {
1626 chain!(iter::once(&self.section), self.lines.iter(),).join("\n")
1627 }
1628
1629 fn parse(&self) -> tor_config::ConfigurationTree {
1632 let s = self.build_string();
1633 eprintln!("parsing\n --\n{}\n --", &s);
1634 let mut sources = tor_config::ConfigurationSources::new_empty();
1635 sources.push_source(
1636 tor_config::ConfigurationSource::from_verbatim(s.clone()),
1637 tor_config::sources::MustRead::MustRead,
1638 );
1639 sources.load().expect(&s)
1640 }
1641
1642 fn resolve<R: tor_config::load::Resolvable>(&self) -> Result<R, ConfigResolveError> {
1643 tor_config::load::resolve(self.parse())
1644 }
1645
1646 fn resolve_return_results<R: tor_config::load::Resolvable>(
1647 &self,
1648 ) -> Result<ResolutionResults<R>, ConfigResolveError> {
1649 tor_config::load::resolve_return_results(self.parse())
1650 }
1651 }
1652
1653 #[test]
1656 fn builder() {
1657 use tor_config_path::CfgPath;
1658 let sec = std::time::Duration::from_secs(1);
1659
1660 let mut authorities = dir::AuthorityContacts::builder();
1661 authorities.v3idents().push([22; 20].into());
1662
1663 let mut fallback = dir::FallbackDir::builder();
1664 fallback
1665 .rsa_identity([23; 20].into())
1666 .ed_identity([99; 32].into())
1667 .orports()
1668 .push("127.0.0.7:7".parse().unwrap());
1669
1670 let mut bld = ArtiConfig::builder();
1671 let mut bld_tor = TorClientConfig::builder();
1672
1673 bld.proxy().socks_listen(Listen::new_localhost(9999));
1674 bld.logging().console("warn");
1675
1676 *bld_tor.tor_network().authorities() = authorities;
1677 bld_tor.tor_network().set_fallback_caches(vec![fallback]);
1678 bld_tor
1679 .storage()
1680 .cache_dir(CfgPath::new("/var/tmp/foo".to_owned()))
1681 .state_dir(CfgPath::new("/var/tmp/bar".to_owned()));
1682 bld_tor.download_schedule().retry_certs().attempts(10);
1683 bld_tor.download_schedule().retry_certs().initial_delay(sec);
1684 bld_tor.download_schedule().retry_certs().parallelism(3);
1685 bld_tor.download_schedule().retry_microdescs().attempts(30);
1686 bld_tor
1687 .download_schedule()
1688 .retry_microdescs()
1689 .initial_delay(10 * sec);
1690 bld_tor
1691 .download_schedule()
1692 .retry_microdescs()
1693 .parallelism(9);
1694 bld_tor
1695 .override_net_params()
1696 .insert("wombats-per-quokka".to_owned(), 7);
1697 bld_tor
1698 .path_rules()
1699 .ipv4_subnet_family_prefix(20)
1700 .ipv6_subnet_family_prefix(48);
1701 bld_tor.preemptive_circuits().disable_at_threshold(12);
1702 bld_tor
1703 .preemptive_circuits()
1704 .set_initial_predicted_ports(vec![80, 443]);
1705 bld_tor
1706 .preemptive_circuits()
1707 .prediction_lifetime(Duration::from_secs(3600))
1708 .min_exit_circs_for_port(2);
1709 bld_tor
1710 .circuit_timing()
1711 .max_dirtiness(90 * sec)
1712 .request_timeout(10 * sec)
1713 .request_max_retries(22)
1714 .request_loyalty(3600 * sec);
1715 bld_tor.address_filter().allow_local_addrs(true);
1716
1717 let val = bld.build().unwrap();
1718
1719 assert_ne!(val, ArtiConfig::default());
1720 }
1721
1722 #[test]
1723 fn articonfig_application() {
1724 let config = ArtiConfig::default();
1725
1726 let application = config.application();
1727 assert_eq!(&config.application, application);
1728 }
1729
1730 #[test]
1731 fn articonfig_logging() {
1732 let config = ArtiConfig::default();
1733
1734 let logging = config.logging();
1735 assert_eq!(&config.logging, logging);
1736 }
1737
1738 #[test]
1739 fn articonfig_proxy() {
1740 let config = ArtiConfig::default();
1741
1742 let proxy = config.proxy();
1743 assert_eq!(&config.proxy, proxy);
1744 }
1745
1746 fn ports_listen(
1750 f: &str,
1751 get_listen: &dyn Fn(&ArtiConfig) -> &Listen,
1752 bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option<Listen>,
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 chk = |cfg: &ArtiConfigBuilder, expected: &Listen| {
1762 dbg!(bld_get_listen(cfg));
1763 let cfg = cfg.build().unwrap();
1764 assert_eq!(get_listen(&cfg), expected);
1765 };
1766
1767 let check_setters = |port, expected: &_| {
1768 let cfg = ArtiConfig::builder();
1769 for listen in match port {
1770 None => vec![Listen::new_none(), Listen::new_localhost(0)],
1771 Some(port) => vec![Listen::new_localhost(port)],
1772 } {
1773 let mut cfg = cfg.clone();
1774 setter_listen(&mut cfg, dbg!(listen));
1775 chk(&cfg, expected);
1776 }
1777 };
1778
1779 {
1780 let expected = Listen::new_localhost(100);
1781
1782 let cfg = from_toml(&format!("proxy.{}_listen = 100", f));
1783 assert_eq!(bld_get_listen(&cfg), &Some(Listen::new_localhost(100)));
1784 chk(&cfg, &expected);
1785
1786 check_setters(Some(100), &expected);
1787 }
1788
1789 {
1790 let expected = Listen::new_none();
1791
1792 let cfg = from_toml(&format!("proxy.{}_listen = 0", f));
1793 chk(&cfg, &expected);
1794
1795 check_setters(None, &expected);
1796 }
1797 }
1798
1799 #[test]
1800 fn ports_listen_socks() {
1801 ports_listen(
1802 "socks",
1803 &|cfg| &cfg.proxy.socks_listen,
1804 &|bld| &bld.proxy.socks_listen,
1805 &|bld, arg| bld.proxy.socks_listen(arg),
1806 );
1807 }
1808
1809 #[test]
1810 fn ports_listen_dns() {
1811 ports_listen(
1812 "dns",
1813 &|cfg| &cfg.proxy.dns_listen,
1814 &|bld| &bld.proxy.dns_listen,
1815 &|bld, arg| bld.proxy.dns_listen(arg),
1816 );
1817 }
1818}