tor: tear down client deterministically on stop (ENV-3101) - #79
tor: tear down client deterministically on stop (ENV-3101)#79Jacksper13 wants to merge 6 commits into
Conversation
Idle accepted SOCKS tasks retained TorClient and tor_cache/dir.lock after the accept loop stopped. Own and shut down the per-client runtime so those tasks are cancelled before Dart drops the final client wrapper. Propagate teardown failures to prevent a false-success restart.
An accept loop that ended before the abort landed is already torn down: its listeners and TorClient clone are released. Reporting its stale error as a teardown failure made stop() throw in exactly the state that most needs a restart, since arti treats most accept() errors as fatal and ends the loop. Only an unfinished task now fails teardown. A stale error or panic is logged and reported as success.
|
Reviewed 085865f and pushed one fix on top as e3d7eb7. The commit is right. Owning the arti runtime explicitly and shutting it down is the correct lever: arti spawns one detached task per accepted SOCKS connection on the client's runtime, so killing that runtime is what actually releases their The bug I fixed. This arm: Ok(Ok(result)) => result.map_err(|e| TorError::ProxyStopError(e.to_string())),fires when the task completed before the abort landed — i.e. the accept loop had already died on its own. That is not hypothetical: arti's In that state teardown has genuinely succeeded — the task is finished, its listeners and client clone are already dropped — yet Now only an unfinished task fails teardown. A stale accept-loop error or a panic is logged via Envoy-side follow-up: Foundation-Devices/envoy-dev#1409 now guards against the propagation change. Envoy's only PR description is stale — it still lists the detached per-connection tasks as a known residual left to later lifecycle work, which 085865f fixes. I'll rewrite that paragraph so reviewers don't think the hole is still open. Validation on the current head: |
The Dart package advanced to 0.2.1 while the native crate and generated framework metadata stayed at 0.2.0. Make Cargo authoritative so rebuilt SwiftPM binaries carry the release version without another synchronized constant.
Refresh the checked-in iOS device and simulator binaries so SwiftPM consumers receive the deterministic proxy teardown implementation. Record crate version 0.2.1 in both framework bundles.
The separately owned Tokio runtime made the first iOS SOCKS route accept requests but fail every circuit immediately. Keep Arti's standard owned runtime and cancel its tracked tasks explicitly so restart teardown still releases accepted connections and dir.lock.
Problem
stop()only aborted the SOCKS accept-loop task. Everything else stayed alive until Dart's GC finalized the opaque wrappers — nondeterministically, potentially minutes later:TorClientitself (dirmgr, circmgr, its tokio runtime), andTorClientclone until its TCP stream closed.A restart therefore created a second in-process client against the same
tor_statedirectory while the zombie still heldtor_cache/dir.lock. Arti's SqliteStore silently degrades to a read-only directory store in that case, so the new client could not fetch a consensus and every SOCKS CONNECT answeredgeneral server failureuntil GC happened to run. This matches the ENV-3039 field reports (Tor works only after staying foregrounded for one to two minutes; restarting does not help) and the ENV-3035 restart loops.There was also a second hidden reference:
TorInstance.client/TorInstance.proxygetters clone, so theTorInstancecontainer kept an extra client reference alive until its own GC finalization even after the extracted wrappers were released.Changes
start_tornow builds and owns the arti runtime (create_arti_runtime) instead of lettingTokioNativeTlsRuntime::create()hide it, and parks the owning handle inTorProxyState.stop_proxyaborts the accept loop, awaits it (bounded byPROXY_SHUTDOWN_TIMEOUT), then shuts the client runtime down — cancelling the per-connection tasks so even idle SOCKS connections release theirTorClient.Tor.stop()disposes the Rust client wrapper in afinallyblock instead of leaving it to GC;_startInternaldisposes theTorInstancecontainer immediately after extracting client/proxy/port.example/pubspec.lockbump.Scope
No public API change, so the generated bindings are untouched.
Left to the broader lifecycle work, which should follow separately: the start/stop generation guard, bounding
start_tor, and surfacing proxy/bootstrap errors to Dart as events.Two follow-ups on the consumer side:
stop()caller wraps stop and start in a singletry, so the propagation change above would otherwise skip the start and leave Tor enabled-but-never-bootstrapped — the state whereisReady()parks every request forever. That PR gives the stop its own catch.b2482791, this branch's previous head) should be bumped to the new head.Validation
cargo fmt --check,cargo clippy --all-targetsandcargo test --lockedcleandart formatclean at the package language versionstop_proxy_drops_an_idle_connectionasserts the client'sArcstrong count returns to baseline after stopping with an idle accepted connection;stop_proxy_succeeds_when_the_accept_loop_already_failedpins that a stale accept-loop error still tears down cleanly