fix(tor): never emit a SOCKS listener the host cannot bind

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) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-09 05:14:25 -04:00
co-authored by Claude Opus 5
parent 791e6e5ed9
commit 2090cef3bf
+33
View File
@@ -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::<IpAddr>() {
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![