1use derive_deftly::Deftly;
6use tor_basic_utils::ByteQty;
7use tor_config_path::CfgPath;
8
9#[cfg(feature = "onion-service-service")]
10use crate::onion_proxy::{
11 OnionServiceProxyConfigBuilder, OnionServiceProxyConfigMap, OnionServiceProxyConfigMapBuilder,
12};
13#[cfg(feature = "rpc")]
14semipublic_use! {
15 use crate::rpc::{
16 RpcConfig, RpcConfigBuilder,
17 listener::{RpcListenerSetConfig, RpcListenerSetConfigBuilder},
18 };
19}
20use arti_client::TorClientConfig;
21#[cfg(feature = "onion-service-service")]
22use tor_config::define_list_builder_accessors;
23use tor_config::derive::prelude::*;
24pub(crate) use tor_config::{ConfigBuildError, Listen, MetricsConfig, MetricsConfigBuilder};
25
26use crate::{LoggingConfig, LoggingConfigBuilder};
27
28#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
33pub(crate) const ARTI_EXAMPLE_CONFIG: &str = concat!(include_str!("./arti-example-config.toml"));
34
35#[cfg(test)]
52const OLDEST_SUPPORTED_CONFIG: &str = concat!(include_str!("./oldest-supported-config.toml"),);
53
54const DEFAULT_SEND_BUF_SIZE: usize = 128_000;
68const DEFAULT_RECV_BUF_SIZE: usize = 128_000;
70
71#[cfg(not(feature = "rpc"))]
73type RpcConfig = ();
74
75#[cfg(not(feature = "onion-service-service"))]
77type OnionServiceProxyConfigMap = ();
78
79#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
81#[derive_deftly(TorConfig)]
82#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
83#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
84pub(crate) struct ApplicationConfig {
85 #[deftly(tor_config(default))]
93 pub(crate) watch_configuration: bool,
94
95 #[deftly(tor_config(default))]
105 pub(crate) permit_debugging: bool,
106
107 #[deftly(tor_config(default))]
111 pub(crate) allow_running_as_root: bool,
112
113 #[deftly(tor_config(default))]
120 pub(crate) defer_bootstrap: bool,
121}
122
123#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
125#[derive_deftly(TorConfig)]
126#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
127#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
128pub(crate) struct ProxyConfig {
129 #[deftly(tor_config(default = "Listen::new_localhost(9150)"))]
133 pub(crate) socks_listen: Listen,
134
135 #[deftly(tor_config(default = "Listen::new_none()"))]
137 pub(crate) dns_listen: Listen,
138
139 #[deftly(tor_config(
145 cfg = r#" feature="http-connect" "#,
146 cfg_desc = "with HTTP CONNECT support"
147 ))]
148 #[deftly(tor_config(default = "true"))]
149 pub(crate) enable_http_connect: bool,
150
151 #[deftly(tor_config(default = "ByteQty(DEFAULT_SEND_BUF_SIZE)"))]
153 pub(crate) socket_send_buf_size: ByteQty,
154
155 #[deftly(tor_config(default = "ByteQty(DEFAULT_RECV_BUF_SIZE)"))]
157 pub(crate) socket_recv_buf_size: ByteQty,
158}
159
160impl ProxyConfig {
161 pub(crate) fn protocols(&self) -> crate::proxy::ListenProtocols {
163 use crate::proxy::ListenProtocols::*;
164 #[cfg(feature = "http-connect")]
165 if self.enable_http_connect {
166 return SocksAndHttpConnect;
167 }
168
169 SocksOnly
170 }
171}
172
173#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
177#[derive_deftly(TorConfig)]
178#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
179#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
180pub(crate) struct ArtiStorageConfig {
181 #[deftly(tor_config(setter(into), default = "default_port_info_file()"))]
183 pub(crate) port_info_file: CfgPath,
184}
185
186fn default_port_info_file() -> CfgPath {
188 CfgPath::new("${ARTI_LOCAL_DATA}/public/port_info.json".to_owned())
189}
190
191#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
205#[derive_deftly(TorConfig)]
206#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
207#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
208#[non_exhaustive]
209pub(crate) struct SystemConfig {
210 #[deftly(tor_config(setter(into), default = "default_max_files()"))]
212 pub(crate) max_files: u64,
213}
214
215fn default_max_files() -> u64 {
217 16384
218}
219
220#[derive(Debug, Deftly, Clone, Eq, PartialEq)]
235#[derive_deftly(TorConfig)]
236#[deftly(tor_config(post_build = "Self::post_build"))]
237#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
238#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
239pub(crate) struct ArtiConfig {
240 #[deftly(tor_config(sub_builder))]
242 application: ApplicationConfig,
243
244 #[deftly(tor_config(sub_builder))]
246 proxy: ProxyConfig,
247
248 #[deftly(tor_config(sub_builder))]
250 logging: LoggingConfig,
251
252 #[deftly(tor_config(sub_builder))]
254 pub(crate) metrics: MetricsConfig,
255
256 #[deftly(tor_config(
258 sub_builder,
259 cfg = r#" feature = "rpc" "#,
260 cfg_desc = "with RPC support"
261 ))]
262 pub(crate) rpc: RpcConfig,
263
264 #[deftly(tor_config(sub_builder))]
270 pub(crate) system: SystemConfig,
271
272 #[deftly(tor_config(sub_builder))]
277 pub(crate) storage: ArtiStorageConfig,
278
279 #[deftly(tor_config(
288 setter(skip),
289 sub_builder,
290 cfg = r#" feature = "onion-service-service" "#,
291 cfg_reject,
292 cfg_desc = "with onion service support"
293 ))]
294 pub(crate) onion_services: OnionServiceProxyConfigMap,
295}
296
297impl ArtiConfigBuilder {
298 #[allow(clippy::unnecessary_wraps)]
300 fn post_build(config: ArtiConfig) -> Result<ArtiConfig, ConfigBuildError> {
301 #[cfg_attr(not(feature = "onion-service-service"), allow(unused_mut))]
302 let mut config = config;
303 #[cfg(feature = "onion-service-service")]
304 for svc in config.onion_services.values_mut() {
305 *svc.svc_cfg
307 .restricted_discovery_mut()
308 .watch_configuration_mut() = config.application.watch_configuration;
309 }
310
311 Ok(config)
312 }
313}
314
315impl tor_config::load::TopLevel for ArtiConfig {
316 type Builder = ArtiConfigBuilder;
317 const DEPRECATED_KEYS: &'static [&'static str] = &["proxy.socks_port", "proxy.dns_port"];
322}
323
324#[cfg(feature = "onion-service-service")]
325define_list_builder_accessors! {
326 struct ArtiConfigBuilder {
327 pub(crate) onion_services: [OnionServiceProxyConfigBuilder],
328 }
329}
330
331#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
335pub(crate) type ArtiCombinedConfig = (ArtiConfig, TorClientConfig);
336
337impl ArtiConfig {
338 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
340 pub(crate) fn application(&self) -> &ApplicationConfig {
341 &self.application
342 }
343
344 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
346 pub(crate) fn logging(&self) -> &LoggingConfig {
347 &self.logging
348 }
349
350 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
352 pub(crate) fn proxy(&self) -> &ProxyConfig {
353 &self.proxy
354 }
355
356 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
358 pub(crate) fn storage(&self) -> &ArtiStorageConfig {
360 &self.storage
361 }
362
363 #[cfg(feature = "rpc")]
365 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
366 pub(crate) fn rpc(&self) -> &RpcConfig {
367 &self.rpc
368 }
369}
370
371#[cfg(test)]
372mod test {
373 #![allow(clippy::bool_assert_comparison)]
375 #![allow(clippy::clone_on_copy)]
376 #![allow(clippy::dbg_macro)]
377 #![allow(clippy::mixed_attributes_style)]
378 #![allow(clippy::print_stderr)]
379 #![allow(clippy::print_stdout)]
380 #![allow(clippy::single_char_pattern)]
381 #![allow(clippy::unwrap_used)]
382 #![allow(clippy::unchecked_time_subtraction)]
383 #![allow(clippy::useless_vec)]
384 #![allow(clippy::needless_pass_by_value)]
385 #![allow(clippy::string_slice)] #![allow(clippy::iter_overeager_cloned)]
389 #![cfg_attr(not(feature = "pt-client"), allow(dead_code))]
391
392 use arti_client::config::TorClientConfigBuilder;
393 use arti_client::config::dir;
394 use itertools::{EitherOrBoth, Itertools, chain};
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.syslog",
566 "logging.time_granularity",
567 "path_rules.long_lived_ports",
568 "circuit_timing.disused_circuit_timeout",
569 "storage.port_info_file",
570 "proxy.socket_send_buf_size",
571 "proxy.socket_recv_buf_size",
572 "application.defer_bootstrap",
573 ],
574 );
575
576 declare_exceptions(
577 None,
578 None,
579 Recognized,
580 &[
581 "tor_network.authorities",
583 "tor_network.fallback_caches",
584 ],
585 );
586
587 declare_exceptions(
588 None,
589 None,
590 Recognized,
591 &[
592 "logging.opentelemetry",
594 ],
595 );
596
597 declare_exceptions(
598 Some(InOld),
599 Some(InNew),
600 if cfg!(target_family = "windows") {
601 Ignored
602 } else {
603 Recognized
604 },
605 &[
606 "storage.permissions.trust_group",
608 "storage.permissions.trust_user",
609 ],
610 );
611
612 declare_exceptions(
613 None,
614 None, FeatureDependent,
616 &[
617 "bridges.transports", ],
620 );
621
622 declare_exceptions(
623 None,
624 Some(InNew),
625 FeatureDependent,
626 &[
627 "storage.keystore",
629 ],
630 );
631
632 declare_exceptions(
633 None,
634 None, FeatureDependent,
636 &[
637 "logging.tokio_console",
639 "logging.tokio_console.enabled",
640 ],
641 );
642
643 declare_exceptions(
644 None,
645 None, Recognized,
647 &[
648 "system.memory",
650 "system.memory.max",
651 "system.memory.low_water",
652 ],
653 );
654
655 declare_exceptions(
656 None,
657 Some(InNew), Recognized,
659 &["metrics"],
660 );
661
662 declare_exceptions(
663 None,
664 None, Recognized,
666 &[
667 "metrics.prometheus",
669 "metrics.prometheus.listen",
670 ],
671 );
672
673 declare_exceptions(
674 None,
675 Some(InNew),
676 FeatureDependent,
677 &[
678 ],
680 );
681
682 declare_exceptions(
683 None,
684 Some(InNew),
685 FeatureDependent,
686 &[
687 "address_filter.allow_onion_addrs",
689 "circuit_timing.hs_desc_fetch_attempts",
690 "circuit_timing.hs_intro_rend_attempts",
691 "circuit_timing.hs_dir_requery_interval",
692 ],
693 );
694
695 declare_exceptions(
696 None,
697 Some(InNew),
698 FeatureDependent,
699 &[
700 "proxy.enable_http_connect",
702 ],
703 );
704
705 declare_exceptions(
706 None,
707 None, FeatureDependent,
709 &[
710 "rpc",
712 "rpc.rpc_listen",
713 ],
714 );
715
716 declare_exceptions(
718 None,
719 None,
720 FeatureDependent,
721 &[
722 "onion_services",
724 ],
725 );
726
727 declare_exceptions(
728 None,
729 Some(InNew),
730 FeatureDependent,
731 &[
732 "vanguards",
734 "vanguards.mode",
735 ],
736 );
737
738 declare_exceptions(
740 None,
741 None,
742 FeatureDependent,
743 &[
744 "storage.keystore.ctor",
745 "storage.keystore.ctor.services",
746 "storage.keystore.ctor.clients",
747 ],
748 );
749
750 out.sort();
751
752 let dupes = out.iter().map(|exc| &exc.key).duplicates().collect_vec();
753 assert!(
754 dupes.is_empty(),
755 "duplicate exceptions in configuration {dupes:?}"
756 );
757
758 eprintln!(
759 "declared config exceptions for this configuration:\n{:#?}",
760 &out
761 );
762 out
763 }
764
765 #[test]
766 fn default_config() {
767 use InExample::*;
768
769 let empty_config = tor_config::ConfigurationSources::new_empty()
770 .load()
771 .unwrap();
772 let empty_config: ArtiCombinedConfig = tor_config::resolve(empty_config).unwrap();
773
774 let default = (ArtiConfig::default(), TorClientConfig::default());
775 let exceptions = declared_config_exceptions();
776
777 #[allow(clippy::needless_pass_by_value)] fn analyse_joined_info(
788 which: WhichExample,
789 uncommented: bool,
790 eob: EitherOrBoth<&String, &ConfigException>,
791 ) -> Result<(), (String, String)> {
792 use EitherOrBoth::*;
793 let (key, err) = match eob {
794 Left(found) => (found, "found in example but not processed".into()),
796 Both(found, exc) => {
797 let but = match (exc.in_example(which), exc.in_code, uncommented) {
798 (Absent, _, _) => "but exception entry expected key to be absent",
799 (_, _, false) => "when processing still-commented-out file!",
800 (_, Some(true), _) => {
801 "but an exception entry says it should have been recognised"
802 }
803 (Present, Some(false), true) => return Ok(()), (Present, None, true) => return Ok(()), };
806 (
807 found,
808 format!("parser reported unrecognised config key, {but}"),
809 )
810 }
811 Right(exc) => {
812 let trouble = match (exc.in_example(which), exc.in_code, uncommented) {
817 (Absent, _, _) => return Ok(()), (_, _, false) => return Ok(()), (_, Some(true), _) => return Ok(()), (Present, Some(false), true) => {
821 "expected an 'unknown config key' report but didn't see one"
822 }
823 (Present, None, true) => return Ok(()), };
825 (&exc.key, trouble.into())
826 }
827 };
828 Err((key.clone(), err))
829 }
830
831 let parses_to_defaults = |example: &str, which: WhichExample, uncommented: bool| {
832 let cfg = {
833 let mut sources = tor_config::ConfigurationSources::new_empty();
834 sources.push_source(
835 tor_config::ConfigurationSource::from_verbatim(example.to_string()),
836 tor_config::sources::MustRead::MustRead,
837 );
838 sources.load().unwrap()
839 };
840
841 let results: ResolutionResults<ArtiCombinedConfig> =
843 tor_config::resolve_return_results(cfg, &Default::default()).unwrap();
844
845 assert_eq!(&results.value, &default, "{which:?} {uncommented:?}");
846 assert_eq!(&results.value, &empty_config, "{which:?} {uncommented:?}");
847
848 let unrecognized = results
851 .unrecognized
852 .iter()
853 .map(|k| k.to_string())
854 .collect_vec();
855
856 eprintln!(
857 "parsing of {which:?} uncommented={uncommented:?}, unrecognized={unrecognized:#?}"
858 );
859
860 let reports =
861 Itertools::merge_join_by(unrecognized.iter(), exceptions.iter(), |u, e| {
862 u.as_str().cmp(&e.key)
863 })
864 .filter_map(|eob| analyse_joined_info(which, uncommented, eob).err())
865 .collect_vec();
866
867 if !reports.is_empty() {
868 let reports = reports.iter().fold(String::new(), |mut out, (k, s)| {
869 writeln!(out, " {}: {}", s, k).unwrap();
870 out
871 });
872
873 panic!(
874 r"
875mismatch: results of parsing example files (& vs declared exceptions):
876example config file {which:?}, uncommented={uncommented:?}
877{reports}
878"
879 );
880 }
881
882 results.value
883 };
884
885 let _ = parses_to_defaults(ARTI_EXAMPLE_CONFIG, WhichExample::New, false);
886 let _ = parses_to_defaults(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, false);
887
888 let built_default = (
889 ArtiConfigBuilder::default().build().unwrap(),
890 TorClientConfigBuilder::default().build().unwrap(),
891 );
892
893 let parsed = parses_to_defaults(
894 &uncomment_example_settings(ARTI_EXAMPLE_CONFIG),
895 WhichExample::New,
896 true,
897 );
898 let parsed_old = parses_to_defaults(
899 &uncomment_example_settings(OLDEST_SUPPORTED_CONFIG),
900 WhichExample::Old,
901 true,
902 );
903
904 assert_eq!(&parsed, &built_default);
905 assert_eq!(&parsed_old, &built_default);
906
907 assert_eq!(&default, &built_default);
908 }
909
910 fn exhaustive_1(example_file: &str, which: WhichExample, deprecated: &[String]) {
942 use InExample::*;
943 use serde_json::Value as JsValue;
944 use std::collections::BTreeSet;
945
946 let example = uncomment_example_settings(example_file);
947 let example: toml::Value = toml::from_str(&example).unwrap();
948 let example = serde_json::to_value(example).unwrap();
950 let exhausts = [
960 serde_json::to_value(TorClientConfig::builder()).unwrap(),
961 serde_json::to_value(ArtiConfig::builder()).unwrap(),
962 ];
963
964 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, derive_more::Display)]
967 enum ProblemKind {
968 #[display("recognised by serialisation, but missing from example config file")]
969 MissingFromExample,
970 #[display("expected that example config file should contain have this as a table")]
971 ExpectedTableInExample,
972 #[display(
973 "declared exception says this key should be recognised but not in file, but that doesn't seem to be the case"
974 )]
975 UnusedException,
976 }
977
978 #[derive(Default, Debug)]
979 struct Walk {
980 current_path: Vec<String>,
981 problems: Vec<(String, ProblemKind)>,
982 }
983
984 impl Walk {
985 fn bad(&mut self, kind: ProblemKind) {
987 self.problems.push((self.current_path.join("."), kind));
988 }
989
990 fn walk<const E: usize>(
997 &mut self,
998 example: Option<&JsValue>,
999 exhausts: [Option<&JsValue>; E],
1000 ) {
1001 assert! { exhausts.into_iter().any(|e| e.is_some()) }
1002
1003 let example = if let Some(e) = example {
1004 e
1005 } else {
1006 self.bad(ProblemKind::MissingFromExample);
1007 return;
1008 };
1009
1010 let tables = exhausts.map(|e| e?.as_object());
1011
1012 let table_keys = tables
1014 .iter()
1015 .flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten())
1016 .collect::<BTreeSet<String>>();
1017
1018 for key in table_keys {
1019 let example = if let Some(e) = example.as_object() {
1020 e
1021 } else {
1022 self.bad(ProblemKind::ExpectedTableInExample);
1025 continue;
1026 };
1027
1028 self.current_path.push(key.clone());
1030 self.walk(example.get(&key), tables.map(|t| t?.get(&key)));
1031 self.current_path.pop().unwrap();
1032 }
1033 }
1034 }
1035
1036 let exhausts = exhausts.iter().map(Some).collect_vec().try_into().unwrap();
1037
1038 let mut walk = Walk::default();
1039 walk.walk::<2>(Some(&example), exhausts);
1040 let mut problems = walk.problems;
1041
1042 #[derive(Debug, Copy, Clone)]
1044 struct DefinitelyRecognized;
1045
1046 let expect_missing = declared_config_exceptions()
1047 .iter()
1048 .filter_map(|exc| {
1049 let definitely = match (exc.in_example(which), exc.in_code) {
1050 (Present, _) => return None, (_, Some(false)) => return None, (Absent, Some(true)) => Some(DefinitelyRecognized),
1053 (Absent, None) => None, };
1055 Some((exc.key.clone(), definitely))
1056 })
1057 .collect_vec();
1058 dbg!(&expect_missing);
1059
1060 let expect_missing: Vec<(String, Option<DefinitelyRecognized>)> = expect_missing
1069 .iter()
1070 .cloned()
1071 .filter({
1072 let original: HashSet<_> = expect_missing.iter().map(|(k, _)| k.clone()).collect();
1073 move |(found, _)| {
1074 !found
1075 .match_indices('.')
1076 .any(|(doti, _)| original.contains(&found[0..doti]))
1077 }
1078 })
1079 .collect_vec();
1080 dbg!(&expect_missing);
1081
1082 for (exp, definitely) in expect_missing {
1083 let was = problems.len();
1084 problems.retain(|(path, _)| path != &exp);
1085 if problems.len() == was && definitely.is_some() {
1086 problems.push((exp, ProblemKind::UnusedException));
1087 }
1088 }
1089
1090 let problems = problems
1091 .into_iter()
1092 .filter(|(key, _kind)| !deprecated.iter().any(|dep| key == dep))
1093 .map(|(path, m)| format!(" config key {:?}: {}", path, m))
1094 .collect_vec();
1095
1096 assert!(
1099 problems.is_empty(),
1100 "example config {which:?} exhaustiveness check failed: {}\n-----8<-----\n{}\n-----8<-----\n",
1101 problems.join("\n"),
1102 example_file,
1103 );
1104 }
1105
1106 #[test]
1107 fn exhaustive() {
1108 let mut deprecated = vec![];
1109 <(ArtiConfig, TorClientConfig) as tor_config::load::Resolvable>::enumerate_deprecated_keys(
1110 &mut |l| {
1111 for k in l {
1112 deprecated.push(k.to_string());
1113 }
1114 },
1115 );
1116 let deprecated = deprecated.iter().cloned().collect_vec();
1117
1118 exhaustive_1(ARTI_EXAMPLE_CONFIG, WhichExample::New, &deprecated);
1123
1124 exhaustive_1(OLDEST_SUPPORTED_CONFIG, WhichExample::Old, &deprecated);
1131 }
1132
1133 #[cfg_attr(feature = "pt-client", allow(dead_code))]
1135 fn expect_err_contains(err: ConfigResolveError, exp: &str) {
1136 use std::error::Error as StdError;
1137 let err: Box<dyn StdError> = Box::new(err);
1138 let err = tor_error::Report(err).to_string();
1139 assert!(
1140 err.contains(exp),
1141 "wrong message, got {:?}, exp {:?}",
1142 err,
1143 exp,
1144 );
1145 }
1146
1147 #[test]
1148 fn bridges() {
1149 let filter_examples = |#[allow(unused_mut)] mut examples: ExampleSectionLines| -> _ {
1163 if cfg!(all(feature = "bridge-client", not(feature = "pt-client"))) {
1165 let looks_like_addr =
1166 |l: &str| l.starts_with(|c: char| c.is_ascii_digit() || c == '[');
1167 examples.lines.retain(|l| looks_like_addr(l));
1168 }
1169
1170 examples
1171 };
1172
1173 let resolve_examples = |examples: &ExampleSectionLines| {
1178 #[cfg(all(feature = "bridge-client", not(feature = "pt-client")))]
1180 {
1181 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1182 expect_err_contains(err, "support disabled in cargo features");
1183 }
1184
1185 let examples = filter_examples(examples.clone());
1186
1187 #[cfg(feature = "bridge-client")]
1188 {
1189 examples.resolve::<TorClientConfig>().unwrap()
1190 }
1191
1192 #[cfg(not(feature = "bridge-client"))]
1193 {
1194 let err = examples.resolve::<TorClientConfig>().unwrap_err();
1195 expect_err_contains(err, "support disabled in cargo features");
1196 ((),)
1198 }
1199 };
1200
1201 let mut examples = ExampleSectionLines::from_section("bridges");
1203 examples.narrow((r#"^# For example:"#, true), NARROW_NONE);
1204
1205 let compare = {
1206 let mut examples = examples.clone();
1208 examples.narrow((r#"^# bridges = '''"#, true), (r#"^# '''"#, true));
1209 examples.uncomment();
1210
1211 let parsed = resolve_examples(&examples);
1212
1213 examples.lines.remove(0);
1216 examples.lines.remove(examples.lines.len() - 1);
1217 examples.expect_lines(3);
1219
1220 #[cfg(feature = "bridge-client")]
1222 {
1223 let examples = filter_examples(examples);
1224 let mut built = TorClientConfig::builder();
1225 for l in &examples.lines {
1226 built.bridges().bridges().push(l.trim().parse().expect(l));
1227 }
1228 let built = built.build().unwrap();
1229
1230 assert_eq!(&parsed, &built);
1231 }
1232
1233 parsed
1234 };
1235
1236 {
1238 examples.narrow((r#"^# bridges = \["#, true), (r#"^# \]"#, true));
1239 examples.uncomment();
1240 let parsed = resolve_examples(&examples);
1241 assert_eq!(&parsed, &compare);
1242 }
1243 }
1244
1245 #[test]
1246 fn transports() {
1247 let mut file =
1253 ExampleSectionLines::from_markers("# An example managed pluggable transport", "[");
1254 file.lines.retain(|line| line.starts_with("# "));
1255 file.uncomment();
1256
1257 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1258 let cfg_got = result.unwrap();
1259
1260 #[cfg(feature = "pt-client")]
1261 {
1262 use arti_client::config::{BridgesConfig, pt::TransportConfig};
1263 use tor_config_path::CfgPath;
1264
1265 let bridges_got: &BridgesConfig = cfg_got.0.as_ref();
1266
1267 let mut bld = BridgesConfig::builder();
1269 {
1270 let mut b = TransportConfig::builder();
1271 b.protocols(vec!["obfs4".parse().unwrap(), "obfs5".parse().unwrap()]);
1272 b.path(CfgPath::new("/usr/bin/obfsproxy".to_string()));
1273 b.arguments(vec!["-obfs4".to_string(), "-obfs5".to_string()]);
1274 b.run_on_startup(true);
1275 bld.transports().push(b);
1276 }
1277 {
1278 let mut b = TransportConfig::builder();
1279 b.protocols(vec!["obfs4".parse().unwrap()]);
1280 b.proxy_addr("127.0.0.1:31337".parse().unwrap());
1281 bld.transports().push(b);
1282 }
1283
1284 let bridges_expected = bld.build().unwrap();
1285 assert_eq!(&bridges_expected, bridges_got);
1286 }
1287 }
1288
1289 #[test]
1290 fn memquota() {
1291 let mut file = ExampleSectionLines::from_section("system");
1294 file.lines.retain(|line| line.starts_with("# memory."));
1295 file.uncomment();
1296
1297 let result = file.resolve_return_results::<(TorClientConfig, ArtiConfig)>();
1298
1299 let result = result.unwrap();
1300
1301 assert_eq!(result.unrecognized, []);
1303 assert_eq!(result.deprecated, []);
1304
1305 let inner: &tor_memquota::testing::ConfigInner =
1306 result.value.0.system_memory().inner().unwrap();
1307
1308 let defaulted_low = tor_memquota::Config::builder()
1311 .max(*inner.max)
1312 .build()
1313 .unwrap();
1314 let inner_defaulted_low = defaulted_low.inner().unwrap();
1315 assert_eq!(inner, inner_defaulted_low);
1316 }
1317
1318 #[test]
1319 fn metrics() {
1320 let mut file = ExampleSectionLines::from_section("metrics");
1322 file.lines
1323 .retain(|line| line.starts_with("# prometheus."));
1324 file.uncomment();
1325
1326 let result = file
1327 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1328 .unwrap();
1329
1330 assert_eq!(result.unrecognized, []);
1332 assert_eq!(result.deprecated, []);
1333
1334 assert_eq!(
1336 result
1337 .value
1338 .1
1339 .metrics
1340 .prometheus
1341 .listen
1342 .single_address_legacy()
1343 .unwrap(),
1344 Some("127.0.0.1:9035".parse().unwrap()),
1345 );
1346
1347 }
1350
1351 #[test]
1352 fn onion_services() {
1353 let mut file = ExampleSectionLines::from_markers("##### ONION SERVICES", "##### RPC");
1357 file.lines.retain(|line| line.starts_with("# "));
1358 file.uncomment();
1359
1360 let result = file.resolve::<(TorClientConfig, ArtiConfig)>();
1361 #[cfg(feature = "onion-service-service")]
1362 {
1363 let svc_expected = {
1364 use tor_hsrproxy::config::*;
1365 let mut b = OnionServiceProxyConfigBuilder::default();
1366 b.service().nickname("allium-cepa".parse().unwrap());
1367 b.proxy().proxy_ports().push(ProxyRule::new(
1368 ProxyPattern::one_port(80).unwrap(),
1369 ProxyAction::Forward(
1370 Encapsulation::Simple,
1371 TargetAddr::Inet("127.0.0.1:10080".parse().unwrap()),
1372 ),
1373 ));
1374 b.proxy().proxy_ports().push(ProxyRule::new(
1375 ProxyPattern::one_port(22).unwrap(),
1376 ProxyAction::DestroyCircuit,
1377 ));
1378 b.proxy().proxy_ports().push(ProxyRule::new(
1379 ProxyPattern::one_port(265).unwrap(),
1380 ProxyAction::IgnoreStream,
1381 ));
1382 b.proxy().proxy_ports().push(ProxyRule::new(
1392 ProxyPattern::one_port(443).unwrap(),
1393 ProxyAction::RejectStream,
1394 ));
1395 b.proxy().proxy_ports().push(ProxyRule::new(
1396 ProxyPattern::all_ports(),
1397 ProxyAction::DestroyCircuit,
1398 ));
1399
1400 #[cfg(feature = "restricted-discovery")]
1401 {
1402 const ALICE_KEY: &str =
1403 "descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA";
1404 const BOB_KEY: &str =
1405 "descriptor:x25519:b5zqgtpermmuda6vc63lhjuf5ihpokjmuk26ly2xksf7vg52aesq";
1406 for (nickname, key) in [("alice", ALICE_KEY), ("bob", BOB_KEY)] {
1407 b.service()
1408 .restricted_discovery()
1409 .enabled(true)
1410 .static_keys()
1411 .access()
1412 .push((
1413 HsClientNickname::from_str(nickname).unwrap(),
1414 HsClientDescEncKey::from_str(key).unwrap(),
1415 ));
1416 }
1417 let mut dir = DirectoryKeyProviderBuilder::default();
1418 dir.path(CfgPath::new(
1419 "/var/lib/tor/hidden_service/authorized_clients".to_string(),
1420 ));
1421
1422 b.service()
1423 .restricted_discovery()
1424 .key_dirs()
1425 .access()
1426 .push(dir);
1427 }
1428
1429 b.build().unwrap()
1430 };
1431
1432 cfg_if::cfg_if! {
1433 if #[cfg(feature = "restricted-discovery")] {
1434 let cfg = result.unwrap();
1435 let services = cfg.1.onion_services;
1436 assert_eq!(services.len(), 1);
1437 let svc = services.values().next().unwrap();
1438 assert_eq!(svc, &svc_expected);
1439 } else {
1440 expect_err_contains(
1441 result.unwrap_err(),
1442 "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
1443 );
1444 }
1445 }
1446 }
1447 #[cfg(not(feature = "onion-service-service"))]
1448 {
1449 expect_err_contains(result.unwrap_err(), "not built with onion service support");
1450 }
1451 }
1452
1453 #[cfg(feature = "rpc")]
1454 #[test]
1455 fn rpc_defaults() {
1456 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1457 file.lines
1461 .retain(|line| line.starts_with("# ") && !line.starts_with("# "));
1462 file.uncomment();
1463
1464 let parsed = file
1465 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1466 .unwrap();
1467 assert!(parsed.unrecognized.is_empty());
1468 assert!(parsed.deprecated.is_empty());
1469 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1470 let rpc_default = RpcConfig::default();
1471 assert_eq!(rpc_parsed, &rpc_default);
1472 }
1473
1474 #[cfg(feature = "rpc")]
1475 #[test]
1476 fn rpc_full() {
1477 use crate::rpc::listener::{ConnectPointOptionsBuilder, RpcListenerSetConfigBuilder};
1478
1479 let mut file = ExampleSectionLines::from_markers("##### RPC", "[");
1481 file.lines
1483 .retain(|line| line.starts_with("# ") && !line.contains("file ="));
1484 file.uncomment();
1485
1486 let parsed = file
1487 .resolve_return_results::<(TorClientConfig, ArtiConfig)>()
1488 .unwrap();
1489 let rpc_parsed: &RpcConfig = parsed.value.1.rpc();
1490
1491 let expected = {
1492 let mut bld_opts = ConnectPointOptionsBuilder::default();
1493 bld_opts.enable(false);
1494
1495 let mut bld_set = RpcListenerSetConfigBuilder::default();
1496 bld_set.dir(CfgPath::new("${HOME}/.my_connect_files/".to_string()));
1497 bld_set.listener_options().enable(true);
1498 bld_set
1499 .file_options()
1500 .insert("bad_file.json".to_string(), bld_opts);
1501
1502 let mut bld = RpcConfigBuilder::default();
1503 bld.listen().insert("label".to_string(), bld_set);
1504 bld.build().unwrap()
1505 };
1506
1507 assert_eq!(&expected, rpc_parsed);
1508 }
1509
1510 #[derive(Debug, Clone)]
1518 struct ExampleSectionLines {
1519 section: String,
1522 lines: Vec<String>,
1524 }
1525
1526 type NarrowInstruction<'s> = (&'s str, bool);
1529 const NARROW_NONE: NarrowInstruction<'static> = ("?<none>", false);
1531
1532 impl ExampleSectionLines {
1533 fn from_section(section: &str) -> Self {
1537 Self::from_markers(format!("[{section}]"), "[")
1538 }
1539
1540 fn from_markers<S, E>(start: S, end: E) -> Self
1551 where
1552 S: AsRef<str>,
1553 E: AsRef<str>,
1554 {
1555 let (start, end) = (start.as_ref(), end.as_ref());
1556 let mut lines = ARTI_EXAMPLE_CONFIG
1557 .lines()
1558 .skip_while(|line| !line.starts_with(start))
1559 .peekable();
1560 let section = lines
1561 .next_if(|l0| l0.starts_with('['))
1562 .map(|section| section.to_owned())
1563 .unwrap_or_default();
1564 let lines = lines
1565 .take_while(|line| !line.starts_with(end))
1566 .map(|l| l.to_owned())
1567 .collect_vec();
1568
1569 Self { section, lines }
1570 }
1571
1572 fn narrow(&mut self, start: NarrowInstruction, end: NarrowInstruction) {
1575 let find_index = |(re, include), start_pos, exactly_one: bool, adjust: [isize; 2]| {
1576 if (re, include) == NARROW_NONE {
1577 return None;
1578 }
1579
1580 let re = Regex::new(re).expect(re);
1581 let i = self
1582 .lines
1583 .iter()
1584 .enumerate()
1585 .skip(start_pos)
1586 .filter(|(_, l)| re.is_match(l))
1587 .map(|(i, _)| i);
1588 let i = if exactly_one {
1589 i.clone().exactly_one().unwrap_or_else(|_| {
1590 panic!("RE={:?} I={:#?} L={:#?}", re, i.collect_vec(), &self.lines)
1591 })
1592 } else {
1593 i.clone().next()?
1594 };
1595
1596 let adjust = adjust[usize::from(include)];
1597 let i = (i as isize + adjust) as usize;
1598 Some(i)
1599 };
1600
1601 eprint!("narrow {:?} {:?}: ", start, end);
1602 let start = find_index(start, 0, true, [1, 0]).unwrap_or(0);
1603 let end = find_index(end, start + 1, false, [0, 1]).unwrap_or(self.lines.len());
1604 eprintln!("{:?} {:?}", start, end);
1605 assert!(start < end, "empty, from {:#?}", &self.lines);
1607 self.lines = self.lines.drain(..).take(end).skip(start).collect_vec();
1608 }
1609
1610 fn expect_lines(&self, n: usize) {
1612 assert_eq!(self.lines.len(), n);
1613 }
1614
1615 fn uncomment(&mut self) {
1617 self.strip_prefix("#");
1618 }
1619
1620 fn strip_prefix(&mut self, prefix: &str) {
1627 for l in &mut self.lines {
1628 if !l.starts_with('[') {
1629 *l = l.strip_prefix(prefix).expect(l).to_string();
1630 }
1631 }
1632 }
1633
1634 fn build_string(&self) -> String {
1636 chain!(iter::once(&self.section), self.lines.iter(),).join("\n")
1637 }
1638
1639 fn parse(&self) -> tor_config::ConfigurationTree {
1642 let s = self.build_string();
1643 eprintln!("parsing\n --\n{}\n --", &s);
1644 let mut sources = tor_config::ConfigurationSources::new_empty();
1645 sources.push_source(
1646 tor_config::ConfigurationSource::from_verbatim(s.clone()),
1647 tor_config::sources::MustRead::MustRead,
1648 );
1649 sources.load().expect(&s)
1650 }
1651
1652 fn resolve<R: tor_config::load::Resolvable>(&self) -> Result<R, ConfigResolveError> {
1653 tor_config::load::resolve(self.parse())
1654 }
1655
1656 fn resolve_return_results<R: tor_config::load::Resolvable>(
1657 &self,
1658 ) -> Result<ResolutionResults<R>, ConfigResolveError> {
1659 tor_config::load::resolve_return_results(self.parse(), &Default::default())
1660 }
1661 }
1662
1663 #[test]
1666 fn builder() {
1667 use tor_config_path::CfgPath;
1668 let sec = std::time::Duration::from_secs(1);
1669
1670 let mut authorities = dir::AuthorityContacts::builder();
1671 authorities.v3idents().push([22; 20].into());
1672
1673 let mut fallback = dir::FallbackDir::builder();
1674 fallback
1675 .rsa_identity([23; 20].into())
1676 .ed_identity([99; 32].into())
1677 .orports()
1678 .push("127.0.0.7:7".parse().unwrap());
1679
1680 let mut bld = ArtiConfig::builder();
1681 let mut bld_tor = TorClientConfig::builder();
1682
1683 bld.proxy().socks_listen(Listen::new_localhost(9999));
1684 bld.logging().console("warn");
1685
1686 *bld_tor.tor_network().authorities() = authorities;
1687 bld_tor.tor_network().set_fallback_caches(vec![fallback]);
1688 bld_tor
1689 .storage()
1690 .cache_dir(CfgPath::new("/var/tmp/foo".to_owned()))
1691 .state_dir(CfgPath::new("/var/tmp/bar".to_owned()));
1692 bld_tor.download_schedule().retry_certs().attempts(10);
1693 bld_tor.download_schedule().retry_certs().initial_delay(sec);
1694 bld_tor.download_schedule().retry_certs().parallelism(3);
1695 bld_tor.download_schedule().retry_microdescs().attempts(30);
1696 bld_tor
1697 .download_schedule()
1698 .retry_microdescs()
1699 .initial_delay(10 * sec);
1700 bld_tor
1701 .download_schedule()
1702 .retry_microdescs()
1703 .parallelism(9);
1704 bld_tor
1705 .override_net_params()
1706 .insert("wombats-per-quokka".to_owned(), 7);
1707 bld_tor
1708 .path_rules()
1709 .ipv4_subnet_family_prefix(20)
1710 .ipv6_subnet_family_prefix(48);
1711 bld_tor.preemptive_circuits().disable_at_threshold(12);
1712 bld_tor
1713 .preemptive_circuits()
1714 .set_initial_predicted_ports(vec![80, 443]);
1715 bld_tor
1716 .preemptive_circuits()
1717 .prediction_lifetime(Duration::from_secs(3600))
1718 .min_exit_circs_for_port(2);
1719 bld_tor
1720 .circuit_timing()
1721 .max_dirtiness(90 * sec)
1722 .request_timeout(10 * sec)
1723 .request_max_retries(22)
1724 .request_loyalty(3600 * sec);
1725 bld_tor.address_filter().allow_local_addrs(true);
1726
1727 let val = bld.build().unwrap();
1728
1729 assert_ne!(val, ArtiConfig::default());
1730 }
1731
1732 #[test]
1733 fn articonfig_application() {
1734 let config = ArtiConfig::default();
1735
1736 let application = config.application();
1737 assert_eq!(&config.application, application);
1738 }
1739
1740 #[test]
1741 fn articonfig_logging() {
1742 let config = ArtiConfig::default();
1743
1744 let logging = config.logging();
1745 assert_eq!(&config.logging, logging);
1746 }
1747
1748 #[test]
1749 fn articonfig_proxy() {
1750 let config = ArtiConfig::default();
1751
1752 let proxy = config.proxy();
1753 assert_eq!(&config.proxy, proxy);
1754 }
1755
1756 fn ports_listen(
1760 f: &str,
1761 get_listen: &dyn Fn(&ArtiConfig) -> &Listen,
1762 bld_get_listen: &dyn Fn(&ArtiConfigBuilder) -> &Option<Listen>,
1763 setter_listen: &dyn Fn(&mut ArtiConfigBuilder, Listen) -> &mut ProxyConfigBuilder,
1764 ) {
1765 let from_toml = |s: &str| -> ArtiConfigBuilder {
1766 let cfg: toml::Value = toml::from_str(dbg!(s)).unwrap();
1767 let cfg: ArtiConfigBuilder = cfg.try_into().unwrap();
1768 cfg
1769 };
1770
1771 let chk = |cfg: &ArtiConfigBuilder, expected: &Listen| {
1772 dbg!(bld_get_listen(cfg));
1773 let cfg = cfg.build().unwrap();
1774 assert_eq!(get_listen(&cfg), expected);
1775 };
1776
1777 let check_setters = |port, expected: &_| {
1778 let cfg = ArtiConfig::builder();
1779 for listen in match port {
1780 None => vec![Listen::new_none(), Listen::new_localhost(0)],
1781 Some(port) => vec![Listen::new_localhost(port)],
1782 } {
1783 let mut cfg = cfg.clone();
1784 setter_listen(&mut cfg, dbg!(listen));
1785 chk(&cfg, expected);
1786 }
1787 };
1788
1789 {
1790 let expected = Listen::new_localhost(100);
1791
1792 let cfg = from_toml(&format!("proxy.{}_listen = 100", f));
1793 assert_eq!(bld_get_listen(&cfg), &Some(Listen::new_localhost(100)));
1794 chk(&cfg, &expected);
1795
1796 check_setters(Some(100), &expected);
1797 }
1798
1799 {
1800 let expected = Listen::new_none();
1801
1802 let cfg = from_toml(&format!("proxy.{}_listen = 0", f));
1803 chk(&cfg, &expected);
1804
1805 check_setters(None, &expected);
1806 }
1807 }
1808
1809 #[test]
1810 fn ports_listen_socks() {
1811 ports_listen(
1812 "socks",
1813 &|cfg| &cfg.proxy.socks_listen,
1814 &|bld| &bld.proxy.socks_listen,
1815 &|bld, arg| bld.proxy.socks_listen(arg),
1816 );
1817 }
1818
1819 #[test]
1820 fn ports_listen_dns() {
1821 ports_listen(
1822 "dns",
1823 &|cfg| &cfg.proxy.dns_listen,
1824 &|bld| &bld.proxy.dns_listen,
1825 &|bld, arg| bld.proxy.dns_listen(arg),
1826 );
1827 }
1828}