Skip to main content

arti/
lib.rs

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