//! In-process Arti onion-service helpers (no C-tor). use std::sync::Arc; use std::time::{Duration, Instant}; use arti_client::config::TorClientConfigBuilder; use arti_client::{TorClient, TorClientConfig}; use futures::StreamExt; use safelog::DisplayRedacted; use tor_hsservice::status::State; use tor_hsservice::{HsNickname, OnionServiceConfig, RunningOnionService}; use tor_rtcompat::PreferredRuntime; pub const HS_PORT: u16 = 80; /// HsDir descriptor upload often stays Bootstrapping past 3 minutes. pub const PUBLISH_WAIT: Duration = Duration::from_secs(360); /// Consensus default `cbtmintimeout` is 10ms; learned CBT can drop to ~1s. /// 4-hop vanguard HS circuits need more than 10s on a slow net. pub const CBT_MIN_TIMEOUT_MS: i32 = 20_000; /// Consensus `cbtinitialtimeout` can be ~2s; match the min floor. pub const CBT_INITIAL_TIMEOUT_MS: i32 = CBT_MIN_TIMEOUT_MS; /// OnionWire never uses exit ports. Default 80/443 preemptive circuits /// compete with IPT + HsDir builds during publish. pub const PREEMPTIVE_PREDICTED_PORTS: &[u16] = &[]; const PROBE_EVERY: Duration = Duration::from_secs(15); const PROBE_TIMEOUT: Duration = Duration::from_secs(12); pub type Client = Arc>; pub fn client_config(state_dir: &std::path::Path, cache_dir: &std::path::Path) -> TorClientConfig { std::fs::create_dir_all(state_dir).expect("state dir"); std::fs::create_dir_all(cache_dir).expect("cache dir"); let mut builder = TorClientConfigBuilder::from_directories(state_dir, cache_dir); builder.storage().permissions().dangerously_trust_everyone(); builder .override_net_params() .insert("cbtmintimeout".to_string(), CBT_MIN_TIMEOUT_MS); builder .override_net_params() .insert("cbtinitialtimeout".to_string(), CBT_INITIAL_TIMEOUT_MS); builder .preemptive_circuits() .set_initial_predicted_ports(PREEMPTIVE_PREDICTED_PORTS.to_vec()); builder.build().expect("TorClientConfig") } pub async fn bootstrapped( state_dir: &std::path::Path, cache_dir: &std::path::Path, ) -> Result { let cfg = client_config(state_dir, cache_dir); TorClient::create_bootstrapped(cfg) .await .map_err(|e| format!("Arti bootstrap failed — fail closed, no C-tor fallback: {e}")) } pub fn hs_config(nickname: &str) -> Result { let nickname = HsNickname::new(nickname.to_string()).map_err(|e| format!("HsNickname: {e}"))?; OnionServiceConfig::builder() .nickname(nickname) .build() .map_err(|e| format!("OnionServiceConfig: {e}")) } pub fn onion_string(svc: &RunningOnionService) -> Result { let id = svc .onion_address() .ok_or_else(|| "onion identity missing from keystore".to_string())?; Ok(id.display_unredacted().to_string()) } /// Combined Arti status stays Bootstrapping through a 5 min HsDir upload /// round. A successful connect means some HsDir already has the descriptor. pub fn hs_is_ready(state: State, probe_ok: bool) -> bool { match state { State::Broken | State::Shutdown => false, State::Running | State::DegradedReachable => true, _ => probe_ok, } } pub async fn wait_until_published( client: &Client, svc: &RunningOnionService, onion: &str, label: &str, ) -> Result<(), String> { let deadline = Instant::now() + PUBLISH_WAIT; let mut events = svc.status_events(); let mut next_probe = Instant::now() + PROBE_EVERY; loop { let st = svc.status(); let state = st.state(); eprintln!("{label} hs status: {state:?}"); if hs_is_ready(state, false) { return Ok(()); } if matches!(state, State::Broken) { return Err(format!( "{label}: onion service broken: {:?}", st.current_problem() )); } if Instant::now() >= deadline { return Err(format!( "{label}: onion service did not publish within {}s: {state:?}", PUBLISH_WAIT.as_secs(), )); } let now = Instant::now(); if now >= next_probe { eprintln!("{label} probing reachability"); let probe_ok = tokio::time::timeout(PROBE_TIMEOUT, client.connect((onion, HS_PORT))) .await .ok() .and_then(Result::ok) .is_some(); if probe_ok { eprintln!("{label} hs reachable while status {state:?}"); } if hs_is_ready(state, probe_ok) { return Ok(()); } next_probe = Instant::now() + PROBE_EVERY; continue; } tokio::select! { _ = events.next() => {} _ = tokio::time::sleep(Duration::from_secs(2)) => {} } } }