From 2090cef3bff069b1217c12c3283c3e20c52920c2 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 9 Aug 2026 05:14:25 -0400 Subject: [PATCH] fix(tor): never emit a SOCKS listener the host cannot bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit regenerate_torrc widened SOCKS to the archy-net gateway whenever `podman network inspect archy-net` reported one. 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 and never appear on a host interface. Tor binds listeners at STARTUP, not at config-check time — `--verify-config` passes happily — so the failure lands as a refusal to start. 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 died completely — loopback SOCKS and every hidden service with it — having run 3d 18h before something restarted it. The node's Home tab still read "connected" because tor.service is Debian's multi-instance MASTER unit, which is active(exited) by design; the real instance, tor@default, was failed. Whether a node is affected is pure timing: archi-dev-box's torrc has only `SocksPort 9050` because archy-net was not discoverable when its torrc was last regenerated, so it fell into the loopback branch and Tor starts fine. Nodes whose torrc was generated while archy-net was visible are one Tor restart away from the same outage. The gateway is now probed the way Tor will use it — bind an ephemeral port on that exact address — and a failure falls through to the existing loopback-only branch, which was already written and commented "fail closed". Widening SOCKS must never be able to take the whole daemon down. Verified on this host: 127.0.0.1 binds, 10.89.0.1 returns EADDRNOTAVAIL. Tor restored on austin-sapien and confirmed end to end — bootstrapped 100%, and a request through the proxy returns {"IsTor":true}. Co-Authored-By: Claude Opus 5 (1M context) --- core/archipelago/src/api/rpc/tor/mod.rs | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) 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![