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