2026-09-10 02:07:40 -04:00
|
|
|
//! 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;
|
2026-09-10 13:43:23 -04:00
|
|
|
/// 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.
|
2026-09-10 16:14:10 -04:00
|
|
|
/// 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);
|
2026-09-10 02:07:40 -04:00
|
|
|
|
|
|
|
|
pub type Client = Arc<TorClient<PreferredRuntime>>;
|
|
|
|
|
|
|
|
|
|
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();
|
2026-09-10 13:43:23 -04:00
|
|
|
builder
|
|
|
|
|
.override_net_params()
|
|
|
|
|
.insert("cbtmintimeout".to_string(), CBT_MIN_TIMEOUT_MS);
|
2026-09-10 16:14:10 -04:00
|
|
|
builder
|
|
|
|
|
.override_net_params()
|
|
|
|
|
.insert("cbtinitialtimeout".to_string(), CBT_INITIAL_TIMEOUT_MS);
|
|
|
|
|
builder
|
|
|
|
|
.preemptive_circuits()
|
|
|
|
|
.set_initial_predicted_ports(PREEMPTIVE_PREDICTED_PORTS.to_vec());
|
2026-09-10 02:07:40 -04:00
|
|
|
builder.build().expect("TorClientConfig")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn bootstrapped(
|
|
|
|
|
state_dir: &std::path::Path,
|
|
|
|
|
cache_dir: &std::path::Path,
|
|
|
|
|
) -> Result<Client, String> {
|
|
|
|
|
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<OnionServiceConfig, String> {
|
|
|
|
|
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<String, String> {
|
|
|
|
|
let id = svc
|
|
|
|
|
.onion_address()
|
|
|
|
|
.ok_or_else(|| "onion identity missing from keystore".to_string())?;
|
|
|
|
|
Ok(id.display_unredacted().to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-10 16:14:10 -04:00
|
|
|
/// 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> {
|
2026-09-10 13:43:23 -04:00
|
|
|
let deadline = Instant::now() + PUBLISH_WAIT;
|
2026-09-10 02:07:40 -04:00
|
|
|
let mut events = svc.status_events();
|
2026-09-10 16:14:10 -04:00
|
|
|
let mut next_probe = Instant::now() + PROBE_EVERY;
|
2026-09-10 02:07:40 -04:00
|
|
|
loop {
|
|
|
|
|
let st = svc.status();
|
2026-09-10 16:14:10 -04:00
|
|
|
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()
|
|
|
|
|
));
|
2026-09-10 02:07:40 -04:00
|
|
|
}
|
|
|
|
|
if Instant::now() >= deadline {
|
|
|
|
|
return Err(format!(
|
2026-09-10 16:14:10 -04:00
|
|
|
"{label}: onion service did not publish within {}s: {state:?}",
|
2026-09-10 13:43:23 -04:00
|
|
|
PUBLISH_WAIT.as_secs(),
|
2026-09-10 02:07:40 -04:00
|
|
|
));
|
|
|
|
|
}
|
2026-09-10 16:14:10 -04:00
|
|
|
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;
|
|
|
|
|
}
|
2026-09-10 02:07:40 -04:00
|
|
|
tokio::select! {
|
|
|
|
|
_ = events.next() => {}
|
|
|
|
|
_ = tokio::time::sleep(Duration::from_secs(2)) => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|