use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::time::timeout;
use rsocks::config::Config;
use rsocks::serve;
async fn spawn_echo() -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
while let Ok((mut sock, _)) = listener.accept().await {
tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match sock.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => {
if sock.write_all(&buf[..n]).await.is_err() {
break;
}
}
}
}
});
}
});
addr
}
fn proxy_config() -> Config {
Config {
listen_interface: None,
endpoint_interface: None,
port: 0,
buffer_size: 2048,
read_timeout: 60_000,
accept_cidr: "0.0.0.0/0".to_owned(),
username: None,
password: None,
}
}
async fn spawn_proxy_with(config: Config) -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let _ = serve(listener, config).await;
});
addr
}
async fn spawn_proxy() -> SocketAddr {
spawn_proxy_with(proxy_config()).await
}
#[tokio::test]
async fn socks5_connect_pumps_data_end_to_end() {
let echo_addr = spawn_echo().await;
let proxy_addr = spawn_proxy().await;
let body = async {
let mut client = TcpStream::connect(proxy_addr).await.unwrap();
client.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
let mut method = [0u8; 2];
client.read_exact(&mut method).await.unwrap();
assert_eq!(method, [0x05, 0x00], "server should select NO_AUTH");
let ip = match echo_addr.ip() {
IpAddr::V4(v4) => v4.octets(),
IpAddr::V6(_) => unreachable!("loopback bind is v4"),
};
let port = echo_addr.port();
let request = [0x05, 0x01, 0x00, 0x01, ip[0], ip[1], ip[2], ip[3], (port >> 8) as u8, (port & 0xff) as u8];
client.write_all(&request).await.unwrap();
let mut reply = [0u8; 10];
client.read_exact(&mut reply).await.unwrap();
assert_eq!(reply[0], 0x05, "reply version");
assert_eq!(reply[1], 0x00, "SOCKS CONNECT should succeed");
let payload = b"hello rusty_socks";
client.write_all(payload).await.unwrap();
let mut echoed = [0u8; 17];
client.read_exact(&mut echoed).await.unwrap();
assert_eq!(&echoed, payload, "payload should round-trip through the proxy");
};
timeout(Duration::from_secs(5), body).await.expect("end-to-end flow should complete within 5s");
}
#[tokio::test]
async fn socks5_userpass_auth_succeeds_then_pumps_data() {
let echo_addr = spawn_echo().await;
let proxy_addr = spawn_proxy_with(Config {
username: Some("bob".to_owned()),
password: Some("s3cret".to_owned()),
..proxy_config()
})
.await;
let body = async {
let mut client = TcpStream::connect(proxy_addr).await.unwrap();
client.write_all(&[0x05, 0x02, 0x00, 0x02]).await.unwrap();
let mut method = [0u8; 2];
client.read_exact(&mut method).await.unwrap();
assert_eq!(method, [0x05, 0x02], "server should select USER_PASS");
client.write_all(&[0x01, 0x03, b'b', b'o', b'b', 0x06, b's', b'3', b'c', b'r', b'e', b't']).await.unwrap();
let mut status = [0u8; 2];
client.read_exact(&mut status).await.unwrap();
assert_eq!(status, [0x01, 0x00], "server should accept correct credentials");
let ip = match echo_addr.ip() {
IpAddr::V4(v4) => v4.octets(),
IpAddr::V6(_) => unreachable!("loopback bind is v4"),
};
let port = echo_addr.port();
let request = [0x05, 0x01, 0x00, 0x01, ip[0], ip[1], ip[2], ip[3], (port >> 8) as u8, (port & 0xff) as u8];
client.write_all(&request).await.unwrap();
let mut reply = [0u8; 10];
client.read_exact(&mut reply).await.unwrap();
assert_eq!(reply[1], 0x00, "CONNECT should succeed after auth");
let payload = b"hello rusty_socks";
client.write_all(payload).await.unwrap();
let mut echoed = [0u8; 17];
client.read_exact(&mut echoed).await.unwrap();
assert_eq!(&echoed, payload, "payload should round-trip after auth");
};
timeout(Duration::from_secs(5), body).await.expect("authenticated flow should complete within 5s");
}
#[tokio::test]
async fn socks5_userpass_auth_rejects_bad_credentials() {
let proxy_addr = spawn_proxy_with(Config {
username: Some("bob".to_owned()),
password: Some("s3cret".to_owned()),
..proxy_config()
})
.await;
let body = async {
let mut client = TcpStream::connect(proxy_addr).await.unwrap();
client.write_all(&[0x05, 0x02, 0x00, 0x02]).await.unwrap();
let mut method = [0u8; 2];
client.read_exact(&mut method).await.unwrap();
assert_eq!(method, [0x05, 0x02]);
client.write_all(&[0x01, 0x03, b'b', b'o', b'b', 0x05, b'w', b'r', b'o', b'n', b'g']).await.unwrap();
let mut status = [0u8; 2];
client.read_exact(&mut status).await.unwrap();
assert_eq!(status, [0x01, 0x01], "server should reject bad credentials with a non-zero status");
let n = client.read(&mut [0u8; 1]).await.unwrap();
assert_eq!(n, 0, "server should close the connection after auth failure");
};
timeout(Duration::from_secs(5), body).await.expect("rejection flow should complete within 5s");
}
#[tokio::test]
async fn socks5_rejects_client_not_offering_user_pass() {
let proxy_addr = spawn_proxy_with(Config {
username: Some("bob".to_owned()),
password: Some("s3cret".to_owned()),
..proxy_config()
})
.await;
let body = async {
let mut client = TcpStream::connect(proxy_addr).await.unwrap();
client.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
let mut method = [0u8; 2];
client.read_exact(&mut method).await.unwrap();
assert_eq!(method, [0x05, 0xFF], "server should reply NO ACCEPTABLE METHODS");
let n = client.read(&mut [0u8; 1]).await.unwrap();
assert_eq!(n, 0, "server should close the connection after rejecting methods");
};
timeout(Duration::from_secs(5), body).await.expect("method-rejection flow should complete within 5s");
}