arti/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2#![doc = include_str!("../README.md")]
3// @@ begin lint list maintained by maint/add_warning @@
4#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6#![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_duration_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39#![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43#![allow(clippy::needless_lifetimes)] // See arti#1765
44#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
46
47// TODO #1645 (either remove this, or decide to have it everywhere)
48#![cfg_attr(not(all(feature = "full", feature = "experimental")), allow(unused))]
49// Overrides specific to this crate:
50#![allow(clippy::print_stderr)]
51#![allow(clippy::print_stdout)]
52
53pub mod cfg;
54pub mod logging;
55#[cfg(not(feature = "onion-service-service"))]
56mod onion_proxy_disabled;
57
58mod subcommands;
59
60/// Helper:
61/// Declare a series of modules as public if experimental_api is set,
62/// and as non-public otherwise.
63//
64// TODO: We'd like to use visibility::make(pub) here, but it doesn't
65// work on modules.
66macro_rules! semipublic_mod {
67    {
68        $(
69            $( #[$meta:meta] )*
70            mod $name:ident ;
71        )*
72    }  => {
73        $(
74            $( #[$meta])*
75            cfg_if::cfg_if! {
76                if #[cfg(feature="experimental-api")] {
77                   pub mod $name;
78                } else {
79                   mod $name;
80                }
81            }
82         )*
83    }
84}
85
86semipublic_mod! {
87    #[cfg(feature = "dns-proxy")]
88    mod dns;
89    mod exit;
90    #[cfg(feature="onion-service-service")]
91    mod onion_proxy;
92    mod process;
93    mod reload_cfg;
94    mod socks;
95}
96#[cfg_attr(not(feature = "rpc"), path = "rpc_stub.rs")]
97mod rpc;
98
99use std::ffi::OsString;
100use std::fmt::Write;
101
102pub use cfg::{
103    ARTI_EXAMPLE_CONFIG, ApplicationConfig, ApplicationConfigBuilder, ArtiCombinedConfig,
104    ArtiConfig, ArtiConfigBuilder, ProxyConfig, ProxyConfigBuilder, SystemConfig,
105    SystemConfigBuilder,
106};
107pub use logging::{LoggingConfig, LoggingConfigBuilder};
108
109use arti_client::TorClient;
110use arti_client::config::default_config_files;
111use safelog::with_safe_logging_suppressed;
112use tor_config::ConfigurationSources;
113use tor_config::mistrust::BuilderExt as _;
114use tor_rtcompat::ToplevelRuntime;
115
116use anyhow::{Context, Error, Result};
117use clap::{Arg, ArgAction, Command, value_parser};
118#[allow(unused_imports)]
119use tracing::{error, info, warn};
120
121#[cfg(any(
122    feature = "hsc",
123    feature = "onion-service-service",
124    feature = "onion-service-cli-extra",
125))]
126use clap::Subcommand as _;
127
128#[cfg(feature = "experimental-api")]
129#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
130pub use subcommands::proxy::run_proxy as run;
131
132/// Create a runtime for Arti to use.
133fn create_runtime() -> std::io::Result<impl ToplevelRuntime> {
134    cfg_if::cfg_if! {
135        if #[cfg(all(feature="tokio", feature="native-tls"))] {
136            use tor_rtcompat::tokio::TokioNativeTlsRuntime as ChosenRuntime;
137        } else if #[cfg(all(feature="tokio", feature="rustls"))] {
138            use tor_rtcompat::tokio::TokioRustlsRuntime as ChosenRuntime;
139            // Note: See comments in tor_rtcompate::impls::rustls::RustlsProvider
140            // about choice of default crypto provider.
141            let _idempotent_ignore = rustls_crate::crypto::CryptoProvider::install_default(
142                rustls_crate::crypto::ring::default_provider(),
143
144            );
145        } else if #[cfg(all(feature="async-std", feature="native-tls"))] {
146            use tor_rtcompat::async_std::AsyncStdNativeTlsRuntime as ChosenRuntime;
147        } else if #[cfg(all(feature="async-std", feature="rustls"))] {
148            use tor_rtcompat::async_std::AsyncStdRustlsRuntime as ChosenRuntime;
149            // Note: See comments in tor_rtcompate::impls::rustls::RustlsProvider
150            // about choice of default crypto provider.
151            let _idempotent_ignore = rustls_crate::crypto::CryptoProvider::install_default(
152                rustls_crate::crypto::ring::default_provider(),
153            );
154        } else {
155            compile_error!("You must configure both an async runtime and a TLS stack. See doc/TROUBLESHOOTING.md for more.");
156        }
157    }
158    ChosenRuntime::create()
159}
160
161/// Return a (non-exhaustive) array of enabled Cargo features, for version printing purposes.
162fn list_enabled_features() -> &'static [&'static str] {
163    // HACK(eta): We can't get this directly, so we just do this awful hack instead.
164    // Note that we only list features that aren't about the runtime used, since that already
165    // gets printed separately.
166    &[
167        #[cfg(feature = "journald")]
168        "journald",
169        #[cfg(any(feature = "static-sqlite", feature = "static"))]
170        "static-sqlite",
171        #[cfg(any(feature = "static-native-tls", feature = "static"))]
172        "static-native-tls",
173    ]
174}
175
176/// Inner function, to handle a set of CLI arguments and return a single
177/// `Result<()>` for convenient handling.
178///
179/// # ⚠️ Warning! ⚠️
180///
181/// If your program needs to call this function, you are setting yourself up for
182/// some serious maintenance headaches.  See discussion on [`main`] and please
183/// reach out to help us build you a better API.
184///
185/// # Panics
186///
187/// Currently, might panic if wrong arguments are specified.
188#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
189#[allow(clippy::cognitive_complexity)]
190fn main_main<I, T>(cli_args: I) -> Result<()>
191where
192    I: IntoIterator<Item = T>,
193    T: Into<std::ffi::OsString> + Clone,
194{
195    // We describe a default here, rather than using `default()`, because the
196    // correct behavior is different depending on whether the filename is given
197    // explicitly or not.
198    let mut config_file_help = "Specify which config file(s) to read.".to_string();
199    if let Ok(default) = default_config_files() {
200        // If we couldn't resolve the default config file, then too bad.  If something
201        // actually tries to use it, it will produce an error, but don't fail here
202        // just for that reason.
203        write!(config_file_help, " Defaults to {:?}", default).expect("Can't write to string");
204    }
205
206    // We create the runtime now so that we can use its `Debug` impl to describe it for
207    // the version string.
208    let runtime = create_runtime()?;
209    let features = list_enabled_features();
210    let long_version = format!(
211        "{}\nusing runtime: {:?}\noptional features: {}",
212        env!("CARGO_PKG_VERSION"),
213        runtime,
214        if features.is_empty() {
215            "<none>".into()
216        } else {
217            features.join(", ")
218        }
219    );
220
221    let clap_app = Command::new("Arti")
222            .version(env!("CARGO_PKG_VERSION"))
223            .long_version(long_version)
224            .author("The Tor Project Developers")
225            .about("A Rust Tor implementation.")
226            // HACK(eta): clap generates "arti [OPTIONS] <SUBCOMMAND>" for this usage string by
227            //            default, but then fails to parse options properly if you do put them
228            //            before the subcommand.
229            //            We just declare all options as `global` and then require them to be
230            //            put after the subcommand, hence this new usage string.
231            .override_usage("arti <SUBCOMMAND> [OPTIONS]")
232            .arg(
233                Arg::new("config-files")
234                    .short('c')
235                    .long("config")
236                    .action(ArgAction::Set)
237                    .value_name("FILE")
238                    .value_parser(value_parser!(OsString))
239                    .action(ArgAction::Append)
240                    // NOTE: don't forget the `global` flag on all arguments declared at this level!
241                    .global(true)
242                    .help(config_file_help),
243            )
244            .arg(
245                Arg::new("option")
246                    .short('o')
247                    .action(ArgAction::Set)
248                    .value_name("KEY=VALUE")
249                    .action(ArgAction::Append)
250                    .global(true)
251                    .help("Override config file parameters, using TOML-like syntax."),
252            )
253            .arg(
254                Arg::new("loglevel")
255                    .short('l')
256                    .long("log-level")
257                    .global(true)
258                    .action(ArgAction::Set)
259                    .value_name("LEVEL")
260                    .help("Override the log level (usually one of 'trace', 'debug', 'info', 'warn', 'error')."),
261            )
262            .arg(
263                Arg::new("disable-fs-permission-checks")
264                    .long("disable-fs-permission-checks")
265                    .global(true)
266                    .action(ArgAction::SetTrue)
267                    .help("Don't check permissions on the files we use."),
268            )
269            .subcommand(
270                Command::new("proxy")
271                    .about(
272                        "Run Arti in SOCKS proxy mode, proxying connections through the Tor network.",
273                    )
274                    .arg(
275                        Arg::new("socks-port")
276                            .short('p')
277                            .action(ArgAction::Set)
278                            .value_name("PORT")
279                            .help("Port to listen on for SOCKS connections (overrides the port in the config if specified).")
280                    )
281                    .arg(
282                        Arg::new("dns-port")
283                            .short('d')
284                            .action(ArgAction::Set)
285                            .value_name("PORT")
286                            .help("Port to listen on for DNS request (overrides the port in the config if specified).")
287                    )
288            )
289            .subcommand_required(true)
290            .arg_required_else_help(true);
291
292    // When adding a subcommand, it may be necessary to add an entry in
293    // `maint/check-cli-help`, to the function `help_arg`.
294
295    cfg_if::cfg_if! {
296        if #[cfg(feature = "onion-service-service")] {
297            let clap_app = subcommands::hss::HssSubcommands::augment_subcommands(clap_app);
298        }
299    }
300
301    cfg_if::cfg_if! {
302        if #[cfg(feature = "hsc")] {
303            let clap_app = subcommands::hsc::HscSubcommands::augment_subcommands(clap_app);
304        }
305    }
306
307    cfg_if::cfg_if! {
308        if #[cfg(feature = "onion-service-cli-extra")] {
309            let clap_app = subcommands::keys::KeysSubcommands::augment_subcommands(clap_app);
310            let clap_app = subcommands::raw::RawSubcommands::augment_subcommands(clap_app);
311        }
312    }
313
314    // Tracing doesn't log anything when there is no subscriber set.  But we want to see
315    // logging messages from config parsing etc.  We can't set the global default subscriber
316    // because we can only set it once.  The other ways involve a closure.  So we have a
317    // closure for all the startup code which runs *before* we set the logging properly.
318    //
319    // There is no cooked way to print our program name, so we do it like this.  This
320    // closure is called to "make" a "Writer" for each message, so it runs at the right time:
321    // before each message.
322    let pre_config_logging_writer = || {
323        // Weirdly, with .without_time(), tracing produces messages with a leading space.
324        eprint!("arti:");
325        std::io::stderr()
326    };
327    let pre_config_logging = tracing_subscriber::fmt()
328        .without_time()
329        .with_writer(pre_config_logging_writer)
330        .finish();
331    let pre_config_logging = tracing::Dispatch::new(pre_config_logging);
332    let pre_config_logging_ret = tracing::dispatcher::with_default(&pre_config_logging, || {
333        let matches = clap_app.try_get_matches_from(cli_args)?;
334
335        let fs_mistrust_disabled = matches.get_flag("disable-fs-permission-checks");
336
337        // A Mistrust object to use for loading our configuration.  Elsewhere, we
338        // use the value _from_ the configuration.
339        let cfg_mistrust = if fs_mistrust_disabled {
340            fs_mistrust::Mistrust::new_dangerously_trust_everyone()
341        } else {
342            fs_mistrust::MistrustBuilder::default()
343                .build_for_arti()
344                .expect("Could not construct default fs-mistrust")
345        };
346
347        let mut override_options: Vec<String> = matches
348            .get_many::<String>("option")
349            .unwrap_or_default()
350            .cloned()
351            .collect();
352        if fs_mistrust_disabled {
353            override_options.push("storage.permissions.dangerously_trust_everyone=true".to_owned());
354        }
355
356        let cfg_sources = {
357            let mut cfg_sources = ConfigurationSources::try_from_cmdline(
358                || default_config_files().context("identify default config file locations"),
359                matches
360                    .get_many::<OsString>("config-files")
361                    .unwrap_or_default(),
362                override_options,
363            )?;
364            cfg_sources.set_mistrust(cfg_mistrust);
365            cfg_sources
366        };
367
368        let cfg = cfg_sources.load()?;
369        let (config, client_config) =
370            tor_config::resolve::<ArtiCombinedConfig>(cfg).context("read configuration")?;
371
372        let log_mistrust = client_config.fs_mistrust().clone();
373
374        Ok::<_, Error>((matches, cfg_sources, config, client_config, log_mistrust))
375    })?;
376    // Sadly I don't seem to be able to persuade rustfmt to format the two lists of
377    // variable names identically.
378    let (matches, cfg_sources, config, client_config, log_mistrust) = pre_config_logging_ret;
379
380    let _log_guards = logging::setup_logging(
381        config.logging(),
382        &log_mistrust,
383        client_config.as_ref(),
384        matches.get_one::<String>("loglevel").map(|s| s.as_str()),
385    )?;
386
387    if !config.application().allow_running_as_root {
388        process::exit_if_root();
389    }
390
391    #[cfg(feature = "harden")]
392    if !config.application().permit_debugging {
393        if let Err(e) = process::enable_process_hardening() {
394            error!(
395                "Encountered a problem while enabling hardening. To disable this feature, set application.permit_debugging to true."
396            );
397            return Err(e);
398        }
399    }
400
401    // Check for the "proxy" subcommand.
402    if let Some(proxy_matches) = matches.subcommand_matches("proxy") {
403        return subcommands::proxy::run(runtime, proxy_matches, cfg_sources, config, client_config);
404    }
405
406    // Check for the optional "keys" and "keys-raw" subcommand.
407    cfg_if::cfg_if! {
408        if #[cfg(feature = "onion-service-cli-extra")] {
409            if let Some(keys_matches) = matches.subcommand_matches("keys") {
410                return subcommands::keys::run(runtime, keys_matches, &config, &client_config);
411            } else if let Some(raw_matches) = matches.subcommand_matches("keys-raw") {
412                return subcommands::raw::run(runtime, raw_matches, &client_config);
413            }
414        }
415    }
416
417    // Check for the optional "hss" subcommand.
418    cfg_if::cfg_if! {
419        if #[cfg(feature = "onion-service-service")] {
420            if let Some(hss_matches) = matches.subcommand_matches("hss") {
421                return subcommands::hss::run(runtime, hss_matches, &config, &client_config);
422            }
423        }
424    }
425
426    // Check for the optional "hsc" subcommand.
427    cfg_if::cfg_if! {
428        if #[cfg(feature = "hsc")] {
429            if let Some(hsc_matches) = matches.subcommand_matches("hsc") {
430                return subcommands::hsc::run(runtime, hsc_matches, &client_config);
431            }
432        }
433    }
434
435    panic!("Subcommand added to clap subcommand list, but not yet implemented");
436}
437
438/// Main program, callable directly from a binary crate's `main`
439///
440/// This function behaves the same as `main_main()`, except:
441///   * It takes command-line arguments from `std::env::args_os` rather than
442///     from an argument.
443///   * It exits the process with an appropriate error code on error.
444///
445/// # ⚠️ Warning ⚠️
446///
447/// Calling this function, or the related experimental function `main_main`, is
448/// probably a bad idea for your code.  It means that you are invoking Arti as
449/// if from the command line, but keeping it embedded inside your process. Doing
450/// this will block your process take over handling for several signal types,
451/// possibly disable debugger attachment, and a lot more junk that a library
452/// really has no business doing for you.  It is not designed to run in this
453/// way, and may give you strange results.
454///
455/// If the functionality you want is available in [`arti_client`] crate, or from
456/// a *non*-experimental API in this crate, it would be better for you to use
457/// that API instead.
458///
459/// Alternatively, if you _do_ need some underlying function from the `arti`
460/// crate, it would be better for all of us if you had a stable interface to that
461/// function. Please reach out to the Arti developers, so we can work together
462/// to get you the stable API you need.
463pub fn main() {
464    match main_main(std::env::args_os()) {
465        Ok(()) => {}
466        Err(e) => {
467            use arti_client::HintableError;
468            if let Some(hint) = e.hint() {
469                info!("{}", hint);
470            }
471
472            match e.downcast_ref::<clap::Error>() {
473                Some(clap_err) => clap_err.exit(),
474                None => with_safe_logging_suppressed(|| tor_error::report_and_exit(e)),
475            }
476        }
477    }
478}