diff --git a/core/archipelago/src/api/rpc/tor/mod.rs b/core/archipelago/src/api/rpc/tor/mod.rs index f4affa5a..1dab358f 100644 --- a/core/archipelago/src/api/rpc/tor/mod.rs +++ b/core/archipelago/src/api/rpc/tor/mod.rs @@ -184,9 +184,42 @@ async fn archy_net_gateway_and_subnet() -> Option<(String, String)> { if gateway.is_empty() || subnet.is_empty() { return None; } + // Podman REPORTING a gateway is not proof the host can bind it. Under + // rootless podman the bridge and its gateway live inside a network + // namespace, so the address never appears on a host interface — and Tor + // binds it at startup, not at config-check time (`--verify-config` passes + // happily). The result on austin-sapien, 2026-08-09: + // + // [warn] Could not bind to 10.89.0.1:9050: Cannot assign requested address + // [warn] Failed to parse/validate config: Failed to bind one of the listener ports. + // [err] Reading config failed--see warnings above. + // + // Tor then refuses to start AT ALL — loopback SOCKS and every hidden + // service go with it. Widening SOCKS must never be able to take the whole + // daemon down, so probe the address exactly the way Tor will and fall back + // to the loopback-only branch when it is not bindable. + if !host_can_bind(&gateway) { + tracing::warn!( + %gateway, + "archy-net gateway is not bindable on this host (rootless podman \ + namespaces the bridge) — keeping Tor SOCKS loopback-only" + ); + return None; + } Some((gateway, subnet)) } +/// Can this host actually bind `addr`? Binds an ephemeral port, the same +/// operation Tor performs, so the answer matches Tor's own behaviour rather +/// than inferring it from interface listings. +fn host_can_bind(addr: &str) -> bool { + use std::net::{IpAddr, SocketAddr, TcpListener}; + match addr.parse::() { + Ok(ip) => TcpListener::bind(SocketAddr::new(ip, 0)).is_ok(), + Err(_) => false, + } +} + pub(in crate::api::rpc) async fn regenerate_torrc(config: &ServicesConfig) -> Result<()> { let base = detect_hidden_service_base(); let mut lines = vec![