Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add docker test for current protocol(vmess, ss, trojan) #324

Merged
merged 25 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b395058
feat: add docker test for current protocol(vmess, ss, trojan)
VendettaReborn Mar 17, 2024
051b460
fix opt.host in http2 config (#323)
VendettaReborn Mar 17, 2024
6f7d131
Bump clap from 4.5.2 to 4.5.3 (#330)
dependabot[bot] Mar 18, 2024
1053cad
Bump h2 from 0.4.2 to 0.4.3 (#328)
dependabot[bot] Mar 18, 2024
35dcef5
Bump async-trait from 0.1.77 to 0.1.78 (#329)
dependabot[bot] Mar 18, 2024
f36331c
Bump serde_yaml from 0.9.32 to 0.9.33 (#326)
dependabot[bot] Mar 18, 2024
0e71cba
Bump brotli from 3.4.0 to 3.5.0 (#327)
dependabot[bot] Mar 18, 2024
931d0c9
Bump async-recursion from 1.0.5 to 1.1.0 (#325)
dependabot[bot] Mar 18, 2024
3e60e5a
optimize docker test structure; use serial test to make sure the corr…
VendettaReborn Mar 18, 2024
8af53f1
move println to tracing
VendettaReborn Mar 18, 2024
90a5b6e
rename; add doc
VendettaReborn Mar 18, 2024
4a692b9
simplify DockerTestRunner type signature
VendettaReborn Mar 19, 2024
675d1f8
simplify DockerTestRunner, remove useless exec function
VendettaReborn Mar 19, 2024
b6a4a6d
fix format error
VendettaReborn Mar 19, 2024
4255805
exclude macos from the docker test's supported archs
VendettaReborn Mar 19, 2024
7911d72
use build flag to gate tests
ibigbug Mar 21, 2024
c91162d
fix ci error; simplify the docker test code, add timeoutlogic
VendettaReborn Mar 22, 2024
958940b
Bump uuid from 1.7.0 to 1.8.0 (#331)
dependabot[bot] Mar 19, 2024
6ff61be
Update README.md
ibigbug Mar 19, 2024
e03ba8c
gix style warnings
VendettaReborn Mar 23, 2024
cdd1875
error propgating
ibigbug Mar 26, 2024
d5ef72f
Merge branch 'master' into feat/docker_test
ibigbug Mar 26, 2024
39d7b4c
fix env variable in github action
VendettaReborn Mar 28, 2024
ebdb619
fix ci error
VendettaReborn Mar 28, 2024
5d18edb
Update ci.yml
ibigbug Mar 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions clash_lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ tokio-test = "0.4.4"
axum-macros = "0.4.0"
bollard = "0.16"
serial_test = "3.0.0"
tracing-test = "0.2.4"

[target.'cfg(macos)'.dependencies]
security-framework = "2.8.0"
3 changes: 3 additions & 0 deletions clash_lib/src/proxy/trojan/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ mod tests {

use std::collections::HashMap;

use tracing_test::traced_test;

use crate::proxy::utils::test_utils::{
config_helper::test_config_base_dir,
consts::*,
Expand Down Expand Up @@ -252,6 +254,7 @@ mod tests {
}

#[tokio::test]
#[traced_test]
#[serial_test::serial]
async fn test_trojan_ws() -> anyhow::Result<()> {
let opts = Opts {
Expand Down
5 changes: 3 additions & 2 deletions clash_lib/src/proxy/utils/test_utils/docker_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use bollard::image::CreateImageOptions;
use anyhow::Result;
use futures::{Future, TryStreamExt};

const TIMEOUT_DURATION: u64 = 3;
const TIMEOUT_DURATION: u64 = 30;

pub struct DockerTestRunner {
instance: Docker,
Expand Down Expand Up @@ -53,9 +53,10 @@ impl DockerTestRunner {
},
_ = tokio::time::sleep(std::time::Duration::from_secs(TIMEOUT_DURATION))=> {
tracing::warn!("timeout");
Ok(())
Err(anyhow::anyhow!("timeout"))
}
};

self.cleanup().await?;

res
Expand Down
111 changes: 67 additions & 44 deletions clash_lib/src/proxy/utils/test_utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
use std::sync::Arc;
use std::{
sync::Arc,
time::{Duration, Instant},
};

use crate::{
app::dispatcher::ChainedStream,
proxy::OutboundHandler,
session::{Session, SocksAddr},
};
use futures::{future::join_all, Future};
use futures::{future::select_all, Future};
use tokio::{
io::{split, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
net::TcpListener,
};
use tracing::{debug, error};
use tracing::info;

use self::docker_runner::DockerTestRunner;

Expand All @@ -33,6 +36,8 @@ pub async fn ping_pong_test(handler: Arc<dyn OutboundHandler>, port: u16) -> any

let listener = TcpListener::bind(format!("0.0.0.0:{}", port).as_str()).await?;

info!("target local server started at: {}", listener.local_addr()?);

async fn destination_fn<T>(incoming: T) -> anyhow::Result<()>
where
T: AsyncRead + AsyncWrite,
Expand All @@ -42,26 +47,30 @@ pub async fn ping_pong_test(handler: Arc<dyn OutboundHandler>, port: u16) -> any
let chunk = "world";
let mut buf = vec![0; 5];

tracing::info!("destination_fn start read");

for _ in 0..100 {
read_half.read_exact(&mut buf).await?;
assert_eq!(&buf, b"hello");
}

tracing::info!("destination_fn start write");

for _ in 0..100 {
write_half.write_all(chunk.as_bytes()).await?;
write_half.flush().await?;
}

tracing::info!("destination_fn end");
Ok(())
}

let target_local_server_handler = tokio::spawn(async move {
match listener.accept().await {
Ok((stream, _)) => destination_fn(stream).await,
Err(e) => {
// Handle error e, log it, or ignore it
error!("Failed to accept connection: {}", e);
Err(anyhow!("Failed to accept connection: {}", e))
}
loop {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VendettaReborn this should be a loop

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for our case it might be fine as we only have 1 conn.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was actually failing for me on macos as the proxy server inside container can't connect to the listener outside the vm on the host mac machine

let (stream, _) = listener.accept().await?;

tracing::info!("Accepted connection from: {}", stream.peer_addr().unwrap());
destination_fn(stream).await?
}
});

Expand All @@ -71,44 +80,48 @@ pub async fn ping_pong_test(handler: Arc<dyn OutboundHandler>, port: u16) -> any
let chunk = "hello";
let mut buf = vec![0; 5];

tracing::info!("proxy_fn start write");

for _ in 0..100 {
write_half.write_all(chunk.as_bytes()).await?;
write_half
.write_all(chunk.as_bytes())
.await
.inspect_err(|x| {
tracing::error!("proxy_fn write error: {}", x);
})?;
}
write_half.flush().await?;
drop(write_half);

tracing::info!("proxy_fn start read");

for _ in 0..100 {
read_half.read_exact(&mut buf).await?;
read_half.read_exact(&mut buf).await.inspect_err(|x| {
tracing::error!("proxy_fn read error: {}", x);
})?;
assert_eq!(buf, "world".as_bytes().to_owned());
}
drop(read_half);

tracing::info!("proxy_fn end");

Ok(())
}

let proxy_task = tokio::spawn(async move {
// give some time for the target local server to start
tokio::time::sleep(Duration::from_secs(3)).await;

match handler.connect_stream(&sess, resolver).await {
Ok(stream) => proxy_fn(stream).await,
Err(e) => {
error!("Failed to accept connection: {}", e);
Err(anyhow!("Failed to accept connection: {}", e))
tracing::error!("Failed to proxy connection: {}", e);
Err(anyhow!("Failed to proxy connection: {}", e))
}
}
});

let futs = vec![proxy_task, target_local_server_handler];

match join_all(futs)
.await
.into_iter()
.filter_map(|x| x.err())
.next()
{
Some(e) => Err(anyhow!("Failed to run ping_pong_test: {}", e)),
None => {
tracing::info!("ping_pong_test success");
Ok(())
}
}
select_all(futs).await.0?
}

/// Represents the options for a latency test.
Expand All @@ -127,7 +140,7 @@ pub struct LatencyTestOption<'a> {
pub async fn latency_test(
handler: Arc<dyn OutboundHandler>,
option: LatencyTestOption<'_>,
) -> anyhow::Result<()> {
) -> anyhow::Result<Duration> {
// our proxy handler -> proxy-server -> destination(google.com)

let sess = Session {
Expand All @@ -145,25 +158,22 @@ pub async fn latency_test(
write_half.flush().await?;
drop(write_half);

let start_time = std::time::SystemTime::now();
let start_time = Instant::now();
let mut response = vec![0; option.expected_resp.len()];

if option.read_exact {
read_half.read_exact(&mut response).await?;
debug!("response:\n{}", String::from_utf8_lossy(&response));
tracing::debug!("response:\n{}", String::from_utf8_lossy(&response));
assert_eq!(&response, option.expected_resp);
} else {
read_half.read_to_end(&mut response).await?;
debug!("response:\n{}", String::from_utf8_lossy(&response));
tracing::debug!("response:\n{}", String::from_utf8_lossy(&response));
assert_eq!(&response, option.expected_resp);
}

let end_time = std::time::SystemTime::now();
debug!(
"time cost:{:?}",
end_time.duration_since(start_time).unwrap()
);
Ok(())
let end_time = Instant::now();
tracing::debug!("time cost:{:?}", end_time.duration_since(start_time));
Ok(end_time.duration_since(start_time))
}

pub async fn run(
Expand All @@ -172,16 +182,23 @@ pub async fn run(
) -> anyhow::Result<()> {
let watch = match runner_creater.await {
Ok(runner) => runner,
Err(_) => {
Err(e) => {
tracing::warn!("cannot start container, please check the docker environment");
return Ok(());
return Err(e);
}
};

watch
.run_and_cleanup(async move {
ping_pong_test(handler.clone(), 10001).await?;
latency_test(
let rv = ping_pong_test(handler.clone(), 10001).await;
if rv.is_err() {
tracing::error!("ping_pong_test failed: {:?}", rv);
return rv;
} else {
tracing::info!("ping_pong_test success");
}

let rv = latency_test(
handler,
LatencyTestOption {
dst: SocksAddr::Domain("example.com".to_owned(), 80),
Expand All @@ -190,8 +207,14 @@ pub async fn run(
read_exact: true,
},
)
.await?;
Ok(())
.await;
if rv.is_err() {
return Err(rv.unwrap_err());
} else {
tracing::info!("latency test success: {}", rv.unwrap().as_millis());
}

return Ok(());
})
.await
}
9 changes: 7 additions & 2 deletions clash_lib/src/proxy/vmess/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{collections::HashMap, io, net::IpAddr, sync::Arc};

use async_trait::async_trait;
use futures::TryFutureExt;
use tracing::debug;

mod vmess_impl;

Expand Down Expand Up @@ -168,6 +169,7 @@ impl OutboundHandler for Handler {
sess: &Session,
resolver: ThreadSafeDNSResolver,
) -> io::Result<BoxedChainedStream> {
debug!("Connecting to {} via VMess", sess);
let stream = new_tcp_stream(
resolver,
self.opts.server.as_str(),
Expand Down Expand Up @@ -254,6 +256,8 @@ impl OutboundHandler for Handler {
#[cfg(all(test, not(ci)))]
mod tests {

use tracing_test::traced_test;

use crate::proxy::utils::test_utils::{
config_helper::test_config_base_dir,
consts::*,
Expand All @@ -265,16 +269,17 @@ mod tests {

async fn get_ws_runner() -> anyhow::Result<DockerTestRunner> {
let test_config_dir = test_config_base_dir();
let trojan_conf = test_config_dir.join("vmess-ws.json");
let vmess_ws_conf = test_config_dir.join("vmess-ws.json");

DockerTestRunnerBuilder::new()
.image(IMAGE_VMESS)
.mounts(&[(trojan_conf.to_str().unwrap(), "/etc/v2ray/config.json")])
.mounts(&[(vmess_ws_conf.to_str().unwrap(), "/etc/v2ray/config.json")])
.build()
.await
}

#[tokio::test]
#[traced_test]
#[serial_test::serial]
async fn test_vmess_ws() -> anyhow::Result<()> {
let opts = HandlerOptions {
Expand Down