Skip to content

refactor: remove docker requirement of tuic#1345

Open
Itsusinn wants to merge 5 commits intomasterfrom
test/remove-docker-of-tuic
Open

refactor: remove docker requirement of tuic#1345
Itsusinn wants to merge 5 commits intomasterfrom
test/remove-docker-of-tuic

Conversation

@Itsusinn
Copy link
Copy Markdown
Member

@Itsusinn Itsusinn commented Apr 27, 2026

Summary by CodeRabbit

  • Tests

    • Added reliable local TUIC integration tests: an end-to-end echo test and an authentication-failure test; introduced a reusable helper to run a local TUIC test server.
  • Chores

    • Updated test/development dependencies and added new dev tooling.
    • Removed Docker-based TUIC test references and simplified CI coverage provisioning.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Adds an in-process TUIC test server and test utilities, moves TUIC tests off Docker to local TCP tests, bumps tuic-core dependency, adds dev-dependencies (moka, tuic-server), removes a TUIC Docker image constant, and removes NASM/Protoc install steps from the coverage workflow.

Changes

Cohort / File(s) Summary
Dependency updates
clash-lib/Cargo.toml
Bumped tuic-core git dependency tag from v1.6.7 to v1.7.2; added dev-dependencies moka = { version = "0.12", features = ["future"] } and tuic-server from git = "https://github.com/Itsusinn/tuic", tag = "v1.7.2".
TUIC test integration
clash-lib/src/proxy/tuic/mod.rs
Removed Docker-gated test harness; exposed #[cfg(test)] pub(crate) mod test_utils;, simplified test config (connect to 127.0.0.1, skip_cert_verify = true, SNI "localhost"), and added two TCP-focused integration tests (echo ping-pong and auth-failure).
TUIC test helper implementation
clash-lib/src/proxy/tuic/test_utils.rs
New TuicServerProcess test helper: selects free UDP port, builds tuic_server::Config (test credentials, TLS with hostname localhost, ALPN h3), spawns server in a Tokio task, signals readiness via oneshot, exposes start() and port(), and aborts the task on Drop.
Docker utils cleanup
clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs
Removed IMAGE_TUIC constant and its tuic feature-gated block.
CI coverage workflow
.github/workflows/coverage.yml
Removed installation steps for NASM and Protoc; retained coverage tooling and upload steps.

Sequence Diagram

sequenceDiagram
    participant Test as Test Code
    participant TSP as TuicServerProcess
    participant Task as Tokio Task
    participant Server as tuic-server
    participant Channel as Oneshot Channel

    Test->>TSP: start()
    activate TSP

    TSP->>TSP: bind UDP to find free port
    TSP->>TSP: construct tuic_server::Config (creds, TLS, ALPN)
    TSP->>Task: spawn async server task with config
    activate Task

    Task->>Server: Server::init(ctx) on selected port
    activate Server

    Server->>Channel: send readiness (oneshot)
    deactivate Server
    deactivate Task

    TSP->>Channel: await readiness (30s timeout)
    Channel-->>TSP: readiness received
    TSP-->>Test: return TuicServerProcess(handle, port)
    deactivate TSP
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

dependencies, rust

Suggested reviewers

  • ibigbug

Poem

