mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-25 05:40:50 +02:00
refactor: socks5 chaining
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user