From 7701f61c71f3dcf67c802b4dd2825bdcbd4a0452 Mon Sep 17 00:00:00 2001 From: Sirius DevOps Date: Thu, 10 Sep 2026 13:43:23 -0400 Subject: [PATCH] hs: wait 360s for HsDir publish; floor cbtmintimeout at 10s 180s fail-closed cut a working descriptor upload while status was still Bootstrapping. Learned CBT can also drop to ~1s and kill HsDir circuits. --- README.md | 2 +- src/hs.rs | 12 ++++++-- tests/hs.rs | 21 ++++++++++++++ tests/tor_hs.rs | 77 ++++++++----------------------------------------- 4 files changed, 44 insertions(+), 68 deletions(-) create mode 100644 tests/hs.rs diff --git a/README.md b/README.md index 5fd576c..1445968 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ Do not run `cargo publish`; `publish = false`. CI needs the repo secret |---|---| | `GLIBC_… not found` | Binary is newer than your libc. Build from source (Option B). | | `sha256sum: FAILED` | Re-download both the binary and `.sha256`; run the check in the same directory. | -| Hang on `bootstrapping Arti…` | Need outbound network. First consensus fetch is slow. Wait a couple of minutes; if it never publishes, it fails closed — no C-tor fallback. | +| Hang on `bootstrapping Arti…` / `hs status: Bootstrapping` | Outbound network required. Client bootstrap is minutes; HsDir descriptor upload can stay Bootstrapping up to ~6 min before `DegradedReachable`. Fail closed after that — no C-tor fallback. | | `onionwire: unknown argument` | No subcommands. Flags are `--version` / `--help` only, then the TUI. | | Blank / broken TUI | Run in a real terminal emulator, not `nohup` / systemd without a TTY. | | Two chats, same laptop | Separate `ONIONWIRE_HOME` per process. | diff --git a/src/hs.rs b/src/hs.rs index 10f9212..2ca2378 100644 --- a/src/hs.rs +++ b/src/hs.rs @@ -12,6 +12,10 @@ 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. +pub const CBT_MIN_TIMEOUT_MS: i32 = 10_000; pub type Client = Arc>; @@ -20,6 +24,9 @@ pub fn client_config(state_dir: &std::path::Path, cache_dir: &std::path::Path) - 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.build().expect("TorClientConfig") } @@ -49,7 +56,7 @@ pub fn onion_string(svc: &RunningOnionService) -> Result { } pub async fn wait_until_published(svc: &RunningOnionService, label: &str) -> Result<(), String> { - let deadline = Instant::now() + Duration::from_secs(180); + let deadline = Instant::now() + PUBLISH_WAIT; let mut events = svc.status_events(); loop { let st = svc.status(); @@ -66,7 +73,8 @@ pub async fn wait_until_published(svc: &RunningOnionService, label: &str) -> Res } if Instant::now() >= deadline { return Err(format!( - "{label}: onion service did not publish within 180s: {:?}", + "{label}: onion service did not publish within {}s: {:?}", + PUBLISH_WAIT.as_secs(), st.state() )); } diff --git a/tests/hs.rs b/tests/hs.rs new file mode 100644 index 0000000..cff7530 --- /dev/null +++ b/tests/hs.rs @@ -0,0 +1,21 @@ +//! HS publish wait and CBT floor — 180s fail-closed cuts a working HsDir upload. + +use std::time::Duration; + +use onionwire::hs; + +#[test] +fn publish_wait_covers_hsdir_retries() { + assert!( + hs::PUBLISH_WAIT >= Duration::from_secs(360), + "180s cuts a working HsDir publish while status is still Bootstrapping" + ); +} + +#[test] +fn cbt_min_timeout_floor_is_at_least_10s() { + assert!( + hs::CBT_MIN_TIMEOUT_MS >= 10_000, + "learned CBT ~1s kills HsDir circuits" + ); +} diff --git a/tests/tor_hs.rs b/tests/tor_hs.rs index 941a8c7..2808df8 100644 --- a/tests/tor_hs.rs +++ b/tests/tor_hs.rs @@ -6,53 +6,22 @@ use std::sync::Arc; use std::time::{Duration, Instant}; -use arti_client::config::TorClientConfigBuilder; -use arti_client::{DormantMode, TorClient, TorClientConfig}; +use arti_client::DormantMode; use futures::StreamExt; use futures::io::{AsyncReadExt, AsyncWriteExt}; -use safelog::DisplayRedacted; +use onionwire::hs::{self, Client, HS_PORT}; use tor_cell::relaycell::msg::Connected; -use tor_hsservice::status::State; -use tor_hsservice::{HsNickname, OnionServiceConfig, RunningOnionService, handle_rend_requests}; -use tor_rtcompat::PreferredRuntime; +use tor_hsservice::{RunningOnionService, handle_rend_requests}; const PING: &[u8] = b"ping"; -const HS_PORT: u16 = 80; const BOOTSTRAP_LOG: &str = "info"; -type Client = Arc>; - -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); - // Temp dirs sit under $TMP; skip fs-mistrust on the parent tree. - builder.storage().permissions().dangerously_trust_everyone(); - builder.build().expect("TorClientConfig") -} - async fn bootstrapped(state_dir: &std::path::Path, cache_dir: &std::path::Path) -> Client { - let cfg = client_config(state_dir, cache_dir); - TorClient::create_bootstrapped(cfg) + hs::bootstrapped(state_dir, cache_dir) .await .expect("Arti bootstrap failed — fail closed, no C-tor fallback") } -fn hs_config(nickname: &str) -> OnionServiceConfig { - let nickname = HsNickname::new(nickname.to_string()).expect("HsNickname"); - OnionServiceConfig::builder() - .nickname(nickname) - .build() - .expect("OnionServiceConfig") -} - -fn onion_string(svc: &RunningOnionService) -> String { - let id = svc - .onion_address() - .expect("onion identity missing from keystore"); - id.display_unredacted().to_string() -} - fn spawn_echo( rend: impl futures::Stream + Send + 'static, ) -> tokio::task::JoinHandle<()> { @@ -79,41 +48,15 @@ async fn launch_echo( nickname: &str, ) -> (Arc, tokio::task::JoinHandle<()>, String) { let launched = client - .launch_onion_service(hs_config(nickname)) + .launch_onion_service(hs::hs_config(nickname).expect("hs_config")) .expect("launch_onion_service") .expect("onion service disabled in config — fail closed"); let (svc, rend) = launched; - let onion = onion_string(&svc); + let onion = hs::onion_string(&svc).expect("onion_string"); let echo = spawn_echo(rend); (svc, echo, onion) } -async fn wait_until_published(svc: &RunningOnionService, label: &str) { - let deadline = Instant::now() + Duration::from_secs(180); - let mut events = svc.status_events(); - loop { - let st = svc.status(); - eprintln!("{label} hs status: {:?}", st.state()); - match st.state() { - State::Running | State::DegradedReachable => return, - State::Broken => { - panic!("{label}: onion service broken: {:?}", st.current_problem()) - } - _ => {} - } - if Instant::now() >= deadline { - panic!( - "{label}: onion service did not publish within 180s: {:?}", - st.state() - ); - } - tokio::select! { - _ = events.next() => {} - _ = tokio::time::sleep(Duration::from_secs(2)) => {} - } - } -} - async fn echo_ping(client: &Client, onion: &str) -> Result<(), String> { let mut stream = client .connect((onion, HS_PORT)) @@ -181,8 +124,12 @@ async fn two_node_byte_pipe_restart_and_dormant() { eprintln!("bob onion={bob_onion}"); assert_ne!(alice_onion, bob_onion, "separate HS identities"); - wait_until_published(&alice_svc, "alice").await; - wait_until_published(&bob_svc, "bob").await; + hs::wait_until_published(&alice_svc, "alice") + .await + .expect("alice publish"); + hs::wait_until_published(&bob_svc, "bob") + .await + .expect("bob publish"); eprintln!("echo ping alice → bob"); echo_ping_retry(&alice, &bob_onion, Duration::from_secs(180)).await;