🐰 I hopped a port and spun a server bright,
No Docker shell — just local light.
Echoes bounced and auth was tried,
Tests ran safe with TLS beside.
A happy rabbit, testing tonight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main objective: removing Docker dependency from TUIC tests and infrastructure.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/remove-docker-of-tuic

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@clash-lib/src/proxy/tuic/mod.rs`:
- Around line 533-559: The test currently sets session.destination to
"127.0.0.1:9999" (where nothing listens) so failures can come from an
unreachable upstream; instead spawn a real local TcpListener and use its bound
address as the session.destination so the upstream is reachable, then proceed to
call handler.connect_stream(&session, resolver).await, accept the incoming
connection on the listener if any, and assert that the TUIC stream is
rejected/closed (i.e., that stream.write_all or stream.read_exact returns Err)
before any successful proxy I/O; update the code around Session construction and
the connect_stream/assertion to create the listener (e.g.,
tokio::net::TcpListener::bind("127.0.0.1:0").await), set destination from
listener.local_addr(), optionally spawn a task to accept the upstream
connection, and ensure the test cleans up the listener/task after the assertion.

In `@clash-lib/src/proxy/tuic/test_utils.rs`:
- Around line 14-15: The current alloc_port() implementation picks a free port
by creating and immediately dropping a temporary UdpSocket, introducing a TOCTOU
race when tuic-server later binds; change alloc_port() to bind a socket to 0
(e.g., UdpSocket::bind("127.0.0.1:0") or TcpListener::bind("127.0.0.1:0")), read
the actual assigned port from socket.local_addr().port(), and return that port
(keeping the socket alive until the server is spawned if necessary), or
alternatively modify the tuic-server startup path to accept port 0 and read the
bound port from the listener it creates; if keeping the original approach, add a
bounded retry loop around server bind (checking for EADDRINUSE) to retry a few
times before failing to avoid flakiness when alloc_port() selection races with
other processes.
- Around line 73-93: The test helper currently treats Server::init errors as
readiness by always sending on ready_tx and then ignoring the channel result;
change the ready signalling to propagate init failures: make ready_tx/ready_rx
send a Result<(), anyhow::Error> (or equivalent) instead of unit, and on Err(e)
from tuic_server::server::Server::init send Err(e) (or forward the error) rather
than sending Ok(()) so the caller can observe the failure; then when awaiting
the timeout on ready_rx, propagate the received Err to the caller (using ? or
map_err) instead of calling .ok(), so start()/the test returns Err when
initialization failed and does not report readiness for a dead server.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 098d516d-f27a-4929-b891-74be75e6d082

📥 Commits

Reviewing files that changed from the base of the PR and between 499e24e and cababdc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • clash-lib/Cargo.toml
  • clash-lib/src/proxy/tuic/mod.rs
  • clash-lib/src/proxy/tuic/test_utils.rs
  • clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs
💤 Files with no reviewable changes (1)
  • clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs

Comment thread clash-lib/src/proxy/tuic/mod.rs
Comment thread clash-lib/src/proxy/tuic/test_utils.rs Outdated
Comment on lines +14 to +15
let port = alloc_port();
let server_addr: SocketAddr = format!("127.0.0.1:{port}").parse()?;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 27, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid the free-port race in alloc_port().

Picking a port with a temporary UdpSocket, dropping it, and then asking tuic-server to bind that same port is a TOCTOU race. Under parallel test execution another process can claim the port between Lines 124 and 73, which turns this helper flaky.

Use a bind-to-:0 flow that returns the actual bound port, or add a retry loop around startup when bind fails with address-in-use.

Also applies to: 120-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clash-lib/src/proxy/tuic/test_utils.rs` around lines 14 - 15, The current
alloc_port() implementation picks a free port by creating and immediately
dropping a temporary UdpSocket, introducing a TOCTOU race when tuic-server later
binds; change alloc_port() to bind a socket to 0 (e.g.,
UdpSocket::bind("127.0.0.1:0") or TcpListener::bind("127.0.0.1:0")), read the
actual assigned port from socket.local_addr().port(), and return that port
(keeping the socket alive until the server is spawned if necessary), or
alternatively modify the tuic-server startup path to accept port 0 and read the
bound port from the listener it creates; if keeping the original approach, add a
bounded retry loop around server bind (checking for EADDRINUSE) to retry a few
times before failing to avoid flakiness when alloc_port() selection races with
other processes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Let me modify tuic-server to fix this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Itsusinn, sounds good! The cleanest approach on the tuic-server side would be:

  1. Allow binding to port 0 in the listener setup so the OS assigns a free port atomically.
  2. Expose the actual bound port back to the caller — for example, by sending it over the readiness oneshot channel (changing its type from () to u16) or storing it in a shared Arc<AtomicU16> that the server writes before signalling readiness.

Then on the test_utils.rs side, the flow becomes:

// Instead of alloc_port(), just pass 0:
let server_addr: SocketAddr = "127.0.0.1:0".parse()?;

// After the ready signal, receive the actual bound port:
let port = ready_rx.await?; // if oneshot carries u16

This completely eliminates the TOCTOU window since the port is never released between selection and bind. Let me know if you'd like a more detailed sketch of the tuic-server-side changes once you start working on it!


