mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 07:48:27 +00:00
fix(tor): map Arti errors to accurate SOCKS reply codes
The JNI wrapper answered every failed client.connect() with SOCKS reply 0x05,
so a domain that no longer exists, an exit that timed out, and a genuinely
refused port were indistinguishable. Java renders 0x05 as
SocketException("SOCKS: Connection refused"), so the whole failure taxonomy
collapsed into one opaque string and callers could only apply their most
generic retry policy. On a cold start with the default settings — torType
INTERNAL and newRelaysViaTor true, so the entire outbox fan-out is routed
through Tor — 639 of roughly 768 relay failures arrived this way.
This is why the DNS classification added earlier was effectively dead code for
default users: name resolution happens at the exit, so UnknownHostException is
never raised locally.
socks_reply_for() maps ErrorKind onto the codes Java surfaces with distinct
messages, so a caller can tell "this relay is gone" from "this circuit had a
bad minute". No new dependency — arti-client re-exports ErrorKind and HasKind.
RemoteHostResolutionFailed is mapped to 0x04 even though Arti documents it as
retryable, because an exit's resolver failing is not proof the name is dead.
The caller's response to 0x04 is a bounded backoff rather than permanent
condemnation, which is a retry, just a slower one; probing the relays that
produced this error found 17 of 21 to be NXDOMAIN from an ordinary resolver, so
the conservative reading costs far more than it saves. Changing that arm to
0x01 restores Arti's reading if that tradeoff is ever judged wrong.
Verified on device: Java now reports "SOCKS: TTL expired", "SOCKS: Host
unreachable", "SOCKS: Connection not allowed by ruleset" and friends where
everything was previously "SOCKS: Connection refused".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,9 @@ use jni::JavaVM;
|
||||
|
||||
use arti_client::TorClient;
|
||||
use arti_client::config::TorClientConfigBuilder;
|
||||
// `kind()` is a trait method (tor_error::HasKind), not inherent, so the trait has to be in
|
||||
// scope wherever we classify a connect failure. Both are re-exported by arti-client.
|
||||
use arti_client::HasKind;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
|
||||
use std::sync::{Arc, Mutex, Once};
|
||||
@@ -308,6 +311,50 @@ pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_startSocksPr
|
||||
}
|
||||
|
||||
/// Handle a single SOCKS5 connection through Tor.
|
||||
/// Maps an Arti connect failure onto the SOCKS5 reply code that means the same thing.
|
||||
///
|
||||
/// Why this matters: every failure used to be reported as 0x05 (connection refused), so a
|
||||
/// domain that no longer exists, an exit that timed out, and a genuinely refused port were
|
||||
/// indistinguishable to the client. Java's SOCKS client renders 0x05 as
|
||||
/// `SocketException("SOCKS: Connection refused")`, so the whole failure taxonomy collapsed
|
||||
/// into one opaque string and the caller could only apply its most generic retry policy.
|
||||
/// Measured on a cold start: 639 of ~768 relay failures arrived this way.
|
||||
///
|
||||
/// The codes below are the ones Java surfaces with distinct messages, which is what lets the
|
||||
/// caller tell "this relay is gone" from "this circuit had a bad minute":
|
||||
/// 0x01 general failure -> "SOCKS server general failure"
|
||||
/// 0x02 not allowed -> "SOCKS: Connection not allowed by ruleset"
|
||||
/// 0x03 network unreachable -> "SOCKS: Network unreachable"
|
||||
/// 0x04 host unreachable -> "SOCKS: Host unreachable"
|
||||
/// 0x05 connection refused -> "SOCKS: Connection refused"
|
||||
/// 0x06 TTL expired -> "SOCKS: TTL expired"
|
||||
///
|
||||
/// Note on name-resolution failures: Arti documents `RemoteHostResolutionFailed` as
|
||||
/// retryable, because an exit's resolver failing is not proof the name is dead. We still map
|
||||
/// it to 0x04 rather than 0x01, because the caller's response to 0x04 is a bounded backoff,
|
||||
/// not permanent condemnation — which *is* a retry, just a slower one. Probing the relays
|
||||
/// that produced this error found 17 of 21 to be NXDOMAIN from a normal resolver, so the
|
||||
/// conservative reading costs far more than it saves. `RemoteHostNotFound` (the unambiguous
|
||||
/// case) maps to 0x04 too.
|
||||
fn socks_reply_for(e: &arti_client::Error) -> u8 {
|
||||
use arti_client::ErrorKind::*;
|
||||
|
||||
match e.kind() {
|
||||
// The name does not resolve, or the exit could not resolve it.
|
||||
RemoteHostNotFound | RemoteHostResolutionFailed => 0x04,
|
||||
// The host answered and said no. A real, specific refusal.
|
||||
RemoteConnectionRefused => 0x05,
|
||||
// The exit refuses this destination by policy; another exit may allow it.
|
||||
ExitPolicyRejected => 0x02,
|
||||
// No route from the exit.
|
||||
RemoteNetworkFailed => 0x03,
|
||||
// Timed out — transient by nature, at the exit or against our own threshold.
|
||||
ExitTimeout | RemoteNetworkTimeout => 0x06,
|
||||
// Anything else stays deliberately vague rather than being mislabelled.
|
||||
_ => 0x01,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_socks_connection(
|
||||
mut stream: tokio::net::TcpStream,
|
||||
client: Arc<TorClient<PreferredRuntime>>,
|
||||
@@ -381,8 +428,12 @@ async fn handle_socks_connection(
|
||||
let tor_stream = match client.connect((target_host.as_str(), target_port)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log_error!("Failed to connect through Tor to {}:{}: {:?}", target_host, target_port, e);
|
||||
stream.write_all(&[0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||
let reply = socks_reply_for(&e);
|
||||
log_error!(
|
||||
"Failed to connect through Tor to {}:{}: kind={:?} socks_reply=0x{:02x} {:?}",
|
||||
target_host, target_port, e.kind(), reply, e
|
||||
);
|
||||
stream.write_all(&[0x05, reply, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user