feat: fix Tor rotation to handle system Tor and hostname caching
read_onion_address() now checks tor-hostnames readable cache first, clears cache before wait_for_hostname, updates it after rotation. Rotation restarts system Tor (not just archy-tor container). Created test-tor-rotation.sh with 10 automated checks (INSTALL-03). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -161,28 +161,58 @@ impl RpcHandler {
|
||||
return Err(anyhow::anyhow!("Failed to rename hidden service directory for rotation"));
|
||||
}
|
||||
|
||||
info!(service = name, old_onion = ?old_onion, "Rotated Tor service — restarting container");
|
||||
// Clear the readable tor-hostnames cache so wait_for_hostname reads the new key
|
||||
let hostnames_dir = std::path::Path::new(&base)
|
||||
.parent()
|
||||
.unwrap_or(std::path::Path::new("/var/lib/archipelago"))
|
||||
.join("tor-hostnames");
|
||||
let _ = tokio::fs::remove_file(hostnames_dir.join(name)).await;
|
||||
|
||||
// Restart archy-tor container so Tor generates new keys
|
||||
let restart_status = tokio::process::Command::new("sudo")
|
||||
.args(["podman", "restart", "archy-tor"])
|
||||
info!(service = name, old_onion = ?old_onion, "Rotated Tor service — restarting Tor");
|
||||
|
||||
// Try system Tor first (hidden services may be in /etc/tor/torrc), then container
|
||||
let system_ok = tokio::process::Command::new("sudo")
|
||||
.args(["systemctl", "restart", "tor"])
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to restart archy-tor container")?;
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !restart_status.success() {
|
||||
warn!("Failed to restart archy-tor container after rotation");
|
||||
// Try to restore old directory
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["mv", &old_dir, &service_dir])
|
||||
if !system_ok {
|
||||
// Fall back to container restart
|
||||
let container_ok = tokio::process::Command::new("sudo")
|
||||
.args(["podman", "restart", "archy-tor"])
|
||||
.status()
|
||||
.await;
|
||||
return Err(anyhow::anyhow!("Failed to restart Tor — rotation rolled back"));
|
||||
.await
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
if !container_ok {
|
||||
warn!("Failed to restart Tor after rotation");
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["mv", &old_dir, &service_dir])
|
||||
.status()
|
||||
.await;
|
||||
return Err(anyhow::anyhow!("Failed to restart Tor — rotation rolled back"));
|
||||
}
|
||||
}
|
||||
|
||||
// Wait up to 60s for new hostname file to appear
|
||||
let new_onion = wait_for_hostname(name, 60).await;
|
||||
|
||||
// Update the readable tor-hostnames copy
|
||||
if let Some(ref new_addr) = new_onion {
|
||||
let hostnames_dir = std::path::Path::new(&base)
|
||||
.parent()
|
||||
.unwrap_or(std::path::Path::new("/var/lib/archipelago"))
|
||||
.join("tor-hostnames");
|
||||
if let Err(e) = tokio::fs::create_dir_all(&hostnames_dir).await {
|
||||
warn!("Failed to create tor-hostnames dir: {}", e);
|
||||
}
|
||||
if let Err(e) = tokio::fs::write(hostnames_dir.join(name), new_addr).await {
|
||||
warn!("Failed to update tor-hostnames copy: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate address change to Nostr relays and federation peers (fire-and-forget)
|
||||
if let Some(ref new_addr) = new_onion {
|
||||
let data_dir = self.config.data_dir.clone();
|
||||
@@ -386,12 +416,39 @@ async fn list_services(config_dir: &std::path::Path) -> Result<Vec<TorService>>
|
||||
}
|
||||
|
||||
/// Read .onion address from hostname file.
|
||||
/// Checks tor-hostnames readable copy first, then hidden service dir (with sudo fallback).
|
||||
fn read_onion_address(service_name: &str) -> Option<String> {
|
||||
let path = std::path::Path::new(&tor_data_dir())
|
||||
let base = tor_data_dir();
|
||||
let base_path = std::path::Path::new(&base);
|
||||
|
||||
// Try readable hostname copy first (system Tor owns hidden_service dirs at 0700)
|
||||
let hostnames_dir = base_path
|
||||
.parent()
|
||||
.unwrap_or(std::path::Path::new("/var/lib/archipelago"))
|
||||
.join("tor-hostnames")
|
||||
.join(service_name);
|
||||
if let Some(addr) = std::fs::read_to_string(&hostnames_dir)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| s.ends_with(".onion") && s.len() >= 60)
|
||||
{
|
||||
return Some(addr);
|
||||
}
|
||||
|
||||
// Fall back to hidden service directory (direct read, then sudo)
|
||||
let path = base_path
|
||||
.join(format!("hidden_service_{}", service_name))
|
||||
.join("hostname");
|
||||
std::fs::read_to_string(path)
|
||||
std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
std::process::Command::new("sudo")
|
||||
.args(["cat", &path.to_string_lossy()])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
})
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| s.ends_with(".onion") && s.len() >= 60)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user