🧠 Learnings used
Learnt from: CR
Repo: Watfaq/clash-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-19T07:48:02.063Z
Learning: Applies to **/*.rs : Use conditional compilation with feature flags for different protocols (shadowsocks, tuic, ssh, onion, shadowquic, tokio-console, bench)

Comment thread clash-lib/src/proxy/tuic/test_utils.rs Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

❌ Patch coverage is 78.77095% with 38 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
clash-lib/src/proxy/tuic/mod.rs 69.13% 5 Missing and 20 partials ⚠️
clash-lib/src/proxy/tuic/test_utils.rs 86.73% 9 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

📊 Proxy Throughput Results

⚠️ No results file found — tests may have failed before producing output.

Full test log

Download the throughput-results artifact for the full log.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/coverage.yml:
- Around line 36-39: The coverage workflow removed explicit NASM provisioning
but README.md/CLAUDE.md still declare NASM required for Windows builds; restore
an explicit NASM install step in .github/workflows/coverage.yml (near the
"Install cargo-llvm-cov" step) that runs only for the windows-latest job, or
alternatively update the docs to state that GitHub runners now include NASM;
locate the relevant job in coverage.yml around the "Install cargo-llvm-cov" step
and either insert a Windows-only NASM install action (e.g., choco/winget
installation) before build/coverage steps or change README.md/CLAUDE.md to
reflect the runner provisioning so the declared requirements match the workflow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d4d0947-98f7-4d56-8cbd-a1bc2d990bed

📥 Commits

Reviewing files that changed from the base of the PR and between cababdc and 0d8ecde.

📒 Files selected for processing (1)
  • .github/workflows/coverage.yml

Comment thread .github/workflows/coverage.yml
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
clash-lib/src/proxy/tuic/test_utils.rs (1)

60-74: Consider combining duplicate iteration patterns.

Both online_counter and traffic_stats iterate over cfg.users with identical structure. A single loop would be cleaner.

♻️ Suggested refactor
-        let mut online_counter = HashMap::new();
-        for (user, _) in cfg.users.iter() {
-            online_counter
-                .insert(user.to_owned(), std::sync::atomic::AtomicUsize::new(0));
-        }
-        let mut traffic_stats = HashMap::new();
-        for (user, _) in cfg.users.iter() {
-            traffic_stats.insert(
-                user.to_owned(),
-                (
-                    std::sync::atomic::AtomicUsize::new(0),
-                    std::sync::atomic::AtomicUsize::new(0),
-                ),
-            );
-        }
+        let (online_counter, traffic_stats) = cfg
+            .users
+            .keys()
+            .map(|user| {
+                let counter = (user.to_owned(), std::sync::atomic::AtomicUsize::new(0));
+                let stats = (
+                    user.to_owned(),
+                    (
+                        std::sync::atomic::AtomicUsize::new(0),
+                        std::sync::atomic::AtomicUsize::new(0),
+                    ),
+                );
+                (counter, stats)
+            })
+            .unzip();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clash-lib/src/proxy/tuic/test_utils.rs` around lines 60 - 74, The code
duplicates iteration over cfg.users to populate online_counter and
traffic_stats; refactor by using a single loop over cfg.users and within that
loop insert both online_counter.insert(user.to_owned(), AtomicUsize::new(0)) and
traffic_stats.insert(user.to_owned(), (AtomicUsize::new(0),
AtomicUsize::new(0))); reference the existing symbols online_counter,
traffic_stats, and cfg.users and keep the same to_owned()/AtomicUsize::new(0)
semantics to preserve behavior.
clash-lib/src/proxy/tuic/mod.rs (1)

564-575: Arbitrary sleep makes test potentially slow and flaky.

The 1-second sleep at line 567 is a fixed delay hoping the server processes auth rejection. This could be too short under load or unnecessarily slow in normal conditions.

Consider polling with a short timeout in a loop instead:

♻️ Suggested approach
         if let Ok(mut stream) = result {
             let mut buf = [0u8; 5];
-            // Give the server time to process auth and close
-            tokio::time::sleep(Duration::from_secs(1)).await;
-            let write_result = stream.write_all(b"hello").await;
-            let read_result = stream.read_exact(&mut buf).await;
-            assert!(
-                write_result.is_err() || read_result.is_err(),
-                "expected IO error after auth failure, but both read and write \
-                 succeeded"
-            );
+            // Poll for failure with timeout instead of fixed sleep
+            let io_result = tokio::time::timeout(Duration::from_secs(5), async {
+                loop {
+                    if stream.write_all(b"hello").await.is_err() {
+                        return true; // write failed as expected
+                    }
+                    if stream.read_exact(&mut buf).await.is_err() {
+                        return true; // read failed as expected
+                    }
+                    tokio::time::sleep(Duration::from_millis(50)).await;
+                }
+            })
+            .await;
+            assert!(
+                io_result.is_ok(),
+                "expected IO error after auth failure within timeout"
+            );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clash-lib/src/proxy/tuic/mod.rs` around lines 564 - 575, The fixed 1s sleep
before testing stream IO makes the test slow and flaky; replace the
tokio::time::sleep(Duration::from_secs(1)).await in the block that handles
Ok(mut stream) (the variable result/stream used with stream.write_all and
stream.read_exact) with a short polling loop that repeatedly attempts a
non-blocking/read-write probe (call the same stream.write_all and
stream.read_exact) until either you observe the expected IO error or a small
overall timeout elapses (use tokio::time::timeout or loop with
tokio::time::sleep(Duration::from_millis(...)) between probes); fail the test if
the timeout elapses without an IO error and keep the existing assert semantics
once an error is observed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@clash-lib/src/proxy/tuic/mod.rs`:
- Around line 564-575: The fixed 1s sleep before testing stream IO makes the
test slow and flaky; replace the
tokio::time::sleep(Duration::from_secs(1)).await in the block that handles
Ok(mut stream) (the variable result/stream used with stream.write_all and
stream.read_exact) with a short polling loop that repeatedly attempts a
non-blocking/read-write probe (call the same stream.write_all and
stream.read_exact) until either you observe the expected IO error or a small
overall timeout elapses (use tokio::time::timeout or loop with
tokio::time::sleep(Duration::from_millis(...)) between probes); fail the test if
the timeout elapses without an IO error and keep the existing assert semantics
once an error is observed.

In `@clash-lib/src/proxy/tuic/test_utils.rs`:
- Around line 60-74: The code duplicates iteration over cfg.users to populate
online_counter and traffic_stats; refactor by using a single loop over cfg.users
and within that loop insert both online_counter.insert(user.to_owned(),
AtomicUsize::new(0)) and traffic_stats.insert(user.to_owned(),
(AtomicUsize::new(0), AtomicUsize::new(0))); reference the existing symbols
online_counter, traffic_stats, and cfg.users and keep the same
to_owned()/AtomicUsize::new(0) semantics to preserve behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 14c9eb89-3b23-424a-aaa9-350e3f6cd16f

📥 Commits

Reviewing files that changed from the base of the PR and between 0d8ecde and 6898057.

📒 Files selected for processing (2)
  • clash-lib/src/proxy/tuic/mod.rs
  • clash-lib/src/proxy/tuic/test_utils.rs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
clash-lib/Cargo.toml (1)

176-176: Consider adding explicit rev values to TUIC git dependencies for self-contained manifests.

Line 176 and Line 225 specify tag = "v1.7.2" without explicit rev values. While Cargo.lock already pins the exact commit (18b74bcf11fe33caf9dcfc9e2d6685c5230a2e0a), adding rev directly in Cargo.toml makes the dependency specification more transparent and reduces reliance on Cargo.lock for reproducibility. This is especially valuable for supply-chain clarity in projects with frequent dependency updates.

Also applies to: 225-225

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clash-lib/Cargo.toml` at line 176, The TUIC git dependency entries (e.g., the
tuic-core dependency declaration) currently specify tag = "v1.7.2" without an
explicit rev; update each TUIC git dependency in Cargo.toml (the tuic-core entry
at the shown locations and the other TUIC git entry around the later occurrence)
to include rev = "18b74bcf11fe33caf9dcfc9e2d6685c5230a2e0a" (the commit from
Cargo.lock) alongside tag = "v1.7.2" so the manifest is self-contained and
reproducible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@clash-lib/Cargo.toml`:
- Line 176: The TUIC git dependency entries (e.g., the tuic-core dependency
declaration) currently specify tag = "v1.7.2" without an explicit rev; update
each TUIC git dependency in Cargo.toml (the tuic-core entry at the shown
locations and the other TUIC git entry around the later occurrence) to include
rev = "18b74bcf11fe33caf9dcfc9e2d6685c5230a2e0a" (the commit from Cargo.lock)
alongside tag = "v1.7.2" so the manifest is self-contained and reproducible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b936b8f5-8796-4ac1-b55a-af719f3aaeb1

📥 Commits

Reviewing files that changed from the base of the PR and between 6898057 and 0ffaff5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • clash-lib/Cargo.toml

let server = TuicServerProcess::start().await?;
let port = server.port();

// Start a local echo server as the target
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

any chance to have this reuse the existing test suites and echo servers so we don't need reinvent the wheels here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

tcp echo stuff?

@ibigbug
Copy link
Copy Markdown
Member

ibigbug commented Apr 28, 2026

is it overall better to run the server like this than using a docker container? i think docker is doing it pretty well to avoid crafting the server logic manually for each outbounds, don't see much benefits here replacing it

@Itsusinn
Copy link
Copy Markdown
Member Author

is it overall better to run the server like this than using a docker container? i think docker is doing it pretty well to avoid crafting the server logic manually for each outbounds, don't see much benefits here replacing it

It doesn't run on all platform, such as windows. I think using dep could test more cases, not just on linux which is the only platform supplies docker

@ibigbug
Copy link
Copy Markdown
Member

ibigbug commented Apr 28, 2026

ok that's a fair point. we should have macOS support after this #1039

maybe we should enable them all and fix Windows support too if not working already

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants