refactor: socks5 chaining

This commit is contained in:
zhom
2026-03-16 17:48:02 +04:00
parent 29dd5abb34
commit 8511535d69
2 changed files with 168 additions and 164 deletions
+63 -49
View File
@@ -1228,40 +1228,50 @@ async fn test_local_proxy_with_socks5_upstream(
let (socks_port, socks_handle) = start_mock_socks5_server().await;
println!("Mock SOCKS5 server on port {socks_port}");
// Start donut-proxy with socks5 upstream
let output = TestUtils::execute_command(
&binary_path,
&[
"proxy",
"start",
"--host",
"127.0.0.1",
"--proxy-port",
&socks_port.to_string(),
"--type",
"socks5",
],
)
.await?;
// Helper to start a socks5 proxy
async fn start_socks5_proxy(
binary_path: &std::path::PathBuf,
socks_port: u16,
) -> Result<(String, u16), Box<dyn std::error::Error + Send + Sync>> {
let output = TestUtils::execute_command(
binary_path,
&[
"proxy",
"start",
"--host",
"127.0.0.1",
"--proxy-port",
&socks_port.to_string(),
"--type",
"socks5",
],
)
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Proxy start failed: {stderr}").into());
}
let config: Value = serde_json::from_str(&String::from_utf8(output.stdout)?)?;
let id = config["id"].as_str().unwrap().to_string();
let port = config["localPort"].as_u64().unwrap() as u16;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
target_handle.abort();
socks_handle.abort();
return Err(format!("Proxy start failed - stdout: {stdout}, stderr: {stderr}").into());
// Wait for proxy to be fully ready by verifying it accepts and responds
for _ in 0..20 {
sleep(Duration::from_millis(100)).await;
if TcpStream::connect(("127.0.0.1", port)).await.is_ok() {
break;
}
}
// Extra settle time for the accept loop to be fully initialized
sleep(Duration::from_millis(200)).await;
Ok((id, port))
}
let stdout = String::from_utf8(output.stdout)?;
let config: Value = serde_json::from_str(&stdout)?;
let proxy_id = config["id"].as_str().unwrap().to_string();
let local_port = config["localPort"].as_u64().unwrap() as u16;
tracker.track_proxy(proxy_id.clone());
println!("donut-proxy started: id={proxy_id}, port={local_port}");
sleep(Duration::from_millis(500)).await;
// Test 1: HTTP request through donut-proxy -> SOCKS5 -> target
let (proxy_id, local_port) = start_socks5_proxy(&binary_path, socks_port).await?;
tracker.track_proxy(proxy_id);
let mut stream = TcpStream::connect(("127.0.0.1", local_port)).await?;
let request = format!(
"GET http://127.0.0.1:{target_port}/ HTTP/1.1\r\nHost: 127.0.0.1:{target_port}\r\nConnection: close\r\n\r\n"
@@ -1283,17 +1293,22 @@ async fn test_local_proxy_with_socks5_upstream(
println!("SOCKS5 HTTP proxy test passed");
drop(stream);
// Allow proxy to settle between tests
sleep(Duration::from_millis(500)).await;
// Test 2: CONNECT tunnel through a FRESH proxy -> SOCKS5 -> target
// Use a separate proxy instance so the first connection can't interfere.
// The raw TCP CONNECT handler can have timing sensitivity in test environments
// (passes reliably in production and with --nocapture), so retry a few times.
let (proxy_id2, local_port2) = start_socks5_proxy(&binary_path, socks_port).await?;
tracker.track_proxy(proxy_id2);
// Test 2: CONNECT tunnel through donut-proxy -> SOCKS5 -> target
// This is the critical path for HTTPS browsing.
// The proxy's raw TCP handler can race with prior connection cleanup, so retry.
let mut connect_ok = false;
for attempt in 1..=5 {
sleep(Duration::from_millis(200)).await;
let Ok(mut stream) = TcpStream::connect(("127.0.0.1", local_port)).await else {
continue;
let mut connect_passed = false;
for attempt in 1..=10 {
if attempt > 1 {
sleep(Duration::from_secs(1)).await;
}
let mut stream = match TcpStream::connect(("127.0.0.1", local_port2)).await {
Ok(s) => s,
Err(_) => continue,
};
let _ = stream.set_nodelay(true);
let connect_req =
@@ -1305,20 +1320,16 @@ async fn test_local_proxy_with_socks5_upstream(
let mut buf = [0u8; 4096];
let n = match tokio::time::timeout(Duration::from_secs(5), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => n,
_ => {
println!("CONNECT attempt {attempt}/5: empty response, retrying");
continue;
}
_ => continue,
};
if !String::from_utf8_lossy(&buf[..n]).contains("200") {
continue;
}
// Tunnel established — send HTTP through it
let inner_req =
let inner_request =
format!("GET / HTTP/1.1\r\nHost: 127.0.0.1:{target_port}\r\nConnection: close\r\n\r\n");
if stream.write_all(inner_req.as_bytes()).await.is_err() {
if stream.write_all(inner_request.as_bytes()).await.is_err() {
continue;
}
@@ -1329,12 +1340,15 @@ async fn test_local_proxy_with_socks5_upstream(
};
if String::from_utf8_lossy(&resp[..n]).contains("SOCKS5-TARGET-RESPONSE") {
connect_ok = true;
connect_passed = true;
println!("SOCKS5 CONNECT tunnel test passed (attempt {attempt})");
break;
}
}
assert!(connect_ok, "CONNECT tunnel through SOCKS5 should work");
assert!(
connect_passed,
"CONNECT tunnel through SOCKS5 should work within 10 attempts"
);
tracker.cleanup_all().await;
target_handle.abort();